@ours.network/cli 0.1.0 → 0.2.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,178 @@
1
+ import type { OperationName } from './operations.js';
2
+ export interface OperationCommandSpec {
3
+ kind: 'operation';
4
+ operation: OperationName;
5
+ defaults?: Readonly<Record<string, unknown>>;
6
+ }
7
+ export type NativeCommandSpec = {
8
+ kind: 'daemon';
9
+ handler: 'serve' | 'start' | 'stop' | 'restart' | 'status' | 'install-service' | 'uninstall-service';
10
+ summary: string;
11
+ effects: string;
12
+ example: string;
13
+ } | {
14
+ kind: 'config';
15
+ handler: 'show' | 'setup';
16
+ summary: string;
17
+ effects: string;
18
+ example: string;
19
+ };
20
+ export type CommandSpec = OperationCommandSpec | NativeCommandSpec;
21
+ export interface CommandGroupSpec {
22
+ summary: string;
23
+ commands: Readonly<Record<string, CommandSpec>>;
24
+ }
25
+ export declare const COMMAND_GROUPS: {
26
+ readonly daemon: {
27
+ readonly summary: "Run, inspect, and integrate the shared ours daemon.";
28
+ readonly commands: {
29
+ readonly serve: {
30
+ readonly kind: "daemon";
31
+ readonly handler: "serve";
32
+ readonly summary: "Run the daemon in the foreground until interrupted.";
33
+ readonly effects: "Starts a daemon in this process. --managed also writes the CLI-owned PID record used by stop and restart.";
34
+ readonly example: "ours daemon serve --config /etc/ours/config.json";
35
+ };
36
+ readonly start: {
37
+ readonly kind: "daemon";
38
+ readonly handler: "start";
39
+ readonly summary: "Start a detached daemon managed by this CLI.";
40
+ readonly effects: "Spawns a background process, writes a PID record and log in the selected state directory, and waits up to 20 seconds for readiness.";
41
+ readonly example: "ours daemon start --config /etc/ours/config.json";
42
+ };
43
+ readonly stop: {
44
+ readonly kind: "daemon";
45
+ readonly handler: "stop";
46
+ readonly summary: "Stop a daemon previously started by this CLI.";
47
+ readonly effects: "Signals only a process corroborated by the selected daemon and CLI-owned PID record; it refuses to signal externally managed daemons.";
48
+ readonly example: "ours daemon stop --config /etc/ours/config.json";
49
+ };
50
+ readonly restart: {
51
+ readonly kind: "daemon";
52
+ readonly handler: "restart";
53
+ readonly summary: "Stop and start a CLI-managed daemon.";
54
+ readonly effects: "Uses the same ownership checks as stop, then starts a detached replacement.";
55
+ readonly example: "ours daemon restart --config /etc/ours/config.json";
56
+ };
57
+ readonly status: {
58
+ readonly kind: "daemon";
59
+ readonly handler: "status";
60
+ readonly summary: "Report whether the selected daemon is running and CLI-managed.";
61
+ readonly effects: "Read-only. Exit status is 0 when running and 3 when stopped.";
62
+ readonly example: "ours daemon status --json";
63
+ };
64
+ readonly 'install-service': {
65
+ readonly kind: "daemon";
66
+ readonly handler: "install-service";
67
+ readonly summary: "Install and enable the Linux user systemd service.";
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.";
69
+ readonly example: "ours daemon install-service --dry-run --json";
70
+ };
71
+ readonly 'uninstall-service': {
72
+ readonly kind: "daemon";
73
+ readonly handler: "uninstall-service";
74
+ readonly summary: "Disable and remove the Linux user systemd service owned by this CLI.";
75
+ readonly effects: "Changes the user service manager. Requires --yes unless --dry-run is used and refuses to remove an unowned unit.";
76
+ readonly example: "ours daemon uninstall-service --dry-run --json";
77
+ };
78
+ readonly info: OperationCommandSpec;
79
+ readonly identities: OperationCommandSpec;
80
+ readonly unread: OperationCommandSpec;
81
+ readonly watch: OperationCommandSpec;
82
+ };
83
+ };
84
+ readonly config: {
85
+ readonly summary: "Inspect redacted connection configuration or update safe daemon settings.";
86
+ readonly commands: {
87
+ readonly show: {
88
+ readonly kind: "config";
89
+ readonly handler: "show";
90
+ readonly summary: "Show the resolved daemon selection and redacted stored configuration.";
91
+ readonly effects: "Read-only. Reports whether token/STT sections exist but never prints their secret values.";
92
+ readonly example: "ours config show --json";
93
+ };
94
+ readonly setup: {
95
+ readonly kind: "config";
96
+ readonly handler: "setup";
97
+ readonly summary: "Write one or more safe daemon settings noninteractively.";
98
+ readonly effects: "Writes a mode-0600 JSON config unless --dry-run is used. Unknown and secret fields already present are preserved and never printed.";
99
+ readonly example: "ours config setup --port 3070 --state-dir /srv/ours-a --api-visibility owner";
100
+ };
101
+ };
102
+ };
103
+ readonly identity: {
104
+ readonly summary: "Create, bind, inspect, pin, release, and remove identities.";
105
+ readonly commands: {
106
+ readonly create: OperationCommandSpec;
107
+ readonly 'create-temporary': OperationCommandSpec;
108
+ readonly 'close-temporary': OperationCommandSpec;
109
+ readonly 'create-root': OperationCommandSpec;
110
+ readonly pin: OperationCommandSpec;
111
+ readonly use: OperationCommandSpec;
112
+ readonly list: OperationCommandSpec;
113
+ readonly show: OperationCommandSpec;
114
+ readonly remove: OperationCommandSpec;
115
+ readonly release: OperationCommandSpec;
116
+ };
117
+ };
118
+ readonly profile: {
119
+ readonly summary: "Update the bound identity’s public bio or private local persona.";
120
+ readonly commands: {
121
+ readonly 'set-bio': OperationCommandSpec;
122
+ readonly 'set-persona': OperationCommandSpec;
123
+ };
124
+ };
125
+ readonly invite: {
126
+ readonly summary: "Create, inspect, revoke, and accept encrypted contact invitations.";
127
+ readonly commands: {
128
+ readonly create: OperationCommandSpec;
129
+ readonly list: OperationCommandSpec;
130
+ readonly revoke: OperationCommandSpec;
131
+ readonly accept: OperationCommandSpec;
132
+ };
133
+ };
134
+ readonly contact: {
135
+ readonly summary: "Inspect contacts and manage local introductions and contact policy.";
136
+ readonly commands: {
137
+ readonly list: OperationCommandSpec;
138
+ readonly local: OperationCommandSpec;
139
+ readonly policy: OperationCommandSpec;
140
+ readonly remove: OperationCommandSpec;
141
+ readonly rename: OperationCommandSpec;
142
+ readonly respond: OperationCommandSpec;
143
+ };
144
+ };
145
+ readonly message: {
146
+ readonly summary: "Send encrypted text and manage the incoming message queue.";
147
+ readonly commands: {
148
+ readonly send: OperationCommandSpec;
149
+ readonly list: OperationCommandSpec;
150
+ readonly get: OperationCommandSpec;
151
+ readonly defer: OperationCommandSpec;
152
+ };
153
+ };
154
+ readonly file: {
155
+ readonly summary: "Send encrypted files and authorize, retrieve, or defer received files.";
156
+ readonly commands: {
157
+ readonly send: OperationCommandSpec;
158
+ readonly list: OperationCommandSpec;
159
+ readonly get: OperationCommandSpec;
160
+ readonly fetch: OperationCommandSpec;
161
+ readonly defer: OperationCommandSpec;
162
+ };
163
+ };
164
+ readonly conversation: {
165
+ readonly summary: "Inspect retained conversations and receipts, mark them read, and set retention policy.";
166
+ readonly commands: {
167
+ readonly show: OperationCommandSpec;
168
+ readonly receipts: OperationCommandSpec;
169
+ readonly 'mark-read': OperationCommandSpec;
170
+ readonly policy: OperationCommandSpec;
171
+ };
172
+ };
173
+ };
174
+ export type CommandGroup = keyof typeof COMMAND_GROUPS;
175
+ export declare const DESTRUCTIVE_OPERATIONS: Set<"version" | "identities" | "unread" | "create-identity" | "create-temporary-identity" | "close-temporary-identity" | "create-root-identity" | "define-local-identity-file" | "choose-identity" | "list-identities" | "current-identity" | "remove-identity" | "release-lease" | "generate-invite" | "list-invites" | "revoke-invite" | "add-contact" | "list-contacts" | "list-local-contact-book" | "set-local-book-policy" | "remove-contact" | "rename-contact" | "respond-to-introduction" | "set-bio" | "set-persona" | "send-message" | "send-file" | "list-incoming-messages" | "get-messages" | "defer-messages" | "defer-files" | "get-conversation" | "get-receipts" | "mark-read" | "set-conversation-policy" | "list-incoming-files" | "get-files" | "state-dir" | "watch-notifications" | "fetch-file">;
176
+ export declare const IDENTITY_PREBIND_EXCEPTIONS: Set<"version" | "identities" | "unread" | "create-identity" | "create-temporary-identity" | "close-temporary-identity" | "create-root-identity" | "define-local-identity-file" | "choose-identity" | "list-identities" | "current-identity" | "remove-identity" | "release-lease" | "generate-invite" | "list-invites" | "revoke-invite" | "add-contact" | "list-contacts" | "list-local-contact-book" | "set-local-book-policy" | "remove-contact" | "rename-contact" | "respond-to-introduction" | "set-bio" | "set-persona" | "send-message" | "send-file" | "list-incoming-messages" | "get-messages" | "defer-messages" | "defer-files" | "get-conversation" | "get-receipts" | "mark-read" | "set-conversation-policy" | "list-incoming-files" | "get-files" | "state-dir" | "watch-notifications" | "fetch-file">;
177
+ export declare function isCommandGroup(value: string): value is CommandGroup;
178
+ export declare function commandSpec(group: CommandGroup, command: string): CommandSpec | undefined;
@@ -0,0 +1,14 @@
1
+ import {
2
+ COMMAND_GROUPS,
3
+ DESTRUCTIVE_OPERATIONS,
4
+ IDENTITY_PREBIND_EXCEPTIONS,
5
+ commandSpec,
6
+ isCommandGroup
7
+ } from "./chunk-YSHMCQ2B.js";
8
+ export {
9
+ COMMAND_GROUPS,
10
+ DESTRUCTIVE_OPERATIONS,
11
+ IDENTITY_PREBIND_EXCEPTIONS,
12
+ commandSpec,
13
+ isCommandGroup
14
+ };
package/dist/help.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ export declare function renderTopHelp(version: string): string;
2
+ export declare function helpRequestPath(argv: string[]): string[] | null;
3
+ export declare function renderHelp(path: string[], version: string): string;
4
+ export declare function helpHint(group?: string, action?: string): string;
package/dist/help.js ADDED
@@ -0,0 +1,15 @@
1
+ import {
2
+ helpHint,
3
+ helpRequestPath,
4
+ renderHelp,
5
+ renderTopHelp
6
+ } from "./chunk-HNBWRI4O.js";
7
+ import "./chunk-QRNZFPNJ.js";
8
+ import "./chunk-6NFATG6P.js";
9
+ import "./chunk-YSHMCQ2B.js";
10
+ export {
11
+ helpHint,
12
+ helpRequestPath,
13
+ renderHelp,
14
+ renderTopHelp
15
+ };
package/dist/main.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ processIo,
3
+ writeError,
4
+ writeResult
5
+ } from "./chunk-YC7QBGVC.js";
1
6
  import {
2
7
  createLinuxUserSystemdAdapter
3
8
  } from "./chunk-67BC2E5U.js";
@@ -6,96 +11,49 @@ import {
6
11
  showConfig
7
12
  } from "./chunk-G2GHHOYX.js";
8
13
  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";
14
+ helpHint,
15
+ helpRequestPath,
16
+ renderHelp
17
+ } from "./chunk-HNBWRI4O.js";
19
18
  import {
20
19
  invokeOperation,
21
20
  isOperationName,
21
+ operationEmptyValueFlags,
22
22
  operationInputFromFlags,
23
23
  operationNames,
24
24
  operationValueFlags,
25
25
  parseOperationJson
26
- } from "./chunk-IUZOQT4D.js";
26
+ } from "./chunk-QRNZFPNJ.js";
27
27
  import {
28
28
  CliUsageError,
29
29
  parseBoolean,
30
30
  parseFlags,
31
31
  parseInteger
32
- } from "./chunk-AXXFFER2.js";
32
+ } from "./chunk-6NFATG6P.js";
33
33
  import {
34
- processIo,
35
- writeError,
36
- writeResult
37
- } from "./chunk-YC7QBGVC.js";
34
+ DESTRUCTIVE_OPERATIONS,
35
+ IDENTITY_PREBIND_EXCEPTIONS,
36
+ commandSpec,
37
+ isCommandGroup
38
+ } from "./chunk-YSHMCQ2B.js";
39
+ import {
40
+ inspectDaemon,
41
+ serveDaemon,
42
+ startDaemonManaged,
43
+ stopDaemonManaged
44
+ } from "./chunk-4IFFMMQO.js";
45
+ import {
46
+ attachClient,
47
+ defaultConfigPath,
48
+ resolveSelection
49
+ } from "./chunk-6W3RHW3C.js";
38
50
 
39
51
  // src/main.ts
40
52
  import { readFileSync } from "node:fs";
41
53
  import { resolve } from "node:path";
42
- var CLI_VERSION = false ? "0.0.0-dev" : "0.1.0";
54
+ var CLI_VERSION = false ? "0.0.0-dev" : "0.2.0";
43
55
  var COMMON_VALUES = /* @__PURE__ */ new Set(["--endpoint", "--port", "--state-dir", "--config", "--identity"]);
44
56
  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
57
  function selectionFrom(values) {
100
58
  return {
101
59
  endpoint: values["--endpoint"],
@@ -105,11 +63,11 @@ function selectionFrom(values) {
105
63
  };
106
64
  }
107
65
  function requireYes(operation, yes) {
108
- if (DESTRUCTIVE.has(operation) && !yes) throw new CliUsageError(`${operation} is destructive; re-run with --yes`);
66
+ if (DESTRUCTIVE_OPERATIONS.has(operation) && !yes) throw new CliUsageError(`${operation} is destructive; re-run with --yes`);
109
67
  }
110
68
  async function runOperation(operation, input, selection, identity, io, json) {
111
69
  const client = await attachClient(await resolveSelection(selection));
112
- if (identity !== void 0 && !["choose-identity", "create-identity", "create-temporary-identity", "create-root-identity"].includes(operation)) {
70
+ if (identity !== void 0 && !IDENTITY_PREBIND_EXCEPTIONS.has(operation)) {
113
71
  await client.chooseIdentity({ name: identity, force: false });
114
72
  }
115
73
  if (operation === "watch-notifications") {
@@ -133,7 +91,8 @@ async function runOperation(operation, input, selection, identity, io, json) {
133
91
  async function runApi(args, io) {
134
92
  const operation = args[0];
135
93
  if (operation === "list") {
136
- const flags2 = parseFlags(args.filter((value) => value !== "list"), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["--json", "--help"]));
94
+ const flags2 = parseFlags(args.slice(1), /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set(["--json", "--help"]));
95
+ if (flags2.positionals.length !== 0) throw new CliUsageError("api list accepts no positional arguments");
137
96
  writeResult(io, operationNames(), flags2.booleans.has("--json"), "operations");
138
97
  return 0;
139
98
  }
@@ -150,9 +109,14 @@ async function runApi(args, io) {
150
109
  }
151
110
  async function runGrouped(group, args, io) {
152
111
  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);
112
+ const spec = action === void 0 ? void 0 : commandSpec(group, action);
113
+ if (!action || !spec || spec.kind !== "operation") throw new CliUsageError(`unknown ${group} command ${JSON.stringify(action ?? "")}`);
114
+ const flags = parseFlags(
115
+ args,
116
+ /* @__PURE__ */ new Set([...COMMON_VALUES, ...operationValueFlags(spec.operation)]),
117
+ COMMON_BOOLEANS,
118
+ operationEmptyValueFlags(spec.operation)
119
+ );
156
120
  if (flags.positionals.length !== 1) throw new CliUsageError(`${group} accepts exactly one command`);
157
121
  requireYes(spec.operation, flags.booleans.has("--yes"));
158
122
  const input = operationInputFromFlags(spec.operation, flags.values, spec.defaults);
@@ -163,9 +127,10 @@ async function runConfig(args, io) {
163
127
  const values = /* @__PURE__ */ new Set([...COMMON_VALUES, "--broker-url", "--gc-interval-ms", "--auto-start", "--api-visibility"]);
164
128
  const flags = parseFlags(args, values, /* @__PURE__ */ new Set(["--json", "--dry-run", "--help"]));
165
129
  const action = flags.positionals[0];
166
- if (flags.positionals.length !== 1 || !["show", "setup"].includes(action)) throw new CliUsageError("config requires show or setup");
130
+ const spec = action === void 0 ? void 0 : commandSpec("config", action);
131
+ if (flags.positionals.length !== 1 || !spec || spec.kind !== "config") throw new CliUsageError("config requires show or setup");
167
132
  const json = flags.booleans.has("--json");
168
- if (action === "show") {
133
+ if (spec.handler === "show") {
169
134
  writeResult(io, await showConfig(selectionFrom(flags.values)), json, "config");
170
135
  return 0;
171
136
  }
@@ -186,48 +151,54 @@ async function runConfig(args, io) {
186
151
  }
187
152
  async function runDaemon(args, io) {
188
153
  const action = args[0];
189
- if (action && GROUPED.daemon[action]) return runGrouped("daemon", args, io);
154
+ const spec = action === void 0 ? void 0 : commandSpec("daemon", action);
155
+ if (spec?.kind === "operation") return runGrouped("daemon", args, io);
190
156
  const values = new Set(COMMON_VALUES);
191
157
  const booleans = /* @__PURE__ */ new Set([...COMMON_BOOLEANS, "--managed", "--dry-run", "--force"]);
192
158
  const flags = parseFlags(args, values, booleans);
193
159
  if (flags.positionals.length !== 1) throw new CliUsageError("daemon accepts exactly one command");
160
+ if (!spec || spec.kind !== "daemon") throw new CliUsageError(`unknown daemon command ${JSON.stringify(action ?? "")}`);
161
+ const handler = spec.handler;
194
162
  const selection = selectionFrom(flags.values);
195
163
  const json = flags.booleans.has("--json");
196
- if (action === "serve") return serveDaemon(selection, flags.booleans.has("--managed"));
197
- if (action === "status") {
164
+ if (handler === "serve") return serveDaemon(selection, flags.booleans.has("--managed"));
165
+ if (handler === "status") {
198
166
  const status = await inspectDaemon(await resolveSelection(selection));
199
167
  writeResult(io, status, json, status.state);
200
168
  return status.state === "running" ? 0 : 3;
201
169
  }
202
- if (action === "start") {
170
+ if (handler === "start") {
203
171
  writeResult(io, await startDaemonManaged(selection), json, "started");
204
172
  return 0;
205
173
  }
206
- if (action === "stop") {
174
+ if (handler === "stop") {
207
175
  writeResult(io, await stopDaemonManaged(await resolveSelection(selection)), json, "stopped");
208
176
  return 0;
209
177
  }
210
- if (action === "restart") {
178
+ if (handler === "restart") {
179
+ if (selection.endpoint !== void 0) throw new CliUsageError("--endpoint selects a running daemon and cannot be used with daemon restart");
211
180
  await stopDaemonManaged(await resolveSelection(selection));
212
181
  writeResult(io, await startDaemonManaged(selection), json, "restarted");
213
182
  return 0;
214
183
  }
215
- if (action === "install-service" || action === "uninstall-service") {
184
+ if (handler === "install-service" || handler === "uninstall-service") {
216
185
  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`);
186
+ if (!dryRun && !flags.booleans.has("--yes")) throw new CliUsageError(`${handler} changes the user service manager; re-run with --yes or preview with --dry-run`);
218
187
  const adapter = createLinuxUserSystemdAdapter();
219
188
  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);
189
+ const result = handler === "install-service" ? await adapter.install(context) : await adapter.uninstall(context);
190
+ writeResult(io, result, json, handler);
222
191
  return 0;
223
192
  }
224
- throw new CliUsageError(`unknown daemon command ${JSON.stringify(action ?? "")}`);
193
+ const unsupported = handler;
194
+ throw new Error(`unsupported daemon handler: ${String(unsupported)}`);
225
195
  }
226
196
  async function runCli(argv, io = processIo) {
227
197
  const wantsJson = argv.includes("--json");
228
198
  try {
229
- if (argv.length === 0 || argv.includes("--help") || argv[0] === "help") {
230
- io.stdout(HELP);
199
+ const helpPath = helpRequestPath(argv);
200
+ if (helpPath !== null) {
201
+ io.stdout(renderHelp(helpPath, CLI_VERSION));
231
202
  return 0;
232
203
  }
233
204
  const [command, ...args] = argv;
@@ -235,14 +206,18 @@ async function runCli(argv, io = processIo) {
235
206
  writeResult(io, wantsJson ? { name: "@ours.network/cli", version: CLI_VERSION } : `ours ${CLI_VERSION}`, wantsJson);
236
207
  return 0;
237
208
  }
238
- if (command === "daemon") return await runDaemon(args, io);
239
- if (command === "config") return await runConfig(args, io);
240
209
  if (command === "api") return await runApi(args, io);
241
- if (GROUPED[command]) return await runGrouped(command, args, io);
210
+ if (isCommandGroup(command)) {
211
+ if (command === "daemon") return await runDaemon(args, io);
212
+ if (command === "config") return await runConfig(args, io);
213
+ return await runGrouped(command, args, io);
214
+ }
242
215
  throw new CliUsageError(`unknown command ${JSON.stringify(command)}`);
243
216
  } catch (error) {
244
- writeError(io, error, wantsJson);
245
- return error instanceof CliUsageError ? error.exitCode : 1;
217
+ const reported = error instanceof CliUsageError && !/run `ours help/i.test(error.message) ? new CliUsageError(`${error.message}
218
+ ${helpHint(argv[0], argv[1])}`) : error;
219
+ writeError(io, reported, wantsJson);
220
+ return reported instanceof CliUsageError ? reported.exitCode : 1;
246
221
  }
247
222
  }
248
223
  export {