tapeo-cli 0.4.0 → 0.5.0

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.
package/README.md CHANGED
@@ -35,7 +35,7 @@ In VS Code, install **Tapeo** from the Extensions view and run **Tapeo: Connect
35
35
 
36
36
  ## About
37
37
 
38
- A terminal client and a VS Code extension for the same Tapeo projects as the web app. They call the same server, file storage, workflow queue, and plan allowances as the browser, through the token-authenticated API under `/api/client/v1`. Version 0.4.0 was published on 2026-10-24 (`tapeo-cli` on npm and **Tapeo**, `tapeo.tapeo-vscode`, on the VS Code Marketplace). They are early releases (0.x).
38
+ A terminal client and a VS Code extension for the same Tapeo projects as the web app. They call the same server, file storage, workflow queue, and plan allowances as the browser, through the token-authenticated API under `/api/client/v1`. Version 0.5.0 was published on 2026-10-24 (`tapeo-cli` on npm and **Tapeo**, `tapeo.tapeo-vscode`, on the VS Code Marketplace). They are early releases (0.x).
39
39
 
40
40
  The edit → run → results loop works without the browser: push your files, start a run, follow it stage by stage, and read stage results, tool logs, and goal results. You can also start a new design with the Architect, ask the AI Engineer and apply its proposed changes locally, and see tool errors on the lines they name. The 2D/3D layout views, Evaluation-plan review, and Architect image/PDF attachments stay in the web workspace.
41
41
 
@@ -50,6 +50,29 @@ tapeo login # https://tapeo.net; or: tapeo login https://y
50
50
 
51
51
  `tapeo login` opens `/device?code=ABCD-EFGH` in your browser. Check that the code matches, approve the device, and the terminal finishes signing in. Only a digest of the device code is stored, and no token exists until you approve. The device gets its own token: it expires after 90 days, appears on **Account → VS Code and terminal access**, and can be revoked there. Without a browser, create a token on that page and use `tapeo login <url> --token` (hidden prompt). The sign-in is saved in `~/.config/tapeo/client.json` (mode 0600); `tapeo logout` removes it. `TAPEO_URL` and `TAPEO_TOKEN` override it for automation. `TAPEO_NO_BROWSER=1` makes `tapeo login` only print the approval address instead of opening a browser. Remote servers must use HTTPS; HTTP is accepted for localhost only.
52
52
 
53
+ ## Bringing an existing project (tapeo.json)
54
+
55
+ Any folder layout works: `tapeo create "My design" .` then `tapeo push --all` keeps every path. By convention Tapeo treats every `.v`/`.sv` outside testbench folders as the design, picks the top module that nothing else instantiates, and finds the testbench by name. When that guess is wrong for your repository, add a `tapeo.json` at the project root:
56
+
57
+ ```json
58
+ {
59
+ "top": "soc_top",
60
+ "filelist": "hw/files.f",
61
+ "sources": ["hw/extra/**/*.sv", "!hw/legacy/**"],
62
+ "includeDirs": ["hw/include"],
63
+ "defines": ["WIDTH=32"],
64
+ "verification": ["verif/common/**/*.sv"],
65
+ "testbenches": { "default": "verif/tb_soc.sv", "uart": { "files": ["verif/uart/*.sv"], "top": "tb_uart" } },
66
+ "constraints": "constraints/soc.sdc"
67
+ }
68
+ ```
69
+
70
+ - `sources` are patterns (`**` matches folders, `*` part of a name, `!` removes); `filelist` reads an existing simulator file list (`.f`: files in compile order, `+incdir+`, `+define+`, `-I`, `-D`, `-f`, `-v`). Either one is required.
71
+ - Lint, simulation, synthesis, the FPGA build, and timing all use the same sources, top, defines, and include folders. Simulation compiles only the design, the `verification` files, and the chosen testbench; workflows run `default` (or the first), and `tapeo sim --testbench <name>` picks another.
72
+ - `tapeo manifest` (or **Show Project Manifest**) shows what Tapeo resolved. A broken `tapeo.json` stops the stages with a message that says what to fix; deleting it returns to the conventions.
73
+ - Include folders may use only letters, digits, `.`, `_`, `+`, `-`, and `/`; defines are `NAME` or `NAME=value` without spaces or quotes.
74
+ - Without `tapeo.json`, nothing changes.
75
+
53
76
  ## Terminal
54
77
 
55
78
  ```sh
@@ -57,6 +80,14 @@ tapeo projects # your projects
57
80
  tapeo create "My sensor" ./my-sensor # new empty project, bound to the folder
58
81
  tapeo pull PROJECT_ID ./existing # or bind an existing project
59
82
  tapeo delete PROJECT_ID # delete a project on the server for good (asks for its name)
83
+ tapeo lock rtl/top.sv # protect a file: the AI's fixes become suggestions (pushed files start protected)
84
+ tapeo unlock rtl/top.sv # let the AI Engineer repair it during runs again
85
+ tapeo checkpoints # saved checkpoints, newest first
86
+ tapeo checkpoint "before the FIFO" # save one (holds the files saved in Tapeo now)
87
+ tapeo restore 12 rtl/top.sv # put back a file's version from checkpoint 12 (saved as your edit)
88
+ tapeo target # where the design runs, with the boards
89
+ tapeo target fpga --board tangnano20k --clock 50
90
+ tapeo process sky130hs # change the chip process (old flows and layouts are rebuilt)
60
91
  cd ./my-sensor
61
92
  tapeo status # what changed locally, and the latest run
62
93
  tapeo push --all # push every modified and new source file
@@ -91,13 +122,27 @@ tapeo push --all # then save them in Tapeo
91
122
  tapeo discard # or forget them
92
123
  tapeo history # the Engineer conversation, shared with the browser (--limit N, default 10)
93
124
  tapeo problems # tool errors and warnings of the latest run, as file:line:col
125
+ tapeo lint # lint the pushed design RTL in seconds (no build), as file:line:col
126
+ tapeo sim # simulate the saved files now (no workflow run, no build)
127
+ tapeo sim --testbench uart # a testbench named in tapeo.json
128
+ tapeo manifest # what tapeo.json resolves to: sources, top, testbenches
129
+ tapeo sims # recent simulations
130
+ tapeo wave --open # save the latest waveform (.tapeo/waveforms/) and open it in Surfer or GTKWave
131
+ tapeo goals list # the goals and how each is measured
132
+ tapeo goals add fpga_logic_utilization between 0..80 % --note "room to grow"
133
+ tapeo goals remove fpga_logic_utilization
134
+ tapeo goals edit # write .tapeo/goals.json; edit it, then: tapeo goals save
94
135
  ```
95
136
 
137
+ Goals you add are measured once a full workflow run plans how (it keeps your goals and adds the measuring step). Checkpoints, restores, goal edits, and target or process changes are the same actions as in the browser and show up there.
138
+
96
139
  `tapeo new` asks the Architect's clarifying questions and lets you pick an option when run in a terminal. For scripts, `--auto-answers` answers "Not sure — you choose", `--option N` picks a design, `--yes` confirms the build (one build from your plan), and `--no-build` creates the project without building. After the build, the generated files are pulled into the folder. The Architect conversation is not saved on the server; the project keeps the chosen requirements and design.
97
140
 
98
141
  The Engineer reads the saved server copy of your files (`tapeo ask` warns when local edits are not pushed). Its proposed changes are kept in `.tapeo/proposals.json` until you apply or discard them, and a new answer with changes replaces them. Applying follows the browser's rules: a new file must not exist yet, and the code being replaced must occur exactly once. A change that no longer fits is marked failed with the reason and can be retried after you fix the file. Changes to tool outputs (`generated/`, `signoff/`) or paths outside the project are refused. Deleting a file applies locally; `tapeo rm` deletes it on the server. If the Engineer also proposed an evaluation plan, review it in the web workspace.
99
142
 
100
- `tapeo problems [run-id]` reads the run's tool logs and lists file-level errors from every tool run, plus warnings from tool runs that failed. It recognizes Verilator, Icarus Verilog, Yosys, and Tcl script locations and matches log paths to project files by their ending. Messages without a project file and line stay in `tapeo logs`.
143
+ `tapeo lint` runs only the lint stage (Verilator on the design RTL; testbenches and FPGA board wrappers are left out) on the files saved in Tapeo, so push first. It prints errors, warnings that usually mean a bug (widths, latches, multiple drivers), and style notes such as unused signals, and exits with an error when lint finds errors. **Lint Design** does the same in VS Code and fills the Problems panel. Every full workflow run also lints first.
144
+
145
+ `tapeo problems [run-id]` reads the run's tool logs and lists file-level errors from every tool run, plus warnings from tool runs that failed and from lint. Lint style notes (unused signals, zero-extended widths) are labelled `note`, and appear as information in VS Code's Problems panel. It recognizes Verilator, Icarus Verilog, Yosys, and Tcl script locations and matches log paths to project files by their ending. Messages without a project file and line stay in `tapeo logs`.
101
146
 
102
147
  Stages: `simulation`, `synthesis`, `timing`, `physical`, `post-route-timing`, `drc`, `extraction`, `erc`, `lvs`. `--no-ai-repairs` turns off AI repairs for a stage run. A full run uses one build from your plan; a stage run does not. `tapeo run --watch` exits with code 3 when the run fails.
103
148
 
@@ -131,6 +176,13 @@ Run `npm run build:vscode`, open `clients/vscode` in VS Code, and press F5; or r
131
176
  - **Open AI Engineer** opens the **Tapeo Engineer** panel beside the editor: the project conversation, streamed answers, and the proposed changes with **Review** (a side-by-side diff), **Apply**, **Apply all**, and **Discard**. **Ask the AI Engineer**, **Review Proposed Changes**, **Apply All Proposed Changes**, and **Discard Proposed Changes** do the same from the Command Palette. Applying is refused while the file has unsaved edits; push afterwards.
132
177
  - **Program FPGA Board** (FPGA projects): downloads the bitstream and programs the connected board with `openFPGALoader` in a **Tapeo FPGA** terminal.
133
178
  - **Open Serial Monitor** (macOS and Linux): shows the board's serial output in a terminal and sends the lines you type.
179
+ - **Protect File from AI Repairs** / **Allow AI Repairs of File**: the open file's protection, like the lock icon in the browser.
180
+ - **Save Checkpoint** and **Restore File from Checkpoint** (pick a checkpoint; the open file's version from it is written and saved as your edit).
181
+ - **Show Project Manifest**: what `tapeo.json` resolves to. Editing `tapeo.json` in VS Code has completion and checks.
182
+ - **Simulate** (no build; the result in the Tapeo output) and **Open Waveform** (saves the VCD under `.tapeo/waveforms/`; a waveform extension such as VaporView or Surfer shows it as signals).
183
+ - **Edit Goals** opens `.tapeo/goals.json`; **Save Goals** uploads it.
184
+ - **Change Target** (simulation, an FPGA board with its design clock, a chip, or both) and **Change Process**.
185
+ - **Lint Design**: lint the pushed design RTL in seconds (no build) and show its errors and warnings in the Problems panel.
134
186
  - **Show Tool Problems**: the latest run's tool errors and warnings in the Problems panel. This also happens automatically when a run the status bar follows ends, and starting a run clears them.
135
187
 
136
188
  The setting `tapeo.serverUrl` holds the server (Connect to Server sets it; remote servers must use HTTPS).
package/cli/tapeo.mjs CHANGED
@@ -8,6 +8,7 @@ import { assertUnbound, designFilesFrom, downloadFile, localChanges, moveProject
8
8
  import { applyProposals, changeDiff, describeChange, discardProposals, loadProposals, saveProposals } from "../shared/proposals.mjs";
9
9
  import { collectRunProblems, formatProblem } from "../shared/problems.mjs";
10
10
  import { NOT_SURE, clarificationAnswersMessage } from "../shared/stream.mjs";
11
+ import { GOALS_FILE, describeGoal, describeManifest, goalFromArguments, readGoalsFile, removeLocalOutputs, restoreFromCheckpoint, saveWaveform, writeGoalsFile } from "../shared/project-tools.mjs";
11
12
  import { chooseSerialPort, openSerialPort, parseBaud, serialCandidates } from "../shared/serial.mjs";
12
13
  import { OPENFPGALOADER_INSTALL, chooseBitstream, downloadBitstream, flashArguments, openFpgaLoaderAvailable, openFpgaLoaderExecutable } from "../shared/fpga.mjs";
13
14
  import { createInterface } from "node:readline/promises";
@@ -39,6 +40,20 @@ Projects and files
39
40
  tapeo rm <path> Delete a file on the server and locally
40
41
  tapeo mv <from> <to> Rename a file on the server and locally
41
42
  tapeo download <path> [dest] Download one project file (any size, e.g. a GDS layout)
43
+ tapeo manifest What tapeo.json resolves to: sources, top, testbenches
44
+ tapeo lock <path...> Protect files from automatic AI repairs (suggestions only)
45
+ tapeo unlock <path...> Let the AI Engineer repair these files during runs again
46
+
47
+ Checkpoints
48
+ tapeo checkpoints Saved checkpoints, newest first
49
+ tapeo checkpoint ["name"] Save a checkpoint of the files saved in Tapeo now
50
+ tapeo restore <n> <path...> Put back a file's version from checkpoint n (saved as your edit)
51
+
52
+ Settings
53
+ tapeo target [simulation|fpga|asic|fpga-asic] [--board <id>] [--clock <MHz>]
54
+ Show or change where the design runs (board and design clock)
55
+ tapeo process [id] Show or change the chip process (clears the old process's
56
+ flows, netlists, and layouts; the next run rebuilds them)
42
57
 
43
58
  AI
44
59
  tapeo new [description] [dir] Design a new chip with the Architect, then build it
@@ -50,6 +65,8 @@ AI
50
65
  tapeo apply [n...] Apply proposed changes to local files (all by default)
51
66
  tapeo discard Forget the proposed changes
52
67
  tapeo problems [run-id] Tool errors and warnings as file:line (latest run by default)
68
+ tapeo lint Lint the pushed design RTL with Verilator (seconds; does not
69
+ use a build) and list its problems as file:line
53
70
 
54
71
  Workflow
55
72
  tapeo run [--stage <s> | --from <s>] [--no-ai-repairs] [--watch]
@@ -59,6 +76,16 @@ Workflow
59
76
  tapeo show [run-id] Stage results, AI changes, and tool runs of a run
60
77
  tapeo logs <tool-run-id> A tool run's saved output
61
78
  tapeo goals The latest goal report
79
+ tapeo goals list The goals and how each is measured
80
+ tapeo goals add <key> <op> <value> [unit] [--note "…"]
81
+ Add a goal (op: "<=", ">=", "=", between low..high)
82
+ tapeo goals remove <key> Remove a goal
83
+ tapeo goals edit | save Write the goals to .tapeo/goals.json to edit, then save them
84
+ tapeo sim [--testbench <name>] Simulate the saved files now (no workflow run, no build);
85
+ --testbench picks one named in tapeo.json
86
+ tapeo sims Recent simulations
87
+ tapeo wave [sim-id] [--out <file>] [--open]
88
+ Save a simulation's waveform (VCD); --open starts Surfer or GTKWave
62
89
  tapeo cancel [run-id] Cancel a queued or running workflow
63
90
  tapeo open Print this project's web address
64
91
 
@@ -71,7 +98,7 @@ FPGA boards
71
98
  Show the board's serial (UART) output and send what you
72
99
  type; default 115200 baud (macOS and Linux)
73
100
 
74
- Stages: simulation, synthesis, timing, physical, post-route-timing, drc, extraction, erc, lvs.
101
+ Stages: lint, simulation, synthesis, timing, physical, post-route-timing, drc, extraction, erc, lvs.
75
102
  TAPEO_URL and TAPEO_TOKEN override the saved sign-in (for automation).`);
76
103
  }
77
104
 
@@ -319,7 +346,7 @@ async function main() {
319
346
  return;
320
347
  }
321
348
  const client = await configuredClient();
322
- const { flags, positional: args } = parseArguments(rest, ["stage", "from", "option", "limit", "board"]);
349
+ const { flags, positional: args } = parseArguments(rest, ["stage", "from", "option", "limit", "board", "clock", "note", "out", "testbench"]);
323
350
  if (command === "new") {
324
351
  await newDesign(client, args[0], path.resolve(args[1] || "."), flags);
325
352
  } else if (command === "ask") {
@@ -360,6 +387,25 @@ async function main() {
360
387
  } else if (command === "discard") {
361
388
  await discardProposals(process.cwd());
362
389
  console.log("Proposed changes discarded.");
390
+ } else if (command === "lint") {
391
+ const directory = process.cwd();
392
+ const projectId = await projectIdFor(client);
393
+ const changes = await localChanges(directory).catch(() => null);
394
+ if (changes && (changes.modified.length || changes.added.length)) console.log(`Note: lint reads the files saved in Tapeo; ${changes.modified.length + changes.added.length} local change(s) are not pushed yet (tapeo push --all).`);
395
+ let run;
396
+ try {
397
+ ({ run } = await client.run(projectId, stageScope({ stage: "lint", aiRepairs: false })));
398
+ } catch (error) {
399
+ if (error?.status === 400 && /Unknown workflow stage/i.test(error.message)) throw new Error("This Tapeo server does not lint yet. Run the full workflow instead (tapeo run --watch), or ask the server owner to update it.");
400
+ throw error;
401
+ }
402
+ const final = await watchRun(client, projectId, run.id, {});
403
+ const stage = final.run.details?.stages?.find((item) => item.label === "RTL lint");
404
+ console.log(stage ? `${stage.status === "completed" ? "✓" : stage.status === "issues" ? "!" : stage.status === "skipped" ? "–" : "✗"} ${stage.summary}` : `Lint run ${run.id} ended: ${final.run.status}.`);
405
+ const { files } = await client.project(projectId);
406
+ const problems = await collectRunProblems(client, projectId, run.id, files.map((file) => file.path));
407
+ for (const problem of problems) console.log(formatProblem(problem));
408
+ if (stage?.status === "failed") process.exitCode = 1;
363
409
  } else if (command === "problems") {
364
410
  const projectId = await projectIdFor(client);
365
411
  const runId = await client.latestRunId(projectId, args[0]);
@@ -367,7 +413,8 @@ async function main() {
367
413
  const problems = await collectRunProblems(client, projectId, runId, files.map((file) => file.path));
368
414
  for (const problem of problems) console.log(formatProblem(problem));
369
415
  const errors = problems.filter((problem) => problem.severity === "error").length;
370
- console.log(problems.length ? `\n${errors} error(s), ${problems.length - errors} warning(s) in run ${runId}.` : `No file-level problems in run ${runId}'s tool logs. See tapeo show for stage results.`);
416
+ const notes = problems.filter((problem) => problem.severity === "note").length;
417
+ console.log(problems.length ? `\n${errors} error(s), ${problems.length - errors - notes} warning(s)${notes ? `, ${notes} note(s)` : ""} in run ${runId}.` : `No file-level problems in run ${runId}'s tool logs. See tapeo show for stage results.`);
371
418
  } else if (command === "projects") {
372
419
  const { projects } = await client.projects();
373
420
  for (const project of projects) console.log(`${project.id}\t${project.name}`);
@@ -475,9 +522,128 @@ async function main() {
475
522
  if (toolRun.stdout) console.log(`--- stdout ---\n${toolRun.stdout}`);
476
523
  if (toolRun.stderr) console.log(`--- stderr ---\n${toolRun.stderr}`);
477
524
  if (!toolRun.stdout && !toolRun.stderr) console.log("This tool run saved no output.");
525
+ } else if (command === "goals" && ["list", "add", "remove", "edit", "save"].includes(args[0])) {
526
+ const directory = process.cwd();
527
+ const projectId = await projectIdFor(client);
528
+ const [action, ...rest] = args;
529
+ if (action === "edit") {
530
+ const { plan } = await client.goalPlan(projectId);
531
+ const target = await writeGoalsFile(directory, plan);
532
+ console.log(`Wrote ${plan.goals.length} goal(s) to ${path.relative(directory, target).split(path.sep).join("/")}. Edit it, then run tapeo goals save.`);
533
+ } else if (action === "save") {
534
+ const plan = await readGoalsFile(directory);
535
+ const saved = await client.saveGoalPlan(projectId, plan);
536
+ console.log(`Saved ${saved.plan.goals.length} goal(s). The goal report is out of date until the next run measures them.`);
537
+ } else {
538
+ const { plan } = await client.goalPlan(projectId);
539
+ if (action === "list") {
540
+ for (const goal of plan.goals) console.log(` ${describeGoal(goal)}`);
541
+ if (!plan.goals.length) console.log("No goals yet. Add one with tapeo goals add, or run the workflow to plan them.");
542
+ } else if (action === "add") {
543
+ const goal = goalFromArguments(rest, typeof flags.note === "string" ? flags.note : undefined);
544
+ if (plan.goals.some((item) => item.key === goal.key)) throw new Error(`A goal for ${goal.key} exists. Remove it first, or edit it with tapeo goals edit.`);
545
+ await client.saveGoalPlan(projectId, { ...plan, goals: [...plan.goals, goal] });
546
+ console.log(`Added: ${describeGoal(goal)}.`);
547
+ } else {
548
+ if (!rest[0]) throw new Error("Usage: tapeo goals remove <key>");
549
+ const kept = plan.goals.filter((goal) => goal.key !== rest[0] && goal.id !== rest[0]);
550
+ if (kept.length === plan.goals.length) throw new Error(`No goal named ${rest[0]}. List them with tapeo goals list.`);
551
+ await client.saveGoalPlan(projectId, { ...plan, goals: kept });
552
+ console.log(`Removed ${rest[0]}.`);
553
+ }
554
+ }
478
555
  } else if (command === "goals") {
479
556
  const projectId = await projectIdFor(client, args[0]);
480
557
  console.log(describeGoals((await client.goals(projectId)).report));
558
+ } else if (command === "lock" || command === "unlock") {
559
+ const projectId = await projectIdFor(client);
560
+ if (!args.length) throw new Error(`Usage: tapeo ${command} <path...>`);
561
+ for (const file of args) {
562
+ await client.lockFile(projectId, file, command === "lock");
563
+ console.log(command === "lock" ? `Protected ${file}: AI repairs become suggestions you review.` : `Unprotected ${file}: the AI Engineer may repair it during runs.`);
564
+ }
565
+ } else if (command === "checkpoints") {
566
+ const projectId = await projectIdFor(client);
567
+ const { checkpoints } = await client.checkpoints(projectId);
568
+ for (const item of checkpoints) console.log(` ${String(item.version).padStart(3)} ${new Date(item.createdAt).toLocaleString()} ${item.note ?? ""}`);
569
+ if (!checkpoints.length) console.log("No checkpoints yet. Save one with tapeo checkpoint \"name\".");
570
+ } else if (command === "checkpoint") {
571
+ const directory = process.cwd();
572
+ const projectId = await projectIdFor(client);
573
+ const changes = await localChanges(directory).catch(() => null);
574
+ if (changes && (changes.modified.length || changes.added.length)) console.log(`Note: the checkpoint holds the files saved in Tapeo; ${changes.modified.length + changes.added.length} local change(s) are not pushed yet.`);
575
+ const { checkpoint } = await client.saveCheckpoint(projectId, args.join(" ") || undefined);
576
+ console.log(`Saved checkpoint ${checkpoint.version}: ${checkpoint.note}.`);
577
+ } else if (command === "restore") {
578
+ const projectId = await projectIdFor(client);
579
+ const version = Number(args[0]);
580
+ if (!Number.isSafeInteger(version) || version < 1 || args.length < 2) throw new Error("Usage: tapeo restore <checkpoint> <path...> (list checkpoints with tapeo checkpoints)");
581
+ for (const item of await restoreFromCheckpoint(client, projectId, process.cwd(), version, args.slice(1))) console.log(`Restored ${item.path} from checkpoint ${version} (${item.sha256.slice(0, 12)}).`);
582
+ } else if (command === "sim") {
583
+ const directory = process.cwd();
584
+ const projectId = await projectIdFor(client);
585
+ const changes = await localChanges(directory).catch(() => null);
586
+ if (changes && (changes.modified.length || changes.added.length)) console.log(`Note: this simulates the files saved in Tapeo; ${changes.modified.length + changes.added.length} local change(s) are not pushed yet.`);
587
+ console.log("Simulating…");
588
+ const result = await client.simulate(projectId, typeof flags.testbench === "string" ? flags.testbench : undefined);
589
+ console.log(`${result.status === "passed" ? "✓" : "✗"} ${result.message}${result.testbenchPath ? ` (${result.testbenchPath})` : ""}`);
590
+ for (const metric of result.metrics ?? []) console.log(` ${metric.key} = ${metric.value} ${metric.unit}`);
591
+ if (result.status !== "passed") console.log(String([result.stdout, result.stderr].filter(Boolean).join("\n")).split("\n").filter(Boolean).slice(-15).join("\n"));
592
+ if (result.hasWaveform) console.log(`Waveform: tapeo wave ${result.runId}`);
593
+ if (result.status !== "passed") process.exitCode = 1;
594
+ } else if (command === "manifest") {
595
+ const projectId = await projectIdFor(client);
596
+ const { manifest } = await client.manifest(projectId);
597
+ for (const line of describeManifest(manifest)) console.log(line);
598
+ if (manifest?.errors.length) process.exitCode = 1;
599
+ } else if (command === "sims") {
600
+ const projectId = await projectIdFor(client);
601
+ const { simulations } = await client.simulations(projectId);
602
+ for (const run of simulations) console.log(` ${run.runId} ${run.status.padEnd(7)} ${new Date(run.createdAt).toLocaleString()} ${run.message}${run.hasWaveform ? " · waveform" : ""}`);
603
+ if (!simulations.length) console.log("No simulations yet. Run tapeo sim.");
604
+ } else if (command === "wave") {
605
+ const directory = process.cwd();
606
+ const projectId = await projectIdFor(client);
607
+ let runId = args[0];
608
+ if (!runId) {
609
+ runId = (await client.simulations(projectId)).simulations.find((run) => run.hasWaveform)?.runId;
610
+ if (!runId) throw new Error("No simulation with a waveform yet. Run tapeo sim.");
611
+ }
612
+ const file = await saveWaveform(client, projectId, runId, directory, typeof flags.out === "string" ? flags.out : undefined);
613
+ console.log(`Saved the waveform to ${path.relative(directory, file).split(path.sep).join("/") || file}.`);
614
+ if (flags.open) {
615
+ const viewer = ["surfer", "gtkwave"].find((name) => spawnSync(name, ["--version"], { stdio: "ignore" }).status === 0);
616
+ if (!viewer) console.log("Install Surfer (https://surfer-project.org) or GTKWave to open it, or open it in the web workspace.");
617
+ else { spawn(viewer, [file], { detached: true, stdio: "ignore" }).unref(); console.log(`Opened it in ${viewer}.`); }
618
+ }
619
+ } else if (command === "target" || command === "process") {
620
+ const projectId = await projectIdFor(client);
621
+ const describe = (settings, choices) => {
622
+ const board = choices.boards.find((item) => item.id === settings.fpgaBoard);
623
+ const platform = choices.platforms.find((item) => item.id === settings.technologyPlatform);
624
+ return `Target: ${choices.targets.find((item) => item.id === settings.target)?.label ?? settings.target}${board && settings.target.startsWith("fpga") ? ` · ${board.label}${settings.fpgaClockMhz ? ` · ${settings.fpgaClockMhz} MHz design clock (PLL)` : ` · ${board.clockMhz} MHz board clock`}` : ""}${settings.target === "asic" || settings.target === "fpga-asic" ? ` · process ${platform?.label ?? (settings.technology || "not chosen")}` : ""}`;
625
+ };
626
+ const { settings, choices } = await client.settings(projectId);
627
+ if (!args.length) {
628
+ console.log(describe(settings, choices));
629
+ if (command === "target") {
630
+ console.log(`Targets: ${choices.targets.map((item) => item.id).join(", ")}`);
631
+ console.log(`Boards:\n${choices.boards.map((item) => ` ${item.id.padEnd(12)} ${item.label} · ${item.part} · ${item.clockMhz} MHz (PLL ${item.pllMinMhz}-${item.pllMaxMhz} MHz)`).join("\n")}`);
632
+ } else {
633
+ console.log(`Processes:\n${choices.platforms.map((item) => ` ${item.id.padEnd(14)} ${item.label}${item.oneClick ? "" : ` · ${item.blocker ?? "manual setup"}`}`).join("\n")}`);
634
+ }
635
+ } else {
636
+ const change = command === "target"
637
+ ? { target: args[0], ...(typeof flags.board === "string" ? { fpgaBoard: flags.board } : {}), ...(typeof flags.clock === "string" ? { fpgaClockMhz: Number(flags.clock) } : {}) }
638
+ : { technologyPlatform: args[0] };
639
+ const saved = await client.saveSettings(projectId, change);
640
+ console.log(`Saved. ${describe(saved.settings, choices)}`);
641
+ if (saved.removed?.length) {
642
+ const local = await removeLocalOutputs(process.cwd(), saved.removed);
643
+ console.log(`Removed ${saved.removed.length} file(s) from the old process: ${saved.removed.join(", ")}${local.length ? ` (and ${local.length} local cop${local.length === 1 ? "y" : "ies"})` : ""}. Run the workflow to rebuild them for the new process.`);
644
+ }
645
+ else console.log("Run the workflow to build for the new settings (tapeo run --watch).");
646
+ }
481
647
  } else if (command === "cancel") {
482
648
  const projectId = await projectIdFor(client);
483
649
  let runId = args[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tapeo-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Terminal client for Tapeo: edit hardware projects locally, run the server's EDA workflow, read results, and work with the AI Architect and Engineer.",
5
5
  "keywords": [
6
6
  "tapeo",
package/shared/api.mjs CHANGED
@@ -5,7 +5,7 @@ export class TapeoApiError extends Error {
5
5
  }
6
6
 
7
7
  /** Workflow stages a run can start from or stop at (lib/workflow-stages.ts). */
8
- export const WORKFLOW_STAGE_KEYS = ["simulation", "synthesis", "timing", "physical", "post-route-timing", "drc", "extraction", "erc", "lvs"];
8
+ export const WORKFLOW_STAGE_KEYS = ["lint", "simulation", "synthesis", "timing", "physical", "post-route-timing", "drc", "extraction", "erc", "lvs"];
9
9
  export const ACTIVE_RUN_STATUSES = ["queued", "running"];
10
10
 
11
11
  /** Keep CLI and VS Code status wording consistent with the persisted run. */
@@ -78,7 +78,8 @@ export function normalizeServerUrl(value) {
78
78
  return url.origin;
79
79
  }
80
80
 
81
- async function send(serverUrl, method, path, { token, body, raw = false } = {}) {
81
+ async function send(serverUrl, method, path, { token, body, raw = false, timeoutMs } = {}) {
82
+ const limit = timeoutMs ?? (raw ? 300_000 : 20_000);
82
83
  let response;
83
84
  try {
84
85
  response = await fetch(new URL(`/api/client/v1${path}`, serverUrl), {
@@ -86,10 +87,10 @@ async function send(serverUrl, method, path, { token, body, raw = false } = {})
86
87
  headers: { "X-Tapeo-Client": clientIdentity, ...(token ? { Authorization: `Bearer ${token}` } : {}), ...(body === undefined ? {} : { "Content-Type": "application/json" }) },
87
88
  ...(body === undefined ? {} : { body: JSON.stringify(body) }),
88
89
  redirect: "error",
89
- signal: AbortSignal.timeout(raw ? 300_000 : 20_000)
90
+ signal: AbortSignal.timeout(limit)
90
91
  });
91
92
  } catch (error) {
92
- const detail = error?.name === "TimeoutError" ? `The server did not answer within ${raw ? 300 : 20} seconds.` : "Could not reach the server.";
93
+ const detail = error?.name === "TimeoutError" ? `The server did not answer within ${Math.round(limit / 1000)} seconds.` : "Could not reach the server.";
93
94
  throw new TapeoApiError(`${detail} Check ${serverUrl} and retry; local files were not changed.`, 0);
94
95
  }
95
96
  if (raw && response.ok) return response;
@@ -136,7 +137,7 @@ export class TapeoClient {
136
137
  this.token = token;
137
138
  }
138
139
 
139
- request(method, path, body) { return send(this.serverUrl, method, path, { token: this.token, body }); }
140
+ request(method, path, body, options = {}) { return send(this.serverUrl, method, path, { token: this.token, body, ...options }); }
140
141
  /** One file as bytes (text or binary), with its SHA-256 and origin. */
141
142
  async rawFile(id, path) {
142
143
  const response = await send(this.serverUrl, "GET", `/projects/${encodeURIComponent(id)}/files/raw?path=${encodeURIComponent(path)}`, { token: this.token, raw: true });
@@ -196,6 +197,37 @@ export class TapeoClient {
196
197
  saveFile(id, path, content, baseSha256, language) { return this.request("PUT", `/projects/${encodeURIComponent(id)}/files?path=${encodeURIComponent(path)}`, { content, baseSha256, language }); }
197
198
  deleteFile(id, path, baseSha256) { return this.request("DELETE", `/projects/${encodeURIComponent(id)}/files?path=${encodeURIComponent(path)}&baseSha256=${encodeURIComponent(baseSha256)}`); }
198
199
  runs(id) { return this.request("GET", `/projects/${encodeURIComponent(id)}/runs`); }
200
+
201
+ // ---- Project tools that were browser-only (lib/client-delegate.ts on the server) ----
202
+ /** Older servers have no route for these; say so plainly instead of a bare 404/405. */
203
+ async newerRequest(feature, method, path, body, options) {
204
+ try {
205
+ return await this.request(method, path, body, options);
206
+ } catch (error) {
207
+ if ((error?.status === 404 && !error?.data?.error) || error?.status === 405) throw new TapeoApiError(`This Tapeo server does not support ${feature} from local clients yet. Use the web workspace: ${this.serverUrl}`, error.status);
208
+ throw error;
209
+ }
210
+ }
211
+ /** Protect a file from automatic AI repairs, or allow them. */
212
+ lockFile(id, path, locked) { return this.newerRequest("protecting files", "PATCH", `/projects/${encodeURIComponent(id)}/files/lock`, { path, locked }); }
213
+ checkpoints(id) { return this.newerRequest("checkpoints", "GET", `/projects/${encodeURIComponent(id)}/checkpoints`); }
214
+ saveCheckpoint(id, note) { return this.newerRequest("checkpoints", "POST", `/projects/${encodeURIComponent(id)}/checkpoints`, { note }); }
215
+ checkpoint(id, version) { return this.newerRequest("checkpoints", "GET", `/projects/${encodeURIComponent(id)}/checkpoints/${encodeURIComponent(version)}`); }
216
+ checkpointFile(id, version, path) { return this.newerRequest("checkpoints", "GET", `/projects/${encodeURIComponent(id)}/checkpoints/${encodeURIComponent(version)}?path=${encodeURIComponent(path)}`); }
217
+ /** Simulate the saved files now, without a workflow run. */
218
+ /** A simulation can take minutes (a large design, or the runner starting), so it may wait 5 minutes. */
219
+ simulate(id, testbench) { return this.newerRequest("quick simulation", "POST", `/projects/${encodeURIComponent(id)}/simulations`, testbench ? { testbench } : {}, { timeoutMs: 300_000 }); }
220
+ /** What tapeo.json resolves to on the saved files (null when there is none). */
221
+ manifest(id) { return this.newerRequest("project manifests", "GET", `/projects/${encodeURIComponent(id)}/manifest`); }
222
+ simulations(id) { return this.newerRequest("quick simulation", "GET", `/projects/${encodeURIComponent(id)}/simulations`); }
223
+ async waveform(id, runId) {
224
+ const response = await send(this.serverUrl, "GET", `/projects/${encodeURIComponent(id)}/simulations/${encodeURIComponent(runId)}/waveform`, { token: this.token, raw: true });
225
+ return Buffer.from(await response.arrayBuffer());
226
+ }
227
+ goalPlan(id) { return this.newerRequest("editing goals", "GET", `/projects/${encodeURIComponent(id)}/goals?plan=1`); }
228
+ saveGoalPlan(id, evaluationPlan) { return this.newerRequest("editing goals", "PUT", `/projects/${encodeURIComponent(id)}/goals`, { evaluationPlan }); }
229
+ settings(id) { return this.newerRequest("project settings", "GET", `/projects/${encodeURIComponent(id)}/settings`); }
230
+ saveSettings(id, settings) { return this.newerRequest("project settings", "PUT", `/projects/${encodeURIComponent(id)}/settings`, settings); }
199
231
  runDetail(id, runId) { return this.request("GET", `/projects/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}`); }
200
232
  cancelRun(id, runId) { return this.request("POST", `/projects/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/cancel`, {}); }
201
233
  toolRunLog(id, toolRunId) { return this.request("GET", `/projects/${encodeURIComponent(id)}/tool-runs/${encodeURIComponent(toolRunId)}`); }
@@ -10,9 +10,26 @@
10
10
  * Paths in logs may be absolute (the tool's workspace); they are matched to
11
11
  * project files by their ending. Lines without a project file are left out.
12
12
  */
13
+ /**
14
+ * Verilator warnings that are style rather than likely bugs, shown as notes.
15
+ * Mirrors LINT_STYLE_CODES in lib/rtl-lint.ts (tests/client-ai.test.ts keeps them equal).
16
+ */
17
+ export const VERILATOR_STYLE_CODES = new Set([
18
+ "UNUSED", "UNUSEDSIGNAL", "UNUSEDPARAM", "UNUSEDGENVAR", "DECLFILENAME", "PINCONNECTEMPTY", "PINNOCONNECT", "VARHIDDEN", "GENUNNAMED",
19
+ "WIDTHEXPAND", "EOFNEWLINE", "PROCASSINIT", "LITENDIAN", "ASSIGNDLY", "INCABSPATH", "IMPORTSTAR", "DEFPARAM", "STMTDLY", "BLKLOOPINIT", "SYMRSVDWORD", "TIMESCALEMOD", "REALCVT"
20
+ ]);
21
+
22
+ /** Verilator severity: errors; a warning that loses bits or is otherwise a likely bug; or a style note (as lib/rtl-lint.ts classifies them). */
23
+ function verilatorSeverity(kind, code, message) {
24
+ if (kind === "Error") return "error";
25
+ const width = code === "WIDTH" ? /expects (\d+) bits?.*generates (\d+) bits?/i.exec(message) : null;
26
+ const effective = width ? (Number(width[2]) > Number(width[1]) ? "WIDTHTRUNC" : "WIDTHEXPAND") : code;
27
+ return VERILATOR_STYLE_CODES.has(effective) ? "note" : "warning";
28
+ }
29
+
13
30
  const PATTERNS = [
14
31
  // Verilator: %Error: path:line:col: message (also %Warning-CODE)
15
- { regex: /%(Error|Warning)(?:-[A-Z0-9_]+)?:\s*([^\s:]+):(\d+):(?:(\d+):)?\s*(.*)$/, map: (m) => ({ severity: m[1], path: m[2], line: m[3], column: m[4], message: m[5] }) },
32
+ { regex: /%(Error|Warning)(?:-([A-Z0-9_]+))?:\s*([^\s:]+):(\d+):(?:(\d+):)?\s*(.*)$/, map: (m) => ({ severity: verilatorSeverity(m[1], m[2] ?? "", m[6]), path: m[3], line: m[4], column: m[5], message: m[6] }) },
16
33
  // path:line[:col]: [error|warning|ERROR|syntax error]: message (Icarus, Yosys, GCC-style)
17
34
  { regex: /(?:^|\s)([^\s:'"()]+\.[A-Za-z0-9]+):(\d+)(?::(\d+))?:\s*(?:(error|warning|ERROR|WARNING|Error|Warning)\b:?\s*)?(.*)$/, map: (m) => ({ severity: m[4] ?? (/warn/i.test(m[5]) ? "warning" : "error"), path: m[1], line: m[2], column: m[3], message: m[5] }) },
18
35
  // Yosys location at the end: "... at rtl/a.sv:12.3-12.10"
@@ -46,8 +63,8 @@ export function extractProblems(text, projectPaths, { tool = "", includeWarnings
46
63
  const found = pattern.map(match);
47
64
  const path = matchProjectPath(found.path, projectPaths);
48
65
  if (!path) continue;
49
- const severity = /warn/i.test(found.severity) ? "warning" : "error";
50
- if (severity === "warning" && !includeWarnings) break;
66
+ const severity = found.severity === "note" ? "note" : /warn/i.test(found.severity) ? "warning" : "error";
67
+ if (severity !== "error" && !includeWarnings) break;
51
68
  const message = (found.message || line).trim().replace(/^:\s*/, "") || line;
52
69
  const problem = { path, line: Math.max(1, Number(found.line)), ...(found.column ? { column: Math.max(1, Number(found.column)) } : {}), severity, message: message.slice(0, 500), ...(tool ? { tool } : {}) };
53
70
  const key = `${problem.path}:${problem.line}:${problem.message}`;
@@ -61,7 +78,8 @@ export function extractProblems(text, projectPaths, { tool = "", includeWarnings
61
78
 
62
79
  /**
63
80
  * Problems from a run's tool logs: every tool run's errors, and warnings
64
- * from the tool runs that failed. Returns them grouped by file order.
81
+ * from the tool runs that failed, or from lint (whose warnings are its result).
82
+ * Returns them grouped by file order.
65
83
  */
66
84
  export async function collectRunProblems(client, projectId, runId, projectPaths) {
67
85
  const { toolRuns = [] } = await client.runDetail(projectId, runId);
@@ -69,7 +87,7 @@ export async function collectRunProblems(client, projectId, runId, projectPaths)
69
87
  for (const tool of toolRuns.slice(-12)) {
70
88
  const failed = !/^(passed|completed|success)/i.test(tool.status ?? "");
71
89
  const { toolRun } = await client.toolRunLog(projectId, tool.id);
72
- problems.push(...extractProblems(`${toolRun.summary ?? ""}\n${toolRun.stdout ?? ""}\n${toolRun.stderr ?? ""}`, projectPaths, { tool: toolRun.tool, includeWarnings: failed }));
90
+ problems.push(...extractProblems(`${toolRun.summary ?? ""}\n${toolRun.stdout ?? ""}\n${toolRun.stderr ?? ""}`, projectPaths, { tool: toolRun.tool, includeWarnings: failed || /\blint\b/i.test(toolRun.tool ?? tool.tool ?? "") }));
73
91
  }
74
92
  const seen = new Set();
75
93
  return problems.filter((problem) => { const key = `${problem.path}:${problem.line}:${problem.message}`; if (seen.has(key)) return false; seen.add(key); return true; })
@@ -0,0 +1,113 @@
1
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { localTarget, pushProjectFile, safeSourcePath } from "./sync.mjs";
4
+
5
+ /**
6
+ * Project tools shared by the CLI and VS Code that used to need the browser:
7
+ * restoring files from a checkpoint, editing goals as a file, and saving a
8
+ * simulation's waveform. Everything goes through the token API.
9
+ */
10
+
11
+ /** Put a checkpoint's version of each file back: written locally, then saved in Tapeo as your edit (like Restore in Project history). */
12
+ export async function restoreFromCheckpoint(client, projectId, directory, version, paths) {
13
+ const restored = [];
14
+ for (const projectPath of paths) {
15
+ if (!safeSourcePath(projectPath)) throw new Error(`${projectPath} cannot be restored: tool outputs (generated/, signoff/) are rebuilt by runs, not restored.`);
16
+ const { file } = await client.checkpointFile(projectId, version, projectPath);
17
+ const target = await localTarget(directory, projectPath);
18
+ await mkdir(path.dirname(target), { recursive: true });
19
+ await writeFile(target, file.content, "utf8");
20
+ const saved = await pushProjectFile(client, directory, projectPath);
21
+ restored.push({ path: projectPath, sha256: saved.sha256 });
22
+ }
23
+ return restored;
24
+ }
25
+
26
+ export const GOALS_FILE = ".tapeo/goals.json";
27
+
28
+ /** The goal plan as an editable file (.tapeo/goals.json, never pushed as a project file). */
29
+ export async function writeGoalsFile(directory, plan) {
30
+ const target = path.join(directory, GOALS_FILE);
31
+ await mkdir(path.dirname(target), { recursive: true });
32
+ await writeFile(target, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
33
+ return target;
34
+ }
35
+
36
+ export async function readGoalsFile(directory) {
37
+ let text;
38
+ try { text = await readFile(path.join(directory, GOALS_FILE), "utf8"); }
39
+ catch (error) { if (error?.code === "ENOENT") throw new Error(`No ${GOALS_FILE} yet. Run tapeo goals edit first.`); throw error; }
40
+ let plan;
41
+ try { plan = JSON.parse(text); } catch (error) { throw new Error(`${GOALS_FILE} is not valid JSON: ${error.message}`); }
42
+ if (!plan || !Array.isArray(plan.goals)) throw new Error(`${GOALS_FILE} must hold { "schemaVersion": 1, "goals": [ ... ] }.`);
43
+ return { schemaVersion: 1, ...plan };
44
+ }
45
+
46
+ const COMPARISONS = { "<=": "less-than-or-equal", ">=": "greater-than-or-equal", "=": "equal", "==": "equal" };
47
+ const SYMBOLS = { "less-than-or-equal": "≤", "greater-than-or-equal": "≥", equal: "=", between: "between" };
48
+
49
+ /**
50
+ * A goal from the command line: `<key> <= 5 ns`, `<key> >= 50 MHz`, `<key> = 1 result`,
51
+ * or `<key> between 10..20 %`. Its measurement is planned by the next full workflow run.
52
+ */
53
+ export function goalFromArguments([key, operator, target, unit = "result"], note) {
54
+ if (!key || !/^[A-Za-z0-9_.-]{1,80}$/.test(key)) throw new Error("Name the goal's measurement, e.g. tapeo goals add lint_error_count \"<=\" 0 issues");
55
+ const id = `${key}-${Date.now().toString(36)}`;
56
+ if (operator === "between") {
57
+ const match = /^(-?\d+(?:\.\d+)?)\.\.(-?\d+(?:\.\d+)?)$/.exec(target ?? "");
58
+ if (!match || Number(match[1]) > Number(match[2])) throw new Error("A range is written low..high, e.g. tapeo goals add fpga_logic_utilization between 0..80 %");
59
+ return { id, key, comparison: "between", target: Number(match[1]), upperTarget: Number(match[2]), unit, source: "user", ...(note ? { note } : {}) };
60
+ }
61
+ const comparison = COMPARISONS[operator];
62
+ if (!comparison) throw new Error(`Use <=, >=, =, or between (got ${operator ?? "nothing"}). Quote < and > in a shell: "<=".`);
63
+ const number = Number(target);
64
+ if (target === undefined || target === "") throw new Error("Give the goal's target value.");
65
+ return { id, key, comparison, target: Number.isFinite(number) ? number : target, unit, source: "user", ...(note ? { note } : {}) };
66
+ }
67
+
68
+ export function describeGoal(goal) {
69
+ const target = goal.comparison === "between" ? `${goal.target}..${goal.upperTarget}` : goal.target;
70
+ return `${goal.key} ${SYMBOLS[goal.comparison] ?? goal.comparison} ${target} ${goal.unit}${goal.verification?.steps?.length ? ` · measured by ${goal.verification.steps.map((step) => step.tool || step.adapterId).join(", ")}` : " · measurement planned on the next full run"}`;
71
+ }
72
+
73
+ /** A resolved tapeo.json as readable lines (tapeo manifest, Tapeo: Show Project Manifest). */
74
+ export function describeManifest(manifest) {
75
+ if (!manifest) return ["This project has no tapeo.json: Tapeo finds the design, top, and testbench by convention."];
76
+ const lines = [];
77
+ if (manifest.errors.length) lines.push("tapeo.json has problems that stop the stages:", ...manifest.errors.map((error) => ` ✗ ${error}`));
78
+ else {
79
+ lines.push(`Design sources (${manifest.sources.length}, in compile order):`, ...manifest.sources.map((source) => ` ${source}`));
80
+ lines.push(`Top: ${manifest.top ?? "found by Tapeo (the module nothing else instantiates)"}`);
81
+ if (manifest.includeDirs.length) lines.push(`Include folders: ${manifest.includeDirs.join(", ")}`);
82
+ if (manifest.defines.length) lines.push(`Defines: ${manifest.defines.join(", ")}`);
83
+ if (manifest.verification.length) lines.push(`Shared verification files: ${manifest.verification.join(", ")}`);
84
+ lines.push(manifest.testbenches.length ? `Testbenches: ${manifest.testbenches.map((bench) => `${bench.name} (${bench.files.join(", ")}${bench.top ? `, top ${bench.top}` : ""})`).join("; ")}` : "Testbench: found by name (tb/, *_tb.sv, …)");
85
+ if (manifest.constraints) lines.push(`Constraints: ${manifest.constraints}`);
86
+ }
87
+ for (const warning of manifest.warnings) lines.push(` ! ${warning}`);
88
+ return lines;
89
+ }
90
+
91
+ /** Save a simulation's waveform under .tapeo/waveforms/ (local only). */
92
+ export async function saveWaveform(client, projectId, runId, directory, destination) {
93
+ const bytes = await client.waveform(projectId, runId);
94
+ const target = destination ? path.resolve(directory, destination) : path.join(directory, ".tapeo", "waveforms", `${runId}.vcd`);
95
+ await mkdir(path.dirname(target), { recursive: true });
96
+ await writeFile(target, bytes);
97
+ return target;
98
+ }
99
+
100
+ /** Delete local copies of tool outputs the server removed (a process change); only generated/ and signoff/ paths. */
101
+ export async function removeLocalOutputs(directory, paths) {
102
+ const removed = [];
103
+ for (const projectPath of paths) {
104
+ if (!/^(?:generated|signoff)\//.test(projectPath) || projectPath.split("/").includes("..")) continue;
105
+ const target = await localTarget(directory, projectPath);
106
+ try {
107
+ await chmod(target, 0o644);
108
+ await rm(target);
109
+ removed.push(projectPath);
110
+ } catch (error) { if (error?.code !== "ENOENT") throw error; }
111
+ }
112
+ return removed;
113
+ }
package/shared/sync.mjs CHANGED
@@ -271,12 +271,12 @@ export async function downloadFile(client, projectId, projectPath, destination)
271
271
  }
272
272
 
273
273
  /** Design files a new design can start from: the browser's upload types and limits (20 files, 300 KB). */
274
- export const DESIGN_EXTENSIONS = [".v", ".sv", ".vh", ".svh", ".vhd", ".vhdl", ".sdc", ".tcl", ".hex", ".mem"];
274
+ export const DESIGN_EXTENSIONS = [".v", ".sv", ".vh", ".svh", ".vhd", ".vhdl", ".sdc", ".tcl", ".hex", ".mem", ".f"];
275
275
  export async function designFilesFrom(directory, { maxFiles = 20, maxBytes = 300 * 1024 } = {}) {
276
276
  const files = [];
277
277
  let total = 0;
278
278
  for (const candidate of await sourceFiles(path.resolve(directory))) {
279
- if (!DESIGN_EXTENSIONS.includes(path.extname(candidate))) continue;
279
+ if (!DESIGN_EXTENSIONS.includes(path.extname(candidate)) && candidate !== "tapeo.json") continue;
280
280
  const content = await readFile(await localTarget(directory, candidate), "utf8");
281
281
  total += Buffer.byteLength(content, "utf8");
282
282
  files.push({ path: candidate, content });