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 +54 -2
- package/cli/tapeo.mjs +169 -3
- package/package.json +1 -1
- package/shared/api.mjs +37 -5
- package/shared/problems.mjs +23 -5
- package/shared/project-tools.mjs +113 -0
- package/shared/sync.mjs +2 -2
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.
|
|
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
|
|
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
|
-
|
|
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
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(
|
|
90
|
+
signal: AbortSignal.timeout(limit)
|
|
90
91
|
});
|
|
91
92
|
} catch (error) {
|
|
92
|
-
const detail = error?.name === "TimeoutError" ? `The server did not answer within ${
|
|
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)}`); }
|
package/shared/problems.mjs
CHANGED
|
@@ -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[
|
|
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
|
|
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
|
|
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 });
|