@agenzo/admin-cli 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CardInfoLink
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,285 @@
1
+ # agenzo-admin-cli
2
+
3
+ [![npm version](https://img.shields.io/npm/v/agenzo-admin-cli)](https://www.npmjs.com/package/agenzo-admin-cli)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
5
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
6
+
7
+ Control-plane CLI for the Agenzo platform — login, organizations, developers, API keys, and settlement accounts. Built for humans and AI Agents, with interactive prompts, transparent token refresh, multi-org switching, machine-readable JSON output, and stable exit codes.
8
+
9
+ [Install](#installation) · [AI Agent Skill](#ai-agent-skill) · [Quick Start](#quick-start) · [Commands](#commands) · [Output](#output-formats) · [Exit Codes](#exit-codes) · [Auth](#authentication) · [Contributing](CONTRIBUTING.md)
10
+
11
+ > **Scope** — `agenzo-admin-cli` owns the **control plane** (`auth`, `config`, `orgs`, `developers`, `keys`, `accounts`). The **runtime plane** (`payment-methods`, `payment-tokens`) lives in the separate `agenzo-token-cli` binary.
12
+
13
+ ## Why agenzo-admin-cli?
14
+
15
+ - **Agent-Native Design** — Structured [SKILL.md](SKILL.md) out of the box, plus default-on machine output knobs (`--format json`, stable exit codes) so AI Agents can branch on results without scraping text
16
+ - **Noun-Verb Surface** — Predictable `<noun> <verb>` commands (`auth login`, `orgs get`, `keys create`) that are easy to discover and script
17
+ - **Interactive & Scriptable** — Smart prompts for humans, or pass every flag for automation
18
+ - **Secure by Default** — Bearer tokens never touch stdout; the one-time API key is shown once and kept out of read commands
19
+ - **Multi-Developer** — One org, multiple developers, scoped API keys
20
+
21
+ ## Features
22
+
23
+ | Category | Capabilities |
24
+ |----------|-------------|
25
+ | 🔐 Auth | Magic Link login, auto-registration, transparent token refresh, multi-org switch |
26
+ | 👤 Developers | Create, list, get, update developers; set `--billing-mode` (`pay_per_call` / `monthly_settlement`) at creation |
27
+ | 🔑 API Keys | Create, list, get, rotate, disable keys; per-key `--scope` (`token` / `merchant` / `payment`) |
28
+ | 🏢 Orgs | View / update the current org, list signed-in orgs, switch active org |
29
+ | 💳 Accounts | Query a developer's settlement account (balance, currency, status) |
30
+ | ⚙️ Config | Set API host, view config, reset to defaults |
31
+ | 📤 Output | Human `table` by default, opt-in `--format json` for Agents (`AGENZO_FORMAT` supported) |
32
+ | 🚦 Exit Codes | Stable `0–5` matrix + SCREAMING_SNAKE error envelope on stderr |
33
+ | 🔁 Idempotency | `--idempotency-key` forwarded verbatim on every server-write command |
34
+
35
+ ## Installation
36
+
37
+ ```bash
38
+ npm install -g agenzo-admin-cli
39
+ ```
40
+
41
+ **Requirements**: Node.js 18+
42
+
43
+ ### Version compatibility
44
+
45
+ The server advertises the minimum required CLI version via the `X-CLI-Min-Version` response header. If your installed CLI is below that minimum, any command refuses to run and exits with code `2`:
46
+
47
+ ```
48
+ ✗ CLI 1.2.0 is below the required minimum 1.3.0.
49
+ ℹ Upgrade: npm install -g agenzo-admin-cli@latest
50
+ ```
51
+
52
+ Run the upgrade command and retry. The floor is controlled server-side (`AGENT_PAY_CLI_MIN_VERSION` env var), so raising it does not require a CLI release.
53
+
54
+ ## AI Agent Skill
55
+
56
+ This CLI ships with a structured [SKILL.md](SKILL.md) that AI Agents can use to understand and operate the control-plane flows. The skill covers:
57
+
58
+ - The onboarding flow (login → developer → API key)
59
+ - All command signatures and parameters
60
+ - Output-format and idempotency conventions
61
+ - Common errors and exit codes
62
+
63
+ To use with your AI Agent, include `SKILL.md` in the agent's context or tool definition.
64
+
65
+ ## Quick Start
66
+
67
+ ### For Humans
68
+
69
+ ```bash
70
+ # 1. Sign in (auto-registers on first use)
71
+ agenzo-admin-cli auth login --email your@email.com
72
+
73
+ # 2. Create a developer
74
+ agenzo-admin-cli developers create --developer-name "My Agent" --developer-email agent@example.com
75
+
76
+ # 3. Create an API Key (save it — only shown once!)
77
+ agenzo-admin-cli keys create --developer-id dev_01KPX... --key-name "Production Key"
78
+
79
+ # 4. Inspect your organization
80
+ agenzo-admin-cli orgs get
81
+ ```
82
+
83
+ ### For AI Agents
84
+
85
+ > Read [SKILL.md](SKILL.md) for the complete guide. Key points:
86
+
87
+ 1. Output defaults to human `table`. Opt in to machine output with `--format json` (or set `AGENZO_FORMAT=json`).
88
+ 2. stdout carries **only** the payload (one JSON value in json mode); all logs, prompts, and spinners go to stderr.
89
+ 3. Branch on the exit code, not on text — see the [Exit Codes](#exit-codes) matrix.
90
+ 4. Pass `--idempotency-key` on writes so a retried `create` / `rotate` is safe to repeat.
91
+
92
+ ```bash
93
+ # Machine-readable output, payload-only on stdout
94
+ agenzo-admin-cli orgs get --format json
95
+ AGENZO_FORMAT=json agenzo-admin-cli developers list
96
+
97
+ # Pipe straight into jq
98
+ agenzo-admin-cli config show --format json | jq .api_host
99
+
100
+ # Safe-retry a write with a caller-supplied idempotency key
101
+ agenzo-admin-cli developers create \
102
+ --developer-name "My Agent" --developer-email agent@example.com \
103
+ --idempotency-key dev-create-001 --format json
104
+ ```
105
+
106
+ ## Commands
107
+
108
+ 19 commands across 6 noun groups:
109
+
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `auth login` | Sign in via Magic Link (auto-registers on first use) |
113
+ | `auth logout` | Sign out of the current organization |
114
+ | `config set-host / show / reset-host` | Local API host configuration |
115
+ | `orgs get / update / list / switch` | Organization management (`get` replaces the legacy `orgs me`) |
116
+ | `developers create / list / get / update` | Developer management (`create` takes `--billing-mode`) |
117
+ | `keys create / list / get / rotate / disable` | API Key management (`create` takes `--scope`) |
118
+ | `accounts get` | Query a developer's settlement account |
119
+
120
+ ## Command Reference
121
+
122
+ ### Authentication
123
+ ```bash
124
+ agenzo-admin-cli auth login --email your@email.com # Sign in / auto-register via Magic Link
125
+ agenzo-admin-cli auth login --email your@email.com --idempotency-key login-001
126
+ agenzo-admin-cli auth logout # Sign out of the current org (local + best-effort)
127
+ ```
128
+
129
+ `login` and `logout` are grouped under the `auth` noun (they are **not** top-level commands).
130
+
131
+ ### Organization Management
132
+ ```bash
133
+ agenzo-admin-cli orgs get # View current org (replaces the old `orgs me`)
134
+ agenzo-admin-cli orgs list # List all signed-in orgs (local, host-filtered)
135
+ agenzo-admin-cli orgs switch <org_id> # Switch active org (local; cross-env guarded)
136
+ agenzo-admin-cli orgs update --name "New Org Name" --idempotency-key org-001
137
+ agenzo-admin-cli orgs update --email new@example.com # Update org email (requires verification)
138
+ ```
139
+
140
+ ### Developer Management
141
+ ```bash
142
+ agenzo-admin-cli developers create --developer-name "My Agent" --developer-email agent@example.com --idempotency-key dev-001
143
+ # Optionally set the billing mode (default: pay_per_call):
144
+ agenzo-admin-cli developers create --developer-name "Monthly Agent" --developer-email ops@example.com --billing-mode monthly_settlement --idempotency-key dev-002
145
+ agenzo-admin-cli developers list
146
+ agenzo-admin-cli developers get <developer_id>
147
+ agenzo-admin-cli developers update <developer_id> --name "New Name" --idempotency-key dev-003
148
+ agenzo-admin-cli developers update <developer_id> --email new@example.com
149
+ ```
150
+
151
+ `--billing-mode` is one of `pay_per_call` (default) or `monthly_settlement`, fixed at creation (switching is an offline admin operation). A settlement account is auto-provisioned for every developer — query it with `accounts get`.
152
+
153
+ ### API Key Management
154
+ ```bash
155
+ agenzo-admin-cli keys create --developer-id <dev_id> --key-name "Prod Key" --idempotency-key key-001
156
+ # Restrict which runtime CLIs the key may call (default: all three):
157
+ agenzo-admin-cli keys create --developer-id <dev_id> --key-name "Token-only Key" --scope token --idempotency-key key-002
158
+ agenzo-admin-cli keys list --developer-id <dev_id>
159
+ agenzo-admin-cli keys get <key_id>
160
+ agenzo-admin-cli keys rotate <key_id> --idempotency-key key-003 # New key value (old one invalidated)
161
+ agenzo-admin-cli keys disable <key_id> --idempotency-key key-004 # Permanently disable key
162
+ ```
163
+
164
+ `--scope` is a comma-separated subset of `token` / `merchant` / `payment` (default: all three) controlling which runtime CLIs the key may call. The scope is persisted server-side and shown by `create` / `list` / `get` / `rotate`. The plaintext API key returned by `create` / `rotate` is shown **only once** (on stderr in `table` mode, in the JSON payload in `json` mode). `keys list` / `keys get` return metadata only — never the key value.
165
+
166
+ ### Accounts
167
+ ```bash
168
+ agenzo-admin-cli accounts get --developer-id <dev_id> # Query a developer's settlement account
169
+ ```
170
+
171
+ Returns the settlement account (`balance` in minor units, `currency`, `status`). An account is auto-created for every developer; the read-only `accounts get` is most relevant for `monthly_settlement` developers. Top-ups and status changes are offline / admin operations.
172
+
173
+ ### Configuration
174
+ ```bash
175
+ agenzo-admin-cli config set-host http://localhost:8000 # Set API host (e.g. local dev)
176
+ agenzo-admin-cli config reset-host # Reset to default (https://agent.everonet.com)
177
+ agenzo-admin-cli config show # Show current config (no API call)
178
+ ```
179
+
180
+ ## Output Formats
181
+
182
+ The CLI emits a deterministic output contract so the same command serves humans and Agents.
183
+
184
+ - **Default `table`** — a human-readable projection of the payload.
185
+ - **`--format json`** — opt in to machine output. stdout is exactly one `JSON.parse`-able value (the backend response `data` forwarded verbatim, or a small client-built object for local-only commands).
186
+ - **`AGENZO_FORMAT`** — set the format via environment when you cannot pass a flag.
187
+ - **Resolution precedence**: `--format` flag → `AGENZO_FORMAT` env → default `table`. Any invalid value falls back to `table`. The resolved value is always `json` or `table`.
188
+
189
+ > This default of `table` is a deliberate deviation from the platform CLI standard (which defaults to `json`); it preserves the existing human-friendly output while keeping a machine path one flag away.
190
+
191
+ stdout vs stderr:
192
+
193
+ | Stream | Content |
194
+ |--------|---------|
195
+ | **stdout** | The business payload only — one JSON value (`json`) or the rendered table/key-value view (`table`) |
196
+ | **stderr** | Everything else — spinners, prompts, hints, `--verbose` logs, the one-time key warning, and error envelopes |
197
+
198
+ ```bash
199
+ # stdout stays clean, so this round-trips through jq
200
+ agenzo-admin-cli orgs get --format json | jq .
201
+
202
+ # logs and the error envelope never pollute the JSON on stdout
203
+ agenzo-admin-cli developers list --format json > devs.json
204
+ ```
205
+
206
+ ## Idempotency
207
+
208
+ Every command that issues a **server-side write** accepts `--idempotency-key <key>`. The CLI forwards the value **verbatim** as the HTTP `Idempotency-Key` header and **never auto-generates** one — the caller supplies it. When the flag is absent, no header is sent.
209
+
210
+ The flag is accepted only by the seven server-write commands:
211
+
212
+ | Command | Server write |
213
+ |---------|--------------|
214
+ | `auth login` | `POST /auth/login` (+ `/auth/register` when new) |
215
+ | `orgs update` | `POST /organizations/me/update` |
216
+ | `developers create` | `POST /developers/create` |
217
+ | `developers update` | `POST /developers/{id}/update` |
218
+ | `keys create` | `POST /keys/create` |
219
+ | `keys rotate` | `POST /keys/{id}/rotate` |
220
+ | `keys disable` | `POST /keys/{id}/disable` |
221
+
222
+ Local-only writes (`config set-host`, `config reset-host`, `orgs switch`, `auth logout`) and read commands do **not** accept the flag.
223
+
224
+ > **Backend enforcement pending (BACK-090).** The CLI side is complete today — the flag is accepted and the header is forwarded. Server-side de-duplication for these admin endpoints is not yet implemented, so a retried write is currently honored by the backend but not de-duplicated. When the backend adds the idempotency middleware, no CLI change is needed. Until then, confirm a rotated key is fully retired before rotating again.
225
+
226
+ ## Exit Codes
227
+
228
+ Branch on the exit code for stable automation:
229
+
230
+ | Code | Meaning |
231
+ |------|---------|
232
+ | `0` | Success |
233
+ | `1` | Business / parameter error (4xx — validation, not-found, conflict, rate-limited) |
234
+ | `2` | Upgrade required (CLI below the server-advertised `X-CLI-Min-Version`) |
235
+ | `3` | Authentication failure or invalid key (not signed in, session expired, 401/403) |
236
+ | `4` | Network error or backend 5xx |
237
+ | `5` | User cancel (Ctrl+C or declining a prompt) |
238
+
239
+ On any failure the CLI writes a SCREAMING_SNAKE error envelope to **stderr** (never stdout) and exits with the mapped code. In `json` mode:
240
+
241
+ ```json
242
+ { "error": { "code": "AUTH_NOT_SIGNED_IN", "message": "Not signed in. Run `agenzo-admin-cli auth login`.", "http": 401 } }
243
+ ```
244
+
245
+ In `table` mode the same failure renders as `✗ <message>` (plus a suggestion line for auth errors). The `http` field is included only when the failure originated from an HTTP call. Error codes use the domain prefixes `AUTH_`, `ORG_`, `KEY_`, `RESOURCE_`, `PARAM_`, `RATE_`, `UPSTREAM_`, plus `UPGRADE_REQUIRED`, `USER_CANCELLED`, and `INTERNAL_ERROR`; any unrecognized error maps to `INTERNAL_ERROR`.
246
+
247
+ ## Authentication
248
+
249
+ All commands operate on the control plane and authenticate with a Bearer token obtained via `auth login`:
250
+
251
+ | Surface | Commands | Auth Method |
252
+ |---------|----------|-------------|
253
+ | Control Plane | `orgs`, `developers`, `keys`, `accounts` | Bearer Token (via `auth login`) |
254
+ | Local-only | `config`, `orgs list`, `orgs switch`, `auth logout` | No API call — local state only |
255
+
256
+ `AuthService` injects `Authorization: Bearer <token>` and transparently refreshes the token within 300 seconds of expiry, re-running login automatically when the session has fully expired. Config, credentials, and the key cache are stored under `~/.agenzo-admin-cli/`.
257
+
258
+ ## Project Structure
259
+
260
+ ```
261
+ ├── SKILL.md # AI Agent skill definition
262
+ ├── src/
263
+ │ ├── auth/ # auth login / logout + AuthService
264
+ │ ├── orgs/ # Organization management (get / update / list / switch)
265
+ │ ├── developers/ # Developer management (+ billing-mode helper)
266
+ │ ├── keys/ # API Key management (+ scope helper)
267
+ │ ├── accounts/ # Settlement account query (accounts get)
268
+ │ ├── config/ # Local config, credentials, key cache (~/.agenzo-admin-cli)
269
+ │ ├── api/ # HTTP client + X-CLI-Min-Version negotiation
270
+ │ ├── utils/ # output renderer, exit-code mapper, errors, formatting, prompts
271
+ │ └── types/ # TypeScript type definitions
272
+ ```
273
+
274
+ ## Development
275
+
276
+ ```bash
277
+ npm install # Install dependencies
278
+ npm run dev # Dev build (watch)
279
+ npm test # Run tests
280
+ npm run build # Production build
281
+ ```
282
+
283
+ ## License
284
+
285
+ MIT
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/utils/formatter.ts
4
+ var STATUS_ICONS = {
5
+ success: "\u2713",
6
+ error: "\u2717",
7
+ info: "\u2139",
8
+ warning: "\u26A0",
9
+ loading: "\u280B"
10
+ };
11
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
12
+ function createSpinner(message, intervalMs = 60) {
13
+ let frameIdx = 0;
14
+ let currentMessage = message;
15
+ const render = () => {
16
+ process.stdout.write(`\r\x1B[K${SPINNER_FRAMES[frameIdx]} ${currentMessage}`);
17
+ frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;
18
+ };
19
+ render();
20
+ const timer = setInterval(render, intervalMs);
21
+ return {
22
+ update(msg) {
23
+ currentMessage = msg;
24
+ },
25
+ stop(type, finalMessage) {
26
+ clearInterval(timer);
27
+ process.stdout.write("\r\x1B[K");
28
+ if (type && finalMessage) {
29
+ console.log(Formatter.status(type, finalMessage));
30
+ }
31
+ }
32
+ };
33
+ }
34
+ var Formatter = class _Formatter {
35
+ /** Get display width of a string (CJK characters count as 2) */
36
+ static displayWidth(str) {
37
+ let width = 0;
38
+ for (const char of str) {
39
+ const code = char.codePointAt(0);
40
+ if (code >= 19968 && code <= 40959 || // CJK Unified
41
+ code >= 12288 && code <= 12351 || // CJK Punctuation
42
+ code >= 13312 && code <= 19903 || // CJK Extension A
43
+ code >= 65280 && code <= 65519 || // Fullwidth Forms
44
+ code >= 63744 && code <= 64255 || // CJK Compatibility
45
+ code >= 131072 && code <= 173791) {
46
+ width += 2;
47
+ } else {
48
+ width += 1;
49
+ }
50
+ }
51
+ return width;
52
+ }
53
+ /** Pad string to target display width (right-pad with spaces) */
54
+ static padEndDisplay(str, targetWidth) {
55
+ const currentWidth = _Formatter.displayWidth(str);
56
+ const padding = Math.max(0, targetWidth - currentWidth);
57
+ return str + " ".repeat(padding);
58
+ }
59
+ /** Pad string to target display width (left-pad with spaces) */
60
+ static padStartDisplay(str, targetWidth) {
61
+ const currentWidth = _Formatter.displayWidth(str);
62
+ const padding = Math.max(0, targetWidth - currentWidth);
63
+ return " ".repeat(padding) + str;
64
+ }
65
+ /** Table output for list commands — columns aligned by max width */
66
+ static table(headers, rows) {
67
+ const colWidths = headers.map((h, i) => {
68
+ const cellWidths = rows.map((r) => _Formatter.displayWidth(r[i] ?? ""));
69
+ return Math.max(_Formatter.displayWidth(h), ...cellWidths);
70
+ });
71
+ const sep = " ";
72
+ const headerLine = headers.map((h, i) => _Formatter.padEndDisplay(h, colWidths[i])).join(sep);
73
+ const divider = colWidths.map((w) => "-".repeat(w)).join(sep);
74
+ const dataLines = rows.map(
75
+ (row) => row.map((cell, i) => _Formatter.padEndDisplay(cell ?? "", colWidths[i])).join(sep)
76
+ );
77
+ return [headerLine, divider, ...dataLines].join("\n");
78
+ }
79
+ /** Key-value output for detail commands — keys left-aligned */
80
+ static keyValue(entries) {
81
+ const maxKeyWidth = Math.max(...entries.map(([k]) => _Formatter.displayWidth(k)));
82
+ return entries.map(([key, value]) => `${_Formatter.padEndDisplay(key, maxKeyWidth)} ${value}`).join("\n");
83
+ }
84
+ /** Status-prefixed message */
85
+ static status(type, message) {
86
+ return `${STATUS_ICONS[type]} ${message}`;
87
+ }
88
+ /** Mask sensitive info, keeping only the prefix */
89
+ static maskKey(key, prefixLength = 8) {
90
+ if (key.length <= prefixLength) {
91
+ return key;
92
+ }
93
+ return key.slice(0, prefixLength) + "*".repeat(key.length - prefixLength);
94
+ }
95
+ /**
96
+ * Format ISO 8601 timestamp to local timezone display.
97
+ * Automatically uses the user's system timezone.
98
+ * e.g. "2026-05-07T03:24:53+00:00" → "2026-05-07 11:24:53" (in CST)
99
+ * "2026-05-07T03:24:53+00:00" → "2026-05-06 20:24:53" (in PDT)
100
+ * Returns the original string if parsing fails or input is empty.
101
+ */
102
+ static formatTime(iso) {
103
+ if (!iso) return "";
104
+ const date = new Date(iso);
105
+ if (isNaN(date.getTime())) return iso;
106
+ const pad = (n) => String(n).padStart(2, "0");
107
+ const y = date.getFullYear();
108
+ const m = pad(date.getMonth() + 1);
109
+ const d = pad(date.getDate());
110
+ const h = pad(date.getHours());
111
+ const min = pad(date.getMinutes());
112
+ const s = pad(date.getSeconds());
113
+ const offsetMin = -date.getTimezoneOffset();
114
+ const sign = offsetMin >= 0 ? "+" : "-";
115
+ const absOffset = Math.abs(offsetMin);
116
+ const tzH = pad(Math.floor(absOffset / 60));
117
+ const tzM = pad(absOffset % 60);
118
+ return `${y}-${m}-${d} ${h}:${min}:${s} (UTC${sign}${tzH}:${tzM})`;
119
+ }
120
+ };
121
+
122
+ export {
123
+ createSpinner,
124
+ Formatter
125
+ };
126
+ //# sourceMappingURL=chunk-AZALPHQR.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/formatter.ts"],"sourcesContent":["export type StatusPrefix = 'success' | 'error' | 'info' | 'warning' | 'loading';\n\nconst STATUS_ICONS: Record<StatusPrefix, string> = {\n success: '✓',\n error: '✗',\n info: 'ℹ',\n warning: '⚠',\n loading: '⠋',\n};\n\nconst SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];\n\nexport interface Spinner {\n /** Update the spinner message while it's running */\n update(message: string): void;\n /** Stop the spinner and show a final status line */\n stop(type?: StatusPrefix, finalMessage?: string): void;\n}\n\n/**\n * Creates an animated terminal spinner that cycles through braille frames.\n * Call `spinner.stop()` when the async work is done.\n */\nexport function createSpinner(message: string, intervalMs = 60): Spinner {\n let frameIdx = 0;\n let currentMessage = message;\n\n const render = () => {\n process.stdout.write(`\\r\\x1b[K${SPINNER_FRAMES[frameIdx]} ${currentMessage}`);\n frameIdx = (frameIdx + 1) % SPINNER_FRAMES.length;\n };\n\n render();\n const timer = setInterval(render, intervalMs);\n\n return {\n update(msg: string) {\n currentMessage = msg;\n },\n stop(type?: StatusPrefix, finalMessage?: string) {\n clearInterval(timer);\n process.stdout.write('\\r\\x1b[K');\n if (type && finalMessage) {\n console.log(Formatter.status(type, finalMessage));\n }\n },\n };\n}\n\nexport class Formatter {\n /** Get display width of a string (CJK characters count as 2) */\n private static displayWidth(str: string): number {\n let width = 0;\n for (const char of str) {\n const code = char.codePointAt(0)!;\n // CJK Unified Ideographs and common fullwidth ranges\n if (\n (code >= 0x4e00 && code <= 0x9fff) || // CJK Unified\n (code >= 0x3000 && code <= 0x303f) || // CJK Punctuation\n (code >= 0x3400 && code <= 0x4dbf) || // CJK Extension A\n (code >= 0xff00 && code <= 0xffef) || // Fullwidth Forms\n (code >= 0xf900 && code <= 0xfaff) || // CJK Compatibility\n (code >= 0x20000 && code <= 0x2a6df) // CJK Extension B\n ) {\n width += 2;\n } else {\n width += 1;\n }\n }\n return width;\n }\n\n /** Pad string to target display width (right-pad with spaces) */\n private static padEndDisplay(str: string, targetWidth: number): string {\n const currentWidth = Formatter.displayWidth(str);\n const padding = Math.max(0, targetWidth - currentWidth);\n return str + ' '.repeat(padding);\n }\n\n /** Pad string to target display width (left-pad with spaces) */\n private static padStartDisplay(str: string, targetWidth: number): string {\n const currentWidth = Formatter.displayWidth(str);\n const padding = Math.max(0, targetWidth - currentWidth);\n return ' '.repeat(padding) + str;\n }\n\n /** Table output for list commands — columns aligned by max width */\n static table(headers: string[], rows: string[][]): string {\n const colWidths = headers.map((h, i) => {\n const cellWidths = rows.map((r) => Formatter.displayWidth(r[i] ?? ''));\n return Math.max(Formatter.displayWidth(h), ...cellWidths);\n });\n\n const sep = ' ';\n\n const headerLine = headers.map((h, i) => Formatter.padEndDisplay(h, colWidths[i])).join(sep);\n const divider = colWidths.map((w) => '-'.repeat(w)).join(sep);\n const dataLines = rows.map((row) =>\n row.map((cell, i) => Formatter.padEndDisplay(cell ?? '', colWidths[i])).join(sep),\n );\n\n return [headerLine, divider, ...dataLines].join('\\n');\n }\n\n /** Key-value output for detail commands — keys left-aligned */\n static keyValue(entries: [string, string][]): string {\n const maxKeyWidth = Math.max(...entries.map(([k]) => Formatter.displayWidth(k)));\n return entries\n .map(([key, value]) => `${Formatter.padEndDisplay(key, maxKeyWidth)} ${value}`)\n .join('\\n');\n }\n\n /** Status-prefixed message */\n static status(type: StatusPrefix, message: string): string {\n return `${STATUS_ICONS[type]} ${message}`;\n }\n\n /** Mask sensitive info, keeping only the prefix */\n static maskKey(key: string, prefixLength = 8): string {\n if (key.length <= prefixLength) {\n return key;\n }\n return key.slice(0, prefixLength) + '*'.repeat(key.length - prefixLength);\n }\n\n /**\n * Format ISO 8601 timestamp to local timezone display.\n * Automatically uses the user's system timezone.\n * e.g. \"2026-05-07T03:24:53+00:00\" → \"2026-05-07 11:24:53\" (in CST)\n * \"2026-05-07T03:24:53+00:00\" → \"2026-05-06 20:24:53\" (in PDT)\n * Returns the original string if parsing fails or input is empty.\n */\n static formatTime(iso: string | null | undefined): string {\n if (!iso) return '';\n const date = new Date(iso);\n if (isNaN(date.getTime())) return iso;\n const pad = (n: number) => String(n).padStart(2, '0');\n const y = date.getFullYear();\n const m = pad(date.getMonth() + 1);\n const d = pad(date.getDate());\n const h = pad(date.getHours());\n const min = pad(date.getMinutes());\n const s = pad(date.getSeconds());\n const offsetMin = -date.getTimezoneOffset();\n const sign = offsetMin >= 0 ? '+' : '-';\n const absOffset = Math.abs(offsetMin);\n const tzH = pad(Math.floor(absOffset / 60));\n const tzM = pad(absOffset % 60);\n return `${y}-${m}-${d} ${h}:${min}:${s} (UTC${sign}${tzH}:${tzM})`;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Formatter
4
+ } from "./chunk-AZALPHQR.js";
5
+
6
+ // src/utils/output.ts
7
+ var DEFAULT_FORMAT = "table";
8
+ function isOutputFormat(value) {
9
+ return value === "json" || value === "table";
10
+ }
11
+ function resolveFormat(flag, env = process.env.AGENZO_FORMAT) {
12
+ if (flag !== void 0) {
13
+ return isOutputFormat(flag) ? flag : DEFAULT_FORMAT;
14
+ }
15
+ if (env !== void 0 && isOutputFormat(env)) {
16
+ return env;
17
+ }
18
+ return DEFAULT_FORMAT;
19
+ }
20
+ function render(result, opts) {
21
+ if (opts.format === "json") {
22
+ process.stdout.write(`${JSON.stringify(result.data, null, 2)}
23
+ `);
24
+ return;
25
+ }
26
+ process.stdout.write(`${result.text()}
27
+ `);
28
+ }
29
+ function notify(format, type, message) {
30
+ if (format === "json") {
31
+ return;
32
+ }
33
+ console.error(Formatter.status(type, message));
34
+ }
35
+
36
+ export {
37
+ resolveFormat,
38
+ render,
39
+ notify
40
+ };
41
+ //# sourceMappingURL=chunk-UIGWXIDT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/output.ts"],"sourcesContent":["import type { CommandResult } from '../types/commands.js';\nimport { Formatter, type StatusPrefix } from './formatter.js';\n\n/**\n * Central output renderer.\n *\n * Single choke point that turns a {@link CommandResult} into stdout bytes,\n * switching between machine-readable JSON and a human-readable table view.\n * This is what lets command handlers stop calling `console.log` directly:\n * they build a `CommandResult` and hand it here.\n *\n * Contract (cli-standard §5.1/§5.2):\n * - stdout carries ONLY the business payload — a single JSON value\n * (`--format json`) or human-readable text (`--format table`).\n * - logs, spinners, prompts, hints and progress lines belong on stderr and\n * are NEVER written here.\n *\n * Recorded deviation from cli-standard §5.1: the default format is `table`\n * (not `json`), and the flag values are `json | table` (not `json | text`).\n */\n\n/** The two supported output formats. */\nexport type OutputFormat = 'json' | 'table';\n\n/** Options consumed by {@link render}. */\nexport interface RenderOptions {\n format: OutputFormat;\n}\n\n/** The default format used when nothing valid is supplied (deliberate deviation: `table`). */\nconst DEFAULT_FORMAT: OutputFormat = 'table';\n\n/** Narrow an arbitrary string to a known {@link OutputFormat}. */\nfunction isOutputFormat(value: string): value is OutputFormat {\n return value === 'json' || value === 'table';\n}\n\n/**\n * Resolve the active output format.\n *\n * Precedence: `--format` flag > `AGENZO_FORMAT` env var > default `table`.\n * Any invalid value falls back to the default. The result is always one of\n * `'json' | 'table'`.\n *\n * The `--format` flag is authoritative when provided: a provided-but-invalid\n * flag falls back to the default rather than deferring to the environment.\n */\nexport function resolveFormat(\n flag?: string,\n env: string | undefined = process.env.AGENZO_FORMAT,\n): OutputFormat {\n // 1. --format flag is authoritative when provided.\n if (flag !== undefined) {\n return isOutputFormat(flag) ? flag : DEFAULT_FORMAT;\n }\n // 2. AGENZO_FORMAT environment value, when set to a valid format.\n if (env !== undefined && isOutputFormat(env)) {\n return env;\n }\n // 3. Default.\n return DEFAULT_FORMAT;\n}\n\n/**\n * Emit a successful command result to stdout in the chosen format.\n *\n * - `json`: writes `JSON.stringify(result.data, null, 2)` (the machine payload\n * only — never the text-mode chrome).\n * - `table`: writes the lazy human presenter `result.text()`.\n *\n * Only the payload reaches stdout; status/progress lines must go to stderr by\n * the caller. A trailing newline is appended so the output pipes cleanly into\n * tools like `jq`.\n */\nexport function render<T>(result: CommandResult<T>, opts: RenderOptions): void {\n if (opts.format === 'json') {\n process.stdout.write(`${JSON.stringify(result.data, null, 2)}\\n`);\n return;\n }\n process.stdout.write(`${result.text()}\\n`);\n}\n\n/**\n * Emit a human-facing status line (✓ / ℹ / ⚠) to stderr — but ONLY in `table`\n * mode. In `json` mode the output is consumed by other agents/scripts, so any\n * decorative status text (even on stderr) is noise that can confuse parsing;\n * `json` mode therefore stays completely silent here. Errors are NOT routed\n * through this helper — they are owned by the top-level handler in index.ts.\n *\n * This is the single choke point for command success/progress notices, so the\n * `json`-silence rule is enforced in one place rather than scattered across\n * every handler.\n */\nexport function notify(\n format: OutputFormat,\n type: StatusPrefix,\n message: string,\n): void {\n if (format === 'json') {\n return;\n }\n console.error(Formatter.status(type, message));\n}\n"],"mappings":";;;;;;AA8BA,IAAM,iBAA+B;AAGrC,SAAS,eAAe,OAAsC;AAC5D,SAAO,UAAU,UAAU,UAAU;AACvC;AAYO,SAAS,cACd,MACA,MAA0B,QAAQ,IAAI,eACxB;AAEd,MAAI,SAAS,QAAW;AACtB,WAAO,eAAe,IAAI,IAAI,OAAO;AAAA,EACvC;AAEA,MAAI,QAAQ,UAAa,eAAe,GAAG,GAAG;AAC5C,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAaO,SAAS,OAAU,QAA0B,MAA2B;AAC7E,MAAI,KAAK,WAAW,QAAQ;AAC1B,YAAQ,OAAO,MAAM,GAAG,KAAK,UAAU,OAAO,MAAM,MAAM,CAAC,CAAC;AAAA,CAAI;AAChE;AAAA,EACF;AACA,UAAQ,OAAO,MAAM,GAAG,OAAO,KAAK,CAAC;AAAA,CAAI;AAC3C;AAaO,SAAS,OACd,QACA,MACA,SACM;AACN,MAAI,WAAW,QAAQ;AACrB;AAAA,EACF;AACA,UAAQ,MAAM,UAAU,OAAO,MAAM,OAAO,CAAC;AAC/C;","names":[]}
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ Formatter,
4
+ createSpinner
5
+ } from "./chunk-AZALPHQR.js";
6
+ export {
7
+ Formatter,
8
+ createSpinner
9
+ };
10
+ //# sourceMappingURL=formatter-3JVOJXN6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,2 @@
1
+
2
+ export { }