chipfoundry-cli 2.4.5__tar.gz → 2.4.9__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.5
3
+ Version: 2.4.9
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
@@ -681,6 +681,10 @@ cf pull [--project-name NAME]
681
681
  **Prerequisites:** `cf login`, `cf link` (or `cf init`), `cf config`
682
682
 
683
683
  - Downloads project results from SFTP server
684
+ - **Resolves the remote results directory by `platform_project_id` (UUID), not by project name** — survives case changes (e.g. `kyttar` → `Kyttar`) and renames on the platform without manual intervention
685
+ - First asks the platform API for the canonical project name and tries `outgoing/results/<canonical_name>`
686
+ - If that path is missing, falls back to scanning `outgoing/results/*/config/project.json` for a matching `platform_project_id`
687
+ - Pass `--project-name NAME` to bypass UUID resolution and force a literal directory lookup (debugging / unlinked legacy use)
684
688
  - Saves to `sftp-output/<project_name>/`
685
689
  - **Automatically updates** your local `.cf/project.json` with the pulled version (preserving the platform link)
686
690
  - **Syncs with the platform** and displays admin review notes if your project has been reviewed
@@ -885,20 +889,26 @@ The CLI tracks your project submission state through the `submission_state` fiel
885
889
  - Connects to SFTP server securely
886
890
  - Shows clean connection status
887
891
 
888
- 2. **Download:**
892
+ 2. **Resolve remote directory by UUID:**
893
+ - Looks up the canonical project name from the platform via `platform_project_id`
894
+ - Tries `outgoing/results/<canonical_name>` first
895
+ - If that path is missing, scans `outgoing/results/*/config/project.json` for a directory whose embedded `platform_project_id` matches yours
896
+ - Warns if your local project name differs from the canonical platform name (the local copy is corrected automatically in step 4)
897
+
898
+ 3. **Download:**
889
899
  - Downloads all project results recursively
890
900
  - Shows professional download progress
891
901
  - Saves to `sftp-output/<project_name>/`
892
902
 
893
- 3. **Config Update:**
903
+ 4. **Config Update:**
894
904
  - **Automatically merges** the pulled `project.json` with your local version (preserving the platform link)
895
905
 
896
- 4. **Platform Sync:**
906
+ 5. **Platform Sync:**
897
907
  - Sends the updated `project.json` to the platform
898
908
  - Records the pull timestamp on the platform
899
909
  - Fetches and displays any admin review notes
900
910
 
901
- 5. **Success:**
911
+ 6. **Success:**
902
912
  - Shows confirmation of downloaded files, sync status, and review notes
903
913
 
904
914
  ---
@@ -655,6 +655,10 @@ cf pull [--project-name NAME]
655
655
  **Prerequisites:** `cf login`, `cf link` (or `cf init`), `cf config`
656
656
 
657
657
  - Downloads project results from SFTP server
658
+ - **Resolves the remote results directory by `platform_project_id` (UUID), not by project name** — survives case changes (e.g. `kyttar` → `Kyttar`) and renames on the platform without manual intervention
659
+ - First asks the platform API for the canonical project name and tries `outgoing/results/<canonical_name>`
660
+ - If that path is missing, falls back to scanning `outgoing/results/*/config/project.json` for a matching `platform_project_id`
661
+ - Pass `--project-name NAME` to bypass UUID resolution and force a literal directory lookup (debugging / unlinked legacy use)
658
662
  - Saves to `sftp-output/<project_name>/`
659
663
  - **Automatically updates** your local `.cf/project.json` with the pulled version (preserving the platform link)
660
664
  - **Syncs with the platform** and displays admin review notes if your project has been reviewed
@@ -859,20 +863,26 @@ The CLI tracks your project submission state through the `submission_state` fiel
859
863
  - Connects to SFTP server securely
860
864
  - Shows clean connection status
861
865
 
862
- 2. **Download:**
866
+ 2. **Resolve remote directory by UUID:**
867
+ - Looks up the canonical project name from the platform via `platform_project_id`
868
+ - Tries `outgoing/results/<canonical_name>` first
869
+ - If that path is missing, scans `outgoing/results/*/config/project.json` for a directory whose embedded `platform_project_id` matches yours
870
+ - Warns if your local project name differs from the canonical platform name (the local copy is corrected automatically in step 4)
871
+
872
+ 3. **Download:**
863
873
  - Downloads all project results recursively
864
874
  - Shows professional download progress
865
875
  - Saves to `sftp-output/<project_name>/`
866
876
 
867
- 3. **Config Update:**
877
+ 4. **Config Update:**
868
878
  - **Automatically merges** the pulled `project.json` with your local version (preserving the platform link)
869
879
 
870
- 4. **Platform Sync:**
880
+ 5. **Platform Sync:**
871
881
  - Sends the updated `project.json` to the platform
872
882
  - Records the pull timestamp on the platform
873
883
  - Fetches and displays any admin review notes
874
884
 
875
- 5. **Success:**
885
+ 6. **Success:**
876
886
  - Shows confirmation of downloaded files, sync status, and review notes
877
887
 
878
888
  ---
@@ -1,2 +1,2 @@
1
1
  """ChipFoundry CLI package: Automate project submission to SFTP."""
2
- __version__ = "2.4.1"
2
+ __version__ = "2.4.6"
@@ -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')
@@ -2078,6 +2169,9 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
2078
2169
  @click.option('--sftp-key', type=click.Path(exists=True, dir_okay=False), help='Path to SFTP private key file (defaults to config).', default=None, show_default=False)
2079
2170
  def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key):
2080
2171
  """Download results/artifacts from SFTP output dir to local sftp-output/<project_name>."""
2172
+ # Track whether the user explicitly passed --project-name (overrides
2173
+ # canonical-name resolution via the platform API below).
2174
+ explicit_project_name = project_name
2081
2175
  # If .cf/project.json exists in cwd, use its project name as default
2082
2176
  _, cwd_project_name = get_project_json_from_cwd()
2083
2177
  if not project_name and cwd_project_name:
@@ -2142,16 +2236,67 @@ def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key):
2142
2236
  raise click.Abort()
2143
2237
 
2144
2238
  try:
2239
+ # Resolve the remote results directory.
2240
+ #
2241
+ # Priority:
2242
+ # 1. If the user passed --project-name explicitly, honor that name
2243
+ # verbatim (escape hatch / debugging).
2244
+ # 2. Otherwise, ask the platform API for the canonical project name
2245
+ # via the platform_project_id (UUID) and try that name first.
2246
+ # 3. If that directory does not exist on SFTP (e.g. the platform was
2247
+ # renamed but the old export directory still has the previous
2248
+ # name), scan `outgoing/results/*/config/project.json` and match
2249
+ # on `platform_project_id`. This is the authoritative UUID match
2250
+ # and survives case changes and renames.
2251
+ if explicit_project_name:
2252
+ resolved_name = explicit_project_name
2253
+ try:
2254
+ sftp.stat(f"outgoing/results/{resolved_name}")
2255
+ except Exception:
2256
+ console.print(f"[yellow]No results found for project '{resolved_name}' on SFTP server.[/yellow]")
2257
+ return
2258
+ else:
2259
+ try:
2260
+ platform_proj = _api_get(f"/projects/{platform_id}")
2261
+ except SystemExit:
2262
+ console.print(f"[red]Could not resolve canonical project name for platform_project_id={platform_id} from the platform API.[/red]")
2263
+ raise click.Abort()
2264
+ canonical_name = platform_proj.get("name") if isinstance(platform_proj, dict) else None
2265
+ if not canonical_name:
2266
+ console.print(f"[red]Platform did not return a name for project {platform_id}; cannot resolve SFTP directory.[/red]")
2267
+ raise click.Abort()
2268
+
2269
+ try:
2270
+ sftp.stat(f"outgoing/results/{canonical_name}")
2271
+ resolved_name = canonical_name
2272
+ if cwd_project_name and cwd_project_name != canonical_name:
2273
+ console.print(
2274
+ f"[yellow]Local project name '{cwd_project_name}' does not match the platform "
2275
+ f"name '{canonical_name}'. Using the platform name; your local .cf/project.json "
2276
+ f"will be updated after the pull completes.[/yellow]"
2277
+ )
2278
+ except Exception:
2279
+ console.print(
2280
+ f"[yellow]'outgoing/results/{canonical_name}' not found on SFTP. "
2281
+ f"Searching by project UUID ({platform_id})...[/yellow]"
2282
+ )
2283
+ matched_dir = _find_remote_results_dir_by_uuid(sftp, platform_id)
2284
+ if matched_dir is None:
2285
+ console.print(
2286
+ f"[yellow]No results found for project '{canonical_name}' (UUID {platform_id}) on SFTP server.[/yellow]"
2287
+ )
2288
+ return
2289
+ resolved_name = matched_dir
2290
+ console.print(
2291
+ f"[yellow]Found a results directory matching this project's UUID at "
2292
+ f"'outgoing/results/{matched_dir}'. The directory name on SFTP differs from the "
2293
+ f"platform name '{canonical_name}' — using the SFTP directory.[/yellow]"
2294
+ )
2295
+
2296
+ project_name = resolved_name
2145
2297
  remote_dir = f"outgoing/results/{project_name}"
2146
2298
  output_dir = os.path.join(os.getcwd(), "sftp-output", project_name)
2147
-
2148
- # Check if remote directory exists
2149
- try:
2150
- sftp.stat(remote_dir)
2151
- except Exception:
2152
- console.print(f"[yellow]No results found for project '{project_name}' on SFTP server.[/yellow]")
2153
- return
2154
-
2299
+
2155
2300
  # Create output directory
2156
2301
  os.makedirs(output_dir, exist_ok=True)
2157
2302
 
@@ -4735,6 +4880,34 @@ def _load_project_platform_id(project_root: str):
4735
4880
  return data.get('project', {}).get('platform_project_id')
4736
4881
 
4737
4882
 
4883
+ def _find_remote_results_dir_by_uuid(sftp, platform_id: str) -> Optional[str]:
4884
+ """Scan outgoing/results/*/config/project.json for a directory whose embedded
4885
+ platform_project_id matches `platform_id`. Returns the bare directory name
4886
+ (not the full path) of the first match, or None if no match is found.
4887
+
4888
+ Used by `cf pull` as a UUID-based fallback when the canonical project
4889
+ name from the platform does not resolve to an SFTP directory (e.g. the
4890
+ project was renamed on the platform but the old SFTP results directory
4891
+ still has the previous name on disk).
4892
+ """
4893
+ try:
4894
+ dirs = sftp.listdir("outgoing/results")
4895
+ except Exception:
4896
+ return None
4897
+
4898
+ for d in dirs:
4899
+ cfg_path = f"outgoing/results/{d}/config/project.json"
4900
+ try:
4901
+ with sftp.open(cfg_path, "r") as f:
4902
+ data = json.loads(f.read().decode("utf-8"))
4903
+ except Exception:
4904
+ continue
4905
+ proj = data.get("project", {}) if isinstance(data, dict) else {}
4906
+ if isinstance(proj, dict) and proj.get("platform_project_id") == platform_id:
4907
+ return d
4908
+ return None
4909
+
4910
+
4738
4911
  def _save_platform_id(project_root: str, platform_id: str, project_name: str = None):
4739
4912
  """Write platform_project_id (and optionally project name) into .cf/project.json."""
4740
4913
  pj = Path(project_root) / '.cf' / 'project.json'
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chipfoundry-cli"
3
- version = "2.4.5"
3
+ version = "2.4.9"
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"
File without changes