@nasebanal/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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NASEBANAL
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ # NASEBANAL CLI
2
+
3
+ `nb` — a gcloud-style command-line interface for the NASEBANAL APIs.
4
+
5
+ The command tree is **generated from the OpenAPI contracts** published by
6
+ [`nb-api-specs`](https://github.com/nasebanal/nb-api-specs). Every endpoint
7
+ becomes a `nb <api> <resource> <verb>` command, so the CLI stays a pure function
8
+ of the API contracts — it is the command-line consumer in the NASEBANAL
9
+ Contract-Driven Development (CDD) pipeline, alongside the web frontends.
10
+
11
+ ```console
12
+ $ nb account me get
13
+ $ nb recorder records list
14
+ $ nb target shares accept <id>
15
+ $ nb account me tokens create --data '{"name":"ci"}'
16
+ ```
17
+
18
+ ## Tech Stack
19
+
20
+ - **Language:** TypeScript (ESM, Node ≥ 20)
21
+ - **CLI framework:** commander
22
+ - **Spec parsing:** yaml
23
+ - **Contracts:** `@nasebanal/api-specs-*` (OpenAPI 3.0) — the same packages backends and frontends consume
24
+ - **Authentication:** browser/device-code Auth0 login, or a NASEBANAL Personal Access Token (`nbpat_…`)
25
+ - **Testing:** Vitest
26
+ - **Distribution:** npm (`npx @nasebanal/cli`); single-binary builds are a planned follow-up
27
+
28
+ ## Installation
29
+
30
+ The CLI installs the `nb` command. Node ≥ 20 is required.
31
+
32
+ ### From the npm registry (end users)
33
+
34
+ `@nasebanal/cli` is published to the public npm registry — no `git clone`, no
35
+ registry configuration, no token:
36
+
37
+ ```bash
38
+ npm install -g @nasebanal/cli # global: the `nb` command on your PATH
39
+ nb --version
40
+ nb --help
41
+
42
+ # or run without installing (pin a version for reproducible scripts)
43
+ npx @nasebanal/cli@0.1.0 account me get
44
+ ```
45
+
46
+ The published package bundles the OpenAPI specs, so no `nb-api-specs` checkout is
47
+ needed for a registry install.
48
+
49
+ > **NASEBANAL developers:** if your `~/.npmrc` maps `@nasebanal:registry` to
50
+ > GitHub Packages (needed for the private `@nasebanal/shared-navigation` /
51
+ > `api-specs-*` packages), it captures this package too. Install with
52
+ > `npm install -g @nasebanal/cli --@nasebanal:registry=https://registry.npmjs.org/`.
53
+
54
+ ### Versioning and releases
55
+
56
+ Versions follow SemVer (0.x until 1.0: breaking changes bump the minor). Each
57
+ release bundles a fixed set of API contract versions, recorded in
58
+ [`spec-versions.json`](spec-versions.json). A release is cut by pushing a
59
+ `vX.Y.Z` tag that matches `package.json`; the `Release` workflow publishes it to
60
+ npm with provenance.
61
+
62
+ ### From source (contributors)
63
+
64
+ Requires a sibling `nb-api-specs` checkout (the build syncs specs from it):
65
+
66
+ ```bash
67
+ git clone https://github.com/nasebanal/nb-cli.git
68
+ cd nb-cli
69
+ npm install
70
+ npm run build # sync specs + compile to dist/
71
+ npm link # symlink the `nb` command onto your PATH
72
+ nb --help
73
+
74
+ # undo later with: npm unlink -g @nasebanal/cli
75
+ ```
76
+
77
+ Without linking, you can always run the compiled binary directly
78
+ (`node dist/index.js …`) or from source via `npm run dev -- …` (see
79
+ [Local Development](#local-development)).
80
+
81
+ ### First run
82
+
83
+ ```bash
84
+ nb auth login # browser login (see Authentication below)
85
+ nb account me get # verify the token works against a real API
86
+ ```
87
+
88
+ ## Command Mapping
89
+
90
+ The specs carry no `operationId` yet, so commands are derived deterministically
91
+ from each path + method:
92
+
93
+ | OpenAPI operation | Command |
94
+ |-------------------|---------|
95
+ | `GET /api/v1/me` (singleton) | `nb account me get` |
96
+ | `GET /api/v1/me/tokens` (collection) | `nb account me tokens list` |
97
+ | `POST /api/v1/me/tokens` | `nb account me tokens create --data '…'` |
98
+ | `DELETE /api/v1/me/tokens/{id}` | `nb account me tokens delete <id>` |
99
+ | `POST /api/v1/shares/{id}/accept` (action) | `nb target shares accept <id>` |
100
+ | `POST /api/v1/stripe/checkout` | `nb account stripe checkout --data '…'` |
101
+
102
+ Rules: a path ending in a plural noun is a collection (`list` / `create`); a
103
+ singular noun is a singleton (`get`) or, for write methods, an action whose
104
+ segment becomes the verb. Path parameters become positional arguments; query
105
+ parameters become `--flags`; a request body is `--data <json>` (or `--data @file`).
106
+ When two operations collapse to the same command (e.g. `/shares/accept` and
107
+ `/shares/{id}/accept`), the more specific one — more path parameters — wins.
108
+
109
+ > When the specs gain `operationId` / `x-cli-name` hints, `src/spec/build.ts` is
110
+ > the single place that consumes them. Adding those hints is the recommended way
111
+ > to refine command names without touching the CLI.
112
+
113
+ ## Authentication
114
+
115
+ `nb auth login` opens a browser, you log in with Auth0, and control returns to
116
+ the terminal automatically — the same experience as `gcloud auth login`.
117
+
118
+ ```bash
119
+ nb auth login # browser login (loopback OAuth + PKCE)
120
+ nb auth login --no-launch-browser # device-code flow (SSH / headless boxes)
121
+ nb auth status # show active credential source + environment
122
+ nb auth logout # remove stored credentials
123
+ ```
124
+
125
+ Under the hood this is OAuth 2.0 Authorization Code + PKCE against a shared
126
+ Auth0 **Native** application. On login the CLI stores the access token and a
127
+ refresh token; expired access tokens are refreshed automatically on the next
128
+ command, so you rarely need to log in again.
129
+
130
+ **PAT / CI auth** is still supported for scripting:
131
+
132
+ ```bash
133
+ nb auth login --token nbpat_… # store a Personal Access Token non-interactively
134
+ nb auth login --token-stdin # paste a PAT from stdin (no shell history)
135
+ NB_TOKEN=nbpat_… nb … # env override (wins over everything; no login needed)
136
+ ```
137
+
138
+ Create a PAT in the web app under **Settings → API tokens** (Account API).
139
+
140
+ - Credentials live at `~/.config/nasebanal/config.json` with `0600` permissions.
141
+ - Precedence on each request: `NB_TOKEN` env → OAuth tokens → stored PAT.
142
+
143
+ ### Auth0 setup (one time, tenant admin)
144
+
145
+ The browser flow needs a public **Native** application in Auth0:
146
+
147
+ 1. Create an Application of type **Native** (PKCE, no client secret).
148
+ 2. Grant types: **Authorization Code**, **Refresh Token**, **Device Code**.
149
+ 3. Allowed Callback URLs — this tenant validates the port, so register every
150
+ loopback port the CLI may use (the CLI binds the first free one):
151
+ `http://localhost:8085/callback, http://localhost:8086/callback, http://localhost:8087/callback, http://localhost:8088/callback, http://localhost:8089/callback`
152
+ (`NB_AUTH0_REDIRECT_PORT` pins a single port if you prefer.)
153
+ 4. Enable **Allow Offline Access** on the NASEBANAL API (for refresh tokens).
154
+ 5. Put the resulting **Client ID** (public, not a secret) into `AUTH0_CLIENT_ID`
155
+ in `src/oauth.ts`, or set `NB_AUTH0_CLIENT_ID` to test before baking it in.
156
+
157
+ Overridable env vars: `NB_AUTH0_DOMAIN`, `NB_AUTH0_AUDIENCE`,
158
+ `NB_AUTH0_CLIENT_ID`, `NB_AUTH0_REDIRECT_PORT`.
159
+
160
+ ## Global Options
161
+
162
+ | Option | Description |
163
+ |--------|-------------|
164
+ | `--env <production\|local>` | Target environment; picks the matching `servers` entry from the spec. Also `NB_ENV`. |
165
+ | `--token <token>` | Override stored credentials with a PAT for one invocation. |
166
+ | `-V, --version` | Print the CLI version. |
167
+
168
+ The active environment defaults to `production`; `--env local` targets the
169
+ `wrangler dev` servers declared in each spec.
170
+
171
+ ## Adding a New API
172
+
173
+ 1. Publish/own its spec in `nb-api-specs` (it ships as `@nasebanal/api-specs-<api>`).
174
+ 2. Add an entry to `API_CATALOG` in `src/apis.ts` and to the list in `scripts/sync-specs.mjs`.
175
+ 3. `npm run sync-specs && npm run build` — the command group appears automatically.
176
+
177
+ ## Project Structure
178
+
179
+ ```
180
+ nb-cli/
181
+ ├── src/
182
+ │ ├── index.ts # Entry: global options, auth group, one group per API (★)
183
+ │ ├── apis.ts # API catalog (★ register new APIs here)
184
+ │ ├── auth.ts # `nb auth login | logout | status`
185
+ │ ├── oauth.ts # Auth0 loopback + device-code login, PKCE, token refresh
186
+ │ ├── config.ts # credentials + environment persisted to ~/.config/nasebanal/config.json
187
+ │ ├── http.ts # base-URL resolution + bearer request layer
188
+ │ ├── output.ts # JSON / error formatting
189
+ │ └── spec/
190
+ │ ├── loader.ts # read + parse spec YAML from specs/
191
+ │ ├── build.ts # ★ OpenAPI -> commander command tree (the core mapping)
192
+ │ └── build.test.ts # command-mapping tests
193
+ ├── scripts/
194
+ │ └── sync-specs.mjs # Copy specs into specs/ (local dev convenience)
195
+ ├── specs/ # ★ Generated: OpenAPI specs, not committed (see below)
196
+ ├── package.json
197
+ ├── tsconfig.json
198
+ └── README.md
199
+ ```
200
+
201
+ Files marked with ★ are the usual customization targets.
202
+
203
+ ## Development
204
+
205
+ ### Local Development
206
+
207
+ ```bash
208
+ npm install
209
+ npm run sync-specs # copy specs from a sibling nb-api-specs checkout into specs/
210
+ npm run dev -- --help # run from source (tsx), e.g. `npm run dev -- account me get`
211
+ npm run build # sync specs + type-check + emit dist/
212
+ npm test # Vitest
213
+ npm run typecheck # tsc --noEmit
214
+ ```
215
+
216
+ `sync-specs` resolves each spec from, in order:
217
+
218
+ 1. `node_modules/@nasebanal/api-specs-<api>/spec.yaml` (installed package), then
219
+ 2. `../nb-api-specs/packages/<api>/spec.yaml` (local monorepo checkout).
220
+
221
+ In production the published `@nasebanal/api-specs-*` packages are pinned as
222
+ dependencies — the consumer side of CDD. In local dev we copy straight from the
223
+ sibling repo so contract edits are visible without a publish (mirroring how
224
+ `@nasebanal/shared-navigation` has both a published and a local-copy path).
225
+
226
+ ### Conventions
227
+
228
+ - **The command surface is generated, not hand-written** — refine commands by
229
+ improving the contract (e.g. add `operationId`), not by special-casing the CLI.
230
+ - **Specs are dependencies, not source** — `specs/` is git-ignored; production
231
+ pins the published `@nasebanal/api-specs-*` packages.
232
+ - **Never commit a PAT** — config lives outside the repo; CI uses `NB_TOKEN`.
233
+ - **Deploy/release via PR merge and CI** — never publish from a local machine.
234
+
235
+ ## License
236
+
237
+ MIT
package/dist/apis.js ADDED
@@ -0,0 +1,6 @@
1
+ export const API_CATALOG = [
2
+ { name: "account", spec: "account.yaml", summary: "User profiles, subscriptions, billing, and API tokens" },
3
+ { name: "recorder", spec: "recorder.yaml", summary: "Records and tags (NASEBANAL Recorder)" },
4
+ { name: "target", spec: "target.yaml", summary: "Targets, cells, and shares (NASEBANAL Target)" },
5
+ { name: "app-template", spec: "app-template.yaml", summary: "Reference API surface (App Template)" },
6
+ ];
package/dist/auth.js ADDED
@@ -0,0 +1,98 @@
1
+ /**
2
+ * `nb auth` commands.
3
+ *
4
+ * The default `nb auth login` is an interactive Auth0 login (gcloud-style): it
5
+ * opens a browser, you log in, and control returns to the terminal automatically
6
+ * (loopback flow), falling back to a device code on headless boxes. For CI and
7
+ * scripting a NASEBANAL Personal Access Token (`nbpat_…`) is still supported via
8
+ * `--token` / `--token-stdin`, or the `NB_TOKEN` env var (which overrides both).
9
+ */
10
+ import { Command } from "commander";
11
+ import { createInterface } from "node:readline";
12
+ import { loadConfig, saveConfig, resolveToken, resolveEnv, configFilePath } from "./config.js";
13
+ import { login } from "./oauth.js";
14
+ function readSecretLine(promptText) {
15
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: false });
16
+ process.stdout.write(promptText);
17
+ return new Promise((resolve) => {
18
+ rl.once("line", (line) => {
19
+ rl.close();
20
+ resolve(line.trim());
21
+ });
22
+ });
23
+ }
24
+ function storePat(config, token) {
25
+ if (!token) {
26
+ process.stderr.write("No token provided.\n");
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+ if (!token.startsWith("nbpat_")) {
31
+ process.stderr.write("warning: token does not start with 'nbpat_'; storing anyway.\n");
32
+ }
33
+ config.token = token;
34
+ delete config.auth;
35
+ saveConfig(config);
36
+ process.stdout.write(`Token saved to ${configFilePath()}\n`);
37
+ }
38
+ export function buildAuthCommand() {
39
+ const auth = new Command("auth").description("Manage CLI authentication");
40
+ auth
41
+ .command("login")
42
+ .description("Log in via the browser (or store a PAT with --token / --token-stdin)")
43
+ .option("--no-launch-browser", "use the device-code flow instead of opening a browser")
44
+ .option("--token <token>", "store a Personal Access Token (nbpat_…) non-interactively")
45
+ .option("--token-stdin", "read a Personal Access Token from stdin")
46
+ .action(async (opts) => {
47
+ const config = loadConfig();
48
+ if (opts.token !== undefined) {
49
+ storePat(config, opts.token);
50
+ return;
51
+ }
52
+ if (opts.tokenStdin) {
53
+ storePat(config, await readSecretLine("Paste your NASEBANAL PAT (nbpat_…): "));
54
+ return;
55
+ }
56
+ const tokens = await login({ launchBrowser: opts.launchBrowser !== false });
57
+ config.auth = tokens;
58
+ delete config.token;
59
+ saveConfig(config);
60
+ const expiry = tokens.expires_at ? new Date(tokens.expires_at).toLocaleString() : "n/a";
61
+ process.stdout.write(`\n✓ Logged in. Credentials saved to ${configFilePath()}\n`);
62
+ process.stdout.write(` Access token expires: ${expiry}${tokens.refresh_token ? " (auto-refreshes)" : ""}\n`);
63
+ });
64
+ auth
65
+ .command("logout")
66
+ .description("Remove stored credentials")
67
+ .action(() => {
68
+ const config = loadConfig();
69
+ delete config.token;
70
+ delete config.auth;
71
+ saveConfig(config);
72
+ process.stdout.write("Credentials removed.\n");
73
+ });
74
+ auth
75
+ .command("status")
76
+ .description("Show the active credential source and target environment")
77
+ .action(() => {
78
+ const config = loadConfig();
79
+ const token = resolveToken(config);
80
+ const source = process.env.NB_TOKEN
81
+ ? "NB_TOKEN env"
82
+ : config.auth
83
+ ? "OAuth login"
84
+ : config.token
85
+ ? "stored PAT"
86
+ : "none";
87
+ const masked = token ? `${token.slice(0, 10)}…${token.slice(-4)}` : "(not set)";
88
+ process.stdout.write(`Environment: ${resolveEnv(config)}\n`);
89
+ process.stdout.write(`Token: ${masked} [source: ${source}]\n`);
90
+ if (config.auth?.expires_at) {
91
+ const expired = config.auth.expires_at <= Date.now();
92
+ process.stdout.write(`Expires: ${new Date(config.auth.expires_at).toLocaleString()}` +
93
+ `${expired ? config.auth.refresh_token ? " (expired — will refresh)" : " (expired)" : ""}\n`);
94
+ }
95
+ process.stdout.write(`Config file: ${configFilePath()}\n`);
96
+ });
97
+ return auth;
98
+ }
package/dist/config.js ADDED
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Persistent CLI configuration (credentials, target environment).
3
+ *
4
+ * Stored as JSON at `$XDG_CONFIG_HOME/nasebanal/config.json` (falling back to
5
+ * `~/.config/nasebanal/config.json`). Two credential shapes can live here:
6
+ * - `auth` — OAuth tokens from `nb auth login` (browser / device-code flow).
7
+ * - `token` — a Personal Access Token (`nbpat_…`) for manual / scripted use.
8
+ * `NB_TOKEN` in the environment always overrides both, which is the supported
9
+ * path for CI.
10
+ */
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync } from "node:fs";
14
+ import { refreshTokens } from "./oauth.js";
15
+ function configDir() {
16
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
17
+ return join(base, "nasebanal");
18
+ }
19
+ function configPath() {
20
+ return join(configDir(), "config.json");
21
+ }
22
+ export function loadConfig() {
23
+ const path = configPath();
24
+ if (!existsSync(path))
25
+ return {};
26
+ try {
27
+ return JSON.parse(readFileSync(path, "utf8"));
28
+ }
29
+ catch {
30
+ return {};
31
+ }
32
+ }
33
+ export function saveConfig(config) {
34
+ const dir = configDir();
35
+ mkdirSync(dir, { recursive: true });
36
+ const path = configPath();
37
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", { mode: 0o600 });
38
+ // Tighten perms in case the file already existed with a looser mode.
39
+ try {
40
+ chmodSync(path, 0o600);
41
+ }
42
+ catch {
43
+ /* best effort */
44
+ }
45
+ }
46
+ /**
47
+ * Resolve the active token synchronously, WITHOUT refreshing — for display only
48
+ * (`nb auth status`). Precedence: `NB_TOKEN` env, OAuth access token, stored PAT.
49
+ */
50
+ export function resolveToken(config = loadConfig()) {
51
+ return process.env.NB_TOKEN || config.auth?.access_token || config.token;
52
+ }
53
+ /** Refresh OAuth tokens in-place when the access token is expired (or close to). */
54
+ async function ensureFreshAuth(config) {
55
+ const auth = config.auth;
56
+ if (!auth)
57
+ return undefined;
58
+ const skewMs = 60_000;
59
+ if (!auth.expires_at || auth.expires_at - skewMs > Date.now())
60
+ return auth;
61
+ if (!auth.refresh_token)
62
+ return auth; // can't refresh; let the request 401
63
+ try {
64
+ const next = await refreshTokens(auth.refresh_token);
65
+ // Token rotation may not return a new refresh token — keep the old one.
66
+ const merged = { ...next, refresh_token: next.refresh_token ?? auth.refresh_token };
67
+ config.auth = merged;
68
+ saveConfig(config);
69
+ return merged;
70
+ }
71
+ catch {
72
+ return auth; // refresh failed; surface as a 401 on the actual request
73
+ }
74
+ }
75
+ /**
76
+ * Resolve the token to send on API requests, refreshing an expired OAuth access
77
+ * token first. Precedence: `NB_TOKEN` env, OAuth (refreshed), stored PAT.
78
+ */
79
+ export async function resolveAccessToken(config = loadConfig()) {
80
+ if (process.env.NB_TOKEN)
81
+ return process.env.NB_TOKEN;
82
+ if (config.auth) {
83
+ const fresh = await ensureFreshAuth(config);
84
+ return fresh?.access_token;
85
+ }
86
+ return config.token;
87
+ }
88
+ /** Resolve the active environment: `NB_ENV` env wins, else stored, else production. */
89
+ export function resolveEnv(config = loadConfig()) {
90
+ const fromEnv = process.env.NB_ENV;
91
+ if (fromEnv === "local" || fromEnv === "production")
92
+ return fromEnv;
93
+ return config.env ?? "production";
94
+ }
95
+ export function configFilePath() {
96
+ return configPath();
97
+ }
package/dist/http.js ADDED
@@ -0,0 +1,55 @@
1
+ export class ApiError extends Error {
2
+ status;
3
+ data;
4
+ constructor(message, status, data) {
5
+ super(message);
6
+ this.status = status;
7
+ this.data = data;
8
+ this.name = "ApiError";
9
+ }
10
+ }
11
+ /** Pick the server URL matching the active environment. */
12
+ export function resolveBaseUrl(servers, env) {
13
+ if (!servers || servers.length === 0) {
14
+ throw new Error("Spec declares no servers; cannot resolve a base URL.");
15
+ }
16
+ const isLocal = (s) => /localhost|127\.0\.0\.1/.test(s.url);
17
+ const match = env === "local" ? servers.find(isLocal) : servers.find((s) => !isLocal(s));
18
+ return (match ?? servers[0]).url.replace(/\/$/, "");
19
+ }
20
+ export async function request(opts) {
21
+ const url = new URL(opts.baseUrl + opts.path);
22
+ for (const [key, value] of Object.entries(opts.query ?? {})) {
23
+ if (value !== undefined)
24
+ url.searchParams.set(key, value);
25
+ }
26
+ const headers = { Accept: "application/json" };
27
+ if (opts.token)
28
+ headers.Authorization = `Bearer ${opts.token}`;
29
+ const init = { method: opts.method.toUpperCase(), headers };
30
+ if (opts.body !== undefined) {
31
+ headers["Content-Type"] = "application/json";
32
+ init.body = JSON.stringify(opts.body);
33
+ }
34
+ let res;
35
+ try {
36
+ res = await fetch(url, init);
37
+ }
38
+ catch (err) {
39
+ throw new Error(`Request to ${url.host} failed: ${err.message}`);
40
+ }
41
+ const text = await res.text();
42
+ let data = text;
43
+ if (text) {
44
+ try {
45
+ data = JSON.parse(text);
46
+ }
47
+ catch {
48
+ /* leave as raw text */
49
+ }
50
+ }
51
+ if (!res.ok) {
52
+ throw new ApiError(`${res.status} ${res.statusText}`, res.status, data);
53
+ }
54
+ return { status: res.status, ok: true, data };
55
+ }
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `nb` — gcloud-style CLI for NASEBANAL APIs.
4
+ *
5
+ * The command tree under each API group is generated at startup from the
6
+ * OpenAPI specs in `specs/` (synced from nb-api-specs). This file only wires up
7
+ * the global options, the `auth` group, and one command group per catalog entry.
8
+ */
9
+ import { createRequire } from "node:module";
10
+ import { Command } from "commander";
11
+ import { API_CATALOG } from "./apis.js";
12
+ import { buildApiCommand } from "./spec/build.js";
13
+ import { buildAuthCommand } from "./auth.js";
14
+ import { specExists } from "./spec/loader.js";
15
+ import { printError } from "./output.js";
16
+ // Single source of truth for the version: package.json (dist/ and src/ both sit
17
+ // next to it). A hard-coded string here drifts from the release tag.
18
+ const { version } = createRequire(import.meta.url)("../package.json");
19
+ const program = new Command();
20
+ program
21
+ .name("nb")
22
+ .description("Command-line interface for NASEBANAL APIs (generated from OpenAPI contracts).")
23
+ .version(version)
24
+ .option("--env <env>", "target environment: production | local")
25
+ .option("--token <token>", "override the stored PAT for this invocation")
26
+ .showHelpAfterError();
27
+ program.addCommand(buildAuthCommand());
28
+ for (const api of API_CATALOG) {
29
+ if (!specExists(api.spec)) {
30
+ // Spec not synced yet — register a stub that explains how to fix it rather
31
+ // than crashing the whole CLI.
32
+ program
33
+ .command(api.name)
34
+ .description(`${api.summary} (spec not loaded)`)
35
+ .allowUnknownOption()
36
+ .action(() => {
37
+ printError(new Error(`Spec '${api.spec}' is missing. Run \`npm run sync-specs\` (local dev) or reinstall the package.`));
38
+ process.exitCode = 1;
39
+ });
40
+ continue;
41
+ }
42
+ program.addCommand(buildApiCommand(api));
43
+ }
44
+ async function main() {
45
+ try {
46
+ await program.parseAsync(process.argv);
47
+ }
48
+ catch (err) {
49
+ printError(err);
50
+ process.exitCode = 1;
51
+ }
52
+ }
53
+ void main();