@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 +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 +169 -124
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
* bundle's own list.
|
|
10
10
|
*/
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
14
|
import { homedir } from 'node:os';
|
|
16
15
|
import { dirname, join, posix, resolve, win32 } from 'node:path';
|
|
17
16
|
import { parseDocument } from 'yaml';
|
|
17
|
+
import { projectEdition, readDistributionFacts } from "./manifest.js";
|
|
18
18
|
import { readPatchSet } from "./patch-set.js";
|
|
19
19
|
/** Profile name a standalone installation owns when its package declares none. */
|
|
20
20
|
export const STANDALONE_PROFILE = 'plus';
|
|
@@ -94,16 +94,29 @@ function requireRecord(value, label) {
|
|
|
94
94
|
throw new Error(label + ' must be an object');
|
|
95
95
|
return value;
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Resolve the file that was actually invoked from the path the shell was handed.
|
|
99
|
+
*
|
|
100
|
+
* A global npm install links `<prefix>/bin/dsh-plus` to the package entry. Resolving
|
|
101
|
+
* `@sparkelf/dsh-plus` from that link starts at the bin directory, which has no
|
|
102
|
+
* `node_modules`, so every globally installed command reported the distribution as
|
|
103
|
+
* missing. The real path is the package file, and the package resolves its own name.
|
|
104
|
+
*
|
|
105
|
+
* @param invoked - `process.argv[1]`, or undefined when node ran without a script.
|
|
106
|
+
* @param fallback - this module's own file, used when there is no script path.
|
|
107
|
+
* @returns an absolute path that exists, or the input when it cannot be resolved.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveInvocationAnchor(invoked, fallback) {
|
|
110
|
+
if (invoked === undefined || invoked === '')
|
|
111
|
+
return realpathSync(fallback);
|
|
112
|
+
try {
|
|
113
|
+
return realpathSync(invoked);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// A path the filesystem no longer knows cannot be resolved; the caller's error message is
|
|
117
|
+
// more useful with what it was given than with a second failure here.
|
|
118
|
+
return resolve(invoked);
|
|
119
|
+
}
|
|
107
120
|
}
|
|
108
121
|
/** Resolve the DSH home, honouring the environment override the launcher itself reads. */
|
|
109
122
|
export function resolveHome(env = process.env) {
|
|
@@ -143,15 +156,12 @@ export function isWithin(candidate, parent, platform = process.platform) {
|
|
|
143
156
|
* @returns absolute path to the installed distribution.
|
|
144
157
|
*/
|
|
145
158
|
export function resolveDistributionDirectory(anchor) {
|
|
146
|
-
const ownDirectory = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
147
159
|
let resolved;
|
|
148
160
|
try {
|
|
149
161
|
resolved = dirname(createRequire(anchor).resolve('@sparkelf/dsh-plus/package.json'));
|
|
150
162
|
}
|
|
151
163
|
catch {
|
|
152
|
-
|
|
153
|
-
// tree has no copy to resolve. The running module's own package is the distribution.
|
|
154
|
-
return ownDirectory;
|
|
164
|
+
throw new Error('@sparkelf/dsh-plus is not installed in ' + dirname(anchor) + '; run this command from the directory that installed it');
|
|
155
165
|
}
|
|
156
166
|
// A resolution that leaves the consumer tree means the module answered from its own
|
|
157
167
|
// installation, which reports health for a distribution the consumer never installed.
|
|
@@ -167,64 +177,25 @@ export function resolveDistributionDirectory(anchor) {
|
|
|
167
177
|
break;
|
|
168
178
|
current = parent;
|
|
169
179
|
}
|
|
170
|
-
|
|
180
|
+
throw new Error('@sparkelf/dsh-plus resolved outside ' + resolve(dirname(anchor)) + ' (found ' + resolved + '); run this command from the directory that installed it');
|
|
171
181
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const dependencies = requireDependencyMap(requireRecord(profile.dependencies, 'dshPlus.profile.dependencies'), 'dshPlus.profile.dependencies');
|
|
177
|
-
const rawAllowBuilds = requireRecord(profile.allowBuilds, 'dshPlus.profile.allowBuilds');
|
|
178
|
-
const allowBuilds = {};
|
|
179
|
-
for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
|
|
180
|
-
if (typeof allowed !== 'boolean')
|
|
181
|
-
throw new Error('dshPlus.profile.allowBuilds.' + name + ' must be a boolean');
|
|
182
|
-
allowBuilds[name] = allowed;
|
|
183
|
-
}
|
|
184
|
-
// An override substitutes our republished build for an official package by name: the
|
|
185
|
-
// built code imports the official specifier, so the installed location has to keep
|
|
186
|
-
// that name while its contents come from ours.
|
|
187
|
-
const overrides = {};
|
|
188
|
-
const rawOverrides = profile.overrides === undefined
|
|
189
|
-
? {}
|
|
190
|
-
: requireRecord(profile.overrides, 'dshPlus.profile.overrides');
|
|
191
|
-
for (const [name, spec] of Object.entries(rawOverrides)) {
|
|
192
|
-
if (typeof spec !== 'string' || spec === '')
|
|
193
|
-
throw new Error('dshPlus.profile.overrides.' + name + ' must be a non-empty string');
|
|
194
|
-
overrides[name] = spec;
|
|
195
|
-
}
|
|
196
|
-
const editions = {};
|
|
197
|
-
const rawEditions = plus.editions === undefined ? {} : requireRecord(plus.editions, 'dshPlus.editions');
|
|
198
|
-
for (const [id, value] of Object.entries(rawEditions)) {
|
|
199
|
-
const edition = requireRecord(value, 'dshPlus.editions.' + id);
|
|
200
|
-
editions[id] = {
|
|
201
|
-
label: requireString(edition.label, 'dshPlus.editions.' + id + '.label'),
|
|
202
|
-
bundles: requireStringArray(edition.bundles, 'dshPlus.editions.' + id + '.bundles'),
|
|
203
|
-
packages: requireDependencyMap(requireRecord(edition.packages, 'dshPlus.editions.' + id + '.packages'), 'dshPlus.editions.' + id + '.packages'),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
const compatibility = requireRecord(plus.compatibility, 'dshPlus.compatibility');
|
|
182
|
+
/** The reviewed bundle order and pins the installed distribution declares, for one variant. */
|
|
183
|
+
export function readDistributionProfile(distributionDirectory, editionId, extraExcludes = []) {
|
|
184
|
+
const facts = readDistributionFacts(distributionDirectory);
|
|
185
|
+
const projection = projectEdition(facts, editionId, extraExcludes);
|
|
207
186
|
return {
|
|
208
|
-
name:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
187
|
+
name: facts.name,
|
|
188
|
+
version: facts.version,
|
|
189
|
+
dshRange: '>=' + facts.runtimeVersion,
|
|
190
|
+
runtimeVersion: facts.runtimeVersion,
|
|
191
|
+
bundles: projection.bundles,
|
|
192
|
+
dependencies: projection.dependencies,
|
|
193
|
+
allowBuilds: projection.allowBuilds,
|
|
194
|
+
overrides: projection.overrides,
|
|
195
|
+
omittedPackages: projection.omittedPackages,
|
|
196
|
+
editions: facts.editions,
|
|
216
197
|
};
|
|
217
198
|
}
|
|
218
|
-
/** Read one package-name-to-spec map, rejecting empty entries. */
|
|
219
|
-
function requireDependencyMap(value, label) {
|
|
220
|
-
const dependencies = {};
|
|
221
|
-
for (const [name, spec] of Object.entries(value)) {
|
|
222
|
-
if (typeof spec !== 'string' || spec === '')
|
|
223
|
-
throw new Error(label + '.' + name + ' must be a non-empty string');
|
|
224
|
-
dependencies[name] = spec;
|
|
225
|
-
}
|
|
226
|
-
return dependencies;
|
|
227
|
-
}
|
|
228
199
|
/**
|
|
229
200
|
* Give the profile its own installed tree, built from the distribution's declarations.
|
|
230
201
|
*
|
|
@@ -241,18 +212,11 @@ function requireDependencyMap(value, label) {
|
|
|
241
212
|
* twice; `dsh-plus apply` remains the command that reinstalls after a change.
|
|
242
213
|
*
|
|
243
214
|
* @param paths - resolved standalone paths.
|
|
244
|
-
* @param consumerDirectory - directory whose `node_modules` holds the installation.
|
|
245
215
|
*/
|
|
246
|
-
export function installProfilePackages(paths,
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// beside it. The profile installs its own tree, so there is nothing to link from
|
|
251
|
-
// the caller; only the profile's packages matter.
|
|
252
|
-
installWithRegistryFallback(paths);
|
|
253
|
-
alignReplacedPackageNames(join(paths.profileDirectory, 'node_modules'));
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
216
|
+
export function installProfilePackages(paths, omitted = new Set(), runtimeVersion, registry) {
|
|
217
|
+
// The profile owns its dependency tree: it declares the launcher and every plugin, so it can
|
|
218
|
+
// install without a consumer tree beside it. A globally installed distribution has none --
|
|
219
|
+
// its only node_modules is the one npm laid out around the command itself.
|
|
256
220
|
const profileModules = join(paths.profileDirectory, 'node_modules');
|
|
257
221
|
if (existsSync(profileModules)) {
|
|
258
222
|
// A profile installed before the distribution declared overrides still needs the
|
|
@@ -260,9 +224,142 @@ export function installProfilePackages(paths, consumerDirectory) {
|
|
|
260
224
|
linkNestedBundles(paths, profileModules);
|
|
261
225
|
return;
|
|
262
226
|
}
|
|
263
|
-
installWithRegistryFallback(paths);
|
|
264
|
-
|
|
227
|
+
installWithRegistryFallback(paths.profileDirectory);
|
|
228
|
+
// A range admits the next prerelease, so the official family lands one generation ahead of
|
|
229
|
+
// the runtime the republished builds were cut against. Realign, then finish the tree.
|
|
230
|
+
if (runtimeVersion !== undefined && registry !== undefined) {
|
|
231
|
+
alignOfficialVersions(paths.profileDirectory, runtimeVersion, registry);
|
|
232
|
+
}
|
|
233
|
+
alignReplacedPackageNames(profileModules, omitted);
|
|
265
234
|
linkNestedBundles(paths, profileModules);
|
|
235
|
+
assertProfileBundles(paths.profileDirectory);
|
|
236
|
+
}
|
|
237
|
+
/** Package names a package declares as peers on the official runtime family. */
|
|
238
|
+
function officialPeers(profileDirectory) {
|
|
239
|
+
const peers = new Set();
|
|
240
|
+
const manifests = [];
|
|
241
|
+
for (const scope of [join(profileDirectory, 'node_modules'), join(profileDirectory, 'node_modules', '@deepseek-ai')]) {
|
|
242
|
+
if (!existsSync(scope))
|
|
243
|
+
continue;
|
|
244
|
+
for (const entry of readdirSync(scope)) {
|
|
245
|
+
if (entry.startsWith('.'))
|
|
246
|
+
continue;
|
|
247
|
+
if (entry.startsWith('@') && entry !== '@deepseek-ai')
|
|
248
|
+
continue;
|
|
249
|
+
if (entry === '@deepseek-ai') {
|
|
250
|
+
for (const scoped of readdirSync(scope))
|
|
251
|
+
manifests.push(join(scope, scoped, 'package.json'));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
manifests.push(join(scope, entry, 'package.json'));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const manifestPath of manifests) {
|
|
258
|
+
if (!existsSync(manifestPath))
|
|
259
|
+
continue;
|
|
260
|
+
try {
|
|
261
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
262
|
+
for (const peer of Object.keys(manifest.peerDependencies ?? {})) {
|
|
263
|
+
if (peer.startsWith('@deepseek-ai/dsh') && peer !== '@deepseek-ai/dsh')
|
|
264
|
+
peers.add(peer);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// A package whose manifest cannot be read declares no peer this step can act on.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return peers;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Which of the given package versions a registry serves.
|
|
275
|
+
*
|
|
276
|
+
* The probe runs in one Node child: one process per name is slow, and the caller needs the
|
|
277
|
+
* answer before the second install.
|
|
278
|
+
*
|
|
279
|
+
* @param names - package names to check.
|
|
280
|
+
* @param version - exact version each must serve.
|
|
281
|
+
* @param registry - registry base URL.
|
|
282
|
+
* @returns the names the registry serves at that version.
|
|
283
|
+
*/
|
|
284
|
+
function servedVersions(names, version, registry) {
|
|
285
|
+
if (names.length === 0)
|
|
286
|
+
return new Set();
|
|
287
|
+
const script = `
|
|
288
|
+
const [names, version, registry] = JSON.parse(process.argv[1])
|
|
289
|
+
const base = registry.replace(/\\/$/, '')
|
|
290
|
+
const queue = [...names]
|
|
291
|
+
const served = []
|
|
292
|
+
await Promise.all(Array.from({ length: 10 }, async () => {
|
|
293
|
+
while (queue.length > 0) {
|
|
294
|
+
const name = queue.shift()
|
|
295
|
+
try {
|
|
296
|
+
const response = await fetch(base + '/' + name.replace('/', '%2f') + '/' + version)
|
|
297
|
+
if (response.ok) served.push(name)
|
|
298
|
+
} catch {}
|
|
299
|
+
}
|
|
300
|
+
}))
|
|
301
|
+
process.stdout.write(served.join('\\n'))
|
|
302
|
+
`;
|
|
303
|
+
const result = spawnSync(process.execPath, ['--input-type=module', '-e', script, JSON.stringify([names, version, registry])], { encoding: 'utf8' });
|
|
304
|
+
if (result.status !== 0)
|
|
305
|
+
return new Set();
|
|
306
|
+
return new Set(result.stdout.split('\n').map(entry => entry.trim()).filter(entry => entry !== ''));
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Move the shared official services onto the runtime's exact version.
|
|
310
|
+
*
|
|
311
|
+
* The republished builds were cut against one runtime, and DSH disables a row when the exact
|
|
312
|
+
* peers a plugin declares disagree with the process it shares. Plugins declare those services as
|
|
313
|
+
* peers, so the names worth pinning are the peers actually installed -- read from the tree and
|
|
314
|
+
* checked against the registry. The rest of the official family keeps its resolved versions:
|
|
315
|
+
* pinning everything traded one mismatch for a tree with holes.
|
|
316
|
+
*
|
|
317
|
+
* @param profileDirectory - profile whose tree was just installed.
|
|
318
|
+
* @param runtimeVersion - exact runtime the profile boots.
|
|
319
|
+
* @param registry - registry the profile installs from.
|
|
320
|
+
* @returns how many services were realigned.
|
|
321
|
+
*/
|
|
322
|
+
export function alignOfficialVersions(profileDirectory, runtimeVersion, registry) {
|
|
323
|
+
const wanted = [...officialPeers(profileDirectory)];
|
|
324
|
+
const served = servedVersions(wanted, runtimeVersion, registry);
|
|
325
|
+
if (served.size === 0)
|
|
326
|
+
return 0;
|
|
327
|
+
const workspacePath = join(profileDirectory, 'pnpm-workspace.yaml');
|
|
328
|
+
const document = parseDocument(readFileSync(workspacePath, 'utf8'));
|
|
329
|
+
let changed = 0;
|
|
330
|
+
for (const name of served) {
|
|
331
|
+
if (document.getIn(['overrides', name]) === runtimeVersion)
|
|
332
|
+
continue;
|
|
333
|
+
document.setIn(['overrides', name], runtimeVersion);
|
|
334
|
+
changed += 1;
|
|
335
|
+
}
|
|
336
|
+
if (changed === 0)
|
|
337
|
+
return 0;
|
|
338
|
+
writeFileSync(workspacePath, String(document));
|
|
339
|
+
rmSync(join(profileDirectory, 'pnpm-lock.yaml'), { force: true });
|
|
340
|
+
installWithRegistryFallback(profileDirectory);
|
|
341
|
+
return changed;
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Assert every bundle row the profile declares resolves in its installed tree.
|
|
345
|
+
*
|
|
346
|
+
* A row without an installed package mounts nothing and reports nothing: the client module
|
|
347
|
+
* system simply never registers it, and every earlier step passes. Reading the rows back from
|
|
348
|
+
* the manifest on disk rather than from the projection is what makes this cover the tree the
|
|
349
|
+
* launcher actually boots.
|
|
350
|
+
*
|
|
351
|
+
* @param profileDirectory - profile whose node_modules was just installed.
|
|
352
|
+
*/
|
|
353
|
+
export function assertProfileBundles(profileDirectory) {
|
|
354
|
+
const manifest = JSON.parse(readFileSync(join(profileDirectory, 'package.json'), 'utf8'));
|
|
355
|
+
const missing = [];
|
|
356
|
+
for (const name of manifest.dsh?.profile?.bundles ?? []) {
|
|
357
|
+
if (!existsSync(join(profileDirectory, 'node_modules', name, 'package.json')))
|
|
358
|
+
missing.push(name);
|
|
359
|
+
}
|
|
360
|
+
if (missing.length > 0) {
|
|
361
|
+
throw new Error('the profile mounts bundles it did not install: ' + missing.join(', '));
|
|
362
|
+
}
|
|
266
363
|
}
|
|
267
364
|
/**
|
|
268
365
|
* Install the profile from the first registry that answers.
|
|
@@ -273,21 +370,13 @@ export function installProfilePackages(paths, consumerDirectory) {
|
|
|
273
370
|
*
|
|
274
371
|
* @param paths - resolved standalone paths.
|
|
275
372
|
*/
|
|
276
|
-
function installWithRegistryFallback(
|
|
373
|
+
function installWithRegistryFallback(profileDirectory) {
|
|
277
374
|
const registries = registryOrder();
|
|
278
375
|
for (const [index, registry] of registries.entries()) {
|
|
279
|
-
// A lockfile written against the previous registry keeps its resolved URLs, so the
|
|
280
|
-
// second attempt would fetch from the registry that just failed. Drop it and resolve
|
|
281
|
-
// against the registry being tried.
|
|
282
|
-
if (index > 0)
|
|
283
|
-
rmSync(join(paths.profileDirectory, 'pnpm-lock.yaml'), { force: true });
|
|
284
376
|
const result = spawnSync('pnpm', ['install', '--no-frozen-lockfile', '--registry', registry], {
|
|
285
|
-
cwd:
|
|
377
|
+
cwd: profileDirectory,
|
|
286
378
|
stdio: 'inherit',
|
|
287
379
|
shell: process.platform === 'win32',
|
|
288
|
-
// A registry that accepts the connection but never answers would otherwise hold the
|
|
289
|
-
// first start for pnpm's full retry budget before the fallback got a chance.
|
|
290
|
-
timeout: 5 * 60_000,
|
|
291
380
|
});
|
|
292
381
|
if (result.error !== undefined)
|
|
293
382
|
throw result.error;
|
|
@@ -316,22 +405,54 @@ function installWithRegistryFallback(paths) {
|
|
|
316
405
|
*
|
|
317
406
|
* @param profileModules - the profile's \`node_modules\` directory.
|
|
318
407
|
*/
|
|
319
|
-
export function alignReplacedPackageNames(profileModules) {
|
|
320
|
-
|
|
321
|
-
if (!existsSync(scoped))
|
|
408
|
+
export function alignReplacedPackageNames(profileModules, omitted = new Set()) {
|
|
409
|
+
if (!existsSync(profileModules))
|
|
322
410
|
return;
|
|
323
|
-
|
|
324
|
-
|
|
411
|
+
// A replaced package is installed at the name its loader row declares, but the manifest inside
|
|
412
|
+
// still names our scope: the client module system requires the directory's package to declare
|
|
413
|
+
// the directory name, so the panels it renders would never register. Both scopes are walked
|
|
414
|
+
// because a row can be an unscoped alias (`dsh-better-sidebar`) onto a scoped build.
|
|
415
|
+
const align = (expected, manifestPath) => {
|
|
325
416
|
if (!existsSync(manifestPath))
|
|
326
|
-
|
|
417
|
+
return;
|
|
327
418
|
const declared = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
continue;
|
|
331
|
-
const replaced = { ...declared, name: expected };
|
|
419
|
+
if (declared.name === expected || omitted.has(expected))
|
|
420
|
+
return;
|
|
332
421
|
const temporary = manifestPath + '.dsh-name';
|
|
333
|
-
writeFileSync(temporary, JSON.stringify(
|
|
422
|
+
writeFileSync(temporary, JSON.stringify({ ...declared, name: expected }, null, 2) + '\n');
|
|
334
423
|
renameSync(temporary, manifestPath);
|
|
424
|
+
};
|
|
425
|
+
for (const entry of readdirSync(profileModules)) {
|
|
426
|
+
if (entry.startsWith('.'))
|
|
427
|
+
continue;
|
|
428
|
+
if (!entry.startsWith('@')) {
|
|
429
|
+
// An unscoped row is an npm alias onto a scoped build. The package imports its real
|
|
430
|
+
// scoped name, so renaming it would break its own imports; exposing the scoped name as
|
|
431
|
+
// a link is what makes both the row and the package resolve.
|
|
432
|
+
const manifestPath = join(profileModules, entry, 'package.json');
|
|
433
|
+
if (!existsSync(manifestPath))
|
|
434
|
+
continue;
|
|
435
|
+
const declared = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
436
|
+
if (declared.name === undefined || declared.name === entry)
|
|
437
|
+
continue;
|
|
438
|
+
const [scope, name] = declared.name.split('/');
|
|
439
|
+
if (scope === undefined || name === undefined)
|
|
440
|
+
continue;
|
|
441
|
+
mkdirSync(join(profileModules, scope), { recursive: true });
|
|
442
|
+
const link = join(profileModules, scope, name);
|
|
443
|
+
if (existsSync(link))
|
|
444
|
+
continue;
|
|
445
|
+
try {
|
|
446
|
+
symlinkSync(join('..', entry), link, 'junction');
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
// A concurrent install may have created the same link; the existing one is equivalent.
|
|
450
|
+
}
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
for (const scoped of readdirSync(join(profileModules, entry))) {
|
|
454
|
+
align(entry + '/' + scoped, join(profileModules, entry, scoped, 'package.json'));
|
|
455
|
+
}
|
|
335
456
|
}
|
|
336
457
|
}
|
|
337
458
|
/** The npm registry the distribution installs from by default. */
|
|
@@ -350,12 +471,8 @@ const MAINLAND_REGISTRY = 'https://registry.npmmirror.com';
|
|
|
350
471
|
*/
|
|
351
472
|
export function registryOrder() {
|
|
352
473
|
const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
|
|
353
|
-
if (configured !== undefined && configured !== '')
|
|
354
|
-
|
|
355
|
-
// set to the mirror used to fall back to the same mirror and fail twice.
|
|
356
|
-
const fallback = configured === MAINLAND_REGISTRY ? OFFICIAL_REGISTRY : MAINLAND_REGISTRY;
|
|
357
|
-
return [configured, fallback];
|
|
358
|
-
}
|
|
474
|
+
if (configured !== undefined && configured !== '')
|
|
475
|
+
return [configured, MAINLAND_REGISTRY];
|
|
359
476
|
// Intl reports the system locale on every platform. The POSIX variables are empty on
|
|
360
477
|
// Windows, so reading them alone classified every Windows console as non-mainland and
|
|
361
478
|
// reached the origin first — measured in a consumer's log, which showed npmjs failing
|
|
@@ -447,15 +564,30 @@ function linkBundle(consumerModules, name, source) {
|
|
|
447
564
|
}
|
|
448
565
|
}
|
|
449
566
|
/** Resolve every path a command needs, without creating anything. */
|
|
450
|
-
export function resolvePaths(anchor, env = process.env) {
|
|
567
|
+
export function resolvePaths(anchor, env = process.env, editionId) {
|
|
451
568
|
const home = resolveHome(env);
|
|
452
|
-
const
|
|
569
|
+
const distributionDirectory = resolveDistributionDirectory(anchor);
|
|
570
|
+
// An edition is a fact of the distribution now, so the profile name and the omission set come
|
|
571
|
+
// from it. Without one, a legacy forwarder package may still declare them; the distribution's
|
|
572
|
+
// own default profile is the fallback.
|
|
573
|
+
let profileName = STANDALONE_PROFILE;
|
|
574
|
+
let omittedPackages = {};
|
|
575
|
+
if (editionId !== undefined) {
|
|
576
|
+
const projection = projectEdition(readDistributionFacts(distributionDirectory), editionId);
|
|
577
|
+
profileName = projection.profileName;
|
|
578
|
+
omittedPackages = projection.omittedPackages;
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
const declaration = readStandaloneDeclaration(anchor);
|
|
582
|
+
profileName = declaration.profileName;
|
|
583
|
+
omittedPackages = declaration.omittedPackages;
|
|
584
|
+
}
|
|
453
585
|
return {
|
|
454
586
|
home,
|
|
455
|
-
profileName
|
|
456
|
-
omittedPackages
|
|
457
|
-
profileDirectory: join(home, 'profiles',
|
|
458
|
-
distributionDirectory
|
|
587
|
+
profileName,
|
|
588
|
+
omittedPackages,
|
|
589
|
+
profileDirectory: join(home, 'profiles', profileName),
|
|
590
|
+
distributionDirectory,
|
|
459
591
|
};
|
|
460
592
|
}
|
|
461
593
|
/**
|
|
@@ -471,140 +603,51 @@ export function resolvePaths(anchor, env = process.env) {
|
|
|
471
603
|
* that installed them.
|
|
472
604
|
*
|
|
473
605
|
* @param paths - resolved standalone paths.
|
|
474
|
-
* @param
|
|
475
|
-
* @param
|
|
606
|
+
* @param editionId - product edition to materialize.
|
|
607
|
+
* @param extraExcludes - packages the caller drops on top of the edition's own exclusions.
|
|
608
|
+
* @param dryRun - write the profile files without installing their dependency tree.
|
|
476
609
|
* @returns whether this call created the manifest.
|
|
477
610
|
*/
|
|
478
|
-
export function ensureProfile(paths,
|
|
611
|
+
export function ensureProfile(paths, editionId, extraExcludes = [], dryRun = false) {
|
|
479
612
|
const manifestPath = join(paths.profileDirectory, 'package.json');
|
|
480
|
-
const distribution = readDistributionProfile(paths.distributionDirectory);
|
|
481
|
-
const composition = edition === undefined ? undefined : distribution.editions[edition];
|
|
482
|
-
if (edition !== undefined && composition === undefined) {
|
|
483
|
-
const offered = Object.keys(distribution.editions);
|
|
484
|
-
throw new Error('unknown edition "' + edition + '"; this release offers '
|
|
485
|
-
+ (offered.length === 0 ? '(none)' : offered.join(', ')));
|
|
486
|
-
}
|
|
487
|
-
// An edition lists only third-party, fork, and self-developed plugins. The official
|
|
488
|
-
// runtime is the CLI's responsibility and must not be duplicated in that list.
|
|
489
|
-
for (const name of Object.keys(composition?.packages ?? {})) {
|
|
490
|
-
if (name.startsWith('@deepseek-ai/')) {
|
|
491
|
-
throw new Error('edition "' + String(edition) + '" lists the official package ' + name
|
|
492
|
-
+ '; editions contain only third-party or self-developed plugins');
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
const runtime = distribution.dshRange.replace(/^>=?/u, '');
|
|
496
|
-
const baseBundles = composition === undefined
|
|
497
|
-
? distribution.bundles
|
|
498
|
-
: distribution.bundles.filter(bundle => bundle.startsWith('@deepseek-ai/') || bundle === '@sparkelf/dsh-plus');
|
|
499
|
-
const basePackages = composition === undefined
|
|
500
|
-
? distribution.dependencies
|
|
501
|
-
: {
|
|
502
|
-
'@deepseek-ai/dsh': distribution.dshRange,
|
|
503
|
-
'@sparkelf/dsh-plus': distribution.version,
|
|
504
|
-
// `dsh-app-boot` declares its launcher as peer dependencies, and pnpm does not
|
|
505
|
-
// install a profile's transitive peers. Without these the boot loader resolves
|
|
506
|
-
// `@deepseek-ai/cordis-plugin-group` and fails before any plugin mounts.
|
|
507
|
-
'@deepseek-ai/cordis': '^4.0.6-alpha.1',
|
|
508
|
-
'@deepseek-ai/cordis-plugin-group': '^1.0.6-alpha.1',
|
|
509
|
-
'@deepseek-ai/cordis-plugin-include': '^1.0.11-alpha.1',
|
|
510
|
-
'@deepseek-ai/cordis-plugin-loader': '^1.0.7-alpha.1',
|
|
511
|
-
'@deepseek-ai/dsh-home-paths': runtime,
|
|
512
|
-
'@deepseek-ai/dsh-launch-environment': runtime,
|
|
513
|
-
// `dsh-session` / `dsh-tools` declare the rest of the runtime as peers; pnpm leaves
|
|
514
|
-
// them out of a profile install for the same reason as the launcher peers above.
|
|
515
|
-
'@deepseek-ai/dsh-agent': runtime,
|
|
516
|
-
'@deepseek-ai/dsh-llm': runtime,
|
|
517
|
-
'@deepseek-ai/dsh-scope': runtime,
|
|
518
|
-
'@deepseek-ai/dsh-session': runtime,
|
|
519
|
-
'@deepseek-ai/dsh-system-prompt': runtime,
|
|
520
|
-
'@deepseek-ai/dsh-user-approval': runtime,
|
|
521
|
-
// Capability packages that the official plugins import but declare only as peers.
|
|
522
|
-
// A profile install leaves them out and boot then fails inside an otherwise
|
|
523
|
-
// healthy plugin's import with ERR_MODULE_NOT_FOUND.
|
|
524
|
-
'@deepseek-ai/dsh-attachment': runtime,
|
|
525
|
-
'@deepseek-ai/dsh-compaction': runtime,
|
|
526
|
-
'@deepseek-ai/dsh-fs': runtime,
|
|
527
|
-
'@deepseek-ai/dsh-shell': runtime,
|
|
528
|
-
'@deepseek-ai/dsh-spill': runtime,
|
|
529
|
-
'@deepseek-ai/dsh-sandbox': runtime,
|
|
530
|
-
'@deepseek-ai/dsh-ptc-runtime': runtime,
|
|
531
|
-
'@deepseek-ai/dsh-llm-deepseek': runtime,
|
|
532
|
-
'@deepseek-ai/dsh-deepseek-account': runtime,
|
|
533
|
-
'@deepseek-ai/dsh-anonymous-user-id': runtime,
|
|
534
|
-
'@deepseek-ai/dsh-bash-local': runtime,
|
|
535
|
-
'@deepseek-ai/dsh-client-store': runtime,
|
|
536
|
-
'@deepseek-ai/dsh-jobs': runtime,
|
|
537
|
-
'@deepseek-ai/dsh-sdk-protocol': runtime,
|
|
538
|
-
'@deepseek-ai/dsh-session-persistence': runtime,
|
|
539
|
-
'@deepseek-ai/dsh-session-telemetry': runtime,
|
|
540
|
-
'@deepseek-ai/dsh-session-title-llm': runtime,
|
|
541
|
-
'@deepseek-ai/dsh-util-workspace-path': runtime,
|
|
542
|
-
'@deepseek-ai/dsh-workflow': runtime,
|
|
543
|
-
...Object.fromEntries(baseBundles
|
|
544
|
-
.filter(bundle => bundle.startsWith('@deepseek-ai/dsh-'))
|
|
545
|
-
.map(bundle => [bundle, runtime])),
|
|
546
|
-
};
|
|
547
|
-
const compositionPackages = composition === undefined
|
|
548
|
-
? basePackages
|
|
549
|
-
: { ...basePackages, ...composition.packages };
|
|
550
|
-
// Official cordis rows name the historical unscoped packages. The fork ships under
|
|
551
|
-
// `@sparkelf/*`, so the row would fail to resolve unless the profile also carries the
|
|
552
|
-
// historical name as an alias to the same version.
|
|
553
|
-
const rowAliases = {
|
|
554
|
-
'dsh-better-sidebar': '@sparkelf/dsh-better-sidebar',
|
|
555
|
-
'dsh-sql-workbench': '@sparkelf/dsh-sql-workbench',
|
|
556
|
-
};
|
|
557
|
-
for (const [alias, target] of Object.entries(rowAliases)) {
|
|
558
|
-
const spec = compositionPackages[target];
|
|
559
|
-
if (spec !== undefined && compositionPackages[alias] === undefined) {
|
|
560
|
-
compositionPackages[alias] = 'npm:' + target + '@' + spec;
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
const compositionBundles = composition === undefined
|
|
564
|
-
? baseBundles
|
|
565
|
-
: [...baseBundles, ...composition.bundles];
|
|
613
|
+
const distribution = readDistributionProfile(paths.distributionDirectory, editionId, extraExcludes);
|
|
566
614
|
if (existsSync(manifestPath)) {
|
|
567
|
-
// A selected edition is an explicit install choice, so its packages and bundle order
|
|
568
|
-
// replace the profile's rather than being ignored. Other profile decisions stay.
|
|
569
|
-
if (composition !== undefined) {
|
|
570
|
-
const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), 'Plus profile manifest');
|
|
571
|
-
manifest.dependencies = { ...manifest.dependencies, ...compositionPackages };
|
|
572
|
-
manifest.dsh = {
|
|
573
|
-
...manifest.dsh,
|
|
574
|
-
profile: {
|
|
575
|
-
...manifest.dsh?.profile,
|
|
576
|
-
bundles: [...compositionBundles],
|
|
577
|
-
...(edition === undefined ? {} : { plusEdition: edition }),
|
|
578
|
-
},
|
|
579
|
-
};
|
|
580
|
-
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
581
|
-
}
|
|
582
615
|
// The workspace carries decisions the distribution owns — overrides and the build
|
|
583
616
|
// script allowlist — and a distribution release changes them. Rewriting on every
|
|
584
617
|
// start is what lets an upgraded installation receive the new values; a profile
|
|
585
618
|
// written once keeps whatever its own release decided and can never be corrected.
|
|
586
|
-
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
|
|
587
|
-
|
|
619
|
+
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, { ...paths.omittedPackages, ...distribution.omittedPackages });
|
|
620
|
+
if (!dryRun) {
|
|
621
|
+
installProfilePackages(paths, new Set(Object.keys(distribution.omittedPackages)), distribution.runtimeVersion, registryOrder()[0]);
|
|
622
|
+
}
|
|
588
623
|
return false;
|
|
589
624
|
}
|
|
590
625
|
mkdirSync(paths.profileDirectory, { recursive: true });
|
|
591
626
|
const manifest = {
|
|
592
|
-
name: 'dsh-profile-' +
|
|
627
|
+
name: 'dsh-profile-' + paths.profileName,
|
|
593
628
|
private: true,
|
|
594
629
|
type: 'module',
|
|
595
|
-
dependencies:
|
|
630
|
+
dependencies: {
|
|
631
|
+
'@sparkelf/dsh-plus': distribution.version,
|
|
632
|
+
// The launcher's own tree supplies every service package the bundles mount. A
|
|
633
|
+
// profile that lists only the distribution's plugins installs a partial tree and
|
|
634
|
+
// fails at load with a module the launcher would have carried.
|
|
635
|
+
'@deepseek-ai/dsh': distribution.dshRange,
|
|
636
|
+
...distribution.dependencies,
|
|
637
|
+
},
|
|
596
638
|
dsh: {
|
|
597
639
|
profile: {
|
|
598
|
-
bundles:
|
|
640
|
+
bundles: distribution.bundles,
|
|
599
641
|
patchReload: 'live',
|
|
600
|
-
...(edition === undefined ? {} : { plusEdition: edition }),
|
|
601
642
|
},
|
|
602
643
|
},
|
|
603
644
|
};
|
|
604
645
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
605
646
|
// The overrides must reach the workspace before the install that reads them.
|
|
606
|
-
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
|
|
607
|
-
|
|
647
|
+
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, { ...paths.omittedPackages, ...distribution.omittedPackages });
|
|
648
|
+
if (!dryRun) {
|
|
649
|
+
installProfilePackages(paths, new Set(Object.keys(distribution.omittedPackages)), distribution.runtimeVersion, registryOrder()[0]);
|
|
650
|
+
}
|
|
608
651
|
return true;
|
|
609
652
|
}
|
|
610
653
|
/**
|
|
@@ -642,7 +685,7 @@ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omitted
|
|
|
642
685
|
if (document.get('nodeLinker') === undefined)
|
|
643
686
|
document.set('nodeLinker', 'hoisted');
|
|
644
687
|
if (document.get('autoInstallPeers') === undefined)
|
|
645
|
-
document.set('autoInstallPeers',
|
|
688
|
+
document.set('autoInstallPeers', false);
|
|
646
689
|
writeFileSync(workspacePath, String(document));
|
|
647
690
|
}
|
|
648
691
|
/** Run git in one directory, returning undefined instead of throwing when asked to. */
|
|
@@ -676,8 +719,6 @@ export function applyProfileNpmPatches(distributionDirectory, profileDirectory)
|
|
|
676
719
|
const patchSet = readPatchSet(distributionDirectory, manifest);
|
|
677
720
|
const applied = [];
|
|
678
721
|
for (const entry of [...patchSet.entries, ...patchSet.thirdPartyPatches]) {
|
|
679
|
-
// Only npm targets reach an installed package; a source target needs the
|
|
680
|
-
// official checkout, which a standalone installation does not have.
|
|
681
722
|
if (entry.target.kind !== 'npm')
|
|
682
723
|
continue;
|
|
683
724
|
const targetName = entry.target.name;
|
|
@@ -698,22 +739,7 @@ function resolveInstalledPackage(from, packageName) {
|
|
|
698
739
|
manifestPath = requireFrom.resolve(packageName + '/package.json');
|
|
699
740
|
}
|
|
700
741
|
catch {
|
|
701
|
-
|
|
702
|
-
// up to the directory that holds the manifest.
|
|
703
|
-
let directory;
|
|
704
|
-
try {
|
|
705
|
-
directory = dirname(requireFrom.resolve(packageName));
|
|
706
|
-
}
|
|
707
|
-
catch {
|
|
708
|
-
throw new Error(packageName + ' is not installed under ' + from);
|
|
709
|
-
}
|
|
710
|
-
while (!existsSync(resolve(directory, 'package.json'))) {
|
|
711
|
-
const parent = dirname(directory);
|
|
712
|
-
if (parent === directory)
|
|
713
|
-
throw new Error(packageName + ' has no resolvable package manifest under ' + from);
|
|
714
|
-
directory = parent;
|
|
715
|
-
}
|
|
716
|
-
manifestPath = resolve(directory, 'package.json');
|
|
742
|
+
throw new Error(packageName + ' is not installed under ' + from);
|
|
717
743
|
}
|
|
718
744
|
const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
|
|
719
745
|
return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };
|