@ours.network/cli 0.3.0 → 0.4.1

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,132 @@
1
+ import {
2
+ instanceNameFromStateDir,
3
+ serviceDescription,
4
+ unitNameForStateDir
5
+ } from "./chunk-3RLAHOPS.js";
6
+
7
+ // src/service.ts
8
+ import { execFile as nodeExecFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { dirname, join, resolve } from "node:path";
13
+ var execFile = promisify(nodeExecFile);
14
+ var MARKER = "# Managed by @ours.network/cli";
15
+ var SERVICE_NAME = "ours.service";
16
+ var defaultDeps = {
17
+ platform: process.platform,
18
+ run: async (command, args) => {
19
+ await execFile(command, args, { windowsHide: true });
20
+ }
21
+ };
22
+ function systemdEscapeArgument(value) {
23
+ if (/[\n\r]/.test(value)) throw new Error("service arguments must not contain newlines");
24
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
25
+ }
26
+ function systemdUnescapeArgument(value) {
27
+ const trimmed = value.trim();
28
+ if (!trimmed.startsWith('"') || !trimmed.endsWith('"') || trimmed.length < 2) return void 0;
29
+ return trimmed.slice(1, -1).replaceAll("%%", "%").replaceAll('\\"', '"').replaceAll("\\\\", "\\");
30
+ }
31
+ function bakedStateDir(unit) {
32
+ for (const line of unit.split("\n")) {
33
+ if (!line.startsWith("Environment=OURS_STATE_DIR=")) continue;
34
+ return systemdUnescapeArgument(line.slice("Environment=OURS_STATE_DIR=".length));
35
+ }
36
+ return void 0;
37
+ }
38
+ function describeConflict(previous, serviceFile, stateDir, verb = "overwrite") {
39
+ if (!previous.startsWith(MARKER)) {
40
+ return {
41
+ kind: "unmanaged",
42
+ serviceFile,
43
+ message: verb === "remove" ? `${serviceFile} is not managed by @ours.network/cli; refusing to remove it` : `${serviceFile} is not managed by @ours.network/cli; refusing to overwrite it without --force`
44
+ };
45
+ }
46
+ const owner = bakedStateDir(previous);
47
+ if (owner !== void 0 && owner !== stateDir) {
48
+ return {
49
+ kind: "other-state-dir",
50
+ serviceFile,
51
+ owner,
52
+ message: `${serviceFile} is the boot service for the daemon at ${owner}, not ${stateDir}; refusing to ${verb} it without --force`
53
+ };
54
+ }
55
+ return void 0;
56
+ }
57
+ function resolveStateDir(context) {
58
+ return resolve(context.stateDir ?? join(context.homeDir ?? homedir(), ".ours"));
59
+ }
60
+ function createLinuxUserSystemdAdapter(deps = defaultDeps) {
61
+ if (deps.platform !== "linux") throw new Error(`service management is not supported on ${deps.platform}; use an external launcher for \`ours daemon serve\``);
62
+ return {
63
+ name: "systemd-user",
64
+ async install(context) {
65
+ const home = context.homeDir ?? homedir();
66
+ const stateDir = resolveStateDir(context);
67
+ const unitName = unitNameForStateDir(stateDir);
68
+ const serviceFile = join(home, ".config", "systemd", "user", unitName);
69
+ const unitConfig = context.configPath ? resolve(context.configPath) : void 0;
70
+ const args = [process.execPath, resolve(context.cliPath), "daemon", "serve"];
71
+ if (unitConfig) args.push("--config", unitConfig);
72
+ const environment = `Environment=OURS_STATE_DIR=${systemdEscapeArgument(stateDir)}
73
+ `;
74
+ const unit = `${MARKER}
75
+ [Unit]
76
+ Description=${serviceDescription(instanceNameFromStateDir(stateDir).name)}
77
+ After=network-online.target
78
+
79
+ [Service]
80
+ Type=simple
81
+ ExecStart=${args.map(systemdEscapeArgument).join(" ")}
82
+ ${environment}Restart=on-failure
83
+ RestartSec=2
84
+
85
+ [Install]
86
+ WantedBy=default.target
87
+ `;
88
+ if (existsSync(serviceFile)) {
89
+ const previous = readFileSync(serviceFile, "utf8");
90
+ const conflict = describeConflict(previous, serviceFile, stateDir);
91
+ if (conflict && !context.force) {
92
+ if (!context.dryRun) throw new Error(conflict.message);
93
+ return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands: [], changed: false, dryRun: true, conflict };
94
+ }
95
+ if (previous === unit) return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands: [], changed: false, dryRun: context.dryRun === true };
96
+ }
97
+ const commands = [["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "enable", "--now", unitName]];
98
+ if (!context.dryRun) {
99
+ mkdirSync(dirname(serviceFile), { recursive: true });
100
+ writeFileSync(serviceFile, unit, { mode: 420 });
101
+ for (const [command, ...args2] of commands) await deps.run(command, args2);
102
+ }
103
+ return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands, changed: true, dryRun: context.dryRun === true };
104
+ },
105
+ async uninstall(context) {
106
+ const home = context.homeDir ?? homedir();
107
+ const stateDir = resolveStateDir(context);
108
+ const unitName = unitNameForStateDir(stateDir);
109
+ const serviceFile = join(home, ".config", "systemd", "user", unitName);
110
+ if (!existsSync(serviceFile)) return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands: [], changed: false, dryRun: context.dryRun === true };
111
+ const previous = readFileSync(serviceFile, "utf8");
112
+ const conflict = describeConflict(previous, serviceFile, stateDir, "remove");
113
+ if (conflict && !(context.force && previous.startsWith(MARKER))) {
114
+ if (!context.dryRun) throw new Error(conflict.message);
115
+ return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands: [], changed: false, dryRun: true, conflict };
116
+ }
117
+ const commands = [["systemctl", "--user", "disable", "--now", unitName], ["systemctl", "--user", "daemon-reload"]];
118
+ if (!context.dryRun) {
119
+ await deps.run(commands[0][0], commands[0].slice(1));
120
+ unlinkSync(serviceFile);
121
+ await deps.run(commands[1][0], commands[1].slice(1));
122
+ }
123
+ return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands, changed: true, dryRun: context.dryRun === true };
124
+ }
125
+ };
126
+ }
127
+
128
+ export {
129
+ SERVICE_NAME,
130
+ bakedStateDir,
131
+ createLinuxUserSystemdAdapter
132
+ };
@@ -0,0 +1,57 @@
1
+ // src/service-instance.ts
2
+ import { basename, resolve } from "node:path";
3
+ var DEFAULT_SYSTEMD_UNIT = "ours.service";
4
+ var INSTANCE_RE = /^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,30}[A-Za-z0-9])?$/;
5
+ function normalizeInstanceName(raw) {
6
+ const s = typeof raw === "string" ? raw.trim() : "";
7
+ if (!s) return { ok: true, name: "" };
8
+ if (s.length > 32) {
9
+ return { ok: false, name: "", reason: "service name must be 32 characters or fewer" };
10
+ }
11
+ if (!INSTANCE_RE.test(s)) {
12
+ return {
13
+ ok: false,
14
+ name: "",
15
+ reason: "service name must be 1\u201332 characters of letters, digits, hyphen or underscore, starting and ending with a letter or digit"
16
+ };
17
+ }
18
+ return { ok: true, name: s };
19
+ }
20
+ function systemdUnitName(instance = "") {
21
+ const { ok, name } = normalizeInstanceName(instance);
22
+ if (!ok || !name) return DEFAULT_SYSTEMD_UNIT;
23
+ return `ours-${name}.service`;
24
+ }
25
+ function serviceDescription(instance = "") {
26
+ const { ok, name } = normalizeInstanceName(instance);
27
+ return ok && name ? `ours daemon (instance "${name}")` : "ours shared daemon";
28
+ }
29
+ function instanceNameFromStateDir(stateDir) {
30
+ const segment = basename(resolve(stateDir));
31
+ const undotted = segment.startsWith(".") ? segment.slice(1) : segment;
32
+ if (undotted === "ours") return { ok: true, name: "" };
33
+ const stripped = undotted.startsWith("ours-") ? undotted.slice("ours-".length) : undotted;
34
+ const result = normalizeInstanceName(stripped);
35
+ if (!result.ok) {
36
+ return {
37
+ ok: false,
38
+ name: "",
39
+ reason: `state directory ${JSON.stringify(resolve(stateDir))} does not yield a usable service name (${JSON.stringify(stripped)}): ${result.reason}`
40
+ };
41
+ }
42
+ return result;
43
+ }
44
+ function unitNameForStateDir(stateDir) {
45
+ const { ok, name, reason } = instanceNameFromStateDir(stateDir);
46
+ if (!ok) throw new Error(reason);
47
+ return systemdUnitName(name);
48
+ }
49
+
50
+ export {
51
+ DEFAULT_SYSTEMD_UNIT,
52
+ normalizeInstanceName,
53
+ systemdUnitName,
54
+ serviceDescription,
55
+ instanceNameFromStateDir,
56
+ unitNameForStateDir
57
+ };
@@ -103,8 +103,8 @@ function nativeDaemonOptions(action) {
103
103
  const install = action === "install-service";
104
104
  const rows = [
105
105
  ["--endpoint URL", selected ? action === "serve" || action === "start" || action === "restart" ? "Rejected: this action must start a local selected daemon." : "Select a running endpoint; pair it with --state-dir." : "Compatibility-only/ignored for service management."],
106
- ["--port N", selected ? "Select daemon port 1\u201365535." : "Compatibility-only/ignored for service management."],
107
- ["--state-dir PATH", selected ? "Select the daemon state directory." : "Compatibility-only/ignored for service management."],
106
+ ["--port N", selected ? "Select daemon port 1\u201365535." : "Checked against the port recorded for this state directory; a disagreement is refused and nothing is written."],
107
+ ["--state-dir PATH", selected ? "Select the daemon state directory." : "Select the daemon whose unit this is; the unit name derives from it (~/.ours \u2192 ours.service, ~/.ours-tg \u2192 ours-tg.service)."],
108
108
  ["--config PATH", selected ? "Use this daemon config for lifecycle selection." : action === "install-service" ? "Embed this config file\u2019s absolute path in the installed unit." : "Compatibility-only/ignored by uninstall-service."],
109
109
  ["--identity NAME", "Compatibility-only/ignored by daemon lifecycle and service commands."],
110
110
  ["--json", action === "serve" ? "Compatibility-only/ignored because serve runs until interrupted." : "Write one JSON result to stdout."],
package/dist/help.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  helpRequestPath,
4
4
  renderHelp,
5
5
  renderTopHelp
6
- } from "./chunk-HNBWRI4O.js";
6
+ } from "./chunk-VGWQD7CJ.js";
7
7
  import "./chunk-QRNZFPNJ.js";
8
8
  import "./chunk-6NFATG6P.js";
9
9
  import "./chunk-YSHMCQ2B.js";
package/dist/main.js CHANGED
@@ -5,7 +5,10 @@ import {
5
5
  } from "./chunk-YC7QBGVC.js";
6
6
  import {
7
7
  createLinuxUserSystemdAdapter
8
- } from "./chunk-67BC2E5U.js";
8
+ } from "./chunk-227DBTDK.js";
9
+ import {
10
+ instanceNameFromStateDir
11
+ } from "./chunk-3RLAHOPS.js";
9
12
  import {
10
13
  setupConfig,
11
14
  showConfig
@@ -14,7 +17,7 @@ import {
14
17
  helpHint,
15
18
  helpRequestPath,
16
19
  renderHelp
17
- } from "./chunk-HNBWRI4O.js";
20
+ } from "./chunk-VGWQD7CJ.js";
18
21
  import {
19
22
  invokeOperation,
20
23
  isOperationName,
@@ -49,9 +52,10 @@ import {
49
52
  } from "./chunk-6W3RHW3C.js";
50
53
 
51
54
  // src/main.ts
52
- import { readFileSync } from "node:fs";
53
- import { resolve } from "node:path";
54
- var CLI_VERSION = false ? "0.0.0-dev" : "0.3.0";
55
+ import { existsSync, readFileSync } from "node:fs";
56
+ import { homedir } from "node:os";
57
+ import { join, resolve } from "node:path";
58
+ var CLI_VERSION = false ? "0.0.0-dev" : "0.4.1";
55
59
  var COMMON_VALUES = /* @__PURE__ */ new Set(["--endpoint", "--port", "--state-dir", "--config", "--identity"]);
56
60
  var COMMON_BOOLEANS = /* @__PURE__ */ new Set(["--json", "--yes", "--help"]);
57
61
  function selectionFrom(values) {
@@ -62,6 +66,41 @@ function selectionFrom(values) {
62
66
  configPath: values["--config"]
63
67
  };
64
68
  }
69
+ function readDaemonConfig(path) {
70
+ if (!existsSync(path)) return void 0;
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(readFileSync(path, "utf8"));
74
+ } catch (error) {
75
+ throw new CliUsageError(`${path} is not readable JSON: ${error instanceof Error ? error.message : String(error)}`);
76
+ }
77
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
78
+ throw new CliUsageError(`${path} must contain a JSON object`);
79
+ }
80
+ return parsed;
81
+ }
82
+ function serviceTarget(values) {
83
+ const namedConfig = values["--config"] ?? process.env.OURS_CONFIG;
84
+ const seedConfigPath = namedConfig ?? defaultConfigPath();
85
+ const seedConfig = readDaemonConfig(resolve(seedConfigPath));
86
+ const fromFile = typeof seedConfig?.stateDir === "string" ? seedConfig.stateDir : void 0;
87
+ const stateDir = resolve(
88
+ values["--state-dir"] ?? process.env.OURS_STATE_DIR ?? fromFile ?? join(homedir(), ".ours")
89
+ );
90
+ const configPath = namedConfig ?? join(stateDir, "config.json");
91
+ const ownConfig = join(stateDir, "config.json");
92
+ const unitConfigPath = values["--config"] ?? (existsSync(ownConfig) ? ownConfig : void 0);
93
+ return { stateDir, configPath, unitConfigPath };
94
+ }
95
+ function assertPortAgrees(port, target) {
96
+ if (port === void 0 || target.configPath === void 0) return;
97
+ const config = readDaemonConfig(resolve(target.configPath));
98
+ const recorded = config?.port;
99
+ if (typeof recorded !== "number" || recorded === port) return;
100
+ throw new CliUsageError(
101
+ `--port ${port} disagrees with port ${recorded}, which ${resolve(target.configPath)} records for the daemon at ${target.stateDir}. Re-run without --port to use the recorded port, or change the recorded port first. Nothing was written.`
102
+ );
103
+ }
65
104
  function requireYes(operation, yes) {
66
105
  if (DESTRUCTIVE_OPERATIONS.has(operation) && !yes) throw new CliUsageError(`${operation} is destructive; re-run with --yes`);
67
106
  }
@@ -184,8 +223,12 @@ async function runDaemon(args, io) {
184
223
  if (handler === "install-service" || handler === "uninstall-service") {
185
224
  const dryRun = flags.booleans.has("--dry-run");
186
225
  if (!dryRun && !flags.booleans.has("--yes")) throw new CliUsageError(`${handler} changes the user service manager; re-run with --yes or preview with --dry-run`);
226
+ const target = serviceTarget(flags.values);
227
+ const derived = instanceNameFromStateDir(target.stateDir);
228
+ if (!derived.ok) throw new CliUsageError(derived.reason ?? `cannot derive a service name from ${target.stateDir}`);
229
+ assertPortAgrees(selection.port, target);
187
230
  const adapter = createLinuxUserSystemdAdapter();
188
- const context = { cliPath: process.argv[1], configPath: selection.configPath, dryRun, force: flags.booleans.has("--force") };
231
+ const context = { cliPath: process.argv[1], configPath: target.unitConfigPath, stateDir: target.stateDir, dryRun, force: flags.booleans.has("--force") };
189
232
  const result = handler === "install-service" ? await adapter.install(context) : await adapter.uninstall(context);
190
233
  writeResult(io, result, json, handler);
191
234
  return 0;
@@ -0,0 +1,12 @@
1
+ export declare const DEFAULT_SYSTEMD_UNIT = "ours.service";
2
+ export interface InstanceResult {
3
+ ok: boolean;
4
+ /** '' means "the default, unnamed daemon" — the historical single-unit behaviour. */
5
+ name: string;
6
+ reason?: string;
7
+ }
8
+ export declare function normalizeInstanceName(raw: unknown): InstanceResult;
9
+ export declare function systemdUnitName(instance?: string): string;
10
+ export declare function serviceDescription(instance?: string): string;
11
+ export declare function instanceNameFromStateDir(stateDir: string): InstanceResult;
12
+ export declare function unitNameForStateDir(stateDir: string): string;
@@ -0,0 +1,16 @@
1
+ import {
2
+ DEFAULT_SYSTEMD_UNIT,
3
+ instanceNameFromStateDir,
4
+ normalizeInstanceName,
5
+ serviceDescription,
6
+ systemdUnitName,
7
+ unitNameForStateDir
8
+ } from "./chunk-3RLAHOPS.js";
9
+ export {
10
+ DEFAULT_SYSTEMD_UNIT,
11
+ instanceNameFromStateDir,
12
+ normalizeInstanceName,
13
+ serviceDescription,
14
+ systemdUnitName,
15
+ unitNameForStateDir
16
+ };
package/dist/service.d.ts CHANGED
@@ -1,15 +1,45 @@
1
+ /**
2
+ * The unit for the default, unnamed daemon. Kept as a named export because it
3
+ * is the historical public constant; every other unit name comes from
4
+ * `unitNameForStateDir`, which returns exactly this string for `~/.ours`.
5
+ */
1
6
  export declare const SERVICE_NAME = "ours.service";
2
7
  export interface ServicePlan {
3
8
  adapter: string;
4
9
  action: 'install' | 'uninstall';
5
10
  serviceFile: string;
11
+ unitName: string;
12
+ stateDir: string;
13
+ /** The config file the unit's ExecStart selects, or undefined when it selects none. */
14
+ configPath?: string;
15
+ /**
16
+ * Set ONLY on a --dry-run that found something in the way. A real run throws
17
+ * on the same condition; a preview reports it, because a preview that raises
18
+ * on the one case it exists to preview is not a preview.
19
+ */
20
+ conflict?: ServiceConflict;
6
21
  commands: string[][];
7
22
  changed: boolean;
8
23
  dryRun: boolean;
9
24
  }
25
+ /** Why a run would refuse to touch the unit file that is already there. */
26
+ export interface ServiceConflict {
27
+ kind: 'unmanaged' | 'other-state-dir';
28
+ serviceFile: string;
29
+ /** For 'other-state-dir': the state directory the existing unit belongs to. */
30
+ owner?: string;
31
+ message: string;
32
+ }
10
33
  export interface ServiceContext {
11
34
  cliPath: string;
12
35
  configPath?: string;
36
+ /**
37
+ * The state directory this unit belongs to. It decides the unit NAME and is
38
+ * baked into the unit FILE, so a unit can always be traced back to its
39
+ * daemon. Defaults to `<home>/.ours`, which derives the historical
40
+ * `ours.service`.
41
+ */
42
+ stateDir?: string;
13
43
  homeDir?: string;
14
44
  dryRun?: boolean;
15
45
  force?: boolean;
@@ -23,4 +53,9 @@ export interface ServiceAdapter {
23
53
  install(context: ServiceContext): Promise<ServicePlan>;
24
54
  uninstall(context: ServiceContext): Promise<ServicePlan>;
25
55
  }
56
+ /**
57
+ * The state directory a CLI-managed unit was installed for, or undefined for a
58
+ * unit written before this CLI baked one in.
59
+ */
60
+ export declare function bakedStateDir(unit: string): string | undefined;
26
61
  export declare function createLinuxUserSystemdAdapter(deps?: ServiceDeps): ServiceAdapter;
package/dist/service.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import {
2
2
  SERVICE_NAME,
3
+ bakedStateDir,
3
4
  createLinuxUserSystemdAdapter
4
- } from "./chunk-67BC2E5U.js";
5
+ } from "./chunk-227DBTDK.js";
6
+ import "./chunk-3RLAHOPS.js";
5
7
  export {
6
8
  SERVICE_NAME,
9
+ bakedStateDir,
7
10
  createLinuxUserSystemdAdapter
8
11
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Transport-neutral operator CLI for the ours shared daemon",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",
@@ -1,78 +0,0 @@
1
- // src/service.ts
2
- import { execFile as nodeExecFile } from "node:child_process";
3
- import { promisify } from "node:util";
4
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
5
- import { homedir } from "node:os";
6
- import { dirname, join, resolve } from "node:path";
7
- var execFile = promisify(nodeExecFile);
8
- var MARKER = "# Managed by @ours.network/cli";
9
- var SERVICE_NAME = "ours.service";
10
- var defaultDeps = {
11
- platform: process.platform,
12
- run: async (command, args) => {
13
- await execFile(command, args, { windowsHide: true });
14
- }
15
- };
16
- function systemdEscapeArgument(value) {
17
- if (/[\n\r]/.test(value)) throw new Error("service arguments must not contain newlines");
18
- return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
19
- }
20
- function createLinuxUserSystemdAdapter(deps = defaultDeps) {
21
- if (deps.platform !== "linux") throw new Error(`service management is not supported on ${deps.platform}; use an external launcher for \`ours daemon serve\``);
22
- return {
23
- name: "systemd-user",
24
- async install(context) {
25
- const home = context.homeDir ?? homedir();
26
- const serviceFile = join(home, ".config", "systemd", "user", SERVICE_NAME);
27
- const args = [process.execPath, resolve(context.cliPath), "daemon", "serve"];
28
- if (context.configPath) args.push("--config", resolve(context.configPath));
29
- const unit = `${MARKER}
30
- [Unit]
31
- Description=ours shared daemon
32
- After=network-online.target
33
-
34
- [Service]
35
- Type=simple
36
- ExecStart=${args.map(systemdEscapeArgument).join(" ")}
37
- Restart=on-failure
38
- RestartSec=2
39
-
40
- [Install]
41
- WantedBy=default.target
42
- `;
43
- if (existsSync(serviceFile)) {
44
- const previous = readFileSync(serviceFile, "utf8");
45
- if (!previous.startsWith(MARKER) && !context.force) {
46
- throw new Error(`${serviceFile} is not managed by @ours.network/cli; refusing to overwrite it without --force`);
47
- }
48
- if (previous === unit) return { adapter: this.name, action: "install", serviceFile, commands: [], changed: false, dryRun: context.dryRun === true };
49
- }
50
- const commands = [["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "enable", "--now", SERVICE_NAME]];
51
- if (!context.dryRun) {
52
- mkdirSync(dirname(serviceFile), { recursive: true });
53
- writeFileSync(serviceFile, unit, { mode: 420 });
54
- for (const [command, ...args2] of commands) await deps.run(command, args2);
55
- }
56
- return { adapter: this.name, action: "install", serviceFile, commands, changed: true, dryRun: context.dryRun === true };
57
- },
58
- async uninstall(context) {
59
- const home = context.homeDir ?? homedir();
60
- const serviceFile = join(home, ".config", "systemd", "user", SERVICE_NAME);
61
- if (!existsSync(serviceFile)) return { adapter: this.name, action: "uninstall", serviceFile, commands: [], changed: false, dryRun: context.dryRun === true };
62
- const previous = readFileSync(serviceFile, "utf8");
63
- if (!previous.startsWith(MARKER)) throw new Error(`${serviceFile} is not managed by @ours.network/cli; refusing to remove it`);
64
- const commands = [["systemctl", "--user", "disable", "--now", SERVICE_NAME], ["systemctl", "--user", "daemon-reload"]];
65
- if (!context.dryRun) {
66
- await deps.run(commands[0][0], commands[0].slice(1));
67
- unlinkSync(serviceFile);
68
- await deps.run(commands[1][0], commands[1].slice(1));
69
- }
70
- return { adapter: this.name, action: "uninstall", serviceFile, commands, changed: true, dryRun: context.dryRun === true };
71
- }
72
- };
73
- }
74
-
75
- export {
76
- SERVICE_NAME,
77
- createLinuxUserSystemdAdapter
78
- };