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.
- package/README.md +108 -0
- package/drizzle/0000_init.sql +35 -0
- package/drizzle/0001_add_runs.sql +13 -0
- package/drizzle/meta/_journal.json +20 -0
- package/package.json +59 -0
- package/src/cli/commands/createDeleteCommand.ts +93 -0
- package/src/cli/commands/createDoctorCommand.ts +93 -0
- package/src/cli/commands/createExportCommand.ts +75 -0
- package/src/cli/commands/createImportCommand.ts +80 -0
- package/src/cli/commands/createKillCommand.ts +89 -0
- package/src/cli/commands/createListCommand.ts +67 -0
- package/src/cli/commands/createLogsCommand.ts +125 -0
- package/src/cli/commands/createPauseCommand.ts +78 -0
- package/src/cli/commands/createResetCommand.ts +78 -0
- package/src/cli/commands/createResumeCommand.ts +78 -0
- package/src/cli/commands/createRootCommand.ts +50 -0
- package/src/cli/commands/createRunOnceCommand.ts +77 -0
- package/src/cli/commands/createRunsCommand.ts +106 -0
- package/src/cli/commands/createScheduleCommand.ts +184 -0
- package/src/cli/commands/createShowCommand.ts +92 -0
- package/src/cli/commands/createStatusCommand.ts +145 -0
- package/src/cli/commands/createStopCommand.ts +89 -0
- package/src/cli/commands/createUpdateCommand.ts +13 -0
- package/src/cli/commands/daemon/createDaemonCommand.ts +24 -0
- package/src/cli/commands/daemon/createDaemonInstallCommand.ts +144 -0
- package/src/cli/commands/daemon/createDaemonServeCommand.ts +14 -0
- package/src/cli/commands/daemon/createDaemonStartCommand.ts +73 -0
- package/src/cli/commands/daemon/createDaemonStatusCommand.ts +5 -0
- package/src/cli/commands/daemon/createDaemonStopCommand.ts +59 -0
- package/src/cli/commands/daemon/createDaemonUninstallCommand.ts +99 -0
- package/src/cli/commands/daemon/createLaunchdPlist.ts +34 -0
- package/src/cli/commands/daemon/createSystemdService.ts +24 -0
- package/src/cli/commands/daemon/escapeXml.ts +8 -0
- package/src/cli/commands/daemon/getDaemonServiceArgs.ts +10 -0
- package/src/cli/commands/daemon/quoteWindowsArg.ts +4 -0
- package/src/cli/commands/getCommandArgs.ts +8 -0
- package/src/cli/commands/parseEnvArgs.ts +28 -0
- package/src/cli/getDaemonSpawnArgs.ts +9 -0
- package/src/cli/main.ts +8 -0
- package/src/daemon/autostart/ensureAutostart.ts +30 -0
- package/src/daemon/autostart/getAutostartPath.ts +29 -0
- package/src/daemon/autostart/getDaemonInstallArgs.ts +10 -0
- package/src/daemon/createLogger.ts +13 -0
- package/src/daemon/createShutdownHandler.ts +29 -0
- package/src/daemon/jobs/createJobsFileSync.ts +113 -0
- package/src/daemon/jobs/deleteJobByName.ts +18 -0
- package/src/daemon/jobs/upsertJob.ts +85 -0
- package/src/daemon/main.ts +64 -0
- package/src/daemon/runner/createRunOutputFds.ts +18 -0
- package/src/daemon/runner/getRunStatus.ts +14 -0
- package/src/daemon/runner/recordSkippedRun.ts +27 -0
- package/src/daemon/runner/recoverRunningRuns.ts +36 -0
- package/src/daemon/runner/runJob.ts +94 -0
- package/src/daemon/scheduler/createScheduler.ts +45 -0
- package/src/daemon/scheduler/createSchedulerState.ts +8 -0
- package/src/daemon/scheduler/loadJobs.ts +10 -0
- package/src/daemon/scheduler/runJobWithTracking.ts +48 -0
- package/src/daemon/scheduler/scheduleJob.ts +32 -0
- package/src/daemon/scheduler/unscheduleJob.ts +11 -0
- package/src/daemon/scheduler/updateNextRunAt.ts +14 -0
- package/src/daemon/server/createApp.ts +76 -0
- package/src/daemon/server/createAuthMiddleware.ts +16 -0
- package/src/daemon/server/routes/registerExportRoute.ts +21 -0
- package/src/daemon/server/routes/registerHealthRoute.ts +16 -0
- package/src/daemon/server/routes/registerImportRoute.ts +22 -0
- package/src/daemon/server/routes/registerJobRunsRoute.ts +46 -0
- package/src/daemon/server/routes/registerJobsDeleteRoute.ts +37 -0
- package/src/daemon/server/routes/registerJobsGetRoute.ts +29 -0
- package/src/daemon/server/routes/registerJobsKillRoute.ts +45 -0
- package/src/daemon/server/routes/registerJobsListRoute.ts +13 -0
- package/src/daemon/server/routes/registerJobsPauseRoute.ts +53 -0
- package/src/daemon/server/routes/registerJobsResetRoute.ts +54 -0
- package/src/daemon/server/routes/registerJobsResumeRoute.ts +53 -0
- package/src/daemon/server/routes/registerJobsRunRoute.ts +37 -0
- package/src/daemon/server/routes/registerJobsStopRoute.ts +45 -0
- package/src/daemon/server/routes/registerJobsUpsertRoute.ts +30 -0
- package/src/daemon/server/routes/registerRunGetRoute.ts +25 -0
- package/src/daemon/server/routes/registerRunLogsRoute.ts +32 -0
- package/src/daemon/server/routes/registerShutdownRoute.ts +9 -0
- package/src/daemon/server/startServer.ts +23 -0
- package/src/db/getMigrationsDir.ts +5 -0
- package/src/db/migrateDatabase.ts +21 -0
- package/src/db/openDatabase.ts +12 -0
- package/src/db/schema/jobs.ts +26 -0
- package/src/db/schema/jobsSchemas.ts +10 -0
- package/src/db/schema/runs.ts +23 -0
- package/src/db/schema/runsSchemas.ts +10 -0
- package/src/db/schema.ts +5 -0
- package/src/shared/auth/createToken.ts +5 -0
- package/src/shared/events/appendEvent.ts +16 -0
- package/src/shared/jobs/createCommandSchema.ts +5 -0
- package/src/shared/jobs/createEnvSchema.ts +5 -0
- package/src/shared/jobs/createJobInputSchema.ts +31 -0
- package/src/shared/jobs/createTomlJobSchema.ts +31 -0
- package/src/shared/jobs/formatJobRow.ts +14 -0
- package/src/shared/jobs/isOverlapPolicy.ts +5 -0
- package/src/shared/jobs/parseCommand.ts +6 -0
- package/src/shared/jobs/parseEnv.ts +10 -0
- package/src/shared/jobs/parseJobsToml.ts +36 -0
- package/src/shared/jobs/readJobsToml.ts +17 -0
- package/src/shared/jobs/serializeCommand.ts +6 -0
- package/src/shared/jobs/serializeEnv.ts +6 -0
- package/src/shared/jobs/serializeJobsToml.ts +45 -0
- package/src/shared/jobs/writeJobsToml.ts +12 -0
- package/src/shared/paths/ensureDir.ts +6 -0
- package/src/shared/paths/getConfigDir.ts +6 -0
- package/src/shared/paths/getEventsPath.ts +6 -0
- package/src/shared/paths/getJobRunsDir.ts +7 -0
- package/src/shared/paths/getJobsTomlPath.ts +6 -0
- package/src/shared/paths/getPaths.ts +16 -0
- package/src/shared/paths/getRunOutputPaths.ts +10 -0
- package/src/shared/paths/getRunsDir.ts +7 -0
- package/src/shared/paths/getStateDir.ts +8 -0
- package/src/shared/rpc/createRpcClient.ts +20 -0
- package/src/shared/runs/formatRunRow.ts +6 -0
- package/src/shared/state/daemonStateSchema.ts +13 -0
- package/src/shared/state/getDaemonStatePath.ts +6 -0
- package/src/shared/state/readDaemonState.ts +14 -0
- package/src/shared/state/removeDaemonState.ts +9 -0
- package/src/shared/state/writeDaemonState.ts +8 -0
- package/src/shared/utils/isRecord.ts +5 -0
- 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
|
+
}
|