@animaapp/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -0
- package/dist/index.js +1167 -0
- package/dist/index.js.map +1 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command6 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/create.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
// src/auth/store.ts
|
|
10
|
+
import fs from "fs";
|
|
11
|
+
import path from "path";
|
|
12
|
+
import os from "os";
|
|
13
|
+
var CONFIG_DIR = path.join(os.homedir(), ".config", "anima");
|
|
14
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
15
|
+
function readCredentials() {
|
|
16
|
+
try {
|
|
17
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) return null;
|
|
18
|
+
const content = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
19
|
+
const parsed = JSON.parse(content);
|
|
20
|
+
if (!parsed.token) return null;
|
|
21
|
+
return parsed;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeCredentials(credentials) {
|
|
27
|
+
const existing = readCredentials() ?? {};
|
|
28
|
+
const merged = { ...existing, ...credentials };
|
|
29
|
+
for (const key of Object.keys(merged)) {
|
|
30
|
+
if (merged[key] === void 0) delete merged[key];
|
|
31
|
+
}
|
|
32
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
CREDENTIALS_FILE,
|
|
35
|
+
JSON.stringify(merged, null, 2) + "\n",
|
|
36
|
+
{ mode: 384 }
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
function deleteCredentials() {
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
42
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function getCredentialsPath() {
|
|
51
|
+
return CREDENTIALS_FILE;
|
|
52
|
+
}
|
|
53
|
+
function extractTeamIdFromJWT(token) {
|
|
54
|
+
try {
|
|
55
|
+
const parts = token.split(".");
|
|
56
|
+
if (parts.length !== 3) return void 0;
|
|
57
|
+
const payload = JSON.parse(
|
|
58
|
+
Buffer.from(parts[1], "base64url").toString("utf-8")
|
|
59
|
+
);
|
|
60
|
+
return payload.defaultTeamId ?? void 0;
|
|
61
|
+
} catch {
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/api/errors.ts
|
|
67
|
+
var EXIT_CODES = {
|
|
68
|
+
SUCCESS: 0,
|
|
69
|
+
API_ERROR: 1,
|
|
70
|
+
AUTH_ERROR: 2,
|
|
71
|
+
VALIDATION_ERROR: 3,
|
|
72
|
+
TIMEOUT: 4
|
|
73
|
+
};
|
|
74
|
+
var CliError = class extends Error {
|
|
75
|
+
code;
|
|
76
|
+
exitCode;
|
|
77
|
+
detail;
|
|
78
|
+
constructor(code, message, exitCode, detail) {
|
|
79
|
+
super(message);
|
|
80
|
+
this.name = "CliError";
|
|
81
|
+
this.code = code;
|
|
82
|
+
this.exitCode = exitCode;
|
|
83
|
+
this.detail = detail;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function authError(message) {
|
|
87
|
+
return new CliError("AUTH_REQUIRED", message, EXIT_CODES.AUTH_ERROR);
|
|
88
|
+
}
|
|
89
|
+
function validationError(message, detail) {
|
|
90
|
+
return new CliError(
|
|
91
|
+
"VALIDATION_ERROR",
|
|
92
|
+
message,
|
|
93
|
+
EXIT_CODES.VALIDATION_ERROR,
|
|
94
|
+
detail
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
function apiError(message, detail) {
|
|
98
|
+
return new CliError("API_ERROR", message, EXIT_CODES.API_ERROR, detail);
|
|
99
|
+
}
|
|
100
|
+
function streamError(message) {
|
|
101
|
+
return new CliError("STREAM_ERROR", message, EXIT_CODES.API_ERROR);
|
|
102
|
+
}
|
|
103
|
+
function timeoutError() {
|
|
104
|
+
return new CliError(
|
|
105
|
+
"TIMEOUT",
|
|
106
|
+
"Request timed out. Generation can take 3-7 minutes. Try increasing --timeout.",
|
|
107
|
+
EXIT_CODES.TIMEOUT
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// src/auth/token.ts
|
|
112
|
+
function resolveAuth() {
|
|
113
|
+
const creds = readCredentials();
|
|
114
|
+
const envToken = process.env.ANIMA_API_TOKEN;
|
|
115
|
+
if (envToken) {
|
|
116
|
+
return {
|
|
117
|
+
token: envToken,
|
|
118
|
+
teamId: process.env.ANIMA_TEAM_ID,
|
|
119
|
+
figmaToken: process.env.FIGMA_TOKEN || creds?.figmaToken,
|
|
120
|
+
source: "env"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (creds) {
|
|
124
|
+
return {
|
|
125
|
+
token: creds.token,
|
|
126
|
+
teamId: creds.teamId,
|
|
127
|
+
figmaToken: process.env.FIGMA_TOKEN || creds.figmaToken,
|
|
128
|
+
source: "file"
|
|
129
|
+
};
|
|
130
|
+
}
|
|
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
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// src/api/client.ts
|
|
137
|
+
async function streamGeneration(url, body, headers, callbacks = {}, timeoutMs = 6e5, signal) {
|
|
138
|
+
const controller = new AbortController();
|
|
139
|
+
const combinedSignal = signal ?? controller.signal;
|
|
140
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
141
|
+
try {
|
|
142
|
+
const response = await fetch(url, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers: {
|
|
145
|
+
...headers,
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
Accept: "text/event-stream"
|
|
148
|
+
},
|
|
149
|
+
body: JSON.stringify(body),
|
|
150
|
+
signal: combinedSignal
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
const errorText = await response.text();
|
|
154
|
+
let detail;
|
|
155
|
+
try {
|
|
156
|
+
detail = JSON.parse(errorText);
|
|
157
|
+
} catch {
|
|
158
|
+
detail = errorText;
|
|
159
|
+
}
|
|
160
|
+
throw apiError(
|
|
161
|
+
`API returned ${response.status}: ${response.statusText}`,
|
|
162
|
+
detail
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (!response.body) {
|
|
166
|
+
throw streamError("Response body is null");
|
|
167
|
+
}
|
|
168
|
+
return await parseSSEStream(
|
|
169
|
+
response.body,
|
|
170
|
+
(data, result) => {
|
|
171
|
+
switch (data.type) {
|
|
172
|
+
case "queueing":
|
|
173
|
+
callbacks.onQueueing?.(data.payload.sessionId);
|
|
174
|
+
break;
|
|
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();
|
|
212
|
+
}
|
|
213
|
+
throw err;
|
|
214
|
+
} finally {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function parseSSEStream(body, handler, initial) {
|
|
219
|
+
const reader = body.getReader();
|
|
220
|
+
const decoder = new TextDecoder();
|
|
221
|
+
let buffer = "";
|
|
222
|
+
const result = { ...initial };
|
|
223
|
+
try {
|
|
224
|
+
while (true) {
|
|
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
|
+
}
|
|
244
|
+
} finally {
|
|
245
|
+
reader.cancel();
|
|
246
|
+
}
|
|
247
|
+
throw streamError("Connection closed before receiving 'done' message");
|
|
248
|
+
}
|
|
249
|
+
function buildHeaders(token, teamId) {
|
|
250
|
+
const headers = {
|
|
251
|
+
Authorization: `Bearer ${token}`
|
|
252
|
+
};
|
|
253
|
+
if (teamId) {
|
|
254
|
+
headers["X-Team-Id"] = teamId;
|
|
255
|
+
}
|
|
256
|
+
return headers;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// src/output/formatter.ts
|
|
260
|
+
function detectOutputMode(flags) {
|
|
261
|
+
if (flags.json !== void 0) {
|
|
262
|
+
return flags.json ? "json" : "pretty";
|
|
263
|
+
}
|
|
264
|
+
return process.stdout.isTTY ? "pretty" : "json";
|
|
265
|
+
}
|
|
266
|
+
function outputResult(mode, data) {
|
|
267
|
+
if (mode === "json") {
|
|
268
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function outputError(mode, error) {
|
|
272
|
+
if (mode === "json") {
|
|
273
|
+
process.stdout.write(
|
|
274
|
+
JSON.stringify({ success: false, error }) + "\n"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/output/progress.ts
|
|
280
|
+
import ora from "ora";
|
|
281
|
+
var ProgressReporter = class {
|
|
282
|
+
#spinner = null;
|
|
283
|
+
#mode;
|
|
284
|
+
#startTime = 0;
|
|
285
|
+
#verbose;
|
|
286
|
+
constructor(mode, verbose = false) {
|
|
287
|
+
this.#mode = mode;
|
|
288
|
+
this.#verbose = verbose;
|
|
289
|
+
}
|
|
290
|
+
start(message) {
|
|
291
|
+
this.#startTime = Date.now();
|
|
292
|
+
if (this.#mode === "pretty") {
|
|
293
|
+
this.#spinner = ora(message).start();
|
|
294
|
+
} else if (this.#verbose) {
|
|
295
|
+
process.stderr.write(JSON.stringify({ status: "started", message }) + "\n");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
update(message) {
|
|
299
|
+
if (this.#mode === "pretty" && this.#spinner) {
|
|
300
|
+
const elapsed = this.#elapsed();
|
|
301
|
+
this.#spinner.text = `${message} ${elapsed}`;
|
|
302
|
+
} else if (this.#verbose) {
|
|
303
|
+
process.stderr.write(JSON.stringify({ status: "progress", message }) + "\n");
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
succeed(message) {
|
|
307
|
+
if (this.#mode === "pretty" && this.#spinner) {
|
|
308
|
+
const elapsed = this.#elapsed();
|
|
309
|
+
this.#spinner.succeed(`${message} ${elapsed}`);
|
|
310
|
+
} else if (this.#verbose) {
|
|
311
|
+
process.stderr.write(JSON.stringify({ status: "done", message }) + "\n");
|
|
312
|
+
}
|
|
313
|
+
this.#spinner = null;
|
|
314
|
+
}
|
|
315
|
+
fail(message) {
|
|
316
|
+
if (this.#mode === "pretty" && this.#spinner) {
|
|
317
|
+
this.#spinner.fail(message);
|
|
318
|
+
} else if (this.#verbose) {
|
|
319
|
+
process.stderr.write(JSON.stringify({ status: "error", message }) + "\n");
|
|
320
|
+
}
|
|
321
|
+
this.#spinner = null;
|
|
322
|
+
}
|
|
323
|
+
#elapsed() {
|
|
324
|
+
const ms = Date.now() - this.#startTime;
|
|
325
|
+
const seconds = Math.floor(ms / 1e3);
|
|
326
|
+
if (seconds < 60) return `(${seconds}s)`;
|
|
327
|
+
const minutes = Math.floor(seconds / 60);
|
|
328
|
+
const secs = seconds % 60;
|
|
329
|
+
return `(${minutes}m ${secs}s)`;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// src/output/pretty.ts
|
|
334
|
+
import chalk from "chalk";
|
|
335
|
+
function printSuccess(message) {
|
|
336
|
+
console.log(chalk.green("\u2713") + " " + message);
|
|
337
|
+
}
|
|
338
|
+
function printError(message) {
|
|
339
|
+
console.error(chalk.red("\u2717") + " " + message);
|
|
340
|
+
}
|
|
341
|
+
function printInfo(message) {
|
|
342
|
+
console.log(chalk.blue("\u2139") + " " + message);
|
|
343
|
+
}
|
|
344
|
+
function printKeyValue(key, value) {
|
|
345
|
+
console.log(` ${chalk.gray(key + ":")} ${value}`);
|
|
346
|
+
}
|
|
347
|
+
function printSessionResult(result) {
|
|
348
|
+
console.log();
|
|
349
|
+
printSuccess("Playground created successfully!");
|
|
350
|
+
printKeyValue("Session ID", result.sessionId);
|
|
351
|
+
if (result.playgroundUrl) {
|
|
352
|
+
printKeyValue("Playground", chalk.cyan.underline(result.playgroundUrl));
|
|
353
|
+
}
|
|
354
|
+
if (result.tokenUsage) {
|
|
355
|
+
printKeyValue("Token usage", result.tokenUsage.toLocaleString());
|
|
356
|
+
}
|
|
357
|
+
console.log();
|
|
358
|
+
}
|
|
359
|
+
function printDeployResult(result) {
|
|
360
|
+
console.log();
|
|
361
|
+
printSuccess("Published successfully!");
|
|
362
|
+
if (result.url) {
|
|
363
|
+
printKeyValue("Live URL", chalk.cyan.underline(result.url));
|
|
364
|
+
}
|
|
365
|
+
if (result.packageName) {
|
|
366
|
+
printKeyValue("Package", result.packageName);
|
|
367
|
+
printKeyValue("Version", result.packageVersion ?? "");
|
|
368
|
+
if (result.packageUrl) {
|
|
369
|
+
printKeyValue("URL", chalk.cyan.underline(result.packageUrl));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
console.log();
|
|
373
|
+
}
|
|
374
|
+
function printFilesWritten(outputDir, count) {
|
|
375
|
+
console.log();
|
|
376
|
+
printSuccess(`${count} files written to ${chalk.cyan(outputDir)}`);
|
|
377
|
+
console.log();
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/utils/figma.ts
|
|
381
|
+
function parseFigmaUrl(input) {
|
|
382
|
+
if (!input.includes("figma.com")) {
|
|
383
|
+
return { fileKey: input };
|
|
384
|
+
}
|
|
385
|
+
try {
|
|
386
|
+
const url = new URL(input);
|
|
387
|
+
const pathParts = url.pathname.split("/").filter(Boolean);
|
|
388
|
+
const typeIndex = pathParts.findIndex(
|
|
389
|
+
(p) => p === "file" || p === "design"
|
|
390
|
+
);
|
|
391
|
+
if (typeIndex === -1 || typeIndex + 1 >= pathParts.length) {
|
|
392
|
+
throw new Error("Could not find file key in Figma URL");
|
|
393
|
+
}
|
|
394
|
+
const fileKey = pathParts[typeIndex + 1];
|
|
395
|
+
const nodeIdParam = url.searchParams.get("node-id");
|
|
396
|
+
let nodeId;
|
|
397
|
+
if (nodeIdParam) {
|
|
398
|
+
nodeId = nodeIdParam.replace(/-/g, ":");
|
|
399
|
+
}
|
|
400
|
+
return { fileKey, nodeId };
|
|
401
|
+
} catch (err) {
|
|
402
|
+
if (err instanceof Error && err.message.includes("file key")) {
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
throw new Error(`Invalid Figma URL: ${input}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function normalizeNodeId(nodeId) {
|
|
409
|
+
return nodeId.replace(/-/g, ":");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/commands/create.ts
|
|
413
|
+
var DEFAULT_API_URL = "https://public-api.animaapp.com";
|
|
414
|
+
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", DEFAULT_API_URL).action(async (opts) => {
|
|
416
|
+
const mode = detectOutputMode(opts);
|
|
417
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
418
|
+
try {
|
|
419
|
+
const type = opts.type;
|
|
420
|
+
validateCreateOptions(type, opts);
|
|
421
|
+
const auth = resolveAuth();
|
|
422
|
+
const headers = buildHeaders(auth.token, auth.teamId);
|
|
423
|
+
const { endpoint, body } = buildCreateRequest(type, opts, auth.figmaToken);
|
|
424
|
+
progress.start(`Creating ${type.toUpperCase()} playground...`);
|
|
425
|
+
const result = await streamGeneration(
|
|
426
|
+
`${opts.apiUrl}${endpoint}`,
|
|
427
|
+
body,
|
|
428
|
+
headers,
|
|
429
|
+
{
|
|
430
|
+
onQueueing: () => progress.update("Queued, waiting..."),
|
|
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)
|
|
440
|
+
);
|
|
441
|
+
const playgroundUrl = `${PLAYGROUND_BASE}/${result.sessionId}`;
|
|
442
|
+
progress.succeed("Playground created!");
|
|
443
|
+
if (mode === "pretty") {
|
|
444
|
+
printSessionResult({
|
|
445
|
+
sessionId: result.sessionId,
|
|
446
|
+
playgroundUrl,
|
|
447
|
+
tokenUsage: result.tokenUsage
|
|
448
|
+
});
|
|
449
|
+
} else {
|
|
450
|
+
outputResult(mode, {
|
|
451
|
+
success: true,
|
|
452
|
+
sessionId: result.sessionId,
|
|
453
|
+
playgroundUrl,
|
|
454
|
+
tokenUsage: result.tokenUsage
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
} catch (err) {
|
|
458
|
+
progress.fail("Failed");
|
|
459
|
+
if (err instanceof CliError) {
|
|
460
|
+
if (mode === "pretty") {
|
|
461
|
+
console.error(err.message);
|
|
462
|
+
} else {
|
|
463
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
464
|
+
}
|
|
465
|
+
process.exit(err.exitCode);
|
|
466
|
+
}
|
|
467
|
+
throw err;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
function validateCreateOptions(type, opts) {
|
|
471
|
+
if (!["p2c", "l2c", "f2c"].includes(type)) {
|
|
472
|
+
throw validationError(`Invalid type "${type}". Must be p2c, l2c, or f2c.`);
|
|
473
|
+
}
|
|
474
|
+
if (type === "p2c" && !opts.prompt) {
|
|
475
|
+
throw validationError("--prompt is required for p2c type");
|
|
476
|
+
}
|
|
477
|
+
if (type === "l2c" && !opts.url) {
|
|
478
|
+
throw validationError("--url is required for l2c type");
|
|
479
|
+
}
|
|
480
|
+
if (type === "f2c" && !opts.fileKey) {
|
|
481
|
+
throw validationError("--file-key is required for f2c type");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function buildCreateRequest(type, opts, storedFigmaToken) {
|
|
485
|
+
const language = opts.language ?? (opts.framework === "react" ? "typescript" : void 0);
|
|
486
|
+
const conventions = {
|
|
487
|
+
framework: opts.framework,
|
|
488
|
+
language,
|
|
489
|
+
styling: opts.styling,
|
|
490
|
+
uiLibrary: opts.uiLibrary
|
|
491
|
+
};
|
|
492
|
+
Object.keys(conventions).forEach((key) => {
|
|
493
|
+
if (conventions[key] === void 0) {
|
|
494
|
+
delete conventions[key];
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
switch (type) {
|
|
498
|
+
case "p2c":
|
|
499
|
+
return {
|
|
500
|
+
endpoint: "/v1/p2c",
|
|
501
|
+
body: {
|
|
502
|
+
params: {
|
|
503
|
+
prompt: opts.prompt,
|
|
504
|
+
conventions,
|
|
505
|
+
...opts.guidelines ? { guidelines: opts.guidelines } : {}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
case "l2c":
|
|
510
|
+
return {
|
|
511
|
+
endpoint: "/v1/l2c",
|
|
512
|
+
body: {
|
|
513
|
+
params: {
|
|
514
|
+
input: { type: "url", url: opts.url },
|
|
515
|
+
conventions,
|
|
516
|
+
assetsStorage: { type: "bundled" }
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
};
|
|
520
|
+
case "f2c": {
|
|
521
|
+
const { fileKey, nodeId: parsedNodeId } = parseFigmaUrl(opts.fileKey);
|
|
522
|
+
let nodesId = [];
|
|
523
|
+
if (opts.nodes) {
|
|
524
|
+
nodesId = opts.nodes.split(",").map((n) => normalizeNodeId(n.trim()));
|
|
525
|
+
} else if (parsedNodeId) {
|
|
526
|
+
nodesId = [parsedNodeId];
|
|
527
|
+
}
|
|
528
|
+
const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || storedFigmaToken;
|
|
529
|
+
if (!figmaToken) {
|
|
530
|
+
throw validationError(
|
|
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
|
+
};
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/commands/publish.ts
|
|
551
|
+
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) => {
|
|
644
|
+
const mode = detectOutputMode(opts);
|
|
645
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
646
|
+
try {
|
|
647
|
+
if (!["webapp", "designSystem"].includes(opts.mode)) {
|
|
648
|
+
throw validationError(
|
|
649
|
+
`Invalid mode "${opts.mode}". Must be webapp or designSystem.`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
if (opts.mode === "designSystem" && !opts.packageName) {
|
|
653
|
+
throw validationError("--package-name is required for designSystem mode");
|
|
654
|
+
}
|
|
655
|
+
const auth = resolveAuth();
|
|
656
|
+
const args = {
|
|
657
|
+
sessionId,
|
|
658
|
+
mode: opts.mode
|
|
659
|
+
};
|
|
660
|
+
if (opts.packageName) args.packageName = opts.packageName;
|
|
661
|
+
if (opts.packageVersion) args.packageVersion = opts.packageVersion;
|
|
662
|
+
progress.start(`Publishing as ${opts.mode}...`);
|
|
663
|
+
const result = await callMCPTool(
|
|
664
|
+
opts.apiUrl,
|
|
665
|
+
auth.token,
|
|
666
|
+
auth.teamId,
|
|
667
|
+
"playground-publish",
|
|
668
|
+
args
|
|
669
|
+
);
|
|
670
|
+
if (!result.success) {
|
|
671
|
+
throw new CliError(
|
|
672
|
+
"API_ERROR",
|
|
673
|
+
result.error || result.reason || "Publish failed",
|
|
674
|
+
1,
|
|
675
|
+
result
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
progress.succeed("Published!");
|
|
679
|
+
if (mode === "pretty") {
|
|
680
|
+
printDeployResult({
|
|
681
|
+
url: result.liveUrl,
|
|
682
|
+
packageUrl: result.packageUrl,
|
|
683
|
+
packageName: result.packageName,
|
|
684
|
+
packageVersion: result.packageVersion
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
outputResult(mode, {
|
|
688
|
+
success: true,
|
|
689
|
+
sessionId,
|
|
690
|
+
...result.liveUrl ? { liveUrl: result.liveUrl, subdomain: result.subdomain } : {},
|
|
691
|
+
...result.packageName ? {
|
|
692
|
+
packageName: result.packageName,
|
|
693
|
+
packageVersion: result.packageVersion,
|
|
694
|
+
packageUrl: result.packageUrl
|
|
695
|
+
} : {}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
} catch (err) {
|
|
699
|
+
progress.fail("Failed");
|
|
700
|
+
if (err instanceof CliError) {
|
|
701
|
+
if (mode === "pretty") {
|
|
702
|
+
console.error(err.message);
|
|
703
|
+
} else {
|
|
704
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
705
|
+
}
|
|
706
|
+
process.exit(err.exitCode);
|
|
707
|
+
}
|
|
708
|
+
throw err;
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
// src/commands/codegen.ts
|
|
713
|
+
import { Command as Command3 } from "commander";
|
|
714
|
+
|
|
715
|
+
// src/utils/files.ts
|
|
716
|
+
import fs2 from "fs";
|
|
717
|
+
import path2 from "path";
|
|
718
|
+
function writeFilesToDisk(files, outputDir) {
|
|
719
|
+
fs2.mkdirSync(outputDir, { recursive: true });
|
|
720
|
+
let count = 0;
|
|
721
|
+
for (const [filePath, fileContent] of Object.entries(files)) {
|
|
722
|
+
const fullPath = path2.join(outputDir, filePath);
|
|
723
|
+
const dirPath = path2.dirname(fullPath);
|
|
724
|
+
fs2.mkdirSync(dirPath, { recursive: true });
|
|
725
|
+
if (fileContent.isBinary) {
|
|
726
|
+
const buffer = Buffer.from(fileContent.content, "base64");
|
|
727
|
+
fs2.writeFileSync(fullPath, buffer);
|
|
728
|
+
} else {
|
|
729
|
+
fs2.writeFileSync(fullPath, fileContent.content, "utf-8");
|
|
730
|
+
}
|
|
731
|
+
count++;
|
|
732
|
+
}
|
|
733
|
+
return count;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// src/commands/codegen.ts
|
|
737
|
+
var DEFAULT_API_URL2 = "https://public-api.animaapp.com";
|
|
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) => {
|
|
739
|
+
const mode = detectOutputMode(opts);
|
|
740
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
741
|
+
try {
|
|
742
|
+
const { fileKey, nodeId: parsedNodeId } = parseFigmaUrl(opts.fileKey);
|
|
743
|
+
let nodesId = [];
|
|
744
|
+
if (opts.nodes) {
|
|
745
|
+
nodesId = opts.nodes.split(",").map((n) => normalizeNodeId(n.trim()));
|
|
746
|
+
} else if (parsedNodeId) {
|
|
747
|
+
nodesId = [parsedNodeId];
|
|
748
|
+
}
|
|
749
|
+
if (nodesId.length === 0) {
|
|
750
|
+
throw validationError(
|
|
751
|
+
"No node IDs provided. Use --nodes or include node-id in the Figma URL."
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const auth = resolveAuth();
|
|
755
|
+
const figmaToken = opts.figmaToken || auth.figmaToken;
|
|
756
|
+
if (!figmaToken) {
|
|
757
|
+
throw validationError(
|
|
758
|
+
"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
|
+
);
|
|
760
|
+
}
|
|
761
|
+
const headers = buildHeaders(auth.token, auth.teamId);
|
|
762
|
+
const language = opts.language ?? (opts.framework === "react" ? "typescript" : void 0);
|
|
763
|
+
const requestBody = {
|
|
764
|
+
fileKey,
|
|
765
|
+
nodesId,
|
|
766
|
+
figmaToken,
|
|
767
|
+
framework: opts.framework,
|
|
768
|
+
language,
|
|
769
|
+
styling: opts.styling,
|
|
770
|
+
uiLibrary: opts.uiLibrary
|
|
771
|
+
};
|
|
772
|
+
progress.start("Generating code from Figma...");
|
|
773
|
+
const result = await streamGeneration(
|
|
774
|
+
`${opts.apiUrl}/v1/codegen`,
|
|
775
|
+
requestBody,
|
|
776
|
+
headers,
|
|
777
|
+
{
|
|
778
|
+
onQueueing: () => progress.update("Queued, waiting..."),
|
|
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)
|
|
788
|
+
);
|
|
789
|
+
const count = writeFilesToDisk(result.files, opts.output);
|
|
790
|
+
progress.succeed(`Generated ${count} files`);
|
|
791
|
+
if (mode === "pretty") {
|
|
792
|
+
printFilesWritten(opts.output, count);
|
|
793
|
+
} else {
|
|
794
|
+
outputResult(mode, {
|
|
795
|
+
success: true,
|
|
796
|
+
output: opts.output,
|
|
797
|
+
fileCount: count,
|
|
798
|
+
sessionId: result.sessionId,
|
|
799
|
+
tokenUsage: result.tokenUsage
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
} catch (err) {
|
|
803
|
+
progress.fail("Failed");
|
|
804
|
+
if (err instanceof CliError) {
|
|
805
|
+
if (mode === "pretty") {
|
|
806
|
+
console.error(err.message);
|
|
807
|
+
} else {
|
|
808
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
809
|
+
}
|
|
810
|
+
process.exit(err.exitCode);
|
|
811
|
+
}
|
|
812
|
+
throw err;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// src/commands/download.ts
|
|
817
|
+
import { Command as Command4 } from "commander";
|
|
818
|
+
import fs4 from "fs";
|
|
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";
|
|
823
|
+
|
|
824
|
+
// src/utils/download.ts
|
|
825
|
+
import fs3 from "fs";
|
|
826
|
+
import path3 from "path";
|
|
827
|
+
import { pipeline } from "stream/promises";
|
|
828
|
+
import { Readable } from "stream";
|
|
829
|
+
function parsePlaygroundUrl(input) {
|
|
830
|
+
if (!input.includes("/")) {
|
|
831
|
+
return input;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const url = new URL(input);
|
|
835
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
836
|
+
const chatIndex = parts.indexOf("chat");
|
|
837
|
+
if (chatIndex !== -1 && chatIndex + 1 < parts.length) {
|
|
838
|
+
return parts[chatIndex + 1];
|
|
839
|
+
}
|
|
840
|
+
return parts[parts.length - 1];
|
|
841
|
+
} catch {
|
|
842
|
+
return input;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/commands/download.ts
|
|
847
|
+
var DEFAULT_API_URL3 = "https://public-api.animaapp.com";
|
|
848
|
+
var downloadCommand = new Command4("download").description("Download project from an Anima playground").argument("<playgroundUrl>", "Playground URL or session ID").option("-o, --output <dir>", "Output directory", "./downloaded").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL", DEFAULT_API_URL3).action(async (playgroundUrl, opts) => {
|
|
849
|
+
const mode = detectOutputMode(opts);
|
|
850
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
851
|
+
try {
|
|
852
|
+
const sessionId = parsePlaygroundUrl(playgroundUrl);
|
|
853
|
+
const auth = resolveAuth();
|
|
854
|
+
progress.start("Requesting download...");
|
|
855
|
+
const result = await callMCPTool(
|
|
856
|
+
opts.apiUrl,
|
|
857
|
+
auth.token,
|
|
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)
|
|
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`);
|
|
898
|
+
if (mode === "pretty") {
|
|
899
|
+
printFilesWritten(opts.output, count);
|
|
900
|
+
} else {
|
|
901
|
+
outputResult(mode, {
|
|
902
|
+
success: true,
|
|
903
|
+
output: opts.output,
|
|
904
|
+
fileCount: count,
|
|
905
|
+
sessionId
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
} catch (err) {
|
|
909
|
+
progress.fail("Failed");
|
|
910
|
+
if (err instanceof CliError) {
|
|
911
|
+
if (mode === "pretty") {
|
|
912
|
+
console.error(err.message);
|
|
913
|
+
} else {
|
|
914
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
915
|
+
}
|
|
916
|
+
process.exit(err.exitCode);
|
|
917
|
+
}
|
|
918
|
+
throw err;
|
|
919
|
+
}
|
|
920
|
+
});
|
|
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
|
+
|
|
933
|
+
// src/commands/auth.ts
|
|
934
|
+
import { Command as Command5 } from "commander";
|
|
935
|
+
import chalk2 from "chalk";
|
|
936
|
+
|
|
937
|
+
// src/auth/oauth.ts
|
|
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) => {
|
|
986
|
+
const mode = detectOutputMode(opts);
|
|
987
|
+
try {
|
|
988
|
+
if (opts.token || opts.figmaToken) {
|
|
989
|
+
const toSave = {};
|
|
990
|
+
if (opts.token) {
|
|
991
|
+
toSave.token = opts.token;
|
|
992
|
+
toSave.teamId = opts.teamId ?? extractTeamIdFromJWT(opts.token) ?? "";
|
|
993
|
+
}
|
|
994
|
+
if (opts.figmaToken) {
|
|
995
|
+
toSave.figmaToken = opts.figmaToken;
|
|
996
|
+
}
|
|
997
|
+
writeCredentials(toSave);
|
|
998
|
+
if (mode === "pretty") {
|
|
999
|
+
printSuccess("Credentials saved to " + getCredentialsPath());
|
|
1000
|
+
if (opts.token) {
|
|
1001
|
+
printKeyValue("Anima token", "saved");
|
|
1002
|
+
if (toSave.teamId) printKeyValue("Team ID", toSave.teamId);
|
|
1003
|
+
}
|
|
1004
|
+
if (opts.figmaToken) {
|
|
1005
|
+
printKeyValue("Figma token", "saved");
|
|
1006
|
+
}
|
|
1007
|
+
} else {
|
|
1008
|
+
outputResult(mode, {
|
|
1009
|
+
success: true,
|
|
1010
|
+
action: "token_saved",
|
|
1011
|
+
path: getCredentialsPath(),
|
|
1012
|
+
...opts.token ? { teamId: toSave.teamId } : {},
|
|
1013
|
+
...opts.figmaToken ? { figmaToken: true } : {}
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
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
|
+
if (opts.logout) {
|
|
1033
|
+
const deleted = deleteCredentials();
|
|
1034
|
+
if (mode === "pretty") {
|
|
1035
|
+
if (deleted) {
|
|
1036
|
+
printSuccess("Credentials cleared");
|
|
1037
|
+
} else {
|
|
1038
|
+
printInfo("No stored credentials found");
|
|
1039
|
+
}
|
|
1040
|
+
} else {
|
|
1041
|
+
outputResult(mode, { success: true, action: "logged_out", hadCredentials: deleted });
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
if (opts.status) {
|
|
1046
|
+
try {
|
|
1047
|
+
const auth = resolveAuth();
|
|
1048
|
+
const creds = readCredentials();
|
|
1049
|
+
const sourceName = auth.source === "env" ? "ANIMA_API_TOKEN env var" : getCredentialsPath();
|
|
1050
|
+
const hasFigma = !!(process.env.FIGMA_TOKEN || creds?.figmaToken);
|
|
1051
|
+
if (mode === "pretty") {
|
|
1052
|
+
printSuccess("Token configured");
|
|
1053
|
+
printKeyValue("Source", sourceName);
|
|
1054
|
+
printKeyValue("Anima token", auth.token.slice(0, 12) + "..." + auth.token.slice(-4));
|
|
1055
|
+
if (auth.teamId) printKeyValue("Team ID", auth.teamId);
|
|
1056
|
+
printKeyValue("Figma token", hasFigma ? "configured" : "not set (needed for codegen/f2c)");
|
|
1057
|
+
} else {
|
|
1058
|
+
outputResult(mode, {
|
|
1059
|
+
success: true,
|
|
1060
|
+
source: auth.source,
|
|
1061
|
+
tokenPreview: auth.token.slice(0, 12) + "..." + auth.token.slice(-4),
|
|
1062
|
+
teamId: auth.teamId,
|
|
1063
|
+
figmaToken: hasFigma
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
if (err instanceof CliError) {
|
|
1068
|
+
if (mode === "pretty") {
|
|
1069
|
+
printError(err.message);
|
|
1070
|
+
} else {
|
|
1071
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1072
|
+
}
|
|
1073
|
+
} else {
|
|
1074
|
+
throw err;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (mode === "pretty") {
|
|
1080
|
+
console.log();
|
|
1081
|
+
printInfo("Auth commands:");
|
|
1082
|
+
console.log(` ${chalk2.cyan("anima auth --token <T>")} Save Anima API token`);
|
|
1083
|
+
console.log(` ${chalk2.cyan("anima auth --figma-token <T>")} Save Figma token`);
|
|
1084
|
+
console.log(` ${chalk2.cyan("anima auth --token <T> --figma-token <T>")} Save both at once`);
|
|
1085
|
+
console.log(` ${chalk2.cyan("anima auth --login")} Login via browser`);
|
|
1086
|
+
console.log(` ${chalk2.cyan("anima auth --status")} Check auth status`);
|
|
1087
|
+
console.log(` ${chalk2.cyan("anima auth --logout")} Clear all credentials`);
|
|
1088
|
+
console.log();
|
|
1089
|
+
console.log(` Anima token: ${chalk2.cyan.underline("https://dev.animaapp.com")} > Settings > API Keys`);
|
|
1090
|
+
console.log(` Figma token: ${chalk2.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
1091
|
+
console.log();
|
|
1092
|
+
} else {
|
|
1093
|
+
outputResult(mode, {
|
|
1094
|
+
success: true,
|
|
1095
|
+
message: "Use --token, --login, --status, or --logout"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
if (err instanceof CliError) {
|
|
1100
|
+
if (mode === "pretty") {
|
|
1101
|
+
printError(err.message);
|
|
1102
|
+
} else {
|
|
1103
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1104
|
+
}
|
|
1105
|
+
process.exit(err.exitCode);
|
|
1106
|
+
}
|
|
1107
|
+
throw err;
|
|
1108
|
+
}
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
// src/index.ts
|
|
1112
|
+
var program = new Command6().name("anima").description(
|
|
1113
|
+
`Anima CLI - Generate production-ready apps from prompts, URLs, or Figma designs.
|
|
1114
|
+
|
|
1115
|
+
AUTHENTICATION:
|
|
1116
|
+
Set ANIMA_API_TOKEN env var or run: npx @animaapp/cli auth --token <token>
|
|
1117
|
+
Get key: https://dev.animaapp.com > Settings > API Keys
|
|
1118
|
+
|
|
1119
|
+
TIMING (important for AI agents):
|
|
1120
|
+
create \u2192 3-7 minutes (SSE streaming, set timeout to 600000ms)
|
|
1121
|
+
publish \u2192 1-3 minutes
|
|
1122
|
+
codegen \u2192 2-5 minutes
|
|
1123
|
+
download \u2192 seconds
|
|
1124
|
+
The --timeout default is 600000ms (10 min). Do NOT reduce it.
|
|
1125
|
+
|
|
1126
|
+
COMMANDS:
|
|
1127
|
+
create Create app from prompt, URL, or Figma design
|
|
1128
|
+
publish Deploy playground to live URL or npm package
|
|
1129
|
+
codegen Convert Figma design to code files (no playground)
|
|
1130
|
+
download Download project from an Anima playground
|
|
1131
|
+
auth Manage authentication (token stored in ~/.config/anima/)
|
|
1132
|
+
|
|
1133
|
+
EXAMPLES:
|
|
1134
|
+
npx @animaapp/cli auth --token <your-token>
|
|
1135
|
+
npx @animaapp/cli create -t p2c -p "SaaS dashboard with sidebar and analytics"
|
|
1136
|
+
npx @animaapp/cli create -t l2c -u https://stripe.com
|
|
1137
|
+
npx @animaapp/cli create -t f2c --file-key abc --nodes 42:15
|
|
1138
|
+
npx @animaapp/cli publish abc123xyz
|
|
1139
|
+
npx @animaapp/cli codegen --file-key abc --nodes 42:15 -o ./components
|
|
1140
|
+
npx @animaapp/cli download https://dev.animaapp.com/chat/abc123xyz
|
|
1141
|
+
|
|
1142
|
+
OUTPUT: JSON to stdout when piped (for AI tools), pretty text in terminals.
|
|
1143
|
+
Progress is streamed to stderr via --verbose when in JSON mode.`
|
|
1144
|
+
).version("0.1.0", "-v, --version");
|
|
1145
|
+
program.addCommand(createCommand);
|
|
1146
|
+
program.addCommand(publishCommand);
|
|
1147
|
+
program.addCommand(codegenCommand);
|
|
1148
|
+
program.addCommand(downloadCommand);
|
|
1149
|
+
program.addCommand(authCommand);
|
|
1150
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1151
|
+
if (err instanceof CliError) {
|
|
1152
|
+
if (process.stdout.isTTY) {
|
|
1153
|
+
console.error(err.message);
|
|
1154
|
+
} else {
|
|
1155
|
+
process.stdout.write(
|
|
1156
|
+
JSON.stringify({
|
|
1157
|
+
success: false,
|
|
1158
|
+
error: { code: err.code, message: err.message }
|
|
1159
|
+
}) + "\n"
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
1162
|
+
process.exit(err.exitCode);
|
|
1163
|
+
}
|
|
1164
|
+
console.error(err);
|
|
1165
|
+
process.exit(1);
|
|
1166
|
+
});
|
|
1167
|
+
//# sourceMappingURL=index.js.map
|