omnicrawl-cli 0.2.3 → 0.2.6

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.
Files changed (2) hide show
  1. package/bin/update.mjs +288 -290
  2. package/package.json +6 -6
package/bin/update.mjs CHANGED
@@ -1,290 +1,288 @@
1
- // 启动自动更新:检测 npm registry 上的 `omnicrawl-cli` 最新版,落后时全局升级并重启。
2
- //
3
- // 语义与 Python `omnicrawl/maintenance/{version_check,updater}.py` 一路对齐,差别只在版本源与
4
- // 升级方式:版本源从 PyPI RSS 换成 npm registry 的 `latest`,升级方式从
5
- // `pip install --upgrade` 换成 `npm install -g`。
6
- //
7
- // 流程(仅默认工作台路径,非逃生口):
8
- //
9
- // 1. 读取 `[update].enabled`(缺省启用),并检查环境变量跳过开关;
10
- // 2. 读 24h 本地缓存,过期或缺失才请求 registry(3s 超时);
11
- // 3. 本地版本落后时打印升级说明,执行 `npm install -g omnicrawl-cli@<latest>`;
12
- // 4. 升级成功后重新拉起启动器(`node <launcher> <原参数>`),把子进程退出码作为本次退出码;
13
- // 5. 任何失败(离线、npm 不可用、安装报错)都只打印原因与手动升级命令,继续用当前版本启动,
14
- // 绝不让更新流程阻塞工具可用性。
15
- //
16
- // 安全护栏:
17
- // - 源码检出 / 开发环境(祖先目录里出现 `.git`)直接跳过;
18
- // - `OMNICRAWL_SKIP_AUTO_UPDATE=1` 可彻底关闭;
19
- // - 逃生口 `OMNICRAWL_HOST` / `OMNICRAWL_BINARY`(协议测试与开发)不走更新检查;
20
- // - 执行过一次升级尝试后写入 `OMNICRAWL_AUTO_UPDATE_ATTEMPTED=1`,重启后的子进程继承该标记
21
- // 不再重复尝试,避免升级失败/版本探测异常时无限重启循环。
22
- import { spawn } from 'node:child_process'
23
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
24
- import { homedir } from 'node:os'
25
- import { dirname, join, resolve } from 'node:path'
26
- import process from 'node:process'
27
-
28
- export const PACKAGE_NAME = 'omnicrawl-cli'
29
- /** 彻底关闭自动更新(用户 / CI / 故障排查用)。 */
30
- export const SKIP_AUTO_UPDATE_ENV = 'OMNICRAWL_SKIP_AUTO_UPDATE'
31
- /** 标记「本次升级已经尝试过」,重启后的子进程据此不再重复尝试。 */
32
- export const AUTO_UPDATE_ATTEMPTED_ENV = 'OMNICRAWL_AUTO_UPDATE_ATTEMPTED'
33
- export const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
34
- export const VERSION_CHECK_TIMEOUT_MS = 3000
35
- export const VERSION_CHECK_CACHE_TTL_SECONDS = 24 * 60 * 60
36
- export const VERSION_CHECK_CACHE_FILENAME = 'npm-version-check.json'
37
- export const USER_CONFIG_DIRNAME = '.OmniCrawl'
38
- export const CONFIG_FILENAME = 'config.toml'
39
-
40
- /** 已识别的子命令:这些路径不做启动更新(工作台才是自动更新的入口)。 */
41
- const DISPATCH_COMMANDS = new Set(['api', 'kernel', 'plugin'])
42
- /** `npm install -g` 的附加参数(减少噪音与交互)。 */
43
- const NPM_QUIET_ARGS = ['--no-fund', '--no-audit']
44
-
45
- const VERSION_PATTERN =
46
- /^v?(?<release>\d+(?:\.\d+)*)(?:-(?<pre>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
47
-
48
- /** 解析常见 semver(`1.2.3`、`1.2.3-rc.1`、`v1.2.3+build`);非法值返回 null。 */
49
- export function parseVersion(value) {
50
- const text = String(value ?? '').trim()
51
- const match = VERSION_PATTERN.exec(text)
52
- if (!match) return null
53
- const release = match.groups.release.split('.').map((part) => Number.parseInt(part, 10))
54
- if (release.some((part) => !Number.isFinite(part))) return null
55
- const pre = match.groups.pre
56
- return { release, pre: pre ? pre.split('.') : null }
57
- }
58
-
59
- /** semver 预发布标识比较:数字段按数值,字母段按字典序,数字段低于字母段。 */
60
- function comparePre(a, b) {
61
- if (a === null && b === null) return 0
62
- if (a === null) return 1 // 无预发布高于有预发布
63
- if (b === null) return -1
64
- const width = Math.max(a.length, b.length)
65
- for (let index = 0; index < width; index += 1) {
66
- const left = a[index]
67
- const right = b[index]
68
- if (left === undefined) return -1
69
- if (right === undefined) return 1
70
- const leftNumber = /^\d+$/.test(left) ? Number.parseInt(left, 10) : null
71
- const rightNumber = /^\d+$/.test(right) ? Number.parseInt(right, 10) : null
72
- if (leftNumber !== null && rightNumber !== null) {
73
- if (leftNumber !== rightNumber) return leftNumber < rightNumber ? -1 : 1
74
- } else if (leftNumber !== null) {
75
- return -1
76
- } else if (rightNumber !== null) {
77
- return 1
78
- } else if (left !== right) {
79
- return left < right ? -1 : 1
80
- }
81
- }
82
- return 0
83
- }
84
-
85
- /** 比较版本;任一侧非法(异常值)都按「不可升级」处理(Python `is_newer_version` 同口径)。 */
86
- export function isNewerVersion(candidate, current) {
87
- const candidateKey = parseVersion(candidate)
88
- const currentKey = parseVersion(current)
89
- if (!candidateKey || !currentKey) return false
90
-
91
- const width = Math.max(candidateKey.release.length, currentKey.release.length)
92
- for (let index = 0; index < width; index += 1) {
93
- const left = candidateKey.release[index] ?? 0
94
- const right = currentKey.release[index] ?? 0
95
- if (left !== right) return left > right
96
- }
97
- return comparePre(candidateKey.pre, currentKey.pre) > 0
98
- }
99
-
100
- /** 从 registry 的 `latest` 响应体解析版本号;解析失败抛错(调用方降级)。 */
101
- export function parseRegistryVersion(payload) {
102
- const body = typeof payload === 'string' ? JSON.parse(payload) : payload
103
- const version = String(body?.version ?? '').trim()
104
- if (!version || !parseVersion(version)) {
105
- throw new Error('npm registry 的 latest 响应里没有有效版本。')
106
- }
107
- return version
108
- }
109
-
110
- /** 默认取版本方式:直连 npm registry,带 3s 超时,失败抛错。 */
111
- export async function fetchRegistryLatest(url = REGISTRY_URL) {
112
- const response = await fetch(url, {
113
- headers: { Accept: 'application/json', 'User-Agent': `${PACKAGE_NAME} version-check` },
114
- signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS),
115
- })
116
- if (!response.ok) throw new Error(`npm registry 返回 ${response.status}`)
117
- return parseRegistryVersion(await response.json())
118
- }
119
-
120
- function cachePath(home) {
121
- return join(home, USER_CONFIG_DIRNAME, VERSION_CHECK_CACHE_FILENAME)
122
- }
123
-
124
- function readFreshCache(home, now) {
125
- try {
126
- const data = JSON.parse(readFileSync(cachePath(home), 'utf8'))
127
- const checkedAt = Number(data.checked_at)
128
- const latest = String(data.latest_version ?? '').trim()
129
- const age = Math.max(0, now / 1000 - checkedAt)
130
- if (age < VERSION_CHECK_CACHE_TTL_SECONDS && parseVersion(latest)) return latest
131
- } catch {
132
- return null
133
- }
134
- return null
135
- }
136
-
137
- function writeCache(home, now, latestVersion) {
138
- const target = cachePath(home)
139
- const temp = `${target}.${process.pid}.tmp`
140
- try {
141
- mkdirSync(dirname(target), { recursive: true })
142
- writeFileSync(
143
- temp,
144
- JSON.stringify({ checked_at: now / 1000, latest_version: latestVersion }),
145
- 'utf8',
146
- )
147
- renameSync(temp, target)
148
- } catch {
149
- // 缓存失败不应抹掉已经取得的在线检查结果。
150
- } finally {
151
- rmSync(temp, { force: true })
152
- }
153
- }
154
-
155
- /** 读取 `[update].enabled`;缺省开启,配置异常也按开启处理(保底默认)。 */
156
- export function loadUpdateEnabled(home = homedir()) {
157
- try {
158
- const text = readFileSync(join(home, USER_CONFIG_DIRNAME, CONFIG_FILENAME), 'utf8')
159
- const section = text.match(/(?:^|\n)\[update\]\s*\n(?<body>[\s\S]*?)(?:\n\[|$)/)
160
- if (!section) return true
161
- const match = section.groups.body.match(/^\s*enabled\s*=\s*(true|false)\s*$/m)
162
- if (!match) return true
163
- return match[1] === 'true'
164
- } catch {
165
- return true // 更新功能不能因配置问题影响启动
166
- }
167
- }
168
-
169
- /**
170
- * 判断是否从源码检出 / 开发环境运行(此类环境不自动更新)。
171
- *
172
- * npm 全局安装的启动器会落在 `node_modules/@omnicrawl/cli/bin` 这类目录里,祖先里没有 `.git`;
173
- * 仓库内开发时祖先里有 `.git`。与 Python `is_source_checkout`(`pyproject.toml + .git`)同义。
174
- */
175
- export function isSourceCheckout(launcherPath, maxDepth = 8) {
176
- let current = dirname(resolve(launcherPath))
177
- for (let depth = 0; depth < maxDepth; depth += 1) {
178
- if (existsSync(join(current, '.git'))) return true
179
- const parent = dirname(current)
180
- if (parent === current) break
181
- current = parent
182
- }
183
- return false
184
- }
185
-
186
- /** 是否需要为这组参数做启动更新:只认工作台路径,已知子命令与逃生口都跳过。 */
187
- export function shouldCheckUpdate(args, env = process.env) {
188
- if (env.OMNICRAWL_HOST || env.OMNICRAWL_BINARY) return false
189
- const first = args[0]
190
- if (first !== undefined && DISPATCH_COMMANDS.has(first)) return false
191
- return true
192
- }
193
-
194
- function buildInstallCommand(latestVersion) {
195
- const command = process.env.npm_execpath
196
- return command
197
- ? [process.execPath, command, 'install', '-g', ...NPM_QUIET_ARGS, `${PACKAGE_NAME}@${latestVersion}`]
198
- : ['npm', 'install', '-g', ...NPM_QUIET_ARGS, `${PACKAGE_NAME}@${latestVersion}`]
199
- }
200
-
201
- function runDefault(command) {
202
- const child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: false })
203
- return new Promise((resolveExit) => {
204
- child.on('error', () => resolveExit(1))
205
- child.on('exit', (code) => resolveExit(code ?? 1))
206
- })
207
- }
208
-
209
- function relaunchDefault(launcherPath, args) {
210
- const child = spawn(process.execPath, [launcherPath, ...args], { stdio: 'inherit' })
211
- return new Promise((resolveExit) => {
212
- child.on('error', () => resolveExit(null))
213
- child.on('exit', (code) => resolveExit(code ?? 0))
214
- })
215
- }
216
-
217
- function manualInstallHint(latestVersion) {
218
- return `可手动执行升级:npm install -g ${PACKAGE_NAME}@${latestVersion}`
219
- }
220
-
221
- /**
222
- * 启动阶段自动更新入口。
223
- *
224
- * @returns `null`:无需更新 / 已跳过 / 更新失败按用户策略继续用当前版本启动;
225
- * 其他整数:已升级并重启,返回新进程的退出码,调用方应直接退出。
226
- */
227
- export async function runStartupUpdateIfDue({
228
- argv = [],
229
- currentVersion,
230
- launcherPath = new URL(import.meta.url).pathname,
231
- env = process.env,
232
- home = homedir(),
233
- print = (line) => console.error(line),
234
- now = Date.now,
235
- latestChecker = null,
236
- installer = null,
237
- relauncher = null,
238
- sourceCheckout = null,
239
- configEnabled = null,
240
- } = {}) {
241
- if (env[SKIP_AUTO_UPDATE_ENV] === '1') return null
242
- if (env[AUTO_UPDATE_ATTEMPTED_ENV] === '1') return null
243
- if (env.OMNICRAWL_HOST || env.OMNICRAWL_BINARY) return null
244
- if ((sourceCheckout ?? isSourceCheckout)(launcherPath)) return null
245
- if (!(configEnabled ?? loadUpdateEnabled)(home)) return null
246
-
247
- const installed = String(currentVersion ?? '').trim()
248
- let latest = null
249
- try {
250
- latest = readFreshCache(home, now())
251
- if (latest === null) {
252
- latest = latestChecker
253
- ? await latestChecker()
254
- : await fetchRegistryLatest()
255
- if (latest) writeCache(home, now(), latest)
256
- }
257
- } catch {
258
- // 版本检查不能改变应用可用性;离线、代理和缓存损坏均只隐藏更新提示。
259
- return null
260
- }
261
- if (!latest || !isNewerVersion(latest, installed)) return null
262
-
263
- // 先标记尝试,避免子进程(继承环境变量)在升级后因探测不一致再次升级。
264
- env[AUTO_UPDATE_ATTEMPTED_ENV] = '1'
265
- print(`发现新版本 ${latest}(当前 ${installed}),正在自动升级…`)
266
-
267
- let installedOk = false
268
- try {
269
- installedOk = (await (installer
270
- ? installer(latest)
271
- : runDefault(buildInstallCommand(latest)))) === 0
272
- } catch {
273
- installedOk = false
274
- }
275
- if (!installedOk) {
276
- print(`自动升级失败,本次继续使用当前版本 ${installed} 启动。`)
277
- print(manualInstallHint(latest))
278
- return null
279
- }
280
-
281
- print(`升级完成(${installed} → ${latest}),正在重新启动工作台…`)
282
- const code = await (relauncher
283
- ? relauncher(launcherPath, argv)
284
- : relaunchDefault(launcherPath, argv))
285
- if (code === null) {
286
- print('重新启动工作台失败,请手动执行 omnicrawl 重新进入。')
287
- return 0
288
- }
289
- return code
290
- }
1
+ // 启动自动更新:检测 npm registry 上的 `omnicrawl-cli` 最新版,落后时全局升级并重启。
2
+ //
3
+ // 版本源是 npm registry 的 `latest`,升级方式是 `npm install -g`。
4
+ //
5
+ // 流程(仅默认工作台路径,非逃生口):
6
+ //
7
+ // 1. 读取 `[update].enabled`(缺省启用),并检查环境变量跳过开关;
8
+ // 2. 读 24h 本地缓存,过期或缺失才请求 registry(3s 超时);
9
+ // 3. 本地版本落后时打印升级说明,执行 `npm install -g omnicrawl-cli@<latest>`;
10
+ // 4. 升级成功后重新拉起启动器(`node <launcher> <原参数>`),把子进程退出码作为本次退出码;
11
+ // 5. 任何失败(离线、npm 不可用、安装报错)都只打印原因与手动升级命令,继续用当前版本启动,
12
+ // 绝不让更新流程阻塞工具可用性。
13
+ //
14
+ // 安全护栏:
15
+ // - 源码检出 / 开发环境(祖先目录里出现 `.git`)直接跳过;
16
+ // - `OMNICRAWL_SKIP_AUTO_UPDATE=1` 可彻底关闭;
17
+ // - 逃生口 `OMNICRAWL_HOST` / `OMNICRAWL_BINARY`(协议测试与开发)不走更新检查;
18
+ // - 执行过一次升级尝试后写入 `OMNICRAWL_AUTO_UPDATE_ATTEMPTED=1`,重启后的子进程继承该标记
19
+ // 不再重复尝试,避免升级失败/版本探测异常时无限重启循环。
20
+ import { spawn } from 'node:child_process'
21
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
22
+ import { homedir } from 'node:os'
23
+ import { dirname, join, resolve } from 'node:path'
24
+ import process from 'node:process'
25
+
26
+ export const PACKAGE_NAME = 'omnicrawl-cli'
27
+ /** 彻底关闭自动更新(用户 / CI / 故障排查用)。 */
28
+ export const SKIP_AUTO_UPDATE_ENV = 'OMNICRAWL_SKIP_AUTO_UPDATE'
29
+ /** 标记「本次升级已经尝试过」,重启后的子进程据此不再重复尝试。 */
30
+ export const AUTO_UPDATE_ATTEMPTED_ENV = 'OMNICRAWL_AUTO_UPDATE_ATTEMPTED'
31
+ export const REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`
32
+ export const VERSION_CHECK_TIMEOUT_MS = 3000
33
+ export const VERSION_CHECK_CACHE_TTL_SECONDS = 24 * 60 * 60
34
+ export const VERSION_CHECK_CACHE_FILENAME = 'npm-version-check.json'
35
+ export const USER_CONFIG_DIRNAME = '.OmniCrawl'
36
+ export const CONFIG_FILENAME = 'config.toml'
37
+
38
+ /** 已识别的子命令:这些路径不做启动更新(工作台才是自动更新的入口)。 */
39
+ const DISPATCH_COMMANDS = new Set(['api', 'kernel', 'plugin'])
40
+ /** `npm install -g` 的附加参数(减少噪音与交互)。 */
41
+ const NPM_QUIET_ARGS = ['--no-fund', '--no-audit']
42
+
43
+ const VERSION_PATTERN =
44
+ /^v?(?<release>\d+(?:\.\d+)*)(?:-(?<pre>[0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
45
+
46
+ /** 解析常见 semver(`1.2.3`、`1.2.3-rc.1`、`v1.2.3+build`);非法值返回 null。 */
47
+ export function parseVersion(value) {
48
+ const text = String(value ?? '').trim()
49
+ const match = VERSION_PATTERN.exec(text)
50
+ if (!match) return null
51
+ const release = match.groups.release.split('.').map((part) => Number.parseInt(part, 10))
52
+ if (release.some((part) => !Number.isFinite(part))) return null
53
+ const pre = match.groups.pre
54
+ return { release, pre: pre ? pre.split('.') : null }
55
+ }
56
+
57
+ /** semver 预发布标识比较:数字段按数值,字母段按字典序,数字段低于字母段。 */
58
+ function comparePre(a, b) {
59
+ if (a === null && b === null) return 0
60
+ if (a === null) return 1 // 无预发布高于有预发布
61
+ if (b === null) return -1
62
+ const width = Math.max(a.length, b.length)
63
+ for (let index = 0; index < width; index += 1) {
64
+ const left = a[index]
65
+ const right = b[index]
66
+ if (left === undefined) return -1
67
+ if (right === undefined) return 1
68
+ const leftNumber = /^\d+$/.test(left) ? Number.parseInt(left, 10) : null
69
+ const rightNumber = /^\d+$/.test(right) ? Number.parseInt(right, 10) : null
70
+ if (leftNumber !== null && rightNumber !== null) {
71
+ if (leftNumber !== rightNumber) return leftNumber < rightNumber ? -1 : 1
72
+ } else if (leftNumber !== null) {
73
+ return -1
74
+ } else if (rightNumber !== null) {
75
+ return 1
76
+ } else if (left !== right) {
77
+ return left < right ? -1 : 1
78
+ }
79
+ }
80
+ return 0
81
+ }
82
+
83
+ /** 比较版本;任一侧非法(异常值)都按「不可升级」处理(Python `is_newer_version` 同口径)。 */
84
+ export function isNewerVersion(candidate, current) {
85
+ const candidateKey = parseVersion(candidate)
86
+ const currentKey = parseVersion(current)
87
+ if (!candidateKey || !currentKey) return false
88
+
89
+ const width = Math.max(candidateKey.release.length, currentKey.release.length)
90
+ for (let index = 0; index < width; index += 1) {
91
+ const left = candidateKey.release[index] ?? 0
92
+ const right = currentKey.release[index] ?? 0
93
+ if (left !== right) return left > right
94
+ }
95
+ return comparePre(candidateKey.pre, currentKey.pre) > 0
96
+ }
97
+
98
+ /** 从 registry 的 `latest` 响应体解析版本号;解析失败抛错(调用方降级)。 */
99
+ export function parseRegistryVersion(payload) {
100
+ const body = typeof payload === 'string' ? JSON.parse(payload) : payload
101
+ const version = String(body?.version ?? '').trim()
102
+ if (!version || !parseVersion(version)) {
103
+ throw new Error('npm registry 的 latest 响应里没有有效版本。')
104
+ }
105
+ return version
106
+ }
107
+
108
+ /** 默认取版本方式:直连 npm registry,带 3s 超时,失败抛错。 */
109
+ export async function fetchRegistryLatest(url = REGISTRY_URL) {
110
+ const response = await fetch(url, {
111
+ headers: { Accept: 'application/json', 'User-Agent': `${PACKAGE_NAME} version-check` },
112
+ signal: AbortSignal.timeout(VERSION_CHECK_TIMEOUT_MS),
113
+ })
114
+ if (!response.ok) throw new Error(`npm registry 返回 ${response.status}`)
115
+ return parseRegistryVersion(await response.json())
116
+ }
117
+
118
+ function cachePath(home) {
119
+ return join(home, USER_CONFIG_DIRNAME, VERSION_CHECK_CACHE_FILENAME)
120
+ }
121
+
122
+ function readFreshCache(home, now) {
123
+ try {
124
+ const data = JSON.parse(readFileSync(cachePath(home), 'utf8'))
125
+ const checkedAt = Number(data.checked_at)
126
+ const latest = String(data.latest_version ?? '').trim()
127
+ const age = Math.max(0, now / 1000 - checkedAt)
128
+ if (age < VERSION_CHECK_CACHE_TTL_SECONDS && parseVersion(latest)) return latest
129
+ } catch {
130
+ return null
131
+ }
132
+ return null
133
+ }
134
+
135
+ function writeCache(home, now, latestVersion) {
136
+ const target = cachePath(home)
137
+ const temp = `${target}.${process.pid}.tmp`
138
+ try {
139
+ mkdirSync(dirname(target), { recursive: true })
140
+ writeFileSync(
141
+ temp,
142
+ JSON.stringify({ checked_at: now / 1000, latest_version: latestVersion }),
143
+ 'utf8',
144
+ )
145
+ renameSync(temp, target)
146
+ } catch {
147
+ // 缓存失败不应抹掉已经取得的在线检查结果。
148
+ } finally {
149
+ rmSync(temp, { force: true })
150
+ }
151
+ }
152
+
153
+ /** 读取 `[update].enabled`;缺省开启,配置异常也按开启处理(保底默认)。 */
154
+ export function loadUpdateEnabled(home = homedir()) {
155
+ try {
156
+ const text = readFileSync(join(home, USER_CONFIG_DIRNAME, CONFIG_FILENAME), 'utf8')
157
+ const section = text.match(/(?:^|\n)\[update\]\s*\n(?<body>[\s\S]*?)(?:\n\[|$)/)
158
+ if (!section) return true
159
+ const match = section.groups.body.match(/^\s*enabled\s*=\s*(true|false)\s*$/m)
160
+ if (!match) return true
161
+ return match[1] === 'true'
162
+ } catch {
163
+ return true // 更新功能不能因配置问题影响启动
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 判断是否从源码检出 / 开发环境运行(此类环境不自动更新)。
169
+ *
170
+ * npm 全局安装的启动器会落在 `node_modules/@omnicrawl/cli/bin` 这类目录里,祖先里没有 `.git`;
171
+ * 仓库内开发时祖先里有 `.git`。
172
+ */
173
+ export function isSourceCheckout(launcherPath, maxDepth = 8) {
174
+ let current = dirname(resolve(launcherPath))
175
+ for (let depth = 0; depth < maxDepth; depth += 1) {
176
+ if (existsSync(join(current, '.git'))) return true
177
+ const parent = dirname(current)
178
+ if (parent === current) break
179
+ current = parent
180
+ }
181
+ return false
182
+ }
183
+
184
+ /** 是否需要为这组参数做启动更新:只认工作台路径,已知子命令与逃生口都跳过。 */
185
+ export function shouldCheckUpdate(args, env = process.env) {
186
+ if (env.OMNICRAWL_HOST || env.OMNICRAWL_BINARY) return false
187
+ const first = args[0]
188
+ if (first !== undefined && DISPATCH_COMMANDS.has(first)) return false
189
+ return true
190
+ }
191
+
192
+ function buildInstallCommand(latestVersion) {
193
+ const command = process.env.npm_execpath
194
+ return command
195
+ ? [process.execPath, command, 'install', '-g', ...NPM_QUIET_ARGS, `${PACKAGE_NAME}@${latestVersion}`]
196
+ : ['npm', 'install', '-g', ...NPM_QUIET_ARGS, `${PACKAGE_NAME}@${latestVersion}`]
197
+ }
198
+
199
+ function runDefault(command) {
200
+ const child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: false })
201
+ return new Promise((resolveExit) => {
202
+ child.on('error', () => resolveExit(1))
203
+ child.on('exit', (code) => resolveExit(code ?? 1))
204
+ })
205
+ }
206
+
207
+ function relaunchDefault(launcherPath, args) {
208
+ const child = spawn(process.execPath, [launcherPath, ...args], { stdio: 'inherit' })
209
+ return new Promise((resolveExit) => {
210
+ child.on('error', () => resolveExit(null))
211
+ child.on('exit', (code) => resolveExit(code ?? 0))
212
+ })
213
+ }
214
+
215
+ function manualInstallHint(latestVersion) {
216
+ return `可手动执行升级:npm install -g ${PACKAGE_NAME}@${latestVersion}`
217
+ }
218
+
219
+ /**
220
+ * 启动阶段自动更新入口。
221
+ *
222
+ * @returns `null`:无需更新 / 已跳过 / 更新失败按用户策略继续用当前版本启动;
223
+ * 其他整数:已升级并重启,返回新进程的退出码,调用方应直接退出。
224
+ */
225
+ export async function runStartupUpdateIfDue({
226
+ argv = [],
227
+ currentVersion,
228
+ launcherPath = new URL(import.meta.url).pathname,
229
+ env = process.env,
230
+ home = homedir(),
231
+ print = (line) => console.error(line),
232
+ now = Date.now,
233
+ latestChecker = null,
234
+ installer = null,
235
+ relauncher = null,
236
+ sourceCheckout = null,
237
+ configEnabled = null,
238
+ } = {}) {
239
+ if (env[SKIP_AUTO_UPDATE_ENV] === '1') return null
240
+ if (env[AUTO_UPDATE_ATTEMPTED_ENV] === '1') return null
241
+ if (env.OMNICRAWL_HOST || env.OMNICRAWL_BINARY) return null
242
+ if ((sourceCheckout ?? isSourceCheckout)(launcherPath)) return null
243
+ if (!(configEnabled ?? loadUpdateEnabled)(home)) return null
244
+
245
+ const installed = String(currentVersion ?? '').trim()
246
+ let latest = null
247
+ try {
248
+ latest = readFreshCache(home, now())
249
+ if (latest === null) {
250
+ latest = latestChecker
251
+ ? await latestChecker()
252
+ : await fetchRegistryLatest()
253
+ if (latest) writeCache(home, now(), latest)
254
+ }
255
+ } catch {
256
+ // 版本检查不能改变应用可用性;离线、代理和缓存损坏均只隐藏更新提示。
257
+ return null
258
+ }
259
+ if (!latest || !isNewerVersion(latest, installed)) return null
260
+
261
+ // 先标记尝试,避免子进程(继承环境变量)在升级后因探测不一致再次升级。
262
+ env[AUTO_UPDATE_ATTEMPTED_ENV] = '1'
263
+ print(`发现新版本 ${latest}(当前 ${installed}),正在自动升级…`)
264
+
265
+ let installedOk = false
266
+ try {
267
+ installedOk = (await (installer
268
+ ? installer(latest)
269
+ : runDefault(buildInstallCommand(latest)))) === 0
270
+ } catch {
271
+ installedOk = false
272
+ }
273
+ if (!installedOk) {
274
+ print(`自动升级失败,本次继续使用当前版本 ${installed} 启动。`)
275
+ print(manualInstallHint(latest))
276
+ return null
277
+ }
278
+
279
+ print(`升级完成(${installed} → ${latest}),正在重新启动工作台…`)
280
+ const code = await (relauncher
281
+ ? relauncher(launcherPath, argv)
282
+ : relaunchDefault(launcherPath, argv))
283
+ if (code === null) {
284
+ print('重新启动工作台失败,请手动执行 omnicrawl 重新进入。')
285
+ return 0
286
+ }
287
+ return code
288
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omnicrawl-cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.6",
4
4
  "description": "OmniCrawl 启动器:按平台启动完整宿主(工作台 / api / 插件)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,10 +25,10 @@
25
25
  },
26
26
  "license": "MIT",
27
27
  "optionalDependencies": {
28
- "@omnicrawl/cli-win32-x64": "0.2.3",
29
- "@omnicrawl/cli-win32-ia32": "0.2.3",
30
- "@omnicrawl/cli-linux-x64": "0.2.3",
31
- "@omnicrawl/cli-linux-arm-musl": "0.2.3",
32
- "@omnicrawl/cli-linux-arm64-musl": "0.2.3"
28
+ "@omnicrawl/cli-win32-x64": "0.2.6",
29
+ "@omnicrawl/cli-win32-ia32": "0.2.6",
30
+ "@omnicrawl/cli-linux-x64": "0.2.6",
31
+ "@omnicrawl/cli-linux-arm-musl": "0.2.6",
32
+ "@omnicrawl/cli-linux-arm64-musl": "0.2.6"
33
33
  }
34
34
  }