@genrtl/grtl 0.2.0 → 0.4.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 +80 -60
- package/dist/index.js +518 -35
- package/package.json +38 -34
package/README.md
CHANGED
|
@@ -1,69 +1,89 @@
|
|
|
1
|
-
# @genrtl/grtl
|
|
2
|
-
|
|
3
|
-
CLI and coding-agent integration for the GenRTL RTL engineering knowledge
|
|
4
|
-
service.
|
|
5
|
-
|
|
6
|
-
## Install
|
|
7
|
-
|
|
8
|
-
```bash
|
|
9
|
-
npm install --global @genrtl/grtl
|
|
10
|
-
export GRTL_API_KEY="gtr_live_your_api_key"
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
PowerShell:
|
|
14
|
-
|
|
15
|
-
```powershell
|
|
16
|
-
$env:GRTL_API_KEY = "gtr_live_your_api_key"
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Agent Setup
|
|
20
|
-
|
|
21
|
-
Install a Skill that tells the agent to call the `grtl` CLI:
|
|
1
|
+
# @genrtl/grtl
|
|
2
|
+
|
|
3
|
+
CLI and coding-agent integration for the GenRTL RTL engineering knowledge
|
|
4
|
+
service.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install --global @genrtl/grtl
|
|
10
|
+
export GRTL_API_KEY="gtr_live_your_api_key"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
PowerShell:
|
|
14
|
+
|
|
15
|
+
```powershell
|
|
16
|
+
$env:GRTL_API_KEY = "gtr_live_your_api_key"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Agent Setup
|
|
20
|
+
|
|
21
|
+
Install a Skill that tells the agent to call the `grtl` CLI:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
grtl setup --cli --codex
|
|
25
|
+
grtl setup --cli --cursor --project
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Configure hosted MCP and install a Skill for the four MCP tools:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
grtl setup --mcp --codex
|
|
32
|
+
grtl setup --mcp --cursor --project
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Without `--cli` or `--mcp`, setup asks which mode to install.
|
|
36
|
+
|
|
37
|
+
For Codex, Skills are installed under `.agents/skills` for project setup or
|
|
38
|
+
`~/.agents/skills` for global setup. MCP mode also updates
|
|
39
|
+
`.codex/config.toml` or `~/.codex/config.toml`.
|
|
40
|
+
|
|
41
|
+
The hosted MCP endpoint is:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
https://genrtl.com/api/mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Knowledge Commands
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
grtl knowledge-search "AXI stream backpressure design"
|
|
51
|
+
grtl spec2rtl-search "Design an APB register block"
|
|
52
|
+
grtl spec2plan-search "Plan an APB register block implementation"
|
|
53
|
+
grtl verification-search "Verify an async FIFO"
|
|
54
|
+
grtl debug-search "Explain this Vivado CDC warning"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Use `--json` for structured output. `--type` accepts `spec2rtl`, `spec2plan`, `verification`, or `debug`; other filters include
|
|
58
|
+
`--domain`, `--tool`, `--tool-version`, `--error-type`, `--severity`,
|
|
59
|
+
`--interface`, `--target`, `--tag`, `--top-k`, `--min-score`, and
|
|
60
|
+
`--workspace-id`.
|
|
22
61
|
|
|
23
|
-
|
|
24
|
-
grtl setup --cli --codex
|
|
25
|
-
grtl setup --cli --cursor --project
|
|
26
|
-
```
|
|
62
|
+
## CBB Installation
|
|
27
63
|
|
|
28
|
-
|
|
64
|
+
Install an exact reusable RTL CBB version into the current project:
|
|
29
65
|
|
|
30
66
|
```bash
|
|
31
|
-
grtl
|
|
32
|
-
grtl
|
|
67
|
+
grtl cbb install cbb_uart@1.2.0
|
|
68
|
+
grtl cbb install cbb_uart@1.2.0 --target rtl/vendor/uart
|
|
33
69
|
```
|
|
34
70
|
|
|
35
|
-
|
|
71
|
+
The command calls the hosted `genrtl_cbb_acquire` MCP tool with
|
|
72
|
+
`GRTL_API_KEY`, downloads the short-lived ZIP artifact, verifies its size and
|
|
73
|
+
SHA-256, safely extracts it, and atomically installs it. The default target is
|
|
74
|
+
`rtl/cbb/<cbb_id>_<version>`.
|
|
36
75
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
The hosted MCP endpoint is:
|
|
42
|
-
|
|
43
|
-
```text
|
|
44
|
-
https://genrtl.com/api/mcp
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
## Knowledge Commands
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
grtl knowledge-search "AXI stream backpressure design"
|
|
51
|
-
grtl spec2rtl-search "Design an APB register block"
|
|
52
|
-
grtl verification-search "Verify an async FIFO"
|
|
53
|
-
grtl debug-search "Explain this Vivado CDC warning"
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Use `--json` for structured output. Available filters include `--type`,
|
|
57
|
-
`--domain`, `--tool`, `--tool-version`, `--error-type`, `--severity`,
|
|
58
|
-
`--interface`, `--target`, `--tag`, `--top-k`, `--min-score`, and
|
|
59
|
-
`--workspace-id`.
|
|
76
|
+
Installed packages are recorded in `.genrtl/cbb-lock.json`. Existing targets
|
|
77
|
+
are left untouched unless `--force` is provided; replacement occurs only after
|
|
78
|
+
the new archive has been fully verified and extracted. Use `--json` for
|
|
79
|
+
machine-readable output.
|
|
60
80
|
|
|
61
81
|
## Development
|
|
62
|
-
|
|
63
|
-
```bash
|
|
64
|
-
pnpm install
|
|
65
|
-
pnpm --filter @genrtl/grtl lint:check
|
|
66
|
-
pnpm --filter @genrtl/grtl typecheck
|
|
67
|
-
pnpm --filter @genrtl/grtl test
|
|
68
|
-
pnpm --filter @genrtl/grtl build
|
|
69
|
-
```
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm install
|
|
85
|
+
pnpm --filter @genrtl/grtl lint:check
|
|
86
|
+
pnpm --filter @genrtl/grtl typecheck
|
|
87
|
+
pnpm --filter @genrtl/grtl test
|
|
88
|
+
pnpm --filter @genrtl/grtl build
|
|
89
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command as Command2 } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc7 from "picocolors";
|
|
6
6
|
import figlet from "figlet";
|
|
7
7
|
|
|
8
8
|
// src/commands/setup.ts
|
|
@@ -376,6 +376,7 @@ var FALLBACK_MCP = `Use GenRTL MCP tools for grounded RTL engineering knowledge.
|
|
|
376
376
|
Choose one tool:
|
|
377
377
|
- \`genrtl_knowledge_search\` for cross-domain RTL questions
|
|
378
378
|
- \`genrtl_spec2rtl_search\` for requirements and RTL design
|
|
379
|
+
- \`genrtl_spec2plan_search\` for implementation planning from a specification
|
|
379
380
|
- \`genrtl_verification_search\` for testbenches and verification
|
|
380
381
|
- \`genrtl_debug_search\` for lint, CDC, compile, synthesis, and RTL bugs
|
|
381
382
|
|
|
@@ -385,6 +386,7 @@ var FALLBACK_CLI = `Use the \`grtl\` CLI for grounded RTL engineering knowledge.
|
|
|
385
386
|
Choose one command:
|
|
386
387
|
- \`npx @genrtl/grtl@latest knowledge-search "<query>"\`
|
|
387
388
|
- \`npx @genrtl/grtl@latest spec2rtl-search "<query>"\`
|
|
389
|
+
- \`npx @genrtl/grtl@latest spec2plan-search "<query>"\`
|
|
388
390
|
- \`npx @genrtl/grtl@latest verification-search "<query>"\`
|
|
389
391
|
- \`npx @genrtl/grtl@latest debug-search "<query>"\`
|
|
390
392
|
|
|
@@ -411,6 +413,7 @@ Choose exactly one MCP tool:
|
|
|
411
413
|
|
|
412
414
|
- \`genrtl_knowledge_search\` for cross-domain RTL questions.
|
|
413
415
|
- \`genrtl_spec2rtl_search\` for requirements, protocols, control logic, or algorithm-to-RTL work.
|
|
416
|
+
- \`genrtl_spec2plan_search\` for turning a specification into an actionable implementation plan.
|
|
414
417
|
- \`genrtl_verification_search\` for testbenches and verification.
|
|
415
418
|
- \`genrtl_debug_search\` for lint, CDC, compile, synthesis, or RTL bugs.
|
|
416
419
|
|
|
@@ -431,6 +434,7 @@ Choose exactly one command:
|
|
|
431
434
|
|
|
432
435
|
- \`grtl knowledge-search "<query>" --json\` for cross-domain RTL questions.
|
|
433
436
|
- \`grtl spec2rtl-search "<query>" --json\` for requirements, protocols, control logic, or algorithm-to-RTL work.
|
|
437
|
+
- \`grtl spec2plan-search "<query>" --json\` for turning a specification into an actionable implementation plan.
|
|
434
438
|
- \`grtl verification-search "<query>" --json\` for testbenches and verification.
|
|
435
439
|
- \`grtl debug-search "<query>" --json\` for lint, CDC, compile, synthesis, or RTL bugs.
|
|
436
440
|
|
|
@@ -855,6 +859,9 @@ import { InvalidArgumentError } from "commander";
|
|
|
855
859
|
import pc4 from "picocolors";
|
|
856
860
|
import ora2 from "ora";
|
|
857
861
|
|
|
862
|
+
// src/utils/knowledge-api.ts
|
|
863
|
+
import { randomUUID } from "crypto";
|
|
864
|
+
|
|
858
865
|
// src/constants.ts
|
|
859
866
|
import { readFileSync } from "fs";
|
|
860
867
|
import { fileURLToPath } from "url";
|
|
@@ -897,7 +904,7 @@ function getStructuredMcpError(content) {
|
|
|
897
904
|
code: typeof errorContent.code === "string" ? errorContent.code : void 0
|
|
898
905
|
};
|
|
899
906
|
}
|
|
900
|
-
async function
|
|
907
|
+
async function callGenrtlMcpTool(toolName, input) {
|
|
901
908
|
const apiKey = getApiKey();
|
|
902
909
|
if (!apiKey) {
|
|
903
910
|
throw new Error("Authentication required. Set GRTL_API_KEY or GENRTL_API_KEY.");
|
|
@@ -929,14 +936,18 @@ async function callGenrtlKnowledgeTool(toolName, input) {
|
|
|
929
936
|
if (!result) throw new Error("GenRTL MCP returned an empty result.");
|
|
930
937
|
if (result.isError) {
|
|
931
938
|
const structuredError = getStructuredMcpError(result.structuredContent);
|
|
932
|
-
const message = structuredError?.error || getMcpErrorMessage(result.content) || "GenRTL
|
|
939
|
+
const message = structuredError?.error || getMcpErrorMessage(result.content) || "GenRTL MCP tool call failed.";
|
|
933
940
|
throw new Error(structuredError?.code ? `${message} (${structuredError.code})` : message);
|
|
934
941
|
}
|
|
935
942
|
if (!result.structuredContent) {
|
|
936
|
-
throw new Error("GenRTL MCP response did not include structured
|
|
943
|
+
throw new Error("GenRTL MCP response did not include structured results.");
|
|
937
944
|
}
|
|
938
945
|
return result.structuredContent;
|
|
939
946
|
}
|
|
947
|
+
async function callGenrtlKnowledgeTool(toolName, input) {
|
|
948
|
+
const requestInput = input.idempotency_key ? input : { ...input, idempotency_key: randomUUID() };
|
|
949
|
+
return callGenrtlMcpTool(toolName, requestInput);
|
|
950
|
+
}
|
|
940
951
|
|
|
941
952
|
// src/commands/knowledge.ts
|
|
942
953
|
var isTTY = process.stdout.isTTY;
|
|
@@ -951,6 +962,11 @@ var TOOL_COMMANDS = [
|
|
|
951
962
|
alias: "spec2rtl-search",
|
|
952
963
|
description: "Search Spec2RTL knowledge cards"
|
|
953
964
|
},
|
|
965
|
+
{
|
|
966
|
+
name: "genrtl_spec2plan_search",
|
|
967
|
+
alias: "spec2plan-search",
|
|
968
|
+
description: "Search Spec2Plan knowledge cards"
|
|
969
|
+
},
|
|
954
970
|
{
|
|
955
971
|
name: "genrtl_verification_search",
|
|
956
972
|
alias: "verification-search",
|
|
@@ -988,7 +1004,7 @@ function buildKnowledgeSearchInput(query, options) {
|
|
|
988
1004
|
}
|
|
989
1005
|
const filters = {};
|
|
990
1006
|
if (options.type?.length) {
|
|
991
|
-
const allowed = /* @__PURE__ */ new Set(["spec2rtl", "verification", "debug"]);
|
|
1007
|
+
const allowed = /* @__PURE__ */ new Set(["spec2rtl", "spec2plan", "verification", "debug"]);
|
|
992
1008
|
const invalid = options.type.find((type) => !allowed.has(type));
|
|
993
1009
|
if (invalid) {
|
|
994
1010
|
throw new InvalidArgumentError(`Invalid knowledge type: ${invalid}`);
|
|
@@ -1065,7 +1081,7 @@ function printKnowledgeResult(result) {
|
|
|
1065
1081
|
}
|
|
1066
1082
|
}
|
|
1067
1083
|
function addSearchOptions(command) {
|
|
1068
|
-
return command.argument("<query>", "Natural-language RTL engineering question or diagnostic").option("--type <type...>", "Knowledge types: spec2rtl, verification, debug").option("--domain <domain>", "Filter by engineering domain").option("--tool <tool>", "Filter by EDA tool").option("--tool-version <version>", "Filter by EDA tool version").option("--error-type <type>", "Filter by error type").option("--severity <severity>", "Filter by severity").option("--interface <interface>", "Filter by hardware interface").option("--target <target>", "Filter by target: fpga, asic, or both").option("--tag <tag...>", "Filter by one or more tags").option("--top-k <count>", "Maximum results (1-20)", parseInteger).option("--min-score <score>", "Minimum similarity score (0-1)", parseNumber).option("--workspace-id <id>", "GenRTL workspace ID").option("--json", "Output the structured MCP result as JSON");
|
|
1084
|
+
return command.argument("<query>", "Natural-language RTL engineering question or diagnostic").option("--type <type...>", "Knowledge types: spec2rtl, spec2plan, verification, debug").option("--domain <domain>", "Filter by engineering domain").option("--tool <tool>", "Filter by EDA tool").option("--tool-version <version>", "Filter by EDA tool version").option("--error-type <type>", "Filter by error type").option("--severity <severity>", "Filter by severity").option("--interface <interface>", "Filter by hardware interface").option("--target <target>", "Filter by target: fpga, asic, or both").option("--tag <tag...>", "Filter by one or more tags").option("--top-k <count>", "Maximum results (1-20)", parseInteger).option("--min-score <score>", "Minimum similarity score (0-1)", parseNumber).option("--workspace-id <id>", "GenRTL workspace ID").option("--json", "Output the structured MCP result as JSON");
|
|
1069
1085
|
}
|
|
1070
1086
|
function registerKnowledgeCommands(program2) {
|
|
1071
1087
|
for (const tool of TOOL_COMMANDS) {
|
|
@@ -1076,23 +1092,485 @@ function registerKnowledgeCommands(program2) {
|
|
|
1076
1092
|
}
|
|
1077
1093
|
}
|
|
1078
1094
|
|
|
1095
|
+
// src/commands/cbb.ts
|
|
1096
|
+
import ora3 from "ora";
|
|
1097
|
+
import pc5 from "picocolors";
|
|
1098
|
+
|
|
1099
|
+
// src/utils/cbb-install.ts
|
|
1100
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1101
|
+
import { createWriteStream } from "fs";
|
|
1102
|
+
import {
|
|
1103
|
+
access as access3,
|
|
1104
|
+
mkdir as mkdir4,
|
|
1105
|
+
mkdtemp,
|
|
1106
|
+
open,
|
|
1107
|
+
readFile as readFile3,
|
|
1108
|
+
realpath,
|
|
1109
|
+
rename,
|
|
1110
|
+
rm as rm2,
|
|
1111
|
+
writeFile as writeFile4
|
|
1112
|
+
} from "fs/promises";
|
|
1113
|
+
import { dirname as dirname6, isAbsolute, join as join4, relative, resolve as resolve3, sep } from "path";
|
|
1114
|
+
import { Transform } from "stream";
|
|
1115
|
+
import { pipeline } from "stream/promises";
|
|
1116
|
+
import yauzl from "yauzl";
|
|
1117
|
+
var MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
1118
|
+
var MAX_EXTRACTED_BYTES = 512 * 1024 * 1024;
|
|
1119
|
+
var MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
1120
|
+
var MAX_COMPRESSION_RATIO = 200;
|
|
1121
|
+
var MAX_ZIP_ENTRIES = 1e4;
|
|
1122
|
+
function pathExists2(path) {
|
|
1123
|
+
return access3(path).then(
|
|
1124
|
+
() => true,
|
|
1125
|
+
() => false
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
function safePathPart(value) {
|
|
1129
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1130
|
+
}
|
|
1131
|
+
function parseCbbSpec(spec) {
|
|
1132
|
+
const separator = spec.lastIndexOf("@");
|
|
1133
|
+
if (separator <= 0 || separator === spec.length - 1) {
|
|
1134
|
+
throw new Error("CBB must use the form <cbb_id>@<version>.");
|
|
1135
|
+
}
|
|
1136
|
+
const cbbId = spec.slice(0, separator).trim();
|
|
1137
|
+
const version = spec.slice(separator + 1).trim();
|
|
1138
|
+
if (!cbbId || !version) {
|
|
1139
|
+
throw new Error("CBB must use the form <cbb_id>@<version>.");
|
|
1140
|
+
}
|
|
1141
|
+
return { cbbId, version };
|
|
1142
|
+
}
|
|
1143
|
+
function defaultCbbTarget(cbbId, version) {
|
|
1144
|
+
return `rtl/cbb/${safePathPart(cbbId)}_${safePathPart(version)}`;
|
|
1145
|
+
}
|
|
1146
|
+
function normalizeRelativeTarget(value) {
|
|
1147
|
+
const slashPath = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
1148
|
+
if (!slashPath || slashPath.includes("\0") || slashPath.startsWith("/") || /^[A-Za-z]:/.test(slashPath)) {
|
|
1149
|
+
throw new Error("Target must be a non-empty project-relative path.");
|
|
1150
|
+
}
|
|
1151
|
+
const parts = slashPath.split("/").filter((part) => part && part !== ".");
|
|
1152
|
+
if (parts.length === 0 || parts.some(
|
|
1153
|
+
(part) => part === ".." || part.length > 255 || part.includes(":") || part.endsWith(".") || part.endsWith(" ") || windowsReservedName(part)
|
|
1154
|
+
)) {
|
|
1155
|
+
throw new Error("Target must not escape the project directory.");
|
|
1156
|
+
}
|
|
1157
|
+
return parts.join("/");
|
|
1158
|
+
}
|
|
1159
|
+
function resolveProjectTarget(projectRoot, targetDir) {
|
|
1160
|
+
const targetPath = resolve3(projectRoot, ...targetDir.split("/"));
|
|
1161
|
+
const relativePath = relative(projectRoot, targetPath);
|
|
1162
|
+
if (!relativePath || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
1163
|
+
throw new Error("Target must resolve inside the project directory.");
|
|
1164
|
+
}
|
|
1165
|
+
return targetPath;
|
|
1166
|
+
}
|
|
1167
|
+
async function readLockfile(projectRoot) {
|
|
1168
|
+
const lockPath = join4(projectRoot, ".genrtl", "cbb-lock.json");
|
|
1169
|
+
if (!await pathExists2(lockPath)) {
|
|
1170
|
+
return { schema_version: 1, packages: {} };
|
|
1171
|
+
}
|
|
1172
|
+
let parsed;
|
|
1173
|
+
try {
|
|
1174
|
+
parsed = JSON.parse(await readFile3(lockPath, "utf8"));
|
|
1175
|
+
} catch {
|
|
1176
|
+
throw new Error(`Invalid CBB lockfile: ${lockPath}`);
|
|
1177
|
+
}
|
|
1178
|
+
if (!parsed || typeof parsed !== "object" || parsed.schema_version !== 1 || !parsed.packages || typeof parsed.packages !== "object") {
|
|
1179
|
+
throw new Error(`Unsupported CBB lockfile format: ${lockPath}`);
|
|
1180
|
+
}
|
|
1181
|
+
return parsed;
|
|
1182
|
+
}
|
|
1183
|
+
async function prepareCbbInstall(projectRootInput, targetInput, cbbId, version, force) {
|
|
1184
|
+
const projectRoot = await realpath(resolve3(projectRootInput));
|
|
1185
|
+
const targetDir = normalizeRelativeTarget(targetInput);
|
|
1186
|
+
const targetPath = resolveProjectTarget(projectRoot, targetDir);
|
|
1187
|
+
if (!force && await pathExists2(targetPath)) {
|
|
1188
|
+
throw new Error(`Target already exists: ${targetPath}. Use --force to replace it.`);
|
|
1189
|
+
}
|
|
1190
|
+
await readLockfile(projectRoot);
|
|
1191
|
+
const projectHash = createHash("sha256").update(projectRoot).digest("hex");
|
|
1192
|
+
const operationHash = createHash("sha256").update(`${projectRoot}\0${targetDir}\0${cbbId}\0${version}`).digest("hex");
|
|
1193
|
+
return {
|
|
1194
|
+
projectRoot,
|
|
1195
|
+
targetDir,
|
|
1196
|
+
targetPath,
|
|
1197
|
+
workspaceId: `cli:${projectHash.slice(0, 40)}`,
|
|
1198
|
+
jobId: `cbb-install:${safePathPart(cbbId)}@${safePathPart(version)}:${operationHash.slice(0, 16)}`,
|
|
1199
|
+
idempotencyKey: `grtl-cbb-install:${operationHash}`
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
async function writeAll(file, chunk) {
|
|
1203
|
+
let offset = 0;
|
|
1204
|
+
while (offset < chunk.byteLength) {
|
|
1205
|
+
const { bytesWritten } = await file.write(chunk, offset, chunk.byteLength - offset, null);
|
|
1206
|
+
if (bytesWritten <= 0) throw new Error("Failed to write downloaded artifact.");
|
|
1207
|
+
offset += bytesWritten;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
async function downloadArtifact(descriptor, archivePath) {
|
|
1211
|
+
if (!Number.isSafeInteger(descriptor.file_size) || descriptor.file_size <= 0 || descriptor.file_size > MAX_ARCHIVE_BYTES) {
|
|
1212
|
+
throw new Error("CBB archive size is invalid or exceeds the 512 MiB limit.");
|
|
1213
|
+
}
|
|
1214
|
+
if (!/^[a-fA-F0-9]{64}$/.test(descriptor.sha256)) {
|
|
1215
|
+
throw new Error("CBB archive SHA256 is invalid.");
|
|
1216
|
+
}
|
|
1217
|
+
const response = await fetch(descriptor.artifact_url, {
|
|
1218
|
+
redirect: "follow",
|
|
1219
|
+
headers: { Accept: "application/zip, application/octet-stream" }
|
|
1220
|
+
});
|
|
1221
|
+
if (!response.ok || !response.body) {
|
|
1222
|
+
const message = (await response.text().catch(() => "")).slice(0, 500);
|
|
1223
|
+
throw new Error(
|
|
1224
|
+
`CBB download failed with HTTP ${response.status}${message ? `: ${message}` : ""}`
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
1228
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_ARCHIVE_BYTES) {
|
|
1229
|
+
throw new Error("CBB download exceeds the 512 MiB limit.");
|
|
1230
|
+
}
|
|
1231
|
+
const hash = createHash("sha256");
|
|
1232
|
+
const file = await open(archivePath, "wx");
|
|
1233
|
+
let downloaded = 0;
|
|
1234
|
+
try {
|
|
1235
|
+
const reader = response.body.getReader();
|
|
1236
|
+
while (true) {
|
|
1237
|
+
const { value, done } = await reader.read();
|
|
1238
|
+
if (done) break;
|
|
1239
|
+
downloaded += value.byteLength;
|
|
1240
|
+
if (downloaded > MAX_ARCHIVE_BYTES || downloaded > descriptor.file_size) {
|
|
1241
|
+
await reader.cancel();
|
|
1242
|
+
throw new Error("CBB download exceeded the declared archive size.");
|
|
1243
|
+
}
|
|
1244
|
+
hash.update(value);
|
|
1245
|
+
await writeAll(file, value);
|
|
1246
|
+
}
|
|
1247
|
+
} finally {
|
|
1248
|
+
await file.close();
|
|
1249
|
+
}
|
|
1250
|
+
if (downloaded !== descriptor.file_size) {
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
`CBB archive size mismatch: expected ${descriptor.file_size}, received ${downloaded}.`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const actualSha256 = hash.digest("hex");
|
|
1256
|
+
if (actualSha256 !== descriptor.sha256.toLowerCase()) {
|
|
1257
|
+
throw new Error("CBB archive SHA256 verification failed.");
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function windowsReservedName(part) {
|
|
1261
|
+
const stem = part.split(".")[0].toUpperCase();
|
|
1262
|
+
return /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(stem);
|
|
1263
|
+
}
|
|
1264
|
+
function validateZipEntryPath(entryName) {
|
|
1265
|
+
const slashPath = entryName.replace(/\\/g, "/");
|
|
1266
|
+
if (!slashPath || slashPath.includes("\0") || slashPath.startsWith("/") || /^[A-Za-z]:/.test(slashPath) || slashPath.length > 4096) {
|
|
1267
|
+
throw new Error(`Unsafe ZIP entry path: ${entryName}`);
|
|
1268
|
+
}
|
|
1269
|
+
const directory = slashPath.endsWith("/");
|
|
1270
|
+
const parts = slashPath.split("/").filter(Boolean);
|
|
1271
|
+
if (parts.length === 0 || parts.some(
|
|
1272
|
+
(part) => part === "." || part === ".." || part.length > 255 || part.includes(":") || part.endsWith(".") || part.endsWith(" ") || windowsReservedName(part)
|
|
1273
|
+
)) {
|
|
1274
|
+
throw new Error(`Unsafe ZIP entry path: ${entryName}`);
|
|
1275
|
+
}
|
|
1276
|
+
return `${parts.join("/")}${directory ? "/" : ""}`;
|
|
1277
|
+
}
|
|
1278
|
+
function openZip(path) {
|
|
1279
|
+
return new Promise((resolvePromise, reject) => {
|
|
1280
|
+
yauzl.open(path, { lazyEntries: true, autoClose: true }, (error, zipFile) => {
|
|
1281
|
+
if (error || !zipFile) reject(error || new Error("Unable to open ZIP archive."));
|
|
1282
|
+
else resolvePromise(zipFile);
|
|
1283
|
+
});
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
function entryFileType(entry) {
|
|
1287
|
+
return entry.externalFileAttributes >>> 16 & 61440;
|
|
1288
|
+
}
|
|
1289
|
+
function extractEntry(zipFile, entry, outputPath, extractedBytes) {
|
|
1290
|
+
return new Promise((resolvePromise, reject) => {
|
|
1291
|
+
zipFile.openReadStream(entry, (error, stream) => {
|
|
1292
|
+
if (error || !stream) {
|
|
1293
|
+
reject(error || new Error(`Unable to read ZIP entry: ${entry.fileName}`));
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
let entryBytes = 0;
|
|
1297
|
+
const limiter = new Transform({
|
|
1298
|
+
transform(chunk, _encoding, callback) {
|
|
1299
|
+
entryBytes += chunk.length;
|
|
1300
|
+
extractedBytes.value += chunk.length;
|
|
1301
|
+
if (entryBytes > MAX_ENTRY_BYTES || extractedBytes.value > MAX_EXTRACTED_BYTES) {
|
|
1302
|
+
callback(new Error("CBB archive exceeds safe extraction limits."));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
callback(null, chunk);
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
pipeline(stream, limiter, createWriteStream(outputPath, { flags: "wx", mode: 420 })).then(
|
|
1309
|
+
() => {
|
|
1310
|
+
if (entryBytes !== entry.uncompressedSize) {
|
|
1311
|
+
reject(new Error(`ZIP entry size mismatch: ${entry.fileName}`));
|
|
1312
|
+
} else {
|
|
1313
|
+
resolvePromise();
|
|
1314
|
+
}
|
|
1315
|
+
},
|
|
1316
|
+
reject
|
|
1317
|
+
);
|
|
1318
|
+
});
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
async function extractZipSafely(archivePath, destination) {
|
|
1322
|
+
const zipFile = await openZip(archivePath);
|
|
1323
|
+
if (zipFile.entryCount > MAX_ZIP_ENTRIES) {
|
|
1324
|
+
zipFile.close();
|
|
1325
|
+
throw new Error(`CBB archive contains more than ${MAX_ZIP_ENTRIES} entries.`);
|
|
1326
|
+
}
|
|
1327
|
+
await mkdir4(destination, { recursive: true });
|
|
1328
|
+
const destinationRoot = `${resolve3(destination)}${sep}`;
|
|
1329
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1330
|
+
const extractedBytes = { value: 0 };
|
|
1331
|
+
await new Promise((resolvePromise, reject) => {
|
|
1332
|
+
let settled = false;
|
|
1333
|
+
const fail = (error) => {
|
|
1334
|
+
if (settled) return;
|
|
1335
|
+
settled = true;
|
|
1336
|
+
zipFile.close();
|
|
1337
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1338
|
+
};
|
|
1339
|
+
zipFile.on("error", fail);
|
|
1340
|
+
zipFile.on("end", () => {
|
|
1341
|
+
if (settled) return;
|
|
1342
|
+
settled = true;
|
|
1343
|
+
resolvePromise();
|
|
1344
|
+
});
|
|
1345
|
+
zipFile.on("entry", (entry) => {
|
|
1346
|
+
void (async () => {
|
|
1347
|
+
if ((entry.generalPurposeBitFlag & 1) !== 0) {
|
|
1348
|
+
throw new Error(`Encrypted ZIP entries are not supported: ${entry.fileName}`);
|
|
1349
|
+
}
|
|
1350
|
+
const normalizedName = validateZipEntryPath(entry.fileName);
|
|
1351
|
+
const collisionKey = normalizedName.toLowerCase();
|
|
1352
|
+
if (seenPaths.has(collisionKey)) {
|
|
1353
|
+
throw new Error(`Duplicate ZIP entry path: ${entry.fileName}`);
|
|
1354
|
+
}
|
|
1355
|
+
seenPaths.add(collisionKey);
|
|
1356
|
+
if (entry.uncompressedSize > MAX_ENTRY_BYTES || extractedBytes.value + entry.uncompressedSize > MAX_EXTRACTED_BYTES) {
|
|
1357
|
+
throw new Error("CBB archive exceeds safe extraction limits.");
|
|
1358
|
+
}
|
|
1359
|
+
if (entry.compressedSize > 0 && entry.uncompressedSize > 1024 * 1024 && entry.uncompressedSize / entry.compressedSize > MAX_COMPRESSION_RATIO) {
|
|
1360
|
+
throw new Error(`Suspicious ZIP compression ratio: ${entry.fileName}`);
|
|
1361
|
+
}
|
|
1362
|
+
const fileType = entryFileType(entry);
|
|
1363
|
+
if (fileType === 40960) {
|
|
1364
|
+
throw new Error(`Symbolic links are not allowed in CBB archives: ${entry.fileName}`);
|
|
1365
|
+
}
|
|
1366
|
+
const isDirectory = normalizedName.endsWith("/") || fileType === 16384;
|
|
1367
|
+
if (fileType !== 0 && fileType !== 16384 && fileType !== 32768) {
|
|
1368
|
+
throw new Error(`Special files are not allowed in CBB archives: ${entry.fileName}`);
|
|
1369
|
+
}
|
|
1370
|
+
const outputPath = resolve3(destination, ...normalizedName.split("/").filter(Boolean));
|
|
1371
|
+
if (!outputPath.startsWith(destinationRoot)) {
|
|
1372
|
+
throw new Error(`ZIP entry escapes the target directory: ${entry.fileName}`);
|
|
1373
|
+
}
|
|
1374
|
+
if (isDirectory) {
|
|
1375
|
+
await mkdir4(outputPath, { recursive: true });
|
|
1376
|
+
} else {
|
|
1377
|
+
await mkdir4(dirname6(outputPath), { recursive: true });
|
|
1378
|
+
await extractEntry(zipFile, entry, outputPath, extractedBytes);
|
|
1379
|
+
}
|
|
1380
|
+
zipFile.readEntry();
|
|
1381
|
+
})().catch(fail);
|
|
1382
|
+
});
|
|
1383
|
+
zipFile.readEntry();
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
async function validatePackageManifest(stagingPath, cbbId, version) {
|
|
1387
|
+
for (const filename of ["manifest.json", "cbb.json", "CBB.json"]) {
|
|
1388
|
+
const manifestPath = join4(stagingPath, filename);
|
|
1389
|
+
if (!await pathExists2(manifestPath)) continue;
|
|
1390
|
+
let manifest;
|
|
1391
|
+
try {
|
|
1392
|
+
manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
|
|
1393
|
+
} catch {
|
|
1394
|
+
throw new Error(`Invalid package manifest: ${filename}`);
|
|
1395
|
+
}
|
|
1396
|
+
if (!manifest || typeof manifest !== "object") {
|
|
1397
|
+
throw new Error(`Invalid package manifest: ${filename}`);
|
|
1398
|
+
}
|
|
1399
|
+
const record = manifest;
|
|
1400
|
+
const manifestId = typeof record.id === "string" ? record.id : typeof record.cbb_id === "string" ? record.cbb_id : void 0;
|
|
1401
|
+
if (manifestId && manifestId !== cbbId) {
|
|
1402
|
+
throw new Error(`CBB manifest ID mismatch: expected ${cbbId}, found ${manifestId}.`);
|
|
1403
|
+
}
|
|
1404
|
+
if (typeof record.version === "string" && record.version !== version) {
|
|
1405
|
+
throw new Error(
|
|
1406
|
+
`CBB manifest version mismatch: expected ${version}, found ${record.version}.`
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
async function writeLockfile(projectRoot, cbbId, entry) {
|
|
1413
|
+
const lockfile = await readLockfile(projectRoot);
|
|
1414
|
+
lockfile.packages[cbbId] = entry;
|
|
1415
|
+
const lockDir = join4(projectRoot, ".genrtl");
|
|
1416
|
+
const lockPath = join4(lockDir, "cbb-lock.json");
|
|
1417
|
+
const tempPath = join4(lockDir, `.cbb-lock-${randomUUID2()}.tmp`);
|
|
1418
|
+
const backupPath = join4(lockDir, `.cbb-lock-${randomUUID2()}.bak`);
|
|
1419
|
+
await mkdir4(lockDir, { recursive: true });
|
|
1420
|
+
await writeFile4(tempPath, `${JSON.stringify(lockfile, null, 2)}
|
|
1421
|
+
`, "utf8");
|
|
1422
|
+
const hadLockfile = await pathExists2(lockPath);
|
|
1423
|
+
try {
|
|
1424
|
+
if (hadLockfile) await rename(lockPath, backupPath);
|
|
1425
|
+
await rename(tempPath, lockPath);
|
|
1426
|
+
if (hadLockfile) await rm2(backupPath, { force: true });
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
await rm2(tempPath, { force: true }).catch(() => {
|
|
1429
|
+
});
|
|
1430
|
+
if (hadLockfile && await pathExists2(backupPath)) {
|
|
1431
|
+
await rename(backupPath, lockPath).catch(() => {
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
async function installCbbArtifact(descriptor, plan, force) {
|
|
1438
|
+
if (descriptor.format !== "zip") {
|
|
1439
|
+
throw new Error(`Unsupported CBB artifact format: ${descriptor.format}`);
|
|
1440
|
+
}
|
|
1441
|
+
const targetParent = dirname6(plan.targetPath);
|
|
1442
|
+
await mkdir4(targetParent, { recursive: true });
|
|
1443
|
+
const tempRoot = await mkdtemp(join4(targetParent, ".grtl-cbb-"));
|
|
1444
|
+
const archivePath = join4(tempRoot, "artifact.zip");
|
|
1445
|
+
const stagingPath = join4(tempRoot, "content");
|
|
1446
|
+
const backupPath = `${plan.targetPath}.grtl-backup-${randomUUID2()}`;
|
|
1447
|
+
let backupCreated = false;
|
|
1448
|
+
let installed = false;
|
|
1449
|
+
try {
|
|
1450
|
+
await downloadArtifact(descriptor, archivePath);
|
|
1451
|
+
await extractZipSafely(archivePath, stagingPath);
|
|
1452
|
+
await validatePackageManifest(stagingPath, descriptor.cbb_id, descriptor.version);
|
|
1453
|
+
if (await pathExists2(plan.targetPath)) {
|
|
1454
|
+
if (!force) {
|
|
1455
|
+
throw new Error(`Target already exists: ${plan.targetPath}. Use --force to replace it.`);
|
|
1456
|
+
}
|
|
1457
|
+
await rename(plan.targetPath, backupPath);
|
|
1458
|
+
backupCreated = true;
|
|
1459
|
+
}
|
|
1460
|
+
await rename(stagingPath, plan.targetPath);
|
|
1461
|
+
installed = true;
|
|
1462
|
+
await writeLockfile(plan.projectRoot, descriptor.cbb_id, {
|
|
1463
|
+
version: descriptor.version,
|
|
1464
|
+
target: plan.targetDir,
|
|
1465
|
+
sha256: descriptor.sha256.toLowerCase(),
|
|
1466
|
+
file_size: descriptor.file_size,
|
|
1467
|
+
receipt_id: descriptor.receipt_id,
|
|
1468
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1469
|
+
});
|
|
1470
|
+
if (backupCreated) {
|
|
1471
|
+
await rm2(backupPath, { recursive: true, force: true }).catch(() => {
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
return {
|
|
1475
|
+
cbb_id: descriptor.cbb_id,
|
|
1476
|
+
version: descriptor.version,
|
|
1477
|
+
target: plan.targetDir,
|
|
1478
|
+
target_path: plan.targetPath,
|
|
1479
|
+
sha256: descriptor.sha256.toLowerCase(),
|
|
1480
|
+
file_size: descriptor.file_size,
|
|
1481
|
+
receipt_id: descriptor.receipt_id,
|
|
1482
|
+
trace_id: descriptor.trace_id
|
|
1483
|
+
};
|
|
1484
|
+
} catch (error) {
|
|
1485
|
+
if (installed) {
|
|
1486
|
+
await rm2(plan.targetPath, { recursive: true, force: true }).catch(() => {
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
if (backupCreated && await pathExists2(backupPath)) {
|
|
1490
|
+
await rename(backupPath, plan.targetPath).catch(() => {
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
throw error;
|
|
1494
|
+
} finally {
|
|
1495
|
+
await rm2(tempRoot, { recursive: true, force: true }).catch(() => {
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/commands/cbb.ts
|
|
1501
|
+
async function installCbb(spec, options) {
|
|
1502
|
+
trackEvent("command", { name: "cbb-install" });
|
|
1503
|
+
const spinner = process.stdout.isTTY && !options.json ? ora3("Preparing CBB installation...").start() : null;
|
|
1504
|
+
try {
|
|
1505
|
+
const { cbbId, version } = parseCbbSpec(spec);
|
|
1506
|
+
const target = options.target || defaultCbbTarget(cbbId, version);
|
|
1507
|
+
const plan = await prepareCbbInstall(
|
|
1508
|
+
options.projectRoot || process.cwd(),
|
|
1509
|
+
target,
|
|
1510
|
+
cbbId,
|
|
1511
|
+
version,
|
|
1512
|
+
options.force || false
|
|
1513
|
+
);
|
|
1514
|
+
spinner && (spinner.text = "Acquiring CBB artifact...");
|
|
1515
|
+
const acquireInput = {
|
|
1516
|
+
cbb_id: cbbId,
|
|
1517
|
+
version,
|
|
1518
|
+
target_dir: plan.targetDir,
|
|
1519
|
+
workspace_id: plan.workspaceId,
|
|
1520
|
+
job_id: plan.jobId,
|
|
1521
|
+
idempotency_key: plan.idempotencyKey
|
|
1522
|
+
};
|
|
1523
|
+
const descriptor = await callGenrtlMcpTool(
|
|
1524
|
+
"genrtl_cbb_acquire",
|
|
1525
|
+
acquireInput
|
|
1526
|
+
);
|
|
1527
|
+
if (descriptor.cbb_id !== cbbId || descriptor.version !== version) {
|
|
1528
|
+
throw new Error("GenRTL returned a CBB artifact that does not match the request.");
|
|
1529
|
+
}
|
|
1530
|
+
spinner && (spinner.text = "Downloading and verifying CBB artifact...");
|
|
1531
|
+
const result = await installCbbArtifact(descriptor, plan, options.force || false);
|
|
1532
|
+
spinner?.succeed(`Installed ${cbbId}@${version}`);
|
|
1533
|
+
if (options.json) {
|
|
1534
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
log.blank();
|
|
1538
|
+
log.success(`Installed ${pc5.bold(`${cbbId}@${version}`)}`);
|
|
1539
|
+
log.item(`Target: ${result.target_path}`);
|
|
1540
|
+
log.item(`SHA256: ${result.sha256}`);
|
|
1541
|
+
log.item(`Lockfile: ${plan.projectRoot}/.genrtl/cbb-lock.json`);
|
|
1542
|
+
log.blank();
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1545
|
+
spinner?.fail(message);
|
|
1546
|
+
if (!spinner) log.error(message);
|
|
1547
|
+
process.exitCode = 1;
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function registerCbbCommands(program2) {
|
|
1551
|
+
const cbb = program2.command("cbb").description("Discover and install reusable RTL CBBs");
|
|
1552
|
+
cbb.command("install").description("Acquire, verify, and install a CBB ZIP into the current project").argument("<cbb>", "CBB coordinate in the form <cbb_id>@<version>").option("-t, --target <dir>", "Project-relative target directory").option("--project-root <dir>", "Project root (default: current directory)").option("-f, --force", "Replace an existing target after successful verification").option("--json", "Output the installation result as JSON").action(async (spec, options) => {
|
|
1553
|
+
await installCbb(spec, options);
|
|
1554
|
+
});
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1079
1557
|
// src/commands/upgrade.ts
|
|
1080
1558
|
import { confirm } from "@inquirer/prompts";
|
|
1081
1559
|
import { spawn } from "child_process";
|
|
1082
|
-
import
|
|
1560
|
+
import pc6 from "picocolors";
|
|
1083
1561
|
|
|
1084
1562
|
// src/utils/update-check.ts
|
|
1085
1563
|
import { homedir as homedir2 } from "os";
|
|
1086
|
-
import { dirname as
|
|
1087
|
-
import { mkdir as
|
|
1564
|
+
import { dirname as dirname7, join as join5 } from "path";
|
|
1565
|
+
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
1088
1566
|
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1089
|
-
var UPDATE_STATE_FILE =
|
|
1567
|
+
var UPDATE_STATE_FILE = join5(homedir2(), ".genrtl", "cli-state.json");
|
|
1090
1568
|
function getStateFilePath(stateFile) {
|
|
1091
1569
|
return stateFile ?? UPDATE_STATE_FILE;
|
|
1092
1570
|
}
|
|
1093
1571
|
async function readUpdateState(stateFile) {
|
|
1094
1572
|
try {
|
|
1095
|
-
const raw = await
|
|
1573
|
+
const raw = await readFile4(getStateFilePath(stateFile), "utf-8");
|
|
1096
1574
|
return JSON.parse(raw);
|
|
1097
1575
|
} catch {
|
|
1098
1576
|
return {};
|
|
@@ -1100,8 +1578,8 @@ async function readUpdateState(stateFile) {
|
|
|
1100
1578
|
}
|
|
1101
1579
|
async function writeUpdateState(state, stateFile) {
|
|
1102
1580
|
const path = getStateFilePath(stateFile);
|
|
1103
|
-
await
|
|
1104
|
-
await
|
|
1581
|
+
await mkdir5(dirname7(path), { recursive: true });
|
|
1582
|
+
await writeFile5(path, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1105
1583
|
}
|
|
1106
1584
|
function compareVersions(a, b) {
|
|
1107
1585
|
const normalize = (version) => version.split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -1277,20 +1755,20 @@ function registerUpgradeCommand(program2) {
|
|
|
1277
1755
|
});
|
|
1278
1756
|
}
|
|
1279
1757
|
function runCommand(command, args) {
|
|
1280
|
-
return new Promise((
|
|
1758
|
+
return new Promise((resolve4, reject) => {
|
|
1281
1759
|
const child = spawn(command, args, {
|
|
1282
1760
|
stdio: "inherit",
|
|
1283
1761
|
shell: process.platform === "win32"
|
|
1284
1762
|
});
|
|
1285
1763
|
child.on("error", reject);
|
|
1286
|
-
child.on("close", (code) =>
|
|
1764
|
+
child.on("close", (code) => resolve4(code));
|
|
1287
1765
|
});
|
|
1288
1766
|
}
|
|
1289
1767
|
async function runUpgradePlan(plan) {
|
|
1290
1768
|
return runCommand(plan.command, plan.args);
|
|
1291
1769
|
}
|
|
1292
1770
|
function showUpgradeFailureHelp(plan) {
|
|
1293
|
-
log.info(`Try rerunning: ${
|
|
1771
|
+
log.info(`Try rerunning: ${pc6.cyan(plan.displayCommand)}`);
|
|
1294
1772
|
const isGlobalNpmInstall = (plan.installMethod === "npm-global" || plan.installMethod === "unknown") && plan.command === "npm" && plan.args.includes("-g");
|
|
1295
1773
|
const isGlobalAltInstall = (plan.installMethod === "pnpm-global" || plan.installMethod === "bun-global") && plan.args.includes("-g");
|
|
1296
1774
|
if (isGlobalNpmInstall) {
|
|
@@ -1317,8 +1795,8 @@ async function maybeShowUpgradeNotice(options = {}) {
|
|
|
1317
1795
|
log.blank();
|
|
1318
1796
|
if (info.upgradePlan.needsExplicitVersion) {
|
|
1319
1797
|
log.box([
|
|
1320
|
-
`${
|
|
1321
|
-
`${
|
|
1798
|
+
`${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
|
|
1799
|
+
`${pc6.white("Use")} ${pc6.yellow(pc6.bold(info.upgradePlan.displayCommand))} ${pc6.white("to run the latest version")}`
|
|
1322
1800
|
]);
|
|
1323
1801
|
await markUpdateNotificationShown(info.latestVersion);
|
|
1324
1802
|
log.blank();
|
|
@@ -1326,18 +1804,18 @@ async function maybeShowUpgradeNotice(options = {}) {
|
|
|
1326
1804
|
}
|
|
1327
1805
|
if (!info.upgradePlan.canRun) {
|
|
1328
1806
|
log.box([
|
|
1329
|
-
`${
|
|
1330
|
-
`${
|
|
1331
|
-
`${
|
|
1807
|
+
`${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
|
|
1808
|
+
`${pc6.white("Run")} ${pc6.yellow(pc6.bold("grtl upgrade"))} ${pc6.white("for update steps")}`,
|
|
1809
|
+
`${pc6.white("Or run")} ${pc6.yellow(info.upgradePlan.displayCommand)}`
|
|
1332
1810
|
]);
|
|
1333
1811
|
await markUpdateNotificationShown(info.latestVersion);
|
|
1334
1812
|
log.blank();
|
|
1335
1813
|
return;
|
|
1336
1814
|
}
|
|
1337
1815
|
log.box([
|
|
1338
|
-
`${
|
|
1339
|
-
`${
|
|
1340
|
-
`${
|
|
1816
|
+
`${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
|
|
1817
|
+
`${pc6.white("Run")} ${pc6.yellow(pc6.bold("grtl upgrade"))} ${pc6.white("to update now")}`,
|
|
1818
|
+
`${pc6.white("Or run")} ${pc6.yellow(info.upgradePlan.displayCommand)}`
|
|
1341
1819
|
]);
|
|
1342
1820
|
await markUpdateNotificationShown(info.latestVersion);
|
|
1343
1821
|
log.blank();
|
|
@@ -1348,28 +1826,28 @@ async function upgradeCommand(options) {
|
|
|
1348
1826
|
const plan = info?.upgradePlan ?? getUpgradePlan();
|
|
1349
1827
|
if (!info) {
|
|
1350
1828
|
log.warn("Couldn't check for updates right now.");
|
|
1351
|
-
log.info(`Try again later or run ${
|
|
1829
|
+
log.info(`Try again later or run ${pc6.cyan(plan.displayCommand)} manually.`);
|
|
1352
1830
|
return;
|
|
1353
1831
|
}
|
|
1354
1832
|
if (!info.updateAvailable) {
|
|
1355
|
-
log.success(`grtl is up to date (${
|
|
1833
|
+
log.success(`grtl is up to date (${pc6.bold(`v${VERSION}`)})`);
|
|
1356
1834
|
return;
|
|
1357
1835
|
}
|
|
1358
1836
|
log.blank();
|
|
1359
1837
|
log.info(
|
|
1360
|
-
`Update available: ${
|
|
1838
|
+
`Update available: ${pc6.bold(`v${info.currentVersion}`)} ${pc6.dim("->")} ${pc6.bold(`v${info.latestVersion}`)}`
|
|
1361
1839
|
);
|
|
1362
1840
|
if (plan.needsExplicitVersion) {
|
|
1363
1841
|
log.info(`You're using an ephemeral runner (${plan.installMethod}).`);
|
|
1364
|
-
log.info(`Use ${
|
|
1365
|
-
log.info(`Or install globally with ${
|
|
1842
|
+
log.info(`Use ${pc6.cyan(plan.displayCommand)} to run the latest version immediately.`);
|
|
1843
|
+
log.info(`Or install globally with ${pc6.cyan("npm install -g @genrtl/grtl@latest")}.`);
|
|
1366
1844
|
return;
|
|
1367
1845
|
}
|
|
1368
1846
|
if (!plan.canRun) {
|
|
1369
|
-
log.info(`Run ${
|
|
1847
|
+
log.info(`Run ${pc6.cyan(plan.displayCommand)} to update your installed version.`);
|
|
1370
1848
|
return;
|
|
1371
1849
|
}
|
|
1372
|
-
log.info(`Upgrade command: ${
|
|
1850
|
+
log.info(`Upgrade command: ${pc6.cyan(plan.displayCommand)}`);
|
|
1373
1851
|
if (options.check) {
|
|
1374
1852
|
return;
|
|
1375
1853
|
}
|
|
@@ -1389,7 +1867,7 @@ async function upgradeCommand(options) {
|
|
|
1389
1867
|
if (exitCode === 0) {
|
|
1390
1868
|
log.blank();
|
|
1391
1869
|
log.success("Upgrade complete.");
|
|
1392
|
-
log.info(`Run ${
|
|
1870
|
+
log.info(`Run ${pc6.cyan("grtl --version")} to verify the installed version.`);
|
|
1393
1871
|
return;
|
|
1394
1872
|
}
|
|
1395
1873
|
log.blank();
|
|
@@ -1400,8 +1878,8 @@ async function upgradeCommand(options) {
|
|
|
1400
1878
|
|
|
1401
1879
|
// src/index.ts
|
|
1402
1880
|
var brand = {
|
|
1403
|
-
primary:
|
|
1404
|
-
dim:
|
|
1881
|
+
primary: pc7.green,
|
|
1882
|
+
dim: pc7.dim
|
|
1405
1883
|
};
|
|
1406
1884
|
var program = new Command2();
|
|
1407
1885
|
program.name("grtl").description("GenRTL CLI - Search RTL engineering knowledge and configure GenRTL").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
@@ -1423,15 +1901,20 @@ Examples:
|
|
|
1423
1901
|
${brand.primary("npx @genrtl/grtl setup --cli --codex --project")}
|
|
1424
1902
|
${brand.primary("GRTL_API_KEY=your_key npx @genrtl/grtl setup --mcp --codex --project")}
|
|
1425
1903
|
|
|
1426
|
-
${brand.dim("# Search the same
|
|
1904
|
+
${brand.dim("# Search the same five tools exposed by the GenRTL MCP server")}
|
|
1427
1905
|
${brand.primary('npx @genrtl/grtl knowledge-search "AXI stream backpressure design"')}
|
|
1428
1906
|
${brand.primary('npx @genrtl/grtl spec2rtl-search "Generate an APB register block"')}
|
|
1907
|
+
${brand.primary('npx @genrtl/grtl spec2plan-search "Plan an APB register block implementation"')}
|
|
1429
1908
|
${brand.primary('npx @genrtl/grtl verification-search "Verify an async FIFO"')}
|
|
1430
1909
|
${brand.primary('npx @genrtl/grtl debug-search "Explain this Vivado CDC warning"')}
|
|
1910
|
+
|
|
1911
|
+
${brand.dim("# Acquire and install a reusable RTL CBB")}
|
|
1912
|
+
${brand.primary("npx @genrtl/grtl cbb install cbb_uart@1.2.0")}
|
|
1431
1913
|
`
|
|
1432
1914
|
);
|
|
1433
1915
|
registerSetupCommand(program);
|
|
1434
1916
|
registerKnowledgeCommands(program);
|
|
1917
|
+
registerCbbCommands(program);
|
|
1435
1918
|
registerUpgradeCommand(program);
|
|
1436
1919
|
program.action(() => {
|
|
1437
1920
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
2
|
"name": "@genrtl/grtl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI, MCP, and coding-agent Skills for GenRTL RTL engineering knowledge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"grtl": "dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"build": "tsup",
|
|
14
|
-
"dev": "tsup --watch",
|
|
15
|
-
"typecheck": "tsc --noEmit",
|
|
16
|
-
"lint": "eslint src --fix",
|
|
17
|
-
"lint:check": "eslint src",
|
|
18
|
-
"format": "prettier --write src",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"lint": "eslint src --fix",
|
|
17
|
+
"lint:check": "eslint src",
|
|
18
|
+
"format": "prettier --write src",
|
|
19
19
|
"format:check": "prettier --check src",
|
|
20
20
|
"clean": "rm -rf dist node_modules",
|
|
21
21
|
"test": "vitest run",
|
|
@@ -25,28 +25,32 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@inquirer/prompts": "^8.2.0",
|
|
28
|
+
"boxen": "^8.0.1",
|
|
28
29
|
"commander": "^13.1.0",
|
|
29
30
|
"figlet": "^1.9.4",
|
|
31
|
+
"open": "^10.2.0",
|
|
30
32
|
"ora": "^9.0.0",
|
|
31
|
-
"picocolors": "^1.1.1"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"@types/
|
|
36
|
-
"@
|
|
37
|
-
"@
|
|
38
|
-
"eslint": "^
|
|
39
|
-
"eslint
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
33
|
+
"picocolors": "^1.1.1",
|
|
34
|
+
"yauzl": "^3.2.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/figlet": "^1.7.0",
|
|
38
|
+
"@types/node": "^22.19.1",
|
|
39
|
+
"@types/yauzl": "^2.10.3",
|
|
40
|
+
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
|
41
|
+
"@typescript-eslint/parser": "^8.28.0",
|
|
42
|
+
"eslint": "^9.34.0",
|
|
43
|
+
"eslint-plugin-prettier": "^5.2.5",
|
|
44
|
+
"prettier": "^3.6.2",
|
|
45
|
+
"tsup": "^8.5.0",
|
|
46
|
+
"typescript": "^5.8.2",
|
|
47
|
+
"typescript-eslint": "^8.28.0",
|
|
48
|
+
"vitest": "^4.0.13"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"genrtl",
|
|
52
|
+
"cli",
|
|
53
|
+
"ai",
|
|
50
54
|
"rtl",
|
|
51
55
|
"systemverilog",
|
|
52
56
|
"verilog",
|
|
@@ -54,9 +58,9 @@
|
|
|
54
58
|
"mcp"
|
|
55
59
|
],
|
|
56
60
|
"author": "xroting",
|
|
57
|
-
"license": "MIT",
|
|
58
|
-
"repository": {
|
|
59
|
-
"type": "git",
|
|
61
|
+
"license": "MIT",
|
|
62
|
+
"repository": {
|
|
63
|
+
"type": "git",
|
|
60
64
|
"url": "git+https://github.com/xroting/grtl.git",
|
|
61
65
|
"directory": "packages/cli"
|
|
62
66
|
},
|