dsh-update-status 0.4.0 → 0.4.2

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.
@@ -1,6 +1,6 @@
1
- import { a as WINDOWS_TASK_NAME, c as parseSystemdMainPid, d as bootStampFile, f as countRecentRestarts, h as serviceLogDir, i as SYSTEMD_UNIT, l as parseWindowsTaskRunning, n as specFromArgs, s as parseLaunchdTrackedPid, t as planFor, u as RESTART_STORM_WINDOW_MS } from "./plan-BornHBEZ.js";
1
+ import { _ as parseSystemdMainPid, a as planFor, b as bootStampFile, c as specFromArgs, d as servicePathOf, g as parseLaunchdTrackedPid, i as pathsFor, m as WINDOWS_TASK_NAME, n as nativeServiceCommandCandidates, o as servicePlatform, p as SYSTEMD_UNIT, r as nativeServiceCommandOf, s as serviceStateFileFromArgs, t as SERVICE_LABEL, u as normalizeServicePathExt, v as parseWindowsTaskRunning, w as serviceLogDir, x as countRecentRestarts, y as RESTART_STORM_WINDOW_MS } from "./plan-D9Ybaj-3.js";
2
2
  import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
- import { dirname } from "node:path";
3
+ import { dirname, isAbsolute } from "node:path";
4
4
  import { spawnSync } from "node:child_process";
5
5
  import { createServer } from "node:net";
6
6
  async function observeServiceState(spec, run, uid) {
@@ -103,8 +103,15 @@ Options:
103
103
  --host <127.0.0.1|0.0.0.0> Bind host (default: 127.0.0.1)
104
104
  --port <1..65535> Web port (default: 3080)
105
105
  --node <path> Absolute Node executable (default: current Node)
106
- --dsh <path> Absolute dsh executable (default: resolve from PATH)
106
+ --dsh <path> Absolute dsh executable (default: resolve from profile)
107
107
  --log-dir <path> Absolute log directory (default: $DSH_HOME/logs)
108
+ --path-source <mode> Service PATH source: current (default), minimal, explicit
109
+ --service-path <value> Explicit PATH (implies --path-source explicit)
110
+ --service-pathext <v> Windows PATHEXT override (for example .COM;.EXE;.CMD)
111
+
112
+ current snapshots this command's PATH; it never executes your shell startup files.
113
+ The final PATH, source, capture time and warnings are stored in the receipt and
114
+ reused by preflight and every later service action.
108
115
 
109
116
  Workflow: plan --dry-run → install → stop the terminal-started DSH yourself → activate.
110
117
 
@@ -114,7 +121,7 @@ makes activate fail instead of reporting success.
114
121
  `;
115
122
  }
116
123
  function assertSafeText(value, description) {
117
- if (/[\u0000\r\n]/.test(value)) throw new Error(`${description} must not contain NUL or line breaks`);
124
+ if (/[\u0000-\u0008\u000A-\u001F\u007F]/.test(value)) throw new Error(`${description} must not contain XML-illegal control characters or line breaks`);
118
125
  }
119
126
  function assertReadableRegular(path, description) {
120
127
  assertSafeText(path, description);
@@ -135,20 +142,73 @@ function assertExecutable(path, description) {
135
142
  throw new Error(`${description} is not executable: ${path}`);
136
143
  }
137
144
  }
138
- function validateSpec(spec) {
145
+ function envValue(env, key) {
146
+ return Object.entries(env).find(([name]) => name.toLowerCase() === key.toLowerCase())?.[1];
147
+ }
148
+ function validateSpec(spec, level = "launch", expectedPlatform, expectedHome, expectedSystemRoot) {
149
+ if (spec.schemaVersion !== 1) throw new Error("managed service receipt has an unsupported schema version");
150
+ if (spec.platform !== "darwin" && spec.platform !== "linux" && spec.platform !== "win32") throw new Error("managed service receipt has an invalid platform");
151
+ if (expectedPlatform !== void 0 && spec.platform !== expectedPlatform) throw new Error("managed service receipt belongs to a different platform");
152
+ if (spec.label !== SERVICE_LABEL || spec.supervisorMarker !== "dsh-update-status") throw new Error("managed service receipt has an unexpected service identity");
153
+ if (expectedHome !== void 0 && spec.home !== expectedHome) throw new Error("managed service receipt belongs to a different home directory");
139
154
  for (const [description, value] of [
140
155
  ["profile", spec.profile],
141
156
  ["workspace", spec.workspace],
142
157
  ["home", spec.home],
143
158
  ["log directory", spec.logDir],
144
- ["host", spec.host]
145
- ]) assertSafeText(value, description);
159
+ ["host", spec.host],
160
+ ["Node executable", spec.nodePath],
161
+ ["dsh executable", spec.dshPath]
162
+ ]) {
163
+ if (typeof value !== "string") throw new Error(`managed service receipt has an invalid ${description}`);
164
+ assertSafeText(value, description);
165
+ }
166
+ for (const [description, value] of [
167
+ ["workspace", spec.workspace],
168
+ ["home", spec.home],
169
+ ["log directory", spec.logDir],
170
+ ["Node executable", spec.nodePath],
171
+ ["dsh executable", spec.dshPath]
172
+ ]) if (!isAbsolute(value)) throw new Error(`managed service receipt has a non-absolute ${description}`);
173
+ if (spec.dshHome !== void 0) {
174
+ if (typeof spec.dshHome !== "string") throw new Error("managed service receipt has an invalid DSH_HOME");
175
+ assertSafeText(spec.dshHome, "DSH_HOME");
176
+ if (!isAbsolute(spec.dshHome)) throw new Error("managed service receipt has a non-absolute DSH_HOME");
177
+ }
178
+ if (!Number.isInteger(spec.port) || spec.port < 1 || spec.port > 65535) throw new Error("managed service receipt has an invalid port");
179
+ if (spec.servicePath !== void 0 && typeof spec.servicePath !== "string") throw new Error("managed service receipt has an invalid service PATH");
180
+ if (spec.servicePathSource !== void 0 && spec.servicePathSource !== "current" && spec.servicePathSource !== "minimal" && spec.servicePathSource !== "explicit") throw new Error("managed service receipt has an invalid service PATH source");
181
+ if (spec.servicePathCapturedAt !== void 0 && (typeof spec.servicePathCapturedAt !== "string" || Number.isNaN(Date.parse(spec.servicePathCapturedAt)))) throw new Error("managed service receipt has an invalid service PATH capture time");
182
+ if (spec.servicePathExt !== void 0 && typeof spec.servicePathExt !== "string") throw new Error("managed service receipt has an invalid PATHEXT");
183
+ if (spec.servicePathWarnings !== void 0 && (!Array.isArray(spec.servicePathWarnings) || spec.servicePathWarnings.length > 256 || spec.servicePathWarnings.some((value) => typeof value !== "string"))) throw new Error("managed service receipt has invalid service PATH warnings");
184
+ if (spec.servicePath === void 0 && (spec.servicePathSource !== void 0 || spec.servicePathCapturedAt !== void 0 || spec.servicePathExt !== void 0 || spec.servicePathWarnings !== void 0)) throw new Error("managed service receipt has PATH metadata without a service PATH");
185
+ if (spec.servicePath !== void 0 && (spec.servicePathSource === void 0 || spec.servicePathCapturedAt === void 0)) throw new Error("managed service receipt has an incomplete service PATH record");
186
+ if (spec.platform === "win32" && spec.servicePath !== void 0 && spec.servicePathExt === void 0) throw new Error("managed Windows service receipt has no PATHEXT");
187
+ if (spec.platform !== "win32" && spec.servicePathExt !== void 0) throw new Error("managed non-Windows service receipt has PATHEXT");
188
+ if (spec.nativeServiceCommand !== void 0 && typeof spec.nativeServiceCommand !== "string") throw new Error("managed service receipt has an invalid native service command");
189
+ const servicePath = servicePathOf(spec);
190
+ assertSafeText(servicePath, "service PATH");
191
+ if (servicePath.length > 8192) throw new Error("managed service receipt has an overlong service PATH");
192
+ if (spec.servicePathExt !== void 0) {
193
+ assertSafeText(spec.servicePathExt, "PATHEXT");
194
+ if (spec.servicePathExt.length > 8192) throw new Error("managed service receipt has an overlong PATHEXT");
195
+ normalizeServicePathExt(spec.servicePathExt);
196
+ }
197
+ for (const warning of spec.servicePathWarnings ?? []) assertSafeText(warning, "service PATH warning");
198
+ const nativeCommand = nativeServiceCommandOf(spec);
199
+ assertSafeText(nativeCommand, "native service command");
200
+ if (!isAbsolute(nativeCommand)) throw new Error("managed service receipt has a non-absolute native service command");
201
+ const allowedNativeCommands = nativeServiceCommandCandidates(spec.platform, expectedSystemRoot);
202
+ const nativeKey = spec.platform === "win32" ? nativeCommand.toLowerCase() : nativeCommand;
203
+ if (!allowedNativeCommands.some((candidate) => (spec.platform === "win32" ? candidate.toLowerCase() : candidate) === nativeKey)) throw new Error("managed service receipt has an unexpected native service command");
146
204
  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");
147
205
  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");
148
- assertExecutable(spec.nodePath, "Node executable");
149
- assertReadableRegular(spec.dshPath, "dsh executable");
150
- assertSafeText(spec.workspace, "workspace");
151
- if (!existsSync(spec.workspace) || !statSync(spec.workspace).isDirectory()) throw new Error(`workspace is not a directory: ${spec.workspace}`);
206
+ if (level === "launch") {
207
+ assertExecutable(spec.nodePath, "Node executable");
208
+ assertReadableRegular(spec.dshPath, "dsh executable");
209
+ assertExecutable(nativeCommand, "Native service command");
210
+ if (!existsSync(spec.workspace) || !statSync(spec.workspace).isDirectory()) throw new Error(`workspace is not a directory: ${spec.workspace}`);
211
+ }
152
212
  }
153
213
  function atomicWrite(path, content) {
154
214
  mkdirSync(dirname(path), {
@@ -182,19 +242,31 @@ function ensureReplaceable(path, expected, managed) {
182
242
  }
183
243
  function receiptOwnsPath(previous, path) {
184
244
  if (previous === void 0) return false;
185
- return previous.paths.definitionFile === path || previous.paths.stateFile === path || previous.paths.wrapperFile === path;
245
+ const owned = pathsFor(previous.spec);
246
+ return owned.definitionFile === path || owned.stateFile === path || owned.wrapperFile === path;
186
247
  }
187
248
  function receiptTextOf(receipt) {
188
249
  return JSON.stringify(receipt, null, 2) + "\n";
189
250
  }
190
- function readReceiptIfPresent(path) {
191
- return existsSync(path) ? readReceipt(path) : void 0;
251
+ function samePaths(left, right) {
252
+ return left.stateFile === right.stateFile && left.definitionFile === right.definitionFile && left.wrapperFile === right.wrapperFile && left.stdoutFile === right.stdoutFile && left.stderrFile === right.stderrFile;
253
+ }
254
+ function readReceiptIfPresent(path, env) {
255
+ return existsSync(path) ? readReceipt(path, env) : void 0;
192
256
  }
193
- function readReceipt(path) {
257
+ function readReceipt(path, env) {
194
258
  try {
195
259
  const value = JSON.parse(readFileSync(path, "utf8"));
196
260
  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");
197
- return value;
261
+ validateSpec(value.spec, "structural", servicePlatform(env.platform), env.home, envValue(env.env, "SYSTEMROOT"));
262
+ const canonical = pathsFor(value.spec);
263
+ if (canonical.stateFile !== path || !samePaths(value.paths, canonical)) throw new Error("managed service receipt paths do not match its service spec");
264
+ return {
265
+ version: RECEIPT_VERSION,
266
+ managedBy: "dsh-update-status",
267
+ spec: value.spec,
268
+ paths: canonical
269
+ };
198
270
  } catch (error) {
199
271
  throw new Error(`cannot read managed service receipt ${path}: ${error instanceof Error ? error.message : String(error)}`);
200
272
  }
@@ -207,6 +279,7 @@ async function portIsFree(host, port) {
207
279
  });
208
280
  }
209
281
  function serialisePlan(plan) {
282
+ const inheritedWindowsPath = plan.spec.platform === "win32" && plan.spec.servicePath === void 0;
210
283
  return JSON.stringify({
211
284
  platform: plan.spec.platform,
212
285
  label: plan.spec.label,
@@ -216,6 +289,14 @@ function serialisePlan(plan) {
216
289
  workspace: plan.spec.workspace,
217
290
  host: plan.spec.host,
218
291
  port: plan.spec.port,
292
+ nativeServiceCommand: nativeServiceCommandOf(plan.spec),
293
+ servicePath: {
294
+ source: inheritedWindowsPath ? "inherited logon environment (legacy receipt)" : plan.spec.servicePathSource ?? "minimal (legacy receipt)",
295
+ value: inheritedWindowsPath ? null : servicePathOf(plan.spec),
296
+ ...plan.spec.servicePathCapturedAt === void 0 ? {} : { capturedAt: plan.spec.servicePathCapturedAt },
297
+ ...plan.spec.servicePathExt === void 0 ? {} : { pathExt: plan.spec.servicePathExt },
298
+ warnings: plan.spec.servicePathWarnings ?? []
299
+ },
219
300
  definitionFile: plan.paths.definitionFile,
220
301
  wrapperFile: plan.paths.wrapperFile,
221
302
  stateFile: plan.paths.stateFile,
@@ -233,38 +314,9 @@ function serialisePlan(plan) {
233
314
  }
234
315
  }, null, 2) + "\n";
235
316
  }
236
- function commandsForActivate(plan, uid) {
237
- if (plan.spec.platform === "darwin") return [[
238
- "launchctl",
239
- "bootstrap",
240
- `gui/${uid}`,
241
- plan.paths.definitionFile
242
- ]];
243
- if (plan.spec.platform === "linux") return [[
244
- "systemctl",
245
- "--user",
246
- "daemon-reload"
247
- ], [
248
- "systemctl",
249
- "--user",
250
- "enable",
251
- "--now",
252
- "dsh-update-status-web.service"
253
- ]];
254
- return [[
255
- "schtasks.exe",
256
- "/Create",
257
- "/TN",
258
- "DSH Update Status Web",
259
- "/XML",
260
- plan.paths.definitionFile,
261
- "/F"
262
- ], [
263
- "schtasks.exe",
264
- "/Run",
265
- "/TN",
266
- "DSH Update Status Web"
267
- ]];
317
+ function commandsForActivate(plan, _uid) {
318
+ if (plan.spec.platform === "darwin") return [plan.installCommand];
319
+ return [plan.installCommand, plan.startCommand];
268
320
  }
269
321
  function invoke(command, run) {
270
322
  const [file, ...args] = command;
@@ -286,7 +338,8 @@ function preflight(spec) {
286
338
  env: {
287
339
  HOME: spec.home,
288
340
  ...spec.dshHome === void 0 ? {} : { DSH_HOME: spec.dshHome },
289
- PATH: `${dirname(spec.nodePath)}${process.platform === "win32" ? ";" : ":"}${process.env.PATH ?? ""}`
341
+ PATH: servicePathOf(spec),
342
+ ...spec.platform === "win32" && spec.servicePathExt !== void 0 ? { PATHEXT: spec.servicePathExt } : {}
290
343
  }
291
344
  });
292
345
  if (result.status !== 0) {
@@ -294,6 +347,32 @@ function preflight(spec) {
294
347
  throw new Error(`DSH boot-free preflight failed; no service definition was written: ${diagnostic}`);
295
348
  }
296
349
  }
350
+ const VALUE_OPTIONS = /* @__PURE__ */ new Set([
351
+ "--profile",
352
+ "--workspace",
353
+ "--dsh-home",
354
+ "--host",
355
+ "--port",
356
+ "--node",
357
+ "--dsh",
358
+ "--log-dir",
359
+ "--path-source",
360
+ "--service-path",
361
+ "--service-pathext"
362
+ ]);
363
+ function assertKnownOptions(args) {
364
+ for (let index = 0; index < args.length; index += 1) {
365
+ const value = args[index];
366
+ if (value === "--dry-run") continue;
367
+ if (!VALUE_OPTIONS.has(value)) throw new Error(`unknown option: ${value}`);
368
+ const next = args[index + 1];
369
+ if (next === void 0 || next.startsWith("--")) throw new Error(`missing value after ${value}`);
370
+ index += 1;
371
+ }
372
+ }
373
+ function warnAboutPath(spec) {
374
+ for (const warning of spec.servicePathWarnings ?? []) process.stderr.write(`Warning: ${warning}\n`);
375
+ }
297
376
  async function main(argv = process.argv.slice(2), env = environment(), run = runner, hooks = {}) {
298
377
  const [action, ...rawArgs] = argv;
299
378
  if (action === void 0 || action === "--help" || action === "-h") {
@@ -311,19 +390,27 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
311
390
  process.stderr.write(`Unknown command: ${action}\n${usage()}`);
312
391
  return 2;
313
392
  }
393
+ try {
394
+ assertKnownOptions(rawArgs);
395
+ } catch (error) {
396
+ process.stderr.write(`dsh-update-status-service: ${error instanceof Error ? error.message : String(error)}\n`);
397
+ return 2;
398
+ }
314
399
  const dryRun = rawArgs.includes("--dry-run") || action === "plan";
315
400
  const args = rawArgs.filter((value) => value !== "--dry-run");
316
401
  try {
317
- const spec = specFromArgs(args, env);
318
- validateSpec(spec);
319
- const plan = planFor(spec, env.uid ?? 0);
320
- if (dryRun) {
321
- process.stdout.write(serialisePlan(plan));
322
- return 0;
323
- }
324
- if (action === "install") {
402
+ if (dryRun || action === "install") {
403
+ const spec = specFromArgs(args, env);
404
+ validateSpec(spec, "launch", servicePlatform(env.platform), env.home, envValue(env.env, "SYSTEMROOT"));
405
+ const plan = planFor(spec, env.uid ?? 0);
406
+ if (dryRun) {
407
+ process.stdout.write(serialisePlan(plan));
408
+ return 0;
409
+ }
410
+ process.stdout.write(`Service PATH (${spec.servicePathSource ?? "minimal"}): ${servicePathOf(spec)}\n`);
411
+ warnAboutPath(spec);
325
412
  (hooks.preflight ?? preflight)(spec);
326
- const previous = readReceiptIfPresent(plan.paths.stateFile);
413
+ const previous = readReceiptIfPresent(plan.paths.stateFile, env);
327
414
  ensureReplaceable(plan.paths.definitionFile, plan.definition, receiptOwnsPath(previous, plan.paths.definitionFile));
328
415
  if (plan.wrapper !== void 0 && plan.paths.wrapperFile !== void 0) ensureReplaceable(plan.paths.wrapperFile, plan.wrapper, receiptOwnsPath(previous, plan.paths.wrapperFile));
329
416
  const receiptText = receiptTextOf({
@@ -343,7 +430,9 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
343
430
  process.stdout.write(`Staged user service definition: ${plan.paths.definitionFile}\nNo service has started. Stop your terminal-started DSH, then run activate.\n`);
344
431
  return 0;
345
432
  }
346
- const receipt = readReceipt(plan.paths.stateFile);
433
+ const receipt = readReceipt(serviceStateFileFromArgs(args, env), env);
434
+ const plan = planFor(receipt.spec, env.uid ?? 0);
435
+ if (action === "activate") validateSpec(receipt.spec, "launch", servicePlatform(env.platform), env.home, envValue(env.env, "SYSTEMROOT"));
347
436
  if (action === "activate") {
348
437
  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.`);
349
438
  for (const command of commandsForActivate(plan, env.uid ?? 0)) invoke(command, run);
@@ -369,9 +458,9 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
369
458
  }
370
459
  const result = run(plan.uninstallCommand[0], plan.uninstallCommand.slice(1));
371
460
  if (result.status !== 0) process.stderr.write(`Warning: ${result.stderr || result.stdout || "service was not loaded"}\n`);
372
- rmSync(receipt.paths.definitionFile, { force: true });
373
- if (receipt.paths.wrapperFile !== void 0) rmSync(receipt.paths.wrapperFile, { force: true });
374
- rmSync(receipt.paths.stateFile, { force: true });
461
+ rmSync(plan.paths.definitionFile, { force: true });
462
+ if (plan.paths.wrapperFile !== void 0) rmSync(plan.paths.wrapperFile, { force: true });
463
+ rmSync(plan.paths.stateFile, { force: true });
375
464
  process.stdout.write("Removed the managed user-service definition. Logs were preserved.\n");
376
465
  return 0;
377
466
  } catch (error) {
@@ -383,4 +472,4 @@ if (import.meta.url === new URL(process.argv[1] ?? "", "file:").href) main().the
383
472
  process.exitCode = code;
384
473
  });
385
474
  //#endregion
386
- export { main };
475
+ export { main, serialisePlan };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-update-status",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
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",
@@ -71,7 +71,8 @@
71
71
  "0.1.7-rc.2": "compatible",
72
72
  "0.2.0-rc.1": "compatible",
73
73
  "0.2.0-rc.2": "compatible",
74
- "0.2.1-alpha.1": "compatible"
74
+ "0.2.1-alpha.1": "compatible",
75
+ "0.2.1-alpha.3": "compatible"
75
76
  }
76
77
  }
77
78
  },