@sparkelf/dsh-plus 0.1.0-rc.33 → 0.1.0-rc.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/bin.js CHANGED
@@ -6,6 +6,7 @@ import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, rm
6
6
  import { dirname, join, posix, resolve, win32 } from "node:path";
7
7
  import { homedir } from "node:os";
8
8
  import semver from "semver";
9
+ import { parseDocument } from "yaml";
9
10
  import { fileURLToPath } from "node:url";
10
11
  import { createServer } from "node:net";
11
12
  //#region lib/types/registry-versions.js
@@ -319,11 +320,18 @@ function readDistributionProfile(distributionDirectory) {
319
320
  if (typeof allowed !== "boolean") throw new Error("dshPlus.profile.allowBuilds." + name + " must be a boolean");
320
321
  allowBuilds[name] = allowed;
321
322
  }
323
+ const overrides = {};
324
+ const rawOverrides = profile.overrides === void 0 ? {} : requireRecord(profile.overrides, "dshPlus.profile.overrides");
325
+ for (const [name, spec] of Object.entries(rawOverrides)) {
326
+ if (typeof spec !== "string" || spec === "") throw new Error("dshPlus.profile.overrides." + name + " must be a non-empty string");
327
+ overrides[name] = spec;
328
+ }
322
329
  return {
323
330
  name: String(manifest.name),
324
331
  bundles: requireStringArray(profile.bundles, "dshPlus.profile.bundles"),
325
332
  dependencies,
326
333
  allowBuilds,
334
+ overrides,
327
335
  version: String(manifest.version)
328
336
  };
329
337
  }
@@ -424,8 +432,102 @@ function ensureProfile(paths, consumerDirectory) {
424
432
  } }
425
433
  };
426
434
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
435
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides);
427
436
  return true;
428
437
  }
438
+ /**
439
+ * Record the distribution's package substitutions in the profile workspace.
440
+ *
441
+ * pnpm reads \`overrides\` from \`pnpm-workspace.yaml\` since version 10 and ignores the
442
+ * same key in \`package.json\`, so a profile that carried it in the manifest would
443
+ * silently install the official package the override meant to replace.
444
+ *
445
+ * @param profileDirectory - the standalone profile directory.
446
+ * @param overrides - official package name to published replacement spec.
447
+ */
448
+ function writeProfileOverrides(profileDirectory, overrides) {
449
+ const workspacePath = join(profileDirectory, "pnpm-workspace.yaml");
450
+ const document = parseDocument(existsSync(workspacePath) ? readFileSync(workspacePath, "utf8") : "");
451
+ const [documentError] = document.errors;
452
+ if (documentError !== void 0) throw new Error("Plus profile workspace is not valid YAML", { cause: documentError });
453
+ if (document.get("packages") === void 0) document.set("packages", ["."]);
454
+ for (const [name, spec] of Object.entries(overrides)) document.setIn(["overrides", name], spec);
455
+ if (document.get("nodeLinker") === void 0) document.set("nodeLinker", "hoisted");
456
+ if (document.get("autoInstallPeers") === void 0) document.set("autoInstallPeers", false);
457
+ writeFileSync(workspacePath, String(document));
458
+ }
459
+ /** Run git in one directory, returning undefined instead of throwing when asked to. */
460
+ function git(root, args, acceptFailure = false) {
461
+ const result = spawnSync("git", args, {
462
+ cwd: root,
463
+ encoding: "utf8"
464
+ });
465
+ if (result.status === 0) return result.stdout.trim();
466
+ if (acceptFailure) return void 0;
467
+ const detail = result.stderr.trim();
468
+ throw new Error("git " + args.join(" ") + " failed" + (detail === "" ? "" : ": " + detail));
469
+ }
470
+ /**
471
+ * Apply the reviewed npm-target patches to the profile's installed packages.
472
+ *
473
+ * A standalone installation runs no `apply` step: it installs the distribution from
474
+ * the registry, links the consumer's packages, and starts. The npm patches a
475
+ * distribution declares therefore need an owner that does not require an official
476
+ * source checkout — the source half of the apply step needs one, this does not.
477
+ *
478
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
479
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
480
+ * which is why this runs on every start rather than once.
481
+ *
482
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
483
+ * @param profileDirectory - the standalone profile directory.
484
+ * @returns the labels of the patches that were applied.
485
+ */
486
+ function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
487
+ const names = requireRecord(requireRecord(JSON.parse(readFileSync(join(distributionDirectory, "package.json"), "utf8")), "Plus distribution manifest").dshPlus, "dshPlus").patchPackages;
488
+ if (!Array.isArray(names)) throw new Error("dshPlus.patchPackages must be an array");
489
+ const applied = [];
490
+ for (const value of names) {
491
+ if (typeof value !== "string" || value === "") throw new Error("dshPlus.patchPackages entries must be non-empty strings");
492
+ const patchPackage = resolveInstalledPackage(distributionDirectory, value);
493
+ const variants = requireRecord(patchPackage.manifest.dshPatch, value + " dshPatch").variants;
494
+ if (!Array.isArray(variants)) throw new Error(value + " dshPatch.variants must be an array");
495
+ for (const entry of variants) {
496
+ const variant = requireRecord(entry, value + " variant");
497
+ const target = requireRecord(variant.target, value + " variant target");
498
+ if (target.kind !== "npm") continue;
499
+ const targetName = String(target.name);
500
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
501
+ const file = resolve(patchPackage.directory, String(variant.file));
502
+ if (git(patched.directory, [
503
+ "apply",
504
+ "--reverse",
505
+ "--check",
506
+ file
507
+ ], true) !== void 0) continue;
508
+ git(patched.directory, ["apply", file]);
509
+ applied.push(value + " -> " + targetName);
510
+ }
511
+ }
512
+ return applied;
513
+ }
514
+ /** Resolve one installed package's manifest from a requiring directory. */
515
+ function resolveInstalledPackage(from, packageName) {
516
+ const requireFrom = createRequire(join(from, "package.json"));
517
+ let manifestPath;
518
+ try {
519
+ manifestPath = requireFrom.resolve(packageName + "/package.json");
520
+ } catch {
521
+ throw new Error(packageName + " is not installed under " + from);
522
+ }
523
+ const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, "utf8")), packageName + " manifest");
524
+ return {
525
+ name: packageName,
526
+ version: String(manifest.version),
527
+ directory: dirname(manifestPath),
528
+ manifest
529
+ };
530
+ }
429
531
  //#endregion
430
532
  //#region lib/types/standalone-cli.js
431
533
  /**
@@ -529,6 +631,7 @@ async function start(argv) {
529
631
  const paths = resolvePaths(anchor);
530
632
  const created = ensureProfile(paths, installationRoot());
531
633
  console.log(created ? "Created the plus profile at " + paths.profileDirectory : "Using the existing plus profile");
634
+ for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
532
635
  const entry = launcherEntry(anchor);
533
636
  if (options.foreground) return runForeground(entry, options.port, options.host, options.open);
534
637
  const existing = readState(paths.home);
@@ -14,7 +14,7 @@ import { dirname, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { newerVersion } from "./registry-versions.js";
16
16
  import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForAuthenticatedUrl, waitForServer, writeState, } from "./standalone-server.js";
17
- import { STANDALONE_PROFILE, ensureProfile, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
17
+ import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
18
18
  /** Milliseconds a start waits for the server to answer before reporting failure. */
19
19
  const READY_TIMEOUT_MILLISECONDS = 90_000;
20
20
  function parseStartOptions(argv) {
@@ -104,6 +104,12 @@ async function start(argv) {
104
104
  console.log(created
105
105
  ? 'Created the ' + STANDALONE_PROFILE + ' profile at ' + paths.profileDirectory
106
106
  : 'Using the existing ' + STANDALONE_PROFILE + ' profile');
107
+ // The profile symlinks the consumer's packages, so a patch lands on the installed
108
+ // copy the launcher loads. A reinstall restores the published bytes, which is why
109
+ // this runs on every start rather than only when the profile was created.
110
+ for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) {
111
+ console.log('Applied the reviewed patch ' + label);
112
+ }
107
113
  const entry = launcherEntry(anchor);
108
114
  if (options.foreground)
109
115
  return runForeground(entry, options.port, options.host, options.open);
@@ -54,6 +54,7 @@ export declare function readDistributionProfile(distributionDirectory: string):
54
54
  readonly bundles: readonly string[];
55
55
  readonly dependencies: Readonly<Record<string, string>>;
56
56
  readonly allowBuilds: Readonly<Record<string, boolean>>;
57
+ readonly overrides: Readonly<Record<string, string>>;
57
58
  readonly version: string;
58
59
  };
59
60
  /**
@@ -91,4 +92,21 @@ export declare function resolvePaths(anchor: string, env?: NodeJS.ProcessEnv): S
91
92
  * @returns whether this call created the manifest.
92
93
  */
93
94
  export declare function ensureProfile(paths: StandalonePaths, consumerDirectory: string): boolean;
95
+ /**
96
+ * Apply the reviewed npm-target patches to the profile's installed packages.
97
+ *
98
+ * A standalone installation runs no `apply` step: it installs the distribution from
99
+ * the registry, links the consumer's packages, and starts. The npm patches a
100
+ * distribution declares therefore need an owner that does not require an official
101
+ * source checkout — the source half of the apply step needs one, this does not.
102
+ *
103
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
104
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
105
+ * which is why this runs on every start rather than once.
106
+ *
107
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
108
+ * @param profileDirectory - the standalone profile directory.
109
+ * @returns the labels of the patches that were applied.
110
+ */
111
+ export declare function applyProfileNpmPatches(distributionDirectory: string, profileDirectory: string): string[];
94
112
  //# sourceMappingURL=standalone-profile.d.ts.map
@@ -8,10 +8,12 @@
8
8
  * the launcher mounts exactly the bundles the profile names and nothing expands a
9
9
  * bundle's own list.
10
10
  */
11
+ import { spawnSync } from 'node:child_process';
11
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
12
13
  import { createRequire } from 'node:module';
13
14
  import { homedir } from 'node:os';
14
15
  import { dirname, join, posix, resolve, win32 } from 'node:path';
16
+ import { parseDocument } from 'yaml';
15
17
  /** Profile name a standalone installation owns. */
16
18
  export const STANDALONE_PROFILE = 'plus';
17
19
  function requireRecord(value, label) {
@@ -104,11 +106,24 @@ export function readDistributionProfile(distributionDirectory) {
104
106
  throw new Error('dshPlus.profile.allowBuilds.' + name + ' must be a boolean');
105
107
  allowBuilds[name] = allowed;
106
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
+ }
107
121
  return {
108
122
  name: String(manifest.name),
109
123
  bundles: requireStringArray(profile.bundles, 'dshPlus.profile.bundles'),
110
124
  dependencies,
111
125
  allowBuilds,
126
+ overrides,
112
127
  version: String(manifest.version),
113
128
  };
114
129
  }
@@ -221,6 +236,107 @@ export function ensureProfile(paths, consumerDirectory) {
221
236
  },
222
237
  };
223
238
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
239
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides);
224
240
  return true;
225
241
  }
242
+ /**
243
+ * Record the distribution's package substitutions in the profile workspace.
244
+ *
245
+ * pnpm reads \`overrides\` from \`pnpm-workspace.yaml\` since version 10 and ignores the
246
+ * same key in \`package.json\`, so a profile that carried it in the manifest would
247
+ * silently install the official package the override meant to replace.
248
+ *
249
+ * @param profileDirectory - the standalone profile directory.
250
+ * @param overrides - official package name to published replacement spec.
251
+ */
252
+ function writeProfileOverrides(profileDirectory, overrides) {
253
+ const workspacePath = join(profileDirectory, 'pnpm-workspace.yaml');
254
+ const document = parseDocument(existsSync(workspacePath) ? readFileSync(workspacePath, 'utf8') : '');
255
+ const [documentError] = document.errors;
256
+ if (documentError !== undefined)
257
+ throw new Error('Plus profile workspace is not valid YAML', { cause: documentError });
258
+ if (document.get('packages') === undefined)
259
+ document.set('packages', ['.']);
260
+ for (const [name, spec] of Object.entries(overrides))
261
+ document.setIn(['overrides', name], spec);
262
+ // The profile resolves bundles from this directory, so peers the official tree would
263
+ // supply have to come from what the consumer installed.
264
+ if (document.get('nodeLinker') === undefined)
265
+ document.set('nodeLinker', 'hoisted');
266
+ if (document.get('autoInstallPeers') === undefined)
267
+ document.set('autoInstallPeers', false);
268
+ writeFileSync(workspacePath, String(document));
269
+ }
270
+ /** Run git in one directory, returning undefined instead of throwing when asked to. */
271
+ function git(root, args, acceptFailure = false) {
272
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
273
+ if (result.status === 0)
274
+ return result.stdout.trim();
275
+ if (acceptFailure)
276
+ return undefined;
277
+ const detail = result.stderr.trim();
278
+ throw new Error('git ' + args.join(' ') + ' failed' + (detail === '' ? '' : ': ' + detail));
279
+ }
280
+ /**
281
+ * Apply the reviewed npm-target patches to the profile's installed packages.
282
+ *
283
+ * A standalone installation runs no `apply` step: it installs the distribution from
284
+ * the registry, links the consumer's packages, and starts. The npm patches a
285
+ * distribution declares therefore need an owner that does not require an official
286
+ * source checkout — the source half of the apply step needs one, this does not.
287
+ *
288
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
289
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
290
+ * which is why this runs on every start rather than once.
291
+ *
292
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
293
+ * @param profileDirectory - the standalone profile directory.
294
+ * @returns the labels of the patches that were applied.
295
+ */
296
+ export function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
297
+ const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
298
+ const plus = requireRecord(manifest.dshPlus, 'dshPlus');
299
+ const names = plus.patchPackages;
300
+ if (!Array.isArray(names))
301
+ throw new Error('dshPlus.patchPackages must be an array');
302
+ const applied = [];
303
+ for (const value of names) {
304
+ if (typeof value !== 'string' || value === '')
305
+ throw new Error('dshPlus.patchPackages entries must be non-empty strings');
306
+ const patchPackage = resolveInstalledPackage(distributionDirectory, value);
307
+ const declaration = requireRecord(patchPackage.manifest.dshPatch, value + ' dshPatch');
308
+ const variants = declaration.variants;
309
+ if (!Array.isArray(variants))
310
+ throw new Error(value + ' dshPatch.variants must be an array');
311
+ for (const entry of variants) {
312
+ const variant = requireRecord(entry, value + ' variant');
313
+ const target = requireRecord(variant.target, value + ' variant target');
314
+ // Only npm targets reach an installed package; a source target needs the
315
+ // official checkout, which a standalone installation does not have.
316
+ if (target.kind !== 'npm')
317
+ continue;
318
+ const targetName = String(target.name);
319
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
320
+ const file = resolve(patchPackage.directory, String(variant.file));
321
+ if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
322
+ continue;
323
+ git(patched.directory, ['apply', file]);
324
+ applied.push(value + ' -> ' + targetName);
325
+ }
326
+ }
327
+ return applied;
328
+ }
329
+ /** Resolve one installed package's manifest from a requiring directory. */
330
+ function resolveInstalledPackage(from, packageName) {
331
+ const requireFrom = createRequire(join(from, 'package.json'));
332
+ let manifestPath;
333
+ try {
334
+ manifestPath = requireFrom.resolve(packageName + '/package.json');
335
+ }
336
+ catch {
337
+ throw new Error(packageName + ' is not installed under ' + from);
338
+ }
339
+ const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
340
+ return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };
341
+ }
226
342
  //# sourceMappingURL=standalone-profile.js.map
package/package.json CHANGED
@@ -98,6 +98,28 @@
98
98
  "dsh-better-sidebar": "0.19.1",
99
99
  "dsh-sql-workbench": "0.5.1",
100
100
  "dsh-video-preview": "0.1.4"
101
+ },
102
+ "overrides": {
103
+ "@deepseek-ai/dsh-agent-presets": "npm:@sparkelf/dsh-agent-presets@0.1.5-rc.2",
104
+ "@deepseek-ai/dsh-api-gateway": "npm:@sparkelf/dsh-api-gateway@0.1.5-rc.2",
105
+ "@deepseek-ai/dsh-api-session-controller": "npm:@sparkelf/dsh-api-session-controller@0.1.5-rc.2",
106
+ "@deepseek-ai/dsh-client-connection": "npm:@sparkelf/dsh-client-connection@0.1.5-rc.2",
107
+ "@deepseek-ai/dsh-client-ui-agent-preset": "npm:@sparkelf/dsh-client-ui-agent-preset@0.1.5-rc.2",
108
+ "@deepseek-ai/dsh-client-ui-conversation": "npm:@sparkelf/dsh-client-ui-conversation@0.1.5-rc.2",
109
+ "@deepseek-ai/dsh-client-ui-deliverables": "npm:@sparkelf/dsh-client-ui-deliverables@0.1.5-rc.2",
110
+ "@deepseek-ai/dsh-client-ui-layout": "npm:@sparkelf/dsh-client-ui-layout@0.1.5-rc.2",
111
+ "@deepseek-ai/dsh-client-ui-model-selection": "npm:@sparkelf/dsh-client-ui-model-selection@0.1.5-rc.2",
112
+ "@deepseek-ai/dsh-client-ui-primitives": "npm:@sparkelf/dsh-client-ui-primitives@0.1.5-rc.2",
113
+ "@deepseek-ai/dsh-client-ui-settings-models": "npm:@sparkelf/dsh-client-ui-settings-models@0.1.5-rc.2",
114
+ "@deepseek-ai/dsh-client-ui-trajectory": "npm:@sparkelf/dsh-client-ui-trajectory@0.1.5-rc.2",
115
+ "@deepseek-ai/dsh-host-frontend-static": "npm:@sparkelf/dsh-host-frontend-static@0.1.5-rc.2",
116
+ "@deepseek-ai/dsh-host-webserver": "npm:@sparkelf/dsh-host-webserver@0.1.5-rc.2",
117
+ "@deepseek-ai/dsh-llm-pi-ai": "npm:@sparkelf/dsh-llm-pi-ai@0.1.5-rc.2",
118
+ "@deepseek-ai/dsh-session-log-export": "npm:@sparkelf/dsh-session-log-export@0.1.5-rc.2",
119
+ "@deepseek-ai/dsh-tools": "npm:@sparkelf/dsh-tools@0.1.5-rc.2",
120
+ "@deepseek-ai/dsh-web-app": "npm:@sparkelf/dsh-web-app@0.1.5-rc.2",
121
+ "@deepseek-ai/dsh-web-frontend": "npm:@sparkelf/dsh-web-frontend@0.1.5-rc.2",
122
+ "@deepseek-ai/dsh-workspace": "npm:@sparkelf/dsh-workspace@0.1.5-rc.2"
101
123
  }
102
124
  },
103
125
  "sourceBase": {
@@ -140,5 +162,5 @@
140
162
  },
141
163
  "type": "module",
142
164
  "types": "lib/types/index.d.ts",
143
- "version": "0.1.0-rc.33"
165
+ "version": "0.1.0-rc.35"
144
166
  }