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.
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/PKG-INFO +15 -5
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/README.md +14 -4
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/chipfoundry_cli/__init__.py +1 -1
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/chipfoundry_cli/main.py +182 -9
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/pyproject.toml +1 -1
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/LICENSE +0 -0
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/chipfoundry_cli/check_refs.py +0 -0
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/chipfoundry_cli/remote_precheck_git.py +0 -0
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/chipfoundry_cli/utils.py +0 -0
- {chipfoundry_cli-2.4.5 → chipfoundry_cli-2.4.9}/chipfoundry_cli/version_check.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: chipfoundry-cli
|
|
3
|
-
Version: 2.4.
|
|
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. **
|
|
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
|
-
|
|
903
|
+
4. **Config Update:**
|
|
894
904
|
- **Automatically merges** the pulled `project.json` with your local version (preserving the platform link)
|
|
895
905
|
|
|
896
|
-
|
|
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
|
-
|
|
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. **
|
|
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
|
-
|
|
877
|
+
4. **Config Update:**
|
|
868
878
|
- **Automatically merges** the pulled `project.json` with your local version (preserving the platform link)
|
|
869
879
|
|
|
870
|
-
|
|
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
|
-
|
|
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.
|
|
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=
|
|
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'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|