chipfoundry-cli 1.0.4__tar.gz → 1.1.0__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: 1.0.4
3
+ Version: 1.1.0
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
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Requires-Dist: click (>=8.0.0,<9)
19
+ Requires-Dist: httpx (>=0.24.0,<1.0)
19
20
  Requires-Dist: paramiko (>=3.0.0,<4)
20
21
  Requires-Dist: rich (>=13,<14)
21
22
  Requires-Dist: toml (>=0.10,<1.0)
@@ -82,6 +83,11 @@ cf --help
82
83
  cf view-tapeout-report
83
84
  ```
84
85
 
86
+ 8. **Confirm final tapeout** (when ready to send GDS to foundry):
87
+ ```bash
88
+ cf confirm
89
+ ```
90
+
85
91
  ---
86
92
 
87
93
  ## Project Structure Requirements
@@ -95,6 +101,7 @@ Your project directory **must** contain:
95
101
  - **Note**: Both compressed (`.gz`) and uncompressed (`.gds`) files are supported
96
102
  - `verilog/rtl/user_defines.v` (required for digital/analog)
97
103
  - `.cf/project.json` (optional; will be created/updated automatically)
104
+ - Contains project metadata including `submission_state` ("Draft" or "Final")
98
105
 
99
106
  **Example:**
100
107
  ```
@@ -217,6 +224,23 @@ cf pull [--project-name NAME]
217
224
  └── consolidated_report.html
218
225
  ```
219
226
 
227
+ ### Confirm Final Tapeout
228
+
229
+ ```bash
230
+ cf confirm [OPTIONS]
231
+ ```
232
+
233
+ - **Confirms your final tapeout** by setting `submission_state` to "Final"
234
+ - **Uploads only the project.json** to the SFTP server (not the entire project)
235
+ - **Use this when you're ready to send your current GDS file to the foundry** for tapeout processing
236
+ - **Options:**
237
+ - `--project-root`: Specify project directory
238
+ - `--project-name`: Override project name
239
+ - `--sftp-username`: Override configured username
240
+ - `--sftp-key`: Override configured key path
241
+
242
+ **Important:** This command confirms that your current GDS file is ready to be sent to the foundry for tapeout. Only run this when you are completely satisfied with your design and ready for final tapeout processing. This action cannot be easily undone.
243
+
220
244
  ### View Tapeout Report
221
245
 
222
246
  ```bash
@@ -240,6 +264,102 @@ cf status
240
264
  - Shows which projects have input files and/or results
241
265
  - Displays project status in a clean table format
242
266
 
267
+ ### Repository Management
268
+
269
+ #### Update Repository Files
270
+
271
+ ```bash
272
+ cf repo update [OPTIONS]
273
+ ```
274
+
275
+ - **Updates local project files** from the upstream GitHub repository
276
+ - **Fetches configuration** from `.cf/repo.json` in the specified repository and branch
277
+ - **Downloads and overwrites** local files based on the changes list in the configuration
278
+ - **Saves configuration** locally to `.cf/repo.json` for reference
279
+
280
+ **Options:**
281
+ - `--project-root`: Specify project directory (defaults to current directory)
282
+ - `--repo-owner`: GitHub repository owner (default: chipfoundry)
283
+ - `--repo-name`: GitHub repository name (default: caravel_user_project)
284
+ - `--branch`: Branch name containing the repo.json file (default: cli-update)
285
+ - `--dry-run`: Preview changes without updating files
286
+
287
+ **What happens:**
288
+ 1. Fetches `.cf/repo.json` from the specified GitHub repository and branch
289
+ 2. Downloads and saves the configuration file locally to `.cf/repo.json`
290
+ 3. Downloads and overwrites each file listed in the `changes` array
291
+ 4. Provides detailed feedback on success/failure of each file update
292
+
293
+ **Example:**
294
+ ```bash
295
+ # Preview what would be updated
296
+ cf repo update --dry-run
297
+ # Output:
298
+ # Files that would be updated:
299
+ # • .cf/repo.json (configuration file)
300
+ # • README.md
301
+ # • Makefile
302
+ # • openlane/Makefile
303
+
304
+ # Update files from upstream
305
+ cf repo update
306
+ # Output:
307
+ # Update results:
308
+ # ✓ Updated: .cf/repo.json
309
+ # ✓ Updated: README.md
310
+ # ✓ Updated: Makefile
311
+ # ✓ Updated: openlane/Makefile
312
+ # Successfully updated 4 file(s)
313
+ # All files updated successfully!
314
+ ```
315
+
316
+ **Use cases:**
317
+ - Keep your project up-to-date with upstream changes
318
+ - Apply bug fixes and improvements from the template repository
319
+ - Sync configuration changes from the upstream project
320
+ - Maintain consistency with the latest project structure
321
+
322
+ ---
323
+
324
+ ## Submission Workflow and States
325
+
326
+ The CLI tracks your project submission state through the `submission_state` field in `project.json`:
327
+
328
+ ### Project States
329
+
330
+ - **"Draft"** - Initial state when you run `cf init`
331
+ - Project is ready for development and testing
332
+ - You can push updates multiple times
333
+ - Project is not yet ready for tapeout processing
334
+
335
+ - **"Final"** - Confirmed state when you run `cf confirm`
336
+ - GDS file is ready to be sent to the foundry for tapeout
337
+ - No further changes should be made to the design
338
+ - Only the project.json is uploaded (not the full project)
339
+
340
+ ### Recommended Workflow
341
+
342
+ 1. **Development Phase:**
343
+ ```bash
344
+ cf init # Creates project with submission_state: "Draft"
345
+ cf push # Upload project files (state remains "Draft")
346
+ # ... make changes to your project ...
347
+ cf push # Upload updated files (state remains "Draft")
348
+ ```
349
+
350
+ 2. **Review Phase (Optional):**
351
+ ```bash
352
+ cf pull # Download results for review (if available)
353
+ cf view-tapeout-report # Review the tapeout report (if available)
354
+ ```
355
+
356
+ 3. **Final Tapeout Confirmation:**
357
+ ```bash
358
+ cf confirm # Confirm current GDS file is ready for foundry tapeout
359
+ ```
360
+
361
+ **Important:** Only run `cf confirm` when you are completely satisfied with your GDS file and ready to send it to the foundry for tapeout processing. This action cannot be easily undone.
362
+
243
363
  ---
244
364
 
245
365
  ## How the GDS Hash Works
@@ -327,9 +447,15 @@ cf pull
327
447
  # ✓ All files downloaded to sftp-output/my_awesome_project
328
448
  # ✓ Project config automatically updated
329
449
 
330
- # View the tapeout report
450
+ # Review the tapeout report (if available)
331
451
  cf view-tapeout-report
332
452
  # ✓ Opened tapeout report in browser: sftp-output/my_awesome_project/consolidated_reports/consolidated_report.html
453
+
454
+ # When ready, confirm final tapeout (sends GDS to foundry)
455
+ cf confirm
456
+ # ✓ Updated project.json with submission_state = Final
457
+ # ✓ Confirmed project submission: my_awesome_project
458
+ # ✓ Uploaded project.json to incoming/projects/my_awesome_project/.cf/project.json
333
459
  ```
334
460
 
335
461
  ### Advanced Usage
@@ -350,6 +476,9 @@ cf pull --project-name other_project
350
476
  # View report for specific project
351
477
  cf view-tapeout-report --project-name other_project
352
478
 
479
+ # Confirm final tapeout for specific project
480
+ cf confirm --project-name other_project
481
+
353
482
  # View custom report file
354
483
  cf view-tapeout-report --report-path /path/to/custom_report.html
355
484
 
@@ -397,6 +526,12 @@ gds/user_project_wrapper.gds.gz # ← Remove this one
397
526
  - Check that the report exists at the expected location
398
527
  - Use `--report-path` to specify a custom report location
399
528
 
529
+ - **Repository update errors:**
530
+ - Check your internet connection
531
+ - Verify the repository and branch exist
532
+ - Use `--dry-run` to preview changes before applying them
533
+ - Check that the `.cf/repo.json` file exists in the upstream repository
534
+
400
535
  - **ModuleNotFoundError:**
401
536
  - Upgrade the CLI: `pip install --upgrade chipfoundry-cli`
402
537
 
@@ -58,6 +58,11 @@ cf --help
58
58
  cf view-tapeout-report
59
59
  ```
60
60
 
61
+ 8. **Confirm final tapeout** (when ready to send GDS to foundry):
62
+ ```bash
63
+ cf confirm
64
+ ```
65
+
61
66
  ---
62
67
 
63
68
  ## Project Structure Requirements
@@ -71,6 +76,7 @@ Your project directory **must** contain:
71
76
  - **Note**: Both compressed (`.gz`) and uncompressed (`.gds`) files are supported
72
77
  - `verilog/rtl/user_defines.v` (required for digital/analog)
73
78
  - `.cf/project.json` (optional; will be created/updated automatically)
79
+ - Contains project metadata including `submission_state` ("Draft" or "Final")
74
80
 
75
81
  **Example:**
76
82
  ```
@@ -193,6 +199,23 @@ cf pull [--project-name NAME]
193
199
  └── consolidated_report.html
194
200
  ```
195
201
 
202
+ ### Confirm Final Tapeout
203
+
204
+ ```bash
205
+ cf confirm [OPTIONS]
206
+ ```
207
+
208
+ - **Confirms your final tapeout** by setting `submission_state` to "Final"
209
+ - **Uploads only the project.json** to the SFTP server (not the entire project)
210
+ - **Use this when you're ready to send your current GDS file to the foundry** for tapeout processing
211
+ - **Options:**
212
+ - `--project-root`: Specify project directory
213
+ - `--project-name`: Override project name
214
+ - `--sftp-username`: Override configured username
215
+ - `--sftp-key`: Override configured key path
216
+
217
+ **Important:** This command confirms that your current GDS file is ready to be sent to the foundry for tapeout. Only run this when you are completely satisfied with your design and ready for final tapeout processing. This action cannot be easily undone.
218
+
196
219
  ### View Tapeout Report
197
220
 
198
221
  ```bash
@@ -216,6 +239,102 @@ cf status
216
239
  - Shows which projects have input files and/or results
217
240
  - Displays project status in a clean table format
218
241
 
242
+ ### Repository Management
243
+
244
+ #### Update Repository Files
245
+
246
+ ```bash
247
+ cf repo update [OPTIONS]
248
+ ```
249
+
250
+ - **Updates local project files** from the upstream GitHub repository
251
+ - **Fetches configuration** from `.cf/repo.json` in the specified repository and branch
252
+ - **Downloads and overwrites** local files based on the changes list in the configuration
253
+ - **Saves configuration** locally to `.cf/repo.json` for reference
254
+
255
+ **Options:**
256
+ - `--project-root`: Specify project directory (defaults to current directory)
257
+ - `--repo-owner`: GitHub repository owner (default: chipfoundry)
258
+ - `--repo-name`: GitHub repository name (default: caravel_user_project)
259
+ - `--branch`: Branch name containing the repo.json file (default: cli-update)
260
+ - `--dry-run`: Preview changes without updating files
261
+
262
+ **What happens:**
263
+ 1. Fetches `.cf/repo.json` from the specified GitHub repository and branch
264
+ 2. Downloads and saves the configuration file locally to `.cf/repo.json`
265
+ 3. Downloads and overwrites each file listed in the `changes` array
266
+ 4. Provides detailed feedback on success/failure of each file update
267
+
268
+ **Example:**
269
+ ```bash
270
+ # Preview what would be updated
271
+ cf repo update --dry-run
272
+ # Output:
273
+ # Files that would be updated:
274
+ # • .cf/repo.json (configuration file)
275
+ # • README.md
276
+ # • Makefile
277
+ # • openlane/Makefile
278
+
279
+ # Update files from upstream
280
+ cf repo update
281
+ # Output:
282
+ # Update results:
283
+ # ✓ Updated: .cf/repo.json
284
+ # ✓ Updated: README.md
285
+ # ✓ Updated: Makefile
286
+ # ✓ Updated: openlane/Makefile
287
+ # Successfully updated 4 file(s)
288
+ # All files updated successfully!
289
+ ```
290
+
291
+ **Use cases:**
292
+ - Keep your project up-to-date with upstream changes
293
+ - Apply bug fixes and improvements from the template repository
294
+ - Sync configuration changes from the upstream project
295
+ - Maintain consistency with the latest project structure
296
+
297
+ ---
298
+
299
+ ## Submission Workflow and States
300
+
301
+ The CLI tracks your project submission state through the `submission_state` field in `project.json`:
302
+
303
+ ### Project States
304
+
305
+ - **"Draft"** - Initial state when you run `cf init`
306
+ - Project is ready for development and testing
307
+ - You can push updates multiple times
308
+ - Project is not yet ready for tapeout processing
309
+
310
+ - **"Final"** - Confirmed state when you run `cf confirm`
311
+ - GDS file is ready to be sent to the foundry for tapeout
312
+ - No further changes should be made to the design
313
+ - Only the project.json is uploaded (not the full project)
314
+
315
+ ### Recommended Workflow
316
+
317
+ 1. **Development Phase:**
318
+ ```bash
319
+ cf init # Creates project with submission_state: "Draft"
320
+ cf push # Upload project files (state remains "Draft")
321
+ # ... make changes to your project ...
322
+ cf push # Upload updated files (state remains "Draft")
323
+ ```
324
+
325
+ 2. **Review Phase (Optional):**
326
+ ```bash
327
+ cf pull # Download results for review (if available)
328
+ cf view-tapeout-report # Review the tapeout report (if available)
329
+ ```
330
+
331
+ 3. **Final Tapeout Confirmation:**
332
+ ```bash
333
+ cf confirm # Confirm current GDS file is ready for foundry tapeout
334
+ ```
335
+
336
+ **Important:** Only run `cf confirm` when you are completely satisfied with your GDS file and ready to send it to the foundry for tapeout processing. This action cannot be easily undone.
337
+
219
338
  ---
220
339
 
221
340
  ## How the GDS Hash Works
@@ -303,9 +422,15 @@ cf pull
303
422
  # ✓ All files downloaded to sftp-output/my_awesome_project
304
423
  # ✓ Project config automatically updated
305
424
 
306
- # View the tapeout report
425
+ # Review the tapeout report (if available)
307
426
  cf view-tapeout-report
308
427
  # ✓ Opened tapeout report in browser: sftp-output/my_awesome_project/consolidated_reports/consolidated_report.html
428
+
429
+ # When ready, confirm final tapeout (sends GDS to foundry)
430
+ cf confirm
431
+ # ✓ Updated project.json with submission_state = Final
432
+ # ✓ Confirmed project submission: my_awesome_project
433
+ # ✓ Uploaded project.json to incoming/projects/my_awesome_project/.cf/project.json
309
434
  ```
310
435
 
311
436
  ### Advanced Usage
@@ -326,6 +451,9 @@ cf pull --project-name other_project
326
451
  # View report for specific project
327
452
  cf view-tapeout-report --project-name other_project
328
453
 
454
+ # Confirm final tapeout for specific project
455
+ cf confirm --project-name other_project
456
+
329
457
  # View custom report file
330
458
  cf view-tapeout-report --report-path /path/to/custom_report.html
331
459
 
@@ -373,6 +501,12 @@ gds/user_project_wrapper.gds.gz # ← Remove this one
373
501
  - Check that the report exists at the expected location
374
502
  - Use `--report-path` to specify a custom report location
375
503
 
504
+ - **Repository update errors:**
505
+ - Check your internet connection
506
+ - Verify the repository and branch exist
507
+ - Use `--dry-run` to preview changes before applying them
508
+ - Check that the `.cf/repo.json` file exists in the upstream repository
509
+
376
510
  - **ModuleNotFoundError:**
377
511
  - Upgrade the CLI: `pip install --upgrade chipfoundry-cli`
378
512
 
@@ -4,7 +4,7 @@ from chipfoundry_cli.utils import (
4
4
  collect_project_files, ensure_cf_directory, update_or_create_project_json,
5
5
  sftp_connect, upload_with_progress, sftp_ensure_dirs, sftp_download_recursive,
6
6
  get_config_path, load_user_config, save_user_config, GDS_TYPE_MAP,
7
- open_html_in_browser, download_with_progress
7
+ open_html_in_browser, download_with_progress, update_repo_files
8
8
  )
9
9
  import os
10
10
  from pathlib import Path
@@ -196,7 +196,7 @@ def init(project_root):
196
196
  project_type = console.input(f"Project type (digital/analog/openframe) (detected: [cyan]{gds_type}[/cyan]): ").strip() or gds_type
197
197
  else:
198
198
  project_type = console.input("Project type (digital/analog/openframe): ").strip()
199
- version = console.input("Version (default 1.0.0): ").strip() or "1.0.0"
199
+ version = "1" # Start with version 1, will be auto-incremented on push
200
200
  # No hash yet, will be filled by push
201
201
  data = {
202
202
  "project": {
@@ -204,7 +204,8 @@ def init(project_root):
204
204
  "type": project_type,
205
205
  "user": username,
206
206
  "version": version,
207
- "user_project_wrapper_hash": ""
207
+ "user_project_wrapper_hash": "",
208
+ "submission_state": "Draft"
208
209
  }
209
210
  }
210
211
  with open(project_json_path, 'w') as f:
@@ -770,5 +771,184 @@ def view_tapeout_report(project_name, report_path):
770
771
  console.print(f"[red]Failed to open tapeout report in browser: {e}[/red]")
771
772
  raise click.Abort()
772
773
 
774
+ @main.command('confirm')
775
+ @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).')
776
+ @click.option('--sftp-host', default=DEFAULT_SFTP_HOST, show_default=True, help='SFTP server hostname.')
777
+ @click.option('--sftp-username', required=False, help='SFTP username (defaults to config).')
778
+ @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)
779
+ @click.option('--project-name', help='Project name (e.g., "my_project"). Overrides project.json if exists.')
780
+ def confirm(project_root, sftp_host, sftp_username, sftp_key, project_name):
781
+ """Confirm project submission by setting submission_state to Final and pushing project.json to SFTP."""
782
+ # If .cf/project.json exists in cwd, use it as default project_root and project_name
783
+ cwd_root, cwd_project_name = get_project_json_from_cwd()
784
+ if not project_root and cwd_root:
785
+ project_root = cwd_root
786
+ if not project_name and cwd_project_name:
787
+ project_name = cwd_project_name
788
+ if not project_root:
789
+ console.print("[bold red]No project root specified and no .cf/project.json found in current directory. Please provide --project-root.[/bold red]")
790
+ raise click.Abort()
791
+
792
+ # Load user config for defaults
793
+ config = load_user_config()
794
+ if not sftp_username:
795
+ sftp_username = config.get("sftp_username")
796
+ if not sftp_username:
797
+ console.print("[bold red]No SFTP username provided and not found in config. Please run 'cf config' or provide --sftp-username.[/bold red]")
798
+ raise click.Abort()
799
+ if not sftp_key:
800
+ sftp_key = config.get("sftp_key")
801
+
802
+ # Always resolve key_path to absolute path if set
803
+ if sftp_key:
804
+ key_path = os.path.abspath(os.path.expanduser(sftp_key))
805
+ else:
806
+ key_path = DEFAULT_SSH_KEY
807
+
808
+ if not os.path.exists(key_path):
809
+ console.print(f"[red]SFTP key file not found: {key_path}[/red]")
810
+ console.print("[yellow]Please run 'cf keygen' to generate a key or 'cf config' to set a custom key path.[/yellow]")
811
+ raise click.Abort()
812
+
813
+ # Load and update project.json
814
+ project_json_path = Path(project_root) / '.cf' / 'project.json'
815
+ if not project_json_path.exists():
816
+ console.print(f"[red]Project configuration not found at {project_json_path}[/red]")
817
+ console.print("[yellow]Please run 'cf init' first to initialize your project.[/yellow]")
818
+ raise click.Abort()
819
+
820
+ # Load existing project.json
821
+ try:
822
+ with open(project_json_path, 'r') as f:
823
+ project_data = json.load(f)
824
+ except Exception as e:
825
+ console.print(f"[red]Failed to read project.json: {e}[/red]")
826
+ raise click.Abort()
827
+
828
+ # Set submission_state to Final
829
+ if "project" not in project_data:
830
+ project_data["project"] = {}
831
+
832
+ project_data["project"]["submission_state"] = "Final"
833
+
834
+ # Save updated project.json
835
+ try:
836
+ with open(project_json_path, 'w') as f:
837
+ json.dump(project_data, f, indent=2)
838
+ console.print("[green]✓ Updated project.json with submission_state = Final[/green]")
839
+ except Exception as e:
840
+ console.print(f"[red]Failed to update project.json: {e}[/red]")
841
+ raise click.Abort()
842
+
843
+ # Get final project name for SFTP upload
844
+ final_project_name = project_name or project_data.get("project", {}).get("name")
845
+ if not final_project_name:
846
+ console.print("[red]No project name found in project.json. Please provide --project-name.[/red]")
847
+ raise click.Abort()
848
+
849
+ # Connect to SFTP and upload project.json
850
+ console.print(f"Connecting to {sftp_host}...")
851
+ transport = None
852
+ try:
853
+ sftp, transport = sftp_connect(
854
+ host=sftp_host,
855
+ username=sftp_username,
856
+ key_path=key_path
857
+ )
858
+ # Ensure the project directory exists before uploading
859
+ sftp_project_dir = f"incoming/projects/{final_project_name}"
860
+ sftp_ensure_dirs(sftp, sftp_project_dir)
861
+ except Exception as e:
862
+ console.print(f"[red]Failed to connect to SFTP: {e}[/red]")
863
+ raise click.Abort()
864
+
865
+ try:
866
+ # Upload only the project.json file
867
+ remote_path = os.path.join(sftp_project_dir, ".cf", "project.json")
868
+ upload_with_progress(
869
+ sftp,
870
+ local_path=str(project_json_path),
871
+ remote_path=remote_path,
872
+ force_overwrite=True # Always overwrite for confirmation
873
+ )
874
+ console.print(f"[green]✓ Confirmed project submission: {final_project_name}[/green]")
875
+ console.print(f"[green]✓ Uploaded project.json to {remote_path}[/green]")
876
+
877
+ except Exception as e:
878
+ console.print(f"[red]Upload failed: {e}[/red]")
879
+ raise click.Abort()
880
+ finally:
881
+ if transport:
882
+ sftp.close()
883
+ transport.close()
884
+
885
+ @main.group('repo')
886
+ def repo_group():
887
+ """Repository management commands."""
888
+ pass
889
+
890
+ @repo_group.command('update')
891
+ @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).')
892
+ @click.option('--repo-owner', default='chipfoundry', help='GitHub repository owner (default: chipfoundry)')
893
+ @click.option('--repo-name', default='caravel_user_project', help='GitHub repository name (default: caravel_user_project)')
894
+ @click.option('--branch', default='cli-update', help='Branch name containing the repo.json file (default: cli-update)')
895
+ @click.option('--dry-run', is_flag=True, help='Preview changes without updating files')
896
+ def repo_update(project_root, repo_owner, repo_name, branch, dry_run):
897
+ """Update local repository files from upstream GitHub repository based on .cf/repo.json changes list."""
898
+ # If .cf/project.json exists in cwd, use it as default project_root
899
+ cwd_root, _ = get_project_json_from_cwd()
900
+ if not project_root and cwd_root:
901
+ project_root = cwd_root
902
+ if not project_root:
903
+ project_root = os.getcwd()
904
+
905
+ console.print(f"[bold cyan]Updating repository files from {repo_owner}/{repo_name}@{branch}[/bold cyan]")
906
+
907
+ try:
908
+ if dry_run:
909
+ console.print("[yellow]Dry run mode - no files will be modified[/yellow]")
910
+ # Fetch repo.json to show what would be updated
911
+ from chipfoundry_cli.utils import fetch_github_file
912
+ repo_json_content = fetch_github_file(repo_owner, repo_name, ".cf/repo.json", branch)
913
+ repo_data = json.loads(repo_json_content)
914
+ changes = repo_data.get("changes", [])
915
+
916
+ console.print(f"[cyan]Files that would be updated:[/cyan]")
917
+ console.print(f" • .cf/repo.json (configuration file)")
918
+ for file_path in changes:
919
+ console.print(f" • {file_path}")
920
+ else:
921
+ # Perform the actual update
922
+ results = update_repo_files(project_root, repo_owner, repo_name, branch)
923
+
924
+ if "error" in results:
925
+ console.print(f"[red]Failed to fetch repository information: {results['error']}[/red]")
926
+ raise click.Abort()
927
+
928
+ # Display results
929
+ success_count = 0
930
+ failure_count = 0
931
+
932
+ console.print(f"[cyan]Update results:[/cyan]")
933
+ for file_path, success in results.items():
934
+ if success:
935
+ console.print(f"[green]✓ Updated: {file_path}[/green]")
936
+ success_count += 1
937
+ else:
938
+ console.print(f"[red]✗ Failed: {file_path}[/red]")
939
+ failure_count += 1
940
+
941
+ if success_count > 0:
942
+ console.print(f"[green]Successfully updated {success_count} file(s)[/green]")
943
+ if failure_count > 0:
944
+ console.print(f"[red]Failed to update {failure_count} file(s)[/red]")
945
+ raise click.Abort()
946
+ else:
947
+ console.print("[green]All files updated successfully![/green]")
948
+
949
+ except Exception as e:
950
+ console.print(f"[red]Repository update failed: {e}[/red]")
951
+ raise click.Abort()
952
+
773
953
  if __name__ == "__main__":
774
954
  main()
@@ -7,6 +7,7 @@ import hashlib
7
7
  import paramiko
8
8
  from rich.progress import Progress, BarColumn, TextColumn, TimeElapsedColumn, TaskProgressColumn
9
9
  import toml
10
+ import httpx
10
11
 
11
12
  REQUIRED_FILES = {
12
13
  ".cf/project.json": False, # Optional, may not exist
@@ -158,7 +159,7 @@ def update_or_create_project_json(
158
159
  ) -> str:
159
160
  """
160
161
  Update or create project.json in cf_dir. If existing_json_path is given, load and update it.
161
- Otherwise, create a new one. Always update the user_project_wrapper_hash.
162
+ Otherwise, create a new one. Always update the user_project_wrapper_hash and auto-increment version.
162
163
  Returns the path to the updated/created project.json.
163
164
  """
164
165
  project_json_path = str(Path(cf_dir) / "project.json")
@@ -169,14 +170,37 @@ def update_or_create_project_json(
169
170
  data["project"] = {}
170
171
  else:
171
172
  data = {"project": {}}
173
+
174
+ # Handle version auto-increment only if GDS hash changed
175
+ current_version = data["project"].get("version")
176
+ current_hash = data["project"].get("user_project_wrapper_hash", "")
177
+
178
+ if current_version is None:
179
+ # No existing version, start with 1
180
+ new_version = "1"
181
+ elif current_hash != hash_val:
182
+ # GDS hash changed, increment version
183
+ try:
184
+ # Convert to int, increment, and convert back to string
185
+ version_num = int(current_version)
186
+ new_version = str(version_num + 1)
187
+ except (ValueError, TypeError):
188
+ # If version is not a valid integer, start from 1
189
+ new_version = "1"
190
+ else:
191
+ # GDS hash unchanged, keep same version
192
+ new_version = current_version
193
+
172
194
  # Required fields with defaults
173
- data["project"].setdefault("version", "1.0.0")
195
+ data["project"]["version"] = new_version
174
196
  data["project"]["user_project_wrapper_hash"] = hash_val
175
- # Apply CLI overrides
176
- for key in ["id", "name", "type", "user", "version"]:
197
+
198
+ # Apply CLI overrides (but don't override auto-incremented version)
199
+ for key in ["id", "name", "type", "user"]: # Removed "version" from CLI overrides
177
200
  cli_key = f"project_{key}" if key != "user" else "sftp_username"
178
201
  if cli_key in cli_overrides and cli_overrides[cli_key] is not None:
179
202
  data["project"][key] = cli_overrides[cli_key]
203
+
180
204
  save_project_json(project_json_path, data)
181
205
  return project_json_path
182
206
 
@@ -396,4 +420,97 @@ def open_html_in_browser(html_path: str):
396
420
  file_url = f"file://{urllib.parse.quote(abs_path)}"
397
421
 
398
422
  # Open in default browser
399
- webbrowser.open(file_url)
423
+ webbrowser.open(file_url)
424
+
425
+ def fetch_github_file(repo_owner: str, repo_name: str, file_path: str, branch: str = "main") -> str:
426
+ """
427
+ Fetch a file from a GitHub repository using the GitHub API.
428
+
429
+ Args:
430
+ repo_owner: GitHub repository owner (e.g., "chipfoundry")
431
+ repo_name: GitHub repository name (e.g., "caravel_user_project")
432
+ file_path: Path to the file in the repository (e.g., ".cf/repo.json")
433
+ branch: Branch name (default: "main")
434
+
435
+ Returns:
436
+ File content as string
437
+
438
+ Raises:
439
+ httpx.HTTPError: If the request fails
440
+ """
441
+ url = f"https://raw.githubusercontent.com/{repo_owner}/{repo_name}/{branch}/{file_path}"
442
+
443
+ with httpx.Client() as client:
444
+ response = client.get(url)
445
+ response.raise_for_status()
446
+ return response.text
447
+
448
+ def download_github_file(repo_owner: str, repo_name: str, file_path: str, local_path: str, branch: str = "main") -> bool:
449
+ """
450
+ Download a file from a GitHub repository and save it locally.
451
+
452
+ Args:
453
+ repo_owner: GitHub repository owner
454
+ repo_name: GitHub repository name
455
+ file_path: Path to the file in the repository
456
+ local_path: Local path to save the file
457
+ branch: Branch name (default: "main")
458
+
459
+ Returns:
460
+ True if successful, False otherwise
461
+ """
462
+ try:
463
+ content = fetch_github_file(repo_owner, repo_name, file_path, branch)
464
+
465
+ # Ensure the local directory exists
466
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
467
+
468
+ # Write the content to the local file
469
+ with open(local_path, 'w', encoding='utf-8') as f:
470
+ f.write(content)
471
+
472
+ return True
473
+ except Exception:
474
+ return False
475
+
476
+ def update_repo_files(project_root: str, repo_owner: str = "chipfoundry", repo_name: str = "caravel_user_project", branch: str = "cli-update") -> Dict[str, bool]:
477
+ """
478
+ Update local repository files based on the repo.json changes list.
479
+
480
+ Args:
481
+ project_root: Local project root directory
482
+ repo_owner: GitHub repository owner
483
+ repo_name: GitHub repository name
484
+ branch: Branch name containing the repo.json file
485
+
486
+ Returns:
487
+ Dictionary mapping file paths to success status
488
+ """
489
+ results = {}
490
+
491
+ try:
492
+ # Fetch the repo.json file
493
+ repo_json_content = fetch_github_file(repo_owner, repo_name, ".cf/repo.json", branch)
494
+ repo_data = json.loads(repo_json_content)
495
+
496
+ # Save the repo.json file to local .cf directory
497
+ cf_dir = os.path.join(project_root, ".cf")
498
+ os.makedirs(cf_dir, exist_ok=True)
499
+ local_repo_json_path = os.path.join(cf_dir, "repo.json")
500
+
501
+ with open(local_repo_json_path, 'w', encoding='utf-8') as f:
502
+ f.write(repo_json_content)
503
+ results[".cf/repo.json"] = True
504
+
505
+ changes = repo_data.get("changes", [])
506
+
507
+ for file_path in changes:
508
+ local_file_path = os.path.join(project_root, file_path)
509
+ success = download_github_file(repo_owner, repo_name, file_path, local_file_path, branch)
510
+ results[file_path] = success
511
+
512
+ except Exception as e:
513
+ # If we can't fetch the repo.json, return empty results
514
+ results["error"] = str(e)
515
+
516
+ return results
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "chipfoundry-cli"
3
- version = "1.0.4"
3
+ version = "1.1.0"
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"
@@ -15,6 +15,7 @@ click = ">=8.0.0,<9"
15
15
  rich = ">=13,<14"
16
16
  paramiko = ">=3.0.0,<4"
17
17
  toml = ">=0.10,<1.0"
18
+ httpx = ">=0.24.0,<1.0"
18
19
 
19
20
  [tool.poetry.dev-dependencies]
20
21
  wheel = "*"
@@ -27,4 +28,4 @@ cf = "chipfoundry_cli.main:main"
27
28
 
28
29
  [build-system]
29
30
  requires = ["poetry-core>=1.0.0"]
30
- build-backend = "poetry.core.masonry.api"
31
+ build-backend = "poetry.core.masonry.api"
File without changes