@sparkelf/dsh-plus 0.2.4-alpha.47 → 0.2.5-alpha.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/bin.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import { i as readPatchSet, r as runApply } from "./apply-08huHfLG.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, spawn, spawnSync } from "node:child_process";
5
- import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
5
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join, posix, resolve, win32 } from "node:path";
7
7
  import { homedir } from "node:os";
8
- import semver from "semver";
8
+ import semver, { valid, validRange } from "semver";
9
9
  import { parseDocument } from "yaml";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { createServer } from "node:net";
@@ -75,7 +75,301 @@ function newerVersion(name, installed, registry = "https://registry.npmjs.org")
75
75
  return publishedVersions(name, registry).filter((entry) => semver.gt(entry.version, installed)).at(-1);
76
76
  }
77
77
  //#endregion
78
- //#region lib/types/standalone-capabilities.js
78
+ //#region lib/types/manifest.js
79
+ /**
80
+ * Derive the profile's override table from the distribution's own facts.
81
+ *
82
+ * The distribution records one truth per fact: the patch set names the official packages it
83
+ * patches, the runtime names the version its republished builds carry, and an edition names the
84
+ * capabilities a deployment omits. The override table npm and pnpm read is a projection of those
85
+ * three, never a fourth list: an entry that cannot be derived fails the build instead of being
86
+ * carried forward, which is what kept a stale `@deepseek-ai/dsh-client-ui-slots` pin alive long
87
+ * after the patch behind it had gone.
88
+ *
89
+ * @module manifest
90
+ */
91
+ /** npm scope every patched official package is republished under. */
92
+ const OFFICIAL_PREFIX = "@deepseek-ai/";
93
+ /** npm scope the distribution publishes its republished builds under. */
94
+ const REPUBLISHED_PREFIX = "@sparkelf/";
95
+ function record(value, label) {
96
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be an object");
97
+ return value;
98
+ }
99
+ /** A declared value as text, or the fallback when the manifest holds something else. */
100
+ function text(value, fallback = "") {
101
+ return typeof value === "string" ? value : fallback;
102
+ }
103
+ function stringArray(value, label) {
104
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string" || entry === "")) throw new Error(label + " must be a non-empty string array");
105
+ return value;
106
+ }
107
+ function stringMap(value, label) {
108
+ const result = {};
109
+ for (const [name, spec] of Object.entries(record(value, label))) {
110
+ if (typeof spec !== "string" || spec === "") throw new Error(label + "." + name + " must be a non-empty string");
111
+ result[name] = spec;
112
+ }
113
+ return result;
114
+ }
115
+ /**
116
+ * The republished scope name for one official package.
117
+ *
118
+ * @param official - an `@deepseek-ai/dsh-*` name.
119
+ * @returns the `@sparkelf/dsh-*` name the distribution publishes it as.
120
+ */
121
+ function republishedName(official) {
122
+ if (!official.startsWith(OFFICIAL_PREFIX)) throw new Error(official + " is not an official package name");
123
+ return REPUBLISHED_PREFIX + official.slice(13);
124
+ }
125
+ /**
126
+ * Derive the redirect table from the patch set.
127
+ *
128
+ * Every `dsh-source` entry republishes the packages its payload modifies, because a registry
129
+ * installation has no source tree to patch. An entry that changes only types sets
130
+ * `target.republish: false` and must say why; anything else would look like a redirect that
131
+ * silently never happens.
132
+ *
133
+ * @param entries - the distribution's patch entries.
134
+ * @param runtimeVersion - exact version the republished builds carry.
135
+ * @returns official name to `npm:` alias spec.
136
+ */
137
+ function deriveRedirects(entries, runtimeVersion) {
138
+ const redirects = {};
139
+ const ids = {};
140
+ for (const entry of entries) {
141
+ if (entry.target.kind !== "dsh-source") continue;
142
+ const packages = entry.target.packages ?? [];
143
+ if (packages.length === 0) throw new Error("patch entry " + entry.id + " declares no target packages");
144
+ if (!(entry.target.republish !== false)) {
145
+ if (entry.target.reason === void 0 || entry.target.reason.trim() === "") throw new Error("patch entry " + entry.id + " sets republish:false without a reason");
146
+ continue;
147
+ }
148
+ for (const official of packages) {
149
+ const spec = "npm:" + republishedName(official) + "@" + runtimeVersion;
150
+ if (redirects[official] !== void 0 && redirects[official] !== spec) throw new Error("patch entries " + (ids[official] ?? "?") + " and " + entry.id + " redirect " + official + " to different targets");
151
+ redirects[official] = spec;
152
+ ids[official] = entry.id;
153
+ }
154
+ }
155
+ if (Object.keys(redirects).length === 0) throw new Error("the patch set declares no republishable target");
156
+ return {
157
+ redirects,
158
+ ids
159
+ };
160
+ }
161
+ /** Read and validate the facts one installed distribution declares. */
162
+ function readDistributionFacts(directory) {
163
+ const manifest = record(JSON.parse(readFileSync(join(directory, "package.json"), "utf8")), "distribution manifest");
164
+ const plus = record(manifest.dshPlus, "dshPlus");
165
+ const profile = record(plus.profile, "dshPlus.profile");
166
+ if (profile.overrides !== void 0) throw new Error("dshPlus.profile.overrides is retired; overrides are derived from dshPlus.patchSet");
167
+ const runtimeVersion = text(record(plus.runtime, "dshPlus.runtime").version);
168
+ if (valid(runtimeVersion) === null) throw new Error("dshPlus.runtime.version must be an exact version, got: " + runtimeVersion);
169
+ const omission = record(plus.omission, "dshPlus.omission");
170
+ const omissionPackage = text(omission.package);
171
+ const omissionVersion = text(omission.version);
172
+ if (!omissionPackage.startsWith("@sparkelf/") || valid(omissionVersion) === null) throw new Error("dshPlus.omission must name an @sparkelf package and an exact version");
173
+ if (validRange(text(record(plus.compatibility, "dshPlus.compatibility").dsh)) === null) throw new Error("dshPlus.compatibility.dsh must be a semver range");
174
+ const rawEntries = record(plus.patchSet, "dshPlus.patchSet").entries;
175
+ if (!Array.isArray(rawEntries) || rawEntries.length === 0) throw new Error("dshPlus.patchSet.entries must be a non-empty array");
176
+ const { redirects, ids } = deriveRedirects(rawEntries.map((value, index) => {
177
+ const entry = record(value, "patchSet.entries[" + String(index) + "]");
178
+ const target = record(entry.target, "patchSet.entries[" + String(index) + "].target");
179
+ const targetFacts = { kind: text(target.kind) };
180
+ if (target.packages !== void 0) Object.assign(targetFacts, { packages: stringArray(target.packages, "target.packages") });
181
+ if (target.republish === false) Object.assign(targetFacts, { republish: false });
182
+ if (typeof target.reason === "string") Object.assign(targetFacts, { reason: target.reason });
183
+ return {
184
+ id: text(entry.id),
185
+ target: targetFacts
186
+ };
187
+ }), runtimeVersion);
188
+ const dependencies = stringMap(profile.dependencies, "dshPlus.profile.dependencies");
189
+ const allowBuilds = {};
190
+ for (const [name, allowed] of Object.entries(record(profile.allowBuilds, "dshPlus.profile.allowBuilds"))) {
191
+ if (typeof allowed !== "boolean") throw new Error("dshPlus.profile.allowBuilds." + name + " must be a boolean");
192
+ allowBuilds[name] = allowed;
193
+ }
194
+ const editions = {};
195
+ const rawEditions = plus.editions === void 0 ? {} : record(plus.editions, "dshPlus.editions");
196
+ for (const [id, value] of Object.entries(rawEditions)) {
197
+ const edition = record(value, "dshPlus.editions." + id);
198
+ const rawPackages = edition.packages;
199
+ if (rawPackages === void 0) throw new Error("dshPlus.editions." + id + ".packages is required");
200
+ const names = Array.isArray(rawPackages) ? stringArray(rawPackages, "dshPlus.editions." + id + ".packages") : Object.keys(record(rawPackages, "dshPlus.editions." + id + ".packages"));
201
+ for (const name of names) if (dependencyKey(dependencies, name) === void 0) throw new Error("edition \"" + id + "\" installs " + name + ", which dshPlus.profile.dependencies does not pin");
202
+ const omit = stringArray(edition.omit ?? [], "dshPlus.editions." + id + ".omit");
203
+ for (const name of omit) if (dependencies[name] === void 0 && manifest.dependencies?.[name] === void 0) throw new Error("edition \"" + id + "\" omits " + name + ", which the distribution does not declare");
204
+ const excludePackages = stringArray(edition.excludePackages ?? [], "dshPlus.editions." + id + ".excludePackages");
205
+ for (const name of excludePackages) {
206
+ if (dependencies[name] === void 0 && !stringArray(edition.bundles ?? [], "x").includes(name)) throw new Error("edition \"" + id + "\" excludes " + name + ", which it does not install or mount");
207
+ if (omit.includes(name)) throw new Error("edition \"" + id + "\" both excludes and omits " + name);
208
+ for (const [other, spec] of Object.entries(dependencies)) if (/^npm:/u.test(spec) && spec.includes(name + "@")) throw new Error("edition \"" + id + "\" excludes " + name + " but " + other + " aliases it");
209
+ }
210
+ const bundles = stringArray(edition.bundles ?? [], "dshPlus.editions." + id + ".bundles");
211
+ for (const name of bundles) if (omit.includes(name)) throw new Error("edition \"" + id + "\" mounts " + name + " but also omits it");
212
+ editions[id] = {
213
+ label: text(edition.label, id),
214
+ profile: text(edition.profile, "plus"),
215
+ bundles,
216
+ packages: names,
217
+ omit,
218
+ excludePackages
219
+ };
220
+ }
221
+ return {
222
+ name: String(manifest.name),
223
+ version: String(manifest.version),
224
+ runtimeVersion,
225
+ omissionSpec: "npm:" + omissionPackage + "@" + omissionVersion,
226
+ bundles: stringArray(profile.bundles, "dshPlus.profile.bundles"),
227
+ dependencies,
228
+ allowBuilds,
229
+ redirects,
230
+ redirectIds: ids,
231
+ editions
232
+ };
233
+ }
234
+ /**
235
+ * The dependency-table key that installs one package.
236
+ *
237
+ * A bundle published under a scope we own is installed through an alias: the official name a
238
+ * consumer imports (`dsh-better-sidebar`) is the key, and the spec names our build
239
+ * (`npm:@sparkelf/dsh-better-sidebar@…`). Editions name the real package, so the
240
+ * lookup has to follow the alias or it reports a reviewed package as unpinned.
241
+ *
242
+ * @param dependencies - the distribution's dependency table.
243
+ * @param name - package name an edition names.
244
+ * @returns the key that installs it, or undefined when the distribution does not carry it.
245
+ */
246
+ function dependencyKey(dependencies, name) {
247
+ if (dependencies[name] !== void 0) return name;
248
+ for (const [key, spec] of Object.entries(dependencies)) if (/^npm:(@[^/]+\/[^@]+)@/u.exec(spec)?.[1] === name) return key;
249
+ }
250
+ /**
251
+ * Project an edition into the profile npm and pnpm install.
252
+ *
253
+ * The distribution's reviewed order is the base; the edition names the bundle order and package
254
+ * set it installs and the capabilities it must not run. Versions always come from
255
+ * `profile.dependencies`, so an edition cannot pin a version the distribution did not review.
256
+ * No edition means the full distribution, which is what a launcher start without a selection
257
+ * materializes.
258
+ *
259
+ * @param facts - validated distribution facts.
260
+ * @param editionId - edition to project, or undefined for the full distribution.
261
+ * @returns the profile's dependencies, bundles, and override table.
262
+ */
263
+ function projectEdition(facts, editionId, extraExcludes = []) {
264
+ const edition = editionId === void 0 ? void 0 : facts.editions[editionId];
265
+ if (editionId !== void 0 && edition === void 0) throw new Error("unknown edition \"" + editionId + "\"; declared: " + (Object.keys(facts.editions).join(", ") || "(none)"));
266
+ const excludedNames = new Set([...edition?.excludePackages ?? [], ...extraExcludes]);
267
+ for (const name of excludedNames) if (!(facts.dependencies[name] !== void 0 || facts.bundles.includes(name))) throw new Error("exclude names " + name + ", which the distribution does not install or mount");
268
+ const omittedNames = new Set(edition?.omit ?? []);
269
+ const omitted = {};
270
+ for (const name of omittedNames) omitted[name] = facts.omissionSpec;
271
+ const dependencies = {};
272
+ const sharedOfficials = Object.keys(facts.dependencies).filter((name) => name.startsWith("@deepseek-ai/dsh"));
273
+ const packageNames = edition === void 0 ? [...new Set([...Object.keys(facts.dependencies), ...sharedOfficials])] : [...new Set([...edition.packages, ...sharedOfficials])];
274
+ for (const name of packageNames) {
275
+ if (omittedNames.has(name) || excludedNames.has(name)) continue;
276
+ const key = dependencyKey(facts.dependencies, name) ?? name;
277
+ const spec = facts.dependencies[key];
278
+ if (spec === void 0) throw new Error(name + " is not a reviewed package");
279
+ dependencies[key] = spec;
280
+ }
281
+ for (const [name, spec] of Object.entries(facts.redirects)) {
282
+ if (omittedNames.has(name) || excludedNames.has(name)) continue;
283
+ dependencies[name] = spec;
284
+ }
285
+ const bundles = (edition === void 0 ? [...facts.bundles] : [...new Set(edition.bundles)]).filter((name) => !excludedNames.has(name));
286
+ for (const name of bundles) if (!facts.bundles.includes(name) && !(edition?.bundles ?? []).includes(name)) throw new Error("edition \"" + String(editionId) + "\" mounts bundle " + name + ", which the distribution does not declare");
287
+ const overrides = {};
288
+ for (const [name, spec] of Object.entries(facts.redirects)) {
289
+ if (omittedNames.has(name) || excludedNames.has(name)) continue;
290
+ overrides[name] = spec;
291
+ }
292
+ overrides["@deepseek-ai/dsh"] = facts.runtimeVersion;
293
+ Object.assign(overrides, omitted);
294
+ return {
295
+ profileName: edition?.profile ?? "plus",
296
+ dependencies,
297
+ bundles,
298
+ overrides,
299
+ allowBuilds: facts.allowBuilds,
300
+ omittedPackages: omitted
301
+ };
302
+ }
303
+ /**
304
+ * Assert every redirect target is served by a registry.
305
+ *
306
+ * A redirect names a published build; one that names a version the registry does not serve
307
+ * installs nothing at all, and the failure surfaces as a resolver error that reads like a
308
+ * packaging defect rather than a release that never happened.
309
+ *
310
+ * @param redirects - official name to `npm:` alias spec.
311
+ * @param registry - npm registry base URL.
312
+ * @returns nothing; throws on the first unavailable target.
313
+ */
314
+ async function assertRedirectsPublished(redirects, registry = "https://registry.npmjs.org") {
315
+ for (const spec of Object.values(redirects)) {
316
+ const match = /^npm:(@[^/]+\/[^@]+)@(.+)$/u.exec(spec);
317
+ if (match === null) throw new Error("redirect " + spec + " is not an npm alias");
318
+ const [, name, version] = match;
319
+ if (name === void 0 || version === void 0) throw new Error("redirect " + spec + " is not an npm alias");
320
+ if (!(await fetch(registry.replace(/\/$/u, "") + "/" + name.replace("/", "%2f") + "/" + version)).ok) throw new Error("redirect target " + name + "@" + version + " is not served by " + registry);
321
+ }
322
+ }
323
+ /**
324
+ * Write the profile manifest and the pnpm workspace the install reads.
325
+ *
326
+ * npm reads `overrides` from the root manifest and pnpm reads them from the workspace file, so
327
+ * both are written from the same projection. Resolver settings a container needs (store location,
328
+ * supported architectures, release-age window) belong to the caller and are appended to the
329
+ * workspace file afterwards.
330
+ *
331
+ * @param directory - profile directory to write into.
332
+ * @param facts - validated distribution facts.
333
+ * @param editionId - edition to project.
334
+ * @param extraExcludes - packages the caller drops on top of the edition's own exclusions.
335
+ * @returns the projection that was written.
336
+ */
337
+ function writeProfileProjection(directory, facts, editionId, extraExcludes = []) {
338
+ const projection = projectEdition(facts, editionId, extraExcludes);
339
+ mkdirSync(directory, { recursive: true });
340
+ const dependencies = {
341
+ "@sparkelf/dsh-plus": facts.version,
342
+ "@deepseek-ai/dsh": facts.runtimeVersion,
343
+ ...projection.dependencies
344
+ };
345
+ writeFileSync(join(directory, "package.json"), JSON.stringify({
346
+ name: "dsh-profile-" + projection.profileName,
347
+ private: true,
348
+ type: "module",
349
+ dependencies,
350
+ dsh: { profile: {
351
+ bundles: projection.bundles,
352
+ patchReload: "live"
353
+ } }
354
+ }, null, 2) + "\n");
355
+ const workspace = [
356
+ "packages:",
357
+ " - .",
358
+ "nodeLinker: hoisted",
359
+ "autoInstallPeers: false",
360
+ "",
361
+ "overrides:",
362
+ ...Object.keys(projection.overrides).sort().map((name) => " " + JSON.stringify(name) + ": " + (projection.overrides[name] ?? "")),
363
+ "",
364
+ "allowBuilds:",
365
+ ...Object.keys(projection.allowBuilds).sort().map((name) => " " + JSON.stringify(name) + ": " + String(projection.allowBuilds[name])),
366
+ ""
367
+ ];
368
+ writeFileSync(join(directory, "pnpm-workspace.yaml"), workspace.join("\n"));
369
+ return projection;
370
+ }
371
+ //#endregion
372
+ //#region lib/types/capabilities.js
79
373
  /** Capabilities offered at install time, in interview order. */
80
374
  const CAPABILITIES = [
81
375
  {
@@ -498,7 +792,7 @@ function insertCapabilityRows(state, rows, withSelector) {
498
792
  * @param home - the deployment home, where generated service units are recorded.
499
793
  * @returns the ids whose service is ready.
500
794
  */
501
- async function installCapabilityServices(answers, home) {
795
+ function installCapabilityServices(answers, home) {
502
796
  const enabled = new Set(answers.enabled);
503
797
  const ready = [];
504
798
  mkdirSync(home, { recursive: true });
@@ -634,7 +928,7 @@ function desktopDriverAvailable() {
634
928
  return spawnSync("cua-driver", ["--version"], { stdio: "ignore" }).status === 0;
635
929
  }
636
930
  //#endregion
637
- //#region lib/types/standalone-server.js
931
+ //#region lib/types/server.js
638
932
  /**
639
933
  * Process lifecycle for one standalone Plus server.
640
934
  *
@@ -790,7 +1084,7 @@ async function waitForAuthenticatedUrl(logPath, timeoutMilliseconds) {
790
1084
  }
791
1085
  }
792
1086
  //#endregion
793
- //#region lib/types/standalone-profile.js
1087
+ //#region lib/types/profile.js
794
1088
  /**
795
1089
  * Runtime paths and profile composition for a standalone Plus installation.
796
1090
  *
@@ -801,67 +1095,31 @@ async function waitForAuthenticatedUrl(logPath, timeoutMilliseconds) {
801
1095
  * the launcher mounts exactly the bundles the profile names and nothing expands a
802
1096
  * bundle's own list.
803
1097
  */
804
- /** Profile name a standalone installation owns when its package declares none. */
805
- const STANDALONE_PROFILE = "plus";
1098
+ /** Profile name the launcher materializes when no edition is selected. */
1099
+ const DEFAULT_PROFILE = "plus";
1100
+ function requireRecord(value, label) {
1101
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be an object");
1102
+ return value;
1103
+ }
806
1104
  /**
807
- * Read the deployment facts the installing package declares.
1105
+ * Resolve the file that was actually invoked from the path the shell was handed.
808
1106
  *
809
- * A standalone package describes the profile it materializes and the capabilities it
810
- * omits, so a reduced variant owns a differently named profile and a reduced install
811
- * without the distribution naming either. The declaration travels with the package
812
- * because npm resolves the tree long before any command of ours runs.
1107
+ * A global npm install links `<prefix>/bin/dsh-plus` to the package entry. Resolving
1108
+ * `@sparkelf/dsh-plus` from that link starts at the bin directory, which has no
1109
+ * `node_modules`, so every globally installed command reported the distribution as
1110
+ * missing. The real path is the package file, and the package resolves its own name.
813
1111
  *
814
- * @param anchor - path inside the installing package's tree.
815
- * @param fallback - profile name to use when the declaration is absent.
816
- * @returns the declared profile name and omitted capabilities.
1112
+ * @param invoked - `process.argv[1]`, or undefined when node ran without a script.
1113
+ * @param fallback - this module's own file, used when there is no script path.
1114
+ * @returns an absolute path that exists, or the input when it cannot be resolved.
817
1115
  */
818
- function readStandaloneDeclaration(anchor, fallback = STANDALONE_PROFILE) {
819
- let current = resolve(anchor);
820
- for (;;) {
821
- const declared = readStandaloneFacts(join(current, "package.json"));
822
- if (declared !== void 0) return declared;
823
- const parent = dirname(current);
824
- if (parent === current) return {
825
- profileName: fallback,
826
- omittedPackages: {}
827
- };
828
- current = parent;
829
- }
830
- }
831
- /** Read one manifest's standalone declaration, or undefined when it carries none. */
832
- function readStandaloneFacts(manifestPath) {
833
- if (!existsSync(manifestPath)) return void 0;
834
- let standalone;
1116
+ function resolveInvocationAnchor(invoked, fallback) {
1117
+ if (invoked === void 0 || invoked === "") return realpathSync(fallback);
835
1118
  try {
836
- standalone = JSON.parse(readFileSync(manifestPath, "utf8")).dshPlusStandalone;
1119
+ return realpathSync(invoked);
837
1120
  } catch {
838
- return;
839
- }
840
- const profile = standalone?.profile;
841
- if (standalone === void 0 || typeof profile !== "string" || profile === "") return void 0;
842
- const rawOmitted = standalone.omittedPackages;
843
- const omittedPackages = {};
844
- if (rawOmitted !== void 0 && typeof rawOmitted === "object" && !Array.isArray(rawOmitted)) for (const [name, spec] of Object.entries(rawOmitted)) {
845
- if (typeof spec !== "string" || spec === "") throw new Error("dshPlusStandalone.omittedPackages." + name + " must be a non-empty string");
846
- omittedPackages[name] = spec;
1121
+ return resolve(invoked);
847
1122
  }
848
- return {
849
- profileName: profile,
850
- omittedPackages
851
- };
852
- }
853
- function requireRecord(value, label) {
854
- if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be an object");
855
- return value;
856
- }
857
- function requireStringArray(value, label) {
858
- if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(label + " must be a string array");
859
- return value;
860
- }
861
- /** Read one non-empty string field. */
862
- function requireString(value, label) {
863
- if (typeof value !== "string" || value.trim() === "") throw new Error(label + " must be a non-empty string");
864
- return value;
865
1123
  }
866
1124
  /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
867
1125
  function resolveHome(env = process.env) {
@@ -900,12 +1158,11 @@ function isWithin(candidate, parent, platform = process.platform) {
900
1158
  * @returns absolute path to the installed distribution.
901
1159
  */
902
1160
  function resolveDistributionDirectory(anchor) {
903
- const ownDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
904
1161
  let resolved;
905
1162
  try {
906
1163
  resolved = dirname(createRequire(anchor).resolve("@sparkelf/dsh-plus/package.json"));
907
1164
  } catch {
908
- return ownDirectory;
1165
+ throw new Error("@sparkelf/dsh-plus is not installed in " + dirname(anchor) + "; run this command from the directory that installed it");
909
1166
  }
910
1167
  let current = resolve(dirname(anchor));
911
1168
  for (;;) {
@@ -914,56 +1171,25 @@ function resolveDistributionDirectory(anchor) {
914
1171
  if (parent === current) break;
915
1172
  current = parent;
916
1173
  }
917
- return ownDirectory;
1174
+ throw new Error("@sparkelf/dsh-plus resolved outside " + resolve(dirname(anchor)) + " (found " + resolved + "); run this command from the directory that installed it");
918
1175
  }
919
- function readDistributionProfile(distributionDirectory) {
920
- const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, "package.json"), "utf8")), "Plus distribution manifest");
921
- const plus = requireRecord(manifest.dshPlus, "dshPlus");
922
- const profile = requireRecord(plus.profile, "dshPlus.profile");
923
- const dependencies = requireDependencyMap(requireRecord(profile.dependencies, "dshPlus.profile.dependencies"), "dshPlus.profile.dependencies");
924
- const rawAllowBuilds = requireRecord(profile.allowBuilds, "dshPlus.profile.allowBuilds");
925
- const allowBuilds = {};
926
- for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
927
- if (typeof allowed !== "boolean") throw new Error("dshPlus.profile.allowBuilds." + name + " must be a boolean");
928
- allowBuilds[name] = allowed;
929
- }
930
- const overrides = {};
931
- const rawOverrides = profile.overrides === void 0 ? {} : requireRecord(profile.overrides, "dshPlus.profile.overrides");
932
- for (const [name, spec] of Object.entries(rawOverrides)) {
933
- if (typeof spec !== "string" || spec === "") throw new Error("dshPlus.profile.overrides." + name + " must be a non-empty string");
934
- overrides[name] = spec;
935
- }
936
- const editions = {};
937
- const rawEditions = plus.editions === void 0 ? {} : requireRecord(plus.editions, "dshPlus.editions");
938
- for (const [id, value] of Object.entries(rawEditions)) {
939
- const edition = requireRecord(value, "dshPlus.editions." + id);
940
- editions[id] = {
941
- label: requireString(edition.label, "dshPlus.editions." + id + ".label"),
942
- bundles: requireStringArray(edition.bundles, "dshPlus.editions." + id + ".bundles"),
943
- packages: requireDependencyMap(requireRecord(edition.packages, "dshPlus.editions." + id + ".packages"), "dshPlus.editions." + id + ".packages")
944
- };
945
- }
946
- const compatibility = requireRecord(plus.compatibility, "dshPlus.compatibility");
1176
+ /** The reviewed bundle order and pins the installed distribution declares, for one variant. */
1177
+ function readDistributionProfile(distributionDirectory, editionId, extraExcludes = []) {
1178
+ const facts = readDistributionFacts(distributionDirectory);
1179
+ const projection = projectEdition(facts, editionId, extraExcludes);
947
1180
  return {
948
- name: String(manifest.name),
949
- bundles: requireStringArray(profile.bundles, "dshPlus.profile.bundles"),
950
- dependencies,
951
- allowBuilds,
952
- overrides,
953
- editions,
954
- dshRange: String(compatibility.dsh),
955
- version: String(manifest.version)
1181
+ name: facts.name,
1182
+ version: facts.version,
1183
+ dshRange: ">=" + facts.runtimeVersion,
1184
+ runtimeVersion: facts.runtimeVersion,
1185
+ bundles: projection.bundles,
1186
+ dependencies: projection.dependencies,
1187
+ allowBuilds: projection.allowBuilds,
1188
+ overrides: projection.overrides,
1189
+ omittedPackages: projection.omittedPackages,
1190
+ editions: facts.editions
956
1191
  };
957
1192
  }
958
- /** Read one package-name-to-spec map, rejecting empty entries. */
959
- function requireDependencyMap(value, label) {
960
- const dependencies = {};
961
- for (const [name, spec] of Object.entries(value)) {
962
- if (typeof spec !== "string" || spec === "") throw new Error(label + "." + name + " must be a non-empty string");
963
- dependencies[name] = spec;
964
- }
965
- return dependencies;
966
- }
967
1193
  /**
968
1194
  * Give the profile its own installed tree, built from the distribution's declarations.
969
1195
  *
@@ -980,22 +1206,131 @@ function requireDependencyMap(value, label) {
980
1206
  * twice; `dsh-plus apply` remains the command that reinstalls after a change.
981
1207
  *
982
1208
  * @param paths - resolved standalone paths.
983
- * @param consumerDirectory - directory whose `node_modules` holds the installation.
984
1209
  */
985
- function installProfilePackages(paths, consumerDirectory) {
986
- if (!existsSync(join(consumerDirectory, "node_modules"))) {
987
- installWithRegistryFallback(paths);
988
- alignReplacedPackageNames(join(paths.profileDirectory, "node_modules"));
989
- return;
990
- }
1210
+ function installProfilePackages(paths, omitted = /* @__PURE__ */ new Set(), runtimeVersion, registry) {
991
1211
  const profileModules = join(paths.profileDirectory, "node_modules");
992
1212
  if (existsSync(profileModules)) {
993
1213
  linkNestedBundles(paths, profileModules);
994
1214
  return;
995
1215
  }
996
- installWithRegistryFallback(paths);
997
- alignReplacedPackageNames(profileModules);
1216
+ installWithRegistryFallback(paths.profileDirectory);
1217
+ if (runtimeVersion !== void 0 && registry !== void 0) alignOfficialVersions(paths.profileDirectory, runtimeVersion, registry);
1218
+ alignReplacedPackageNames(profileModules, omitted);
998
1219
  linkNestedBundles(paths, profileModules);
1220
+ assertProfileBundles(paths.profileDirectory);
1221
+ }
1222
+ /** Package names a package declares as peers on the official runtime family. */
1223
+ function officialPeers(profileDirectory) {
1224
+ const peers = /* @__PURE__ */ new Set();
1225
+ const manifests = [];
1226
+ for (const scope of [join(profileDirectory, "node_modules"), join(profileDirectory, "node_modules", "@deepseek-ai")]) {
1227
+ if (!existsSync(scope)) continue;
1228
+ for (const entry of readdirSync(scope)) {
1229
+ if (entry.startsWith(".")) continue;
1230
+ if (entry.startsWith("@") && entry !== "@deepseek-ai") continue;
1231
+ if (entry === "@deepseek-ai") {
1232
+ for (const scoped of readdirSync(scope)) manifests.push(join(scope, scoped, "package.json"));
1233
+ continue;
1234
+ }
1235
+ manifests.push(join(scope, entry, "package.json"));
1236
+ }
1237
+ }
1238
+ for (const manifestPath of manifests) {
1239
+ if (!existsSync(manifestPath)) continue;
1240
+ try {
1241
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
1242
+ for (const peer of Object.keys(manifest.peerDependencies ?? {})) if (peer.startsWith("@deepseek-ai/dsh") && peer !== "@deepseek-ai/dsh") peers.add(peer);
1243
+ } catch {}
1244
+ }
1245
+ return peers;
1246
+ }
1247
+ /**
1248
+ * Which of the given package versions a registry serves.
1249
+ *
1250
+ * The probe runs in one Node child: one process per name is slow, and the caller needs the
1251
+ * answer before the second install.
1252
+ *
1253
+ * @param names - package names to check.
1254
+ * @param version - exact version each must serve.
1255
+ * @param registry - registry base URL.
1256
+ * @returns the names the registry serves at that version.
1257
+ */
1258
+ function servedVersions(names, version, registry) {
1259
+ if (names.length === 0) return /* @__PURE__ */ new Set();
1260
+ const result = spawnSync(process.execPath, [
1261
+ "--input-type=module",
1262
+ "-e",
1263
+ `
1264
+ const [names, version, registry] = JSON.parse(process.argv[1])
1265
+ const base = registry.replace(/\\/$/, '')
1266
+ const queue = [...names]
1267
+ const served = []
1268
+ await Promise.all(Array.from({ length: 10 }, async () => {
1269
+ while (queue.length > 0) {
1270
+ const name = queue.shift()
1271
+ try {
1272
+ const response = await fetch(base + '/' + name.replace('/', '%2f') + '/' + version)
1273
+ if (response.ok) served.push(name)
1274
+ } catch {}
1275
+ }
1276
+ }))
1277
+ process.stdout.write(served.join('\\n'))
1278
+ `,
1279
+ JSON.stringify([
1280
+ names,
1281
+ version,
1282
+ registry
1283
+ ])
1284
+ ], { encoding: "utf8" });
1285
+ if (result.status !== 0) return /* @__PURE__ */ new Set();
1286
+ return new Set(result.stdout.split("\n").map((entry) => entry.trim()).filter((entry) => entry !== ""));
1287
+ }
1288
+ /**
1289
+ * Move the shared official services onto the runtime's exact version.
1290
+ *
1291
+ * The republished builds were cut against one runtime, and DSH disables a row when the exact
1292
+ * peers a plugin declares disagree with the process it shares. Plugins declare those services as
1293
+ * peers, so the names worth pinning are the peers actually installed -- read from the tree and
1294
+ * checked against the registry. The rest of the official family keeps its resolved versions:
1295
+ * pinning everything traded one mismatch for a tree with holes.
1296
+ *
1297
+ * @param profileDirectory - profile whose tree was just installed.
1298
+ * @param runtimeVersion - exact runtime the profile boots.
1299
+ * @param registry - registry the profile installs from.
1300
+ * @returns how many services were realigned.
1301
+ */
1302
+ function alignOfficialVersions(profileDirectory, runtimeVersion, registry) {
1303
+ const served = servedVersions([...officialPeers(profileDirectory)], runtimeVersion, registry);
1304
+ if (served.size === 0) return 0;
1305
+ const workspacePath = join(profileDirectory, "pnpm-workspace.yaml");
1306
+ const document = parseDocument(readFileSync(workspacePath, "utf8"));
1307
+ let changed = 0;
1308
+ for (const name of served) {
1309
+ if (document.getIn(["overrides", name]) === runtimeVersion) continue;
1310
+ document.setIn(["overrides", name], runtimeVersion);
1311
+ changed += 1;
1312
+ }
1313
+ if (changed === 0) return 0;
1314
+ writeFileSync(workspacePath, String(document));
1315
+ rmSync(join(profileDirectory, "pnpm-lock.yaml"), { force: true });
1316
+ installWithRegistryFallback(profileDirectory);
1317
+ return changed;
1318
+ }
1319
+ /**
1320
+ * Assert every bundle row the profile declares resolves in its installed tree.
1321
+ *
1322
+ * A row without an installed package mounts nothing and reports nothing: the client module
1323
+ * system simply never registers it, and every earlier step passes. Reading the rows back from
1324
+ * the manifest on disk rather than from the projection is what makes this cover the tree the
1325
+ * launcher actually boots.
1326
+ *
1327
+ * @param profileDirectory - profile whose node_modules was just installed.
1328
+ */
1329
+ function assertProfileBundles(profileDirectory) {
1330
+ const manifest = JSON.parse(readFileSync(join(profileDirectory, "package.json"), "utf8"));
1331
+ const missing = [];
1332
+ for (const name of manifest.dsh?.profile?.bundles ?? []) if (!existsSync(join(profileDirectory, "node_modules", name, "package.json"))) missing.push(name);
1333
+ if (missing.length > 0) throw new Error("the profile mounts bundles it did not install: " + missing.join(", "));
999
1334
  }
1000
1335
  /**
1001
1336
  * Install the profile from the first registry that answers.
@@ -1006,20 +1341,18 @@ function installProfilePackages(paths, consumerDirectory) {
1006
1341
  *
1007
1342
  * @param paths - resolved standalone paths.
1008
1343
  */
1009
- function installWithRegistryFallback(paths) {
1344
+ function installWithRegistryFallback(profileDirectory) {
1010
1345
  const registries = registryOrder();
1011
1346
  for (const [index, registry] of registries.entries()) {
1012
- if (index > 0) rmSync(join(paths.profileDirectory, "pnpm-lock.yaml"), { force: true });
1013
1347
  const result = spawnSync("pnpm", [
1014
1348
  "install",
1015
1349
  "--no-frozen-lockfile",
1016
1350
  "--registry",
1017
1351
  registry
1018
1352
  ], {
1019
- cwd: paths.profileDirectory,
1353
+ cwd: profileDirectory,
1020
1354
  stdio: "inherit",
1021
- shell: process.platform === "win32",
1022
- timeout: 5 * 6e4
1355
+ shell: process.platform === "win32"
1023
1356
  });
1024
1357
  if (result.error !== void 0) throw result.error;
1025
1358
  if (result.status === 0) return;
@@ -1044,22 +1377,37 @@ function installWithRegistryFallback(paths) {
1044
1377
  *
1045
1378
  * @param profileModules - the profile's \`node_modules\` directory.
1046
1379
  */
1047
- function alignReplacedPackageNames(profileModules) {
1048
- const scoped = join(profileModules, "@deepseek-ai");
1049
- if (!existsSync(scoped)) return;
1050
- for (const entry of readdirSync(scoped)) {
1051
- const manifestPath = join(scoped, entry, "package.json");
1052
- if (!existsSync(manifestPath)) continue;
1380
+ function alignReplacedPackageNames(profileModules, omitted = /* @__PURE__ */ new Set()) {
1381
+ if (!existsSync(profileModules)) return;
1382
+ const align = (expected, manifestPath) => {
1383
+ if (!existsSync(manifestPath)) return;
1053
1384
  const declared = JSON.parse(readFileSync(manifestPath, "utf8"));
1054
- const expected = "@deepseek-ai/" + entry;
1055
- if (declared.name === expected) continue;
1056
- const replaced = {
1385
+ if (declared.name === expected || omitted.has(expected)) return;
1386
+ const temporary = manifestPath + ".dsh-name";
1387
+ writeFileSync(temporary, JSON.stringify({
1057
1388
  ...declared,
1058
1389
  name: expected
1059
- };
1060
- const temporary = manifestPath + ".dsh-name";
1061
- writeFileSync(temporary, JSON.stringify(replaced, null, 2) + "\n");
1390
+ }, null, 2) + "\n");
1062
1391
  renameSync(temporary, manifestPath);
1392
+ };
1393
+ for (const entry of readdirSync(profileModules)) {
1394
+ if (entry.startsWith(".")) continue;
1395
+ if (!entry.startsWith("@")) {
1396
+ const manifestPath = join(profileModules, entry, "package.json");
1397
+ if (!existsSync(manifestPath)) continue;
1398
+ const declared = JSON.parse(readFileSync(manifestPath, "utf8"));
1399
+ if (declared.name === void 0 || declared.name === entry) continue;
1400
+ const [scope, name] = declared.name.split("/");
1401
+ if (scope === void 0 || name === void 0) continue;
1402
+ mkdirSync(join(profileModules, scope), { recursive: true });
1403
+ const link = join(profileModules, scope, name);
1404
+ if (existsSync(link)) continue;
1405
+ try {
1406
+ symlinkSync(join("..", entry), link, "junction");
1407
+ } catch {}
1408
+ continue;
1409
+ }
1410
+ for (const scoped of readdirSync(join(profileModules, entry))) align(entry + "/" + scoped, join(profileModules, entry, scoped, "package.json"));
1063
1411
  }
1064
1412
  }
1065
1413
  /** The npm registry the distribution installs from by default. */
@@ -1078,7 +1426,7 @@ const MAINLAND_REGISTRY = "https://registry.npmmirror.com";
1078
1426
  */
1079
1427
  function registryOrder() {
1080
1428
  const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
1081
- if (configured !== void 0 && configured !== "") return [configured, configured === MAINLAND_REGISTRY ? OFFICIAL_REGISTRY : MAINLAND_REGISTRY];
1429
+ if (configured !== void 0 && configured !== "") return [configured, MAINLAND_REGISTRY];
1082
1430
  const locale = [
1083
1431
  process.env.LANG,
1084
1432
  process.env.LC_ALL,
@@ -1154,15 +1502,22 @@ function linkBundle(consumerModules, name, source) {
1154
1502
  } catch {}
1155
1503
  }
1156
1504
  /** Resolve every path a command needs, without creating anything. */
1157
- function resolvePaths(anchor, env = process.env) {
1505
+ function resolvePaths(anchor, env = process.env, editionId) {
1158
1506
  const home = resolveHome(env);
1159
- const declaration = readStandaloneDeclaration(anchor);
1507
+ const distributionDirectory = resolveDistributionDirectory(anchor);
1508
+ let profileName = DEFAULT_PROFILE;
1509
+ let omittedPackages = {};
1510
+ if (editionId !== void 0) {
1511
+ const projection = projectEdition(readDistributionFacts(distributionDirectory), editionId);
1512
+ profileName = projection.profileName;
1513
+ omittedPackages = projection.omittedPackages;
1514
+ }
1160
1515
  return {
1161
1516
  home,
1162
- profileName: declaration.profileName,
1163
- omittedPackages: declaration.omittedPackages,
1164
- profileDirectory: join(home, "profiles", declaration.profileName),
1165
- distributionDirectory: resolveDistributionDirectory(anchor)
1517
+ profileName,
1518
+ omittedPackages,
1519
+ profileDirectory: join(home, "profiles", profileName),
1520
+ distributionDirectory
1166
1521
  };
1167
1522
  }
1168
1523
  /**
@@ -1178,105 +1533,43 @@ function resolvePaths(anchor, env = process.env) {
1178
1533
  * that installed them.
1179
1534
  *
1180
1535
  * @param paths - resolved standalone paths.
1181
- * @param consumerDirectory - directory whose `node_modules` holds the installed packages.
1182
- * @param edition - edition id whose package list to install, or undefined for the default.
1536
+ * @param editionId - product edition to materialize.
1537
+ * @param extraExcludes - packages the caller drops on top of the edition's own exclusions.
1538
+ * @param dryRun - write the profile files without installing their dependency tree.
1183
1539
  * @returns whether this call created the manifest.
1184
1540
  */
1185
- function ensureProfile(paths, consumerDirectory, edition) {
1541
+ function ensureProfile(paths, editionId, extraExcludes = [], dryRun = false) {
1186
1542
  const manifestPath = join(paths.profileDirectory, "package.json");
1187
- const distribution = readDistributionProfile(paths.distributionDirectory);
1188
- const composition = edition === void 0 ? void 0 : distribution.editions[edition];
1189
- if (edition !== void 0 && composition === void 0) {
1190
- const offered = Object.keys(distribution.editions);
1191
- throw new Error("unknown edition \"" + edition + "\"; this release offers " + (offered.length === 0 ? "(none)" : offered.join(", ")));
1192
- }
1193
- for (const name of Object.keys(composition?.packages ?? {})) if (name.startsWith("@deepseek-ai/")) throw new Error("edition \"" + String(edition) + "\" lists the official package " + name + "; editions contain only third-party or self-developed plugins");
1194
- const runtime = distribution.dshRange.replace(/^>=?/u, "");
1195
- const baseBundles = composition === void 0 ? distribution.bundles : distribution.bundles.filter((bundle) => bundle.startsWith("@deepseek-ai/") || bundle === "@sparkelf/dsh-plus");
1196
- const basePackages = composition === void 0 ? distribution.dependencies : {
1197
- "@deepseek-ai/dsh": distribution.dshRange,
1198
- "@sparkelf/dsh-plus": distribution.version,
1199
- "@deepseek-ai/cordis": "^4.0.6-alpha.1",
1200
- "@deepseek-ai/cordis-plugin-group": "^1.0.6-alpha.1",
1201
- "@deepseek-ai/cordis-plugin-include": "^1.0.11-alpha.1",
1202
- "@deepseek-ai/cordis-plugin-loader": "^1.0.7-alpha.1",
1203
- "@deepseek-ai/dsh-home-paths": runtime,
1204
- "@deepseek-ai/dsh-launch-environment": runtime,
1205
- "@deepseek-ai/dsh-agent": runtime,
1206
- "@deepseek-ai/dsh-llm": runtime,
1207
- "@deepseek-ai/dsh-scope": runtime,
1208
- "@deepseek-ai/dsh-session": runtime,
1209
- "@deepseek-ai/dsh-system-prompt": runtime,
1210
- "@deepseek-ai/dsh-user-approval": runtime,
1211
- "@deepseek-ai/dsh-attachment": runtime,
1212
- "@deepseek-ai/dsh-compaction": runtime,
1213
- "@deepseek-ai/dsh-fs": runtime,
1214
- "@deepseek-ai/dsh-shell": runtime,
1215
- "@deepseek-ai/dsh-spill": runtime,
1216
- "@deepseek-ai/dsh-sandbox": runtime,
1217
- "@deepseek-ai/dsh-ptc-runtime": runtime,
1218
- "@deepseek-ai/dsh-llm-deepseek": runtime,
1219
- "@deepseek-ai/dsh-deepseek-account": runtime,
1220
- "@deepseek-ai/dsh-anonymous-user-id": runtime,
1221
- "@deepseek-ai/dsh-bash-local": runtime,
1222
- "@deepseek-ai/dsh-client-store": runtime,
1223
- "@deepseek-ai/dsh-jobs": runtime,
1224
- "@deepseek-ai/dsh-sdk-protocol": runtime,
1225
- "@deepseek-ai/dsh-session-persistence": runtime,
1226
- "@deepseek-ai/dsh-session-telemetry": runtime,
1227
- "@deepseek-ai/dsh-session-title-llm": runtime,
1228
- "@deepseek-ai/dsh-util-workspace-path": runtime,
1229
- "@deepseek-ai/dsh-workflow": runtime,
1230
- ...Object.fromEntries(baseBundles.filter((bundle) => bundle.startsWith("@deepseek-ai/dsh-")).map((bundle) => [bundle, runtime]))
1231
- };
1232
- const compositionPackages = composition === void 0 ? basePackages : {
1233
- ...basePackages,
1234
- ...composition.packages
1235
- };
1236
- for (const [alias, target] of Object.entries({
1237
- "dsh-better-sidebar": "@sparkelf/dsh-better-sidebar",
1238
- "dsh-sql-workbench": "@sparkelf/dsh-sql-workbench"
1239
- })) {
1240
- const spec = compositionPackages[target];
1241
- if (spec !== void 0 && compositionPackages[alias] === void 0) compositionPackages[alias] = "npm:" + target + "@" + spec;
1242
- }
1243
- const compositionBundles = composition === void 0 ? baseBundles : [...baseBundles, ...composition.bundles];
1543
+ const distribution = readDistributionProfile(paths.distributionDirectory, editionId, extraExcludes);
1244
1544
  if (existsSync(manifestPath)) {
1245
- if (composition !== void 0) {
1246
- const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, "utf8")), "Plus profile manifest");
1247
- manifest.dependencies = {
1248
- ...manifest.dependencies,
1249
- ...compositionPackages
1250
- };
1251
- manifest.dsh = {
1252
- ...manifest.dsh,
1253
- profile: {
1254
- ...manifest.dsh?.profile,
1255
- bundles: [...compositionBundles],
1256
- ...edition === void 0 ? {} : { plusEdition: edition }
1257
- }
1258
- };
1259
- writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
1260
- }
1261
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
1262
- installProfilePackages(paths, consumerDirectory);
1545
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, {
1546
+ ...paths.omittedPackages,
1547
+ ...distribution.omittedPackages
1548
+ });
1549
+ if (!dryRun) installProfilePackages(paths, new Set(Object.keys(distribution.omittedPackages)), distribution.runtimeVersion, registryOrder()[0]);
1263
1550
  return false;
1264
1551
  }
1265
1552
  mkdirSync(paths.profileDirectory, { recursive: true });
1266
1553
  const manifest = {
1267
- name: "dsh-profile-plus",
1554
+ name: "dsh-profile-" + paths.profileName,
1268
1555
  private: true,
1269
1556
  type: "module",
1270
- dependencies: compositionPackages,
1557
+ dependencies: {
1558
+ "@sparkelf/dsh-plus": distribution.version,
1559
+ "@deepseek-ai/dsh": distribution.dshRange,
1560
+ ...distribution.dependencies
1561
+ },
1271
1562
  dsh: { profile: {
1272
- bundles: [...compositionBundles],
1273
- patchReload: "live",
1274
- ...edition === void 0 ? {} : { plusEdition: edition }
1563
+ bundles: distribution.bundles,
1564
+ patchReload: "live"
1275
1565
  } }
1276
1566
  };
1277
1567
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
1278
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
1279
- installProfilePackages(paths, consumerDirectory);
1568
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, {
1569
+ ...paths.omittedPackages,
1570
+ ...distribution.omittedPackages
1571
+ });
1572
+ if (!dryRun) installProfilePackages(paths, new Set(Object.keys(distribution.omittedPackages)), distribution.runtimeVersion, registryOrder()[0]);
1280
1573
  return true;
1281
1574
  }
1282
1575
  /**
@@ -1299,7 +1592,7 @@ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omitted
1299
1592
  for (const [name, spec] of Object.entries(omittedPackages)) document.setIn(["overrides", name], spec);
1300
1593
  for (const [name, allowed] of Object.entries(allowBuilds)) document.setIn(["allowBuilds", name], allowed);
1301
1594
  if (document.get("nodeLinker") === void 0) document.set("nodeLinker", "hoisted");
1302
- if (document.get("autoInstallPeers") === void 0) document.set("autoInstallPeers", true);
1595
+ if (document.get("autoInstallPeers") === void 0) document.set("autoInstallPeers", false);
1303
1596
  writeFileSync(workspacePath, String(document));
1304
1597
  }
1305
1598
  /** Run git in one directory, returning undefined instead of throwing when asked to. */
@@ -1355,18 +1648,7 @@ function resolveInstalledPackage(from, packageName) {
1355
1648
  try {
1356
1649
  manifestPath = requireFrom.resolve(packageName + "/package.json");
1357
1650
  } catch {
1358
- let directory;
1359
- try {
1360
- directory = dirname(requireFrom.resolve(packageName));
1361
- } catch {
1362
- throw new Error(packageName + " is not installed under " + from);
1363
- }
1364
- while (!existsSync(resolve(directory, "package.json"))) {
1365
- const parent = dirname(directory);
1366
- if (parent === directory) throw new Error(packageName + " has no resolvable package manifest under " + from);
1367
- directory = parent;
1368
- }
1369
- manifestPath = resolve(directory, "package.json");
1651
+ throw new Error(packageName + " is not installed under " + from);
1370
1652
  }
1371
1653
  const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, "utf8")), packageName + " manifest");
1372
1654
  return {
@@ -1377,7 +1659,7 @@ function resolveInstalledPackage(from, packageName) {
1377
1659
  };
1378
1660
  }
1379
1661
  //#endregion
1380
- //#region lib/types/standalone-cli.js
1662
+ //#region lib/types/cli.js
1381
1663
  /**
1382
1664
  * The `dsh-plus` command line for a standalone installation.
1383
1665
  *
@@ -1387,6 +1669,8 @@ function resolveInstalledPackage(from, packageName) {
1387
1669
  * layer — a profile created on first start, a free port chosen without asking, a
1388
1670
  * server that survives the terminal, and one place to read the URL.
1389
1671
  */
1672
+ /** Edition the offline build projects when the caller names none. */
1673
+ const DEFAULT_EDITION = "intranet-team";
1390
1674
  /** Milliseconds a start waits for the server to answer before reporting failure. */
1391
1675
  const READY_TIMEOUT_MILLISECONDS = 9e4;
1392
1676
  /** Profile patch file the capability interview rewrites (the profile's user layer). */
@@ -1396,8 +1680,9 @@ function parseStartOptions(argv) {
1396
1680
  let host = "127.0.0.1";
1397
1681
  let open = true;
1398
1682
  let foreground = false;
1399
- let edition;
1400
1683
  let capabilities;
1684
+ let edition;
1685
+ const exclude = [];
1401
1686
  for (let index = 0; index < argv.length; index += 1) {
1402
1687
  const token = argv[index];
1403
1688
  if (token === "--port" || token === "-p") {
@@ -1429,6 +1714,13 @@ function parseStartOptions(argv) {
1429
1714
  index += 1;
1430
1715
  continue;
1431
1716
  }
1717
+ if (token === "--exclude") {
1718
+ const value = argv[index + 1];
1719
+ if (value === void 0 || value === "") throw new Error("--exclude requires a package name");
1720
+ exclude.push(value);
1721
+ index += 1;
1722
+ continue;
1723
+ }
1432
1724
  if (token === "--capabilities") {
1433
1725
  const value = argv[index + 1];
1434
1726
  if (value === void 0) throw new Error("--capabilities requires a comma-separated list, or an empty string for none");
@@ -1446,8 +1738,9 @@ function parseStartOptions(argv) {
1446
1738
  host,
1447
1739
  open,
1448
1740
  foreground,
1741
+ capabilities,
1449
1742
  edition,
1450
- capabilities
1743
+ exclude
1451
1744
  };
1452
1745
  }
1453
1746
  /**
@@ -1511,12 +1804,21 @@ function installationAnchor() {
1511
1804
  * @returns absolute path to the invoked command, or this module when argv carries none.
1512
1805
  */
1513
1806
  function declarationAnchor() {
1514
- const invoked = process.argv[1];
1515
- return invoked === void 0 || invoked === "" ? fileURLToPath(import.meta.url) : resolve(invoked);
1807
+ return resolveInvocationAnchor(process.argv[1], fileURLToPath(import.meta.url));
1516
1808
  }
1517
- /** The launcher entry this installation must drive. */
1518
- function launcherEntry(anchor) {
1519
- return createRequire(anchor).resolve("@deepseek-ai/dsh/lib/bin.js");
1809
+ /**
1810
+ * The launcher entry this installation must drive.
1811
+ *
1812
+ * The profile owns the runtime tree, so the launcher is resolved from the profile rather than
1813
+ * from the directory that installed the command: a global distribution has no launcher of its
1814
+ * own, and one resolved from the consumer would be a second copy of the process the profile
1815
+ * mounts.
1816
+ *
1817
+ * @param profileDirectory - profile whose node_modules carries the launcher.
1818
+ * @returns absolute path to the launcher entry.
1819
+ */
1820
+ function launcherEntry(profileDirectory) {
1821
+ return createRequire(join(profileDirectory, "package.json")).resolve("@deepseek-ai/dsh/lib/bin.js");
1520
1822
  }
1521
1823
  /** Run the server in this process, inheriting stdio. */
1522
1824
  function runForeground(entry, profileName, port, host, open) {
@@ -1532,60 +1834,9 @@ function runForeground(entry, profileName, port, host, open) {
1532
1834
  if (!open) args.push("--no-open");
1533
1835
  return spawnSync(process.execPath, args, { stdio: "inherit" }).status ?? 1;
1534
1836
  }
1535
- /** Parse the install command's edition selection. */
1536
- function parseInstallOptions(argv) {
1537
- let edition;
1538
- let profile;
1539
- for (let index = 0; index < argv.length; index += 1) {
1540
- const token = argv[index];
1541
- if (token === "--edition") {
1542
- const value = argv[index + 1];
1543
- if (value === void 0 || value === "") throw new Error("--edition requires a value");
1544
- edition = value;
1545
- index += 1;
1546
- continue;
1547
- }
1548
- if (token === "--profile") {
1549
- const value = argv[index + 1];
1550
- if (value === void 0 || value === "") throw new Error("--profile requires a value");
1551
- profile = value;
1552
- index += 1;
1553
- continue;
1554
- }
1555
- throw new Error("unknown option: " + String(token));
1556
- }
1557
- if (edition === void 0) throw new Error("install requires --edition personal or --edition team");
1558
- return {
1559
- edition,
1560
- profile
1561
- };
1562
- }
1563
- /**
1564
- * Install one edition's package manifest into the profile.
1565
- *
1566
- * The profile's `package.json` is the install artifact; there is no separate standalone
1567
- * package. A later `start` uses the same manifest, so changing the edition is a new
1568
- * install rather than a second product.
1569
- *
1570
- * @param argv - arguments after `install`.
1571
- * @returns the process exit code.
1572
- */
1573
- function install(argv) {
1574
- const { edition, profile } = parseInstallOptions(argv);
1575
- const base = resolvePaths(declarationAnchor());
1576
- const paths = profile === void 0 ? base : {
1577
- ...base,
1578
- profileName: profile,
1579
- profileDirectory: join(base.home, "profiles", profile)
1580
- };
1581
- ensureProfile(paths, installationRoot(), edition);
1582
- console.log("Installed the " + edition + " edition into " + paths.profileDirectory);
1583
- return 0;
1584
- }
1585
1837
  async function start(argv) {
1586
1838
  const options = parseStartOptions(argv);
1587
- const anchor = installationAnchor();
1588
- const paths = resolvePaths(declarationAnchor());
1839
+ const paths = resolvePaths(declarationAnchor(), process.env, options.edition);
1589
1840
  if (!pnpmAvailable()) {
1590
1841
  const command = pnpmInstallCommand();
1591
1842
  console.log("pnpm is required to install the plus profile, and was not found.");
@@ -1615,20 +1866,19 @@ async function start(argv) {
1615
1866
  }
1616
1867
  console.log("pnpm installed.");
1617
1868
  }
1618
- const created = ensureProfile(paths, installationRoot(), options.edition);
1869
+ const created = ensureProfile(paths, options.edition, options.exclude);
1619
1870
  console.log(created ? "Created the " + paths.profileName + " profile at " + paths.profileDirectory : "Using the existing " + paths.profileName + " profile");
1620
- if (options.edition !== void 0) console.log("Edition: " + options.edition);
1621
1871
  for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
1622
1872
  const answers = created || !existsSync(join(paths.home, "capabilities.json")) ? options.capabilities === void 0 ? await interviewCapabilities(true) : selectCapabilities(options.capabilities) : void 0;
1623
1873
  if (answers !== void 0) {
1624
- const ready = await installCapabilityServices(answers, paths.home);
1874
+ const ready = installCapabilityServices(answers, paths.home);
1625
1875
  writeCapabilityPatch(paths.profileDirectory, answers);
1626
1876
  writeCapabilityEnvironment(paths.home, answers);
1627
1877
  console.log(" Enabled: " + (answers.enabled.length === 0 ? "(none)" : answers.enabled.join(", ")));
1628
1878
  if (answers.enabled.length > 0) console.log(" Services ready: " + (ready.length === 0 ? "(none)" : ready.join(", ")));
1629
1879
  if (answers.enabled.includes("exa") && answers.exaApiKey === void 0) console.log(" Exa: add EXA_API_KEY to " + join(paths.home, CAPABILITY_ENV_FILE) + " when you have a key.");
1630
1880
  }
1631
- const entry = launcherEntry(anchor);
1881
+ const entry = launcherEntry(paths.profileDirectory);
1632
1882
  if (options.foreground) return runForeground(entry, paths.profileName, options.port, options.host, options.open);
1633
1883
  const existing = readState(paths.home);
1634
1884
  if (existing !== void 0) {
@@ -1960,31 +2210,223 @@ async function doctor() {
1960
2210
  const record = readCapabilityRecord(paths.home);
1961
2211
  const drifted = record === void 0 ? [] : CAPABILITIES.filter((capability) => record.enabled.includes(capability.id) !== actual.mounted.includes(capability.id));
1962
2212
  check(drifted.length === 0, drifted.length === 0 ? "capabilities: " + (actual.mounted.length === 0 ? "(none enabled)" : actual.mounted.join(", ")) : "capabilities disagree with capabilities.json: " + drifted.map((capability) => capability.id).join(", ") + " (run dsh-plus capabilities)");
1963
- const profileManifestPath = join(paths.profileDirectory, "package.json");
1964
- if (existsSync(profileManifestPath)) {
1965
- const profileManifest = JSON.parse(readFileSync(profileManifestPath, "utf8"));
1966
- const editionId = profileManifest.dsh?.profile?.plusEdition;
1967
- if (editionId !== void 0) {
1968
- const edition = readDistributionProfile(paths.distributionDirectory).editions[editionId];
1969
- if (edition === void 0) check(false, "edition \"" + editionId + "\" is not declared by this distribution");
1970
- else {
1971
- const bundles = new Set(profileManifest.dsh?.profile?.bundles ?? []);
1972
- const missingBundles = edition.bundles.filter((bundle) => !bundles.has(bundle));
1973
- const packageNames = Object.keys(edition.packages);
1974
- const missingPackages = packageNames.filter((name) => !existsSync(join(paths.profileDirectory, "node_modules", ...name.split("/"), "package.json")));
1975
- check(missingBundles.length === 0, "edition " + editionId + ": " + String(edition.bundles.length - missingBundles.length) + "/" + String(edition.bundles.length) + " bundles mounted" + (missingBundles.length === 0 ? "" : " (missing " + missingBundles.join(", ") + ")"));
1976
- check(missingPackages.length === 0, "edition " + editionId + ": " + String(packageNames.length - missingPackages.length) + "/" + String(packageNames.length) + " packages installed" + (missingPackages.length === 0 ? "" : " (missing " + missingPackages.join(", ") + ")"));
1977
- }
1978
- }
1979
- }
1980
2213
  console.log(failures === 0 ? "No problems found." : String(failures) + " problem(s) found.");
1981
2214
  return failures === 0 ? 0 : 1;
1982
2215
  }
2216
+ function parseInstallOptions(argv) {
2217
+ let edition;
2218
+ const exclude = [];
2219
+ let dryRun = false;
2220
+ for (let index = 0; index < argv.length; index += 1) {
2221
+ const token = argv[index];
2222
+ const value = argv[index + 1];
2223
+ if (token === "--dry-run") {
2224
+ dryRun = true;
2225
+ continue;
2226
+ }
2227
+ if (token === "--edition") {
2228
+ if (value === void 0 || value === "") throw new Error("--edition requires a value");
2229
+ edition = value;
2230
+ index += 1;
2231
+ continue;
2232
+ }
2233
+ if (token === "--exclude") {
2234
+ if (value === void 0 || value === "") throw new Error("--exclude requires a package name");
2235
+ exclude.push(value);
2236
+ index += 1;
2237
+ continue;
2238
+ }
2239
+ throw new Error("unknown option: " + String(token));
2240
+ }
2241
+ return {
2242
+ edition,
2243
+ exclude,
2244
+ dryRun
2245
+ };
2246
+ }
2247
+ /**
2248
+ * Materialize a profile without starting the server.
2249
+ *
2250
+ * The build system installs the distribution once and needs the profile on disk before it
2251
+ * snapshots the image; starting a server to create it would also start a process the image
2252
+ * must then kill.
2253
+ *
2254
+ * @param argv - arguments after the command word.
2255
+ * @returns the process exit code.
2256
+ */
2257
+ function install(argv) {
2258
+ if (argv.includes("--help") || argv.includes("-h")) {
2259
+ console.log(USAGE);
2260
+ return 0;
2261
+ }
2262
+ const options = parseInstallOptions(argv);
2263
+ const paths = resolvePaths(declarationAnchor(), process.env, options.edition);
2264
+ const created = ensureProfile(paths, options.edition, options.exclude, options.dryRun);
2265
+ console.log((created ? "Created the " : "Using the existing ") + paths.profileName + " profile at " + paths.profileDirectory);
2266
+ return 0;
2267
+ }
2268
+ function parseManifestOptions(argv) {
2269
+ let distribution;
2270
+ let out;
2271
+ let edition = DEFAULT_EDITION;
2272
+ const exclude = [];
2273
+ let runtime;
2274
+ let registry = "https://registry.npmjs.org";
2275
+ let verify = false;
2276
+ let check = false;
2277
+ for (let index = 0; index < argv.length; index += 1) {
2278
+ const token = argv[index];
2279
+ const value = argv[index + 1];
2280
+ if (token === "--distribution") {
2281
+ if (value === void 0) throw new Error("--distribution requires a value");
2282
+ distribution = value;
2283
+ index += 1;
2284
+ continue;
2285
+ }
2286
+ if (token === "--out") {
2287
+ if (value === void 0) throw new Error("--out requires a value");
2288
+ out = value;
2289
+ index += 1;
2290
+ continue;
2291
+ }
2292
+ if (token === "--edition") {
2293
+ if (value === void 0) throw new Error("--edition requires a value");
2294
+ edition = value;
2295
+ index += 1;
2296
+ continue;
2297
+ }
2298
+ if (token === "--exclude") {
2299
+ if (value === void 0) throw new Error("--exclude requires a package name");
2300
+ exclude.push(value);
2301
+ index += 1;
2302
+ continue;
2303
+ }
2304
+ if (token === "--runtime") {
2305
+ if (value === void 0) throw new Error("--runtime requires a value");
2306
+ runtime = value;
2307
+ index += 1;
2308
+ continue;
2309
+ }
2310
+ if (token === "--registry") {
2311
+ if (value === void 0) throw new Error("--registry requires a value");
2312
+ registry = value;
2313
+ index += 1;
2314
+ continue;
2315
+ }
2316
+ if (token === "--verify") {
2317
+ verify = true;
2318
+ continue;
2319
+ }
2320
+ if (token === "--check") {
2321
+ check = true;
2322
+ continue;
2323
+ }
2324
+ throw new Error("unknown option: " + String(token));
2325
+ }
2326
+ return {
2327
+ distribution,
2328
+ out,
2329
+ edition,
2330
+ exclude,
2331
+ runtime,
2332
+ registry,
2333
+ verify,
2334
+ check
2335
+ };
2336
+ }
2337
+ /**
2338
+ * Write or verify the profile manifest a build installs.
2339
+ *
2340
+ * `--out` names the profile directory. `--check` compares what is there against a fresh
2341
+ * projection instead of writing, so a committed profile cannot drift from the distribution.
2342
+ *
2343
+ * @param argv - arguments after the command word.
2344
+ * @returns the process exit code.
2345
+ */
2346
+ async function manifest(argv) {
2347
+ const options = parseManifestOptions(argv);
2348
+ const directory = options.distribution ?? resolveDistributionDirectory(declarationAnchor());
2349
+ const facts = options.runtime === void 0 ? readDistributionFacts(directory) : {
2350
+ ...readDistributionFacts(directory),
2351
+ runtimeVersion: options.runtime
2352
+ };
2353
+ const projection = projectEdition(facts, options.edition, options.exclude);
2354
+ if (options.verify) await assertRedirectsPublished({
2355
+ ...facts.redirects,
2356
+ ...projection.omittedPackages
2357
+ }, options.registry);
2358
+ if (options.out === void 0) throw new Error("manifest requires --out <directory>");
2359
+ if (options.check) {
2360
+ const written = join(options.out, "package.json");
2361
+ if (!existsSync(written)) throw new Error("manifest: " + written + " does not exist; run the generator");
2362
+ const actual = JSON.parse(readFileSync(written, "utf8"));
2363
+ const expectedDependencies = {
2364
+ "@sparkelf/dsh-plus": facts.version,
2365
+ "@deepseek-ai/dsh": ">=" + facts.runtimeVersion,
2366
+ ...projection.dependencies
2367
+ };
2368
+ const drift = [];
2369
+ for (const [name, spec] of Object.entries(expectedDependencies)) if (actual.dependencies?.[name] !== spec) drift.push("dependency " + name);
2370
+ for (const name of Object.keys(actual.dependencies ?? {})) if (expectedDependencies[name] === void 0) drift.push("extra dependency " + name);
2371
+ for (const bundle of projection.bundles) if (!(actual.dsh?.profile?.bundles ?? []).includes(bundle)) drift.push("bundle " + bundle);
2372
+ if (drift.length > 0) throw new Error("manifest: " + written + " is stale in " + drift.join(", "));
2373
+ console.log("manifest: " + written + " matches the distribution");
2374
+ return 0;
2375
+ }
2376
+ writeProfileProjection(options.out, facts, options.edition, options.exclude);
2377
+ console.log(JSON.stringify({
2378
+ profile: projection.profileName,
2379
+ runtime: facts.runtimeVersion,
2380
+ bundles: projection.bundles.length,
2381
+ dependencies: Object.keys(projection.dependencies).length,
2382
+ redirects: Object.keys(facts.redirects).length,
2383
+ overrides: Object.keys(projection.overrides).length,
2384
+ omitted: Object.keys(projection.omittedPackages)
2385
+ }));
2386
+ return 0;
2387
+ }
2388
+ /**
2389
+ * Print the distribution facts a build or a release gate consumes.
2390
+ *
2391
+ * @param argv - accepts `--distribution <dir>` only.
2392
+ * @returns the process exit code.
2393
+ */
2394
+ function inspect(argv) {
2395
+ let directory;
2396
+ for (let index = 0; index < argv.length; index += 1) {
2397
+ if (argv[index] !== "--distribution") throw new Error("unknown option: " + String(argv[index]));
2398
+ directory = argv[index + 1];
2399
+ if (directory === void 0) throw new Error("--distribution requires a value");
2400
+ index += 1;
2401
+ }
2402
+ const facts = readDistributionFacts(directory ?? resolveDistributionDirectory(declarationAnchor()));
2403
+ console.log(JSON.stringify({
2404
+ name: facts.name,
2405
+ version: facts.version,
2406
+ runtime: facts.runtimeVersion,
2407
+ profile: facts.name,
2408
+ bundles: facts.bundles,
2409
+ dependencies: facts.dependencies,
2410
+ redirects: facts.redirects,
2411
+ redirectPatch: facts.redirectIds,
2412
+ editions: Object.entries(facts.editions).map(([id, edition]) => ({
2413
+ id,
2414
+ label: edition.label,
2415
+ profile: edition.profile,
2416
+ bundles: edition.bundles.length,
2417
+ packages: edition.packages.length,
2418
+ omit: edition.omit.length
2419
+ }))
2420
+ }, null, 2));
2421
+ return 0;
2422
+ }
1983
2423
  const USAGE = [
1984
2424
  "Usage: dsh-plus <command> [options]",
1985
2425
  "",
1986
2426
  "Commands:",
1987
- " install install an edition package manifest into the profile",
2427
+ " install materialize the profile without starting the server",
2428
+ " manifest write or verify the profile manifest a build installs",
2429
+ " inspect print the distribution facts as JSON",
1988
2430
  " start start the server (first run creates the profile)",
1989
2431
  " stop stop the server",
1990
2432
  " status report whether the server is running",
@@ -1992,16 +2434,26 @@ const USAGE = [
1992
2434
  " update move to a newer distribution release",
1993
2435
  " doctor check this installation",
1994
2436
  "",
2437
+ "install options:",
2438
+ " --edition <id> product edition to materialize (personal, team, intranet-team)",
2439
+ " --exclude <pkg> drop a package and its bundle row (repeatable)",
2440
+ "",
2441
+ "manifest options:",
2442
+ " --out <dir> profile directory to write",
2443
+ " --edition <id> product edition to project (default: intranet-team)",
2444
+ " --exclude <pkg> drop a package and its bundle row (repeatable)",
2445
+ " --runtime <version> override the runtime version the redirects name",
2446
+ " --registry <url> registry checked by --verify",
2447
+ " --verify fail unless every redirect target is published",
2448
+ " --check compare --out with a fresh projection instead of writing",
2449
+ "",
1995
2450
  "start options:",
1996
2451
  " --port <n> port to prefer (default " + String(DEFAULT_PORT) + "; a taken port moves to the next free one)",
1997
2452
  " --host <h> interface to bind (default 127.0.0.1)",
1998
2453
  " --no-open do not open a browser",
1999
2454
  " --foreground run in this terminal instead of in the background",
2000
- " --edition <id> install the named edition manifest (personal or team)",
2001
- "",
2002
- "install options:",
2003
- " --edition <id> package manifest to install (personal or team)",
2004
- " --profile <n> profile name to install into (default: this installation)",
2455
+ " --edition <id> product edition to materialize",
2456
+ " --exclude <pkg> drop a package and its bundle row (repeatable)",
2005
2457
  "",
2006
2458
  "capabilities options:",
2007
2459
  " --enable <ids> enable the comma-separated capability ids",
@@ -2025,6 +2477,8 @@ async function runStandaloneCli(argv) {
2025
2477
  return 0;
2026
2478
  }
2027
2479
  if (command === "install") return install(rest);
2480
+ if (command === "manifest") return await manifest(rest);
2481
+ if (command === "inspect") return inspect(rest);
2028
2482
  if (command === "start") return await start(rest);
2029
2483
  if (command === "stop") return stop();
2030
2484
  if (command === "status") return status();