@whyshu/argent 0.1.8 → 0.1.9

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
@@ -22,6 +22,37 @@ npx -y @whyshu/argent service install # 生成 launchd(macOS)/ systemd(L
22
22
  本地仓库联调:`ARGENT_REPO=/path/to/argent-dsh node launcher/bin/argent.mjs setup --dsh-home /tmp/x`
23
23
  (从仓库 `plugins/` 用 `file:` 装配;正式发布时改用 npm 依赖解析)。
24
24
 
25
+ > 客户机更新的唯一口径(禁止 SSH/EasyTier、runtime 与插件的边界、自动更新设计)见
26
+ > [`docs/客户机更新通道-唯一口径.md`](../docs/客户机更新通道-唯一口径.md)。
27
+
28
+ ## 客户机更新(outbound npm 通道,2026-10-22)
29
+
30
+ 不再使用 SSH / EasyTier / `/dl`。客户机只访问 npm registry:
31
+
32
+ ```bash
33
+ # 只检查(不写任何东西)
34
+ npx -y @whyshu/argent@latest update check --json
35
+
36
+ # 真正更新 runtime + profile 插件(必须 --yes)
37
+ npx -y @whyshu/argent@latest update run --yes
38
+
39
+ # 看计划 / 指定版本 / 国内镜像
40
+ npx -y @whyshu/argent@latest update run --dry-run
41
+ npx -y @whyshu/argent@latest update run --yes --to 0.1.9
42
+ npx -y @whyshu/argent@latest update check --registry https://registry.npmmirror.com
43
+ ```
44
+
45
+ `update run` 会:
46
+
47
+ 1. 读取 npm `@whyshu/argent` 的 `latest` 版本;
48
+ 2. 备份 profile 的 `package.json` / patch / lock;
49
+ 3. 用目标版本启动器执行 `runtime install`;
50
+ 4. 执行 `setup --force-install` 更新 profile 插件;
51
+ 5. 执行 `health --json` 验收;
52
+ 6. 返回 `restartRequired: true`,由空闲窗口重启宿主。
53
+
54
+ 失败不会静默:保留更新前 profile 清单备份,并打印分步重试命令。当前不做自动重启。
55
+
25
56
  ## 运行资产更新(npm 通道,2026-10-19)
26
57
 
27
58
  客户机脚本/技能随 `@whyshu/argent` 的 runtime 依赖一起更新,不需要 `/dl/`:
package/bin/argent.mjs CHANGED
@@ -6,17 +6,18 @@
6
6
  * argent health 版本/宿主/profile/runner 自检(--json)
7
7
  * argent setup 只生成/更新 profile
8
8
  * argent service install|uninstall runner 常驻(macOS launchd;--print-only/--plist-dir)
9
+ * argent update check|run 客户机 outbound npm 更新(不依赖 SSH/EasyTier /dl)
9
10
  *
10
11
  * 公共参数:--dsh-home <dir> --profile <name> --repo <dir> --json --dry-run
11
12
  */
12
13
  import { spawn, spawnSync } from 'node:child_process';
13
- import { mkdirSync, writeFileSync, rmSync, existsSync, copyFileSync, chmodSync, readFileSync } from 'node:fs';
14
+ import { mkdirSync, writeFileSync, rmSync, existsSync, copyFileSync, chmodSync, readFileSync, cpSync } from 'node:fs';
14
15
  import { join, dirname } from 'node:path';
15
16
  import { homedir } from 'node:os';
16
17
  import {
17
18
  DSH_EXACT_VERSION, PROFILE_NAME, PROD_BUNDLES, buildLaunchdPlist, buildProfile, dshHomeOf, runnerScriptPath,
18
19
  depsInstalled, health, installDeps, loginWithPhone, pairRunner, probeDshVersion, readAuthToken, repoRoot, runnerConfigPath, sendLoginCode, versionMatches,
19
- engineRoot, materializeRuntime,
20
+ engineRoot, materializeRuntime, checkForUpdate, compareVersions, launcherVersion,
20
21
  } from '../lib/index.mjs';
21
22
 
22
23
  const argv = process.argv.slice(2);
@@ -27,9 +28,9 @@ const DRY_RUN = has('--dry-run');
27
28
 
28
29
  const command = (() => {
29
30
  const positional = argv.filter((a) => !a.startsWith('--') && !['-h'].includes(a));
30
- const i = argv.findIndex((a) => a === 'web' || a === 'health' || a === 'setup' || a === 'pair' || a === 'login' || a === 'service' || a === 'runtime');
31
+ const i = argv.findIndex((a) => a === 'web' || a === 'health' || a === 'setup' || a === 'pair' || a === 'login' || a === 'service' || a === 'runtime' || a === 'update');
31
32
  if (i < 0) return '';
32
- if (argv[i] === 'service' || argv[i] === 'runtime') return `${argv[i]} ${argv[i + 1] || ''}`.trim();
33
+ if (argv[i] === 'service' || argv[i] === 'runtime' || argv[i] === 'update') return `${argv[i]} ${argv[i + 1] || ''}`.trim();
33
34
  return argv[i];
34
35
  })();
35
36
 
@@ -40,6 +41,7 @@ const opts = {
40
41
  port: Number(val('--port', '3080')) || 3080,
41
42
  target: val('--target', join(homedir(), '.argent-dsh')),
42
43
  skillsTarget: has('--no-skills') ? '' : val('--skills-target', join(dshHomeOf(val('--dsh-home')), 'skills')),
44
+ registry: val('--registry', process.env.ARGENT_NPM_REGISTRY || 'https://registry.npmjs.org'),
43
45
  extraBundles: (() => { const raw = val('--bundles', '').split(',').map((x) => x.trim()).filter(Boolean); return raw.flatMap((x) => (x === 'prod' ? PROD_BUNDLES : [x])); })(),
44
46
  };
45
47
 
@@ -72,7 +74,7 @@ function ensureProfile({ install = true } = {}) {
72
74
  console.error(`❌ ${built.message}`);
73
75
  process.exit(2);
74
76
  }
75
- if (install && !depsInstalled(opts.dshHome, opts.profile) && !has('--no-install')) {
77
+ if (install && (has('--force-install') || !depsInstalled(opts.dshHome, opts.profile)) && !has('--no-install')) {
76
78
  console.error('→ 装配 profile 依赖(dsh plugin --profile ' + opts.profile + ' install)…');
77
79
  const r = installDeps({ dshHome: opts.dshHome, profile: opts.profile });
78
80
  if (!r.ok) {
@@ -199,6 +201,86 @@ function runtimeCommand(sub) {
199
201
  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 });
200
202
  }
201
203
 
204
+ function runCli(bin, args, { cwd = '', env = {} } = {}) {
205
+ const r = spawnSync(bin, args, {
206
+ cwd: cwd || undefined,
207
+ stdio: inherit_stdout() ? 'inherit' : 'pipe',
208
+ encoding: 'utf8',
209
+ env: { ...process.env, ...env },
210
+ timeout: 600000,
211
+ })
212
+ if (r.error) throw new Error(`${bin} ${args.join(' ')} 启动失败:${r.error.message}`)
213
+ if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出 ${r.status}${r.stderr ? `:${String(r.stderr).slice(-400)}` : ''}`)
214
+ return r
215
+ }
216
+
217
+ function inherit_stdout() { return !JSON_OUT }
218
+
219
+ async function updateCommand(sub) {
220
+ if (sub !== 'check' && sub !== 'run') {
221
+ console.error('用法:argent update check|run [--to <version>] [--yes] [--registry <url>] [--dsh-home <dir>] [--target <dir>] [--json] [--dry-run]');
222
+ process.exit(2)
223
+ }
224
+ const current = launcherVersion()
225
+ let remote
226
+ try { remote = await checkForUpdate({ target: opts.target, registry: opts.registry }) }
227
+ catch (e) { console.error(`❌ 检查更新失败:${e.message}`); process.exit(2) }
228
+ if (!remote.ok) {
229
+ if (JSON_OUT) console.log(JSON.stringify(remote, null, 2)); else console.error(`❌ ${remote.message}`)
230
+ process.exit(2)
231
+ }
232
+ const targetVersion = val('--to', remote.latest)
233
+ if (sub === 'check') {
234
+ const out = { ...remote, targetVersion, launcher: current, restartRequired: Boolean(remote.updateAvailable) }
235
+ if (JSON_OUT) console.log(JSON.stringify(out, null, 2))
236
+ else {
237
+ console.log(`当前启动器: ${current || '(未知)'}`)
238
+ console.log(`npm latest: ${remote.latest}`)
239
+ console.log(remote.updateAvailable ? '→ 有可用更新' : '→ 已是最新')
240
+ console.log(`runtime: ${remote.runtime || '(未物化)'} @ ${remote.runtimeTarget}`)
241
+ console.log(`来源: ${remote.registry}`)
242
+ }
243
+ return
244
+ }
245
+ if (compareVersions(targetVersion, current) <= 0 && !has('--force')) {
246
+ console.log(`已是最新(${current}),无需更新。`)
247
+ return
248
+ }
249
+ const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'
250
+ const common = ['--yes', `@whyshu/argent@${targetVersion}`]
251
+ const installArgs = [...common, 'runtime', 'install', '--target', opts.target, '--skills-target', opts.skillsTarget]
252
+ const setupArgs = [...common, 'setup', '--dsh-home', opts.dshHome, '--force-install']
253
+ const healthArgs = [...common, 'health', '--dsh-home', opts.dshHome, '--json']
254
+ if (DRY_RUN) {
255
+ print({ ok: true, dryRun: true, current, targetVersion, commands: [installArgs, setupArgs, healthArgs].map((args) => [npx, ...args].join(' ')) })
256
+ return
257
+ }
258
+ if (!has('--yes')) {
259
+ console.error('⛔ 更新会改 runtime / profile 插件,必须显式加 --yes。先看计划:argent update run --dry-run')
260
+ process.exit(2)
261
+ }
262
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
263
+ const profileDir = join(opts.dshHome, 'profiles', opts.profile)
264
+ const backupDir = join(opts.dshHome, 'argent', 'backup', `update-${stamp}`)
265
+ mkdirSync(backupDir, { recursive: true })
266
+ for (const f of ['package.json', 'cordis.patch.yml', 'pnpm-lock.yaml', 'pnpm-workspace.yaml']) {
267
+ const src = join(profileDir, f)
268
+ if (existsSync(src)) { mkdirSync(dirname(join(backupDir, f)), { recursive: true }); cpSync(src, join(backupDir, f)) }
269
+ }
270
+ const env = { NPM_CONFIG_REGISTRY: opts.registry }
271
+ try {
272
+ runCli(npx, installArgs, { env })
273
+ runCli(npx, setupArgs, { env })
274
+ runCli(npx, healthArgs, { env })
275
+ } catch (e) {
276
+ console.error(`❌ 更新失败:${e.message}`)
277
+ console.error(` 已保留更新前 profile 清单备份:${backupDir}`)
278
+ console.error(' 可分步重试:npx -y @whyshu/argent@' + targetVersion + ' runtime install / setup --force-install;必要时用备份恢复 package.json/patch。')
279
+ process.exit(2)
280
+ }
281
+ print({ ok: true, from: current, to: targetVersion, runtimeTarget: opts.target, profile: profileDir, backupDir, restartRequired: true, next: '空闲窗口重启 dsh web(service install/launchd 或现有重启脚本)' })
282
+ }
283
+
202
284
  function serviceCommand(sub) {
203
285
  const bundled = runnerScriptPath({ repo: opts.repo });
204
286
  // 固定 runner 路径:复制进 DSH_HOME,避免 launchd 指向 npx 临时缓存(缓存清理后失效)
@@ -238,7 +320,7 @@ function serviceCommand(sub) {
238
320
 
239
321
  const main = async () => {
240
322
  if (!command || has('-h') || has('--help')) {
241
- console.log('用法:argent <web|health|setup|login|pair|runtime install|runtime status|service install|service uninstall> [--dsh-home <dir>] [--target <dir>] [--skills-target <dir>|--no-skills] [--json] [--dry-run]');
323
+ 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]');
242
324
  process.exit(command ? 0 : 2);
243
325
  }
244
326
  if (command === 'health') return healthCommand();
@@ -247,6 +329,7 @@ const main = async () => {
247
329
  if (command === 'login') return loginCommand();
248
330
  if (command === 'runtime install' || command === 'runtime status') return runtimeCommand(command.split(' ')[1]);
249
331
  if (command === 'service install' || command === 'service uninstall') return serviceCommand(command.split(' ')[1]);
332
+ if (command === 'update check' || command === 'update run') return updateCommand(command.split(' ')[1]);
250
333
  return webCommand();
251
334
  };
252
335
 
package/lib/index.mjs CHANGED
@@ -346,3 +346,75 @@ export async function pairRunner({
346
346
  writeFileSync(cfgPath, JSON.stringify(config, null, 2) + '\n', { mode: 0o600 });
347
347
  return { ok: true, configPath: cfgPath, serverUrl: base, tenantAppId: appId, runnerId: config.runnerId, tokenTail: token.slice(-6) };
348
348
  }
349
+
350
+
351
+ // ── 客户机 outbound 更新(npm 唯一制品源;2026-10-22)────────────────
352
+ /** 启动器包名与默认 registry。客户机只做出站 HTTPS;不依赖 /dl、SSH、组网。 */
353
+ export const LAUNCHER_PACKAGE = '@whyshu/argent'
354
+ export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org'
355
+
356
+ /** 读取启动器自身版本(发布包 / 本地仓库均适用)。 */
357
+ export function launcherVersion(launcherLib = dirname(fileURLToPath(import.meta.url))) {
358
+ try {
359
+ const pkg = JSON.parse(readFileSync(join(launcherLib, '..', 'package.json'), 'utf8'))
360
+ return String(pkg.version || '')
361
+ } catch { return '' }
362
+ }
363
+
364
+ /** 比较 x.y.z(支持 prerelease 简单后缀;返回 -1/0/1)。 */
365
+ export function compareVersions(a, b) {
366
+ const parse = (v) => String(v || '').trim().replace(/^v/, '').split('-')[0].split('.').map((x) => Number(x) || 0)
367
+ const pa = parse(a); const pb = parse(b)
368
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
369
+ const d = (pa[i] || 0) - (pb[i] || 0)
370
+ if (d) return d > 0 ? 1 : -1
371
+ }
372
+ return 0
373
+ }
374
+
375
+ /** 从 npm registry 读取 @whyshu/argent 的 latest 版本(公开包,无需 token)。 */
376
+ export async function latestLauncherVersion({
377
+ registry = process.env.ARGENT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY,
378
+ packageName = LAUNCHER_PACKAGE,
379
+ fetchImpl = globalThis.fetch,
380
+ } = {}) {
381
+ const base = String(registry || DEFAULT_NPM_REGISTRY).replace(/\/+$/, '')
382
+ const encoded = String(packageName).replace('/', '%2F')
383
+ const url = `${base}/${encoded}/latest`
384
+ try {
385
+ const res = await fetchImpl(url, { headers: { accept: 'application/json' }, signal: AbortSignal.timeout(15000) })
386
+ const text = await res.text()
387
+ let data = null
388
+ try { data = JSON.parse(text) } catch { data = null }
389
+ if (!res.ok) return { ok: false, code: 'registry_http', message: `registry HTTP ${res.status}`, url }
390
+ const version = String(data?.version || data?.['dist-tags']?.latest || '').trim()
391
+ if (!version) return { ok: false, code: 'registry_shape', message: 'registry 未返回 version', url }
392
+ return { ok: true, version, integrity: String(data?.dist?.integrity || ''), tarball: String(data?.dist?.tarball || ''), registry: base, url }
393
+ } catch (e) {
394
+ return { ok: false, code: 'registry_fetch', message: e?.message || String(e), url }
395
+ }
396
+ }
397
+
398
+ /** 读取已物化 runtime 的版本(没有则空串)。 */
399
+ export function readRuntimeVersion(target = join(homedir(), '.argent-dsh')) {
400
+ try {
401
+ const d = JSON.parse(readFileSync(join(target, 'RUNTIME_VERSION.json'), 'utf8'))
402
+ return String(d?.version || '')
403
+ } catch { return '' }
404
+ }
405
+
406
+ /** 检查更新(只读;不下载、不安装)。 */
407
+ export async function checkForUpdate({
408
+ launcherLib, target = join(homedir(), '.argent-dsh'), registry = process.env.ARGENT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY,
409
+ fetchImpl = globalThis.fetch,
410
+ } = {}) {
411
+ const current = launcherVersion(launcherLib)
412
+ const remote = await latestLauncherVersion({ registry, fetchImpl })
413
+ const runtime = readRuntimeVersion(target)
414
+ if (!remote.ok) return { ok: false, code: remote.code, message: remote.message, current, runtime, registry }
415
+ const updateAvailable = current ? compareVersions(remote.version, current) > 0 : true
416
+ return {
417
+ ok: true, current, latest: remote.version, updateAvailable, registry: remote.registry,
418
+ integrity: remote.integrity, tarball: remote.tarball, runtime, runtimeTarget: target,
419
+ }
420
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whyshu/argent",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Argent 启动器:一条命令启动「打包好的 dsh + Argent 插件/角色预设」,并管理本机 runner 常驻。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,6 +25,6 @@
25
25
  ],
26
26
  "license": "MIT",
27
27
  "scripts": {
28
- "test": "node --test test/"
28
+ "test": "node --test test/*.test.mjs"
29
29
  }
30
30
  }