dsh-update-status 0.3.1 → 0.4.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.
@@ -1,344 +1,67 @@
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";
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";
2
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
6
4
  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);
5
+ import { createServer } from "node:net";
6
+ async function observeServiceState(spec, run, uid) {
227
7
  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
- ]
8
+ const result = run("launchctl", ["print", `gui/${String(uid)}/${spec.label}`]);
9
+ const pid = parseLaunchdTrackedPid(result.stdout);
10
+ return pid === void 0 ? { running: false } : {
11
+ running: true,
12
+ pid
260
13
  };
261
14
  }
262
- if (spec.platform === "linux") return {
263
- spec,
264
- paths,
265
- definition: systemdDefinition(spec),
266
- installCommand: [
267
- "systemctl",
15
+ if (spec.platform === "linux") {
16
+ const result = run("systemctl", [
268
17
  "--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
- };
18
+ "show",
19
+ SYSTEMD_UNIT,
20
+ "-p",
21
+ "MainPID",
22
+ "--value"
23
+ ]);
24
+ const pid = parseSystemdMainPid(result.stdout);
25
+ return pid === void 0 ? { running: false } : {
26
+ running: true,
27
+ pid
28
+ };
29
+ }
30
+ const result = run("powershell.exe", [
31
+ "-NoProfile",
32
+ "-NonInteractive",
33
+ "-Command",
34
+ `(Get-ScheduledTask -TaskName '${WINDOWS_TASK_NAME}').State`
35
+ ]);
36
+ return { running: parseWindowsTaskRunning(result.stdout) };
37
+ }
38
+ function defaultSleep(ms) {
39
+ return new Promise((resolve) => {
40
+ setTimeout(resolve, ms);
41
+ });
42
+ }
43
+ async function verifyActivatedService(spec, options) {
44
+ const attempts = options.attempts ?? 4;
45
+ const intervalMs = options.intervalMs ?? 5e3;
46
+ const sleep = options.sleep ?? defaultSleep;
47
+ const uid = options.uid ?? 0;
48
+ const recentRestarts = options.recentRestarts ?? (() => countRecentRestarts(serviceLogDir(), bootStampFile(), Date.now(), 18e4));
49
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
50
+ const restarts = recentRestarts();
51
+ if (restarts >= 4) return {
52
+ kind: "crash-loop",
53
+ recentRestarts: restarts,
54
+ windowMs: RESTART_STORM_WINDOW_MS
55
+ };
56
+ if ((await observeServiceState(spec, options.run, uid)).running) {
57
+ if (attempt === attempts - 1) return { kind: "settled" };
58
+ await sleep(intervalMs);
59
+ continue;
60
+ }
61
+ if (attempt === attempts - 1) return { kind: "not-running" };
62
+ await sleep(intervalMs);
63
+ }
64
+ return { kind: "not-running" };
342
65
  }
343
66
  //#endregion
344
67
  //#region src/service/cli.ts
@@ -384,6 +107,10 @@ Options:
384
107
  --log-dir <path> Absolute log directory (default: $DSH_HOME/logs)
385
108
 
386
109
  Workflow: plan --dry-run → install → stop the terminal-started DSH yourself → activate.
110
+
111
+ activate refuses an occupied port, never kills a process, and then verifies that
112
+ the service it started actually took over; a service that begins crash-looping
113
+ makes activate fail instead of reporting success.
387
114
  `;
388
115
  }
389
116
  function assertSafeText(value, description) {
@@ -435,7 +162,14 @@ function atomicWrite(path, content) {
435
162
  });
436
163
  renameSync(temporary, path);
437
164
  }
438
- function ensureManagedTarget(path, expected) {
165
+ /**
166
+ * Replace an artifact only when we can prove we wrote it: either it already holds
167
+ * exactly what we are about to write, or our own receipt names that path. The
168
+ * receipt is what makes an upgrade possible at all — an improved definition
169
+ * differs from the stored one by exactly the fix being installed, so
170
+ * byte-equality with the previous rendering can never authorise it.
171
+ */
172
+ function ensureReplaceable(path, expected, managed) {
439
173
  if (!existsSync(path)) return;
440
174
  let current;
441
175
  try {
@@ -443,7 +177,18 @@ function ensureManagedTarget(path, expected) {
443
177
  } catch {
444
178
  throw new Error(`refusing to replace unreadable existing file: ${path}`);
445
179
  }
446
- if (current !== expected) throw new Error(`refusing to overwrite an existing unmanaged service artifact: ${path}`);
180
+ if (current === expected || managed) return;
181
+ throw new Error(`refusing to overwrite an existing unmanaged service artifact: ${path}`);
182
+ }
183
+ function receiptOwnsPath(previous, path) {
184
+ if (previous === void 0) return false;
185
+ return previous.paths.definitionFile === path || previous.paths.stateFile === path || previous.paths.wrapperFile === path;
186
+ }
187
+ function receiptTextOf(receipt) {
188
+ return JSON.stringify(receipt, null, 2) + "\n";
189
+ }
190
+ function readReceiptIfPresent(path) {
191
+ return existsSync(path) ? readReceipt(path) : void 0;
447
192
  }
448
193
  function readReceipt(path) {
449
194
  try {
@@ -483,7 +228,8 @@ function serialisePlan(plan) {
483
228
  start: plan.startCommand,
484
229
  status: plan.statusCommand,
485
230
  stop: plan.stopCommand,
486
- uninstall: plan.uninstallCommand
231
+ uninstall: plan.uninstallCommand,
232
+ recover: plan.recoverCommand
487
233
  }
488
234
  }, null, 2) + "\n";
489
235
  }
@@ -577,16 +323,16 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
577
323
  }
578
324
  if (action === "install") {
579
325
  (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 = {
326
+ const previous = readReceiptIfPresent(plan.paths.stateFile);
327
+ ensureReplaceable(plan.paths.definitionFile, plan.definition, receiptOwnsPath(previous, plan.paths.definitionFile));
328
+ if (plan.wrapper !== void 0 && plan.paths.wrapperFile !== void 0) ensureReplaceable(plan.paths.wrapperFile, plan.wrapper, receiptOwnsPath(previous, plan.paths.wrapperFile));
329
+ const receiptText = receiptTextOf({
583
330
  version: RECEIPT_VERSION,
584
331
  managedBy: "dsh-update-status",
585
332
  spec,
586
333
  paths: plan.paths
587
- };
588
- const receiptText = JSON.stringify(receipt, null, 2) + "\n";
589
- ensureManagedTarget(plan.paths.stateFile, receiptText);
334
+ });
335
+ ensureReplaceable(plan.paths.stateFile, receiptText, receiptOwnsPath(previous, plan.paths.stateFile));
590
336
  mkdirSync(spec.logDir, {
591
337
  recursive: true,
592
338
  mode: 448
@@ -601,7 +347,13 @@ async function main(argv = process.argv.slice(2), env = environment(), run = run
601
347
  if (action === "activate") {
602
348
  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
349
  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");
350
+ const verification = await (hooks.verify ?? ((installed) => verifyActivatedService(installed, {
351
+ run,
352
+ uid: env.uid ?? 0
353
+ })))(receipt.spec);
354
+ if (verification.kind === "crash-loop") throw new Error(`the activated service is restarting repeatedly (${String(verification.recentRestarts)} starts within ${String(Math.round(verification.windowMs / 1e3))}s); another process probably holds ${receipt.spec.host}:${String(receipt.spec.port)}. Nothing was killed. Stop that instance, fix the cause, then run activate again.`);
355
+ if (verification.kind === "not-running") throw new Error(`the activated service reported no running process; check the native status and the logs in ${receipt.spec.logDir}. Nothing was killed.`);
356
+ process.stdout.write("User service activated and verified to be running. Wait for DSH to become ready, then open the existing Web URL.\n");
605
357
  return 0;
606
358
  }
607
359
  if (action === "status") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-update-status",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
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",
@@ -17,11 +17,7 @@
17
17
  "./package.json": "./package.json"
18
18
  },
19
19
  "files": [
20
- "lib/index.js",
21
- "lib/index.d.ts",
22
- "lib/client.js",
23
- "lib/client.js.map",
24
- "lib/service-cli.js",
20
+ "lib",
25
21
  "bin/dsh-update-status-service.mjs",
26
22
  "cordis.patch.yml",
27
23
  "README.md",