@4ge/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.
Files changed (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +228 -0
  3. package/bin/dev.js +3 -0
  4. package/bin/run.js +3 -0
  5. package/dist/base.js +398 -0
  6. package/dist/commands/auth/login.js +197 -0
  7. package/dist/commands/auth/logout.js +31 -0
  8. package/dist/commands/auth/profile/delete.js +34 -0
  9. package/dist/commands/auth/profile/list.js +32 -0
  10. package/dist/commands/auth/profile/use.js +30 -0
  11. package/dist/commands/auth/status.js +107 -0
  12. package/dist/commands/config/get.js +37 -0
  13. package/dist/commands/config/index.js +9 -0
  14. package/dist/commands/config/manage.js +165 -0
  15. package/dist/commands/config/set.js +42 -0
  16. package/dist/commands/config/show.js +46 -0
  17. package/dist/commands/epic/index.js +10 -0
  18. package/dist/commands/epic/list.js +91 -0
  19. package/dist/commands/epic/pull.js +137 -0
  20. package/dist/commands/epic/show.js +67 -0
  21. package/dist/commands/epic/update.js +104 -0
  22. package/dist/commands/feature/index.js +2 -0
  23. package/dist/commands/feature/list.js +93 -0
  24. package/dist/commands/feature/pull.js +122 -0
  25. package/dist/commands/feature/show.js +60 -0
  26. package/dist/commands/feature/update.js +103 -0
  27. package/dist/commands/idea/create.js +88 -0
  28. package/dist/commands/idea/graduate.js +89 -0
  29. package/dist/commands/idea/index.js +5 -0
  30. package/dist/commands/idea/list.js +83 -0
  31. package/dist/commands/idea/show.js +86 -0
  32. package/dist/commands/idea/update.js +108 -0
  33. package/dist/commands/init.js +354 -0
  34. package/dist/commands/login.js +5 -0
  35. package/dist/commands/plan/index.js +10 -0
  36. package/dist/commands/plan/pull.js +104 -0
  37. package/dist/commands/plan/versions.js +64 -0
  38. package/dist/commands/project/create.js +84 -0
  39. package/dist/commands/project/index.js +9 -0
  40. package/dist/commands/project/info.js +74 -0
  41. package/dist/commands/project/list.js +78 -0
  42. package/dist/commands/project/pull.js +127 -0
  43. package/dist/commands/project/update.js +104 -0
  44. package/dist/commands/root.js +13 -0
  45. package/dist/commands/skill/index.js +8 -0
  46. package/dist/commands/skill/install.js +71 -0
  47. package/dist/commands/story/index.js +10 -0
  48. package/dist/commands/story/list.js +81 -0
  49. package/dist/commands/story/pull.js +125 -0
  50. package/dist/commands/story/show.js +57 -0
  51. package/dist/commands/story/update.js +103 -0
  52. package/dist/commands/template/create.js +72 -0
  53. package/dist/commands/template/delete.js +62 -0
  54. package/dist/commands/template/feature/create.js +70 -0
  55. package/dist/commands/template/feature/delete.js +75 -0
  56. package/dist/commands/template/feature/list.js +69 -0
  57. package/dist/commands/template/feature/update.js +82 -0
  58. package/dist/commands/template/info.js +52 -0
  59. package/dist/commands/template/list.js +76 -0
  60. package/dist/commands/template/pull.js +99 -0
  61. package/dist/commands/template/update.js +71 -0
  62. package/dist/commands/whoami.js +5 -0
  63. package/dist/core/api/action-hints.js +17 -0
  64. package/dist/core/api/index.js +3 -0
  65. package/dist/core/api/retry.js +95 -0
  66. package/dist/core/api/validate-response.js +41 -0
  67. package/dist/core/auth/api-client.js +156 -0
  68. package/dist/core/auth/config.js +3 -0
  69. package/dist/core/auth/credentials.js +143 -0
  70. package/dist/core/auth/email-auth.js +30 -0
  71. package/dist/core/auth/errors.js +20 -0
  72. package/dist/core/auth/index.js +6 -0
  73. package/dist/core/auth/realtime.js +82 -0
  74. package/dist/core/auth/resolution.js +63 -0
  75. package/dist/core/auth/state.js +9 -0
  76. package/dist/core/auth/token-refresh.js +100 -0
  77. package/dist/core/config/defaults.js +31 -0
  78. package/dist/core/config/index.js +3 -0
  79. package/dist/core/config/parser.js +213 -0
  80. package/dist/core/config/schema.js +29 -0
  81. package/dist/core/project/markdown.js +37 -0
  82. package/dist/core/skill/installer.js +59 -0
  83. package/dist/core/skill/paths.js +52 -0
  84. package/dist/generated/api/client/client.gen.js +217 -0
  85. package/dist/generated/api/client/index.js +6 -0
  86. package/dist/generated/api/client/types.gen.js +2 -0
  87. package/dist/generated/api/client/utils.gen.js +231 -0
  88. package/dist/generated/api/client.gen.js +3 -0
  89. package/dist/generated/api/core/auth.gen.js +14 -0
  90. package/dist/generated/api/core/bodySerializer.gen.js +57 -0
  91. package/dist/generated/api/core/params.gen.js +103 -0
  92. package/dist/generated/api/core/pathSerializer.gen.js +114 -0
  93. package/dist/generated/api/core/queryKeySerializer.gen.js +99 -0
  94. package/dist/generated/api/core/serverSentEvents.gen.js +136 -0
  95. package/dist/generated/api/core/types.gen.js +2 -0
  96. package/dist/generated/api/core/utils.gen.js +87 -0
  97. package/dist/generated/api/index.js +2 -0
  98. package/dist/generated/api/sdk.gen.js +556 -0
  99. package/dist/generated/api/types.gen.js +2 -0
  100. package/dist/generated/api/zod.gen.js +1262 -0
  101. package/dist/ui/components/action-hints.js +33 -0
  102. package/dist/ui/components/cli-header.js +91 -0
  103. package/dist/ui/components/confirm-prompt.js +28 -0
  104. package/dist/ui/components/data-table.js +35 -0
  105. package/dist/ui/components/detail-view.js +5 -0
  106. package/dist/ui/components/error-display.js +5 -0
  107. package/dist/ui/components/prompt-layout.js +6 -0
  108. package/dist/ui/components/select-prompt.js +28 -0
  109. package/dist/ui/components/spinner.js +14 -0
  110. package/dist/ui/components/success-message.js +5 -0
  111. package/dist/ui/components/text-input.js +29 -0
  112. package/dist/ui/hooks/use-action-hints.js +21 -0
  113. package/dist/ui/hooks/use-status.js +22 -0
  114. package/dist/ui/index.js +15 -0
  115. package/dist/ui/prompt-helpers.js +55 -0
  116. package/dist/ui/render.js +9 -0
  117. package/package.json +89 -0
  118. package/skills/4ge-cli.md +113 -0
  119. package/usage.md +320 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Conflate AI
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,228 @@
1
+ # 4ge-CLI: A Universal Interface for 4ge Resources
2
+
3
+ The official Command Line Interface and API Bridge for the 4ge Service Architecture.
4
+
5
+ This CLI is designed with a **Dual-Output Architecture**: it provides a rich, interactive Terminal User Interface (TUI) for human developers and a strict, deterministic JSON output stream for AI Agents.
6
+
7
+ ## Project Overview
8
+
9
+ **Problem Statement:**
10
+ Developing and automating workflows within the 4ge ecosystem presents a dual challenge:
11
+ 1. **For Human Developers:** Interacting with complex APIs often involves fragmented tools, manual data parsing, and a steep learning curve to translate service mesh logic into actionable insights. This leads to inefficient debugging, inconsistent context, and slower development cycles when trying to understand project states or access artifacts locally.
12
+ 2. **For AI Agents:** Automating tasks against APIs demands highly reliable, deterministic, and self-validating interfaces. Traditional command-line tools, primarily designed for human readability, frequently produce inconsistent outputs, lack robust error handling, and can easily break fragile automation scripts. This hinders the development of dependable, AI-driven workflows.
13
+
14
+ Both human developers and AI agents currently lack a unified, secure, and context-aware local interface that caters specifically to their distinct interaction needs, forcing compromises between usability and automation reliability.
15
+
16
+ **Solution: The 4ge-CLI**
17
+ The 4ge-CLI is designed as the definitive local interface for the 4ge ecosystem, bridging the gap between powerful 4ge APIs and diverse user environments. It provides a robust, intuitive, and secure command-line experience specifically engineered for both high-fidelity human interaction and seamless, error-proof AI Agent orchestration.
18
+
19
+ **Core Vision:**
20
+ To be the primary external gateway to 4ge resources, the 4ge-CLI translates the intricate 4ge service mesh into an intuitive, nested command structure. Its innovative "Dual-Output" strategy ensures optimal experiences: human developers benefit from a rich, interactive Terminal User Interface (TUI), while AI agents receive a consistently structured, schema-validated JSON stream, guaranteeing reliability for automated processes.
21
+
22
+ **Key Personas:**
23
+
24
+ * **The Developer (Human):**
25
+ * **Description:** An engineer, product manager, or data scientist who interacts directly with 4ge resources.
26
+ * **Goals:** Quickly understand the state of their 4ge workspace, projects, and artifacts; efficiently troubleshoot issues; explore 4ge resources with minimal friction; and securely manage local configuration and credentials.
27
+ * **Motivations:** Desires a visually appealing and interactive TUI that offers immediate feedback, guided exploration, and clear status updates, reducing cognitive load and accelerating workflow. They want to avoid manual API calls or cumbersome web interfaces for routine information retrieval.
28
+ * **CLI Value:** Provides a rich, color-coded TUI with interactive prompts, progress indicators, and easily navigable tables, making complex data digestible and interaction intuitive.
29
+
30
+ * **The AI Agent (Machine):**
31
+ * **Description:** An automated script, bot, or intelligent agent designed to interact with 4ge APIs programmatically. This includes CI/CD pipelines, custom automation scripts, or advanced AI-driven workflow engines.
32
+ * **Goals:** Execute commands deterministically, receive predictable and machine-readable outputs, ensure zero-error execution loops, and securely authenticate without human intervention.
33
+ * **Motivations:** Requires absolute reliability and consistency to build resilient automation. Any deviation in output format or unexpected errors can halt critical operations. They need strict validation and clear error reporting to maintain continuous operation.
34
+ * **CLI Value:** Offers a `--json` output flag that returns strictly schema-validated JSON, leveraging Zod for contract enforcement. Supports headless authentication via API keys, robust error tracing, and "Action Hints" to guide subsequent automated commands, enabling highly reliable and autonomous operations.
35
+
36
+ **Core Capabilities & Functional Highlights:**
37
+
38
+ The 4ge-CLI is built upon a foundation designed for reliability, security, and developer efficiency:
39
+
40
+ 1. **Unified Gateway Communication:** Acts as a thin client, delegating business logic to the 4ge Core API. It intelligently signals `X-Client-Type: 4ge-agent` for machine interactions and processes "Action Hints" for guided automation.
41
+ 2. **Secure Local State Management:** Manages non-sensitive configuration in `.4ge/config.yaml` (e.g., `project_id`, `environment`) and securely stores sensitive credentials (JWTs, Refresh Tokens, API Keys) in the user's home directory (`~/.4ge/credentials.json`) to prevent accidental exposure.
42
+ 3. **Flexible Authentication & Identity:** Supports PKCE-based OAuth for interactive human login and `4GE_API_KEY` (Personal Access Tokens) for seamless headless authentication in automated environments. Commands inherently respect user permissions via Supabase RLS policies.
43
+ 4. **Context-Aware Operations:** The `4ge init` command establishes a local `.4ge/config.yaml`, anchoring the CLI to a specific project. Subsequent commands automatically leverage this local context, eliminating redundant flag usage and providing folder-specific information.
44
+ 5. **Schema-Driven Reliability:** Employs a "Contract-First" approach, where internal Zod schemas mirror Core API definitions. This ensures robust input validation and response parsing, providing detailed error traces for agents and user-friendly "Update Required" messages for humans in case of schema mismatches.
45
+ 6. **Three-Tier Output Design (UX):**
46
+ * **Pretty Mode (Default):** Ink-rendered styled output — color-coded tables, detail views, spinners, and success/error feedback. No follow-up prompts. Command runs, shows result, exits.
47
+ * **Interactive Mode (`--interactive` / `-i`):** Everything in Pretty, plus action-hint navigation menus, follow-up command suggestions, and prompts for missing values. Configurable as default via `cli.default_mode: interactive` in `.4ge/config.yaml`.
48
+ * **Machine Mode (`--json`):** Strict `{ "data": T, "error": null, "metadata": {...} }` JSON envelope. No Ink, no prompts, no colors. Fully deterministic for AI agents.
49
+
50
+ **Mode Resolution:** `--interactive`/`--no-interactive` flag → `cli.default_mode` config → `pretty` (hard default)
51
+
52
+ > **Note for Contributors**: When implementing new commands, always extract the inner `.data` from SDK responses before passing to `outputJson()`. See the [Technical Specification](docs/technical-specification.md#-outputjson-double-nesting-anti-pattern) for details.
53
+ 7. **Foundational Integration Features (MVP):** Enables read-only inspection of active 4ge resources, fetching specific project details, and loading artifact statuses and the artifacts themselves. Future enhancements include resource management (scaffolding) and syncing capabilities.
54
+ 8. **Enhanced Safety & Validation:** Supports `--dry-run` for previewing changes, crucial for automated workflows. Comprehensive Zod schema validation for all command inputs and API responses prevents "state-drift" and ensures data integrity.
55
+
56
+ -----
57
+
58
+ ## 🏗 Architecture & Stack
59
+
60
+ - **Runtime:** Node.js 24.x (LTS)
61
+ - **Package Manager:** pnpm
62
+ - **Framework:** [oclif v4](https://oclif.io/) (Nested Command Structure)
63
+ - **UI (Human):** [Ink](https://github.com/vadimdemedes/ink) (React-based terminal rendering)
64
+ - **UI (Machine):** Standardized `--json` flag output
65
+ - **API Sync:** `@hey-api/openapi-ts` (Contract-driven Zod schema and TypeScript generation)
66
+ - **State Management:** Local `.4ge/config.yaml`
67
+
68
+
69
+ ## 🛠 TUI Mode Configuration
70
+
71
+ The 4ge-CLI provides three output modes, configurable per-command or as a project default:
72
+
73
+ | Mode | Trigger | Behavior |
74
+ |------|---------|----------|
75
+ | **Pretty** | Default (no flags) | Styled Ink output (tables, spinners, colors). No follow-up prompts. |
76
+ | **Interactive** | `--interactive` / `-i` | Pretty output + action-hint menus, follow-up suggestions, selection prompts. |
77
+ | **Machine** | `--json` / `-j` | Strict JSON envelope. No TUI, no prompts, no colors. Fully deterministic for AI agents. |
78
+
79
+ ### Setting the Default Mode
80
+
81
+ To make interactive mode the default for a project:
82
+
83
+ ```bash
84
+ 4ge config:set cli.default_mode interactive
85
+ ```
86
+
87
+ This writes to `.4ge/config.yaml`:
88
+ ```yaml
89
+ cli:
90
+ default_mode: interactive
91
+ ```
92
+
93
+ ### Resolution Order
94
+
95
+ When running a command, the mode is resolved as:
96
+ 1. `--interactive` flag → forced interactive
97
+ 2. `--no-interactive` flag → forced pretty (overrides config)
98
+ 3. `cli.default_mode` in `.4ge/config.yaml` → uses config value
99
+ 4. *(fallback)* → pretty mode
100
+
101
+ **Note:** `4ge init` is always interactive regardless of mode settings — it's an inherently interactive setup flow.
102
+
103
+ ### Examples
104
+
105
+ ```bash
106
+ # Pretty mode — styled table, no prompts
107
+ 4ge project:list
108
+
109
+ # Interactive mode — table + select an item + action hints
110
+ 4ge project:list --interactive
111
+
112
+ # Override config back to pretty
113
+ 4ge project:list --no-interactive
114
+
115
+ # Machine mode — strict JSON
116
+ 4ge project:list --json
117
+ ```
118
+
119
+ ## 🚀 Prerequisites
120
+
121
+ **End users:** Node.js v24+ (git + GitHub SSH key for private install).
122
+
123
+ **Contributors:** Node.js v24+, pnpm v9+.
124
+
125
+
126
+ ## 🛠 Getting Started
127
+
128
+ ### End users (install from npm)
129
+
130
+ ```bash
131
+ npm install -g @4ge/cli
132
+ 4ge --help
133
+ ```
134
+
135
+ Requires Node.js 24+ (no git, no SSH key needed). The package ships prebuilt
136
+ — no build step on your machine. Update later with `npm update -g @4ge/cli`.
137
+
138
+ ### Contributors (from source)
139
+
140
+ 1. **Clone and Install:**
141
+
142
+ ```bash
143
+ git clone git@github.com:Conflate-AI/4ge-cli.git
144
+ cd 4ge-cli
145
+ pnpm install
146
+ ```
147
+
148
+ 2. **Sync the Core API Contract:**
149
+ The 4ge-CLI uses a "Contract-First" approach. It does not define its own data models. Before running or building the CLI, you must sync the Zod schemas and API clients from the Core API's OpenAPI specification.
150
+ *Ensure the 4ge Core API is running locally or accessible via the URL in `openapi-ts.config.ts`, then run:*
151
+
152
+ ```bash
153
+ pnpm sync-api
154
+ ```
155
+
156
+ *This will populate the `src/generated/api` directory.*
157
+
158
+
159
+ 3. **Run Locally:**
160
+
161
+ ```bash
162
+ pnpm dev --help
163
+ ```
164
+
165
+ 4. **Authenticate:**
166
+ ```bash
167
+ 4ge auth:login --token <api_key> # installed
168
+ # or, in dev:
169
+ pnpm dev auth:login --token <api_key>
170
+ ```
171
+
172
+ 5. **Initialize a Project:**
173
+ ```bash
174
+ 4ge init # offers to install the agent skill (default: No)
175
+ ```
176
+
177
+ ## 📂 Repository Structure
178
+
179
+ ```
180
+ .
181
+ ├── bin/ # Executable entry points (run.js, dev.js)
182
+ ├── src/
183
+ │ ├── base.ts # BaseCommand with --json flag support
184
+ │ ├── commands/ # oclif command implementations
185
+ │ │ ├── index.ts # Root command (help)
186
+ │ │ ├── init.ts # 4ge init
187
+ │ │ ├── login.ts # 4ge login (alias)
188
+ │ │ ├── whoami.ts # 4ge whoami (alias)
189
+ │ │ └── auth/ # 4ge auth:login | auth:logout | auth:status | auth:profile:use
190
+ │ ├── core/ # Business logic (framework-agnostic)
191
+ │ │ ├── auth/ # Credential storage, resolution, API client
192
+ │ │ ├── config/ # .4ge/config.yaml parser, Zod schema
193
+ │ │ ├── skill/ # Agent skill path resolution + installer
194
+ │ │ └── api/ # Response extraction, validation, retry
195
+ │ ├── generated/ # ⚠️ DO NOT EDIT: Auto-generated from Core API
196
+ │ └── ui/ # Ink React components (TUI)
197
+ │ ├── components/ # Spinner, DataTable, DetailView, SelectPrompt, ConfirmPrompt, etc.
198
+ │ ├── hooks/ # useActionHints hook
199
+ │ ├── prompt-helpers.tsx # Ink-based interactive prompt renderers
200
+ │ └── render.ts # renderTui() helper
201
+ ├── skills/ # Canonical agent skill (4ge-cli.md) — ships in npm pkg
202
+ ├── openapi-ts.config.ts # Configuration for the API contract sync
203
+ └── package.json # Scripts and dependencies
204
+ ```
205
+
206
+ ## ✅ Current Implementation Status
207
+
208
+ | Phase | Status | Commands Available |
209
+ |-------|--------|--------------------|
210
+ | Phase 1: Foundation (Auth + Config) | ✅ Complete | `auth:login | auth:status | auth:logout | auth:profile:list | auth:profile:use | auth:profile:delete`, `init`, `login`, `whoami` |
211
+ | Phase 2: Read Operations | ✅ Complete | `project:list | project:info | project:pull | project:create | project:update`, `plan:pull | plan:versions`, `epic/story/feature:list | :show | :pull` |
212
+ | Phase 3: Write Operations | ✅ Complete | `epic/story/feature:update`, `idea:create | idea:graduate | idea:list | idea:show | idea:update`, `template:list | template:info | template:create | template:update | template:delete`, `template:feature:create | :list | :update | :delete`, `config:set | config:get | config:show | config:manage` |
213
+ | Phase 4: Enhanced UX (TUI) | ✅ Complete | Ink TUI with spinner, tables, detail views, selection prompts, action hints, `--interactive` flag, `cli.default_mode` config |
214
+ | Phase 5: Advanced UX | ✅ Partial | `config:manage` ✅, `--dry-run` ✅, `--retry` ✅, hierarchy config defaults ✅, agent skill install (`skill:install`) ✅, OAuth realtime flow ✅ |
215
+ | Phase 6: Flows | 🗓 Planned | `flow:list | flow:show | flow:pull | flow:update` (issue #012) |
216
+
217
+
218
+ ## 🤖 AI Agent Integration
219
+
220
+ This CLI is a primary tool for 4ge AI Agents. When integrating, Agents must adhere to the following rules:
221
+
222
+ 1. **JSON Mode:** Always append the `--json` flag. This disables all interactive prompts and returns a deterministic, Zod-validated payload.
223
+ 2. **Authentication:** Agents should bypass OAuth by providing a Personal Access Token via the `4GE_API_KEY` environment variable.
224
+ 3. **Telemetry:** Agents interacting with the CLI should expect the `X-Client-Type: 4ge-agent` header to be automatically applied to upstream Core API requests.
225
+ 4. **Skill install:** To teach an agent how to use the CLI, install the bundled skill into the project: `4ge skill:install` (default target `.agents/skills/4ge-cli/SKILL.md`, picked up by Cursor/Cline/Codex/Gemini CLI/GitHub Copilot/OpenCode). Use `--agent pi`/`--agent claude-code`/etc. for a specific harness, `--global` for user scope, `--force` to overwrite. `4ge init` also offers this on completion.
226
+
227
+ See `skills/4ge-cli.md` for the canonical skill content (command reference, response envelope, workflow patterns).
228
+
package/bin/dev.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env -S node --import tsx
2
+ import { execute } from '@oclif/core'
3
+ await execute({ dir: import.meta.url, development: true })
package/bin/run.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { execute } from '@oclif/core'
3
+ await execute({ dir: import.meta.url })
package/dist/base.js ADDED
@@ -0,0 +1,398 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { createApiClient, checkConnectivity, ConnectivityError, ensureFreshToken, getApiBaseUrl, isAuthError, getAuthErrorHint } from './core/auth/index.js';
3
+ import { withRetry as runWithRetry } from './core/api/retry.js';
4
+ export class BaseCommand extends Command {
5
+ // --json flag: when true, suppress interactive prompts, output deterministic JSON,
6
+ // and send X-Client-Type: 4ge-agent header
7
+ static baseFlags = {
8
+ json: Flags.boolean({
9
+ char: 'j',
10
+ description: 'Output deterministic JSON for machine consumption (suppresses interactive prompts)',
11
+ default: false,
12
+ }),
13
+ interactive: Flags.boolean({
14
+ description: 'Enable interactive mode with follow-up prompts and suggestions',
15
+ aliases: ['i'],
16
+ allowNo: true, // enables --no-interactive
17
+ }),
18
+ 'dry-run': Flags.boolean({
19
+ description: 'Preview changes without executing them',
20
+ default: false,
21
+ }),
22
+ retry: Flags.integer({
23
+ description: 'Retry transient API failures (network errors, 429, 5xx) with exponential backoff',
24
+ default: 0,
25
+ }),
26
+ };
27
+ flags;
28
+ localConfig = null;
29
+ static connectivityChecked = false;
30
+ static assetsMigrated = false;
31
+ get isJsonMode() {
32
+ return this.flags?.json ?? false;
33
+ }
34
+ get isInteractiveMode() {
35
+ if (this.isJsonMode)
36
+ return false;
37
+ const interactive = this.flags?.interactive;
38
+ if (interactive === true)
39
+ return true;
40
+ if (interactive === false)
41
+ return false;
42
+ return this.localConfig?.cli?.default_mode === 'interactive';
43
+ }
44
+ get isDryRun() {
45
+ return this.flags?.['dry-run'] === true;
46
+ }
47
+ get retryCount() {
48
+ const n = this.flags?.retry;
49
+ return Number.isFinite(n) && n > 0 ? n : 0;
50
+ }
51
+ /** Retry attempts recorded by the most recent {@link withRetry} call. */
52
+ lastRetryAttempts = [];
53
+ async init() {
54
+ await super.init();
55
+ const { flags } = await this.parse(this.constructor);
56
+ this.flags = flags;
57
+ // Load local config for isInteractiveMode resolution
58
+ try {
59
+ const { readConfig } = await import('./core/config/index.js');
60
+ this.localConfig = await readConfig();
61
+ }
62
+ catch {
63
+ this.localConfig = null;
64
+ }
65
+ }
66
+ async renderInk(element) {
67
+ if (this.isJsonMode)
68
+ return; // safety gate — should never be called in json mode
69
+ const { render } = await import('ink');
70
+ const instance = render(element);
71
+ await instance.waitUntilExit();
72
+ }
73
+ /**
74
+ * Renders the CLI header and exits.
75
+ * Useful for non-interactive commands that want to show branding/status.
76
+ */
77
+ async renderHeader(status = 'ready', headerVariant = 'full') {
78
+ if (this.isJsonMode)
79
+ return;
80
+ const { renderHeaderString } = await import('./ui/components/cli-header.js');
81
+ this.log(renderHeaderString(this.config.version, status, headerVariant));
82
+ }
83
+ async getProjectIdOrExit() {
84
+ if (!this.localConfig?.project_id) {
85
+ const message = 'Project not initialized. Run `4ge init` first to set project context.';
86
+ if (this.isJsonMode) {
87
+ this.outputErrorAndExit({ message, code: 'NOT_INITIALIZED' });
88
+ }
89
+ else {
90
+ this.error(message);
91
+ }
92
+ }
93
+ return this.localConfig.project_id;
94
+ }
95
+ /**
96
+ * One-time migration of legacy root-level assets into .4ge/assets/.
97
+ *
98
+ * Pull commands call this before writing so old layouts are upgraded transparently.
99
+ * Fail-safe: never throws — commands must still work with stale layouts.
100
+ */
101
+ async migrateAssetsIfNeeded() {
102
+ if (BaseCommand.assetsMigrated)
103
+ return;
104
+ try {
105
+ const { migrateLegacyAssets } = await import('./core/config/index.js');
106
+ await migrateLegacyAssets();
107
+ // Only mark complete after a successful (or no-op) migration.
108
+ // A failed attempt leaves the guard unset so the next command retries —
109
+ // migration is idempotent, so retries are safe.
110
+ BaseCommand.assetsMigrated = true;
111
+ }
112
+ catch {
113
+ // best-effort: never block command execution on migration errors.
114
+ }
115
+ }
116
+ async getClient(options) {
117
+ options?.onStatusChange?.('refreshing-token');
118
+ const credential = await ensureFreshToken();
119
+ if (!credential) {
120
+ const message = 'Not authenticated. Run `4ge auth login --token <api_key>` first.';
121
+ if (this.isJsonMode) {
122
+ this.outputErrorAndExit({ message, code: 'NO_CREDENTIALS' });
123
+ }
124
+ this.error(message);
125
+ }
126
+ const baseUrl = getApiBaseUrl();
127
+ const client = createApiClient({ credential, isJsonMode: this.isJsonMode, baseUrl });
128
+ if (!BaseCommand.connectivityChecked) {
129
+ const check = async () => {
130
+ try {
131
+ options?.onStatusChange?.('checking-connectivity');
132
+ await checkConnectivity(baseUrl);
133
+ BaseCommand.connectivityChecked = true;
134
+ options?.onStatusChange?.('ready');
135
+ }
136
+ catch (error) {
137
+ if (error.code === 'CONNECTION_ERROR' || error instanceof ConnectivityError) {
138
+ options?.onStatusChange?.('no-connection');
139
+ const message = error.message || 'Failed to connect to API';
140
+ const code = error.code || 'CONNECTION_ERROR';
141
+ if (this.isJsonMode) {
142
+ this.outputErrorAndExit({ message, code });
143
+ }
144
+ else {
145
+ this.error(message);
146
+ }
147
+ }
148
+ else {
149
+ throw error;
150
+ }
151
+ }
152
+ };
153
+ if (this.isJsonMode) {
154
+ await check();
155
+ }
156
+ else if (options?.onStatusChange) {
157
+ await check();
158
+ }
159
+ else {
160
+ await this.withSpinner('Connecting...', check);
161
+ }
162
+ }
163
+ return client;
164
+ }
165
+ outputJson(data, metadata) {
166
+ this.log(JSON.stringify({
167
+ data,
168
+ error: null,
169
+ metadata: {
170
+ // Spread caller metadata first (e.g. source, request_id, action_hints
171
+ // from the API), then enforce CLI-owned keys so a caller-provided
172
+ // `timestamp` never overrides the locally-generated one.
173
+ ...metadata,
174
+ // Surface retry attempts from the most recent withRetry call, if any.
175
+ ...(this.lastRetryAttempts.length > 0 ? { retries: this.lastRetryAttempts } : {}),
176
+ timestamp: new Date().toISOString(),
177
+ },
178
+ }, null, 2));
179
+ }
180
+ outputError(error, metadata) {
181
+ const errorObj = typeof error === 'string'
182
+ ? { message: error }
183
+ : error;
184
+ this.log(JSON.stringify({
185
+ data: null,
186
+ error: errorObj,
187
+ metadata: {
188
+ timestamp: new Date().toISOString(),
189
+ ...metadata,
190
+ },
191
+ }, null, 2));
192
+ }
193
+ outputErrorAndExit(error, metadata) {
194
+ this.outputError(error, metadata);
195
+ this.exit(1);
196
+ }
197
+ /**
198
+ * Extract the error code from an auth-related error object.
199
+ * Returns the auth code (TOKEN_EXPIRED, INVALID_API_KEY, UNAUTHORIZED) if present,
200
+ * or 'AUTH_ERROR' as a fallback for 401-status errors without a specific code.
201
+ */
202
+ getAuthErrorCode(error) {
203
+ if (error?.code === 'TOKEN_EXPIRED' || error?.code === 'INVALID_API_KEY' || error?.code === 'UNAUTHORIZED') {
204
+ return error.code;
205
+ }
206
+ if (error?.error?.code === 'TOKEN_EXPIRED' || error?.error?.code === 'INVALID_API_KEY' || error?.error?.code === 'UNAUTHORIZED') {
207
+ return error.error.code;
208
+ }
209
+ if (error?.status === 401 || error?.error?.status === 401) {
210
+ return 'AUTH_ERROR';
211
+ }
212
+ return 'AUTH_ERROR';
213
+ }
214
+ /**
215
+ * Extract the human-readable detail message from an auth-related error.
216
+ */
217
+ getAuthDetail(error) {
218
+ if (error?.message)
219
+ return error.message;
220
+ if (error?.error?.message)
221
+ return error.error.message;
222
+ if (error instanceof Error)
223
+ return error.message;
224
+ if (typeof error === 'string')
225
+ return error;
226
+ if (error?.status === 401)
227
+ return 'Authentication required';
228
+ return 'Unknown authentication error';
229
+ }
230
+ /**
231
+ * Determine the hint for an auth-related error, using getAuthErrorHint()
232
+ * for recognized auth codes, or a generic fallback for other 401s.
233
+ */
234
+ getAuthHint(error) {
235
+ const specificHint = getAuthErrorHint(error) || getAuthErrorHint(error?.error);
236
+ if (specificHint)
237
+ return specificHint;
238
+ if (error?.code === 'UNAUTHORIZED' || error?.error?.code === 'UNAUTHORIZED') {
239
+ return 'Please check your authentication credentials';
240
+ }
241
+ if (error?.status === 401 || error?.error?.status === 401) {
242
+ return 'Please check your authentication credentials';
243
+ }
244
+ return 'Please check your authentication credentials';
245
+ }
246
+ handleError(message, error) {
247
+ const errObj = error;
248
+ // Detect auth-related errors (TOKEN_EXPIRED, INVALID_API_KEY, UNAUTHORIZED, 401)
249
+ if (isAuthError(errObj) || isAuthError(errObj?.error) ||
250
+ errObj?.code === 'UNAUTHORIZED' || errObj?.error?.code === 'UNAUTHORIZED' ||
251
+ errObj?.status === 401 || errObj?.error?.status === 401) {
252
+ const authCode = this.getAuthErrorCode(errObj);
253
+ const authDetail = this.getAuthDetail(errObj);
254
+ const authHint = this.getAuthHint(errObj);
255
+ if (this.isJsonMode) {
256
+ this.outputErrorAndExit({
257
+ message: `${message}: ${authDetail}`,
258
+ code: authCode,
259
+ hint: authHint,
260
+ });
261
+ }
262
+ else {
263
+ this.error(`${message}: ${authDetail}
264
+ 💡 ${authHint}`);
265
+ }
266
+ return;
267
+ }
268
+ // Non-auth errors: existing logic
269
+ let detail = 'Unknown error';
270
+ if (error instanceof Error) {
271
+ detail = error.message;
272
+ }
273
+ else if (typeof error === 'string') {
274
+ detail = error;
275
+ }
276
+ else if (error && typeof error === 'object') {
277
+ // Handle SDK error response structure: { error: { message: ... } } or { message: ... }
278
+ const nestedError = errObj.error;
279
+ if (nestedError && typeof nestedError === 'object') {
280
+ detail = nestedError.message || JSON.stringify(nestedError);
281
+ }
282
+ else if (typeof nestedError === 'string') {
283
+ detail = nestedError;
284
+ }
285
+ else if (errObj.message) {
286
+ detail = errObj.message;
287
+ }
288
+ else {
289
+ detail = JSON.stringify(errObj);
290
+ }
291
+ }
292
+ if (this.isJsonMode) {
293
+ this.outputErrorAndExit({ message, code: 'API_ERROR', details: detail });
294
+ }
295
+ else {
296
+ this.error(`${message}: ${detail}`);
297
+ }
298
+ }
299
+ async selectOption(message, options, config) {
300
+ if (this.isJsonMode) {
301
+ this.outputError({ message: 'Interactive selection not available in JSON mode', code: 'INTERACTIVE_REQUIRED' });
302
+ this.exit(1);
303
+ }
304
+ const { selectOption } = await import('./ui/prompt-helpers.js');
305
+ const result = await selectOption(message, options, config?.context, config?.helpText, config?.headerVariant);
306
+ return result;
307
+ }
308
+ async textInput(message, options) {
309
+ if (this.isJsonMode) {
310
+ this.outputError({ message: 'Interactive text input not available in JSON mode', code: 'INTERACTIVE_REQUIRED' });
311
+ this.exit(1);
312
+ }
313
+ const { textInput } = await import('./ui/prompt-helpers.js');
314
+ const result = await textInput(message, options);
315
+ return result;
316
+ }
317
+ async confirmPrompt(message, defaultYes = false, config) {
318
+ if (this.isJsonMode)
319
+ return false;
320
+ const { confirmPrompt } = await import('./ui/prompt-helpers.js');
321
+ const result = await confirmPrompt(message, defaultYes, config?.context, config?.helpText, config?.headerVariant);
322
+ return result ?? false;
323
+ }
324
+ async confirmAction(message, force, config) {
325
+ if (this.isDryRun || force)
326
+ return true;
327
+ if (this.isJsonMode) {
328
+ this.outputError({ message: 'Confirmation required. Use --force to skip confirmation.', code: 'CONFIRMATION_REQUIRED' });
329
+ this.exit(1);
330
+ }
331
+ const confirmed = await this.confirmPrompt(message, false, config);
332
+ if (!confirmed) {
333
+ this.log('Aborted.');
334
+ return false;
335
+ }
336
+ return true;
337
+ }
338
+ async withRetry(fn) {
339
+ if (this.retryCount === 0) {
340
+ return fn();
341
+ }
342
+ const { result, attempts } = await runWithRetry(fn, { maxAttempts: this.retryCount });
343
+ // Accumulate across multiple calls within a single run so commands that
344
+ // make several API calls (e.g. template:pull fetches project + template)
345
+ // report every retry in metadata.retries.
346
+ this.lastRetryAttempts.push(...attempts);
347
+ return result;
348
+ }
349
+ async withSpinner(message, fn) {
350
+ const run = this.retryCount > 0 ? () => this.withRetry(fn) : fn;
351
+ if (this.isJsonMode)
352
+ return run();
353
+ const { render } = await import('ink');
354
+ const { Spinner } = await import('./ui/components/spinner.js');
355
+ const React = await import('react');
356
+ const instance = render(React.createElement(Spinner, { message }));
357
+ try {
358
+ const result = await run();
359
+ instance.clear?.();
360
+ instance.unmount();
361
+ return result;
362
+ }
363
+ catch (error) {
364
+ instance.clear?.();
365
+ instance.unmount();
366
+ throw error;
367
+ }
368
+ }
369
+ async handleActionHints(rawHints) {
370
+ if (!this.isInteractiveMode)
371
+ return;
372
+ const { parseActionHints } = await import('./core/api/action-hints.js');
373
+ const hints = parseActionHints(rawHints);
374
+ if (hints.length === 0)
375
+ return;
376
+ const { renderActionHintsMenu } = await import('./ui/prompt-helpers.js');
377
+ await renderActionHintsMenu(hints);
378
+ }
379
+ async handleDryRun(payload) {
380
+ if (this.isJsonMode) {
381
+ this.outputJson(null, { dry_run: true, would_execute: payload });
382
+ }
383
+ else {
384
+ const { SuccessMessage } = await import('./ui/index.js');
385
+ const React = await import('react');
386
+ await this.renderInk(React.createElement(SuccessMessage, { message: '[DRY-RUN] Would execute operation' }));
387
+ this.log(` Payload: ${JSON.stringify(payload, null, 2)}`);
388
+ }
389
+ }
390
+ // Handle errors — in JSON mode output JSON error, otherwise use oclif default
391
+ async catch(error) {
392
+ if (this.isJsonMode) {
393
+ this.outputError({ message: error.message, code: error.code || error.constructor.name });
394
+ this.exit(1);
395
+ }
396
+ throw error;
397
+ }
398
+ }