@wonsukchoi/crondex 0.15.0 → 0.16.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.
package/README.md CHANGED
@@ -37,17 +37,22 @@ npx @wonsukchoi/crondex deploy ssl-cert-expiry-check --var host=example.com
37
37
  - `init <id> [--category x]` — scaffold a brand-new job from the template
38
38
  - `update <path>` — re-pull a job you already `add`ed/`init`ed (matched by
39
39
  its `id` field) against the current catalog and overwrite it in place
40
- - `deploy <id> [--target crontab|github-actions|systemd|docker] [--var name=value ...]`
40
+ - `deploy <id> [--target crontab|github-actions|systemd|docker|k8s-cronjob|eventbridge|cloud-scheduler] [--var name=value ...]`
41
41
  — turn a job into something that actually runs: prints a ready crontab
42
42
  line (or installs it into your own crontab with `--install`), writes a
43
43
  scheduled GitHub Actions workflow file, writes a systemd
44
- `<id>.service` + `<id>.timer` pair, or writes a Dockerfile + `/etc/cron.d`
45
- entry that runs the job on its own schedule in a container. `--var`
46
- overrides a variable's default (repeatable). `hybrid` jobs deploy their
47
- `command` by default; pass `--mode prompt` to deploy the `prompt` side
48
- instead for crontab/systemd/docker targets that means wiring in your
49
- own agent CLI via a `CRONDEX_AGENT_CLI` env var, since crondex can't
50
- guess its syntax.
44
+ `<id>.service` + `<id>.timer` pair, writes a Dockerfile + `/etc/cron.d`
45
+ entry that runs the job on its own schedule in a container, or writes a
46
+ self-contained `batch/v1` CronJob manifest (`k8s-cronjob`). `eventbridge`
47
+ and `cloud-scheduler` print a ready `aws`/`gcloud` CLI command instead
48
+ those services invoke a target (Lambda/ECS/HTTP endpoint) rather than
49
+ running a shell command directly, so the command leaves that target as a
50
+ TODO for you to wire up. `--var` overrides a variable's default
51
+ (repeatable). `hybrid` jobs deploy their `command` by default; pass
52
+ `--mode prompt` to deploy the `prompt` side instead — for
53
+ crontab/systemd/docker/k8s targets that means wiring in your own agent
54
+ CLI via a `CRONDEX_AGENT_CLI` env var, since crondex can't guess its
55
+ syntax.
51
56
  - `deploy --list-installed` — show every crondex-managed line in your
52
57
  crontab (the ones left by `--install`)
53
58
  - `uninstall <id>` — remove one of those installed crontab entries
package/bin/crondex.js CHANGED
@@ -14,6 +14,9 @@ import {
14
14
  buildGithubActionsWorkflow,
15
15
  buildSystemdUnits,
16
16
  buildDockerArtifacts,
17
+ buildK8sCronJob,
18
+ buildEventBridgeCommand,
19
+ buildCloudSchedulerCommand,
17
20
  } from "../lib/deploy.js";
18
21
 
19
22
  const ROOT = new URL("..", import.meta.url).pathname;
@@ -68,7 +71,8 @@ Usage:
68
71
  crondex recommend "<what you want done>" [--limit <n>] [--json]
69
72
  crondex init <id> [--category <name>] [--dest <path>]
70
73
  crondex update <path>
71
- crondex deploy <id> [--target crontab|github-actions|systemd|docker] [--mode script|prompt]
74
+ crondex deploy <id> [--target crontab|github-actions|systemd|docker|k8s-cronjob|
75
+ eventbridge|cloud-scheduler] [--mode script|prompt]
72
76
  [--var name=value ...] [--dest <path>] [--install]
73
77
  crondex deploy --list-installed [--json]
74
78
  crondex uninstall <id>
@@ -85,6 +89,9 @@ Examples:
85
89
  crondex deploy repo-health-check --target github-actions
86
90
  crondex deploy repo-health-check --target systemd --dest ./systemd
87
91
  crondex deploy repo-health-check --target docker --dest ./docker/repo-health-check
92
+ crondex deploy repo-health-check --target k8s-cronjob --dest ./k8s/repo-health-check.yaml
93
+ crondex deploy repo-health-check --target eventbridge
94
+ crondex deploy repo-health-check --target cloud-scheduler
88
95
  crondex deploy --list-installed
89
96
  crondex uninstall ssl-cert-expiry-check
90
97
 
@@ -98,9 +105,15 @@ crontab with --install; --target github-actions writes a scheduled
98
105
  workflow file (default .github/workflows/<id>.yml); --target systemd
99
106
  writes a <id>.service + <id>.timer pair (default ./systemd/); --target
100
107
  docker writes a Dockerfile + crontab pair that runs the job on its own
101
- schedule in a container (default ./docker/<id>/). --var overrides a job's
102
- variable defaults (repeatable). hybrid jobs default to --mode script (zero
103
- tokens); pass --mode prompt to deploy the agent-prompt side instead.
108
+ schedule in a container (default ./docker/<id>/); --target k8s-cronjob
109
+ writes a self-contained batch/v1 CronJob manifest (default
110
+ ./k8s/<id>.cronjob.yaml). --target eventbridge and --target
111
+ cloud-scheduler print a ready aws/gcloud CLI command instead — those
112
+ services invoke a target (Lambda/ECS/HTTP endpoint) rather than running a
113
+ shell command directly, so the command leaves that target as a TODO for
114
+ you to wire up. --var overrides a job's variable defaults (repeatable).
115
+ hybrid jobs default to --mode script (zero tokens); pass --mode prompt to
116
+ deploy the agent-prompt side instead.
104
117
 
105
118
  deploy --list-installed shows every crondex-managed line in your crontab
106
119
  (the ones with a "# crondex:<id>" marker, as left by --install). uninstall
@@ -345,6 +358,20 @@ function deploy(id) {
345
358
  writeFileSync(serviceDest, service);
346
359
  writeFileSync(timerDest, timer);
347
360
  console.log(`wrote ${serviceDest} and ${timerDest} — enable with:\n systemctl --user enable --now ${id}.timer`);
361
+ } else if (target === "k8s-cronjob") {
362
+ const manifest = buildK8sCronJob(doc, mode === "prompt" ? prompt : command, mode === "prompt");
363
+ const dest = flag("dest") ?? join("./k8s", `${id}.cronjob.yaml`);
364
+ if (existsSync(dest)) {
365
+ console.error(`${dest} already exists — refusing to overwrite. Pass --dest to choose another path.`);
366
+ process.exit(1);
367
+ }
368
+ mkdirSync(dirname(dest), { recursive: true });
369
+ writeFileSync(dest, manifest);
370
+ console.log(`wrote ${dest} — apply with:\n kubectl apply -f ${dest}`);
371
+ } else if (target === "eventbridge") {
372
+ console.log(buildEventBridgeCommand(doc, mode === "prompt" ? prompt : command, mode === "prompt"));
373
+ } else if (target === "cloud-scheduler") {
374
+ console.log(buildCloudSchedulerCommand(doc, mode === "prompt" ? prompt : command, mode === "prompt"));
348
375
  } else if (target === "docker") {
349
376
  const { dockerfile, crontab } = buildDockerArtifacts(doc, mode === "prompt" ? prompt : command, mode === "prompt");
350
377
  const destDir = flag("dest") ?? join("./docker", id);
@@ -359,7 +386,7 @@ function deploy(id) {
359
386
  writeFileSync(crontabDest, crontab);
360
387
  console.log(`wrote ${dockerfileDest} and ${crontabDest} — build with:\n docker build -t ${id} ${destDir}`);
361
388
  } else {
362
- console.error(`unknown --target "${target}" — use "crontab", "github-actions", "systemd", or "docker".`);
389
+ console.error(`unknown --target "${target}" — use "crontab", "github-actions", "systemd", "docker", "k8s-cronjob", "eventbridge", or "cloud-scheduler".`);
363
390
  process.exit(1);
364
391
  }
365
392
  }
package/lib/deploy.js CHANGED
@@ -30,17 +30,27 @@ function escapeSingleQuotes(text) {
30
30
  return text.replace(/'/g, `'"'"'`);
31
31
  }
32
32
 
33
- // Flattens a (possibly multi-line) resolved command/prompt into one crontab line.
34
- // isPrompt jobs can't assume any particular agent CLI syntax, so the line defers to
35
- // a CRONDEX_AGENT_CLI env var the user sets themselves (e.g. `export
36
- // CRONDEX_AGENT_CLI="claude -p"`) rather than guessing wrong.
37
- export function buildCrontabLine(job, resolvedText, isPrompt) {
38
- const flat = resolvedText.trim().replace(/\s*\n\s*/g, " ");
39
- const escaped = escapeSingleQuotes(flat);
40
- const body = isPrompt
33
+ // Collapses a (possibly multi-line) resolved command/prompt onto one line.
34
+ function flattenScript(resolvedText) {
35
+ return resolvedText.trim().replace(/\s*\n\s*/g, " ");
36
+ }
37
+
38
+ // Builds a single-quoted shell invocation from a flattened script. isPrompt jobs can't
39
+ // assume any particular agent CLI syntax, so the body defers to a CRONDEX_AGENT_CLI env
40
+ // var the user sets themselves (e.g. `export CRONDEX_AGENT_CLI="claude -p"`) rather than
41
+ // guessing wrong. Shared by every deploy target that embeds the job as a single-quoted
42
+ // string inside a larger shell line (crontab, systemd, docker) — targets that build
43
+ // their own command array (k8s) flatten/quote the script directly instead.
44
+ function buildShellBody(resolvedText, isPrompt) {
45
+ const escaped = escapeSingleQuotes(flattenScript(resolvedText));
46
+ return isPrompt
41
47
  ? `\${CRONDEX_AGENT_CLI:?set CRONDEX_AGENT_CLI to your agent CLI invocation, e.g. "claude -p"} '${escaped}'`
42
48
  : `bash -lc '${escaped}'`;
43
- return `${job.schedule} ${body} # crondex:${job.id}`;
49
+ }
50
+
51
+ // Flattens a (possibly multi-line) resolved command/prompt into one crontab line.
52
+ export function buildCrontabLine(job, resolvedText, isPrompt) {
53
+ return `${job.schedule} ${buildShellBody(resolvedText, isPrompt)} # crondex:${job.id}`;
44
54
  }
45
55
 
46
56
  const DOW = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
@@ -80,11 +90,7 @@ export function cronToSystemdCalendar(schedule) {
80
90
  // to CRONDEX_AGENT_CLI (see buildCrontabLine) — systemd services run with a minimal
81
91
  // environment, so the unit points at EnvironmentFile as the place to set it.
82
92
  export function buildSystemdUnits(job, resolvedText, isPrompt) {
83
- const flat = resolvedText.trim().replace(/\s*\n\s*/g, " ");
84
- const escaped = escapeSingleQuotes(flat);
85
- const body = isPrompt
86
- ? `\${CRONDEX_AGENT_CLI:?set CRONDEX_AGENT_CLI to your agent CLI invocation, e.g. "claude -p"} '${escaped}'`
87
- : `bash -lc '${escaped}'`;
93
+ const body = buildShellBody(resolvedText, isPrompt);
88
94
  const service = `[Unit]
89
95
  Description=${job.name} (crondex:${job.id})
90
96
 
@@ -109,11 +115,7 @@ WantedBy=timers.target
109
115
  // inside a container — for teams that want the job shipped as an image rather than
110
116
  // installed on a host crontab or run via CI.
111
117
  export function buildDockerArtifacts(job, resolvedText, isPrompt) {
112
- const flat = resolvedText.trim().replace(/\s*\n\s*/g, " ");
113
- const escaped = escapeSingleQuotes(flat);
114
- const body = isPrompt
115
- ? `\${CRONDEX_AGENT_CLI:?set CRONDEX_AGENT_CLI to your agent CLI invocation, e.g. "claude -p"} '${escaped}'`
116
- : `bash -lc '${escaped}'`;
118
+ const body = buildShellBody(resolvedText, isPrompt);
117
119
  const crontab = `${job.schedule} root ${body} # crondex:${job.id}\n`;
118
120
  const dockerfile = `# Generated by \`crondex deploy ${job.id} --target docker\` from ${job.path}.
119
121
  FROM debian:bookworm-slim
@@ -126,6 +128,106 @@ ${isPrompt ? "# NOTE: prompt-mode job — pass CRONDEX_AGENT_CLI via `docker run
126
128
  return { dockerfile, crontab };
127
129
  }
128
130
 
131
+ // YAML double-quoted scalars only need backslash and the quote itself escaped —
132
+ // used for embedding an arbitrary shell body inside a k8s manifest's command array.
133
+ function yamlDoubleQuote(text) {
134
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
135
+ }
136
+
137
+ // Builds a self-contained batch/v1 CronJob manifest — the one target here that
138
+ // actually runs the job (crontab/systemd/docker's sibling), rather than just
139
+ // scheduling an invocation of something else the user still has to build (see
140
+ // buildEventBridgeCommand/buildCloudSchedulerCommand below).
141
+ export function buildK8sCronJob(job, resolvedText, isPrompt) {
142
+ const flat = flattenScript(resolvedText);
143
+ const shell = isPrompt ? "sh" : "bash";
144
+ const command = isPrompt
145
+ ? `\${CRONDEX_AGENT_CLI:?set CRONDEX_AGENT_CLI to your agent CLI invocation, e.g. "claude -p"} '${escapeSingleQuotes(flat)}'`
146
+ : flat;
147
+ return `# Generated by \`crondex deploy ${job.id} --target k8s-cronjob\` from ${job.path}.
148
+ apiVersion: batch/v1
149
+ kind: CronJob
150
+ metadata:
151
+ name: ${job.id}
152
+ labels:
153
+ app.kubernetes.io/managed-by: crondex
154
+ spec:
155
+ schedule: "${job.schedule}"
156
+ jobTemplate:
157
+ spec:
158
+ template:
159
+ spec:
160
+ restartPolicy: OnFailure
161
+ containers:
162
+ - name: ${job.id}
163
+ image: bash:5
164
+ ${isPrompt ? ` env:
165
+ - name: CRONDEX_AGENT_CLI
166
+ value: "REPLACE_ME" # e.g. "claude -p"
167
+ ` : ""} command: ["${shell}", "-lc", ${yamlDoubleQuote(command)}]
168
+ `;
169
+ }
170
+
171
+ const AWS_DOW = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
172
+
173
+ // AWS's cron() requires exactly one of day-of-month/day-of-week to be "?" — the other
174
+ // carries the restriction. Cron's day-of-week is numeric (0-6 or 7, both Sun); AWS
175
+ // wants day names. Minute/hour/month pass through unchanged except `*/n` -> `0/n`
176
+ // (same systemd gotcha — a bare `*` can't carry a step).
177
+ export function cronToAwsCron(schedule) {
178
+ const [minute, hour, dom, month, dow] = schedule.trim().split(/\s+/);
179
+ const min = translateNumericField(minute);
180
+ const hr = translateNumericField(hour);
181
+ const mon = translateNumericField(month);
182
+ if (dow === "*") {
183
+ return `cron(${min} ${hr} ${translateNumericField(dom)} ${mon} ? *)`;
184
+ }
185
+ const awsDow = dow
186
+ .split(",")
187
+ .map((part) => {
188
+ const range = part.match(/^(\d)-(\d)$/);
189
+ if (range) return `${AWS_DOW[Number(range[1]) % 7]}-${AWS_DOW[Number(range[2]) % 7]}`;
190
+ return AWS_DOW[Number(part) % 7];
191
+ })
192
+ .join(",");
193
+ return `cron(${min} ${hr} ? ${mon} ${awsDow} *)`;
194
+ }
195
+
196
+ // EventBridge Scheduler (unlike crontab/systemd/docker/k8s) can't run a shell command
197
+ // directly — it invokes a target ARN (Lambda/ECS/Step Functions). So this only gets
198
+ // the schedule right and leaves the target as a TODO, same spirit as the GitHub
199
+ // Actions prompt-mode TODO: get the user unstuck, don't guess their infra for them.
200
+ export function buildEventBridgeCommand(job, resolvedText, isPrompt) {
201
+ const body = buildShellBody(resolvedText, isPrompt);
202
+ return `# Generated by \`crondex deploy ${job.id} --target eventbridge\` from ${job.path}.
203
+ # EventBridge Scheduler invokes a target (Lambda/ECS/Step Functions) — it can't run a
204
+ # shell command directly. Point TODO_TARGET_ARN/TODO_ROLE_ARN at something that runs:
205
+ # ${body}
206
+ aws scheduler create-schedule \\
207
+ --name "${job.id}" \\
208
+ --schedule-expression "${cronToAwsCron(job.schedule)}" \\
209
+ --flexible-time-window '{"Mode":"OFF"}' \\
210
+ --target '{"Arn":"TODO_TARGET_ARN","RoleArn":"TODO_ROLE_ARN"}'
211
+ `;
212
+ }
213
+
214
+ // Cloud Scheduler accepts standard unix-cron directly (no conversion needed) but, like
215
+ // EventBridge, invokes an HTTP/Pub/Sub/App Engine target rather than running a shell
216
+ // command itself — --uri is left as a TODO for whatever endpoint actually runs it.
217
+ export function buildCloudSchedulerCommand(job, resolvedText, isPrompt) {
218
+ const body = buildShellBody(resolvedText, isPrompt);
219
+ return `# Generated by \`crondex deploy ${job.id} --target cloud-scheduler\` from ${job.path}.
220
+ # Cloud Scheduler invokes an HTTP endpoint (e.g. a Cloud Run job) — it can't run a
221
+ # shell command directly. Point TODO_HTTPS_ENDPOINT at something that runs:
222
+ # ${body}
223
+ gcloud scheduler jobs create http "${job.id}" \\
224
+ --schedule="${job.schedule}" \\
225
+ --uri="TODO_HTTPS_ENDPOINT" \\
226
+ --http-method=POST \\
227
+ --time-zone="${job.timezone ?? "UTC"}"
228
+ `;
229
+ }
230
+
129
231
  // Builds a ready-to-commit GitHub Actions workflow file for the job. GitHub Actions
130
232
  // schedules always run in UTC regardless of the `cron:` string's intent, so a
131
233
  // non-UTC job gets a visible warning comment rather than silently firing at the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wonsukchoi/crondex",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "A public directory of pre-made, agent-editable cron jobs.",
5
5
  "type": "module",
6
6
  "license": "MIT",