c0de-agent 1.10.0 → 1.11.0
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/README.md +5 -2
- package/dist/cli/commands/acp.js +3 -3
- package/dist/cli/commands/auth.d.ts +17 -0
- package/dist/cli/commands/auth.js +73 -0
- package/dist/cli/commands/chat.js +8 -0
- package/dist/cli/commands/config.d.ts +2 -9
- package/dist/cli/commands/config.js +9 -52
- package/dist/cli/commands/init.js +4 -2
- package/dist/cli/commands/plugin.js +8 -4
- package/dist/cli/commands/sessions.d.ts +10 -0
- package/dist/cli/commands/sessions.js +35 -0
- package/dist/cli/commands/update.js +18 -10
- package/dist/cli/index.js +30 -2
- package/dist/cli/modes/print.js +11 -1
- package/dist/core/config-path.d.ts +9 -0
- package/dist/core/config-path.js +48 -0
- package/dist/core/config.d.ts +10 -2
- package/dist/core/config.js +32 -10
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/loop.js +13 -3
- package/dist/core/slash.js +92 -30
- package/dist/core/title.js +15 -0
- package/dist/core/types.d.ts +2 -0
- package/dist/db/client.d.ts +4 -0
- package/dist/db/client.js +5 -0
- package/dist/db/schema.d.ts +34 -0
- package/dist/db/schema.js +4 -0
- package/dist/server/agent-manager.d.ts +5 -0
- package/dist/server/agent-manager.js +28 -0
- package/dist/server/app.js +12 -2
- package/dist/server/auth-manager.d.ts +79 -0
- package/dist/server/auth-manager.js +270 -0
- package/dist/server/context.js +1 -0
- package/dist/server/middleware/auth.d.ts +4 -3
- package/dist/server/middleware/auth.js +5 -5
- package/dist/server/permission/interactive.d.ts +2 -0
- package/dist/server/permission/interactive.js +5 -1
- package/dist/server/permission/store.d.ts +4 -0
- package/dist/server/permission/store.js +1 -0
- package/dist/server/routes/auth.d.ts +4 -0
- package/dist/server/routes/auth.js +94 -0
- package/dist/server/routes/chat.d.ts +6 -1
- package/dist/server/routes/chat.js +87 -7
- package/dist/server/routes/config.js +59 -11
- package/dist/server/routes/permissions.d.ts +4 -1
- package/dist/server/routes/permissions.js +27 -4
- package/dist/server/routes/project.js +56 -2
- package/dist/server/routes/session.js +31 -7
- package/dist/server/routes/terminal.js +17 -2
- package/dist/server/routes/update.js +23 -1
- package/dist/server/server.js +47 -8
- package/dist/server/types.d.ts +8 -1
- package/dist/session/archive.d.ts +1 -1
- package/dist/session/branch.d.ts +1 -0
- package/dist/session/branch.js +12 -5
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.js +1 -1
- package/dist/session/session.d.ts +25 -5
- package/dist/session/session.js +129 -10
- package/dist/session/types.d.ts +1 -1
- package/dist/shared/types/agent.d.ts +11 -0
- package/dist/shared/types/config.d.ts +5 -0
- package/dist/shared/types/message.d.ts +6 -2
- package/dist/tools/index.js +2 -3
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/ipc.d.ts +6 -0
- package/dist/update/ipc.js +7 -4
- package/dist/update/snapshot.d.ts +4 -0
- package/dist/update/snapshot.js +8 -0
- package/dist/update/updater.d.ts +55 -8
- package/dist/update/updater.js +230 -27
- package/drizzle/0006_sour_susan_delgado.sql +2 -0
- package/drizzle/0007_same_wilson_fisk.sql +2 -0
- package/drizzle/meta/0006_snapshot.json +867 -0
- package/drizzle/meta/0007_snapshot.json +869 -0
- package/drizzle/meta/_journal.json +14 -0
- package/package.json +1 -1
package/dist/update/updater.js
CHANGED
|
@@ -1,21 +1,155 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { realpathSync, statSync, watch } from 'node:fs';
|
|
2
3
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
+
import { basename, dirname, join } from 'node:path';
|
|
5
6
|
const DEFAULT_PACKAGE = 'c0de-agent';
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const DEFAULT_MANUAL_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
8
|
+
/**
|
|
9
|
+
* 识别当前进程的安装方式:
|
|
10
|
+
* - 入口脚本 realpath 含 node_modules/.pnpm → pnpm
|
|
11
|
+
* - 含 node_modules(非 .pnpm)→ npm
|
|
12
|
+
* - 否则 unknown(npx 缓存、源码 dev、其他)→ 不自动安装
|
|
13
|
+
* 检测不到时返回 unknown,由上层提示用户手动安装。
|
|
14
|
+
*/
|
|
15
|
+
function detectInstallMethod() {
|
|
16
|
+
const entry = process.argv[1];
|
|
17
|
+
if (!entry)
|
|
18
|
+
return { kind: 'unknown', hint: '无法确定入口脚本' };
|
|
19
|
+
let real = entry;
|
|
20
|
+
try {
|
|
21
|
+
real = realpathSync(entry);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// 入口不存在/不可解析:尝试保留原始路径判断
|
|
25
|
+
}
|
|
26
|
+
const p = real.replace(/\\/g, '/');
|
|
27
|
+
if (p.includes('/.pnpm/'))
|
|
28
|
+
return { kind: 'pnpm' };
|
|
29
|
+
if (p.includes('/node_modules/'))
|
|
30
|
+
return { kind: 'npm' };
|
|
31
|
+
if (p.includes('/_npx/')) {
|
|
32
|
+
return { kind: 'unknown', hint: '通过 npx 临时缓存运行,无法自动更新' };
|
|
33
|
+
}
|
|
34
|
+
return { kind: 'unknown', hint: `无法识别安装方式(入口:${basename(entry)})` };
|
|
35
|
+
}
|
|
36
|
+
/** 默认自更新:按检测到的安装方式执行。unknown 时抛错(上层转 manual_install_required)。 */
|
|
37
|
+
function defaultInstall(pkg, method) {
|
|
8
38
|
return new Promise((resolve, reject) => {
|
|
9
|
-
const
|
|
39
|
+
const cmd = method.kind === 'pnpm' ? 'pnpm' : 'npm';
|
|
40
|
+
const args = method.kind === 'pnpm' ? ['add', '-g', pkg] : ['install', '-g', pkg];
|
|
41
|
+
const child = spawn(cmd, args, { stdio: 'ignore' });
|
|
10
42
|
child.on('error', reject);
|
|
11
43
|
child.on('exit', (code) => {
|
|
12
44
|
if (code === 0)
|
|
13
45
|
resolve();
|
|
14
46
|
else
|
|
15
|
-
reject(new Error(
|
|
47
|
+
reject(new Error(`${cmd} ${args.join(' ')} exited with ${code}`));
|
|
16
48
|
});
|
|
17
49
|
});
|
|
18
50
|
}
|
|
51
|
+
/** 手动安装提示命令(unknown 安装方式时返回给前端展示)。 */
|
|
52
|
+
function manualInstallCommand(pkg) {
|
|
53
|
+
return `npm install -g ${pkg} # 或 pnpm add -g ${pkg}`;
|
|
54
|
+
}
|
|
55
|
+
function statFingerprint(path) {
|
|
56
|
+
try {
|
|
57
|
+
const s = statSync(path);
|
|
58
|
+
return { size: s.size, mtimeMs: s.mtimeMs };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** 采集当前执行程序文件(argv[1] 及 realpath)的 stat 基线。 */
|
|
65
|
+
function captureProgramBaseline() {
|
|
66
|
+
const entry = process.argv[1];
|
|
67
|
+
if (!entry)
|
|
68
|
+
return null;
|
|
69
|
+
const paths = new Set([entry]);
|
|
70
|
+
try {
|
|
71
|
+
paths.add(realpathSync(entry));
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// 入口不存在时仅记录原始路径
|
|
75
|
+
}
|
|
76
|
+
const baseline = new Map();
|
|
77
|
+
for (const p of paths)
|
|
78
|
+
baseline.set(p, statFingerprint(p));
|
|
79
|
+
return baseline;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* watch 当前执行程序文件(argv[1] 及其 realpath 所在目录)变更。
|
|
83
|
+
* 返回 Promise:文件内容变更(size/mtime 变化)后 resolve。
|
|
84
|
+
* 采用目录 fs.watch + 1s 轮询 stat 双保险(npm 原子替换文件时 watch 事件可能丢失)。
|
|
85
|
+
* baseline 由调用方在「变更发生前」采集(安装前),否则安装已改写文件导致永远等不到变化。
|
|
86
|
+
* 超时(timeoutMs)或可注入的 cancel 信号触发时 reject。
|
|
87
|
+
*/
|
|
88
|
+
function waitForProgramChange(timeoutMs, baseline, signal, pollMs = 1000) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const entry = process.argv[1];
|
|
91
|
+
if (!entry) {
|
|
92
|
+
reject(new Error('无法确定程序入口文件'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const paths = new Set([entry]);
|
|
96
|
+
try {
|
|
97
|
+
paths.add(realpathSync(entry));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// 入口不存在时仅 watch 原始路径目录
|
|
101
|
+
}
|
|
102
|
+
let settled = false;
|
|
103
|
+
let timer = null;
|
|
104
|
+
let interval = null;
|
|
105
|
+
const watchers = [];
|
|
106
|
+
const finish = (fn) => {
|
|
107
|
+
if (settled)
|
|
108
|
+
return;
|
|
109
|
+
settled = true;
|
|
110
|
+
if (timer)
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
if (interval)
|
|
113
|
+
clearInterval(interval);
|
|
114
|
+
for (const w of watchers)
|
|
115
|
+
w.close();
|
|
116
|
+
fn();
|
|
117
|
+
};
|
|
118
|
+
const changed = () => {
|
|
119
|
+
for (const [p, before] of baseline) {
|
|
120
|
+
const after = statFingerprint(p);
|
|
121
|
+
if (before === null) {
|
|
122
|
+
if (after !== null)
|
|
123
|
+
return true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (after === null || after.size !== before.size || after.mtimeMs !== before.mtimeMs) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
};
|
|
132
|
+
const onAbort = () => finish(() => reject(new Error('waiting aborted')));
|
|
133
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
134
|
+
timer = setTimeout(() => finish(() => reject(new Error('等待程序文件变更超时'))), timeoutMs);
|
|
135
|
+
interval = setInterval(() => {
|
|
136
|
+
if (changed())
|
|
137
|
+
finish(() => resolve());
|
|
138
|
+
}, pollMs);
|
|
139
|
+
for (const p of paths) {
|
|
140
|
+
try {
|
|
141
|
+
const dir = dirname(p);
|
|
142
|
+
watchers.push(watch(dir, () => {
|
|
143
|
+
if (changed())
|
|
144
|
+
finish(() => resolve());
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// 目录不可 watch:依赖轮询兜底
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
19
153
|
/** 默认启动新实例:detached spawn CLI bin,argv = serve <restore> <handoff>。
|
|
20
154
|
* 必须以 `serve` 子命令开头(CLI dispatch 按首参选命令);
|
|
21
155
|
* --port 复用旧实例端口,--handoff-port 让新实例启动后请求旧实例退出。
|
|
@@ -36,56 +170,125 @@ function defaultSpawn(_snapshotPath, argv, opts) {
|
|
|
36
170
|
}
|
|
37
171
|
});
|
|
38
172
|
}
|
|
173
|
+
/** 组装新实例 argv(serve --restore <snapshot> [--port] [--handoff-port])。 */
|
|
174
|
+
function buildSpawnArgv(snapshotPath, opts) {
|
|
175
|
+
const restoreFlag = opts.restoreFlag ?? '--restore';
|
|
176
|
+
const argv = [
|
|
177
|
+
'serve',
|
|
178
|
+
restoreFlag,
|
|
179
|
+
snapshotPath,
|
|
180
|
+
...(opts.port !== undefined ? ['--port', String(opts.port)] : []),
|
|
181
|
+
];
|
|
182
|
+
if (opts.handoffPort !== undefined) {
|
|
183
|
+
argv.push('--handoff-port', String(opts.handoffPort));
|
|
184
|
+
}
|
|
185
|
+
return argv;
|
|
186
|
+
}
|
|
39
187
|
/**
|
|
40
|
-
* 执行热更新(spec §18.1-18.2
|
|
188
|
+
* 执行热更新(spec §18.1-18.2 + P0-2 修订流程):
|
|
41
189
|
* 1. 序列化当前会话状态到快照文件
|
|
42
|
-
* 2.
|
|
43
|
-
* 3.
|
|
190
|
+
* 2. 识别当前进程安装方式(npm/pnpm/unknown)
|
|
191
|
+
* 3. 已知方式 → 自动安装;unknown → 不安装,等待用户手动安装
|
|
192
|
+
* 4. watch 当前执行程序文件变更 → 触发滚动切换
|
|
193
|
+
* (detached 启动新实例 + restore 快照 + handoff 端口接管)
|
|
44
194
|
*
|
|
45
195
|
* 注:旧实例的 graceful shutdown / 端口让渡由新实例启动后通过 IPC 协调
|
|
46
|
-
* (见 ipc.ts),本函数只负责"序列化 + 安装 + 接力"。
|
|
196
|
+
* (见 ipc.ts),本函数只负责"序列化 + 安装 + 等待程序变更 + 接力"。
|
|
47
197
|
*/
|
|
48
198
|
async function performHotUpdate(snapshot, opts = {}) {
|
|
49
199
|
const pkg = opts.packageName ?? DEFAULT_PACKAGE;
|
|
50
200
|
const dir = await mkdtemp(join(tmpdir(), 'c0de-update-'));
|
|
51
201
|
const snapshotPath = opts.snapshotPath ?? join(dir, 'snapshot.json');
|
|
52
202
|
await writeFile(snapshotPath, JSON.stringify(snapshot), 'utf8');
|
|
53
|
-
|
|
203
|
+
// 识别安装方式
|
|
204
|
+
const method = detectInstallMethod();
|
|
205
|
+
// 安装前采集程序文件基线(watch 依赖此判断「变更已发生」)
|
|
206
|
+
const baseline = captureProgramBaseline();
|
|
207
|
+
// 已知安装方式:自动安装;unknown/安装失败 → 等用户手动安装。
|
|
208
|
+
let installReason = null;
|
|
209
|
+
if (method.kind !== 'unknown') {
|
|
210
|
+
const install = opts.installFn ?? defaultInstall;
|
|
211
|
+
try {
|
|
212
|
+
await install(pkg, method);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
// 已知安装方式安装失败:立即返回(不进入 10 分钟等待),快照已落盘可恢复。
|
|
216
|
+
// 用户可用 c0de update --apply 重试或手动安装后重启。
|
|
217
|
+
return {
|
|
218
|
+
_tag: 'install_failed',
|
|
219
|
+
error: error instanceof Error ? error.message : String(error),
|
|
220
|
+
snapshotPath,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
installReason = `${method.hint},请手动执行 ${manualInstallCommand(pkg)} 完成更新`;
|
|
226
|
+
}
|
|
227
|
+
if (installReason !== null) {
|
|
228
|
+
if (!baseline) {
|
|
229
|
+
return {
|
|
230
|
+
_tag: 'manual_install_required',
|
|
231
|
+
error: '无法确定程序入口文件,无法监听更新',
|
|
232
|
+
snapshotPath,
|
|
233
|
+
command: manualInstallCommand(pkg),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
try {
|
|
237
|
+
await waitForProgramChange(opts.manualWaitTimeoutMs ?? DEFAULT_MANUAL_WAIT_TIMEOUT_MS, baseline);
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
return {
|
|
241
|
+
_tag: 'manual_install_required',
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
snapshotPath,
|
|
244
|
+
command: manualInstallCommand(pkg),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// 程序文件已变更(自动安装落盘或用户手动安装完成)→ 滚动切换
|
|
249
|
+
const spawnNew = opts.spawnNewInstanceFn ?? defaultSpawn;
|
|
250
|
+
const argv = buildSpawnArgv(snapshotPath, opts);
|
|
54
251
|
try {
|
|
55
|
-
await
|
|
252
|
+
await spawnNew(snapshotPath, argv, opts);
|
|
56
253
|
}
|
|
57
254
|
catch (error) {
|
|
58
255
|
return {
|
|
59
|
-
_tag: '
|
|
256
|
+
_tag: 'spawn_failed',
|
|
60
257
|
error: error instanceof Error ? error.message : String(error),
|
|
61
258
|
snapshotPath,
|
|
62
259
|
};
|
|
63
260
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
261
|
+
return { _tag: 'success', snapshotPath, installMethod: method.kind };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* 仅安装新版本(P2-7:无活跃 serve 的 `c0de update --apply`)。
|
|
265
|
+
* 不序列化快照、不 spawn `serve`——此前独立 CLI 执行会拉起一个用户未请求的常驻服务。
|
|
266
|
+
* 运行中的 serve 由 `/api/update/apply` 走 performHotUpdate 热更新路径。
|
|
267
|
+
*/
|
|
268
|
+
async function performInstall(opts = {}) {
|
|
269
|
+
const pkg = opts.packageName ?? DEFAULT_PACKAGE;
|
|
270
|
+
const method = detectInstallMethod();
|
|
271
|
+
if (method.kind === 'unknown') {
|
|
272
|
+
return {
|
|
273
|
+
_tag: 'manual_install_required',
|
|
274
|
+
error: method.hint,
|
|
275
|
+
command: manualInstallCommand(pkg),
|
|
276
|
+
};
|
|
73
277
|
}
|
|
74
|
-
const
|
|
278
|
+
const install = opts.installFn ?? defaultInstall;
|
|
75
279
|
try {
|
|
76
|
-
await
|
|
280
|
+
await install(pkg, method);
|
|
77
281
|
}
|
|
78
282
|
catch (error) {
|
|
79
283
|
return {
|
|
80
|
-
_tag: '
|
|
284
|
+
_tag: 'install_failed',
|
|
81
285
|
error: error instanceof Error ? error.message : String(error),
|
|
82
|
-
snapshotPath,
|
|
83
286
|
};
|
|
84
287
|
}
|
|
85
|
-
return { _tag: 'success',
|
|
288
|
+
return { _tag: 'success', installMethod: method.kind };
|
|
86
289
|
}
|
|
87
290
|
/** 清理快照临时目录(热更新完成或放弃后调用)。 */
|
|
88
291
|
async function cleanupSnapshot(snapshotPath) {
|
|
89
292
|
await rm(join(snapshotPath, '..'), { recursive: true, force: true }).catch(() => { });
|
|
90
293
|
}
|
|
91
|
-
export { cleanupSnapshot, performHotUpdate };
|
|
294
|
+
export { cleanupSnapshot, detectInstallMethod, manualInstallCommand, performHotUpdate, performInstall, };
|