@animaapp/cli 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -42
- package/dist/index.js +782 -269
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command14 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/create.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -13,29 +13,61 @@ import path from "path";
|
|
|
13
13
|
import os from "os";
|
|
14
14
|
var CONFIG_DIR = path.join(os.homedir(), ".config", "anima");
|
|
15
15
|
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
16
|
-
|
|
16
|
+
var HANDOFF_FILE = path.join(CONFIG_DIR, "handoff.json");
|
|
17
|
+
function classifyCredentials() {
|
|
18
|
+
let parsed;
|
|
17
19
|
try {
|
|
18
|
-
if (!fs.existsSync(CREDENTIALS_FILE)) return
|
|
19
|
-
|
|
20
|
-
const parsed = JSON.parse(content);
|
|
21
|
-
if (!parsed.token) return null;
|
|
22
|
-
return parsed;
|
|
20
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) return { kind: "absent" };
|
|
21
|
+
parsed = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
23
22
|
} catch {
|
|
24
|
-
return
|
|
23
|
+
return { kind: "malformed" };
|
|
24
|
+
}
|
|
25
|
+
const figmaToken = typeof parsed.figmaToken === "string" ? parsed.figmaToken : void 0;
|
|
26
|
+
if (typeof parsed.token !== "string") {
|
|
27
|
+
return figmaToken ? { kind: "figma-only", figmaToken } : { kind: "absent" };
|
|
28
|
+
}
|
|
29
|
+
if (parsed.tokenType === "agent") {
|
|
30
|
+
const required2 = ["teamId", "agentId", "expiresAt", "refreshToken", "refreshExpiresAt"];
|
|
31
|
+
if (required2.some((field) => typeof parsed[field] !== "string")) {
|
|
32
|
+
return { kind: "malformed" };
|
|
33
|
+
}
|
|
34
|
+
return { kind: "agent", credential: parsed, figmaToken };
|
|
25
35
|
}
|
|
36
|
+
return {
|
|
37
|
+
kind: "account",
|
|
38
|
+
credential: {
|
|
39
|
+
tokenType: "account",
|
|
40
|
+
token: parsed.token,
|
|
41
|
+
teamId: typeof parsed.teamId === "string" ? parsed.teamId : void 0
|
|
42
|
+
},
|
|
43
|
+
figmaToken
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function readCredentials() {
|
|
47
|
+
const stored = classifyCredentials();
|
|
48
|
+
if (stored.kind !== "agent" && stored.kind !== "account") return null;
|
|
49
|
+
return { ...stored.credential, ...stored.figmaToken ? { figmaToken: stored.figmaToken } : {} };
|
|
50
|
+
}
|
|
51
|
+
function saveCredential(credential) {
|
|
52
|
+
const figmaToken = readCredentials()?.figmaToken;
|
|
53
|
+
write({ ...credential, ...figmaToken ? { figmaToken } : {} });
|
|
54
|
+
}
|
|
55
|
+
function saveFigmaToken(figmaToken) {
|
|
56
|
+
const existing = readCredentials();
|
|
57
|
+
write({ ...existing ?? {}, figmaToken });
|
|
26
58
|
}
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
59
|
+
function write(credentials) {
|
|
60
|
+
writeSecretJson(CREDENTIALS_FILE, { ...credentials });
|
|
61
|
+
}
|
|
62
|
+
function writeSecretJson(file, data) {
|
|
63
|
+
const merged = { ...data };
|
|
30
64
|
for (const key of Object.keys(merged)) {
|
|
31
65
|
if (merged[key] === void 0) delete merged[key];
|
|
32
66
|
}
|
|
33
67
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{ mode: 384 }
|
|
38
|
-
);
|
|
68
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
69
|
+
fs.writeFileSync(tmp, JSON.stringify(merged, null, 2) + "\n", { mode: 384 });
|
|
70
|
+
fs.renameSync(tmp, file);
|
|
39
71
|
}
|
|
40
72
|
function deleteCredentials() {
|
|
41
73
|
try {
|
|
@@ -48,8 +80,35 @@ function deleteCredentials() {
|
|
|
48
80
|
return false;
|
|
49
81
|
}
|
|
50
82
|
}
|
|
83
|
+
function savePendingHandoff(pending) {
|
|
84
|
+
writeSecretJson(HANDOFF_FILE, { ...pending });
|
|
85
|
+
}
|
|
86
|
+
function readPendingHandoff() {
|
|
87
|
+
try {
|
|
88
|
+
if (!fs.existsSync(HANDOFF_FILE)) return null;
|
|
89
|
+
const parsed = JSON.parse(fs.readFileSync(HANDOFF_FILE, "utf-8"));
|
|
90
|
+
const required2 = ["handoffToken", "sessionId", "claimUrl", "claimableUntil", "apiUrl"];
|
|
91
|
+
if (required2.some((field) => typeof parsed[field] !== "string")) return null;
|
|
92
|
+
return parsed;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function clearPendingHandoff() {
|
|
98
|
+
try {
|
|
99
|
+
if (!fs.existsSync(HANDOFF_FILE)) return false;
|
|
100
|
+
fs.unlinkSync(HANDOFF_FILE);
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function getHandoffPath() {
|
|
107
|
+
return HANDOFF_FILE;
|
|
108
|
+
}
|
|
51
109
|
function clearStorage() {
|
|
52
110
|
const had = deleteCredentials();
|
|
111
|
+
clearPendingHandoff();
|
|
53
112
|
try {
|
|
54
113
|
if (fs.existsSync(CONFIG_DIR) && fs.readdirSync(CONFIG_DIR).length === 0) {
|
|
55
114
|
fs.rmdirSync(CONFIG_DIR);
|
|
@@ -74,6 +133,17 @@ function extractTeamIdFromJWT(token) {
|
|
|
74
133
|
}
|
|
75
134
|
}
|
|
76
135
|
|
|
136
|
+
// src/auth/jwt.ts
|
|
137
|
+
function decodeJwtPayload(token) {
|
|
138
|
+
try {
|
|
139
|
+
const parts = token.split(".");
|
|
140
|
+
if (parts.length !== 3) return void 0;
|
|
141
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
142
|
+
} catch {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
77
147
|
// src/api/errors.ts
|
|
78
148
|
var EXIT_CODES = {
|
|
79
149
|
SUCCESS: 0,
|
|
@@ -116,37 +186,39 @@ function timeoutError() {
|
|
|
116
186
|
);
|
|
117
187
|
}
|
|
118
188
|
|
|
119
|
-
// src/auth/
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
token: envToken,
|
|
126
|
-
teamId: process.env.ANIMA_TEAM_ID,
|
|
127
|
-
figmaToken: process.env.FIGMA_TOKEN || creds?.figmaToken,
|
|
128
|
-
source: "env"
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (creds) {
|
|
132
|
-
if (creds.tokenType === "agent" && creds.expiresAt && new Date(creds.expiresAt) <= /* @__PURE__ */ new Date()) {
|
|
133
|
-
throw authError("Your agent token has expired. Run: anima login");
|
|
134
|
-
}
|
|
135
|
-
return {
|
|
136
|
-
token: creds.token,
|
|
137
|
-
teamId: creds.teamId,
|
|
138
|
-
figmaToken: process.env.FIGMA_TOKEN || creds.figmaToken,
|
|
139
|
-
source: "file",
|
|
140
|
-
tokenType: creds.tokenType
|
|
141
|
-
};
|
|
189
|
+
// src/auth/agentSession.ts
|
|
190
|
+
function required(value, field) {
|
|
191
|
+
if (value === void 0) {
|
|
192
|
+
throw authError(
|
|
193
|
+
`The server's token response is missing \`${field}\`. Check --api-url points at an Anima API that issues agent credentials.`
|
|
194
|
+
);
|
|
142
195
|
}
|
|
143
|
-
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
function agentSessionFromTokenResponse(body, carriedOver) {
|
|
199
|
+
const token = typeof body.access_token === "string" ? body.access_token : void 0;
|
|
200
|
+
const claims = decodeJwtPayload(required(token, "access_token"));
|
|
201
|
+
return {
|
|
202
|
+
token,
|
|
203
|
+
teamId: required(typeof claims?.team_id === "string" ? claims.team_id : void 0, "team_id"),
|
|
204
|
+
agentId: required(typeof claims?.agent_id === "string" ? claims.agent_id : void 0, "agent_id"),
|
|
205
|
+
expiresAt: required(
|
|
206
|
+
typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0,
|
|
207
|
+
"exp"
|
|
208
|
+
),
|
|
209
|
+
refreshToken: required(
|
|
210
|
+
typeof body.refresh_token === "string" ? body.refresh_token : carriedOver?.refreshToken,
|
|
211
|
+
"refresh_token"
|
|
212
|
+
),
|
|
213
|
+
refreshExpiresAt: required(
|
|
214
|
+
typeof body.refresh_expires_in === "number" ? new Date(Date.now() + body.refresh_expires_in * 1e3).toISOString() : carriedOver?.refreshExpiresAt,
|
|
215
|
+
"refresh_expires_in"
|
|
216
|
+
)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function persistAgentSession(session) {
|
|
220
|
+
saveCredential({ tokenType: "agent", ...session });
|
|
144
221
|
}
|
|
145
|
-
|
|
146
|
-
// src/api/mcp.ts
|
|
147
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
148
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
149
|
-
import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
150
222
|
|
|
151
223
|
// src/output/log.ts
|
|
152
224
|
import fs2 from "fs";
|
|
@@ -180,6 +252,173 @@ function redact(value) {
|
|
|
180
252
|
return out;
|
|
181
253
|
}
|
|
182
254
|
|
|
255
|
+
// src/auth/refresh.ts
|
|
256
|
+
var REFRESH_GRANT_TYPE = "refresh_token";
|
|
257
|
+
function readFailure(body) {
|
|
258
|
+
const raw = body.error;
|
|
259
|
+
const nested = typeof raw === "object" && raw !== null ? raw : null;
|
|
260
|
+
const code = typeof raw === "string" ? raw : typeof nested?.code === "string" ? nested.code : typeof nested?.error === "string" ? nested.error : null;
|
|
261
|
+
const described = typeof body.error_description === "string" ? body.error_description : null;
|
|
262
|
+
const message = described ?? (typeof raw === "string" ? raw : raw == null ? null : JSON.stringify(raw));
|
|
263
|
+
return { code, message };
|
|
264
|
+
}
|
|
265
|
+
async function refreshAccessToken(apiUrl, carriedOver, fetchFn = fetch) {
|
|
266
|
+
const base = apiUrl.replace(/\/$/, "");
|
|
267
|
+
const response = await fetchFn(`${base}/oauth/token`, {
|
|
268
|
+
method: "POST",
|
|
269
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
270
|
+
body: new URLSearchParams({
|
|
271
|
+
grant_type: REFRESH_GRANT_TYPE,
|
|
272
|
+
refresh_token: carriedOver.refreshToken
|
|
273
|
+
}).toString()
|
|
274
|
+
});
|
|
275
|
+
const body = await response.json().catch(() => ({}));
|
|
276
|
+
const failure = readFailure(body);
|
|
277
|
+
logEvent("refresh_token_response", { status: response.status, error: failure.code });
|
|
278
|
+
if (!response.ok || typeof body.access_token !== "string") {
|
|
279
|
+
if (failure.code === "invalid_grant") {
|
|
280
|
+
clearStorage();
|
|
281
|
+
throw authError(`${failure.message ?? "Your credentials are no longer valid"}
|
|
282
|
+
Run: anima login`);
|
|
283
|
+
}
|
|
284
|
+
throw apiError(
|
|
285
|
+
`Could not refresh credentials (HTTP ${response.status})${failure.message ? `: ${failure.message}` : ""}`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const session = agentSessionFromTokenResponse(body, carriedOver);
|
|
289
|
+
saveCredential({ tokenType: "agent", ...session });
|
|
290
|
+
return session;
|
|
291
|
+
}
|
|
292
|
+
async function revokeStoredRefreshToken(apiUrl, fetchFn = fetch) {
|
|
293
|
+
const creds = readCredentials();
|
|
294
|
+
if (creds?.tokenType !== "agent") return false;
|
|
295
|
+
try {
|
|
296
|
+
const response = await fetchFn(`${apiUrl.replace(/\/$/, "")}/oauth/revoke`, {
|
|
297
|
+
method: "POST",
|
|
298
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
299
|
+
body: new URLSearchParams({ token: creds.refreshToken, token_type_hint: "refresh_token" }).toString()
|
|
300
|
+
});
|
|
301
|
+
logEvent("revoke_response", { status: response.status });
|
|
302
|
+
return response.ok;
|
|
303
|
+
} catch (err) {
|
|
304
|
+
logEvent("revoke_failed", { error: err instanceof Error ? err.message : String(err) });
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/config/store.ts
|
|
310
|
+
import fs3 from "fs";
|
|
311
|
+
import path3 from "path";
|
|
312
|
+
import os2 from "os";
|
|
313
|
+
var CONFIG_DIR2 = path3.join(os2.homedir(), ".config", "anima");
|
|
314
|
+
var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
|
|
315
|
+
var DEFAULT_API_URL = "https://api.agentgrid.io";
|
|
316
|
+
var CONFIG_KEYS = {
|
|
317
|
+
"api-url": "apiUrl"
|
|
318
|
+
};
|
|
319
|
+
function isConfigKey(key) {
|
|
320
|
+
return key in CONFIG_KEYS;
|
|
321
|
+
}
|
|
322
|
+
function configKeys() {
|
|
323
|
+
return Object.keys(CONFIG_KEYS);
|
|
324
|
+
}
|
|
325
|
+
function readConfig() {
|
|
326
|
+
try {
|
|
327
|
+
if (!fs3.existsSync(CONFIG_FILE)) return {};
|
|
328
|
+
const parsed = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf-8"));
|
|
329
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
330
|
+
} catch {
|
|
331
|
+
return {};
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function setConfig(key, value) {
|
|
335
|
+
const merged = { ...readConfig(), [CONFIG_KEYS[key]]: value };
|
|
336
|
+
fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
337
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
338
|
+
}
|
|
339
|
+
function deleteConfig() {
|
|
340
|
+
try {
|
|
341
|
+
if (!fs3.existsSync(CONFIG_FILE)) return false;
|
|
342
|
+
fs3.unlinkSync(CONFIG_FILE);
|
|
343
|
+
return true;
|
|
344
|
+
} catch {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function unsetConfig(key) {
|
|
349
|
+
const config = readConfig();
|
|
350
|
+
const field = CONFIG_KEYS[key];
|
|
351
|
+
if (!(field in config)) return false;
|
|
352
|
+
delete config[field];
|
|
353
|
+
fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
354
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
function getConfigPath() {
|
|
358
|
+
return CONFIG_FILE;
|
|
359
|
+
}
|
|
360
|
+
function resolveApiUrl(flagValue) {
|
|
361
|
+
return flagValue || process.env.ANIMA_API_URL || readConfig().apiUrl || DEFAULT_API_URL;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/auth/token.ts
|
|
365
|
+
var RENEW_BEFORE_EXPIRY_MS = 6e4;
|
|
366
|
+
function needsRenewal(expiresAt) {
|
|
367
|
+
if (!expiresAt) return false;
|
|
368
|
+
const expiry = new Date(expiresAt).getTime();
|
|
369
|
+
return Number.isNaN(expiry) || expiry - Date.now() <= RENEW_BEFORE_EXPIRY_MS;
|
|
370
|
+
}
|
|
371
|
+
async function resolveAuth(apiUrlOverride) {
|
|
372
|
+
const stored = classifyCredentials();
|
|
373
|
+
const envToken = process.env.ANIMA_API_TOKEN;
|
|
374
|
+
if (envToken) {
|
|
375
|
+
return {
|
|
376
|
+
token: envToken,
|
|
377
|
+
teamId: process.env.ANIMA_TEAM_ID,
|
|
378
|
+
figmaToken: process.env.FIGMA_TOKEN || ("figmaToken" in stored ? stored.figmaToken : void 0),
|
|
379
|
+
source: "env"
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
const figmaToken = process.env.FIGMA_TOKEN || ("figmaToken" in stored ? stored.figmaToken : void 0);
|
|
383
|
+
if (stored.kind === "agent") {
|
|
384
|
+
const { credential } = stored;
|
|
385
|
+
if (needsRenewal(credential.expiresAt)) {
|
|
386
|
+
const renewed = await refreshAccessToken(resolveApiUrl(apiUrlOverride), {
|
|
387
|
+
refreshToken: credential.refreshToken,
|
|
388
|
+
refreshExpiresAt: credential.refreshExpiresAt
|
|
389
|
+
});
|
|
390
|
+
return { token: renewed.token, teamId: renewed.teamId, figmaToken, source: "file", tokenType: "agent" };
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
token: credential.token,
|
|
394
|
+
teamId: credential.teamId,
|
|
395
|
+
figmaToken,
|
|
396
|
+
source: "file",
|
|
397
|
+
tokenType: "agent"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
if (stored.kind === "account") {
|
|
401
|
+
return {
|
|
402
|
+
token: stored.credential.token,
|
|
403
|
+
teamId: stored.credential.teamId,
|
|
404
|
+
figmaToken,
|
|
405
|
+
source: "file",
|
|
406
|
+
tokenType: "account"
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (stored.kind === "malformed") {
|
|
410
|
+
throw authError(
|
|
411
|
+
`Your credentials file is damaged or incomplete (${getCredentialsPath()}). Run: anima login`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
throw authError("No credentials found. Run: anima login\n(or set the ANIMA_API_TOKEN env var)");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/api/mcp.ts
|
|
418
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
419
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
420
|
+
import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
421
|
+
|
|
183
422
|
// src/version.ts
|
|
184
423
|
import { createRequire } from "module";
|
|
185
424
|
var { version } = createRequire(import.meta.url)("../package.json");
|
|
@@ -367,10 +606,10 @@ function printKeyValue(key, value) {
|
|
|
367
606
|
}
|
|
368
607
|
function printSessionResult(result) {
|
|
369
608
|
console.log();
|
|
370
|
-
printSuccess("
|
|
609
|
+
printSuccess("Artifact created successfully!");
|
|
371
610
|
printKeyValue("Session ID", result.sessionId);
|
|
372
611
|
if (result.playgroundUrl) {
|
|
373
|
-
printKeyValue("Your
|
|
612
|
+
printKeyValue("Your artifact", chalk.cyan.underline(result.playgroundUrl));
|
|
374
613
|
}
|
|
375
614
|
if (result.tokenUsage) {
|
|
376
615
|
printKeyValue("Token usage", result.tokenUsage.toLocaleString());
|
|
@@ -383,13 +622,6 @@ function printDeployResult(result) {
|
|
|
383
622
|
if (result.url) {
|
|
384
623
|
printKeyValue("Live URL", chalk.cyan.underline(result.url));
|
|
385
624
|
}
|
|
386
|
-
if (result.packageName) {
|
|
387
|
-
printKeyValue("Package", result.packageName);
|
|
388
|
-
printKeyValue("Version", result.packageVersion ?? "");
|
|
389
|
-
if (result.packageUrl) {
|
|
390
|
-
printKeyValue("URL", chalk.cyan.underline(result.packageUrl));
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
625
|
console.log();
|
|
394
626
|
}
|
|
395
627
|
function printFilesWritten(outputDir, count) {
|
|
@@ -533,78 +765,81 @@ function parsePlaygroundUrl(input) {
|
|
|
533
765
|
}
|
|
534
766
|
}
|
|
535
767
|
|
|
536
|
-
// src/
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
768
|
+
// src/api/anonymousCreate.ts
|
|
769
|
+
async function createAnonymousArtifact(request, fetchFn = fetch) {
|
|
770
|
+
const url = `${request.apiUrl.replace(/\/$/, "")}/v1/generationSessions`;
|
|
771
|
+
const body = {
|
|
772
|
+
type: "import",
|
|
773
|
+
...request.files ? { files: request.files } : {},
|
|
774
|
+
...request.zipUploadId ? { zipUploadId: request.zipUploadId } : {},
|
|
775
|
+
...request.framework ? { framework: request.framework } : {},
|
|
776
|
+
...request.artifactType ? { artifactType: request.artifactType } : {},
|
|
777
|
+
...request.name ? { name: request.name } : {},
|
|
778
|
+
...request.handoffToken ? { handoffToken: request.handoffToken } : {},
|
|
779
|
+
...request.clientName ? { clientName: request.clientName } : {}
|
|
780
|
+
};
|
|
781
|
+
logEvent("anonymous_create_request", { url, type: "import", artifactType: request.artifactType });
|
|
782
|
+
const response = await fetchFn(url, {
|
|
783
|
+
method: "POST",
|
|
784
|
+
headers: { "Content-Type": "application/json" },
|
|
785
|
+
body: JSON.stringify(body)
|
|
786
|
+
});
|
|
787
|
+
const payload = await response.json().catch(() => ({}));
|
|
788
|
+
logEvent("anonymous_create_response", { url, status: response.status, error: payload.error });
|
|
789
|
+
if (!response.ok || payload.success !== true) {
|
|
790
|
+
const message = typeof payload.reason === "string" && payload.reason || typeof payload.error === "string" && payload.error || `Anonymous create failed (HTTP ${response.status})`;
|
|
791
|
+
const nextSteps = typeof payload.nextSteps === "string" ? payload.nextSteps : void 0;
|
|
792
|
+
throw apiError(nextSteps ? `${message}
|
|
793
|
+
${nextSteps}` : message, payload);
|
|
559
794
|
}
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
565
|
-
}
|
|
566
|
-
function deleteConfig() {
|
|
567
|
-
try {
|
|
568
|
-
if (!fs3.existsSync(CONFIG_FILE)) return false;
|
|
569
|
-
fs3.unlinkSync(CONFIG_FILE);
|
|
570
|
-
return true;
|
|
571
|
-
} catch {
|
|
572
|
-
return false;
|
|
795
|
+
for (const field of ["sessionId", "handoffToken", "claimUrl", "claimableUntil"]) {
|
|
796
|
+
if (typeof payload[field] !== "string") {
|
|
797
|
+
throw apiError(`The API's create response is missing \`${field}\`.`, payload);
|
|
798
|
+
}
|
|
573
799
|
}
|
|
800
|
+
return payload;
|
|
574
801
|
}
|
|
575
|
-
function
|
|
576
|
-
const
|
|
577
|
-
const
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
}
|
|
584
|
-
function getConfigPath() {
|
|
585
|
-
return CONFIG_FILE;
|
|
586
|
-
}
|
|
587
|
-
function resolveApiUrl(flagValue) {
|
|
588
|
-
return flagValue || process.env.ANIMA_API_URL || readConfig().apiUrl || DEFAULT_API_URL;
|
|
802
|
+
async function getAnonymousZipUploadUrl(apiUrl, fetchFn = fetch) {
|
|
803
|
+
const url = `${apiUrl.replace(/\/$/, "")}/v1/generationSessions/zip-upload-url`;
|
|
804
|
+
const response = await fetchFn(url, { method: "POST" });
|
|
805
|
+
const payload = await response.json().catch(() => ({}));
|
|
806
|
+
if (!response.ok || payload.success !== true || typeof payload.uploadUrl !== "string") {
|
|
807
|
+
const message = typeof payload.reason === "string" && payload.reason || typeof payload.error === "string" && payload.error || `Could not get a zip upload URL (HTTP ${response.status})`;
|
|
808
|
+
throw apiError(message, payload);
|
|
809
|
+
}
|
|
810
|
+
return { zipUploadId: payload.zipUploadId, uploadUrl: payload.uploadUrl };
|
|
589
811
|
}
|
|
590
812
|
|
|
591
813
|
// src/commands/create.ts
|
|
592
814
|
var createCommand = new Command("create").description(
|
|
593
|
-
"Create
|
|
815
|
+
"Create an artifact from your own code: empty for code you'll push (instant) or import your existing code (instant). Also supports AI generation: p2c/l2c/f2c (3-7 min)."
|
|
594
816
|
).requiredOption(
|
|
595
817
|
"-t, --type <type>",
|
|
596
818
|
"empty (empty repo you push to), import (create FROM your code \u2014 use --from), or AI-generated: p2c, l2c, f2c"
|
|
597
819
|
).option("-p, --prompt <prompt>", "Text prompt (p2c)").option("-u, --url <url>", "Website URL (l2c)").option("--file-key <key>", "Figma file key or URL (f2c)").option("--nodes <nodes>", "Figma node IDs, comma-separated (f2c)").option("--figma-token <token>", "Figma personal access token (f2c)").option(
|
|
598
820
|
"--framework <fw>",
|
|
599
821
|
"html | react \u2014 the only supported frameworks. REQUIRED for empty (declare react if you will push React code); detected from package.json for import"
|
|
600
|
-
).option("--styling <style>", "tailwind | css | plain_css | css_modules | inline_styles", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--guidelines <text>", "Additional constraints (p2c only)").option(
|
|
822
|
+
).option("--styling <style>", "tailwind | css | plain_css | css_modules | inline_styles", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--guidelines <text>", "Additional constraints (p2c only)").option(
|
|
823
|
+
"--artifact-type <kind>",
|
|
824
|
+
"app | markdown \u2014 what the artifact IS. app (default) needs an index.html; markdown needs .md files"
|
|
825
|
+
).option("--name <name>", "Project name (empty/import only)").option("--from <path>", "Folder or .zip with your project (import only) \u2014 becomes the first commit").option(
|
|
826
|
+
"--anonymous",
|
|
827
|
+
"Create without an account (import only). Returns a claim link to send a human, and a handoff token to log in with once they claim it"
|
|
828
|
+
).option(
|
|
829
|
+
"--handoff <token>",
|
|
830
|
+
"Add this artifact to an existing claim (--anonymous only), so one claim link covers them all. Defaults to the token from your last anonymous create"
|
|
831
|
+
).option("--client-name <name>", "How this CLI is credited as the creator of an anonymous artifact", "AgentGrid CLI").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
601
832
|
const mode = detectOutputMode(opts);
|
|
602
833
|
initLogger(opts.logFile);
|
|
603
834
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
604
835
|
try {
|
|
605
836
|
const type = opts.type;
|
|
606
837
|
validateCreateOptions(type, opts);
|
|
607
|
-
|
|
838
|
+
if (opts.anonymous) {
|
|
839
|
+
await runAnonymousCreate(opts, mode, progress);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
608
843
|
const figmaToken = type === "f2c" ? resolveFigmaToken(opts, auth.figmaToken) : void 0;
|
|
609
844
|
const args = buildCreateArgs(type, opts);
|
|
610
845
|
const mcpConfig = { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken };
|
|
@@ -617,7 +852,7 @@ var createCommand = new Command("create").description(
|
|
|
617
852
|
progress.update(`Uploading project zip (${source.reason})...`);
|
|
618
853
|
const grant = await withMcp(
|
|
619
854
|
mcpConfig,
|
|
620
|
-
(client) => client.callTool("
|
|
855
|
+
(client) => client.callTool("artifact-get_zip_upload_url", {})
|
|
621
856
|
);
|
|
622
857
|
if (!grant.success || !grant.uploadUrl || !grant.zipUploadId) {
|
|
623
858
|
const message = grant.reason || grant.error || "Could not get a zip upload URL";
|
|
@@ -635,11 +870,11 @@ ${grant.nextSteps}` : message, grant);
|
|
|
635
870
|
}
|
|
636
871
|
}
|
|
637
872
|
progress.start(
|
|
638
|
-
type === "empty" ? "Creating empty
|
|
873
|
+
type === "empty" ? "Creating empty artifact..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} artifact...`
|
|
639
874
|
);
|
|
640
875
|
const result = await withMcp(
|
|
641
876
|
mcpConfig,
|
|
642
|
-
(client) => client.callTool("
|
|
877
|
+
(client) => client.callTool("artifact-create", args, {
|
|
643
878
|
timeoutMs: parseInt(opts.timeout, 10),
|
|
644
879
|
onProgress: (p) => progress.update(p.message ?? `Generating (${Math.round(p.progress)}%)...`)
|
|
645
880
|
})
|
|
@@ -650,20 +885,20 @@ ${grant.nextSteps}` : message, grant);
|
|
|
650
885
|
${result.nextSteps}` : message, result);
|
|
651
886
|
}
|
|
652
887
|
const playgroundUrl = result.playgroundUrl || `${PLAYGROUND_BASE}/${result.sessionId}`;
|
|
653
|
-
progress.succeed("
|
|
888
|
+
progress.succeed("Artifact created!");
|
|
654
889
|
if (mode === "pretty") {
|
|
655
890
|
printSessionResult({ sessionId: result.sessionId, playgroundUrl });
|
|
656
891
|
if (result.gitRemoteUrl) {
|
|
657
892
|
console.log(` ${chalk2.cyan(`git clone ${result.gitRemoteUrl}`)}`);
|
|
658
893
|
console.log();
|
|
659
|
-
console.log(" Commit and push to update the live
|
|
894
|
+
console.log(" Commit and push to update the live artifact.");
|
|
660
895
|
if (result.expiresAt) {
|
|
661
896
|
console.log(
|
|
662
897
|
` The URL embeds a token that expires at ${result.expiresAt} and cannot be renewed;
|
|
663
898
|
run ${chalk2.cyan(`anima get-git-token ${result.sessionId}`)} and ${chalk2.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
664
899
|
);
|
|
665
900
|
}
|
|
666
|
-
console.log(" Treat the URL as a secret \u2014 it grants access to this
|
|
901
|
+
console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
|
|
667
902
|
console.log();
|
|
668
903
|
} else if (type === "empty") {
|
|
669
904
|
console.log(
|
|
@@ -707,6 +942,88 @@ ${result.nextSteps}` : message, result);
|
|
|
707
942
|
throw err;
|
|
708
943
|
}
|
|
709
944
|
});
|
|
945
|
+
async function runAnonymousCreate(opts, mode, progress) {
|
|
946
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
947
|
+
progress.start("Collecting project files...");
|
|
948
|
+
const source = collectProjectSource(opts.from);
|
|
949
|
+
let files;
|
|
950
|
+
let zipUploadId;
|
|
951
|
+
if (source.kind === "files") {
|
|
952
|
+
files = source.files;
|
|
953
|
+
} else {
|
|
954
|
+
progress.update(`Uploading project zip (${source.reason})...`);
|
|
955
|
+
const grant = await getAnonymousZipUploadUrl(apiUrl);
|
|
956
|
+
const putResponse = await fetch(grant.uploadUrl, {
|
|
957
|
+
method: "PUT",
|
|
958
|
+
body: new Uint8Array(source.buffer)
|
|
959
|
+
});
|
|
960
|
+
if (!putResponse.ok) {
|
|
961
|
+
throw apiError(`Zip upload failed: HTTP ${putResponse.status}`, { status: putResponse.status });
|
|
962
|
+
}
|
|
963
|
+
zipUploadId = grant.zipUploadId;
|
|
964
|
+
}
|
|
965
|
+
progress.start("Creating anonymous artifact...");
|
|
966
|
+
const request = {
|
|
967
|
+
apiUrl,
|
|
968
|
+
files,
|
|
969
|
+
zipUploadId,
|
|
970
|
+
...opts.framework && opts.artifactType !== "markdown" ? { framework: opts.framework } : {},
|
|
971
|
+
...opts.artifactType ? { artifactType: opts.artifactType } : {},
|
|
972
|
+
...opts.name ? { name: opts.name } : {},
|
|
973
|
+
clientName: opts.clientName
|
|
974
|
+
};
|
|
975
|
+
const reusable = opts.handoff ? { handoffToken: opts.handoff } : livePendingHandoff();
|
|
976
|
+
const result = reusable ? await createAnonymousArtifact({ ...request, handoffToken: reusable.handoffToken }).catch((err) => {
|
|
977
|
+
if (!isHandoffTokenInvalid(err)) throw err;
|
|
978
|
+
clearPendingHandoff();
|
|
979
|
+
progress.update("Previous claim is no longer open \u2014 starting a new one...");
|
|
980
|
+
return createAnonymousArtifact(request);
|
|
981
|
+
}) : await createAnonymousArtifact(request);
|
|
982
|
+
const claimUrl = result.claimUrl;
|
|
983
|
+
savePendingHandoff({
|
|
984
|
+
handoffToken: result.handoffToken,
|
|
985
|
+
sessionId: result.sessionId,
|
|
986
|
+
claimUrl,
|
|
987
|
+
claimableUntil: result.claimableUntil,
|
|
988
|
+
apiUrl
|
|
989
|
+
});
|
|
990
|
+
progress.succeed("Artifact created!");
|
|
991
|
+
if (mode === "pretty") {
|
|
992
|
+
printSessionResult({ sessionId: result.sessionId, playgroundUrl: result.previewUrl });
|
|
993
|
+
console.log(" Send your human this link to claim it:");
|
|
994
|
+
console.log(` ${chalk2.cyan.underline(claimUrl)}`);
|
|
995
|
+
console.log();
|
|
996
|
+
console.log(` Claimable until ${result.claimableUntil}. Until then the artifact is read-only.`);
|
|
997
|
+
console.log(` Then run ${chalk2.cyan("anima login --handoff")} to pick up your credentials.`);
|
|
998
|
+
if (result.notes) console.log(` Note: ${result.notes}`);
|
|
999
|
+
console.log();
|
|
1000
|
+
} else {
|
|
1001
|
+
outputResult(mode, {
|
|
1002
|
+
success: true,
|
|
1003
|
+
sessionId: result.sessionId,
|
|
1004
|
+
previewUrl: result.previewUrl,
|
|
1005
|
+
claimUrl,
|
|
1006
|
+
claimableUntil: result.claimableUntil,
|
|
1007
|
+
// Returned, not just stored: it is delivered exactly once and can never
|
|
1008
|
+
// be re-fetched, so a caller whose config does not outlive the process
|
|
1009
|
+
// would otherwise lose the only route to a credential.
|
|
1010
|
+
handoffToken: result.handoffToken,
|
|
1011
|
+
handoffStored: getHandoffPath(),
|
|
1012
|
+
...result.fileCount !== void 0 ? { fileCount: result.fileCount } : {},
|
|
1013
|
+
...result.skippedFiles?.length ? { skippedFiles: result.skippedFiles } : {},
|
|
1014
|
+
...result.notes ? { notes: result.notes } : {},
|
|
1015
|
+
nextSteps: `Send claimUrl to your human \u2014 that is the link that transfers the artifact to them. handoffToken is yours, never theirs: use it to log in (\`anima login --handoff\`, which waits until they claim and then stores your credentials), or to add more artifacts to the same claim (\`anima create --anonymous --handoff <token>\`). Run the login in the background if you have other work.`
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
function livePendingHandoff() {
|
|
1020
|
+
const pending = readPendingHandoff();
|
|
1021
|
+
if (!pending) return null;
|
|
1022
|
+
return new Date(pending.claimableUntil).getTime() > Date.now() ? pending : null;
|
|
1023
|
+
}
|
|
1024
|
+
function isHandoffTokenInvalid(err) {
|
|
1025
|
+
return err instanceof CliError && /handoff_token_invalid/.test(JSON.stringify(err.detail ?? ""));
|
|
1026
|
+
}
|
|
710
1027
|
function validateCreateOptions(type, opts) {
|
|
711
1028
|
if (!["empty", "import", "p2c", "l2c", "f2c"].includes(type)) {
|
|
712
1029
|
throw validationError(`Invalid type "${type}". Must be empty, import, p2c, l2c, or f2c.`);
|
|
@@ -720,11 +1037,27 @@ function validateCreateOptions(type, opts) {
|
|
|
720
1037
|
if (type === "f2c" && !opts.fileKey) {
|
|
721
1038
|
throw validationError("--file-key is required for f2c type");
|
|
722
1039
|
}
|
|
723
|
-
if (type === "empty" && !opts.framework) {
|
|
1040
|
+
if (type === "empty" && !opts.framework && opts.artifactType !== "markdown") {
|
|
724
1041
|
throw validationError(
|
|
725
1042
|
"--framework is required for type empty \u2014 html for plain HTML/CSS/JS, react for React (declare react if you will push React code)"
|
|
726
1043
|
);
|
|
727
1044
|
}
|
|
1045
|
+
if (opts.artifactType && !["app", "markdown"].includes(opts.artifactType)) {
|
|
1046
|
+
throw validationError(
|
|
1047
|
+
`Invalid --artifact-type "${opts.artifactType}". Must be app or markdown.`
|
|
1048
|
+
);
|
|
1049
|
+
}
|
|
1050
|
+
if (opts.handoff && !opts.anonymous) {
|
|
1051
|
+
throw validationError(
|
|
1052
|
+
"--handoff only applies to --anonymous creates \u2014 it adds the new artifact to an existing claim. An authenticated create already belongs to your team."
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
if (opts.anonymous && type !== "import") {
|
|
1056
|
+
throw validationError(
|
|
1057
|
+
`--anonymous only works with -t import (got "${type}"). Anonymous artifacts are created FROM code you already have: there is no way to push to an empty one, and AI generation needs an account.
|
|
1058
|
+
Log in first for the other types: anima login`
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
728
1061
|
if (type === "import" && !opts.from) {
|
|
729
1062
|
throw validationError("--from <folder|zip> is required for type import \u2014 the path to the project to import");
|
|
730
1063
|
}
|
|
@@ -745,15 +1078,21 @@ function buildCreateArgs(type, opts) {
|
|
|
745
1078
|
if (type === "empty" || type === "import") {
|
|
746
1079
|
return {
|
|
747
1080
|
type,
|
|
748
|
-
|
|
1081
|
+
// A document has no framework to build with, and declaring one on an
|
|
1082
|
+
// import is what made markdown artifacts render as empty apps.
|
|
1083
|
+
...opts.framework && opts.artifactType !== "markdown" ? { framework: opts.framework } : {},
|
|
1084
|
+
...opts.artifactType ? { artifactType: opts.artifactType } : {},
|
|
749
1085
|
...opts.name ? { name: opts.name } : {}
|
|
750
1086
|
};
|
|
751
1087
|
}
|
|
752
1088
|
const framework = opts.framework ?? "html";
|
|
753
1089
|
const language = opts.language ?? (framework === "react" ? "typescript" : void 0);
|
|
1090
|
+
const artifactType = opts.artifactType;
|
|
754
1091
|
const args = {
|
|
755
1092
|
type,
|
|
756
|
-
framework
|
|
1093
|
+
// framework describes how to build an app; a document has none.
|
|
1094
|
+
...artifactType === "markdown" ? {} : { framework },
|
|
1095
|
+
...artifactType ? { artifactType } : {},
|
|
757
1096
|
styling: opts.styling
|
|
758
1097
|
};
|
|
759
1098
|
if (language) args.language = language;
|
|
@@ -791,7 +1130,7 @@ var listCommand = new Command2("list").aliases(["ls", "artifacts"]).description(
|
|
|
791
1130
|
const mode = detectOutputMode(opts);
|
|
792
1131
|
initLogger(opts.logFile);
|
|
793
1132
|
try {
|
|
794
|
-
const auth = resolveAuth();
|
|
1133
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
795
1134
|
const result = await withMcp(
|
|
796
1135
|
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
797
1136
|
(client) => client.callTool("workspace-list_artifacts", {})
|
|
@@ -861,30 +1200,16 @@ function formatUpdatedAt(iso) {
|
|
|
861
1200
|
|
|
862
1201
|
// src/commands/publish.ts
|
|
863
1202
|
import { Command as Command3 } from "commander";
|
|
864
|
-
var publishCommand = new Command3("publish").description("Deploy
|
|
1203
|
+
var publishCommand = new Command3("publish").description("Deploy an artifact's app to a live public URL (1-3 min)").argument("<sessionId>", "Session ID from create command").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
865
1204
|
const mode = detectOutputMode(opts);
|
|
866
1205
|
initLogger(opts.logFile);
|
|
867
1206
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
868
1207
|
try {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
`Invalid mode "${opts.mode}". Must be webapp or designSystem.`
|
|
872
|
-
);
|
|
873
|
-
}
|
|
874
|
-
if (opts.mode === "designSystem" && !opts.packageName) {
|
|
875
|
-
throw validationError("--package-name is required for designSystem mode");
|
|
876
|
-
}
|
|
877
|
-
const auth = resolveAuth();
|
|
878
|
-
const args = {
|
|
879
|
-
sessionId,
|
|
880
|
-
mode: opts.mode
|
|
881
|
-
};
|
|
882
|
-
if (opts.packageName) args.packageName = opts.packageName;
|
|
883
|
-
if (opts.packageVersion) args.packageVersion = opts.packageVersion;
|
|
884
|
-
progress.start(`Publishing as ${opts.mode}...`);
|
|
1208
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1209
|
+
progress.start("Publishing...");
|
|
885
1210
|
const result = await withMcp(
|
|
886
1211
|
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
887
|
-
(client) => client.callTool("
|
|
1212
|
+
(client) => client.callTool("artifact-publish", { sessionId }, {
|
|
888
1213
|
onProgress: (p) => progress.update(p.message ?? "Publishing...")
|
|
889
1214
|
})
|
|
890
1215
|
);
|
|
@@ -898,22 +1223,12 @@ var publishCommand = new Command3("publish").description("Deploy playground to l
|
|
|
898
1223
|
}
|
|
899
1224
|
progress.succeed("Published!");
|
|
900
1225
|
if (mode === "pretty") {
|
|
901
|
-
printDeployResult({
|
|
902
|
-
url: result.liveUrl,
|
|
903
|
-
packageUrl: result.packageUrl,
|
|
904
|
-
packageName: result.packageName,
|
|
905
|
-
packageVersion: result.packageVersion
|
|
906
|
-
});
|
|
1226
|
+
printDeployResult({ url: result.liveUrl });
|
|
907
1227
|
} else {
|
|
908
1228
|
outputResult(mode, {
|
|
909
1229
|
success: true,
|
|
910
1230
|
sessionId,
|
|
911
|
-
...result.liveUrl ? { liveUrl: result.liveUrl, subdomain: result.subdomain } : {}
|
|
912
|
-
...result.packageName ? {
|
|
913
|
-
packageName: result.packageName,
|
|
914
|
-
packageVersion: result.packageVersion,
|
|
915
|
-
packageUrl: result.packageUrl
|
|
916
|
-
} : {}
|
|
1231
|
+
...result.liveUrl ? { liveUrl: result.liveUrl, subdomain: result.subdomain } : {}
|
|
917
1232
|
});
|
|
918
1233
|
}
|
|
919
1234
|
} catch (err) {
|
|
@@ -959,7 +1274,7 @@ function writeFilesToDisk(files, outputDir) {
|
|
|
959
1274
|
}
|
|
960
1275
|
|
|
961
1276
|
// src/commands/codegen.ts
|
|
962
|
-
var codegenCommand = new Command4("codegen").description("Convert Figma design to code files (no
|
|
1277
|
+
var codegenCommand = new Command4("codegen").description("Convert Figma design to code files (no artifact)").requiredOption("--file-key <key>", "Figma file key or URL").option("--nodes <nodes>", "Figma node IDs, comma-separated").option("--figma-token <token>", "Figma personal access token (or set FIGMA_TOKEN env var)").option("-o, --output <dir>", "Output directory", "./generated").option("--framework <fw>", "react | html", "react").option("--styling <style>", "tailwind | plain_css", "tailwind").option("--language <lang>", "typescript | javascript").option("--ui-library <lib>", "mui | antd | shadcn | clean_react").option("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (opts) => {
|
|
963
1278
|
const mode = detectOutputMode(opts);
|
|
964
1279
|
initLogger(opts.logFile);
|
|
965
1280
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -976,7 +1291,7 @@ var codegenCommand = new Command4("codegen").description("Convert Figma design t
|
|
|
976
1291
|
"No node IDs provided. Use --nodes or include node-id in the Figma URL."
|
|
977
1292
|
);
|
|
978
1293
|
}
|
|
979
|
-
const auth = resolveAuth();
|
|
1294
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
980
1295
|
const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || auth.figmaToken;
|
|
981
1296
|
if (!figmaToken) {
|
|
982
1297
|
throw validationError(
|
|
@@ -1045,18 +1360,18 @@ function parseGitTokenResponse(payload) {
|
|
|
1045
1360
|
try {
|
|
1046
1361
|
value = JSON.parse(value);
|
|
1047
1362
|
} catch {
|
|
1048
|
-
throw apiError(`Unexpected response from
|
|
1363
|
+
throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
|
|
1049
1364
|
}
|
|
1050
1365
|
}
|
|
1051
1366
|
if (value === null || typeof value !== "object") {
|
|
1052
|
-
throw apiError(`Unexpected response from
|
|
1367
|
+
throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
|
|
1053
1368
|
}
|
|
1054
1369
|
const body = value;
|
|
1055
1370
|
if (body.success === false) {
|
|
1056
1371
|
throw apiError(`Git access ${body.error ?? "denied"}: ${(body.reason ?? "").trim()}`);
|
|
1057
1372
|
}
|
|
1058
1373
|
if (!body.gitRemoteUrl) {
|
|
1059
|
-
throw apiError(`Unexpected response from
|
|
1374
|
+
throw apiError(`Unexpected response from artifact-get_git_token: ${preview(payload)}`);
|
|
1060
1375
|
}
|
|
1061
1376
|
return {
|
|
1062
1377
|
gitRemoteUrl: body.gitRemoteUrl,
|
|
@@ -1066,17 +1381,17 @@ function parseGitTokenResponse(payload) {
|
|
|
1066
1381
|
}
|
|
1067
1382
|
|
|
1068
1383
|
// src/commands/getGitToken.ts
|
|
1069
|
-
var getGitTokenCommand = new Command5("get-git-token").description("Mint a short-lived git remote URL for
|
|
1384
|
+
var getGitTokenCommand = new Command5("get-git-token").description("Mint a short-lived git remote URL for an artifact (clone/push with git)").argument("<artifactUrl>", "Artifact URL or session ID").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
1070
1385
|
const mode = detectOutputMode(opts);
|
|
1071
1386
|
initLogger(opts.logFile);
|
|
1072
1387
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1073
1388
|
try {
|
|
1074
|
-
const sessionId = parsePlaygroundUrl(
|
|
1075
|
-
const auth = resolveAuth();
|
|
1389
|
+
const sessionId = parsePlaygroundUrl(artifactUrl);
|
|
1390
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1076
1391
|
progress.start("Requesting git access...");
|
|
1077
1392
|
const response = await withMcp(
|
|
1078
1393
|
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1079
|
-
(client) => client.callTool("
|
|
1394
|
+
(client) => client.callTool("artifact-get_git_token", { sessionId })
|
|
1080
1395
|
);
|
|
1081
1396
|
const grant = parseGitTokenResponse(response);
|
|
1082
1397
|
logEvent("git_token_granted", { sessionId, access: grant.access, expiresAt: grant.expiresAt });
|
|
@@ -1086,7 +1401,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1086
1401
|
console.log(` ${chalk4.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
|
|
1087
1402
|
console.log();
|
|
1088
1403
|
if (grant.access === "rw") {
|
|
1089
|
-
console.log(" Commit and push to update the live
|
|
1404
|
+
console.log(" Commit and push to update the live artifact.");
|
|
1090
1405
|
} else {
|
|
1091
1406
|
console.log(" Read-only: cloning works, pushing will be rejected.");
|
|
1092
1407
|
}
|
|
@@ -1096,7 +1411,7 @@ var getGitTokenCommand = new Command5("get-git-token").description("Mint a short
|
|
|
1096
1411
|
re-run this command and ${chalk4.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
1097
1412
|
);
|
|
1098
1413
|
}
|
|
1099
|
-
console.log(" Treat the URL as a secret \u2014 it grants access to this
|
|
1414
|
+
console.log(" Treat the URL as a secret \u2014 it grants access to this artifact.");
|
|
1100
1415
|
console.log();
|
|
1101
1416
|
} else {
|
|
1102
1417
|
outputResult(mode, {
|
|
@@ -1133,15 +1448,6 @@ import open from "open";
|
|
|
1133
1448
|
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1134
1449
|
var DEFAULT_CLIENT_NAME = "AgentGrid CLI";
|
|
1135
1450
|
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1136
|
-
function decodeJwtPayload(token) {
|
|
1137
|
-
try {
|
|
1138
|
-
const parts = token.split(".");
|
|
1139
|
-
if (parts.length !== 3) return void 0;
|
|
1140
|
-
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
1141
|
-
} catch {
|
|
1142
|
-
return void 0;
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
1451
|
async function loginWithDeviceFlow(options) {
|
|
1146
1452
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1147
1453
|
const sleep = options.sleepFn ?? defaultSleep;
|
|
@@ -1186,22 +1492,9 @@ async function loginWithDeviceFlow(options) {
|
|
|
1186
1492
|
const body = await pollResponse.json();
|
|
1187
1493
|
logEvent("device_token_poll", { status: pollResponse.status, error: body.error });
|
|
1188
1494
|
if (pollResponse.ok && typeof body.access_token === "string") {
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
token,
|
|
1193
|
-
teamId: typeof claims?.team_id === "string" ? claims.team_id : void 0,
|
|
1194
|
-
agentId: typeof claims?.agent_id === "string" ? claims.agent_id : void 0,
|
|
1195
|
-
expiresAt: typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0
|
|
1196
|
-
};
|
|
1197
|
-
writeCredentials({
|
|
1198
|
-
token: result.token,
|
|
1199
|
-
teamId: result.teamId,
|
|
1200
|
-
tokenType: "agent",
|
|
1201
|
-
agentId: result.agentId,
|
|
1202
|
-
expiresAt: result.expiresAt
|
|
1203
|
-
});
|
|
1204
|
-
return result;
|
|
1495
|
+
const session = agentSessionFromTokenResponse(body);
|
|
1496
|
+
persistAgentSession(session);
|
|
1497
|
+
return session;
|
|
1205
1498
|
}
|
|
1206
1499
|
switch (body.error) {
|
|
1207
1500
|
case "authorization_pending":
|
|
@@ -1248,15 +1541,6 @@ function parseInvite(input) {
|
|
|
1248
1541
|
}
|
|
1249
1542
|
return { code: trimmed.replace(/\.md$/, "") };
|
|
1250
1543
|
}
|
|
1251
|
-
function decodeJwtPayload2(token) {
|
|
1252
|
-
try {
|
|
1253
|
-
const parts = token.split(".");
|
|
1254
|
-
if (parts.length !== 3) return void 0;
|
|
1255
|
-
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
1256
|
-
} catch {
|
|
1257
|
-
return void 0;
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
1544
|
async function loginWithInvite(options) {
|
|
1261
1545
|
const fetchFn = options.fetchFn ?? fetch;
|
|
1262
1546
|
const { code } = parseInvite(options.invite);
|
|
@@ -1277,31 +1561,89 @@ async function loginWithInvite(options) {
|
|
|
1277
1561
|
const description = typeof body.error_description === "string" ? body.error_description : `Invite redemption failed (HTTP ${response.status})`;
|
|
1278
1562
|
throw new CliError("AUTH_INVALID", description, EXIT_CODES.AUTH_ERROR);
|
|
1279
1563
|
}
|
|
1280
|
-
const
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1564
|
+
const session = agentSessionFromTokenResponse(body);
|
|
1565
|
+
persistAgentSession(session);
|
|
1566
|
+
return session;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
// src/auth/handoff.ts
|
|
1570
|
+
var HANDOFF_GRANT_TYPE = "urn:animaapp:params:oauth:grant-type:handoff_token";
|
|
1571
|
+
var POLL_INTERVAL_SECONDS = 30;
|
|
1572
|
+
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1573
|
+
async function loginWithHandoff(options) {
|
|
1574
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
1575
|
+
const sleep = options.sleepFn ?? defaultSleep2;
|
|
1576
|
+
const now = options.nowFn ?? Date.now;
|
|
1577
|
+
const base = options.apiUrl.replace(/\/$/, "");
|
|
1578
|
+
const tokenUrl = `${base}/oauth/token`;
|
|
1579
|
+
const deadline = resolveDeadline(options.claimableUntil, now);
|
|
1580
|
+
let intervalMs = POLL_INTERVAL_SECONDS * 1e3;
|
|
1581
|
+
for (; ; ) {
|
|
1582
|
+
const response = await fetchFn(tokenUrl, {
|
|
1583
|
+
method: "POST",
|
|
1584
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1585
|
+
body: new URLSearchParams({
|
|
1586
|
+
grant_type: HANDOFF_GRANT_TYPE,
|
|
1587
|
+
handoff_token: options.handoffToken
|
|
1588
|
+
}).toString()
|
|
1589
|
+
});
|
|
1590
|
+
const body = await response.json().catch(() => ({}));
|
|
1591
|
+
logEvent("handoff_token_poll", { status: response.status, error: body.error });
|
|
1592
|
+
if (response.ok && typeof body.access_token === "string") {
|
|
1593
|
+
const session = agentSessionFromTokenResponse(body);
|
|
1594
|
+
persistAgentSession(session);
|
|
1595
|
+
clearPendingHandoff();
|
|
1596
|
+
return session;
|
|
1597
|
+
}
|
|
1598
|
+
switch (body.error) {
|
|
1599
|
+
case "authorization_pending":
|
|
1600
|
+
break;
|
|
1601
|
+
case "slow_down":
|
|
1602
|
+
intervalMs += 5e3;
|
|
1603
|
+
break;
|
|
1604
|
+
case "expired_token":
|
|
1605
|
+
clearPendingHandoff();
|
|
1606
|
+
throw authError(
|
|
1607
|
+
"This handoff token can no longer be redeemed \u2014 the claim window closed unclaimed, the token was already redeemed, or the human claimed the artifact without granting agent access. Share again with a new `anima create --anonymous` \u2014 or, if they claimed it and want you to keep editing, have them grant your agent from the artifact page."
|
|
1608
|
+
);
|
|
1609
|
+
default:
|
|
1610
|
+
throw new CliError(
|
|
1611
|
+
"API_ERROR",
|
|
1612
|
+
`Unexpected token response: ${body.error ?? `HTTP ${response.status}`}`,
|
|
1613
|
+
EXIT_CODES.API_ERROR
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
const remainingMs = deadline.getTime() - now();
|
|
1617
|
+
if (remainingMs <= 0) break;
|
|
1618
|
+
const waitMs = Math.min(intervalMs, remainingMs);
|
|
1619
|
+
options.onPending?.({ nextPollInSeconds: Math.round(waitMs / 1e3), deadline });
|
|
1620
|
+
await sleep(waitMs);
|
|
1621
|
+
}
|
|
1622
|
+
throw authError(
|
|
1623
|
+
"The claim window closed before anyone claimed the artifact. Share again with a new `anima create --anonymous` \u2014 the previous one has expired and can no longer be claimed."
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
var FALLBACK_POLL_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1627
|
+
function resolveDeadline(claimableUntil, now) {
|
|
1628
|
+
const parsed = claimableUntil ? new Date(claimableUntil) : null;
|
|
1629
|
+
if (parsed && !Number.isNaN(parsed.getTime())) return parsed;
|
|
1630
|
+
return new Date(now() + FALLBACK_POLL_WINDOW_MS);
|
|
1296
1631
|
}
|
|
1297
1632
|
|
|
1298
1633
|
// src/commands/login.ts
|
|
1299
|
-
var loginCommand = new Command6("login").description("Log in via the device flow,
|
|
1634
|
+
var loginCommand = new Command6("login").description("Log in via the device flow, redeem an invite with --invite, or finish an anonymous create with --handoff").option("--invite <url-or-code>", "Redeem an invite link (\u2026/invite/<code>.md) or bare code \u2014 no approval step").option(
|
|
1635
|
+
"--handoff [token]",
|
|
1636
|
+
"Log in with the handoff token from an anonymous create. Waits until a human claims the artifact. Defaults to your last one"
|
|
1637
|
+
).option("--client-name <name>", "How this CLI appears on the consent screen", "AgentGrid CLI").option("--no-open", "Do not try to open the verification page in a browser").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").option(
|
|
1300
1638
|
"--print-mcp-config",
|
|
1301
|
-
"After login, also print an MCP server config (
|
|
1639
|
+
"After login, also print an MCP server config (URL only \u2014 the client authorizes itself) to paste into an MCP-capable agent"
|
|
1302
1640
|
).action(async (opts) => {
|
|
1303
1641
|
const mode = detectOutputMode(opts);
|
|
1304
1642
|
initLogger(opts.logFile);
|
|
1643
|
+
if (opts.handoff) {
|
|
1644
|
+
await runHandoffLogin(opts, mode);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1305
1647
|
if (opts.invite) {
|
|
1306
1648
|
const inviteOrigin = parseInvite(opts.invite).origin;
|
|
1307
1649
|
const apiUrl2 = opts.apiUrl ? resolveApiUrl(opts.apiUrl) : inviteOrigin ?? resolveApiUrl();
|
|
@@ -1310,17 +1652,17 @@ var loginCommand = new Command6("login").description("Log in via the device flow
|
|
|
1310
1652
|
if (inviteOrigin && readConfig().apiUrl !== apiUrl2) {
|
|
1311
1653
|
setConfig("api-url", apiUrl2);
|
|
1312
1654
|
}
|
|
1313
|
-
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl2
|
|
1655
|
+
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl2) : void 0;
|
|
1314
1656
|
if (mode === "pretty") {
|
|
1315
1657
|
printSuccess("Connected!");
|
|
1316
1658
|
printKeyValue("Credentials", getCredentialsPath());
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1659
|
+
printKeyValue("Agent", result.agentId);
|
|
1660
|
+
printKeyValue("Team ID", result.teamId);
|
|
1661
|
+
printKeyValue("Token expires", result.expiresAt);
|
|
1320
1662
|
printInfo("This is a scoped agent token \u2014 it acts within what was approved, and the team can revoke it.");
|
|
1321
1663
|
if (mcpConfig) {
|
|
1322
1664
|
console.log();
|
|
1323
|
-
printInfo("MCP server config
|
|
1665
|
+
printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
|
|
1324
1666
|
console.log();
|
|
1325
1667
|
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
1326
1668
|
}
|
|
@@ -1381,17 +1723,17 @@ var loginCommand = new Command6("login").description("Log in via the device flow
|
|
|
1381
1723
|
}
|
|
1382
1724
|
}
|
|
1383
1725
|
});
|
|
1384
|
-
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl
|
|
1726
|
+
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl) : void 0;
|
|
1385
1727
|
if (mode === "pretty") {
|
|
1386
1728
|
printSuccess("Connected!");
|
|
1387
1729
|
printKeyValue("Credentials", getCredentialsPath());
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1730
|
+
printKeyValue("Agent", result.agentId);
|
|
1731
|
+
printKeyValue("Team ID", result.teamId);
|
|
1732
|
+
printKeyValue("Token expires", result.expiresAt);
|
|
1391
1733
|
printInfo("This is a scoped agent token \u2014 it acts within what you approved, and your team can revoke it.");
|
|
1392
1734
|
if (mcpConfig) {
|
|
1393
1735
|
console.log();
|
|
1394
|
-
printInfo("MCP server config
|
|
1736
|
+
printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
|
|
1395
1737
|
console.log();
|
|
1396
1738
|
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
1397
1739
|
}
|
|
@@ -1418,41 +1760,113 @@ var loginCommand = new Command6("login").description("Log in via the device flow
|
|
|
1418
1760
|
throw err;
|
|
1419
1761
|
}
|
|
1420
1762
|
});
|
|
1421
|
-
function
|
|
1763
|
+
async function runHandoffLogin(opts, mode) {
|
|
1764
|
+
const explicitToken = typeof opts.handoff === "string" ? opts.handoff : void 0;
|
|
1765
|
+
const stored = readPendingHandoff();
|
|
1766
|
+
const pending = stored && (!explicitToken || stored.handoffToken === explicitToken) ? stored : explicitToken ? { handoffToken: explicitToken, sessionId: "", claimUrl: "", claimableUntil: "", apiUrl: resolveApiUrl() } : null;
|
|
1767
|
+
if (!pending) {
|
|
1768
|
+
const message = "No handoff token. Pass one (`anima login --handoff <token>`), or run `anima create --anonymous -t import --from <path>` to get one. (Already logged in another way? `anima auth --status`.)";
|
|
1769
|
+
if (mode === "pretty") {
|
|
1770
|
+
printError(message);
|
|
1771
|
+
} else {
|
|
1772
|
+
outputError(mode, { code: "VALIDATION_ERROR", message });
|
|
1773
|
+
}
|
|
1774
|
+
process.exit(1);
|
|
1775
|
+
}
|
|
1776
|
+
const apiUrl = opts.apiUrl ? resolveApiUrl(opts.apiUrl) : pending.apiUrl;
|
|
1777
|
+
try {
|
|
1778
|
+
if (mode === "pretty") {
|
|
1779
|
+
console.log();
|
|
1780
|
+
printInfo("Waiting for a human to claim the artifact:");
|
|
1781
|
+
if (pending.claimUrl) {
|
|
1782
|
+
console.log();
|
|
1783
|
+
console.log(` ${chalk5.cyan.underline(pending.claimUrl)}`);
|
|
1784
|
+
}
|
|
1785
|
+
console.log();
|
|
1786
|
+
printInfo(
|
|
1787
|
+
pending.claimableUntil ? `Claimable until ${pending.claimableUntil}. Ctrl-C to stop \u2014 this does not cancel the claim.` : "Ctrl-C to stop \u2014 this does not cancel the claim."
|
|
1788
|
+
);
|
|
1789
|
+
} else {
|
|
1790
|
+
process.stderr.write(
|
|
1791
|
+
JSON.stringify({
|
|
1792
|
+
event: "claim_required",
|
|
1793
|
+
...pending.claimUrl ? { claimUrl: pending.claimUrl } : {},
|
|
1794
|
+
...pending.sessionId ? { sessionId: pending.sessionId } : {},
|
|
1795
|
+
...pending.claimableUntil ? { claimableUntil: pending.claimableUntil } : {}
|
|
1796
|
+
}) + "\n"
|
|
1797
|
+
);
|
|
1798
|
+
}
|
|
1799
|
+
const result = await loginWithHandoff({
|
|
1800
|
+
apiUrl,
|
|
1801
|
+
handoffToken: pending.handoffToken,
|
|
1802
|
+
claimableUntil: pending.claimableUntil || void 0
|
|
1803
|
+
});
|
|
1804
|
+
if (readConfig().apiUrl !== apiUrl) {
|
|
1805
|
+
setConfig("api-url", apiUrl);
|
|
1806
|
+
}
|
|
1807
|
+
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl) : void 0;
|
|
1808
|
+
if (mode === "pretty") {
|
|
1809
|
+
printSuccess("Claimed \u2014 you're connected!");
|
|
1810
|
+
printKeyValue("Credentials", getCredentialsPath());
|
|
1811
|
+
printKeyValue("Agent", result.agentId);
|
|
1812
|
+
printKeyValue("Team ID", result.teamId);
|
|
1813
|
+
printKeyValue("Token expires", result.expiresAt);
|
|
1814
|
+
printInfo("The artifact is now editable: anima get-git-token " + pending.sessionId);
|
|
1815
|
+
if (mcpConfig) {
|
|
1816
|
+
console.log();
|
|
1817
|
+
printInfo("MCP server config \u2014 your client will prompt to authorize on first use:");
|
|
1818
|
+
console.log();
|
|
1819
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
1820
|
+
}
|
|
1821
|
+
} else {
|
|
1822
|
+
outputResult(mode, {
|
|
1823
|
+
success: true,
|
|
1824
|
+
action: "logged_in",
|
|
1825
|
+
via: "handoff",
|
|
1826
|
+
tokenType: "agent",
|
|
1827
|
+
agentId: result.agentId,
|
|
1828
|
+
teamId: result.teamId,
|
|
1829
|
+
expiresAt: result.expiresAt,
|
|
1830
|
+
sessionId: pending.sessionId,
|
|
1831
|
+
...mcpConfig ? { mcpConfig } : {}
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
if (err instanceof CliError) {
|
|
1836
|
+
if (mode === "pretty") {
|
|
1837
|
+
printError(err.message);
|
|
1838
|
+
} else {
|
|
1839
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1840
|
+
}
|
|
1841
|
+
process.exit(err.exitCode);
|
|
1842
|
+
}
|
|
1843
|
+
throw err;
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
function buildMcpConfig(apiUrl) {
|
|
1422
1847
|
return {
|
|
1423
1848
|
mcpServers: {
|
|
1424
1849
|
anima: {
|
|
1425
1850
|
type: "http",
|
|
1426
|
-
url: `${apiUrl.replace(/\/$/, "")}/v1/mcp
|
|
1427
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
1851
|
+
url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`
|
|
1428
1852
|
}
|
|
1429
1853
|
}
|
|
1430
1854
|
};
|
|
1431
1855
|
}
|
|
1432
1856
|
|
|
1433
1857
|
// src/commands/mcpConfig.ts
|
|
1434
|
-
var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP server config
|
|
1858
|
+
var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP server config for your MCP client").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
|
|
1435
1859
|
const mode = detectOutputMode(opts);
|
|
1436
1860
|
try {
|
|
1437
|
-
const
|
|
1438
|
-
const creds = readCredentials();
|
|
1439
|
-
const expiresAt = auth.source === "env" ? void 0 : creds?.expiresAt;
|
|
1440
|
-
const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl), auth.token);
|
|
1861
|
+
const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl));
|
|
1441
1862
|
if (mode === "pretty") {
|
|
1442
|
-
printInfo("MCP server config (add to your MCP client
|
|
1863
|
+
printInfo("MCP server config (add to your MCP client):");
|
|
1443
1864
|
console.log();
|
|
1444
1865
|
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
1445
1866
|
console.log();
|
|
1446
|
-
|
|
1447
|
-
printInfo(`Token expires ${expiresAt} \u2014 run \`anima login\` then \`anima mcp-config\` to refresh.`);
|
|
1448
|
-
}
|
|
1867
|
+
printInfo("Your client will prompt you to authorize on first use.");
|
|
1449
1868
|
} else {
|
|
1450
|
-
outputResult(mode, {
|
|
1451
|
-
success: true,
|
|
1452
|
-
mcpConfig,
|
|
1453
|
-
...expiresAt ? { expiresAt } : {},
|
|
1454
|
-
...creds?.agentId && auth.source !== "env" ? { agentId: creds.agentId } : {}
|
|
1455
|
-
});
|
|
1869
|
+
outputResult(mode, { success: true, mcpConfig });
|
|
1456
1870
|
}
|
|
1457
1871
|
} catch (err) {
|
|
1458
1872
|
if (err instanceof CliError) {
|
|
@@ -1471,19 +1885,37 @@ var mcpConfigCommand = new Command7("mcp-config").description("Print an MCP serv
|
|
|
1471
1885
|
import { Command as Command8 } from "commander";
|
|
1472
1886
|
import chalk6 from "chalk";
|
|
1473
1887
|
var DEFAULT_API_URL2 = "https://api.agentgrid.io";
|
|
1888
|
+
function describePendingHandoff(pending, state) {
|
|
1889
|
+
return {
|
|
1890
|
+
state,
|
|
1891
|
+
sessionId: pending.sessionId,
|
|
1892
|
+
claimUrl: pending.claimUrl,
|
|
1893
|
+
claimableUntil: pending.claimableUntil,
|
|
1894
|
+
nextSteps: state === "expired" ? "The claim window closed. Create another artifact, or ask your human for an agent invite: anima login --invite <url-or-code>" : "Send claimUrl to your human, then run `anima login --handoff` to wait for the credentials their claim grants."
|
|
1895
|
+
};
|
|
1896
|
+
}
|
|
1897
|
+
function printPendingHandoff(pending, state) {
|
|
1898
|
+
console.log();
|
|
1899
|
+
if (state === "expired") {
|
|
1900
|
+
printInfo(`Anonymous artifact ${pending.sessionId} was never claimed \u2014 its window closed at ${pending.claimableUntil}.`);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
printInfo(`Anonymous artifact ${pending.sessionId} is awaiting a claim (until ${pending.claimableUntil}):`);
|
|
1904
|
+
console.log(` ${chalk6.cyan.underline(pending.claimUrl)}`);
|
|
1905
|
+
console.log(` Run ${chalk6.cyan("anima login --handoff")} to wait for it.`);
|
|
1906
|
+
}
|
|
1474
1907
|
var authCommand = new Command8("auth").description("Manage authentication").option("--token <token>", "Store AgentGrid API token").option("--figma-token <token>", "Store Figma personal access token").option("--team-id <teamId>", "Team ID to associate with token").option("--status", "Show current auth status").option("--logout", "Clear stored credentials (alias for `anima logout`)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL", DEFAULT_API_URL2).action(async (opts) => {
|
|
1475
1908
|
const mode = detectOutputMode(opts);
|
|
1476
1909
|
try {
|
|
1477
1910
|
if (opts.token || opts.figmaToken) {
|
|
1478
1911
|
const toSave = {};
|
|
1479
1912
|
if (opts.token) {
|
|
1480
|
-
toSave.
|
|
1481
|
-
|
|
1913
|
+
toSave.teamId = opts.teamId ?? extractTeamIdFromJWT(opts.token);
|
|
1914
|
+
saveCredential({ tokenType: "account", token: opts.token, teamId: toSave.teamId });
|
|
1482
1915
|
}
|
|
1483
1916
|
if (opts.figmaToken) {
|
|
1484
|
-
|
|
1917
|
+
saveFigmaToken(opts.figmaToken);
|
|
1485
1918
|
}
|
|
1486
|
-
writeCredentials(toSave);
|
|
1487
1919
|
if (mode === "pretty") {
|
|
1488
1920
|
printSuccess("Credentials saved to " + getCredentialsPath());
|
|
1489
1921
|
if (opts.token) {
|
|
@@ -1505,6 +1937,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
1505
1937
|
return;
|
|
1506
1938
|
}
|
|
1507
1939
|
if (opts.logout) {
|
|
1940
|
+
const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
|
|
1508
1941
|
const deleted = clearStorage();
|
|
1509
1942
|
if (mode === "pretty") {
|
|
1510
1943
|
if (deleted) {
|
|
@@ -1513,13 +1946,15 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
1513
1946
|
printInfo("No stored credentials found");
|
|
1514
1947
|
}
|
|
1515
1948
|
} else {
|
|
1516
|
-
outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted });
|
|
1949
|
+
outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted, revoked });
|
|
1517
1950
|
}
|
|
1518
1951
|
return;
|
|
1519
1952
|
}
|
|
1520
1953
|
if (opts.status) {
|
|
1954
|
+
const pending = readPendingHandoff();
|
|
1955
|
+
const pendingState = !pending ? void 0 : new Date(pending.claimableUntil).getTime() <= Date.now() ? "expired" : "awaiting_claim";
|
|
1521
1956
|
try {
|
|
1522
|
-
const auth = resolveAuth();
|
|
1957
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1523
1958
|
const creds = readCredentials();
|
|
1524
1959
|
const sourceName = auth.source === "env" ? "ANIMA_API_TOKEN env var" : getCredentialsPath();
|
|
1525
1960
|
const hasFigma = !!(process.env.FIGMA_TOKEN || creds?.figmaToken);
|
|
@@ -1527,24 +1962,35 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
1527
1962
|
printSuccess("Token configured");
|
|
1528
1963
|
printKeyValue("Source", sourceName);
|
|
1529
1964
|
printKeyValue("AgentGrid token", auth.token.slice(0, 12) + "..." + auth.token.slice(-4));
|
|
1530
|
-
printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "personal
|
|
1965
|
+
printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "account (personal API token)");
|
|
1531
1966
|
if (creds?.tokenType === "agent" && creds.expiresAt) printKeyValue("Expires", creds.expiresAt);
|
|
1532
1967
|
if (auth.teamId) printKeyValue("Team ID", auth.teamId);
|
|
1533
1968
|
printKeyValue("Figma token", hasFigma ? "configured" : "not set (needed for codegen/f2c)");
|
|
1969
|
+
if (pending) printPendingHandoff(pending, pendingState);
|
|
1534
1970
|
} else {
|
|
1535
1971
|
outputResult(mode, {
|
|
1536
1972
|
success: true,
|
|
1537
1973
|
source: auth.source,
|
|
1538
|
-
tokenType: auth.tokenType ?? "
|
|
1974
|
+
tokenType: auth.tokenType ?? "account",
|
|
1539
1975
|
tokenPreview: auth.token.slice(0, 12) + "..." + auth.token.slice(-4),
|
|
1540
1976
|
teamId: auth.teamId,
|
|
1541
1977
|
expiresAt: creds?.tokenType === "agent" ? creds.expiresAt : void 0,
|
|
1542
|
-
figmaToken: hasFigma
|
|
1978
|
+
figmaToken: hasFigma,
|
|
1979
|
+
...pending ? { pendingHandoff: describePendingHandoff(pending, pendingState) } : {}
|
|
1543
1980
|
});
|
|
1544
1981
|
}
|
|
1545
1982
|
} catch (err) {
|
|
1546
1983
|
if (err instanceof CliError) {
|
|
1547
|
-
if (mode === "pretty") {
|
|
1984
|
+
if (pending && mode === "pretty") {
|
|
1985
|
+
printInfo("Not connected yet.");
|
|
1986
|
+
printPendingHandoff(pending, pendingState);
|
|
1987
|
+
} else if (pending) {
|
|
1988
|
+
outputResult(mode, {
|
|
1989
|
+
success: true,
|
|
1990
|
+
tokenType: null,
|
|
1991
|
+
pendingHandoff: describePendingHandoff(pending, pendingState)
|
|
1992
|
+
});
|
|
1993
|
+
} else if (mode === "pretty") {
|
|
1548
1994
|
printError(err.message);
|
|
1549
1995
|
} else {
|
|
1550
1996
|
outputError(mode, { code: err.code, message: err.message });
|
|
@@ -1559,6 +2005,7 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
1559
2005
|
console.log();
|
|
1560
2006
|
printInfo("Auth commands:");
|
|
1561
2007
|
console.log(` ${chalk6.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
|
|
2008
|
+
console.log(` ${chalk6.cyan("anima login --handoff")} Wait for a human to claim your anonymous artifact`);
|
|
1562
2009
|
console.log(` ${chalk6.cyan("anima logout")} Clear all credentials`);
|
|
1563
2010
|
console.log(` ${chalk6.cyan("anima auth --status")} Check auth status`);
|
|
1564
2011
|
console.log(` ${chalk6.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
|
|
@@ -1586,40 +2033,42 @@ var authCommand = new Command8("auth").description("Manage authentication").opti
|
|
|
1586
2033
|
|
|
1587
2034
|
// src/commands/logout.ts
|
|
1588
2035
|
import { Command as Command9 } from "commander";
|
|
1589
|
-
var logoutCommand = new Command9("logout").description("Clear all stored credentials (AgentGrid + Figma tokens) from this machine").option("--json", "Output as JSON").action((opts) => {
|
|
2036
|
+
var logoutCommand = new Command9("logout").description("Clear all stored credentials (AgentGrid + Figma tokens) from this machine").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
|
|
1590
2037
|
const mode = detectOutputMode(opts);
|
|
2038
|
+
const revoked = await revokeStoredRefreshToken(resolveApiUrl(opts.apiUrl));
|
|
1591
2039
|
const hadCredentials = clearStorage();
|
|
1592
2040
|
deleteConfig();
|
|
1593
2041
|
if (mode === "pretty") {
|
|
1594
2042
|
if (hadCredentials) {
|
|
1595
2043
|
printSuccess("Logged out \u2014 all credentials cleared from " + getCredentialsPath());
|
|
2044
|
+
if (revoked) printInfo("Agent credentials revoked server-side");
|
|
1596
2045
|
} else {
|
|
1597
2046
|
printInfo("No stored credentials found");
|
|
1598
2047
|
}
|
|
1599
2048
|
} else {
|
|
1600
|
-
outputResult(mode, { success: true, action: "logged_out", hadCredentials });
|
|
2049
|
+
outputResult(mode, { success: true, action: "logged_out", hadCredentials, revoked });
|
|
1601
2050
|
}
|
|
1602
2051
|
});
|
|
1603
2052
|
|
|
1604
2053
|
// src/commands/unpublish.ts
|
|
1605
2054
|
import { Command as Command10 } from "commander";
|
|
1606
|
-
var unpublishCommand = new Command10("unpublish").description("Take a published
|
|
2055
|
+
var unpublishCommand = new Command10("unpublish").description("Take a published artifact offline (clears its live URL; code and artifact stay intact)").argument("<sessionId>", "Session ID of the published artifact").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
1607
2056
|
const mode = detectOutputMode(opts);
|
|
1608
2057
|
initLogger(opts.logFile);
|
|
1609
2058
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1610
2059
|
try {
|
|
1611
|
-
const auth = resolveAuth();
|
|
2060
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1612
2061
|
progress.start("Unpublishing...");
|
|
1613
2062
|
const result = await withMcp(
|
|
1614
2063
|
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1615
|
-
(client) => client.callTool("
|
|
2064
|
+
(client) => client.callTool("artifact-unpublish", { sessionId })
|
|
1616
2065
|
);
|
|
1617
2066
|
if (!result.success) {
|
|
1618
2067
|
throw apiError(result.reason || result.error || "Unpublish failed", result);
|
|
1619
2068
|
}
|
|
1620
2069
|
progress.succeed("Unpublished");
|
|
1621
2070
|
if (mode === "pretty") {
|
|
1622
|
-
printSuccess(result.message || `
|
|
2071
|
+
printSuccess(result.message || `Artifact ${sessionId} is no longer publicly reachable.`);
|
|
1623
2072
|
} else {
|
|
1624
2073
|
outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
|
|
1625
2074
|
}
|
|
@@ -1639,7 +2088,7 @@ var unpublishCommand = new Command10("unpublish").description("Take a published
|
|
|
1639
2088
|
|
|
1640
2089
|
// src/commands/update.ts
|
|
1641
2090
|
import { Command as Command11 } from "commander";
|
|
1642
|
-
var updateCommand = new Command11("update").description("Update
|
|
2091
|
+
var updateCommand = new Command11("update").description("Update an artifact's metadata: display name and/or visibility (never touches code)").argument("<sessionId>", "Session ID of the artifact").option("--name <name>", "New display name").option("--privacy <privacy>", "public (anyone with the link) | private (team only)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
1643
2092
|
const mode = detectOutputMode(opts);
|
|
1644
2093
|
initLogger(opts.logFile);
|
|
1645
2094
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -1650,11 +2099,11 @@ var updateCommand = new Command11("update").description("Update a playground's m
|
|
|
1650
2099
|
if (opts.privacy && !["public", "private"].includes(opts.privacy)) {
|
|
1651
2100
|
throw validationError(`Invalid privacy "${opts.privacy}". Must be public or private.`);
|
|
1652
2101
|
}
|
|
1653
|
-
const auth = resolveAuth();
|
|
2102
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
1654
2103
|
progress.start("Updating...");
|
|
1655
2104
|
const result = await withMcp(
|
|
1656
2105
|
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1657
|
-
(client) => client.callTool("
|
|
2106
|
+
(client) => client.callTool("artifact-update_metadata", {
|
|
1658
2107
|
sessionId,
|
|
1659
2108
|
...opts.name ? { name: opts.name } : {},
|
|
1660
2109
|
...opts.privacy ? { privacy: opts.privacy } : {}
|
|
@@ -1665,7 +2114,7 @@ var updateCommand = new Command11("update").description("Update a playground's m
|
|
|
1665
2114
|
}
|
|
1666
2115
|
progress.succeed("Updated");
|
|
1667
2116
|
if (mode === "pretty") {
|
|
1668
|
-
printSuccess(result.message || `
|
|
2117
|
+
printSuccess(result.message || `Artifact ${sessionId} updated.`);
|
|
1669
2118
|
} else {
|
|
1670
2119
|
outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
|
|
1671
2120
|
}
|
|
@@ -1750,18 +2199,79 @@ function assertUrl(value) {
|
|
|
1750
2199
|
}
|
|
1751
2200
|
}
|
|
1752
2201
|
|
|
2202
|
+
// src/commands/duplicate.ts
|
|
2203
|
+
import { Command as Command13 } from "commander";
|
|
2204
|
+
import chalk7 from "chalk";
|
|
2205
|
+
function buildDuplicateArgs(artifactUrl, name) {
|
|
2206
|
+
const sessionId = parsePlaygroundUrl(artifactUrl);
|
|
2207
|
+
if (!sessionId) {
|
|
2208
|
+
throw validationError("A source artifact URL or session ID is required");
|
|
2209
|
+
}
|
|
2210
|
+
if (name !== void 0 && (name.length === 0 || name.length > 120)) {
|
|
2211
|
+
throw validationError("--name must be between 1 and 120 characters");
|
|
2212
|
+
}
|
|
2213
|
+
return { sessionId, ...name !== void 0 ? { name } : {} };
|
|
2214
|
+
}
|
|
2215
|
+
function createDuplicateCommand() {
|
|
2216
|
+
return new Command13("duplicate").description(
|
|
2217
|
+
"Duplicate an artifact into a new, independent artifact. Copies code, assets, and supported database content; chat and custom domains are not copied. Duplication is non-idempotent: if a request times out or its response is lost, list recent artifacts before retrying because the first request may already have created the copy."
|
|
2218
|
+
).argument("<artifactUrl>", "Source artifact URL or session ID").option("--name <name>", "Display name for the duplicate (defaults to '<source name> (Copy)')").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (artifactUrl, opts) => {
|
|
2219
|
+
const mode = detectOutputMode(opts);
|
|
2220
|
+
initLogger(opts.logFile);
|
|
2221
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
2222
|
+
try {
|
|
2223
|
+
const args = buildDuplicateArgs(artifactUrl, opts.name);
|
|
2224
|
+
const auth = await resolveAuth(opts.apiUrl);
|
|
2225
|
+
progress.start("Duplicating artifact...");
|
|
2226
|
+
const result = await withMcp(
|
|
2227
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
2228
|
+
(client) => client.callTool("artifact-duplicate", args)
|
|
2229
|
+
);
|
|
2230
|
+
if (!result.success) {
|
|
2231
|
+
throw apiError(result.reason || result.error || "Artifact duplication failed", result);
|
|
2232
|
+
}
|
|
2233
|
+
progress.succeed("Artifact duplicated");
|
|
2234
|
+
if (mode === "pretty") {
|
|
2235
|
+
console.log();
|
|
2236
|
+
printSuccess("Artifact duplicated successfully!");
|
|
2237
|
+
printKeyValue("Name", result.name);
|
|
2238
|
+
printKeyValue("Session ID", result.sessionId);
|
|
2239
|
+
printKeyValue("Source session", result.sourceSessionId);
|
|
2240
|
+
printKeyValue("Artifact", chalk7.cyan.underline(result.playgroundUrl));
|
|
2241
|
+
printKeyValue("Preview", chalk7.cyan.underline(result.previewUrl));
|
|
2242
|
+
printKeyValue("Next steps", result.nextSteps);
|
|
2243
|
+
console.log();
|
|
2244
|
+
} else {
|
|
2245
|
+
outputResult(mode, result);
|
|
2246
|
+
}
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
progress.fail("Failed");
|
|
2249
|
+
if (err instanceof CliError) {
|
|
2250
|
+
if (mode === "pretty") {
|
|
2251
|
+
printError(err.message);
|
|
2252
|
+
} else {
|
|
2253
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
2254
|
+
}
|
|
2255
|
+
process.exit(err.exitCode);
|
|
2256
|
+
}
|
|
2257
|
+
throw err;
|
|
2258
|
+
}
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
var duplicateCommand = createDuplicateCommand();
|
|
2262
|
+
|
|
1753
2263
|
// src/index.ts
|
|
1754
|
-
var program = new
|
|
1755
|
-
`AgentGrid CLI \u2014 host, edit, and publish your work on AgentGrid. Every
|
|
2264
|
+
var program = new Command14().name("anima").description(
|
|
2265
|
+
`AgentGrid CLI \u2014 host, edit, and publish your work on AgentGrid. Every artifact is a
|
|
1756
2266
|
real git repository; each command runs an AgentGrid tool for you.
|
|
1757
2267
|
|
|
1758
2268
|
THE MAIN FLOW (build something and put it on AgentGrid):
|
|
1759
2269
|
1. npx @animaapp/cli login # once; a human approves
|
|
1760
|
-
2. npx @animaapp/cli create -t import --from ./my-project # your code \u2192 a live
|
|
2270
|
+
2. npx @animaapp/cli create -t import --from ./my-project # your code \u2192 a live app
|
|
1761
2271
|
(or: create -t empty --framework html \u2014 an empty repo you push to)
|
|
1762
2272
|
3. The response includes a rw gitRemoteUrl \u2014 git clone, edit, commit, push:
|
|
1763
|
-
every push updates the live
|
|
1764
|
-
4. Share the playgroundUrl with your human. That IS sharing \u2014 the
|
|
2273
|
+
every push updates the live artifact.
|
|
2274
|
+
4. Share the playgroundUrl with your human. That IS sharing \u2014 the artifact
|
|
1765
2275
|
is visible at its URL. Do NOT run publish unless the human explicitly
|
|
1766
2276
|
asked for a public site; offer it as a follow-up question instead.
|
|
1767
2277
|
|
|
@@ -1774,19 +2284,20 @@ TIMING (important for AI agents):
|
|
|
1774
2284
|
create -t empty|import \u2192 seconds
|
|
1775
2285
|
create -t p2c|l2c|f2c \u2192 3-7 minutes AI generation (keep the default 600000ms timeout)
|
|
1776
2286
|
publish/unpublish \u2192 1-3 minutes
|
|
1777
|
-
get-git-token / update \u2192 seconds
|
|
2287
|
+
duplicate / get-git-token / update \u2192 seconds
|
|
1778
2288
|
|
|
1779
2289
|
COMMANDS:
|
|
1780
2290
|
login Connect (device flow, or --invite to redeem an invite link)
|
|
1781
2291
|
logout Full local reset \u2014 clears credentials and CLI config
|
|
1782
2292
|
list List your team's artifacts (most-recently-updated first) to resume work
|
|
1783
|
-
create Make
|
|
1784
|
-
|
|
1785
|
-
|
|
2293
|
+
create Make an artifact: import your code, empty repo, or AI-generated (p2c/l2c/f2c)
|
|
2294
|
+
duplicate Copy an artifact into a new, independent artifact
|
|
2295
|
+
get-git-token Fresh git access to an existing artifact (or after token expiry)
|
|
2296
|
+
update Rename an artifact or set visibility public/private (metadata only)
|
|
1786
2297
|
publish Deploy to a public live URL \u2014 ONLY when the human asked for it
|
|
1787
|
-
unpublish Take a published
|
|
2298
|
+
unpublish Take a published artifact offline (inverse of publish)
|
|
1788
2299
|
mcp-config Print an MCP server config from stored credentials (no new login)
|
|
1789
|
-
codegen Convert Figma design to local code files (no
|
|
2300
|
+
codegen Convert Figma design to local code files (no artifact)
|
|
1790
2301
|
config Store CLI preferences (e.g. a default API URL)
|
|
1791
2302
|
auth Inspect or manage credentials (status / figma token)
|
|
1792
2303
|
|
|
@@ -1794,6 +2305,7 @@ EXAMPLES (headless flow):
|
|
|
1794
2305
|
npx @animaapp/cli login --invite https://api.agentgrid.io/invite/<code>.md
|
|
1795
2306
|
npx @animaapp/cli create -t import --from ./my-project
|
|
1796
2307
|
npx @animaapp/cli create -t empty --framework react --name "My app"
|
|
2308
|
+
npx @animaapp/cli duplicate <artifactUrl> --name "My copy"
|
|
1797
2309
|
npx @animaapp/cli get-git-token <sessionId>
|
|
1798
2310
|
npx @animaapp/cli update <sessionId> --privacy public
|
|
1799
2311
|
npx @animaapp/cli publish <sessionId> # only on explicit request
|
|
@@ -1806,6 +2318,7 @@ program.addCommand(loginCommand);
|
|
|
1806
2318
|
program.addCommand(logoutCommand);
|
|
1807
2319
|
program.addCommand(listCommand);
|
|
1808
2320
|
program.addCommand(createCommand);
|
|
2321
|
+
program.addCommand(duplicateCommand);
|
|
1809
2322
|
program.addCommand(publishCommand);
|
|
1810
2323
|
program.addCommand(unpublishCommand);
|
|
1811
2324
|
program.addCommand(updateCommand);
|