@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.
@@ -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 subcommands
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, validateJobId } from "../cron/store";
6
- import { isBuiltinJob } from "../cron/helpers";
7
- import { CronRunner } from "../cron/runner";
8
- import { validateSchedule } from "../cron/schedule";
9
- import { validateTemplate } from "../cron/template";
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
- switch (sub) {
55
- case "add": {
56
- const id = positional[1];
57
- if (!id) { console.error("Usage: roundhouse cron add <id> --prompt '...' --cron '...' --tz '...'"); process.exit(1); }
58
- validateJobId(id);
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
  }
@@ -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 "./systemd";
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