@eve-horizon/cli 0.2.0 → 0.2.1

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.
@@ -18,11 +18,26 @@ async function handleAdmin(subcommand, positionals, flags, context) {
18
18
  if (!['owner', 'admin', 'member'].includes(role)) {
19
19
  throw new Error(`Invalid role: ${role}. Must be one of: owner, admin, member`);
20
20
  }
21
+ if (!orgId) {
22
+ throw new Error('No org specified. Use --org <org_id> or set a default org in your profile.');
23
+ }
21
24
  const results = {
22
25
  keys_registered: 0,
23
26
  identities: [],
24
27
  };
28
+ // Add user to org first - this creates the user if they don't exist
29
+ if (orgId) {
30
+ const membership = await (0, client_1.requestJson)(context, `/orgs/${orgId}/members`, {
31
+ method: 'POST',
32
+ body: {
33
+ email,
34
+ role,
35
+ },
36
+ });
37
+ results.membership = membership;
38
+ }
25
39
  // Fetch and register GitHub SSH keys if username provided
40
+ // Now that user exists (created via org membership), identity registration will work
26
41
  if (githubUsername) {
27
42
  const keys = await fetchGitHubKeys(githubUsername);
28
43
  if (keys.length === 0) {
@@ -41,24 +56,6 @@ async function handleAdmin(subcommand, positionals, flags, context) {
41
56
  results.keys_registered += 1;
42
57
  }
43
58
  }
44
- // Add user to org if org_id provided
45
- if (orgId) {
46
- // Get user_id from the first registered identity, or look up by email
47
- let userId;
48
- if (results.identities.length > 0) {
49
- userId = results.identities[0].user_id;
50
- }
51
- if (userId) {
52
- const membership = await (0, client_1.requestJson)(context, `/orgs/${orgId}/members`, {
53
- method: 'POST',
54
- body: {
55
- user_id: userId,
56
- role,
57
- },
58
- });
59
- results.membership = membership;
60
- }
61
- }
62
59
  const summary = [
63
60
  `Invited ${email}`,
64
61
  results.keys_registered > 0 ? `${results.keys_registered} SSH key(s) registered` : null,
@@ -275,11 +275,26 @@ async function handleAuth(subcommand, flags, context, credentials) {
275
275
  const claudeOnly = (0, args_1.getBooleanFlag)(flags, ['claude']) ?? false;
276
276
  const codexOnly = (0, args_1.getBooleanFlag)(flags, ['codex']) ?? false;
277
277
  const dryRun = (0, args_1.getBooleanFlag)(flags, ['dry-run']) ?? false;
278
- const system = (0, args_1.getBooleanFlag)(flags, ['system']) ?? false;
278
+ const orgIdFlag = (0, args_1.getStringFlag)(flags, ['org']);
279
279
  const projectIdFlag = (0, args_1.getStringFlag)(flags, ['project']);
280
- const projectId = projectIdFlag ?? context.projectId;
281
- if (!system && !projectId) {
282
- throw new Error('No project specified. Use --project <id> or set default in profile, or use --system for system secrets');
280
+ let scope;
281
+ if (projectIdFlag) {
282
+ scope = { type: 'project', projectId: projectIdFlag };
283
+ }
284
+ else if (orgIdFlag) {
285
+ scope = { type: 'org', orgId: orgIdFlag };
286
+ }
287
+ else {
288
+ // Default to user scope - need to fetch user ID
289
+ const meResponse = await (0, client_1.requestRaw)(context, '/auth/me', { allowError: true });
290
+ if (!meResponse.ok) {
291
+ throw new Error('Not authenticated. Run "eve auth login" first.');
292
+ }
293
+ const meData = meResponse.data;
294
+ if (!meData.user_id) {
295
+ throw new Error('Could not determine user ID. Try specifying --org or --project instead.');
296
+ }
297
+ scope = { type: 'user', userId: meData.user_id };
283
298
  }
284
299
  const extractClaude = !codexOnly; // Extract Claude unless --codex is specified
285
300
  const extractCodex = !claudeOnly; // Extract Codex unless --claude is specified
@@ -287,31 +302,42 @@ async function handleAuth(subcommand, flags, context, credentials) {
287
302
  const platform = process.platform;
288
303
  // Extract Claude OAuth tokens
289
304
  if (extractClaude) {
305
+ // Try macOS Keychain first
290
306
  if (platform === 'darwin') {
291
- try {
292
- const output = (0, node_child_process_1.execSync)('security find-generic-password -s "anthropic.claude" -w', { encoding: 'utf8' }).trim();
293
- if (output) {
294
- extractedTokens.CLAUDE_OAUTH_TOKEN = output;
307
+ for (const service of ['Claude Code-credentials', 'anthropic.claude']) {
308
+ try {
309
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
310
+ if (output) {
311
+ extractedTokens.CLAUDE_CODE_OAUTH_TOKEN = output;
312
+ break;
313
+ }
314
+ }
315
+ catch {
316
+ // Token not found in keychain, continue
295
317
  }
296
- }
297
- catch (error) {
298
- // Token not found in keychain, continue
299
318
  }
300
319
  }
301
- else {
302
- // Linux: Check various credential file locations
320
+ // Check credential files (all platforms)
321
+ if (!extractedTokens.CLAUDE_CODE_OAUTH_TOKEN) {
303
322
  const credentialPaths = [
304
323
  `${(0, node_os_1.homedir)()}/.claude/.credentials.json`,
305
324
  `${(0, node_os_1.homedir)()}/.claude/credentials.json`,
306
325
  `${(0, node_os_1.homedir)()}/.config/claude/credentials.json`,
307
326
  ];
308
- for (const path of credentialPaths) {
309
- if ((0, node_fs_1.existsSync)(path)) {
327
+ for (const credPath of credentialPaths) {
328
+ if ((0, node_fs_1.existsSync)(credPath)) {
310
329
  try {
311
- const content = (0, node_fs_1.readFileSync)(path, 'utf8');
330
+ const content = (0, node_fs_1.readFileSync)(credPath, 'utf8');
312
331
  const creds = JSON.parse(content);
332
+ // Handle nested claudeAiOauth format (current Claude Code format)
333
+ const claudeOauth = creds.claudeAiOauth;
334
+ if (claudeOauth?.accessToken) {
335
+ extractedTokens.CLAUDE_CODE_OAUTH_TOKEN = claudeOauth.accessToken;
336
+ break;
337
+ }
338
+ // Fallback to legacy root-level tokens
313
339
  if (creds.oauth_token || creds.access_token) {
314
- extractedTokens.CLAUDE_OAUTH_TOKEN = creds.oauth_token || creds.access_token;
340
+ extractedTokens.CLAUDE_CODE_OAUTH_TOKEN = (creds.oauth_token || creds.access_token);
315
341
  break;
316
342
  }
317
343
  }
@@ -322,48 +348,78 @@ async function handleAuth(subcommand, flags, context, credentials) {
322
348
  }
323
349
  }
324
350
  }
325
- // Extract Codex OAuth tokens
351
+ // Extract Codex/Code OAuth tokens
326
352
  if (extractCodex) {
353
+ // Try macOS Keychain first
327
354
  if (platform === 'darwin') {
328
- try {
329
- const output = (0, node_child_process_1.execSync)('security find-generic-password -s "openai.codex" -w', { encoding: 'utf8' }).trim();
330
- if (output) {
331
- extractedTokens.CODEX_OAUTH_TOKEN = output;
355
+ for (const service of ['openai.codex', 'Code-credentials']) {
356
+ try {
357
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
358
+ if (output) {
359
+ extractedTokens.CODEX_OAUTH_ACCESS_TOKEN = output;
360
+ break;
361
+ }
362
+ }
363
+ catch {
364
+ // Token not found in keychain, continue
332
365
  }
333
- }
334
- catch (error) {
335
- // Token not found in keychain, continue
336
366
  }
337
367
  }
338
- // Check ~/.codex/auth.json for Codex tokens (all platforms)
339
- const codexAuthPath = `${(0, node_os_1.homedir)()}/.codex/auth.json`;
340
- if ((0, node_fs_1.existsSync)(codexAuthPath)) {
341
- try {
342
- const content = (0, node_fs_1.readFileSync)(codexAuthPath, 'utf8');
343
- const auth = JSON.parse(content);
344
- if (auth.oauth_token || auth.access_token) {
345
- extractedTokens.CODEX_OAUTH_TOKEN = auth.oauth_token || auth.access_token;
368
+ // Check auth files (all platforms): ~/.codex/auth.json and ~/.code/auth.json
369
+ if (!extractedTokens.CODEX_OAUTH_ACCESS_TOKEN) {
370
+ const codexAuthPaths = [
371
+ `${(0, node_os_1.homedir)()}/.codex/auth.json`,
372
+ `${(0, node_os_1.homedir)()}/.code/auth.json`,
373
+ ];
374
+ for (const authPath of codexAuthPaths) {
375
+ if ((0, node_fs_1.existsSync)(authPath)) {
376
+ try {
377
+ const content = (0, node_fs_1.readFileSync)(authPath, 'utf8');
378
+ const auth = JSON.parse(content);
379
+ // Handle nested tokens format (current Codex CLI format)
380
+ const tokens = auth.tokens;
381
+ if (tokens?.access_token) {
382
+ extractedTokens.CODEX_OAUTH_ACCESS_TOKEN = tokens.access_token;
383
+ break;
384
+ }
385
+ // Fallback to root-level tokens
386
+ if (auth.oauth_token || auth.access_token) {
387
+ extractedTokens.CODEX_OAUTH_ACCESS_TOKEN = (auth.oauth_token || auth.access_token);
388
+ break;
389
+ }
390
+ // Also check for OPENAI_API_KEY in the auth file
391
+ if (auth.OPENAI_API_KEY && typeof auth.OPENAI_API_KEY === 'string') {
392
+ extractedTokens.OPENAI_API_KEY = auth.OPENAI_API_KEY;
393
+ break;
394
+ }
395
+ }
396
+ catch {
397
+ // Failed to parse, continue
398
+ }
346
399
  }
347
400
  }
348
- catch {
349
- // Failed to parse, continue
350
- }
351
401
  }
352
402
  }
353
403
  if (Object.keys(extractedTokens).length === 0) {
354
404
  (0, output_1.outputJson)({ extracted: 0, tokens: [] }, json, 'No tokens found on host machine');
355
405
  return;
356
406
  }
407
+ // Build target label for output
408
+ const targetLabel = scope.type === 'user' ? 'user' :
409
+ scope.type === 'org' ? `org ${scope.orgId}` :
410
+ `project ${scope.projectId}`;
357
411
  if (dryRun) {
358
412
  const tokenList = Object.keys(extractedTokens).map(key => ({
359
413
  name: key,
360
414
  value: `${extractedTokens[key].substring(0, 10)}...`,
361
415
  }));
362
- (0, output_1.outputJson)({ dry_run: true, would_set: tokenList, target: system ? 'system' : `project ${projectId}` }, json, `Would set ${tokenList.length} token(s) on ${system ? 'system' : `project ${projectId}`}:\n${tokenList.map(t => ` - ${t.name}`).join('\n')}`);
416
+ (0, output_1.outputJson)({ dry_run: true, would_set: tokenList, target: targetLabel, scope }, json, `Would set ${tokenList.length} token(s) on ${targetLabel}:\n${tokenList.map(t => ` - ${t.name}`).join('\n')}`);
363
417
  return;
364
418
  }
365
- // Set secrets via API
366
- const endpoint = system ? '/system/secrets' : `/projects/${projectId}/secrets`;
419
+ // Set secrets via API - endpoint depends on scope
420
+ const endpoint = scope.type === 'user' ? `/users/${scope.userId}/secrets` :
421
+ scope.type === 'org' ? `/orgs/${scope.orgId}/secrets` :
422
+ `/projects/${scope.projectId}/secrets`;
367
423
  const results = [];
368
424
  for (const [name, value] of Object.entries(extractedTokens)) {
369
425
  try {
@@ -387,15 +443,194 @@ async function handleAuth(subcommand, flags, context, credentials) {
387
443
  const successCount = results.filter(r => r.success).length;
388
444
  const failCount = results.filter(r => !r.success).length;
389
445
  (0, output_1.outputJson)({
390
- target: system ? 'system' : `project ${projectId}`,
446
+ target: targetLabel,
447
+ scope,
391
448
  results,
392
449
  success: successCount,
393
450
  failed: failCount,
394
- }, json, `✓ Set ${successCount} secret(s) on ${system ? 'system' : `project ${projectId}`}${failCount > 0 ? ` (${failCount} failed)` : ''}`);
451
+ }, json, `✓ Set ${successCount} secret(s) on ${targetLabel}${failCount > 0 ? ` (${failCount} failed)` : ''}`);
452
+ return;
453
+ }
454
+ case 'creds': {
455
+ // Show local credential status without syncing
456
+ const claudeOnly = (0, args_1.getBooleanFlag)(flags, ['claude']) ?? false;
457
+ const codexOnly = (0, args_1.getBooleanFlag)(flags, ['codex']) ?? false;
458
+ const checkClaude = !codexOnly;
459
+ const checkCodex = !claudeOnly;
460
+ const credentials = [];
461
+ const plat = process.platform;
462
+ // Check Claude credentials
463
+ if (checkClaude) {
464
+ let claudeFound = false;
465
+ let claudeSource = '';
466
+ let claudePreview = '';
467
+ let claudeExpires;
468
+ // Try macOS Keychain
469
+ if (plat === 'darwin' && !claudeFound) {
470
+ for (const service of ['Claude Code-credentials', 'anthropic.claude']) {
471
+ try {
472
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
473
+ if (output) {
474
+ claudeFound = true;
475
+ claudeSource = `macOS Keychain (${service})`;
476
+ claudePreview = output.substring(0, 15) + '...';
477
+ break;
478
+ }
479
+ }
480
+ catch {
481
+ // Not found
482
+ }
483
+ }
484
+ }
485
+ // Check credential files
486
+ if (!claudeFound) {
487
+ const credentialPaths = [
488
+ `${(0, node_os_1.homedir)()}/.claude/.credentials.json`,
489
+ `${(0, node_os_1.homedir)()}/.claude/credentials.json`,
490
+ `${(0, node_os_1.homedir)()}/.config/claude/credentials.json`,
491
+ ];
492
+ for (const credPath of credentialPaths) {
493
+ if ((0, node_fs_1.existsSync)(credPath)) {
494
+ try {
495
+ const content = (0, node_fs_1.readFileSync)(credPath, 'utf8');
496
+ const creds = JSON.parse(content);
497
+ const claudeOauth = creds.claudeAiOauth;
498
+ if (claudeOauth?.accessToken) {
499
+ claudeFound = true;
500
+ claudeSource = credPath.replace((0, node_os_1.homedir)(), '~');
501
+ claudePreview = claudeOauth.accessToken.substring(0, 15) + '...';
502
+ if (claudeOauth.expiresAt) {
503
+ const expDate = new Date(claudeOauth.expiresAt);
504
+ claudeExpires = expDate.toISOString();
505
+ }
506
+ break;
507
+ }
508
+ if (creds.oauth_token || creds.access_token) {
509
+ claudeFound = true;
510
+ claudeSource = credPath.replace((0, node_os_1.homedir)(), '~');
511
+ const token = (creds.oauth_token || creds.access_token);
512
+ claudePreview = token.substring(0, 15) + '...';
513
+ break;
514
+ }
515
+ }
516
+ catch {
517
+ // Failed to parse
518
+ }
519
+ }
520
+ }
521
+ }
522
+ credentials.push({
523
+ name: 'Claude Code OAuth',
524
+ source: claudeFound ? claudeSource : 'not found',
525
+ found: claudeFound,
526
+ preview: claudePreview || undefined,
527
+ expiresAt: claudeExpires,
528
+ });
529
+ }
530
+ // Check Codex credentials
531
+ if (checkCodex) {
532
+ let codexFound = false;
533
+ let codexSource = '';
534
+ let codexPreview = '';
535
+ // Try macOS Keychain
536
+ if (plat === 'darwin' && !codexFound) {
537
+ for (const service of ['openai.codex', 'Code-credentials']) {
538
+ try {
539
+ const output = (0, node_child_process_1.execSync)(`security find-generic-password -s "${service}" -w`, { encoding: 'utf8' }).trim();
540
+ if (output) {
541
+ codexFound = true;
542
+ codexSource = `macOS Keychain (${service})`;
543
+ codexPreview = output.substring(0, 15) + '...';
544
+ break;
545
+ }
546
+ }
547
+ catch {
548
+ // Not found
549
+ }
550
+ }
551
+ }
552
+ // Check auth files
553
+ if (!codexFound) {
554
+ const codexAuthPaths = [
555
+ `${(0, node_os_1.homedir)()}/.codex/auth.json`,
556
+ `${(0, node_os_1.homedir)()}/.code/auth.json`,
557
+ ];
558
+ for (const authPath of codexAuthPaths) {
559
+ if ((0, node_fs_1.existsSync)(authPath)) {
560
+ try {
561
+ const content = (0, node_fs_1.readFileSync)(authPath, 'utf8');
562
+ const auth = JSON.parse(content);
563
+ const tokens = auth.tokens;
564
+ if (tokens?.access_token) {
565
+ codexFound = true;
566
+ codexSource = authPath.replace((0, node_os_1.homedir)(), '~');
567
+ codexPreview = tokens.access_token.substring(0, 15) + '...';
568
+ break;
569
+ }
570
+ if (auth.oauth_token || auth.access_token) {
571
+ codexFound = true;
572
+ codexSource = authPath.replace((0, node_os_1.homedir)(), '~');
573
+ const token = (auth.oauth_token || auth.access_token);
574
+ codexPreview = token.substring(0, 15) + '...';
575
+ break;
576
+ }
577
+ if (auth.OPENAI_API_KEY && typeof auth.OPENAI_API_KEY === 'string') {
578
+ codexFound = true;
579
+ codexSource = authPath.replace((0, node_os_1.homedir)(), '~') + ' (API key)';
580
+ codexPreview = auth.OPENAI_API_KEY.substring(0, 10) + '...';
581
+ break;
582
+ }
583
+ }
584
+ catch {
585
+ // Failed to parse
586
+ }
587
+ }
588
+ }
589
+ }
590
+ credentials.push({
591
+ name: 'Codex/Code OAuth',
592
+ source: codexFound ? codexSource : 'not found',
593
+ found: codexFound,
594
+ preview: codexPreview || undefined,
595
+ });
596
+ }
597
+ const foundCount = credentials.filter(c => c.found).length;
598
+ if (json) {
599
+ (0, output_1.outputJson)({ credentials, found: foundCount }, json);
600
+ return;
601
+ }
602
+ console.log('Local AI Tool Credentials:');
603
+ console.log('');
604
+ for (const cred of credentials) {
605
+ const status = cred.found ? '✓' : '✗';
606
+ console.log(` ${status} ${cred.name}`);
607
+ console.log(` Source: ${cred.source}`);
608
+ if (cred.preview) {
609
+ console.log(` Token: ${cred.preview}`);
610
+ }
611
+ if (cred.expiresAt) {
612
+ const expDate = new Date(cred.expiresAt);
613
+ const now = new Date();
614
+ const isExpired = expDate < now;
615
+ const expLabel = isExpired ? '(expired)' : '';
616
+ console.log(` Expires: ${cred.expiresAt} ${expLabel}`);
617
+ }
618
+ console.log('');
619
+ }
620
+ if (foundCount > 0) {
621
+ console.log(`Found ${foundCount} credential(s). Run 'eve auth sync' to sync to Eve.`);
622
+ }
623
+ else {
624
+ console.log('No local credentials found.');
625
+ console.log('');
626
+ console.log('To set up credentials:');
627
+ console.log(' Claude: Run "claude" CLI and log in');
628
+ console.log(' Codex: Run "codex" CLI and log in');
629
+ }
395
630
  return;
396
631
  }
397
632
  default:
398
- throw new Error('Usage: eve auth <login|logout|status|whoami|bootstrap|sync|token>');
633
+ throw new Error('Usage: eve auth <login|logout|status|whoami|bootstrap|sync|creds|token>');
399
634
  }
400
635
  }
401
636
  function signNonceWithSsh(keyPath, nonce) {
@@ -55,6 +55,8 @@ async function handleEnv(subcommand, positionals, flags, context) {
55
55
  return handleDeploy(positionals, flags, context, json);
56
56
  case 'logs':
57
57
  return handleLogs(positionals, flags, context, json);
58
+ case 'diagnose':
59
+ return handleDiagnose(positionals, flags, context, json);
58
60
  case 'delete':
59
61
  return handleDelete(positionals, flags, context, json);
60
62
  default:
@@ -64,6 +66,7 @@ async function handleEnv(subcommand, positionals, flags, context) {
64
66
  ' create <name> --type=<type> [options] - create an environment\n' +
65
67
  ' deploy <env> --ref <sha> [--direct] [--inputs <json>] - deploy to an environment\n' +
66
68
  ' logs <project> <env> <service> [--since <seconds>] [--tail <n>] [--grep <text>] - get service logs\n' +
69
+ ' diagnose <project> <env> - diagnose deployment health and events\n' +
67
70
  ' delete <name> [--project=<id>] [--force] - delete an environment');
68
71
  }
69
72
  }
@@ -106,11 +109,12 @@ async function handleShow(positionals, flags, context, json) {
106
109
  throw new Error('Usage: eve env show <project> <name> [--project=<id>] [--name=<name>]');
107
110
  }
108
111
  const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/envs/${envName}`);
112
+ const health = await (0, client_1.requestJson)(context, `/projects/${projectId}/envs/${envName}/health`);
109
113
  if (json) {
110
- (0, output_1.outputJson)(response, json);
114
+ (0, output_1.outputJson)({ ...response, health }, json);
111
115
  }
112
116
  else {
113
- formatEnvironmentDetails(response);
117
+ formatEnvironmentDetails(response, health);
114
118
  }
115
119
  }
116
120
  /**
@@ -241,11 +245,46 @@ async function handleDeploy(positionals, flags, context, json) {
241
245
  (0, output_1.outputJson)(response, json);
242
246
  }
243
247
  else {
248
+ if (response.pipeline_run) {
249
+ console.log('');
250
+ console.log('Pipeline deployment queued.');
251
+ console.log(` Pipeline Run: ${response.pipeline_run.run.id}`);
252
+ console.log(` Pipeline: ${response.pipeline_run.run.pipeline_name}`);
253
+ console.log(` Status: ${response.pipeline_run.run.status}`);
254
+ console.log(` Environment: ${response.environment.name}`);
255
+ return;
256
+ }
244
257
  console.log('');
245
- console.log(`Deployment successful!`);
246
- console.log(` Release ID: ${response.release.id}`);
258
+ console.log(`Deployment submitted.`);
259
+ if (response.release) {
260
+ console.log(` Release ID: ${response.release.id}`);
261
+ }
247
262
  console.log(` Environment: ${response.environment.name}`);
248
263
  console.log(` Namespace: ${response.environment.namespace || '(none)'}`);
264
+ if (response.deployment_status?.k8s_status) {
265
+ const status = response.deployment_status.k8s_status;
266
+ const readiness = `${status.available_replicas}/${status.desired_replicas}`;
267
+ console.log(` Status: ${response.deployment_status.state} (${readiness} ready)`);
268
+ }
269
+ else if (response.deployment_status?.state) {
270
+ console.log(` Status: ${response.deployment_status.state}`);
271
+ }
272
+ const warnings = response.warnings ?? getDeploymentWarnings(response.deployment_status);
273
+ if (warnings.length > 0) {
274
+ console.log('');
275
+ console.log('Warnings:');
276
+ for (const warning of warnings) {
277
+ console.log(` - ${warning}`);
278
+ }
279
+ }
280
+ const watchFlag = (0, args_1.toBoolean)(flags.watch);
281
+ const shouldWatch = (watchFlag ?? true)
282
+ && response.deployment_status?.state !== 'ready';
283
+ if (shouldWatch) {
284
+ const timeoutRaw = (0, args_1.getStringFlag)(flags, ['timeout']);
285
+ const timeoutSeconds = timeoutRaw ? parseInt(timeoutRaw, 10) : 120;
286
+ await watchDeploymentStatus(context, projectId, envName, Number.isFinite(timeoutSeconds) ? timeoutSeconds : 120);
287
+ }
249
288
  }
250
289
  }
251
290
  /**
@@ -277,6 +316,26 @@ async function handleLogs(positionals, flags, context, json) {
277
316
  console.log(`[${entry.timestamp}] ${entry.line}`);
278
317
  }
279
318
  }
319
+ /**
320
+ * eve env diagnose <project> <env>
321
+ * Diagnose deployment health for an environment (k8s-only)
322
+ */
323
+ async function handleDiagnose(positionals, flags, context, json) {
324
+ const projectId = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
325
+ const envName = positionals[1] ?? (0, args_1.getStringFlag)(flags, ['env', 'name']);
326
+ if (!projectId || !envName) {
327
+ throw new Error('Usage: eve env diagnose <project> <env> [--events <n>]');
328
+ }
329
+ const query = buildQuery({
330
+ events: (0, args_1.getStringFlag)(flags, ['events']),
331
+ });
332
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/envs/${envName}/diagnose${query}`);
333
+ if (json) {
334
+ (0, output_1.outputJson)(response, json);
335
+ return;
336
+ }
337
+ formatEnvDiagnose(response);
338
+ }
280
339
  /**
281
340
  * eve env delete <name> [--project=<id>] [--force]
282
341
  * Delete an environment
@@ -399,7 +458,7 @@ function formatEnvironmentsTable(environments) {
399
458
  /**
400
459
  * Format a single environment's details
401
460
  */
402
- function formatEnvironmentDetails(env) {
461
+ function formatEnvironmentDetails(env, health) {
403
462
  console.log(`Environment: ${env.name}`);
404
463
  console.log('');
405
464
  console.log(` ID: ${env.id}`);
@@ -408,6 +467,19 @@ function formatEnvironmentDetails(env) {
408
467
  console.log(` Namespace: ${env.namespace || '(none)'}`);
409
468
  console.log(` Database Ref: ${env.db_ref || '(none)'}`);
410
469
  console.log(` Current Release: ${env.current_release_id || '(none)'}`);
470
+ if (health) {
471
+ console.log('');
472
+ console.log(` Deployment Status: ${health.status}`);
473
+ if (health.deployment) {
474
+ console.log(` Deployment Ready: ${health.deployment.available_replicas}/${health.deployment.desired_replicas}`);
475
+ }
476
+ if (health.warnings && health.warnings.length > 0) {
477
+ console.log(' Warnings:');
478
+ for (const warning of health.warnings) {
479
+ console.log(` - ${warning}`);
480
+ }
481
+ }
482
+ }
411
483
  if (env.overrides && Object.keys(env.overrides).length > 0) {
412
484
  console.log('');
413
485
  console.log(' Overrides:');
@@ -419,6 +491,118 @@ function formatEnvironmentDetails(env) {
419
491
  console.log(` Created: ${formatDate(env.created_at)}`);
420
492
  console.log(` Updated: ${formatDate(env.updated_at)}`);
421
493
  }
494
+ function formatEnvDiagnose(report) {
495
+ console.log(`Environment Diagnose: ${report.env_name}`);
496
+ console.log('');
497
+ console.log(` Namespace: ${report.namespace || '(none)'}`);
498
+ console.log(` Status: ${report.status}`);
499
+ console.log(` Ready: ${report.ready ? 'yes' : 'no'}`);
500
+ console.log(` K8s: ${report.k8s_available ? 'available' : 'unavailable'}`);
501
+ if (report.warnings && report.warnings.length > 0) {
502
+ console.log('');
503
+ console.log('Warnings:');
504
+ for (const warning of report.warnings) {
505
+ console.log(` - ${warning}`);
506
+ }
507
+ }
508
+ if (report.deployments.length > 0) {
509
+ console.log('');
510
+ console.log('Deployments:');
511
+ const nameWidth = Math.max(4, ...report.deployments.map((d) => d.name.length));
512
+ const readyWidth = Math.max(5, ...report.deployments.map((d) => `${d.available_replicas}/${d.desired_replicas}`.length));
513
+ const header = [
514
+ padRight('Name', nameWidth),
515
+ padRight('Ready', readyWidth),
516
+ 'Status',
517
+ ].join(' ');
518
+ console.log(header);
519
+ console.log('-'.repeat(header.length));
520
+ for (const deployment of report.deployments) {
521
+ const readiness = `${deployment.available_replicas}/${deployment.desired_replicas}`;
522
+ const status = deployment.ready ? 'ready' : 'not-ready';
523
+ console.log([
524
+ padRight(deployment.name, nameWidth),
525
+ padRight(readiness, readyWidth),
526
+ status,
527
+ ].join(' '));
528
+ }
529
+ }
530
+ if (report.pods.length > 0) {
531
+ console.log('');
532
+ console.log('Pods:');
533
+ const nameWidth = Math.max(4, ...report.pods.map((p) => p.name.length));
534
+ const phaseWidth = Math.max(5, ...report.pods.map((p) => p.phase.length));
535
+ const restartsWidth = Math.max(8, ...report.pods.map((p) => String(p.restarts).length));
536
+ const header = [
537
+ padRight('Name', nameWidth),
538
+ padRight('Phase', phaseWidth),
539
+ padRight('Restarts', restartsWidth),
540
+ 'Ready',
541
+ 'Age',
542
+ ].join(' ');
543
+ console.log(header);
544
+ console.log('-'.repeat(header.length));
545
+ for (const pod of report.pods) {
546
+ console.log([
547
+ padRight(pod.name, nameWidth),
548
+ padRight(pod.phase, phaseWidth),
549
+ padRight(String(pod.restarts), restartsWidth),
550
+ pod.ready ? 'yes' : 'no',
551
+ pod.age,
552
+ ].join(' '));
553
+ }
554
+ }
555
+ if (report.events.length > 0) {
556
+ console.log('');
557
+ console.log('Events:');
558
+ for (const event of report.events) {
559
+ const timestamp = event.timestamp ?? 'unknown';
560
+ const reason = event.reason ?? 'Unknown';
561
+ const message = event.message ?? '';
562
+ console.log(` [${timestamp}] ${event.type} ${reason}: ${message}`);
563
+ }
564
+ }
565
+ }
566
+ function getDeploymentWarnings(status) {
567
+ if (!status)
568
+ return [];
569
+ const warnings = [];
570
+ if (status.state !== 'ready') {
571
+ warnings.push(`Deployment state: ${status.state}`);
572
+ }
573
+ if (status.k8s_status) {
574
+ const { available_replicas, desired_replicas, ready, conditions } = status.k8s_status;
575
+ if (!ready) {
576
+ warnings.push(`Deployment replicas not ready (${available_replicas}/${desired_replicas})`);
577
+ }
578
+ for (const condition of conditions) {
579
+ if (condition.status !== 'True' && condition.message) {
580
+ warnings.push(`${condition.type}: ${condition.message}`);
581
+ }
582
+ }
583
+ }
584
+ return Array.from(new Set(warnings));
585
+ }
586
+ async function watchDeploymentStatus(context, projectId, envName, timeoutSeconds) {
587
+ const start = Date.now();
588
+ const pollIntervalMs = 3000;
589
+ console.log('');
590
+ console.log('Watching deployment status...');
591
+ while ((Date.now() - start) / 1000 < timeoutSeconds) {
592
+ const health = await (0, client_1.requestJson)(context, `/projects/${projectId}/envs/${envName}/health`);
593
+ const elapsed = Math.floor((Date.now() - start) / 1000);
594
+ const readiness = health.deployment
595
+ ? `${health.deployment.available_replicas}/${health.deployment.desired_replicas}`
596
+ : 'n/a';
597
+ console.log(` [${elapsed}s] ${health.status} (${readiness} ready)`);
598
+ if (health.ready) {
599
+ console.log(' Deployment is ready.');
600
+ return;
601
+ }
602
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
603
+ }
604
+ console.log(` Timeout after ${timeoutSeconds}s. Run "eve env diagnose ${projectId} ${envName}" for details.`);
605
+ }
422
606
  /**
423
607
  * Format a date string for display
424
608
  */
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleManifest = handleManifest;
4
+ const args_1 = require("../lib/args");
5
+ const client_1 = require("../lib/client");
6
+ const output_1 = require("../lib/output");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ async function handleManifest(subcommand, positionals, flags, context) {
10
+ const json = Boolean(flags.json);
11
+ switch (subcommand) {
12
+ case 'validate': {
13
+ const useLatest = (0, args_1.toBoolean)(flags.latest) ?? false;
14
+ const strict = (0, args_1.toBoolean)(flags.strict) ?? false;
15
+ const validateSecretsFlag = flags['validate-secrets'] ?? flags.validate_secrets;
16
+ const validateSecrets = (0, args_1.toBoolean)(validateSecretsFlag) ?? false;
17
+ const dir = typeof flags.dir === 'string' ? flags.dir : process.cwd();
18
+ const manifestPath = (0, args_1.getStringFlag)(flags, ['path']) ?? (0, node_path_1.join)(dir, '.eve', 'manifest.yaml');
19
+ let manifestYaml;
20
+ if (!useLatest) {
21
+ try {
22
+ manifestYaml = (0, node_fs_1.readFileSync)(manifestPath, 'utf-8');
23
+ }
24
+ catch (error) {
25
+ throw new Error(`Failed to read manifest at ${manifestPath}: ${error.message}`);
26
+ }
27
+ }
28
+ let projectId = (0, args_1.getStringFlag)(flags, ['project']) ?? context.projectId;
29
+ if (!projectId && manifestYaml) {
30
+ const match = manifestYaml.match(/^project:\s*(\S+)/m);
31
+ if (match) {
32
+ projectId = match[1];
33
+ }
34
+ }
35
+ if (!projectId) {
36
+ throw new Error('Missing project id. Provide --project, set a profile default, or add "project: proj_xxx" to manifest.');
37
+ }
38
+ const response = await (0, client_1.requestJson)(context, `/projects/${projectId}/manifest/validate`, {
39
+ method: 'POST',
40
+ body: {
41
+ manifest_yaml: manifestYaml,
42
+ validate_secrets: validateSecrets || strict,
43
+ strict,
44
+ },
45
+ });
46
+ if (json) {
47
+ (0, output_1.outputJson)(response, json);
48
+ }
49
+ else {
50
+ if (response.valid) {
51
+ console.log('✓ Manifest valid');
52
+ }
53
+ else {
54
+ console.log('✗ Manifest invalid');
55
+ }
56
+ if (response.manifest_hash) {
57
+ console.log(` Hash: ${response.manifest_hash.substring(0, 12)}...`);
58
+ }
59
+ if (response.errors && response.errors.length > 0) {
60
+ console.log('');
61
+ console.log('Errors:');
62
+ response.errors.forEach((error) => console.log(` - ${error}`));
63
+ }
64
+ if (response.warnings && response.warnings.length > 0) {
65
+ console.log('');
66
+ console.log('Warnings:');
67
+ response.warnings.forEach((warning) => console.log(` - ${warning}`));
68
+ }
69
+ if (response.secret_validation?.missing?.length) {
70
+ console.log('');
71
+ console.log('Missing secrets:');
72
+ response.secret_validation.missing.forEach((item) => {
73
+ const hint = item.hints?.[0] ? ` (${item.hints[0]})` : '';
74
+ console.log(` - ${item.key}${hint}`);
75
+ });
76
+ }
77
+ }
78
+ if (!response.valid) {
79
+ process.exitCode = 1;
80
+ }
81
+ return;
82
+ }
83
+ default:
84
+ throw new Error('Usage: eve manifest <validate>');
85
+ }
86
+ }
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ const admin_1 = require("./commands/admin");
24
24
  const agents_1 = require("./commands/agents");
25
25
  const init_1 = require("./commands/init");
26
26
  const release_1 = require("./commands/release");
27
+ const manifest_1 = require("./commands/manifest");
27
28
  async function main() {
28
29
  const { flags, positionals } = (0, args_1.parseArgs)(process.argv.slice(2));
29
30
  const command = positionals[0];
@@ -107,6 +108,9 @@ async function main() {
107
108
  case 'release':
108
109
  await (0, release_1.handleRelease)(subcommand, rest, flags, context);
109
110
  return;
111
+ case 'manifest':
112
+ await (0, manifest_1.handleManifest)(subcommand, rest, flags, context);
113
+ return;
110
114
  default:
111
115
  (0, help_1.showMainHelp)();
112
116
  }
package/dist/lib/help.js CHANGED
@@ -106,6 +106,28 @@ exports.HELP = {
106
106
  },
107
107
  },
108
108
  },
109
+ manifest: {
110
+ description: 'Validate project manifests for schema and required secrets.',
111
+ usage: 'eve manifest <subcommand> [options]',
112
+ subcommands: {
113
+ validate: {
114
+ description: 'Validate a manifest (schema + secrets)',
115
+ usage: 'eve manifest validate [--project <id>] [--path <path>] [--latest]',
116
+ options: [
117
+ '--project <id> Project ID (uses profile default)',
118
+ '--path <path> Path to manifest (default: .eve/manifest.yaml)',
119
+ '--latest Validate latest synced manifest instead of local file',
120
+ '--validate-secrets Validate required secrets (from manifest)',
121
+ '--strict Fail validation if required secrets are missing',
122
+ ],
123
+ examples: [
124
+ 'eve manifest validate',
125
+ 'eve manifest validate --project proj_xxx',
126
+ 'eve manifest validate --latest --project proj_xxx',
127
+ ],
128
+ },
129
+ },
130
+ },
109
131
  secrets: {
110
132
  description: 'Manage secrets at system/org/user/project scope. Values are never returned in plaintext.',
111
133
  usage: 'eve secrets <subcommand> [options]',
@@ -576,7 +598,7 @@ have to specify them on every command. Useful when working with multiple environ
576
598
  auth: {
577
599
  description: `Authenticate with Eve Horizon. Auth is optional for local development but required
578
600
  for cloud deployments. Credentials are stored per-profile.`,
579
- usage: 'eve auth <login|logout|status|whoami|bootstrap|sync|token>',
601
+ usage: 'eve auth <login|logout|status|whoami|bootstrap|sync|creds|token>',
580
602
  subcommands: {
581
603
  login: {
582
604
  description: 'Login via GitHub SSH challenge (default) or Supabase (legacy)',
@@ -647,18 +669,35 @@ for cloud deployments. Credentials are stored per-profile.`,
647
669
  },
648
670
  sync: {
649
671
  description: 'Extract OAuth tokens from host and set as Eve secrets',
650
- usage: 'eve auth sync [--claude] [--codex] [--project <id>] [--system] [--dry-run]',
672
+ usage: 'eve auth sync [--claude] [--codex] [--org <id>] [--project <id>] [--dry-run]',
651
673
  options: [
652
674
  '--claude Only extract Claude/Anthropic tokens',
653
675
  '--codex Only extract Codex/OpenAI tokens',
654
- '--project <id> Project to set secrets on (uses profile default)',
655
- '--system Set as system secrets instead of project secrets',
676
+ '--org <id> Set as org-level secrets',
677
+ '--project <id> Set as project-level secrets',
656
678
  '--dry-run Show what would be set without actually setting',
679
+ '',
680
+ 'Scope priority: --project > --org > user (default)',
681
+ 'Default scope is user-level, so credentials are available to all your jobs.',
657
682
  ],
658
683
  examples: [
659
- 'eve auth sync',
660
- 'eve auth sync --claude --dry-run',
661
- 'eve auth sync --project proj_xxx',
684
+ 'eve auth sync # Sync to user-level (default)',
685
+ 'eve auth sync --org org_xxx # Sync to org-level',
686
+ 'eve auth sync --project proj_xxx # Sync to project-level',
687
+ 'eve auth sync --dry-run # Preview without syncing',
688
+ ],
689
+ },
690
+ creds: {
691
+ description: 'Show local AI tool credentials (Claude Code, Codex/Code) without syncing',
692
+ usage: 'eve auth creds [--claude] [--codex]',
693
+ options: [
694
+ '--claude Only check Claude/Anthropic credentials',
695
+ '--codex Only check Codex/OpenAI credentials',
696
+ ],
697
+ examples: [
698
+ 'eve auth creds',
699
+ 'eve auth creds --claude',
700
+ 'eve auth creds --json',
662
701
  ],
663
702
  },
664
703
  },
@@ -704,6 +743,8 @@ for cloud deployments. Credentials are stored per-profile.`,
704
743
  '--direct Bypass pipeline and do direct deploy',
705
744
  '--inputs <json> JSON inputs for the deployment (e.g., \'{"release_id":"rel_xxx"}\')',
706
745
  '--project <id> Project ID or slug (uses profile default if omitted)',
746
+ '--watch Poll deployment status until ready (default: true)',
747
+ '--timeout <seconds> Watch timeout in seconds (default: 120)',
707
748
  ],
708
749
  examples: [
709
750
  'eve env deploy staging --ref abc123',
@@ -712,6 +753,19 @@ for cloud deployments. Credentials are stored per-profile.`,
712
753
  'eve env deploy staging --ref abc123 --direct --inputs \'{"release_id":"rel_xxx"}\'',
713
754
  ],
714
755
  },
756
+ diagnose: {
757
+ description: 'Diagnose environment deployments (k8s-only)',
758
+ usage: 'eve env diagnose <project> <env> [--events <n>]',
759
+ options: [
760
+ '<project> Project ID or slug',
761
+ '<env> Environment name',
762
+ '--events <n> Limit number of recent events',
763
+ ],
764
+ examples: [
765
+ 'eve env diagnose proj_xxx staging',
766
+ 'eve env diagnose proj_xxx staging --events 20',
767
+ ],
768
+ },
715
769
  logs: {
716
770
  description: 'Fetch logs for a service in an environment (k8s-only)',
717
771
  usage: 'eve env logs <project> <env> <service> [--since <seconds>] [--tail <n>] [--grep <text>]',
@@ -748,6 +802,7 @@ for cloud deployments. Credentials are stored per-profile.`,
748
802
  'eve env create test --type=persistent',
749
803
  'eve env deploy staging --ref abc123',
750
804
  'eve env logs proj_xxx staging api --tail 200',
805
+ 'eve env diagnose proj_xxx staging',
751
806
  ],
752
807
  },
753
808
  api: {
@@ -1162,6 +1217,7 @@ function showMainHelp() {
1162
1217
  console.log('Commands:');
1163
1218
  console.log(' org Manage organizations');
1164
1219
  console.log(' project Manage projects');
1220
+ console.log(' manifest Validate manifests (schema, secrets)');
1165
1221
  console.log(' job Manage jobs (create, list, show, update, claim, etc.)');
1166
1222
  console.log(' env Manage environments (list, show, deploy)');
1167
1223
  console.log(' release Manage and inspect releases');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eve-horizon/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Eve Horizon CLI",
5
5
  "license": "MIT",
6
6
  "repository": {