@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.
- package/.claude-plugin/plugin.json +1 -1
- package/official-web-bridge/lib/client.js +1 -1
- package/official-web-bridge/overlay-entry.mjs +5 -1
- package/official-web-bridge/package.json +1 -1
- package/package.json +1 -1
- package/scripts/setup.mjs +39 -33
- package/scripts/verify-npm-install.mjs +2 -2
- package/src/adaptive-routing.mjs +2 -2
- package/src/ci-evidence.mjs +1 -1
- package/src/client/quick-panel.tsx +6 -2
- package/src/credential-lifecycle.mjs +0 -3
- package/src/credential-purge-state.mjs +1 -5
- package/src/credential-reference.mjs +0 -8
- package/src/crew-workspaces.mjs +1 -1
- package/src/delivery.mjs +2 -2
- package/src/dsh-cli-runtime.mjs +227 -30
- package/src/extension-contract.mjs +1 -1
- package/src/failure-classification.mjs +0 -10
- package/src/hub/entry.mjs +1 -1
- package/src/hub/index.mjs +57 -7
- package/src/i18n.mjs +0 -2
- package/src/install/crew-paths.mjs +16 -11
- package/src/install/crew-skill.mjs +1 -1
- package/src/install/desktop-profile.mjs +168 -0
- package/src/install/harness-config-import.mjs +204 -0
- package/src/install/install-legacy.mjs +3 -3
- package/src/install/npx-lifecycle.mjs +210 -125
- package/src/install/official-web.mjs +1 -1
- package/src/install/windows-startup.mjs +5 -5
- package/src/install/windows-supervisor-adapter.mjs +3 -2
- package/src/install/windows-supervisor-lifecycle.mjs +2 -2
- package/src/install/zcode.mjs +0 -2
- package/src/job-contracts.mjs +1 -1
- package/src/job-identity.mjs +2 -2
- package/src/jobs.mjs +3 -4
- package/src/model-routing.mjs +3 -4
- package/src/model-schedule.mjs +0 -1
- package/src/official-web-bridge.mjs +3 -1
- package/src/policy-legacy.mjs +10 -12
- package/src/provider-inventory.mjs +1 -1
- package/src/provider-layer-migration.mjs +0 -2
- package/src/provider-settings-store.mjs +19 -1
- package/src/readiness-matrix.mjs +1 -1
- package/src/release-in-use.mjs +1 -1
- package/src/role-profiles.mjs +2 -2
- package/src/runtime-controls.mjs +1 -1
- package/src/runtime-identity.mjs +1 -1
- package/src/status-shard.mjs +1 -1
- package/src/supervisor/restart-request.mjs +4 -4
- package/src/workflow-runtime.mjs +1 -2
- package/src/workspace-context.mjs +2 -2
- package/src/workspace-isolation.mjs +4 -4
- package/src/workspace-lock.mjs +2 -3
- package/scripts/policy-probe.mjs +0 -101
- package/scripts/verify-crew-ui-polish.mjs +0 -145
- package/scripts/verify-history-ui.mjs +0 -98
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|