archbyte 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/bin/archbyte.js +32 -14
- package/dist/agents/pipeline/merger.js +16 -11
- package/dist/agents/providers/claude-sdk.d.ts +7 -0
- package/dist/agents/providers/claude-sdk.js +59 -0
- package/dist/agents/providers/router.d.ts +5 -0
- package/dist/agents/providers/router.js +23 -1
- package/dist/agents/runtime/types.d.ts +2 -2
- package/dist/agents/runtime/types.js +5 -0
- package/dist/cli/analyze.js +8 -4
- package/dist/cli/auth.d.ts +11 -2
- package/dist/cli/auth.js +312 -73
- package/dist/cli/config.d.ts +1 -0
- package/dist/cli/config.js +51 -15
- package/dist/cli/constants.js +4 -1
- package/dist/cli/export.js +64 -2
- package/dist/cli/license-gate.d.ts +1 -1
- package/dist/cli/license-gate.js +3 -2
- package/dist/cli/mcp.js +8 -12
- package/dist/cli/setup.js +166 -35
- package/dist/cli/ui.d.ts +14 -0
- package/dist/cli/ui.js +98 -14
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +52 -0
- package/dist/server/src/index.js +59 -5
- package/package.json +4 -1
- package/ui/dist/assets/index-DmO1qYan.js +70 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/index-Bdr9FnaA.js +0 -70
package/dist/cli/export.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
+
import { fileURLToPath } from "url";
|
|
2
3
|
import { resolveArchitecturePath, loadArchitectureFile } from "./shared.js";
|
|
3
4
|
/**
|
|
4
5
|
* Export architecture diagram to Mermaid, Markdown, or JSON format.
|
|
@@ -7,11 +8,24 @@ export async function handleExport(options) {
|
|
|
7
8
|
const diagramPath = resolveArchitecturePath(options);
|
|
8
9
|
const arch = loadArchitectureFile(diagramPath);
|
|
9
10
|
const format = options.format || "mermaid";
|
|
10
|
-
const SUPPORTED_FORMATS = ["mermaid", "markdown", "json", "plantuml", "dot"];
|
|
11
|
+
const SUPPORTED_FORMATS = ["mermaid", "markdown", "json", "plantuml", "dot", "html"];
|
|
11
12
|
if (!SUPPORTED_FORMATS.includes(format)) {
|
|
12
|
-
|
|
13
|
+
const formatList = SUPPORTED_FORMATS.map(f => f === "html" ? `html ${chalk.yellow("[Pro]")}` : f).join(", ");
|
|
14
|
+
console.error(chalk.red(`Unknown format: "${format}". Supported: ${formatList}`));
|
|
13
15
|
process.exit(1);
|
|
14
16
|
}
|
|
17
|
+
// HTML export requires Pro tier (skip in dev/local builds)
|
|
18
|
+
if (format === "html" && !process.env.ARCHBYTE_DEV) {
|
|
19
|
+
const { loadCredentials } = await import("./auth.js");
|
|
20
|
+
const creds = loadCredentials();
|
|
21
|
+
if (!creds || creds.tier !== "premium") {
|
|
22
|
+
console.error();
|
|
23
|
+
console.error(chalk.red("HTML export requires a Pro subscription."));
|
|
24
|
+
console.error(chalk.gray("Upgrade at https://heartbyte.io/archbyte"));
|
|
25
|
+
console.error();
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
15
29
|
let output;
|
|
16
30
|
switch (format) {
|
|
17
31
|
case "mermaid":
|
|
@@ -26,6 +40,9 @@ export async function handleExport(options) {
|
|
|
26
40
|
case "dot":
|
|
27
41
|
output = exportDot(arch);
|
|
28
42
|
break;
|
|
43
|
+
case "html":
|
|
44
|
+
output = await exportHtml(arch);
|
|
45
|
+
break;
|
|
29
46
|
default:
|
|
30
47
|
output = exportMarkdown(arch);
|
|
31
48
|
break;
|
|
@@ -306,6 +323,51 @@ function exportDot(arch) {
|
|
|
306
323
|
lines.push("}");
|
|
307
324
|
return lines.join("\n");
|
|
308
325
|
}
|
|
326
|
+
/**
|
|
327
|
+
* Export architecture as a self-contained interactive HTML file.
|
|
328
|
+
* Reads the pre-built UI assets from ui/dist/ and inlines them
|
|
329
|
+
* with the architecture data injected as window.__ARCHBYTE_DATA__.
|
|
330
|
+
*/
|
|
331
|
+
async function exportHtml(arch) {
|
|
332
|
+
const fs = await import("fs");
|
|
333
|
+
const pathMod = await import("path");
|
|
334
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
335
|
+
const __dirname = pathMod.dirname(__filename);
|
|
336
|
+
const uiDist = pathMod.resolve(__dirname, "../../ui/dist");
|
|
337
|
+
if (!fs.existsSync(uiDist)) {
|
|
338
|
+
console.error(chalk.red("UI build not found at " + uiDist));
|
|
339
|
+
console.error(chalk.gray("Run: npm run build:ui"));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
// Read CSS and JS assets
|
|
343
|
+
const assetsDir = pathMod.join(uiDist, "assets");
|
|
344
|
+
const assetFiles = fs.readdirSync(assetsDir);
|
|
345
|
+
let cssContent = "";
|
|
346
|
+
for (const f of assetFiles.filter((f) => f.endsWith(".css"))) {
|
|
347
|
+
cssContent += fs.readFileSync(pathMod.join(assetsDir, f), "utf-8") + "\n";
|
|
348
|
+
}
|
|
349
|
+
let jsContent = "";
|
|
350
|
+
for (const f of assetFiles.filter((f) => f.endsWith(".js"))) {
|
|
351
|
+
jsContent += fs.readFileSync(pathMod.join(assetsDir, f), "utf-8") + "\n";
|
|
352
|
+
}
|
|
353
|
+
const projectName = process.cwd().split("/").pop() || "Architecture";
|
|
354
|
+
// Build HTML from scratch (avoids fragile regex on Vite template)
|
|
355
|
+
return `<!DOCTYPE html>
|
|
356
|
+
<html lang="en" data-theme="dark">
|
|
357
|
+
<head>
|
|
358
|
+
<meta charset="UTF-8">
|
|
359
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
360
|
+
<title>${projectName} - ArchByte Architecture</title>
|
|
361
|
+
<style>${cssContent}</style>
|
|
362
|
+
</head>
|
|
363
|
+
<body>
|
|
364
|
+
<script>try{if(localStorage.getItem('archbyte-theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}</script>
|
|
365
|
+
<div id="root"></div>
|
|
366
|
+
<script>window.__ARCHBYTE_DATA__ = ${JSON.stringify(arch)};</script>
|
|
367
|
+
<script type="module">${jsContent}</script>
|
|
368
|
+
</body>
|
|
369
|
+
</html>`;
|
|
370
|
+
}
|
|
309
371
|
/**
|
|
310
372
|
* Sanitize an ID for use in PlantUML diagrams.
|
|
311
373
|
*/
|
package/dist/cli/license-gate.js
CHANGED
|
@@ -28,8 +28,9 @@ export async function requireLicense(action) {
|
|
|
28
28
|
console.error(chalk.gray("Free tier includes unlimited scans. No credit card required."));
|
|
29
29
|
process.exit(1);
|
|
30
30
|
}
|
|
31
|
-
// Token expired locally
|
|
32
|
-
|
|
31
|
+
// Token expired locally (treat invalid dates as expired — fail-closed)
|
|
32
|
+
const expiry = new Date(creds.expiresAt);
|
|
33
|
+
if (isNaN(expiry.getTime()) || expiry < new Date()) {
|
|
33
34
|
console.error();
|
|
34
35
|
console.error(chalk.red("Session expired."));
|
|
35
36
|
console.error(chalk.gray("Run `archbyte login` to refresh your session."));
|
package/dist/cli/mcp.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
2
|
import * as fs from "fs";
|
|
3
3
|
import * as path from "path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
return false;
|
|
12
|
-
}
|
|
13
|
-
}
|
|
5
|
+
import { isInPath } from "./utils.js";
|
|
6
|
+
import { CONFIG_DIR } from "./constants.js";
|
|
14
7
|
export async function handleMcpInstall() {
|
|
15
8
|
console.log();
|
|
16
9
|
console.log(chalk.bold.cyan("ArchByte MCP Setup"));
|
|
@@ -30,7 +23,7 @@ export async function handleMcpInstall() {
|
|
|
30
23
|
console.log();
|
|
31
24
|
}
|
|
32
25
|
// ─── Codex CLI ───
|
|
33
|
-
const codexDir = path.join(
|
|
26
|
+
const codexDir = path.join(CONFIG_DIR, "../.codex");
|
|
34
27
|
const codexConfig = path.join(codexDir, "config.toml");
|
|
35
28
|
if (fs.existsSync(codexDir)) {
|
|
36
29
|
console.log(chalk.white("Detected Codex CLI."));
|
|
@@ -46,7 +39,10 @@ export async function handleMcpInstall() {
|
|
|
46
39
|
configured = true;
|
|
47
40
|
}
|
|
48
41
|
else {
|
|
49
|
-
|
|
42
|
+
// Ensure a trailing newline before appending the TOML block
|
|
43
|
+
// so headers don't merge with the last line of the existing file.
|
|
44
|
+
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
45
|
+
const block = `${needsNewline ? "\n" : ""}
|
|
50
46
|
[mcp_servers.archbyte]
|
|
51
47
|
type = "stdio"
|
|
52
48
|
command = "npx"
|
package/dist/cli/setup.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { execSync } from "child_process";
|
|
5
4
|
import chalk from "chalk";
|
|
6
5
|
import { resolveModel } from "../agents/runtime/types.js";
|
|
7
6
|
import { createProvider } from "../agents/providers/router.js";
|
|
8
7
|
import { select, spinner, confirm } from "./ui.js";
|
|
9
8
|
import { CONFIG_DIR, CONFIG_PATH } from "./constants.js";
|
|
9
|
+
import { isInPath, maskKey, isTTY, isValidEmail } from "./utils.js";
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
const PROVIDERS = [
|
|
@@ -15,6 +15,12 @@ const PROVIDERS = [
|
|
|
15
15
|
{ name: "google", label: "Google", hint: "Gemini 2.5 Pro / Flash" },
|
|
16
16
|
];
|
|
17
17
|
const PROVIDER_MODELS = {
|
|
18
|
+
"claude-sdk": [
|
|
19
|
+
{ id: "", label: "Default (recommended)", hint: "Sonnet for all agents" },
|
|
20
|
+
{ id: "opus", label: "Claude Opus 4.6", hint: "Most capable" },
|
|
21
|
+
{ id: "sonnet", label: "Claude Sonnet 4.5", hint: "Fast, great quality" },
|
|
22
|
+
{ id: "haiku", label: "Claude Haiku 4.5", hint: "Fastest, cheapest" },
|
|
23
|
+
],
|
|
18
24
|
anthropic: [
|
|
19
25
|
{ id: "", label: "Default (recommended)", hint: "Opus for all agents" },
|
|
20
26
|
{ id: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable" },
|
|
@@ -49,13 +55,23 @@ function saveConfig(config) {
|
|
|
49
55
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
50
56
|
}
|
|
51
57
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
58
|
+
// Restrict permissions — config contains API keys in profiles
|
|
59
|
+
try {
|
|
60
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Windows doesn't support chmod
|
|
64
|
+
}
|
|
52
65
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
66
|
+
/**
|
|
67
|
+
* Read a line from stdin with character masking (for API keys).
|
|
68
|
+
* Non-TTY fallback: reads from stdin as a line (for piped input / CI).
|
|
69
|
+
*/
|
|
58
70
|
function askHidden(prompt) {
|
|
71
|
+
if (!isTTY()) {
|
|
72
|
+
process.stdout.write(prompt);
|
|
73
|
+
return readLineFromStdin();
|
|
74
|
+
}
|
|
59
75
|
return new Promise((resolve) => {
|
|
60
76
|
process.stdout.write(prompt);
|
|
61
77
|
const stdin = process.stdin;
|
|
@@ -93,7 +109,15 @@ function askHidden(prompt) {
|
|
|
93
109
|
stdin.on("data", onData);
|
|
94
110
|
});
|
|
95
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Read a line of visible text from stdin.
|
|
114
|
+
* Non-TTY fallback: reads from stdin as a line (for piped input / CI).
|
|
115
|
+
*/
|
|
96
116
|
function askText(prompt) {
|
|
117
|
+
if (!isTTY()) {
|
|
118
|
+
process.stdout.write(prompt);
|
|
119
|
+
return readLineFromStdin();
|
|
120
|
+
}
|
|
97
121
|
return new Promise((resolve) => {
|
|
98
122
|
process.stdout.write(prompt);
|
|
99
123
|
const stdin = process.stdin;
|
|
@@ -131,6 +155,32 @@ function askText(prompt) {
|
|
|
131
155
|
stdin.on("data", onData);
|
|
132
156
|
});
|
|
133
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Non-TTY line reader. Reads a single line from stdin (for piped input).
|
|
160
|
+
*/
|
|
161
|
+
function readLineFromStdin() {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const stdin = process.stdin;
|
|
164
|
+
stdin.resume();
|
|
165
|
+
stdin.setEncoding("utf8");
|
|
166
|
+
let buf = "";
|
|
167
|
+
const onData = (data) => {
|
|
168
|
+
buf += data;
|
|
169
|
+
const nl = buf.indexOf("\n");
|
|
170
|
+
if (nl !== -1) {
|
|
171
|
+
stdin.removeListener("data", onData);
|
|
172
|
+
stdin.pause();
|
|
173
|
+
resolve(buf.slice(0, nl).trim());
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const onEnd = () => {
|
|
177
|
+
stdin.removeListener("data", onData);
|
|
178
|
+
resolve(buf.trim());
|
|
179
|
+
};
|
|
180
|
+
stdin.on("data", onData);
|
|
181
|
+
stdin.once("end", onEnd);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
134
184
|
async function validateProviderSilent(providerName, apiKey, model) {
|
|
135
185
|
try {
|
|
136
186
|
const provider = createProvider({ provider: providerName, apiKey });
|
|
@@ -157,29 +207,14 @@ async function validateProviderSilent(providerName, apiKey, model) {
|
|
|
157
207
|
function getProfiles(config) {
|
|
158
208
|
return config.profiles ?? {};
|
|
159
209
|
}
|
|
160
|
-
function isInPath(cmd) {
|
|
161
|
-
try {
|
|
162
|
-
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
210
|
export async function handleSetup() {
|
|
170
211
|
console.log();
|
|
171
212
|
console.log(chalk.bold.cyan("ArchByte Setup"));
|
|
172
213
|
console.log(chalk.gray("Configure your model provider and API key.\n"));
|
|
173
214
|
// Detect AI coding tools — suggest MCP instead of BYOK
|
|
174
215
|
const hasClaude = isInPath("claude");
|
|
175
|
-
const codexDir = path.join(
|
|
216
|
+
const codexDir = path.join(CONFIG_DIR, "../.codex");
|
|
176
217
|
const hasCodex = fs.existsSync(codexDir);
|
|
177
|
-
if (hasClaude || hasCodex) {
|
|
178
|
-
const tools = [hasClaude && "Claude Code", hasCodex && "Codex CLI"].filter(Boolean).join(" and ");
|
|
179
|
-
console.log(chalk.cyan(` Detected ${tools} on this machine.`));
|
|
180
|
-
console.log(chalk.white(` After setup, run `) + chalk.bold.cyan(`archbyte mcp install`) + chalk.white(` to use ArchByte from your AI tool.`));
|
|
181
|
-
console.log();
|
|
182
|
-
}
|
|
183
218
|
const config = loadConfig();
|
|
184
219
|
const profiles = getProfiles(config);
|
|
185
220
|
// Migrate legacy flat config → profiles
|
|
@@ -212,6 +247,79 @@ export async function handleSetup() {
|
|
|
212
247
|
}
|
|
213
248
|
console.log();
|
|
214
249
|
}
|
|
250
|
+
// Detect AI coding tools and offer zero-config options
|
|
251
|
+
if (hasClaude || hasCodex) {
|
|
252
|
+
const tools = [hasClaude && "Claude Code", hasCodex && "Codex CLI"].filter(Boolean).join(" and ");
|
|
253
|
+
console.log(chalk.cyan(` Detected ${tools} on this machine.\n`));
|
|
254
|
+
// Build options based on what's detected
|
|
255
|
+
const toolOptions = [];
|
|
256
|
+
if (hasClaude) {
|
|
257
|
+
toolOptions.push({
|
|
258
|
+
label: `Claude Code (SDK) ${chalk.gray("zero config — uses your Claude Code subscription")}`,
|
|
259
|
+
value: "claude-sdk",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (hasCodex) {
|
|
263
|
+
toolOptions.push({
|
|
264
|
+
label: `Codex CLI ${chalk.gray("zero config — uses your Codex subscription")}`,
|
|
265
|
+
value: "codex",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
toolOptions.push({
|
|
269
|
+
label: `Bring your own API key ${chalk.gray("Anthropic, OpenAI, or Google")}`,
|
|
270
|
+
value: "byok",
|
|
271
|
+
});
|
|
272
|
+
const toolIdx = await select("How do you want to run ArchByte?", toolOptions.map((o) => o.label));
|
|
273
|
+
const choice = toolOptions[toolIdx].value;
|
|
274
|
+
if (choice === "claude-sdk") {
|
|
275
|
+
config.provider = "claude-sdk";
|
|
276
|
+
// Model selection
|
|
277
|
+
const models = PROVIDER_MODELS["claude-sdk"];
|
|
278
|
+
const modelIdx = await select("\n Choose a model:", models.map((m) => `${m.label} ${chalk.gray(m.hint)}`));
|
|
279
|
+
const chosenModel = models[modelIdx];
|
|
280
|
+
if (chosenModel.id) {
|
|
281
|
+
config.model = chosenModel.id;
|
|
282
|
+
console.log(chalk.green(` ✓ Model: ${chosenModel.label}`));
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
delete config.model;
|
|
286
|
+
console.log(chalk.green(` ✓ Model: Sonnet (default)`));
|
|
287
|
+
}
|
|
288
|
+
config.profiles = profiles;
|
|
289
|
+
delete config.apiKey;
|
|
290
|
+
saveConfig(config);
|
|
291
|
+
const dim = chalk.gray;
|
|
292
|
+
const sep = dim(" ───");
|
|
293
|
+
console.log();
|
|
294
|
+
console.log(chalk.bold.green(" ✓ Setup complete — using Claude Code (SDK)"));
|
|
295
|
+
console.log();
|
|
296
|
+
console.log(sep);
|
|
297
|
+
console.log();
|
|
298
|
+
console.log(dim(" No API key needed. ArchByte uses your Claude Code subscription."));
|
|
299
|
+
console.log(dim(" All model calls go through Claude Code on this machine."));
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(sep);
|
|
302
|
+
console.log();
|
|
303
|
+
console.log(" " + chalk.bold("Next steps"));
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
|
|
306
|
+
if (hasCodex) {
|
|
307
|
+
console.log(" " + chalk.cyan("archbyte mcp install") + " Use from Codex CLI");
|
|
308
|
+
}
|
|
309
|
+
console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
|
|
310
|
+
console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
|
|
311
|
+
console.log();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (choice === "codex") {
|
|
315
|
+
// TODO: Add Codex SDK provider when available
|
|
316
|
+
console.log(chalk.yellow("\n Codex SDK provider coming soon. Setting up with API key for now."));
|
|
317
|
+
console.log(chalk.gray(" In the meantime, use archbyte mcp install to run ArchByte from Codex.\n"));
|
|
318
|
+
}
|
|
319
|
+
// User chose BYOK — continue to normal provider selection below
|
|
320
|
+
if (choice === "byok")
|
|
321
|
+
console.log();
|
|
322
|
+
}
|
|
215
323
|
// Step 1: Choose provider
|
|
216
324
|
const idx = await select("Choose your model provider:", PROVIDERS.map((p) => {
|
|
217
325
|
const active = config.provider === p.name ? chalk.green(" (active)") : "";
|
|
@@ -258,7 +366,7 @@ export async function handleSetup() {
|
|
|
258
366
|
if (!profiles[provider])
|
|
259
367
|
profiles[provider] = { apiKey: "" };
|
|
260
368
|
profiles[provider].apiKey = apiKey;
|
|
261
|
-
// Step 2b: Email for this provider account (optional)
|
|
369
|
+
// Step 2b: Email for this provider account (optional, with validation)
|
|
262
370
|
const existingEmail = profiles[provider].email;
|
|
263
371
|
if (existingEmail) {
|
|
264
372
|
console.log(chalk.gray(`\n Account email: ${existingEmail}`));
|
|
@@ -270,16 +378,26 @@ export async function handleSetup() {
|
|
|
270
378
|
if (emailIdx === 1) {
|
|
271
379
|
const newEmail = await askText(chalk.bold(" Email: "));
|
|
272
380
|
if (newEmail) {
|
|
273
|
-
|
|
274
|
-
|
|
381
|
+
if (!isValidEmail(newEmail)) {
|
|
382
|
+
console.log(chalk.yellow(` "${newEmail}" doesn't look like a valid email. Skipping.`));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
profiles[provider].email = newEmail;
|
|
386
|
+
console.log(chalk.green(` ✓ Email: ${newEmail}`));
|
|
387
|
+
}
|
|
275
388
|
}
|
|
276
389
|
}
|
|
277
390
|
}
|
|
278
391
|
else {
|
|
279
392
|
const email = await askText(chalk.bold(` ${selected.label} account email ${chalk.gray("(optional, Enter to skip)")}: `));
|
|
280
393
|
if (email) {
|
|
281
|
-
|
|
282
|
-
|
|
394
|
+
if (!isValidEmail(email)) {
|
|
395
|
+
console.log(chalk.yellow(` "${email}" doesn't look like a valid email. Skipping.`));
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
profiles[provider].email = email;
|
|
399
|
+
console.log(chalk.green(` ✓ Email: ${email}`));
|
|
400
|
+
}
|
|
283
401
|
}
|
|
284
402
|
}
|
|
285
403
|
// Step 3: Model selection
|
|
@@ -323,11 +441,14 @@ export async function handleSetup() {
|
|
|
323
441
|
if (result === false) {
|
|
324
442
|
let retries = 0;
|
|
325
443
|
while (result === false && retries < 2) {
|
|
326
|
-
if (!await confirm(" Retry?"))
|
|
444
|
+
if (!await confirm(" Retry with a different key?"))
|
|
327
445
|
break;
|
|
328
446
|
retries++;
|
|
329
447
|
const newKey = await askHidden(chalk.bold(" API key: "));
|
|
330
|
-
if (newKey) {
|
|
448
|
+
if (!newKey) {
|
|
449
|
+
console.log(chalk.yellow(" No key entered. Retrying with existing key."));
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
331
452
|
profiles[provider].apiKey = newKey;
|
|
332
453
|
console.log(chalk.green(` ✓ API key: ${maskKey(newKey)}`));
|
|
333
454
|
}
|
|
@@ -433,10 +554,16 @@ export async function handleSetup() {
|
|
|
433
554
|
catch { /* ignore */ }
|
|
434
555
|
}
|
|
435
556
|
const templatePath = path.resolve(__dirname, "../../templates/archbyte.yaml");
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
557
|
+
if (!fs.existsSync(templatePath)) {
|
|
558
|
+
console.log(chalk.yellow(" Could not find archbyte.yaml template. Skipping."));
|
|
559
|
+
console.log(chalk.gray(` Expected at: ${templatePath}`));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
let template = fs.readFileSync(templatePath, "utf-8");
|
|
563
|
+
template = template.replace("name: my-project", `name: ${projectName}`);
|
|
564
|
+
fs.writeFileSync(yamlPath, template, "utf-8");
|
|
565
|
+
yamlCreated = true;
|
|
566
|
+
}
|
|
440
567
|
}
|
|
441
568
|
// Generate README.md in .archbyte/
|
|
442
569
|
writeArchbyteReadme(archbyteDir);
|
|
@@ -465,15 +592,19 @@ export async function handleSetup() {
|
|
|
465
592
|
console.log();
|
|
466
593
|
console.log(sep);
|
|
467
594
|
console.log();
|
|
468
|
-
console.log(" " + chalk.bold("Next"));
|
|
595
|
+
console.log(" " + chalk.bold("Next steps"));
|
|
596
|
+
console.log();
|
|
469
597
|
console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
|
|
470
598
|
if (hasClaude || hasCodex) {
|
|
471
599
|
console.log(" " + chalk.cyan("archbyte mcp install") + " Use from your AI tool");
|
|
472
600
|
}
|
|
601
|
+
console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
|
|
602
|
+
console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
|
|
473
603
|
if (result === false) {
|
|
474
|
-
console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
|
|
475
604
|
console.log();
|
|
605
|
+
console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
|
|
476
606
|
}
|
|
607
|
+
console.log();
|
|
477
608
|
}
|
|
478
609
|
function writeArchbyteReadme(archbyteDir) {
|
|
479
610
|
const readmePath = path.join(archbyteDir, "README.md");
|
package/dist/cli/ui.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare function spinner(label: string): Spinner;
|
|
|
8
8
|
/**
|
|
9
9
|
* Arrow-key selection menu. Returns the selected index.
|
|
10
10
|
* Non-TTY fallback: returns 0 (first option).
|
|
11
|
+
*
|
|
12
|
+
* Navigation: ↑/↓ arrows to move, Enter to confirm, Ctrl+C to exit.
|
|
11
13
|
*/
|
|
12
14
|
export declare function select(prompt: string, options: string[]): Promise<number>;
|
|
13
15
|
interface ProgressBar {
|
|
@@ -23,6 +25,18 @@ export declare function progressBar(totalSteps: number): ProgressBar;
|
|
|
23
25
|
/**
|
|
24
26
|
* Y/n confirmation prompt. Returns true for y/Enter, false for n.
|
|
25
27
|
* Non-TTY fallback: returns true.
|
|
28
|
+
*
|
|
29
|
+
* Only responds to explicit y/n/Enter/Ctrl+C. Ignores escape sequences
|
|
30
|
+
* (arrow keys, etc.) to prevent accidental confirmation.
|
|
26
31
|
*/
|
|
27
32
|
export declare function confirm(prompt: string): Promise<boolean>;
|
|
33
|
+
/**
|
|
34
|
+
* Text input prompt. Returns the entered string.
|
|
35
|
+
* Non-TTY fallback: returns empty string.
|
|
36
|
+
*
|
|
37
|
+
* @param mask - If true, replaces each character with * (for passwords).
|
|
38
|
+
*/
|
|
39
|
+
export declare function textInput(prompt: string, opts?: {
|
|
40
|
+
mask?: boolean;
|
|
41
|
+
}): Promise<string>;
|
|
28
42
|
export {};
|
package/dist/cli/ui.js
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
const BRAILLE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
// ─── Cursor Safety ───
|
|
4
|
+
// Ensure the terminal cursor is always restored, even on unhandled crashes.
|
|
5
|
+
let cursorHidden = false;
|
|
6
|
+
function hideCursor() {
|
|
7
|
+
if (!cursorHidden) {
|
|
8
|
+
process.stdout.write("\x1b[?25l");
|
|
9
|
+
cursorHidden = true;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function showCursor() {
|
|
13
|
+
if (cursorHidden) {
|
|
14
|
+
process.stdout.write("\x1b[?25h");
|
|
15
|
+
cursorHidden = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// Restore cursor on any exit path
|
|
19
|
+
for (const event of ["exit", "SIGINT", "SIGTERM", "uncaughtException", "unhandledRejection"]) {
|
|
20
|
+
process.on(event, () => {
|
|
21
|
+
showCursor();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
3
24
|
/**
|
|
4
25
|
* Animated braille spinner. Falls back to static console.log when not a TTY.
|
|
5
26
|
*/
|
|
@@ -30,6 +51,8 @@ export function spinner(label) {
|
|
|
30
51
|
/**
|
|
31
52
|
* Arrow-key selection menu. Returns the selected index.
|
|
32
53
|
* Non-TTY fallback: returns 0 (first option).
|
|
54
|
+
*
|
|
55
|
+
* Navigation: ↑/↓ arrows to move, Enter to confirm, Ctrl+C to exit.
|
|
33
56
|
*/
|
|
34
57
|
export function select(prompt, options) {
|
|
35
58
|
if (!process.stdout.isTTY || options.length === 0) {
|
|
@@ -45,10 +68,9 @@ export function select(prompt, options) {
|
|
|
45
68
|
stdin.resume();
|
|
46
69
|
stdin.setEncoding("utf8");
|
|
47
70
|
let selected = 0;
|
|
48
|
-
|
|
49
|
-
process.stdout.write("\x1b[?25l");
|
|
71
|
+
hideCursor();
|
|
50
72
|
function render() {
|
|
51
|
-
// Move up to clear previous render
|
|
73
|
+
// Move up to clear previous render
|
|
52
74
|
const lines = options.length + 1; // prompt + options
|
|
53
75
|
process.stdout.write(`\x1b[${lines}A`);
|
|
54
76
|
process.stdout.write(`\x1b[K ${chalk.bold(prompt)}\n`);
|
|
@@ -74,13 +96,13 @@ export function select(prompt, options) {
|
|
|
74
96
|
}
|
|
75
97
|
}
|
|
76
98
|
const onData = (data) => {
|
|
77
|
-
if (data === "\x1b[A") {
|
|
78
|
-
// Up arrow
|
|
99
|
+
if (data === "\x1b[A" || data === "k") {
|
|
100
|
+
// Up arrow or k (vim-style)
|
|
79
101
|
selected = (selected - 1 + options.length) % options.length;
|
|
80
102
|
render();
|
|
81
103
|
}
|
|
82
|
-
else if (data === "\x1b[B") {
|
|
83
|
-
// Down arrow
|
|
104
|
+
else if (data === "\x1b[B" || data === "j") {
|
|
105
|
+
// Down arrow or j (vim-style)
|
|
84
106
|
selected = (selected + 1) % options.length;
|
|
85
107
|
render();
|
|
86
108
|
}
|
|
@@ -89,9 +111,10 @@ export function select(prompt, options) {
|
|
|
89
111
|
cleanup();
|
|
90
112
|
resolve(selected);
|
|
91
113
|
}
|
|
92
|
-
else if (data === "\x03" || data === "q"
|
|
93
|
-
// Ctrl+C or q
|
|
114
|
+
else if (data === "\x03" || data === "q") {
|
|
115
|
+
// Ctrl+C or q — clean exit
|
|
94
116
|
cleanup();
|
|
117
|
+
process.stdout.write("\n");
|
|
95
118
|
process.exit(0);
|
|
96
119
|
}
|
|
97
120
|
};
|
|
@@ -99,8 +122,7 @@ export function select(prompt, options) {
|
|
|
99
122
|
stdin.removeListener("data", onData);
|
|
100
123
|
stdin.setRawMode(wasRaw ?? false);
|
|
101
124
|
stdin.pause();
|
|
102
|
-
|
|
103
|
-
process.stdout.write("\x1b[?25h");
|
|
125
|
+
showCursor();
|
|
104
126
|
}
|
|
105
127
|
stdin.on("data", onData);
|
|
106
128
|
});
|
|
@@ -152,6 +174,9 @@ export function progressBar(totalSteps) {
|
|
|
152
174
|
/**
|
|
153
175
|
* Y/n confirmation prompt. Returns true for y/Enter, false for n.
|
|
154
176
|
* Non-TTY fallback: returns true.
|
|
177
|
+
*
|
|
178
|
+
* Only responds to explicit y/n/Enter/Ctrl+C. Ignores escape sequences
|
|
179
|
+
* (arrow keys, etc.) to prevent accidental confirmation.
|
|
155
180
|
*/
|
|
156
181
|
export function confirm(prompt) {
|
|
157
182
|
if (!process.stdout.isTTY) {
|
|
@@ -166,6 +191,9 @@ export function confirm(prompt) {
|
|
|
166
191
|
stdin.resume();
|
|
167
192
|
stdin.setEncoding("utf8");
|
|
168
193
|
const onData = (data) => {
|
|
194
|
+
// Ignore escape sequences (arrow keys, function keys, etc.)
|
|
195
|
+
if (data.startsWith("\x1b"))
|
|
196
|
+
return;
|
|
169
197
|
stdin.removeListener("data", onData);
|
|
170
198
|
stdin.setRawMode(wasRaw ?? false);
|
|
171
199
|
stdin.pause();
|
|
@@ -173,15 +201,71 @@ export function confirm(prompt) {
|
|
|
173
201
|
process.stdout.write("n\n");
|
|
174
202
|
resolve(false);
|
|
175
203
|
}
|
|
176
|
-
else if (data === "\x03" || data === "q"
|
|
204
|
+
else if (data === "\x03" || data === "q") {
|
|
205
|
+
// Ctrl+C or q — clean exit
|
|
177
206
|
process.stdout.write("\n");
|
|
178
207
|
process.exit(0);
|
|
179
208
|
}
|
|
180
|
-
else {
|
|
181
|
-
// y, Y, Enter — all true
|
|
209
|
+
else if (data === "y" || data === "Y" || data === "\r" || data === "\n") {
|
|
182
210
|
process.stdout.write("y\n");
|
|
183
211
|
resolve(true);
|
|
184
212
|
}
|
|
213
|
+
// Ignore any other single keypresses — wait for y/n/Enter
|
|
214
|
+
};
|
|
215
|
+
stdin.on("data", onData);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Text input prompt. Returns the entered string.
|
|
220
|
+
* Non-TTY fallback: returns empty string.
|
|
221
|
+
*
|
|
222
|
+
* @param mask - If true, replaces each character with * (for passwords).
|
|
223
|
+
*/
|
|
224
|
+
export function textInput(prompt, opts) {
|
|
225
|
+
if (!process.stdout.isTTY) {
|
|
226
|
+
console.log(` ${prompt}: `);
|
|
227
|
+
return Promise.resolve("");
|
|
228
|
+
}
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
process.stdout.write(` ${prompt}: `);
|
|
231
|
+
const stdin = process.stdin;
|
|
232
|
+
const wasRaw = stdin.isRaw;
|
|
233
|
+
stdin.setRawMode(true);
|
|
234
|
+
stdin.resume();
|
|
235
|
+
stdin.setEncoding("utf8");
|
|
236
|
+
let value = "";
|
|
237
|
+
const onData = (data) => {
|
|
238
|
+
if (data === "\r" || data === "\n") {
|
|
239
|
+
// Enter — submit
|
|
240
|
+
stdin.removeListener("data", onData);
|
|
241
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
242
|
+
stdin.pause();
|
|
243
|
+
process.stdout.write("\n");
|
|
244
|
+
resolve(value);
|
|
245
|
+
}
|
|
246
|
+
else if (data === "\x03") {
|
|
247
|
+
// Ctrl+C
|
|
248
|
+
stdin.removeListener("data", onData);
|
|
249
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
250
|
+
stdin.pause();
|
|
251
|
+
process.stdout.write("\n");
|
|
252
|
+
process.exit(0);
|
|
253
|
+
}
|
|
254
|
+
else if (data === "\x7f" || data === "\b") {
|
|
255
|
+
// Backspace
|
|
256
|
+
if (value.length > 0) {
|
|
257
|
+
value = value.slice(0, -1);
|
|
258
|
+
process.stdout.write("\b \b");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (data.startsWith("\x1b")) {
|
|
262
|
+
// Ignore escape sequences
|
|
263
|
+
}
|
|
264
|
+
else if (data >= " ") {
|
|
265
|
+
// Printable character
|
|
266
|
+
value += data;
|
|
267
|
+
process.stdout.write(opts?.mask ? "*" : data);
|
|
268
|
+
}
|
|
185
269
|
};
|
|
186
270
|
stdin.on("data", onData);
|
|
187
271
|
});
|