@camox/cli 0.44.0 → 0.46.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/dist/index.mjs CHANGED
@@ -1,14 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { a as getProjectBySlug, i as createProject, n as checkSlugAvailability, o as listOrganizations, r as createOrganization, s as setActiveOrganization, t as callTool } from "./api-COfB7AKI.mjs";
3
- import { a as writeAuthTokenForUrl, i as verifyOneTimeToken, n as readAuthTokenForUrl, r as removeAuthTokenForUrl, t as normalizeUrl } from "./auth-core-4Jdf5q-u.mjs";
2
+ import { c as NOT_PUBLISHED_CODES, i as getProject, l as NO_DEVELOPMENT_DRAFT_ERROR, n as developmentDraftHeaders, o as DEVELOPMENT_DRAFT, r as getDevelopmentDraft, s as DRAFT_ENDED_ERROR_CODE, t as callTool } from "./api-DNIQC1ea.mjs";
3
+ import { t as installLocalhostDns } from "./local-dns-DQIT1ucr.mjs";
4
+ import { a as removeAuthTokenForUrl, i as readAuthTokenForUrl, n as isLoopbackUrl, o as verifyOneTimeToken, r as normalizeUrl, s as writeAuthTokenForUrl, t as createPreviewSignInUrl } from "./auth-core-xLeorUUV.mjs";
4
5
  import { object, or } from "@optique/core/constructs";
5
6
  import { message } from "@optique/core/message";
6
7
  import { defineProgram } from "@optique/core/program";
7
8
  import { runSync } from "@optique/run";
9
+ import { mkdtemp, open, readFile, rm, stat } from "node:fs/promises";
8
10
  import { multiple, optional } from "@optique/core/modifiers";
9
11
  import { command, constant, option } from "@optique/core/primitives";
10
- import { choice, integer, string } from "@optique/core/valueparser";
11
- import { execSync, spawn, spawnSync } from "node:child_process";
12
+ import { choice, string } from "@optique/core/valueparser";
13
+ import { execSync, spawnSync } from "node:child_process";
12
14
  import http from "node:http";
13
15
  import * as p from "@clack/prompts";
14
16
  import { log } from "@clack/prompts";
@@ -16,13 +18,10 @@ import fs, { openAsBlob } from "node:fs";
16
18
  import { tmpdir } from "node:os";
17
19
  import path, { basename, extname, join } from "node:path";
18
20
  import { ZodError, z } from "zod";
19
- import { mkdtemp, open, rm, stat } from "node:fs/promises";
20
- import { fileURLToPath } from "node:url";
21
- import slugify from "slugify";
22
21
  //#region src/lib/auth.ts
23
22
  const CAMOX_URL = process.env.CAMOX_URL || "https://app.camox.dev";
24
23
  const CAMOX_API_URL = process.env.CAMOX_API_URL || "https://api.camox.dev";
25
- const AUTH_TIMEOUT_MS = 300 * 1e3;
24
+ const AUTH_TIMEOUT_MS = 3e5;
26
25
  function readAuthToken() {
27
26
  return readAuthTokenForUrl(CAMOX_URL);
28
27
  }
@@ -107,7 +106,7 @@ async function authenticateUser() {
107
106
  s.stop(`Authenticated as ${authToken.name}`);
108
107
  return authToken;
109
108
  } catch (err) {
110
- s.stop("Authentication failed.");
109
+ s.error("Authentication failed.");
111
110
  throw err;
112
111
  } finally {
113
112
  close();
@@ -119,7 +118,7 @@ async function authenticateUser() {
119
118
  async function getOrAuthenticate() {
120
119
  const stored = readAuthToken();
121
120
  if (stored) {
122
- const { verifySession } = await import("./api-LuNKejJE.mjs");
121
+ const { verifySession } = await import("./api-DeE66CPa.mjs");
123
122
  if (await verifySession(stored.token)) {
124
123
  p.log.info(`Authenticated as ${stored.name}`);
125
124
  return stored;
@@ -158,8 +157,22 @@ function parseJsonFlag(name, raw) {
158
157
  } });
159
158
  }
160
159
  }
160
+ /**
161
+ * Only the first dev-mode sync after login creates the development draft, so the CLI can't
162
+ * (ADR 0010).
163
+ */
164
+ const NO_DEVELOPMENT_DRAFT_CLI_ERROR = {
165
+ code: NO_DEVELOPMENT_DRAFT_ERROR,
166
+ message: "You have no development draft in this project yet. Run your site locally once (e.g. `pnpm dev`) to create your development draft, then retry."
167
+ };
168
+ /** Whether an error, or an error response body, says the caller has no development draft. */
169
+ function isNoDevelopmentDraft(err) {
170
+ if (!err || typeof err !== "object" || !("code" in err)) return false;
171
+ return err.code === NO_DEVELOPMENT_DRAFT_ERROR;
172
+ }
161
173
  function asCliError(err) {
162
174
  if (err && typeof err === "object" && "__cliError" in err) return err.__cliError;
175
+ if (isNoDevelopmentDraft(err)) return NO_DEVELOPMENT_DRAFT_CLI_ERROR;
163
176
  if (err instanceof Error) return {
164
177
  code: "INTERNAL_ERROR",
165
178
  message: err.message
@@ -170,6 +183,15 @@ function asCliError(err) {
170
183
  };
171
184
  }
172
185
  //#endregion
186
+ //#region ../api-contract/src/ids.ts
187
+ /** The id of a content entity: one UUIDv7, kept in Live and every draft (ADR 0004). */
188
+ const uuidv7Schema = /* @__PURE__ */ z.uuidv7();
189
+ /**
190
+ * A Project ID: a UUIDv7 in lowercase only, the form the project's Worker is named by, so an
191
+ * uppercase one fails here rather than silently naming no project.
192
+ */
193
+ const projectIdSchema = /* @__PURE__ */ z.uuidv7().regex(/^[0-9a-f-]+$/, "A Project ID is a UUIDv7 in lowercase");
194
+ //#endregion
173
195
  //#region src/lib/runtime.ts
174
196
  /**
175
197
  * Subset of the camox vite plugin's options that the CLI needs at dispatch
@@ -181,7 +203,8 @@ function asCliError(err) {
181
203
  * `z.infer` so the two never drift.
182
204
  */
183
205
  const runtimeSchema = z.object({
184
- projectSlug: z.string().min(1),
206
+ /** The Project ID the camox() config names: the project commands target by default. */
207
+ projectId: projectIdSchema,
185
208
  apiUrl: z.string().url(),
186
209
  authenticationUrl: z.string().url(),
187
210
  disableTelemetry: z.boolean().optional().default(false)
@@ -230,7 +253,9 @@ function loadRuntime(cwd = process.cwd()) {
230
253
  throw new RuntimeMalformedError(candidate, e instanceof Error ? e.message : String(e));
231
254
  }
232
255
  try {
233
- return runtimeSchema.parse(raw);
256
+ const runtime = runtimeSchema.parse(raw);
257
+ installLocalhostDns(new URL(runtime.apiUrl).hostname);
258
+ return runtime;
234
259
  } catch (e) {
235
260
  if (e instanceof ZodError) throw new RuntimeMalformedError(candidate, e.issues.map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; "));
236
261
  throw e;
@@ -253,17 +278,18 @@ function compact(input) {
253
278
  for (const [k, v] of Object.entries(input)) if (v !== void 0) out[k] = v;
254
279
  return out;
255
280
  }
256
- function fail$1(err, code) {
281
+ function fail$2(err, code) {
257
282
  printError(err);
258
283
  process.exit(code);
259
284
  }
260
- async function resolveProjectId(token, slug, apiUrl) {
285
+ async function resolveProjectId(token, project, apiUrl) {
261
286
  try {
262
- return (await getProjectBySlug(token, slug, apiUrl)).id;
287
+ return (await getProject(token, project, apiUrl)).id;
263
288
  } catch (err) {
264
- return fail$1({
289
+ if (isNoDevelopmentDraft(err)) return fail$2(NO_DEVELOPMENT_DRAFT_CLI_ERROR, 2);
290
+ return fail$2({
265
291
  code: "PROJECT_LOOKUP_FAILED",
266
- message: `Could not load project "${slug}".`,
292
+ message: `Could not load project "${project}".`,
267
293
  details: err instanceof Error ? err.message : String(err)
268
294
  }, 2);
269
295
  }
@@ -272,7 +298,7 @@ async function callRemote(params) {
272
298
  try {
273
299
  return await callTool(params);
274
300
  } catch (err) {
275
- return fail$1(asCliError(err), 1);
301
+ return fail$2(asCliError(err), 1);
276
302
  }
277
303
  }
278
304
  /**
@@ -281,141 +307,173 @@ async function callRemote(params) {
281
307
  * auth/project resolution failure.
282
308
  *
283
309
  * Project, apiUrl, and authenticationUrl come from the vite plugin's
284
- * `node_modules/.camox/runtime.json` sidecar. `--project <slug>` and
285
- * `CAMOX_PROJECT` may override the slug. The environment is *not* read
286
- * from the sidecar — it's derived from auth (`dev:<email>`) by default,
287
- * with `--production` as the only opt-in for prod.
310
+ * `node_modules/.camox/runtime.json` sidecar. `--project` and `CAMOX_PROJECT`
311
+ * may override the project, by its Project ID or its Project Slug. Every request names the caller's
312
+ * development draft (ADR 0010); no command takes a draft, Live or production flag.
288
313
  */
289
314
  async function resolveCommandContext(opts) {
290
315
  let runtime;
291
316
  try {
292
317
  runtime = loadRuntime(opts.cwd);
293
318
  } catch (err) {
294
- if (err instanceof RuntimeDirectoryError) return fail$1({
319
+ if (err instanceof RuntimeDirectoryError) return fail$2({
295
320
  code: "INVALID_CWD",
296
321
  message: err.message
297
322
  }, 2);
298
- if (err instanceof RuntimeNotFoundError || err instanceof RuntimeMalformedError) return fail$1({
323
+ if (err instanceof RuntimeNotFoundError || err instanceof RuntimeMalformedError) return fail$2({
299
324
  code: "RUNTIME_NOT_FOUND",
300
325
  message: err.message
301
326
  }, 2);
302
327
  throw err;
303
328
  }
304
- const slug = opts.projectFlag?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectSlug;
329
+ const project = opts.projectFlag?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectId;
305
330
  const token = readAuthTokenForUrl(runtime.authenticationUrl, opts.cwd);
306
- if (!token) return fail$1({
331
+ if (!token) return fail$2({
307
332
  code: "NOT_AUTHENTICATED",
308
333
  message: `No stored credentials for ${runtime.authenticationUrl}. Run \`camox login\` against this backend first.`
309
334
  }, 2);
310
- if (!opts.production && !token.email) return fail$1({
311
- code: "AUTH_INCOMPLETE",
312
- message: "Auth token is missing an email. Run `camox login` to refresh credentials."
313
- }, 2);
314
- const environmentName = opts.production ? "production" : `dev:${token.email}`;
315
- const projectId = await resolveProjectId(token.token, slug, runtime.apiUrl);
335
+ const projectId = await resolveProjectId(token.token, project, runtime.apiUrl);
316
336
  return {
317
337
  token: token.token,
318
338
  apiUrl: runtime.apiUrl,
319
- environmentName,
320
339
  projectId,
321
340
  disableTelemetry: runtime.disableTelemetry
322
341
  };
323
342
  }
324
- async function dispatch(opts) {
343
+ /**
344
+ * Resolve auth + project and call a tool, returning its result for the command to render.
345
+ * Exits like `dispatch` when the call fails.
346
+ */
347
+ async function callToolResult(opts) {
325
348
  const response = await callRemote({
326
349
  ...await resolveCommandContext(opts),
327
350
  name: opts.toolName,
328
- args: compact(opts.args)
351
+ args: compact({
352
+ ...opts.args,
353
+ locale: opts.locale
354
+ })
329
355
  });
330
356
  if (!response.ok) {
331
357
  printError(response.error);
332
358
  process.exit(1);
333
359
  }
334
- printResult(response.result, opts.outputMode);
360
+ return response.result;
361
+ }
362
+ async function dispatch(opts) {
363
+ printResult(await callToolResult(opts), opts.outputMode);
335
364
  process.exit(0);
336
365
  }
337
366
  //#endregion
338
367
  //#region src/lib/runtime-options.ts
339
368
  const cwdFlag = optional(option("--cwd", string({ metavar: "PATH" }), { description: message`Start runtime lookup in this directory and walk upward. Relative to the shell's current directory; does not change how other file paths resolve.` }));
369
+ /** The project a command targets, overriding the one in the runtime sidecar. */
370
+ const projectFlag = optional(option("--project", string({ metavar: "PROJECT" }), { description: message`Target this project, by its Project ID or its Project Slug. Takes precedence over CAMOX_PROJECT. Defaults to the project of the app found by the runtime lookup.` }));
371
+ /** The locale a content command reads or writes (ADR 0015). */
372
+ const localeFlag = optional(option("--locale", string({ metavar: "TAG" }), { description: message`Read or write content in this locale, by its BCP 47 tag, such as de. Defaults to the default locale. List locales with locales list.` }));
373
+ //#endregion
374
+ //#region src/lib/uuidv7.ts
375
+ /** An entity id option: rejects anything that isn't a UUIDv7 before any request. */
376
+ function uuidv7() {
377
+ return {
378
+ mode: "sync",
379
+ metavar: "ID",
380
+ placeholder: "",
381
+ parse(input) {
382
+ if (uuidv7Schema.safeParse(input).success) return {
383
+ success: true,
384
+ value: input
385
+ };
386
+ return {
387
+ success: false,
388
+ error: message`Expected a UUIDv7, but got ${input}.`
389
+ };
390
+ },
391
+ format: (value) => value
392
+ };
393
+ }
340
394
  //#endregion
341
395
  //#region src/commands/blocks.ts
342
- const projectFlag$3 = optional(option("--project", string({ metavar: "SLUG" })));
343
396
  const jsonFlag$3 = option("--json");
344
- const productionFlag$2 = option("--production");
345
- const liveFlag$1 = option("--live");
346
- const parser$11 = command("blocks", or(command("types", object({
397
+ const fieldFileFlag = multiple(option("--field-file", string({ metavar: "NAME=PATH" }), { description: message`Set the string field NAME to the raw contents of the file at PATH, without JSON escaping. Repeat to set several fields. Merges with --content; when both set a field, the file wins.` }));
398
+ const types = command("types", object({
347
399
  command: constant("blocks.types"),
348
400
  cwd: cwdFlag,
349
- project: projectFlag$3,
350
- production: productionFlag$2,
401
+ project: projectFlag,
351
402
  json: jsonFlag$3
352
- })), command("describe", object({
403
+ }));
404
+ const describe = command("describe", object({
353
405
  command: constant("blocks.describe"),
354
406
  type: multiple(option("--type", string({ metavar: "TYPE" })), { min: 1 }),
355
407
  cwd: cwdFlag,
356
- project: projectFlag$3,
357
- production: productionFlag$2,
408
+ project: projectFlag,
358
409
  json: jsonFlag$3
359
- })), command("get", object({
410
+ }));
411
+ const get$2 = command("get", object({
360
412
  command: constant("blocks.get"),
361
- id: option("--id", integer({ metavar: "ID" })),
362
- live: liveFlag$1,
413
+ id: option("--id", uuidv7()),
363
414
  cwd: cwdFlag,
364
- project: projectFlag$3,
365
- production: productionFlag$2,
415
+ project: projectFlag,
416
+ locale: localeFlag,
366
417
  json: jsonFlag$3
367
- })), command("get-many", object({
418
+ }));
419
+ const getMany = command("get-many", object({
368
420
  command: constant("blocks.get-many"),
369
- id: multiple(option("--id", integer({ metavar: "ID" })), { min: 1 }),
370
- live: liveFlag$1,
421
+ id: multiple(option("--id", uuidv7()), { min: 1 }),
371
422
  cwd: cwdFlag,
372
- project: projectFlag$3,
373
- production: productionFlag$2,
423
+ project: projectFlag,
424
+ locale: localeFlag,
374
425
  json: jsonFlag$3
375
- })), command("create", object({
426
+ }));
427
+ const create$1 = command("create", object({
376
428
  command: constant("blocks.create"),
377
- pageId: option("--page-id", integer({ metavar: "ID" })),
429
+ pageId: option("--page-id", uuidv7()),
378
430
  type: option("--type", string({ metavar: "TYPE" })),
379
- content: option("--content", string({ metavar: "JSON" })),
431
+ content: optional(option("--content", string({ metavar: "JSON" }))),
432
+ fieldFile: fieldFileFlag,
380
433
  settings: optional(option("--settings", string({ metavar: "JSON" }))),
381
434
  position: optional(option("--position", choice(["first", "last"], { metavar: "WHERE" }))),
382
- afterId: optional(option("--after-id", integer({ metavar: "ID" }))),
383
- beforeId: optional(option("--before-id", integer({ metavar: "ID" }))),
435
+ afterId: optional(option("--after-id", uuidv7())),
436
+ beforeId: optional(option("--before-id", uuidv7())),
384
437
  afterPosition: optional(option("--after-position", string({ metavar: "POS" }))),
385
438
  beforePosition: optional(option("--before-position", string({ metavar: "POS" }))),
386
439
  cwd: cwdFlag,
387
- project: projectFlag$3,
388
- production: productionFlag$2,
440
+ project: projectFlag,
441
+ locale: localeFlag,
389
442
  json: jsonFlag$3
390
- })), command("edit", object({
443
+ }));
444
+ const edit = command("edit", object({
391
445
  command: constant("blocks.edit"),
392
- id: option("--id", integer({ metavar: "ID" })),
446
+ id: option("--id", uuidv7()),
393
447
  content: optional(option("--content", string({ metavar: "JSON" }))),
448
+ fieldFile: fieldFileFlag,
394
449
  settings: optional(option("--settings", string({ metavar: "JSON" }))),
395
450
  cwd: cwdFlag,
396
- project: projectFlag$3,
397
- production: productionFlag$2,
451
+ project: projectFlag,
452
+ locale: localeFlag,
398
453
  json: jsonFlag$3
399
- })), command("move", object({
454
+ }));
455
+ const move = command("move", object({
400
456
  command: constant("blocks.move"),
401
- id: option("--id", integer({ metavar: "ID" })),
457
+ id: option("--id", uuidv7()),
402
458
  position: optional(option("--position", choice(["first", "last"], { metavar: "WHERE" }))),
403
- afterId: optional(option("--after-id", integer({ metavar: "ID" }))),
404
- beforeId: optional(option("--before-id", integer({ metavar: "ID" }))),
459
+ afterId: optional(option("--after-id", uuidv7())),
460
+ beforeId: optional(option("--before-id", uuidv7())),
405
461
  afterPosition: optional(option("--after-position", string({ metavar: "POS" }))),
406
462
  beforePosition: optional(option("--before-position", string({ metavar: "POS" }))),
407
463
  cwd: cwdFlag,
408
- project: projectFlag$3,
409
- production: productionFlag$2,
464
+ project: projectFlag,
465
+ locale: localeFlag,
410
466
  json: jsonFlag$3
411
- })), command("delete", object({
467
+ }));
468
+ const del$1 = command("delete", object({
412
469
  command: constant("blocks.delete"),
413
- id: option("--id", integer({ metavar: "ID" })),
470
+ id: option("--id", uuidv7()),
414
471
  cwd: cwdFlag,
415
- project: projectFlag$3,
416
- production: productionFlag$2,
472
+ project: projectFlag,
473
+ locale: localeFlag,
417
474
  json: jsonFlag$3
418
- }))));
475
+ }));
476
+ const parser$12 = command("blocks", or(types, describe, get$2, getMany, create$1, edit, move, del$1));
419
477
  const POSITIONING_FLAGS = [
420
478
  ["position", "--position"],
421
479
  ["afterId", "--after-id"],
@@ -442,11 +500,40 @@ function collectPositioningArgs(args) {
442
500
  toolArgs
443
501
  };
444
502
  }
445
- async function handler$11(args) {
503
+ function fail$1(code, message) {
504
+ throw Object.assign(new Error(message), { __cliError: {
505
+ code,
506
+ message
507
+ } });
508
+ }
509
+ /**
510
+ * Build block content from `--content` and repeated `--field-file NAME=PATH`
511
+ * flags. Every file is read before anything is sent, so a bad path fails the
512
+ * whole command without a partial write. Files win over `--content` on conflicts.
513
+ */
514
+ async function resolveContent(rawContent, fieldFiles) {
515
+ const content = rawContent !== void 0 ? parseJsonFlag("--content", rawContent) : void 0;
516
+ if (fieldFiles.length === 0) return content;
517
+ if (content !== void 0 && (typeof content !== "object" || content === null || Array.isArray(content))) fail$1("INVALID_ARGS", "--content must be a JSON object when combined with --field-file.");
518
+ const merged = { ...content };
519
+ for (const entry of fieldFiles) {
520
+ const separator = entry.indexOf("=");
521
+ const name = entry.slice(0, separator);
522
+ const path = entry.slice(separator + 1);
523
+ if (separator <= 0 || !path) fail$1("INVALID_ARGS", `Invalid --field-file "${entry}". Use --field-file NAME=PATH.`);
524
+ try {
525
+ merged[name] = await readFile(path, "utf8");
526
+ } catch (err) {
527
+ fail$1("FILE_READ_ERROR", `Cannot read --field-file ${name}=${path} (${err instanceof Error && "code" in err ? String(err.code) : String(err)}).`);
528
+ }
529
+ }
530
+ return merged;
531
+ }
532
+ async function handler$12(args) {
446
533
  const outputMode = args.json ? "json" : "auto";
447
534
  const cwd = args.cwd;
448
535
  const projectFlag = args.project;
449
- const production = args.production;
536
+ const locale = args.locale;
450
537
  try {
451
538
  switch (args.command) {
452
539
  case "blocks.types": return dispatch({
@@ -454,7 +541,7 @@ async function handler$11(args) {
454
541
  args: {},
455
542
  cwd,
456
543
  projectFlag,
457
- production,
544
+ locale,
458
545
  outputMode
459
546
  });
460
547
  case "blocks.describe": return dispatch({
@@ -462,33 +549,34 @@ async function handler$11(args) {
462
549
  args: { types: [...args.type] },
463
550
  cwd,
464
551
  projectFlag,
465
- production,
552
+ locale,
466
553
  outputMode
467
554
  });
468
555
  case "blocks.get": return dispatch({
469
556
  toolName: "getBlock",
470
- args: {
471
- id: args.id,
472
- source: args.live ? "live" : "draft"
473
- },
557
+ args: { id: args.id },
474
558
  cwd,
475
559
  projectFlag,
476
- production,
560
+ locale,
477
561
  outputMode
478
562
  });
479
563
  case "blocks.get-many": return dispatch({
480
564
  toolName: "getBlocks",
481
- args: {
482
- ids: [...args.id],
483
- source: args.live ? "live" : "draft"
484
- },
565
+ args: { ids: [...args.id] },
485
566
  cwd,
486
567
  projectFlag,
487
- production,
568
+ locale,
488
569
  outputMode
489
570
  });
490
571
  case "blocks.create": {
491
- const content = parseJsonFlag("--content", args.content);
572
+ if (args.content === void 0 && args.fieldFile.length === 0) {
573
+ printError({
574
+ code: "INVALID_ARGS",
575
+ message: "Pass --content, --field-file, or both."
576
+ });
577
+ process.exit(2);
578
+ }
579
+ const content = await resolveContent(args.content, args.fieldFile);
492
580
  const settings = args.settings !== void 0 ? parseJsonFlag("--settings", args.settings) : void 0;
493
581
  const { used, toolArgs } = collectPositioningArgs(args);
494
582
  if (used.length > 1) {
@@ -509,12 +597,12 @@ async function handler$11(args) {
509
597
  },
510
598
  cwd,
511
599
  projectFlag,
512
- production,
600
+ locale,
513
601
  outputMode
514
602
  });
515
603
  }
516
604
  case "blocks.edit": {
517
- const content = args.content !== void 0 ? parseJsonFlag("--content", args.content) : void 0;
605
+ const content = await resolveContent(args.content, args.fieldFile);
518
606
  const settings = args.settings !== void 0 ? parseJsonFlag("--settings", args.settings) : void 0;
519
607
  return dispatch({
520
608
  toolName: "editBlock",
@@ -525,7 +613,7 @@ async function handler$11(args) {
525
613
  },
526
614
  cwd,
527
615
  projectFlag,
528
- production,
616
+ locale,
529
617
  outputMode
530
618
  });
531
619
  }
@@ -553,7 +641,7 @@ async function handler$11(args) {
553
641
  },
554
642
  cwd,
555
643
  projectFlag,
556
- production,
644
+ locale,
557
645
  outputMode
558
646
  });
559
647
  }
@@ -562,7 +650,7 @@ async function handler$11(args) {
562
650
  args: { id: args.id },
563
651
  cwd,
564
652
  projectFlag,
565
- production,
653
+ locale,
566
654
  outputMode
567
655
  });
568
656
  }
@@ -572,96 +660,46 @@ async function handler$11(args) {
572
660
  }
573
661
  }
574
662
  //#endregion
575
- //#region src/commands/comments.ts
576
- const common$1 = {
663
+ //#region src/commands/discard.ts
664
+ const parser$11 = command("discard", object({
665
+ command: constant("discard"),
666
+ yes: option("--yes", "-y", { description: message`Discard without asking for confirmation.` }),
577
667
  cwd: cwdFlag,
578
- project: optional(option("--project", string({ metavar: "SLUG" }))),
579
- production: option("--production"),
668
+ project: projectFlag,
580
669
  json: option("--json")
581
- };
582
- const parser$10 = command("comments", or(command("list", object({
583
- command: constant("comments.list"),
584
- pageId: option("--page-id", integer({
585
- min: 1,
586
- metavar: "ID"
587
- })),
588
- ...common$1
589
- }), { description: message`List all comments (including resolved ones) for a page.` }), command("resolve", object({
590
- command: constant("comments.resolve"),
591
- pageId: option("--page-id", integer({
592
- min: 1,
593
- metavar: "ID"
594
- })),
595
- id: option("--id", string({ metavar: "UUID" })),
596
- ...common$1
597
- }), { description: message`Resolve a comment by its UUID and page ID. Use comments list to find both.` })), { description: message`Read and resolve page comments in the selected environment. Defaults to development; use --production to target production.` });
598
- async function handler$10(args) {
599
- return dispatch({
600
- toolName: args.command === "comments.list" ? "listComments" : "resolveComment",
601
- args: {
602
- pageId: args.pageId,
603
- ...args.command === "comments.resolve" ? { id: args.id } : {}
604
- },
605
- cwd: args.cwd,
606
- projectFlag: args.project,
607
- production: args.production,
608
- outputMode: args.json ? "json" : "auto"
609
- });
670
+ }), { description: message`Discard every change in your development draft. Live is untouched.` });
671
+ const WARNING = "This discards every change in your development draft and can't be undone.";
672
+ async function confirmed() {
673
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
674
+ printError({
675
+ code: "CONFIRMATION_REQUIRED",
676
+ message: `${WARNING} Re-run with --yes to confirm.`
677
+ });
678
+ process.exit(2);
679
+ }
680
+ return await p.confirm({
681
+ message: `${WARNING} Discard it?`,
682
+ initialValue: false
683
+ }) === true;
610
684
  }
611
- //#endregion
612
- //#region src/commands/env.ts
613
- const projectFlag$2 = optional(option("--project", string({ metavar: "SLUG" })));
614
- const jsonFlag$2 = option("--json");
615
- const yesFlag = option("--yes", "-y");
616
- const parser$9 = command("env", or(command("check", object({
617
- command: constant("env.check"),
618
- cwd: cwdFlag,
619
- project: projectFlag$2,
620
- json: jsonFlag$2
621
- })), command("push", object({
622
- command: constant("env.push"),
623
- yes: yesFlag,
624
- cwd: cwdFlag,
625
- project: projectFlag$2,
626
- json: jsonFlag$2
627
- })), command("pull", object({
628
- command: constant("env.pull"),
629
- yes: yesFlag,
630
- cwd: cwdFlag,
631
- project: projectFlag$2,
632
- json: jsonFlag$2
633
- }))));
634
- async function handler$9(args) {
685
+ async function handler$11(args) {
635
686
  const outputMode = args.json ? "json" : "auto";
636
- const cwd = args.cwd;
637
- const projectFlag = args.project;
638
- switch (args.command) {
639
- case "env.check": return dispatch({
640
- toolName: "checkEnvironmentCompatibility",
641
- args: {},
642
- cwd,
643
- projectFlag,
644
- outputMode
687
+ if (!args.yes && !await confirmed()) {
688
+ printError({
689
+ code: "CANCELLED",
690
+ message: "Nothing was discarded."
645
691
  });
646
- case "env.push":
647
- case "env.pull": {
648
- const isPush = args.command === "env.push";
649
- if (!args.yes) {
650
- printError({
651
- code: "CONFIRMATION_REQUIRED",
652
- message: `This will replace all content in ${isPush ? "production" : "your dev environment"} with ${isPush ? "your dev environment" : "production"}. This action cannot be undone. Re-run with --yes to confirm.`
653
- });
654
- process.exit(2);
655
- }
656
- return dispatch({
657
- toolName: "replicateEnvironment",
658
- args: { direction: isPush ? "push" : "pull" },
659
- cwd,
660
- projectFlag,
661
- outputMode
662
- });
663
- }
692
+ process.exit(1);
664
693
  }
694
+ const result = await callToolResult({
695
+ toolName: "discardDraft",
696
+ args: {},
697
+ cwd: args.cwd,
698
+ projectFlag: args.project
699
+ });
700
+ if (isJsonMode(outputMode)) printResult(result, outputMode);
701
+ else process.stdout.write("Discarded every change in your development draft.\n");
702
+ process.exit(0);
665
703
  }
666
704
  const TIMEOUT_MS = 12e4;
667
705
  const MAX_REDIRECTS = 5;
@@ -791,13 +829,8 @@ async function uploadFile(context, options, targetId) {
791
829
  body.set("file", await openAsBlob(path, { type }), options.filename ?? filename);
792
830
  body.set("projectId", String(context.projectId));
793
831
  if (options.alt !== void 0) body.set("alt", options.alt);
794
- if (options.aiMetadata !== void 0 || options.alt !== void 0) body.set("aiMetadataEnabled", String(options.alt === void 0 && options.aiMetadata === "on"));
795
- const headers = {
796
- Authorization: `Bearer ${context.token}`,
797
- "x-camox-client": "cli",
798
- "x-environment-name": context.environmentName
799
- };
800
- if (context.disableTelemetry) headers["x-camox-telemetry-disabled"] = "1";
832
+ if (options.aiMetadata !== void 0 || options.alt !== void 0) body.set("aiMetadata", String(options.alt === void 0 && options.aiMetadata === "on"));
833
+ const headers = developmentDraftHeaders(context.token, context.disableTelemetry);
801
834
  let response;
802
835
  try {
803
836
  const endpoint = targetId === void 0 ? "/files/upload" : `/files/${targetId}/content`;
@@ -811,7 +844,10 @@ async function uploadFile(context, options, targetId) {
811
844
  } catch {
812
845
  return fail("UPLOAD_FAILED", targetId === void 0 ? "Media upload failed (network or timeout). Check files list before retrying." : `File replacement failed (network or timeout). Check files get --id ${targetId} before retrying.`);
813
846
  }
814
- if (!response.ok) fail("UPLOAD_FAILED", `${targetId === void 0 ? "Media upload" : "File replacement"} failed (HTTP ${response.status}).`);
847
+ if (!response.ok) {
848
+ if (isNoDevelopmentDraft(await response.json().catch(() => null))) throw Object.assign(new Error(NO_DEVELOPMENT_DRAFT_CLI_ERROR.message), { __cliError: NO_DEVELOPMENT_DRAFT_CLI_ERROR });
849
+ fail("UPLOAD_FAILED", `${targetId === void 0 ? "Media upload" : "File replacement"} failed (HTTP ${response.status}).`);
850
+ }
815
851
  return await response.json();
816
852
  } finally {
817
853
  if (temporaryDirectory) await rm(temporaryDirectory, {
@@ -824,49 +860,41 @@ async function uploadFile(context, options, targetId) {
824
860
  //#region src/commands/files.ts
825
861
  const common = {
826
862
  cwd: cwdFlag,
827
- project: optional(option("--project", string({ metavar: "SLUG" }))),
828
- production: option("--production"),
863
+ project: projectFlag,
829
864
  json: option("--json")
830
865
  };
831
866
  const metadata = {
832
- alt: optional(option("--alt", string({ metavar: "TEXT" }), { description: message`Alt text (including an empty string). Disables automatic metadata.` })),
833
- aiMetadata: optional(option("--ai-metadata", choice(["on", "off"]), { description: message`Enable or disable automatic metadata. Cannot enable alongside --alt. Enabling schedules generation for raster images.` }))
867
+ alt: optional(option("--alt", string({ metavar: "TEXT" }), { description: message`Alt text (including an empty string). Alt text and filename are one group: writing either makes both manual, keeping the current value of the other.` })),
868
+ aiMetadata: optional(option("--ai-metadata", choice(["on", "off"]), { description: message`on makes alt text and filename generated again and schedules asynchronous generation for raster images; it cannot be combined with --alt, nor with --filename on a metadata-only update. off keeps the current values as manual.` }))
834
869
  };
835
- const parser$8 = command("files", or(command("upload", object({
870
+ const parser$10 = command("files", or(command("upload", object({
836
871
  command: constant("files.upload"),
837
872
  file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Local file to upload. Pass exactly one of --file or --url.` })),
838
873
  url: optional(option("--url", string({ metavar: "URL" }), { description: message`HTTP(S) URL to download and copy into Camox, not an external reference.` })),
839
874
  filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Override the local basename or final URL path filename.` })),
840
875
  ...metadata,
841
876
  ...common
842
- }), { description: message`Upload media (maximum 100 MiB). Example: camox files upload --url https://example.com/hero.webp --alt "Product screenshot". Returns the stored file ID and URL. Without metadata options, raster images use automatic metadata.` }), command("update", object({
877
+ }), { description: message`Upload a file (maximum 100 MiB). Example: camox files upload --url https://example.com/hero.webp --alt "Product screenshot". Returns the stored file ID and URL. Without metadata options, raster images use automatic metadata.` }), command("update", object({
843
878
  command: constant("files.update"),
844
- id: option("--id", integer({
845
- min: 1,
846
- metavar: "ID"
847
- })),
879
+ id: option("--id", uuidv7()),
848
880
  file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Replace file content from a local path, keeping its ID and references. Mutually exclusive with --url.` })),
849
881
  url: optional(option("--url", string({ metavar: "URL" }), { description: message`Download and replace file content from an HTTP(S) URL, keeping its ID and references. Mutually exclusive with --file.` })),
850
- filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Set the filename. Metadata-only renames preserve the URL; replacement uses the source filename unless overridden. Disable AI to prevent future automatic renaming.` })),
882
+ filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Set the filename. Metadata-only renames preserve the URL and make alt text and filename manual; replacement uses the source filename unless overridden.` })),
851
883
  ...metadata,
852
884
  ...common
853
- }), { description: message`Update metadata or replace file content with --file or --url (maximum 100 MiB). Replacing affects every reference to this file in the selected environment; its ID stays unchanged but its URL changes. Omitted alt text and AI settings are preserved. Example: camox files update --id 123 --file ./hero.jpg --alt "Updated description". Use --alt "" for intentionally empty alt text.` }), command("get", object({
885
+ }), { description: message`Update metadata or replace file content with --file or --url (maximum 100 MiB). Replacing affects every reference to this file in your development draft; its ID stays unchanged but its URL changes. Omitted alt text and AI settings are preserved. Example: camox files update --id <FILE_ID> --file ./hero.jpg --alt "Updated description". Use --alt "" for intentionally empty alt text.` }), command("get", object({
854
886
  command: constant("files.get"),
855
- id: option("--id", integer({
856
- min: 1,
857
- metavar: "ID"
858
- })),
887
+ id: option("--id", uuidv7()),
859
888
  ...common
860
- }), { description: message`Inspect a file in the selected project and environment.` }), command("list", object({
889
+ }), { description: message`Inspect a file in the selected project.` }), command("list", object({
861
890
  command: constant("files.list"),
862
891
  ...common
863
- }), { description: message`List files in the selected project and environment.` })), { description: message`Upload and manage media. Defaults to your development environment; --production explicitly targets production.` });
864
- async function handler$8(args) {
892
+ }), { description: message`List files in the selected project.` })), { description: message`Upload and manage files in your development draft.` });
893
+ async function handler$10(args) {
865
894
  const outputMode = args.json ? "json" : "auto";
866
895
  const options = {
867
896
  cwd: args.cwd,
868
897
  projectFlag: args.project,
869
- production: args.production,
870
898
  outputMode
871
899
  };
872
900
  try {
@@ -892,6 +920,13 @@ async function handler$8(args) {
892
920
  }
893
921
  validateMetadata(args);
894
922
  validateFilename(args.filename);
923
+ if (args.filename !== void 0 && args.aiMetadata === "on") {
924
+ printError({
925
+ code: "INVALID_ARGS",
926
+ message: "--filename cannot be combined with --ai-metadata on."
927
+ });
928
+ process.exit(2);
929
+ }
895
930
  if (args.alt === void 0 && args.aiMetadata === void 0 && args.filename === void 0) {
896
931
  printError({
897
932
  code: "INVALID_ARGS",
@@ -906,7 +941,7 @@ async function handler$8(args) {
906
941
  id: args.id,
907
942
  alt: args.alt,
908
943
  filename: args.filename,
909
- aiMetadataEnabled: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
944
+ aiMetadata: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
910
945
  }
911
946
  });
912
947
  } catch (err) {
@@ -916,386 +951,115 @@ async function handler$8(args) {
916
951
  }
917
952
  }
918
953
  //#endregion
919
- //#region src/lib/agent-skills.ts
920
- function createAgentSkillLinks(targetDir) {
921
- const agentsSkillsDir = path.join(targetDir, ".agents", "skills");
922
- const claudeSkillsDir = path.join(targetDir, ".claude", "skills");
923
- fs.mkdirSync(agentsSkillsDir, { recursive: true });
924
- fs.mkdirSync(claudeSkillsDir, { recursive: true });
925
- for (const skill of ["camox", "camoxify"]) {
926
- fs.symlinkSync(`../../node_modules/camox/skills/${skill}`, path.join(agentsSkillsDir, skill));
927
- fs.symlinkSync(`../../.agents/skills/${skill}`, path.join(claudeSkillsDir, skill));
928
- }
929
- }
930
- //#endregion
931
- //#region src/lib/utils.ts
932
- const packageManagerVersions = {
933
- pnpm: "pnpm@11.1.3",
934
- bun: "bun@1.2.23",
935
- npm: "npm@11.6.2"
936
- };
937
- const pmCommands = {
938
- pnpm: {
939
- install: {
940
- bin: "corepack",
941
- args: ["pnpm", "install"],
942
- display: "pnpm install",
943
- fallback: {
944
- bin: "pnpm",
945
- args: ["install"]
946
- }
947
- },
948
- dev: {
949
- bin: "corepack",
950
- args: ["pnpm", "dev"],
951
- display: "pnpm dev",
952
- fallback: {
953
- bin: "pnpm",
954
- args: ["dev"]
955
- }
956
- }
957
- },
958
- bun: {
959
- install: {
960
- bin: "bun",
961
- args: ["install"],
962
- display: "bun install"
963
- },
964
- dev: {
965
- bin: "bun",
966
- args: ["dev"],
967
- display: "bun dev"
968
- }
969
- },
970
- npm: {
971
- install: {
972
- bin: "npm",
973
- args: ["install"],
974
- display: "npm install"
975
- },
976
- dev: {
977
- bin: "npm",
978
- args: ["run", "dev"],
979
- display: "npm run dev"
980
- }
981
- }
982
- };
983
- function copyDir(src, dest, replacements) {
984
- fs.mkdirSync(dest, { recursive: true });
985
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
986
- const srcPath = path.join(src, entry.name);
987
- const destPath = path.join(dest, entry.name);
988
- if (fs.statSync(srcPath).isDirectory()) {
989
- copyDir(srcPath, destPath, replacements);
990
- continue;
991
- }
992
- let content = fs.readFileSync(srcPath, "utf-8");
993
- for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(key, value);
994
- fs.writeFileSync(destPath, content);
995
- }
996
- }
997
- //#endregion
998
954
  //#region src/commands/init.ts
999
- const parser$7 = command("init", object({ command: constant("init") }));
1000
- const handler$7 = init;
1001
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
1002
- const ownPkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8"));
1003
- const PNPM_WORKSPACE = `allowBuilds:
1004
- core-js: true
1005
- msw: true
1006
- protobufjs: true
1007
- `;
1008
- var CommandError = class extends Error {
1009
- constructor(message, cause) {
1010
- super(message);
1011
- this.cause = cause;
1012
- }
1013
- };
1014
- function onCancel() {
1015
- p.cancel("Cancelled.");
1016
- process.exit(0);
1017
- }
1018
- function runCommand(bin, args, cwd) {
1019
- return new Promise((resolve, reject) => {
1020
- const child = spawn(bin, args, {
1021
- cwd,
1022
- stdio: "ignore"
1023
- });
1024
- child.on("close", (code) => {
1025
- if (code === 0) resolve();
1026
- else reject(/* @__PURE__ */ new Error(`Exit code ${code}`));
1027
- });
1028
- child.on("error", (err) => reject(new CommandError(`Failed to start ${bin}`, err)));
1029
- });
1030
- }
1031
- function spawnCommand(bin, args, cwd) {
1032
- return spawn(bin, args, {
1033
- cwd,
1034
- stdio: "inherit",
1035
- detached: true
1036
- });
1037
- }
1038
- function isMissingCommandError(err) {
1039
- if (!(err instanceof CommandError)) return false;
1040
- const cause = err.cause;
1041
- if (!(cause instanceof Error) || "code" in cause === false) return false;
1042
- return cause.code === "ENOENT";
1043
- }
1044
- async function runPackageManagerCommand(command, cwd) {
1045
- try {
1046
- await runCommand(command.bin, command.args, cwd);
1047
- } catch (err) {
1048
- if (!isMissingCommandError(err) || !command.fallback) throw err;
1049
- await runCommand(command.fallback.bin, command.fallback.args, cwd);
1050
- }
1051
- }
1052
- function startPackageManagerCommand(command, cwd) {
1053
- return new Promise((resolve, reject) => {
1054
- const child = spawnCommand(command.bin, command.args, cwd);
1055
- child.once("spawn", () => resolve(child));
1056
- child.once("error", (err) => {
1057
- const commandError = new CommandError(`Failed to start ${command.bin}`, err);
1058
- if (!isMissingCommandError(commandError) || !command.fallback) {
1059
- reject(commandError);
1060
- return;
1061
- }
1062
- const fallback = spawnCommand(command.fallback.bin, command.fallback.args, cwd);
1063
- fallback.once("spawn", () => resolve(fallback));
1064
- fallback.once("error", (fallbackErr) => reject(new CommandError(`Failed to start ${command.fallback?.bin}`, fallbackErr)));
1065
- });
1066
- });
1067
- }
1068
- function getCommandFailureMessage(pm, err) {
1069
- if (!isMissingCommandError(err)) return "Failed to install dependencies.";
1070
- if (pm === "pnpm") return "Corepack or pnpm is required to install dependencies with the selected package manager. Enable Corepack with `corepack enable` or install pnpm, then run the setup commands below.";
1071
- if (pm === "bun") return "Bun is required to install dependencies with the selected package manager. Install Bun, then run the setup commands below.";
1072
- return "npm is required to install dependencies. Install npm, then run the setup commands below.";
1073
- }
1074
- const CREATE_NEW_ORG = "__create_new__";
1075
- async function selectOrCreateOrganization(token) {
1076
- const orgs = await listOrganizations(token);
1077
- if (orgs.length === 0) {
1078
- p.log.info("You don't have any organizations yet. Let's create one.");
1079
- return promptCreateOrganization(token);
1080
- }
1081
- const selected = await p.select({
1082
- message: "Select an organization for your new project",
1083
- options: [...orgs.map((org) => ({
1084
- value: org.id,
1085
- label: `${org.name} (${org.slug})`
1086
- })), {
1087
- value: CREATE_NEW_ORG,
1088
- label: "Create a new organization"
1089
- }]
1090
- });
1091
- if (p.isCancel(selected)) return onCancel();
1092
- if (selected === CREATE_NEW_ORG) return promptCreateOrganization(token);
1093
- const org = orgs.find((o) => o.id === selected);
1094
- await setActiveOrganization(token, org.id);
1095
- return org.id;
1096
- }
1097
- async function promptCreateOrganization(token) {
1098
- const orgName = await p.text({
1099
- message: "Organization name",
1100
- placeholder: "My Company",
1101
- validate: (value) => {
1102
- if (!value.trim()) return "Organization name is required";
1103
- }
1104
- });
1105
- if (p.isCancel(orgName)) return onCancel();
1106
- const org = await createOrganization(token, orgName, slugify(orgName, {
1107
- lower: true,
1108
- strict: true
1109
- }));
1110
- p.log.success(`Created organization: ${org.name}`);
1111
- return org.id;
1112
- }
1113
- async function init() {
1114
- p.intro(`Camox v${ownPkg.version}`);
1115
- const stored = readAuthToken();
1116
- if (stored) p.log.info(`Welcome back, ${stored.name}!`);
1117
- p.log.info("Let's create your Camox application.");
1118
- const name = await p.text({
1119
- message: "Project display name",
1120
- placeholder: "My Website",
1121
- validate: (value) => {
1122
- if (!value.trim()) return "Project name is required";
1123
- }
1124
- });
1125
- if (p.isCancel(name)) return onCancel();
1126
- const auth = await getOrAuthenticate();
1127
- const orgId = await selectOrCreateOrganization(auth.token);
1128
- let projectSlug;
1129
- while (true) {
1130
- const slugInput = await p.text({
1131
- message: "Project slug",
1132
- initialValue: slugify(name, {
1133
- lower: true,
1134
- strict: true
1135
- }) || "my-site",
1136
- validate: (value) => {
1137
- if (!value.trim()) return "Slug is required";
1138
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return "Slug must be lowercase alphanumeric with hyphens";
1139
- }
1140
- });
1141
- if (p.isCancel(slugInput)) return onCancel();
1142
- const s = p.spinner();
1143
- s.start("Checking slug availability...");
1144
- const { available } = await checkSlugAvailability(auth.token, slugInput);
1145
- if (available) {
1146
- s.stop("Slug is available!");
1147
- projectSlug = slugInput;
1148
- break;
1149
- }
1150
- s.stop(`Slug "${slugInput}" is already taken. Please choose another.`);
1151
- }
1152
- const projectPath = await p.text({
1153
- message: "Project path",
1154
- initialValue: `./${projectSlug}`,
1155
- validate: (value) => {
1156
- if (!value.trim()) return "Path is required";
1157
- const resolved = path.resolve(value);
1158
- if (fs.existsSync(resolved) && fs.readdirSync(resolved).length > 0) return "Directory is not empty";
1159
- }
1160
- });
1161
- if (p.isCancel(projectPath)) return onCancel();
1162
- const resolvedPath = projectPath;
1163
- const targetDir = path.resolve(resolvedPath);
1164
- const s0 = p.spinner();
1165
- s0.start("Creating project...");
1166
- let project;
1167
- try {
1168
- project = await createProject(auth.token, name, projectSlug, orgId);
1169
- s0.stop(`Project created with slug: ${project.slug}`);
1170
- } catch (err) {
1171
- s0.stop("Failed to create project.");
1172
- p.log.error(err instanceof Error ? err.message : "Unknown error");
1173
- process.exit(1);
1174
- }
1175
- const selected = await p.select({
1176
- message: "Which package manager?",
1177
- options: [
1178
- {
1179
- value: "pnpm",
1180
- label: "pnpm (recommended)"
1181
- },
1182
- {
1183
- value: "bun",
1184
- label: "bun"
1185
- },
1186
- {
1187
- value: "npm",
1188
- label: "npm"
1189
- }
1190
- ]
1191
- });
1192
- if (p.isCancel(selected)) return onCancel();
1193
- const pm = selected;
1194
- const s = p.spinner();
1195
- s.start("Scaffolding project...");
1196
- copyDir(path.resolve(__dirname, "..", "templates", "default"), targetDir, { "{{projectName}}": name });
1197
- createAgentSkillLinks(targetDir);
1198
- const viteConfigPath = path.join(targetDir, "vite.config.ts");
1199
- let viteConfig = fs.readFileSync(viteConfigPath, "utf-8");
1200
- viteConfig = viteConfig.replace(/"[^"]*"(,?)[ \t]*\/\/[ \t]*camox-cli:replace-slug.*$/gm, `"${project.slug}"$1`);
1201
- viteConfig = viteConfig.replace(/^[ \t]*\/\/[ \t]*camox-cli:dev-only-start[ \t]*\r?\n[\s\S]*?^[ \t]*\/\/[ \t]*camox-cli:dev-only-end[ \t]*\r?\n/gm, "");
1202
- fs.writeFileSync(viteConfigPath, viteConfig);
1203
- const pkgPath = path.join(targetDir, "package.json");
1204
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1205
- pkg.name = project.slug;
1206
- delete pkg.version;
1207
- pkg.dependencies.camox = `^${ownPkg.version}`;
1208
- pkg.packageManager = packageManagerVersions[pm];
1209
- fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
1210
- if (pm === "pnpm") fs.writeFileSync(path.join(targetDir, "pnpm-workspace.yaml"), PNPM_WORKSPACE);
1211
- fs.writeFileSync(path.join(targetDir, ".gitignore"), `node_modules
1212
- .DS_Store
1213
- dist
1214
- dist-ssr
1215
- *.local
1216
- count.txt
1217
- .env
1218
- .nitro
1219
- .tanstack
1220
- .output
1221
- .vinxi
1222
- node_modules
1223
-
1224
- # Auto generated by Camox
1225
- src/camox/app.ts
1226
- src/routes/(camox)/
1227
-
1228
- # Auto generated by Tanstack Router
1229
- src/routeTree.gen.ts
1230
- `);
1231
- s.stop("Project scaffolded!");
1232
- const { install: installCmd, dev: devCmd } = pmCommands[pm];
1233
- const s2 = p.spinner();
1234
- s2.start(`Running ${installCmd.display}...`);
1235
- try {
1236
- await runPackageManagerCommand(installCmd, targetDir);
1237
- s2.stop("Dependencies installed!");
1238
- } catch (err) {
1239
- s2.stop("Install failed.");
1240
- p.log.error(getCommandFailureMessage(pm, err));
1241
- p.outro(`To finish setup:\n cd ${resolvedPath}\n ${installCmd.display}\n ${devCmd.display}`);
1242
- process.exit(1);
1243
- }
1244
- p.log.info(`Starting dev server... (Ctrl+C to stop)`);
1245
- let child;
1246
- try {
1247
- child = await startPackageManagerCommand(devCmd, targetDir);
1248
- } catch (err) {
1249
- p.log.error(getCommandFailureMessage(pm, err));
1250
- p.outro(`To start the dev server:\n cd ${resolvedPath}\n ${devCmd.display}`);
1251
- process.exit(1);
1252
- }
1253
- const sigintHandler = () => {
1254
- if (child.pid) try {
1255
- process.kill(-child.pid, "SIGTERM");
1256
- } catch {}
1257
- };
1258
- process.on("SIGINT", sigintHandler);
1259
- process.on("exit", sigintHandler);
1260
- child.on("close", () => {
1261
- process.removeListener("SIGINT", sigintHandler);
1262
- process.removeListener("exit", sigintHandler);
1263
- p.outro(`To restart the dev server:\n cd ${resolvedPath}\n ${devCmd.display}`);
1264
- process.exit(0);
1265
- });
955
+ const parser$9 = command("init", object({ command: constant("init") }));
956
+ const handler$9 = init;
957
+ const DASHBOARD_URL = process.env.CAMOX_URL || "https://app.camox.dev";
958
+ /**
959
+ * Projects are created from the dashboard, which hosts them, so `camox init` (and
960
+ * `npm create camox`, which runs it) only says where to go. It exits non-zero, since it created
961
+ * nothing.
962
+ */
963
+ function init() {
964
+ p.intro("camox init");
965
+ p.log.info(`Projects are now created from the Camox dashboard: ${DASHBOARD_URL}`);
966
+ p.outro("Nothing was created.");
967
+ process.exit(1);
1266
968
  }
1267
969
  //#endregion
1268
970
  //#region src/commands/layouts.ts
1269
- const projectFlag$1 = optional(option("--project", string({ metavar: "SLUG" })));
1270
- const jsonFlag$1 = option("--json");
1271
- const productionFlag$1 = option("--production");
1272
- const parser$6 = command("layouts", or(command("list", object({
971
+ const jsonFlag$2 = option("--json");
972
+ const list$2 = command("list", object({
1273
973
  command: constant("layouts.list"),
1274
974
  cwd: cwdFlag,
1275
- project: projectFlag$1,
1276
- production: productionFlag$1,
1277
- json: jsonFlag$1
1278
- }))));
1279
- async function handler$6(args) {
975
+ project: projectFlag,
976
+ json: jsonFlag$2
977
+ }), { description: message`List layout metadata. Use layouts get --layout-id NAME with a returned layoutId to discover its blocks.` });
978
+ const get$1 = command("get", object({
979
+ command: constant("layouts.get"),
980
+ layoutId: option("--layout-id", string({ metavar: "NAME" })),
981
+ cwd: cwdFlag,
982
+ project: projectFlag,
983
+ locale: localeFlag,
984
+ json: jsonFlag$2
985
+ }), { description: message`Inspect a layout and its blocks, including shared navigation and footer IDs for blocks get and blocks edit. A layout is named by the layoutId its code gives it, as listed by layouts list.` });
986
+ const parser$8 = command("layouts", or(list$2, get$1));
987
+ async function handler$8(args) {
1280
988
  const outputMode = args.json ? "json" : "auto";
1281
989
  const cwd = args.cwd;
1282
990
  const projectFlag = args.project;
1283
- const production = args.production;
991
+ const locale = args.locale;
1284
992
  switch (args.command) {
1285
993
  case "layouts.list": return dispatch({
1286
994
  toolName: "listLayouts",
1287
995
  args: {},
1288
996
  cwd,
1289
997
  projectFlag,
1290
- production,
998
+ locale,
999
+ outputMode
1000
+ });
1001
+ case "layouts.get": return dispatch({
1002
+ toolName: "getLayout",
1003
+ args: { layoutId: args.layoutId },
1004
+ cwd,
1005
+ projectFlag,
1006
+ locale,
1291
1007
  outputMode
1292
1008
  });
1293
1009
  }
1294
1010
  }
1295
1011
  //#endregion
1012
+ //#region src/commands/locales.ts
1013
+ const jsonFlag$1 = option("--json");
1014
+ const tagFlag = option("--tag", string({ metavar: "TAG" }), { description: message`A BCP 47 language tag, such as de, pt-BR or fr-CA.` });
1015
+ const list$1 = command("list", object({
1016
+ command: constant("locales.list"),
1017
+ cwd: cwdFlag,
1018
+ project: projectFlag,
1019
+ json: jsonFlag$1
1020
+ }), { description: message`List the draft's locales, the default locale first. Pass a locale's tag to --locale on pages, blocks and layouts commands to read or write its content.` });
1021
+ const add = command("add", object({
1022
+ command: constant("locales.add"),
1023
+ tag: tagFlag,
1024
+ cwd: cwdFlag,
1025
+ project: projectFlag,
1026
+ json: jsonFlag$1
1027
+ }), { description: message`Add a locale. Every page, block, repeatable item, synced block and collection record of the default locale gets a copy in it, keeping its id, which Camox translates in the background. Publishing puts the locale on Live. Example: camox locales add --tag de.` });
1028
+ const remove = command("remove", object({
1029
+ command: constant("locales.remove"),
1030
+ tag: tagFlag,
1031
+ cwd: cwdFlag,
1032
+ project: projectFlag,
1033
+ json: jsonFlag$1
1034
+ }), { description: message`Remove a locale with all of its content. Publishing deletes them from Live for good. The default locale can't be removed.` });
1035
+ const setDefaultTag = command("set-default-tag", object({
1036
+ command: constant("locales.set-default-tag"),
1037
+ tag: tagFlag,
1038
+ cwd: cwdFlag,
1039
+ project: projectFlag,
1040
+ json: jsonFlag$1
1041
+ }), { description: message`Correct the default locale's tag, such as from en to en-GB, on Live and in every draft at once. Only possible while it's the only locale.` });
1042
+ const parser$7 = command("locales", or(list$1, add, remove, setDefaultTag));
1043
+ const TOOLS = {
1044
+ "locales.list": "listLocales",
1045
+ "locales.add": "addLocale",
1046
+ "locales.remove": "removeLocale",
1047
+ "locales.set-default-tag": "setDefaultLocaleTag"
1048
+ };
1049
+ async function handler$7(args) {
1050
+ const outputMode = args.json ? "json" : "auto";
1051
+ return dispatch({
1052
+ toolName: TOOLS[args.command],
1053
+ args: "tag" in args ? { tag: args.tag } : {},
1054
+ cwd: args.cwd,
1055
+ projectFlag: args.project,
1056
+ outputMode
1057
+ });
1058
+ }
1059
+ //#endregion
1296
1060
  //#region src/commands/login.ts
1297
- const parser$5 = command("login", object({ command: constant("login") }));
1298
- const handler$5 = login;
1061
+ const parser$6 = command("login", object({ command: constant("login") }));
1062
+ const handler$6 = login;
1299
1063
  async function login() {
1300
1064
  p.intro("camox login");
1301
1065
  try {
@@ -1309,11 +1073,11 @@ async function login() {
1309
1073
  }
1310
1074
  //#endregion
1311
1075
  //#region src/commands/logout.ts
1312
- const parser$4 = command("logout", object({
1076
+ const parser$5 = command("logout", object({
1313
1077
  command: constant("logout"),
1314
1078
  cwd: cwdFlag
1315
1079
  }));
1316
- const handler$4 = logout;
1080
+ const handler$5 = logout;
1317
1081
  function logout(args = {}) {
1318
1082
  p.intro("camox logout");
1319
1083
  let authenticationUrl = null;
@@ -1337,110 +1101,88 @@ function logout(args = {}) {
1337
1101
  }
1338
1102
  //#endregion
1339
1103
  //#region src/commands/pages.ts
1340
- const projectFlag = optional(option("--project", string({ metavar: "SLUG" })));
1341
1104
  const jsonFlag = option("--json");
1342
- const productionFlag = option("--production");
1343
- const liveFlag = option("--live");
1344
1105
  const list = command("list", object({
1345
1106
  command: constant("pages.list"),
1346
1107
  cwd: cwdFlag,
1347
1108
  project: projectFlag,
1348
- production: productionFlag,
1109
+ locale: localeFlag,
1349
1110
  json: jsonFlag
1350
1111
  }));
1351
1112
  const get = command("get", object({
1352
1113
  command: constant("pages.get"),
1353
- id: optional(option("--id", integer({ metavar: "ID" }))),
1354
- path: optional(option("--path", string({ metavar: "PATH" }))),
1355
- live: liveFlag,
1356
- cwd: cwdFlag,
1357
- project: projectFlag,
1358
- production: productionFlag,
1359
- json: jsonFlag
1360
- }));
1361
- const publish = command("publish", object({
1362
- command: constant("pages.publish"),
1363
- id: optional(option("--id", integer({ metavar: "ID" }))),
1114
+ id: optional(option("--id", uuidv7())),
1364
1115
  path: optional(option("--path", string({ metavar: "PATH" }))),
1365
- noLayout: option("--no-layout"),
1366
1116
  cwd: cwdFlag,
1367
1117
  project: projectFlag,
1368
- production: productionFlag,
1118
+ locale: localeFlag,
1369
1119
  json: jsonFlag
1370
1120
  }));
1371
- const unpublish = command("unpublish", object({
1372
- command: constant("pages.unpublish"),
1373
- id: optional(option("--id", integer({ metavar: "ID" }))),
1374
- path: optional(option("--path", string({ metavar: "PATH" }))),
1375
- cwd: cwdFlag,
1376
- project: projectFlag,
1377
- production: productionFlag,
1378
- json: jsonFlag
1379
- }));
1380
- const discardChanges = command("discard-changes", object({
1381
- command: constant("pages.discard-changes"),
1382
- id: optional(option("--id", integer({ metavar: "ID" }))),
1383
- path: optional(option("--path", string({ metavar: "PATH" }))),
1384
- cwd: cwdFlag,
1385
- project: projectFlag,
1386
- production: productionFlag,
1387
- json: jsonFlag
1388
- }));
1389
- const parser$3 = command("pages", or(list, get, command("create", object({
1121
+ const create = command("create", object({
1390
1122
  command: constant("pages.create"),
1391
1123
  nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
1392
1124
  pathSegment: option("--path-segment", string({ metavar: "SEGMENT" })),
1393
- layoutId: option("--layout-id", integer({ metavar: "ID" })),
1394
- parentPageId: optional(option("--parent-page-id", integer({ metavar: "ID" }))),
1125
+ layoutId: option("--layout-id", string({ metavar: "NAME" })),
1126
+ parentPageId: optional(option("--parent-page-id", uuidv7())),
1395
1127
  cwd: cwdFlag,
1396
1128
  project: projectFlag,
1397
- production: productionFlag,
1129
+ locale: localeFlag,
1398
1130
  json: jsonFlag
1399
- })), command("update", object({
1131
+ }));
1132
+ const update = command("update", object({
1400
1133
  command: constant("pages.update"),
1401
- id: optional(option("--id", integer({
1402
- min: 1,
1403
- metavar: "ID"
1404
- }))),
1134
+ id: optional(option("--id", uuidv7())),
1405
1135
  path: optional(option("--path", string({ metavar: "PATH" }))),
1406
- metaTitle: optional(option("--meta-title", string({ metavar: "TEXT" }), { description: message`Set the SEO title. An empty string clears it. Disables automatic SEO for both fields.` })),
1407
- metaDescription: optional(option("--meta-description", string({ metavar: "TEXT" }), { description: message`Set the SEO description. An empty string clears it. Disables automatic SEO for both fields.` })),
1408
- aiSeo: optional(option("--ai-seo", choice(["on", "off"]), { description: message`Enable or disable automatic SEO. Enabling schedules asynchronous generation; it cannot be combined with manual metadata. Disabling preserves current metadata.` })),
1409
1136
  nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
1410
1137
  pathSegment: optional(option("--path-segment", string({ metavar: "SEGMENT" }))),
1411
- parentPageId: optional(option("--parent-page-id", integer({ metavar: "ID" }))),
1138
+ parentPageId: optional(option("--parent-page-id", uuidv7())),
1412
1139
  cwd: cwdFlag,
1413
1140
  project: projectFlag,
1414
- production: productionFlag,
1141
+ locale: localeFlag,
1415
1142
  json: jsonFlag
1416
- }), { description: message`Update a page draft, including in --production. Pass exactly one of --id or --path and at least one update field. Omitted fields are preserved. Publish separately with pages publish. Example: camox pages update --path /about --meta-title "About us" --meta-description "Meet the team". Read metadata with pages get; add --live for published metadata.` }), command("set-layout", object({
1143
+ }), { description: message`Update a page. Pass exactly one of --id or --path and at least one update field. Omitted fields are preserved. Example: camox pages update --path /about --nickname "About". Edit SEO metadata with pages set-seo.` });
1144
+ const setSeo = command("set-seo", object({
1145
+ command: constant("pages.set-seo"),
1146
+ id: optional(option("--id", uuidv7())),
1147
+ path: optional(option("--path", string({ metavar: "PATH" }))),
1148
+ metaTitle: optional(option("--meta-title", string({ metavar: "TEXT" }), { description: message`Set the SEO title. Makes both fields manual.` })),
1149
+ metaDescription: optional(option("--meta-description", string({ metavar: "TEXT" }), { description: message`Set the SEO description. An empty string clears it. Makes both fields manual.` })),
1150
+ aiSeo: optional(option("--ai-seo", choice(["on", "off"]), { description: message`on makes both fields generated again and schedules asynchronous generation; it cannot be combined with --meta-title or --meta-description. off keeps the current values as manual.` })),
1151
+ cwd: cwdFlag,
1152
+ project: projectFlag,
1153
+ locale: localeFlag,
1154
+ json: jsonFlag
1155
+ }), { description: message`Set a page's SEO metadata. The meta title and description are one group: both generated by AI, or both manual. Writing one field makes both fields manual, keeping the current value of the other. Pass exactly one of --id or --path and at least one of --meta-title, --meta-description or --ai-seo. An empty meta title blocks publishing. Example: camox pages set-seo --path /about --meta-title "About us" --meta-description "Meet the team". Read metadata with pages get.` });
1156
+ const setLayout = command("set-layout", object({
1417
1157
  command: constant("pages.set-layout"),
1418
- id: option("--id", integer({ metavar: "ID" })),
1419
- layoutId: option("--layout-id", integer({ metavar: "ID" })),
1158
+ id: option("--id", uuidv7()),
1159
+ layoutId: option("--layout-id", string({ metavar: "NAME" })),
1420
1160
  cwd: cwdFlag,
1421
1161
  project: projectFlag,
1422
- production: productionFlag,
1162
+ locale: localeFlag,
1423
1163
  json: jsonFlag
1424
- })), command("delete", object({
1164
+ }));
1165
+ const del = command("delete", object({
1425
1166
  command: constant("pages.delete"),
1426
- id: option("--id", integer({ metavar: "ID" })),
1167
+ id: option("--id", uuidv7()),
1427
1168
  cwd: cwdFlag,
1428
1169
  project: projectFlag,
1429
- production: productionFlag,
1170
+ locale: localeFlag,
1430
1171
  json: jsonFlag
1431
- })), publish, unpublish, discardChanges));
1432
- async function handler$3(args) {
1172
+ }));
1173
+ const parser$4 = command("pages", or(list, get, create, update, setSeo, setLayout, del));
1174
+ async function handler$4(args) {
1433
1175
  const outputMode = args.json ? "json" : "auto";
1434
1176
  const cwd = args.cwd;
1435
1177
  const projectFlag = args.project;
1436
- const production = args.production;
1178
+ const locale = args.locale;
1437
1179
  switch (args.command) {
1438
1180
  case "pages.list": return dispatch({
1439
1181
  toolName: "listPages",
1440
1182
  args: {},
1441
1183
  cwd,
1442
1184
  projectFlag,
1443
- production,
1185
+ locale,
1444
1186
  outputMode
1445
1187
  });
1446
1188
  case "pages.get":
@@ -1453,13 +1195,10 @@ async function handler$3(args) {
1453
1195
  }
1454
1196
  return dispatch({
1455
1197
  toolName: "getPage",
1456
- args: {
1457
- ...args.id != null ? { id: args.id } : { path: args.path },
1458
- source: args.live ? "live" : "draft"
1459
- },
1198
+ args: args.id != null ? { id: args.id } : { path: args.path },
1460
1199
  cwd,
1461
1200
  projectFlag,
1462
- production,
1201
+ locale,
1463
1202
  outputMode
1464
1203
  });
1465
1204
  case "pages.create": return dispatch({
@@ -1472,7 +1211,7 @@ async function handler$3(args) {
1472
1211
  },
1473
1212
  cwd,
1474
1213
  projectFlag,
1475
- production,
1214
+ locale,
1476
1215
  outputMode
1477
1216
  });
1478
1217
  case "pages.update":
@@ -1483,21 +1222,11 @@ async function handler$3(args) {
1483
1222
  });
1484
1223
  process.exit(2);
1485
1224
  }
1486
- if (args.aiSeo === "on" && (args.metaTitle !== void 0 || args.metaDescription !== void 0)) {
1487
- printError({
1488
- code: "INVALID_ARGS",
1489
- message: "Cannot enable automatic SEO alongside manual metadata."
1490
- });
1491
- process.exit(2);
1492
- }
1493
1225
  if ([
1494
1226
  args.nickname,
1495
1227
  args.pathSegment,
1496
- args.parentPageId,
1497
- args.metaTitle,
1498
- args.metaDescription,
1499
- args.aiSeo
1500
- ].every((value) => value === void 0)) {
1228
+ args.parentPageId
1229
+ ].every((v) => v === void 0)) {
1501
1230
  printError({
1502
1231
  code: "INVALID_ARGS",
1503
1232
  message: "Pass at least one field to update."
@@ -1508,38 +1237,16 @@ async function handler$3(args) {
1508
1237
  toolName: "updatePage",
1509
1238
  args: {
1510
1239
  ...args.id != null ? { id: args.id } : { path: args.path },
1511
- metaTitle: args.metaTitle,
1512
- metaDescription: args.metaDescription,
1513
- aiSeoEnabled: args.aiSeo === void 0 ? void 0 : args.aiSeo === "on",
1514
1240
  nickname: args.nickname,
1515
1241
  pathSegment: args.pathSegment,
1516
1242
  parentPageId: args.parentPageId
1517
1243
  },
1518
1244
  cwd,
1519
1245
  projectFlag,
1520
- production,
1246
+ locale,
1521
1247
  outputMode
1522
1248
  });
1523
- case "pages.set-layout": return dispatch({
1524
- toolName: "setPageLayout",
1525
- args: {
1526
- id: args.id,
1527
- layoutId: args.layoutId
1528
- },
1529
- cwd,
1530
- projectFlag,
1531
- production,
1532
- outputMode
1533
- });
1534
- case "pages.delete": return dispatch({
1535
- toolName: "deletePage",
1536
- args: { id: args.id },
1537
- cwd,
1538
- projectFlag,
1539
- production,
1540
- outputMode
1541
- });
1542
- case "pages.publish":
1249
+ case "pages.set-seo": {
1543
1250
  if (args.id == null === (args.path == null)) {
1544
1251
  printError({
1545
1252
  code: "INVALID_ARGS",
@@ -1547,67 +1254,68 @@ async function handler$3(args) {
1547
1254
  });
1548
1255
  process.exit(2);
1549
1256
  }
1550
- return dispatch({
1551
- toolName: "publishPage",
1552
- args: {
1553
- ...args.id != null ? { id: args.id } : { path: args.path },
1554
- alsoPublishLayout: !args.noLayout
1555
- },
1556
- cwd,
1557
- projectFlag,
1558
- production,
1559
- outputMode
1560
- });
1561
- case "pages.unpublish":
1562
- if (args.id == null === (args.path == null)) {
1257
+ const writesField = args.metaTitle !== void 0 || args.metaDescription !== void 0;
1258
+ if (!writesField && args.aiSeo === void 0) {
1563
1259
  printError({
1564
1260
  code: "INVALID_ARGS",
1565
- message: "Pass exactly one of --id or --path."
1261
+ message: "Pass at least one of --meta-title, --meta-description or --ai-seo."
1566
1262
  });
1567
1263
  process.exit(2);
1568
1264
  }
1569
- return dispatch({
1570
- toolName: "unpublishPage",
1571
- args: args.id != null ? { id: args.id } : { path: args.path },
1572
- cwd,
1573
- projectFlag,
1574
- production,
1575
- outputMode
1576
- });
1577
- case "pages.discard-changes":
1578
- if (args.id == null === (args.path == null)) {
1265
+ if (args.aiSeo === "on" && writesField) {
1579
1266
  printError({
1580
1267
  code: "INVALID_ARGS",
1581
- message: "Pass exactly one of --id or --path."
1268
+ message: "--ai-seo on cannot be combined with --meta-title or --meta-description."
1582
1269
  });
1583
1270
  process.exit(2);
1584
1271
  }
1585
1272
  return dispatch({
1586
- toolName: "discardPageChanges",
1587
- args: args.id != null ? { id: args.id } : { path: args.path },
1273
+ toolName: "setPageSeo",
1274
+ args: {
1275
+ ...args.id != null ? { id: args.id } : { path: args.path },
1276
+ metaTitle: args.metaTitle,
1277
+ metaDescription: args.metaDescription,
1278
+ aiSeo: args.aiSeo === void 0 ? void 0 : args.aiSeo === "on"
1279
+ },
1588
1280
  cwd,
1589
1281
  projectFlag,
1590
- production,
1282
+ locale,
1591
1283
  outputMode
1592
1284
  });
1285
+ }
1286
+ case "pages.set-layout": return dispatch({
1287
+ toolName: "setPageLayout",
1288
+ args: {
1289
+ id: args.id,
1290
+ layoutId: args.layoutId
1291
+ },
1292
+ cwd,
1293
+ projectFlag,
1294
+ locale,
1295
+ outputMode
1296
+ });
1297
+ case "pages.delete": return dispatch({
1298
+ toolName: "deletePage",
1299
+ args: { id: args.id },
1300
+ cwd,
1301
+ projectFlag,
1302
+ locale,
1303
+ outputMode
1304
+ });
1593
1305
  }
1594
1306
  }
1595
1307
  //#endregion
1596
1308
  //#region src/commands/preview.ts
1597
- const parser$2 = command("preview", object({
1309
+ const parser$3 = command("preview", object({
1598
1310
  command: constant("preview"),
1599
1311
  cwd: cwdFlag,
1600
1312
  url: option("--url", string()),
1601
1313
  json: option("--json")
1602
- }), { description: message`Create a single-use sign-in URL for local draft verification. Open it in your automation browser within three minutes. The URL is a credential; do not share it.` });
1603
- async function handler$2(args) {
1314
+ }), { description: message`Create a single-use sign-in URL for local verification. Open it in your automation browser within three minutes. The URL is a credential; do not share it.` });
1315
+ async function handler$3(args) {
1604
1316
  try {
1605
1317
  const url = new URL(args.url);
1606
- if (!["http:", "https:"].includes(url.protocol) || ![
1607
- "localhost",
1608
- "127.0.0.1",
1609
- "[::1]"
1610
- ].includes(url.hostname) || url.username || url.password) throw new Error("Preview requires an http(s) loopback URL without credentials.");
1318
+ if (!isLoopbackUrl(url)) throw new Error("Preview requires an http(s) loopback URL without credentials.");
1611
1319
  const runtime = loadRuntime(args.cwd);
1612
1320
  const auth = readAuthTokenForUrl(runtime.authenticationUrl, args.cwd);
1613
1321
  if (!auth?.email) {
@@ -1618,25 +1326,14 @@ async function handler$2(args) {
1618
1326
  process.exitCode = 2;
1619
1327
  return;
1620
1328
  }
1621
- const response = await fetch(`${normalizeUrl(runtime.apiUrl)}/api/auth/one-time-token/generate`, {
1622
- headers: { Authorization: `Bearer ${auth.token}` },
1623
- redirect: "error",
1624
- signal: AbortSignal.timeout(15e3)
1625
- });
1626
- if (!response.ok) throw new Error(`Could not create preview sign-in (${response.status}). Check your session with camox login.`);
1627
- const data = await response.json();
1628
- if (typeof data.token !== "string" || !data.token) throw new Error("Authentication backend returned no one-time token.");
1629
1329
  const target = {
1630
- projectSlug: runtime.projectSlug,
1631
- environmentName: `dev:${auth.email}`,
1330
+ projectUuid: runtime.projectId,
1632
1331
  apiUrl: normalizeUrl(runtime.apiUrl)
1633
1332
  };
1634
- url.searchParams.set("camox-preview", JSON.stringify(target));
1635
- url.searchParams.set("ott", data.token);
1636
1333
  printResult({
1637
- url: url.href,
1638
- ...target,
1639
- source: "draft",
1334
+ url: await createPreviewSignInUrl(url.href, target, auth.token),
1335
+ projectId: target.projectUuid,
1336
+ apiUrl: target.apiUrl,
1640
1337
  warning: "Single-use sign-in credential, valid for three minutes. Open in your verification browser; do not share or commit."
1641
1338
  }, args.json ? "json" : "auto");
1642
1339
  } catch (error) {
@@ -1648,31 +1345,167 @@ async function handler$2(args) {
1648
1345
  }
1649
1346
  }
1650
1347
  //#endregion
1348
+ //#region src/lib/publish-preview.ts
1349
+ function where(owner) {
1350
+ if (!owner) return "";
1351
+ switch (owner.kind) {
1352
+ case "page": return ` on page ${owner.path}`;
1353
+ case "layout": return ` in layout ${owner.name}`;
1354
+ case "syncedBlock": return ` in synced block ${owner.title}`;
1355
+ case "collection": return ` in collection ${owner.title}`;
1356
+ }
1357
+ }
1358
+ const subject = (item) => `${item.entity} "${item.label}"${where(item.owner)}`;
1359
+ const when = (at) => at === null ? "" : ` on ${new Date(at).toLocaleString()}`;
1360
+ function conflictLine(conflict) {
1361
+ if (conflict.liveVersion === null) return `${subject(conflict)}: deleted on Live`;
1362
+ if (conflict.release) return `${subject(conflict)}: changed by a release${when(conflict.publishedAt)}`;
1363
+ const by = conflict.publishedBy?.name ?? "someone";
1364
+ return `${subject(conflict)}: published by ${by}${when(conflict.publishedAt)}`;
1365
+ }
1366
+ const LOST_REASONS = {
1367
+ parentGone: "what it belongs to is gone from Live and the draft",
1368
+ writtenAfterDeletion: "published after the draft deleted what it belongs to"
1369
+ };
1370
+ function section(title, lines) {
1371
+ if (lines.length === 0) return [];
1372
+ return [
1373
+ `${title} (${lines.length})`,
1374
+ ...lines.map((line) => ` - ${line}`),
1375
+ ""
1376
+ ];
1377
+ }
1378
+ const changeLine = (change) => `${change.change} ${subject(change)}`;
1379
+ /**
1380
+ * The draft's changes, under the locale each is in once the draft changes more than its default
1381
+ * locale: the default locale first, then the others, then what every locale shares, like files.
1382
+ */
1383
+ function changesSection(preview) {
1384
+ const { changes, defaultLocale } = preview;
1385
+ const tags = new Set(changes.map((change) => change.locale));
1386
+ if ([...tags].every((tag) => tag === null || tag === defaultLocale)) return section("Changes", changes.map(changeLine));
1387
+ const rank = (tag) => tag === defaultLocale ? 0 : tag === null ? 2 : 1;
1388
+ const ordered = [...tags].sort((a, b) => rank(a) - rank(b) || (a ?? "").localeCompare(b ?? ""));
1389
+ return [
1390
+ `Changes (${changes.length})`,
1391
+ ...ordered.flatMap((tag) => [` ${tag ?? "Every locale"}`, ...changes.filter((change) => change.locale === tag).map((change) => ` - ${changeLine(change)}`)]),
1392
+ ""
1393
+ ];
1394
+ }
1395
+ /** The preview as text, in the order ADR 0009 lists it, ending with the token. */
1396
+ function formatPreview(preview) {
1397
+ const lines = [
1398
+ ...section("Blockers: fix these in your development draft before publishing", preview.blockers.map((problem) => `${subject(problem)}: ${problem.message}`)),
1399
+ ...section("Conflicts: Live changed these since your draft did; publishing overwrites Live's version", preview.conflicts.map(conflictLine)),
1400
+ ...section("Lost changes: publishing drops these", preview.lost.map((lost) => `${subject(lost)}: ${LOST_REASONS[lost.reason]}`)),
1401
+ ...section("Warnings: already on Live, not blocking", preview.warnings.map((problem) => `${subject(problem)}: ${problem.message}`)),
1402
+ ...changesSection(preview)
1403
+ ];
1404
+ if (preview.changes.length === 0) lines.push("Your development draft has no changes.", "");
1405
+ lines.push(`Token: ${preview.token}`, ...nextStep(preview));
1406
+ return `${lines.join("\n")}\n`;
1407
+ }
1408
+ /**
1409
+ * What to do next: fix the blockers, or publish when there is something to publish, waiting for
1410
+ * running translations or publishing anyway.
1411
+ */
1412
+ function nextStep(preview) {
1413
+ if (preview.blockers.length > 0) return ["Publishing is blocked until the blockers are fixed."];
1414
+ if (preview.changes.length === 0) return [];
1415
+ const publish = `camox publish --confirm ${preview.token}`;
1416
+ if (preview.translating.length > 0) return [`Translations into ${preview.translating.join(", ")} are still running: publish once they finish, or publish anyway and Camox finishes them in a draft of its own.`, `Publish anyway with: ${publish} --anyway`];
1417
+ return [`Publish with: ${publish}`];
1418
+ }
1419
+ //#endregion
1420
+ //#region src/commands/publish.ts
1421
+ const parser$2 = command("publish", object({
1422
+ command: constant("publish"),
1423
+ confirm: optional(option("--confirm", string({ metavar: "TOKEN" }), { description: message`Publish, with the token \`camox publish\` printed with the list you reviewed.` })),
1424
+ anyway: option("--anyway", { description: message`With --confirm, publish while translations are still running: Camox finishes them in a draft of its own and publishes it once they're done.` }),
1425
+ cwd: cwdFlag,
1426
+ project: projectFlag,
1427
+ json: option("--json")
1428
+ }), { description: message`List what publishing your development draft to Live would do, with a token; publish with --confirm <token>.` });
1429
+ async function handler$2(args) {
1430
+ const outputMode = args.json ? "json" : "auto";
1431
+ const target = {
1432
+ cwd: args.cwd,
1433
+ projectFlag: args.project
1434
+ };
1435
+ if (args.confirm === void 0) {
1436
+ printPreview(await callToolResult({
1437
+ ...target,
1438
+ toolName: "previewPublish",
1439
+ args: {}
1440
+ }), outputMode);
1441
+ process.exit(0);
1442
+ }
1443
+ const result = await callToolResult({
1444
+ ...target,
1445
+ toolName: "publishDraft",
1446
+ args: {
1447
+ token: args.confirm,
1448
+ ...args.anyway && { publishAnyway: true }
1449
+ }
1450
+ });
1451
+ if (result.published !== false) {
1452
+ if (isJsonMode(outputMode)) printResult(result, outputMode);
1453
+ else process.stdout.write("Published your development draft to Live.\n");
1454
+ process.exit(0);
1455
+ }
1456
+ if (result.reason === "empty") {
1457
+ if (isJsonMode(outputMode)) printResult(result, outputMode);
1458
+ else process.stdout.write("Your development draft has no changes: nothing to publish.\n");
1459
+ process.exit(0);
1460
+ }
1461
+ if (result.reason === "discarded") {
1462
+ printError({
1463
+ code: DRAFT_ENDED_ERROR_CODE,
1464
+ message: `${result.message} Nothing was published.`
1465
+ });
1466
+ process.exit(1);
1467
+ }
1468
+ printError({
1469
+ code: NOT_PUBLISHED_CODES[result.reason],
1470
+ message: `${result.message} Nothing was published.`
1471
+ });
1472
+ printPreview(result.preview, outputMode);
1473
+ process.exit(1);
1474
+ }
1475
+ function printPreview(preview, outputMode) {
1476
+ if (isJsonMode(outputMode)) return printResult(preview, outputMode);
1477
+ process.stdout.write(formatPreview(preview));
1478
+ }
1479
+ //#endregion
1651
1480
  //#region src/commands/release.ts
1652
- const parser$1 = command("release", object({ command: constant("release") }));
1653
- function handler$1() {
1654
- p.intro("camox release");
1655
- if (!process.env.CAMOX_DEPLOY_TOKEN) {
1656
- p.log.error("CAMOX_DEPLOY_TOKEN is required to release to production.");
1481
+ const parser$1 = command("release", object({
1482
+ command: constant("release"),
1483
+ dryRun: option("--dry-run", { description: message`Build the website as a release would, without logging in or syncing its definitions to Live.` })
1484
+ }));
1485
+ function handler$1(args) {
1486
+ p.intro(args.dryRun ? "camox release --dry-run" : "camox release");
1487
+ if (!args.dryRun && !process.env.CAMOX_DEPLOY_TOKEN) {
1488
+ p.log.error("CAMOX_DEPLOY_TOKEN is required to release to Live.");
1657
1489
  process.exit(1);
1658
1490
  }
1659
- p.log.info("Building and syncing the production environment...");
1660
- const result = spawnSync(process.platform === "win32" ? "vp.cmd" : "vp", ["build"], {
1491
+ p.log.info(args.dryRun ? "Building for Live, without syncing definitions..." : "Building and syncing definitions to Live...");
1492
+ const executable = process.platform === "win32" ? "vp.cmd" : "vp";
1493
+ const result = spawnSync(executable, ["build"], {
1661
1494
  stdio: "inherit",
1662
1495
  env: {
1663
1496
  ...process.env,
1664
- CAMOX_INTERNAL_RELEASE: "1"
1497
+ CAMOX_INTERNAL_RELEASE: args.dryRun ? "dry-run" : "1"
1665
1498
  }
1666
1499
  });
1667
1500
  if (result.error) {
1668
- p.log.error(`Could not start the production build: ${result.error.message}`);
1501
+ p.log.error(`Could not start the release build: ${result.error.message}`);
1669
1502
  process.exit(1);
1670
1503
  }
1671
1504
  if (result.status !== 0) {
1672
- p.log.error("Production release failed.");
1505
+ p.log.error(args.dryRun ? "Dry run failed." : "Release failed.");
1673
1506
  process.exit(result.status ?? 1);
1674
1507
  }
1675
- p.outro("Production release prepared successfully.");
1508
+ p.outro(args.dryRun ? "Dry run built successfully." : "Release prepared successfully.");
1676
1509
  process.exit(0);
1677
1510
  }
1678
1511
  //#endregion
@@ -1680,9 +1513,37 @@ function handler$1() {
1680
1513
  const parser = command("status", object({
1681
1514
  command: constant("status"),
1682
1515
  cwd: cwdFlag,
1683
- production: option("--production"),
1516
+ project: projectFlag,
1684
1517
  json: option("--json")
1685
1518
  }));
1519
+ async function developmentDraftStatus(params) {
1520
+ try {
1521
+ const project = await getProject(params.token, params.project, params.apiUrl);
1522
+ return {
1523
+ exists: true,
1524
+ changeCount: (await getDevelopmentDraft({
1525
+ ...params,
1526
+ projectId: project.id
1527
+ })).changeCount
1528
+ };
1529
+ } catch (err) {
1530
+ if (isNoDevelopmentDraft(err)) return {
1531
+ exists: false,
1532
+ changeCount: 0
1533
+ };
1534
+ return {
1535
+ exists: null,
1536
+ error: err instanceof Error ? err.message : String(err)
1537
+ };
1538
+ }
1539
+ }
1540
+ function describeDraft(draft, authenticationUrl) {
1541
+ if (!draft) return `(sign in to see it — run \`camox login\` for ${authenticationUrl})`;
1542
+ if (draft.exists === null) return `(couldn't be checked: ${draft.error})`;
1543
+ if (!draft.exists) return "no development draft yet — run your site locally once to create it";
1544
+ if (draft.changeCount === 0) return "no changes";
1545
+ return `${draft.changeCount} change${draft.changeCount === 1 ? "" : "s"} — \`camox publish\` lists them`;
1546
+ }
1686
1547
  async function handler(args) {
1687
1548
  let runtime;
1688
1549
  try {
@@ -1704,11 +1565,18 @@ async function handler(args) {
1704
1565
  }
1705
1566
  throw err;
1706
1567
  }
1568
+ const project = args.project?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectId;
1707
1569
  const token = readAuthTokenForUrl(runtime.authenticationUrl, args.cwd);
1708
- const environmentName = args.production ? "production" : token?.email ? `dev:${token.email}` : null;
1570
+ const developmentDraft = token ? await developmentDraftStatus({
1571
+ token: token.token,
1572
+ apiUrl: runtime.apiUrl,
1573
+ project,
1574
+ disableTelemetry: runtime.disableTelemetry
1575
+ }) : null;
1709
1576
  const status = {
1710
- projectSlug: runtime.projectSlug,
1711
- environmentName,
1577
+ project,
1578
+ draft: DEVELOPMENT_DRAFT,
1579
+ developmentDraft,
1712
1580
  apiUrl: runtime.apiUrl,
1713
1581
  authenticationUrl: runtime.authenticationUrl,
1714
1582
  authenticated: token !== null,
@@ -1722,8 +1590,8 @@ async function handler(args) {
1722
1590
  process.exit(0);
1723
1591
  }
1724
1592
  const lines = [
1725
- `project: ${status.projectSlug}`,
1726
- `environment: ${status.environmentName ?? "(unknown — run `camox login`)"}`,
1593
+ `project: ${status.project}`,
1594
+ `draft: ${describeDraft(developmentDraft, status.authenticationUrl)}`,
1727
1595
  `api: ${status.apiUrl}`,
1728
1596
  `auth: ${status.authenticationUrl}`,
1729
1597
  status.user ? `signed in: ${status.user.name} <${status.user.email}>` : `signed in: (no token for ${status.authenticationUrl} — run \`camox login\`)`
@@ -1733,28 +1601,29 @@ async function handler(args) {
1733
1601
  }
1734
1602
  //#endregion
1735
1603
  //#region src/index.ts
1736
- const result = runSync(defineProgram({
1737
- parser: or(parser$7, parser$5, parser$4, parser$1, parser, parser$2, parser$3, parser$11, parser$10, parser$6, parser$9, parser$8),
1604
+ const program = defineProgram({
1605
+ parser: or(parser$9, parser$6, parser$5, parser$1, parser, parser$3, parser$4, parser$12, parser$8, parser$7, parser$2, parser$11, parser$10),
1738
1606
  metadata: {
1739
1607
  name: "camox",
1740
1608
  brief: message`Camox CLI`
1741
1609
  }
1742
- }), { help: "both" });
1610
+ });
1611
+ const result = runSync(program, { help: "both" });
1743
1612
  switch (result.command) {
1744
1613
  case "init":
1745
- await handler$7();
1614
+ handler$9();
1746
1615
  break;
1747
1616
  case "login":
1748
- await handler$5();
1617
+ await handler$6();
1749
1618
  break;
1750
1619
  case "logout":
1751
- handler$4(result);
1620
+ handler$5(result);
1752
1621
  break;
1753
1622
  case "release":
1754
- handler$1();
1623
+ handler$1(result);
1755
1624
  break;
1756
1625
  case "preview":
1757
- await handler$2(result);
1626
+ await handler$3(result);
1758
1627
  break;
1759
1628
  case "status":
1760
1629
  await handler(result);
@@ -1763,12 +1632,10 @@ switch (result.command) {
1763
1632
  case "pages.get":
1764
1633
  case "pages.create":
1765
1634
  case "pages.update":
1635
+ case "pages.set-seo":
1766
1636
  case "pages.set-layout":
1767
1637
  case "pages.delete":
1768
- case "pages.publish":
1769
- case "pages.unpublish":
1770
- case "pages.discard-changes":
1771
- await handler$3(result);
1638
+ await handler$4(result);
1772
1639
  break;
1773
1640
  case "blocks.types":
1774
1641
  case "blocks.describe":
@@ -1778,26 +1645,28 @@ switch (result.command) {
1778
1645
  case "blocks.edit":
1779
1646
  case "blocks.move":
1780
1647
  case "blocks.delete":
1781
- await handler$11(result);
1782
- break;
1783
- case "comments.list":
1784
- case "comments.resolve":
1785
- await handler$10(result);
1648
+ await handler$12(result);
1786
1649
  break;
1787
1650
  case "files.upload":
1788
1651
  case "files.update":
1789
1652
  case "files.get":
1790
1653
  case "files.list":
1791
- await handler$8(result);
1654
+ await handler$10(result);
1792
1655
  break;
1793
1656
  case "layouts.list":
1794
- await handler$6(result);
1657
+ case "layouts.get":
1658
+ await handler$8(result);
1659
+ break;
1660
+ case "locales.list":
1661
+ case "locales.add":
1662
+ case "locales.remove":
1663
+ case "locales.set-default-tag":
1664
+ await handler$7(result);
1795
1665
  break;
1796
- case "env.check":
1797
- case "env.push":
1798
- case "env.pull":
1799
- await handler$9(result);
1666
+ case "publish":
1667
+ await handler$2(result);
1800
1668
  break;
1669
+ case "discard": await handler$11(result);
1801
1670
  }
1802
1671
  //#endregion
1803
1672
  export {};