create-avocado-site 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -55,6 +55,30 @@ For an existing site of any size this is the smaller half of the job. The
55
55
  larger half — your components, your content model, your CMS — is at
56
56
  [docs.avocadostudio.dev/sites](https://docs.avocadostudio.dev/sites).
57
57
 
58
+ ## Unattended runs
59
+
60
+ The demo path takes a directory name and needs no terminal, so it can be
61
+ scripted, run in CI, or baked into an image:
62
+
63
+ ```bash
64
+ npm create avocado-site@latest my-demo -- --yes --no-install
65
+ ```
66
+
67
+ | Option | |
68
+ |---|---|
69
+ | `-y`, `--yes` | Never ask. Implied whenever stdin is not a terminal. |
70
+ | `--no-install` | Write the files and stop, for callers that install their own way. |
71
+ | `--api-key KEY` | Write `KEY` to `.env.local`; the variable is chosen from its prefix. Also read from `AVOCADO_SETUP_API_KEY`. |
72
+ | `-h`, `--help` | Print the usage. |
73
+
74
+ A key is written only when you name it. A provider variable that happens to be
75
+ exported in your shell is left alone — it is usually your own credential, and a
76
+ scaffold is about to become a git repository.
77
+
78
+ Unattended runs never start the dev server; they print the command instead. A
79
+ run with no terminal and no directory name exits 1 and says why, because the
80
+ guided path has questions that nothing can answer for you.
81
+
58
82
  ## Requirements
59
83
 
60
84
  - Node.js 22+
package/dist/args.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * What the command was asked to do, decided before anything can block.
3
+ *
4
+ * This file exists because the scaffolder could not be run by anything that is
5
+ * not a person at a keyboard. The demo path asks for an API key and then, at
6
+ * the end, whether to start the dev server — both unconditional, both
7
+ * `@clack/prompts`. With stdin closed the first one cancels and the process
8
+ * exits 1 having created nothing; under a pty with a newline piped in it
9
+ * ignores the keystroke and waits forever. So no CI job, no Dockerfile, no
10
+ * agent and no gate could run `npm create avocado-site`, which is the one
11
+ * command every new user runs first. `scaffold-serve-check.mjs` generates from
12
+ * the scaffolder's *templates* rather than by running it, so nothing in the
13
+ * repo noticed.
14
+ *
15
+ * Parsing is separated from `main()` for the reason the prompts had to be: a
16
+ * pure function over `argv` and `env` can be tested without a terminal.
17
+ */
18
+ export type ParsedArgs = {
19
+ /** The directory to create. Its absence is what selects the guided path. */
20
+ target?: string;
21
+ /** No question may block: `--yes`, or stdin is not a terminal. */
22
+ nonInteractive: boolean;
23
+ /** `--no-install` writes the files and stops, for gates and images. */
24
+ install: boolean;
25
+ /** Only ever from an explicit flag or `AVOCADO_SETUP_API_KEY` — see below. */
26
+ apiKey?: {
27
+ variable: string;
28
+ value: string;
29
+ };
30
+ help: boolean;
31
+ unknown: string[];
32
+ };
33
+ export declare const USAGE = "Usage\n npm create avocado-site [directory] [options]\n\n With a directory name, bootstraps the demo into it. With none, asks what to\n build \u2014 which needs a terminal.\n\nOptions\n -y, --yes Never ask. Implied when stdin is not a terminal.\n --no-install Write the files, skip `npm install`.\n --api-key KEY Write KEY to .env.local. The variable is chosen from the\n key's prefix. Also read from AVOCADO_SETUP_API_KEY.\n -h, --help Show this.\n\nNon-interactive runs never start the dev server; the command to start it is\nprinted instead.";
34
+ export declare function parseArgs(argv: string[], env?: Record<string, string | undefined>, isTty?: boolean): ParsedArgs;
35
+ /**
36
+ * Why a run with no terminal and no directory name cannot continue.
37
+ *
38
+ * The guided path is four questions; there is no answer to guess at. What this
39
+ * must not do is what it used to: exit 1 with an empty directory listing and
40
+ * nothing on stderr, which reads as a crash rather than as a choice.
41
+ */
42
+ export declare const NEEDS_A_NAME = "create-avocado-site: no terminal, and no directory name.\n\nThe guided setup asks what to build, which needs a terminal. To run unattended,\nname the directory to create:\n\n npm create avocado-site my-site -- --yes\n\nUsage\n npm create avocado-site [directory] [options]\n\n With a directory name, bootstraps the demo into it. With none, asks what to\n build \u2014 which needs a terminal.\n\nOptions\n -y, --yes Never ask. Implied when stdin is not a terminal.\n --no-install Write the files, skip `npm install`.\n --api-key KEY Write KEY to .env.local. The variable is chosen from the\n key's prefix. Also read from AVOCADO_SETUP_API_KEY.\n -h, --help Show this.\n\nNon-interactive runs never start the dev server; the command to start it is\nprinted instead.";
package/dist/args.js ADDED
@@ -0,0 +1,97 @@
1
+ import { variableForKey } from "./prompts.js";
2
+ export const USAGE = `Usage
3
+ npm create avocado-site [directory] [options]
4
+
5
+ With a directory name, bootstraps the demo into it. With none, asks what to
6
+ build — which needs a terminal.
7
+
8
+ Options
9
+ -y, --yes Never ask. Implied when stdin is not a terminal.
10
+ --no-install Write the files, skip \`npm install\`.
11
+ --api-key KEY Write KEY to .env.local. The variable is chosen from the
12
+ key's prefix. Also read from AVOCADO_SETUP_API_KEY.
13
+ -h, --help Show this.
14
+
15
+ Non-interactive runs never start the dev server; the command to start it is
16
+ printed instead.`;
17
+ /**
18
+ * The key is taken only when it is named, never inherited from the shell.
19
+ *
20
+ * `--api-key` and `AVOCADO_SETUP_API_KEY` both say "put this in the project I
21
+ * am creating". A bare `ANTHROPIC_API_KEY` in the environment says nothing of
22
+ * the sort — it is usually the developer's own key, exported in a shell
23
+ * profile, and harvesting it would write a live credential into a new
24
+ * `.env.local` in a directory that is about to become a git repository. The
25
+ * interactive path has always required the key to be typed at it; the
26
+ * unattended path requires it to be named. Neither reads what happens to be
27
+ * lying around.
28
+ */
29
+ const API_KEY_ENV_VAR = "AVOCADO_SETUP_API_KEY";
30
+ export function parseArgs(argv, env = process.env, isTty = Boolean(process.stdin.isTTY)) {
31
+ let target;
32
+ let yes = false;
33
+ let install = true;
34
+ let help = false;
35
+ let rawKey;
36
+ const unknown = [];
37
+ for (let i = 0; i < argv.length; i++) {
38
+ const arg = argv[i];
39
+ /*
40
+ * `--api-key sk-ant-…` takes the next token, which is why the old
41
+ * one-liner could not survive a flag that takes a value: it picked the
42
+ * target with `find(arg => !arg.startsWith("-"))`, so the key itself would
43
+ * have become the directory name — and the scaffold would have been
44
+ * created in a directory named after a secret.
45
+ */
46
+ if (arg === "--api-key") {
47
+ rawKey = argv[++i];
48
+ continue;
49
+ }
50
+ if (arg.startsWith("--api-key=")) {
51
+ rawKey = arg.slice("--api-key=".length);
52
+ continue;
53
+ }
54
+ if (arg === "-y" || arg === "--yes") {
55
+ yes = true;
56
+ continue;
57
+ }
58
+ if (arg === "--no-install") {
59
+ install = false;
60
+ continue;
61
+ }
62
+ if (arg === "-h" || arg === "--help") {
63
+ help = true;
64
+ continue;
65
+ }
66
+ if (arg.startsWith("-")) {
67
+ unknown.push(arg);
68
+ continue;
69
+ }
70
+ target ??= arg;
71
+ }
72
+ const value = (rawKey ?? env[API_KEY_ENV_VAR] ?? "").trim();
73
+ const variable = value ? variableForKey(value) : null;
74
+ return {
75
+ target,
76
+ nonInteractive: yes || !isTty,
77
+ install,
78
+ apiKey: variable && value ? { variable, value } : undefined,
79
+ help,
80
+ unknown,
81
+ };
82
+ }
83
+ /**
84
+ * Why a run with no terminal and no directory name cannot continue.
85
+ *
86
+ * The guided path is four questions; there is no answer to guess at. What this
87
+ * must not do is what it used to: exit 1 with an empty directory listing and
88
+ * nothing on stderr, which reads as a crash rather than as a choice.
89
+ */
90
+ export const NEEDS_A_NAME = `create-avocado-site: no terminal, and no directory name.
91
+
92
+ The guided setup asks what to build, which needs a terminal. To run unattended,
93
+ name the directory to create:
94
+
95
+ npm create avocado-site my-site -- --yes
96
+
97
+ ${USAGE}`;
package/dist/index.js CHANGED
@@ -7,8 +7,33 @@ import { resolve } from "node:path";
7
7
  import { runPrompts, demoConfig, promptForApiKey, AVOCADO_INTRO } from "./prompts.js";
8
8
  import { collectFiles, collectDemoFiles, generateFiles } from "./generator.js";
9
9
  import { printInstructions } from "./instructions.js";
10
+ import { parseArgs, USAGE, NEEDS_A_NAME } from "./args.js";
10
11
  async function main() {
11
12
  const cwd = process.cwd();
13
+ const args = parseArgs(process.argv.slice(2));
14
+ if (args.help) {
15
+ console.log(USAGE);
16
+ return;
17
+ }
18
+ if (args.unknown.length > 0) {
19
+ console.error(`create-avocado-site: unknown option ${args.unknown.join(", ")}\n\n${USAGE}`);
20
+ process.exitCode = 1;
21
+ return;
22
+ }
23
+ /*
24
+ * Say so, rather than cancelling into an empty directory listing.
25
+ *
26
+ * Without a terminal the guided path's first prompt returns a cancel
27
+ * symbol, `main` returns, and the process exits 1 with nothing written and
28
+ * nothing printed. Every unattended caller that ever tried this — a CI job,
29
+ * an image build, an agent — saw a bare exit code and no way to tell a crash
30
+ * from a refusal.
31
+ */
32
+ if (args.nonInteractive && !args.target) {
33
+ console.error(NEEDS_A_NAME);
34
+ process.exitCode = 1;
35
+ return;
36
+ }
12
37
  /*
13
38
  * `npm create avocado-site my-demo` — a directory argument means the demo,
14
39
  * with no *configuration* questions. Somebody who has typed a name has
@@ -24,15 +49,21 @@ async function main() {
24
49
  * and nothing in the product confirms either happened. "I added the key and
25
50
  * nothing changed" has no error message anywhere in the system, and this
26
51
  * prompt is the cheapest way to stop most people arriving there.
52
+ *
53
+ * Unattended, that reasoning inverts: there is nobody to ask, and a question
54
+ * nobody can answer is not a prompt, it is a hang. `--api-key` says the same
55
+ * thing in advance, and skipping stays free — a scaffold with no key is a
56
+ * complete, working project.
27
57
  */
28
- const target = process.argv.slice(2).find((arg) => !arg.startsWith("-"));
58
+ const target = args.target;
29
59
  let config;
30
60
  if (target) {
31
61
  // `runPrompts` opens with this; the fast path had no intro at all, so a
32
62
  // stranger's first interaction with the product was an unexplained request
33
63
  // for a credential on an otherwise blank terminal.
34
64
  p.intro(AVOCADO_INTRO);
35
- config = await demoConfig(target, await promptForApiKey());
65
+ const apiKey = args.apiKey ?? (args.nonInteractive ? undefined : await promptForApiKey());
66
+ config = await demoConfig(target, apiKey);
36
67
  }
37
68
  else {
38
69
  config = await runPrompts(cwd);
@@ -40,7 +71,7 @@ async function main() {
40
71
  if (!config)
41
72
  return;
42
73
  if (config.mode === "demo") {
43
- await bootstrapDemo(cwd, target ?? config.siteId, config);
74
+ await bootstrapDemo(cwd, target ?? config.siteId, config, args);
44
75
  return;
45
76
  }
46
77
  await scaffoldInto(cwd, collectFiles(config), config);
@@ -69,7 +100,7 @@ async function scaffoldInto(dir, files, config) {
69
100
  * ended, and every printed step is a place for somebody evaluating the product
70
101
  * to put it down.
71
102
  */
72
- async function bootstrapDemo(cwd, dirName, config) {
103
+ async function bootstrapDemo(cwd, dirName, config, options) {
73
104
  const dir = resolve(cwd, dirName);
74
105
  if (existsSync(dir) && (await readdir(dir)).length > 0) {
75
106
  p.log.error(`${dirName} already exists and is not empty.`);
@@ -81,21 +112,32 @@ async function bootstrapDemo(cwd, dirName, config) {
81
112
  const files = collectDemoFiles(config);
82
113
  await generateFiles(dir, files);
83
114
  p.log.success(`Created ${dirName} — ${files.length} files`);
84
- const spinner = p.spinner();
85
- spinner.start("Installing dependencies");
86
- const installed = await run("npm", ["install", "--no-audit", "--no-fund"], dir);
87
- if (installed) {
88
- spinner.stop("Installed dependencies");
115
+ /*
116
+ * `--no-install` is for the callers that install differently or not at all —
117
+ * a gate that overlays tarballs packed from a working tree, an image build
118
+ * with its own cache, a test that only cares that the files are correct.
119
+ * Without it, proving the command runs costs a full `npm install`.
120
+ */
121
+ if (options.install) {
122
+ const spinner = p.spinner();
123
+ spinner.start("Installing dependencies");
124
+ const installed = await run("npm", ["install", "--no-audit", "--no-fund"], dir);
125
+ if (installed) {
126
+ spinner.stop("Installed dependencies");
127
+ }
128
+ else {
129
+ /*
130
+ * Not fatal, and worth being explicit about rather than exiting. A failed
131
+ * install is usually a registry or proxy problem on the user's side, and
132
+ * the project on disk is complete and correct — `npm install` in it will
133
+ * work once that is fixed. Deleting it would throw away the good half.
134
+ */
135
+ spinner.stop("Could not install dependencies");
136
+ p.log.warn(`Run \`npm install\` in ${dirName} yourself — everything else is in place.`);
137
+ }
89
138
  }
90
139
  else {
91
- /*
92
- * Not fatal, and worth being explicit about rather than exiting. A failed
93
- * install is usually a registry or proxy problem on the user's side, and
94
- * the project on disk is complete and correct — `npm install` in it will
95
- * work once that is fixed. Deleting it would throw away the good half.
96
- */
97
- spinner.stop("Could not install dependencies");
98
- p.log.warn(`Run \`npm install\` in ${dirName} yourself — everything else is in place.`);
140
+ p.log.warn(`Skipped \`npm install\` — run it in ${dirName} before starting.`);
99
141
  }
100
142
  p.log.message([
101
143
  ` cd ${dirName} && npm run dev`,
@@ -159,10 +201,19 @@ async function bootstrapDemo(cwd, dirName, config) {
159
201
  * script or a container wants the files and nothing else. The default is yes
160
202
  * because the overwhelmingly common case is a person who wants to see it.
161
203
  */
162
- const start = await p.confirm({
163
- message: `Start it now? (runs \`npm run dev\` in ${dirName})`,
164
- initialValue: true,
165
- });
204
+ /*
205
+ * The second thing that could not be answered without a keyboard, and the
206
+ * more dangerous of the two: a `confirm` whose default is *yes* would, if it
207
+ * ever stopped blocking, spawn two long-running dev servers inside a CI job
208
+ * or an image build. Unattended, the answer is always no — the caller asked
209
+ * for a project, not for a process it has no way to stop.
210
+ */
211
+ const start = options.nonInteractive
212
+ ? false
213
+ : await p.confirm({
214
+ message: `Start it now? (runs \`npm run dev\` in ${dirName})`,
215
+ initialValue: true,
216
+ });
166
217
  if (p.isCancel(start) || !start) {
167
218
  p.outro(`Done. Run \`cd ${dirName} && npm run dev\` when you are ready.`);
168
219
  return;
@@ -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.17.0";
20
+ export declare const AVOCADO = "0.18.0";
21
21
  /**
22
22
  * Next 15.5.25 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.17.0";
20
+ export const AVOCADO = "0.18.0";
21
21
  /**
22
22
  * Next 15.5.25 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.17.0",
3
+ "version": "0.18.0",
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": {
@@ -29,14 +29,14 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@clack/prompts": "^0.9.1",
32
- "@avocadostudio-ai/skills": "^0.17.0"
32
+ "@avocadostudio-ai/skills": "^0.18.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^22.13.10",
36
36
  "tsx": "^4.19.0",
37
37
  "typescript": "^5.7.3",
38
- "@avocadostudio-ai/shared": "0.17.0",
39
- "@avocadostudio-ai/site-sdk": "0.17.0"
38
+ "@avocadostudio-ai/site-sdk": "0.18.0",
39
+ "@avocadostudio-ai/shared": "0.18.0"
40
40
  },
41
41
  "license": "Apache-2.0",
42
42
  "homepage": "https://docs.avocadostudio.dev",