wogiflow 2.32.0 → 2.33.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/.claude/docs/claude-code-compatibility.md +51 -0
- package/.claude/docs/scheduled-mode.md +213 -0
- package/.claude/docs/skill-portability.md +190 -0
- package/.claude/rules/alternative-hook-args-exec-form.md +6 -0
- package/.claude/settings.json +2 -1
- package/.claude/skills/_template/skill.md +1 -0
- package/.claude/skills/conventional-commit/knowledge/examples.md +65 -0
- package/.claude/skills/conventional-commit/skill.md +76 -0
- package/bin/flow +16 -0
- package/lib/scheduled-mode.js +377 -0
- package/lib/skill-export-agentskills.js +211 -0
- package/lib/skill-export-claude-plugin.js +143 -0
- package/lib/skill-portability.js +324 -0
- package/lib/skill-registry.js +32 -2
- package/package.json +2 -2
- package/scripts/flow +8 -0
- package/scripts/flow-config-defaults.js +20 -0
- package/scripts/flow-schedule.js +469 -0
- package/scripts/flow-scheduled-runner.js +614 -0
- package/scripts/flow-skill-export.js +334 -0
- package/scripts/hooks/adapters/claude-code.js +12 -1
- package/scripts/hooks/core/git-safety-gate.js +92 -20
- package/scripts/hooks/core/long-input-enforcement.js +139 -4
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wogi Flow — `flow schedule` CLI (Phase 1A — wf-b211a076).
|
|
7
|
+
*
|
|
8
|
+
* Installs platform-native unit files that invoke `flow-scheduled-runner.js`
|
|
9
|
+
* for users who prefer not to use GitHub Actions.
|
|
10
|
+
*
|
|
11
|
+
* Subcommands:
|
|
12
|
+
* flow schedule install --target=launchd|cron|systemd [--dry-run]
|
|
13
|
+
* flow schedule status
|
|
14
|
+
* flow schedule remove --target=launchd|cron|systemd
|
|
15
|
+
*
|
|
16
|
+
* Targets:
|
|
17
|
+
* launchd — macOS user-domain LaunchAgent plists in ~/Library/LaunchAgents/
|
|
18
|
+
* cron — crontab entries appended for the current user
|
|
19
|
+
* systemd — systemd --user .service + .timer units in ~/.config/systemd/user/
|
|
20
|
+
*
|
|
21
|
+
* The same 4 jobs are installed regardless of target. Schedules:
|
|
22
|
+
* nightly-regression 03:00 daily
|
|
23
|
+
* weekly-audit Mon 09:00
|
|
24
|
+
* weekly-digest Fri 17:00
|
|
25
|
+
* per-pr-review on-demand only (no schedule entry — invoked by gh webhook
|
|
26
|
+
* or `flow scheduled-runner per-pr-review` manually)
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('node:fs');
|
|
30
|
+
const os = require('node:os');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
const { JOB_NAMES } = require('../lib/scheduled-mode');
|
|
33
|
+
|
|
34
|
+
// ============================================================
|
|
35
|
+
// Schedule table — shared across all targets
|
|
36
|
+
// ============================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Job schedule definitions.
|
|
40
|
+
* - cronExpr is in classic 5-field cron format (UTC where the platform supports it).
|
|
41
|
+
* - macOS launchd uses StartCalendarInterval (hour/minute/weekday fields).
|
|
42
|
+
* - systemd uses OnCalendar with extended cron-like syntax.
|
|
43
|
+
*
|
|
44
|
+
* per-pr-review is intentionally omitted from schedules — it runs on PR events
|
|
45
|
+
* (via the GH Actions workflow), or on demand via the CLI.
|
|
46
|
+
*/
|
|
47
|
+
const SCHEDULES = Object.freeze({
|
|
48
|
+
'nightly-regression': {
|
|
49
|
+
cronExpr: '0 3 * * *',
|
|
50
|
+
launchd: { Hour: 3, Minute: 0 },
|
|
51
|
+
systemdOnCalendar: '*-*-* 03:00:00',
|
|
52
|
+
},
|
|
53
|
+
'weekly-audit': {
|
|
54
|
+
cronExpr: '0 9 * * 1',
|
|
55
|
+
launchd: { Hour: 9, Minute: 0, Weekday: 1 },
|
|
56
|
+
systemdOnCalendar: 'Mon *-*-* 09:00:00',
|
|
57
|
+
},
|
|
58
|
+
'weekly-digest': {
|
|
59
|
+
cronExpr: '0 17 * * 5',
|
|
60
|
+
launchd: { Hour: 17, Minute: 0, Weekday: 5 },
|
|
61
|
+
systemdOnCalendar: 'Fri *-*-* 17:00:00',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const SCHEDULED_JOB_NAMES = Object.keys(SCHEDULES);
|
|
66
|
+
|
|
67
|
+
const ALLOWED_TARGETS = new Set(['launchd', 'cron', 'systemd']);
|
|
68
|
+
|
|
69
|
+
// ============================================================
|
|
70
|
+
// Path helpers
|
|
71
|
+
// ============================================================
|
|
72
|
+
|
|
73
|
+
function repoRoot() {
|
|
74
|
+
// Best-effort: walk up looking for package.json with name "wogiflow"
|
|
75
|
+
let dir = process.cwd();
|
|
76
|
+
for (let i = 0; i < 25; i++) {
|
|
77
|
+
const pkg = path.join(dir, 'package.json');
|
|
78
|
+
if (fs.existsSync(pkg)) {
|
|
79
|
+
try {
|
|
80
|
+
const data = fs.readFileSync(pkg, 'utf-8');
|
|
81
|
+
if (data.includes('"wogiflow"') || data.includes('wogi-flow')) return dir;
|
|
82
|
+
} catch (_err) { /* */ }
|
|
83
|
+
}
|
|
84
|
+
const parent = path.dirname(dir);
|
|
85
|
+
if (parent === dir) break;
|
|
86
|
+
dir = parent;
|
|
87
|
+
}
|
|
88
|
+
return process.cwd();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function runnerScriptPath() {
|
|
92
|
+
return path.join(repoRoot(), 'scripts', 'flow-scheduled-runner.js');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nodeBinary() {
|
|
96
|
+
return process.execPath; // canonical, absolute
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ============================================================
|
|
100
|
+
// Unit content generators (pure — no I/O)
|
|
101
|
+
// ============================================================
|
|
102
|
+
|
|
103
|
+
function generateCrontabLines(opts = {}) {
|
|
104
|
+
const node = opts.node || nodeBinary();
|
|
105
|
+
const runner = opts.runner || runnerScriptPath();
|
|
106
|
+
const projectRoot = opts.projectRoot || repoRoot();
|
|
107
|
+
const lines = [
|
|
108
|
+
`# === wogi-scheduled (managed by flow schedule) — DO NOT EDIT BELOW ===`,
|
|
109
|
+
];
|
|
110
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
111
|
+
const spec = SCHEDULES[jobName];
|
|
112
|
+
lines.push(
|
|
113
|
+
`${spec.cronExpr} cd ${projectRoot} && ${node} ${runner} ${jobName} ` +
|
|
114
|
+
`>> ${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`)} 2>&1`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
lines.push(`# === wogi-scheduled end ===`);
|
|
118
|
+
return lines;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function generateLaunchdPlist(jobName, opts = {}) {
|
|
122
|
+
const node = opts.node || nodeBinary();
|
|
123
|
+
const runner = opts.runner || runnerScriptPath();
|
|
124
|
+
const projectRoot = opts.projectRoot || repoRoot();
|
|
125
|
+
const spec = SCHEDULES[jobName];
|
|
126
|
+
if (!spec) throw new Error(`No schedule defined for "${jobName}"`);
|
|
127
|
+
|
|
128
|
+
const calBlock = [
|
|
129
|
+
` <key>Hour</key>`,
|
|
130
|
+
` <integer>${spec.launchd.Hour}</integer>`,
|
|
131
|
+
` <key>Minute</key>`,
|
|
132
|
+
` <integer>${spec.launchd.Minute}</integer>`,
|
|
133
|
+
];
|
|
134
|
+
if (typeof spec.launchd.Weekday === 'number') {
|
|
135
|
+
calBlock.push(` <key>Weekday</key>`);
|
|
136
|
+
calBlock.push(` <integer>${spec.launchd.Weekday}</integer>`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
140
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
141
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
142
|
+
<plist version="1.0">
|
|
143
|
+
<dict>
|
|
144
|
+
<key>Label</key>
|
|
145
|
+
<string>io.wogi.scheduled.${jobName}</string>
|
|
146
|
+
<key>ProgramArguments</key>
|
|
147
|
+
<array>
|
|
148
|
+
<string>${node}</string>
|
|
149
|
+
<string>${runner}</string>
|
|
150
|
+
<string>${jobName}</string>
|
|
151
|
+
</array>
|
|
152
|
+
<key>WorkingDirectory</key>
|
|
153
|
+
<string>${projectRoot}</string>
|
|
154
|
+
<key>StandardOutPath</key>
|
|
155
|
+
<string>${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`)}</string>
|
|
156
|
+
<key>StandardErrorPath</key>
|
|
157
|
+
<string>${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.err.log`)}</string>
|
|
158
|
+
<key>StartCalendarInterval</key>
|
|
159
|
+
<dict>
|
|
160
|
+
${calBlock.join('\n')}
|
|
161
|
+
</dict>
|
|
162
|
+
<key>RunAtLoad</key>
|
|
163
|
+
<false/>
|
|
164
|
+
</dict>
|
|
165
|
+
</plist>
|
|
166
|
+
`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function generateSystemdServiceUnit(jobName, opts = {}) {
|
|
170
|
+
const node = opts.node || nodeBinary();
|
|
171
|
+
const runner = opts.runner || runnerScriptPath();
|
|
172
|
+
const projectRoot = opts.projectRoot || repoRoot();
|
|
173
|
+
if (!SCHEDULES[jobName]) throw new Error(`No schedule defined for "${jobName}"`);
|
|
174
|
+
|
|
175
|
+
return `[Unit]
|
|
176
|
+
Description=Wogi Flow scheduled job: ${jobName}
|
|
177
|
+
After=network-online.target
|
|
178
|
+
|
|
179
|
+
[Service]
|
|
180
|
+
Type=oneshot
|
|
181
|
+
WorkingDirectory=${projectRoot}
|
|
182
|
+
ExecStart=${node} ${runner} ${jobName}
|
|
183
|
+
StandardOutput=append:${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.log`)}
|
|
184
|
+
StandardError=append:${path.join(projectRoot, '.workflow', 'scratch', `scheduled-${jobName}.err.log`)}
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function generateSystemdTimerUnit(jobName) {
|
|
189
|
+
const spec = SCHEDULES[jobName];
|
|
190
|
+
if (!spec) throw new Error(`No schedule defined for "${jobName}"`);
|
|
191
|
+
return `[Unit]
|
|
192
|
+
Description=Wogi Flow scheduled timer: ${jobName}
|
|
193
|
+
|
|
194
|
+
[Timer]
|
|
195
|
+
OnCalendar=${spec.systemdOnCalendar}
|
|
196
|
+
Persistent=true
|
|
197
|
+
Unit=wogi-scheduled-${jobName}.service
|
|
198
|
+
|
|
199
|
+
[Install]
|
|
200
|
+
WantedBy=timers.target
|
|
201
|
+
`;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================
|
|
205
|
+
// Install / remove / status (with FS injection for tests)
|
|
206
|
+
// ============================================================
|
|
207
|
+
|
|
208
|
+
function installLaunchd(opts = {}, deps = {}) {
|
|
209
|
+
const fsx = deps.fs || fs;
|
|
210
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
211
|
+
const dir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
212
|
+
const dryRun = Boolean(opts.dryRun);
|
|
213
|
+
const written = [];
|
|
214
|
+
|
|
215
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
216
|
+
const filename = `io.wogi.scheduled.${jobName}.plist`;
|
|
217
|
+
const dest = path.join(dir, filename);
|
|
218
|
+
const content = generateLaunchdPlist(jobName, opts);
|
|
219
|
+
if (!dryRun) {
|
|
220
|
+
fsx.mkdirSync(dir, { recursive: true });
|
|
221
|
+
fsx.writeFileSync(dest, content);
|
|
222
|
+
}
|
|
223
|
+
written.push({ path: dest, jobName, content });
|
|
224
|
+
}
|
|
225
|
+
return { target: 'launchd', dryRun, written };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function installCron(opts = {}, deps = {}) {
|
|
229
|
+
const fsx = deps.fs || fs;
|
|
230
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
231
|
+
const fragmentPath = opts.fragmentPath ||
|
|
232
|
+
path.join(homeDir, '.config', 'wogi-flow', 'crontab-fragment');
|
|
233
|
+
const dryRun = Boolean(opts.dryRun);
|
|
234
|
+
const lines = generateCrontabLines(opts);
|
|
235
|
+
const content = lines.join('\n') + '\n';
|
|
236
|
+
|
|
237
|
+
if (!dryRun) {
|
|
238
|
+
fsx.mkdirSync(path.dirname(fragmentPath), { recursive: true });
|
|
239
|
+
fsx.writeFileSync(fragmentPath, content);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
target: 'cron',
|
|
244
|
+
dryRun,
|
|
245
|
+
written: [{ path: fragmentPath, content }],
|
|
246
|
+
note:
|
|
247
|
+
`Crontab fragment written. Activate with:\n` +
|
|
248
|
+
` (crontab -l 2>/dev/null; cat ${fragmentPath}) | crontab -\n` +
|
|
249
|
+
`Idempotency: re-running install OVERWRITES the fragment but does NOT auto-install ` +
|
|
250
|
+
`into crontab — the user runs the command above.`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function installSystemd(opts = {}, deps = {}) {
|
|
255
|
+
const fsx = deps.fs || fs;
|
|
256
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
257
|
+
const dir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
258
|
+
const dryRun = Boolean(opts.dryRun);
|
|
259
|
+
const written = [];
|
|
260
|
+
|
|
261
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
262
|
+
const serviceUnit = generateSystemdServiceUnit(jobName, opts);
|
|
263
|
+
const timerUnit = generateSystemdTimerUnit(jobName);
|
|
264
|
+
const servicePath = path.join(dir, `wogi-scheduled-${jobName}.service`);
|
|
265
|
+
const timerPath = path.join(dir, `wogi-scheduled-${jobName}.timer`);
|
|
266
|
+
if (!dryRun) {
|
|
267
|
+
fsx.mkdirSync(dir, { recursive: true });
|
|
268
|
+
fsx.writeFileSync(servicePath, serviceUnit);
|
|
269
|
+
fsx.writeFileSync(timerPath, timerUnit);
|
|
270
|
+
}
|
|
271
|
+
written.push({ path: servicePath, jobName, content: serviceUnit });
|
|
272
|
+
written.push({ path: timerPath, jobName, content: timerUnit });
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
target: 'systemd',
|
|
276
|
+
dryRun,
|
|
277
|
+
written,
|
|
278
|
+
note:
|
|
279
|
+
`Activate with:\n` +
|
|
280
|
+
SCHEDULED_JOB_NAMES.map(
|
|
281
|
+
(j) => ` systemctl --user enable --now wogi-scheduled-${j}.timer`
|
|
282
|
+
).join('\n'),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function removeLaunchd(opts = {}, deps = {}) {
|
|
287
|
+
const fsx = deps.fs || fs;
|
|
288
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
289
|
+
const dir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
290
|
+
const removed = [];
|
|
291
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
292
|
+
const dest = path.join(dir, `io.wogi.scheduled.${jobName}.plist`);
|
|
293
|
+
try {
|
|
294
|
+
if (fsx.existsSync(dest)) {
|
|
295
|
+
fsx.unlinkSync(dest);
|
|
296
|
+
removed.push(dest);
|
|
297
|
+
}
|
|
298
|
+
} catch (_err) { /* fail-open */ }
|
|
299
|
+
}
|
|
300
|
+
return { target: 'launchd', removed };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function removeCron(opts = {}, deps = {}) {
|
|
304
|
+
const fsx = deps.fs || fs;
|
|
305
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
306
|
+
const fragmentPath = opts.fragmentPath ||
|
|
307
|
+
path.join(homeDir, '.config', 'wogi-flow', 'crontab-fragment');
|
|
308
|
+
const removed = [];
|
|
309
|
+
try {
|
|
310
|
+
if (fsx.existsSync(fragmentPath)) {
|
|
311
|
+
fsx.unlinkSync(fragmentPath);
|
|
312
|
+
removed.push(fragmentPath);
|
|
313
|
+
}
|
|
314
|
+
} catch (_err) { /* */ }
|
|
315
|
+
return {
|
|
316
|
+
target: 'cron',
|
|
317
|
+
removed,
|
|
318
|
+
note:
|
|
319
|
+
`Fragment file removed. Manually purge the lines between the\n` +
|
|
320
|
+
`'# === wogi-scheduled ...' markers from your active crontab with:\n` +
|
|
321
|
+
` crontab -e`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function removeSystemd(opts = {}, deps = {}) {
|
|
326
|
+
const fsx = deps.fs || fs;
|
|
327
|
+
const homeDir = opts.homeDir || os.homedir();
|
|
328
|
+
const dir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
329
|
+
const removed = [];
|
|
330
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
331
|
+
for (const ext of ['service', 'timer']) {
|
|
332
|
+
const dest = path.join(dir, `wogi-scheduled-${jobName}.${ext}`);
|
|
333
|
+
try {
|
|
334
|
+
if (fsx.existsSync(dest)) {
|
|
335
|
+
fsx.unlinkSync(dest);
|
|
336
|
+
removed.push(dest);
|
|
337
|
+
}
|
|
338
|
+
} catch (_err) { /* */ }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return { target: 'systemd', removed };
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================
|
|
345
|
+
// Status — list what is currently installed
|
|
346
|
+
// ============================================================
|
|
347
|
+
|
|
348
|
+
function getStatus(deps = {}) {
|
|
349
|
+
const fsx = deps.fs || fs;
|
|
350
|
+
const homeDir = deps.homeDir || os.homedir();
|
|
351
|
+
const status = { launchd: [], cron: [], systemd: [] };
|
|
352
|
+
|
|
353
|
+
// launchd
|
|
354
|
+
const ldir = path.join(homeDir, 'Library', 'LaunchAgents');
|
|
355
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
356
|
+
const p = path.join(ldir, `io.wogi.scheduled.${jobName}.plist`);
|
|
357
|
+
if (fsx.existsSync(p)) status.launchd.push(p);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// cron
|
|
361
|
+
const cfrag = path.join(homeDir, '.config', 'wogi-flow', 'crontab-fragment');
|
|
362
|
+
if (fsx.existsSync(cfrag)) status.cron.push(cfrag);
|
|
363
|
+
|
|
364
|
+
// systemd
|
|
365
|
+
const sdir = path.join(homeDir, '.config', 'systemd', 'user');
|
|
366
|
+
for (const jobName of SCHEDULED_JOB_NAMES) {
|
|
367
|
+
for (const ext of ['service', 'timer']) {
|
|
368
|
+
const p = path.join(sdir, `wogi-scheduled-${jobName}.${ext}`);
|
|
369
|
+
if (fsx.existsSync(p)) status[ext === 'timer' ? 'systemd' : 'systemd'].push(p);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return status;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ============================================================
|
|
376
|
+
// CLI dispatch
|
|
377
|
+
// ============================================================
|
|
378
|
+
|
|
379
|
+
function parseCliArgs(argv) {
|
|
380
|
+
const args = { subcommand: argv[0] || null, target: null, dryRun: false };
|
|
381
|
+
for (let i = 1; i < argv.length; i++) {
|
|
382
|
+
const a = argv[i];
|
|
383
|
+
if (a.startsWith('--target=')) args.target = a.slice('--target='.length);
|
|
384
|
+
else if (a === '--dry-run') args.dryRun = true;
|
|
385
|
+
}
|
|
386
|
+
return args;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function dispatch(argv, deps = {}) {
|
|
390
|
+
const args = parseCliArgs(argv);
|
|
391
|
+
|
|
392
|
+
if (!args.subcommand || args.subcommand === '--help' || args.subcommand === '-h') {
|
|
393
|
+
return { ok: true, output: helpText() };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (args.subcommand === 'status') {
|
|
397
|
+
const status = getStatus(deps);
|
|
398
|
+
return { ok: true, output: JSON.stringify(status, null, 2), status };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (args.subcommand === 'install' || args.subcommand === 'remove') {
|
|
402
|
+
if (!ALLOWED_TARGETS.has(args.target)) {
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
output:
|
|
406
|
+
`Error: --target=<launchd|cron|systemd> is required\n\n${helpText()}`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const op = args.subcommand === 'install' ? installFor(args.target) : removeFor(args.target);
|
|
410
|
+
const opOpts = { dryRun: args.dryRun };
|
|
411
|
+
if (deps.homeDir) opOpts.homeDir = deps.homeDir;
|
|
412
|
+
const result = op(opOpts, deps);
|
|
413
|
+
return { ok: true, output: JSON.stringify(result, null, 2), result };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { ok: false, output: `Unknown subcommand: ${args.subcommand}\n\n${helpText()}` };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function installFor(target) {
|
|
420
|
+
return ({ launchd: installLaunchd, cron: installCron, systemd: installSystemd })[target];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function removeFor(target) {
|
|
424
|
+
return ({ launchd: removeLaunchd, cron: removeCron, systemd: removeSystemd })[target];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function helpText() {
|
|
428
|
+
return `flow schedule — install platform-native scheduled-mode units
|
|
429
|
+
|
|
430
|
+
Subcommands:
|
|
431
|
+
install --target=<launchd|cron|systemd> [--dry-run]
|
|
432
|
+
remove --target=<launchd|cron|systemd>
|
|
433
|
+
status
|
|
434
|
+
|
|
435
|
+
Jobs installed: ${SCHEDULED_JOB_NAMES.join(', ')}
|
|
436
|
+
(per-pr-review runs via GH Actions / on-demand only, no schedule entry.)
|
|
437
|
+
`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ============================================================
|
|
441
|
+
// Exports + CLI
|
|
442
|
+
// ============================================================
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
SCHEDULES,
|
|
446
|
+
SCHEDULED_JOB_NAMES,
|
|
447
|
+
ALLOWED_TARGETS,
|
|
448
|
+
generateCrontabLines,
|
|
449
|
+
generateLaunchdPlist,
|
|
450
|
+
generateSystemdServiceUnit,
|
|
451
|
+
generateSystemdTimerUnit,
|
|
452
|
+
installLaunchd,
|
|
453
|
+
installCron,
|
|
454
|
+
installSystemd,
|
|
455
|
+
removeLaunchd,
|
|
456
|
+
removeCron,
|
|
457
|
+
removeSystemd,
|
|
458
|
+
getStatus,
|
|
459
|
+
dispatch,
|
|
460
|
+
parseCliArgs,
|
|
461
|
+
// Re-export for convenience to consumers
|
|
462
|
+
JOB_NAMES,
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (require.main === module) {
|
|
466
|
+
const result = dispatch(process.argv.slice(2));
|
|
467
|
+
if (result.output) console.log(result.output);
|
|
468
|
+
process.exit(result.ok ? 0 : 1);
|
|
469
|
+
}
|