@archal/cli 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,561 @@
1
+ // src/commands/login.ts
2
+ import { Command } from "commander";
3
+ import { exec } from "child_process";
4
+ import { randomBytes } from "crypto";
5
+ import { createServer } from "http";
6
+
7
+ // src/auth.ts
8
+ import { chmodSync, existsSync as existsSync2, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
9
+ import { join as join2 } from "path";
10
+
11
+ // src/config/config.ts
12
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
13
+ import { join } from "path";
14
+ import { homedir } from "os";
15
+ import { z } from "zod";
16
+
17
+ // src/utils/logger.ts
18
+ var LOG_LEVEL_PRIORITY = {
19
+ debug: 0,
20
+ info: 1,
21
+ warn: 2,
22
+ error: 3
23
+ };
24
+ var LOG_LEVEL_COLORS = {
25
+ debug: "\x1B[90m",
26
+ // gray
27
+ info: "\x1B[36m",
28
+ // cyan
29
+ warn: "\x1B[33m",
30
+ // yellow
31
+ error: "\x1B[31m"
32
+ // red
33
+ };
34
+ var RESET = "\x1B[0m";
35
+ var BOLD = "\x1B[1m";
36
+ var DIM = "\x1B[2m";
37
+ var globalOptions = {
38
+ level: "warn",
39
+ quiet: false,
40
+ json: false,
41
+ verbose: false
42
+ };
43
+ function configureLogger(options) {
44
+ globalOptions = { ...globalOptions, ...options };
45
+ }
46
+ function shouldLog(level) {
47
+ if (globalOptions.quiet && level !== "error") {
48
+ return false;
49
+ }
50
+ return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[globalOptions.level];
51
+ }
52
+ function formatTimestamp() {
53
+ return (/* @__PURE__ */ new Date()).toISOString();
54
+ }
55
+ function formatLogEntry(entry) {
56
+ if (globalOptions.json) {
57
+ return JSON.stringify(entry);
58
+ }
59
+ const color = LOG_LEVEL_COLORS[entry.level];
60
+ const levelTag = `${color}${BOLD}${entry.level.toUpperCase().padEnd(5)}${RESET}`;
61
+ const timestamp = `${DIM}${entry.timestamp}${RESET}`;
62
+ let line = `${timestamp} ${levelTag} ${entry.message}`;
63
+ if (entry.data && Object.keys(entry.data).length > 0) {
64
+ const dataStr = Object.entries(entry.data).map(([k, v]) => `${DIM}${k}=${RESET}${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
65
+ line += ` ${dataStr}`;
66
+ }
67
+ return line;
68
+ }
69
+ function log(level, message, data) {
70
+ if (!shouldLog(level)) {
71
+ return;
72
+ }
73
+ const entry = {
74
+ level,
75
+ message,
76
+ timestamp: formatTimestamp(),
77
+ data
78
+ };
79
+ const formatted = formatLogEntry(entry);
80
+ process.stderr.write(formatted + "\n");
81
+ }
82
+ function debug(message, data) {
83
+ log("debug", message, data);
84
+ }
85
+ function info(message, data) {
86
+ log("info", message, data);
87
+ }
88
+ function warn(message, data) {
89
+ log("warn", message, data);
90
+ }
91
+ function error(message, data) {
92
+ log("error", message, data);
93
+ }
94
+ function success(message) {
95
+ if (globalOptions.quiet) return;
96
+ process.stderr.write(`\x1B[32m${BOLD} OK${RESET} ${message}
97
+ `);
98
+ }
99
+ function fail(message) {
100
+ if (globalOptions.quiet) return;
101
+ process.stderr.write(`\x1B[31m${BOLD}FAIL${RESET} ${message}
102
+ `);
103
+ }
104
+ function progress(message) {
105
+ if (!globalOptions.verbose) return;
106
+ process.stderr.write(`${DIM} ...${RESET} ${message}
107
+ `);
108
+ }
109
+ function banner(text) {
110
+ if (!globalOptions.verbose) return;
111
+ const line = "=".repeat(Math.max(text.length + 4, 40));
112
+ process.stderr.write(`
113
+ \x1B[36m${BOLD}${line}${RESET}
114
+ `);
115
+ process.stderr.write(`\x1B[36m${BOLD} ${text}${RESET}
116
+ `);
117
+ process.stderr.write(`\x1B[36m${BOLD}${line}${RESET}
118
+
119
+ `);
120
+ }
121
+ function table(headers, rows) {
122
+ if (globalOptions.quiet) return;
123
+ const colWidths = headers.map((h, i) => {
124
+ const maxDataWidth = rows.reduce((max, row) => {
125
+ const cell = row[i] ?? "";
126
+ return Math.max(max, cell.length);
127
+ }, 0);
128
+ return Math.max(h.length, maxDataWidth);
129
+ });
130
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i] ?? 0)).join(" ");
131
+ const separator = colWidths.map((w) => "-".repeat(w)).join(" ");
132
+ process.stderr.write(`${BOLD}${headerLine}${RESET}
133
+ `);
134
+ process.stderr.write(`${DIM}${separator}${RESET}
135
+ `);
136
+ for (const row of rows) {
137
+ const line = row.map((cell, i) => cell.padEnd(colWidths[i] ?? 0)).join(" ");
138
+ process.stderr.write(`${line}
139
+ `);
140
+ }
141
+ }
142
+
143
+ // src/config/config.ts
144
+ var ARCHAL_DIR_NAME = ".archal";
145
+ var CONFIG_FILE_NAME = "config.json";
146
+ var evaluatorConfigSchema = z.object({
147
+ model: z.string().default("claude-sonnet-4-20250514"),
148
+ apiKey: z.string().default("env:ANTHROPIC_API_KEY")
149
+ });
150
+ var defaultsConfigSchema = z.object({
151
+ runs: z.number().int().positive().default(5),
152
+ timeout: z.number().int().positive().default(120)
153
+ });
154
+ var configFileSchema = z.object({
155
+ telemetry: z.boolean().default(false),
156
+ evaluator: evaluatorConfigSchema.default({}),
157
+ defaults: defaultsConfigSchema.default({})
158
+ });
159
+ function getArchalDir() {
160
+ return join(homedir(), ARCHAL_DIR_NAME);
161
+ }
162
+ function getConfigPath() {
163
+ return join(getArchalDir(), CONFIG_FILE_NAME);
164
+ }
165
+ function ensureArchalDir() {
166
+ const dir = getArchalDir();
167
+ if (!existsSync(dir)) {
168
+ mkdirSync(dir, { recursive: true });
169
+ debug("Created archal directory", { path: dir });
170
+ }
171
+ return dir;
172
+ }
173
+ function loadConfigFile() {
174
+ const configPath = getConfigPath();
175
+ if (!existsSync(configPath)) {
176
+ debug("No config file found, using defaults", { path: configPath });
177
+ return configFileSchema.parse({});
178
+ }
179
+ try {
180
+ const raw = readFileSync(configPath, "utf-8");
181
+ const parsed = JSON.parse(raw);
182
+ const config = configFileSchema.parse(parsed);
183
+ debug("Loaded config file", { path: configPath });
184
+ return config;
185
+ } catch (err) {
186
+ const message = err instanceof Error ? err.message : String(err);
187
+ warn(`Failed to parse config file at ${configPath}: ${message}`);
188
+ return configFileSchema.parse({});
189
+ }
190
+ }
191
+ function resolveApiKey(apiKeyConfig) {
192
+ if (apiKeyConfig.startsWith("env:")) {
193
+ const envVar = apiKeyConfig.slice(4);
194
+ return process.env[envVar] ?? "";
195
+ }
196
+ return apiKeyConfig;
197
+ }
198
+ function loadConfig() {
199
+ const file = loadConfigFile();
200
+ const envTelemetry = process.env["ARCHAL_TELEMETRY"];
201
+ const envModel = process.env["ARCHAL_MODEL"];
202
+ const envRuns = process.env["ARCHAL_RUNS"];
203
+ const envTimeout = process.env["ARCHAL_TIMEOUT"];
204
+ const envApiKey = process.env["ANTHROPIC_API_KEY"];
205
+ const telemetry = envTelemetry !== void 0 ? envTelemetry === "true" : file.telemetry;
206
+ const model = envModel ?? file.evaluator.model;
207
+ const runs = envRuns !== void 0 ? parseInt(envRuns, 10) : file.defaults.runs;
208
+ const timeout = envTimeout !== void 0 ? parseInt(envTimeout, 10) : file.defaults.timeout;
209
+ const apiKey = envApiKey ?? resolveApiKey(file.evaluator.apiKey);
210
+ return {
211
+ telemetry,
212
+ apiKey,
213
+ model,
214
+ runs: Number.isNaN(runs) ? 5 : runs,
215
+ timeout: Number.isNaN(timeout) ? 120 : timeout,
216
+ archalDir: getArchalDir(),
217
+ configPath: getConfigPath()
218
+ };
219
+ }
220
+ function saveConfig(config) {
221
+ const dir = ensureArchalDir();
222
+ const configPath = join(dir, CONFIG_FILE_NAME);
223
+ let existing;
224
+ if (existsSync(configPath)) {
225
+ try {
226
+ const raw = readFileSync(configPath, "utf-8");
227
+ existing = configFileSchema.parse(JSON.parse(raw));
228
+ } catch {
229
+ existing = configFileSchema.parse({});
230
+ }
231
+ } else {
232
+ existing = configFileSchema.parse({});
233
+ }
234
+ const merged = {
235
+ telemetry: config.telemetry ?? existing.telemetry,
236
+ evaluator: {
237
+ ...existing.evaluator,
238
+ ...config.evaluator
239
+ },
240
+ defaults: {
241
+ ...existing.defaults,
242
+ ...config.defaults
243
+ }
244
+ };
245
+ writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
246
+ debug("Saved config file", { path: configPath });
247
+ }
248
+ function initConfig() {
249
+ const configPath = getConfigPath();
250
+ if (existsSync(configPath)) {
251
+ warn(`Config file already exists at ${configPath}`);
252
+ return configPath;
253
+ }
254
+ const defaultConfig = configFileSchema.parse({});
255
+ ensureArchalDir();
256
+ writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + "\n", "utf-8");
257
+ return configPath;
258
+ }
259
+ function setConfigValue(key, value) {
260
+ const file = loadConfigFile();
261
+ const parts = key.split(".");
262
+ if (parts.length === 1) {
263
+ const topKey = parts[0];
264
+ if (topKey === "telemetry") {
265
+ saveConfig({ ...file, telemetry: value === "true" });
266
+ return;
267
+ }
268
+ }
269
+ if (parts.length === 2) {
270
+ const [section, prop] = parts;
271
+ if (section === "evaluator" && (prop === "model" || prop === "apiKey")) {
272
+ saveConfig({
273
+ ...file,
274
+ evaluator: { ...file.evaluator, [prop]: value }
275
+ });
276
+ return;
277
+ }
278
+ if (section === "defaults" && (prop === "runs" || prop === "timeout")) {
279
+ const numValue = parseInt(value, 10);
280
+ if (Number.isNaN(numValue) || numValue <= 0) {
281
+ throw new Error(`Invalid numeric value for ${key}: ${value}`);
282
+ }
283
+ saveConfig({
284
+ ...file,
285
+ defaults: { ...file.defaults, [prop]: numValue }
286
+ });
287
+ return;
288
+ }
289
+ }
290
+ throw new Error(
291
+ `Unknown config key: "${key}". Valid keys: telemetry, evaluator.model, evaluator.apiKey, defaults.runs, defaults.timeout`
292
+ );
293
+ }
294
+ function getConfigDisplay() {
295
+ const resolved = loadConfig();
296
+ return {
297
+ telemetry: resolved.telemetry,
298
+ evaluator: {
299
+ model: resolved.model,
300
+ apiKey: resolved.apiKey ? "***" + resolved.apiKey.slice(-4) : "(not set)"
301
+ },
302
+ defaults: {
303
+ runs: resolved.runs,
304
+ timeout: resolved.timeout
305
+ },
306
+ paths: {
307
+ archalDir: resolved.archalDir,
308
+ configFile: resolved.configPath
309
+ }
310
+ };
311
+ }
312
+
313
+ // src/constants.ts
314
+ var BUNDLED_TWINS = ["github", "slack", "stripe"];
315
+ var SELECTABLE_TWINS = [
316
+ "linear",
317
+ "jira",
318
+ "supabase",
319
+ "browser",
320
+ "google-workspace"
321
+ ];
322
+ var ALL_TWINS = [...BUNDLED_TWINS, ...SELECTABLE_TWINS];
323
+ var FREE_TIER_MAX_SELECTIONS = 3;
324
+ var TWIN_TOOL_COUNTS = {
325
+ github: 18,
326
+ slack: 10,
327
+ stripe: 16,
328
+ linear: 12,
329
+ jira: 14,
330
+ supabase: 12,
331
+ browser: 8,
332
+ "google-workspace": 15
333
+ };
334
+
335
+ // src/auth.ts
336
+ var CREDENTIALS_FILE = "credentials.json";
337
+ function getCredentialsPath() {
338
+ return join2(ensureArchalDir(), CREDENTIALS_FILE);
339
+ }
340
+ function readCredentialsFile() {
341
+ const path = getCredentialsPath();
342
+ if (!existsSync2(path)) {
343
+ return null;
344
+ }
345
+ try {
346
+ const raw = readFileSync2(path, "utf-8");
347
+ const parsed = JSON.parse(raw);
348
+ if (typeof parsed.token !== "string" || typeof parsed.email !== "string" || typeof parsed.plan !== "string" || !Array.isArray(parsed.selectedTwins) || typeof parsed.expiresAt !== "number") {
349
+ return null;
350
+ }
351
+ return {
352
+ token: parsed.token,
353
+ email: parsed.email,
354
+ plan: parsed.plan,
355
+ selectedTwins: parsed.selectedTwins,
356
+ expiresAt: parsed.expiresAt
357
+ };
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+ function getCredentials() {
363
+ const creds = readCredentialsFile();
364
+ if (!creds) {
365
+ return null;
366
+ }
367
+ const nowSeconds = Math.floor(Date.now() / 1e3);
368
+ if (creds.expiresAt <= nowSeconds) {
369
+ return null;
370
+ }
371
+ return creds;
372
+ }
373
+ function saveCredentials(creds) {
374
+ const path = getCredentialsPath();
375
+ writeFileSync2(path, JSON.stringify(creds, null, 2) + "\n", "utf-8");
376
+ try {
377
+ chmodSync(path, 384);
378
+ } catch {
379
+ }
380
+ }
381
+ function deleteCredentials() {
382
+ const path = getCredentialsPath();
383
+ if (!existsSync2(path)) {
384
+ return false;
385
+ }
386
+ unlinkSync(path);
387
+ return true;
388
+ }
389
+ function requireAuth() {
390
+ const creds = getCredentials();
391
+ if (!creds) {
392
+ process.stderr.write("Not logged in. Run: archal login\n");
393
+ process.exit(1);
394
+ }
395
+ return creds;
396
+ }
397
+ async function refreshAuthFromServer(creds) {
398
+ try {
399
+ const { fetchAuthMe } = await import("./api-client-ZUMDL3TP.js");
400
+ const result = await fetchAuthMe(creds.token);
401
+ if (!result.ok) {
402
+ return creds;
403
+ }
404
+ const updated = {
405
+ ...creds,
406
+ email: result.data.email,
407
+ plan: result.data.plan,
408
+ selectedTwins: result.data.selectedTwins
409
+ };
410
+ if (updated.email !== creds.email || updated.plan !== creds.plan || JSON.stringify(updated.selectedTwins) !== JSON.stringify(creds.selectedTwins)) {
411
+ saveCredentials(updated);
412
+ }
413
+ return updated;
414
+ } catch {
415
+ return creds;
416
+ }
417
+ }
418
+ function isEntitled(creds, twinName) {
419
+ if (creds.plan === "pro" || creds.plan === "enterprise") {
420
+ return true;
421
+ }
422
+ if (BUNDLED_TWINS.includes(twinName)) {
423
+ return true;
424
+ }
425
+ return creds.selectedTwins.includes(twinName);
426
+ }
427
+ function getJwtExpiry(token) {
428
+ try {
429
+ const payloadPart = token.split(".")[1];
430
+ if (!payloadPart) return null;
431
+ const normalized = payloadPart.replace(/-/g, "+").replace(/_/g, "/");
432
+ const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
433
+ const decoded = JSON.parse(Buffer.from(padded, "base64").toString("utf-8"));
434
+ return typeof decoded.exp === "number" ? decoded.exp : null;
435
+ } catch {
436
+ return null;
437
+ }
438
+ }
439
+
440
+ // src/commands/login.ts
441
+ var AUTH_BASE_URL = process.env["ARCHAL_AUTH_URL"] ?? "https://archal.ai";
442
+ var START_PORT = 51423;
443
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
444
+ function openBrowser(url) {
445
+ const platform = process.platform;
446
+ const command = platform === "darwin" ? `open "${url}"` : platform === "win32" ? `start "" "${url}"` : `xdg-open "${url}"`;
447
+ exec(command, () => {
448
+ });
449
+ }
450
+ function findFreePort(startPort) {
451
+ return new Promise((resolve, reject) => {
452
+ const server = createServer();
453
+ server.listen(startPort, "127.0.0.1", () => {
454
+ const address = server.address();
455
+ const port = typeof address === "object" && address ? address.port : startPort;
456
+ server.close(() => resolve(port));
457
+ });
458
+ server.on("error", () => {
459
+ if (startPort < START_PORT + 100) {
460
+ findFreePort(startPort + 1).then(resolve).catch(reject);
461
+ } else {
462
+ reject(new Error("Could not find a free localhost callback port"));
463
+ }
464
+ });
465
+ });
466
+ }
467
+ function createLoginCommand() {
468
+ return new Command("login").description("Log in via archal.ai browser auth").action(async () => {
469
+ const port = await findFreePort(START_PORT);
470
+ const state = randomBytes(16).toString("hex");
471
+ const redirectUrl = `http://localhost:${port}/callback`;
472
+ const authUrl = `${AUTH_BASE_URL}/cli-auth?redirect=${encodeURIComponent(redirectUrl)}&state=${encodeURIComponent(state)}`;
473
+ info("Opening browser for authentication...");
474
+ info(`If your browser does not open, visit:
475
+ ${authUrl}`);
476
+ openBrowser(authUrl);
477
+ await new Promise((resolve, reject) => {
478
+ const server = createServer((req, res) => {
479
+ const requestUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
480
+ if (requestUrl.pathname !== "/callback") {
481
+ res.writeHead(404);
482
+ res.end("Not found");
483
+ return;
484
+ }
485
+ const returnedState = requestUrl.searchParams.get("state");
486
+ if (returnedState !== state) {
487
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
488
+ res.end("<h1>Login failed</h1><p>State mismatch.</p>");
489
+ server.close();
490
+ reject(new Error("State mismatch in callback"));
491
+ return;
492
+ }
493
+ const token = requestUrl.searchParams.get("token");
494
+ const email = requestUrl.searchParams.get("email");
495
+ const plan = requestUrl.searchParams.get("plan");
496
+ const twins = requestUrl.searchParams.get("twins");
497
+ if (!token || !email || !plan) {
498
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
499
+ res.end("<h1>Login failed</h1><p>Missing callback parameters.</p>");
500
+ server.close();
501
+ reject(new Error("Missing token/email/plan in callback"));
502
+ return;
503
+ }
504
+ const expiresAt = getJwtExpiry(token) ?? Math.floor(Date.now() / 1e3) + 30 * 24 * 60 * 60;
505
+ const credentials = {
506
+ token,
507
+ email,
508
+ plan,
509
+ selectedTwins: twins ? twins.split(",").filter(Boolean) : [],
510
+ expiresAt
511
+ };
512
+ saveCredentials(credentials);
513
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
514
+ res.end("<h1>Login successful</h1><p>You can close this tab.</p>");
515
+ success(`Logged in as ${email} (${plan})`);
516
+ server.close(() => resolve());
517
+ });
518
+ server.listen(port, "127.0.0.1");
519
+ const timeout = setTimeout(() => {
520
+ server.close();
521
+ reject(new Error("Login timed out. Run archal login again."));
522
+ }, LOGIN_TIMEOUT_MS);
523
+ server.on("close", () => clearTimeout(timeout));
524
+ }).catch((error2) => {
525
+ const message = error2 instanceof Error ? error2.message : String(error2);
526
+ error(message);
527
+ process.exit(1);
528
+ });
529
+ });
530
+ }
531
+
532
+ export {
533
+ configureLogger,
534
+ debug,
535
+ info,
536
+ warn,
537
+ error,
538
+ success,
539
+ fail,
540
+ progress,
541
+ banner,
542
+ table,
543
+ getArchalDir,
544
+ getConfigPath,
545
+ ensureArchalDir,
546
+ loadConfig,
547
+ initConfig,
548
+ setConfigValue,
549
+ getConfigDisplay,
550
+ BUNDLED_TWINS,
551
+ ALL_TWINS,
552
+ FREE_TIER_MAX_SELECTIONS,
553
+ TWIN_TOOL_COUNTS,
554
+ getCredentials,
555
+ saveCredentials,
556
+ deleteCredentials,
557
+ requireAuth,
558
+ refreshAuthFromServer,
559
+ isEntitled,
560
+ createLoginCommand
561
+ };