@deepseek-ai/dsh-app-boot 0.2.1-alpha.2 → 0.2.1-alpha.4

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
@@ -249,7 +249,7 @@ var Include = class extends EntryTree {
249
249
  };
250
250
  //#endregion
251
251
  //#region lib/types/plugin-compatibility.js
252
- /** Evaluate plugin dsh peer requirements without importing plugin code. */
252
+ /** Evaluate plugin dsh requirements (`engines.dsh`, else dsh peers) without importing plugin code. */
253
253
  function objectOf$1(value, field) {
254
254
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${field} must be an object`);
255
255
  return value;
@@ -260,7 +260,7 @@ function runtimeVersionOf(value) {
260
260
  }
261
261
  function identityField(manifest, field) {
262
262
  const value = Object.hasOwn(manifest, field) ? manifest[field] : void 0;
263
- if (typeof value !== "string" || value.trim() === "") throw new Error(`Plugin manifest ${field} must be a non-empty string when dsh peers are incompatible`);
263
+ if (typeof value !== "string" || value.trim() === "") throw new Error(`Plugin manifest ${field} must be a non-empty string when dsh requirements are unmet`);
264
264
  return value;
265
265
  }
266
266
  /**
@@ -273,33 +273,46 @@ function getDshRuntimeVersion() {
273
273
  const manifest = objectOf$1(JSON.parse(fs.readFileSync(filename, "utf8")), "app-boot package.json");
274
274
  return runtimeVersionOf(Object.hasOwn(manifest, "version") ? manifest.version : void 0);
275
275
  }
276
- /**
277
- * Check every @deepseek-ai/dsh or @deepseek-ai/dsh-* peer against the runtime.
278
- * Prereleases participate in ranges. workspace:^, workspace:~, and workspace:*
279
- * refer to the current runtime; other invalid ranges are incompatible.
280
- * @param manifest - parsed plugin package.json; inherited fields are ignored.
281
- * @param exemptions - exact plugin name@version keys mapped to exact runtime versions.
282
- * @param runtimeVersion - running dsh version, defaulting to this app-boot package.
283
- * @returns incompatible peers and exemption status, or undefined when none are incompatible.
284
- * @throws for malformed manifest peer fields, invalid runtime versions, or missing identity on a mismatch.
285
- */
286
- function evaluatePluginCompatibility(manifest, exemptions = {}, runtimeVersion = getDshRuntimeVersion()) {
287
- runtimeVersionOf(runtimeVersion);
288
- const fields = objectOf$1(manifest, "Plugin manifest");
289
- if (!Object.hasOwn(fields, "peerDependencies")) return void 0;
276
+ function satisfies(runtimeVersion, range) {
277
+ return range.trim() !== "" && semver.satisfies(runtimeVersion, range, { includePrerelease: true });
278
+ }
279
+ function unmetRequirements(fields, runtimeVersion) {
280
+ const engines = Object.hasOwn(fields, "engines") ? objectOf$1(fields.engines, "Plugin manifest engines") : {};
281
+ if (Object.hasOwn(engines, "dsh")) {
282
+ const range = engines.dsh;
283
+ if (typeof range !== "string") throw new Error("Plugin manifest engines.dsh must be a string");
284
+ return satisfies(runtimeVersion, range) ? {} : { "engines.dsh": range };
285
+ }
286
+ if (!Object.hasOwn(fields, "peerDependencies")) return {};
290
287
  const dependencies = objectOf$1(fields.peerDependencies, "Plugin manifest peerDependencies");
291
- const peers = {};
288
+ const unmet = {};
292
289
  for (const [name, range] of Object.entries(dependencies)) {
293
290
  if (typeof range !== "string") throw new Error(`Plugin manifest peerDependencies[${JSON.stringify(name)}] must be a string`);
294
291
  if (name !== "@deepseek-ai/dsh" && !name.startsWith("@deepseek-ai/dsh-")) continue;
295
- const requirement = [
292
+ if (!satisfies(runtimeVersion, [
296
293
  "workspace:^",
297
294
  "workspace:~",
298
295
  "workspace:*"
299
- ].includes(range) ? runtimeVersion : range;
300
- if (requirement.trim() === "" || !semver.satisfies(runtimeVersion, requirement, { includePrerelease: true })) peers[name] = range;
296
+ ].includes(range) ? runtimeVersion : range)) unmet[name] = range;
301
297
  }
302
- if (Object.keys(peers).length === 0) return void 0;
298
+ return unmet;
299
+ }
300
+ /**
301
+ * Check the plugin's dsh requirements against the runtime. An own `engines.dsh` range decides alone and
302
+ * peerDependencies are ignored; otherwise every @deepseek-ai/dsh or @deepseek-ai/dsh-* peer must match.
303
+ * Prereleases participate in ranges. For peers only, workspace:^, workspace:~, and workspace:* refer to
304
+ * the current runtime; other empty or invalid ranges, including any workspace: `engines.dsh`, are unmet.
305
+ * @param manifest - parsed plugin package.json; inherited fields are ignored.
306
+ * @param exemptions - exact plugin name@version keys mapped to exact runtime versions.
307
+ * @param runtimeVersion - running dsh version, defaulting to this app-boot package.
308
+ * @returns unmet requirements and exemption status, or undefined when every requirement is met.
309
+ * @throws for malformed manifest engines or peer fields, invalid runtime versions, or missing identity on a mismatch.
310
+ */
311
+ function evaluatePluginCompatibility(manifest, exemptions = {}, runtimeVersion = getDshRuntimeVersion()) {
312
+ runtimeVersionOf(runtimeVersion);
313
+ const fields = objectOf$1(manifest, "Plugin manifest");
314
+ const requirements = unmetRequirements(fields, runtimeVersion);
315
+ if (Object.keys(requirements).length === 0) return void 0;
303
316
  const name = identityField(fields, "name");
304
317
  const version = identityField(fields, "version");
305
318
  const key = `${name}@${version}`;
@@ -307,19 +320,19 @@ function evaluatePluginCompatibility(manifest, exemptions = {}, runtimeVersion =
307
320
  name,
308
321
  version,
309
322
  runtimeVersion,
310
- peers,
323
+ requirements,
311
324
  exempted: (Object.hasOwn(exemptions, key) ? exemptions[key] : void 0)?.includes(runtimeVersion) === true
312
325
  };
313
326
  }
314
327
  /**
315
- * Describe incompatible peers, their risk, and the exact-version remedy.
328
+ * Describe unmet requirements, their risk, and the exact-version remedy.
316
329
  * Surfaces with their own grant mechanism or locale render the structured result themselves.
317
330
  * @param issue - incompatible plugin/runtime result, including exempted mismatches.
318
331
  * @returns an English diagnostic for logs and stderr.
319
332
  */
320
333
  function pluginCompatibilityWarning(issue) {
321
334
  const key = `${issue.name}@${issue.version}`;
322
- return `Plugin ${key} is incompatible with dsh ${issue.runtimeVersion}: peerDependencies ${JSON.stringify(issue.peers)}. Running it may cause crashes or data loss. Update the plugin or install a plugin version compatible with this dsh runtime. To accept this risk explicitly, grant the exact-version exemption for ${key} on dsh ${issue.runtimeVersion} with \`dsh plugin allow-version\` or the plugin manager, then retry the installation or restart dsh. Exact-version exemption: ${issue.exempted ? "active" : "not active"}.`;
335
+ return `Plugin ${key} is incompatible with dsh ${issue.runtimeVersion}: requirements ${JSON.stringify(issue.requirements)}. Running it may cause crashes or data loss. Update the plugin or install a plugin version compatible with this dsh runtime. To accept this risk explicitly, grant the exact-version exemption for ${key} on dsh ${issue.runtimeVersion} with \`dsh plugin allow-version\` or the plugin manager, then retry the installation or restart dsh. Exact-version exemption: ${issue.exempted ? "active" : "not active"}.`;
323
336
  }
324
337
  //#endregion
325
338
  //#region lib/types/profile-compatibility.js
@@ -574,6 +587,7 @@ const OPTIONAL_BUNDLES = [
574
587
  "@deepseek-ai/dsh-experimental-cot-translation-bundle",
575
588
  "@deepseek-ai/dsh-experimental-auto-review",
576
589
  "@deepseek-ai/dsh-experimental-inspector-profile",
590
+ "@deepseek-ai/dsh-experimental-plugin-market-bundle",
577
591
  "@deepseek-ai/dsh-experimental-tool-worktree"
578
592
  ];
579
593
  const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
@@ -990,7 +1004,7 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
990
1004
  * the shared Harness home. This is used by application-owned profiles whose
991
1005
  * package project and lifecycle belong to that application.
992
1006
  * Retired bundles are removed from the stored bundle list first, rewriting the
993
- * manifest when it listed one. Unreadable bundles, and bundles whose own dsh peers the profile does not exempt, are skipped
1007
+ * manifest when it listed one. Unreadable bundles, and bundles whose own dsh requirements the profile does not exempt, are skipped
994
1008
  * without changing the manifest and listed in `skippedBundles`; nothing is printed.
995
1009
  * @param binName - the diagnostic prefix on thrown errors.
996
1010
  * @param dir - absolute profile package directory.
@@ -1095,6 +1109,15 @@ function resolveTelemetryPatch(disabledEnv, hasRow) {
1095
1109
  disabled: true
1096
1110
  };
1097
1111
  }
1112
+ /** Read the selected bundles' patches, which apply before the profile patch file.
1113
+ * @param binName Diagnostic prefix for malformed or missing configuration.
1114
+ * @param context Data supplied by the profile launcher.
1115
+ * @param initialProfile Already loaded startup profile; omitted reads the current files.
1116
+ * @returns Bundle patches in application order.
1117
+ */
1118
+ function readBundlePatches(binName, context, initialProfile) {
1119
+ return (initialProfile ?? loadProfileDirectory(binName, context.dir, context.installAnchor, { userLayer: false })).layers.flatMap((layer) => layer.patches);
1120
+ }
1098
1121
  /** Read current bundle and user layers with the launch-time overlays.
1099
1122
  * @param binName Diagnostic prefix for malformed or missing configuration.
1100
1123
  * @param context Data supplied by the profile launcher.
@@ -1102,9 +1125,8 @@ function resolveTelemetryPatch(disabledEnv, hasRow) {
1102
1125
  * @returns Detached ordered patches; this function does not update the Loader.
1103
1126
  */
1104
1127
  function readProfilePatches(binName, context, initialProfile) {
1105
- const profile = initialProfile ?? loadProfileDirectory(binName, context.dir, context.installAnchor, { userLayer: false });
1106
1128
  const patches = structuredClone([
1107
- ...profile.layers.flatMap((layer) => layer.patches),
1129
+ ...readBundlePatches(binName, context, initialProfile),
1108
1130
  ...initialProfile?.patches ?? loadOptionalPatches(binName, context.patchPath) ?? [],
1109
1131
  ...loadOptionalPatches(binName, join(context.home, "cordis.patch.yml")) ?? [],
1110
1132
  ...context.overlays
@@ -1238,6 +1260,14 @@ function sanitizeProfile(binName, profileDir, bundles) {
1238
1260
  /** In-memory profile package routing for Node's default ESM and CommonJS loaders. */
1239
1261
  const WORKER_RESOLUTION_KEY = "@deepseek-ai/dsh-app-boot/profile-resolution";
1240
1262
  const EMPTY_ATTRIBUTES = Object.freeze({});
1263
+ /** A successor resolution the running process cannot adopt, such as a retained mapping in another directory; it waits for a restart. */
1264
+ var ResolutionRestartRequired = class extends Error {
1265
+ /** @param message Diagnostic naming the rejected changes. */
1266
+ constructor(message) {
1267
+ super(message);
1268
+ this.name = "ResolutionRestartRequired";
1269
+ }
1270
+ };
1241
1271
  /**
1242
1272
  * Split a bare request into its package name without allocating path segments.
1243
1273
  * @param request - module specifier to classify.
@@ -1251,6 +1281,15 @@ function barePackageName(request) {
1251
1281
  const second = request.indexOf("/", first + 1);
1252
1282
  return second < 0 ? request : request.slice(0, second);
1253
1283
  }
1284
+ /** The JavaScript and native real paths of an existing directory; none for a directory that no longer exists. */
1285
+ function realSpellings(path) {
1286
+ try {
1287
+ const native = Reflect.get(realpathSync, "native");
1288
+ return [realpathSync(path), ...typeof native === "function" ? [String(Reflect.apply(native, realpathSync, [path]))] : []];
1289
+ } catch (_error) {
1290
+ return [];
1291
+ }
1292
+ }
1254
1293
  function canonicalPath(path) {
1255
1294
  try {
1256
1295
  return realpathSync(path);
@@ -1265,7 +1304,10 @@ function prefixes(path) {
1265
1304
  }
1266
1305
  function compileResolution(resolution) {
1267
1306
  return {
1268
- entries: new Map(resolution.entries.map((entry) => [entry.name, entry])),
1307
+ entries: new Map(resolution.entries.map((entry) => [entry.name, {
1308
+ ...entry,
1309
+ realDir: canonicalPath(entry.packageDir)
1310
+ }])),
1269
1311
  profilesDir: resolution.profilesDir,
1270
1312
  profileDir: resolution.profileDir,
1271
1313
  profilePaths: prefixes(resolution.profilesDir),
@@ -1428,11 +1470,65 @@ function sameResolution(left, right) {
1428
1470
  if (left === right) return true;
1429
1471
  return canonicalPath(left) === canonicalPath(right);
1430
1472
  }
1473
+ /** A directory as given and in its JavaScript and native real paths, each with a trailing separator. */
1474
+ function spellings(dir) {
1475
+ const result = new Set([dir + sep]);
1476
+ for (const real of realSpellings(dir)) result.add(real + sep);
1477
+ return [...result];
1478
+ }
1479
+ /**
1480
+ * The previous real directories of packages that moved, each mapped to the current one. Node keeps the real path of
1481
+ * every link it resolved through and never refreshes it, so a lookup through a link that now points elsewhere still
1482
+ * selects a file below the target Node saw first; {@link MovedDirectories.relocate} selects the same file below the
1483
+ * current target instead.
1484
+ */
1485
+ var MovedDirectories = class {
1486
+ /** Previous directory to current directory, both with a trailing separator. */
1487
+ targets = /* @__PURE__ */ new Map();
1488
+ /**
1489
+ * Record that a package moved from one directory to another. Every spelling of the previous directory leads to the
1490
+ * current one. A directory that an earlier move led to the previous one now leads to the current one as well, since
1491
+ * Node still selects files below the earliest target.
1492
+ * @param from Previous real directory.
1493
+ * @param to Current real directory.
1494
+ */
1495
+ record(from, to) {
1496
+ const previous = spellings(from);
1497
+ const current = to + sep;
1498
+ for (const [key, value] of this.targets) if (previous.includes(value)) this.targets.set(key, current);
1499
+ for (const key of previous) this.targets.set(key, current);
1500
+ }
1501
+ /**
1502
+ * Forget the previous directories that are current again, in any spelling, and records that lead to themselves.
1503
+ * @param current The real directories the published resolution maps.
1504
+ */
1505
+ forget(current) {
1506
+ const active = /* @__PURE__ */ new Set();
1507
+ for (const dir of current) for (const spelling of spellings(dir)) active.add(spelling);
1508
+ for (const [key, value] of [...this.targets]) if (active.has(key) || key === value) this.targets.delete(key);
1509
+ }
1510
+ /**
1511
+ * The same file below the current directory when it exists, for a file below a previous directory that an importer
1512
+ * outside that directory selected. An importer inside the previous directory keeps its own package's files. The
1513
+ * innermost previous directory decides.
1514
+ * @param filename Selected file.
1515
+ * @param parent Importer path.
1516
+ * @returns The file to load.
1517
+ */
1518
+ relocate(filename, parent) {
1519
+ let from = "";
1520
+ for (const dir of this.targets.keys()) if (filename.startsWith(dir) && dir.length > from.length) from = dir;
1521
+ if (from === "" || parent?.startsWith(from) === true) return filename;
1522
+ const relocated = this.targets.get(from) + filename.slice(from.length);
1523
+ return existsSync(relocated) ? relocated : filename;
1524
+ }
1525
+ };
1431
1526
  /** One mutable pointer to the immutable runtime resolution. */
1432
1527
  var ResolutionRouter = class {
1433
1528
  nodeModulePaths;
1434
1529
  current;
1435
1530
  linkedTargets;
1531
+ moved = new MovedDirectories();
1436
1532
  constructor(resolution, nodeModulePaths) {
1437
1533
  this.nodeModulePaths = nodeModulePaths;
1438
1534
  this.current = compileResolution(resolution);
@@ -1441,21 +1537,49 @@ var ResolutionRouter = class {
1441
1537
  replace(successor) {
1442
1538
  const entries = new Map(successor.entries.map((entry) => [entry.name, entry]));
1443
1539
  if (successor.profilesDir !== this.current.profilesDir || successor.profileDir !== this.current.profileDir) throw new Error("profile resolution: a successor cannot change its profile scope");
1540
+ const rejected = [];
1541
+ const moves = [];
1444
1542
  for (const [name, current] of this.current.entries) {
1445
1543
  const next = entries.get(name);
1446
1544
  if (next === void 0 && current.scope === "profile") continue;
1447
- if (next === void 0 || !sameResolution(current.packageDir, next.packageDir) || current.scope === "installation" && !sameResolution(current.declarer, next.declarer) || current.version !== next.version || current.scope !== next.scope) throw new Error(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`);
1545
+ if (next !== void 0 && current.scope === "profile" && next.scope === "profile") {
1546
+ if (current.realDir === canonicalPath(next.packageDir)) continue;
1547
+ rejected.push(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`);
1548
+ continue;
1549
+ }
1550
+ if (next === void 0 || !sameResolution(current.packageDir, next.packageDir) || current.scope === "installation" && !sameResolution(current.declarer, next.declarer) || current.scope === "installation" && current.version !== next.version || current.scope !== next.scope) rejected.push(`profile resolution: replacing ${JSON.stringify(name)} requires a process restart`);
1448
1551
  }
1449
1552
  const localPackageNames = new Set(successor.localPackageNames);
1450
- for (const name of localPackageNames) if (!this.current.localPackageNames.has(name) && this.current.entries.has(name)) throw new Error(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`);
1451
- for (const root of successor.linkedRoots) {
1452
- const previous = this.linkedTargets.get(root.name);
1453
- if (previous !== void 0 && !sameResolution(previous, root.realPath)) throw new Error(`profile resolution: relinking ${JSON.stringify(root.name)} requires a process restart`);
1553
+ for (const name of localPackageNames) if (!this.current.localPackageNames.has(name) && this.current.entries.has(name)) rejected.push(`profile resolution: overriding ${JSON.stringify(name)} locally requires a process restart`);
1554
+ const targets = new Map(this.linkedTargets);
1555
+ for (const [name, previous] of this.linkedTargets) {
1556
+ const root = successor.linkedRoots.find((candidate) => candidate.name === name);
1557
+ const entry = entries.get(name);
1558
+ const target = root?.realPath ?? (entry?.scope === "profile" ? canonicalPath(entry.packageDir) : void 0);
1559
+ if (target === void 0 || sameResolution(previous, target)) continue;
1560
+ moves.push([previous, target]);
1561
+ targets.set(name, target);
1454
1562
  }
1563
+ if (rejected.length > 0) throw new ResolutionRestartRequired(rejected.join("; "));
1455
1564
  const next = compileResolution(successor);
1456
- for (const { name, realPath } of successor.linkedRoots) if (!this.linkedTargets.has(name)) this.linkedTargets.set(name, realPath);
1565
+ for (const { name, realPath } of successor.linkedRoots) targets.set(name, realPath);
1566
+ this.linkedTargets = targets;
1567
+ for (const [from, to] of moves) this.moved.record(from, to);
1568
+ const current = [];
1569
+ for (const entry of next.entries.values()) current.push(entry.realDir);
1570
+ for (const root of successor.linkedRoots) current.push(root.realPath);
1571
+ this.moved.forget(current);
1457
1572
  this.current = next;
1458
1573
  }
1574
+ /**
1575
+ * The file to load instead of one Node selected through a link that has moved since; see {@link MovedDirectories}.
1576
+ * @param filename Selected file.
1577
+ * @param parent Importer path.
1578
+ * @returns The file to load.
1579
+ */
1580
+ relocate(filename, parent) {
1581
+ return this.moved.relocate(filename, parent);
1582
+ }
1459
1583
  /** Whether a module path has an interception layer: it lies in the profiles tree, the active profile, or a linked root. */
1460
1584
  hasInterceptionLayerForPath(path) {
1461
1585
  return findInterceptionLayer(path, this.current) !== void 0;
@@ -1774,7 +1898,23 @@ function installRuntimeInterception(resolution) {
1774
1898
  delegatedEsm = previous;
1775
1899
  }
1776
1900
  };
1777
- return adapted;
1901
+ const relocate = (result, parent) => {
1902
+ if (!result.url.startsWith("file:")) return result;
1903
+ const url = new URL(result.url);
1904
+ const filename = fileURLToPath(url);
1905
+ const relocated = router.relocate(filename, parent?.startsWith("file:") === true ? fileURLToPath(parent) : void 0);
1906
+ if (relocated === filename) return result;
1907
+ url.pathname = pathToFileURL(relocated).pathname;
1908
+ return {
1909
+ ...result,
1910
+ url: url.href
1911
+ };
1912
+ };
1913
+ return (request, parent, attributes) => {
1914
+ const result = adapted(request, parent, attributes);
1915
+ /* v8 ignore next -- Node 24+ resolves synchronously; the Node 22 matrix covers its Promise result */
1916
+ return result instanceof Promise ? result.then((resolved) => relocate(resolved, parent)) : relocate(result, parent);
1917
+ };
1778
1918
  };
1779
1919
  let restoreEsm;
1780
1920
  /* v8 ignore else -- CI coverage runs Node 24 v2; the Node 22 matrix exercises the v1 adapter */
@@ -1898,7 +2038,11 @@ function installRuntimeInterception(resolution) {
1898
2038
  delegatedCjs--;
1899
2039
  }
1900
2040
  };
1901
- cjs._resolveFilename = wrappedFilename;
2041
+ const relocatedFilename = (request, parent, main, options) => {
2042
+ const filename = wrappedFilename(request, parent, main, options);
2043
+ return isAbsolute(filename) ? router.relocate(filename, parent?.filename ?? void 0) : filename;
2044
+ };
2045
+ cjs._resolveFilename = relocatedFilename;
1902
2046
  return {
1903
2047
  packageDir(specifier, parentURL) {
1904
2048
  return router.packageDir(specifier, parentURL);
@@ -1908,7 +2052,7 @@ function installRuntimeInterception(resolution) {
1908
2052
  },
1909
2053
  dispose() {
1910
2054
  /* v8 ignore else -- interceptions are disposed in reverse installation order */
1911
- if (cjs._resolveFilename === wrappedFilename) cjs._resolveFilename = originalFilename;
2055
+ if (cjs._resolveFilename === relocatedFilename) cjs._resolveFilename = originalFilename;
1912
2056
  restoreEsm();
1913
2057
  }
1914
2058
  };
@@ -2183,7 +2327,7 @@ function preflight(ctx, entries, parentURL, report) {
2183
2327
  const issue = evaluatePluginCompatibility(manifest, exemptions);
2184
2328
  return issue === void 0 || issue.exempted ? void 0 : pluginCompatibilityWarning(issue);
2185
2329
  } catch (error) {
2186
- return `its declared peer dependencies cannot be validated: ${error instanceof Error ? error.message : String(error)}`;
2330
+ return `its declared dsh requirements cannot be validated: ${error instanceof Error ? error.message : String(error)}`;
2187
2331
  }
2188
2332
  };
2189
2333
  const deny = (row, reason) => {
@@ -3362,10 +3506,14 @@ var PluginPackages = class extends Service {
3362
3506
  interception.dispose();
3363
3507
  }, "profile package resolution");
3364
3508
  }
3509
+ /** The published runtime resolution, or `undefined` when Node resolves packages alone. */
3510
+ get resolution() {
3511
+ return this.current;
3512
+ }
3365
3513
  /**
3366
3514
  * Publish a complete successor generation for this process and subsequently created Workers.
3367
3515
  * Profile mappings and local package names may be removed after their plugins stop; linked roots may also be removed.
3368
- * Retained profile mappings may change their declarer, but not their normalized directory, version, or scope;
3516
+ * Retained profile mappings may change their declarer and version, but not their normalized directory or scope;
3369
3517
  * installation mappings must remain unchanged. Publication does not unload modules or clear Node caches.
3370
3518
  * @param successor - fully constructed generation accepted by {@link RuntimeInterception.replace}.
3371
3519
  */
@@ -3613,7 +3761,30 @@ function loadLayeredEnv(binName, cwd = process.cwd(), warn = (line) => void proc
3613
3761
  }
3614
3762
  const bootstrapIncludes = /* @__PURE__ */ new WeakMap();
3615
3763
  const userPatchesSchema = entryListSchema;
3616
- /** Apply one complete patch generation and wait for Loader activation diagnostics.
3764
+ /**
3765
+ * Recompose the profile from its complete patch list, together with its runtime resolution. The rows the generation no
3766
+ * longer lists are removed first and their disposal finishes while their packages are still mapped; the latest
3767
+ * generation of an installed, recomputable resolution is published; the compatibility preflight reads the published
3768
+ * packages; the generation is applied to the root Include; entries that ran, and enabled entries whose import failed,
3769
+ * start afterwards when the update left them without a running fiber; and activation is judged. A failure before the generation applies,
3770
+ * including a publication the resolution rejects, which reports `package-moved`, puts the removed rows back at their
3771
+ * places, starting them again, and starts the entries that ran again. An entry waiting for a service is reported as a
3772
+ * warning unless it is required. Patches are copied with `structuredClone`, so they hold no functions.
3773
+ * @param ctx Booted root context.
3774
+ * @param patches Complete ordered patch list.
3775
+ * @param binName Diagnostic prefix.
3776
+ * @param options What the caller already did and requires.
3777
+ * @returns Diagnostics for unchanged pre-existing inactive entries, and the restart reason of a rejected publication.
3778
+ * @throws When the root Include is missing; the latest resolution cannot be read or published; preparation or the
3779
+ * Loader update rejects; or for a new or changed failure, an inactive required entry, a failed entry that ran, or a
3780
+ * healthy fiber whose disposal rejected.
3781
+ */
3782
+ function recomposeProfile(ctx, patches, binName, options = {}) {
3783
+ return recompose(ctx, patches, binName, options, true);
3784
+ }
3785
+ /**
3786
+ * Apply a complete patch list to the root Include and judge activation, leaving the runtime resolution and entries
3787
+ * whose import failed as they are.
3617
3788
  * @param ctx Booted root context.
3618
3789
  * @param patches Complete ordered patch list.
3619
3790
  * @param binName Diagnostic prefix.
@@ -3621,33 +3792,141 @@ const userPatchesSchema = entryListSchema;
3621
3792
  * @returns Diagnostics for unchanged pre-existing inactive entries; new or changed failures reject.
3622
3793
  */
3623
3794
  async function reconcileProfilePatches(ctx, patches, binName, requiredIds = []) {
3624
- const entry = bootstrapIncludes.get(ctx);
3625
- if (entry === void 0) throw new Error(`${binName}: profile reload requires the root Include entry`);
3626
- const previousFailures = (await inactiveEntries(ctx)).map((failure) => ({
3627
- ...failure,
3628
- diagnostic: inactiveDiagnostic(failure),
3629
- fiber: failure.entry.fiber,
3630
- options: JSON.stringify(failure.entry.options)
3631
- }));
3632
- const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [{
3633
- fiber: row.fiber,
3634
- failed: row.fiber.state === FIBER_FAILED || row.fiber.state === FIBER_DISPOSED
3635
- }]);
3636
- const { patches: _previous, ...includeConfig } = entry.options.config;
3637
- const parentURL = new URL(".", new URL(includeConfig.path, entry.parent.tree.ctx.baseUrl)).href;
3638
- const prepared = prepareProfilePatches(ctx, patches, parentURL, binName);
3639
- await entry.update({ config: {
3640
- ...includeConfig,
3641
- patches: prepared
3642
- } });
3643
- const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await()));
3795
+ return (await recompose(ctx, patches, binName, { requiredIds }, false)).warnings;
3796
+ }
3797
+ async function recompose(ctx, patches, binName, options, withResolution) {
3798
+ const { requiredIds = [], running = [] } = options;
3799
+ let applying = false;
3800
+ let stopped = [];
3801
+ try {
3802
+ const entry = bootstrapIncludes.get(ctx);
3803
+ if (entry === void 0) throw new Error(`${binName}: profile reload requires the root Include entry`);
3804
+ const previousFailures = (await inactiveEntries(ctx, false)).map((failure) => ({
3805
+ entry: failure.entry,
3806
+ fiber: failure.entry.fiber,
3807
+ options: JSON.stringify(failure.entry.options),
3808
+ diagnostic: failure.entry.fiber?.uid ? reloadDiagnostic(failure) : void 0
3809
+ }));
3810
+ const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [{
3811
+ fiber: row.fiber,
3812
+ failed: row.fiber.state === FIBER_FAILED || row.fiber.state === FIBER_DISPOSED
3813
+ }]);
3814
+ const { patches: previous = [], ...includeConfig } = entry.options.config;
3815
+ const current = withResolution ? ctx.get("pluginPackages")?.resolution : void 0;
3816
+ const successor = current instanceof ProfileRuntimeResolution ? await current.computeLatestResolution() : void 0;
3817
+ if (successor !== void 0) {
3818
+ const listed = rowIds(structuredClone(patches));
3819
+ const composed = rowIds(structuredClone(previous));
3820
+ const tree = entry.subgroup?.tree;
3821
+ const dropped = [...ctx.loader.entries()].filter((row) => row.parent.tree === tree && composed.has(row.options.id) && !listed.has(row.options.id));
3822
+ const ids = new Set(dropped.map((row) => row.options.id));
3823
+ const topmost = dropped.filter((row) => !ids.has(row.parent.ctx.fiber.entry?.options.id));
3824
+ stopped = topmost.map((row) => ({
3825
+ group: row.parent,
3826
+ options: structuredClone(row.options),
3827
+ index: row.parent.data.indexOf(row.options)
3828
+ }));
3829
+ if (topmost.length > 0) {
3830
+ const fibers = dropped.flatMap((row) => row.fiber === void 0 ? [] : [row.fiber]);
3831
+ for (const row of topmost) {
3832
+ row.options.disabled = true;
3833
+ row.parent.remove(row.options.id);
3834
+ }
3835
+ await Promise.allSettled(fibers.map((fiber) => fiber.await()));
3836
+ await ctx.loader.await();
3837
+ }
3838
+ try {
3839
+ publish(ctx, successor);
3840
+ } catch (error) {
3841
+ if (!(error instanceof ResolutionRestartRequired)) throw error;
3842
+ return {
3843
+ warnings: [],
3844
+ restartReason: "package-moved"
3845
+ };
3846
+ }
3847
+ }
3848
+ const parentURL = new URL(".", new URL(includeConfig.path, entry.parent.tree.ctx.baseUrl)).href;
3849
+ const prepared = prepareProfilePatches(ctx, patches, parentURL, binName);
3850
+ applying = true;
3851
+ const retried = [...new Set([...running, ...withResolution ? [...ctx.loader.entries()].filter(importFailed) : []])];
3852
+ try {
3853
+ await entry.update({ config: {
3854
+ ...includeConfig,
3855
+ patches: prepared
3856
+ } });
3857
+ } finally {
3858
+ await resume(ctx, retried);
3859
+ }
3860
+ const results = await Promise.allSettled(previousFibers.map(({ fiber }) => fiber.await()));
3861
+ await ctx.loader.await();
3862
+ const failures = await inactiveEntries(ctx);
3863
+ const introduced = failures.filter((failure) => {
3864
+ if (requiredIds.includes(failure.entry.options.id)) return true;
3865
+ if (failure.outcome.kind === "pending") return false;
3866
+ if (running.includes(failure.entry)) return true;
3867
+ return !previousFailures.some((previous) => sameFailure(previous, failure));
3868
+ });
3869
+ if (introduced.length > 0) throw new Error(activationDiagnostic(binName, introduced, false).trimEnd());
3870
+ for (const [index, result] of results.entries()) if (result.status === "rejected" && !previousFibers[index]?.failed) throw result.reason;
3871
+ ctx.emit("app-boot/config-reload");
3872
+ return { warnings: failures.map(reloadDiagnostic) };
3873
+ } finally {
3874
+ if (!applying && (stopped.length > 0 || running.length > 0)) try {
3875
+ for (const { group, options, index } of stopped.toSorted((left, right) => left.index - right.index)) {
3876
+ group.data.splice(index, 0, options);
3877
+ await group.create(options);
3878
+ }
3879
+ await resume(ctx, running);
3880
+ const ran = new Set(running.map((row) => row.options.id));
3881
+ for (const failure of await inactiveEntries(ctx)) if (ran.has(failure.entry.options.id)) ctx.logger.warn(`${binName}: ${reloadDiagnostic(failure)}`);
3882
+ } catch (error) {
3883
+ ctx.logger.warn(error);
3884
+ }
3885
+ }
3886
+ }
3887
+ /** Whether an inactive entry is the failure recorded before: the same entry, fiber and options, and a matching diagnostic. */
3888
+ function sameFailure(previous, failure) {
3889
+ return previous.entry === failure.entry && previous.fiber === failure.entry.fiber && previous.options === JSON.stringify(failure.entry.options) && (previous.diagnostic === void 0 || previous.diagnostic === reloadDiagnostic(failure));
3890
+ }
3891
+ /** The ids of the rows a patch list inserts, including the rows of groups. */
3892
+ function rowIds(patches) {
3893
+ const ids = /* @__PURE__ */ new Set();
3894
+ const visit = (rows) => {
3895
+ for (const row of rows) {
3896
+ ids.add(row.id);
3897
+ if ((row.group === true || row.name === "cordis:group") && Array.isArray(row.config)) visit(row.config);
3898
+ }
3899
+ };
3900
+ visit(applyEntryPatches([], patches, () => {}));
3901
+ return ids;
3902
+ }
3903
+ /** Publish a generation unless it maps exactly what is published, which would only drop the package caches. */
3904
+ function publish(ctx, next) {
3905
+ const table = ({ localPackageNames, entries, linkedRoots }) => JSON.stringify([
3906
+ localPackageNames,
3907
+ entries,
3908
+ linkedRoots
3909
+ ]);
3910
+ const packages = ctx.pluginPackages;
3911
+ if (table(packages.resolution) !== table(next)) packages.replace(next);
3912
+ }
3913
+ /** Whether an enabled entry below a running parent has no fiber or keeps the disposed fiber of a failed import. */
3914
+ function importFailed(entry) {
3915
+ return enabledEntry(entry) && entry.parent.ctx.fiber.uid !== null && (entry.fiber === void 0 || entry.fiber.state === FIBER_DISPOSED);
3916
+ }
3917
+ /** Whether an entry is enabled now; a throwing `disabled` expression counts as disabled. */
3918
+ function enabledEntry(entry) {
3919
+ try {
3920
+ return !entry.disabled;
3921
+ } catch (_error) {
3922
+ return false;
3923
+ }
3924
+ }
3925
+ /** Start invalidated entries the Loader still holds without a running fiber, then wait until it settles. */
3926
+ async function resume(ctx, invalidated) {
3927
+ const entries = new Set(ctx.loader.entries());
3928
+ for (const row of invalidated) if (entries.has(row) && !row.fiber?.uid) await row.update({}, false, true);
3644
3929
  await ctx.loader.await();
3645
- const failures = await inactiveEntries(ctx);
3646
- const introduced = failures.filter((failure) => requiredIds.includes(failure.entry.options.id) || !previousFailures.some((previous) => previous.entry === failure.entry && previous.fiber === failure.entry.fiber && previous.options === JSON.stringify(failure.entry.options) && previous.diagnostic === inactiveDiagnostic(failure)));
3647
- if (introduced.length > 0) throw new Error(activationDiagnostic(binName, introduced).trimEnd());
3648
- for (const [index, result] of results.entries()) if (result.status === "rejected" && !previousFibers[index]?.failed) throw result.reason;
3649
- ctx.emit("app-boot/config-reload");
3650
- return failures.map(inactiveDiagnostic);
3651
3930
  }
3652
3931
  /**
3653
3932
  * Load an optional patch-list file: a top-level YAML array of loader patch
@@ -3999,7 +4278,7 @@ const requiredStartupEntryIds = new Set([
3999
4278
  "sdk-jsonrpc-server"
4000
4279
  ]);
4001
4280
  /** Render plugin stacks, nested causes, and aggregate member failures once per error. */
4002
- function formatActivationError(error) {
4281
+ function formatActivationError(error, stacks = true) {
4003
4282
  const details = [];
4004
4283
  const seen = /* @__PURE__ */ new Set();
4005
4284
  function visit(value) {
@@ -4009,7 +4288,7 @@ function formatActivationError(error) {
4009
4288
  }
4010
4289
  if (seen.has(value)) return;
4011
4290
  seen.add(value);
4012
- details.push(value.stack ?? value.message);
4291
+ details.push(stacks ? value.stack ?? value.message : String(value));
4013
4292
  if (value.cause !== void 0) visit(value.cause);
4014
4293
  if (value instanceof AggregateError) value.errors.forEach(visit);
4015
4294
  }
@@ -4035,12 +4314,23 @@ var StartupError = class extends Error {
4035
4314
  });
4036
4315
  }
4037
4316
  };
4317
+ /** The error importing an entry's module raises now, which Node returns from its cache for a module that failed before. */
4318
+ async function importFailure(entry) {
4319
+ try {
4320
+ await entry.parent.tree.import(entry.options.name);
4321
+ return;
4322
+ } catch (error) {
4323
+ return error;
4324
+ }
4325
+ }
4038
4326
  /**
4039
4327
  * Collect Loader activation failures and disabled-expression errors. Failed
4040
4328
  * fibers are awaited to recover their recorded rejection reason and coalesce
4041
4329
  * duplicate Loader notifications through the next process rejection checkpoint.
4330
+ * An entry whose import failed reports the error importing it raises now; without
4331
+ * `importNow` nothing is imported and the entry reports a failed import without the import error.
4042
4332
  */
4043
- async function inactiveEntries(ctx) {
4333
+ async function inactiveEntries(ctx, importNow = true) {
4044
4334
  const failures = [];
4045
4335
  const rejectionReasons = [];
4046
4336
  for (const entry of ctx.loader.entries()) {
@@ -4058,12 +4348,17 @@ async function inactiveEntries(ctx) {
4058
4348
  continue;
4059
4349
  }
4060
4350
  const fiber = entry.fiber;
4061
- if (fiber === void 0) {
4351
+ if (fiber === void 0 || fiber.state === FIBER_DISPOSED) {
4352
+ const error = importNow ? await importFailure(entry) : void 0;
4062
4353
  failures.push({
4063
4354
  entry,
4064
- outcome: {
4355
+ outcome: error === void 0 ? {
4356
+ kind: "failed",
4357
+ error: fiber === void 0 ? "failed to import" : `fiber state ${String(fiber.state)}`
4358
+ } : {
4065
4359
  kind: "failed",
4066
- error: "failed to import"
4360
+ error,
4361
+ phase: "failed to import"
4067
4362
  }
4068
4363
  });
4069
4364
  continue;
@@ -4106,17 +4401,21 @@ async function inactiveEntries(ctx) {
4106
4401
  return failures;
4107
4402
  }
4108
4403
  /** Render one failed plugin's original error and activation phase. */
4109
- function failureDetail(outcome) {
4110
- return `${outcome.phase === void 0 ? "" : `${outcome.phase}: `}${formatActivationError(outcome.error)}`;
4404
+ function failureDetail(outcome, stacks = true) {
4405
+ return `${outcome.phase === void 0 ? "" : `${outcome.phase}: `}${formatActivationError(outcome.error, stacks)}`;
4111
4406
  }
4112
4407
  /** Render optional-only warnings without changing startup policy. */
4113
- function activationDiagnostic(binName, failures) {
4408
+ function activationDiagnostic(binName, failures, stacks = true) {
4114
4409
  const noun = failures.length === 1 ? "entry" : "entries";
4115
- return `${binName}: warning: ${String(failures.length)} ${noun} did not activate\n${failures.map(inactiveDiagnostic).join("\n")}\n`;
4410
+ return `${binName}: warning: ${String(failures.length)} ${noun} did not activate\n${failures.map((failure) => inactiveDiagnostic(failure, stacks)).join("\n")}\n`;
4411
+ }
4412
+ /** Per-entry reload text, which people read through the Web UI and agents, so it carries error messages without stack frames. */
4413
+ function reloadDiagnostic(failure) {
4414
+ return inactiveDiagnostic(failure, false);
4116
4415
  }
4117
4416
  /** Stable per-entry text for reload comparisons and optional warnings. */
4118
- function inactiveDiagnostic({ entry, outcome }) {
4119
- const detail = outcome.kind === "failed" ? failureDetail(outcome) : `pending (waiting for ${outcome.missing.length === 1 ? "service" : "services"}: ${outcome.missing.join(", ") || "unknown"})`;
4417
+ function inactiveDiagnostic({ entry, outcome }, stacks = true) {
4418
+ const detail = outcome.kind === "failed" ? failureDetail(outcome, stacks) : `pending (waiting for ${outcome.missing.length === 1 ? "service" : "services"}: ${outcome.missing.join(", ") || "unknown"})`;
4120
4419
  return `${entry.options.id} (${entry.options.name}): ${detail}`;
4121
4420
  }
4122
4421
  /** Group startup failures and pending services, marking every required entry. */
@@ -4257,37 +4556,11 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
4257
4556
  seen.add(deepest);
4258
4557
  deepest = deepest.cause;
4259
4558
  }
4260
- 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}` : "";
4559
+ const stack = deepest instanceof AggregateError ? `\n${deepest.stack ?? deepest.message}\n${deepest.errors.map((error) => formatActivationError(error)).join("\n")}` : deepest instanceof Error && deepest !== cause ? `\n${deepest.stack ?? deepest.message}` : "";
4261
4560
  throw new Error(`${binName}: ${stage}: ${detail}${stack}`, { cause });
4262
4561
  } finally {
4263
4562
  await diagnostics.fiber.dispose();
4264
4563
  }
4265
4564
  }
4266
- /** Prompt-section name for the harness-source location line an app bin adds after boot. */
4267
- const HARNESS_SOURCE_SECTION = "harness:source";
4268
- /**
4269
- * Add a global prompt section naming the on-disk harness source checkout while
4270
- * explicitly distinguishing it from the task workspace and current working
4271
- * directory. The self-referential `dsh-tool-cordis` toolset reads and edits this
4272
- * checkout. Call once on the settled boot context ({@link boot}); the section
4273
- * uses the shared first-party placement after reusable instructions
4274
- * and before the Web surface and persona suffix. A booted tree with no
4275
- * `systemPrompt` service has no prompt to augment, so this is then a no-op
4276
- * that returns `undefined`. The section is
4277
- * registered against the `systemPrompt` service's fiber, so a dev HMR reload of
4278
- * that plugin drops it until the next boot.
4279
- * @param ctx - the settled boot context whose global system prompt to augment.
4280
- * @param sourceRoot - the absolute path to the harness checkout root.
4281
- * @returns the section disposer, or `undefined` when no `systemPrompt` service is mounted.
4282
- */
4283
- function addHarnessSourceSection(ctx, sourceRoot) {
4284
- const systemPrompt = ctx.get("systemPrompt");
4285
- if (systemPrompt === void 0) return void 0;
4286
- return systemPrompt.section({
4287
- name: HARNESS_SOURCE_SECTION,
4288
- order: systemPrompt.getSectionOrder("HARNESS_SOURCE"),
4289
- 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.`
4290
- });
4291
- }
4292
4565
  //#endregion
4293
- export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, HARNESS_SOURCE_SECTION, LOADER_EXPRESSION_SCHEMA, OFFICIAL_ON_DEMAND_CATALOG, ON_DEMAND_BUNDLES, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_COMPATIBILITY_FILENAME, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, ProfileRuntimeResolution, StartupError, addHarnessSourceSection, auditStartupEntries, boot, bundlePatchFiles, bundlePatchPaths, composeEntries, createConfigProjector, createRuntimeResolution, evaluatePluginCompatibility, generateConfigSchema, getDshRuntimeVersion, initProfile, installFailLoud, isNativeConfigSchema, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, loadProfileDirectory, mountRootInclude, pluginCompatibilityWarning, prepareProfileEntries, prepareProfilePatches, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfilePlugins, readProfileVersionExemptions, reconcileProfilePatches, reconcileProfilePlugins, removeLinkProjections, renderConfigDump, reportSkippedBundles, resolveBundleDir, resolveConfigPath, resolvePluginResource, resolveProfileDir, resolveTelemetryPatch, sanitizeProfile, setProfileVersionExemption, writeProfileBundles, writeProfileManifest };
4566
+ export { DEFAULT_PROFILE_BUNDLES, FAIL_LOUD_RELEASE_TIMEOUT_MS, LOADER_EXPRESSION_SCHEMA, OFFICIAL_ON_DEMAND_CATALOG, ON_DEMAND_BUNDLES, OPTIONAL_BUNDLES, PROFILES_DIR, PROFILE_COMPATIBILITY_FILENAME, PROFILE_PATCH_FILENAME, PROFILE_TEMPLATES, PluginPackages, ProfileRuntimeResolution, ResolutionRestartRequired, StartupError, auditStartupEntries, boot, bundlePatchFiles, bundlePatchPaths, composeEntries, createConfigProjector, createRuntimeResolution, evaluatePluginCompatibility, generateConfigSchema, getDshRuntimeVersion, initProfile, installFailLoud, isNativeConfigSchema, loadEnv, loadLayeredEnv, loadOptionalPatches, loadOverlayPatches, loadProfile, loadProfileDirectory, mountRootInclude, pluginCompatibilityWarning, prepareProfileEntries, prepareProfilePatches, readBundlePatches, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfilePlugins, readProfileVersionExemptions, recomposeProfile, reconcileProfilePatches, reconcileProfilePlugins, removeLinkProjections, renderConfigDump, reportSkippedBundles, resolveBundleDir, resolveConfigPath, resolvePluginResource, resolveProfileDir, resolveTelemetryPatch, sanitizeProfile, setProfileVersionExemption, writeProfileBundles, writeProfileManifest };