@ours.network/cli 0.1.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.
@@ -0,0 +1,152 @@
1
+ import {
2
+ CliUsageError,
3
+ parseBoolean
4
+ } from "./chunk-AXXFFER2.js";
5
+
6
+ // src/operations.ts
7
+ var str = (required = false, values) => ({ kind: "string", required, values });
8
+ var bool = (required = false) => ({ kind: "boolean", required });
9
+ var integer = (required = false, positive = false) => ({ kind: "integer", required, positive });
10
+ var strings = (required = false) => ({ kind: "string[]", required });
11
+ var integers = (required = false) => ({ kind: "integer[]", required });
12
+ var none = {};
13
+ var OPERATION_SPECS = {
14
+ "create-identity": { method: "createIdentity", fields: { name: str(true), bio: str(true), exposeLocal: bool(true), localAutoAccept: bool(true) } },
15
+ "create-temporary-identity": { method: "createTemporaryIdentity", fields: { name: str(), bio: str(true), exposeLocal: bool(true), localAutoAccept: bool(true) } },
16
+ "close-temporary-identity": { method: "closeTemporaryIdentityOp", fields: { name: str() } },
17
+ "create-root-identity": { method: "createRootIdentity", fields: { name: str(true), bio: str(true), exposeLocal: bool(true), localAutoAccept: bool(true), skipIfRootExists: bool(true) } },
18
+ "define-local-identity-file": { method: "defineLocalIdentityFile", fields: { name: str(true), path: str(true), force: bool(true), exposeLocal: bool(true), localAutoAccept: bool(true), overwrite: bool(true) } },
19
+ "choose-identity": { method: "chooseIdentity", fields: { name: str(true), force: bool(true) } },
20
+ "list-identities": { method: "listIdentities", fields: none },
21
+ "current-identity": { method: "currentIdentity", fields: none },
22
+ "remove-identity": { method: "removeIdentity", fields: { name: str(true) } },
23
+ "release-lease": { method: "releaseLease", fields: none },
24
+ "generate-invite": { method: "generateInvite", fields: { name: str(), mode: str(false, ["one_time", "public"]) } },
25
+ "list-invites": { method: "listInvites", fields: none },
26
+ "revoke-invite": { method: "revokeInvite", fields: { invite_id: str(true) } },
27
+ "add-contact": { method: "addContact", fields: { invite: str(true), name: str() } },
28
+ "list-contacts": { method: "listContacts", fields: none },
29
+ "list-local-contact-book": { method: "listLocalContactBook", fields: none },
30
+ "set-local-book-policy": {
31
+ method: "setLocalBookPolicy",
32
+ fields: { expose: bool(), auto_accept: bool() },
33
+ refine: (a) => {
34
+ if (a.expose === void 0 && a.auto_accept === void 0) throw new CliUsageError("set-local-book-policy requires expose and/or auto_accept");
35
+ }
36
+ },
37
+ "remove-contact": { method: "removeContact", fields: { contact: str(true) } },
38
+ "rename-contact": { method: "renameContact", fields: { contact: str(true), name: str(true) } },
39
+ "respond-to-introduction": { method: "respondToIntroduction", fields: { contact: str(true), action: str(true, ["approve", "reject"]) } },
40
+ "set-bio": { method: "setBio", fields: { bio: str(true) } },
41
+ "set-persona": { method: "setPersona", fields: { persona: str(true) } },
42
+ "send-message": { method: "sendMessage", fields: { contact: str(true), text: str(true), reply_to_wire_id: str(), reply_to_sentence: integer(false, true) } },
43
+ "send-file": {
44
+ method: "sendFile",
45
+ fields: { contact: str(true), path: str(), data_base64: str(), filename: str(), mime: str(), reply_to_wire_id: str(), reply_to_sentence: integer(false, true) },
46
+ refine: (a) => {
47
+ if (a.path === void 0 === (a.data_base64 === void 0)) throw new CliUsageError("send-file requires exactly one of path or data_base64");
48
+ if (a.data_base64 !== void 0 && a.filename === void 0) throw new CliUsageError("send-file requires filename with data_base64");
49
+ }
50
+ },
51
+ "list-incoming-messages": { method: "listIncomingMessages", fields: none },
52
+ "get-messages": { method: "getMessages", fields: none },
53
+ "defer-messages": { method: "deferMessages", fields: { msg_ids: integers(true) } },
54
+ "defer-files": { method: "deferFiles", fields: { file_ids: integers(true) } },
55
+ "get-conversation": { method: "getConversation", fields: { contact: str(true) } },
56
+ "get-receipts": { method: "getReceipts", fields: { contact: str(true) } },
57
+ "mark-read": { method: "markRead", fields: { contact: str(true) } },
58
+ "set-conversation-policy": { method: "setConversationPolicy", fields: { keep_history: bool(true) } },
59
+ "list-incoming-files": { method: "listIncomingFiles", fields: none },
60
+ "get-files": { method: "getFiles", fields: { wire_ids: strings() } },
61
+ version: { method: "version", fields: none },
62
+ "state-dir": { method: "stateDir", fields: none },
63
+ identities: { method: "identities", fields: none },
64
+ unread: { method: "unread", fields: none },
65
+ "watch-notifications": { method: "watchNotifications", fields: { identity: str(true), since: { kind: "cursor" } } },
66
+ "fetch-file": { method: "fetchFile", fields: { wire_id: str(true) } }
67
+ };
68
+ var operationNames = () => Object.keys(OPERATION_SPECS).sort();
69
+ var isOperationName = (value) => Object.prototype.hasOwnProperty.call(OPERATION_SPECS, value);
70
+ function validateInput(input, operation) {
71
+ const spec = OPERATION_SPECS[operation];
72
+ for (const key of Object.keys(input)) {
73
+ if (!Object.prototype.hasOwnProperty.call(spec.fields, key)) throw new CliUsageError(`${operation}: unknown input field ${JSON.stringify(key)}`);
74
+ }
75
+ for (const [name, field] of Object.entries(spec.fields)) {
76
+ const item = input[name];
77
+ if (item === void 0) {
78
+ if (field.required) throw new CliUsageError(`${operation}: missing required input field ${JSON.stringify(name)}`);
79
+ continue;
80
+ }
81
+ let valid = false;
82
+ if (field.kind === "string") valid = typeof item === "string" && (field.values === void 0 || field.values.includes(item));
83
+ else if (field.kind === "boolean") valid = typeof item === "boolean";
84
+ else if (field.kind === "integer") valid = Number.isSafeInteger(item) && (!field.positive || Number(item) >= 1);
85
+ else if (field.kind === "string[]") valid = Array.isArray(item) && item.every((v) => typeof v === "string");
86
+ else if (field.kind === "integer[]") valid = Array.isArray(item) && item.every((v) => Number.isSafeInteger(v));
87
+ else valid = item === "tip" || Number.isSafeInteger(item) && Number(item) >= 0;
88
+ if (!valid) {
89
+ const expected = field.values ? field.values.map((value) => JSON.stringify(value)).join(" or ") : field.kind;
90
+ throw new CliUsageError(`${operation}: input field ${JSON.stringify(name)} must be ${expected}`);
91
+ }
92
+ }
93
+ spec.refine?.(input);
94
+ return input;
95
+ }
96
+ function parseOperationJson(raw, operation) {
97
+ let value = {};
98
+ if (raw !== void 0) {
99
+ try {
100
+ value = JSON.parse(raw);
101
+ } catch (error) {
102
+ throw new CliUsageError(`--input is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ }
105
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new CliUsageError("--input must be a JSON object");
106
+ return validateInput(value, operation);
107
+ }
108
+ var fieldFlag = (name) => `--${name.replaceAll("_", "-").replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
109
+ function operationValueFlags(operation) {
110
+ return new Set(Object.keys(OPERATION_SPECS[operation].fields).map(fieldFlag));
111
+ }
112
+ function operationInputFromFlags(operation, values, defaults = {}) {
113
+ const input = { ...defaults };
114
+ for (const [name, field] of Object.entries(OPERATION_SPECS[operation].fields)) {
115
+ const flag = fieldFlag(name);
116
+ const raw = values[flag];
117
+ if (raw === void 0) continue;
118
+ if (field.kind === "boolean") input[name] = parseBoolean(raw, flag);
119
+ else if (field.kind === "integer") input[name] = parseCliInteger(raw, flag, field.positive === true);
120
+ else if (field.kind === "integer[]") input[name] = raw === "" ? [] : raw.split(",").map((v) => parseCliInteger(v, flag));
121
+ else if (field.kind === "string[]") input[name] = raw === "" ? [] : raw.split(",");
122
+ else if (field.kind === "cursor") input[name] = raw === "tip" ? "tip" : parseCliInteger(raw, flag);
123
+ else input[name] = raw;
124
+ }
125
+ return validateInput(input, operation);
126
+ }
127
+ function parseCliInteger(raw, flag, positive = false) {
128
+ if (!/^[0-9]+$/.test(raw)) throw new CliUsageError(`${flag} must be an integer`);
129
+ const value = Number(raw);
130
+ if (!Number.isSafeInteger(value) || positive && value < 1) throw new CliUsageError(`${flag} is outside the supported integer range`);
131
+ return value;
132
+ }
133
+ async function invokeOperation(client, operation, input) {
134
+ if (operation === "watch-notifications") throw new Error("watch-notifications is a streaming operation");
135
+ if (operation === "fetch-file") {
136
+ const bytes = await client.fetchFile(input.wire_id);
137
+ return { wire_id: input.wire_id, bytes: bytes.byteLength, data_base64: Buffer.from(bytes).toString("base64") };
138
+ }
139
+ const spec = OPERATION_SPECS[operation];
140
+ const fn = client[spec.method];
141
+ return Object.keys(spec.fields).length === 0 ? fn.call(client) : fn.call(client, input);
142
+ }
143
+
144
+ export {
145
+ OPERATION_SPECS,
146
+ operationNames,
147
+ isOperationName,
148
+ parseOperationJson,
149
+ operationValueFlags,
150
+ operationInputFromFlags,
151
+ invokeOperation
152
+ };
@@ -0,0 +1,31 @@
1
+ // src/output.ts
2
+ var processIo = {
3
+ stdout: (text) => process.stdout.write(text),
4
+ stderr: (text) => process.stderr.write(text)
5
+ };
6
+ function writeResult(io, result, json, label = "ok") {
7
+ if (json) {
8
+ io.stdout(`${JSON.stringify(result, null, 2)}
9
+ `);
10
+ return;
11
+ }
12
+ if (result === void 0 || result === null) io.stdout(`${label}
13
+ `);
14
+ else if (typeof result === "string" || typeof result === "number" || typeof result === "boolean") io.stdout(`${result}
15
+ `);
16
+ else io.stdout(`${label}: ${JSON.stringify(result)}
17
+ `);
18
+ }
19
+ function writeError(io, error, json) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ if (json) io.stderr(`${JSON.stringify({ error: { message, name: error instanceof Error ? error.name : "Error" } })}
22
+ `);
23
+ else io.stderr(`ours: ${message}
24
+ `);
25
+ }
26
+
27
+ export {
28
+ processIo,
29
+ writeResult,
30
+ writeError
31
+ };
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ var argv = process.argv.slice(2);
5
+ if (argv[0] === "daemon" && argv[1] === "serve") {
6
+ const value = (flag) => {
7
+ const exact = argv.indexOf(flag);
8
+ if (exact >= 0) return argv[exact + 1];
9
+ const prefix = `${flag}=`;
10
+ return argv.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
11
+ };
12
+ const config = value("--config");
13
+ const port = value("--port");
14
+ const stateDir = value("--state-dir");
15
+ if (config) process.env.OURS_CONFIG = config;
16
+ if (port) process.env.OURS_PORT = port;
17
+ if (stateDir) process.env.OURS_STATE_DIR = stateDir;
18
+ }
19
+ var { runCli } = await import("./main.js");
20
+ runCli(argv).then(
21
+ (code) => {
22
+ process.exitCode = code;
23
+ },
24
+ (error) => {
25
+ process.stderr.write(`ours: ${error instanceof Error ? error.message : String(error)}
26
+ `);
27
+ process.exitCode = 1;
28
+ }
29
+ );
@@ -0,0 +1,3 @@
1
+ import { type SelectionOptions } from './connection.js';
2
+ export declare function showConfig(selection: SelectionOptions): Promise<Record<string, unknown>>;
3
+ export declare function setupConfig(path: string, patch: Record<string, unknown>, dryRun?: boolean): Record<string, unknown>;
@@ -0,0 +1,9 @@
1
+ import {
2
+ setupConfig,
3
+ showConfig
4
+ } from "./chunk-G2GHHOYX.js";
5
+ import "./chunk-6W3RHW3C.js";
6
+ export {
7
+ setupConfig,
8
+ showConfig
9
+ };
@@ -0,0 +1,14 @@
1
+ import { OursClient, type ResolvedDaemonConfig } from '@ours.network/sdk/client';
2
+ export interface SelectionOptions {
3
+ endpoint?: string;
4
+ port?: number;
5
+ stateDir?: string;
6
+ configPath?: string;
7
+ }
8
+ export declare function defaultConfigPath(): string;
9
+ export declare function resolveSelection(options?: SelectionOptions): ResolvedDaemonConfig;
10
+ export declare function redactedSelection(config: ResolvedDaemonConfig): Record<string, unknown>;
11
+ export declare function attachClient(config: ResolvedDaemonConfig, opts?: {
12
+ fetch?: typeof globalThis.fetch;
13
+ leaseToken?: string;
14
+ }): Promise<OursClient>;
@@ -0,0 +1,12 @@
1
+ import {
2
+ attachClient,
3
+ defaultConfigPath,
4
+ redactedSelection,
5
+ resolveSelection
6
+ } from "./chunk-6W3RHW3C.js";
7
+ export {
8
+ attachClient,
9
+ defaultConfigPath,
10
+ redactedSelection,
11
+ resolveSelection
12
+ };
@@ -0,0 +1,36 @@
1
+ import { spawn as nodeSpawn, type ChildProcess } from 'node:child_process';
2
+ import type { DaemonInfo, ResolvedDaemonConfig } from '@ours.network/sdk/client';
3
+ import { type SelectionOptions } from './connection.js';
4
+ export declare const MANAGED_PID_FILE = "ours-cli-daemon.json";
5
+ export declare const DAEMON_LOG_FILE = "ours-cli-daemon.log";
6
+ interface ManagedRecord {
7
+ version: 1;
8
+ owner: '@ours.network/cli';
9
+ pid: number;
10
+ port: number;
11
+ stateDir: string;
12
+ startedAt: string;
13
+ }
14
+ export interface DaemonStatus {
15
+ state: 'running' | 'stopped';
16
+ managed: boolean;
17
+ info: DaemonInfo | null;
18
+ stateDir: string;
19
+ pidFile: string;
20
+ stalePidFile: boolean;
21
+ }
22
+ export interface LifecycleDeps {
23
+ fetch: typeof globalThis.fetch;
24
+ spawn(executable: string, args: string[], options: Parameters<typeof nodeSpawn>[2]): ChildProcess;
25
+ kill(pid: number, signal: NodeJS.Signals | 0): void;
26
+ now(): number;
27
+ delay(ms: number): Promise<void>;
28
+ cliPath: string;
29
+ }
30
+ export declare function readManagedRecord(stateDir: string): ManagedRecord | null;
31
+ export declare function writeManagedRecord(stateDir: string, port: number): string;
32
+ export declare function inspectDaemon(config: ResolvedDaemonConfig, deps?: LifecycleDeps): Promise<DaemonStatus>;
33
+ export declare function serveDaemon(selection: SelectionOptions, managed?: boolean): Promise<never>;
34
+ export declare function startDaemonManaged(selection: SelectionOptions, deps?: LifecycleDeps): Promise<DaemonStatus>;
35
+ export declare function stopDaemonManaged(config: ResolvedDaemonConfig, deps?: LifecycleDeps): Promise<DaemonStatus>;
36
+ export {};
@@ -0,0 +1,21 @@
1
+ import {
2
+ DAEMON_LOG_FILE,
3
+ MANAGED_PID_FILE,
4
+ inspectDaemon,
5
+ readManagedRecord,
6
+ serveDaemon,
7
+ startDaemonManaged,
8
+ stopDaemonManaged,
9
+ writeManagedRecord
10
+ } from "./chunk-4IFFMMQO.js";
11
+ import "./chunk-6W3RHW3C.js";
12
+ export {
13
+ DAEMON_LOG_FILE,
14
+ MANAGED_PID_FILE,
15
+ inspectDaemon,
16
+ readManagedRecord,
17
+ serveDaemon,
18
+ startDaemonManaged,
19
+ stopDaemonManaged,
20
+ writeManagedRecord
21
+ };
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { type CliIo } from './output.js';
2
+ export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
package/dist/main.js ADDED
@@ -0,0 +1,250 @@
1
+ import {
2
+ createLinuxUserSystemdAdapter
3
+ } from "./chunk-67BC2E5U.js";
4
+ import {
5
+ setupConfig,
6
+ showConfig
7
+ } from "./chunk-G2GHHOYX.js";
8
+ import {
9
+ inspectDaemon,
10
+ serveDaemon,
11
+ startDaemonManaged,
12
+ stopDaemonManaged
13
+ } from "./chunk-4IFFMMQO.js";
14
+ import {
15
+ attachClient,
16
+ defaultConfigPath,
17
+ resolveSelection
18
+ } from "./chunk-6W3RHW3C.js";
19
+ import {
20
+ invokeOperation,
21
+ isOperationName,
22
+ operationInputFromFlags,
23
+ operationNames,
24
+ operationValueFlags,
25
+ parseOperationJson
26
+ } from "./chunk-IUZOQT4D.js";
27
+ import {
28
+ CliUsageError,
29
+ parseBoolean,
30
+ parseFlags,
31
+ parseInteger
32
+ } from "./chunk-AXXFFER2.js";
33
+ import {
34
+ processIo,
35
+ writeError,
36
+ writeResult
37
+ } from "./chunk-YC7QBGVC.js";
38
+
39
+ // src/main.ts
40
+ import { readFileSync } from "node:fs";
41
+ import { resolve } from "node:path";
42
+ var CLI_VERSION = false ? "0.0.0-dev" : "0.1.0";
43
+ var COMMON_VALUES = /* @__PURE__ */ new Set(["--endpoint", "--port", "--state-dir", "--config", "--identity"]);
44
+ var COMMON_BOOLEANS = /* @__PURE__ */ new Set(["--json", "--yes", "--help"]);
45
+ var DESTRUCTIVE = /* @__PURE__ */ new Set(["close-temporary-identity", "remove-identity", "revoke-invite", "remove-contact"]);
46
+ var GROUPED = {
47
+ daemon: {
48
+ info: { operation: "version" },
49
+ identities: { operation: "identities" },
50
+ unread: { operation: "unread" },
51
+ watch: { operation: "watch-notifications" }
52
+ },
53
+ identity: {
54
+ create: { operation: "create-identity", defaults: { bio: "", exposeLocal: true, localAutoAccept: true } },
55
+ "create-temporary": { operation: "create-temporary-identity", defaults: { bio: "", exposeLocal: false, localAutoAccept: true } },
56
+ "close-temporary": { operation: "close-temporary-identity" },
57
+ "create-root": { operation: "create-root-identity", defaults: { bio: "", exposeLocal: true, localAutoAccept: true, skipIfRootExists: true } },
58
+ pin: { operation: "define-local-identity-file", defaults: { force: false, exposeLocal: true, localAutoAccept: true, overwrite: false } },
59
+ use: { operation: "choose-identity", defaults: { force: false } },
60
+ list: { operation: "list-identities" },
61
+ show: { operation: "current-identity" },
62
+ remove: { operation: "remove-identity" },
63
+ release: { operation: "release-lease" }
64
+ },
65
+ profile: { "set-bio": { operation: "set-bio" }, "set-persona": { operation: "set-persona" } },
66
+ invite: { create: { operation: "generate-invite" }, list: { operation: "list-invites" }, revoke: { operation: "revoke-invite" }, accept: { operation: "add-contact" } },
67
+ contact: {
68
+ list: { operation: "list-contacts" },
69
+ local: { operation: "list-local-contact-book" },
70
+ policy: { operation: "set-local-book-policy" },
71
+ remove: { operation: "remove-contact" },
72
+ rename: { operation: "rename-contact" },
73
+ respond: { operation: "respond-to-introduction" }
74
+ },
75
+ message: { send: { operation: "send-message" }, list: { operation: "list-incoming-messages" }, get: { operation: "get-messages" }, defer: { operation: "defer-messages" } },
76
+ file: { send: { operation: "send-file" }, list: { operation: "list-incoming-files" }, get: { operation: "get-files" }, fetch: { operation: "fetch-file" }, defer: { operation: "defer-files" } },
77
+ conversation: {
78
+ show: { operation: "get-conversation" },
79
+ receipts: { operation: "get-receipts" },
80
+ "mark-read": { operation: "mark-read" },
81
+ policy: { operation: "set-conversation-policy" }
82
+ }
83
+ };
84
+ var HELP = `ours ${CLI_VERSION} \u2014 operator CLI for the shared ours daemon
85
+
86
+ Usage:
87
+ ours daemon serve|start|stop|restart|status [selection options]
88
+ ours daemon install-service|uninstall-service --yes [--dry-run]
89
+ ours daemon info|identities|unread|watch [operation options]
90
+ ours config show|setup [configuration options]
91
+ ours identity|profile|invite|contact|message|file|conversation <command> [options]
92
+ ours api list | ours api <operation> [--input JSON|--input-file PATH]
93
+ ours version
94
+
95
+ Selection: --endpoint URL --port N --state-dir PATH --config PATH
96
+ Output: --json. Destructive operations require --yes.
97
+ Run \`ours api list --json\` for the expert operation allowlist.
98
+ `;
99
+ function selectionFrom(values) {
100
+ return {
101
+ endpoint: values["--endpoint"],
102
+ port: values["--port"] === void 0 ? void 0 : parseInteger(values["--port"], "--port", 1, 65535),
103
+ stateDir: values["--state-dir"],
104
+ configPath: values["--config"]
105
+ };
106
+ }
107
+ function requireYes(operation, yes) {
108
+ if (DESTRUCTIVE.has(operation) && !yes) throw new CliUsageError(`${operation} is destructive; re-run with --yes`);
109
+ }
110
+ async function runOperation(operation, input, selection, identity, io, json) {
111
+ const client = await attachClient(await resolveSelection(selection));
112
+ if (identity !== void 0 && !["choose-identity", "create-identity", "create-temporary-identity", "create-root-identity"].includes(operation)) {
113
+ await client.chooseIdentity({ name: identity, force: false });
114
+ }
115
+ if (operation === "watch-notifications") {
116
+ const controller = new AbortController();
117
+ const abort = () => controller.abort();
118
+ process.once("SIGINT", abort);
119
+ process.once("SIGTERM", abort);
120
+ try {
121
+ for await (const event of client.watchNotifications(input.identity, { since: input.since, signal: controller.signal })) {
122
+ io.stdout(`${JSON.stringify(event)}
123
+ `);
124
+ }
125
+ } finally {
126
+ process.off("SIGINT", abort);
127
+ process.off("SIGTERM", abort);
128
+ }
129
+ return;
130
+ }
131
+ writeResult(io, await invokeOperation(client, operation, input), json, operation);
132
+ }
133
+ async function runApi(args, io) {
134
+ const operation = args[0];
135
+ if (operation === "list") {
136
+ const flags2 = parseFlags(args.filter((value) => value !== "list"), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["--json", "--help"]));
137
+ writeResult(io, operationNames(), flags2.booleans.has("--json"), "operations");
138
+ return 0;
139
+ }
140
+ if (!operation || !isOperationName(operation)) throw new CliUsageError(`unknown API operation ${JSON.stringify(operation ?? "")}; run \`ours api list\``);
141
+ const valueFlags = /* @__PURE__ */ new Set([...COMMON_VALUES, "--input", "--input-file"]);
142
+ const flags = parseFlags(args, valueFlags, COMMON_BOOLEANS);
143
+ if (flags.positionals.length !== 1) throw new CliUsageError("api accepts exactly one operation");
144
+ if (flags.values["--input"] !== void 0 && flags.values["--input-file"] !== void 0) throw new CliUsageError("--input and --input-file are mutually exclusive");
145
+ const fromFile = flags.values["--input-file"];
146
+ const raw = fromFile === void 0 ? flags.values["--input"] : readFileSync(fromFile === "-" ? 0 : fromFile, "utf8");
147
+ requireYes(operation, flags.booleans.has("--yes"));
148
+ await runOperation(operation, parseOperationJson(raw, operation), selectionFrom(flags.values), flags.values["--identity"], io, flags.booleans.has("--json"));
149
+ return 0;
150
+ }
151
+ async function runGrouped(group, args, io) {
152
+ const action = args[0];
153
+ const spec = action === void 0 ? void 0 : GROUPED[group]?.[action];
154
+ if (!action || !spec) throw new CliUsageError(`unknown ${group} command ${JSON.stringify(action ?? "")}`);
155
+ const flags = parseFlags(args, /* @__PURE__ */ new Set([...COMMON_VALUES, ...operationValueFlags(spec.operation)]), COMMON_BOOLEANS);
156
+ if (flags.positionals.length !== 1) throw new CliUsageError(`${group} accepts exactly one command`);
157
+ requireYes(spec.operation, flags.booleans.has("--yes"));
158
+ const input = operationInputFromFlags(spec.operation, flags.values, spec.defaults);
159
+ await runOperation(spec.operation, input, selectionFrom(flags.values), flags.values["--identity"], io, flags.booleans.has("--json"));
160
+ return 0;
161
+ }
162
+ async function runConfig(args, io) {
163
+ const values = /* @__PURE__ */ new Set([...COMMON_VALUES, "--broker-url", "--gc-interval-ms", "--auto-start", "--api-visibility"]);
164
+ const flags = parseFlags(args, values, /* @__PURE__ */ new Set(["--json", "--dry-run", "--help"]));
165
+ const action = flags.positionals[0];
166
+ if (flags.positionals.length !== 1 || !["show", "setup"].includes(action)) throw new CliUsageError("config requires show or setup");
167
+ const json = flags.booleans.has("--json");
168
+ if (action === "show") {
169
+ writeResult(io, await showConfig(selectionFrom(flags.values)), json, "config");
170
+ return 0;
171
+ }
172
+ const patch = {};
173
+ if (flags.values["--broker-url"]) patch.brokerUrl = flags.values["--broker-url"];
174
+ if (flags.values["--port"]) patch.port = parseInteger(flags.values["--port"], "--port", 1, 65535);
175
+ if (flags.values["--state-dir"]) patch.stateDir = resolve(flags.values["--state-dir"]);
176
+ if (flags.values["--gc-interval-ms"]) patch.gcIntervalMs = parseInteger(flags.values["--gc-interval-ms"], "--gc-interval-ms", 1);
177
+ if (flags.values["--auto-start"]) patch.autoStart = parseBoolean(flags.values["--auto-start"], "--auto-start");
178
+ if (flags.values["--api-visibility"]) {
179
+ const visibility = flags.values["--api-visibility"];
180
+ if (!["owner", "shared", "open"].includes(visibility)) throw new CliUsageError("--api-visibility must be owner, shared, or open");
181
+ patch.apiVisibility = visibility;
182
+ }
183
+ if (Object.keys(patch).length === 0) throw new CliUsageError("config setup requires at least one setting");
184
+ writeResult(io, setupConfig(flags.values["--config"] ?? defaultConfigPath(), patch, flags.booleans.has("--dry-run")), json, "config updated");
185
+ return 0;
186
+ }
187
+ async function runDaemon(args, io) {
188
+ const action = args[0];
189
+ if (action && GROUPED.daemon[action]) return runGrouped("daemon", args, io);
190
+ const values = new Set(COMMON_VALUES);
191
+ const booleans = /* @__PURE__ */ new Set([...COMMON_BOOLEANS, "--managed", "--dry-run", "--force"]);
192
+ const flags = parseFlags(args, values, booleans);
193
+ if (flags.positionals.length !== 1) throw new CliUsageError("daemon accepts exactly one command");
194
+ const selection = selectionFrom(flags.values);
195
+ const json = flags.booleans.has("--json");
196
+ if (action === "serve") return serveDaemon(selection, flags.booleans.has("--managed"));
197
+ if (action === "status") {
198
+ const status = await inspectDaemon(await resolveSelection(selection));
199
+ writeResult(io, status, json, status.state);
200
+ return status.state === "running" ? 0 : 3;
201
+ }
202
+ if (action === "start") {
203
+ writeResult(io, await startDaemonManaged(selection), json, "started");
204
+ return 0;
205
+ }
206
+ if (action === "stop") {
207
+ writeResult(io, await stopDaemonManaged(await resolveSelection(selection)), json, "stopped");
208
+ return 0;
209
+ }
210
+ if (action === "restart") {
211
+ await stopDaemonManaged(await resolveSelection(selection));
212
+ writeResult(io, await startDaemonManaged(selection), json, "restarted");
213
+ return 0;
214
+ }
215
+ if (action === "install-service" || action === "uninstall-service") {
216
+ const dryRun = flags.booleans.has("--dry-run");
217
+ if (!dryRun && !flags.booleans.has("--yes")) throw new CliUsageError(`${action} changes the user service manager; re-run with --yes or preview with --dry-run`);
218
+ const adapter = createLinuxUserSystemdAdapter();
219
+ const context = { cliPath: process.argv[1], configPath: selection.configPath, dryRun, force: flags.booleans.has("--force") };
220
+ const result = action === "install-service" ? await adapter.install(context) : await adapter.uninstall(context);
221
+ writeResult(io, result, json, action);
222
+ return 0;
223
+ }
224
+ throw new CliUsageError(`unknown daemon command ${JSON.stringify(action ?? "")}`);
225
+ }
226
+ async function runCli(argv, io = processIo) {
227
+ const wantsJson = argv.includes("--json");
228
+ try {
229
+ if (argv.length === 0 || argv.includes("--help") || argv[0] === "help") {
230
+ io.stdout(HELP);
231
+ return 0;
232
+ }
233
+ const [command, ...args] = argv;
234
+ if (command === "version") {
235
+ writeResult(io, wantsJson ? { name: "@ours.network/cli", version: CLI_VERSION } : `ours ${CLI_VERSION}`, wantsJson);
236
+ return 0;
237
+ }
238
+ if (command === "daemon") return await runDaemon(args, io);
239
+ if (command === "config") return await runConfig(args, io);
240
+ if (command === "api") return await runApi(args, io);
241
+ if (GROUPED[command]) return await runGrouped(command, args, io);
242
+ throw new CliUsageError(`unknown command ${JSON.stringify(command)}`);
243
+ } catch (error) {
244
+ writeError(io, error, wantsJson);
245
+ return error instanceof CliUsageError ? error.exitCode : 1;
246
+ }
247
+ }
248
+ export {
249
+ runCli
250
+ };