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