crnd 0.0.1 → 0.0.4

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/bin/crnd +88 -0
  2. package/package.json +19 -43
  3. package/drizzle/0000_init.sql +0 -35
  4. package/drizzle/0001_add_runs.sql +0 -13
  5. package/drizzle/meta/_journal.json +0 -20
  6. package/src/cli/commands/createDeleteCommand.ts +0 -93
  7. package/src/cli/commands/createDoctorCommand.ts +0 -93
  8. package/src/cli/commands/createExportCommand.ts +0 -75
  9. package/src/cli/commands/createImportCommand.ts +0 -80
  10. package/src/cli/commands/createKillCommand.ts +0 -89
  11. package/src/cli/commands/createListCommand.ts +0 -67
  12. package/src/cli/commands/createLogsCommand.ts +0 -125
  13. package/src/cli/commands/createPauseCommand.ts +0 -78
  14. package/src/cli/commands/createResetCommand.ts +0 -78
  15. package/src/cli/commands/createResumeCommand.ts +0 -78
  16. package/src/cli/commands/createRootCommand.ts +0 -50
  17. package/src/cli/commands/createRunOnceCommand.ts +0 -77
  18. package/src/cli/commands/createRunsCommand.ts +0 -106
  19. package/src/cli/commands/createScheduleCommand.ts +0 -184
  20. package/src/cli/commands/createShowCommand.ts +0 -92
  21. package/src/cli/commands/createStatusCommand.ts +0 -145
  22. package/src/cli/commands/createStopCommand.ts +0 -89
  23. package/src/cli/commands/createUpdateCommand.ts +0 -13
  24. package/src/cli/commands/daemon/createDaemonCommand.ts +0 -24
  25. package/src/cli/commands/daemon/createDaemonInstallCommand.ts +0 -144
  26. package/src/cli/commands/daemon/createDaemonServeCommand.ts +0 -14
  27. package/src/cli/commands/daemon/createDaemonStartCommand.ts +0 -73
  28. package/src/cli/commands/daemon/createDaemonStatusCommand.ts +0 -5
  29. package/src/cli/commands/daemon/createDaemonStopCommand.ts +0 -59
  30. package/src/cli/commands/daemon/createDaemonUninstallCommand.ts +0 -99
  31. package/src/cli/commands/daemon/createLaunchdPlist.ts +0 -34
  32. package/src/cli/commands/daemon/createSystemdService.ts +0 -24
  33. package/src/cli/commands/daemon/escapeXml.ts +0 -8
  34. package/src/cli/commands/daemon/getDaemonServiceArgs.ts +0 -10
  35. package/src/cli/commands/daemon/quoteWindowsArg.ts +0 -4
  36. package/src/cli/commands/getCommandArgs.ts +0 -8
  37. package/src/cli/commands/parseEnvArgs.ts +0 -28
  38. package/src/cli/getDaemonSpawnArgs.ts +0 -9
  39. package/src/cli/main.ts +0 -8
  40. package/src/daemon/autostart/ensureAutostart.ts +0 -30
  41. package/src/daemon/autostart/getAutostartPath.ts +0 -29
  42. package/src/daemon/autostart/getDaemonInstallArgs.ts +0 -10
  43. package/src/daemon/createLogger.ts +0 -13
  44. package/src/daemon/createShutdownHandler.ts +0 -29
  45. package/src/daemon/jobs/createJobsFileSync.ts +0 -113
  46. package/src/daemon/jobs/deleteJobByName.ts +0 -18
  47. package/src/daemon/jobs/upsertJob.ts +0 -85
  48. package/src/daemon/main.ts +0 -64
  49. package/src/daemon/runner/createRunOutputFds.ts +0 -18
  50. package/src/daemon/runner/getRunStatus.ts +0 -14
  51. package/src/daemon/runner/recordSkippedRun.ts +0 -27
  52. package/src/daemon/runner/recoverRunningRuns.ts +0 -36
  53. package/src/daemon/runner/runJob.ts +0 -94
  54. package/src/daemon/scheduler/createScheduler.ts +0 -45
  55. package/src/daemon/scheduler/createSchedulerState.ts +0 -8
  56. package/src/daemon/scheduler/loadJobs.ts +0 -10
  57. package/src/daemon/scheduler/runJobWithTracking.ts +0 -48
  58. package/src/daemon/scheduler/scheduleJob.ts +0 -32
  59. package/src/daemon/scheduler/unscheduleJob.ts +0 -11
  60. package/src/daemon/scheduler/updateNextRunAt.ts +0 -14
  61. package/src/daemon/server/createApp.ts +0 -76
  62. package/src/daemon/server/createAuthMiddleware.ts +0 -16
  63. package/src/daemon/server/routes/registerExportRoute.ts +0 -21
  64. package/src/daemon/server/routes/registerHealthRoute.ts +0 -16
  65. package/src/daemon/server/routes/registerImportRoute.ts +0 -22
  66. package/src/daemon/server/routes/registerJobRunsRoute.ts +0 -46
  67. package/src/daemon/server/routes/registerJobsDeleteRoute.ts +0 -37
  68. package/src/daemon/server/routes/registerJobsGetRoute.ts +0 -29
  69. package/src/daemon/server/routes/registerJobsKillRoute.ts +0 -45
  70. package/src/daemon/server/routes/registerJobsListRoute.ts +0 -13
  71. package/src/daemon/server/routes/registerJobsPauseRoute.ts +0 -53
  72. package/src/daemon/server/routes/registerJobsResetRoute.ts +0 -54
  73. package/src/daemon/server/routes/registerJobsResumeRoute.ts +0 -53
  74. package/src/daemon/server/routes/registerJobsRunRoute.ts +0 -37
  75. package/src/daemon/server/routes/registerJobsStopRoute.ts +0 -45
  76. package/src/daemon/server/routes/registerJobsUpsertRoute.ts +0 -30
  77. package/src/daemon/server/routes/registerRunGetRoute.ts +0 -25
  78. package/src/daemon/server/routes/registerRunLogsRoute.ts +0 -32
  79. package/src/daemon/server/routes/registerShutdownRoute.ts +0 -9
  80. package/src/daemon/server/startServer.ts +0 -23
  81. package/src/db/getMigrationsDir.ts +0 -5
  82. package/src/db/migrateDatabase.ts +0 -21
  83. package/src/db/openDatabase.ts +0 -12
  84. package/src/db/schema/jobs.ts +0 -26
  85. package/src/db/schema/jobsSchemas.ts +0 -10
  86. package/src/db/schema/runs.ts +0 -23
  87. package/src/db/schema/runsSchemas.ts +0 -10
  88. package/src/db/schema.ts +0 -5
  89. package/src/shared/auth/createToken.ts +0 -5
  90. package/src/shared/events/appendEvent.ts +0 -16
  91. package/src/shared/jobs/createCommandSchema.ts +0 -5
  92. package/src/shared/jobs/createEnvSchema.ts +0 -5
  93. package/src/shared/jobs/createJobInputSchema.ts +0 -31
  94. package/src/shared/jobs/createTomlJobSchema.ts +0 -31
  95. package/src/shared/jobs/formatJobRow.ts +0 -14
  96. package/src/shared/jobs/isOverlapPolicy.ts +0 -5
  97. package/src/shared/jobs/parseCommand.ts +0 -6
  98. package/src/shared/jobs/parseEnv.ts +0 -10
  99. package/src/shared/jobs/parseJobsToml.ts +0 -36
  100. package/src/shared/jobs/readJobsToml.ts +0 -17
  101. package/src/shared/jobs/serializeCommand.ts +0 -6
  102. package/src/shared/jobs/serializeEnv.ts +0 -6
  103. package/src/shared/jobs/serializeJobsToml.ts +0 -45
  104. package/src/shared/jobs/writeJobsToml.ts +0 -12
  105. package/src/shared/paths/ensureDir.ts +0 -6
  106. package/src/shared/paths/getConfigDir.ts +0 -6
  107. package/src/shared/paths/getEventsPath.ts +0 -6
  108. package/src/shared/paths/getJobRunsDir.ts +0 -7
  109. package/src/shared/paths/getJobsTomlPath.ts +0 -6
  110. package/src/shared/paths/getPaths.ts +0 -16
  111. package/src/shared/paths/getRunOutputPaths.ts +0 -10
  112. package/src/shared/paths/getRunsDir.ts +0 -7
  113. package/src/shared/paths/getStateDir.ts +0 -8
  114. package/src/shared/rpc/createRpcClient.ts +0 -20
  115. package/src/shared/runs/formatRunRow.ts +0 -6
  116. package/src/shared/state/daemonStateSchema.ts +0 -13
  117. package/src/shared/state/getDaemonStatePath.ts +0 -6
  118. package/src/shared/state/readDaemonState.ts +0 -14
  119. package/src/shared/state/removeDaemonState.ts +0 -9
  120. package/src/shared/state/writeDaemonState.ts +0 -8
  121. package/src/shared/utils/isRecord.ts +0 -5
  122. package/src/shared/version.ts +0 -5
@@ -1,73 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import createRpcClient from "../../../shared/rpc/createRpcClient";
3
- import removeDaemonState from "../../../shared/state/removeDaemonState";
4
- import getDaemonSpawnArgs from "../../getDaemonSpawnArgs";
5
-
6
- export default function createDaemonStartCommand() {
7
- return defineCommand({
8
- meta: {
9
- name: "start",
10
- description: "Start the crnd daemon",
11
- },
12
- args: {
13
- json: {
14
- type: "boolean",
15
- alias: "j",
16
- },
17
- },
18
- async run({ args }) {
19
- const existing = createRpcClient();
20
- if (existing) {
21
- try {
22
- const res = await existing.health.$get();
23
- if (res.ok) {
24
- const payload = { status: "already_running" };
25
- if (!process.stdout.isTTY || args.json) {
26
- console.log(JSON.stringify(payload));
27
- } else {
28
- console.log("daemon: already running");
29
- }
30
- return;
31
- }
32
- } catch {
33
- removeDaemonState();
34
- }
35
- }
36
-
37
- const proc = Bun.spawn(getDaemonSpawnArgs(), {
38
- stdin: "ignore",
39
- stdout: "ignore",
40
- stderr: "ignore",
41
- });
42
- proc.unref();
43
-
44
- for (let attempt = 0; attempt < 20; attempt += 1) {
45
- await new Promise((resolve) => setTimeout(resolve, 100));
46
- const client = createRpcClient();
47
- if (!client) {
48
- continue;
49
- }
50
- try {
51
- const res = await client.health.$get();
52
- if (res.ok) {
53
- const data = await res.json();
54
- if (!process.stdout.isTTY || args.json) {
55
- console.log(JSON.stringify({ status: "started", daemon: data }));
56
- } else {
57
- console.log("daemon: started");
58
- }
59
- return;
60
- }
61
- } catch {}
62
- }
63
-
64
- const payload = { status: "start_timeout" };
65
- if (!process.stdout.isTTY || args.json) {
66
- console.log(JSON.stringify(payload));
67
- } else {
68
- console.log("daemon: start timeout");
69
- }
70
- process.exitCode = 3;
71
- },
72
- });
73
- }
@@ -1,5 +0,0 @@
1
- import createStatusCommand from "../createStatusCommand";
2
-
3
- export default function createDaemonStatusCommand() {
4
- return createStatusCommand();
5
- }
@@ -1,59 +0,0 @@
1
- import { defineCommand } from "citty";
2
- import createRpcClient from "../../../shared/rpc/createRpcClient";
3
-
4
- export default function createDaemonStopCommand() {
5
- return defineCommand({
6
- meta: {
7
- name: "stop",
8
- description: "Stop the crnd daemon",
9
- },
10
- args: {
11
- json: {
12
- type: "boolean",
13
- alias: "j",
14
- },
15
- },
16
- async run({ args }) {
17
- const client = createRpcClient();
18
- if (!client) {
19
- const payload = { status: "not_running" };
20
- if (!process.stdout.isTTY || args.json) {
21
- console.log(JSON.stringify(payload));
22
- } else {
23
- console.log("daemon: not running");
24
- }
25
- process.exitCode = 3;
26
- return;
27
- }
28
-
29
- try {
30
- const res = await client.daemon.shutdown.$post();
31
- if (!res.ok) {
32
- const payload = { status: "stop_failed", code: res.status };
33
- if (!process.stdout.isTTY || args.json) {
34
- console.log(JSON.stringify(payload));
35
- } else {
36
- console.log(`daemon: stop failed (${res.status})`);
37
- }
38
- process.exitCode = 3;
39
- return;
40
- }
41
-
42
- const payload = { status: "stopped" };
43
- if (!process.stdout.isTTY || args.json) {
44
- console.log(JSON.stringify(payload));
45
- } else {
46
- console.log("daemon: stopped");
47
- }
48
- } catch {
49
- const payload = { status: "stop_failed" };
50
- if (!process.stdout.isTTY || args.json) {
51
- console.log(JSON.stringify(payload));
52
- } else {
53
- console.log("daemon: stop failed");
54
- }
55
- process.exitCode = 3;
56
- }
57
- },
58
- });
59
- }
@@ -1,99 +0,0 @@
1
- import { existsSync, unlinkSync } from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { defineCommand } from "citty";
5
-
6
- export default function createDaemonUninstallCommand() {
7
- return defineCommand({
8
- meta: {
9
- name: "uninstall",
10
- description: "Remove auto-start service",
11
- },
12
- args: {
13
- json: {
14
- type: "boolean",
15
- alias: "j",
16
- },
17
- },
18
- run({ args }) {
19
- if (process.env.CRND_AUTOSTART_DRY_RUN === "1") {
20
- const payload = { ok: true, dryRun: true };
21
- if (!process.stdout.isTTY || args.json) {
22
- console.log(JSON.stringify(payload));
23
- } else {
24
- console.log("daemon: uninstall dry run");
25
- }
26
- return;
27
- }
28
-
29
- const platform = process.platform;
30
-
31
- if (platform === "darwin") {
32
- const plistPath = path.join(
33
- os.homedir(),
34
- "Library",
35
- "LaunchAgents",
36
- "com.crnd.daemon.plist",
37
- );
38
- Bun.spawnSync(["launchctl", "unload", plistPath]);
39
- if (existsSync(plistPath)) {
40
- unlinkSync(plistPath);
41
- }
42
-
43
- if (!process.stdout.isTTY || args.json) {
44
- console.log(JSON.stringify({ ok: true, path: plistPath }));
45
- } else {
46
- console.log(`daemon: uninstalled (${plistPath})`);
47
- }
48
- return;
49
- }
50
-
51
- if (platform === "linux") {
52
- const servicePath = path.join(
53
- os.homedir(),
54
- ".config",
55
- "systemd",
56
- "user",
57
- "crnd.service",
58
- );
59
- Bun.spawnSync([
60
- "systemctl",
61
- "--user",
62
- "disable",
63
- "--now",
64
- "crnd.service",
65
- ]);
66
- Bun.spawnSync(["systemctl", "--user", "daemon-reload"]);
67
- if (existsSync(servicePath)) {
68
- unlinkSync(servicePath);
69
- }
70
-
71
- if (!process.stdout.isTTY || args.json) {
72
- console.log(JSON.stringify({ ok: true, path: servicePath }));
73
- } else {
74
- console.log(`daemon: uninstalled (${servicePath})`);
75
- }
76
- return;
77
- }
78
-
79
- if (platform === "win32") {
80
- const taskName = "crnd";
81
- Bun.spawnSync(["schtasks", "/Delete", "/TN", taskName, "/F"]);
82
- if (!process.stdout.isTTY || args.json) {
83
- console.log(JSON.stringify({ ok: true, task: taskName }));
84
- } else {
85
- console.log(`daemon: uninstalled (${taskName})`);
86
- }
87
- return;
88
- }
89
-
90
- const payload = { ok: false, error: "unsupported_platform" };
91
- if (!process.stdout.isTTY || args.json) {
92
- console.log(JSON.stringify(payload));
93
- } else {
94
- console.log("daemon: unsupported platform");
95
- }
96
- process.exitCode = 1;
97
- },
98
- });
99
- }
@@ -1,34 +0,0 @@
1
- import escapeXml from "./escapeXml";
2
-
3
- export default function createLaunchdPlist(
4
- args: string[],
5
- stdoutPath: string,
6
- stderrPath: string,
7
- ) {
8
- const items = args
9
- .map((arg) => ` <string>${escapeXml(arg)}</string>`)
10
- .join("\n");
11
- return [
12
- '<?xml version="1.0" encoding="UTF-8"?>',
13
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
14
- '<plist version="1.0">',
15
- " <dict>",
16
- " <key>Label</key>",
17
- " <string>com.crnd.daemon</string>",
18
- " <key>ProgramArguments</key>",
19
- " <array>",
20
- items,
21
- " </array>",
22
- " <key>RunAtLoad</key>",
23
- " <true/>",
24
- " <key>KeepAlive</key>",
25
- " <true/>",
26
- " <key>StandardOutPath</key>",
27
- ` <string>${escapeXml(stdoutPath)}</string>`,
28
- " <key>StandardErrorPath</key>",
29
- ` <string>${escapeXml(stderrPath)}</string>`,
30
- " </dict>",
31
- "</plist>",
32
- "",
33
- ].join("\n");
34
- }
@@ -1,24 +0,0 @@
1
- export default function createSystemdService(
2
- args: string[],
3
- stdoutPath: string,
4
- stderrPath: string,
5
- ) {
6
- const execStart = args
7
- .map((arg) => (arg.includes(" ") ? `"${arg}"` : arg))
8
- .join(" ");
9
- return [
10
- "[Unit]",
11
- "Description=crnd daemon",
12
- "",
13
- "[Service]",
14
- `ExecStart=${execStart}`,
15
- "Restart=always",
16
- "RestartSec=1",
17
- `StandardOutput=append:${stdoutPath}`,
18
- `StandardError=append:${stderrPath}`,
19
- "",
20
- "[Install]",
21
- "WantedBy=default.target",
22
- "",
23
- ].join("\n");
24
- }
@@ -1,8 +0,0 @@
1
- export default function escapeXml(value: string) {
2
- return value
3
- .replaceAll("&", "&amp;")
4
- .replaceAll("<", "&lt;")
5
- .replaceAll(">", "&gt;")
6
- .replaceAll('"', "&quot;")
7
- .replaceAll("'", "&apos;");
8
- }
@@ -1,10 +0,0 @@
1
- import path from "node:path";
2
-
3
- export default function getDaemonServiceArgs() {
4
- const [execPath, scriptPath] = process.argv;
5
- if (scriptPath?.endsWith(".ts")) {
6
- return [execPath, path.resolve(scriptPath), "daemon", "serve"];
7
- }
8
-
9
- return [execPath, "daemon", "serve"];
10
- }
@@ -1,4 +0,0 @@
1
- export default function quoteWindowsArg(value: string) {
2
- const escaped = value.replaceAll('"', '\\"');
3
- return `"${escaped}"`;
4
- }
@@ -1,8 +0,0 @@
1
- export default function getCommandArgs(argv: string[]) {
2
- const index = argv.indexOf("--");
3
- if (index === -1 || index >= argv.length - 1) {
4
- return [];
5
- }
6
-
7
- return argv.slice(index + 1);
8
- }
@@ -1,28 +0,0 @@
1
- import createEnvSchema from "../../shared/jobs/createEnvSchema";
2
-
3
- export default function parseEnvArgs(input: unknown) {
4
- if (!input) {
5
- return undefined;
6
- }
7
-
8
- const values = Array.isArray(input) ? input : [input];
9
- const env: Record<string, string> = {};
10
-
11
- for (const value of values) {
12
- if (typeof value !== "string") {
13
- continue;
14
- }
15
- const index = value.indexOf("=");
16
- if (index <= 0) {
17
- throw new Error(`Invalid env entry: ${value}`);
18
- }
19
- const key = value.slice(0, index);
20
- const val = value.slice(index + 1);
21
- if (!key || !val) {
22
- throw new Error(`Invalid env entry: ${value}`);
23
- }
24
- env[key] = val;
25
- }
26
-
27
- return createEnvSchema().parse(env);
28
- }
@@ -1,9 +0,0 @@
1
- export default function getDaemonSpawnArgs() {
2
- const [execPath, scriptPath] = process.argv;
3
-
4
- if (scriptPath?.endsWith(".ts")) {
5
- return [execPath, scriptPath, "daemon", "serve"];
6
- }
7
-
8
- return [execPath, "daemon", "serve"];
9
- }
package/src/cli/main.ts DELETED
@@ -1,8 +0,0 @@
1
- import { runMain } from "citty";
2
- import createRootCommand from "./commands/createRootCommand";
3
-
4
- export default function runCli() {
5
- return runMain(createRootCommand());
6
- }
7
-
8
- runCli();
@@ -1,30 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import getAutostartPath from "./getAutostartPath";
3
- import getDaemonInstallArgs from "./getDaemonInstallArgs";
4
-
5
- export default function ensureAutostart() {
6
- if (process.env.CRND_DISABLE_AUTOSTART === "1") {
7
- return { ok: true, reason: "disabled" };
8
- }
9
-
10
- const path = getAutostartPath();
11
- if (!path) {
12
- return { ok: false, reason: "unsupported_platform" };
13
- }
14
-
15
- if (process.platform === "win32") {
16
- const result = Bun.spawnSync(["schtasks", "/Query", "/TN", path]);
17
- if (result.success) {
18
- return { ok: true, reason: "already_installed" };
19
- }
20
- } else if (existsSync(path)) {
21
- return { ok: true, reason: "already_installed" };
22
- }
23
-
24
- const result = Bun.spawnSync(getDaemonInstallArgs());
25
- if (result.success) {
26
- return { ok: true, reason: "installed" };
27
- }
28
-
29
- return { ok: false, reason: "install_failed" };
30
- }
@@ -1,29 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
-
4
- export default function getAutostartPath() {
5
- if (process.platform === "darwin") {
6
- return path.join(
7
- os.homedir(),
8
- "Library",
9
- "LaunchAgents",
10
- "com.crnd.daemon.plist",
11
- );
12
- }
13
-
14
- if (process.platform === "linux") {
15
- return path.join(
16
- os.homedir(),
17
- ".config",
18
- "systemd",
19
- "user",
20
- "crnd.service",
21
- );
22
- }
23
-
24
- if (process.platform === "win32") {
25
- return "crnd";
26
- }
27
-
28
- return null;
29
- }
@@ -1,10 +0,0 @@
1
- import path from "node:path";
2
-
3
- export default function getDaemonInstallArgs() {
4
- const [execPath, scriptPath] = process.argv;
5
- if (scriptPath?.endsWith(".ts")) {
6
- return [execPath, path.resolve(scriptPath), "daemon", "install"];
7
- }
8
-
9
- return [execPath, "daemon", "install"];
10
- }
@@ -1,13 +0,0 @@
1
- import { createConsola } from "consola";
2
-
3
- export default function createLogger() {
4
- return createConsola({
5
- reporters: [
6
- {
7
- log: (logObj) => {
8
- console.log(JSON.stringify(logObj));
9
- },
10
- },
11
- ],
12
- });
13
- }
@@ -1,29 +0,0 @@
1
- import appendEvent from "../shared/events/appendEvent";
2
- import removeDaemonState from "../shared/state/removeDaemonState";
3
- import type createLogger from "./createLogger";
4
- import type createJobsFileSync from "./jobs/createJobsFileSync";
5
- import type createScheduler from "./scheduler/createScheduler";
6
-
7
- type Logger = ReturnType<typeof createLogger>;
8
- type Server = ReturnType<typeof Bun.serve>;
9
- type Scheduler = ReturnType<typeof createScheduler>;
10
- type JobsFileSync = ReturnType<typeof createJobsFileSync>;
11
-
12
- export default function createShutdownHandler(
13
- server: Server,
14
- logger: Logger,
15
- scheduler: Scheduler,
16
- jobsFileSync: JobsFileSync,
17
- ) {
18
- return () => {
19
- logger.info("daemon_shutdown");
20
- scheduler.stop();
21
- jobsFileSync.stop();
22
- appendEvent("daemon_stopped", { pid: process.pid });
23
- removeDaemonState();
24
- server.stop();
25
- if (process.env.CRND_TEST_MODE !== "1") {
26
- process.exit(0);
27
- }
28
- };
29
- }
@@ -1,113 +0,0 @@
1
- import type { FSWatcher } from "node:fs";
2
- import { existsSync, watch } from "node:fs";
3
- import type openDatabase from "../../db/openDatabase";
4
- import { jobs } from "../../db/schema";
5
- import formatJobRow from "../../shared/jobs/formatJobRow";
6
- import parseJobsToml from "../../shared/jobs/parseJobsToml";
7
- import readJobsToml from "../../shared/jobs/readJobsToml";
8
- import writeJobsToml from "../../shared/jobs/writeJobsToml";
9
- import getJobsTomlPath from "../../shared/paths/getJobsTomlPath";
10
- import type createLogger from "../createLogger";
11
- import type createScheduler from "../scheduler/createScheduler";
12
- import deleteJobByName from "./deleteJobByName";
13
- import upsertJob from "./upsertJob";
14
-
15
- type Db = ReturnType<typeof openDatabase>["orm"];
16
- type Logger = ReturnType<typeof createLogger>;
17
- type Scheduler = ReturnType<typeof createScheduler>;
18
-
19
- export default function createJobsFileSync(
20
- db: Db,
21
- scheduler: Scheduler,
22
- logger: Logger,
23
- ) {
24
- let ignore = false;
25
- let watcher: FSWatcher | null = null;
26
-
27
- const applyJobs = (jobsFromToml: ReturnType<typeof readJobsToml>) => {
28
- const existing = db.select().from(jobs).all().map(formatJobRow);
29
- const existingByName = new Map(existing.map((job) => [job.name, job]));
30
- const seen = new Set<string>();
31
-
32
- for (const entry of jobsFromToml) {
33
- const payload = {
34
- name: entry.name,
35
- description: entry.description,
36
- command: entry.command,
37
- cwd: entry.cwd,
38
- env: entry.env,
39
- schedule: entry.schedule,
40
- runAt: entry.run_at,
41
- timezone: entry.timezone,
42
- timeoutMs: entry.timeout_ms,
43
- paused: entry.paused,
44
- overlapPolicy: entry.overlap_policy,
45
- };
46
-
47
- const existingJob = existingByName.get(entry.name);
48
- const { job } = upsertJob(db, payload, entry.id ?? existingJob?.id);
49
- scheduler.upsert(job);
50
- seen.add(entry.name);
51
- }
52
-
53
- for (const job of existing) {
54
- if (!seen.has(job.name)) {
55
- scheduler.remove(job.id);
56
- deleteJobByName(db, job.name);
57
- }
58
- }
59
- };
60
-
61
- const applyFromFile = () => {
62
- try {
63
- const jobsFromToml = readJobsToml();
64
- applyJobs(jobsFromToml);
65
- } catch (error) {
66
- logger.error({ event: "jobs_toml_error", message: String(error) });
67
- }
68
- };
69
-
70
- const writeFromDb = () => {
71
- const rows = db.select().from(jobs).all().map(formatJobRow);
72
- ignore = true;
73
- try {
74
- writeJobsToml(rows);
75
- } finally {
76
- setTimeout(() => {
77
- ignore = false;
78
- }, 50);
79
- }
80
- };
81
-
82
- return {
83
- init() {
84
- const path = getJobsTomlPath();
85
- if (existsSync(path)) {
86
- applyFromFile();
87
- } else {
88
- writeFromDb();
89
- }
90
-
91
- watcher = watch(path, { persistent: false }, () => {
92
- if (ignore) {
93
- return;
94
- }
95
- applyFromFile();
96
- });
97
- },
98
- stop() {
99
- watcher?.close();
100
- },
101
- writeFromDb,
102
- applyFromText(content: string) {
103
- try {
104
- const jobsFromToml = parseJobsToml(content);
105
- applyJobs(jobsFromToml);
106
- writeFromDb();
107
- return { ok: true };
108
- } catch (error) {
109
- return { ok: false, error: String(error) };
110
- }
111
- },
112
- };
113
- }
@@ -1,18 +0,0 @@
1
- import { eq } from "drizzle-orm";
2
- import type openDatabase from "../../db/openDatabase";
3
- import { jobs, runs } from "../../db/schema";
4
- import appendEvent from "../../shared/events/appendEvent";
5
-
6
- type Db = ReturnType<typeof openDatabase>["orm"];
7
-
8
- export default function deleteJobByName(db: Db, name: string) {
9
- const row = db.select().from(jobs).where(eq(jobs.name, name)).get();
10
- if (!row) {
11
- return null;
12
- }
13
-
14
- db.delete(runs).where(eq(runs.jobId, row.id)).run();
15
- db.delete(jobs).where(eq(jobs.id, row.id)).run();
16
- appendEvent("job_deleted", { jobId: row.id, name: row.name });
17
- return row.id;
18
- }