cli(schedule): require --cwd when --host is set (#685)
process.cwd() is the local path and won't exist on a remote daemon, so fail fast client-side instead of running the schedule in a wrong dir.
This commit is contained in:
@@ -17,6 +17,7 @@ export interface ScheduleCreateOptions extends ScheduleCommandOptions {
|
|||||||
target?: string;
|
target?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
cwd?: string;
|
||||||
maxRuns?: string;
|
maxRuns?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,8 @@ export async function runCreateCommand(
|
|||||||
target: options.target,
|
target: options.target,
|
||||||
provider: options.provider,
|
provider: options.provider,
|
||||||
mode: options.mode,
|
mode: options.mode,
|
||||||
|
cwd: options.cwd,
|
||||||
|
host: options.host,
|
||||||
maxRuns: options.maxRuns,
|
maxRuns: options.maxRuns,
|
||||||
expiresIn: options.expiresIn,
|
expiresIn: options.expiresIn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function createScheduleCommand(): Command {
|
|||||||
"--mode <mode>",
|
"--mode <mode>",
|
||||||
"Provider-specific mode (e.g. claude bypassPermissions, opencode build)",
|
"Provider-specific mode (e.g. claude bypassPermissions, opencode build)",
|
||||||
)
|
)
|
||||||
|
.option("--cwd <path>", "Working directory (default: current; required with --host)")
|
||||||
.option("--max-runs <n>", "Maximum number of runs")
|
.option("--max-runs <n>", "Maximum number of runs")
|
||||||
.option("--expires-in <duration>", "Time to live for the schedule"),
|
.option("--expires-in <duration>", "Time to live for the schedule"),
|
||||||
).action(withOutput(runCreateCommand));
|
).action(withOutput(runCreateCommand));
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
|
import { parseScheduleCreateInput } from "./shared.js";
|
||||||
|
|
||||||
|
const baseOptions = {
|
||||||
|
prompt: "do the thing",
|
||||||
|
every: "5m",
|
||||||
|
provider: "claude",
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("parseScheduleCreateInput cwd/host validation", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(process, "cwd").mockReturnValue("/local/project");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no host, no cwd → defaults to process.cwd()", () => {
|
||||||
|
const input = parseScheduleCreateInput(baseOptions);
|
||||||
|
expect(input.target).toEqual({
|
||||||
|
type: "new-agent",
|
||||||
|
config: { provider: "claude", cwd: "/local/project" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no host, with cwd → uses provided cwd", () => {
|
||||||
|
const input = parseScheduleCreateInput({ ...baseOptions, cwd: "/some/other/path" });
|
||||||
|
expect(input.target).toEqual({
|
||||||
|
type: "new-agent",
|
||||||
|
config: { provider: "claude", cwd: "/some/other/path" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("host with cwd → uses provided cwd", () => {
|
||||||
|
const input = parseScheduleCreateInput({
|
||||||
|
...baseOptions,
|
||||||
|
host: "dev:6767",
|
||||||
|
cwd: "/remote/project",
|
||||||
|
});
|
||||||
|
expect(input.target).toEqual({
|
||||||
|
type: "new-agent",
|
||||||
|
config: { provider: "claude", cwd: "/remote/project" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("host without cwd → throws MISSING_CWD", () => {
|
||||||
|
expect(() => parseScheduleCreateInput({ ...baseOptions, host: "dev:6767" })).toThrow(
|
||||||
|
expect.objectContaining({
|
||||||
|
code: "MISSING_CWD",
|
||||||
|
message: expect.stringContaining("--cwd is required when --host is specified"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("host with whitespace-only cwd → throws MISSING_CWD", () => {
|
||||||
|
expect(() =>
|
||||||
|
parseScheduleCreateInput({ ...baseOptions, host: "dev:6767", cwd: " " }),
|
||||||
|
).toThrow(expect.objectContaining({ code: "MISSING_CWD" }));
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -131,6 +131,8 @@ export function parseScheduleCreateInput(options: {
|
|||||||
target?: string;
|
target?: string;
|
||||||
provider?: string;
|
provider?: string;
|
||||||
mode?: string;
|
mode?: string;
|
||||||
|
cwd?: string;
|
||||||
|
host?: string;
|
||||||
maxRuns?: string;
|
maxRuns?: string;
|
||||||
expiresIn?: string;
|
expiresIn?: string;
|
||||||
}): CreateScheduleInput {
|
}): CreateScheduleInput {
|
||||||
@@ -154,6 +156,15 @@ export function parseScheduleCreateInput(options: {
|
|||||||
? { type: "every", everyMs: parseDuration(options.every) }
|
? { type: "every", everyMs: parseDuration(options.every) }
|
||||||
: { type: "cron", expression: options.cron!.trim() };
|
: { type: "cron", expression: options.cron!.trim() };
|
||||||
|
|
||||||
|
const cwdInput = options.cwd?.trim();
|
||||||
|
if (options.host !== undefined && !cwdInput) {
|
||||||
|
throw {
|
||||||
|
code: "MISSING_CWD",
|
||||||
|
message:
|
||||||
|
"--cwd is required when --host is specified (the local working directory will not exist on the remote daemon)",
|
||||||
|
} satisfies CommandError;
|
||||||
|
}
|
||||||
|
|
||||||
const targetValue = options.target?.trim();
|
const targetValue = options.target?.trim();
|
||||||
const modeId = options.mode?.trim();
|
const modeId = options.mode?.trim();
|
||||||
const hasExplicitNewAgentOption = options.provider !== undefined || options.mode !== undefined;
|
const hasExplicitNewAgentOption = options.provider !== undefined || options.mode !== undefined;
|
||||||
@@ -165,7 +176,7 @@ export function parseScheduleCreateInput(options: {
|
|||||||
type: "new-agent",
|
type: "new-agent",
|
||||||
config: {
|
config: {
|
||||||
provider: resolvedProviderModel.provider,
|
provider: resolvedProviderModel.provider,
|
||||||
cwd: process.cwd(),
|
cwd: cwdInput ?? process.cwd(),
|
||||||
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
...(resolvedProviderModel.model ? { model: resolvedProviderModel.model } : {}),
|
||||||
...(modeId ? { modeId } : {}),
|
...(modeId ? { modeId } : {}),
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user