@lmzhen/dsh-evolution-commands 0.21.0 → 0.22.0

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
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { effectiveSessionPolicy } from "@lmzhen/dsh-evolution-approval";
4
4
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
- import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isMissingPath, isPresent, isUnknown, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
5
+ import { DEFAULT_SKILL_LIMITS, MAX_TIMER_DELAY_MS, PARAM_EXPOSURE, PARAM_GROUP_LABELS, absentHostServices, appendEvolutionEvent, applyNamespaceMigration, assertSkillsRootAliasRetired, buildLearnPrompt, canonicalWriteId, clampedNumber, classifyNamespaceMigration, composePresetEntry, elapsedSince, errorText, eventsFile, evolutionHome, evolutionRoot, isDeprecatedParamId, isMissingPath, isPresent, isUnknown, liveBool, mergePresetRow, newRunRegistry, newSkillLibrary, paramSettingsId, partitionVersions, planNamespaceMigration, policyStageLimits, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeText, reportsSweepLockTarget, resolveBasePresetPatch, resolveParamId, resolveRootConfig, scopedProbeReport, sweepReports, transactIo } from "@lmzhen/dsh-evolution-core";
6
6
  import { buildEnrichment, buildMaintainFacts, enrichmentSnapshotOptions, runMaintain, snapshotFromLibrary } from "@lmzhen/dsh-evolution-maintenance";
7
7
  import { copyFileSync, existsSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
@@ -235,7 +235,7 @@ async function readPlatformView(ctx) {
235
235
  */
236
236
  /** The patch file name inside a profile directory (`packages/boot/app-boot/src/profile.ts`,
237
237
  * `PROFILE_PATCH_FILENAME`). */
238
- const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
238
+ const PROFILE_PATCH_FILENAME$1 = "cordis.patch.yml";
239
239
  /**
240
240
  * The profile patch this process writes into, or undefined outside a profile.
241
241
  *
@@ -251,7 +251,7 @@ function presetProfileTarget(ctx) {
251
251
  return {
252
252
  profile: profile.name,
253
253
  profileDir: profile.dir,
254
- patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME),
254
+ patchPath: typeof profile.patchPath === "string" && profile.patchPath !== "" ? profile.patchPath : join(profile.dir, PROFILE_PATCH_FILENAME$1),
255
255
  bundles: Array.isArray(profile.startedBundles) ? profile.startedBundles : []
256
256
  };
257
257
  }
@@ -314,6 +314,135 @@ function moduleGraphPath(specifier) {
314
314
  return fileURLToPath(resolve(specifier));
315
315
  }
316
316
  //#endregion
317
+ //#region lib/types/profiles.js
318
+ /**
319
+ * The ONE reader of `<home>/profiles` (G3, 0.22).
320
+ *
321
+ * Two very different questions used to share one bare helper: diagnostics want EVERY profile under
322
+ * the home (doctor's cross-profile summary), while the preset-install exclusion must only ever look
323
+ * at THIS plane — a leftover profile from the other plane's home used to make the exclusion refuse a
324
+ * legitimate install (the two-plane split made that reachable: `~/.dsh/profiles/web` sat next
325
+ * to `profiles/desktop`). The directory is therefore read here and nowhere else:
326
+ * `scanProfiles` is the single `readdirSync` of it, `homeProfiles` answers the diagnostic
327
+ * question, and `installExclusionTarget` answers the write-path one — fail-closed, because an
328
+ * unreadable home must never read as "nothing is installed".
329
+ *
330
+ * @module evolution-commands/src/profiles
331
+ */
332
+ /** The patch file name inside a profile directory (the platform's own spelling). */
333
+ const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
334
+ /** The ONLY directory read of `<home>/profiles` in the family.
335
+ * `profiles/node_modules` is a platform-created directory that never carries a manifest (S2.1), dot-entries
336
+ * are not profiles, and a directory without `package.json` is not one either.
337
+ * @param home - the resolved DSH home.
338
+ * @returns the scanned entries, or the enumeration error (never a silent empty list).
339
+ */
340
+ function scanProfiles(home) {
341
+ const root = join(home, "profiles");
342
+ if (!existsSync(root)) return {
343
+ kind: "scanned",
344
+ profiles: []
345
+ };
346
+ let entries;
347
+ try {
348
+ entries = readdirSync(root, { withFileTypes: true });
349
+ } catch (error) {
350
+ return {
351
+ kind: "unreadable",
352
+ error
353
+ };
354
+ }
355
+ const profiles = [];
356
+ for (const entry of entries) {
357
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
358
+ if (entry.name === "node_modules") continue;
359
+ const dir = join(root, entry.name);
360
+ if (!existsSync(join(dir, "package.json"))) continue;
361
+ profiles.push({
362
+ profile: entry.name,
363
+ dir
364
+ });
365
+ }
366
+ return {
367
+ kind: "scanned",
368
+ profiles
369
+ };
370
+ }
371
+ /** Every profile directory under one home, by name — the DIAGNOSTIC question (doctor's cross-profile
372
+ * summary). An unreadable home answers the empty list here because doctor reports the failure on its
373
+ * own DEGRADED row; a write path must ask {@link installExclusionTarget} instead.
374
+ * @param home - the resolved DSH home.
375
+ * @returns one entry per profile directory carrying a manifest.
376
+ */
377
+ function homeProfiles(home) {
378
+ const scan = scanProfiles(home);
379
+ return scan.kind === "scanned" ? scan.profiles : [];
380
+ }
381
+ /** The bundle names one profile's manifest declares.
382
+ * @param profileDir - the profile directory.
383
+ * @param onError - called with the read/parse failure; a torn manifest is NOT "no bundles".
384
+ * @returns the declared bundle names (empty when the manifest is absent).
385
+ */
386
+ function profileBundles(profileDir, onError) {
387
+ try {
388
+ const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
389
+ return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
390
+ } catch (error) {
391
+ onError?.(error);
392
+ return [];
393
+ }
394
+ }
395
+ /** One optional file's text, or `''` when it is absent or unreadable.
396
+ * @param path - the file to read.
397
+ * @returns the text.
398
+ */
399
+ function readTextOrEmpty(path) {
400
+ try {
401
+ return readFileSync(path, "utf8");
402
+ } catch {
403
+ return "";
404
+ }
405
+ }
406
+ /** The profile's own patch text, or `''` when it has none.
407
+ * @param profileDir - the profile directory.
408
+ * @returns the patch text.
409
+ */
410
+ function profilePatchText(profileDir) {
411
+ return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
412
+ }
413
+ /** Where the preset-install exclusion must look: THIS plane only, or an explicit refusal.
414
+ *
415
+ * The write path does NOT scan the home: the mounted profile is the whole question (its started
416
+ * bundles plus the ones its own manifest declares — either can double-mount), and a question that
417
+ * cannot be answered still REFUSES. That is what makes a leftover profile from the other plane
418
+ * harmless here: it is not this plane's profile, so it is never consulted.
419
+ * @param mounted - the mounted profile, or undefined outside a profile (a bare library mount).
420
+ * @returns the profile to judge with its bundle rows, or the reason a refusal is mandatory.
421
+ */
422
+ function installExclusionTarget(mounted) {
423
+ if (mounted === void 0 || mounted.dir === "") return {
424
+ kind: "undecidable",
425
+ reason: "no profile is mounted in this process, so the double-mount question cannot be answered here"
426
+ };
427
+ let manifestFailure;
428
+ const declared = profileBundles(mounted.dir, (error) => {
429
+ manifestFailure = error;
430
+ });
431
+ const manifestCode = manifestFailure?.code;
432
+ if (manifestFailure !== void 0 && manifestCode !== "ENOENT") return {
433
+ kind: "undecidable",
434
+ reason: "the mounted profile manifest could not be read"
435
+ };
436
+ return {
437
+ kind: "mounted",
438
+ profile: {
439
+ profile: mounted.profile,
440
+ dir: mounted.dir
441
+ },
442
+ bundles: [...mounted.bundles, ...declared]
443
+ };
444
+ }
445
+ //#endregion
317
446
  //#region lib/types/doctor.js
318
447
  /**
319
448
  * `/evolution doctor` — read-only self-check (0.3.55, WB2).
@@ -460,22 +589,16 @@ function asMapping(value) {
460
589
  * list alone, and treating it as "no bundles" is exactly the fail-open state
461
590
  * the double-mount exclusion exists to prevent. */
462
591
  function collectEvolutionBundles(home, onReadError) {
463
- const profilesDir = join(home, "profiles");
464
- if (!existsSync(profilesDir)) return [];
465
- const bundles = [];
466
- let profileEntries;
467
- try {
468
- profileEntries = readdirSync(profilesDir, { withFileTypes: true });
469
- } catch (error) {
470
- onReadError?.(error, "profiles-dir");
471
- return bundles;
592
+ const scan = scanProfiles(home);
593
+ if (scan.kind === "unreadable") {
594
+ onReadError?.(scan.error, "profiles-dir");
595
+ return [];
472
596
  }
473
- for (const profile of profileEntries) {
474
- if (!profile.isDirectory()) continue;
475
- if (profile.name === "node_modules" || profile.name.startsWith(".")) continue;
597
+ const bundles = [];
598
+ for (const { dir } of scan.profiles) {
476
599
  let raw;
477
600
  try {
478
- raw = readFileSync(join(profilesDir, profile.name, "package.json"), "utf8");
601
+ raw = readFileSync(join(dir, "package.json"), "utf8");
479
602
  } catch (error) {
480
603
  if (error?.code === "ENOENT") continue;
481
604
  onReadError?.(error, "profile-manifest");
@@ -579,62 +702,6 @@ function readAgentPresetBases$1(path) {
579
702
  return null;
580
703
  }
581
704
  }
582
- /** The bundle rows one profile manifest mounts (scope-agnostic, as written) — the target a preset
583
- * recompute resolves its platform base patch through.
584
- * @param profileDir - the profile directory.
585
- * @returns the bundle names; empty when the manifest is missing or torn.
586
- */
587
- function profileBundles(profileDir) {
588
- try {
589
- const bundles = JSON.parse(readFileSync(join(profileDir, "package.json"), "utf8")).dsh?.profile?.bundles;
590
- return Array.isArray(bundles) ? bundles.filter((name) => typeof name === "string") : [];
591
- } catch {
592
- return [];
593
- }
594
- }
595
- /** Every profile directory under one DSH_HOME, by name.
596
- * @param home - resolved DSH_HOME.
597
- * @returns `{ profile, dir }` per directory under `profiles/` that carries a manifest.
598
- */
599
- function profileDirectories(home) {
600
- const out = [];
601
- const root = join(home, "profiles");
602
- let entries;
603
- try {
604
- entries = readdirSync(root, { withFileTypes: true });
605
- } catch {
606
- return out;
607
- }
608
- for (const entry of entries) {
609
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
610
- if (entry.name === "node_modules") continue;
611
- const dir = join(root, entry.name);
612
- if (!existsSync(join(dir, "package.json"))) continue;
613
- out.push({
614
- profile: entry.name,
615
- dir
616
- });
617
- }
618
- return out;
619
- }
620
- /** One optional file's text, or `''` when it is absent or unreadable.
621
- * @param path - the file to read.
622
- * @returns the text.
623
- */
624
- function readTextOrEmpty(path) {
625
- try {
626
- return readFileSync(path, "utf8");
627
- } catch {
628
- return "";
629
- }
630
- }
631
- /** The profile patch text, or `''` when the profile has none.
632
- * @param profileDir - the profile directory.
633
- * @returns the patch text.
634
- */
635
- function profilePatchText(profileDir) {
636
- return readTextOrEmpty(join(profileDir, PROFILE_PATCH_FILENAME));
637
- }
638
705
  /** Every USER-layer patch text that can carry a family preset row: each profile's own patch, then
639
706
  * the home layer (`<home>/cordis.patch.yml`), which the platform composes AFTER the profile layer
640
707
  * (`readProfilePatches` in `packages/boot/app-boot/src/profile-context.ts`). A row written there
@@ -644,7 +711,7 @@ function profilePatchText(profileDir) {
644
711
  * @returns the patch texts that exist, in profile-then-home order.
645
712
  */
646
713
  function userPatchTexts(home) {
647
- const texts = profileDirectories(home).map(({ dir }) => profilePatchText(dir));
714
+ const texts = homeProfiles(home).map(({ dir }) => profilePatchText(dir));
648
715
  texts.push(readTextOrEmpty(join(home, PROFILE_PATCH_FILENAME)));
649
716
  return texts.filter((text) => text !== "");
650
717
  }
@@ -718,7 +785,7 @@ function probePresetFreshness(home, servicePresent) {
718
785
  } catch (error) {
719
786
  deltaFailure = `${deltaPath} could not be read (${error instanceof Error ? error.message : String(error)})`;
720
787
  }
721
- const profiles = profileDirectories(home);
788
+ const profiles = homeProfiles(home);
722
789
  if (bases === null) {
723
790
  const tablePath = basesPath ?? `${AGENT_PRESET_PACKAGE}/bases.json`;
724
791
  return profiles.flatMap(({ profile, dir }) => {
@@ -849,6 +916,7 @@ async function legacyMigrationRow(ctx, home) {
849
916
  }
850
917
  async function diagnose(ctx, options = {}) {
851
918
  const home = options.home ?? evolutionRoot();
919
+ const mounted = options.mounted;
852
920
  const conflicts = [];
853
921
  const bundles = collectEvolutionBundles(home, (error, scope) => {
854
922
  const detail = error instanceof Error ? error.message : String(error);
@@ -944,6 +1012,7 @@ async function diagnose(ctx, options = {}) {
944
1012
  installForm,
945
1013
  deploymentForm,
946
1014
  bundles,
1015
+ foreignProfiles: homeProfiles(home).filter(({ profile, dir }) => mounted === void 0 || profile !== mounted.profile && dir !== mounted.dir).map(({ profile }) => profile),
947
1016
  conflicts,
948
1017
  envIssues: env,
949
1018
  memoryIssues,
@@ -1102,6 +1171,7 @@ function renderDoctorText(report) {
1102
1171
  lines.push(`preset: ${row.base} → ${row.destination} ${verdict}`);
1103
1172
  }
1104
1173
  if (report.presetIssues.length > 0) lines.push("agent presets:", ...report.presetIssues.map((line) => ` ! ${line}`));
1174
+ if (report.foreignProfiles.length > 0) lines.push("profiles-foreign: " + report.foreignProfiles.join(", ") + " — not part of this plane: /evolution preset install ignores them, while the bundles (all profiles) line above counts them");
1105
1175
  if (report.conflicts.length > 0) lines.push("conflicts:", ...report.conflicts.map((line) => ` ! ${line}`));
1106
1176
  if (report.envIssues.length > 0) lines.push("env:", ...report.envIssues.map((line) => ` ! ${line}`));
1107
1177
  if (report.memoryIssues.length > 0) lines.push("memory:", ...report.memoryIssues.map((line) => ` ! ${line}`));
@@ -1565,19 +1635,6 @@ async function readRunReport(io, record) {
1565
1635
  };
1566
1636
  }
1567
1637
  }
1568
- /**
1569
- * Read a switch that may be a live settings reference.
1570
- *
1571
- * The loader hands a `.volatile()` field as `{ get() }` and the value is read at USE time; a
1572
- * directly constructed plugin passes a plain boolean (the schema is the loader's face — the same
1573
- * split the numeric knobs document as their second clamp layer). A single-form read is wrong
1574
- * either way: `=== true` is never true for a reference, and `?.get()` ignores the plain value.
1575
- * @param value - the configured switch, in either form, or undefined.
1576
- * @returns the switch; undefined reads as off.
1577
- */
1578
- function liveFlag(value) {
1579
- return typeof value === "boolean" ? value : value?.get() ?? false;
1580
- }
1581
1638
  function apply(ctx, rawConfig = {}) {
1582
1639
  const config = rawConfig;
1583
1640
  assertSkillsRootAliasRetired(rawConfig);
@@ -1989,7 +2046,7 @@ function apply(ctx, rawConfig = {}) {
1989
2046
  }).catch((error) => {
1990
2047
  ctx.logger.warn(`evolution-commands: failed to record maintain event: ${String(error)}`);
1991
2048
  });
1992
- if (liveFlag(config.maintainHandoffOnSettle) && resultRef !== void 0 && runAgent !== void 0) try {
2049
+ if (liveBool(config.maintainHandoffOnSettle) && resultRef !== void 0 && runAgent !== void 0) try {
1993
2050
  if (deliverHandoff(runAgent, createUserMessage({
1994
2051
  content: [{
1995
2052
  type: "text",
@@ -2148,17 +2205,17 @@ function apply(ctx, rawConfig = {}) {
2148
2205
  if (presetInstall !== null || /^preset\b/.test(input)) {
2149
2206
  if (presetInstall === null) return err("Unknown preset arguments: expected `preset install` or `preset install --base <name>[,<name>...]` (the names live in the agent package's bases.json). Got: " + input);
2150
2207
  {
2151
- let installedBundles;
2152
- try {
2153
- installedBundles = collectEvolutionBundles(evolutionRoot(), (error) => {
2154
- throw error;
2155
- });
2156
- } catch (enumerationError) {
2157
- return err(`Refusing to install the layered Evolution preset: the home profile directory or one of its profile manifests could not be read (${enumerationError instanceof Error ? enumerationError.message : String(enumerationError)}) — the all/preset double-mount check could not run. Resolve access to the profiles directory and retry, or install via the layered installer, which enforces the same exclusion up front.`);
2158
- }
2208
+ const mounted = presetProfileTarget(ctx);
2209
+ const exclusion = installExclusionTarget(mounted === void 0 ? void 0 : {
2210
+ profile: mounted.profile,
2211
+ dir: mounted.profileDir,
2212
+ bundles: mounted.bundles
2213
+ });
2214
+ if (exclusion.kind === "undecidable") return err(`Refusing to install the layered Evolution preset: ${exclusion.reason} — the all/preset double-mount check could not run. Resolve access to this profile and retry, or install via the layered installer, which enforces the same exclusion up front.`);
2215
+ const installedBundles = exclusion.bundles;
2159
2216
  const allBundle = installedBundles.find((name) => name === "dsh-evolution-all" || name.endsWith("/dsh-evolution-all"));
2160
2217
  const presetBundle = installedBundles.find((name) => name === "dsh-evolution-preset" || name.endsWith("/dsh-evolution-preset"));
2161
- if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this home — its model rows (tool-memory / tool-skill-manage / tool-session-query / skill-catalog) would double-mount with the preset's and the profile fails loud at startup. Keep ONE: remove the ${allBundle ? "evolution-all bundle" : "evolution-preset bundle"} before installing the layered preset (see /evolution doctor).`);
2218
+ if (allBundle || presetBundle) return err(`Refusing to install the layered Evolution preset: "${allBundle ?? presetBundle}" is installed in this profile — its model rows (tool-memory / tool-skill-manage / tool-session-query / skill-catalog) would double-mount with the preset's and the profile fails loud at startup. Keep ONE: remove the ${allBundle ? "evolution-all bundle" : "evolution-preset bundle"} before installing the layered preset (see /evolution doctor).`);
2162
2219
  }
2163
2220
  try {
2164
2221
  const source = resolveAgentPresetDir(import.meta.url);
@@ -2197,7 +2254,7 @@ function apply(ctx, rawConfig = {}) {
2197
2254
  merged = next;
2198
2255
  }
2199
2256
  if (merged !== current) atomicWriteFiles(dirname(profile.patchPath), [{
2200
- name: PROFILE_PATCH_FILENAME,
2257
+ name: PROFILE_PATCH_FILENAME$1,
2201
2258
  content: presetPatchText(merged)
2202
2259
  }], void 0, (message) => {
2203
2260
  ctx.logger.warn(message);
@@ -2366,7 +2423,16 @@ function apply(ctx, rawConfig = {}) {
2366
2423
  return ok(renderNamespaceMigration(attempt.outcome));
2367
2424
  }
2368
2425
  if (input === "doctor" || input === "doctor --json") {
2369
- const report = await diagnose(ctx, { home: evolutionRoot() });
2426
+ const mountedForDoctor = presetProfileTarget(ctx);
2427
+ const mounted = mountedForDoctor === void 0 ? {} : { mounted: {
2428
+ profile: mountedForDoctor.profile,
2429
+ dir: mountedForDoctor.profileDir,
2430
+ bundles: mountedForDoctor.bundles
2431
+ } };
2432
+ const report = await diagnose(ctx, {
2433
+ home: evolutionRoot(),
2434
+ ...mounted
2435
+ });
2370
2436
  if (input === "doctor --json") return ok(JSON.stringify(report, null, 2));
2371
2437
  return ok(renderDoctorText(report));
2372
2438
  }
@@ -1,4 +1,5 @@
1
1
  import { type PlatformServiceProbe, type ScopedProbeReport } from '@lmzhen/dsh-evolution-core';
2
+ import type { MountedProfile } from './profiles.ts';
2
3
  /** P2-20 (audit): the TS-side copy of the install-target tails. The script
3
4
  * side (install-layered.mjs, verify-profile-bundles.mjs) imports the list
4
5
  * from packages/scripts/lib-family-packages.mjs; guard-scripts.spec.ts pins
@@ -79,6 +80,11 @@ export interface DoctorReport {
79
80
  * counterpart (inferred from the install form, P0-1) and may disagree on a
80
81
  * multi-profile machine; the render marks the aggregation explicitly. */
81
82
  bundles: string[];
83
+ /** G3 (0.22): profile directories under this home that are NOT the profile this process runs in.
84
+ * On a one-home-two-planes machine (or after a home clone) they are leftovers from the other
85
+ * plane: the write-path exclusion ignores them, while {@link bundles} still counts them — this
86
+ * list is what makes that difference visible instead of mysterious. */
87
+ foreignProfiles: string[];
82
88
  conflicts: string[];
83
89
  envIssues: string[];
84
90
  /** S0.2 (v37 P0-1): memory files carrying the platform's `{{` prompt-variable
@@ -153,6 +159,7 @@ export declare function diagnose(ctx: {
153
159
  get(name: string): unknown;
154
160
  }, options?: {
155
161
  home?: string;
162
+ mounted?: MountedProfile;
156
163
  }): Promise<DoctorReport>;
157
164
  export declare function renderDoctorText(report: DoctorReport): string;
158
165
  //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1,74 @@
1
+ /** The patch file name inside a profile directory (the platform's own spelling). */
2
+ export declare const PROFILE_PATCH_FILENAME = "cordis.patch.yml";
3
+ /** One profile directory under a home. */
4
+ export interface HomeProfile {
5
+ /** The profile's directory name (what `--profile` spells). */
6
+ readonly profile: string;
7
+ /** Its absolute (or home-relative-joined) directory. */
8
+ readonly dir: string;
9
+ }
10
+ /** The outcome of reading `<home>/profiles`: the entries, or why they could not be read. */
11
+ export type ProfilesScan = {
12
+ readonly kind: 'scanned';
13
+ readonly profiles: readonly HomeProfile[];
14
+ } | {
15
+ readonly kind: 'unreadable';
16
+ readonly error: unknown;
17
+ };
18
+ /** The ONLY directory read of `<home>/profiles` in the family.
19
+ * `profiles/node_modules` is a platform-created directory that never carries a manifest (S2.1), dot-entries
20
+ * are not profiles, and a directory without `package.json` is not one either.
21
+ * @param home - the resolved DSH home.
22
+ * @returns the scanned entries, or the enumeration error (never a silent empty list).
23
+ */
24
+ export declare function scanProfiles(home: string): ProfilesScan;
25
+ /** Every profile directory under one home, by name — the DIAGNOSTIC question (doctor's cross-profile
26
+ * summary). An unreadable home answers the empty list here because doctor reports the failure on its
27
+ * own DEGRADED row; a write path must ask {@link installExclusionTarget} instead.
28
+ * @param home - the resolved DSH home.
29
+ * @returns one entry per profile directory carrying a manifest.
30
+ */
31
+ export declare function homeProfiles(home: string): readonly HomeProfile[];
32
+ /** The bundle names one profile's manifest declares.
33
+ * @param profileDir - the profile directory.
34
+ * @param onError - called with the read/parse failure; a torn manifest is NOT "no bundles".
35
+ * @returns the declared bundle names (empty when the manifest is absent).
36
+ */
37
+ export declare function profileBundles(profileDir: string, onError?: (error: unknown) => void): string[];
38
+ /** One optional file's text, or `''` when it is absent or unreadable.
39
+ * @param path - the file to read.
40
+ * @returns the text.
41
+ */
42
+ export declare function readTextOrEmpty(path: string): string;
43
+ /** The profile's own patch text, or `''` when it has none.
44
+ * @param profileDir - the profile directory.
45
+ * @returns the patch text.
46
+ */
47
+ export declare function profilePatchText(profileDir: string): string;
48
+ /** The profile this PROCESS is mounted in, as the write-path exclusion must see it. */
49
+ export interface MountedProfile {
50
+ /** The mounted profile's name. */
51
+ readonly profile: string;
52
+ /** Its directory. */
53
+ readonly dir: string;
54
+ /** The bundles this plane actually STARTED (the platform's `startedBundles`). */
55
+ readonly bundles: readonly string[];
56
+ }
57
+ /** Where the preset-install exclusion must look: THIS plane only, or an explicit refusal.
58
+ *
59
+ * The write path does NOT scan the home: the mounted profile is the whole question (its started
60
+ * bundles plus the ones its own manifest declares — either can double-mount), and a question that
61
+ * cannot be answered still REFUSES. That is what makes a leftover profile from the other plane
62
+ * harmless here: it is not this plane's profile, so it is never consulted.
63
+ * @param mounted - the mounted profile, or undefined outside a profile (a bare library mount).
64
+ * @returns the profile to judge with its bundle rows, or the reason a refusal is mandatory.
65
+ */
66
+ export declare function installExclusionTarget(mounted: MountedProfile | undefined): {
67
+ readonly kind: 'mounted';
68
+ readonly profile: HomeProfile;
69
+ readonly bundles: readonly string[];
70
+ } | {
71
+ readonly kind: 'undecidable';
72
+ readonly reason: string;
73
+ };
74
+ //# sourceMappingURL=profiles.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lmzhen/dsh-evolution-commands",
3
3
  "description": "Human commands for the evolution family (/evolution pending|curator|maintain|doctor) (community build)",
4
- "version": "0.21.0",
4
+ "version": "0.22.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -27,12 +27,12 @@
27
27
  "license": "MIT",
28
28
  "dependencies": {
29
29
  "@deepseek-ai/schemastery": "^3.18.4",
30
- "@lmzhen/dsh-evolution-approval": "^0.21.0",
31
- "@lmzhen/dsh-evolution-core": "^0.21.0",
32
- "@lmzhen/dsh-evolution-maintenance": "^0.21.0"
30
+ "@lmzhen/dsh-evolution-approval": "^0.22.0",
31
+ "@lmzhen/dsh-evolution-core": "^0.22.0",
32
+ "@lmzhen/dsh-evolution-maintenance": "^0.22.0"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@lmzhen/dsh-evolution-agent-preset": "^0.21.0"
35
+ "@lmzhen/dsh-evolution-agent-preset": "^0.22.0"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",