@sparkelf/dsh-plus 0.2.0-rc.2 → 0.2.0-rc.20
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 +23 -11
- package/lib/bin.js +469 -20
- package/lib/types/bin.js +14 -0
- package/lib/types/standalone-capabilities.d.ts +109 -0
- package/lib/types/standalone-capabilities.js +332 -0
- package/lib/types/standalone-cli.js +136 -14
- package/lib/types/standalone-profile.d.ts +45 -1
- package/lib/types/standalone-profile.js +84 -5
- package/lib/types/standalone-server.js +2 -2
- package/package.json +77 -28
package/lib/bin.js
CHANGED
|
@@ -75,6 +75,297 @@ function newerVersion(name, installed, registry = "https://registry.npmjs.org")
|
|
|
75
75
|
return publishedVersions(name, registry).filter((entry) => semver.gt(entry.version, installed)).at(-1);
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
+
//#region lib/types/standalone-capabilities.js
|
|
79
|
+
/** Capabilities offered at install time, in interview order. */
|
|
80
|
+
const CAPABILITIES = [
|
|
81
|
+
{
|
|
82
|
+
id: "exa",
|
|
83
|
+
title: "Web search (Exa)",
|
|
84
|
+
detail: "Search the web through Exa instead of the built-in provider; needs an API key",
|
|
85
|
+
needsEndpoint: false
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
id: "mineru",
|
|
89
|
+
title: "PDF parsing (MinerU)",
|
|
90
|
+
detail: "Parse uploaded PDFs locally; the service is installed and started here",
|
|
91
|
+
needsEndpoint: true
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "officecli",
|
|
95
|
+
title: "Office documents (OfficeCLI)",
|
|
96
|
+
detail: "Create and open DOCX, XLSX, and PPTX deliverables",
|
|
97
|
+
needsEndpoint: false
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "computer-use",
|
|
101
|
+
title: "Windows desktop control (computer-use)",
|
|
102
|
+
detail: "Drive the Windows desktop from WSL through the cua driver",
|
|
103
|
+
needsEndpoint: false
|
|
104
|
+
}
|
|
105
|
+
];
|
|
106
|
+
/** Where this command starts MinerU when the capability is enabled. */
|
|
107
|
+
const DEFAULT_MINERU_ENDPOINT = "http://127.0.0.1:8000/file_parse";
|
|
108
|
+
/** File under the deployment home recording which capabilities were enabled. */
|
|
109
|
+
const CAPABILITY_RECORD = "capabilities.json";
|
|
110
|
+
/** Deployment env file the launcher reads as its `user-env` layer. */
|
|
111
|
+
const CAPABILITY_ENV_FILE = ".env";
|
|
112
|
+
/** One prompt on the terminal, resolving the trimmed line the user typed. */
|
|
113
|
+
function ask(question) {
|
|
114
|
+
return new Promise((resolveAnswer) => {
|
|
115
|
+
process.stdout.write(question);
|
|
116
|
+
process.stdin.once("data", (chunk) => {
|
|
117
|
+
resolveAnswer(String(chunk).trim());
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** Ask one yes/no question, defaulting to yes. */
|
|
122
|
+
async function confirmDefaultYes(question) {
|
|
123
|
+
const answer = (await ask(question + " [Y/n] ")).toLowerCase();
|
|
124
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Interview the user about the optional capabilities.
|
|
128
|
+
*
|
|
129
|
+
* Each is offered on its own line and defaults to enabled, so pressing Enter
|
|
130
|
+
* through the interview produces a deployment whose features work. The answers are
|
|
131
|
+
* returned rather than applied, so a caller can report what it will do before it
|
|
132
|
+
* touches the profile.
|
|
133
|
+
*
|
|
134
|
+
* @param interactive - when false, every capability is enabled without asking.
|
|
135
|
+
* @returns the ids to enable and the values the enabled ones need.
|
|
136
|
+
*/
|
|
137
|
+
async function interviewCapabilities(interactive) {
|
|
138
|
+
if (!interactive) return { enabled: CAPABILITIES.map((capability) => capability.id) };
|
|
139
|
+
console.log("");
|
|
140
|
+
console.log("Optional capabilities. Press Enter to accept the default (all enabled),");
|
|
141
|
+
console.log("or answer n for any you do not want.");
|
|
142
|
+
console.log("");
|
|
143
|
+
const enabled = [];
|
|
144
|
+
for (const capability of CAPABILITIES) if (await confirmDefaultYes(" Enable " + capability.title + "? (" + capability.detail + ")")) enabled.push(capability.id);
|
|
145
|
+
let exaApiKey;
|
|
146
|
+
if (enabled.includes("exa")) {
|
|
147
|
+
console.log("");
|
|
148
|
+
console.log(" Exa needs an API key. Create one at https://dashboard.exa.ai/api-keys");
|
|
149
|
+
const typed = await ask(" Exa API key (leave empty to add it later): ");
|
|
150
|
+
if (typed !== "") exaApiKey = typed;
|
|
151
|
+
}
|
|
152
|
+
let mineruEndpoint;
|
|
153
|
+
if (enabled.includes("mineru")) {
|
|
154
|
+
const typed = await ask(" MinerU endpoint [http://127.0.0.1:8000/file_parse]: ");
|
|
155
|
+
mineruEndpoint = typed === "" ? DEFAULT_MINERU_ENDPOINT : typed;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
enabled,
|
|
159
|
+
...exaApiKey === void 0 ? {} : { exaApiKey },
|
|
160
|
+
...mineruEndpoint === void 0 ? {} : { mineruEndpoint }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Names this module owns in the deployment env file. */
|
|
164
|
+
const CAPABILITY_ENV_NAMES = ["DSH_MINERU_ENDPOINT", "EXA_API_KEY"];
|
|
165
|
+
/**
|
|
166
|
+
* The deployment env file, with one assignment per enabled capability that reaches
|
|
167
|
+
* its service through the launch environment.
|
|
168
|
+
*
|
|
169
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
170
|
+
* the launch environment, so a capability the user selected is enabled here as well
|
|
171
|
+
* as in the profile layer. Only the names this interview owns are rewritten: a
|
|
172
|
+
* deployment that keeps its own assignments in the same file keeps them, and
|
|
173
|
+
* deselecting a capability removes the assignment that turned it on.
|
|
174
|
+
*
|
|
175
|
+
* @param answers - the interview's answers.
|
|
176
|
+
* @param existing - the file's current text, when it already exists.
|
|
177
|
+
* @returns the environment file's text.
|
|
178
|
+
*/
|
|
179
|
+
function capabilityEnvironment(answers, existing = "") {
|
|
180
|
+
const enabled = new Set(answers.enabled);
|
|
181
|
+
const owned = new Set(CAPABILITY_ENV_NAMES);
|
|
182
|
+
const kept = existing.split("\n").filter((line) => {
|
|
183
|
+
const name = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)?.[1];
|
|
184
|
+
return line.trim() !== "" && !line.trimStart().startsWith("#") && (name === void 0 || !owned.has(name));
|
|
185
|
+
});
|
|
186
|
+
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("mineru")) written.push("DSH_MINERU_ENDPOINT=" + (answers.mineruEndpoint ?? "http://127.0.0.1:8000/file_parse"));
|
|
188
|
+
if (enabled.has("exa") && answers.exaApiKey !== void 0) written.push("EXA_API_KEY=" + answers.exaApiKey);
|
|
189
|
+
return [...written, ...kept].join("\n") + "\n";
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The profile patch layer that turns the selected capabilities on.
|
|
193
|
+
*
|
|
194
|
+
* The file is data the profile's loader merges, so enabling a capability writes a
|
|
195
|
+
* row rather than editing the deployment. An empty selection still writes the file,
|
|
196
|
+
* because a layer that exists and mounts nothing is how a previously enabled
|
|
197
|
+
* capability is turned back off.
|
|
198
|
+
*
|
|
199
|
+
* The profile's own layer is a separate file with its own name: a deployment that
|
|
200
|
+
* configured something by hand keeps it, and the capability rows are rewritten
|
|
201
|
+
* whole on every start.
|
|
202
|
+
*
|
|
203
|
+
* @param answers - the interview's answers.
|
|
204
|
+
* @returns the patch layer's YAML text.
|
|
205
|
+
*/
|
|
206
|
+
function capabilityPatchLayer(answers) {
|
|
207
|
+
const enabled = new Set(answers.enabled);
|
|
208
|
+
const rows = [];
|
|
209
|
+
if (enabled.has("exa")) rows.push(" - id: web-search-exa", " name: '@deepseek-ai/dsh-web-search-exa'", " config:", " searchType: auto", " numResults: 8");
|
|
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
|
+
const lines = ["# Written by dsh-plus start from the capability interview. Enabling or disabling a capability", "# rewrites this file; edits here are replaced."];
|
|
212
|
+
if (rows.length > 0) lines.push("- insert:", ...rows);
|
|
213
|
+
else lines.push("[]");
|
|
214
|
+
if (enabled.has("exa")) lines.push("", "# Route web_search through Exa rather than the built-in provider.", "- id: web", " config:", " searchProvider: exa");
|
|
215
|
+
return lines.join("\n") + "\n";
|
|
216
|
+
}
|
|
217
|
+
/** Run one command, reporting a failure rather than throwing. */
|
|
218
|
+
function runStep(command, args) {
|
|
219
|
+
return spawnSync(command, [...args], { stdio: "inherit" }).status === 0;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Install and start the backing services the selected capabilities need.
|
|
223
|
+
*
|
|
224
|
+
* Enabling a capability means the deployment expects its service to answer, so the
|
|
225
|
+
* command that offers the choice is also the command that provides it. Each step
|
|
226
|
+
* reports what it is doing and, on failure, the command a user can run by hand —
|
|
227
|
+
* a missing runtime is a fact about the host, not a reason to abandon the install.
|
|
228
|
+
*
|
|
229
|
+
* @param answers - the interview's answers.
|
|
230
|
+
* @param home - the deployment home, where generated service units are recorded.
|
|
231
|
+
* @returns the ids whose service is ready.
|
|
232
|
+
*/
|
|
233
|
+
async function installCapabilityServices(answers, home) {
|
|
234
|
+
const enabled = new Set(answers.enabled);
|
|
235
|
+
const ready = [];
|
|
236
|
+
mkdirSync(home, { recursive: true });
|
|
237
|
+
writeFileSync(join(home, CAPABILITY_RECORD), JSON.stringify({
|
|
238
|
+
enabled: [...answers.enabled],
|
|
239
|
+
...answers.mineruEndpoint === void 0 ? {} : { mineruEndpoint: answers.mineruEndpoint }
|
|
240
|
+
}, null, 2) + "\n");
|
|
241
|
+
if (enabled.has("mineru")) {
|
|
242
|
+
console.log("");
|
|
243
|
+
console.log(" MinerU: installing the parser and starting its service...");
|
|
244
|
+
if (installMineru(home) && startMineru(home)) {
|
|
245
|
+
ready.push("mineru");
|
|
246
|
+
console.log(" MinerU: ready at " + (answers.mineruEndpoint ?? "http://127.0.0.1:8000/file_parse"));
|
|
247
|
+
} else {
|
|
248
|
+
console.log(" MinerU: not installed. Install it later with:");
|
|
249
|
+
console.log(" pip install -U \"mineru[core]\"");
|
|
250
|
+
console.log(" mineru-api --host 127.0.0.1 --port 8000");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (enabled.has("officecli")) ready.push("officecli");
|
|
254
|
+
if (enabled.has("computer-use")) if (desktopDriverAvailable()) ready.push("computer-use");
|
|
255
|
+
else {
|
|
256
|
+
console.log("");
|
|
257
|
+
console.log(" computer-use: the cua driver was not found on PATH. The plugin is");
|
|
258
|
+
console.log(" still mounted, so it starts once the driver is installed:");
|
|
259
|
+
console.log(" install the cua-driver release for this architecture, then restart");
|
|
260
|
+
}
|
|
261
|
+
return ready;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Install MinerU when it is absent and start its API service.
|
|
265
|
+
*
|
|
266
|
+
* MinerU is a Python package whose API server answers on a local port; the profile
|
|
267
|
+
* points at that port. The parser is installed into a virtual environment under the
|
|
268
|
+
* deployment home and an existing one is upgraded rather than skipped, so enabling
|
|
269
|
+
* the capability keeps the parser current without touching the system interpreter.
|
|
270
|
+
*
|
|
271
|
+
* @param home - the deployment home that owns the venv.
|
|
272
|
+
* @returns whether the package is installed; not whether it answered.
|
|
273
|
+
*/
|
|
274
|
+
function installMineru(home) {
|
|
275
|
+
const python = pythonInterpreter();
|
|
276
|
+
if (python === void 0) {
|
|
277
|
+
console.log(" MinerU: no Python interpreter found on PATH.");
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
const venv = join(home, ".mineru-venv");
|
|
281
|
+
if (!runStep(python, [
|
|
282
|
+
"-m",
|
|
283
|
+
"venv",
|
|
284
|
+
venv
|
|
285
|
+
])) return false;
|
|
286
|
+
return runStep(join(venv, "bin", "python"), [
|
|
287
|
+
"-m",
|
|
288
|
+
"pip",
|
|
289
|
+
"install",
|
|
290
|
+
"-U",
|
|
291
|
+
"mineru[all]"
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Start the MinerU API server as a background service when none answers yet.
|
|
296
|
+
*
|
|
297
|
+
* The service has to outlive the install command, so it is registered with the
|
|
298
|
+
* host's service manager when one is available and otherwise started detached.
|
|
299
|
+
*
|
|
300
|
+
* @returns whether the server is running after this call.
|
|
301
|
+
*/
|
|
302
|
+
function startMineru(home) {
|
|
303
|
+
const api = mineruApiBinary(home);
|
|
304
|
+
if (api === void 0) return false;
|
|
305
|
+
if (mineruAnswers()) return true;
|
|
306
|
+
if (spawnSync("systemctl", ["--version"], { stdio: "ignore" }).status === 0) {
|
|
307
|
+
writeFileSync("/etc/systemd/system/mineru-api.service", [
|
|
308
|
+
"[Unit]",
|
|
309
|
+
"Description=MinerU document parsing API for DeepSeek Harness Plus",
|
|
310
|
+
"After=network-online.target",
|
|
311
|
+
"Wants=network-online.target",
|
|
312
|
+
"",
|
|
313
|
+
"[Service]",
|
|
314
|
+
"Type=simple",
|
|
315
|
+
"User=root",
|
|
316
|
+
"ExecStart=" + api + " --host 127.0.0.1 --port 8000",
|
|
317
|
+
"Restart=on-failure",
|
|
318
|
+
"RestartSec=2",
|
|
319
|
+
"TimeoutStopSec=15",
|
|
320
|
+
"",
|
|
321
|
+
"[Install]",
|
|
322
|
+
"WantedBy=multi-user.target",
|
|
323
|
+
""
|
|
324
|
+
].join("\n"));
|
|
325
|
+
spawnSync("systemctl", ["daemon-reload"], { stdio: "ignore" });
|
|
326
|
+
return spawnSync("systemctl", [
|
|
327
|
+
"enable",
|
|
328
|
+
"--now",
|
|
329
|
+
"mineru-api.service"
|
|
330
|
+
], { stdio: "ignore" }).status === 0;
|
|
331
|
+
}
|
|
332
|
+
console.log(" MinerU: start the API server with:");
|
|
333
|
+
console.log(" " + api + " --host 127.0.0.1 --port 8000");
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
/** The Python interpreter to install MinerU with, preferring python3. */
|
|
337
|
+
function pythonInterpreter() {
|
|
338
|
+
return ["python3", "python"].find((candidate) => spawnSync(candidate, ["--version"], { stdio: "ignore" }).status === 0);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* The MinerU API entry point, wherever the install placed it.
|
|
342
|
+
*
|
|
343
|
+
* The venv this command creates comes first: it is the interpreter the parser was
|
|
344
|
+
* installed into, so its `mineru-api` is the one that can import MinerU. A binary on
|
|
345
|
+
* PATH is a fallback for a deployment that installed MinerU some other way.
|
|
346
|
+
*
|
|
347
|
+
* @param home - the deployment home that owns the venv.
|
|
348
|
+
* @returns the command to run, or `undefined` when none answers.
|
|
349
|
+
*/
|
|
350
|
+
function mineruApiBinary(home) {
|
|
351
|
+
return [join(home, ".mineru-venv", "bin", "mineru-api"), "mineru-api"].find((candidate) => spawnSync(candidate, ["--help"], { stdio: "ignore" }).status === 0);
|
|
352
|
+
}
|
|
353
|
+
/** Whether a MinerU API server already answers on the default port. */
|
|
354
|
+
function mineruAnswers() {
|
|
355
|
+
return spawnSync("curl", [
|
|
356
|
+
"-sf",
|
|
357
|
+
"-o",
|
|
358
|
+
"/dev/null",
|
|
359
|
+
"--max-time",
|
|
360
|
+
"3",
|
|
361
|
+
"http://127.0.0.1:8000/docs"
|
|
362
|
+
], { stdio: "ignore" }).status === 0;
|
|
363
|
+
}
|
|
364
|
+
/** Whether the Windows-side desktop driver answers from this shell. */
|
|
365
|
+
function desktopDriverAvailable() {
|
|
366
|
+
return spawnSync("cua-driver", ["--version"], { stdio: "ignore" }).status === 0;
|
|
367
|
+
}
|
|
368
|
+
//#endregion
|
|
78
369
|
//#region lib/types/standalone-server.js
|
|
79
370
|
/**
|
|
80
371
|
* Process lifecycle for one standalone Plus server.
|
|
@@ -177,8 +468,8 @@ function spawnServer(options) {
|
|
|
177
468
|
env: options.env
|
|
178
469
|
});
|
|
179
470
|
const stream = createWriteStream(options.logPath);
|
|
180
|
-
output.stdout
|
|
181
|
-
output.stderr
|
|
471
|
+
output.stdout.pipe(stream);
|
|
472
|
+
output.stderr.pipe(stream);
|
|
182
473
|
output.unref();
|
|
183
474
|
if (output.pid === void 0) throw new Error("the server process did not start");
|
|
184
475
|
return output.pid;
|
|
@@ -242,8 +533,55 @@ async function waitForAuthenticatedUrl(logPath, timeoutMilliseconds) {
|
|
|
242
533
|
* the launcher mounts exactly the bundles the profile names and nothing expands a
|
|
243
534
|
* bundle's own list.
|
|
244
535
|
*/
|
|
245
|
-
/** Profile name a standalone installation owns. */
|
|
536
|
+
/** Profile name a standalone installation owns when its package declares none. */
|
|
246
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
|
+
}
|
|
247
585
|
function requireRecord(value, label) {
|
|
248
586
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(label + " must be an object");
|
|
249
587
|
return value;
|
|
@@ -524,9 +862,12 @@ function linkBundle(consumerModules, name, source) {
|
|
|
524
862
|
/** Resolve every path a command needs, without creating anything. */
|
|
525
863
|
function resolvePaths(anchor, env = process.env) {
|
|
526
864
|
const home = resolveHome(env);
|
|
865
|
+
const declaration = readStandaloneDeclaration(anchor);
|
|
527
866
|
return {
|
|
528
867
|
home,
|
|
529
|
-
|
|
868
|
+
profileName: declaration.profileName,
|
|
869
|
+
omittedPackages: declaration.omittedPackages,
|
|
870
|
+
profileDirectory: join(home, "profiles", declaration.profileName),
|
|
530
871
|
distributionDirectory: resolveDistributionDirectory(anchor)
|
|
531
872
|
};
|
|
532
873
|
}
|
|
@@ -550,7 +891,7 @@ function ensureProfile(paths, consumerDirectory) {
|
|
|
550
891
|
const manifestPath = join(paths.profileDirectory, "package.json");
|
|
551
892
|
const distribution = readDistributionProfile(paths.distributionDirectory);
|
|
552
893
|
if (existsSync(manifestPath)) {
|
|
553
|
-
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
|
|
894
|
+
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
|
|
554
895
|
installProfilePackages(paths, consumerDirectory);
|
|
555
896
|
return false;
|
|
556
897
|
}
|
|
@@ -570,7 +911,7 @@ function ensureProfile(paths, consumerDirectory) {
|
|
|
570
911
|
} }
|
|
571
912
|
};
|
|
572
913
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
573
|
-
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds);
|
|
914
|
+
writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
|
|
574
915
|
installProfilePackages(paths, consumerDirectory);
|
|
575
916
|
return true;
|
|
576
917
|
}
|
|
@@ -584,13 +925,14 @@ function ensureProfile(paths, consumerDirectory) {
|
|
|
584
925
|
* @param profileDirectory - the standalone profile directory.
|
|
585
926
|
* @param overrides - official package name to published replacement spec.
|
|
586
927
|
*/
|
|
587
|
-
function writeProfileOverrides(profileDirectory, overrides, allowBuilds) {
|
|
928
|
+
function writeProfileOverrides(profileDirectory, overrides, allowBuilds, omittedPackages) {
|
|
588
929
|
const workspacePath = join(profileDirectory, "pnpm-workspace.yaml");
|
|
589
930
|
const document = parseDocument(existsSync(workspacePath) ? readFileSync(workspacePath, "utf8") : "");
|
|
590
931
|
const [documentError] = document.errors;
|
|
591
932
|
if (documentError !== void 0) throw new Error("Plus profile workspace is not valid YAML", { cause: documentError });
|
|
592
933
|
if (document.get("packages") === void 0) document.set("packages", ["."]);
|
|
593
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);
|
|
594
936
|
for (const [name, allowed] of Object.entries(allowBuilds)) document.setIn(["allowBuilds", name], allowed);
|
|
595
937
|
if (document.get("nodeLinker") === void 0) document.set("nodeLinker", "hoisted");
|
|
596
938
|
if (document.get("autoInstallPeers") === void 0) document.set("autoInstallPeers", false);
|
|
@@ -681,11 +1023,14 @@ function resolveInstalledPackage(from, packageName) {
|
|
|
681
1023
|
*/
|
|
682
1024
|
/** Milliseconds a start waits for the server to answer before reporting failure. */
|
|
683
1025
|
const READY_TIMEOUT_MILLISECONDS = 9e4;
|
|
1026
|
+
/** Profile patch file the capability interview rewrites (the profile's user layer). */
|
|
1027
|
+
const CAPABILITY_PATCH_FILE = "cordis.patch.yml";
|
|
684
1028
|
function parseStartOptions(argv) {
|
|
685
1029
|
let port = DEFAULT_PORT;
|
|
686
1030
|
let host = "127.0.0.1";
|
|
687
1031
|
let open = true;
|
|
688
1032
|
let foreground = false;
|
|
1033
|
+
let capabilities;
|
|
689
1034
|
for (let index = 0; index < argv.length; index += 1) {
|
|
690
1035
|
const token = argv[index];
|
|
691
1036
|
if (token === "--port" || token === "-p") {
|
|
@@ -710,13 +1055,41 @@ function parseStartOptions(argv) {
|
|
|
710
1055
|
foreground = true;
|
|
711
1056
|
continue;
|
|
712
1057
|
}
|
|
713
|
-
|
|
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
|
+
}
|
|
1068
|
+
throw new Error("unknown option: " + String(token));
|
|
714
1069
|
}
|
|
715
1070
|
return {
|
|
716
1071
|
port,
|
|
717
1072
|
host,
|
|
718
1073
|
open,
|
|
719
|
-
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 } : {}
|
|
720
1093
|
};
|
|
721
1094
|
}
|
|
722
1095
|
/**
|
|
@@ -747,16 +1120,35 @@ function installationRoot() {
|
|
|
747
1120
|
function installationAnchor() {
|
|
748
1121
|
return join(installationRoot(), "package.json");
|
|
749
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
|
+
}
|
|
750
1142
|
/** The launcher entry this installation must drive. */
|
|
751
1143
|
function launcherEntry(anchor) {
|
|
752
1144
|
return createRequire(anchor).resolve("@deepseek-ai/dsh/lib/bin.js");
|
|
753
1145
|
}
|
|
754
1146
|
/** Run the server in this process, inheriting stdio. */
|
|
755
|
-
function runForeground(entry, port, host, open) {
|
|
1147
|
+
function runForeground(entry, profileName, port, host, open) {
|
|
756
1148
|
const args = [
|
|
757
1149
|
entry,
|
|
758
1150
|
"--profile",
|
|
759
|
-
|
|
1151
|
+
profileName,
|
|
760
1152
|
"--port",
|
|
761
1153
|
String(port),
|
|
762
1154
|
"--host",
|
|
@@ -768,7 +1160,7 @@ function runForeground(entry, port, host, open) {
|
|
|
768
1160
|
async function start(argv) {
|
|
769
1161
|
const options = parseStartOptions(argv);
|
|
770
1162
|
const anchor = installationAnchor();
|
|
771
|
-
const paths = resolvePaths(
|
|
1163
|
+
const paths = resolvePaths(declarationAnchor());
|
|
772
1164
|
if (!pnpmAvailable()) {
|
|
773
1165
|
const command = pnpmInstallCommand();
|
|
774
1166
|
console.log("pnpm is required to install the plus profile, and was not found.");
|
|
@@ -799,19 +1191,29 @@ async function start(argv) {
|
|
|
799
1191
|
console.log("pnpm installed.");
|
|
800
1192
|
}
|
|
801
1193
|
const created = ensureProfile(paths, installationRoot());
|
|
802
|
-
console.log(created ? "Created the
|
|
1194
|
+
console.log(created ? "Created the " + paths.profileName + " profile at " + paths.profileDirectory : "Using the existing " + paths.profileName + " profile");
|
|
803
1195
|
for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
|
|
1196
|
+
const answers = created || !existsSync(join(paths.home, "capabilities.json")) ? options.capabilities === void 0 ? await interviewCapabilities(true) : selectCapabilities(options.capabilities) : void 0;
|
|
1197
|
+
if (answers !== void 0) {
|
|
1198
|
+
const ready = await installCapabilityServices(answers, paths.home);
|
|
1199
|
+
writeCapabilityPatch(paths.profileDirectory, answers);
|
|
1200
|
+
writeCapabilityEnvironment(paths.home, answers);
|
|
1201
|
+
console.log(" Enabled: " + (answers.enabled.length === 0 ? "(none)" : answers.enabled.join(", ")));
|
|
1202
|
+
if (answers.enabled.length > 0) console.log(" Services ready: " + (ready.length === 0 ? "(none)" : ready.join(", ")));
|
|
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.");
|
|
1204
|
+
}
|
|
804
1205
|
const entry = launcherEntry(anchor);
|
|
805
|
-
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);
|
|
806
1207
|
const existing = readState(paths.home);
|
|
807
1208
|
if (existing !== void 0) {
|
|
808
1209
|
console.log("Plus is already running at " + existing.url);
|
|
809
1210
|
console.log("Stop it with: dsh-plus stop");
|
|
810
1211
|
return 0;
|
|
811
1212
|
}
|
|
812
|
-
return startDetached(paths
|
|
1213
|
+
return startDetached(paths, entry, options);
|
|
813
1214
|
}
|
|
814
|
-
async function startDetached(
|
|
1215
|
+
async function startDetached(paths, entry, options) {
|
|
1216
|
+
const home = paths.home;
|
|
815
1217
|
const port = await choosePort(options.port, options.host);
|
|
816
1218
|
if (port === void 0) {
|
|
817
1219
|
console.error("No free port in the range " + String(options.port) + "-" + String(options.port + 9) + ".");
|
|
@@ -819,15 +1221,15 @@ async function startDetached(home, entry, options) {
|
|
|
819
1221
|
return 1;
|
|
820
1222
|
}
|
|
821
1223
|
if (port !== options.port) console.log("Port " + String(options.port) + " is in use; using " + String(port) + ".");
|
|
822
|
-
const logPath = join(stateDirectory(home), "server.log");
|
|
1224
|
+
const logPath = join(stateDirectory(paths.home), "server.log");
|
|
823
1225
|
const env = {
|
|
824
1226
|
...process.env,
|
|
825
|
-
DSH_HOME: home
|
|
1227
|
+
DSH_HOME: paths.home
|
|
826
1228
|
};
|
|
827
1229
|
const args = [
|
|
828
1230
|
entry,
|
|
829
1231
|
"--profile",
|
|
830
|
-
|
|
1232
|
+
paths.profileName,
|
|
831
1233
|
"--port",
|
|
832
1234
|
String(port),
|
|
833
1235
|
"--host",
|
|
@@ -942,6 +1344,40 @@ async function update(argv) {
|
|
|
942
1344
|
if (readState(paths.home) !== void 0) console.log("The running server still serves " + installed + "; run dsh-plus restart to load the new release.");
|
|
943
1345
|
return 0;
|
|
944
1346
|
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Write the profile's capability patch layer.
|
|
1349
|
+
*
|
|
1350
|
+
* The profile's loader merges this file, so enabling a capability is a data change
|
|
1351
|
+
* rather than an edit to the deployment. Writing it on every configured run also
|
|
1352
|
+
* turns a capability back off when the interview no longer selects it.
|
|
1353
|
+
*
|
|
1354
|
+
* @param profileDirectory - the profile whose layer is replaced.
|
|
1355
|
+
* @param answers - the interview's answers.
|
|
1356
|
+
*/
|
|
1357
|
+
function writeCapabilityPatch(profileDirectory, answers) {
|
|
1358
|
+
const path = join(profileDirectory, CAPABILITY_PATCH_FILE);
|
|
1359
|
+
const existing = existsSync(path) ? readFileSync(path, "utf8") : void 0;
|
|
1360
|
+
if (existing !== void 0 && !existing.includes("Written by dsh-plus start from the capability interview.")) {
|
|
1361
|
+
writeFileSync(path + ".before-capabilities", existing);
|
|
1362
|
+
console.log(" Kept the existing profile layer at cordis.patch.yml.before-capabilities");
|
|
1363
|
+
}
|
|
1364
|
+
writeFileSync(path, capabilityPatchLayer(answers));
|
|
1365
|
+
}
|
|
1366
|
+
/**
|
|
1367
|
+
* Write the deployment's capability environment file.
|
|
1368
|
+
*
|
|
1369
|
+
* MinerU is switched on by the presence of its endpoint and Exa reads its key from
|
|
1370
|
+
* the launch environment, so the answers reach those two through `$DSH_HOME/.env`
|
|
1371
|
+
* rather than through the profile layer. The file is replaced whole on every
|
|
1372
|
+
* configured start, which is also how a capability the user dropped stops applying.
|
|
1373
|
+
*
|
|
1374
|
+
* @param home - the deployment home whose env file the launcher reads.
|
|
1375
|
+
* @param answers - the interview's answers.
|
|
1376
|
+
*/
|
|
1377
|
+
function writeCapabilityEnvironment(home, answers) {
|
|
1378
|
+
const path = join(home, CAPABILITY_ENV_FILE);
|
|
1379
|
+
writeFileSync(path, capabilityEnvironment(answers, existsSync(path) ? readFileSync(path, "utf8") : ""));
|
|
1380
|
+
}
|
|
945
1381
|
/** Ask one yes/no question on the terminal. */
|
|
946
1382
|
function confirm(question) {
|
|
947
1383
|
return new Promise((resolveAnswer) => {
|
|
@@ -1010,6 +1446,8 @@ async function runStandaloneCli(argv) {
|
|
|
1010
1446
|
}
|
|
1011
1447
|
//#endregion
|
|
1012
1448
|
//#region lib/types/bin.js
|
|
1449
|
+
/** This package's own manifest, read beside the built entry. */
|
|
1450
|
+
const manifestPath = fileURLToPath(new URL("../package.json", import.meta.url));
|
|
1013
1451
|
/**
|
|
1014
1452
|
* Dispatch the command line.
|
|
1015
1453
|
*
|
|
@@ -1018,11 +1456,22 @@ async function runStandaloneCli(argv) {
|
|
|
1018
1456
|
* one executable for both, so a user who installed the distribution from the registry
|
|
1019
1457
|
* never needs to know which half owns a command.
|
|
1020
1458
|
*
|
|
1459
|
+
* `--version` answers with the installed distribution version. The release sequence
|
|
1460
|
+
* drives every family's entry through it to prove an installed artifact runs and reports
|
|
1461
|
+
* the version its tarball carried, so the standalone installer has to answer like the
|
|
1462
|
+
* official launcher does.
|
|
1463
|
+
*
|
|
1021
1464
|
* @returns the process exit code.
|
|
1022
1465
|
*/
|
|
1023
1466
|
async function main() {
|
|
1024
1467
|
const argv = process.argv.slice(2);
|
|
1025
|
-
|
|
1468
|
+
const first = argv[0];
|
|
1469
|
+
if (first === "--version" || first === "-v") {
|
|
1470
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1471
|
+
console.log(String(manifest.version));
|
|
1472
|
+
return 0;
|
|
1473
|
+
}
|
|
1474
|
+
if (first === "apply") {
|
|
1026
1475
|
runApply(argv);
|
|
1027
1476
|
return 0;
|
|
1028
1477
|
}
|
package/lib/types/bin.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
2
4
|
import { runApply } from "./apply.js";
|
|
3
5
|
import { runStandaloneCli } from "./standalone-cli.js";
|
|
6
|
+
/** This package's own manifest, read beside the built entry. */
|
|
7
|
+
const manifestPath = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
4
8
|
/**
|
|
5
9
|
* Dispatch the command line.
|
|
6
10
|
*
|
|
@@ -9,11 +13,21 @@ import { runStandaloneCli } from "./standalone-cli.js";
|
|
|
9
13
|
* one executable for both, so a user who installed the distribution from the registry
|
|
10
14
|
* never needs to know which half owns a command.
|
|
11
15
|
*
|
|
16
|
+
* `--version` answers with the installed distribution version. The release sequence
|
|
17
|
+
* drives every family's entry through it to prove an installed artifact runs and reports
|
|
18
|
+
* the version its tarball carried, so the standalone installer has to answer like the
|
|
19
|
+
* official launcher does.
|
|
20
|
+
*
|
|
12
21
|
* @returns the process exit code.
|
|
13
22
|
*/
|
|
14
23
|
async function main() {
|
|
15
24
|
const argv = process.argv.slice(2);
|
|
16
25
|
const first = argv[0];
|
|
26
|
+
if (first === '--version' || first === '-v') {
|
|
27
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
28
|
+
console.log(String(manifest.version));
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
17
31
|
if (first === 'apply') {
|
|
18
32
|
// `runApply` parses the command word itself, so it receives the arguments this
|
|
19
33
|
// dispatcher already inspected rather than a slice that dropped it.
|