@ran-sh/dsh-crew 0.3.7 → 0.3.8
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 +36 -67
- package/README.zh.md +37 -68
- package/lib/client.js +3446 -3446
- package/official-web-bridge/cordis.patch.yml +4 -0
- package/official-web-bridge/entry.mjs +1 -0
- package/official-web-bridge/lib/client.js +3446 -0
- package/official-web-bridge/package.json +26 -0
- package/package.json +5 -3
- package/scripts/build-client.mjs +5 -1
- package/scripts/verify-official-bridge-e2e.mjs +159 -0
- package/src/dsh-cli-runtime.mjs +219 -208
- package/src/install/npx-lifecycle.mjs +67 -3
- package/src/install/official-web.mjs +132 -0
- package/src/official-web-bridge.mjs +196 -0
- package/src/runtime-identity.mjs +1 -1
package/src/dsh-cli-runtime.mjs
CHANGED
|
@@ -5,27 +5,27 @@
|
|
|
5
5
|
// compatibility fallbacks. Status and uninstall callers can resolve without
|
|
6
6
|
// allowing a download.
|
|
7
7
|
|
|
8
|
-
import { spawnSync } from 'node:child_process';
|
|
9
|
-
import { createRequire } from 'node:module';
|
|
10
|
-
import {
|
|
11
|
-
existsSync,
|
|
12
|
-
lstatSync,
|
|
13
|
-
mkdirSync,
|
|
14
|
-
readFileSync,
|
|
15
|
-
realpathSync,
|
|
16
|
-
symlinkSync,
|
|
17
|
-
unlinkSync,
|
|
18
|
-
writeFileSync,
|
|
19
|
-
} from 'node:fs';
|
|
20
|
-
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
|
+
import { spawnSync } from 'node:child_process';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import {
|
|
11
|
+
existsSync,
|
|
12
|
+
lstatSync,
|
|
13
|
+
mkdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
symlinkSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from 'node:fs';
|
|
20
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { crewDshHome, crewProfileDir } from './install/install.mjs';
|
|
23
23
|
|
|
24
|
-
export const DSH_CLI_PACKAGE = '@deepseek-ai/dsh';
|
|
25
|
-
export const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
|
|
26
|
-
const CREW_PROFILE_DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base'];
|
|
27
|
-
const PROFILE_PATCH_TEMPLATE = '[]\n';
|
|
28
|
-
const PROFILE_PNPM_WORKSPACE = 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n';
|
|
24
|
+
export const DSH_CLI_PACKAGE = '@deepseek-ai/dsh';
|
|
25
|
+
export const CREW_DSH_RUNTIME_DIRNAME = 'runtime';
|
|
26
|
+
const CREW_PROFILE_DEFAULT_BUNDLES = ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'];
|
|
27
|
+
const PROFILE_PATCH_TEMPLATE = '[]\n';
|
|
28
|
+
const PROFILE_PNPM_WORKSPACE = 'packages:\n - .\n\nnodeLinker: hoisted\nautoInstallPeers: false\n';
|
|
29
29
|
|
|
30
30
|
function defaultFindCommand(name) {
|
|
31
31
|
const probe = process.platform === 'win32' ? 'where.exe' : 'which';
|
|
@@ -236,170 +236,181 @@ export function ensureCrewDshRuntime({
|
|
|
236
236
|
return { ok: true, cli, reused: false, version: cli.version, runtimeRoot };
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
export function describeDshCli(cli) {
|
|
239
|
+
export function describeDshCli(cli) {
|
|
240
240
|
if (!cli) return 'unavailable';
|
|
241
241
|
const version = cli.version ? `@${cli.version}` : '';
|
|
242
242
|
return `${cli.kind}${version}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function isObject(value) {
|
|
246
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function isSafePackageName(name) {
|
|
250
|
-
return typeof name === 'string'
|
|
251
|
-
&& /^(?:@[^/\s]+\/)?[^/\s]+$/u.test(name)
|
|
252
|
-
&& !name.split('/').some((part) => part === '.' || part === '..');
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function isWithin(root, candidate) {
|
|
256
|
-
const rel = relative(root, candidate);
|
|
257
|
-
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function fileSpec(target) {
|
|
261
|
-
return `link:${target.replace(/\\/g, '/')}`;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function profileLinkPath(profileRoot, name) {
|
|
265
|
-
return join(profileRoot, 'node_modules', ...name.split('/'));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function readPluginRoot(root, expectedName) {
|
|
269
|
-
if (typeof root !== 'string' || !isAbsolute(root)) return { ok: false, code: 'INVALID_CREW_PLUGIN_ROOT' };
|
|
270
|
-
let targetRoot;
|
|
271
|
-
try { targetRoot = realpathSync(root); } catch { return { ok: false, code: 'CREW_PLUGIN_ROOT_NOT_FOUND' }; }
|
|
272
|
-
const packageFile = join(targetRoot, 'package.json');
|
|
273
|
-
let pkg;
|
|
274
|
-
try { pkg = JSON.parse(readFileSync(packageFile, 'utf8')); } catch { return { ok: false, code: 'CREW_PLUGIN_MANIFEST_INVALID' }; }
|
|
275
|
-
if (!isObject(pkg) || !isSafePackageName(pkg.name) || (expectedName !== undefined && pkg.name !== expectedName)) {
|
|
276
|
-
return { ok: false, code: 'CREW_PLUGIN_MANIFEST_INVALID' };
|
|
277
|
-
}
|
|
278
|
-
const patchSpec = pkg.dsh?.bundle?.patch;
|
|
279
|
-
if (typeof patchSpec !== 'string' || !patchSpec.trim()) return { ok: false, code: 'CREW_PLUGIN_BUNDLE_INVALID' };
|
|
280
|
-
const patchPath = resolve(targetRoot, patchSpec);
|
|
281
|
-
if (!isWithin(targetRoot, patchPath) || !existsSync(patchPath)) return { ok: false, code: 'CREW_PLUGIN_BUNDLE_INVALID' };
|
|
282
|
-
return { ok: true, targetRoot, packageFile, packageName: pkg.name, packageVersion: pkg.version ?? null, patchPath, pkg };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function readProfileManifestForRegistration(profileRoot) {
|
|
286
|
-
const profileManifest = join(profileRoot, 'package.json');
|
|
287
|
-
if (!existsSync(profileManifest)) {
|
|
288
|
-
return {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
if (
|
|
303
|
-
if (manifest.dependencies &&
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (manifest.dsh
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (stat) {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function isObject(value) {
|
|
246
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isSafePackageName(name) {
|
|
250
|
+
return typeof name === 'string'
|
|
251
|
+
&& /^(?:@[^/\s]+\/)?[^/\s]+$/u.test(name)
|
|
252
|
+
&& !name.split('/').some((part) => part === '.' || part === '..');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function isWithin(root, candidate) {
|
|
256
|
+
const rel = relative(root, candidate);
|
|
257
|
+
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function fileSpec(target) {
|
|
261
|
+
return `link:${target.replace(/\\/g, '/')}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function profileLinkPath(profileRoot, name) {
|
|
265
|
+
return join(profileRoot, 'node_modules', ...name.split('/'));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function readPluginRoot(root, expectedName) {
|
|
269
|
+
if (typeof root !== 'string' || !isAbsolute(root)) return { ok: false, code: 'INVALID_CREW_PLUGIN_ROOT' };
|
|
270
|
+
let targetRoot;
|
|
271
|
+
try { targetRoot = realpathSync(root); } catch { return { ok: false, code: 'CREW_PLUGIN_ROOT_NOT_FOUND' }; }
|
|
272
|
+
const packageFile = join(targetRoot, 'package.json');
|
|
273
|
+
let pkg;
|
|
274
|
+
try { pkg = JSON.parse(readFileSync(packageFile, 'utf8')); } catch { return { ok: false, code: 'CREW_PLUGIN_MANIFEST_INVALID' }; }
|
|
275
|
+
if (!isObject(pkg) || !isSafePackageName(pkg.name) || (expectedName !== undefined && pkg.name !== expectedName)) {
|
|
276
|
+
return { ok: false, code: 'CREW_PLUGIN_MANIFEST_INVALID' };
|
|
277
|
+
}
|
|
278
|
+
const patchSpec = pkg.dsh?.bundle?.patch;
|
|
279
|
+
if (typeof patchSpec !== 'string' || !patchSpec.trim()) return { ok: false, code: 'CREW_PLUGIN_BUNDLE_INVALID' };
|
|
280
|
+
const patchPath = resolve(targetRoot, patchSpec);
|
|
281
|
+
if (!isWithin(targetRoot, patchPath) || !existsSync(patchPath)) return { ok: false, code: 'CREW_PLUGIN_BUNDLE_INVALID' };
|
|
282
|
+
return { ok: true, targetRoot, packageFile, packageName: pkg.name, packageVersion: pkg.version ?? null, patchPath, pkg };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function readProfileManifestForRegistration(profileRoot, { create = true, defaultBundles = CREW_PROFILE_DEFAULT_BUNDLES } = {}) {
|
|
286
|
+
const profileManifest = join(profileRoot, 'package.json');
|
|
287
|
+
if (!existsSync(profileManifest)) {
|
|
288
|
+
if (!create) return { ok: false, code: 'PROFILE_NOT_FOUND', profileManifest };
|
|
289
|
+
return {
|
|
290
|
+
profileManifest,
|
|
291
|
+
manifest: {
|
|
292
|
+
name: `dsh-profile-${profileRoot.split(/[\\/]/u).at(-1)}`,
|
|
293
|
+
private: true,
|
|
294
|
+
dependencies: {},
|
|
295
|
+
dsh: { profile: { bundles: [...defaultBundles] } },
|
|
296
|
+
},
|
|
297
|
+
created: true,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
let manifest;
|
|
301
|
+
try { manifest = JSON.parse(readFileSync(profileManifest, 'utf8')); } catch { return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' }; }
|
|
302
|
+
if (!isObject(manifest)) return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
303
|
+
if (manifest.dependencies !== undefined && !isObject(manifest.dependencies)) return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
304
|
+
if (manifest.dependencies && Object.values(manifest.dependencies).some((value) => typeof value !== 'string')) {
|
|
305
|
+
return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
306
|
+
}
|
|
307
|
+
if (manifest.dsh !== undefined && !isObject(manifest.dsh)) return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
308
|
+
if (manifest.dsh?.profile !== undefined && !isObject(manifest.dsh.profile)) return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
309
|
+
const bundles = manifest.dsh?.profile?.bundles;
|
|
310
|
+
if (bundles !== undefined && (!Array.isArray(bundles) || bundles.some((name) => !isSafePackageName(name)))) {
|
|
311
|
+
return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
312
|
+
}
|
|
313
|
+
return { profileManifest, manifest, created: false };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function ensureProfileScaffold(profileRoot) {
|
|
317
|
+
mkdirSync(profileRoot, { recursive: true });
|
|
318
|
+
let changed = false;
|
|
319
|
+
const patchFile = join(profileRoot, 'cordis.patch.yml');
|
|
320
|
+
if (!existsSync(patchFile)) { writeFileSync(patchFile, PROFILE_PATCH_TEMPLATE); changed = true; }
|
|
321
|
+
const workspaceFile = join(profileRoot, 'pnpm-workspace.yaml');
|
|
322
|
+
if (!existsSync(workspaceFile)) { writeFileSync(workspaceFile, PROFILE_PNPM_WORKSPACE); changed = true; }
|
|
323
|
+
return changed;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function ensureDirectoryLink(linkPath, targetRoot) {
|
|
327
|
+
let stat;
|
|
328
|
+
try { stat = lstatSync(linkPath); } catch { stat = null; }
|
|
329
|
+
if (stat && !stat.isSymbolicLink()) return { ok: false, code: 'CREW_PLUGIN_LINK_CONFLICT' };
|
|
330
|
+
if (stat) {
|
|
331
|
+
try {
|
|
332
|
+
if (realpathSync(linkPath) === targetRoot) return { ok: true, changed: false };
|
|
333
|
+
} catch {}
|
|
334
|
+
unlinkSync(linkPath);
|
|
335
|
+
}
|
|
336
|
+
mkdirSync(dirname(linkPath), { recursive: true });
|
|
337
|
+
symlinkSync(targetRoot, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
338
|
+
return { ok: true, changed: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Register a local Crew plugin without invoking DSH's pnpm-forwarding plugin
|
|
343
|
+
* command. The profile manifest and one loader-visible directory link are the
|
|
344
|
+
* only state changed; no dependency resolution or policy bypass is attempted.
|
|
345
|
+
*/
|
|
346
|
+
export function ensurePluginRegistration({
|
|
347
|
+
profileRoot,
|
|
348
|
+
root,
|
|
349
|
+
name,
|
|
350
|
+
createProfile = true,
|
|
351
|
+
defaultBundles = CREW_PROFILE_DEFAULT_BUNDLES,
|
|
352
|
+
} = {}) {
|
|
353
|
+
const plugin = readPluginRoot(root, name);
|
|
354
|
+
if (!plugin.ok) return plugin;
|
|
355
|
+
if (typeof profileRoot !== 'string' || !isAbsolute(profileRoot)) return { ok: false, code: 'INVALID_PROFILE_ROOT' };
|
|
356
|
+
const profile = readProfileManifestForRegistration(profileRoot, { create: createProfile, defaultBundles });
|
|
357
|
+
if (profile.ok === false) return profile;
|
|
358
|
+
const scaffoldChanged = createProfile ? ensureProfileScaffold(profileRoot) : false;
|
|
359
|
+
const current = profile.manifest;
|
|
360
|
+
const dependencies = { ...(current.dependencies ?? {}) };
|
|
361
|
+
const currentBundles = current.dsh?.profile?.bundles ?? [...CREW_PROFILE_DEFAULT_BUNDLES];
|
|
362
|
+
const nextBundles = [];
|
|
363
|
+
let seen = false;
|
|
364
|
+
for (const bundle of currentBundles) {
|
|
365
|
+
if (bundle === plugin.packageName) {
|
|
366
|
+
if (!seen) { nextBundles.push(bundle); seen = true; }
|
|
367
|
+
} else nextBundles.push(bundle);
|
|
368
|
+
}
|
|
369
|
+
if (!seen) nextBundles.push(plugin.packageName);
|
|
370
|
+
dependencies[plugin.packageName] = fileSpec(plugin.targetRoot);
|
|
371
|
+
const next = {
|
|
372
|
+
...current,
|
|
373
|
+
dependencies,
|
|
374
|
+
dsh: {
|
|
375
|
+
...(current.dsh ?? {}),
|
|
376
|
+
profile: { ...(current.dsh?.profile ?? {}), bundles: nextBundles },
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
const manifestChanged = JSON.stringify(current) !== JSON.stringify(next) || profile.created;
|
|
380
|
+
const linkPath = profileLinkPath(profileRoot, plugin.packageName);
|
|
381
|
+
let link;
|
|
382
|
+
try { link = ensureDirectoryLink(linkPath, plugin.targetRoot); } catch { return { ok: false, code: 'CREW_PLUGIN_LINK_FAILED' }; }
|
|
383
|
+
if (!link.ok) return link;
|
|
384
|
+
try {
|
|
385
|
+
if (manifestChanged) writeFileSync(profile.profileManifest, JSON.stringify(next, null, 2) + '\n');
|
|
386
|
+
// Resolve the intended package entry from the validated checkout. Node's
|
|
387
|
+
// package-name resolution cache can retain the old target after a stale
|
|
388
|
+
// junction is replaced in the same process, so loader visibility is proven
|
|
389
|
+
// by the link target plus a normal package entry resolution from that
|
|
390
|
+
// target rather than by trusting a cached package-name lookup.
|
|
391
|
+
const resolvedEntry = createRequire(join(plugin.targetRoot, 'package.json')).resolve('.');
|
|
392
|
+
const resolvedRoot = realpathSync(linkPath);
|
|
393
|
+
if (resolvedRoot !== plugin.targetRoot || !isWithin(plugin.targetRoot, realpathSync(resolvedEntry))) {
|
|
394
|
+
return { ok: false, code: 'CREW_PLUGIN_NOT_LOADABLE' };
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
ok: true,
|
|
398
|
+
changed: scaffoldChanged || manifestChanged || link.changed,
|
|
399
|
+
profileRoot,
|
|
400
|
+
profileManifest: profile.profileManifest,
|
|
401
|
+
linkPath,
|
|
402
|
+
targetRoot: plugin.targetRoot,
|
|
403
|
+
packageName: plugin.packageName,
|
|
404
|
+
packageVersion: plugin.packageVersion,
|
|
405
|
+
patchPath: plugin.patchPath,
|
|
406
|
+
resolvedEntry,
|
|
407
|
+
};
|
|
408
|
+
} catch { return { ok: false, code: 'CREW_PLUGIN_NOT_LOADABLE' }; }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function ensureCrewPluginRegistration({ home = homedir(), root, name } = {}) {
|
|
412
|
+
return ensurePluginRegistration({ profileRoot: crewProfileDir({ home }), root, name });
|
|
413
|
+
}
|
|
403
414
|
|
|
404
415
|
/**
|
|
405
416
|
* Remove one Crew plugin registration without invoking a package manager.
|
|
@@ -407,41 +418,41 @@ export function ensureCrewPluginRegistration({ home = homedir(), root, name } =
|
|
|
407
418
|
* status/uninstall probe cannot touch the official web profile or download
|
|
408
419
|
* anything merely to remove a stale registration.
|
|
409
420
|
*/
|
|
410
|
-
export function removeCrewPluginRegistration({ home = homedir(), name, profileRoot = crewProfileDir({ home }) } = {}) {
|
|
411
|
-
if (!isSafePackageName(name)) {
|
|
412
|
-
return { ok: false, code: 'INVALID_CREW_PLUGIN_NAME' };
|
|
413
|
-
}
|
|
421
|
+
export function removeCrewPluginRegistration({ home = homedir(), name, profileRoot = crewProfileDir({ home }) } = {}) {
|
|
422
|
+
if (!isSafePackageName(name)) {
|
|
423
|
+
return { ok: false, code: 'INVALID_CREW_PLUGIN_NAME' };
|
|
424
|
+
}
|
|
414
425
|
const packageFile = join(profileRoot, 'package.json');
|
|
415
426
|
if (!existsSync(packageFile)) return { ok: true, removed: false };
|
|
416
|
-
let pkg;
|
|
417
|
-
try { pkg = JSON.parse(readFileSync(packageFile, 'utf8')); } catch { return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' }; }
|
|
418
|
-
if (!isObject(pkg)
|
|
419
|
-
|| (pkg.dependencies !== undefined && !isObject(pkg.dependencies))
|
|
420
|
-
|| (pkg.dsh !== undefined && !isObject(pkg.dsh))
|
|
421
|
-
|| (pkg.dsh?.profile !== undefined && !isObject(pkg.dsh.profile))
|
|
422
|
-
|| (pkg.dsh?.profile?.bundles !== undefined
|
|
423
|
-
&& (!Array.isArray(pkg.dsh.profile.bundles)
|
|
424
|
-
|| pkg.dsh.profile.bundles.some((item) => !isSafePackageName(item))))) {
|
|
425
|
-
return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
426
|
-
}
|
|
427
|
-
const hadDependency = Boolean(pkg.dependencies?.[name]);
|
|
428
|
-
const bundles = Array.isArray(pkg.dsh?.profile?.bundles) ? pkg.dsh.profile.bundles : [];
|
|
427
|
+
let pkg;
|
|
428
|
+
try { pkg = JSON.parse(readFileSync(packageFile, 'utf8')); } catch { return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' }; }
|
|
429
|
+
if (!isObject(pkg)
|
|
430
|
+
|| (pkg.dependencies !== undefined && !isObject(pkg.dependencies))
|
|
431
|
+
|| (pkg.dsh !== undefined && !isObject(pkg.dsh))
|
|
432
|
+
|| (pkg.dsh?.profile !== undefined && !isObject(pkg.dsh.profile))
|
|
433
|
+
|| (pkg.dsh?.profile?.bundles !== undefined
|
|
434
|
+
&& (!Array.isArray(pkg.dsh.profile.bundles)
|
|
435
|
+
|| pkg.dsh.profile.bundles.some((item) => !isSafePackageName(item))))) {
|
|
436
|
+
return { ok: false, code: 'CREW_PROFILE_METADATA_INVALID' };
|
|
437
|
+
}
|
|
438
|
+
const hadDependency = Boolean(pkg.dependencies?.[name]);
|
|
439
|
+
const bundles = Array.isArray(pkg.dsh?.profile?.bundles) ? pkg.dsh.profile.bundles : [];
|
|
429
440
|
const hadBundle = bundles.includes(name);
|
|
430
441
|
if (!hadDependency && !hadBundle) return { ok: true, removed: false };
|
|
431
|
-
const linkPath = profileLinkPath(profileRoot, name);
|
|
432
|
-
let linkStat;
|
|
433
|
-
try { linkStat = lstatSync(linkPath); } catch { linkStat = null; }
|
|
434
|
-
if (linkStat && !linkStat.isSymbolicLink()) return { ok: false, code: 'CREW_PLUGIN_LINK_CONFLICT' };
|
|
435
|
-
const next = { ...pkg };
|
|
442
|
+
const linkPath = profileLinkPath(profileRoot, name);
|
|
443
|
+
let linkStat;
|
|
444
|
+
try { linkStat = lstatSync(linkPath); } catch { linkStat = null; }
|
|
445
|
+
if (linkStat && !linkStat.isSymbolicLink()) return { ok: false, code: 'CREW_PLUGIN_LINK_CONFLICT' };
|
|
446
|
+
const next = { ...pkg };
|
|
436
447
|
if (hadDependency) {
|
|
437
448
|
next.dependencies = { ...pkg.dependencies };
|
|
438
449
|
delete next.dependencies[name];
|
|
439
450
|
if (Object.keys(next.dependencies).length === 0) delete next.dependencies;
|
|
440
451
|
}
|
|
441
|
-
if (hadBundle) {
|
|
442
|
-
next.dsh = { ...pkg.dsh, profile: { ...pkg.dsh.profile, bundles: bundles.filter((item) => item !== name) } };
|
|
443
|
-
}
|
|
444
|
-
writeFileSync(packageFile, JSON.stringify(next, null, 2) + '\n');
|
|
445
|
-
if (linkStat) unlinkSync(linkPath);
|
|
446
|
-
return { ok: true, removed: true };
|
|
447
|
-
}
|
|
452
|
+
if (hadBundle) {
|
|
453
|
+
next.dsh = { ...pkg.dsh, profile: { ...pkg.dsh.profile, bundles: bundles.filter((item) => item !== name) } };
|
|
454
|
+
}
|
|
455
|
+
writeFileSync(packageFile, JSON.stringify(next, null, 2) + '\n');
|
|
456
|
+
if (linkStat) unlinkSync(linkPath);
|
|
457
|
+
return { ok: true, removed: true };
|
|
458
|
+
}
|
|
@@ -40,6 +40,11 @@ import { homedir } from 'node:os';
|
|
|
40
40
|
import * as realInstaller from './install.mjs';
|
|
41
41
|
import { crewProfileDir } from './install.mjs';
|
|
42
42
|
import { ensureCrewDshRuntime, ensureCrewPluginRegistration, removeCrewPluginRegistration } from '../dsh-cli-runtime.mjs';
|
|
43
|
+
import {
|
|
44
|
+
ensureOfficialWebIntegration,
|
|
45
|
+
officialWebIntegrationStatus,
|
|
46
|
+
removeOfficialWebIntegration,
|
|
47
|
+
} from './official-web.mjs';
|
|
43
48
|
|
|
44
49
|
export const CREW_APP_DIRNAME = 'app';
|
|
45
50
|
export const RELEASES_DIRNAME = 'releases';
|
|
@@ -429,7 +434,11 @@ export function validateInstalledPayload(dir, { expectedName, expectedVersion, a
|
|
|
429
434
|
}
|
|
430
435
|
}
|
|
431
436
|
}
|
|
432
|
-
for (const rel of [
|
|
437
|
+
for (const rel of [
|
|
438
|
+
'cordis.patch.yml', 'src/server.mjs', 'src/hub/entry.mjs', 'lib/client.js', 'bin/dsh-crew.mjs',
|
|
439
|
+
'official-web-bridge/package.json', 'official-web-bridge/cordis.patch.yml',
|
|
440
|
+
'official-web-bridge/entry.mjs', 'official-web-bridge/lib/client.js',
|
|
441
|
+
]) {
|
|
433
442
|
if (!existsSync(join(dir, rel))) errors.push(`payload artifact missing: ${rel}`);
|
|
434
443
|
}
|
|
435
444
|
if (!allowIncomplete && existsSync(join(dir, INCOMPLETE_MARKER))) errors.push('release is still marked incomplete');
|
|
@@ -553,9 +562,52 @@ async function activateRelease({ home, releaseDir, manifest, log, installer }) {
|
|
|
553
562
|
return false;
|
|
554
563
|
}
|
|
555
564
|
log('✓ Claude Code integration');
|
|
565
|
+
|
|
566
|
+
const official = officialWebIntegrationStatus({ home });
|
|
567
|
+
if (official.enabled) {
|
|
568
|
+
const repaired = ensureOfficialWebIntegration({ home, releaseDir });
|
|
569
|
+
if (!repaired.ok) {
|
|
570
|
+
log(`✗ official 3080 bridge repair failed (${repaired.code ?? 'unknown'})`);
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
log('✓ official 3080 UI bridge → isolated Crew backend on 3210');
|
|
574
|
+
}
|
|
556
575
|
return true;
|
|
557
576
|
}
|
|
558
577
|
|
|
578
|
+
export async function npxIntegrate({ home = homedir(), log = console.log } = {}) {
|
|
579
|
+
const pointer = readCurrentPointer({ home });
|
|
580
|
+
if (!pointer || !existsSync(pointer.path)) {
|
|
581
|
+
log('✗ install DSH Crew before enabling the official 3080 integration');
|
|
582
|
+
return { ok: false, error: 'DSH Crew is not installed' };
|
|
583
|
+
}
|
|
584
|
+
const validated = validateInstalledPayload(pointer.path, { expectedName: pointer.name, expectedVersion: pointer.version });
|
|
585
|
+
if (!validated.ok) {
|
|
586
|
+
log('✗ installed DSH Crew payload is damaged; run dsh-crew update first');
|
|
587
|
+
return { ok: false, error: 'installed payload invalid' };
|
|
588
|
+
}
|
|
589
|
+
const result = ensureOfficialWebIntegration({ home, releaseDir: pointer.path });
|
|
590
|
+
if (!result.ok) {
|
|
591
|
+
log(`✗ official 3080 integration failed (${result.code ?? 'unknown'})`);
|
|
592
|
+
return { ok: false, error: result.code ?? 'integration failed' };
|
|
593
|
+
}
|
|
594
|
+
log(result.changed
|
|
595
|
+
? '✓ official 3080 UI connected to the isolated Crew backend on 3210'
|
|
596
|
+
: '- official 3080 UI integration already healthy');
|
|
597
|
+
log(` backup: ${result.backupFile}`);
|
|
598
|
+
return { ok: true, changed: result.changed, backupFile: result.backupFile };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export async function npxDetach({ home = homedir(), log = console.log } = {}) {
|
|
602
|
+
const result = removeOfficialWebIntegration({ home });
|
|
603
|
+
if (!result.ok) {
|
|
604
|
+
log(`✗ official 3080 integration removal failed (${result.code ?? 'unknown'})`);
|
|
605
|
+
return { ok: false, error: result.code ?? 'detach failed' };
|
|
606
|
+
}
|
|
607
|
+
log(result.removed ? '✓ official 3080 bridge removed; isolated 3210 mode remains available' : '- official 3080 bridge already absent');
|
|
608
|
+
return { ok: true, removed: result.removed };
|
|
609
|
+
}
|
|
610
|
+
|
|
559
611
|
export async function npxInstall({
|
|
560
612
|
home = homedir(),
|
|
561
613
|
log = console.log,
|
|
@@ -895,6 +947,8 @@ export function npxStatus({
|
|
|
895
947
|
const st = installer.installStatus ? installer.installStatus({ home }) : realInstaller.installStatus({ home });
|
|
896
948
|
const codex = st?.codex?.installed ? 'installed' : 'not installed';
|
|
897
949
|
const claude = st?.claude?.installed ? 'installed' : 'not installed';
|
|
950
|
+
const official = officialWebIntegrationStatus({ home, releaseDir: pointer?.path });
|
|
951
|
+
const officialWeb = !official.enabled ? 'disabled' : official.healthy ? 'installed' : 'needs repair';
|
|
898
952
|
|
|
899
953
|
log(`DSH Crew launcher/candidate: ${candidateVersion ?? 'unknown'}`);
|
|
900
954
|
log(`Installed DSH Crew payload: ${installedLine}`);
|
|
@@ -908,7 +962,8 @@ export function npxStatus({
|
|
|
908
962
|
log(` Refresh the launcher with: npm install -g ${UPDATE_PACKAGE_NAME}@${installedVersion}`);
|
|
909
963
|
}
|
|
910
964
|
}
|
|
911
|
-
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile
|
|
965
|
+
log(`DSH plugin: ${dshPlugin} (dedicated dsh-crew profile on 3210)`);
|
|
966
|
+
log(`Official 3080 UI bridge: ${officialWeb}`);
|
|
912
967
|
log(`Codex Desktop integration: ${codex}`);
|
|
913
968
|
log(`Claude Code integration: ${claude}`);
|
|
914
969
|
|
|
@@ -918,6 +973,7 @@ export function npxStatus({
|
|
|
918
973
|
installedVersion,
|
|
919
974
|
installedPath: pointer?.path ?? null,
|
|
920
975
|
dshPlugin,
|
|
976
|
+
officialWeb,
|
|
921
977
|
codex,
|
|
922
978
|
claude,
|
|
923
979
|
};
|
|
@@ -944,6 +1000,10 @@ export async function npxUninstall({
|
|
|
944
1000
|
if (cl.ok !== false) log('✓ Claude Code integration removed');
|
|
945
1001
|
else fail('Claude Code integration removal failed');
|
|
946
1002
|
|
|
1003
|
+
const official = removeOfficialWebIntegration({ home, preserveIntent: !purge, remember: !purge });
|
|
1004
|
+
if (!official.ok) fail(`official 3080 bridge removal failed (${official.code ?? 'unknown'})`);
|
|
1005
|
+
else log(official.removed ? '✓ official 3080 bridge removed' : '- official 3080 bridge already absent');
|
|
1006
|
+
|
|
947
1007
|
if (name) {
|
|
948
1008
|
const removed = removeCrewPluginRegistration({ home, name });
|
|
949
1009
|
if (!removed.ok) fail(`Harness registration removal failed (${removed.code ?? 'unknown'})`);
|
|
@@ -982,6 +1042,8 @@ export const USAGE = `usage: dsh-crew <command> [--purge] [--candidate <path>]
|
|
|
982
1042
|
|
|
983
1043
|
Commands:
|
|
984
1044
|
install persist the candidate package into Crew-owned state and register it
|
|
1045
|
+
integrate show Crew inside the official 3080 UI; backend stays isolated on 3210
|
|
1046
|
+
detach remove only the official 3080 bridge; isolated 3210 mode remains available
|
|
985
1047
|
status read-only report of launcher/installed versions and integrations
|
|
986
1048
|
update resolve the newest permitted package from the configured npm registry (or
|
|
987
1049
|
--candidate), stage and validate it, then activate; idempotent when current
|
|
@@ -1032,13 +1094,15 @@ export async function runNpxCli({
|
|
|
1032
1094
|
error(USAGE);
|
|
1033
1095
|
return 1;
|
|
1034
1096
|
}
|
|
1035
|
-
if (unknown.length > 0 || !['install', 'status', 'update', 'uninstall'].includes(command)) {
|
|
1097
|
+
if (unknown.length > 0 || !['install', 'integrate', 'detach', 'status', 'update', 'uninstall'].includes(command)) {
|
|
1036
1098
|
error(`unknown command: ${command ?? '<none>'}\n\n${USAGE}`);
|
|
1037
1099
|
return 1;
|
|
1038
1100
|
}
|
|
1039
1101
|
try {
|
|
1040
1102
|
const actions = {
|
|
1041
1103
|
install: commands.install ?? npxInstall,
|
|
1104
|
+
integrate: commands.integrate ?? npxIntegrate,
|
|
1105
|
+
detach: commands.detach ?? npxDetach,
|
|
1042
1106
|
status: commands.status ?? npxStatus,
|
|
1043
1107
|
update: commands.update ?? npxUpdate,
|
|
1044
1108
|
uninstall: commands.uninstall ?? npxUninstall,
|