@sparkelf/dsh-plus 0.1.0-rc.9 → 0.2.0-rc.2

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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Runtime paths and profile composition for a standalone Plus installation.
3
+ *
4
+ * A standalone consumer installs the distribution from the registry and owns its
5
+ * profile, so this module answers the two questions every command shares: where the
6
+ * DSH home and profile live, and what the profile manifest must contain. The bundle
7
+ * order comes from the installed distribution rather than from a copy here, because
8
+ * the launcher mounts exactly the bundles the profile names and nothing expands a
9
+ * bundle's own list.
10
+ */
11
+ /** Profile name a standalone installation owns. */
12
+ export declare const STANDALONE_PROFILE = "plus";
13
+ /** Resolved locations for one standalone installation. */
14
+ export interface StandalonePaths {
15
+ /** DSH home holding profiles, credentials, and session data. */
16
+ readonly home: string;
17
+ /** Profile directory the launcher boots. */
18
+ readonly profileDirectory: string;
19
+ /** Installed Plus distribution directory. */
20
+ readonly distributionDirectory: string;
21
+ }
22
+ /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
23
+ export declare function resolveHome(env?: NodeJS.ProcessEnv): string;
24
+ /**
25
+ * Whether one absolute path is the other or sits beneath it.
26
+ *
27
+ * A path outside the parent has a relative form starting with `..`, which every
28
+ * platform answers the same way. Comparing the text against a literal separator is
29
+ * not: Windows separates with a backslash, so a nested path never matched its own
30
+ * ancestor and every Windows installation reported the distribution as sitting
31
+ * outside its own tree.
32
+ *
33
+ * @param candidate - absolute path to test.
34
+ * @param parent - absolute path that may contain it.
35
+ * @param platform - separating convention, injectable so the Windows answer is
36
+ * testable where the suite runs on a POSIX host.
37
+ * @returns true when the candidate is the parent or inside it.
38
+ */
39
+ export declare function isWithin(candidate: string, parent: string, platform?: NodeJS.Platform): boolean;
40
+ /**
41
+ * Resolve the installed distribution directory from one requiring anchor.
42
+ *
43
+ * The anchor must be a path inside the consumer's own tree. Resolving from this
44
+ * module instead would find the distribution the module itself was loaded from,
45
+ * which is the repository during development and the consumer's install otherwise.
46
+ *
47
+ * @param anchor - `package.json` path or directory in the consumer tree.
48
+ * @returns absolute path to the installed distribution.
49
+ */
50
+ export declare function resolveDistributionDirectory(anchor: string): string;
51
+ /** The reviewed bundle order and pins the installed distribution declares. */
52
+ export declare function readDistributionProfile(distributionDirectory: string): {
53
+ readonly name: string;
54
+ readonly bundles: readonly string[];
55
+ readonly dependencies: Readonly<Record<string, string>>;
56
+ readonly allowBuilds: Readonly<Record<string, boolean>>;
57
+ readonly overrides: Readonly<Record<string, string>>;
58
+ readonly dshRange: string;
59
+ readonly version: string;
60
+ };
61
+ /**
62
+ * Give the profile its own installed tree, built from the distribution's declarations.
63
+ *
64
+ * The launcher resolves a bundle from the profile directory, so the profile needs its
65
+ * own `node_modules`. Pointing it at the consumer's tree was cheaper, but it made the
66
+ * profile inherit whatever npm had already installed — including the official packages
67
+ * the distribution's `overrides` exist to replace. npm applies `overrides` only from a
68
+ * project's own root, so a profile without its own tree cannot receive them at all, and
69
+ * a patch delivered that way silently never arrives.
70
+ *
71
+ * Installing here makes the profile that root: pnpm reads `overrides` from the
72
+ * profile's own `pnpm-workspace.yaml`, which `writeProfileOverrides` writes before this
73
+ * runs. The install is skipped once the tree exists so a start does not pay for it
74
+ * twice; `dsh-plus apply` remains the command that reinstalls after a change.
75
+ *
76
+ * @param paths - resolved standalone paths.
77
+ * @param consumerDirectory - directory whose `node_modules` holds the installation.
78
+ */
79
+ export declare function installProfilePackages(paths: StandalonePaths, consumerDirectory: string): void;
80
+ /**
81
+ * Make each replaced package declare the name of the location it occupies.
82
+ *
83
+ * \`overrides\` installs our build at the official path, but the manifest inside still
84
+ * names our scope. The client module system resolves a loader entry's declared name and
85
+ * then requires the manifest it finds to declare that same name
86
+ * (\`client/modules\`: \`name === expectedPackageName\`); a mismatch makes the package own no
87
+ * browser module at all. The symptom is silent — the packages install, the server starts,
88
+ * and the panels those packages render simply never appear.
89
+ *
90
+ * The rewrite replaces the file rather than writing through it: pnpm hard-links a package
91
+ * manifest into its content-addressed store, so an in-place write would edit every
92
+ * profile sharing that store entry.
93
+ *
94
+ * @param profileModules - the profile's \`node_modules\` directory.
95
+ */
96
+ export declare function alignReplacedPackageNames(profileModules: string): void;
97
+ /**
98
+ * The registries to try, in order, for this machine.
99
+ *
100
+ * A mainland locale reaches the mirror faster than the origin, which is why the Desktop
101
+ * installer already prefers it there. The same choice belongs to the profile install: it
102
+ * fetches a full dependency closure, so the delay is the first thing a consumer notices.
103
+ * `DSH_PLUS_INSTALL_REGISTRY` overrides the choice, and a failure falls back once.
104
+ *
105
+ * @returns registries to try in order.
106
+ */
107
+ export declare function registryOrder(): readonly string[];
108
+ /**
109
+ * Report whether pnpm can run.
110
+ *
111
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
112
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
113
+ * first turns a cryptic failure from the install into a message naming what is missing.
114
+ *
115
+ * @returns \`true\` when pnpm answers with a version.
116
+ */
117
+ export declare function pnpmAvailable(): boolean;
118
+ /**
119
+ * The commands that install pnpm, in the order worth trying.
120
+ *
121
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
122
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
123
+ * toolchain, which is what lets the first start offer to install rather than only report.
124
+ *
125
+ * @returns commands to try in order, stopping at the first that works.
126
+ */
127
+ export declare function pnpmInstallCommands(): readonly string[];
128
+ /**
129
+ * The command to show a consumer who declines the automatic install.
130
+ *
131
+ * @returns the preferred command, which is the one the offer runs first.
132
+ */
133
+ export declare function pnpmInstallCommand(): string;
134
+ /** Resolve every path a command needs, without creating anything. */
135
+ export declare function resolvePaths(anchor: string, env?: NodeJS.ProcessEnv): StandalonePaths;
136
+ /**
137
+ * Write the profile manifest when it is absent, leaving an existing one untouched.
138
+ *
139
+ * The distribution's own plugin dependencies are seeded here so the launcher can
140
+ * resolve every bundle from the profile directory: a bundle the profile cannot
141
+ * resolve fails activation with a module-resolution error, and no step in the
142
+ * launcher expands the distribution's bundle list on its own.
143
+ *
144
+ * The profile also links the consumer's installed packages, because the launcher
145
+ * resolves each bundle from the profile directory rather than from the directory
146
+ * that installed them.
147
+ *
148
+ * @param paths - resolved standalone paths.
149
+ * @param consumerDirectory - directory whose `node_modules` holds the installed packages.
150
+ * @returns whether this call created the manifest.
151
+ */
152
+ export declare function ensureProfile(paths: StandalonePaths, consumerDirectory: string): boolean;
153
+ /**
154
+ * Apply the reviewed npm-target patches to the profile's installed packages.
155
+ *
156
+ * A standalone installation runs no `apply` step: it installs the distribution from
157
+ * the registry, links the consumer's packages, and starts. The npm patches a
158
+ * distribution declares therefore need an owner that does not require an official
159
+ * source checkout — the source half of the apply step needs one, this does not.
160
+ *
161
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
162
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
163
+ * which is why this runs on every start rather than once.
164
+ *
165
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
166
+ * @param profileDirectory - the standalone profile directory.
167
+ * @returns the labels of the patches that were applied.
168
+ */
169
+ export declare function applyProfileNpmPatches(distributionDirectory: string, profileDirectory: string): string[];
170
+ //# sourceMappingURL=standalone-profile.d.ts.map
@@ -0,0 +1,504 @@
1
+ /**
2
+ * Runtime paths and profile composition for a standalone Plus installation.
3
+ *
4
+ * A standalone consumer installs the distribution from the registry and owns its
5
+ * profile, so this module answers the two questions every command shares: where the
6
+ * DSH home and profile live, and what the profile manifest must contain. The bundle
7
+ * order comes from the installed distribution rather than from a copy here, because
8
+ * the launcher mounts exactly the bundles the profile names and nothing expands a
9
+ * bundle's own list.
10
+ */
11
+ import { spawnSync } from 'node:child_process';
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, symlinkSync, writeFileSync } from 'node:fs';
13
+ import { createRequire } from 'node:module';
14
+ import { homedir } from 'node:os';
15
+ import { dirname, join, posix, resolve, win32 } from 'node:path';
16
+ import { parseDocument } from 'yaml';
17
+ /** Profile name a standalone installation owns. */
18
+ export const STANDALONE_PROFILE = 'plus';
19
+ function requireRecord(value, label) {
20
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
21
+ throw new Error(label + ' must be an object');
22
+ return value;
23
+ }
24
+ function requireStringArray(value, label) {
25
+ if (!Array.isArray(value) || value.some(entry => typeof entry !== 'string'))
26
+ throw new Error(label + ' must be a string array');
27
+ return value;
28
+ }
29
+ /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
30
+ export function resolveHome(env = process.env) {
31
+ const configured = env.DSH_HOME;
32
+ return configured === undefined || configured === '' ? join(homedir(), '.dsh') : resolve(configured);
33
+ }
34
+ /**
35
+ * Whether one absolute path is the other or sits beneath it.
36
+ *
37
+ * A path outside the parent has a relative form starting with `..`, which every
38
+ * platform answers the same way. Comparing the text against a literal separator is
39
+ * not: Windows separates with a backslash, so a nested path never matched its own
40
+ * ancestor and every Windows installation reported the distribution as sitting
41
+ * outside its own tree.
42
+ *
43
+ * @param candidate - absolute path to test.
44
+ * @param parent - absolute path that may contain it.
45
+ * @param platform - separating convention, injectable so the Windows answer is
46
+ * testable where the suite runs on a POSIX host.
47
+ * @returns true when the candidate is the parent or inside it.
48
+ */
49
+ export function isWithin(candidate, parent, platform = process.platform) {
50
+ if (candidate === parent)
51
+ return true;
52
+ const path = platform === 'win32' ? win32 : posix;
53
+ const inside = path.relative(parent, candidate);
54
+ return inside !== '' && !inside.startsWith('..') && !inside.startsWith(path.sep + '..');
55
+ }
56
+ /**
57
+ * Resolve the installed distribution directory from one requiring anchor.
58
+ *
59
+ * The anchor must be a path inside the consumer's own tree. Resolving from this
60
+ * module instead would find the distribution the module itself was loaded from,
61
+ * which is the repository during development and the consumer's install otherwise.
62
+ *
63
+ * @param anchor - `package.json` path or directory in the consumer tree.
64
+ * @returns absolute path to the installed distribution.
65
+ */
66
+ export function resolveDistributionDirectory(anchor) {
67
+ let resolved;
68
+ try {
69
+ resolved = dirname(createRequire(anchor).resolve('@sparkelf/dsh-plus/package.json'));
70
+ }
71
+ catch {
72
+ throw new Error('@sparkelf/dsh-plus is not installed in ' + dirname(anchor) + '; run this command from the directory that installed it');
73
+ }
74
+ // A resolution that leaves the consumer tree means the module answered from its own
75
+ // installation, which reports health for a distribution the consumer never installed.
76
+ // The comparison walks up from the anchor because the resolved package sits under the
77
+ // anchor's \`node_modules\`, not beside it: requiring \`<root>/package.json\` legitimately
78
+ // reports \`<root>/node_modules/@sparkelf/dsh-plus\`.
79
+ let current = resolve(dirname(anchor));
80
+ for (;;) {
81
+ if (isWithin(resolved, current))
82
+ return resolved;
83
+ const parent = dirname(current);
84
+ if (parent === current)
85
+ break;
86
+ current = parent;
87
+ }
88
+ throw new Error('@sparkelf/dsh-plus resolved outside ' + resolve(dirname(anchor)) + ' (found ' + resolved + '); run this command from the directory that installed it');
89
+ }
90
+ /** The reviewed bundle order and pins the installed distribution declares. */
91
+ export function readDistributionProfile(distributionDirectory) {
92
+ const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
93
+ const plus = requireRecord(manifest.dshPlus, 'dshPlus');
94
+ const profile = requireRecord(plus.profile, 'dshPlus.profile');
95
+ const rawDependencies = requireRecord(profile.dependencies, 'dshPlus.profile.dependencies');
96
+ const rawAllowBuilds = requireRecord(profile.allowBuilds, 'dshPlus.profile.allowBuilds');
97
+ const dependencies = {};
98
+ for (const [name, spec] of Object.entries(rawDependencies)) {
99
+ if (typeof spec !== 'string' || spec === '')
100
+ throw new Error('dshPlus.profile.dependencies.' + name + ' must be a non-empty string');
101
+ dependencies[name] = spec;
102
+ }
103
+ const allowBuilds = {};
104
+ for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
105
+ if (typeof allowed !== 'boolean')
106
+ throw new Error('dshPlus.profile.allowBuilds.' + name + ' must be a boolean');
107
+ allowBuilds[name] = allowed;
108
+ }
109
+ // An override substitutes our republished build for an official package by name: the
110
+ // built code imports the official specifier, so the installed location has to keep
111
+ // that name while its contents come from ours.
112
+ const overrides = {};
113
+ const rawOverrides = profile.overrides === undefined
114
+ ? {}
115
+ : requireRecord(profile.overrides, 'dshPlus.profile.overrides');
116
+ for (const [name, spec] of Object.entries(rawOverrides)) {
117
+ if (typeof spec !== 'string' || spec === '')
118
+ throw new Error('dshPlus.profile.overrides.' + name + ' must be a non-empty string');
119
+ overrides[name] = spec;
120
+ }
121
+ const compatibility = requireRecord(plus.compatibility, 'dshPlus.compatibility');
122
+ return {
123
+ name: String(manifest.name),
124
+ bundles: requireStringArray(profile.bundles, 'dshPlus.profile.bundles'),
125
+ dependencies,
126
+ allowBuilds,
127
+ overrides,
128
+ dshRange: String(compatibility.dsh),
129
+ version: String(manifest.version),
130
+ };
131
+ }
132
+ /**
133
+ * Give the profile its own installed tree, built from the distribution's declarations.
134
+ *
135
+ * The launcher resolves a bundle from the profile directory, so the profile needs its
136
+ * own `node_modules`. Pointing it at the consumer's tree was cheaper, but it made the
137
+ * profile inherit whatever npm had already installed — including the official packages
138
+ * the distribution's `overrides` exist to replace. npm applies `overrides` only from a
139
+ * project's own root, so a profile without its own tree cannot receive them at all, and
140
+ * a patch delivered that way silently never arrives.
141
+ *
142
+ * Installing here makes the profile that root: pnpm reads `overrides` from the
143
+ * profile's own `pnpm-workspace.yaml`, which `writeProfileOverrides` writes before this
144
+ * runs. The install is skipped once the tree exists so a start does not pay for it
145
+ * twice; `dsh-plus apply` remains the command that reinstalls after a change.
146
+ *
147
+ * @param paths - resolved standalone paths.
148
+ * @param consumerDirectory - directory whose `node_modules` holds the installation.
149
+ */
150
+ export function installProfilePackages(paths, consumerDirectory) {
151
+ const consumerModules = join(consumerDirectory, 'node_modules');
152
+ if (!existsSync(consumerModules)) {
153
+ throw new Error('no node_modules in ' + consumerDirectory + '; run npm install there first');
154
+ }
155
+ const profileModules = join(paths.profileDirectory, 'node_modules');
156
+ if (existsSync(profileModules)) {
157
+ // A profile installed before the distribution declared overrides still needs the
158
+ // packages it reaches from the consumer tree, which npm nested rather than hoisted.
159
+ linkNestedBundles(paths, profileModules);
160
+ return;
161
+ }
162
+ installWithRegistryFallback(paths);
163
+ alignReplacedPackageNames(profileModules);
164
+ linkNestedBundles(paths, profileModules);
165
+ }
166
+ /**
167
+ * Install the profile from the first registry that answers.
168
+ *
169
+ * The install fetches a full dependency closure, and a mainland consumer reaches the
170
+ * mirror far faster than the origin. Trying the preferred registry and falling back once
171
+ * keeps a first start quick without failing when the preferred one is unreachable.
172
+ *
173
+ * @param paths - resolved standalone paths.
174
+ */
175
+ function installWithRegistryFallback(paths) {
176
+ const registries = registryOrder();
177
+ for (const [index, registry] of registries.entries()) {
178
+ const result = spawnSync('pnpm', ['install', '--no-frozen-lockfile', '--registry', registry], {
179
+ cwd: paths.profileDirectory,
180
+ stdio: 'inherit',
181
+ shell: process.platform === 'win32',
182
+ });
183
+ if (result.error !== undefined)
184
+ throw result.error;
185
+ if (result.status === 0)
186
+ return;
187
+ const next = registries[index + 1];
188
+ if (next === undefined) {
189
+ throw new Error('pnpm install in the plus profile failed with exit code ' + String(result.status));
190
+ }
191
+ console.log('Install from ' + registry + ' failed; trying ' + next + '.');
192
+ }
193
+ }
194
+ /**
195
+ * Make each replaced package declare the name of the location it occupies.
196
+ *
197
+ * \`overrides\` installs our build at the official path, but the manifest inside still
198
+ * names our scope. The client module system resolves a loader entry's declared name and
199
+ * then requires the manifest it finds to declare that same name
200
+ * (\`client/modules\`: \`name === expectedPackageName\`); a mismatch makes the package own no
201
+ * browser module at all. The symptom is silent — the packages install, the server starts,
202
+ * and the panels those packages render simply never appear.
203
+ *
204
+ * The rewrite replaces the file rather than writing through it: pnpm hard-links a package
205
+ * manifest into its content-addressed store, so an in-place write would edit every
206
+ * profile sharing that store entry.
207
+ *
208
+ * @param profileModules - the profile's \`node_modules\` directory.
209
+ */
210
+ export function alignReplacedPackageNames(profileModules) {
211
+ const scoped = join(profileModules, '@deepseek-ai');
212
+ if (!existsSync(scoped))
213
+ return;
214
+ for (const entry of readdirSync(scoped)) {
215
+ const manifestPath = join(scoped, entry, 'package.json');
216
+ if (!existsSync(manifestPath))
217
+ continue;
218
+ const declared = JSON.parse(readFileSync(manifestPath, 'utf8'));
219
+ const expected = '@deepseek-ai/' + entry;
220
+ if (declared.name === expected)
221
+ continue;
222
+ const replaced = { ...declared, name: expected };
223
+ const temporary = manifestPath + '.dsh-name';
224
+ writeFileSync(temporary, JSON.stringify(replaced, null, 2) + '\n');
225
+ renameSync(temporary, manifestPath);
226
+ }
227
+ }
228
+ /** The npm registry the distribution installs from by default. */
229
+ const OFFICIAL_REGISTRY = 'https://registry.npmjs.org';
230
+ /** The mainland mirror, which serves the same packages and answers faster there. */
231
+ const MAINLAND_REGISTRY = 'https://registry.npmmirror.com';
232
+ /**
233
+ * The registries to try, in order, for this machine.
234
+ *
235
+ * A mainland locale reaches the mirror faster than the origin, which is why the Desktop
236
+ * installer already prefers it there. The same choice belongs to the profile install: it
237
+ * fetches a full dependency closure, so the delay is the first thing a consumer notices.
238
+ * `DSH_PLUS_INSTALL_REGISTRY` overrides the choice, and a failure falls back once.
239
+ *
240
+ * @returns registries to try in order.
241
+ */
242
+ export function registryOrder() {
243
+ const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
244
+ if (configured !== undefined && configured !== '')
245
+ return [configured, MAINLAND_REGISTRY];
246
+ // Intl reports the system locale on every platform. The POSIX variables are empty on
247
+ // Windows, so reading them alone classified every Windows console as non-mainland and
248
+ // reached the origin first — measured in a consumer's log, which showed npmjs failing
249
+ // before the mirror answered.
250
+ const locale = [process.env.LANG, process.env.LC_ALL, Intl.DateTimeFormat().resolvedOptions().locale]
251
+ .filter(value => value !== undefined)
252
+ .join(' ')
253
+ .toLowerCase();
254
+ const mainlandFirst = locale.includes('zh') || locale.includes('cn');
255
+ return mainlandFirst ? [MAINLAND_REGISTRY, OFFICIAL_REGISTRY] : [OFFICIAL_REGISTRY, MAINLAND_REGISTRY];
256
+ }
257
+ /**
258
+ * Report whether pnpm can run.
259
+ *
260
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
261
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
262
+ * first turns a cryptic failure from the install into a message naming what is missing.
263
+ *
264
+ * @returns \`true\` when pnpm answers with a version.
265
+ */
266
+ export function pnpmAvailable() {
267
+ const probe = spawnSync('pnpm', ['--version'], {
268
+ stdio: 'pipe',
269
+ shell: process.platform === 'win32',
270
+ encoding: 'utf8',
271
+ });
272
+ return probe.status === 0;
273
+ }
274
+ /**
275
+ * The commands that install pnpm, in the order worth trying.
276
+ *
277
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
278
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
279
+ * toolchain, which is what lets the first start offer to install rather than only report.
280
+ *
281
+ * @returns commands to try in order, stopping at the first that works.
282
+ */
283
+ export function pnpmInstallCommands() {
284
+ const registry = registryOrder()[0];
285
+ if (registry === undefined)
286
+ return ['corepack enable pnpm', 'npm install -g pnpm'];
287
+ // Both installers take the registry explicitly. npm does so with a flag; corepack reads
288
+ // COREPACK_NPM_REGISTRY, which the caller sets. A mainland consumer therefore downloads
289
+ // the package from the mirror, for the same reason the profile install prefers it.
290
+ return ['corepack enable pnpm', 'npm install -g pnpm --registry ' + registry];
291
+ }
292
+ /**
293
+ * The command to show a consumer who declines the automatic install.
294
+ *
295
+ * @returns the preferred command, which is the one the offer runs first.
296
+ */
297
+ export function pnpmInstallCommand() {
298
+ return pnpmInstallCommands()[0] ?? 'npm install -g pnpm';
299
+ }
300
+ /**
301
+ * Expose the distribution's nested packages at the top level the profile searches.
302
+ *
303
+ * The profile resolves a bundle from one directory, so a package npm nested under the
304
+ * distribution is invisible there even though the installation carries it. Linking each
305
+ * one beside the hoisted packages keeps a single installed copy and needs no reinstall.
306
+ *
307
+ * @param paths - resolved standalone paths.
308
+ * @param consumerModules - the consumer's `node_modules` directory.
309
+ */
310
+ function linkNestedBundles(paths, consumerModules) {
311
+ const nested = join(paths.distributionDirectory, 'node_modules');
312
+ if (!existsSync(nested))
313
+ return;
314
+ for (const entry of readdirSync(nested)) {
315
+ if (entry.startsWith('@')) {
316
+ for (const scoped of readdirSync(join(nested, entry))) {
317
+ linkBundle(consumerModules, join(entry, scoped), join(nested, entry, scoped));
318
+ }
319
+ continue;
320
+ }
321
+ linkBundle(consumerModules, entry, join(nested, entry));
322
+ }
323
+ }
324
+ /** Link one nested bundle into the consumer's top level when it is not already there. */
325
+ function linkBundle(consumerModules, name, source) {
326
+ const destination = join(consumerModules, name);
327
+ if (existsSync(destination))
328
+ return;
329
+ try {
330
+ symlinkSync(source, destination, 'junction');
331
+ }
332
+ catch {
333
+ // A concurrent start may have created the same link; the existing one is equivalent.
334
+ }
335
+ }
336
+ /** Resolve every path a command needs, without creating anything. */
337
+ export function resolvePaths(anchor, env = process.env) {
338
+ const home = resolveHome(env);
339
+ return {
340
+ home,
341
+ profileDirectory: join(home, 'profiles', STANDALONE_PROFILE),
342
+ distributionDirectory: resolveDistributionDirectory(anchor),
343
+ };
344
+ }
345
+ /**
346
+ * Write the profile manifest when it is absent, leaving an existing one untouched.
347
+ *
348
+ * The distribution's own plugin dependencies are seeded here so the launcher can
349
+ * resolve every bundle from the profile directory: a bundle the profile cannot
350
+ * resolve fails activation with a module-resolution error, and no step in the
351
+ * launcher expands the distribution's bundle list on its own.
352
+ *
353
+ * The profile also links the consumer's installed packages, because the launcher
354
+ * resolves each bundle from the profile directory rather than from the directory
355
+ * that installed them.
356
+ *
357
+ * @param paths - resolved standalone paths.
358
+ * @param consumerDirectory - directory whose `node_modules` holds the installed packages.
359
+ * @returns whether this call created the manifest.
360
+ */
361
+ export function ensureProfile(paths, consumerDirectory) {
362
+ const manifestPath = join(paths.profileDirectory, 'package.json');
363
+ const distribution = readDistributionProfile(paths.distributionDirectory);
364
+ if (existsSync(manifestPath)) {
365
+ // The workspace carries decisions the distribution owns — overrides and the build
366
+ // script allowlist — and a distribution release changes them. Rewriting on every
367
+ // start is what lets an upgraded installation receive the new values; a profile
368
+ // written once keeps whatever its own release decided and can never be corrected.
369
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
370
+ installProfilePackages(paths, consumerDirectory);
371
+ return false;
372
+ }
373
+ mkdirSync(paths.profileDirectory, { recursive: true });
374
+ const manifest = {
375
+ name: 'dsh-profile-' + STANDALONE_PROFILE,
376
+ private: true,
377
+ type: 'module',
378
+ dependencies: {
379
+ '@sparkelf/dsh-plus': distribution.version,
380
+ // The launcher's own tree supplies every service package the bundles mount. A
381
+ // profile that lists only the distribution's plugins installs a partial tree and
382
+ // fails at load with a module the launcher would have carried.
383
+ '@deepseek-ai/dsh': distribution.dshRange,
384
+ ...distribution.dependencies,
385
+ },
386
+ dsh: {
387
+ profile: {
388
+ bundles: distribution.bundles,
389
+ patchReload: 'live',
390
+ },
391
+ },
392
+ };
393
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
394
+ // The overrides must reach the workspace before the install that reads them.
395
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
396
+ installProfilePackages(paths, consumerDirectory);
397
+ return true;
398
+ }
399
+ /**
400
+ * Record the distribution's package substitutions in the profile workspace.
401
+ *
402
+ * pnpm reads \`overrides\` from \`pnpm-workspace.yaml\` since version 10 and ignores the
403
+ * same key in \`package.json\`, so a profile that carried it in the manifest would
404
+ * silently install the official package the override meant to replace.
405
+ *
406
+ * @param profileDirectory - the standalone profile directory.
407
+ * @param overrides - official package name to published replacement spec.
408
+ */
409
+ function writeProfileOverrides(profileDirectory, overrides, allowBuilds) {
410
+ const workspacePath = join(profileDirectory, 'pnpm-workspace.yaml');
411
+ const document = parseDocument(existsSync(workspacePath) ? readFileSync(workspacePath, 'utf8') : '');
412
+ const [documentError] = document.errors;
413
+ if (documentError !== undefined)
414
+ throw new Error('Plus profile workspace is not valid YAML', { cause: documentError });
415
+ if (document.get('packages') === undefined)
416
+ document.set('packages', ['.']);
417
+ for (const [name, spec] of Object.entries(overrides))
418
+ document.setIn(['overrides', name], spec);
419
+ // pnpm refuses an install whose packages want to run build scripts until each is
420
+ // decided, so the distribution's reviewed decisions travel with the install rather
421
+ // than waiting for an interactive approval no start can offer.
422
+ for (const [name, allowed] of Object.entries(allowBuilds))
423
+ document.setIn(['allowBuilds', name], allowed);
424
+ // The profile resolves bundles from this directory, so peers the official tree would
425
+ // supply have to come from what the consumer installed.
426
+ if (document.get('nodeLinker') === undefined)
427
+ document.set('nodeLinker', 'hoisted');
428
+ if (document.get('autoInstallPeers') === undefined)
429
+ document.set('autoInstallPeers', false);
430
+ writeFileSync(workspacePath, String(document));
431
+ }
432
+ /** Run git in one directory, returning undefined instead of throwing when asked to. */
433
+ function git(root, args, acceptFailure = false) {
434
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
435
+ if (result.status === 0)
436
+ return result.stdout.trim();
437
+ if (acceptFailure)
438
+ return undefined;
439
+ const detail = result.stderr.trim();
440
+ throw new Error('git ' + args.join(' ') + ' failed' + (detail === '' ? '' : ': ' + detail));
441
+ }
442
+ /**
443
+ * Apply the reviewed npm-target patches to the profile's installed packages.
444
+ *
445
+ * A standalone installation runs no `apply` step: it installs the distribution from
446
+ * the registry, links the consumer's packages, and starts. The npm patches a
447
+ * distribution declares therefore need an owner that does not require an official
448
+ * source checkout — the source half of the apply step needs one, this does not.
449
+ *
450
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
451
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
452
+ * which is why this runs on every start rather than once.
453
+ *
454
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
455
+ * @param profileDirectory - the standalone profile directory.
456
+ * @returns the labels of the patches that were applied.
457
+ */
458
+ export function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
459
+ const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
460
+ const plus = requireRecord(manifest.dshPlus, 'dshPlus');
461
+ const names = plus.patchPackages;
462
+ if (!Array.isArray(names))
463
+ throw new Error('dshPlus.patchPackages must be an array');
464
+ const applied = [];
465
+ for (const value of names) {
466
+ if (typeof value !== 'string' || value === '')
467
+ throw new Error('dshPlus.patchPackages entries must be non-empty strings');
468
+ const patchPackage = resolveInstalledPackage(distributionDirectory, value);
469
+ const declaration = requireRecord(patchPackage.manifest.dshPatch, value + ' dshPatch');
470
+ const variants = declaration.variants;
471
+ if (!Array.isArray(variants))
472
+ throw new Error(value + ' dshPatch.variants must be an array');
473
+ for (const entry of variants) {
474
+ const variant = requireRecord(entry, value + ' variant');
475
+ const target = requireRecord(variant.target, value + ' variant target');
476
+ // Only npm targets reach an installed package; a source target needs the
477
+ // official checkout, which a standalone installation does not have.
478
+ if (target.kind !== 'npm')
479
+ continue;
480
+ const targetName = String(target.name);
481
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
482
+ const file = resolve(patchPackage.directory, String(variant.file));
483
+ if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
484
+ continue;
485
+ git(patched.directory, ['apply', file]);
486
+ applied.push(value + ' -> ' + targetName);
487
+ }
488
+ }
489
+ return applied;
490
+ }
491
+ /** Resolve one installed package's manifest from a requiring directory. */
492
+ function resolveInstalledPackage(from, packageName) {
493
+ const requireFrom = createRequire(join(from, 'package.json'));
494
+ let manifestPath;
495
+ try {
496
+ manifestPath = requireFrom.resolve(packageName + '/package.json');
497
+ }
498
+ catch {
499
+ throw new Error(packageName + ' is not installed under ' + from);
500
+ }
501
+ const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
502
+ return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };
503
+ }
504
+ //# sourceMappingURL=standalone-profile.js.map