@crewx/cron 0.1.10-rc.1 → 0.1.10-rc.100
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/dist/cli.d.ts +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +27 -11
- package/dist/cli.js.map +1 -1
- package/dist/src/builtins.d.ts +14 -0
- package/dist/src/builtins.d.ts.map +1 -0
- package/dist/src/builtins.js +71 -0
- package/dist/src/builtins.js.map +1 -0
- package/dist/src/engine.d.ts +19 -7
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +372 -79
- package/dist/src/engine.js.map +1 -1
- package/dist/src/types.d.ts +24 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +10 -11
package/dist/src/engine.js
CHANGED
|
@@ -33,13 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DATA_PATHS = void 0;
|
|
36
|
+
exports.DATA_PATHS = exports.getProjectRoot = exports.BUILTIN_SCHEDULES = void 0;
|
|
37
37
|
exports.getLocalTimestamp = getLocalTimestamp;
|
|
38
38
|
exports.logUsage = logUsage;
|
|
39
|
-
exports.getProjectRoot = getProjectRoot;
|
|
40
39
|
exports.normalizeMode = normalizeMode;
|
|
41
40
|
exports.loadSchedules = loadSchedules;
|
|
42
41
|
exports.saveSchedules = saveSchedules;
|
|
42
|
+
exports.getRemovedBuiltins = getRemovedBuiltins;
|
|
43
|
+
exports.seedBuiltinSchedules = seedBuiltinSchedules;
|
|
43
44
|
exports.acquireLock = acquireLock;
|
|
44
45
|
exports.releaseLock = releaseLock;
|
|
45
46
|
exports.shouldRunNow = shouldRunNow;
|
|
@@ -48,6 +49,7 @@ exports.calculateNextRun = calculateNextRun;
|
|
|
48
49
|
exports.isDaemonRunning = isDaemonRunning;
|
|
49
50
|
exports.startDaemon = startDaemon;
|
|
50
51
|
exports.stopDaemon = stopDaemon;
|
|
52
|
+
exports.startDaemonSafe = startDaemonSafe;
|
|
51
53
|
exports.ensureDaemonRunning = ensureDaemonRunning;
|
|
52
54
|
exports.getDaemonStatus = getDaemonStatus;
|
|
53
55
|
exports.executeSchedule = executeSchedule;
|
|
@@ -62,12 +64,16 @@ const child_process_1 = require("child_process");
|
|
|
62
64
|
const path = __importStar(require("path"));
|
|
63
65
|
const fs = __importStar(require("fs"));
|
|
64
66
|
const sdk_1 = require("@crewx/sdk");
|
|
67
|
+
const builtins_1 = require("./builtins");
|
|
68
|
+
Object.defineProperty(exports, "BUILTIN_SCHEDULES", { enumerable: true, get: function () { return builtins_1.BUILTIN_SCHEDULES; } });
|
|
69
|
+
Object.defineProperty(exports, "getProjectRoot", { enumerable: true, get: function () { return builtins_1.getProjectRoot; } });
|
|
65
70
|
const DATA_DIR = path.join(process.cwd(), '.crewx');
|
|
66
71
|
const SCHEDULES_FILE = path.join(DATA_DIR, '.cron-data.json');
|
|
67
72
|
const LOG_FILE = path.join(DATA_DIR, '.cron-daemon.log');
|
|
68
73
|
const LOCK_FILE = path.join(DATA_DIR, '.cron-data.json.lock');
|
|
69
74
|
const USAGE_LOG_FILE = path.join(DATA_DIR, 'cron-usage.log');
|
|
70
75
|
const PID_FILE = path.join(DATA_DIR, '.cron-daemon.pid');
|
|
76
|
+
const BUILTINS_META_FILE = path.join(DATA_DIR, '.cron-builtins.json');
|
|
71
77
|
function resolveDataDir(dir) {
|
|
72
78
|
return dir ? path.join(dir, '.crewx') : DATA_DIR;
|
|
73
79
|
}
|
|
@@ -77,8 +83,14 @@ function resolveSchedulesFile(dir) {
|
|
|
77
83
|
function resolvePidFile(dir) {
|
|
78
84
|
return path.join(resolveDataDir(dir), '.cron-daemon.pid');
|
|
79
85
|
}
|
|
86
|
+
function resolveBuiltinsMetaFile(dir) {
|
|
87
|
+
return path.join(resolveDataDir(dir), '.cron-builtins.json');
|
|
88
|
+
}
|
|
89
|
+
function resolveRunLockFile(scheduleId, dir) {
|
|
90
|
+
return path.join(resolveDataDir(dir), `.cron-run-${scheduleId}.lock`);
|
|
91
|
+
}
|
|
80
92
|
const DAEMON_INTERVAL_MS = 60 * 1000;
|
|
81
|
-
const
|
|
93
|
+
const RUN_LOCK_STALE_GRACE_MS = 5000;
|
|
82
94
|
function getLocalTimestamp() {
|
|
83
95
|
const now = new Date();
|
|
84
96
|
const y = now.getFullYear();
|
|
@@ -110,27 +122,16 @@ function safeReadJsonFile(filePath) {
|
|
|
110
122
|
return null;
|
|
111
123
|
}
|
|
112
124
|
}
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
return (0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return process.cwd();
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
function getBuiltinMemoryCliPath() {
|
|
122
|
-
return path.join(getProjectRoot(), 'packages', 'built-in', 'memory', 'dist', 'cli.js');
|
|
123
|
-
}
|
|
125
|
+
const MEMORY_DEBOUNCE_BUILTIN = (0, builtins_1.findBuiltinById)('memory-debounce');
|
|
124
126
|
function maybeRewriteLegacyMemoryCommand(command) {
|
|
125
127
|
if (!command.includes('/skills/memory-v2/memory-v2.js')) {
|
|
126
128
|
return command;
|
|
127
129
|
}
|
|
128
|
-
|
|
129
|
-
return `node ${builtinMemoryCli} summarize-dirty`;
|
|
130
|
+
return MEMORY_DEBOUNCE_BUILTIN.buildCommand();
|
|
130
131
|
}
|
|
131
132
|
function migrateLegacyMemoryDebounceSchedule() {
|
|
132
133
|
ensureDataDir();
|
|
133
|
-
const legacyFile = path.join(getProjectRoot(), 'skills', 'cron', '.cron-data.json');
|
|
134
|
+
const legacyFile = path.join((0, builtins_1.getProjectRoot)(), 'skills', 'cron', '.cron-data.json');
|
|
134
135
|
if (!fs.existsSync(legacyFile)) {
|
|
135
136
|
return;
|
|
136
137
|
}
|
|
@@ -138,12 +139,12 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
138
139
|
if (!Array.isArray(legacySchedules)) {
|
|
139
140
|
return;
|
|
140
141
|
}
|
|
141
|
-
const legacySchedule = legacySchedules.find(s => s.name ===
|
|
142
|
+
const legacySchedule = legacySchedules.find(s => s.name === MEMORY_DEBOUNCE_BUILTIN.name);
|
|
142
143
|
if (!legacySchedule) {
|
|
143
144
|
return;
|
|
144
145
|
}
|
|
145
146
|
const targetSchedules = fs.existsSync(SCHEDULES_FILE) ? safeReadJsonFile(SCHEDULES_FILE) ?? [] : [];
|
|
146
|
-
if (targetSchedules.some(s => s.name ===
|
|
147
|
+
if (targetSchedules.some(s => s.name === MEMORY_DEBOUNCE_BUILTIN.name)) {
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
150
|
const migratedSchedule = {
|
|
@@ -154,6 +155,11 @@ function migrateLegacyMemoryDebounceSchedule() {
|
|
|
154
155
|
};
|
|
155
156
|
saveSchedules([...targetSchedules, migratedSchedule]);
|
|
156
157
|
}
|
|
158
|
+
function warnIfOverdriveInCommand(command) {
|
|
159
|
+
if (command.includes('--overdrive')) {
|
|
160
|
+
console.warn('warning: --overdrive in cron command is ignored and may silently override --model');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
157
163
|
function normalizeMode(mode) {
|
|
158
164
|
if (mode === 'query')
|
|
159
165
|
return 'query';
|
|
@@ -187,6 +193,61 @@ function saveSchedules(schedules, dir) {
|
|
|
187
193
|
fs.writeFileSync(tempFile, JSON.stringify(schedules, null, 2));
|
|
188
194
|
fs.renameSync(tempFile, schedulesFile);
|
|
189
195
|
}
|
|
196
|
+
function loadBuiltinsMeta(dir) {
|
|
197
|
+
const data = safeReadJsonFile(resolveBuiltinsMetaFile(dir));
|
|
198
|
+
return { removed_builtins: Array.isArray(data?.removed_builtins) ? data.removed_builtins : [] };
|
|
199
|
+
}
|
|
200
|
+
function saveBuiltinsMeta(meta, dir) {
|
|
201
|
+
ensureDataDir(dir);
|
|
202
|
+
const metaFile = resolveBuiltinsMetaFile(dir);
|
|
203
|
+
const tempFile = `${metaFile}.tmp`;
|
|
204
|
+
fs.writeFileSync(tempFile, JSON.stringify(meta, null, 2));
|
|
205
|
+
fs.renameSync(tempFile, metaFile);
|
|
206
|
+
}
|
|
207
|
+
function getRemovedBuiltins(dir) {
|
|
208
|
+
return loadBuiltinsMeta(dir).removed_builtins;
|
|
209
|
+
}
|
|
210
|
+
function recordRemovedBuiltin(builtinId, dir) {
|
|
211
|
+
const meta = loadBuiltinsMeta(dir);
|
|
212
|
+
if (!meta.removed_builtins.includes(builtinId)) {
|
|
213
|
+
meta.removed_builtins.push(builtinId);
|
|
214
|
+
saveBuiltinsMeta(meta, dir);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function seedBuiltinSchedules(dir) {
|
|
218
|
+
const schedules = loadSchedules(dir);
|
|
219
|
+
const existingNames = new Set(schedules.map(s => s.name));
|
|
220
|
+
const removedBuiltins = new Set(getRemovedBuiltins(dir));
|
|
221
|
+
const seeded = [];
|
|
222
|
+
for (const def of builtins_1.BUILTIN_SCHEDULES) {
|
|
223
|
+
if (existingNames.has(def.name))
|
|
224
|
+
continue;
|
|
225
|
+
if (removedBuiltins.has(def.builtin_id))
|
|
226
|
+
continue;
|
|
227
|
+
const now = new Date().toISOString();
|
|
228
|
+
schedules.push({
|
|
229
|
+
id: (0, sdk_1.generateId)('cron'),
|
|
230
|
+
cron: def.cron,
|
|
231
|
+
command: def.buildCommand(),
|
|
232
|
+
mode: def.mode,
|
|
233
|
+
enabled: true,
|
|
234
|
+
name: def.name,
|
|
235
|
+
working_directory: null,
|
|
236
|
+
timezone: null,
|
|
237
|
+
timeout_ms: 600000,
|
|
238
|
+
allow_concurrent: false,
|
|
239
|
+
created_at: now,
|
|
240
|
+
updated_at: now,
|
|
241
|
+
run_count: 0,
|
|
242
|
+
next_run: calculateNextRun(def.cron, null),
|
|
243
|
+
});
|
|
244
|
+
seeded.push(def.builtin_id);
|
|
245
|
+
}
|
|
246
|
+
if (seeded.length > 0) {
|
|
247
|
+
saveSchedules(schedules, dir);
|
|
248
|
+
}
|
|
249
|
+
return seeded;
|
|
250
|
+
}
|
|
190
251
|
async function acquireLock(timeout = 5000) {
|
|
191
252
|
ensureDataDir();
|
|
192
253
|
const startTime = Date.now();
|
|
@@ -224,6 +285,58 @@ function releaseLock() {
|
|
|
224
285
|
catch {
|
|
225
286
|
}
|
|
226
287
|
}
|
|
288
|
+
function readRunLease(lockFile) {
|
|
289
|
+
return safeReadJsonFile(lockFile);
|
|
290
|
+
}
|
|
291
|
+
function acquireRunLock(scheduleId, taskId, timeoutMs, dir) {
|
|
292
|
+
ensureDataDir(dir);
|
|
293
|
+
const lockFile = resolveRunLockFile(scheduleId, dir);
|
|
294
|
+
const lease = { pid: process.pid, taskId, startedAt: new Date().toISOString() };
|
|
295
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
296
|
+
try {
|
|
297
|
+
fs.writeFileSync(lockFile, JSON.stringify(lease), { flag: 'wx' });
|
|
298
|
+
return { acquired: true };
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
const err = e;
|
|
302
|
+
if (err.code !== 'EEXIST')
|
|
303
|
+
throw err;
|
|
304
|
+
const existing = readRunLease(lockFile);
|
|
305
|
+
if (!existing) {
|
|
306
|
+
try {
|
|
307
|
+
fs.unlinkSync(lockFile);
|
|
308
|
+
}
|
|
309
|
+
catch { }
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let ageMs;
|
|
313
|
+
try {
|
|
314
|
+
ageMs = Date.now() - fs.statSync(lockFile).mtimeMs;
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
const stale = !isPidAlive(existing.pid) || ageMs > timeoutMs + RUN_LOCK_STALE_GRACE_MS;
|
|
320
|
+
if (stale) {
|
|
321
|
+
try {
|
|
322
|
+
fs.unlinkSync(lockFile);
|
|
323
|
+
}
|
|
324
|
+
catch { }
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
return { acquired: false, runningTaskId: existing.taskId };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const existing = readRunLease(lockFile);
|
|
331
|
+
return { acquired: false, runningTaskId: existing?.taskId ?? taskId };
|
|
332
|
+
}
|
|
333
|
+
function releaseRunLock(scheduleId, dir) {
|
|
334
|
+
try {
|
|
335
|
+
fs.unlinkSync(resolveRunLockFile(scheduleId, dir));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
}
|
|
339
|
+
}
|
|
227
340
|
const DAY_NAMES = {
|
|
228
341
|
SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6,
|
|
229
342
|
};
|
|
@@ -452,8 +565,10 @@ function calculateNextRun(cronExpr, timezone) {
|
|
|
452
565
|
function readDaemonPid(dir) {
|
|
453
566
|
try {
|
|
454
567
|
const raw = fs.readFileSync(resolvePidFile(dir), 'utf8').trim();
|
|
455
|
-
|
|
456
|
-
|
|
568
|
+
if (!/^\d+$/.test(raw))
|
|
569
|
+
return null;
|
|
570
|
+
const pid = Number(raw);
|
|
571
|
+
return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
457
572
|
}
|
|
458
573
|
catch {
|
|
459
574
|
return null;
|
|
@@ -476,37 +591,102 @@ function isPidAlive(pid) {
|
|
|
476
591
|
return true;
|
|
477
592
|
}
|
|
478
593
|
catch (e) {
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
return true;
|
|
482
|
-
return false;
|
|
594
|
+
const error = e;
|
|
595
|
+
return error.code === 'ESRCH' ? false : 'unknown';
|
|
483
596
|
}
|
|
484
597
|
}
|
|
485
|
-
function
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
598
|
+
function getProcessCommandLine(pid) {
|
|
599
|
+
try {
|
|
600
|
+
const output = process.platform === 'win32'
|
|
601
|
+
? (0, child_process_1.execFileSync)('powershell.exe', [
|
|
602
|
+
'-NoLogo',
|
|
603
|
+
'-NoProfile',
|
|
604
|
+
'-NonInteractive',
|
|
605
|
+
'-Command',
|
|
606
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}").CommandLine`,
|
|
607
|
+
], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
|
|
608
|
+
: (0, child_process_1.execFileSync)('ps', ['-ww', '-p', String(pid), '-o', 'command='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
609
|
+
return output.trim() || null;
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return null;
|
|
489
613
|
}
|
|
490
|
-
throw new Error(`Cron CLI entrypoint not found: ${distCli}`);
|
|
491
614
|
}
|
|
492
|
-
|
|
615
|
+
const DEFAULT_DAEMON_PROCESS_PROBE = {
|
|
616
|
+
isAlive: isPidAlive,
|
|
617
|
+
getCommandLine: getProcessCommandLine,
|
|
618
|
+
};
|
|
619
|
+
function normalizeCommandLine(value) {
|
|
620
|
+
const normalized = value.replace(/\\/g, '/');
|
|
621
|
+
return process.platform === 'win32' ? normalized.toLowerCase() : normalized;
|
|
622
|
+
}
|
|
623
|
+
function getDaemonCliPathTail() {
|
|
624
|
+
try {
|
|
625
|
+
const segments = normalizeCommandLine(getDaemonCliPath()).split('/').filter(Boolean);
|
|
626
|
+
const tail = segments.slice(-3).join('/');
|
|
627
|
+
return tail || null;
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function classifyDaemonIdentity(commandLine) {
|
|
634
|
+
if (!commandLine?.trim())
|
|
635
|
+
return 'unknown';
|
|
636
|
+
const cliPathTail = getDaemonCliPathTail();
|
|
637
|
+
if (!cliPathTail)
|
|
638
|
+
return 'unknown';
|
|
639
|
+
const normalizedCommandLine = normalizeCommandLine(commandLine);
|
|
640
|
+
return normalizedCommandLine.includes('daemon --foreground')
|
|
641
|
+
&& normalizedCommandLine.includes(cliPathTail)
|
|
642
|
+
? 'running'
|
|
643
|
+
: 'stale';
|
|
644
|
+
}
|
|
645
|
+
function getDaemonCliPath() {
|
|
646
|
+
const candidates = [
|
|
647
|
+
path.join(__dirname, '..', 'cli.js'),
|
|
648
|
+
path.join(__dirname, '..', 'dist', 'cli.js'),
|
|
649
|
+
];
|
|
650
|
+
const cliPath = candidates.find(candidate => fs.existsSync(candidate));
|
|
651
|
+
if (cliPath) {
|
|
652
|
+
return cliPath;
|
|
653
|
+
}
|
|
654
|
+
throw new Error(`Cron CLI entrypoint not found: ${candidates[0]}`);
|
|
655
|
+
}
|
|
656
|
+
function inspectDaemon(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
493
657
|
const pid = readDaemonPid(dir);
|
|
494
658
|
if (!pid) {
|
|
495
|
-
|
|
659
|
+
removeDaemonPidFile(dir);
|
|
660
|
+
return 'stale';
|
|
661
|
+
}
|
|
662
|
+
let alive;
|
|
663
|
+
try {
|
|
664
|
+
alive = probe.isAlive(pid);
|
|
665
|
+
}
|
|
666
|
+
catch {
|
|
667
|
+
alive = 'unknown';
|
|
496
668
|
}
|
|
497
|
-
const alive = isPidAlive(pid);
|
|
498
669
|
if (!alive) {
|
|
499
670
|
removeDaemonPidFile(dir);
|
|
671
|
+
return 'stale';
|
|
500
672
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
673
|
+
let commandLine;
|
|
674
|
+
try {
|
|
675
|
+
commandLine = probe.getCommandLine(pid);
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
return 'unknown';
|
|
679
|
+
}
|
|
680
|
+
const liveness = classifyDaemonIdentity(commandLine);
|
|
681
|
+
if (liveness === 'stale') {
|
|
682
|
+
removeDaemonPidFile(dir);
|
|
509
683
|
}
|
|
684
|
+
return liveness;
|
|
685
|
+
}
|
|
686
|
+
function isDaemonRunning(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
687
|
+
return inspectDaemon(dir, probe) === 'running';
|
|
688
|
+
}
|
|
689
|
+
function spawnDaemon(dir) {
|
|
510
690
|
ensureDataDir(dir);
|
|
511
691
|
const cliPath = getDaemonCliPath();
|
|
512
692
|
const child = (0, child_process_1.spawn)('node', [cliPath, 'daemon', '--foreground'], {
|
|
@@ -522,37 +702,64 @@ function startDaemon(dir) {
|
|
|
522
702
|
writeDaemonPid(child.pid, dir);
|
|
523
703
|
return child.pid;
|
|
524
704
|
}
|
|
525
|
-
function
|
|
526
|
-
const
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
705
|
+
function startDaemon(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
706
|
+
const liveness = inspectDaemon(dir, probe);
|
|
707
|
+
if (liveness === 'running') {
|
|
708
|
+
const pid = readDaemonPid(dir);
|
|
709
|
+
if (pid)
|
|
710
|
+
return pid;
|
|
711
|
+
throw new Error('Daemon is already running');
|
|
530
712
|
}
|
|
531
|
-
if (
|
|
532
|
-
|
|
533
|
-
|
|
713
|
+
if (liveness === 'unknown') {
|
|
714
|
+
const pid = readDaemonPid(dir);
|
|
715
|
+
throw new Error(`Unable to determine cron daemon liveness (pid=${pid ?? 'unknown'}, pidFile=${resolvePidFile(dir)}); refusing to start`);
|
|
534
716
|
}
|
|
717
|
+
return spawnDaemon(dir);
|
|
718
|
+
}
|
|
719
|
+
function stopDaemon(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
720
|
+
if (inspectDaemon(dir, probe) !== 'running')
|
|
721
|
+
return false;
|
|
722
|
+
const pid = readDaemonPid(dir);
|
|
723
|
+
if (!pid)
|
|
724
|
+
return false;
|
|
535
725
|
process.kill(pid, 'SIGTERM');
|
|
536
726
|
return true;
|
|
537
727
|
}
|
|
538
|
-
function
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
728
|
+
function startDaemonSafe(dir, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
729
|
+
const liveness = inspectDaemon(dir, probe);
|
|
730
|
+
if (liveness === 'running') {
|
|
731
|
+
return { started: false };
|
|
732
|
+
}
|
|
733
|
+
if (liveness === 'unknown') {
|
|
734
|
+
return {
|
|
735
|
+
started: false,
|
|
736
|
+
error: `Unable to determine cron daemon liveness (pid=${readDaemonPid(dir) ?? 'unknown'}, pidFile=${resolvePidFile(dir)}); refusing to start`,
|
|
737
|
+
};
|
|
738
|
+
}
|
|
542
739
|
try {
|
|
543
|
-
|
|
740
|
+
const pid = spawnDaemon(dir);
|
|
741
|
+
return { started: true, pid };
|
|
544
742
|
}
|
|
545
743
|
catch (e) {
|
|
546
744
|
const err = e;
|
|
547
|
-
|
|
745
|
+
return { started: false, error: err.message };
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
function ensureDaemonRunning(dir) {
|
|
749
|
+
if (isDaemonRunning(dir))
|
|
750
|
+
return;
|
|
751
|
+
console.log('⚡ Cron daemon is not running; starting it automatically...');
|
|
752
|
+
const result = startDaemonSafe(dir);
|
|
753
|
+
if (!result.started) {
|
|
754
|
+
console.error('Failed to auto-start daemon:', result.error);
|
|
548
755
|
process.exit(1);
|
|
549
756
|
}
|
|
550
757
|
}
|
|
551
758
|
function getDaemonStatus(dir) {
|
|
552
759
|
const schedules = loadSchedules(dir);
|
|
553
760
|
const enabled = schedules.filter(s => s.enabled).length;
|
|
554
|
-
const
|
|
555
|
-
const
|
|
761
|
+
const running = isDaemonRunning(dir);
|
|
762
|
+
const pid = running ? readDaemonPid(dir) : null;
|
|
556
763
|
let started_at = null;
|
|
557
764
|
const pidFile = resolvePidFile(dir);
|
|
558
765
|
if (running && fs.existsSync(pidFile)) {
|
|
@@ -573,33 +780,89 @@ function getDaemonStatus(dir) {
|
|
|
573
780
|
},
|
|
574
781
|
};
|
|
575
782
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
return {
|
|
783
|
+
function resolveCliBinAndBaseArgs(crewxCli) {
|
|
784
|
+
const parts = crewxCli?.trim().split(/\s+/).filter(Boolean) ?? [];
|
|
785
|
+
if (parts.length === 0)
|
|
786
|
+
return { bin: 'crewx', baseArgs: [] };
|
|
787
|
+
return { bin: parts[0], baseArgs: parts.slice(1) };
|
|
788
|
+
}
|
|
789
|
+
async function executeSchedule(schedule, options) {
|
|
790
|
+
const dir = options?.dir;
|
|
791
|
+
const trigger = options?.trigger ?? 'cron';
|
|
792
|
+
const timeoutMs = schedule.timeout_ms || 600000;
|
|
793
|
+
const taskId = (0, sdk_1.generateId)('tsk');
|
|
794
|
+
if (!schedule.allow_concurrent) {
|
|
795
|
+
const lock = acquireRunLock(schedule.id, taskId, timeoutMs, dir);
|
|
796
|
+
if (!lock.acquired) {
|
|
797
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} already running (task ${lock.runningTaskId}), skipping`);
|
|
798
|
+
return { status: 'conflict', runningTaskId: lock.runningTaskId };
|
|
799
|
+
}
|
|
580
800
|
}
|
|
581
|
-
|
|
582
|
-
const workingDir = schedule.working_directory || getProjectRoot();
|
|
801
|
+
const workingDir = schedule.working_directory || options?.dir || (0, builtins_1.getProjectRoot)();
|
|
583
802
|
const mode = schedule.mode;
|
|
584
803
|
const modeFlag = mode === 'execute' ? 'x' : 'q';
|
|
585
|
-
|
|
804
|
+
const effectiveModel = options?.overrides?.model ?? schedule.model;
|
|
805
|
+
console.log(`[${getLocalTimestamp()}] Executing schedule ${schedule.id}: ${schedule.name ?? schedule.cron} (task ${taskId})`);
|
|
586
806
|
console.log(`[${getLocalTimestamp()}] Command: ${schedule.command}`);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
807
|
+
const releaseLease = () => {
|
|
808
|
+
if (!schedule.allow_concurrent)
|
|
809
|
+
releaseRunLock(schedule.id, dir);
|
|
810
|
+
};
|
|
811
|
+
const env = { ...process.env, ...schedule.env, CREWX_TRACE_ID: taskId };
|
|
812
|
+
delete env['CLAUDECODE'];
|
|
813
|
+
delete env['CREWX_PARENT_TASK_ID'];
|
|
814
|
+
delete env['CREWX_CALLER_AGENT_ID'];
|
|
815
|
+
if (!schedule.env || !('CREWX_CONFIG' in schedule.env)) {
|
|
816
|
+
delete env['CREWX_CONFIG'];
|
|
817
|
+
}
|
|
818
|
+
env['CREWX_WORKSPACE'] = workingDir;
|
|
819
|
+
const cliArgs = [modeFlag, schedule.command];
|
|
820
|
+
if (effectiveModel) {
|
|
821
|
+
cliArgs.push('--model', effectiveModel);
|
|
822
|
+
}
|
|
823
|
+
cliArgs.push('--metadata', JSON.stringify({ trigger, schedule_id: schedule.id }));
|
|
824
|
+
let child;
|
|
825
|
+
try {
|
|
826
|
+
if (mode === 'command') {
|
|
827
|
+
child = (0, child_process_1.spawn)('sh', ['-c', schedule.command], {
|
|
592
828
|
cwd: workingDir,
|
|
593
|
-
stdio: '
|
|
829
|
+
stdio: 'ignore',
|
|
594
830
|
windowsHide: true,
|
|
831
|
+
detached: true,
|
|
832
|
+
env,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
const { bin, baseArgs } = resolveCliBinAndBaseArgs(env['CREWX_CLI']);
|
|
837
|
+
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
838
|
+
command: bin,
|
|
595
839
|
env,
|
|
596
|
-
})
|
|
597
|
-
: (0, child_process_1.spawn)('crewx', [modeFlag, schedule.command], {
|
|
598
840
|
cwd: workingDir,
|
|
599
|
-
|
|
600
|
-
|
|
841
|
+
packageName: 'crewx',
|
|
842
|
+
});
|
|
843
|
+
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, [...baseArgs, ...cliArgs]);
|
|
844
|
+
child = (0, child_process_1.spawn)(invocation.command, invocation.argv, {
|
|
845
|
+
cwd: workingDir,
|
|
846
|
+
stdio: 'ignore',
|
|
847
|
+
windowsHide: invocation.windowsHide,
|
|
848
|
+
detached: true,
|
|
601
849
|
env,
|
|
850
|
+
shell: invocation.shell ?? false,
|
|
602
851
|
});
|
|
852
|
+
}
|
|
853
|
+
await new Promise((resolve, reject) => {
|
|
854
|
+
child.once('spawn', () => resolve());
|
|
855
|
+
child.once('error', (err) => reject(err));
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
catch (e) {
|
|
859
|
+
const err = e;
|
|
860
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to spawn: ${err.message}`);
|
|
861
|
+
releaseLease();
|
|
862
|
+
throw err;
|
|
863
|
+
}
|
|
864
|
+
child.unref();
|
|
865
|
+
const done = new Promise((resolve) => {
|
|
603
866
|
const timeout = setTimeout(() => {
|
|
604
867
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} timeout, killing process`);
|
|
605
868
|
try {
|
|
@@ -609,12 +872,12 @@ async function executeSchedule(schedule, dir) {
|
|
|
609
872
|
const err = e;
|
|
610
873
|
console.error(`Failed to kill process: ${err.message}`);
|
|
611
874
|
}
|
|
612
|
-
|
|
875
|
+
releaseLease();
|
|
613
876
|
resolve({ success: false, error: 'Timeout' });
|
|
614
|
-
},
|
|
877
|
+
}, timeoutMs);
|
|
615
878
|
child.on('close', (code) => {
|
|
616
879
|
clearTimeout(timeout);
|
|
617
|
-
|
|
880
|
+
releaseLease();
|
|
618
881
|
const schedules = loadSchedules(dir);
|
|
619
882
|
const idx = schedules.findIndex(s => s.id === schedule.id);
|
|
620
883
|
if (idx !== -1) {
|
|
@@ -635,11 +898,12 @@ async function executeSchedule(schedule, dir) {
|
|
|
635
898
|
});
|
|
636
899
|
child.on('error', (err) => {
|
|
637
900
|
clearTimeout(timeout);
|
|
638
|
-
|
|
901
|
+
releaseLease();
|
|
639
902
|
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} error: ${err.message}`);
|
|
640
903
|
resolve({ success: false, error: err.message });
|
|
641
904
|
});
|
|
642
905
|
});
|
|
906
|
+
return { status: 'running', taskId, done };
|
|
643
907
|
}
|
|
644
908
|
function getLocalMinuteKey(now, timezone) {
|
|
645
909
|
if (!timezone || !isValidTimezone(timezone)) {
|
|
@@ -668,7 +932,16 @@ async function daemonTick(dir) {
|
|
|
668
932
|
continue;
|
|
669
933
|
}
|
|
670
934
|
}
|
|
671
|
-
|
|
935
|
+
try {
|
|
936
|
+
const outcome = await executeSchedule(schedule, { dir });
|
|
937
|
+
if (outcome.status === 'running') {
|
|
938
|
+
await outcome.done;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
catch (e) {
|
|
942
|
+
const err = e;
|
|
943
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to spawn: ${err.message}`);
|
|
944
|
+
}
|
|
672
945
|
}
|
|
673
946
|
}
|
|
674
947
|
}
|
|
@@ -708,6 +981,7 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
708
981
|
if (options.timezone && !isValidTimezone(options.timezone)) {
|
|
709
982
|
throw new Error(`Invalid timezone: ${options.timezone}`);
|
|
710
983
|
}
|
|
984
|
+
warnIfOverdriveInCommand(command);
|
|
711
985
|
const schedule = {
|
|
712
986
|
id: (0, sdk_1.generateId)('cron'),
|
|
713
987
|
cron,
|
|
@@ -723,6 +997,9 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
723
997
|
updated_at: new Date().toISOString(),
|
|
724
998
|
run_count: 0,
|
|
725
999
|
};
|
|
1000
|
+
if (options.model) {
|
|
1001
|
+
schedule.model = options.model;
|
|
1002
|
+
}
|
|
726
1003
|
schedule.next_run = calculateNextRun(cron, schedule.timezone);
|
|
727
1004
|
const schedules = loadSchedules(dataDir);
|
|
728
1005
|
schedules.push(schedule);
|
|
@@ -737,6 +1014,10 @@ function removeSchedule(id, dir) {
|
|
|
737
1014
|
}
|
|
738
1015
|
const removed = schedules.splice(idx, 1)[0];
|
|
739
1016
|
saveSchedules(schedules, dir);
|
|
1017
|
+
const matchedBuiltin = (0, builtins_1.findBuiltinByName)(removed.name);
|
|
1018
|
+
if (matchedBuiltin) {
|
|
1019
|
+
recordRemovedBuiltin(matchedBuiltin.builtin_id, dir);
|
|
1020
|
+
}
|
|
740
1021
|
return removed;
|
|
741
1022
|
}
|
|
742
1023
|
function enableSchedule(id, dir) {
|
|
@@ -777,6 +1058,7 @@ function updateSchedule(id, options, dir) {
|
|
|
777
1058
|
changes.push(`cron: ${options.cron}`);
|
|
778
1059
|
}
|
|
779
1060
|
if (options.command !== undefined) {
|
|
1061
|
+
warnIfOverdriveInCommand(options.command);
|
|
780
1062
|
schedule.command = options.command;
|
|
781
1063
|
changes.push(`command: ${options.command}`);
|
|
782
1064
|
}
|
|
@@ -804,6 +1086,16 @@ function updateSchedule(id, options, dir) {
|
|
|
804
1086
|
schedule.allow_concurrent = options.allow_concurrent;
|
|
805
1087
|
changes.push(`allow_concurrent: ${options.allow_concurrent}`);
|
|
806
1088
|
}
|
|
1089
|
+
if (options.model !== undefined) {
|
|
1090
|
+
if (options.model === null) {
|
|
1091
|
+
delete schedule.model;
|
|
1092
|
+
changes.push('model: (cleared, reverts to agent default)');
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
schedule.model = options.model;
|
|
1096
|
+
changes.push(`model: ${options.model}`);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
807
1099
|
schedule.updated_at = new Date().toISOString();
|
|
808
1100
|
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
809
1101
|
saveSchedules(schedules, dir);
|
|
@@ -815,5 +1107,6 @@ exports.DATA_PATHS = {
|
|
|
815
1107
|
LOG_FILE,
|
|
816
1108
|
USAGE_LOG_FILE,
|
|
817
1109
|
PID_FILE,
|
|
1110
|
+
BUILTINS_META_FILE,
|
|
818
1111
|
};
|
|
819
1112
|
//# sourceMappingURL=engine.js.map
|