@ran-sh/dsh-crew 2.0.8 → 2.0.10
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/package.json
CHANGED
|
@@ -23,6 +23,11 @@ const PLUGIN_KEY = `dsh-crew@${MARKETPLACE_NAME}`;
|
|
|
23
23
|
const CLAUDE_PLUGIN_TIMEOUT_MS = 300_000;
|
|
24
24
|
const CLAUDE_SNAPSHOT_SETTLE_MS = 180_000;
|
|
25
25
|
const CLAUDE_SNAPSHOT_POLL_MS = 5_000;
|
|
26
|
+
// The one scope this installer writes, and therefore the only scope whose record
|
|
27
|
+
// means "the integration is installed". Accepting any scope let a project-scope
|
|
28
|
+
// record stand in for a missing user-scope snapshot, so a failed install read as
|
|
29
|
+
// a current one.
|
|
30
|
+
const CLAUDE_PLUGIN_SCOPE = 'user';
|
|
26
31
|
const POLICY_START = '<!-- DSH CREW MANAGED POLICY:START -->';
|
|
27
32
|
const POLICY_END = '<!-- DSH CREW MANAGED POLICY:END -->';
|
|
28
33
|
|
|
@@ -200,7 +205,14 @@ function managedClaudeFileManifest(root, { maxFiles = 512, maxBytes = 8 * 1024 *
|
|
|
200
205
|
try {
|
|
201
206
|
add(join(root, '.claude-plugin', 'plugin.json'), join('.claude-plugin', 'plugin.json'));
|
|
202
207
|
if (isFile(join(root, 'package.json'))) add(join(root, 'package.json'), 'package.json');
|
|
203
|
-
|
|
208
|
+
// What the plugin loads, not just what names it. `worker.cordis.yml` is
|
|
209
|
+
// required before any dispatch (`src/jobs.mjs` throws without it) and the
|
|
210
|
+
// statusline scripts are named by the settings this installer writes, so a
|
|
211
|
+
// snapshot missing either is not this release even when the manifest it came
|
|
212
|
+
// from is byte-identical — which is exactly how a stale snapshot used to read
|
|
213
|
+
// as current.
|
|
214
|
+
if (isFile(join(root, 'worker.cordis.yml'))) add(join(root, 'worker.cordis.yml'), 'worker.cordis.yml');
|
|
215
|
+
for (const directory of ['agents', 'commands', 'skills', 'src', 'statusline']) walk(join(root, directory), directory);
|
|
204
216
|
return files;
|
|
205
217
|
} catch { return null; }
|
|
206
218
|
}
|
|
@@ -212,11 +224,12 @@ function sameManagedClaudeFiles(expectedRoot, snapshotRoot) {
|
|
|
212
224
|
return expected !== null && snapshot !== null && JSON.stringify(snapshot) === JSON.stringify(expected);
|
|
213
225
|
}
|
|
214
226
|
|
|
215
|
-
function claudeSnapshotReady(home, root) {
|
|
227
|
+
function claudeSnapshotReady(home, root, { scope = null } = {}) {
|
|
216
228
|
const installed = readJson(join(home, '.claude', 'plugins', 'installed_plugins.json'), {});
|
|
217
229
|
const record = installed?.plugins?.[PLUGIN_KEY];
|
|
218
230
|
const entries = Array.isArray(record) ? record : [record];
|
|
219
|
-
return entries.some((entry) =>
|
|
231
|
+
return entries.some((entry) => (scope === null || entry?.scope === scope)
|
|
232
|
+
&& sameManagedClaudeFiles(root, entry?.installPath));
|
|
220
233
|
}
|
|
221
234
|
|
|
222
235
|
function claudePermissionsReady(settings) {
|
|
@@ -399,7 +412,10 @@ export function installStatus({ home = homedir(), root = ROOT, env = process.env
|
|
|
399
412
|
const claudeComponents = {
|
|
400
413
|
enabled: claudeInstalled,
|
|
401
414
|
marketplace: normalizedPath(marketplaceRoot) === normalizedPath(effectiveRoot) && claudePluginRootReady(effectiveRoot),
|
|
402
|
-
|
|
415
|
+
// User scope only: this installer writes that scope, so it is the one whose
|
|
416
|
+
// record means the integration is installed. Accepting any scope let a
|
|
417
|
+
// project-scope record make a missing user-scope snapshot read as present.
|
|
418
|
+
snapshot: claudeSnapshotReady(home, effectiveRoot, { scope: CLAUDE_PLUGIN_SCOPE }),
|
|
403
419
|
permissions: claudePermissionsReady(settings),
|
|
404
420
|
};
|
|
405
421
|
const claudeMissing = Object.entries(claudeComponents).filter(([, present]) => !present).map(([key]) => key);
|
|
@@ -512,12 +528,33 @@ export function uninstallCodex({ home = homedir(), env = process.env } = {}) {
|
|
|
512
528
|
*/
|
|
513
529
|
export function claudeIntegrationLine(result) {
|
|
514
530
|
if (result?.ok === false) return '✗ Claude Code integration failed';
|
|
531
|
+
if (result?.detected === false) {
|
|
532
|
+
return '- Claude Code not detected; settings registered, CLI step skipped';
|
|
533
|
+
}
|
|
515
534
|
if (result?.degraded === true) {
|
|
516
535
|
return `- Claude Code integration registered, but not loaded: ${result.reason ?? 'plugin snapshot not refreshed'}`;
|
|
517
536
|
}
|
|
518
537
|
return '✓ Claude Code integration';
|
|
519
538
|
}
|
|
520
539
|
|
|
540
|
+
/**
|
|
541
|
+
* How long to keep watching the plugin snapshot after a failed CLI attempt.
|
|
542
|
+
*
|
|
543
|
+
* Zero unless that attempt timed out. The install is a real copy of the plugin
|
|
544
|
+
* tree, and a timed-out child on Windows is not in the shell's process tree, so
|
|
545
|
+
* it can go on writing after the shell has given up — that is the only case with
|
|
546
|
+
* something to wait for. A `claude` that is not installed exits with status 1 and
|
|
547
|
+
* never started a copy, and a CLI that failed on its own terms has already
|
|
548
|
+
* stopped; waiting on either cost a fixed 180s on every update of a machine
|
|
549
|
+
* without Claude Code, to learn nothing it did not already know.
|
|
550
|
+
*/
|
|
551
|
+
export function claudeSnapshotSettleMs(err) {
|
|
552
|
+
// Only the shell's own timeout can leave a copy running. Matching on the signal
|
|
553
|
+
// as well spent the whole window on an `ENOBUFS` (output over maxBuffer) child
|
|
554
|
+
// that had already stopped and could never make the snapshot current.
|
|
555
|
+
return err?.code === 'ETIMEDOUT' ? CLAUDE_SNAPSHOT_SETTLE_MS : 0;
|
|
556
|
+
}
|
|
557
|
+
|
|
521
558
|
export async function installClaudeCode({ home = homedir(), statusline = false, root = ROOT } = {}) {
|
|
522
559
|
const actions = [];
|
|
523
560
|
|
|
@@ -532,58 +569,66 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
532
569
|
|
|
533
570
|
// 2. settings.json: marketplace + enabledPlugins + permissions allowlist.
|
|
534
571
|
const settingsFile = join(home, '.claude', 'settings.json');
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
572
|
+
try {
|
|
573
|
+
mkdirSync(dirname(settingsFile), { recursive: true });
|
|
574
|
+
const bak = backup(settingsFile);
|
|
575
|
+
if (bak) actions.push(`backup: ${bak}`);
|
|
576
|
+
const settings = readJson(settingsFile, {});
|
|
577
|
+
|
|
578
|
+
// Both fields are records (see json.schemastore.org/claude-code-settings.json).
|
|
579
|
+
// Older versions of this installer wrote arrays, which Claude Code ignores
|
|
580
|
+
// with a warning — migrate those in place.
|
|
581
|
+
const markets = (settings.extraKnownMarketplaces && !Array.isArray(settings.extraKnownMarketplaces)
|
|
582
|
+
&& typeof settings.extraKnownMarketplaces === 'object') ? settings.extraKnownMarketplaces : {};
|
|
583
|
+
if (Array.isArray(settings.extraKnownMarketplaces)) actions.push('migrated legacy extraKnownMarketplaces array');
|
|
584
|
+
markets[MARKETPLACE_NAME] = { source: { source: 'directory', path: mpDir } };
|
|
585
|
+
if (markets['dsh-workers']) {
|
|
586
|
+
delete markets['dsh-workers'];
|
|
587
|
+
actions.push('removed pre-rename dsh-workers marketplace entry');
|
|
588
|
+
}
|
|
589
|
+
settings.extraKnownMarketplaces = markets;
|
|
552
590
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
591
|
+
const enabled = (settings.enabledPlugins && !Array.isArray(settings.enabledPlugins)
|
|
592
|
+
&& typeof settings.enabledPlugins === 'object') ? settings.enabledPlugins : {};
|
|
593
|
+
if (Array.isArray(settings.enabledPlugins)) {
|
|
594
|
+
for (const key of settings.enabledPlugins) if (typeof key === 'string') enabled[key] = true;
|
|
595
|
+
actions.push('migrated legacy enabledPlugins array');
|
|
596
|
+
}
|
|
597
|
+
enabled[PLUGIN_KEY] = true;
|
|
598
|
+
if (enabled['dsh-workers@dsh-workers']) {
|
|
599
|
+
delete enabled['dsh-workers@dsh-workers'];
|
|
600
|
+
actions.push('removed pre-rename dsh-workers plugin entry');
|
|
601
|
+
}
|
|
602
|
+
settings.enabledPlugins = enabled;
|
|
603
|
+
|
|
604
|
+
settings.permissions = settings.permissions ?? {};
|
|
605
|
+
let allow = Array.isArray(settings.permissions.allow) ? settings.permissions.allow : [];
|
|
606
|
+
const preRename = allow.filter((r) => typeof r === 'string' && r.startsWith('mcp__plugin_dsh-workers_'));
|
|
607
|
+
if (preRename.length) {
|
|
608
|
+
allow = allow.filter((r) => !preRename.includes(r));
|
|
609
|
+
actions.push(`removed ${preRename.length} pre-rename permission rules`);
|
|
610
|
+
}
|
|
611
|
+
for (const tool of MCP_TOOLS) {
|
|
612
|
+
const rule = `mcp__plugin_dsh-crew_dsh-crew__${tool}`;
|
|
613
|
+
if (!allow.includes(rule)) allow.push(rule);
|
|
614
|
+
}
|
|
615
|
+
settings.permissions.allow = allow;
|
|
578
616
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
617
|
+
if (statusline && !settings.statusLine) {
|
|
618
|
+
settings.statusLine = { type: 'command', command: `bash ${join(root, 'statusline', 'statusline.sh')}` };
|
|
619
|
+
actions.push('statusline: installed');
|
|
620
|
+
} else if (statusline) {
|
|
621
|
+
actions.push('statusline: skipped (one already configured)');
|
|
622
|
+
}
|
|
585
623
|
|
|
586
|
-
|
|
624
|
+
writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
|
|
625
|
+
} catch (error) {
|
|
626
|
+
// Both install entries branch on `ok === false` for this integration and
|
|
627
|
+
// neither could ever see it: a settings path that could not be backed up or
|
|
628
|
+
// written threw straight out of both of them. Failing to register is the
|
|
629
|
+
// integration failing, so it gets the shape its callers already handle.
|
|
630
|
+
return { ok: false, code: 'CLAUDE_SETTINGS_UNWRITABLE', error: String(error?.message ?? error), actions };
|
|
631
|
+
}
|
|
587
632
|
actions.push(`settings: registered ${PLUGIN_KEY} + ${MCP_TOOLS.length} permission rules`);
|
|
588
633
|
|
|
589
634
|
// Materialize the install through the claude CLI: registers the marketplace
|
|
@@ -596,7 +641,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
596
641
|
const marketplaceCurrent = registered?.source?.source === 'directory'
|
|
597
642
|
&& normalizedPath(registered.source.path) === normalizedPath(root)
|
|
598
643
|
&& normalizedPath(registered.installLocation) === normalizedPath(root);
|
|
599
|
-
if (marketplaceCurrent && installedEntries.some((entry) => entry?.scope ===
|
|
644
|
+
if (marketplaceCurrent && installedEntries.some((entry) => entry?.scope === CLAUDE_PLUGIN_SCOPE
|
|
600
645
|
&& sameManagedClaudeFiles(root, entry.installPath))) {
|
|
601
646
|
actions.push('cli: skipped (registered marketplace and snapshot already current)');
|
|
602
647
|
return { ok: true, actions };
|
|
@@ -605,8 +650,22 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
605
650
|
actions.push('cli: skipped (non-default home; test mode)');
|
|
606
651
|
return { ok: true, actions };
|
|
607
652
|
}
|
|
653
|
+
// The CLI is best-effort, but a host that does not have it must not be told to
|
|
654
|
+
// run it. The checkout entry already gates on this; doing it here too keeps the
|
|
655
|
+
// two entries describing the same machine the same way.
|
|
656
|
+
const { execSync, spawnSync } = await import('node:child_process');
|
|
657
|
+
const claudePresent = spawnSync(/^win/.test(process.platform) ? 'where' : 'which', ['claude'], {
|
|
658
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
659
|
+
}).status === 0;
|
|
660
|
+
if (!claudePresent) {
|
|
661
|
+
actions.push('cli: skipped (claude not found)');
|
|
662
|
+
return { ok: true, detected: false, actions };
|
|
663
|
+
}
|
|
664
|
+
// Set only when a CLI step threw; only a timeout then justifies waiting for the
|
|
665
|
+
// snapshot, and `marketplace add` / `uninstall` can time out too — they are
|
|
666
|
+
// caught for other reasons and would otherwise hide it.
|
|
667
|
+
let installError = null;
|
|
608
668
|
try {
|
|
609
|
-
const { execSync } = await import('node:child_process');
|
|
610
669
|
// 300s, not 120: the install below runs after the uninstall, so it does a real
|
|
611
670
|
// copy of the plugin tree rather than the no-op an already-installed plugin
|
|
612
671
|
// gets. Measured on this machine: `marketplace add` 3s, `uninstall` 3s,
|
|
@@ -614,21 +673,23 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
614
673
|
// and left Claude Code without the plugin the same run had just removed.
|
|
615
674
|
const run = (cmd) => execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
616
675
|
try { run(`claude plugin marketplace add ${JSON.stringify(mpDir)}`); actions.push('cli: marketplace registered'); }
|
|
617
|
-
catch { actions.push('cli: marketplace add skipped (already registered)'); }
|
|
676
|
+
catch (err) { if (err?.code === 'ETIMEDOUT') installError = err; actions.push('cli: marketplace add skipped (already registered)'); }
|
|
618
677
|
// `plugin install` on an already-installed plugin is a no-op and leaves a
|
|
619
678
|
// stale snapshot in ~/.claude/plugins/cache — uninstall first so an update
|
|
620
679
|
// always re-copies the current code.
|
|
621
|
-
try { run(`claude plugin uninstall ${PLUGIN_KEY}`); }
|
|
680
|
+
try { run(`claude plugin uninstall ${PLUGIN_KEY}`); }
|
|
681
|
+
catch (err) { if (err?.code === 'ETIMEDOUT') installError = err; }
|
|
622
682
|
// Newer Claude Code (>= 2.1.x) dropped the -y flag; older builds accepted
|
|
623
683
|
// it. Try without it first, fall back to the legacy flag.
|
|
624
684
|
try {
|
|
625
|
-
run(`claude plugin install ${PLUGIN_KEY} --scope
|
|
685
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope ${CLAUDE_PLUGIN_SCOPE}`);
|
|
626
686
|
} catch (errNoFlag) {
|
|
627
687
|
if (!String(errNoFlag?.message ?? '').includes('unknown option')) throw errNoFlag;
|
|
628
|
-
run(`claude plugin install ${PLUGIN_KEY} --scope
|
|
688
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope ${CLAUDE_PLUGIN_SCOPE} -y`);
|
|
629
689
|
}
|
|
630
690
|
actions.push(`cli: plugin snapshot refreshed (${PLUGIN_KEY})`);
|
|
631
691
|
} catch (err) {
|
|
692
|
+
installError = err;
|
|
632
693
|
actions.push(`cli: plugin install failed — run manually: claude plugin install ${PLUGIN_KEY} (${String(err?.message ?? err).slice(0, 120)})`);
|
|
633
694
|
}
|
|
634
695
|
// Report the state that resulted, not the step that was attempted. The CLI is
|
|
@@ -637,7 +698,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
637
698
|
// or failed leaves the snapshot exactly as stale as it was, and it is the
|
|
638
699
|
// snapshot that `installStatus` reads. Saying so here is what lets the caller
|
|
639
700
|
// stop printing a checkmark for a state it never verified.
|
|
640
|
-
if (claudeSnapshotReady(home, root)) return { ok: true, actions };
|
|
701
|
+
if (claudeSnapshotReady(home, root, { scope: CLAUDE_PLUGIN_SCOPE })) return { ok: true, actions };
|
|
641
702
|
// The CLI can outlive the ceiling above. The install is a real copy of the
|
|
642
703
|
// plugin tree — 163s measured on an idle machine, and ~6 minutes measured
|
|
643
704
|
// during an activation, where the record landed well after any ceiling — and a
|
|
@@ -645,11 +706,11 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
645
706
|
// is still running keeps writing while this function has already given up on
|
|
646
707
|
// it. Wait for the snapshot to settle before reporting it missing, or the
|
|
647
708
|
// update says "not loaded" about a plugin that is loading.
|
|
648
|
-
const settleDeadline = Date.now() +
|
|
709
|
+
const settleDeadline = Date.now() + claudeSnapshotSettleMs(installError);
|
|
649
710
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
650
711
|
while (Date.now() < settleDeadline) {
|
|
651
712
|
await wait(CLAUDE_SNAPSHOT_POLL_MS);
|
|
652
|
-
if (claudeSnapshotReady(home, root)) {
|
|
713
|
+
if (claudeSnapshotReady(home, root, { scope: CLAUDE_PLUGIN_SCOPE })) {
|
|
653
714
|
return { ok: true, actions: [...actions, 'cli: snapshot confirmed current after the ceiling'] };
|
|
654
715
|
}
|
|
655
716
|
}
|
package/src/runtime-identity.mjs
CHANGED
|
@@ -36,7 +36,7 @@ export {
|
|
|
36
36
|
// included in the identity contract.
|
|
37
37
|
const RUNTIME_ID = randomUUID();
|
|
38
38
|
|
|
39
|
-
export const RUNTIME_VERSION = '2.0.
|
|
39
|
+
export const RUNTIME_VERSION = '2.0.10';
|
|
40
40
|
export const HUB_PROTOCOL_VERSION = 1;
|
|
41
41
|
|
|
42
42
|
export const HUB_CAPABILITIES = Object.freeze([
|