@camox/cli 0.43.0 → 0.45.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 } from "./auth-core-cCuk5aZy.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;
305
- const token = readAuthTokenForUrl(runtime.authenticationUrl);
306
- if (!token) return fail$1({
329
+ const project = opts.projectFlag?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectId;
330
+ const token = readAuthTokenForUrl(runtime.authenticationUrl, opts.cwd);
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$9 = 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$9(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$9(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$9(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$9(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$9(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$9(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$9(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,59 +660,46 @@ async function handler$9(args) {
572
660
  }
573
661
  }
574
662
  //#endregion
575
- //#region src/commands/env.ts
576
- const projectFlag$2 = optional(option("--project", string({ metavar: "SLUG" })));
577
- const jsonFlag$2 = option("--json");
578
- const yesFlag = option("--yes", "-y");
579
- const parser$8 = command("env", or(command("check", object({
580
- command: constant("env.check"),
581
- cwd: cwdFlag,
582
- project: projectFlag$2,
583
- json: jsonFlag$2
584
- })), command("push", object({
585
- command: constant("env.push"),
586
- yes: yesFlag,
587
- cwd: cwdFlag,
588
- project: projectFlag$2,
589
- json: jsonFlag$2
590
- })), command("pull", object({
591
- command: constant("env.pull"),
592
- yes: yesFlag,
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.` }),
593
667
  cwd: cwdFlag,
594
- project: projectFlag$2,
595
- json: jsonFlag$2
596
- }))));
597
- async function handler$8(args) {
668
+ project: projectFlag,
669
+ json: option("--json")
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;
684
+ }
685
+ async function handler$11(args) {
598
686
  const outputMode = args.json ? "json" : "auto";
599
- const cwd = args.cwd;
600
- const projectFlag = args.project;
601
- switch (args.command) {
602
- case "env.check": return dispatch({
603
- toolName: "checkEnvironmentCompatibility",
604
- args: {},
605
- cwd,
606
- projectFlag,
607
- outputMode
687
+ if (!args.yes && !await confirmed()) {
688
+ printError({
689
+ code: "CANCELLED",
690
+ message: "Nothing was discarded."
608
691
  });
609
- case "env.push":
610
- case "env.pull": {
611
- const isPush = args.command === "env.push";
612
- if (!args.yes) {
613
- printError({
614
- code: "CONFIRMATION_REQUIRED",
615
- 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.`
616
- });
617
- process.exit(2);
618
- }
619
- return dispatch({
620
- toolName: "replicateEnvironment",
621
- args: { direction: isPush ? "push" : "pull" },
622
- cwd,
623
- projectFlag,
624
- outputMode
625
- });
626
- }
692
+ process.exit(1);
627
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);
628
703
  }
629
704
  const TIMEOUT_MS = 12e4;
630
705
  const MAX_REDIRECTS = 5;
@@ -754,13 +829,8 @@ async function uploadFile(context, options, targetId) {
754
829
  body.set("file", await openAsBlob(path, { type }), options.filename ?? filename);
755
830
  body.set("projectId", String(context.projectId));
756
831
  if (options.alt !== void 0) body.set("alt", options.alt);
757
- if (options.aiMetadata !== void 0 || options.alt !== void 0) body.set("aiMetadataEnabled", String(options.alt === void 0 && options.aiMetadata === "on"));
758
- const headers = {
759
- Authorization: `Bearer ${context.token}`,
760
- "x-camox-client": "cli",
761
- "x-environment-name": context.environmentName
762
- };
763
- 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);
764
834
  let response;
765
835
  try {
766
836
  const endpoint = targetId === void 0 ? "/files/upload" : `/files/${targetId}/content`;
@@ -774,7 +844,10 @@ async function uploadFile(context, options, targetId) {
774
844
  } catch {
775
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.`);
776
846
  }
777
- 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
+ }
778
851
  return await response.json();
779
852
  } finally {
780
853
  if (temporaryDirectory) await rm(temporaryDirectory, {
@@ -787,49 +860,41 @@ async function uploadFile(context, options, targetId) {
787
860
  //#region src/commands/files.ts
788
861
  const common = {
789
862
  cwd: cwdFlag,
790
- project: optional(option("--project", string({ metavar: "SLUG" }))),
791
- production: option("--production"),
863
+ project: projectFlag,
792
864
  json: option("--json")
793
865
  };
794
866
  const metadata = {
795
- alt: optional(option("--alt", string({ metavar: "TEXT" }), { description: message`Alt text (including an empty string). Disables automatic metadata.` })),
796
- 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.` }))
797
869
  };
798
- const parser$7 = command("files", or(command("upload", object({
870
+ const parser$10 = command("files", or(command("upload", object({
799
871
  command: constant("files.upload"),
800
872
  file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Local file to upload. Pass exactly one of --file or --url.` })),
801
873
  url: optional(option("--url", string({ metavar: "URL" }), { description: message`HTTP(S) URL to download and copy into Camox, not an external reference.` })),
802
874
  filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Override the local basename or final URL path filename.` })),
803
875
  ...metadata,
804
876
  ...common
805
- }), { 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({
806
878
  command: constant("files.update"),
807
- id: option("--id", integer({
808
- min: 1,
809
- metavar: "ID"
810
- })),
879
+ id: option("--id", uuidv7()),
811
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.` })),
812
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.` })),
813
- 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.` })),
814
883
  ...metadata,
815
884
  ...common
816
- }), { 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({
817
886
  command: constant("files.get"),
818
- id: option("--id", integer({
819
- min: 1,
820
- metavar: "ID"
821
- })),
887
+ id: option("--id", uuidv7()),
822
888
  ...common
823
- }), { 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({
824
890
  command: constant("files.list"),
825
891
  ...common
826
- }), { 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.` });
827
- async function handler$7(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) {
828
894
  const outputMode = args.json ? "json" : "auto";
829
895
  const options = {
830
896
  cwd: args.cwd,
831
897
  projectFlag: args.project,
832
- production: args.production,
833
898
  outputMode
834
899
  };
835
900
  try {
@@ -855,6 +920,13 @@ async function handler$7(args) {
855
920
  }
856
921
  validateMetadata(args);
857
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
+ }
858
930
  if (args.alt === void 0 && args.aiMetadata === void 0 && args.filename === void 0) {
859
931
  printError({
860
932
  code: "INVALID_ARGS",
@@ -869,7 +941,7 @@ async function handler$7(args) {
869
941
  id: args.id,
870
942
  alt: args.alt,
871
943
  filename: args.filename,
872
- aiMetadataEnabled: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
944
+ aiMetadata: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
873
945
  }
874
946
  });
875
947
  } catch (err) {
@@ -879,386 +951,115 @@ async function handler$7(args) {
879
951
  }
880
952
  }
881
953
  //#endregion
882
- //#region src/lib/agent-skills.ts
883
- function createAgentSkillLinks(targetDir) {
884
- const agentsSkillsDir = path.join(targetDir, ".agents", "skills");
885
- const claudeSkillsDir = path.join(targetDir, ".claude", "skills");
886
- fs.mkdirSync(agentsSkillsDir, { recursive: true });
887
- fs.mkdirSync(claudeSkillsDir, { recursive: true });
888
- for (const skill of ["camox", "camoxify"]) {
889
- fs.symlinkSync(`../../node_modules/camox/skills/${skill}`, path.join(agentsSkillsDir, skill));
890
- fs.symlinkSync(`../../.agents/skills/${skill}`, path.join(claudeSkillsDir, skill));
891
- }
892
- }
893
- //#endregion
894
- //#region src/lib/utils.ts
895
- const packageManagerVersions = {
896
- pnpm: "pnpm@11.1.3",
897
- bun: "bun@1.2.23",
898
- npm: "npm@11.6.2"
899
- };
900
- const pmCommands = {
901
- pnpm: {
902
- install: {
903
- bin: "corepack",
904
- args: ["pnpm", "install"],
905
- display: "pnpm install",
906
- fallback: {
907
- bin: "pnpm",
908
- args: ["install"]
909
- }
910
- },
911
- dev: {
912
- bin: "corepack",
913
- args: ["pnpm", "dev"],
914
- display: "pnpm dev",
915
- fallback: {
916
- bin: "pnpm",
917
- args: ["dev"]
918
- }
919
- }
920
- },
921
- bun: {
922
- install: {
923
- bin: "bun",
924
- args: ["install"],
925
- display: "bun install"
926
- },
927
- dev: {
928
- bin: "bun",
929
- args: ["dev"],
930
- display: "bun dev"
931
- }
932
- },
933
- npm: {
934
- install: {
935
- bin: "npm",
936
- args: ["install"],
937
- display: "npm install"
938
- },
939
- dev: {
940
- bin: "npm",
941
- args: ["run", "dev"],
942
- display: "npm run dev"
943
- }
944
- }
945
- };
946
- function copyDir(src, dest, replacements) {
947
- fs.mkdirSync(dest, { recursive: true });
948
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
949
- const srcPath = path.join(src, entry.name);
950
- const destPath = path.join(dest, entry.name);
951
- if (fs.statSync(srcPath).isDirectory()) {
952
- copyDir(srcPath, destPath, replacements);
953
- continue;
954
- }
955
- let content = fs.readFileSync(srcPath, "utf-8");
956
- for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(key, value);
957
- fs.writeFileSync(destPath, content);
958
- }
959
- }
960
- //#endregion
961
954
  //#region src/commands/init.ts
962
- const parser$6 = command("init", object({ command: constant("init") }));
963
- const handler$6 = init;
964
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
965
- const ownPkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8"));
966
- const PNPM_WORKSPACE = `allowBuilds:
967
- core-js: true
968
- msw: true
969
- protobufjs: true
970
- `;
971
- var CommandError = class extends Error {
972
- constructor(message, cause) {
973
- super(message);
974
- this.cause = cause;
975
- }
976
- };
977
- function onCancel() {
978
- p.cancel("Cancelled.");
979
- process.exit(0);
980
- }
981
- function runCommand(bin, args, cwd) {
982
- return new Promise((resolve, reject) => {
983
- const child = spawn(bin, args, {
984
- cwd,
985
- stdio: "ignore"
986
- });
987
- child.on("close", (code) => {
988
- if (code === 0) resolve();
989
- else reject(/* @__PURE__ */ new Error(`Exit code ${code}`));
990
- });
991
- child.on("error", (err) => reject(new CommandError(`Failed to start ${bin}`, err)));
992
- });
993
- }
994
- function spawnCommand(bin, args, cwd) {
995
- return spawn(bin, args, {
996
- cwd,
997
- stdio: "inherit",
998
- detached: true
999
- });
1000
- }
1001
- function isMissingCommandError(err) {
1002
- if (!(err instanceof CommandError)) return false;
1003
- const cause = err.cause;
1004
- if (!(cause instanceof Error) || "code" in cause === false) return false;
1005
- return cause.code === "ENOENT";
1006
- }
1007
- async function runPackageManagerCommand(command, cwd) {
1008
- try {
1009
- await runCommand(command.bin, command.args, cwd);
1010
- } catch (err) {
1011
- if (!isMissingCommandError(err) || !command.fallback) throw err;
1012
- await runCommand(command.fallback.bin, command.fallback.args, cwd);
1013
- }
1014
- }
1015
- function startPackageManagerCommand(command, cwd) {
1016
- return new Promise((resolve, reject) => {
1017
- const child = spawnCommand(command.bin, command.args, cwd);
1018
- child.once("spawn", () => resolve(child));
1019
- child.once("error", (err) => {
1020
- const commandError = new CommandError(`Failed to start ${command.bin}`, err);
1021
- if (!isMissingCommandError(commandError) || !command.fallback) {
1022
- reject(commandError);
1023
- return;
1024
- }
1025
- const fallback = spawnCommand(command.fallback.bin, command.fallback.args, cwd);
1026
- fallback.once("spawn", () => resolve(fallback));
1027
- fallback.once("error", (fallbackErr) => reject(new CommandError(`Failed to start ${command.fallback?.bin}`, fallbackErr)));
1028
- });
1029
- });
1030
- }
1031
- function getCommandFailureMessage(pm, err) {
1032
- if (!isMissingCommandError(err)) return "Failed to install dependencies.";
1033
- 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.";
1034
- if (pm === "bun") return "Bun is required to install dependencies with the selected package manager. Install Bun, then run the setup commands below.";
1035
- return "npm is required to install dependencies. Install npm, then run the setup commands below.";
1036
- }
1037
- const CREATE_NEW_ORG = "__create_new__";
1038
- async function selectOrCreateOrganization(token) {
1039
- const orgs = await listOrganizations(token);
1040
- if (orgs.length === 0) {
1041
- p.log.info("You don't have any organizations yet. Let's create one.");
1042
- return promptCreateOrganization(token);
1043
- }
1044
- const selected = await p.select({
1045
- message: "Select an organization for your new project",
1046
- options: [...orgs.map((org) => ({
1047
- value: org.id,
1048
- label: `${org.name} (${org.slug})`
1049
- })), {
1050
- value: CREATE_NEW_ORG,
1051
- label: "Create a new organization"
1052
- }]
1053
- });
1054
- if (p.isCancel(selected)) return onCancel();
1055
- if (selected === CREATE_NEW_ORG) return promptCreateOrganization(token);
1056
- const org = orgs.find((o) => o.id === selected);
1057
- await setActiveOrganization(token, org.id);
1058
- return org.id;
1059
- }
1060
- async function promptCreateOrganization(token) {
1061
- const orgName = await p.text({
1062
- message: "Organization name",
1063
- placeholder: "My Company",
1064
- validate: (value) => {
1065
- if (!value.trim()) return "Organization name is required";
1066
- }
1067
- });
1068
- if (p.isCancel(orgName)) return onCancel();
1069
- const org = await createOrganization(token, orgName, slugify(orgName, {
1070
- lower: true,
1071
- strict: true
1072
- }));
1073
- p.log.success(`Created organization: ${org.name}`);
1074
- return org.id;
1075
- }
1076
- async function init() {
1077
- p.intro(`Camox v${ownPkg.version}`);
1078
- const stored = readAuthToken();
1079
- if (stored) p.log.info(`Welcome back, ${stored.name}!`);
1080
- p.log.info("Let's create your Camox application.");
1081
- const name = await p.text({
1082
- message: "Project display name",
1083
- placeholder: "My Website",
1084
- validate: (value) => {
1085
- if (!value.trim()) return "Project name is required";
1086
- }
1087
- });
1088
- if (p.isCancel(name)) return onCancel();
1089
- const auth = await getOrAuthenticate();
1090
- const orgId = await selectOrCreateOrganization(auth.token);
1091
- let projectSlug;
1092
- while (true) {
1093
- const slugInput = await p.text({
1094
- message: "Project slug",
1095
- initialValue: slugify(name, {
1096
- lower: true,
1097
- strict: true
1098
- }) || "my-site",
1099
- validate: (value) => {
1100
- if (!value.trim()) return "Slug is required";
1101
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return "Slug must be lowercase alphanumeric with hyphens";
1102
- }
1103
- });
1104
- if (p.isCancel(slugInput)) return onCancel();
1105
- const s = p.spinner();
1106
- s.start("Checking slug availability...");
1107
- const { available } = await checkSlugAvailability(auth.token, slugInput);
1108
- if (available) {
1109
- s.stop("Slug is available!");
1110
- projectSlug = slugInput;
1111
- break;
1112
- }
1113
- s.stop(`Slug "${slugInput}" is already taken. Please choose another.`);
1114
- }
1115
- const projectPath = await p.text({
1116
- message: "Project path",
1117
- initialValue: `./${projectSlug}`,
1118
- validate: (value) => {
1119
- if (!value.trim()) return "Path is required";
1120
- const resolved = path.resolve(value);
1121
- if (fs.existsSync(resolved) && fs.readdirSync(resolved).length > 0) return "Directory is not empty";
1122
- }
1123
- });
1124
- if (p.isCancel(projectPath)) return onCancel();
1125
- const resolvedPath = projectPath;
1126
- const targetDir = path.resolve(resolvedPath);
1127
- const s0 = p.spinner();
1128
- s0.start("Creating project...");
1129
- let project;
1130
- try {
1131
- project = await createProject(auth.token, name, projectSlug, orgId);
1132
- s0.stop(`Project created with slug: ${project.slug}`);
1133
- } catch (err) {
1134
- s0.stop("Failed to create project.");
1135
- p.log.error(err instanceof Error ? err.message : "Unknown error");
1136
- process.exit(1);
1137
- }
1138
- const selected = await p.select({
1139
- message: "Which package manager?",
1140
- options: [
1141
- {
1142
- value: "pnpm",
1143
- label: "pnpm (recommended)"
1144
- },
1145
- {
1146
- value: "bun",
1147
- label: "bun"
1148
- },
1149
- {
1150
- value: "npm",
1151
- label: "npm"
1152
- }
1153
- ]
1154
- });
1155
- if (p.isCancel(selected)) return onCancel();
1156
- const pm = selected;
1157
- const s = p.spinner();
1158
- s.start("Scaffolding project...");
1159
- copyDir(path.resolve(__dirname, "..", "templates", "default"), targetDir, { "{{projectName}}": name });
1160
- createAgentSkillLinks(targetDir);
1161
- const viteConfigPath = path.join(targetDir, "vite.config.ts");
1162
- let viteConfig = fs.readFileSync(viteConfigPath, "utf-8");
1163
- viteConfig = viteConfig.replace(/"[^"]*"(,?)[ \t]*\/\/[ \t]*camox-cli:replace-slug.*$/gm, `"${project.slug}"$1`);
1164
- 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, "");
1165
- fs.writeFileSync(viteConfigPath, viteConfig);
1166
- const pkgPath = path.join(targetDir, "package.json");
1167
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
1168
- pkg.name = project.slug;
1169
- delete pkg.version;
1170
- pkg.dependencies.camox = `^${ownPkg.version}`;
1171
- pkg.packageManager = packageManagerVersions[pm];
1172
- fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
1173
- if (pm === "pnpm") fs.writeFileSync(path.join(targetDir, "pnpm-workspace.yaml"), PNPM_WORKSPACE);
1174
- fs.writeFileSync(path.join(targetDir, ".gitignore"), `node_modules
1175
- .DS_Store
1176
- dist
1177
- dist-ssr
1178
- *.local
1179
- count.txt
1180
- .env
1181
- .nitro
1182
- .tanstack
1183
- .output
1184
- .vinxi
1185
- node_modules
1186
-
1187
- # Auto generated by Camox
1188
- src/camox/app.ts
1189
- src/routes/(camox)/
1190
-
1191
- # Auto generated by Tanstack Router
1192
- src/routeTree.gen.ts
1193
- `);
1194
- s.stop("Project scaffolded!");
1195
- const { install: installCmd, dev: devCmd } = pmCommands[pm];
1196
- const s2 = p.spinner();
1197
- s2.start(`Running ${installCmd.display}...`);
1198
- try {
1199
- await runPackageManagerCommand(installCmd, targetDir);
1200
- s2.stop("Dependencies installed!");
1201
- } catch (err) {
1202
- s2.stop("Install failed.");
1203
- p.log.error(getCommandFailureMessage(pm, err));
1204
- p.outro(`To finish setup:\n cd ${resolvedPath}\n ${installCmd.display}\n ${devCmd.display}`);
1205
- process.exit(1);
1206
- }
1207
- p.log.info(`Starting dev server... (Ctrl+C to stop)`);
1208
- let child;
1209
- try {
1210
- child = await startPackageManagerCommand(devCmd, targetDir);
1211
- } catch (err) {
1212
- p.log.error(getCommandFailureMessage(pm, err));
1213
- p.outro(`To start the dev server:\n cd ${resolvedPath}\n ${devCmd.display}`);
1214
- process.exit(1);
1215
- }
1216
- const sigintHandler = () => {
1217
- if (child.pid) try {
1218
- process.kill(-child.pid, "SIGTERM");
1219
- } catch {}
1220
- };
1221
- process.on("SIGINT", sigintHandler);
1222
- process.on("exit", sigintHandler);
1223
- child.on("close", () => {
1224
- process.removeListener("SIGINT", sigintHandler);
1225
- process.removeListener("exit", sigintHandler);
1226
- p.outro(`To restart the dev server:\n cd ${resolvedPath}\n ${devCmd.display}`);
1227
- process.exit(0);
1228
- });
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);
1229
968
  }
1230
969
  //#endregion
1231
970
  //#region src/commands/layouts.ts
1232
- const projectFlag$1 = optional(option("--project", string({ metavar: "SLUG" })));
1233
- const jsonFlag$1 = option("--json");
1234
- const productionFlag$1 = option("--production");
1235
- const parser$5 = command("layouts", or(command("list", object({
971
+ const jsonFlag$2 = option("--json");
972
+ const list$2 = command("list", object({
1236
973
  command: constant("layouts.list"),
1237
974
  cwd: cwdFlag,
1238
- project: projectFlag$1,
1239
- production: productionFlag$1,
1240
- json: jsonFlag$1
1241
- }))));
1242
- async function handler$5(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) {
1243
988
  const outputMode = args.json ? "json" : "auto";
1244
989
  const cwd = args.cwd;
1245
990
  const projectFlag = args.project;
1246
- const production = args.production;
991
+ const locale = args.locale;
1247
992
  switch (args.command) {
1248
993
  case "layouts.list": return dispatch({
1249
994
  toolName: "listLayouts",
1250
995
  args: {},
1251
996
  cwd,
1252
997
  projectFlag,
1253
- 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,
1254
1007
  outputMode
1255
1008
  });
1256
1009
  }
1257
1010
  }
1258
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
1259
1060
  //#region src/commands/login.ts
1260
- const parser$4 = command("login", object({ command: constant("login") }));
1261
- const handler$4 = login;
1061
+ const parser$6 = command("login", object({ command: constant("login") }));
1062
+ const handler$6 = login;
1262
1063
  async function login() {
1263
1064
  p.intro("camox login");
1264
1065
  try {
@@ -1272,11 +1073,11 @@ async function login() {
1272
1073
  }
1273
1074
  //#endregion
1274
1075
  //#region src/commands/logout.ts
1275
- const parser$3 = command("logout", object({
1076
+ const parser$5 = command("logout", object({
1276
1077
  command: constant("logout"),
1277
1078
  cwd: cwdFlag
1278
1079
  }));
1279
- const handler$3 = logout;
1080
+ const handler$5 = logout;
1280
1081
  function logout(args = {}) {
1281
1082
  p.intro("camox logout");
1282
1083
  let authenticationUrl = null;
@@ -1289,121 +1090,99 @@ function logout(args = {}) {
1289
1090
  }
1290
1091
  if (!(error instanceof RuntimeNotFoundError)) throw error;
1291
1092
  }
1292
- const token = authenticationUrl ? readAuthTokenForUrl(authenticationUrl) : readAuthToken();
1093
+ const token = authenticationUrl ? readAuthTokenForUrl(authenticationUrl, args.cwd) : readAuthToken();
1293
1094
  if (!token) {
1294
1095
  log.error("Not logged in.");
1295
1096
  return;
1296
1097
  }
1297
- if (authenticationUrl) removeAuthTokenForUrl(authenticationUrl);
1098
+ if (authenticationUrl) removeAuthTokenForUrl(authenticationUrl, args.cwd);
1298
1099
  else removeAuthToken();
1299
1100
  p.log.success(`Logged out from ${token.name}.`);
1300
1101
  }
1301
1102
  //#endregion
1302
1103
  //#region src/commands/pages.ts
1303
- const projectFlag = optional(option("--project", string({ metavar: "SLUG" })));
1304
1104
  const jsonFlag = option("--json");
1305
- const productionFlag = option("--production");
1306
- const liveFlag = option("--live");
1307
1105
  const list = command("list", object({
1308
1106
  command: constant("pages.list"),
1309
1107
  cwd: cwdFlag,
1310
1108
  project: projectFlag,
1311
- production: productionFlag,
1109
+ locale: localeFlag,
1312
1110
  json: jsonFlag
1313
1111
  }));
1314
1112
  const get = command("get", object({
1315
1113
  command: constant("pages.get"),
1316
- id: optional(option("--id", integer({ metavar: "ID" }))),
1114
+ id: optional(option("--id", uuidv7())),
1317
1115
  path: optional(option("--path", string({ metavar: "PATH" }))),
1318
- live: liveFlag,
1319
1116
  cwd: cwdFlag,
1320
1117
  project: projectFlag,
1321
- production: productionFlag,
1118
+ locale: localeFlag,
1322
1119
  json: jsonFlag
1323
1120
  }));
1324
- const publish = command("publish", object({
1325
- command: constant("pages.publish"),
1326
- id: optional(option("--id", integer({ metavar: "ID" }))),
1327
- path: optional(option("--path", string({ metavar: "PATH" }))),
1328
- noLayout: option("--no-layout"),
1329
- cwd: cwdFlag,
1330
- project: projectFlag,
1331
- production: productionFlag,
1332
- json: jsonFlag
1333
- }));
1334
- const unpublish = command("unpublish", object({
1335
- command: constant("pages.unpublish"),
1336
- id: optional(option("--id", integer({ metavar: "ID" }))),
1337
- path: optional(option("--path", string({ metavar: "PATH" }))),
1338
- cwd: cwdFlag,
1339
- project: projectFlag,
1340
- production: productionFlag,
1341
- json: jsonFlag
1342
- }));
1343
- const discardChanges = command("discard-changes", object({
1344
- command: constant("pages.discard-changes"),
1345
- id: optional(option("--id", integer({ metavar: "ID" }))),
1346
- path: optional(option("--path", string({ metavar: "PATH" }))),
1347
- cwd: cwdFlag,
1348
- project: projectFlag,
1349
- production: productionFlag,
1350
- json: jsonFlag
1351
- }));
1352
- const parser$2 = command("pages", or(list, get, command("create", object({
1121
+ const create = command("create", object({
1353
1122
  command: constant("pages.create"),
1354
1123
  nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
1355
1124
  pathSegment: option("--path-segment", string({ metavar: "SEGMENT" })),
1356
- layoutId: option("--layout-id", integer({ metavar: "ID" })),
1357
- 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())),
1358
1127
  cwd: cwdFlag,
1359
1128
  project: projectFlag,
1360
- production: productionFlag,
1129
+ locale: localeFlag,
1361
1130
  json: jsonFlag
1362
- })), command("update", object({
1131
+ }));
1132
+ const update = command("update", object({
1363
1133
  command: constant("pages.update"),
1364
- id: optional(option("--id", integer({
1365
- min: 1,
1366
- metavar: "ID"
1367
- }))),
1134
+ id: optional(option("--id", uuidv7())),
1368
1135
  path: optional(option("--path", string({ metavar: "PATH" }))),
1369
- 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.` })),
1370
- 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.` })),
1371
- 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.` })),
1372
1136
  nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
1373
1137
  pathSegment: optional(option("--path-segment", string({ metavar: "SEGMENT" }))),
1374
- parentPageId: optional(option("--parent-page-id", integer({ metavar: "ID" }))),
1138
+ parentPageId: optional(option("--parent-page-id", uuidv7())),
1139
+ cwd: cwdFlag,
1140
+ project: projectFlag,
1141
+ locale: localeFlag,
1142
+ json: jsonFlag
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.` })),
1375
1151
  cwd: cwdFlag,
1376
1152
  project: projectFlag,
1377
- production: productionFlag,
1153
+ locale: localeFlag,
1378
1154
  json: jsonFlag
1379
- }), { 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({
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({
1380
1157
  command: constant("pages.set-layout"),
1381
- id: option("--id", integer({ metavar: "ID" })),
1382
- layoutId: option("--layout-id", integer({ metavar: "ID" })),
1158
+ id: option("--id", uuidv7()),
1159
+ layoutId: option("--layout-id", string({ metavar: "NAME" })),
1383
1160
  cwd: cwdFlag,
1384
1161
  project: projectFlag,
1385
- production: productionFlag,
1162
+ locale: localeFlag,
1386
1163
  json: jsonFlag
1387
- })), command("delete", object({
1164
+ }));
1165
+ const del = command("delete", object({
1388
1166
  command: constant("pages.delete"),
1389
- id: option("--id", integer({ metavar: "ID" })),
1167
+ id: option("--id", uuidv7()),
1390
1168
  cwd: cwdFlag,
1391
1169
  project: projectFlag,
1392
- production: productionFlag,
1170
+ locale: localeFlag,
1393
1171
  json: jsonFlag
1394
- })), publish, unpublish, discardChanges));
1395
- async function handler$2(args) {
1172
+ }));
1173
+ const parser$4 = command("pages", or(list, get, create, update, setSeo, setLayout, del));
1174
+ async function handler$4(args) {
1396
1175
  const outputMode = args.json ? "json" : "auto";
1397
1176
  const cwd = args.cwd;
1398
1177
  const projectFlag = args.project;
1399
- const production = args.production;
1178
+ const locale = args.locale;
1400
1179
  switch (args.command) {
1401
1180
  case "pages.list": return dispatch({
1402
1181
  toolName: "listPages",
1403
1182
  args: {},
1404
1183
  cwd,
1405
1184
  projectFlag,
1406
- production,
1185
+ locale,
1407
1186
  outputMode
1408
1187
  });
1409
1188
  case "pages.get":
@@ -1416,13 +1195,10 @@ async function handler$2(args) {
1416
1195
  }
1417
1196
  return dispatch({
1418
1197
  toolName: "getPage",
1419
- args: {
1420
- ...args.id != null ? { id: args.id } : { path: args.path },
1421
- source: args.live ? "live" : "draft"
1422
- },
1198
+ args: args.id != null ? { id: args.id } : { path: args.path },
1423
1199
  cwd,
1424
1200
  projectFlag,
1425
- production,
1201
+ locale,
1426
1202
  outputMode
1427
1203
  });
1428
1204
  case "pages.create": return dispatch({
@@ -1435,7 +1211,7 @@ async function handler$2(args) {
1435
1211
  },
1436
1212
  cwd,
1437
1213
  projectFlag,
1438
- production,
1214
+ locale,
1439
1215
  outputMode
1440
1216
  });
1441
1217
  case "pages.update":
@@ -1446,21 +1222,11 @@ async function handler$2(args) {
1446
1222
  });
1447
1223
  process.exit(2);
1448
1224
  }
1449
- if (args.aiSeo === "on" && (args.metaTitle !== void 0 || args.metaDescription !== void 0)) {
1450
- printError({
1451
- code: "INVALID_ARGS",
1452
- message: "Cannot enable automatic SEO alongside manual metadata."
1453
- });
1454
- process.exit(2);
1455
- }
1456
1225
  if ([
1457
1226
  args.nickname,
1458
1227
  args.pathSegment,
1459
- args.parentPageId,
1460
- args.metaTitle,
1461
- args.metaDescription,
1462
- args.aiSeo
1463
- ].every((value) => value === void 0)) {
1228
+ args.parentPageId
1229
+ ].every((v) => v === void 0)) {
1464
1230
  printError({
1465
1231
  code: "INVALID_ARGS",
1466
1232
  message: "Pass at least one field to update."
@@ -1471,38 +1237,16 @@ async function handler$2(args) {
1471
1237
  toolName: "updatePage",
1472
1238
  args: {
1473
1239
  ...args.id != null ? { id: args.id } : { path: args.path },
1474
- metaTitle: args.metaTitle,
1475
- metaDescription: args.metaDescription,
1476
- aiSeoEnabled: args.aiSeo === void 0 ? void 0 : args.aiSeo === "on",
1477
1240
  nickname: args.nickname,
1478
1241
  pathSegment: args.pathSegment,
1479
1242
  parentPageId: args.parentPageId
1480
1243
  },
1481
1244
  cwd,
1482
1245
  projectFlag,
1483
- production,
1246
+ locale,
1484
1247
  outputMode
1485
1248
  });
1486
- case "pages.set-layout": return dispatch({
1487
- toolName: "setPageLayout",
1488
- args: {
1489
- id: args.id,
1490
- layoutId: args.layoutId
1491
- },
1492
- cwd,
1493
- projectFlag,
1494
- production,
1495
- outputMode
1496
- });
1497
- case "pages.delete": return dispatch({
1498
- toolName: "deletePage",
1499
- args: { id: args.id },
1500
- cwd,
1501
- projectFlag,
1502
- production,
1503
- outputMode
1504
- });
1505
- case "pages.publish":
1249
+ case "pages.set-seo": {
1506
1250
  if (args.id == null === (args.path == null)) {
1507
1251
  printError({
1508
1252
  code: "INVALID_ARGS",
@@ -1510,77 +1254,258 @@ async function handler$2(args) {
1510
1254
  });
1511
1255
  process.exit(2);
1512
1256
  }
1513
- return dispatch({
1514
- toolName: "publishPage",
1515
- args: {
1516
- ...args.id != null ? { id: args.id } : { path: args.path },
1517
- alsoPublishLayout: !args.noLayout
1518
- },
1519
- cwd,
1520
- projectFlag,
1521
- production,
1522
- outputMode
1523
- });
1524
- case "pages.unpublish":
1525
- 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) {
1526
1259
  printError({
1527
1260
  code: "INVALID_ARGS",
1528
- message: "Pass exactly one of --id or --path."
1261
+ message: "Pass at least one of --meta-title, --meta-description or --ai-seo."
1529
1262
  });
1530
1263
  process.exit(2);
1531
1264
  }
1532
- return dispatch({
1533
- toolName: "unpublishPage",
1534
- args: args.id != null ? { id: args.id } : { path: args.path },
1535
- cwd,
1536
- projectFlag,
1537
- production,
1538
- outputMode
1539
- });
1540
- case "pages.discard-changes":
1541
- if (args.id == null === (args.path == null)) {
1265
+ if (args.aiSeo === "on" && writesField) {
1542
1266
  printError({
1543
1267
  code: "INVALID_ARGS",
1544
- message: "Pass exactly one of --id or --path."
1268
+ message: "--ai-seo on cannot be combined with --meta-title or --meta-description."
1545
1269
  });
1546
1270
  process.exit(2);
1547
1271
  }
1548
1272
  return dispatch({
1549
- toolName: "discardPageChanges",
1550
- 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
+ },
1551
1280
  cwd,
1552
1281
  projectFlag,
1553
- production,
1282
+ locale,
1554
1283
  outputMode
1555
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
+ });
1556
1305
  }
1557
1306
  }
1558
1307
  //#endregion
1308
+ //#region src/commands/preview.ts
1309
+ const parser$3 = command("preview", object({
1310
+ command: constant("preview"),
1311
+ cwd: cwdFlag,
1312
+ url: option("--url", string()),
1313
+ json: option("--json")
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) {
1316
+ try {
1317
+ const url = new URL(args.url);
1318
+ if (!isLoopbackUrl(url)) throw new Error("Preview requires an http(s) loopback URL without credentials.");
1319
+ const runtime = loadRuntime(args.cwd);
1320
+ const auth = readAuthTokenForUrl(runtime.authenticationUrl, args.cwd);
1321
+ if (!auth?.email) {
1322
+ printError({
1323
+ code: "NOT_AUTHENTICATED",
1324
+ message: `Run camox login for ${runtime.authenticationUrl} first.`
1325
+ });
1326
+ process.exitCode = 2;
1327
+ return;
1328
+ }
1329
+ const target = {
1330
+ projectUuid: runtime.projectId,
1331
+ apiUrl: normalizeUrl(runtime.apiUrl)
1332
+ };
1333
+ printResult({
1334
+ url: await createPreviewSignInUrl(url.href, target, auth.token),
1335
+ projectId: target.projectUuid,
1336
+ apiUrl: target.apiUrl,
1337
+ warning: "Single-use sign-in credential, valid for three minutes. Open in your verification browser; do not share or commit."
1338
+ }, args.json ? "json" : "auto");
1339
+ } catch (error) {
1340
+ printError({
1341
+ code: "PREVIEW_FAILED",
1342
+ message: error instanceof Error ? error.message : "Could not create preview."
1343
+ });
1344
+ process.exitCode = 2;
1345
+ }
1346
+ }
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
1559
1480
  //#region src/commands/release.ts
1560
- const parser$1 = command("release", object({ command: constant("release") }));
1561
- function handler$1() {
1562
- p.intro("camox release");
1563
- if (!process.env.CAMOX_DEPLOY_TOKEN) {
1564
- 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.");
1565
1489
  process.exit(1);
1566
1490
  }
1567
- p.log.info("Building and syncing the production environment...");
1568
- 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"], {
1569
1494
  stdio: "inherit",
1570
1495
  env: {
1571
1496
  ...process.env,
1572
- CAMOX_INTERNAL_RELEASE: "1"
1497
+ CAMOX_INTERNAL_RELEASE: args.dryRun ? "dry-run" : "1"
1573
1498
  }
1574
1499
  });
1575
1500
  if (result.error) {
1576
- 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}`);
1577
1502
  process.exit(1);
1578
1503
  }
1579
1504
  if (result.status !== 0) {
1580
- p.log.error("Production release failed.");
1505
+ p.log.error(args.dryRun ? "Dry run failed." : "Release failed.");
1581
1506
  process.exit(result.status ?? 1);
1582
1507
  }
1583
- p.outro("Production release prepared successfully.");
1508
+ p.outro(args.dryRun ? "Dry run built successfully." : "Release prepared successfully.");
1584
1509
  process.exit(0);
1585
1510
  }
1586
1511
  //#endregion
@@ -1588,9 +1513,37 @@ function handler$1() {
1588
1513
  const parser = command("status", object({
1589
1514
  command: constant("status"),
1590
1515
  cwd: cwdFlag,
1591
- production: option("--production"),
1516
+ project: projectFlag,
1592
1517
  json: option("--json")
1593
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
+ }
1594
1547
  async function handler(args) {
1595
1548
  let runtime;
1596
1549
  try {
@@ -1612,11 +1565,18 @@ async function handler(args) {
1612
1565
  }
1613
1566
  throw err;
1614
1567
  }
1615
- const token = readAuthTokenForUrl(runtime.authenticationUrl);
1616
- const environmentName = args.production ? "production" : token?.email ? `dev:${token.email}` : null;
1568
+ const project = args.project?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectId;
1569
+ const token = readAuthTokenForUrl(runtime.authenticationUrl, args.cwd);
1570
+ const developmentDraft = token ? await developmentDraftStatus({
1571
+ token: token.token,
1572
+ apiUrl: runtime.apiUrl,
1573
+ project,
1574
+ disableTelemetry: runtime.disableTelemetry
1575
+ }) : null;
1617
1576
  const status = {
1618
- projectSlug: runtime.projectSlug,
1619
- environmentName,
1577
+ project,
1578
+ draft: DEVELOPMENT_DRAFT,
1579
+ developmentDraft,
1620
1580
  apiUrl: runtime.apiUrl,
1621
1581
  authenticationUrl: runtime.authenticationUrl,
1622
1582
  authenticated: token !== null,
@@ -1630,8 +1590,8 @@ async function handler(args) {
1630
1590
  process.exit(0);
1631
1591
  }
1632
1592
  const lines = [
1633
- `project: ${status.projectSlug}`,
1634
- `environment: ${status.environmentName ?? "(unknown — run `camox login`)"}`,
1593
+ `project: ${status.project}`,
1594
+ `draft: ${describeDraft(developmentDraft, status.authenticationUrl)}`,
1635
1595
  `api: ${status.apiUrl}`,
1636
1596
  `auth: ${status.authenticationUrl}`,
1637
1597
  status.user ? `signed in: ${status.user.name} <${status.user.email}>` : `signed in: (no token for ${status.authenticationUrl} — run \`camox login\`)`
@@ -1641,25 +1601,29 @@ async function handler(args) {
1641
1601
  }
1642
1602
  //#endregion
1643
1603
  //#region src/index.ts
1644
- const result = runSync(defineProgram({
1645
- parser: or(parser$6, parser$4, parser$3, parser$1, parser, parser$2, parser$9, parser$5, parser$8, parser$7),
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),
1646
1606
  metadata: {
1647
1607
  name: "camox",
1648
1608
  brief: message`Camox CLI`
1649
1609
  }
1650
- }), { help: "both" });
1610
+ });
1611
+ const result = runSync(program, { help: "both" });
1651
1612
  switch (result.command) {
1652
1613
  case "init":
1653
- await handler$6();
1614
+ handler$9();
1654
1615
  break;
1655
1616
  case "login":
1656
- await handler$4();
1617
+ await handler$6();
1657
1618
  break;
1658
1619
  case "logout":
1659
- handler$3(result);
1620
+ handler$5(result);
1660
1621
  break;
1661
1622
  case "release":
1662
- handler$1();
1623
+ handler$1(result);
1624
+ break;
1625
+ case "preview":
1626
+ await handler$3(result);
1663
1627
  break;
1664
1628
  case "status":
1665
1629
  await handler(result);
@@ -1668,12 +1632,10 @@ switch (result.command) {
1668
1632
  case "pages.get":
1669
1633
  case "pages.create":
1670
1634
  case "pages.update":
1635
+ case "pages.set-seo":
1671
1636
  case "pages.set-layout":
1672
1637
  case "pages.delete":
1673
- case "pages.publish":
1674
- case "pages.unpublish":
1675
- case "pages.discard-changes":
1676
- await handler$2(result);
1638
+ await handler$4(result);
1677
1639
  break;
1678
1640
  case "blocks.types":
1679
1641
  case "blocks.describe":
@@ -1683,22 +1645,28 @@ switch (result.command) {
1683
1645
  case "blocks.edit":
1684
1646
  case "blocks.move":
1685
1647
  case "blocks.delete":
1686
- await handler$9(result);
1648
+ await handler$12(result);
1687
1649
  break;
1688
1650
  case "files.upload":
1689
1651
  case "files.update":
1690
1652
  case "files.get":
1691
1653
  case "files.list":
1692
- await handler$7(result);
1654
+ await handler$10(result);
1693
1655
  break;
1694
1656
  case "layouts.list":
1695
- await handler$5(result);
1696
- break;
1697
- case "env.check":
1698
- case "env.push":
1699
- case "env.pull":
1657
+ case "layouts.get":
1700
1658
  await handler$8(result);
1701
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);
1665
+ break;
1666
+ case "publish":
1667
+ await handler$2(result);
1668
+ break;
1669
+ case "discard": await handler$11(result);
1702
1670
  }
1703
1671
  //#endregion
1704
1672
  export {};