@contentrail/cli 0.2.1 → 0.2.3

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 (53) hide show
  1. package/README.md +37 -29
  2. package/dist/cli.d.ts +5 -0
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +223 -140
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/connect.d.ts +3 -0
  7. package/dist/commands/connect.d.ts.map +1 -1
  8. package/dist/commands/connect.js +37 -8
  9. package/dist/commands/connect.js.map +1 -1
  10. package/dist/commands/doctor.d.ts.map +1 -1
  11. package/dist/commands/doctor.js +14 -0
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/init-next-admin.d.ts +7 -0
  14. package/dist/commands/init-next-admin.d.ts.map +1 -0
  15. package/dist/commands/init-next-admin.js +170 -0
  16. package/dist/commands/init-next-admin.js.map +1 -0
  17. package/dist/commands/init.d.ts +6 -0
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +87 -16
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/install/plan.d.ts +8 -0
  22. package/dist/install/plan.d.ts.map +1 -0
  23. package/dist/install/plan.js +29 -0
  24. package/dist/install/plan.js.map +1 -0
  25. package/dist/install/run.d.ts +5 -0
  26. package/dist/install/run.d.ts.map +1 -0
  27. package/dist/install/run.js +80 -0
  28. package/dist/install/run.js.map +1 -0
  29. package/dist/interactive/clack-host.d.ts +3 -0
  30. package/dist/interactive/clack-host.d.ts.map +1 -0
  31. package/dist/interactive/clack-host.js +47 -0
  32. package/dist/interactive/clack-host.js.map +1 -0
  33. package/dist/interactive/connect-wizard.d.ts +17 -0
  34. package/dist/interactive/connect-wizard.d.ts.map +1 -0
  35. package/dist/interactive/connect-wizard.js +41 -0
  36. package/dist/interactive/connect-wizard.js.map +1 -0
  37. package/dist/interactive/init-wizard.d.ts +12 -0
  38. package/dist/interactive/init-wizard.d.ts.map +1 -0
  39. package/dist/interactive/init-wizard.js +73 -0
  40. package/dist/interactive/init-wizard.js.map +1 -0
  41. package/dist/interactive/is-interactive.d.ts +9 -0
  42. package/dist/interactive/is-interactive.d.ts.map +1 -0
  43. package/dist/interactive/is-interactive.js +19 -0
  44. package/dist/interactive/is-interactive.js.map +1 -0
  45. package/dist/interactive/menu.d.ts +4 -0
  46. package/dist/interactive/menu.d.ts.map +1 -0
  47. package/dist/interactive/menu.js +50 -0
  48. package/dist/interactive/menu.js.map +1 -0
  49. package/dist/interactive/types.d.ts +34 -0
  50. package/dist/interactive/types.d.ts.map +1 -0
  51. package/dist/interactive/types.js +2 -0
  52. package/dist/interactive/types.js.map +1 -0
  53. 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.1`)
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.1
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,14 +80,13 @@ schemas use Zod **`^4.6.7`** via `@contentrail/core`.
71
80
 
72
81
  ## Typical workflow
73
82
 
74
- 1. Create an Astro or Vite app (or use a monorepo `examples/*` app).
75
- 2. Add `@contentrail/core` + `zod`, plus `@contentrail/astro` or `@contentrail/vite`
76
- (workspace links today; `pnpm add` from npm only after public release).
77
- 3. `pnpm exec contentrail init --framework vite` (or `astro`)
78
- 4. Edit collections / JSON under `content/`
79
- 5. `pnpm exec contentrail validate`
80
- 6. Framework `dev` / `contentrail build`
81
- 7. For `/admin`, add `@contentrail/editor` + `preact` and the admin plugin (`docs/ADMIN.md`)
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**
89
+ 7. For `/admin`, add `@contentrail/editor` + `preact@^10.26.0` (`docs/ADMIN.md`)
82
90
  8. `contentrail doctor` when something fails
83
91
 
84
92
  ## Limitations
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":"AAaA,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAI3C,OAAO,KAAK,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA8IxE,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
@@ -12,16 +12,25 @@ import { runStatus } from "./commands/status.js";
12
12
  import { runValidate } from "./commands/validate.js";
13
13
  import { loadProject } from "./config/load.js";
14
14
  import { ExitCode } from "./exit-codes.js";
15
+ import { createClackPromptHost } from "./interactive/clack-host.js";
16
+ import { canUseInteractiveUi } from "./interactive/is-interactive.js";
17
+ import { runMainMenu } from "./interactive/menu.js";
18
+ import { createPackageInstallRunner } from "./install/run.js";
15
19
  import { createLogger } from "./util/print.js";
16
20
  import { findProjectRoot } from "./util/paths.js";
17
21
  const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
18
- const HELP = `ContentRail CLI — local developer tooling
22
+ const HELP = `ContentRail CLI — developer control panel for your static site
19
23
 
20
24
  Usage:
25
+ contentrail Interactive menu (TTY)
21
26
  contentrail <command> [options]
22
27
 
28
+ Install once:
29
+ npm i -D @contentrail/cli
30
+ npx contentrail
31
+
23
32
  Commands:
24
- init Add ContentRail to an existing static site (any framework)
33
+ init Add ContentRail + optionally install packages for this site
25
34
  validate Validate configuration and local JSON content
26
35
  build Validate then run the project build script
27
36
  doctor Diagnose common setup problems
@@ -43,12 +52,13 @@ Global options:
43
52
  init options:
44
53
  --framework <astro|vite|next|static> Force framework (default: auto-detect)
45
54
  --force Overwrite existing generated files
55
+ --install Install ContentRail packages with the project package manager
46
56
  --no-editor Skip contentrail.editor.json
47
57
  --no-functions Skip Pages Functions templates
48
- -y, --yes Non-interactive; accept defaults
58
+ -y, --yes Non-interactive; accept defaults (skips prompts)
49
59
 
50
60
  connect / deploy:
51
- --confirm --yes Required to create or change Pages projects
61
+ --confirm --yes Required to create or change Pages projects (non-interactive)
52
62
  --account-id --project Cloudflare account and Pages project name
53
63
  --skip-build Deploy without running the site build
54
64
 
@@ -66,7 +76,7 @@ deploy status options:
66
76
  --project <pages-project-name>
67
77
 
68
78
  pages setup options:
69
- --confirm Required before any Cloudflare mutation (still blocked in CLI)
79
+ --confirm Required before any Cloudflare mutation
70
80
  -y, --yes Non-interactive confirmation
71
81
 
72
82
  Exit codes:
@@ -78,13 +88,16 @@ Exit codes:
78
88
  Notes:
79
89
  rollback is not a CLI command. Use the authenticated Worker/Pages API.
80
90
  CLI publish never sends GitHub tokens; it uses CONTENTRAIL_PUBLISH_TOKEN.
91
+ Secrets never go in contentrail.config.json.
81
92
  `;
82
93
  function printCommandHelp(command) {
83
94
  switch (command) {
84
95
  case "init":
85
- return `Usage: contentrail init [--framework astro|vite|static] [--force] [--with-editor] [--with-functions] [--dry-run] [-y]
96
+ return `Usage: contentrail init [--framework astro|vite|next|static] [--force] [--install] [--no-editor] [--no-functions] [--dry-run] [-y]
86
97
 
87
98
  Creates contentrail.config.json, a sample collection module, and sample JSON.
99
+ With --install (or interactive Setup), installs the ContentRail packages this
100
+ site needs so you only add @contentrail/cli yourself.
88
101
  Works on Next.js, React, vanilla, Astro, and Vite sites. Does not generate the public site.
89
102
  Never overwrites existing files unless --force is set.`;
90
103
  case "validate":
@@ -138,141 +151,192 @@ Prints the Cloudflare automation capability map. Does not mutate production.`;
138
151
  export async function runCli(argv, options) {
139
152
  const log = createLogger(options?.stdout, options?.stderr);
140
153
  const baseCwd = options?.cwd ?? process.cwd();
154
+ const env = options?.env ?? process.env;
155
+ const prompts = options?.prompts ?? createClackPromptHost();
156
+ const installDeps = options?.installDeps ?? createPackageInstallRunner({ env });
141
157
  try {
142
158
  const { command, subcommand, flags, cwd } = parseArgv(argv, baseCwd);
159
+ const interactive = options?.interactive ??
160
+ (canUseInteractiveUi(env) && !flags.yes && !flags.help && !flags.version);
143
161
  if (flags.version) {
144
162
  log.info(VERSION);
145
163
  return ExitCode.Success;
146
164
  }
147
- if (command === undefined || command === "help" || flags.help) {
148
- log.info(command !== undefined && command !== "help"
149
- ? printCommandHelp(command)
150
- : HELP);
165
+ if (flags.help) {
166
+ log.info(command !== undefined ? printCommandHelp(command) : HELP);
151
167
  return ExitCode.Success;
152
168
  }
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);
169
+ if (command === "help") {
170
+ log.info(HELP);
171
+ return ExitCode.Success;
172
+ }
173
+ if (command === undefined) {
174
+ if (!interactive) {
175
+ log.info(HELP);
176
+ return ExitCode.Success;
175
177
  }
176
- case "validate": {
177
- const project = await loadProject(projectRoot);
178
- return (await runValidate(project, log)).code;
178
+ const action = await runMainMenu(prompts, log);
179
+ if (action === "cancelled" || action === "exit") {
180
+ return ExitCode.Success;
179
181
  }
180
- case "build": {
181
- const project = await loadProject(projectRoot);
182
- return runBuild(project, log);
182
+ if (action === "help") {
183
+ log.info(HELP);
184
+ return ExitCode.Success;
183
185
  }
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);
186
+ return await dispatchCommand(action, {
187
+ projectRoot: findProjectRoot(cwd),
188
+ flags: {
189
+ ...flags,
190
+ yes: false,
191
+ },
192
+ env,
193
+ log,
194
+ interactive: true,
195
+ prompts,
196
+ installDeps,
197
+ ...(options !== undefined ? { options } : {}),
198
+ });
199
+ }
200
+ const projectRoot = findProjectRoot(cwd);
201
+ return await dispatchCommand(command, {
202
+ projectRoot,
203
+ ...(subcommand !== undefined ? { subcommand } : {}),
204
+ flags,
205
+ env,
206
+ log,
207
+ interactive: interactive && !flags.yes,
208
+ prompts,
209
+ installDeps,
210
+ ...(options !== undefined ? { options } : {}),
211
+ });
212
+ }
213
+ catch (error) {
214
+ const message = error instanceof Error ? error.message : String(error);
215
+ log.error(message);
216
+ if (/Unknown option|Unexpected argument|requires a directory|--framework requires/i.test(message)) {
217
+ return ExitCode.Usage;
218
+ }
219
+ if (/contentrail\.config\.json|Invalid contentrail|collections module|escapes the project/i.test(message)) {
220
+ return ExitCode.Config;
221
+ }
222
+ return ExitCode.Failure;
223
+ }
224
+ }
225
+ async function dispatchCommand(command, ctx) {
226
+ const { projectRoot, subcommand, flags, env, log, interactive, prompts, installDeps, options, } = ctx;
227
+ switch (command) {
228
+ case "setup":
229
+ case "init": {
230
+ const framework = flags.framework;
231
+ if (framework !== undefined &&
232
+ framework !== "astro" &&
233
+ framework !== "vite" &&
234
+ framework !== "static" &&
235
+ framework !== "next") {
236
+ log.error('--framework must be "astro", "vite", "next", or "static".');
237
+ return ExitCode.Usage;
202
238
  }
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);
239
+ const useWizard = interactive && (command === "setup" || !flags.yes);
240
+ return runInit({
241
+ projectRoot,
242
+ ...(framework !== undefined ? { framework } : {}),
243
+ force: flags.force,
244
+ ...(useWizard && !flags.editorFlagSet
245
+ ? {}
246
+ : { withEditor: flags.withEditor }),
247
+ ...(useWizard && !flags.functionsFlagSet
248
+ ? {}
249
+ : { withFunctions: flags.withFunctions }),
250
+ dryRun: flags.dryRun,
251
+ yes: flags.yes,
252
+ install: flags.install,
253
+ interactive: useWizard,
254
+ prompts,
255
+ installDeps,
256
+ }, log);
257
+ }
258
+ case "validate": {
259
+ const project = await loadProject(projectRoot);
260
+ return (await runValidate(project, log)).code;
261
+ }
262
+ case "build": {
263
+ const project = await loadProject(projectRoot);
264
+ return runBuild(project, log);
265
+ }
266
+ case "doctor":
267
+ return runDoctor(projectRoot, log);
268
+ case "status":
269
+ return runStatus(projectRoot, log);
270
+ case "dev":
271
+ return runDev(projectRoot, log);
272
+ case "import": {
273
+ if (flags.collection === undefined || flags.file === undefined) {
274
+ log.error("import requires --collection and --file.");
275
+ return ExitCode.Usage;
224
276
  }
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);
277
+ return runImport({
278
+ projectRoot,
279
+ collection: flags.collection,
280
+ file: flags.file,
281
+ dryRun: flags.dryRun,
282
+ force: flags.force,
283
+ }, log);
284
+ }
285
+ case "publish": {
286
+ if (flags.collection === undefined) {
287
+ log.error("publish requires --collection.");
288
+ return ExitCode.Usage;
289
+ }
290
+ return runPublish({
291
+ projectRoot,
292
+ collection: flags.collection,
293
+ dryRun: flags.dryRun,
294
+ env,
295
+ ...(flags.expectedVersion !== undefined
296
+ ? {
297
+ expectedVersion: flags.expectedVersion === "null"
298
+ ? null
299
+ : flags.expectedVersion,
300
+ }
301
+ : {}),
302
+ ...(options?.fetchImpl !== undefined
303
+ ? { fetchImpl: options.fetchImpl }
304
+ : {}),
305
+ }, log);
306
+ }
307
+ case "connect":
308
+ case "pages": {
309
+ if (command === "pages" && subcommand !== "setup") {
310
+ log.error("Use `contentrail pages setup` or `contentrail connect`.");
311
+ return ExitCode.Usage;
250
312
  }
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({
313
+ return runConnect({
314
+ projectRoot,
315
+ dryRun: flags.dryRun,
316
+ confirm: flags.confirm,
317
+ yes: flags.yes,
318
+ env,
319
+ interactive,
320
+ prompts,
321
+ ...(flags.accountId !== undefined
322
+ ? { accountId: flags.accountId }
323
+ : {}),
324
+ ...(flags.project !== undefined
325
+ ? { projectName: flags.project }
326
+ : {}),
327
+ ...(options?.fetchImpl !== undefined
328
+ ? { fetchImpl: options.fetchImpl }
329
+ : {}),
330
+ ...(options?.gitRemoteUrl !== undefined
331
+ ? { gitRemoteUrl: options.gitRemoteUrl }
332
+ : {}),
333
+ }, log);
334
+ }
335
+ case "deploy": {
336
+ if (subcommand === "status") {
337
+ return runDeployStatus({
272
338
  projectRoot,
273
339
  env,
274
- skipBuild: flags.skipBuild,
275
- dryRun: flags.dryRun,
276
340
  ...(flags.accountId !== undefined
277
341
  ? { accountId: flags.accountId }
278
342
  : {}),
@@ -284,24 +348,32 @@ export async function runCli(argv, options) {
284
348
  : {}),
285
349
  }, log);
286
350
  }
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.`);
351
+ if (subcommand !== undefined) {
352
+ log.error("Use `contentrail deploy` or `contentrail deploy status`.");
292
353
  return ExitCode.Usage;
354
+ }
355
+ return runDeploy({
356
+ projectRoot,
357
+ env,
358
+ skipBuild: flags.skipBuild,
359
+ dryRun: flags.dryRun,
360
+ ...(flags.accountId !== undefined
361
+ ? { accountId: flags.accountId }
362
+ : {}),
363
+ ...(flags.project !== undefined
364
+ ? { projectName: flags.project }
365
+ : {}),
366
+ ...(options?.fetchImpl !== undefined
367
+ ? { fetchImpl: options.fetchImpl }
368
+ : {}),
369
+ }, log);
293
370
  }
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)) {
371
+ case "rollback":
372
+ log.error('Command "rollback" is not implemented in the CLI. Use the authenticated Worker or Pages API.');
373
+ return ExitCode.Usage;
374
+ default:
375
+ log.error(`Unknown command "${command}". Run contentrail --help.`);
299
376
  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
377
  }
306
378
  }
307
379
  function parseArgv(argv, baseCwd) {
@@ -314,10 +386,13 @@ function parseArgv(argv, baseCwd) {
314
386
  force: false,
315
387
  withEditor: true,
316
388
  withFunctions: true,
389
+ editorFlagSet: false,
390
+ functionsFlagSet: false,
317
391
  yes: false,
318
392
  dryRun: false,
319
393
  confirm: false,
320
394
  skipBuild: false,
395
+ install: false,
321
396
  };
322
397
  const take = (index, flag) => {
323
398
  const value = argv[index];
@@ -340,20 +415,28 @@ function parseArgv(argv, baseCwd) {
340
415
  flags.force = true;
341
416
  continue;
342
417
  }
418
+ if (arg === "--install") {
419
+ flags.install = true;
420
+ continue;
421
+ }
343
422
  if (arg === "--with-editor") {
344
423
  flags.withEditor = true;
424
+ flags.editorFlagSet = true;
345
425
  continue;
346
426
  }
347
427
  if (arg === "--with-functions") {
348
428
  flags.withFunctions = true;
429
+ flags.functionsFlagSet = true;
349
430
  continue;
350
431
  }
351
432
  if (arg === "--no-editor") {
352
433
  flags.withEditor = false;
434
+ flags.editorFlagSet = true;
353
435
  continue;
354
436
  }
355
437
  if (arg === "--no-functions") {
356
438
  flags.withFunctions = false;
439
+ flags.functionsFlagSet = true;
357
440
  continue;
358
441
  }
359
442
  if (arg === "--skip-build") {