recce-cloud 1.33.1__py3-none-any.whl → 1.34.0__py3-none-any.whl

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.
recce_cloud/cli.py CHANGED
@@ -3,6 +3,7 @@
3
3
  Recce Cloud CLI - Lightweight command for managing Recce Cloud operations.
4
4
  """
5
5
 
6
+ import json
6
7
  import logging
7
8
  import os
8
9
  import subprocess
@@ -26,6 +27,7 @@ from recce_cloud.download import (
26
27
  download_with_platform_apis,
27
28
  )
28
29
  from recce_cloud.report import fetch_and_generate_report
30
+ from recce_cloud.review import run_review_command
29
31
  from recce_cloud.upload import upload_to_existing_session, upload_with_platform_apis
30
32
 
31
33
  # Configure logging
@@ -208,17 +210,19 @@ def init(org, project, status, clear):
208
210
  console = Console()
209
211
 
210
212
  # Check authentication first
211
- token = get_api_token()
213
+ token = os.getenv("RECCE_API_TOKEN") or get_api_token()
212
214
  if not token:
213
- console.print("[red]Error:[/red] Not logged in")
214
- console.print("Run 'recce-cloud login' first")
215
+ console.print("[red]Error:[/red] No RECCE_API_TOKEN provided and not logged in")
216
+ console.print("Either set RECCE_API_TOKEN environment variable or run 'recce-cloud login' first")
215
217
  sys.exit(1)
216
218
 
217
219
  # Status check mode
218
220
  if status:
219
221
  binding = get_project_binding()
220
222
  if binding:
221
- console.print(f"[green]✓[/green] Bound to [cyan]{binding['org']}/{binding['project']}[/cyan]")
223
+ console.print(
224
+ f"[green]✓[/green] Bound to org_id=[cyan]{binding['org_id']}[/cyan], project_id=[cyan]{binding['project_id']}[/cyan]"
225
+ )
222
226
  if binding.get("bound_at"):
223
227
  console.print(f" Bound at: {binding['bound_at']}")
224
228
  if binding.get("bound_by"):
@@ -262,12 +266,14 @@ def init(org, project, status, clear):
262
266
  console.print(f"[red]Error:[/red] Project '{project}' not found in organization '{org}'")
263
267
  sys.exit(1)
264
268
 
265
- # Use slug for storage (more stable than name)
266
- org_slug = org_obj.get("slug", org)
267
- project_slug = project_obj.get("slug", project)
269
+ # Store IDs (immutable) instead of slugs (can be renamed)
270
+ org_id = str(org_obj.get("id"))
271
+ project_id = str(project_obj.get("id"))
268
272
 
269
- save_project_binding(org_slug, project_slug, user_email)
270
- console.print(f"[green]✓[/green] Bound to [cyan]{org_slug}/{project_slug}[/cyan]")
273
+ save_project_binding(org_id, project_id, user_email)
274
+ console.print(
275
+ f"[green]✓[/green] Bound to org_id=[cyan]{org_id}[/cyan], project_id=[cyan]{project_id}[/cyan]"
276
+ )
271
277
  console.print(f" Config saved to {get_config_path()}")
272
278
 
273
279
  # Offer to add to .gitignore
@@ -323,16 +329,17 @@ def init(org, project, status, clear):
323
329
  console.print("Please create a project at https://cloud.datarecce.io first")
324
330
  sys.exit(1)
325
331
 
326
- # Build project choices: (name for config, display_name for UI)
332
+ # Build project choices: (project_id for config, display_name for UI)
327
333
  # Filter out archived projects
328
334
  project_choices = []
329
335
  for p in projects:
330
336
  # Skip archived projects (check status field and archived flags)
331
337
  if p.get("status") == "archived" or p.get("archived") or p.get("is_archived"):
332
338
  continue
333
- project_name = p.get("name") or p.get("slug") or str(p.get("id"))
339
+ project_id = str(p.get("id"))
340
+ project_name = p.get("name") or p.get("slug") or project_id
334
341
  display_name = p.get("display_name") or project_name
335
- project_choices.append((project_name, display_name))
342
+ project_choices.append((project_id, display_name))
336
343
 
337
344
  if not project_choices:
338
345
  console.print(f"[yellow]No active projects found in {selected_org_display}[/yellow]")
@@ -346,12 +353,14 @@ def init(org, project, status, clear):
346
353
  console.print(f" {i}. {display_name}")
347
354
 
348
355
  project_idx = click.prompt("Enter number", type=click.IntRange(1, len(project_choices)))
349
- selected_project_name, selected_project_display = project_choices[project_idx - 1]
356
+ selected_project_id, selected_project_display = project_choices[project_idx - 1]
350
357
 
351
- # Save binding (use names for config, not IDs)
352
- save_project_binding(selected_org_name, selected_project_name, user_email)
358
+ # Save binding using IDs (immutable) instead of slugs (can be renamed)
359
+ save_project_binding(str(selected_org_id), selected_project_id, user_email)
353
360
  console.print()
354
- console.print(f"[green]✓[/green] Bound to [cyan]{selected_org_name}/{selected_project_name}[/cyan]")
361
+ console.print(
362
+ f"[green]✓[/green] Bound to org_id=[cyan]{selected_org_id}[/cyan], project_id=[cyan]{selected_project_id}[/cyan]"
363
+ )
355
364
  console.print(f" Config saved to {get_config_path()}")
356
365
 
357
366
  # Offer to add to .gitignore
@@ -381,44 +390,25 @@ def _get_production_session_id(console: Console, token: str) -> Optional[str]:
381
390
  from recce_cloud.api.exceptions import RecceCloudException
382
391
  from recce_cloud.config.project_config import get_project_binding
383
392
 
384
- # Get project binding
393
+ # Get project binding (now stores IDs directly)
385
394
  binding = get_project_binding()
386
395
  if not binding:
387
- # Check environment variables as fallback
396
+ # Check environment variables as fallback (accept both slugs and IDs)
388
397
  env_org = os.environ.get("RECCE_ORG")
389
398
  env_project = os.environ.get("RECCE_PROJECT")
390
399
  if env_org and env_project:
391
- binding = {"org": env_org, "project": env_project}
400
+ binding = {"org_id": env_org, "project_id": env_project}
392
401
  else:
393
402
  console.print("[red]Error:[/red] No project binding found")
394
403
  console.print("Run 'recce-cloud init' to bind this directory to a project")
395
404
  return None
396
405
 
397
- org_slug = binding.get("org")
398
- project_slug = binding.get("project")
406
+ org_id = binding.get("org_id")
407
+ project_id = binding.get("project_id")
399
408
 
400
409
  try:
401
410
  client = RecceCloudClient(token)
402
411
 
403
- # Get org and project IDs
404
- org_info = client.get_organization(org_slug)
405
- if not org_info:
406
- console.print(f"[red]Error:[/red] Organization '{org_slug}' not found")
407
- return None
408
- org_id = org_info.get("id")
409
- if not org_id:
410
- console.print(f"[red]Error:[/red] Organization '{org_slug}' response missing ID")
411
- return None
412
-
413
- project_info = client.get_project(org_id, project_slug)
414
- if not project_info:
415
- console.print(f"[red]Error:[/red] Project '{project_slug}' not found")
416
- return None
417
- project_id = project_info.get("id")
418
- if not project_id:
419
- console.print(f"[red]Error:[/red] Project '{project_slug}' response missing ID")
420
- return None
421
-
422
412
  # List sessions and find production session
423
413
  sessions = client.list_sessions(org_id, project_id)
424
414
  for session in sessions:
@@ -474,14 +464,14 @@ def _get_production_session_id(console: Console, token: str) -> Optional[str]:
474
464
  help="Skip confirmation prompts (auto-create session if not found).",
475
465
  )
476
466
  @click.option(
477
- "--cr",
467
+ "--pr",
478
468
  type=int,
479
- help="Change request number (PR/MR) (overrides auto-detection)",
469
+ help="Pull/Merge request number (PR/MR) (overrides auto-detection)",
480
470
  )
481
471
  @click.option(
482
472
  "--type",
483
473
  "session_type",
484
- type=click.Choice(["cr", "prod", "dev"]),
474
+ type=click.Choice(["pr", "prod", "dev"]),
485
475
  help="Session type (overrides auto-detection)",
486
476
  )
487
477
  @click.option(
@@ -489,7 +479,7 @@ def _get_production_session_id(console: Console, token: str) -> Optional[str]:
489
479
  is_flag=True,
490
480
  help="Show what would be uploaded without actually uploading",
491
481
  )
492
- def upload(target_path, session_id, session_name, skip_confirmation, cr, session_type, dry_run):
482
+ def upload(target_path, session_id, session_name, skip_confirmation, pr, session_type, dry_run):
493
483
  """
494
484
  Upload dbt artifacts (manifest.json, catalog.json) to Recce Cloud.
495
485
 
@@ -525,7 +515,7 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
525
515
  console.rule("CI Environment Detection", style="blue")
526
516
  try:
527
517
  ci_info = CIDetector.detect()
528
- ci_info = CIDetector.apply_overrides(ci_info, cr=cr, session_type=session_type)
518
+ ci_info = CIDetector.apply_overrides(ci_info, pr=pr, session_type=session_type)
529
519
 
530
520
  # Display detected CI information immediately
531
521
  if ci_info:
@@ -534,22 +524,22 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
534
524
  info_table.append(f"[cyan]Platform:[/cyan] {ci_info.platform}")
535
525
 
536
526
  # Display CR number as PR or MR based on platform
537
- if ci_info.cr_number is not None:
527
+ if ci_info.pr_number is not None:
538
528
  if ci_info.platform == "github-actions":
539
- info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.cr_number}")
529
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
540
530
  elif ci_info.platform == "gitlab-ci":
541
- info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.cr_number}")
531
+ info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.pr_number}")
542
532
  else:
543
- info_table.append(f"[cyan]CR Number:[/cyan] {ci_info.cr_number}")
533
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
544
534
 
545
- # Display CR URL as PR URL or MR URL based on platform
546
- if ci_info.cr_url:
535
+ # Display PR URL as PR URL or MR URL based on platform
536
+ if ci_info.pr_url:
547
537
  if ci_info.platform == "github-actions":
548
- info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.cr_url}")
538
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
549
539
  elif ci_info.platform == "gitlab-ci":
550
- info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.cr_url}")
540
+ info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.pr_url}")
551
541
  else:
552
- info_table.append(f"[cyan]CR URL:[/cyan] {ci_info.cr_url}")
542
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
553
543
 
554
544
  if ci_info.session_type:
555
545
  info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
@@ -600,8 +590,8 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
600
590
  console.print(f" • Platform: {ci_info.platform}")
601
591
  if ci_info.repository:
602
592
  console.print(f" • Repository: {ci_info.repository}")
603
- if ci_info.cr_number is not None:
604
- console.print(f" • CR Number: {ci_info.cr_number}")
593
+ if ci_info.pr_number is not None:
594
+ console.print(f" • PR Number: {ci_info.pr_number}")
605
595
  if ci_info.commit_sha:
606
596
  console.print(f" • Commit SHA: {ci_info.commit_sha[:8]}")
607
597
  if ci_info.source_branch:
@@ -719,8 +709,8 @@ def upload(target_path, session_id, session_name, skip_confirmation, cr, session
719
709
  @click.option(
720
710
  "--type",
721
711
  "session_type",
722
- type=click.Choice(["cr", "prod", "dev"]),
723
- help="Filter by session type (prod=base, cr=has PR link, dev=other)",
712
+ type=click.Choice(["pr", "prod", "dev"]),
713
+ help="Filter by session type (prod=base, pr=has PR link, dev=other)",
724
714
  )
725
715
  @click.option(
726
716
  "--json",
@@ -748,8 +738,6 @@ def list_sessions_cmd(session_type, output_json):
748
738
  # Output as JSON
749
739
  recce-cloud list --json
750
740
  """
751
- import json
752
-
753
741
  from rich.table import Table
754
742
 
755
743
  from recce_cloud.api.client import RecceCloudClient
@@ -768,8 +756,8 @@ def list_sessions_cmd(session_type, output_json):
768
756
  # 2. Resolve org/project configuration
769
757
  try:
770
758
  config = resolve_config()
771
- org = config.org
772
- project = config.project
759
+ org = config.org_id
760
+ project = config.project_id
773
761
  except ConfigurationError as e:
774
762
  console.print("[red]Error:[/red] Could not resolve org/project configuration")
775
763
  console.print(f"Reason: {e}")
@@ -808,13 +796,13 @@ def list_sessions_cmd(session_type, output_json):
808
796
 
809
797
  # Helper to derive session type from fields:
810
798
  # - prod: is_base = True
811
- # - cr: pr_link is not null
799
+ # - pr: pr_link is not null
812
800
  # - dev: everything else
813
801
  def get_session_type(s):
814
802
  if s.get("is_base"):
815
803
  return "prod"
816
804
  elif s.get("pr_link"):
817
- return "cr"
805
+ return "pr"
818
806
  else:
819
807
  return "dev"
820
808
 
@@ -957,22 +945,22 @@ def download(target_path, session_id, prod, dry_run, force):
957
945
  info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
958
946
 
959
947
  # Only show CR number and URL for CR sessions (not for prod)
960
- if ci_info.session_type == "cr" and ci_info.cr_number is not None:
948
+ if ci_info.session_type == "pr" and ci_info.pr_number is not None:
961
949
  if ci_info.platform == "github-actions":
962
- info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.cr_number}")
950
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
963
951
  elif ci_info.platform == "gitlab-ci":
964
- info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.cr_number}")
952
+ info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.pr_number}")
965
953
  else:
966
- info_table.append(f"[cyan]CR Number:[/cyan] {ci_info.cr_number}")
954
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
967
955
 
968
- # Only show CR URL for CR sessions
969
- if ci_info.session_type == "cr" and ci_info.cr_url:
956
+ # Only show PR URL for CR sessions
957
+ if ci_info.session_type == "pr" and ci_info.pr_url:
970
958
  if ci_info.platform == "github-actions":
971
- info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.cr_url}")
959
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
972
960
  elif ci_info.platform == "gitlab-ci":
973
- info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.cr_url}")
961
+ info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.pr_url}")
974
962
  else:
975
- info_table.append(f"[cyan]CR URL:[/cyan] {ci_info.cr_url}")
963
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
976
964
 
977
965
  for line in info_table:
978
966
  console.print(line)
@@ -997,8 +985,8 @@ def download(target_path, session_id, prod, dry_run, force):
997
985
  console.print(f" • Repository: {ci_info.repository}")
998
986
  if ci_info.session_type:
999
987
  console.print(f" • Session Type: {ci_info.session_type}")
1000
- if ci_info.session_type == "cr" and ci_info.cr_number is not None:
1001
- console.print(f" • CR Number: {ci_info.cr_number}")
988
+ if ci_info.session_type == "pr" and ci_info.pr_number is not None:
989
+ console.print(f" • PR Number: {ci_info.pr_number}")
1002
990
  console.print()
1003
991
 
1004
992
  # Display download summary
@@ -1124,22 +1112,22 @@ def delete(session_id, dry_run, force):
1124
1112
  info_table.append(f"[cyan]Session Type:[/cyan] {ci_info.session_type}")
1125
1113
 
1126
1114
  # Only show CR number and URL for CR sessions (not for prod)
1127
- if ci_info.session_type == "cr" and ci_info.cr_number is not None:
1115
+ if ci_info.session_type == "pr" and ci_info.pr_number is not None:
1128
1116
  if ci_info.platform == "github-actions":
1129
- info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.cr_number}")
1117
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
1130
1118
  elif ci_info.platform == "gitlab-ci":
1131
- info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.cr_number}")
1119
+ info_table.append(f"[cyan]MR Number:[/cyan] {ci_info.pr_number}")
1132
1120
  else:
1133
- info_table.append(f"[cyan]CR Number:[/cyan] {ci_info.cr_number}")
1121
+ info_table.append(f"[cyan]PR Number:[/cyan] {ci_info.pr_number}")
1134
1122
 
1135
- # Only show CR URL for CR sessions
1136
- if ci_info.session_type == "cr" and ci_info.cr_url:
1123
+ # Only show PR URL for CR sessions
1124
+ if ci_info.session_type == "pr" and ci_info.pr_url:
1137
1125
  if ci_info.platform == "github-actions":
1138
- info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.cr_url}")
1126
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
1139
1127
  elif ci_info.platform == "gitlab-ci":
1140
- info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.cr_url}")
1128
+ info_table.append(f"[cyan]MR URL:[/cyan] {ci_info.pr_url}")
1141
1129
  else:
1142
- info_table.append(f"[cyan]CR URL:[/cyan] {ci_info.cr_url}")
1130
+ info_table.append(f"[cyan]PR URL:[/cyan] {ci_info.pr_url}")
1143
1131
 
1144
1132
  for line in info_table:
1145
1133
  console.print(line)
@@ -1164,8 +1152,8 @@ def delete(session_id, dry_run, force):
1164
1152
  console.print(f" • Repository: {ci_info.repository}")
1165
1153
  if ci_info.session_type:
1166
1154
  console.print(f" • Session Type: {ci_info.session_type}")
1167
- if ci_info.session_type == "cr" and ci_info.cr_number is not None:
1168
- console.print(f" • CR Number: {ci_info.cr_number}")
1155
+ if ci_info.session_type == "pr" and ci_info.pr_number is not None:
1156
+ console.print(f" • PR Number: {ci_info.pr_number}")
1169
1157
  console.print()
1170
1158
 
1171
1159
  # Display delete summary
@@ -1355,5 +1343,175 @@ def report(repo, since, until, base_branch, merged_only, output):
1355
1343
  sys.exit(exit_code)
1356
1344
 
1357
1345
 
1346
+ @cloud_cli.command()
1347
+ @click.option(
1348
+ "--session-id",
1349
+ envvar="RECCE_SESSION_ID",
1350
+ help="Session ID to generate data review for (or use RECCE_SESSION_ID env var). "
1351
+ "Mutually exclusive with --session-name.",
1352
+ )
1353
+ @click.option(
1354
+ "--session-name",
1355
+ help="Name of the session to generate data review for. " "Mutually exclusive with --session-id.",
1356
+ )
1357
+ @click.option(
1358
+ "--org",
1359
+ default=None,
1360
+ help="Organization name or slug (auto-detected from project binding if not provided)",
1361
+ )
1362
+ @click.option(
1363
+ "--project",
1364
+ default=None,
1365
+ help="Project name or slug (auto-detected from project binding if not provided)",
1366
+ )
1367
+ @click.option(
1368
+ "--regenerate",
1369
+ is_flag=True,
1370
+ help="Force regeneration even if a data review already exists",
1371
+ )
1372
+ @click.option(
1373
+ "--timeout",
1374
+ type=int,
1375
+ default=300,
1376
+ help="Maximum seconds to wait for review generation (default: 300)",
1377
+ )
1378
+ @click.option(
1379
+ "--json",
1380
+ "json_output",
1381
+ is_flag=True,
1382
+ help="Output result as JSON (for scripting)",
1383
+ )
1384
+ def review(session_id, session_name, org, project, regenerate, timeout, json_output):
1385
+ """
1386
+ Generate a data review for a session.
1387
+
1388
+ Data reviews provide AI-generated insights comparing your session's data
1389
+ with the production baseline. This command triggers review generation and
1390
+ waits for completion.
1391
+
1392
+ \b
1393
+ Prerequisites:
1394
+ - The session must exist and have artifacts uploaded
1395
+ - A base (production) session must exist with artifacts uploaded
1396
+ - You must be logged in or have RECCE_API_TOKEN set
1397
+
1398
+ \b
1399
+ Authentication:
1400
+ - RECCE_API_TOKEN env var or 'recce-cloud login' profile
1401
+
1402
+ \b
1403
+ Examples:
1404
+ # Generate review for a session by name (uses project binding)
1405
+ recce-cloud review --session-name my-pr-session
1406
+
1407
+ # Generate review for a session by ID
1408
+ recce-cloud review --session-id abc123def456
1409
+
1410
+ # Explicit org/project specification
1411
+ recce-cloud review --session-name my-session --org myorg --project myproject
1412
+
1413
+ # Force regeneration of existing review
1414
+ recce-cloud review --session-name my-session --regenerate
1415
+
1416
+ # JSON output for CI/CD scripting
1417
+ recce-cloud review --session-name my-session --json
1418
+
1419
+ # Custom timeout (10 minutes)
1420
+ recce-cloud review --session-name my-session --timeout 600
1421
+ """
1422
+ console = Console()
1423
+
1424
+ # Validate that at least one of session_id or session_name is provided
1425
+ if not session_id and not session_name:
1426
+ if json_output:
1427
+ print(
1428
+ json.dumps(
1429
+ {
1430
+ "success": False,
1431
+ "error": "Either --session-id or --session-name must be provided",
1432
+ }
1433
+ )
1434
+ )
1435
+ else:
1436
+ console.print("[red]Error:[/red] Either --session-id or --session-name must be provided")
1437
+ sys.exit(1)
1438
+
1439
+ # Warn if both are provided (session-id takes precedence)
1440
+ if session_id and session_name:
1441
+ if not json_output:
1442
+ console.print(
1443
+ "[yellow]Warning:[/yellow] Both --session-id and --session-name provided. " "Using --session-id."
1444
+ )
1445
+ session_name = None # Clear session_name to use session_id
1446
+
1447
+ # 1. Get API token
1448
+ from recce_cloud.auth.profile import get_api_token
1449
+
1450
+ token = os.getenv("RECCE_API_TOKEN") or get_api_token()
1451
+ if not token:
1452
+ if json_output:
1453
+ print(
1454
+ json.dumps(
1455
+ {
1456
+ "success": False,
1457
+ "error": "No RECCE_API_TOKEN provided and not logged in",
1458
+ }
1459
+ )
1460
+ )
1461
+ else:
1462
+ console.print("[red]Error:[/red] No RECCE_API_TOKEN provided and not logged in")
1463
+ console.print("Either set RECCE_API_TOKEN environment variable or run 'recce-cloud login' first")
1464
+ sys.exit(2)
1465
+
1466
+ # 2. Resolve org/project configuration
1467
+ from recce_cloud.config.resolver import ConfigurationError, resolve_config
1468
+
1469
+ try:
1470
+ config = resolve_config(cli_org=org, cli_project=project)
1471
+ org_id = config.org_id
1472
+ project_id = config.project_id
1473
+ except ConfigurationError as e:
1474
+ if json_output:
1475
+ print(
1476
+ json.dumps(
1477
+ {
1478
+ "success": False,
1479
+ "error": str(e),
1480
+ }
1481
+ )
1482
+ )
1483
+ else:
1484
+ console.print(f"[red]Error:[/red] {e}")
1485
+ console.print()
1486
+ console.print("Provide --org and --project options, or run 'recce-cloud init' to bind to a project")
1487
+ sys.exit(1)
1488
+
1489
+ if not json_output:
1490
+ console.rule("Data Review", style="blue")
1491
+ console.print(f"[cyan]Organization:[/cyan] {org_id}")
1492
+ console.print(f"[cyan]Project:[/cyan] {project_id}")
1493
+ if session_id:
1494
+ session_id_display = session_id[:8] + "..." if len(session_id) > 8 else session_id
1495
+ console.print(f"[cyan]Session ID:[/cyan] {session_id_display}")
1496
+ else:
1497
+ console.print(f"[cyan]Session:[/cyan] {session_name}")
1498
+ if regenerate:
1499
+ console.print("[yellow]Regenerate mode enabled[/yellow]")
1500
+ console.print()
1501
+
1502
+ # 3. Run the review command
1503
+ run_review_command(
1504
+ console=console,
1505
+ token=token,
1506
+ org_id=org_id,
1507
+ project_id=project_id,
1508
+ session_name=session_name,
1509
+ session_id=session_id,
1510
+ regenerate=regenerate,
1511
+ timeout=timeout,
1512
+ json_output=json_output,
1513
+ )
1514
+
1515
+
1358
1516
  if __name__ == "__main__":
1359
1517
  cloud_cli()
@@ -57,11 +57,11 @@ class DiagnosticRenderer:
57
57
  check = results.project_binding
58
58
 
59
59
  if check.passed:
60
- org = check.details.get("org")
61
- project = check.details.get("project")
60
+ org_id = check.details.get("org_id")
61
+ project_id = check.details.get("project_id")
62
62
  source = check.details.get("source")
63
63
  source_label = " (via env vars)" if source == "env_vars" else ""
64
- self.console.print(f"[green]✓[/green] Bound to [cyan]{org}/{project}[/cyan]{source_label}")
64
+ self.console.print(f"[green]✓[/green] Bound to [cyan]{org_id}/{project_id}[/cyan]{source_label}")
65
65
  else:
66
66
  self._render_failure(check)
67
67
 
@@ -87,26 +87,26 @@ def get_project_binding(project_dir: Optional[str] = None) -> Optional[Dict[str,
87
87
  project_dir: Project directory path. Defaults to current directory.
88
88
 
89
89
  Returns:
90
- Dictionary with org, project, bound_at, bound_by fields,
90
+ Dictionary with org_id, project_id, bound_at, bound_by fields,
91
91
  or None if not bound.
92
92
  """
93
93
  config = load_config(project_dir)
94
94
  cloud_config = config.get("cloud", {})
95
95
 
96
- if not cloud_config.get("org") or not cloud_config.get("project"):
96
+ if not cloud_config.get("org_id") or not cloud_config.get("project_id"):
97
97
  return None
98
98
 
99
99
  return {
100
- "org": cloud_config.get("org"),
101
- "project": cloud_config.get("project"),
100
+ "org_id": cloud_config.get("org_id"),
101
+ "project_id": cloud_config.get("project_id"),
102
102
  "bound_at": cloud_config.get("bound_at"),
103
103
  "bound_by": cloud_config.get("bound_by"),
104
104
  }
105
105
 
106
106
 
107
107
  def save_project_binding(
108
- org: str,
109
- project: str,
108
+ org_id: str,
109
+ project_id: str,
110
110
  bound_by: Optional[str] = None,
111
111
  project_dir: Optional[str] = None,
112
112
  ) -> None:
@@ -114,8 +114,8 @@ def save_project_binding(
114
114
  Save project binding to .recce/config.
115
115
 
116
116
  Args:
117
- org: Organization name/slug.
118
- project: Project name/slug.
117
+ org_id: Organization ID.
118
+ project_id: Project ID.
119
119
  bound_by: Email of user who created the binding.
120
120
  project_dir: Project directory path. Defaults to current directory.
121
121
  """
@@ -123,8 +123,8 @@ def save_project_binding(
123
123
 
124
124
  config["version"] = 1
125
125
  config["cloud"] = {
126
- "org": org,
127
- "project": project,
126
+ "org_id": org_id,
127
+ "project_id": project_id,
128
128
  "bound_at": datetime.now(timezone.utc).isoformat(),
129
129
  }
130
130