From e9fc5489013123544f43067f1aa5fdfe55326d77 Mon Sep 17 00:00:00 2001 From: jdicorpo Date: Mon, 20 Apr 2026 17:57:15 -0700 Subject: [PATCH] fix(init): don't require SFTP account for cf init and non-SFTP commands (2.4.1) Users without an SFTP account (not yet eligible: no paid/waived/sponsored project or no SSH key on profile) could not run cf init, because the command aborted with "No SFTP account linked to your platform account. Please run 'cf login' first." The SFTP account is only used as a label in .cf/project.json, which the backend stores as an opaque blob and the CLI never reads back, so blocking init on it was incorrect. - cf init: fall back to user_email (or "unknown") for the proj['user'] metadata field; only refresh sftp_username from /auth/cli/whoami when an api_key is present, and never abort. - cf status: SFTP listing becomes a best-effort extra. When no SFTP account is linked, print the platform status, skip SFTP with a soft hint, and exit cleanly. - cf push (SFTP), cf pull, cf tapeouts, cf confirm: keep the SFTP gate but replace the "Contact support" message with one that explains the eligibility path (deposit paid/waived/sponsored + SSH key on profile). - Bump version to 2.4.1 in pyproject.toml and align __init__.py which had drifted at 2.3.2. cf precheck (local), cf precheck --remote, and cf push --remote already worked without SFTP and are unchanged. Made-with: Cursor --- chipfoundry_cli/__init__.py | 2 +- chipfoundry_cli/main.py | 66 +++++++++++++++++++++++++++---------- pyproject.toml | 2 +- 3 files changed, 50 insertions(+), 20 deletions(-) diff --git a/chipfoundry_cli/__init__.py b/chipfoundry_cli/__init__.py index 9ea8481..bf5d4c7 100644 --- a/chipfoundry_cli/__init__.py +++ b/chipfoundry_cli/__init__.py @@ -1,2 +1,2 @@ """ChipFoundry CLI package: Automate project submission to SFTP.""" -__version__ = "2.3.2" \ No newline at end of file +__version__ = "2.4.1" \ No newline at end of file diff --git a/chipfoundry_cli/main.py b/chipfoundry_cli/main.py index ce62fd8..83db85d 100644 --- a/chipfoundry_cli/main.py +++ b/chipfoundry_cli/main.py @@ -402,8 +402,13 @@ def init(project_root, shuttle, description): local_proj = local_data.get('project', {}) if isinstance(local_data, dict) else {} config = load_user_config() + api_key = config.get('api_key') username = config.get("sftp_username") - if not username: + # Try to refresh sftp_username from the platform, but don't block init on it. + # SFTP accounts are only auto-provisioned once a project deposit is paid/waived/ + # sponsored and the user has an SSH key on their profile; init must work before + # that so users can configure locally and use `cf precheck` / `cf push --remote`. + if not username and api_key: try: me = _api_get("/auth/cli/whoami") username = me.get("sftp_username") @@ -412,11 +417,10 @@ def init(project_root, shuttle, description): save_user_config(config) except SystemExit: pass - if not username: - console.print("[bold red]No SFTP account linked to your platform account. Please run 'cf login' first.[/bold red]") - raise click.Abort() - - api_key = config.get('api_key') + # Fall back to email (or 'unknown') purely as a label in .cf/project.json. + # This field is metadata only: SFTP routing uses the live session identity, + # and the backend stores cli_project_json as an opaque blob. + user_label = username or config.get("user_email") or "unknown" platform_id = local_proj.get('platform_project_id') platform_proj: Optional[dict] = None if platform_id and api_key: @@ -469,7 +473,7 @@ def _merged(key_local: str, key_platform: Optional[str] = None) -> Optional[str] proj = data.setdefault('project', {}) proj['name'] = name proj['type'] = project_type - proj['user'] = username + proj['user'] = user_label proj.setdefault('version', local_proj.get('version') or "1") proj.setdefault('user_project_wrapper_hash', local_proj.get('user_project_wrapper_hash', "")) proj.setdefault('submission_state', local_proj.get('submission_state', "Draft")) @@ -1619,7 +1623,11 @@ def push(project_root, sftp_host, sftp_username, sftp_key, project_id, project_n sftp_username = me.get("sftp_username") if not sftp_username: console.print("[bold red]No SFTP account linked to your platform account.[/bold red]") - console.print("Contact support or provide --sftp-username.") + console.print( + "An SFTP account is provisioned once a project deposit is paid/waived/sponsored " + "and an SSH public key is on your profile." + ) + console.print("Override with --sftp-username if you already know yours, or contact support.") raise click.Abort() config["sftp_username"] = sftp_username save_user_config(config) @@ -1805,7 +1813,11 @@ def pull(project_name, output_dir, sftp_host, sftp_username, sftp_key): sftp_username = me.get("sftp_username") if not sftp_username: console.print("[bold red]No SFTP account linked to your platform account.[/bold red]") - console.print("Contact support or provide --sftp-username.") + console.print( + "An SFTP account is provisioned once a project deposit is paid/waived/sponsored " + "and an SSH public key is on your profile." + ) + console.print("Override with --sftp-username if you already know yours, or contact support.") raise click.Abort() config["sftp_username"] = sftp_username save_user_config(config) @@ -2089,24 +2101,34 @@ def status(sftp_host, sftp_username, sftp_key, json_output, show_all): platform_id = _load_project_platform_id(os.getcwd()) if not platform_id: console.print("[dim]Tip: Run [bold]cf link[/bold] to connect this project to the platform.[/dim]\n") + # SFTP listing is a best-effort extra on top of the platform status above. + # Skip it quietly when the user has no SFTP account yet (auto-provisioned + # after a project deposit is paid/waived/sponsored + an SSH key is on file). if not sftp_username: - me = _api_get("/auth/cli/whoami") - sftp_username = me.get("sftp_username") + if config.get("api_key"): + try: + me = _api_get("/auth/cli/whoami") + sftp_username = me.get("sftp_username") + except SystemExit: + sftp_username = None if not sftp_username: - console.print("[red]No SFTP account linked to your platform account.[/red]") - console.print("Contact support or provide --sftp-username.") - raise click.Abort() + console.print( + "[dim]SFTP listing skipped — no SFTP account linked yet. " + "An account is provisioned once a project deposit is paid/waived/sponsored " + "and an SSH public key is on your profile.[/dim]" + ) + return config["sftp_username"] = sftp_username save_user_config(config) if not sftp_key: sftp_key = config.get("sftp_key") - + # Always resolve key_path to absolute path if set if sftp_key: key_path = os.path.abspath(os.path.expanduser(sftp_key)) else: key_path = DEFAULT_SSH_KEY - + if not os.path.exists(key_path): console.print(f"[red]SFTP key file not found: {key_path}[/red]") console.print("[yellow]Please run 'cf keygen' to generate a key or 'cf config' to set a custom key path.[/yellow]") @@ -2232,7 +2254,11 @@ def tapeouts(sftp_host, sftp_username, sftp_key, limit, days): sftp_username = me.get("sftp_username") if not sftp_username: console.print("[red]No SFTP account linked to your platform account.[/red]") - console.print("Contact support or provide --sftp-username.") + console.print( + "An SFTP account is provisioned once a project deposit is paid/waived/sponsored " + "and an SSH public key is on your profile." + ) + console.print("Override with --sftp-username if you already know yours, or contact support.") raise click.Abort() config["sftp_username"] = sftp_username save_user_config(config) @@ -2422,7 +2448,11 @@ def confirm(project_root, sftp_host, sftp_username, sftp_key, project_name): sftp_username = me.get("sftp_username") if not sftp_username: console.print("[bold red]No SFTP account linked to your platform account.[/bold red]") - console.print("Contact support or provide --sftp-username.") + console.print( + "An SFTP account is provisioned once a project deposit is paid/waived/sponsored " + "and an SSH public key is on your profile." + ) + console.print("Override with --sftp-username if you already know yours, or contact support.") raise click.Abort() config["sftp_username"] = sftp_username save_user_config(config) diff --git a/pyproject.toml b/pyproject.toml index 55f1189..f3e4230 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "chipfoundry-cli" -version = "2.4.0" +version = "2.4.1" description = "CLI tool to automate ChipFoundry project submission to SFTP server" authors = ["ChipFoundry "] readme = "README.md"