@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/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. The Loader's `baseUrl` is the profile directory, whose
302
- * `node_modules` pnpm manages for out-of-tree plugins, while the maintained
303
- * flat fallback directory `$DSH_HOME/profiles/node_modules` (one symlink per
304
- * package the installation's app and bundles depend on) makes every in-box
305
- * plugin Node-resolvable from any profile through the ordinary parent-walk.
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
- web: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"],
325
- headless: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"]
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: { bundles: [...bundles] } }
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
- /** Ensure `link` is a symlink to `target`, replacing a wrong or dangling link; a real directory throws. */
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()) throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`);
380
- if (readlinkSync(link) === target) return;
381
- unlinkSync(link);
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() || readlinkSync(link) !== target) throw error;
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
- * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
392
- * symlink per package in the dsh app's resolvable dependency CLOSURE (BFS
393
- * over `dependencies` from the app manifest), each resolved from its own
394
- * real location. Node's parent-directory walk from any profile finds this
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 healProfilesModuleFallback(installAnchor, home = resolveDshHome()) {
410
- const modulesDir = join(join(home, PROFILES_DIR), "node_modules");
411
- mkdirSync(modulesDir, { recursive: true });
412
- const appManifest = JSON.parse(readFileSync(installAnchor, "utf8"));
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 [...Object.keys(next.manifest.dependencies ?? {}), ...Object.keys(next.manifest.peerDependencies ?? {})]) {
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: JSON.parse(readFileSync(manifestPath, "utf8"))
608
+ manifest: readModuleFallbackManifest(manifestPath)
431
609
  });
432
610
  }
433
- for (const [packageName, target] of links) {
434
- const link = join(modulesDir, packageName);
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
- ensureSymlink(link, target);
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
- * while preserving every other manifest field. Any other list is user-owned.
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 current = PROFILE_TEMPLATES[name];
778
+ const template = PROFILE_TEMPLATES[name];
476
779
  const bundles = manifest.dsh?.profile?.bundles;
477
- if (installationOwned === void 0 || current === void 0 || bundles === void 0 || !sameBundles(bundles, installationOwned)) return manifest;
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: [...current]
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 layers = (normalizeShippedProfile(name, dir, readProfileManifest(binName, dir)).dsh?.profile?.bundles ?? []).map((packageName) => {
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 app bins (`dsh`, `dsh-acp-demo`): load the gitignored
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
- * orders just after the harness identity opener (`-100`) and before the deployment
1198
- * persona (`0`). A booted tree with no `systemPrompt` service has no prompt to
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: -99,
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 };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Shared boot glue for the app bins (`dsh`, `dsh-acp-demo`): load the gitignored
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
- * orders just after the harness identity opener (`-100`) and before the deployment
258
- * persona (`0`). A booted tree with no `systemPrompt` service has no prompt to
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.