popeye-cli 1.0.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 (209) hide show
  1. package/.env.example +25 -0
  2. package/.prettierrc +8 -0
  3. package/README.md +320 -0
  4. package/dist/adapters/claude.d.ts +82 -0
  5. package/dist/adapters/claude.d.ts.map +1 -0
  6. package/dist/adapters/claude.js +230 -0
  7. package/dist/adapters/claude.js.map +1 -0
  8. package/dist/adapters/openai.d.ts +48 -0
  9. package/dist/adapters/openai.d.ts.map +1 -0
  10. package/dist/adapters/openai.js +257 -0
  11. package/dist/adapters/openai.js.map +1 -0
  12. package/dist/auth/claude.d.ts +44 -0
  13. package/dist/auth/claude.d.ts.map +1 -0
  14. package/dist/auth/claude.js +139 -0
  15. package/dist/auth/claude.js.map +1 -0
  16. package/dist/auth/index.d.ts +61 -0
  17. package/dist/auth/index.d.ts.map +1 -0
  18. package/dist/auth/index.js +141 -0
  19. package/dist/auth/index.js.map +1 -0
  20. package/dist/auth/keychain.d.ts +66 -0
  21. package/dist/auth/keychain.d.ts.map +1 -0
  22. package/dist/auth/keychain.js +125 -0
  23. package/dist/auth/keychain.js.map +1 -0
  24. package/dist/auth/openai-entry.d.ts +9 -0
  25. package/dist/auth/openai-entry.d.ts.map +1 -0
  26. package/dist/auth/openai-entry.js +410 -0
  27. package/dist/auth/openai-entry.js.map +1 -0
  28. package/dist/auth/openai.d.ts +71 -0
  29. package/dist/auth/openai.d.ts.map +1 -0
  30. package/dist/auth/openai.js +212 -0
  31. package/dist/auth/openai.js.map +1 -0
  32. package/dist/auth/server.d.ts +32 -0
  33. package/dist/auth/server.d.ts.map +1 -0
  34. package/dist/auth/server.js +213 -0
  35. package/dist/auth/server.js.map +1 -0
  36. package/dist/cli/commands/auth.d.ts +10 -0
  37. package/dist/cli/commands/auth.d.ts.map +1 -0
  38. package/dist/cli/commands/auth.js +162 -0
  39. package/dist/cli/commands/auth.js.map +1 -0
  40. package/dist/cli/commands/config.d.ts +10 -0
  41. package/dist/cli/commands/config.d.ts.map +1 -0
  42. package/dist/cli/commands/config.js +215 -0
  43. package/dist/cli/commands/config.js.map +1 -0
  44. package/dist/cli/commands/create.d.ts +10 -0
  45. package/dist/cli/commands/create.d.ts.map +1 -0
  46. package/dist/cli/commands/create.js +240 -0
  47. package/dist/cli/commands/create.js.map +1 -0
  48. package/dist/cli/commands/index.d.ts +10 -0
  49. package/dist/cli/commands/index.d.ts.map +1 -0
  50. package/dist/cli/commands/index.js +10 -0
  51. package/dist/cli/commands/index.js.map +1 -0
  52. package/dist/cli/commands/resume.d.ts +18 -0
  53. package/dist/cli/commands/resume.d.ts.map +1 -0
  54. package/dist/cli/commands/resume.js +241 -0
  55. package/dist/cli/commands/resume.js.map +1 -0
  56. package/dist/cli/commands/status.d.ts +18 -0
  57. package/dist/cli/commands/status.d.ts.map +1 -0
  58. package/dist/cli/commands/status.js +154 -0
  59. package/dist/cli/commands/status.js.map +1 -0
  60. package/dist/cli/index.d.ts +17 -0
  61. package/dist/cli/index.d.ts.map +1 -0
  62. package/dist/cli/index.js +71 -0
  63. package/dist/cli/index.js.map +1 -0
  64. package/dist/cli/interactive.d.ts +9 -0
  65. package/dist/cli/interactive.d.ts.map +1 -0
  66. package/dist/cli/interactive.js +330 -0
  67. package/dist/cli/interactive.js.map +1 -0
  68. package/dist/cli/output.d.ts +182 -0
  69. package/dist/cli/output.d.ts.map +1 -0
  70. package/dist/cli/output.js +355 -0
  71. package/dist/cli/output.js.map +1 -0
  72. package/dist/config/defaults.d.ts +57 -0
  73. package/dist/config/defaults.d.ts.map +1 -0
  74. package/dist/config/defaults.js +103 -0
  75. package/dist/config/defaults.js.map +1 -0
  76. package/dist/config/index.d.ts +138 -0
  77. package/dist/config/index.d.ts.map +1 -0
  78. package/dist/config/index.js +244 -0
  79. package/dist/config/index.js.map +1 -0
  80. package/dist/config/schema.d.ts +220 -0
  81. package/dist/config/schema.d.ts.map +1 -0
  82. package/dist/config/schema.js +141 -0
  83. package/dist/config/schema.js.map +1 -0
  84. package/dist/generators/index.d.ts +101 -0
  85. package/dist/generators/index.d.ts.map +1 -0
  86. package/dist/generators/index.js +200 -0
  87. package/dist/generators/index.js.map +1 -0
  88. package/dist/generators/python.d.ts +48 -0
  89. package/dist/generators/python.d.ts.map +1 -0
  90. package/dist/generators/python.js +262 -0
  91. package/dist/generators/python.js.map +1 -0
  92. package/dist/generators/templates/index.d.ts +6 -0
  93. package/dist/generators/templates/index.d.ts.map +1 -0
  94. package/dist/generators/templates/index.js +6 -0
  95. package/dist/generators/templates/index.js.map +1 -0
  96. package/dist/generators/templates/python.d.ts +53 -0
  97. package/dist/generators/templates/python.d.ts.map +1 -0
  98. package/dist/generators/templates/python.js +454 -0
  99. package/dist/generators/templates/python.js.map +1 -0
  100. package/dist/generators/templates/typescript.d.ts +53 -0
  101. package/dist/generators/templates/typescript.d.ts.map +1 -0
  102. package/dist/generators/templates/typescript.js +394 -0
  103. package/dist/generators/templates/typescript.js.map +1 -0
  104. package/dist/generators/typescript.d.ts +64 -0
  105. package/dist/generators/typescript.d.ts.map +1 -0
  106. package/dist/generators/typescript.js +271 -0
  107. package/dist/generators/typescript.js.map +1 -0
  108. package/dist/index.d.ts +7 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +12 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/state/index.d.ts +168 -0
  113. package/dist/state/index.d.ts.map +1 -0
  114. package/dist/state/index.js +338 -0
  115. package/dist/state/index.js.map +1 -0
  116. package/dist/state/persistence.d.ts +91 -0
  117. package/dist/state/persistence.d.ts.map +1 -0
  118. package/dist/state/persistence.js +201 -0
  119. package/dist/state/persistence.js.map +1 -0
  120. package/dist/types/cli.d.ts +132 -0
  121. package/dist/types/cli.d.ts.map +1 -0
  122. package/dist/types/cli.js +17 -0
  123. package/dist/types/cli.js.map +1 -0
  124. package/dist/types/consensus.d.ts +111 -0
  125. package/dist/types/consensus.d.ts.map +1 -0
  126. package/dist/types/consensus.js +29 -0
  127. package/dist/types/consensus.js.map +1 -0
  128. package/dist/types/index.d.ts +9 -0
  129. package/dist/types/index.d.ts.map +1 -0
  130. package/dist/types/index.js +13 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/dist/types/project.d.ts +73 -0
  133. package/dist/types/project.d.ts.map +1 -0
  134. package/dist/types/project.js +55 -0
  135. package/dist/types/project.js.map +1 -0
  136. package/dist/types/workflow.d.ts +236 -0
  137. package/dist/types/workflow.d.ts.map +1 -0
  138. package/dist/types/workflow.js +74 -0
  139. package/dist/types/workflow.js.map +1 -0
  140. package/dist/workflow/consensus.d.ts +89 -0
  141. package/dist/workflow/consensus.d.ts.map +1 -0
  142. package/dist/workflow/consensus.js +220 -0
  143. package/dist/workflow/consensus.js.map +1 -0
  144. package/dist/workflow/execution-mode.d.ts +82 -0
  145. package/dist/workflow/execution-mode.d.ts.map +1 -0
  146. package/dist/workflow/execution-mode.js +346 -0
  147. package/dist/workflow/execution-mode.js.map +1 -0
  148. package/dist/workflow/index.d.ts +110 -0
  149. package/dist/workflow/index.d.ts.map +1 -0
  150. package/dist/workflow/index.js +283 -0
  151. package/dist/workflow/index.js.map +1 -0
  152. package/dist/workflow/plan-mode.d.ts +83 -0
  153. package/dist/workflow/plan-mode.d.ts.map +1 -0
  154. package/dist/workflow/plan-mode.js +241 -0
  155. package/dist/workflow/plan-mode.js.map +1 -0
  156. package/dist/workflow/test-runner.d.ts +87 -0
  157. package/dist/workflow/test-runner.d.ts.map +1 -0
  158. package/dist/workflow/test-runner.js +273 -0
  159. package/dist/workflow/test-runner.js.map +1 -0
  160. package/eslint.config.js +25 -0
  161. package/package.json +66 -0
  162. package/src/adapters/claude.ts +298 -0
  163. package/src/adapters/openai.ts +300 -0
  164. package/src/auth/claude.ts +166 -0
  165. package/src/auth/index.ts +171 -0
  166. package/src/auth/keychain.ts +138 -0
  167. package/src/auth/openai-entry.ts +410 -0
  168. package/src/auth/openai.ts +260 -0
  169. package/src/auth/server.ts +252 -0
  170. package/src/cli/commands/auth.ts +194 -0
  171. package/src/cli/commands/config.ts +241 -0
  172. package/src/cli/commands/create.ts +308 -0
  173. package/src/cli/commands/index.ts +10 -0
  174. package/src/cli/commands/resume.ts +304 -0
  175. package/src/cli/commands/status.ts +189 -0
  176. package/src/cli/index.ts +90 -0
  177. package/src/cli/interactive.ts +418 -0
  178. package/src/cli/output.ts +410 -0
  179. package/src/config/defaults.ts +114 -0
  180. package/src/config/index.ts +315 -0
  181. package/src/config/schema.ts +164 -0
  182. package/src/generators/index.ts +251 -0
  183. package/src/generators/python.ts +318 -0
  184. package/src/generators/templates/index.ts +6 -0
  185. package/src/generators/templates/python.ts +465 -0
  186. package/src/generators/templates/typescript.ts +417 -0
  187. package/src/generators/typescript.ts +340 -0
  188. package/src/index.ts +13 -0
  189. package/src/state/index.ts +454 -0
  190. package/src/state/persistence.ts +230 -0
  191. package/src/types/cli.ts +146 -0
  192. package/src/types/consensus.ts +116 -0
  193. package/src/types/index.ts +64 -0
  194. package/src/types/project.ts +85 -0
  195. package/src/types/workflow.ts +149 -0
  196. package/src/workflow/consensus.ts +299 -0
  197. package/src/workflow/execution-mode.ts +517 -0
  198. package/src/workflow/index.ts +396 -0
  199. package/src/workflow/plan-mode.ts +356 -0
  200. package/src/workflow/test-runner.ts +345 -0
  201. package/tests/adapters/openai.test.ts +145 -0
  202. package/tests/config/config.test.ts +208 -0
  203. package/tests/generators/generators.test.ts +185 -0
  204. package/tests/types/consensus.test.ts +152 -0
  205. package/tests/types/project.test.ts +134 -0
  206. package/tests/workflow/consensus.test.ts +221 -0
  207. package/tests/workflow/test-runner.test.ts +214 -0
  208. package/tsconfig.json +25 -0
  209. package/vitest.config.ts +22 -0
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Local OAuth callback server for authentication flows
3
+ * Handles browser-based OAuth redirects and token entry
4
+ */
5
+
6
+ import express, { type Express, type Request, type Response } from 'express';
7
+ import { createServer, type Server } from 'http';
8
+ import { getOpenAIEntryHTML } from './openai-entry.js';
9
+
10
+ /**
11
+ * Find an available port in the specified range
12
+ */
13
+ export async function findAvailablePort(start: number, end: number): Promise<number> {
14
+ const net = await import('net');
15
+
16
+ for (let port = start; port <= end; port++) {
17
+ const isAvailable = await new Promise<boolean>((resolve) => {
18
+ const server = net.createServer();
19
+ server.once('error', () => resolve(false));
20
+ server.once('listening', () => {
21
+ server.close();
22
+ resolve(true);
23
+ });
24
+ server.listen(port, '127.0.0.1');
25
+ });
26
+
27
+ if (isAvailable) {
28
+ return port;
29
+ }
30
+ }
31
+
32
+ throw new Error(`No available ports found in range ${start}-${end}`);
33
+ }
34
+
35
+ /**
36
+ * Result from the auth callback server
37
+ */
38
+ export interface AuthCallbackResult {
39
+ success: boolean;
40
+ token?: string;
41
+ error?: string;
42
+ }
43
+
44
+ /**
45
+ * Create and start an OAuth callback server
46
+ *
47
+ * @param options - Server options
48
+ * @returns Promise that resolves when a token is received
49
+ */
50
+ export async function startAuthCallbackServer(options: {
51
+ port?: number;
52
+ timeout?: number;
53
+ type: 'claude' | 'openai';
54
+ }): Promise<AuthCallbackResult> {
55
+ const { type, timeout = 300000 } = options; // 5 minute default timeout
56
+ const port = options.port || (await findAvailablePort(3000, 3100));
57
+
58
+ return new Promise((resolve) => {
59
+ const app: Express = express();
60
+ // eslint-disable-next-line prefer-const
61
+ let server: Server;
62
+
63
+ // Parse JSON bodies
64
+ app.use(express.json());
65
+ app.use(express.urlencoded({ extended: true }));
66
+
67
+ // Serve the token entry page for OpenAI
68
+ if (type === 'openai') {
69
+ app.get('/', (_req: Request, res: Response) => {
70
+ res.setHeader('Content-Type', 'text/html');
71
+ res.send(getOpenAIEntryHTML(port));
72
+ });
73
+
74
+ // Handle token submission
75
+ app.get('/submit', (req: Request, res: Response) => {
76
+ const token = req.query.token as string;
77
+
78
+ if (!token) {
79
+ res.status(400).send('<html><body><h1>Error: No token provided</h1></body></html>');
80
+ return;
81
+ }
82
+
83
+ res.send(`
84
+ <html>
85
+ <head>
86
+ <style>
87
+ body {
88
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
89
+ display: flex;
90
+ justify-content: center;
91
+ align-items: center;
92
+ height: 100vh;
93
+ margin: 0;
94
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
95
+ color: white;
96
+ }
97
+ .container {
98
+ text-align: center;
99
+ padding: 40px;
100
+ background: rgba(255, 255, 255, 0.1);
101
+ border-radius: 16px;
102
+ backdrop-filter: blur(10px);
103
+ }
104
+ h1 { margin-bottom: 16px; }
105
+ p { opacity: 0.9; }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div class="container">
110
+ <h1>Token Received!</h1>
111
+ <p>You can close this window and return to the terminal.</p>
112
+ </div>
113
+ </body>
114
+ </html>
115
+ `);
116
+
117
+ // Close server after sending response
118
+ setTimeout(() => {
119
+ server.close();
120
+ resolve({ success: true, token });
121
+ }, 100);
122
+ });
123
+
124
+ // Handle POST submission as well
125
+ app.post('/submit', (req: Request, res: Response) => {
126
+ const token = req.body.token as string;
127
+
128
+ if (!token) {
129
+ res.status(400).json({ error: 'No token provided' });
130
+ return;
131
+ }
132
+
133
+ res.json({ success: true });
134
+
135
+ setTimeout(() => {
136
+ server.close();
137
+ resolve({ success: true, token });
138
+ }, 100);
139
+ });
140
+ }
141
+
142
+ // Handle Claude OAuth callback
143
+ if (type === 'claude') {
144
+ app.get('/callback', (req: Request, res: Response) => {
145
+ const code = req.query.code as string;
146
+ const error = req.query.error as string;
147
+
148
+ if (error) {
149
+ res.send(`
150
+ <html>
151
+ <body>
152
+ <h1>Authentication Failed</h1>
153
+ <p>Error: ${error}</p>
154
+ <p>You can close this window.</p>
155
+ </body>
156
+ </html>
157
+ `);
158
+ setTimeout(() => {
159
+ server.close();
160
+ resolve({ success: false, error });
161
+ }, 100);
162
+ return;
163
+ }
164
+
165
+ if (!code) {
166
+ res.status(400).send('<html><body><h1>Error: No authorization code</h1></body></html>');
167
+ return;
168
+ }
169
+
170
+ res.send(`
171
+ <html>
172
+ <head>
173
+ <style>
174
+ body {
175
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
176
+ display: flex;
177
+ justify-content: center;
178
+ align-items: center;
179
+ height: 100vh;
180
+ margin: 0;
181
+ background: linear-gradient(135deg, #da7756 0%, #c35f3b 100%);
182
+ color: white;
183
+ }
184
+ .container {
185
+ text-align: center;
186
+ padding: 40px;
187
+ background: rgba(255, 255, 255, 0.1);
188
+ border-radius: 16px;
189
+ backdrop-filter: blur(10px);
190
+ }
191
+ </style>
192
+ </head>
193
+ <body>
194
+ <div class="container">
195
+ <h1>Authentication Successful!</h1>
196
+ <p>You can close this window and return to the terminal.</p>
197
+ </div>
198
+ </body>
199
+ </html>
200
+ `);
201
+
202
+ setTimeout(() => {
203
+ server.close();
204
+ resolve({ success: true, token: code });
205
+ }, 100);
206
+ });
207
+ }
208
+
209
+ // Handle cancel
210
+ app.get('/cancel', (_req: Request, res: Response) => {
211
+ res.send(`
212
+ <html>
213
+ <body>
214
+ <h1>Authentication Cancelled</h1>
215
+ <p>You can close this window.</p>
216
+ </body>
217
+ </html>
218
+ `);
219
+ setTimeout(() => {
220
+ server.close();
221
+ resolve({ success: false, error: 'User cancelled authentication' });
222
+ }, 100);
223
+ });
224
+
225
+ // Start server
226
+ server = createServer(app);
227
+ server.listen(port, '127.0.0.1', () => {
228
+ console.log(`Auth server listening on http://127.0.0.1:${port}`);
229
+ });
230
+
231
+ // Set timeout
232
+ const timeoutId = setTimeout(() => {
233
+ server.close();
234
+ resolve({ success: false, error: 'Authentication timed out' });
235
+ }, timeout);
236
+
237
+ // Clear timeout when server closes
238
+ server.on('close', () => {
239
+ clearTimeout(timeoutId);
240
+ });
241
+ });
242
+ }
243
+
244
+ /**
245
+ * Get the callback URL for OAuth flows
246
+ */
247
+ export function getCallbackUrl(port: number, type: 'claude' | 'openai'): string {
248
+ if (type === 'claude') {
249
+ return `http://127.0.0.1:${port}/callback`;
250
+ }
251
+ return `http://127.0.0.1:${port}`;
252
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Authentication commands
3
+ * Handles login, logout, and status for Claude and OpenAI
4
+ */
5
+
6
+ import { Command } from 'commander';
7
+ import {
8
+ getAuthStatusForDisplay,
9
+ authenticateService,
10
+ logout,
11
+ isAuthenticated,
12
+ } from '../../auth/index.js';
13
+ import { authenticateOpenAIWithKey } from '../../auth/openai.js';
14
+ import {
15
+ printHeader,
16
+ printAuthStatus,
17
+ printSuccess,
18
+ printError,
19
+ printInfo,
20
+ startSpinner,
21
+ succeedSpinner,
22
+ failSpinner,
23
+ } from '../output.js';
24
+
25
+ /**
26
+ * Create the auth command
27
+ */
28
+ export function createAuthCommand(): Command {
29
+ const auth = new Command('auth')
30
+ .description('Manage authentication for Claude CLI and OpenAI API');
31
+
32
+ // Status subcommand
33
+ auth
34
+ .command('status')
35
+ .description('Show authentication status')
36
+ .action(async () => {
37
+ startSpinner('Checking authentication status...');
38
+
39
+ try {
40
+ const status = await getAuthStatusForDisplay();
41
+ succeedSpinner('Status retrieved');
42
+
43
+ printAuthStatus(status);
44
+
45
+ if (!status.claude.authenticated || !status.openai.authenticated) {
46
+ console.log();
47
+ printInfo('Run "popeye-cli auth login" to authenticate missing services.');
48
+ }
49
+ } catch (error) {
50
+ failSpinner('Failed to check status');
51
+ printError(error instanceof Error ? error.message : 'Unknown error');
52
+ process.exit(1);
53
+ }
54
+ });
55
+
56
+ // Login subcommand
57
+ auth
58
+ .command('login')
59
+ .description('Authenticate with services')
60
+ .argument('[service]', 'Service to authenticate (claude, openai, all)', 'all')
61
+ .option('--api-key <key>', 'OpenAI API key (for openai service)')
62
+ .action(async (service: 'claude' | 'openai' | 'all', options) => {
63
+ // Validate service
64
+ if (!['claude', 'openai', 'all'].includes(service)) {
65
+ printError(`Invalid service: ${service}. Use 'claude', 'openai', or 'all'.`);
66
+ process.exit(1);
67
+ }
68
+
69
+ printHeader('Authentication');
70
+
71
+ try {
72
+ // Handle API key for OpenAI
73
+ if ((service === 'openai' || service === 'all') && options.apiKey) {
74
+ startSpinner('Validating OpenAI API key...');
75
+ const success = await authenticateOpenAIWithKey(options.apiKey);
76
+
77
+ if (success) {
78
+ succeedSpinner('OpenAI API authenticated');
79
+ } else {
80
+ failSpinner('OpenAI API authentication failed');
81
+ if (service === 'openai') {
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ // If only OpenAI was requested, we're done
87
+ if (service === 'openai') {
88
+ return;
89
+ }
90
+
91
+ // Continue with Claude if 'all'
92
+ if (service === 'all') {
93
+ service = 'claude';
94
+ }
95
+ }
96
+
97
+ // Interactive authentication
98
+ const success = await authenticateService(service);
99
+
100
+ if (success) {
101
+ printSuccess('Authentication complete!');
102
+ } else {
103
+ printError('Authentication failed');
104
+ process.exit(1);
105
+ }
106
+ } catch (error) {
107
+ printError(error instanceof Error ? error.message : 'Unknown error');
108
+ process.exit(1);
109
+ }
110
+ });
111
+
112
+ // Logout subcommand
113
+ auth
114
+ .command('logout')
115
+ .description('Remove stored credentials')
116
+ .argument('[service]', 'Service to logout from (claude, openai, all)', 'all')
117
+ .action(async (service: 'claude' | 'openai' | 'all') => {
118
+ // Validate service
119
+ if (!['claude', 'openai', 'all'].includes(service)) {
120
+ printError(`Invalid service: ${service}. Use 'claude', 'openai', or 'all'.`);
121
+ process.exit(1);
122
+ }
123
+
124
+ try {
125
+ await logout(service);
126
+ printSuccess(`Logged out from ${service === 'all' ? 'all services' : service}`);
127
+ } catch (error) {
128
+ printError(error instanceof Error ? error.message : 'Unknown error');
129
+ process.exit(1);
130
+ }
131
+ });
132
+
133
+ // Claude-specific subcommand
134
+ auth
135
+ .command('claude')
136
+ .description('Authenticate with Claude CLI')
137
+ .action(async () => {
138
+ printHeader('Claude CLI Authentication');
139
+
140
+ const alreadyAuth = await isAuthenticated('claude');
141
+ if (alreadyAuth) {
142
+ printInfo('Already authenticated with Claude CLI');
143
+ return;
144
+ }
145
+
146
+ const success = await authenticateService('claude');
147
+
148
+ if (success) {
149
+ printSuccess('Claude CLI authenticated!');
150
+ } else {
151
+ printError('Claude CLI authentication failed');
152
+ process.exit(1);
153
+ }
154
+ });
155
+
156
+ // OpenAI-specific subcommand
157
+ auth
158
+ .command('openai')
159
+ .description('Authenticate with OpenAI API')
160
+ .option('--api-key <key>', 'OpenAI API key')
161
+ .action(async (options) => {
162
+ printHeader('OpenAI API Authentication');
163
+
164
+ if (options.apiKey) {
165
+ startSpinner('Validating API key...');
166
+ const success = await authenticateOpenAIWithKey(options.apiKey);
167
+
168
+ if (success) {
169
+ succeedSpinner('OpenAI API authenticated!');
170
+ } else {
171
+ failSpinner('Invalid API key');
172
+ process.exit(1);
173
+ }
174
+ return;
175
+ }
176
+
177
+ const alreadyAuth = await isAuthenticated('openai');
178
+ if (alreadyAuth) {
179
+ printInfo('Already authenticated with OpenAI API');
180
+ return;
181
+ }
182
+
183
+ const success = await authenticateService('openai');
184
+
185
+ if (success) {
186
+ printSuccess('OpenAI API authenticated!');
187
+ } else {
188
+ printError('OpenAI API authentication failed');
189
+ process.exit(1);
190
+ }
191
+ });
192
+
193
+ return auth;
194
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Config command
3
+ * Manage CLI configuration
4
+ */
5
+
6
+ import { Command } from 'commander';
7
+ import path from 'node:path';
8
+ import { loadConfig, getConfigPath } from '../../config/index.js';
9
+ import { DEFAULT_CONFIG } from '../../config/defaults.js';
10
+ import {
11
+ printHeader,
12
+ printSection,
13
+ printSuccess,
14
+ printError,
15
+ printInfo,
16
+ printKeyValue,
17
+ } from '../output.js';
18
+
19
+ /**
20
+ * Create the config command
21
+ */
22
+ export function createConfigCommand(): Command {
23
+ const config = new Command('config')
24
+ .description('Manage CLI configuration');
25
+
26
+ // Show current config
27
+ config
28
+ .command('show')
29
+ .description('Show current configuration')
30
+ .option('--json', 'Output as JSON')
31
+ .action(async (options) => {
32
+ try {
33
+ const loadedConfig = await loadConfig();
34
+ const configPath = getConfigPath();
35
+
36
+ if (options.json) {
37
+ console.log(JSON.stringify(loadedConfig, null, 2));
38
+ return;
39
+ }
40
+
41
+ printHeader('Current Configuration');
42
+
43
+ if (configPath) {
44
+ printInfo(`Config file: ${configPath}`);
45
+ } else {
46
+ printInfo('Using default configuration');
47
+ }
48
+
49
+ console.log();
50
+ printConfigSection('Consensus', loadedConfig.consensus);
51
+ printConfigSection('APIs', loadedConfig.apis);
52
+ printConfigSection('Project', loadedConfig.project);
53
+ printConfigSection('Directories', loadedConfig.directories);
54
+ printConfigSection('Output', loadedConfig.output);
55
+ } catch (error) {
56
+ printError(error instanceof Error ? error.message : 'Unknown error');
57
+ process.exit(1);
58
+ }
59
+ });
60
+
61
+ // Show defaults
62
+ config
63
+ .command('defaults')
64
+ .description('Show default configuration values')
65
+ .option('--json', 'Output as JSON')
66
+ .action((options) => {
67
+ if (options.json) {
68
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
69
+ return;
70
+ }
71
+
72
+ printHeader('Default Configuration');
73
+
74
+ printConfigSection('Consensus', DEFAULT_CONFIG.consensus);
75
+ printConfigSection('APIs', DEFAULT_CONFIG.apis);
76
+ printConfigSection('Project', DEFAULT_CONFIG.project);
77
+ printConfigSection('Directories', DEFAULT_CONFIG.directories);
78
+ printConfigSection('Output', DEFAULT_CONFIG.output);
79
+ });
80
+
81
+ // Get a specific config value
82
+ config
83
+ .command('get')
84
+ .description('Get a specific configuration value')
85
+ .argument('<key>', 'Configuration key (e.g., consensus.threshold)')
86
+ .action(async (key: string) => {
87
+ try {
88
+ const loadedConfig = await loadConfig();
89
+ const value = getNestedValue(loadedConfig, key);
90
+
91
+ if (value === undefined) {
92
+ printError(`Configuration key not found: ${key}`);
93
+ process.exit(1);
94
+ }
95
+
96
+ if (typeof value === 'object') {
97
+ console.log(JSON.stringify(value, null, 2));
98
+ } else {
99
+ console.log(value);
100
+ }
101
+ } catch (error) {
102
+ printError(error instanceof Error ? error.message : 'Unknown error');
103
+ process.exit(1);
104
+ }
105
+ });
106
+
107
+ // Show config file path
108
+ config
109
+ .command('path')
110
+ .description('Show configuration file path')
111
+ .action(() => {
112
+ const configPath = getConfigPath();
113
+
114
+ if (configPath) {
115
+ console.log(configPath);
116
+ } else {
117
+ printInfo('No configuration file found');
118
+ printInfo('Create one at: .popeyerc, .popeyerc.json, .popeyerc.yaml, or popeye.config.js');
119
+ }
120
+ });
121
+
122
+ // Init config file
123
+ config
124
+ .command('init')
125
+ .description('Create a configuration file')
126
+ .option('-f, --format <format>', 'Config format (json, yaml)', 'json')
127
+ .action(async (options) => {
128
+ const { promises: fs } = await import('node:fs');
129
+
130
+ const format = options.format;
131
+ let filename: string;
132
+ let content: string;
133
+
134
+ if (format === 'yaml') {
135
+ filename = '.popeyerc.yaml';
136
+ content = generateYamlConfig();
137
+ } else {
138
+ filename = '.popeyerc.json';
139
+ content = JSON.stringify(DEFAULT_CONFIG, null, 2);
140
+ }
141
+
142
+ const filepath = path.join(process.cwd(), filename);
143
+
144
+ try {
145
+ // Check if file exists
146
+ try {
147
+ await fs.access(filepath);
148
+ printError(`Configuration file already exists: ${filepath}`);
149
+ process.exit(1);
150
+ } catch {
151
+ // File doesn't exist, good to create
152
+ }
153
+
154
+ await fs.writeFile(filepath, content, 'utf-8');
155
+ printSuccess(`Created configuration file: ${filepath}`);
156
+ } catch (error) {
157
+ printError(error instanceof Error ? error.message : 'Failed to create config file');
158
+ process.exit(1);
159
+ }
160
+ });
161
+
162
+ return config;
163
+ }
164
+
165
+ /**
166
+ * Print a configuration section
167
+ */
168
+ function printConfigSection(name: string, section: Record<string, unknown>): void {
169
+ printSection(name);
170
+
171
+ for (const [key, value] of Object.entries(section)) {
172
+ if (typeof value === 'object' && value !== null) {
173
+ console.log(` ${key}:`);
174
+ for (const [subKey, subValue] of Object.entries(value as Record<string, unknown>)) {
175
+ printKeyValue(` ${subKey}`, String(subValue));
176
+ }
177
+ } else {
178
+ printKeyValue(` ${key}`, String(value));
179
+ }
180
+ }
181
+
182
+ console.log();
183
+ }
184
+
185
+ /**
186
+ * Get a nested value from an object using dot notation
187
+ */
188
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
189
+ const keys = path.split('.');
190
+ let current: unknown = obj;
191
+
192
+ for (const key of keys) {
193
+ if (current === null || current === undefined) {
194
+ return undefined;
195
+ }
196
+ if (typeof current !== 'object') {
197
+ return undefined;
198
+ }
199
+ current = (current as Record<string, unknown>)[key];
200
+ }
201
+
202
+ return current;
203
+ }
204
+
205
+ /**
206
+ * Generate YAML configuration content
207
+ */
208
+ function generateYamlConfig(): string {
209
+ return `# Popeye CLI Configuration
210
+ # See documentation for all available options
211
+
212
+ # Consensus settings
213
+ consensus:
214
+ threshold: 95
215
+ maxIterations: 5
216
+ temperature: 0.3
217
+ maxTokens: 4096
218
+
219
+ # API settings
220
+ apis:
221
+ openai:
222
+ model: gpt-4o
223
+ timeout: 120000
224
+
225
+ # Project defaults
226
+ project:
227
+ defaultLanguage: python
228
+ defaultName: my-project
229
+
230
+ # Directory settings
231
+ directories:
232
+ output: ./output
233
+ state: .popeye
234
+
235
+ # Output settings
236
+ output:
237
+ verbose: false
238
+ colors: true
239
+ progress: true
240
+ `;
241
+ }