@sparkelf/dsh-plus 0.2.0-rc.18 → 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/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
  }
@@ -1029,6 +1030,7 @@ function parseStartOptions(argv) {
1029
1030
  let host = "127.0.0.1";
1030
1031
  let open = true;
1031
1032
  let foreground = false;
1033
+ let capabilities;
1032
1034
  for (let index = 0; index < argv.length; index += 1) {
1033
1035
  const token = argv[index];
1034
1036
  if (token === "--port" || token === "-p") {
@@ -1053,13 +1055,41 @@ function parseStartOptions(argv) {
1053
1055
  foreground = true;
1054
1056
  continue;
1055
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
+ }
1056
1068
  throw new Error("unknown option: " + String(token));
1057
1069
  }
1058
1070
  return {
1059
1071
  port,
1060
1072
  host,
1061
1073
  open,
1062
- 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 } : {}
1063
1093
  };
1064
1094
  }
1065
1095
  /**
@@ -1090,6 +1120,25 @@ function installationRoot() {
1090
1120
  function installationAnchor() {
1091
1121
  return join(installationRoot(), "package.json");
1092
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
+ }
1093
1142
  /** The launcher entry this installation must drive. */
1094
1143
  function launcherEntry(anchor) {
1095
1144
  return createRequire(anchor).resolve("@deepseek-ai/dsh/lib/bin.js");
@@ -1111,7 +1160,7 @@ function runForeground(entry, profileName, port, host, open) {
1111
1160
  async function start(argv) {
1112
1161
  const options = parseStartOptions(argv);
1113
1162
  const anchor = installationAnchor();
1114
- const paths = resolvePaths(anchor);
1163
+ const paths = resolvePaths(declarationAnchor());
1115
1164
  if (!pnpmAvailable()) {
1116
1165
  const command = pnpmInstallCommand();
1117
1166
  console.log("pnpm is required to install the plus profile, and was not found.");
@@ -1144,7 +1193,7 @@ async function start(argv) {
1144
1193
  const created = ensureProfile(paths, installationRoot());
1145
1194
  console.log(created ? "Created the " + paths.profileName + " profile at " + paths.profileDirectory : "Using the existing " + paths.profileName + " profile");
1146
1195
  for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
1147
- 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;
1148
1197
  if (answers !== void 0) {
1149
1198
  const ready = await installCapabilityServices(answers, paths.home);
1150
1199
  writeCapabilityPatch(paths.profileDirectory, answers);
@@ -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,6 +126,25 @@ 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');
@@ -102,7 +160,9 @@ function runForeground(entry, profileName, 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.
@@ -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);
package/package.json CHANGED
@@ -120,9 +120,10 @@
120
120
  "dsh-video-preview": "0.1.4"
121
121
  },
122
122
  "overrides": {
123
- "@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",
124
124
  "@deepseek-ai/dsh-api-gateway": "npm:@sparkelf/dsh-api-gateway@0.1.6-alpha.2",
125
- "@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",
126
127
  "@deepseek-ai/dsh-client-connection": "npm:@sparkelf/dsh-client-connection@0.1.6-alpha.2",
127
128
  "@deepseek-ai/dsh-client-ui-agent-preset": "npm:@sparkelf/dsh-client-ui-agent-preset@0.1.6-alpha.2",
128
129
  "@deepseek-ai/dsh-client-ui-deliverables": "npm:@sparkelf/dsh-client-ui-deliverables@0.1.6-alpha.2",
@@ -203,5 +204,5 @@
203
204
  },
204
205
  "type": "module",
205
206
  "types": "lib/types/index.d.ts",
206
- "version": "0.2.0-rc.18"
207
+ "version": "0.2.0-rc.19"
207
208
  }