specli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/CLAUDE.md +111 -0
  2. package/PLAN.md +274 -0
  3. package/README.md +474 -0
  4. package/biome.jsonc +1 -0
  5. package/bun.lock +98 -0
  6. package/cli.ts +74 -0
  7. package/fixtures/openapi-array-items.json +22 -0
  8. package/fixtures/openapi-auth.json +34 -0
  9. package/fixtures/openapi-body.json +41 -0
  10. package/fixtures/openapi-collision.json +21 -0
  11. package/fixtures/openapi-oauth.json +54 -0
  12. package/fixtures/openapi-servers.json +35 -0
  13. package/fixtures/openapi.json +87 -0
  14. package/index.ts +1 -0
  15. package/package.json +27 -0
  16. package/scripts/smoke-specs.ts +64 -0
  17. package/src/cli/auth-requirements.test.ts +27 -0
  18. package/src/cli/auth-requirements.ts +91 -0
  19. package/src/cli/auth-schemes.test.ts +66 -0
  20. package/src/cli/auth-schemes.ts +187 -0
  21. package/src/cli/capabilities.test.ts +94 -0
  22. package/src/cli/capabilities.ts +88 -0
  23. package/src/cli/command-id.test.ts +32 -0
  24. package/src/cli/command-id.ts +16 -0
  25. package/src/cli/command-index.ts +19 -0
  26. package/src/cli/command-model.test.ts +44 -0
  27. package/src/cli/command-model.ts +128 -0
  28. package/src/cli/compile.ts +119 -0
  29. package/src/cli/crypto.ts +9 -0
  30. package/src/cli/derive-name.ts +101 -0
  31. package/src/cli/exec.ts +72 -0
  32. package/src/cli/main.ts +336 -0
  33. package/src/cli/naming.test.ts +86 -0
  34. package/src/cli/naming.ts +224 -0
  35. package/src/cli/operations.test.ts +57 -0
  36. package/src/cli/operations.ts +152 -0
  37. package/src/cli/params.test.ts +70 -0
  38. package/src/cli/params.ts +71 -0
  39. package/src/cli/pluralize.ts +41 -0
  40. package/src/cli/positional.test.ts +65 -0
  41. package/src/cli/positional.ts +75 -0
  42. package/src/cli/request-body.test.ts +35 -0
  43. package/src/cli/request-body.ts +94 -0
  44. package/src/cli/runtime/argv.ts +14 -0
  45. package/src/cli/runtime/auth/resolve.ts +31 -0
  46. package/src/cli/runtime/body.ts +24 -0
  47. package/src/cli/runtime/collect.ts +6 -0
  48. package/src/cli/runtime/context.ts +62 -0
  49. package/src/cli/runtime/execute.ts +138 -0
  50. package/src/cli/runtime/generated.ts +200 -0
  51. package/src/cli/runtime/headers.ts +37 -0
  52. package/src/cli/runtime/index.ts +3 -0
  53. package/src/cli/runtime/profile/secrets.ts +42 -0
  54. package/src/cli/runtime/profile/store.ts +98 -0
  55. package/src/cli/runtime/request.test.ts +153 -0
  56. package/src/cli/runtime/request.ts +487 -0
  57. package/src/cli/runtime/server-url.ts +44 -0
  58. package/src/cli/runtime/template.ts +26 -0
  59. package/src/cli/runtime/validate/ajv.ts +13 -0
  60. package/src/cli/runtime/validate/coerce.ts +71 -0
  61. package/src/cli/runtime/validate/error.ts +29 -0
  62. package/src/cli/runtime/validate/index.ts +4 -0
  63. package/src/cli/runtime/validate/schema.ts +54 -0
  64. package/src/cli/schema-shape.ts +36 -0
  65. package/src/cli/schema.ts +76 -0
  66. package/src/cli/server.test.ts +35 -0
  67. package/src/cli/server.ts +88 -0
  68. package/src/cli/spec-id.ts +12 -0
  69. package/src/cli/spec-loader.ts +58 -0
  70. package/src/cli/stable-json.ts +35 -0
  71. package/src/cli/strings.ts +21 -0
  72. package/src/cli/types.ts +59 -0
  73. package/src/compiled.ts +23 -0
  74. package/src/macros/env.ts +25 -0
  75. package/src/macros/spec.ts +17 -0
  76. package/tsconfig.json +29 -0
@@ -0,0 +1,119 @@
1
+ import { deriveBinaryName } from "./derive-name.ts";
2
+
3
+ export type CompileOptions = {
4
+ name?: string;
5
+ outfile?: string;
6
+ target?: string;
7
+ minify?: boolean;
8
+ bytecode?: boolean;
9
+ dotenv?: boolean; // --no-dotenv sets this to false
10
+ bunfig?: boolean; // --no-bunfig sets this to false
11
+ execArgv?: string[];
12
+ define?: string[];
13
+ server?: string;
14
+ serverVar?: string[];
15
+ auth?: string;
16
+ };
17
+
18
+ function parseKeyValue(input: string): { key: string; value: string } {
19
+ const idx = input.indexOf("=");
20
+ if (idx === -1)
21
+ throw new Error(`Invalid --define '${input}', expected key=value`);
22
+ const key = input.slice(0, idx).trim();
23
+ const value = input.slice(idx + 1).trim();
24
+ if (!key) throw new Error(`Invalid --define '${input}', missing key`);
25
+ return { key, value };
26
+ }
27
+
28
+ export async function compileCommand(
29
+ spec: string,
30
+ options: CompileOptions,
31
+ ): Promise<void> {
32
+ // Derive name from spec if not provided
33
+ const name = options.name ?? (await deriveBinaryName(spec));
34
+ const outfile = options.outfile ?? `./dist/${name}`;
35
+
36
+ const target = options.target
37
+ ? (options.target as Bun.Build.Target)
38
+ : (`bun-${process.platform}-${process.arch}` as Bun.Build.Target);
39
+
40
+ // Build embedded execArgv for runtime defaults
41
+ const embeddedExecArgv: string[] = [];
42
+ if (options.server) {
43
+ embeddedExecArgv.push(`--server=${options.server}`);
44
+ }
45
+ if (options.serverVar) {
46
+ for (const pair of options.serverVar) {
47
+ embeddedExecArgv.push(`--server-var=${pair}`);
48
+ }
49
+ }
50
+ if (options.auth) {
51
+ embeddedExecArgv.push(`--auth=${options.auth}`);
52
+ }
53
+
54
+ // User-provided exec-argv
55
+ const compileExecArgv = options.execArgv ?? [];
56
+
57
+ // Parse --define pairs
58
+ const define: Record<string, string> = {};
59
+ if (options.define) {
60
+ for (const pair of options.define) {
61
+ const { key, value } = parseKeyValue(pair);
62
+ define[key] = JSON.stringify(value);
63
+ }
64
+ }
65
+
66
+ // Build command args
67
+ const buildArgs = [
68
+ "build",
69
+ "--compile",
70
+ `--outfile=${outfile}`,
71
+ `--target=${target}`,
72
+ ];
73
+
74
+ if (options.minify) buildArgs.push("--minify");
75
+ if (options.bytecode) buildArgs.push("--bytecode");
76
+
77
+ for (const [k, v] of Object.entries(define)) {
78
+ buildArgs.push("--define", `${k}=${v}`);
79
+ }
80
+
81
+ const execArgv =
82
+ embeddedExecArgv.length || compileExecArgv.length
83
+ ? [...embeddedExecArgv, ...compileExecArgv]
84
+ : [];
85
+ for (const arg of execArgv) {
86
+ buildArgs.push("--compile-exec-argv", arg);
87
+ }
88
+
89
+ if (options.dotenv === false) buildArgs.push("--no-compile-autoload-dotenv");
90
+ if (options.bunfig === false) buildArgs.push("--no-compile-autoload-bunfig");
91
+
92
+ buildArgs.push("./src/compiled.ts");
93
+
94
+ const proc = Bun.spawn({
95
+ cmd: ["bun", ...buildArgs],
96
+ stdout: "pipe",
97
+ stderr: "pipe",
98
+ env: {
99
+ ...process.env,
100
+ OPENCLI_EMBED_SPEC: spec,
101
+ OPENCLI_CLI_NAME: name,
102
+ },
103
+ });
104
+
105
+ const output = await new Response(proc.stdout).text();
106
+ const error = await new Response(proc.stderr).text();
107
+ const code = await proc.exited;
108
+
109
+ if (output) process.stdout.write(output);
110
+ if (error) process.stderr.write(error);
111
+ if (code !== 0) {
112
+ process.exitCode = code;
113
+ return;
114
+ }
115
+
116
+ process.stdout.write(`ok: built ${outfile}\n`);
117
+ process.stdout.write(`target: ${target}\n`);
118
+ process.stdout.write(`name: ${name}\n`);
119
+ }
@@ -0,0 +1,9 @@
1
+ export async function sha256Hex(text: string): Promise<string> {
2
+ const data = new TextEncoder().encode(text);
3
+ const hash = await crypto.subtle.digest("SHA-256", data);
4
+ const bytes = new Uint8Array(hash);
5
+
6
+ let out = "";
7
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
8
+ return out;
9
+ }
@@ -0,0 +1,101 @@
1
+ const RESERVED_NAMES = [
2
+ "exec",
3
+ "compile",
4
+ "profile",
5
+ "auth",
6
+ "help",
7
+ "version",
8
+ ];
9
+
10
+ /**
11
+ * Derives a clean binary name from an OpenAPI spec.
12
+ * Priority:
13
+ * 1. info.title (kebab-cased, sanitized)
14
+ * 2. Host from spec URL (if URL provided)
15
+ * 3. Fallback to "opencli"
16
+ */
17
+ export async function deriveBinaryName(spec: string): Promise<string> {
18
+ try {
19
+ // Load spec to extract title
20
+ const text = await loadSpecText(spec);
21
+ const doc = parseSpec(text);
22
+
23
+ const title = doc?.info?.title;
24
+ if (title && typeof title === "string") {
25
+ const name = sanitizeName(title);
26
+ if (name) return name;
27
+ }
28
+ } catch {
29
+ // Fall through to URL-based derivation
30
+ }
31
+
32
+ // Try to derive from URL host
33
+ if (/^https?:\/\//i.test(spec)) {
34
+ try {
35
+ const url = new URL(spec);
36
+ const hostParts = url.hostname.split(".");
37
+ // Use first meaningful segment (skip www, api prefixes)
38
+ const meaningful = hostParts.find(
39
+ (p) => p !== "www" && p !== "api" && p.length > 2,
40
+ );
41
+ if (meaningful) {
42
+ const name = sanitizeName(meaningful);
43
+ if (name) return name;
44
+ }
45
+ } catch {
46
+ // Invalid URL, fall through
47
+ }
48
+ }
49
+
50
+ // Fallback
51
+ return "opencli";
52
+ }
53
+
54
+ async function loadSpecText(spec: string): Promise<string> {
55
+ if (/^https?:\/\//i.test(spec)) {
56
+ const res = await fetch(spec);
57
+ if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`);
58
+ return res.text();
59
+ }
60
+ return Bun.file(spec).text();
61
+ }
62
+
63
+ function parseSpec(text: string): { info?: { title?: string } } | null {
64
+ try {
65
+ const trimmed = text.trimStart();
66
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
67
+ return JSON.parse(text);
68
+ }
69
+ // Use Bun's YAML parser
70
+ const { YAML } = globalThis.Bun ?? {};
71
+ if (YAML?.parse) {
72
+ return YAML.parse(text) as { info?: { title?: string } };
73
+ }
74
+ // Fallback: only JSON supported
75
+ return null;
76
+ } catch {
77
+ return null;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Convert title to valid binary name:
83
+ * - kebab-case
84
+ * - lowercase
85
+ * - remove invalid chars
86
+ * - max 32 chars
87
+ * - avoid reserved names
88
+ */
89
+ function sanitizeName(input: string): string {
90
+ let name = input
91
+ .toLowerCase()
92
+ .replace(/[^a-z0-9]+/g, "-") // Replace non-alphanumeric with dash
93
+ .replace(/^-+|-+$/g, "") // Trim leading/trailing dashes
94
+ .slice(0, 32); // Limit length
95
+
96
+ if (RESERVED_NAMES.includes(name)) {
97
+ name = `${name}-cli`;
98
+ }
99
+
100
+ return name;
101
+ }
@@ -0,0 +1,72 @@
1
+ import { main } from "./main.ts";
2
+
3
+ export type ExecOptions = {
4
+ server?: string;
5
+ serverVar?: string[];
6
+ auth?: string;
7
+ bearerToken?: string;
8
+ oauthToken?: string;
9
+ username?: string;
10
+ password?: string;
11
+ apiKey?: string;
12
+ profile?: string;
13
+ json?: boolean;
14
+ };
15
+
16
+ export async function execCommand(
17
+ spec: string,
18
+ options: ExecOptions,
19
+ commandArgs: string[],
20
+ ): Promise<void> {
21
+ // commandArgs includes the spec as first element, filter it out
22
+ // to get the remaining args (resource, action, etc.)
23
+ const remainingArgs = commandArgs.slice(1);
24
+
25
+ // Reconstruct argv for main():
26
+ // [node, script, --spec, <spec>, ...options, ...remainingArgs]
27
+ const argv = [
28
+ process.argv[0] ?? "bun",
29
+ process.argv[1] ?? "opencli",
30
+ "--spec",
31
+ spec,
32
+ ];
33
+
34
+ // Add common options back as flags
35
+ if (options.server) {
36
+ argv.push("--server", options.server);
37
+ }
38
+ if (options.serverVar) {
39
+ for (const v of options.serverVar) {
40
+ argv.push("--server-var", v);
41
+ }
42
+ }
43
+ if (options.auth) {
44
+ argv.push("--auth", options.auth);
45
+ }
46
+ if (options.bearerToken) {
47
+ argv.push("--bearer-token", options.bearerToken);
48
+ }
49
+ if (options.oauthToken) {
50
+ argv.push("--oauth-token", options.oauthToken);
51
+ }
52
+ if (options.username) {
53
+ argv.push("--username", options.username);
54
+ }
55
+ if (options.password) {
56
+ argv.push("--password", options.password);
57
+ }
58
+ if (options.apiKey) {
59
+ argv.push("--api-key", options.apiKey);
60
+ }
61
+ if (options.profile) {
62
+ argv.push("--profile", options.profile);
63
+ }
64
+ if (options.json) {
65
+ argv.push("--json");
66
+ }
67
+
68
+ // Append remaining args (subcommand + its args)
69
+ argv.push(...remainingArgs);
70
+
71
+ await main(argv);
72
+ }
@@ -0,0 +1,336 @@
1
+ import { Command } from "commander";
2
+
3
+ import { getArgValue, hasAnyArg } from "./runtime/argv.ts";
4
+ import { collectRepeatable } from "./runtime/collect.ts";
5
+ import { buildRuntimeContext } from "./runtime/context.ts";
6
+ import { addGeneratedCommands } from "./runtime/generated.ts";
7
+ import { deleteToken, getToken, setToken } from "./runtime/profile/secrets.ts";
8
+ import {
9
+ getProfile,
10
+ readProfiles,
11
+ removeProfile,
12
+ upsertProfile,
13
+ writeProfiles,
14
+ } from "./runtime/profile/store.ts";
15
+ import { toMinimalSchemaOutput } from "./schema.ts";
16
+ import { stableStringify } from "./stable-json.ts";
17
+
18
+ type MainOptions = {
19
+ embeddedSpecText?: string;
20
+ cliName?: string;
21
+ };
22
+
23
+ export async function main(argv: string[], options: MainOptions = {}) {
24
+ const program = new Command();
25
+
26
+ program
27
+ .name(options.cliName ?? "opencli")
28
+ .description("Generate a CLI from an OpenAPI spec")
29
+ .option("--spec <urlOrPath>", "OpenAPI URL or file path")
30
+ .option("--server <url>", "Override server/base URL")
31
+ .option(
32
+ "--server-var <name=value>",
33
+ "Server URL template variable (repeatable)",
34
+ collectRepeatable,
35
+ )
36
+ .option("--auth <scheme>", "Select auth scheme by key")
37
+ .option("--bearer-token <token>", "Bearer token (Authorization: Bearer)")
38
+ .option("--oauth-token <token>", "OAuth token (alias of bearer)")
39
+ .option("--username <username>", "Basic auth username")
40
+ .option("--password <password>", "Basic auth password")
41
+ .option("--api-key <key>", "API key value")
42
+ .option("--profile <name>", "Profile name (stored under ~/.config/opencli)")
43
+ .option("--json", "Machine-readable output")
44
+ .showHelpAfterError();
45
+
46
+ // Provide namespaced variants for flags which may collide with operation flags.
47
+ // (Some real-world APIs define parameters named "accept", etc.)
48
+ program
49
+ .option(
50
+ "--oc-header <header>",
51
+ "Extra header (repeatable, namespaced)",
52
+ collectRepeatable,
53
+ )
54
+ .option("--oc-accept <type>", "Override Accept header (namespaced)")
55
+ .option("--oc-status", "Include status in --json output (namespaced)")
56
+ .option("--oc-headers", "Include headers in --json output (namespaced)")
57
+ .option("--oc-dry-run", "Print request without sending (namespaced)")
58
+ .option("--oc-curl", "Print curl command without sending (namespaced)")
59
+ .option("--oc-timeout <ms>", "Request timeout in milliseconds (namespaced)")
60
+ .option("--oc-data <data>", "Inline request body (namespaced)")
61
+ .option("--oc-file <path>", "Request body from file (namespaced)")
62
+ .option(
63
+ "--oc-content-type <type>",
64
+ "Override Content-Type (defaults from OpenAPI) (namespaced)",
65
+ );
66
+
67
+ // If user asks for help and we have no embedded spec and no --spec, show minimal help.
68
+ const spec = getArgValue(argv, "--spec");
69
+ const wantsHelp = hasAnyArg(argv, ["-h", "--help"]);
70
+ if (!spec && !options.embeddedSpecText && wantsHelp) {
71
+ program.addHelpText(
72
+ "after",
73
+ "\nTo see generated commands, run with --spec <url|path>.\n",
74
+ );
75
+ program.parse(argv);
76
+ return;
77
+ }
78
+
79
+ const ctx = await buildRuntimeContext({
80
+ spec,
81
+ embeddedSpecText: options.embeddedSpecText,
82
+ });
83
+
84
+ const profileCmd = program
85
+ .command("profile")
86
+ .description("Manage OpenCLI profiles");
87
+
88
+ profileCmd
89
+ .command("list")
90
+ .description("List profiles")
91
+ .action(async (_opts, command) => {
92
+ const globals = command.optsWithGlobals() as { json?: boolean };
93
+ const file = await readProfiles();
94
+
95
+ const payload = {
96
+ defaultProfile: file.defaultProfile,
97
+ profiles: file.profiles.map((p) => ({
98
+ name: p.name,
99
+ server: p.server,
100
+ authScheme: p.authScheme,
101
+ })),
102
+ };
103
+
104
+ if (globals.json) {
105
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
106
+ return;
107
+ }
108
+
109
+ if (payload.defaultProfile) {
110
+ process.stdout.write(`default: ${payload.defaultProfile}\n`);
111
+ }
112
+ for (const p of payload.profiles) {
113
+ process.stdout.write(`${p.name}\n`);
114
+ if (p.server) process.stdout.write(` server: ${p.server}\n`);
115
+ if (p.authScheme)
116
+ process.stdout.write(` authScheme: ${p.authScheme}\n`);
117
+ }
118
+ });
119
+
120
+ profileCmd
121
+ .command("set")
122
+ .description("Create or update a profile")
123
+ .requiredOption("--name <name>", "Profile name")
124
+ .option("--server <url>", "Default server/base URL")
125
+ .option("--auth <scheme>", "Default auth scheme key")
126
+ .option("--default", "Set as default profile")
127
+ .action(async (opts, command) => {
128
+ const globals = command.optsWithGlobals() as {
129
+ json?: boolean;
130
+ server?: string;
131
+ auth?: string;
132
+ };
133
+
134
+ const file = await readProfiles();
135
+ const next = upsertProfile(file, {
136
+ name: String(opts.name),
137
+ server:
138
+ typeof opts.server === "string"
139
+ ? opts.server
140
+ : typeof globals.server === "string"
141
+ ? globals.server
142
+ : undefined,
143
+ authScheme:
144
+ typeof opts.auth === "string"
145
+ ? opts.auth
146
+ : typeof globals.auth === "string"
147
+ ? globals.auth
148
+ : undefined,
149
+ });
150
+ const final = opts.default
151
+ ? { ...next, defaultProfile: String(opts.name) }
152
+ : next;
153
+ await writeProfiles(final);
154
+
155
+ if (globals.json) {
156
+ process.stdout.write(
157
+ `${JSON.stringify({ ok: true, profile: String(opts.name) })}\n`,
158
+ );
159
+ return;
160
+ }
161
+ process.stdout.write(`ok: profile ${String(opts.name)}\n`);
162
+ });
163
+
164
+ profileCmd
165
+ .command("rm")
166
+ .description("Remove a profile")
167
+ .requiredOption("--name <name>", "Profile name")
168
+ .action(async (opts, command) => {
169
+ const globals = command.optsWithGlobals() as { json?: boolean };
170
+ const file = await readProfiles();
171
+ const removed = removeProfile(file, String(opts.name));
172
+
173
+ const final =
174
+ file.defaultProfile === opts.name
175
+ ? { ...removed, defaultProfile: undefined }
176
+ : removed;
177
+
178
+ await writeProfiles(final);
179
+
180
+ if (globals.json) {
181
+ process.stdout.write(
182
+ `${JSON.stringify({ ok: true, profile: String(opts.name) })}\n`,
183
+ );
184
+ return;
185
+ }
186
+
187
+ process.stdout.write(`ok: removed ${String(opts.name)}\n`);
188
+ });
189
+
190
+ profileCmd
191
+ .command("use")
192
+ .description("Set the default profile")
193
+ .requiredOption("--name <name>", "Profile name")
194
+ .action(async (opts, command) => {
195
+ const globals = command.optsWithGlobals() as { json?: boolean };
196
+ const file = await readProfiles();
197
+ const profile = getProfile(file, String(opts.name));
198
+ if (!profile) throw new Error(`Profile not found: ${String(opts.name)}`);
199
+
200
+ await writeProfiles({ ...file, defaultProfile: String(opts.name) });
201
+
202
+ if (globals.json) {
203
+ process.stdout.write(
204
+ `${JSON.stringify({ ok: true, defaultProfile: String(opts.name) })}\n`,
205
+ );
206
+ return;
207
+ }
208
+ process.stdout.write(`ok: default ${String(opts.name)}\n`);
209
+ });
210
+
211
+ const authCmd = program.command("auth").description("Manage auth secrets");
212
+
213
+ authCmd
214
+ .command("token")
215
+ .description("Set or get bearer token for a profile")
216
+ .option("--name <name>", "Profile name (defaults to global --profile)")
217
+ .option("--set <token>", "Set token")
218
+ .option("--get", "Get token")
219
+ .option("--delete", "Delete token")
220
+ .action(async (opts, command) => {
221
+ const globals = command.optsWithGlobals() as {
222
+ json?: boolean;
223
+ profile?: string;
224
+ };
225
+
226
+ const profileName = String(opts.name ?? globals.profile ?? "");
227
+ if (!profileName) {
228
+ throw new Error(
229
+ "Missing profile name. Provide --name <name> or global --profile <name>.",
230
+ );
231
+ }
232
+ if (opts.set && (opts.get || opts.delete)) {
233
+ throw new Error("Use only one of --set, --get, --delete");
234
+ }
235
+ if (opts.get && opts.delete) {
236
+ throw new Error("Use only one of --get or --delete");
237
+ }
238
+ if (!opts.set && !opts.get && !opts.delete) {
239
+ throw new Error("Provide one of --set, --get, --delete");
240
+ }
241
+
242
+ if (typeof opts.set === "string") {
243
+ await setToken(ctx.loaded.id, profileName, opts.set);
244
+ if (globals.json) {
245
+ process.stdout.write(
246
+ `${JSON.stringify({ ok: true, profile: profileName })}\n`,
247
+ );
248
+ return;
249
+ }
250
+ process.stdout.write(`ok: token set for ${profileName}\n`);
251
+ return;
252
+ }
253
+
254
+ if (opts.get) {
255
+ const token = await getToken(ctx.loaded.id, profileName);
256
+ if (globals.json) {
257
+ process.stdout.write(
258
+ `${JSON.stringify({ profile: profileName, token })}\n`,
259
+ );
260
+ return;
261
+ }
262
+ process.stdout.write(`${token ?? ""}\n`);
263
+ return;
264
+ }
265
+
266
+ if (opts.delete) {
267
+ const ok = await deleteToken(ctx.loaded.id, profileName);
268
+ if (globals.json) {
269
+ process.stdout.write(
270
+ `${JSON.stringify({ ok, profile: profileName })}\n`,
271
+ );
272
+ return;
273
+ }
274
+ process.stdout.write(`ok: ${ok ? "deleted" : "not-found"}\n`);
275
+ }
276
+ });
277
+
278
+ program
279
+ .command("__schema")
280
+ .description("Print indexed operations (machine-readable when --json)")
281
+ .option("--pretty", "Pretty-print JSON when used with --json")
282
+ .option("--min", "Minimal JSON output (commands + metadata only)")
283
+ .action(async (_opts, command) => {
284
+ const flags = command.optsWithGlobals() as {
285
+ json?: boolean;
286
+ pretty?: boolean;
287
+ min?: boolean;
288
+ };
289
+
290
+ if (flags.json) {
291
+ const pretty = Boolean(flags.pretty);
292
+ const payload = flags.min
293
+ ? toMinimalSchemaOutput(ctx.schema)
294
+ : ctx.schema;
295
+ const text = stableStringify(payload, { space: pretty ? 2 : 0 });
296
+ process.stdout.write(`${text}\n`);
297
+ return;
298
+ }
299
+
300
+ process.stdout.write(`${ctx.schema.openapi.title ?? "(untitled)"}\n`);
301
+ process.stdout.write(`OpenAPI: ${ctx.schema.openapi.version}\n`);
302
+ process.stdout.write(
303
+ `Spec: ${ctx.schema.spec.id} (${ctx.schema.spec.source})\n`,
304
+ );
305
+ process.stdout.write(`Fingerprint: ${ctx.schema.spec.fingerprint}\n`);
306
+ process.stdout.write(`Servers: ${ctx.schema.servers.length}\n`);
307
+ process.stdout.write(`Auth Schemes: ${ctx.schema.authSchemes.length}\n`);
308
+ process.stdout.write(`Operations: ${ctx.schema.operations.length}\n`);
309
+
310
+ for (const op of ctx.schema.operations) {
311
+ const id = op.operationId ? ` (${op.operationId})` : "";
312
+ process.stdout.write(`- ${op.method} ${op.path}${id}\n`);
313
+ }
314
+
315
+ if (ctx.schema.planned?.length) {
316
+ process.stdout.write("\nPlanned commands:\n");
317
+ for (const op of ctx.schema.planned) {
318
+ const args = op.pathArgs.length
319
+ ? ` ${op.pathArgs.map((a) => `<${a}>`).join(" ")}`
320
+ : "";
321
+ process.stdout.write(
322
+ `- opencli ${op.resource} ${op.action}${args}\n`,
323
+ );
324
+ }
325
+ }
326
+ });
327
+
328
+ addGeneratedCommands(program, {
329
+ servers: ctx.servers,
330
+ authSchemes: ctx.authSchemes,
331
+ commands: ctx.commands,
332
+ specId: ctx.loaded.id,
333
+ });
334
+
335
+ await program.parseAsync(argv);
336
+ }