crnd 0.0.1

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 (122) hide show
  1. package/README.md +108 -0
  2. package/drizzle/0000_init.sql +35 -0
  3. package/drizzle/0001_add_runs.sql +13 -0
  4. package/drizzle/meta/_journal.json +20 -0
  5. package/package.json +59 -0
  6. package/src/cli/commands/createDeleteCommand.ts +93 -0
  7. package/src/cli/commands/createDoctorCommand.ts +93 -0
  8. package/src/cli/commands/createExportCommand.ts +75 -0
  9. package/src/cli/commands/createImportCommand.ts +80 -0
  10. package/src/cli/commands/createKillCommand.ts +89 -0
  11. package/src/cli/commands/createListCommand.ts +67 -0
  12. package/src/cli/commands/createLogsCommand.ts +125 -0
  13. package/src/cli/commands/createPauseCommand.ts +78 -0
  14. package/src/cli/commands/createResetCommand.ts +78 -0
  15. package/src/cli/commands/createResumeCommand.ts +78 -0
  16. package/src/cli/commands/createRootCommand.ts +50 -0
  17. package/src/cli/commands/createRunOnceCommand.ts +77 -0
  18. package/src/cli/commands/createRunsCommand.ts +106 -0
  19. package/src/cli/commands/createScheduleCommand.ts +184 -0
  20. package/src/cli/commands/createShowCommand.ts +92 -0
  21. package/src/cli/commands/createStatusCommand.ts +145 -0
  22. package/src/cli/commands/createStopCommand.ts +89 -0
  23. package/src/cli/commands/createUpdateCommand.ts +13 -0
  24. package/src/cli/commands/daemon/createDaemonCommand.ts +24 -0
  25. package/src/cli/commands/daemon/createDaemonInstallCommand.ts +144 -0
  26. package/src/cli/commands/daemon/createDaemonServeCommand.ts +14 -0
  27. package/src/cli/commands/daemon/createDaemonStartCommand.ts +73 -0
  28. package/src/cli/commands/daemon/createDaemonStatusCommand.ts +5 -0
  29. package/src/cli/commands/daemon/createDaemonStopCommand.ts +59 -0
  30. package/src/cli/commands/daemon/createDaemonUninstallCommand.ts +99 -0
  31. package/src/cli/commands/daemon/createLaunchdPlist.ts +34 -0
  32. package/src/cli/commands/daemon/createSystemdService.ts +24 -0
  33. package/src/cli/commands/daemon/escapeXml.ts +8 -0
  34. package/src/cli/commands/daemon/getDaemonServiceArgs.ts +10 -0
  35. package/src/cli/commands/daemon/quoteWindowsArg.ts +4 -0
  36. package/src/cli/commands/getCommandArgs.ts +8 -0
  37. package/src/cli/commands/parseEnvArgs.ts +28 -0
  38. package/src/cli/getDaemonSpawnArgs.ts +9 -0
  39. package/src/cli/main.ts +8 -0
  40. package/src/daemon/autostart/ensureAutostart.ts +30 -0
  41. package/src/daemon/autostart/getAutostartPath.ts +29 -0
  42. package/src/daemon/autostart/getDaemonInstallArgs.ts +10 -0
  43. package/src/daemon/createLogger.ts +13 -0
  44. package/src/daemon/createShutdownHandler.ts +29 -0
  45. package/src/daemon/jobs/createJobsFileSync.ts +113 -0
  46. package/src/daemon/jobs/deleteJobByName.ts +18 -0
  47. package/src/daemon/jobs/upsertJob.ts +85 -0
  48. package/src/daemon/main.ts +64 -0
  49. package/src/daemon/runner/createRunOutputFds.ts +18 -0
  50. package/src/daemon/runner/getRunStatus.ts +14 -0
  51. package/src/daemon/runner/recordSkippedRun.ts +27 -0
  52. package/src/daemon/runner/recoverRunningRuns.ts +36 -0
  53. package/src/daemon/runner/runJob.ts +94 -0
  54. package/src/daemon/scheduler/createScheduler.ts +45 -0
  55. package/src/daemon/scheduler/createSchedulerState.ts +8 -0
  56. package/src/daemon/scheduler/loadJobs.ts +10 -0
  57. package/src/daemon/scheduler/runJobWithTracking.ts +48 -0
  58. package/src/daemon/scheduler/scheduleJob.ts +32 -0
  59. package/src/daemon/scheduler/unscheduleJob.ts +11 -0
  60. package/src/daemon/scheduler/updateNextRunAt.ts +14 -0
  61. package/src/daemon/server/createApp.ts +76 -0
  62. package/src/daemon/server/createAuthMiddleware.ts +16 -0
  63. package/src/daemon/server/routes/registerExportRoute.ts +21 -0
  64. package/src/daemon/server/routes/registerHealthRoute.ts +16 -0
  65. package/src/daemon/server/routes/registerImportRoute.ts +22 -0
  66. package/src/daemon/server/routes/registerJobRunsRoute.ts +46 -0
  67. package/src/daemon/server/routes/registerJobsDeleteRoute.ts +37 -0
  68. package/src/daemon/server/routes/registerJobsGetRoute.ts +29 -0
  69. package/src/daemon/server/routes/registerJobsKillRoute.ts +45 -0
  70. package/src/daemon/server/routes/registerJobsListRoute.ts +13 -0
  71. package/src/daemon/server/routes/registerJobsPauseRoute.ts +53 -0
  72. package/src/daemon/server/routes/registerJobsResetRoute.ts +54 -0
  73. package/src/daemon/server/routes/registerJobsResumeRoute.ts +53 -0
  74. package/src/daemon/server/routes/registerJobsRunRoute.ts +37 -0
  75. package/src/daemon/server/routes/registerJobsStopRoute.ts +45 -0
  76. package/src/daemon/server/routes/registerJobsUpsertRoute.ts +30 -0
  77. package/src/daemon/server/routes/registerRunGetRoute.ts +25 -0
  78. package/src/daemon/server/routes/registerRunLogsRoute.ts +32 -0
  79. package/src/daemon/server/routes/registerShutdownRoute.ts +9 -0
  80. package/src/daemon/server/startServer.ts +23 -0
  81. package/src/db/getMigrationsDir.ts +5 -0
  82. package/src/db/migrateDatabase.ts +21 -0
  83. package/src/db/openDatabase.ts +12 -0
  84. package/src/db/schema/jobs.ts +26 -0
  85. package/src/db/schema/jobsSchemas.ts +10 -0
  86. package/src/db/schema/runs.ts +23 -0
  87. package/src/db/schema/runsSchemas.ts +10 -0
  88. package/src/db/schema.ts +5 -0
  89. package/src/shared/auth/createToken.ts +5 -0
  90. package/src/shared/events/appendEvent.ts +16 -0
  91. package/src/shared/jobs/createCommandSchema.ts +5 -0
  92. package/src/shared/jobs/createEnvSchema.ts +5 -0
  93. package/src/shared/jobs/createJobInputSchema.ts +31 -0
  94. package/src/shared/jobs/createTomlJobSchema.ts +31 -0
  95. package/src/shared/jobs/formatJobRow.ts +14 -0
  96. package/src/shared/jobs/isOverlapPolicy.ts +5 -0
  97. package/src/shared/jobs/parseCommand.ts +6 -0
  98. package/src/shared/jobs/parseEnv.ts +10 -0
  99. package/src/shared/jobs/parseJobsToml.ts +36 -0
  100. package/src/shared/jobs/readJobsToml.ts +17 -0
  101. package/src/shared/jobs/serializeCommand.ts +6 -0
  102. package/src/shared/jobs/serializeEnv.ts +6 -0
  103. package/src/shared/jobs/serializeJobsToml.ts +45 -0
  104. package/src/shared/jobs/writeJobsToml.ts +12 -0
  105. package/src/shared/paths/ensureDir.ts +6 -0
  106. package/src/shared/paths/getConfigDir.ts +6 -0
  107. package/src/shared/paths/getEventsPath.ts +6 -0
  108. package/src/shared/paths/getJobRunsDir.ts +7 -0
  109. package/src/shared/paths/getJobsTomlPath.ts +6 -0
  110. package/src/shared/paths/getPaths.ts +16 -0
  111. package/src/shared/paths/getRunOutputPaths.ts +10 -0
  112. package/src/shared/paths/getRunsDir.ts +7 -0
  113. package/src/shared/paths/getStateDir.ts +8 -0
  114. package/src/shared/rpc/createRpcClient.ts +20 -0
  115. package/src/shared/runs/formatRunRow.ts +6 -0
  116. package/src/shared/state/daemonStateSchema.ts +13 -0
  117. package/src/shared/state/getDaemonStatePath.ts +6 -0
  118. package/src/shared/state/readDaemonState.ts +14 -0
  119. package/src/shared/state/removeDaemonState.ts +9 -0
  120. package/src/shared/state/writeDaemonState.ts +8 -0
  121. package/src/shared/utils/isRecord.ts +5 -0
  122. package/src/shared/version.ts +5 -0
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # crnd
2
+
3
+ Cron daemon built for agents. JSON output, no prompts, real OS processes.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ # npm/bun
9
+ bunx crnd
10
+
11
+ # or homebrew
12
+ brew install ysm-dev/crnd/crnd
13
+ ```
14
+
15
+ ## 30 seconds to your first job
16
+
17
+ ```sh
18
+ # start daemon (happens automatically, but let's be explicit)
19
+ crnd daemon start
20
+
21
+ # schedule a backup at 2am UTC daily
22
+ crnd schedule -n backup -s "0 2 * * *" -- rsync -a ~/docs ~/backup
23
+
24
+ # run it now
25
+ crnd run-once -n backup
26
+
27
+ # check status
28
+ crnd status -n backup
29
+ ```
30
+
31
+ That's it. Job definitions live in `~/.config/crnd/jobs.toml` - edit it directly and the daemon picks up changes.
32
+
33
+ ## Why crnd
34
+
35
+ Most cron tools are built for humans clicking around. crnd is built for scripts and agents that need to:
36
+
37
+ - Schedule jobs without interactive prompts
38
+ - Parse structured output (`crnd list --json`)
39
+ - Stream logs from running processes
40
+ - Kill/stop jobs by name
41
+ - Trust that jobs run as real OS processes (not some container abstraction)
42
+
43
+ Everything runs locally. No cloud, no Docker, no account, no network calls.
44
+
45
+ ## Commands
46
+
47
+ ```sh
48
+ crnd schedule -n NAME -s "CRON" -- command args # create/update job
49
+ crnd schedule -n NAME -a "ISO_TIMESTAMP" -- cmd # one-time job
50
+ crnd list # all jobs
51
+ crnd status -n NAME # job details
52
+ crnd runs -n NAME # run history
53
+ crnd logs -n NAME -s # stream stdout/stderr
54
+ crnd run-once -n NAME # trigger now
55
+ crnd pause -n NAME # pause scheduling
56
+ crnd resume -n NAME # resume
57
+ crnd stop -n NAME # graceful stop (SIGTERM)
58
+ crnd kill -n NAME # hard kill (SIGKILL)
59
+ crnd delete -n NAME -f # remove job
60
+ crnd export # dump jobs.toml
61
+ crnd import -f jobs.toml # load jobs
62
+ crnd daemon install # autostart on login
63
+ crnd doctor # check setup
64
+ ```
65
+
66
+ All commands support `--json` for machine-readable output.
67
+
68
+ ## jobs.toml
69
+
70
+ ```toml
71
+ [jobs.backup]
72
+ command = ["rsync", "-a", "/src", "/dst"]
73
+ schedule = "0 2 * * *"
74
+ timezone = "UTC"
75
+ timeout_ms = 600000
76
+
77
+ [jobs.deploy]
78
+ command = ["./deploy.sh"]
79
+ run_at = "2026-02-01T10:00:00Z"
80
+ ```
81
+
82
+ Edit this file directly. The daemon watches it and syncs automatically.
83
+
84
+ **Paths:**
85
+ - macOS: `~/Library/Application Support/crnd/`
86
+ - Linux: `~/.config/crnd/`
87
+ - Windows: `%APPDATA%\crnd\`
88
+
89
+ ## How it works
90
+
91
+ - Each job spawns a real OS process via `Bun.spawn`
92
+ - State lives in SQLite, job definitions in TOML
93
+ - Daemon runs per-user, binds to `127.0.0.1`
94
+ - Autostart via launchd (macOS), systemd user (Linux), or Task Scheduler (Windows)
95
+ - Stdout/stderr saved per-run in `state/runs/<jobId>/<runId>.out|.err`
96
+
97
+ ## Development
98
+
99
+ ```sh
100
+ bun install
101
+ bun run dev # start daemon in dev mode
102
+ bun run build # compile single binary
103
+ bun test
104
+ ```
105
+
106
+ ## License
107
+
108
+ MIT
@@ -0,0 +1,35 @@
1
+ CREATE TABLE IF NOT EXISTS "jobs" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "name" text NOT NULL UNIQUE,
4
+ "description" text,
5
+ "command" text NOT NULL,
6
+ "cwd" text,
7
+ "env" text,
8
+ "schedule_type" text NOT NULL,
9
+ "cron" text,
10
+ "run_at" text,
11
+ "timezone" text,
12
+ "overlap_policy" text NOT NULL DEFAULT 'skip',
13
+ "timeout_ms" integer,
14
+ "paused" integer NOT NULL DEFAULT 0,
15
+ "created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
16
+ "updated_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP,
17
+ "last_run_at" text,
18
+ "next_run_at" text
19
+ );
20
+
21
+ --> statement-breakpoint
22
+
23
+ CREATE TABLE IF NOT EXISTS "runs" (
24
+ "id" text PRIMARY KEY NOT NULL,
25
+ "job_id" text NOT NULL REFERENCES "jobs"("id"),
26
+ "status" text NOT NULL,
27
+ "pid" integer,
28
+ "exit_code" integer,
29
+ "signal" text,
30
+ "started_at" text,
31
+ "ended_at" text,
32
+ "stdout_path" text,
33
+ "stderr_path" text,
34
+ "error_message" text
35
+ );
@@ -0,0 +1,13 @@
1
+ CREATE TABLE IF NOT EXISTS "runs" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "job_id" text NOT NULL REFERENCES "jobs"("id"),
4
+ "status" text NOT NULL,
5
+ "pid" integer,
6
+ "exit_code" integer,
7
+ "signal" text,
8
+ "started_at" text,
9
+ "ended_at" text,
10
+ "stdout_path" text,
11
+ "stderr_path" text,
12
+ "error_message" text
13
+ );
@@ -0,0 +1,20 @@
1
+ {
2
+ "version": "5",
3
+ "dialect": "sqlite",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "0000",
8
+ "when": 1700000000000,
9
+ "tag": "0000_init",
10
+ "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "0001",
15
+ "when": 1700000001000,
16
+ "tag": "0001_add_runs",
17
+ "breakpoints": true
18
+ }
19
+ ]
20
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "crnd",
3
+ "version": "0.0.1",
4
+ "description": "Agent-first CLI for cron scheduling and process management",
5
+ "type": "module",
6
+ "bin": {
7
+ "crnd": "src/cli/main.ts"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "drizzle"
12
+ ],
13
+ "scripts": {
14
+ "dev": "bun run src/cli/main.ts",
15
+ "build": "bun build ./src/cli/main.ts --compile --outfile dist/crnd",
16
+ "lint": "biome lint .",
17
+ "format": "biome format . --write",
18
+ "check": "biome check .",
19
+ "typecheck": "tsgo --noEmit",
20
+ "test": "bun test"
21
+ },
22
+ "keywords": [
23
+ "cron",
24
+ "scheduler",
25
+ "daemon",
26
+ "jobs",
27
+ "cli",
28
+ "process-manager"
29
+ ],
30
+ "author": "ysm-dev",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/ysm-dev/crnd.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/ysm-dev/crnd/issues"
38
+ },
39
+ "homepage": "https://github.com/ysm-dev/crnd#readme",
40
+ "dependencies": {
41
+ "@hono/zod-validator": "^0.7.6",
42
+ "@iarna/toml": "^2.2.5",
43
+ "citty": "^0.2.0",
44
+ "consola": "^3.4.2",
45
+ "croner": "^9.1.0",
46
+ "drizzle-orm": "^0.45.1",
47
+ "drizzle-zod": "^0.8.3",
48
+ "env-paths": "^4.0.0",
49
+ "hono": "^4.11.7",
50
+ "ulid": "^3.0.2",
51
+ "zod": "^4.3.6"
52
+ },
53
+ "devDependencies": {
54
+ "@biomejs/biome": "^2.3.13",
55
+ "@typescript/native-preview": "7.0.0-dev.20260131.1",
56
+ "bun-types": "^1.3.8",
57
+ "drizzle-kit": "^0.31.8"
58
+ }
59
+ }
@@ -0,0 +1,93 @@
1
+ import { defineCommand } from "citty";
2
+ import createRpcClient from "../../shared/rpc/createRpcClient";
3
+
4
+ export default function createDeleteCommand() {
5
+ return defineCommand({
6
+ meta: {
7
+ name: "delete",
8
+ description: "Delete a job",
9
+ },
10
+ args: {
11
+ name: {
12
+ type: "string",
13
+ alias: "n",
14
+ required: true,
15
+ },
16
+ force: {
17
+ type: "boolean",
18
+ alias: "f",
19
+ },
20
+ json: {
21
+ type: "boolean",
22
+ alias: "j",
23
+ },
24
+ },
25
+ async run({ args }) {
26
+ if (!args.force) {
27
+ const payload = { status: "missing_force" };
28
+ if (!process.stdout.isTTY || args.json) {
29
+ console.log(JSON.stringify(payload));
30
+ } else {
31
+ console.log("delete: requires --force");
32
+ }
33
+ process.exitCode = 2;
34
+ return;
35
+ }
36
+
37
+ const client = createRpcClient();
38
+ if (!client) {
39
+ const payload = { status: "unreachable" };
40
+ if (!process.stdout.isTTY || args.json) {
41
+ console.log(JSON.stringify(payload));
42
+ } else {
43
+ console.log("daemon: unreachable");
44
+ }
45
+ process.exitCode = 3;
46
+ return;
47
+ }
48
+
49
+ try {
50
+ const res = await client.jobs[":name"].$delete({
51
+ param: { name: args.name },
52
+ });
53
+ if (res.status === 404) {
54
+ const payload = { status: "not_found" };
55
+ if (!process.stdout.isTTY || args.json) {
56
+ console.log(JSON.stringify(payload));
57
+ } else {
58
+ console.log("delete: job not found");
59
+ }
60
+ process.exitCode = 1;
61
+ return;
62
+ }
63
+
64
+ if (!res.ok) {
65
+ const payload = { status: "error", code: res.status };
66
+ if (!process.stdout.isTTY || args.json) {
67
+ console.log(JSON.stringify(payload));
68
+ } else {
69
+ console.log(`delete: error (${res.status})`);
70
+ }
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+
75
+ const data = await res.json();
76
+ if (!process.stdout.isTTY || args.json) {
77
+ console.log(JSON.stringify(data));
78
+ return;
79
+ }
80
+
81
+ console.log(`delete: removed (${data.jobId})`);
82
+ } catch {
83
+ const payload = { status: "unreachable" };
84
+ if (!process.stdout.isTTY || args.json) {
85
+ console.log(JSON.stringify(payload));
86
+ } else {
87
+ console.log("daemon: unreachable");
88
+ }
89
+ process.exitCode = 3;
90
+ }
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,93 @@
1
+ import { accessSync, constants, existsSync } from "node:fs";
2
+ import { defineCommand } from "citty";
3
+ import getAutostartPath from "../../daemon/autostart/getAutostartPath";
4
+ import getJobsTomlPath from "../../shared/paths/getJobsTomlPath";
5
+ import createRpcClient from "../../shared/rpc/createRpcClient";
6
+
7
+ export default function createDoctorCommand() {
8
+ return defineCommand({
9
+ meta: {
10
+ name: "doctor",
11
+ description: "Check crnd health",
12
+ },
13
+ args: {
14
+ json: {
15
+ type: "boolean",
16
+ alias: "j",
17
+ },
18
+ },
19
+ async run({ args }) {
20
+ const results: Array<{ check: string; ok: boolean; detail?: string }> =
21
+ [];
22
+ const client = createRpcClient();
23
+
24
+ if (!client) {
25
+ results.push({ check: "daemon", ok: false, detail: "unreachable" });
26
+ } else {
27
+ try {
28
+ const res = await client.health.$get();
29
+ results.push({
30
+ check: "daemon",
31
+ ok: res.ok,
32
+ detail: res.ok ? "running" : `status ${res.status}`,
33
+ });
34
+ } catch {
35
+ results.push({ check: "daemon", ok: false, detail: "unreachable" });
36
+ }
37
+ }
38
+
39
+ const jobsToml = getJobsTomlPath();
40
+ if (!existsSync(jobsToml)) {
41
+ results.push({ check: "jobs.toml", ok: false, detail: "missing" });
42
+ } else {
43
+ try {
44
+ accessSync(jobsToml, constants.R_OK | constants.W_OK);
45
+ results.push({ check: "jobs.toml", ok: true });
46
+ } catch {
47
+ results.push({
48
+ check: "jobs.toml",
49
+ ok: false,
50
+ detail: "not_readable",
51
+ });
52
+ }
53
+ }
54
+
55
+ const autostartPath = getAutostartPath();
56
+ if (!autostartPath) {
57
+ results.push({ check: "autostart", ok: false, detail: "unsupported" });
58
+ } else if (process.platform === "win32") {
59
+ const result = Bun.spawnSync([
60
+ "schtasks",
61
+ "/Query",
62
+ "/TN",
63
+ autostartPath,
64
+ ]);
65
+ results.push({
66
+ check: "autostart",
67
+ ok: result.success,
68
+ detail: "task",
69
+ });
70
+ } else {
71
+ results.push({
72
+ check: "autostart",
73
+ ok: existsSync(autostartPath),
74
+ detail: autostartPath,
75
+ });
76
+ }
77
+
78
+ const ok = results.every((item) => item.ok);
79
+ if (!process.stdout.isTTY || args.json) {
80
+ console.log(JSON.stringify({ ok, results }));
81
+ } else {
82
+ for (const item of results) {
83
+ const detail = item.detail ? ` (${item.detail})` : "";
84
+ console.log(`${item.check}: ${item.ok ? "ok" : "fail"}${detail}`);
85
+ }
86
+ }
87
+
88
+ if (!ok) {
89
+ process.exitCode = 1;
90
+ }
91
+ },
92
+ });
93
+ }
@@ -0,0 +1,75 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { defineCommand } from "citty";
3
+ import createRpcClient from "../../shared/rpc/createRpcClient";
4
+
5
+ export default function createExportCommand() {
6
+ return defineCommand({
7
+ meta: {
8
+ name: "export",
9
+ description: "Export jobs to TOML",
10
+ },
11
+ args: {
12
+ output: {
13
+ type: "string",
14
+ alias: "o",
15
+ },
16
+ json: {
17
+ type: "boolean",
18
+ alias: "j",
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const client = createRpcClient();
23
+ if (!client) {
24
+ const payload = { status: "unreachable" };
25
+ if (!process.stdout.isTTY || args.json) {
26
+ console.log(JSON.stringify(payload));
27
+ } else {
28
+ console.log("daemon: unreachable");
29
+ }
30
+ process.exitCode = 3;
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const res = await client.export.$post();
36
+ if (!res.ok) {
37
+ const payload = { status: "error", code: res.status };
38
+ if (!process.stdout.isTTY || args.json) {
39
+ console.log(JSON.stringify(payload));
40
+ } else {
41
+ console.log(`export: error (${res.status})`);
42
+ }
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ const data = await res.json();
48
+ if (args.output) {
49
+ writeFileSync(args.output, data.toml, "utf-8");
50
+ if (!process.stdout.isTTY || args.json) {
51
+ console.log(JSON.stringify({ ok: true, output: args.output }));
52
+ } else {
53
+ console.log(`export: wrote ${args.output}`);
54
+ }
55
+ return;
56
+ }
57
+
58
+ if (!process.stdout.isTTY || args.json) {
59
+ console.log(JSON.stringify(data));
60
+ return;
61
+ }
62
+
63
+ console.log(data.toml);
64
+ } catch {
65
+ const payload = { status: "unreachable" };
66
+ if (!process.stdout.isTTY || args.json) {
67
+ console.log(JSON.stringify(payload));
68
+ } else {
69
+ console.log("daemon: unreachable");
70
+ }
71
+ process.exitCode = 3;
72
+ }
73
+ },
74
+ });
75
+ }
@@ -0,0 +1,80 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { defineCommand } from "citty";
3
+ import createRpcClient from "../../shared/rpc/createRpcClient";
4
+
5
+ export default function createImportCommand() {
6
+ return defineCommand({
7
+ meta: {
8
+ name: "import",
9
+ description: "Import jobs from TOML",
10
+ },
11
+ args: {
12
+ file: {
13
+ type: "string",
14
+ alias: "f",
15
+ required: true,
16
+ },
17
+ json: {
18
+ type: "boolean",
19
+ alias: "j",
20
+ },
21
+ },
22
+ async run({ args }) {
23
+ const client = createRpcClient();
24
+ if (!client) {
25
+ const payload = { status: "unreachable" };
26
+ if (!process.stdout.isTTY || args.json) {
27
+ console.log(JSON.stringify(payload));
28
+ } else {
29
+ console.log("daemon: unreachable");
30
+ }
31
+ process.exitCode = 3;
32
+ return;
33
+ }
34
+
35
+ let toml: string;
36
+ try {
37
+ toml = readFileSync(args.file, "utf-8");
38
+ } catch {
39
+ const payload = { status: "missing_file" };
40
+ if (!process.stdout.isTTY || args.json) {
41
+ console.log(JSON.stringify(payload));
42
+ } else {
43
+ console.log("import: failed to read file");
44
+ }
45
+ process.exitCode = 2;
46
+ return;
47
+ }
48
+
49
+ try {
50
+ const res = await client.import.$post({ json: { toml } });
51
+ if (!res.ok) {
52
+ const payload = { status: "error", code: res.status };
53
+ if (!process.stdout.isTTY || args.json) {
54
+ console.log(JSON.stringify(payload));
55
+ } else {
56
+ console.log(`import: error (${res.status})`);
57
+ }
58
+ process.exitCode = 1;
59
+ return;
60
+ }
61
+
62
+ const data = await res.json();
63
+ if (!process.stdout.isTTY || args.json) {
64
+ console.log(JSON.stringify(data));
65
+ return;
66
+ }
67
+
68
+ console.log("import: ok");
69
+ } catch {
70
+ const payload = { status: "unreachable" };
71
+ if (!process.stdout.isTTY || args.json) {
72
+ console.log(JSON.stringify(payload));
73
+ } else {
74
+ console.log("daemon: unreachable");
75
+ }
76
+ process.exitCode = 3;
77
+ }
78
+ },
79
+ });
80
+ }
@@ -0,0 +1,89 @@
1
+ import { defineCommand } from "citty";
2
+ import createRpcClient from "../../shared/rpc/createRpcClient";
3
+
4
+ export default function createKillCommand() {
5
+ return defineCommand({
6
+ meta: {
7
+ name: "kill",
8
+ description: "Kill a running job",
9
+ },
10
+ args: {
11
+ name: {
12
+ type: "string",
13
+ alias: "n",
14
+ required: true,
15
+ },
16
+ json: {
17
+ type: "boolean",
18
+ alias: "j",
19
+ },
20
+ },
21
+ async run({ args }) {
22
+ const client = createRpcClient();
23
+ if (!client) {
24
+ const payload = { status: "unreachable" };
25
+ if (!process.stdout.isTTY || args.json) {
26
+ console.log(JSON.stringify(payload));
27
+ } else {
28
+ console.log("daemon: unreachable");
29
+ }
30
+ process.exitCode = 3;
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const res = await client.jobs[":name"].kill.$post({
36
+ param: { name: args.name },
37
+ });
38
+ if (res.status === 404) {
39
+ const payload = { status: "not_found" };
40
+ if (!process.stdout.isTTY || args.json) {
41
+ console.log(JSON.stringify(payload));
42
+ } else {
43
+ console.log("kill: job not found");
44
+ }
45
+ process.exitCode = 1;
46
+ return;
47
+ }
48
+
49
+ if (res.status === 409) {
50
+ const payload = { status: "not_running" };
51
+ if (!process.stdout.isTTY || args.json) {
52
+ console.log(JSON.stringify(payload));
53
+ } else {
54
+ console.log("kill: no running job");
55
+ }
56
+ process.exitCode = 1;
57
+ return;
58
+ }
59
+
60
+ if (!res.ok) {
61
+ const payload = { status: "error", code: res.status };
62
+ if (!process.stdout.isTTY || args.json) {
63
+ console.log(JSON.stringify(payload));
64
+ } else {
65
+ console.log(`kill: error (${res.status})`);
66
+ }
67
+ process.exitCode = 1;
68
+ return;
69
+ }
70
+
71
+ const data = await res.json();
72
+ if (!process.stdout.isTTY || args.json) {
73
+ console.log(JSON.stringify(data));
74
+ return;
75
+ }
76
+
77
+ console.log(`kill: requested (${data.runId})`);
78
+ } catch {
79
+ const payload = { status: "unreachable" };
80
+ if (!process.stdout.isTTY || args.json) {
81
+ console.log(JSON.stringify(payload));
82
+ } else {
83
+ console.log("daemon: unreachable");
84
+ }
85
+ process.exitCode = 3;
86
+ }
87
+ },
88
+ });
89
+ }