@whyshu/argent 0.1.26 → 0.2.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/bin/argent.mjs DELETED
@@ -1,385 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @whyshu/argent CLI
4
- *
5
- * argent web 生成/复用 profile 并启动 dsh web
6
- * argent health 版本/宿主/profile/runner 自检(--json)
7
- * argent setup 只生成/更新 profile
8
- * argent service install|uninstall runner 常驻(macOS launchd;--print-only/--plist-dir)
9
- * argent update check|run 客户机 outbound npm 更新(不依赖 SSH/EasyTier /dl)
10
- *
11
- * 公共参数:--dsh-home <dir> --profile <name> --repo <dir> --json --dry-run
12
- */
13
- import { spawn, spawnSync } from 'node:child_process';
14
- import { mkdirSync, writeFileSync, rmSync, existsSync, copyFileSync, chmodSync, readFileSync, cpSync } from 'node:fs';
15
- import { join, dirname } from 'node:path';
16
- import { homedir } from 'node:os';
17
- import {
18
- DSH_EXACT_VERSION, PROFILE_NAME, PROD_BUNDLES, buildLaunchdPlist, buildProfile, dshHomeOf, runnerScriptPath,
19
- depsInstalled, health, installDeps, loginWithPhone, pairRunner, probeDshVersion, readAuthToken, repoRoot, runnerConfigPath, sendLoginCode, versionMatches,
20
- engineRoot, materializeRuntime, checkForUpdate, compareVersions, launcherVersion,
21
- PUBLISHED_BUNDLE_VERSIONS, resolveDshBin, updateProfilePluginVersions, writeEngineContract,
22
- } from '../lib/index.mjs';
23
-
24
- const argv = process.argv.slice(2);
25
- const has = (f) => argv.includes(f);
26
- const val = (f, d = '') => { const i = argv.indexOf(f); return i > -1 ? (argv[i + 1] ?? d) : d; };
27
- const JSON_OUT = has('--json');
28
- const DRY_RUN = has('--dry-run');
29
-
30
- const command = (() => {
31
- const positional = argv.filter((a) => !a.startsWith('--') && !['-h'].includes(a));
32
- const i = argv.findIndex((a) => a === 'web' || a === 'health' || a === 'setup' || a === 'pair' || a === 'login' || a === 'service' || a === 'runtime' || a === 'update');
33
- if (i < 0) return '';
34
- if (argv[i] === 'service' || argv[i] === 'runtime' || argv[i] === 'update') return `${argv[i]} ${argv[i + 1] || ''}`.trim();
35
- return argv[i];
36
- })();
37
-
38
- const opts = {
39
- dshHome: dshHomeOf(val('--dsh-home')),
40
- profile: val('--profile', PROFILE_NAME),
41
- repo: repoRoot({ repo: val('--repo') }),
42
- port: Number(val('--port', '3080')) || 3080,
43
- target: val('--target', join(homedir(), '.argent-dsh')),
44
- skillsTarget: has('--no-skills') ? '' : val('--skills-target', join(dshHomeOf(val('--dsh-home')), 'skills')),
45
- registry: val('--registry', process.env.ARGENT_NPM_REGISTRY || 'https://registry.npmjs.org'),
46
- dshBin: val('--dsh-bin', resolveDshBin()),
47
- extraBundles: (() => { const raw = val('--bundles', '').split(',').map((x) => x.trim()).filter(Boolean); return raw.flatMap((x) => (x === 'prod' ? PROD_BUNDLES : [x])); })(),
48
- };
49
-
50
- function print(obj) {
51
- if (JSON_OUT) console.log(JSON.stringify(obj, null, 2));
52
- else for (const [k, v] of Object.entries(obj)) console.log(`${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`);
53
- }
54
-
55
- function ensureBundleDeps() {
56
- const dir = join(opts.dshHome, 'profiles', opts.profile);
57
- let pkg;
58
- try { pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')); } catch { return; }
59
- for (const [name, version] of Object.entries(pkg.dependencies || {})) {
60
- const depDir = join(dir, 'node_modules', ...name.split('/'));
61
- if (existsSync(depDir)) continue;
62
- console.error(`→ 补装缺失 bundle ${name}@${version} …`);
63
- const r = spawnSync('dsh', ['plugin', '--profile', opts.profile, 'add', `${name}@${version}`], {
64
- cwd: dir, stdio: 'inherit', env: { ...process.env, DSH_HOME: opts.dshHome },
65
- });
66
- if (r.status !== 0) {
67
- console.error(`❌ 补装失败:${name}(exit ${r.status})`);
68
- process.exit(2);
69
- }
70
- }
71
- }
72
-
73
- function ensureProfile({ install = true } = {}) {
74
- const built = buildProfile({ dshHome: opts.dshHome, profile: opts.profile, repo: opts.repo, extraBundles: opts.extraBundles });
75
- if (!built.ok) {
76
- console.error(`❌ ${built.message}`);
77
- process.exit(2);
78
- }
79
- if (install && (has('--force-install') || !depsInstalled(opts.dshHome, opts.profile)) && !has('--no-install')) {
80
- console.error('→ 装配 profile 依赖(dsh plugin --profile ' + opts.profile + ' install)…');
81
- const r = installDeps({ dshHome: opts.dshHome, profile: opts.profile });
82
- if (!r.ok) {
83
- console.error(`❌ profile 依赖装配失败:${r.message}`);
84
- console.error(' 可手动重试:DSH_HOME=' + opts.dshHome + ' dsh plugin --profile ' + opts.profile + ' install');
85
- process.exit(2);
86
- }
87
- console.error('✅ profile 依赖装配完成');
88
- }
89
- if (install && !has('--no-install')) ensureBundleDeps();
90
- return built;
91
- }
92
-
93
- function checkVersionOrExit() {
94
- const probe = probeDshVersion();
95
- if (!probe.ok) {
96
- console.error(`❌ 未找到 dsh(${probe.error})。安装器不代装宿主:请先按官方方式准备 dsh@${DSH_EXACT_VERSION}(npx @deepseek-ai/dsh@${DSH_EXACT_VERSION} web)。`);
97
- process.exit(2);
98
- }
99
- if (!versionMatches(probe.version)) {
100
- console.error(`❌ dsh 版本不匹配:当前 ${probe.version},要求 exact ${DSH_EXACT_VERSION}。`);
101
- console.error(' 启动器按 exact pin 运行(防混合版本事故);请切换 dsh 版本后重试。');
102
- process.exit(2);
103
- }
104
- return probe.version;
105
- }
106
-
107
- function launchWeb() {
108
- const built = ensureProfile();
109
- const version = checkVersionOrExit();
110
- const args = ['--profile', opts.profile, ...(opts.port !== 3080 ? ['--port', String(opts.port)] : [])];
111
- if (DRY_RUN) {
112
- print({ ok: true, dryRun: true, dshVersion: version, profile: built.dir, command: `dsh ${args.join(' ')}` });
113
- return;
114
- }
115
- const child = spawn('dsh', args, { stdio: 'inherit', env: process.env });
116
- child.on('close', (code) => process.exit(code ?? 0));
117
- child.on('error', (error) => { console.error(`❌ 启动 dsh 失败:${error.message}`); process.exit(1); });
118
- }
119
-
120
- async function webCommand() {
121
- if (DRY_RUN || JSON_OUT) {
122
- const built = ensureProfile();
123
- const probe = probeDshVersion();
124
- print({
125
- ok: probe.ok && versionMatches(probe.version),
126
- dshVersion: probe.ok ? probe.version : '', expected: DSH_EXACT_VERSION,
127
- profile: built.dir, command: 'dsh --profile ' + opts.profile + (opts.port !== 3080 ? ' --port ' + opts.port : ''),
128
- note: probe.ok && versionMatches(probe.version) ? '版本匹配,可启动' : '版本不匹配/未安装 dsh',
129
- });
130
- return;
131
- }
132
- launchWeb();
133
- }
134
-
135
- async function healthCommand() {
136
- const h = health({ dshHome: opts.dshHome, profile: opts.profile, repo: opts.repo, port: opts.port, dshBin: opts.dshBin });
137
- if (JSON_OUT) console.log(JSON.stringify(h, null, 2));
138
- else {
139
- console.log(`dsh: ${h.dsh.ok ? h.dsh.version : '(未找到)'} ${h.dsh.matchesExact ? '✅ exact' : `❌ 期望 ${h.expectedDsh}`}`);
140
- console.log(`profile: ${h.profile.dir} ${h.profile.packageJson && h.profile.patch ? '✅' : '❌ 未生成(跑 argent setup)'}`);
141
- console.log(`runner 配置: ${h.runner.configPresent ? `✅ ${h.runner.configPath}(${h.runner.mode})` : `— 未配置(${h.runner.configPath})`}`);
142
- console.log(`web: ${h.webUrl}`);
143
- console.log(h.ok ? '✅ 可启动' : '❌ 有前置未满足');
144
- }
145
- process.exit(h.ok ? 0 : 1);
146
- }
147
-
148
- async function readLine(promptText) {
149
- const { createInterface } = await import('node:readline/promises');
150
- const rl = createInterface({ input: process.stdin, output: process.stdout });
151
- try { return (await rl.question(promptText)).trim(); } finally { rl.close(); }
152
- }
153
-
154
- async function loginCommand() {
155
- const serverUrl = val('--server-url');
156
- const phone = val('--phone');
157
- if (!serverUrl || !phone) { console.error('用法:argent login --server-url <url> --phone <手机号> [--code <验证码>]'); process.exit(2); }
158
- let code = val('--code');
159
- if (!code) {
160
- const sent = await sendLoginCode({ serverUrl, phone });
161
- if (!sent.ok) { console.error(`❌ 发送验证码失败:${sent.message}`); process.exit(2); }
162
- if (sent.devCode) console.error(`(开发环境验证码:${sent.devCode})`);
163
- code = await readLine('请输入短信验证码:');
164
- }
165
- const r = await loginWithPhone({ serverUrl, phone, code, dshHome: opts.dshHome });
166
- if (!r.ok) { console.error(`❌ 登录失败:${r.message}`); process.exit(2); }
167
- print({ ok: true, auth: r.path, serverUrl: r.serverUrl, phone: r.phone, tokenTail: `…${r.tokenTail}`, next: '运行 argent pair --from-login --tenant <appId>' });
168
- }
169
-
170
- async function pairCommand() {
171
- const r = await pairRunner({
172
- serverUrl: val('--server-url'),
173
- tenantAppId: val('--tenant') || val('--tenant-app-id'),
174
- adminToken: val('--admin-token') || (has('--from-login') ? (readAuthToken(opts.dshHome)?.token || '') : ''),
175
- runnerToken: val('--runner-token'),
176
- runnerId: val('--runner-id'),
177
- label: val('--label', 'argent-launcher'),
178
- dshHome: opts.dshHome,
179
- });
180
- if (!r.ok) { console.error(`❌ ${r.message}`); process.exit(2); }
181
- print({ ok: true, config: r.configPath, serverUrl: r.serverUrl, tenantAppId: r.tenantAppId, runnerId: r.runnerId, tokenTail: `…${r.tokenTail}` });
182
- }
183
-
184
- function setupCommand() {
185
- const built = ensureProfile();
186
- print({ ok: true, profile: built.dir, bundles: built.bundles.join(', '), plugins: built.plugins.join(', '), preset: built.preset, depsInstalled: depsInstalled(opts.dshHome, opts.profile) });
187
- }
188
-
189
- function runtimeCommand(sub) {
190
- const engine = engineRoot();
191
- if (!engine) { console.error('❌ 未找到 @whyshu/argent-engine;请用新版启动器(npm 依赖应随包安装)'); process.exit(2); }
192
- const version = readFileSync(join(engine, 'VERSION'), 'utf8').trim();
193
- if (sub === 'status' || sub === '') {
194
- let current = null;
195
- try { current = JSON.parse(readFileSync(join(opts.target, 'RUNTIME_VERSION.json'), 'utf8')); } catch { current = null; }
196
- print({ ok: true, engine, engineVersion: version, target: opts.target, current: current ? current.version : null, installed: Boolean(current) });
197
- return;
198
- }
199
- if (sub !== 'install') { console.error('用法:argent runtime install|status [--target <dir>] [--dry-run]'); process.exit(2); }
200
- if (DRY_RUN) { print({ ok: true, dryRun: true, engine, version, target: opts.target, action: 'materialize runtime files' }); return; }
201
- const r = materializeRuntime({ target: opts.target, engine, skillsTarget: opts.skillsTarget });
202
- if (!r.ok) { console.error(`❌ runtime 安装失败:${r.message}`); process.exit(2); }
203
- print({ ok: true, package: '@whyshu/argent-engine', version: r.version, files: r.files, skillsFiles: r.skillsFiles, target: r.target, skillsTarget: r.skillsTarget, backupDir: r.backupDir });
204
- }
205
-
206
- function runCli(bin, args, { cwd = '', env = {} } = {}) {
207
- const r = spawnSync(bin, args, {
208
- cwd: cwd || undefined,
209
- stdio: inherit_stdout() ? 'inherit' : 'pipe',
210
- encoding: 'utf8',
211
- env: { ...process.env, ...env },
212
- timeout: 600000,
213
- })
214
- if (r.error) throw new Error(`${bin} ${args.join(' ')} 启动失败:${r.error.message}`)
215
- if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出 ${r.status}${r.stderr ? `:${String(r.stderr).slice(-400)}` : ''}`)
216
- return r
217
- }
218
-
219
- function inherit_stdout() { return !JSON_OUT }
220
-
221
-
222
- /** desktop profile 的依赖安装器:ARGENT_PNPM → 宿主自带 pnpm → PATH 上的 pnpm */
223
- function resolvePnpm() {
224
- const cands = [
225
- process.env.ARGENT_PNPM,
226
- '/Applications/DeepSeek Harness.app/Contents/Resources/runtime/primary-runtime/dependencies/pnpm/bin/pnpm.mjs',
227
- ].filter(Boolean)
228
- for (const c of cands) { if (existsSync(c)) return { cmd: process.execPath, args: [c] } }
229
- const which = spawnSync('pnpm', ['--version'], { encoding: 'utf8' })
230
- if (which.status === 0) return { cmd: 'pnpm', args: [] }
231
- return null
232
- }
233
-
234
- async function updateCommand(sub) {
235
- if (sub !== 'check' && sub !== 'run') {
236
- console.error('用法:argent update check|run [--to <version>] [--yes] [--registry <url>] [--dsh-home <dir>] [--target <dir>] [--json] [--dry-run]');
237
- process.exit(2)
238
- }
239
- const current = launcherVersion()
240
- let remote
241
- try { remote = await checkForUpdate({ target: opts.target, registry: opts.registry, dshHome: opts.dshHome, profile: opts.profile }) }
242
- catch (e) { console.error(`❌ 检查更新失败:${e.message}`); process.exit(2) }
243
- if (!remote.ok) {
244
- if (JSON_OUT) console.log(JSON.stringify(remote, null, 2)); else console.error(`❌ ${remote.message}`)
245
- process.exit(2)
246
- }
247
- const targetVersion = val('--to', remote.latest)
248
- if (sub === 'check') {
249
- const out = { ...remote, targetVersion, launcher: current, restartRequired: Boolean(remote.updateAvailable) }
250
- if (JSON_OUT) console.log(JSON.stringify(out, null, 2))
251
- else {
252
- console.log(`当前启动器: ${current || '(未知)'}`)
253
- console.log(`npm latest: ${remote.latest}`)
254
- console.log(remote.updateAvailable ? '→ 有可用更新' : '→ 已是最新')
255
- if (remote.pluginUpdates?.length) console.log('插件可更新:' + remote.pluginUpdates.map((x) => `${x.name} ${x.current}→${x.latest}`).join(';'))
256
- console.log(`runtime: ${remote.runtime || '(未物化)'} @ ${remote.runtimeTarget}`)
257
- console.log(`来源: ${remote.registry}`)
258
- }
259
- return
260
- }
261
- if (!remote.updateAvailable && !has('--force')) {
262
- console.log(`已是最新(${current}${remote.pluginUpdates?.length ? ',插件也无更新' : ''}),无需更新。`)
263
- return
264
- }
265
- const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'
266
- const common = ['--yes', `@whyshu/argent@${targetVersion}`]
267
- const installArgs = [...common, 'runtime', 'install', '--target', opts.target, '--skills-target', opts.skillsTarget]
268
- const healthArgs = [...common, 'health', '--dsh-home', opts.dshHome, '--profile', opts.profile, '--json', '--dsh-bin', opts.dshBin]
269
- if (DRY_RUN) {
270
- print({ ok: true, dryRun: true, current, targetVersion, dshBin: opts.dshBin, commands: [
271
- [npx, ...installArgs].join(' '),
272
- `update profile plugin versions (${Object.keys(PUBLISHED_BUNDLE_VERSIONS).join(', ')})`,
273
- `${opts.dshBin} plugin --profile ${opts.profile} install # DSH_HOME=${opts.dshHome}`,
274
- [npx, ...healthArgs].join(' '),
275
- ] })
276
- return
277
- }
278
- if (!has('--yes')) {
279
- console.error('⛔ 更新会改 runtime / profile 插件,必须显式加 --yes。先看计划:argent update run --dry-run')
280
- process.exit(2)
281
- }
282
- // 宿主版本不匹配时绝不能先改 profile(新插件可能要求新 dsh;改完再 health 失败会留下半升级)。
283
- const dshProbe = probeDshVersion({ dshBin: opts.dshBin })
284
- if (!dshProbe.ok || !versionMatches(dshProbe.version)) {
285
- console.error(`⛔ 宿主 dsh 版本不匹配:当前 ${dshProbe.ok ? dshProbe.version : '(未找到)'},启动器要求 ${DSH_EXACT_VERSION}。`)
286
- console.error(' 请先按客户机 dsh 升级流程升级宿主,再重跑 update run;本次未改任何文件。')
287
- process.exit(2)
288
- }
289
- const stamp = new Date().toISOString().replace(/[:.]/g, '-')
290
- const profileDir = join(opts.dshHome, 'profiles', opts.profile)
291
- const backupDir = join(opts.dshHome, 'argent', 'backup', `update-${stamp}`)
292
- mkdirSync(backupDir, { recursive: true })
293
- for (const f of ['package.json', 'cordis.patch.yml', 'pnpm-lock.yaml', 'pnpm-workspace.yaml', 'argent.json']) {
294
- const src = join(profileDir, f)
295
- if (existsSync(src)) { mkdirSync(dirname(join(backupDir, f)), { recursive: true }); cpSync(src, join(backupDir, f)) }
296
- }
297
- const env = { NPM_CONFIG_REGISTRY: opts.registry }
298
- let eng = null
299
- try {
300
- runCli(npx, installArgs, { env })
301
- // 不调用 setup:setup 会重建 profile,可能丢掉客户现有 bundles/依赖结构(2026-10-22 实测)。
302
- const prof = updateProfilePluginVersions({ dshHome: opts.dshHome, profile: opts.profile, versions: PUBLISHED_BUNDLE_VERSIONS })
303
- if (!prof.ok) throw new Error(`更新 profile 插件版本失败:${prof.message}`)
304
- // 方案 C · P3:runtime 换完必须重写契约文件(否则壳仍按旧 engineDir/版本读)
305
- eng = writeEngineContract(profileDir, { repo: opts.repo, target: opts.target })
306
- // ⚠️ dsh CLI **拒绝管理 desktop profile**("managed exclusively by the Electron application")
307
- // ⇒ desktop 必须由宿主自己的 pnpm 装依赖(2026-11-02 实测:`dsh plugin --profile desktop install` 直接 exit 1)。
308
- if (opts.profile === 'desktop') {
309
- const ws = join(profileDir, 'pnpm-workspace.yaml')
310
- if (!existsSync(ws)) {
311
- writeFileSync(ws, 'packages:\n - .\nnodeLinker: hoisted\nautoInstallPeers: false\nallowBuilds:\n protobufjs: true\n')
312
- }
313
- const pnpm = resolvePnpm()
314
- if (!pnpm) throw new Error('desktop profile 需要 pnpm:设置 ARGENT_PNPM,或确保 PATH 上有 pnpm')
315
- runCli(pnpm.cmd, pnpm.args.concat(['install', '--reporter=append-only']), { cwd: profileDir, env })
316
- } else {
317
- runCli(opts.dshBin, ['plugin', '--profile', opts.profile, 'install'], { env: { ...env, DSH_HOME: opts.dshHome }, cwd: opts.dshHome })
318
- }
319
- runCli(npx, healthArgs, { env })
320
- } catch (e) {
321
- console.error(`❌ 更新失败:${e.message}`)
322
- console.error(` 已保留更新前 profile 清单备份:${backupDir}`)
323
- console.error(' 可分步重试:npx -y @whyshu/argent@' + targetVersion + ' runtime install,再按 dsh plugin install 重试;必要时用备份恢复 package.json/patch。')
324
- process.exit(2)
325
- }
326
- print({
327
- ok: true, from: current, to: targetVersion, runtimeTarget: opts.target, profile: profileDir, backupDir,
328
- contract: { engineDir: eng.engineDir, engineVersion: eng.engineVersion, contract: eng.contract, source: eng.source },
329
- restartRequired: true, next: '空闲窗口重启 dsh web(service install/launchd 或现有重启脚本)',
330
- })
331
- }
332
-
333
- function serviceCommand(sub) {
334
- const bundled = runnerScriptPath({ repo: opts.repo });
335
- // 固定 runner 路径:复制进 DSH_HOME,避免 launchd 指向 npx 临时缓存(缓存清理后失效)
336
- const stableRunner = join(opts.dshHome, 'argent', 'runner', 'argent-runner.mjs');
337
- const runnerPath = bundled ? stableRunner : '';
338
- const plist = buildLaunchdPlist({ dshHome: opts.dshHome, repo: opts.repo, runnerPath });
339
- if (sub === 'install' && bundled && !has('--print-only')) {
340
- mkdirSync(dirname(stableRunner), { recursive: true });
341
- copyFileSync(bundled, stableRunner);
342
- chmodSync(stableRunner, 0o755);
343
- }
344
- const outDir = val('--plist-dir', '');
345
- const targetDir = outDir || join(homedir(), 'Library', 'LaunchAgents');
346
- const target = join(targetDir, plist.file);
347
- if (sub === 'uninstall') {
348
- if (existsSync(target)) { rmSync(target); print({ ok: true, removed: target }); } else print({ ok: true, removed: '', note: '未安装' });
349
- return;
350
- }
351
- if (sub !== 'install') { console.error('用法:argent service install|uninstall [--print-only|--plist-dir <dir>]'); process.exit(2); }
352
- if (!plist.script) {
353
- console.error('❌ 找不到 scripts/argent-runner.mjs(发布包应内置 runner;本地联调请给 --repo)');
354
- process.exit(2);
355
- }
356
- if (has('--print-only')) { console.log(plist.content); return; }
357
- mkdirSync(targetDir, { recursive: true });
358
- writeFileSync(target, plist.content);
359
- print({
360
- ok: true, plist: target, label: plist.label,
361
- next: [
362
- `launchctl unload ${target} 2>/dev/null || true`,
363
- `launchctl load -w ${target}`,
364
- `# 日志:${join(opts.dshHome, 'argent', 'runner', 'logs')}`,
365
- ].join('\n'),
366
- runnerConfig: runnerConfigPath(opts.dshHome),
367
- });
368
- }
369
-
370
- const main = async () => {
371
- if (!command || has('-h') || has('--help')) {
372
- console.log('用法:argent <web|health|setup|login|pair|runtime install|runtime status|service install|service uninstall|update check|update run> [--dsh-home <dir>] [--target <dir>] [--skills-target <dir>|--no-skills] [--json] [--dry-run]');
373
- process.exit(command ? 0 : 2);
374
- }
375
- if (command === 'health') return healthCommand();
376
- if (command === 'setup') return setupCommand();
377
- if (command === 'pair') return pairCommand();
378
- if (command === 'login') return loginCommand();
379
- if (command === 'runtime install' || command === 'runtime status') return runtimeCommand(command.split(' ')[1]);
380
- if (command === 'service install' || command === 'service uninstall') return serviceCommand(command.split(' ')[1]);
381
- if (command === 'update check' || command === 'update run') return updateCommand(command.split(' ')[1]);
382
- return webCommand();
383
- };
384
-
385
- main().catch((error) => { console.error(`❌ ${error.message}`); process.exit(1); });