@genrtl/grtl 0.3.0 → 0.4.1
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 +37 -9
- package/dist/index.js +521 -38
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -32,9 +32,18 @@ grtl setup --mcp --codex
|
|
|
32
32
|
grtl setup --mcp --cursor --project
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
Without `--cli` or `--mcp`, setup asks which mode to install.
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
Without `--cli` or `--mcp`, setup asks which mode to install.
|
|
36
|
+
|
|
37
|
+
Installing a newer npm package does not modify Skills already written to an
|
|
38
|
+
agent configuration directory. Run the matching setup command again after an
|
|
39
|
+
upgrade to refresh the Skill. For example:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
grtl setup --cli --cursor
|
|
43
|
+
grtl setup --cli --cursor --project
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
For Codex, Skills are installed under `.agents/skills` for project setup or
|
|
38
47
|
`~/.agents/skills` for global setup. MCP mode also updates
|
|
39
48
|
`.codex/config.toml` or `~/.codex/config.toml`.
|
|
40
49
|
|
|
@@ -54,12 +63,31 @@ grtl verification-search "Verify an async FIFO"
|
|
|
54
63
|
grtl debug-search "Explain this Vivado CDC warning"
|
|
55
64
|
```
|
|
56
65
|
|
|
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`.
|
|
61
|
-
|
|
62
|
-
##
|
|
66
|
+
Use `--json` for structured output. `--type` accepts `spec2rtl`, `spec2plan`, `verification`, or `debug`; other filters include
|
|
67
|
+
`--domain`, `--tool`, `--tool-version`, `--error-type`, `--severity`,
|
|
68
|
+
`--interface`, `--target`, `--tag`, `--top-k`, `--min-score`, and
|
|
69
|
+
`--workspace-id`.
|
|
70
|
+
|
|
71
|
+
## CBB Installation
|
|
72
|
+
|
|
73
|
+
Install an exact reusable RTL CBB version into the current project:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
grtl cbb install cbb_uart@1.2.0
|
|
77
|
+
grtl cbb install cbb_uart@1.2.0 --target rtl/vendor/uart
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The command calls the hosted `genrtl_cbb_acquire` MCP tool with
|
|
81
|
+
`GRTL_API_KEY`, downloads the short-lived ZIP artifact, verifies its size and
|
|
82
|
+
SHA-256, safely extracts it, and atomically installs it. The default target is
|
|
83
|
+
`rtl/cbb/<cbb_id>_<version>`.
|
|
84
|
+
|
|
85
|
+
Installed packages are recorded in `.genrtl/cbb-lock.json`. Existing targets
|
|
86
|
+
are left untouched unless `--force` is provided; replacement occurs only after
|
|
87
|
+
the new archive has been fully verified and extracted. Use `--json` for
|
|
88
|
+
machine-readable output.
|
|
89
|
+
|
|
90
|
+
## Development
|
|
63
91
|
|
|
64
92
|
```bash
|
|
65
93
|
pnpm install
|
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
|
|
@@ -381,15 +381,19 @@ Choose one tool:
|
|
|
381
381
|
- \`genrtl_debug_search\` for lint, CDC, compile, synthesis, and RTL bugs
|
|
382
382
|
|
|
383
383
|
Pass the complete engineering question in \`query\`. Add filters only when useful.`;
|
|
384
|
-
var FALLBACK_CLI = `Use the \`grtl\` CLI for grounded RTL engineering knowledge.
|
|
384
|
+
var FALLBACK_CLI = `Use the \`grtl\` CLI for grounded RTL engineering knowledge and reusable CBB installation.
|
|
385
385
|
|
|
386
|
-
|
|
386
|
+
For knowledge retrieval, choose one command:
|
|
387
387
|
- \`npx @genrtl/grtl@latest knowledge-search "<query>"\`
|
|
388
388
|
- \`npx @genrtl/grtl@latest spec2rtl-search "<query>"\`
|
|
389
389
|
- \`npx @genrtl/grtl@latest spec2plan-search "<query>"\`
|
|
390
390
|
- \`npx @genrtl/grtl@latest verification-search "<query>"\`
|
|
391
391
|
- \`npx @genrtl/grtl@latest debug-search "<query>"\`
|
|
392
392
|
|
|
393
|
+
To install a reusable RTL block into the current project:
|
|
394
|
+
- \`npx @genrtl/grtl@latest cbb install <cbb_id>@<version>\`
|
|
395
|
+
- Add \`--target <relative-dir>\` only when a non-default install path is needed.
|
|
396
|
+
|
|
393
397
|
Pass the complete engineering question. Add filters such as \`--tool\`,
|
|
394
398
|
\`--tool-version\`, \`--target\`, \`--interface\`, or \`--tag\` when known.
|
|
395
399
|
If authentication fails, set \`GRTL_API_KEY\` or \`GENRTL_API_KEY\` in the
|
|
@@ -422,15 +426,15 @@ Pass the complete engineering question in \`query\`. Add \`filters\`, \`top_k\`,
|
|
|
422
426
|
`;
|
|
423
427
|
var CLI_SKILL = `---
|
|
424
428
|
name: genrtl-cli
|
|
425
|
-
description: Use the grtl CLI for grounded RTL design, verification,
|
|
429
|
+
description: Use the grtl CLI for grounded RTL design, verification, debugging knowledge, and secure installation of reusable RTL CBBs.
|
|
426
430
|
---
|
|
427
431
|
|
|
428
432
|
# GenRTL CLI
|
|
429
433
|
|
|
430
|
-
Use this skill when an RTL engineering task needs grounded GenRTL knowledge
|
|
431
|
-
|
|
434
|
+
Use this skill when an RTL engineering task needs grounded GenRTL knowledge or
|
|
435
|
+
a reusable CBB must be installed into the user's local project.
|
|
432
436
|
|
|
433
|
-
|
|
437
|
+
For knowledge retrieval, choose exactly one command:
|
|
434
438
|
|
|
435
439
|
- \`grtl knowledge-search "<query>" --json\` for cross-domain RTL questions.
|
|
436
440
|
- \`grtl spec2rtl-search "<query>" --json\` for requirements, protocols, control logic, or algorithm-to-RTL work.
|
|
@@ -438,6 +442,12 @@ Choose exactly one command:
|
|
|
438
442
|
- \`grtl verification-search "<query>" --json\` for testbenches and verification.
|
|
439
443
|
- \`grtl debug-search "<query>" --json\` for lint, CDC, compile, synthesis, or RTL bugs.
|
|
440
444
|
|
|
445
|
+
For a CBB selected from GenRTL search results:
|
|
446
|
+
|
|
447
|
+
- \`grtl cbb install <cbb_id>@<version>\` downloads, verifies, and safely extracts it.
|
|
448
|
+
- Add \`--target <relative-dir>\` only when the user requests a specific project path.
|
|
449
|
+
- Do not download or extract the artifact manually; the CLI verifies SHA-256 and prevents unsafe ZIP paths.
|
|
450
|
+
|
|
441
451
|
Pass the complete engineering question. Add filters such as \`--tool\`,
|
|
442
452
|
\`--tool-version\`, \`--target\`, \`--interface\`, or \`--tag\` only when useful.
|
|
443
453
|
The CLI requires \`GRTL_API_KEY\` or \`GENRTL_API_KEY\` in its environment.
|
|
@@ -859,6 +869,9 @@ import { InvalidArgumentError } from "commander";
|
|
|
859
869
|
import pc4 from "picocolors";
|
|
860
870
|
import ora2 from "ora";
|
|
861
871
|
|
|
872
|
+
// src/utils/knowledge-api.ts
|
|
873
|
+
import { randomUUID } from "crypto";
|
|
874
|
+
|
|
862
875
|
// src/constants.ts
|
|
863
876
|
import { readFileSync } from "fs";
|
|
864
877
|
import { fileURLToPath } from "url";
|
|
@@ -901,7 +914,7 @@ function getStructuredMcpError(content) {
|
|
|
901
914
|
code: typeof errorContent.code === "string" ? errorContent.code : void 0
|
|
902
915
|
};
|
|
903
916
|
}
|
|
904
|
-
async function
|
|
917
|
+
async function callGenrtlMcpTool(toolName, input) {
|
|
905
918
|
const apiKey = getApiKey();
|
|
906
919
|
if (!apiKey) {
|
|
907
920
|
throw new Error("Authentication required. Set GRTL_API_KEY or GENRTL_API_KEY.");
|
|
@@ -933,14 +946,18 @@ async function callGenrtlKnowledgeTool(toolName, input) {
|
|
|
933
946
|
if (!result) throw new Error("GenRTL MCP returned an empty result.");
|
|
934
947
|
if (result.isError) {
|
|
935
948
|
const structuredError = getStructuredMcpError(result.structuredContent);
|
|
936
|
-
const message = structuredError?.error || getMcpErrorMessage(result.content) || "GenRTL
|
|
949
|
+
const message = structuredError?.error || getMcpErrorMessage(result.content) || "GenRTL MCP tool call failed.";
|
|
937
950
|
throw new Error(structuredError?.code ? `${message} (${structuredError.code})` : message);
|
|
938
951
|
}
|
|
939
952
|
if (!result.structuredContent) {
|
|
940
|
-
throw new Error("GenRTL MCP response did not include structured
|
|
953
|
+
throw new Error("GenRTL MCP response did not include structured results.");
|
|
941
954
|
}
|
|
942
955
|
return result.structuredContent;
|
|
943
956
|
}
|
|
957
|
+
async function callGenrtlKnowledgeTool(toolName, input) {
|
|
958
|
+
const requestInput = input.idempotency_key ? input : { ...input, idempotency_key: randomUUID() };
|
|
959
|
+
return callGenrtlMcpTool(toolName, requestInput);
|
|
960
|
+
}
|
|
944
961
|
|
|
945
962
|
// src/commands/knowledge.ts
|
|
946
963
|
var isTTY = process.stdout.isTTY;
|
|
@@ -1085,23 +1102,485 @@ function registerKnowledgeCommands(program2) {
|
|
|
1085
1102
|
}
|
|
1086
1103
|
}
|
|
1087
1104
|
|
|
1105
|
+
// src/commands/cbb.ts
|
|
1106
|
+
import ora3 from "ora";
|
|
1107
|
+
import pc5 from "picocolors";
|
|
1108
|
+
|
|
1109
|
+
// src/utils/cbb-install.ts
|
|
1110
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
1111
|
+
import { createWriteStream } from "fs";
|
|
1112
|
+
import {
|
|
1113
|
+
access as access3,
|
|
1114
|
+
mkdir as mkdir4,
|
|
1115
|
+
mkdtemp,
|
|
1116
|
+
open,
|
|
1117
|
+
readFile as readFile3,
|
|
1118
|
+
realpath,
|
|
1119
|
+
rename,
|
|
1120
|
+
rm as rm2,
|
|
1121
|
+
writeFile as writeFile4
|
|
1122
|
+
} from "fs/promises";
|
|
1123
|
+
import { dirname as dirname6, isAbsolute, join as join4, relative, resolve as resolve3, sep } from "path";
|
|
1124
|
+
import { Transform } from "stream";
|
|
1125
|
+
import { pipeline } from "stream/promises";
|
|
1126
|
+
import yauzl from "yauzl";
|
|
1127
|
+
var MAX_ARCHIVE_BYTES = 512 * 1024 * 1024;
|
|
1128
|
+
var MAX_EXTRACTED_BYTES = 512 * 1024 * 1024;
|
|
1129
|
+
var MAX_ENTRY_BYTES = 128 * 1024 * 1024;
|
|
1130
|
+
var MAX_COMPRESSION_RATIO = 200;
|
|
1131
|
+
var MAX_ZIP_ENTRIES = 1e4;
|
|
1132
|
+
function pathExists2(path) {
|
|
1133
|
+
return access3(path).then(
|
|
1134
|
+
() => true,
|
|
1135
|
+
() => false
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
function safePathPart(value) {
|
|
1139
|
+
return value.replace(/[^A-Za-z0-9._-]+/g, "_");
|
|
1140
|
+
}
|
|
1141
|
+
function parseCbbSpec(spec) {
|
|
1142
|
+
const separator = spec.lastIndexOf("@");
|
|
1143
|
+
if (separator <= 0 || separator === spec.length - 1) {
|
|
1144
|
+
throw new Error("CBB must use the form <cbb_id>@<version>.");
|
|
1145
|
+
}
|
|
1146
|
+
const cbbId = spec.slice(0, separator).trim();
|
|
1147
|
+
const version = spec.slice(separator + 1).trim();
|
|
1148
|
+
if (!cbbId || !version) {
|
|
1149
|
+
throw new Error("CBB must use the form <cbb_id>@<version>.");
|
|
1150
|
+
}
|
|
1151
|
+
return { cbbId, version };
|
|
1152
|
+
}
|
|
1153
|
+
function defaultCbbTarget(cbbId, version) {
|
|
1154
|
+
return `rtl/cbb/${safePathPart(cbbId)}_${safePathPart(version)}`;
|
|
1155
|
+
}
|
|
1156
|
+
function normalizeRelativeTarget(value) {
|
|
1157
|
+
const slashPath = value.trim().replace(/\\/g, "/").replace(/\/+/g, "/");
|
|
1158
|
+
if (!slashPath || slashPath.includes("\0") || slashPath.startsWith("/") || /^[A-Za-z]:/.test(slashPath)) {
|
|
1159
|
+
throw new Error("Target must be a non-empty project-relative path.");
|
|
1160
|
+
}
|
|
1161
|
+
const parts = slashPath.split("/").filter((part) => part && part !== ".");
|
|
1162
|
+
if (parts.length === 0 || parts.some(
|
|
1163
|
+
(part) => part === ".." || part.length > 255 || part.includes(":") || part.endsWith(".") || part.endsWith(" ") || windowsReservedName(part)
|
|
1164
|
+
)) {
|
|
1165
|
+
throw new Error("Target must not escape the project directory.");
|
|
1166
|
+
}
|
|
1167
|
+
return parts.join("/");
|
|
1168
|
+
}
|
|
1169
|
+
function resolveProjectTarget(projectRoot, targetDir) {
|
|
1170
|
+
const targetPath = resolve3(projectRoot, ...targetDir.split("/"));
|
|
1171
|
+
const relativePath = relative(projectRoot, targetPath);
|
|
1172
|
+
if (!relativePath || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
|
|
1173
|
+
throw new Error("Target must resolve inside the project directory.");
|
|
1174
|
+
}
|
|
1175
|
+
return targetPath;
|
|
1176
|
+
}
|
|
1177
|
+
async function readLockfile(projectRoot) {
|
|
1178
|
+
const lockPath = join4(projectRoot, ".genrtl", "cbb-lock.json");
|
|
1179
|
+
if (!await pathExists2(lockPath)) {
|
|
1180
|
+
return { schema_version: 1, packages: {} };
|
|
1181
|
+
}
|
|
1182
|
+
let parsed;
|
|
1183
|
+
try {
|
|
1184
|
+
parsed = JSON.parse(await readFile3(lockPath, "utf8"));
|
|
1185
|
+
} catch {
|
|
1186
|
+
throw new Error(`Invalid CBB lockfile: ${lockPath}`);
|
|
1187
|
+
}
|
|
1188
|
+
if (!parsed || typeof parsed !== "object" || parsed.schema_version !== 1 || !parsed.packages || typeof parsed.packages !== "object") {
|
|
1189
|
+
throw new Error(`Unsupported CBB lockfile format: ${lockPath}`);
|
|
1190
|
+
}
|
|
1191
|
+
return parsed;
|
|
1192
|
+
}
|
|
1193
|
+
async function prepareCbbInstall(projectRootInput, targetInput, cbbId, version, force) {
|
|
1194
|
+
const projectRoot = await realpath(resolve3(projectRootInput));
|
|
1195
|
+
const targetDir = normalizeRelativeTarget(targetInput);
|
|
1196
|
+
const targetPath = resolveProjectTarget(projectRoot, targetDir);
|
|
1197
|
+
if (!force && await pathExists2(targetPath)) {
|
|
1198
|
+
throw new Error(`Target already exists: ${targetPath}. Use --force to replace it.`);
|
|
1199
|
+
}
|
|
1200
|
+
await readLockfile(projectRoot);
|
|
1201
|
+
const projectHash = createHash("sha256").update(projectRoot).digest("hex");
|
|
1202
|
+
const operationHash = createHash("sha256").update(`${projectRoot}\0${targetDir}\0${cbbId}\0${version}`).digest("hex");
|
|
1203
|
+
return {
|
|
1204
|
+
projectRoot,
|
|
1205
|
+
targetDir,
|
|
1206
|
+
targetPath,
|
|
1207
|
+
workspaceId: `cli:${projectHash.slice(0, 40)}`,
|
|
1208
|
+
jobId: `cbb-install:${safePathPart(cbbId)}@${safePathPart(version)}:${operationHash.slice(0, 16)}`,
|
|
1209
|
+
idempotencyKey: `grtl-cbb-install:${operationHash}`
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
async function writeAll(file, chunk) {
|
|
1213
|
+
let offset = 0;
|
|
1214
|
+
while (offset < chunk.byteLength) {
|
|
1215
|
+
const { bytesWritten } = await file.write(chunk, offset, chunk.byteLength - offset, null);
|
|
1216
|
+
if (bytesWritten <= 0) throw new Error("Failed to write downloaded artifact.");
|
|
1217
|
+
offset += bytesWritten;
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
async function downloadArtifact(descriptor, archivePath) {
|
|
1221
|
+
if (!Number.isSafeInteger(descriptor.file_size) || descriptor.file_size <= 0 || descriptor.file_size > MAX_ARCHIVE_BYTES) {
|
|
1222
|
+
throw new Error("CBB archive size is invalid or exceeds the 512 MiB limit.");
|
|
1223
|
+
}
|
|
1224
|
+
if (!/^[a-fA-F0-9]{64}$/.test(descriptor.sha256)) {
|
|
1225
|
+
throw new Error("CBB archive SHA256 is invalid.");
|
|
1226
|
+
}
|
|
1227
|
+
const response = await fetch(descriptor.artifact_url, {
|
|
1228
|
+
redirect: "follow",
|
|
1229
|
+
headers: { Accept: "application/zip, application/octet-stream" }
|
|
1230
|
+
});
|
|
1231
|
+
if (!response.ok || !response.body) {
|
|
1232
|
+
const message = (await response.text().catch(() => "")).slice(0, 500);
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`CBB download failed with HTTP ${response.status}${message ? `: ${message}` : ""}`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
const contentLength = Number(response.headers.get("content-length"));
|
|
1238
|
+
if (Number.isFinite(contentLength) && contentLength > MAX_ARCHIVE_BYTES) {
|
|
1239
|
+
throw new Error("CBB download exceeds the 512 MiB limit.");
|
|
1240
|
+
}
|
|
1241
|
+
const hash = createHash("sha256");
|
|
1242
|
+
const file = await open(archivePath, "wx");
|
|
1243
|
+
let downloaded = 0;
|
|
1244
|
+
try {
|
|
1245
|
+
const reader = response.body.getReader();
|
|
1246
|
+
while (true) {
|
|
1247
|
+
const { value, done } = await reader.read();
|
|
1248
|
+
if (done) break;
|
|
1249
|
+
downloaded += value.byteLength;
|
|
1250
|
+
if (downloaded > MAX_ARCHIVE_BYTES || downloaded > descriptor.file_size) {
|
|
1251
|
+
await reader.cancel();
|
|
1252
|
+
throw new Error("CBB download exceeded the declared archive size.");
|
|
1253
|
+
}
|
|
1254
|
+
hash.update(value);
|
|
1255
|
+
await writeAll(file, value);
|
|
1256
|
+
}
|
|
1257
|
+
} finally {
|
|
1258
|
+
await file.close();
|
|
1259
|
+
}
|
|
1260
|
+
if (downloaded !== descriptor.file_size) {
|
|
1261
|
+
throw new Error(
|
|
1262
|
+
`CBB archive size mismatch: expected ${descriptor.file_size}, received ${downloaded}.`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
const actualSha256 = hash.digest("hex");
|
|
1266
|
+
if (actualSha256 !== descriptor.sha256.toLowerCase()) {
|
|
1267
|
+
throw new Error("CBB archive SHA256 verification failed.");
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function windowsReservedName(part) {
|
|
1271
|
+
const stem = part.split(".")[0].toUpperCase();
|
|
1272
|
+
return /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/.test(stem);
|
|
1273
|
+
}
|
|
1274
|
+
function validateZipEntryPath(entryName) {
|
|
1275
|
+
const slashPath = entryName.replace(/\\/g, "/");
|
|
1276
|
+
if (!slashPath || slashPath.includes("\0") || slashPath.startsWith("/") || /^[A-Za-z]:/.test(slashPath) || slashPath.length > 4096) {
|
|
1277
|
+
throw new Error(`Unsafe ZIP entry path: ${entryName}`);
|
|
1278
|
+
}
|
|
1279
|
+
const directory = slashPath.endsWith("/");
|
|
1280
|
+
const parts = slashPath.split("/").filter(Boolean);
|
|
1281
|
+
if (parts.length === 0 || parts.some(
|
|
1282
|
+
(part) => part === "." || part === ".." || part.length > 255 || part.includes(":") || part.endsWith(".") || part.endsWith(" ") || windowsReservedName(part)
|
|
1283
|
+
)) {
|
|
1284
|
+
throw new Error(`Unsafe ZIP entry path: ${entryName}`);
|
|
1285
|
+
}
|
|
1286
|
+
return `${parts.join("/")}${directory ? "/" : ""}`;
|
|
1287
|
+
}
|
|
1288
|
+
function openZip(path) {
|
|
1289
|
+
return new Promise((resolvePromise, reject) => {
|
|
1290
|
+
yauzl.open(path, { lazyEntries: true, autoClose: true }, (error, zipFile) => {
|
|
1291
|
+
if (error || !zipFile) reject(error || new Error("Unable to open ZIP archive."));
|
|
1292
|
+
else resolvePromise(zipFile);
|
|
1293
|
+
});
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
function entryFileType(entry) {
|
|
1297
|
+
return entry.externalFileAttributes >>> 16 & 61440;
|
|
1298
|
+
}
|
|
1299
|
+
function extractEntry(zipFile, entry, outputPath, extractedBytes) {
|
|
1300
|
+
return new Promise((resolvePromise, reject) => {
|
|
1301
|
+
zipFile.openReadStream(entry, (error, stream) => {
|
|
1302
|
+
if (error || !stream) {
|
|
1303
|
+
reject(error || new Error(`Unable to read ZIP entry: ${entry.fileName}`));
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
let entryBytes = 0;
|
|
1307
|
+
const limiter = new Transform({
|
|
1308
|
+
transform(chunk, _encoding, callback) {
|
|
1309
|
+
entryBytes += chunk.length;
|
|
1310
|
+
extractedBytes.value += chunk.length;
|
|
1311
|
+
if (entryBytes > MAX_ENTRY_BYTES || extractedBytes.value > MAX_EXTRACTED_BYTES) {
|
|
1312
|
+
callback(new Error("CBB archive exceeds safe extraction limits."));
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
callback(null, chunk);
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
pipeline(stream, limiter, createWriteStream(outputPath, { flags: "wx", mode: 420 })).then(
|
|
1319
|
+
() => {
|
|
1320
|
+
if (entryBytes !== entry.uncompressedSize) {
|
|
1321
|
+
reject(new Error(`ZIP entry size mismatch: ${entry.fileName}`));
|
|
1322
|
+
} else {
|
|
1323
|
+
resolvePromise();
|
|
1324
|
+
}
|
|
1325
|
+
},
|
|
1326
|
+
reject
|
|
1327
|
+
);
|
|
1328
|
+
});
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
async function extractZipSafely(archivePath, destination) {
|
|
1332
|
+
const zipFile = await openZip(archivePath);
|
|
1333
|
+
if (zipFile.entryCount > MAX_ZIP_ENTRIES) {
|
|
1334
|
+
zipFile.close();
|
|
1335
|
+
throw new Error(`CBB archive contains more than ${MAX_ZIP_ENTRIES} entries.`);
|
|
1336
|
+
}
|
|
1337
|
+
await mkdir4(destination, { recursive: true });
|
|
1338
|
+
const destinationRoot = `${resolve3(destination)}${sep}`;
|
|
1339
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
1340
|
+
const extractedBytes = { value: 0 };
|
|
1341
|
+
await new Promise((resolvePromise, reject) => {
|
|
1342
|
+
let settled = false;
|
|
1343
|
+
const fail = (error) => {
|
|
1344
|
+
if (settled) return;
|
|
1345
|
+
settled = true;
|
|
1346
|
+
zipFile.close();
|
|
1347
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
1348
|
+
};
|
|
1349
|
+
zipFile.on("error", fail);
|
|
1350
|
+
zipFile.on("end", () => {
|
|
1351
|
+
if (settled) return;
|
|
1352
|
+
settled = true;
|
|
1353
|
+
resolvePromise();
|
|
1354
|
+
});
|
|
1355
|
+
zipFile.on("entry", (entry) => {
|
|
1356
|
+
void (async () => {
|
|
1357
|
+
if ((entry.generalPurposeBitFlag & 1) !== 0) {
|
|
1358
|
+
throw new Error(`Encrypted ZIP entries are not supported: ${entry.fileName}`);
|
|
1359
|
+
}
|
|
1360
|
+
const normalizedName = validateZipEntryPath(entry.fileName);
|
|
1361
|
+
const collisionKey = normalizedName.toLowerCase();
|
|
1362
|
+
if (seenPaths.has(collisionKey)) {
|
|
1363
|
+
throw new Error(`Duplicate ZIP entry path: ${entry.fileName}`);
|
|
1364
|
+
}
|
|
1365
|
+
seenPaths.add(collisionKey);
|
|
1366
|
+
if (entry.uncompressedSize > MAX_ENTRY_BYTES || extractedBytes.value + entry.uncompressedSize > MAX_EXTRACTED_BYTES) {
|
|
1367
|
+
throw new Error("CBB archive exceeds safe extraction limits.");
|
|
1368
|
+
}
|
|
1369
|
+
if (entry.compressedSize > 0 && entry.uncompressedSize > 1024 * 1024 && entry.uncompressedSize / entry.compressedSize > MAX_COMPRESSION_RATIO) {
|
|
1370
|
+
throw new Error(`Suspicious ZIP compression ratio: ${entry.fileName}`);
|
|
1371
|
+
}
|
|
1372
|
+
const fileType = entryFileType(entry);
|
|
1373
|
+
if (fileType === 40960) {
|
|
1374
|
+
throw new Error(`Symbolic links are not allowed in CBB archives: ${entry.fileName}`);
|
|
1375
|
+
}
|
|
1376
|
+
const isDirectory = normalizedName.endsWith("/") || fileType === 16384;
|
|
1377
|
+
if (fileType !== 0 && fileType !== 16384 && fileType !== 32768) {
|
|
1378
|
+
throw new Error(`Special files are not allowed in CBB archives: ${entry.fileName}`);
|
|
1379
|
+
}
|
|
1380
|
+
const outputPath = resolve3(destination, ...normalizedName.split("/").filter(Boolean));
|
|
1381
|
+
if (!outputPath.startsWith(destinationRoot)) {
|
|
1382
|
+
throw new Error(`ZIP entry escapes the target directory: ${entry.fileName}`);
|
|
1383
|
+
}
|
|
1384
|
+
if (isDirectory) {
|
|
1385
|
+
await mkdir4(outputPath, { recursive: true });
|
|
1386
|
+
} else {
|
|
1387
|
+
await mkdir4(dirname6(outputPath), { recursive: true });
|
|
1388
|
+
await extractEntry(zipFile, entry, outputPath, extractedBytes);
|
|
1389
|
+
}
|
|
1390
|
+
zipFile.readEntry();
|
|
1391
|
+
})().catch(fail);
|
|
1392
|
+
});
|
|
1393
|
+
zipFile.readEntry();
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
async function validatePackageManifest(stagingPath, cbbId, version) {
|
|
1397
|
+
for (const filename of ["manifest.json", "cbb.json", "CBB.json"]) {
|
|
1398
|
+
const manifestPath = join4(stagingPath, filename);
|
|
1399
|
+
if (!await pathExists2(manifestPath)) continue;
|
|
1400
|
+
let manifest;
|
|
1401
|
+
try {
|
|
1402
|
+
manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
|
|
1403
|
+
} catch {
|
|
1404
|
+
throw new Error(`Invalid package manifest: ${filename}`);
|
|
1405
|
+
}
|
|
1406
|
+
if (!manifest || typeof manifest !== "object") {
|
|
1407
|
+
throw new Error(`Invalid package manifest: ${filename}`);
|
|
1408
|
+
}
|
|
1409
|
+
const record = manifest;
|
|
1410
|
+
const manifestId = typeof record.id === "string" ? record.id : typeof record.cbb_id === "string" ? record.cbb_id : void 0;
|
|
1411
|
+
if (manifestId && manifestId !== cbbId) {
|
|
1412
|
+
throw new Error(`CBB manifest ID mismatch: expected ${cbbId}, found ${manifestId}.`);
|
|
1413
|
+
}
|
|
1414
|
+
if (typeof record.version === "string" && record.version !== version) {
|
|
1415
|
+
throw new Error(
|
|
1416
|
+
`CBB manifest version mismatch: expected ${version}, found ${record.version}.`
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
async function writeLockfile(projectRoot, cbbId, entry) {
|
|
1423
|
+
const lockfile = await readLockfile(projectRoot);
|
|
1424
|
+
lockfile.packages[cbbId] = entry;
|
|
1425
|
+
const lockDir = join4(projectRoot, ".genrtl");
|
|
1426
|
+
const lockPath = join4(lockDir, "cbb-lock.json");
|
|
1427
|
+
const tempPath = join4(lockDir, `.cbb-lock-${randomUUID2()}.tmp`);
|
|
1428
|
+
const backupPath = join4(lockDir, `.cbb-lock-${randomUUID2()}.bak`);
|
|
1429
|
+
await mkdir4(lockDir, { recursive: true });
|
|
1430
|
+
await writeFile4(tempPath, `${JSON.stringify(lockfile, null, 2)}
|
|
1431
|
+
`, "utf8");
|
|
1432
|
+
const hadLockfile = await pathExists2(lockPath);
|
|
1433
|
+
try {
|
|
1434
|
+
if (hadLockfile) await rename(lockPath, backupPath);
|
|
1435
|
+
await rename(tempPath, lockPath);
|
|
1436
|
+
if (hadLockfile) await rm2(backupPath, { force: true });
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
await rm2(tempPath, { force: true }).catch(() => {
|
|
1439
|
+
});
|
|
1440
|
+
if (hadLockfile && await pathExists2(backupPath)) {
|
|
1441
|
+
await rename(backupPath, lockPath).catch(() => {
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
throw error;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
async function installCbbArtifact(descriptor, plan, force) {
|
|
1448
|
+
if (descriptor.format !== "zip") {
|
|
1449
|
+
throw new Error(`Unsupported CBB artifact format: ${descriptor.format}`);
|
|
1450
|
+
}
|
|
1451
|
+
const targetParent = dirname6(plan.targetPath);
|
|
1452
|
+
await mkdir4(targetParent, { recursive: true });
|
|
1453
|
+
const tempRoot = await mkdtemp(join4(targetParent, ".grtl-cbb-"));
|
|
1454
|
+
const archivePath = join4(tempRoot, "artifact.zip");
|
|
1455
|
+
const stagingPath = join4(tempRoot, "content");
|
|
1456
|
+
const backupPath = `${plan.targetPath}.grtl-backup-${randomUUID2()}`;
|
|
1457
|
+
let backupCreated = false;
|
|
1458
|
+
let installed = false;
|
|
1459
|
+
try {
|
|
1460
|
+
await downloadArtifact(descriptor, archivePath);
|
|
1461
|
+
await extractZipSafely(archivePath, stagingPath);
|
|
1462
|
+
await validatePackageManifest(stagingPath, descriptor.cbb_id, descriptor.version);
|
|
1463
|
+
if (await pathExists2(plan.targetPath)) {
|
|
1464
|
+
if (!force) {
|
|
1465
|
+
throw new Error(`Target already exists: ${plan.targetPath}. Use --force to replace it.`);
|
|
1466
|
+
}
|
|
1467
|
+
await rename(plan.targetPath, backupPath);
|
|
1468
|
+
backupCreated = true;
|
|
1469
|
+
}
|
|
1470
|
+
await rename(stagingPath, plan.targetPath);
|
|
1471
|
+
installed = true;
|
|
1472
|
+
await writeLockfile(plan.projectRoot, descriptor.cbb_id, {
|
|
1473
|
+
version: descriptor.version,
|
|
1474
|
+
target: plan.targetDir,
|
|
1475
|
+
sha256: descriptor.sha256.toLowerCase(),
|
|
1476
|
+
file_size: descriptor.file_size,
|
|
1477
|
+
receipt_id: descriptor.receipt_id,
|
|
1478
|
+
installed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1479
|
+
});
|
|
1480
|
+
if (backupCreated) {
|
|
1481
|
+
await rm2(backupPath, { recursive: true, force: true }).catch(() => {
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
cbb_id: descriptor.cbb_id,
|
|
1486
|
+
version: descriptor.version,
|
|
1487
|
+
target: plan.targetDir,
|
|
1488
|
+
target_path: plan.targetPath,
|
|
1489
|
+
sha256: descriptor.sha256.toLowerCase(),
|
|
1490
|
+
file_size: descriptor.file_size,
|
|
1491
|
+
receipt_id: descriptor.receipt_id,
|
|
1492
|
+
trace_id: descriptor.trace_id
|
|
1493
|
+
};
|
|
1494
|
+
} catch (error) {
|
|
1495
|
+
if (installed) {
|
|
1496
|
+
await rm2(plan.targetPath, { recursive: true, force: true }).catch(() => {
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
if (backupCreated && await pathExists2(backupPath)) {
|
|
1500
|
+
await rename(backupPath, plan.targetPath).catch(() => {
|
|
1501
|
+
});
|
|
1502
|
+
}
|
|
1503
|
+
throw error;
|
|
1504
|
+
} finally {
|
|
1505
|
+
await rm2(tempRoot, { recursive: true, force: true }).catch(() => {
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
// src/commands/cbb.ts
|
|
1511
|
+
async function installCbb(spec, options) {
|
|
1512
|
+
trackEvent("command", { name: "cbb-install" });
|
|
1513
|
+
const spinner = process.stdout.isTTY && !options.json ? ora3("Preparing CBB installation...").start() : null;
|
|
1514
|
+
try {
|
|
1515
|
+
const { cbbId, version } = parseCbbSpec(spec);
|
|
1516
|
+
const target = options.target || defaultCbbTarget(cbbId, version);
|
|
1517
|
+
const plan = await prepareCbbInstall(
|
|
1518
|
+
options.projectRoot || process.cwd(),
|
|
1519
|
+
target,
|
|
1520
|
+
cbbId,
|
|
1521
|
+
version,
|
|
1522
|
+
options.force || false
|
|
1523
|
+
);
|
|
1524
|
+
spinner && (spinner.text = "Acquiring CBB artifact...");
|
|
1525
|
+
const acquireInput = {
|
|
1526
|
+
cbb_id: cbbId,
|
|
1527
|
+
version,
|
|
1528
|
+
target_dir: plan.targetDir,
|
|
1529
|
+
workspace_id: plan.workspaceId,
|
|
1530
|
+
job_id: plan.jobId,
|
|
1531
|
+
idempotency_key: plan.idempotencyKey
|
|
1532
|
+
};
|
|
1533
|
+
const descriptor = await callGenrtlMcpTool(
|
|
1534
|
+
"genrtl_cbb_acquire",
|
|
1535
|
+
acquireInput
|
|
1536
|
+
);
|
|
1537
|
+
if (descriptor.cbb_id !== cbbId || descriptor.version !== version) {
|
|
1538
|
+
throw new Error("GenRTL returned a CBB artifact that does not match the request.");
|
|
1539
|
+
}
|
|
1540
|
+
spinner && (spinner.text = "Downloading and verifying CBB artifact...");
|
|
1541
|
+
const result = await installCbbArtifact(descriptor, plan, options.force || false);
|
|
1542
|
+
spinner?.succeed(`Installed ${cbbId}@${version}`);
|
|
1543
|
+
if (options.json) {
|
|
1544
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
log.blank();
|
|
1548
|
+
log.success(`Installed ${pc5.bold(`${cbbId}@${version}`)}`);
|
|
1549
|
+
log.item(`Target: ${result.target_path}`);
|
|
1550
|
+
log.item(`SHA256: ${result.sha256}`);
|
|
1551
|
+
log.item(`Lockfile: ${plan.projectRoot}/.genrtl/cbb-lock.json`);
|
|
1552
|
+
log.blank();
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1555
|
+
spinner?.fail(message);
|
|
1556
|
+
if (!spinner) log.error(message);
|
|
1557
|
+
process.exitCode = 1;
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
function registerCbbCommands(program2) {
|
|
1561
|
+
const cbb = program2.command("cbb").description("Discover and install reusable RTL CBBs");
|
|
1562
|
+
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) => {
|
|
1563
|
+
await installCbb(spec, options);
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1088
1567
|
// src/commands/upgrade.ts
|
|
1089
1568
|
import { confirm } from "@inquirer/prompts";
|
|
1090
1569
|
import { spawn } from "child_process";
|
|
1091
|
-
import
|
|
1570
|
+
import pc6 from "picocolors";
|
|
1092
1571
|
|
|
1093
1572
|
// src/utils/update-check.ts
|
|
1094
1573
|
import { homedir as homedir2 } from "os";
|
|
1095
|
-
import { dirname as
|
|
1096
|
-
import { mkdir as
|
|
1574
|
+
import { dirname as dirname7, join as join5 } from "path";
|
|
1575
|
+
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
|
|
1097
1576
|
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
1098
|
-
var UPDATE_STATE_FILE =
|
|
1577
|
+
var UPDATE_STATE_FILE = join5(homedir2(), ".genrtl", "cli-state.json");
|
|
1099
1578
|
function getStateFilePath(stateFile) {
|
|
1100
1579
|
return stateFile ?? UPDATE_STATE_FILE;
|
|
1101
1580
|
}
|
|
1102
1581
|
async function readUpdateState(stateFile) {
|
|
1103
1582
|
try {
|
|
1104
|
-
const raw = await
|
|
1583
|
+
const raw = await readFile4(getStateFilePath(stateFile), "utf-8");
|
|
1105
1584
|
return JSON.parse(raw);
|
|
1106
1585
|
} catch {
|
|
1107
1586
|
return {};
|
|
@@ -1109,8 +1588,8 @@ async function readUpdateState(stateFile) {
|
|
|
1109
1588
|
}
|
|
1110
1589
|
async function writeUpdateState(state, stateFile) {
|
|
1111
1590
|
const path = getStateFilePath(stateFile);
|
|
1112
|
-
await
|
|
1113
|
-
await
|
|
1591
|
+
await mkdir5(dirname7(path), { recursive: true });
|
|
1592
|
+
await writeFile5(path, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1114
1593
|
}
|
|
1115
1594
|
function compareVersions(a, b) {
|
|
1116
1595
|
const normalize = (version) => version.split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -1286,20 +1765,20 @@ function registerUpgradeCommand(program2) {
|
|
|
1286
1765
|
});
|
|
1287
1766
|
}
|
|
1288
1767
|
function runCommand(command, args) {
|
|
1289
|
-
return new Promise((
|
|
1768
|
+
return new Promise((resolve4, reject) => {
|
|
1290
1769
|
const child = spawn(command, args, {
|
|
1291
1770
|
stdio: "inherit",
|
|
1292
1771
|
shell: process.platform === "win32"
|
|
1293
1772
|
});
|
|
1294
1773
|
child.on("error", reject);
|
|
1295
|
-
child.on("close", (code) =>
|
|
1774
|
+
child.on("close", (code) => resolve4(code));
|
|
1296
1775
|
});
|
|
1297
1776
|
}
|
|
1298
1777
|
async function runUpgradePlan(plan) {
|
|
1299
1778
|
return runCommand(plan.command, plan.args);
|
|
1300
1779
|
}
|
|
1301
1780
|
function showUpgradeFailureHelp(plan) {
|
|
1302
|
-
log.info(`Try rerunning: ${
|
|
1781
|
+
log.info(`Try rerunning: ${pc6.cyan(plan.displayCommand)}`);
|
|
1303
1782
|
const isGlobalNpmInstall = (plan.installMethod === "npm-global" || plan.installMethod === "unknown") && plan.command === "npm" && plan.args.includes("-g");
|
|
1304
1783
|
const isGlobalAltInstall = (plan.installMethod === "pnpm-global" || plan.installMethod === "bun-global") && plan.args.includes("-g");
|
|
1305
1784
|
if (isGlobalNpmInstall) {
|
|
@@ -1326,8 +1805,8 @@ async function maybeShowUpgradeNotice(options = {}) {
|
|
|
1326
1805
|
log.blank();
|
|
1327
1806
|
if (info.upgradePlan.needsExplicitVersion) {
|
|
1328
1807
|
log.box([
|
|
1329
|
-
`${
|
|
1330
|
-
`${
|
|
1808
|
+
`${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
|
|
1809
|
+
`${pc6.white("Use")} ${pc6.yellow(pc6.bold(info.upgradePlan.displayCommand))} ${pc6.white("to run the latest version")}`
|
|
1331
1810
|
]);
|
|
1332
1811
|
await markUpdateNotificationShown(info.latestVersion);
|
|
1333
1812
|
log.blank();
|
|
@@ -1335,18 +1814,18 @@ async function maybeShowUpgradeNotice(options = {}) {
|
|
|
1335
1814
|
}
|
|
1336
1815
|
if (!info.upgradePlan.canRun) {
|
|
1337
1816
|
log.box([
|
|
1338
|
-
`${
|
|
1339
|
-
`${
|
|
1340
|
-
`${
|
|
1817
|
+
`${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
|
|
1818
|
+
`${pc6.white("Run")} ${pc6.yellow(pc6.bold("grtl upgrade"))} ${pc6.white("for update steps")}`,
|
|
1819
|
+
`${pc6.white("Or run")} ${pc6.yellow(info.upgradePlan.displayCommand)}`
|
|
1341
1820
|
]);
|
|
1342
1821
|
await markUpdateNotificationShown(info.latestVersion);
|
|
1343
1822
|
log.blank();
|
|
1344
1823
|
return;
|
|
1345
1824
|
}
|
|
1346
1825
|
log.box([
|
|
1347
|
-
`${
|
|
1348
|
-
`${
|
|
1349
|
-
`${
|
|
1826
|
+
`${pc6.white(pc6.bold("Update available:"))} ${pc6.green(pc6.bold(`v${info.currentVersion}`))} ${pc6.dim("->")} ${pc6.green(pc6.bold(`v${info.latestVersion}`))}`,
|
|
1827
|
+
`${pc6.white("Run")} ${pc6.yellow(pc6.bold("grtl upgrade"))} ${pc6.white("to update now")}`,
|
|
1828
|
+
`${pc6.white("Or run")} ${pc6.yellow(info.upgradePlan.displayCommand)}`
|
|
1350
1829
|
]);
|
|
1351
1830
|
await markUpdateNotificationShown(info.latestVersion);
|
|
1352
1831
|
log.blank();
|
|
@@ -1357,28 +1836,28 @@ async function upgradeCommand(options) {
|
|
|
1357
1836
|
const plan = info?.upgradePlan ?? getUpgradePlan();
|
|
1358
1837
|
if (!info) {
|
|
1359
1838
|
log.warn("Couldn't check for updates right now.");
|
|
1360
|
-
log.info(`Try again later or run ${
|
|
1839
|
+
log.info(`Try again later or run ${pc6.cyan(plan.displayCommand)} manually.`);
|
|
1361
1840
|
return;
|
|
1362
1841
|
}
|
|
1363
1842
|
if (!info.updateAvailable) {
|
|
1364
|
-
log.success(`grtl is up to date (${
|
|
1843
|
+
log.success(`grtl is up to date (${pc6.bold(`v${VERSION}`)})`);
|
|
1365
1844
|
return;
|
|
1366
1845
|
}
|
|
1367
1846
|
log.blank();
|
|
1368
1847
|
log.info(
|
|
1369
|
-
`Update available: ${
|
|
1848
|
+
`Update available: ${pc6.bold(`v${info.currentVersion}`)} ${pc6.dim("->")} ${pc6.bold(`v${info.latestVersion}`)}`
|
|
1370
1849
|
);
|
|
1371
1850
|
if (plan.needsExplicitVersion) {
|
|
1372
1851
|
log.info(`You're using an ephemeral runner (${plan.installMethod}).`);
|
|
1373
|
-
log.info(`Use ${
|
|
1374
|
-
log.info(`Or install globally with ${
|
|
1852
|
+
log.info(`Use ${pc6.cyan(plan.displayCommand)} to run the latest version immediately.`);
|
|
1853
|
+
log.info(`Or install globally with ${pc6.cyan("npm install -g @genrtl/grtl@latest")}.`);
|
|
1375
1854
|
return;
|
|
1376
1855
|
}
|
|
1377
1856
|
if (!plan.canRun) {
|
|
1378
|
-
log.info(`Run ${
|
|
1857
|
+
log.info(`Run ${pc6.cyan(plan.displayCommand)} to update your installed version.`);
|
|
1379
1858
|
return;
|
|
1380
1859
|
}
|
|
1381
|
-
log.info(`Upgrade command: ${
|
|
1860
|
+
log.info(`Upgrade command: ${pc6.cyan(plan.displayCommand)}`);
|
|
1382
1861
|
if (options.check) {
|
|
1383
1862
|
return;
|
|
1384
1863
|
}
|
|
@@ -1398,7 +1877,7 @@ async function upgradeCommand(options) {
|
|
|
1398
1877
|
if (exitCode === 0) {
|
|
1399
1878
|
log.blank();
|
|
1400
1879
|
log.success("Upgrade complete.");
|
|
1401
|
-
log.info(`Run ${
|
|
1880
|
+
log.info(`Run ${pc6.cyan("grtl --version")} to verify the installed version.`);
|
|
1402
1881
|
return;
|
|
1403
1882
|
}
|
|
1404
1883
|
log.blank();
|
|
@@ -1409,8 +1888,8 @@ async function upgradeCommand(options) {
|
|
|
1409
1888
|
|
|
1410
1889
|
// src/index.ts
|
|
1411
1890
|
var brand = {
|
|
1412
|
-
primary:
|
|
1413
|
-
dim:
|
|
1891
|
+
primary: pc7.green,
|
|
1892
|
+
dim: pc7.dim
|
|
1414
1893
|
};
|
|
1415
1894
|
var program = new Command2();
|
|
1416
1895
|
program.name("grtl").description("GenRTL CLI - Search RTL engineering knowledge and configure GenRTL").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
@@ -1438,10 +1917,14 @@ Examples:
|
|
|
1438
1917
|
${brand.primary('npx @genrtl/grtl spec2plan-search "Plan an APB register block implementation"')}
|
|
1439
1918
|
${brand.primary('npx @genrtl/grtl verification-search "Verify an async FIFO"')}
|
|
1440
1919
|
${brand.primary('npx @genrtl/grtl debug-search "Explain this Vivado CDC warning"')}
|
|
1920
|
+
|
|
1921
|
+
${brand.dim("# Acquire and install a reusable RTL CBB")}
|
|
1922
|
+
${brand.primary("npx @genrtl/grtl cbb install cbb_uart@1.2.0")}
|
|
1441
1923
|
`
|
|
1442
1924
|
);
|
|
1443
1925
|
registerSetupCommand(program);
|
|
1444
1926
|
registerKnowledgeCommands(program);
|
|
1927
|
+
registerCbbCommands(program);
|
|
1445
1928
|
registerUpgradeCommand(program);
|
|
1446
1929
|
program.action(() => {
|
|
1447
1930
|
console.log("");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genrtl/grtl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "CLI, MCP, and coding-agent Skills for GenRTL RTL engineering knowledge",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,14 +25,18 @@
|
|
|
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"
|
|
33
|
+
"picocolors": "^1.1.1",
|
|
34
|
+
"yauzl": "^3.2.0"
|
|
32
35
|
},
|
|
33
36
|
"devDependencies": {
|
|
34
37
|
"@types/figlet": "^1.7.0",
|
|
35
38
|
"@types/node": "^22.19.1",
|
|
39
|
+
"@types/yauzl": "^2.10.3",
|
|
36
40
|
"@typescript-eslint/eslint-plugin": "^8.28.0",
|
|
37
41
|
"@typescript-eslint/parser": "^8.28.0",
|
|
38
42
|
"eslint": "^9.34.0",
|