@animaapp/cli 0.2.1 → 0.3.2
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 +236 -67
- package/dist/index.js +649 -483
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
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 Command10 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/create.ts
|
|
7
7
|
import { Command } from "commander";
|
|
@@ -47,6 +47,16 @@ function deleteCredentials() {
|
|
|
47
47
|
return false;
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
function clearStorage() {
|
|
51
|
+
const had = deleteCredentials();
|
|
52
|
+
try {
|
|
53
|
+
if (fs.existsSync(CONFIG_DIR) && fs.readdirSync(CONFIG_DIR).length === 0) {
|
|
54
|
+
fs.rmdirSync(CONFIG_DIR);
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
return had;
|
|
59
|
+
}
|
|
50
60
|
function getCredentialsPath() {
|
|
51
61
|
return CREDENTIALS_FILE;
|
|
52
62
|
}
|
|
@@ -97,9 +107,6 @@ function validationError(message, detail) {
|
|
|
97
107
|
function apiError(message, detail) {
|
|
98
108
|
return new CliError("API_ERROR", message, EXIT_CODES.API_ERROR, detail);
|
|
99
109
|
}
|
|
100
|
-
function streamError(message) {
|
|
101
|
-
return new CliError("STREAM_ERROR", message, EXIT_CODES.API_ERROR);
|
|
102
|
-
}
|
|
103
110
|
function timeoutError() {
|
|
104
111
|
return new CliError(
|
|
105
112
|
"TIMEOUT",
|
|
@@ -121,139 +128,151 @@ function resolveAuth() {
|
|
|
121
128
|
};
|
|
122
129
|
}
|
|
123
130
|
if (creds) {
|
|
131
|
+
if (creds.tokenType === "agent" && creds.expiresAt && new Date(creds.expiresAt) <= /* @__PURE__ */ new Date()) {
|
|
132
|
+
throw authError("Your agent token has expired. Run: anima login");
|
|
133
|
+
}
|
|
124
134
|
return {
|
|
125
135
|
token: creds.token,
|
|
126
136
|
teamId: creds.teamId,
|
|
127
137
|
figmaToken: process.env.FIGMA_TOKEN || creds.figmaToken,
|
|
128
|
-
source: "file"
|
|
138
|
+
source: "file",
|
|
139
|
+
tokenType: creds.tokenType
|
|
129
140
|
};
|
|
130
141
|
}
|
|
131
|
-
throw authError(
|
|
132
|
-
"No API token found. Set ANIMA_API_TOKEN env var or run: anima auth --token <token>\nGet your token at: https://dev.animaapp.com > Settings > API Keys"
|
|
133
|
-
);
|
|
142
|
+
throw authError("No credentials found. Run: anima login\n(or set the ANIMA_API_TOKEN env var)");
|
|
134
143
|
}
|
|
135
144
|
|
|
136
|
-
// src/api/
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
145
|
+
// src/api/mcp.ts
|
|
146
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
147
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
148
|
+
import { CallToolResultSchema, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
149
|
+
|
|
150
|
+
// src/output/log.ts
|
|
151
|
+
import fs2 from "fs";
|
|
152
|
+
import path2 from "path";
|
|
153
|
+
var logPath = null;
|
|
154
|
+
var REDACT_KEY = /token|authorization|secret|password|_auth|api[-_]?key/i;
|
|
155
|
+
var REDACTED = "***REDACTED***";
|
|
156
|
+
function initLogger(filePath) {
|
|
157
|
+
const target = filePath || process.env.ANIMA_LOG_FILE;
|
|
158
|
+
if (!target || logPath) return;
|
|
159
|
+
logPath = path2.resolve(target);
|
|
160
|
+
fs2.mkdirSync(path2.dirname(logPath), { recursive: true });
|
|
161
|
+
logEvent("log_started", { pid: process.pid, argv: process.argv.slice(2) });
|
|
162
|
+
}
|
|
163
|
+
function logEvent(event, data) {
|
|
164
|
+
if (!logPath) return;
|
|
165
|
+
const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), event, ...data ? redact(data) : {} };
|
|
166
|
+
fs2.appendFileSync(logPath, JSON.stringify(entry) + "\n");
|
|
167
|
+
}
|
|
168
|
+
function redact(value) {
|
|
169
|
+
const out = {};
|
|
170
|
+
for (const [key, val] of Object.entries(value)) {
|
|
171
|
+
if (REDACT_KEY.test(key)) {
|
|
172
|
+
out[key] = REDACTED;
|
|
173
|
+
} else if (val && typeof val === "object" && !Array.isArray(val)) {
|
|
174
|
+
out[key] = redact(val);
|
|
175
|
+
} else {
|
|
176
|
+
out[key] = val;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/version.ts
|
|
183
|
+
import { createRequire } from "module";
|
|
184
|
+
var { version } = createRequire(import.meta.url)("../package.json");
|
|
185
|
+
|
|
186
|
+
// src/api/mcp.ts
|
|
187
|
+
var MCP_PATH = "/v1/mcp";
|
|
188
|
+
var CLIENT_INFO = { name: "anima-cli", version };
|
|
189
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
190
|
+
var AnimaMcpClient = class {
|
|
191
|
+
#client;
|
|
192
|
+
#transport;
|
|
193
|
+
#url;
|
|
194
|
+
constructor(config) {
|
|
195
|
+
const headers = {
|
|
196
|
+
Authorization: `Bearer ${config.token}`
|
|
197
|
+
};
|
|
198
|
+
if (config.teamId) headers["X-Team-Id"] = config.teamId;
|
|
199
|
+
if (config.figmaToken) headers["X-Figma-Token"] = config.figmaToken;
|
|
200
|
+
this.#url = new URL(MCP_PATH, config.apiBaseUrl).toString();
|
|
201
|
+
this.#transport = new StreamableHTTPClientTransport(new URL(this.#url), {
|
|
202
|
+
requestInit: { headers }
|
|
151
203
|
});
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
throw
|
|
161
|
-
|
|
162
|
-
|
|
204
|
+
this.#client = new Client(CLIENT_INFO, { capabilities: {} });
|
|
205
|
+
}
|
|
206
|
+
async connect() {
|
|
207
|
+
logEvent("mcp_connect", { url: this.#url });
|
|
208
|
+
try {
|
|
209
|
+
await this.#client.connect(this.#transport);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
logEvent("mcp_connect_failed", { url: this.#url, error: err?.message });
|
|
212
|
+
throw connectionError(err);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Call a tool and return its parsed result. Anima tools return a single JSON
|
|
217
|
+
* text block; a `{ success: false }` payload is a normal result, so callers
|
|
218
|
+
* still inspect `success` themselves.
|
|
219
|
+
*/
|
|
220
|
+
async callTool(name, args, options = {}) {
|
|
221
|
+
logEvent("mcp_tool_call", { tool: name, arguments: args });
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = await this.#client.callTool(
|
|
225
|
+
{ name, arguments: args },
|
|
226
|
+
CallToolResultSchema,
|
|
227
|
+
{
|
|
228
|
+
timeout: options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
229
|
+
resetTimeoutOnProgress: true,
|
|
230
|
+
onprogress: options.onProgress ? (p) => options.onProgress({ progress: p.progress, total: p.total, message: p.message }) : void 0
|
|
231
|
+
}
|
|
163
232
|
);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
logEvent("mcp_tool_error", { tool: name, error: err?.message });
|
|
235
|
+
if (err?.code === ErrorCode.RequestTimeout) {
|
|
236
|
+
throw timeoutError();
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
164
239
|
}
|
|
165
|
-
|
|
166
|
-
|
|
240
|
+
logEvent("mcp_tool_result", { tool: name, isError: result.isError ?? false });
|
|
241
|
+
const content = Array.isArray(result.content) ? result.content : [];
|
|
242
|
+
const text = content.find((c) => c.type === "text")?.text;
|
|
243
|
+
if (result.isError) {
|
|
244
|
+
throw apiError(text || "MCP tool returned an error");
|
|
167
245
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
case "start":
|
|
176
|
-
result.sessionId = data.sessionId;
|
|
177
|
-
callbacks.onStart?.(data.sessionId);
|
|
178
|
-
break;
|
|
179
|
-
case "progress_messages_updated":
|
|
180
|
-
callbacks.onProgress?.(data.payload.progressMessages);
|
|
181
|
-
break;
|
|
182
|
-
case "generating_code":
|
|
183
|
-
if (data.payload.status === "success") {
|
|
184
|
-
result.files = data.payload.files;
|
|
185
|
-
}
|
|
186
|
-
callbacks.onGeneratingCode?.(data.payload.progress);
|
|
187
|
-
break;
|
|
188
|
-
case "assets_list":
|
|
189
|
-
result.assets = data.payload.assets;
|
|
190
|
-
break;
|
|
191
|
-
case "codegen_completed":
|
|
192
|
-
case "generation_completed":
|
|
193
|
-
callbacks.onCompleted?.();
|
|
194
|
-
break;
|
|
195
|
-
case "error":
|
|
196
|
-
throw apiError(
|
|
197
|
-
`${data.payload.errorName}: ${data.payload.reason}`,
|
|
198
|
-
data.payload
|
|
199
|
-
);
|
|
200
|
-
case "done":
|
|
201
|
-
result.tokenUsage = data.payload.tokenUsage;
|
|
202
|
-
result.sessionId = data.payload.sessionId;
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
return false;
|
|
206
|
-
},
|
|
207
|
-
{ sessionId: "", files: {}, tokenUsage: 0 }
|
|
208
|
-
);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
211
|
-
throw timeoutError();
|
|
246
|
+
if (!text) {
|
|
247
|
+
throw apiError("MCP tool returned no text content");
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(text);
|
|
251
|
+
} catch {
|
|
252
|
+
return text;
|
|
212
253
|
}
|
|
213
|
-
throw err;
|
|
214
|
-
} finally {
|
|
215
|
-
clearTimeout(timer);
|
|
216
254
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
255
|
+
async close() {
|
|
256
|
+
await this.#transport.close().catch(() => {
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
async function withMcp(config, fn) {
|
|
261
|
+
const client = new AnimaMcpClient(config);
|
|
262
|
+
await client.connect();
|
|
223
263
|
try {
|
|
224
|
-
|
|
225
|
-
const { done, value } = await reader.read();
|
|
226
|
-
if (done) break;
|
|
227
|
-
buffer += decoder.decode(value, { stream: true });
|
|
228
|
-
const lines = buffer.split("\n");
|
|
229
|
-
buffer = lines.pop() || "";
|
|
230
|
-
for (const line of lines) {
|
|
231
|
-
if (!line.trim() || line.startsWith(":")) continue;
|
|
232
|
-
if (line.startsWith("data: ")) {
|
|
233
|
-
let data;
|
|
234
|
-
try {
|
|
235
|
-
data = JSON.parse(line.slice(6));
|
|
236
|
-
} catch {
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
const isDone = handler(data, result);
|
|
240
|
-
if (isDone) return result;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
264
|
+
return await fn(client);
|
|
244
265
|
} finally {
|
|
245
|
-
|
|
266
|
+
await client.close();
|
|
246
267
|
}
|
|
247
|
-
throw streamError("Connection closed before receiving 'done' message");
|
|
248
268
|
}
|
|
249
|
-
function
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return headers;
|
|
269
|
+
function connectionError(err) {
|
|
270
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
271
|
+
return new CliError(
|
|
272
|
+
"CONNECTION_ERROR",
|
|
273
|
+
`Could not reach the Anima MCP endpoint: ${message}`,
|
|
274
|
+
1
|
|
275
|
+
);
|
|
257
276
|
}
|
|
258
277
|
|
|
259
278
|
// src/output/formatter.ts
|
|
@@ -409,50 +428,81 @@ function normalizeNodeId(nodeId) {
|
|
|
409
428
|
return nodeId.replace(/-/g, ":");
|
|
410
429
|
}
|
|
411
430
|
|
|
412
|
-
// src/
|
|
431
|
+
// src/config/store.ts
|
|
432
|
+
import fs3 from "fs";
|
|
433
|
+
import path3 from "path";
|
|
434
|
+
import os2 from "os";
|
|
435
|
+
var CONFIG_DIR2 = path3.join(os2.homedir(), ".config", "anima");
|
|
436
|
+
var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
|
|
413
437
|
var DEFAULT_API_URL = "https://public-api.animaapp.com";
|
|
438
|
+
var CONFIG_KEYS = {
|
|
439
|
+
"api-url": "apiUrl"
|
|
440
|
+
};
|
|
441
|
+
function isConfigKey(key) {
|
|
442
|
+
return key in CONFIG_KEYS;
|
|
443
|
+
}
|
|
444
|
+
function configKeys() {
|
|
445
|
+
return Object.keys(CONFIG_KEYS);
|
|
446
|
+
}
|
|
447
|
+
function readConfig() {
|
|
448
|
+
try {
|
|
449
|
+
if (!fs3.existsSync(CONFIG_FILE)) return {};
|
|
450
|
+
const parsed = JSON.parse(fs3.readFileSync(CONFIG_FILE, "utf-8"));
|
|
451
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
452
|
+
} catch {
|
|
453
|
+
return {};
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function setConfig(key, value) {
|
|
457
|
+
const merged = { ...readConfig(), [CONFIG_KEYS[key]]: value };
|
|
458
|
+
fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
459
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + "\n");
|
|
460
|
+
}
|
|
461
|
+
function unsetConfig(key) {
|
|
462
|
+
const config = readConfig();
|
|
463
|
+
const field = CONFIG_KEYS[key];
|
|
464
|
+
if (!(field in config)) return false;
|
|
465
|
+
delete config[field];
|
|
466
|
+
fs3.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
467
|
+
fs3.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n");
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
function getConfigPath() {
|
|
471
|
+
return CONFIG_FILE;
|
|
472
|
+
}
|
|
473
|
+
function resolveApiUrl(flagValue) {
|
|
474
|
+
return flagValue || process.env.ANIMA_API_URL || readConfig().apiUrl || DEFAULT_API_URL;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/commands/create.ts
|
|
414
478
|
var PLAYGROUND_BASE = "https://dev.animaapp.com/chat";
|
|
415
|
-
var createCommand = new Command("create").description("Create app from prompt, URL, or Figma design (3-7 min)").requiredOption("-t, --type <type>", "Generation type: p2c, l2c, or f2c").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("--framework <fw>", "react | html", "react").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("--json", "Output as JSON").option("--timeout <ms>", "Timeout in milliseconds", "600000").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL",
|
|
479
|
+
var createCommand = new Command("create").description("Create app from prompt, URL, or Figma design (3-7 min)").requiredOption("-t, --type <type>", "Generation type: p2c, l2c, or f2c").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("--framework <fw>", "react | html", "react").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("--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) => {
|
|
416
480
|
const mode = detectOutputMode(opts);
|
|
481
|
+
initLogger(opts.logFile);
|
|
417
482
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
418
483
|
try {
|
|
419
484
|
const type = opts.type;
|
|
420
485
|
validateCreateOptions(type, opts);
|
|
421
486
|
const auth = resolveAuth();
|
|
422
|
-
const
|
|
423
|
-
const
|
|
487
|
+
const figmaToken = type === "f2c" ? resolveFigmaToken(opts, auth.figmaToken) : void 0;
|
|
488
|
+
const args = buildCreateArgs(type, opts);
|
|
424
489
|
progress.start(`Creating ${type.toUpperCase()} playground...`);
|
|
425
|
-
const result = await
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
onStart: (sid) => progress.update(`Started (${sid.slice(0, 8)}...)...`),
|
|
432
|
-
onProgress: (msgs) => {
|
|
433
|
-
const running = msgs.find((m) => m.status === "running");
|
|
434
|
-
if (running) progress.update(running.title);
|
|
435
|
-
},
|
|
436
|
-
onGeneratingCode: (pct) => progress.update(`Generating code (${pct}%)...`),
|
|
437
|
-
onCompleted: () => progress.update("Finalizing...")
|
|
438
|
-
},
|
|
439
|
-
parseInt(opts.timeout, 10)
|
|
490
|
+
const result = await withMcp(
|
|
491
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken },
|
|
492
|
+
(client) => client.callTool("playground-create", args, {
|
|
493
|
+
timeoutMs: parseInt(opts.timeout, 10),
|
|
494
|
+
onProgress: (p) => progress.update(p.message ?? `Generating (${Math.round(p.progress)}%)...`)
|
|
495
|
+
})
|
|
440
496
|
);
|
|
441
|
-
|
|
497
|
+
if (!result.success || !result.sessionId) {
|
|
498
|
+
throw apiError(result.reason || result.error || "Generation failed", result);
|
|
499
|
+
}
|
|
500
|
+
const playgroundUrl = result.playgroundUrl || `${PLAYGROUND_BASE}/${result.sessionId}`;
|
|
442
501
|
progress.succeed("Playground created!");
|
|
443
502
|
if (mode === "pretty") {
|
|
444
|
-
printSessionResult({
|
|
445
|
-
sessionId: result.sessionId,
|
|
446
|
-
playgroundUrl,
|
|
447
|
-
tokenUsage: result.tokenUsage
|
|
448
|
-
});
|
|
503
|
+
printSessionResult({ sessionId: result.sessionId, playgroundUrl });
|
|
449
504
|
} else {
|
|
450
|
-
outputResult(mode, {
|
|
451
|
-
success: true,
|
|
452
|
-
sessionId: result.sessionId,
|
|
453
|
-
playgroundUrl,
|
|
454
|
-
tokenUsage: result.tokenUsage
|
|
455
|
-
});
|
|
505
|
+
outputResult(mode, { success: true, sessionId: result.sessionId, playgroundUrl });
|
|
456
506
|
}
|
|
457
507
|
} catch (err) {
|
|
458
508
|
progress.fail("Failed");
|
|
@@ -481,42 +531,32 @@ function validateCreateOptions(type, opts) {
|
|
|
481
531
|
throw validationError("--file-key is required for f2c type");
|
|
482
532
|
}
|
|
483
533
|
}
|
|
484
|
-
function
|
|
534
|
+
function resolveFigmaToken(opts, stored) {
|
|
535
|
+
const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || stored;
|
|
536
|
+
if (!figmaToken) {
|
|
537
|
+
throw validationError(
|
|
538
|
+
"Figma token required for f2c. Use --figma-token, set FIGMA_TOKEN env var, or run: anima auth --figma-token <token>\nGet one at: https://www.figma.com > Settings > Personal access tokens"
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
return figmaToken;
|
|
542
|
+
}
|
|
543
|
+
function buildCreateArgs(type, opts) {
|
|
485
544
|
const language = opts.language ?? (opts.framework === "react" ? "typescript" : void 0);
|
|
486
|
-
const
|
|
545
|
+
const args = {
|
|
546
|
+
type,
|
|
487
547
|
framework: opts.framework,
|
|
488
|
-
|
|
489
|
-
styling: opts.styling,
|
|
490
|
-
uiLibrary: opts.uiLibrary
|
|
548
|
+
styling: opts.styling
|
|
491
549
|
};
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
delete conventions[key];
|
|
495
|
-
}
|
|
496
|
-
});
|
|
550
|
+
if (language) args.language = language;
|
|
551
|
+
if (opts.uiLibrary) args.uiLibrary = opts.uiLibrary;
|
|
497
552
|
switch (type) {
|
|
498
553
|
case "p2c":
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
params: {
|
|
503
|
-
prompt: opts.prompt,
|
|
504
|
-
conventions,
|
|
505
|
-
...opts.guidelines ? { guidelines: opts.guidelines } : {}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
};
|
|
554
|
+
args.prompt = opts.prompt;
|
|
555
|
+
if (opts.guidelines) args.guidelines = opts.guidelines;
|
|
556
|
+
break;
|
|
509
557
|
case "l2c":
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
body: {
|
|
513
|
-
params: {
|
|
514
|
-
input: { type: "url", url: opts.url },
|
|
515
|
-
conventions,
|
|
516
|
-
assetsStorage: { type: "bundled" }
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
};
|
|
558
|
+
args.url = opts.url;
|
|
559
|
+
break;
|
|
520
560
|
case "f2c": {
|
|
521
561
|
const { fileKey, nodeId: parsedNodeId } = parseFigmaUrl(opts.fileKey);
|
|
522
562
|
let nodesId = [];
|
|
@@ -525,123 +565,19 @@ function buildCreateRequest(type, opts, storedFigmaToken) {
|
|
|
525
565
|
} else if (parsedNodeId) {
|
|
526
566
|
nodesId = [parsedNodeId];
|
|
527
567
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
"Figma token required for f2c. Use --figma-token, set FIGMA_TOKEN env var, or run: anima auth --figma-token <token>\nGet one at: https://www.figma.com > Settings > Personal access tokens"
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
return {
|
|
535
|
-
endpoint: "/v1/codegen",
|
|
536
|
-
body: {
|
|
537
|
-
fileKey,
|
|
538
|
-
nodesId,
|
|
539
|
-
figmaToken,
|
|
540
|
-
framework: conventions.framework,
|
|
541
|
-
language: conventions.language,
|
|
542
|
-
styling: conventions.styling,
|
|
543
|
-
uiLibrary: conventions.uiLibrary
|
|
544
|
-
}
|
|
545
|
-
};
|
|
568
|
+
args.fileKey = fileKey;
|
|
569
|
+
args.nodesId = nodesId;
|
|
570
|
+
break;
|
|
546
571
|
}
|
|
547
572
|
}
|
|
573
|
+
return args;
|
|
548
574
|
}
|
|
549
575
|
|
|
550
576
|
// src/commands/publish.ts
|
|
551
577
|
import { Command as Command2 } from "commander";
|
|
552
|
-
|
|
553
|
-
// src/api/mcp-client.ts
|
|
554
|
-
async function callMCPTool(apiBaseUrl, token, teamId, toolName, args) {
|
|
555
|
-
const request = {
|
|
556
|
-
jsonrpc: "2.0",
|
|
557
|
-
id: 1,
|
|
558
|
-
method: "tools/call",
|
|
559
|
-
params: {
|
|
560
|
-
name: toolName,
|
|
561
|
-
arguments: args
|
|
562
|
-
}
|
|
563
|
-
};
|
|
564
|
-
const headers = {
|
|
565
|
-
"Content-Type": "application/json",
|
|
566
|
-
Accept: "application/json, text/event-stream",
|
|
567
|
-
Authorization: `Bearer ${token}`
|
|
568
|
-
};
|
|
569
|
-
if (teamId) {
|
|
570
|
-
headers["X-Team-Id"] = teamId;
|
|
571
|
-
}
|
|
572
|
-
const response = await fetch(`${apiBaseUrl}/v1/mcp`, {
|
|
573
|
-
method: "POST",
|
|
574
|
-
headers,
|
|
575
|
-
body: JSON.stringify(request)
|
|
576
|
-
});
|
|
577
|
-
if (!response.ok) {
|
|
578
|
-
const errorText = await response.text();
|
|
579
|
-
throw apiError(`MCP request failed (${response.status}): ${errorText}`);
|
|
580
|
-
}
|
|
581
|
-
const contentType = response.headers.get("content-type") || "";
|
|
582
|
-
if (contentType.includes("text/event-stream")) {
|
|
583
|
-
return parseMCPSSEResponse(response);
|
|
584
|
-
}
|
|
585
|
-
const data = await response.json();
|
|
586
|
-
return extractMCPResult(data);
|
|
587
|
-
}
|
|
588
|
-
async function parseMCPSSEResponse(response) {
|
|
589
|
-
if (!response.body) {
|
|
590
|
-
throw streamError("Response body is null");
|
|
591
|
-
}
|
|
592
|
-
const reader = response.body.getReader();
|
|
593
|
-
const decoder = new TextDecoder();
|
|
594
|
-
let buffer = "";
|
|
595
|
-
let lastData = null;
|
|
596
|
-
try {
|
|
597
|
-
while (true) {
|
|
598
|
-
const { done, value } = await reader.read();
|
|
599
|
-
if (done) break;
|
|
600
|
-
buffer += decoder.decode(value, { stream: true });
|
|
601
|
-
const lines = buffer.split("\n");
|
|
602
|
-
buffer = lines.pop() || "";
|
|
603
|
-
for (const line of lines) {
|
|
604
|
-
if (!line.trim() || line.startsWith(":")) continue;
|
|
605
|
-
if (line.startsWith("data: ")) {
|
|
606
|
-
try {
|
|
607
|
-
const parsed = JSON.parse(line.slice(6));
|
|
608
|
-
if (parsed.jsonrpc === "2.0") {
|
|
609
|
-
lastData = parsed;
|
|
610
|
-
}
|
|
611
|
-
} catch {
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
} finally {
|
|
617
|
-
reader.cancel();
|
|
618
|
-
}
|
|
619
|
-
if (!lastData) {
|
|
620
|
-
throw apiError("MCP SSE stream ended without a JSON-RPC response");
|
|
621
|
-
}
|
|
622
|
-
return extractMCPResult(lastData);
|
|
623
|
-
}
|
|
624
|
-
function extractMCPResult(data) {
|
|
625
|
-
if (data.error) {
|
|
626
|
-
throw apiError(`MCP tool error: ${data.error.message}`);
|
|
627
|
-
}
|
|
628
|
-
if (!data.result?.content?.length) {
|
|
629
|
-
throw apiError("MCP tool returned empty result");
|
|
630
|
-
}
|
|
631
|
-
const textContent = data.result.content.find((c) => c.type === "text");
|
|
632
|
-
if (!textContent) {
|
|
633
|
-
throw apiError("MCP tool returned no text content");
|
|
634
|
-
}
|
|
635
|
-
try {
|
|
636
|
-
return JSON.parse(textContent.text);
|
|
637
|
-
} catch {
|
|
638
|
-
return textContent.text;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// src/commands/publish.ts
|
|
643
|
-
var publishCommand = new Command2("publish").description("Deploy playground to live URL or npm package (1-3 min)").argument("<sessionId>", "Session ID from create command").option("--mode <mode>", "webapp | designSystem", "webapp").option("--package-name <name>", "NPM package name (designSystem mode)").option("--package-version <version>", "NPM package version (designSystem mode)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL", "https://public-api.animaapp.com").action(async (sessionId, opts) => {
|
|
578
|
+
var publishCommand = new Command2("publish").description("Deploy playground to live URL or npm package (1-3 min)").argument("<sessionId>", "Session ID from create command").option("--mode <mode>", "webapp | designSystem", "webapp").option("--package-name <name>", "NPM package name (designSystem mode)").option("--package-version <version>", "NPM package version (designSystem mode)").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) => {
|
|
644
579
|
const mode = detectOutputMode(opts);
|
|
580
|
+
initLogger(opts.logFile);
|
|
645
581
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
646
582
|
try {
|
|
647
583
|
if (!["webapp", "designSystem"].includes(opts.mode)) {
|
|
@@ -660,12 +596,11 @@ var publishCommand = new Command2("publish").description("Deploy playground to l
|
|
|
660
596
|
if (opts.packageName) args.packageName = opts.packageName;
|
|
661
597
|
if (opts.packageVersion) args.packageVersion = opts.packageVersion;
|
|
662
598
|
progress.start(`Publishing as ${opts.mode}...`);
|
|
663
|
-
const result = await
|
|
664
|
-
opts.apiUrl,
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
args
|
|
599
|
+
const result = await withMcp(
|
|
600
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
601
|
+
(client) => client.callTool("playground-publish", args, {
|
|
602
|
+
onProgress: (p) => progress.update(p.message ?? "Publishing...")
|
|
603
|
+
})
|
|
669
604
|
);
|
|
670
605
|
if (!result.success) {
|
|
671
606
|
throw new CliError(
|
|
@@ -713,20 +648,24 @@ var publishCommand = new Command2("publish").description("Deploy playground to l
|
|
|
713
648
|
import { Command as Command3 } from "commander";
|
|
714
649
|
|
|
715
650
|
// src/utils/files.ts
|
|
716
|
-
import
|
|
717
|
-
import
|
|
651
|
+
import fs4 from "fs";
|
|
652
|
+
import path4 from "path";
|
|
718
653
|
function writeFilesToDisk(files, outputDir) {
|
|
719
|
-
|
|
654
|
+
fs4.mkdirSync(outputDir, { recursive: true });
|
|
720
655
|
let count = 0;
|
|
656
|
+
const resolvedOutput = path4.resolve(outputDir);
|
|
721
657
|
for (const [filePath, fileContent] of Object.entries(files)) {
|
|
722
|
-
const fullPath =
|
|
723
|
-
|
|
724
|
-
|
|
658
|
+
const fullPath = path4.resolve(outputDir, filePath);
|
|
659
|
+
if (!fullPath.startsWith(resolvedOutput + path4.sep)) {
|
|
660
|
+
throw new Error(`Path traversal detected: ${filePath}`);
|
|
661
|
+
}
|
|
662
|
+
const dirPath = path4.dirname(fullPath);
|
|
663
|
+
fs4.mkdirSync(dirPath, { recursive: true });
|
|
725
664
|
if (fileContent.isBinary) {
|
|
726
665
|
const buffer = Buffer.from(fileContent.content, "base64");
|
|
727
|
-
|
|
666
|
+
fs4.writeFileSync(fullPath, buffer);
|
|
728
667
|
} else {
|
|
729
|
-
|
|
668
|
+
fs4.writeFileSync(fullPath, fileContent.content, "utf-8");
|
|
730
669
|
}
|
|
731
670
|
count++;
|
|
732
671
|
}
|
|
@@ -734,9 +673,9 @@ function writeFilesToDisk(files, outputDir) {
|
|
|
734
673
|
}
|
|
735
674
|
|
|
736
675
|
// src/commands/codegen.ts
|
|
737
|
-
var
|
|
738
|
-
var codegenCommand = new Command3("codegen").description("Convert Figma design to code files (no playground)").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 | css | plain_css | css_modules | inline_styles", "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", DEFAULT_API_URL2).action(async (opts) => {
|
|
676
|
+
var codegenCommand = new Command3("codegen").description("Convert Figma design to code files (no playground)").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) => {
|
|
739
677
|
const mode = detectOutputMode(opts);
|
|
678
|
+
initLogger(opts.logFile);
|
|
740
679
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
741
680
|
try {
|
|
742
681
|
const { fileKey, nodeId: parsedNodeId } = parseFigmaUrl(opts.fileKey);
|
|
@@ -752,40 +691,32 @@ var codegenCommand = new Command3("codegen").description("Convert Figma design t
|
|
|
752
691
|
);
|
|
753
692
|
}
|
|
754
693
|
const auth = resolveAuth();
|
|
755
|
-
const figmaToken = opts.figmaToken || auth.figmaToken;
|
|
694
|
+
const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || auth.figmaToken;
|
|
756
695
|
if (!figmaToken) {
|
|
757
696
|
throw validationError(
|
|
758
697
|
"Figma token required. Use --figma-token, set FIGMA_TOKEN env var, or run: anima auth --figma-token <token>\nGet one at: https://www.figma.com > Settings > Personal access tokens"
|
|
759
698
|
);
|
|
760
699
|
}
|
|
761
|
-
const headers = buildHeaders(auth.token, auth.teamId);
|
|
762
700
|
const language = opts.language ?? (opts.framework === "react" ? "typescript" : void 0);
|
|
763
|
-
const
|
|
701
|
+
const args = {
|
|
764
702
|
fileKey,
|
|
765
703
|
nodesId,
|
|
766
|
-
figmaToken,
|
|
767
704
|
framework: opts.framework,
|
|
768
|
-
|
|
769
|
-
styling: opts.styling,
|
|
770
|
-
uiLibrary: opts.uiLibrary
|
|
705
|
+
styling: opts.styling
|
|
771
706
|
};
|
|
707
|
+
if (language) args.language = language;
|
|
708
|
+
if (opts.uiLibrary) args.uiLibrary = opts.uiLibrary;
|
|
772
709
|
progress.start("Generating code from Figma...");
|
|
773
|
-
const result = await
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
onStart: (sid) => progress.update(`Started (${sid.slice(0, 8)}...)...`),
|
|
780
|
-
onProgress: (msgs) => {
|
|
781
|
-
const running = msgs.find((m) => m.status === "running");
|
|
782
|
-
if (running) progress.update(running.title);
|
|
783
|
-
},
|
|
784
|
-
onGeneratingCode: (pct) => progress.update(`Generating code (${pct}%)...`),
|
|
785
|
-
onCompleted: () => progress.update("Writing files...")
|
|
786
|
-
},
|
|
787
|
-
parseInt(opts.timeout, 10)
|
|
710
|
+
const result = await withMcp(
|
|
711
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken },
|
|
712
|
+
(client) => client.callTool("codegen-figma_to_code", args, {
|
|
713
|
+
timeoutMs: parseInt(opts.timeout, 10),
|
|
714
|
+
onProgress: (p) => progress.update(p.message ?? `Generating (${Math.round(p.progress)}%)...`)
|
|
715
|
+
})
|
|
788
716
|
);
|
|
717
|
+
if (!result.success || !result.files) {
|
|
718
|
+
throw apiError(result.reason || result.error || "Code generation failed", result);
|
|
719
|
+
}
|
|
789
720
|
const count = writeFilesToDisk(result.files, opts.output);
|
|
790
721
|
progress.succeed(`Generated ${count} files`);
|
|
791
722
|
if (mode === "pretty") {
|
|
@@ -813,19 +744,11 @@ var codegenCommand = new Command3("codegen").description("Convert Figma design t
|
|
|
813
744
|
}
|
|
814
745
|
});
|
|
815
746
|
|
|
816
|
-
// src/commands/
|
|
747
|
+
// src/commands/getGitToken.ts
|
|
817
748
|
import { Command as Command4 } from "commander";
|
|
818
|
-
import
|
|
819
|
-
import path4 from "path";
|
|
820
|
-
import { pipeline as pipeline2 } from "stream/promises";
|
|
821
|
-
import { createWriteStream } from "fs";
|
|
822
|
-
import { Readable as Readable2 } from "stream";
|
|
749
|
+
import chalk2 from "chalk";
|
|
823
750
|
|
|
824
|
-
// src/utils/
|
|
825
|
-
import fs3 from "fs";
|
|
826
|
-
import path3 from "path";
|
|
827
|
-
import { pipeline } from "stream/promises";
|
|
828
|
-
import { Readable } from "stream";
|
|
751
|
+
// src/utils/playgroundUrl.ts
|
|
829
752
|
function parsePlaygroundUrl(input) {
|
|
830
753
|
if (!input.includes("/")) {
|
|
831
754
|
return input;
|
|
@@ -843,66 +766,61 @@ function parsePlaygroundUrl(input) {
|
|
|
843
766
|
}
|
|
844
767
|
}
|
|
845
768
|
|
|
846
|
-
// src/
|
|
847
|
-
|
|
848
|
-
|
|
769
|
+
// src/utils/gitToken.ts
|
|
770
|
+
function parseGitTokenResponse(text) {
|
|
771
|
+
const denied = text.match(/^Error \((\w+)\): ([\s\S]*)$/);
|
|
772
|
+
if (denied) {
|
|
773
|
+
throw apiError(`Git access ${denied[1]}: ${denied[2].trim()}`);
|
|
774
|
+
}
|
|
775
|
+
const url = text.match(/git clone (\S+)/)?.[1];
|
|
776
|
+
if (!url) {
|
|
777
|
+
throw apiError(`Unexpected response from project-get_git_token: ${text.slice(0, 200)}`);
|
|
778
|
+
}
|
|
779
|
+
const access = text.match(/\*\*(ro|rw)\*\*/)?.[1];
|
|
780
|
+
const expiresAt = text.match(/expires at (\S+)/)?.[1];
|
|
781
|
+
return { gitRemoteUrl: url, access: access ?? "ro", expiresAt };
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/commands/getGitToken.ts
|
|
785
|
+
var getGitTokenCommand = new Command4("get-git-token").description("Mint a short-lived git remote URL for a playground (clone/push with git)").argument("<playgroundUrl>", "Playground 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 (playgroundUrl, opts) => {
|
|
849
786
|
const mode = detectOutputMode(opts);
|
|
787
|
+
initLogger(opts.logFile);
|
|
850
788
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
851
789
|
try {
|
|
852
790
|
const sessionId = parsePlaygroundUrl(playgroundUrl);
|
|
853
791
|
const auth = resolveAuth();
|
|
854
|
-
progress.start("Requesting
|
|
855
|
-
const
|
|
856
|
-
opts.apiUrl,
|
|
857
|
-
|
|
858
|
-
auth.teamId,
|
|
859
|
-
"project-download_from_playground",
|
|
860
|
-
{ playgroundUrl: `https://dev.animaapp.com/chat/${sessionId}` }
|
|
861
|
-
);
|
|
862
|
-
if (!result.success) {
|
|
863
|
-
throw apiError(
|
|
864
|
-
result.error || "Download failed",
|
|
865
|
-
result
|
|
866
|
-
);
|
|
867
|
-
}
|
|
868
|
-
const downloadUrl = result.downloadUrl;
|
|
869
|
-
const fileCount = result.fileCount;
|
|
870
|
-
if (!downloadUrl) {
|
|
871
|
-
throw apiError("No download URL returned");
|
|
872
|
-
}
|
|
873
|
-
progress.update(`Downloading zip (${fileCount} files)...`);
|
|
874
|
-
const zipPath = path4.join(opts.output, "__download.zip");
|
|
875
|
-
fs4.mkdirSync(opts.output, { recursive: true });
|
|
876
|
-
const zipResponse = await fetch(downloadUrl);
|
|
877
|
-
if (!zipResponse.ok || !zipResponse.body) {
|
|
878
|
-
throw apiError(`Failed to download zip: ${zipResponse.status}`);
|
|
879
|
-
}
|
|
880
|
-
await pipeline2(
|
|
881
|
-
Readable2.fromWeb(zipResponse.body),
|
|
882
|
-
createWriteStream(zipPath)
|
|
792
|
+
progress.start("Requesting git access...");
|
|
793
|
+
const response = await withMcp(
|
|
794
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
795
|
+
(client) => client.callTool("project-get_git_token", { session_id: sessionId })
|
|
883
796
|
);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
execSync(`unzip -o -q "${zipPath}" -d "${opts.output}"`, {
|
|
888
|
-
stdio: "pipe"
|
|
889
|
-
});
|
|
890
|
-
} catch {
|
|
891
|
-
execSync(`tar -xf "${zipPath}" -C "${opts.output}"`, {
|
|
892
|
-
stdio: "pipe"
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
fs4.unlinkSync(zipPath);
|
|
896
|
-
const count = countFiles(opts.output);
|
|
897
|
-
progress.succeed(`Downloaded ${count} files`);
|
|
797
|
+
const grant = parseGitTokenResponse(response);
|
|
798
|
+
logEvent("git_token_granted", { sessionId, access: grant.access, expiresAt: grant.expiresAt });
|
|
799
|
+
progress.succeed(`Git access granted (${grant.access === "rw" ? "read-write" : "read-only"})`);
|
|
898
800
|
if (mode === "pretty") {
|
|
899
|
-
|
|
801
|
+
console.log();
|
|
802
|
+
console.log(` ${chalk2.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
|
|
803
|
+
console.log();
|
|
804
|
+
if (grant.access === "rw") {
|
|
805
|
+
console.log(" Commit and push to update the live playground.");
|
|
806
|
+
} else {
|
|
807
|
+
console.log(" Read-only: cloning works, pushing will be rejected.");
|
|
808
|
+
}
|
|
809
|
+
if (grant.expiresAt) {
|
|
810
|
+
console.log(
|
|
811
|
+
` The URL embeds a token that expires at ${grant.expiresAt} and cannot be renewed;
|
|
812
|
+
re-run this command and ${chalk2.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
|
|
816
|
+
console.log();
|
|
900
817
|
} else {
|
|
901
818
|
outputResult(mode, {
|
|
902
819
|
success: true,
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
820
|
+
sessionId,
|
|
821
|
+
gitRemoteUrl: grant.gitRemoteUrl,
|
|
822
|
+
access: grant.access,
|
|
823
|
+
expiresAt: grant.expiresAt
|
|
906
824
|
});
|
|
907
825
|
}
|
|
908
826
|
} catch (err) {
|
|
@@ -918,71 +836,240 @@ var downloadCommand = new Command4("download").description("Download project fro
|
|
|
918
836
|
throw err;
|
|
919
837
|
}
|
|
920
838
|
});
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
839
|
+
|
|
840
|
+
// src/commands/mcpConfig.ts
|
|
841
|
+
import { Command as Command6 } from "commander";
|
|
842
|
+
|
|
843
|
+
// src/commands/login.ts
|
|
844
|
+
import { Command as Command5 } from "commander";
|
|
845
|
+
import chalk3 from "chalk";
|
|
846
|
+
import open from "open";
|
|
847
|
+
|
|
848
|
+
// src/auth/device.ts
|
|
849
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
850
|
+
var DEFAULT_CLIENT_NAME = "Anima CLI";
|
|
851
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
852
|
+
function decodeJwtPayload(token) {
|
|
853
|
+
try {
|
|
854
|
+
const parts = token.split(".");
|
|
855
|
+
if (parts.length !== 3) return void 0;
|
|
856
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
857
|
+
} catch {
|
|
858
|
+
return void 0;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function loginWithDeviceFlow(options) {
|
|
862
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
863
|
+
const sleep = options.sleepFn ?? defaultSleep;
|
|
864
|
+
const base = options.apiUrl.replace(/\/$/, "");
|
|
865
|
+
const startUrl = `${base}/oauth/device_authorization`;
|
|
866
|
+
logEvent("device_authorization_request", { url: startUrl });
|
|
867
|
+
const startResponse = await fetchFn(startUrl, {
|
|
868
|
+
method: "POST",
|
|
869
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
870
|
+
body: new URLSearchParams({ client_name: options.clientName ?? DEFAULT_CLIENT_NAME }).toString()
|
|
871
|
+
});
|
|
872
|
+
logEvent("device_authorization_response", { url: startUrl, status: startResponse.status });
|
|
873
|
+
if (!startResponse.ok) {
|
|
874
|
+
const detail = await startResponse.text().catch(() => "");
|
|
875
|
+
logEvent("device_authorization_failed", { url: startUrl, status: startResponse.status, body: detail.slice(0, 500) });
|
|
876
|
+
const hint = startResponse.status === 404 ? ` \u2014 no device endpoint at ${startUrl}. Check --api-url points to an API that has the device grant deployed.` : "";
|
|
877
|
+
throw new CliError(
|
|
878
|
+
"API_ERROR",
|
|
879
|
+
`Device authorization failed (HTTP ${startResponse.status})${hint}`,
|
|
880
|
+
EXIT_CODES.API_ERROR
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const start = await startResponse.json();
|
|
884
|
+
options.onVerification({
|
|
885
|
+
userCode: start.user_code,
|
|
886
|
+
verificationUri: start.verification_uri,
|
|
887
|
+
verificationUriComplete: start.verification_uri_complete,
|
|
888
|
+
expiresInSeconds: start.expires_in
|
|
889
|
+
});
|
|
890
|
+
let intervalMs = Math.max(1, start.interval) * 1e3;
|
|
891
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
892
|
+
while (Date.now() < deadline) {
|
|
893
|
+
await sleep(intervalMs);
|
|
894
|
+
const pollResponse = await fetchFn(`${base}/oauth/token`, {
|
|
895
|
+
method: "POST",
|
|
896
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
897
|
+
body: new URLSearchParams({
|
|
898
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
899
|
+
device_code: start.device_code
|
|
900
|
+
}).toString()
|
|
901
|
+
});
|
|
902
|
+
const body = await pollResponse.json();
|
|
903
|
+
logEvent("device_token_poll", { status: pollResponse.status, error: body.error });
|
|
904
|
+
if (pollResponse.ok && typeof body.access_token === "string") {
|
|
905
|
+
const token = body.access_token;
|
|
906
|
+
const claims = decodeJwtPayload(token);
|
|
907
|
+
const result = {
|
|
908
|
+
token,
|
|
909
|
+
teamId: typeof claims?.team_id === "string" ? claims.team_id : void 0,
|
|
910
|
+
agentId: typeof claims?.agent_id === "string" ? claims.agent_id : void 0,
|
|
911
|
+
expiresAt: typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0
|
|
912
|
+
};
|
|
913
|
+
writeCredentials({
|
|
914
|
+
token: result.token,
|
|
915
|
+
teamId: result.teamId,
|
|
916
|
+
tokenType: "agent",
|
|
917
|
+
agentId: result.agentId,
|
|
918
|
+
expiresAt: result.expiresAt
|
|
919
|
+
});
|
|
920
|
+
return result;
|
|
921
|
+
}
|
|
922
|
+
switch (body.error) {
|
|
923
|
+
case "authorization_pending":
|
|
924
|
+
continue;
|
|
925
|
+
case "slow_down":
|
|
926
|
+
intervalMs += 5e3;
|
|
927
|
+
continue;
|
|
928
|
+
case "access_denied":
|
|
929
|
+
throw authError("The authorization request was denied.");
|
|
930
|
+
case "expired_token":
|
|
931
|
+
throw authError("The device code expired before approval. Run the login again.");
|
|
932
|
+
default:
|
|
933
|
+
throw new CliError(
|
|
934
|
+
"API_ERROR",
|
|
935
|
+
`Unexpected token response: ${body.error ?? `HTTP ${pollResponse.status}`}`,
|
|
936
|
+
EXIT_CODES.API_ERROR
|
|
937
|
+
);
|
|
928
938
|
}
|
|
929
939
|
}
|
|
930
|
-
|
|
940
|
+
throw authError("The device code expired before approval. Run the login again.");
|
|
931
941
|
}
|
|
932
942
|
|
|
933
|
-
// src/commands/
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
943
|
+
// src/commands/login.ts
|
|
944
|
+
var loginCommand = new Command5("login").description("Log in via the device flow \u2014 approve in a browser on any device").option("--client-name <name>", "How this CLI appears on the consent screen", "Anima 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(
|
|
945
|
+
"--print-mcp-config",
|
|
946
|
+
"After login, also print an MCP server config (includes the token) to paste into an MCP-capable agent"
|
|
947
|
+
).action(async (opts) => {
|
|
948
|
+
const mode = detectOutputMode(opts);
|
|
949
|
+
initLogger(opts.logFile);
|
|
950
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
951
|
+
try {
|
|
952
|
+
const result = await loginWithDeviceFlow({
|
|
953
|
+
apiUrl,
|
|
954
|
+
clientName: opts.clientName,
|
|
955
|
+
onVerification: (info) => {
|
|
956
|
+
if (mode === "pretty") {
|
|
957
|
+
console.log();
|
|
958
|
+
printInfo("To connect, open the verification page and enter this code:");
|
|
959
|
+
console.log();
|
|
960
|
+
console.log(` ${chalk3.cyan.underline(info.verificationUri)}`);
|
|
961
|
+
console.log();
|
|
962
|
+
console.log(` ${chalk3.bold.white(info.userCode)}`);
|
|
963
|
+
console.log();
|
|
964
|
+
printInfo(`Waiting for approval\u2026 (code expires in ${Math.round(info.expiresInSeconds / 60)} minutes)`);
|
|
965
|
+
} else {
|
|
966
|
+
process.stderr.write(
|
|
967
|
+
JSON.stringify({
|
|
968
|
+
event: "verification_required",
|
|
969
|
+
verificationUri: info.verificationUri,
|
|
970
|
+
verificationUriComplete: info.verificationUriComplete,
|
|
971
|
+
userCode: info.userCode,
|
|
972
|
+
expiresIn: info.expiresInSeconds
|
|
973
|
+
}) + "\n"
|
|
953
974
|
);
|
|
954
|
-
server.close();
|
|
955
|
-
reject(new Error("No token received from OAuth flow"));
|
|
956
|
-
return;
|
|
957
975
|
}
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
);
|
|
963
|
-
server.close();
|
|
964
|
-
resolve({ token, teamId });
|
|
976
|
+
if (mode === "pretty" && opts.open) {
|
|
977
|
+
open(info.verificationUriComplete).catch(() => {
|
|
978
|
+
});
|
|
979
|
+
}
|
|
965
980
|
}
|
|
966
981
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
982
|
+
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl, result.token) : void 0;
|
|
983
|
+
if (mode === "pretty") {
|
|
984
|
+
printSuccess("Connected!");
|
|
985
|
+
printKeyValue("Credentials", getCredentialsPath());
|
|
986
|
+
if (result.agentId) printKeyValue("Agent", result.agentId);
|
|
987
|
+
if (result.teamId) printKeyValue("Team ID", result.teamId);
|
|
988
|
+
if (result.expiresAt) printKeyValue("Token expires", result.expiresAt);
|
|
989
|
+
printInfo("This is a scoped agent token \u2014 it acts within what you approved, and your team can revoke it.");
|
|
990
|
+
if (mcpConfig) {
|
|
991
|
+
console.log();
|
|
992
|
+
printInfo("MCP server config (token expires ~7 days \u2014 re-run `anima login` to refresh):");
|
|
993
|
+
console.log();
|
|
994
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
outputResult(mode, {
|
|
998
|
+
success: true,
|
|
999
|
+
action: "logged_in",
|
|
1000
|
+
tokenType: "agent",
|
|
1001
|
+
agentId: result.agentId,
|
|
1002
|
+
teamId: result.teamId,
|
|
1003
|
+
expiresAt: result.expiresAt,
|
|
1004
|
+
...mcpConfig ? { mcpConfig } : {}
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
} catch (err) {
|
|
1008
|
+
if (err instanceof CliError) {
|
|
1009
|
+
if (mode === "pretty") {
|
|
1010
|
+
printError(err.message);
|
|
1011
|
+
} else {
|
|
1012
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1013
|
+
}
|
|
1014
|
+
process.exit(err.exitCode);
|
|
1015
|
+
}
|
|
1016
|
+
throw err;
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
function buildMcpConfig(apiUrl, token) {
|
|
1020
|
+
return {
|
|
1021
|
+
mcpServers: {
|
|
1022
|
+
anima: {
|
|
1023
|
+
type: "http",
|
|
1024
|
+
url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`,
|
|
1025
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
981
1029
|
}
|
|
982
1030
|
|
|
1031
|
+
// src/commands/mcpConfig.ts
|
|
1032
|
+
var mcpConfigCommand = new Command6("mcp-config").description("Print an MCP server config from your stored credentials (run `login` first)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").action(async (opts) => {
|
|
1033
|
+
const mode = detectOutputMode(opts);
|
|
1034
|
+
try {
|
|
1035
|
+
const auth = resolveAuth();
|
|
1036
|
+
const creds = readCredentials();
|
|
1037
|
+
const expiresAt = auth.source === "env" ? void 0 : creds?.expiresAt;
|
|
1038
|
+
const mcpConfig = buildMcpConfig(resolveApiUrl(opts.apiUrl), auth.token);
|
|
1039
|
+
if (mode === "pretty") {
|
|
1040
|
+
printInfo("MCP server config (add to your MCP client; the token is a secret):");
|
|
1041
|
+
console.log();
|
|
1042
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
1043
|
+
console.log();
|
|
1044
|
+
if (expiresAt) {
|
|
1045
|
+
printInfo(`Token expires ${expiresAt} \u2014 run \`anima login\` then \`anima mcp-config\` to refresh.`);
|
|
1046
|
+
}
|
|
1047
|
+
} else {
|
|
1048
|
+
outputResult(mode, {
|
|
1049
|
+
success: true,
|
|
1050
|
+
mcpConfig,
|
|
1051
|
+
...expiresAt ? { expiresAt } : {},
|
|
1052
|
+
...creds?.agentId && auth.source !== "env" ? { agentId: creds.agentId } : {}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
} catch (err) {
|
|
1056
|
+
if (err instanceof CliError) {
|
|
1057
|
+
if (mode === "pretty") {
|
|
1058
|
+
console.error(err.message);
|
|
1059
|
+
} else {
|
|
1060
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1061
|
+
}
|
|
1062
|
+
process.exit(err.exitCode);
|
|
1063
|
+
}
|
|
1064
|
+
throw err;
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
983
1068
|
// src/commands/auth.ts
|
|
984
|
-
|
|
985
|
-
|
|
1069
|
+
import { Command as Command7 } from "commander";
|
|
1070
|
+
import chalk4 from "chalk";
|
|
1071
|
+
var DEFAULT_API_URL2 = "https://public-api.animaapp.com";
|
|
1072
|
+
var authCommand = new Command7("auth").description("Manage authentication").option("--token <token>", "Store Anima 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) => {
|
|
986
1073
|
const mode = detectOutputMode(opts);
|
|
987
1074
|
try {
|
|
988
1075
|
if (opts.token || opts.figmaToken) {
|
|
@@ -1015,22 +1102,8 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1015
1102
|
}
|
|
1016
1103
|
return;
|
|
1017
1104
|
}
|
|
1018
|
-
if (opts.login) {
|
|
1019
|
-
if (mode === "pretty") {
|
|
1020
|
-
printInfo("Opening browser for authentication...");
|
|
1021
|
-
}
|
|
1022
|
-
const result = await loginWithBrowser();
|
|
1023
|
-
if (mode === "pretty") {
|
|
1024
|
-
printSuccess("Authenticated successfully!");
|
|
1025
|
-
printKeyValue("Token", result.token.slice(0, 8) + "...");
|
|
1026
|
-
if (result.teamId) printKeyValue("Team ID", result.teamId);
|
|
1027
|
-
} else {
|
|
1028
|
-
outputResult(mode, { success: true, action: "logged_in" });
|
|
1029
|
-
}
|
|
1030
|
-
return;
|
|
1031
|
-
}
|
|
1032
1105
|
if (opts.logout) {
|
|
1033
|
-
const deleted =
|
|
1106
|
+
const deleted = clearStorage();
|
|
1034
1107
|
if (mode === "pretty") {
|
|
1035
1108
|
if (deleted) {
|
|
1036
1109
|
printSuccess("Credentials cleared");
|
|
@@ -1052,14 +1125,18 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1052
1125
|
printSuccess("Token configured");
|
|
1053
1126
|
printKeyValue("Source", sourceName);
|
|
1054
1127
|
printKeyValue("Anima token", auth.token.slice(0, 12) + "..." + auth.token.slice(-4));
|
|
1128
|
+
printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "personal/API");
|
|
1129
|
+
if (creds?.tokenType === "agent" && creds.expiresAt) printKeyValue("Expires", creds.expiresAt);
|
|
1055
1130
|
if (auth.teamId) printKeyValue("Team ID", auth.teamId);
|
|
1056
1131
|
printKeyValue("Figma token", hasFigma ? "configured" : "not set (needed for codegen/f2c)");
|
|
1057
1132
|
} else {
|
|
1058
1133
|
outputResult(mode, {
|
|
1059
1134
|
success: true,
|
|
1060
1135
|
source: auth.source,
|
|
1136
|
+
tokenType: auth.tokenType ?? "personal",
|
|
1061
1137
|
tokenPreview: auth.token.slice(0, 12) + "..." + auth.token.slice(-4),
|
|
1062
1138
|
teamId: auth.teamId,
|
|
1139
|
+
expiresAt: creds?.tokenType === "agent" ? creds.expiresAt : void 0,
|
|
1063
1140
|
figmaToken: hasFigma
|
|
1064
1141
|
});
|
|
1065
1142
|
}
|
|
@@ -1079,20 +1156,17 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1079
1156
|
if (mode === "pretty") {
|
|
1080
1157
|
console.log();
|
|
1081
1158
|
printInfo("Auth commands:");
|
|
1082
|
-
console.log(` ${
|
|
1083
|
-
console.log(` ${
|
|
1084
|
-
console.log(` ${
|
|
1085
|
-
console.log(` ${
|
|
1086
|
-
console.log(` ${chalk2.cyan("anima auth --status")} Check auth status`);
|
|
1087
|
-
console.log(` ${chalk2.cyan("anima auth --logout")} Clear all credentials`);
|
|
1159
|
+
console.log(` ${chalk4.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
|
|
1160
|
+
console.log(` ${chalk4.cyan("anima logout")} Clear all credentials`);
|
|
1161
|
+
console.log(` ${chalk4.cyan("anima auth --status")} Check auth status`);
|
|
1162
|
+
console.log(` ${chalk4.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
|
|
1088
1163
|
console.log();
|
|
1089
|
-
console.log(`
|
|
1090
|
-
console.log(` Figma token: ${chalk2.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
1164
|
+
console.log(` Figma token: ${chalk4.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
1091
1165
|
console.log();
|
|
1092
1166
|
} else {
|
|
1093
1167
|
outputResult(mode, {
|
|
1094
1168
|
success: true,
|
|
1095
|
-
message: "
|
|
1169
|
+
message: "Run `anima login`, or use --status / --logout / --figma-token"
|
|
1096
1170
|
});
|
|
1097
1171
|
}
|
|
1098
1172
|
} catch (err) {
|
|
@@ -1108,44 +1182,136 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1108
1182
|
}
|
|
1109
1183
|
});
|
|
1110
1184
|
|
|
1185
|
+
// src/commands/logout.ts
|
|
1186
|
+
import { Command as Command8 } from "commander";
|
|
1187
|
+
var logoutCommand = new Command8("logout").description("Clear all stored credentials (Anima + Figma tokens) from this machine").option("--json", "Output as JSON").action((opts) => {
|
|
1188
|
+
const mode = detectOutputMode(opts);
|
|
1189
|
+
const hadCredentials = clearStorage();
|
|
1190
|
+
if (mode === "pretty") {
|
|
1191
|
+
if (hadCredentials) {
|
|
1192
|
+
printSuccess("Logged out \u2014 all credentials cleared from " + getCredentialsPath());
|
|
1193
|
+
} else {
|
|
1194
|
+
printInfo("No stored credentials found");
|
|
1195
|
+
}
|
|
1196
|
+
} else {
|
|
1197
|
+
outputResult(mode, { success: true, action: "logged_out", hadCredentials });
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1201
|
+
// src/commands/config.ts
|
|
1202
|
+
import { Command as Command9 } from "commander";
|
|
1203
|
+
var configCommand = new Command9("config").description(
|
|
1204
|
+
`Store CLI preferences (keys: ${configKeys().join(", ")})`
|
|
1205
|
+
);
|
|
1206
|
+
configCommand.command("set <key> <value>").description("Set a config value").option("--json", "Output as JSON").action((key, value, opts) => {
|
|
1207
|
+
const mode = detectOutputMode(opts);
|
|
1208
|
+
assertKey(key);
|
|
1209
|
+
if (key === "api-url") assertUrl(value);
|
|
1210
|
+
setConfig(key, value);
|
|
1211
|
+
if (mode === "pretty") printSuccess(`Set ${key} = ${value}`);
|
|
1212
|
+
else outputResult(mode, { success: true, action: "config_set", key, value });
|
|
1213
|
+
});
|
|
1214
|
+
configCommand.command("get <key>").description("Print a config value").option("--json", "Output as JSON").action((key, opts) => {
|
|
1215
|
+
const mode = detectOutputMode(opts);
|
|
1216
|
+
assertKey(key);
|
|
1217
|
+
const value = readConfig()[CONFIG_KEYS[key]];
|
|
1218
|
+
if (mode === "pretty") {
|
|
1219
|
+
if (value) console.log(value);
|
|
1220
|
+
else printInfo(`${key} is not set`);
|
|
1221
|
+
} else {
|
|
1222
|
+
outputResult(mode, { success: true, key, value: value ?? null });
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
configCommand.command("list").alias("ls").description("Show all config values and where the API URL resolves from").option("--json", "Output as JSON").action((opts) => {
|
|
1226
|
+
const mode = detectOutputMode(opts);
|
|
1227
|
+
const config = readConfig();
|
|
1228
|
+
const resolvedApiUrl = resolveApiUrl();
|
|
1229
|
+
if (mode === "pretty") {
|
|
1230
|
+
printInfo("Config: " + getConfigPath());
|
|
1231
|
+
printKeyValue("api-url", config.apiUrl ?? `(default) ${DEFAULT_API_URL}`);
|
|
1232
|
+
if (process.env.ANIMA_API_URL) {
|
|
1233
|
+
printInfo(`ANIMA_API_URL env is set \u2014 it overrides config (effective: ${resolvedApiUrl})`);
|
|
1234
|
+
}
|
|
1235
|
+
} else {
|
|
1236
|
+
outputResult(mode, {
|
|
1237
|
+
success: true,
|
|
1238
|
+
path: getConfigPath(),
|
|
1239
|
+
config,
|
|
1240
|
+
resolved: { apiUrl: resolvedApiUrl }
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
configCommand.command("unset <key>").description("Remove a config value").option("--json", "Output as JSON").action((key, opts) => {
|
|
1245
|
+
const mode = detectOutputMode(opts);
|
|
1246
|
+
assertKey(key);
|
|
1247
|
+
const removed = unsetConfig(key);
|
|
1248
|
+
if (mode === "pretty") {
|
|
1249
|
+
if (removed) printSuccess(`Unset ${key}`);
|
|
1250
|
+
else printInfo(`${key} was not set`);
|
|
1251
|
+
} else {
|
|
1252
|
+
outputResult(mode, { success: true, action: "config_unset", key, removed });
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
function assertKey(key) {
|
|
1256
|
+
if (!isConfigKey(key)) {
|
|
1257
|
+
throw validationError(`Unknown config key "${key}". Valid keys: ${configKeys().join(", ")}`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
function assertUrl(value) {
|
|
1261
|
+
try {
|
|
1262
|
+
new URL(value);
|
|
1263
|
+
} catch {
|
|
1264
|
+
throw validationError(`"${value}" is not a valid URL`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1111
1268
|
// src/index.ts
|
|
1112
|
-
var program = new
|
|
1269
|
+
var program = new Command10().name("anima").description(
|
|
1113
1270
|
`Anima CLI - Generate production-ready apps from prompts, URLs, or Figma designs.
|
|
1114
1271
|
|
|
1115
1272
|
AUTHENTICATION:
|
|
1116
|
-
|
|
1117
|
-
|
|
1273
|
+
Run: npx @animaapp/cli login
|
|
1274
|
+
Approve in a browser on any device \u2014 the CLI receives a scoped agent token
|
|
1275
|
+
(stored in ~/.config/anima/). CI/automation can set ANIMA_API_TOKEN instead.
|
|
1118
1276
|
|
|
1119
1277
|
TIMING (important for AI agents):
|
|
1120
|
-
create
|
|
1121
|
-
publish
|
|
1122
|
-
codegen
|
|
1123
|
-
|
|
1278
|
+
create \u2192 3-7 minutes (keep the default 600000ms timeout)
|
|
1279
|
+
publish \u2192 1-3 minutes
|
|
1280
|
+
codegen \u2192 2-5 minutes
|
|
1281
|
+
get-git-token \u2192 seconds
|
|
1124
1282
|
The --timeout default is 600000ms (10 min). Do NOT reduce it.
|
|
1125
1283
|
|
|
1126
1284
|
COMMANDS:
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1285
|
+
login Log in via the device flow (no browser needed on this machine)
|
|
1286
|
+
logout Clear all stored credentials from this machine
|
|
1287
|
+
mcp-config Print an MCP server config from stored credentials (no new login)
|
|
1288
|
+
create Create app from prompt, URL, or Figma design
|
|
1289
|
+
publish Deploy playground to live URL or npm package
|
|
1290
|
+
codegen Convert Figma design to code files (no playground)
|
|
1291
|
+
get-git-token Mint a git remote URL for a playground; clone/edit/push with git
|
|
1292
|
+
config Store CLI preferences (e.g. a default API URL)
|
|
1293
|
+
auth Inspect or manage credentials (status / figma token)
|
|
1132
1294
|
|
|
1133
1295
|
EXAMPLES:
|
|
1134
|
-
npx @animaapp/cli
|
|
1296
|
+
npx @animaapp/cli login
|
|
1135
1297
|
npx @animaapp/cli create -t p2c -p "SaaS dashboard with sidebar and analytics"
|
|
1136
1298
|
npx @animaapp/cli create -t l2c -u https://stripe.com
|
|
1137
1299
|
npx @animaapp/cli create -t f2c --file-key abc --nodes 42:15
|
|
1138
1300
|
npx @animaapp/cli publish abc123xyz
|
|
1139
1301
|
npx @animaapp/cli codegen --file-key abc --nodes 42:15 -o ./components
|
|
1140
|
-
npx @animaapp/cli
|
|
1302
|
+
npx @animaapp/cli get-git-token https://dev.animaapp.com/chat/abc123xyz
|
|
1141
1303
|
|
|
1142
1304
|
OUTPUT: JSON to stdout when piped (for AI tools), pretty text in terminals.
|
|
1143
1305
|
Progress is streamed to stderr via --verbose when in JSON mode.`
|
|
1144
|
-
).version(
|
|
1306
|
+
).version(version, "-v, --version");
|
|
1307
|
+
program.addCommand(loginCommand);
|
|
1308
|
+
program.addCommand(logoutCommand);
|
|
1145
1309
|
program.addCommand(createCommand);
|
|
1146
1310
|
program.addCommand(publishCommand);
|
|
1147
1311
|
program.addCommand(codegenCommand);
|
|
1148
|
-
program.addCommand(
|
|
1312
|
+
program.addCommand(getGitTokenCommand);
|
|
1313
|
+
program.addCommand(mcpConfigCommand);
|
|
1314
|
+
program.addCommand(configCommand);
|
|
1149
1315
|
program.addCommand(authCommand);
|
|
1150
1316
|
program.parseAsync(process.argv).catch((err) => {
|
|
1151
1317
|
if (err instanceof CliError) {
|