@ran-sh/dsh-crew 2.2.0 → 2.2.2

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.
Files changed (57) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/official-web-bridge/lib/client.js +1 -1
  3. package/official-web-bridge/overlay-entry.mjs +5 -1
  4. package/official-web-bridge/package.json +1 -1
  5. package/package.json +1 -1
  6. package/scripts/setup.mjs +39 -33
  7. package/scripts/verify-npm-install.mjs +2 -2
  8. package/src/adaptive-routing.mjs +2 -2
  9. package/src/ci-evidence.mjs +1 -1
  10. package/src/client/quick-panel.tsx +6 -2
  11. package/src/credential-lifecycle.mjs +0 -3
  12. package/src/credential-purge-state.mjs +1 -5
  13. package/src/credential-reference.mjs +0 -8
  14. package/src/crew-workspaces.mjs +1 -1
  15. package/src/delivery.mjs +2 -2
  16. package/src/dsh-cli-runtime.mjs +227 -30
  17. package/src/extension-contract.mjs +1 -1
  18. package/src/failure-classification.mjs +0 -10
  19. package/src/hub/entry.mjs +1 -1
  20. package/src/hub/index.mjs +57 -7
  21. package/src/i18n.mjs +0 -2
  22. package/src/install/crew-paths.mjs +16 -11
  23. package/src/install/crew-skill.mjs +1 -1
  24. package/src/install/desktop-profile.mjs +168 -0
  25. package/src/install/harness-config-import.mjs +204 -0
  26. package/src/install/install-legacy.mjs +3 -3
  27. package/src/install/npx-lifecycle.mjs +210 -125
  28. package/src/install/official-web.mjs +1 -1
  29. package/src/install/windows-startup.mjs +5 -5
  30. package/src/install/windows-supervisor-adapter.mjs +3 -2
  31. package/src/install/windows-supervisor-lifecycle.mjs +2 -2
  32. package/src/install/zcode.mjs +0 -2
  33. package/src/job-contracts.mjs +1 -1
  34. package/src/job-identity.mjs +2 -2
  35. package/src/jobs.mjs +3 -4
  36. package/src/model-routing.mjs +3 -4
  37. package/src/model-schedule.mjs +0 -1
  38. package/src/official-web-bridge.mjs +3 -1
  39. package/src/policy-legacy.mjs +10 -12
  40. package/src/provider-inventory.mjs +1 -1
  41. package/src/provider-layer-migration.mjs +0 -2
  42. package/src/provider-settings-store.mjs +19 -1
  43. package/src/readiness-matrix.mjs +1 -1
  44. package/src/release-in-use.mjs +1 -1
  45. package/src/role-profiles.mjs +2 -2
  46. package/src/runtime-controls.mjs +1 -1
  47. package/src/runtime-identity.mjs +1 -1
  48. package/src/status-shard.mjs +1 -1
  49. package/src/supervisor/restart-request.mjs +4 -4
  50. package/src/workflow-runtime.mjs +1 -2
  51. package/src/workspace-context.mjs +2 -2
  52. package/src/workspace-isolation.mjs +4 -4
  53. package/src/workspace-lock.mjs +2 -3
  54. package/scripts/policy-probe.mjs +0 -101
  55. package/scripts/verify-crew-ui-polish.mjs +0 -145
  56. package/scripts/verify-history-ui.mjs +0 -98
  57. package/scripts/verify-installer-fix.mjs +0 -26
@@ -0,0 +1,168 @@
1
+ // Desktop-client integration for the official DSH desktop application.
2
+ //
3
+ // The desktop app (an Electron client) boots its own Harness against ~/.dsh with
4
+ // the app-exclusive `desktop` profile — the CLI refuses to manage that profile —
5
+ // and applies that profile's user patch layer, which the Harness hot-watches. So
6
+ // the only way the desktop app shows the Crew panel is an insert in
7
+ // <official home>/profiles/desktop/cordis.patch.yml pointing at the Crew web
8
+ // bridge snapshot. That file lives in the official home, which Crew otherwise
9
+ // treats as read-only: this module is reachable ONLY from the explicit
10
+ // `dsh-crew desktop attach|detach|status` commands and never runs during install,
11
+ // update or any supervision path. attach is idempotent and re-points a stale
12
+ // revision; detach removes exactly the block this module wrote, leaving the rest
13
+ // of the file byte for byte.
14
+
15
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { basename, dirname, join } from 'node:path';
17
+ import { homedir } from 'node:os';
18
+ import { fileURLToPath } from 'node:url';
19
+
20
+ const BRIDGE_ID = 'dsh-crew-official-web-bridge';
21
+ const BRIDGE_PACKAGE = '@ran-sh/dsh-crew-web-bridge';
22
+ const MARKER = '# dsh-crew desktop bridge (managed by dsh-crew; remove with: dsh-crew desktop detach)';
23
+ const REVISION_RE = /^[a-f0-9]{64}$/;
24
+
25
+ export function desktopPatchFile({ home = homedir() } = {}) {
26
+ return join(home, '.dsh', 'profiles', 'desktop', 'cordis.patch.yml');
27
+ }
28
+
29
+ /** The bridge entry the Crew launcher itself would load, straight from the overlay. */
30
+ export function crewBridgeEntry({ home = homedir() } = {}) {
31
+ const overlayFile = join(home, '.config', 'dsh-crew', 'frontend', 'official-web.patch.json');
32
+ let parsed;
33
+ try { parsed = JSON.parse(readFileSync(overlayFile, 'utf8')); } catch { return { ok: false, code: 'CREW_BRIDGE_OVERLAY_UNAVAILABLE', overlayFile }; }
34
+ const root = Array.isArray(parsed) && parsed.length === 1 ? parsed[0] : null;
35
+ const insert = root && Array.isArray(root.insert) && root.insert.length === 1 ? root.insert[0] : null;
36
+ const entryUri = insert && typeof insert.name === 'string' ? insert.name : null;
37
+ if (!entryUri || insert.id !== BRIDGE_ID || !entryUri.startsWith('file://')) {
38
+ return { ok: false, code: 'CREW_BRIDGE_OVERLAY_INVALID', overlayFile };
39
+ }
40
+ let entryPath;
41
+ try { entryPath = fileURLToPath(entryUri); } catch { return { ok: false, code: 'CREW_BRIDGE_OVERLAY_INVALID', overlayFile }; }
42
+ if (!existsSync(entryPath)) return { ok: false, code: 'CREW_BRIDGE_SNAPSHOT_MISSING', overlayFile, entry: entryUri };
43
+ // The launcher checks the same pair before it starts a frontend: the snapshot's
44
+ // own manifest must claim to be Crew-managed and must agree with the revision
45
+ // its directory is named after.
46
+ const revision = basename(dirname(dirname(entryPath)));
47
+ let manifest;
48
+ try { manifest = JSON.parse(readFileSync(join(dirname(entryPath), 'package.json'), 'utf8')); } catch { manifest = null; }
49
+ if (!manifest || manifest.name !== BRIDGE_PACKAGE || manifest.dshCrewManagedFrontend !== true
50
+ || manifest.dshCrewFrontendRevision !== revision || !REVISION_RE.test(revision)) {
51
+ return { ok: false, code: 'CREW_BRIDGE_SNAPSHOT_INVALID', overlayFile, entry: entryUri, revision };
52
+ }
53
+ return { ok: true, overlayFile, entry: entryUri, entryPath, revision };
54
+ }
55
+
56
+ function blockFor(entryUri, eol) {
57
+ return [
58
+ MARKER,
59
+ '- insert:',
60
+ ` - id: ${BRIDGE_ID}`,
61
+ ` name: '${entryUri}'`,
62
+ '',
63
+ ].join(eol);
64
+ }
65
+
66
+ function blockPattern(eol) {
67
+ const e = eol === '\r\n' ? '\\r\\n' : '\\n';
68
+ return new RegExp(
69
+ `^${MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}${e}- insert:${e} {4}- id: ${BRIDGE_ID}${e} {6}name: '[^'${e}]*'${e}?`,
70
+ 'm',
71
+ );
72
+ }
73
+
74
+ function detectsEol(text) {
75
+ return text.includes('\r\n') ? '\r\n' : '\n';
76
+ }
77
+
78
+ // Fail closed on a patch layer whose shape this module does not understand: a
79
+ // top-level YAML array of patch entries. Anything else is somebody's file.
80
+ function looksLikePatchArray(text) {
81
+ const firstMeaningful = text.split(/\r?\n/).find((line) => line.trim() !== '' && !line.trimStart().startsWith('#'));
82
+ if (firstMeaningful === undefined) return true;
83
+ const trimmed = firstMeaningful.trimStart();
84
+ return trimmed.startsWith('-') || trimmed === '[]' || trimmed === '---';
85
+ }
86
+
87
+ export function desktopStatus({ home = homedir() } = {}) {
88
+ const patchFile = desktopPatchFile({ home });
89
+ const bridge = crewBridgeEntry({ home });
90
+ let text = null;
91
+ try { text = readFileSync(patchFile, 'utf8'); } catch { text = null; }
92
+ const eol = text === null ? '\n' : detectsEol(text);
93
+ const block = text === null ? null : blockPattern(eol).exec(text);
94
+ return {
95
+ ok: true,
96
+ path: patchFile,
97
+ file_present: text !== null,
98
+ bridge: bridge.ok ? { entry: bridge.entry, revision: bridge.revision } : null,
99
+ bridge_error: bridge.ok ? null : bridge.code,
100
+ attached: block !== null,
101
+ current: block !== null && bridge.ok ? block[0].includes(bridge.entry) : false,
102
+ };
103
+ }
104
+
105
+ function writeAtomic(file, text) {
106
+ const temp = `${file}.dsh-crew.${process.pid}.tmp`;
107
+ writeFileSync(temp, text);
108
+ renameSync(temp, file);
109
+ }
110
+
111
+ export function desktopAttach({ home = homedir(), dryRun = false, log = () => {} } = {}) {
112
+ const bridge = crewBridgeEntry({ home });
113
+ if (!bridge.ok) return { ok: false, code: bridge.code, error: `Crew bridge snapshot is unavailable (${bridge.code})` };
114
+ const patchFile = desktopPatchFile({ home });
115
+ let text = null;
116
+ try { text = readFileSync(patchFile, 'utf8'); } catch { text = null; }
117
+ const eol = text === null ? '\n' : detectsEol(text);
118
+ if (text !== null && !looksLikePatchArray(text)) {
119
+ return { ok: false, code: 'DESKTOP_PATCH_SHAPE_UNSUPPORTED', error: `${patchFile} is not a top-level patch array; refusing to edit it`, path: patchFile };
120
+ }
121
+ const block = blockFor(bridge.entry, eol);
122
+ const withoutBlock = text === null ? null : text.replace(blockPattern(eol), '');
123
+ const current = withoutBlock !== null && text.includes(bridge.entry) && withoutBlock !== text;
124
+ const base = withoutBlock ?? '';
125
+ const separator = base === '' || base.endsWith('\n') ? '' : eol;
126
+ const next = `${base}${separator}${block}`;
127
+ if (text !== null && next === text) {
128
+ log(`- desktop app already carries the Crew bridge for revision ${bridge.revision}`);
129
+ return { ok: true, changed: false, path: patchFile, entry: bridge.entry, revision: bridge.revision };
130
+ }
131
+ if (dryRun) {
132
+ log(`- would ${current ? 're-point' : 'attach'} the Crew bridge in ${patchFile} (revision ${bridge.revision})`);
133
+ return { ok: true, changed: true, dryRun: true, path: patchFile, entry: bridge.entry, revision: bridge.revision };
134
+ }
135
+ mkdirSync(dirname(patchFile), { recursive: true });
136
+ writeAtomic(patchFile, next);
137
+ log(`✓ desktop app ${current ? 're-pointed to' : 'attached to'} the Crew bridge (revision ${bridge.revision})`);
138
+ log(' the Harness hot-watches this patch layer; if the panel does not appear, restart the desktop app');
139
+ return { ok: true, changed: true, path: patchFile, entry: bridge.entry, revision: bridge.revision };
140
+ }
141
+
142
+ export function desktopDetach({ home = homedir(), dryRun = false, log = () => {} } = {}) {
143
+ const patchFile = desktopPatchFile({ home });
144
+ let text;
145
+ try { text = readFileSync(patchFile, 'utf8'); } catch {
146
+ log('- desktop patch layer is absent; nothing to remove');
147
+ return { ok: true, changed: false, path: patchFile };
148
+ }
149
+ const eol = detectsEol(text);
150
+ const next = text.replace(blockPattern(eol), '');
151
+ if (next === text) {
152
+ log('- the Crew bridge is not present in the desktop patch layer');
153
+ return { ok: true, changed: false, path: patchFile };
154
+ }
155
+ if (dryRun) return { ok: true, changed: true, dryRun: true, path: patchFile };
156
+ // A file this module created holds only its own block: remove the file rather
157
+ // than leave an empty one behind.
158
+ if (next.trim() === '') {
159
+ rmSync(patchFile, { force: true });
160
+ log('✓ removed the Crew bridge from the desktop patch layer (the file held nothing else)');
161
+ return { ok: true, changed: true, path: patchFile, removed_file: true };
162
+ }
163
+ writeAtomic(patchFile, next);
164
+ log('✓ removed the Crew bridge from the desktop patch layer');
165
+ return { ok: true, changed: true, path: patchFile };
166
+ }
167
+
168
+ export { MARKER as DESKTOP_BRIDGE_MARKER, BRIDGE_ID as DESKTOP_BRIDGE_ID };
@@ -0,0 +1,204 @@
1
+ // Mirror the operator's user-level DSH configuration from the OFFICIAL home into
2
+ // Crew's own home, so the Crew hub works with the same models, providers, policy
3
+ // and MCP servers as the desktop app the operator actually uses.
4
+ //
5
+ // The official home is read-only here — nothing under ~/.dsh is ever written.
6
+ // Secrets are never copied either: provider entries reference their keys through
7
+ // `apiKeyEnv`, and those variables live in the operator's user environment, which
8
+ // every harness process inherits.
9
+ //
10
+ // Two hazards shape the rules:
11
+ // 1. The desktop app runs a DIFFERENT cohort than Crew pins (0.1.7-rc.2 vs
12
+ // 0.1.6-alpha.1), so an entry may name a package Crew's runtime does not
13
+ // have. The Harness installs patches fail-loud, so such an entry would break
14
+ // the hub: it is skipped and reported instead.
15
+ // 2. Crew's own managed blocks (the desktop bridge insert) are not user
16
+ // configuration and are never mirrored.
17
+
18
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { homedir } from 'node:os';
21
+ import { crewDshHome } from './install.mjs';
22
+ import { crewDshRuntimeRoot } from '../dsh-cli-runtime.mjs';
23
+ import { DESKTOP_BRIDGE_ID, DESKTOP_BRIDGE_MARKER } from './desktop-profile.mjs';
24
+
25
+ const CREW_PROFILE_NAME = 'dsh-crew';
26
+
27
+ export function officialHomeDir({ home = homedir() } = {}) {
28
+ return join(home, '.dsh');
29
+ }
30
+
31
+ export function configMirrorTargets({ home = homedir() } = {}) {
32
+ const crewHome = crewDshHome({ home });
33
+ const official = officialHomeDir({ home });
34
+ return {
35
+ crewHome,
36
+ homeLayer: {
37
+ label: 'home layer (MCP servers)',
38
+ source: join(official, 'cordis.patch.yml'),
39
+ target: join(crewHome, 'cordis.patch.yml'),
40
+ },
41
+ profileLayer: {
42
+ label: 'profile layer (models/providers/policy)',
43
+ source: join(official, 'profiles', 'desktop', 'cordis.patch.yml'),
44
+ target: join(crewHome, 'profiles', CREW_PROFILE_NAME, 'cordis.patch.yml'),
45
+ },
46
+ };
47
+ }
48
+
49
+ // Does the Harness's resolution of this specifier succeed inside the Crew runtime
50
+ // tree? npm nests whole subtrees under family packages, so the nested scopes are
51
+ // checked too — that is where a hoisted sibling actually resolves from.
52
+ function resolvesInRuntime(runtimeRoot, specifier) {
53
+ if (typeof specifier !== 'string' || specifier === '') return true;
54
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('file:')) return true;
55
+ const moduleRoot = join(runtimeRoot, 'node_modules');
56
+ const parts = specifier.split('/');
57
+ if (existsSync(join(moduleRoot, ...parts))) return true;
58
+ const scopeDir = join(moduleRoot, '@deepseek-ai');
59
+ let packages;
60
+ try { packages = readdirSync(scopeDir); } catch { return false; }
61
+ for (const name of packages) {
62
+ const nested = join(scopeDir, name, 'node_modules');
63
+ if (existsSync(join(nested, ...parts))) return true;
64
+ let inner;
65
+ try { inner = readdirSync(join(nested, '@deepseek-ai')); } catch { continue; }
66
+ for (const deeper of inner) {
67
+ if (existsSync(join(nested, '@deepseek-ai', deeper, 'node_modules', ...parts))) return true;
68
+ }
69
+ }
70
+ return false;
71
+ }
72
+
73
+ // Split a patch layer into its top-level entries, verbatim. A comment at column 0
74
+ // is NOT part of the preceding entry — it separates entries (Crew's own managed
75
+ // block marker sits between entries, and attaching it to the entry above would
76
+ // drop that entry from the mirror).
77
+ function splitEntries(text) {
78
+ const entries = [];
79
+ let current = null;
80
+ const flush = () => { if (current) entries.push(current); current = null; };
81
+ for (const line of text.split(/\r?\n/)) {
82
+ if (line.startsWith('- ') || line === '-') {
83
+ flush();
84
+ current = [line];
85
+ continue;
86
+ }
87
+ if (line.startsWith('#')) {
88
+ flush();
89
+ entries.push([line]);
90
+ continue;
91
+ }
92
+ if (current) current.push(line);
93
+ }
94
+ flush();
95
+ return entries.map((lines) => lines.join('\n').replace(/\s+$/, ''));
96
+ }
97
+
98
+ function entryMeta(entry) {
99
+ return {
100
+ id: entry.match(/^- id:[ \t]*(\S+)/m)?.[1] ?? null,
101
+ insertId: entry.match(/^[ \t]+- id:[ \t]*(\S+)/m)?.[1] ?? null,
102
+ name: entry.match(/^[ \t]+name:[ \t]*["']?([^"'\s]+)/m)?.[1] ?? null,
103
+ };
104
+ }
105
+
106
+ function isOwnManagedBlock(entry, meta) {
107
+ return entry.includes(DESKTOP_BRIDGE_MARKER) || meta.id === DESKTOP_BRIDGE_ID || meta.insertId === DESKTOP_BRIDGE_ID;
108
+ }
109
+
110
+ function mirrorLayer({ layer, crewHome, runtimeRoot, dryRun, log }) {
111
+ const { source, target, label } = layer;
112
+ if (!existsSync(source)) {
113
+ log(`- ${label}: no official source at ${source}; nothing to mirror`);
114
+ return { ok: true, changed: false, entries: 0, skipped: [], source, target, label };
115
+ }
116
+ const eol = readFileSync(source, 'utf8').includes('\r\n') ? '\r\n' : '\n';
117
+ const kept = [];
118
+ const skipped = [];
119
+ const isCommentOnly = (entry) => entry.split(/\r?\n/).every((line) => line.trim() === '' || line.startsWith('#'));
120
+ for (const entry of splitEntries(readFileSync(source, 'utf8'))) {
121
+ if (entry.trim() === '') continue;
122
+ if (isCommentOnly(entry) && !entry.includes(DESKTOP_BRIDGE_MARKER)) continue; // file prose, not configuration
123
+ const meta = entryMeta(entry);
124
+ if (isOwnManagedBlock(entry, meta)) {
125
+ skipped.push({ id: meta.insertId ?? meta.id, reason: 'crew-managed' });
126
+ continue;
127
+ }
128
+ if (!meta.id && !meta.insertId) {
129
+ skipped.push({ id: null, reason: 'unrecognized-entry' });
130
+ continue;
131
+ }
132
+ if (!resolvesInRuntime(runtimeRoot, meta.name)) {
133
+ skipped.push({ id: meta.id ?? meta.insertId, name: meta.name, reason: 'not-in-crew-cohort' });
134
+ continue;
135
+ }
136
+ kept.push(entry);
137
+ }
138
+ const header = [
139
+ `# Mirrored from ${source} by dsh-crew config import.`,
140
+ '# Crew keeps its own DSH home; this layer carries the operator\'s user-level',
141
+ '# configuration so the Crew hub matches the desktop app. Edit the source and',
142
+ '# re-run the import — changes here are overwritten.',
143
+ '',
144
+ ].join(eol);
145
+ const body = kept.length ? `${header}${kept.join(`${eol}${eol}`)}${eol}` : '';
146
+ const existing = existsSync(target) ? readFileSync(target, 'utf8') : null;
147
+ for (const item of skipped) {
148
+ log(` ! skipped ${item.id ?? '(unknown entry)'}${item.name ? ` [${item.name}]` : ''}: ${
149
+ item.reason === 'not-in-crew-cohort'
150
+ ? 'package is not in the Crew runtime cohort'
151
+ : item.reason === 'crew-managed' ? 'Crew-managed block' : 'unrecognized entry shape'
152
+ }`);
153
+ }
154
+ if (existing === body) {
155
+ log(`- ${label}: already mirrored (${kept.length} entries)`);
156
+ return { ok: true, changed: false, entries: kept.length, skipped, source, target, label };
157
+ }
158
+ if (dryRun) {
159
+ log(`- ${label}: would mirror ${kept.length} entries -> ${target}`);
160
+ return { ok: true, changed: true, dryRun: true, entries: kept.length, skipped, source, target, label };
161
+ }
162
+ if (!existsSync(dirname(target))) {
163
+ return { ok: false, code: 'CREW_HOME_LAYER_MISSING', error: `Crew directory is absent: ${dirname(target)}`, label };
164
+ }
165
+ const temp = `${target}.dsh-crew.${process.pid}.tmp`;
166
+ writeFileSync(temp, body);
167
+ renameSync(temp, target);
168
+ log(`✓ ${label}: mirrored ${kept.length} entries -> ${target}`);
169
+ return { ok: true, changed: true, entries: kept.length, skipped, source, target, label };
170
+ }
171
+
172
+ export function mirrorOfficialHarnessConfig({ home = homedir(), dryRun = false, log = () => {} } = {}) {
173
+ const targets = configMirrorTargets({ home });
174
+ const runtimeRoot = crewDshRuntimeRoot({ home });
175
+ const layers = [];
176
+ for (const layer of [targets.homeLayer, targets.profileLayer]) {
177
+ const result = mirrorLayer({ layer, crewHome: targets.crewHome, runtimeRoot, dryRun, log });
178
+ layers.push(result);
179
+ if (!result.ok) return { ok: false, code: result.code, error: result.error, layers };
180
+ }
181
+ return {
182
+ ok: true,
183
+ changed: layers.some((layer) => layer.changed),
184
+ dryRun,
185
+ layers: layers.map(({ label, source, target, changed, entries, skipped }) => ({ label, source, target, changed, entries, skipped })),
186
+ };
187
+ }
188
+
189
+ export function officialConfigMirrorStatus({ home = homedir() } = {}) {
190
+ const targets = configMirrorTargets({ home });
191
+ const inspect = (layer) => {
192
+ let targetText = null;
193
+ try { targetText = readFileSync(layer.target, 'utf8'); } catch { targetText = null; }
194
+ return {
195
+ label: layer.label,
196
+ source: layer.source,
197
+ source_present: existsSync(layer.source),
198
+ target: layer.target,
199
+ target_present: targetText !== null,
200
+ mirrored: targetText !== null && targetText.includes('by dsh-crew config import'),
201
+ };
202
+ };
203
+ return { ok: true, home_layer: inspect(targets.homeLayer), profile_layer: inspect(targets.profileLayer) };
204
+ }
@@ -57,7 +57,7 @@ const REGEX_MAY_FOLLOW = new Set([
57
57
  * from the raw text, so a keyword inside a comment or a string is not a
58
58
  * declaration and a quote inside a regex is not a string.
59
59
  */
60
- export function tokenizeModuleSource(source) {
60
+ function tokenizeModuleSource(source) {
61
61
  const tokens = [];
62
62
  const text = String(source ?? '');
63
63
  let i = 0;
@@ -748,7 +748,7 @@ export function pickClaudeCommand(candidates, { platform = process.platform } =
748
748
  ?? list[0];
749
749
  }
750
750
 
751
- export function resolveClaudeCommand({ platform = process.platform } = {}) {
751
+ function resolveClaudeCommand({ platform = process.platform } = {}) {
752
752
  const probe = spawnSync(platform === 'win32' ? 'where' : 'which', ['claude'], {
753
753
  encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
754
754
  });
@@ -1019,7 +1019,7 @@ export async function installClaudeCode({ home = homedir(), statusline = false,
1019
1019
  }
1020
1020
 
1021
1021
  // Keep CLI transaction ordering testable without changing the operator's home.
1022
- export async function refreshClaudePlugin({ home, root, actions = [], claudeCommand, runStep = runClaudeStep }) {
1022
+ async function refreshClaudePlugin({ home, root, actions = [], claudeCommand, runStep = runClaudeStep }) {
1023
1023
  // Each step runs to completion — including ending the whole process tree on a
1024
1024
  // timeout — before the next one starts, and the settle window is the widest any
1025
1025
  // step asks for. Two things this replaces: an `uninstall` left running past its