crontick 0.1.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/dist/chunk-35FFLWP3.js +79 -0
  4. package/dist/chunk-35FFLWP3.js.map +1 -0
  5. package/dist/chunk-FMGZ3SSS.js +57 -0
  6. package/dist/chunk-FMGZ3SSS.js.map +1 -0
  7. package/dist/chunk-LPAFZNXK.js +214 -0
  8. package/dist/chunk-LPAFZNXK.js.map +1 -0
  9. package/dist/chunk-YMAT5MON.js +22 -0
  10. package/dist/chunk-YMAT5MON.js.map +1 -0
  11. package/dist/cli/index.cjs +879 -0
  12. package/dist/cli/index.cjs.map +1 -0
  13. package/dist/cli/index.d.cts +2 -0
  14. package/dist/cli/index.d.ts +2 -0
  15. package/dist/cli/index.js +554 -0
  16. package/dist/cli/index.js.map +1 -0
  17. package/dist/daemon/index.cjs +1655 -0
  18. package/dist/daemon/index.cjs.map +1 -0
  19. package/dist/daemon/index.d.cts +2 -0
  20. package/dist/daemon/index.d.ts +2 -0
  21. package/dist/daemon/index.js +1306 -0
  22. package/dist/daemon/index.js.map +1 -0
  23. package/dist/dashboard/.gitkeep +0 -0
  24. package/dist/dashboard/dashboard.css +192 -0
  25. package/dist/dashboard/dashboard.js +316 -0
  26. package/dist/dashboard/index.html +108 -0
  27. package/dist/index.cjs +180 -0
  28. package/dist/index.cjs.map +1 -0
  29. package/dist/index.d.cts +121 -0
  30. package/dist/index.d.ts +121 -0
  31. package/dist/index.js +32 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/job-JQGLDEJV.js +26 -0
  34. package/dist/job-JQGLDEJV.js.map +1 -0
  35. package/dist/mcp/index.cjs +1000 -0
  36. package/dist/mcp/index.cjs.map +1 -0
  37. package/dist/mcp/index.d.cts +13 -0
  38. package/dist/mcp/index.d.ts +13 -0
  39. package/dist/mcp/index.js +876 -0
  40. package/dist/mcp/index.js.map +1 -0
  41. package/package.json +81 -0
  42. package/plugin/.gitkeep +0 -0
  43. package/plugin/README.md +50 -0
  44. package/plugin/install.mjs +155 -0
  45. package/plugin/plugin.json +11 -0
  46. package/plugin/uninstall.mjs +68 -0
  47. package/src/skill/SKILL.md +227 -0
@@ -0,0 +1,879 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/index.ts
27
+ var import_commander = require("commander");
28
+ var import_node_child_process2 = require("child_process");
29
+ var import_node_fs3 = require("fs");
30
+ var import_node_url = require("url");
31
+ var import_node_path3 = require("path");
32
+ var import_node_readline = require("readline");
33
+
34
+ // src/version.ts
35
+ var VERSION = "0.1.0";
36
+
37
+ // src/paths.ts
38
+ var import_env_paths = __toESM(require("env-paths"), 1);
39
+ var import_node_fs = require("fs");
40
+ var import_node_path = require("path");
41
+ function root() {
42
+ const override = process.env["CRONTICK_HOME"];
43
+ if (override) return override;
44
+ return (0, import_env_paths.default)("crontick", { suffix: "" }).data;
45
+ }
46
+ function dataDir() {
47
+ return root();
48
+ }
49
+ function jobsDir() {
50
+ return (0, import_node_path.join)(root(), "jobs");
51
+ }
52
+ function logsDir() {
53
+ return (0, import_node_path.join)(root(), "logs");
54
+ }
55
+ function pidFilePath() {
56
+ return (0, import_node_path.join)(root(), "daemon.pid");
57
+ }
58
+ function portFilePath() {
59
+ return (0, import_node_path.join)(root(), "daemon.port");
60
+ }
61
+ function autostartDir() {
62
+ return (0, import_node_path.join)(root(), "autostart");
63
+ }
64
+ function ensureDirs() {
65
+ for (const dir of [dataDir(), jobsDir(), logsDir()]) {
66
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
67
+ }
68
+ }
69
+
70
+ // src/schemas/job.ts
71
+ var import_zod = require("zod");
72
+ var CronScheduleSchema = import_zod.z.object({
73
+ kind: import_zod.z.literal("cron"),
74
+ cron: import_zod.z.string().min(1),
75
+ tz: import_zod.z.string().optional()
76
+ });
77
+ var IntervalScheduleSchema = import_zod.z.object({
78
+ kind: import_zod.z.literal("interval"),
79
+ everySec: import_zod.z.number().positive(),
80
+ startAt: import_zod.z.string().optional()
81
+ // ISO-8601
82
+ });
83
+ var OneShotScheduleSchema = import_zod.z.object({
84
+ kind: import_zod.z.literal("one-shot"),
85
+ runAt: import_zod.z.string().min(1)
86
+ // ISO-8601
87
+ });
88
+ var ScheduleSchema = import_zod.z.discriminatedUnion("kind", [
89
+ CronScheduleSchema,
90
+ IntervalScheduleSchema,
91
+ OneShotScheduleSchema
92
+ ]);
93
+ var ScriptActionSchema = import_zod.z.object({
94
+ kind: import_zod.z.literal("script"),
95
+ script: import_zod.z.string().min(1),
96
+ shell: import_zod.z.enum(["auto", "bash", "pwsh", "cmd"]).default("auto"),
97
+ cwd: import_zod.z.string().optional(),
98
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
99
+ envFile: import_zod.z.string().optional(),
100
+ timeoutSec: import_zod.z.number().positive().optional()
101
+ });
102
+ var ExecActionSchema = import_zod.z.object({
103
+ kind: import_zod.z.literal("exec"),
104
+ command: import_zod.z.string().min(1),
105
+ args: import_zod.z.array(import_zod.z.string()).default([]),
106
+ cwd: import_zod.z.string().optional(),
107
+ env: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).optional(),
108
+ envFile: import_zod.z.string().optional(),
109
+ timeoutSec: import_zod.z.number().positive().optional()
110
+ // shell is intentionally absent: exec always uses shell=false to prevent injection
111
+ });
112
+ var ActionSchema = import_zod.z.discriminatedUnion("kind", [ScriptActionSchema, ExecActionSchema]);
113
+ var RetrySchema = import_zod.z.object({
114
+ max: import_zod.z.number().int().min(0).default(0),
115
+ backoffSec: import_zod.z.number().positive().default(30)
116
+ });
117
+ var BudgetsSchema = import_zod.z.object({
118
+ maxRunsPerDay: import_zod.z.number().int().positive().nullable().default(null),
119
+ maxTokensPerRun: import_zod.z.number().int().positive().nullable().default(null)
120
+ });
121
+ var kebabCase = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
122
+ var JobSchema = import_zod.z.object({
123
+ id: import_zod.z.string().regex(kebabCase, 'Job ID must be kebab-case (e.g. "my-job")'),
124
+ description: import_zod.z.string().optional(),
125
+ enabled: import_zod.z.boolean().default(true),
126
+ schedule: ScheduleSchema,
127
+ action: ActionSchema,
128
+ catchup: import_zod.z.enum(["run-once", "run-all", "skip"]).default("skip"),
129
+ overlap: import_zod.z.enum(["skip", "queue", "cancel-previous"]).default("skip"),
130
+ retry: RetrySchema.default({ max: 0, backoffSec: 30 }),
131
+ budgets: BudgetsSchema.default({ maxRunsPerDay: null, maxTokensPerRun: null })
132
+ });
133
+
134
+ // src/errors.ts
135
+ var CrontickError = class _CrontickError extends Error {
136
+ code;
137
+ details;
138
+ constructor(code, message, details) {
139
+ super(message);
140
+ this.name = "CrontickError";
141
+ this.code = code;
142
+ this.details = details;
143
+ Object.setPrototypeOf(this, _CrontickError.prototype);
144
+ }
145
+ toJSON() {
146
+ return { code: this.code, message: this.message, details: this.details };
147
+ }
148
+ };
149
+
150
+ // src/autostart/win32.ts
151
+ var import_node_child_process = require("child_process");
152
+ var import_node_fs2 = require("fs");
153
+ var import_node_path2 = require("path");
154
+ var RUN_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
155
+ function getValueName() {
156
+ return process.env["CRONTICK_AUTOSTART_TEST_VALUE"] ?? "crontick-daemon";
157
+ }
158
+ function getVbsPath() {
159
+ return (0, import_node_path2.join)(autostartDir(), "crontick-daemon.vbs");
160
+ }
161
+ function findDaemonBinary() {
162
+ const override = process.env["CRONTICK_DAEMON_BINARY"];
163
+ if (override) return override;
164
+ const whereResult = (0, import_node_child_process.spawnSync)("where.exe", ["crontick-daemon"], {
165
+ encoding: "utf-8",
166
+ timeout: 5e3,
167
+ windowsHide: true
168
+ });
169
+ if (whereResult.status === 0 && whereResult.stdout.trim()) {
170
+ const first = whereResult.stdout.trim().split(/[\r\n]+/)[0]?.trim();
171
+ if (first) return first;
172
+ }
173
+ const nodeDir = (0, import_node_path2.dirname)(process.execPath);
174
+ for (const candidate of ["crontick-daemon.cmd", "crontick-daemon"]) {
175
+ const p = (0, import_node_path2.join)(nodeDir, candidate);
176
+ if ((0, import_node_fs2.existsSync)(p)) return p;
177
+ }
178
+ return null;
179
+ }
180
+ function buildVbsContent(daemonPath) {
181
+ const safePath = daemonPath.replace(/"/g, "");
182
+ return [
183
+ "' crontick-daemon autostart shim \u2014 generated by crontick autostart install",
184
+ "' Do not edit manually. Run: crontick autostart install to regenerate.",
185
+ 'Set WshShell = CreateObject("WScript.Shell")',
186
+ `WshShell.Run "cmd /c ""${safePath}""", 0, False`,
187
+ ""
188
+ ].join("\r\n");
189
+ }
190
+ function regQuery(valueName) {
191
+ const result = (0, import_node_child_process.spawnSync)(
192
+ "reg",
193
+ ["query", RUN_KEY, "/v", valueName],
194
+ { encoding: "utf-8", timeout: 5e3, windowsHide: true }
195
+ );
196
+ if (result.status !== 0 || !result.stdout) return null;
197
+ const match = result.stdout.match(/REG_SZ\s+(.+)/i);
198
+ return match?.[1]?.trim() ?? null;
199
+ }
200
+ function regWrite(valueName, data) {
201
+ const result = (0, import_node_child_process.spawnSync)(
202
+ "reg",
203
+ ["add", RUN_KEY, "/v", valueName, "/t", "REG_SZ", "/d", data, "/f"],
204
+ { encoding: "utf-8", timeout: 5e3, windowsHide: true }
205
+ );
206
+ if (result.status !== 0) {
207
+ throw new Error(
208
+ `reg.exe add failed (exit ${result.status ?? "?"}): ${result.stderr?.trim() ?? "unknown error"}`
209
+ );
210
+ }
211
+ }
212
+ function regDelete(valueName) {
213
+ (0, import_node_child_process.spawnSync)(
214
+ "reg",
215
+ ["delete", RUN_KEY, "/v", valueName, "/f"],
216
+ { encoding: "utf-8", timeout: 5e3, windowsHide: true }
217
+ );
218
+ }
219
+ var Win32Autostart = class {
220
+ async install() {
221
+ const daemonPath = findDaemonBinary();
222
+ if (!daemonPath) {
223
+ throw new Error(
224
+ "crontick-daemon binary not found. Ensure crontick is installed globally: npm i -g crontick"
225
+ );
226
+ }
227
+ const vbsPath = getVbsPath();
228
+ const valueName = getValueName();
229
+ (0, import_node_fs2.mkdirSync)(autostartDir(), { recursive: true });
230
+ (0, import_node_fs2.writeFileSync)(vbsPath, buildVbsContent(daemonPath), "utf-8");
231
+ regDelete(valueName);
232
+ regWrite(valueName, `wscript.exe "${vbsPath}"`);
233
+ return { ok: true };
234
+ }
235
+ async remove() {
236
+ const valueName = getValueName();
237
+ regDelete(valueName);
238
+ const vbsPath = getVbsPath();
239
+ if ((0, import_node_fs2.existsSync)(vbsPath)) {
240
+ try {
241
+ (0, import_node_fs2.unlinkSync)(vbsPath);
242
+ } catch {
243
+ }
244
+ }
245
+ return { ok: true };
246
+ }
247
+ async status() {
248
+ const valueName = getValueName();
249
+ const data = regQuery(valueName);
250
+ const installed = data !== null;
251
+ return {
252
+ installed,
253
+ backend: "win32",
254
+ details: installed ? { registryValue: valueName, registryData: data, vbsPath: getVbsPath() } : { registryValue: valueName }
255
+ };
256
+ }
257
+ };
258
+
259
+ // src/autostart/manual.ts
260
+ function instructions() {
261
+ switch (process.platform) {
262
+ case "win32":
263
+ return "Windows: add the following command to your startup folder or HKCU Run key:\n crontick-daemon\nOr run: crontick autostart install (uses HKCU Run + hidden VBS shim)";
264
+ case "darwin":
265
+ return "macOS (post-v1): create a launchd plist at\n ~/Library/LaunchAgents/com.crontick.daemon.plist\nwith ProgramArguments pointing to the crontick-daemon binary.\nThen run: launchctl load ~/Library/LaunchAgents/com.crontick.daemon.plist\n\nFor now, add the following to your shell profile (~/.zprofile or ~/.bash_profile):\n crontick-daemon &";
266
+ case "linux":
267
+ return "Linux (post-v1): create a systemd user unit at\n ~/.config/systemd/user/crontick.service\nThen run: systemctl --user enable --now crontick.service\n\nFor now, add the following to your shell profile (~/.profile or ~/.bashrc):\n crontick-daemon &\nOr add a @reboot crontab entry: crontab -e\n @reboot crontick-daemon";
268
+ default:
269
+ return "Add `crontick-daemon` to your system startup mechanism.\nThe daemon listens on a random localhost port; the port is written to the data directory.";
270
+ }
271
+ }
272
+ var ManualAutostart = class {
273
+ async install() {
274
+ return { ok: true };
275
+ }
276
+ async remove() {
277
+ return { ok: true };
278
+ }
279
+ async status() {
280
+ return {
281
+ installed: false,
282
+ backend: "manual",
283
+ details: { instructions: instructions() }
284
+ };
285
+ }
286
+ };
287
+
288
+ // src/autostart/darwin.ts
289
+ var DarwinAutostart = class {
290
+ async install() {
291
+ throw new NotImplementedInV1Error(
292
+ "darwin autostart is planned for post-v1; use manual for now. See https://github.com/crontick/crontick for updates."
293
+ );
294
+ }
295
+ async remove() {
296
+ throw new NotImplementedInV1Error(
297
+ "darwin autostart is planned for post-v1; use manual for now."
298
+ );
299
+ }
300
+ async status() {
301
+ throw new NotImplementedInV1Error(
302
+ "darwin autostart is planned for post-v1; use manual for now."
303
+ );
304
+ }
305
+ };
306
+
307
+ // src/autostart/linux.ts
308
+ var LinuxAutostart = class {
309
+ async install() {
310
+ throw new NotImplementedInV1Error(
311
+ "linux autostart is planned for post-v1; use manual for now. See https://github.com/crontick/crontick for updates."
312
+ );
313
+ }
314
+ async remove() {
315
+ throw new NotImplementedInV1Error(
316
+ "linux autostart is planned for post-v1; use manual for now."
317
+ );
318
+ }
319
+ async status() {
320
+ throw new NotImplementedInV1Error(
321
+ "linux autostart is planned for post-v1; use manual for now."
322
+ );
323
+ }
324
+ };
325
+
326
+ // src/autostart/index.ts
327
+ var NotImplementedInV1Error = class _NotImplementedInV1Error extends Error {
328
+ constructor(message) {
329
+ super(message);
330
+ this.name = "NotImplementedInV1Error";
331
+ Object.setPrototypeOf(this, _NotImplementedInV1Error.prototype);
332
+ }
333
+ };
334
+ function createAutostart(opts) {
335
+ const backend = opts?.backend ?? (process.platform === "win32" ? "win32" : "manual");
336
+ switch (backend) {
337
+ case "win32":
338
+ return new Win32Autostart();
339
+ case "darwin":
340
+ return new DarwinAutostart();
341
+ case "linux":
342
+ return new LinuxAutostart();
343
+ case "manual":
344
+ default:
345
+ return new ManualAutostart();
346
+ }
347
+ }
348
+
349
+ // src/cli/index.ts
350
+ var import_meta = {};
351
+ var __dirname = (0, import_node_path3.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
352
+ function getPort() {
353
+ const pf = portFilePath();
354
+ if (!(0, import_node_fs3.existsSync)(pf)) {
355
+ throw new CrontickError(
356
+ "DAEMON_NOT_RUNNING",
357
+ "Daemon is not running. Start it with: crontick daemon start"
358
+ );
359
+ }
360
+ const port = parseInt((0, import_node_fs3.readFileSync)(pf, "utf-8").trim(), 10);
361
+ if (isNaN(port)) {
362
+ throw new CrontickError("DAEMON_NOT_RUNNING", "Could not read daemon port file");
363
+ }
364
+ return port;
365
+ }
366
+ async function api(method, path, body) {
367
+ const port = getPort();
368
+ const url = `http://127.0.0.1:${port}${path}`;
369
+ const options = {
370
+ method,
371
+ headers: { "Content-Type": "application/json" }
372
+ };
373
+ if (body !== void 0) options.body = JSON.stringify(body);
374
+ const res = await fetch(url, options);
375
+ const text = await res.text();
376
+ let data;
377
+ try {
378
+ data = JSON.parse(text);
379
+ } catch {
380
+ throw new CrontickError("PARSE_ERROR", `Unexpected response: ${text.slice(0, 200)}`);
381
+ }
382
+ if (!res.ok) {
383
+ const err = data?.error;
384
+ throw new CrontickError(err?.code ?? "API_ERROR", err?.message ?? `HTTP ${res.status}`);
385
+ }
386
+ return data;
387
+ }
388
+ function print(data, useJson) {
389
+ if (useJson) {
390
+ console.log(JSON.stringify(data, null, 2));
391
+ return;
392
+ }
393
+ if (Array.isArray(data)) {
394
+ if (data.length === 0) {
395
+ console.log("(no items)");
396
+ return;
397
+ }
398
+ const rows = data;
399
+ const keys = Object.keys(rows[0]);
400
+ console.log(keys.join(" "));
401
+ for (const row of rows) {
402
+ console.log(
403
+ keys.map((k) => {
404
+ const v = row[k];
405
+ return v !== null && typeof v === "object" ? JSON.stringify(v) : String(v ?? "");
406
+ }).join(" ")
407
+ );
408
+ }
409
+ } else if (data !== null && typeof data === "object") {
410
+ const obj = data;
411
+ for (const [key, value] of Object.entries(obj)) {
412
+ const display = value !== null && typeof value === "object" ? JSON.stringify(value) : String(value ?? "");
413
+ console.log(`${key}: ${display}`);
414
+ }
415
+ } else {
416
+ console.log(String(data ?? ""));
417
+ }
418
+ }
419
+ function handleError(err) {
420
+ if (err instanceof CrontickError) {
421
+ console.error(`Error [${err.code}]: ${err.message}`);
422
+ } else {
423
+ console.error(`Error: ${String(err)}`);
424
+ }
425
+ process.exit(1);
426
+ }
427
+ function daemonScript() {
428
+ return (0, import_node_path3.resolve)(__dirname, "../daemon/index.js");
429
+ }
430
+ function waitForPort(timeout = 1e4) {
431
+ const start = Date.now();
432
+ return new Promise((resolve2, reject) => {
433
+ const check = () => {
434
+ try {
435
+ const port = getPort();
436
+ resolve2(port);
437
+ } catch {
438
+ if (Date.now() - start > timeout) {
439
+ reject(new CrontickError("DAEMON_TIMEOUT", "Timed out waiting for daemon to start"));
440
+ } else {
441
+ setTimeout(check, 200);
442
+ }
443
+ }
444
+ };
445
+ check();
446
+ });
447
+ }
448
+ var program = new import_commander.Command();
449
+ program.name("crontick").description("A standalone cron daemon, CLI, and MCP server for local scheduled jobs.").version(VERSION).option("--json", "Output as JSON");
450
+ program.command("new <id>").description("Create a new job").option("--cron <expr>", 'Cron expression (e.g. "0 9 * * *")').option("--every <sec>", "Interval in seconds", parseInt).option("--at <iso>", "One-shot run-at ISO-8601 time").option("--tz <tz>", "Timezone for cron schedule").option("--script <body>", "Inline script body").option("--exec <cmd>", "Command to exec (use -- for args)").option("--file <path>", "Load full job from JSON file").option("--shell <shell>", "Shell: auto|bash|pwsh|cmd", "auto").option("--env-file <path>", "Load extra environment variables from a .env file").option("--timeout <sec>", "Timeout in seconds", parseInt).option("--overlap <policy>", "Overlap policy: skip|queue|cancel-previous", "skip").option("--retry <max>", "Retry count", parseInt).option("--desc <description>", "Job description").action(async (id, opts) => {
451
+ try {
452
+ let jobData;
453
+ if (opts.file) {
454
+ const raw = (0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(process.cwd(), opts.file), "utf-8");
455
+ jobData = JSON.parse(raw);
456
+ } else {
457
+ let schedule;
458
+ if (opts.cron) {
459
+ schedule = { kind: "cron", cron: opts.cron, tz: opts.tz };
460
+ } else if (opts.every) {
461
+ schedule = { kind: "interval", everySec: opts.every };
462
+ } else if (opts.at) {
463
+ schedule = { kind: "one-shot", runAt: opts.at };
464
+ } else {
465
+ throw new CrontickError("MISSING_ARG", "Provide --cron, --every <sec>, or --at <iso>");
466
+ }
467
+ let action;
468
+ if (opts.script) {
469
+ action = {
470
+ kind: "script",
471
+ script: opts.script,
472
+ shell: opts.shell,
473
+ envFile: opts.envFile,
474
+ timeoutSec: opts.timeout
475
+ };
476
+ } else if (opts.exec) {
477
+ const parts = opts.exec.split(/\s+/);
478
+ action = {
479
+ kind: "exec",
480
+ command: parts[0],
481
+ args: parts.slice(1),
482
+ envFile: opts.envFile,
483
+ timeoutSec: opts.timeout
484
+ };
485
+ } else {
486
+ throw new CrontickError("MISSING_ARG", "Provide --script or --exec");
487
+ }
488
+ jobData = {
489
+ id,
490
+ description: opts.desc,
491
+ schedule,
492
+ action,
493
+ overlap: opts.overlap,
494
+ retry: opts.retry !== void 0 ? { max: opts.retry, backoffSec: 30 } : void 0
495
+ };
496
+ }
497
+ const parsed = JobSchema.safeParse(jobData);
498
+ if (!parsed.success) {
499
+ throw new CrontickError("VALIDATION_ERROR", "Invalid job", parsed.error.format());
500
+ }
501
+ const result = await api("POST", "/api/jobs", parsed.data);
502
+ print(result, !!program.opts().json);
503
+ } catch (err) {
504
+ handleError(err);
505
+ }
506
+ });
507
+ program.command("list").description("List all jobs").action(async () => {
508
+ try {
509
+ const jobs = await api("GET", "/api/jobs");
510
+ print(jobs, !!program.opts().json);
511
+ } catch (err) {
512
+ handleError(err);
513
+ }
514
+ });
515
+ program.command("get <id>").description("Get a job by ID").action(async (id) => {
516
+ try {
517
+ const job = await api("GET", `/api/jobs/${encodeURIComponent(id)}`);
518
+ print(job, !!program.opts().json);
519
+ } catch (err) {
520
+ handleError(err);
521
+ }
522
+ });
523
+ program.command("enable <id>").description("Enable a job").action(async (id) => {
524
+ try {
525
+ const result = await api("POST", `/api/jobs/${encodeURIComponent(id)}/enable`);
526
+ print(result, !!program.opts().json);
527
+ } catch (err) {
528
+ handleError(err);
529
+ }
530
+ });
531
+ program.command("disable <id>").description("Disable a job").action(async (id) => {
532
+ try {
533
+ const result = await api("POST", `/api/jobs/${encodeURIComponent(id)}/disable`);
534
+ print(result, !!program.opts().json);
535
+ } catch (err) {
536
+ handleError(err);
537
+ }
538
+ });
539
+ program.command("delete <id>").description("Delete a job").action(async (id) => {
540
+ try {
541
+ const result = await api("DELETE", `/api/jobs/${encodeURIComponent(id)}`);
542
+ print(result, !!program.opts().json);
543
+ } catch (err) {
544
+ handleError(err);
545
+ }
546
+ });
547
+ program.command("run-now <id>").description("Trigger an immediate run of a job").action(async (id) => {
548
+ try {
549
+ const result = await api("POST", `/api/jobs/${encodeURIComponent(id)}/run`);
550
+ print(result, !!program.opts().json);
551
+ } catch (err) {
552
+ handleError(err);
553
+ }
554
+ });
555
+ program.command("logs <runId>").description("Get logs for a run").option("--follow", "Follow (SSE stream) \u2014 not implemented in CLI yet; use --tail").option("--tail <n>", "Show last N lines", parseInt).action(async (runId, opts) => {
556
+ try {
557
+ const useJson = !!program.opts().json;
558
+ const logs = await api("GET", `/api/runs/${encodeURIComponent(runId)}/logs`);
559
+ const lines = Array.isArray(logs) ? logs : [];
560
+ const tail = opts.tail;
561
+ const display = tail ? lines.slice(-tail) : lines;
562
+ if (useJson) {
563
+ console.log(JSON.stringify(display, null, 2));
564
+ } else {
565
+ for (const entry of display) {
566
+ process.stdout.write(`[${entry.stream}] ${entry.data}`);
567
+ }
568
+ }
569
+ } catch (err) {
570
+ handleError(err);
571
+ }
572
+ });
573
+ program.command("export").description("Export all jobs").option("--out <file>", "Output file (default: stdout)").action(async (opts) => {
574
+ try {
575
+ const data = await api("GET", "/api/export");
576
+ const json = JSON.stringify(data, null, 2);
577
+ if (opts.out) {
578
+ (0, import_node_fs3.writeFileSync)((0, import_node_path3.resolve)(process.cwd(), opts.out), json, "utf-8");
579
+ console.log(`Exported to ${opts.out}`);
580
+ } else {
581
+ console.log(json);
582
+ }
583
+ } catch (err) {
584
+ handleError(err);
585
+ }
586
+ });
587
+ program.command("import <file>").description("Import jobs from a JSON file").action(async (file) => {
588
+ try {
589
+ const raw = (0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(process.cwd(), file), "utf-8");
590
+ const data = JSON.parse(raw);
591
+ const result = await api("POST", "/api/import", data);
592
+ print(result, !!program.opts().json);
593
+ } catch (err) {
594
+ handleError(err);
595
+ }
596
+ });
597
+ program.command("doctor").description("Check system health").action(async () => {
598
+ const checks = [];
599
+ const major = parseInt(process.versions.node.split(".")[0], 10);
600
+ checks.push({
601
+ name: "Node.js >= 22.5",
602
+ ok: major >= 22,
603
+ note: `v${process.versions.node}`
604
+ });
605
+ try {
606
+ const { DatabaseSync } = await import("node:sqlite");
607
+ new DatabaseSync(":memory:").close();
608
+ checks.push({ name: "node:sqlite", ok: true });
609
+ } catch (err) {
610
+ checks.push({ name: "node:sqlite", ok: false, note: String(err) });
611
+ }
612
+ try {
613
+ ensureDirs();
614
+ checks.push({ name: "data dir writable", ok: true });
615
+ } catch (err) {
616
+ checks.push({ name: "data dir writable", ok: false, note: String(err) });
617
+ }
618
+ const portFile = portFilePath();
619
+ const portFileExists = (0, import_node_fs3.existsSync)(portFile);
620
+ checks.push({ name: "port file readable", ok: portFileExists, note: portFileExists ? portFile : "not found" });
621
+ try {
622
+ await api("GET", "/health");
623
+ checks.push({ name: "daemon reachable", ok: true });
624
+ } catch {
625
+ checks.push({ name: "daemon reachable", ok: false, note: "not running" });
626
+ }
627
+ try {
628
+ const port2 = getPort();
629
+ const dashRes = await fetch(`http://127.0.0.1:${port2}/dashboard`, {
630
+ signal: AbortSignal.timeout(2e3)
631
+ });
632
+ const text = await dashRes.text();
633
+ checks.push({
634
+ name: "dashboard reachable",
635
+ ok: dashRes.status === 200 && text.includes("crontick"),
636
+ note: dashRes.status === 200 ? "ok" : `HTTP ${dashRes.status}`
637
+ });
638
+ } catch {
639
+ checks.push({ name: "dashboard reachable", ok: false, note: "daemon not running or no dashboard" });
640
+ }
641
+ try {
642
+ const asResult = await api("GET", "/api/autostart/status");
643
+ const as = asResult;
644
+ checks.push({
645
+ name: "autostart",
646
+ ok: true,
647
+ note: `backend=${as.backend ?? "?"}, installed=${String(as.installed ?? false)}`
648
+ });
649
+ } catch {
650
+ checks.push({ name: "autostart", ok: false, note: "could not check (daemon not running)" });
651
+ }
652
+ const mcpScript = (0, import_node_path3.resolve)(__dirname, "../mcp/index.js");
653
+ checks.push({
654
+ name: "MCP server binary",
655
+ ok: (0, import_node_fs3.existsSync)(mcpScript),
656
+ note: mcpScript
657
+ });
658
+ try {
659
+ const result = (0, import_node_child_process2.spawnSync)(process.execPath, [mcpScript, "--help"], {
660
+ timeout: 5e3,
661
+ encoding: "utf-8",
662
+ env: { ...process.env, CRONTICK_MCP_NO_AUTOSTART: "1" }
663
+ });
664
+ const helpOk = result.status === 0 || (result.stdout ?? "").includes("stdio");
665
+ checks.push({ name: "MCP server --help", ok: helpOk });
666
+ } catch (err) {
667
+ checks.push({ name: "MCP server --help", ok: false, note: String(err) });
668
+ }
669
+ for (const c of checks) {
670
+ const icon = c.ok ? "\u2713" : "\u2717";
671
+ console.log(`${icon} ${c.name}${c.note ? ` (${c.note})` : ""}`);
672
+ }
673
+ const failed = checks.filter((c) => !c.ok);
674
+ if (failed.length > 0) process.exit(1);
675
+ });
676
+ var daemon = program.command("daemon").description("Manage the crontick daemon");
677
+ daemon.command("start").description("Start the daemon (detached by default)").option("--foreground", "Run in foreground (blocking)").action(async (opts) => {
678
+ try {
679
+ const script = daemonScript();
680
+ if (!(0, import_node_fs3.existsSync)(script)) {
681
+ throw new CrontickError("NOT_BUILT", `Daemon script not found: ${script}. Run: npm run build`);
682
+ }
683
+ if (opts.foreground) {
684
+ const result = (0, import_node_child_process2.spawnSync)(process.execPath, [script], {
685
+ stdio: "inherit",
686
+ env: process.env
687
+ });
688
+ process.exit(result.status ?? 0);
689
+ } else {
690
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [script], {
691
+ detached: true,
692
+ stdio: "ignore",
693
+ env: process.env
694
+ });
695
+ child.unref();
696
+ console.log(`Starting daemon (pid will be written to port file)\u2026`);
697
+ const port = await waitForPort();
698
+ console.log(`Daemon started on port ${port}`);
699
+ }
700
+ } catch (err) {
701
+ handleError(err);
702
+ }
703
+ });
704
+ daemon.command("stop").description("Stop the daemon").action(async () => {
705
+ try {
706
+ const pidFile = pidFilePath();
707
+ if (!(0, import_node_fs3.existsSync)(pidFile)) {
708
+ console.log("Daemon is not running");
709
+ return;
710
+ }
711
+ const pid = parseInt((0, import_node_fs3.readFileSync)(pidFile, "utf-8").trim(), 10);
712
+ process.kill(pid, "SIGTERM");
713
+ console.log(`Sent SIGTERM to daemon (pid ${pid})`);
714
+ } catch (err) {
715
+ handleError(err);
716
+ }
717
+ });
718
+ daemon.command("status").description("Show daemon status").action(async () => {
719
+ try {
720
+ const status = await api("GET", "/api/daemon/status");
721
+ print(status, !!program.opts().json);
722
+ } catch {
723
+ console.log("Daemon is not running");
724
+ }
725
+ });
726
+ daemon.command("reload").description("Reload jobs from disk").action(async () => {
727
+ try {
728
+ const result = await api("POST", "/api/daemon/reload");
729
+ print(result, !!program.opts().json);
730
+ } catch (err) {
731
+ handleError(err);
732
+ }
733
+ });
734
+ daemon.command("restart").description("Restart the daemon").action(async () => {
735
+ try {
736
+ const pidFile = pidFilePath();
737
+ if ((0, import_node_fs3.existsSync)(pidFile)) {
738
+ const pid = parseInt((0, import_node_fs3.readFileSync)(pidFile, "utf-8").trim(), 10);
739
+ try {
740
+ process.kill(pid, "SIGTERM");
741
+ await new Promise((r) => setTimeout(r, 1e3));
742
+ } catch {
743
+ }
744
+ }
745
+ const script = daemonScript();
746
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [script], {
747
+ detached: true,
748
+ stdio: "ignore",
749
+ env: process.env
750
+ });
751
+ child.unref();
752
+ const port = await waitForPort();
753
+ console.log(`Daemon restarted on port ${port}`);
754
+ } catch (err) {
755
+ handleError(err);
756
+ }
757
+ });
758
+ var autostart = program.command("autostart").description("Manage daemon autostart at login");
759
+ autostart.command("install").description("Register the daemon to start automatically at login").option("--backend <backend>", "Backend: win32|darwin|linux|manual (default: auto-detect)").action(async (opts) => {
760
+ try {
761
+ const backend = opts.backend;
762
+ const as = createAutostart({ backend });
763
+ const result = await as.install();
764
+ print(result, !!program.opts().json);
765
+ if (process.platform !== "win32" && !backend) {
766
+ const statusResult = await as.status();
767
+ const details = statusResult.details;
768
+ const instr = details?.["instructions"];
769
+ if (instr) console.log("\nManual setup instructions:\n" + instr);
770
+ }
771
+ } catch (err) {
772
+ handleError(err);
773
+ }
774
+ });
775
+ autostart.command("remove").description("Remove the daemon from automatic startup").option("--backend <backend>", "Backend: win32|darwin|linux|manual (default: auto-detect)").action(async (opts) => {
776
+ try {
777
+ const backend = opts.backend;
778
+ const as = createAutostart({ backend });
779
+ const result = await as.remove();
780
+ print(result, !!program.opts().json);
781
+ } catch (err) {
782
+ handleError(err);
783
+ }
784
+ });
785
+ autostart.command("status").description("Check whether the daemon is registered for automatic startup").option("--backend <backend>", "Backend: win32|darwin|linux|manual (default: auto-detect)").action(async (opts) => {
786
+ try {
787
+ const backend = opts.backend;
788
+ const as = createAutostart({ backend });
789
+ const result = await as.status();
790
+ print(result, !!program.opts().json);
791
+ } catch (err) {
792
+ handleError(err);
793
+ }
794
+ });
795
+ program.command("uninstall").description("Remove autostart entry and optionally delete all crontick data").option("--purge", "Also delete the data directory (jobs, runs, config)").option("--yes", "Skip confirmation prompts").action(async (opts) => {
796
+ try {
797
+ const as = createAutostart();
798
+ await as.remove();
799
+ console.log("\u2713 Autostart entry removed.");
800
+ if (opts.purge) {
801
+ const dir = dataDir();
802
+ let confirmed = opts.yes;
803
+ if (!confirmed) {
804
+ confirmed = await new Promise((resolve2) => {
805
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
806
+ rl.question(`Delete all crontick data at ${dir}? [y/N] `, (answer) => {
807
+ rl.close();
808
+ resolve2(answer.trim().toLowerCase() === "y");
809
+ });
810
+ });
811
+ }
812
+ if (confirmed) {
813
+ (0, import_node_fs3.rmSync)(dir, { recursive: true, force: true });
814
+ console.log(`\u2713 Data directory deleted: ${dir}`);
815
+ } else {
816
+ console.log("Skipped data directory deletion.");
817
+ }
818
+ } else {
819
+ console.log(
820
+ "Data directory preserved. Run `crontick uninstall --purge` to also delete it."
821
+ );
822
+ }
823
+ } catch (err) {
824
+ handleError(err);
825
+ }
826
+ });
827
+ program.command("dashboard").description("Open the crontick dashboard in a browser").option("--open", "Open in the default browser").action(async (opts) => {
828
+ try {
829
+ const port = getPort();
830
+ const url = `http://127.0.0.1:${port}/dashboard`;
831
+ if (opts.open) {
832
+ if (process.platform === "win32") {
833
+ (0, import_node_child_process2.spawn)("cmd", ["/c", "start", url], { detached: true, stdio: "ignore" }).unref();
834
+ } else if (process.platform === "darwin") {
835
+ (0, import_node_child_process2.spawn)("open", [url], { detached: true, stdio: "ignore" }).unref();
836
+ } else {
837
+ try {
838
+ (0, import_node_child_process2.spawn)("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
839
+ } catch {
840
+ }
841
+ }
842
+ console.log(`Dashboard opened: ${url}`);
843
+ } else {
844
+ console.log(`Dashboard: ${url}`);
845
+ }
846
+ } catch (err) {
847
+ handleError(err);
848
+ }
849
+ });
850
+ program.command("mcp").description("Start the crontick MCP server on stdio (for use with Claude Desktop, Copilot, Cursor, etc.)").option("--no-autostart", "Do not auto-start the daemon if it is not already running").option("--daemon-url <url>", "Override the daemon URL (default: resolved from port file)").addHelpText(
851
+ "after",
852
+ `
853
+ Transport: stdio (JSON-RPC 2.0 over stdin/stdout)
854
+ Tool prefix: crontick_
855
+ Autostart: Daemon is auto-started unless --no-autostart or CRONTICK_MCP_NO_AUTOSTART=1 is set
856
+
857
+ Example MCP host config (Claude Desktop):
858
+ {
859
+ "mcpServers": {
860
+ "crontick": { "command": "crontick", "args": ["mcp"] }
861
+ }
862
+ }`
863
+ ).action((opts) => {
864
+ const mcpScript = (0, import_node_path3.resolve)(__dirname, "../mcp/index.js");
865
+ if (!(0, import_node_fs3.existsSync)(mcpScript)) {
866
+ console.error(`MCP server script not found: ${mcpScript}. Run: npm run build`);
867
+ process.exit(1);
868
+ }
869
+ const env = { ...process.env };
870
+ if (opts.noAutostart) env["CRONTICK_MCP_NO_AUTOSTART"] = "1";
871
+ if (opts.daemonUrl) env["CRONTICK_DAEMON_URL"] = opts.daemonUrl;
872
+ const result = (0, import_node_child_process2.spawnSync)(process.execPath, [mcpScript], {
873
+ stdio: "inherit",
874
+ env
875
+ });
876
+ process.exit(result.status ?? 0);
877
+ });
878
+ program.parse();
879
+ //# sourceMappingURL=index.cjs.map