@ours.network/cli 0.6.0 → 1.0.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/README.md +13 -13
- package/dist/boot-35BOHNSZ.js +9 -0
- package/dist/{chunk-6W3RHW3C.js → chunk-6K2JBKHI.js} +5 -6
- package/dist/{chunk-G2GHHOYX.js → chunk-DRQEV4SN.js} +2 -2
- package/dist/chunk-FXKFSNKS.js +166 -0
- package/dist/chunk-MHVYJY3H.js +2530 -0
- package/dist/{chunk-4IFFMMQO.js → chunk-QZIL3IT7.js} +3 -3
- package/dist/{chunk-RJWUCR3R.js → chunk-ZFE47KH3.js} +0 -1
- package/dist/config-command.js +2 -2
- package/dist/connection.js +1 -1
- package/dist/daemon-UUG6P6GK.js +250 -0
- package/dist/help.js +1 -1
- package/dist/lifecycle.js +2 -2
- package/dist/main.js +6 -8
- package/dist/server-TDJBEWTU.js +2077 -0
- package/package.json +4 -2
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
attachClient,
|
|
3
3
|
defaultConfigPath,
|
|
4
4
|
resolveSelection
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-6K2JBKHI.js";
|
|
6
6
|
|
|
7
7
|
// src/lifecycle.ts
|
|
8
8
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
@@ -54,7 +54,7 @@ async function inspectDaemon(config, deps = depsDefault) {
|
|
|
54
54
|
try {
|
|
55
55
|
client = await attachClient(config, { fetch: deps.fetch, leaseToken: "ours-cli-status" });
|
|
56
56
|
} catch (error) {
|
|
57
|
-
if (error instanceof Error && error.name === "
|
|
57
|
+
if (error instanceof Error && error.name === "DaemonUnavailableError") {
|
|
58
58
|
return { state: "stopped", managed: false, info: null, ...base };
|
|
59
59
|
}
|
|
60
60
|
throw error;
|
|
@@ -87,7 +87,7 @@ function childEnvironment(selection) {
|
|
|
87
87
|
async function serveDaemon(selection, managed = false) {
|
|
88
88
|
if (selection.endpoint !== void 0) throw new Error("--endpoint selects a running daemon and cannot be used with daemon serve");
|
|
89
89
|
Object.assign(process.env, childEnvironment(selection));
|
|
90
|
-
const { startDaemon } = await import("
|
|
90
|
+
const { startDaemon } = await import("./daemon-UUG6P6GK.js");
|
|
91
91
|
const handle = await startDaemon();
|
|
92
92
|
if (managed) {
|
|
93
93
|
const response = await fetch(`http://127.0.0.1:${handle.port}/state-dir`);
|
|
@@ -126,7 +126,6 @@ function nativeConfigOptions(action) {
|
|
|
126
126
|
["--identity NAME", "Compatibility-only/ignored by config commands."],
|
|
127
127
|
["--broker-url URL", show ? "Compatibility-only/ignored by config show." : "Set the broker WebSocket URL."],
|
|
128
128
|
["--gc-interval-ms N", show ? "Compatibility-only/ignored by config show." : "Set a positive garbage-collection interval in milliseconds."],
|
|
129
|
-
["--auto-start true|false", show ? "Compatibility-only/ignored by config show." : "Set automatic daemon startup (1/0 are also accepted)."],
|
|
130
129
|
["--api-visibility MODE", show ? "Compatibility-only/ignored by config show." : "Set owner, shared, or open API visibility."],
|
|
131
130
|
["--dry-run", show ? "Compatibility-only/ignored by config show." : "Return the merged redacted result without writing the file."],
|
|
132
131
|
["--json", "Write one JSON value to stdout."],
|
package/dist/config-command.js
CHANGED
package/dist/connection.js
CHANGED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertConfigNotDrifted,
|
|
3
|
+
loadConfig
|
|
4
|
+
} from "./chunk-FXKFSNKS.js";
|
|
5
|
+
|
|
6
|
+
// ../../src/internal/daemon.ts
|
|
7
|
+
import * as fs2 from "node:fs";
|
|
8
|
+
|
|
9
|
+
// ../../src/internal/state-root-lock.ts
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
import * as net from "node:net";
|
|
13
|
+
import { dirname, join, resolve } from "node:path";
|
|
14
|
+
var SUPPORTED_PLATFORMS = /* @__PURE__ */ new Set(["linux", "darwin"]);
|
|
15
|
+
var MAX_SOCKET_PATH_BYTES = 103;
|
|
16
|
+
var MAX_ACQUIRE_ATTEMPTS = 8;
|
|
17
|
+
var StateRootLockedError = class extends Error {
|
|
18
|
+
constructor(stateDir, ownerPid, owner) {
|
|
19
|
+
super(
|
|
20
|
+
`ours daemon state directory ${JSON.stringify(stateDir)} is already owned by ` + (ownerPid === null ? "another live process" : `pid ${ownerPid}`) + ". Stop that daemon through the ours CLI before starting another one for the same state directory."
|
|
21
|
+
);
|
|
22
|
+
this.stateDir = stateDir;
|
|
23
|
+
this.ownerPid = ownerPid;
|
|
24
|
+
this.owner = owner;
|
|
25
|
+
this.name = "StateRootLockedError";
|
|
26
|
+
}
|
|
27
|
+
code = "STATE_ROOT_LOCKED";
|
|
28
|
+
};
|
|
29
|
+
function fsCode(error) {
|
|
30
|
+
return error && typeof error === "object" && "code" in error ? String(error.code) : void 0;
|
|
31
|
+
}
|
|
32
|
+
function canonicalStateDir(input) {
|
|
33
|
+
let cursor = resolve(input);
|
|
34
|
+
const missing = [];
|
|
35
|
+
while (true) {
|
|
36
|
+
try {
|
|
37
|
+
const canonicalPrefix = fs.realpathSync.native(cursor);
|
|
38
|
+
return resolve(canonicalPrefix, ...missing.reverse());
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (fsCode(error) !== "ENOENT") throw error;
|
|
41
|
+
const parent = dirname(cursor);
|
|
42
|
+
if (parent === cursor) throw error;
|
|
43
|
+
missing.push(cursor.slice(parent.length + (parent.endsWith("/") ? 0 : 1)));
|
|
44
|
+
cursor = parent;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function ensurePrivateDirectory(path, uid) {
|
|
49
|
+
try {
|
|
50
|
+
fs.mkdirSync(path, { mode: 448 });
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (fsCode(error) !== "EEXIST") throw error;
|
|
53
|
+
}
|
|
54
|
+
const stat = fs.lstatSync(path);
|
|
55
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
56
|
+
throw new Error(`daemon lock path ${JSON.stringify(path)} must be a real directory, not a symlink or file`);
|
|
57
|
+
}
|
|
58
|
+
if (stat.uid !== uid) {
|
|
59
|
+
throw new Error(`daemon lock path ${JSON.stringify(path)} is owned by uid ${stat.uid}, expected uid ${uid}`);
|
|
60
|
+
}
|
|
61
|
+
fs.chmodSync(path, 448);
|
|
62
|
+
}
|
|
63
|
+
function stateRootLockPath(stateDir) {
|
|
64
|
+
if (!SUPPORTED_PLATFORMS.has(process.platform)) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`the ours daemon state-root lock supports Linux and macOS only; platform ${JSON.stringify(process.platform)} has no verified Unix-domain-socket lock contract, so startup is refused`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const getuid = process.getuid;
|
|
70
|
+
if (typeof getuid !== "function") {
|
|
71
|
+
throw new Error("the ours daemon state-root lock requires a Unix uid; startup is refused");
|
|
72
|
+
}
|
|
73
|
+
const uid = getuid.call(process);
|
|
74
|
+
const base = `/tmp/ours-${uid}`;
|
|
75
|
+
const directory = join(base, "locks");
|
|
76
|
+
ensurePrivateDirectory(base, uid);
|
|
77
|
+
ensurePrivateDirectory(directory, uid);
|
|
78
|
+
const key = createHash("sha256").update(stateDir).digest("hex");
|
|
79
|
+
const socketPath = join(directory, `${key}.sock`);
|
|
80
|
+
if (Buffer.byteLength(socketPath) > MAX_SOCKET_PATH_BYTES) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`daemon lock socket path is ${Buffer.byteLength(socketPath)} bytes, exceeding the portable ${MAX_SOCKET_PATH_BYTES}-byte Linux/macOS contract: ${JSON.stringify(socketPath)}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return { directory, socketPath };
|
|
86
|
+
}
|
|
87
|
+
function probeSocket(socketPath) {
|
|
88
|
+
return new Promise((resolveProbe) => {
|
|
89
|
+
const socket = net.createConnection(socketPath);
|
|
90
|
+
let data = "";
|
|
91
|
+
let settled = false;
|
|
92
|
+
const finish = (result) => {
|
|
93
|
+
if (settled) return;
|
|
94
|
+
settled = true;
|
|
95
|
+
socket.destroy();
|
|
96
|
+
resolveProbe(result);
|
|
97
|
+
};
|
|
98
|
+
socket.setEncoding("utf8");
|
|
99
|
+
socket.setTimeout(1e3);
|
|
100
|
+
socket.on("data", (chunk) => {
|
|
101
|
+
data += chunk;
|
|
102
|
+
});
|
|
103
|
+
socket.once("end", () => {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(data);
|
|
106
|
+
const owner = Number.isInteger(parsed.pid) && typeof parsed.stateDir === "string" && typeof parsed.startedAt === "string" ? parsed : null;
|
|
107
|
+
finish({ kind: "live", owner });
|
|
108
|
+
} catch {
|
|
109
|
+
finish({ kind: "live", owner: null });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
socket.once("timeout", () => finish({ kind: "ambiguous", error: new Error("timed out probing the live lock socket") }));
|
|
113
|
+
socket.once("error", (error) => {
|
|
114
|
+
const code = fsCode(error);
|
|
115
|
+
if (code === "ECONNREFUSED" || code === "ENOENT") finish({ kind: "stale" });
|
|
116
|
+
else finish({ kind: "ambiguous", error });
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function listen(server, socketPath) {
|
|
121
|
+
return new Promise((resolveListen, reject) => {
|
|
122
|
+
const onError = (error) => reject(error);
|
|
123
|
+
server.once("error", onError);
|
|
124
|
+
server.listen(socketPath, () => {
|
|
125
|
+
server.off("error", onError);
|
|
126
|
+
resolveListen();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
async function acquireStateRootLock(inputStateDir) {
|
|
131
|
+
const stateDir = canonicalStateDir(inputStateDir);
|
|
132
|
+
const { socketPath } = stateRootLockPath(stateDir);
|
|
133
|
+
const owner = { pid: process.pid, stateDir, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
134
|
+
for (let attempt = 1; attempt <= MAX_ACQUIRE_ATTEMPTS; attempt++) {
|
|
135
|
+
const server = net.createServer((socket) => socket.end(`${JSON.stringify(owner)}
|
|
136
|
+
`));
|
|
137
|
+
try {
|
|
138
|
+
await listen(server, socketPath);
|
|
139
|
+
server.unref();
|
|
140
|
+
let closed = false;
|
|
141
|
+
return {
|
|
142
|
+
stateDir,
|
|
143
|
+
socketPath,
|
|
144
|
+
owner,
|
|
145
|
+
close: async () => {
|
|
146
|
+
if (closed) return;
|
|
147
|
+
closed = true;
|
|
148
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
149
|
+
try {
|
|
150
|
+
fs.unlinkSync(socketPath);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
if (fsCode(error) !== "ENOENT") throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
} catch (error) {
|
|
157
|
+
try {
|
|
158
|
+
server.close();
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
if (fsCode(error) !== "EADDRINUSE") throw error;
|
|
162
|
+
const probe = await probeSocket(socketPath);
|
|
163
|
+
if (probe.kind === "live") {
|
|
164
|
+
throw new StateRootLockedError(stateDir, probe.owner?.pid ?? null, probe.owner);
|
|
165
|
+
}
|
|
166
|
+
if (probe.kind === "ambiguous") {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`could not prove whether daemon lock ${JSON.stringify(socketPath)} is live or stale; refusing to remove it: ${probe.error.message}`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
fs.unlinkSync(socketPath);
|
|
173
|
+
} catch (unlinkError) {
|
|
174
|
+
if (fsCode(unlinkError) !== "ENOENT") throw unlinkError;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw new Error(`could not acquire daemon state-root lock for ${JSON.stringify(stateDir)} after ${MAX_ACQUIRE_ATTEMPTS} races`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ../../src/internal/daemon.ts
|
|
182
|
+
var ENTRY_CONFIG = loadConfig();
|
|
183
|
+
var heldLock = null;
|
|
184
|
+
async function ensureLock() {
|
|
185
|
+
assertConfigNotDrifted();
|
|
186
|
+
if (heldLock) return { lock: heldLock, acquired: false };
|
|
187
|
+
const lock = await acquireStateRootLock(ENTRY_CONFIG.stateDir);
|
|
188
|
+
fs2.mkdirSync(lock.stateDir, { recursive: true, mode: 448 });
|
|
189
|
+
const actual = fs2.realpathSync.native(lock.stateDir);
|
|
190
|
+
if (actual !== lock.stateDir) {
|
|
191
|
+
await lock.close();
|
|
192
|
+
throw new Error(
|
|
193
|
+
`state directory canonical path changed during lock acquisition: locked ${JSON.stringify(lock.stateDir)}, created ${JSON.stringify(actual)}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
heldLock = lock;
|
|
197
|
+
return { lock, acquired: true };
|
|
198
|
+
}
|
|
199
|
+
async function releaseHeld(lock) {
|
|
200
|
+
if (heldLock !== lock) return;
|
|
201
|
+
heldLock = null;
|
|
202
|
+
await lock.close();
|
|
203
|
+
}
|
|
204
|
+
async function bootKernel() {
|
|
205
|
+
const { lock, acquired } = await ensureLock();
|
|
206
|
+
try {
|
|
207
|
+
const { bootWrapper } = await import("./boot-35BOHNSZ.js");
|
|
208
|
+
await bootWrapper();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
if (acquired) await releaseHeld(lock);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function startDaemon(opts = {}) {
|
|
215
|
+
const { lock, acquired } = await ensureLock();
|
|
216
|
+
try {
|
|
217
|
+
const runtime = await import("./server-TDJBEWTU.js");
|
|
218
|
+
const { onRuntimeLoaded, ...daemonOptions } = opts;
|
|
219
|
+
onRuntimeLoaded?.();
|
|
220
|
+
const handle = await runtime.startDaemon({
|
|
221
|
+
...daemonOptions,
|
|
222
|
+
// The server owns its existing signal behavior. This callback releases
|
|
223
|
+
// the lock after teardown and before its signal path exits the process.
|
|
224
|
+
onShutdown: async () => releaseHeld(lock)
|
|
225
|
+
});
|
|
226
|
+
let closed = false;
|
|
227
|
+
return {
|
|
228
|
+
port: handle.port,
|
|
229
|
+
close: async () => {
|
|
230
|
+
if (closed) return;
|
|
231
|
+
closed = true;
|
|
232
|
+
try {
|
|
233
|
+
await handle.close();
|
|
234
|
+
} finally {
|
|
235
|
+
await releaseHeld(lock);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (acquired) await releaseHeld(lock);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
export {
|
|
245
|
+
StateRootLockedError,
|
|
246
|
+
bootKernel,
|
|
247
|
+
canonicalStateDir,
|
|
248
|
+
startDaemon,
|
|
249
|
+
stateRootLockPath
|
|
250
|
+
};
|
package/dist/help.js
CHANGED
package/dist/lifecycle.js
CHANGED
package/dist/main.js
CHANGED
|
@@ -12,12 +12,12 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
setupConfig,
|
|
14
14
|
showConfig
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-DRQEV4SN.js";
|
|
16
16
|
import {
|
|
17
17
|
helpHint,
|
|
18
18
|
helpRequestPath,
|
|
19
19
|
renderHelp
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-ZFE47KH3.js";
|
|
21
21
|
import {
|
|
22
22
|
invokeOperation,
|
|
23
23
|
isOperationName,
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
} from "./chunk-QRNZFPNJ.js";
|
|
30
30
|
import {
|
|
31
31
|
CliUsageError,
|
|
32
|
-
parseBoolean,
|
|
33
32
|
parseFlags,
|
|
34
33
|
parseInteger
|
|
35
34
|
} from "./chunk-6NFATG6P.js";
|
|
@@ -44,18 +43,18 @@ import {
|
|
|
44
43
|
serveDaemon,
|
|
45
44
|
startDaemonManaged,
|
|
46
45
|
stopDaemonManaged
|
|
47
|
-
} from "./chunk-
|
|
46
|
+
} from "./chunk-QZIL3IT7.js";
|
|
48
47
|
import {
|
|
49
48
|
attachClient,
|
|
50
49
|
defaultConfigPath,
|
|
51
50
|
resolveSelection
|
|
52
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-6K2JBKHI.js";
|
|
53
52
|
|
|
54
53
|
// src/main.ts
|
|
55
54
|
import { existsSync, readFileSync } from "node:fs";
|
|
56
55
|
import { homedir } from "node:os";
|
|
57
56
|
import { join, resolve } from "node:path";
|
|
58
|
-
var CLI_VERSION = false ? "0.0.0-dev" : "0.
|
|
57
|
+
var CLI_VERSION = false ? "0.0.0-dev" : "1.0.0";
|
|
59
58
|
var COMMON_VALUES = /* @__PURE__ */ new Set(["--endpoint", "--port", "--state-dir", "--config", "--identity"]);
|
|
60
59
|
var COMMON_BOOLEANS = /* @__PURE__ */ new Set(["--json", "--yes", "--help"]);
|
|
61
60
|
function selectionFrom(values) {
|
|
@@ -163,7 +162,7 @@ async function runGrouped(group, args, io) {
|
|
|
163
162
|
return 0;
|
|
164
163
|
}
|
|
165
164
|
async function runConfig(args, io) {
|
|
166
|
-
const values = /* @__PURE__ */ new Set([...COMMON_VALUES, "--broker-url", "--gc-interval-ms", "--
|
|
165
|
+
const values = /* @__PURE__ */ new Set([...COMMON_VALUES, "--broker-url", "--gc-interval-ms", "--api-visibility"]);
|
|
167
166
|
const flags = parseFlags(args, values, /* @__PURE__ */ new Set(["--json", "--dry-run", "--help"]));
|
|
168
167
|
const action = flags.positionals[0];
|
|
169
168
|
const spec = action === void 0 ? void 0 : commandSpec("config", action);
|
|
@@ -178,7 +177,6 @@ async function runConfig(args, io) {
|
|
|
178
177
|
if (flags.values["--port"]) patch.port = parseInteger(flags.values["--port"], "--port", 1, 65535);
|
|
179
178
|
if (flags.values["--state-dir"]) patch.stateDir = resolve(flags.values["--state-dir"]);
|
|
180
179
|
if (flags.values["--gc-interval-ms"]) patch.gcIntervalMs = parseInteger(flags.values["--gc-interval-ms"], "--gc-interval-ms", 1);
|
|
181
|
-
if (flags.values["--auto-start"]) patch.autoStart = parseBoolean(flags.values["--auto-start"], "--auto-start");
|
|
182
180
|
if (flags.values["--api-visibility"]) {
|
|
183
181
|
const visibility = flags.values["--api-visibility"];
|
|
184
182
|
if (!["owner", "shared", "open"].includes(visibility)) throw new CliUsageError("--api-visibility must be owner, shared, or open");
|