xuym-codex-collab 0.4.4 → 0.6.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 +23 -2
- package/apps/onboarding/bundle.mjs +1 -6
- package/apps/onboarding/commands.mjs +46 -0
- package/apps/onboarding/guided.mjs +3 -2
- package/apps/onboarding/main.mjs +72 -19
- package/apps/onboarding/manager.mjs +116 -55
- package/apps/onboarding/reset.mjs +3 -1
- package/apps/onboarding/server.mjs +1 -1
- package/apps/onboarding/startup.mjs +61 -0
- package/apps/onboarding/supervisor.mjs +41 -0
- package/apps/onboarding/web/app.js +78 -13
- package/apps/onboarding/web/index.html +17 -17
- package/apps/onboarding/web/style.css +3 -0
- package/apps/onboarding/workspace.mjs +36 -0
- package/apps/peer/desktop.mjs +4 -3
- package/apps/peer/intent.mjs +19 -0
- package/apps/peer/mcp.mjs +1 -1
- package/apps/peer/process.mjs +13 -0
- package/apps/peer/runner.mjs +4 -3
- package/apps/peer/runtime.mjs +6 -1
- package/apps/peer/wecom.mjs +1 -1
- package/bin/codex-collab.mjs +21 -9
- package/docs/desktop-poll.md +2 -2
- package/docs/onboarding-audit.md +12 -2
- package/docs/onboarding.md +37 -9
- package/docs/peer.md +4 -2
- package/docs/releases/0.5.0.md +14 -0
- package/docs/team-registry.md +7 -1
- package/docs/terminal-onboarding.md +3 -3
- package/package.json +1 -1
- package/packages/codex-kit/templates/.agents/skills/team-collab/SKILL.md +2 -2
- package/packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md +2 -2
- package/packages/peer/config.mjs +2 -0
- package/packages/peer/git.mjs +14 -0
- package/packages/peer/protocol.mjs +4 -2
- package/packages/peer/repository.mjs +67 -0
- package/packages/peer/service.mjs +11 -8
- package/packages/peer/store.mjs +16 -16
- package/schemas/peer-task.schema.json +15 -13
- package/schemas/team-policy.schema.json +12 -1
- package/scripts/peer/cli.mjs +14 -8
- package/scripts/peer/init-agent.mjs +6 -15
- package/scripts/peer/onboarding.mjs +26 -20
- package/scripts/peer/refresh-project.mjs +23 -15
package/scripts/peer/cli.mjs
CHANGED
|
@@ -5,10 +5,13 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { join, resolve } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { diagnosePeer } from './onboarding.mjs';
|
|
8
9
|
import { loadRuntime, readPeerConfig } from '../../apps/peer/runtime.mjs';
|
|
9
|
-
import { alive, readRunnerLock } from '../../apps/peer/process.mjs';
|
|
10
|
+
import { alive, readRunnerLock, acquireRunnerControl } from '../../apps/peer/process.mjs';
|
|
10
11
|
import { git } from '../../packages/peer/store.mjs';
|
|
11
12
|
|
|
13
|
+
import { readRunnerIntent, writeRunnerIntent } from '../../apps/peer/intent.mjs';
|
|
14
|
+
|
|
12
15
|
const root = fileURLToPath(new URL('../../', import.meta.url));
|
|
13
16
|
const [command, configArg, ...args] = process.argv.slice(2);
|
|
14
17
|
if (!command || !configArg) throw new Error('usage: node scripts/peer/cli.mjs install|doctor|probe|start|stop|status|list|read|review|complete|reopen|desktop-status|desktop-unbind /abs/peer.json [task-id] [decision/reason]');
|
|
@@ -20,6 +23,8 @@ async function running() {
|
|
|
20
23
|
return state && alive(state.pid) ? state : null;
|
|
21
24
|
}
|
|
22
25
|
|
|
26
|
+
const releaseControl = ['start', 'recover', 'stop'].includes(command) ? await acquireRunnerControl(config.data_dir, configPath) : null;
|
|
27
|
+
try {
|
|
23
28
|
switch (command) {
|
|
24
29
|
case 'probe': {
|
|
25
30
|
try {
|
|
@@ -51,14 +56,12 @@ switch (command) {
|
|
|
51
56
|
break;
|
|
52
57
|
}
|
|
53
58
|
case 'doctor': {
|
|
54
|
-
|
|
55
|
-
const main = await git(config.repo, 'ls-remote', '--heads', 'origin', 'refs/heads/main');
|
|
56
|
-
if (!main) throw new Error('shared origin needs a main branch');
|
|
57
|
-
execFileSync('git', ['-C', config.repo, 'check-ref-format', 'refs/heads/agent-collab'], { stdio: 'ignore' });
|
|
58
|
-
console.log(JSON.stringify({ identity: config.agent_id, auto_execute: config.auto_execute !== false, execution_session: config.execution_mode === 'desktop_poll' ? 'bound desktop heartbeat; adapter never launches SDK (check desktop-status and app heartbeat separately)' : 'new Runner-owned SDK thread; not an existing desktop conversation', config: 'valid; private key matches policy', git: 'shared origin and main readable', origin_configured: !!origin, wecom: config.wecom ? 'configured; live authentication not checked' : 'disabled', codex_provider: config.execution_mode === 'desktop_poll' ? 'uses the existing desktop conversation model; no SDK provider is launched by this adapter' : 'not tested; uses selected CODEX_HOME/provider configuration', test_argv: config.test_argv || ['sh', 'test.sh'] }, null, 2));
|
|
59
|
+
console.log(JSON.stringify(await diagnosePeer(configPath), null, 2));
|
|
59
60
|
break;
|
|
60
61
|
}
|
|
61
|
-
case 'start': {
|
|
62
|
+
case 'start': case 'recover': {
|
|
63
|
+
if (command === 'recover') { if (!(await readRunnerIntent(config.data_dir))?.enabled) break; }
|
|
64
|
+
else await writeRunnerIntent(config.data_dir, true);
|
|
62
65
|
const current = await running();
|
|
63
66
|
if (current) { console.log(`Runner already online (PID ${current.pid})`); break; }
|
|
64
67
|
await mkdir(config.data_dir, { recursive: true, mode: 0o700 });
|
|
@@ -81,10 +84,11 @@ switch (command) {
|
|
|
81
84
|
if (health.pid === state.pid && new Date(health.updated_at).getTime() >= new Date(state.started_at).getTime() && ['connecting', 'authenticated', 'reconnecting', 'disconnected', 'failed'].includes(health.status)) wecom = health.status;
|
|
82
85
|
} catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
83
86
|
}
|
|
84
|
-
console.log(JSON.stringify({ identity: config.agent_id, execution_mode: config.execution_mode || 'legacy', auto_execute: config.auto_execute !== false, online: !!state, wecom, pid: state?.pid || null, log: join(config.data_dir, 'runner.log') }, null, 2));
|
|
87
|
+
console.log(JSON.stringify({ identity: config.agent_id, execution_mode: config.execution_mode || 'legacy', auto_execute: config.auto_execute !== false, online: !!state, connection_enabled: (await readRunnerIntent(config.data_dir))?.enabled ?? !!state, wecom, pid: state?.pid || null, log: join(config.data_dir, 'runner.log') }, null, 2));
|
|
85
88
|
break;
|
|
86
89
|
}
|
|
87
90
|
case 'stop': {
|
|
91
|
+
await writeRunnerIntent(config.data_dir, false);
|
|
88
92
|
const state = await running();
|
|
89
93
|
if (!state) { console.log('Runner offline'); break; }
|
|
90
94
|
if (state.config !== configPath) throw new Error('PID configuration mismatch; refusing to signal');
|
|
@@ -114,3 +118,5 @@ switch (command) {
|
|
|
114
118
|
}
|
|
115
119
|
default: throw new Error('unknown peer command');
|
|
116
120
|
}
|
|
121
|
+
|
|
122
|
+
} finally { await releaseControl?.(); }
|
|
@@ -1,15 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const team = validateTeam(JSON.parse(await readFile(resolve(teamPath), 'utf8')));
|
|
8
|
-
const identity = await readPrivateJson(resolve(identityPath));
|
|
9
|
-
if (!team.agents[identity.agent_id]) throw new Error('identity is not in team policy');
|
|
10
|
-
const publicKey = createPublicKey(identity.private_key).export({ type: 'spki', format: 'pem' });
|
|
11
|
-
if (publicKey !== team.agents[identity.agent_id].public_key) throw new Error('private identity does not match the public key pinned in team policy');
|
|
12
|
-
const config = { schema_version: 2, agent_id: identity.agent_id, repo: resolve(repo), data_dir: resolve(dataDir), team_file: resolve(teamPath), private_key: identity.private_key, test_argv: ['sh', 'test.sh'] };
|
|
13
|
-
await mkdir(dirname(resolve(out)), { recursive: true, mode: 0o700 });
|
|
14
|
-
await writeFile(resolve(out), JSON.stringify(config, null, 2) + '\n', { mode: 0o600, flag: 'wx' }); await chmod(resolve(out), 0o600);
|
|
15
|
-
console.log(`private peer config written: ${resolve(out)} (keep it local)`);
|
|
1
|
+
import { isAbsolute } from 'node:path';
|
|
2
|
+
import { createMemberConfig } from './onboarding.mjs';
|
|
3
|
+
const [teamPath, identityPath, repoPath, dataDir, configPath] = process.argv.slice(2);
|
|
4
|
+
if (![teamPath, identityPath, repoPath, dataDir, configPath].every((v) => v && isAbsolute(v))) throw new Error('usage: node scripts/peer/init-agent.mjs /abs/team.json /abs/bob.private.json /abs/clone /abs/data /abs/bob-peer.json');
|
|
5
|
+
await createMemberConfig({ teamPath, identityPath, repoPath, dataDir, configPath });
|
|
6
|
+
console.log(`private peer config written: ${configPath} (keep it local)`);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { inspectRepository, resolveRemote, teamBaseBranch } from '../../packages/peer/repository.mjs';
|
|
1
2
|
import { createPublicKey } from 'node:crypto';
|
|
2
3
|
import { spawnSync } from 'node:child_process';
|
|
3
4
|
import { emitKeypressEvents } from 'node:readline';
|
|
@@ -62,10 +63,11 @@ export async function getOrCreateIdentityFiles(agentId, directory) {
|
|
|
62
63
|
return { agent_id: agentId, private_key_file: privatePath, public_key_file: publicPath };
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
export function buildTeamPolicy({ repo, reviewer, defaultRoute, agents, routes, policy = {} }) {
|
|
66
|
+
export function buildTeamPolicy({ repo, reviewer, defaultRoute, agents, routes, policy = {}, baseBranch }) {
|
|
66
67
|
const team = validateTeam({
|
|
67
68
|
schema_version: 2,
|
|
68
69
|
repo,
|
|
70
|
+
...(baseBranch !== undefined ? { base_branch: baseBranch } : {}),
|
|
69
71
|
reviewer,
|
|
70
72
|
default_route: defaultRoute,
|
|
71
73
|
agents: Object.fromEntries(agents.map((agent) => [agent.id, {
|
|
@@ -95,7 +97,7 @@ export async function writeTeamPolicy(path, team) {
|
|
|
95
97
|
return path;
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
export async function createMemberConfig({ teamPath, identityPath, repoPath, dataDir, configPath, testArgv = ['sh', 'test.sh'] }) {
|
|
100
|
+
export async function createMemberConfig({ teamPath, identityPath, repoPath, dataDir, configPath, testArgv = ['sh', 'test.sh'], remoteName, remoteUrl }) {
|
|
99
101
|
for (const path of [teamPath, identityPath, repoPath, dataDir, configPath]) {
|
|
100
102
|
if (!path || !isAbsolute(path)) throw new Error('team, identity, repository, data and config paths must be absolute');
|
|
101
103
|
}
|
|
@@ -109,13 +111,13 @@ export async function createMemberConfig({ teamPath, identityPath, repoPath, dat
|
|
|
109
111
|
if (await git(repo, 'rev-parse', '--show-toplevel') !== repo) throw new Error('select the root directory of a Git project');
|
|
110
112
|
const privatePaths = [dataDir, configPath, identityPath];
|
|
111
113
|
if (privatePaths.some((path) => isWithin(repoPath, path) || isWithin(repo, path))) throw new Error('private identity, data and config files must stay outside the code repository');
|
|
112
|
-
const
|
|
113
|
-
const main = await git(repo, 'ls-remote', '--heads', 'origin', 'refs/heads/main');
|
|
114
|
-
if (!main) throw new Error('the shared origin must have a readable main branch');
|
|
114
|
+
const selected = await inspectRepository(repo, { remoteName, expectedRemote: remoteUrl, baseBranch: teamBaseBranch(team) });
|
|
115
115
|
const config = {
|
|
116
116
|
schema_version: 2,
|
|
117
117
|
agent_id: identity.agent_id,
|
|
118
118
|
repo,
|
|
119
|
+
remote_name: selected.remote_name,
|
|
120
|
+
remote_url: selected.remote,
|
|
119
121
|
data_dir: dataDir,
|
|
120
122
|
team_file: teamPath,
|
|
121
123
|
private_key: identity.private_key,
|
|
@@ -133,29 +135,32 @@ export async function createMemberConfig({ teamPath, identityPath, repoPath, dat
|
|
|
133
135
|
await rm(configPath, { force: true });
|
|
134
136
|
throw error;
|
|
135
137
|
}
|
|
136
|
-
return { config, team, origin };
|
|
138
|
+
return { config, team, origin: selected.remote };
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
export async function diagnosePeer(configPath, { testPush = false } = {}) {
|
|
140
|
-
const { config, team } = await readPeerConfig(configPath);
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
144
|
-
const
|
|
142
|
+
const { config, team: cachedTeam } = await readPeerConfig(configPath);
|
|
143
|
+
const remote = await resolveRemote(config.repo, { remoteName: config.remote_name, expectedRemote: config.remote_url, legacyOrigin: !config.remote_name && !config.remote_url });
|
|
144
|
+
const ledger = await git(config.repo, 'ls-remote', '--heads', remote.remote_name, 'refs/heads/agent-collab');
|
|
145
|
+
if (config.team_registry_root && !ledger) throw new Error('signed team ledger is missing; restore agent-collab before continuing');
|
|
146
|
+
const team = config.team_registry_root ? (await loadRuntime(configPath)).store.team : cachedTeam;
|
|
147
|
+
const selected = await inspectRepository(config.repo, { remoteName: remote.remote_name, expectedRemote: remote.remote, baseBranch: teamBaseBranch(team) });
|
|
145
148
|
let writeAccess = 'not checked';
|
|
146
149
|
if (testPush) {
|
|
147
150
|
const probe = `refs/heads/codex-collab-doctor/${process.pid}-${Date.now()}`;
|
|
148
|
-
await git(config.repo, 'push', '--dry-run',
|
|
151
|
+
await git(config.repo, 'push', '--dry-run', selected.remote_name, `HEAD:${probe}`);
|
|
149
152
|
writeAccess = 'push dry-run passed; no remote ref was created';
|
|
150
153
|
}
|
|
151
154
|
return {
|
|
152
155
|
agent_id: config.agent_id,
|
|
153
156
|
mode: config.execution_mode,
|
|
154
|
-
origin,
|
|
157
|
+
origin: selected.remote,
|
|
158
|
+
remote_name: selected.remote_name,
|
|
159
|
+
base_branch: selected.base_branch,
|
|
155
160
|
team_repo: team.repo,
|
|
156
161
|
checks: {
|
|
157
162
|
identity: 'valid; private key matches team public key',
|
|
158
|
-
repository:
|
|
163
|
+
repository: `Git root with readable ${selected.remote_name}/${selected.base_branch}`,
|
|
159
164
|
ledger: ledger ? 'agent-collab exists' : 'not initialized; first start may create it',
|
|
160
165
|
push_access: writeAccess,
|
|
161
166
|
wecom: config.wecom ? 'configured; authentication requires starting the adapter' : 'not configured',
|
|
@@ -305,8 +310,9 @@ async function createAdminTeam(prompts, output) {
|
|
|
305
310
|
const reviewer = await prompts.ask('人工审核管理员 agent ID', id);
|
|
306
311
|
const defaultRoute = await prompts.ask('默认任务能力标签', capabilities[0]);
|
|
307
312
|
const repo = await prompts.required('共享代码仓库标识(例如 owner/repo)');
|
|
313
|
+
const baseBranch = await prompts.ask('业务基准分支(供所有成员继承;网页向导可自动识别)', 'main');
|
|
308
314
|
const teamPath = resolve(await prompts.ask('团队策略文件路径', join(homedir(), 'codex-collab-team.json')));
|
|
309
|
-
const team = buildTeamPolicy({ repo, reviewer, defaultRoute, agents, routes });
|
|
315
|
+
const team = buildTeamPolicy({ repo, reviewer, defaultRoute, agents, routes, baseBranch });
|
|
310
316
|
await writeTeamPolicy(teamPath, team);
|
|
311
317
|
output.write(`\n团队策略已创建:${teamPath}\n只分发此 team.json;各成员仍须在自己的电脑运行 setup 并使用自己的私钥。\n`);
|
|
312
318
|
return { teamPath, identityPath: identityPaths.private_key_file, agentId: id };
|
|
@@ -335,7 +341,7 @@ async function configureWeCom(prompts, configPath, config, team, output) {
|
|
|
335
341
|
}
|
|
336
342
|
|
|
337
343
|
async function joinTeam(prompts, output, initial = {}) {
|
|
338
|
-
output.write('\n
|
|
344
|
+
output.write('\n成员接入:私钥留在本机;共享仓库必须允许读取团队基准分支,并具备推送协作分支的权限。\n\n');
|
|
339
345
|
const teamPath = resolve(initial.teamPath || await prompts.required('管理员提供的 team.json 路径'));
|
|
340
346
|
const identityPath = resolve(initial.identityPath || await prompts.required('本机私有身份文件路径'));
|
|
341
347
|
const identity = await readPrivateJson(identityPath);
|
|
@@ -347,10 +353,10 @@ async function joinTeam(prompts, output, initial = {}) {
|
|
|
347
353
|
let parsedArgv;
|
|
348
354
|
try { parsedArgv = JSON.parse(rawArgv); }
|
|
349
355
|
catch { throw new Error('固定验证命令必须是 JSON 字符串数组,例如 ["npm", "test"]'); }
|
|
350
|
-
const { config, team
|
|
351
|
-
output.write(`\n身份 ${agentId}
|
|
356
|
+
const { config, team } = await createMemberConfig({ teamPath, identityPath, repoPath, dataDir, configPath, testArgv: parsedArgv });
|
|
357
|
+
output.write(`\n身份 ${agentId} 已验证;共享远程:${config.remote_name};基准分支:${teamBaseBranch(team)}\n执行模式:desktop_poll(不启动后台 Codex SDK)\n`);
|
|
352
358
|
|
|
353
|
-
if (await prompts.confirm('把本项目接入 Codex MCP + Skill?(只写本机 project config
|
|
359
|
+
if (await prompts.confirm('把本项目接入 Codex MCP + Skill?(只写本机 project config)', true)) {
|
|
354
360
|
const attached = await attachProject({ configPath, repoPath: config.repo });
|
|
355
361
|
output.write(`MCP 配置:${attached.mcp_config}\nSkill:${attached.skill}\n`);
|
|
356
362
|
}
|
|
@@ -361,7 +367,7 @@ async function joinTeam(prompts, output, initial = {}) {
|
|
|
361
367
|
const diagnosis = await diagnosePeer(configPath, { testPush: true });
|
|
362
368
|
output.write('\n连接检查:\n');
|
|
363
369
|
for (const [name, result] of Object.entries(diagnosis.checks)) output.write(`- ${name}: ${result}\n`);
|
|
364
|
-
if (await prompts.confirm('现在初始化/同步共享 agent-collab
|
|
370
|
+
if (await prompts.confirm('现在初始化/同步共享 agent-collab 账本分支?首次初始化会向所选协作远程推送该分支。', true)) {
|
|
365
371
|
await loadRuntime(configPath);
|
|
366
372
|
output.write('共享账本分支已就绪。\n');
|
|
367
373
|
}
|
|
@@ -6,14 +6,15 @@ import { readPeerConfig } from '../../apps/peer/runtime.mjs';
|
|
|
6
6
|
import { git } from '../../packages/peer/store.mjs';
|
|
7
7
|
import { bindingPrompt } from './setup-desktop-project.mjs';
|
|
8
8
|
const root = fileURLToPath(new URL('../../', import.meta.url));
|
|
9
|
-
const legacyHashes = new Set(['46543ca4fe00836f0ae0d7d7c12ea2756c9ab0220f01d29c391d608c5e405c53', '6e4d1ea6a644fb3078c541e40bcc9e8bf5069462994152f3ec2fc3aa5fedd5fa', 'f98ad425e2ea3c8ab6fb86febef7d9cb288029ef55bea6937dda6de8b467fece']);
|
|
9
|
+
const legacyHashes = new Set(['f0f5f51ec7f2e0952d15e079d37e9f00b958ac7fda0d819c0a20e4ed620abaff', '46543ca4fe00836f0ae0d7d7c12ea2756c9ab0220f01d29c391d608c5e405c53', '6e4d1ea6a644fb3078c541e40bcc9e8bf5069462994152f3ec2fc3aa5fedd5fa', 'f98ad425e2ea3c8ab6fb86febef7d9cb288029ef55bea6937dda6de8b467fece']);
|
|
10
10
|
const hash = (value) => createHash('sha256').update(value).digest('hex');
|
|
11
11
|
const inside = (parent, child) => { const path = relative(parent, child); return !path || path !== '..' && !path.startsWith('../') && !isAbsolute(path); };
|
|
12
|
-
async function plainPath(base, path) {
|
|
12
|
+
async function plainPath(base, path, { allowMissing = false } = {}) {
|
|
13
13
|
let current = base;
|
|
14
14
|
for (const part of relative(base, path).split('/').filter(Boolean)) {
|
|
15
15
|
current = join(current, part);
|
|
16
|
-
|
|
16
|
+
let info; try { info = await lstat(current); } catch (error) { if (allowMissing && error.code === 'ENOENT') return; throw error; }
|
|
17
|
+
if (info.isSymbolicLink()) throw new Error('协作配置或 Skill 是符号链接,请先手动检查;未覆盖文件。');
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
async function privateWrite(path, contents) {
|
|
@@ -22,14 +23,17 @@ async function privateWrite(path, contents) {
|
|
|
22
23
|
finally { await rm(temporary, { force: true }); }
|
|
23
24
|
}
|
|
24
25
|
// Only recognize our generated MCP entry. Never replace another identity's entry.
|
|
25
|
-
export async function installedProjectConfig({ repoPath, agentId }) {
|
|
26
|
+
export async function installedProjectConfig({ repoPath, agentId, allowMissing = false }) {
|
|
26
27
|
const repo = await realpath(repoPath);
|
|
27
|
-
const path = join(repo, '.codex/config.toml'); await plainPath(repo, path);
|
|
28
|
-
const before = await readFile(path, 'utf8');
|
|
28
|
+
const path = join(repo, '.codex/config.toml'); await plainPath(repo, path, { allowMissing });
|
|
29
|
+
const before = await readFile(path, 'utf8').catch((error) => { if (allowMissing && error.code === 'ENOENT') return null; throw error; });
|
|
29
30
|
const server = `team-collab-${agentId}`;
|
|
30
|
-
const headers = [...before.matchAll(/^\s*\[([^\]\n]+)\][^\n]*$/gm)];
|
|
31
|
+
const headers = [...(before || '').matchAll(/^\s*\[([^\]\n]+)\][^\n]*$/gm)];
|
|
31
32
|
if (headers.some((item) => item[1].startsWith(`mcp_servers.${server}.`))) throw new Error('协作 MCP 有自定义嵌套配置,请手动检查;未覆盖文件。');
|
|
32
33
|
const matching = headers.filter((item) => item[1] === `mcp_servers.${server}`);
|
|
34
|
+
// A missing block can be rebuilt from the private profile, but ambiguous or
|
|
35
|
+
// other identities' collaboration settings always require manual inspection.
|
|
36
|
+
if (!matching.length && allowMissing && !(before || '').includes('team-collab-')) return { repo, path, before, missing: true };
|
|
33
37
|
if (matching.length !== 1) throw new Error('未找到唯一的已安装协作 MCP,请先完成项目接入。');
|
|
34
38
|
const header = matching[0]; const end = headers.find((item) => item.index > header.index)?.index ?? before.length;
|
|
35
39
|
const section = before.slice(header.index, end);
|
|
@@ -46,17 +50,19 @@ export async function refreshProject({ configPath }) {
|
|
|
46
50
|
if (!isAbsolute(configPath || '')) throw new Error('usage: codex-collab refresh-project /absolute/private-peer.json');
|
|
47
51
|
const { config } = await readPeerConfig(configPath);
|
|
48
52
|
if (config.execution_mode !== 'desktop_poll' || config.auto_execute !== false) throw new Error('refresh-project 需要 desktop_poll 配置且 SDK 已关闭。');
|
|
49
|
-
const installed = await installedProjectConfig({ repoPath: config.repo, agentId: config.agent_id });
|
|
53
|
+
const installed = await installedProjectConfig({ repoPath: config.repo, agentId: config.agent_id, allowMissing: true });
|
|
50
54
|
const { repo, path: configFile, before, section } = installed;
|
|
51
|
-
if (await git(repo, 'rev-parse', '--show-toplevel') !== repo || await realpath(installed.config_path) !== await realpath(configPath)) throw new Error('项目 MCP 已绑定其他配置,请使用该 MCP 实际使用的个人配置路径;未更换身份。');
|
|
55
|
+
if (await git(repo, 'rev-parse', '--show-toplevel') !== repo || !installed.missing && await realpath(installed.config_path) !== await realpath(configPath)) throw new Error('项目 MCP 已绑定其他配置,请使用该 MCP 实际使用的个人配置路径;未更换身份。');
|
|
52
56
|
const skillFile = join(repo, '.agents/skills/team-collab-desktop/SKILL.md');
|
|
53
|
-
await plainPath(repo, skillFile);
|
|
57
|
+
await plainPath(repo, skillFile, { allowMissing: true });
|
|
54
58
|
if (await git(repo, 'ls-files', '--', '.codex/config.toml', '.agents/skills/team-collab-desktop')) throw new Error('协作配置或 Skill 已提交到项目 Git,请手动合并;未覆盖共享文件。');
|
|
55
|
-
const oldSkill = await readFile(skillFile, 'utf8');
|
|
59
|
+
const oldSkill = await readFile(skillFile, 'utf8').catch((error) => { if (error.code === 'ENOENT') return null; throw error; });
|
|
56
60
|
const template = await readFile(join(root, 'packages/codex-kit/templates/.agents/skills/team-collab-desktop/SKILL.md'), 'utf8');
|
|
57
|
-
if (oldSkill !== template && !legacyHashes.has(hash(oldSkill))) throw new Error('协作 Skill 已自定义,请手动合并自然语言处理说明;未覆盖文件。');
|
|
58
|
-
const
|
|
59
|
-
const
|
|
61
|
+
if (oldSkill !== null && oldSkill !== template && !legacyHashes.has(hash(oldSkill))) throw new Error('协作 Skill 已自定义,请手动合并自然语言处理说明;未覆盖文件。');
|
|
62
|
+
const command = `command = ${JSON.stringify(process.execPath)}`;
|
|
63
|
+
const args = `args = ${JSON.stringify([join(root, 'apps/peer/mcp-main.mjs'), installed.config_path || configPath])}`;
|
|
64
|
+
const nextSection = installed.missing ? `[mcp_servers.team-collab-${config.agent_id}]\n${command}\n${args}\n` : section.replace(/^command\s*=.*$/m, () => command).replace(/^args\s*=.*$/m, () => args);
|
|
65
|
+
const next = installed.missing ? `${before || ''}\n${nextSection}` : before.replace(section, () => nextSection);
|
|
60
66
|
if (inside(repo, resolve(config.data_dir))) throw new Error('个人数据目录必须位于项目仓库之外。');
|
|
61
67
|
await mkdir(config.data_dir, { recursive: true, mode: 0o700 });
|
|
62
68
|
const dataDir = await realpath(config.data_dir);
|
|
@@ -67,8 +73,10 @@ export async function refreshProject({ configPath }) {
|
|
|
67
73
|
if (changed) {
|
|
68
74
|
backup = join(dataDir, `project-refresh-${randomUUID()}.json`);
|
|
69
75
|
await writeFile(backup, JSON.stringify({ repo, config_toml: before, skill: oldSkill, created_at: new Date().toISOString() }, null, 2) + '\n', { mode: 0o600, flag: 'wx' });
|
|
76
|
+
await mkdir(dirname(skillFile), { recursive: true, mode: 0o700 });
|
|
77
|
+
await mkdir(dirname(configFile), { recursive: true, mode: 0o700 });
|
|
70
78
|
try { await privateWrite(configFile, next); await privateWrite(skillFile, template); }
|
|
71
|
-
catch (error) { await privateWrite(configFile, before); await privateWrite(skillFile, oldSkill); throw error; }
|
|
79
|
+
catch (error) { if (before === null) await rm(configFile, { force: true }); else await privateWrite(configFile, before); if (oldSkill === null) await rm(skillFile, { force: true }); else await privateWrite(skillFile, oldSkill); throw error; }
|
|
72
80
|
}
|
|
73
81
|
await privateWrite(prompt, bindingPrompt(config.agent_id, repo));
|
|
74
82
|
return { agent_id: config.agent_id, repo, changed, mcp_config: configFile, skill: skillFile, backup, binding_prompt: prompt,
|