min-agent 0.3.0 → 0.4.1

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 (120) hide show
  1. package/README.md +154 -216
  2. package/dist/agent.js +1119 -256
  3. package/dist/cli/commands/chat.js +10 -0
  4. package/dist/cli/commands/exec.js +32 -0
  5. package/dist/cli/commands/history.js +58 -0
  6. package/dist/cli/commands/index.js +224 -0
  7. package/dist/cli/commands/init.js +18 -0
  8. package/dist/cli/commands/mcp.js +173 -0
  9. package/dist/cli/commands/memory.js +69 -0
  10. package/dist/cli/commands/models.js +21 -0
  11. package/dist/cli/commands/permission.js +12 -0
  12. package/dist/cli/commands/rules.js +33 -0
  13. package/dist/cli/commands/sandbox.js +13 -0
  14. package/dist/cli/commands/serve.js +9 -0
  15. package/dist/cli/commands/setup.js +4 -0
  16. package/dist/cli/commands/shared.js +16 -0
  17. package/dist/cli/commands/skills.js +119 -0
  18. package/dist/cli/commands/update.js +7 -0
  19. package/dist/cli/commands/write-config.js +30 -0
  20. package/dist/cli/errors.js +36 -0
  21. package/dist/cli/exec-prompt.js +26 -0
  22. package/dist/cli/option-helpers.js +53 -0
  23. package/dist/cli/program.js +180 -0
  24. package/dist/cli.js +5 -888
  25. package/dist/code-mode.js +32 -14
  26. package/dist/compaction.js +347 -160
  27. package/dist/config.js +119 -10
  28. package/dist/confirm.js +56 -9
  29. package/dist/context-window.js +107 -39
  30. package/dist/doom-loop.js +264 -29
  31. package/dist/fetch-timeout.js +152 -0
  32. package/dist/http-approvals.js +60 -0
  33. package/dist/instructions.js +21 -0
  34. package/dist/logger.js +33 -4
  35. package/dist/markdown.js +37 -11
  36. package/dist/mcp.js +328 -30
  37. package/dist/memory.js +97 -56
  38. package/dist/output.js +7 -5
  39. package/dist/permission-cli.js +43 -0
  40. package/dist/plugins.js +46 -8
  41. package/dist/pricing.js +4 -4
  42. package/dist/provider.js +23 -6
  43. package/dist/question-format.js +60 -0
  44. package/dist/sandbox-cli.js +82 -0
  45. package/dist/sandbox.js +403 -0
  46. package/dist/save-throttle.js +45 -0
  47. package/dist/serve/common.js +404 -0
  48. package/dist/serve/routes-chat.js +347 -0
  49. package/dist/serve/routes-mcp.js +212 -0
  50. package/dist/serve/routes-memory.js +66 -0
  51. package/dist/serve/routes-meta.js +205 -0
  52. package/dist/serve/routes-sessions.js +61 -0
  53. package/dist/serve/routes-skills.js +70 -0
  54. package/dist/serve.js +33 -883
  55. package/dist/sessions.js +53 -9
  56. package/dist/skills.js +82 -18
  57. package/dist/title-gen.js +8 -2
  58. package/dist/token-display.js +36 -0
  59. package/dist/tool-display.js +5 -0
  60. package/dist/tool-output.js +1 -3
  61. package/dist/tools/apply_patch.js +85 -11
  62. package/dist/tools/atomic-file.js +35 -0
  63. package/dist/tools/backend.js +2 -2
  64. package/dist/tools/bash.js +57 -19
  65. package/dist/tools/code_search.js +7 -1
  66. package/dist/tools/edit.js +11 -10
  67. package/dist/tools/explore.js +74 -14
  68. package/dist/tools/glob.js +4 -0
  69. package/dist/tools/grep.js +17 -10
  70. package/dist/tools/index.js +6 -21
  71. package/dist/tools/question.js +28 -9
  72. package/dist/tools/read.js +6 -4
  73. package/dist/tools/search-searxng.js +223 -0
  74. package/dist/tools/search-serper.js +189 -0
  75. package/dist/tools/task.js +84 -30
  76. package/dist/tools/todo.js +120 -19
  77. package/dist/tools/web_fetch.js +11 -3
  78. package/dist/tools/web_search.js +66 -556
  79. package/dist/tools/write.js +23 -6
  80. package/dist/tui/App.js +63 -14
  81. package/dist/tui/ConfirmBar.js +45 -13
  82. package/dist/tui/InputBar.js +150 -35
  83. package/dist/tui/MessageList.js +266 -125
  84. package/dist/tui/ModelPicker.js +8 -3
  85. package/dist/tui/QuestionBar.js +51 -19
  86. package/dist/tui/SessionPicker.js +79 -0
  87. package/dist/tui/StatusBar.js +8 -14
  88. package/dist/tui/agent-runner.js +142 -22
  89. package/dist/tui/caret-pos.js +48 -5
  90. package/dist/tui/caret.js +1 -1
  91. package/dist/tui/click-count.js +13 -0
  92. package/dist/tui/drag-state.js +8 -3
  93. package/dist/tui/hydrate.js +129 -0
  94. package/dist/tui/index.js +42 -13
  95. package/dist/tui/input-history.js +92 -11
  96. package/dist/tui/layout.js +75 -4
  97. package/dist/tui/prompt-queue.js +24 -0
  98. package/dist/tui/selection.js +113 -21
  99. package/dist/tui/session-switch.js +28 -0
  100. package/dist/tui/slash-commands.js +22 -6
  101. package/dist/tui/slash-handler.js +233 -58
  102. package/dist/tui/text-width.js +38 -16
  103. package/dist/tui/token-info.js +7 -0
  104. package/dist/tui/tool-children.js +19 -0
  105. package/dist/tui/undo-stack.js +1 -1
  106. package/dist/tui/use-sgr-mouse.js +3 -1
  107. package/dist/tui-chat.js +276 -40
  108. package/dist/updater.js +88 -29
  109. package/dist/xml-search.js +194 -0
  110. package/docs/API.md +257 -25
  111. package/docs/superpowers/plans/2026-08-20-tui-completeness.md +873 -0
  112. package/docs/superpowers/plans/2026-08-20-unified-tui-default.md +631 -0
  113. package/docs/superpowers/specs/2026-08-20-config-http-alignment-design.md +47 -0
  114. package/docs/superpowers/specs/2026-08-20-mcp-plugins-alignment-design.md +37 -0
  115. package/docs/superpowers/specs/2026-08-20-sandbox-permissions-design.md +68 -0
  116. package/docs/superpowers/specs/2026-08-20-tui-completeness-design.md +273 -0
  117. package/docs/superpowers/specs/2026-08-20-unified-tui-default-design.md +165 -0
  118. package/package.json +6 -1
  119. package/skills/self-config/SKILL.md +90 -0
  120. package/skills/self-config/reference.md +149 -0
package/dist/tui-chat.js CHANGED
@@ -4,16 +4,74 @@ import { discoverSkills, getAllSkills, getStaleDisabledSkills } from "./skills.j
4
4
  import { setSlashArgProvider } from "./tui/slash-commands.js";
5
5
  import { loadInstructions } from "./instructions.js";
6
6
  import { TokenTracker } from "./compaction.js";
7
+ import { emptyTaskState, copyTaskState, resetTaskState } from "./tools/todo.js";
7
8
  import { loadImageParts } from "./agent.js";
8
- import { createUndoStack } from "./tui/undo-stack.js";
9
+ import { createUndoStack, clearStack } from "./tui/undo-stack.js";
9
10
  import { killActiveProcesses } from "./tools/bash.js";
10
11
  import { setTuiConfirm, setTuiQuestion } from "./confirm.js";
11
12
  import { loadConfig, getActiveProvider } from "./config.js";
12
13
  import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
13
14
  import { handleSlashCommand, sysMsg } from "./tui/slash-handler.js";
14
15
  import { createAgentRunner } from "./tui/agent-runner.js";
16
+ import { listSessions, loadSession, saveSession } from "./sessions.js";
17
+ import { hydrateMessages, lastUserText } from "./tui/hydrate.js";
18
+ import { createPromptQueue } from "./tui/prompt-queue.js";
19
+ import { prepareSwitch, switchFailMessage, prepareNew, newFailMessage } from "./tui/session-switch.js";
15
20
  let confirmResolver = null;
16
21
  let questionResolver = null;
22
+ /** Only one confirm/question overlay can be shown at a time; parallel tool calls queue up. */
23
+ const promptQueue = createPromptQueue();
24
+ function registerSessionCompletion() {
25
+ const provider = () => listSessions().map((s) => ({ value: s.id, description: s.title }));
26
+ setSlashArgProvider("sessions", provider);
27
+ setSlashArgProvider("resume", provider);
28
+ }
29
+ function registerPermissionCompletion() {
30
+ setSlashArgProvider("permission", (tokens) => {
31
+ const flags = [
32
+ { value: "--project", description: "写入当前项目" },
33
+ { value: "--global", description: "写入全局配置" },
34
+ ];
35
+ const used = new Set(tokens);
36
+ const remainingFlags = flags.filter((f) => !used.has(f.value));
37
+ if (tokens.length === 0) {
38
+ return [
39
+ { value: "ask", description: "危险操作前询问(默认)" },
40
+ { value: "accept-edits", description: "自动允许改文件,危险命令仍询问" },
41
+ { value: "allow-all", description: "全部自动允许" },
42
+ ...remainingFlags,
43
+ ];
44
+ }
45
+ return remainingFlags;
46
+ });
47
+ }
48
+ function registerSandboxCompletion() {
49
+ setSlashArgProvider("sandbox", (tokens) => {
50
+ const flags = [
51
+ { value: "--project", description: "写入当前项目" },
52
+ { value: "--global", description: "写入全局配置" },
53
+ ];
54
+ const used = new Set(tokens);
55
+ const remainingFlags = flags.filter((f) => !used.has(f.value));
56
+ if (tokens.length === 0) {
57
+ return [
58
+ { value: "off", description: "不隔离(默认)" },
59
+ { value: "workspace", description: "只允许写入工作目录" },
60
+ { value: "strict", description: "读写都限制在工作目录" },
61
+ { value: "network", description: "设置是否允许联网" },
62
+ ...remainingFlags,
63
+ ];
64
+ }
65
+ if (tokens[0] === "network") {
66
+ const nets = [
67
+ { value: "allow", description: "允许联网" },
68
+ { value: "deny", description: "禁止联网" },
69
+ ];
70
+ return [...nets.filter((n) => !used.has(n.value)), ...remainingFlags];
71
+ }
72
+ return remainingFlags;
73
+ });
74
+ }
17
75
  function registerSkillCompletion() {
18
76
  setSlashArgProvider("skills", (tokens) => {
19
77
  const action = tokens[0];
@@ -31,53 +89,93 @@ function registerSkillCompletion() {
31
89
  return [];
32
90
  const all = getAllSkills();
33
91
  const pool = action === "enable"
34
- ? [...all.filter((s) => !s.enabled).map((s) => ({ value: s.name, description: s.description })), ...getStaleDisabledSkills().map((name) => ({ value: name, description: "(配置中的孤儿条目)" }))]
92
+ ? [
93
+ ...all.filter((s) => !s.enabled).map((s) => ({ value: s.name, description: s.description })),
94
+ ...getStaleDisabledSkills().map((name) => ({ value: name, description: "(配置中的孤儿条目)" })),
95
+ ]
35
96
  : all.filter((s) => s.enabled).map((s) => ({ value: s.name, description: s.description }));
36
- return [...pool.filter((o) => !tokens.includes(o.value)), ...(tokens.includes("--project") ? [] : [{ value: "--project", description: "仅对当前项目生效" }])];
97
+ return [
98
+ ...pool.filter((o) => !tokens.includes(o.value)),
99
+ ...(tokens.includes("--project") ? [] : [{ value: "--project", description: "仅对当前项目生效" }]),
100
+ ];
37
101
  });
38
102
  }
39
103
  export async function runTui(opts) {
40
- const { modelId, resumeSessionId, mode, images, providerName } = opts;
104
+ const { modelId, resumeSessionId, images, providerName, initialPrompt } = opts;
41
105
  await initMcp();
42
106
  discoverSkills();
43
107
  registerSkillCompletion();
108
+ registerPermissionCompletion();
109
+ registerSandboxCompletion();
110
+ registerSessionCompletion();
44
111
  const instructions = await loadInstructions();
45
112
  const config = loadConfig();
46
- let codeSystemPrompt;
47
- if (mode === "code") {
48
- const project = scanProject();
49
- codeSystemPrompt = buildCodeSystemPrompt(project, instructions);
50
- }
113
+ const project = scanProject();
114
+ let systemPrompt = buildCodeSystemPrompt(project, instructions);
51
115
  let messages = [];
52
116
  let sessionId = resumeSessionId;
53
117
  let currentModel = modelId ?? getActiveProvider(config)?.defaultModel;
54
118
  const currentProvider = providerName;
55
119
  const tracker = new TokenTracker();
120
+ const taskState = emptyTaskState();
56
121
  let undoStack = createUndoStack();
57
122
  let lastUserMessage = null;
58
123
  let planMode = false;
59
124
  if (resumeSessionId) {
60
- const { loadSession } = await import("./sessions.js");
61
125
  const session = loadSession(resumeSessionId);
62
126
  if (session) {
63
127
  messages = session.messages;
64
128
  sessionId = resumeSessionId;
129
+ copyTaskState(session.taskState, taskState);
65
130
  }
66
131
  }
67
132
  let runner = null;
68
133
  const tui = new TuiRenderer({
69
134
  onSubmit: async (text) => {
70
135
  if (text.startsWith("/")) {
71
- const result = await handleSlashCommand(text, tui, messages, currentModel, tracker, (model) => { currentModel = model; tui.setModel(model); }, requestExit, { undoStack, setUndoStack: (s) => { undoStack = s; }, lastUserMessage, setLastUserMessage: (t) => { lastUserMessage = t; }, sessionId, setSessionId: (id) => { sessionId = id; }, planMode, togglePlanMode: () => { planMode = !planMode; } });
72
- if (result === "run" && runner)
136
+ const result = await handleSlashCommand(text, tui, messages, currentModel, tracker, (model) => {
137
+ currentModel = model;
138
+ tui.setModel(model);
139
+ }, requestExit, {
140
+ undoStack,
141
+ setUndoStack: (s) => {
142
+ undoStack = s;
143
+ },
144
+ lastUserMessage,
145
+ setLastUserMessage: (t) => {
146
+ lastUserMessage = t;
147
+ },
148
+ sessionId,
149
+ setSessionId: (id) => {
150
+ sessionId = id;
151
+ },
152
+ planMode,
153
+ togglePlanMode: () => {
154
+ planMode = !planMode;
155
+ },
156
+ reloadSystemPrompt: async () => {
157
+ const next = await loadInstructions();
158
+ systemPrompt = buildCodeSystemPrompt(scanProject(), next);
159
+ sysMsg(tui, `✓ 已重载规则(${next.join("").length} 字符)`);
160
+ },
161
+ applySession,
162
+ startNewSession,
163
+ openSessionPicker,
164
+ taskState,
165
+ });
166
+ if (result === "run" && runner) {
73
167
  await runner.run();
168
+ persistSession();
169
+ }
74
170
  return;
75
171
  }
76
172
  lastUserMessage = text;
77
173
  tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: text, timestamp: Date.now() });
78
174
  messages.push({ role: "user", content: text });
79
- if (runner)
175
+ if (runner) {
80
176
  await runner.run();
177
+ persistSession();
178
+ }
81
179
  },
82
180
  onConfirm: (accepted) => {
83
181
  tui.hideConfirm();
@@ -105,17 +203,130 @@ export async function runTui(opts) {
105
203
  tui.setModel(model);
106
204
  sysMsg(tui, `✓ 默认模型已设为: ${model}`);
107
205
  },
108
- onModelCancel: () => { tui.hideModelPicker(); },
109
- onExit: () => { requestExit(); },
110
- }, modelId ?? getActiveProvider(config)?.defaultModel);
111
- runner = createAgentRunner({ tui, messages, tracker, getModel: () => currentModel, getProvider: () => currentProvider, getPlanMode: () => planMode, getUndoStack: () => undoStack, setUndoStack: (s) => { undoStack = s; }, mode, codeSystemPrompt });
112
- setTuiConfirm((message) => new Promise((resolve) => { confirmResolver = resolve; tui.showConfirm(message); }));
113
- setTuiQuestion((prompt, options) => new Promise((resolve) => { questionResolver = resolve; tui.showQuestion(prompt, options); }));
206
+ onModelCancel: () => {
207
+ tui.hideModelPicker();
208
+ },
209
+ onSessionPick: (id) => {
210
+ tui.hideSessionPicker();
211
+ applySession(id);
212
+ },
213
+ onSessionCancel: () => {
214
+ tui.hideSessionPicker();
215
+ },
216
+ onToggleSelectionMode: () => {
217
+ tui.toggleSelectionMode();
218
+ },
219
+ onExitSelectionMode: () => {
220
+ tui.setSelectionMode(false);
221
+ },
222
+ onExit: () => {
223
+ requestExit();
224
+ },
225
+ }, modelId ?? getActiveProvider(config)?.defaultModel, sessionId);
226
+ runner = createAgentRunner({
227
+ tui,
228
+ messages,
229
+ tracker,
230
+ getModel: () => currentModel,
231
+ getProvider: () => currentProvider,
232
+ getPlanMode: () => planMode,
233
+ getUndoStack: () => undoStack,
234
+ setUndoStack: (s) => {
235
+ undoStack = s;
236
+ },
237
+ getSystemPrompt: () => systemPrompt,
238
+ getTaskState: () => taskState,
239
+ persist: () => persistSession(),
240
+ });
241
+ function persistSession() {
242
+ if (messages.length === 0)
243
+ return;
244
+ try {
245
+ sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
246
+ }
247
+ catch { }
248
+ }
249
+ async function refreshTokenBar() {
250
+ const { snapshotTokenInfo } = await import("./tui/token-info.js");
251
+ tui.setTokenInfo(await snapshotTokenInfo(tracker, currentModel, messages));
252
+ }
253
+ function applySession(id) {
254
+ const ready = prepareSwitch({
255
+ running: Boolean(runner?.getController()),
256
+ currentId: sessionId,
257
+ targetId: id,
258
+ exists: Boolean(loadSession(id)),
259
+ });
260
+ if (!ready.ok) {
261
+ sysMsg(tui, switchFailMessage(ready.reason));
262
+ return;
263
+ }
264
+ if (messages.length > 0)
265
+ sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
266
+ const session = loadSession(id);
267
+ if (!session) {
268
+ sysMsg(tui, switchFailMessage("missing"));
269
+ return;
270
+ }
271
+ messages.length = 0;
272
+ messages.push(...session.messages);
273
+ copyTaskState(session.taskState, taskState);
274
+ sessionId = id;
275
+ undoStack = clearStack(undoStack);
276
+ tracker.reset();
277
+ lastUserMessage = lastUserText(session.messages);
278
+ tui.setSessionId(id);
279
+ tui.update({ messages: hydrateMessages(session.messages) });
280
+ void refreshTokenBar();
281
+ sysMsg(tui, `✓ 已切换到: ${session.meta.title}`);
282
+ }
283
+ function startNewSession() {
284
+ const ready = prepareNew({
285
+ running: Boolean(runner?.getController()),
286
+ sessionId,
287
+ messageCount: messages.length,
288
+ });
289
+ if (!ready.ok) {
290
+ sysMsg(tui, newFailMessage(ready.reason));
291
+ return;
292
+ }
293
+ if (messages.length > 0)
294
+ sessionId = saveSession(messages, sessionId, undefined, undefined, taskState);
295
+ messages.length = 0;
296
+ resetTaskState(taskState);
297
+ sessionId = undefined;
298
+ undoStack = clearStack(undoStack);
299
+ tracker.reset();
300
+ lastUserMessage = null;
301
+ tui.setSessionId(undefined);
302
+ tui.update({ messages: [], tokenInfo: undefined });
303
+ sysMsg(tui, "✓ 已新建会话");
304
+ }
305
+ function openSessionPicker() {
306
+ if (listSessions().length === 0) {
307
+ sysMsg(tui, "无已保存的会话");
308
+ return;
309
+ }
310
+ tui.showSessionPicker();
311
+ }
312
+ setTuiConfirm((message) => promptQueue.run(() => new Promise((resolve) => {
313
+ confirmResolver = resolve;
314
+ tui.showConfirm(message);
315
+ }), () => false));
316
+ setTuiQuestion((prompt, options) => promptQueue.run(() => new Promise((resolve) => {
317
+ questionResolver = resolve;
318
+ tui.showQuestion(prompt, options);
319
+ }), () => null));
114
320
  let exitResolve;
115
- const exitPromise = new Promise((resolve) => { exitResolve = resolve; });
321
+ const exitPromise = new Promise((resolve) => {
322
+ exitResolve = resolve;
323
+ });
116
324
  const requestExit = () => {
325
+ tui.setSelectionMode(false);
326
+ persistSession();
117
327
  if (runner?.getController()) {
118
328
  killActiveProcesses();
329
+ promptQueue.cancelAll();
119
330
  if (confirmResolver) {
120
331
  confirmResolver(false);
121
332
  confirmResolver = null;
@@ -134,31 +345,56 @@ export async function runTui(opts) {
134
345
  exitResolve?.();
135
346
  };
136
347
  tui.start();
348
+ if (messages.length > 0)
349
+ tui.update({ messages: hydrateMessages(messages) });
350
+ if (messages.length > 0)
351
+ await refreshTokenBar();
352
+ const prompt = initialPrompt?.trim() ?? "";
353
+ const notify = (kind, imgPath) => {
354
+ if (kind === "not_found")
355
+ sysMsg(tui, `⚠ 图片不存在: ${imgPath}`);
356
+ else if (kind === "too_large")
357
+ sysMsg(tui, `⚠ 图片超过 5 MB 限制: ${imgPath}`);
358
+ else
359
+ sysMsg(tui, `📎 ${imgPath}`);
360
+ };
137
361
  if (images && images.length > 0) {
138
- const notify = (kind, imgPath) => {
139
- if (kind === "not_found")
140
- sysMsg(tui, `⚠ 图片不存在: ${imgPath}`);
141
- else if (kind === "too_large")
142
- sysMsg(tui, `⚠ 图片超过 5 MB 限制: ${imgPath}`);
143
- else
144
- sysMsg(tui, `📎 ${imgPath}`);
145
- };
146
- const parts = [{ type: "text", text: "" }, ...loadImageParts(images, notify)];
147
- if (parts.length > 1) {
362
+ const parts = [{ type: "text", text: prompt }, ...loadImageParts(images, notify)];
363
+ if (parts.length > 1 || prompt) {
148
364
  messages.push({ role: "user", content: parts });
149
- tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: `[📎 图片] ${images.join(", ")}`, timestamp: Date.now() });
365
+ tui.addMessage({
366
+ id: `user-${Date.now()}`,
367
+ role: "user",
368
+ content: prompt ? prompt : `[📎 图片] ${images.join(", ")}`,
369
+ timestamp: Date.now(),
370
+ });
371
+ if (prompt)
372
+ lastUserMessage = prompt;
150
373
  await runner.run();
374
+ persistSession();
151
375
  }
152
376
  }
153
- process.on("SIGINT", requestExit);
377
+ else if (prompt) {
378
+ lastUserMessage = prompt;
379
+ tui.addMessage({ id: `user-${Date.now()}`, role: "user", content: prompt, timestamp: Date.now() });
380
+ messages.push({ role: "user", content: prompt });
381
+ await runner.run();
382
+ persistSession();
383
+ }
384
+ const onSigint = () => {
385
+ if (runner?.getController())
386
+ requestExit();
387
+ };
388
+ process.on("SIGINT", onSigint);
154
389
  await exitPromise;
155
- process.off("SIGINT", requestExit);
156
- if (messages.length > 0) {
157
- const { saveSession } = await import("./sessions.js");
158
- sessionId = saveSession(messages, sessionId);
390
+ process.off("SIGINT", onSigint);
391
+ try {
392
+ persistSession();
393
+ }
394
+ finally {
395
+ setTuiConfirm(null);
396
+ setTuiQuestion(null);
397
+ tui.destroy();
398
+ await shutdownMcp();
159
399
  }
160
- setTuiConfirm(null);
161
- setTuiQuestion(null);
162
- tui.destroy();
163
- await shutdownMcp();
164
400
  }
package/dist/updater.js CHANGED
@@ -1,11 +1,25 @@
1
1
  import { readFileSync } from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { execSync } from "child_process";
4
+ import { spawnSync } from "child_process";
5
+ export const UPDATE_PACKAGE = "min-agent";
6
+ export const NPM_LATEST_URL = `https://registry.npmjs.org/${UPDATE_PACKAGE}/latest`;
7
+ export const UPDATE_NPM_BIN = "npm";
8
+ export const UPDATE_NPM_ARGS = ["install", "-g", UPDATE_PACKAGE];
9
+ const VERSION_RE = /^\d+\.\d+\.\d+/;
10
+ const FETCH_TIMEOUT_MS = 10_000;
11
+ export function updateInstallCommand() {
12
+ return [UPDATE_NPM_BIN, ...UPDATE_NPM_ARGS].join(" ");
13
+ }
5
14
  export function localVersion() {
6
- const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
7
- const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
8
- return pkg.version ?? "0.0.0";
15
+ try {
16
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
17
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
18
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "0.0.0";
19
+ }
20
+ catch {
21
+ return "0.0.0";
22
+ }
9
23
  }
10
24
  export function compareVersions(a, b) {
11
25
  const pa = a.split("-")[0].split(".").map(Number);
@@ -13,45 +27,90 @@ export function compareVersions(a, b) {
13
27
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
14
28
  const va = pa[i] ?? 0;
15
29
  const vb = pb[i] ?? 0;
16
- if (va !== vb)
17
- return va - vb;
30
+ const na = Number.isFinite(va) ? va : 0;
31
+ const nb = Number.isFinite(vb) ? vb : 0;
32
+ if (na !== nb)
33
+ return na - nb;
18
34
  }
19
35
  return 0;
20
36
  }
21
- export async function fetchLatestVersion() {
37
+ function parseVersion(value) {
38
+ if (typeof value !== "string" || !VERSION_RE.test(value))
39
+ return null;
40
+ return value;
41
+ }
42
+ export async function fetchLatestVersion(fetchFn = fetch) {
43
+ let response;
22
44
  try {
23
- const response = await fetch("https://registry.npmjs.org/min-agent/latest", {
24
- signal: AbortSignal.timeout(10000),
45
+ response = await fetchFn(NPM_LATEST_URL, {
46
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
25
47
  headers: { Accept: "application/json" },
26
48
  });
27
- if (!response.ok)
28
- return null;
29
- const data = (await response.json());
30
- return data.version ?? null;
31
49
  }
32
50
  catch {
33
51
  return null;
34
52
  }
53
+ if (!response.ok)
54
+ return null;
55
+ let data;
56
+ try {
57
+ data = await response.json();
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ if (!data || typeof data !== "object" || !("version" in data))
63
+ return null;
64
+ return parseVersion(data.version);
65
+ }
66
+ export async function checkForUpdate(opts) {
67
+ const current = opts?.currentVersion ?? localVersion();
68
+ const latest = await fetchLatestVersion(opts?.fetchFn);
69
+ return {
70
+ current,
71
+ latest,
72
+ updateAvailable: latest !== null && compareVersions(current, latest) < 0,
73
+ };
74
+ }
75
+ export function installLatest() {
76
+ const result = spawnSync(UPDATE_NPM_BIN, [...UPDATE_NPM_ARGS], {
77
+ stdio: "inherit",
78
+ shell: process.platform === "win32",
79
+ env: process.env,
80
+ });
81
+ if (result.error) {
82
+ const code = result.error.code;
83
+ return { ok: false, missing: code === "ENOENT" };
84
+ }
85
+ return { ok: result.status === 0 };
35
86
  }
36
- export async function runUpdate() {
37
- const current = localVersion();
38
- console.log(`当前版本: ${current}`);
39
- const latest = await fetchLatestVersion();
87
+ export async function runUpdate(io = {}) {
88
+ const log = io.log ?? ((msg) => console.log(msg));
89
+ const error = io.error ?? ((msg) => console.error(msg));
90
+ const current = io.currentVersion?.() ?? localVersion();
91
+ const cmd = updateInstallCommand();
92
+ log(`当前版本: ${current}`);
93
+ const latest = io.fetchLatest ? await io.fetchLatest() : await fetchLatestVersion();
40
94
  if (!latest) {
41
- console.error("无法检查新版本(网络不可用)。");
42
- return;
95
+ error(`无法检查新版本(网络不可用)。请稍后重试,或手动执行: ${cmd}`);
96
+ return { status: "check-failed", current, latest: null };
43
97
  }
44
98
  if (compareVersions(current, latest) >= 0) {
45
- console.log(`已是最新版本 (${latest})`);
46
- return;
99
+ log(`已是最新版本 (${latest})`);
100
+ return { status: "up-to-date", current, latest };
47
101
  }
48
- console.log(`发现新版本: ${latest}`);
49
- console.log("正在升级 min-agent...");
50
- try {
51
- execSync("npm install -g min-agent@latest", { stdio: "inherit" });
52
- console.log(`✓ 已升级到 ${latest},请重启终端后生效`);
53
- }
54
- catch {
55
- console.error("自动升级失败。请手动执行: npm install -g min-agent@latest");
102
+ log(`发现新版本: ${latest}`);
103
+ log(`正在执行 ${cmd} ...`);
104
+ const installed = (io.install ?? installLatest)();
105
+ if (!installed.ok) {
106
+ if (installed.missing) {
107
+ error(`未找到 npm。请先安装 Node.js,然后执行: ${cmd}`);
108
+ }
109
+ else {
110
+ error(`自动升级失败。请手动执行: ${cmd}`);
111
+ }
112
+ return { status: "install-failed", current, latest };
56
113
  }
114
+ log(`✓ 已升级到 ${latest}`);
115
+ return { status: "updated", current, latest };
57
116
  }