@crewx/cron 0.1.10-rc.13 → 0.1.10-rc.130
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 +22 -8
- package/dist/src/engine.d.ts.map +1 -1
- package/dist/src/engine.js +378 -81
- 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 +3 -3
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,40 +591,105 @@ 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, spawnImpl = child_process_1.spawn) {
|
|
510
690
|
ensureDataDir(dir);
|
|
511
691
|
const cliPath = getDaemonCliPath();
|
|
512
|
-
const child = (
|
|
692
|
+
const child = spawnImpl('node', [cliPath, 'daemon', '--foreground'], {
|
|
513
693
|
cwd: dir || undefined,
|
|
514
694
|
detached: true,
|
|
515
695
|
stdio: 'ignore',
|
|
@@ -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, probe.spawnImpl);
|
|
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, probe.spawnImpl);
|
|
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, probe = DEFAULT_DAEMON_PROCESS_PROBE) {
|
|
749
|
+
if (isDaemonRunning(dir, probe))
|
|
750
|
+
return;
|
|
751
|
+
console.log('⚡ Cron daemon is not running; starting it automatically...');
|
|
752
|
+
const result = startDaemonSafe(dir, probe);
|
|
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,93 @@ function getDaemonStatus(dir) {
|
|
|
573
780
|
},
|
|
574
781
|
};
|
|
575
782
|
}
|
|
576
|
-
async function executeSchedule(schedule,
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
783
|
+
async function executeSchedule(schedule, options) {
|
|
784
|
+
const dir = options?.dir;
|
|
785
|
+
const trigger = options?.trigger ?? 'cron';
|
|
786
|
+
const timeoutMs = schedule.timeout_ms || 600000;
|
|
787
|
+
const taskId = (0, sdk_1.generateId)('tsk');
|
|
788
|
+
if (!schedule.allow_concurrent) {
|
|
789
|
+
const lock = acquireRunLock(schedule.id, taskId, timeoutMs, dir);
|
|
790
|
+
if (!lock.acquired) {
|
|
791
|
+
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} already running (task ${lock.runningTaskId}), skipping`);
|
|
792
|
+
return { status: 'conflict', runningTaskId: lock.runningTaskId };
|
|
793
|
+
}
|
|
580
794
|
}
|
|
581
|
-
|
|
582
|
-
const workingDir = schedule.working_directory || getProjectRoot();
|
|
795
|
+
const workingDir = schedule.working_directory || options?.dir || (0, builtins_1.getProjectRoot)();
|
|
583
796
|
const mode = schedule.mode;
|
|
584
797
|
const modeFlag = mode === 'execute' ? 'x' : 'q';
|
|
585
|
-
|
|
798
|
+
const effectiveModel = options?.overrides?.model ?? schedule.model;
|
|
799
|
+
console.log(`[${getLocalTimestamp()}] Executing schedule ${schedule.id}: ${schedule.name ?? schedule.cron} (task ${taskId})`);
|
|
586
800
|
console.log(`[${getLocalTimestamp()}] Command: ${schedule.command}`);
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
801
|
+
const releaseLease = () => {
|
|
802
|
+
if (!schedule.allow_concurrent)
|
|
803
|
+
releaseRunLock(schedule.id, dir);
|
|
804
|
+
};
|
|
805
|
+
const env = (0, sdk_1.withoutCrewxRootContext)({ ...process.env, ...schedule.env, CREWX_TRACE_ID: taskId });
|
|
806
|
+
delete env['CLAUDECODE'];
|
|
807
|
+
if (!schedule.env || !('CREWX_CONFIG' in schedule.env)) {
|
|
808
|
+
delete env['CREWX_CONFIG'];
|
|
809
|
+
}
|
|
810
|
+
env['CREWX_WORKSPACE'] = workingDir;
|
|
811
|
+
const cliArgs = [modeFlag, schedule.command];
|
|
812
|
+
if (effectiveModel) {
|
|
813
|
+
cliArgs.push('--model', effectiveModel);
|
|
814
|
+
}
|
|
815
|
+
cliArgs.push('--metadata', JSON.stringify({ trigger, schedule_id: schedule.id }));
|
|
816
|
+
let child;
|
|
817
|
+
try {
|
|
818
|
+
if (mode === 'command') {
|
|
819
|
+
child = (0, child_process_1.spawn)('sh', ['-c', schedule.command], {
|
|
592
820
|
cwd: workingDir,
|
|
593
|
-
stdio: '
|
|
821
|
+
stdio: 'ignore',
|
|
594
822
|
windowsHide: true,
|
|
823
|
+
detached: true,
|
|
595
824
|
env,
|
|
596
|
-
})
|
|
597
|
-
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
const resolution = (0, sdk_1.resolveCrewxExecutable)({ env, cwd: workingDir });
|
|
829
|
+
if (!resolution.ok) {
|
|
830
|
+
const error = new Error((0, sdk_1.formatCrewxExecutableFailure)(resolution));
|
|
831
|
+
Object.assign(error, { code: 'CLI_UNRESOLVABLE' });
|
|
832
|
+
throw error;
|
|
833
|
+
}
|
|
834
|
+
const childEnv = {
|
|
835
|
+
...env,
|
|
836
|
+
CREWX_CLI_ARGV: JSON.stringify(resolution.argv),
|
|
837
|
+
};
|
|
838
|
+
const program = (0, sdk_1.resolveWindowsSpawnProgram)({
|
|
839
|
+
command: resolution.argv[0],
|
|
840
|
+
env: childEnv,
|
|
598
841
|
cwd: workingDir,
|
|
599
|
-
|
|
600
|
-
windowsHide: true,
|
|
601
|
-
env,
|
|
842
|
+
packageName: 'crewx',
|
|
602
843
|
});
|
|
844
|
+
const invocation = (0, sdk_1.materializeWindowsSpawnProgram)(program, [
|
|
845
|
+
...resolution.argv.slice(1),
|
|
846
|
+
...cliArgs,
|
|
847
|
+
]);
|
|
848
|
+
child = (0, child_process_1.spawn)(invocation.command, invocation.argv, {
|
|
849
|
+
cwd: workingDir,
|
|
850
|
+
stdio: 'ignore',
|
|
851
|
+
windowsHide: invocation.windowsHide,
|
|
852
|
+
detached: true,
|
|
853
|
+
env: childEnv,
|
|
854
|
+
shell: invocation.shell ?? false,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
await new Promise((resolve, reject) => {
|
|
858
|
+
child.once('spawn', () => resolve());
|
|
859
|
+
child.once('error', (err) => reject(err));
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
catch (e) {
|
|
863
|
+
const err = e;
|
|
864
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to spawn: ${err.message}`);
|
|
865
|
+
releaseLease();
|
|
866
|
+
throw err;
|
|
867
|
+
}
|
|
868
|
+
child.unref();
|
|
869
|
+
const done = new Promise((resolve) => {
|
|
603
870
|
const timeout = setTimeout(() => {
|
|
604
871
|
console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} timeout, killing process`);
|
|
605
872
|
try {
|
|
@@ -609,12 +876,12 @@ async function executeSchedule(schedule, dir) {
|
|
|
609
876
|
const err = e;
|
|
610
877
|
console.error(`Failed to kill process: ${err.message}`);
|
|
611
878
|
}
|
|
612
|
-
|
|
879
|
+
releaseLease();
|
|
613
880
|
resolve({ success: false, error: 'Timeout' });
|
|
614
|
-
},
|
|
881
|
+
}, timeoutMs);
|
|
615
882
|
child.on('close', (code) => {
|
|
616
883
|
clearTimeout(timeout);
|
|
617
|
-
|
|
884
|
+
releaseLease();
|
|
618
885
|
const schedules = loadSchedules(dir);
|
|
619
886
|
const idx = schedules.findIndex(s => s.id === schedule.id);
|
|
620
887
|
if (idx !== -1) {
|
|
@@ -635,11 +902,12 @@ async function executeSchedule(schedule, dir) {
|
|
|
635
902
|
});
|
|
636
903
|
child.on('error', (err) => {
|
|
637
904
|
clearTimeout(timeout);
|
|
638
|
-
|
|
905
|
+
releaseLease();
|
|
639
906
|
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} error: ${err.message}`);
|
|
640
907
|
resolve({ success: false, error: err.message });
|
|
641
908
|
});
|
|
642
909
|
});
|
|
910
|
+
return { status: 'running', taskId, done };
|
|
643
911
|
}
|
|
644
912
|
function getLocalMinuteKey(now, timezone) {
|
|
645
913
|
if (!timezone || !isValidTimezone(timezone)) {
|
|
@@ -668,7 +936,16 @@ async function daemonTick(dir) {
|
|
|
668
936
|
continue;
|
|
669
937
|
}
|
|
670
938
|
}
|
|
671
|
-
|
|
939
|
+
try {
|
|
940
|
+
const outcome = await executeSchedule(schedule, { dir });
|
|
941
|
+
if (outcome.status === 'running') {
|
|
942
|
+
await outcome.done;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
catch (e) {
|
|
946
|
+
const err = e;
|
|
947
|
+
console.error(`[${getLocalTimestamp()}] Schedule ${schedule.id} failed to spawn: ${err.message}`);
|
|
948
|
+
}
|
|
672
949
|
}
|
|
673
950
|
}
|
|
674
951
|
}
|
|
@@ -708,6 +985,7 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
708
985
|
if (options.timezone && !isValidTimezone(options.timezone)) {
|
|
709
986
|
throw new Error(`Invalid timezone: ${options.timezone}`);
|
|
710
987
|
}
|
|
988
|
+
warnIfOverdriveInCommand(command);
|
|
711
989
|
const schedule = {
|
|
712
990
|
id: (0, sdk_1.generateId)('cron'),
|
|
713
991
|
cron,
|
|
@@ -723,6 +1001,9 @@ function addSchedule(cron, command, options, dataDir) {
|
|
|
723
1001
|
updated_at: new Date().toISOString(),
|
|
724
1002
|
run_count: 0,
|
|
725
1003
|
};
|
|
1004
|
+
if (options.model) {
|
|
1005
|
+
schedule.model = options.model;
|
|
1006
|
+
}
|
|
726
1007
|
schedule.next_run = calculateNextRun(cron, schedule.timezone);
|
|
727
1008
|
const schedules = loadSchedules(dataDir);
|
|
728
1009
|
schedules.push(schedule);
|
|
@@ -737,6 +1018,10 @@ function removeSchedule(id, dir) {
|
|
|
737
1018
|
}
|
|
738
1019
|
const removed = schedules.splice(idx, 1)[0];
|
|
739
1020
|
saveSchedules(schedules, dir);
|
|
1021
|
+
const matchedBuiltin = (0, builtins_1.findBuiltinByName)(removed.name);
|
|
1022
|
+
if (matchedBuiltin) {
|
|
1023
|
+
recordRemovedBuiltin(matchedBuiltin.builtin_id, dir);
|
|
1024
|
+
}
|
|
740
1025
|
return removed;
|
|
741
1026
|
}
|
|
742
1027
|
function enableSchedule(id, dir) {
|
|
@@ -777,6 +1062,7 @@ function updateSchedule(id, options, dir) {
|
|
|
777
1062
|
changes.push(`cron: ${options.cron}`);
|
|
778
1063
|
}
|
|
779
1064
|
if (options.command !== undefined) {
|
|
1065
|
+
warnIfOverdriveInCommand(options.command);
|
|
780
1066
|
schedule.command = options.command;
|
|
781
1067
|
changes.push(`command: ${options.command}`);
|
|
782
1068
|
}
|
|
@@ -804,6 +1090,16 @@ function updateSchedule(id, options, dir) {
|
|
|
804
1090
|
schedule.allow_concurrent = options.allow_concurrent;
|
|
805
1091
|
changes.push(`allow_concurrent: ${options.allow_concurrent}`);
|
|
806
1092
|
}
|
|
1093
|
+
if (options.model !== undefined) {
|
|
1094
|
+
if (options.model === null) {
|
|
1095
|
+
delete schedule.model;
|
|
1096
|
+
changes.push('model: (cleared, reverts to agent default)');
|
|
1097
|
+
}
|
|
1098
|
+
else {
|
|
1099
|
+
schedule.model = options.model;
|
|
1100
|
+
changes.push(`model: ${options.model}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
807
1103
|
schedule.updated_at = new Date().toISOString();
|
|
808
1104
|
schedule.next_run = calculateNextRun(schedule.cron, schedule.timezone);
|
|
809
1105
|
saveSchedules(schedules, dir);
|
|
@@ -815,5 +1111,6 @@ exports.DATA_PATHS = {
|
|
|
815
1111
|
LOG_FILE,
|
|
816
1112
|
USAGE_LOG_FILE,
|
|
817
1113
|
PID_FILE,
|
|
1114
|
+
BUILTINS_META_FILE,
|
|
818
1115
|
};
|
|
819
1116
|
//# sourceMappingURL=engine.js.map
|