@ran-sh/dsh-crew 0.3.3 → 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 +22 -10
- package/README.zh.md +22 -10
- package/package.json +1 -1
- package/src/install/npx-lifecycle.mjs +272 -50
- package/src/runtime-identity.mjs +1 -1
package/README.md
CHANGED
|
@@ -40,21 +40,33 @@ Codex Desktop / Claude Code
|
|
|
40
40
|
|
|
41
41
|
Prerequisites: **Node.js** (Git is needed only for coding-worker isolation).
|
|
42
42
|
|
|
43
|
-
Recommended:
|
|
43
|
+
Recommended: install a stable, package-manager-installed launcher and manage DSH Crew with it:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
npm install -g @ran-sh/dsh-crew@latest
|
|
47
|
+
dsh-crew install
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
Manage the installation later with the same CLI:
|
|
50
51
|
|
|
51
52
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
dsh-crew status
|
|
54
|
+
dsh-crew update
|
|
55
|
+
dsh-crew uninstall # add --purge to also remove config/backups
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
The globally installed package is only the launcher/manager. The actual Crew runtime/plugin payload is persisted under Crew-owned state (`~/.config/dsh-crew/app`) before Harness registration, so runtime behavior never depends on npm cache paths.
|
|
59
|
+
|
|
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
|
+
|
|
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
|
+
> ```
|
|
58
70
|
|
|
59
71
|
Developer / source setup (alternative path):
|
|
60
72
|
|
|
@@ -113,16 +125,16 @@ A coding worker receives a temporary git worktree at the requested revision, so
|
|
|
113
125
|
|
|
114
126
|
## Update / uninstall
|
|
115
127
|
|
|
116
|
-
|
|
128
|
+
The managed Crew payload updates in place (config, credentials, and backups are preserved; the candidate is staged and validated before switching):
|
|
117
129
|
|
|
118
130
|
```bash
|
|
119
|
-
|
|
131
|
+
dsh-crew update
|
|
120
132
|
```
|
|
121
133
|
|
|
122
|
-
Uninstall
|
|
134
|
+
Uninstall the managed payload and integrations:
|
|
123
135
|
|
|
124
136
|
```bash
|
|
125
|
-
|
|
137
|
+
dsh-crew uninstall
|
|
126
138
|
```
|
|
127
139
|
|
|
128
140
|
Update a source install instead with:
|
package/README.zh.md
CHANGED
|
@@ -40,21 +40,33 @@ Codex Desktop / Claude Code
|
|
|
40
40
|
|
|
41
41
|
需要:**Node.js**(编码 worker 隔离需要 **Git**)。
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
推荐安装稳定的包管理器全局启动器,并用它管理 DSH Crew:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
npm install -g @ran-sh/dsh-crew@latest
|
|
47
|
+
dsh-crew install
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
后续管理使用同一 CLI:
|
|
50
51
|
|
|
51
52
|
```bash
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
dsh-crew status
|
|
54
|
+
dsh-crew update
|
|
55
|
+
dsh-crew uninstall # 加 --purge 才会同时删除配置/备份
|
|
55
56
|
```
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
全局安装的包只是启动器/管理器;真正的 Crew 运行时/插件载荷会先持久化到 Crew 自有状态(`~/.config/dsh-crew/app`)再注册,运行时行为不依赖 npm 缓存路径。
|
|
59
|
+
|
|
60
|
+
> 已知兼容性问题:部分 npm 版本下,临时 `npx @ran-sh/dsh-crew …` 执行不可靠(npm/cli#9870:npx 缓存中的 bin 未加入子进程 PATH)。在该上游修复可用之前,请使用上面的全局启动器方式。
|
|
61
|
+
|
|
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
|
+
> ```
|
|
58
70
|
|
|
59
71
|
开发者 / 源码安装(备选路径):
|
|
60
72
|
|
|
@@ -113,16 +125,16 @@ Use ds-reviewer to review the implementation.
|
|
|
113
125
|
|
|
114
126
|
## 更新 / 卸载
|
|
115
127
|
|
|
116
|
-
|
|
128
|
+
托管 Crew 载荷使用 update 原地升级(配置、凭据和备份保留;候选包先暂存校验再切换):
|
|
117
129
|
|
|
118
130
|
```bash
|
|
119
|
-
|
|
131
|
+
dsh-crew update
|
|
120
132
|
```
|
|
121
133
|
|
|
122
|
-
|
|
134
|
+
卸载托管载荷与集成:
|
|
123
135
|
|
|
124
136
|
```bash
|
|
125
|
-
|
|
137
|
+
dsh-crew uninstall
|
|
126
138
|
```
|
|
127
139
|
|
|
128
140
|
源码安装的更新方式:
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Packaged global-launcher lifecycle: install / status / update / uninstall.
|
|
2
2
|
//
|
|
3
3
|
// Public UX:
|
|
4
|
-
//
|
|
4
|
+
// npm install -g @ran-sh/dsh-crew@latest
|
|
5
|
+
// dsh-crew install|status|update|uninstall
|
|
5
6
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
//
|
|
@@ -565,57 +566,252 @@ export async function npxInstall({
|
|
|
565
566
|
return { ok: true, version: manifest.version, path: releaseDir };
|
|
566
567
|
}
|
|
567
568
|
|
|
569
|
+
export const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
|
|
570
|
+
export const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Extract a packed npm tarball with the platform `tar` binary (bsdtar ships
|
|
574
|
+
* with Windows 10+, macOS, and Linux) into destDir; npm tarballs always root
|
|
575
|
+
* at `package/`.
|
|
576
|
+
*/
|
|
577
|
+
export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
578
|
+
mkdirSync(destDir, { recursive: true });
|
|
579
|
+
const result = runner('tar', ['-xzf', String(tgzPath), '-C', destDir], {
|
|
580
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
|
581
|
+
});
|
|
582
|
+
const root = join(destDir, 'package');
|
|
583
|
+
if (result.status !== 0 || !existsSync(join(root, 'package.json'))) {
|
|
584
|
+
return { ok: false, detail: `tar extract failed (${result.status}): ${(result.stderr || result.stdout || '').trim().slice(-200)}` };
|
|
585
|
+
}
|
|
586
|
+
return { ok: true, sourceRoot: root };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* The globally installed launcher intentionally does not self-replace. When
|
|
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.
|
|
593
|
+
*/
|
|
594
|
+
function noteLauncherDivergence({ log, home = homedir() }) {
|
|
595
|
+
const launcherVersion = readManifest(runningPackageRoot())?.version ?? null;
|
|
596
|
+
let installedVersion = null;
|
|
597
|
+
try { installedVersion = readCurrentPointer({ home }).version ?? null; } catch { installedVersion = null; }
|
|
598
|
+
if (launcherVersion && installedVersion && compareVersions(installedVersion, launcherVersion) > 0) {
|
|
599
|
+
log('');
|
|
600
|
+
log(`- note: managed payload ${installedVersion} is newer than the global launcher ${launcherVersion}; the payload remains authoritative.`);
|
|
601
|
+
log(` Refresh the launcher when convenient: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Resolve an update candidate WITHOUT a source checkout:
|
|
607
|
+
* - explicit candidate path: a payload directory or a packed .tgz file;
|
|
608
|
+
* - otherwise npm registry mode: `npm pack <spec>` using the user's own
|
|
609
|
+
* configured registry/auth (no policy or config mutation), extracted to a
|
|
610
|
+
* disposable directory the caller cleans up via returned cleanup().
|
|
611
|
+
*/
|
|
612
|
+
export function resolveUpdateCandidate({
|
|
613
|
+
candidate,
|
|
614
|
+
spec = UPDATE_DEFAULT_SPEC,
|
|
615
|
+
home = homedir(),
|
|
616
|
+
log = () => {},
|
|
617
|
+
runner = spawnSync,
|
|
618
|
+
} = {}) {
|
|
619
|
+
const candidateValue = candidate ?? process.env.DSH_CREW_CANDIDATE ?? undefined;
|
|
620
|
+
if (candidateValue !== undefined && typeof candidateValue !== 'string') {
|
|
621
|
+
return { ok: false, code: 'INVALID_CANDIDATE', detail: 'candidate must be a directory or .tgz path' };
|
|
622
|
+
}
|
|
623
|
+
if (typeof candidateValue === 'string' && candidateValue.trim()) {
|
|
624
|
+
const value = resolve(candidateValue.trim());
|
|
625
|
+
if (!existsSync(value)) return { ok: false, code: 'CANDIDATE_NOT_FOUND', detail: value };
|
|
626
|
+
if (existsSync(join(value, 'package.json'))) {
|
|
627
|
+
const manifest = readManifest(value);
|
|
628
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: value };
|
|
629
|
+
return { ok: true, sourceRoot: value, version: manifest.version, cleanup: null };
|
|
630
|
+
}
|
|
631
|
+
if (/\.tgz$/i.test(value)) {
|
|
632
|
+
const tmpRoot = join(crewReleasesDir({ home }), `.candidate-${timestampStamp()}-${process.pid}`);
|
|
633
|
+
const extracted = extractPackageTarball(value, tmpRoot, { runner });
|
|
634
|
+
if (!extracted.ok) {
|
|
635
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
636
|
+
return { ok: false, code: 'CANDIDATE_EXTRACT_FAILED', detail: extracted.detail };
|
|
637
|
+
}
|
|
638
|
+
const manifest = readManifest(extracted.sourceRoot);
|
|
639
|
+
if (!manifest?.name || !manifest?.version) {
|
|
640
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
641
|
+
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: value };
|
|
642
|
+
}
|
|
643
|
+
return {
|
|
644
|
+
ok: true,
|
|
645
|
+
sourceRoot: extracted.sourceRoot,
|
|
646
|
+
version: manifest.version,
|
|
647
|
+
cleanup: () => rmSync(tmpRoot, { recursive: true, force: true }),
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
return { ok: false, code: 'UNSUPPORTED_CANDIDATE', detail: value };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Registry mode.
|
|
654
|
+
const tmpDir = join(crewReleasesDir({ home }), `.candidate-${timestampStamp()}-${process.pid}`);
|
|
655
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
656
|
+
try {
|
|
657
|
+
const effectiveSpec = process.env.DSH_CREW_UPDATE_SPEC ?? spec;
|
|
658
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
659
|
+
const packArgs = ['pack', effectiveSpec, '--pack-destination', tmpDir, '--json', '--loglevel=error'];
|
|
660
|
+
log(`- resolving update candidate from the configured npm registry (${effectiveSpec})`);
|
|
661
|
+
const packed = runner(npm, packArgs, {
|
|
662
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32',
|
|
663
|
+
timeout: 600_000, windowsHide: true, env: sanitizedPackageManagerEnv(),
|
|
664
|
+
});
|
|
665
|
+
if (packed.status !== 0) {
|
|
666
|
+
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: `${effectiveSpec}: ${(packed.stderr || packed.stdout || '').trim().slice(-300)}` };
|
|
667
|
+
}
|
|
668
|
+
let info;
|
|
669
|
+
try { info = JSON.parse(packed.stdout)[0]; } catch {
|
|
670
|
+
return { ok: false, code: 'REGISTRY_PACK_UNPARSEABLE', detail: String(packed.stdout).slice(-200) };
|
|
671
|
+
}
|
|
672
|
+
if (!info?.filename) return { ok: false, code: 'REGISTRY_PACK_UNPARSEABLE', detail: 'pack output missing filename' };
|
|
673
|
+
const tgzPath = join(tmpDir, info.filename);
|
|
674
|
+
const extracted = extractPackageTarball(tgzPath, join(tmpDir, 'x'), { runner });
|
|
675
|
+
if (!extracted.ok) {
|
|
676
|
+
return { ok: false, code: 'CANDIDATE_EXTRACT_FAILED', detail: extracted.detail };
|
|
677
|
+
}
|
|
678
|
+
const manifest = readManifest(extracted.sourceRoot);
|
|
679
|
+
if (!manifest?.name || !manifest?.version) {
|
|
680
|
+
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: info.filename };
|
|
681
|
+
}
|
|
682
|
+
if (info.name !== manifest.name || info.version !== manifest.version) {
|
|
683
|
+
return { ok: false, code: 'CANDIDATE_IDENTITY_MISMATCH', detail: `pack ${info.name}@${info.version} vs manifest ${manifest.name}@${manifest.version}` };
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
ok: true,
|
|
687
|
+
sourceRoot: extracted.sourceRoot,
|
|
688
|
+
version: manifest.version,
|
|
689
|
+
cleanup: () => rmSync(tmpDir, { recursive: true, force: true }),
|
|
690
|
+
};
|
|
691
|
+
} catch (error) {
|
|
692
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
693
|
+
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: String(error?.message ?? error).slice(-300) };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
568
697
|
export async function npxUpdate({
|
|
569
698
|
home = homedir(),
|
|
570
699
|
log = console.log,
|
|
571
700
|
sourceRoot,
|
|
701
|
+
candidate,
|
|
702
|
+
spec,
|
|
572
703
|
installer = realInstaller,
|
|
573
704
|
ensureRuntime,
|
|
574
705
|
npmInstaller,
|
|
706
|
+
runner = spawnSync,
|
|
575
707
|
} = {}) {
|
|
576
|
-
log('DSH Crew updater
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
708
|
+
log('DSH Crew updater');
|
|
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
|
+
}
|
|
729
|
+
if (!resolved.ok) {
|
|
730
|
+
log(`✗ candidate resolution failed (${resolved.code})${resolved.detail ? `: ${resolved.detail}` : ''}`);
|
|
731
|
+
return { ok: false, error: `candidate resolution failed (${resolved.code})` };
|
|
732
|
+
}
|
|
733
|
+
try {
|
|
734
|
+
const manifest = readManifest(resolved.sourceRoot);
|
|
735
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, error: 'candidate package manifest invalid' };
|
|
736
|
+
|
|
737
|
+
const health = currentInstallationHealth({ home });
|
|
738
|
+
|
|
739
|
+
if (health.installed && health.healthy && health.pointer.version === manifest.version) {
|
|
740
|
+
log(`- already current (${manifest.version}); repairing registration/integrations idempotently`);
|
|
741
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest, log, installer });
|
|
742
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
743
|
+
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
744
|
+
log('');
|
|
745
|
+
log('Done.');
|
|
746
|
+
return { ok: true, idempotent: true, version: manifest.version, path: health.pointer.path };
|
|
747
|
+
}
|
|
580
748
|
|
|
581
|
-
|
|
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`);
|
|
752
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest: readManifest(health.pointer.path), log, installer });
|
|
753
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
754
|
+
return { ok: true, idempotent: true, version: health.pointer.version, path: health.pointer.path };
|
|
755
|
+
}
|
|
582
756
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
757
|
+
// Stale, unhealthy, older, or missing installation: stage the candidate
|
|
758
|
+
// fully and validate it before switching. The previous usable release is
|
|
759
|
+
// left in place until the replacement has been committed and activated.
|
|
760
|
+
if (health.installed && !health.healthy) {
|
|
761
|
+
log('- existing installation is stale or incomplete; repairing via fresh candidate staging');
|
|
762
|
+
} else if (health.installed) {
|
|
763
|
+
log(`- updating managed payload ${health.pointer.version} -> ${manifest.version}`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const staged = stageCandidatePayload({ sourceRoot: resolved.sourceRoot, home, log, npmInstaller });
|
|
767
|
+
if (!staged.ok) {
|
|
768
|
+
log(`✗ staging failed (${staged.code})${staged.detail ? `: ${staged.detail.join('; ')}` : ''}`);
|
|
769
|
+
return { ok: false, error: `staging failed (${staged.code})` };
|
|
770
|
+
}
|
|
771
|
+
log(`✓ candidate payload staged and validated (${manifest.version})`);
|
|
772
|
+
|
|
773
|
+
const releaseDir = commitStagedRelease({ stageDir: staged.stageDir, manifest, home });
|
|
774
|
+
log('✓ durable release committed under Crew-owned state');
|
|
775
|
+
|
|
776
|
+
const activated = await activateRelease({ home, releaseDir, manifest, log, installer });
|
|
586
777
|
if (!activated) return { ok: false, error: 'activation failed' };
|
|
778
|
+
|
|
587
779
|
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
780
|
+
|
|
781
|
+
noteLauncherDivergence({ log, home });
|
|
588
782
|
log('');
|
|
589
783
|
log('Done.');
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
// validate it before switching. The previous usable release is left in place
|
|
595
|
-
// until the replacement has been committed and activated successfully.
|
|
596
|
-
if (health.installed && !health.healthy) {
|
|
597
|
-
log('- existing installation is stale or incomplete; repairing via fresh candidate staging');
|
|
784
|
+
log('Restart DeepSeek Harness and Codex Desktop.');
|
|
785
|
+
return { ok: true, updated: true, version: manifest.version, path: releaseDir };
|
|
786
|
+
} finally {
|
|
787
|
+
resolved.cleanup?.();
|
|
598
788
|
}
|
|
789
|
+
}
|
|
599
790
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
791
|
+
/**
|
|
792
|
+
* Compare dotted numeric versions; returns -1/0/1. Non-numeric segments fall
|
|
793
|
+
* back to string comparison so prerelease identifiers stay deterministic.
|
|
794
|
+
*/
|
|
795
|
+
export function compareVersions(left, right) {
|
|
796
|
+
const asParts = (value) => String(value ?? '').split('.');
|
|
797
|
+
const a = asParts(left);
|
|
798
|
+
const b = asParts(right);
|
|
799
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
800
|
+
const x = a[index] ?? '0';
|
|
801
|
+
const y = b[index] ?? '0';
|
|
802
|
+
const xn = /^\d+$/.test(x);
|
|
803
|
+
const yn = /^\d+$/.test(y);
|
|
804
|
+
if (xn && yn) {
|
|
805
|
+
const diff = Number(x) - Number(y);
|
|
806
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
807
|
+
} else if (xn !== yn) {
|
|
808
|
+
// npm semantics: a numeric release segment outranks a prerelease segment.
|
|
809
|
+
return xn ? 1 : -1;
|
|
810
|
+
} else if (x !== y) {
|
|
811
|
+
return x < y ? -1 : 1;
|
|
812
|
+
}
|
|
604
813
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const releaseDir = commitStagedRelease({ stageDir: staged.stageDir, manifest, home });
|
|
608
|
-
log('✓ durable release committed under Crew-owned state');
|
|
609
|
-
|
|
610
|
-
const activated = await activateRelease({ home, releaseDir, manifest, log, installer });
|
|
611
|
-
if (!activated) return { ok: false, error: 'activation failed' };
|
|
612
|
-
|
|
613
|
-
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
614
|
-
|
|
615
|
-
log('');
|
|
616
|
-
log('Done.');
|
|
617
|
-
log('Restart DeepSeek Harness and Codex Desktop.');
|
|
618
|
-
return { ok: true, updated: true, version: manifest.version, path: releaseDir };
|
|
814
|
+
return 0;
|
|
619
815
|
}
|
|
620
816
|
|
|
621
817
|
export function npxStatus({
|
|
@@ -664,8 +860,18 @@ export function npxStatus({
|
|
|
664
860
|
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
665
861
|
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
666
862
|
|
|
667
|
-
log(`DSH Crew
|
|
668
|
-
log(`Installed DSH Crew: ${installedLine}`);
|
|
863
|
+
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
864
|
+
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
865
|
+
if (candidateVersion && installedVersion && candidateVersion !== 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
|
+
}
|
|
874
|
+
}
|
|
669
875
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
|
|
670
876
|
log(`Codex Desktop integration: ${codex}`);
|
|
671
877
|
log(`Claude Code integration: ${claude}`);
|
|
@@ -736,25 +942,40 @@ export async function npxUninstall({
|
|
|
736
942
|
|
|
737
943
|
// ---- CLI dispatch --------------------------------------------------------------
|
|
738
944
|
|
|
739
|
-
export const USAGE = `usage: dsh-crew <command> [--purge]
|
|
945
|
+
export const USAGE = `usage: dsh-crew <command> [--purge] [--candidate <path>]
|
|
740
946
|
|
|
741
947
|
Commands:
|
|
742
948
|
install persist the candidate package into Crew-owned state and register it
|
|
743
|
-
status read-only report of
|
|
744
|
-
update
|
|
949
|
+
status read-only report of launcher/installed versions and integrations
|
|
950
|
+
update resolve the newest permitted package from the configured npm registry (or
|
|
951
|
+
--candidate), stage and validate it, then activate; idempotent when current
|
|
745
952
|
uninstall remove the Crew-managed payload, registration, and integrations (config kept)
|
|
746
953
|
|
|
747
954
|
Options:
|
|
748
|
-
--
|
|
749
|
-
--
|
|
955
|
+
--candidate <path> update from a local payload directory or packed .tgz instead of the registry
|
|
956
|
+
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
957
|
+
--help show this help
|
|
750
958
|
|
|
959
|
+
Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
|
|
751
960
|
Source checkouts use scripts/setup.mjs instead.`;
|
|
752
961
|
|
|
753
962
|
function normalizeCommand(argv) {
|
|
754
963
|
const flags = argv.slice(1);
|
|
964
|
+
let candidate;
|
|
965
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
966
|
+
if (flags[index] === '--candidate') {
|
|
967
|
+
candidate = flags[index + 1];
|
|
968
|
+
flags.splice(index, 2);
|
|
969
|
+
index -= 1;
|
|
970
|
+
} else if (flags[index]?.startsWith('--candidate=')) {
|
|
971
|
+
candidate = flags[index].slice('--candidate='.length);
|
|
972
|
+
flags.splice(index, 1);
|
|
973
|
+
index -= 1;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
755
976
|
const knownFlags = new Set(['--purge']);
|
|
756
977
|
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
757
|
-
return { command: argv[0], purge: flags.includes('--purge'), unknown };
|
|
978
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, unknown };
|
|
758
979
|
}
|
|
759
980
|
|
|
760
981
|
/**
|
|
@@ -766,7 +987,7 @@ export async function runNpxCli({
|
|
|
766
987
|
error = console.error,
|
|
767
988
|
commands = {},
|
|
768
989
|
} = {}) {
|
|
769
|
-
const { command, purge, unknown } = normalizeCommand(argv);
|
|
990
|
+
const { command, purge, candidate, unknown } = normalizeCommand(argv);
|
|
770
991
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
771
992
|
log(USAGE);
|
|
772
993
|
return 0;
|
|
@@ -786,9 +1007,10 @@ export async function runNpxCli({
|
|
|
786
1007
|
update: commands.update ?? npxUpdate,
|
|
787
1008
|
uninstall: commands.uninstall ?? npxUninstall,
|
|
788
1009
|
};
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1010
|
+
let result;
|
|
1011
|
+
if (command === 'uninstall') result = await actions.uninstall({ purge, log });
|
|
1012
|
+
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
1013
|
+
else result = await actions[command]({ log });
|
|
792
1014
|
return result?.ok === false ? 1 : 0;
|
|
793
1015
|
} catch (err) {
|
|
794
1016
|
error(`dsh-crew ${command} failed: ${err?.message ?? err}`);
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -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.
|
|
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([
|