@contentrail/cli 0.2.2 → 0.2.4

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.
Files changed (57) hide show
  1. package/README.md +36 -28
  2. package/dist/cli.d.ts +5 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +238 -140
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cloudflare/pages-client.d.ts +4 -0
  7. package/dist/cloudflare/pages-client.d.ts.map +1 -1
  8. package/dist/cloudflare/pages-client.js +15 -1
  9. package/dist/cloudflare/pages-client.js.map +1 -1
  10. package/dist/commands/configure.d.ts +17 -0
  11. package/dist/commands/configure.d.ts.map +1 -0
  12. package/dist/commands/configure.js +109 -0
  13. package/dist/commands/configure.js.map +1 -0
  14. package/dist/commands/connect.d.ts +3 -0
  15. package/dist/commands/connect.d.ts.map +1 -1
  16. package/dist/commands/connect.js +37 -8
  17. package/dist/commands/connect.js.map +1 -1
  18. package/dist/commands/init-next-admin.d.ts.map +1 -1
  19. package/dist/commands/init-next-admin.js +34 -0
  20. package/dist/commands/init-next-admin.js.map +1 -1
  21. package/dist/commands/init.d.ts +6 -0
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +141 -31
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/install/plan.d.ts +8 -0
  26. package/dist/install/plan.d.ts.map +1 -0
  27. package/dist/install/plan.js +29 -0
  28. package/dist/install/plan.js.map +1 -0
  29. package/dist/install/run.d.ts +5 -0
  30. package/dist/install/run.d.ts.map +1 -0
  31. package/dist/install/run.js +80 -0
  32. package/dist/install/run.js.map +1 -0
  33. package/dist/interactive/clack-host.d.ts +3 -0
  34. package/dist/interactive/clack-host.d.ts.map +1 -0
  35. package/dist/interactive/clack-host.js +47 -0
  36. package/dist/interactive/clack-host.js.map +1 -0
  37. package/dist/interactive/connect-wizard.d.ts +17 -0
  38. package/dist/interactive/connect-wizard.d.ts.map +1 -0
  39. package/dist/interactive/connect-wizard.js +41 -0
  40. package/dist/interactive/connect-wizard.js.map +1 -0
  41. package/dist/interactive/init-wizard.d.ts +12 -0
  42. package/dist/interactive/init-wizard.d.ts.map +1 -0
  43. package/dist/interactive/init-wizard.js +73 -0
  44. package/dist/interactive/init-wizard.js.map +1 -0
  45. package/dist/interactive/is-interactive.d.ts +9 -0
  46. package/dist/interactive/is-interactive.d.ts.map +1 -0
  47. package/dist/interactive/is-interactive.js +19 -0
  48. package/dist/interactive/is-interactive.js.map +1 -0
  49. package/dist/interactive/menu.d.ts +4 -0
  50. package/dist/interactive/menu.d.ts.map +1 -0
  51. package/dist/interactive/menu.js +55 -0
  52. package/dist/interactive/menu.js.map +1 -0
  53. package/dist/interactive/types.d.ts +34 -0
  54. package/dist/interactive/types.d.ts.map +1 -0
  55. package/dist/interactive/types.js +2 -0
  56. package/dist/interactive/types.js.map +1 -0
  57. package/package.json +5 -4
package/README.md CHANGED
@@ -1,25 +1,31 @@
1
1
  # `@contentrail/cli`
2
2
 
3
- Node.js CLI for ContentRail local development (ADR 020).
3
+ Node.js CLI for ContentRail local development (ADR 020 / ADR 029).
4
4
 
5
5
  Requires **Node.js ≥ 22**. Init works on any static site (`framework: "static"`).
6
6
  Astro and Vite have extra helpers (Astro **7.3.8**, Vite **8.3.5**;
7
7
  `@contentrail/astro` needs Node ≥ **22.12**).
8
8
 
9
- ## Install
10
-
11
- ### Monorepo
9
+ ## Install (one package)
12
10
 
13
11
  ```sh
14
- pnpm install
15
- pnpm --filter @contentrail/cli build
16
- pnpm exec contentrail --help
12
+ npm i -D @contentrail/cli
13
+ npx contentrail
17
14
  ```
18
15
 
19
- ### From npm (`0.2.2`)
16
+ On a TTY, `contentrail` opens an interactive menu: Setup, Connect, Doctor,
17
+ Status, Deploy. Setup scaffolds files and can install the other `@contentrail/*`
18
+ packages for your framework. You still configure Git + Cloudflare (tokens stay
19
+ in env / the dashboard — never in `contentrail.config.json`).
20
+
21
+ Non-interactive / CI: pass commands and `-y` / `--yes`. Use `init --install` to
22
+ add packages without prompts.
23
+
24
+ ### Monorepo
20
25
 
21
26
  ```sh
22
- pnpm add -D @contentrail/cli@0.2.2
27
+ pnpm install
28
+ pnpm --filter @contentrail/cli build
23
29
  pnpm exec contentrail --help
24
30
  ```
25
31
 
@@ -28,18 +34,21 @@ schemas use Zod **`^4.6.7`** via `@contentrail/core`.
28
34
 
29
35
  ## Commands
30
36
 
31
- | Command | Purpose |
32
- | --------------------------- | --------------------------------------------------------------------------------- |
33
- | `contentrail init` | Scaffold config, sample collection, sample JSON (`--with-functions`, `--dry-run`) |
34
- | `contentrail validate` | Validate config + local JSON content |
35
- | `contentrail build` | Validate, then run `package.json` build script |
36
- | `contentrail doctor` | Diagnose setup (no secret values printed) |
37
- | `contentrail status` | Summarize local readiness |
38
- | `contentrail dev` | Run `package.json` `dev` |
39
- | `contentrail import` | CSV → validated JSON for a registered collection |
40
- | `contentrail publish` | POST local JSON to `publishApiUrl` (env token) |
41
- | `contentrail deploy status` | Pages deployment status (`CF_API_TOKEN`) |
42
- | `contentrail pages setup` | Print Cloudflare automation capabilities |
37
+ | Command | Purpose |
38
+ | --------------------------- | --------------------------------------------------------------------------- |
39
+ | `contentrail` | Interactive control panel (TTY) |
40
+ | `contentrail init` | Scaffold config + optional `--install` of ContentRail packages |
41
+ | `contentrail validate` | Validate config + local JSON content |
42
+ | `contentrail build` | Validate, then run `package.json` build script |
43
+ | `contentrail doctor` | Diagnose setup (no secret values printed) |
44
+ | `contentrail status` | Summarize local readiness |
45
+ | `contentrail dev` | Run `package.json` `dev` |
46
+ | `contentrail import` | CSV → validated JSON for a registered collection |
47
+ | `contentrail publish` | POST local JSON to `publishApiUrl` (env token) |
48
+ | `contentrail connect` | GitHub origin + Cloudflare Pages project (interactive or `--confirm --yes`) |
49
+ | `contentrail deploy` | Build and deploy to Pages |
50
+ | `contentrail deploy status` | Pages deployment status (`CF_API_TOKEN`) |
51
+ | `contentrail pages setup` | Alias for connect |
43
52
 
44
53
  `rollback` is API-only. `publish` never sends GitHub tokens (ADR 020). See `docs/CONFIGURATION.md`.
45
54
 
@@ -71,13 +80,12 @@ schemas use Zod **`^4.6.7`** via `@contentrail/core`.
71
80
 
72
81
  ## Typical workflow
73
82
 
74
- 1. Create an Astro, Vite, or Next.js app (or use a monorepo `examples/*` app).
75
- 2. Add `@contentrail/core` + `zod@^4.6.7`, plus `@contentrail/astro` or `@contentrail/vite`
76
- when using those frameworks. Next.js uses `vite@^8.3.5` only to bundle `/admin`.
77
- 3. `pnpm exec contentrail init --framework vite` (or `astro` / `next`)
78
- 4. Edit collections / JSON under `content/`
79
- 5. `pnpm exec contentrail validate`
80
- 6. Framework `dev` / `contentrail build`
83
+ 1. Create an Astro, Vite, Next.js, or static site.
84
+ 2. `npm i -D @contentrail/cli` then `npx contentrail` → **Setup** (installs the rest).
85
+ 3. Export `CF_API_TOKEN` (never commit it) → **Connect** for Pages.
86
+ 4. Edit collections / JSON under `content/` (or use `/admin`).
87
+ 5. `npx contentrail validate` / `doctor`
88
+ 6. Framework `dev` / `contentrail build` / **Deploy**
81
89
  7. For `/admin`, add `@contentrail/editor` + `preact@^10.26.0` (`docs/ADMIN.md`)
82
90
  8. `contentrail doctor` when something fails
83
91
 
package/dist/cli.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { ExitCode } from "./exit-codes.js";
2
+ import type { InstallDepsFn, PromptHost } from "./interactive/types.js";
2
3
  export interface RunCliOptions {
3
4
  readonly cwd?: string;
4
5
  readonly stdout?: NodeJS.WritableStream;
@@ -6,6 +7,10 @@ export interface RunCliOptions {
6
7
  readonly env?: NodeJS.ProcessEnv;
7
8
  readonly fetchImpl?: typeof fetch;
8
9
  readonly gitRemoteUrl?: string;
10
+ /** Force interactive UI on/off (tests). Default: TTY + non-CI detection. */
11
+ readonly interactive?: boolean;
12
+ readonly prompts?: PromptHost;
13
+ readonly installDeps?: InstallDepsFn;
9
14
  }
10
15
  export declare function runCli(argv: readonly string[], options?: RunCliOptions): Promise<ExitCode>;
11
16
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAoI3C,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,QAAQ,CAAC,CA+MnB"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAI3C,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA+IxE,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IACxC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,cAAc,CAAC;IACxC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IAClC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,QAAQ,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,aAAa,CAAC;CACtC;AAED,wBAAsB,MAAM,CAC1B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,aAAa,GACtB,OAAO,CAAC,QAAQ,CAAC,CAyFnB"}
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { runBuild } from "./commands/build.js";
4
+ import { runConfigure } from "./commands/configure.js";
4
5
  import { runConnect } from "./commands/connect.js";
5
6
  import { runDeploy, runDeployStatus } from "./commands/deploy.js";
6
7
  import { runDev } from "./commands/dev.js";
@@ -12,16 +13,25 @@ import { runStatus } from "./commands/status.js";
12
13
  import { runValidate } from "./commands/validate.js";
13
14
  import { loadProject } from "./config/load.js";
14
15
  import { ExitCode } from "./exit-codes.js";
16
+ import { createClackPromptHost } from "./interactive/clack-host.js";
17
+ import { canUseInteractiveUi } from "./interactive/is-interactive.js";
18
+ import { runMainMenu } from "./interactive/menu.js";
19
+ import { createPackageInstallRunner } from "./install/run.js";
15
20
  import { createLogger } from "./util/print.js";
16
21
  import { findProjectRoot } from "./util/paths.js";
17
22
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
18
- const HELP = `ContentRail CLI — local developer tooling
23
+ const HELP = `ContentRail CLI — developer control panel for your static site
19
24
 
20
25
  Usage:
26
+ contentrail Interactive menu (TTY)
21
27
  contentrail <command> [options]
22
28
 
29
+ Install once:
30
+ npm i -D @contentrail/cli
31
+ npx contentrail
32
+
23
33
  Commands:
24
- init Add ContentRail to an existing static site (any framework)
34
+ init Add ContentRail + optionally install packages for this site
25
35
  validate Validate configuration and local JSON content
26
36
  build Validate then run the project build script
27
37
  doctor Diagnose common setup problems
@@ -30,6 +40,7 @@ Commands:
30
40
  import Import a local CSV file into a registered collection
31
41
  publish POST local JSON to the configured publish API (needs token)
32
42
  connect Record GitHub origin + create/patch their Cloudflare Pages project
43
+ configure Push admin username/password + GitHub token to Pages secrets
33
44
  deploy Build and deploy to their Cloudflare Pages project
34
45
  deploy status Fetch Cloudflare Pages deployment status
35
46
  pages setup Alias for connect
@@ -43,12 +54,13 @@ Global options:
43
54
  init options:
44
55
  --framework <astro|vite|next|static> Force framework (default: auto-detect)
45
56
  --force Overwrite existing generated files
57
+ --install Install ContentRail packages with the project package manager
46
58
  --no-editor Skip contentrail.editor.json
47
59
  --no-functions Skip Pages Functions templates
48
- -y, --yes Non-interactive; accept defaults
60
+ -y, --yes Non-interactive; accept defaults (skips prompts)
49
61
 
50
62
  connect / deploy:
51
- --confirm --yes Required to create or change Pages projects
63
+ --confirm --yes Required to create or change Pages projects (non-interactive)
52
64
  --account-id --project Cloudflare account and Pages project name
53
65
  --skip-build Deploy without running the site build
54
66
 
@@ -66,7 +78,7 @@ deploy status options:
66
78
  --project <pages-project-name>
67
79
 
68
80
  pages setup options:
69
- --confirm Required before any Cloudflare mutation (still blocked in CLI)
81
+ --confirm Required before any Cloudflare mutation
70
82
  -y, --yes Non-interactive confirmation
71
83
 
72
84
  Exit codes:
@@ -78,13 +90,16 @@ Exit codes:
78
90
  Notes:
79
91
  rollback is not a CLI command. Use the authenticated Worker/Pages API.
80
92
  CLI publish never sends GitHub tokens; it uses CONTENTRAIL_PUBLISH_TOKEN.
93
+ Secrets never go in contentrail.config.json.
81
94
  `;
82
95
  function printCommandHelp(command) {
83
96
  switch (command) {
84
97
  case "init":
85
- return `Usage: contentrail init [--framework astro|vite|static] [--force] [--with-editor] [--with-functions] [--dry-run] [-y]
98
+ return `Usage: contentrail init [--framework astro|vite|next|static] [--force] [--install] [--no-editor] [--no-functions] [--dry-run] [-y]
86
99
 
87
100
  Creates contentrail.config.json, a sample collection module, and sample JSON.
101
+ With --install (or interactive Setup), installs the ContentRail packages this
102
+ site needs so you only add @contentrail/cli yourself.
88
103
  Works on Next.js, React, vanilla, Astro, and Vite sites. Does not generate the public site.
89
104
  Never overwrites existing files unless --force is set.`;
90
105
  case "validate":
@@ -138,141 +153,205 @@ Prints the Cloudflare automation capability map. Does not mutate production.`;
138
153
  export async function runCli(argv, options) {
139
154
  const log = createLogger(options?.stdout, options?.stderr);
140
155
  const baseCwd = options?.cwd ?? process.cwd();
156
+ const env = options?.env ?? process.env;
157
+ const prompts = options?.prompts ?? createClackPromptHost();
158
+ const installDeps = options?.installDeps ?? createPackageInstallRunner({ env });
141
159
  try {
142
160
  const { command, subcommand, flags, cwd } = parseArgv(argv, baseCwd);
161
+ const interactive = options?.interactive ??
162
+ (canUseInteractiveUi(env) && !flags.yes && !flags.help && !flags.version);
143
163
  if (flags.version) {
144
164
  log.info(VERSION);
145
165
  return ExitCode.Success;
146
166
  }
147
- if (command === undefined || command === "help" || flags.help) {
148
- log.info(command !== undefined && command !== "help"
149
- ? printCommandHelp(command)
150
- : HELP);
167
+ if (flags.help) {
168
+ log.info(command !== undefined ? printCommandHelp(command) : HELP);
151
169
  return ExitCode.Success;
152
170
  }
153
- const projectRoot = findProjectRoot(cwd);
154
- const env = options?.env ?? process.env;
155
- switch (command) {
156
- case "init": {
157
- const framework = flags.framework;
158
- if (framework !== undefined &&
159
- framework !== "astro" &&
160
- framework !== "vite" &&
161
- framework !== "static" &&
162
- framework !== "next") {
163
- log.error('--framework must be "astro", "vite", "next", or "static".');
164
- return ExitCode.Usage;
165
- }
166
- return runInit({
167
- projectRoot,
168
- ...(framework !== undefined ? { framework } : {}),
169
- force: flags.force,
170
- withEditor: flags.withEditor,
171
- withFunctions: flags.withFunctions,
172
- dryRun: flags.dryRun,
173
- yes: flags.yes,
174
- }, log);
171
+ if (command === "help") {
172
+ log.info(HELP);
173
+ return ExitCode.Success;
174
+ }
175
+ if (command === undefined) {
176
+ if (!interactive) {
177
+ log.info(HELP);
178
+ return ExitCode.Success;
175
179
  }
176
- case "validate": {
177
- const project = await loadProject(projectRoot);
178
- return (await runValidate(project, log)).code;
180
+ const action = await runMainMenu(prompts, log);
181
+ if (action === "cancelled" || action === "exit") {
182
+ return ExitCode.Success;
179
183
  }
180
- case "build": {
181
- const project = await loadProject(projectRoot);
182
- return runBuild(project, log);
184
+ if (action === "help") {
185
+ log.info(HELP);
186
+ return ExitCode.Success;
183
187
  }
184
- case "doctor":
185
- return runDoctor(projectRoot, log);
186
- case "status":
187
- return runStatus(projectRoot, log);
188
- case "dev":
189
- return runDev(projectRoot, log);
190
- case "import": {
191
- if (flags.collection === undefined || flags.file === undefined) {
192
- log.error("import requires --collection and --file.");
193
- return ExitCode.Usage;
194
- }
195
- return runImport({
196
- projectRoot,
197
- collection: flags.collection,
198
- file: flags.file,
199
- dryRun: flags.dryRun,
200
- force: flags.force,
201
- }, log);
188
+ return await dispatchCommand(action, {
189
+ projectRoot: findProjectRoot(cwd),
190
+ flags: {
191
+ ...flags,
192
+ yes: false,
193
+ },
194
+ env,
195
+ log,
196
+ interactive: true,
197
+ prompts,
198
+ installDeps,
199
+ ...(options !== undefined ? { options } : {}),
200
+ });
201
+ }
202
+ const projectRoot = findProjectRoot(cwd);
203
+ return await dispatchCommand(command, {
204
+ projectRoot,
205
+ ...(subcommand !== undefined ? { subcommand } : {}),
206
+ flags,
207
+ env,
208
+ log,
209
+ interactive: interactive && !flags.yes,
210
+ prompts,
211
+ installDeps,
212
+ ...(options !== undefined ? { options } : {}),
213
+ });
214
+ }
215
+ catch (error) {
216
+ const message = error instanceof Error ? error.message : String(error);
217
+ log.error(message);
218
+ if (/Unknown option|Unexpected argument|requires a directory|--framework requires/i.test(message)) {
219
+ return ExitCode.Usage;
220
+ }
221
+ if (/contentrail\.config\.json|Invalid contentrail|collections module|escapes the project/i.test(message)) {
222
+ return ExitCode.Config;
223
+ }
224
+ return ExitCode.Failure;
225
+ }
226
+ }
227
+ async function dispatchCommand(command, ctx) {
228
+ const { projectRoot, subcommand, flags, env, log, interactive, prompts, installDeps, options, } = ctx;
229
+ switch (command) {
230
+ case "setup":
231
+ case "init": {
232
+ const framework = flags.framework;
233
+ if (framework !== undefined &&
234
+ framework !== "astro" &&
235
+ framework !== "vite" &&
236
+ framework !== "static" &&
237
+ framework !== "next") {
238
+ log.error('--framework must be "astro", "vite", "next", or "static".');
239
+ return ExitCode.Usage;
202
240
  }
203
- case "publish": {
204
- if (flags.collection === undefined) {
205
- log.error("publish requires --collection.");
206
- return ExitCode.Usage;
207
- }
208
- return runPublish({
209
- projectRoot,
210
- collection: flags.collection,
211
- dryRun: flags.dryRun,
212
- env,
213
- ...(flags.expectedVersion !== undefined
214
- ? {
215
- expectedVersion: flags.expectedVersion === "null"
216
- ? null
217
- : flags.expectedVersion,
218
- }
219
- : {}),
220
- ...(options?.fetchImpl !== undefined
221
- ? { fetchImpl: options.fetchImpl }
222
- : {}),
223
- }, log);
241
+ const useWizard = interactive && (command === "setup" || !flags.yes);
242
+ return runInit({
243
+ projectRoot,
244
+ ...(framework !== undefined ? { framework } : {}),
245
+ force: flags.force,
246
+ ...(useWizard && !flags.editorFlagSet
247
+ ? {}
248
+ : { withEditor: flags.withEditor }),
249
+ ...(useWizard && !flags.functionsFlagSet
250
+ ? {}
251
+ : { withFunctions: flags.withFunctions }),
252
+ dryRun: flags.dryRun,
253
+ yes: flags.yes,
254
+ install: flags.install,
255
+ interactive: useWizard,
256
+ prompts,
257
+ installDeps,
258
+ }, log);
259
+ }
260
+ case "validate": {
261
+ const project = await loadProject(projectRoot);
262
+ return (await runValidate(project, log)).code;
263
+ }
264
+ case "build": {
265
+ const project = await loadProject(projectRoot);
266
+ return runBuild(project, log);
267
+ }
268
+ case "doctor":
269
+ return runDoctor(projectRoot, log);
270
+ case "status":
271
+ return runStatus(projectRoot, log);
272
+ case "dev":
273
+ return runDev(projectRoot, log);
274
+ case "import": {
275
+ if (flags.collection === undefined || flags.file === undefined) {
276
+ log.error("import requires --collection and --file.");
277
+ return ExitCode.Usage;
224
278
  }
225
- case "connect":
226
- case "pages": {
227
- if (command === "pages" && subcommand !== "setup") {
228
- log.error("Use `contentrail pages setup` or `contentrail connect`.");
229
- return ExitCode.Usage;
230
- }
231
- return runConnect({
232
- projectRoot,
233
- dryRun: flags.dryRun,
234
- confirm: flags.confirm,
235
- yes: flags.yes,
236
- env,
237
- ...(flags.accountId !== undefined
238
- ? { accountId: flags.accountId }
239
- : {}),
240
- ...(flags.project !== undefined
241
- ? { projectName: flags.project }
242
- : {}),
243
- ...(options?.fetchImpl !== undefined
244
- ? { fetchImpl: options.fetchImpl }
245
- : {}),
246
- ...(options?.gitRemoteUrl !== undefined
247
- ? { gitRemoteUrl: options.gitRemoteUrl }
248
- : {}),
249
- }, log);
279
+ return runImport({
280
+ projectRoot,
281
+ collection: flags.collection,
282
+ file: flags.file,
283
+ dryRun: flags.dryRun,
284
+ force: flags.force,
285
+ }, log);
286
+ }
287
+ case "publish": {
288
+ if (flags.collection === undefined) {
289
+ log.error("publish requires --collection.");
290
+ return ExitCode.Usage;
291
+ }
292
+ return runPublish({
293
+ projectRoot,
294
+ collection: flags.collection,
295
+ dryRun: flags.dryRun,
296
+ env,
297
+ ...(flags.expectedVersion !== undefined
298
+ ? {
299
+ expectedVersion: flags.expectedVersion === "null"
300
+ ? null
301
+ : flags.expectedVersion,
302
+ }
303
+ : {}),
304
+ ...(options?.fetchImpl !== undefined
305
+ ? { fetchImpl: options.fetchImpl }
306
+ : {}),
307
+ }, log);
308
+ }
309
+ case "configure": {
310
+ return runConfigure({
311
+ projectRoot,
312
+ dryRun: flags.dryRun,
313
+ yes: flags.yes,
314
+ interactive,
315
+ prompts,
316
+ env,
317
+ ...(options?.fetchImpl !== undefined
318
+ ? { fetchImpl: options.fetchImpl }
319
+ : {}),
320
+ }, log);
321
+ }
322
+ case "connect":
323
+ case "pages": {
324
+ if (command === "pages" && subcommand !== "setup") {
325
+ log.error("Use `contentrail pages setup` or `contentrail connect`.");
326
+ return ExitCode.Usage;
250
327
  }
251
- case "deploy": {
252
- if (subcommand === "status") {
253
- return runDeployStatus({
254
- projectRoot,
255
- env,
256
- ...(flags.accountId !== undefined
257
- ? { accountId: flags.accountId }
258
- : {}),
259
- ...(flags.project !== undefined
260
- ? { projectName: flags.project }
261
- : {}),
262
- ...(options?.fetchImpl !== undefined
263
- ? { fetchImpl: options.fetchImpl }
264
- : {}),
265
- }, log);
266
- }
267
- if (subcommand !== undefined) {
268
- log.error("Use `contentrail deploy` or `contentrail deploy status`.");
269
- return ExitCode.Usage;
270
- }
271
- return runDeploy({
328
+ return runConnect({
329
+ projectRoot,
330
+ dryRun: flags.dryRun,
331
+ confirm: flags.confirm,
332
+ yes: flags.yes,
333
+ env,
334
+ interactive,
335
+ prompts,
336
+ ...(flags.accountId !== undefined
337
+ ? { accountId: flags.accountId }
338
+ : {}),
339
+ ...(flags.project !== undefined
340
+ ? { projectName: flags.project }
341
+ : {}),
342
+ ...(options?.fetchImpl !== undefined
343
+ ? { fetchImpl: options.fetchImpl }
344
+ : {}),
345
+ ...(options?.gitRemoteUrl !== undefined
346
+ ? { gitRemoteUrl: options.gitRemoteUrl }
347
+ : {}),
348
+ }, log);
349
+ }
350
+ case "deploy": {
351
+ if (subcommand === "status") {
352
+ return runDeployStatus({
272
353
  projectRoot,
273
354
  env,
274
- skipBuild: flags.skipBuild,
275
- dryRun: flags.dryRun,
276
355
  ...(flags.accountId !== undefined
277
356
  ? { accountId: flags.accountId }
278
357
  : {}),
@@ -284,24 +363,32 @@ export async function runCli(argv, options) {
284
363
  : {}),
285
364
  }, log);
286
365
  }
287
- case "rollback":
288
- log.error('Command "rollback" is not implemented in the CLI. Use the authenticated Worker or Pages API.');
289
- return ExitCode.Usage;
290
- default:
291
- log.error(`Unknown command "${command}". Run contentrail --help.`);
366
+ if (subcommand !== undefined) {
367
+ log.error("Use `contentrail deploy` or `contentrail deploy status`.");
292
368
  return ExitCode.Usage;
369
+ }
370
+ return runDeploy({
371
+ projectRoot,
372
+ env,
373
+ skipBuild: flags.skipBuild,
374
+ dryRun: flags.dryRun,
375
+ ...(flags.accountId !== undefined
376
+ ? { accountId: flags.accountId }
377
+ : {}),
378
+ ...(flags.project !== undefined
379
+ ? { projectName: flags.project }
380
+ : {}),
381
+ ...(options?.fetchImpl !== undefined
382
+ ? { fetchImpl: options.fetchImpl }
383
+ : {}),
384
+ }, log);
293
385
  }
294
- }
295
- catch (error) {
296
- const message = error instanceof Error ? error.message : String(error);
297
- log.error(message);
298
- if (/Unknown option|Unexpected argument|requires a directory|--framework requires/i.test(message)) {
386
+ case "rollback":
387
+ log.error('Command "rollback" is not implemented in the CLI. Use the authenticated Worker or Pages API.');
388
+ return ExitCode.Usage;
389
+ default:
390
+ log.error(`Unknown command "${command}". Run contentrail --help.`);
299
391
  return ExitCode.Usage;
300
- }
301
- if (/contentrail\.config\.json|Invalid contentrail|collections module|escapes the project/i.test(message)) {
302
- return ExitCode.Config;
303
- }
304
- return ExitCode.Failure;
305
392
  }
306
393
  }
307
394
  function parseArgv(argv, baseCwd) {
@@ -314,10 +401,13 @@ function parseArgv(argv, baseCwd) {
314
401
  force: false,
315
402
  withEditor: true,
316
403
  withFunctions: true,
404
+ editorFlagSet: false,
405
+ functionsFlagSet: false,
317
406
  yes: false,
318
407
  dryRun: false,
319
408
  confirm: false,
320
409
  skipBuild: false,
410
+ install: false,
321
411
  };
322
412
  const take = (index, flag) => {
323
413
  const value = argv[index];
@@ -340,20 +430,28 @@ function parseArgv(argv, baseCwd) {
340
430
  flags.force = true;
341
431
  continue;
342
432
  }
433
+ if (arg === "--install") {
434
+ flags.install = true;
435
+ continue;
436
+ }
343
437
  if (arg === "--with-editor") {
344
438
  flags.withEditor = true;
439
+ flags.editorFlagSet = true;
345
440
  continue;
346
441
  }
347
442
  if (arg === "--with-functions") {
348
443
  flags.withFunctions = true;
444
+ flags.functionsFlagSet = true;
349
445
  continue;
350
446
  }
351
447
  if (arg === "--no-editor") {
352
448
  flags.withEditor = false;
449
+ flags.editorFlagSet = true;
353
450
  continue;
354
451
  }
355
452
  if (arg === "--no-functions") {
356
453
  flags.withFunctions = false;
454
+ flags.functionsFlagSet = true;
357
455
  continue;
358
456
  }
359
457
  if (arg === "--skip-build") {