mocode-ai 1.0.4 → 1.0.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.
package/bin/mocode.js CHANGED
@@ -1,4 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  // mocode 全局入口:shebang + 拉起编译产物。子命令(--resume / config)路由在 dist/index.js 的 main() 内,
3
3
  // 用动态 import 按需加载,避免 `mocode config`(零配置时)触发 config/index.ts 的 requireEnv 退出。
4
+
5
+ // 抑制 openai v4 -> node-fetch v2 -> whatwg-url v5 链触发的 Node 内置 punycode 弃用警告,
6
+ // 防止 [DEP0040] 输出污染终端 UI 的输入框区域。
7
+ const originalEmit = process.emit;
8
+ process.emit = function (event, warning, ...args) {
9
+ if (
10
+ event === 'warning' &&
11
+ warning &&
12
+ warning.code === 'DEP0040' &&
13
+ typeof warning.message === 'string' &&
14
+ warning.message.includes('punycode')
15
+ ) {
16
+ return true;
17
+ }
18
+ return originalEmit.apply(this, [event, warning, ...args]);
19
+ };
20
+
4
21
  await import('../dist/index.js');
@@ -0,0 +1,104 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { spawn } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ /**
7
+ * /upgrade 命令实现:按需检查/升级 mocode,不阻塞启动。
8
+ *
9
+ * 设计要点:
10
+ * - 启动时不检查、不联网、不 spawn:把旧自动升级完全删除后,REPL 首屏即进输入态。
11
+ * - 用户主动输入 /upgrade 时才拉 registry / 执行 npm i -g。
12
+ * - 当前进程正在运行 dist/*.js,后台 npm install 只改磁盘,**下次启动**才用上新版本。
13
+ * - dev 运行(tsx src/index.ts)时禁真 spawn,防止误改全局包;编译产物运行(mocode 全局命令)才执行。
14
+ * - 失败时返回可读错误,不抛异常,避免 REPL 崩掉。
15
+ */
16
+ const PKG_NAME = 'mocode-ai';
17
+ const PKG_PATH = fileURLToPath(new URL('../../package.json', import.meta.url));
18
+ const FETCH_TIMEOUT_MS = 5000;
19
+ /** 是否 tsx 开发运行(import.meta.url 以 .ts 结尾)。 */
20
+ export function isDevRun() {
21
+ try {
22
+ return fileURLToPath(import.meta.url).endsWith('.ts');
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ }
28
+ /** 读当前安装版本(package.json 的 version)。读不到返 '0.0.0'。 */
29
+ export function getCurrentVersion() {
30
+ try {
31
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
32
+ return pkg.version ?? '0.0.0';
33
+ }
34
+ catch {
35
+ return '0.0.0';
36
+ }
37
+ }
38
+ /** registry 取自 ~/.npmrc(与 `npm i -g` 同源);无则用官方端点。保证尾 `/`。 */
39
+ export function readRegistry() {
40
+ let reg = 'https://registry.npmjs.org/';
41
+ try {
42
+ const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
43
+ const m = /^registry\s*=\s*["']?([^"'\s]+)["']?\s*$/m.exec(npmrc);
44
+ if (m && m[1])
45
+ reg = m[1];
46
+ }
47
+ catch {
48
+ // 无 ~/.npmrc:用默认
49
+ }
50
+ return reg.endsWith('/') ? reg : reg + '/';
51
+ }
52
+ /** 简易 semver 比较(只管 X.Y.Z 数字,无 pre-release):a>b 返正,a<b 返负,等返 0。 */
53
+ export function compareSemver(a, b) {
54
+ const pa = a.split('.').map((x) => Number(x) || 0);
55
+ const pb = b.split('.').map((x) => Number(x) || 0);
56
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
57
+ const da = pa[i] ?? 0;
58
+ const db = pb[i] ?? 0;
59
+ if (da !== db)
60
+ return da - db;
61
+ }
62
+ return 0;
63
+ }
64
+ /** 拉 registry 最新版。超时/失败返 null,不抛异常。 */
65
+ export async function fetchLatestVersion(timeoutMs = FETCH_TIMEOUT_MS) {
66
+ try {
67
+ const ctrl = new AbortController();
68
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
69
+ const res = await fetch(`${readRegistry()}${PKG_NAME}/latest`, { signal: ctrl.signal });
70
+ clearTimeout(t);
71
+ if (!res.ok)
72
+ return null;
73
+ const data = (await res.json());
74
+ return data.version ?? null;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ /** 检查当前版本与 registry 最新版本。 */
81
+ export async function checkVersion() {
82
+ const current = getCurrentVersion();
83
+ const latest = await fetchLatestVersion();
84
+ const hasUpdate = latest ? compareSemver(latest, current) > 0 : false;
85
+ return { current, latest, hasUpdate };
86
+ }
87
+ /** 后台 spawn `npm i -g mocode-ai@latest`。 */
88
+ export function spawnUpgrade() {
89
+ if (isDevRun() || process.env.MOCODE_NO_SPAWN)
90
+ return;
91
+ try {
92
+ const child = spawn('npm', ['install', '-g', `${PKG_NAME}@latest`], {
93
+ shell: true,
94
+ detached: true,
95
+ stdio: 'ignore',
96
+ windowsHide: true,
97
+ });
98
+ child.unref();
99
+ child.on('error', () => { });
100
+ }
101
+ catch {
102
+ // 忽略
103
+ }
104
+ }
@@ -57,6 +57,10 @@ const zhCN = {
57
57
  'commands.language': '切换界面与回复语言',
58
58
  'commands.languageZh': '切换到中文',
59
59
  'commands.languageEn': 'Switch to English',
60
+ 'commands.upgrade': '升级 mocode 到最新版本',
61
+ 'commands.upgradeNow': '立即升级',
62
+ 'commands.upgradeCheck': '检查当前版本与最新版本差异',
63
+ 'commands.upgradeStatus': '显示当前本地版本',
60
64
  'help.title': '可用斜杠命令',
61
65
  'help.hint': '(› 表示可进入子菜单;→ 后为实际执行命令)',
62
66
  'language.current': '当前语言:{language}',
@@ -92,6 +96,8 @@ const zhCN = {
92
96
  'running.skill': '项目 Skill',
93
97
  'running.language': '切语言',
94
98
  'running.chooseLanguage': '选择语言…',
99
+ 'running.upgrade': '升级',
100
+ 'running.upgrading': '升级中…',
95
101
  'prompt.lines': '{count} 行',
96
102
  'prompt.chars': '{count} 字符',
97
103
  'prompt.chooseRollback': '↑↓ 选择 · Enter 回滚到该轮 · Esc 取消',
@@ -230,6 +236,17 @@ const zhCN = {
230
236
  'plan.running': '执行',
231
237
  'plan.executing': '按计划执行…',
232
238
  'plan.executePrompt': '请按上述计划执行。',
239
+ 'upgrade.currentVersion': '当前版本:{version}',
240
+ 'upgrade.latestVersion': '最新版本:{version}',
241
+ 'upgrade.noUpdate': '已是最新版本 v{version}',
242
+ 'upgrade.hasUpdate': '发现新版本:当前 v{current} → 最新 v{latest}',
243
+ 'upgrade.upgradeStarted': '已启动后台升级 → {version}',
244
+ 'upgrade.upgradeHint': 'npm install -g mocode-ai@latest 正在后台运行,完成后下次启动 mocode 即生效。',
245
+ 'upgrade.devSkip': '当前为开发运行(tsx),已跳过真实 npm install,避免误改全局包。',
246
+ 'upgrade.fetchFailed': '无法获取最新版本(请检查网络或 npm registry)。',
247
+ 'upgrade.pkgName': '包名:mocode-ai',
248
+ 'upgrade.failed': '[upgrade] 失败:{message}',
249
+ 'upgrade.usage': '/upgrade 用法:\n /upgrade 升级到最新版本\n /upgrade now 同 /upgrade\n /upgrade check 检查当前版本与最新版本差异\n /upgrade status 显示当前本地版本',
233
250
  'assistant.languageInstruction': 'Reply to the user in Chinese.',
234
251
  };
235
252
  const en = {
@@ -291,6 +308,10 @@ const en = {
291
308
  'commands.language': 'Switch interface and reply language',
292
309
  'commands.languageZh': '切换到中文',
293
310
  'commands.languageEn': 'Switch to English',
311
+ 'commands.upgrade': 'Upgrade mocode to the latest version',
312
+ 'commands.upgradeNow': 'Upgrade now',
313
+ 'commands.upgradeCheck': 'Check current vs latest version',
314
+ 'commands.upgradeStatus': 'Show current local version',
294
315
  'help.title': 'Available slash commands',
295
316
  'help.hint': '(› opens a submenu; → shows the command that will run)',
296
317
  'language.current': 'Current language: {language}',
@@ -326,6 +347,8 @@ const en = {
326
347
  'running.skill': 'Project Skill',
327
348
  'running.language': 'Language',
328
349
  'running.chooseLanguage': 'Choose a language…',
350
+ 'running.upgrade': 'Upgrade',
351
+ 'running.upgrading': 'Upgrading…',
329
352
  'prompt.lines': '{count} lines',
330
353
  'prompt.chars': '{count} characters',
331
354
  'prompt.chooseRollback': '↑↓ Select · Enter Roll back to turn · Esc Cancel',
@@ -464,6 +487,17 @@ const en = {
464
487
  'plan.running': 'Execute',
465
488
  'plan.executing': 'Executing the plan…',
466
489
  'plan.executePrompt': 'Execute the plan above.',
490
+ 'upgrade.currentVersion': 'Current version: {version}',
491
+ 'upgrade.latestVersion': 'Latest version: {version}',
492
+ 'upgrade.noUpdate': 'Already up to date: v{version}',
493
+ 'upgrade.hasUpdate': 'New version available: v{current} → v{latest}',
494
+ 'upgrade.upgradeStarted': 'Background upgrade started → {version}',
495
+ 'upgrade.upgradeHint': 'npm install -g mocode-ai@latest is running in the background; restart mocode to use it.',
496
+ 'upgrade.devSkip': 'Running in dev mode (tsx); skipped real npm install to avoid touching the global package.',
497
+ 'upgrade.fetchFailed': 'Unable to fetch the latest version (check network or npm registry).',
498
+ 'upgrade.pkgName': 'Package: mocode-ai',
499
+ 'upgrade.failed': '[upgrade] failed: {message}',
500
+ 'upgrade.usage': 'Usage: /upgrade\n /upgrade Upgrade to latest\n /upgrade now Same as /upgrade\n /upgrade check Check current vs latest\n /upgrade status Show current local version',
467
501
  'assistant.languageInstruction': 'Reply to the user in English.',
468
502
  };
469
503
  const resources = {
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { exitAltScreen } from './ui/layout.js';
2
- import { checkAndMaybeUpdate } from './updater/index.js';
3
2
  import { readConfigFile } from './config/file.js';
4
3
  import { detectLanguage, setLanguage, t } from './i18n/index.js';
5
4
  // 终端恢复兜底:任一退出 / 中断 / 未捕获异常路径都要恢复 alt screen,避免残留备用屏 + 滚动区域。
@@ -79,14 +78,12 @@ async function main() {
79
78
  console.error(t('cli.sessionMissing', { id }));
80
79
  process.exit(1);
81
80
  }
82
- const updateNotice = checkAndMaybeUpdate();
83
81
  const { startRepl } = await import('./repl/index.js');
84
- await startRepl(loaded.history, loaded.id, updateNotice, sandboxRootOverride, loaded.queryHistory);
82
+ await startRepl(loaded.history, loaded.id, sandboxRootOverride, loaded.queryHistory);
85
83
  }
86
84
  else {
87
- const updateNotice = checkAndMaybeUpdate();
88
85
  const { startRepl } = await import('./repl/index.js');
89
- await startRepl(undefined, undefined, updateNotice, sandboxRootOverride);
86
+ await startRepl(undefined, undefined, sandboxRootOverride);
90
87
  }
91
88
  process.exit(0);
92
89
  }
@@ -32,6 +32,7 @@ import fs from 'node:fs';
32
32
  import path from 'node:path';
33
33
  import { getSandboxRoot } from '../sandbox/root.js';
34
34
  import { setCurrentSessionId, getCurrentSessionId } from '../session/state.js';
35
+ import { checkVersion, fetchLatestVersion, getCurrentVersion, isDevRun, spawnUpgrade, } from '../commands/upgrade.js';
35
36
  /**
36
37
  * readline 的 prompt 必须是纯文本(无 ANSI):readline 按字符数算光标位置,
37
38
  * 颜色码会让光标错位、编辑时漂移。颜色只用在直接 stdout.write 的横幅 / 工具行 / 回复。
@@ -126,6 +127,13 @@ function buildSlashCommands() {
126
127
  { name: 'en', value: '/language en', desc: d('commands.languageEn') },
127
128
  ],
128
129
  },
130
+ {
131
+ name: '/upgrade', desc: d('commands.upgrade'), children: [
132
+ { name: 'now', value: '/upgrade', desc: d('commands.upgradeNow') },
133
+ { name: 'check', value: '/upgrade check', desc: d('commands.upgradeCheck') },
134
+ { name: 'status', value: '/upgrade status', desc: d('commands.upgradeStatus') },
135
+ ],
136
+ },
129
137
  ];
130
138
  }
131
139
  /** 从菜单树递归生成 /help 内容;叶子 value 与菜单路径不同则同时展示真实命令。 */
@@ -342,6 +350,8 @@ function runningStateFor(cmd) {
342
350
  return { status: t('running.snapshot'), placeholder: t('running.switching') };
343
351
  case '/language':
344
352
  return { status: t('running.language'), placeholder: t('running.chooseLanguage') };
353
+ case '/upgrade':
354
+ return { status: t('running.upgrade'), placeholder: t('running.upgrading') };
345
355
  default:
346
356
  return { status: '', placeholder: '' };
347
357
  }
@@ -704,7 +714,7 @@ export function renderHistory(history) {
704
714
  * contentWrite 落入内容区(滚动区域内自动滚动,底栏不动)。history 由本模块持有,在轮次间持久;
705
715
  * agent 只读取并追加(+ 经 session/ 压缩)。每轮成功结束后自动落盘,退出后可用 --resume / /resume 续接。
706
716
  */
707
- export async function startRepl(initialHistory, sessionId, updateNotice = null, sandboxRootOverride, initialQueryHistory) {
717
+ export async function startRepl(initialHistory, sessionId, sandboxRootOverride, initialQueryHistory) {
708
718
  // 模式重置:agentMode 不落盘,每个 REPL 会话从 auto 开始(/resume / --resume 亦重置)。
709
719
  setAgentMode('auto');
710
720
  // 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
@@ -809,11 +819,6 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
809
819
  }
810
820
  }
811
821
  }
812
- if (updateNotice) {
813
- // 自更新提示:放 /plan · /auto 行下方,黄色(警示色)突出"你正在用的版本较旧",与 dim 提示区分。
814
- // 开场静态段(进 INPUT 态前),一行,纯文本不与流式 / 输入争用。
815
- layout.contentWrite(` ${ui.yellow}⚠ ${updateNotice}${ui.reset}\n`);
816
- }
817
822
  /**
818
823
  * 切换 agent 模式(Shift+Tab 触发,经 prompt.ts 的 onCycleMode 回调)。
819
824
  * cycleMode 翻 agentMode + applyMode 重写 history[0] + refreshStatusBase 设状态行 modeTag;
@@ -1154,6 +1159,72 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1154
1159
  // /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 MOCODE.md),fall through 走 runAgent
1155
1160
  joined = INIT_PROMPT;
1156
1161
  }
1162
+ if (line === '/upgrade' || line.startsWith('/upgrade ')) {
1163
+ const arg = line === '/upgrade' ? '' : line.slice('/upgrade '.length).trim().toLowerCase();
1164
+ // /upgrade check — 联网检查当前版本与最新版本差异
1165
+ if (arg === 'check') {
1166
+ const signal = startRunningListener(t('running.upgrading'));
1167
+ try {
1168
+ const info = await checkVersion();
1169
+ if (!info.latest) {
1170
+ layout.contentWrite(`${ui.yellow}⚠ ${t('upgrade.fetchFailed')}${ui.reset}\n`);
1171
+ }
1172
+ else if (info.hasUpdate) {
1173
+ layout.contentWrite(`${ui.cyan}● ${t('upgrade.hasUpdate', { current: info.current, latest: info.latest })}${ui.reset}\n`);
1174
+ layout.contentWrite(`${ui.dim}${t('upgrade.upgradeStarted', { version: `v${info.latest}` })}${ui.reset}\n`);
1175
+ }
1176
+ else {
1177
+ layout.contentWrite(`${ui.green}✓ ${t('upgrade.noUpdate', { version: info.current })}${ui.reset}\n`);
1178
+ }
1179
+ }
1180
+ catch (e) {
1181
+ const msg = e instanceof Error ? e.message : String(e);
1182
+ layout.contentWrite(`${ui.red}${t('upgrade.failed', { message: msg })}${ui.reset}\n`);
1183
+ }
1184
+ finally {
1185
+ stopRunningListener();
1186
+ }
1187
+ continue;
1188
+ }
1189
+ // /upgrade status — 只显示本地当前版本(不联网)
1190
+ if (arg === 'status') {
1191
+ const current = getCurrentVersion();
1192
+ layout.contentWrite(`${ui.dim}${t('upgrade.currentVersion', { version: `v${current}` })}${ui.reset}\n`);
1193
+ layout.contentWrite(`${ui.dim}${t('upgrade.pkgName')}${ui.reset}\n`);
1194
+ continue;
1195
+ }
1196
+ // /upgrade 或 /upgrade now — 直接执行升级
1197
+ if (arg === '' || arg === 'now') {
1198
+ const signal = startRunningListener(t('running.upgrading'));
1199
+ try {
1200
+ // 先快速拉一次最新版本,给出一个明确的升级目标提示
1201
+ const latest = await fetchLatestVersion();
1202
+ const current = getCurrentVersion();
1203
+ if (latest && latest === current) {
1204
+ layout.contentWrite(`${ui.green}✓ ${t('upgrade.noUpdate', { version: current })}${ui.reset}\n`);
1205
+ continue;
1206
+ }
1207
+ spawnUpgrade();
1208
+ const target = latest ? `v${latest}` : t('upgrade.latestVersion', { version: '?' });
1209
+ layout.contentWrite(`${ui.cyan}● ${t('upgrade.upgradeStarted', { version: target })}${ui.reset}\n`);
1210
+ layout.contentWrite(`${ui.dim}${t('upgrade.upgradeHint')}${ui.reset}\n`);
1211
+ if (isDevRun()) {
1212
+ layout.contentWrite(`${ui.yellow}⚠ ${t('upgrade.devSkip')}${ui.reset}\n`);
1213
+ }
1214
+ }
1215
+ catch (e) {
1216
+ const msg = e instanceof Error ? e.message : String(e);
1217
+ layout.contentWrite(`${ui.red}${t('upgrade.failed', { message: msg })}${ui.reset}\n`);
1218
+ }
1219
+ finally {
1220
+ stopRunningListener();
1221
+ }
1222
+ continue;
1223
+ }
1224
+ // 未知子命令
1225
+ layout.contentWrite(`${ui.yellow}${t('upgrade.usage')}${ui.reset}\n`);
1226
+ continue;
1227
+ }
1157
1228
  if (line === '/plan') {
1158
1229
  // /plan:切到 plan 模式(只读探查 + 产出计划)。
1159
1230
  if (getAgentMode() === 'plan') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,22 +0,0 @@
1
- export interface UpdateCache {
2
- lastCheck?: number;
3
- latest?: string;
4
- lastSpawn?: number;
5
- }
6
- /** 简易 semver 比较(只管 X.Y.Z 数字,无 pre-release):a>b 返正,a<b 返负,等返 0。 */
7
- export declare function compareSemver(a: string, b: string): number;
8
- export interface UpdateDecision {
9
- notice: string | null;
10
- spawn: boolean;
11
- refresh: boolean;
12
- }
13
- /** 纯决策:据当前版本 / 缓存 / 时间,算是否提示 / spawn / 后台刷新。无副作用,可离线测。
14
- * notice 同时携带当前版本(`current`)与最新版本(`latest`),让 REPL 提示能向用户
15
- * 清晰展示"你正在用的 mocode 是 X,最新是 Y",不只是单调的"有新版本"。 */
16
- export declare function evaluateUpdate(current: string, cache: UpdateCache, now: number): UpdateDecision;
17
- /**
18
- * 启动时调(同步,零延迟):返一行纯文本更新提示或 null。
19
- * 流程:dev 跳过 → 读当前版本 → 读缓存 → evaluateUpdate 决策 → 按需后台刷新 / spawn → 返提示。
20
- * 提示由 repl 上色写入内容区开场段(进 INPUT 态前)。
21
- */
22
- export declare function checkAndMaybeUpdate(): string | null;
@@ -1,163 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { spawn } from 'node:child_process';
5
- import { fileURLToPath } from 'node:url';
6
- /**
7
- * 启动时自动检测并后台自更新(仿 update-notifier,但**真更新**而非仅提示)。
8
- *
9
- * 设计要点:
10
- * - **零启动延迟**:`checkAndMaybeUpdate()` 同步返回提示串或 null;联网刷新与 `npm i -g` 都是
11
- * fire-and-forget 后台操作(不 await),不卡 REPL 启动。
12
- * - **下次启动生效**:当前进程已把 dist/*.js 读入内存,运行中不持有文件句柄;后台 `npm i -g`
13
- * 覆写磁盘包(含 mocode.cmd shim),当前进程不受影响,**下次** `mocode` 即新版本。
14
- * - **节流**:检查 24h、spawn 6h(防每跑必联网 / 重复 spawn)。更新成功后下次重读 package.json
15
- * 得 current=latest → 不再 spawn、不再提示。
16
- * - **不依赖 config**:只用 stdlib + 全局 fetch,缺 LLM 配置也能跑(在 config 校验前调)。
17
- * - **dev 跳过**:tsx 运行 `.ts`(`npm start`)不自更新;编译态 `.js`(`mocode` 全局命令)才更新。
18
- * - **失败静默**:断网 / 无 npm / 权限不足都不阻断启动。
19
- */
20
- const PKG_NAME = 'mocode-ai';
21
- /** 包根 package.json:从 dist/updater/ 或 src/updater/ 均 `../../package.json`(npm 必带 package.json)。 */
22
- const PKG_PATH = fileURLToPath(new URL('../../package.json', import.meta.url));
23
- const CACHE_PATH = path.join(os.homedir(), '.mocode', 'update-check.json');
24
- const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24h:多久重新拉一次 registry
25
- const SPAWN_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h:同一待更新版本多久内不重复 spawn
26
- const FETCH_TIMEOUT_MS = 2500;
27
- /** 缓存路径:MOCODE_UPDATE_CACHE 可重定向(测试用临时文件,避免污染真实 ~/.mocode 缓存)。 */
28
- function cachePath() {
29
- return process.env.MOCODE_UPDATE_CACHE || CACHE_PATH;
30
- }
31
- /** tsx 开发态(import.meta.url 以 .ts 结尾)跳过自更新;编译态 .js 才更新。 */
32
- function isDevRun() {
33
- try {
34
- return fileURLToPath(import.meta.url).endsWith('.ts');
35
- }
36
- catch {
37
- return false;
38
- }
39
- }
40
- /** 读当前安装版本(package.json 的 version)。读不到返 ''。 */
41
- function readCurrentVersion() {
42
- try {
43
- const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8'));
44
- return pkg.version ?? '';
45
- }
46
- catch {
47
- return '';
48
- }
49
- }
50
- function readCache() {
51
- try {
52
- return JSON.parse(fs.readFileSync(cachePath(), 'utf8'));
53
- }
54
- catch {
55
- return {};
56
- }
57
- }
58
- /** 读-合并-写(各写者独立 merge,降低后台 fetch 与同步 spawn 写的竞态丢字段风险)。 */
59
- function writeCache(patch) {
60
- try {
61
- const merged = { ...readCache(), ...patch };
62
- fs.mkdirSync(path.dirname(cachePath()), { recursive: true });
63
- fs.writeFileSync(cachePath(), JSON.stringify(merged), 'utf8');
64
- }
65
- catch {
66
- // 写缓存失败不阻断
67
- }
68
- }
69
- /** registry 取自 ~/.npmrc(国内镜像用户与 `npm i -g` 装的源一致);无则用官方端点。保证尾 `/`。 */
70
- function readRegistry() {
71
- let reg = 'https://registry.npmjs.org/';
72
- try {
73
- const npmrc = fs.readFileSync(path.join(os.homedir(), '.npmrc'), 'utf8');
74
- const m = /^registry\s*=\s*["']?([^"'\s]+)["']?\s*$/m.exec(npmrc);
75
- if (m && m[1])
76
- reg = m[1];
77
- }
78
- catch {
79
- // 无 ~/.npmrc:用默认
80
- }
81
- return reg.endsWith('/') ? reg : reg + '/';
82
- }
83
- /** 后台拉 registry 最新版(不 await):成功写 latest+lastCheck,失败只写 lastCheck(防离线每跑必联网)。 */
84
- async function fetchLatestInBackground() {
85
- try {
86
- const ctrl = new AbortController();
87
- const t = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
88
- const res = await fetch(`${readRegistry()}${PKG_NAME}/latest`, { signal: ctrl.signal });
89
- clearTimeout(t);
90
- if (!res.ok)
91
- throw new Error(`HTTP ${res.status}`);
92
- const data = (await res.json());
93
- writeCache(data.version ? { latest: data.version, lastCheck: Date.now() } : { lastCheck: Date.now() });
94
- }
95
- catch {
96
- writeCache({ lastCheck: Date.now() });
97
- }
98
- }
99
- /** 后台 spawn `npm i -g <pkg>@latest`(Win:shell 解析 npm.cmd + windowsHide 防控制台闪;detached+unref 跨进程存活)。 */
100
- function spawnUpdate() {
101
- if (process.env.MOCODE_NO_SPAWN)
102
- return; // 测试门控:禁真跑 npm
103
- try {
104
- const child = spawn('npm', ['install', '-g', `${PKG_NAME}@latest`], {
105
- shell: true,
106
- detached: true,
107
- stdio: 'ignore',
108
- windowsHide: true,
109
- });
110
- child.unref();
111
- child.on('error', () => { });
112
- }
113
- catch {
114
- // 忽略
115
- }
116
- }
117
- /** 简易 semver 比较(只管 X.Y.Z 数字,无 pre-release):a>b 返正,a<b 返负,等返 0。 */
118
- export function compareSemver(a, b) {
119
- const pa = a.split('.').map((x) => Number(x) || 0);
120
- const pb = b.split('.').map((x) => Number(x) || 0);
121
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
122
- const da = pa[i] ?? 0;
123
- const db = pb[i] ?? 0;
124
- if (da !== db)
125
- return da - db;
126
- }
127
- return 0;
128
- }
129
- /** 纯决策:据当前版本 / 缓存 / 时间,算是否提示 / spawn / 后台刷新。无副作用,可离线测。
130
- * notice 同时携带当前版本(`current`)与最新版本(`latest`),让 REPL 提示能向用户
131
- * 清晰展示"你正在用的 mocode 是 X,最新是 Y",不只是单调的"有新版本"。 */
132
- export function evaluateUpdate(current, cache, now) {
133
- const refresh = !cache.latest || now - (cache.lastCheck ?? 0) > CHECK_INTERVAL_MS;
134
- let notice = null;
135
- let spawn = false;
136
- if (cache.latest && compareSemver(cache.latest, current) > 0) {
137
- notice = `当前 mocode v${current} 已是较旧版本,最新为 v${cache.latest},后台更新中,下次启动生效。`;
138
- spawn = now - (cache.lastSpawn ?? 0) > SPAWN_INTERVAL_MS;
139
- }
140
- return { notice, spawn, refresh };
141
- }
142
- /**
143
- * 启动时调(同步,零延迟):返一行纯文本更新提示或 null。
144
- * 流程:dev 跳过 → 读当前版本 → 读缓存 → evaluateUpdate 决策 → 按需后台刷新 / spawn → 返提示。
145
- * 提示由 repl 上色写入内容区开场段(进 INPUT 态前)。
146
- */
147
- export function checkAndMaybeUpdate() {
148
- if (isDevRun())
149
- return null;
150
- const current = readCurrentVersion();
151
- if (!current)
152
- return null;
153
- const cache = readCache();
154
- const now = Date.now();
155
- const d = evaluateUpdate(current, cache, now);
156
- if (d.refresh)
157
- void fetchLatestInBackground(); // fire-and-forget:刷新缓存供下次启动用
158
- if (d.spawn) {
159
- spawnUpdate();
160
- writeCache({ lastSpawn: now });
161
- }
162
- return d.notice;
163
- }