archbyte 0.3.4 → 0.3.5
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/bin/archbyte.js +8 -9
- package/dist/cli/auth.d.ts +3 -0
- package/dist/cli/auth.js +71 -42
- package/dist/cli/config.d.ts +1 -0
- package/dist/cli/config.js +20 -10
- package/dist/cli/constants.js +4 -1
- 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 +87 -29
- package/dist/cli/ui.d.ts +5 -0
- package/dist/cli/ui.js +44 -14
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +52 -0
- package/dist/server/src/index.js +3 -3
- package/package.json +1 -1
- package/ui/dist/assets/{index-Bdr9FnaA.js → index-BTo0zV5E.js} +13 -13
- package/ui/dist/index.html +1 -1
package/bin/archbyte.js
CHANGED
|
@@ -32,17 +32,15 @@ const program = new Command();
|
|
|
32
32
|
|
|
33
33
|
program
|
|
34
34
|
.name('archbyte')
|
|
35
|
-
.description('ArchByte -
|
|
36
|
-
.version(PKG_VERSION)
|
|
35
|
+
.description('ArchByte - AI architecture analysis with an interactive diagram UI')
|
|
36
|
+
.version(PKG_VERSION, '-v, --version', 'Show version number')
|
|
37
37
|
.addHelpText('after', `
|
|
38
38
|
Quick start:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
4. $ archbyte run Analyze → generate → serve
|
|
39
|
+
$ archbyte login Sign in or create a free account
|
|
40
|
+
$ archbyte init Configure your model provider
|
|
41
|
+
$ archbyte run Analyze + open interactive diagram UI
|
|
43
42
|
|
|
44
43
|
https://archbyte.heartbyte.io
|
|
45
|
-
Support: archbyte@heartbyte.io
|
|
46
44
|
`);
|
|
47
45
|
|
|
48
46
|
// — Getting started —
|
|
@@ -220,8 +218,9 @@ program
|
|
|
220
218
|
.argument('[action]', 'show, set, get, or path')
|
|
221
219
|
.argument('[key]', 'config key (provider, api-key, model)')
|
|
222
220
|
.argument('[value]', 'config value')
|
|
223
|
-
.
|
|
224
|
-
|
|
221
|
+
.option('--raw', 'Show unmasked values (for scripting)')
|
|
222
|
+
.action(async (action, key, value, options) => {
|
|
223
|
+
await handleConfig({ args: [action, key, value].filter(Boolean), raw: options.raw });
|
|
225
224
|
});
|
|
226
225
|
|
|
227
226
|
program
|
package/dist/cli/auth.d.ts
CHANGED
|
@@ -34,6 +34,9 @@ export declare function getVerifiedTier(): "free" | "premium";
|
|
|
34
34
|
/**
|
|
35
35
|
* Check if an offline action is allowed. Returns true if within limits.
|
|
36
36
|
* Increments the counter when allowed.
|
|
37
|
+
*
|
|
38
|
+
* Uses atomic write-to-temp-then-rename to prevent race conditions
|
|
39
|
+
* between concurrent CLI invocations.
|
|
37
40
|
*/
|
|
38
41
|
export declare function checkOfflineAction(): {
|
|
39
42
|
allowed: boolean;
|
package/dist/cli/auth.js
CHANGED
|
@@ -178,8 +178,17 @@ function saveCredentials(creds) {
|
|
|
178
178
|
// Windows doesn't support chmod
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Check if credentials are expired. Treats invalid/unparseable dates
|
|
183
|
+
* as expired (fail-closed) to prevent corrupted files from bypassing
|
|
184
|
+
* expiry checks.
|
|
185
|
+
*/
|
|
181
186
|
function isExpired(creds) {
|
|
182
|
-
|
|
187
|
+
const expiry = new Date(creds.expiresAt);
|
|
188
|
+
// Invalid Date — treat as expired (fail-closed)
|
|
189
|
+
if (isNaN(expiry.getTime()))
|
|
190
|
+
return true;
|
|
191
|
+
return expiry < new Date();
|
|
183
192
|
}
|
|
184
193
|
function parseJWTPayload(token) {
|
|
185
194
|
try {
|
|
@@ -268,6 +277,9 @@ const OFFLINE_MAX_FREE = 0; // Free users: 0 offline actions (must verify online
|
|
|
268
277
|
/**
|
|
269
278
|
* Check if an offline action is allowed. Returns true if within limits.
|
|
270
279
|
* Increments the counter when allowed.
|
|
280
|
+
*
|
|
281
|
+
* Uses atomic write-to-temp-then-rename to prevent race conditions
|
|
282
|
+
* between concurrent CLI invocations.
|
|
271
283
|
*/
|
|
272
284
|
export function checkOfflineAction() {
|
|
273
285
|
const tier = getVerifiedTier();
|
|
@@ -293,13 +305,15 @@ export function checkOfflineAction() {
|
|
|
293
305
|
reason: `Offline action limit reached (${maxActions}/${maxActions}). Reconnect to the license server to continue.`,
|
|
294
306
|
};
|
|
295
307
|
}
|
|
296
|
-
// Increment and save
|
|
308
|
+
// Increment and save atomically (write to temp file, then rename)
|
|
297
309
|
data.count++;
|
|
298
|
-
|
|
310
|
+
const tmpPath = OFFLINE_ACTIONS_PATH + `.${process.pid}.tmp`;
|
|
311
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data), "utf-8");
|
|
299
312
|
try {
|
|
300
|
-
fs.chmodSync(
|
|
313
|
+
fs.chmodSync(tmpPath, 0o600);
|
|
301
314
|
}
|
|
302
315
|
catch { /* Windows */ }
|
|
316
|
+
fs.renameSync(tmpPath, OFFLINE_ACTIONS_PATH);
|
|
303
317
|
return { allowed: true };
|
|
304
318
|
}
|
|
305
319
|
catch {
|
|
@@ -322,51 +336,66 @@ export function resetOfflineActions() {
|
|
|
322
336
|
// === OAuth Flow ===
|
|
323
337
|
function startOAuthFlow(provider = "github") {
|
|
324
338
|
return new Promise((resolve, reject) => {
|
|
339
|
+
let resolved = false;
|
|
325
340
|
const timeout = setTimeout(() => {
|
|
326
341
|
server.close();
|
|
327
|
-
|
|
342
|
+
if (!resolved) {
|
|
343
|
+
resolved = true;
|
|
344
|
+
reject(new Error("Login timed out (60s). Try again or use --token."));
|
|
345
|
+
}
|
|
328
346
|
}, OAUTH_TIMEOUT_MS);
|
|
329
347
|
const server = http.createServer(async (req, res) => {
|
|
330
348
|
const url = new URL(req.url ?? "/", `http://localhost:${CLI_CALLBACK_PORT}`);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
res.end("<h1>Login failed</h1><p>No token received. Close this window and try again.</p>");
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
349
|
+
// Only handle /callback — return 404 for everything else
|
|
350
|
+
if (url.pathname !== "/callback") {
|
|
351
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
352
|
+
res.end("<h1>Not found</h1><p>This server only handles OAuth callbacks.</p>");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
// Prevent double-processing (e.g. browser retry, double-click)
|
|
356
|
+
if (resolved) {
|
|
342
357
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
343
|
-
res.end("<h1>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
reject(
|
|
358
|
+
res.end("<h1>Already processed</h1><p>You can close this window.</p>");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// Extract token from raw query string, not URLSearchParams
|
|
362
|
+
// (URLSearchParams decodes '+' as space per x-www-form-urlencoded, corrupting JWT signatures)
|
|
363
|
+
const rawQuery = (req.url ?? "").split("?")[1] ?? "";
|
|
364
|
+
const tokenMatch = rawQuery.match(/(?:^|&)token=([^&]+)/);
|
|
365
|
+
const token = tokenMatch ? decodeURIComponent(tokenMatch[1]) : null;
|
|
366
|
+
if (!token) {
|
|
367
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
368
|
+
res.end("<h1>Login failed</h1><p>No token received. Close this window and try again.</p>");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
372
|
+
res.end("<h1>Login successful!</h1><p>You can close this window and return to your terminal.</p>");
|
|
373
|
+
resolved = true;
|
|
374
|
+
clearTimeout(timeout);
|
|
375
|
+
server.close();
|
|
376
|
+
// Fetch user info with the token
|
|
377
|
+
try {
|
|
378
|
+
const meRes = await fetch(`${API_BASE}/api/v1/me`, {
|
|
379
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
380
|
+
});
|
|
381
|
+
if (!meRes.ok) {
|
|
382
|
+
const errBody = await meRes.text().catch(() => "");
|
|
383
|
+
reject(new Error(`Failed to fetch user info (${meRes.status}: ${errBody})`));
|
|
384
|
+
return;
|
|
369
385
|
}
|
|
386
|
+
const { user } = (await meRes.json());
|
|
387
|
+
const payload = parseJWTPayload(token);
|
|
388
|
+
resolve({
|
|
389
|
+
token,
|
|
390
|
+
email: user.email,
|
|
391
|
+
tier: user.tier,
|
|
392
|
+
expiresAt: payload?.exp
|
|
393
|
+
? new Date(payload.exp * 1000).toISOString()
|
|
394
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
reject(err);
|
|
370
399
|
}
|
|
371
400
|
});
|
|
372
401
|
server.listen(CLI_CALLBACK_PORT, "127.0.0.1", () => {
|
package/dist/cli/config.d.ts
CHANGED
package/dist/cli/config.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { CONFIG_DIR, CONFIG_PATH } from "./constants.js";
|
|
4
|
+
import { maskKey } from "./utils.js";
|
|
4
5
|
const VALID_PROVIDERS = ["anthropic", "openai", "google"];
|
|
5
6
|
export async function handleConfig(options) {
|
|
6
7
|
const [action, key, value] = options.args;
|
|
@@ -22,7 +23,7 @@ export async function handleConfig(options) {
|
|
|
22
23
|
console.error(chalk.red("Usage: archbyte config get <key>"));
|
|
23
24
|
process.exit(1);
|
|
24
25
|
}
|
|
25
|
-
getConfig(key);
|
|
26
|
+
getConfig(key, options.raw);
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
if (action === "path") {
|
|
@@ -52,6 +53,13 @@ function saveConfig(config) {
|
|
|
52
53
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
53
54
|
}
|
|
54
55
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
56
|
+
// Restrict permissions — config contains API keys in profiles
|
|
57
|
+
try {
|
|
58
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Windows doesn't support chmod
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
function showConfig() {
|
|
57
65
|
const config = loadConfig();
|
|
@@ -149,7 +157,7 @@ function setConfig(key, value) {
|
|
|
149
157
|
console.log(chalk.green(`Set ${key} = ${key.includes("key") ? maskKey(value) : value}`));
|
|
150
158
|
}
|
|
151
159
|
}
|
|
152
|
-
function getConfig(key) {
|
|
160
|
+
function getConfig(key, raw = false) {
|
|
153
161
|
const config = loadConfig();
|
|
154
162
|
const profiles = (config.profiles ?? {});
|
|
155
163
|
const active = config.provider;
|
|
@@ -160,9 +168,13 @@ function getConfig(key) {
|
|
|
160
168
|
break;
|
|
161
169
|
case "api-key":
|
|
162
170
|
case "apiKey":
|
|
163
|
-
case "key":
|
|
164
|
-
|
|
171
|
+
case "key": {
|
|
172
|
+
const apiKey = profile?.apiKey ?? config.apiKey ?? "";
|
|
173
|
+
// Mask by default to prevent accidental exposure in logs/recordings.
|
|
174
|
+
// Use `archbyte config get api-key --raw` for the unmasked value.
|
|
175
|
+
console.log(raw ? apiKey : (apiKey ? maskKey(apiKey) : ""));
|
|
165
176
|
break;
|
|
177
|
+
}
|
|
166
178
|
case "model":
|
|
167
179
|
console.log(profile?.model ?? config.model ?? "");
|
|
168
180
|
break;
|
|
@@ -171,11 +183,6 @@ function getConfig(key) {
|
|
|
171
183
|
process.exit(1);
|
|
172
184
|
}
|
|
173
185
|
}
|
|
174
|
-
function maskKey(key) {
|
|
175
|
-
if (key.length <= 8)
|
|
176
|
-
return "****";
|
|
177
|
-
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
178
|
-
}
|
|
179
186
|
/**
|
|
180
187
|
* Resolve the full ArchByteConfig from config file + env vars.
|
|
181
188
|
* Supports profiles (new) and legacy flat config (backward compat).
|
|
@@ -184,8 +191,11 @@ function maskKey(key) {
|
|
|
184
191
|
export function resolveConfig() {
|
|
185
192
|
const config = loadConfig();
|
|
186
193
|
const provider = process.env.ARCHBYTE_PROVIDER ?? config.provider;
|
|
187
|
-
// Reject unknown providers
|
|
194
|
+
// Reject unknown providers with a helpful message
|
|
188
195
|
if (provider && !VALID_PROVIDERS.includes(provider)) {
|
|
196
|
+
if (process.env.ARCHBYTE_PROVIDER) {
|
|
197
|
+
console.error(chalk.red(`Invalid ARCHBYTE_PROVIDER="${provider}". Must be: ${VALID_PROVIDERS.join(", ")}`));
|
|
198
|
+
}
|
|
189
199
|
return null;
|
|
190
200
|
}
|
|
191
201
|
// Resolve API key + model from profiles first, then legacy flat keys
|
package/dist/cli/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Shared constants for the ArchByte CLI.
|
|
2
2
|
// Single source of truth for URLs, ports, paths, and timeouts.
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { resolveHome } from "./utils.js";
|
|
4
5
|
// ─── API ───
|
|
5
6
|
export const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
|
|
6
7
|
export const SITE_URL = "https://archbyte.heartbyte.io";
|
|
@@ -8,7 +9,9 @@ export const SITE_URL = "https://archbyte.heartbyte.io";
|
|
|
8
9
|
export const DEFAULT_PORT = 3847;
|
|
9
10
|
export const CLI_CALLBACK_PORT = 19274;
|
|
10
11
|
// ─── Paths ───
|
|
11
|
-
|
|
12
|
+
// resolveHome() throws if HOME/USERPROFILE is unset (e.g. in bare containers),
|
|
13
|
+
// giving a clear error instead of silently writing to "./.archbyte".
|
|
14
|
+
export const CONFIG_DIR = path.join(resolveHome(), ".archbyte");
|
|
12
15
|
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
16
|
export const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
14
17
|
/** Project-local .archbyte directory name */
|
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 = [
|
|
@@ -49,13 +49,23 @@ function saveConfig(config) {
|
|
|
49
49
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
50
50
|
}
|
|
51
51
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
52
|
+
// Restrict permissions — config contains API keys in profiles
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Windows doesn't support chmod
|
|
58
|
+
}
|
|
52
59
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
60
|
+
/**
|
|
61
|
+
* Read a line from stdin with character masking (for API keys).
|
|
62
|
+
* Non-TTY fallback: reads from stdin as a line (for piped input / CI).
|
|
63
|
+
*/
|
|
58
64
|
function askHidden(prompt) {
|
|
65
|
+
if (!isTTY()) {
|
|
66
|
+
process.stdout.write(prompt);
|
|
67
|
+
return readLineFromStdin();
|
|
68
|
+
}
|
|
59
69
|
return new Promise((resolve) => {
|
|
60
70
|
process.stdout.write(prompt);
|
|
61
71
|
const stdin = process.stdin;
|
|
@@ -93,7 +103,15 @@ function askHidden(prompt) {
|
|
|
93
103
|
stdin.on("data", onData);
|
|
94
104
|
});
|
|
95
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Read a line of visible text from stdin.
|
|
108
|
+
* Non-TTY fallback: reads from stdin as a line (for piped input / CI).
|
|
109
|
+
*/
|
|
96
110
|
function askText(prompt) {
|
|
111
|
+
if (!isTTY()) {
|
|
112
|
+
process.stdout.write(prompt);
|
|
113
|
+
return readLineFromStdin();
|
|
114
|
+
}
|
|
97
115
|
return new Promise((resolve) => {
|
|
98
116
|
process.stdout.write(prompt);
|
|
99
117
|
const stdin = process.stdin;
|
|
@@ -131,6 +149,32 @@ function askText(prompt) {
|
|
|
131
149
|
stdin.on("data", onData);
|
|
132
150
|
});
|
|
133
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Non-TTY line reader. Reads a single line from stdin (for piped input).
|
|
154
|
+
*/
|
|
155
|
+
function readLineFromStdin() {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
const stdin = process.stdin;
|
|
158
|
+
stdin.resume();
|
|
159
|
+
stdin.setEncoding("utf8");
|
|
160
|
+
let buf = "";
|
|
161
|
+
const onData = (data) => {
|
|
162
|
+
buf += data;
|
|
163
|
+
const nl = buf.indexOf("\n");
|
|
164
|
+
if (nl !== -1) {
|
|
165
|
+
stdin.removeListener("data", onData);
|
|
166
|
+
stdin.pause();
|
|
167
|
+
resolve(buf.slice(0, nl).trim());
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
const onEnd = () => {
|
|
171
|
+
stdin.removeListener("data", onData);
|
|
172
|
+
resolve(buf.trim());
|
|
173
|
+
};
|
|
174
|
+
stdin.on("data", onData);
|
|
175
|
+
stdin.once("end", onEnd);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
134
178
|
async function validateProviderSilent(providerName, apiKey, model) {
|
|
135
179
|
try {
|
|
136
180
|
const provider = createProvider({ provider: providerName, apiKey });
|
|
@@ -157,22 +201,13 @@ async function validateProviderSilent(providerName, apiKey, model) {
|
|
|
157
201
|
function getProfiles(config) {
|
|
158
202
|
return config.profiles ?? {};
|
|
159
203
|
}
|
|
160
|
-
function isInPath(cmd) {
|
|
161
|
-
try {
|
|
162
|
-
execSync(`which ${cmd}`, { stdio: "ignore" });
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
return false;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
204
|
export async function handleSetup() {
|
|
170
205
|
console.log();
|
|
171
206
|
console.log(chalk.bold.cyan("ArchByte Setup"));
|
|
172
207
|
console.log(chalk.gray("Configure your model provider and API key.\n"));
|
|
173
208
|
// Detect AI coding tools — suggest MCP instead of BYOK
|
|
174
209
|
const hasClaude = isInPath("claude");
|
|
175
|
-
const codexDir = path.join(
|
|
210
|
+
const codexDir = path.join(CONFIG_DIR, "../.codex");
|
|
176
211
|
const hasCodex = fs.existsSync(codexDir);
|
|
177
212
|
if (hasClaude || hasCodex) {
|
|
178
213
|
const tools = [hasClaude && "Claude Code", hasCodex && "Codex CLI"].filter(Boolean).join(" and ");
|
|
@@ -258,7 +293,7 @@ export async function handleSetup() {
|
|
|
258
293
|
if (!profiles[provider])
|
|
259
294
|
profiles[provider] = { apiKey: "" };
|
|
260
295
|
profiles[provider].apiKey = apiKey;
|
|
261
|
-
// Step 2b: Email for this provider account (optional)
|
|
296
|
+
// Step 2b: Email for this provider account (optional, with validation)
|
|
262
297
|
const existingEmail = profiles[provider].email;
|
|
263
298
|
if (existingEmail) {
|
|
264
299
|
console.log(chalk.gray(`\n Account email: ${existingEmail}`));
|
|
@@ -270,16 +305,26 @@ export async function handleSetup() {
|
|
|
270
305
|
if (emailIdx === 1) {
|
|
271
306
|
const newEmail = await askText(chalk.bold(" Email: "));
|
|
272
307
|
if (newEmail) {
|
|
273
|
-
|
|
274
|
-
|
|
308
|
+
if (!isValidEmail(newEmail)) {
|
|
309
|
+
console.log(chalk.yellow(` "${newEmail}" doesn't look like a valid email. Skipping.`));
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
profiles[provider].email = newEmail;
|
|
313
|
+
console.log(chalk.green(` ✓ Email: ${newEmail}`));
|
|
314
|
+
}
|
|
275
315
|
}
|
|
276
316
|
}
|
|
277
317
|
}
|
|
278
318
|
else {
|
|
279
319
|
const email = await askText(chalk.bold(` ${selected.label} account email ${chalk.gray("(optional, Enter to skip)")}: `));
|
|
280
320
|
if (email) {
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
if (!isValidEmail(email)) {
|
|
322
|
+
console.log(chalk.yellow(` "${email}" doesn't look like a valid email. Skipping.`));
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
profiles[provider].email = email;
|
|
326
|
+
console.log(chalk.green(` ✓ Email: ${email}`));
|
|
327
|
+
}
|
|
283
328
|
}
|
|
284
329
|
}
|
|
285
330
|
// Step 3: Model selection
|
|
@@ -323,11 +368,14 @@ export async function handleSetup() {
|
|
|
323
368
|
if (result === false) {
|
|
324
369
|
let retries = 0;
|
|
325
370
|
while (result === false && retries < 2) {
|
|
326
|
-
if (!await confirm(" Retry?"))
|
|
371
|
+
if (!await confirm(" Retry with a different key?"))
|
|
327
372
|
break;
|
|
328
373
|
retries++;
|
|
329
374
|
const newKey = await askHidden(chalk.bold(" API key: "));
|
|
330
|
-
if (newKey) {
|
|
375
|
+
if (!newKey) {
|
|
376
|
+
console.log(chalk.yellow(" No key entered. Retrying with existing key."));
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
331
379
|
profiles[provider].apiKey = newKey;
|
|
332
380
|
console.log(chalk.green(` ✓ API key: ${maskKey(newKey)}`));
|
|
333
381
|
}
|
|
@@ -433,10 +481,16 @@ export async function handleSetup() {
|
|
|
433
481
|
catch { /* ignore */ }
|
|
434
482
|
}
|
|
435
483
|
const templatePath = path.resolve(__dirname, "../../templates/archbyte.yaml");
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
484
|
+
if (!fs.existsSync(templatePath)) {
|
|
485
|
+
console.log(chalk.yellow(" Could not find archbyte.yaml template. Skipping."));
|
|
486
|
+
console.log(chalk.gray(` Expected at: ${templatePath}`));
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
let template = fs.readFileSync(templatePath, "utf-8");
|
|
490
|
+
template = template.replace("name: my-project", `name: ${projectName}`);
|
|
491
|
+
fs.writeFileSync(yamlPath, template, "utf-8");
|
|
492
|
+
yamlCreated = true;
|
|
493
|
+
}
|
|
440
494
|
}
|
|
441
495
|
// Generate README.md in .archbyte/
|
|
442
496
|
writeArchbyteReadme(archbyteDir);
|
|
@@ -465,15 +519,19 @@ export async function handleSetup() {
|
|
|
465
519
|
console.log();
|
|
466
520
|
console.log(sep);
|
|
467
521
|
console.log();
|
|
468
|
-
console.log(" " + chalk.bold("Next"));
|
|
522
|
+
console.log(" " + chalk.bold("Next steps"));
|
|
523
|
+
console.log();
|
|
469
524
|
console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
|
|
470
525
|
if (hasClaude || hasCodex) {
|
|
471
526
|
console.log(" " + chalk.cyan("archbyte mcp install") + " Use from your AI tool");
|
|
472
527
|
}
|
|
528
|
+
console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
|
|
529
|
+
console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
|
|
473
530
|
if (result === false) {
|
|
474
|
-
console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
|
|
475
531
|
console.log();
|
|
532
|
+
console.log(chalk.yellow(" Warning: credentials unverified. Saving anyway."));
|
|
476
533
|
}
|
|
534
|
+
console.log();
|
|
477
535
|
}
|
|
478
536
|
function writeArchbyteReadme(archbyteDir) {
|
|
479
537
|
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,9 @@ 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>;
|
|
28
33
|
export {};
|