@ohos-cpf/3rdloop 0.0.6 → 0.0.8

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/lib/serve.js CHANGED
@@ -1,22 +1,28 @@
1
1
  /**
2
- * serve.js —— 3rdloop serve 启动 Server HTTP 后端
2
+ * serve.js —— 3rdloop serve:一条命令同时启动 Server HTTP 后端 + Web 前端
3
3
  *
4
- * 定位:为前端展示启动现成 Server 后端(复用 Server/Routes/server.js
5
- * createServer,不新增任何 daemon/调度逻辑)。CLI 其余命令仍为嵌入式,不受影响。
4
+ * 定位:serve Web 前端绑定启动(原独立 `3rdloop web` 命令已合并至此):
5
+ * - 后端:复用 Server/Routes/server.js 的 createServer(默认 3000,不新增任何 daemon/调度逻辑)
6
+ * - 前端:进程内启动 Web 服务(lib/web.js:静态托管 + /api 反代 + 扩展挂载,默认 8080)
6
7
  *
7
8
  * 行为:
8
- * - 默认仅启动 HTTP 后端(--port / 3LIB_SERVER_PORT / 3000
9
- * - -w/--web 可选:再拉起 Web/server.js 静态前端(反向代理 /api/* 到后端)
10
- * - 复用 CLI 的环境装配(cli.js 已统一执行 .env 链 + stdout 保护),
11
- * 与 run/submit 等命令保持一致;json 模式下本模块自行纯化 stdout
9
+ * - 默认同时启动后端与 Web 前端(--port / 3LIB_SERVER_PORT、--web-port / 3LIB_WEB_PORT
10
+ * - 复用 CLI 的环境装配(cli.js 已统一执行 .env 链 + stdout 保护),与 run/submit 等命令一致
11
+ * - 扩展管理子命令:install-ext / list-ext / remove-ext(不启动服务)
12
12
  * - 端口冲突 / 启动失败给出明确错误,不静默
13
13
  */
14
14
 
15
- import { spawn, execSync } from 'node:child_process';
16
15
  import fs from 'node:fs';
17
16
  import path from 'node:path';
18
17
  import { fileURLToPath } from 'node:url';
19
18
  import process from 'node:process';
19
+ import {
20
+ startWebServer,
21
+ resolveUserExtRoot,
22
+ webExtInstall,
23
+ webExtList,
24
+ webExtRemove,
25
+ } from './web.js';
20
26
 
21
27
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
22
28
  export const CLI_ROOT = path.resolve(moduleDir, '..');
@@ -49,60 +55,112 @@ async function _importServerModule() {
49
55
  return import(url.href);
50
56
  }
51
57
 
52
- /** Web 静态前端目录(开发期仓库 Web/;发布期 vendor/Web 预留)。 */
53
- function resolveWebDir() {
54
- const candidates = [
55
- path.resolve(CLI_ROOT, '..', 'Web'),
56
- path.join(CLI_ROOT, 'vendor', 'Web'),
57
- ];
58
- for (const dir of candidates) {
59
- if (fs.existsSync(path.join(dir, 'server.js')) && fs.existsSync(path.join(dir, 'public'))) {
60
- return dir;
61
- }
62
- }
63
- return null;
64
- }
65
-
66
58
  /**
67
- * 解析 flags.rest 中 serve 专用参数(不依赖 cli.js 的 parseGlobalFlags
68
- * 因为 -w/--web 会被它归入 rest)。
69
- * @returns {{ web: boolean, port?: number, webPort?: number }}
59
+ * 解析 serve 专用参数(不依赖 cli.js 的 parseGlobalFlags,全局 flags 已被其消费)。
60
+ * Web 前端随 serve 默认启动,原 -w/--web 仅作兼容接受(不再有独立语义)。
61
+ * @returns {{ port?: number, webPort?: number, webRoot: string, extRoots: string[], extNames: string[], noExt: boolean, open: boolean, positionals: string[] }}
70
62
  */
71
63
  function _parseServeArgs(rest = []) {
72
- const args = { web: false };
64
+ const args = { webRoot: '', extRoots: [], extNames: [], noExt: false, open: false, positionals: [] };
65
+ const parsePort = (raw, name) => {
66
+ const n = parseInt(raw, 10);
67
+ if (!Number.isFinite(n) || n <= 0 || n > 65535) {
68
+ throw new Error(`非法端口: "${raw}"(${name})`);
69
+ }
70
+ return n;
71
+ };
73
72
  for (let i = 0; i < rest.length; i++) {
74
73
  const arg = rest[i];
75
- if (arg === '-w' || arg === '--web') args.web = true;
76
- else if (arg === '--port' || arg === '--web-port') {
77
- const key = arg === '--port' ? 'port' : 'webPort';
78
- const n = parseInt(rest[i + 1], 10);
79
- if (Number.isFinite(n) && n > 0) args[key] = n;
80
- i++; // 跳过值
74
+
75
+ if (arg === '-w' || arg === '--web') {
76
+ // 兼容旧参数:Web 前端现已默认启动,提示后忽略
77
+ process.stderr.write('[3rdloop serve] 提示: -w/--web 已无需指定,Web 前端随 serve 默认启动\n');
78
+ } else if (arg === '--port') {
79
+ args.port = parsePort(rest[++i], '--port');
80
+ } else if (arg === '--web-port') {
81
+ args.webPort = parsePort(rest[++i], '--web-port');
82
+ } else if (arg === '--web-root') {
83
+ args.webRoot = rest[++i] || '';
84
+ } else if (arg === '--ext-root') {
85
+ const raw = rest[++i];
86
+ if (raw) args.extRoots.push(raw);
87
+ } else if (arg === '--ext') {
88
+ // 白名单:逗号分隔 + 可重复指定(--ext tag --ext issue 等效 --ext tag,issue)
89
+ const raw = rest[++i];
90
+ if (raw) {
91
+ for (const n of raw.split(',')) {
92
+ const t = n.trim();
93
+ if (t) args.extNames.push(t);
94
+ }
95
+ }
96
+ } else if (arg === '--no-ext') {
97
+ args.noExt = true;
98
+ } else if (arg === '--open') {
99
+ args.open = true;
100
+ } else if (arg.startsWith('--')) {
101
+ throw new Error(`未知选项: ${arg}(查看帮助: 3rdloop serve --help)`);
102
+ } else {
103
+ args.positionals.push(arg);
81
104
  }
82
105
  }
83
106
  return args;
84
107
  }
85
108
 
109
+ /** --json 模式的 stdout 输出函数(非 json 模式为 no-op)。 */
110
+ function _jsonOut(jsonMode) {
111
+ return (data) => {
112
+ if (jsonMode) process.stdout.write(JSON.stringify(data, null, 2) + '\n');
113
+ };
114
+ }
115
+
86
116
  /**
87
- * 3rdloop serve 命令实现。
117
+ * 3rdloop serve 命令实现:同时启动 Server HTTP 后端 + Web 前端。
88
118
  * @param {object} opts
89
- * @param {string[]} opts.rest - flags.rest(含 serve 专用参数)
119
+ * @param {string[]} opts.rest - flags.rest(含 serve 专用参数与扩展管理子命令)
90
120
  * @param {boolean} opts.jsonMode
91
121
  * @param {string} [opts.dataDir] - CLI 解析后的数据目录(--data-dir / 3LIB_DATA_DIR / 默认)
92
- * @returns {Promise<number>} 退出码(serve 为长驻命令,仅 --help/启动失败正常返回)
122
+ * @param {string} [opts.user3libHome] - 用户 3lib 主目录(~/.3lib,扩展根解析用)
123
+ * @returns {Promise<number>} 退出码(serve 为长驻命令,仅 --help/子命令/启动失败正常返回)
93
124
  */
94
- export async function cmdServe({ rest = [], jsonMode = false, dataDir = '' }) {
125
+ export async function cmdServe({ rest = [], jsonMode = false, dataDir = '', user3libHome = '' }) {
95
126
  // --help / -h:打印用法
96
127
  if (rest.includes('--help') || rest.includes('-h')) {
97
128
  _printServeHelp();
98
129
  return 0;
99
130
  }
100
131
 
101
- const args = _parseServeArgs(rest);
132
+ let args;
133
+ try {
134
+ args = _parseServeArgs(rest);
135
+ } catch (err) {
136
+ process.stderr.write(`${err.message}\n`);
137
+ _printServeHelp();
138
+ return 1;
139
+ }
140
+
141
+ const userExtRoot = resolveUserExtRoot(user3libHome);
142
+
143
+ // ── 扩展管理子命令(不启动服务) ──
144
+ const [sub, subArg] = args.positionals;
145
+ if (sub === 'install-ext') {
146
+ return webExtInstall(subArg, { userExtRoot, jsonMode, out: _jsonOut(jsonMode) });
147
+ }
148
+ if (sub === 'list-ext') {
149
+ return webExtList({ userExtRoot, jsonMode, out: _jsonOut(jsonMode) });
150
+ }
151
+ if (sub === 'remove-ext') {
152
+ return webExtRemove(subArg, { userExtRoot, jsonMode, out: _jsonOut(jsonMode) });
153
+ }
154
+ if (sub !== undefined) {
155
+ process.stderr.write(`未知子命令: ${sub}\n`);
156
+ _printServeHelp();
157
+ return 1;
158
+ }
159
+
102
160
  const serverPort = args.port ?? _envInt('3LIB_SERVER_PORT', 3000);
103
161
  const webPort = args.webPort ?? _envInt('3LIB_WEB_PORT', 8080);
104
162
 
105
- if (args.web && serverPort === webPort) {
163
+ if (serverPort === webPort) {
106
164
  process.stderr.write(`3rdloop serve: 后端端口与 Web 端口相同 (${serverPort}),请用 --web-port 指定不同端口\n`);
107
165
  return 1;
108
166
  }
@@ -118,106 +176,77 @@ export async function cmdServe({ rest = [], jsonMode = false, dataDir = '' }) {
118
176
 
119
177
  const serverModule = await _importServerModule();
120
178
 
121
- // json 模式下纯化 stdout:Server 内部 console.log/info 重定向到 stderr
122
- let restoreStdout = null;
123
- if (jsonMode) {
124
- restoreStdout = _redirectStdoutLogs();
125
- }
126
-
127
- let webChild = null;
128
-
129
- // 优雅退出:清理 Web 子进程(Windows taskkill 树)后退出
130
- const cleanup = () => {
131
- if (webChild && webChild.pid != null) {
132
- try {
133
- if (process.platform === 'win32') {
134
- execSync(`taskkill /pid ${webChild.pid} /T /F`, { stdio: 'ignore' });
135
- } else {
136
- webChild.kill('SIGTERM');
137
- }
138
- } catch { /* 进程可能已退出 */ }
139
- webChild = null;
140
- }
141
- };
179
+ // ── 优雅退出 ──
180
+ // 必须在 createServer 之前注册:Server 内部也注册了 SIGINT/SIGTERM 处理器
181
+ // (退出码 0),先注册者先执行,保证 CLI 约定的 130/143 退出码语义。
182
+ let webServer = null;
142
183
  let shuttingDown = false;
143
184
  const onSignal = (sig) => {
144
185
  if (shuttingDown) return;
145
186
  shuttingDown = true;
146
- cleanup();
187
+ try {
188
+ // 立即停止 Web 服务并断开存量连接(含 SSE 反代长连接)
189
+ webServer?.close();
190
+ webServer?.closeAllConnections?.();
191
+ } catch { /* 尽力而为 */ }
147
192
  process.exit(sig === 'SIGINT' ? 130 : 143);
148
193
  };
149
194
  process.once('SIGINT', () => onSignal('SIGINT'));
150
195
  process.once('SIGTERM', () => onSignal('SIGTERM'));
151
196
 
197
+ // json 模式下静默 Web 启动横幅(stdout 只放结构化启动信息)
198
+ const log = jsonMode ? () => {} : (msg) => process.stderr.write(`${msg}\n`);
199
+
152
200
  try {
153
- // cliType 由环境决定(shell > .env > 默认 opencode),与 CLI 嵌入式一致;
201
+ // 1) Web 前端先监听(后端反代指向本进程后端端口;
202
+ // Web 端口冲突时在后端启动前快速失败)
203
+ webServer = await startWebServer({
204
+ port: webPort,
205
+ backend: `http://127.0.0.1:${serverPort}`,
206
+ webRootFlag: args.webRoot,
207
+ extRootFlags: args.extRoots,
208
+ userExtRoot,
209
+ noExt: args.noExt,
210
+ extNames: args.extNames,
211
+ dataDir,
212
+ open: args.open,
213
+ log,
214
+ tag: '3rdloop serve',
215
+ });
216
+
217
+ // 2) Server HTTP 后端
218
+ // cliType 由环境决定(shell > .env > 默认 deveco-code),与 CLI 嵌入式一致;
154
219
  // dataDir 已通过 3LIB_DATA_DIR env 注入(见上),Server 内部所有默认
155
220
  // createStorage() 均落到用户数据目录;skillDir 沿用包内 vendor 默认
156
221
  // (npm 更新自动获得新 SKILL,不与 CLI 的 3LIB_SKILL_DIR 联动)
157
- const server = await serverModule.createServer({ port: serverPort });
222
+ await serverModule.createServer({ port: serverPort });
158
223
 
159
224
  if (jsonMode) {
160
225
  process.stdout.write(JSON.stringify({
161
226
  ok: true,
162
227
  type: 'server',
163
228
  pid: process.pid,
164
- port: serverPort,
165
- url: `http://127.0.0.1:${serverPort}`,
166
- web: args.web ? { port: webPort, url: `http://localhost:${webPort}` } : null,
229
+ backend: { port: serverPort, url: `http://127.0.0.1:${serverPort}` },
230
+ web: { port: webPort, url: `http://localhost:${webPort}` },
167
231
  }, null, 2) + '\n');
168
232
  } else {
169
- process.stderr.write(`[3rdloop serve] Server HTTP 后端已启动: http://127.0.0.1:${serverPort}\n`);
233
+ process.stderr.write(`[3rdloop serve] Server 后端已启动: http://127.0.0.1:${serverPort}\n`);
170
234
  process.stderr.write(`[3rdloop serve] 通过 Ctrl+C 停止\n`);
171
235
  }
172
236
 
173
- // ── 可选启动 Web 前端(默认不拉起,保持聚焦后端) ──
174
- if (args.web) {
175
- await _startWebFrontend(webPort, serverPort, jsonMode, (child) => { webChild = child; });
176
- }
177
-
178
237
  // 长驻:保持进程存活直到收到信号
179
238
  await new Promise(() => {});
180
239
  return 0;
181
240
  } catch (err) {
182
- process.stderr.write(`3rdloop serve 启动失败: ${err.message}\n`);
241
+ try {
242
+ webServer?.close();
243
+ webServer?.closeAllConnections?.();
244
+ } catch { /* 尽力而为 */ }
245
+ const reason = err?.code === 'EADDRINUSE'
246
+ ? `端口 ${err.port ?? '?'} 已被占用,可用 --port / --web-port 指定其他端口`
247
+ : err.message;
248
+ process.stderr.write(`3rdloop serve 启动失败: ${reason}\n`);
183
249
  return 1;
184
- } finally {
185
- restoreStdout?.();
186
- }
187
- }
188
-
189
- /** 启动 Web 静态前端(反向代理 /api/* → 后端)。 */
190
- async function _startWebFrontend(webPort, serverPort, jsonMode, onChild) {
191
- try {
192
- if (await _isPortOpen(webPort)) {
193
- process.stderr.write(`[3rdloop serve] Web 端口 ${webPort} 已被占用,跳过拉起前端(可直接访问该地址)\n`);
194
- return;
195
- }
196
- } catch { /* 探测失败不阻断 */ }
197
-
198
- const webDir = resolveWebDir();
199
- if (!webDir) {
200
- process.stderr.write('[3rdloop serve] 未找到 Web/ 前端目录,跳过拉起前端\n');
201
- return;
202
- }
203
-
204
- const child = spawn(process.execPath, [
205
- path.join(webDir, 'server.js'),
206
- '--port', String(webPort),
207
- '--backend', `http://127.0.0.1:${serverPort}`,
208
- ], {
209
- stdio: ['ignore', 'pipe', 'pipe'],
210
- shell: false,
211
- detached: false,
212
- });
213
- child.stdout?.on('data', (d) => process.stderr.write(`[web] ${d}`));
214
- child.stderr?.on('data', (d) => process.stderr.write(`[web] ${d}`));
215
- child.on('error', (err) => {
216
- process.stderr.write(`[3rdloop serve] 拉起 Web 前端失败: ${err.message}\n`);
217
- });
218
- onChild(child);
219
- if (!jsonMode) {
220
- process.stderr.write(`[3rdloop serve] Web 前端已启动: http://localhost:${webPort}\n`);
221
250
  }
222
251
  }
223
252
 
@@ -227,54 +256,38 @@ function _envInt(name, def) {
227
256
  return Number.isFinite(n) && n > 0 ? n : def;
228
257
  }
229
258
 
230
- /** 探测端口是否有响应(尽力而为,不阻断)。 */
231
- function _isPortOpen(port, timeoutMs = 1500) {
232
- return new Promise((resolve) => {
233
- const controller = new AbortController();
234
- const timer = setTimeout(() => controller.abort(), timeoutMs);
235
- fetch(`http://127.0.0.1:${port}/`, { signal: controller.signal })
236
- .then(() => resolve(true))
237
- .catch(() => resolve(false))
238
- .finally(() => clearTimeout(timer));
239
- });
240
- }
241
-
242
- /** json 模式下把 Server 内部 console.log/info 重定向到 stderr,返回恢复函数。 */
243
- function _redirectStdoutLogs() {
244
- const safeStringify = (a) => {
245
- try { return JSON.stringify(a); } catch { return String(a); }
246
- };
247
- const toStderr = (args) => {
248
- process.stderr.write(args.map((a) => (typeof a === 'string' ? a : safeStringify(a))).join(' ') + '\n');
249
- };
250
- const origLog = console.log;
251
- const origInfo = console.info;
252
- console.log = (...args) => toStderr(args);
253
- console.info = (...args) => toStderr(args);
254
- return () => {
255
- console.log = origLog;
256
- console.info = origInfo;
257
- };
258
- }
259
-
260
259
  function _printServeHelp() {
261
- process.stdout.write(`3rdloop serve —— 启动 Server HTTP 后端(供前端展示)
260
+ process.stdout.write(`3rdloop serve —— 同时启动 Server HTTP 后端 + Web 前端
262
261
 
263
262
  用法:
264
- 3rdloop serve [--port N] [-w|--web] [--web-port N]
263
+ 3rdloop serve [选项] 启动后端(默认 3000)+ Web 前端(默认 8080)
264
+ 3rdloop serve install-ext <dir> 安装扩展到 ~/.3lib/3rdloop/web-ext/
265
+ 3rdloop serve list-ext 列出可用扩展(含来源与挂载点)
266
+ 3rdloop serve remove-ext <name> 移除已安装扩展
265
267
 
266
268
  选项:
267
- --port N 后端端口(默认 3000,可用 3LIB_SERVER_PORT 覆盖)
268
- -w, --web 同时拉起 Web 前端(默认 8080,可用 --web-port / 3LIB_WEB_PORT 覆盖)
269
- --web-port N Web 前端端口
270
- --json 输出结构化启动信息到 stdout(进度/日志走 stderr)
271
- -h, --help 本帮助
269
+ --port <N> 后端端口(默认 3000,可用 3LIB_SERVER_PORT 覆盖)
270
+ --web-port <N> Web 前端端口(默认 8080,可用 3LIB_WEB_PORT 覆盖)
271
+ --web-root <dir> 核心前端资产目录覆盖(默认: 仓库 Web/public,发布包为内置 cli/web)
272
+ --ext-root <dir> 追加扩展根目录(可多次指定;目录下每个子目录是一个扩展)
273
+ --ext <name,...> 白名单:仅挂载指定扩展(逗号分隔,可重复指定;未发现的名字打警告)
274
+ --no-ext 禁用全部扩展(纯核心工作台模式;优先于 --ext)
275
+ --open 启动后自动打开浏览器
276
+ --json 输出结构化启动信息到 stdout(进度/日志走 stderr)
277
+ -h, --help 本帮助
272
278
 
273
279
  说明:
274
- 默认只启动 Server HTTP 后端(http://127.0.0.1:3000),供 Web 前端 / 脚本反向代理调用。
280
+ Web 前端随 serve 默认启动(原 "3rdloop web" 命令已合并到 serve):
281
+ 静态托管核心工作台 + /api/*、/stream* 反代到后端 + 扩展挂载。
275
282
  CLI 的 run/submit/wait 等命令保持嵌入式模式,不经此后端。
276
- 退出: Ctrl+C / SIGTERM 优雅停止(Windows 下会清理 Web 子进程树)。
283
+ 退出: Ctrl+C / SIGTERM 优雅停止(退出码 130/143)。
284
+
285
+ 环境变量:
286
+ 3LIB_SERVER_PORT 后端端口(等效 --port)
287
+ 3LIB_WEB_PORT Web 前端端口(等效 --web-port)
288
+ 3LIB_WEB_ROOT 核心前端资产目录(等效 --web-root)
289
+ 3LIB_WEB_EXT_ROOT 用户扩展根目录(默认 ~/.3lib/3rdloop/web-ext)
277
290
  `);
278
291
  }
279
292
 
280
- export default cmdServe;
293
+ export default cmdServe;
package/lib/step.js CHANGED
@@ -94,11 +94,22 @@ async function cmdStepRun({ flags, rest, dataDir, skillDir, jsonMode, quiet, ver
94
94
 
95
95
  const specifiedSkill = _stringFlag(flags, 'specified-skill');
96
96
 
97
- // 挂载 opencode 生命周期(复用 runner 语义)
97
+ // ── 环境预检(引擎执行前快速失败:凭据/目录类问题直接退出码 1) ──
98
+ {
99
+ const { precheckEnv, reportPrecheckFailures } = await import('./doctor.js');
100
+ const pre = await precheckEnv({ projectRoot, dataDir, skillDir });
101
+ if (!pre.ok) {
102
+ if (jsonMode) out({ ok: false, error: '环境预检未通过', taskId, failures: pre.failures });
103
+ else reportPrecheckFailures(pre.failures);
104
+ return EXIT.ERROR;
105
+ }
106
+ }
107
+
108
+ // 挂载 AI CLI serve 生命周期(复用 runner 语义,类型由 CLI_TYPE 决定)
98
109
  const r = await _runner();
99
- const ctx = r.createRunContext({ projectRoot, dataDir, skillDir, cliType: 'opencode', verbose });
100
- const { OpencodeManager } = await import('./opencode.js');
101
- const oc = new OpencodeManager({ projectRoot });
110
+ const ctx = r.createRunContext({ projectRoot, dataDir, skillDir, verbose });
111
+ const { CliServeManager } = await import('./opencode.js');
112
+ const oc = new CliServeManager({ projectRoot, cliType: ctx.cliType });
102
113
  const ocHealth = await oc.ensureRunning();
103
114
  if (!ocHealth.ok) {
104
115
  if (jsonMode) out({ ok: false, error: ocHealth.message, taskId });
package/lib/update.js CHANGED
@@ -3,8 +3,7 @@
3
3
  *
4
4
  * 职责:
5
5
  * - 检测本地版本(getCliVersion)与 npm registry 最新版
6
- * - 识别开发态(npm link 软链)与正式全局安装
7
- * - 交互确认后通过 npm install -g 重装
6
+ * - 交互确认后卸载旧版 + npm install -g 重装最新版
8
7
  *
9
8
  * 交互/输出契约:
10
9
  * - 进度/日志 → stderr;--json 结构化结果 → stdout
@@ -17,7 +16,6 @@
17
16
  * 该分支对用户可控的 --registry 值做白名单校验防 shell 注入
18
17
  */
19
18
 
20
- import fs from 'node:fs';
21
19
  import path from 'node:path';
22
20
  import readline from 'node:readline';
23
21
  import { execFileSync } from 'node:child_process';
@@ -26,7 +24,6 @@ import { fileURLToPath } from 'node:url';
26
24
  // 本文件在 cli/lib/ 下
27
25
  const CLI_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
28
26
  const PKG_NAME = '@ohos-cpf/3rdloop';
29
- const BIN_ABS = path.join(CLI_ROOT, 'bin', '3rdloop.mjs');
30
27
 
31
28
  // ── npm 调用封装 ─────────────────────────────────────────────────
32
29
 
@@ -92,22 +89,6 @@ export function compareVersions(a, b) {
92
89
  return String(preA).localeCompare(String(preB));
93
90
  }
94
91
 
95
- /**
96
- * 检测当前命令是否为 npm link 软链(开发态)。
97
- *
98
- * 原理:npm link 的全局 bin 是指向仓库 bin 的软链;真实全局安装则是
99
- * node_modules 内的副本。比较 process.argv[1] 与仓库内 bin 的 realpath 是否相同。
100
- */
101
- export function detectLinkMode(argv1 = process.argv[1]) {
102
- try {
103
- const realArgv = fs.realpathSync(argv1 || '');
104
- const realLocal = fs.realpathSync(BIN_ABS);
105
- return realArgv === realLocal;
106
- } catch {
107
- return false;
108
- }
109
- }
110
-
111
92
  /**
112
93
  * 获取最新版本。
113
94
  * 优先 `npm view`(尊重用户 .npmrc 镜像配置);失败回退 packument API。
@@ -171,33 +152,15 @@ export async function confirmUpdate({ current, latest } = {}) {
171
152
  }
172
153
 
173
154
  /**
174
- * 执行更新。
155
+ * 执行更新:卸载旧版后通过 npm install -g 重装最新版。
175
156
  * @param {object} opts
176
157
  * @param {string} opts.latest 目标版本(registry latest)
177
158
  * @param {string} [opts.registry] registry 覆盖
178
- * @param {boolean} [opts.force] 开发态也强制替换
179
- * @param {boolean} [opts.isLink] 是否 link 模式(可注入便于测试)
180
159
  */
181
- export function performUpdate({ latest, registry, force = false, isLink = detectLinkMode() } = {}) {
160
+ export function performUpdate({ latest, registry } = {}) {
182
161
  const installArgs = ['install', '-g', `${PKG_NAME}@latest`];
183
162
  if (registry) installArgs.push('--registry', registry);
184
163
 
185
- // 开发态(npm link):默认警告并跳过,仅提示 git pull
186
- if (isLink && !force) {
187
- process.stderr.write(
188
- `\n[update] 检测到当前为开发态(npm link),已跳过自更新。\n` +
189
- ` 开发态请用 git pull 更新仓库源码(数据目录 ~/.3lib/3rdloop/db 不受影响)。\n` +
190
- ` 如需强制用正式包替换链接,可加 --force。\n`
191
- );
192
- return { updated: false, skipped: 'link-mode' };
193
- }
194
-
195
- // 开发态 + --force:先解除链接
196
- if (isLink && force) {
197
- process.stderr.write(`[update] 检测到 npm link,先解除链接...\n`);
198
- runNpm(['unlink', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
199
- }
200
-
201
164
  process.stderr.write(`[update] 卸载旧版本(@${PKG_NAME})...\n`);
202
165
  try {
203
166
  runNpm(['uninstall', '-g', PKG_NAME], { encoding: 'utf-8', stdio: 'inherit' });
@@ -219,19 +182,17 @@ export function performUpdate({ latest, registry, force = false, isLink = detect
219
182
  export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
220
183
  const checkOnly = rest.includes('--check');
221
184
  const yes = rest.includes('--yes') || rest.includes('-y');
222
- const force = rest.includes('--force');
223
185
  const regIdx = rest.indexOf('--registry');
224
186
  const registry = regIdx !== -1 ? rest[regIdx + 1] : undefined;
225
187
 
226
188
  // update 用法说明(--help,离线可测,不触碰网络)
227
189
  if (rest.includes('--help') || rest.includes('-h')) {
228
190
  const help = [
229
- '用法: 3rdloop update [--check] [--yes] [--force] [--registry <url>]',
191
+ '用法: 3rdloop update [--check] [--yes] [--registry <url>]',
230
192
  '',
231
193
  ' 默认: 检测本地版本与 npm registry 最新版,差异存在时交互确认后重装',
232
194
  ' --check 仅检查是否有新版本,不执行更新',
233
195
  ' --yes, -y 跳过交互确认直接更新(非 TTY 必须)',
234
- ' --force 开发态(npm link)也强制替换为 registry 正式包',
235
196
  ' --registry <url> 覆盖 npm registry(默认尊重用户 npm 镜像配置)',
236
197
  ' 注意: 数据目录 ~/.3lib/3rdloop/db 与 SKILL 配置不受更新影响',
237
198
  ].join('\n');
@@ -268,7 +229,7 @@ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
268
229
  if (!checkOnly) {
269
230
  // --json 下不交互,直接更新(用户显式要求命令行为,等价 --yes)
270
231
  try {
271
- const r = performUpdate({ latest, registry, force });
232
+ const r = performUpdate({ latest, registry });
272
233
  process.stdout.write(JSON.stringify({ ...r, ok: true }, null, 2) + '\n');
273
234
  return { ok: true, ...r };
274
235
  } catch (err) {
@@ -310,7 +271,7 @@ export async function cmdUpdateImpl({ jsonMode = false, rest = [] } = {}) {
310
271
 
311
272
  process.stderr.write(`\n`);
312
273
  try {
313
- const r = performUpdate({ latest, registry, force });
274
+ const r = performUpdate({ latest, registry });
314
275
  return { ok: true, ...r };
315
276
  } catch (err) {
316
277
  const msg = `更新失败: ${err.message}`;