dsh-update-status 0.2.1 → 0.3.0

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.
@@ -0,0 +1,634 @@
1
+ import { createRequire } from "node:module";
2
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { createServer } from "node:net";
6
+ import { spawnSync } from "node:child_process";
7
+ //#region src/service/plan.ts
8
+ /** Build transparent per-OS user-service definitions; do not install them here. */
9
+ const SERVICE_LABEL = "com.idoall.dsh-update-status.web";
10
+ const SUPERVISOR_MARKER = "dsh-update-status";
11
+ function xml(value) {
12
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
13
+ }
14
+ function systemd(value) {
15
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
16
+ }
17
+ function powerShell(value) {
18
+ return value.replace(/'/g, "''");
19
+ }
20
+ function boundedPort(value) {
21
+ const port = typeof value === "number" ? value : Number(value);
22
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("`--port` must be an integer from 1 to 65535");
23
+ return port;
24
+ }
25
+ function valueAfter(args, flag) {
26
+ const index = args.indexOf(flag);
27
+ if (index < 0) return void 0;
28
+ const value = args[index + 1];
29
+ if (value === void 0 || value.startsWith("--")) throw new Error(`missing value after ${flag}`);
30
+ return value;
31
+ }
32
+ /**
33
+ * Resolve the real JS file behind `@deepseek-ai/dsh`'s `bin.dsh`, using the
34
+ * selected profile as a Node resolution anchor. This avoids platform shell
35
+ * shims (`dsh.cmd`) and keeps every native service on `node <absolute-js>`.
36
+ */
37
+ function dshPathOf(env, profile, dshHome, override) {
38
+ const fromArg = override?.trim();
39
+ if (fromArg !== void 0 && fromArg !== "") return isAbsolute(fromArg) ? fromArg : resolve(env.cwd, fromArg);
40
+ const home = dshHome ?? join(env.home || homedir(), ".dsh");
41
+ try {
42
+ const anchor = join(home, "profiles", profile, "dsh-update-status-service-resolver.cjs");
43
+ const manifestPath = createRequire(anchor).resolve("@deepseek-ai/dsh/package.json");
44
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
45
+ if (manifest.name !== "@deepseek-ai/dsh" || typeof manifest.bin?.dsh !== "string") throw new Error("invalid dsh package manifest");
46
+ return realpathSync(join(dirname(manifestPath), manifest.bin.dsh));
47
+ } catch (error) {
48
+ throw new Error(`cannot resolve @deepseek-ai/dsh from profile ${profile}; rerun with --dsh /absolute/path/to/lib/bin.js (${error instanceof Error ? error.message : String(error)})`);
49
+ }
50
+ }
51
+ function servicePlatform(value) {
52
+ if (value === "darwin" || value === "linux" || value === "win32") return value;
53
+ throw new Error(`unsupported platform: ${value}; supported: macOS, Linux, Windows`);
54
+ }
55
+ function specFromArgs(args, env) {
56
+ const platform = servicePlatform(env.platform);
57
+ const profile = valueAfter(args, "--profile") ?? env.env.DSH_PROFILE ?? "web";
58
+ const workspace = valueAfter(args, "--workspace");
59
+ if (workspace === void 0) throw new Error("`--workspace` is required and must be the absolute directory DSH should use");
60
+ const host = valueAfter(args, "--host") ?? "127.0.0.1";
61
+ const port = boundedPort(valueAfter(args, "--port") ?? 3080);
62
+ const home = env.home || homedir();
63
+ const dshHome = valueAfter(args, "--dsh-home") ?? (env.env.DSH_HOME?.trim() || void 0);
64
+ if (dshHome !== void 0 && !isAbsolute(dshHome)) throw new Error("`--dsh-home` must be an absolute path");
65
+ const logDir = valueAfter(args, "--log-dir") ?? join(dshHome ?? join(home, ".dsh"), "logs");
66
+ const dshPath = dshPathOf(env, profile, dshHome, valueAfter(args, "--dsh"));
67
+ const nodePath = valueAfter(args, "--node") ?? env.execPath;
68
+ if (!isAbsolute(nodePath)) throw new Error("`--node` must be an absolute executable path");
69
+ if (!isAbsolute(dshPath)) throw new Error("`--dsh` must be an absolute executable path");
70
+ if (!isAbsolute(workspace)) throw new Error("`--workspace` must be an absolute path");
71
+ if (!isAbsolute(logDir)) throw new Error("`--log-dir` must be an absolute path");
72
+ return {
73
+ schemaVersion: 1,
74
+ platform,
75
+ label: SERVICE_LABEL,
76
+ nodePath,
77
+ dshPath,
78
+ profile,
79
+ workspace,
80
+ host,
81
+ port,
82
+ home,
83
+ ...dshHome === void 0 ? {} : { dshHome },
84
+ logDir,
85
+ supervisorMarker: SUPERVISOR_MARKER
86
+ };
87
+ }
88
+ function pathsFor(spec) {
89
+ const root = spec.dshHome ?? join(spec.home, ".dsh");
90
+ const stdoutFile = join(spec.logDir, "dsh-web-service.stdout.log");
91
+ const stderrFile = join(spec.logDir, "dsh-web-service.stderr.log");
92
+ if (spec.platform === "darwin") return {
93
+ stateFile: join(root, "dsh-update-status-service.json"),
94
+ definitionFile: join(spec.home, "Library", "LaunchAgents", `${spec.label}.plist`),
95
+ stdoutFile,
96
+ stderrFile
97
+ };
98
+ if (spec.platform === "linux") return {
99
+ stateFile: join(root, "dsh-update-status-service.json"),
100
+ definitionFile: join(spec.home, ".config", "systemd", "user", "dsh-update-status-web.service"),
101
+ stdoutFile,
102
+ stderrFile
103
+ };
104
+ const base = envLocalAppData(spec);
105
+ return {
106
+ stateFile: join(root, "dsh-update-status-service.json"),
107
+ definitionFile: join(base, "dsh-update-status", "dsh-web-task.xml"),
108
+ wrapperFile: join(base, "dsh-update-status", "dsh-web-supervisor.ps1"),
109
+ stdoutFile,
110
+ stderrFile
111
+ };
112
+ }
113
+ function envLocalAppData(spec) {
114
+ return join(spec.home, "AppData", "Local");
115
+ }
116
+ function webArguments(spec) {
117
+ return [
118
+ spec.dshPath,
119
+ "--profile",
120
+ spec.profile,
121
+ "--host",
122
+ spec.host,
123
+ "--port",
124
+ String(spec.port),
125
+ "--no-open"
126
+ ];
127
+ }
128
+ function launchdDefinition(spec, paths) {
129
+ const args = [spec.nodePath, ...webArguments(spec)].map((value) => ` <string>${xml(value)}</string>`).join("\n");
130
+ const envXml = [
131
+ ["HOME", spec.home],
132
+ ["PATH", `${dirname(spec.nodePath)}:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin`],
133
+ ["DSH_WEB_SUPERVISOR", spec.supervisorMarker],
134
+ ...spec.dshHome === void 0 ? [] : [["DSH_HOME", spec.dshHome]]
135
+ ].map(([key, value]) => ` <key>${xml(key)}</key>\n <string>${xml(value)}</string>`).join("\n");
136
+ return `<?xml version="1.0" encoding="UTF-8"?>
137
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
138
+ <plist version="1.0">
139
+ <dict>
140
+ <key>Label</key>
141
+ <string>${xml(spec.label)}</string>
142
+ <key>ProgramArguments</key>
143
+ <array>
144
+ ${args}
145
+ </array>
146
+ <key>WorkingDirectory</key>
147
+ <string>${xml(spec.workspace)}</string>
148
+ <key>RunAtLoad</key>
149
+ <true/>
150
+ <key>KeepAlive</key>
151
+ <true/>
152
+ <key>ThrottleInterval</key>
153
+ <integer>3</integer>
154
+ <key>EnvironmentVariables</key>
155
+ <dict>
156
+ ${envXml}
157
+ </dict>
158
+ <key>StandardOutPath</key>
159
+ <string>${xml(paths.stdoutFile)}</string>
160
+ <key>StandardErrorPath</key>
161
+ <string>${xml(paths.stderrFile)}</string>
162
+ </dict>
163
+ </plist>
164
+ `;
165
+ }
166
+ function systemdDefinition(spec) {
167
+ const command = [spec.nodePath, ...webArguments(spec)].map((value) => `"${systemd(value)}"`).join(" ");
168
+ const env = [
169
+ `Environment="HOME=${systemd(spec.home)}"`,
170
+ `Environment="PATH=${systemd(`${dirname(spec.nodePath)}:/usr/local/bin:/usr/bin:/bin`)}"`,
171
+ `Environment="DSH_WEB_SUPERVISOR=${spec.supervisorMarker}"`,
172
+ ...spec.dshHome === void 0 ? [] : [`Environment="DSH_HOME=${systemd(spec.dshHome)}"`]
173
+ ].join("\n");
174
+ return `[Unit]
175
+ Description=DSH Web managed by dsh-update-status
176
+ After=network-online.target
177
+
178
+ [Service]
179
+ Type=simple
180
+ WorkingDirectory="${systemd(spec.workspace)}"
181
+ ${env}
182
+ ExecStart=${command}
183
+ Restart=on-failure
184
+ RestartSec=3
185
+
186
+ [Install]
187
+ WantedBy=default.target
188
+ `;
189
+ }
190
+ function windowsWrapper(spec, paths) {
191
+ const node = powerShell(spec.nodePath);
192
+ const dsh = powerShell(spec.dshPath);
193
+ const workspace = powerShell(spec.workspace);
194
+ const out = powerShell(paths.stdoutFile);
195
+ const err = powerShell(paths.stderrFile);
196
+ const home = powerShell(spec.home);
197
+ const dshHome = spec.dshHome === void 0 ? "" : `$env:DSH_HOME = '${powerShell(spec.dshHome)}'\n`;
198
+ return `$ErrorActionPreference = 'Stop'
199
+ $env:HOME = '${home}'
200
+ $env:DSH_WEB_SUPERVISOR = '${spec.supervisorMarker}'
201
+ ${dshHome}Set-Location -LiteralPath '${workspace}'
202
+ while ($true) {
203
+ & '${node}' '${dsh}' --profile '${powerShell(spec.profile)}' --host '${powerShell(spec.host)}' --port '${String(spec.port)}' --no-open 1>> '${out}' 2>> '${err}'
204
+ $exitCode = $LASTEXITCODE
205
+ Start-Sleep -Seconds 3
206
+ # The user service owns deliberate restarts (exit 42) and unexpected exits.
207
+ # Manual uninstall stops the scheduled task, which ends this wrapper.
208
+ }
209
+ `;
210
+ }
211
+ function windowsDefinition(spec, paths) {
212
+ const wrapper = paths.wrapperFile;
213
+ if (wrapper === void 0) throw new Error("Windows service plan requires a wrapper path");
214
+ return `<?xml version="1.0" encoding="UTF-8"?>
215
+ <Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
216
+ <RegistrationInfo><Description>DSH Web managed by dsh-update-status</Description></RegistrationInfo>
217
+ <Triggers><LogonTrigger><Enabled>true</Enabled></LogonTrigger></Triggers>
218
+ <Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
219
+ <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit></Settings>
220
+ <Actions Context="Author"><Exec><Command>powershell.exe</Command><Arguments>-NoProfile -ExecutionPolicy Bypass -File &quot;${xml(wrapper)}&quot;</Arguments></Exec></Actions>
221
+ </Task>
222
+ `;
223
+ }
224
+ /** Generate a service plan without writing, starting, or stopping anything. */
225
+ function planFor(spec, uid = 0) {
226
+ const paths = pathsFor(spec);
227
+ if (spec.platform === "darwin") {
228
+ const domain = `gui/${uid}`;
229
+ return {
230
+ spec,
231
+ paths,
232
+ definition: launchdDefinition(spec, paths),
233
+ installCommand: [
234
+ "launchctl",
235
+ "bootstrap",
236
+ domain,
237
+ paths.definitionFile
238
+ ],
239
+ startCommand: [
240
+ "launchctl",
241
+ "kickstart",
242
+ "-k",
243
+ `${domain}/${spec.label}`
244
+ ],
245
+ statusCommand: [
246
+ "launchctl",
247
+ "print",
248
+ `${domain}/${spec.label}`
249
+ ],
250
+ stopCommand: [
251
+ "launchctl",
252
+ "bootout",
253
+ `${domain}/${spec.label}`
254
+ ],
255
+ uninstallCommand: [
256
+ "launchctl",
257
+ "bootout",
258
+ `${domain}/${spec.label}`
259
+ ]
260
+ };
261
+ }
262
+ if (spec.platform === "linux") return {
263
+ spec,
264
+ paths,
265
+ definition: systemdDefinition(spec),
266
+ installCommand: [
267
+ "systemctl",
268
+ "--user",
269
+ "daemon-reload"
270
+ ],
271
+ startCommand: [
272
+ "systemctl",
273
+ "--user",
274
+ "enable",
275
+ "--now",
276
+ "dsh-update-status-web.service"
277
+ ],
278
+ statusCommand: [
279
+ "systemctl",
280
+ "--user",
281
+ "status",
282
+ "dsh-update-status-web.service"
283
+ ],
284
+ stopCommand: [
285
+ "systemctl",
286
+ "--user",
287
+ "disable",
288
+ "--now",
289
+ "dsh-update-status-web.service"
290
+ ],
291
+ uninstallCommand: [
292
+ "systemctl",
293
+ "--user",
294
+ "disable",
295
+ "--now",
296
+ "dsh-update-status-web.service"
297
+ ]
298
+ };
299
+ return {
300
+ spec,
301
+ paths,
302
+ definition: windowsDefinition(spec, paths),
303
+ wrapper: windowsWrapper(spec, paths),
304
+ installCommand: [
305
+ "schtasks.exe",
306
+ "/Create",
307
+ "/TN",
308
+ "DSH Update Status Web",
309
+ "/XML",
310
+ paths.definitionFile,
311
+ "/F"
312
+ ],
313
+ startCommand: [
314
+ "schtasks.exe",
315
+ "/Run",
316
+ "/TN",
317
+ "DSH Update Status Web"
318
+ ],
319
+ statusCommand: [
320
+ "schtasks.exe",
321
+ "/Query",
322
+ "/TN",
323
+ "DSH Update Status Web",
324
+ "/V",
325
+ "/FO",
326
+ "LIST"
327
+ ],
328
+ stopCommand: [
329
+ "schtasks.exe",
330
+ "/End",
331
+ "/TN",
332
+ "DSH Update Status Web"
333
+ ],
334
+ uninstallCommand: [
335
+ "schtasks.exe",
336
+ "/Delete",
337
+ "/TN",
338
+ "DSH Update Status Web",
339
+ "/F"
340
+ ]
341
+ };
342
+ }
343
+ //#endregion
344
+ //#region src/service/cli.ts
345
+ /** Explicit, cross-platform user-service installer CLI. Never runs at plugin mount. */
346
+ const RECEIPT_VERSION = 1;
347
+ function environment() {
348
+ return {
349
+ platform: process.platform,
350
+ home: process.env.HOME ?? process.env.USERPROFILE ?? "",
351
+ cwd: process.cwd(),
352
+ execPath: process.execPath,
353
+ env: process.env,
354
+ path: process.env.PATH,
355
+ ...typeof process.getuid === "function" ? { uid: process.getuid() } : {}
356
+ };
357
+ }
358
+ function runner(file, args) {
359
+ const result = spawnSync(file, [...args], {
360
+ encoding: "utf8",
361
+ windowsHide: true
362
+ });
363
+ return {
364
+ status: result.status,
365
+ stdout: result.stdout ?? "",
366
+ stderr: result.stderr ?? "",
367
+ ...result.error === void 0 ? {} : { error: result.error.message }
368
+ };
369
+ }
370
+ function usage() {
371
+ return `Usage: dsh-update-status-service <plan|install|activate|status|stop|uninstall> [options]
372
+
373
+ Create a current-user DSH Web supervisor. This CLI never runs automatically.
374
+
375
+ Options:
376
+ --dry-run Print the expanded plan without writing or invoking OS services
377
+ --profile <name> DSH profile (default: DSH_PROFILE or web)
378
+ --workspace <path> Required absolute working directory for DSH
379
+ --dsh-home <path> Optional absolute DSH home (default: DSH_HOME or ~/.dsh)
380
+ --host <127.0.0.1|0.0.0.0> Bind host (default: 127.0.0.1)
381
+ --port <1..65535> Web port (default: 3080)
382
+ --node <path> Absolute Node executable (default: current Node)
383
+ --dsh <path> Absolute dsh executable (default: resolve from PATH)
384
+ --log-dir <path> Absolute log directory (default: $DSH_HOME/logs)
385
+
386
+ Workflow: plan --dry-run → install → stop the terminal-started DSH yourself → activate.
387
+ `;
388
+ }
389
+ function assertSafeText(value, description) {
390
+ if (/[\u0000\r\n]/.test(value)) throw new Error(`${description} must not contain NUL or line breaks`);
391
+ }
392
+ function assertReadableRegular(path, description) {
393
+ assertSafeText(path, description);
394
+ if (!existsSync(path)) throw new Error(`${description} does not exist: ${path}`);
395
+ try {
396
+ if (!statSync(path).isFile()) throw new Error(`${description} is not a regular file: ${path}`);
397
+ accessSync(path, constants.R_OK);
398
+ } catch (error) {
399
+ if (error instanceof Error && error.message.startsWith(description)) throw error;
400
+ throw new Error(`${description} is not readable: ${path}`);
401
+ }
402
+ }
403
+ function assertExecutable(path, description) {
404
+ assertReadableRegular(path, description);
405
+ if (process.platform !== "win32") try {
406
+ accessSync(path, constants.X_OK);
407
+ } catch {
408
+ throw new Error(`${description} is not executable: ${path}`);
409
+ }
410
+ }
411
+ function validateSpec(spec) {
412
+ for (const [description, value] of [
413
+ ["profile", spec.profile],
414
+ ["workspace", spec.workspace],
415
+ ["home", spec.home],
416
+ ["log directory", spec.logDir],
417
+ ["host", spec.host]
418
+ ]) assertSafeText(value, description);
419
+ if (spec.host !== "127.0.0.1" && spec.host !== "0.0.0.0") throw new Error("`--host` must be 127.0.0.1 or 0.0.0.0");
420
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(spec.profile)) throw new Error("`--profile` must contain only letters, numbers, dot, underscore, or hyphen");
421
+ assertExecutable(spec.nodePath, "Node executable");
422
+ assertReadableRegular(spec.dshPath, "dsh executable");
423
+ assertSafeText(spec.workspace, "workspace");
424
+ if (!existsSync(spec.workspace) || !statSync(spec.workspace).isDirectory()) throw new Error(`workspace is not a directory: ${spec.workspace}`);
425
+ }
426
+ function atomicWrite(path, content) {
427
+ mkdirSync(dirname(path), {
428
+ recursive: true,
429
+ mode: 448
430
+ });
431
+ const temporary = `${path}.${String(process.pid)}.tmp`;
432
+ writeFileSync(temporary, content, {
433
+ encoding: "utf8",
434
+ mode: 384
435
+ });
436
+ renameSync(temporary, path);
437
+ }
438
+ function ensureManagedTarget(path, expected) {
439
+ if (!existsSync(path)) return;
440
+ let current;
441
+ try {
442
+ current = readFileSync(path, "utf8");
443
+ } catch {
444
+ throw new Error(`refusing to replace unreadable existing file: ${path}`);
445
+ }
446
+ if (current !== expected) throw new Error(`refusing to overwrite an existing unmanaged service artifact: ${path}`);
447
+ }
448
+ function readReceipt(path) {
449
+ try {
450
+ const value = JSON.parse(readFileSync(path, "utf8"));
451
+ if (value.version !== RECEIPT_VERSION || value.managedBy !== "dsh-update-status" || value.spec === void 0 || value.paths === void 0) throw new Error("not a dsh-update-status service receipt");
452
+ return value;
453
+ } catch (error) {
454
+ throw new Error(`cannot read managed service receipt ${path}: ${error instanceof Error ? error.message : String(error)}`);
455
+ }
456
+ }
457
+ async function portIsFree(host, port) {
458
+ return await new Promise((resolve) => {
459
+ const server = createServer();
460
+ server.once("error", () => resolve(false));
461
+ server.listen(port, host, () => server.close((error) => resolve(error === void 0)));
462
+ });
463
+ }
464
+ function serialisePlan(plan) {
465
+ return JSON.stringify({
466
+ platform: plan.spec.platform,
467
+ label: plan.spec.label,
468
+ nodePath: plan.spec.nodePath,
469
+ dshPath: plan.spec.dshPath,
470
+ profile: plan.spec.profile,
471
+ workspace: plan.spec.workspace,
472
+ host: plan.spec.host,
473
+ port: plan.spec.port,
474
+ definitionFile: plan.paths.definitionFile,
475
+ wrapperFile: plan.paths.wrapperFile,
476
+ stateFile: plan.paths.stateFile,
477
+ logs: {
478
+ stdout: plan.paths.stdoutFile,
479
+ stderr: plan.paths.stderrFile
480
+ },
481
+ commands: {
482
+ install: plan.installCommand,
483
+ start: plan.startCommand,
484
+ status: plan.statusCommand,
485
+ stop: plan.stopCommand,
486
+ uninstall: plan.uninstallCommand
487
+ }
488
+ }, null, 2) + "\n";
489
+ }
490
+ function commandsForActivate(plan, uid) {
491
+ if (plan.spec.platform === "darwin") return [[
492
+ "launchctl",
493
+ "bootstrap",
494
+ `gui/${uid}`,
495
+ plan.paths.definitionFile
496
+ ]];
497
+ if (plan.spec.platform === "linux") return [[
498
+ "systemctl",
499
+ "--user",
500
+ "daemon-reload"
501
+ ], [
502
+ "systemctl",
503
+ "--user",
504
+ "enable",
505
+ "--now",
506
+ "dsh-update-status-web.service"
507
+ ]];
508
+ return [[
509
+ "schtasks.exe",
510
+ "/Create",
511
+ "/TN",
512
+ "DSH Update Status Web",
513
+ "/XML",
514
+ plan.paths.definitionFile,
515
+ "/F"
516
+ ], [
517
+ "schtasks.exe",
518
+ "/Run",
519
+ "/TN",
520
+ "DSH Update Status Web"
521
+ ]];
522
+ }
523
+ function invoke(command, run) {
524
+ const [file, ...args] = command;
525
+ if (file === void 0) throw new Error("empty service command");
526
+ const result = run(file, args);
527
+ if (result.status !== 0) throw new Error(`${command.join(" ")} failed (${String(result.status)}): ${(result.stderr || result.stdout || result.error || "no diagnostic").trim()}`);
528
+ }
529
+ function preflight(spec) {
530
+ const result = spawnSync(spec.nodePath, [
531
+ spec.dshPath,
532
+ "--profile",
533
+ spec.profile,
534
+ "--dump-config"
535
+ ], {
536
+ cwd: spec.workspace,
537
+ encoding: "utf8",
538
+ timeout: 9e4,
539
+ windowsHide: true,
540
+ env: {
541
+ HOME: spec.home,
542
+ ...spec.dshHome === void 0 ? {} : { DSH_HOME: spec.dshHome },
543
+ PATH: `${dirname(spec.nodePath)}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`
544
+ }
545
+ });
546
+ if (result.status !== 0) {
547
+ const diagnostic = (result.stderr || result.stdout || result.error?.message || "no diagnostic").trim().slice(0, 800);
548
+ throw new Error(`DSH boot-free preflight failed; no service definition was written: ${diagnostic}`);
549
+ }
550
+ }
551
+ async function main(argv = process.argv.slice(2), env = environment(), run = runner, hooks = {}) {
552
+ const [action, ...rawArgs] = argv;
553
+ if (action === void 0 || action === "--help" || action === "-h") {
554
+ process.stdout.write(usage());
555
+ return action === void 0 ? 2 : 0;
556
+ }
557
+ if (![
558
+ "plan",
559
+ "install",
560
+ "activate",
561
+ "status",
562
+ "stop",
563
+ "uninstall"
564
+ ].includes(action)) {
565
+ process.stderr.write(`Unknown command: ${action}\n${usage()}`);
566
+ return 2;
567
+ }
568
+ const dryRun = rawArgs.includes("--dry-run") || action === "plan";
569
+ const args = rawArgs.filter((value) => value !== "--dry-run");
570
+ try {
571
+ const spec = specFromArgs(args, env);
572
+ validateSpec(spec);
573
+ const plan = planFor(spec, env.uid ?? 0);
574
+ if (dryRun) {
575
+ process.stdout.write(serialisePlan(plan));
576
+ return 0;
577
+ }
578
+ if (action === "install") {
579
+ (hooks.preflight ?? preflight)(spec);
580
+ ensureManagedTarget(plan.paths.definitionFile, plan.definition);
581
+ if (plan.wrapper !== void 0 && plan.paths.wrapperFile !== void 0) ensureManagedTarget(plan.paths.wrapperFile, plan.wrapper);
582
+ const receipt = {
583
+ version: RECEIPT_VERSION,
584
+ managedBy: "dsh-update-status",
585
+ spec,
586
+ paths: plan.paths
587
+ };
588
+ const receiptText = JSON.stringify(receipt, null, 2) + "\n";
589
+ ensureManagedTarget(plan.paths.stateFile, receiptText);
590
+ mkdirSync(spec.logDir, {
591
+ recursive: true,
592
+ mode: 448
593
+ });
594
+ atomicWrite(plan.paths.definitionFile, plan.definition);
595
+ if (plan.wrapper !== void 0 && plan.paths.wrapperFile !== void 0) atomicWrite(plan.paths.wrapperFile, plan.wrapper);
596
+ atomicWrite(plan.paths.stateFile, receiptText);
597
+ process.stdout.write(`Staged user service definition: ${plan.paths.definitionFile}\nNo service has started. Stop your terminal-started DSH, then run activate.\n`);
598
+ return 0;
599
+ }
600
+ const receipt = readReceipt(plan.paths.stateFile);
601
+ if (action === "activate") {
602
+ if (!await portIsFree(receipt.spec.host, receipt.spec.port)) throw new Error(`port ${receipt.spec.host}:${String(receipt.spec.port)} is in use; stop the existing DSH yourself before activate. Nothing was killed.`);
603
+ for (const command of commandsForActivate(plan, env.uid ?? 0)) invoke(command, run);
604
+ process.stdout.write("User service activated. Wait for DSH to become ready, then open the existing Web URL.\n");
605
+ return 0;
606
+ }
607
+ if (action === "status") {
608
+ const result = run(plan.statusCommand[0], plan.statusCommand.slice(1));
609
+ process.stdout.write(serialisePlan(plan));
610
+ process.stdout.write(`Native status (${plan.statusCommand.join(" ")}):\n${result.stdout || result.stderr || result.error || `exit ${String(result.status)}`}\n`);
611
+ return result.status === 0 ? 0 : 1;
612
+ }
613
+ if (action === "stop") {
614
+ invoke(plan.stopCommand, run);
615
+ process.stdout.write("User service stopped. Its definition and receipt remain; run activate to start it again.\n");
616
+ return 0;
617
+ }
618
+ const result = run(plan.uninstallCommand[0], plan.uninstallCommand.slice(1));
619
+ if (result.status !== 0) process.stderr.write(`Warning: ${result.stderr || result.stdout || "service was not loaded"}\n`);
620
+ rmSync(receipt.paths.definitionFile, { force: true });
621
+ if (receipt.paths.wrapperFile !== void 0) rmSync(receipt.paths.wrapperFile, { force: true });
622
+ rmSync(receipt.paths.stateFile, { force: true });
623
+ process.stdout.write("Removed the managed user-service definition. Logs were preserved.\n");
624
+ return 0;
625
+ } catch (error) {
626
+ process.stderr.write(`dsh-update-status-service: ${error instanceof Error ? error.message : String(error)}\n`);
627
+ return 1;
628
+ }
629
+ }
630
+ if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) main().then((code) => {
631
+ process.exitCode = code;
632
+ });
633
+ //#endregion
634
+ export { main };
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "dsh-update-status",
3
- "version": "0.2.1",
4
- "description": "Read-only DeepSeek Harness Web plugin that shows the running DSH version, discovers latest/next/alpha releases, warns about compatibility, and generates copy-only upgrade commands.",
3
+ "version": "0.3.0",
4
+ "description": "DeepSeek Harness Web plugin for version status, copy-only upgrade guidance, and safe restart when an explicit user-level supervisor is installed.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
8
+ "bin": {
9
+ "dsh-update-status-service": "./bin/dsh-update-status-service.mjs"
10
+ },
8
11
  "exports": {
9
12
  ".": {
10
13
  "types": "./lib/index.d.ts",
@@ -18,14 +21,18 @@
18
21
  "lib/index.d.ts",
19
22
  "lib/client.js",
20
23
  "lib/client.js.map",
24
+ "lib/service-cli.js",
25
+ "bin/dsh-update-status-service.mjs",
21
26
  "cordis.patch.yml",
22
27
  "README.md",
23
28
  "README.zh.md",
24
29
  "CHANGELOG.md",
25
30
  "LICENSE",
26
31
  "assets/update-panel.png",
32
+ "assets/update-panel.zh.png",
27
33
  "assets/sidebar-chip.png",
28
- "docs/RELEASING.md"
34
+ "docs/RELEASING.md",
35
+ "docs/service-supervision.md"
29
36
  ],
30
37
  "engines": {
31
38
  "node": ">=20"