cron-up 26.10.32 → 26.10.34

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/src/cron.mjs CHANGED
@@ -12,11 +12,13 @@ import { isDecInt } from './internals.mjs';
12
12
  const MS_DAY = 86400_000;
13
13
  const MS_MINUTE = 60_000;
14
14
 
15
- // 严格十进制整数(isDecInt 的谓词见 internals):parseInt("5x") 会静默返回
16
- // 5,必须自己卡。
15
+ // 严格十进制安全整数(isDecInt 的谓词见 internals):parseInt("5x") 会
16
+ // 静默返回 5;达到 2^53 后 v+=1 不再改变数值,区间循环会永久挂住。
17
17
  function strictInt(s) {
18
18
  if (!isDecInt(s)) throw new Error(`不是整数:${s}`);
19
- return parseInt(s, 10);
19
+ const n = parseInt(s, 10);
20
+ if (!Number.isSafeInteger(n)) throw new Error(`整数超出安全范围:${s}`);
21
+ return n;
20
22
  }
21
23
 
22
24
  // 解析一个 cron 字段为它命名的合法值域集合。
@@ -53,9 +55,16 @@ export function parseCronField(expr, lo, hi) {
53
55
  start = end = strictInt(rng);
54
56
  if (slash >= 0) end = hi; // vixie:"5/10" 表示 5,15,25,… 直到 hi
55
57
  }
56
- for (let v = start; v <= end; v += step) {
57
- if (v >= lo && v <= hi) values.add(v);
58
- }
58
+ if (start > end || end < lo || start > hi) continue;
59
+ // 只迭代合法值域内的至多 hi-lo+1 个值。起点低于 lo 时用步进算出
60
+ // 首个候选,不能直接钳到 lo,否则 0-20/3 在 lo=1 时会从 3,6,…
61
+ // 变成 1,4,…;终点直接钳到 hi,超大区间不会拖死整轮巡检。
62
+ const first = start < lo
63
+ ? start + Math.ceil((lo - start) / step) * step
64
+ : start;
65
+ if (!Number.isSafeInteger(first)) continue;
66
+ const last = Math.min(end, hi);
67
+ for (let v = first; v <= last; v += step) values.add(v);
59
68
  }
60
69
  return values;
61
70
  }
@@ -251,7 +260,7 @@ export function taskWanted(task, now, leadMs) {
251
260
  // 两个口径供展示视图复用(同一个已解析 cron 顺带回答巡检问题)。
252
261
  export { wantedNow, wantedWithin };
253
262
 
254
- // ---- Claude Code 投递抖动(预计实际触发时间)----
263
+ // ---- Claude Code 投递抖动(预计投递时间)----
255
264
  // 复刻 v2.1.285 内部的 M():任务 id 前 8 位十六进制除以 2^32,得 [0,1) 的
256
265
  // 固定哈希——同一任务每次触发的抖动完全相同,不是随机数。id 缺失或前 8
257
266
  // 位都不是十六进制时 parseInt 得 NaN,上游回退 0;任务文件由别的程序书
@@ -262,7 +271,7 @@ export function taskIdHash(taskId) {
262
271
  return Number.isFinite(n) ? n : 0;
263
272
  }
264
273
 
265
- // 任务在展示语境里的「落点 → 预计实际投递」对:一次性任务是落点加网格
274
+ // 任务在展示语境里的「设定时间 → 预计投递」对:一次性任务是落点加网格
266
275
  // 提前量;周期任务在投递尾窗内属于上一落点(即将到来的这次延迟投递,
267
276
  // 不是 nextAfter 给出的下个落点——后者严格在未来,会把展示拐到明天),
268
277
  // 否则是下一落点 + 按 id 的抖动。先于窗口检查尾窗:2 月 29 日这类长周期
@@ -317,7 +326,7 @@ function recurringDelayMs(cron, slot, taskId, withinDays) {
317
326
 
318
327
  // 周期任务的「投递尾窗」判定:上一落点已过、按 id 算的延迟投递时刻未
319
328
  // 到。返回 [落点, 投递时刻 ms];不在尾窗、找不到上一或下一落点返回
320
- // null。wanted(保活判定)与 nextDelivery(展示)共用这一口径,两边不会
329
+ // null。wanted(会话需求判定)与 nextDelivery(展示)共用这一口径,两边不会
321
330
  // 各说各话;窗口取 TAIL_WINDOW_DAYS,理由见 constants。
322
331
  function recurringTailWindow(cron, taskId, now) {
323
332
  const prev = cron.prevAtOrBefore(now, TAIL_WINDOW_DAYS);
package/src/display.mjs CHANGED
@@ -110,7 +110,7 @@ export function taskView(task, now, leadMs) {
110
110
  // wanted 恒为 null 或布尔:没传 leadMs(不问)与 cron 无效(问不了)
111
111
  // 都是 null,键的形状不随参数变化。
112
112
  wanted: leadMs !== undefined ? wantedWithin(cron, task, now, leadMs) : null,
113
- // 计入 Claude Code 投递抖动后的预计实际触发时刻;已错过的一次性任务
113
+ // 计入 Claude Code 投递抖动后的预计投递时刻;已错过的一次性任务
114
114
  // 没有未来触发,为 null;尾窗内的周期任务是即将到来的这次延迟投递。
115
115
  // 与 nxt 同为 Date 或 null。
116
116
  fire: missed ? null : fire,
@@ -135,7 +135,7 @@ export function pad2(n) {
135
135
  return String(n).padStart(2, '0');
136
136
  }
137
137
 
138
- // 「设定落点 → 预计实际触发」文案。抖动四舍五入到秒(调度器秒级轮询):
138
+ // 「设定时间 → 预计投递」文案。抖动四舍五入到秒(调度器秒级轮询):
139
139
  // 同日省略第二个日期;预计值带非零秒时显示 HH:MM:SS(如 05:15:37);落
140
140
  // 在同一分钟(无抖动)则只显示落点,不画无意义的箭头。
141
141
  export function fmtFireRange(nxt, fire) {
package/src/list.mjs CHANGED
@@ -19,7 +19,7 @@ function rank(v) {
19
19
  return v.nxt ? 1 : 2;
20
20
  }
21
21
 
22
- // 状态列文案:有效任务是「设定落点 → 预计实际触发」(无抖动时只有落点)。
22
+ // 状态列文案:有效任务是「设定时间 → 预计投递」(无抖动时只有设定时间)。
23
23
  function statusText(v) {
24
24
  if (!v.valid) return 'cron 无效';
25
25
  if (v.missed) return '已错过';
package/src/logs.mjs CHANGED
@@ -72,7 +72,8 @@ export function cmdLogs(args) {
72
72
  + `「${args.workspace}」匹配的会话日志`);
73
73
  throw new ExitError(1);
74
74
  }
75
- const tailArgs = args.follow ? ['-f'] : ['-n', '100'];
75
+ // -F 同时按名字与描述符跟随:日志 rename 后原路径重建也能接上。
76
+ const tailArgs = args.follow ? ['-F'] : ['-n', '100'];
76
77
  tailArgs.push(file);
77
78
  const r = deps.spawnSync('/usr/bin/tail', tailArgs, { stdio: 'inherit' });
78
79
  if (r.error) throw r.error;
package/src/overview.mjs CHANGED
@@ -77,7 +77,7 @@ function overviewTasks(cfg, cols, details, state) {
77
77
  bad += 1;
78
78
  continue;
79
79
  }
80
- // 「最近」按计入抖动后的预计实际触发排,而非 cron 落点。
80
+ // 「最近」按计入抖动后的预计投递时间排,而非 cron 落点。
81
81
  const fire = v.fire ?? v.nxt;
82
82
  if (fire && (!soonest || fire.getTime() < soonest[0].getTime())) {
83
83
  soonest = [fire, ws, v.summary, v.nxt];
@@ -189,18 +189,18 @@ function overviewSessions(cols, state, details, suppressCooling, discovered) {
189
189
  }
190
190
 
191
191
  const COMMANDS = [
192
- ['list', '逐个列出任务的内容摘要、设定/预计触发时间与预热状态'],
192
+ ['list', '逐个列出任务的内容摘要、设定/预计投递时间与预热状态'],
193
193
  ['logs [目录片段] [-f]', '查看巡检日志或后台会话记录'],
194
194
  ['run', '立即手动巡检一轮'],
195
- ['install --roots ~/a,~/b', '更新扫描目录并重新安装(不带参数保留现有配置)'],
195
+ ['install --roots ~/a,~/b', '更新扫描目录并重新安装(不带参数沿用有效配置)'],
196
196
  ];
197
197
 
198
198
  export async function cmdOverview(args) {
199
199
  const cols = deps.terminalWidth();
200
200
  const v = packageVersion();
201
201
  deps.print(v === null
202
- ? 'cron-up —— 为定时任务提前备好交互会话'
203
- : `cron-up ${v} —— 为定时任务提前备好交互会话(一任务一会话)`);
202
+ ? 'cron-up —— 为 Claude Code 定时任务按需预热交互会话'
203
+ : `cron-up ${v} —— 为 Claude Code 定时任务按需预热交互会话(一任务一会话)`);
204
204
 
205
205
  const cfgPath = args?.config ?? deps.paths.configPath;
206
206
  let cfg = null;
@@ -232,8 +232,8 @@ export async function cmdOverview(args) {
232
232
  } else if (cfg === null) {
233
233
  deps.print('配置:缺失,运行 cron-up install 生成');
234
234
  } else {
235
- deps.print(`巡检:每 ${INTERVAL_SECONDS} 秒一轮,提前 ${LEAD_SECONDS} `
236
- + '秒为每个窗口内任务启动专属预热会话');
235
+ deps.print(`巡检:每 ${INTERVAL_SECONDS} 秒一轮,周期任务提前 ${LEAD_SECONDS} `
236
+ + '秒进入预热窗口');
237
237
  if (info && info.interval !== null && info.interval !== undefined
238
238
  && info.interval !== INTERVAL_SECONDS) {
239
239
  alerts.push(`launchd 实际间隔 ${info.interval} 秒与当前版本的 `