@sparkelf/dsh-plus 0.2.0-rc.17 → 0.2.0-rc.19

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/cordis.patch.yml CHANGED
@@ -28,17 +28,9 @@
28
28
  - id: plus-backup
29
29
  name: '@sparkelf/dsh-plugin-backup'
30
30
 
31
- - id: plus-dataops
32
- name: '@sparkelf/dsh-plugin-dataops'
33
- disabled: !!js "!process.env.DSH_DATAOPS_BASE_URL"
34
- config:
35
- baseUrl: !!js process.env.DSH_DATAOPS_BASE_URL
36
- serverName: dataops
37
- credentialRef: dataops_access_token
38
- targetCredentialRef: dataops_target_ref
39
- callbackOrigin: !!js process.env.DSH_DATAOPS_CALLBACK_ORIGIN
40
- toolCallTimeoutMs: 60000
41
- failOnStartupError: false
31
+ # DataOps tools belong to the DataOps-managed workspace profile only. A
32
+ # standalone profile must not carry DataOps identity or endpoint config, so
33
+ # this layer mounts no DataOps plugin.
42
34
 
43
35
  # A disabled root node carries the package's Web Settings entry into the
44
36
  # client inventory; the startup nodes below exclusively own Host schemas.
package/lib/bin.js CHANGED
@@ -210,6 +210,7 @@ function capabilityPatchLayer(answers) {
210
210
  if (enabled.has("computer-use")) rows.push(" - id: computer-use", " name: '@deepseek-ai/dsh-computer-use'", " - id: computer-use-cua-driver-mcp", " name: '@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp'");
211
211
  const lines = ["# Written by dsh-plus start from the capability interview. Enabling or disabling a capability", "# rewrites this file; edits here are replaced."];
212
212
  if (rows.length > 0) lines.push("- insert:", ...rows);
213
+ else lines.push("[]");
213
214
  if (enabled.has("exa")) lines.push("", "# Route web_search through Exa rather than the built-in provider.", "- id: web", " config:", " searchProvider: exa");
214
215
  return lines.join("\n") + "\n";
215
216
  }
@@ -532,8 +533,55 @@ async function waitForAuthenticatedUrl(logPath, timeoutMilliseconds) {
532
533
  * the launcher mounts exactly the bundles the profile names and nothing expands a
533
534
  * bundle's own list.
534
535
  */
535
- /** Profile name a standalone installation owns. */
536
+ /** Profile name a standalone installation owns when its package declares none. */
536
537
  const STANDALONE_PROFILE = "plus";
538
+ /**
539
+ * Read the deployment facts the installing package declares.
540
+ *
541
+ * A standalone package describes the profile it materializes and the capabilities it
542
+ * omits, so a reduced variant owns a differently named profile and a reduced install
543
+ * without the distribution naming either. The declaration travels with the package
544
+ * because npm resolves the tree long before any command of ours runs.
545
+ *
546
+ * @param anchor - path inside the installing package's tree.
547
+ * @param fallback - profile name to use when the declaration is absent.
548
+ * @returns the declared profile name and omitted capabilities.
549
+ */
550
+ function readStandaloneDeclaration(anchor, fallback = STANDALONE_PROFILE) {
551
+ let current = resolve(anchor);
552
+ for (;;) {
553
+ const declared = readStandaloneFacts(join(current, "package.json"));
554
+ if (declared !== void 0) return declared;
555
+ const parent = dirname(current);
556
+ if (parent === current) return {
557
+ profileName: fallback,
558
+ omittedPackages: {}
559
+ };
560
+ current = parent;
561
+ }
562
+ }
563
+ /** Read one manifest's standalone declaration, or undefined when it carries none. */
564
+ function readStandaloneFacts(manifestPath) {
565
+ if (!existsSync(manifestPath)) return void 0;
566
+ let standalone;
567
+ try {
568
+ standalone = JSON.parse(readFileSync(manifestPath, "utf8")).dshPlusStandalone;
569
+ } catch {
570
+ return;
571
+ }
572
+ const profile = standalone?.profile;
573
+ if (standalone === void 0 || typeof profile !== "string" || profile === "") return void 0;
574
+ const rawOmitted = standalone.omittedPackages;
575
+ const omittedPackages = {};
576
+ if (rawOmitted !== void 0 && typeof rawOmitted === "object" && !Array.isArray(rawOmitted)) for (const [name, spec] of Object.entries(rawOmitted)) {
577
+ if (typeof spec !== "string" || spec === "") throw new Error("dshPlusStandalone.omittedPackages." + name + " must be a non-empty string");
578
+ omittedPackages[name] = spec;
579
+ }
580
+ return {
581
+ profileName: profile,
582
+ omittedPackages
583
+ };
584
+ }
537
585
  function requireRecord(value, label) {
538
586
  if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be an object");
539
587
  return value;
@@ -814,9 +862,12 @@ function linkBundle(consumerModules, name, source) {
814
862
  /** Resolve every path a command needs, without creating anything. */
815
863
  function resolvePaths(anchor, env = process.env) {
816
864
  const home = resolveHome(env);
865
+ const declaration = readStandaloneDeclaration(anchor);
817
866
  return {
818
867
  home,
819
- profileDirectory: join(home, "profiles", STANDALONE_PROFILE),
868
+ profileName: declaration.profileName,
869
+ omittedPackages: declaration.omittedPackages,
870
+ profileDirectory: join(home, "profiles", declaration.profileName),
820
871
  distributionDirectory: resolveDistributionDirectory(anchor)
821
872
  };
822
873
  }
@@ -840,7 +891,7 @@ function ensureProfile(paths, consumerDirectory) {
840
891
  const manifestPath = join(paths.profileDirectory, "package.json");
841
892
  const distribution = readDistributionProfile(paths.distributionDirectory);
842
893
  if (existsSync(manifestPath)) {
843
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
894
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
844
895
  installProfilePackages(paths, consumerDirectory);
845
896
  return false;
846
897
  }
@@ -860,7 +911,7 @@ function ensureProfile(paths, consumerDirectory) {
860
911
  } }
861
912
  };
862
913
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
863
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
914
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
864
915
  installProfilePackages(paths, consumerDirectory);
865
916
  return true;
866
917
  }
@@ -874,13 +925,14 @@ function ensureProfile(paths, consumerDirectory) {
874
925
  * @param profileDirectory - the standalone profile directory.
875
926
  * @param overrides - official package name to published replacement spec.
876
927
  */
877
- function writeProfileOverrides(profileDirectory, overrides, allowBuilds) {
928
+ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omittedPackages) {
878
929
  const workspacePath = join(profileDirectory, "pnpm-workspace.yaml");
879
930
  const document = parseDocument(existsSync(workspacePath) ? readFileSync(workspacePath, "utf8") : "");
880
931
  const [documentError] = document.errors;
881
932
  if (documentError !== void 0) throw new Error("Plus profile workspace is not valid YAML", { cause: documentError });
882
933
  if (document.get("packages") === void 0) document.set("packages", ["."]);
883
934
  for (const [name, spec] of Object.entries(overrides)) document.setIn(["overrides", name], spec);
935
+ for (const [name, spec] of Object.entries(omittedPackages)) document.setIn(["overrides", name], spec);
884
936
  for (const [name, allowed] of Object.entries(allowBuilds)) document.setIn(["allowBuilds", name], allowed);
885
937
  if (document.get("nodeLinker") === void 0) document.set("nodeLinker", "hoisted");
886
938
  if (document.get("autoInstallPeers") === void 0) document.set("autoInstallPeers", false);
@@ -978,6 +1030,7 @@ function parseStartOptions(argv) {
978
1030
  let host = "127.0.0.1";
979
1031
  let open = true;
980
1032
  let foreground = false;
1033
+ let capabilities;
981
1034
  for (let index = 0; index < argv.length; index += 1) {
982
1035
  const token = argv[index];
983
1036
  if (token === "--port" || token === "-p") {
@@ -1002,13 +1055,41 @@ function parseStartOptions(argv) {
1002
1055
  foreground = true;
1003
1056
  continue;
1004
1057
  }
1058
+ if (token === "--capabilities") {
1059
+ const value = argv[index + 1];
1060
+ if (value === void 0) throw new Error("--capabilities requires a comma-separated list, or an empty string for none");
1061
+ const stated = value === "" ? [] : value.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
1062
+ const offered = new Set(CAPABILITIES.map((capability) => capability.id));
1063
+ for (const id of stated) if (!offered.has(id)) throw new Error("unknown capability \"" + id + "\"; this release offers " + [...offered].join(", "));
1064
+ capabilities = stated;
1065
+ index += 1;
1066
+ continue;
1067
+ }
1005
1068
  throw new Error("unknown option: " + String(token));
1006
1069
  }
1007
1070
  return {
1008
1071
  port,
1009
1072
  host,
1010
1073
  open,
1011
- foreground
1074
+ foreground,
1075
+ capabilities
1076
+ };
1077
+ }
1078
+ /**
1079
+ * Resolve a stated capability selection.
1080
+ *
1081
+ * An unattended install states what it wants instead of answering the interview, so a
1082
+ * name it does not offer has to fail here rather than silently enable nothing: the
1083
+ * deployment would otherwise start with a capability the caller believes it selected.
1084
+ *
1085
+ * @param ids - capability ids the caller selected.
1086
+ * @returns the answers the interview would have returned.
1087
+ */
1088
+ function selectCapabilities(ids) {
1089
+ const selected = [...new Set(ids)];
1090
+ return {
1091
+ enabled: selected,
1092
+ ...selected.includes("mineru") ? { mineruEndpoint: DEFAULT_MINERU_ENDPOINT } : {}
1012
1093
  };
1013
1094
  }
1014
1095
  /**
@@ -1039,16 +1120,35 @@ function installationRoot() {
1039
1120
  function installationAnchor() {
1040
1121
  return join(installationRoot(), "package.json");
1041
1122
  }
1123
+ /**
1124
+ * Path to the installed command's own file, which is where a declaration lookup starts.
1125
+ *
1126
+ * The declaration belongs to the package the user installed, and that package is a
1127
+ * sibling of this module rather than an ancestor: a variant's forwarder imports this
1128
+ * CLI in-process, so `import.meta.url` names `@sparkelf/dsh-plus` while the installed
1129
+ * command is the variant. `process.argv[1]` is the entry that was actually invoked,
1130
+ * which is the package whose declaration applies.
1131
+ *
1132
+ * Walking out from the installation root cannot reach it either: that root is the
1133
+ * project the user ran the command in, so every variant would fall back to the full
1134
+ * profile and keep the capabilities it excluded.
1135
+ *
1136
+ * @returns absolute path to the invoked command, or this module when argv carries none.
1137
+ */
1138
+ function declarationAnchor() {
1139
+ const invoked = process.argv[1];
1140
+ return invoked === void 0 || invoked === "" ? fileURLToPath(import.meta.url) : resolve(invoked);
1141
+ }
1042
1142
  /** The launcher entry this installation must drive. */
1043
1143
  function launcherEntry(anchor) {
1044
1144
  return createRequire(anchor).resolve("@deepseek-ai/dsh/lib/bin.js");
1045
1145
  }
1046
1146
  /** Run the server in this process, inheriting stdio. */
1047
- function runForeground(entry, port, host, open) {
1147
+ function runForeground(entry, profileName, port, host, open) {
1048
1148
  const args = [
1049
1149
  entry,
1050
1150
  "--profile",
1051
- STANDALONE_PROFILE,
1151
+ profileName,
1052
1152
  "--port",
1053
1153
  String(port),
1054
1154
  "--host",
@@ -1060,7 +1160,7 @@ function runForeground(entry, port, host, open) {
1060
1160
  async function start(argv) {
1061
1161
  const options = parseStartOptions(argv);
1062
1162
  const anchor = installationAnchor();
1063
- const paths = resolvePaths(anchor);
1163
+ const paths = resolvePaths(declarationAnchor());
1064
1164
  if (!pnpmAvailable()) {
1065
1165
  const command = pnpmInstallCommand();
1066
1166
  console.log("pnpm is required to install the plus profile, and was not found.");
@@ -1091,9 +1191,9 @@ async function start(argv) {
1091
1191
  console.log("pnpm installed.");
1092
1192
  }
1093
1193
  const created = ensureProfile(paths, installationRoot());
1094
- console.log(created ? "Created the plus profile at " + paths.profileDirectory : "Using the existing plus profile");
1194
+ console.log(created ? "Created the " + paths.profileName + " profile at " + paths.profileDirectory : "Using the existing " + paths.profileName + " profile");
1095
1195
  for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
1096
- const answers = created || !existsSync(join(paths.home, "capabilities.json")) ? await interviewCapabilities(true) : void 0;
1196
+ const answers = created || !existsSync(join(paths.home, "capabilities.json")) ? options.capabilities === void 0 ? await interviewCapabilities(true) : selectCapabilities(options.capabilities) : void 0;
1097
1197
  if (answers !== void 0) {
1098
1198
  const ready = await installCapabilityServices(answers, paths.home);
1099
1199
  writeCapabilityPatch(paths.profileDirectory, answers);
@@ -1103,16 +1203,17 @@ async function start(argv) {
1103
1203
  if (answers.enabled.includes("exa") && answers.exaApiKey === void 0) console.log(" Exa: add EXA_API_KEY to " + join(paths.home, CAPABILITY_ENV_FILE) + " when you have a key.");
1104
1204
  }
1105
1205
  const entry = launcherEntry(anchor);
1106
- if (options.foreground) return runForeground(entry, options.port, options.host, options.open);
1206
+ if (options.foreground) return runForeground(entry, paths.profileName, options.port, options.host, options.open);
1107
1207
  const existing = readState(paths.home);
1108
1208
  if (existing !== void 0) {
1109
1209
  console.log("Plus is already running at " + existing.url);
1110
1210
  console.log("Stop it with: dsh-plus stop");
1111
1211
  return 0;
1112
1212
  }
1113
- return startDetached(paths.home, entry, options);
1213
+ return startDetached(paths, entry, options);
1114
1214
  }
1115
- async function startDetached(home, entry, options) {
1215
+ async function startDetached(paths, entry, options) {
1216
+ const home = paths.home;
1116
1217
  const port = await choosePort(options.port, options.host);
1117
1218
  if (port === void 0) {
1118
1219
  console.error("No free port in the range " + String(options.port) + "-" + String(options.port + 9) + ".");
@@ -1120,15 +1221,15 @@ async function startDetached(home, entry, options) {
1120
1221
  return 1;
1121
1222
  }
1122
1223
  if (port !== options.port) console.log("Port " + String(options.port) + " is in use; using " + String(port) + ".");
1123
- const logPath = join(stateDirectory(home), "server.log");
1224
+ const logPath = join(stateDirectory(paths.home), "server.log");
1124
1225
  const env = {
1125
1226
  ...process.env,
1126
- DSH_HOME: home
1227
+ DSH_HOME: paths.home
1127
1228
  };
1128
1229
  const args = [
1129
1230
  entry,
1130
1231
  "--profile",
1131
- STANDALONE_PROFILE,
1232
+ paths.profileName,
1132
1233
  "--port",
1133
1234
  String(port),
1134
1235
  "--host",
@@ -159,8 +159,16 @@ export function capabilityPatchLayer(answers) {
159
159
  '# ' + CAPABILITY_MARKER + ' Enabling or disabling a capability',
160
160
  '# rewrites this file; edits here are replaced.',
161
161
  ];
162
- if (rows.length > 0)
162
+ // The loader requires a top-level YAML array, so a selection that mounts nothing
163
+ // still has to produce one. Comments alone parse as `null`, and the profile then
164
+ // refuses to boot with "must be a top-level YAML array of loader patch entries" —
165
+ // which is the whole deployment, not just the missing capability.
166
+ if (rows.length > 0) {
163
167
  lines.push('- insert:', ...rows);
168
+ }
169
+ else {
170
+ lines.push('[]');
171
+ }
164
172
  if (enabled.has('exa')) {
165
173
  lines.push('', '# Route web_search through Exa rather than the built-in provider.', '- id: web', ' config:', ' searchProvider: exa');
166
174
  }
@@ -10,10 +10,10 @@
10
10
  import { spawnSync } from 'node:child_process';
11
11
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
12
12
  import { createRequire } from 'node:module';
13
- import { dirname, join } from 'node:path';
13
+ import { dirname, join, resolve } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { newerVersion } from "./registry-versions.js";
16
- import { CAPABILITY_ENV_FILE, CAPABILITY_MARKER, CAPABILITY_RECORD, capabilityEnvironment, capabilityPatchLayer, installCapabilityServices, interviewCapabilities, } from "./standalone-capabilities.js";
16
+ import { CAPABILITIES, CAPABILITY_ENV_FILE, CAPABILITY_MARKER, CAPABILITY_RECORD, DEFAULT_MINERU_ENDPOINT, capabilityEnvironment, capabilityPatchLayer, installCapabilityServices, interviewCapabilities, } from "./standalone-capabilities.js";
17
17
  import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForAuthenticatedUrl, waitForServer, writeState, } from "./standalone-server.js";
18
18
  import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, pnpmAvailable, pnpmInstallCommand, pnpmInstallCommands, registryOrder, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
19
19
  /** Milliseconds a start waits for the server to answer before reporting failure. */
@@ -25,6 +25,7 @@ function parseStartOptions(argv) {
25
25
  let host = '127.0.0.1';
26
26
  let open = true;
27
27
  let foreground = false;
28
+ let capabilities;
28
29
  for (let index = 0; index < argv.length; index += 1) {
29
30
  const token = argv[index];
30
31
  if (token === '--port' || token === '-p') {
@@ -51,9 +52,47 @@ function parseStartOptions(argv) {
51
52
  foreground = true;
52
53
  continue;
53
54
  }
55
+ if (token === '--capabilities') {
56
+ const value = argv[index + 1];
57
+ if (value === undefined)
58
+ throw new Error('--capabilities requires a comma-separated list, or an empty string for none');
59
+ const stated = value === '' ? [] : value.split(',').map(entry => entry.trim()).filter(entry => entry !== '');
60
+ // A stated selection is the only way an unattended install chooses capabilities, so
61
+ // a name this release does not offer fails here: the deployment would otherwise
62
+ // start without a capability its caller believes it selected. Validating at parse
63
+ // time also makes the failure independent of what is installed.
64
+ const offered = new Set(CAPABILITIES.map(capability => capability.id));
65
+ for (const id of stated) {
66
+ if (!offered.has(id)) {
67
+ throw new Error('unknown capability "' + id + '"; this release offers ' + [...offered].join(', '));
68
+ }
69
+ }
70
+ capabilities = stated;
71
+ index += 1;
72
+ continue;
73
+ }
54
74
  throw new Error('unknown option: ' + String(token));
55
75
  }
56
- return { port, host, open, foreground };
76
+ return { port, host, open, foreground, capabilities };
77
+ }
78
+ /**
79
+ * Resolve a stated capability selection.
80
+ *
81
+ * An unattended install states what it wants instead of answering the interview, so a
82
+ * name it does not offer has to fail here rather than silently enable nothing: the
83
+ * deployment would otherwise start with a capability the caller believes it selected.
84
+ *
85
+ * @param ids - capability ids the caller selected.
86
+ * @returns the answers the interview would have returned.
87
+ */
88
+ function selectCapabilities(ids) {
89
+ const selected = [...new Set(ids)];
90
+ // MinerU is started from an endpoint the interview collects; an unattended install
91
+ // gets the documented default rather than an unset one.
92
+ return {
93
+ enabled: selected,
94
+ ...selected.includes('mineru') ? { mineruEndpoint: DEFAULT_MINERU_ENDPOINT } : {},
95
+ };
57
96
  }
58
97
  /**
59
98
  * Where the installation that owns this command keeps its packages.
@@ -87,13 +126,32 @@ function installationRoot() {
87
126
  function installationAnchor() {
88
127
  return join(installationRoot(), 'package.json');
89
128
  }
129
+ /**
130
+ * Path to the installed command's own file, which is where a declaration lookup starts.
131
+ *
132
+ * The declaration belongs to the package the user installed, and that package is a
133
+ * sibling of this module rather than an ancestor: a variant's forwarder imports this
134
+ * CLI in-process, so `import.meta.url` names `@sparkelf/dsh-plus` while the installed
135
+ * command is the variant. `process.argv[1]` is the entry that was actually invoked,
136
+ * which is the package whose declaration applies.
137
+ *
138
+ * Walking out from the installation root cannot reach it either: that root is the
139
+ * project the user ran the command in, so every variant would fall back to the full
140
+ * profile and keep the capabilities it excluded.
141
+ *
142
+ * @returns absolute path to the invoked command, or this module when argv carries none.
143
+ */
144
+ function declarationAnchor() {
145
+ const invoked = process.argv[1];
146
+ return invoked === undefined || invoked === '' ? fileURLToPath(import.meta.url) : resolve(invoked);
147
+ }
90
148
  /** The launcher entry this installation must drive. */
91
149
  function launcherEntry(anchor) {
92
150
  return createRequire(anchor).resolve('@deepseek-ai/dsh/lib/bin.js');
93
151
  }
94
152
  /** Run the server in this process, inheriting stdio. */
95
- function runForeground(entry, port, host, open) {
96
- const args = [entry, '--profile', STANDALONE_PROFILE, '--port', String(port), '--host', host];
153
+ function runForeground(entry, profileName, port, host, open) {
154
+ const args = [entry, '--profile', profileName, '--port', String(port), '--host', host];
97
155
  if (!open)
98
156
  args.push('--no-open');
99
157
  const result = spawnSync(process.execPath, args, { stdio: 'inherit' });
@@ -102,7 +160,9 @@ function runForeground(entry, port, host, open) {
102
160
  async function start(argv) {
103
161
  const options = parseStartOptions(argv);
104
162
  const anchor = installationAnchor();
105
- const paths = resolvePaths(anchor);
163
+ // The profile's own name and omissions come from the package that installed this
164
+ // command, which is a different tree position than the dependencies it resolves.
165
+ const paths = resolvePaths(declarationAnchor());
106
166
  // The profile installs its own dependency tree, which needs pnpm. Asking here rather
107
167
  // than failing inside the install turns a missing prerequisite into a decision the
108
168
  // consumer makes, and the install it can run is the one command that provides it.
@@ -144,8 +204,8 @@ async function start(argv) {
144
204
  }
145
205
  const created = ensureProfile(paths, installationRoot());
146
206
  console.log(created
147
- ? 'Created the ' + STANDALONE_PROFILE + ' profile at ' + paths.profileDirectory
148
- : 'Using the existing ' + STANDALONE_PROFILE + ' profile');
207
+ ? 'Created the ' + paths.profileName + ' profile at ' + paths.profileDirectory
208
+ : 'Using the existing ' + paths.profileName + ' profile');
149
209
  // The profile symlinks the consumer's packages, so a patch lands on the installed
150
210
  // copy the launcher loads. A reinstall restores the published bytes, which is why
151
211
  // this runs on every start rather than only when the profile was created.
@@ -157,7 +217,9 @@ async function start(argv) {
157
217
  // on every start would make a restart look like a first install.
158
218
  const needsInterview = created || !existsSync(join(paths.home, CAPABILITY_RECORD));
159
219
  const answers = needsInterview
160
- ? await interviewCapabilities(true)
220
+ ? options.capabilities === undefined
221
+ ? await interviewCapabilities(true)
222
+ : selectCapabilities(options.capabilities)
161
223
  : undefined;
162
224
  if (answers !== undefined) {
163
225
  const ready = await installCapabilityServices(answers, paths.home);
@@ -172,16 +234,17 @@ async function start(argv) {
172
234
  }
173
235
  const entry = launcherEntry(anchor);
174
236
  if (options.foreground)
175
- return runForeground(entry, options.port, options.host, options.open);
237
+ return runForeground(entry, paths.profileName, options.port, options.host, options.open);
176
238
  const existing = readState(paths.home);
177
239
  if (existing !== undefined) {
178
240
  console.log('Plus is already running at ' + existing.url);
179
241
  console.log('Stop it with: dsh-plus stop');
180
242
  return 0;
181
243
  }
182
- return startDetached(paths.home, entry, options);
244
+ return startDetached(paths, entry, options);
183
245
  }
184
- async function startDetached(home, entry, options) {
246
+ async function startDetached(paths, entry, options) {
247
+ const home = paths.home;
185
248
  const port = await choosePort(options.port, options.host);
186
249
  if (port === undefined) {
187
250
  console.error('No free port in the range ' + String(options.port) + '-' + String(options.port + 9) + '.');
@@ -190,9 +253,9 @@ async function startDetached(home, entry, options) {
190
253
  }
191
254
  if (port !== options.port)
192
255
  console.log('Port ' + String(options.port) + ' is in use; using ' + String(port) + '.');
193
- const logPath = join(stateDirectory(home), 'server.log');
194
- const env = { ...process.env, DSH_HOME: home };
195
- const args = [entry, '--profile', STANDALONE_PROFILE, '--port', String(port), '--host', options.host, '--no-open'];
256
+ const logPath = join(stateDirectory(paths.home), 'server.log');
257
+ const env = { ...process.env, DSH_HOME: paths.home };
258
+ const args = [entry, '--profile', paths.profileName, '--port', String(port), '--host', options.host, '--no-open'];
196
259
  const pid = spawnServer({ command: process.execPath, args, env, logPath });
197
260
  const url = 'http://' + options.host + ':' + String(port) + '/';
198
261
  const ready = await waitForServer(url, READY_TIMEOUT_MILLISECONDS);
@@ -8,12 +8,56 @@
8
8
  * the launcher mounts exactly the bundles the profile names and nothing expands a
9
9
  * bundle's own list.
10
10
  */
11
- /** Profile name a standalone installation owns. */
11
+ /** Profile name a standalone installation owns when its package declares none. */
12
12
  export declare const STANDALONE_PROFILE = "plus";
13
+ /**
14
+ * Read the profile name the installing package declares.
15
+ *
16
+ * A standalone package describes the profile it materializes, so a reduced variant can
17
+ * own a differently named profile beside the full one instead of overwriting it. The
18
+ * declaration travels with the package because npm resolves the tree long before any
19
+ * command of ours runs, and the distribution cannot name a profile for a package it
20
+ * does not own.
21
+ *
22
+ * @param anchor - path inside the installing package's tree.
23
+ * @param fallback - profile name to use when the declaration is absent.
24
+ * @returns the declared profile name, or the fallback.
25
+ */
26
+ export declare function resolveStandaloneProfile(anchor: string, fallback?: string): string;
27
+ /** What the installing package declares about the deployment it owns. */
28
+ export interface StandaloneDeclaration {
29
+ /** Profile name the launcher materializes. */
30
+ readonly profileName: string;
31
+ /**
32
+ * Capabilities the deployment must not install, as package name to override spec.
33
+ *
34
+ * npm substitutes rather than deletes, so each entry names the placeholder the
35
+ * capability is replaced with. The profile workspace applies these as overrides,
36
+ * which is where pnpm reads them.
37
+ */
38
+ readonly omittedPackages: Readonly<Record<string, string>>;
39
+ }
40
+ /**
41
+ * Read the deployment facts the installing package declares.
42
+ *
43
+ * A standalone package describes the profile it materializes and the capabilities it
44
+ * omits, so a reduced variant owns a differently named profile and a reduced install
45
+ * without the distribution naming either. The declaration travels with the package
46
+ * because npm resolves the tree long before any command of ours runs.
47
+ *
48
+ * @param anchor - path inside the installing package's tree.
49
+ * @param fallback - profile name to use when the declaration is absent.
50
+ * @returns the declared profile name and omitted capabilities.
51
+ */
52
+ export declare function readStandaloneDeclaration(anchor: string, fallback?: string): StandaloneDeclaration;
13
53
  /** Resolved locations for one standalone installation. */
14
54
  export interface StandalonePaths {
15
55
  /** DSH home holding profiles, credentials, and session data. */
16
56
  readonly home: string;
57
+ /** Profile name the launcher boots, as the installing package declares it. */
58
+ readonly profileName: string;
59
+ /** Capabilities the deployment omits, as package name to override spec. */
60
+ readonly omittedPackages: Readonly<Record<string, string>>;
17
61
  /** Profile directory the launcher boots. */
18
62
  readonly profileDirectory: string;
19
63
  /** Installed Plus distribution directory. */
@@ -14,8 +14,79 @@ import { createRequire } from 'node:module';
14
14
  import { homedir } from 'node:os';
15
15
  import { dirname, join, posix, resolve, win32 } from 'node:path';
16
16
  import { parseDocument } from 'yaml';
17
- /** Profile name a standalone installation owns. */
17
+ /** Profile name a standalone installation owns when its package declares none. */
18
18
  export const STANDALONE_PROFILE = 'plus';
19
+ /**
20
+ * Read the profile name the installing package declares.
21
+ *
22
+ * A standalone package describes the profile it materializes, so a reduced variant can
23
+ * own a differently named profile beside the full one instead of overwriting it. The
24
+ * declaration travels with the package because npm resolves the tree long before any
25
+ * command of ours runs, and the distribution cannot name a profile for a package it
26
+ * does not own.
27
+ *
28
+ * @param anchor - path inside the installing package's tree.
29
+ * @param fallback - profile name to use when the declaration is absent.
30
+ * @returns the declared profile name, or the fallback.
31
+ */
32
+ export function resolveStandaloneProfile(anchor, fallback = STANDALONE_PROFILE) {
33
+ return readStandaloneDeclaration(anchor, fallback).profileName;
34
+ }
35
+ /**
36
+ * Read the deployment facts the installing package declares.
37
+ *
38
+ * A standalone package describes the profile it materializes and the capabilities it
39
+ * omits, so a reduced variant owns a differently named profile and a reduced install
40
+ * without the distribution naming either. The declaration travels with the package
41
+ * because npm resolves the tree long before any command of ours runs.
42
+ *
43
+ * @param anchor - path inside the installing package's tree.
44
+ * @param fallback - profile name to use when the declaration is absent.
45
+ * @returns the declared profile name and omitted capabilities.
46
+ */
47
+ export function readStandaloneDeclaration(anchor, fallback = STANDALONE_PROFILE) {
48
+ // The anchor is the CLI file; walk out to the package that declares the forwarder.
49
+ let current = resolve(anchor);
50
+ for (;;) {
51
+ const manifestPath = join(current, 'package.json');
52
+ const declared = readStandaloneFacts(manifestPath);
53
+ if (declared !== undefined)
54
+ return declared;
55
+ const parent = dirname(current);
56
+ if (parent === current)
57
+ return { profileName: fallback, omittedPackages: {} };
58
+ current = parent;
59
+ }
60
+ }
61
+ /** Read one manifest's standalone declaration, or undefined when it carries none. */
62
+ function readStandaloneFacts(manifestPath) {
63
+ if (!existsSync(manifestPath))
64
+ return undefined;
65
+ let standalone;
66
+ try {
67
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
68
+ standalone = manifest.dshPlusStandalone;
69
+ }
70
+ catch {
71
+ // A package whose manifest cannot be read declares nothing; the caller keeps walking
72
+ // because the declaring package may sit above it.
73
+ return undefined;
74
+ }
75
+ const profile = standalone?.profile;
76
+ if (standalone === undefined || typeof profile !== 'string' || profile === '')
77
+ return undefined;
78
+ const rawOmitted = standalone.omittedPackages;
79
+ const omittedPackages = {};
80
+ if (rawOmitted !== undefined && typeof rawOmitted === 'object' && !Array.isArray(rawOmitted)) {
81
+ for (const [name, spec] of Object.entries(rawOmitted)) {
82
+ if (typeof spec !== 'string' || spec === '') {
83
+ throw new Error('dshPlusStandalone.omittedPackages.' + name + ' must be a non-empty string');
84
+ }
85
+ omittedPackages[name] = spec;
86
+ }
87
+ }
88
+ return { profileName: profile, omittedPackages };
89
+ }
19
90
  function requireRecord(value, label) {
20
91
  if (value === null || typeof value !== 'object' || Array.isArray(value))
21
92
  throw new Error(label + ' must be an object');
@@ -336,9 +407,12 @@ function linkBundle(consumerModules, name, source) {
336
407
  /** Resolve every path a command needs, without creating anything. */
337
408
  export function resolvePaths(anchor, env = process.env) {
338
409
  const home = resolveHome(env);
410
+ const declaration = readStandaloneDeclaration(anchor);
339
411
  return {
340
412
  home,
341
- profileDirectory: join(home, 'profiles', STANDALONE_PROFILE),
413
+ profileName: declaration.profileName,
414
+ omittedPackages: declaration.omittedPackages,
415
+ profileDirectory: join(home, 'profiles', declaration.profileName),
342
416
  distributionDirectory: resolveDistributionDirectory(anchor),
343
417
  };
344
418
  }
@@ -366,7 +440,7 @@ export function ensureProfile(paths, consumerDirectory) {
366
440
  // script allowlist — and a distribution release changes them. Rewriting on every
367
441
  // start is what lets an upgraded installation receive the new values; a profile
368
442
  // written once keeps whatever its own release decided and can never be corrected.
369
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
443
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
370
444
  installProfilePackages(paths, consumerDirectory);
371
445
  return false;
372
446
  }
@@ -392,7 +466,7 @@ export function ensureProfile(paths, consumerDirectory) {
392
466
  };
393
467
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
394
468
  // The overrides must reach the workspace before the install that reads them.
395
- writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
469
+ writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
396
470
  installProfilePackages(paths, consumerDirectory);
397
471
  return true;
398
472
  }
@@ -406,7 +480,7 @@ export function ensureProfile(paths, consumerDirectory) {
406
480
  * @param profileDirectory - the standalone profile directory.
407
481
  * @param overrides - official package name to published replacement spec.
408
482
  */
409
- function writeProfileOverrides(profileDirectory, overrides, allowBuilds) {
483
+ function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omittedPackages) {
410
484
  const workspacePath = join(profileDirectory, 'pnpm-workspace.yaml');
411
485
  const document = parseDocument(existsSync(workspacePath) ? readFileSync(workspacePath, 'utf8') : '');
412
486
  const [documentError] = document.errors;
@@ -416,6 +490,11 @@ function writeProfileOverrides(profileDirectory, overrides, allowBuilds) {
416
490
  document.set('packages', ['.']);
417
491
  for (const [name, spec] of Object.entries(overrides))
418
492
  document.setIn(['overrides', name], spec);
493
+ // A capability the deployment omits is substituted rather than deleted, because npm's
494
+ // override has no removal form. Writing it here keeps the omission in the one place
495
+ // pnpm reads, so an install never receives the capability the variant excluded.
496
+ for (const [name, spec] of Object.entries(omittedPackages))
497
+ document.setIn(['overrides', name], spec);
419
498
  // pnpm refuses an install whose packages want to run build scripts until each is
420
499
  // decided, so the distribution's reviewed decisions travel with the install rather
421
500
  // than waiting for an interactive approval no start can offer.
package/package.json CHANGED
@@ -3,12 +3,8 @@
3
3
  "dsh-plus": "lib/bin.js"
4
4
  },
5
5
  "dependencies": {
6
- "@deepseek-ai/dsh-computer-use": ">=0.1.6-alpha.1",
7
- "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": ">=0.1.6-alpha.1",
8
6
  "@deepseek-ai/dsh-tool-session-query": ">=0.1.6-alpha.1",
9
- "@deepseek-ai/dsh-web-search-exa": ">=0.1.6-alpha.1",
10
7
  "@sparkelf/dsh-client-ui-skill-center": ">=0.2.0-rc.17",
11
- "@sparkelf/dsh-mobile-bridge": ">=0.2.11",
12
8
  "@sparkelf/dsh-patch-better-sidebar-browser-url-seed": ">=0.2.0-rc.17",
13
9
  "@sparkelf/dsh-patch-better-sidebar-html-preview-path": ">=0.2.0-rc.17",
14
10
  "@sparkelf/dsh-patch-better-sidebar-main-view-session": ">=0.2.0-rc.17",
@@ -29,7 +25,6 @@
29
25
  "@sparkelf/dsh-patch-workspace-storage-restore": ">=0.2.0-rc.17",
30
26
  "@sparkelf/dsh-patch-wsl-native-open": ">=0.2.0-rc.17",
31
27
  "@sparkelf/dsh-plugin-backup": ">=0.2.0-rc.17",
32
- "@sparkelf/dsh-plugin-dataops": ">=0.2.0-rc.17",
33
28
  "@sparkelf/dsh-plugin-mcp-credentials": ">=0.2.0-rc.17",
34
29
  "@sparkelf/dsh-plugin-subagent-settings": ">=0.2.0-rc.17",
35
30
  "dshmarket": ">=1.45.1",
@@ -67,11 +62,11 @@
67
62
  "@sparkelf/dsh-patch-ptc-mcp-schema-types",
68
63
  "@sparkelf/dsh-patch-web-base-path",
69
64
  "@sparkelf/dsh-patch-composer-popover-boundaries",
70
- "@sparkelf/dsh-patch-mobile-journal-generation",
71
65
  "@sparkelf/dsh-patch-session-format-legacy-restart",
72
66
  "@sparkelf/dsh-patch-wsl-native-open",
73
67
  "@sparkelf/dsh-patch-better-sidebar-html-preview-path",
74
- "@sparkelf/dsh-patch-session-query-unindexable-session"
68
+ "@sparkelf/dsh-patch-session-query-unindexable-session",
69
+ "@sparkelf/dsh-patch-mobile-journal-generation"
75
70
  ],
76
71
  "profile": {
77
72
  "allowBuilds": {
@@ -92,7 +87,6 @@
92
87
  "@deepseek-ai/dsh-experimental-agent-team-web-profile",
93
88
  "@sparkelf/dsh-mineru",
94
89
  "@sparkelf/dsh-officecli",
95
- "@sparkelf/dsh-mobile-bridge",
96
90
  "dshmarket",
97
91
  "@huanlin/dsh-plugin-better-locale",
98
92
  "dsh-better-sidebar",
@@ -104,7 +98,8 @@
104
98
  "@sparkelf/dsh-plus",
105
99
  "dsh-sql-workbench",
106
100
  "@sparkelf/dsh-ssh-manager",
107
- "@sparkelf/dsh-api-client"
101
+ "@sparkelf/dsh-api-client",
102
+ "dsh-right-bg-anim"
108
103
  ],
109
104
  "dependencies": {
110
105
  "@changfenhuang/dsh-genui": "0.11.0",
@@ -120,13 +115,15 @@
120
115
  "@sparkelf/dsh-ssh-manager": "0.7.2",
121
116
  "@sparkelf/dsh-workbench-vault": "0.1.2",
122
117
  "dsh-better-sidebar": "0.19.1",
118
+ "dsh-right-bg-anim": "1.1.0",
123
119
  "dsh-sql-workbench": "0.5.1",
124
120
  "dsh-video-preview": "0.1.4"
125
121
  },
126
122
  "overrides": {
127
- "@deepseek-ai/dsh-agent-presets": "npm:@sparkelf/dsh-agent-presets@0.1.6-alpha.2",
123
+ "@deepseek-ai/dsh-agent-presets": "npm:@sparkelf/dsh-agent-presets@0.1.6-alpha.3",
128
124
  "@deepseek-ai/dsh-api-gateway": "npm:@sparkelf/dsh-api-gateway@0.1.6-alpha.2",
129
- "@deepseek-ai/dsh-api-session-controller": "npm:@sparkelf/dsh-api-session-controller@0.1.6-alpha.2",
125
+ "@deepseek-ai/dsh-api-session-controller": "npm:@sparkelf/dsh-api-session-controller@0.1.6-alpha.3",
126
+ "@deepseek-ai/dsh-api-terminal-controller": "npm:@sparkelf/dsh-api-terminal-controller@0.1.6-alpha.3",
130
127
  "@deepseek-ai/dsh-client-connection": "npm:@sparkelf/dsh-client-connection@0.1.6-alpha.2",
131
128
  "@deepseek-ai/dsh-client-ui-agent-preset": "npm:@sparkelf/dsh-client-ui-agent-preset@0.1.6-alpha.2",
132
129
  "@deepseek-ai/dsh-client-ui-deliverables": "npm:@sparkelf/dsh-client-ui-deliverables@0.1.6-alpha.2",
@@ -143,10 +140,23 @@
143
140
  "@deepseek-ai/dsh-native-command": "npm:@sparkelf/dsh-native-command@0.1.6-alpha.2",
144
141
  "@deepseek-ai/dsh-session-format-v0-to-v1": "npm:@sparkelf/dsh-session-format-v0-to-v1@0.1.6-alpha.2",
145
142
  "@deepseek-ai/dsh-session-log-export": "npm:@sparkelf/dsh-session-log-export@0.1.6-alpha.2",
143
+ "@deepseek-ai/dsh-session-query-sqlite": "npm:@sparkelf/dsh-session-query-sqlite@0.1.6-alpha.2",
146
144
  "@deepseek-ai/dsh-tools": "npm:@sparkelf/dsh-tools@0.1.6-alpha.2",
147
145
  "@deepseek-ai/dsh-web-app": "npm:@sparkelf/dsh-web-app@0.1.6-alpha.2",
148
146
  "@deepseek-ai/dsh-web-frontend": "npm:@sparkelf/dsh-web-frontend@0.1.6-alpha.2",
149
147
  "@deepseek-ai/dsh-workspace": "npm:@sparkelf/dsh-workspace@0.1.6-alpha.2"
148
+ },
149
+ "standaloneVariants": {
150
+ "dataops": {
151
+ "excludeBundles": [],
152
+ "excludePackages": [
153
+ "@deepseek-ai/dsh-computer-use",
154
+ "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp",
155
+ "@deepseek-ai/dsh-web-search-exa"
156
+ ],
157
+ "packageName": "@sparkelf/dsh-dataops-standalone",
158
+ "profile": "dataops-web"
159
+ }
150
160
  }
151
161
  },
152
162
  "sourceBase": {
@@ -176,6 +186,11 @@
176
186
  "license": "MIT",
177
187
  "main": "lib/index.js",
178
188
  "name": "@sparkelf/dsh-plus",
189
+ "optionalDependencies": {
190
+ "@deepseek-ai/dsh-computer-use": ">=0.1.6-alpha.1",
191
+ "@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp": ">=0.1.6-alpha.1",
192
+ "@deepseek-ai/dsh-web-search-exa": ">=0.1.6-alpha.1"
193
+ },
179
194
  "peerDependencies": {
180
195
  "@deepseek-ai/cordis": ">=4.0.1"
181
196
  },
@@ -189,5 +204,5 @@
189
204
  },
190
205
  "type": "module",
191
206
  "types": "lib/types/index.d.ts",
192
- "version": "0.2.0-rc.17"
207
+ "version": "0.2.0-rc.19"
193
208
  }