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.
- package/.env.example +25 -0
- package/.prettierrc +8 -0
- package/README.md +320 -0
- package/dist/adapters/claude.d.ts +82 -0
- package/dist/adapters/claude.d.ts.map +1 -0
- package/dist/adapters/claude.js +230 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/openai.d.ts +48 -0
- package/dist/adapters/openai.d.ts.map +1 -0
- package/dist/adapters/openai.js +257 -0
- package/dist/adapters/openai.js.map +1 -0
- package/dist/auth/claude.d.ts +44 -0
- package/dist/auth/claude.d.ts.map +1 -0
- package/dist/auth/claude.js +139 -0
- package/dist/auth/claude.js.map +1 -0
- package/dist/auth/index.d.ts +61 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +141 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/keychain.d.ts +66 -0
- package/dist/auth/keychain.d.ts.map +1 -0
- package/dist/auth/keychain.js +125 -0
- package/dist/auth/keychain.js.map +1 -0
- package/dist/auth/openai-entry.d.ts +9 -0
- package/dist/auth/openai-entry.d.ts.map +1 -0
- package/dist/auth/openai-entry.js +410 -0
- package/dist/auth/openai-entry.js.map +1 -0
- package/dist/auth/openai.d.ts +71 -0
- package/dist/auth/openai.d.ts.map +1 -0
- package/dist/auth/openai.js +212 -0
- package/dist/auth/openai.js.map +1 -0
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -0
- package/dist/auth/server.js +213 -0
- package/dist/auth/server.js.map +1 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.d.ts.map +1 -0
- package/dist/cli/commands/auth.js +162 -0
- package/dist/cli/commands/auth.js.map +1 -0
- package/dist/cli/commands/config.d.ts +10 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +215 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/create.d.ts +10 -0
- package/dist/cli/commands/create.d.ts.map +1 -0
- package/dist/cli/commands/create.js +240 -0
- package/dist/cli/commands/create.js.map +1 -0
- package/dist/cli/commands/index.d.ts +10 -0
- package/dist/cli/commands/index.d.ts.map +1 -0
- package/dist/cli/commands/index.js +10 -0
- package/dist/cli/commands/index.js.map +1 -0
- package/dist/cli/commands/resume.d.ts +18 -0
- package/dist/cli/commands/resume.d.ts.map +1 -0
- package/dist/cli/commands/resume.js +241 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/status.d.ts +18 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +154 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +71 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/interactive.d.ts +9 -0
- package/dist/cli/interactive.d.ts.map +1 -0
- package/dist/cli/interactive.js +330 -0
- package/dist/cli/interactive.js.map +1 -0
- package/dist/cli/output.d.ts +182 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +355 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/config/defaults.d.ts +57 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +103 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +138 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +244 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +220 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +141 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/generators/index.d.ts +101 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +200 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/python.d.ts +48 -0
- package/dist/generators/python.d.ts.map +1 -0
- package/dist/generators/python.js +262 -0
- package/dist/generators/python.js.map +1 -0
- package/dist/generators/templates/index.d.ts +6 -0
- package/dist/generators/templates/index.d.ts.map +1 -0
- package/dist/generators/templates/index.js +6 -0
- package/dist/generators/templates/index.js.map +1 -0
- package/dist/generators/templates/python.d.ts +53 -0
- package/dist/generators/templates/python.d.ts.map +1 -0
- package/dist/generators/templates/python.js +454 -0
- package/dist/generators/templates/python.js.map +1 -0
- package/dist/generators/templates/typescript.d.ts +53 -0
- package/dist/generators/templates/typescript.d.ts.map +1 -0
- package/dist/generators/templates/typescript.js +394 -0
- package/dist/generators/templates/typescript.js.map +1 -0
- package/dist/generators/typescript.d.ts +64 -0
- package/dist/generators/typescript.d.ts.map +1 -0
- package/dist/generators/typescript.js +271 -0
- package/dist/generators/typescript.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/state/index.d.ts +168 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +338 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/persistence.d.ts +91 -0
- package/dist/state/persistence.d.ts.map +1 -0
- package/dist/state/persistence.js +201 -0
- package/dist/state/persistence.js.map +1 -0
- package/dist/types/cli.d.ts +132 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +17 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/consensus.d.ts +111 -0
- package/dist/types/consensus.d.ts.map +1 -0
- package/dist/types/consensus.js +29 -0
- package/dist/types/consensus.js.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +73 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +55 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/workflow.d.ts +236 -0
- package/dist/types/workflow.d.ts.map +1 -0
- package/dist/types/workflow.js +74 -0
- package/dist/types/workflow.js.map +1 -0
- package/dist/workflow/consensus.d.ts +89 -0
- package/dist/workflow/consensus.d.ts.map +1 -0
- package/dist/workflow/consensus.js +220 -0
- package/dist/workflow/consensus.js.map +1 -0
- package/dist/workflow/execution-mode.d.ts +82 -0
- package/dist/workflow/execution-mode.d.ts.map +1 -0
- package/dist/workflow/execution-mode.js +346 -0
- package/dist/workflow/execution-mode.js.map +1 -0
- package/dist/workflow/index.d.ts +110 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +283 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/plan-mode.d.ts +83 -0
- package/dist/workflow/plan-mode.d.ts.map +1 -0
- package/dist/workflow/plan-mode.js +241 -0
- package/dist/workflow/plan-mode.js.map +1 -0
- package/dist/workflow/test-runner.d.ts +87 -0
- package/dist/workflow/test-runner.d.ts.map +1 -0
- package/dist/workflow/test-runner.js +273 -0
- package/dist/workflow/test-runner.js.map +1 -0
- package/eslint.config.js +25 -0
- package/package.json +66 -0
- package/src/adapters/claude.ts +298 -0
- package/src/adapters/openai.ts +300 -0
- package/src/auth/claude.ts +166 -0
- package/src/auth/index.ts +171 -0
- package/src/auth/keychain.ts +138 -0
- package/src/auth/openai-entry.ts +410 -0
- package/src/auth/openai.ts +260 -0
- package/src/auth/server.ts +252 -0
- package/src/cli/commands/auth.ts +194 -0
- package/src/cli/commands/config.ts +241 -0
- package/src/cli/commands/create.ts +308 -0
- package/src/cli/commands/index.ts +10 -0
- package/src/cli/commands/resume.ts +304 -0
- package/src/cli/commands/status.ts +189 -0
- package/src/cli/index.ts +90 -0
- package/src/cli/interactive.ts +418 -0
- package/src/cli/output.ts +410 -0
- package/src/config/defaults.ts +114 -0
- package/src/config/index.ts +315 -0
- package/src/config/schema.ts +164 -0
- package/src/generators/index.ts +251 -0
- package/src/generators/python.ts +318 -0
- package/src/generators/templates/index.ts +6 -0
- package/src/generators/templates/python.ts +465 -0
- package/src/generators/templates/typescript.ts +417 -0
- package/src/generators/typescript.ts +340 -0
- package/src/index.ts +13 -0
- package/src/state/index.ts +454 -0
- package/src/state/persistence.ts +230 -0
- package/src/types/cli.ts +146 -0
- package/src/types/consensus.ts +116 -0
- package/src/types/index.ts +64 -0
- package/src/types/project.ts +85 -0
- package/src/types/workflow.ts +149 -0
- package/src/workflow/consensus.ts +299 -0
- package/src/workflow/execution-mode.ts +517 -0
- package/src/workflow/index.ts +396 -0
- package/src/workflow/plan-mode.ts +356 -0
- package/src/workflow/test-runner.ts +345 -0
- package/tests/adapters/openai.test.ts +145 -0
- package/tests/config/config.test.ts +208 -0
- package/tests/generators/generators.test.ts +185 -0
- package/tests/types/consensus.test.ts +152 -0
- package/tests/types/project.test.ts +134 -0
- package/tests/workflow/consensus.test.ts +221 -0
- package/tests/workflow/test-runner.test.ts +214 -0
- package/tsconfig.json +25 -0
- 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
|
+
}
|