@animaapp/cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -68
- package/dist/index.js +615 -491
- 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 Command9 } 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}` }
|
|
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 })
|
|
861
796
|
);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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)
|
|
883
|
-
);
|
|
884
|
-
progress.update("Extracting...");
|
|
885
|
-
const { execSync } = await import("child_process");
|
|
886
|
-
try {
|
|
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,12 @@ var downloadCommand = new Command4("download").description("Download project fro
|
|
|
918
836
|
throw err;
|
|
919
837
|
}
|
|
920
838
|
});
|
|
921
|
-
function countFiles(dir) {
|
|
922
|
-
let count = 0;
|
|
923
|
-
for (const entry of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
924
|
-
if (entry.isDirectory()) {
|
|
925
|
-
count += countFiles(path4.join(dir, entry.name));
|
|
926
|
-
} else {
|
|
927
|
-
count++;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return count;
|
|
931
|
-
}
|
|
932
839
|
|
|
933
840
|
// src/commands/auth.ts
|
|
934
841
|
import { Command as Command5 } from "commander";
|
|
935
|
-
import
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
import http from "http";
|
|
939
|
-
import open from "open";
|
|
940
|
-
var OAUTH_PORT = 19284;
|
|
941
|
-
var OAUTH_BASE = "https://dev.animaapp.com";
|
|
942
|
-
async function loginWithBrowser() {
|
|
943
|
-
return new Promise((resolve, reject) => {
|
|
944
|
-
const server = http.createServer((req, res) => {
|
|
945
|
-
const url = new URL(req.url, `http://localhost:${OAUTH_PORT}`);
|
|
946
|
-
if (url.pathname === "/callback") {
|
|
947
|
-
const token = url.searchParams.get("token");
|
|
948
|
-
const teamId = url.searchParams.get("teamId") ?? void 0;
|
|
949
|
-
if (!token) {
|
|
950
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
951
|
-
res.end(
|
|
952
|
-
"<html><body><h1>Error</h1><p>No token received. Please try again.</p></body></html>"
|
|
953
|
-
);
|
|
954
|
-
server.close();
|
|
955
|
-
reject(new Error("No token received from OAuth flow"));
|
|
956
|
-
return;
|
|
957
|
-
}
|
|
958
|
-
writeCredentials({ token, teamId });
|
|
959
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
960
|
-
res.end(
|
|
961
|
-
"<html><body><h1>Authenticated!</h1><p>You can close this window and return to the terminal.</p></body></html>"
|
|
962
|
-
);
|
|
963
|
-
server.close();
|
|
964
|
-
resolve({ token, teamId });
|
|
965
|
-
}
|
|
966
|
-
});
|
|
967
|
-
server.listen(OAUTH_PORT, () => {
|
|
968
|
-
const authUrl = `${OAUTH_BASE}/cli-auth?redirect=http://localhost:${OAUTH_PORT}/callback`;
|
|
969
|
-
open(authUrl);
|
|
970
|
-
});
|
|
971
|
-
server.on("error", (err) => {
|
|
972
|
-
reject(
|
|
973
|
-
new Error(`Failed to start OAuth callback server: ${err.message}`)
|
|
974
|
-
);
|
|
975
|
-
});
|
|
976
|
-
setTimeout(() => {
|
|
977
|
-
server.close();
|
|
978
|
-
reject(new Error("OAuth login timed out after 5 minutes"));
|
|
979
|
-
}, 3e5);
|
|
980
|
-
});
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// src/commands/auth.ts
|
|
984
|
-
var DEFAULT_API_URL4 = "https://public-api.animaapp.com";
|
|
985
|
-
var authCommand = new Command5("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("--login", "Login via browser OAuth flow").option("--status", "Show current auth status").option("--logout", "Clear stored credentials").option("--json", "Output as JSON").option("--api-url <url>", "API base URL", DEFAULT_API_URL4).action(async (opts) => {
|
|
842
|
+
import chalk3 from "chalk";
|
|
843
|
+
var DEFAULT_API_URL2 = "https://public-api.animaapp.com";
|
|
844
|
+
var authCommand = new Command5("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
845
|
const mode = detectOutputMode(opts);
|
|
987
846
|
try {
|
|
988
847
|
if (opts.token || opts.figmaToken) {
|
|
@@ -1015,22 +874,8 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1015
874
|
}
|
|
1016
875
|
return;
|
|
1017
876
|
}
|
|
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
877
|
if (opts.logout) {
|
|
1033
|
-
const deleted =
|
|
878
|
+
const deleted = clearStorage();
|
|
1034
879
|
if (mode === "pretty") {
|
|
1035
880
|
if (deleted) {
|
|
1036
881
|
printSuccess("Credentials cleared");
|
|
@@ -1052,14 +897,18 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1052
897
|
printSuccess("Token configured");
|
|
1053
898
|
printKeyValue("Source", sourceName);
|
|
1054
899
|
printKeyValue("Anima token", auth.token.slice(0, 12) + "..." + auth.token.slice(-4));
|
|
900
|
+
printKeyValue("Token type", auth.tokenType === "agent" ? "agent (scoped, revocable)" : "personal/API");
|
|
901
|
+
if (creds?.tokenType === "agent" && creds.expiresAt) printKeyValue("Expires", creds.expiresAt);
|
|
1055
902
|
if (auth.teamId) printKeyValue("Team ID", auth.teamId);
|
|
1056
903
|
printKeyValue("Figma token", hasFigma ? "configured" : "not set (needed for codegen/f2c)");
|
|
1057
904
|
} else {
|
|
1058
905
|
outputResult(mode, {
|
|
1059
906
|
success: true,
|
|
1060
907
|
source: auth.source,
|
|
908
|
+
tokenType: auth.tokenType ?? "personal",
|
|
1061
909
|
tokenPreview: auth.token.slice(0, 12) + "..." + auth.token.slice(-4),
|
|
1062
910
|
teamId: auth.teamId,
|
|
911
|
+
expiresAt: creds?.tokenType === "agent" ? creds.expiresAt : void 0,
|
|
1063
912
|
figmaToken: hasFigma
|
|
1064
913
|
});
|
|
1065
914
|
}
|
|
@@ -1079,20 +928,194 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1079
928
|
if (mode === "pretty") {
|
|
1080
929
|
console.log();
|
|
1081
930
|
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`);
|
|
931
|
+
console.log(` ${chalk3.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
|
|
932
|
+
console.log(` ${chalk3.cyan("anima logout")} Clear all credentials`);
|
|
933
|
+
console.log(` ${chalk3.cyan("anima auth --status")} Check auth status`);
|
|
934
|
+
console.log(` ${chalk3.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
|
|
1088
935
|
console.log();
|
|
1089
|
-
console.log(`
|
|
1090
|
-
console.log(` Figma token: ${chalk2.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
936
|
+
console.log(` Figma token: ${chalk3.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
1091
937
|
console.log();
|
|
1092
938
|
} else {
|
|
1093
939
|
outputResult(mode, {
|
|
1094
940
|
success: true,
|
|
1095
|
-
message: "
|
|
941
|
+
message: "Run `anima login`, or use --status / --logout / --figma-token"
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
} catch (err) {
|
|
945
|
+
if (err instanceof CliError) {
|
|
946
|
+
if (mode === "pretty") {
|
|
947
|
+
printError(err.message);
|
|
948
|
+
} else {
|
|
949
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
950
|
+
}
|
|
951
|
+
process.exit(err.exitCode);
|
|
952
|
+
}
|
|
953
|
+
throw err;
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
// src/commands/login.ts
|
|
958
|
+
import { Command as Command6 } from "commander";
|
|
959
|
+
import chalk4 from "chalk";
|
|
960
|
+
import open from "open";
|
|
961
|
+
|
|
962
|
+
// src/auth/device.ts
|
|
963
|
+
var DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
964
|
+
var DEFAULT_CLIENT_NAME = "Anima CLI";
|
|
965
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
966
|
+
function decodeJwtPayload(token) {
|
|
967
|
+
try {
|
|
968
|
+
const parts = token.split(".");
|
|
969
|
+
if (parts.length !== 3) return void 0;
|
|
970
|
+
return JSON.parse(Buffer.from(parts[1], "base64url").toString("utf-8"));
|
|
971
|
+
} catch {
|
|
972
|
+
return void 0;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
async function loginWithDeviceFlow(options) {
|
|
976
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
977
|
+
const sleep = options.sleepFn ?? defaultSleep;
|
|
978
|
+
const base = options.apiUrl.replace(/\/$/, "");
|
|
979
|
+
const startUrl = `${base}/oauth/device_authorization`;
|
|
980
|
+
logEvent("device_authorization_request", { url: startUrl });
|
|
981
|
+
const startResponse = await fetchFn(startUrl, {
|
|
982
|
+
method: "POST",
|
|
983
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
984
|
+
body: new URLSearchParams({ client_name: options.clientName ?? DEFAULT_CLIENT_NAME }).toString()
|
|
985
|
+
});
|
|
986
|
+
logEvent("device_authorization_response", { url: startUrl, status: startResponse.status });
|
|
987
|
+
if (!startResponse.ok) {
|
|
988
|
+
const detail = await startResponse.text().catch(() => "");
|
|
989
|
+
logEvent("device_authorization_failed", { url: startUrl, status: startResponse.status, body: detail.slice(0, 500) });
|
|
990
|
+
const hint = startResponse.status === 404 ? ` \u2014 no device endpoint at ${startUrl}. Check --api-url points to an API that has the device grant deployed.` : "";
|
|
991
|
+
throw new CliError(
|
|
992
|
+
"API_ERROR",
|
|
993
|
+
`Device authorization failed (HTTP ${startResponse.status})${hint}`,
|
|
994
|
+
EXIT_CODES.API_ERROR
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
const start = await startResponse.json();
|
|
998
|
+
options.onVerification({
|
|
999
|
+
userCode: start.user_code,
|
|
1000
|
+
verificationUri: start.verification_uri,
|
|
1001
|
+
verificationUriComplete: start.verification_uri_complete,
|
|
1002
|
+
expiresInSeconds: start.expires_in
|
|
1003
|
+
});
|
|
1004
|
+
let intervalMs = Math.max(1, start.interval) * 1e3;
|
|
1005
|
+
const deadline = Date.now() + start.expires_in * 1e3;
|
|
1006
|
+
while (Date.now() < deadline) {
|
|
1007
|
+
await sleep(intervalMs);
|
|
1008
|
+
const pollResponse = await fetchFn(`${base}/oauth/token`, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1011
|
+
body: new URLSearchParams({
|
|
1012
|
+
grant_type: DEVICE_GRANT_TYPE,
|
|
1013
|
+
device_code: start.device_code
|
|
1014
|
+
}).toString()
|
|
1015
|
+
});
|
|
1016
|
+
const body = await pollResponse.json();
|
|
1017
|
+
logEvent("device_token_poll", { status: pollResponse.status, error: body.error });
|
|
1018
|
+
if (pollResponse.ok && typeof body.access_token === "string") {
|
|
1019
|
+
const token = body.access_token;
|
|
1020
|
+
const claims = decodeJwtPayload(token);
|
|
1021
|
+
const result = {
|
|
1022
|
+
token,
|
|
1023
|
+
teamId: typeof claims?.team_id === "string" ? claims.team_id : void 0,
|
|
1024
|
+
agentId: typeof claims?.agent_id === "string" ? claims.agent_id : void 0,
|
|
1025
|
+
expiresAt: typeof claims?.exp === "number" ? new Date(claims.exp * 1e3).toISOString() : void 0
|
|
1026
|
+
};
|
|
1027
|
+
writeCredentials({
|
|
1028
|
+
token: result.token,
|
|
1029
|
+
teamId: result.teamId,
|
|
1030
|
+
tokenType: "agent",
|
|
1031
|
+
agentId: result.agentId,
|
|
1032
|
+
expiresAt: result.expiresAt
|
|
1033
|
+
});
|
|
1034
|
+
return result;
|
|
1035
|
+
}
|
|
1036
|
+
switch (body.error) {
|
|
1037
|
+
case "authorization_pending":
|
|
1038
|
+
continue;
|
|
1039
|
+
case "slow_down":
|
|
1040
|
+
intervalMs += 5e3;
|
|
1041
|
+
continue;
|
|
1042
|
+
case "access_denied":
|
|
1043
|
+
throw authError("The authorization request was denied.");
|
|
1044
|
+
case "expired_token":
|
|
1045
|
+
throw authError("The device code expired before approval. Run the login again.");
|
|
1046
|
+
default:
|
|
1047
|
+
throw new CliError(
|
|
1048
|
+
"API_ERROR",
|
|
1049
|
+
`Unexpected token response: ${body.error ?? `HTTP ${pollResponse.status}`}`,
|
|
1050
|
+
EXIT_CODES.API_ERROR
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
throw authError("The device code expired before approval. Run the login again.");
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// src/commands/login.ts
|
|
1058
|
+
var loginCommand = new Command6("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(
|
|
1059
|
+
"--print-mcp-config",
|
|
1060
|
+
"After login, also print an MCP server config (includes the token) to paste into an MCP-capable agent"
|
|
1061
|
+
).action(async (opts) => {
|
|
1062
|
+
const mode = detectOutputMode(opts);
|
|
1063
|
+
initLogger(opts.logFile);
|
|
1064
|
+
const apiUrl = resolveApiUrl(opts.apiUrl);
|
|
1065
|
+
try {
|
|
1066
|
+
const result = await loginWithDeviceFlow({
|
|
1067
|
+
apiUrl,
|
|
1068
|
+
clientName: opts.clientName,
|
|
1069
|
+
onVerification: (info) => {
|
|
1070
|
+
if (mode === "pretty") {
|
|
1071
|
+
console.log();
|
|
1072
|
+
printInfo("To connect, open the verification page and enter this code:");
|
|
1073
|
+
console.log();
|
|
1074
|
+
console.log(` ${chalk4.cyan.underline(info.verificationUri)}`);
|
|
1075
|
+
console.log();
|
|
1076
|
+
console.log(` ${chalk4.bold.white(info.userCode)}`);
|
|
1077
|
+
console.log();
|
|
1078
|
+
printInfo(`Waiting for approval\u2026 (code expires in ${Math.round(info.expiresInSeconds / 60)} minutes)`);
|
|
1079
|
+
} else {
|
|
1080
|
+
process.stderr.write(
|
|
1081
|
+
JSON.stringify({
|
|
1082
|
+
event: "verification_required",
|
|
1083
|
+
verificationUri: info.verificationUri,
|
|
1084
|
+
verificationUriComplete: info.verificationUriComplete,
|
|
1085
|
+
userCode: info.userCode,
|
|
1086
|
+
expiresIn: info.expiresInSeconds
|
|
1087
|
+
}) + "\n"
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
if (mode === "pretty" && opts.open) {
|
|
1091
|
+
open(info.verificationUriComplete).catch(() => {
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
const mcpConfig = opts.printMcpConfig ? buildMcpConfig(apiUrl, result.token) : void 0;
|
|
1097
|
+
if (mode === "pretty") {
|
|
1098
|
+
printSuccess("Connected!");
|
|
1099
|
+
printKeyValue("Credentials", getCredentialsPath());
|
|
1100
|
+
if (result.agentId) printKeyValue("Agent", result.agentId);
|
|
1101
|
+
if (result.teamId) printKeyValue("Team ID", result.teamId);
|
|
1102
|
+
if (result.expiresAt) printKeyValue("Token expires", result.expiresAt);
|
|
1103
|
+
printInfo("This is a scoped agent token \u2014 it acts within what you approved, and your team can revoke it.");
|
|
1104
|
+
if (mcpConfig) {
|
|
1105
|
+
console.log();
|
|
1106
|
+
printInfo("MCP server config (token expires ~7 days \u2014 re-run `anima login` to refresh):");
|
|
1107
|
+
console.log();
|
|
1108
|
+
console.log(JSON.stringify(mcpConfig, null, 2));
|
|
1109
|
+
}
|
|
1110
|
+
} else {
|
|
1111
|
+
outputResult(mode, {
|
|
1112
|
+
success: true,
|
|
1113
|
+
action: "logged_in",
|
|
1114
|
+
tokenType: "agent",
|
|
1115
|
+
agentId: result.agentId,
|
|
1116
|
+
teamId: result.teamId,
|
|
1117
|
+
expiresAt: result.expiresAt,
|
|
1118
|
+
...mcpConfig ? { mcpConfig } : {}
|
|
1096
1119
|
});
|
|
1097
1120
|
}
|
|
1098
1121
|
} catch (err) {
|
|
@@ -1107,45 +1130,146 @@ var authCommand = new Command5("auth").description("Manage authentication").opti
|
|
|
1107
1130
|
throw err;
|
|
1108
1131
|
}
|
|
1109
1132
|
});
|
|
1133
|
+
function buildMcpConfig(apiUrl, token) {
|
|
1134
|
+
return {
|
|
1135
|
+
mcpServers: {
|
|
1136
|
+
anima: {
|
|
1137
|
+
type: "http",
|
|
1138
|
+
url: `${apiUrl.replace(/\/$/, "")}/v1/mcp`,
|
|
1139
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/commands/logout.ts
|
|
1146
|
+
import { Command as Command7 } from "commander";
|
|
1147
|
+
var logoutCommand = new Command7("logout").description("Clear all stored credentials (Anima + Figma tokens) from this machine").option("--json", "Output as JSON").action((opts) => {
|
|
1148
|
+
const mode = detectOutputMode(opts);
|
|
1149
|
+
const hadCredentials = clearStorage();
|
|
1150
|
+
if (mode === "pretty") {
|
|
1151
|
+
if (hadCredentials) {
|
|
1152
|
+
printSuccess("Logged out \u2014 all credentials cleared from " + getCredentialsPath());
|
|
1153
|
+
} else {
|
|
1154
|
+
printInfo("No stored credentials found");
|
|
1155
|
+
}
|
|
1156
|
+
} else {
|
|
1157
|
+
outputResult(mode, { success: true, action: "logged_out", hadCredentials });
|
|
1158
|
+
}
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
// src/commands/config.ts
|
|
1162
|
+
import { Command as Command8 } from "commander";
|
|
1163
|
+
var configCommand = new Command8("config").description(
|
|
1164
|
+
`Store CLI preferences (keys: ${configKeys().join(", ")})`
|
|
1165
|
+
);
|
|
1166
|
+
configCommand.command("set <key> <value>").description("Set a config value").option("--json", "Output as JSON").action((key, value, opts) => {
|
|
1167
|
+
const mode = detectOutputMode(opts);
|
|
1168
|
+
assertKey(key);
|
|
1169
|
+
if (key === "api-url") assertUrl(value);
|
|
1170
|
+
setConfig(key, value);
|
|
1171
|
+
if (mode === "pretty") printSuccess(`Set ${key} = ${value}`);
|
|
1172
|
+
else outputResult(mode, { success: true, action: "config_set", key, value });
|
|
1173
|
+
});
|
|
1174
|
+
configCommand.command("get <key>").description("Print a config value").option("--json", "Output as JSON").action((key, opts) => {
|
|
1175
|
+
const mode = detectOutputMode(opts);
|
|
1176
|
+
assertKey(key);
|
|
1177
|
+
const value = readConfig()[CONFIG_KEYS[key]];
|
|
1178
|
+
if (mode === "pretty") {
|
|
1179
|
+
if (value) console.log(value);
|
|
1180
|
+
else printInfo(`${key} is not set`);
|
|
1181
|
+
} else {
|
|
1182
|
+
outputResult(mode, { success: true, key, value: value ?? null });
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
configCommand.command("list").alias("ls").description("Show all config values and where the API URL resolves from").option("--json", "Output as JSON").action((opts) => {
|
|
1186
|
+
const mode = detectOutputMode(opts);
|
|
1187
|
+
const config = readConfig();
|
|
1188
|
+
const resolvedApiUrl = resolveApiUrl();
|
|
1189
|
+
if (mode === "pretty") {
|
|
1190
|
+
printInfo("Config: " + getConfigPath());
|
|
1191
|
+
printKeyValue("api-url", config.apiUrl ?? `(default) ${DEFAULT_API_URL}`);
|
|
1192
|
+
if (process.env.ANIMA_API_URL) {
|
|
1193
|
+
printInfo(`ANIMA_API_URL env is set \u2014 it overrides config (effective: ${resolvedApiUrl})`);
|
|
1194
|
+
}
|
|
1195
|
+
} else {
|
|
1196
|
+
outputResult(mode, {
|
|
1197
|
+
success: true,
|
|
1198
|
+
path: getConfigPath(),
|
|
1199
|
+
config,
|
|
1200
|
+
resolved: { apiUrl: resolvedApiUrl }
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
configCommand.command("unset <key>").description("Remove a config value").option("--json", "Output as JSON").action((key, opts) => {
|
|
1205
|
+
const mode = detectOutputMode(opts);
|
|
1206
|
+
assertKey(key);
|
|
1207
|
+
const removed = unsetConfig(key);
|
|
1208
|
+
if (mode === "pretty") {
|
|
1209
|
+
if (removed) printSuccess(`Unset ${key}`);
|
|
1210
|
+
else printInfo(`${key} was not set`);
|
|
1211
|
+
} else {
|
|
1212
|
+
outputResult(mode, { success: true, action: "config_unset", key, removed });
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
function assertKey(key) {
|
|
1216
|
+
if (!isConfigKey(key)) {
|
|
1217
|
+
throw validationError(`Unknown config key "${key}". Valid keys: ${configKeys().join(", ")}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
function assertUrl(value) {
|
|
1221
|
+
try {
|
|
1222
|
+
new URL(value);
|
|
1223
|
+
} catch {
|
|
1224
|
+
throw validationError(`"${value}" is not a valid URL`);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1110
1227
|
|
|
1111
1228
|
// src/index.ts
|
|
1112
|
-
var program = new
|
|
1229
|
+
var program = new Command9().name("anima").description(
|
|
1113
1230
|
`Anima CLI - Generate production-ready apps from prompts, URLs, or Figma designs.
|
|
1114
1231
|
|
|
1115
1232
|
AUTHENTICATION:
|
|
1116
|
-
|
|
1117
|
-
|
|
1233
|
+
Run: npx @animaapp/cli login
|
|
1234
|
+
Approve in a browser on any device \u2014 the CLI receives a scoped agent token
|
|
1235
|
+
(stored in ~/.config/anima/). CI/automation can set ANIMA_API_TOKEN instead.
|
|
1118
1236
|
|
|
1119
1237
|
TIMING (important for AI agents):
|
|
1120
|
-
create
|
|
1121
|
-
publish
|
|
1122
|
-
codegen
|
|
1123
|
-
|
|
1238
|
+
create \u2192 3-7 minutes (keep the default 600000ms timeout)
|
|
1239
|
+
publish \u2192 1-3 minutes
|
|
1240
|
+
codegen \u2192 2-5 minutes
|
|
1241
|
+
get-git-token \u2192 seconds
|
|
1124
1242
|
The --timeout default is 600000ms (10 min). Do NOT reduce it.
|
|
1125
1243
|
|
|
1126
1244
|
COMMANDS:
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1245
|
+
login Log in via the device flow (no browser needed on this machine)
|
|
1246
|
+
logout Clear all stored credentials from this machine
|
|
1247
|
+
create Create app from prompt, URL, or Figma design
|
|
1248
|
+
publish Deploy playground to live URL or npm package
|
|
1249
|
+
codegen Convert Figma design to code files (no playground)
|
|
1250
|
+
get-git-token Mint a git remote URL for a playground; clone/edit/push with git
|
|
1251
|
+
config Store CLI preferences (e.g. a default API URL)
|
|
1252
|
+
auth Inspect or manage credentials (status / figma token)
|
|
1132
1253
|
|
|
1133
1254
|
EXAMPLES:
|
|
1134
|
-
npx @animaapp/cli
|
|
1255
|
+
npx @animaapp/cli login
|
|
1135
1256
|
npx @animaapp/cli create -t p2c -p "SaaS dashboard with sidebar and analytics"
|
|
1136
1257
|
npx @animaapp/cli create -t l2c -u https://stripe.com
|
|
1137
1258
|
npx @animaapp/cli create -t f2c --file-key abc --nodes 42:15
|
|
1138
1259
|
npx @animaapp/cli publish abc123xyz
|
|
1139
1260
|
npx @animaapp/cli codegen --file-key abc --nodes 42:15 -o ./components
|
|
1140
|
-
npx @animaapp/cli
|
|
1261
|
+
npx @animaapp/cli get-git-token https://dev.animaapp.com/chat/abc123xyz
|
|
1141
1262
|
|
|
1142
1263
|
OUTPUT: JSON to stdout when piped (for AI tools), pretty text in terminals.
|
|
1143
1264
|
Progress is streamed to stderr via --verbose when in JSON mode.`
|
|
1144
|
-
).version(
|
|
1265
|
+
).version(version, "-v, --version");
|
|
1266
|
+
program.addCommand(loginCommand);
|
|
1267
|
+
program.addCommand(logoutCommand);
|
|
1145
1268
|
program.addCommand(createCommand);
|
|
1146
1269
|
program.addCommand(publishCommand);
|
|
1147
1270
|
program.addCommand(codegenCommand);
|
|
1148
|
-
program.addCommand(
|
|
1271
|
+
program.addCommand(getGitTokenCommand);
|
|
1272
|
+
program.addCommand(configCommand);
|
|
1149
1273
|
program.addCommand(authCommand);
|
|
1150
1274
|
program.parseAsync(process.argv).catch((err) => {
|
|
1151
1275
|
if (err instanceof CliError) {
|