@deepseek-ai/dsh-app-boot 0.1.1-rc.2 → 0.1.2-alpha.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/README.i18n.yaml +2 -2
- package/README.md +126 -33
- package/README.zh.md +131 -38
- package/lib/index.js +383 -59
- package/lib/types/index.d.ts +4 -4
- package/lib/types/profile.d.ts +44 -25
- package/package.json +23 -21
package/lib/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { parseEnv } from "node:util";
|
|
5
|
-
import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import * as yaml from "js-yaml";
|
|
7
7
|
import { Context, Service } from "@deepseek-ai/cordis";
|
|
8
8
|
import Loader, { EntryGroup, EntryTree, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
|
|
@@ -11,6 +11,8 @@ import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
|
11
11
|
import Group from "@deepseek-ai/cordis-plugin-group";
|
|
12
12
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
13
13
|
import { createLaunchEnvironmentSnapshot } from "@deepseek-ai/dsh-launch-environment";
|
|
14
|
+
import { withFileLock } from "@deepseek-ai/dsh-atomic-write";
|
|
15
|
+
import { resolve as resolve$1 } from "resolve.exports";
|
|
14
16
|
//#region ../../../vendor/include/src/index.ts
|
|
15
17
|
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
|
|
16
18
|
kind: "scalar",
|
|
@@ -298,17 +300,20 @@ var Include = class extends EntryTree {
|
|
|
298
300
|
*
|
|
299
301
|
* Module resolution is two-anchor by construction: a bundle name resolves
|
|
300
302
|
* first from the dsh installation (the launcher's own package), then from the
|
|
301
|
-
* profile directory.
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
303
|
+
* profile directory. Pnpm-managed entries in the profile's `node_modules`
|
|
304
|
+
* resolve first. Dsh-owned links add packages carried only by selected
|
|
305
|
+
* bundles, while `$DSH_HOME/profiles/node_modules` supplies the installation
|
|
306
|
+
* dependency closure through Node's ordinary parent-walk. Plain Node uses
|
|
307
|
+
* symlinks for that shared fallback; packaged executables use ESM proxies so
|
|
308
|
+
* external plugins retain the installation's module instances.
|
|
306
309
|
* @module @deepseek-ai/dsh-app-boot/profile
|
|
307
310
|
*/
|
|
308
311
|
/** Directory under the Harness home holding every profile. */
|
|
309
312
|
const PROFILES_DIR = "profiles";
|
|
310
313
|
/** The user patch layer inside a profile directory (hot-reloaded on long-lived surfaces). */
|
|
311
314
|
const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
|
|
315
|
+
/** Profile-private package links projected into its pnpm-managed node_modules. */
|
|
316
|
+
const PROFILE_MODULE_FALLBACK_DIR = ".dsh-module-fallback";
|
|
312
317
|
/**
|
|
313
318
|
* Resolve a profile's directory under the Harness home.
|
|
314
319
|
* @param name - the profile name (`dsh --profile <name>`).
|
|
@@ -321,8 +326,26 @@ function resolveProfileDir(name, home = resolveDshHome()) {
|
|
|
321
326
|
}
|
|
322
327
|
/** The shipped profile templates auto-initialized on first use, by name. */
|
|
323
328
|
const PROFILE_TEMPLATES = {
|
|
324
|
-
|
|
325
|
-
|
|
329
|
+
acp: {
|
|
330
|
+
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-acp-app"],
|
|
331
|
+
patchReload: "startup"
|
|
332
|
+
},
|
|
333
|
+
web: {
|
|
334
|
+
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"],
|
|
335
|
+
patchReload: "live"
|
|
336
|
+
},
|
|
337
|
+
headless: {
|
|
338
|
+
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"],
|
|
339
|
+
patchReload: "startup"
|
|
340
|
+
},
|
|
341
|
+
sdk: {
|
|
342
|
+
bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-sdk-app"],
|
|
343
|
+
patchReload: "startup"
|
|
344
|
+
},
|
|
345
|
+
"sdk-minimal": {
|
|
346
|
+
bundles: ["@deepseek-ai/dsh-sdk-minimal"],
|
|
347
|
+
patchReload: "startup"
|
|
348
|
+
}
|
|
326
349
|
};
|
|
327
350
|
/** Installation-owned bundle tuples normalized to the shipped template. */
|
|
328
351
|
const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
@@ -332,6 +355,8 @@ const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
|
332
355
|
] };
|
|
333
356
|
/** The bundle list a `dsh plugin` init uses for a name with no shipped template. */
|
|
334
357
|
const DEFAULT_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base"];
|
|
358
|
+
/** Custom profiles retain the historical live patch-file behavior. */
|
|
359
|
+
const DEFAULT_PROFILE_PATCH_RELOAD = "live";
|
|
335
360
|
const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
|
|
336
361
|
# a top-level YAML array of loader patch entries (id-targeted config
|
|
337
362
|
# overrides, disables, and insert lists; \`!!js\` expressions allowed).
|
|
@@ -349,8 +374,9 @@ autoInstallPeers: false
|
|
|
349
374
|
* so re-running is a no-op on an initialized profile.
|
|
350
375
|
* @param dir - the profile directory from {@link resolveProfileDir}.
|
|
351
376
|
* @param bundles - the initial `dsh.profile.bundles` layer list.
|
|
377
|
+
* @param patchReload - user patch-file lifecycle; custom profiles default to live reload.
|
|
352
378
|
*/
|
|
353
|
-
function initProfile(dir, bundles) {
|
|
379
|
+
function initProfile(dir, bundles, patchReload = DEFAULT_PROFILE_PATCH_RELOAD) {
|
|
354
380
|
mkdirSync(dir, { recursive: true });
|
|
355
381
|
const manifestPath = join(dir, "package.json");
|
|
356
382
|
if (!existsSync(manifestPath)) {
|
|
@@ -358,7 +384,10 @@ function initProfile(dir, bundles) {
|
|
|
358
384
|
name: `dsh-profile-${basename(dir)}`,
|
|
359
385
|
private: true,
|
|
360
386
|
dependencies: {},
|
|
361
|
-
dsh: { profile: {
|
|
387
|
+
dsh: { profile: {
|
|
388
|
+
bundles: [...bundles],
|
|
389
|
+
patchReload
|
|
390
|
+
} }
|
|
362
391
|
};
|
|
363
392
|
writeFileSync(manifestPath, JSON.stringify(manifest, void 0, 2) + "\n");
|
|
364
393
|
}
|
|
@@ -367,7 +396,14 @@ function initProfile(dir, bundles) {
|
|
|
367
396
|
const workspacePath = join(dir, "pnpm-workspace.yaml");
|
|
368
397
|
if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE);
|
|
369
398
|
}
|
|
370
|
-
|
|
399
|
+
function readModuleProxyRecord(link) {
|
|
400
|
+
try {
|
|
401
|
+
return JSON.parse(readFileSync(join(link, "package.json"), "utf8"));
|
|
402
|
+
} catch {
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** Ensure `link` is a symlink to `target`, replacing a wrong link or a dsh-managed packaged proxy. */
|
|
371
407
|
function ensureSymlink(link, target) {
|
|
372
408
|
let stat;
|
|
373
409
|
try {
|
|
@@ -376,40 +412,182 @@ function ensureSymlink(link, target) {
|
|
|
376
412
|
stat = void 0;
|
|
377
413
|
}
|
|
378
414
|
if (stat !== void 0) {
|
|
379
|
-
if (!stat.isSymbolicLink())
|
|
380
|
-
|
|
381
|
-
|
|
415
|
+
if (!stat.isSymbolicLink()) {
|
|
416
|
+
if ((stat.isDirectory() ? readModuleProxyRecord(link) : void 0)?.dsh?.moduleFallback?.targets === void 0) throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`);
|
|
417
|
+
rmSync(link, { recursive: true });
|
|
418
|
+
stat = void 0;
|
|
419
|
+
}
|
|
420
|
+
if (stat !== void 0) {
|
|
421
|
+
if (symlinkPointsTo(link, target)) return;
|
|
422
|
+
unlinkSync(link);
|
|
423
|
+
}
|
|
382
424
|
}
|
|
383
425
|
try {
|
|
384
426
|
symlinkSync(target, link, "junction");
|
|
385
427
|
} catch (error) {
|
|
386
428
|
/* v8 ignore next 4 */
|
|
387
|
-
if (error.code !== "EEXIST" || !lstatSync(link).isSymbolicLink() ||
|
|
429
|
+
if (error.code !== "EEXIST" || !lstatSync(link).isSymbolicLink() || !symlinkPointsTo(link, target)) throw error;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
/** Resolve a link target without following the final path component. */
|
|
433
|
+
function canonicalLinkPath(path) {
|
|
434
|
+
try {
|
|
435
|
+
return join(realpathSync.native(dirname(path)), basename(path));
|
|
436
|
+
} catch (error) {
|
|
437
|
+
/* v8 ignore next 2 -- a non-ENOENT realpath failure requires a host filesystem fault */
|
|
438
|
+
if (error.code === "ENOENT") return void 0;
|
|
439
|
+
/* v8 ignore next -- see the host-filesystem exception above */
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Return whether a symlink or junction points at the same path as `target`. */
|
|
444
|
+
function symlinkPointsTo(link, target) {
|
|
445
|
+
const canonicalActual = canonicalLinkPath(resolve(dirname(link), readlinkSync(link)));
|
|
446
|
+
const canonicalTarget = canonicalLinkPath(resolve(target));
|
|
447
|
+
return canonicalActual !== void 0 && canonicalActual === canonicalTarget;
|
|
448
|
+
}
|
|
449
|
+
/** Add one profile-owned fallback link without replacing a pnpm-managed entry. */
|
|
450
|
+
function ensureProfileSymlink(link, target) {
|
|
451
|
+
try {
|
|
452
|
+
lstatSync(link);
|
|
453
|
+
return;
|
|
454
|
+
} catch (error) {
|
|
455
|
+
/* v8 ignore next -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
456
|
+
if (error.code !== "ENOENT") throw error;
|
|
457
|
+
}
|
|
458
|
+
ensureSymlink(link, target);
|
|
459
|
+
}
|
|
460
|
+
/** Package names represented by owned symlinks below one fallback node_modules. */
|
|
461
|
+
function ownedPackageNames(modulesDir) {
|
|
462
|
+
return readdirSync(modulesDir, { withFileTypes: true }).flatMap((entry) => {
|
|
463
|
+
if (entry.name.startsWith("@") && entry.isDirectory()) return readdirSync(join(modulesDir, entry.name), { withFileTypes: true }).filter((child) => child.isSymbolicLink()).map((child) => `${entry.name}/${child.name}`);
|
|
464
|
+
return entry.isSymbolicLink() ? [entry.name] : [];
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
/** Remove an obsolete owned target and its profile projection when still connected. */
|
|
468
|
+
function removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName) {
|
|
469
|
+
const ownedLink = join(ownedModulesDir, packageName);
|
|
470
|
+
const profileLink = join(profileModulesDir, packageName);
|
|
471
|
+
try {
|
|
472
|
+
if (lstatSync(profileLink).isSymbolicLink() && symlinkPointsTo(profileLink, ownedLink)) unlinkSync(profileLink);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
/* v8 ignore next -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
475
|
+
if (error.code !== "ENOENT") throw error;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
unlinkSync(ownedLink);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
/* v8 ignore next -- concurrent identical cleanup may remove the link first */
|
|
481
|
+
if (error.code !== "ENOENT") throw error;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/** Return whether the process reads application modules from pkg's virtual filesystem. */
|
|
485
|
+
function isPackagedExecutable() {
|
|
486
|
+
return process.pkg !== void 0;
|
|
487
|
+
}
|
|
488
|
+
/** Resolve one available explicit package export under Node ESM import conditions. */
|
|
489
|
+
function packageEntryFromPackage(packageName, packageDir, declared, subpath) {
|
|
490
|
+
let candidates;
|
|
491
|
+
try {
|
|
492
|
+
candidates = resolve$1({
|
|
493
|
+
name: packageName,
|
|
494
|
+
exports: declared
|
|
495
|
+
}, subpath);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
if (error.message.startsWith("No known conditions for ")) return void 0;
|
|
498
|
+
const specifier = subpath === "." ? packageName : packageName + subpath.slice(1);
|
|
499
|
+
throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error });
|
|
500
|
+
}
|
|
501
|
+
for (const candidate of candidates ?? []) {
|
|
502
|
+
const target = candidate;
|
|
503
|
+
const entry = resolve(packageDir, target);
|
|
504
|
+
const relativeEntry = relative(packageDir, entry);
|
|
505
|
+
if (!target.startsWith("./") || /^\.\.(?:[\\/]|$)/u.test(relativeEntry)) throw new Error(`dsh: installed package ${packageName} export ${subpath} resolves outside its package: ${target}`);
|
|
506
|
+
if (existsSync(entry) && statSync(entry).isFile()) return pathToFileURL(entry).href;
|
|
388
507
|
}
|
|
389
508
|
}
|
|
509
|
+
/** Resolve every explicit ESM runtime export that an out-of-tree plugin can import. */
|
|
510
|
+
function packageProxySource(packageName, packageDir) {
|
|
511
|
+
const manifest = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8"));
|
|
512
|
+
if (typeof manifest.version !== "string" || manifest.version.length === 0) throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`);
|
|
513
|
+
const declared = manifest.exports;
|
|
514
|
+
if (declared === void 0) {
|
|
515
|
+
const main = typeof manifest.main === "string" && manifest.main.length > 0 ? manifest.main : void 0;
|
|
516
|
+
const entry = join(packageDir, main ?? "index");
|
|
517
|
+
try {
|
|
518
|
+
const resolved = createRequire(join(packageDir, "package.json")).resolve(entry);
|
|
519
|
+
return {
|
|
520
|
+
version: manifest.version,
|
|
521
|
+
targets: { ".": pathToFileURL(resolved).href }
|
|
522
|
+
};
|
|
523
|
+
} catch (error) {
|
|
524
|
+
if (main === void 0 && (manifest.bin !== void 0 || manifest.types !== void 0 || manifest.typings !== void 0)) return {
|
|
525
|
+
version: manifest.version,
|
|
526
|
+
targets: {}
|
|
527
|
+
};
|
|
528
|
+
throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`, { cause: error });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const subpaths = declared !== null && typeof declared === "object" && !Array.isArray(declared) && Object.keys(declared).some((key) => key.startsWith(".")) ? Object.keys(declared).filter((key) => key === "." || key.startsWith("./") && !key.includes("*") && !key.endsWith("/") && key !== "./package.json") : ["."];
|
|
532
|
+
const targets = {};
|
|
533
|
+
for (const subpath of subpaths) {
|
|
534
|
+
const target = packageEntryFromPackage(packageName, packageDir, declared, subpath);
|
|
535
|
+
if (target !== void 0) targets[subpath] = target;
|
|
536
|
+
}
|
|
537
|
+
return {
|
|
538
|
+
version: manifest.version,
|
|
539
|
+
targets
|
|
540
|
+
};
|
|
541
|
+
}
|
|
390
542
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* directory after the profile's own `node_modules`, so every in-box plugin
|
|
396
|
-
* resolves without pnpm ever managing it — the exact "bundles come from the
|
|
397
|
-
* installation" contract. The closure (not just direct dependencies) is
|
|
398
|
-
* required for out-of-tree plugins: their peer dependencies name Service
|
|
399
|
-
* Definition packages (`dsh-compaction`, `dsh-invariants`, ...) that the app
|
|
400
|
-
* reaches only through its Service Provider packages. Symlinked packages
|
|
401
|
-
* resolve their own dependencies from their real directories (Node's default
|
|
402
|
-
* symlink-following), so each package needs only its one flat link.
|
|
403
|
-
* Idempotent: correct links are kept and moved installations are
|
|
404
|
-
* re-pointed; a stale link to a vanished package stays until its name is
|
|
405
|
-
* reused (dangling links are invisible to resolution).
|
|
406
|
-
* @param installAnchor - absolute path of the dsh app's package.json.
|
|
407
|
-
* @param home - the Harness home; defaults to {@link resolveDshHome}.
|
|
543
|
+
* Materialize a real package proxy whose exports retain pkg's virtual module
|
|
544
|
+
* URL. Files outside the executable cannot traverse a symlink into
|
|
545
|
+
* `/snapshot`, while an ESM re-export can import that URL and preserves the
|
|
546
|
+
* executable's single module instance for out-of-tree plugin peers.
|
|
408
547
|
*/
|
|
409
|
-
function
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
548
|
+
function ensureModuleProxy(link, packageName, version, targets) {
|
|
549
|
+
const manifest = {
|
|
550
|
+
name: packageName,
|
|
551
|
+
version,
|
|
552
|
+
private: true,
|
|
553
|
+
type: "module",
|
|
554
|
+
exports: Object.fromEntries(Object.keys(targets).map((subpath, index) => [subpath, `./entry-${index}.js`])),
|
|
555
|
+
dsh: { moduleFallback: { targets } }
|
|
556
|
+
};
|
|
557
|
+
let stat;
|
|
558
|
+
try {
|
|
559
|
+
stat = lstatSync(link);
|
|
560
|
+
} catch {
|
|
561
|
+
stat = void 0;
|
|
562
|
+
}
|
|
563
|
+
if (stat?.isSymbolicLink()) {
|
|
564
|
+
unlinkSync(link);
|
|
565
|
+
stat = void 0;
|
|
566
|
+
}
|
|
567
|
+
if (stat !== void 0) {
|
|
568
|
+
const existing = readModuleProxyRecord(link);
|
|
569
|
+
if (existing?.dsh?.moduleFallback?.targets === void 0) throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`);
|
|
570
|
+
if (existing.version === version && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets) && Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return;
|
|
571
|
+
rmSync(link, { recursive: true });
|
|
572
|
+
}
|
|
573
|
+
mkdirSync(link, { recursive: true });
|
|
574
|
+
writeFileSync(join(link, "package.json"), JSON.stringify(manifest, void 0, 2) + "\n");
|
|
575
|
+
for (const [index, target] of Object.values(targets).entries()) {
|
|
576
|
+
const specifier = JSON.stringify(target);
|
|
577
|
+
writeFileSync(join(link, `entry-${index}.js`), `export * from ${specifier}\nimport * as target from ${specifier}\nexport default target.default\n`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** Read one package manifest used while traversing a module-fallback dependency graph. */
|
|
581
|
+
function readModuleFallbackManifest(anchor) {
|
|
582
|
+
return JSON.parse(readFileSync(anchor, "utf8"));
|
|
583
|
+
}
|
|
584
|
+
/** Return dependency names that may be imported by a loader-visible plugin. */
|
|
585
|
+
function profileDependencyNames(manifest) {
|
|
586
|
+
return [...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {})];
|
|
587
|
+
}
|
|
588
|
+
/** Resolve the installation generation that every profile must find through the fallback directory. */
|
|
589
|
+
function resolveModuleFallbackEntries(installAnchor) {
|
|
590
|
+
const appManifest = readModuleFallbackManifest(installAnchor);
|
|
413
591
|
const links = /* @__PURE__ */ new Map();
|
|
414
592
|
/* v8 ignore next -- a real app manifest always declares its name */
|
|
415
593
|
if (appManifest.name !== void 0) links.set(appManifest.name, dirname(installAnchor));
|
|
@@ -419,7 +597,7 @@ function healProfilesModuleFallback(installAnchor, home = resolveDshHome()) {
|
|
|
419
597
|
}];
|
|
420
598
|
for (let next = queue.shift(); next !== void 0; next = queue.shift())
|
|
421
599
|
/* v8 ignore next -- a real app manifest always declares dependencies */
|
|
422
|
-
for (const dep of
|
|
600
|
+
for (const dep of profileDependencyNames(next.manifest)) {
|
|
423
601
|
if (links.has(dep)) continue;
|
|
424
602
|
const dir = packageDirFromAnchor(next.anchor, dep);
|
|
425
603
|
if (dir === void 0) continue;
|
|
@@ -427,13 +605,136 @@ function healProfilesModuleFallback(installAnchor, home = resolveDshHome()) {
|
|
|
427
605
|
const manifestPath = join(dir, "package.json");
|
|
428
606
|
queue.push({
|
|
429
607
|
anchor: manifestPath,
|
|
430
|
-
manifest:
|
|
608
|
+
manifest: readModuleFallbackManifest(manifestPath)
|
|
431
609
|
});
|
|
432
610
|
}
|
|
433
|
-
|
|
434
|
-
|
|
611
|
+
return {
|
|
612
|
+
entries: !isPackagedExecutable() ? [...links].map(([packageName, packageDir]) => ({
|
|
613
|
+
kind: "symlink",
|
|
614
|
+
packageName,
|
|
615
|
+
packageDir
|
|
616
|
+
})) : [...links].flatMap(([packageName, packageDir]) => {
|
|
617
|
+
const source = packageProxySource(packageName, packageDir);
|
|
618
|
+
return Object.keys(source.targets).length === 0 ? [] : [{
|
|
619
|
+
kind: "proxy",
|
|
620
|
+
packageName,
|
|
621
|
+
version: source.version,
|
|
622
|
+
targets: source.targets
|
|
623
|
+
}];
|
|
624
|
+
}),
|
|
625
|
+
packageNames: new Set(links.keys())
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
/** Return whether one existing fallback entry already matches its resolved installation generation. */
|
|
629
|
+
function moduleFallbackEntryCurrent(modulesDir, entry) {
|
|
630
|
+
const link = join(modulesDir, entry.packageName);
|
|
631
|
+
try {
|
|
632
|
+
const stat = lstatSync(link);
|
|
633
|
+
if (entry.kind === "symlink") return stat.isSymbolicLink() && readlinkSync(link) === entry.packageDir;
|
|
634
|
+
if (!stat.isDirectory()) return false;
|
|
635
|
+
const existing = readModuleProxyRecord(link);
|
|
636
|
+
return existing?.version === entry.version && JSON.stringify(existing.dsh?.moduleFallback?.targets) === JSON.stringify(entry.targets) && Object.keys(entry.targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)));
|
|
637
|
+
} catch {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/** Return whether every required fallback entry is already ready for this installation. */
|
|
642
|
+
function moduleFallbackCurrent(modulesDir, entries) {
|
|
643
|
+
return entries.every((entry) => moduleFallbackEntryCurrent(modulesDir, entry));
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Maintain module fallbacks for one profile launch. The shared
|
|
647
|
+
* `$DSH_HOME/profiles/node_modules` mirrors the dsh installation dependency
|
|
648
|
+
* closure. Plain Node writes symlinks; a packaged executable writes ESM
|
|
649
|
+
* proxies under a cross-process lock because operating-system links cannot
|
|
650
|
+
* enter pkg's virtual filesystem. Missing packages carried only by selected
|
|
651
|
+
* bundles are linked through a profile-owned directory into that profile's
|
|
652
|
+
* `node_modules`; pnpm-managed entries remain authoritative, and another
|
|
653
|
+
* profile's links cannot change its resolution.
|
|
654
|
+
* @param options - installation anchor, optional loaded profile, and Harness home.
|
|
655
|
+
* @returns settlement after the shared fallback and profile-local links are current.
|
|
656
|
+
*/
|
|
657
|
+
async function healProfilesModuleFallback(options) {
|
|
658
|
+
const { installAnchor, profile, home = resolveDshHome() } = options;
|
|
659
|
+
const modulesDir = join(join(home, PROFILES_DIR), "node_modules");
|
|
660
|
+
mkdirSync(modulesDir, { recursive: true });
|
|
661
|
+
const { entries, packageNames } = resolveModuleFallbackEntries(installAnchor);
|
|
662
|
+
if (!moduleFallbackCurrent(modulesDir, entries)) await withFileLock(modulesDir, () => {
|
|
663
|
+
if (!moduleFallbackCurrent(modulesDir, entries)) healProfilesModuleFallbackLocked(entries, modulesDir);
|
|
664
|
+
return Promise.resolve();
|
|
665
|
+
});
|
|
666
|
+
if (profile !== void 0) healProfileModuleFallback(profile, packageNames);
|
|
667
|
+
}
|
|
668
|
+
/** Heal one module-fallback generation while the cross-process writer lock is held. */
|
|
669
|
+
function healProfilesModuleFallbackLocked(entries, modulesDir) {
|
|
670
|
+
for (const entry of entries) {
|
|
671
|
+
const link = join(modulesDir, entry.packageName);
|
|
435
672
|
mkdirSync(dirname(link), { recursive: true });
|
|
436
|
-
|
|
673
|
+
if (entry.kind === "proxy") ensureModuleProxy(link, entry.packageName, entry.version, entry.targets);
|
|
674
|
+
else ensureSymlink(link, entry.packageDir);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/** Collect the first resolvable package directory for each dependency name. */
|
|
678
|
+
function dependencyClosure(anchors, reserved, exclude) {
|
|
679
|
+
const links = /* @__PURE__ */ new Map();
|
|
680
|
+
const visited = new Set(reserved);
|
|
681
|
+
for (const anchor of anchors) {
|
|
682
|
+
const canonicalAnchor = realpathSync.native(anchor);
|
|
683
|
+
const manifest = readModuleFallbackManifest(canonicalAnchor);
|
|
684
|
+
/* v8 ignore next -- an installable package manifest always declares its name */
|
|
685
|
+
if (manifest.name === void 0) continue;
|
|
686
|
+
if (!visited.has(manifest.name)) {
|
|
687
|
+
visited.add(manifest.name);
|
|
688
|
+
links.set(manifest.name, dirname(canonicalAnchor));
|
|
689
|
+
}
|
|
690
|
+
const queue = [{
|
|
691
|
+
anchor: canonicalAnchor,
|
|
692
|
+
manifest
|
|
693
|
+
}];
|
|
694
|
+
for (let next = queue.shift(); next !== void 0; next = queue.shift())
|
|
695
|
+
/* v8 ignore next -- an installable package manifest always declares dependencies or peers */
|
|
696
|
+
for (const dep of profileDependencyNames(next.manifest)) {
|
|
697
|
+
if (visited.has(dep)) continue;
|
|
698
|
+
const dir = packageDirFromAnchor(next.anchor, dep, exclude);
|
|
699
|
+
if (dir === void 0) continue;
|
|
700
|
+
visited.add(dep);
|
|
701
|
+
links.set(dep, dir);
|
|
702
|
+
const manifestPath = join(dir, "package.json");
|
|
703
|
+
queue.push({
|
|
704
|
+
anchor: manifestPath,
|
|
705
|
+
manifest: readModuleFallbackManifest(manifestPath)
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return links;
|
|
710
|
+
}
|
|
711
|
+
/** Reconcile packages carried only by selected bundles into one profile. */
|
|
712
|
+
function healProfileModuleFallback(profile, installationPackageNames) {
|
|
713
|
+
const profileModulesDir = join(profile.dir, "node_modules");
|
|
714
|
+
const ownedModulesDir = join(profile.dir, PROFILE_MODULE_FALLBACK_DIR, "node_modules");
|
|
715
|
+
mkdirSync(profileModulesDir, { recursive: true });
|
|
716
|
+
mkdirSync(ownedModulesDir, { recursive: true });
|
|
717
|
+
const bundleLinks = dependencyClosure(profile.layers.filter((layer) => !installationPackageNames.has(layer.packageName)).map((layer) => join(layer.packageDir, "package.json")), installationPackageNames, (candidate, packageName) => {
|
|
718
|
+
const profileLink = join(profileModulesDir, packageName);
|
|
719
|
+
if (canonicalLinkPath(candidate) !== canonicalLinkPath(profileLink)) return false;
|
|
720
|
+
try {
|
|
721
|
+
return lstatSync(profileLink).isSymbolicLink() && symlinkPointsTo(profileLink, join(ownedModulesDir, packageName));
|
|
722
|
+
} catch (error) {
|
|
723
|
+
/* v8 ignore next 2 -- a non-ENOENT lstat failure requires a host filesystem fault */
|
|
724
|
+
if (error.code === "ENOENT") return true;
|
|
725
|
+
/* v8 ignore next -- see the host-filesystem exception above */
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
for (const layer of profile.layers) bundleLinks.delete(layer.packageName);
|
|
730
|
+
for (const packageName of ownedPackageNames(ownedModulesDir)) if (!bundleLinks.has(packageName)) removeProfileSymlink(profileModulesDir, ownedModulesDir, packageName);
|
|
731
|
+
for (const [packageName, target] of bundleLinks) {
|
|
732
|
+
const ownedLink = join(ownedModulesDir, packageName);
|
|
733
|
+
mkdirSync(dirname(ownedLink), { recursive: true });
|
|
734
|
+
ensureSymlink(ownedLink, target);
|
|
735
|
+
const profileLink = join(profileModulesDir, packageName);
|
|
736
|
+
mkdirSync(dirname(profileLink), { recursive: true });
|
|
737
|
+
ensureProfileSymlink(profileLink, ownedLink);
|
|
437
738
|
}
|
|
438
739
|
}
|
|
439
740
|
/**
|
|
@@ -467,21 +768,28 @@ function sameBundles(left, right) {
|
|
|
467
768
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
468
769
|
}
|
|
469
770
|
/**
|
|
470
|
-
* Normalize an exact installation-owned bundle tuple to its shipped template
|
|
471
|
-
*
|
|
771
|
+
* Normalize an exact installation-owned bundle tuple to its shipped template,
|
|
772
|
+
* or add the shipped reload default to an exact current tuple. A changed value
|
|
773
|
+
* is written back during profile loading while every other manifest field is
|
|
774
|
+
* preserved; any other bundle list is user-owned and remains untouched.
|
|
472
775
|
*/
|
|
473
776
|
function normalizeShippedProfile(name, dir, manifest) {
|
|
474
777
|
const installationOwned = INSTALLATION_OWNED_PROFILE_TUPLES[name];
|
|
475
|
-
const
|
|
778
|
+
const template = PROFILE_TEMPLATES[name];
|
|
476
779
|
const bundles = manifest.dsh?.profile?.bundles;
|
|
477
|
-
if (
|
|
780
|
+
if (template === void 0 || bundles === void 0) return manifest;
|
|
781
|
+
const isRetiredTuple = installationOwned !== void 0 && sameBundles(bundles, installationOwned);
|
|
782
|
+
const isCurrentTuple = sameBundles(bundles, template.bundles);
|
|
783
|
+
const needsReloadDefault = manifest.dsh?.profile?.patchReload === void 0 && isCurrentTuple;
|
|
784
|
+
if (!isRetiredTuple && !needsReloadDefault) return manifest;
|
|
478
785
|
const normalized = {
|
|
479
786
|
...manifest,
|
|
480
787
|
dsh: {
|
|
481
788
|
...manifest.dsh,
|
|
482
789
|
profile: {
|
|
483
790
|
...manifest.dsh?.profile,
|
|
484
|
-
bundles: [...
|
|
791
|
+
bundles: [...template.bundles],
|
|
792
|
+
patchReload: manifest.dsh?.profile?.patchReload ?? template.patchReload
|
|
485
793
|
}
|
|
486
794
|
}
|
|
487
795
|
};
|
|
@@ -496,11 +804,11 @@ function normalizeShippedProfile(name, dir, manifest) {
|
|
|
496
804
|
* matches what the Loader would import from the same anchor, and
|
|
497
805
|
* `existsSync` follows the symlinks pnpm's isolated layout uses.
|
|
498
806
|
*/
|
|
499
|
-
function packageDirFromAnchor(anchor, packageName) {
|
|
807
|
+
function packageDirFromAnchor(anchor, packageName, exclude = () => false) {
|
|
500
808
|
/* v8 ignore next */
|
|
501
809
|
for (const searchPath of createRequire(anchor).resolve.paths(packageName) ?? []) {
|
|
502
810
|
const candidate = join(searchPath, packageName);
|
|
503
|
-
if (existsSync(join(candidate, "package.json"))) return candidate;
|
|
811
|
+
if (existsSync(join(candidate, "package.json")) && !exclude(candidate, packageName)) return candidate;
|
|
504
812
|
}
|
|
505
813
|
}
|
|
506
814
|
/**
|
|
@@ -541,9 +849,14 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
541
849
|
if (!existsSync(join(dir, "package.json"))) {
|
|
542
850
|
const template = PROFILE_TEMPLATES[name];
|
|
543
851
|
if (template === void 0) throw new Error(`${binName}: profile ${JSON.stringify(name)} does not exist; create it with 'dsh plugin --profile ${name} add <package>'`);
|
|
544
|
-
initProfile(dir, template);
|
|
852
|
+
initProfile(dir, template.bundles, template.patchReload);
|
|
545
853
|
}
|
|
546
|
-
const
|
|
854
|
+
const manifest = normalizeShippedProfile(name, dir, readProfileManifest(binName, dir));
|
|
855
|
+
const bundles = manifest.dsh?.profile?.bundles ?? [];
|
|
856
|
+
const rawPatchReload = manifest.dsh?.profile?.patchReload;
|
|
857
|
+
if (rawPatchReload !== void 0 && rawPatchReload !== "live" && rawPatchReload !== "startup") throw new Error(`${binName}: profile manifest ${join(dir, "package.json")} dsh.profile.patchReload must be "live" or "startup"`);
|
|
858
|
+
const patchReload = rawPatchReload ?? "live";
|
|
859
|
+
const layers = bundles.map((packageName) => {
|
|
547
860
|
const packageDir = resolveBundleDir(binName, packageName, installAnchor, dir);
|
|
548
861
|
const declared = JSON.parse(readFileSync(join(packageDir, "package.json"), "utf8")).dsh?.bundle?.patch;
|
|
549
862
|
if (declared === void 0) throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.bundle in its package.json`);
|
|
@@ -561,7 +874,8 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
561
874
|
dir,
|
|
562
875
|
layers,
|
|
563
876
|
patchPath,
|
|
564
|
-
patches: options.userLayer !== false && existsSync(patchPath) ? loadOverlayPatches(binName, patchPath) : []
|
|
877
|
+
patches: options.userLayer !== false && existsSync(patchPath) ? loadOverlayPatches(binName, patchPath) : [],
|
|
878
|
+
patchReload
|
|
565
879
|
};
|
|
566
880
|
}
|
|
567
881
|
/**
|
|
@@ -581,7 +895,7 @@ function composeEntries(layers, warn = () => {}) {
|
|
|
581
895
|
//#endregion
|
|
582
896
|
//#region lib/types/index.js
|
|
583
897
|
/**
|
|
584
|
-
* Shared boot glue for the
|
|
898
|
+
* Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored
|
|
585
899
|
* `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the
|
|
586
900
|
* optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to
|
|
587
901
|
* config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles.
|
|
@@ -818,6 +1132,16 @@ function loadOverlayPatches(binName, file) {
|
|
|
818
1132
|
}
|
|
819
1133
|
return parsePatchList(binName, file, content, "overlay");
|
|
820
1134
|
}
|
|
1135
|
+
/** Resolve relative plugin paths in one patch file's `insert` rows without changing assertion names. */
|
|
1136
|
+
function anchorInsertedPluginNames(patches, file) {
|
|
1137
|
+
const base = dirname(resolve(file));
|
|
1138
|
+
const visit = (entry) => {
|
|
1139
|
+
if (typeof entry.name === "string" && (entry.name.startsWith("./") || entry.name.startsWith("../"))) entry.name = pathToFileURL(resolve(base, entry.name)).href;
|
|
1140
|
+
if (entry.group && Array.isArray(entry.config)) entry.config.forEach(visit);
|
|
1141
|
+
};
|
|
1142
|
+
for (const patch of patches) patch.insert?.forEach(visit);
|
|
1143
|
+
return patches;
|
|
1144
|
+
}
|
|
821
1145
|
/**
|
|
822
1146
|
* Parse one loader patch list: a top-level YAML array of
|
|
823
1147
|
* `@deepseek-ai/cordis-plugin-include` `PatchOptions` (id-targeted config overrides and
|
|
@@ -842,7 +1166,7 @@ function parsePatchList(binName, file, content, label) {
|
|
|
842
1166
|
parsed.forEach((entry, index) => {
|
|
843
1167
|
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new Error(`${binName}: ${label} entry ${index + 1} in ${file} must be a mapping (a loader patch entry)`);
|
|
844
1168
|
});
|
|
845
|
-
return parsed;
|
|
1169
|
+
return anchorInsertedPluginNames(parsed, file);
|
|
846
1170
|
}
|
|
847
1171
|
/**
|
|
848
1172
|
* Compose the effective entry list exactly as `boot()` would mount it: parse
|
|
@@ -1183,7 +1507,7 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
1183
1507
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
1184
1508
|
let deepest = cause;
|
|
1185
1509
|
while (deepest instanceof Error && deepest.cause !== void 0) deepest = deepest.cause;
|
|
1186
|
-
const stack = deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : "";
|
|
1510
|
+
const stack = deepest instanceof AggregateError ? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map(formatActivationError).join("\n")}` : deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : "";
|
|
1187
1511
|
throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause });
|
|
1188
1512
|
}
|
|
1189
1513
|
}
|
|
@@ -1194,8 +1518,8 @@ const HARNESS_SOURCE_SECTION = "harness:source";
|
|
|
1194
1518
|
* explicitly distinguishing it from the task workspace and current working
|
|
1195
1519
|
* directory. The self-referential `dsh-tool-cordis` toolset reads and edits this
|
|
1196
1520
|
* checkout. Call once on the settled boot context ({@link boot}); the section
|
|
1197
|
-
*
|
|
1198
|
-
* persona
|
|
1521
|
+
* uses the shared first-party placement just after the harness identity opener
|
|
1522
|
+
* and before the deployment persona. A booted tree with no `systemPrompt` service has no prompt to
|
|
1199
1523
|
* augment, so this is then a no-op that returns `undefined`. The section is
|
|
1200
1524
|
* registered against the `systemPrompt` service's fiber, so a dev HMR reload of
|
|
1201
1525
|
* that plugin drops it until the next boot.
|
|
@@ -1208,9 +1532,9 @@ function addHarnessSourceSection(ctx, sourceRoot) {
|
|
|
1208
1532
|
if (systemPrompt === void 0) return void 0;
|
|
1209
1533
|
return systemPrompt.section({
|
|
1210
1534
|
name: HARNESS_SOURCE_SECTION,
|
|
1211
|
-
order:
|
|
1535
|
+
order: systemPrompt.getSectionOrder("HARNESS_SOURCE"),
|
|
1212
1536
|
text: `The DeepSeek Harness implementation checkout is at ${sourceRoot}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`
|
|
1213
1537
|
});
|
|
1214
1538
|
}
|
|
1215
1539
|
//#endregion
|
|
1216
|
-
export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, PROFILES_DIR, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, addHarnessSourceSection, assertEntriesActivated, assertEntriesLoaded, boot, composeEntries, healProfilesModuleFallback, initProfile, installFailLoud, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, mountRootInclude, readProfileManifest, renderConfigDump, resolveBundleDir, resolveConfigPath, resolveProfileDir, watchUserPatches, writeProfileManifest };
|
|
1540
|
+
export { DEFAULT_PROFILE_BUNDLES, DEFAULT_PROFILE_PATCH_RELOAD, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, PROFILES_DIR, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, addHarnessSourceSection, assertEntriesActivated, assertEntriesLoaded, boot, composeEntries, healProfilesModuleFallback, initProfile, installFailLoud, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, mountRootInclude, readProfileManifest, renderConfigDump, resolveBundleDir, resolveConfigPath, resolveProfileDir, watchUserPatches, writeProfileManifest };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared boot glue for the
|
|
2
|
+
* Shared boot glue for `dsh` profiles, including the CLI packaged by the Python runtime wheel: load the gitignored
|
|
3
3
|
* `.env`, install the fail-loud Loader guards, resolve the config path (snapshot-aware), load the
|
|
4
4
|
* optional user patch layers from the Harness home (`~/.dsh`), expose its path resolver to
|
|
5
5
|
* config expressions, and drive the Cordis Loader against a leaf `cordis.yml` until the tree settles.
|
|
@@ -16,7 +16,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
16
16
|
dshHomePath?: typeof dshHomePath;
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
|
-
export { composeEntries, DEFAULT_PROFILE_BUNDLES, healProfilesModuleFallback, initProfile, loadProfile, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PROFILES_DIR, readProfileManifest, resolveBundleDir, resolveProfileDir, writeProfileManifest, type DshBundleManifest, type DshManifestSection, type DshProfileManifest, type Profile, type ProfileLayer, type ProfileManifest, } from './profile.ts';
|
|
19
|
+
export { composeEntries, DEFAULT_PROFILE_BUNDLES, DEFAULT_PROFILE_PATCH_RELOAD, healProfilesModuleFallback, initProfile, loadProfile, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PROFILES_DIR, readProfileManifest, resolveBundleDir, resolveProfileDir, writeProfileManifest, type DshBundleManifest, type DshManifestSection, type DshProfileManifest, type Profile, type ProfileLayer, type ProfileManifest, type ProfileModuleFallbackOptions, type ProfilePatchReload, type ProfileTemplate, } from './profile.ts';
|
|
20
20
|
/**
|
|
21
21
|
* Resolve the config to boot. Replay swaps a `cordis.yml` basename for
|
|
22
22
|
* `cordis.snapshot.yml` in the same directory; every other mode keeps the path.
|
|
@@ -254,8 +254,8 @@ export declare const HARNESS_SOURCE_SECTION = "harness:source";
|
|
|
254
254
|
* explicitly distinguishing it from the task workspace and current working
|
|
255
255
|
* directory. The self-referential `dsh-tool-cordis` toolset reads and edits this
|
|
256
256
|
* checkout. Call once on the settled boot context ({@link boot}); the section
|
|
257
|
-
*
|
|
258
|
-
* persona
|
|
257
|
+
* uses the shared first-party placement just after the harness identity opener
|
|
258
|
+
* and before the deployment persona. A booted tree with no `systemPrompt` service has no prompt to
|
|
259
259
|
* augment, so this is then a no-op that returns `undefined`. The section is
|
|
260
260
|
* registered against the `systemPrompt` service's fiber, so a dev HMR reload of
|
|
261
261
|
* that plugin drops it until the next boot.
|