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 +24 -0
- package/dist/args.d.ts +42 -0
- package/dist/args.js +97 -0
- package/dist/index.js +72 -21
- package/dist/versions.d.ts +1 -1
- package/dist/versions.js +1 -1
- package/package.json +4 -4
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 =
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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;
|
package/dist/versions.d.ts
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 declare const AVOCADO = "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.
|
|
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.
|
|
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.
|
|
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/
|
|
39
|
-
"@avocadostudio-ai/
|
|
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",
|