@lmzhen/dsh-evolution-core 0.26.26 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +66 -850
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
package/README.md
CHANGED
|
@@ -93,8 +93,11 @@ consumer — so the `@mode` target stays at six (decision 10, provisional: keep)
|
|
|
93
93
|
| `E-320` | the profile home is not reported by this host, so the legacy settings document cannot be located | run the command in a session whose profile home is set |
|
|
94
94
|
| `E-321` | no evolution-io provider is mounted, so the legacy settings document cannot be read | mount the `evolution-io-node` row |
|
|
95
95
|
| `E-322` | a cross-field parameter constraint would be broken (the field, its floor and the attempted value are included) | raise the value to the named floor, or lower the sibling first |
|
|
96
|
+
| `E-323` | the subagent channel is unavailable: no subagents/agentPresets service, or the spawn provider lacks the agentPreset capability | run `/evolution doctor`; review keeps its inject channel, maintenance reports the channel unavailable instead of degrading silently |
|
|
97
|
+
| `E-324` | a restricted preset could not be created (parent preset read, YAML parse, register, or restrictor mount failed; the reason is included) | run `/evolution doctor` to see the preset id, allow set and failure reason; the review/maintenance channel stays unavailable until it is fixed |
|
|
98
|
+
| `E-325` | the host data plane is unavailable: no connection service, so panel/approval routes are not registered | use `/evolution pending`, `approve`, `reject` (same service methods) and run `/evolution doctor` |
|
|
96
99
|
| `E-319` | the confirmation before a create or bare delete went unanswered for `skillWriteConfirmTimeoutSeconds` (`skillWriteConfirm: 'timeout'`), so the write was cancelled | set `skillWriteConfirm` to `auto` (write without asking), keep `ask` (wait until answered), or raise the timeout |
|
|
97
|
-
Symptom → next step; the exact text of every code (`E-301`–`E-
|
|
100
|
+
Symptom → next step; the exact text of every code (`E-301`–`E-325`) is stored once in
|
|
98
101
|
|
|
99
102
|
## 单元与撤销(本包负责的契约)
|
|
100
103
|
|
|
@@ -102,3 +105,15 @@ Symptom → next step; the exact text of every code (`E-301`–`E-322`) is store
|
|
|
102
105
|
- 成员解析与孤路标判定在 src/unit-members.ts:candidateSkills(代表片首位、去重保序)、resolveUnitMembersFromIndexes(只按已知 key、三向校验)、landmarkVerdict 与 staleRouteStubs(可跳 ⟺ 目标存在 ∧ 其索引仍持该 unitId;输出按 skill/version/owner 确定性排序)。
|
|
103
106
|
- 写作端的硬规则:kind 判 previous === null(不是 undefined);path 抄循环里既有的 rel;同一目标写两次时清单保留最后一次写入的 afterRef。
|
|
104
107
|
- 面向读者的完整说明见仓库 docs/units-and-undo.md。
|
|
108
|
+
|
|
109
|
+
## Restricted preset ids
|
|
110
|
+
|
|
111
|
+
The review and maintenance channels run under two internally generated restricted presets (they are
|
|
112
|
+
NOT user-facing preset picker entries):
|
|
113
|
+
|
|
114
|
+
- `evolution-reviewer-readonly` — the review channel's child preset;
|
|
115
|
+
- `evolution-maintainer-readonly` — the maintenance channel's child preset.
|
|
116
|
+
|
|
117
|
+
The family's own agent-preset ids (`EVOLUTION_PRESET_IDS`) are `evolution`, `evolution-ptc`,
|
|
118
|
+
`evolution-cordis` and `evolution-minimal`. The two id sets are deliberately separate: a restricted
|
|
119
|
+
preset is created at runtime from a parent preset, never installed or selected.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as entryListSchema, c as MAINTAINER_PRESET_ID, d as RESTRICTED_TOOLS_ROW_ID, f as RESTRICTOR_MODULE, h as restrictedPresetId, i as composeRestrictedPresetDefinition, l as RESTRICTED_PRESET_DELIVERY, m as normalizeAllow, n as ensureRestrictedPreset, o as parsePresetRows, p as REVIEWER_PRESET_ID, r as restrictedPresetState, s as EVOLUTION_PRESET_IDS, t as disposeRestrictedPreset, u as RESTRICTED_PRESET_DISPLAY } from "../ensure-MEYMj9jn.js";
|
|
2
|
+
import { C as trimTrailingBlankLines, S as rowSiblingEnd, _ as presetRowId, a as composePresetComposition, b as resolveBasePresetPatch, c as composePresetRow, d as listDirectories, f as loadRowOverrides, g as presetRowBody, h as presetRowBlock, i as basePresetPlugins, l as compositionRowIds, m as presetPatchText, n as AGENT_PRESET_ROOT_HINT, o as composePresetEntry, p as mergePresetRow, r as EMPTY_PATCH_SEED, s as composePresetInsert, t as AGENT_PRESET_PACKAGE, u as denseBlockSequence, v as probeDesktopScope, x as rowBlockSpan, y as removePresetRow } from "../composition-D8Y3-ROd.js";
|
|
3
|
+
export { AGENT_PRESET_PACKAGE, AGENT_PRESET_ROOT_HINT, EMPTY_PATCH_SEED, EVOLUTION_PRESET_IDS, MAINTAINER_PRESET_ID, RESTRICTED_PRESET_DELIVERY, RESTRICTED_PRESET_DISPLAY, RESTRICTED_TOOLS_ROW_ID, RESTRICTOR_MODULE, REVIEWER_PRESET_ID, basePresetPlugins, composePresetComposition, composePresetEntry, composePresetInsert, composePresetRow, composeRestrictedPresetDefinition, compositionRowIds, denseBlockSequence, disposeRestrictedPreset, ensureRestrictedPreset, entryListSchema, listDirectories, loadRowOverrides, mergePresetRow, normalizeAllow, parsePresetRows, presetPatchText, presetRowBlock, presetRowBody, presetRowId, probeDesktopScope, removePresetRow, resolveBasePresetPatch, restrictedPresetId, restrictedPresetState, rowBlockSpan, rowSiblingEnd, trimTrailingBlankLines };
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { _ as scopedPackageName } from "./ensure-MEYMj9jn.js";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
//#region lib/types/env.js
|
|
6
|
+
/**
|
|
7
|
+
* Evolution environment-variable reading, single-source (WC, 0.3.56).
|
|
8
|
+
*
|
|
9
|
+
* Every `DSH_EVOLUTION_*` value the PLUGIN code reads goes through this module
|
|
10
|
+
* so the trim/whitelist patterns (v10 P2-12/13) live in exactly one place and
|
|
11
|
+
* the generated env reference can point at one behavior. Configuration-layer
|
|
12
|
+
* reads inside patch YAML `!!js` expressions (session-query path/openAt) stay
|
|
13
|
+
* in the profile config evaluation — they are NOT migrated (they are resolved
|
|
14
|
+
* at cordis config time, not plugin time) but are documented in the README
|
|
15
|
+
* env table. `EVOLUTION_SCOPE` is read by the source installers only
|
|
16
|
+
* (`packages/scripts/install-layered.mjs`, `packages/test-support/row-contract.ts`),
|
|
17
|
+
* never by plugin runtime code.
|
|
18
|
+
*
|
|
19
|
+
* v14 P3-1: the former `EVOLUTION_ENV_KEYS` export was deleted — nothing read
|
|
20
|
+
* it, so the "generated env reference" it claimed to source was never
|
|
21
|
+
* generated (the README table is maintained by hand and now lists every key).
|
|
22
|
+
*/
|
|
23
|
+
const ALLOW_ROW_COLLISIONS = "1";
|
|
24
|
+
/** N-5 escape: `DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1` downgrades a delta-row
|
|
25
|
+
* collision from fail-loud to warn+keep-both. Any other value (including an
|
|
26
|
+
* EMPTY/whitespace string — a set-but-unset variable) keeps the fail-loud. */
|
|
27
|
+
function allowRowCollisions(env = process.env) {
|
|
28
|
+
return env.DSH_EVOLUTION_ALLOW_ROW_COLLISIONS === ALLOW_ROW_COLLISIONS;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region lib/types/composition/composer.js
|
|
32
|
+
/**
|
|
33
|
+
* Build the user-root Evolution preset composition from the RUNTIME platform's
|
|
34
|
+
* `standard` preset rows plus the evolution delta rows (P1-1 follow-up,
|
|
35
|
+
* 0.3.15): the agent-preset registry mounts ONE composition file verbatim, so
|
|
36
|
+
* a delta-only `agent.cordis.yml` would produce an agent carrying only the
|
|
37
|
+
* delta rows.
|
|
38
|
+
*
|
|
39
|
+
* Same contract as `install-layered.mjs` `generateAgentPreset` (the source
|
|
40
|
+
* install path) — installer.spec pins byte parity between the two, and both
|
|
41
|
+
* apply the identical row-collision contract: a delta id that overlaps a
|
|
42
|
+
* standard row id fails loud by default, and `DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1`
|
|
43
|
+
* downgrades it to a warning that keeps both (the row mounts twice).
|
|
44
|
+
*
|
|
45
|
+
* V10-14 / 0.3.53 (P1-2): BOTH composers now inject the Hermes 60-char catalog
|
|
46
|
+
* cap onto the standard-sourced `- id: tool-skill` row of the composed preset
|
|
47
|
+
* (see injectCatalogDescriptionCap) — the session-visible tool-skill instance
|
|
48
|
+
* mounts in the preset's own standing scope, where no profile-root patch can
|
|
49
|
+
* reach it. 0.3.53 moved the injection INTO the composer so
|
|
50
|
+
* `/evolution preset install` (the npm user's only preset path) gets it too;
|
|
51
|
+
* install-layered applies the byte-identical rule, pinned by installer.spec.
|
|
52
|
+
*
|
|
53
|
+
* Row ids are read from `- id:` lines; an id present in both fragments would
|
|
54
|
+
* mount twice and could shadow the platform row, so it fails loud.
|
|
55
|
+
* @param standardComposition - the runtime `standard` preset composition.
|
|
56
|
+
* @param deltaComposition - the evolution delta fragment.
|
|
57
|
+
* @returns the composed preset composition (standard rows first, then delta).
|
|
58
|
+
*/
|
|
59
|
+
function composePresetComposition(standardComposition, deltaComposition) {
|
|
60
|
+
const standardIds = compositionRowIds(standardComposition);
|
|
61
|
+
const collisions = [...compositionRowIds(deltaComposition)].filter((id) => standardIds.has(id)).sort();
|
|
62
|
+
if (collisions.length > 0 && !allowRowCollisions()) throw new Error(`evolution preset composition: delta rows collide with runtime standard rows: ${collisions.join(", ")}`);
|
|
63
|
+
if (collisions.length > 0) console.warn(`evolution preset composition: warning — delta rows collide with standard rows (${collisions.join(", ")}); keeping both (DSH_EVOLUTION_ALLOW_ROW_COLLISIONS=1)`);
|
|
64
|
+
return applyRowOverrides(`${standardComposition.replace(/\s+$/, "")}\n\n${deltaComposition.trim()}\n`);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* One table, two generation paths: `row-overrides.json` at the PACKAGE ROOT
|
|
68
|
+
* states what a generated preset must carry beyond the platform composition,
|
|
69
|
+
* and the source installer (`scripts/install-layered.mjs`) reads the same file.
|
|
70
|
+
* The path resolves from both `src/` and the built `lib/`, which is why the
|
|
71
|
+
* file sits at the root and ships in `files`.
|
|
72
|
+
*
|
|
73
|
+
* Before 0.3.77 each side carried its own hand-kept copy of this table; they
|
|
74
|
+
* happened to stay byte-identical, which is exactly the kind of agreement no
|
|
75
|
+
* test can keep — the entries are now data, and a divergence is impossible.
|
|
76
|
+
*/
|
|
77
|
+
/**
|
|
78
|
+
* Locate row-overrides.json beside the nearest package manifest above this module.
|
|
79
|
+
* A fixed ../ hop is WRONG once the module is bundled: tsdown may emit the composer body into
|
|
80
|
+
* lib/composition/index.js or flatten it into lib/<chunk>.js, so the caller-relative depth varies.
|
|
81
|
+
* The manifest anchor is stable in every shape (src/, lib/, lib/composition/, chunk files).
|
|
82
|
+
*/
|
|
83
|
+
function rowOverridesPath() {
|
|
84
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
85
|
+
for (let hops = 0; hops < 8; hops += 1) {
|
|
86
|
+
const candidate = join(dir, "row-overrides.json");
|
|
87
|
+
if (existsSync(candidate) && existsSync(join(dir, "package.json"))) return candidate;
|
|
88
|
+
const parent = dirname(dir);
|
|
89
|
+
if (parent === dir) break;
|
|
90
|
+
dir = parent;
|
|
91
|
+
}
|
|
92
|
+
throw new Error("evolution-core: row-overrides.json not found beside the package manifest (bundled path?)");
|
|
93
|
+
}
|
|
94
|
+
/** One cache per PATH: the installer reads the same file through its own root, and a single-slot
|
|
95
|
+
* cache would hand the second caller the first caller's bytes. */
|
|
96
|
+
const overridesCache = /* @__PURE__ */ new Map();
|
|
97
|
+
/**
|
|
98
|
+
* Read and validate the shared override table.
|
|
99
|
+
* G6.1/M1: the path is a PARAMETER (default: the table beside this module). The source installer
|
|
100
|
+
* runs the generated port from `scripts/`, so its own root has to be passed in — a module-relative
|
|
101
|
+
* read would look for `scripts/../row-overrides.json` and fail.
|
|
102
|
+
* @param path - the table to read.
|
|
103
|
+
* @returns the entries, in file order. A malformed table fails LOUD: a composer
|
|
104
|
+
* that silently skipped an entry would ship a preset running the platform
|
|
105
|
+
* default while nothing downstream reported it.
|
|
106
|
+
*/
|
|
107
|
+
function loadRowOverrides(path = rowOverridesPath()) {
|
|
108
|
+
const cached = overridesCache.get(path);
|
|
109
|
+
if (cached !== void 0) return cached;
|
|
110
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
111
|
+
if (!Array.isArray(parsed)) throw new Error("evolution-core: row-overrides.json must be an array of override entries");
|
|
112
|
+
const entries = [];
|
|
113
|
+
for (const [index, entry] of parsed.entries()) {
|
|
114
|
+
const candidate = entry;
|
|
115
|
+
if (candidate === null || typeof candidate !== "object" || typeof candidate.row !== "string" || candidate.row === "" || typeof candidate.key !== "string" || candidate.key === "" || !Array.isArray(candidate.lines) || candidate.lines.some((line) => typeof line !== "string") || typeof candidate.missingReason !== "string") throw new Error(`evolution-core: row-overrides.json entry ${index} is malformed (row/key/lines/missingReason are required)`);
|
|
116
|
+
entries.push({
|
|
117
|
+
row: candidate.row,
|
|
118
|
+
key: candidate.key,
|
|
119
|
+
lines: candidate.lines,
|
|
120
|
+
missingReason: candidate.missingReason
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
overridesCache.set(path, entries);
|
|
124
|
+
return entries;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Ensure every {@link RowOverride} inside its target row.
|
|
128
|
+
*
|
|
129
|
+
* Key-level by construction: we INSERT the override's own lines and leave a row
|
|
130
|
+
* that already carries the key byte-identical, so a re-run never doubles a key.
|
|
131
|
+
* This is the deliberate opposite of the platform's patch layers, where an
|
|
132
|
+
* override REPLACES `config` wholesale — the difference is why the composer can
|
|
133
|
+
* add a key without erasing the platform's own config defaults.
|
|
134
|
+
*/
|
|
135
|
+
function applyRowOverrides(composition, overrides = loadRowOverrides()) {
|
|
136
|
+
let lines = composition.split("\n");
|
|
137
|
+
for (const override of overrides) lines = applyOneOverride(lines, override);
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
function applyOneOverride(lines, override) {
|
|
141
|
+
const rowRe = new RegExp("^- id:\\s*" + override.row.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$");
|
|
142
|
+
let found = false;
|
|
143
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
144
|
+
if (!rowRe.test(lines[i] ?? "")) continue;
|
|
145
|
+
found = true;
|
|
146
|
+
let end = i;
|
|
147
|
+
let hasConfig = false;
|
|
148
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
149
|
+
const next = lines[j] ?? "";
|
|
150
|
+
if (next.trim() === "") break;
|
|
151
|
+
if (!/^\s/.test(next)) break;
|
|
152
|
+
if (new RegExp("^ {2}" + override.key + ":(\\s|$)").test(next)) hasConfig = true;
|
|
153
|
+
end = j;
|
|
154
|
+
}
|
|
155
|
+
if (hasConfig) continue;
|
|
156
|
+
lines.splice(end + 1, 0, ...override.lines);
|
|
157
|
+
i = end + override.lines.length;
|
|
158
|
+
}
|
|
159
|
+
if (!found) console.warn("evolution preset composition: warning — " + override.missingReason);
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Row ids of a composition fragment (line scan, no YAML library).
|
|
164
|
+
*
|
|
165
|
+
* PLAN S5.9 (2026-09-16, audit P2-27): the id extraction accepts INDENTED
|
|
166
|
+
* `- id:` rows too, so a collision hidden in a nested group is still caught —
|
|
167
|
+
* the old `^- id:` anchored at column 0 and was blind to exactly the rows an
|
|
168
|
+
* upstream group nesting would produce. Twin of `rowIds` in
|
|
169
|
+
* `scripts/install-layered.mjs` (installer.spec pins detection parity).
|
|
170
|
+
* Boundary (current, deliberate): DETECTION covers nested rows, while the
|
|
171
|
+
* override INJECTION anchors (`applyOneOverride` below) still match top-level
|
|
172
|
+
* rows only — the injection indent contract (`^ {2}key:`) is defined against a
|
|
173
|
+
* column-0 row.
|
|
174
|
+
*/
|
|
175
|
+
function compositionRowIds(composition) {
|
|
176
|
+
const ids = /* @__PURE__ */ new Set();
|
|
177
|
+
for (const line of composition.split("\n")) {
|
|
178
|
+
const id = /^\s*- id:\s*(\S+)/.exec(line)?.[1];
|
|
179
|
+
if (id) ids.add(id);
|
|
180
|
+
}
|
|
181
|
+
return ids;
|
|
182
|
+
}
|
|
183
|
+
/** The platform package a DECLARATIVE preset row names (0.2.x replaced preset directories with it). */
|
|
184
|
+
const AGENT_PRESET_PACKAGE = scopedPackageName("dsh-agent-preset", import.meta.url);
|
|
185
|
+
/**
|
|
186
|
+
* Compose the DECLARATIVE preset row from the platform base composition and the family delta.
|
|
187
|
+
*
|
|
188
|
+
* 0.2.x removed the directory mechanism: `dsh-agent-preset` is an ordinary composition row whose
|
|
189
|
+
* `config.plugins` carries the preset's complete child plugin list
|
|
190
|
+
* (`packages/preset/agent-preset/src/index.ts`, `agent-preset-registry/src/definition.ts` — the
|
|
191
|
+
* definition has NO base/extends field, which is why the platform base is merged in here).
|
|
192
|
+
*
|
|
193
|
+
* The composition itself is {@link composePresetComposition}: same row-collision contract and the
|
|
194
|
+
* same `row-overrides.json` injection, applied while the composed rows are still at column 0 — the
|
|
195
|
+
* override anchors are defined against a column-0 `- id:` row, so the indentation that puts them
|
|
196
|
+
* under `plugins:` must come last.
|
|
197
|
+
* @param baseComposition - the platform base preset patch/composition rows.
|
|
198
|
+
* @param deltaComposition - the family delta rows.
|
|
199
|
+
* @param identity - row id, preset id, display fields and order.
|
|
200
|
+
* @returns the row text, newline-terminated, ready to merge into a profile patch.
|
|
201
|
+
*/
|
|
202
|
+
/**
|
|
203
|
+
* Drop the blank lines that merely separate block-sequence items, keeping the ones INSIDE a block
|
|
204
|
+
* literal (`key: |` / `>`): there a blank line is part of the value, and filtering it flattened a
|
|
205
|
+
* platform base preset's multi-paragraph prompt into one paragraph (G6 review, P1).
|
|
206
|
+
* @param text - the composition rows.
|
|
207
|
+
* @returns the rows with the separating blank lines removed.
|
|
208
|
+
*/
|
|
209
|
+
function denseBlockSequence(text) {
|
|
210
|
+
const out = [];
|
|
211
|
+
let scalarIndent = -1;
|
|
212
|
+
for (const line of text.split("\n")) {
|
|
213
|
+
const trimmed = line.trim();
|
|
214
|
+
const indent = line.length - line.trimStart().length;
|
|
215
|
+
if (scalarIndent >= 0 && trimmed !== "" && indent <= scalarIndent) scalarIndent = -1;
|
|
216
|
+
if (scalarIndent < 0) {
|
|
217
|
+
const match = /^(\s*)[\w".-]+:\s*[|>]/.exec(line);
|
|
218
|
+
if (match !== null) scalarIndent = (match[1] ?? "").length;
|
|
219
|
+
}
|
|
220
|
+
if (trimmed === "" && scalarIndent < 0) continue;
|
|
221
|
+
out.push(line);
|
|
222
|
+
}
|
|
223
|
+
return out.join("\n");
|
|
224
|
+
}
|
|
225
|
+
function composePresetRow(baseComposition, deltaComposition, identity) {
|
|
226
|
+
const plugins = denseBlockSequence(composePresetComposition(baseComposition, deltaComposition)).split("\n").map((line) => line === "" ? "" : " " + line).join("\n");
|
|
227
|
+
return [
|
|
228
|
+
"- id: " + identity.rowId,
|
|
229
|
+
" name: '" + AGENT_PRESET_PACKAGE + "'",
|
|
230
|
+
" config:",
|
|
231
|
+
" id: " + identity.id,
|
|
232
|
+
" name: " + JSON.stringify(identity.name),
|
|
233
|
+
" description: " + JSON.stringify(identity.description),
|
|
234
|
+
" order: " + String(identity.order),
|
|
235
|
+
" plugins:",
|
|
236
|
+
plugins,
|
|
237
|
+
""
|
|
238
|
+
].join("\n");
|
|
239
|
+
}
|
|
240
|
+
/** The patch-entry key that ADDS a row: a plain entry overrides the row with the same id, so a
|
|
241
|
+
* preset the platform does not ship has to arrive inside an `insert` entry
|
|
242
|
+
* (the platform's own `user-patches.spec.ts` proves the override-vs-insert split). */
|
|
243
|
+
const PATCH_INSERT_KEY = "insert";
|
|
244
|
+
/**
|
|
245
|
+
* Wrap a composed row as the patch entry a profile patch needs.
|
|
246
|
+
*
|
|
247
|
+
* The row {@link composePresetRow} returns starts at column 0; a patch entry's list nests it four
|
|
248
|
+
* spaces deeper. Wrapping is a separate step because the same row text is also what
|
|
249
|
+
* {@link mergePresetRow} matches by id, and because the composer's override anchors are defined
|
|
250
|
+
* against column-0 rows.
|
|
251
|
+
* @param row - the row text, as {@link composePresetRow} returns it.
|
|
252
|
+
* @returns the patch entry text, newline-terminated.
|
|
253
|
+
*/
|
|
254
|
+
function composePresetInsert(row) {
|
|
255
|
+
return `- ${PATCH_INSERT_KEY}:\n${row.replace(/\s+$/, "").split("\n").map((line) => " " + line).join("\n")}\n`;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The row list a platform base preset patch carries under `config.plugins`, dedented to column 0.
|
|
259
|
+
*
|
|
260
|
+
* A base preset arrives as a whole patch entry — `- insert:` / `- id: preset-standard` /
|
|
261
|
+
* `config:` / `plugins:` — because that is how the platform ships it
|
|
262
|
+
* (`packages/bundle/web-app/presets/standard.patch.yml`). The preset's own list is the
|
|
263
|
+
* SHALLOWEST `plugins:` line: a child row may carry a key by that name far deeper.
|
|
264
|
+
* @param patchText - the base preset patch file's text.
|
|
265
|
+
* @returns the plugin rows, dedented and newline-terminated.
|
|
266
|
+
* @throws {Error} when the patch carries no `plugins:` list.
|
|
267
|
+
*/
|
|
268
|
+
function basePresetPlugins(patchText) {
|
|
269
|
+
const lines = patchText.split("\n");
|
|
270
|
+
let at = -1;
|
|
271
|
+
let indent = Number.POSITIVE_INFINITY;
|
|
272
|
+
for (const [index, line] of lines.entries()) {
|
|
273
|
+
const width = /^(\s*)plugins:\s*$/.exec(line)?.[1]?.length;
|
|
274
|
+
if (width === void 0) continue;
|
|
275
|
+
if (width < indent) {
|
|
276
|
+
indent = width;
|
|
277
|
+
at = index;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (at < 0) throw new Error("evolution preset composition: the base preset patch carries no `plugins:` list");
|
|
281
|
+
const block = [];
|
|
282
|
+
for (const line of lines.slice(at + 1)) {
|
|
283
|
+
if (line.trim() === "") {
|
|
284
|
+
block.push(line);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (line.length - line.trimStart().length <= indent) break;
|
|
288
|
+
block.push(line);
|
|
289
|
+
}
|
|
290
|
+
while (block.length > 0 && block[block.length - 1]?.trim() === "") block.pop();
|
|
291
|
+
const widths = block.filter((line) => line.trim() !== "").map((line) => line.length - line.trimStart().length);
|
|
292
|
+
const dedent = widths.length === 0 ? 0 : Math.min(...widths);
|
|
293
|
+
return block.map((line) => line.slice(0, dedent).trim() === "" ? line.slice(dedent) : line).join("\n") + "\n";
|
|
294
|
+
}
|
|
295
|
+
/** The row id a family preset occupies: the platform ships \`preset-standard\` for id `standard`
|
|
296
|
+
* (`packages/bundle/web-app/presets/standard.patch.yml`), so a family preset id mirrors it. */
|
|
297
|
+
function presetRowId(id) {
|
|
298
|
+
return `preset-${id}`;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* The patch entry a profile patch receives for one preset: base rows plus the family delta, wrapped
|
|
302
|
+
* as \`- insert:\`.
|
|
303
|
+
*
|
|
304
|
+
* The two halves are also exposed on their own — {@link composePresetRow} for the row, so
|
|
305
|
+
* {@link mergePresetRow} can match it by id, and {@link basePresetPlugins} to read the platform
|
|
306
|
+
* base out of a shipped patch.
|
|
307
|
+
* @param basePatchText - the platform base preset patch's text.
|
|
308
|
+
* @param deltaComposition - the family delta rows.
|
|
309
|
+
* @param identity - row id, preset id, display fields and order.
|
|
310
|
+
* @returns the patch entry text, newline-terminated.
|
|
311
|
+
*/
|
|
312
|
+
function composePresetEntry(basePatchText, deltaComposition, identity) {
|
|
313
|
+
return composePresetInsert(composePresetRow(basePresetPlugins(basePatchText), deltaComposition, identity));
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The text of one row's own patch item, or null when the patch carries no such row.
|
|
317
|
+
*
|
|
318
|
+
* The comparison unit for a freshness check: this is byte-for-byte what {@link composePresetEntry}
|
|
319
|
+
* writes for the same row id, whether the row sits inside an \`- insert:\` entry (installer-written)
|
|
320
|
+
* or at column 0 (saved by the Web editor).
|
|
321
|
+
* @param patchText - the patch text.
|
|
322
|
+
* @param rowId - the row id to read.
|
|
323
|
+
* @returns the item's text, newline-terminated, or null.
|
|
324
|
+
*/
|
|
325
|
+
function presetRowBody(block) {
|
|
326
|
+
const lines = block.split("\n");
|
|
327
|
+
if (!/^- insert:\s*$/.test(lines[0] ?? "")) return block;
|
|
328
|
+
return lines.slice(1).map((line) => line.startsWith(" ") ? line.slice(4) : line).join("\n");
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The row BODY of a patch block, whichever form it is in.
|
|
332
|
+
*
|
|
333
|
+
* {@link presetRowBlock} returns the enclosing item verbatim: an installer-written row sits inside
|
|
334
|
+
* `- insert:` while a row the platform's Web editor saved IS that column-0 item. A comparison must
|
|
335
|
+
* compare like with like — comparing the raw block against `composePresetEntry()` (which always
|
|
336
|
+
* starts with `- insert:`) reported DIFFERS for every editor-saved row, forever (G6 review, P2), and
|
|
337
|
+
* the remedy it printed (`re-run the installer`) was refused by the `exists; use --force` guard.
|
|
338
|
+
* @param block - a block as {@link presetRowBlock} returns it.
|
|
339
|
+
* @returns the row text, `- insert:` unwrapped and its four-space indent stripped.
|
|
340
|
+
*/
|
|
341
|
+
function presetRowBlock(patchText, rowId) {
|
|
342
|
+
const lines = patchText.split("\n");
|
|
343
|
+
const span = rowBlockSpan(lines, rowId);
|
|
344
|
+
if (span === null) return null;
|
|
345
|
+
const block = [...lines.slice(span.start, span.end)];
|
|
346
|
+
while (block.length > 0 && (block[block.length - 1] ?? "").trim() === "") block.pop();
|
|
347
|
+
return block.join("\n") + "\n";
|
|
348
|
+
}
|
|
349
|
+
/** The package every shipped base preset patch declares its row with
|
|
350
|
+
* (`packages/bundle/web-app/presets/standard.patch.yml`). The platform's only bundle carrying
|
|
351
|
+
* preset patches today; a bundle a caller mounts is probed beside it. */
|
|
352
|
+
const PLATFORM_BUNDLE_PACKAGE = "@deepseek-ai/dsh-web-app";
|
|
353
|
+
/**
|
|
354
|
+
* What `DSH_AGENT_PRESET_ROOT` must name: a directory holding the platform base preset patches
|
|
355
|
+
* under their shipped file names (`<base>.patch.yml`) — e.g. a copy of a same-version platform
|
|
356
|
+
* tree's `packages/bundle/web-app/presets/`. The desktop application is the shape that needs it:
|
|
357
|
+
* its platform packages live inside `resources/app.asar`, so there is no plain
|
|
358
|
+
* `node_modules/@deepseek-ai/dsh-web-app/presets/` on disk for a source installer to read.
|
|
359
|
+
*/
|
|
360
|
+
const AGENT_PRESET_ROOT_HINT = "point DSH_AGENT_PRESET_ROOT at a directory holding <base>.patch.yml (copy it out of a same-version dsh tree: packages/bundle/web-app/presets/)";
|
|
361
|
+
/**
|
|
362
|
+
* Resolve the platform patch file one `--base` names.
|
|
363
|
+
*
|
|
364
|
+
* 0.2.x ships each base preset as a bundle patch layer
|
|
365
|
+
* (`packages/bundle/web-app/presets/<base>.patch.yml`, declared in that package's
|
|
366
|
+
* `dsh.bundle.patch`), so the FILE — not a preset directory — is the base a family row is composed
|
|
367
|
+
* from. Candidates, in order: an explicit `DSH_AGENT_PRESET_ROOT`; a module-graph probe when the
|
|
368
|
+
* caller supplies one (a desktop/Electron host resolves the bundle inside `app.asar`, where no file
|
|
369
|
+
* walk can reach); the target profile's own `node_modules`; then, walking up from the caller,
|
|
370
|
+
* every `presets/` directory of a checkout's bundle packages, then every
|
|
371
|
+
* `@deepseek-ai/dsh-<name>/presets/` directory of an installed tree.
|
|
372
|
+
*
|
|
373
|
+
* A base with no patch file is REFUSED with every probed candidate listed: composing a family preset
|
|
374
|
+
* from another base's rows would mount rows the user did not ask for, with nothing saying so.
|
|
375
|
+
* @param base - the base name, already validated against `evolution-agent/bases.json`.
|
|
376
|
+
* @param sources - the roots to probe.
|
|
377
|
+
* @returns the patch file's absolute path.
|
|
378
|
+
* @throws {Error} when no candidate exists.
|
|
379
|
+
*/
|
|
380
|
+
function resolveBasePresetPatch(base, sources) {
|
|
381
|
+
const file = `${base}.patch.yml`;
|
|
382
|
+
const explicit = sources.root?.trim();
|
|
383
|
+
if (explicit !== void 0 && explicit !== "") {
|
|
384
|
+
const path = join(explicit, file);
|
|
385
|
+
if (!existsSync(path)) throw new Error(`evolution preset composition: DSH_AGENT_PRESET_ROOT is set but ${path} does not exist — ${AGENT_PRESET_ROOT_HINT}`);
|
|
386
|
+
return path;
|
|
387
|
+
}
|
|
388
|
+
const probes = [];
|
|
389
|
+
const probe = (path) => {
|
|
390
|
+
probes.push(path);
|
|
391
|
+
return existsSync(path) ? path : void 0;
|
|
392
|
+
};
|
|
393
|
+
if (sources.resolve !== void 0) {
|
|
394
|
+
const probeSpecifier = `${PLATFORM_BUNDLE_PACKAGE}/presets/${file}`;
|
|
395
|
+
try {
|
|
396
|
+
const found = probe(sources.resolve(probeSpecifier));
|
|
397
|
+
if (found !== void 0) return found;
|
|
398
|
+
} catch {
|
|
399
|
+
probes.push(`node module graph: ${probeSpecifier}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (sources.profileDir !== void 0 && sources.profileDir !== "") for (const name of new Set([PLATFORM_BUNDLE_PACKAGE, ...sources.bundles ?? []])) {
|
|
403
|
+
const found = probe(join(sources.profileDir, "node_modules", ...name.split("/"), "presets", file));
|
|
404
|
+
if (found !== void 0) return found;
|
|
405
|
+
}
|
|
406
|
+
for (let level = sources.fromDir; level !== dirname(level); level = dirname(level)) {
|
|
407
|
+
const bundleRoot = join(level, "packages", "bundle");
|
|
408
|
+
for (const name of listDirectories(bundleRoot)) {
|
|
409
|
+
const found = probe(join(bundleRoot, name, "presets", file));
|
|
410
|
+
if (found !== void 0) return found;
|
|
411
|
+
}
|
|
412
|
+
const scopeRoot = join(level, "node_modules", "@deepseek-ai");
|
|
413
|
+
for (const name of listDirectories(scopeRoot)) {
|
|
414
|
+
if (!name.startsWith("dsh-")) continue;
|
|
415
|
+
const found = probe(join(scopeRoot, name, "presets", file));
|
|
416
|
+
if (found !== void 0) return found;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
for (const root of installRoots(sources.fromDir)) for (const shape of DESKTOP_RESOURCE_SHAPES) {
|
|
420
|
+
const found = probe(join(root, ...shape, "dsh", "node_modules", "@deepseek-ai", "dsh-web-app", "presets", file));
|
|
421
|
+
if (found !== void 0) return found;
|
|
422
|
+
const scanned = probeDesktopScope(join(root, ...shape), file, probes);
|
|
423
|
+
if (scanned !== void 0) return scanned;
|
|
424
|
+
}
|
|
425
|
+
throw new Error(`evolution preset composition: cannot find the runtime platform '${base}' preset patch — no candidate carries presets/${file}. A base with no runtime patch is refused rather than composed from another base: the generated preset must follow the platform it runs on. Probed (in order): ${probes.join(", ")}. On a desktop install the platform packages live inside resources/app.asar and no plain file exists — ${AGENT_PRESET_ROOT_HINT}`);
|
|
426
|
+
}
|
|
427
|
+
/** The Electron resource layouts a desktop installation exposes its unpacked packages under. */
|
|
428
|
+
const DESKTOP_RESOURCE_SHAPES = [["resources", "app.asar.unpacked"], [
|
|
429
|
+
"resources",
|
|
430
|
+
"app",
|
|
431
|
+
"resources",
|
|
432
|
+
"app.asar.unpacked"
|
|
433
|
+
]];
|
|
434
|
+
/**
|
|
435
|
+
* Installation directories a desktop/Electron tree may sit at, probed before the source walk.
|
|
436
|
+
*
|
|
437
|
+
* The default Windows desktop location plus every ancestor that carries an
|
|
438
|
+
* `resources/app.asar.unpacked` directory (a script run from inside an installation).
|
|
439
|
+
* @param fromDir - the caller's own directory.
|
|
440
|
+
* @returns the candidate installation roots.
|
|
441
|
+
*/
|
|
442
|
+
function installRoots(fromDir) {
|
|
443
|
+
const roots = [];
|
|
444
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
445
|
+
if (process.platform === "win32" && localAppData !== void 0 && localAppData !== "") roots.push(join(localAppData, "Programs", "DeepSeek Harness"));
|
|
446
|
+
for (let level = fromDir; level !== dirname(level); level = dirname(level)) if (existsSync(join(level, "resources", "app.asar.unpacked"))) roots.push(level);
|
|
447
|
+
return roots;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Probe every `@deepseek-ai/dsh-*` package under one unpacked resource root.
|
|
451
|
+
* @param resourceRoot - e.g. `<install>/resources/app.asar.unpacked`.
|
|
452
|
+
* @param file - the patch file name to look for.
|
|
453
|
+
* @param probes - the caller's probe log, appended to for the failure report.
|
|
454
|
+
* @returns the patch path when one exists.
|
|
455
|
+
*/
|
|
456
|
+
function probeDesktopScope(resourceRoot, file, probes) {
|
|
457
|
+
const scopeRoot = join(resourceRoot, "dsh", "node_modules", "@deepseek-ai");
|
|
458
|
+
for (const name of listDirectories(scopeRoot)) {
|
|
459
|
+
if (!name.startsWith("dsh-")) continue;
|
|
460
|
+
const path = join(scopeRoot, name, "presets", file);
|
|
461
|
+
probes.push(path);
|
|
462
|
+
if (existsSync(path)) return path;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Subdirectories of one directory, or none when it does not exist: a missing level is an ordinary
|
|
467
|
+
* miss on a candidate walk, not a failure.
|
|
468
|
+
* @param dir - the directory to list.
|
|
469
|
+
* @returns the directory names.
|
|
470
|
+
*/
|
|
471
|
+
function listDirectories(dir) {
|
|
472
|
+
try {
|
|
473
|
+
return readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
474
|
+
} catch {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The block of one row id: the enclosing column-0 list item and the row's own first line.
|
|
480
|
+
*
|
|
481
|
+
* An installer-written row sits inside `- insert:` (four spaces deeper); a row the Web editor
|
|
482
|
+
* saved is that column-0 item itself. Both must be replaced whole, so the span reaches back to the
|
|
483
|
+
* enclosing item and forward to the next one.
|
|
484
|
+
* @param lines - the patch text's lines.
|
|
485
|
+
* @param rowId - the row id to locate.
|
|
486
|
+
* @returns the span, or null when the patch carries no such row.
|
|
487
|
+
*/
|
|
488
|
+
function rowBlockSpan(lines, rowId) {
|
|
489
|
+
const rowRe = new RegExp("^\\s*- id:\\s*" + rowId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*$");
|
|
490
|
+
const rowStart = lines.findIndex((line) => rowRe.test(line));
|
|
491
|
+
if (rowStart < 0) return null;
|
|
492
|
+
let start = rowStart;
|
|
493
|
+
while (start > 0 && !/^- /.test(lines[start] ?? "")) start -= 1;
|
|
494
|
+
let end = lines.length;
|
|
495
|
+
for (let index = start + 1; index < lines.length; index += 1) if (/^- /.test(lines[index] ?? "")) {
|
|
496
|
+
end = index;
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
return {
|
|
500
|
+
start,
|
|
501
|
+
end,
|
|
502
|
+
rowStart
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Remove one row from a patch, taking its `- insert:` item with it when that item holds nothing
|
|
507
|
+
* else (an empty insert entry is left-over structure the platform would keep parsing).
|
|
508
|
+
* @param patchText - the patch text to edit.
|
|
509
|
+
* @param rowId - the row id to remove.
|
|
510
|
+
* @returns the patch text, newline-terminated; byte-identical when the id is absent.
|
|
511
|
+
*/
|
|
512
|
+
function removePresetRow(patchText, rowId) {
|
|
513
|
+
const lines = patchText.split("\n");
|
|
514
|
+
const span = rowBlockSpan(lines, rowId);
|
|
515
|
+
if (span === null) return patchText;
|
|
516
|
+
const rowLine = lines[span.rowStart] ?? "";
|
|
517
|
+
const rowIndent = rowLine.length - rowLine.trimStart().length;
|
|
518
|
+
const nested = rowIndent > 0;
|
|
519
|
+
const siblings = lines.slice(span.start + 1, span.end).filter((line) => {
|
|
520
|
+
return line.length - line.trimStart().length === rowIndent && line.trimStart().startsWith("- ");
|
|
521
|
+
}).length;
|
|
522
|
+
const cut = nested && siblings > 1 ? {
|
|
523
|
+
start: span.rowStart,
|
|
524
|
+
end: rowSiblingEnd(lines, span.rowStart, span.end)
|
|
525
|
+
} : span;
|
|
526
|
+
const kept = [...lines.slice(0, cut.start), ...lines.slice(cut.end)];
|
|
527
|
+
while (kept.length > 1 && (kept[kept.length - 1] ?? "") === "") kept.pop();
|
|
528
|
+
const text = kept.join("\n");
|
|
529
|
+
if (text === "") return "";
|
|
530
|
+
return text.endsWith("\n") ? text : text + "\n";
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* The end of one nested row's own block inside a patch entry.
|
|
534
|
+
* @param lines - the patch text's lines.
|
|
535
|
+
* @param rowStart - the row's `- id:` line.
|
|
536
|
+
* @param itemEnd - the enclosing item's end.
|
|
537
|
+
* @returns the index of the next sibling row, or the item's end.
|
|
538
|
+
*/
|
|
539
|
+
function rowSiblingEnd(lines, rowStart, itemEnd) {
|
|
540
|
+
const indent = (lines[rowStart] ?? "").length - (lines[rowStart] ?? "").trimStart().length;
|
|
541
|
+
for (let index = rowStart + 1; index < itemEnd; index += 1) {
|
|
542
|
+
const line = lines[index] ?? "";
|
|
543
|
+
if (line.trim() === "") continue;
|
|
544
|
+
const width = line.length - line.trimStart().length;
|
|
545
|
+
if (width === indent && line.trimStart().startsWith("- ")) return index;
|
|
546
|
+
if (width < indent) return index;
|
|
547
|
+
}
|
|
548
|
+
return itemEnd;
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Merge one generated row into a composition (a profile patch) by row id.
|
|
552
|
+
*
|
|
553
|
+
* Idempotent by construction: an existing row with the same `- id:` is REPLACED whole — its block
|
|
554
|
+
* is the enclosing column-0 list item, so an installer-written row inside `- insert:` and a
|
|
555
|
+
* Web-editor-saved row at column 0 are both matched by {@link rowBlockSpan} — and anything else in
|
|
556
|
+
* the patch stays byte-identical, because the patch usually carries rows this installer knows
|
|
557
|
+
* nothing about. A missing row is appended.
|
|
558
|
+
*
|
|
559
|
+
* The platform seeds a fresh profile patch with the empty list `[]`
|
|
560
|
+
* (`packages/boot/app-boot/src/profile.ts`, `PROFILE_PATCH_FILENAME`), which is a COMPLETE YAML
|
|
561
|
+
* document: a block sequence appended after it is not parsable. That seed is therefore replaced, not
|
|
562
|
+
* extended.
|
|
563
|
+
* @param patchText - the current patch text (`''` for a fresh one).
|
|
564
|
+
* @param row - the row text, as {@link composePresetRow} returns it.
|
|
565
|
+
* @param rowId - the row id to replace or append.
|
|
566
|
+
* @returns the merged patch text, newline-terminated.
|
|
567
|
+
*/
|
|
568
|
+
function mergePresetRow(patchText, row, rowId) {
|
|
569
|
+
const lines = patchText.split("\n");
|
|
570
|
+
const block = row.replace(/\s+$/, "").split("\n");
|
|
571
|
+
const span = rowBlockSpan(lines, rowId);
|
|
572
|
+
if (span === null) {
|
|
573
|
+
const seedIndex = lines.findIndex((line) => EMPTY_PATCH_SEED_RE.test(line));
|
|
574
|
+
const kept = trimTrailingBlankLines(seedIndex >= 0 ? [...lines.slice(0, seedIndex), ...lines.slice(seedIndex + 1)] : [...lines]);
|
|
575
|
+
return (kept.length === 0 ? block : [...kept, ...block]).join("\n") + "\n";
|
|
576
|
+
}
|
|
577
|
+
return trimTrailingBlankLines([
|
|
578
|
+
...lines.slice(0, span.start),
|
|
579
|
+
...block,
|
|
580
|
+
...lines.slice(span.end)
|
|
581
|
+
]).join("\n") + "\n";
|
|
582
|
+
}
|
|
583
|
+
/** The empty patch list the platform seeds a profile patch with; see {@link mergePresetRow}. */
|
|
584
|
+
const EMPTY_PATCH_SEED = "[]";
|
|
585
|
+
/** The seed in any spelling the platform accepts (`[]`, `[ ]`, `[] # empty`). */
|
|
586
|
+
const EMPTY_PATCH_SEED_RE = /^\s*\[\s*\](\s*#.*)?\s*$/;
|
|
587
|
+
/** Drop trailing blank lines only: a kept line's own terminator (a CRLF file's `\r`) is never touched. */
|
|
588
|
+
function trimTrailingBlankLines(lines) {
|
|
589
|
+
const out = [...lines];
|
|
590
|
+
while (out.length > 0 && (out[out.length - 1] ?? "").trim() === "") out.pop();
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* The text to write when a patch is left with no entries.
|
|
595
|
+
*
|
|
596
|
+
* A zero-byte patch file is not what the platform writes or parses; its own seed is the empty list.
|
|
597
|
+
* @param patchText - the text {@link removePresetRow} returned.
|
|
598
|
+
* @returns the platform's empty-list seed when nothing is left, the text otherwise.
|
|
599
|
+
*/
|
|
600
|
+
function presetPatchText(patchText) {
|
|
601
|
+
return patchText.trim() === "" ? `[]\n` : patchText;
|
|
602
|
+
}
|
|
603
|
+
//#endregion
|
|
604
|
+
export { trimTrailingBlankLines as C, rowSiblingEnd as S, presetRowId as _, composePresetComposition as a, resolveBasePresetPatch as b, composePresetRow as c, listDirectories as d, loadRowOverrides as f, presetRowBody as g, presetRowBlock as h, basePresetPlugins as i, compositionRowIds as l, presetPatchText as m, AGENT_PRESET_ROOT_HINT as n, composePresetEntry as o, mergePresetRow as p, EMPTY_PATCH_SEED as r, composePresetInsert as s, AGENT_PRESET_PACKAGE as t, denseBlockSequence as u, probeDesktopScope as v, allowRowCollisions as w, rowBlockSpan as x, removePresetRow as y };
|