@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 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: blank playground -> git push -> publish
99
- npx @animaapp/cli create -t blank --name "My project"
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 blank --name "My project" # EMPTY playground for your own code (instant)
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 blank` is the upload path: it creates an empty playground and returns a
152
- `sessionId` — follow with `get-git-token <sessionId>`, `git clone`, add your
153
- code, and `git push`; then `publish <sessionId>` for a live URL. The other
154
- types are AI generation (3–7 min).
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` | `blank` (own code), `p2c` (prompt), `l2c` (URL), `f2c` (Figma) | _required_ |
159
- | `--name` | project name | _(blank)_ |
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 Command10 } from "commander";
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("Create a playground: AI-generated (p2c/l2c/f2c, 3-7 min) or blank for your own code (instant)").requiredOption("-t, --type <type>", "p2c, l2c, f2c, or blank (empty playground \u2014 push your own code with get-git-token)").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>", "html | react \u2014 the only supported frameworks (default: html)").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 (blank 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) => {
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
- progress.start(type === "blank" ? "Creating blank playground..." : `Creating ${type.toUpperCase()} playground...`);
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
- { apiBaseUrl: resolveApiUrl(opts.apiUrl), token: auth.token, teamId: auth.teamId, figmaToken },
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
- throw apiError(result.reason || result.error || "Generation failed", result);
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
- ` Add your code, commit, and git push \u2014 the playground goes live. The URL embeds a token${result.expiresAt ? ` (expires ${result.expiresAt})` : ""}; when it expires, run anima get-git-token ${result.sessionId} and git remote set-url origin <new url>.`
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 === "blank") {
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", "blank"].includes(type)) {
550
- throw validationError(`Invalid type "${type}". Must be p2c, l2c, f2c, or blank.`);
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
- const framework = opts.framework ?? "html";
573
- if (type === "blank") {
574
- return { type, framework, ...opts.name ? { name: opts.name } : {} };
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 chalk2 from "chalk";
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(` ${chalk2.cyan(`git clone ${grant.gitRemoteUrl}`)}`);
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 ${chalk2.cyan("git remote set-url origin <new url>")} to keep pushing.`
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 chalk3 from "chalk";
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(` ${chalk3.cyan.underline(info.verificationUri)}`);
1285
+ console.log(` ${chalk4.cyan.underline(info.verificationUri)}`);
1127
1286
  console.log();
1128
- console.log(` ${chalk3.bold.white(info.userCode)}`);
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 chalk4 from "chalk";
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(` ${chalk4.cyan("anima login")} Log in (device flow \u2014 approve on any browser)`);
1326
- console.log(` ${chalk4.cyan("anima logout")} Clear all credentials`);
1327
- console.log(` ${chalk4.cyan("anima auth --status")} Check auth status`);
1328
- console.log(` ${chalk4.cyan("anima auth --figma-token <T>")} Save Figma token (needed for codegen/f2c)`);
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: ${chalk4.cyan.underline("https://www.figma.com")} > Settings > Personal access tokens`);
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/config.ts
1527
+ // src/commands/unpublish.ts
1369
1528
  import { Command as Command9 } from "commander";
1370
- var configCommand = new Command9("config").description(
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 Command10().name("anima").description(
1437
- `Anima CLI - Generate production-ready apps from prompts, URLs, or Figma designs.
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
- Run: npx @animaapp/cli login
1441
- Approve in a browser on any device \u2014 the CLI receives a scoped agent token
1442
- (stored in ~/.config/anima/). CI/automation can set ANIMA_API_TOKEN instead.
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 \u2192 3-7 minutes (keep the default 600000ms timeout)
1446
- publish \u2192 1-3 minutes
1447
- codegen \u2192 2-5 minutes
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 Log in via the device flow (no browser needed on this machine)
1453
- logout Clear all stored credentials from this machine
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
- create Create app from prompt, URL, or Figma design
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 p2c -p "SaaS dashboard with sidebar and analytics"
1465
- npx @animaapp/cli create -t l2c -u https://stripe.com
1466
- npx @animaapp/cli create -t f2c --file-key abc --nodes 42:15
1467
- npx @animaapp/cli publish abc123xyz
1468
- npx @animaapp/cli codegen --file-key abc --nodes 42:15 -o ./components
1469
- npx @animaapp/cli get-git-token https://dev.animaapp.com/chat/abc123xyz
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);