feat: gate tarball install behind --beta=tarball flag (#2482)

* feat: gate tarball install behind --beta=tarball flag

Tarball install is not yet reliable enough to be the default.
Move it behind an opt-in --beta=tarball flag so users can test it
explicitly while live install remains the default path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: support multiple --beta flags (repeatable)

Parse all --beta flags from args in a loop, collecting them into a
comma-separated SPAWN_BETA env var. Consumers check for their feature
with Set.has() so multiple beta features can be active simultaneously.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace for(;;) loop with extractAllFlagValues helper

Cleaner approach: a dedicated helper mutates args in place and returns
all values for a repeatable flag, replacing the infinite loop pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-03-10 21:24:51 -07:00 committed by GitHub
parent e127308af6
commit 9a1dad7fcb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 64 additions and 5 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.16.5",
"version": "0.16.6",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -117,8 +117,9 @@ describe("runOrchestration", () => {
process.env.SPAWN_HOME = testDir;
// Skip GitHub auth prompts during tests
process.env.SPAWN_SKIP_GITHUB_AUTH = "1";
// Ensure no stale SPAWN_ENABLED_STEPS leaks between tests
// Ensure no stale env leaks between tests
delete process.env.SPAWN_ENABLED_STEPS;
delete process.env.SPAWN_BETA;
stderrSpy = spyOn(process.stderr, "write").mockImplementation(() => true);
exitSpy = spyOn(process, "exit").mockImplementation((code) => {
capturedExitCode = isNumber(code) ? code : 0;
@ -524,7 +525,8 @@ describe("runOrchestration", () => {
// ── Tarball install ──────────────────────────────────────────────────
it("attempts tarball install before agent.install on non-local clouds", async () => {
it("attempts tarball install when --beta=tarball is set on non-local clouds", async () => {
process.env.SPAWN_BETA = "tarball";
const install = mock(() => Promise.resolve());
const cloud = createMockCloud({
cloudName: "digitalocean",
@ -543,7 +545,25 @@ describe("runOrchestration", () => {
exitSpy.mockRestore();
});
it("skips tarball install by default (no --beta flag)", async () => {
const install = mock(() => Promise.resolve());
const cloud = createMockCloud({
cloudName: "digitalocean",
});
const agent = createMockAgent({
install,
});
await runOrchestrationSafe(cloud, agent, "testagent");
expect(mockTryTarballInstall).not.toHaveBeenCalled();
expect(install).toHaveBeenCalledTimes(1);
stderrSpy.mockRestore();
exitSpy.mockRestore();
});
it("skips agent.install when tarball succeeds", async () => {
process.env.SPAWN_BETA = "tarball";
mockTryTarballInstall.mockImplementation(() => Promise.resolve(true));
const install = mock(() => Promise.resolve());
const cloud = createMockCloud({
@ -561,7 +581,8 @@ describe("runOrchestration", () => {
exitSpy.mockRestore();
});
it("skips tarball install for local cloud", async () => {
it("skips tarball install for local cloud even with --beta=tarball", async () => {
process.env.SPAWN_BETA = "tarball";
const install = mock(() => Promise.resolve());
const cloud = createMockCloud({
cloudName: "local",
@ -579,6 +600,7 @@ describe("runOrchestration", () => {
});
it("skips tarball install when agent has skipTarball set", async () => {
process.env.SPAWN_BETA = "tarball";
const install = mock(() => Promise.resolve());
const cloud = createMockCloud({
cloudName: "digitalocean",

View file

@ -30,6 +30,7 @@ export const KNOWN_FLAGS = new Set([
"--size",
"--prune",
"--json",
"--beta",
]);
/** Return the first unknown flag in args, or null if all are known/positional */

View file

@ -76,6 +76,23 @@ function extractFlagValue(
];
}
/** Extract all occurrences of a repeatable flag, mutating args in place. */
function extractAllFlagValues(args: string[], flag: string, usageHint: string): string[] {
const values: string[] = [];
let idx = args.indexOf(flag);
while (idx !== -1) {
if (!args[idx + 1] || args[idx + 1].startsWith("-")) {
console.error(pc.red(`Error: ${pc.bold(flag)} requires a value`));
console.error(`\nUsage: ${pc.cyan(usageHint)}`);
process.exit(1);
}
values.push(args[idx + 1]);
args.splice(idx, 2);
idx = args.indexOf(flag);
}
return values;
}
const HELP_FLAGS = [
"--help",
"-h",
@ -100,6 +117,7 @@ function checkUnknownFlags(args: string[]): void {
console.error(` ${pc.cyan("--size, --machine-type")} Set instance size (e.g. e2-standard-4, s-2vcpu-2gb)`);
console.error(` ${pc.cyan("--name")} Set the spawn/resource name`);
console.error(` ${pc.cyan("--reauth")} Force re-prompting for cloud credentials`);
console.error(` ${pc.cyan("--beta tarball")} Use pre-built tarball for agent install (repeatable)`);
console.error(` ${pc.cyan("--help, -h")} Show help information`);
console.error(` ${pc.cyan("--version, -v")} Show version`);
console.error();
@ -761,6 +779,23 @@ async function main(): Promise<void> {
process.env.SPAWN_REAUTH = "1";
}
// Extract all --beta <feature> flags (repeatable, opt-in to experimental features)
const VALID_BETA_FEATURES = new Set([
"tarball",
]);
const betaFeatures = extractAllFlagValues(filteredArgs, "--beta", "spawn <agent> <cloud> --beta tarball");
for (const flag of betaFeatures) {
if (!VALID_BETA_FEATURES.has(flag)) {
console.error(pc.red(`Unknown beta feature: ${pc.bold(flag)}`));
console.error("\nAvailable beta features:");
console.error(` ${pc.cyan("tarball")} Use pre-built tarball for agent installation`);
process.exit(1);
}
}
if (betaFeatures.length > 0) {
process.env.SPAWN_BETA = betaFeatures.join(",");
}
// Extract --output <format> flag
const [outputFormat, outputFilteredArgs] = extractFlagValue(
filteredArgs,

View file

@ -153,7 +153,8 @@ export async function runOrchestration(
logInfo("Snapshot boot — skipping agent install");
} else {
let installedFromTarball = false;
if (cloud.cloudName !== "local" && !agent.skipTarball) {
const betaFeatures = new Set((process.env.SPAWN_BETA ?? "").split(",").filter(Boolean));
if (cloud.cloudName !== "local" && !agent.skipTarball && betaFeatures.has("tarball")) {
const tarball = options?.tryTarball ?? tryTarballInstall;
installedFromTarball = await tarball(cloud.runner, agentName);
}