@inceptionstack/roundhouse 0.5.2 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/architecture.md +94 -32
- package/package.json +1 -1
- package/src/agents/kiro/kiro-adapter.ts +8 -1
- package/src/agents/pi/message-format.ts +87 -0
- package/src/agents/pi/pi-adapter.ts +9 -72
- package/src/cli/agent-command.ts +210 -0
- package/src/cli/cli.ts +63 -305
- package/src/cli/cron-commands.ts +258 -0
- package/src/cli/cron.ts +26 -267
- package/src/cli/launchd.ts +1 -1
- package/src/cli/service-manager.ts +192 -0
- package/src/cli/setup/args.ts +109 -0
- package/src/cli/setup/flows.ts +273 -0
- package/src/cli/setup/helpers.ts +66 -0
- package/src/cli/setup/index.ts +7 -0
- package/src/cli/setup/runtime.ts +109 -0
- package/src/cli/setup/steps.ts +617 -0
- package/src/cli/setup/types.ts +52 -0
- package/src/cli/setup.ts +79 -1275
- package/src/cli/shell.ts +49 -0
- package/src/cli/systemd.ts +6 -33
- package/src/config.ts +67 -53
- package/src/gateway/attachments.ts +147 -0
- package/src/gateway/commands.ts +371 -0
- package/src/gateway/helpers.ts +104 -0
- package/src/gateway/index.ts +11 -0
- package/src/gateway/streaming.ts +211 -0
- package/src/gateway.ts +212 -763
- package/src/types.ts +14 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cli/cron-commands.ts — Individual cron subcommand handlers
|
|
3
|
+
*
|
|
4
|
+
* Each handler receives store, positional args, and flags.
|
|
5
|
+
* Extracted from the monolithic cmdCron switch statement.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CronStore, validateJobId } from "../cron/store";
|
|
9
|
+
import { isBuiltinJob } from "../cron/helpers";
|
|
10
|
+
import { CronRunner } from "../cron/runner";
|
|
11
|
+
import { validateSchedule } from "../cron/schedule";
|
|
12
|
+
import { validateTemplate } from "../cron/template";
|
|
13
|
+
import { parseDuration } from "../cron/durations";
|
|
14
|
+
import type { CronJobConfig, CronSchedule } from "../cron/types";
|
|
15
|
+
import { DEFAULT_TIMEOUT_MS, DEFAULT_TIMEZONE, VALID_NOTIFY_ON, DEFAULT_RUNS_LIMIT } from "../cron/constants";
|
|
16
|
+
import { formatSchedule, formatRunCounts, formatJobSummary, formatJobDetail, formatRunLine, runStatusIcon, jobEnabledIcon } from "../cron/format";
|
|
17
|
+
|
|
18
|
+
function rejectBuiltin(id: string): void {
|
|
19
|
+
if (isBuiltinJob(id)) {
|
|
20
|
+
console.error(`Job ID "${id}" is reserved for built-in jobs.`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateNotifyOn(value?: string): "always" | "success" | "failure" {
|
|
26
|
+
const v = value ?? "always";
|
|
27
|
+
if (!(VALID_NOTIFY_ON as readonly string[]).includes(v)) {
|
|
28
|
+
console.error(`Invalid --notify-on: "${v}". Use ${VALID_NOTIFY_ON.join(", ")}.`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
return v as "always" | "success" | "failure";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function cronAdd(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
35
|
+
const id = positional[1];
|
|
36
|
+
if (!id) { console.error("Usage: roundhouse cron add <id> --prompt '...' --cron '...' --tz '...'"); process.exit(1); }
|
|
37
|
+
validateJobId(id);
|
|
38
|
+
rejectBuiltin(id);
|
|
39
|
+
|
|
40
|
+
const existing = await store.getJob(id);
|
|
41
|
+
if (existing && !flags.replace) {
|
|
42
|
+
console.error(`Job "${id}" already exists. Use --replace to overwrite.`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const prompt = flags.prompt;
|
|
47
|
+
if (!prompt) { console.error("--prompt is required"); process.exit(1); }
|
|
48
|
+
|
|
49
|
+
const schedCount = [flags.cron, flags.every, flags.at].filter(Boolean).length;
|
|
50
|
+
if (schedCount > 1) { console.error("Specify only one of --cron, --every, or --at"); process.exit(1); }
|
|
51
|
+
let schedule: CronSchedule;
|
|
52
|
+
if (flags.cron) {
|
|
53
|
+
schedule = { type: "cron", cron: flags.cron, tz: flags.tz ?? DEFAULT_TIMEZONE };
|
|
54
|
+
} else if (flags.every) {
|
|
55
|
+
schedule = { type: "interval", every: flags.every };
|
|
56
|
+
} else if (flags.at) {
|
|
57
|
+
schedule = { type: "once", at: flags.at, tz: flags.tz };
|
|
58
|
+
} else {
|
|
59
|
+
console.error("Schedule required: --cron '...', --every '...', or --at '...'");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
validateSchedule(schedule);
|
|
64
|
+
|
|
65
|
+
const vars: Record<string, string> = {};
|
|
66
|
+
if (flags.var) {
|
|
67
|
+
for (const v of flags.var.split(",")) {
|
|
68
|
+
const [k, ...rest] = v.split("=");
|
|
69
|
+
if (k && rest.length) vars[k.trim()] = rest.join("=").trim();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const templateErrors = validateTemplate(prompt, new Set(Object.keys(vars)));
|
|
74
|
+
if (templateErrors.length) {
|
|
75
|
+
console.error("Template errors:");
|
|
76
|
+
templateErrors.forEach((e) => console.error(` ${e}`));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const notify: CronJobConfig["notify"] = {};
|
|
81
|
+
if (flags.telegram) {
|
|
82
|
+
notify.telegram = {
|
|
83
|
+
chatIds: flags.telegram.split(",").map((s) => s.trim()),
|
|
84
|
+
onlyOn: validateNotifyOn(flags["notify-on"]),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const job: CronJobConfig = {
|
|
90
|
+
id,
|
|
91
|
+
enabled: true,
|
|
92
|
+
description: flags.description,
|
|
93
|
+
createdAt: existing?.createdAt ?? now,
|
|
94
|
+
updatedAt: now,
|
|
95
|
+
schedule,
|
|
96
|
+
prompt,
|
|
97
|
+
vars: Object.keys(vars).length ? vars : undefined,
|
|
98
|
+
timeoutMs: flags.timeout ? parseDuration(flags.timeout) : undefined,
|
|
99
|
+
notify: Object.keys(notify).length ? notify : undefined,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
await store.writeJob(job);
|
|
103
|
+
console.log(`✅ Cron job "${id}" ${existing ? "updated" : "created"}.`);
|
|
104
|
+
if (flags.json) console.log(JSON.stringify(job, null, 2));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function cronList(store: CronStore, _positional: string[], flags: Record<string, string>): Promise<void> {
|
|
108
|
+
const jobs = await store.listJobs();
|
|
109
|
+
if (jobs.length === 0) {
|
|
110
|
+
console.log("No cron jobs configured.");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (flags.json) {
|
|
114
|
+
console.log(JSON.stringify(jobs, null, 2));
|
|
115
|
+
} else {
|
|
116
|
+
for (const j of jobs) {
|
|
117
|
+
const state = await store.getState(j.id);
|
|
118
|
+
console.log(` ${formatJobSummary(j, state)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function cronShow(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
124
|
+
const id = positional[1];
|
|
125
|
+
if (!id) { console.error("Usage: roundhouse cron show <id>"); process.exit(1); }
|
|
126
|
+
const job = await store.getJob(id);
|
|
127
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
128
|
+
const state = await store.getState(id);
|
|
129
|
+
const runs = await store.listRuns(id, 5);
|
|
130
|
+
if (flags.json) {
|
|
131
|
+
console.log(JSON.stringify({ job, state, recentRuns: runs }, null, 2));
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`\n${formatJobDetail(job, state, runs)}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function cronTrigger(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
138
|
+
const id = positional[1];
|
|
139
|
+
if (!id) { console.error("Usage: roundhouse cron trigger <id>"); process.exit(1); }
|
|
140
|
+
rejectBuiltin(id);
|
|
141
|
+
const job = await store.getJob(id);
|
|
142
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
143
|
+
console.log(`Triggering ${id}...`);
|
|
144
|
+
const runner = new CronRunner(store);
|
|
145
|
+
const record = await runner.runJob(job, new Date(), "manual");
|
|
146
|
+
console.log(`\nResult: ${record.status} (${record.durationMs}ms)`);
|
|
147
|
+
if (record.responseText) console.log(`\n${record.responseText.slice(0, 2000)}`);
|
|
148
|
+
if (record.error) console.log(`\nError: ${record.error}`);
|
|
149
|
+
process.exit(record.status === "completed" ? 0 : 1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function cronRuns(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
153
|
+
const id = positional[1];
|
|
154
|
+
if (!id) { console.error("Usage: roundhouse cron runs <id>"); process.exit(1); }
|
|
155
|
+
const runs = await store.listRuns(id, parseInt(flags.limit ?? String(DEFAULT_RUNS_LIMIT), 10));
|
|
156
|
+
if (runs.length === 0) {
|
|
157
|
+
console.log(`No runs for ${id}.`);
|
|
158
|
+
} else if (flags.json) {
|
|
159
|
+
console.log(JSON.stringify(runs, null, 2));
|
|
160
|
+
} else {
|
|
161
|
+
for (const r of runs) {
|
|
162
|
+
console.log(` ${formatRunLine(r)} (${r.kind})`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function cronPause(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
168
|
+
const id = positional[1];
|
|
169
|
+
if (!id) { console.error("Usage: roundhouse cron pause <id>"); process.exit(1); }
|
|
170
|
+
rejectBuiltin(id);
|
|
171
|
+
const job = await store.getJob(id);
|
|
172
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
173
|
+
job.enabled = false;
|
|
174
|
+
job.updatedAt = new Date().toISOString();
|
|
175
|
+
await store.writeJob(job);
|
|
176
|
+
console.log(`⏸️ Job "${id}" paused.`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function cronResume(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
180
|
+
const id = positional[1];
|
|
181
|
+
if (!id) { console.error("Usage: roundhouse cron resume <id>"); process.exit(1); }
|
|
182
|
+
rejectBuiltin(id);
|
|
183
|
+
const job = await store.getJob(id);
|
|
184
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
185
|
+
job.enabled = true;
|
|
186
|
+
job.updatedAt = new Date().toISOString();
|
|
187
|
+
await store.writeJob(job);
|
|
188
|
+
console.log(`▶️ Job "${id}" resumed.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function cronEdit(store: CronStore, positional: string[], flags: Record<string, string>): Promise<void> {
|
|
192
|
+
const id = positional[1];
|
|
193
|
+
if (!id) { console.error("Usage: roundhouse cron edit <id> [--prompt '...'] [--cron '...'] ..."); process.exit(1); }
|
|
194
|
+
rejectBuiltin(id);
|
|
195
|
+
const job = await store.getJob(id);
|
|
196
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
197
|
+
|
|
198
|
+
if (flags.prompt) job.prompt = flags.prompt;
|
|
199
|
+
if (flags.description) job.description = flags.description;
|
|
200
|
+
if (flags.timeout) job.timeoutMs = parseDuration(flags.timeout);
|
|
201
|
+
const editSchedCount = [flags.cron, flags.every, flags.at].filter(Boolean).length;
|
|
202
|
+
if (editSchedCount > 1) { console.error("Specify only one of --cron, --every, or --at"); process.exit(1); }
|
|
203
|
+
if (flags.cron) job.schedule = { type: "cron", cron: flags.cron, tz: flags.tz ?? (job.schedule.type === "cron" ? job.schedule.tz : DEFAULT_TIMEZONE) };
|
|
204
|
+
if (flags.every) job.schedule = { type: "interval", every: flags.every };
|
|
205
|
+
if (flags.at) job.schedule = { type: "once", at: flags.at, tz: flags.tz };
|
|
206
|
+
if (flags.telegram) {
|
|
207
|
+
job.notify = { ...job.notify, telegram: { chatIds: flags.telegram.split(",").map((s) => s.trim()), onlyOn: validateNotifyOn(flags["notify-on"]) } };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
validateSchedule(job.schedule);
|
|
211
|
+
const editVars = new Set(Object.keys(job.vars ?? {}));
|
|
212
|
+
const editErrors = validateTemplate(job.prompt, editVars);
|
|
213
|
+
if (editErrors.length) {
|
|
214
|
+
console.error("Template errors:");
|
|
215
|
+
editErrors.forEach((e) => console.error(` ${e}`));
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
job.updatedAt = new Date().toISOString();
|
|
219
|
+
await store.writeJob(job);
|
|
220
|
+
console.log(`✅ Job "${id}" updated.`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function cronDelete(store: CronStore, positional: string[], _flags: Record<string, string>): Promise<void> {
|
|
224
|
+
const id = positional[1];
|
|
225
|
+
if (!id) { console.error("Usage: roundhouse cron delete <id>"); process.exit(1); }
|
|
226
|
+
rejectBuiltin(id);
|
|
227
|
+
const job = await store.getJob(id);
|
|
228
|
+
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
229
|
+
await store.deleteJob(id);
|
|
230
|
+
console.log(`🗑️ Job "${id}" deleted.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function cronHelp(): void {
|
|
234
|
+
console.log(`roundhouse cron <command>
|
|
235
|
+
|
|
236
|
+
Commands:
|
|
237
|
+
add <id> [flags] Create a cron job
|
|
238
|
+
list List all jobs
|
|
239
|
+
show <id> Show job details
|
|
240
|
+
trigger <id> Run job now
|
|
241
|
+
runs <id> Show run history
|
|
242
|
+
edit <id> [flags] Edit a job
|
|
243
|
+
pause <id> Disable a job
|
|
244
|
+
resume <id> Enable a job
|
|
245
|
+
delete <id> Delete a job
|
|
246
|
+
|
|
247
|
+
Flags for add/edit:
|
|
248
|
+
--prompt "..." Prompt template (required for add)
|
|
249
|
+
--cron "..." Cron expression (e.g. "0 8 * * *")
|
|
250
|
+
--every "..." Interval (e.g. "6h")
|
|
251
|
+
--at "..." One-shot time (e.g. "30m" or ISO date)
|
|
252
|
+
--tz "..." Timezone (e.g. "Asia/Jerusalem")
|
|
253
|
+
--telegram "..." Telegram chat IDs (comma-separated)
|
|
254
|
+
--var "k=v,..." Template variables (comma-separated)
|
|
255
|
+
--timeout "..." Timeout (e.g. "30m")
|
|
256
|
+
--description "..." Job description
|
|
257
|
+
--json JSON output`);
|
|
258
|
+
}
|
package/src/cli/cron.ts
CHANGED
|
@@ -1,32 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cli/cron.ts — roundhouse cron CLI
|
|
2
|
+
* cli/cron.ts — roundhouse cron CLI dispatcher
|
|
3
|
+
*
|
|
4
|
+
* Parses args and dispatches to individual command handlers.
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
|
-
import { CronStore
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import { parseDuration } from "../cron/durations";
|
|
11
|
-
import type { CronJobConfig, CronSchedule } from "../cron/types";
|
|
12
|
-
import { DEFAULT_TIMEOUT_MS, DEFAULT_TIMEZONE, VALID_NOTIFY_ON, DEFAULT_RUNS_LIMIT } from "../cron/constants";
|
|
13
|
-
import { formatSchedule, formatRunCounts, formatJobSummary, formatJobDetail, formatRunLine, runStatusIcon, jobEnabledIcon } from "../cron/format";
|
|
14
|
-
|
|
15
|
-
function rejectBuiltin(id: string): void {
|
|
16
|
-
if (isBuiltinJob(id)) {
|
|
17
|
-
console.error(`Job ID "${id}" is reserved for built-in jobs.`);
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function validateNotifyOn(value?: string): "always" | "success" | "failure" {
|
|
23
|
-
const v = value ?? "always";
|
|
24
|
-
if (!(VALID_NOTIFY_ON as readonly string[]).includes(v)) {
|
|
25
|
-
console.error(`Invalid --notify-on: "${v}". Use ${VALID_NOTIFY_ON.join(", ")}.`);
|
|
26
|
-
process.exit(1);
|
|
27
|
-
}
|
|
28
|
-
return v as "always" | "success" | "failure";
|
|
29
|
-
}
|
|
7
|
+
import { CronStore } from "../cron/store";
|
|
8
|
+
import {
|
|
9
|
+
cronAdd, cronList, cronShow, cronTrigger, cronRuns,
|
|
10
|
+
cronPause, cronResume, cronEdit, cronDelete, cronHelp,
|
|
11
|
+
} from "./cron-commands";
|
|
30
12
|
|
|
31
13
|
function parseArgs(args: string[]): { positional: string[]; flags: Record<string, string> } {
|
|
32
14
|
const positional: string[] = [];
|
|
@@ -44,6 +26,19 @@ function parseArgs(args: string[]): { positional: string[]; flags: Record<string
|
|
|
44
26
|
return { positional, flags };
|
|
45
27
|
}
|
|
46
28
|
|
|
29
|
+
const COMMANDS: Record<string, (store: CronStore, pos: string[], flags: Record<string, string>) => Promise<void>> = {
|
|
30
|
+
add: cronAdd,
|
|
31
|
+
list: cronList,
|
|
32
|
+
show: cronShow,
|
|
33
|
+
trigger: cronTrigger,
|
|
34
|
+
run: cronTrigger,
|
|
35
|
+
runs: cronRuns,
|
|
36
|
+
pause: cronPause,
|
|
37
|
+
resume: cronResume,
|
|
38
|
+
edit: cronEdit,
|
|
39
|
+
delete: cronDelete,
|
|
40
|
+
};
|
|
41
|
+
|
|
47
42
|
export async function cmdCron(args: string[]): Promise<void> {
|
|
48
43
|
const { positional, flags } = parseArgs(args);
|
|
49
44
|
const sub = positional[0];
|
|
@@ -51,246 +46,10 @@ export async function cmdCron(args: string[]): Promise<void> {
|
|
|
51
46
|
const store = new CronStore();
|
|
52
47
|
await store.ensureDirs();
|
|
53
48
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
rejectBuiltin(id);
|
|
60
|
-
|
|
61
|
-
const existing = await store.getJob(id);
|
|
62
|
-
if (existing && !flags.replace) {
|
|
63
|
-
console.error(`Job "${id}" already exists. Use --replace to overwrite.`);
|
|
64
|
-
process.exit(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const prompt = flags.prompt;
|
|
68
|
-
if (!prompt) { console.error("--prompt is required"); process.exit(1); }
|
|
69
|
-
|
|
70
|
-
// Parse schedule — only one allowed
|
|
71
|
-
const schedCount = [flags.cron, flags.every, flags.at].filter(Boolean).length;
|
|
72
|
-
if (schedCount > 1) { console.error("Specify only one of --cron, --every, or --at"); process.exit(1); }
|
|
73
|
-
let schedule: CronSchedule;
|
|
74
|
-
if (flags.cron) {
|
|
75
|
-
schedule = { type: "cron", cron: flags.cron, tz: flags.tz ?? DEFAULT_TIMEZONE };
|
|
76
|
-
} else if (flags.every) {
|
|
77
|
-
schedule = { type: "interval", every: flags.every };
|
|
78
|
-
} else if (flags.at) {
|
|
79
|
-
schedule = { type: "once", at: flags.at, tz: flags.tz };
|
|
80
|
-
} else {
|
|
81
|
-
console.error("Schedule required: --cron '...', --every '...', or --at '...'");
|
|
82
|
-
process.exit(1);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
validateSchedule(schedule);
|
|
86
|
-
|
|
87
|
-
// Parse vars
|
|
88
|
-
const vars: Record<string, string> = {};
|
|
89
|
-
if (flags.var) {
|
|
90
|
-
for (const v of flags.var.split(",")) {
|
|
91
|
-
const [k, ...rest] = v.split("=");
|
|
92
|
-
if (k && rest.length) vars[k.trim()] = rest.join("=").trim();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Validate template
|
|
97
|
-
const templateErrors = validateTemplate(prompt, new Set(Object.keys(vars)));
|
|
98
|
-
if (templateErrors.length) {
|
|
99
|
-
console.error("Template errors:");
|
|
100
|
-
templateErrors.forEach((e) => console.error(` ${e}`));
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Parse notify
|
|
105
|
-
const notify: CronJobConfig["notify"] = {};
|
|
106
|
-
if (flags.telegram) {
|
|
107
|
-
notify.telegram = {
|
|
108
|
-
chatIds: flags.telegram.split(",").map((s) => s.trim()),
|
|
109
|
-
onlyOn: validateNotifyOn(flags["notify-on"]),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const now = new Date().toISOString();
|
|
114
|
-
const job: CronJobConfig = {
|
|
115
|
-
id,
|
|
116
|
-
enabled: true,
|
|
117
|
-
description: flags.description,
|
|
118
|
-
createdAt: existing?.createdAt ?? now,
|
|
119
|
-
updatedAt: now,
|
|
120
|
-
schedule,
|
|
121
|
-
prompt,
|
|
122
|
-
vars: Object.keys(vars).length ? vars : undefined,
|
|
123
|
-
timeoutMs: flags.timeout ? parseDuration(flags.timeout) : undefined,
|
|
124
|
-
notify: Object.keys(notify).length ? notify : undefined,
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
await store.writeJob(job);
|
|
128
|
-
console.log(`✅ Cron job "${id}" ${existing ? "updated" : "created"}.`);
|
|
129
|
-
if (flags.json) console.log(JSON.stringify(job, null, 2));
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
case "list": {
|
|
134
|
-
const jobs = await store.listJobs();
|
|
135
|
-
if (jobs.length === 0) {
|
|
136
|
-
console.log("No cron jobs configured.");
|
|
137
|
-
break;
|
|
138
|
-
}
|
|
139
|
-
if (flags.json) {
|
|
140
|
-
console.log(JSON.stringify(jobs, null, 2));
|
|
141
|
-
} else {
|
|
142
|
-
for (const j of jobs) {
|
|
143
|
-
const state = await store.getState(j.id);
|
|
144
|
-
console.log(` ${formatJobSummary(j, state)}`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
case "show": {
|
|
151
|
-
const id = positional[1];
|
|
152
|
-
if (!id) { console.error("Usage: roundhouse cron show <id>"); process.exit(1); }
|
|
153
|
-
const job = await store.getJob(id);
|
|
154
|
-
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
155
|
-
const state = await store.getState(id);
|
|
156
|
-
const runs = await store.listRuns(id, 5);
|
|
157
|
-
if (flags.json) {
|
|
158
|
-
console.log(JSON.stringify({ job, state, recentRuns: runs }, null, 2));
|
|
159
|
-
} else {
|
|
160
|
-
console.log(`\n${formatJobDetail(job, state, runs)}`);
|
|
161
|
-
}
|
|
162
|
-
break;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
case "trigger":
|
|
166
|
-
case "run": {
|
|
167
|
-
const id = positional[1];
|
|
168
|
-
if (!id) { console.error("Usage: roundhouse cron trigger <id>"); process.exit(1); }
|
|
169
|
-
rejectBuiltin(id);
|
|
170
|
-
const job = await store.getJob(id);
|
|
171
|
-
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
172
|
-
console.log(`Triggering ${id}...`);
|
|
173
|
-
const runner = new CronRunner(store);
|
|
174
|
-
const record = await runner.runJob(job, new Date(), "manual");
|
|
175
|
-
console.log(`\nResult: ${record.status} (${record.durationMs}ms)`);
|
|
176
|
-
if (record.responseText) console.log(`\n${record.responseText.slice(0, 2000)}`);
|
|
177
|
-
if (record.error) console.log(`\nError: ${record.error}`);
|
|
178
|
-
process.exit(record.status === "completed" ? 0 : 1);
|
|
179
|
-
break;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
case "runs": {
|
|
183
|
-
const id = positional[1];
|
|
184
|
-
if (!id) { console.error("Usage: roundhouse cron runs <id>"); process.exit(1); }
|
|
185
|
-
const runs = await store.listRuns(id, parseInt(flags.limit ?? String(DEFAULT_RUNS_LIMIT), 10));
|
|
186
|
-
if (runs.length === 0) {
|
|
187
|
-
console.log(`No runs for ${id}.`);
|
|
188
|
-
} else if (flags.json) {
|
|
189
|
-
console.log(JSON.stringify(runs, null, 2));
|
|
190
|
-
} else {
|
|
191
|
-
for (const r of runs) {
|
|
192
|
-
console.log(` ${formatRunLine(r)} (${r.kind})`);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
case "pause": {
|
|
199
|
-
const id = positional[1];
|
|
200
|
-
if (!id) { console.error("Usage: roundhouse cron pause <id>"); process.exit(1); }
|
|
201
|
-
rejectBuiltin(id);
|
|
202
|
-
const job = await store.getJob(id);
|
|
203
|
-
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
204
|
-
job.enabled = false;
|
|
205
|
-
job.updatedAt = new Date().toISOString();
|
|
206
|
-
await store.writeJob(job);
|
|
207
|
-
console.log(`⏸️ Job "${id}" paused.`);
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
case "resume": {
|
|
212
|
-
const id = positional[1];
|
|
213
|
-
if (!id) { console.error("Usage: roundhouse cron resume <id>"); process.exit(1); }
|
|
214
|
-
rejectBuiltin(id);
|
|
215
|
-
const job = await store.getJob(id);
|
|
216
|
-
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
217
|
-
job.enabled = true;
|
|
218
|
-
job.updatedAt = new Date().toISOString();
|
|
219
|
-
await store.writeJob(job);
|
|
220
|
-
console.log(`▶️ Job "${id}" resumed.`);
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
case "edit": {
|
|
225
|
-
const id = positional[1];
|
|
226
|
-
if (!id) { console.error("Usage: roundhouse cron edit <id> [--prompt '...'] [--cron '...'] ..."); process.exit(1); }
|
|
227
|
-
rejectBuiltin(id);
|
|
228
|
-
const job = await store.getJob(id);
|
|
229
|
-
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
230
|
-
|
|
231
|
-
if (flags.prompt) job.prompt = flags.prompt;
|
|
232
|
-
if (flags.description) job.description = flags.description;
|
|
233
|
-
if (flags.timeout) job.timeoutMs = parseDuration(flags.timeout);
|
|
234
|
-
// Reject multiple schedule flags
|
|
235
|
-
const editSchedCount = [flags.cron, flags.every, flags.at].filter(Boolean).length;
|
|
236
|
-
if (editSchedCount > 1) { console.error("Specify only one of --cron, --every, or --at"); process.exit(1); }
|
|
237
|
-
if (flags.cron) job.schedule = { type: "cron", cron: flags.cron, tz: flags.tz ?? (job.schedule.type === "cron" ? job.schedule.tz : DEFAULT_TIMEZONE) };
|
|
238
|
-
if (flags.every) job.schedule = { type: "interval", every: flags.every };
|
|
239
|
-
if (flags.at) job.schedule = { type: "once", at: flags.at, tz: flags.tz };
|
|
240
|
-
if (flags.telegram) {
|
|
241
|
-
job.notify = { ...job.notify, telegram: { chatIds: flags.telegram.split(",").map((s) => s.trim()), onlyOn: validateNotifyOn(flags["notify-on"]) } };
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
validateSchedule(job.schedule);
|
|
245
|
-
// Validate template after edit
|
|
246
|
-
const editVars = new Set(Object.keys(job.vars ?? {}));
|
|
247
|
-
const editErrors = validateTemplate(job.prompt, editVars);
|
|
248
|
-
if (editErrors.length) {
|
|
249
|
-
console.error("Template errors:");
|
|
250
|
-
editErrors.forEach((e) => console.error(` ${e}`));
|
|
251
|
-
process.exit(1);
|
|
252
|
-
}
|
|
253
|
-
job.updatedAt = new Date().toISOString();
|
|
254
|
-
await store.writeJob(job);
|
|
255
|
-
console.log(`✅ Job "${id}" updated.`);
|
|
256
|
-
break;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
case "delete": {
|
|
260
|
-
const id = positional[1];
|
|
261
|
-
if (!id) { console.error("Usage: roundhouse cron delete <id>"); process.exit(1); }
|
|
262
|
-
rejectBuiltin(id);
|
|
263
|
-
const job = await store.getJob(id);
|
|
264
|
-
if (!job) { console.error(`Job not found: ${id}`); process.exit(1); }
|
|
265
|
-
await store.deleteJob(id);
|
|
266
|
-
console.log(`🗑️ Job "${id}" deleted.`);
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
default:
|
|
271
|
-
console.log(`roundhouse cron <command>
|
|
272
|
-
|
|
273
|
-
Commands:
|
|
274
|
-
add <id> [flags] Create a cron job
|
|
275
|
-
list List all jobs
|
|
276
|
-
show <id> Show job details
|
|
277
|
-
trigger <id> Run job now
|
|
278
|
-
runs <id> Show run history
|
|
279
|
-
edit <id> [flags] Edit a job
|
|
280
|
-
pause <id> Disable a job
|
|
281
|
-
resume <id> Enable a job
|
|
282
|
-
delete <id> Delete a job
|
|
283
|
-
|
|
284
|
-
Flags for add/edit:
|
|
285
|
-
--prompt "..." Prompt template (required for add)
|
|
286
|
-
--cron "..." Cron expression (e.g. "0 8 * * *")
|
|
287
|
-
--every "..." Interval (e.g. "6h")
|
|
288
|
-
--at "..." One-shot time (e.g. "30m" or ISO date)
|
|
289
|
-
--tz "..." Timezone (e.g. "Asia/Jerusalem")
|
|
290
|
-
--telegram "..." Telegram chat IDs (comma-separated)
|
|
291
|
-
--var "k=v,..." Template variables (comma-separated)
|
|
292
|
-
--timeout "..." Timeout (e.g. "30m")
|
|
293
|
-
--description "..." Job description
|
|
294
|
-
--json JSON output`);
|
|
49
|
+
const handler = sub && Object.hasOwn(COMMANDS, sub) ? COMMANDS[sub] : undefined;
|
|
50
|
+
if (handler) {
|
|
51
|
+
await handler(store, positional, flags);
|
|
52
|
+
} else {
|
|
53
|
+
cronHelp();
|
|
295
54
|
}
|
|
296
55
|
}
|
package/src/cli/launchd.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
11
11
|
import { existsSync } from "node:fs";
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
|
-
import { whichSync } from "./
|
|
13
|
+
import { whichSync } from "./shell";
|
|
14
14
|
import { ROUNDHOUSE_DIR } from "../config";
|
|
15
15
|
const __dirname = new URL(".", import.meta.url).pathname.replace(/\/$/, "");
|
|
16
16
|
|