@zibby/cli 0.1.26 → 0.1.28

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 (46) hide show
  1. package/README.md +44 -44
  2. package/dist/auth/cli-login.js +18 -0
  3. package/dist/bin/zibby.js +2 -0
  4. package/dist/commands/agent-reliability.js +8 -0
  5. package/dist/commands/analyze-graph.js +18 -0
  6. package/dist/commands/chat-session-store.js +1 -0
  7. package/dist/commands/chat.js +79 -0
  8. package/dist/commands/ci-setup.js +18 -0
  9. package/dist/commands/generate.js +69 -0
  10. package/dist/commands/implement.js +65 -0
  11. package/dist/commands/init.js +344 -0
  12. package/dist/commands/list-projects.js +11 -0
  13. package/dist/commands/memory.js +39 -0
  14. package/dist/commands/run-capacity-queue-cli.js +4 -0
  15. package/dist/commands/run.js +112 -0
  16. package/dist/commands/setup-scripts.js +15 -0
  17. package/dist/commands/studio.js +33 -0
  18. package/dist/commands/upload.js +22 -0
  19. package/dist/commands/video.js +6 -0
  20. package/dist/commands/workflow.js +45 -0
  21. package/dist/config/config.js +1 -0
  22. package/dist/config/environments.js +1 -0
  23. package/dist/utils/chat-run-lifecycle.js +3 -0
  24. package/dist/utils/execution-context.js +1 -0
  25. package/dist/utils/progress-reporter.js +1 -0
  26. package/dist/utils/studio-cli-log-mirror.js +1 -0
  27. package/dist/utils/studio-installer.js +7 -0
  28. package/dist/utils/studio-launcher.js +1 -0
  29. package/package.json +20 -16
  30. package/bin/zibby.js +0 -273
  31. package/src/auth/cli-login.js +0 -404
  32. package/src/commands/analyze-graph.js +0 -336
  33. package/src/commands/ci-setup.js +0 -65
  34. package/src/commands/implement.js +0 -664
  35. package/src/commands/init.js +0 -770
  36. package/src/commands/list-projects.js +0 -77
  37. package/src/commands/memory.js +0 -171
  38. package/src/commands/run.js +0 -919
  39. package/src/commands/setup-scripts.js +0 -114
  40. package/src/commands/upload.js +0 -162
  41. package/src/commands/video.js +0 -30
  42. package/src/commands/workflow.js +0 -368
  43. package/src/config/config.js +0 -117
  44. package/src/config/environments.js +0 -145
  45. package/src/utils/execution-context.js +0 -25
  46. package/src/utils/progress-reporter.js +0 -155
package/bin/zibby.js DELETED
@@ -1,273 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // Prevent EPIPE errors from crashing when stdout/stderr is closed
4
- process.stdout.on('error', (err) => {
5
- if (err.code === 'EPIPE') {
6
- // Suppress EPIPE (broken pipe) errors
7
- }
8
- });
9
- process.stderr.on('error', (err) => {
10
- if (err.code === 'EPIPE') {
11
- // Suppress EPIPE (broken pipe) errors
12
- }
13
- });
14
-
15
- // Suppress dotenv promotional messages
16
- process.env.DOTENV_CONFIG_QUIET = 'true';
17
-
18
- import '@zibby/skills';
19
- import { Command } from 'commander';
20
- import { initCommand } from '../src/commands/init.js';
21
- import { runCommand } from '../src/commands/run.js';
22
- import { videoCommand } from '../src/commands/video.js';
23
- import { uploadCommand } from '../src/commands/upload.js';
24
- import { ciSetupCommand } from '../src/commands/ci-setup.js';
25
- // implement and analyze are lazy-loaded (they pull in @aws-sdk/client-s3 which isn't needed for local commands)
26
- import { setupPlaywrightMcpCommand, setupCiCommand, testWithVideoCommand } from '../src/commands/setup-scripts.js';
27
- import { readFileSync } from 'fs';
28
- import { fileURLToPath } from 'url';
29
- import { dirname, join } from 'path';
30
-
31
- const __filename = fileURLToPath(import.meta.url);
32
- const __dirname = dirname(__filename);
33
- const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
34
-
35
- const program = new Command();
36
-
37
- program
38
- .name('zibby')
39
- .description('Zibby Test Automation - AI-powered test generation')
40
- .version(packageJson.version);
41
-
42
- program
43
- .command('init')
44
- .description('Initialize a new Zibby test project (like rails new)')
45
- .argument('[project-name]', 'Project name (optional, uses current directory if not provided)')
46
- .option('--agent <type>', 'Agent to use (claude, cursor)')
47
- .option('--skip-install', 'Skip npm install')
48
- .option('-f, --force', 'Force reinitialize (overwrite existing config)')
49
- .option('--headed', 'Run MCP browser in headed mode (visible browser)')
50
- .option('--headless', 'Run MCP browser in headless mode (hidden browser)')
51
- .option('--api-key <key>', 'Zibby API key for cloud sync')
52
- .option('--cloud-sync', 'Enable cloud sync and install Zibby MCP')
53
- .option('-m, --mem', 'Enable test memory (initializes Dolt DB and includes memory skill)')
54
- .action(initCommand);
55
-
56
- program
57
- .command('run')
58
- .description('Run a test specification')
59
- .argument('[spec-path]', 'Path to test spec file or inline test description in quotes')
60
- .option('--sources <ids>', 'Comma-separated test case IDs to fetch from cloud')
61
- .option('--execution <id>', 'Execution ID containing the test cases (required with --sources)')
62
- .option('--agent <type>', 'Agent to use (claude, cursor) - overrides config')
63
- .option('--workflow <name>', 'Workflow to use (e.g., QuickSmokeWorkflow, quick-smoke)')
64
- .option('--headless', 'Run browser in headless mode')
65
- .option('--node <name>', 'Run only a specific node (e.g., execute_live, generate_script)')
66
- .option('--session <id>', 'Use existing session (e.g., 1768974629717 or "last") - requires --node')
67
- .option('--project <id>', 'Project ID (optional, auto-detected from ZIBBY_API_KEY)')
68
- .option('--collection <id-or-name>', 'Collection ID or name (creates new if name doesn\'t exist)')
69
- .option('--folder <path>', 'Folder path within collection (optional, requires --collection)')
70
- .option('--sync', 'Force upload to cloud (overrides cloudSync: false)')
71
- .option('--no-sync', 'Skip upload to cloud (overrides cloudSync: true)')
72
- .option('--config <path>', 'Path to config file', '.zibby.config.mjs')
73
- .option('--auto-approve', 'Auto-approve MCP tools (for CI/CD)')
74
- .option('-o, --open', 'Open test results in browser after completion')
75
- .option('--verbose', 'Show info level logs')
76
- .option('--debug', 'Show debug level logs (most verbose)')
77
- .option('-m, --mem', 'Enable test memory (Dolt-backed knowledge from previous runs)')
78
- .action((specPath, options) => {
79
- if (options.debug) {
80
- process.env.ZIBBY_DEBUG = 'true';
81
- } else if (options.verbose) {
82
- process.env.ZIBBY_VERBOSE = 'true';
83
- }
84
- // --mem flag is handled by init command when generating files
85
- return runCommand(specPath, options);
86
- });
87
-
88
- program
89
- .command('implement')
90
- .description('Implement a Jira ticket using AI agent (runs in ECS container)')
91
- .action(async (...args) => {
92
- const { implementCommand } = await import('../src/commands/implement.js');
93
- return implementCommand(...args);
94
- });
95
-
96
- program
97
- .command('analyze')
98
- .description('Analyze a Jira ticket against the codebase (runs in ECS container)')
99
- .option('--workflow <path>', 'Path to a local workflow JSON file (e.g., .zibby/workflow-analysis.json)')
100
- .action(async (...args) => {
101
- const { analyzeCommand } = await import('../src/commands/analyze-graph.js');
102
- return analyzeCommand(...args);
103
- });
104
-
105
- program
106
- .command('video')
107
- .description('Organize test videos next to test files')
108
- .action(videoCommand);
109
-
110
- program
111
- .command('upload <spec-path>')
112
- .description('Upload existing test artifacts to Zibby Cloud')
113
- .option('--project <id>', 'Project ID (REQUIRED - use flag or ZIBBY_PROJECT_ID env)')
114
- .option('--collection <id-or-name>', 'Collection ID or name (creates new if name doesn\'t exist)')
115
- .option('--folder <path>', 'Folder path within collection (optional)')
116
- .option('--agent <type>', 'Agent used (for metadata)')
117
- .action(uploadCommand);
118
-
119
- program
120
- .command('login')
121
- .description('Log in to Zibby (opens browser for authentication)')
122
- .action(async () => {
123
- const { loginCli } = await import('../src/auth/cli-login.js');
124
- await loginCli();
125
- process.exit(0);
126
- });
127
-
128
- program
129
- .command('logout')
130
- .description('Log out from Zibby (clears saved session)')
131
- .action(async () => {
132
- const { logoutCli } = await import('../src/auth/cli-login.js');
133
- logoutCli();
134
- process.exit(0);
135
- });
136
-
137
- program
138
- .command('status')
139
- .description('Show current authentication status and token details')
140
- .option('--json', 'Output in JSON format (for scripts)')
141
- .action(async (options) => {
142
- const { showLoginStatus } = await import('../src/auth/cli-login.js');
143
- await showLoginStatus(options);
144
- process.exit(0);
145
- });
146
-
147
- program
148
- .command('list')
149
- .description('List your projects and API tokens')
150
- .action(async () => {
151
- const { listProjectsCommand } = await import('../src/commands/list-projects.js');
152
- await listProjectsCommand();
153
- });
154
-
155
- program
156
- .command('ci-setup')
157
- .description('Setup Cursor Agent for CI/CD (patch and configure)')
158
- .option('--get-keys', 'Get MCP approval keys')
159
- .option('--save', 'Save approval keys to project')
160
- .action(ciSetupCommand);
161
-
162
- program
163
- .command('setup-playwright')
164
- .description('Setup official Playwright MCP (from cursor-agent-package)')
165
- .option('--headed', 'Configure MCP in headed mode (visible browser)')
166
- .option('--viewport-width <width>', 'Viewport width (default: 1280)', '1280')
167
- .option('--viewport-height <height>', 'Viewport height (default: 720)', '720')
168
- .action((options) => {
169
- const viewport = {
170
- width: parseInt(options.viewportWidth, 10) || 1280,
171
- height: parseInt(options.viewportHeight, 10) || 720
172
- };
173
- return setupPlaywrightMcpCommand({ ...options, viewport });
174
- });
175
-
176
- program
177
- .command('setup-ci-full')
178
- .description('Complete CI/CD setup from scratch')
179
- .action(setupCiCommand);
180
-
181
- program
182
- .command('test-video')
183
- .description('Run Playwright tests with video recording')
184
- .argument('[test-file]', 'Test file to run (default: tests/)')
185
- .option('--headed', 'Run in headed mode (visible browser)')
186
- .action(testWithVideoCommand);
187
-
188
- // ============================================
189
- // MEMORY COMMANDS (test knowledge database)
190
- // ============================================
191
- const memory = program
192
- .command('memory')
193
- .description('Test memory database — version-controlled knowledge from runs');
194
-
195
- memory
196
- .command('stats')
197
- .description('Show memory database statistics')
198
- .action(async () => {
199
- const { memoryStatsCommand } = await import('../src/commands/memory.js');
200
- return memoryStatsCommand();
201
- });
202
-
203
- memory
204
- .command('init')
205
- .description('Initialize the memory database (Dolt)')
206
- .action(async () => {
207
- const { memoryInitCommand } = await import('../src/commands/memory.js');
208
- return memoryInitCommand();
209
- });
210
-
211
- memory
212
- .command('compact')
213
- .description('Prune old data and run Dolt GC to reclaim storage')
214
- .option('--max-runs <n>', 'Keep last N runs per spec (default: 50)', parseInt)
215
- .option('--max-age <days>', 'Remove data older than N days (default: 90)', parseInt)
216
- .action(async (options) => {
217
- const { memoryCompactCommand } = await import('../src/commands/memory.js');
218
- return memoryCompactCommand(options);
219
- });
220
-
221
- memory
222
- .command('reset')
223
- .description('Wipe the memory database')
224
- .option('-f, --force', 'Confirm reset')
225
- .action(async (options) => {
226
- const { memoryResetCommand } = await import('../src/commands/memory.js');
227
- return memoryResetCommand(options);
228
- });
229
-
230
- // ============================================
231
- // WORKFLOW COMMANDS (download / upload / list)
232
- // ============================================
233
- const workflow = program
234
- .command('workflow')
235
- .description('Manage workflow graphs (download, upload, list)');
236
-
237
- workflow
238
- .command('download')
239
- .description('Download a workflow graph from Zibby Cloud to .zibby/')
240
- .option('--project <id>', 'Project ID (or ZIBBY_PROJECT_ID env)')
241
- .option('--type <type>', 'Workflow type: analysis, implementation, run_test')
242
- .option('--api-key <key>', 'API key (or ZIBBY_API_KEY env)')
243
- .option('--output <dir>', 'Output directory (default: .zibby/)')
244
- .option('--include-default', 'Download the built-in default graph if no custom one exists')
245
- .action(async (options) => {
246
- const { workflowDownloadCommand } = await import('../src/commands/workflow.js');
247
- return workflowDownloadCommand(options);
248
- });
249
-
250
- workflow
251
- .command('upload')
252
- .description('Upload a local workflow graph to Zibby Cloud (validates before upload)')
253
- .option('--project <id>', 'Project ID (or ZIBBY_PROJECT_ID env)')
254
- .option('--type <type>', 'Workflow type: analysis, implementation, run_test')
255
- .option('--api-key <key>', 'API key (or ZIBBY_API_KEY env)')
256
- .option('--file <path>', 'Path to workflow JSON (default: .zibby/workflow-<type>.json)')
257
- .action(async (options) => {
258
- const { workflowUploadCommand } = await import('../src/commands/workflow.js');
259
- return workflowUploadCommand(options);
260
- });
261
-
262
- workflow
263
- .command('list')
264
- .description('List all workflows for a project')
265
- .option('--project <id>', 'Project ID (or ZIBBY_PROJECT_ID env)')
266
- .option('--api-key <key>', 'API key (or ZIBBY_API_KEY env)')
267
- .action(async (options) => {
268
- const { workflowListCommand } = await import('../src/commands/workflow.js');
269
- return workflowListCommand(options);
270
- });
271
-
272
- program.parse();
273
-
@@ -1,404 +0,0 @@
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 response = await fetch(`${apiUrl}/projects`, {
276
- headers: { 'Authorization': `Bearer ${token}` }
277
- });
278
-
279
- status.tokenValid = response.ok;
280
- if (response.ok) {
281
- const data = await response.json();
282
- status.projectCount = (data.projects || []).length;
283
- }
284
- } catch (error) {
285
- status.tokenValid = false;
286
- status.error = error.message;
287
- }
288
- } else {
289
- status.tokenValid = false;
290
- }
291
-
292
- console.log(JSON.stringify(status, null, 2));
293
- return;
294
- }
295
-
296
- // Human-readable output
297
- console.log('');
298
-
299
- if (!existingSession.loggedIn && !envToken) {
300
- console.log(chalk.yellow('⚠️ Not authenticated'));
301
- console.log('');
302
- console.log(chalk.white('To authenticate:'));
303
- console.log(chalk.gray(' Local: zibby login'));
304
- console.log(chalk.gray(' CI/CD: Set ZIBBY_USER_TOKEN env variable\n'));
305
- return;
306
- }
307
-
308
- console.log(chalk.green('✅ Authenticated'));
309
- console.log('');
310
-
311
- // User Details
312
- console.log(chalk.bold.white('User Details:'));
313
- if (existingSession.user) {
314
- console.log(chalk.gray(` Email: ${existingSession.user.email}`));
315
- if (existingSession.user.userId) {
316
- console.log(chalk.gray(` User ID: ${existingSession.user.userId}`));
317
- }
318
- if (existingSession.user.name) {
319
- console.log(chalk.gray(` Name: ${existingSession.user.name}`));
320
- }
321
- } else if (envToken) {
322
- console.log(chalk.gray(' (User details not available with PAT token)'));
323
- }
324
- console.log('');
325
-
326
- // Token Source
327
- console.log(chalk.bold.white('Token Source:'));
328
- if (envToken) {
329
- console.log(chalk.gray(' Type: Personal Access Token (PAT)'));
330
- console.log(chalk.gray(' Location: ZIBBY_USER_TOKEN environment variable'));
331
- console.log(chalk.gray(` Preview: ${envToken.substring(0, 8)}••••`));
332
- } else {
333
- console.log(chalk.gray(' Type: Session Token (JWT)'));
334
- console.log(chalk.gray(' Location: ~/.zibby/config.json'));
335
- if (existingSession.token) {
336
- console.log(chalk.gray(` Preview: ${existingSession.token.substring(0, 8)}••••`));
337
- }
338
- }
339
- console.log('');
340
-
341
- // Verify token by fetching projects
342
- if (token) {
343
- try {
344
- const oraSpinner = (await import('ora')).default;
345
-
346
- const spinner = oraSpinner('Verifying authentication...').start();
347
-
348
- const response = await fetch(`${apiUrl}/projects`, {
349
- headers: {
350
- 'Authorization': `Bearer ${token}`,
351
- },
352
- });
353
-
354
- if (response.ok) {
355
- const data = await response.json();
356
- const projectCount = (data.projects || []).length;
357
- spinner.succeed(chalk.white('Token verified'));
358
- console.log(chalk.gray(` Projects: ${projectCount} accessible`));
359
- } else {
360
- spinner.fail(chalk.white('Token verification failed'));
361
- console.log(chalk.yellow(` Status: Invalid or expired (HTTP ${response.status})`));
362
- }
363
- } catch (error) {
364
- console.log(chalk.yellow(` Status: Could not verify (${error.message})`));
365
- }
366
- }
367
-
368
- console.log('');
369
- console.log(chalk.gray('💡 Run \'zibby list\' to see your projects'));
370
- console.log('');
371
- }
372
-
373
- /**
374
- * Validate current session token
375
- */
376
- export async function validateSession() {
377
- const token = getSessionToken();
378
- if (!token) {
379
- return { valid: false };
380
- }
381
-
382
- const apiUrl = getApiUrl();
383
-
384
- try {
385
- // Try to make an authenticated request to verify token
386
- const response = await fetch(`${apiUrl}/projects`, {
387
- headers: { Authorization: `Bearer ${token}` },
388
- });
389
-
390
- if (response.ok) {
391
- return { valid: true };
392
- }
393
-
394
- // Token invalid or expired, clear it
395
- clearSession();
396
- return { valid: false };
397
- } catch {
398
- return { valid: false };
399
- }
400
- }
401
-
402
- function sleep(ms) {
403
- return new Promise(resolve => setTimeout(resolve, ms));
404
- }