@deepseek-ai/dsh-app-boot 0.2.1-rc.5 → 0.2.2-alpha.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +8 -8
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/lib/index.js +1002 -253
- package/lib/types/bundle-platform.d.ts +17 -0
- package/lib/types/index.d.ts +7 -3
- package/lib/types/profile-plugins.d.ts +2 -0
- package/lib/types/profile.d.ts +4 -21
- package/lib/types/startup-diagnostics.d.ts +38 -0
- package/lib/worker/profile-resolution-bootstrap.js +5 -5
- package/package.json +19 -18
- package/lib/types/official-bundle-packages.d.ts +0 -4
- package/lib/types/official-bundles.d.ts +0 -12
- package/lib/types/official-bundles.generated.d.ts +0 -5
package/lib/index.js
CHANGED
|
@@ -2,65 +2,509 @@ import { createRequire, isBuiltin } from "node:module";
|
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import fs, { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { inspect, parseEnv } from "node:util";
|
|
5
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
5
6
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
6
7
|
import * as yaml from "js-yaml";
|
|
7
8
|
import { load } from "js-yaml";
|
|
8
|
-
import { Context, Service } from "@deepseek-ai/cordis";
|
|
9
9
|
import Loader, { EntryGroup, EntryTree, ModuleLoader, isJsExpr } from "@deepseek-ai/cordis-plugin-loader";
|
|
10
|
-
import { access, constants, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
|
|
10
|
+
import { access, constants, mkdir, readFile, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
11
11
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
12
12
|
import Group from "@deepseek-ai/cordis-plugin-group";
|
|
13
13
|
import { dshHomePath, resolveDshHome } from "@deepseek-ai/dsh-home-paths";
|
|
14
14
|
import { createLaunchEnvironmentSnapshot } from "@deepseek-ai/dsh-launch-environment";
|
|
15
|
+
import { INSTALLATION_OWNED_PROFILE_TUPLES, PROFILE_TEMPLATES, RETIRED_BUNDLES } from "@deepseek-ai/dsh-official-bundles";
|
|
15
16
|
import semver, { parse } from "semver";
|
|
16
17
|
import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
17
18
|
import { getEnvironmentData, setEnvironmentData } from "node:worker_threads";
|
|
18
19
|
import { imports } from "resolve.exports";
|
|
19
|
-
//#region
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
//#region lib/types/startup-diagnostics.js
|
|
21
|
+
/** Configuration-free snapshots captured before a failed startup disposes its plugins. */
|
|
22
|
+
function stateName(state) {
|
|
23
|
+
return {
|
|
24
|
+
0: "pending",
|
|
25
|
+
1: "loading",
|
|
26
|
+
2: "active",
|
|
27
|
+
3: "failed",
|
|
28
|
+
4: "disposed",
|
|
29
|
+
5: "unloading"
|
|
30
|
+
}[state];
|
|
31
|
+
}
|
|
32
|
+
function pluginState(ctx, entry) {
|
|
33
|
+
let state;
|
|
34
|
+
try {
|
|
35
|
+
state = entry.disabled ? "disabled" : entry.fiber === void 0 ? "failed" : stateName(entry.fiber.state);
|
|
36
|
+
} catch (_error) {
|
|
37
|
+
state = "failed";
|
|
38
|
+
}
|
|
39
|
+
let version = "unknown";
|
|
40
|
+
const source = entry.parent.tree.ctx.baseUrl;
|
|
41
|
+
try {
|
|
42
|
+
if (source !== void 0) version = ctx.get("pluginPackages")?.packageOf(entry.options.name, source)?.version ?? "unknown";
|
|
43
|
+
} catch (_error) {}
|
|
44
|
+
const fiber = entry.fiber;
|
|
45
|
+
return {
|
|
46
|
+
id: entry.id,
|
|
47
|
+
module: entry.options.name,
|
|
48
|
+
version,
|
|
49
|
+
state,
|
|
50
|
+
source: source ?? "unknown",
|
|
51
|
+
missing: state === "pending" && fiber !== void 0 ? Object.keys(fiber.inject).filter((name) => fiber.ctx.get(name) === void 0) : []
|
|
52
|
+
};
|
|
53
|
+
}
|
|
27
54
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
55
|
+
* Capture Loader state and registered service ownership before teardown.
|
|
56
|
+
* Providers that never registered cannot be inferred from their package names.
|
|
57
|
+
* @param ctx - Live application context with its Loader still mounted.
|
|
58
|
+
* @returns At most 256 plugin rows and 256 missing-service observations, with omission counts.
|
|
32
59
|
*/
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
function captureStartupComposition(ctx) {
|
|
61
|
+
const rows = [...ctx.loader.entries()].map((entry) => ({
|
|
62
|
+
entry,
|
|
63
|
+
state: pluginState(ctx, entry)
|
|
64
|
+
}));
|
|
65
|
+
rows.sort((a, b) => Number(a.state.state === "active") - Number(b.state.state === "active"));
|
|
66
|
+
const services = [];
|
|
67
|
+
let omittedServices = 0;
|
|
68
|
+
const store = ctx.reflect.store;
|
|
69
|
+
for (const { entry, state } of rows) {
|
|
70
|
+
const fiber = entry.fiber;
|
|
71
|
+
if (fiber === void 0) continue;
|
|
72
|
+
for (const service of state.missing) {
|
|
73
|
+
if (services.length === 256) {
|
|
74
|
+
omittedServices++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const scope = fiber.ctx[Context.isolate][service];
|
|
78
|
+
const providers = [];
|
|
79
|
+
for (const key of Object.getOwnPropertySymbols(store)) {
|
|
80
|
+
const impl = store[key];
|
|
81
|
+
if (impl?.name !== service) continue;
|
|
82
|
+
providers.push({
|
|
83
|
+
entry: impl.ctx.fiber.entry?.id ?? "unknown",
|
|
84
|
+
state: stateName(impl.ctx.fiber.state),
|
|
85
|
+
visibility: key === scope ? "same-scope" : "other-scope"
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
services.push({
|
|
89
|
+
consumer: state.id,
|
|
90
|
+
service,
|
|
91
|
+
providers
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
plugins: rows.slice(0, 256).map((row) => row.state),
|
|
97
|
+
services,
|
|
98
|
+
omittedPlugins: Math.max(0, rows.length - 256),
|
|
99
|
+
omittedServices
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region ../../../vendor/cosmokit/src/misc.ts
|
|
104
|
+
/** Return true when a value is `null` or `undefined`. */
|
|
105
|
+
function isNullable(value) {
|
|
106
|
+
return value === null || value === void 0;
|
|
107
|
+
}
|
|
108
|
+
/** Omit selected keys from a shallow object copy. */
|
|
109
|
+
function omit(source, keys) {
|
|
110
|
+
if (!keys) return { ...source };
|
|
111
|
+
const result = { ...source };
|
|
112
|
+
for (const key of keys) Reflect.deleteProperty(result, key);
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region ../../../vendor/cosmokit/src/volatile.ts
|
|
117
|
+
/** Shared config references used by schema validators and plugin runtimes. */
|
|
118
|
+
const write = Symbol.for("cosmokit.volatile.write");
|
|
119
|
+
/**
|
|
120
|
+
* Identify references across ESM/CJS copies of the shared library.
|
|
121
|
+
* @param value - a parsed config value.
|
|
122
|
+
* @returns whether the value implements the shared reference protocol.
|
|
123
|
+
*/
|
|
124
|
+
function isVolatile(value) {
|
|
125
|
+
return typeof value === "object" && value !== null && write in value;
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region ../../../vendor/cosmokit/src/types.ts
|
|
129
|
+
/** Test values using `instanceof` with a `toStringTag` fallback. */
|
|
130
|
+
function is(type, value) {
|
|
131
|
+
if (arguments.length === 1) return (value) => is(type, value);
|
|
132
|
+
return type in globalThis && value instanceof globalThis[type] || Object.prototype.toString.call(value).slice(8, -1) === type;
|
|
133
|
+
}
|
|
134
|
+
function isArrayBufferLike(value) {
|
|
135
|
+
return is("ArrayBuffer", value) || is("SharedArrayBuffer", value);
|
|
136
|
+
}
|
|
137
|
+
function isArrayBufferSource(value) {
|
|
138
|
+
return isArrayBufferLike(value) || ArrayBuffer.isView(value);
|
|
139
|
+
}
|
|
140
|
+
let Binary;
|
|
141
|
+
(function(_Binary) {
|
|
142
|
+
_Binary.is = isArrayBufferLike;
|
|
143
|
+
_Binary.isSource = isArrayBufferSource;
|
|
144
|
+
function fromSource(source) {
|
|
145
|
+
if (ArrayBuffer.isView(source)) return source.buffer.slice(source.byteOffset, source.byteOffset + source.byteLength);
|
|
146
|
+
else return source;
|
|
147
|
+
}
|
|
148
|
+
_Binary.fromSource = fromSource;
|
|
149
|
+
function toBase64(source) {
|
|
150
|
+
source = fromSource(source);
|
|
151
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("base64");
|
|
152
|
+
let binary = "";
|
|
153
|
+
const bytes = new Uint8Array(source);
|
|
154
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
155
|
+
return btoa(binary);
|
|
156
|
+
}
|
|
157
|
+
_Binary.toBase64 = toBase64;
|
|
158
|
+
function fromBase64(source) {
|
|
159
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "base64"));
|
|
160
|
+
return Uint8Array.from(atob(source), (c) => c.charCodeAt(0));
|
|
161
|
+
}
|
|
162
|
+
_Binary.fromBase64 = fromBase64;
|
|
163
|
+
function toHex(source) {
|
|
164
|
+
source = fromSource(source);
|
|
165
|
+
if (typeof Buffer !== "undefined") return Buffer.from(source).toString("hex");
|
|
166
|
+
return Array.from(new Uint8Array(source), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
167
|
+
}
|
|
168
|
+
_Binary.toHex = toHex;
|
|
169
|
+
function fromHex(source) {
|
|
170
|
+
if (typeof Buffer !== "undefined") return fromSource(Buffer.from(source, "hex"));
|
|
171
|
+
const hex = source.length % 2 === 0 ? source : source.slice(0, source.length - 1);
|
|
172
|
+
const buffer = [];
|
|
173
|
+
for (let i = 0; i < hex.length; i += 2) buffer.push(parseInt(`${hex[i]}${hex[i + 1]}`, 16));
|
|
174
|
+
return Uint8Array.from(buffer).buffer;
|
|
175
|
+
}
|
|
176
|
+
_Binary.fromHex = fromHex;
|
|
177
|
+
})(Binary || (Binary = {}));
|
|
178
|
+
Binary.fromBase64;
|
|
179
|
+
Binary.toBase64;
|
|
180
|
+
Binary.fromHex;
|
|
181
|
+
Binary.toHex;
|
|
182
|
+
/**
|
|
183
|
+
* Compare values recursively, treating two volatile references as equal regardless of value.
|
|
184
|
+
* Strict comparison distinguishes null/undefined, treats opaque objects by identity,
|
|
185
|
+
* compares URLs by normalized href, treats array holes as undefined, and considers distinct cyclic structures unequal.
|
|
186
|
+
* @param a - first value.
|
|
187
|
+
* @param b - second value.
|
|
188
|
+
* @param strict - whether to require strict data equality outside volatile references.
|
|
189
|
+
* @returns whether the values compare equal.
|
|
190
|
+
*/
|
|
191
|
+
function deepEqual(a, b, strict) {
|
|
192
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
193
|
+
function compare(a, b) {
|
|
194
|
+
if (a === b) return true;
|
|
195
|
+
if (isVolatile(a) || isVolatile(b)) return isVolatile(a) && isVolatile(b);
|
|
196
|
+
if (!strict && isNullable(a) && isNullable(b)) return true;
|
|
197
|
+
if (typeof a !== typeof b || typeof a !== "object" || !a || !b) return false;
|
|
198
|
+
if (ancestors.has(a)) return false;
|
|
199
|
+
function check(test, then) {
|
|
200
|
+
return test(a) ? test(b) ? then(a, b) : false : test(b) ? false : void 0;
|
|
201
|
+
}
|
|
202
|
+
ancestors.add(a);
|
|
203
|
+
try {
|
|
204
|
+
return check(Array.isArray, (a, b) => {
|
|
205
|
+
if (a.length !== b.length) return false;
|
|
206
|
+
for (let index = 0; index < a.length; index++) if (!compare(a[index], b[index])) return false;
|
|
207
|
+
return true;
|
|
208
|
+
}) ?? check(is("Date"), (a, b) => a.valueOf() === b.valueOf()) ?? check(is("URL"), (a, b) => a.href === b.href) ?? check(is("RegExp"), (a, b) => a.source === b.source && a.flags === b.flags) ?? check(isArrayBufferLike, (a, b) => {
|
|
209
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
210
|
+
const viewA = new Uint8Array(a);
|
|
211
|
+
const viewB = new Uint8Array(b);
|
|
212
|
+
for (let i = 0; i < viewA.length; i++) if (viewA[i] !== viewB[i]) return false;
|
|
213
|
+
return true;
|
|
214
|
+
}) ?? ((!strict || [a, b].every((value) => Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null)) && Object.keys({
|
|
215
|
+
...a,
|
|
216
|
+
...b
|
|
217
|
+
}).every((key) => compare(a[key], b[key])));
|
|
218
|
+
} finally {
|
|
219
|
+
ancestors.delete(a);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return compare(a, b);
|
|
223
|
+
}
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region ../../../vendor/cosmokit/src/time.ts
|
|
226
|
+
let Time;
|
|
227
|
+
(function(_Time) {
|
|
228
|
+
_Time.millisecond = 1;
|
|
229
|
+
const second = _Time.second = 1e3;
|
|
230
|
+
const minute = _Time.minute = second * 60;
|
|
231
|
+
const hour = _Time.hour = minute * 60;
|
|
232
|
+
const day = _Time.day = hour * 24;
|
|
233
|
+
const week = _Time.week = day * 7;
|
|
234
|
+
let timezoneOffset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
235
|
+
function setTimezoneOffset(offset) {
|
|
236
|
+
timezoneOffset = offset;
|
|
237
|
+
}
|
|
238
|
+
_Time.setTimezoneOffset = setTimezoneOffset;
|
|
239
|
+
function getTimezoneOffset() {
|
|
240
|
+
return timezoneOffset;
|
|
241
|
+
}
|
|
242
|
+
_Time.getTimezoneOffset = getTimezoneOffset;
|
|
243
|
+
function getDateNumber(date = /* @__PURE__ */ new Date(), offset) {
|
|
244
|
+
if (typeof date === "number") date = new Date(date);
|
|
245
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
246
|
+
return Math.floor((date.valueOf() / minute - offset) / 1440);
|
|
247
|
+
}
|
|
248
|
+
_Time.getDateNumber = getDateNumber;
|
|
249
|
+
function fromDateNumber(value, offset) {
|
|
250
|
+
const date = new Date(value * day);
|
|
251
|
+
if (offset === void 0) offset = timezoneOffset;
|
|
252
|
+
return new Date(+date + offset * minute);
|
|
253
|
+
}
|
|
254
|
+
_Time.fromDateNumber = fromDateNumber;
|
|
255
|
+
const numeric = /\d+(?:\.\d+)?/.source;
|
|
256
|
+
const timeRegExp = new RegExp(`^${[
|
|
257
|
+
"w(?:eek(?:s)?)?",
|
|
258
|
+
"d(?:ay(?:s)?)?",
|
|
259
|
+
"h(?:our(?:s)?)?",
|
|
260
|
+
"m(?:in(?:ute)?(?:s)?)?",
|
|
261
|
+
"s(?:ec(?:ond)?(?:s)?)?"
|
|
262
|
+
].map((unit) => `(${numeric}${unit})?`).join("")}$`);
|
|
263
|
+
function parseTime(source) {
|
|
264
|
+
const capture = timeRegExp.exec(source);
|
|
265
|
+
if (!capture) return 0;
|
|
266
|
+
return (parseFloat(capture[1]) * week || 0) + (parseFloat(capture[2]) * day || 0) + (parseFloat(capture[3]) * hour || 0) + (parseFloat(capture[4]) * minute || 0) + (parseFloat(capture[5]) * second || 0);
|
|
267
|
+
}
|
|
268
|
+
_Time.parseTime = parseTime;
|
|
269
|
+
function parseDate(date) {
|
|
270
|
+
const parsed = parseTime(date);
|
|
271
|
+
if (parsed) date = Date.now() + parsed;
|
|
272
|
+
else if (/^\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).toLocaleDateString()}-${date}`;
|
|
273
|
+
else if (/^\d{1,2}-\d{1,2}-\d{1,2}(:\d{1,2}){1,2}$/.test(date)) date = `${(/* @__PURE__ */ new Date()).getFullYear()}-${date}`;
|
|
274
|
+
return date ? new Date(date) : /* @__PURE__ */ new Date();
|
|
275
|
+
}
|
|
276
|
+
_Time.parseDate = parseDate;
|
|
277
|
+
function format(ms) {
|
|
278
|
+
const abs = Math.abs(ms);
|
|
279
|
+
if (abs >= day - hour / 2) return Math.round(ms / day) + "d";
|
|
280
|
+
else if (abs >= hour - minute / 2) return Math.round(ms / hour) + "h";
|
|
281
|
+
else if (abs >= minute - second / 2) return Math.round(ms / minute) + "m";
|
|
282
|
+
else if (abs >= second) return Math.round(ms / second) + "s";
|
|
283
|
+
return ms + "ms";
|
|
284
|
+
}
|
|
285
|
+
_Time.format = format;
|
|
286
|
+
function toDigits(source, length = 2) {
|
|
287
|
+
return source.toString().padStart(length, "0");
|
|
288
|
+
}
|
|
289
|
+
_Time.toDigits = toDigits;
|
|
290
|
+
function template(template, time = /* @__PURE__ */ new Date()) {
|
|
291
|
+
return template.replace("yyyy", time.getFullYear().toString()).replace("yy", time.getFullYear().toString().slice(2)).replace("MM", toDigits(time.getMonth() + 1)).replace("dd", toDigits(time.getDate())).replace("hh", toDigits(time.getHours())).replace("mm", toDigits(time.getMinutes())).replace("ss", toDigits(time.getSeconds())).replace("SSS", toDigits(time.getMilliseconds(), 3));
|
|
292
|
+
}
|
|
293
|
+
_Time.template = template;
|
|
294
|
+
})(Time || (Time = {}));
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region ../../../vendor/include/src/journal.ts
|
|
297
|
+
function diffOptions(legacy, options) {
|
|
298
|
+
const changes = {};
|
|
299
|
+
for (const key of new Set([...Object.keys(legacy), ...Object.keys(options)])) {
|
|
300
|
+
if (key === "id") continue;
|
|
301
|
+
if (deepEqual(legacy[key], options[key])) continue;
|
|
302
|
+
changes[key] = options[key];
|
|
303
|
+
}
|
|
304
|
+
return changes;
|
|
305
|
+
}
|
|
306
|
+
/** Fold a later record into an earlier one for the same entry. */
|
|
307
|
+
function mergeRecords(older, newer) {
|
|
308
|
+
if (!older) return newer;
|
|
309
|
+
if (newer.kind === "remove") return older.kind === "upsert" && older.created ? void 0 : newer;
|
|
310
|
+
if (older.kind === "remove") return {
|
|
311
|
+
...newer,
|
|
312
|
+
created: false
|
|
313
|
+
};
|
|
314
|
+
const relocated = newer.parent !== void 0;
|
|
315
|
+
return {
|
|
316
|
+
kind: "upsert",
|
|
317
|
+
created: older.created,
|
|
318
|
+
parent: relocated ? newer.parent : older.parent,
|
|
319
|
+
position: relocated ? newer.position : older.position,
|
|
320
|
+
changes: {
|
|
321
|
+
...older.changes,
|
|
322
|
+
...newer.changes
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function merge(journal, id, record) {
|
|
327
|
+
const merged = mergeRecords(journal.get(id), record);
|
|
328
|
+
if (merged) journal.set(id, merged);
|
|
329
|
+
else journal.delete(id);
|
|
330
|
+
}
|
|
331
|
+
function record$1(journal, change, parent) {
|
|
332
|
+
const { id, options, legacy } = change;
|
|
333
|
+
if (!options) return merge(journal, id, { kind: "remove" });
|
|
334
|
+
const position = change.group.data.indexOf(options);
|
|
335
|
+
if (!legacy) return merge(journal, id, {
|
|
336
|
+
kind: "upsert",
|
|
337
|
+
created: true,
|
|
338
|
+
parent,
|
|
339
|
+
position,
|
|
340
|
+
changes: omit(options, ["id"])
|
|
341
|
+
});
|
|
342
|
+
merge(journal, id, {
|
|
343
|
+
kind: "upsert",
|
|
344
|
+
created: false,
|
|
345
|
+
parent: change.from ? parent : void 0,
|
|
346
|
+
position,
|
|
347
|
+
changes: diffOptions(legacy, options)
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
/** Index every entry (including nested group members) by id. */
|
|
351
|
+
function flatten(data, parent = null, result = /* @__PURE__ */ new Map()) {
|
|
352
|
+
data.forEach((options, position) => {
|
|
353
|
+
if (options.id) result.set(options.id, {
|
|
354
|
+
parent,
|
|
355
|
+
position,
|
|
356
|
+
options,
|
|
357
|
+
list: data
|
|
358
|
+
});
|
|
359
|
+
if (options.group && Array.isArray(options.config)) flatten(options.config, options.id ?? null, result);
|
|
360
|
+
});
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
function applyChanges(options, changes, filter) {
|
|
364
|
+
for (const [key, value] of Object.entries(changes)) {
|
|
365
|
+
if (filter && !filter(key)) continue;
|
|
366
|
+
if (value === void 0) delete options[key];
|
|
367
|
+
else options[key] = value;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function detach(data, id) {
|
|
371
|
+
const flat = flatten(data).get(id);
|
|
372
|
+
if (!flat) return;
|
|
373
|
+
flat.list.splice(flat.list.indexOf(flat.options), 1);
|
|
374
|
+
return flat.options;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Insert `options` under `parent` (a group id, or `null` for the root) at
|
|
378
|
+
* `position`, clamped to the group's length. Returns false when the parent
|
|
379
|
+
* group does not exist.
|
|
380
|
+
*/
|
|
381
|
+
function place(data, options, parent, position) {
|
|
382
|
+
let list = data;
|
|
383
|
+
if (parent !== null) {
|
|
384
|
+
const group = flatten(data).get(parent);
|
|
385
|
+
if (!group?.options.group) return false;
|
|
386
|
+
list = group.options.config ??= [];
|
|
387
|
+
}
|
|
388
|
+
list.splice(Math.min(position, list.length), 0, options);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Overlay a journal onto `data` in place. `filter(id, key)` restricts which
|
|
393
|
+
* changes apply; `key` is `null` when asking about the entry as a whole
|
|
394
|
+
* (creation, removal, relocation).
|
|
395
|
+
*/
|
|
396
|
+
function applyJournal(data, journal, filter = () => true, warn = () => {}) {
|
|
397
|
+
for (const [id, record] of journal) {
|
|
398
|
+
if (!filter(id, null)) continue;
|
|
399
|
+
if (record.kind === "remove") {
|
|
400
|
+
detach(data, id);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const flat = flatten(data).get(id);
|
|
404
|
+
if (!flat) {
|
|
405
|
+
const options = { id };
|
|
406
|
+
applyChanges(options, record.changes, (key) => filter(id, key));
|
|
407
|
+
const parent = record.parent ?? null;
|
|
408
|
+
if (!place(data, options, parent, record.position)) warn("cannot place entry %C: group %C not found", id, parent);
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
applyChanges(flat.options, record.changes, (key) => filter(id, key));
|
|
412
|
+
if (record.parent !== void 0 && flat.parent !== record.parent) {
|
|
413
|
+
detach(data, id);
|
|
414
|
+
if (!place(data, flat.options, record.parent, record.position)) {
|
|
415
|
+
warn("cannot move entry %C: group %C not found", id, record.parent);
|
|
416
|
+
place(data, flat.options, flat.parent, flat.position);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return data;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Three-way reconciliation of a journal against a new file state. The file
|
|
424
|
+
* wins every conflict: conflicting keys are dropped from the journal so that
|
|
425
|
+
* the tree follows the file, and each drop is reported.
|
|
426
|
+
*
|
|
427
|
+
* `fileOwned(id, key)` scopes the check to what the file can actually express
|
|
428
|
+
* (patch-owned keys and patch-inserted entries live elsewhere).
|
|
429
|
+
*/
|
|
430
|
+
function reconcile(journal, base, theirs, fileOwned) {
|
|
431
|
+
const conflicts = [];
|
|
432
|
+
for (const [id, record] of journal) {
|
|
433
|
+
if (!fileOwned(id, null)) continue;
|
|
434
|
+
const b = base.get(id);
|
|
435
|
+
const t = theirs.get(id);
|
|
436
|
+
if (record.kind === "remove") {
|
|
437
|
+
if (!t) journal.delete(id);
|
|
438
|
+
else if (!b || !deepEqual(t.options, b.options)) {
|
|
439
|
+
conflicts.push({
|
|
440
|
+
id,
|
|
441
|
+
reason: b ? "modified in file, removed at runtime" : "added in file, removed at runtime"
|
|
442
|
+
});
|
|
443
|
+
journal.delete(id);
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
if (!t) {
|
|
448
|
+
if (b) {
|
|
449
|
+
conflicts.push({
|
|
450
|
+
id,
|
|
451
|
+
reason: "removed in file, modified at runtime"
|
|
452
|
+
});
|
|
453
|
+
journal.delete(id);
|
|
454
|
+
}
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (record.created) record.created = false;
|
|
458
|
+
for (const key of Object.keys(record.changes)) {
|
|
459
|
+
if (!fileOwned(id, key)) continue;
|
|
460
|
+
const bv = b?.options[key];
|
|
461
|
+
const tv = t.options[key];
|
|
462
|
+
if (deepEqual(bv, tv) || deepEqual(tv, record.changes[key])) continue;
|
|
463
|
+
conflicts.push({
|
|
464
|
+
id,
|
|
465
|
+
reason: `key "${key}" modified both in file and at runtime`
|
|
466
|
+
});
|
|
467
|
+
delete record.changes[key];
|
|
468
|
+
}
|
|
469
|
+
if (record.parent !== void 0 && b && t.parent !== b.parent && t.parent !== record.parent) {
|
|
470
|
+
conflicts.push({
|
|
471
|
+
id,
|
|
472
|
+
reason: "moved both in file and at runtime"
|
|
473
|
+
});
|
|
474
|
+
record.parent = void 0;
|
|
475
|
+
}
|
|
476
|
+
const moved = record.parent !== void 0 && record.parent !== t.parent;
|
|
477
|
+
if (!Object.keys(record.changes).length && !moved) journal.delete(id);
|
|
478
|
+
}
|
|
479
|
+
return conflicts;
|
|
480
|
+
}
|
|
481
|
+
//#endregion
|
|
482
|
+
//#region ../../../vendor/include/src/patch.ts
|
|
483
|
+
function randomId(used) {
|
|
484
|
+
let id;
|
|
485
|
+
do
|
|
486
|
+
id = Math.random().toString(16).slice(2, 10);
|
|
487
|
+
while (used(id));
|
|
488
|
+
return id;
|
|
489
|
+
}
|
|
490
|
+
/** Give every entry (nested groups included) an id, in place. */
|
|
491
|
+
function ensureIds(entries, used) {
|
|
492
|
+
for (const entry of entries) {
|
|
493
|
+
if (!entry.id) entry.id = randomId(used);
|
|
494
|
+
if (entry.group && Array.isArray(entry.config)) ensureIds(entry.config, used);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/** Runtime changes are routed back to an inserted entry by its id, so every insert needs one. */
|
|
498
|
+
function ensureInsertIds(patches, used) {
|
|
499
|
+
for (const patch of patches ?? []) if (patch.insert) ensureIds(patch.insert, used);
|
|
46
500
|
}
|
|
47
501
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* (`dsh --dump-config`) so a dump can never drift from what boots. The input
|
|
51
|
-
* is never mutated: patching shared entry objects would bake earlier patch
|
|
52
|
-
* values into the cached parse, so repeated application (config hot-reloads)
|
|
53
|
-
* could never revert a removed or changed patch. Inserted entries are indexed
|
|
54
|
-
* as they are added, so a later patch in the same list can target a row an
|
|
55
|
-
* earlier patch inserted. A patch that matches nothing warns and is skipped.
|
|
56
|
-
* @param data - the parsed entry list (JSON-safe plain data).
|
|
57
|
-
* @param patches - the patch list to apply, in order.
|
|
58
|
-
* @param warn - sink for skipped-patch diagnostics (printf-style, `%C` = code).
|
|
59
|
-
* @returns a detached entry list with every applicable patch applied.
|
|
502
|
+
* Overlay `patches` onto a fresh clone of `data`. Inserted entries are cloned
|
|
503
|
+
* too, so the tree owns every object it mounts.
|
|
60
504
|
*/
|
|
61
|
-
function
|
|
62
|
-
if (!patches?.length) return [...data];
|
|
505
|
+
function applyPatches(data, patches, warn) {
|
|
63
506
|
data = structuredClone(data);
|
|
507
|
+
if (!patches?.length) return data;
|
|
64
508
|
const entryMap = /* @__PURE__ */ new Map();
|
|
65
509
|
const buildMap = (entries) => {
|
|
66
510
|
for (const entry of entries) {
|
|
@@ -72,6 +516,7 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
72
516
|
for (const patch of patches) {
|
|
73
517
|
const { id, insert, name, ...overrides } = patch;
|
|
74
518
|
if (insert) {
|
|
519
|
+
const clone = structuredClone(insert);
|
|
75
520
|
if (id) {
|
|
76
521
|
const target = entryMap.get(id);
|
|
77
522
|
if (!target) {
|
|
@@ -83,9 +528,9 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
83
528
|
continue;
|
|
84
529
|
}
|
|
85
530
|
if (!Array.isArray(target.config)) target.config = [];
|
|
86
|
-
target.config.push(...
|
|
87
|
-
} else data.push(...
|
|
88
|
-
buildMap(
|
|
531
|
+
target.config.push(...clone);
|
|
532
|
+
} else data.push(...clone);
|
|
533
|
+
buildMap(clone);
|
|
89
534
|
continue;
|
|
90
535
|
}
|
|
91
536
|
if (!id) {
|
|
@@ -108,145 +553,518 @@ function applyEntryPatches(data, patches, warn) {
|
|
|
108
553
|
}
|
|
109
554
|
return data;
|
|
110
555
|
}
|
|
111
|
-
|
|
556
|
+
var PatchIndex = class {
|
|
557
|
+
patches;
|
|
558
|
+
keys = /* @__PURE__ */ new Map();
|
|
559
|
+
inserts = /* @__PURE__ */ new Map();
|
|
560
|
+
constructor(patches) {
|
|
561
|
+
this.patches = patches;
|
|
562
|
+
patches.forEach((patch, index) => {
|
|
563
|
+
const { id, insert } = patch;
|
|
564
|
+
if (insert) {
|
|
565
|
+
const walk = (list, parent) => {
|
|
566
|
+
for (const options of list) {
|
|
567
|
+
this.inserts.set(options.id, {
|
|
568
|
+
type: "insert",
|
|
569
|
+
index,
|
|
570
|
+
list,
|
|
571
|
+
options,
|
|
572
|
+
parent
|
|
573
|
+
});
|
|
574
|
+
if (options.group && Array.isArray(options.config)) walk(options.config, options.id);
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
walk(insert, id ?? null);
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if (!id) return;
|
|
581
|
+
const keys = this.keys.get(id) ?? /* @__PURE__ */ new Map();
|
|
582
|
+
for (const key of Object.keys(omit(patch, [
|
|
583
|
+
"id",
|
|
584
|
+
"insert",
|
|
585
|
+
"name"
|
|
586
|
+
]))) keys.set(key, index);
|
|
587
|
+
this.keys.set(id, keys);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
/** Owner of the entry as a whole. */
|
|
591
|
+
entry(id) {
|
|
592
|
+
if (id === null) return { type: "file" };
|
|
593
|
+
return this.inserts.get(id) ?? { type: "file" };
|
|
594
|
+
}
|
|
595
|
+
/** Owner of one key of an entry. */
|
|
596
|
+
key(id, key) {
|
|
597
|
+
const insert = this.inserts.get(id);
|
|
598
|
+
if (insert) return insert;
|
|
599
|
+
const index = this.keys.get(id)?.get(key);
|
|
600
|
+
return index === void 0 ? { type: "file" } : {
|
|
601
|
+
type: "patch",
|
|
602
|
+
index
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
fileOwned(id, key) {
|
|
606
|
+
return (key === null ? this.entry(id) : this.key(id, key)).type === "file";
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
/**
|
|
610
|
+
* Route a journal to its owners: file-owned changes are applied to `data`,
|
|
611
|
+
* patch-owned ones to `patches`. Both are mutated in place, so pass clones.
|
|
612
|
+
* Returns whether any patch changed.
|
|
613
|
+
*/
|
|
614
|
+
function routeJournal(journal, data, patches, warn) {
|
|
615
|
+
const index = new PatchIndex(patches);
|
|
616
|
+
let patched = false;
|
|
617
|
+
const insertList = (owner) => owner.options.config ??= [];
|
|
618
|
+
const placeAt = (options, parent, position) => {
|
|
619
|
+
const owner = index.entry(parent);
|
|
620
|
+
if (owner.type === "insert") {
|
|
621
|
+
const list = insertList(owner);
|
|
622
|
+
list.splice(Math.min(position, list.length), 0, options);
|
|
623
|
+
patched = true;
|
|
624
|
+
} else if (!place(data, options, parent, position)) warn("cannot place entry %C: group %C not found", options.id, parent);
|
|
625
|
+
};
|
|
626
|
+
for (const [id, record] of journal) {
|
|
627
|
+
const owner = index.entry(id);
|
|
628
|
+
if (record.kind === "remove") {
|
|
629
|
+
if (owner.type === "insert") {
|
|
630
|
+
owner.list.splice(owner.list.indexOf(owner.options), 1);
|
|
631
|
+
patched = true;
|
|
632
|
+
} else detach(data, id);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
const current = owner.type === "insert" ? owner : flatten(data).get(id);
|
|
636
|
+
if (!current) {
|
|
637
|
+
const options = { id };
|
|
638
|
+
applyChanges(options, record.changes);
|
|
639
|
+
placeAt(options, record.parent ?? null, record.position);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (record.parent !== void 0 && current.parent !== record.parent) {
|
|
643
|
+
if (owner.type === "insert") {
|
|
644
|
+
owner.list.splice(owner.list.indexOf(owner.options), 1);
|
|
645
|
+
patched = true;
|
|
646
|
+
} else detach(data, id);
|
|
647
|
+
applyChanges(current.options, record.changes);
|
|
648
|
+
placeAt(current.options, record.parent, record.position);
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (owner.type === "insert") {
|
|
652
|
+
applyChanges(current.options, record.changes);
|
|
653
|
+
patched = true;
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const patchChanges = {};
|
|
657
|
+
applyChanges(current.options, record.changes, (key) => {
|
|
658
|
+
const keyOwner = index.key(id, key);
|
|
659
|
+
if (keyOwner.type !== "patch") return true;
|
|
660
|
+
(patchChanges[keyOwner.index] ??= {})[key] = record.changes[key];
|
|
661
|
+
return false;
|
|
662
|
+
});
|
|
663
|
+
for (const [i, changes] of Object.entries(patchChanges)) {
|
|
664
|
+
applyChanges(patches[i], changes);
|
|
665
|
+
patched = true;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return patched;
|
|
669
|
+
}
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region ../../../vendor/include/src/index.ts
|
|
672
|
+
const JsExpr = new yaml.Type("tag:yaml.org,2002:js", {
|
|
673
|
+
kind: "scalar",
|
|
674
|
+
resolve: (data) => typeof data === "string",
|
|
675
|
+
construct: (data) => ({ __jsExpr: data }),
|
|
676
|
+
predicate: isJsExpr,
|
|
677
|
+
represent: (data) => data["__jsExpr"]
|
|
678
|
+
});
|
|
679
|
+
/** YAML dialect preserving child-owned JavaScript expression nodes. */
|
|
680
|
+
const entryListSchema = yaml.JSON_SCHEMA.extend(JsExpr);
|
|
681
|
+
const schema = entryListSchema;
|
|
682
|
+
const types = {
|
|
683
|
+
".json": "application/json",
|
|
684
|
+
".yaml": "application/yaml",
|
|
685
|
+
".yml": "application/yaml"
|
|
686
|
+
};
|
|
687
|
+
var ConfigFileError = class extends Error {
|
|
688
|
+
stage;
|
|
689
|
+
name = "ConfigFileError";
|
|
690
|
+
constructor(stage, path, cause) {
|
|
691
|
+
super(`failed to ${stage} config file ${path}`, { cause });
|
|
692
|
+
this.stage = stage;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
/** The file changed under us between the pre-check and the rename. */
|
|
696
|
+
var StaleWriteError = class extends Error {
|
|
697
|
+
name = "StaleWriteError";
|
|
698
|
+
};
|
|
699
|
+
function isENOENT(error) {
|
|
700
|
+
return error?.code === "ENOENT";
|
|
701
|
+
}
|
|
702
|
+
const RENAME_RETRIES = 10;
|
|
703
|
+
const RENAME_BACKOFF = 20;
|
|
704
|
+
/**
|
|
705
|
+
* On Windows a rename over a file that another process holds open (typically
|
|
706
|
+
* an antivirus scanning what we just wrote) fails with one of these; the lock
|
|
707
|
+
* is normally gone within milliseconds. A bounded retry is cheap enough to
|
|
708
|
+
* apply on every platform.
|
|
709
|
+
*/
|
|
710
|
+
function isTransientRenameError(error) {
|
|
711
|
+
const code = error?.code;
|
|
712
|
+
return code === "EACCES" || code === "EPERM" || code === "EBUSY";
|
|
713
|
+
}
|
|
112
714
|
var Include = class extends EntryTree {
|
|
113
715
|
config;
|
|
114
716
|
static inject = ["loader"];
|
|
115
717
|
static [EntryGroup.key] = true;
|
|
116
718
|
filename;
|
|
117
719
|
type;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
720
|
+
/** The last file state we read or wrote. */
|
|
721
|
+
cache;
|
|
722
|
+
/** Tree-side changes that have not reached the file yet. */
|
|
723
|
+
journal = /* @__PURE__ */ new Map();
|
|
724
|
+
/** Ids we assigned to entries the file leaves anonymous. */
|
|
725
|
+
anonymous = /* @__PURE__ */ new Map();
|
|
726
|
+
dirtyRead = false;
|
|
727
|
+
dirtyWrite = false;
|
|
728
|
+
_draining;
|
|
729
|
+
/** Only the latest intended tree is ever applied; older ones are skipped. */
|
|
730
|
+
_pendingTree;
|
|
731
|
+
_applyTask;
|
|
732
|
+
_disposed = false;
|
|
733
|
+
_seq = 0;
|
|
734
|
+
persistenceError;
|
|
124
735
|
constructor(ctx, config) {
|
|
125
736
|
super(ctx);
|
|
126
737
|
this.config = config;
|
|
127
738
|
this.enableLogs = config.enableLogs ?? ctx.fiber.entry?.parent.tree.enableLogs ?? false;
|
|
128
739
|
this.filename = fileURLToPath(new URL(this.config.path, this.ctx.baseUrl));
|
|
129
740
|
const ext = extname(this.filename);
|
|
130
|
-
if (!
|
|
131
|
-
this.type =
|
|
132
|
-
this.readonly = !this.type;
|
|
741
|
+
if (!types[ext]) throw new Error(`extension "${ext}" not supported`);
|
|
742
|
+
this.type = types[ext];
|
|
133
743
|
this.ctx.baseUrl = new URL(".", pathToFileURL(this.filename)).href;
|
|
134
|
-
|
|
744
|
+
ensureInsertIds(config.patches, (id) => !!this.store[id]);
|
|
745
|
+
ctx.on("internal/update", (config, next) => {
|
|
135
746
|
if (config.path !== this.config.path) return next();
|
|
136
747
|
this.config = config;
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
async checkAccess() {
|
|
144
|
-
if (!this.type) return;
|
|
145
|
-
try {
|
|
146
|
-
await access(this.filename, constants.W_OK);
|
|
147
|
-
} catch {
|
|
148
|
-
this.readonly = true;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
async read(forced = false) {
|
|
152
|
-
const content = await readFile(this.filename, "utf8");
|
|
153
|
-
if (!forced && this.content === content) return false;
|
|
154
|
-
let data;
|
|
155
|
-
if (this.type === "application/yaml") data = yaml.load(content, { schema: entryListSchema });
|
|
156
|
-
else if (this.type === "application/json") data = JSON.parse(content);
|
|
157
|
-
else {
|
|
158
|
-
const module = await import(
|
|
159
|
-
/* @vite-ignore */
|
|
160
|
-
this.filename
|
|
161
|
-
);
|
|
162
|
-
data = module.default || module;
|
|
163
|
-
}
|
|
164
|
-
if (!Array.isArray(data)) throw new TypeError(`config file must be a top-level array of entries: ${this.filename}`);
|
|
165
|
-
this.content = content;
|
|
166
|
-
this.data = data;
|
|
167
|
-
await this.checkAccess();
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
applyPatches(data, patches = this.config.patches) {
|
|
171
|
-
return applyEntryPatches(data, patches, (message, ...args) => {
|
|
172
|
-
this.ctx.root.logger?.("loader").warn(message, ...args);
|
|
748
|
+
ensureInsertIds(config.patches, (id) => !!this.store[id]);
|
|
749
|
+
this._scheduleApply();
|
|
750
|
+
return this._applyTask;
|
|
173
751
|
});
|
|
174
752
|
}
|
|
753
|
+
warn = (format, ...args) => {
|
|
754
|
+
this.ctx.root.logger?.("loader").warn(format, ...args);
|
|
755
|
+
};
|
|
175
756
|
async *[Service.init]() {
|
|
176
757
|
try {
|
|
177
|
-
await this.
|
|
758
|
+
await this._read();
|
|
178
759
|
} catch (error) {
|
|
179
|
-
if (error
|
|
180
|
-
if (this.config.initial) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
760
|
+
if (!isENOENT(error instanceof ConfigFileError && error.stage === "read" ? error.cause : void 0)) throw error;
|
|
761
|
+
if (!this.config.initial) throw new Error(`config file not found: ${this.filename}`);
|
|
762
|
+
const initial = structuredClone(this.config.initial);
|
|
763
|
+
ensureIds(initial, (id) => !!this.store[id]);
|
|
764
|
+
await this._writeText(this.dump(initial));
|
|
765
|
+
await this._read();
|
|
184
766
|
}
|
|
185
767
|
yield () => this.stop();
|
|
186
|
-
await this.
|
|
768
|
+
await this._applyTask;
|
|
187
769
|
}
|
|
188
770
|
async stop() {
|
|
771
|
+
this._disposed = true;
|
|
772
|
+
this._pendingTree = void 0;
|
|
773
|
+
if (this.journal.size) {
|
|
774
|
+
this.dirtyWrite = true;
|
|
775
|
+
this._drain();
|
|
776
|
+
}
|
|
189
777
|
try {
|
|
190
|
-
await this.
|
|
778
|
+
await this.flush();
|
|
191
779
|
} finally {
|
|
192
780
|
this.root.stop();
|
|
193
|
-
await this.flushWrite();
|
|
194
781
|
}
|
|
195
782
|
}
|
|
196
783
|
/**
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* tree: a hot-reload of a live app must never take the process down.
|
|
784
|
+
* Resolves once every pending read and write has been processed and the
|
|
785
|
+
* tree reflects the outcome. Retries pending writes and rejects while changes remain unsaved.
|
|
200
786
|
*/
|
|
201
|
-
async
|
|
787
|
+
async flush() {
|
|
788
|
+
if (this.journal.size && !this._draining) {
|
|
789
|
+
this.dirtyWrite = true;
|
|
790
|
+
this._drain();
|
|
791
|
+
}
|
|
792
|
+
while (this._draining || this._applyTask) {
|
|
793
|
+
await this._draining;
|
|
794
|
+
await this._applyTask;
|
|
795
|
+
}
|
|
796
|
+
if (this.journal.size) throw this.persistenceError ?? /* @__PURE__ */ new Error(`config file ${this.filename} has ${this.journal.size} unsaved change(s)`);
|
|
797
|
+
this.persistenceError = void 0;
|
|
798
|
+
}
|
|
799
|
+
/** Re-read the file and reconcile the tree with it. */
|
|
800
|
+
refresh() {
|
|
801
|
+
this.dirtyRead = true;
|
|
802
|
+
this._drain();
|
|
803
|
+
return this.flush();
|
|
804
|
+
}
|
|
805
|
+
/** Called synchronously by the loader once a tree-side change is in place. */
|
|
806
|
+
commit(change) {
|
|
807
|
+
this.context.emit("loader/config-update");
|
|
808
|
+
const parent = change.group === this.root ? null : change.group.ctx.fiber.entry.options.id;
|
|
809
|
+
record$1(this.journal, change, parent);
|
|
810
|
+
this.dirtyWrite = true;
|
|
811
|
+
this._drain();
|
|
812
|
+
}
|
|
813
|
+
_drain() {
|
|
814
|
+
if (this._draining) return;
|
|
815
|
+
this._draining = this._loop().finally(() => {
|
|
816
|
+
this._draining = void 0;
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
async _loop() {
|
|
820
|
+
while (this.dirtyRead || this.dirtyWrite) {
|
|
821
|
+
if (this.dirtyRead) {
|
|
822
|
+
this.dirtyRead = false;
|
|
823
|
+
try {
|
|
824
|
+
await this._read();
|
|
825
|
+
} catch (error) {
|
|
826
|
+
this.persistenceError = error;
|
|
827
|
+
this.ctx.logger.warn(error);
|
|
828
|
+
if (this.dirtyRead) continue;
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (this.dirtyWrite) {
|
|
833
|
+
this.dirtyWrite = false;
|
|
834
|
+
try {
|
|
835
|
+
await this._write();
|
|
836
|
+
} catch (error) {
|
|
837
|
+
this.persistenceError = error;
|
|
838
|
+
this.ctx.logger.error(error);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async _read() {
|
|
844
|
+
let content;
|
|
845
|
+
try {
|
|
846
|
+
content = await readFile(this.filename, "utf8");
|
|
847
|
+
} catch (error) {
|
|
848
|
+
throw new ConfigFileError("read", this.filename, error);
|
|
849
|
+
}
|
|
850
|
+
if (content === this.cache?.content) return;
|
|
851
|
+
let data;
|
|
202
852
|
try {
|
|
203
|
-
|
|
204
|
-
await this.root.update(this.applyPatches(this.data));
|
|
853
|
+
data = this.type === "application/yaml" ? yaml.load(content, { schema }) : JSON.parse(content);
|
|
205
854
|
} catch (error) {
|
|
206
|
-
|
|
207
|
-
this.ctx.logger.warn(error);
|
|
855
|
+
throw new ConfigFileError("parse", this.filename, error);
|
|
208
856
|
}
|
|
857
|
+
if (!Array.isArray(data)) throw new ConfigFileError("validate", this.filename, /* @__PURE__ */ new TypeError("config file must be a top-level array"));
|
|
858
|
+
this._assignIds(data);
|
|
859
|
+
if (this.cache) {
|
|
860
|
+
const index = new PatchIndex(this.config.patches ?? []);
|
|
861
|
+
const conflicts = reconcile(this.journal, flatten(this.cache.data), flatten(data), (id, key) => index.fileOwned(id, key));
|
|
862
|
+
for (const { id, reason } of conflicts) this.ctx.logger.error("config conflict in %C: entry %C %s; file wins", this.filename, id, reason);
|
|
863
|
+
}
|
|
864
|
+
this.cache = {
|
|
865
|
+
content,
|
|
866
|
+
data
|
|
867
|
+
};
|
|
868
|
+
this._scheduleApply();
|
|
869
|
+
if (this.journal.size) this.dirtyWrite = true;
|
|
209
870
|
}
|
|
210
|
-
async
|
|
211
|
-
if (this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
871
|
+
async _write() {
|
|
872
|
+
if (!this.cache || !this.journal.size) return;
|
|
873
|
+
const current = await readFile(this.filename, "utf8").catch((error) => {
|
|
874
|
+
if (isENOENT(error)) return void 0;
|
|
875
|
+
throw error;
|
|
876
|
+
});
|
|
877
|
+
if (current === void 0) {
|
|
878
|
+
this.ctx.logger.warn("config file %C is missing, %d pending change(s) kept in memory", this.filename, this.journal.size);
|
|
217
879
|
return;
|
|
880
|
+
}
|
|
881
|
+
if (current !== this.cache.content) {
|
|
882
|
+
this.dirtyRead = true;
|
|
883
|
+
this.dirtyWrite = true;
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const batch = this.journal;
|
|
887
|
+
this.journal = /* @__PURE__ */ new Map();
|
|
888
|
+
const restore = () => {
|
|
889
|
+
for (const [id, record] of this.journal) merge(batch, id, record);
|
|
890
|
+
this.journal = batch;
|
|
891
|
+
};
|
|
892
|
+
const data = structuredClone(this.cache.data);
|
|
893
|
+
const patches = structuredClone(this.config.patches ?? []);
|
|
894
|
+
if (routeJournal(batch, data, patches, this.warn)) {
|
|
895
|
+
this.config = {
|
|
896
|
+
...this.config,
|
|
897
|
+
patches
|
|
898
|
+
};
|
|
899
|
+
if (this.ctx.fiber.entry) try {
|
|
900
|
+
Promise.resolve(this.ctx.fiber.update(this.config)).catch((error) => {
|
|
901
|
+
this.ctx.logger.error(error);
|
|
902
|
+
});
|
|
903
|
+
} catch (error) {
|
|
904
|
+
this.ctx.logger.error(error);
|
|
905
|
+
}
|
|
906
|
+
else this.ctx.logger.warn("patches of %C changed at runtime but cannot be persisted", this.filename);
|
|
907
|
+
}
|
|
908
|
+
if (deepEqual(data, this.cache.data)) {
|
|
909
|
+
this._scheduleApply();
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const text = this.dump(data);
|
|
913
|
+
try {
|
|
914
|
+
await access(this.filename, constants.W_OK);
|
|
915
|
+
} catch {
|
|
916
|
+
restore();
|
|
917
|
+
this.ctx.logger.warn("config file %C is read-only, %d pending change(s) kept in memory", this.filename, this.journal.size);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
await this._writeText(text, this.cache.content);
|
|
218
922
|
} catch (error) {
|
|
219
|
-
|
|
220
|
-
|
|
923
|
+
restore();
|
|
924
|
+
if (error instanceof StaleWriteError) {
|
|
925
|
+
this.dirtyRead = true;
|
|
926
|
+
this.dirtyWrite = true;
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
this.ctx.logger.warn("failed to write config file %C, %d pending change(s) kept in memory", this.filename, this.journal.size);
|
|
930
|
+
throw error;
|
|
221
931
|
}
|
|
932
|
+
this.cache = {
|
|
933
|
+
content: text,
|
|
934
|
+
data
|
|
935
|
+
};
|
|
936
|
+
this._scheduleApply();
|
|
222
937
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
938
|
+
/**
|
|
939
|
+
* Write through a temp file and a rename, so that no reader ever sees a
|
|
940
|
+
* truncated file. When `expected` is given, the rename (and every retry of
|
|
941
|
+
* it) only goes ahead while the file still holds that content.
|
|
942
|
+
*/
|
|
943
|
+
async _writeText(text, expected) {
|
|
944
|
+
const tmp = join(dirname(this.filename), `.${basename(this.filename)}.${process.pid}.${this._seq++}.tmp`);
|
|
945
|
+
try {
|
|
946
|
+
await writeFile(tmp, text);
|
|
947
|
+
for (let attempt = 0;; attempt++) {
|
|
948
|
+
if (expected !== void 0) {
|
|
949
|
+
if (await readFile(this.filename, "utf8").catch((error) => {
|
|
950
|
+
if (isENOENT(error)) return void 0;
|
|
951
|
+
throw error;
|
|
952
|
+
}) !== expected) throw new StaleWriteError();
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
await rename(tmp, this.filename);
|
|
956
|
+
return;
|
|
957
|
+
} catch (error) {
|
|
958
|
+
if (attempt >= RENAME_RETRIES || !isTransientRenameError(error)) throw error;
|
|
959
|
+
}
|
|
960
|
+
await setTimeout$1(RENAME_BACKOFF * (attempt + 1));
|
|
961
|
+
}
|
|
962
|
+
} catch (error) {
|
|
963
|
+
await rm(tmp, { force: true }).catch(() => {});
|
|
964
|
+
throw error;
|
|
965
|
+
}
|
|
243
966
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
967
|
+
dump(data) {
|
|
968
|
+
return this.type === "application/yaml" ? yaml.dump(data, { schema }) : JSON.stringify(data, null, 2);
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Entries the file leaves anonymous get an id here, before the loader sees
|
|
972
|
+
* them. One that looks exactly like an entry we named on the previous read
|
|
973
|
+
* keeps that id, so editing a neighbor does not restart it; anything else
|
|
974
|
+
* gets a fresh id. Assigned ids stay in memory until the next write.
|
|
975
|
+
*/
|
|
976
|
+
_assignIds(data) {
|
|
977
|
+
const used = /* @__PURE__ */ new Set();
|
|
978
|
+
const pending = [];
|
|
979
|
+
const walk = (entries, parent) => {
|
|
980
|
+
for (const options of entries) {
|
|
981
|
+
if (options.id) used.add(options.id);
|
|
982
|
+
else pending.push({
|
|
983
|
+
options,
|
|
984
|
+
parent
|
|
985
|
+
});
|
|
986
|
+
if (options.group && Array.isArray(options.config)) walk(options.config, options.id ?? null);
|
|
987
|
+
}
|
|
988
|
+
};
|
|
989
|
+
walk(data, null);
|
|
990
|
+
const previous = this.anonymous;
|
|
991
|
+
this.anonymous = /* @__PURE__ */ new Map();
|
|
992
|
+
for (const { options, parent } of pending) {
|
|
993
|
+
let id;
|
|
994
|
+
for (const [candidate, info] of previous) {
|
|
995
|
+
if (info.parent !== parent || !deepEqual(info.options, options)) continue;
|
|
996
|
+
id = candidate;
|
|
997
|
+
previous.delete(candidate);
|
|
998
|
+
break;
|
|
999
|
+
}
|
|
1000
|
+
if (!id) do
|
|
1001
|
+
id = Math.random().toString(16).slice(2, 10);
|
|
1002
|
+
while (used.has(id) || this.store[id]);
|
|
1003
|
+
used.add(id);
|
|
1004
|
+
this.anonymous.set(id, {
|
|
1005
|
+
parent,
|
|
1006
|
+
options: structuredClone(options)
|
|
1007
|
+
});
|
|
1008
|
+
const rest = { ...options };
|
|
1009
|
+
for (const key of Object.keys(options)) delete options[key];
|
|
1010
|
+
Object.assign(options, { id }, rest);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
_scheduleApply() {
|
|
1014
|
+
if (this._disposed || !this.cache) return;
|
|
1015
|
+
const tree = applyPatches(this.cache.data, this.config.patches, this.warn);
|
|
1016
|
+
this._pendingTree = applyJournal(tree, this.journal, void 0, this.warn);
|
|
1017
|
+
this._applyTask ??= (async () => {
|
|
1018
|
+
try {
|
|
1019
|
+
while (this._pendingTree) {
|
|
1020
|
+
const tree = this._pendingTree;
|
|
1021
|
+
this._pendingTree = void 0;
|
|
1022
|
+
await this.root.update(tree);
|
|
1023
|
+
}
|
|
1024
|
+
} finally {
|
|
1025
|
+
this._applyTask = void 0;
|
|
1026
|
+
}
|
|
1027
|
+
})();
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
//#endregion
|
|
1031
|
+
//#region lib/types/bundle-platform.js
|
|
1032
|
+
/** Host operating-system admission for optional bundle layers. */
|
|
1033
|
+
const NODE_PLATFORMS = Object.freeze([
|
|
1034
|
+
"aix",
|
|
1035
|
+
"android",
|
|
1036
|
+
"darwin",
|
|
1037
|
+
"freebsd",
|
|
1038
|
+
"haiku",
|
|
1039
|
+
"linux",
|
|
1040
|
+
"openbsd",
|
|
1041
|
+
"sunos",
|
|
1042
|
+
"win32",
|
|
1043
|
+
"cygwin",
|
|
1044
|
+
"netbsd"
|
|
1045
|
+
]);
|
|
1046
|
+
/** A valid bundle declaration excludes the operating system running DSH. */
|
|
1047
|
+
var UnsupportedBundlePlatformError = class extends Error {
|
|
1048
|
+
/**
|
|
1049
|
+
* @param platforms Node platform names supported by the bundle.
|
|
1050
|
+
* @param platform Operating system of the DSH Host.
|
|
1051
|
+
*/
|
|
1052
|
+
constructor(platforms, platform) {
|
|
1053
|
+
super(`Bundle supports Host platforms ${platforms.join(", ")}; the DSH Host is running ${platform}`);
|
|
1054
|
+
this.name = "UnsupportedBundlePlatformError";
|
|
248
1055
|
}
|
|
249
1056
|
};
|
|
1057
|
+
/**
|
|
1058
|
+
* Validate package metadata and reject an unsupported Host before selecting or composing a bundle.
|
|
1059
|
+
* @param platforms The optional `dsh.bundle.platforms` field read from package.json.
|
|
1060
|
+
* @param platform Operating system of the DSH Host, from Node's `process.platform`.
|
|
1061
|
+
* @throws When the declaration is malformed or excludes the Host; omission allows every platform.
|
|
1062
|
+
*/
|
|
1063
|
+
function assertBundlePlatform(platforms, platform) {
|
|
1064
|
+
if (platforms === void 0) return;
|
|
1065
|
+
if (!Array.isArray(platforms) || platforms.length === 0 || !platforms.every((value) => typeof value === "string" && NODE_PLATFORMS.includes(value))) throw new Error("dsh.bundle.platforms must be a nonempty list of Node platform names");
|
|
1066
|
+
if (!platforms.includes(platform)) throw new UnsupportedBundlePlatformError(platforms, platform);
|
|
1067
|
+
}
|
|
250
1068
|
//#endregion
|
|
251
1069
|
//#region lib/types/plugin-compatibility.js
|
|
252
1070
|
/** Evaluate plugin dsh requirements (`engines.dsh`, else dsh peers) without importing plugin code. */
|
|
@@ -547,49 +1365,6 @@ function resolveProfileDir(name, home = resolveDshHome()) {
|
|
|
547
1365
|
if (name === "" || name.includes("/") || name.includes("\\") || name === "." || name === ".." || name === "node_modules") throw new Error(`dsh: invalid profile name ${JSON.stringify(name)}`);
|
|
548
1366
|
return join(home, PROFILES_DIR, name);
|
|
549
1367
|
}
|
|
550
|
-
/** The shipped profile templates auto-initialized on first use, by name. */
|
|
551
|
-
const PROFILE_TEMPLATES = {
|
|
552
|
-
acp: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-acp-app"] },
|
|
553
|
-
web: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"] },
|
|
554
|
-
headless: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-headless"] },
|
|
555
|
-
sdk: { bundles: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-sdk-app"] },
|
|
556
|
-
"sdk-minimal": { bundles: ["@deepseek-ai/dsh-sdk-minimal"] }
|
|
557
|
-
};
|
|
558
|
-
/** Installation-owned bundle tuples normalized to the shipped template. */
|
|
559
|
-
const INSTALLATION_OWNED_PROFILE_TUPLES = { headless: [
|
|
560
|
-
"@deepseek-ai/dsh-base",
|
|
561
|
-
"@deepseek-ai/dsh-web-app",
|
|
562
|
-
"@deepseek-ai/dsh-headless"
|
|
563
|
-
] };
|
|
564
|
-
/**
|
|
565
|
-
* Bundles an earlier release shipped and the installation no longer carries;
|
|
566
|
-
* {@link loadProfileDirectory} removes them from the profile's bundle list.
|
|
567
|
-
*/
|
|
568
|
-
const RETIRED_BUNDLES = new Set(["@deepseek-ai/dsh-experimental-schedule-bundle"]);
|
|
569
|
-
/** The bundle list a `dsh plugin` init uses for a name with no shipped template. */
|
|
570
|
-
const DEFAULT_PROFILE_BUNDLES = ["@deepseek-ai/dsh-base"];
|
|
571
|
-
/**
|
|
572
|
-
* The bundles the dsh installation ships for a person to switch on: each a
|
|
573
|
-
* runtime dependency of the installation that declares `dsh.bundle.patch`,
|
|
574
|
-
* an `icon`, and `./locale/*.json` display metadata, selected by no shipped
|
|
575
|
-
* template, and offered switched off by the plugin manager
|
|
576
|
-
* ([rationale](../../../../.agents/notes/implemented/process/2026-09-15-shipped-optional-bundles.md),
|
|
577
|
-
* [admission](../../../../.agents/notes/implemented/architecture/2026-09-21-experimental-capabilities-as-optional-bundles.md)).
|
|
578
|
-
*/
|
|
579
|
-
const OPTIONAL_BUNDLES = [
|
|
580
|
-
"@deepseek-ai/dsh-experimental-session-search",
|
|
581
|
-
"@deepseek-ai/dsh-experimental-ralph-bundle",
|
|
582
|
-
"@deepseek-ai/dsh-experimental-terminal-bundle",
|
|
583
|
-
"@deepseek-ai/dsh-experimental-badge-skill-bundle",
|
|
584
|
-
"@deepseek-ai/dsh-experimental-session-titles-bundle",
|
|
585
|
-
"@deepseek-ai/dsh-experimental-agent-team-profile",
|
|
586
|
-
"@deepseek-ai/dsh-experimental-voice-input-bundle",
|
|
587
|
-
"@deepseek-ai/dsh-experimental-cot-translation-bundle",
|
|
588
|
-
"@deepseek-ai/dsh-experimental-auto-review",
|
|
589
|
-
"@deepseek-ai/dsh-experimental-inspector-profile",
|
|
590
|
-
"@deepseek-ai/dsh-experimental-plugin-market-bundle",
|
|
591
|
-
"@deepseek-ai/dsh-experimental-tool-worktree"
|
|
592
|
-
];
|
|
593
1368
|
const PROFILE_PATCH_TEMPLATE = `# Your patch layer for this dsh profile, applied after every bundle layer:
|
|
594
1369
|
# a top-level YAML array of loader patch entries (id-targeted config
|
|
595
1370
|
# overrides, disables, and insert lists; \`!!js\` expressions allowed).
|
|
@@ -710,7 +1485,11 @@ function readPackageManifest(anchor) {
|
|
|
710
1485
|
}
|
|
711
1486
|
/** Return dependency names that may be imported by a loader-visible plugin. */
|
|
712
1487
|
function profileDependencyNames(manifest) {
|
|
713
|
-
return [
|
|
1488
|
+
return [
|
|
1489
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
1490
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
1491
|
+
...Object.keys(manifest.optionalDependencies ?? {})
|
|
1492
|
+
];
|
|
714
1493
|
}
|
|
715
1494
|
/** Resolve the installation packages that the runtime resolver supplies to every profile. */
|
|
716
1495
|
function collectInstallationScopePackages(installAnchor, skippedBundles) {
|
|
@@ -1004,7 +1783,8 @@ function resolveBundleDir(binName, packageName, installAnchor, profileDir) {
|
|
|
1004
1783
|
* the shared Harness home. This is used by application-owned profiles whose
|
|
1005
1784
|
* package project and lifecycle belong to that application.
|
|
1006
1785
|
* Retired bundles are removed from the stored bundle list first, rewriting the
|
|
1007
|
-
* manifest when it listed one. Unreadable bundles, and bundles whose own dsh requirements
|
|
1786
|
+
* manifest when it listed one. Unreadable bundles, unsupported Host platforms, and bundles whose own dsh requirements
|
|
1787
|
+
* the profile does not exempt are skipped
|
|
1008
1788
|
* without changing the manifest and listed in `skippedBundles`; nothing is printed.
|
|
1009
1789
|
* @param binName - the diagnostic prefix on thrown errors.
|
|
1010
1790
|
* @param dir - absolute profile package directory.
|
|
@@ -1022,6 +1802,7 @@ function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
|
1022
1802
|
const bundleManifest = readProfileManifest(binName, packageDir);
|
|
1023
1803
|
const bundle = bundleManifest.dsh?.bundle;
|
|
1024
1804
|
if (bundle === void 0) throw new Error(`${binName}: profile bundle ${JSON.stringify(packageName)} declares no dsh.bundle in its package.json`);
|
|
1805
|
+
assertBundlePlatform(bundle.platforms, process.platform);
|
|
1025
1806
|
const issue = evaluatePluginCompatibility(bundleManifest, exemptions);
|
|
1026
1807
|
if (issue !== void 0 && !issue.exempted) throw new Error(pluginCompatibilityWarning(issue));
|
|
1027
1808
|
const patchPaths = bundlePatchPaths(packageDir, bundle);
|
|
@@ -1051,8 +1832,8 @@ function loadProfileDirectory(binName, dir, installAnchor, options = {}) {
|
|
|
1051
1832
|
}
|
|
1052
1833
|
/**
|
|
1053
1834
|
* Load a profile: resolve every `dsh.profile.bundles` entry to its patch
|
|
1054
|
-
* layer and parse the profile's own patch file. Unreadable
|
|
1055
|
-
* are skipped and listed in `skippedBundles`; profile manifest and user patch errors still throw.
|
|
1835
|
+
* layer and parse the profile's own patch file. Unreadable bundles, unsupported Host platforms,
|
|
1836
|
+
* and incompatible bundles are skipped and listed in `skippedBundles`; profile manifest and user patch errors still throw.
|
|
1056
1837
|
* @param binName - the diagnostic prefix on thrown errors.
|
|
1057
1838
|
* @param name - the profile name.
|
|
1058
1839
|
* @param installAnchor - absolute path of the dsh app's package.json (first resolution anchor).
|
|
@@ -1082,7 +1863,7 @@ function loadProfile(binName, name, installAnchor, home = resolveDshHome(), opti
|
|
|
1082
1863
|
* @returns the composed entry list.
|
|
1083
1864
|
*/
|
|
1084
1865
|
function composeEntries(layers, warn = () => {}) {
|
|
1085
|
-
return
|
|
1866
|
+
return applyPatches([], structuredClone(layers.flat()), (message, ...args) => {
|
|
1086
1867
|
let index = 0;
|
|
1087
1868
|
warn(message.replace(/%C/g, () => JSON.stringify(args[index++])));
|
|
1088
1869
|
});
|
|
@@ -1205,6 +1986,8 @@ function writeProfileBundles(profileDir, manifest, bundles) {
|
|
|
1205
1986
|
* template entries retain their order and duplicates. New bundle dependencies activate automatically.
|
|
1206
1987
|
* @param options - location, inventory captured before pnpm, and whether explicitly disabled bundles remain disabled.
|
|
1207
1988
|
* @returns updated inventory and newly added ordinary dependencies for caller-owned warnings.
|
|
1989
|
+
* @throws If a new selection has invalid platform metadata or excludes the Host. Installed dependencies
|
|
1990
|
+
* remain; no bundle selection changes are written.
|
|
1208
1991
|
*/
|
|
1209
1992
|
function reconcileProfilePlugins(options) {
|
|
1210
1993
|
const after = readProfilePlugins(options);
|
|
@@ -1214,7 +1997,10 @@ function reconcileProfilePlugins(options) {
|
|
|
1214
1997
|
const disabled = new Set(options.preserveDisabled ? options.before.dependencies.filter((dependency) => dependency.bundle && !dependency.enabled).map((dependency) => dependency.name) : []);
|
|
1215
1998
|
const previous = after.manifest.dsh?.profile?.bundles ?? [];
|
|
1216
1999
|
const bundles = previous.filter((name) => !(beforeNames.has(name) || afterNames.has(name)) || bundleNames.has(name));
|
|
1217
|
-
for (const dependency of after.dependencies) if (dependency.bundle && !disabled.has(dependency.name) && !bundles.includes(dependency.name))
|
|
2000
|
+
for (const dependency of after.dependencies) if (dependency.bundle && !disabled.has(dependency.name) && !bundles.includes(dependency.name)) {
|
|
2001
|
+
assertBundlePlatform(bundleManifest(options, dependency.name)?.dsh?.bundle?.platforms, process.platform);
|
|
2002
|
+
bundles.push(dependency.name);
|
|
2003
|
+
}
|
|
1218
2004
|
return {
|
|
1219
2005
|
plugins: {
|
|
1220
2006
|
manifest: bundles.length !== previous.length || bundles.some((name, index) => name !== previous[index]) ? writeProfileBundles(options.profileDir, after.manifest, bundles) : after.manifest,
|
|
@@ -1785,7 +2571,7 @@ function internalModules() {
|
|
|
1785
2571
|
const esmResolve = addon.requireBuiltin("internal/modules/esm/resolve");
|
|
1786
2572
|
const esm = esmModule.getOrInitializeCascadedLoader();
|
|
1787
2573
|
const modern = "getOrCreateModuleJob" in esm;
|
|
1788
|
-
/* v8 ignore start -- the supported Node
|
|
2574
|
+
/* v8 ignore start -- the supported Node 24/26 matrix validates each available Internal interface */
|
|
1789
2575
|
if (typeof esm.resolveSync !== "function" || typeof Reflect.get(esm, modern ? "getOrCreateModuleJob" : "getModuleJobForImport") !== "function" || !modern && typeof Reflect.get(esm, "resolve") !== "function" || typeof cjsModule.Module._resolveFilename !== "function" || typeof cjsHelpers.getCjsConditions !== "function" || typeof esmUtils.getDefaultConditions !== "function" || typeof esmResolve.defaultResolve !== "function") throw new Error("profile resolution: unsupported Node module loader");
|
|
1790
2576
|
/* v8 ignore stop */
|
|
1791
2577
|
return {
|
|
@@ -1861,7 +2647,7 @@ function installRuntimeInterception(resolution) {
|
|
|
1861
2647
|
let expected;
|
|
1862
2648
|
try {
|
|
1863
2649
|
expected = adapted(target.specifier, target.parentURL, attributes);
|
|
1864
|
-
/* v8 ignore next --
|
|
2650
|
+
/* v8 ignore next -- the current loader resolves synchronously; the private interface also permits Promise results */
|
|
1865
2651
|
if (expected instanceof Promise) expected = expected.catch(restoreImporter);
|
|
1866
2652
|
} catch (error) {
|
|
1867
2653
|
return restoreImporter(error);
|
|
@@ -1890,7 +2676,7 @@ function installRuntimeInterception(resolution) {
|
|
|
1890
2676
|
} catch (error) {
|
|
1891
2677
|
return restoreImporter(error);
|
|
1892
2678
|
}
|
|
1893
|
-
/* v8 ignore next --
|
|
2679
|
+
/* v8 ignore next -- the current loader resolves synchronously; the private interface also permits Promise results */
|
|
1894
2680
|
if (result instanceof Promise) return result.catch(restoreImporter);
|
|
1895
2681
|
if (cacheable) state.esm = result;
|
|
1896
2682
|
return result;
|
|
@@ -1912,12 +2698,12 @@ function installRuntimeInterception(resolution) {
|
|
|
1912
2698
|
};
|
|
1913
2699
|
return (request, parent, attributes) => {
|
|
1914
2700
|
const result = adapted(request, parent, attributes);
|
|
1915
|
-
/* v8 ignore next --
|
|
2701
|
+
/* v8 ignore next -- the current loader resolves synchronously; the private interface also permits Promise results */
|
|
1916
2702
|
return result instanceof Promise ? result.then((resolved) => relocate(resolved, parent)) : relocate(result, parent);
|
|
1917
2703
|
};
|
|
1918
2704
|
};
|
|
1919
2705
|
let restoreEsm;
|
|
1920
|
-
/* v8 ignore else -- CI coverage runs Node 24 v2; the Node
|
|
2706
|
+
/* v8 ignore else -- CI coverage runs Node 24 v2; the Node 24.0 matrix exercises the v1 adapter */
|
|
1921
2707
|
if (modern) {
|
|
1922
2708
|
const loader = esm;
|
|
1923
2709
|
const original = Reflect.get(loader, "resolveSync");
|
|
@@ -2376,7 +3162,7 @@ function preflight(ctx, entries, parentURL, report) {
|
|
|
2376
3162
|
data = config.initial;
|
|
2377
3163
|
}
|
|
2378
3164
|
if (!Array.isArray(data)) return void 0;
|
|
2379
|
-
return check(
|
|
3165
|
+
return check(applyPatches(data, config.patches, patchWarning(ctx)), pathToFileURL(filename).href) ? `its included file ${filename} reaches an incompatible plugin, and that file is never rewritten` : void 0;
|
|
2380
3166
|
} finally {
|
|
2381
3167
|
includes.delete(filename);
|
|
2382
3168
|
}
|
|
@@ -2397,46 +3183,10 @@ function preflight(ctx, entries, parentURL, report) {
|
|
|
2397
3183
|
*/
|
|
2398
3184
|
function prepareProfilePatches(ctx, patches, parentURL, binName = "dsh") {
|
|
2399
3185
|
if (ctx.get("profileContext") === void 0) return patches;
|
|
2400
|
-
const rows = prepareProfileEntries(ctx,
|
|
3186
|
+
const rows = prepareProfileEntries(ctx, applyPatches([], patches, patchWarning(ctx)), parentURL, binName);
|
|
2401
3187
|
return rows.length === 0 ? [] : [{ insert: rows }];
|
|
2402
3188
|
}
|
|
2403
3189
|
//#endregion
|
|
2404
|
-
//#region lib/types/official-bundle-packages.js
|
|
2405
|
-
/** Package admission shared by offline catalog generation and release ordering. */
|
|
2406
|
-
/** Public DSH release members available through the ordinary on-demand bundle installer. */
|
|
2407
|
-
const ON_DEMAND_BUNDLES = ["@deepseek-ai/dsh-subagent-claude-code", "@deepseek-ai/dsh-subagent-codex"];
|
|
2408
|
-
//#endregion
|
|
2409
|
-
//#region lib/types/official-bundles.generated.js
|
|
2410
|
-
/** Generated offline Official bundle metadata. Run pnpm gen-official-bundle-catalog; do not edit. */
|
|
2411
|
-
/** Official on-demand entries, independent of installed packages and registry availability. */
|
|
2412
|
-
const OFFICIAL_ON_DEMAND_CATALOG = [{
|
|
2413
|
-
packageName: "@deepseek-ai/dsh-subagent-claude-code",
|
|
2414
|
-
meta: {
|
|
2415
|
-
title: {
|
|
2416
|
-
"en": "Claude Code subagent",
|
|
2417
|
-
"zh": "Claude Code 子智能体"
|
|
2418
|
-
},
|
|
2419
|
-
description: {
|
|
2420
|
-
"en": "Delegate one task to Claude Code using its native authentication and settings.",
|
|
2421
|
-
"zh": "使用 Claude Code 原生认证和设置委派单个任务。"
|
|
2422
|
-
},
|
|
2423
|
-
icon: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K"
|
|
2424
|
-
}
|
|
2425
|
-
}, {
|
|
2426
|
-
packageName: "@deepseek-ai/dsh-subagent-codex",
|
|
2427
|
-
meta: {
|
|
2428
|
-
title: {
|
|
2429
|
-
"en": "Codex subagent",
|
|
2430
|
-
"zh": "Codex 子智能体"
|
|
2431
|
-
},
|
|
2432
|
-
description: {
|
|
2433
|
-
"en": "Delegate one task to Codex using its native authentication and settings.",
|
|
2434
|
-
"zh": "使用 Codex 原生认证和设置委派单个任务。"
|
|
2435
|
-
},
|
|
2436
|
-
icon: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSIxNC45ODkzIiB5PSIxNS4xODc3IiB3aWR0aD0iMTIuMjM2NSIgaGVpZ2h0PSIxMi4yMzY1IiByeD0iMiIgZmlsbD0idXJsKCNkc2hfcGx1Z2luX2FydF9SMGEpIiBmaWxsLW9wYWNpdHk9IjAuOCI+PC9yZWN0PjxyZWN0IHg9IjguODcxMDkiIHk9IjkuMDY5NyIgd2lkdGg9IjEyLjIzNjUiIGhlaWdodD0iMTIuMjM2NSIgcng9IjIiIGZpbGw9InVybCgjZHNoX3BsdWdpbl9hcnRfUjBiKSIgZmlsbC1vcGFjaXR5PSIwLjgiPjwvcmVjdD48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYSIgeDE9IjIxLjEwNzUiIHkxPSIxNS4xODc3IiB4Mj0iMjEuMTA3NSIgeTI9IjI3LjQyNDMiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjNDVFN0E0Ij48L3N0b3A+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDU5MDlEIj48L3N0b3A+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImRzaF9wbHVnaW5fYXJ0X1IwYiIgeDE9IjE0Ljk4OTQiIHkxPSI5LjA2OTciIHgyPSIxNC45ODk0IiB5Mj0iMjEuMzA2MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiM2OUI5RkYiPjwvc3RvcD48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMjRERTIiPjwvc3RvcD48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48L3N2Zz4K"
|
|
2437
|
-
}
|
|
2438
|
-
}];
|
|
2439
|
-
//#endregion
|
|
2440
3190
|
//#region lib/types/config-schema/native.js
|
|
2441
3191
|
/** Identity checks at the plugin-export and lazy-builder boundaries. */
|
|
2442
3192
|
/**
|
|
@@ -3359,7 +4109,7 @@ async function collectConfigSchemas(profile, entries, resolution, diagnostics =
|
|
|
3359
4109
|
const rows = entryList(source);
|
|
3360
4110
|
let children;
|
|
3361
4111
|
try {
|
|
3362
|
-
children =
|
|
4112
|
+
children = applyPatches(rows, patches, warn(path));
|
|
3363
4113
|
} catch (error) {
|
|
3364
4114
|
throw new Error(`include patches could not be composed for ${filename}; child declarations are unavailable`, { cause: error });
|
|
3365
4115
|
}
|
|
@@ -3807,10 +4557,7 @@ async function recompose(ctx, patches, binName, options, withResolution) {
|
|
|
3807
4557
|
options: JSON.stringify(failure.entry.options),
|
|
3808
4558
|
diagnostic: failure.entry.fiber?.uid ? reloadDiagnostic(failure) : void 0
|
|
3809
4559
|
}));
|
|
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
|
-
}]);
|
|
4560
|
+
const previousFibers = [...ctx.loader.entries()].flatMap((row) => row.fiber === void 0 ? [] : [row.fiber]);
|
|
3814
4561
|
const { patches: previous = [], ...includeConfig } = entry.options.config;
|
|
3815
4562
|
const current = withResolution ? ctx.get("pluginPackages")?.resolution : void 0;
|
|
3816
4563
|
const successor = current instanceof ProfileRuntimeResolution ? await current.computeLatestResolution() : void 0;
|
|
@@ -3849,15 +4596,16 @@ async function recompose(ctx, patches, binName, options, withResolution) {
|
|
|
3849
4596
|
const prepared = prepareProfilePatches(ctx, patches, parentURL, binName);
|
|
3850
4597
|
applying = true;
|
|
3851
4598
|
const retried = [...new Set([...running, ...withResolution ? [...ctx.loader.entries()].filter(importFailed) : []])];
|
|
4599
|
+
const priorData = new Map(retried.map((row) => [row, row.parent.data]));
|
|
3852
4600
|
try {
|
|
3853
4601
|
await entry.update({ config: {
|
|
3854
4602
|
...includeConfig,
|
|
3855
4603
|
patches: prepared
|
|
3856
4604
|
} });
|
|
3857
4605
|
} finally {
|
|
3858
|
-
await resume(ctx, retried);
|
|
4606
|
+
await resume(ctx, retried.filter((row) => row.parent.data === priorData.get(row)));
|
|
3859
4607
|
}
|
|
3860
|
-
|
|
4608
|
+
await Promise.allSettled(previousFibers.map((fiber) => fiber.await()));
|
|
3861
4609
|
await ctx.loader.await();
|
|
3862
4610
|
const failures = await inactiveEntries(ctx);
|
|
3863
4611
|
const introduced = failures.filter((failure) => {
|
|
@@ -3867,7 +4615,6 @@ async function recompose(ctx, patches, binName, options, withResolution) {
|
|
|
3867
4615
|
return !previousFailures.some((previous) => sameFailure(previous, failure));
|
|
3868
4616
|
});
|
|
3869
4617
|
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
4618
|
ctx.emit("app-boot/config-reload");
|
|
3872
4619
|
return { warnings: failures.map(reloadDiagnostic) };
|
|
3873
4620
|
} finally {
|
|
@@ -3897,7 +4644,7 @@ function rowIds(patches) {
|
|
|
3897
4644
|
if ((row.group === true || row.name === "cordis:group") && Array.isArray(row.config)) visit(row.config);
|
|
3898
4645
|
}
|
|
3899
4646
|
};
|
|
3900
|
-
visit(
|
|
4647
|
+
visit(applyPatches([], patches, () => {}));
|
|
3901
4648
|
return ids;
|
|
3902
4649
|
}
|
|
3903
4650
|
/** Publish a generation unless it maps exactly what is published, which would only drop the package caches. */
|
|
@@ -4052,7 +4799,7 @@ function renderConfigDump(binName, absoluteConfigPath, layers, warn = (line) =>
|
|
|
4052
4799
|
const baseLabel = basename(absoluteConfigPath);
|
|
4053
4800
|
const base = parsed;
|
|
4054
4801
|
const snapshot = (count, warnings) => {
|
|
4055
|
-
return
|
|
4802
|
+
return applyPatches(base, structuredClone(layers.slice(0, count).flatMap((layer) => layer.patches)), (message, ...args) => {
|
|
4056
4803
|
let index = 0;
|
|
4057
4804
|
warnings.push(message.replace(/%C/g, () => JSON.stringify(args[index++])));
|
|
4058
4805
|
});
|
|
@@ -4269,6 +5016,8 @@ const FIBER_DISPOSED = 4;
|
|
|
4269
5016
|
*/
|
|
4270
5017
|
const requiredStartupEntryIds = new Set([
|
|
4271
5018
|
"agent-loop",
|
|
5019
|
+
"tool-executor-native",
|
|
5020
|
+
"tool-executor-ptc",
|
|
4272
5021
|
"webserver",
|
|
4273
5022
|
"web-runtime",
|
|
4274
5023
|
"modules",
|
|
@@ -4298,6 +5047,8 @@ function formatActivationError(error, stacks = true) {
|
|
|
4298
5047
|
/** Startup audit failure with non-enumerable metadata and original failures as its cause. */
|
|
4299
5048
|
var StartupError = class extends Error {
|
|
4300
5049
|
entries;
|
|
5050
|
+
/** Plugin states captured before disposal, excluding configuration values. */
|
|
5051
|
+
composition;
|
|
4301
5052
|
/** Root configuration and startup logs, attached by boot after disposal. */
|
|
4302
5053
|
startup;
|
|
4303
5054
|
/**
|
|
@@ -4310,7 +5061,8 @@ var StartupError = class extends Error {
|
|
|
4310
5061
|
this.entries = entries;
|
|
4311
5062
|
Object.defineProperties(this, {
|
|
4312
5063
|
entries: { enumerable: false },
|
|
4313
|
-
startup: { enumerable: false }
|
|
5064
|
+
startup: { enumerable: false },
|
|
5065
|
+
composition: { enumerable: false }
|
|
4314
5066
|
});
|
|
4315
5067
|
}
|
|
4316
5068
|
};
|
|
@@ -4524,14 +5276,6 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
4524
5276
|
try {
|
|
4525
5277
|
ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + "/";
|
|
4526
5278
|
ctx.provide("dshHomePath", dshHomePath);
|
|
4527
|
-
ctx.on("internal/update", (_config, _noSave, next) => {
|
|
4528
|
-
Promise.resolve(next()).catch((error) => {
|
|
4529
|
-
ctx.logger.error(error);
|
|
4530
|
-
});
|
|
4531
|
-
}, {
|
|
4532
|
-
global: true,
|
|
4533
|
-
prepend: true
|
|
4534
|
-
});
|
|
4535
5279
|
await ctx.plugin(Loader);
|
|
4536
5280
|
await prepare?.(ctx);
|
|
4537
5281
|
stage = "plugin tree failed to load";
|
|
@@ -4541,6 +5285,11 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
4541
5285
|
await auditStartupEntries(ctx, binName);
|
|
4542
5286
|
return ctx;
|
|
4543
5287
|
} catch (cause) {
|
|
5288
|
+
if (cause instanceof StartupError) try {
|
|
5289
|
+
cause.composition = captureStartupComposition(ctx);
|
|
5290
|
+
} catch (error) {
|
|
5291
|
+
ctx.logger.warn("Startup composition diagnostic unavailable", error);
|
|
5292
|
+
}
|
|
4544
5293
|
await ctx.fiber.dispose();
|
|
4545
5294
|
if (cause instanceof StartupError) {
|
|
4546
5295
|
cause.startup = {
|
|
@@ -4563,4 +5312,4 @@ async function boot(binName, absoluteConfigPath, patches, prepare, bareModuleBas
|
|
|
4563
5312
|
}
|
|
4564
5313
|
}
|
|
4565
5314
|
//#endregion
|
|
4566
|
-
export {
|
|
5315
|
+
export { FAIL_LOUD_RELEASE_TIMEOUT_MS, LOADER_EXPRESSION_SCHEMA, PROFILES_DIR, PROFILE_COMPATIBILITY_FILENAME, PROFILE_PATCH_FILENAME, PluginPackages, ProfileRuntimeResolution, ResolutionRestartRequired, StartupError, UnsupportedBundlePlatformError, assertBundlePlatform, 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, validatePluginVersionExemption, writeProfileBundles, writeProfileManifest };
|