@whyshu/argent 0.0.0-stage → 0.1.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 CHANGED
@@ -1,3 +1,22 @@
1
- # Temporary Holding Version
1
+ # @whyshu/argent(启动器)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 一条命令启动**打包好的 dsh + Argent 插件/角色预设**,并管理本机 runner 常驻:
4
+
5
+ ```bash
6
+ npx -y @whyshu/argent login --server-url https://whyshu.com --phone <手机号> # 首跑登录(短信验证码,写 600 auth.json)
7
+ npx -y @whyshu/argent pair --from-login --tenant ly-0001 # 用登录态签发 runner token(写 600 runner.json)
8
+ npx -y @whyshu/argent web # 生成/复用 profile 并启动 dsh web(默认 http://127.0.0.1:3080)
9
+ npx -y @whyshu/argent health # 版本/宿主/profile/端口/runner 配置自检
10
+ npx -y @whyshu/argent setup # 只生成/更新 $DSH_HOME/profiles/argent,不启动
11
+ npx -y @whyshu/argent service install # 生成 launchd(macOS)/ systemd(Linux)常驻 P4 runner
12
+ ```
13
+
14
+ - 启动器 **exact pin** `@deepseek-ai/dsh@0.2.1-rc.2`,避免混合版本事故;
15
+ - profile 由启动器生成:bundles(base/web-app/argent-presets,可用 `--bundles a,b` 追加)+ 用户层 patch(默认角色 preset);
16
+ - `setup` 会自动执行 `dsh plugin --profile argent install` 装配依赖(`--no-install` 可跳过);
17
+ - 两种装配模式:本地仓库(`ARGENT_REPO`/`--repo`,插件用 `file:`)与**发布模式**(无本地 plugins/ 时用启动器 package.json 的 npm 版本);
18
+ - 首跑写入 `$DSH_HOME/argent/runner.json`(600,含 serverUrl/tenantAppId/runnerToken);
19
+ - ⛔ 凭据不进包、不进日志;发布与客户机重装由交付方执行。
20
+
21
+ 本地仓库联调:`ARGENT_REPO=/path/to/argent-dsh node launcher/bin/argent.mjs setup --dsh-home /tmp/x`
22
+ (从仓库 `plugins/` 用 `file:` 装配;正式发布时改用 npm 依赖解析)。
package/bin/argent.mjs ADDED
@@ -0,0 +1,204 @@
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
+ *
10
+ * 公共参数:--dsh-home <dir> --profile <name> --repo <dir> --json --dry-run
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs';
14
+ import { join } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import {
17
+ DSH_EXACT_VERSION, PROFILE_NAME, PROD_BUNDLES, buildLaunchdPlist, buildProfile, dshHomeOf,
18
+ depsInstalled, health, installDeps, loginWithPhone, pairRunner, probeDshVersion, readAuthToken, repoRoot, runnerConfigPath, sendLoginCode, versionMatches,
19
+ } from '../lib/index.mjs';
20
+
21
+ const argv = process.argv.slice(2);
22
+ const has = (f) => argv.includes(f);
23
+ const val = (f, d = '') => { const i = argv.indexOf(f); return i > -1 ? (argv[i + 1] ?? d) : d; };
24
+ const JSON_OUT = has('--json');
25
+ const DRY_RUN = has('--dry-run');
26
+
27
+ const command = (() => {
28
+ const positional = argv.filter((a) => !a.startsWith('--') && !['-h'].includes(a));
29
+ const i = argv.findIndex((a) => a === 'web' || a === 'health' || a === 'setup' || a === 'pair' || a === 'login' || a === 'service');
30
+ if (i < 0) return '';
31
+ if (argv[i] === 'service') return `service ${argv[i + 1] || ''}`.trim();
32
+ return argv[i];
33
+ })();
34
+
35
+ const opts = {
36
+ dshHome: dshHomeOf(val('--dsh-home')),
37
+ profile: val('--profile', PROFILE_NAME),
38
+ repo: repoRoot({ repo: val('--repo') }),
39
+ port: Number(val('--port', '3080')) || 3080,
40
+ extraBundles: (() => { const raw = val('--bundles', '').split(',').map((x) => x.trim()).filter(Boolean); return raw.flatMap((x) => (x === 'prod' ? PROD_BUNDLES : [x])); })(),
41
+ };
42
+
43
+ function print(obj) {
44
+ if (JSON_OUT) console.log(JSON.stringify(obj, null, 2));
45
+ else for (const [k, v] of Object.entries(obj)) console.log(`${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`);
46
+ }
47
+
48
+ function ensureProfile({ install = true } = {}) {
49
+ const built = buildProfile({ dshHome: opts.dshHome, profile: opts.profile, repo: opts.repo, extraBundles: opts.extraBundles });
50
+ if (!built.ok) {
51
+ console.error(`❌ ${built.message}`);
52
+ process.exit(2);
53
+ }
54
+ if (install && !depsInstalled(opts.dshHome, opts.profile) && !has('--no-install')) {
55
+ console.error('→ 装配 profile 依赖(dsh plugin --profile ' + opts.profile + ' install)…');
56
+ const r = installDeps({ dshHome: opts.dshHome, profile: opts.profile });
57
+ if (!r.ok) {
58
+ console.error(`❌ profile 依赖装配失败:${r.message}`);
59
+ console.error(' 可手动重试:DSH_HOME=' + opts.dshHome + ' dsh plugin --profile ' + opts.profile + ' install');
60
+ process.exit(2);
61
+ }
62
+ console.error('✅ profile 依赖装配完成');
63
+ }
64
+ return built;
65
+ }
66
+
67
+ function checkVersionOrExit() {
68
+ const probe = probeDshVersion();
69
+ if (!probe.ok) {
70
+ console.error(`❌ 未找到 dsh(${probe.error})。安装器不代装宿主:请先按官方方式准备 dsh@${DSH_EXACT_VERSION}(npx @deepseek-ai/dsh@${DSH_EXACT_VERSION} web)。`);
71
+ process.exit(2);
72
+ }
73
+ if (!versionMatches(probe.version)) {
74
+ console.error(`❌ dsh 版本不匹配:当前 ${probe.version},要求 exact ${DSH_EXACT_VERSION}。`);
75
+ console.error(' 启动器按 exact pin 运行(防混合版本事故);请切换 dsh 版本后重试。');
76
+ process.exit(2);
77
+ }
78
+ return probe.version;
79
+ }
80
+
81
+ function launchWeb() {
82
+ const built = ensureProfile();
83
+ const version = checkVersionOrExit();
84
+ const args = ['--profile', opts.profile, ...(opts.port !== 3080 ? ['--port', String(opts.port)] : [])];
85
+ if (DRY_RUN) {
86
+ print({ ok: true, dryRun: true, dshVersion: version, profile: built.dir, command: `dsh ${args.join(' ')}` });
87
+ return;
88
+ }
89
+ const child = spawn('dsh', args, { stdio: 'inherit', env: process.env });
90
+ child.on('close', (code) => process.exit(code ?? 0));
91
+ child.on('error', (error) => { console.error(`❌ 启动 dsh 失败:${error.message}`); process.exit(1); });
92
+ }
93
+
94
+ async function webCommand() {
95
+ if (DRY_RUN || JSON_OUT) {
96
+ const built = ensureProfile();
97
+ const probe = probeDshVersion();
98
+ print({
99
+ ok: probe.ok && versionMatches(probe.version),
100
+ dshVersion: probe.ok ? probe.version : '', expected: DSH_EXACT_VERSION,
101
+ profile: built.dir, command: 'dsh --profile ' + opts.profile + (opts.port !== 3080 ? ' --port ' + opts.port : ''),
102
+ note: probe.ok && versionMatches(probe.version) ? '版本匹配,可启动' : '版本不匹配/未安装 dsh',
103
+ });
104
+ return;
105
+ }
106
+ launchWeb();
107
+ }
108
+
109
+ async function healthCommand() {
110
+ const h = health({ dshHome: opts.dshHome, profile: opts.profile, repo: opts.repo, port: opts.port });
111
+ if (JSON_OUT) console.log(JSON.stringify(h, null, 2));
112
+ else {
113
+ console.log(`dsh: ${h.dsh.ok ? h.dsh.version : '(未找到)'} ${h.dsh.matchesExact ? '✅ exact' : `❌ 期望 ${h.expectedDsh}`}`);
114
+ console.log(`profile: ${h.profile.dir} ${h.profile.packageJson && h.profile.patch ? '✅' : '❌ 未生成(跑 argent setup)'}`);
115
+ console.log(`runner 配置: ${h.runner.configPresent ? `✅ ${h.runner.configPath}(${h.runner.mode})` : `— 未配置(${h.runner.configPath})`}`);
116
+ console.log(`web: ${h.webUrl}`);
117
+ console.log(h.ok ? '✅ 可启动' : '❌ 有前置未满足');
118
+ }
119
+ process.exit(h.ok ? 0 : 1);
120
+ }
121
+
122
+ async function readLine(promptText) {
123
+ const { createInterface } = await import('node:readline/promises');
124
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
125
+ try { return (await rl.question(promptText)).trim(); } finally { rl.close(); }
126
+ }
127
+
128
+ async function loginCommand() {
129
+ const serverUrl = val('--server-url');
130
+ const phone = val('--phone');
131
+ if (!serverUrl || !phone) { console.error('用法:argent login --server-url <url> --phone <手机号> [--code <验证码>]'); process.exit(2); }
132
+ let code = val('--code');
133
+ if (!code) {
134
+ const sent = await sendLoginCode({ serverUrl, phone });
135
+ if (!sent.ok) { console.error(`❌ 发送验证码失败:${sent.message}`); process.exit(2); }
136
+ if (sent.devCode) console.error(`(开发环境验证码:${sent.devCode})`);
137
+ code = await readLine('请输入短信验证码:');
138
+ }
139
+ const r = await loginWithPhone({ serverUrl, phone, code, dshHome: opts.dshHome });
140
+ if (!r.ok) { console.error(`❌ 登录失败:${r.message}`); process.exit(2); }
141
+ print({ ok: true, auth: r.path, serverUrl: r.serverUrl, phone: r.phone, tokenTail: `…${r.tokenTail}`, next: '运行 argent pair --from-login --tenant <appId>' });
142
+ }
143
+
144
+ async function pairCommand() {
145
+ const r = await pairRunner({
146
+ serverUrl: val('--server-url'),
147
+ tenantAppId: val('--tenant') || val('--tenant-app-id'),
148
+ adminToken: val('--admin-token') || (has('--from-login') ? (readAuthToken(opts.dshHome)?.token || '') : ''),
149
+ runnerToken: val('--runner-token'),
150
+ runnerId: val('--runner-id'),
151
+ label: val('--label', 'argent-launcher'),
152
+ dshHome: opts.dshHome,
153
+ });
154
+ if (!r.ok) { console.error(`❌ ${r.message}`); process.exit(2); }
155
+ print({ ok: true, config: r.configPath, serverUrl: r.serverUrl, tenantAppId: r.tenantAppId, runnerId: r.runnerId, tokenTail: `…${r.tokenTail}` });
156
+ }
157
+
158
+ function setupCommand() {
159
+ const built = ensureProfile();
160
+ print({ ok: true, profile: built.dir, bundles: built.bundles.join(', '), plugins: built.plugins.join(', '), preset: built.preset, depsInstalled: depsInstalled(opts.dshHome, opts.profile) });
161
+ }
162
+
163
+ function serviceCommand(sub) {
164
+ const plist = buildLaunchdPlist({ dshHome: opts.dshHome, repo: opts.repo });
165
+ const outDir = val('--plist-dir', '');
166
+ const targetDir = outDir || join(homedir(), 'Library', 'LaunchAgents');
167
+ const target = join(targetDir, plist.file);
168
+ if (sub === 'uninstall') {
169
+ if (existsSync(target)) { rmSync(target); print({ ok: true, removed: target }); } else print({ ok: true, removed: '', note: '未安装' });
170
+ return;
171
+ }
172
+ if (sub !== 'install') { console.error('用法:argent service install|uninstall [--print-only|--plist-dir <dir>]'); process.exit(2); }
173
+ if (!plist.script) {
174
+ console.error('❌ 找不到 scripts/argent-runner.mjs(发布包应内置 runner;本地联调请给 --repo)');
175
+ process.exit(2);
176
+ }
177
+ if (has('--print-only')) { console.log(plist.content); return; }
178
+ mkdirSync(targetDir, { recursive: true });
179
+ writeFileSync(target, plist.content);
180
+ print({
181
+ ok: true, plist: target, label: plist.label,
182
+ next: [
183
+ `launchctl unload ${target} 2>/dev/null || true`,
184
+ `launchctl load -w ${target}`,
185
+ `# 日志:${join(opts.dshHome, 'argent', 'runner', 'logs')}`,
186
+ ].join('\n'),
187
+ runnerConfig: runnerConfigPath(opts.dshHome),
188
+ });
189
+ }
190
+
191
+ const main = async () => {
192
+ if (!command || has('-h') || has('--help')) {
193
+ console.log('用法:argent <web|health|setup|login|pair|service install|service uninstall> [--dsh-home <dir>] [--json] [--dry-run]');
194
+ process.exit(command ? 0 : 2);
195
+ }
196
+ if (command === 'health') return healthCommand();
197
+ if (command === 'setup') return setupCommand();
198
+ if (command === 'pair') return pairCommand();
199
+ if (command === 'login') return loginCommand();
200
+ if (command === 'service install' || command === 'service uninstall') return serviceCommand(command.split(' ')[1]);
201
+ return webCommand();
202
+ };
203
+
204
+ main().catch((error) => { console.error(`❌ ${error.message}`); process.exit(1); });
package/lib/index.mjs ADDED
@@ -0,0 +1,274 @@
1
+ /**
2
+ * @whyshu/argent 启动器内核(纯函数,便于单测;bin 只做 CLI 编排)。
3
+ *
4
+ * 职责:
5
+ * · exact pin 的 dsh 版本(0.2.1-rc.2)——启动前校验,避免混合版本事故;
6
+ * · 生成/更新 $DSH_HOME/profiles/argent(bundles + 用户层 patch)——本地仓库用 file: 装配;
7
+ * · runner 配置路径与 launchd/systemd 常驻单元生成;
8
+ * · 状态自检(版本/profile/runner 配置/端口)。
9
+ * ⛔ 不读写任何凭据内容(runner.json 由首跑引导写,权限 600)。
10
+ */
11
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from 'node:fs';
12
+ import { spawnSync } from 'node:child_process';
13
+ import { homedir, hostname as osHostname } from 'node:os';
14
+ import { dirname, join, resolve } from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ export const DSH_EXACT_VERSION = '0.2.1-rc.2';
18
+ export const PROFILE_NAME = 'argent';
19
+ export const BASE_BUNDLES = [
20
+ '@deepseek-ai/dsh-base',
21
+ '@deepseek-ai/dsh-web-app',
22
+ '@whyshu/argent-presets',
23
+ ];
24
+ export const DEFAULT_PRESET = 'argent-general';
25
+ /** 生产 profile 追加 bundles(--bundles prod 展开;版本见启动器 package.json 依赖) */
26
+ export const PROD_BUNDLES = [
27
+ '@whyshu/argent-core', '@whyshu/argent-dsh', '@whyshu/argent-skin',
28
+ '@whyshu/argent-gateway', '@whyshu/argent-flow', '@whyshu/argent-ml',
29
+ '@michengai/dsh-im-connect', '@linxin666/dsh-client-ui-task-board',
30
+ ];
31
+ export const RUNNER_LABEL = 'com.whyshu.argent-runner';
32
+
33
+ /** 仓库根:launcher/lib → ../../(本地联调);发布包内由 ARGENT_REPO 覆盖 */
34
+ export function repoRoot({ repo } = {}) {
35
+ if (repo) return resolve(repo);
36
+ if (process.env.ARGENT_REPO) return resolve(process.env.ARGENT_REPO);
37
+ return resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
38
+ }
39
+
40
+ export function dshHomeOf(value) {
41
+ return resolve(value || process.env.DSH_HOME || join(homedir(), '.dsh'));
42
+ }
43
+
44
+ /** 解析 `dsh --version` 输出(只取第一行,去掉 v 前缀) */
45
+ export function parseVersion(text) {
46
+ const line = String(text || '').trim().split('\n')[0] || '';
47
+ return line.replace(/^v/, '').trim();
48
+ }
49
+
50
+ export function versionMatches(version, expected = DSH_EXACT_VERSION) {
51
+ return parseVersion(version) === parseVersion(expected);
52
+ }
53
+
54
+ /** 跑 `dsh --version`(可注入 bin;默认 PATH 上的 dsh) */
55
+ export function probeDshVersion({ dshBin = 'dsh', spawn = spawnSync } = {}) {
56
+ const r = spawn(dshBin, ['--version'], { encoding: 'utf8', timeout: 15000 });
57
+ if (r.error || r.status !== 0) return { ok: false, version: '', error: r.error?.message || `exit ${r.status}` };
58
+ return { ok: true, version: parseVersion(r.stdout), error: '' };
59
+ }
60
+
61
+ /** 仓库内插件目录(本地 file: 装配用;发布包改为其自身 node_modules) */
62
+ export function pluginDirs({ repo = repoRoot() } = {}) {
63
+ const candidates = {
64
+ '@whyshu/argent-presets': join(repo, 'plugins', 'argent-presets'),
65
+ };
66
+ const out = {};
67
+ for (const [name, dir] of Object.entries(candidates)) {
68
+ if (existsSync(join(dir, 'package.json'))) out[name] = dir;
69
+ }
70
+ return out;
71
+ }
72
+
73
+ /** 启动器自身依赖里的版本(发布模式:本地没有 plugins/ 时改用 npm 依赖) */
74
+ function launcherDependencyVersions() {
75
+ try {
76
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
77
+ const deps = JSON.parse(readFileSync(pkgPath, 'utf8')).dependencies || {};
78
+ return deps;
79
+ } catch { return {}; }
80
+ }
81
+
82
+ export function profileDir(dshHome, profile = PROFILE_NAME) {
83
+ return join(dshHome, 'profiles', profile);
84
+ }
85
+
86
+ /** 生成/更新 profile(幂等):package.json(file: 依赖 + bundles)+ cordis.patch.yml(用户层) */
87
+ export function buildProfile({ dshHome = dshHomeOf(), profile = PROFILE_NAME, repo = repoRoot(), extraBundles = [] } = {}) {
88
+ const dir = profileDir(dshHome, profile);
89
+ mkdirSync(dir, { recursive: true });
90
+ const plugins = pluginDirs({ repo });
91
+ const npmVersions = launcherDependencyVersions();
92
+ const bundles = [...BASE_BUNDLES, ...(extraBundles || [])];
93
+ const deps = {};
94
+ const missing = [];
95
+ for (const name of bundles.filter((b) => !b.startsWith('@deepseek-ai/'))) {
96
+ if (plugins[name]) deps[name] = `file:${plugins[name]}`;
97
+ else if (npmVersions[name]) deps[name] = npmVersions[name];
98
+ else missing.push(name);
99
+ }
100
+ if (missing.length > 0) {
101
+ return { ok: false, code: 'plugins_missing', message: `找不到插件:${missing.join('、')}(本地无 plugins/ 且启动器 package.json 未声明依赖)`, dir };
102
+ }
103
+ // 非 @whyshu 的 bundle(dsh 官方)由宿主管,不进 profile dependencies
104
+ const pkg = {
105
+ name: `dsh-profile-${profile}`,
106
+ private: true,
107
+ dependencies: deps,
108
+ dsh: { profile: { bundles } },
109
+ };
110
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
111
+ const patch = [
112
+ '# @whyshu/argent 生成(幂等;手工改动会在此文件重生成时被覆盖)',
113
+ '- id: agent-preset-registry',
114
+ ' config:',
115
+ ` default: ${DEFAULT_PRESET}`,
116
+ '',
117
+ ].join('\n');
118
+ writeFileSync(join(dir, 'cordis.patch.yml'), patch);
119
+ // pnpm 配置必须与现有 web profile 同口径:否则 pnpm 会尝试从 registry 自动装 peer(含未发布版本)⇒ 安装失败
120
+ writeFileSync(join(dir, 'pnpm-workspace.yaml'), 'packages:\n - .\nnodeLinker: hoisted\nautoInstallPeers: false\nallowBuilds:\n protobufjs: true\n');
121
+ return { ok: true, dir, plugins: Object.keys(plugins), bundles: [...bundles], preset: DEFAULT_PRESET };
122
+ }
123
+
124
+ export function depsInstalled(dshHome = dshHomeOf(), profile = PROFILE_NAME) {
125
+ return existsSync(join(profileDir(dshHome, profile), 'node_modules'));
126
+ }
127
+
128
+ /** 装配 profile 依赖:`dsh plugin --profile <p> install`(pnpm 在 profile 目录内执行) */
129
+ export function installDeps({ dshHome = dshHomeOf(), profile = PROFILE_NAME, dshBin = 'dsh', spawn = spawnSync } = {}) {
130
+ const r = spawn(dshBin, ['plugin', '--profile', profile, 'install'], {
131
+ cwd: profileDir(dshHome, profile), env: { ...process.env, DSH_HOME: dshHome }, encoding: 'utf8', timeout: 300000,
132
+ });
133
+ const out = `${r.stdout || ''}${r.stderr || ''}`.trim();
134
+ if (r.error || r.status !== 0) return { ok: false, status: r.status, message: out.slice(-500) || r.error?.message || 'install failed' };
135
+ return { ok: true, status: 0, tail: out.slice(-300) };
136
+ }
137
+
138
+ export function authPath(dshHome = dshHomeOf()) {
139
+ return join(dshHome, 'argent', 'auth.json');
140
+ }
141
+
142
+ /** 读取已登录账号 token(600 文件;不存在/损坏返回 null) */
143
+ export function readAuthToken(dshHome = dshHomeOf()) {
144
+ try {
145
+ const doc = JSON.parse(readFileSync(authPath(dshHome), 'utf8'));
146
+ return doc?.token ? doc : null;
147
+ } catch { return null; }
148
+ }
149
+
150
+ /** 发送短信验证码(首跑引导;开发环境服务端会返回 devCode) */
151
+ export async function sendLoginCode({ serverUrl, phone, fetchImpl = globalThis.fetch }) {
152
+ const base = String(serverUrl || '').replace(/\/+$/, '');
153
+ const res = await fetchImpl(`${base}/api/user/sms/send`, {
154
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ phone }),
155
+ });
156
+ const text = await res.text(); let data = null; try { data = JSON.parse(text); } catch { data = null; }
157
+ if (!res.ok) return { ok: false, code: 'send_failed', message: data?.error || `HTTP ${res.status}`, cooldown: data?.cooldown };
158
+ return { ok: true, cooldown: data?.cooldown, devCode: data?.devCode || data?.code || '' };
159
+ }
160
+
161
+ /** 手机号+验证码登录(保存 token 到 600 auth.json,供 pair 使用) */
162
+ export async function loginWithPhone({ serverUrl, phone, code, dshHome = dshHomeOf(), fetchImpl = globalThis.fetch }) {
163
+ const base = String(serverUrl || '').replace(/\/+$/, '');
164
+ const res = await fetchImpl(`${base}/api/user/login/phone`, {
165
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ phone, code }),
166
+ });
167
+ const text = await res.text(); let data = null; try { data = JSON.parse(text); } catch { data = null; }
168
+ const token = data?.token || data?.data?.token || '';
169
+ if (!res.ok || !token) return { ok: false, code: 'login_failed', message: data?.error || `HTTP ${res.status}` };
170
+ const path = authPath(dshHome);
171
+ mkdirSync(dirname(path), { recursive: true });
172
+ writeFileSync(path, JSON.stringify({ serverUrl: base, phone, token, savedAt: new Date().toISOString() }, null, 2) + '\n', { mode: 0o600 });
173
+ return { ok: true, path, phone, serverUrl: base, tokenTail: token.slice(-6) };
174
+ }
175
+
176
+ export function runnerConfigPath(dshHome = dshHomeOf()) {
177
+ return join(dshHome, 'argent', 'runner.json');
178
+ }
179
+
180
+ export function runnerScriptPath({ repo = repoRoot() } = {}) {
181
+ const candidate = join(repo, 'scripts', 'argent-runner.mjs');
182
+ return existsSync(candidate) ? candidate : '';
183
+ }
184
+
185
+ /** P4 runner 的 launchd plist(macOS);Linux 可换 systemd unit(TODO 发布前补) */
186
+ export function buildLaunchdPlist({ dshHome = dshHomeOf(), repo = repoRoot(), nodeBin = process.execPath } = {}) {
187
+ const script = runnerScriptPath({ repo });
188
+ const logDir = join(dshHome, 'argent', 'runner', 'logs');
189
+ return {
190
+ file: `${RUNNER_LABEL}.plist`,
191
+ content: `<?xml version="1.0" encoding="UTF-8"?>
192
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
193
+ <plist version="1.0">
194
+ <dict>
195
+ <key>Label</key><string>${RUNNER_LABEL}</string>
196
+ <key>ProgramArguments</key>
197
+ <array>
198
+ <string>${nodeBin}</string>
199
+ <string>${script || '<argent-runner.mjs>'}</string>
200
+ <string>--config</string>
201
+ <string>${runnerConfigPath(dshHome)}</string>
202
+ </array>
203
+ <key>RunAtLoad</key><true/>
204
+ <key>KeepAlive</key><true/>
205
+ <key>StandardOutPath</key><string>${join(logDir, 'launchd.out.log')}</string>
206
+ <key>StandardErrorPath</key><string>${join(logDir, 'launchd.err.log')}</string>
207
+ </dict>
208
+ </plist>
209
+ `,
210
+ label: RUNNER_LABEL,
211
+ script,
212
+ };
213
+ }
214
+
215
+ /** 状态自检(不启动任何东西;JSON 友好) */
216
+ export function health({ dshHome = dshHomeOf(), profile = PROFILE_NAME, repo = repoRoot(), dshBin = 'dsh', port = 3080 } = {}) {
217
+ const version = probeDshVersion({ dshBin });
218
+ const dir = profileDir(dshHome, profile);
219
+ const profilePackage = join(dir, 'package.json');
220
+ const profilePatch = join(dir, 'cordis.patch.yml');
221
+ const runnerCfg = runnerConfigPath(dshHome);
222
+ const runnerCfgMode = existsSync(runnerCfg) ? (statSync(runnerCfg).mode & 0o777).toString(8) : '';
223
+ return {
224
+ ok: version.ok && versionMatches(version.version) && existsSync(profilePackage) && existsSync(profilePatch),
225
+ expectedDsh: DSH_EXACT_VERSION,
226
+ dsh: { ok: version.ok, version: version.ok ? version.version : '', matchesExact: version.ok && versionMatches(version.version), error: version.error },
227
+ profile: { dir, packageJson: existsSync(profilePackage), patch: existsSync(profilePatch) },
228
+ runner: { configPath: runnerCfg, configPresent: existsSync(runnerCfg), mode: runnerCfgMode },
229
+ webUrl: `http://127.0.0.1:${port}`,
230
+ repo,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * 首跑配对:把 runner 配置写到 `$DSH_HOME/argent/runner.json`(600)。
236
+ * 两种来源:
237
+ * · adminToken:调服务端 `POST /tenants/:appId/runner-tokens` 现场签发(90 天、绑定 appId);
238
+ * · runnerToken:直接使用已签发的 token(由管理员线下交付)。
239
+ * ⛔ 返回值/日志不回显 token;写盘前校验端口与响应。
240
+ */
241
+ export async function pairRunner({
242
+ serverUrl, tenantAppId, adminToken = '', runnerToken = '',
243
+ runnerId = '', label = '', dshHome = dshHomeOf(), fetchImpl = globalThis.fetch,
244
+ }) {
245
+ const base = String(serverUrl || '').replace(/\/+$/, '');
246
+ const appId = String(tenantAppId || '').trim();
247
+ if (!base || !appId) return { ok: false, code: 'validation', message: 'serverUrl / tenantAppId 必填' };
248
+ if (!adminToken && !runnerToken) return { ok: false, code: 'validation', message: '需要 --admin-token 或 --runner-token 之一' };
249
+ let token = String(runnerToken || '').trim();
250
+ if (!token) {
251
+ const res = await fetchImpl(`${base}/api/argent/v1/tenants/${encodeURIComponent(appId)}/runner-tokens`, {
252
+ method: 'POST',
253
+ headers: { authorization: `Bearer ${String(adminToken).trim()}`, 'content-type': 'application/json' },
254
+ body: JSON.stringify({ label: String(label || 'argent-launcher') }),
255
+ });
256
+ const text = await res.text();
257
+ let data = null;
258
+ try { data = JSON.parse(text); } catch { data = null; }
259
+ if (!res.ok || !data?.data?.token) {
260
+ return { ok: false, code: 'issue_failed', message: `签发 runner token 失败:HTTP ${res.status} ${data?.error?.code || ''} ${data?.error?.message || ''}`.trim() };
261
+ }
262
+ token = data.data.token;
263
+ }
264
+ if (token.split('.').length !== 3) return { ok: false, code: 'token_shape', message: 'runner token 形态不正确(应为 JWT)' };
265
+ const cfgPath = runnerConfigPath(dshHome);
266
+ mkdirSync(dirname(cfgPath), { recursive: true });
267
+ const config = {
268
+ serverUrl: base, tenantAppId: appId, runnerToken: token,
269
+ runnerId: runnerId || `runner-${osHostname()}`,
270
+ leaseSec: 90, pollWaitMs: 20000, execTimeoutSec: 900,
271
+ };
272
+ writeFileSync(cfgPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
273
+ return { ok: true, configPath: cfgPath, serverUrl: base, tenantAppId: appId, runnerId: config.runnerId, tokenTail: token.slice(-6) };
274
+ }
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@whyshu/argent",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Argent 启动器:一条命令启动「打包好的 dsh + Argent 插件/角色预设」,并管理本机 runner 常驻。",
5
+ "type": "module",
6
+ "bin": {
7
+ "argent": "./bin/argent.mjs"
8
+ },
9
+ "exports": {
10
+ ".": "./lib/index.mjs",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "engines": {
14
+ "node": ">=20.0.0"
15
+ },
16
+ "dependencies": {
17
+ "@deepseek-ai/dsh": "0.2.1-rc.2",
18
+ "@whyshu/argent-presets": "0.1.1",
19
+ "@whyshu/argent-core": "0.1.5",
20
+ "@whyshu/argent-dsh": "0.1.0",
21
+ "@whyshu/argent-skin": "0.1.4",
22
+ "@whyshu/argent-gateway": "0.2.3",
23
+ "@whyshu/argent-flow": "0.1.0",
24
+ "@whyshu/argent-ml": "0.1.0",
25
+ "@michengai/dsh-im-connect": "1.0.2",
26
+ "@linxin666/dsh-client-ui-task-board": "0.4.7"
27
+ },
28
+ "files": [
29
+ "bin/",
30
+ "lib/",
31
+ "README.md"
32
+ ],
33
+ "license": "MIT",
34
+ "scripts": {
35
+ "test": "node --test test/"
36
+ }
37
+ }