@puckeditor/cli 0.24.0-canary.301a0304 → 0.24.0-canary.6f40e09b

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
@@ -20,8 +20,10 @@ Other commands:
20
20
  npx @puckeditor/cli add editor # add the Puck editor
21
21
  npx @puckeditor/cli add cloud # connect Puck Cloud
22
22
  npx @puckeditor/cli add ai # add the Puck AI plugin
23
+ npx @puckeditor/cli connect # log in to Puck Cloud again for a new API key
23
24
  npx @puckeditor/cli status # show what's set up
24
25
  npx @puckeditor/cli doctor # diagnose problems
26
+ npx @puckeditor/cli frameworks # show supported frameworks and versions
25
27
  npx @puckeditor/cli docs # read the docs for this version
26
28
  ```
27
29
 
package/dist/bin.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/bin.ts
4
4
  import os from "os";
5
- import path30 from "path";
5
+ import path32 from "path";
6
6
  import fs7 from "fs";
7
7
  import { fileURLToPath } from "url";
8
8
 
@@ -38,6 +38,7 @@ var exitCodeByError = {
38
38
  "PUCK-CLI-FRAMEWORK-MISMATCH": EXIT_CODES.unsupported,
39
39
  "PUCK-CLI-BACKEND-REQUIRED": EXIT_CODES.usage,
40
40
  "PUCK-CLI-BACKEND-URL-REQUIRED": EXIT_CODES.usage,
41
+ "PUCK-CLI-EXTERNAL-BACKEND": EXIT_CODES.unsupported,
41
42
  "PUCK-CLI-WORKSPACE-TARGET-NOT-FOUND": EXIT_CODES.unsupported,
42
43
  "PUCK-CLI-TYPESCRIPT-REQUIRED": EXIT_CODES.unsupported,
43
44
  "PUCK-CLI-TARGET-DIR-NOT-EMPTY": EXIT_CODES.filesystem,
@@ -67,9 +68,6 @@ var CliError = class extends Error {
67
68
  // src/args.ts
68
69
  import { parseArgs } from "util";
69
70
 
70
- // src/detect/framework.ts
71
- import path3 from "path";
72
-
73
71
  // src/constants.ts
74
72
  var ENV_KEY = "PUCK_API_KEY";
75
73
  var DEFAULT_CLOUD_URL = "https://cloud.puckeditor.com";
@@ -82,6 +80,9 @@ var CLOUD_CLIENT_PACKAGE = "@puckeditor/cloud-client";
82
80
  var PLUGIN_AI_PACKAGE = "@puckeditor/plugin-ai";
83
81
  var CANONICAL_INVOCATION = "npx @puckeditor/cli";
84
82
 
83
+ // src/detect/framework.ts
84
+ import path3 from "path";
85
+
85
86
  // src/ast/parse.ts
86
87
  import { parse } from "@babel/parser";
87
88
  var isTsx = (filename) => /\.(tsx|jsx|js|mjs|cjs)$/.test(filename);
@@ -190,12 +191,13 @@ var resolveInstalledVersion = (vfs, fromDir, name) => {
190
191
  dir = parent;
191
192
  }
192
193
  };
193
- var majorOf = (versionOrRange) => {
194
+ var versionOf = (versionOrRange) => {
194
195
  if (!versionOrRange) return null;
195
196
  const cleaned = versionOrRange.replace(/^(workspace:|npm:[^@]+@)/, "");
196
- const match = /(\d+)(?:\.\d+)?/.exec(cleaned);
197
- return match ? Number(match[1]) : null;
197
+ const match = /(\d+)(?:\.(\d+))?/.exec(cleaned);
198
+ return match ? [Number(match[1]), Number(match[2] ?? 0)] : null;
198
199
  };
200
+ var majorOf = (versionOrRange) => versionOf(versionOrRange)?.[0] ?? null;
199
201
 
200
202
  // src/detect/server.ts
201
203
  import path2 from "path";
@@ -283,7 +285,42 @@ var FRAMEWORK_LABELS = {
283
285
  hono: "Hono",
284
286
  express: "Express"
285
287
  };
288
+ var MIN_VERSIONS = {
289
+ next: "15",
290
+ "react-router": "7",
291
+ // Server route handlers on createFileRoute arrived in the Start RC
292
+ "tanstack-start": "1.132",
293
+ // vinext 1.0 moved to Vite 8, which the recipe uses
294
+ vinext: "1",
295
+ // @hono/vite-dev-server is built and tested against Vite 6
296
+ vite: "6",
297
+ // @astrojs/node 11, which `astro add node` installs, needs Astro 7
298
+ astro: "7",
299
+ // @hono/node-server 2 needs Hono 4
300
+ hono: "4",
301
+ // express.json() arrived in 4.16
302
+ express: "4.16"
303
+ };
286
304
  var SUPPORTED_FRAMEWORKS = "Next.js (App Router), React Router 7 framework mode, TanStack Start, vinext, Astro and Vite React apps, or a Hono or Express server";
305
+ var unsupportedVersion = (id, version, range) => {
306
+ const found = versionOf(version) ?? versionOf(range);
307
+ const [major, minor] = versionOf(MIN_VERSIONS[id]);
308
+ if (!found || found[0] > major || found[0] === major && found[1] >= minor)
309
+ return null;
310
+ const label2 = FRAMEWORK_LABELS[id];
311
+ return {
312
+ status: "unsupported",
313
+ error: {
314
+ code: "PUCK-CLI-UNSUPPORTED-FRAMEWORK-VERSION",
315
+ message: `${label2} ${version ?? range} is not supported. Upgrade to ${label2} ${MIN_VERSIONS[id]} or later, or integrate Puck manually.`,
316
+ details: {
317
+ docs: MANUAL_INTEGRATION_DOCS_URL,
318
+ version: version ?? range,
319
+ minVersion: MIN_VERSIONS[id]
320
+ }
321
+ }
322
+ };
323
+ };
287
324
  var firstExisting = (vfs, root, candidates) => candidates.find((c) => vfs.exists(path3.join(root, c))) ?? null;
288
325
  var CONFIG_EXTENSIONS = ["ts", "mts", "js", "mjs", "cts", "cjs"];
289
326
  var findViteConfig = (vfs, root) => firstExisting(
@@ -338,19 +375,8 @@ var detectNext = (vfs, root, range) => {
338
375
  const major = majorOf(version) ?? majorOf(range) ?? 16;
339
376
  const layout = detectAppRouter(vfs, root, "Next.js");
340
377
  if ("status" in layout) return layout;
341
- if (major < 15) {
342
- return {
343
- status: "unsupported",
344
- error: {
345
- code: "PUCK-CLI-UNSUPPORTED-FRAMEWORK-VERSION",
346
- message: `Next.js ${version ?? range} is not supported. Upgrade to Next.js 15 or later, or integrate Puck manually.`,
347
- details: {
348
- docs: MANUAL_INTEGRATION_DOCS_URL,
349
- version: version ?? range
350
- }
351
- }
352
- };
353
- }
378
+ const tooOld = unsupportedVersion("next", version, range);
379
+ if (tooOld) return tooOld;
354
380
  return {
355
381
  status: "detected",
356
382
  info: {
@@ -364,6 +390,8 @@ var detectNext = (vfs, root, range) => {
364
390
  };
365
391
  var detectVinext = (vfs, root, range) => {
366
392
  const version = resolveInstalledVersion(vfs, root, "vinext");
393
+ const tooOld = unsupportedVersion("vinext", version, range);
394
+ if (tooOld) return tooOld;
367
395
  const layout = detectAppRouter(vfs, root, "vinext");
368
396
  if ("status" in layout) return layout;
369
397
  const base = layout.baseDir ? `${layout.baseDir}/` : "";
@@ -385,16 +413,8 @@ var detectVinext = (vfs, root, range) => {
385
413
  var detectReactRouter = (vfs, root, range) => {
386
414
  const version = resolveInstalledVersion(vfs, root, "@react-router/dev") ?? resolveInstalledVersion(vfs, root, "react-router");
387
415
  const major = majorOf(version) ?? majorOf(range);
388
- if (major !== null && major < 7) {
389
- return {
390
- status: "unsupported",
391
- error: {
392
- code: "PUCK-CLI-UNSUPPORTED-FRAMEWORK-VERSION",
393
- message: `React Router ${version ?? range} is not supported. Upgrade to React Router 7 framework mode.`,
394
- details: { docs: MANUAL_INTEGRATION_DOCS_URL }
395
- }
396
- };
397
- }
416
+ const tooOld = unsupportedVersion("react-router", version, range);
417
+ if (tooOld) return tooOld;
398
418
  const configFile = firstExisting(
399
419
  vfs,
400
420
  root,
@@ -456,6 +476,8 @@ var detectReactRouter = (vfs, root, range) => {
456
476
  };
457
477
  var detectTanStackStart = (vfs, root, range) => {
458
478
  const version = resolveInstalledVersion(vfs, root, "@tanstack/react-start");
479
+ const tooOld = unsupportedVersion("tanstack-start", version, range);
480
+ if (tooOld) return tooOld;
459
481
  const viteConfig = findViteConfig(vfs, root);
460
482
  const code = viteConfig ? vfs.readText(path3.join(root, viteConfig)) ?? "" : "";
461
483
  const dirs = { srcDirectory: "src", routesDirectory: "routes" };
@@ -503,6 +525,8 @@ var detectVite = (vfs, root, range, deps) => {
503
525
  (f) => Boolean(f) && vfs.exists(path3.join(root, f))
504
526
  ) ?? null;
505
527
  const version = resolveInstalledVersion(vfs, root, "vite");
528
+ const tooOld = unsupportedVersion("vite", version, range);
529
+ if (tooOld) return tooOld;
506
530
  const viteConfig = findViteConfig(vfs, root);
507
531
  const code = viteConfig ? vfs.readText(path3.join(root, viteConfig)) ?? "" : "";
508
532
  return {
@@ -527,6 +551,8 @@ var ASTRO_ADAPTERS = [
527
551
  ];
528
552
  var detectAstro = (vfs, root, range, deps) => {
529
553
  const version = resolveInstalledVersion(vfs, root, "astro");
554
+ const tooOld = unsupportedVersion("astro", version, range);
555
+ if (tooOld) return tooOld;
530
556
  const config = firstExisting(
531
557
  vfs,
532
558
  root,
@@ -550,6 +576,8 @@ var detectAstro = (vfs, root, range, deps) => {
550
576
  };
551
577
  var detectServer = (id) => (vfs, root, range, deps) => {
552
578
  const version = resolveInstalledVersion(vfs, root, id);
579
+ const tooOld = unsupportedVersion(id, version, range);
580
+ if (tooOld) return tooOld;
553
581
  return {
554
582
  status: "detected",
555
583
  info: {
@@ -772,17 +800,21 @@ var MUTATING = [
772
800
  var COMMAND_OPTIONS = {
773
801
  init: [...GLOBAL, ...MUTATING, "framework", "name", "ai", "no-ai"],
774
802
  add: [...GLOBAL, ...MUTATING],
803
+ connect: [...GLOBAL, "api-key", "config", "wait"],
775
804
  status: ["json", "help", "version", "cwd", "workspace", "config"],
776
805
  doctor: ["json", "help", "version", "cwd", "workspace", "config", "offline"],
777
806
  docs: ["json", "help", "version"],
807
+ frameworks: ["json", "help", "version"],
778
808
  help: GLOBAL
779
809
  };
780
810
  var COMMANDS = [
781
811
  "init",
782
812
  "add",
813
+ "connect",
783
814
  "status",
784
815
  "doctor",
785
816
  "docs",
817
+ "frameworks",
786
818
  "help"
787
819
  ];
788
820
  var FRAMEWORKS = FRAMEWORK_IDS;
@@ -807,7 +839,7 @@ var parseCliArgs = (argv) => {
807
839
  if (!COMMANDS.includes(first)) {
808
840
  throw new CliError(
809
841
  "PUCK-CLI-UNKNOWN-COMMAND",
810
- `Unknown command "${first}". Run \`puck --help\` to see available commands.`
842
+ `Unknown command "${first}". Run \`${CANONICAL_INVOCATION} --help\` to see available commands.`
811
843
  );
812
844
  }
813
845
  command = first;
@@ -818,7 +850,7 @@ var parseCliArgs = (argv) => {
818
850
  if (!allowed.includes(key)) {
819
851
  throw new CliError(
820
852
  "PUCK-CLI-INVALID-ARGS",
821
- `--${key} can't be used with \`${command ?? "puck"}\`.`
853
+ `--${key} can't be used with \`${command ?? CANONICAL_INVOCATION}\`.`
822
854
  );
823
855
  }
824
856
  }
@@ -1127,11 +1159,19 @@ var COMMANDS2 = [
1127
1159
  name: "add <editor|cloud|ai>",
1128
1160
  description: "Add one capability (ai includes cloud, cloud includes editor)"
1129
1161
  },
1162
+ {
1163
+ name: "connect",
1164
+ description: "Log in to Puck Cloud again and replace PUCK_API_KEY"
1165
+ },
1130
1166
  { name: "status", description: "Show what's installed and configured" },
1131
1167
  {
1132
1168
  name: "doctor",
1133
1169
  description: "Diagnose problems and suggest fixes (read-only)"
1134
1170
  },
1171
+ {
1172
+ name: "frameworks",
1173
+ description: "Show the supported frameworks and versions"
1174
+ },
1135
1175
  {
1136
1176
  name: "docs <command>",
1137
1177
  description: "Read the docs for this version: ls, cat <page>, find <query>, grep <text>"
@@ -1471,7 +1511,8 @@ var templates_default = {
1471
1511
  "*.tsbuildinfo",
1472
1512
  "eslint.config.mjs",
1473
1513
  ".env*"
1474
- ]
1514
+ ],
1515
+ allowBuilds: ["esbuild"]
1475
1516
  };
1476
1517
 
1477
1518
  // src/templates/source.ts
@@ -2517,10 +2558,10 @@ var analyzeRoutes = (code, filename) => {
2517
2558
  const topLevel = new Set(exported.elements.filter(Boolean));
2518
2559
  walk(exported, (node) => {
2519
2560
  if (!isRouteCall(node, routeLocal)) return;
2520
- const path31 = stringArg(node, 0);
2561
+ const path33 = stringArg(node, 0);
2521
2562
  const isTop = topLevel.has(node);
2522
- calls.push({ path: path31, file: stringArg(node, 1), topLevel: isTop });
2523
- if (path31 === "*") {
2563
+ calls.push({ path: path33, file: stringArg(node, 1), topLevel: isTop });
2564
+ if (path33 === "*") {
2524
2565
  if (isTop) splat = node;
2525
2566
  else nestedSplat = true;
2526
2567
  }
@@ -4671,6 +4712,26 @@ var statusTable = (result) => {
4671
4712
  const width = Math.max(...rows.map(([k]) => k.length), 0) + 2;
4672
4713
  return rows.map(([k, v]) => `${k.padEnd(width)}${v}`);
4673
4714
  };
4715
+ var frameworksTable = (frameworks) => {
4716
+ const rows = [
4717
+ ["Framework", "ID", "Versions", "Editor", "Puck Cloud & AI", "Notes"],
4718
+ ...frameworks.map((f) => [
4719
+ f.name,
4720
+ f.id,
4721
+ `${f.minVersion}+`,
4722
+ f.kind === "server" ? "API only" : "\u2713",
4723
+ f.needsServer ? "needs a server*" : "\u2713",
4724
+ f.notes
4725
+ ])
4726
+ ];
4727
+ const widths = rows[0].map(
4728
+ (_, i) => Math.max(...rows.map((row) => row[i].length))
4729
+ );
4730
+ return rows.map((row, r) => {
4731
+ const line = row.map((cell, i) => i === row.length - 1 ? cell : cell.padEnd(widths[i])).join(" ").trimEnd();
4732
+ return r === 0 ? pc.bold(line) : line;
4733
+ });
4734
+ };
4674
4735
  var presentHuman = (out, result, secrets) => {
4675
4736
  const lines = [];
4676
4737
  const write = () => out.write(secrets.scrub(lines.join("\n")) + "\n");
@@ -4704,6 +4765,18 @@ var presentHuman = (out, result, secrets) => {
4704
4765
  lines.push(` ${" ".repeat(24)} ${pc.cyan(f.fix)}`);
4705
4766
  }
4706
4767
  lines.push("", result.message);
4768
+ } else if (result.command === "frameworks" && result.status !== "error") {
4769
+ lines.push(...frameworksTable(result.frameworks ?? []));
4770
+ const needServer = (result.frameworks ?? []).filter((f) => f.needsServer);
4771
+ if (needServer.length) {
4772
+ lines.push(
4773
+ "",
4774
+ `* ${needServer.map((f) => f.name).join(
4775
+ " and "
4776
+ )} need a server for Puck Cloud and Puck AI, unless they already have one. Choose one with --backend.`
4777
+ );
4778
+ }
4779
+ lines.push("", pc.dim(result.message));
4707
4780
  } else if (result.command === "docs" && result.status !== "error") {
4708
4781
  const { page, pages, matches } = result.docs ?? {};
4709
4782
  if (page) lines.push(page.content.trimEnd());
@@ -4765,7 +4838,7 @@ var presentHuman = (out, result, secrets) => {
4765
4838
  ...result.warnings.map((w) => `${pc.yellow("!")} ${w.message}`)
4766
4839
  );
4767
4840
  }
4768
- if (result.status !== "error" && result.command !== "doctor" && result.command !== "docs" && result.nextSteps.length) {
4841
+ if (result.status !== "error" && result.command !== "doctor" && result.command !== "docs" && result.command !== "frameworks" && result.nextSteps.length) {
4769
4842
  lines.push(
4770
4843
  "",
4771
4844
  pc.bold("Next:"),
@@ -4778,7 +4851,7 @@ var presentHuman = (out, result, secrets) => {
4778
4851
  };
4779
4852
 
4780
4853
  // src/commands/init.ts
4781
- import path27 from "path";
4854
+ import path28 from "path";
4782
4855
 
4783
4856
  // src/io/vfs.ts
4784
4857
  import fs2 from "fs";
@@ -5248,7 +5321,7 @@ var resolveTarget = async (rc, opts = {}) => {
5248
5321
  };
5249
5322
 
5250
5323
  // src/commands/mutate.ts
5251
- import path26 from "path";
5324
+ import path27 from "path";
5252
5325
 
5253
5326
  // src/plan/planner.ts
5254
5327
  import path22 from "path";
@@ -5506,6 +5579,66 @@ var planEnvWrite = (p, secret) => {
5506
5579
  planGitignore(p, target);
5507
5580
  };
5508
5581
 
5582
+ // src/plan/pnpm-builds.ts
5583
+ import path24 from "path";
5584
+ import { Document, isMap, isSeq, parseDocument } from "yaml";
5585
+ var LEGACY_LISTS = [
5586
+ "onlyBuiltDependencies",
5587
+ "neverBuiltDependencies",
5588
+ "ignoredBuiltDependencies"
5589
+ ];
5590
+ var allowAll = (names) => Object.fromEntries(names.map((name) => [name, true]));
5591
+ var withAllowedBuilds = (text, names) => {
5592
+ const doc = parseDocument(text ?? "");
5593
+ if (doc.errors.length > 0) return null;
5594
+ if (doc.get("dangerouslyAllowAllBuilds") === true) return null;
5595
+ const allowBuilds = doc.get("allowBuilds");
5596
+ const covered = /* @__PURE__ */ new Set();
5597
+ if (isMap(allowBuilds)) {
5598
+ for (const item of allowBuilds.items) covered.add(String(item.key));
5599
+ }
5600
+ for (const list of LEGACY_LISTS) {
5601
+ const seq = doc.get(list);
5602
+ if (isSeq(seq)) for (const item of seq.toJSON()) covered.add(String(item));
5603
+ }
5604
+ const missing = names.filter((name) => !covered.has(name));
5605
+ if (missing.length === 0) return null;
5606
+ const legacy = doc.get("onlyBuiltDependencies");
5607
+ if (isMap(allowBuilds)) {
5608
+ for (const name of missing) allowBuilds.set(name, true);
5609
+ } else if (isSeq(legacy)) {
5610
+ for (const name of missing) legacy.add(name);
5611
+ } else if (doc.contents === null) {
5612
+ const created = new Document({ allowBuilds: allowAll(missing) });
5613
+ created.commentBefore = " Dependencies allowed to run install scripts (required by pnpm 11+)";
5614
+ return created.toString();
5615
+ } else {
5616
+ doc.set("allowBuilds", allowAll(missing));
5617
+ }
5618
+ return doc.toString();
5619
+ };
5620
+ var planAllowedBuilds = (planner) => {
5621
+ const { ctx, vfs } = planner;
5622
+ if (ctx.packageManager.name !== "pnpm") return;
5623
+ const ws = ctx.workspace;
5624
+ if (ws && ws.source !== "pnpm-workspace.yaml") return;
5625
+ const file = path24.join(ws?.root ?? ctx.root, "pnpm-workspace.yaml");
5626
+ const names = templates_default.allowBuilds;
5627
+ const list = names.join(", ");
5628
+ const next = withAllowedBuilds(vfs.readText(file), names);
5629
+ if (next === null) return;
5630
+ vfs.write(file, next);
5631
+ if (!ws) return;
5632
+ planner.steps.push({
5633
+ id: "bootstrap:allow-builds",
5634
+ kind: "modify_file",
5635
+ capability: "bootstrap",
5636
+ summary: `Allow ${list} build scripts in the workspace's pnpm-workspace.yaml`,
5637
+ path: file,
5638
+ edits: []
5639
+ });
5640
+ };
5641
+
5509
5642
  // src/plan/backend.ts
5510
5643
  var choicesFor = (options, wantsCloud) => [
5511
5644
  { value: "add", label: options.addLabel },
@@ -5575,14 +5708,14 @@ var resolveBackend = async (rc, options, capabilities, planned) => {
5575
5708
 
5576
5709
  // src/auth/session-store.ts
5577
5710
  import fs4 from "fs";
5578
- import path24 from "path";
5711
+ import path25 from "path";
5579
5712
  import { createHash as createHash2 } from "crypto";
5580
5713
  var SessionStore = class {
5581
5714
  #file;
5582
5715
  constructor(cacheDir, projectRoot, baseUrl) {
5583
5716
  const key = createHash2("sha256").update(`${projectRoot}
5584
5717
  ${baseUrl}`).digest("hex").slice(0, 32);
5585
- this.#file = path24.join(cacheDir, "puck", "connect", `${key}.json`);
5718
+ this.#file = path25.join(cacheDir, "puck", "connect", `${key}.json`);
5586
5719
  }
5587
5720
  get path() {
5588
5721
  return this.#file;
@@ -5596,14 +5729,14 @@ ${baseUrl}`).digest("hex").slice(0, 32);
5596
5729
  }
5597
5730
  }
5598
5731
  save(session) {
5599
- fs4.mkdirSync(path24.dirname(this.#file), { recursive: true, mode: 448 });
5732
+ fs4.mkdirSync(path25.dirname(this.#file), { recursive: true, mode: 448 });
5600
5733
  fs4.writeFileSync(this.#file, JSON.stringify(session), { mode: 384 });
5601
5734
  }
5602
5735
  clear() {
5603
5736
  fs4.rmSync(this.#file, { force: true });
5604
5737
  }
5605
5738
  };
5606
- var cacheDirFor = (env2, homedir) => env2.XDG_CACHE_HOME || path24.join(homedir, ".cache");
5739
+ var cacheDirFor = (env2, homedir) => env2.XDG_CACHE_HOME || path25.join(homedir, ".cache");
5607
5740
 
5608
5741
  // src/auth/credentials.ts
5609
5742
  var apiKeysUrl = (rc) => new URL(API_KEYS_PATH, rc.cloud.baseUrl).toString();
@@ -5826,7 +5959,7 @@ var promptForKey = async (rc) => {
5826
5959
  warnings: []
5827
5960
  };
5828
5961
  };
5829
- var resolveCredential = async (rc, state, info) => {
5962
+ var resolveCredential = async (rc, state, info, { fresh = false } = {}) => {
5830
5963
  const warnings = [];
5831
5964
  const fromExplicit = async (value, source) => {
5832
5965
  const secret = secretFrom(
@@ -5852,10 +5985,15 @@ var resolveCredential = async (rc, state, info) => {
5852
5985
  };
5853
5986
  if (rc.flags.apiKey !== void 0)
5854
5987
  return fromExplicit(rc.flags.apiKey, "flag");
5855
- if (rc.deps.env[ENV_KEY])
5988
+ if (fresh && rc.deps.env[ENV_KEY]) {
5989
+ warnings.push({
5990
+ code: "PUCK-CLI-W-KEY-IN-PROCESS-ENV",
5991
+ message: `${ENV_KEY} is set in your environment and overrides the new key in the env file. Unset it to use the new key.`
5992
+ });
5993
+ } else if (rc.deps.env[ENV_KEY])
5856
5994
  return fromExplicit(rc.deps.env[ENV_KEY], "process.env");
5857
5995
  const existing = state.cloud.apiKey;
5858
- if (existing.present && existing.value) {
5996
+ if (!fresh && existing.present && existing.value) {
5859
5997
  const secret = secretFrom(
5860
5998
  rc,
5861
5999
  existing.value,
@@ -5916,7 +6054,7 @@ var resolveCredential = async (rc, state, info) => {
5916
6054
 
5917
6055
  // src/apply/applier.ts
5918
6056
  import fs5 from "fs";
5919
- import path25 from "path";
6057
+ import path26 from "path";
5920
6058
  var tail = (text, lines = 20) => text.trim().split("\n").slice(-lines).join("\n");
5921
6059
  var run = async (rc, spec) => {
5922
6060
  rc.log(`$ ${formatCommand(spec)}`);
@@ -5946,14 +6084,14 @@ ${result.stderr}`))
5946
6084
  }
5947
6085
  };
5948
6086
  var writeFile = (file, content, mode) => {
5949
- fs5.mkdirSync(path25.dirname(file), { recursive: true });
6087
+ fs5.mkdirSync(path26.dirname(file), { recursive: true });
5950
6088
  if (mode === void 0) {
5951
6089
  fs5.writeFileSync(file, content);
5952
6090
  return;
5953
6091
  }
5954
- const temp = path25.join(
5955
- path25.dirname(file),
5956
- `.${path25.basename(file)}.${process.pid}.tmp`
6092
+ const temp = path26.join(
6093
+ path26.dirname(file),
6094
+ `.${path26.basename(file)}.${process.pid}.tmp`
5957
6095
  );
5958
6096
  fs5.writeFileSync(temp, content, { mode });
5959
6097
  fs5.chmodSync(temp, mode);
@@ -6154,6 +6292,7 @@ var runMutation = async (rc, input) => {
6154
6292
  }
6155
6293
  planCapabilities(planner, capabilities);
6156
6294
  planner.finalize();
6295
+ if (bootstrap) planAllowedBuilds(planner);
6157
6296
  if (scaffoldStep) planner.steps.unshift(scaffoldStep);
6158
6297
  const wantsCloud = capabilities.includes("cloud") && planner.backend?.mode !== "external" && !state.cloud.external;
6159
6298
  const needsConsent = planner.steps.length > 0 || wantsCloud && !state.cloud.apiKey.present;
@@ -6192,7 +6331,7 @@ var runMutation = async (rc, input) => {
6192
6331
  if (wantsCloud) {
6193
6332
  credential = await resolveCredential(rc, state, {
6194
6333
  projectRoot: root,
6195
- projectName: bootstrap?.appName ?? ctx.packageJson?.name ?? path26.basename(root),
6334
+ projectName: bootstrap?.appName ?? ctx.packageJson?.name ?? path27.basename(root),
6196
6335
  framework: ctx.framework.id
6197
6336
  });
6198
6337
  for (const w of credential.warnings) planner.warn(w.code, w.message);
@@ -6383,14 +6522,14 @@ var runInit = async (rc) => {
6383
6522
  { globs: target.workspace.globs }
6384
6523
  );
6385
6524
  }
6386
- parentDir = path27.join(target.workspace.root, appsDir);
6525
+ parentDir = path28.join(target.workspace.root, appsDir);
6387
6526
  workspaceRoot = target.workspace.root;
6388
6527
  for (const pkg of target.workspace.packages) {
6389
6528
  if (pkg.name) existingNames.add(pkg.name);
6390
- existingNames.add(path27.basename(pkg.dir));
6529
+ existingNames.add(path28.basename(pkg.dir));
6391
6530
  }
6392
6531
  } else {
6393
- if (vfs.exists(path27.join(target.root, "package.json"))) {
6532
+ if (vfs.exists(path28.join(target.root, "package.json"))) {
6394
6533
  const read = readPackageJson(vfs, target.root);
6395
6534
  const deps = depsOf(read.status === "ok" ? read.pkg : null);
6396
6535
  const detection = detectFramework(vfs, target.root, deps);
@@ -6480,7 +6619,7 @@ var runInit = async (rc) => {
6480
6619
  `A workspace package named "${name}" already exists.`
6481
6620
  );
6482
6621
  }
6483
- const dir = inPlace ? parentDir : path27.join(parentDir, name);
6622
+ const dir = inPlace ? parentDir : path28.join(parentDir, name);
6484
6623
  if (!inPlace && vfs.exists(dir) && !isEmptyDir(vfs, dir)) {
6485
6624
  throw new CliError(
6486
6625
  "PUCK-CLI-TARGET-DIR-NOT-EMPTY",
@@ -6499,7 +6638,7 @@ var runInit = async (rc) => {
6499
6638
  bootstrap: {
6500
6639
  framework,
6501
6640
  dir,
6502
- appName: name ?? sanitizeAppName(path27.basename(dir)),
6641
+ appName: name ?? sanitizeAppName(path28.basename(dir)),
6503
6642
  workspaceRoot
6504
6643
  },
6505
6644
  workspace: target.kind === "workspace-empty" ? target.summary : null
@@ -6513,7 +6652,7 @@ var runAdd = async (rc, positionals) => {
6513
6652
  "PUCK-CLI-INVALID-ARGS",
6514
6653
  `Specify what to add: ${CAPABILITY_IDS.join(
6515
6654
  ", "
6516
- )}. For example \`puck add cloud\`.`
6655
+ )}. For example \`${CANONICAL_INVOCATION} add cloud\`.`
6517
6656
  );
6518
6657
  }
6519
6658
  const unknown = positionals.filter(
@@ -6551,6 +6690,144 @@ var runAdd = async (rc, positionals) => {
6551
6690
  });
6552
6691
  };
6553
6692
 
6693
+ // src/commands/connect.ts
6694
+ import path29 from "path";
6695
+ var runConnect = async (rc) => {
6696
+ const { deps, flags } = rc;
6697
+ const result = emptyResult("connect", flags.dryRun);
6698
+ const target = await resolveTarget(rc);
6699
+ if (target.kind === "action") {
6700
+ result.status = "action_required";
6701
+ result.message = target.action.message;
6702
+ result.actions = [target.action];
6703
+ return result;
6704
+ }
6705
+ if (target.kind === "workspace-empty") {
6706
+ throw new CliError(
6707
+ "PUCK-CLI-UNSUPPORTED-FRAMEWORK",
6708
+ `No supported app was found in this workspace. Run \`${CANONICAL_INVOCATION} init\` to create one.`,
6709
+ { fix: `${CANONICAL_INVOCATION} init --name <name>` }
6710
+ );
6711
+ }
6712
+ const base = baseDir(rc);
6713
+ const root = target.root;
6714
+ const vfs = new Vfs();
6715
+ const ctx = detectProject({
6716
+ vfs,
6717
+ root,
6718
+ cwd: base,
6719
+ packageManagerFlag: flags.packageManager,
6720
+ env: deps.env
6721
+ });
6722
+ assertSupported(rc, ctx);
6723
+ const state = detectState(vfs, ctx, deps.env, { configFlag: flags.config });
6724
+ if (state.cloud.external) {
6725
+ throw new CliError(
6726
+ "PUCK-CLI-EXTERNAL-BACKEND",
6727
+ "This app sends Puck Cloud requests to a server elsewhere, and the key lives there. Run connect against that server instead.",
6728
+ { fix: `${CANONICAL_INVOCATION} connect --cwd <server dir>` }
6729
+ );
6730
+ }
6731
+ const planner = new Planner(vfs, ctx, state, deps.templates, deps.cliVersion);
6732
+ for (const warning of ctx.warnings)
6733
+ planner.warn("PUCK-CLI-W-DETECTION", warning);
6734
+ const cloudReady = state.cloud.clientInstalled && Boolean(state.cloud.routeFile) && state.cloud.routeRegistered !== false;
6735
+ const where = root === base ? "" : ` --cwd ${displayPath(base, root)}`;
6736
+ if (!cloudReady) {
6737
+ planner.warn(
6738
+ "PUCK-CLI-W-CLOUD-NOT-SET-UP",
6739
+ `Puck Cloud isn't set up in this app yet, so nothing uses ${ENV_KEY}. Run \`${CANONICAL_INVOCATION} add cloud\` to set it up.`
6740
+ );
6741
+ }
6742
+ const finish = (status, message) => {
6743
+ result.status = status;
6744
+ result.message = message;
6745
+ result.project = projectSummary(ctx, target.workspace);
6746
+ result.cloud = cloudSummary(state, "none");
6747
+ result.warnings = planner.warnings;
6748
+ return result;
6749
+ };
6750
+ if (!flags.dryRun && !rc.interactive && !flags.yes) {
6751
+ result.actions = [
6752
+ {
6753
+ id: "confirm",
6754
+ type: "confirm_plan",
6755
+ required: true,
6756
+ flag: "--yes",
6757
+ message: `Re-run with --yes to log in to Puck Cloud and replace ${ENV_KEY}.`,
6758
+ rerun: `${rerunCommand(rc)} --yes`
6759
+ }
6760
+ ];
6761
+ return finish(
6762
+ "action_required",
6763
+ `Re-run with --yes to log in to Puck Cloud and replace ${ENV_KEY}.`
6764
+ );
6765
+ }
6766
+ const credential = await resolveCredential(
6767
+ rc,
6768
+ state,
6769
+ {
6770
+ projectRoot: root,
6771
+ projectName: ctx.packageJson?.name ?? path29.basename(root),
6772
+ framework: ctx.framework.id
6773
+ },
6774
+ { fresh: true }
6775
+ );
6776
+ for (const w of credential.warnings) planner.warn(w.code, w.message);
6777
+ if (credential.kind === "action") {
6778
+ result.actions = [credential.action];
6779
+ return finish(
6780
+ "action_required",
6781
+ credential.action.type === "browser_login" ? "Puck Cloud login required. No changes have been made yet." : "A Puck API key is required. No changes have been made yet."
6782
+ );
6783
+ }
6784
+ if (credential.kind === "none") {
6785
+ planner.warn(
6786
+ "PUCK-CLI-W-KEY-NEEDED",
6787
+ "You'll be asked to log in to Puck Cloud when you run this without --dry-run."
6788
+ );
6789
+ return finish("success", "Dry run: no changes were made.");
6790
+ }
6791
+ planEnvWrite(planner, credential.secret);
6792
+ result.plan = { steps: serializeSteps(base, planner.steps) };
6793
+ if (flags.dryRun) return finish("success", "Dry run: no changes were made.");
6794
+ const outcome = await applyPlan(rc, vfs, planner.steps);
6795
+ const after = new Vfs();
6796
+ const verifiedCtx = detectProject({
6797
+ vfs: after,
6798
+ root,
6799
+ cwd: base,
6800
+ packageManagerFlag: flags.packageManager,
6801
+ env: deps.env
6802
+ });
6803
+ const verifiedState = detectState(after, verifiedCtx, deps.env, {
6804
+ configFlag: flags.config
6805
+ });
6806
+ const check = credential.verified;
6807
+ finish(
6808
+ "success",
6809
+ `Connected to Puck Cloud. ${planner.steps.length ? `Saved ${ENV_KEY} to ${displayPath(
6810
+ base,
6811
+ verifiedState.cloud.apiKey.file ?? root
6812
+ )}.` : `${ENV_KEY} was already set to this key.`}`
6813
+ );
6814
+ result.project = projectSummary(verifiedCtx, target.workspace);
6815
+ result.cloud = cloudSummary(
6816
+ verifiedState,
6817
+ check === "valid" ? "remote" : check === "invalid" ? "failed" : "local"
6818
+ );
6819
+ result.changed = planner.steps.length > 0;
6820
+ result.filesCreated = outcome.created.map((f) => displayPath(base, f)).sort();
6821
+ result.filesModified = [...outcome.created, ...outcome.modified].map((f) => displayPath(base, f)).sort();
6822
+ const pm = verifiedCtx.packageManager.name;
6823
+ const cdPrefix = root === base ? "" : `cd ${displayPath(base, root)} && `;
6824
+ result.nextSteps = cloudReady ? [
6825
+ "# Restart the dev server so it picks up the new key",
6826
+ `${cdPrefix}${pm} ${pm === "npm" ? "run " : ""}dev`
6827
+ ] : [`${CANONICAL_INVOCATION} add cloud${where}`];
6828
+ return result;
6829
+ };
6830
+
6554
6831
  // src/commands/status.ts
6555
6832
  var runStatus = async (rc) => {
6556
6833
  const base = baseDir(rc);
@@ -6624,7 +6901,7 @@ var runStatus = async (rc) => {
6624
6901
  };
6625
6902
 
6626
6903
  // src/commands/doctor.ts
6627
- import path28 from "path";
6904
+ import path30 from "path";
6628
6905
  var finding = (check, status, evidence, fix = null) => ({ check, status, evidence, fix });
6629
6906
  var runDoctor = async (rc) => {
6630
6907
  const base = baseDir(rc);
@@ -6709,7 +6986,7 @@ var runDoctor = async (rc) => {
6709
6986
  finding(
6710
6987
  "project.package_json",
6711
6988
  "ok",
6712
- displayPath(base, path28.join(ctx.root, "package.json"))
6989
+ displayPath(base, path30.join(ctx.root, "package.json"))
6713
6990
  )
6714
6991
  );
6715
6992
  result.project = projectSummary(ctx, target.workspace);
@@ -6951,7 +7228,7 @@ var runDoctor = async (rc) => {
6951
7228
  "cloud.connection",
6952
7229
  "fail",
6953
7230
  "Puck Cloud rejected the API key",
6954
- `${CANONICAL_INVOCATION} add cloud${where}`
7231
+ `${CANONICAL_INVOCATION} connect${where}`
6955
7232
  )
6956
7233
  );
6957
7234
  } else {
@@ -7090,6 +7367,41 @@ var runDocs = async (rc, positionals) => {
7090
7367
  return result;
7091
7368
  };
7092
7369
 
7370
+ // src/commands/frameworks.ts
7371
+ var NOTES = {
7372
+ next: "App Router, in app/ or src/app/",
7373
+ "react-router": "Framework mode",
7374
+ "tanstack-start": "",
7375
+ vinext: "App Router, in app/ or src/app/",
7376
+ vite: "React apps using @vitejs/plugin-react",
7377
+ astro: "Adds React with astro add react if needed",
7378
+ hono: "Serves the APIs for an editor in another app",
7379
+ express: "Serves the APIs for an editor in another app"
7380
+ };
7381
+ var frameworkSupport = () => FRAMEWORK_IDS.map((id) => {
7382
+ const adapter = ADAPTERS[id];
7383
+ return {
7384
+ id,
7385
+ name: FRAMEWORK_LABELS[id],
7386
+ minVersion: MIN_VERSIONS[id],
7387
+ kind: adapter.kind ?? "app",
7388
+ needsServer: Boolean(adapter.backend),
7389
+ notes: NOTES[id]
7390
+ };
7391
+ });
7392
+ var runFrameworks = async (_rc, positionals) => {
7393
+ if (positionals.length > 0) {
7394
+ throw new CliError(
7395
+ "PUCK-CLI-INVALID-ARGS",
7396
+ `\`frameworks\` doesn't take arguments. Run \`${CANONICAL_INVOCATION} frameworks\`.`
7397
+ );
7398
+ }
7399
+ const result = emptyResult("frameworks");
7400
+ result.frameworks = frameworkSupport();
7401
+ result.message = `Create a new app with \`${CANONICAL_INVOCATION} init --framework <id>\`.`;
7402
+ return result;
7403
+ };
7404
+
7093
7405
  // src/cli.ts
7094
7406
  var exitCodeFor = (result) => {
7095
7407
  if (result.status === "error")
@@ -7143,8 +7455,11 @@ var runCli = async (argv, deps) => {
7143
7455
  try {
7144
7456
  if (command === "init") result = await runInit(rc);
7145
7457
  else if (command === "add") result = await runAdd(rc, positionals);
7458
+ else if (command === "connect") result = await runConnect(rc);
7146
7459
  else if (command === "status") result = await runStatus(rc);
7147
7460
  else if (command === "docs") result = await runDocs(rc, positionals);
7461
+ else if (command === "frameworks")
7462
+ result = await runFrameworks(rc, positionals);
7148
7463
  else result = await runDoctor(rc);
7149
7464
  } catch (err) {
7150
7465
  result = errorResult(command, err, flags.dryRun);
@@ -7248,7 +7563,7 @@ var runDevServer = (spec, onReady) => new Promise((resolve) => {
7248
7563
 
7249
7564
  // src/docs/source.ts
7250
7565
  import fs6 from "fs";
7251
- import path29 from "path";
7566
+ import path31 from "path";
7252
7567
  var DirDocsSource = class {
7253
7568
  #dir;
7254
7569
  constructor(dir) {
@@ -7256,21 +7571,21 @@ var DirDocsSource = class {
7256
7571
  }
7257
7572
  list() {
7258
7573
  return JSON.parse(
7259
- fs6.readFileSync(path29.join(this.#dir, "index.json"), "utf8")
7574
+ fs6.readFileSync(path31.join(this.#dir, "index.json"), "utf8")
7260
7575
  );
7261
7576
  }
7262
7577
  read(page) {
7263
- return fs6.readFileSync(path29.join(this.#dir, `${page}.md`), "utf8");
7578
+ return fs6.readFileSync(path31.join(this.#dir, `${page}.md`), "utf8");
7264
7579
  }
7265
7580
  };
7266
7581
 
7267
7582
  // src/bin.ts
7268
- var distDir = path30.dirname(fileURLToPath(import.meta.url));
7269
- var templatesDir = path30.join(distDir, "templates");
7583
+ var distDir = path32.dirname(fileURLToPath(import.meta.url));
7584
+ var templatesDir = path32.join(distDir, "templates");
7270
7585
  var templates = new DirTemplateSource(templatesDir, {
7271
7586
  gitignoreName: "_gitignore",
7272
7587
  manifest: () => JSON.parse(
7273
- fs7.readFileSync(path30.join(templatesDir, "manifest.json"), "utf8")
7588
+ fs7.readFileSync(path32.join(templatesDir, "manifest.json"), "utf8")
7274
7589
  )
7275
7590
  });
7276
7591
  var env = { ...process.env };
@@ -7280,14 +7595,14 @@ runCli(process.argv.slice(2), {
7280
7595
  stdout: process.stdout,
7281
7596
  stderr: process.stderr,
7282
7597
  stdinIsTTY: Boolean(process.stdin.isTTY),
7283
- cliVersion: "0.24.0-canary.301a0304",
7598
+ cliVersion: "0.24.0-canary.6f40e09b",
7284
7599
  runner: createRunner(env),
7285
7600
  fetch: globalThis.fetch,
7286
7601
  openUrl,
7287
7602
  runDevServer,
7288
7603
  createPrompter: () => createInquirerPrompter(process.stderr),
7289
7604
  templates,
7290
- docs: new DirDocsSource(path30.join(distDir, "docs")),
7605
+ docs: new DirDocsSource(path32.join(distDir, "docs")),
7291
7606
  homedir: os.homedir(),
7292
7607
  hostname: os.hostname(),
7293
7608
  now: () => Date.now(),
package/dist/docs/cli.md CHANGED
@@ -21,14 +21,16 @@ Puck AI is optional. It's the editor plugin that uses Puck Cloud as its backend,
21
21
 
22
22
  - Next.js 15+ using the App Router (`app/` or `src/app/`)
23
23
  - React Router 7+ in framework mode
24
- - [TanStack Start](https://tanstack.com/start) 1.x
25
- - [vinext](https://github.com/cloudflare/vinext) using the App Router
26
- - [Vite](https://vite.dev) React apps (see [Vite and Astro](#vite-and-astro))
24
+ - [TanStack Start](https://tanstack.com/start) 1.132+
25
+ - [vinext](https://github.com/cloudflare/vinext) 1+ using the App Router
26
+ - [Vite](https://vite.dev) 6+ React apps (see [Vite and Astro](#vite-and-astro))
27
27
  - [Astro](https://astro.build) 7+ (see [Vite and Astro](#vite-and-astro))
28
- - [Hono](https://hono.dev) and [Express](https://expressjs.com) servers (see [Servers](#servers))
28
+ - [Hono](https://hono.dev) 4+ and [Express](https://expressjs.com) 4.16+ servers (see [Servers](#servers))
29
29
  - npm, pnpm, Yarn and Bun, including workspaces
30
30
  - TypeScript projects
31
31
 
32
+ Run `npx @puckeditor/cli frameworks` to see what the CLI sets up for each framework. Older versions fail with `PUCK-CLI-UNSUPPORTED-FRAMEWORK-VERSION`.
33
+
32
34
  When the CLI can't safely change something, like an existing `middleware.ts`, it leaves the file alone and tells you exactly what to add.
33
35
 
34
36
  ## Vite and Astro
@@ -157,8 +159,10 @@ npx skills add puckeditor/skills --skill puck
157
159
  | `add editor` | Add the Puck editor |
158
160
  | `add cloud` | Connect Puck Cloud (adds the editor first if needed) |
159
161
  | `add ai` | Add the Puck AI plugin (adds Puck Cloud first if needed) |
162
+ | `connect` | Log in to Puck Cloud again and replace `PUCK_API_KEY` |
160
163
  | `status` | Show what's installed and configured |
161
164
  | `doctor` | Diagnose problems and suggest fixes. Never makes changes |
165
+ | `frameworks` | Show the supported frameworks and versions |
162
166
  | `docs` | Read these docs: `ls`, `cat <page>`, `find <query>`, `grep <text>` |
163
167
 
164
168
  ## Flags
@@ -192,6 +196,12 @@ To use an existing key, prefer the environment over `--api-key`, which is visibl
192
196
  PUCK_API_KEY=your-key npx @puckeditor/cli add cloud --yes
193
197
  ```
194
198
 
199
+ To log in again, for example to replace a revoked key, run `connect`. It always starts a new login and replaces the existing key, without changing any code:
200
+
201
+ ```sh
202
+ npx @puckeditor/cli connect
203
+ ```
204
+
195
205
  Remember to set `PUCK_API_KEY` in your production environment too. `react-router-serve` and Cloudflare Workers don't load `.env` files; on Workers, use `wrangler secret put PUCK_API_KEY`.
196
206
 
197
207
  The `/api/puck` route and the editor are public by default. Add authentication before deploying.
@@ -27,13 +27,18 @@ puckPages.get("/api/pages", async (req, res) => {
27
27
  puckPages.post(
28
28
  "/api/pages",
29
29
  express.json({ limit: "10mb" }),
30
- async (req, res) => {
31
- const { path, data } = req.body;
32
- const pages = await readPages();
33
-
34
- pages[path] = data;
35
- await fs.writeFile(databasePath, JSON.stringify(pages));
36
-
37
- res.json({ status: "ok" });
30
+ async (req, res, next) => {
31
+ // Express 4 doesn't pass errors from async handlers to next()
32
+ try {
33
+ const { path, data } = req.body;
34
+ const pages = await readPages();
35
+
36
+ pages[path] = data;
37
+ await fs.writeFile(databasePath, JSON.stringify(pages));
38
+
39
+ res.json({ status: "ok" });
40
+ } catch (error) {
41
+ next(error);
42
+ }
38
43
  }
39
44
  );
@@ -27,13 +27,18 @@ puckPages.get("/api/pages", async (req, res) => {
27
27
  puckPages.post(
28
28
  "/api/pages",
29
29
  express.json({ limit: "10mb" }),
30
- async (req, res) => {
31
- const { path, data } = req.body;
32
- const pages = await readPages();
33
-
34
- pages[path] = data;
35
- await fs.writeFile(databasePath, JSON.stringify(pages));
36
-
37
- res.json({ status: "ok" });
30
+ async (req, res, next) => {
31
+ // Express 4 doesn't pass errors from async handlers to next()
32
+ try {
33
+ const { path, data } = req.body;
34
+ const pages = await readPages();
35
+
36
+ pages[path] = data;
37
+ await fs.writeFile(databasePath, JSON.stringify(pages));
38
+
39
+ res.json({ status: "ok" });
40
+ } catch (error) {
41
+ next(error);
42
+ }
38
43
  }
39
44
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@puckeditor/cli",
3
- "version": "0.24.0-canary.301a0304",
3
+ "version": "0.24.0-canary.6f40e09b",
4
4
  "description": "Set up Puck and Puck Cloud from the command line. Built for coding agents, friendly for humans.",
5
5
  "author": "Chris Villa <chris@puckeditor.com>",
6
6
  "repository": {