@genrtl/grtl 0.1.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 +21 -0
- package/README.md +106 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1217 -0
- package/package.json +73 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command2 } from "commander";
|
|
5
|
+
import pc6 from "picocolors";
|
|
6
|
+
import figlet from "figlet";
|
|
7
|
+
|
|
8
|
+
// src/commands/setup.ts
|
|
9
|
+
import pc3 from "picocolors";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
12
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
13
|
+
|
|
14
|
+
// src/utils/logger.ts
|
|
15
|
+
import pc from "picocolors";
|
|
16
|
+
var ANSI_PATTERN = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
|
|
17
|
+
function visibleLength(text) {
|
|
18
|
+
return text.replace(ANSI_PATTERN, "").length;
|
|
19
|
+
}
|
|
20
|
+
function padVisible(text, width) {
|
|
21
|
+
const padding = Math.max(0, width - visibleLength(text));
|
|
22
|
+
return text + " ".repeat(padding);
|
|
23
|
+
}
|
|
24
|
+
function box(lines, color = pc.green) {
|
|
25
|
+
const contentWidth = Math.max(...lines.map((line) => visibleLength(line)), 0);
|
|
26
|
+
const top = color(`\u250C${"\u2500".repeat(contentWidth + 2)}\u2510`);
|
|
27
|
+
const bottom = color(`\u2514${"\u2500".repeat(contentWidth + 2)}\u2518`);
|
|
28
|
+
console.log(top);
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
console.log(color("\u2502 ") + padVisible(line, contentWidth) + color(" \u2502"));
|
|
31
|
+
}
|
|
32
|
+
console.log(bottom);
|
|
33
|
+
}
|
|
34
|
+
var log = {
|
|
35
|
+
info: (message) => console.log(pc.cyan(message)),
|
|
36
|
+
success: (message) => console.log(pc.green(`\u2714 ${message}`)),
|
|
37
|
+
warn: (message) => console.log(pc.yellow(`\u26A0 ${message}`)),
|
|
38
|
+
error: (message) => console.log(pc.red(`\u2716 ${message}`)),
|
|
39
|
+
dim: (message) => console.log(pc.dim(message)),
|
|
40
|
+
item: (message) => console.log(pc.green(` ${message}`)),
|
|
41
|
+
itemAdd: (message) => console.log(` ${pc.green("+")} ${message}`),
|
|
42
|
+
plain: (message) => console.log(message),
|
|
43
|
+
blank: () => console.log(""),
|
|
44
|
+
box
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/utils/prompts.ts
|
|
48
|
+
import pc2 from "picocolors";
|
|
49
|
+
import { checkbox } from "@inquirer/prompts";
|
|
50
|
+
import readline from "readline";
|
|
51
|
+
async function checkboxWithHover(config, options) {
|
|
52
|
+
const choices = config.choices.filter(
|
|
53
|
+
(c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
|
|
54
|
+
);
|
|
55
|
+
const values = choices.map((c) => c.value);
|
|
56
|
+
const totalItems = values.length;
|
|
57
|
+
let cursorPosition = choices.findIndex((c) => !c.disabled);
|
|
58
|
+
if (cursorPosition < 0) cursorPosition = 0;
|
|
59
|
+
const getName = options?.getName ?? ((v) => v.name);
|
|
60
|
+
const keypressHandler = (_str, key) => {
|
|
61
|
+
if (key.name === "up") {
|
|
62
|
+
let next = cursorPosition - 1;
|
|
63
|
+
while (next >= 0 && choices[next].disabled) next--;
|
|
64
|
+
if (next >= 0) cursorPosition = next;
|
|
65
|
+
} else if (key.name === "down") {
|
|
66
|
+
let next = cursorPosition + 1;
|
|
67
|
+
while (next < totalItems && choices[next].disabled) next++;
|
|
68
|
+
if (next < totalItems) cursorPosition = next;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
readline.emitKeypressEvents(process.stdin);
|
|
72
|
+
process.stdin.on("keypress", keypressHandler);
|
|
73
|
+
const customConfig = {
|
|
74
|
+
...config,
|
|
75
|
+
theme: {
|
|
76
|
+
...config.theme,
|
|
77
|
+
style: {
|
|
78
|
+
answer: (text) => pc2.green(text),
|
|
79
|
+
...config.theme?.style,
|
|
80
|
+
highlight: (text) => pc2.green(text),
|
|
81
|
+
renderSelectedChoices: (selected, _allChoices) => {
|
|
82
|
+
if (selected.length === 0) {
|
|
83
|
+
return pc2.dim(getName(values[cursorPosition]));
|
|
84
|
+
}
|
|
85
|
+
return selected.map((c) => getName(c.value)).join(", ");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const selected = await checkbox(customConfig);
|
|
92
|
+
if (selected.length === 0) {
|
|
93
|
+
return [values[cursorPosition]];
|
|
94
|
+
}
|
|
95
|
+
return selected;
|
|
96
|
+
} finally {
|
|
97
|
+
process.stdin.removeListener("keypress", keypressHandler);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/utils/tracking.ts
|
|
102
|
+
function trackEvent(_event, _data) {
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// src/setup/http-agents.ts
|
|
106
|
+
import { access } from "fs/promises";
|
|
107
|
+
import { homedir } from "os";
|
|
108
|
+
import { join } from "path";
|
|
109
|
+
var SETUP_AGENT_NAMES = {
|
|
110
|
+
claude: "Claude Code",
|
|
111
|
+
cursor: "Cursor",
|
|
112
|
+
opencode: "OpenCode",
|
|
113
|
+
codex: "Codex",
|
|
114
|
+
antigravity: "Antigravity",
|
|
115
|
+
gemini: "Gemini CLI"
|
|
116
|
+
};
|
|
117
|
+
var mcpBaseUrl = "https://www.genrtl.com/api/mcp";
|
|
118
|
+
function setMcpBaseUrl(url) {
|
|
119
|
+
const normalized = url.replace(/\/+$/, "");
|
|
120
|
+
mcpBaseUrl = normalized.endsWith("/api/mcp") ? normalized : `${normalized}/api/mcp`;
|
|
121
|
+
}
|
|
122
|
+
function headers(auth) {
|
|
123
|
+
return { Authorization: `Bearer ${auth.apiKey}` };
|
|
124
|
+
}
|
|
125
|
+
function claudeConfigDir() {
|
|
126
|
+
return process.env.CLAUDE_CONFIG_DIR || join(homedir(), ".claude");
|
|
127
|
+
}
|
|
128
|
+
function claudeGlobalMcpPath() {
|
|
129
|
+
return process.env.CLAUDE_CONFIG_DIR ? join(claudeConfigDir(), ".claude.json") : join(homedir(), ".claude.json");
|
|
130
|
+
}
|
|
131
|
+
var agents = {
|
|
132
|
+
claude: {
|
|
133
|
+
name: "claude",
|
|
134
|
+
displayName: "Claude Code",
|
|
135
|
+
mcp: {
|
|
136
|
+
projectPaths: [".mcp.json"],
|
|
137
|
+
get globalPaths() {
|
|
138
|
+
return [claudeGlobalMcpPath()];
|
|
139
|
+
},
|
|
140
|
+
configKey: "mcpServers",
|
|
141
|
+
buildEntry: (auth) => ({
|
|
142
|
+
type: "http",
|
|
143
|
+
url: mcpBaseUrl,
|
|
144
|
+
headers: headers(auth)
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
rule: {
|
|
148
|
+
kind: "file",
|
|
149
|
+
dir: (scope) => scope === "global" ? join(claudeConfigDir(), "rules") : join(".claude", "rules"),
|
|
150
|
+
filename: "genrtl.md"
|
|
151
|
+
},
|
|
152
|
+
detect: {
|
|
153
|
+
projectPaths: [".mcp.json", ".claude"],
|
|
154
|
+
get globalPaths() {
|
|
155
|
+
return [claudeConfigDir()];
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
cursor: {
|
|
160
|
+
name: "cursor",
|
|
161
|
+
displayName: "Cursor",
|
|
162
|
+
mcp: {
|
|
163
|
+
projectPaths: [join(".cursor", "mcp.json")],
|
|
164
|
+
globalPaths: [join(homedir(), ".cursor", "mcp.json")],
|
|
165
|
+
configKey: "mcpServers",
|
|
166
|
+
buildEntry: (auth) => ({ url: mcpBaseUrl, headers: headers(auth) })
|
|
167
|
+
},
|
|
168
|
+
rule: {
|
|
169
|
+
kind: "file",
|
|
170
|
+
dir: (scope) => scope === "global" ? join(homedir(), ".cursor", "rules") : join(".cursor", "rules"),
|
|
171
|
+
filename: "genrtl.mdc"
|
|
172
|
+
},
|
|
173
|
+
detect: {
|
|
174
|
+
projectPaths: [".cursor"],
|
|
175
|
+
globalPaths: [join(homedir(), ".cursor")]
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
opencode: {
|
|
179
|
+
name: "opencode",
|
|
180
|
+
displayName: "OpenCode",
|
|
181
|
+
mcp: {
|
|
182
|
+
projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
|
|
183
|
+
globalPaths: [
|
|
184
|
+
join(homedir(), ".config", "opencode", "opencode.json"),
|
|
185
|
+
join(homedir(), ".config", "opencode", "opencode.jsonc"),
|
|
186
|
+
join(homedir(), ".config", "opencode", ".opencode.json"),
|
|
187
|
+
join(homedir(), ".config", "opencode", ".opencode.jsonc")
|
|
188
|
+
],
|
|
189
|
+
configKey: "mcp",
|
|
190
|
+
buildEntry: (auth) => ({
|
|
191
|
+
type: "remote",
|
|
192
|
+
url: mcpBaseUrl,
|
|
193
|
+
enabled: true,
|
|
194
|
+
headers: headers(auth)
|
|
195
|
+
})
|
|
196
|
+
},
|
|
197
|
+
rule: {
|
|
198
|
+
kind: "append",
|
|
199
|
+
file: (scope) => scope === "global" ? join(homedir(), ".config", "opencode", "AGENTS.md") : "AGENTS.md",
|
|
200
|
+
sectionMarker: "<!-- genrtl -->"
|
|
201
|
+
},
|
|
202
|
+
detect: {
|
|
203
|
+
projectPaths: ["opencode.json", "opencode.jsonc", ".opencode.json", ".opencode.jsonc"],
|
|
204
|
+
globalPaths: [join(homedir(), ".config", "opencode")]
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
codex: {
|
|
208
|
+
name: "codex",
|
|
209
|
+
displayName: "Codex",
|
|
210
|
+
mcp: {
|
|
211
|
+
projectPaths: [join(".codex", "config.toml")],
|
|
212
|
+
globalPaths: [join(homedir(), ".codex", "config.toml")],
|
|
213
|
+
configKey: "mcp_servers",
|
|
214
|
+
buildEntry: (auth) => auth.apiKeyEnvVar ? {
|
|
215
|
+
type: "http",
|
|
216
|
+
url: mcpBaseUrl,
|
|
217
|
+
bearer_token_env_var: auth.apiKeyEnvVar
|
|
218
|
+
} : {
|
|
219
|
+
type: "http",
|
|
220
|
+
url: mcpBaseUrl,
|
|
221
|
+
headers: headers(auth)
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
rule: {
|
|
225
|
+
kind: "append",
|
|
226
|
+
file: (scope) => scope === "global" ? join(homedir(), ".codex", "AGENTS.md") : "AGENTS.md",
|
|
227
|
+
sectionMarker: "<!-- genrtl -->"
|
|
228
|
+
},
|
|
229
|
+
detect: {
|
|
230
|
+
projectPaths: [".codex"],
|
|
231
|
+
globalPaths: [join(homedir(), ".codex")]
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
antigravity: {
|
|
235
|
+
name: "antigravity",
|
|
236
|
+
displayName: "Antigravity",
|
|
237
|
+
mcp: {
|
|
238
|
+
projectPaths: [],
|
|
239
|
+
globalPaths: [join(homedir(), ".gemini", "antigravity", "mcp_config.json")],
|
|
240
|
+
configKey: "mcpServers",
|
|
241
|
+
buildEntry: (auth) => ({ serverUrl: mcpBaseUrl, headers: headers(auth) })
|
|
242
|
+
},
|
|
243
|
+
rule: {
|
|
244
|
+
kind: "append",
|
|
245
|
+
file: (scope) => scope === "global" ? join(homedir(), ".gemini", "GEMINI.md") : "GEMINI.md",
|
|
246
|
+
sectionMarker: "<!-- genrtl -->"
|
|
247
|
+
},
|
|
248
|
+
detect: {
|
|
249
|
+
projectPaths: [".agent"],
|
|
250
|
+
globalPaths: [join(homedir(), ".gemini", "antigravity"), join(homedir(), ".agent")]
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
gemini: {
|
|
254
|
+
name: "gemini",
|
|
255
|
+
displayName: "Gemini CLI",
|
|
256
|
+
mcp: {
|
|
257
|
+
projectPaths: [join(".gemini", "settings.json")],
|
|
258
|
+
globalPaths: [join(homedir(), ".gemini", "settings.json")],
|
|
259
|
+
configKey: "mcpServers",
|
|
260
|
+
buildEntry: (auth) => ({ httpUrl: mcpBaseUrl, headers: headers(auth) })
|
|
261
|
+
},
|
|
262
|
+
rule: {
|
|
263
|
+
kind: "append",
|
|
264
|
+
file: (scope) => scope === "global" ? join(homedir(), ".gemini", "GEMINI.md") : "GEMINI.md",
|
|
265
|
+
sectionMarker: "<!-- genrtl -->"
|
|
266
|
+
},
|
|
267
|
+
detect: {
|
|
268
|
+
projectPaths: [".gemini"],
|
|
269
|
+
globalPaths: [join(homedir(), ".gemini")]
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
function getAgent(name) {
|
|
274
|
+
return agents[name];
|
|
275
|
+
}
|
|
276
|
+
var ALL_AGENT_NAMES = Object.keys(agents);
|
|
277
|
+
async function pathExists(path) {
|
|
278
|
+
try {
|
|
279
|
+
await access(path);
|
|
280
|
+
return true;
|
|
281
|
+
} catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function detectAgents(scope) {
|
|
286
|
+
const detected = [];
|
|
287
|
+
for (const agent of Object.values(agents)) {
|
|
288
|
+
const paths = scope === "global" ? agent.detect.globalPaths : agent.detect.projectPaths;
|
|
289
|
+
for (const path of paths) {
|
|
290
|
+
const fullPath = scope === "global" ? path : join(process.cwd(), path);
|
|
291
|
+
if (await pathExists(fullPath)) {
|
|
292
|
+
detected.push(agent.name);
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return detected;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// src/setup/templates.ts
|
|
301
|
+
var GITHUB_RAW_URLS = ["https://raw.githubusercontent.com/xroting/grtl/main/rules"];
|
|
302
|
+
var FALLBACK_MCP = `Use GenRTL MCP tools for grounded RTL engineering knowledge.
|
|
303
|
+
|
|
304
|
+
Choose one tool:
|
|
305
|
+
- \`genrtl_knowledge_search\` for cross-domain RTL questions
|
|
306
|
+
- \`genrtl_spec2rtl_search\` for requirements and RTL design
|
|
307
|
+
- \`genrtl_verification_search\` for testbenches and verification
|
|
308
|
+
- \`genrtl_debug_search\` for lint, CDC, compile, synthesis, and RTL bugs
|
|
309
|
+
|
|
310
|
+
Pass the complete engineering question in \`query\`. Add filters only when useful.`;
|
|
311
|
+
var FALLBACK_CLI = `Use the \`grtl\` CLI for grounded RTL engineering knowledge.
|
|
312
|
+
|
|
313
|
+
Choose one command:
|
|
314
|
+
- \`npx @genrtl/grtl@latest knowledge-search "<query>"\`
|
|
315
|
+
- \`npx @genrtl/grtl@latest spec2rtl-search "<query>"\`
|
|
316
|
+
- \`npx @genrtl/grtl@latest verification-search "<query>"\`
|
|
317
|
+
- \`npx @genrtl/grtl@latest debug-search "<query>"\`
|
|
318
|
+
|
|
319
|
+
Pass the complete engineering question. Add filters such as \`--tool\`,
|
|
320
|
+
\`--tool-version\`, \`--target\`, \`--interface\`, or \`--tag\` when known.
|
|
321
|
+
If authentication fails, set \`GRTL_API_KEY\`.`;
|
|
322
|
+
var CURSOR_FRONTMATTER = `---
|
|
323
|
+
alwaysApply: true
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
`;
|
|
327
|
+
var CODEX_CLI_SANDBOX_GUIDANCE = `Run GenRTL CLI requests outside Codex's default sandbox. If a GenRTL CLI command fails with DNS or network errors such as ENOTFOUND, host resolution failures, or fetch failed, rerun it outside the sandbox instead of retrying inside the sandbox.`;
|
|
328
|
+
async function fetchRule(filename, fallback) {
|
|
329
|
+
for (const base of GITHUB_RAW_URLS) {
|
|
330
|
+
try {
|
|
331
|
+
const res = await fetch(`${base}/${filename}`);
|
|
332
|
+
if (res.ok) return await res.text();
|
|
333
|
+
} catch {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return fallback;
|
|
338
|
+
}
|
|
339
|
+
async function getRuleContent(mode, agent) {
|
|
340
|
+
const [filename, fallback] = mode === "mcp" ? ["genrtl-mcp.md", FALLBACK_MCP] : ["genrtl-cli.md", FALLBACK_CLI];
|
|
341
|
+
let body = await fetchRule(filename, fallback);
|
|
342
|
+
if (mode === "cli" && agent === "codex" && !body.includes(CODEX_CLI_SANDBOX_GUIDANCE)) {
|
|
343
|
+
body = `${body.trimEnd()}
|
|
344
|
+
${CODEX_CLI_SANDBOX_GUIDANCE}
|
|
345
|
+
`;
|
|
346
|
+
}
|
|
347
|
+
return agent === "cursor" ? `${CURSOR_FRONTMATTER}${body}` : body;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// src/setup/mcp-writer.ts
|
|
351
|
+
import { access as access2, readFile, writeFile, mkdir } from "fs/promises";
|
|
352
|
+
import { dirname } from "path";
|
|
353
|
+
function stripJsonComments(text) {
|
|
354
|
+
let result = "";
|
|
355
|
+
let i = 0;
|
|
356
|
+
while (i < text.length) {
|
|
357
|
+
if (text[i] === '"') {
|
|
358
|
+
const start = i++;
|
|
359
|
+
while (i < text.length && text[i] !== '"') {
|
|
360
|
+
if (text[i] === "\\") i++;
|
|
361
|
+
i++;
|
|
362
|
+
}
|
|
363
|
+
result += text.slice(start, ++i);
|
|
364
|
+
} else if (text[i] === "/" && text[i + 1] === "/") {
|
|
365
|
+
i += 2;
|
|
366
|
+
while (i < text.length && text[i] !== "\n") i++;
|
|
367
|
+
} else if (text[i] === "/" && text[i + 1] === "*") {
|
|
368
|
+
i += 2;
|
|
369
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i++;
|
|
370
|
+
i += 2;
|
|
371
|
+
} else {
|
|
372
|
+
result += text[i++];
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
async function readJsonConfig(filePath) {
|
|
378
|
+
let raw;
|
|
379
|
+
try {
|
|
380
|
+
raw = await readFile(filePath, "utf-8");
|
|
381
|
+
} catch {
|
|
382
|
+
return {};
|
|
383
|
+
}
|
|
384
|
+
raw = raw.trim();
|
|
385
|
+
if (!raw) return {};
|
|
386
|
+
return JSON.parse(stripJsonComments(raw));
|
|
387
|
+
}
|
|
388
|
+
function mergeServerEntry(existing, configKey, serverName, entry) {
|
|
389
|
+
const section = existing[configKey] ?? {};
|
|
390
|
+
const alreadyExists = serverName in section;
|
|
391
|
+
return {
|
|
392
|
+
config: {
|
|
393
|
+
...existing,
|
|
394
|
+
[configKey]: {
|
|
395
|
+
...section,
|
|
396
|
+
[serverName]: entry
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
alreadyExists
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function resolveMcpPath(candidates) {
|
|
403
|
+
for (const candidate of candidates) {
|
|
404
|
+
try {
|
|
405
|
+
await access2(candidate);
|
|
406
|
+
return candidate;
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return candidates[0];
|
|
411
|
+
}
|
|
412
|
+
async function writeJsonConfig(filePath, config) {
|
|
413
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
414
|
+
await writeFile(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
415
|
+
}
|
|
416
|
+
function buildTomlServerBlock(serverName, entry) {
|
|
417
|
+
const lines = [`[mcp_servers.${serverName}]`];
|
|
418
|
+
const headers2 = entry.headers;
|
|
419
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
420
|
+
if (key === "headers") continue;
|
|
421
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
422
|
+
}
|
|
423
|
+
if (headers2 && Object.keys(headers2).length > 0) {
|
|
424
|
+
lines.push("");
|
|
425
|
+
lines.push(`[mcp_servers.${serverName}.http_headers]`);
|
|
426
|
+
for (const [key, value] of Object.entries(headers2)) {
|
|
427
|
+
lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return lines.join("\n") + "\n";
|
|
431
|
+
}
|
|
432
|
+
async function appendTomlServer(filePath, serverName, entry) {
|
|
433
|
+
const block = buildTomlServerBlock(serverName, entry);
|
|
434
|
+
let existing = "";
|
|
435
|
+
try {
|
|
436
|
+
existing = await readFile(filePath, "utf-8");
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
const sectionHeader = `[mcp_servers.${serverName}]`;
|
|
440
|
+
const alreadyExists = existing.includes(sectionHeader);
|
|
441
|
+
if (alreadyExists) {
|
|
442
|
+
const subPrefix = `[mcp_servers.${serverName}.`;
|
|
443
|
+
const startIdx = existing.indexOf(sectionHeader);
|
|
444
|
+
const rest = existing.slice(startIdx + sectionHeader.length);
|
|
445
|
+
let endOffset = rest.length;
|
|
446
|
+
const re = /^\[/gm;
|
|
447
|
+
let m;
|
|
448
|
+
while ((m = re.exec(rest)) !== null) {
|
|
449
|
+
const lineEnd = rest.indexOf("\n", m.index);
|
|
450
|
+
const line = rest.slice(m.index, lineEnd === -1 ? void 0 : lineEnd);
|
|
451
|
+
if (!line.startsWith(subPrefix)) {
|
|
452
|
+
endOffset = m.index;
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
|
|
457
|
+
const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
|
|
458
|
+
const before = rawBefore.length > 0 ? rawBefore + "\n\n" : "";
|
|
459
|
+
const after = rawAfter.length > 0 ? "\n" + rawAfter : "";
|
|
460
|
+
const content = before + block + after;
|
|
461
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
462
|
+
await writeFile(filePath, content, "utf-8");
|
|
463
|
+
} else {
|
|
464
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
465
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
466
|
+
await writeFile(filePath, existing + separator + block, "utf-8");
|
|
467
|
+
}
|
|
468
|
+
return { alreadyExists };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/commands/setup.ts
|
|
472
|
+
var CHECKBOX_THEME = {
|
|
473
|
+
style: {
|
|
474
|
+
highlight: (text) => pc3.green(text),
|
|
475
|
+
disabledChoice: (text) => ` ${pc3.dim("-")} ${pc3.dim(text)}`
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
function getSelectedAgents(options) {
|
|
479
|
+
const agents2 = [];
|
|
480
|
+
if (options.claude) agents2.push("claude");
|
|
481
|
+
if (options.cursor) agents2.push("cursor");
|
|
482
|
+
if (options.opencode) agents2.push("opencode");
|
|
483
|
+
if (options.codex) agents2.push("codex");
|
|
484
|
+
if (options.antigravity) agents2.push("antigravity");
|
|
485
|
+
if (options.gemini) agents2.push("gemini");
|
|
486
|
+
return agents2;
|
|
487
|
+
}
|
|
488
|
+
function resolveSetupAuth(options) {
|
|
489
|
+
if (options.apiKey) return { apiKey: options.apiKey };
|
|
490
|
+
if (process.env.GRTL_API_KEY) {
|
|
491
|
+
return { apiKey: process.env.GRTL_API_KEY, apiKeyEnvVar: "GRTL_API_KEY" };
|
|
492
|
+
}
|
|
493
|
+
if (process.env.GENRTL_API_KEY) {
|
|
494
|
+
return { apiKey: process.env.GENRTL_API_KEY, apiKeyEnvVar: "GENRTL_API_KEY" };
|
|
495
|
+
}
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
function registerSetupCommand(program2) {
|
|
499
|
+
program2.command("setup").description("Configure the hosted GenRTL HTTP MCP server for a coding agent").option("--claude", "Set up Claude Code").option("--cursor", "Set up Cursor").option("--antigravity", "Set up Antigravity").option("--opencode", "Set up OpenCode").option("--codex", "Set up Codex").option("--gemini", "Set up Gemini CLI").option("-p, --project", "Configure the current project instead of the user profile").option("-y, --yes", "Use all detected agents without prompting").option("--api-key <key>", "GenRTL API key (prefer the GRTL_API_KEY environment variable)").action(async (options) => {
|
|
500
|
+
await setupCommand(options);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
async function promptAgents() {
|
|
504
|
+
try {
|
|
505
|
+
return await checkboxWithHover(
|
|
506
|
+
{
|
|
507
|
+
message: "Which agents do you want to set up?",
|
|
508
|
+
choices: ALL_AGENT_NAMES.map((name) => ({
|
|
509
|
+
name: SETUP_AGENT_NAMES[name],
|
|
510
|
+
value: name
|
|
511
|
+
})),
|
|
512
|
+
loop: false,
|
|
513
|
+
theme: CHECKBOX_THEME
|
|
514
|
+
},
|
|
515
|
+
{ getName: (agent) => SETUP_AGENT_NAMES[agent] }
|
|
516
|
+
);
|
|
517
|
+
} catch {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
async function resolveAgents(options, scope) {
|
|
522
|
+
const explicit = getSelectedAgents(options);
|
|
523
|
+
if (explicit.length > 0) return explicit;
|
|
524
|
+
const detected = await detectAgents(scope);
|
|
525
|
+
if (detected.length > 0 && options.yes) return detected;
|
|
526
|
+
if (options.yes) {
|
|
527
|
+
log.error("No supported coding agents were detected. Pass an agent flag such as --cursor.");
|
|
528
|
+
return [];
|
|
529
|
+
}
|
|
530
|
+
log.blank();
|
|
531
|
+
const selected = await promptAgents();
|
|
532
|
+
if (!selected) log.warn("Setup cancelled");
|
|
533
|
+
return selected ?? [];
|
|
534
|
+
}
|
|
535
|
+
async function installRule(agentName, scope) {
|
|
536
|
+
const rule = getAgent(agentName).rule;
|
|
537
|
+
const content = await getRuleContent("mcp", agentName);
|
|
538
|
+
if (rule.kind === "file") {
|
|
539
|
+
const ruleDir = scope === "global" ? rule.dir("global") : join2(process.cwd(), rule.dir("project"));
|
|
540
|
+
const rulePath = join2(ruleDir, rule.filename);
|
|
541
|
+
await mkdir2(dirname2(rulePath), { recursive: true });
|
|
542
|
+
await writeFile2(rulePath, content, "utf-8");
|
|
543
|
+
return { status: "installed", path: rulePath };
|
|
544
|
+
}
|
|
545
|
+
const filePath = scope === "global" ? rule.file("global") : join2(process.cwd(), rule.file("project"));
|
|
546
|
+
const escapedMarker = rule.sectionMarker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
547
|
+
const section = `${rule.sectionMarker}
|
|
548
|
+
${content.trimEnd()}
|
|
549
|
+
${rule.sectionMarker}`;
|
|
550
|
+
let existing = "";
|
|
551
|
+
try {
|
|
552
|
+
existing = await readFile2(filePath, "utf-8");
|
|
553
|
+
} catch {
|
|
554
|
+
}
|
|
555
|
+
if (existing.includes(rule.sectionMarker)) {
|
|
556
|
+
const regex = new RegExp(`${escapedMarker}\\n[\\s\\S]*?${escapedMarker}`);
|
|
557
|
+
await writeFile2(filePath, existing.replace(regex, section), "utf-8");
|
|
558
|
+
return { status: "updated", path: filePath };
|
|
559
|
+
}
|
|
560
|
+
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
561
|
+
await mkdir2(dirname2(filePath), { recursive: true });
|
|
562
|
+
await writeFile2(filePath, `${existing}${separator}${section}
|
|
563
|
+
`, "utf-8");
|
|
564
|
+
return { status: "installed", path: filePath };
|
|
565
|
+
}
|
|
566
|
+
async function setupAgent(agentName, auth, scope) {
|
|
567
|
+
const agent = getAgent(agentName);
|
|
568
|
+
const candidates = scope === "global" || agent.mcp.projectPaths.length === 0 ? agent.mcp.globalPaths : agent.mcp.projectPaths.map((path) => join2(process.cwd(), path));
|
|
569
|
+
const mcpPath = await resolveMcpPath(candidates);
|
|
570
|
+
const entry = agent.mcp.buildEntry(auth);
|
|
571
|
+
let mcpStatus;
|
|
572
|
+
try {
|
|
573
|
+
if (mcpPath.endsWith(".toml")) {
|
|
574
|
+
const { alreadyExists } = await appendTomlServer(mcpPath, "genrtl", entry);
|
|
575
|
+
mcpStatus = alreadyExists ? "reconfigured" : "configured";
|
|
576
|
+
} else {
|
|
577
|
+
const existing = await readJsonConfig(mcpPath);
|
|
578
|
+
const { config, alreadyExists } = mergeServerEntry(
|
|
579
|
+
existing,
|
|
580
|
+
agent.mcp.configKey,
|
|
581
|
+
"genrtl",
|
|
582
|
+
entry
|
|
583
|
+
);
|
|
584
|
+
await writeJsonConfig(mcpPath, config);
|
|
585
|
+
mcpStatus = alreadyExists ? "reconfigured" : "configured";
|
|
586
|
+
}
|
|
587
|
+
} catch (error) {
|
|
588
|
+
mcpStatus = `failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
589
|
+
}
|
|
590
|
+
let ruleStatus;
|
|
591
|
+
let rulePath = "";
|
|
592
|
+
try {
|
|
593
|
+
const result = await installRule(agentName, scope);
|
|
594
|
+
ruleStatus = result.status;
|
|
595
|
+
rulePath = result.path;
|
|
596
|
+
} catch (error) {
|
|
597
|
+
ruleStatus = `failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
598
|
+
}
|
|
599
|
+
return { agent: agent.displayName, mcpStatus, mcpPath, ruleStatus, rulePath };
|
|
600
|
+
}
|
|
601
|
+
async function setupCommand(options) {
|
|
602
|
+
trackEvent("command", { name: "setup" });
|
|
603
|
+
const auth = resolveSetupAuth(options);
|
|
604
|
+
if (!auth) {
|
|
605
|
+
log.error("Set GRTL_API_KEY or pass --api-key before running setup.");
|
|
606
|
+
process.exitCode = 1;
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const scope = options.project ? "project" : "global";
|
|
610
|
+
const agents2 = await resolveAgents(options, scope);
|
|
611
|
+
if (agents2.length === 0) return;
|
|
612
|
+
const spinner = ora("Configuring GenRTL MCP...").start();
|
|
613
|
+
const results = [];
|
|
614
|
+
for (const agentName of agents2) {
|
|
615
|
+
spinner.text = `Configuring ${getAgent(agentName).displayName}...`;
|
|
616
|
+
results.push(await setupAgent(agentName, auth, scope));
|
|
617
|
+
}
|
|
618
|
+
const failed = results.some(
|
|
619
|
+
(result) => result.mcpStatus.startsWith("failed") || result.ruleStatus.startsWith("failed")
|
|
620
|
+
);
|
|
621
|
+
if (failed) {
|
|
622
|
+
spinner.warn("GenRTL setup completed with errors");
|
|
623
|
+
process.exitCode = 1;
|
|
624
|
+
} else {
|
|
625
|
+
spinner.succeed("GenRTL setup complete");
|
|
626
|
+
}
|
|
627
|
+
log.blank();
|
|
628
|
+
for (const result of results) {
|
|
629
|
+
log.plain(` ${pc3.bold(result.agent)}`);
|
|
630
|
+
log.plain(` ${pc3.green("+")} MCP server ${result.mcpStatus}`);
|
|
631
|
+
log.plain(` ${pc3.dim(result.mcpPath)}`);
|
|
632
|
+
log.plain(` ${pc3.green("+")} Rule ${result.ruleStatus}`);
|
|
633
|
+
log.plain(` ${pc3.dim(result.rulePath)}`);
|
|
634
|
+
}
|
|
635
|
+
log.blank();
|
|
636
|
+
trackEvent("setup", { agents: agents2, scope, authMode: "api-key" });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/commands/knowledge.ts
|
|
640
|
+
import { InvalidArgumentError } from "commander";
|
|
641
|
+
import pc4 from "picocolors";
|
|
642
|
+
import ora2 from "ora";
|
|
643
|
+
|
|
644
|
+
// src/constants.ts
|
|
645
|
+
import { readFileSync } from "fs";
|
|
646
|
+
import { fileURLToPath } from "url";
|
|
647
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
648
|
+
var __dirname = dirname3(fileURLToPath(import.meta.url));
|
|
649
|
+
var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
|
|
650
|
+
var VERSION = pkg.version;
|
|
651
|
+
var NAME = pkg.name;
|
|
652
|
+
|
|
653
|
+
// src/utils/knowledge-api.ts
|
|
654
|
+
var baseUrl = "https://www.genrtl.com";
|
|
655
|
+
function setBaseUrl(url) {
|
|
656
|
+
baseUrl = url.replace(/\/+$/, "");
|
|
657
|
+
}
|
|
658
|
+
function getMcpEndpoint() {
|
|
659
|
+
return baseUrl.endsWith("/api/mcp") ? baseUrl : `${baseUrl}/api/mcp`;
|
|
660
|
+
}
|
|
661
|
+
function getApiKey() {
|
|
662
|
+
return process.env.GRTL_API_KEY || process.env.GENRTL_API_KEY;
|
|
663
|
+
}
|
|
664
|
+
function getMcpErrorMessage(content) {
|
|
665
|
+
if (!Array.isArray(content)) return void 0;
|
|
666
|
+
const item = content.find(
|
|
667
|
+
(entry) => entry && typeof entry === "object" && "type" in entry && entry.type === "text" && "text" in entry && typeof entry.text === "string"
|
|
668
|
+
);
|
|
669
|
+
return item?.text;
|
|
670
|
+
}
|
|
671
|
+
async function callGenrtlKnowledgeTool(toolName, input) {
|
|
672
|
+
const apiKey = getApiKey();
|
|
673
|
+
if (!apiKey) {
|
|
674
|
+
throw new Error("Authentication required. Set GRTL_API_KEY or GENRTL_API_KEY.");
|
|
675
|
+
}
|
|
676
|
+
const response = await fetch(getMcpEndpoint(), {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
"Content-Type": "application/json",
|
|
680
|
+
Authorization: `Bearer ${apiKey}`,
|
|
681
|
+
"MCP-Protocol-Version": "2025-06-18",
|
|
682
|
+
"X-GenRTL-Source": "cli",
|
|
683
|
+
"X-GenRTL-Client-Version": VERSION
|
|
684
|
+
},
|
|
685
|
+
body: JSON.stringify({
|
|
686
|
+
jsonrpc: "2.0",
|
|
687
|
+
id: 1,
|
|
688
|
+
method: "tools/call",
|
|
689
|
+
params: { name: toolName, arguments: input }
|
|
690
|
+
})
|
|
691
|
+
});
|
|
692
|
+
const payload = await response.json().catch(() => null);
|
|
693
|
+
if (!response.ok || payload?.error) {
|
|
694
|
+
const message = payload?.error?.message || `GenRTL MCP request failed with HTTP ${response.status}`;
|
|
695
|
+
const code = payload?.error?.data?.code;
|
|
696
|
+
throw new Error(code ? `${message} (${code})` : message);
|
|
697
|
+
}
|
|
698
|
+
const result = payload?.result;
|
|
699
|
+
if (!result) throw new Error("GenRTL MCP returned an empty result.");
|
|
700
|
+
if (result.isError) {
|
|
701
|
+
throw new Error(getMcpErrorMessage(result.content) || "GenRTL knowledge search failed.");
|
|
702
|
+
}
|
|
703
|
+
if (!result.structuredContent) {
|
|
704
|
+
throw new Error("GenRTL MCP response did not include structured knowledge results.");
|
|
705
|
+
}
|
|
706
|
+
return result.structuredContent;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/commands/knowledge.ts
|
|
710
|
+
var isTTY = process.stdout.isTTY;
|
|
711
|
+
var TOOL_COMMANDS = [
|
|
712
|
+
{
|
|
713
|
+
name: "genrtl_knowledge_search",
|
|
714
|
+
alias: "knowledge-search",
|
|
715
|
+
description: "Search all approved GenRTL knowledge cards"
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
name: "genrtl_spec2rtl_search",
|
|
719
|
+
alias: "spec2rtl-search",
|
|
720
|
+
description: "Search Spec2RTL knowledge cards"
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
name: "genrtl_verification_search",
|
|
724
|
+
alias: "verification-search",
|
|
725
|
+
description: "Search verification and testbench knowledge cards"
|
|
726
|
+
},
|
|
727
|
+
{
|
|
728
|
+
name: "genrtl_debug_search",
|
|
729
|
+
alias: "debug-search",
|
|
730
|
+
description: "Search lint, CDC, compile, synthesis, and RTL debug knowledge cards"
|
|
731
|
+
}
|
|
732
|
+
];
|
|
733
|
+
function parseInteger(value) {
|
|
734
|
+
const parsed = Number(value);
|
|
735
|
+
if (!Number.isInteger(parsed)) {
|
|
736
|
+
throw new InvalidArgumentError("Expected an integer.");
|
|
737
|
+
}
|
|
738
|
+
return parsed;
|
|
739
|
+
}
|
|
740
|
+
function parseNumber(value) {
|
|
741
|
+
const parsed = Number(value);
|
|
742
|
+
if (!Number.isFinite(parsed)) {
|
|
743
|
+
throw new InvalidArgumentError("Expected a number.");
|
|
744
|
+
}
|
|
745
|
+
return parsed;
|
|
746
|
+
}
|
|
747
|
+
function buildKnowledgeSearchInput(query, options) {
|
|
748
|
+
if (options.topK !== void 0 && (options.topK < 1 || options.topK > 20)) {
|
|
749
|
+
throw new InvalidArgumentError("--top-k must be between 1 and 20.");
|
|
750
|
+
}
|
|
751
|
+
if (options.minScore !== void 0 && (options.minScore < 0 || options.minScore > 1)) {
|
|
752
|
+
throw new InvalidArgumentError("--min-score must be between 0 and 1.");
|
|
753
|
+
}
|
|
754
|
+
if (options.target && !["fpga", "asic", "both"].includes(options.target)) {
|
|
755
|
+
throw new InvalidArgumentError("--target must be fpga, asic, or both.");
|
|
756
|
+
}
|
|
757
|
+
const filters = {};
|
|
758
|
+
if (options.type?.length) {
|
|
759
|
+
const allowed = /* @__PURE__ */ new Set(["spec2rtl", "verification", "debug"]);
|
|
760
|
+
const invalid = options.type.find((type) => !allowed.has(type));
|
|
761
|
+
if (invalid) {
|
|
762
|
+
throw new InvalidArgumentError(`Invalid knowledge type: ${invalid}`);
|
|
763
|
+
}
|
|
764
|
+
filters.types = options.type;
|
|
765
|
+
}
|
|
766
|
+
if (options.domain) filters.domain = options.domain;
|
|
767
|
+
if (options.tool) filters.tool = options.tool;
|
|
768
|
+
if (options.toolVersion) filters.tool_version = options.toolVersion;
|
|
769
|
+
if (options.errorType) filters.error_type = options.errorType;
|
|
770
|
+
if (options.severity) filters.severity = options.severity;
|
|
771
|
+
if (options.interface) filters.interface = options.interface;
|
|
772
|
+
if (options.target) filters.target = options.target;
|
|
773
|
+
if (options.tag?.length) filters.tags = options.tag;
|
|
774
|
+
return {
|
|
775
|
+
query,
|
|
776
|
+
...Object.keys(filters).length > 0 ? { filters } : {},
|
|
777
|
+
...options.topK !== void 0 ? { top_k: options.topK } : {},
|
|
778
|
+
...options.minScore !== void 0 ? { min_score: options.minScore } : {},
|
|
779
|
+
...options.workspaceId ? { workspace_id: options.workspaceId } : {}
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
function formatMatch(match, index) {
|
|
783
|
+
const lines = [
|
|
784
|
+
`${pc4.dim(`${index + 1}.`)} ${pc4.bold(match.title)} ${pc4.cyan(`[${match.type}]`)}`,
|
|
785
|
+
` ${pc4.dim(`Confidence: ${Math.round(match.confidence * 100)}%`)}`,
|
|
786
|
+
` ${match.summary}`
|
|
787
|
+
];
|
|
788
|
+
if (match.root_cause) lines.push(` ${pc4.bold("Root cause:")} ${match.root_cause}`);
|
|
789
|
+
if (match.fix_strategy?.length) {
|
|
790
|
+
lines.push(` ${pc4.bold("Fix strategy:")} ${match.fix_strategy.join("; ")}`);
|
|
791
|
+
}
|
|
792
|
+
if (match.code_example) {
|
|
793
|
+
lines.push("", "```systemverilog", match.code_example, "```");
|
|
794
|
+
}
|
|
795
|
+
if (match.recommended_next_action) {
|
|
796
|
+
lines.push(` ${pc4.bold("Next:")} ${match.recommended_next_action}`);
|
|
797
|
+
}
|
|
798
|
+
return lines.join("\n");
|
|
799
|
+
}
|
|
800
|
+
async function searchCommand(toolName, query, options) {
|
|
801
|
+
trackEvent("command", { name: toolName });
|
|
802
|
+
const spinner = isTTY ? ora2("Searching GenRTL knowledge...").start() : null;
|
|
803
|
+
try {
|
|
804
|
+
const input = buildKnowledgeSearchInput(query, options);
|
|
805
|
+
const result = await callGenrtlKnowledgeTool(toolName, input);
|
|
806
|
+
spinner?.stop();
|
|
807
|
+
if (options.json) {
|
|
808
|
+
console.log(JSON.stringify(result, null, 2));
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
printKnowledgeResult(result);
|
|
812
|
+
} catch (err) {
|
|
813
|
+
spinner?.fail(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
814
|
+
if (!spinner) log.error(err instanceof Error ? err.message : String(err));
|
|
815
|
+
process.exitCode = 1;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function printKnowledgeResult(result) {
|
|
819
|
+
log.blank();
|
|
820
|
+
log.plain(pc4.bold(result.summary));
|
|
821
|
+
log.blank();
|
|
822
|
+
if (result.matched_cards.length === 0) {
|
|
823
|
+
log.warn("No matching GenRTL knowledge cards were found.");
|
|
824
|
+
} else {
|
|
825
|
+
result.matched_cards.forEach((match, index) => {
|
|
826
|
+
log.plain(formatMatch(match, index));
|
|
827
|
+
log.blank();
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
if (result.recommended_next_action) {
|
|
831
|
+
log.plain(`${pc4.bold("Recommended next action:")} ${result.recommended_next_action}`);
|
|
832
|
+
log.blank();
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function addSearchOptions(command) {
|
|
836
|
+
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");
|
|
837
|
+
}
|
|
838
|
+
function registerKnowledgeCommands(program2) {
|
|
839
|
+
for (const tool of TOOL_COMMANDS) {
|
|
840
|
+
const command = program2.command(tool.name).alias(tool.alias).description(tool.description);
|
|
841
|
+
addSearchOptions(command).action(async (query, options) => {
|
|
842
|
+
await searchCommand(tool.name, query, options);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// src/commands/upgrade.ts
|
|
848
|
+
import { confirm } from "@inquirer/prompts";
|
|
849
|
+
import { spawn } from "child_process";
|
|
850
|
+
import pc5 from "picocolors";
|
|
851
|
+
|
|
852
|
+
// src/utils/update-check.ts
|
|
853
|
+
import { homedir as homedir2 } from "os";
|
|
854
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
855
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
856
|
+
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
857
|
+
var UPDATE_STATE_FILE = join4(homedir2(), ".genrtl", "cli-state.json");
|
|
858
|
+
function getStateFilePath(stateFile) {
|
|
859
|
+
return stateFile ?? UPDATE_STATE_FILE;
|
|
860
|
+
}
|
|
861
|
+
async function readUpdateState(stateFile) {
|
|
862
|
+
try {
|
|
863
|
+
const raw = await readFile3(getStateFilePath(stateFile), "utf-8");
|
|
864
|
+
return JSON.parse(raw);
|
|
865
|
+
} catch {
|
|
866
|
+
return {};
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async function writeUpdateState(state, stateFile) {
|
|
870
|
+
const path = getStateFilePath(stateFile);
|
|
871
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
872
|
+
await writeFile3(path, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
873
|
+
}
|
|
874
|
+
function compareVersions(a, b) {
|
|
875
|
+
const normalize = (version) => version.split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
876
|
+
const left = normalize(a);
|
|
877
|
+
const right = normalize(b);
|
|
878
|
+
const max = Math.max(left.length, right.length);
|
|
879
|
+
for (let i = 0; i < max; i++) {
|
|
880
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0);
|
|
881
|
+
if (diff !== 0) return diff;
|
|
882
|
+
}
|
|
883
|
+
return 0;
|
|
884
|
+
}
|
|
885
|
+
function detectInstallMethod(env = process.env) {
|
|
886
|
+
const execPath = env.npm_execpath?.toLowerCase() ?? "";
|
|
887
|
+
const npmCommand = env.npm_command?.toLowerCase() ?? "";
|
|
888
|
+
const userAgent = env.npm_config_user_agent?.toLowerCase() ?? "";
|
|
889
|
+
if (execPath.includes("pnpm") && npmCommand === "dlx") return "pnpm-dlx";
|
|
890
|
+
if (execPath.includes("pnpm")) return "pnpm-global";
|
|
891
|
+
if (execPath.includes("bun") && npmCommand === "x") return "bunx";
|
|
892
|
+
if (execPath.includes("bun")) return "bun-global";
|
|
893
|
+
if (execPath.includes("npm") && npmCommand === "exec") return "npx";
|
|
894
|
+
if (execPath.includes("npm")) return "npm-global";
|
|
895
|
+
if (userAgent.startsWith("pnpm/")) return "pnpm-global";
|
|
896
|
+
if (userAgent.startsWith("bun/")) return "bun-global";
|
|
897
|
+
if (userAgent.startsWith("npm/")) return "npm-global";
|
|
898
|
+
return "unknown";
|
|
899
|
+
}
|
|
900
|
+
function getUpgradePlan(installMethod = detectInstallMethod(), packageName = NAME) {
|
|
901
|
+
switch (installMethod) {
|
|
902
|
+
case "pnpm-global":
|
|
903
|
+
return {
|
|
904
|
+
installMethod,
|
|
905
|
+
command: "pnpm",
|
|
906
|
+
args: ["add", "-g", `${packageName}@latest`],
|
|
907
|
+
displayCommand: `pnpm add -g ${packageName}@latest`,
|
|
908
|
+
canRun: true,
|
|
909
|
+
needsExplicitVersion: false
|
|
910
|
+
};
|
|
911
|
+
case "bun-global":
|
|
912
|
+
return {
|
|
913
|
+
installMethod,
|
|
914
|
+
command: "bun",
|
|
915
|
+
args: ["add", "-g", `${packageName}@latest`],
|
|
916
|
+
displayCommand: `bun add -g ${packageName}@latest`,
|
|
917
|
+
canRun: true,
|
|
918
|
+
needsExplicitVersion: false
|
|
919
|
+
};
|
|
920
|
+
case "npx":
|
|
921
|
+
return {
|
|
922
|
+
installMethod,
|
|
923
|
+
command: "npx",
|
|
924
|
+
args: [`${packageName}@latest`],
|
|
925
|
+
displayCommand: `npx ${packageName}@latest <command>`,
|
|
926
|
+
canRun: false,
|
|
927
|
+
needsExplicitVersion: true
|
|
928
|
+
};
|
|
929
|
+
case "pnpm-dlx":
|
|
930
|
+
return {
|
|
931
|
+
installMethod,
|
|
932
|
+
command: "pnpm",
|
|
933
|
+
args: ["dlx", `${packageName}@latest`],
|
|
934
|
+
displayCommand: `pnpm dlx ${packageName}@latest <command>`,
|
|
935
|
+
canRun: false,
|
|
936
|
+
needsExplicitVersion: true
|
|
937
|
+
};
|
|
938
|
+
case "bunx":
|
|
939
|
+
return {
|
|
940
|
+
installMethod,
|
|
941
|
+
command: "bunx",
|
|
942
|
+
args: [`${packageName}@latest`],
|
|
943
|
+
displayCommand: `bunx ${packageName}@latest <command>`,
|
|
944
|
+
canRun: false,
|
|
945
|
+
needsExplicitVersion: true
|
|
946
|
+
};
|
|
947
|
+
case "unknown":
|
|
948
|
+
return {
|
|
949
|
+
installMethod,
|
|
950
|
+
command: "npm",
|
|
951
|
+
args: ["install", "-g", `${packageName}@latest`],
|
|
952
|
+
displayCommand: `npm install -g ${packageName}@latest`,
|
|
953
|
+
canRun: false,
|
|
954
|
+
needsExplicitVersion: false
|
|
955
|
+
};
|
|
956
|
+
case "npm-global":
|
|
957
|
+
default:
|
|
958
|
+
return {
|
|
959
|
+
installMethod: "npm-global",
|
|
960
|
+
command: "npm",
|
|
961
|
+
args: ["install", "-g", `${packageName}@latest`],
|
|
962
|
+
displayCommand: `npm install -g ${packageName}@latest`,
|
|
963
|
+
canRun: true,
|
|
964
|
+
needsExplicitVersion: false
|
|
965
|
+
};
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async function fetchLatestVersion(packageName = NAME) {
|
|
969
|
+
try {
|
|
970
|
+
const response = await fetch(
|
|
971
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`,
|
|
972
|
+
{
|
|
973
|
+
headers: { Accept: "application/json" },
|
|
974
|
+
signal: AbortSignal.timeout(1500)
|
|
975
|
+
}
|
|
976
|
+
);
|
|
977
|
+
if (!response.ok) return null;
|
|
978
|
+
const data = await response.json();
|
|
979
|
+
return typeof data.version === "string" ? data.version : null;
|
|
980
|
+
} catch {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
async function checkForUpdates(options = {}) {
|
|
985
|
+
const now = options.now ?? Date.now();
|
|
986
|
+
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
987
|
+
const stateFile = options.stateFile;
|
|
988
|
+
const state = await readUpdateState(stateFile);
|
|
989
|
+
const isStale = options.force || !state.lastCheckedAt || now - state.lastCheckedAt >= cacheTtlMs || !state.latestVersion;
|
|
990
|
+
let latestVersion = state.latestVersion ?? null;
|
|
991
|
+
if (isStale) {
|
|
992
|
+
const fetchedVersion = await fetchLatestVersion();
|
|
993
|
+
if (fetchedVersion) {
|
|
994
|
+
latestVersion = fetchedVersion;
|
|
995
|
+
await writeUpdateState(
|
|
996
|
+
{
|
|
997
|
+
...state,
|
|
998
|
+
latestVersion: fetchedVersion,
|
|
999
|
+
lastCheckedAt: now
|
|
1000
|
+
},
|
|
1001
|
+
stateFile
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (!latestVersion) return null;
|
|
1006
|
+
const installMethod = detectInstallMethod();
|
|
1007
|
+
return {
|
|
1008
|
+
currentVersion: VERSION,
|
|
1009
|
+
latestVersion,
|
|
1010
|
+
updateAvailable: compareVersions(latestVersion, VERSION) > 0,
|
|
1011
|
+
installMethod,
|
|
1012
|
+
upgradePlan: getUpgradePlan(installMethod)
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async function shouldShowUpdateNotification(info, options = {}) {
|
|
1016
|
+
if (!info.updateAvailable) return false;
|
|
1017
|
+
const now = options.now ?? Date.now();
|
|
1018
|
+
const cooldownMs = options.cooldownMs ?? DEFAULT_CACHE_TTL_MS;
|
|
1019
|
+
const state = await readUpdateState(options.stateFile);
|
|
1020
|
+
if (state.notifiedVersion === info.latestVersion && state.lastNotifiedAt && now - state.lastNotifiedAt < cooldownMs) {
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
return true;
|
|
1024
|
+
}
|
|
1025
|
+
async function markUpdateNotificationShown(latestVersion, options = {}) {
|
|
1026
|
+
const now = options.now ?? Date.now();
|
|
1027
|
+
const state = await readUpdateState(options.stateFile);
|
|
1028
|
+
await writeUpdateState(
|
|
1029
|
+
{
|
|
1030
|
+
...state,
|
|
1031
|
+
notifiedVersion: latestVersion,
|
|
1032
|
+
lastNotifiedAt: now
|
|
1033
|
+
},
|
|
1034
|
+
options.stateFile
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
function shouldSkipUpdateNotifier(argv = process.argv) {
|
|
1038
|
+
return argv.includes("--json") || argv.includes("-v") || argv.includes("--version");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/commands/upgrade.ts
|
|
1042
|
+
function registerUpgradeCommand(program2) {
|
|
1043
|
+
program2.command("upgrade").description("Check for a newer grtl version and upgrade when possible").option("-y, --yes", "Run the suggested upgrade command without prompting").option("--check", "Only check for updates without running the upgrade command").action(async (options) => {
|
|
1044
|
+
await upgradeCommand(options);
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
function runCommand(command, args) {
|
|
1048
|
+
return new Promise((resolve, reject) => {
|
|
1049
|
+
const child = spawn(command, args, {
|
|
1050
|
+
stdio: "inherit",
|
|
1051
|
+
shell: process.platform === "win32"
|
|
1052
|
+
});
|
|
1053
|
+
child.on("error", reject);
|
|
1054
|
+
child.on("close", (code) => resolve(code));
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
async function runUpgradePlan(plan) {
|
|
1058
|
+
return runCommand(plan.command, plan.args);
|
|
1059
|
+
}
|
|
1060
|
+
function showUpgradeFailureHelp(plan) {
|
|
1061
|
+
log.info(`Try rerunning: ${pc5.cyan(plan.displayCommand)}`);
|
|
1062
|
+
const isGlobalNpmInstall = (plan.installMethod === "npm-global" || plan.installMethod === "unknown") && plan.command === "npm" && plan.args.includes("-g");
|
|
1063
|
+
const isGlobalAltInstall = (plan.installMethod === "pnpm-global" || plan.installMethod === "bun-global") && plan.args.includes("-g");
|
|
1064
|
+
if (isGlobalNpmInstall) {
|
|
1065
|
+
log.dim(
|
|
1066
|
+
"If this failed due to permissions, your global npm directory may require elevated privileges on this machine."
|
|
1067
|
+
);
|
|
1068
|
+
} else if (isGlobalAltInstall) {
|
|
1069
|
+
log.dim(
|
|
1070
|
+
"If this failed due to permissions, your global package manager install location may require additional privileges on this machine."
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
async function maybeShowUpgradeNotice(options = {}) {
|
|
1075
|
+
const actionName = options.actionName ?? "";
|
|
1076
|
+
const argv = options.argv ?? process.argv;
|
|
1077
|
+
const isInteractive = options.isInteractive ?? Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
1078
|
+
if (!isInteractive || shouldSkipUpdateNotifier(argv) || actionName === "upgrade") {
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
const info = await checkForUpdates();
|
|
1082
|
+
if (!info || !info.updateAvailable || !await shouldShowUpdateNotification(info)) {
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
log.blank();
|
|
1086
|
+
if (info.upgradePlan.needsExplicitVersion) {
|
|
1087
|
+
log.box([
|
|
1088
|
+
`${pc5.white(pc5.bold("Update available:"))} ${pc5.green(pc5.bold(`v${info.currentVersion}`))} ${pc5.dim("->")} ${pc5.green(pc5.bold(`v${info.latestVersion}`))}`,
|
|
1089
|
+
`${pc5.white("Use")} ${pc5.yellow(pc5.bold(info.upgradePlan.displayCommand))} ${pc5.white("to run the latest version")}`
|
|
1090
|
+
]);
|
|
1091
|
+
await markUpdateNotificationShown(info.latestVersion);
|
|
1092
|
+
log.blank();
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
if (!info.upgradePlan.canRun) {
|
|
1096
|
+
log.box([
|
|
1097
|
+
`${pc5.white(pc5.bold("Update available:"))} ${pc5.green(pc5.bold(`v${info.currentVersion}`))} ${pc5.dim("->")} ${pc5.green(pc5.bold(`v${info.latestVersion}`))}`,
|
|
1098
|
+
`${pc5.white("Run")} ${pc5.yellow(pc5.bold("grtl upgrade"))} ${pc5.white("for update steps")}`,
|
|
1099
|
+
`${pc5.white("Or run")} ${pc5.yellow(info.upgradePlan.displayCommand)}`
|
|
1100
|
+
]);
|
|
1101
|
+
await markUpdateNotificationShown(info.latestVersion);
|
|
1102
|
+
log.blank();
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
log.box([
|
|
1106
|
+
`${pc5.white(pc5.bold("Update available:"))} ${pc5.green(pc5.bold(`v${info.currentVersion}`))} ${pc5.dim("->")} ${pc5.green(pc5.bold(`v${info.latestVersion}`))}`,
|
|
1107
|
+
`${pc5.white("Run")} ${pc5.yellow(pc5.bold("grtl upgrade"))} ${pc5.white("to update now")}`,
|
|
1108
|
+
`${pc5.white("Or run")} ${pc5.yellow(info.upgradePlan.displayCommand)}`
|
|
1109
|
+
]);
|
|
1110
|
+
await markUpdateNotificationShown(info.latestVersion);
|
|
1111
|
+
log.blank();
|
|
1112
|
+
}
|
|
1113
|
+
async function upgradeCommand(options) {
|
|
1114
|
+
trackEvent("command", { name: "upgrade" });
|
|
1115
|
+
const info = await checkForUpdates({ force: true });
|
|
1116
|
+
const plan = info?.upgradePlan ?? getUpgradePlan();
|
|
1117
|
+
if (!info) {
|
|
1118
|
+
log.warn("Couldn't check for updates right now.");
|
|
1119
|
+
log.info(`Try again later or run ${pc5.cyan(plan.displayCommand)} manually.`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (!info.updateAvailable) {
|
|
1123
|
+
log.success(`grtl is up to date (${pc5.bold(`v${VERSION}`)})`);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
log.blank();
|
|
1127
|
+
log.info(
|
|
1128
|
+
`Update available: ${pc5.bold(`v${info.currentVersion}`)} ${pc5.dim("->")} ${pc5.bold(`v${info.latestVersion}`)}`
|
|
1129
|
+
);
|
|
1130
|
+
if (plan.needsExplicitVersion) {
|
|
1131
|
+
log.info(`You're using an ephemeral runner (${plan.installMethod}).`);
|
|
1132
|
+
log.info(`Use ${pc5.cyan(plan.displayCommand)} to run the latest version immediately.`);
|
|
1133
|
+
log.info(`Or install globally with ${pc5.cyan("npm install -g @genrtl/grtl@latest")}.`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
if (!plan.canRun) {
|
|
1137
|
+
log.info(`Run ${pc5.cyan(plan.displayCommand)} to update your installed version.`);
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
log.info(`Upgrade command: ${pc5.cyan(plan.displayCommand)}`);
|
|
1141
|
+
if (options.check) {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
let shouldRun = options.yes ?? false;
|
|
1145
|
+
if (!shouldRun && process.stdout.isTTY) {
|
|
1146
|
+
shouldRun = await confirm({
|
|
1147
|
+
message: `Run ${plan.displayCommand} now?`,
|
|
1148
|
+
default: true
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
if (!shouldRun) {
|
|
1152
|
+
log.dim("Upgrade skipped.");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
log.blank();
|
|
1156
|
+
const exitCode = await runUpgradePlan(plan);
|
|
1157
|
+
if (exitCode === 0) {
|
|
1158
|
+
log.blank();
|
|
1159
|
+
log.success("Upgrade complete.");
|
|
1160
|
+
log.info(`Run ${pc5.cyan("grtl --version")} to verify the installed version.`);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
log.blank();
|
|
1164
|
+
log.error(`Upgrade command exited with code ${exitCode ?? "unknown"}.`);
|
|
1165
|
+
showUpgradeFailureHelp(plan);
|
|
1166
|
+
process.exitCode = 1;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/index.ts
|
|
1170
|
+
var brand = {
|
|
1171
|
+
primary: pc6.green,
|
|
1172
|
+
dim: pc6.dim
|
|
1173
|
+
};
|
|
1174
|
+
var program = new Command2();
|
|
1175
|
+
program.name("grtl").description("GenRTL CLI - Search RTL engineering knowledge and configure GenRTL").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
1176
|
+
const opts = thisCommand.opts();
|
|
1177
|
+
if (opts.baseUrl) {
|
|
1178
|
+
setBaseUrl(opts.baseUrl);
|
|
1179
|
+
setMcpBaseUrl(opts.baseUrl);
|
|
1180
|
+
}
|
|
1181
|
+
}).hook("preAction", async (_thisCommand, actionCommand) => {
|
|
1182
|
+
await maybeShowUpgradeNotice({
|
|
1183
|
+
actionName: actionCommand.name(),
|
|
1184
|
+
argv: process.argv
|
|
1185
|
+
});
|
|
1186
|
+
}).addHelpText(
|
|
1187
|
+
"after",
|
|
1188
|
+
`
|
|
1189
|
+
Examples:
|
|
1190
|
+
${brand.dim("# Configure GenRTL for your coding agent")}
|
|
1191
|
+
${brand.primary("GRTL_API_KEY=your_key npx @genrtl/grtl setup --cursor")}
|
|
1192
|
+
${brand.primary("GRTL_API_KEY=your_key npx @genrtl/grtl setup --codex --project")}
|
|
1193
|
+
|
|
1194
|
+
${brand.dim("# Search the same four tools exposed by the GenRTL MCP server")}
|
|
1195
|
+
${brand.primary('npx @genrtl/grtl knowledge-search "AXI stream backpressure design"')}
|
|
1196
|
+
${brand.primary('npx @genrtl/grtl spec2rtl-search "Generate an APB register block"')}
|
|
1197
|
+
${brand.primary('npx @genrtl/grtl verification-search "Verify an async FIFO"')}
|
|
1198
|
+
${brand.primary('npx @genrtl/grtl debug-search "Explain this Vivado CDC warning"')}
|
|
1199
|
+
`
|
|
1200
|
+
);
|
|
1201
|
+
registerSetupCommand(program);
|
|
1202
|
+
registerKnowledgeCommands(program);
|
|
1203
|
+
registerUpgradeCommand(program);
|
|
1204
|
+
program.action(() => {
|
|
1205
|
+
console.log("");
|
|
1206
|
+
const banner = figlet.textSync("GenRTL", { font: "ANSI Shadow" });
|
|
1207
|
+
console.log(brand.primary(banner));
|
|
1208
|
+
console.log(brand.dim(" RTL engineering knowledge for AI coding agents"));
|
|
1209
|
+
console.log("");
|
|
1210
|
+
console.log(" Quick start:");
|
|
1211
|
+
console.log(` ${brand.primary("npx @genrtl/grtl setup")}`);
|
|
1212
|
+
console.log(` ${brand.primary('npx @genrtl/grtl knowledge-search "your RTL question"')}`);
|
|
1213
|
+
console.log("");
|
|
1214
|
+
console.log(` Run ${brand.primary("npx @genrtl/grtl --help")} for all commands and options`);
|
|
1215
|
+
console.log("");
|
|
1216
|
+
});
|
|
1217
|
+
await program.parseAsync();
|