@zibby/cli 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +926 -0
- package/bin/zibby.js +266 -0
- package/package.json +65 -0
- package/src/auth/cli-login.js +406 -0
- package/src/commands/analyze-graph.js +334 -0
- package/src/commands/ci-setup.js +65 -0
- package/src/commands/implement.js +664 -0
- package/src/commands/init.js +736 -0
- package/src/commands/list-projects.js +78 -0
- package/src/commands/memory.js +171 -0
- package/src/commands/run.js +926 -0
- package/src/commands/setup-scripts.js +101 -0
- package/src/commands/upload.js +163 -0
- package/src/commands/video.js +30 -0
- package/src/commands/workflow.js +369 -0
- package/src/config/config.js +117 -0
- package/src/config/environments.js +145 -0
- package/src/utils/execution-context.js +25 -0
- package/src/utils/progress-reporter.js +155 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Login Flow
|
|
3
|
+
* Implements OAuth device flow for CLI authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import { getApiUrl } from '../config/environments.js';
|
|
10
|
+
import {
|
|
11
|
+
saveSessionToken,
|
|
12
|
+
saveUserInfo,
|
|
13
|
+
getSessionToken,
|
|
14
|
+
getUserInfo,
|
|
15
|
+
clearSession
|
|
16
|
+
} from '../config/config.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Open URL in default browser (safe: no shell interpolation)
|
|
20
|
+
*/
|
|
21
|
+
function openBrowser(url) {
|
|
22
|
+
const platform = process.platform;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
let cmd, args;
|
|
26
|
+
if (platform === 'darwin') {
|
|
27
|
+
cmd = 'open';
|
|
28
|
+
args = [url];
|
|
29
|
+
} else if (platform === 'win32') {
|
|
30
|
+
cmd = 'cmd';
|
|
31
|
+
args = ['/c', 'start', '', url];
|
|
32
|
+
} else {
|
|
33
|
+
cmd = 'xdg-open';
|
|
34
|
+
args = [url];
|
|
35
|
+
}
|
|
36
|
+
spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
|
|
37
|
+
return true;
|
|
38
|
+
} catch (_error) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if user is already logged in
|
|
45
|
+
*/
|
|
46
|
+
export function checkExistingSession() {
|
|
47
|
+
const token = getSessionToken();
|
|
48
|
+
const user = getUserInfo();
|
|
49
|
+
|
|
50
|
+
if (token && user) {
|
|
51
|
+
return { loggedIn: true, user, token };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { loggedIn: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initiate CLI login flow
|
|
59
|
+
*/
|
|
60
|
+
export async function loginCli() {
|
|
61
|
+
try {
|
|
62
|
+
console.log(chalk.cyan('\n🔐 Initiating login...\n'));
|
|
63
|
+
|
|
64
|
+
// Check if already logged in
|
|
65
|
+
const existingSession = checkExistingSession();
|
|
66
|
+
if (existingSession.loggedIn) {
|
|
67
|
+
console.log(chalk.green('✅ Already logged in!'));
|
|
68
|
+
console.log(chalk.gray(`User: ${existingSession.user.email}`));
|
|
69
|
+
console.log(chalk.gray(`Name: ${existingSession.user.name}\n`));
|
|
70
|
+
|
|
71
|
+
// Ask if user wants to continue with existing session
|
|
72
|
+
const { createInterface } = await import('readline');
|
|
73
|
+
const rl = createInterface({
|
|
74
|
+
input: process.stdin,
|
|
75
|
+
output: process.stdout,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
// Handle Ctrl+C gracefully
|
|
80
|
+
const cleanup = () => {
|
|
81
|
+
rl.close();
|
|
82
|
+
if (process.stdin.isTTY) {
|
|
83
|
+
process.stdin.setRawMode(false);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const sigintHandler = () => {
|
|
88
|
+
console.log(chalk.yellow('\n\n⚠️ Login cancelled\n'));
|
|
89
|
+
cleanup();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
process.on('SIGINT', sigintHandler);
|
|
94
|
+
|
|
95
|
+
rl.question(chalk.yellow('Continue with this session? (Y/n): '), async (answer) => {
|
|
96
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
97
|
+
cleanup();
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
if (answer.toLowerCase() === 'n' || answer.toLowerCase() === 'no') {
|
|
101
|
+
console.log(chalk.gray('Starting new login...\n'));
|
|
102
|
+
const result = await performLogin();
|
|
103
|
+
resolve(result);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(chalk.green('Using existing session.\n'));
|
|
106
|
+
resolve({ success: true, ...existingSession });
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
reject(err);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// No existing session, start login flow
|
|
116
|
+
return await performLogin();
|
|
117
|
+
} catch (err) {
|
|
118
|
+
console.error(chalk.red('\n❌ Login failed:', err.message));
|
|
119
|
+
return { success: false, error: err.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Perform the actual login flow
|
|
125
|
+
*/
|
|
126
|
+
async function performLogin() {
|
|
127
|
+
const apiUrl = getApiUrl();
|
|
128
|
+
// Step 1: Request device code from backend
|
|
129
|
+
const spinner = ora('Requesting login code...').start();
|
|
130
|
+
|
|
131
|
+
const initResponse = await fetch(`${apiUrl}/cli/login/initiate`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!initResponse.ok) {
|
|
137
|
+
spinner.fail('Failed to request login code');
|
|
138
|
+
const errorData = await initResponse.json();
|
|
139
|
+
throw new Error(errorData.error || 'Failed to initiate login');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { deviceCode, userCode: _userCode, verificationUrl, expiresIn, interval } = await initResponse.json();
|
|
143
|
+
spinner.succeed('Login code generated');
|
|
144
|
+
|
|
145
|
+
// Step 2: Display instructions to user
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(chalk.cyan('╔════════════════════════════════════════╗'));
|
|
148
|
+
console.log(chalk.cyan('║') + chalk.white.bold(' Complete login in your browser ') + chalk.cyan('║'));
|
|
149
|
+
console.log(chalk.cyan('╚════════════════════════════════════════╝'));
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk.white('Opening browser to login page...'));
|
|
152
|
+
console.log(chalk.gray(`Code expires in ${Math.floor(expiresIn / 60)} minutes`));
|
|
153
|
+
console.log('');
|
|
154
|
+
|
|
155
|
+
// Step 3: Open browser to /connect page with device code
|
|
156
|
+
const opened = await openBrowser(verificationUrl);
|
|
157
|
+
if (!opened) {
|
|
158
|
+
console.log(chalk.yellow('⚠️ Could not open browser automatically.'));
|
|
159
|
+
console.log(chalk.white('Please open this URL manually: ') + chalk.blue(verificationUrl));
|
|
160
|
+
console.log('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Step 4: Poll for authorization
|
|
164
|
+
const pollSpinner = ora('Waiting for authorization...').start();
|
|
165
|
+
|
|
166
|
+
const pollInterval = (interval || 3) * 1000;
|
|
167
|
+
const maxAttempts = Math.floor(expiresIn / (interval || 3));
|
|
168
|
+
let attempts = 0;
|
|
169
|
+
let cancelled = false;
|
|
170
|
+
|
|
171
|
+
// Handle Ctrl+C during polling
|
|
172
|
+
const sigintHandler = () => {
|
|
173
|
+
cancelled = true;
|
|
174
|
+
pollSpinner.stop();
|
|
175
|
+
console.log(chalk.yellow('\n\n⚠️ Login cancelled\n'));
|
|
176
|
+
process.exit(0);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
process.on('SIGINT', sigintHandler);
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
while (attempts < maxAttempts && !cancelled) {
|
|
183
|
+
await sleep(pollInterval);
|
|
184
|
+
attempts++;
|
|
185
|
+
|
|
186
|
+
const pollResponse = await fetch(`${apiUrl}/cli/login/poll`, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
189
|
+
body: JSON.stringify({ deviceCode }),
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (pollResponse.status === 202) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!pollResponse.ok) {
|
|
197
|
+
pollSpinner.fail('Authorization failed');
|
|
198
|
+
const errorData = await pollResponse.json();
|
|
199
|
+
throw new Error(errorData.error || 'Authorization failed');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const result = await pollResponse.json();
|
|
203
|
+
|
|
204
|
+
if (result.status === 'authorized') {
|
|
205
|
+
pollSpinner.succeed(chalk.white('Authorization successful!'));
|
|
206
|
+
|
|
207
|
+
// Save session locally
|
|
208
|
+
saveSessionToken(result.token);
|
|
209
|
+
saveUserInfo(result.user);
|
|
210
|
+
|
|
211
|
+
console.log('');
|
|
212
|
+
console.log(chalk.gray(`User: ${result.user.email}`));
|
|
213
|
+
console.log(chalk.gray(`Session saved to: ~/.zibby/config.json\n`));
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
loggedIn: true,
|
|
218
|
+
user: result.user,
|
|
219
|
+
token: result.token
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (result.status === 'denied') {
|
|
224
|
+
pollSpinner.fail('Authorization denied');
|
|
225
|
+
throw new Error('User denied authorization');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
pollSpinner.fail('Login timeout');
|
|
230
|
+
throw new Error('Login timed out - please try again');
|
|
231
|
+
} finally {
|
|
232
|
+
process.removeListener('SIGINT', sigintHandler);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Logout - clear saved session
|
|
238
|
+
*/
|
|
239
|
+
export function logoutCli() {
|
|
240
|
+
const existingSession = checkExistingSession();
|
|
241
|
+
|
|
242
|
+
if (!existingSession.loggedIn) {
|
|
243
|
+
console.log(chalk.yellow('\n⚠️ Not logged in.\n'));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
clearSession();
|
|
248
|
+
console.log(chalk.green('✔') + chalk.white(' Logged out successfully!'));
|
|
249
|
+
console.log(chalk.gray('Session cleared from ~/.zibby/config.json\n'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Show current login status with detailed information
|
|
254
|
+
*/
|
|
255
|
+
export async function showLoginStatus(options = {}) {
|
|
256
|
+
const existingSession = checkExistingSession();
|
|
257
|
+
const envToken = process.env.ZIBBY_USER_TOKEN;
|
|
258
|
+
const apiUrl = getApiUrl();
|
|
259
|
+
const token = envToken || existingSession.token;
|
|
260
|
+
|
|
261
|
+
// JSON output for scripts
|
|
262
|
+
if (options.json) {
|
|
263
|
+
const status = {
|
|
264
|
+
authenticated: !!(existingSession.loggedIn || envToken),
|
|
265
|
+
user: existingSession.user || null,
|
|
266
|
+
tokenSource: envToken ? 'environment' : (existingSession.token ? 'session' : null),
|
|
267
|
+
tokenType: envToken ? 'PAT' : (existingSession.token ? 'JWT' : null),
|
|
268
|
+
apiUrl,
|
|
269
|
+
configPath: existingSession.loggedIn ? '~/.zibby/config.json' : null
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Verify token
|
|
273
|
+
if (token) {
|
|
274
|
+
try {
|
|
275
|
+
const fetch = (await import('node-fetch')).default;
|
|
276
|
+
const response = await fetch(`${apiUrl}/projects`, {
|
|
277
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
status.tokenValid = response.ok;
|
|
281
|
+
if (response.ok) {
|
|
282
|
+
const data = await response.json();
|
|
283
|
+
status.projectCount = (data.projects || []).length;
|
|
284
|
+
}
|
|
285
|
+
} catch (error) {
|
|
286
|
+
status.tokenValid = false;
|
|
287
|
+
status.error = error.message;
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
status.tokenValid = false;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
console.log(JSON.stringify(status, null, 2));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Human-readable output
|
|
298
|
+
console.log('');
|
|
299
|
+
|
|
300
|
+
if (!existingSession.loggedIn && !envToken) {
|
|
301
|
+
console.log(chalk.yellow('⚠️ Not authenticated'));
|
|
302
|
+
console.log('');
|
|
303
|
+
console.log(chalk.white('To authenticate:'));
|
|
304
|
+
console.log(chalk.gray(' Local: zibby login'));
|
|
305
|
+
console.log(chalk.gray(' CI/CD: Set ZIBBY_USER_TOKEN env variable\n'));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
console.log(chalk.green('✅ Authenticated'));
|
|
310
|
+
console.log('');
|
|
311
|
+
|
|
312
|
+
// User Details
|
|
313
|
+
console.log(chalk.bold.white('User Details:'));
|
|
314
|
+
if (existingSession.user) {
|
|
315
|
+
console.log(chalk.gray(` Email: ${existingSession.user.email}`));
|
|
316
|
+
if (existingSession.user.userId) {
|
|
317
|
+
console.log(chalk.gray(` User ID: ${existingSession.user.userId}`));
|
|
318
|
+
}
|
|
319
|
+
if (existingSession.user.name) {
|
|
320
|
+
console.log(chalk.gray(` Name: ${existingSession.user.name}`));
|
|
321
|
+
}
|
|
322
|
+
} else if (envToken) {
|
|
323
|
+
console.log(chalk.gray(' (User details not available with PAT token)'));
|
|
324
|
+
}
|
|
325
|
+
console.log('');
|
|
326
|
+
|
|
327
|
+
// Token Source
|
|
328
|
+
console.log(chalk.bold.white('Token Source:'));
|
|
329
|
+
if (envToken) {
|
|
330
|
+
console.log(chalk.gray(' Type: Personal Access Token (PAT)'));
|
|
331
|
+
console.log(chalk.gray(' Location: ZIBBY_USER_TOKEN environment variable'));
|
|
332
|
+
console.log(chalk.gray(` Preview: ${envToken.substring(0, 8)}••••`));
|
|
333
|
+
} else {
|
|
334
|
+
console.log(chalk.gray(' Type: Session Token (JWT)'));
|
|
335
|
+
console.log(chalk.gray(' Location: ~/.zibby/config.json'));
|
|
336
|
+
if (existingSession.token) {
|
|
337
|
+
console.log(chalk.gray(` Preview: ${existingSession.token.substring(0, 8)}••••`));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
console.log('');
|
|
341
|
+
|
|
342
|
+
// Verify token by fetching projects
|
|
343
|
+
if (token) {
|
|
344
|
+
try {
|
|
345
|
+
const oraSpinner = (await import('ora')).default;
|
|
346
|
+
const fetch = (await import('node-fetch')).default;
|
|
347
|
+
|
|
348
|
+
const spinner = oraSpinner('Verifying authentication...').start();
|
|
349
|
+
|
|
350
|
+
const response = await fetch(`${apiUrl}/projects`, {
|
|
351
|
+
headers: {
|
|
352
|
+
'Authorization': `Bearer ${token}`,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (response.ok) {
|
|
357
|
+
const data = await response.json();
|
|
358
|
+
const projectCount = (data.projects || []).length;
|
|
359
|
+
spinner.succeed(chalk.white('Token verified'));
|
|
360
|
+
console.log(chalk.gray(` Projects: ${projectCount} accessible`));
|
|
361
|
+
} else {
|
|
362
|
+
spinner.fail(chalk.white('Token verification failed'));
|
|
363
|
+
console.log(chalk.yellow(` Status: Invalid or expired (HTTP ${response.status})`));
|
|
364
|
+
}
|
|
365
|
+
} catch (error) {
|
|
366
|
+
console.log(chalk.yellow(` Status: Could not verify (${error.message})`));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
console.log('');
|
|
371
|
+
console.log(chalk.gray('💡 Run \'zibby list\' to see your projects'));
|
|
372
|
+
console.log('');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Validate current session token
|
|
377
|
+
*/
|
|
378
|
+
export async function validateSession() {
|
|
379
|
+
const token = getSessionToken();
|
|
380
|
+
if (!token) {
|
|
381
|
+
return { valid: false };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const apiUrl = getApiUrl();
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
// Try to make an authenticated request to verify token
|
|
388
|
+
const response = await fetch(`${apiUrl}/projects`, {
|
|
389
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
if (response.ok) {
|
|
393
|
+
return { valid: true };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Token invalid or expired, clear it
|
|
397
|
+
clearSession();
|
|
398
|
+
return { valid: false };
|
|
399
|
+
} catch {
|
|
400
|
+
return { valid: false };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function sleep(ms) {
|
|
405
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
406
|
+
}
|