mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
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:
parent
e127308af6
commit
9a1dad7fcb
5 changed files with 64 additions and 5 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.16.5",
|
||||
"version": "0.16.6",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue