@ran-sh/dsh-crew 0.3.3 → 0.3.4
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 +15 -10
- package/README.zh.md +15 -10
- package/package.json +1 -1
- package/src/install/npx-lifecycle.mjs +244 -46
- package/src/runtime-identity.mjs +1 -1
package/README.md
CHANGED
|
@@ -40,21 +40,26 @@ 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; `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.
|
|
58
63
|
|
|
59
64
|
Developer / source setup (alternative path):
|
|
60
65
|
|
|
@@ -113,16 +118,16 @@ A coding worker receives a temporary git worktree at the requested revision, so
|
|
|
113
118
|
|
|
114
119
|
## Update / uninstall
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
The managed Crew payload updates in place (config, credentials, and backups are preserved; the candidate is staged and validated before switching):
|
|
117
122
|
|
|
118
123
|
```bash
|
|
119
|
-
|
|
124
|
+
dsh-crew update
|
|
120
125
|
```
|
|
121
126
|
|
|
122
|
-
Uninstall
|
|
127
|
+
Uninstall the managed payload and integrations:
|
|
123
128
|
|
|
124
129
|
```bash
|
|
125
|
-
|
|
130
|
+
dsh-crew uninstall
|
|
126
131
|
```
|
|
127
132
|
|
|
128
133
|
Update a source install instead with:
|
package/README.zh.md
CHANGED
|
@@ -40,21 +40,26 @@ 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>` 显式指定),在 Crew 自有状态下校验后再切换。全局启动器不会自我替换;更新完成后 CLI 会打印刷新启动器的准确命令。
|
|
58
63
|
|
|
59
64
|
开发者 / 源码安装(备选路径):
|
|
60
65
|
|
|
@@ -113,16 +118,16 @@ Use ds-reviewer to review the implementation.
|
|
|
113
118
|
|
|
114
119
|
## 更新 / 卸载
|
|
115
120
|
|
|
116
|
-
|
|
121
|
+
托管 Crew 载荷使用 update 原地升级(配置、凭据和备份保留;候选包先暂存校验再切换):
|
|
117
122
|
|
|
118
123
|
```bash
|
|
119
|
-
|
|
124
|
+
dsh-crew update
|
|
120
125
|
```
|
|
121
126
|
|
|
122
|
-
|
|
127
|
+
卸载托管载荷与集成:
|
|
123
128
|
|
|
124
129
|
```bash
|
|
125
|
-
|
|
130
|
+
dsh-crew uninstall
|
|
126
131
|
```
|
|
127
132
|
|
|
128
133
|
源码安装的更新方式:
|
package/package.json
CHANGED
|
@@ -565,57 +565,235 @@ export async function npxInstall({
|
|
|
565
565
|
return { ok: true, version: manifest.version, path: releaseDir };
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
+
export const UPDATE_PACKAGE_NAME = '@ran-sh/dsh-crew';
|
|
569
|
+
export const UPDATE_DEFAULT_SPEC = `${UPDATE_PACKAGE_NAME}@latest`;
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Extract a packed npm tarball with the platform `tar` binary (bsdtar ships
|
|
573
|
+
* with Windows 10+, macOS, and Linux) into destDir; npm tarballs always root
|
|
574
|
+
* at `package/`.
|
|
575
|
+
*/
|
|
576
|
+
export function extractPackageTarball(tgzPath, destDir, { runner = spawnSync } = {}) {
|
|
577
|
+
mkdirSync(destDir, { recursive: true });
|
|
578
|
+
const result = runner('tar', ['-xzf', String(tgzPath), '-C', destDir], {
|
|
579
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
|
|
580
|
+
});
|
|
581
|
+
const root = join(destDir, 'package');
|
|
582
|
+
if (result.status !== 0 || !existsSync(join(root, 'package.json'))) {
|
|
583
|
+
return { ok: false, detail: `tar extract failed (${result.status}): ${(result.stderr || result.stdout || '').trim().slice(-200)}` };
|
|
584
|
+
}
|
|
585
|
+
return { ok: true, sourceRoot: root };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* 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.
|
|
592
|
+
*/
|
|
593
|
+
function noteLauncherDivergence({ log, home = homedir() }) {
|
|
594
|
+
const launcherVersion = readManifest(runningPackageRoot())?.version ?? null;
|
|
595
|
+
let installedVersion = null;
|
|
596
|
+
try { installedVersion = readCurrentPointer({ home }).version ?? null; } catch { installedVersion = null; }
|
|
597
|
+
if (launcherVersion && installedVersion && launcherVersion !== installedVersion) {
|
|
598
|
+
log('');
|
|
599
|
+
log(`- note: the global launcher is still ${launcherVersion}; the managed Crew payload is now ${installedVersion}.`);
|
|
600
|
+
log(` Refresh the launcher when convenient: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Resolve an update candidate WITHOUT a source checkout:
|
|
606
|
+
* - explicit candidate path: a payload directory or a packed .tgz file;
|
|
607
|
+
* - otherwise npm registry mode: `npm pack <spec>` using the user's own
|
|
608
|
+
* configured registry/auth (no policy or config mutation), extracted to a
|
|
609
|
+
* disposable directory the caller cleans up via returned cleanup().
|
|
610
|
+
*/
|
|
611
|
+
export function resolveUpdateCandidate({
|
|
612
|
+
candidate,
|
|
613
|
+
spec = UPDATE_DEFAULT_SPEC,
|
|
614
|
+
home = homedir(),
|
|
615
|
+
log = () => {},
|
|
616
|
+
runner = spawnSync,
|
|
617
|
+
} = {}) {
|
|
618
|
+
const candidateValue = candidate ?? process.env.DSH_CREW_CANDIDATE ?? undefined;
|
|
619
|
+
if (candidateValue !== undefined && typeof candidateValue !== 'string') {
|
|
620
|
+
return { ok: false, code: 'INVALID_CANDIDATE', detail: 'candidate must be a directory or .tgz path' };
|
|
621
|
+
}
|
|
622
|
+
if (typeof candidateValue === 'string' && candidateValue.trim()) {
|
|
623
|
+
const value = resolve(candidateValue.trim());
|
|
624
|
+
if (!existsSync(value)) return { ok: false, code: 'CANDIDATE_NOT_FOUND', detail: value };
|
|
625
|
+
if (existsSync(join(value, 'package.json'))) {
|
|
626
|
+
const manifest = readManifest(value);
|
|
627
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: value };
|
|
628
|
+
return { ok: true, sourceRoot: value, version: manifest.version, cleanup: null };
|
|
629
|
+
}
|
|
630
|
+
if (/\.tgz$/i.test(value)) {
|
|
631
|
+
const tmpRoot = join(crewReleasesDir({ home }), `.candidate-${timestampStamp()}-${process.pid}`);
|
|
632
|
+
const extracted = extractPackageTarball(value, tmpRoot, { runner });
|
|
633
|
+
if (!extracted.ok) {
|
|
634
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
635
|
+
return { ok: false, code: 'CANDIDATE_EXTRACT_FAILED', detail: extracted.detail };
|
|
636
|
+
}
|
|
637
|
+
const manifest = readManifest(extracted.sourceRoot);
|
|
638
|
+
if (!manifest?.name || !manifest?.version) {
|
|
639
|
+
rmSync(tmpRoot, { recursive: true, force: true });
|
|
640
|
+
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: value };
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
ok: true,
|
|
644
|
+
sourceRoot: extracted.sourceRoot,
|
|
645
|
+
version: manifest.version,
|
|
646
|
+
cleanup: () => rmSync(tmpRoot, { recursive: true, force: true }),
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
return { ok: false, code: 'UNSUPPORTED_CANDIDATE', detail: value };
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Registry mode.
|
|
653
|
+
const tmpDir = join(crewReleasesDir({ home }), `.candidate-${timestampStamp()}-${process.pid}`);
|
|
654
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
655
|
+
try {
|
|
656
|
+
const effectiveSpec = process.env.DSH_CREW_UPDATE_SPEC ?? spec;
|
|
657
|
+
const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
658
|
+
const packArgs = ['pack', effectiveSpec, '--pack-destination', tmpDir, '--json', '--loglevel=error'];
|
|
659
|
+
log(`- resolving update candidate from the configured npm registry (${effectiveSpec})`);
|
|
660
|
+
const packed = runner(npm, packArgs, {
|
|
661
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32',
|
|
662
|
+
timeout: 600_000, windowsHide: true, env: sanitizedPackageManagerEnv(),
|
|
663
|
+
});
|
|
664
|
+
if (packed.status !== 0) {
|
|
665
|
+
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: `${effectiveSpec}: ${(packed.stderr || packed.stdout || '').trim().slice(-300)}` };
|
|
666
|
+
}
|
|
667
|
+
let info;
|
|
668
|
+
try { info = JSON.parse(packed.stdout)[0]; } catch {
|
|
669
|
+
return { ok: false, code: 'REGISTRY_PACK_UNPARSEABLE', detail: String(packed.stdout).slice(-200) };
|
|
670
|
+
}
|
|
671
|
+
if (!info?.filename) return { ok: false, code: 'REGISTRY_PACK_UNPARSEABLE', detail: 'pack output missing filename' };
|
|
672
|
+
const tgzPath = join(tmpDir, info.filename);
|
|
673
|
+
const extracted = extractPackageTarball(tgzPath, join(tmpDir, 'x'), { runner });
|
|
674
|
+
if (!extracted.ok) {
|
|
675
|
+
return { ok: false, code: 'CANDIDATE_EXTRACT_FAILED', detail: extracted.detail };
|
|
676
|
+
}
|
|
677
|
+
const manifest = readManifest(extracted.sourceRoot);
|
|
678
|
+
if (!manifest?.name || !manifest?.version) {
|
|
679
|
+
return { ok: false, code: 'CANDIDATE_MANIFEST_INVALID', detail: info.filename };
|
|
680
|
+
}
|
|
681
|
+
if (info.name !== manifest.name || info.version !== manifest.version) {
|
|
682
|
+
return { ok: false, code: 'CANDIDATE_IDENTITY_MISMATCH', detail: `pack ${info.name}@${info.version} vs manifest ${manifest.name}@${manifest.version}` };
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
ok: true,
|
|
686
|
+
sourceRoot: extracted.sourceRoot,
|
|
687
|
+
version: manifest.version,
|
|
688
|
+
cleanup: () => rmSync(tmpDir, { recursive: true, force: true }),
|
|
689
|
+
};
|
|
690
|
+
} catch (error) {
|
|
691
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
692
|
+
return { ok: false, code: 'REGISTRY_PACK_FAILED', detail: String(error?.message ?? error).slice(-300) };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
568
696
|
export async function npxUpdate({
|
|
569
697
|
home = homedir(),
|
|
570
698
|
log = console.log,
|
|
571
699
|
sourceRoot,
|
|
700
|
+
candidate,
|
|
701
|
+
spec,
|
|
572
702
|
installer = realInstaller,
|
|
573
703
|
ensureRuntime,
|
|
574
704
|
npmInstaller,
|
|
705
|
+
runner = spawnSync,
|
|
575
706
|
} = {}) {
|
|
576
|
-
log('DSH Crew updater
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
707
|
+
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 });
|
|
712
|
+
if (!resolved.ok) {
|
|
713
|
+
log(`✗ candidate resolution failed (${resolved.code})${resolved.detail ? `: ${resolved.detail}` : ''}`);
|
|
714
|
+
return { ok: false, error: `candidate resolution failed (${resolved.code})` };
|
|
715
|
+
}
|
|
716
|
+
try {
|
|
717
|
+
const manifest = readManifest(resolved.sourceRoot);
|
|
718
|
+
if (!manifest?.name || !manifest?.version) return { ok: false, error: 'candidate package manifest invalid' };
|
|
719
|
+
|
|
720
|
+
const health = currentInstallationHealth({ home });
|
|
721
|
+
|
|
722
|
+
if (health.installed && health.healthy && health.pointer.version === manifest.version) {
|
|
723
|
+
log(`- already current (${manifest.version}); repairing registration/integrations idempotently`);
|
|
724
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest, log, installer });
|
|
725
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
726
|
+
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
727
|
+
log('');
|
|
728
|
+
log('Done.');
|
|
729
|
+
return { ok: true, idempotent: true, version: manifest.version, path: health.pointer.path };
|
|
730
|
+
}
|
|
580
731
|
|
|
581
|
-
|
|
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`);
|
|
734
|
+
const activated = await activateRelease({ home, releaseDir: health.pointer.path, manifest: readManifest(health.pointer.path), log, installer });
|
|
735
|
+
if (!activated) return { ok: false, error: 'activation failed' };
|
|
736
|
+
return { ok: true, idempotent: true, version: health.pointer.version, path: health.pointer.path };
|
|
737
|
+
}
|
|
582
738
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
739
|
+
// Stale, unhealthy, older, or missing installation: stage the candidate
|
|
740
|
+
// fully and validate it before switching. The previous usable release is
|
|
741
|
+
// left in place until the replacement has been committed and activated.
|
|
742
|
+
if (health.installed && !health.healthy) {
|
|
743
|
+
log('- existing installation is stale or incomplete; repairing via fresh candidate staging');
|
|
744
|
+
} else if (health.installed) {
|
|
745
|
+
log(`- updating managed payload ${health.pointer.version} -> ${manifest.version}`);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const staged = stageCandidatePayload({ sourceRoot: resolved.sourceRoot, home, log, npmInstaller });
|
|
749
|
+
if (!staged.ok) {
|
|
750
|
+
log(`✗ staging failed (${staged.code})${staged.detail ? `: ${staged.detail.join('; ')}` : ''}`);
|
|
751
|
+
return { ok: false, error: `staging failed (${staged.code})` };
|
|
752
|
+
}
|
|
753
|
+
log(`✓ candidate payload staged and validated (${manifest.version})`);
|
|
754
|
+
|
|
755
|
+
const releaseDir = commitStagedRelease({ stageDir: staged.stageDir, manifest, home });
|
|
756
|
+
log('✓ durable release committed under Crew-owned state');
|
|
757
|
+
|
|
758
|
+
const activated = await activateRelease({ home, releaseDir, manifest, log, installer });
|
|
586
759
|
if (!activated) return { ok: false, error: 'activation failed' };
|
|
760
|
+
|
|
587
761
|
if (!await ensureRuntimeStep({ home, log, ensureRuntime })) return { ok: false, error: 'Crew DSH runtime bootstrap failed' };
|
|
762
|
+
|
|
763
|
+
noteLauncherDivergence({ log, home });
|
|
588
764
|
log('');
|
|
589
765
|
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');
|
|
766
|
+
log('Restart DeepSeek Harness and Codex Desktop.');
|
|
767
|
+
return { ok: true, updated: true, version: manifest.version, path: releaseDir };
|
|
768
|
+
} finally {
|
|
769
|
+
resolved.cleanup?.();
|
|
598
770
|
}
|
|
771
|
+
}
|
|
599
772
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
773
|
+
/**
|
|
774
|
+
* Compare dotted numeric versions; returns -1/0/1. Non-numeric segments fall
|
|
775
|
+
* back to string comparison so prerelease identifiers stay deterministic.
|
|
776
|
+
*/
|
|
777
|
+
export function compareVersions(left, right) {
|
|
778
|
+
const asParts = (value) => String(value ?? '').split('.');
|
|
779
|
+
const a = asParts(left);
|
|
780
|
+
const b = asParts(right);
|
|
781
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
782
|
+
const x = a[index] ?? '0';
|
|
783
|
+
const y = b[index] ?? '0';
|
|
784
|
+
const xn = /^\d+$/.test(x);
|
|
785
|
+
const yn = /^\d+$/.test(y);
|
|
786
|
+
if (xn && yn) {
|
|
787
|
+
const diff = Number(x) - Number(y);
|
|
788
|
+
if (diff !== 0) return diff < 0 ? -1 : 1;
|
|
789
|
+
} else if (xn !== yn) {
|
|
790
|
+
// npm semantics: a numeric release segment outranks a prerelease segment.
|
|
791
|
+
return xn ? 1 : -1;
|
|
792
|
+
} else if (x !== y) {
|
|
793
|
+
return x < y ? -1 : 1;
|
|
794
|
+
}
|
|
604
795
|
}
|
|
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 };
|
|
796
|
+
return 0;
|
|
619
797
|
}
|
|
620
798
|
|
|
621
799
|
export function npxStatus({
|
|
@@ -664,8 +842,12 @@ export function npxStatus({
|
|
|
664
842
|
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
665
843
|
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
666
844
|
|
|
667
|
-
log(`DSH Crew
|
|
668
|
-
log(`Installed DSH Crew: ${installedLine}`);
|
|
845
|
+
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
846
|
+
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
847
|
+
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}`);
|
|
850
|
+
}
|
|
669
851
|
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile; official web profile ignored)`);
|
|
670
852
|
log(`Codex Desktop integration: ${codex}`);
|
|
671
853
|
log(`Claude Code integration: ${claude}`);
|
|
@@ -736,25 +918,40 @@ export async function npxUninstall({
|
|
|
736
918
|
|
|
737
919
|
// ---- CLI dispatch --------------------------------------------------------------
|
|
738
920
|
|
|
739
|
-
export const USAGE = `usage: dsh-crew <command> [--purge]
|
|
921
|
+
export const USAGE = `usage: dsh-crew <command> [--purge] [--candidate <path>]
|
|
740
922
|
|
|
741
923
|
Commands:
|
|
742
924
|
install persist the candidate package into Crew-owned state and register it
|
|
743
|
-
status read-only report of
|
|
744
|
-
update
|
|
925
|
+
status read-only report of launcher/installed versions and integrations
|
|
926
|
+
update resolve the newest permitted package from the configured npm registry (or
|
|
927
|
+
--candidate), stage and validate it, then activate; idempotent when current
|
|
745
928
|
uninstall remove the Crew-managed payload, registration, and integrations (config kept)
|
|
746
929
|
|
|
747
930
|
Options:
|
|
748
|
-
--
|
|
749
|
-
--
|
|
931
|
+
--candidate <path> update from a local payload directory or packed .tgz instead of the registry
|
|
932
|
+
--purge with uninstall: also remove ~/.config/dsh-crew config/backups (destructive)
|
|
933
|
+
--help show this help
|
|
750
934
|
|
|
935
|
+
Primary install: npm install -g @ran-sh/dsh-crew (then run: dsh-crew install)
|
|
751
936
|
Source checkouts use scripts/setup.mjs instead.`;
|
|
752
937
|
|
|
753
938
|
function normalizeCommand(argv) {
|
|
754
939
|
const flags = argv.slice(1);
|
|
940
|
+
let candidate;
|
|
941
|
+
for (let index = 0; index < flags.length; index += 1) {
|
|
942
|
+
if (flags[index] === '--candidate') {
|
|
943
|
+
candidate = flags[index + 1];
|
|
944
|
+
flags.splice(index, 2);
|
|
945
|
+
index -= 1;
|
|
946
|
+
} else if (flags[index]?.startsWith('--candidate=')) {
|
|
947
|
+
candidate = flags[index].slice('--candidate='.length);
|
|
948
|
+
flags.splice(index, 1);
|
|
949
|
+
index -= 1;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
755
952
|
const knownFlags = new Set(['--purge']);
|
|
756
953
|
const unknown = flags.filter((f) => f.startsWith('--') && !knownFlags.has(f));
|
|
757
|
-
return { command: argv[0], purge: flags.includes('--purge'), unknown };
|
|
954
|
+
return { command: argv[0], purge: flags.includes('--purge'), candidate, unknown };
|
|
758
955
|
}
|
|
759
956
|
|
|
760
957
|
/**
|
|
@@ -766,7 +963,7 @@ export async function runNpxCli({
|
|
|
766
963
|
error = console.error,
|
|
767
964
|
commands = {},
|
|
768
965
|
} = {}) {
|
|
769
|
-
const { command, purge, unknown } = normalizeCommand(argv);
|
|
966
|
+
const { command, purge, candidate, unknown } = normalizeCommand(argv);
|
|
770
967
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
771
968
|
log(USAGE);
|
|
772
969
|
return 0;
|
|
@@ -786,9 +983,10 @@ export async function runNpxCli({
|
|
|
786
983
|
update: commands.update ?? npxUpdate,
|
|
787
984
|
uninstall: commands.uninstall ?? npxUninstall,
|
|
788
985
|
};
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
986
|
+
let result;
|
|
987
|
+
if (command === 'uninstall') result = await actions.uninstall({ purge, log });
|
|
988
|
+
else if (command === 'update') result = await actions.update({ candidate, log });
|
|
989
|
+
else result = await actions[command]({ log });
|
|
792
990
|
return result?.ok === false ? 1 : 0;
|
|
793
991
|
} catch (err) {
|
|
794
992
|
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.4';
|
|
12
12
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
13
13
|
|
|
14
14
|
export const HUB_CAPABILITIES = Object.freeze([
|