Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,10 @@ cf pull [--project-name NAME]
**Prerequisites:** `cf login`, `cf link` (or `cf init`), `cf config`

- Downloads project results from SFTP server
- **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
- First asks the platform API for the canonical project name and tries `outgoing/results/<canonical_name>`
- If that path is missing, falls back to scanning `outgoing/results/*/config/project.json` for a matching `platform_project_id`
- Pass `--project-name NAME` to bypass UUID resolution and force a literal directory lookup (debugging / unlinked legacy use)
- Saves to `sftp-output/<project_name>/`
- **Automatically updates** your local `.cf/project.json` with the pulled version (preserving the platform link)
- **Syncs with the platform** and displays admin review notes if your project has been reviewed
Expand Down Expand Up @@ -859,20 +863,26 @@ The CLI tracks your project submission state through the `submission_state` fiel
- Connects to SFTP server securely
- Shows clean connection status

2. **Download:**
2. **Resolve remote directory by UUID:**
- Looks up the canonical project name from the platform via `platform_project_id`
- Tries `outgoing/results/<canonical_name>` first
- If that path is missing, scans `outgoing/results/*/config/project.json` for a directory whose embedded `platform_project_id` matches yours
- Warns if your local project name differs from the canonical platform name (the local copy is corrected automatically in step 4)

3. **Download:**
- Downloads all project results recursively
- Shows professional download progress
- Saves to `sftp-output/<project_name>/`

3. **Config Update:**
4. **Config Update:**
- **Automatically merges** the pulled `project.json` with your local version (preserving the platform link)

4. **Platform Sync:**
5. **Platform Sync:**
- Sends the updated `project.json` to the platform
- Records the pull timestamp on the platform
- Fetches and displays any admin review notes

5. **Success:**
6. **Success:**
- Shows confirmation of downloaded files, sync status, and review notes

---
Expand Down
2 changes: 1 addition & 1 deletion chipfoundry_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""ChipFoundry CLI package: Automate project submission to SFTP."""
__version__ = "2.4.1"
__version__ = "2.4.6"
98 changes: 90 additions & 8 deletions chipfoundry_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2078,6 +2078,9 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n
@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)
def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key):
"""Download results/artifacts from SFTP output dir to local sftp-output/<project_name>."""
# Track whether the user explicitly passed --project-name (overrides
# canonical-name resolution via the platform API below).
explicit_project_name = project_name
# If .cf/project.json exists in cwd, use its project name as default
_, cwd_project_name = get_project_json_from_cwd()
if not project_name and cwd_project_name:
Expand Down Expand Up @@ -2142,16 +2145,67 @@ def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key):
raise click.Abort()

try:
# Resolve the remote results directory.
#
# Priority:
# 1. If the user passed --project-name explicitly, honor that name
# verbatim (escape hatch / debugging).
# 2. Otherwise, ask the platform API for the canonical project name
# via the platform_project_id (UUID) and try that name first.
# 3. If that directory does not exist on SFTP (e.g. the platform was
# renamed but the old export directory still has the previous
# name), scan `outgoing/results/*/config/project.json` and match
# on `platform_project_id`. This is the authoritative UUID match
# and survives case changes and renames.
if explicit_project_name:
resolved_name = explicit_project_name
try:
sftp.stat(f"outgoing/results/{resolved_name}")
except Exception:
console.print(f"[yellow]No results found for project '{resolved_name}' on SFTP server.[/yellow]")
return
else:
try:
platform_proj = _api_get(f"/projects/{platform_id}")
except SystemExit:
console.print(f"[red]Could not resolve canonical project name for platform_project_id={platform_id} from the platform API.[/red]")
raise click.Abort()
canonical_name = platform_proj.get("name") if isinstance(platform_proj, dict) else None
if not canonical_name:
console.print(f"[red]Platform did not return a name for project {platform_id}; cannot resolve SFTP directory.[/red]")
raise click.Abort()

try:
sftp.stat(f"outgoing/results/{canonical_name}")
resolved_name = canonical_name
if cwd_project_name and cwd_project_name != canonical_name:
console.print(
f"[yellow]Local project name '{cwd_project_name}' does not match the platform "
f"name '{canonical_name}'. Using the platform name; your local .cf/project.json "
f"will be updated after the pull completes.[/yellow]"
)
except Exception:
console.print(
f"[yellow]'outgoing/results/{canonical_name}' not found on SFTP. "
f"Searching by project UUID ({platform_id})...[/yellow]"
)
matched_dir = _find_remote_results_dir_by_uuid(sftp, platform_id)
if matched_dir is None:
console.print(
f"[yellow]No results found for project '{canonical_name}' (UUID {platform_id}) on SFTP server.[/yellow]"
)
return
resolved_name = matched_dir
console.print(
f"[yellow]Found a results directory matching this project's UUID at "
f"'outgoing/results/{matched_dir}'. The directory name on SFTP differs from the "
f"platform name '{canonical_name}' — using the SFTP directory.[/yellow]"
)

project_name = resolved_name
remote_dir = f"outgoing/results/{project_name}"
output_dir = os.path.join(os.getcwd(), "sftp-output", project_name)

# Check if remote directory exists
try:
sftp.stat(remote_dir)
except Exception:
console.print(f"[yellow]No results found for project '{project_name}' on SFTP server.[/yellow]")
return


# Create output directory
os.makedirs(output_dir, exist_ok=True)

Expand Down Expand Up @@ -4735,6 +4789,34 @@ def _load_project_platform_id(project_root: str):
return data.get('project', {}).get('platform_project_id')


def _find_remote_results_dir_by_uuid(sftp, platform_id: str) -> Optional[str]:
"""Scan outgoing/results/*/config/project.json for a directory whose embedded
platform_project_id matches `platform_id`. Returns the bare directory name
(not the full path) of the first match, or None if no match is found.

Used by `cf pull` as a UUID-based fallback when the canonical project
name from the platform does not resolve to an SFTP directory (e.g. the
project was renamed on the platform but the old SFTP results directory
still has the previous name on disk).
"""
try:
dirs = sftp.listdir("outgoing/results")
except Exception:
return None

for d in dirs:
cfg_path = f"outgoing/results/{d}/config/project.json"
try:
with sftp.open(cfg_path, "r") as f:
data = json.loads(f.read().decode("utf-8"))
except Exception:
continue
proj = data.get("project", {}) if isinstance(data, dict) else {}
if isinstance(proj, dict) and proj.get("platform_project_id") == platform_id:
return d
return None


def _save_platform_id(project_root: str, platform_id: str, project_name: str = None):
"""Write platform_project_id (and optionally project name) into .cf/project.json."""
pj = Path(project_root) / '.cf' / 'project.json'
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "chipfoundry-cli"
version = "2.4.5"
version = "2.4.6"
description = "CLI tool to automate ChipFoundry project submission to SFTP server"
authors = ["ChipFoundry <marwan.abbas@chipfoundry.io>"]
readme = "README.md"
Expand Down
Loading