@animaapp/cli 0.3.11 → 0.4.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 +43 -11
- package/dist/index.js +308 -53
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -95,10 +95,13 @@ npx @animaapp/cli create -t p2c -p "E-commerce product page with cart"
|
|
|
95
95
|
npx @animaapp/cli create -t l2c -u https://linear.app
|
|
96
96
|
npx @animaapp/cli create -t f2c --file-key <key> --nodes 42:15
|
|
97
97
|
|
|
98
|
-
# 3. Or bring YOUR OWN code:
|
|
99
|
-
npx @animaapp/cli create -t
|
|
100
|
-
npx @animaapp/cli get-git-token <sessionId> # then: git clone <url>, add code, git push
|
|
98
|
+
# 3. Or bring YOUR OWN code: import it in one step -> publish
|
|
99
|
+
npx @animaapp/cli create -t import --from ./my-project
|
|
101
100
|
npx @animaapp/cli publish <sessionId>
|
|
101
|
+
|
|
102
|
+
# ...or start empty and push over git
|
|
103
|
+
npx @animaapp/cli create -t empty --framework react --name "My project"
|
|
104
|
+
npx @animaapp/cli get-git-token <sessionId> # then: git clone <url>, add code, git push
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
---
|
|
@@ -142,21 +145,25 @@ by `mcp-config` and `--print-mcp-config`.
|
|
|
142
145
|
### `create` — a playground for your code, or AI-generated
|
|
143
146
|
|
|
144
147
|
```bash
|
|
145
|
-
npx @animaapp/cli create -t
|
|
148
|
+
npx @animaapp/cli create -t import --from ./my-project # import YOUR code (instant)
|
|
149
|
+
npx @animaapp/cli create -t empty --framework react --name "My project" # empty repo you push to (instant)
|
|
146
150
|
npx @animaapp/cli create -t p2c -p "Analytics dashboard with a sidebar"
|
|
147
151
|
npx @animaapp/cli create -t l2c -u https://stripe.com
|
|
148
152
|
npx @animaapp/cli create -t f2c --file-key <key> --nodes 42:15
|
|
149
153
|
```
|
|
150
154
|
|
|
151
|
-
`-t
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
+
`-t import` is the one-step upload path: your folder (or .zip) becomes the
|
|
156
|
+
playground's first commit — small text-only projects are sent inline, larger
|
|
157
|
+
or binary ones are zipped and uploaded via a presigned URL automatically.
|
|
158
|
+
`-t empty` creates an empty repository instead: follow with `git clone` of the
|
|
159
|
+
returned URL, add your code, and `git push`. Then `publish <sessionId>` for a
|
|
160
|
+
live URL. The other types are AI generation (3–7 min).
|
|
155
161
|
|
|
156
162
|
| Option | Values | Default |
|
|
157
163
|
|--------|--------|---------|
|
|
158
|
-
| `-t, --type` | `
|
|
159
|
-
| `--name` | project name | _(
|
|
164
|
+
| `-t, --type` | `import` (your code, with `--from`), `empty` (repo to push to), `p2c` (prompt), `l2c` (URL), `f2c` (Figma) | _required_ |
|
|
165
|
+
| `--name` | project name | _(empty/import)_ |
|
|
166
|
+
| `--from` | folder or .zip to import | _(import)_ |
|
|
160
167
|
| `-p, --prompt` | free text | _(p2c)_ |
|
|
161
168
|
| `-u, --url` | website URL | _(l2c)_ |
|
|
162
169
|
| `--file-key` | Figma file key or URL | _(f2c)_ |
|
|
@@ -188,7 +195,12 @@ npx @animaapp/cli codegen --file-key <key-or-url> --nodes 42:15 -o ./components
|
|
|
188
195
|
| `--language` | `typescript`, `javascript` | `typescript` (react) |
|
|
189
196
|
| `--ui-library` | `shadcn`, `mui`, `antd`, `clean_react` | _(none)_ |
|
|
190
197
|
|
|
191
|
-
### `publish` — deploy a session (1–3 min)
|
|
198
|
+
### `publish` — deploy a session to a public URL (1–3 min)
|
|
199
|
+
|
|
200
|
+
Publishing makes the playground **public to the world**. Sharing usually
|
|
201
|
+
doesn't need it — the playground is already visible at its `playgroundUrl`.
|
|
202
|
+
**Agents:** only publish when the human explicitly asked for a public site;
|
|
203
|
+
otherwise share the playground URL and offer publishing as a follow-up.
|
|
192
204
|
|
|
193
205
|
```bash
|
|
194
206
|
npx @animaapp/cli publish <sessionId>
|
|
@@ -201,6 +213,26 @@ npx @animaapp/cli publish <sessionId> --mode designSystem --package-name my-ds
|
|
|
201
213
|
| `--package-name` | npm package name | _(designSystem)_ |
|
|
202
214
|
| `--package-version` | npm package version | _(designSystem)_ |
|
|
203
215
|
|
|
216
|
+
### `unpublish` — take a published playground offline
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
npx @animaapp/cli unpublish <sessionId>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The inverse of `publish`: clears the live URL so the deployed site stops being
|
|
223
|
+
reachable. The playground, its code, and its content are untouched; publishing
|
|
224
|
+
again reuses the same subdomain.
|
|
225
|
+
|
|
226
|
+
### `update` — rename or change visibility (metadata only)
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
npx @animaapp/cli update <sessionId> --name "New name"
|
|
230
|
+
npx @animaapp/cli update <sessionId> --privacy public # anyone with the link
|
|
231
|
+
npx @animaapp/cli update <sessionId> --privacy private # team only
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Never touches code or content — that's the git flow (`get-git-token`).
|
|
235
|
+
|
|
204
236
|
### `get-git-token` — read/edit a playground's code over git
|
|
205
237
|
|
|
206
238
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { Command as
|
|
4
|
+
import { Command as Command12 } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/create.ts
|
|
7
7
|
import { Command } from "commander";
|
|
8
|
+
import chalk2 from "chalk";
|
|
8
9
|
|
|
9
10
|
// src/auth/store.ts
|
|
10
11
|
import fs from "fs";
|
|
@@ -309,6 +310,7 @@ var ProgressReporter = class {
|
|
|
309
310
|
start(message) {
|
|
310
311
|
this.#startTime = Date.now();
|
|
311
312
|
if (this.#mode === "pretty") {
|
|
313
|
+
this.#spinner?.stop();
|
|
312
314
|
this.#spinner = ora(message).start();
|
|
313
315
|
} else if (this.#verbose) {
|
|
314
316
|
process.stderr.write(JSON.stringify({ status: "started", message }) + "\n");
|
|
@@ -428,6 +430,90 @@ function normalizeNodeId(nodeId) {
|
|
|
428
430
|
return nodeId.replace(/-/g, ":");
|
|
429
431
|
}
|
|
430
432
|
|
|
433
|
+
// src/utils/projectSource.ts
|
|
434
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
435
|
+
import { join, relative, extname } from "path";
|
|
436
|
+
import AdmZip from "adm-zip";
|
|
437
|
+
var SKIPPED_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", "dist", "build", ".next", "out", "coverage", ".turbo", ".cache"]);
|
|
438
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
439
|
+
".png",
|
|
440
|
+
".jpg",
|
|
441
|
+
".jpeg",
|
|
442
|
+
".gif",
|
|
443
|
+
".webp",
|
|
444
|
+
".ico",
|
|
445
|
+
".bmp",
|
|
446
|
+
".woff",
|
|
447
|
+
".woff2",
|
|
448
|
+
".ttf",
|
|
449
|
+
".otf",
|
|
450
|
+
".eot",
|
|
451
|
+
".mp4",
|
|
452
|
+
".webm",
|
|
453
|
+
".mp3",
|
|
454
|
+
".wav",
|
|
455
|
+
".ogg",
|
|
456
|
+
".zip",
|
|
457
|
+
".tar",
|
|
458
|
+
".gz",
|
|
459
|
+
".pdf",
|
|
460
|
+
".wasm"
|
|
461
|
+
]);
|
|
462
|
+
var MAX_INLINE_FILES = 1e3;
|
|
463
|
+
var MAX_INLINE_BYTES = 10 * 1024 * 1024;
|
|
464
|
+
function walk(root, dir, out) {
|
|
465
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
466
|
+
if (entry.isDirectory()) {
|
|
467
|
+
if (!SKIPPED_DIRS.has(entry.name)) walk(root, join(dir, entry.name), out);
|
|
468
|
+
} else if (entry.isFile()) {
|
|
469
|
+
out.push(relative(root, join(dir, entry.name)));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function zipFolder(root) {
|
|
474
|
+
const zip = new AdmZip();
|
|
475
|
+
zip.addLocalFolder(root, "", (entryPath) => {
|
|
476
|
+
const segments = entryPath.split(/[\\/]/);
|
|
477
|
+
return !segments.some((segment) => SKIPPED_DIRS.has(segment));
|
|
478
|
+
});
|
|
479
|
+
return zip.toBuffer();
|
|
480
|
+
}
|
|
481
|
+
function isProbablyBinary(path5, buffer) {
|
|
482
|
+
if (BINARY_EXTENSIONS.has(extname(path5).toLowerCase())) return true;
|
|
483
|
+
return buffer.includes(0);
|
|
484
|
+
}
|
|
485
|
+
function collectProjectSource(fromPath) {
|
|
486
|
+
const stats = statSync(fromPath);
|
|
487
|
+
if (stats.isFile()) {
|
|
488
|
+
if (extname(fromPath).toLowerCase() !== ".zip") {
|
|
489
|
+
throw new Error(`--from must be a folder or a .zip file, got: ${fromPath}`);
|
|
490
|
+
}
|
|
491
|
+
return { kind: "zip", buffer: readFileSync(fromPath), reason: "zip file given" };
|
|
492
|
+
}
|
|
493
|
+
const paths = [];
|
|
494
|
+
walk(fromPath, fromPath, paths);
|
|
495
|
+
if (paths.length === 0) {
|
|
496
|
+
throw new Error(`--from folder is empty: ${fromPath}`);
|
|
497
|
+
}
|
|
498
|
+
if (paths.length > MAX_INLINE_FILES) {
|
|
499
|
+
return { kind: "zip", buffer: zipFolder(fromPath), reason: `${paths.length} files exceeds the inline limit` };
|
|
500
|
+
}
|
|
501
|
+
const files = {};
|
|
502
|
+
let totalBytes = 0;
|
|
503
|
+
for (const path5 of paths) {
|
|
504
|
+
const buffer = readFileSync(join(fromPath, path5));
|
|
505
|
+
if (isProbablyBinary(path5, buffer)) {
|
|
506
|
+
return { kind: "zip", buffer: zipFolder(fromPath), reason: `binary file present (${path5})` };
|
|
507
|
+
}
|
|
508
|
+
totalBytes += buffer.length;
|
|
509
|
+
if (totalBytes > MAX_INLINE_BYTES) {
|
|
510
|
+
return { kind: "zip", buffer: zipFolder(fromPath), reason: "project exceeds the inline size limit" };
|
|
511
|
+
}
|
|
512
|
+
files[path5.split("\\").join("/")] = buffer.toString("utf8");
|
|
513
|
+
}
|
|
514
|
+
return { kind: "files", files, fileCount: paths.length };
|
|
515
|
+
}
|
|
516
|
+
|
|
431
517
|
// src/config/store.ts
|
|
432
518
|
import fs3 from "fs";
|
|
433
519
|
import path3 from "path";
|
|
@@ -485,7 +571,15 @@ function resolveApiUrl(flagValue) {
|
|
|
485
571
|
|
|
486
572
|
// src/commands/create.ts
|
|
487
573
|
var PLAYGROUND_BASE = "https://dev.animaapp.com/chat";
|
|
488
|
-
var createCommand = new Command("create").description(
|
|
574
|
+
var createCommand = new Command("create").description(
|
|
575
|
+
"Create a playground: AI-generated (p2c/l2c/f2c, 3-7 min), empty for code you'll push (instant), or import your existing code (instant)"
|
|
576
|
+
).requiredOption(
|
|
577
|
+
"-t, --type <type>",
|
|
578
|
+
"p2c, l2c, f2c, empty (empty repo you push to), or import (create FROM your code \u2014 use --from)"
|
|
579
|
+
).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(
|
|
580
|
+
"--framework <fw>",
|
|
581
|
+
"html | react \u2014 the only supported frameworks. REQUIRED for empty (declare react if you will push React code); detected from package.json for import"
|
|
582
|
+
).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("--name <name>", "Project name (empty/import only)").option("--from <path>", "Folder or .zip with your project (import only) \u2014 becomes the first commit").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) => {
|
|
489
583
|
const mode = detectOutputMode(opts);
|
|
490
584
|
initLogger(opts.logFile);
|
|
491
585
|
const progress = new ProgressReporter(mode, opts.verbose);
|
|
@@ -495,40 +589,90 @@ var createCommand = new Command("create").description("Create a playground: AI-g
|
|
|
495
589
|
const auth = resolveAuth();
|
|
496
590
|
const figmaToken = type === "f2c" ? resolveFigmaToken(opts, auth.figmaToken) : void 0;
|
|
497
591
|
const args = buildCreateArgs(type, opts);
|
|
498
|
-
|
|
592
|
+
const mcpConfig = { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken };
|
|
593
|
+
if (type === "import") {
|
|
594
|
+
progress.start("Collecting project files...");
|
|
595
|
+
const source = collectProjectSource(opts.from);
|
|
596
|
+
if (source.kind === "files") {
|
|
597
|
+
args.files = source.files;
|
|
598
|
+
} else {
|
|
599
|
+
progress.update(`Uploading project zip (${source.reason})...`);
|
|
600
|
+
const grant = await withMcp(
|
|
601
|
+
mcpConfig,
|
|
602
|
+
(client) => client.callTool("playground-get_zip_upload_url", {})
|
|
603
|
+
);
|
|
604
|
+
if (!grant.success || !grant.uploadUrl || !grant.zipUploadId) {
|
|
605
|
+
const message = grant.reason || grant.error || "Could not get a zip upload URL";
|
|
606
|
+
throw apiError(grant.nextSteps ? `${message}
|
|
607
|
+
${grant.nextSteps}` : message, grant);
|
|
608
|
+
}
|
|
609
|
+
const putResponse = await fetch(grant.uploadUrl, {
|
|
610
|
+
method: "PUT",
|
|
611
|
+
body: new Uint8Array(source.buffer)
|
|
612
|
+
});
|
|
613
|
+
if (!putResponse.ok) {
|
|
614
|
+
throw apiError(`Zip upload failed: HTTP ${putResponse.status}`, { status: putResponse.status });
|
|
615
|
+
}
|
|
616
|
+
args.zipUploadId = grant.zipUploadId;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
progress.start(
|
|
620
|
+
type === "empty" ? "Creating empty playground..." : type === "import" ? "Importing your project..." : `Creating ${type.toUpperCase()} playground...`
|
|
621
|
+
);
|
|
499
622
|
const result = await withMcp(
|
|
500
|
-
|
|
623
|
+
mcpConfig,
|
|
501
624
|
(client) => client.callTool("playground-create", args, {
|
|
502
625
|
timeoutMs: parseInt(opts.timeout, 10),
|
|
503
626
|
onProgress: (p) => progress.update(p.message ?? `Generating (${Math.round(p.progress)}%)...`)
|
|
504
627
|
})
|
|
505
628
|
);
|
|
506
629
|
if (!result.success || !result.sessionId) {
|
|
507
|
-
|
|
630
|
+
const message = result.reason || result.error || "Generation failed";
|
|
631
|
+
throw apiError(result.nextSteps ? `${message}
|
|
632
|
+
${result.nextSteps}` : message, result);
|
|
508
633
|
}
|
|
509
634
|
const playgroundUrl = result.playgroundUrl || `${PLAYGROUND_BASE}/${result.sessionId}`;
|
|
510
635
|
progress.succeed("Playground created!");
|
|
511
636
|
if (mode === "pretty") {
|
|
512
637
|
printSessionResult({ sessionId: result.sessionId, playgroundUrl });
|
|
513
638
|
if (result.gitRemoteUrl) {
|
|
514
|
-
console.log(` git clone ${result.gitRemoteUrl}`);
|
|
639
|
+
console.log(` ${chalk2.cyan(`git clone ${result.gitRemoteUrl}`)}`);
|
|
515
640
|
console.log();
|
|
516
|
-
console.log(
|
|
517
|
-
|
|
518
|
-
|
|
641
|
+
console.log(" Commit and push to update the live playground.");
|
|
642
|
+
if (result.expiresAt) {
|
|
643
|
+
console.log(
|
|
644
|
+
` The URL embeds a token that expires at ${result.expiresAt} and cannot be renewed;
|
|
645
|
+
run ${chalk2.cyan(`anima get-git-token ${result.sessionId}`)} and ${chalk2.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
|
|
519
649
|
console.log();
|
|
520
|
-
} else if (type === "
|
|
650
|
+
} else if (type === "empty") {
|
|
521
651
|
console.log(
|
|
522
652
|
` Next: anima get-git-token ${result.sessionId} \u2014 then git clone, add your code, and git push.`
|
|
523
653
|
);
|
|
524
654
|
console.log();
|
|
525
655
|
}
|
|
656
|
+
if (type === "import" && result.fileCount !== void 0) {
|
|
657
|
+
console.log(` ${result.fileCount} files committed (framework: ${result.framework ?? "detected server-side"}).`);
|
|
658
|
+
if (result.skippedFiles?.length) {
|
|
659
|
+
console.log(` Skipped (blocked/hidden): ${result.skippedFiles.join(", ")}`);
|
|
660
|
+
}
|
|
661
|
+
if (result.notes) {
|
|
662
|
+
console.log(` Note: ${result.notes}`);
|
|
663
|
+
}
|
|
664
|
+
console.log();
|
|
665
|
+
}
|
|
526
666
|
} else {
|
|
527
667
|
outputResult(mode, {
|
|
528
668
|
success: true,
|
|
529
669
|
sessionId: result.sessionId,
|
|
530
670
|
playgroundUrl,
|
|
531
671
|
...result.gitRemoteUrl ? { gitRemoteUrl: result.gitRemoteUrl, access: result.access, expiresAt: result.expiresAt } : {},
|
|
672
|
+
...result.framework ? { framework: result.framework } : {},
|
|
673
|
+
...result.fileCount !== void 0 ? { fileCount: result.fileCount } : {},
|
|
674
|
+
...result.skippedFiles?.length ? { skippedFiles: result.skippedFiles } : {},
|
|
675
|
+
...result.notes ? { notes: result.notes } : {},
|
|
532
676
|
...result.nextSteps ? { nextSteps: result.nextSteps } : {}
|
|
533
677
|
});
|
|
534
678
|
}
|
|
@@ -546,8 +690,8 @@ var createCommand = new Command("create").description("Create a playground: AI-g
|
|
|
546
690
|
}
|
|
547
691
|
});
|
|
548
692
|
function validateCreateOptions(type, opts) {
|
|
549
|
-
if (!["p2c", "l2c", "f2c", "
|
|
550
|
-
throw validationError(`Invalid type "${type}". Must be p2c, l2c, f2c, or
|
|
693
|
+
if (!["p2c", "l2c", "f2c", "empty", "import"].includes(type)) {
|
|
694
|
+
throw validationError(`Invalid type "${type}". Must be p2c, l2c, f2c, empty, or import.`);
|
|
551
695
|
}
|
|
552
696
|
if (type === "p2c" && !opts.prompt) {
|
|
553
697
|
throw validationError("--prompt is required for p2c type");
|
|
@@ -558,6 +702,17 @@ function validateCreateOptions(type, opts) {
|
|
|
558
702
|
if (type === "f2c" && !opts.fileKey) {
|
|
559
703
|
throw validationError("--file-key is required for f2c type");
|
|
560
704
|
}
|
|
705
|
+
if (type === "empty" && !opts.framework) {
|
|
706
|
+
throw validationError(
|
|
707
|
+
"--framework is required for type empty \u2014 html for plain HTML/CSS/JS, react for React (declare react if you will push React code)"
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
if (type === "import" && !opts.from) {
|
|
711
|
+
throw validationError("--from <folder|zip> is required for type import \u2014 the path to the project to import");
|
|
712
|
+
}
|
|
713
|
+
if (type !== "import" && opts.from) {
|
|
714
|
+
throw validationError("--from is only valid with -t import");
|
|
715
|
+
}
|
|
561
716
|
}
|
|
562
717
|
function resolveFigmaToken(opts, stored) {
|
|
563
718
|
const figmaToken = opts.figmaToken || process.env.FIGMA_TOKEN || stored;
|
|
@@ -569,10 +724,14 @@ function resolveFigmaToken(opts, stored) {
|
|
|
569
724
|
return figmaToken;
|
|
570
725
|
}
|
|
571
726
|
function buildCreateArgs(type, opts) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
727
|
+
if (type === "empty" || type === "import") {
|
|
728
|
+
return {
|
|
729
|
+
type,
|
|
730
|
+
...opts.framework ? { framework: opts.framework } : {},
|
|
731
|
+
...opts.name ? { name: opts.name } : {}
|
|
732
|
+
};
|
|
575
733
|
}
|
|
734
|
+
const framework = opts.framework ?? "html";
|
|
576
735
|
const language = opts.language ?? (framework === "react" ? "typescript" : void 0);
|
|
577
736
|
const args = {
|
|
578
737
|
type,
|
|
@@ -778,7 +937,7 @@ var codegenCommand = new Command3("codegen").description("Convert Figma design t
|
|
|
778
937
|
|
|
779
938
|
// src/commands/getGitToken.ts
|
|
780
939
|
import { Command as Command4 } from "commander";
|
|
781
|
-
import
|
|
940
|
+
import chalk3 from "chalk";
|
|
782
941
|
|
|
783
942
|
// src/utils/playgroundUrl.ts
|
|
784
943
|
function parsePlaygroundUrl(input) {
|
|
@@ -847,7 +1006,7 @@ var getGitTokenCommand = new Command4("get-git-token").description("Mint a short
|
|
|
847
1006
|
progress.succeed(`Git access granted (${grant.access === "rw" ? "read-write" : "read-only"})`);
|
|
848
1007
|
if (mode === "pretty") {
|
|
849
1008
|
console.log();
|
|
850
|
-
console.log(` ${
|
|
1009
|
+
console.log(` ${chalk3.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
|
|
851
1010
|
console.log();
|
|
852
1011
|
if (grant.access === "rw") {
|
|
853
1012
|
console.log(" Commit and push to update the live playground.");
|
|
@@ -857,7 +1016,7 @@ var getGitTokenCommand = new Command4("get-git-token").description("Mint a short
|
|
|
857
1016
|
if (grant.expiresAt) {
|
|
858
1017
|
console.log(
|
|
859
1018
|
` The URL embeds a token that expires at ${grant.expiresAt} and cannot be renewed;
|
|
860
|
-
re-run this command and ${
|
|
1019
|
+
re-run this command and ${chalk3.cyan("git remote set-url origin <new url>")} to keep pushing.`
|
|
861
1020
|
);
|
|
862
1021
|
}
|
|
863
1022
|
console.log(" Treat the URL as a secret \u2014 it grants access to this playground.");
|
|
@@ -890,7 +1049,7 @@ import { Command as Command6 } from "commander";
|
|
|
890
1049
|
|
|
891
1050
|
// src/commands/login.ts
|
|
892
1051
|
import { Command as Command5 } from "commander";
|
|
893
|
-
import
|
|
1052
|
+
import chalk4 from "chalk";
|
|
894
1053
|
import open from "open";
|
|
895
1054
|
|
|
896
1055
|
// src/auth/device.ts
|
|
@@ -1123,9 +1282,9 @@ var loginCommand = new Command5("login").description("Log in via the device flow
|
|
|
1123
1282
|
console.log();
|
|
1124
1283
|
printInfo("To connect, open the verification page and enter this code:");
|
|
1125
1284
|
console.log();
|
|
1126
|
-
console.log(` ${
|
|
1285
|
+
console.log(` ${chalk4.cyan.underline(info.verificationUri)}`);
|
|
1127
1286
|
console.log();
|
|
1128
|
-
console.log(` ${
|
|
1287
|
+
console.log(` ${chalk4.bold.white(info.userCode)}`);
|
|
1129
1288
|
console.log();
|
|
1130
1289
|
printInfo(`Waiting for approval\u2026 (code expires in ${Math.round(info.expiresInSeconds / 60)} minutes)`);
|
|
1131
1290
|
} else {
|
|
@@ -1233,7 +1392,7 @@ var mcpConfigCommand = new Command6("mcp-config").description("Print an MCP serv
|
|
|
1233
1392
|
|
|
1234
1393
|
// src/commands/auth.ts
|
|
1235
1394
|
import { Command as Command7 } from "commander";
|
|
1236
|
-
import
|
|
1395
|
+
import chalk5 from "chalk";
|
|
1237
1396
|
var DEFAULT_API_URL2 = "https://public-api.animaapp.com";
|
|
1238
1397
|
var authCommand = new Command7("auth").description("Manage authentication").option("--token <token>", "Store Anima API token").option("--figma-token <token>", "Store Figma personal access token").option("--team-id <teamId>", "Team ID to associate with token").option("--status", "Show current auth status").option("--logout", "Clear stored credentials (alias for `anima logout`)").option("--json", "Output as JSON").option("--api-url <url>", "API base URL", DEFAULT_API_URL2).action(async (opts) => {
|
|
1239
1398
|
const mode = detectOutputMode(opts);
|
|
@@ -1322,12 +1481,12 @@ var authCommand = new Command7("auth").description("Manage authentication").opti
|
|
|
1322
1481
|
if (mode === "pretty") {
|
|
1323
1482
|
console.log();
|
|
1324
1483
|
printInfo("Auth commands:");
|
|
1325
|
-
console.log(` ${
|
|
1326
|
-
console.log(` ${
|
|
1327
|
-
console.log(` ${
|
|
1328
|
-
console.log(` ${
|
|
1484
|
+
console.log(` ${chalk5.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
|
|
1485
|
+
console.log(` ${chalk5.cyan("anima logout")} Clear all credentials`);
|
|
1486
|
+
console.log(` ${chalk5.cyan("anima auth --status")} Check auth status`);
|
|
1487
|
+
console.log(` ${chalk5.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
|
|
1329
1488
|
console.log();
|
|
1330
|
-
console.log(` Figma token: ${
|
|
1489
|
+
console.log(` Figma token: ${chalk5.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
|
|
1331
1490
|
console.log();
|
|
1332
1491
|
} else {
|
|
1333
1492
|
outputResult(mode, {
|
|
@@ -1365,9 +1524,91 @@ var logoutCommand = new Command8("logout").description("Clear all stored credent
|
|
|
1365
1524
|
}
|
|
1366
1525
|
});
|
|
1367
1526
|
|
|
1368
|
-
// src/commands/
|
|
1527
|
+
// src/commands/unpublish.ts
|
|
1369
1528
|
import { Command as Command9 } from "commander";
|
|
1370
|
-
var
|
|
1529
|
+
var unpublishCommand = new Command9("unpublish").description("Take a published playground offline (clears its live URL; code and playground stay intact)").argument("<sessionId>", "Session ID of the published playground").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) => {
|
|
1530
|
+
const mode = detectOutputMode(opts);
|
|
1531
|
+
initLogger(opts.logFile);
|
|
1532
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1533
|
+
try {
|
|
1534
|
+
const auth = resolveAuth();
|
|
1535
|
+
progress.start("Unpublishing...");
|
|
1536
|
+
const result = await withMcp(
|
|
1537
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1538
|
+
(client) => client.callTool("playground-unpublish", { sessionId })
|
|
1539
|
+
);
|
|
1540
|
+
if (!result.success) {
|
|
1541
|
+
throw apiError(result.reason || result.error || "Unpublish failed", result);
|
|
1542
|
+
}
|
|
1543
|
+
progress.succeed("Unpublished");
|
|
1544
|
+
if (mode === "pretty") {
|
|
1545
|
+
printSuccess(result.message || `Playground ${sessionId} is no longer publicly reachable.`);
|
|
1546
|
+
} else {
|
|
1547
|
+
outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
|
|
1548
|
+
}
|
|
1549
|
+
} catch (err) {
|
|
1550
|
+
progress.fail("Failed");
|
|
1551
|
+
if (err instanceof CliError) {
|
|
1552
|
+
if (mode === "pretty") {
|
|
1553
|
+
printError(err.message);
|
|
1554
|
+
} else {
|
|
1555
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1556
|
+
}
|
|
1557
|
+
process.exit(err.exitCode);
|
|
1558
|
+
}
|
|
1559
|
+
throw err;
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
|
|
1563
|
+
// src/commands/update.ts
|
|
1564
|
+
import { Command as Command10 } from "commander";
|
|
1565
|
+
var updateCommand = new Command10("update").description("Update a playground's metadata: display name and/or visibility (never touches code)").argument("<sessionId>", "Session ID of the playground").option("--name <name>", "New display name").option("--privacy <privacy>", "public (anyone with the link) | private (team only)").option("--json", "Output as JSON").option("--verbose", "Show progress on stderr").option("--api-url <url>", "API base URL (overrides config / ANIMA_API_URL)").option("--log-file <path>", "Append a JSON debug log of each step to this file").action(async (sessionId, opts) => {
|
|
1566
|
+
const mode = detectOutputMode(opts);
|
|
1567
|
+
initLogger(opts.logFile);
|
|
1568
|
+
const progress = new ProgressReporter(mode, opts.verbose);
|
|
1569
|
+
try {
|
|
1570
|
+
if (!opts.name && !opts.privacy) {
|
|
1571
|
+
throw validationError("Nothing to update \u2014 pass --name and/or --privacy");
|
|
1572
|
+
}
|
|
1573
|
+
if (opts.privacy && !["public", "private"].includes(opts.privacy)) {
|
|
1574
|
+
throw validationError(`Invalid privacy "${opts.privacy}". Must be public or private.`);
|
|
1575
|
+
}
|
|
1576
|
+
const auth = resolveAuth();
|
|
1577
|
+
progress.start("Updating...");
|
|
1578
|
+
const result = await withMcp(
|
|
1579
|
+
{ apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId },
|
|
1580
|
+
(client) => client.callTool("playground-metadata-update", {
|
|
1581
|
+
sessionId,
|
|
1582
|
+
...opts.name ? { name: opts.name } : {},
|
|
1583
|
+
...opts.privacy ? { privacy: opts.privacy } : {}
|
|
1584
|
+
})
|
|
1585
|
+
);
|
|
1586
|
+
if (!result.success) {
|
|
1587
|
+
throw apiError(result.reason || result.error || "Update failed", result);
|
|
1588
|
+
}
|
|
1589
|
+
progress.succeed("Updated");
|
|
1590
|
+
if (mode === "pretty") {
|
|
1591
|
+
printSuccess(result.message || `Playground ${sessionId} updated.`);
|
|
1592
|
+
} else {
|
|
1593
|
+
outputResult(mode, { success: true, sessionId, ...result.message ? { message: result.message } : {} });
|
|
1594
|
+
}
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
progress.fail("Failed");
|
|
1597
|
+
if (err instanceof CliError) {
|
|
1598
|
+
if (mode === "pretty") {
|
|
1599
|
+
printError(err.message);
|
|
1600
|
+
} else {
|
|
1601
|
+
outputError(mode, { code: err.code, message: err.message });
|
|
1602
|
+
}
|
|
1603
|
+
process.exit(err.exitCode);
|
|
1604
|
+
}
|
|
1605
|
+
throw err;
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
|
|
1609
|
+
// src/commands/config.ts
|
|
1610
|
+
import { Command as Command11 } from "commander";
|
|
1611
|
+
var configCommand = new Command11("config").description(
|
|
1371
1612
|
`Store CLI preferences (keys: ${configKeys().join(", ")})`
|
|
1372
1613
|
);
|
|
1373
1614
|
configCommand.command("set <key> <value>").description("Set a config value").option("--json", "Output as JSON").action((key, value, opts) => {
|
|
@@ -1433,40 +1674,52 @@ function assertUrl(value) {
|
|
|
1433
1674
|
}
|
|
1434
1675
|
|
|
1435
1676
|
// src/index.ts
|
|
1436
|
-
var program = new
|
|
1437
|
-
`Anima CLI
|
|
1677
|
+
var program = new Command12().name("anima").description(
|
|
1678
|
+
`Anima CLI \u2014 host, edit, and publish your work on Anima. Every playground is a
|
|
1679
|
+
real git repository; each command runs an Anima tool for you.
|
|
1680
|
+
|
|
1681
|
+
THE MAIN FLOW (build something and put it on Anima):
|
|
1682
|
+
1. npx @animaapp/cli login # once; a human approves
|
|
1683
|
+
2. npx @animaapp/cli create -t import --from ./my-project # your code \u2192 a live playground
|
|
1684
|
+
(or: create -t empty --framework html \u2014 an empty repo you push to)
|
|
1685
|
+
3. The response includes a rw gitRemoteUrl \u2014 git clone, edit, commit, push:
|
|
1686
|
+
every push updates the live playground.
|
|
1687
|
+
4. Share the playgroundUrl with your human. That IS sharing \u2014 the playground
|
|
1688
|
+
is visible at its URL. Do NOT run publish unless the human explicitly
|
|
1689
|
+
asked for a public site; offer it as a follow-up question instead.
|
|
1438
1690
|
|
|
1439
1691
|
AUTHENTICATION:
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1692
|
+
npx @animaapp/cli login # device flow \u2014 a human approves on any device
|
|
1693
|
+
npx @animaapp/cli login --invite <url> # redeem a pre-approved invite link (no approval step)
|
|
1694
|
+
Credentials live in ~/.config/anima/; logout removes everything.
|
|
1443
1695
|
|
|
1444
1696
|
TIMING (important for AI agents):
|
|
1445
|
-
create
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
get-git-token \u2192 seconds
|
|
1449
|
-
The --timeout default is 600000ms (10 min). Do NOT reduce it.
|
|
1697
|
+
create -t empty|import \u2192 seconds
|
|
1698
|
+
create -t p2c|l2c|f2c \u2192 3-7 minutes AI generation (keep the default 600000ms timeout)
|
|
1699
|
+
publish/unpublish \u2192 1-3 minutes
|
|
1700
|
+
get-git-token / update \u2192 seconds
|
|
1450
1701
|
|
|
1451
1702
|
COMMANDS:
|
|
1452
|
-
login
|
|
1453
|
-
logout
|
|
1703
|
+
login Connect (device flow, or --invite to redeem an invite link)
|
|
1704
|
+
logout Full local reset \u2014 clears credentials and CLI config
|
|
1705
|
+
create Make a playground: import your code, empty repo, or AI-generated (p2c/l2c/f2c)
|
|
1706
|
+
get-git-token Fresh git access to an existing playground (or after token expiry)
|
|
1707
|
+
update Rename a playground or set visibility public/private (metadata only)
|
|
1708
|
+
publish Deploy to a public live URL \u2014 ONLY when the human asked for it
|
|
1709
|
+
unpublish Take a published playground offline (inverse of publish)
|
|
1454
1710
|
mcp-config Print an MCP server config from stored credentials (no new login)
|
|
1455
|
-
|
|
1456
|
-
publish Deploy playground to live URL or npm package
|
|
1457
|
-
codegen Convert Figma design to code files (no playground)
|
|
1458
|
-
get-git-token Mint a git remote URL for a playground; clone/edit/push with git
|
|
1711
|
+
codegen Convert Figma design to local code files (no playground)
|
|
1459
1712
|
config Store CLI preferences (e.g. a default API URL)
|
|
1460
1713
|
auth Inspect or manage credentials (status / figma token)
|
|
1461
1714
|
|
|
1462
|
-
EXAMPLES:
|
|
1463
|
-
npx @animaapp/cli login
|
|
1464
|
-
npx @animaapp/cli create -t
|
|
1465
|
-
npx @animaapp/cli create -t
|
|
1466
|
-
npx @animaapp/cli
|
|
1467
|
-
npx @animaapp/cli
|
|
1468
|
-
npx @animaapp/cli
|
|
1469
|
-
npx @animaapp/cli
|
|
1715
|
+
EXAMPLES (headless flow):
|
|
1716
|
+
npx @animaapp/cli login --invite https://public-api.animaapp.com/invite/<code>.md
|
|
1717
|
+
npx @animaapp/cli create -t import --from ./my-project
|
|
1718
|
+
npx @animaapp/cli create -t empty --framework react --name "My app"
|
|
1719
|
+
npx @animaapp/cli get-git-token <sessionId>
|
|
1720
|
+
npx @animaapp/cli update <sessionId> --privacy public
|
|
1721
|
+
npx @animaapp/cli publish <sessionId> # only on explicit request
|
|
1722
|
+
npx @animaapp/cli unpublish <sessionId>
|
|
1470
1723
|
|
|
1471
1724
|
OUTPUT: JSON to stdout when piped (for AI tools), pretty text in terminals.
|
|
1472
1725
|
Progress is streamed to stderr via --verbose when in JSON mode.`
|
|
@@ -1475,6 +1728,8 @@ program.addCommand(loginCommand);
|
|
|
1475
1728
|
program.addCommand(logoutCommand);
|
|
1476
1729
|
program.addCommand(createCommand);
|
|
1477
1730
|
program.addCommand(publishCommand);
|
|
1731
|
+
program.addCommand(unpublishCommand);
|
|
1732
|
+
program.addCommand(updateCommand);
|
|
1478
1733
|
program.addCommand(codegenCommand);
|
|
1479
1734
|
program.addCommand(getGitTokenCommand);
|
|
1480
1735
|
program.addCommand(mcpConfigCommand);
|