@ours.network/cli 0.4.2 → 0.6.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.
- package/dist/{chunk-VGWQD7CJ.js → chunk-RJWUCR3R.js} +1 -1
- package/dist/{chunk-YSHMCQ2B.js → chunk-SDNTPCKW.js} +3 -3
- package/dist/chunk-TNNBVUKK.js +241 -0
- package/dist/{chunk-3RLAHOPS.js → chunk-UZQWSOUB.js} +15 -1
- package/dist/commands.d.ts +3 -3
- package/dist/commands.js +1 -1
- package/dist/help.js +2 -2
- package/dist/main.js +7 -7
- package/dist/service-instance.d.ts +3 -0
- package/dist/service-instance.js +7 -1
- package/dist/service.d.ts +30 -0
- package/dist/service.js +10 -4
- package/package.json +1 -1
- package/dist/chunk-227DBTDK.js +0 -132
|
@@ -42,14 +42,14 @@ var COMMAND_GROUPS = {
|
|
|
42
42
|
"install-service": {
|
|
43
43
|
kind: "daemon",
|
|
44
44
|
handler: "install-service",
|
|
45
|
-
summary: "Install and enable the Linux
|
|
46
|
-
effects: "Changes ~/.config/systemd/user/ours.service and runs systemctl --user. Requires --yes unless --dry-run is used; --force may replace only an existing unowned unit.",
|
|
45
|
+
summary: "Install and enable the per-user boot service (Linux systemd, macOS launchd).",
|
|
46
|
+
effects: "Changes ~/.config/systemd/user/ours.service and runs systemctl --user on Linux, or ~/Library/LaunchAgents and launchctl on macOS. Requires --yes unless --dry-run is used; --force may replace only an existing unowned unit.",
|
|
47
47
|
example: "ours daemon install-service --dry-run --json"
|
|
48
48
|
},
|
|
49
49
|
"uninstall-service": {
|
|
50
50
|
kind: "daemon",
|
|
51
51
|
handler: "uninstall-service",
|
|
52
|
-
summary: "Disable and remove the
|
|
52
|
+
summary: "Disable and remove the per-user boot service owned by this CLI.",
|
|
53
53
|
effects: "Changes the user service manager. Requires --yes unless --dry-run is used and refuses to remove an unowned unit.",
|
|
54
54
|
example: "ours daemon uninstall-service --dry-run --json"
|
|
55
55
|
},
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import {
|
|
2
|
+
instanceNameFromStateDir,
|
|
3
|
+
labelForStateDir,
|
|
4
|
+
serviceDescription,
|
|
5
|
+
unitNameForStateDir
|
|
6
|
+
} from "./chunk-UZQWSOUB.js";
|
|
7
|
+
|
|
8
|
+
// src/service.ts
|
|
9
|
+
import { execFile as nodeExecFile } from "node:child_process";
|
|
10
|
+
import { promisify } from "node:util";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
var execFile = promisify(nodeExecFile);
|
|
15
|
+
var MARKER = "# Managed by @ours.network/cli";
|
|
16
|
+
var PLIST_MARKER = "<!-- Managed by @ours.network/cli -->";
|
|
17
|
+
var SERVICE_NAME = "ours.service";
|
|
18
|
+
var defaultDeps = {
|
|
19
|
+
platform: process.platform,
|
|
20
|
+
run: async (command, args) => {
|
|
21
|
+
await execFile(command, args, { windowsHide: true });
|
|
22
|
+
},
|
|
23
|
+
uid: process.getuid?.()
|
|
24
|
+
};
|
|
25
|
+
function systemdEscapeArgument(value) {
|
|
26
|
+
if (/[\n\r]/.test(value)) throw new Error("service arguments must not contain newlines");
|
|
27
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%")}"`;
|
|
28
|
+
}
|
|
29
|
+
function systemdUnescapeArgument(value) {
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"') || trimmed.length < 2) return void 0;
|
|
32
|
+
return trimmed.slice(1, -1).replaceAll("%%", "%").replaceAll('\\"', '"').replaceAll("\\\\", "\\");
|
|
33
|
+
}
|
|
34
|
+
function bakedStateDir(unit) {
|
|
35
|
+
for (const line of unit.split("\n")) {
|
|
36
|
+
if (!line.startsWith("Environment=OURS_STATE_DIR=")) continue;
|
|
37
|
+
return systemdUnescapeArgument(line.slice("Environment=OURS_STATE_DIR=".length));
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
function describeConflict(existing, serviceFile, stateDir, verb = "overwrite") {
|
|
42
|
+
if (!existing.managed) {
|
|
43
|
+
return {
|
|
44
|
+
kind: "unmanaged",
|
|
45
|
+
serviceFile,
|
|
46
|
+
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`
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const owner = existing.owner;
|
|
50
|
+
if (owner !== void 0 && owner !== stateDir) {
|
|
51
|
+
return {
|
|
52
|
+
kind: "other-state-dir",
|
|
53
|
+
serviceFile,
|
|
54
|
+
owner,
|
|
55
|
+
message: `${serviceFile} is the boot service for the daemon at ${owner}, not ${stateDir}; refusing to ${verb} it without --force`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
function resolveStateDir(context) {
|
|
61
|
+
return resolve(context.stateDir ?? join(context.homeDir ?? homedir(), ".ours"));
|
|
62
|
+
}
|
|
63
|
+
function readSystemdUnit(previous) {
|
|
64
|
+
return { managed: previous.startsWith(MARKER), owner: bakedStateDir(previous) };
|
|
65
|
+
}
|
|
66
|
+
function createLinuxUserSystemdAdapter(deps = defaultDeps) {
|
|
67
|
+
if (deps.platform !== "linux") throw new Error(`service management is not supported on ${deps.platform}; use an external launcher for \`ours daemon serve\``);
|
|
68
|
+
return {
|
|
69
|
+
name: "systemd-user",
|
|
70
|
+
async install(context) {
|
|
71
|
+
const home = context.homeDir ?? homedir();
|
|
72
|
+
const stateDir = resolveStateDir(context);
|
|
73
|
+
const unitName = unitNameForStateDir(stateDir);
|
|
74
|
+
const serviceFile = join(home, ".config", "systemd", "user", unitName);
|
|
75
|
+
const unitConfig = context.configPath ? resolve(context.configPath) : void 0;
|
|
76
|
+
const args = [process.execPath, resolve(context.cliPath), "daemon", "serve"];
|
|
77
|
+
if (unitConfig) args.push("--config", unitConfig);
|
|
78
|
+
const environment = `Environment=OURS_STATE_DIR=${systemdEscapeArgument(stateDir)}
|
|
79
|
+
`;
|
|
80
|
+
const unit = `${MARKER}
|
|
81
|
+
[Unit]
|
|
82
|
+
Description=${serviceDescription(instanceNameFromStateDir(stateDir).name)}
|
|
83
|
+
After=network-online.target
|
|
84
|
+
|
|
85
|
+
[Service]
|
|
86
|
+
Type=simple
|
|
87
|
+
ExecStart=${args.map(systemdEscapeArgument).join(" ")}
|
|
88
|
+
${environment}Restart=on-failure
|
|
89
|
+
RestartSec=2
|
|
90
|
+
|
|
91
|
+
[Install]
|
|
92
|
+
WantedBy=default.target
|
|
93
|
+
`;
|
|
94
|
+
if (existsSync(serviceFile)) {
|
|
95
|
+
const previous = readFileSync(serviceFile, "utf8");
|
|
96
|
+
const conflict = describeConflict(readSystemdUnit(previous), serviceFile, stateDir);
|
|
97
|
+
if (conflict && !context.force) {
|
|
98
|
+
if (!context.dryRun) throw new Error(conflict.message);
|
|
99
|
+
return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands: [], changed: false, dryRun: true, conflict };
|
|
100
|
+
}
|
|
101
|
+
if (previous === unit) return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands: [], changed: false, dryRun: context.dryRun === true };
|
|
102
|
+
}
|
|
103
|
+
const commands = [["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "enable", "--now", unitName]];
|
|
104
|
+
if (!context.dryRun) {
|
|
105
|
+
mkdirSync(dirname(serviceFile), { recursive: true });
|
|
106
|
+
writeFileSync(serviceFile, unit, { mode: 420 });
|
|
107
|
+
for (const [command, ...args2] of commands) await deps.run(command, args2);
|
|
108
|
+
}
|
|
109
|
+
return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands, changed: true, dryRun: context.dryRun === true };
|
|
110
|
+
},
|
|
111
|
+
async uninstall(context) {
|
|
112
|
+
const home = context.homeDir ?? homedir();
|
|
113
|
+
const stateDir = resolveStateDir(context);
|
|
114
|
+
const unitName = unitNameForStateDir(stateDir);
|
|
115
|
+
const serviceFile = join(home, ".config", "systemd", "user", unitName);
|
|
116
|
+
if (!existsSync(serviceFile)) return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands: [], changed: false, dryRun: context.dryRun === true };
|
|
117
|
+
const previous = readFileSync(serviceFile, "utf8");
|
|
118
|
+
const existing = readSystemdUnit(previous);
|
|
119
|
+
const conflict = describeConflict(existing, serviceFile, stateDir, "remove");
|
|
120
|
+
if (conflict && !(context.force && existing.managed)) {
|
|
121
|
+
if (!context.dryRun) throw new Error(conflict.message);
|
|
122
|
+
return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands: [], changed: false, dryRun: true, conflict };
|
|
123
|
+
}
|
|
124
|
+
const commands = [["systemctl", "--user", "disable", "--now", unitName], ["systemctl", "--user", "daemon-reload"]];
|
|
125
|
+
if (!context.dryRun) {
|
|
126
|
+
await deps.run(commands[0][0], commands[0].slice(1));
|
|
127
|
+
unlinkSync(serviceFile);
|
|
128
|
+
await deps.run(commands[1][0], commands[1].slice(1));
|
|
129
|
+
}
|
|
130
|
+
return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands, changed: true, dryRun: context.dryRun === true };
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function xmlText(value) {
|
|
135
|
+
return String(value).replace(/[&<>"']/g, (character) => ({
|
|
136
|
+
"&": "&",
|
|
137
|
+
"<": "<",
|
|
138
|
+
">": ">",
|
|
139
|
+
'"': """,
|
|
140
|
+
"'": "'"
|
|
141
|
+
})[character]);
|
|
142
|
+
}
|
|
143
|
+
function xmlUntext(value) {
|
|
144
|
+
return value.replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&");
|
|
145
|
+
}
|
|
146
|
+
function bakedStateDirFromPlist(plist) {
|
|
147
|
+
const match = /<key>OURS_STATE_DIR<\/key><string>([^<]*)<\/string>/.exec(plist);
|
|
148
|
+
return match ? xmlUntext(match[1]) : void 0;
|
|
149
|
+
}
|
|
150
|
+
function readLaunchdPlist(previous) {
|
|
151
|
+
const managed = previous.split("\n")[1] === PLIST_MARKER;
|
|
152
|
+
return { managed, owner: bakedStateDirFromPlist(previous) };
|
|
153
|
+
}
|
|
154
|
+
function createMacosUserLaunchdAdapter(deps = defaultDeps) {
|
|
155
|
+
if (deps.platform !== "darwin") throw new Error(`service management is not supported on ${deps.platform}; use an external launcher for \`ours daemon serve\``);
|
|
156
|
+
const domain = `gui/${deps.uid ?? process.getuid?.() ?? 0}`;
|
|
157
|
+
const plan = (context) => {
|
|
158
|
+
const home = context.homeDir ?? homedir();
|
|
159
|
+
const stateDir = resolveStateDir(context);
|
|
160
|
+
const unitName = labelForStateDir(stateDir);
|
|
161
|
+
return { stateDir, unitName, serviceFile: join(home, "Library", "LaunchAgents", `${unitName}.plist`) };
|
|
162
|
+
};
|
|
163
|
+
return {
|
|
164
|
+
name: "launchd-user",
|
|
165
|
+
async install(context) {
|
|
166
|
+
const { stateDir, unitName, serviceFile } = plan(context);
|
|
167
|
+
const unitConfig = context.configPath ? resolve(context.configPath) : void 0;
|
|
168
|
+
const args = [process.execPath, resolve(context.cliPath), "daemon", "serve"];
|
|
169
|
+
if (unitConfig) args.push("--config", unitConfig);
|
|
170
|
+
const unit = `<?xml version="1.0" encoding="UTF-8"?>
|
|
171
|
+
${PLIST_MARKER}
|
|
172
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
173
|
+
<plist version="1.0">
|
|
174
|
+
<dict>
|
|
175
|
+
<key>Label</key><string>${xmlText(unitName)}</string>
|
|
176
|
+
<key>ProgramArguments</key>
|
|
177
|
+
<array>
|
|
178
|
+
${args.map((argument) => ` <string>${xmlText(argument)}</string>`).join("\n")}
|
|
179
|
+
</array>
|
|
180
|
+
<key>EnvironmentVariables</key>
|
|
181
|
+
<dict>
|
|
182
|
+
<key>OURS_STATE_DIR</key><string>${xmlText(stateDir)}</string>
|
|
183
|
+
</dict>
|
|
184
|
+
<key>RunAtLoad</key><true/>
|
|
185
|
+
<key>KeepAlive</key><true/>
|
|
186
|
+
</dict>
|
|
187
|
+
</plist>
|
|
188
|
+
`;
|
|
189
|
+
if (existsSync(serviceFile)) {
|
|
190
|
+
const previous = readFileSync(serviceFile, "utf8");
|
|
191
|
+
const conflict = describeConflict(readLaunchdPlist(previous), serviceFile, stateDir);
|
|
192
|
+
if (conflict && !context.force) {
|
|
193
|
+
if (!context.dryRun) throw new Error(conflict.message);
|
|
194
|
+
return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands: [], changed: false, dryRun: true, conflict };
|
|
195
|
+
}
|
|
196
|
+
if (previous === unit) return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands: [], changed: false, dryRun: context.dryRun === true };
|
|
197
|
+
}
|
|
198
|
+
const commands = [["launchctl", "bootout", domain, serviceFile], ["launchctl", "bootstrap", domain, serviceFile]];
|
|
199
|
+
if (!context.dryRun) {
|
|
200
|
+
mkdirSync(dirname(serviceFile), { recursive: true });
|
|
201
|
+
writeFileSync(serviceFile, unit, { mode: 420 });
|
|
202
|
+
await deps.run(commands[0][0], commands[0].slice(1)).catch(() => {
|
|
203
|
+
});
|
|
204
|
+
await deps.run(commands[1][0], commands[1].slice(1));
|
|
205
|
+
}
|
|
206
|
+
return { adapter: this.name, action: "install", serviceFile, unitName, stateDir, configPath: unitConfig, commands, changed: true, dryRun: context.dryRun === true };
|
|
207
|
+
},
|
|
208
|
+
async uninstall(context) {
|
|
209
|
+
const { stateDir, unitName, serviceFile } = plan(context);
|
|
210
|
+
if (!existsSync(serviceFile)) return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands: [], changed: false, dryRun: context.dryRun === true };
|
|
211
|
+
const previous = readFileSync(serviceFile, "utf8");
|
|
212
|
+
const existing = readLaunchdPlist(previous);
|
|
213
|
+
const conflict = describeConflict(existing, serviceFile, stateDir, "remove");
|
|
214
|
+
if (conflict && !(context.force && existing.managed)) {
|
|
215
|
+
if (!context.dryRun) throw new Error(conflict.message);
|
|
216
|
+
return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands: [], changed: false, dryRun: true, conflict };
|
|
217
|
+
}
|
|
218
|
+
const commands = [["launchctl", "bootout", domain, serviceFile]];
|
|
219
|
+
if (!context.dryRun) {
|
|
220
|
+
await deps.run(commands[0][0], commands[0].slice(1)).catch(() => {
|
|
221
|
+
});
|
|
222
|
+
unlinkSync(serviceFile);
|
|
223
|
+
}
|
|
224
|
+
return { adapter: this.name, action: "uninstall", serviceFile, unitName, stateDir, commands, changed: true, dryRun: context.dryRun === true };
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function createServiceAdapter(deps = defaultDeps) {
|
|
229
|
+
if (deps.platform === "linux") return createLinuxUserSystemdAdapter(deps);
|
|
230
|
+
if (deps.platform === "darwin") return createMacosUserLaunchdAdapter(deps);
|
|
231
|
+
throw new Error(`service management is not supported on ${deps.platform}; use an external launcher for \`ours daemon serve\``);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export {
|
|
235
|
+
SERVICE_NAME,
|
|
236
|
+
bakedStateDir,
|
|
237
|
+
createLinuxUserSystemdAdapter,
|
|
238
|
+
bakedStateDirFromPlist,
|
|
239
|
+
createMacosUserLaunchdAdapter,
|
|
240
|
+
createServiceAdapter
|
|
241
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/service-instance.ts
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
var DEFAULT_SYSTEMD_UNIT = "ours.service";
|
|
4
|
+
var DEFAULT_LAUNCHD_LABEL = "solutions.adaptframework.ours";
|
|
4
5
|
var INSTANCE_RE = /^[A-Za-z0-9](?:[A-Za-z0-9_-]{0,30}[A-Za-z0-9])?$/;
|
|
5
6
|
function normalizeInstanceName(raw) {
|
|
6
7
|
const s = typeof raw === "string" ? raw.trim() : "";
|
|
@@ -22,6 +23,11 @@ function systemdUnitName(instance = "") {
|
|
|
22
23
|
if (!ok || !name) return DEFAULT_SYSTEMD_UNIT;
|
|
23
24
|
return `ours-${name}.service`;
|
|
24
25
|
}
|
|
26
|
+
function launchdLabel(instance = "") {
|
|
27
|
+
const { ok, name } = normalizeInstanceName(instance);
|
|
28
|
+
if (!ok || !name) return DEFAULT_LAUNCHD_LABEL;
|
|
29
|
+
return `${DEFAULT_LAUNCHD_LABEL}.${name}`;
|
|
30
|
+
}
|
|
25
31
|
function serviceDescription(instance = "") {
|
|
26
32
|
const { ok, name } = normalizeInstanceName(instance);
|
|
27
33
|
return ok && name ? `ours daemon (instance "${name}")` : "ours shared daemon";
|
|
@@ -46,12 +52,20 @@ function unitNameForStateDir(stateDir) {
|
|
|
46
52
|
if (!ok) throw new Error(reason);
|
|
47
53
|
return systemdUnitName(name);
|
|
48
54
|
}
|
|
55
|
+
function labelForStateDir(stateDir) {
|
|
56
|
+
const { ok, name, reason } = instanceNameFromStateDir(stateDir);
|
|
57
|
+
if (!ok) throw new Error(reason);
|
|
58
|
+
return launchdLabel(name);
|
|
59
|
+
}
|
|
49
60
|
|
|
50
61
|
export {
|
|
51
62
|
DEFAULT_SYSTEMD_UNIT,
|
|
63
|
+
DEFAULT_LAUNCHD_LABEL,
|
|
52
64
|
normalizeInstanceName,
|
|
53
65
|
systemdUnitName,
|
|
66
|
+
launchdLabel,
|
|
54
67
|
serviceDescription,
|
|
55
68
|
instanceNameFromStateDir,
|
|
56
|
-
unitNameForStateDir
|
|
69
|
+
unitNameForStateDir,
|
|
70
|
+
labelForStateDir
|
|
57
71
|
};
|
package/dist/commands.d.ts
CHANGED
|
@@ -64,14 +64,14 @@ export declare const COMMAND_GROUPS: {
|
|
|
64
64
|
readonly 'install-service': {
|
|
65
65
|
readonly kind: "daemon";
|
|
66
66
|
readonly handler: "install-service";
|
|
67
|
-
readonly summary: "Install and enable the Linux
|
|
68
|
-
readonly effects: "Changes ~/.config/systemd/user/ours.service and runs systemctl --user. Requires --yes unless --dry-run is used; --force may replace only an existing unowned unit.";
|
|
67
|
+
readonly summary: "Install and enable the per-user boot service (Linux systemd, macOS launchd).";
|
|
68
|
+
readonly effects: "Changes ~/.config/systemd/user/ours.service and runs systemctl --user on Linux, or ~/Library/LaunchAgents and launchctl on macOS. Requires --yes unless --dry-run is used; --force may replace only an existing unowned unit.";
|
|
69
69
|
readonly example: "ours daemon install-service --dry-run --json";
|
|
70
70
|
};
|
|
71
71
|
readonly 'uninstall-service': {
|
|
72
72
|
readonly kind: "daemon";
|
|
73
73
|
readonly handler: "uninstall-service";
|
|
74
|
-
readonly summary: "Disable and remove the
|
|
74
|
+
readonly summary: "Disable and remove the per-user boot service owned by this CLI.";
|
|
75
75
|
readonly effects: "Changes the user service manager. Requires --yes unless --dry-run is used and refuses to remove an unowned unit.";
|
|
76
76
|
readonly example: "ours daemon uninstall-service --dry-run --json";
|
|
77
77
|
};
|
package/dist/commands.js
CHANGED
package/dist/help.js
CHANGED
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
helpRequestPath,
|
|
4
4
|
renderHelp,
|
|
5
5
|
renderTopHelp
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RJWUCR3R.js";
|
|
7
7
|
import "./chunk-QRNZFPNJ.js";
|
|
8
8
|
import "./chunk-6NFATG6P.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-SDNTPCKW.js";
|
|
10
10
|
export {
|
|
11
11
|
helpHint,
|
|
12
12
|
helpRequestPath,
|
package/dist/main.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
writeResult
|
|
5
5
|
} from "./chunk-YC7QBGVC.js";
|
|
6
6
|
import {
|
|
7
|
-
|
|
8
|
-
} from "./chunk-
|
|
7
|
+
createServiceAdapter
|
|
8
|
+
} from "./chunk-TNNBVUKK.js";
|
|
9
9
|
import {
|
|
10
10
|
instanceNameFromStateDir
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UZQWSOUB.js";
|
|
12
12
|
import {
|
|
13
13
|
setupConfig,
|
|
14
14
|
showConfig
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
helpHint,
|
|
18
18
|
helpRequestPath,
|
|
19
19
|
renderHelp
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-RJWUCR3R.js";
|
|
21
21
|
import {
|
|
22
22
|
invokeOperation,
|
|
23
23
|
isOperationName,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
IDENTITY_PREBIND_EXCEPTIONS,
|
|
39
39
|
commandSpec,
|
|
40
40
|
isCommandGroup
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-SDNTPCKW.js";
|
|
42
42
|
import {
|
|
43
43
|
inspectDaemon,
|
|
44
44
|
serveDaemon,
|
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
import { existsSync, readFileSync } from "node:fs";
|
|
56
56
|
import { homedir } from "node:os";
|
|
57
57
|
import { join, resolve } from "node:path";
|
|
58
|
-
var CLI_VERSION = false ? "0.0.0-dev" : "0.
|
|
58
|
+
var CLI_VERSION = false ? "0.0.0-dev" : "0.6.0";
|
|
59
59
|
var COMMON_VALUES = /* @__PURE__ */ new Set(["--endpoint", "--port", "--state-dir", "--config", "--identity"]);
|
|
60
60
|
var COMMON_BOOLEANS = /* @__PURE__ */ new Set(["--json", "--yes", "--help"]);
|
|
61
61
|
function selectionFrom(values) {
|
|
@@ -227,7 +227,7 @@ async function runDaemon(args, io) {
|
|
|
227
227
|
const derived = instanceNameFromStateDir(target.stateDir);
|
|
228
228
|
if (!derived.ok) throw new CliUsageError(derived.reason ?? `cannot derive a service name from ${target.stateDir}`);
|
|
229
229
|
assertPortAgrees(selection.port, target);
|
|
230
|
-
const adapter =
|
|
230
|
+
const adapter = createServiceAdapter();
|
|
231
231
|
const context = { cliPath: process.argv[1], configPath: target.unitConfigPath, stateDir: target.stateDir, dryRun, force: flags.booleans.has("--force") };
|
|
232
232
|
const result = handler === "install-service" ? await adapter.install(context) : await adapter.uninstall(context);
|
|
233
233
|
writeResult(io, result, json, handler);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export declare const DEFAULT_SYSTEMD_UNIT = "ours.service";
|
|
2
|
+
export declare const DEFAULT_LAUNCHD_LABEL = "solutions.adaptframework.ours";
|
|
2
3
|
export interface InstanceResult {
|
|
3
4
|
ok: boolean;
|
|
4
5
|
/** '' means "the default, unnamed daemon" — the historical single-unit behaviour. */
|
|
@@ -7,6 +8,8 @@ export interface InstanceResult {
|
|
|
7
8
|
}
|
|
8
9
|
export declare function normalizeInstanceName(raw: unknown): InstanceResult;
|
|
9
10
|
export declare function systemdUnitName(instance?: string): string;
|
|
11
|
+
export declare function launchdLabel(instance?: string): string;
|
|
10
12
|
export declare function serviceDescription(instance?: string): string;
|
|
11
13
|
export declare function instanceNameFromStateDir(stateDir: string): InstanceResult;
|
|
12
14
|
export declare function unitNameForStateDir(stateDir: string): string;
|
|
15
|
+
export declare function labelForStateDir(stateDir: string): string;
|
package/dist/service-instance.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_LAUNCHD_LABEL,
|
|
2
3
|
DEFAULT_SYSTEMD_UNIT,
|
|
3
4
|
instanceNameFromStateDir,
|
|
5
|
+
labelForStateDir,
|
|
6
|
+
launchdLabel,
|
|
4
7
|
normalizeInstanceName,
|
|
5
8
|
serviceDescription,
|
|
6
9
|
systemdUnitName,
|
|
7
10
|
unitNameForStateDir
|
|
8
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UZQWSOUB.js";
|
|
9
12
|
export {
|
|
13
|
+
DEFAULT_LAUNCHD_LABEL,
|
|
10
14
|
DEFAULT_SYSTEMD_UNIT,
|
|
11
15
|
instanceNameFromStateDir,
|
|
16
|
+
labelForStateDir,
|
|
17
|
+
launchdLabel,
|
|
12
18
|
normalizeInstanceName,
|
|
13
19
|
serviceDescription,
|
|
14
20
|
systemdUnitName,
|
package/dist/service.d.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface ServiceContext {
|
|
|
47
47
|
export interface ServiceDeps {
|
|
48
48
|
platform: NodeJS.Platform;
|
|
49
49
|
run(command: string, args: string[]): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* launchd only — the uid whose GUI domain the agent is bootstrapped into.
|
|
52
|
+
* Injected rather than read from `process.getuid()` so the domain a plan
|
|
53
|
+
* targets is assertable off macOS, where the rest of this file is developed.
|
|
54
|
+
*/
|
|
55
|
+
uid?: number;
|
|
50
56
|
}
|
|
51
57
|
export interface ServiceAdapter {
|
|
52
58
|
readonly name: string;
|
|
@@ -58,4 +64,28 @@ export interface ServiceAdapter {
|
|
|
58
64
|
* unit written before this CLI baked one in.
|
|
59
65
|
*/
|
|
60
66
|
export declare function bakedStateDir(unit: string): string | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* What an already-present service definition says about itself, in the terms
|
|
69
|
+
* the guard needs: did this CLI write it, and which daemon is it for. Reading
|
|
70
|
+
* it is per-format; judging it is not, which is what keeps the systemd and
|
|
71
|
+
* launchd guards from drifting into two different sets of refusals.
|
|
72
|
+
*/
|
|
73
|
+
export interface ExistingDefinition {
|
|
74
|
+
managed: boolean;
|
|
75
|
+
/** The baked state directory, or undefined for a definition that bakes none. */
|
|
76
|
+
owner?: string;
|
|
77
|
+
}
|
|
61
78
|
export declare function createLinuxUserSystemdAdapter(deps?: ServiceDeps): ServiceAdapter;
|
|
79
|
+
/**
|
|
80
|
+
* The state directory a CLI-managed launchd agent was installed for. The plist
|
|
81
|
+
* counterpart of `bakedStateDir`, and the reason a label — which is not
|
|
82
|
+
* injective over state directories — is still enough to find its daemon.
|
|
83
|
+
*/
|
|
84
|
+
export declare function bakedStateDirFromPlist(plist: string): string | undefined;
|
|
85
|
+
export declare function createMacosUserLaunchdAdapter(deps?: ServiceDeps): ServiceAdapter;
|
|
86
|
+
/**
|
|
87
|
+
* The adapter for the platform this CLI is running on. The two factories keep
|
|
88
|
+
* refusing every platform but their own — the selector is the only thing that
|
|
89
|
+
* is allowed to know which platform maps to which service manager.
|
|
90
|
+
*/
|
|
91
|
+
export declare function createServiceAdapter(deps?: ServiceDeps): ServiceAdapter;
|
package/dist/service.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SERVICE_NAME,
|
|
3
3
|
bakedStateDir,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
bakedStateDirFromPlist,
|
|
5
|
+
createLinuxUserSystemdAdapter,
|
|
6
|
+
createMacosUserLaunchdAdapter,
|
|
7
|
+
createServiceAdapter
|
|
8
|
+
} from "./chunk-TNNBVUKK.js";
|
|
9
|
+
import "./chunk-UZQWSOUB.js";
|
|
7
10
|
export {
|
|
8
11
|
SERVICE_NAME,
|
|
9
12
|
bakedStateDir,
|
|
10
|
-
|
|
13
|
+
bakedStateDirFromPlist,
|
|
14
|
+
createLinuxUserSystemdAdapter,
|
|
15
|
+
createMacosUserLaunchdAdapter,
|
|
16
|
+
createServiceAdapter
|
|
11
17
|
};
|
package/package.json
CHANGED
package/dist/chunk-227DBTDK.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
};
|