@whyshu/argent 0.1.10 → 0.1.12

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
@@ -47,9 +47,12 @@ npx -y @whyshu/argent@latest update check --registry https://registry.npmmirror.
47
47
  1. 读取 npm `@whyshu/argent` 的 `latest` 版本;
48
48
  2. 备份 profile 的 `package.json` / patch / lock;
49
49
  3. 用目标版本启动器执行 `runtime install`;
50
- 4. 执行 `setup --force-install` 更新 profile 插件;
51
- 5. 执行 `health --json` 验收;
52
- 6. 返回 `restartRequired: true`,由空闲窗口重启宿主。
50
+ 4. **原地更新现有 profile 的插件版本**(不重建 profile、不丢 bundles);
51
+ 5. 用客户机**真实 dsh** 执行 `dsh plugin --profile argent install`;
52
+ 6. 用真实 dsh 执行 `health --json` 验收;
53
+ 7. 返回 `restartRequired: true`,由空闲窗口重启宿主。
54
+
55
+ ⛔ dsh 版本不等于启动器 exact pin 时,`update run` 会在改任何文件前停止;先升级宿主。
53
56
 
54
57
  失败不会静默:保留更新前 profile 清单备份,并打印分步重试命令。当前不做自动重启。
55
58
 
package/bin/argent.mjs CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  DSH_EXACT_VERSION, PROFILE_NAME, PROD_BUNDLES, buildLaunchdPlist, buildProfile, dshHomeOf, runnerScriptPath,
19
19
  depsInstalled, health, installDeps, loginWithPhone, pairRunner, probeDshVersion, readAuthToken, repoRoot, runnerConfigPath, sendLoginCode, versionMatches,
20
20
  engineRoot, materializeRuntime, checkForUpdate, compareVersions, launcherVersion,
21
+ PUBLISHED_BUNDLE_VERSIONS, resolveDshBin, updateProfilePluginVersions,
21
22
  } from '../lib/index.mjs';
22
23
 
23
24
  const argv = process.argv.slice(2);
@@ -42,6 +43,7 @@ const opts = {
42
43
  target: val('--target', join(homedir(), '.argent-dsh')),
43
44
  skillsTarget: has('--no-skills') ? '' : val('--skills-target', join(dshHomeOf(val('--dsh-home')), 'skills')),
44
45
  registry: val('--registry', process.env.ARGENT_NPM_REGISTRY || 'https://registry.npmjs.org'),
46
+ dshBin: val('--dsh-bin', resolveDshBin()),
45
47
  extraBundles: (() => { const raw = val('--bundles', '').split(',').map((x) => x.trim()).filter(Boolean); return raw.flatMap((x) => (x === 'prod' ? PROD_BUNDLES : [x])); })(),
46
48
  };
47
49
 
@@ -131,7 +133,7 @@ async function webCommand() {
131
133
  }
132
134
 
133
135
  async function healthCommand() {
134
- const h = health({ dshHome: opts.dshHome, profile: opts.profile, repo: opts.repo, port: opts.port });
136
+ const h = health({ dshHome: opts.dshHome, profile: opts.profile, repo: opts.repo, port: opts.port, dshBin: opts.dshBin });
135
137
  if (JSON_OUT) console.log(JSON.stringify(h, null, 2));
136
138
  else {
137
139
  console.log(`dsh: ${h.dsh.ok ? h.dsh.version : '(未找到)'} ${h.dsh.matchesExact ? '✅ exact' : `❌ 期望 ${h.expectedDsh}`}`);
@@ -223,7 +225,7 @@ async function updateCommand(sub) {
223
225
  }
224
226
  const current = launcherVersion()
225
227
  let remote
226
- try { remote = await checkForUpdate({ target: opts.target, registry: opts.registry }) }
228
+ try { remote = await checkForUpdate({ target: opts.target, registry: opts.registry, dshHome: opts.dshHome, profile: opts.profile }) }
227
229
  catch (e) { console.error(`❌ 检查更新失败:${e.message}`); process.exit(2) }
228
230
  if (!remote.ok) {
229
231
  if (JSON_OUT) console.log(JSON.stringify(remote, null, 2)); else console.error(`❌ ${remote.message}`)
@@ -237,28 +239,40 @@ async function updateCommand(sub) {
237
239
  console.log(`当前启动器: ${current || '(未知)'}`)
238
240
  console.log(`npm latest: ${remote.latest}`)
239
241
  console.log(remote.updateAvailable ? '→ 有可用更新' : '→ 已是最新')
242
+ if (remote.pluginUpdates?.length) console.log('插件可更新:' + remote.pluginUpdates.map((x) => `${x.name} ${x.current}→${x.latest}`).join(';'))
240
243
  console.log(`runtime: ${remote.runtime || '(未物化)'} @ ${remote.runtimeTarget}`)
241
244
  console.log(`来源: ${remote.registry}`)
242
245
  }
243
246
  return
244
247
  }
245
- if (compareVersions(targetVersion, current) <= 0 && !has('--force')) {
246
- console.log(`已是最新(${current}),无需更新。`)
248
+ if (!remote.updateAvailable && !has('--force')) {
249
+ console.log(`已是最新(${current}${remote.pluginUpdates?.length ? ',插件也无更新' : ''}),无需更新。`)
247
250
  return
248
251
  }
249
252
  const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx'
250
253
  const common = ['--yes', `@whyshu/argent@${targetVersion}`]
251
254
  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']
255
+ const healthArgs = [...common, 'health', '--dsh-home', opts.dshHome, '--json', '--dsh-bin', opts.dshBin]
254
256
  if (DRY_RUN) {
255
- print({ ok: true, dryRun: true, current, targetVersion, commands: [installArgs, setupArgs, healthArgs].map((args) => [npx, ...args].join(' ')) })
257
+ print({ ok: true, dryRun: true, current, targetVersion, dshBin: opts.dshBin, commands: [
258
+ [npx, ...installArgs].join(' '),
259
+ `update profile plugin versions (${Object.keys(PUBLISHED_BUNDLE_VERSIONS).join(', ')})`,
260
+ `${opts.dshBin} plugin --profile ${opts.profile} install # DSH_HOME=${opts.dshHome}`,
261
+ [npx, ...healthArgs].join(' '),
262
+ ] })
256
263
  return
257
264
  }
258
265
  if (!has('--yes')) {
259
266
  console.error('⛔ 更新会改 runtime / profile 插件,必须显式加 --yes。先看计划:argent update run --dry-run')
260
267
  process.exit(2)
261
268
  }
269
+ // 宿主版本不匹配时绝不能先改 profile(新插件可能要求新 dsh;改完再 health 失败会留下半升级)。
270
+ const dshProbe = probeDshVersion({ dshBin: opts.dshBin })
271
+ if (!dshProbe.ok || !versionMatches(dshProbe.version)) {
272
+ console.error(`⛔ 宿主 dsh 版本不匹配:当前 ${dshProbe.ok ? dshProbe.version : '(未找到)'},启动器要求 ${DSH_EXACT_VERSION}。`)
273
+ console.error(' 请先按客户机 dsh 升级流程升级宿主,再重跑 update run;本次未改任何文件。')
274
+ process.exit(2)
275
+ }
262
276
  const stamp = new Date().toISOString().replace(/[:.]/g, '-')
263
277
  const profileDir = join(opts.dshHome, 'profiles', opts.profile)
264
278
  const backupDir = join(opts.dshHome, 'argent', 'backup', `update-${stamp}`)
@@ -270,12 +284,15 @@ async function updateCommand(sub) {
270
284
  const env = { NPM_CONFIG_REGISTRY: opts.registry }
271
285
  try {
272
286
  runCli(npx, installArgs, { env })
273
- runCli(npx, setupArgs, { env })
287
+ // 不调用 setup:setup 会重建 profile,可能丢掉客户现有 bundles/依赖结构(2026-10-22 实测)。
288
+ const prof = updateProfilePluginVersions({ dshHome: opts.dshHome, profile: opts.profile, versions: PUBLISHED_BUNDLE_VERSIONS })
289
+ if (!prof.ok) throw new Error(`更新 profile 插件版本失败:${prof.message}`)
290
+ runCli(opts.dshBin, ['plugin', '--profile', opts.profile, 'install'], { env: { ...env, DSH_HOME: opts.dshHome }, cwd: opts.dshHome })
274
291
  runCli(npx, healthArgs, { env })
275
292
  } catch (e) {
276
293
  console.error(`❌ 更新失败:${e.message}`)
277
294
  console.error(` 已保留更新前 profile 清单备份:${backupDir}`)
278
- console.error(' 可分步重试:npx -y @whyshu/argent@' + targetVersion + ' runtime install / setup --force-install;必要时用备份恢复 package.json/patch。')
295
+ console.error(' 可分步重试:npx -y @whyshu/argent@' + targetVersion + ' runtime install,再按 dsh plugin install 重试;必要时用备份恢复 package.json/patch。')
279
296
  process.exit(2)
280
297
  }
281
298
  print({ ok: true, from: current, to: targetVersion, runtimeTarget: opts.target, profile: profileDir, backupDir, restartRequired: true, next: '空闲窗口重启 dsh web(service install/launchd 或现有重启脚本)' })
package/lib/index.mjs CHANGED
@@ -403,18 +403,83 @@ export function readRuntimeVersion(target = join(homedir(), '.argent-dsh')) {
403
403
  } catch { return '' }
404
404
  }
405
405
 
406
+ /** 读 profile 当前插件版本(用于判断“runtime 已是最新、但插件还是旧版”)。 */
407
+ export function readProfilePluginVersions({ dshHome = dshHomeOf(), profile = PROFILE_NAME } = {}) {
408
+ try {
409
+ const pkg = JSON.parse(readFileSync(join(profileDir(dshHome, profile), 'package.json'), 'utf8'))
410
+ const deps = pkg && typeof pkg.dependencies === 'object' ? pkg.dependencies : {}
411
+ const out = {}
412
+ for (const [name, ver] of Object.entries(deps)) {
413
+ if (!name.startsWith('@whyshu/argent-')) continue
414
+ const v = String(ver || '')
415
+ out[name] = v.startsWith('file:') ? '' : v
416
+ }
417
+ return out
418
+ } catch { return {} }
419
+ }
420
+
406
421
  /** 检查更新(只读;不下载、不安装)。 */
407
422
  export async function checkForUpdate({
408
- launcherLib, target = join(homedir(), '.argent-dsh'), registry = process.env.ARGENT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY,
423
+ launcherLib, dshHome = dshHomeOf(), profile = PROFILE_NAME,
424
+ target = join(homedir(), '.argent-dsh'), registry = process.env.ARGENT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY,
409
425
  fetchImpl = globalThis.fetch,
410
426
  } = {}) {
411
427
  const current = launcherVersion(launcherLib)
412
428
  const remote = await latestLauncherVersion({ registry, fetchImpl })
413
429
  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
430
+ const installedPlugins = readProfilePluginVersions({ dshHome, profile })
431
+ if (!remote.ok) return { ok: false, code: remote.code, message: remote.message, current, runtime, installedPlugins, registry }
432
+ const pluginUpdates = []
433
+ for (const [name, want] of Object.entries(PUBLISHED_BUNDLE_VERSIONS)) {
434
+ const have = installedPlugins[name]
435
+ // 本地 file: 装配(开发机)不算可更新客户机;只比较明确版本号。
436
+ if (!have || !want) continue
437
+ if (compareVersions(want, have) > 0) pluginUpdates.push({ name, current: have, latest: want })
438
+ }
439
+ const launcherUpdate = current ? compareVersions(remote.version, current) > 0 : true
416
440
  return {
417
- ok: true, current, latest: remote.version, updateAvailable, registry: remote.registry,
418
- integrity: remote.integrity, tarball: remote.tarball, runtime, runtimeTarget: target,
441
+ ok: true, current, latest: remote.version,
442
+ updateAvailable: Boolean(launcherUpdate || pluginUpdates.length),
443
+ launcherUpdate, pluginUpdates, installedPlugins,
444
+ registry: remote.registry, integrity: remote.integrity, tarball: remote.tarball,
445
+ runtime, runtimeTarget: target,
446
+ }
447
+ }
448
+
449
+
450
+ /** 解析客户机**真实的** dsh(npx 启动器的依赖里也带一个 dsh,PATH 会被 npx 改写,不能直接用裸 `dsh`)。 */
451
+ export function resolveDshBin({ env = process.env, home = homedir() } = {}) {
452
+ const explicit = String(env.ARGENT_DSH_BIN || env.DSH_BIN || '').trim()
453
+ if (explicit && existsSync(explicit)) return explicit
454
+ const pathEntries = String(env.PATH || '').split(':').filter(Boolean)
455
+ .filter((p) => !p.includes('_npx') && !p.replace(/\/+$/, '').endsWith('node_modules/.bin'))
456
+ for (const dir of pathEntries) {
457
+ const p = join(dir, process.platform === 'win32' ? 'dsh.cmd' : 'dsh')
458
+ if (existsSync(p)) return p
459
+ }
460
+ for (const p of [
461
+ join(home, '.local', 'node', 'bin', 'dsh'),
462
+ '/opt/homebrew/bin/dsh',
463
+ '/usr/local/bin/dsh',
464
+ ]) if (existsSync(p)) return p
465
+ return 'dsh'
466
+ }
467
+
468
+ /** 更新 profile 里已存在的插件版本;不重建 profile、不丢 bundles/依赖结构。 */
469
+ export function updateProfilePluginVersions({
470
+ dshHome = dshHomeOf(), profile = PROFILE_NAME, versions = PUBLISHED_BUNDLE_VERSIONS,
471
+ } = {}) {
472
+ const file = join(profileDir(dshHome, profile), 'package.json')
473
+ let pkg = null
474
+ try { pkg = JSON.parse(readFileSync(file, 'utf8')) } catch { return { ok: false, code: 'profile_missing', message: `profile 不存在:${file}` } }
475
+ if (!pkg.dependencies || typeof pkg.dependencies !== 'object') return { ok: false, code: 'profile_shape', message: 'profile package.json 缺 dependencies' }
476
+ const changed = []
477
+ for (const [name, want] of Object.entries(versions || {})) {
478
+ const have = String(pkg.dependencies[name] || '')
479
+ if (!have || have.startsWith('file:') || have === want) continue
480
+ pkg.dependencies[name] = want
481
+ changed.push({ name, from: have, to: want })
419
482
  }
483
+ if (changed.length) writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n')
484
+ return { ok: true, file, changed }
420
485
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@whyshu/argent",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "Argent 启动器:一条命令启动「打包好的 dsh + Argent 插件/角色预设」,并管理本机 runner 常驻。",
5
5
  "type": "module",
6
6
  "bin": {