create-avocado-site 0.11.5 → 0.11.7

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/dist/index.js CHANGED
@@ -4,19 +4,31 @@ import { spawn } from "node:child_process";
4
4
  import { mkdir, readdir } from "node:fs/promises";
5
5
  import { existsSync } from "node:fs";
6
6
  import { resolve } from "node:path";
7
- import { runPrompts, demoConfig } from "./prompts.js";
7
+ import { runPrompts, demoConfig, promptForApiKey } from "./prompts.js";
8
8
  import { collectFiles, collectDemoFiles, generateFiles } from "./generator.js";
9
9
  import { printInstructions } from "./instructions.js";
10
10
  async function main() {
11
11
  const cwd = process.cwd();
12
12
  /*
13
13
  * `npm create avocado-site my-demo` — a directory argument means the demo,
14
- * with no questions. Somebody who has typed a name has already answered the
15
- * only one that mattered, and the fastest path to seeing the product should
16
- * not be gated behind three selects.
14
+ * with no *configuration* questions. Somebody who has typed a name has
15
+ * already answered the only one that mattered, and the fastest path to
16
+ * seeing the product should not be gated behind three selects.
17
+ *
18
+ * One question survives that rule, and it earns its place: the API key.
19
+ *
20
+ * Not to gate anything — everything except chat works without one, and that
21
+ * is deliberate and worth protecting, so Enter skips it. It is here because
22
+ * **keys are read at startup**. A key given now is live the first time the
23
+ * dev server runs; a key added afterwards needs a file edit *and* a restart,
24
+ * and nothing in the product confirms either happened. "I added the key and
25
+ * nothing changed" has no error message anywhere in the system, and this
26
+ * prompt is the cheapest way to stop most people arriving there.
17
27
  */
18
28
  const target = process.argv.slice(2).find((arg) => !arg.startsWith("-"));
19
- const config = target ? await demoConfig(target) : await runPrompts(cwd);
29
+ const config = target
30
+ ? await demoConfig(target, await promptForApiKey())
31
+ : await runPrompts(cwd);
20
32
  if (!config)
21
33
  return;
22
34
  if (config.mode === "demo") {
@@ -90,7 +102,9 @@ async function bootstrapDemo(cwd, dirName, config) {
90
102
  ` default site when the URL does not name one. \`npm run dev\` opens this`,
91
103
  ` URL for you.`,
92
104
  ``,
93
- ` No API key needed to look around; add ANTHROPIC_API_KEY to .env.local`,
105
+ config.apiKey
106
+ ? ` Chat is enabled \u2014 ${config.apiKey.variable} is in .env.local`
107
+ : ` No API key needed to look around; add ANTHROPIC_API_KEY to .env.local and restart`,
94
108
  ` when you want to edit by chat.`,
95
109
  ].join("\n"));
96
110
  p.outro("Done.");
package/dist/prompts.d.ts CHANGED
@@ -1,8 +1,39 @@
1
1
  import type { ScaffoldConfig } from "./types.js";
2
2
  /** A directory name turned into something the orchestrator will accept as a site id. */
3
3
  export declare function toSiteId(name: string): string;
4
+ /**
5
+ * Which provider a pasted key belongs to, by prefix.
6
+ *
7
+ * Exported so `api-key-prompt.test.ts` can check the classification without a
8
+ * terminal. Unknown shapes are not rejected — a key format can change, and
9
+ * refusing a real key because this list is out of date is worse than writing it
10
+ * under the most likely variable and letting the server report the truth.
11
+ */
12
+ export declare function variableForKey(raw: string): string | null;
13
+ /**
14
+ * Offer to take an API key now. One keypress to skip.
15
+ *
16
+ * The product deliberately works without one — the preview, click-to-select,
17
+ * the property panel, publishing and every metadata guarantee need no model,
18
+ * and "no API key needed to look around" is a real differentiator worth
19
+ * protecting. So this must never become a gate: Enter continues, and the copy
20
+ * says so before it says anything else.
21
+ *
22
+ * What it buys is the restart. Keys are read at startup, so a key set *here* is
23
+ * live the first time the dev server runs, while a key added afterwards needs
24
+ * a file edit and a restart — and nothing in the product confirms either
25
+ * happened. "I added the key and nothing changed" has no error message
26
+ * anywhere, and this is the cheapest way to stop most people reaching it.
27
+ */
28
+ export declare function promptForApiKey(): Promise<{
29
+ variable: string;
30
+ value: string;
31
+ } | undefined>;
4
32
  /** Everything a demo scaffold needs, with no questions asked. */
5
- export declare function demoConfig(dirName: string): Promise<ScaffoldConfig>;
33
+ export declare function demoConfig(dirName: string, apiKey?: {
34
+ variable: string;
35
+ value: string;
36
+ }): Promise<ScaffoldConfig>;
6
37
  /**
7
38
  * The mode question, asked first.
8
39
  *
package/dist/prompts.js CHANGED
@@ -8,8 +8,64 @@ export function toSiteId(name) {
8
8
  const cleaned = name.replace(/[^a-zA-Z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
9
9
  return cleaned.length > 0 ? cleaned : "avocado-demo";
10
10
  }
11
+ /**
12
+ * Which provider a pasted key belongs to, by prefix.
13
+ *
14
+ * Exported so `api-key-prompt.test.ts` can check the classification without a
15
+ * terminal. Unknown shapes are not rejected — a key format can change, and
16
+ * refusing a real key because this list is out of date is worse than writing it
17
+ * under the most likely variable and letting the server report the truth.
18
+ */
19
+ export function variableForKey(raw) {
20
+ const key = raw.trim();
21
+ if (!key)
22
+ return null;
23
+ if (key.startsWith("sk-ant-"))
24
+ return "ANTHROPIC_API_KEY";
25
+ if (key.startsWith("sk-"))
26
+ return "OPENAI_API_KEY";
27
+ if (key.startsWith("AIza"))
28
+ return "GOOGLE_GENAI_API_KEY";
29
+ return "ANTHROPIC_API_KEY";
30
+ }
31
+ /**
32
+ * Offer to take an API key now. One keypress to skip.
33
+ *
34
+ * The product deliberately works without one — the preview, click-to-select,
35
+ * the property panel, publishing and every metadata guarantee need no model,
36
+ * and "no API key needed to look around" is a real differentiator worth
37
+ * protecting. So this must never become a gate: Enter continues, and the copy
38
+ * says so before it says anything else.
39
+ *
40
+ * What it buys is the restart. Keys are read at startup, so a key set *here* is
41
+ * live the first time the dev server runs, while a key added afterwards needs
42
+ * a file edit and a restart — and nothing in the product confirms either
43
+ * happened. "I added the key and nothing changed" has no error message
44
+ * anywhere, and this is the cheapest way to stop most people reaching it.
45
+ */
46
+ export async function promptForApiKey() {
47
+ const key = await p.password({
48
+ message: "Paste an API key to enable chat, or press Enter to skip",
49
+ mask: "\u2022",
50
+ validate: () => undefined,
51
+ });
52
+ if (p.isCancel(key))
53
+ return undefined;
54
+ const value = typeof key === "string" ? key.trim() : "";
55
+ if (!value) {
56
+ p.note("Chat will use the built-in demo planner, which handles simple, literal edits.\n" +
57
+ "Everything else works: the preview, click-to-select, the property panel and publishing.\n" +
58
+ "You can add a key to .env.local later and restart.", "No key \u2014 that is fine");
59
+ return undefined;
60
+ }
61
+ const variable = variableForKey(value);
62
+ if (!variable)
63
+ return undefined;
64
+ p.note(`Writing ${variable} to .env.local. No restart needed.`, "Key saved");
65
+ return { variable, value };
66
+ }
11
67
  /** Everything a demo scaffold needs, with no questions asked. */
12
- export async function demoConfig(dirName) {
68
+ export async function demoConfig(dirName, apiKey) {
13
69
  const sitePort = await findFreePort(3000);
14
70
  const editorPort = await findFreePort(4100);
15
71
  return {
@@ -21,6 +77,7 @@ export async function demoConfig(dirName) {
21
77
  editorPort,
22
78
  draftSecret: randomBytes(24).toString("hex"),
23
79
  publishToken: randomBytes(24).toString("hex"),
80
+ apiKey,
24
81
  };
25
82
  }
26
83
  /**
@@ -101,6 +158,7 @@ async function integratePrompts(cwd) {
101
158
  p.cancel("Cancelled.");
102
159
  return null;
103
160
  }
161
+ const apiKey = await promptForApiKey();
104
162
  return {
105
163
  mode: "integrate",
106
164
  cms: cms,
@@ -117,5 +175,6 @@ async function integratePrompts(cwd) {
117
175
  // that already looks filled in is a step nobody performs.
118
176
  draftSecret: randomBytes(24).toString("hex"),
119
177
  publishToken: randomBytes(24).toString("hex"),
178
+ apiKey,
120
179
  };
121
180
  }
@@ -155,7 +155,9 @@ function envLocal(config) {
155
155
  # installs it for you and a Gemini plan fails on the
156
156
  # first call without it.
157
157
  #
158
- # ANTHROPIC_API_KEY=
158
+ ${config.apiKey
159
+ ? `${config.apiKey.variable}=${config.apiKey.value}`
160
+ : "# ANTHROPIC_API_KEY="}
159
161
 
160
162
  # The orchestrator runs inside this app, at /api/avocado. There is no third
161
163
  # service to start and nothing to deploy separately.
@@ -270,6 +272,7 @@ function devScript(config) {
270
272
  * at /api/avocado. Two processes, one terminal, no third service.
271
273
  */
272
274
  import { spawn } from "node:child_process"
275
+ import { readFileSync } from "node:fs"
273
276
  import { createConnection } from "node:net"
274
277
 
275
278
  const SITE_PORT = process.env.PORT ?? "${config.sitePort}"
@@ -295,8 +298,34 @@ const OPEN_URL = \`\${EDITOR_URL}/?siteId=\${SITE_ID}&session=dev\`
295
298
 
296
299
  const children = []
297
300
 
301
+ /*
302
+ * Read DRAFT_MODE_SECRET out of .env.local and hand it to the children.
303
+ *
304
+ * Next loads .env.local by itself; an npm script does not, so the editor CLI
305
+ * would otherwise never see it. Without it the editor cannot select or edit
306
+ * anything once the site is built for production — the SDK requires \`secret=\`
307
+ * on the preview URL there, and with none it renders an ordinary public page
308
+ * with no block markers at all.
309
+ */
310
+ function envFromLocalFile() {
311
+ try {
312
+ const text = readFileSync(new URL("../.env.local", import.meta.url), "utf8")
313
+ const match = text.match(/^\\s*DRAFT_MODE_SECRET\\s*=\\s*(.+)\\s*$/m)
314
+ const value = match?.[1]?.trim().replace(/^["']|["']$/g, "")
315
+ return value ? { DRAFT_MODE_SECRET: value } : {}
316
+ } catch {
317
+ return {}
318
+ }
319
+ }
320
+
321
+ const EXTRA_ENV = envFromLocalFile()
322
+
298
323
  function run(command, args) {
299
- const child = spawn(command, args, { stdio: "inherit", shell: process.platform === "win32" })
324
+ const child = spawn(command, args, {
325
+ stdio: "inherit",
326
+ shell: process.platform === "win32",
327
+ env: { ...process.env, ...EXTRA_ENV }
328
+ })
300
329
  children.push(child)
301
330
  child.on("exit", (code) => {
302
331
  // If either half dies the other is useless, so take both down rather than
package/dist/types.d.ts CHANGED
@@ -40,6 +40,20 @@ export type ScaffoldConfig = {
40
40
  * `POST /api/editor/publish` took content from anyone who asked.
41
41
  */
42
42
  publishToken: string;
43
+ /**
44
+ * An API key pasted during setup, written straight into `.env.local`.
45
+ *
46
+ * Offered once, skippable in one keypress. The point is not to gate the
47
+ * product — everything except chat works without a key, and that is a real
48
+ * feature — it is that **a key set before the dev server first starts needs
49
+ * no restart**. Keys are read at startup, so one added afterwards requires
50
+ * editing a file *and* restarting, and "I added the key and nothing changed"
51
+ * is a failure mode with no error message anywhere.
52
+ */
53
+ apiKey?: {
54
+ variable: string;
55
+ value: string;
56
+ };
43
57
  };
44
58
  export type GeneratedFile = {
45
59
  path: string;
@@ -17,7 +17,7 @@
17
17
  * `@avocadostudio-ai/orchestrator-core`, already pinned there; naming it again
18
18
  * here is how a project ends up with two.
19
19
  */
20
- export declare const AVOCADO = "0.11.5";
20
+ export declare const AVOCADO = "0.11.7";
21
21
  /**
22
22
  * Next 15.5.15 rather than 16, because that is the version every example app
23
23
  * and the demo site in this repository build and test against. The SDK
package/dist/versions.js CHANGED
@@ -17,7 +17,7 @@
17
17
  * `@avocadostudio-ai/orchestrator-core`, already pinned there; naming it again
18
18
  * here is how a project ends up with two.
19
19
  */
20
- export const AVOCADO = "0.11.5";
20
+ export const AVOCADO = "0.11.7";
21
21
  /**
22
22
  * Next 15.5.15 rather than 16, because that is the version every example app
23
23
  * and the demo site in this repository build and test against. The SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-avocado-site",
3
- "version": "0.11.5",
3
+ "version": "0.11.7",
4
4
  "description": "Bootstrap a runnable Avocado Studio demo site, or wire Avocado into an existing Next.js project",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,8 +34,8 @@
34
34
  "@types/node": "^22.13.10",
35
35
  "tsx": "^4.19.0",
36
36
  "typescript": "^5.7.3",
37
- "@avocadostudio-ai/site-sdk": "0.11.5",
38
- "@avocadostudio-ai/shared": "0.11.5"
37
+ "@avocadostudio-ai/site-sdk": "0.11.7",
38
+ "@avocadostudio-ai/shared": "0.11.7"
39
39
  },
40
40
  "license": "Apache-2.0",
41
41
  "homepage": "https://docs.avocadostudio.dev",