@ran-sh/dsh-crew 0.3.4 → 0.3.5

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.5",
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
  //
@@ -587,16 +588,16 @@ export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } =
587
588
 
588
589
  /**
589
590
  * 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.
591
+ * the payload is newer, keep it authoritative and give the exact launcher
592
+ * refresh command. A newer launcher is handled by npxUpdate before this point.
592
593
  */
593
594
  function noteLauncherDivergence({ log, home = homedir() }) {
594
595
  const launcherVersion = readManifest(runningPackageRoot())?.version ?? null;
595
596
  let installedVersion = null;
596
597
  try { installedVersion = readCurrentPointer({ home }).version ?? null; } catch { installedVersion = null; }
597
- if (launcherVersion && installedVersion && launcherVersion !== installedVersion) {
598
+ if (launcherVersion && installedVersion && compareVersions(installedVersion, launcherVersion) > 0) {
598
599
  log('');
599
- log(`- note: the global launcher is still ${launcherVersion}; the managed Crew payload is now ${installedVersion}.`);
600
+ log(`- note: managed payload ${installedVersion} is newer than the global launcher ${launcherVersion}; the payload remains authoritative.`);
600
601
  log(` Refresh the launcher when convenient: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
601
602
  }
602
603
  }
@@ -705,10 +706,26 @@ export async function npxUpdate({
705
706
  runner = spawnSync,
706
707
  } = {}) {
707
708
  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 });
709
+ // Candidate resolution: explicit path/dir override > a newer validated
710
+ // running launcher > configured npm registry (@latest). This makes the
711
+ // supported legacy bootstrap (`npm install -g ...@latest`, then `update`)
712
+ // independent of registry propagation after the launcher refresh.
713
+ const explicitCandidate = candidate ?? sourceRoot;
714
+ const initialHealth = currentInstallationHealth({ home });
715
+ const launcherRoot = runningPackageRoot();
716
+ const launcherManifest = readManifest(launcherRoot);
717
+ const launcherCanConverge = explicitCandidate === undefined
718
+ && initialHealth.pointer?.version
719
+ && launcherManifest?.name === UPDATE_PACKAGE_NAME
720
+ && launcherManifest?.version
721
+ && compareVersions(launcherManifest.version, initialHealth.pointer.version) > 0;
722
+ let resolved;
723
+ if (launcherCanConverge) {
724
+ log(`- newer launcher ${launcherManifest.version}; converging managed payload ${initialHealth.pointer.version} before registry resolution`);
725
+ resolved = { ok: true, sourceRoot: launcherRoot, version: launcherManifest.version, cleanup: null };
726
+ } else {
727
+ resolved = resolveUpdateCandidate({ candidate: explicitCandidate, spec, home, log, runner });
728
+ }
712
729
  if (!resolved.ok) {
713
730
  log(`✗ candidate resolution failed (${resolved.code})${resolved.detail ? `: ${resolved.detail}` : ''}`);
714
731
  return { ok: false, error: `candidate resolution failed (${resolved.code})` };
@@ -729,8 +746,9 @@ export async function npxUpdate({
729
746
  return { ok: true, idempotent: true, version: manifest.version, path: health.pointer.path };
730
747
  }
731
748
 
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`);
749
+ if (health.installed && health.healthy && compareVersions(manifest.version, health.pointer.version) < 0) {
750
+ const source = explicitCandidate === undefined && !launcherCanConverge ? 'registry latest' : 'candidate';
751
+ log(`- ${source} (${manifest.version}) is not newer than the installed payload (${health.pointer.version}); nothing to update`);
734
752
  const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest: readManifest(health.pointer.path), log, installer });
735
753
  if (!activated) return { ok: false, error: 'activation failed' };
736
754
  return { ok: true, idempotent: true, version: health.pointer.version, path: health.pointer.path };
@@ -845,8 +863,14 @@ export function npxStatus({
845
863
  log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
846
864
  log(`Installed DSH Crew payload: ${installedLine}`);
847
865
  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}`);
866
+ const direction = compareVersions(candidateVersion, installedVersion);
867
+ if (direction > 0) {
868
+ log(`- launcher ${candidateVersion} is newer than the managed payload ${installedVersion}.`);
869
+ log(' Run: dsh-crew update');
870
+ } else {
871
+ log(`- managed payload ${installedVersion} is newer than the launcher ${candidateVersion}; the payload remains authoritative.`);
872
+ log(` Refresh the launcher with: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
873
+ }
850
874
  }
851
875
  log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
852
876
  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.5';
12
12
  export const HUB_PROTOCOL_VERSION = 1;
13
13
 
14
14
  export const HUB_CAPABILITIES = Object.freeze([