@an-sdk/cli 0.0.9 → 0.0.11

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/dist/index.js CHANGED
@@ -9,7 +9,6 @@ import { join } from "path";
9
9
  import { homedir } from "os";
10
10
  var AN_DIR = join(homedir(), ".an");
11
11
  var CREDENTIALS_PATH = join(AN_DIR, "credentials");
12
- var PROJECT_PATH = join(process.cwd(), ".an", "project.json");
13
12
  function getApiKey() {
14
13
  if (process.env.AN_API_KEY) return process.env.AN_API_KEY;
15
14
  try {
@@ -23,24 +22,45 @@ function saveApiKey(apiKey) {
23
22
  mkdirSync(AN_DIR, { recursive: true });
24
23
  writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2));
25
24
  }
26
- function getProject() {
27
- try {
28
- const data = JSON.parse(readFileSync(PROJECT_PATH, "utf-8"));
29
- if (data.projectId && data.projectSlug) return data;
30
- if (data.slug) return { projectId: data.agentId ?? "", projectSlug: data.slug };
31
- return null;
32
- } catch {
33
- return null;
34
- }
25
+
26
+ // src/detect.ts
27
+ function isAgent() {
28
+ return !!(process.env.CLAUDE_CODE || process.env.CLAUDECODE || process.env.CURSOR_TRACE_ID || process.env.CURSOR_AGENT || process.env.CODEX_SANDBOX || process.env.GEMINI_CLI || process.env.AI_AGENT);
35
29
  }
36
- function saveProject(data) {
37
- mkdirSync(join(process.cwd(), ".an"), { recursive: true });
38
- writeFileSync(PROJECT_PATH, JSON.stringify(data, null, 2));
30
+ function isInteractive() {
31
+ return !!process.stdin.isTTY && !isAgent();
39
32
  }
40
33
 
41
34
  // src/login.ts
42
35
  var API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1";
43
- async function login() {
36
+ async function verifyKey(apiKey) {
37
+ const res = await fetch(`${API_BASE}/me`, {
38
+ headers: { Authorization: `Bearer ${apiKey.trim()}` }
39
+ });
40
+ if (!res.ok) {
41
+ throw new Error("Invalid API key");
42
+ }
43
+ return res.json();
44
+ }
45
+ async function login(opts) {
46
+ const key = opts?.apiKey;
47
+ if (key) {
48
+ try {
49
+ const { user, team } = await verifyKey(key);
50
+ saveApiKey(key.trim());
51
+ console.log(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`);
52
+ console.log("Key saved to ~/.an/credentials");
53
+ } catch {
54
+ console.error("Error: Invalid API key. Get a new one at https://an.dev/api-keys");
55
+ process.exit(1);
56
+ }
57
+ return;
58
+ }
59
+ if (!isInteractive()) {
60
+ console.error("Error: No API key provided. Use --api-key KEY or set AN_API_KEY env var.");
61
+ console.error("Get your API key at https://an.dev/api-keys");
62
+ process.exit(1);
63
+ }
44
64
  p.intro("an login");
45
65
  const existing = getApiKey();
46
66
  if (existing) {
@@ -59,39 +79,37 @@ async function login() {
59
79
  }
60
80
  const s = p.spinner();
61
81
  s.start("Verifying API key...");
62
- const res = await fetch(`${API_BASE}/me`, {
63
- headers: { Authorization: `Bearer ${apiKey.trim()}` }
64
- });
65
- if (!res.ok) {
82
+ try {
83
+ const { user, team } = await verifyKey(apiKey);
84
+ saveApiKey(apiKey.trim());
85
+ s.stop("Verified");
86
+ p.log.success(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`);
87
+ p.log.info("Key saved to ~/.an/credentials");
88
+ p.outro("Done");
89
+ } catch {
66
90
  s.stop("Invalid API key");
67
91
  p.log.error("Invalid API key. Get a new one at https://an.dev/api-keys");
68
92
  process.exit(1);
69
93
  }
70
- const { user, team } = await res.json();
71
- saveApiKey(apiKey.trim());
72
- s.stop("Verified");
73
- p.log.success(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`);
74
- p.log.info("Key saved to ~/.an/credentials");
75
- p.outro("Done");
76
94
  }
77
95
 
78
96
  // src/bundler.ts
79
97
  import esbuild from "esbuild";
80
98
  async function findAgentEntryPoints() {
81
- const { existsSync, readdirSync, statSync } = await import("fs");
82
- const { join: join2, basename: basename2, extname } = await import("path");
83
- if (!existsSync("agents") || !statSync("agents").isDirectory()) {
99
+ const { existsSync: existsSync2, readdirSync, statSync } = await import("fs");
100
+ const { join: join3, basename, extname } = await import("path");
101
+ if (!existsSync2("agents") || !statSync("agents").isDirectory()) {
84
102
  throw new Error("No agents/ directory found. See https://an.dev/docs to get started.");
85
103
  }
86
104
  const entries = [];
87
105
  const items = readdirSync("agents");
88
106
  for (const item of items) {
89
- const fullPath = join2("agents", item);
107
+ const fullPath = join3("agents", item);
90
108
  const stat = statSync(fullPath);
91
109
  if (stat.isDirectory()) {
92
110
  for (const indexFile of ["index.ts", "index.js"]) {
93
- const indexPath = join2(fullPath, indexFile);
94
- if (existsSync(indexPath)) {
111
+ const indexPath = join3(fullPath, indexFile);
112
+ if (existsSync2(indexPath)) {
95
113
  entries.push({ slug: item, entryPoint: indexPath });
96
114
  break;
97
115
  }
@@ -99,7 +117,7 @@ async function findAgentEntryPoints() {
99
117
  } else if (stat.isFile()) {
100
118
  const ext = extname(item);
101
119
  if (ext === ".ts" || ext === ".js") {
102
- entries.push({ slug: basename2(item, ext), entryPoint: fullPath });
120
+ entries.push({ slug: basename(item, ext), entryPoint: fullPath });
103
121
  }
104
122
  }
105
123
  }
@@ -128,64 +146,86 @@ ${result.errors.map((e) => e.text).join("\n")}`
128
146
  }
129
147
  return Buffer.from(result.outputFiles[0].contents);
130
148
  }
131
-
132
- // src/deploy.ts
133
- import { basename } from "path";
134
- import * as p2 from "@clack/prompts";
135
- function slugify(name) {
136
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
149
+ async function importBundle(bundle) {
150
+ const { writeFileSync: writeFileSync2, unlinkSync, mkdirSync: mkdirSync2 } = await import("fs");
151
+ const { join: join3 } = await import("path");
152
+ const tmpDir = join3(process.cwd(), ".an-tmp");
153
+ mkdirSync2(tmpDir, { recursive: true });
154
+ const tmpPath = join3(tmpDir, `an-bundle-${Date.now()}.mjs`);
155
+ try {
156
+ writeFileSync2(tmpPath, bundle);
157
+ const mod = await import(tmpPath);
158
+ const config = mod.default;
159
+ if (config?._type === "agent") return config;
160
+ return null;
161
+ } catch {
162
+ return null;
163
+ } finally {
164
+ try {
165
+ unlinkSync(tmpPath);
166
+ } catch {
167
+ }
168
+ try {
169
+ const { rmdirSync } = await import("fs");
170
+ rmdirSync(tmpDir);
171
+ } catch {
172
+ }
173
+ }
137
174
  }
138
- var API_BASE2 = process.env.AN_API_URL || "https://an.dev/api/v1";
139
- var AN_BASE = process.env.AN_URL || "https://an.dev";
140
- async function fetchProjects(apiKey) {
141
- const res = await fetch(`${API_BASE2}/projects`, {
142
- headers: { Authorization: `Bearer ${apiKey}` }
143
- });
144
- if (!res.ok) return [];
145
- const data = await res.json();
146
- return data.projects ?? [];
175
+ async function extractSandboxConfig(bundle) {
176
+ const config = await importBundle(bundle);
177
+ if (!config) return null;
178
+ const sandbox = config.sandbox;
179
+ if (sandbox && sandbox._type === "sandbox") {
180
+ const { _type, ...sandboxConfig } = sandbox;
181
+ return sandboxConfig;
182
+ }
183
+ return null;
147
184
  }
148
- async function linkProject(apiKey) {
149
- const projects = await fetchProjects(apiKey);
150
- if (projects.length === 0) {
151
- const name = await p2.text({
152
- message: "Project name",
153
- defaultValue: basename(process.cwd()),
154
- placeholder: basename(process.cwd())
155
- });
156
- if (p2.isCancel(name)) {
157
- p2.cancel("Deploy cancelled.");
158
- process.exit(0);
185
+ var HOOK_NAMES = ["onStart", "onToolCall", "onToolResult", "onStepFinish", "onFinish", "onError"];
186
+ async function extractAgentMetadata(bundle) {
187
+ try {
188
+ const config = await importBundle(bundle);
189
+ if (!config) return null;
190
+ const tools = [];
191
+ if (config.tools && typeof config.tools === "object") {
192
+ for (const [name, def] of Object.entries(config.tools)) {
193
+ tools.push({ name, description: def?.description ?? "" });
194
+ }
159
195
  }
160
- return { projectId: "", projectSlug: slugify(name) };
161
- }
162
- const options = [
163
- { value: "__new__", label: "Create a new project" },
164
- ...projects.map((proj) => ({ value: proj.slug, label: proj.name || proj.slug }))
165
- ];
166
- const choice = await p2.select({
167
- message: "Link to a project",
168
- options
169
- });
170
- if (p2.isCancel(choice)) {
171
- p2.cancel("Deploy cancelled.");
172
- process.exit(0);
173
- }
174
- if (choice === "__new__") {
175
- const name = await p2.text({
176
- message: "Project name",
177
- defaultValue: basename(process.cwd()),
178
- placeholder: basename(process.cwd())
179
- });
180
- if (p2.isCancel(name)) {
181
- p2.cancel("Deploy cancelled.");
182
- process.exit(0);
196
+ const hooks = HOOK_NAMES.filter((h) => typeof config[h] === "function");
197
+ const metadata = {
198
+ model: config.model ?? "unknown",
199
+ permissionMode: config.permissionMode ?? "default",
200
+ maxTurns: config.maxTurns ?? 50,
201
+ tools,
202
+ hooks
203
+ };
204
+ if (config.systemPrompt !== void 0) {
205
+ metadata.systemPrompt = config.systemPrompt;
206
+ }
207
+ if (config.maxBudgetUsd !== void 0) {
208
+ metadata.maxBudgetUsd = config.maxBudgetUsd;
209
+ }
210
+ const sandbox = config.sandbox;
211
+ if (sandbox && sandbox._type === "sandbox") {
212
+ metadata.sandbox = {};
213
+ if (sandbox.apt) metadata.sandbox.apt = sandbox.apt;
214
+ if (sandbox.setup) metadata.sandbox.setup = sandbox.setup;
215
+ if (sandbox.cwd) metadata.sandbox.cwd = sandbox.cwd;
183
216
  }
184
- return { projectId: "", projectSlug: slugify(name) };
217
+ return metadata;
218
+ } catch {
219
+ return null;
185
220
  }
186
- const selected = projects.find((proj) => proj.slug === choice);
187
- return { projectId: selected.id, projectSlug: selected.slug };
188
221
  }
222
+
223
+ // src/deploy.ts
224
+ import { existsSync } from "fs";
225
+ import { join as join2 } from "path";
226
+ import * as p2 from "@clack/prompts";
227
+ var API_BASE2 = process.env.AN_API_URL || "https://an.dev/api/v1";
228
+ var AN_BASE = process.env.AN_URL || "https://an.dev";
189
229
  async function deploy() {
190
230
  p2.intro("an deploy");
191
231
  const apiKey = getApiKey();
@@ -193,6 +233,9 @@ async function deploy() {
193
233
  p2.log.error("Not logged in. Run `an login` first, or set AN_API_KEY env var.");
194
234
  process.exit(1);
195
235
  }
236
+ if (existsSync(join2(process.cwd(), ".an", "project.json"))) {
237
+ p2.log.warn(".an/project.json is no longer used and can be removed.");
238
+ }
196
239
  let agents;
197
240
  try {
198
241
  agents = await findAgentEntryPoints();
@@ -201,25 +244,13 @@ async function deploy() {
201
244
  process.exit(1);
202
245
  }
203
246
  p2.log.info(`Found ${agents.length} agent${agents.length > 1 ? "s" : ""}`);
204
- let project = getProject();
205
- let projectSlug;
206
- let projectId;
207
- if (project) {
208
- projectSlug = project.projectSlug;
209
- projectId = project.projectId;
210
- } else if (process.stdin.isTTY) {
211
- const linked = await linkProject(apiKey);
212
- projectSlug = linked.projectSlug;
213
- projectId = linked.projectId;
214
- } else {
215
- projectSlug = slugify(basename(process.cwd()));
216
- projectId = "";
217
- }
218
247
  const deployed = [];
219
248
  for (const agent of agents) {
220
249
  const s = p2.spinner();
221
250
  s.start(`Bundling ${agent.slug}...`);
222
251
  const bundle = await bundleAgent(agent.entryPoint);
252
+ const sandboxConfig = await extractSandboxConfig(bundle);
253
+ const metadata = await extractAgentMetadata(bundle);
223
254
  s.stop(`Bundled ${agent.slug} (${(bundle.length / 1024).toFixed(1)}kb)`);
224
255
  const s2 = p2.spinner();
225
256
  s2.start(`Deploying ${agent.slug}...`);
@@ -230,9 +261,10 @@ async function deploy() {
230
261
  "Content-Type": "application/json"
231
262
  },
232
263
  body: JSON.stringify({
233
- projectSlug,
234
264
  slug: agent.slug,
235
- bundle: bundle.toString("base64")
265
+ bundle: bundle.toString("base64"),
266
+ ...sandboxConfig && { sandboxConfig },
267
+ ...metadata && { metadata }
236
268
  })
237
269
  });
238
270
  if (!res.ok) {
@@ -243,31 +275,148 @@ async function deploy() {
243
275
  p2.log.info(`
244
276
  Deployed before failure:`);
245
277
  for (const a of deployed) {
246
- p2.log.info(` ${a.slug} \u2192 ${AN_BASE}/a/${projectSlug}/${a.slug}`);
278
+ p2.log.info(` ${a.slug} \u2192 ${AN_BASE}/agents`);
247
279
  }
248
280
  }
249
281
  process.exit(1);
250
282
  }
251
283
  const result = await res.json();
252
- projectId = result.projectId;
253
- projectSlug = result.projectSlug;
254
284
  deployed.push({ slug: agent.slug });
255
- s2.stop(`${agent.slug} deployed`);
285
+ const versionTag = result.version ? ` (v${result.version})` : "";
286
+ s2.stop(`${agent.slug} deployed${versionTag}`);
256
287
  }
257
- saveProject({ projectId, projectSlug });
258
- p2.log.success(`Deployed ${deployed.length} agent${deployed.length > 1 ? "s" : ""} to ${projectSlug}`);
288
+ p2.log.success(`Deployed ${deployed.length} agent${deployed.length > 1 ? "s" : ""}`);
259
289
  console.log();
260
290
  for (const agent of deployed) {
261
- console.log(` ${agent.slug} \u2192 ${AN_BASE}/a/${projectSlug}/${agent.slug}`);
291
+ console.log(` ${agent.slug} \u2192 ${AN_BASE}/agents`);
262
292
  }
263
293
  console.log();
264
294
  p2.log.info("Next steps:");
265
295
  console.log(" \xB7 Open the link above to test your agent");
266
296
  console.log(" \xB7 Run `an deploy` again after changes");
267
- console.log(` \xB7 View all deployments: ${AN_BASE}/an/deployments`);
297
+ console.log(` \xB7 View all deployments: ${AN_BASE}/agents`);
268
298
  p2.outro("Done");
269
299
  }
270
300
 
301
+ // src/env.ts
302
+ import * as p3 from "@clack/prompts";
303
+ var API_BASE3 = process.env.AN_API_URL || "https://an.dev/api/v1";
304
+ function maskValue(value) {
305
+ if (value.length <= 4) return "\u2022".repeat(value.length);
306
+ return value.slice(0, 2) + "\u2022".repeat(Math.min(value.length - 4, 12)) + value.slice(-2);
307
+ }
308
+ function requireAuth() {
309
+ const apiKey = getApiKey();
310
+ if (!apiKey) {
311
+ p3.log.error("Not logged in. Run `an login` first, or set AN_API_KEY env var.");
312
+ process.exit(1);
313
+ }
314
+ return apiKey;
315
+ }
316
+ function requireAgentSlug(args2) {
317
+ const slug = args2[0];
318
+ if (!slug || slug.startsWith("-")) {
319
+ p3.log.error("Agent slug is required. Usage: an env list <agent-slug>");
320
+ process.exit(1);
321
+ }
322
+ return slug;
323
+ }
324
+ async function fetchEnvVars(apiKey, agentSlug) {
325
+ const res = await fetch(`${API_BASE3}/agents/${agentSlug}/env`, {
326
+ headers: { Authorization: `Bearer ${apiKey}` }
327
+ });
328
+ if (!res.ok) {
329
+ const err = await res.json().catch(() => ({}));
330
+ throw new Error(err.message || `Failed to fetch env vars (${res.status})`);
331
+ }
332
+ const data = await res.json();
333
+ return data.envVars ?? {};
334
+ }
335
+ async function putEnvVars(apiKey, agentSlug, envVars) {
336
+ const res = await fetch(`${API_BASE3}/agents/${agentSlug}/env`, {
337
+ method: "PUT",
338
+ headers: {
339
+ Authorization: `Bearer ${apiKey}`,
340
+ "Content-Type": "application/json"
341
+ },
342
+ body: JSON.stringify({ envVars })
343
+ });
344
+ if (!res.ok) {
345
+ const err = await res.json().catch(() => ({}));
346
+ throw new Error(err.message || `Failed to update env vars (${res.status})`);
347
+ }
348
+ }
349
+ async function envList(args2) {
350
+ const apiKey = requireAuth();
351
+ const agentSlug = requireAgentSlug(args2);
352
+ const showValues = args2.includes("--show-values");
353
+ try {
354
+ const vars = await fetchEnvVars(apiKey, agentSlug);
355
+ const keys = Object.keys(vars);
356
+ if (keys.length === 0) {
357
+ p3.log.info("No environment variables set.");
358
+ return;
359
+ }
360
+ console.log();
361
+ for (const key of keys) {
362
+ const display = showValues ? vars[key] : maskValue(vars[key]);
363
+ console.log(` ${key}=${display}`);
364
+ }
365
+ console.log();
366
+ p3.log.info(`${keys.length} variable${keys.length !== 1 ? "s" : ""}`);
367
+ } catch (err) {
368
+ p3.log.error(err.message);
369
+ process.exit(1);
370
+ }
371
+ }
372
+ async function envSet(args2) {
373
+ const apiKey = requireAuth();
374
+ const agentSlug = requireAgentSlug(args2);
375
+ const key = args2[1];
376
+ const value = args2.slice(2).join(" ");
377
+ if (!key || !value) {
378
+ p3.log.error("Usage: an env set <agent-slug> KEY VALUE");
379
+ process.exit(1);
380
+ }
381
+ if (!/^[A-Z0-9_]+$/.test(key)) {
382
+ p3.log.error("Invalid key. Use uppercase letters, numbers, and underscores only.");
383
+ process.exit(1);
384
+ }
385
+ try {
386
+ const vars = await fetchEnvVars(apiKey, agentSlug);
387
+ const existed = key in vars;
388
+ vars[key] = value;
389
+ await putEnvVars(apiKey, agentSlug, vars);
390
+ p3.log.success(existed ? `Updated ${key}` : `Set ${key}`);
391
+ } catch (err) {
392
+ p3.log.error(err.message);
393
+ process.exit(1);
394
+ }
395
+ }
396
+ async function envRemove(args2) {
397
+ const apiKey = requireAuth();
398
+ const agentSlug = requireAgentSlug(args2);
399
+ const key = args2[1];
400
+ if (!key) {
401
+ p3.log.error("Usage: an env remove <agent-slug> KEY");
402
+ process.exit(1);
403
+ }
404
+ try {
405
+ const vars = await fetchEnvVars(apiKey, agentSlug);
406
+ if (!(key in vars)) {
407
+ p3.log.info(`${key} not found`);
408
+ return;
409
+ }
410
+ delete vars[key];
411
+ const envVars = Object.keys(vars).length > 0 ? vars : null;
412
+ await putEnvVars(apiKey, agentSlug, envVars);
413
+ p3.log.success(`Removed ${key}`);
414
+ } catch (err) {
415
+ p3.log.error(err.message);
416
+ process.exit(1);
417
+ }
418
+ }
419
+
271
420
  // src/index.ts
272
421
  import { createRequire } from "module";
273
422
  var require2 = createRequire(import.meta.url);
@@ -275,23 +424,36 @@ var { version } = require2("../package.json");
275
424
  var command = process.argv[2];
276
425
  var args = process.argv.slice(3);
277
426
  var hasFlag = (flag) => args.includes(flag);
427
+ function getFlagValue(flag) {
428
+ const idx = args.indexOf(flag);
429
+ if (idx === -1 || idx + 1 >= args.length) return void 0;
430
+ return args[idx + 1];
431
+ }
278
432
  function showHelp() {
279
433
  console.log(`AN CLI v${version} \u2014 deploy AI agents
280
434
  `);
281
435
  console.log("Usage: an <command>\n");
282
436
  console.log("Commands:");
283
- console.log(" an login Authenticate with AN platform");
284
- console.log(" an deploy Bundle and deploy your agent");
437
+ console.log(" an login Authenticate with AN platform");
438
+ console.log(" an deploy Bundle and deploy your agent");
439
+ console.log(" an env list <agent> List environment variables");
440
+ console.log(" an env set <agent> K V Set an environment variable");
441
+ console.log(" an env remove <agent> K Remove an environment variable");
285
442
  console.log("\nOptions:");
286
- console.log(" --help, -h Show help");
287
- console.log(" --version, -v Show version");
443
+ console.log(" --help, -h Show help");
444
+ console.log(" --version, -v Show version");
445
+ console.log("\nEnvironment variables:");
446
+ console.log(" AN_API_KEY API key (skips login prompt)");
447
+ console.log(" AN_API_URL API base URL override");
288
448
  console.log("\nGet started: an login");
289
449
  console.log("Docs: https://an.dev/docs");
290
450
  }
291
451
  function showLoginHelp() {
292
- console.log("Usage: an login\n");
293
- console.log("Authenticate with the AN platform using an API key.");
294
- console.log("Get your API key at https://an.dev/api-keys");
452
+ console.log("Usage: an login [options]\n");
453
+ console.log("Authenticate with the AN platform using an API key.\n");
454
+ console.log("Options:");
455
+ console.log(" --api-key KEY Pass API key directly (non-interactive)");
456
+ console.log("\nGet your API key at https://an.dev/api-keys");
295
457
  }
296
458
  function showDeployHelp() {
297
459
  console.log("Usage: an deploy\n");
@@ -303,18 +465,45 @@ function showDeployHelp() {
303
465
  console.log(" another.ts single-file agent");
304
466
  console.log("\nDocs: https://an.dev/docs");
305
467
  }
468
+ function showEnvHelp() {
469
+ console.log("Usage: an env <subcommand> <agent-slug>\n");
470
+ console.log("Manage environment variables for an agent.\n");
471
+ console.log("Subcommands:");
472
+ console.log(" an env list <agent> List all env vars (masked)");
473
+ console.log(" an env list <agent> --show-values List all env vars (plain text)");
474
+ console.log(" an env set <agent> KEY VALUE Set or update an env var");
475
+ console.log(" an env remove <agent> KEY Remove an env var");
476
+ }
306
477
  if (command === "login") {
307
478
  if (hasFlag("--help") || hasFlag("-h")) {
308
479
  showLoginHelp();
309
480
  } else {
310
- await login();
481
+ await login({ apiKey: getFlagValue("--api-key") });
311
482
  }
312
483
  } else if (command === "deploy") {
313
484
  if (hasFlag("--help") || hasFlag("-h")) {
314
485
  showDeployHelp();
315
486
  } else {
487
+ if (hasFlag("--project")) {
488
+ console.log("Warning: --project flag is no longer used and will be ignored.");
489
+ }
316
490
  await deploy();
317
491
  }
492
+ } else if (command === "env") {
493
+ const subcommand = args[0];
494
+ const subArgs = args.slice(1);
495
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
496
+ showEnvHelp();
497
+ } else if (subcommand === "list") {
498
+ await envList(subArgs);
499
+ } else if (subcommand === "set") {
500
+ await envSet(subArgs);
501
+ } else if (subcommand === "remove") {
502
+ await envRemove(subArgs);
503
+ } else {
504
+ console.log(`Unknown env subcommand: ${subcommand}`);
505
+ showEnvHelp();
506
+ }
318
507
  } else if (command === "--version" || command === "-v") {
319
508
  console.log(version);
320
509
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@an-sdk/cli",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "AN CLI — deploy AI agents",
5
5
  "type": "module",
6
6
  "bin": {
package/src/bundler.ts CHANGED
@@ -61,3 +61,96 @@ export async function bundleAgent(entryPoint: string): Promise<Buffer> {
61
61
 
62
62
  return Buffer.from(result.outputFiles[0].contents)
63
63
  }
64
+
65
+ async function importBundle(bundle: Buffer): Promise<Record<string, unknown> | null> {
66
+ const { writeFileSync, unlinkSync, mkdirSync } = await import("fs")
67
+ const { join } = await import("path")
68
+
69
+ // Write to .an-tmp/ in cwd so ESM can resolve @an-sdk/agent from node_modules
70
+ const tmpDir = join(process.cwd(), ".an-tmp")
71
+ mkdirSync(tmpDir, { recursive: true })
72
+ const tmpPath = join(tmpDir, `an-bundle-${Date.now()}.mjs`)
73
+ try {
74
+ writeFileSync(tmpPath, bundle)
75
+ const mod = await import(tmpPath)
76
+ const config = mod.default
77
+ if (config?._type === "agent") return config
78
+ return null
79
+ } catch {
80
+ return null
81
+ } finally {
82
+ try { unlinkSync(tmpPath) } catch {}
83
+ try { const { rmdirSync } = await import("fs"); rmdirSync(tmpDir) } catch {}
84
+ }
85
+ }
86
+
87
+ export async function extractSandboxConfig(
88
+ bundle: Buffer,
89
+ ): Promise<Record<string, unknown> | null> {
90
+ const config = await importBundle(bundle)
91
+ if (!config) return null
92
+ const sandbox = config.sandbox as Record<string, unknown> | undefined
93
+ if (sandbox && (sandbox as any)._type === "sandbox") {
94
+ const { _type, ...sandboxConfig } = sandbox
95
+ return sandboxConfig
96
+ }
97
+ return null
98
+ }
99
+
100
+ export type AgentMetadata = {
101
+ model: string
102
+ systemPrompt?: string | { type: string; preset: string; append?: string }
103
+ permissionMode: string
104
+ maxTurns: number
105
+ maxBudgetUsd?: number
106
+ tools: { name: string; description: string }[]
107
+ hooks: string[]
108
+ sandbox?: { apt?: string[]; setup?: string[]; cwd?: string }
109
+ }
110
+
111
+ const HOOK_NAMES = ["onStart", "onToolCall", "onToolResult", "onStepFinish", "onFinish", "onError"]
112
+
113
+ export async function extractAgentMetadata(
114
+ bundle: Buffer,
115
+ ): Promise<AgentMetadata | null> {
116
+ try {
117
+ const config = await importBundle(bundle)
118
+ if (!config) return null
119
+
120
+ const tools: { name: string; description: string }[] = []
121
+ if (config.tools && typeof config.tools === "object") {
122
+ for (const [name, def] of Object.entries(config.tools as Record<string, any>)) {
123
+ tools.push({ name, description: def?.description ?? "" })
124
+ }
125
+ }
126
+
127
+ const hooks = HOOK_NAMES.filter((h) => typeof (config as any)[h] === "function")
128
+
129
+ const metadata: AgentMetadata = {
130
+ model: (config.model as string) ?? "unknown",
131
+ permissionMode: (config.permissionMode as string) ?? "default",
132
+ maxTurns: (config.maxTurns as number) ?? 50,
133
+ tools,
134
+ hooks,
135
+ }
136
+
137
+ if (config.systemPrompt !== undefined) {
138
+ metadata.systemPrompt = config.systemPrompt as AgentMetadata["systemPrompt"]
139
+ }
140
+ if (config.maxBudgetUsd !== undefined) {
141
+ metadata.maxBudgetUsd = config.maxBudgetUsd as number
142
+ }
143
+
144
+ const sandbox = config.sandbox as Record<string, unknown> | undefined
145
+ if (sandbox && (sandbox as any)._type === "sandbox") {
146
+ metadata.sandbox = {}
147
+ if (sandbox.apt) metadata.sandbox.apt = sandbox.apt as string[]
148
+ if (sandbox.setup) metadata.sandbox.setup = sandbox.setup as string[]
149
+ if (sandbox.cwd) metadata.sandbox.cwd = sandbox.cwd as string
150
+ }
151
+
152
+ return metadata
153
+ } catch {
154
+ return null
155
+ }
156
+ }
package/src/config.ts CHANGED
@@ -4,7 +4,6 @@ import { homedir } from "os"
4
4
 
5
5
  const AN_DIR = join(homedir(), ".an")
6
6
  const CREDENTIALS_PATH = join(AN_DIR, "credentials")
7
- const PROJECT_PATH = join(process.cwd(), ".an", "project.json")
8
7
 
9
8
  // --- Credentials (global, ~/.an/credentials) ---
10
9
 
@@ -23,24 +22,3 @@ export function saveApiKey(apiKey: string): void {
23
22
  mkdirSync(AN_DIR, { recursive: true })
24
23
  writeFileSync(CREDENTIALS_PATH, JSON.stringify({ apiKey }, null, 2))
25
24
  }
26
-
27
- // --- Project linking (local, .an/project.json) ---
28
-
29
- export type ProjectConfig = { projectId: string; projectSlug: string }
30
-
31
- export function getProject(): ProjectConfig | null {
32
- try {
33
- const data = JSON.parse(readFileSync(PROJECT_PATH, "utf-8"))
34
- // Backward compat: old format had { agentId, slug }
35
- if (data.projectId && data.projectSlug) return data
36
- if (data.slug) return { projectId: data.agentId ?? "", projectSlug: data.slug }
37
- return null
38
- } catch {
39
- return null
40
- }
41
- }
42
-
43
- export function saveProject(data: ProjectConfig): void {
44
- mkdirSync(join(process.cwd(), ".an"), { recursive: true })
45
- writeFileSync(PROJECT_PATH, JSON.stringify(data, null, 2))
46
- }
package/src/deploy.ts CHANGED
@@ -1,79 +1,12 @@
1
- import { getApiKey, getProject, saveProject } from "./config.js"
2
- import { findAgentEntryPoints, bundleAgent } from "./bundler.js"
3
- import { basename } from "path"
1
+ import { getApiKey } from "./config.js"
2
+ import { findAgentEntryPoints, bundleAgent, extractSandboxConfig, extractAgentMetadata } from "./bundler.js"
3
+ import { existsSync } from "fs"
4
+ import { join } from "path"
4
5
  import * as p from "@clack/prompts"
5
6
 
6
- function slugify(name: string): string {
7
- return name
8
- .toLowerCase()
9
- .replace(/[^a-z0-9]+/g, "-")
10
- .replace(/^-|-$/g, "")
11
- }
12
-
13
7
  const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
14
8
  const AN_BASE = process.env.AN_URL || "https://an.dev"
15
9
 
16
- type ProjectInfo = { id: string; slug: string; name: string }
17
-
18
- async function fetchProjects(apiKey: string): Promise<ProjectInfo[]> {
19
- const res = await fetch(`${API_BASE}/projects`, {
20
- headers: { Authorization: `Bearer ${apiKey}` },
21
- })
22
- if (!res.ok) return []
23
- const data = await res.json()
24
- return data.projects ?? []
25
- }
26
-
27
- async function linkProject(apiKey: string): Promise<{ projectId: string; projectSlug: string }> {
28
- const projects = await fetchProjects(apiKey)
29
-
30
- if (projects.length === 0) {
31
- // No existing projects — just ask for a name
32
- const name = await p.text({
33
- message: "Project name",
34
- defaultValue: basename(process.cwd()),
35
- placeholder: basename(process.cwd()),
36
- })
37
- if (p.isCancel(name)) {
38
- p.cancel("Deploy cancelled.")
39
- process.exit(0)
40
- }
41
- return { projectId: "", projectSlug: slugify(name) }
42
- }
43
-
44
- // Has existing projects — let them pick or create new
45
- const options = [
46
- { value: "__new__", label: "Create a new project" },
47
- ...projects.map((proj) => ({ value: proj.slug, label: proj.name || proj.slug })),
48
- ]
49
-
50
- const choice = await p.select({
51
- message: "Link to a project",
52
- options,
53
- })
54
- if (p.isCancel(choice)) {
55
- p.cancel("Deploy cancelled.")
56
- process.exit(0)
57
- }
58
-
59
- if (choice === "__new__") {
60
- const name = await p.text({
61
- message: "Project name",
62
- defaultValue: basename(process.cwd()),
63
- placeholder: basename(process.cwd()),
64
- })
65
- if (p.isCancel(name)) {
66
- p.cancel("Deploy cancelled.")
67
- process.exit(0)
68
- }
69
- return { projectId: "", projectSlug: slugify(name) }
70
- }
71
-
72
- // Linked to existing project
73
- const selected = projects.find((proj) => proj.slug === choice)!
74
- return { projectId: selected.id, projectSlug: selected.slug }
75
- }
76
-
77
10
  export async function deploy() {
78
11
  p.intro("an deploy")
79
12
 
@@ -84,6 +17,11 @@ export async function deploy() {
84
17
  process.exit(1)
85
18
  }
86
19
 
20
+ // Deprecation notice for old project config
21
+ if (existsSync(join(process.cwd(), ".an", "project.json"))) {
22
+ p.log.warn(".an/project.json is no longer used and can be removed.")
23
+ }
24
+
87
25
  // 2. Find agent entry points
88
26
  let agents
89
27
  try {
@@ -94,32 +32,15 @@ export async function deploy() {
94
32
  }
95
33
  p.log.info(`Found ${agents.length} agent${agents.length > 1 ? "s" : ""}`)
96
34
 
97
- // 3. Determine project
98
- let project = getProject()
99
- let projectSlug: string
100
- let projectId: string
101
-
102
- if (project) {
103
- projectSlug = project.projectSlug
104
- projectId = project.projectId
105
- } else if (process.stdin.isTTY) {
106
- // Interactive linking
107
- const linked = await linkProject(apiKey)
108
- projectSlug = linked.projectSlug
109
- projectId = linked.projectId
110
- } else {
111
- // Non-interactive: auto-create from cwd name
112
- projectSlug = slugify(basename(process.cwd()))
113
- projectId = ""
114
- }
115
-
116
- // 4. Deploy each agent
35
+ // 3. Deploy each agent
117
36
  const deployed: { slug: string }[] = []
118
37
 
119
38
  for (const agent of agents) {
120
39
  const s = p.spinner()
121
40
  s.start(`Bundling ${agent.slug}...`)
122
41
  const bundle = await bundleAgent(agent.entryPoint)
42
+ const sandboxConfig = await extractSandboxConfig(bundle)
43
+ const metadata = await extractAgentMetadata(bundle)
123
44
  s.stop(`Bundled ${agent.slug} (${(bundle.length / 1024).toFixed(1)}kb)`)
124
45
 
125
46
  const s2 = p.spinner()
@@ -131,9 +52,10 @@ export async function deploy() {
131
52
  "Content-Type": "application/json",
132
53
  },
133
54
  body: JSON.stringify({
134
- projectSlug,
135
55
  slug: agent.slug,
136
56
  bundle: bundle.toString("base64"),
57
+ ...(sandboxConfig && { sandboxConfig }),
58
+ ...(metadata && { metadata }),
137
59
  }),
138
60
  })
139
61
 
@@ -145,33 +67,29 @@ export async function deploy() {
145
67
  if (deployed.length > 0) {
146
68
  p.log.info(`\nDeployed before failure:`)
147
69
  for (const a of deployed) {
148
- p.log.info(` ${a.slug} → ${AN_BASE}/a/${projectSlug}/${a.slug}`)
70
+ p.log.info(` ${a.slug} → ${AN_BASE}/agents`)
149
71
  }
150
72
  }
151
73
  process.exit(1)
152
74
  }
153
75
 
154
76
  const result = await res.json()
155
- projectId = result.projectId
156
- projectSlug = result.projectSlug
157
77
  deployed.push({ slug: agent.slug })
158
- s2.stop(`${agent.slug} deployed`)
78
+ const versionTag = result.version ? ` (v${result.version})` : ""
79
+ s2.stop(`${agent.slug} deployed${versionTag}`)
159
80
  }
160
81
 
161
- // 5. Save project link
162
- saveProject({ projectId, projectSlug })
163
-
164
- // 6. Output
165
- p.log.success(`Deployed ${deployed.length} agent${deployed.length > 1 ? "s" : ""} to ${projectSlug}`)
82
+ // 4. Output
83
+ p.log.success(`Deployed ${deployed.length} agent${deployed.length > 1 ? "s" : ""}`)
166
84
  console.log()
167
85
  for (const agent of deployed) {
168
- console.log(` ${agent.slug} → ${AN_BASE}/a/${projectSlug}/${agent.slug}`)
86
+ console.log(` ${agent.slug} → ${AN_BASE}/agents`)
169
87
  }
170
88
  console.log()
171
89
  p.log.info("Next steps:")
172
90
  console.log(" · Open the link above to test your agent")
173
91
  console.log(" · Run `an deploy` again after changes")
174
- console.log(` · View all deployments: ${AN_BASE}/an/deployments`)
92
+ console.log(` · View all deployments: ${AN_BASE}/agents`)
175
93
 
176
94
  p.outro("Done")
177
95
  }
package/src/detect.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Detect AI agents running in pseudo-TTY
2
+ export function isAgent(): boolean {
3
+ return !!(
4
+ process.env.CLAUDE_CODE ||
5
+ process.env.CLAUDECODE ||
6
+ process.env.CURSOR_TRACE_ID ||
7
+ process.env.CURSOR_AGENT ||
8
+ process.env.CODEX_SANDBOX ||
9
+ process.env.GEMINI_CLI ||
10
+ process.env.AI_AGENT
11
+ )
12
+ }
13
+
14
+ export function isInteractive(): boolean {
15
+ return !!process.stdin.isTTY && !isAgent()
16
+ }
package/src/env.ts ADDED
@@ -0,0 +1,136 @@
1
+ import { getApiKey } from "./config.js"
2
+ import * as p from "@clack/prompts"
3
+
4
+ const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
5
+
6
+ function maskValue(value: string): string {
7
+ if (value.length <= 4) return "\u2022".repeat(value.length)
8
+ return value.slice(0, 2) + "\u2022".repeat(Math.min(value.length - 4, 12)) + value.slice(-2)
9
+ }
10
+
11
+ function requireAuth(): string {
12
+ const apiKey = getApiKey()
13
+ if (!apiKey) {
14
+ p.log.error("Not logged in. Run `an login` first, or set AN_API_KEY env var.")
15
+ process.exit(1)
16
+ }
17
+ return apiKey
18
+ }
19
+
20
+ function requireAgentSlug(args: string[]): string {
21
+ const slug = args[0]
22
+ if (!slug || slug.startsWith("-")) {
23
+ p.log.error("Agent slug is required. Usage: an env list <agent-slug>")
24
+ process.exit(1)
25
+ }
26
+ return slug
27
+ }
28
+
29
+ async function fetchEnvVars(apiKey: string, agentSlug: string): Promise<Record<string, string>> {
30
+ const res = await fetch(`${API_BASE}/agents/${agentSlug}/env`, {
31
+ headers: { Authorization: `Bearer ${apiKey}` },
32
+ })
33
+ if (!res.ok) {
34
+ const err = await res.json().catch(() => ({}))
35
+ throw new Error((err as any).message || `Failed to fetch env vars (${res.status})`)
36
+ }
37
+ const data = await res.json()
38
+ return data.envVars ?? {}
39
+ }
40
+
41
+ async function putEnvVars(apiKey: string, agentSlug: string, envVars: Record<string, string> | null): Promise<void> {
42
+ const res = await fetch(`${API_BASE}/agents/${agentSlug}/env`, {
43
+ method: "PUT",
44
+ headers: {
45
+ Authorization: `Bearer ${apiKey}`,
46
+ "Content-Type": "application/json",
47
+ },
48
+ body: JSON.stringify({ envVars }),
49
+ })
50
+ if (!res.ok) {
51
+ const err = await res.json().catch(() => ({}))
52
+ throw new Error((err as any).message || `Failed to update env vars (${res.status})`)
53
+ }
54
+ }
55
+
56
+ export async function envList(args: string[]) {
57
+ const apiKey = requireAuth()
58
+ const agentSlug = requireAgentSlug(args)
59
+ const showValues = args.includes("--show-values")
60
+
61
+ try {
62
+ const vars = await fetchEnvVars(apiKey, agentSlug)
63
+ const keys = Object.keys(vars)
64
+
65
+ if (keys.length === 0) {
66
+ p.log.info("No environment variables set.")
67
+ return
68
+ }
69
+
70
+ console.log()
71
+ for (const key of keys) {
72
+ const display = showValues ? vars[key] : maskValue(vars[key]!)
73
+ console.log(` ${key}=${display}`)
74
+ }
75
+ console.log()
76
+ p.log.info(`${keys.length} variable${keys.length !== 1 ? "s" : ""}`)
77
+ } catch (err: any) {
78
+ p.log.error(err.message)
79
+ process.exit(1)
80
+ }
81
+ }
82
+
83
+ export async function envSet(args: string[]) {
84
+ const apiKey = requireAuth()
85
+ const agentSlug = requireAgentSlug(args)
86
+
87
+ const key = args[1]
88
+ const value = args.slice(2).join(" ")
89
+
90
+ if (!key || !value) {
91
+ p.log.error("Usage: an env set <agent-slug> KEY VALUE")
92
+ process.exit(1)
93
+ }
94
+
95
+ if (!/^[A-Z0-9_]+$/.test(key)) {
96
+ p.log.error("Invalid key. Use uppercase letters, numbers, and underscores only.")
97
+ process.exit(1)
98
+ }
99
+
100
+ try {
101
+ const vars = await fetchEnvVars(apiKey, agentSlug)
102
+ const existed = key in vars
103
+ vars[key] = value
104
+ await putEnvVars(apiKey, agentSlug, vars)
105
+ p.log.success(existed ? `Updated ${key}` : `Set ${key}`)
106
+ } catch (err: any) {
107
+ p.log.error(err.message)
108
+ process.exit(1)
109
+ }
110
+ }
111
+
112
+ export async function envRemove(args: string[]) {
113
+ const apiKey = requireAuth()
114
+ const agentSlug = requireAgentSlug(args)
115
+
116
+ const key = args[1]
117
+ if (!key) {
118
+ p.log.error("Usage: an env remove <agent-slug> KEY")
119
+ process.exit(1)
120
+ }
121
+
122
+ try {
123
+ const vars = await fetchEnvVars(apiKey, agentSlug)
124
+ if (!(key in vars)) {
125
+ p.log.info(`${key} not found`)
126
+ return
127
+ }
128
+ delete vars[key]
129
+ const envVars = Object.keys(vars).length > 0 ? vars : null
130
+ await putEnvVars(apiKey, agentSlug, envVars)
131
+ p.log.success(`Removed ${key}`)
132
+ } catch (err: any) {
133
+ p.log.error(err.message)
134
+ process.exit(1)
135
+ }
136
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { login } from "./login.js"
2
2
  import { deploy } from "./deploy.js"
3
+ import { envList, envSet, envRemove } from "./env.js"
3
4
  import { createRequire } from "module"
4
5
 
5
6
  const require = createRequire(import.meta.url)
@@ -8,24 +9,37 @@ const { version } = require("../package.json")
8
9
  const command = process.argv[2]
9
10
  const args = process.argv.slice(3)
10
11
  const hasFlag = (flag: string) => args.includes(flag)
12
+ function getFlagValue(flag: string): string | undefined {
13
+ const idx = args.indexOf(flag)
14
+ if (idx === -1 || idx + 1 >= args.length) return undefined
15
+ return args[idx + 1]
16
+ }
11
17
 
12
18
  function showHelp() {
13
19
  console.log(`AN CLI v${version} — deploy AI agents\n`)
14
20
  console.log("Usage: an <command>\n")
15
21
  console.log("Commands:")
16
- console.log(" an login Authenticate with AN platform")
17
- console.log(" an deploy Bundle and deploy your agent")
22
+ console.log(" an login Authenticate with AN platform")
23
+ console.log(" an deploy Bundle and deploy your agent")
24
+ console.log(" an env list <agent> List environment variables")
25
+ console.log(" an env set <agent> K V Set an environment variable")
26
+ console.log(" an env remove <agent> K Remove an environment variable")
18
27
  console.log("\nOptions:")
19
- console.log(" --help, -h Show help")
20
- console.log(" --version, -v Show version")
28
+ console.log(" --help, -h Show help")
29
+ console.log(" --version, -v Show version")
30
+ console.log("\nEnvironment variables:")
31
+ console.log(" AN_API_KEY API key (skips login prompt)")
32
+ console.log(" AN_API_URL API base URL override")
21
33
  console.log("\nGet started: an login")
22
34
  console.log("Docs: https://an.dev/docs")
23
35
  }
24
36
 
25
37
  function showLoginHelp() {
26
- console.log("Usage: an login\n")
27
- console.log("Authenticate with the AN platform using an API key.")
28
- console.log("Get your API key at https://an.dev/api-keys")
38
+ console.log("Usage: an login [options]\n")
39
+ console.log("Authenticate with the AN platform using an API key.\n")
40
+ console.log("Options:")
41
+ console.log(" --api-key KEY Pass API key directly (non-interactive)")
42
+ console.log("\nGet your API key at https://an.dev/api-keys")
29
43
  }
30
44
 
31
45
  function showDeployHelp() {
@@ -39,18 +53,46 @@ function showDeployHelp() {
39
53
  console.log("\nDocs: https://an.dev/docs")
40
54
  }
41
55
 
56
+ function showEnvHelp() {
57
+ console.log("Usage: an env <subcommand> <agent-slug>\n")
58
+ console.log("Manage environment variables for an agent.\n")
59
+ console.log("Subcommands:")
60
+ console.log(" an env list <agent> List all env vars (masked)")
61
+ console.log(" an env list <agent> --show-values List all env vars (plain text)")
62
+ console.log(" an env set <agent> KEY VALUE Set or update an env var")
63
+ console.log(" an env remove <agent> KEY Remove an env var")
64
+ }
65
+
42
66
  if (command === "login") {
43
67
  if (hasFlag("--help") || hasFlag("-h")) {
44
68
  showLoginHelp()
45
69
  } else {
46
- await login()
70
+ await login({ apiKey: getFlagValue("--api-key") })
47
71
  }
48
72
  } else if (command === "deploy") {
49
73
  if (hasFlag("--help") || hasFlag("-h")) {
50
74
  showDeployHelp()
51
75
  } else {
76
+ if (hasFlag("--project")) {
77
+ console.log("Warning: --project flag is no longer used and will be ignored.")
78
+ }
52
79
  await deploy()
53
80
  }
81
+ } else if (command === "env") {
82
+ const subcommand = args[0]
83
+ const subArgs = args.slice(1)
84
+ if (!subcommand || subcommand === "--help" || subcommand === "-h") {
85
+ showEnvHelp()
86
+ } else if (subcommand === "list") {
87
+ await envList(subArgs)
88
+ } else if (subcommand === "set") {
89
+ await envSet(subArgs)
90
+ } else if (subcommand === "remove") {
91
+ await envRemove(subArgs)
92
+ } else {
93
+ console.log(`Unknown env subcommand: ${subcommand}`)
94
+ showEnvHelp()
95
+ }
54
96
  } else if (command === "--version" || command === "-v") {
55
97
  console.log(version)
56
98
  } else {
package/src/login.ts CHANGED
@@ -1,9 +1,44 @@
1
1
  import * as p from "@clack/prompts"
2
2
  import { getApiKey, saveApiKey } from "./config.js"
3
+ import { isInteractive } from "./detect.js"
3
4
 
4
5
  const API_BASE = process.env.AN_API_URL || "https://an.dev/api/v1"
5
6
 
6
- export async function login() {
7
+ async function verifyKey(apiKey: string): Promise<{ user: any; team: any }> {
8
+ const res = await fetch(`${API_BASE}/me`, {
9
+ headers: { Authorization: `Bearer ${apiKey.trim()}` },
10
+ })
11
+ if (!res.ok) {
12
+ throw new Error("Invalid API key")
13
+ }
14
+ return res.json()
15
+ }
16
+
17
+ export async function login(opts?: { apiKey?: string }) {
18
+ const key = opts?.apiKey
19
+
20
+ // Non-interactive: --api-key flag passed directly
21
+ if (key) {
22
+ try {
23
+ const { user, team } = await verifyKey(key)
24
+ saveApiKey(key.trim())
25
+ console.log(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`)
26
+ console.log("Key saved to ~/.an/credentials")
27
+ } catch {
28
+ console.error("Error: Invalid API key. Get a new one at https://an.dev/api-keys")
29
+ process.exit(1)
30
+ }
31
+ return
32
+ }
33
+
34
+ // Non-interactive without key: can't prompt
35
+ if (!isInteractive()) {
36
+ console.error("Error: No API key provided. Use --api-key KEY or set AN_API_KEY env var.")
37
+ console.error("Get your API key at https://an.dev/api-keys")
38
+ process.exit(1)
39
+ }
40
+
41
+ // Interactive flow
7
42
  p.intro("an login")
8
43
 
9
44
  const existing = getApiKey()
@@ -27,22 +62,16 @@ export async function login() {
27
62
  const s = p.spinner()
28
63
  s.start("Verifying API key...")
29
64
 
30
- const res = await fetch(`${API_BASE}/me`, {
31
- headers: { Authorization: `Bearer ${apiKey.trim()}` },
32
- })
33
-
34
- if (!res.ok) {
65
+ try {
66
+ const { user, team } = await verifyKey(apiKey)
67
+ saveApiKey(apiKey.trim())
68
+ s.stop("Verified")
69
+ p.log.success(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`)
70
+ p.log.info("Key saved to ~/.an/credentials")
71
+ p.outro("Done")
72
+ } catch {
35
73
  s.stop("Invalid API key")
36
74
  p.log.error("Invalid API key. Get a new one at https://an.dev/api-keys")
37
75
  process.exit(1)
38
76
  }
39
-
40
- const { user, team } = await res.json()
41
- saveApiKey(apiKey.trim())
42
- s.stop("Verified")
43
-
44
- p.log.success(`Authenticated as ${user.displayName || user.email} (team: ${team.name})`)
45
- p.log.info("Key saved to ~/.an/credentials")
46
-
47
- p.outro("Done")
48
77
  }