@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.
@@ -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 runningSchedules = new Set();
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
- function getProjectRoot() {
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
- const builtinMemoryCli = getBuiltinMemoryCliPath();
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 === 'memory-debounce');
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 === 'memory-debounce')) {
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
- const pid = Number.parseInt(raw, 10);
456
- return Number.isInteger(pid) && pid > 0 ? pid : null;
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 err = e;
480
- if (err.code === 'EPERM')
481
- return true;
482
- return false;
594
+ const error = e;
595
+ return error.code === 'ESRCH' ? false : 'unknown';
483
596
  }
484
597
  }
485
- function getDaemonCliPath() {
486
- const distCli = path.join(__dirname, '..', 'cli.js');
487
- if (fs.existsSync(distCli)) {
488
- return distCli;
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
- function isDaemonRunning(dir) {
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
- return false;
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
- return alive;
502
- }
503
- function startDaemon(dir) {
504
- if (isDaemonRunning(dir)) {
505
- const pid = readDaemonPid(dir);
506
- if (pid)
507
- return pid;
508
- throw new Error('Daemon is already running');
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 = (0, child_process_1.spawn)('node', [cliPath, 'daemon', '--foreground'], {
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 stopDaemon(dir) {
526
- const pid = readDaemonPid(dir);
527
- if (!pid) {
528
- removeDaemonPidFile(dir);
529
- return false;
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 (!isPidAlive(pid)) {
532
- removeDaemonPidFile(dir);
533
- return false;
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 ensureDaemonRunning(dir) {
539
- if (isDaemonRunning(dir))
540
- return;
541
- console.log('⚡ cron 데몬이 꺼져 있어서 자동 시작합니다...');
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
- startDaemon(dir);
740
+ const pid = spawnDaemon(dir, probe.spawnImpl);
741
+ return { started: true, pid };
544
742
  }
545
743
  catch (e) {
546
744
  const err = e;
547
- console.error('Failed to auto-start daemon:', err.message);
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 pid = readDaemonPid(dir);
555
- const running = pid !== null && isDaemonRunning(dir);
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, dir) {
577
- if (!schedule.allow_concurrent && runningSchedules.has(schedule.id)) {
578
- console.log(`[${getLocalTimestamp()}] Schedule ${schedule.id} already running, skipping`);
579
- return { success: false, error: 'Already running' };
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
- runningSchedules.add(schedule.id);
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
- console.log(`[${getLocalTimestamp()}] Executing schedule ${schedule.id}: ${schedule.name ?? schedule.cron}`);
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
- return new Promise((resolve) => {
588
- const env = { ...process.env, ...schedule.env };
589
- delete env['CLAUDECODE'];
590
- const child = mode === 'command'
591
- ? (0, child_process_1.spawn)('sh', ['-c', schedule.command], {
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: 'inherit',
821
+ stdio: 'ignore',
594
822
  windowsHide: true,
823
+ detached: true,
595
824
  env,
596
- })
597
- : (0, child_process_1.spawn)('crewx', [modeFlag, schedule.command], {
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
- stdio: 'inherit',
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
- runningSchedules.delete(schedule.id);
879
+ releaseLease();
613
880
  resolve({ success: false, error: 'Timeout' });
614
- }, schedule.timeout_ms || 600000);
881
+ }, timeoutMs);
615
882
  child.on('close', (code) => {
616
883
  clearTimeout(timeout);
617
- runningSchedules.delete(schedule.id);
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
- runningSchedules.delete(schedule.id);
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
- await executeSchedule(schedule, dir);
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