cron-up 26.9.3 → 26.9.5

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/README.md CHANGED
@@ -48,7 +48,7 @@ cron-up install --roots ~/code,~/work --interval 300 --lead 600
48
48
 
49
49
  `--roots` 填写存放 agent 工作目录的父目录(逗号分隔)。不填时默认扫描家目录下两层以内(`~/` 自身、`~/*` 与 `~/*/*`),并自动跳过 `Library`、`Documents` 等系统目录和 `.git`、`node_modules`、`.cache` 等目录;建议显式指定,范围越小越安全、越快。使用默认范围安装时 install 会再打印一次安全提示:今后 clone 进家目录的任何仓库都会进入巡检,其中的周期任务会被自动续期永久化(原理见「安全说明」)。
50
50
 
51
- launchd 的环境变量极少(PATH 只有 `/usr/bin:/bin:/usr/sbin:/sbin`),npm 全局命令那个 `#!/usr/bin/env node` 壳在它下面找不到 node。因此安装时会按当前机器的 Node 安装方式(fnm 的 `aliases/default`、volta 的 `~/.volta`、nvm 的 default 别名、Homebrew/官网路径)解析出**版本无关的 node 稳定绝对路径**与包入口的绝对路径,写进一个自有的启动脚本(`cron-up-service`),LaunchAgent 只指向这个脚本。这样做的另一个原因是显示名:macOS 系统设置「允许在后台」按 launchd 拉起的第一个可执行文件给条目归组,直连 node 会显示在「Node.js Foundation」(node 的签名者)名下,指向自有脚本则显示 **cron-up-service**。node 路径在 fnm/nvm 切换默认版本后依然有效;**包入口路径在 fnm/nvm 下属于装包时的那个版本目录**,切换默认版本后巡检会继续运行旧版本目录里的旧包——总览页检测到这种分叉会提示重跑 `cron-up install`。Homebrew/官网安装的包路径本身稳定,无此问题。万一只能用上易变路径(非默认的多 shell 路径),安装时会明确提示。
51
+ launchd 的环境变量极少(PATH 只有 `/usr/bin:/bin:/usr/sbin:/sbin`),npm 全局命令那个 `#!/usr/bin/env node` 壳在它下面找不到 node。因此安装时把**跟随默认版本的 bin 目录**(fnm 是 `~/.local/share/fnm/aliases/default/bin`——`fnm default` 切换时这个 symlink 自己跟着切;volta 是 `~/.volta/bin`;Homebrew/官网是系统 bin 目录)写进一个自有的启动脚本(`cron-up-service`)的 PATH,脚本直接 `exec cron-up run`——cron-up 与 node 每轮从 PATH 解析,**npm 升级、fnm/volta 切换默认版本都自动跟随,无需重跑 install**,且 node 与包永远同版本。切换默认版本后只需在新版本里装过一次 `npm i -g cron-up`,忘了装的话总览页会提示。例外:**nvm 的 default 只是个文本别名**(要解析到具体版本目录,没有跟随切换的 symlink),其启动脚本写死绝对路径,切换默认版本后需重跑 `cron-up install`,总览会检测路径失效与版本分叉。自建启动脚本的另一个原因是显示名:macOS 系统设置「允许在后台」按 launchd 拉起的第一个可执行文件给条目归组,直连 node 会显示在「Node.js Foundation」(node 的签名者)名下,指向自有脚本则显示 **cron-up-service**。
52
52
 
53
53
  ### 自动续期(默认开启)
54
54
 
@@ -68,8 +68,8 @@ cron-up install --force --no-auto-renew
68
68
  |---|---|
69
69
  | 命令行与代码 | npm 全局目录(位置因安装方式而异,如 fnm 是 `~/.local/share/fnm/aliases/default/lib/node_modules/cron-up/`,命令在同级 `bin/cron-up`) |
70
70
  | launchd 配置 | `~/Library/LaunchAgents/local.cron-up.plist` |
71
- | 启动脚本 | `~/Library/Application Support/cron-up/cron-up-service`(launchd 拉起的入口,内含 node 与包入口的绝对路径;后台列表显示名即来自它) |
72
- | 配置文件 | `~/Library/Application Support/cron-up/config.json` |
71
+ | 启动脚本 | `~/.local/share/cron-up/cron-up-service`(launchd 拉起的入口,内含 node 与包入口的绝对路径;后台列表显示名即来自它) |
72
+ | 配置文件 | `~/.local/share/cron-up/config.json` |
73
73
  | 运行状态 | 同目录下的 `state.json` |
74
74
  | 日志 | `~/Library/Logs/cron-up/`(巡检日志;各后台会话的界面记录在 `sessions/` 子目录下,该目录权限 700、日志文件 600) |
75
75
 
@@ -127,7 +127,7 @@ cron-up spawn 的预热会话进程环境里带 `CRON_UP_SESSION=1`(值严格
127
127
  - **修改扫描范围**:直接编辑配置文件的 `roots`、`maxDepth`,最多 5 分钟后的下一轮巡检自动生效,无需重新加载服务。
128
128
  - **开关自动续期**:编辑配置文件的 `autoRenew`(`true`/`false`)同样在下一轮巡检生效;或运行 `cron-up install --force --auto-renew`(`--no-auto-renew` 关闭)。想立即处理一次而不改长期开关,用 `cron-up renew`。
129
129
  - **修改巡检间隔或提前量**:重新运行 `cron-up install --force --interval 600`,会重新生成 launchd 配置。`--force` 只更新命令行里显式给出的字段,其余配置(如 `roots`)保留;路径参数建议都加引号书写,例如 `--roots "$HOME/code,$HOME/work"`,避免 shell 对逗号后的波浪号不展开。
130
- - **升级**:`npm i -g cron-up@latest`,下一轮巡检自然跑新版。**例外**:fnm/nvm 下切换默认 Node 版本后,包入口仍指向旧版本目录(见「安装」),巡检会继续跑旧版包,总览页会提示,重跑一次 `cron-up install` 即可。从 26.9.2 及更早升级后建议也重跑一次 `cron-up install --force`:把 LaunchAgent 切到启动脚本形态(后台列表显示名由 Node.js Foundation 变为 cron-up-service),旧形态 plist 在此之前照常巡检。版本号按「年.月.序号」:两位年份、月份、月内发布序号(序号每月重置),如 2026 年 9 月第一次发布是 `26.9.1`,同月再发是 `26.9.2`,到 10 月是 `26.10.1`。发布方式:先把 package.json 的 version 改成新版本并合入 main,再打同名 `v<version>` tag(如 `v26.9.1`);GitHub Actions 会跑通测试(Node 22/24)、校验 package.json 版本与 tag 一致后 publish,版本不符直接失败,杜绝「代码是 26.9.2、tag 叫 26.9.3」这类漂移。push 到 main 的每次提交也会先跑一遍测试(ci workflow)。
130
+ - **升级**:`npm i -g cron-up@latest`,下一轮巡检自然跑新版;fnm/volta 下切换默认 Node 版本同样自动跟随(在新版本里装过一次 `npm i -g cron-up` 即可,忘了装总览会提示),全程无需重跑 install。**例外**:nvm 的启动脚本写死绝对路径,切换默认版本后重跑一次 `cron-up install`。从 26.9.4 及更早升级后建议重跑一次 `cron-up install --force` 把启动脚本切到 PATH 形态(26.9.4 及更早还会顺带把数据目录从 `~/Library/Application Support/cron-up` 迁到 `~/.local/share/cron-up`);不重跑的话旧形态照常巡检。版本号按「年.月.序号」:两位年份、月份、月内发布序号(序号每月重置),如 2026 年 9 月第一次发布是 `26.9.1`,同月再发是 `26.9.2`,到 10 月是 `26.10.1`。发布方式:先把 package.json 的 version 改成新版本并合入 main,再打同名 `v<version>` tag(如 `v26.9.1`);GitHub Actions 会跑通测试(Node 22/24)、校验 package.json 版本与 tag 一致后 publish,版本不符直接失败,杜绝「代码是 26.9.2、tag 叫 26.9.3」这类漂移。push 到 main 的每次提交也会先跑一遍测试(ci workflow)。
131
131
  - **配置校验**:手改配置时字段类型写错(`roots` 不是数组、数值字段写成字符串或布尔、`autoRenew` 不是布尔)不会静默回退默认值——巡检会以退出码 2 响亮失败并在总览页列出具体字段,修好或重跑 `cron-up install --force`(坏字段回默认,其余保留)。软问题只警告不中止:提前量小于间隔(可能来不及在任务前启动会话)、扫描目录不存在。无参数的 `cron-up` 总览随时也会显示这些问题;其中 interval 显示的是 launchd **实际生效**的间隔,手动改过 config 但与实际不一致时会提示需要重新 install。
132
132
  - **路径大小写**:macOS 默认文件系统大小写不敏感,而 `realpath` 并不规范化大小写。扫描入口会把 `roots` 认回磁盘上的真实写法,`cron-up logs` 给绝对路径时也一样——大小写敲错不再是「警告一声然后匹配失效」,而是直接按同一路径工作。
133
133
  - **提前量与间隔的关系**:请保持 `leadSeconds ≥ intervalSeconds`,否则极端情况下任务会在两轮巡检之间到期而来不及启动。默认 600 ≥ 300 有一倍余量。`--lead 0` 是合法取值(表示不提前,只靠错过补执行兜底),会如实写入配置;`--interval 0` 非法,安装时直接报错中止。
@@ -163,9 +163,10 @@ cron-up spawn 的预热会话进程环境里带 `CRON_UP_SESSION=1`(值严格
163
163
  | 现象 | 排查方法 |
164
164
  |---|---|
165
165
  | 总览提示「冷却中」 | 运行 `cron-up logs <目录名>` 查看启动界面:要么停在信任确认或工具授权提问(日志里能看到提问界面),要么 shell 环境缺少命令(日志里是 `command not found`) |
166
- | 总览提示「Node 或入口路径已失效」 | Node 版本目录被删或移动后启动脚本里的绝对路径失效,重跑 `cron-up install` 重新解析即可 |
167
- | 总览提示「启动脚本已失效 / 内容无法解读」 | 启动脚本(`~/Library/Application Support/cron-up/cron-up-service`)被删或手改,重跑 `cron-up install` 覆盖重生成 |
168
- | 总览提示「入口路径属于另一个 node 版本目录」 | fnm/nvm 切换了默认 Node 版本,巡检在跑旧版本目录里的旧包;重跑 `cron-up install` 更新路径 |
166
+ | 总览提示「Node 或入口路径已失效」 | 写死路径形态(nvm 或旧版脚本)下 Node 版本目录被删或移动,重跑 `cron-up install` 重新解析即可 |
167
+ | 总览提示「启动脚本已失效 / 内容无法解读」 | 启动脚本(`~/.local/share/cron-up/cron-up-service`)被删或手改,重跑 `cron-up install` 覆盖重生成 |
168
+ | 总览提示「PATH 目录里找不到 cron-up」 | fnm/volta 切换了默认 Node 版本,新版本里还没装包:在新版本里 `npm i -g cron-up` 即可 |
169
+ | 总览提示「入口路径属于另一个 node 版本目录」 | 写死路径形态(nvm 或旧版脚本)下切换了默认 Node 版本,巡检在跑旧版本目录里的旧包;重跑 `cron-up install` 更新路径 |
169
170
  | 日志里出现「卡死」 | 会话进程起来了但没能变成可用会话,绝大多数是停在信任确认或授权提问。先手动 `cd <目录> && claude` 走完确认,再等下一轮巡检 |
170
171
  | 日志里出现「任务文件读不出 \<目录\>」 | 该目录的 `.claude/scheduled_tasks.json` 存在却解析不了(格式损坏),巡检跳过这一个目录,**其中的定时任务一个都不会执行**;其余目录照常。检查该文件 |
171
172
  | 日志里出现「错误 \<目录\>」 | 巡检该目录时抛出未预期的异常,已按目录隔离、不影响其余目录。这通常意味着 cron-up 自身的 bug,请带上该行反馈 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cron-up",
3
- "version": "26.9.3",
3
+ "version": "26.9.5",
4
4
  "description": "为命令行 AI agent 的 cron 定时任务提前预热并保活交互式会话",
5
5
  "type": "module",
6
6
  "bin": {
package/src/paths.mjs CHANGED
@@ -16,7 +16,12 @@ export const TASK_REL = path.join('.claude', 'scheduled_tasks.json');
16
16
  // HOME。getter 让 sessionLogDir/configPath/statePath 跟随替换后的前缀。
17
17
  export const paths = {
18
18
  home: HOME,
19
- appSupport: path.join(HOME, 'Library', 'Application Support', 'cron-up'),
19
+ // 数据目录用 XDG 风格的 ~/.local/share/cron-up:config、state 与启动脚本
20
+ // 集中一处,purge 一删全清。26.9.3 及更早放在
21
+ // ~/Library/Application Support/cron-up,install 时自动搬家(见
22
+ // service.mjs 的 migrateLegacyDir)。
23
+ dataDir: path.join(HOME, '.local', 'share', 'cron-up'),
24
+ legacyAppSupport: path.join(HOME, 'Library', 'Application Support', 'cron-up'),
20
25
  logDir: path.join(HOME, 'Library', 'Logs', 'cron-up'),
21
26
  plistPath: path.join(HOME, 'Library', 'LaunchAgents', `${LABEL}.plist`),
22
27
  sessionDir: path.join(HOME, '.claude', 'sessions'),
@@ -25,16 +30,16 @@ export const paths = {
25
30
  return path.join(this.logDir, 'sessions');
26
31
  },
27
32
  get configPath() {
28
- return path.join(this.appSupport, 'config.json');
33
+ return path.join(this.dataDir, 'config.json');
29
34
  },
30
35
  get statePath() {
31
- return path.join(this.appSupport, 'state.json');
36
+ return path.join(this.dataDir, 'state.json');
32
37
  },
33
38
  // launchd 拉起的启动脚本。ProgramArguments[0] 直接指向它而非 node:
34
39
  // macOS 后台项目列表按可执行文件名显示条目,直连 node 会归到「Node.js
35
40
  // Foundation」名下(node 的签名者),自有脚本则显示脚本自己的名字。
36
41
  get serviceScriptPath() {
37
- return path.join(this.appSupport, 'cron-up-service');
42
+ return path.join(this.dataDir, 'cron-up-service');
38
43
  },
39
44
  };
40
45
 
package/src/patrol.mjs CHANGED
@@ -58,8 +58,8 @@ function readLockPid(lockPath) {
58
58
  }
59
59
 
60
60
  export function acquireRunLock() {
61
- fs.mkdirSync(deps.paths.appSupport, { recursive: true });
62
- const lockPath = path.join(deps.paths.appSupport, 'run.lock');
61
+ fs.mkdirSync(deps.paths.dataDir, { recursive: true });
62
+ const lockPath = path.join(deps.paths.dataDir, 'run.lock');
63
63
  for (let attempt = 0; attempt < 3; attempt++) {
64
64
  const fd = tryCreateLock(lockPath);
65
65
  if (fd !== null) {
package/src/service.mjs CHANGED
@@ -135,7 +135,12 @@ function nvmNodePath(nvmDir) {
135
135
  }
136
136
 
137
137
  // 返回 {nodePath, entryPath, manager}。注入 home/execPath/entryPath 仅为
138
- // 测试在临时 HOME 里伪造各安装形态。
138
+ // 测试在临时 HOME 里伪造各安装形态。pathDirs 是该安装形态下「跟随默认版
139
+ // 本的 bin 目录」(fnm 的 aliases/default/bin——fnm default 切换时 symlink
140
+ // 跟着切;volta 的 ~/.volta/bin;brew/pkg 的系统 bin):启动脚本把它放进
141
+ // PATH 后 cron-up 与 node 都从 PATH 解析,npm 升级与切换默认版本自动跟
142
+ // 随。nvm 与 exec-path 兜底没有这种目录(nvm 的 default 是文本别名,要解
143
+ // 析到具体版本),pathDirs 为 null,启动脚本退回写死绝对路径。
139
144
  export function resolveLauncher(opts = {}) {
140
145
  const home = opts.home ?? paths.home;
141
146
  const execPath = opts.execPath ?? process.execPath;
@@ -152,13 +157,21 @@ export function resolveLauncher(opts = {}) {
152
157
  ].filter(Boolean);
153
158
  for (const d of fnmDirs) {
154
159
  const nodePath = path.join(d, 'aliases', 'default', 'bin', 'node');
155
- if (isExecutable(nodePath)) return { nodePath, entryPath, manager: 'fnm' };
160
+ if (isExecutable(nodePath)) {
161
+ return {
162
+ nodePath, entryPath, manager: 'fnm',
163
+ pathDirs: [path.dirname(nodePath)],
164
+ };
165
+ }
156
166
  }
157
167
 
158
168
  // 2. volta:~/.volta/bin/node 是稳定的版本无关 shim。
159
169
  const voltaNode = path.join(home, '.volta', 'bin', 'node');
160
170
  if (isExecutable(voltaNode)) {
161
- return { nodePath: voltaNode, entryPath, manager: 'volta' };
171
+ return {
172
+ nodePath: voltaNode, entryPath, manager: 'volta',
173
+ pathDirs: [path.dirname(voltaNode)],
174
+ };
162
175
  }
163
176
 
164
177
  // 3. nvm。
@@ -175,6 +188,7 @@ export function resolveLauncher(opts = {}) {
175
188
  nodePath: cand,
176
189
  entryPath,
177
190
  manager: cand.includes('homebrew') || isBrewCellar(cand) ? 'brew' : 'pkg',
191
+ pathDirs: [path.dirname(cand)],
178
192
  };
179
193
  }
180
194
  }
@@ -219,15 +233,25 @@ function xmlUnescape(s) {
219
233
  // macOS 的后台项目列表(系统设置 → 登录项与扩展 → 允许在后台)按
220
234
  // ProgramArguments[0] 的文件名归组条目:直连 node 会与所有 node 系后台服务
221
235
  // 合并显示在「Node.js Foundation」(node 的签名者)名下;指向自有脚本则
222
- // 显示 cron-up-service。路径变化(换 Node 安装方式、升级)后重跑
223
- // cron-up install 覆盖重生成。
236
+ // 显示 cron-up-service。
237
+ // 有跟随默认版本的 bin 目录的安装形态(fnm/volta/brew/pkg)把目录放进
238
+ // PATH 后直接 exec cron-up run:npm 升级、切换默认版本都自动跟随,无需
239
+ // 重跑 install(node 与 cron-up 同目录,天然同版本)。nvm 与 exec-path
240
+ // 兜底没有这种目录,写死绝对路径,失效与版本分叉由总览检测兜底。
224
241
  export function writeServiceScript(launcher) {
225
242
  const script = deps.paths.serviceScriptPath;
226
243
  fs.mkdirSync(path.dirname(script), { recursive: true });
244
+ let body;
245
+ if (launcher.pathDirs) {
246
+ const dirs = launcher.pathDirs.map((d) => `"${d}"`).join(':');
247
+ body = `export PATH=${dirs}:"$PATH"\nexec cron-up run\n`;
248
+ } else {
249
+ body = `exec "${launcher.nodePath}" "${launcher.entryPath}" run\n`;
250
+ }
227
251
  fs.writeFileSync(script,
228
252
  '#!/bin/bash\n'
229
253
  + '# 由 cron-up install 生成;重跑 cron-up install --force 覆盖重生成\n'
230
- + `exec "${launcher.nodePath}" "${launcher.entryPath}" run\n`);
254
+ + body);
231
255
  fs.chmodSync(script, 0o755);
232
256
  return script;
233
257
  }
@@ -277,18 +301,45 @@ export function readPlistProgramArgs() {
277
301
  }
278
302
 
279
303
  // 从启动脚本内容里解出 [node 路径, 入口路径];脚本是我们生成的固定格
280
- // 式,解不出(手改坏)返回 null。
281
- function readServiceScriptPaths(scriptPath) {
304
+ // 式,解不出(手改坏)返回 null。PATH 形态的脚本没有写死的路径,返回
305
+ // 'path' 标记。
306
+ function readServiceScriptShape(scriptPath) {
282
307
  let text;
283
308
  try {
284
309
  text = fs.readFileSync(scriptPath, 'utf-8');
285
310
  } catch {
286
311
  return null;
287
312
  }
313
+ if (/^export PATH=.+"\$PATH"$/m.test(text)) return 'path';
288
314
  const m = text.match(/^exec "(.+)" "(.+)" run$/m);
289
315
  return m ? [m[1], m[2]] : null;
290
316
  }
291
317
 
318
+ // PATH 形态健康检查:跟随默认版本的 bin 目录里要能找到 cron-up 与
319
+ // node。典型断点是 fnm/volta 切换默认版本后忘了在新版本里装包。
320
+ function pathResolveHealth(scriptPath) {
321
+ let text;
322
+ try {
323
+ text = fs.readFileSync(scriptPath, 'utf-8');
324
+ } catch {
325
+ return null;
326
+ }
327
+ const m = text.match(/^export PATH=(.+):"\$PATH"$/m);
328
+ if (!m) return null;
329
+ const dirs = m[1].split('":"').map((s) => s.replace(/"/g, ''));
330
+ const has = (name) => dirs.some((d) => isExecutable(path.join(d, name)));
331
+ if (!has('cron-up')) {
332
+ return `启动脚本的 PATH 目录(${dirs.join('、')})里找不到 cron-up:切换`
333
+ + '默认 Node 版本后要在新版本里重新 npm i -g cron-up,或重跑 '
334
+ + 'cron-up install';
335
+ }
336
+ if (!has('node')) {
337
+ return `启动脚本的 PATH 目录(${dirs.join('、')})里找不到 node,重跑 `
338
+ + 'cron-up install 重新解析';
339
+ }
340
+ return null;
341
+ }
342
+
292
343
  // fnm 与 nvm 的全局包装在版本化目录里(…/node-versions/v<X>/… 与
293
344
  // …/versions/node/v<X>/…)。default 别名切换后 node 路径依然有效,但入口
294
345
  // 路径仍指向装包时的旧版本目录——巡检会静默继续跑旧版 cron-up,直到旧版
@@ -326,9 +377,9 @@ function pathHealthAlert(nodePath, entryPath) {
326
377
  return null;
327
378
  }
328
379
 
329
- // 总览页健康检查:plist 指定的启动链若已断(脚本或其中的 node/入口路径
330
- // 不存在,如 fnm default 被删、nvm 版本目录被清),launchd 每轮都会静默
331
- // 拉不起来;路径都在但分属不同 node 版本目录时,巡检跑的多半是旧版包。
380
+ // 总览页健康检查:plist 指定的启动链若已断(脚本丢失、PATH 里解析不到
381
+ // cron-up/node、写死路径失效如 nvm 版本目录被清),launchd 每轮都会静默
382
+ // 拉不起来;写死路径的形态还检查 fnm/nvm 版本分叉。
332
383
  export function launcherHealth() {
333
384
  if (!fs.existsSync(deps.paths.plistPath)) return null;
334
385
  const args = readPlistProgramArgs();
@@ -339,12 +390,11 @@ export function launcherHealth() {
339
390
  return `launchd 配置中的启动脚本已失效(${path.basename(scriptPath)} `
340
391
  + '不存在),巡检当前拉不起来;请重新运行 cron-up install';
341
392
  }
342
- const paths = readServiceScriptPaths(scriptPath);
343
- if (paths === null) {
344
- return `启动脚本 ${scriptPath} 内容无法解读(可能被手改),`
345
- + '重跑 cron-up install 覆盖重生成';
346
- }
347
- return pathHealthAlert(...paths);
393
+ const shape = readServiceScriptShape(scriptPath);
394
+ if (shape === 'path') return pathResolveHealth(scriptPath);
395
+ if (Array.isArray(shape)) return pathHealthAlert(...shape);
396
+ return `启动脚本 ${scriptPath} 内容无法解读(可能被手改),`
397
+ + '重跑 cron-up install 覆盖重生成';
348
398
  }
349
399
  if (args.length === 3) {
350
400
  // 旧版 plist(ProgramArguments 直写 node 与入口):升级后重跑
@@ -356,8 +406,34 @@ export function launcherHealth() {
356
406
 
357
407
  // ---- install / uninstall ----
358
408
 
409
+ // 26.9.3 及更早把数据放在 ~/Library/Application Support/cron-up;新版统一
410
+ // 到 ~/.local/share/cron-up。整体搬家(config、state 随行),随后 install
411
+ // 照常重写脚本与 plist 并重启服务,用户无感。搬家只在 install 里发生:
412
+ // 只升级 npm 包而不跑 install 的话,旧目录与旧 plist 依旧配套、照常巡检。
413
+ function migrateLegacyDir() {
414
+ const legacy = deps.paths.legacyAppSupport;
415
+ if (!fs.existsSync(legacy) || fs.existsSync(deps.paths.dataDir)) return;
416
+ try {
417
+ fs.mkdirSync(path.dirname(deps.paths.dataDir), { recursive: true });
418
+ try {
419
+ fs.renameSync(legacy, deps.paths.dataDir);
420
+ } catch {
421
+ // rename 搬不动(目标异常等):退化为复制,旧目录留着无害。
422
+ fs.cpSync(legacy, deps.paths.dataDir, { recursive: true });
423
+ }
424
+ deps.print(`已迁移数据目录:${legacy} → ${deps.paths.dataDir}`);
425
+ } catch {
426
+ // 迁移彻底失败必须当面说:不说的话 install 会按「全新安装」落默认
427
+ // 配置,用户收窄过的 roots 等手改就静默丢了。
428
+ deps.print(`提示:旧数据目录迁移失败(${legacy}),本次按新目录继续;`
429
+ + '如旧目录里还有你改过的配置,请手动复制到 '
430
+ + `${deps.paths.dataDir} 后重跑 cron-up install`);
431
+ }
432
+ }
433
+
359
434
  export async function cmdInstall(args) {
360
- fs.mkdirSync(deps.paths.appSupport, { recursive: true });
435
+ migrateLegacyDir();
436
+ fs.mkdirSync(deps.paths.dataDir, { recursive: true });
361
437
  fs.mkdirSync(deps.paths.sessionLogDir, { recursive: true });
362
438
  try {
363
439
  fs.chmodSync(deps.paths.sessionLogDir, 0o700);
@@ -497,7 +573,8 @@ export async function cmdUninstall(args) {
497
573
  }
498
574
 
499
575
  if (args.purge) {
500
- for (const p of [deps.paths.appSupport, deps.paths.logDir]) {
576
+ for (const p of [deps.paths.dataDir, deps.paths.legacyAppSupport,
577
+ deps.paths.logDir]) {
501
578
  fs.rmSync(p, { recursive: true, force: true });
502
579
  deps.print(`已删除 ${p}`);
503
580
  }