@sparkelf/dsh-plus 0.2.4-alpha.48 → 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 +780 -326
- package/lib/types/bin.js +1 -1
- package/lib/types/capabilities.d.ts +139 -0
- package/lib/types/capabilities.js +688 -0
- package/lib/types/cli.d.ts +17 -0
- package/lib/types/cli.js +913 -0
- package/lib/types/manifest.d.ts +137 -0
- package/lib/types/manifest.js +398 -0
- package/lib/types/profile.d.ts +218 -0
- package/lib/types/profile.js +670 -0
- package/lib/types/server.d.ts +70 -0
- package/lib/types/server.js +158 -0
- package/lib/types/standalone-cli.js +274 -93
- package/lib/types/standalone-profile.d.ts +54 -17
- package/lib/types/standalone-profile.js +273 -247
- package/package.json +168 -125
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/
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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
|
|
805
|
-
const
|
|
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
|
-
*
|
|
1105
|
+
* Resolve the file that was actually invoked from the path the shell was handed.
|
|
808
1106
|
*
|
|
809
|
-
* A
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
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
|
|
815
|
-
* @param fallback -
|
|
816
|
-
* @returns the
|
|
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
|
|
819
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
920
|
-
|
|
921
|
-
const
|
|
922
|
-
const
|
|
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:
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
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,
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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,
|
|
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
|
|
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
|
|
1163
|
-
omittedPackages
|
|
1164
|
-
profileDirectory: join(home, "profiles",
|
|
1165
|
-
distributionDirectory
|
|
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
|
|
1182
|
-
* @param
|
|
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,
|
|
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
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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-
|
|
1554
|
+
name: "dsh-profile-" + paths.profileName,
|
|
1268
1555
|
private: true,
|
|
1269
1556
|
type: "module",
|
|
1270
|
-
dependencies:
|
|
1557
|
+
dependencies: {
|
|
1558
|
+
"@sparkelf/dsh-plus": distribution.version,
|
|
1559
|
+
"@deepseek-ai/dsh": distribution.dshRange,
|
|
1560
|
+
...distribution.dependencies
|
|
1561
|
+
},
|
|
1271
1562
|
dsh: { profile: {
|
|
1272
|
-
bundles:
|
|
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,
|
|
1279
|
-
|
|
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",
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
1518
|
-
|
|
1519
|
-
|
|
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
|
|
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,
|
|
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 =
|
|
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(
|
|
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
|
|
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>
|
|
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();
|