@ran-sh/dsh-crew 2.0.9 → 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,6 +528,9 @@ 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
|
}
|
|
@@ -530,8 +549,10 @@ export function claudeIntegrationLine(result) {
|
|
|
530
549
|
* without Claude Code, to learn nothing it did not already know.
|
|
531
550
|
*/
|
|
532
551
|
export function claudeSnapshotSettleMs(err) {
|
|
533
|
-
|
|
534
|
-
|
|
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;
|
|
535
556
|
}
|
|
536
557
|
|
|
537
558
|
export async function installClaudeCode({ home = homedir(), statusline = false, root = ROOT } = {}) {
|
|
@@ -548,58 +569,66 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
548
569
|
|
|
549
570
|
// 2. settings.json: marketplace + enabledPlugins + permissions allowlist.
|
|
550
571
|
const settingsFile = join(home, '.claude', 'settings.json');
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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;
|
|
568
590
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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;
|
|
594
616
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
+
}
|
|
601
623
|
|
|
602
|
-
|
|
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
|
+
}
|
|
603
632
|
actions.push(`settings: registered ${PLUGIN_KEY} + ${MCP_TOOLS.length} permission rules`);
|
|
604
633
|
|
|
605
634
|
// Materialize the install through the claude CLI: registers the marketplace
|
|
@@ -612,7 +641,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
612
641
|
const marketplaceCurrent = registered?.source?.source === 'directory'
|
|
613
642
|
&& normalizedPath(registered.source.path) === normalizedPath(root)
|
|
614
643
|
&& normalizedPath(registered.installLocation) === normalizedPath(root);
|
|
615
|
-
if (marketplaceCurrent && installedEntries.some((entry) => entry?.scope ===
|
|
644
|
+
if (marketplaceCurrent && installedEntries.some((entry) => entry?.scope === CLAUDE_PLUGIN_SCOPE
|
|
616
645
|
&& sameManagedClaudeFiles(root, entry.installPath))) {
|
|
617
646
|
actions.push('cli: skipped (registered marketplace and snapshot already current)');
|
|
618
647
|
return { ok: true, actions };
|
|
@@ -621,11 +650,22 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
621
650
|
actions.push('cli: skipped (non-default home; test mode)');
|
|
622
651
|
return { ok: true, actions };
|
|
623
652
|
}
|
|
624
|
-
//
|
|
625
|
-
//
|
|
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.
|
|
626
667
|
let installError = null;
|
|
627
668
|
try {
|
|
628
|
-
const { execSync } = await import('node:child_process');
|
|
629
669
|
// 300s, not 120: the install below runs after the uninstall, so it does a real
|
|
630
670
|
// copy of the plugin tree rather than the no-op an already-installed plugin
|
|
631
671
|
// gets. Measured on this machine: `marketplace add` 3s, `uninstall` 3s,
|
|
@@ -633,18 +673,19 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
633
673
|
// and left Claude Code without the plugin the same run had just removed.
|
|
634
674
|
const run = (cmd) => execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
|
|
635
675
|
try { run(`claude plugin marketplace add ${JSON.stringify(mpDir)}`); actions.push('cli: marketplace registered'); }
|
|
636
|
-
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)'); }
|
|
637
677
|
// `plugin install` on an already-installed plugin is a no-op and leaves a
|
|
638
678
|
// stale snapshot in ~/.claude/plugins/cache — uninstall first so an update
|
|
639
679
|
// always re-copies the current code.
|
|
640
|
-
try { run(`claude plugin uninstall ${PLUGIN_KEY}`); }
|
|
680
|
+
try { run(`claude plugin uninstall ${PLUGIN_KEY}`); }
|
|
681
|
+
catch (err) { if (err?.code === 'ETIMEDOUT') installError = err; }
|
|
641
682
|
// Newer Claude Code (>= 2.1.x) dropped the -y flag; older builds accepted
|
|
642
683
|
// it. Try without it first, fall back to the legacy flag.
|
|
643
684
|
try {
|
|
644
|
-
run(`claude plugin install ${PLUGIN_KEY} --scope
|
|
685
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope ${CLAUDE_PLUGIN_SCOPE}`);
|
|
645
686
|
} catch (errNoFlag) {
|
|
646
687
|
if (!String(errNoFlag?.message ?? '').includes('unknown option')) throw errNoFlag;
|
|
647
|
-
run(`claude plugin install ${PLUGIN_KEY} --scope
|
|
688
|
+
run(`claude plugin install ${PLUGIN_KEY} --scope ${CLAUDE_PLUGIN_SCOPE} -y`);
|
|
648
689
|
}
|
|
649
690
|
actions.push(`cli: plugin snapshot refreshed (${PLUGIN_KEY})`);
|
|
650
691
|
} catch (err) {
|
|
@@ -657,7 +698,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
657
698
|
// or failed leaves the snapshot exactly as stale as it was, and it is the
|
|
658
699
|
// snapshot that `installStatus` reads. Saying so here is what lets the caller
|
|
659
700
|
// stop printing a checkmark for a state it never verified.
|
|
660
|
-
if (claudeSnapshotReady(home, root)) return { ok: true, actions };
|
|
701
|
+
if (claudeSnapshotReady(home, root, { scope: CLAUDE_PLUGIN_SCOPE })) return { ok: true, actions };
|
|
661
702
|
// The CLI can outlive the ceiling above. The install is a real copy of the
|
|
662
703
|
// plugin tree — 163s measured on an idle machine, and ~6 minutes measured
|
|
663
704
|
// during an activation, where the record landed well after any ceiling — and a
|
|
@@ -669,7 +710,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
|
|
|
669
710
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
670
711
|
while (Date.now() < settleDeadline) {
|
|
671
712
|
await wait(CLAUDE_SNAPSHOT_POLL_MS);
|
|
672
|
-
if (claudeSnapshotReady(home, root)) {
|
|
713
|
+
if (claudeSnapshotReady(home, root, { scope: CLAUDE_PLUGIN_SCOPE })) {
|
|
673
714
|
return { ok: true, actions: [...actions, 'cli: snapshot confirmed current after the ceiling'] };
|
|
674
715
|
}
|
|
675
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([
|