@sparkelf/dsh-plus 0.2.0-rc.34 → 0.2.0-rc.37
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 +374 -12
- package/lib/types/standalone-capabilities.d.ts +48 -3
- package/lib/types/standalone-capabilities.js +301 -11
- package/lib/types/standalone-cli.js +163 -6
- package/package.json +32 -32
package/lib/bin.js
CHANGED
|
@@ -160,15 +160,21 @@ async function interviewCapabilities(interactive) {
|
|
|
160
160
|
...mineruEndpoint === void 0 ? {} : { mineruEndpoint }
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
-
/**
|
|
164
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Names this module owns in the deployment env file.
|
|
165
|
+
*
|
|
166
|
+
* Only names the loader accepts here: a `DSH_`-prefixed variable is refused in any `.env` because
|
|
167
|
+
* `DSH_HOME` is itself bootstrap-only and the file could otherwise relocate the home it is read
|
|
168
|
+
* from. MinerU's endpoint therefore travels in the profile layer, which is also where the plugin
|
|
169
|
+
* reads it.
|
|
170
|
+
*/
|
|
171
|
+
const CAPABILITY_ENV_NAMES = ["EXA_API_KEY"];
|
|
165
172
|
/**
|
|
166
173
|
* The deployment env file, with one assignment per enabled capability that reaches
|
|
167
174
|
* its service through the launch environment.
|
|
168
175
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
176
|
+
* Exa reads its key from the launch environment, so a capability the user selected is enabled here
|
|
177
|
+
* as well as in the profile layer. Only the names this interview owns are rewritten: a
|
|
172
178
|
* deployment that keeps its own assignments in the same file keeps them, and
|
|
173
179
|
* deselecting a capability removes the assignment that turned it on.
|
|
174
180
|
*
|
|
@@ -179,13 +185,20 @@ const CAPABILITY_ENV_NAMES = ["DSH_MINERU_ENDPOINT", "EXA_API_KEY"];
|
|
|
179
185
|
function capabilityEnvironment(answers, existing = "") {
|
|
180
186
|
const enabled = new Set(answers.enabled);
|
|
181
187
|
const owned = new Set(CAPABILITY_ENV_NAMES);
|
|
188
|
+
const carried = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const line of existing.split("\n")) {
|
|
190
|
+
const parsed = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
|
|
191
|
+
if (parsed?.[1] !== void 0 && owned.has(parsed[1])) carried.set(parsed[1], parsed[2] ?? "");
|
|
192
|
+
}
|
|
182
193
|
const kept = existing.split("\n").filter((line) => {
|
|
183
194
|
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
184
195
|
return line.trim() !== "" && !line.trimStart().startsWith("#") && (name === void 0 || !owned.has(name));
|
|
185
196
|
});
|
|
186
197
|
const written = ["# Written by dsh-plus start from the capability interview. Enabling or", "# disabling a capability rewrites the lines below; other lines are kept."];
|
|
187
|
-
if (enabled.has("
|
|
188
|
-
|
|
198
|
+
if (enabled.has("exa")) {
|
|
199
|
+
const key = answers.exaApiKey ?? carried.get("EXA_API_KEY");
|
|
200
|
+
if (key !== void 0 && key !== "") written.push("EXA_API_KEY=" + key);
|
|
201
|
+
}
|
|
189
202
|
return [...written, ...kept].join("\n") + "\n";
|
|
190
203
|
}
|
|
191
204
|
/**
|
|
@@ -208,6 +221,8 @@ function capabilityPatchLayer(answers) {
|
|
|
208
221
|
const rows = [];
|
|
209
222
|
if (enabled.has("exa")) rows.push(" - id: web-search-exa", " name: '@deepseek-ai/dsh-web-search-exa'", " config:", " searchType: auto", " numResults: 8");
|
|
210
223
|
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'");
|
|
224
|
+
if (enabled.has("officecli")) rows.push(" - id: officecli", " name: '@sparkelf/dsh-officecli'");
|
|
225
|
+
if (enabled.has("mineru")) rows.push(" - id: mineru", " name: '@sparkelf/dsh-mineru'", " config:", " endpoint: " + (answers.mineruEndpoint ?? "http://127.0.0.1:8000/file_parse"));
|
|
211
226
|
const lines = ["# Written by dsh-plus start from the capability interview. Enabling or disabling a capability", "# rewrites this file; edits here are replaced."];
|
|
212
227
|
if (rows.length > 0) lines.push("- insert:", ...rows);
|
|
213
228
|
else lines.push("[]");
|
|
@@ -219,6 +234,212 @@ function runStep(command, args) {
|
|
|
219
234
|
return spawnSync(command, [...args], { stdio: "inherit" }).status === 0;
|
|
220
235
|
}
|
|
221
236
|
/**
|
|
237
|
+
* The capability record a previous configured run wrote, or `undefined` when none exists.
|
|
238
|
+
*
|
|
239
|
+
* The record is what lets a later run report the deployment's answers instead of asking
|
|
240
|
+
* again. A malformed or unreadable file is treated as absent rather than fatal: it is a
|
|
241
|
+
* convenience cache, and refusing to boot over it would strand a deployment whose record
|
|
242
|
+
* was corrupted by an interrupted write.
|
|
243
|
+
*
|
|
244
|
+
* @param home - the deployment home holding the record.
|
|
245
|
+
* @returns the enabled ids, and the MinerU endpoint when the record carries one.
|
|
246
|
+
*/
|
|
247
|
+
function readCapabilityRecord(home) {
|
|
248
|
+
const path = join(home, CAPABILITY_RECORD);
|
|
249
|
+
let parsed;
|
|
250
|
+
try {
|
|
251
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
252
|
+
} catch {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
256
|
+
const record = parsed;
|
|
257
|
+
if (!Array.isArray(record.enabled)) return void 0;
|
|
258
|
+
return {
|
|
259
|
+
enabled: record.enabled.filter((id) => typeof id === "string"),
|
|
260
|
+
...typeof record.mineruEndpoint === "string" ? { mineruEndpoint: record.mineruEndpoint } : {}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The capabilities a deployment actually has mounted, read from the files the loader and
|
|
265
|
+
* the launch environment consume.
|
|
266
|
+
*
|
|
267
|
+
* A record is a cache of an earlier answer and can drift from the deployment — a profile
|
|
268
|
+
* layer edited by hand, a record deleted, a plugin removed from the distribution. What a
|
|
269
|
+
* capability is *doing* is decided by the profile patch rows and the env file, so that is
|
|
270
|
+
* what a report reads.
|
|
271
|
+
*
|
|
272
|
+
* @param profilePatchText - the profile layer's YAML text.
|
|
273
|
+
* @param envText - the deployment env file's text.
|
|
274
|
+
* @returns the ids found in the profile layer and the env names present.
|
|
275
|
+
*/
|
|
276
|
+
function detectEnabledCapabilities(profilePatchText, envText) {
|
|
277
|
+
const mounted = [];
|
|
278
|
+
if (/^\s*- id: web-search-exa\s*$/mu.test(profilePatchText)) mounted.push("exa");
|
|
279
|
+
if (/^\s*- id: mineru\s*$/mu.test(profilePatchText)) mounted.push("mineru");
|
|
280
|
+
if (/^\s*- id: officecli\s*$/mu.test(profilePatchText)) mounted.push("officecli");
|
|
281
|
+
if (/^\s*- id: computer-use\s*$/mu.test(profilePatchText)) mounted.push("computer-use");
|
|
282
|
+
const env = [];
|
|
283
|
+
for (const line of envText.split("\n")) {
|
|
284
|
+
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
285
|
+
if (name !== void 0 && !env.includes(name)) env.push(name);
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
mounted,
|
|
289
|
+
env
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The patch row one capability mounts, as a list of YAML lines, or `undefined` when the
|
|
294
|
+
* capability is turned on by something other than a row this module owns.
|
|
295
|
+
*
|
|
296
|
+
* MinerU is enabled by a row too, but its endpoint is per-deployment, so the caller passes
|
|
297
|
+
* the answers rather than a fixed row.
|
|
298
|
+
*
|
|
299
|
+
* @param id - the capability id.
|
|
300
|
+
* @returns the insert payload lines, without the surrounding `- insert:` entry.
|
|
301
|
+
*/
|
|
302
|
+
function capabilityRows(id) {
|
|
303
|
+
if (id === "exa") return [
|
|
304
|
+
" - id: web-search-exa",
|
|
305
|
+
" name: '@deepseek-ai/dsh-web-search-exa'",
|
|
306
|
+
" config:",
|
|
307
|
+
" searchType: auto",
|
|
308
|
+
" numResults: 8"
|
|
309
|
+
];
|
|
310
|
+
if (id === "officecli") return [" - id: officecli", " name: '@sparkelf/dsh-officecli'"];
|
|
311
|
+
if (id === "computer-use") return [
|
|
312
|
+
" - id: computer-use",
|
|
313
|
+
" name: '@deepseek-ai/dsh-computer-use'",
|
|
314
|
+
" - id: computer-use-cua-driver-mcp",
|
|
315
|
+
" name: '@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp'"
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
/** The row ids a capability owns in the profile layer. */
|
|
319
|
+
function capabilityRowIds(id) {
|
|
320
|
+
if (id === "exa") return ["web-search-exa"];
|
|
321
|
+
if (id === "officecli") return ["officecli"];
|
|
322
|
+
if (id === "mineru") return ["mineru"];
|
|
323
|
+
if (id === "computer-use") return ["computer-use", "computer-use-cua-driver-mcp"];
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* One capability's effect, added to or removed from an existing profile layer without
|
|
328
|
+
* disturbing the rest of the file.
|
|
329
|
+
*
|
|
330
|
+
* `dsh-plus start` owns the whole layer and rewrites it from the interview's answers, which
|
|
331
|
+
* is correct for an install that answered once and never edits afterwards. A deployment
|
|
332
|
+
* that maintains its own rows in that same layer cannot be served that way: replacing the
|
|
333
|
+
* file drops every row the operator wrote. This edits the rows this module owns and leaves
|
|
334
|
+
* every other entry, comment, and blank line exactly where it was.
|
|
335
|
+
*
|
|
336
|
+
* @param existing - the layer's current YAML text.
|
|
337
|
+
* @param id - the capability to add or remove.
|
|
338
|
+
* @param enable - whether to add the capability.
|
|
339
|
+
* @param mineruEndpoint - the endpoint to write when enabling MinerU.
|
|
340
|
+
* @returns the layer's new text.
|
|
341
|
+
*/
|
|
342
|
+
function editCapabilityPatchLayer(existing, id, enable, mineruEndpoint) {
|
|
343
|
+
const withoutRows = dropOwnedRows(existing, capabilityRowIds(id), id === "exa");
|
|
344
|
+
const mineruRows = [
|
|
345
|
+
" - id: mineru",
|
|
346
|
+
" name: '@sparkelf/dsh-mineru'",
|
|
347
|
+
" config:",
|
|
348
|
+
" endpoint: " + (mineruEndpoint ?? "http://127.0.0.1:8000/file_parse")
|
|
349
|
+
];
|
|
350
|
+
const rows = id === "mineru" && enable ? mineruRows : capabilityRows(id);
|
|
351
|
+
const trimmed = (enable && rows !== void 0 ? insertCapabilityRows(withoutRows, rows, id === "exa") : withoutRows.text).replace(/\n+$/u, "");
|
|
352
|
+
return trimmed === "" ? "\n" : trimmed + "\n";
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Remove the rows one capability owns, and for Exa the `web` selector row that routes
|
|
356
|
+
* search to it.
|
|
357
|
+
*
|
|
358
|
+
* A row is dropped with the block that belongs to it: the `- id:` line plus its indented
|
|
359
|
+
* continuation lines, and any comment line directly above that documented it. Comment and
|
|
360
|
+
* blank lines separating it from the next entry stay, so the surrounding file keeps its
|
|
361
|
+
* shape.
|
|
362
|
+
*
|
|
363
|
+
* @param existing - the layer's current YAML text.
|
|
364
|
+
* @param ids - the row ids this capability owns.
|
|
365
|
+
* @param withSelector - whether the capability also owns the shared `web` selector row.
|
|
366
|
+
* @returns the remaining text and whether anything was actually removed.
|
|
367
|
+
*/
|
|
368
|
+
function dropOwnedRows(existing, ids, withSelector) {
|
|
369
|
+
const lines = existing.split("\n");
|
|
370
|
+
const out = [];
|
|
371
|
+
let removed = false;
|
|
372
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
373
|
+
const line = lines[index] ?? "";
|
|
374
|
+
const match = /^(\s*)- id:\s*(\S+)\s*$/.exec(line);
|
|
375
|
+
const isSelector = withSelector && match?.[2] === "web" && match[1] === "";
|
|
376
|
+
if (match === null || !ids.includes(match[2] ?? "") && !isSelector) {
|
|
377
|
+
out.push(line);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
removed = true;
|
|
381
|
+
const indent = match[1] ?? "";
|
|
382
|
+
while (out.length > 0) {
|
|
383
|
+
const previous = out[out.length - 1] ?? "";
|
|
384
|
+
if (previous.trimStart().startsWith("#") && previous.trim() !== "") out.pop();
|
|
385
|
+
else break;
|
|
386
|
+
}
|
|
387
|
+
for (index += 1; index < lines.length; index += 1) {
|
|
388
|
+
const next = lines[index] ?? "";
|
|
389
|
+
if (next.trim() === "") {
|
|
390
|
+
const after = lines[index + 1] ?? "";
|
|
391
|
+
if (after !== "" && (after.startsWith(indent + " ") || after.startsWith(indent + " ") || after.trimStart().startsWith("#"))) continue;
|
|
392
|
+
index -= 1;
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
395
|
+
if ((/^\s*/.exec(next)?.[0] ?? "").length > indent.length) continue;
|
|
396
|
+
index -= 1;
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
text: out.join("\n"),
|
|
402
|
+
removed
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Add a capability's rows to the layer, creating the `- insert:` entry when the file has
|
|
407
|
+
* none and appending to the existing one otherwise.
|
|
408
|
+
*
|
|
409
|
+
* The loader indexes inserted rows under the layer that inserted them, so appending to an
|
|
410
|
+
* existing `- insert:` list and adding a second one both work; appending keeps the file
|
|
411
|
+
* readable instead of growing one `- insert:` per capability.
|
|
412
|
+
*
|
|
413
|
+
* @param state - the layer with this capability's own rows already removed.
|
|
414
|
+
* @param rows - the insert payload lines to add.
|
|
415
|
+
* @param withSelector - whether Exa's `web` selector row must also be present.
|
|
416
|
+
* @returns the layer's new text.
|
|
417
|
+
*/
|
|
418
|
+
function insertCapabilityRows(state, rows, withSelector) {
|
|
419
|
+
const lines = state.text.split("\n");
|
|
420
|
+
const insertIndex = lines.findIndex((line) => /^- insert:\s*$/.test(line));
|
|
421
|
+
if (insertIndex >= 0) {
|
|
422
|
+
let end = insertIndex + 1;
|
|
423
|
+
while (end < lines.length) {
|
|
424
|
+
const line = lines[end] ?? "";
|
|
425
|
+
if (line.trim() === "" || /^\s/.test(line)) end += 1;
|
|
426
|
+
else break;
|
|
427
|
+
}
|
|
428
|
+
while (end > insertIndex + 1 && (lines[end - 1] ?? "").trim() === "") end -= 1;
|
|
429
|
+
lines.splice(end, 0, ...rows);
|
|
430
|
+
} else {
|
|
431
|
+
const emptyIndex = lines.findIndex((line) => line.trim() === "[]");
|
|
432
|
+
if (emptyIndex >= 0) lines.splice(emptyIndex, 1, "- insert:", ...rows);
|
|
433
|
+
else {
|
|
434
|
+
const trailing = lines.length > 0 && (lines[lines.length - 1] ?? "") === "" ? lines.pop() ?? "" : void 0;
|
|
435
|
+
lines.push("- insert:", ...rows);
|
|
436
|
+
if (trailing !== void 0) lines.push(trailing);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (withSelector && !/^\s*- id: web\s*$/mu.test(lines.join("\n"))) lines.push("", "# Route web_search through Exa rather than the built-in provider.", "- id: web", " config:", " searchProvider: exa", " fetchProvider: http");
|
|
440
|
+
return lines.join("\n");
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
222
443
|
* Install and start the backing services the selected capabilities need.
|
|
223
444
|
*
|
|
224
445
|
* Enabling a capability means the deployment expects its service to answer, so the
|
|
@@ -1388,6 +1609,134 @@ function confirm(question) {
|
|
|
1388
1609
|
});
|
|
1389
1610
|
});
|
|
1390
1611
|
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Report and change which optional capabilities this deployment has enabled.
|
|
1614
|
+
*
|
|
1615
|
+
* The interview runs once, at install time, and a deployment that answered it keeps its
|
|
1616
|
+
* answers — so this is the only way to see what it chose or to change one answer later.
|
|
1617
|
+
* The report is read from the profile layer and the env file rather than from the record,
|
|
1618
|
+
* because those two are what the loader and the launch environment actually consume: a
|
|
1619
|
+
* record deleted, or a layer edited by hand, would otherwise describe a deployment that
|
|
1620
|
+
* does not exist.
|
|
1621
|
+
*
|
|
1622
|
+
* `--enable` and `--disable` edit only the rows this module owns. The profile layer is
|
|
1623
|
+
* shared with rows an operator wrote themselves, so it is never replaced wholesale.
|
|
1624
|
+
*
|
|
1625
|
+
* @param argv - arguments after the command word.
|
|
1626
|
+
* @returns the process exit code.
|
|
1627
|
+
*/
|
|
1628
|
+
function capabilities(argv) {
|
|
1629
|
+
const paths = resolvePaths(installationAnchor());
|
|
1630
|
+
const patchPath = join(paths.profileDirectory, CAPABILITY_PATCH_FILE);
|
|
1631
|
+
const envPath = join(paths.home, CAPABILITY_ENV_FILE);
|
|
1632
|
+
const patchText = existsSync(patchPath) ? readFileSync(patchPath, "utf8") : "";
|
|
1633
|
+
const envText = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
|
|
1634
|
+
const actual = detectEnabledCapabilities(patchText, envText);
|
|
1635
|
+
const record = readCapabilityRecord(paths.home);
|
|
1636
|
+
let enable = [];
|
|
1637
|
+
let disable = [];
|
|
1638
|
+
let asJson = false;
|
|
1639
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
1640
|
+
const token = argv[index];
|
|
1641
|
+
if (token === "--json") {
|
|
1642
|
+
asJson = true;
|
|
1643
|
+
continue;
|
|
1644
|
+
}
|
|
1645
|
+
if (token === "--enable" || token === "--disable") {
|
|
1646
|
+
const value = argv[index + 1];
|
|
1647
|
+
const ids = (value ?? "").split(",").map((id) => id.trim()).filter((id) => id !== "");
|
|
1648
|
+
if (value === void 0 || ids.length === 0) {
|
|
1649
|
+
console.error(token + " requires a comma-separated list of capability ids");
|
|
1650
|
+
return 1;
|
|
1651
|
+
}
|
|
1652
|
+
const unknown = ids.filter((id) => !CAPABILITIES.some((capability) => capability.id === id));
|
|
1653
|
+
if (unknown.length > 0) {
|
|
1654
|
+
console.error("unknown capability: " + unknown.join(", "));
|
|
1655
|
+
console.error("Known: " + CAPABILITIES.map((capability) => capability.id).join(", "));
|
|
1656
|
+
return 1;
|
|
1657
|
+
}
|
|
1658
|
+
if (token === "--enable") enable = [...new Set([...enable, ...ids])];
|
|
1659
|
+
else disable = [...new Set([...disable, ...ids])];
|
|
1660
|
+
index += 1;
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
console.error("unknown option: " + String(token));
|
|
1664
|
+
return 1;
|
|
1665
|
+
}
|
|
1666
|
+
const conflicting = enable.filter((id) => disable.includes(id));
|
|
1667
|
+
if (conflicting.length > 0) {
|
|
1668
|
+
console.error("cannot both enable and disable: " + conflicting.join(", "));
|
|
1669
|
+
return 1;
|
|
1670
|
+
}
|
|
1671
|
+
if (enable.length > 0 || disable.length > 0) {
|
|
1672
|
+
const enabledNow = new Set(actual.mounted);
|
|
1673
|
+
let nextPatch = patchText;
|
|
1674
|
+
for (const id of enable) {
|
|
1675
|
+
nextPatch = editCapabilityPatchLayer(nextPatch, id, true, record?.mineruEndpoint);
|
|
1676
|
+
enabledNow.add(id);
|
|
1677
|
+
}
|
|
1678
|
+
for (const id of disable) {
|
|
1679
|
+
nextPatch = editCapabilityPatchLayer(nextPatch, id, false);
|
|
1680
|
+
enabledNow.delete(id);
|
|
1681
|
+
}
|
|
1682
|
+
if (!existsSync(patchPath) || !patchText.includes("Written by dsh-plus start from the capability interview.")) {
|
|
1683
|
+
const backup = patchPath + ".before-capabilities";
|
|
1684
|
+
if (patchText !== "" && !existsSync(backup)) writeFileSync(backup, patchText);
|
|
1685
|
+
}
|
|
1686
|
+
writeFileSync(patchPath, nextPatch);
|
|
1687
|
+
const nextEnv = capabilityEnvironment({
|
|
1688
|
+
enabled: [...enabledNow],
|
|
1689
|
+
...record?.mineruEndpoint === void 0 ? {} : { mineruEndpoint: record.mineruEndpoint }
|
|
1690
|
+
}, envText);
|
|
1691
|
+
writeFileSync(envPath, nextEnv);
|
|
1692
|
+
const after = detectEnabledCapabilities(nextPatch, nextEnv);
|
|
1693
|
+
console.log("Updated cordis.patch.yml and .env.");
|
|
1694
|
+
console.log("Run dsh-plus start (or restart the server) to apply the change.");
|
|
1695
|
+
return reportCapabilities(after, record, asJson);
|
|
1696
|
+
}
|
|
1697
|
+
return reportCapabilities(actual, record, asJson);
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Print the capability report.
|
|
1701
|
+
*
|
|
1702
|
+
* @param actual - the capabilities found in the profile layer and env file.
|
|
1703
|
+
* @param record - what the last configured run recorded, when a record exists.
|
|
1704
|
+
* @param asJson - when true, print machine-readable JSON instead of the table.
|
|
1705
|
+
* @returns the process exit code, always 0.
|
|
1706
|
+
*/
|
|
1707
|
+
function reportCapabilities(actual, record, asJson) {
|
|
1708
|
+
const rows = CAPABILITIES.map((capability) => {
|
|
1709
|
+
const mounted = actual.mounted.includes(capability.id);
|
|
1710
|
+
const recorded = record?.enabled.includes(capability.id);
|
|
1711
|
+
return {
|
|
1712
|
+
id: capability.id,
|
|
1713
|
+
title: capability.title,
|
|
1714
|
+
enabled: mounted,
|
|
1715
|
+
recorded: recorded ?? null
|
|
1716
|
+
};
|
|
1717
|
+
});
|
|
1718
|
+
if (asJson) {
|
|
1719
|
+
console.log(JSON.stringify({
|
|
1720
|
+
capabilities: rows,
|
|
1721
|
+
env: actual.env,
|
|
1722
|
+
recordPresent: record !== void 0
|
|
1723
|
+
}, null, 2));
|
|
1724
|
+
return 0;
|
|
1725
|
+
}
|
|
1726
|
+
console.log("Optional capabilities:");
|
|
1727
|
+
for (const row of rows) {
|
|
1728
|
+
const state = row.enabled ? "enabled " : "disabled";
|
|
1729
|
+
const drift = row.recorded !== null && row.recorded !== row.enabled ? " (record says " + (row.recorded ? "enabled" : "disabled") + ")" : "";
|
|
1730
|
+
console.log(" " + (row.enabled ? "ok " : "-- ") + row.id.padEnd(14) + state + drift);
|
|
1731
|
+
}
|
|
1732
|
+
if (record === void 0) {
|
|
1733
|
+
console.log("");
|
|
1734
|
+
console.log("No capabilities.json yet; the next dsh-plus start will run the interview.");
|
|
1735
|
+
}
|
|
1736
|
+
console.log("");
|
|
1737
|
+
console.log("Change with: dsh-plus capabilities --enable <id> | --disable <id>");
|
|
1738
|
+
return 0;
|
|
1739
|
+
}
|
|
1391
1740
|
async function doctor() {
|
|
1392
1741
|
const paths = resolvePaths(installationAnchor());
|
|
1393
1742
|
let failures = 0;
|
|
@@ -1400,6 +1749,12 @@ async function doctor() {
|
|
|
1400
1749
|
check(existsSync(join(paths.profileDirectory, "package.json")), "profile at " + paths.profileDirectory + " (dsh-plus start creates it)");
|
|
1401
1750
|
const free = await portAvailable(DEFAULT_PORT);
|
|
1402
1751
|
check(true, "port " + String(DEFAULT_PORT) + (free ? " is free" : " is in use; start will choose the next free one"));
|
|
1752
|
+
const patchPath = join(paths.profileDirectory, CAPABILITY_PATCH_FILE);
|
|
1753
|
+
const envPath = join(paths.home, CAPABILITY_ENV_FILE);
|
|
1754
|
+
const actual = detectEnabledCapabilities(existsSync(patchPath) ? readFileSync(patchPath, "utf8") : "", existsSync(envPath) ? readFileSync(envPath, "utf8") : "");
|
|
1755
|
+
const record = readCapabilityRecord(paths.home);
|
|
1756
|
+
const drifted = record === void 0 ? [] : CAPABILITIES.filter((capability) => record.enabled.includes(capability.id) !== actual.mounted.includes(capability.id));
|
|
1757
|
+
check(drifted.length === 0, drifted.length === 0 ? "capabilities: " + (actual.mounted.length === 0 ? "(none enabled)" : actual.mounted.join(", ")) : "capabilities disagree with capabilities.json: " + drifted.map((capability) => capability.id).join(", ") + " (run dsh-plus capabilities)");
|
|
1403
1758
|
console.log(failures === 0 ? "No problems found." : String(failures) + " problem(s) found.");
|
|
1404
1759
|
return failures === 0 ? 0 : 1;
|
|
1405
1760
|
}
|
|
@@ -1407,11 +1762,12 @@ const USAGE = [
|
|
|
1407
1762
|
"Usage: dsh-plus <command> [options]",
|
|
1408
1763
|
"",
|
|
1409
1764
|
"Commands:",
|
|
1410
|
-
" start
|
|
1411
|
-
" stop
|
|
1412
|
-
" status
|
|
1413
|
-
"
|
|
1414
|
-
"
|
|
1765
|
+
" start start the server (first run creates the profile)",
|
|
1766
|
+
" stop stop the server",
|
|
1767
|
+
" status report whether the server is running",
|
|
1768
|
+
" capabilities report or change the optional capabilities",
|
|
1769
|
+
" update move to a newer distribution release",
|
|
1770
|
+
" doctor check this installation",
|
|
1415
1771
|
"",
|
|
1416
1772
|
"start options:",
|
|
1417
1773
|
" --port <n> port to prefer (default " + String(DEFAULT_PORT) + "; a taken port moves to the next free one)",
|
|
@@ -1419,6 +1775,11 @@ const USAGE = [
|
|
|
1419
1775
|
" --no-open do not open a browser",
|
|
1420
1776
|
" --foreground run in this terminal instead of in the background",
|
|
1421
1777
|
"",
|
|
1778
|
+
"capabilities options:",
|
|
1779
|
+
" --enable <ids> enable the comma-separated capability ids",
|
|
1780
|
+
" --disable <ids> disable the comma-separated capability ids",
|
|
1781
|
+
" --json print the report as JSON",
|
|
1782
|
+
"",
|
|
1422
1783
|
"update options:",
|
|
1423
1784
|
" --check report the available release without installing it",
|
|
1424
1785
|
" --yes install without asking"
|
|
@@ -1438,6 +1799,7 @@ async function runStandaloneCli(argv) {
|
|
|
1438
1799
|
if (command === "start") return await start(rest);
|
|
1439
1800
|
if (command === "stop") return stop();
|
|
1440
1801
|
if (command === "status") return status();
|
|
1802
|
+
if (command === "capabilities") return capabilities(rest);
|
|
1441
1803
|
if (command === "update") return await update(rest);
|
|
1442
1804
|
if (command === "doctor") return doctor();
|
|
1443
1805
|
console.error("unknown command: " + command);
|
|
@@ -66,9 +66,8 @@ export declare function interviewCapabilities(interactive: boolean): Promise<Cap
|
|
|
66
66
|
* The deployment env file, with one assignment per enabled capability that reaches
|
|
67
67
|
* its service through the launch environment.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
69
|
+
* Exa reads its key from the launch environment, so a capability the user selected is enabled here
|
|
70
|
+
* as well as in the profile layer. Only the names this interview owns are rewritten: a
|
|
72
71
|
* deployment that keeps its own assignments in the same file keeps them, and
|
|
73
72
|
* deselecting a capability removes the assignment that turned it on.
|
|
74
73
|
*
|
|
@@ -93,6 +92,52 @@ export declare function capabilityEnvironment(answers: CapabilityAnswers, existi
|
|
|
93
92
|
* @returns the patch layer's YAML text.
|
|
94
93
|
*/
|
|
95
94
|
export declare function capabilityPatchLayer(answers: CapabilityAnswers): string;
|
|
95
|
+
/**
|
|
96
|
+
* The capability record a previous configured run wrote, or `undefined` when none exists.
|
|
97
|
+
*
|
|
98
|
+
* The record is what lets a later run report the deployment's answers instead of asking
|
|
99
|
+
* again. A malformed or unreadable file is treated as absent rather than fatal: it is a
|
|
100
|
+
* convenience cache, and refusing to boot over it would strand a deployment whose record
|
|
101
|
+
* was corrupted by an interrupted write.
|
|
102
|
+
*
|
|
103
|
+
* @param home - the deployment home holding the record.
|
|
104
|
+
* @returns the enabled ids, and the MinerU endpoint when the record carries one.
|
|
105
|
+
*/
|
|
106
|
+
export declare function readCapabilityRecord(home: string): CapabilityAnswers | undefined;
|
|
107
|
+
/**
|
|
108
|
+
* The capabilities a deployment actually has mounted, read from the files the loader and
|
|
109
|
+
* the launch environment consume.
|
|
110
|
+
*
|
|
111
|
+
* A record is a cache of an earlier answer and can drift from the deployment — a profile
|
|
112
|
+
* layer edited by hand, a record deleted, a plugin removed from the distribution. What a
|
|
113
|
+
* capability is *doing* is decided by the profile patch rows and the env file, so that is
|
|
114
|
+
* what a report reads.
|
|
115
|
+
*
|
|
116
|
+
* @param profilePatchText - the profile layer's YAML text.
|
|
117
|
+
* @param envText - the deployment env file's text.
|
|
118
|
+
* @returns the ids found in the profile layer and the env names present.
|
|
119
|
+
*/
|
|
120
|
+
export declare function detectEnabledCapabilities(profilePatchText: string, envText: string): {
|
|
121
|
+
readonly mounted: readonly string[];
|
|
122
|
+
readonly env: readonly string[];
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* One capability's effect, added to or removed from an existing profile layer without
|
|
126
|
+
* disturbing the rest of the file.
|
|
127
|
+
*
|
|
128
|
+
* `dsh-plus start` owns the whole layer and rewrites it from the interview's answers, which
|
|
129
|
+
* is correct for an install that answered once and never edits afterwards. A deployment
|
|
130
|
+
* that maintains its own rows in that same layer cannot be served that way: replacing the
|
|
131
|
+
* file drops every row the operator wrote. This edits the rows this module owns and leaves
|
|
132
|
+
* every other entry, comment, and blank line exactly where it was.
|
|
133
|
+
*
|
|
134
|
+
* @param existing - the layer's current YAML text.
|
|
135
|
+
* @param id - the capability to add or remove.
|
|
136
|
+
* @param enable - whether to add the capability.
|
|
137
|
+
* @param mineruEndpoint - the endpoint to write when enabling MinerU.
|
|
138
|
+
* @returns the layer's new text.
|
|
139
|
+
*/
|
|
140
|
+
export declare function editCapabilityPatchLayer(existing: string, id: string, enable: boolean, mineruEndpoint?: string): string;
|
|
96
141
|
/**
|
|
97
142
|
* Install and start the backing services the selected capabilities need.
|
|
98
143
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
/** Capabilities offered at install time, in interview order. */
|
|
5
5
|
export const CAPABILITIES = [
|
|
@@ -96,15 +96,21 @@ export async function interviewCapabilities(interactive) {
|
|
|
96
96
|
...mineruEndpoint === undefined ? {} : { mineruEndpoint },
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Names this module owns in the deployment env file.
|
|
101
|
+
*
|
|
102
|
+
* Only names the loader accepts here: a `DSH_`-prefixed variable is refused in any `.env` because
|
|
103
|
+
* `DSH_HOME` is itself bootstrap-only and the file could otherwise relocate the home it is read
|
|
104
|
+
* from. MinerU's endpoint therefore travels in the profile layer, which is also where the plugin
|
|
105
|
+
* reads it.
|
|
106
|
+
*/
|
|
107
|
+
const CAPABILITY_ENV_NAMES = ['EXA_API_KEY'];
|
|
101
108
|
/**
|
|
102
109
|
* The deployment env file, with one assignment per enabled capability that reaches
|
|
103
110
|
* its service through the launch environment.
|
|
104
111
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
112
|
+
* Exa reads its key from the launch environment, so a capability the user selected is enabled here
|
|
113
|
+
* as well as in the profile layer. Only the names this interview owns are rewritten: a
|
|
108
114
|
* deployment that keeps its own assignments in the same file keeps them, and
|
|
109
115
|
* deselecting a capability removes the assignment that turned it on.
|
|
110
116
|
*
|
|
@@ -115,6 +121,17 @@ const CAPABILITY_ENV_NAMES = ['DSH_MINERU_ENDPOINT', 'EXA_API_KEY'];
|
|
|
115
121
|
export function capabilityEnvironment(answers, existing = '') {
|
|
116
122
|
const enabled = new Set(answers.enabled);
|
|
117
123
|
const owned = new Set(CAPABILITY_ENV_NAMES);
|
|
124
|
+
// Values the file already carries for the names this module owns. Re-running a
|
|
125
|
+
// configured start must not delete a credential the deployment already holds: the
|
|
126
|
+
// interview returns no key when the user says they will add it later, and enabling an
|
|
127
|
+
// unrelated capability still rewrites this whole file. Reading the present value back
|
|
128
|
+
// is what keeps `start` from undoing an earlier answer.
|
|
129
|
+
const carried = new Map();
|
|
130
|
+
for (const line of existing.split('\n')) {
|
|
131
|
+
const parsed = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(.*)$/.exec(line);
|
|
132
|
+
if (parsed?.[1] !== undefined && owned.has(parsed[1]))
|
|
133
|
+
carried.set(parsed[1], parsed[2] ?? '');
|
|
134
|
+
}
|
|
118
135
|
const kept = existing.split('\n').filter((line) => {
|
|
119
136
|
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
120
137
|
return line.trim() !== '' && !line.trimStart().startsWith('#') && (name === undefined || !owned.has(name));
|
|
@@ -123,11 +140,10 @@ export function capabilityEnvironment(answers, existing = '') {
|
|
|
123
140
|
'# Written by dsh-plus start from the capability interview. Enabling or',
|
|
124
141
|
'# disabling a capability rewrites the lines below; other lines are kept.',
|
|
125
142
|
];
|
|
126
|
-
if (enabled.has('
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
written.push('EXA_API_KEY=' + answers.exaApiKey);
|
|
143
|
+
if (enabled.has('exa')) {
|
|
144
|
+
const key = answers.exaApiKey ?? carried.get('EXA_API_KEY');
|
|
145
|
+
if (key !== undefined && key !== '')
|
|
146
|
+
written.push('EXA_API_KEY=' + key);
|
|
131
147
|
}
|
|
132
148
|
return [...written, ...kept].join('\n') + '\n';
|
|
133
149
|
}
|
|
@@ -155,6 +171,20 @@ export function capabilityPatchLayer(answers) {
|
|
|
155
171
|
if (enabled.has('computer-use')) {
|
|
156
172
|
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'");
|
|
157
173
|
}
|
|
174
|
+
if (enabled.has('officecli')) {
|
|
175
|
+
// The plugin ships inside the mounted bundle and carries its own binary, so the
|
|
176
|
+
// capability needs no host step — but a selection that writes no row mounts nothing,
|
|
177
|
+
// and the capability then reports as ready while the tools stay absent. The row id
|
|
178
|
+
// and package match the plugin's own bundle layer.
|
|
179
|
+
rows.push(' - id: officecli', " name: '@sparkelf/dsh-officecli'");
|
|
180
|
+
}
|
|
181
|
+
// MinerU reads its endpoint from plugin config, not from the environment, and a name prefixed
|
|
182
|
+
// `DSH_` is refused in any .env file because `DSH_HOME` is itself bootstrap-only — the file could
|
|
183
|
+
// otherwise relocate the home it is read from. Writing the endpoint as a profile row is both the
|
|
184
|
+
// plugin's own contract and the only place the loader accepts it.
|
|
185
|
+
if (enabled.has('mineru')) {
|
|
186
|
+
rows.push(' - id: mineru', " name: '@sparkelf/dsh-mineru'", ' config:', ' endpoint: ' + (answers.mineruEndpoint ?? DEFAULT_MINERU_ENDPOINT));
|
|
187
|
+
}
|
|
158
188
|
const lines = [
|
|
159
189
|
'# ' + CAPABILITY_MARKER + ' Enabling or disabling a capability',
|
|
160
190
|
'# rewrites this file; edits here are replaced.',
|
|
@@ -179,6 +209,266 @@ function runStep(command, args) {
|
|
|
179
209
|
const result = spawnSync(command, [...args], { stdio: 'inherit' });
|
|
180
210
|
return result.status === 0;
|
|
181
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* The capability record a previous configured run wrote, or `undefined` when none exists.
|
|
214
|
+
*
|
|
215
|
+
* The record is what lets a later run report the deployment's answers instead of asking
|
|
216
|
+
* again. A malformed or unreadable file is treated as absent rather than fatal: it is a
|
|
217
|
+
* convenience cache, and refusing to boot over it would strand a deployment whose record
|
|
218
|
+
* was corrupted by an interrupted write.
|
|
219
|
+
*
|
|
220
|
+
* @param home - the deployment home holding the record.
|
|
221
|
+
* @returns the enabled ids, and the MinerU endpoint when the record carries one.
|
|
222
|
+
*/
|
|
223
|
+
export function readCapabilityRecord(home) {
|
|
224
|
+
const path = join(home, CAPABILITY_RECORD);
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Absent and unreadable are the same answer here: the caller falls back to the
|
|
231
|
+
// profile and env files, which are the authoritative state a record only mirrors.
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
235
|
+
return undefined;
|
|
236
|
+
const record = parsed;
|
|
237
|
+
if (!Array.isArray(record.enabled))
|
|
238
|
+
return undefined;
|
|
239
|
+
const enabled = record.enabled.filter((id) => typeof id === 'string');
|
|
240
|
+
return {
|
|
241
|
+
enabled,
|
|
242
|
+
...typeof record.mineruEndpoint === 'string' ? { mineruEndpoint: record.mineruEndpoint } : {},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* The capabilities a deployment actually has mounted, read from the files the loader and
|
|
247
|
+
* the launch environment consume.
|
|
248
|
+
*
|
|
249
|
+
* A record is a cache of an earlier answer and can drift from the deployment — a profile
|
|
250
|
+
* layer edited by hand, a record deleted, a plugin removed from the distribution. What a
|
|
251
|
+
* capability is *doing* is decided by the profile patch rows and the env file, so that is
|
|
252
|
+
* what a report reads.
|
|
253
|
+
*
|
|
254
|
+
* @param profilePatchText - the profile layer's YAML text.
|
|
255
|
+
* @param envText - the deployment env file's text.
|
|
256
|
+
* @returns the ids found in the profile layer and the env names present.
|
|
257
|
+
*/
|
|
258
|
+
export function detectEnabledCapabilities(profilePatchText, envText) {
|
|
259
|
+
const mounted = [];
|
|
260
|
+
// Matching the row id is what the loader does; matching a bare package name would also
|
|
261
|
+
// hit a comment or a name mentioned in an unrelated row's config.
|
|
262
|
+
if (/^\s*- id: web-search-exa\s*$/mu.test(profilePatchText))
|
|
263
|
+
mounted.push('exa');
|
|
264
|
+
if (/^\s*- id: mineru\s*$/mu.test(profilePatchText))
|
|
265
|
+
mounted.push('mineru');
|
|
266
|
+
if (/^\s*- id: officecli\s*$/mu.test(profilePatchText))
|
|
267
|
+
mounted.push('officecli');
|
|
268
|
+
if (/^\s*- id: computer-use\s*$/mu.test(profilePatchText))
|
|
269
|
+
mounted.push('computer-use');
|
|
270
|
+
const env = [];
|
|
271
|
+
for (const line of envText.split('\n')) {
|
|
272
|
+
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
273
|
+
if (name !== undefined && !env.includes(name))
|
|
274
|
+
env.push(name);
|
|
275
|
+
}
|
|
276
|
+
return { mounted, env };
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* The patch row one capability mounts, as a list of YAML lines, or `undefined` when the
|
|
280
|
+
* capability is turned on by something other than a row this module owns.
|
|
281
|
+
*
|
|
282
|
+
* MinerU is enabled by a row too, but its endpoint is per-deployment, so the caller passes
|
|
283
|
+
* the answers rather than a fixed row.
|
|
284
|
+
*
|
|
285
|
+
* @param id - the capability id.
|
|
286
|
+
* @returns the insert payload lines, without the surrounding `- insert:` entry.
|
|
287
|
+
*/
|
|
288
|
+
function capabilityRows(id) {
|
|
289
|
+
if (id === 'exa') {
|
|
290
|
+
return [
|
|
291
|
+
' - id: web-search-exa',
|
|
292
|
+
" name: '@deepseek-ai/dsh-web-search-exa'",
|
|
293
|
+
' config:',
|
|
294
|
+
' searchType: auto',
|
|
295
|
+
' numResults: 8',
|
|
296
|
+
];
|
|
297
|
+
}
|
|
298
|
+
if (id === 'officecli') {
|
|
299
|
+
return [
|
|
300
|
+
' - id: officecli',
|
|
301
|
+
" name: '@sparkelf/dsh-officecli'",
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
if (id === 'computer-use') {
|
|
305
|
+
return [
|
|
306
|
+
' - id: computer-use',
|
|
307
|
+
" name: '@deepseek-ai/dsh-computer-use'",
|
|
308
|
+
' - id: computer-use-cua-driver-mcp',
|
|
309
|
+
" name: '@deepseek-ai/dsh-experimental-computer-use-cua-driver-mcp'",
|
|
310
|
+
];
|
|
311
|
+
}
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
314
|
+
/** The row ids a capability owns in the profile layer. */
|
|
315
|
+
function capabilityRowIds(id) {
|
|
316
|
+
if (id === 'exa')
|
|
317
|
+
return ['web-search-exa'];
|
|
318
|
+
if (id === 'officecli')
|
|
319
|
+
return ['officecli'];
|
|
320
|
+
if (id === 'mineru')
|
|
321
|
+
return ['mineru'];
|
|
322
|
+
if (id === 'computer-use')
|
|
323
|
+
return ['computer-use', 'computer-use-cua-driver-mcp'];
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* One capability's effect, added to or removed from an existing profile layer without
|
|
328
|
+
* disturbing the rest of the file.
|
|
329
|
+
*
|
|
330
|
+
* `dsh-plus start` owns the whole layer and rewrites it from the interview's answers, which
|
|
331
|
+
* is correct for an install that answered once and never edits afterwards. A deployment
|
|
332
|
+
* that maintains its own rows in that same layer cannot be served that way: replacing the
|
|
333
|
+
* file drops every row the operator wrote. This edits the rows this module owns and leaves
|
|
334
|
+
* every other entry, comment, and blank line exactly where it was.
|
|
335
|
+
*
|
|
336
|
+
* @param existing - the layer's current YAML text.
|
|
337
|
+
* @param id - the capability to add or remove.
|
|
338
|
+
* @param enable - whether to add the capability.
|
|
339
|
+
* @param mineruEndpoint - the endpoint to write when enabling MinerU.
|
|
340
|
+
* @returns the layer's new text.
|
|
341
|
+
*/
|
|
342
|
+
export function editCapabilityPatchLayer(existing, id, enable, mineruEndpoint) {
|
|
343
|
+
const owned = capabilityRowIds(id);
|
|
344
|
+
const withoutRows = dropOwnedRows(existing, owned, id === 'exa');
|
|
345
|
+
const mineruRows = [
|
|
346
|
+
' - id: mineru',
|
|
347
|
+
" name: '@sparkelf/dsh-mineru'",
|
|
348
|
+
' config:',
|
|
349
|
+
' endpoint: ' + (mineruEndpoint ?? DEFAULT_MINERU_ENDPOINT),
|
|
350
|
+
];
|
|
351
|
+
const rows = id === 'mineru' && enable ? mineruRows : capabilityRows(id);
|
|
352
|
+
const text = enable && rows !== undefined
|
|
353
|
+
? insertCapabilityRows(withoutRows, rows, id === 'exa')
|
|
354
|
+
: withoutRows.text;
|
|
355
|
+
// Every file this repository writes ends with exactly one newline; an edited layer must
|
|
356
|
+
// not lose that, and the loader's YAML parse is indifferent to it either way.
|
|
357
|
+
const trimmed = text.replace(/\n+$/u, '');
|
|
358
|
+
return trimmed === '' ? '\n' : trimmed + '\n';
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Remove the rows one capability owns, and for Exa the `web` selector row that routes
|
|
362
|
+
* search to it.
|
|
363
|
+
*
|
|
364
|
+
* A row is dropped with the block that belongs to it: the `- id:` line plus its indented
|
|
365
|
+
* continuation lines, and any comment line directly above that documented it. Comment and
|
|
366
|
+
* blank lines separating it from the next entry stay, so the surrounding file keeps its
|
|
367
|
+
* shape.
|
|
368
|
+
*
|
|
369
|
+
* @param existing - the layer's current YAML text.
|
|
370
|
+
* @param ids - the row ids this capability owns.
|
|
371
|
+
* @param withSelector - whether the capability also owns the shared `web` selector row.
|
|
372
|
+
* @returns the remaining text and whether anything was actually removed.
|
|
373
|
+
*/
|
|
374
|
+
function dropOwnedRows(existing, ids, withSelector) {
|
|
375
|
+
const lines = existing.split('\n');
|
|
376
|
+
const out = [];
|
|
377
|
+
let removed = false;
|
|
378
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
379
|
+
const line = lines[index] ?? '';
|
|
380
|
+
const match = /^(\s*)- id:\s*(\S+)\s*$/.exec(line);
|
|
381
|
+
// The `web` selector is shared with the built-in provider: dropping it turns Exa off
|
|
382
|
+
// without touching the row's other keys, because the bundle layer's row reapplies.
|
|
383
|
+
const isSelector = withSelector && match?.[2] === 'web' && match[1] === '';
|
|
384
|
+
if (match === null || (!ids.includes(match[2] ?? '') && !isSelector)) {
|
|
385
|
+
out.push(line);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
removed = true;
|
|
389
|
+
const indent = match[1] ?? '';
|
|
390
|
+
// Drop the comments that documented this row, stopping at a blank line so an unrelated
|
|
391
|
+
// comment above a blank line is not consumed.
|
|
392
|
+
while (out.length > 0) {
|
|
393
|
+
const previous = out[out.length - 1] ?? '';
|
|
394
|
+
if (previous.trimStart().startsWith('#') && previous.trim() !== '')
|
|
395
|
+
out.pop();
|
|
396
|
+
else
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
// Then the row's own continuation lines: deeper-indented or blank lines until the next
|
|
400
|
+
// entry at the same or a shallower level.
|
|
401
|
+
for (index += 1; index < lines.length; index += 1) {
|
|
402
|
+
const next = lines[index] ?? '';
|
|
403
|
+
if (next.trim() === '') {
|
|
404
|
+
// A blank line inside a row's block keeps it; a blank line starting a new block
|
|
405
|
+
// belongs to the file's spacing and goes back.
|
|
406
|
+
const after = lines[index + 1] ?? '';
|
|
407
|
+
if (after !== '' && (after.startsWith(indent + ' ') || after.startsWith(indent + '\t') || after.trimStart().startsWith('#'))) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
index -= 1;
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
const nextIndent = /^\s*/.exec(next)?.[0] ?? '';
|
|
414
|
+
if (nextIndent.length > indent.length)
|
|
415
|
+
continue;
|
|
416
|
+
index -= 1;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return { text: out.join('\n'), removed };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Add a capability's rows to the layer, creating the `- insert:` entry when the file has
|
|
424
|
+
* none and appending to the existing one otherwise.
|
|
425
|
+
*
|
|
426
|
+
* The loader indexes inserted rows under the layer that inserted them, so appending to an
|
|
427
|
+
* existing `- insert:` list and adding a second one both work; appending keeps the file
|
|
428
|
+
* readable instead of growing one `- insert:` per capability.
|
|
429
|
+
*
|
|
430
|
+
* @param state - the layer with this capability's own rows already removed.
|
|
431
|
+
* @param rows - the insert payload lines to add.
|
|
432
|
+
* @param withSelector - whether Exa's `web` selector row must also be present.
|
|
433
|
+
* @returns the layer's new text.
|
|
434
|
+
*/
|
|
435
|
+
function insertCapabilityRows(state, rows, withSelector) {
|
|
436
|
+
const lines = state.text.split('\n');
|
|
437
|
+
const insertIndex = lines.findIndex(line => /^- insert:\s*$/.test(line));
|
|
438
|
+
if (insertIndex >= 0) {
|
|
439
|
+
// Append at the end of the existing insert block: walk past its payload lines.
|
|
440
|
+
let end = insertIndex + 1;
|
|
441
|
+
while (end < lines.length) {
|
|
442
|
+
const line = lines[end] ?? '';
|
|
443
|
+
if (line.trim() === '' || /^\s/.test(line))
|
|
444
|
+
end += 1;
|
|
445
|
+
else
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
while (end > insertIndex + 1 && (lines[end - 1] ?? '').trim() === '')
|
|
449
|
+
end -= 1;
|
|
450
|
+
lines.splice(end, 0, ...rows);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// A layer with no entries is the literal `[]`; replacing it keeps one valid array.
|
|
454
|
+
const emptyIndex = lines.findIndex(line => line.trim() === '[]');
|
|
455
|
+
if (emptyIndex >= 0)
|
|
456
|
+
lines.splice(emptyIndex, 1, '- insert:', ...rows);
|
|
457
|
+
else {
|
|
458
|
+
const trailing = lines.length > 0 && (lines[lines.length - 1] ?? '') === '' ? lines.pop() ?? '' : undefined;
|
|
459
|
+
lines.push('- insert:', ...rows);
|
|
460
|
+
if (trailing !== undefined)
|
|
461
|
+
lines.push(trailing);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (withSelector && !/^\s*- id: web\s*$/mu.test(lines.join('\n'))) {
|
|
465
|
+
// The selector is a top-level row: it patches the bundle layer's own `web` entry, and
|
|
466
|
+
// it must restate `fetchProvider` because a patch replaces the whole `config` object
|
|
467
|
+
// rather than merging into it.
|
|
468
|
+
lines.push('', '# Route web_search through Exa rather than the built-in provider.', '- id: web', ' config:', ' searchProvider: exa', ' fetchProvider: http');
|
|
469
|
+
}
|
|
470
|
+
return lines.join('\n');
|
|
471
|
+
}
|
|
182
472
|
/**
|
|
183
473
|
* Install and start the backing services the selected capabilities need.
|
|
184
474
|
*
|
|
@@ -13,7 +13,7 @@ import { createRequire } from 'node:module';
|
|
|
13
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 { CAPABILITIES, CAPABILITY_ENV_FILE, CAPABILITY_MARKER, CAPABILITY_RECORD, DEFAULT_MINERU_ENDPOINT, capabilityEnvironment, capabilityPatchLayer, installCapabilityServices, interviewCapabilities, } from "./standalone-capabilities.js";
|
|
16
|
+
import { CAPABILITIES, CAPABILITY_ENV_FILE, CAPABILITY_MARKER, CAPABILITY_RECORD, DEFAULT_MINERU_ENDPOINT, capabilityEnvironment, capabilityPatchLayer, detectEnabledCapabilities, editCapabilityPatchLayer, installCapabilityServices, interviewCapabilities, readCapabilityRecord, } 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. */
|
|
@@ -414,6 +414,140 @@ function confirm(question) {
|
|
|
414
414
|
});
|
|
415
415
|
});
|
|
416
416
|
}
|
|
417
|
+
/**
|
|
418
|
+
* Report and change which optional capabilities this deployment has enabled.
|
|
419
|
+
*
|
|
420
|
+
* The interview runs once, at install time, and a deployment that answered it keeps its
|
|
421
|
+
* answers — so this is the only way to see what it chose or to change one answer later.
|
|
422
|
+
* The report is read from the profile layer and the env file rather than from the record,
|
|
423
|
+
* because those two are what the loader and the launch environment actually consume: a
|
|
424
|
+
* record deleted, or a layer edited by hand, would otherwise describe a deployment that
|
|
425
|
+
* does not exist.
|
|
426
|
+
*
|
|
427
|
+
* `--enable` and `--disable` edit only the rows this module owns. The profile layer is
|
|
428
|
+
* shared with rows an operator wrote themselves, so it is never replaced wholesale.
|
|
429
|
+
*
|
|
430
|
+
* @param argv - arguments after the command word.
|
|
431
|
+
* @returns the process exit code.
|
|
432
|
+
*/
|
|
433
|
+
function capabilities(argv) {
|
|
434
|
+
const anchor = installationAnchor();
|
|
435
|
+
const paths = resolvePaths(anchor);
|
|
436
|
+
const patchPath = join(paths.profileDirectory, CAPABILITY_PATCH_FILE);
|
|
437
|
+
const envPath = join(paths.home, CAPABILITY_ENV_FILE);
|
|
438
|
+
const patchText = existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : '';
|
|
439
|
+
const envText = existsSync(envPath) ? readFileSync(envPath, 'utf8') : '';
|
|
440
|
+
const actual = detectEnabledCapabilities(patchText, envText);
|
|
441
|
+
const record = readCapabilityRecord(paths.home);
|
|
442
|
+
let enable = [];
|
|
443
|
+
let disable = [];
|
|
444
|
+
let asJson = false;
|
|
445
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
446
|
+
const token = argv[index];
|
|
447
|
+
if (token === '--json') {
|
|
448
|
+
asJson = true;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (token === '--enable' || token === '--disable') {
|
|
452
|
+
const value = argv[index + 1];
|
|
453
|
+
const ids = (value ?? '').split(',').map(id => id.trim()).filter(id => id !== '');
|
|
454
|
+
if (value === undefined || ids.length === 0) {
|
|
455
|
+
console.error(token + ' requires a comma-separated list of capability ids');
|
|
456
|
+
return 1;
|
|
457
|
+
}
|
|
458
|
+
const unknown = ids.filter(id => !CAPABILITIES.some(capability => capability.id === id));
|
|
459
|
+
if (unknown.length > 0) {
|
|
460
|
+
console.error('unknown capability: ' + unknown.join(', '));
|
|
461
|
+
console.error('Known: ' + CAPABILITIES.map(capability => capability.id).join(', '));
|
|
462
|
+
return 1;
|
|
463
|
+
}
|
|
464
|
+
if (token === '--enable')
|
|
465
|
+
enable = [...new Set([...enable, ...ids])];
|
|
466
|
+
else
|
|
467
|
+
disable = [...new Set([...disable, ...ids])];
|
|
468
|
+
index += 1;
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
console.error('unknown option: ' + String(token));
|
|
472
|
+
return 1;
|
|
473
|
+
}
|
|
474
|
+
const conflicting = enable.filter(id => disable.includes(id));
|
|
475
|
+
if (conflicting.length > 0) {
|
|
476
|
+
console.error('cannot both enable and disable: ' + conflicting.join(', '));
|
|
477
|
+
return 1;
|
|
478
|
+
}
|
|
479
|
+
if (enable.length > 0 || disable.length > 0) {
|
|
480
|
+
const enabledNow = new Set(actual.mounted);
|
|
481
|
+
let nextPatch = patchText;
|
|
482
|
+
for (const id of enable) {
|
|
483
|
+
nextPatch = editCapabilityPatchLayer(nextPatch, id, true, record?.mineruEndpoint);
|
|
484
|
+
enabledNow.add(id);
|
|
485
|
+
}
|
|
486
|
+
for (const id of disable) {
|
|
487
|
+
nextPatch = editCapabilityPatchLayer(nextPatch, id, false);
|
|
488
|
+
enabledNow.delete(id);
|
|
489
|
+
}
|
|
490
|
+
if (!existsSync(patchPath) || !patchText.includes(CAPABILITY_MARKER)) {
|
|
491
|
+
// The layer belongs to the deployment, not to this command. Keep the previous text
|
|
492
|
+
// beside it so a structural mistake is recoverable without a backup.
|
|
493
|
+
const backup = patchPath + '.before-capabilities';
|
|
494
|
+
if (patchText !== '' && !existsSync(backup))
|
|
495
|
+
writeFileSync(backup, patchText);
|
|
496
|
+
}
|
|
497
|
+
writeFileSync(patchPath, nextPatch);
|
|
498
|
+
// The env file is rewritten from the answers so a disabled capability drops the
|
|
499
|
+
// assignment that turned it on, and an enabled one keeps a key already in the file.
|
|
500
|
+
const answers = {
|
|
501
|
+
enabled: [...enabledNow],
|
|
502
|
+
...record?.mineruEndpoint === undefined ? {} : { mineruEndpoint: record.mineruEndpoint },
|
|
503
|
+
};
|
|
504
|
+
const nextEnv = capabilityEnvironment(answers, envText);
|
|
505
|
+
writeFileSync(envPath, nextEnv);
|
|
506
|
+
const after = detectEnabledCapabilities(nextPatch, nextEnv);
|
|
507
|
+
console.log('Updated ' + CAPABILITY_PATCH_FILE + ' and ' + CAPABILITY_ENV_FILE + '.');
|
|
508
|
+
console.log('Run dsh-plus start (or restart the server) to apply the change.');
|
|
509
|
+
return reportCapabilities(after, record, asJson);
|
|
510
|
+
}
|
|
511
|
+
return reportCapabilities(actual, record, asJson);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Print the capability report.
|
|
515
|
+
*
|
|
516
|
+
* @param actual - the capabilities found in the profile layer and env file.
|
|
517
|
+
* @param record - what the last configured run recorded, when a record exists.
|
|
518
|
+
* @param asJson - when true, print machine-readable JSON instead of the table.
|
|
519
|
+
* @returns the process exit code, always 0.
|
|
520
|
+
*/
|
|
521
|
+
function reportCapabilities(actual, record, asJson) {
|
|
522
|
+
const rows = CAPABILITIES.map((capability) => {
|
|
523
|
+
const mounted = actual.mounted.includes(capability.id);
|
|
524
|
+
const recorded = record?.enabled.includes(capability.id);
|
|
525
|
+
return { id: capability.id, title: capability.title, enabled: mounted, recorded: recorded ?? null };
|
|
526
|
+
});
|
|
527
|
+
if (asJson) {
|
|
528
|
+
console.log(JSON.stringify({
|
|
529
|
+
capabilities: rows,
|
|
530
|
+
env: actual.env,
|
|
531
|
+
recordPresent: record !== undefined,
|
|
532
|
+
}, null, 2));
|
|
533
|
+
return 0;
|
|
534
|
+
}
|
|
535
|
+
console.log('Optional capabilities:');
|
|
536
|
+
for (const row of rows) {
|
|
537
|
+
const state = row.enabled ? 'enabled ' : 'disabled';
|
|
538
|
+
// A record that disagrees with the files is the one case worth calling out: the two
|
|
539
|
+
// are supposed to describe the same deployment.
|
|
540
|
+
const drift = row.recorded !== null && row.recorded !== row.enabled ? ' (record says ' + (row.recorded ? 'enabled' : 'disabled') + ')' : '';
|
|
541
|
+
console.log(' ' + (row.enabled ? 'ok ' : '-- ') + row.id.padEnd(14) + state + drift);
|
|
542
|
+
}
|
|
543
|
+
if (record === undefined) {
|
|
544
|
+
console.log('');
|
|
545
|
+
console.log('No ' + CAPABILITY_RECORD + ' yet; the next dsh-plus start will run the interview.');
|
|
546
|
+
}
|
|
547
|
+
console.log('');
|
|
548
|
+
console.log('Change with: dsh-plus capabilities --enable <id> | --disable <id>');
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
417
551
|
async function doctor() {
|
|
418
552
|
const anchor = installationAnchor();
|
|
419
553
|
const paths = resolvePaths(anchor);
|
|
@@ -429,6 +563,21 @@ async function doctor() {
|
|
|
429
563
|
check(existsSync(join(paths.profileDirectory, 'package.json')), 'profile at ' + paths.profileDirectory + ' (dsh-plus start creates it)');
|
|
430
564
|
const free = await portAvailable(DEFAULT_PORT);
|
|
431
565
|
check(true, 'port ' + String(DEFAULT_PORT) + (free ? ' is free' : ' is in use; start will choose the next free one'));
|
|
566
|
+
// The capability record and the files that actually enable a capability are written by
|
|
567
|
+
// different runs, so they can disagree: a layer edited by hand, or a record deleted.
|
|
568
|
+
// Reporting the drift here is what turns the record from a write-only file into
|
|
569
|
+
// something a user can trust.
|
|
570
|
+
const patchPath = join(paths.profileDirectory, CAPABILITY_PATCH_FILE);
|
|
571
|
+
const envPath = join(paths.home, CAPABILITY_ENV_FILE);
|
|
572
|
+
const actual = detectEnabledCapabilities(existsSync(patchPath) ? readFileSync(patchPath, 'utf8') : '', existsSync(envPath) ? readFileSync(envPath, 'utf8') : '');
|
|
573
|
+
const record = readCapabilityRecord(paths.home);
|
|
574
|
+
const drifted = record === undefined
|
|
575
|
+
? []
|
|
576
|
+
: CAPABILITIES.filter(capability => record.enabled.includes(capability.id) !== actual.mounted.includes(capability.id));
|
|
577
|
+
check(drifted.length === 0, drifted.length === 0
|
|
578
|
+
? 'capabilities: ' + (actual.mounted.length === 0 ? '(none enabled)' : actual.mounted.join(', '))
|
|
579
|
+
: 'capabilities disagree with ' + CAPABILITY_RECORD + ': ' + drifted.map(capability => capability.id).join(', ')
|
|
580
|
+
+ ' (run dsh-plus capabilities)');
|
|
432
581
|
console.log(failures === 0 ? 'No problems found.' : String(failures) + ' problem(s) found.');
|
|
433
582
|
return failures === 0 ? 0 : 1;
|
|
434
583
|
}
|
|
@@ -436,11 +585,12 @@ const USAGE = [
|
|
|
436
585
|
'Usage: dsh-plus <command> [options]',
|
|
437
586
|
'',
|
|
438
587
|
'Commands:',
|
|
439
|
-
' start
|
|
440
|
-
' stop
|
|
441
|
-
' status
|
|
442
|
-
'
|
|
443
|
-
'
|
|
588
|
+
' start start the server (first run creates the profile)',
|
|
589
|
+
' stop stop the server',
|
|
590
|
+
' status report whether the server is running',
|
|
591
|
+
' capabilities report or change the optional capabilities',
|
|
592
|
+
' update move to a newer distribution release',
|
|
593
|
+
' doctor check this installation',
|
|
444
594
|
'',
|
|
445
595
|
'start options:',
|
|
446
596
|
' --port <n> port to prefer (default ' + String(DEFAULT_PORT) + '; a taken port moves to the next free one)',
|
|
@@ -448,6 +598,11 @@ const USAGE = [
|
|
|
448
598
|
' --no-open do not open a browser',
|
|
449
599
|
' --foreground run in this terminal instead of in the background',
|
|
450
600
|
'',
|
|
601
|
+
'capabilities options:',
|
|
602
|
+
' --enable <ids> enable the comma-separated capability ids',
|
|
603
|
+
' --disable <ids> disable the comma-separated capability ids',
|
|
604
|
+
' --json print the report as JSON',
|
|
605
|
+
'',
|
|
451
606
|
'update options:',
|
|
452
607
|
' --check report the available release without installing it',
|
|
453
608
|
' --yes install without asking',
|
|
@@ -470,6 +625,8 @@ export async function runStandaloneCli(argv) {
|
|
|
470
625
|
return stop();
|
|
471
626
|
if (command === 'status')
|
|
472
627
|
return status();
|
|
628
|
+
if (command === 'capabilities')
|
|
629
|
+
return capabilities(rest);
|
|
473
630
|
if (command === 'update')
|
|
474
631
|
return await update(rest);
|
|
475
632
|
if (command === 'doctor')
|
package/package.json
CHANGED
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
"dshPlus": {
|
|
49
49
|
"compatibility": {
|
|
50
|
-
"dsh": ">=0.
|
|
50
|
+
"dsh": ">=0.2.0-rc.1"
|
|
51
51
|
},
|
|
52
52
|
"formatVersion": 1,
|
|
53
53
|
"patchPackages": [
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
],
|
|
107
107
|
"dependencies": {
|
|
108
108
|
"@changfenhuang/dsh-genui": "0.11.1",
|
|
109
|
-
"@deepseek-ai/dsh-experimental-agent-team-profile": ">=0.
|
|
109
|
+
"@deepseek-ai/dsh-experimental-agent-team-profile": ">=0.2.0-rc.1",
|
|
110
110
|
"@huanlin/dsh-plugin-better-locale": "0.4.3",
|
|
111
111
|
"@sparkelf/dsh-api-client": "0.5.2",
|
|
112
112
|
"@sparkelf/dsh-mineru": "0.1.3",
|
|
@@ -122,33 +122,33 @@
|
|
|
122
122
|
"dsh-video-preview": "0.1.4"
|
|
123
123
|
},
|
|
124
124
|
"overrides": {
|
|
125
|
-
"@deepseek-ai/dsh-agent-presets": "npm:@sparkelf/dsh-agent-presets@0.
|
|
126
|
-
"@deepseek-ai/dsh-api-gateway": "npm:@sparkelf/dsh-api-gateway@0.
|
|
127
|
-
"@deepseek-ai/dsh-api-session-controller": "npm:@sparkelf/dsh-api-session-controller@0.
|
|
128
|
-
"@deepseek-ai/dsh-api-terminal-controller": "npm:@sparkelf/dsh-api-terminal-controller@0.
|
|
129
|
-
"@deepseek-ai/dsh-client-connection": "npm:@sparkelf/dsh-client-connection@0.
|
|
130
|
-
"@deepseek-ai/dsh-client-ui-agent-preset": "npm:@sparkelf/dsh-client-ui-agent-preset@0.
|
|
131
|
-
"@deepseek-ai/dsh-client-ui-deliverables": "npm:@sparkelf/dsh-client-ui-deliverables@0.
|
|
132
|
-
"@deepseek-ai/dsh-client-ui-layout": "npm:@sparkelf/dsh-client-ui-layout@0.
|
|
133
|
-
"@deepseek-ai/dsh-client-ui-model-selection": "npm:@sparkelf/dsh-client-ui-model-selection@0.
|
|
134
|
-
"@deepseek-ai/dsh-client-ui-permission-presets": "npm:@sparkelf/dsh-client-ui-permission-presets@0.
|
|
135
|
-
"@deepseek-ai/dsh-client-ui-primitives": "npm:@sparkelf/dsh-client-ui-primitives@0.
|
|
136
|
-
"@deepseek-ai/dsh-client-ui-settings-models": "npm:@sparkelf/dsh-client-ui-settings-models@0.
|
|
137
|
-
"@deepseek-ai/dsh-client-ui-trajectory": "npm:@sparkelf/dsh-client-ui-trajectory@0.
|
|
138
|
-
"@deepseek-ai/dsh-host-frontend-static": "npm:@sparkelf/dsh-host-frontend-static@0.
|
|
139
|
-
"@deepseek-ai/dsh-host-open-in-app": "npm:@sparkelf/dsh-host-open-in-app@0.
|
|
140
|
-
"@deepseek-ai/dsh-host-webserver": "npm:@sparkelf/dsh-host-webserver@0.
|
|
141
|
-
"@deepseek-ai/dsh-llm": "npm:@sparkelf/dsh-llm@0.
|
|
142
|
-
"@deepseek-ai/dsh-llm-pi-ai": "npm:@sparkelf/dsh-llm-pi-ai@0.
|
|
143
|
-
"@deepseek-ai/dsh-native-command": "npm:@sparkelf/dsh-native-command@0.
|
|
144
|
-
"@deepseek-ai/dsh-session-format-v0-to-v1": "npm:@sparkelf/dsh-session-format-v0-to-v1@0.
|
|
145
|
-
"@deepseek-ai/dsh-session-log-export": "npm:@sparkelf/dsh-session-log-export@0.
|
|
146
|
-
"@deepseek-ai/dsh-session-query-sqlite": "npm:@sparkelf/dsh-session-query-sqlite@0.
|
|
147
|
-
"@deepseek-ai/dsh-subagent": "npm:@sparkelf/dsh-subagent@0.
|
|
148
|
-
"@deepseek-ai/dsh-tools": "npm:@sparkelf/dsh-tools@0.
|
|
149
|
-
"@deepseek-ai/dsh-web-app": "npm:@sparkelf/dsh-web-app@0.
|
|
150
|
-
"@deepseek-ai/dsh-web-frontend": "npm:@sparkelf/dsh-web-frontend@0.
|
|
151
|
-
"@deepseek-ai/dsh-workspace": "npm:@sparkelf/dsh-workspace@0.
|
|
125
|
+
"@deepseek-ai/dsh-agent-presets": "npm:@sparkelf/dsh-agent-presets@0.2.0-rc.1",
|
|
126
|
+
"@deepseek-ai/dsh-api-gateway": "npm:@sparkelf/dsh-api-gateway@0.2.0-rc.1",
|
|
127
|
+
"@deepseek-ai/dsh-api-session-controller": "npm:@sparkelf/dsh-api-session-controller@0.2.0-rc.1",
|
|
128
|
+
"@deepseek-ai/dsh-api-terminal-controller": "npm:@sparkelf/dsh-api-terminal-controller@0.2.0-rc.1",
|
|
129
|
+
"@deepseek-ai/dsh-client-connection": "npm:@sparkelf/dsh-client-connection@0.2.0-rc.1",
|
|
130
|
+
"@deepseek-ai/dsh-client-ui-agent-preset": "npm:@sparkelf/dsh-client-ui-agent-preset@0.2.0-rc.1",
|
|
131
|
+
"@deepseek-ai/dsh-client-ui-deliverables": "npm:@sparkelf/dsh-client-ui-deliverables@0.2.0-rc.1",
|
|
132
|
+
"@deepseek-ai/dsh-client-ui-layout": "npm:@sparkelf/dsh-client-ui-layout@0.2.0-rc.1",
|
|
133
|
+
"@deepseek-ai/dsh-client-ui-model-selection": "npm:@sparkelf/dsh-client-ui-model-selection@0.2.0-rc.1",
|
|
134
|
+
"@deepseek-ai/dsh-client-ui-permission-presets": "npm:@sparkelf/dsh-client-ui-permission-presets@0.2.0-rc.1",
|
|
135
|
+
"@deepseek-ai/dsh-client-ui-primitives": "npm:@sparkelf/dsh-client-ui-primitives@0.2.0-rc.1",
|
|
136
|
+
"@deepseek-ai/dsh-client-ui-settings-models": "npm:@sparkelf/dsh-client-ui-settings-models@0.2.0-rc.1",
|
|
137
|
+
"@deepseek-ai/dsh-client-ui-trajectory": "npm:@sparkelf/dsh-client-ui-trajectory@0.2.0-rc.1",
|
|
138
|
+
"@deepseek-ai/dsh-host-frontend-static": "npm:@sparkelf/dsh-host-frontend-static@0.2.0-rc.1",
|
|
139
|
+
"@deepseek-ai/dsh-host-open-in-app": "npm:@sparkelf/dsh-host-open-in-app@0.2.0-rc.1",
|
|
140
|
+
"@deepseek-ai/dsh-host-webserver": "npm:@sparkelf/dsh-host-webserver@0.2.0-rc.1",
|
|
141
|
+
"@deepseek-ai/dsh-llm": "npm:@sparkelf/dsh-llm@0.2.0-rc.1",
|
|
142
|
+
"@deepseek-ai/dsh-llm-pi-ai": "npm:@sparkelf/dsh-llm-pi-ai@0.2.0-rc.1",
|
|
143
|
+
"@deepseek-ai/dsh-native-command": "npm:@sparkelf/dsh-native-command@0.2.0-rc.1",
|
|
144
|
+
"@deepseek-ai/dsh-session-format-v0-to-v1": "npm:@sparkelf/dsh-session-format-v0-to-v1@0.2.0-rc.1",
|
|
145
|
+
"@deepseek-ai/dsh-session-log-export": "npm:@sparkelf/dsh-session-log-export@0.2.0-rc.1",
|
|
146
|
+
"@deepseek-ai/dsh-session-query-sqlite": "npm:@sparkelf/dsh-session-query-sqlite@0.2.0-rc.1",
|
|
147
|
+
"@deepseek-ai/dsh-subagent": "npm:@sparkelf/dsh-subagent@0.2.0-rc.1",
|
|
148
|
+
"@deepseek-ai/dsh-tools": "npm:@sparkelf/dsh-tools@0.2.0-rc.1",
|
|
149
|
+
"@deepseek-ai/dsh-web-app": "npm:@sparkelf/dsh-web-app@0.2.0-rc.1",
|
|
150
|
+
"@deepseek-ai/dsh-web-frontend": "npm:@sparkelf/dsh-web-frontend@0.2.0-rc.1",
|
|
151
|
+
"@deepseek-ai/dsh-workspace": "npm:@sparkelf/dsh-workspace@0.2.0-rc.1"
|
|
152
152
|
},
|
|
153
153
|
"standaloneVariants": {
|
|
154
154
|
"dataops": {
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"@sparkelf/dsh-query-result-analysis"
|
|
165
165
|
],
|
|
166
166
|
"includePackages": {
|
|
167
|
-
"@sparkelf/dsh-dataops-managed": "0.3.
|
|
167
|
+
"@sparkelf/dsh-dataops-managed": "0.3.4",
|
|
168
168
|
"@sparkelf/dsh-query-result-analysis": "0.1.3"
|
|
169
169
|
},
|
|
170
170
|
"packageName": "@sparkelf/dsh-dataops-standalone",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
}
|
|
174
174
|
},
|
|
175
175
|
"sourceBase": {
|
|
176
|
-
"revision": "
|
|
176
|
+
"revision": "4878cdabd87d4041bdaff61d04c966883b9fd07a"
|
|
177
177
|
}
|
|
178
178
|
},
|
|
179
179
|
"exports": {
|
|
@@ -217,5 +217,5 @@
|
|
|
217
217
|
},
|
|
218
218
|
"type": "module",
|
|
219
219
|
"types": "lib/types/index.d.ts",
|
|
220
|
-
"version": "0.2.0-rc.
|
|
220
|
+
"version": "0.2.0-rc.37"
|
|
221
221
|
}
|