@ran-sh/dsh-crew 0.3.4 → 0.3.6

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
@@ -59,7 +59,14 @@ The globally installed package is only the launcher/manager. The actual Crew run
59
59
 
60
60
  > Known compatibility issue: transient `npx @ran-sh/dsh-crew …` execution is currently unreliable on some npm versions (npm/cli#9870: the npx cache bin is not put on the spawned command PATH). Until that upstream fix reaches your npm, use the global-launcher flow above instead.
61
61
 
62
- An update of the managed payload does not require a clone or rebuild; `dsh-crew update` resolves the newest permitted package from your configured npm registry (or an explicit `--candidate <path>` override), validates it under Crew-owned state, and switches only after validation succeeds. The global launcher intentionally does not self-replace; after an update the CLI prints the exact one-line command to refresh it.
62
+ An update of the managed payload does not require a clone or rebuild. When the launcher and payload versions match, `dsh-crew update` resolves the newest permitted package from your configured npm registry (or an explicit `--candidate <path>` override). When the running launcher is newer, its validated package payload is used first to converge the older managed payload. Every path stages and validates before switching. If the managed payload is newer than the launcher, it is never downgraded and the CLI prints the exact launcher-refresh command.
63
+
64
+ > Migration boundary for legacy `<= 0.3.3`: those immutable launchers cannot discover newer registry versions, so their old update behavior cannot be retroactively fixed. Refresh the launcher first, then converge the managed payload—no source checkout is required:
65
+ >
66
+ > ```bash
67
+ > npm install -g @ran-sh/dsh-crew@latest
68
+ > dsh-crew update
69
+ > ```
63
70
 
64
71
  Developer / source setup (alternative path):
65
72
 
package/README.zh.md CHANGED
@@ -59,7 +59,14 @@ dsh-crew uninstall # 加 --purge 才会同时删除配置/备份
59
59
 
60
60
  > 已知兼容性问题:部分 npm 版本下,临时 `npx @ran-sh/dsh-crew …` 执行不可靠(npm/cli#9870:npx 缓存中的 bin 未加入子进程 PATH)。在该上游修复可用之前,请使用上面的全局启动器方式。
61
61
 
62
- 托管载荷的更新无需克隆或构建:`dsh-crew update` 会从你配置的 npm registry 解析最新允许版本(或用 `--candidate <path>` 显式指定),在 Crew 自有状态下校验后再切换。全局启动器不会自我替换;更新完成后 CLI 会打印刷新启动器的准确命令。
62
+ 托管载荷的更新无需克隆或构建。启动器与载荷版本相同时,`dsh-crew update` 会从你配置的 npm registry 解析最新允许版本(或用 `--candidate <path>` 显式指定);当前启动器较新时,会优先用这个已校验的启动器包收敛旧载荷。所有路径都会先暂存、校验,再切换。托管载荷较新时绝不降级,CLI 会打印刷新启动器的准确命令。
63
+
64
+ > 旧版 `<= 0.3.3` 的迁移边界:这些不可变的旧启动器无法发现更新的 registry 版本,其旧 update 行为无法被追溯修复。受支持的迁移方式是先刷新启动器,再收敛托管载荷;无需源码检出:
65
+ >
66
+ > ```bash
67
+ > npm install -g @ran-sh/dsh-crew@latest
68
+ > dsh-crew update
69
+ > ```
63
70
 
64
71
  开发者 / 源码安装(备选路径):
65
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ran-sh/dsh-crew",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "main": "./src/hub/entry.mjs",
6
6
  "bin": {
@@ -1,10 +1,11 @@
1
- // v0.3.3 npx-managed lifecycle: install / status / update / uninstall.
1
+ // Packaged global-launcher lifecycle: install / status / update / uninstall.
2
2
  //
3
3
  // Public UX:
4
- // npx @ran-sh/dsh-crew@latest install|status|update|uninstall
4
+ // npm install -g @ran-sh/dsh-crew@latest
5
+ // dsh-crew install|status|update|uninstall
5
6
  //
6
- // An npx invocation runs from a transient package-manager extraction/cache
7
- // path. This module therefore persists the already-built package payload into
7
+ // A package-manager launcher may run from a replaceable global/cache path.
8
+ // This module therefore persists the already-built package payload into
8
9
  // Crew-owned state BEFORE registering it with the Harness profile, so the
9
10
  // registration never depends on the cache, tarball, or temp extraction dir:
10
11
  //
@@ -124,7 +125,10 @@ function writeCurrentPointer({ home, name, version, path }) {
124
125
 
125
126
  function listPackageEdges(manifest) {
126
127
  // Returns [{name, optional}] covering dependencies and
127
- // optionalDependencies; platform-specific optional bits stay non-fatal.
128
+ // required peerDependencies. Packages in the DSH cohort use peers for
129
+ // shared protocol/runtime modules, but a persisted Crew payload has no host
130
+ // node_modules to supply them, so their transitive peers are runtime edges.
131
+ // Platform-specific optional bits and optional peers stay non-fatal.
128
132
  const edges = [];
129
133
  for (const [name] of Object.entries(manifest?.dependencies ?? {})) {
130
134
  edges.push({ name, optional: false });
@@ -132,6 +136,10 @@ function listPackageEdges(manifest) {
132
136
  for (const [name] of Object.entries(manifest?.optionalDependencies ?? {})) {
133
137
  if (!edges.some((edge) => edge.name === name)) edges.push({ name, optional: true });
134
138
  }
139
+ for (const [name] of Object.entries(manifest?.peerDependencies ?? {})) {
140
+ if (edges.some((edge) => edge.name === name)) continue;
141
+ edges.push({ name, optional: manifest?.peerDependenciesMeta?.[name]?.optional === true });
142
+ }
135
143
  return edges.filter((edge) => typeof edge.name === 'string' && edge.name.trim());
136
144
  }
137
145
 
@@ -328,12 +336,41 @@ export function stageCandidatePayload({
328
336
  * start and answer --help from inside the staged tree alone.
329
337
  */
330
338
  export function defaultPayloadSmoke(dir, { nodePath = process.execPath, runner = spawnSync } = {}) {
331
- const result = runner(nodePath, [join(dir, 'bin', 'dsh-crew.mjs'), '--help'], {
339
+ const cli = runner(nodePath, [join(dir, 'bin', 'dsh-crew.mjs'), '--help'], {
340
+ encoding: 'utf8', timeout: 120_000, windowsHide: true,
341
+ env: { ...process.env },
342
+ });
343
+ if (cli.status !== 0) {
344
+ return { ok: false, detail: `bin --help exited ${cli.status}: ${(cli.stderr || cli.stdout || '').trim().slice(-300)}` };
345
+ }
346
+
347
+ const initialize = {
348
+ jsonrpc: '2.0',
349
+ id: 1,
350
+ method: 'initialize',
351
+ params: {
352
+ protocolVersion: '2024-11-05',
353
+ capabilities: {},
354
+ clientInfo: { name: 'dsh-crew-payload-smoke', version: '1.0.0' },
355
+ },
356
+ };
357
+ const mcp = runner(nodePath, [join(dir, 'src', 'server.mjs')], {
332
358
  encoding: 'utf8', timeout: 120_000, windowsHide: true,
359
+ input: JSON.stringify(initialize) + '\n',
333
360
  env: { ...process.env },
334
361
  });
335
- if (result.status === 0) return { ok: true };
336
- return { ok: false, detail: `bin --help exited ${result.status}: ${(result.stderr || result.stdout || '').trim().slice(-300)}` };
362
+ if (mcp.status !== 0) {
363
+ return { ok: false, detail: `MCP initialize exited ${mcp.status}: ${(mcp.stderr || mcp.stdout || '').trim().slice(-300)}` };
364
+ }
365
+ let response;
366
+ try {
367
+ response = String(mcp.stdout ?? '').split(/\r?\n/).filter(Boolean)
368
+ .map((line) => JSON.parse(line)).find((entry) => entry?.id === initialize.id);
369
+ } catch { /* handled by the fail-closed response check below */ }
370
+ if (response?.result?.serverInfo?.name !== 'dsh-crew') {
371
+ return { ok: false, detail: `MCP initialize returned no valid dsh-crew response: ${String(mcp.stdout ?? '').trim().slice(-300)}` };
372
+ }
373
+ return { ok: true };
337
374
  }
338
375
 
339
376
  // Keyword boundaries reject identifiers containing the keywords
@@ -587,16 +624,16 @@ export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } =
587
624
 
588
625
  /**
589
626
  * The globally installed launcher intentionally does not self-replace. When
590
- * it diverges from the freshly activated managed payload, say so explicitly
591
- * and give the exact refresh command instead of pretending both updated.
627
+ * the payload is newer, keep it authoritative and give the exact launcher
628
+ * refresh command. A newer launcher is handled by npxUpdate before this point.
592
629
  */
593
630
  function noteLauncherDivergence({ log, home = homedir() }) {
594
631
  const launcherVersion = readManifest(runningPackageRoot())?.version ?? null;
595
632
  let installedVersion = null;
596
633
  try { installedVersion = readCurrentPointer({ home }).version ?? null; } catch { installedVersion = null; }
597
- if (launcherVersion && installedVersion && launcherVersion !== installedVersion) {
634
+ if (launcherVersion && installedVersion && compareVersions(installedVersion, launcherVersion) > 0) {
598
635
  log('');
599
- log(`- note: the global launcher is still ${launcherVersion}; the managed Crew payload is now ${installedVersion}.`);
636
+ log(`- note: managed payload ${installedVersion} is newer than the global launcher ${launcherVersion}; the payload remains authoritative.`);
600
637
  log(` Refresh the launcher when convenient: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
601
638
  }
602
639
  }
@@ -705,10 +742,26 @@ export async function npxUpdate({
705
742
  runner = spawnSync,
706
743
  } = {}) {
707
744
  log('DSH Crew updater');
708
- // Candidate resolution: explicit path/dir override > legacy sourceRoot >
709
- // configured npm registry (@latest). Registry mode is a real update
710
- // operation and never downgrades an already-newer managed payload.
711
- const resolved = resolveUpdateCandidate({ candidate: candidate ?? sourceRoot, spec, home, log, runner });
745
+ // Candidate resolution: explicit path/dir override > a newer validated
746
+ // running launcher > configured npm registry (@latest). This makes the
747
+ // supported legacy bootstrap (`npm install -g ...@latest`, then `update`)
748
+ // independent of registry propagation after the launcher refresh.
749
+ const explicitCandidate = candidate ?? sourceRoot;
750
+ const initialHealth = currentInstallationHealth({ home });
751
+ const launcherRoot = runningPackageRoot();
752
+ const launcherManifest = readManifest(launcherRoot);
753
+ const launcherCanConverge = explicitCandidate === undefined
754
+ && initialHealth.pointer?.version
755
+ && launcherManifest?.name === UPDATE_PACKAGE_NAME
756
+ && launcherManifest?.version
757
+ && compareVersions(launcherManifest.version, initialHealth.pointer.version) > 0;
758
+ let resolved;
759
+ if (launcherCanConverge) {
760
+ log(`- newer launcher ${launcherManifest.version}; converging managed payload ${initialHealth.pointer.version} before registry resolution`);
761
+ resolved = { ok: true, sourceRoot: launcherRoot, version: launcherManifest.version, cleanup: null };
762
+ } else {
763
+ resolved = resolveUpdateCandidate({ candidate: explicitCandidate, spec, home, log, runner });
764
+ }
712
765
  if (!resolved.ok) {
713
766
  log(`✗ candidate resolution failed (${resolved.code})${resolved.detail ? `: ${resolved.detail}` : ''}`);
714
767
  return { ok: false, error: `candidate resolution failed (${resolved.code})` };
@@ -729,8 +782,9 @@ export async function npxUpdate({
729
782
  return { ok: true, idempotent: true, version: manifest.version, path: health.pointer.path };
730
783
  }
731
784
 
732
- if (health.installed && health.healthy && compareVersions(manifest.version, health.pointer.version) < 0 && (candidate ?? sourceRoot) === undefined) {
733
- log(`- registry latest (${manifest.version}) is not newer than the installed payload (${health.pointer.version}); nothing to update`);
785
+ if (health.installed && health.healthy && compareVersions(manifest.version, health.pointer.version) < 0) {
786
+ const source = explicitCandidate === undefined && !launcherCanConverge ? 'registry latest' : 'candidate';
787
+ log(`- ${source} (${manifest.version}) is not newer than the installed payload (${health.pointer.version}); nothing to update`);
734
788
  const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest: readManifest(health.pointer.path), log, installer });
735
789
  if (!activated) return { ok: false, error: 'activation failed' };
736
790
  return { ok: true, idempotent: true, version: health.pointer.version, path: health.pointer.path };
@@ -845,8 +899,14 @@ export function npxStatus({
845
899
  log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
846
900
  log(`Installed DSH Crew payload: ${installedLine}`);
847
901
  if (candidateVersion && installedVersion && candidateVersion !== installedVersion) {
848
- log(`- launcher and managed payload differ; the payload is authoritative for runtime behavior.`);
849
- log(` Refresh the launcher with: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
902
+ const direction = compareVersions(candidateVersion, installedVersion);
903
+ if (direction > 0) {
904
+ log(`- launcher ${candidateVersion} is newer than the managed payload ${installedVersion}.`);
905
+ log(' Run: dsh-crew update');
906
+ } else {
907
+ log(`- managed payload ${installedVersion} is newer than the launcher ${candidateVersion}; the payload remains authoritative.`);
908
+ log(` Refresh the launcher with: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
909
+ }
850
910
  }
851
911
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
852
912
  log(`Codex Desktop integration: ${codex}`);
@@ -8,7 +8,7 @@
8
8
  // Keep this module pure and dependency-free so Hub, MCP and tests all use the
9
9
  // exact same compatibility rules.
10
10
 
11
- export const RUNTIME_VERSION = '0.3.4';
11
+ export const RUNTIME_VERSION = '0.3.6';
12
12
  export const HUB_PROTOCOL_VERSION = 1;
13
13
 
14
14
  export const HUB_CAPABILITIES = Object.freeze([