@an-sdk/cli 0.0.9 → 0.0.10
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 +303 -114
- package/package.json +1 -1
- package/src/bundler.ts +93 -0
- package/src/config.ts +0 -22
- package/src/deploy.ts +20 -102
- package/src/detect.ts +16 -0
- package/src/env.ts +136 -0
- package/src/index.ts +50 -8
- package/src/login.ts +44 -15
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
37
|
-
|
|
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
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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:
|
|
83
|
-
if (!
|
|
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 =
|
|
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 =
|
|
94
|
-
if (
|
|
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:
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
|
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,22 +275,20 @@ 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/${
|
|
278
|
+
p2.log.info(` ${a.slug} \u2192 ${AN_BASE}/a/${a.slug}`);
|
|
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
|
-
|
|
285
|
+
const versionTag = result.version ? ` (v${result.version})` : "";
|
|
286
|
+
s2.stop(`${agent.slug} deployed${versionTag}`);
|
|
256
287
|
}
|
|
257
|
-
|
|
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/${
|
|
291
|
+
console.log(` ${agent.slug} \u2192 ${AN_BASE}/a/${agent.slug}`);
|
|
262
292
|
}
|
|
263
293
|
console.log();
|
|
264
294
|
p2.log.info("Next steps:");
|
|
@@ -268,6 +298,125 @@ Deployed before failure:`);
|
|
|
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
|
|
284
|
-
console.log(" an deploy
|
|
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
|
|
287
|
-
console.log(" --version, -v
|
|
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("
|
|
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
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
|
|
2
|
-
import { findAgentEntryPoints, bundleAgent } from "./bundler.js"
|
|
3
|
-
import {
|
|
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.
|
|
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,27 +67,23 @@ 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/${
|
|
70
|
+
p.log.info(` ${a.slug} → ${AN_BASE}/a/${a.slug}`)
|
|
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
|
-
|
|
78
|
+
const versionTag = result.version ? ` (v${result.version})` : ""
|
|
79
|
+
s2.stop(`${agent.slug} deployed${versionTag}`)
|
|
159
80
|
}
|
|
160
81
|
|
|
161
|
-
//
|
|
162
|
-
|
|
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/${
|
|
86
|
+
console.log(` ${agent.slug} → ${AN_BASE}/a/${agent.slug}`)
|
|
169
87
|
}
|
|
170
88
|
console.log()
|
|
171
89
|
p.log.info("Next steps:")
|
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
|
|
17
|
-
console.log(" an deploy
|
|
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
|
|
20
|
-
console.log(" --version, -v
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
}
|