@bolloon/bolloon-agent 0.1.13 → 0.1.14
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/dist/agents/pi-sdk.js +185 -0
- package/dist/agents/shell-guard.js +354 -0
- package/dist/agents/shell-tool.js +83 -0
- package/dist/agents/skill-loader.js +174 -0
- package/dist/bollharness-integration/context-chain-router.js +3 -3
- package/dist/bollharness-integration/context-router.js +1 -1
- package/dist/heartbeat/Watchdog.js +7 -5
- package/dist/heartbeat/index.js +1 -0
- package/dist/heartbeat/self-improve-bus.js +85 -0
- package/dist/pi-ecosystem-judgment/index.js +1 -2
- package/dist/utils/auto-update.js +44 -12
- package/dist/web/client.js +839 -103
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +88 -8
- package/dist/web/server.js +2360 -0
- package/dist/web/style.css +506 -9
- package/package.json +2 -2
- package/scripts/build-cli.js +11 -1
- package/src/agents/pi-sdk.ts +196 -0
- package/src/agents/shell-guard.ts +417 -0
- package/src/agents/shell-tool.ts +103 -0
- package/src/agents/skill-loader.ts +202 -0
- package/src/bollharness-integration/context-chain-router.ts +3 -3
- package/src/bollharness-integration/context-router.ts +1 -1
- package/src/heartbeat/Watchdog.ts +7 -5
- package/src/heartbeat/index.ts +1 -0
- package/src/heartbeat/self-improve-bus.ts +110 -0
- package/src/types.d.ts +12 -0
- package/src/utils/auto-update.ts +45 -14
- package/src/web/client.js +839 -103
- package/src/web/index.html +88 -8
- package/src/web/server.ts +427 -101
- package/src/web/style.css +506 -9
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
- package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
- package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
- package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
- package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
- package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
- package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
- package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
- package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
- package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
- package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
- package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
- package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-loader.ts — 双 frontmatter 兼容的 SKILL.md 加载器
|
|
3
|
+
*
|
|
4
|
+
* 兼容两套 SKILL.md frontmatter:
|
|
5
|
+
* A. Anthropic Agent Skills 标准 (2025-12): name / description / license / compatibility / keywords
|
|
6
|
+
* B. bollharness 现有 frontmatter: name / description / status / tier / triggers / outputs / truth_policy
|
|
7
|
+
*
|
|
8
|
+
* 字段映射规则(统一到内部 SkillMeta):
|
|
9
|
+
* description ← 直接取
|
|
10
|
+
* license ← 取 A,没有则空
|
|
11
|
+
* status ← 取 B,没有则默认 'active'
|
|
12
|
+
* tier ← 取 B("tier" 是 bollharness 概念)
|
|
13
|
+
* triggers / keywords ← 合并 A.keywords 和 B.triggers 数组
|
|
14
|
+
* body ← 去掉 frontmatter 后的 Markdown 正文
|
|
15
|
+
*
|
|
16
|
+
* Skill 的 execute() 把 body 作为 Markdown 文档注入到 LLM context。
|
|
17
|
+
* 这是 Skills 协议的核心 — "告诉 agent 怎么做",与 MCP "能调什么"互补。
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs/promises';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
/** YAML frontmatter 最小解析器 — 避免引入额外依赖, 支持双格式 */
|
|
23
|
+
function parseFrontmatter(raw) {
|
|
24
|
+
// frontmatter 必须以 --- 开头, 紧跟换行, 再以 --- 闭合
|
|
25
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
26
|
+
if (!match) {
|
|
27
|
+
return { frontmatter: {}, body: raw };
|
|
28
|
+
}
|
|
29
|
+
const [, yamlBlock, body] = match;
|
|
30
|
+
const frontmatter = {};
|
|
31
|
+
const lines = yamlBlock.split(/\r?\n/);
|
|
32
|
+
let currentKey = null;
|
|
33
|
+
let currentArray = null;
|
|
34
|
+
for (const rawLine of lines) {
|
|
35
|
+
const line = rawLine.replace(/\s+$/, '');
|
|
36
|
+
if (!line.trim())
|
|
37
|
+
continue;
|
|
38
|
+
// 数组项: " - value"
|
|
39
|
+
const arrItem = line.match(/^\s+-\s+(.*)$/);
|
|
40
|
+
if (arrItem && currentKey && currentArray) {
|
|
41
|
+
currentArray.push(stripQuotes(arrItem[1]));
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
// 键值对: "key: value" 或 "key:"
|
|
45
|
+
const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
46
|
+
if (kv) {
|
|
47
|
+
const [, key, value] = kv;
|
|
48
|
+
if (value === '') {
|
|
49
|
+
// 可能是数组开始 (下一行 " - xxx")
|
|
50
|
+
currentKey = key;
|
|
51
|
+
currentArray = [];
|
|
52
|
+
frontmatter[key] = currentArray;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
currentKey = key;
|
|
56
|
+
currentArray = null;
|
|
57
|
+
frontmatter[key] = stripQuotes(value);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { frontmatter, body: body.replace(/^\r?\n/, '') };
|
|
62
|
+
}
|
|
63
|
+
function stripQuotes(s) {
|
|
64
|
+
const t = s.trim();
|
|
65
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
66
|
+
return t.slice(1, -1);
|
|
67
|
+
}
|
|
68
|
+
return t;
|
|
69
|
+
}
|
|
70
|
+
/** 把 frontmatter 统一到 SkillMeta */
|
|
71
|
+
function normalize(name, sourcePath, raw) {
|
|
72
|
+
const { frontmatter, body } = parseFrontmatter(raw);
|
|
73
|
+
// name 至少要有一个来源: frontmatter.name 或目录名
|
|
74
|
+
const fmName = typeof frontmatter.name === 'string' ? frontmatter.name : name;
|
|
75
|
+
const finalName = fmName || name;
|
|
76
|
+
if (!finalName)
|
|
77
|
+
return null;
|
|
78
|
+
const description = typeof frontmatter.description === 'string'
|
|
79
|
+
? frontmatter.description
|
|
80
|
+
: '';
|
|
81
|
+
const statusRaw = typeof frontmatter.status === 'string' ? frontmatter.status.toLowerCase() : 'active';
|
|
82
|
+
const status = statusRaw === 'archived' || statusRaw === 'draft' ? statusRaw : 'active';
|
|
83
|
+
const tier = typeof frontmatter.tier === 'string' ? frontmatter.tier : 'utility';
|
|
84
|
+
// 合并两套触发字段
|
|
85
|
+
const triggers = [];
|
|
86
|
+
if (Array.isArray(frontmatter.triggers)) {
|
|
87
|
+
for (const t of frontmatter.triggers)
|
|
88
|
+
if (typeof t === 'string')
|
|
89
|
+
triggers.push(t);
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(frontmatter.keywords)) {
|
|
92
|
+
for (const k of frontmatter.keywords)
|
|
93
|
+
if (typeof k === 'string')
|
|
94
|
+
triggers.push(k);
|
|
95
|
+
}
|
|
96
|
+
return { name: finalName, sourcePath, body, frontmatter, description, status, tier, triggers };
|
|
97
|
+
}
|
|
98
|
+
/** 解析单个 SKILL.md 文件 */
|
|
99
|
+
export async function parseSkillFile(filePath) {
|
|
100
|
+
let raw;
|
|
101
|
+
try {
|
|
102
|
+
raw = await fs.readFile(filePath, 'utf-8');
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
// 从路径推目录名作为 name 兜底
|
|
108
|
+
const dirName = path.basename(path.dirname(filePath));
|
|
109
|
+
return normalize(dirName, filePath, raw);
|
|
110
|
+
}
|
|
111
|
+
/** 扫描一个目录, 找所有 {name}/SKILL.md (一层嵌套结构) */
|
|
112
|
+
export async function loadSkillsDir(dir) {
|
|
113
|
+
const out = [];
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.isDirectory())
|
|
123
|
+
continue;
|
|
124
|
+
if (entry.name.startsWith('.'))
|
|
125
|
+
continue;
|
|
126
|
+
const skillFile = path.join(dir, entry.name, 'SKILL.md');
|
|
127
|
+
const meta = await parseSkillFile(skillFile);
|
|
128
|
+
if (meta)
|
|
129
|
+
out.push(meta);
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
/** 默认 skill 路径优先级 (后者覆盖前者同名 skill) */
|
|
134
|
+
export function defaultSkillPaths(home = os.homedir(), cwd = process.cwd()) {
|
|
135
|
+
return [
|
|
136
|
+
path.join(home, '.bolloon', 'skills'), // 全局用户级
|
|
137
|
+
path.join(cwd, '.bolloon', 'skills'), // 项目级
|
|
138
|
+
path.join(home, '.boll', 'skills'), // 全局 (兼容 bollharness 旧用户)
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/** 把 SkillMeta 包成 @bolloon/constraint-runtime 期望的 Skill 对象 */
|
|
142
|
+
export function skillFromMeta(meta) {
|
|
143
|
+
return {
|
|
144
|
+
name: meta.name,
|
|
145
|
+
description: meta.description || meta.tier,
|
|
146
|
+
execute: async (_params) => {
|
|
147
|
+
// Skills 协议: 把 body 当 Markdown 文档返回, 由调用方注入 LLM context
|
|
148
|
+
// 调用方 (use_skill tool) 拿到后会把 body 放到 tool result,
|
|
149
|
+
// LLM 下一轮对话看到这份指南, 按它执行
|
|
150
|
+
const header = `## Skill: ${meta.name}\n\n${meta.description ? `> ${meta.description}\n\n` : ''}`;
|
|
151
|
+
const triggersBlock = meta.triggers.length
|
|
152
|
+
? `**触发条件**: ${meta.triggers.join('; ')}\n\n`
|
|
153
|
+
: '';
|
|
154
|
+
return `${header}${triggersBlock}${meta.body}`;
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** 加载多个目录, 同名 skill 后者覆盖前者 */
|
|
159
|
+
export async function loadSkillsFromPaths(paths) {
|
|
160
|
+
const seen = new Map();
|
|
161
|
+
for (const p of paths) {
|
|
162
|
+
const metas = await loadSkillsDir(p);
|
|
163
|
+
for (const m of metas) {
|
|
164
|
+
if (m.status === 'archived')
|
|
165
|
+
continue; // 归档的跳过
|
|
166
|
+
seen.set(m.name, skillFromMeta(m));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Array.from(seen.values());
|
|
170
|
+
}
|
|
171
|
+
/** 列出已加载的 skills (调试/UI 用) */
|
|
172
|
+
export function describeSkill(s) {
|
|
173
|
+
return `${s.name}: ${s.description}`;
|
|
174
|
+
}
|
|
@@ -4,18 +4,18 @@
|
|
|
4
4
|
* Integrates with existing ContextRouter and Judgment systems.
|
|
5
5
|
*
|
|
6
6
|
* Architecture:
|
|
7
|
-
* - Session end → extract summary by work_type → store in .
|
|
7
|
+
* - Session end → extract summary by work_type → store in .bolloon/state/context-chains/
|
|
8
8
|
* - Session start (Gate 0/3) → lookup related chains → inject summaries
|
|
9
9
|
* - Work type: code_change | review | design | question | planning | debugging
|
|
10
10
|
*
|
|
11
11
|
* Integration points:
|
|
12
12
|
* - Uses existing context-router-judgment.ts pattern (extends, not replaces)
|
|
13
13
|
* - Gate injection via gate-judgment-inject.ts
|
|
14
|
-
* - Storage in .
|
|
14
|
+
* - Storage in .bolloon/state/context-chains/
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from 'fs';
|
|
17
17
|
import * as path from 'path';
|
|
18
|
-
export const CONTEXT_CHAINS_DIR = path.join('.
|
|
18
|
+
export const CONTEXT_CHAINS_DIR = path.join('.bolloon', 'state', 'context-chains');
|
|
19
19
|
/**
|
|
20
20
|
* Ensure context-chains directory exists
|
|
21
21
|
*/
|
|
@@ -105,7 +105,7 @@ export class ContextRouter {
|
|
|
105
105
|
injectedTTL = 3600; // 1 hour
|
|
106
106
|
constructor(fragmentsDir) {
|
|
107
107
|
this.fragmentsDir = fragmentsDir || FRAGMENTS_DIR;
|
|
108
|
-
this.injectedFile = path.join('.
|
|
108
|
+
this.injectedFile = path.join('.bolloon', 'guard', 'injected.json');
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
111
|
* Get fragments for a file path
|
|
@@ -105,12 +105,14 @@ export class Watchdog {
|
|
|
105
105
|
this.triggerRestart(1, `No activity for ${Math.round(silentTime / 1000)}s`);
|
|
106
106
|
return;
|
|
107
107
|
}
|
|
108
|
-
//
|
|
108
|
+
// 检查内存使用: 用绝对阈值, 不看 heapUsed/heapTotal 比例 (V8 内部比例不可靠, 经常 80-95% 误报)
|
|
109
109
|
const usage = process.memoryUsage();
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const heapUsedMB = usage.heapUsed / 1024 / 1024;
|
|
111
|
+
const rssMB = usage.rss / 1024 / 1024;
|
|
112
|
+
// 1.2GB heap 或 1.5GB RSS 才是真危险
|
|
113
|
+
if (heapUsedMB > 1224 || rssMB > 1536) {
|
|
114
|
+
console.warn(`[Watchdog] Memory usage critical: heapUsed=${heapUsedMB.toFixed(0)}MB, rss=${rssMB.toFixed(0)}MB`);
|
|
115
|
+
this.triggerRestart(1, `Memory usage heap=${heapUsedMB.toFixed(0)}MB rss=${rssMB.toFixed(0)}MB`);
|
|
114
116
|
}
|
|
115
117
|
}
|
|
116
118
|
/**
|
package/dist/heartbeat/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export * from './HealthMonitor.js';
|
|
|
7
7
|
export * from './Watchdog.js';
|
|
8
8
|
export * from './DaemonManager.js';
|
|
9
9
|
export * from './StartupVerifier.js';
|
|
10
|
+
export * from './self-improve-bus.js';
|
|
10
11
|
import { createHealthMonitor, getHealthMonitor } from './HealthMonitor.js';
|
|
11
12
|
import { createWatchdog, getWatchdog } from './Watchdog.js';
|
|
12
13
|
import { createDaemonManager, getDaemonManager } from './DaemonManager.js';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-Improve Event Bus
|
|
3
|
+
*
|
|
4
|
+
* 心跳事件 → 自改触发器 (解耦 watchdog)
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:
|
|
7
|
+
* - Watchdog 负责保活 (重启进程), 不知道"自改"是什么
|
|
8
|
+
* - Self-Improve Bus 监听"信号事件" (CI 失败, 任务连续失败, 静默超时)
|
|
9
|
+
* - 信号达到阈值 + 通过冷却期 → 触发 runSelfImproveLoop
|
|
10
|
+
* - 触发时通过 SSE 广播给前端, 用户能在 UI 里看到
|
|
11
|
+
*
|
|
12
|
+
* 关键不变量:
|
|
13
|
+
* 1. 心跳**不**直接调自改 - 通过 emit() 异步触发
|
|
14
|
+
* 2. 触发频率受 SELF_IMPROVE_COOLDOWN_MS 限制
|
|
15
|
+
* 3. 同类事件 24 小时内只触发 1 次
|
|
16
|
+
* 4. 触发后不阻塞健康检查
|
|
17
|
+
*/
|
|
18
|
+
import { SELF_IMPROVE_COOLDOWN_MS } from '../agents/shell-guard.js';
|
|
19
|
+
const EVENT_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 小时内同类型只触发 1 次
|
|
20
|
+
const eventHistory = new Map();
|
|
21
|
+
let lastTriggerAt = null;
|
|
22
|
+
const listeners = new Set();
|
|
23
|
+
/**
|
|
24
|
+
* 订阅自改触发事件
|
|
25
|
+
*/
|
|
26
|
+
export function onSelfImproveTrigger(fn) {
|
|
27
|
+
listeners.add(fn);
|
|
28
|
+
return () => { listeners.delete(fn); };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 心跳事件 → 自改总线
|
|
32
|
+
*
|
|
33
|
+
* @returns { triggered: boolean, reason?: string }
|
|
34
|
+
*/
|
|
35
|
+
export function reportSelfImproveEvent(event) {
|
|
36
|
+
// 1. 24 小时同类事件冷却
|
|
37
|
+
const prev = eventHistory.get(event.kind);
|
|
38
|
+
if (prev && Date.now() - prev.at < EVENT_COOLDOWN_MS) {
|
|
39
|
+
return { triggered: false, reason: `同类事件 ${event.kind} 在 24h 内已记录过, 跳过` };
|
|
40
|
+
}
|
|
41
|
+
// 2. 累加计数
|
|
42
|
+
eventHistory.set(event.kind, {
|
|
43
|
+
at: Date.now(),
|
|
44
|
+
count: (prev?.count || 0) + 1
|
|
45
|
+
});
|
|
46
|
+
// 3. 自改循环冷却
|
|
47
|
+
if (lastTriggerAt && Date.now() - lastTriggerAt < SELF_IMPROVE_COOLDOWN_MS) {
|
|
48
|
+
const waitHrs = Math.ceil((SELF_IMPROVE_COOLDOWN_MS - (Date.now() - lastTriggerAt)) / 3600000);
|
|
49
|
+
return { triggered: false, reason: `自改冷却中, 还需要约 ${waitHrs} 小时` };
|
|
50
|
+
}
|
|
51
|
+
// 4. 触发
|
|
52
|
+
lastTriggerAt = Date.now();
|
|
53
|
+
const goal = `信号事件: ${event.kind} - ${event.details}`;
|
|
54
|
+
console.log(`[self-improve-bus] 🚀 触发自改循环: ${goal}`);
|
|
55
|
+
// 异步触发所有 listener, 不阻塞调用方
|
|
56
|
+
Promise.resolve().then(async () => {
|
|
57
|
+
for (const listener of listeners) {
|
|
58
|
+
try {
|
|
59
|
+
await listener(event, goal);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
console.error(`[self-improve-bus] listener 失败:`, err);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
return { triggered: true };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* 获取当前事件历史 (供调试 / UI 显示)
|
|
70
|
+
*/
|
|
71
|
+
export function getEventHistory() {
|
|
72
|
+
return Array.from(eventHistory.entries()).map(([kind, rec]) => ({
|
|
73
|
+
kind,
|
|
74
|
+
at: new Date(rec.at).toISOString(),
|
|
75
|
+
count: rec.count
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* 强制重置 (仅供调试)
|
|
80
|
+
*/
|
|
81
|
+
export function resetSelfImproveBus() {
|
|
82
|
+
eventHistory.clear();
|
|
83
|
+
lastTriggerAt = null;
|
|
84
|
+
console.log('[self-improve-bus] 已重置');
|
|
85
|
+
}
|
|
@@ -16,8 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as fs from 'fs/promises';
|
|
18
18
|
import * as path from 'path';
|
|
19
|
-
|
|
20
|
-
const yaml = require('js-yaml');
|
|
19
|
+
import yaml from 'js-yaml';
|
|
21
20
|
const JUDGMENTS_DIR = path.join(process.env.HOME || '/tmp', '.bolloon', 'judgments');
|
|
22
21
|
const JUDGMENT_FILES = {
|
|
23
22
|
rule: path.join(JUDGMENTS_DIR, 'rules.yaml'),
|
|
@@ -241,24 +241,25 @@ function checkNpmOutdated() {
|
|
|
241
241
|
* 自动更新 npm 包
|
|
242
242
|
*/
|
|
243
243
|
async function updatePackages(packages) {
|
|
244
|
+
// 记录更新前的版本,用于事后判断"是否真的升级了"
|
|
245
|
+
// 与 getInstalledVersion 的"优先读全局"保持一致 —— install 也用 -g,
|
|
246
|
+
// 否则判断和执行落在不同的目录,永远改不到那个被读取的版本号。
|
|
247
|
+
const targets = packages && packages.length > 0 ? packages : ['@bolloon/bolloon-agent'];
|
|
248
|
+
const before = new Map();
|
|
249
|
+
for (const p of targets)
|
|
250
|
+
before.set(p, getInstalledVersion(p));
|
|
251
|
+
const isGlobal = !packages || packages.length === 0;
|
|
252
|
+
const args = isGlobal
|
|
253
|
+
? ['npm', 'install', '-g', ...targets]
|
|
254
|
+
: ['npm', 'install', ...targets, '--save'];
|
|
255
|
+
log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
|
|
244
256
|
try {
|
|
245
|
-
|
|
246
|
-
? ['npm', 'install', ...packages, '--save']
|
|
247
|
-
: ['npm', 'install', '-g', '@bolloon/bolloon-agent'];
|
|
248
|
-
log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
|
|
249
|
-
// 执行 npm install
|
|
250
|
-
const result = execSync(args.join(' '), {
|
|
257
|
+
execSync(args.join(' '), {
|
|
251
258
|
encoding: 'utf-8',
|
|
252
259
|
timeout: 300000, // 5分钟超时
|
|
253
260
|
stdio: 'inherit',
|
|
254
261
|
cwd: process.cwd()
|
|
255
262
|
});
|
|
256
|
-
return {
|
|
257
|
-
success: true,
|
|
258
|
-
updated: true,
|
|
259
|
-
message: '更新成功',
|
|
260
|
-
updatedPackages: packages
|
|
261
|
-
};
|
|
262
263
|
}
|
|
263
264
|
catch (e) {
|
|
264
265
|
return {
|
|
@@ -268,6 +269,37 @@ async function updatePackages(packages) {
|
|
|
268
269
|
error: e.message
|
|
269
270
|
};
|
|
270
271
|
}
|
|
272
|
+
// install 退出码 0 并不等于"真的升上去了"("up to date" 也是 0)。
|
|
273
|
+
// 重新读取磁盘版本,只有真的达到目标 latest 之一才算 updated。
|
|
274
|
+
const upgraded = [];
|
|
275
|
+
const failed = [];
|
|
276
|
+
for (const p of targets) {
|
|
277
|
+
const after = getInstalledVersion(p);
|
|
278
|
+
const was = before.get(p);
|
|
279
|
+
if (after && was && compareVersions(was, after) < 0) {
|
|
280
|
+
upgraded.push(p);
|
|
281
|
+
}
|
|
282
|
+
else if (after && was && compareVersions(was, after) === 0) {
|
|
283
|
+
// 版本没变 —— install 跑过但没改动;不当作"刚升级"
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
failed.push(p);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (upgraded.length > 0) {
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
updated: true,
|
|
293
|
+
message: `已更新: ${upgraded.join(', ')}`,
|
|
294
|
+
updatedPackages: upgraded
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
success: true,
|
|
299
|
+
updated: false,
|
|
300
|
+
message: '已是最新版本,无需重启',
|
|
301
|
+
updatedPackages: []
|
|
302
|
+
};
|
|
271
303
|
}
|
|
272
304
|
/**
|
|
273
305
|
* 检查并自动更新(启动时调用)
|