tapeo-cli 0.0.0-stage → 0.3.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/LICENSE +11 -0
- package/README.md +101 -2
- package/cli/tapeo.mjs +444 -0
- package/package.json +33 -4
- package/shared/api.mjs +228 -0
- package/shared/problems.mjs +82 -0
- package/shared/proposals.mjs +112 -0
- package/shared/stream.mjs +62 -0
- package/shared/sync.mjs +287 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Copyright (c) 2026 Tapeo (https://tapeo.net). All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software is proprietary. You may install and use it to work with Tapeo
|
|
4
|
+
services. You may not copy, modify, redistribute, sublicense, or create
|
|
5
|
+
derivative works of it, except as permitted by law, without written permission.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
8
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
9
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
10
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING
|
|
11
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,102 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Tapeo local clients (developer preview)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
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`. They are a developer preview: installed from this repository. Publishing on npm (`tapeo-cli`) and the VS Code Marketplace (`tapeo.tapeo-vscode`) is prepared but not done; see [`docs/RELEASING.md`](../docs/RELEASING.md).
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
The server needs migrations `0008_client_tokens` and `0009_device_authorization` (`npx prisma migrate deploy`; the Docker deployment applies them on start).
|
|
8
|
+
|
|
9
|
+
## Sign in
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g ./clients # or: npm run tapeo -- <command> from the repository root
|
|
13
|
+
# once published: npm install -g tapeo-cli
|
|
14
|
+
tapeo login # https://tapeo.net; or: tapeo login https://your-own-server
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`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.
|
|
18
|
+
|
|
19
|
+
## Terminal
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
tapeo projects # your projects
|
|
23
|
+
tapeo create "My sensor" ./my-sensor # new empty project, bound to the folder
|
|
24
|
+
tapeo pull PROJECT_ID ./existing # or bind an existing project
|
|
25
|
+
cd ./my-sensor
|
|
26
|
+
tapeo status # what changed locally, and the latest run
|
|
27
|
+
tapeo push --all # push every modified and new source file
|
|
28
|
+
tapeo run --watch # full run; follow it, then show results and goals
|
|
29
|
+
tapeo run --stage drc # rerun one stage on the saved files
|
|
30
|
+
tapeo run --from synthesis # rerun a stage and every later one
|
|
31
|
+
tapeo runs # recent runs and their status
|
|
32
|
+
tapeo watch # follow the latest run until it ends, then show results
|
|
33
|
+
tapeo show # stage results, AI changes, tool runs of the latest run
|
|
34
|
+
tapeo logs TOOL_RUN_ID # a tool run's saved output (ids from tapeo show)
|
|
35
|
+
tapeo goals # the latest goal report
|
|
36
|
+
tapeo cancel # cancel the active run
|
|
37
|
+
tapeo pull --outputs # also download tool outputs (read-only)
|
|
38
|
+
tapeo download generated/layout/top.openroad.gds
|
|
39
|
+
tapeo rm rtl/old.sv # delete on the server and locally
|
|
40
|
+
tapeo mv rtl/a.sv rtl/core/a.sv # rename
|
|
41
|
+
tapeo open # the project's web address
|
|
42
|
+
tapeo --version # this client's version (tapeo help lists every command)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### AI
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
tapeo new "A sensor that warns when a tank is full" ./tank # design with the Architect, then build
|
|
49
|
+
tapeo new "Wrap my FIFO" . --from-files # start from the RTL in this folder
|
|
50
|
+
tapeo ask "Why does the counter not wrap at 15?" # ask the Engineer (answer streams)
|
|
51
|
+
tapeo proposals # its proposed changes, as diffs
|
|
52
|
+
tapeo apply # apply all of them to local files (or: tapeo apply 1 3)
|
|
53
|
+
tapeo push --all # then save them in Tapeo
|
|
54
|
+
tapeo discard # or forget them
|
|
55
|
+
tapeo history # the Engineer conversation, shared with the browser (--limit N, default 10)
|
|
56
|
+
tapeo problems # tool errors and warnings of the latest run, as file:line:col
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`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.
|
|
60
|
+
|
|
61
|
+
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.
|
|
62
|
+
|
|
63
|
+
`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`.
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
## VS Code
|
|
68
|
+
|
|
69
|
+
Run `npm run build:vscode`, open `clients/vscode` in VS Code, and press F5; or run `npm run package:vscode` and install `clients/vscode/tapeo-vscode.vsix` with **Extensions: Install from VSIX...**. In the Command Palette:
|
|
70
|
+
|
|
71
|
+
- **Tapeo: Connect to Server**: sign in with your browser (recommended) or paste a token. The token is kept in VS Code SecretStorage. **Tapeo: Sign Out** removes it.
|
|
72
|
+
- **Create Project** / **Pull Project Files** bind the open folder; **Pull Tool Outputs (read-only)** adds tool outputs.
|
|
73
|
+
- **Show Local Changes**, **Push Current File**, **Push All Changes**, **Delete Current File on the Server**.
|
|
74
|
+
- **Run Workflow** (full run, one stage, or from a stage). The status bar follows the run; when it ends, a notification offers the details, the goals, or the project in the browser.
|
|
75
|
+
- **Show Workflow Runs**, **Show Run Details**, **Open a Tool Log**, **Show Goal Results**, **Cancel Workflow**, **Open Project in Browser**.
|
|
76
|
+
- **New Design with the Architect** (in an empty folder): the Architect's questions appear as quick picks with "Not sure — you choose", then the design options and the build plan; **Build** creates the project, starts the first build, and pulls the generated files when it ends.
|
|
77
|
+
- **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.
|
|
78
|
+
- **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.
|
|
79
|
+
|
|
80
|
+
The setting `tapeo.serverUrl` holds the server (Connect to Server sets it; remote servers must use HTTPS).
|
|
81
|
+
|
|
82
|
+
In a bound folder, the status bar shows the latest run when VS Code starts and follows it if it is still running.
|
|
83
|
+
|
|
84
|
+
## How files are kept safe
|
|
85
|
+
|
|
86
|
+
- `.tapeo/state.json` records the project id, the server, and the SHA-256 of each file as last pulled or pushed. It holds no token and is ignored by Git.
|
|
87
|
+
- Pull skips `generated/`, `signoff/`, and files over 2 MB, never overwrites a local edit, and reports conflicts. When both sides changed, the server copy is saved under `.tapeo/conflicts/` for comparison.
|
|
88
|
+
- Push (one file or `--all`) sends each file with the hash it was based on. If the server copy changed meanwhile, that file is refused (HTTP 409), the local edit is kept, and the server copy is saved under `.tapeo/conflicts/`; other files still push. New files are found by source extension (`.sv`, `.v`, `.vhd`, `.sdc`, `.tcl`, `.hex`, `.mem`, `.c`, `.h`, `.S`, `.ld`, and similar) outside `generated/`, `signoff/`, hidden folders, and build folders.
|
|
89
|
+
- Deleting a file locally does not delete it on the server; `tapeo rm` does, and only the version last pulled.
|
|
90
|
+
- Tool outputs (`generated/`, `signoff/`) are read-only: they download with mode 0444 and are never pushed. Outputs over 25 MB (a layout) are listed for `tapeo download`.
|
|
91
|
+
- An expired or revoked sign-in asks you to sign in again; an unreachable server reports a network error or timeout. A run is refused before using a build when no workflow worker is connected. None of these change local files.
|
|
92
|
+
|
|
93
|
+
## Workflow runs
|
|
94
|
+
|
|
95
|
+
A run from a local client executes the saved files, including edits just pushed; it never regenerates HDL. It uses the same queue, checkpoints, plan limits, and usage accounting as the browser (`lib/workflow-runs.ts`). Cancelling stops a run before its next stage; a tool step already running finishes first, and finished stages keep their results. A full run still counts as the build it started as.
|
|
96
|
+
|
|
97
|
+
## Tests
|
|
98
|
+
|
|
99
|
+
- `tests/client-ai.test.ts`: streamed answers, clarification wording, problem extraction for each tool's format, proposals (apply rules, retry, refusals, discard), design-file limits, the client API, and the real `tapeo` command for `ask`/`proposals`/`apply`/`history`/`problems`/`discard` and `new` (questions, option, plan, build, follow, pull, bound-folder refusal).
|
|
100
|
+
- `npm test` includes `tests/client-workflow.test.ts` (shared client: sign-in, watching, status, push-all, rm/mv, outputs, formatting), `tests/client-sync.test.ts`, and `tests/client-cli.test.ts` (the real `tapeo` command against a fake server, from sign-in to cancel).
|
|
101
|
+
- `npm run test:e2e` checks the server side against a real database: browser device approval, run details, tool logs, goals, cancel (client and browser), raw download, delete with its conflict check, the Engineer and Architect routes' sign-in, validation, and ownership checks, the build plan, and creating a project from a design (with "build" refused, and no run recorded, when no worker is connected). It does not call the AI.
|
|
102
|
+
- `npm run test:vscode` drives a real VS Code Extension Development Host through connect, create, a new design with the Architect (and its refusal in a bound folder), pull, push, push all, run, tool problems in the Problems panel, run details, goals, the Engineer (ask, review diff, apply refused for an unsaved file, apply, push), and conflict, worker, and sign-in failures, against a local fixture server. `TAPEO_VSCODE_VSIX="$PWD/clients/vscode/tapeo-vscode.vsix" npm run test:vscode` repeats it with the packaged extension. VS Code must be installed; `TAPEO_VSCODE_BIN` selects another `code` executable.
|
package/cli/tapeo.mjs
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir, hostname } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { ACTIVE_RUN_STATUSES, TapeoClient, describeGoals, describeRun, describeRunDetail, normalizeServerUrl, setClientIdentity, signInWithBrowser, stageScope, watchRun, DEFAULT_SERVER_URL } from "../shared/api.mjs";
|
|
7
|
+
import { assertUnbound, designFilesFrom, downloadFile, localChanges, moveProjectFile, pullOutputs, pullProject, pushChanges, pushProjectFile, readState, removeProjectFile } from "../shared/sync.mjs";
|
|
8
|
+
import { applyProposals, changeDiff, describeChange, discardProposals, loadProposals, saveProposals } from "../shared/proposals.mjs";
|
|
9
|
+
import { collectRunProblems, formatProblem } from "../shared/problems.mjs";
|
|
10
|
+
import { NOT_SURE, clarificationAnswersMessage } from "../shared/stream.mjs";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
12
|
+
|
|
13
|
+
const configPath = path.join(homedir(), ".config", "tapeo", "client.json");
|
|
14
|
+
const clientVersion = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
15
|
+
setClientIdentity("tapeo-cli", clientVersion);
|
|
16
|
+
|
|
17
|
+
function usage() {
|
|
18
|
+
console.log(`Tapeo terminal client (developer preview)
|
|
19
|
+
|
|
20
|
+
Sign in
|
|
21
|
+
tapeo login [server-url] Sign in through your browser (approve this device there);
|
|
22
|
+
the server is https://tapeo.net unless you name another
|
|
23
|
+
tapeo login [server-url] --token Paste a token from Account → VS Code and terminal access
|
|
24
|
+
tapeo logout Forget the saved sign-in on this computer
|
|
25
|
+
tapeo --version Show this client's version
|
|
26
|
+
|
|
27
|
+
Projects and files
|
|
28
|
+
tapeo projects List your projects
|
|
29
|
+
tapeo create <name> [dir] Create an empty project and bind a local folder
|
|
30
|
+
tapeo pull <project-id> [dir] Pull source files without overwriting local edits
|
|
31
|
+
tapeo pull --outputs Also download tool outputs (generated/, signoff/) read-only
|
|
32
|
+
tapeo status What changed locally, and the latest run
|
|
33
|
+
tapeo push <path...> | --all Push named files, or every modified and new file
|
|
34
|
+
tapeo rm <path> Delete a file on the server and locally
|
|
35
|
+
tapeo mv <from> <to> Rename a file on the server and locally
|
|
36
|
+
tapeo download <path> [dest] Download one project file (any size, e.g. a GDS layout)
|
|
37
|
+
|
|
38
|
+
AI
|
|
39
|
+
tapeo new [description] [dir] Design a new chip with the Architect, then build it
|
|
40
|
+
(--from-files starts from this folder's RTL; --option N,
|
|
41
|
+
--auto-answers, --yes, --no-build for scripts)
|
|
42
|
+
tapeo ask "<question>" Ask the AI Engineer about this project
|
|
43
|
+
tapeo history The recent Engineer conversation (shared with the browser)
|
|
44
|
+
tapeo proposals The Engineer's proposed file changes, with diffs
|
|
45
|
+
tapeo apply [n...] Apply proposed changes to local files (all by default)
|
|
46
|
+
tapeo discard Forget the proposed changes
|
|
47
|
+
tapeo problems [run-id] Tool errors and warnings as file:line (latest run by default)
|
|
48
|
+
|
|
49
|
+
Workflow
|
|
50
|
+
tapeo run [--stage <s> | --from <s>] [--no-ai-repairs] [--watch]
|
|
51
|
+
Start a server run (a full run uses one build)
|
|
52
|
+
tapeo runs Recent runs
|
|
53
|
+
tapeo watch [run-id] Follow a run until it ends, then show its results
|
|
54
|
+
tapeo show [run-id] Stage results, AI changes, and tool runs of a run
|
|
55
|
+
tapeo logs <tool-run-id> A tool run's saved output
|
|
56
|
+
tapeo goals The latest goal report
|
|
57
|
+
tapeo cancel [run-id] Cancel a queued or running workflow
|
|
58
|
+
tapeo open Print this project's web address
|
|
59
|
+
|
|
60
|
+
Stages: simulation, synthesis, timing, physical, post-route-timing, drc, extraction, erc, lvs.
|
|
61
|
+
TAPEO_URL and TAPEO_TOKEN override the saved sign-in (for automation).`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Split `--flag`, `--key value`, and positional arguments. */
|
|
65
|
+
function parseArguments(args, valueFlags = []) {
|
|
66
|
+
const flags = {}, positional = [];
|
|
67
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
68
|
+
const arg = args[index];
|
|
69
|
+
if (!arg.startsWith("--")) { positional.push(arg); continue; }
|
|
70
|
+
const name = arg.slice(2);
|
|
71
|
+
if (valueFlags.includes(name)) {
|
|
72
|
+
if (index + 1 >= args.length) throw new Error(`--${name} needs a value.`);
|
|
73
|
+
flags[name] = args[++index];
|
|
74
|
+
} else flags[name] = true;
|
|
75
|
+
}
|
|
76
|
+
return { flags, positional };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function hiddenToken() {
|
|
80
|
+
if (!process.stdin.isTTY || !process.stdin.setRawMode) throw new Error("Run login in an interactive terminal, or set TAPEO_TOKEN in your environment.");
|
|
81
|
+
process.stdout.write("Paste project token (input hidden): ");
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
let value = "";
|
|
84
|
+
process.stdin.setRawMode(true);
|
|
85
|
+
process.stdin.resume();
|
|
86
|
+
const done = (error) => {
|
|
87
|
+
process.stdin.off("data", onData);
|
|
88
|
+
process.stdin.setRawMode(false);
|
|
89
|
+
process.stdin.pause();
|
|
90
|
+
process.stdout.write("\n");
|
|
91
|
+
if (error) reject(error); else resolve(value.trim());
|
|
92
|
+
};
|
|
93
|
+
const onData = (chunk) => {
|
|
94
|
+
for (const char of chunk.toString("utf8")) {
|
|
95
|
+
if (char === "\r" || char === "\n") return done();
|
|
96
|
+
if (char === "\u0003") return done(new Error("Canceled."));
|
|
97
|
+
if (char === "\u007f") value = value.slice(0, -1);
|
|
98
|
+
else if (value.length < 200) value += char;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
process.stdin.on("data", onData);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Open a page in the default browser; printing the address is the fallback. */
|
|
106
|
+
function openBrowser(url) {
|
|
107
|
+
if (process.env.TAPEO_NO_BROWSER) return;
|
|
108
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
109
|
+
try { spawn(command, args, { stdio: "ignore", detached: true }).on("error", () => undefined).unref(); } catch { /* the address is printed */ }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function saveLogin(serverUrl, token) {
|
|
113
|
+
await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 });
|
|
114
|
+
await writeFile(configPath, JSON.stringify({ serverUrl, token }, null, 2) + "\n", { mode: 0o600 });
|
|
115
|
+
await chmod(configPath, 0o600);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function configuredClient() {
|
|
119
|
+
let saved = {};
|
|
120
|
+
try { saved = JSON.parse(await readFile(configPath, "utf8")); }
|
|
121
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
122
|
+
const serverUrl = process.env.TAPEO_URL || saved.serverUrl;
|
|
123
|
+
const token = process.env.TAPEO_TOKEN || saved.token;
|
|
124
|
+
if (!serverUrl || !token) throw new Error("Run `tapeo login <server-url>` first, or set TAPEO_URL and TAPEO_TOKEN.");
|
|
125
|
+
return new TapeoClient(serverUrl, token);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function projectIdFor(client, explicit) {
|
|
129
|
+
if (explicit) return explicit;
|
|
130
|
+
const state = await readState(process.cwd()).catch((error) => {
|
|
131
|
+
if (error.code === "ENOENT") throw new Error("This folder is not a Tapeo project. Run the command in a pulled folder, or name the project id.");
|
|
132
|
+
throw error;
|
|
133
|
+
});
|
|
134
|
+
if (state.serverUrl !== client.serverUrl) throw new Error("This folder belongs to another Tapeo server.");
|
|
135
|
+
return state.projectId;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function follow(client, projectId, runId) {
|
|
139
|
+
let lastStage = "";
|
|
140
|
+
const final = await watchRun(client, projectId, runId, {
|
|
141
|
+
onChange: (detail) => {
|
|
142
|
+
const label = `${detail.run.status}${detail.run.currentStage ? ` · ${detail.run.currentStage}` : ""}`;
|
|
143
|
+
if (label !== lastStage) { lastStage = label; console.log(`[${new Date().toLocaleTimeString()}] ${label}`); }
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
console.log("");
|
|
147
|
+
console.log(describeRunDetail(final));
|
|
148
|
+
const { report } = await client.goals(projectId).catch(() => ({ report: null }));
|
|
149
|
+
if (report) { console.log(""); console.log(describeGoals(report)); }
|
|
150
|
+
if (final.run.status === "failed") process.exitCode = 3;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Print an answer as it is written: only the new part each time. */
|
|
154
|
+
function streamPrinter() {
|
|
155
|
+
let printed = "";
|
|
156
|
+
return {
|
|
157
|
+
update(text) { if (text.startsWith(printed)) { process.stdout.write(text.slice(printed.length)); printed = text; } },
|
|
158
|
+
finish(text) { if (text.startsWith(printed)) process.stdout.write(text.slice(printed.length)); else if (text !== printed) process.stdout.write(`\n${text}`); process.stdout.write("\n"); }
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function showProposals(proposals) {
|
|
163
|
+
if (!proposals?.changes?.length) { console.log("No proposed file changes."); return; }
|
|
164
|
+
console.log(`Proposed changes (${proposals.changes.length}), from: "${proposals.question}"`);
|
|
165
|
+
proposals.changes.forEach((change, index) => {
|
|
166
|
+
console.log(`\n${describeChange(change, index)}`);
|
|
167
|
+
if (change.status === "pending" || change.status === "failed") console.log(changeDiff(change).split("\n").map((line) => ` ${line}`).join("\n"));
|
|
168
|
+
});
|
|
169
|
+
if (proposals.evaluationPlanProposed) console.log("\nThe Engineer also proposed an evaluation plan; review it in the web workspace (Evaluation).");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Questions on the terminal; scripts pass answers as flags instead. */
|
|
173
|
+
function prompter() {
|
|
174
|
+
if (!process.stdin.isTTY) return null;
|
|
175
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
176
|
+
return { ask: (question) => rl.question(question), close: () => rl.close() };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function newDesign(client, description, directory, flags) {
|
|
180
|
+
await assertUnbound(directory);
|
|
181
|
+
const prompt = flags["auto-answers"] && flags.yes && flags.option ? null : prompter();
|
|
182
|
+
try {
|
|
183
|
+
if (!description) {
|
|
184
|
+
if (!prompt) throw new Error("Describe the hardware: tapeo new \"A sensor that warns when a tank is full\"");
|
|
185
|
+
description = (await prompt.ask("Describe the hardware you want to build: ")).trim();
|
|
186
|
+
if (!description) throw new Error("No description given.");
|
|
187
|
+
}
|
|
188
|
+
const designFiles = flags["from-files"] ? await designFilesFrom(directory) : [];
|
|
189
|
+
if (flags["from-files"]) console.log(designFiles.length ? `Starting from ${designFiles.length} design file(s): ${designFiles.map((file) => file.path).join(", ")}` : "No design files found in this folder; starting from the description.");
|
|
190
|
+
const messages = [{ role: "user", content: description }];
|
|
191
|
+
let result;
|
|
192
|
+
for (let round = 0; round < 6; round += 1) {
|
|
193
|
+
const printer = streamPrinter();
|
|
194
|
+
process.stdout.write("\nArchitect: ");
|
|
195
|
+
result = await client.architect(messages, { designFiles }, (text) => printer.update(text));
|
|
196
|
+
printer.finish(result.message);
|
|
197
|
+
messages.push({ role: "assistant", content: result.message });
|
|
198
|
+
if (result.mode === "proposal" && result.options?.length) break;
|
|
199
|
+
const clarifications = result.clarifications ?? [];
|
|
200
|
+
if (!clarifications.length) {
|
|
201
|
+
if (!prompt) throw new Error("The Architect asked an open question; run tapeo new interactively to answer it.");
|
|
202
|
+
const reply = (await prompt.ask("\nYour answer: ")).trim();
|
|
203
|
+
messages.push({ role: "user", content: reply || "Choose sensible defaults and propose a design." });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
const answers = {};
|
|
207
|
+
for (const [index, item] of clarifications.entries()) {
|
|
208
|
+
const choices = [...item.choices, NOT_SURE];
|
|
209
|
+
if (!prompt || flags["auto-answers"]) { answers[index] = NOT_SURE; continue; }
|
|
210
|
+
console.log(`\n${index + 1}. ${item.question}`);
|
|
211
|
+
choices.forEach((choice, number) => console.log(` ${number + 1}) ${choice}`));
|
|
212
|
+
const picked = Number((await prompt.ask(" Choose a number (Enter: not sure): ")).trim() || choices.length);
|
|
213
|
+
answers[index] = choices[Math.min(Math.max(1, picked), choices.length) - 1];
|
|
214
|
+
}
|
|
215
|
+
const answer = clarificationAnswersMessage(clarifications, answers);
|
|
216
|
+
console.log(`\nYou: ${answer.replaceAll("\n", "\n ")}`);
|
|
217
|
+
messages.push({ role: "user", content: answer });
|
|
218
|
+
}
|
|
219
|
+
if (result.mode !== "proposal" || !result.options?.length) throw new Error("The Architect did not propose a design yet. Run tapeo new again with more detail.");
|
|
220
|
+
if (result.brief?.summary) console.log(`\nBrief: ${result.brief.summary}${result.brief.successCriteria?.length ? `\n It works when: ${result.brief.successCriteria.join("; ")}` : ""}`);
|
|
221
|
+
console.log("\nOptions:");
|
|
222
|
+
result.options.forEach((option, index) => console.log(` ${index + 1}) ${option.name}${option.recommended ? " (recommended)" : ""}: ${option.summary}${option.tradeoffs?.length ? `\n Trade-offs: ${option.tradeoffs.join("; ")}` : ""}`));
|
|
223
|
+
const recommended = Math.max(0, result.options.findIndex((option) => option.recommended));
|
|
224
|
+
let chosen = flags.option ? Number(flags.option) - 1 : recommended;
|
|
225
|
+
if (!flags.option && prompt) chosen = Number((await prompt.ask(`Choose an option (Enter: ${recommended + 1}): `)).trim() || recommended + 1) - 1;
|
|
226
|
+
const option = result.options[chosen];
|
|
227
|
+
if (!option) throw new Error(`There is no option ${chosen + 1}.`);
|
|
228
|
+
const { plan } = await client.designPlan(result.requirements, designFiles.map((file) => file.path));
|
|
229
|
+
console.log(`\nBuild plan for "${option.name}" · ${plan.platformLabel}${plan.platformNote ? ` (${plan.platformNote})` : ""}`);
|
|
230
|
+
for (const stage of plan.stages) console.log(` ${stage.status === "runs" ? "✓" : stage.status === "conditional" ? "?" : "–"} ${stage.label} (${stage.tool})`);
|
|
231
|
+
if (plan.estimate) console.log(` ${plan.estimate}`);
|
|
232
|
+
const build = !flags["no-build"];
|
|
233
|
+
if (build && !flags.yes) {
|
|
234
|
+
if (!prompt) throw new Error("Confirm the build with --yes (it uses one build from your plan).");
|
|
235
|
+
if (!/^y/i.test((await prompt.ask("Build it now? This uses one build from your plan. [y/N] ")).trim())) { console.log("Not built. Nothing was created."); return; }
|
|
236
|
+
}
|
|
237
|
+
const created = await client.createFromDesign({ requirements: result.requirements, architecture: option, designFiles, build });
|
|
238
|
+
await pullProject(client, created.project.id, directory);
|
|
239
|
+
console.log(`\nCreated ${created.project.name} (${created.project.id}) in ${directory}.`);
|
|
240
|
+
if (created.buildError) { console.log(`The build did not start: ${created.buildError}\nStart it later with: tapeo run`); return; }
|
|
241
|
+
if (!created.run) { console.log("Start the build when ready with: tapeo run"); return; }
|
|
242
|
+
console.log(`Building (run ${created.run.id}). The first run writes the RTL and testbench, then runs every stage.`);
|
|
243
|
+
const previous = process.cwd();
|
|
244
|
+
process.chdir(directory);
|
|
245
|
+
try {
|
|
246
|
+
await follow(client, created.project.id, created.run.id);
|
|
247
|
+
const pulled = await pullProject(client, created.project.id, directory);
|
|
248
|
+
console.log(`\nPulled ${pulled.pulled.length} generated file(s) into ${directory}. Edit them, then tapeo push --all and tapeo run.`);
|
|
249
|
+
} finally { process.chdir(previous); }
|
|
250
|
+
} finally { prompt?.close(); }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function main() {
|
|
254
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
255
|
+
if (!command || command === "help" || command === "--help") return usage();
|
|
256
|
+
if (command === "--version" || command === "version") {
|
|
257
|
+
console.log(clientVersion);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (command === "login") {
|
|
261
|
+
const { flags, positional } = parseArguments(rest);
|
|
262
|
+
const serverUrl = normalizeServerUrl(positional[0] || DEFAULT_SERVER_URL);
|
|
263
|
+
if (flags.token || process.env.TAPEO_TOKEN) {
|
|
264
|
+
const token = process.env.TAPEO_TOKEN || await hiddenToken();
|
|
265
|
+
const client = new TapeoClient(serverUrl, token);
|
|
266
|
+
await client.projects();
|
|
267
|
+
await saveLogin(serverUrl, token);
|
|
268
|
+
console.log(`Connected to ${serverUrl}. Token stored in ${configPath}.`);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const signedIn = await signInWithBrowser(serverUrl, `${hostname()} · terminal`, {
|
|
272
|
+
onCode: ({ userCode, verificationUriComplete }) => {
|
|
273
|
+
console.log(`Approve this device in your browser:\n ${verificationUriComplete}\nConfirm the code ${userCode} matches. Waiting…`);
|
|
274
|
+
openBrowser(verificationUriComplete);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
await saveLogin(signedIn.serverUrl, signedIn.token);
|
|
278
|
+
console.log(`Signed in to ${signedIn.serverUrl} as "${signedIn.name}". Token stored in ${configPath}; revoke it any time on the Account page.`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (command === "logout") {
|
|
282
|
+
await rm(configPath, { force: true });
|
|
283
|
+
console.log("Signed out on this computer. Revoke the token on the Account page to end its access everywhere.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const client = await configuredClient();
|
|
287
|
+
const { flags, positional: args } = parseArguments(rest, ["stage", "from", "option", "limit"]);
|
|
288
|
+
if (command === "new") {
|
|
289
|
+
await newDesign(client, args[0], path.resolve(args[1] || "."), flags);
|
|
290
|
+
} else if (command === "ask") {
|
|
291
|
+
const directory = process.cwd();
|
|
292
|
+
const projectId = await projectIdFor(client);
|
|
293
|
+
let question = args.join(" ").trim();
|
|
294
|
+
if (!question) {
|
|
295
|
+
const prompt = prompter();
|
|
296
|
+
if (!prompt) throw new Error('Usage: tapeo ask "<question>"');
|
|
297
|
+
try { question = (await prompt.ask("Ask the Engineer: ")).trim(); } finally { prompt.close(); }
|
|
298
|
+
if (!question) throw new Error("No question given.");
|
|
299
|
+
}
|
|
300
|
+
const changes = await localChanges(directory).catch(() => null);
|
|
301
|
+
if (changes && (changes.modified.length || changes.added.length)) console.log(`Note: the Engineer sees the server copy. ${changes.modified.length + changes.added.length} local change(s) are not pushed (tapeo push --all).`);
|
|
302
|
+
const printer = streamPrinter();
|
|
303
|
+
process.stdout.write("Engineer: ");
|
|
304
|
+
const result = await client.askEngineer(projectId, question, (text) => printer.update(text));
|
|
305
|
+
printer.finish(result.message);
|
|
306
|
+
if (result.proposedChanges?.length || result.proposedEvaluationPlan) {
|
|
307
|
+
const proposals = await saveProposals(directory, { question, result });
|
|
308
|
+
console.log("");
|
|
309
|
+
showProposals(proposals);
|
|
310
|
+
if (proposals.changes.some((change) => change.status === "pending")) console.log(`\nApply with: tapeo apply${proposals.changes.length > 1 ? " (or only some: tapeo apply 1 2)" : ""}, then tapeo push --all. Or: tapeo discard`);
|
|
311
|
+
}
|
|
312
|
+
if (result.invalidChangeCount) console.log(`(${result.invalidChangeCount} malformed change(s) from the Engineer were left out.)`);
|
|
313
|
+
} else if (command === "history") {
|
|
314
|
+
const projectId = await projectIdFor(client);
|
|
315
|
+
const { messages } = await client.engineerHistory(projectId);
|
|
316
|
+
for (const message of messages.slice(-Number(flags.limit ?? 10))) console.log(`${message.role === "user" ? "You" : "Engineer"} · ${new Date(message.createdAt).toLocaleString()}\n${message.content}\n`);
|
|
317
|
+
if (!messages.length) console.log('No Engineer conversation yet. Start one with: tapeo ask "<question>"');
|
|
318
|
+
} else if (command === "proposals") {
|
|
319
|
+
showProposals(await loadProposals(process.cwd()));
|
|
320
|
+
} else if (command === "apply") {
|
|
321
|
+
const results = await applyProposals(process.cwd(), args.length ? args : "all");
|
|
322
|
+
for (const result of results) console.log(result.ok ? `Applied ${result.file}${result.deleted ? " (deleted locally; run tapeo rm " + result.file + " to delete it on the server)" : ""}.` : `Not applied${result.file ? ` (${result.file})` : ""}: ${result.message}`);
|
|
323
|
+
if (results.some((result) => result.ok)) console.log("Review the files, then save them to Tapeo with: tapeo push --all");
|
|
324
|
+
if (results.some((result) => !result.ok)) process.exitCode = 2;
|
|
325
|
+
} else if (command === "discard") {
|
|
326
|
+
await discardProposals(process.cwd());
|
|
327
|
+
console.log("Proposed changes discarded.");
|
|
328
|
+
} else if (command === "problems") {
|
|
329
|
+
const projectId = await projectIdFor(client);
|
|
330
|
+
const runId = await client.latestRunId(projectId, args[0]);
|
|
331
|
+
const { files } = await client.project(projectId);
|
|
332
|
+
const problems = await collectRunProblems(client, projectId, runId, files.map((file) => file.path));
|
|
333
|
+
for (const problem of problems) console.log(formatProblem(problem));
|
|
334
|
+
const errors = problems.filter((problem) => problem.severity === "error").length;
|
|
335
|
+
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.`);
|
|
336
|
+
} else if (command === "projects") {
|
|
337
|
+
const { projects } = await client.projects();
|
|
338
|
+
for (const project of projects) console.log(`${project.id}\t${project.name}`);
|
|
339
|
+
if (!projects.length) console.log("No projects yet. Run tapeo create <name> [directory].");
|
|
340
|
+
} else if (command === "create") {
|
|
341
|
+
if (!args[0]?.trim()) throw new Error("Usage: tapeo create <name> [directory]");
|
|
342
|
+
const directory = path.resolve(args[1] || ".");
|
|
343
|
+
await assertUnbound(directory);
|
|
344
|
+
const { project } = await client.createProject(args[0].trim());
|
|
345
|
+
await pullProject(client, project.id, directory);
|
|
346
|
+
console.log(`Created ${project.name} (${project.id}) in ${directory}. Add RTL and a testbench, then run tapeo push --all.`);
|
|
347
|
+
} else if (command === "pull") {
|
|
348
|
+
const bound = await readState(process.cwd()).catch(() => null);
|
|
349
|
+
const projectId = args[0] ?? (bound && bound.serverUrl === client.serverUrl ? bound.projectId : null);
|
|
350
|
+
if (!projectId) throw new Error("Usage: tapeo pull <project-id> [directory] [--outputs]");
|
|
351
|
+
const directory = path.resolve(args[0] ? args[1] || "." : ".");
|
|
352
|
+
const result = await pullProject(client, projectId, directory);
|
|
353
|
+
console.log(`Pulled ${result.pulled.length} files. Skipped ${result.skipped.length} generated/large files.`);
|
|
354
|
+
if (result.conflicts.length) {
|
|
355
|
+
console.error(`Local changes kept: ${result.conflicts.join(", ")}`);
|
|
356
|
+
for (const [file, copy] of Object.entries(result.remoteCopies)) console.error(`Compare ${file} with ${copy}`);
|
|
357
|
+
process.exitCode = 2;
|
|
358
|
+
}
|
|
359
|
+
if (flags.outputs) {
|
|
360
|
+
const outputs = await pullOutputs(client, projectId, directory);
|
|
361
|
+
console.log(`Outputs: ${outputs.downloaded.length} downloaded, ${outputs.unchanged.length} unchanged (read-only, under generated/ and signoff/).`);
|
|
362
|
+
for (const item of outputs.skipped) console.log(` Skipped ${item.path} (${(item.sizeBytes / 1048576).toFixed(1)} MB). Download it with: tapeo download ${item.path}`);
|
|
363
|
+
}
|
|
364
|
+
} else if (command === "status") {
|
|
365
|
+
const changes = await localChanges(process.cwd());
|
|
366
|
+
if (changes.serverUrl !== client.serverUrl) throw new Error("This folder belongs to another Tapeo server.");
|
|
367
|
+
const lines = [...changes.modified.map((file) => ` modified ${file}`), ...changes.added.map((file) => ` new ${file}`), ...changes.deleted.map((file) => ` deleted ${file} (run tapeo rm to delete it on the server)`), ...changes.tooLarge.map((file) => ` too large ${file} (over 2 MB; not pushed)`)];
|
|
368
|
+
console.log(lines.length ? `Local changes:\n${lines.join("\n")}\nPush them with: tapeo push --all` : "No local changes since the last pull or push.");
|
|
369
|
+
const { runs } = await client.runs(changes.projectId);
|
|
370
|
+
console.log(runs.length ? `\nLatest run: ${describeRun(runs[0])}` : "\nNo workflow runs yet.");
|
|
371
|
+
} else if (command === "push") {
|
|
372
|
+
if (flags.all) {
|
|
373
|
+
const result = await pushChanges(client, process.cwd());
|
|
374
|
+
console.log(result.saved.length ? `Saved ${result.saved.length} file(s): ${result.saved.join(", ")}` : "Nothing to push.");
|
|
375
|
+
for (const conflict of result.conflicts) console.error(`Not pushed, server changed: ${conflict.path}. ${conflict.message}`);
|
|
376
|
+
for (const failure of result.failed) console.error(`Not pushed: ${failure.path}: ${failure.message}`);
|
|
377
|
+
if (result.deletedLocally.length) console.log(`Deleted locally, still on the server: ${result.deletedLocally.join(", ")} (use tapeo rm).`);
|
|
378
|
+
if (result.conflicts.length || result.failed.length) process.exitCode = 2;
|
|
379
|
+
} else {
|
|
380
|
+
if (!args.length) throw new Error("Usage: tapeo push <project-relative-path...> or tapeo push --all");
|
|
381
|
+
for (const file of args) {
|
|
382
|
+
const result = await pushProjectFile(client, process.cwd(), file);
|
|
383
|
+
console.log(`Saved ${result.path} (${result.sha256.slice(0, 12)}).`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else if (command === "rm") {
|
|
387
|
+
if (!args[0]) throw new Error("Usage: tapeo rm <project-relative-path>");
|
|
388
|
+
await removeProjectFile(client, process.cwd(), args[0]);
|
|
389
|
+
console.log(`Deleted ${args[0]} on the server and locally.`);
|
|
390
|
+
} else if (command === "mv") {
|
|
391
|
+
if (!args[0] || !args[1]) throw new Error("Usage: tapeo mv <from> <to>");
|
|
392
|
+
await moveProjectFile(client, process.cwd(), args[0], args[1]);
|
|
393
|
+
console.log(`Renamed ${args[0]} to ${args[1]}.`);
|
|
394
|
+
} else if (command === "download") {
|
|
395
|
+
if (!args[0]) throw new Error("Usage: tapeo download <project-path> [destination]");
|
|
396
|
+
const projectId = await projectIdFor(client);
|
|
397
|
+
const result = await downloadFile(client, projectId, args[0], args[1] || path.basename(args[0]));
|
|
398
|
+
console.log(`Downloaded ${result.path} (${result.bytes.toLocaleString()} bytes) to ${result.destination}.`);
|
|
399
|
+
} else if (command === "run") {
|
|
400
|
+
const projectId = await projectIdFor(client, args[0]);
|
|
401
|
+
const scope = stageScope({ stage: flags.stage, from: flags.from, aiRepairs: !flags["no-ai-repairs"] });
|
|
402
|
+
const { run } = await client.run(projectId, scope);
|
|
403
|
+
console.log(`Queued workflow ${run.id}${scope ? ` (${scope.from === scope.to ? `${scope.from} only` : `${scope.from} → ${scope.to}`})` : " (full run: uses one build)"}.`);
|
|
404
|
+
if (flags.watch) await follow(client, projectId, run.id);
|
|
405
|
+
else console.log("Follow it with: tapeo watch");
|
|
406
|
+
} else if (command === "runs") {
|
|
407
|
+
const projectId = await projectIdFor(client, args[0]);
|
|
408
|
+
const { runs } = await client.runs(projectId);
|
|
409
|
+
for (const run of runs) console.log(describeRun(run));
|
|
410
|
+
if (!runs.length) console.log("No workflow runs yet.");
|
|
411
|
+
} else if (command === "watch") {
|
|
412
|
+
const projectId = await projectIdFor(client);
|
|
413
|
+
await follow(client, projectId, await client.latestRunId(projectId, args[0]));
|
|
414
|
+
} else if (command === "show") {
|
|
415
|
+
const projectId = await projectIdFor(client);
|
|
416
|
+
console.log(describeRunDetail(await client.runDetail(projectId, await client.latestRunId(projectId, args[0]))));
|
|
417
|
+
} else if (command === "logs") {
|
|
418
|
+
if (!args[0]) throw new Error("Usage: tapeo logs <tool-run-id> (ids are listed by tapeo show)");
|
|
419
|
+
const projectId = await projectIdFor(client);
|
|
420
|
+
const { toolRun } = await client.toolRunLog(projectId, args[0]);
|
|
421
|
+
console.log(`${toolRun.tool} · ${toolRun.status} · ${new Date(toolRun.createdAt).toLocaleString()}\n${toolRun.summary}\n`);
|
|
422
|
+
if (toolRun.stdout) console.log(`--- stdout ---\n${toolRun.stdout}`);
|
|
423
|
+
if (toolRun.stderr) console.log(`--- stderr ---\n${toolRun.stderr}`);
|
|
424
|
+
if (!toolRun.stdout && !toolRun.stderr) console.log("This tool run saved no output.");
|
|
425
|
+
} else if (command === "goals") {
|
|
426
|
+
const projectId = await projectIdFor(client, args[0]);
|
|
427
|
+
console.log(describeGoals((await client.goals(projectId)).report));
|
|
428
|
+
} else if (command === "cancel") {
|
|
429
|
+
const projectId = await projectIdFor(client);
|
|
430
|
+
let runId = args[0];
|
|
431
|
+
if (!runId) {
|
|
432
|
+
const { runs } = await client.runs(projectId);
|
|
433
|
+
runId = runs.find((run) => ACTIVE_RUN_STATUSES.includes(run.status))?.id;
|
|
434
|
+
if (!runId) throw new Error("No queued or running workflow to cancel.");
|
|
435
|
+
}
|
|
436
|
+
const { run } = await client.cancelRun(projectId, runId);
|
|
437
|
+
console.log(`Cancelled ${run.id}. A tool step already running finishes first; finished stages keep their results.`);
|
|
438
|
+
} else if (command === "open") {
|
|
439
|
+
const projectId = await projectIdFor(client, args[0]);
|
|
440
|
+
console.log(`${client.serverUrl}/?project=${encodeURIComponent(projectId)}`);
|
|
441
|
+
} else usage();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
main().catch((error) => { console.error(`Tapeo: ${error.message}`); process.exitCode = process.exitCode || 1; });
|
package/package.json
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tapeo-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"tapeo",
|
|
7
|
+
"hardware",
|
|
8
|
+
"rtl",
|
|
9
|
+
"verilog",
|
|
10
|
+
"systemverilog",
|
|
11
|
+
"eda",
|
|
12
|
+
"asic",
|
|
13
|
+
"fpga",
|
|
14
|
+
"cli"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://tapeo.net",
|
|
17
|
+
"license": "UNLICENSED",
|
|
18
|
+
"author": "Tapeo",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"tapeo": "./cli/tapeo.mjs"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"cli",
|
|
25
|
+
"shared",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|