chipfoundry-cli 2.4.6__tar.gz → 2.4.10__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: chipfoundry-cli
3
- Version: 2.4.6
3
+ Version: 2.4.10
4
4
  Summary: CLI tool to automate ChipFoundry project submission to SFTP server
5
5
  Home-page: https://chipfoundry.io
6
6
  License: Apache-2.0
@@ -598,7 +598,7 @@ cf push [OPTIONS]
598
598
  **Options:**
599
599
  - `--project-root`: Specify project directory
600
600
  - `--force-overwrite`: Overwrite existing files on SFTP (SFTP mode only)
601
- - `--submit`: Submit the project for review after upload
601
+ - `--submit`: Submit the project for review after upload (shortcut for `cf push` + `cf submit`)
602
602
  - `--dry-run`: Preview what would be uploaded
603
603
  - `--sftp-username`: Override configured username (SFTP mode only)
604
604
  - `--sftp-key`: Override configured key path (SFTP mode only)
@@ -659,6 +659,15 @@ What happens:
659
659
  4. Staged S3 objects are deleted on success; any leftovers are expired by the bucket lifecycle after 7 days.
660
660
  5. `--submit` submits for review on success.
661
661
 
662
+ ### Submit a Project for Review (No Re-upload)
663
+
664
+ ```bash
665
+ cf submit [--project-root PATH]
666
+ ```
667
+
668
+ Use this when your latest `cf push` already uploaded the files you want reviewed.
669
+ `cf submit` moves the platform project into review without requiring another upload.
670
+
662
671
  > [!TIP]
663
672
  > Try modes in this order: `cf push` → `cf push --remote` → `cf push --https`.
664
673
  > The SFTP mode is fastest when unrestricted, `--remote` is the best HTTPS
@@ -572,7 +572,7 @@ cf push [OPTIONS]
572
572
  **Options:**
573
573
  - `--project-root`: Specify project directory
574
574
  - `--force-overwrite`: Overwrite existing files on SFTP (SFTP mode only)
575
- - `--submit`: Submit the project for review after upload
575
+ - `--submit`: Submit the project for review after upload (shortcut for `cf push` + `cf submit`)
576
576
  - `--dry-run`: Preview what would be uploaded
577
577
  - `--sftp-username`: Override configured username (SFTP mode only)
578
578
  - `--sftp-key`: Override configured key path (SFTP mode only)
@@ -633,6 +633,15 @@ What happens:
633
633
  4. Staged S3 objects are deleted on success; any leftovers are expired by the bucket lifecycle after 7 days.
634
634
  5. `--submit` submits for review on success.
635
635
 
636
+ ### Submit a Project for Review (No Re-upload)
637
+
638
+ ```bash
639
+ cf submit [--project-root PATH]
640
+ ```
641
+
642
+ Use this when your latest `cf push` already uploaded the files you want reviewed.
643
+ `cf submit` moves the platform project into review without requiring another upload.
644
+
636
645
  > [!TIP]
637
646
  > Try modes in this order: `cf push` → `cf push --remote` → `cf push --https`.
638
647
  > The SFTP mode is fastest when unrestricted, `--remote` is the best HTTPS
@@ -389,6 +389,59 @@ def _prompt_with_default(label: str, current: Optional[str], detected: Optional[
389
389
  return raw
390
390
 
391
391
 
392
+ def _shuttle_sort_key(shuttle: dict) -> str:
393
+ """Sort shuttles by date while handling null/missing dates safely."""
394
+ tapeout_date = shuttle.get("tapeout_date")
395
+ if isinstance(tapeout_date, str) and tapeout_date.strip():
396
+ return tapeout_date
397
+ return "9999-12-31"
398
+
399
+
400
+ def _confirm_new_project_creation() -> bool:
401
+ """Ask for explicit confirmation before creating a new platform project."""
402
+ return click.confirm(
403
+ "Create a NEW platform project now? "
404
+ "(Select 'No' if you intended to link an existing project with `cf link`.)",
405
+ default=False,
406
+ )
407
+
408
+
409
+ def _prompt_init_platform_action() -> str:
410
+ """Ask whether init should link to an existing project or create a new one."""
411
+ console.print("\n[bold]Platform action[/bold]")
412
+ console.print(" [cyan]1[/cyan]. Link to an existing platform project")
413
+ console.print(" [cyan]2[/cyan]. Create a new platform project")
414
+ choice = console.input("Select option [1/2, default 1]: ").strip()
415
+ if choice in ("", "1"):
416
+ return "link"
417
+ if choice == "2":
418
+ return "create"
419
+ console.print("[yellow]Invalid selection — defaulting to linking an existing project.[/yellow]")
420
+ return "link"
421
+
422
+
423
+ def _choose_platform_project(projects: List[dict]) -> Optional[dict]:
424
+ """Show a numbered project list and return the selected project, if any."""
425
+ console.print("\n[bold]Your platform projects:[/bold]")
426
+ for i, p in enumerate(projects, 1):
427
+ status_str = p.get('status', 'unknown')
428
+ shuttle_str = f" — {p.get('shuttle_name', '')}" if p.get('shuttle_name') else ""
429
+ console.print(f" [cyan]{i}[/cyan]. {p['name']}{shuttle_str} [{status_str}]")
430
+ console.print(f" [cyan]{len(projects) + 1}[/cyan]. Create a new platform project")
431
+
432
+ choice = console.input("\nSelect project number: ").strip()
433
+ try:
434
+ idx = int(choice) - 1
435
+ if 0 <= idx < len(projects):
436
+ return projects[idx]
437
+ if idx == len(projects):
438
+ return None
439
+ except ValueError:
440
+ pass
441
+ console.print("[red]Invalid selection.[/red]")
442
+ return None
443
+
444
+
392
445
  @main.command('init')
393
446
  @click.option('--project-root', required=False, type=click.Path(file_okay=False), help='Project directory (defaults to current directory).')
394
447
  @click.option('--shuttle', default=None, help='Shuttle name or ID to associate with the project.')
@@ -539,12 +592,43 @@ def init(project_root, shuttle, description):
539
592
  console.print(f" Portal: {portal_url}/projects/{platform_id}")
540
593
  return
541
594
 
595
+ if api_key:
596
+ try:
597
+ projects = _api_get("/projects/me")
598
+ except SystemExit:
599
+ projects = []
600
+ if projects:
601
+ action = _prompt_init_platform_action()
602
+ if action == "link":
603
+ selected = _choose_platform_project(projects)
604
+ if selected:
605
+ proj['platform_project_id'] = selected['id']
606
+ if selected.get('name'):
607
+ old_name = proj.get('name')
608
+ proj['name'] = selected['name']
609
+ if old_name and old_name != selected['name']:
610
+ console.print(
611
+ f"[yellow]Updated project name: '{old_name}' → '{selected['name']}' "
612
+ "(synced from platform)[/yellow]"
613
+ )
614
+ with open(project_json_path, 'w') as f:
615
+ json.dump(data, f, indent=2)
616
+ portal_url = _get_portal_url()
617
+ console.print(f"\n[green]✓ Linked to existing platform project[/green]")
618
+ console.print(f" Name: {selected['name']}")
619
+ console.print(f" ID: {selected['id']}")
620
+ if github_repo_url:
621
+ console.print(f" GitHub: {github_repo_url}")
622
+ console.print(f" Portal: {portal_url}/projects/{selected['id']}")
623
+ return
624
+ console.print("[dim]Continuing with new project creation.[/dim]")
625
+
542
626
  shuttle_id = shuttle
543
627
  if not shuttle_id:
544
628
  try:
545
629
  shuttles = _api_get("/shuttles/available")
546
630
  if shuttles:
547
- shuttles.sort(key=lambda s: s.get('tapeout_date', '9999-12-31'))
631
+ shuttles.sort(key=_shuttle_sort_key)
548
632
  console.print("\n[bold]Available shuttles:[/bold]")
549
633
  for i, s in enumerate(shuttles, 1):
550
634
  deadline = s.get('tapeout_date', '')
@@ -571,6 +655,13 @@ def init(project_root, shuttle, description):
571
655
  if github_repo_url:
572
656
  create_data["github_repo_url"] = github_repo_url
573
657
 
658
+ if not _confirm_new_project_creation():
659
+ with open(project_json_path, 'w') as f:
660
+ json.dump(data, f, indent=2)
661
+ console.print("[yellow]Skipped platform project creation.[/yellow]")
662
+ console.print("[dim]Tip: Run [bold]cf link[/bold] to select an existing platform project.[/dim]")
663
+ return
664
+
574
665
  try:
575
666
  project_resp = _api_post("/projects", create_data)
576
667
  new_id = project_resp.get('id')
@@ -2769,6 +2860,45 @@ def view_tapeout_report(project_name, report_path):
2769
2860
  console.print(f"[red]Failed to open tapeout report in browser: {e}[/red]")
2770
2861
  raise click.Abort()
2771
2862
 
2863
+ @main.command("submit")
2864
+ @click.option(
2865
+ "--project-root",
2866
+ required=False,
2867
+ type=click.Path(exists=True, file_okay=False),
2868
+ help="Path to the local ChipFoundry project directory (defaults to current directory if .cf/project.json exists).",
2869
+ )
2870
+ def submit(project_root):
2871
+ """Submit a project for admin review without uploading files again."""
2872
+ cwd_root, _ = get_project_json_from_cwd()
2873
+ if not project_root and cwd_root:
2874
+ project_root = cwd_root
2875
+ if not project_root:
2876
+ console.print(
2877
+ "[red]No project root specified and no .cf/project.json found in current directory.[/red]"
2878
+ )
2879
+ console.print("Provide --project-root or run from a linked project.")
2880
+ raise click.Abort()
2881
+
2882
+ project_root = str(Path(project_root).resolve())
2883
+ platform_id = _load_project_platform_id(project_root)
2884
+ if not platform_id:
2885
+ console.print("[red]Project is not linked to the platform.[/red]")
2886
+ console.print(
2887
+ "Run [bold]cf link[/bold] to connect this project, or [bold]cf init[/bold] to create a new one."
2888
+ )
2889
+ raise click.Abort()
2890
+
2891
+ config = load_user_config()
2892
+ if not config.get("api_key"):
2893
+ console.print("[red]Not logged in.[/red] Run [bold]cf login[/bold] before submitting.")
2894
+ raise click.Abort()
2895
+
2896
+ try:
2897
+ _api_post(f"/projects/{platform_id}/submit", {})
2898
+ console.print("[green]✓ Project submitted for review[/green]")
2899
+ except SystemExit:
2900
+ raise click.Abort()
2901
+
2772
2902
  @main.command('confirm')
2773
2903
  @click.option('--project-root', required=False, type=click.Path(exists=True, file_okay=False), help='Path to the local ChipFoundry project directory (defaults to current directory if .cf/project.json exists).')
2774
2904
  @click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chipfoundry-cli"
3
- version = "2.4.6"
3
+ version = "2.4.10"
4
4
  description = "CLI tool to automate ChipFoundry project submission to SFTP server"
5
5
  authors = ["ChipFoundry <marwan.abbas@chipfoundry.io>"]
6
6
  readme = "README.md"