@camox/cli 0.58.0 → 0.59.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,22 +1,22 @@
1
1
  #!/usr/bin/env node
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-CBYfBQrH.mjs";
3
2
  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";
3
+ import { i as UUID, n as pushCode, t as createCodeReadToken } from "./api-DU0iBFMw.mjs";
4
+ import { a as removeAuthTokenForUrl, i as readAuthTokenForUrl, o as verifyOneTimeToken, s as writeAuthTokenForUrl } from "./auth-core-BpVsNVnR.mjs";
5
5
  import { object, or } from "@optique/core/constructs";
6
6
  import { message } from "@optique/core/message";
7
7
  import { defineProgram } from "@optique/core/program";
8
8
  import { runSync } from "@optique/run";
9
- import { mkdtemp, open, readFile, rm, stat } from "node:fs/promises";
10
- import { multiple, optional } from "@optique/core/modifiers";
11
- import { command, constant, option } from "@optique/core/primitives";
12
- import { choice, string } from "@optique/core/valueparser";
13
9
  import { execSync, spawnSync } from "node:child_process";
14
- import http from "node:http";
10
+ import path, { join, resolve } from "node:path";
15
11
  import * as p from "@clack/prompts";
16
12
  import { log } from "@clack/prompts";
17
- import fs, { openAsBlob } from "node:fs";
13
+ import { optional } from "@optique/core/modifiers";
14
+ import { argument, command, constant, option } from "@optique/core/primitives";
15
+ import { string } from "@optique/core/valueparser";
16
+ import http from "node:http";
17
+ import fs, { mkdtempSync, readFileSync, rmSync } from "node:fs";
18
18
  import { tmpdir } from "node:os";
19
- import path, { basename, extname, join } from "node:path";
19
+ import { text } from "node:stream/consumers";
20
20
  import { ZodError, z } from "zod";
21
21
  //#region src/lib/auth.ts
22
22
  const CAMOX_URL = process.env.CAMOX_URL || "https://app.camox.ai";
@@ -118,7 +118,7 @@ async function authenticateUser() {
118
118
  async function getOrAuthenticate() {
119
119
  const stored = readAuthToken();
120
120
  if (stored) {
121
- const { verifySession } = await import("./api-Ph2yMPXw.mjs");
121
+ const { verifySession } = await import("./api-DOHUrl0V.mjs");
122
122
  if (await verifySession(stored.token)) {
123
123
  p.log.info(`Authenticated as ${stored.name}`);
124
124
  return stored;
@@ -129,63 +129,133 @@ async function getOrAuthenticate() {
129
129
  return authenticateUser();
130
130
  }
131
131
  //#endregion
132
- //#region src/lib/output.ts
133
- function isJsonMode(mode) {
134
- if (mode === "json") return true;
135
- return !process.stdout.isTTY;
132
+ //#region src/lib/git-credential.ts
133
+ /**
134
+ * The Git config entries that make `camox git-credential` the credential helper for a remote's
135
+ * host, written in a clone's own config only. An empty `helper` first drops every helper set
136
+ * elsewhere for that host, such as a keychain that would keep expired read tokens. Git sends the
137
+ * path, which names the project.
138
+ */
139
+ function credentialHelperConfig(remote) {
140
+ const { origin } = new URL(remote);
141
+ return [
142
+ [`credential.${origin}.helper`, ""],
143
+ [`credential.${origin}.helper`, `!${helperCommand()}`],
144
+ [`credential.${origin}.useHttpPath`, "true"]
145
+ ];
136
146
  }
137
- function printResult(value, mode) {
138
- const indent = isJsonMode(mode) ? 0 : 2;
139
- process.stdout.write(`${JSON.stringify(value, null, indent)}\n`);
147
+ /** This CLI, as a shell runs it: with this Node.js, so it works without `camox` on the PATH. */
148
+ function helperCommand() {
149
+ const quote = (arg) => `'${arg.replaceAll("'", `'\\''`)}'`;
150
+ return `${quote(process.execPath)} ${quote(process.argv[1])} git-credential`;
140
151
  }
141
- function printError(err) {
142
- process.stderr.write(`${JSON.stringify(err)}\n`);
152
+ /** Parses what Git writes to a credential helper: `key=value` lines up to a blank one. */
153
+ function parseCredentialRequest(input) {
154
+ const request = {};
155
+ for (const line of input.split("\n")) {
156
+ if (line === "") break;
157
+ const at = line.indexOf("=");
158
+ if (at > 0) request[line.slice(0, at)] = line.slice(at + 1);
159
+ }
160
+ return request;
143
161
  }
144
- /**
145
- * Parse a JSON flag value, surfacing a structured CLI error if the input
146
- * isn't valid JSON. Block `content` and `settings` arrive this way.
147
- */
148
- function parseJsonFlag(name, raw) {
162
+ /** The Project ID a repo's remote path names: `git/<namespace>/<Project ID>.git`. */
163
+ function projectIdOfPath(path) {
164
+ return path?.match(/([^/]+?)(?:\.git)?\/?$/)?.[1];
165
+ }
166
+ //#endregion
167
+ //#region src/commands/clone.ts
168
+ const parser$5 = command("clone", object({
169
+ command: constant("clone"),
170
+ project: argument(string({ metavar: "PROJECT" }), { description: message`The project's Project Slug or Project ID.` }),
171
+ directory: optional(argument(string({ metavar: "DIRECTORY" }), { description: message`Where to clone it. Defaults to its Project Slug.` }))
172
+ }), { description: message`Clone a project's code, for a project whose code can be worked on outside Camox.` });
173
+ async function handler$5(args) {
174
+ p.intro(`camox clone ${args.project}`);
175
+ const { token } = await getOrAuthenticate().catch(() => {
176
+ p.log.error("Authentication failed.");
177
+ process.exit(1);
178
+ });
179
+ let access;
149
180
  try {
150
- return JSON.parse(raw);
151
- } catch (e) {
152
- const message = e instanceof Error ? e.message : String(e);
153
- throw Object.assign(/* @__PURE__ */ new Error(`Invalid JSON for ${name}: ${message}`), { __cliError: {
154
- code: "INVALID_JSON",
155
- message: `Invalid JSON for ${name}`,
156
- details: message
157
- } });
181
+ access = await createCodeReadToken(token, args.project);
182
+ } catch (error) {
183
+ p.log.error(error instanceof Error ? error.message : String(error));
184
+ process.exit(1);
185
+ }
186
+ const directory = resolve(args.directory ?? access.projectSlug);
187
+ const config = credentialHelperConfig(access.remote).flatMap(([key, value]) => ["--config", `${key}=${value}`]);
188
+ const cloned = spawnSync("git", [
189
+ "clone",
190
+ ...config,
191
+ access.remote,
192
+ directory
193
+ ], { stdio: "inherit" });
194
+ if (cloned.error || cloned.status !== 0) {
195
+ p.log.error(cloned.error ? `Could not run Git: ${cloned.error.message}` : "Git couldn't clone it.");
196
+ process.exit(1);
158
197
  }
198
+ p.log.info("Installing dependencies...");
199
+ const executable = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
200
+ const installed = spawnSync(executable, ["install"], {
201
+ cwd: directory,
202
+ stdio: "inherit"
203
+ });
204
+ if (installed.error || installed.status !== 0) {
205
+ p.log.error(`Cloned into ${directory}, but installing its dependencies failed.`);
206
+ process.exit(1);
207
+ }
208
+ p.outro(`Cloned into ${directory}.`);
209
+ process.exit(0);
159
210
  }
211
+ //#endregion
212
+ //#region src/commands/git-credential.ts
213
+ const parser$4 = command("git-credential", object({
214
+ command: constant("git-credential"),
215
+ action: argument(string({ metavar: "ACTION" }), { description: message`What Git asks: get, store or erase.` })
216
+ }), { description: message`The Git credential helper \`camox clone\` sets up: answers Git with short-lived read tokens.` });
160
217
  /**
161
- * Only the first dev-mode sync after login creates the development draft, so the CLI can't
162
- * (ADR 0010).
218
+ * Answers Git's credential protocol (git-credential(1)). Only `get` is answered, with a fresh
219
+ * read token: tokens are short-lived, so there's nothing to store or erase. Errors go to stderr,
220
+ * since stdout is Git's.
163
221
  */
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;
222
+ async function handler$4(args) {
223
+ const request = parseCredentialRequest(await text(process.stdin));
224
+ if (args.action !== "get") process.exit(0);
225
+ const project = projectIdOfPath(request.path);
226
+ if (!project) process.exit(0);
227
+ const session = readAuthToken();
228
+ if (!session) {
229
+ process.stderr.write("camox: not logged in. Run `camox login`, then try again.\n");
230
+ process.exit(1);
231
+ }
232
+ try {
233
+ const { token, expiresAt } = await createCodeReadToken(session.token, project);
234
+ const expiry = Math.floor(new Date(expiresAt).getTime() / 1e3);
235
+ process.stdout.write(`username=camox\npassword=${token}\npassword_expiry_utc=${expiry}\n`);
236
+ process.exit(0);
237
+ } catch (error) {
238
+ process.stderr.write(`camox: ${error instanceof Error ? error.message : String(error)}\n`);
239
+ process.exit(1);
240
+ }
172
241
  }
173
- function asCliError(err) {
174
- if (err && typeof err === "object" && "__cliError" in err) return err.__cliError;
175
- if (isNoDevelopmentDraft(err)) return NO_DEVELOPMENT_DRAFT_CLI_ERROR;
176
- if (err instanceof Error) return {
177
- code: "INTERNAL_ERROR",
178
- message: err.message
179
- };
180
- return {
181
- code: "INTERNAL_ERROR",
182
- message: String(err)
183
- };
242
+ //#endregion
243
+ //#region src/commands/login.ts
244
+ const parser$3 = command("login", object({ command: constant("login") }));
245
+ const handler$3 = login;
246
+ async function login() {
247
+ p.intro("camox login");
248
+ try {
249
+ await getOrAuthenticate();
250
+ } catch {
251
+ p.log.error("Authentication failed.");
252
+ process.exit(1);
253
+ }
254
+ p.outro("You're all set!");
255
+ process.exit(0);
184
256
  }
185
257
  //#endregion
186
258
  //#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
259
  /**
190
260
  * A Project ID: a UUIDv7 in lowercase only, the form the project's Worker is named by, so an
191
261
  * uppercase one fails here rather than silently naming no project.
@@ -267,817 +337,15 @@ function loadRuntime(cwd = process.cwd()) {
267
337
  }
268
338
  }
269
339
  //#endregion
270
- //#region src/lib/dispatch.ts
271
- /**
272
- * Strip undefined fields. Optique returns `undefined` for absent optional
273
- * flags, but the tool's Zod input often distinguishes `key: undefined` from
274
- * the key being absent. Send only the keys the user actually passed.
275
- */
276
- function compact(input) {
277
- const out = {};
278
- for (const [k, v] of Object.entries(input)) if (v !== void 0) out[k] = v;
279
- return out;
280
- }
281
- function fail$2(err, code) {
282
- printError(err);
283
- process.exit(code);
284
- }
285
- async function resolveProjectId(token, project, apiUrl) {
286
- try {
287
- return (await getProject(token, project, apiUrl)).id;
288
- } catch (err) {
289
- if (isNoDevelopmentDraft(err)) return fail$2(NO_DEVELOPMENT_DRAFT_CLI_ERROR, 2);
290
- return fail$2({
291
- code: "PROJECT_LOOKUP_FAILED",
292
- message: `Could not load project "${project}".`,
293
- details: err instanceof Error ? err.message : String(err)
294
- }, 2);
295
- }
296
- }
297
- async function callRemote(params) {
298
- try {
299
- return await callTool(params);
300
- } catch (err) {
301
- return fail$2(asCliError(err), 1);
302
- }
303
- }
304
- /**
305
- * Resolve auth + project, call the registered tool via `agent.callTool`,
306
- * and render the result. Exit codes: 0 on success, 1 on tool error, 2 on
307
- * auth/project resolution failure.
308
- *
309
- * Project, apiUrl, and authenticationUrl come from the vite plugin's
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.
313
- */
314
- async function resolveCommandContext(opts) {
315
- let runtime;
316
- try {
317
- runtime = loadRuntime(opts.cwd);
318
- } catch (err) {
319
- if (err instanceof RuntimeDirectoryError) return fail$2({
320
- code: "INVALID_CWD",
321
- message: err.message
322
- }, 2);
323
- if (err instanceof RuntimeNotFoundError || err instanceof RuntimeMalformedError) return fail$2({
324
- code: "RUNTIME_NOT_FOUND",
325
- message: err.message
326
- }, 2);
327
- throw err;
328
- }
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({
332
- code: "NOT_AUTHENTICATED",
333
- message: `No stored credentials for ${runtime.authenticationUrl}. Run \`camox login\` against this backend first.`
334
- }, 2);
335
- const projectId = await resolveProjectId(token.token, project, runtime.apiUrl);
336
- return {
337
- token: token.token,
338
- apiUrl: runtime.apiUrl,
339
- projectId,
340
- disableTelemetry: runtime.disableTelemetry
341
- };
342
- }
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) {
348
- const response = await callRemote({
349
- ...await resolveCommandContext(opts),
350
- name: opts.toolName,
351
- args: compact({
352
- ...opts.args,
353
- locale: opts.locale
354
- })
355
- });
356
- if (!response.ok) {
357
- printError(response.error);
358
- process.exit(1);
359
- }
360
- return response.result;
361
- }
362
- async function dispatch(opts) {
363
- printResult(await callToolResult(opts), opts.outputMode);
364
- process.exit(0);
365
- }
366
- //#endregion
367
340
  //#region src/lib/runtime-options.ts
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
- }
394
- //#endregion
395
- //#region src/commands/blocks.ts
396
- const jsonFlag$3 = option("--json");
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({
399
- command: constant("blocks.types"),
400
- cwd: cwdFlag,
401
- project: projectFlag,
402
- json: jsonFlag$3
403
- }));
404
- const describe = command("describe", object({
405
- command: constant("blocks.describe"),
406
- type: multiple(option("--type", string({ metavar: "TYPE" })), { min: 1 }),
407
- cwd: cwdFlag,
408
- project: projectFlag,
409
- json: jsonFlag$3
410
- }));
411
- const get$2 = command("get", object({
412
- command: constant("blocks.get"),
413
- id: option("--id", uuidv7()),
414
- cwd: cwdFlag,
415
- project: projectFlag,
416
- locale: localeFlag,
417
- json: jsonFlag$3
418
- }));
419
- const getMany = command("get-many", object({
420
- command: constant("blocks.get-many"),
421
- id: multiple(option("--id", uuidv7()), { min: 1 }),
422
- cwd: cwdFlag,
423
- project: projectFlag,
424
- locale: localeFlag,
425
- json: jsonFlag$3
426
- }));
427
- const create$1 = command("create", object({
428
- command: constant("blocks.create"),
429
- pageId: option("--page-id", uuidv7()),
430
- type: option("--type", string({ metavar: "TYPE" })),
431
- content: optional(option("--content", string({ metavar: "JSON" }))),
432
- fieldFile: fieldFileFlag,
433
- settings: optional(option("--settings", string({ metavar: "JSON" }))),
434
- position: optional(option("--position", choice(["first", "last"], { metavar: "WHERE" }))),
435
- afterId: optional(option("--after-id", uuidv7())),
436
- beforeId: optional(option("--before-id", uuidv7())),
437
- afterPosition: optional(option("--after-position", string({ metavar: "POS" }))),
438
- beforePosition: optional(option("--before-position", string({ metavar: "POS" }))),
439
- cwd: cwdFlag,
440
- project: projectFlag,
441
- locale: localeFlag,
442
- json: jsonFlag$3
443
- }));
444
- const edit = command("edit", object({
445
- command: constant("blocks.edit"),
446
- id: option("--id", uuidv7()),
447
- content: optional(option("--content", string({ metavar: "JSON" }))),
448
- fieldFile: fieldFileFlag,
449
- settings: optional(option("--settings", string({ metavar: "JSON" }))),
450
- cwd: cwdFlag,
451
- project: projectFlag,
452
- locale: localeFlag,
453
- json: jsonFlag$3
454
- }));
455
- const move = command("move", object({
456
- command: constant("blocks.move"),
457
- id: option("--id", uuidv7()),
458
- position: optional(option("--position", choice(["first", "last"], { metavar: "WHERE" }))),
459
- afterId: optional(option("--after-id", uuidv7())),
460
- beforeId: optional(option("--before-id", uuidv7())),
461
- afterPosition: optional(option("--after-position", string({ metavar: "POS" }))),
462
- beforePosition: optional(option("--before-position", string({ metavar: "POS" }))),
463
- cwd: cwdFlag,
464
- project: projectFlag,
465
- locale: localeFlag,
466
- json: jsonFlag$3
467
- }));
468
- const del$1 = command("delete", object({
469
- command: constant("blocks.delete"),
470
- id: option("--id", uuidv7()),
471
- cwd: cwdFlag,
472
- project: projectFlag,
473
- locale: localeFlag,
474
- json: jsonFlag$3
475
- }));
476
- const parser$12 = command("blocks", or(types, describe, get$2, getMany, create$1, edit, move, del$1));
477
- const POSITIONING_FLAGS = [
478
- ["position", "--position"],
479
- ["afterId", "--after-id"],
480
- ["beforeId", "--before-id"],
481
- ["afterPosition", "--after-position"],
482
- ["beforePosition", "--before-position"]
483
- ];
484
- /**
485
- * Both `create` and `move` accept multiple positioning inputs, but only one is
486
- * meaningful per call. The server will reject conflicts, but we surface the
487
- * mistake earlier (and with friendlier wording) by checking on the client.
488
- */
489
- function collectPositioningArgs(args) {
490
- const used = [];
491
- const toolArgs = {};
492
- for (const [key, flag] of POSITIONING_FLAGS) {
493
- const value = args[key];
494
- if (value === void 0) continue;
495
- used.push(flag);
496
- Object.assign(toolArgs, { [key]: value });
497
- }
498
- return {
499
- used,
500
- toolArgs
501
- };
502
- }
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) {
533
- const outputMode = args.json ? "json" : "auto";
534
- const cwd = args.cwd;
535
- const projectFlag = args.project;
536
- const locale = args.locale;
537
- try {
538
- switch (args.command) {
539
- case "blocks.types": return dispatch({
540
- toolName: "listBlockTypes",
541
- args: {},
542
- cwd,
543
- projectFlag,
544
- locale,
545
- outputMode
546
- });
547
- case "blocks.describe": return dispatch({
548
- toolName: "describeBlockTypes",
549
- args: { types: [...args.type] },
550
- cwd,
551
- projectFlag,
552
- locale,
553
- outputMode
554
- });
555
- case "blocks.get": return dispatch({
556
- toolName: "getBlock",
557
- args: { id: args.id },
558
- cwd,
559
- projectFlag,
560
- locale,
561
- outputMode
562
- });
563
- case "blocks.get-many": return dispatch({
564
- toolName: "getBlocks",
565
- args: { ids: [...args.id] },
566
- cwd,
567
- projectFlag,
568
- locale,
569
- outputMode
570
- });
571
- case "blocks.create": {
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);
580
- const settings = args.settings !== void 0 ? parseJsonFlag("--settings", args.settings) : void 0;
581
- const { used, toolArgs } = collectPositioningArgs(args);
582
- if (used.length > 1) {
583
- printError({
584
- code: "INVALID_ARGS",
585
- message: `Pass at most one positioning flag — got: ${used.join(", ")}.`
586
- });
587
- process.exit(2);
588
- }
589
- return dispatch({
590
- toolName: "createBlock",
591
- args: {
592
- pageId: args.pageId,
593
- type: args.type,
594
- content,
595
- settings,
596
- ...toolArgs
597
- },
598
- cwd,
599
- projectFlag,
600
- locale,
601
- outputMode
602
- });
603
- }
604
- case "blocks.edit": {
605
- const content = await resolveContent(args.content, args.fieldFile);
606
- const settings = args.settings !== void 0 ? parseJsonFlag("--settings", args.settings) : void 0;
607
- return dispatch({
608
- toolName: "editBlock",
609
- args: {
610
- id: args.id,
611
- content,
612
- settings
613
- },
614
- cwd,
615
- projectFlag,
616
- locale,
617
- outputMode
618
- });
619
- }
620
- case "blocks.move": {
621
- const { used, toolArgs } = collectPositioningArgs(args);
622
- if (used.length === 0) {
623
- printError({
624
- code: "INVALID_ARGS",
625
- message: "Pass a positioning flag — one of --position, --after-id, --before-id, --after-position, --before-position. Use `--position last` to move a block to the end of the page."
626
- });
627
- process.exit(2);
628
- }
629
- if (used.length > 1) {
630
- printError({
631
- code: "INVALID_ARGS",
632
- message: `Pass at most one positioning flag — got: ${used.join(", ")}.`
633
- });
634
- process.exit(2);
635
- }
636
- return dispatch({
637
- toolName: "moveBlock",
638
- args: {
639
- id: args.id,
640
- ...toolArgs
641
- },
642
- cwd,
643
- projectFlag,
644
- locale,
645
- outputMode
646
- });
647
- }
648
- case "blocks.delete": return dispatch({
649
- toolName: "deleteBlock",
650
- args: { id: args.id },
651
- cwd,
652
- projectFlag,
653
- locale,
654
- outputMode
655
- });
656
- }
657
- } catch (err) {
658
- printError(asCliError(err));
659
- process.exit(1);
660
- }
661
- }
662
- //#endregion
663
- //#region src/commands/delete.ts
664
- const parser$11 = command("delete", object({
665
- command: constant("delete"),
666
- yes: option("--yes", "-y", { description: message`Delete without asking for confirmation.` }),
667
- cwd: cwdFlag,
668
- project: projectFlag,
669
- json: option("--json")
670
- }), { description: message`Delete every change in your development draft. Live is untouched.` });
671
- const WARNING = "This deletes 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} Delete it?`,
682
- initialValue: false
683
- }) === true;
684
- }
685
- async function handler$11(args) {
686
- const outputMode = args.json ? "json" : "auto";
687
- if (!args.yes && !await confirmed()) {
688
- printError({
689
- code: "CANCELLED",
690
- message: "Nothing was deleted."
691
- });
692
- process.exit(1);
693
- }
694
- const result = await callToolResult({
695
- toolName: "deleteDraft",
696
- args: {},
697
- cwd: args.cwd,
698
- projectFlag: args.project
699
- });
700
- if (isJsonMode(outputMode)) printResult(result, outputMode);
701
- else process.stdout.write("Deleted every change in your development draft.\n");
702
- process.exit(0);
703
- }
704
- const TIMEOUT_MS = 12e4;
705
- const MAX_REDIRECTS = 5;
706
- function fail(code, message) {
707
- throw Object.assign(new Error(message), { __cliError: {
708
- code,
709
- message
710
- } });
711
- }
712
- function validateMetadata(options) {
713
- if (options.alt !== void 0 && options.aiMetadata === "on") fail("INVALID_ARGS", "--alt cannot be combined with --ai-metadata on.");
714
- }
715
- function validateUpload(options) {
716
- if (options.file !== void 0 === (options.url !== void 0)) fail("INVALID_ARGS", "Pass exactly one of --file or --url.");
717
- validateMetadata(options);
718
- validateFilename(options.filename);
719
- }
720
- function validateFilename(filename) {
721
- if (filename !== void 0 && (!filename.trim() || /[\\/]/.test(filename) || filename.split("").some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) || filename === "." || filename === "..")) fail("INVALID_ARGS", "--filename must be a non-empty filename without directories or control characters.");
722
- }
723
- function httpUrl(value) {
724
- let url;
725
- try {
726
- url = new URL(value);
727
- } catch {
728
- return fail("DOWNLOAD_FAILED", "Invalid media URL.");
729
- }
730
- if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) fail("DOWNLOAD_FAILED", "Media URLs must use HTTP(S) without embedded credentials.");
731
- return url;
732
- }
733
- const MIME_TYPES = {
734
- ".jpg": "image/jpeg",
735
- ".jpeg": "image/jpeg",
736
- ".png": "image/png",
737
- ".webp": "image/webp",
738
- ".gif": "image/gif",
739
- ".avif": "image/avif",
740
- ".svg": "image/svg+xml",
741
- ".ico": "image/x-icon",
742
- ".bmp": "image/bmp",
743
- ".pdf": "application/pdf",
744
- ".mp4": "video/mp4",
745
- ".webm": "video/webm",
746
- ".mov": "video/quicktime",
747
- ".mp3": "audio/mpeg",
748
- ".wav": "audio/wav",
749
- ".ogg": "audio/ogg",
750
- ".txt": "text/plain",
751
- ".json": "application/json"
752
- };
753
- function mimeType(filename) {
754
- return MIME_TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
755
- }
756
- async function download(source, destination) {
757
- try {
758
- let url = httpUrl(source);
759
- const signal = AbortSignal.timeout(TIMEOUT_MS);
760
- for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
761
- const response = await fetch(url, {
762
- redirect: "manual",
763
- signal
764
- });
765
- if ([
766
- 301,
767
- 302,
768
- 303,
769
- 307,
770
- 308
771
- ].includes(response.status)) {
772
- await response.body?.cancel();
773
- const location = response.headers.get("location");
774
- if (!location || redirects === MAX_REDIRECTS) fail("DOWNLOAD_FAILED", "Missing redirect location or too many redirects.");
775
- url = httpUrl(new URL(location, url).href);
776
- continue;
777
- }
778
- if (!response.ok || !response.body) {
779
- await response.body?.cancel();
780
- fail("DOWNLOAD_FAILED", `Media download failed (HTTP ${response.status}).`);
781
- }
782
- if (Number(response.headers.get("content-length")) > 104857600) {
783
- await response.body.cancel();
784
- fail("FILE_TOO_LARGE", "File exceeds the 100 MiB limit.");
785
- }
786
- const file = await open(destination, "w");
787
- let size = 0;
788
- try {
789
- for await (const chunk of response.body) {
790
- size += chunk.byteLength;
791
- if (size > 104857600) fail("FILE_TOO_LARGE", "File exceeds the 100 MiB limit.");
792
- await file.writeFile(chunk);
793
- }
794
- } finally {
795
- await file.close();
796
- }
797
- const filename = basename(decodeURIComponent(url.pathname)) || "upload";
798
- return {
799
- filename,
800
- mimeType: response.headers.get("content-type")?.split(";")[0]?.trim() || mimeType(filename)
801
- };
802
- }
803
- return fail("DOWNLOAD_FAILED", "Too many redirects.");
804
- } catch (error) {
805
- if (error && typeof error === "object" && "__cliError" in error) throw error;
806
- return fail("DOWNLOAD_FAILED", "Could not download media (network, timeout, or local I/O failure).");
807
- }
808
- }
809
- async function uploadFile(context, options, targetId) {
810
- validateUpload(options);
811
- let temporaryDirectory;
812
- try {
813
- let path = options.file;
814
- let filename = path ? basename(path) : "upload";
815
- let type = mimeType(filename);
816
- if (options.url !== void 0) {
817
- temporaryDirectory = await mkdtemp(join(tmpdir(), "camox-upload-"));
818
- path = join(temporaryDirectory, "download");
819
- const downloaded = await download(options.url, path);
820
- filename = downloaded.filename;
821
- type = downloaded.mimeType;
822
- }
823
- if (!path) fail("INVALID_ARGS", "Provide a file path.");
824
- const info = await stat(path);
825
- if (!info.isFile()) fail("INVALID_ARGS", "Upload source must be a regular file.");
826
- if (info.size > 104857600) fail("FILE_TOO_LARGE", "File exceeds the 100 MiB limit.");
827
- const body = new FormData();
828
- if (type === "application/octet-stream") type = mimeType(options.filename ?? filename);
829
- body.set("file", await openAsBlob(path, { type }), options.filename ?? filename);
830
- body.set("projectId", String(context.projectId));
831
- if (options.alt !== void 0) body.set("alt", options.alt);
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);
834
- let response;
835
- try {
836
- const endpoint = targetId === void 0 ? "/files/upload" : `/files/${targetId}/content`;
837
- response = await fetch(`${context.apiUrl}${endpoint}`, {
838
- method: "POST",
839
- headers,
840
- body,
841
- signal: AbortSignal.timeout(TIMEOUT_MS),
842
- redirect: "error"
843
- });
844
- } catch {
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.`);
846
- }
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
- }
851
- return await response.json();
852
- } finally {
853
- if (temporaryDirectory) await rm(temporaryDirectory, {
854
- recursive: true,
855
- force: true
856
- });
857
- }
858
- }
859
- //#endregion
860
- //#region src/commands/files.ts
861
- const common = {
862
- cwd: cwdFlag,
863
- project: projectFlag,
864
- json: option("--json")
865
- };
866
- const metadata = {
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.` }))
869
- };
870
- const parser$10 = command("files", or(command("upload", object({
871
- command: constant("files.upload"),
872
- file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Local file to upload. Pass exactly one of --file or --url.` })),
873
- url: optional(option("--url", string({ metavar: "URL" }), { description: message`HTTP(S) URL to download and copy into Camox, not an external reference.` })),
874
- filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Override the local basename or final URL path filename.` })),
875
- ...metadata,
876
- ...common
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({
878
- command: constant("files.update"),
879
- id: option("--id", uuidv7()),
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.` })),
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.` })),
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.` })),
883
- ...metadata,
884
- ...common
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({
886
- command: constant("files.get"),
887
- id: option("--id", uuidv7()),
888
- ...common
889
- }), { description: message`Inspect a file in the selected project.` }), command("list", object({
890
- command: constant("files.list"),
891
- ...common
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) {
894
- const outputMode = args.json ? "json" : "auto";
895
- const options = {
896
- cwd: args.cwd,
897
- projectFlag: args.project,
898
- outputMode
899
- };
900
- try {
901
- if (args.command === "files.upload") {
902
- validateUpload(args);
903
- printResult(await uploadFile(await resolveCommandContext(options), args), outputMode);
904
- process.exit(0);
905
- }
906
- if (args.command === "files.list") return dispatch({
907
- ...options,
908
- toolName: "listFiles",
909
- args: {}
910
- });
911
- if (args.command === "files.get") return dispatch({
912
- ...options,
913
- toolName: "getFile",
914
- args: { id: args.id }
915
- });
916
- if (args.file !== void 0 || args.url !== void 0) {
917
- validateUpload(args);
918
- printResult(await uploadFile(await resolveCommandContext(options), args, args.id), outputMode);
919
- process.exit(0);
920
- }
921
- validateMetadata(args);
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
- }
930
- if (args.alt === void 0 && args.aiMetadata === void 0 && args.filename === void 0) {
931
- printError({
932
- code: "INVALID_ARGS",
933
- message: "Pass --file, --url, --filename, --alt or --ai-metadata."
934
- });
935
- process.exit(2);
936
- }
937
- return dispatch({
938
- ...options,
939
- toolName: "updateFile",
940
- args: {
941
- id: args.id,
942
- alt: args.alt,
943
- filename: args.filename,
944
- aiMetadata: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
945
- }
946
- });
947
- } catch (err) {
948
- const error = asCliError(err);
949
- printError(error);
950
- process.exit(error.code === "INVALID_ARGS" ? 2 : 1);
951
- }
952
- }
953
- //#endregion
954
- //#region src/commands/init.ts
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.ai";
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);
968
- }
969
- //#endregion
970
- //#region src/commands/layouts.ts
971
- const jsonFlag$2 = option("--json");
972
- const list$2 = command("list", object({
973
- command: constant("layouts.list"),
974
- cwd: cwdFlag,
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) {
988
- const outputMode = args.json ? "json" : "auto";
989
- const cwd = args.cwd;
990
- const projectFlag = args.project;
991
- const locale = args.locale;
992
- switch (args.command) {
993
- case "layouts.list": return dispatch({
994
- toolName: "listLayouts",
995
- args: {},
996
- cwd,
997
- projectFlag,
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,
1007
- outputMode
1008
- });
1009
- }
1010
- }
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
1060
- //#region src/commands/login.ts
1061
- const parser$6 = command("login", object({ command: constant("login") }));
1062
- const handler$6 = login;
1063
- async function login() {
1064
- p.intro("camox login");
1065
- try {
1066
- await getOrAuthenticate();
1067
- } catch {
1068
- p.log.error("Authentication failed.");
1069
- process.exit(1);
1070
- }
1071
- p.outro("You're all set!");
1072
- process.exit(0);
1073
- }
341
+ 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.` }));
1074
342
  //#endregion
1075
343
  //#region src/commands/logout.ts
1076
- const parser$5 = command("logout", object({
344
+ const parser$2 = command("logout", object({
1077
345
  command: constant("logout"),
1078
346
  cwd: cwdFlag
1079
347
  }));
1080
- const handler$5 = logout;
348
+ const handler$2 = logout;
1081
349
  function logout(args = {}) {
1082
350
  p.intro("camox logout");
1083
351
  let authenticationUrl = null;
@@ -1100,389 +368,152 @@ function logout(args = {}) {
1100
368
  p.log.success(`Logged out from ${token.name}.`);
1101
369
  }
1102
370
  //#endregion
1103
- //#region src/commands/pages.ts
1104
- const jsonFlag = option("--json");
1105
- const list = command("list", object({
1106
- command: constant("pages.list"),
1107
- cwd: cwdFlag,
1108
- project: projectFlag,
1109
- locale: localeFlag,
1110
- json: jsonFlag
1111
- }));
1112
- const get = command("get", object({
1113
- command: constant("pages.get"),
1114
- id: optional(option("--id", uuidv7())),
1115
- path: optional(option("--path", string({ metavar: "PATH" }))),
1116
- cwd: cwdFlag,
1117
- project: projectFlag,
1118
- locale: localeFlag,
1119
- json: jsonFlag
1120
- }));
1121
- const create = command("create", object({
1122
- command: constant("pages.create"),
1123
- nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
1124
- pathSegment: option("--path-segment", string({ metavar: "SEGMENT" })),
1125
- layoutId: option("--layout-id", string({ metavar: "NAME" })),
1126
- parentPageId: optional(option("--parent-page-id", uuidv7())),
1127
- cwd: cwdFlag,
1128
- project: projectFlag,
1129
- locale: localeFlag,
1130
- json: jsonFlag
1131
- }));
1132
- const update = command("update", object({
1133
- command: constant("pages.update"),
1134
- id: optional(option("--id", uuidv7())),
1135
- path: optional(option("--path", string({ metavar: "PATH" }))),
1136
- nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
1137
- pathSegment: optional(option("--path-segment", string({ metavar: "SEGMENT" }))),
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.` })),
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({
1157
- command: constant("pages.set-layout"),
1158
- id: option("--id", uuidv7()),
1159
- layoutId: option("--layout-id", string({ metavar: "NAME" })),
1160
- cwd: cwdFlag,
1161
- project: projectFlag,
1162
- locale: localeFlag,
1163
- json: jsonFlag
1164
- }));
1165
- const del = command("delete", object({
1166
- command: constant("pages.delete"),
1167
- id: option("--id", uuidv7()),
1168
- cwd: cwdFlag,
1169
- project: projectFlag,
1170
- locale: localeFlag,
1171
- json: jsonFlag
1172
- }));
1173
- const parser$4 = command("pages", or(list, get, create, update, setSeo, setLayout, del));
1174
- async function handler$4(args) {
1175
- const outputMode = args.json ? "json" : "auto";
1176
- const cwd = args.cwd;
1177
- const projectFlag = args.project;
1178
- const locale = args.locale;
1179
- switch (args.command) {
1180
- case "pages.list": return dispatch({
1181
- toolName: "listPages",
1182
- args: {},
1183
- cwd,
1184
- projectFlag,
1185
- locale,
1186
- outputMode
1187
- });
1188
- case "pages.get":
1189
- if (args.id == null === (args.path == null)) {
1190
- printError({
1191
- code: "INVALID_ARGS",
1192
- message: "Pass exactly one of --id or --path."
1193
- });
1194
- process.exit(2);
1195
- }
1196
- return dispatch({
1197
- toolName: "getPage",
1198
- args: args.id != null ? { id: args.id } : { path: args.path },
1199
- cwd,
1200
- projectFlag,
1201
- locale,
1202
- outputMode
1203
- });
1204
- case "pages.create": return dispatch({
1205
- toolName: "createPage",
1206
- args: {
1207
- nickname: args.nickname,
1208
- pathSegment: args.pathSegment,
1209
- layoutId: args.layoutId,
1210
- parentPageId: args.parentPageId
1211
- },
1212
- cwd,
1213
- projectFlag,
1214
- locale,
1215
- outputMode
1216
- });
1217
- case "pages.update":
1218
- if (args.id == null === (args.path == null)) {
1219
- printError({
1220
- code: "INVALID_ARGS",
1221
- message: "Pass exactly one of --id or --path."
1222
- });
1223
- process.exit(2);
1224
- }
1225
- if ([
1226
- args.nickname,
1227
- args.pathSegment,
1228
- args.parentPageId
1229
- ].every((v) => v === void 0)) {
1230
- printError({
1231
- code: "INVALID_ARGS",
1232
- message: "Pass at least one field to update."
1233
- });
1234
- process.exit(2);
1235
- }
1236
- return dispatch({
1237
- toolName: "updatePage",
1238
- args: {
1239
- ...args.id != null ? { id: args.id } : { path: args.path },
1240
- nickname: args.nickname,
1241
- pathSegment: args.pathSegment,
1242
- parentPageId: args.parentPageId
1243
- },
1244
- cwd,
1245
- projectFlag,
1246
- locale,
1247
- outputMode
1248
- });
1249
- case "pages.set-seo": {
1250
- if (args.id == null === (args.path == null)) {
1251
- printError({
1252
- code: "INVALID_ARGS",
1253
- message: "Pass exactly one of --id or --path."
1254
- });
1255
- process.exit(2);
1256
- }
1257
- const writesField = args.metaTitle !== void 0 || args.metaDescription !== void 0;
1258
- if (!writesField && args.aiSeo === void 0) {
1259
- printError({
1260
- code: "INVALID_ARGS",
1261
- message: "Pass at least one of --meta-title, --meta-description or --ai-seo."
1262
- });
1263
- process.exit(2);
1264
- }
1265
- if (args.aiSeo === "on" && writesField) {
1266
- printError({
1267
- code: "INVALID_ARGS",
1268
- message: "--ai-seo on cannot be combined with --meta-title or --meta-description."
1269
- });
1270
- process.exit(2);
1271
- }
1272
- return dispatch({
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
- },
1280
- cwd,
1281
- projectFlag,
1282
- locale,
1283
- outputMode
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
- });
1305
- }
1306
- }
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"
371
+ //#region src/commands/push.ts
372
+ const parser$1 = command("push", object({
373
+ command: constant("push"),
374
+ draft: optional(option("--draft", string({ metavar: "DRAFT_ID" }), { description: message`Push to this open draft, such as one Camox's agent started, rather than the branch's own.` }))
375
+ }), { description: message`Push the current branch's commits to its draft, on a project whose code can be worked on outside Camox: a new draft on its first push.` });
376
+ /** A draft's branch, as a branch's upstream names it: `origin/<draft id>`. */
377
+ const DRAFT_UPSTREAM = new RegExp(`^origin/(${UUID})$`, "i");
378
+ /** What the spinner says while the push is at each step. */
379
+ const STEPS = {
380
+ checking: "Checking your commits...",
381
+ waiting: "Waiting for Camox's agent to finish its turn in the draft...",
382
+ building: "Building your code..."
1369
383
  };
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
- }
384
+ /** The Project ID the camox() plugin names in a Vite config. */
385
+ const PROJECT_ID = new RegExp(`\\bprojectId\\s*:\\s*["'](${UUID})["']`, "i");
386
+ const fail = (text) => {
387
+ p.log.error(text);
388
+ process.exit(1);
389
+ };
390
+ /** Runs `git` in the checkout, returning its trimmed output, or failing with `why`. */
391
+ function git(args, why, { quiet = false } = {}) {
392
+ const run = spawnSync("git", args, {
393
+ encoding: "utf8",
394
+ stdio: [
395
+ "ignore",
396
+ "pipe",
397
+ "pipe"
398
+ ]
1450
399
  });
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 === "deleted") {
1462
- printError({
1463
- code: DRAFT_ENDED_ERROR_CODE,
1464
- message: `${result.message} Nothing was published.`
400
+ if (run.error || run.status !== 0) {
401
+ if (quiet) return void 0;
402
+ return fail(`${why}${run.stderr ? `\n${run.stderr.trim()}` : ""}`);
403
+ }
404
+ return run.stdout.trim();
405
+ }
406
+ const listed = (errors) => errors.map((error) => {
407
+ const column = "column" in error ? `:${error.column}` : "";
408
+ return error.file ? `${error.file}:${error.line}${column} ${error.message}` : error.message;
409
+ }).join("\n");
410
+ async function handler$1(args) {
411
+ p.intro("camox push");
412
+ const root = git(["rev-parse", "--show-toplevel"], "Run camox push inside a project's checkout.");
413
+ let config;
414
+ try {
415
+ config = readFileSync(join(root, "vite.config.ts"), "utf8");
416
+ } catch {
417
+ return fail("This checkout has no vite.config.ts naming its project.");
418
+ }
419
+ const projectUuid = config.match(PROJECT_ID)?.[1]?.toLowerCase();
420
+ if (!projectUuid) return fail("This checkout's vite.config.ts doesn't name a Project ID.");
421
+ const branch = git([
422
+ "symbolic-ref",
423
+ "--quiet",
424
+ "--short",
425
+ "HEAD"
426
+ ], "Check out a branch to push.");
427
+ if (branch === "main") return fail("Push a branch of your own: main changes only by publishing.");
428
+ const upstream = git([
429
+ "rev-parse",
430
+ "--abbrev-ref",
431
+ "--symbolic-full-name",
432
+ `${branch}@{upstream}`
433
+ ], "", { quiet: true });
434
+ const draftId = args.draft ?? upstream?.match(DRAFT_UPSTREAM)?.[1]?.toLowerCase();
435
+ const { token } = await getOrAuthenticate().catch(() => fail("Authentication failed."));
436
+ git([
437
+ "fetch",
438
+ "--quiet",
439
+ "origin",
440
+ "main"
441
+ ], "Couldn't fetch main.");
442
+ const base = git([
443
+ "merge-base",
444
+ "HEAD",
445
+ "origin/main"
446
+ ], "Your branch shares no history with main.");
447
+ const draftFetched = !!draftId && git([
448
+ "fetch",
449
+ "--quiet",
450
+ "origin",
451
+ `+refs/heads/${draftId}:refs/remotes/origin/${draftId}`
452
+ ], "", { quiet: true }) !== void 0;
453
+ const scratch = mkdtempSync(join(tmpdir(), "camox-push-"));
454
+ let bundle;
455
+ try {
456
+ const file = join(scratch, "push.bundle");
457
+ git([
458
+ "bundle",
459
+ "create",
460
+ "--quiet",
461
+ file,
462
+ branch,
463
+ `^${base}`,
464
+ ...draftFetched ? [`^origin/${draftId}`] : []
465
+ ], "Your branch has no new commits to push.");
466
+ bundle = readFileSync(file);
467
+ } finally {
468
+ rmSync(scratch, {
469
+ recursive: true,
470
+ force: true
1465
471
  });
1466
- process.exit(1);
1467
472
  }
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));
473
+ const spinner = p.spinner();
474
+ spinner.start("Pushing your commits...");
475
+ let pushed;
476
+ try {
477
+ const events = await pushCode(token, {
478
+ projectUuid,
479
+ branch,
480
+ ...draftId && { draftId },
481
+ bundle: new File([new Uint8Array(bundle)], `${branch}.bundle`)
482
+ });
483
+ for await (const event of events) if (event.type === "progress") spinner.message(STEPS[event.step] ?? "Pushing...");
484
+ else pushed = event;
485
+ if (!pushed) throw new Error("Camox ended the push without landing it. Push again.");
486
+ } catch (error) {
487
+ spinner.error("Camox refused the push.");
488
+ const data = error.data;
489
+ return fail([error instanceof Error ? error.message : String(error), data?.build && listed(data.build)].filter(Boolean).join("\n"));
490
+ }
491
+ spinner.stop("Pushed.");
492
+ git([
493
+ "fetch",
494
+ "--quiet",
495
+ "origin",
496
+ pushed.draftId
497
+ ], "Couldn't fetch the draft's branch.");
498
+ git([
499
+ "branch",
500
+ `--set-upstream-to=origin/${pushed.draftId}`,
501
+ branch
502
+ ], "Couldn't set your branch's upstream to the draft's branch.");
503
+ const { result } = pushed;
504
+ if (result.ok) p.log.success("Your code built.");
505
+ else if (result.types.length > 0) p.log.warn(`Your code built, but has type errors, which block publishing:\n${listed(result.types)}`);
506
+ else p.log.warn(`Your code built, but the preview can't show it:\n${listed(result.build)}`);
507
+ p.outro(`Review your draft in the studio: ${pushed.studioUrl}`);
508
+ process.exit(0);
1478
509
  }
1479
510
  //#endregion
1480
511
  //#region src/commands/release.ts
1481
- const parser$1 = command("release", object({
512
+ const parser = command("release", object({
1482
513
  command: constant("release"),
1483
514
  dryRun: option("--dry-run", { description: message`Build the website as a release would, without logging in or syncing its definitions to Live.` })
1484
515
  }));
1485
- function handler$1(args) {
516
+ function handler(args) {
1486
517
  p.intro(args.dryRun ? "camox release --dry-run" : "camox release");
1487
518
  if (!args.dryRun && !process.env.CAMOX_DEPLOY_TOKEN) {
1488
519
  p.log.error("CAMOX_DEPLOY_TOKEN is required to release to Live.");
@@ -1509,100 +540,9 @@ function handler$1(args) {
1509
540
  process.exit(0);
1510
541
  }
1511
542
  //#endregion
1512
- //#region src/commands/status.ts
1513
- const parser = command("status", object({
1514
- command: constant("status"),
1515
- cwd: cwdFlag,
1516
- project: projectFlag,
1517
- json: option("--json")
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
- }
1547
- async function handler(args) {
1548
- let runtime;
1549
- try {
1550
- runtime = loadRuntime(args.cwd);
1551
- } catch (err) {
1552
- if (err instanceof RuntimeDirectoryError) {
1553
- printError({
1554
- code: "INVALID_CWD",
1555
- message: err.message
1556
- });
1557
- process.exit(2);
1558
- }
1559
- if (err instanceof RuntimeNotFoundError || err instanceof RuntimeMalformedError) {
1560
- printError({
1561
- code: "RUNTIME_NOT_FOUND",
1562
- message: err.message
1563
- });
1564
- process.exit(2);
1565
- }
1566
- throw err;
1567
- }
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;
1576
- const status = {
1577
- project,
1578
- draft: DEVELOPMENT_DRAFT,
1579
- developmentDraft,
1580
- apiUrl: runtime.apiUrl,
1581
- authenticationUrl: runtime.authenticationUrl,
1582
- authenticated: token !== null,
1583
- user: token ? {
1584
- name: token.name,
1585
- email: token.email
1586
- } : null
1587
- };
1588
- if (args.json || !process.stdout.isTTY) {
1589
- process.stdout.write(`${JSON.stringify(status, null, process.stdout.isTTY ? 2 : 0)}\n`);
1590
- process.exit(0);
1591
- }
1592
- const lines = [
1593
- `project: ${status.project}`,
1594
- `draft: ${describeDraft(developmentDraft, status.authenticationUrl)}`,
1595
- `api: ${status.apiUrl}`,
1596
- `auth: ${status.authenticationUrl}`,
1597
- status.user ? `signed in: ${status.user.name} <${status.user.email}>` : `signed in: (no token for ${status.authenticationUrl} — run \`camox login\`)`
1598
- ];
1599
- process.stdout.write(`${lines.join("\n")}\n`);
1600
- process.exit(0);
1601
- }
1602
- //#endregion
1603
543
  //#region src/index.ts
1604
544
  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),
545
+ parser: or(parser$3, parser$2, parser, parser$5, parser$1, parser$4),
1606
546
  metadata: {
1607
547
  name: "camox",
1608
548
  brief: message`Camox CLI`
@@ -1610,63 +550,22 @@ const program = defineProgram({
1610
550
  });
1611
551
  const result = runSync(program, { help: "both" });
1612
552
  switch (result.command) {
1613
- case "init":
1614
- handler$9();
1615
- break;
1616
553
  case "login":
1617
- await handler$6();
554
+ await handler$3();
1618
555
  break;
1619
556
  case "logout":
1620
- handler$5(result);
557
+ handler$2(result);
1621
558
  break;
1622
559
  case "release":
1623
- handler$1(result);
1624
- break;
1625
- case "preview":
1626
- await handler$3(result);
1627
- break;
1628
- case "status":
1629
- await handler(result);
1630
- break;
1631
- case "pages.list":
1632
- case "pages.get":
1633
- case "pages.create":
1634
- case "pages.update":
1635
- case "pages.set-seo":
1636
- case "pages.set-layout":
1637
- case "pages.delete":
1638
- await handler$4(result);
1639
- break;
1640
- case "blocks.types":
1641
- case "blocks.describe":
1642
- case "blocks.get":
1643
- case "blocks.get-many":
1644
- case "blocks.create":
1645
- case "blocks.edit":
1646
- case "blocks.move":
1647
- case "blocks.delete":
1648
- await handler$12(result);
1649
- break;
1650
- case "files.upload":
1651
- case "files.update":
1652
- case "files.get":
1653
- case "files.list":
1654
- await handler$10(result);
1655
- break;
1656
- case "layouts.list":
1657
- case "layouts.get":
1658
- await handler$8(result);
560
+ handler(result);
1659
561
  break;
1660
- case "locales.list":
1661
- case "locales.add":
1662
- case "locales.remove":
1663
- case "locales.set-default-tag":
1664
- await handler$7(result);
562
+ case "clone":
563
+ await handler$5(result);
1665
564
  break;
1666
- case "publish":
1667
- await handler$2(result);
565
+ case "push":
566
+ await handler$1(result);
1668
567
  break;
1669
- case "delete": await handler$11(result);
568
+ case "git-credential": await handler$4(result);
1670
569
  }
1671
570
  //#endregion
1672
571
  export {};