@camox/cli 0.43.0 → 0.45.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -40
- package/dist/api-DNIQC1ea.mjs +104 -0
- package/dist/api-DeE66CPa.mjs +2 -0
- package/dist/auth-core-xLeorUUV.mjs +120 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +673 -705
- 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/dist/auth-core-cCuk5aZy.mjs +0 -63
- 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
|
|
305
|
-
const token = readAuthTokenForUrl(runtime.authenticationUrl);
|
|
306
|
-
if (!token) return fail$
|
|
329
|
+
const project = opts.projectFlag?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectId;
|
|
330
|
+
const token = readAuthTokenForUrl(runtime.authenticationUrl, opts.cwd);
|
|
331
|
+
if (!token) return fail$2({
|
|
307
332
|
code: "NOT_AUTHENTICATED",
|
|
308
333
|
message: `No stored credentials for ${runtime.authenticationUrl}. Run \`camox login\` against this backend first.`
|
|
309
334
|
}, 2);
|
|
310
|
-
|
|
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$9 = command("blocks", or(command("types", object({
|
|
397
|
+
const fieldFileFlag = multiple(option("--field-file", string({ metavar: "NAME=PATH" }), { description: message`Set the string field NAME to the raw contents of the file at PATH, without JSON escaping. Repeat to set several fields. Merges with --content; when both set a field, the file wins.` }));
|
|
398
|
+
const types = command("types", object({
|
|
347
399
|
command: constant("blocks.types"),
|
|
348
400
|
cwd: cwdFlag,
|
|
349
|
-
project: projectFlag
|
|
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$9(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$9(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$9(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$9(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$9(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$9(args) {
|
|
|
562
650
|
args: { id: args.id },
|
|
563
651
|
cwd,
|
|
564
652
|
projectFlag,
|
|
565
|
-
|
|
653
|
+
locale,
|
|
566
654
|
outputMode
|
|
567
655
|
});
|
|
568
656
|
}
|
|
@@ -572,59 +660,46 @@ async function handler$9(args) {
|
|
|
572
660
|
}
|
|
573
661
|
}
|
|
574
662
|
//#endregion
|
|
575
|
-
//#region src/commands/
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
const parser$8 = command("env", or(command("check", object({
|
|
580
|
-
command: constant("env.check"),
|
|
581
|
-
cwd: cwdFlag,
|
|
582
|
-
project: projectFlag$2,
|
|
583
|
-
json: jsonFlag$2
|
|
584
|
-
})), command("push", object({
|
|
585
|
-
command: constant("env.push"),
|
|
586
|
-
yes: yesFlag,
|
|
587
|
-
cwd: cwdFlag,
|
|
588
|
-
project: projectFlag$2,
|
|
589
|
-
json: jsonFlag$2
|
|
590
|
-
})), command("pull", object({
|
|
591
|
-
command: constant("env.pull"),
|
|
592
|
-
yes: yesFlag,
|
|
663
|
+
//#region src/commands/discard.ts
|
|
664
|
+
const parser$11 = command("discard", object({
|
|
665
|
+
command: constant("discard"),
|
|
666
|
+
yes: option("--yes", "-y", { description: message`Discard without asking for confirmation.` }),
|
|
593
667
|
cwd: cwdFlag,
|
|
594
|
-
project: projectFlag
|
|
595
|
-
json:
|
|
596
|
-
}))
|
|
597
|
-
|
|
668
|
+
project: projectFlag,
|
|
669
|
+
json: option("--json")
|
|
670
|
+
}), { description: message`Discard every change in your development draft. Live is untouched.` });
|
|
671
|
+
const WARNING = "This discards every change in your development draft and can't be undone.";
|
|
672
|
+
async function confirmed() {
|
|
673
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
674
|
+
printError({
|
|
675
|
+
code: "CONFIRMATION_REQUIRED",
|
|
676
|
+
message: `${WARNING} Re-run with --yes to confirm.`
|
|
677
|
+
});
|
|
678
|
+
process.exit(2);
|
|
679
|
+
}
|
|
680
|
+
return await p.confirm({
|
|
681
|
+
message: `${WARNING} Discard it?`,
|
|
682
|
+
initialValue: false
|
|
683
|
+
}) === true;
|
|
684
|
+
}
|
|
685
|
+
async function handler$11(args) {
|
|
598
686
|
const outputMode = args.json ? "json" : "auto";
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
toolName: "checkEnvironmentCompatibility",
|
|
604
|
-
args: {},
|
|
605
|
-
cwd,
|
|
606
|
-
projectFlag,
|
|
607
|
-
outputMode
|
|
687
|
+
if (!args.yes && !await confirmed()) {
|
|
688
|
+
printError({
|
|
689
|
+
code: "CANCELLED",
|
|
690
|
+
message: "Nothing was discarded."
|
|
608
691
|
});
|
|
609
|
-
|
|
610
|
-
case "env.pull": {
|
|
611
|
-
const isPush = args.command === "env.push";
|
|
612
|
-
if (!args.yes) {
|
|
613
|
-
printError({
|
|
614
|
-
code: "CONFIRMATION_REQUIRED",
|
|
615
|
-
message: `This will replace all content in ${isPush ? "production" : "your dev environment"} with ${isPush ? "your dev environment" : "production"}. This action cannot be undone. Re-run with --yes to confirm.`
|
|
616
|
-
});
|
|
617
|
-
process.exit(2);
|
|
618
|
-
}
|
|
619
|
-
return dispatch({
|
|
620
|
-
toolName: "replicateEnvironment",
|
|
621
|
-
args: { direction: isPush ? "push" : "pull" },
|
|
622
|
-
cwd,
|
|
623
|
-
projectFlag,
|
|
624
|
-
outputMode
|
|
625
|
-
});
|
|
626
|
-
}
|
|
692
|
+
process.exit(1);
|
|
627
693
|
}
|
|
694
|
+
const result = await callToolResult({
|
|
695
|
+
toolName: "discardDraft",
|
|
696
|
+
args: {},
|
|
697
|
+
cwd: args.cwd,
|
|
698
|
+
projectFlag: args.project
|
|
699
|
+
});
|
|
700
|
+
if (isJsonMode(outputMode)) printResult(result, outputMode);
|
|
701
|
+
else process.stdout.write("Discarded every change in your development draft.\n");
|
|
702
|
+
process.exit(0);
|
|
628
703
|
}
|
|
629
704
|
const TIMEOUT_MS = 12e4;
|
|
630
705
|
const MAX_REDIRECTS = 5;
|
|
@@ -754,13 +829,8 @@ async function uploadFile(context, options, targetId) {
|
|
|
754
829
|
body.set("file", await openAsBlob(path, { type }), options.filename ?? filename);
|
|
755
830
|
body.set("projectId", String(context.projectId));
|
|
756
831
|
if (options.alt !== void 0) body.set("alt", options.alt);
|
|
757
|
-
if (options.aiMetadata !== void 0 || options.alt !== void 0) body.set("
|
|
758
|
-
const headers =
|
|
759
|
-
Authorization: `Bearer ${context.token}`,
|
|
760
|
-
"x-camox-client": "cli",
|
|
761
|
-
"x-environment-name": context.environmentName
|
|
762
|
-
};
|
|
763
|
-
if (context.disableTelemetry) headers["x-camox-telemetry-disabled"] = "1";
|
|
832
|
+
if (options.aiMetadata !== void 0 || options.alt !== void 0) body.set("aiMetadata", String(options.alt === void 0 && options.aiMetadata === "on"));
|
|
833
|
+
const headers = developmentDraftHeaders(context.token, context.disableTelemetry);
|
|
764
834
|
let response;
|
|
765
835
|
try {
|
|
766
836
|
const endpoint = targetId === void 0 ? "/files/upload" : `/files/${targetId}/content`;
|
|
@@ -774,7 +844,10 @@ async function uploadFile(context, options, targetId) {
|
|
|
774
844
|
} catch {
|
|
775
845
|
return fail("UPLOAD_FAILED", targetId === void 0 ? "Media upload failed (network or timeout). Check files list before retrying." : `File replacement failed (network or timeout). Check files get --id ${targetId} before retrying.`);
|
|
776
846
|
}
|
|
777
|
-
if (!response.ok)
|
|
847
|
+
if (!response.ok) {
|
|
848
|
+
if (isNoDevelopmentDraft(await response.json().catch(() => null))) throw Object.assign(new Error(NO_DEVELOPMENT_DRAFT_CLI_ERROR.message), { __cliError: NO_DEVELOPMENT_DRAFT_CLI_ERROR });
|
|
849
|
+
fail("UPLOAD_FAILED", `${targetId === void 0 ? "Media upload" : "File replacement"} failed (HTTP ${response.status}).`);
|
|
850
|
+
}
|
|
778
851
|
return await response.json();
|
|
779
852
|
} finally {
|
|
780
853
|
if (temporaryDirectory) await rm(temporaryDirectory, {
|
|
@@ -787,49 +860,41 @@ async function uploadFile(context, options, targetId) {
|
|
|
787
860
|
//#region src/commands/files.ts
|
|
788
861
|
const common = {
|
|
789
862
|
cwd: cwdFlag,
|
|
790
|
-
project:
|
|
791
|
-
production: option("--production"),
|
|
863
|
+
project: projectFlag,
|
|
792
864
|
json: option("--json")
|
|
793
865
|
};
|
|
794
866
|
const metadata = {
|
|
795
|
-
alt: optional(option("--alt", string({ metavar: "TEXT" }), { description: message`Alt text (including an empty string).
|
|
796
|
-
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.` }))
|
|
797
869
|
};
|
|
798
|
-
const parser$
|
|
870
|
+
const parser$10 = command("files", or(command("upload", object({
|
|
799
871
|
command: constant("files.upload"),
|
|
800
872
|
file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Local file to upload. Pass exactly one of --file or --url.` })),
|
|
801
873
|
url: optional(option("--url", string({ metavar: "URL" }), { description: message`HTTP(S) URL to download and copy into Camox, not an external reference.` })),
|
|
802
874
|
filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Override the local basename or final URL path filename.` })),
|
|
803
875
|
...metadata,
|
|
804
876
|
...common
|
|
805
|
-
}), { description: message`Upload
|
|
877
|
+
}), { description: message`Upload a file (maximum 100 MiB). Example: camox files upload --url https://example.com/hero.webp --alt "Product screenshot". Returns the stored file ID and URL. Without metadata options, raster images use automatic metadata.` }), command("update", object({
|
|
806
878
|
command: constant("files.update"),
|
|
807
|
-
id: option("--id",
|
|
808
|
-
min: 1,
|
|
809
|
-
metavar: "ID"
|
|
810
|
-
})),
|
|
879
|
+
id: option("--id", uuidv7()),
|
|
811
880
|
file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Replace file content from a local path, keeping its ID and references. Mutually exclusive with --url.` })),
|
|
812
881
|
url: optional(option("--url", string({ metavar: "URL" }), { description: message`Download and replace file content from an HTTP(S) URL, keeping its ID and references. Mutually exclusive with --file.` })),
|
|
813
|
-
filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Set the filename. Metadata-only renames preserve the URL; replacement uses the source filename unless overridden
|
|
882
|
+
filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Set the filename. Metadata-only renames preserve the URL and make alt text and filename manual; replacement uses the source filename unless overridden.` })),
|
|
814
883
|
...metadata,
|
|
815
884
|
...common
|
|
816
|
-
}), { description: message`Update metadata or replace file content with --file or --url (maximum 100 MiB). Replacing affects every reference to this file in
|
|
885
|
+
}), { description: message`Update metadata or replace file content with --file or --url (maximum 100 MiB). Replacing affects every reference to this file in your development draft; its ID stays unchanged but its URL changes. Omitted alt text and AI settings are preserved. Example: camox files update --id <FILE_ID> --file ./hero.jpg --alt "Updated description". Use --alt "" for intentionally empty alt text.` }), command("get", object({
|
|
817
886
|
command: constant("files.get"),
|
|
818
|
-
id: option("--id",
|
|
819
|
-
min: 1,
|
|
820
|
-
metavar: "ID"
|
|
821
|
-
})),
|
|
887
|
+
id: option("--id", uuidv7()),
|
|
822
888
|
...common
|
|
823
|
-
}), { description: message`Inspect a file in the selected project
|
|
889
|
+
}), { description: message`Inspect a file in the selected project.` }), command("list", object({
|
|
824
890
|
command: constant("files.list"),
|
|
825
891
|
...common
|
|
826
|
-
}), { description: message`List files in the selected project
|
|
827
|
-
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) {
|
|
828
894
|
const outputMode = args.json ? "json" : "auto";
|
|
829
895
|
const options = {
|
|
830
896
|
cwd: args.cwd,
|
|
831
897
|
projectFlag: args.project,
|
|
832
|
-
production: args.production,
|
|
833
898
|
outputMode
|
|
834
899
|
};
|
|
835
900
|
try {
|
|
@@ -855,6 +920,13 @@ async function handler$7(args) {
|
|
|
855
920
|
}
|
|
856
921
|
validateMetadata(args);
|
|
857
922
|
validateFilename(args.filename);
|
|
923
|
+
if (args.filename !== void 0 && args.aiMetadata === "on") {
|
|
924
|
+
printError({
|
|
925
|
+
code: "INVALID_ARGS",
|
|
926
|
+
message: "--filename cannot be combined with --ai-metadata on."
|
|
927
|
+
});
|
|
928
|
+
process.exit(2);
|
|
929
|
+
}
|
|
858
930
|
if (args.alt === void 0 && args.aiMetadata === void 0 && args.filename === void 0) {
|
|
859
931
|
printError({
|
|
860
932
|
code: "INVALID_ARGS",
|
|
@@ -869,7 +941,7 @@ async function handler$7(args) {
|
|
|
869
941
|
id: args.id,
|
|
870
942
|
alt: args.alt,
|
|
871
943
|
filename: args.filename,
|
|
872
|
-
|
|
944
|
+
aiMetadata: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
|
|
873
945
|
}
|
|
874
946
|
});
|
|
875
947
|
} catch (err) {
|
|
@@ -879,386 +951,115 @@ async function handler$7(args) {
|
|
|
879
951
|
}
|
|
880
952
|
}
|
|
881
953
|
//#endregion
|
|
882
|
-
//#region src/lib/agent-skills.ts
|
|
883
|
-
function createAgentSkillLinks(targetDir) {
|
|
884
|
-
const agentsSkillsDir = path.join(targetDir, ".agents", "skills");
|
|
885
|
-
const claudeSkillsDir = path.join(targetDir, ".claude", "skills");
|
|
886
|
-
fs.mkdirSync(agentsSkillsDir, { recursive: true });
|
|
887
|
-
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
888
|
-
for (const skill of ["camox", "camoxify"]) {
|
|
889
|
-
fs.symlinkSync(`../../node_modules/camox/skills/${skill}`, path.join(agentsSkillsDir, skill));
|
|
890
|
-
fs.symlinkSync(`../../.agents/skills/${skill}`, path.join(claudeSkillsDir, skill));
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
//#endregion
|
|
894
|
-
//#region src/lib/utils.ts
|
|
895
|
-
const packageManagerVersions = {
|
|
896
|
-
pnpm: "pnpm@11.1.3",
|
|
897
|
-
bun: "bun@1.2.23",
|
|
898
|
-
npm: "npm@11.6.2"
|
|
899
|
-
};
|
|
900
|
-
const pmCommands = {
|
|
901
|
-
pnpm: {
|
|
902
|
-
install: {
|
|
903
|
-
bin: "corepack",
|
|
904
|
-
args: ["pnpm", "install"],
|
|
905
|
-
display: "pnpm install",
|
|
906
|
-
fallback: {
|
|
907
|
-
bin: "pnpm",
|
|
908
|
-
args: ["install"]
|
|
909
|
-
}
|
|
910
|
-
},
|
|
911
|
-
dev: {
|
|
912
|
-
bin: "corepack",
|
|
913
|
-
args: ["pnpm", "dev"],
|
|
914
|
-
display: "pnpm dev",
|
|
915
|
-
fallback: {
|
|
916
|
-
bin: "pnpm",
|
|
917
|
-
args: ["dev"]
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
},
|
|
921
|
-
bun: {
|
|
922
|
-
install: {
|
|
923
|
-
bin: "bun",
|
|
924
|
-
args: ["install"],
|
|
925
|
-
display: "bun install"
|
|
926
|
-
},
|
|
927
|
-
dev: {
|
|
928
|
-
bin: "bun",
|
|
929
|
-
args: ["dev"],
|
|
930
|
-
display: "bun dev"
|
|
931
|
-
}
|
|
932
|
-
},
|
|
933
|
-
npm: {
|
|
934
|
-
install: {
|
|
935
|
-
bin: "npm",
|
|
936
|
-
args: ["install"],
|
|
937
|
-
display: "npm install"
|
|
938
|
-
},
|
|
939
|
-
dev: {
|
|
940
|
-
bin: "npm",
|
|
941
|
-
args: ["run", "dev"],
|
|
942
|
-
display: "npm run dev"
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
};
|
|
946
|
-
function copyDir(src, dest, replacements) {
|
|
947
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
948
|
-
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
949
|
-
const srcPath = path.join(src, entry.name);
|
|
950
|
-
const destPath = path.join(dest, entry.name);
|
|
951
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
952
|
-
copyDir(srcPath, destPath, replacements);
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
let content = fs.readFileSync(srcPath, "utf-8");
|
|
956
|
-
for (const [key, value] of Object.entries(replacements)) content = content.replaceAll(key, value);
|
|
957
|
-
fs.writeFileSync(destPath, content);
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
//#endregion
|
|
961
954
|
//#region src/commands/init.ts
|
|
962
|
-
const parser$
|
|
963
|
-
const handler$
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
}
|
|
976
|
-
};
|
|
977
|
-
function onCancel() {
|
|
978
|
-
p.cancel("Cancelled.");
|
|
979
|
-
process.exit(0);
|
|
980
|
-
}
|
|
981
|
-
function runCommand(bin, args, cwd) {
|
|
982
|
-
return new Promise((resolve, reject) => {
|
|
983
|
-
const child = spawn(bin, args, {
|
|
984
|
-
cwd,
|
|
985
|
-
stdio: "ignore"
|
|
986
|
-
});
|
|
987
|
-
child.on("close", (code) => {
|
|
988
|
-
if (code === 0) resolve();
|
|
989
|
-
else reject(/* @__PURE__ */ new Error(`Exit code ${code}`));
|
|
990
|
-
});
|
|
991
|
-
child.on("error", (err) => reject(new CommandError(`Failed to start ${bin}`, err)));
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
function spawnCommand(bin, args, cwd) {
|
|
995
|
-
return spawn(bin, args, {
|
|
996
|
-
cwd,
|
|
997
|
-
stdio: "inherit",
|
|
998
|
-
detached: true
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
1001
|
-
function isMissingCommandError(err) {
|
|
1002
|
-
if (!(err instanceof CommandError)) return false;
|
|
1003
|
-
const cause = err.cause;
|
|
1004
|
-
if (!(cause instanceof Error) || "code" in cause === false) return false;
|
|
1005
|
-
return cause.code === "ENOENT";
|
|
1006
|
-
}
|
|
1007
|
-
async function runPackageManagerCommand(command, cwd) {
|
|
1008
|
-
try {
|
|
1009
|
-
await runCommand(command.bin, command.args, cwd);
|
|
1010
|
-
} catch (err) {
|
|
1011
|
-
if (!isMissingCommandError(err) || !command.fallback) throw err;
|
|
1012
|
-
await runCommand(command.fallback.bin, command.fallback.args, cwd);
|
|
1013
|
-
}
|
|
1014
|
-
}
|
|
1015
|
-
function startPackageManagerCommand(command, cwd) {
|
|
1016
|
-
return new Promise((resolve, reject) => {
|
|
1017
|
-
const child = spawnCommand(command.bin, command.args, cwd);
|
|
1018
|
-
child.once("spawn", () => resolve(child));
|
|
1019
|
-
child.once("error", (err) => {
|
|
1020
|
-
const commandError = new CommandError(`Failed to start ${command.bin}`, err);
|
|
1021
|
-
if (!isMissingCommandError(commandError) || !command.fallback) {
|
|
1022
|
-
reject(commandError);
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
const fallback = spawnCommand(command.fallback.bin, command.fallback.args, cwd);
|
|
1026
|
-
fallback.once("spawn", () => resolve(fallback));
|
|
1027
|
-
fallback.once("error", (fallbackErr) => reject(new CommandError(`Failed to start ${command.fallback?.bin}`, fallbackErr)));
|
|
1028
|
-
});
|
|
1029
|
-
});
|
|
1030
|
-
}
|
|
1031
|
-
function getCommandFailureMessage(pm, err) {
|
|
1032
|
-
if (!isMissingCommandError(err)) return "Failed to install dependencies.";
|
|
1033
|
-
if (pm === "pnpm") return "Corepack or pnpm is required to install dependencies with the selected package manager. Enable Corepack with `corepack enable` or install pnpm, then run the setup commands below.";
|
|
1034
|
-
if (pm === "bun") return "Bun is required to install dependencies with the selected package manager. Install Bun, then run the setup commands below.";
|
|
1035
|
-
return "npm is required to install dependencies. Install npm, then run the setup commands below.";
|
|
1036
|
-
}
|
|
1037
|
-
const CREATE_NEW_ORG = "__create_new__";
|
|
1038
|
-
async function selectOrCreateOrganization(token) {
|
|
1039
|
-
const orgs = await listOrganizations(token);
|
|
1040
|
-
if (orgs.length === 0) {
|
|
1041
|
-
p.log.info("You don't have any organizations yet. Let's create one.");
|
|
1042
|
-
return promptCreateOrganization(token);
|
|
1043
|
-
}
|
|
1044
|
-
const selected = await p.select({
|
|
1045
|
-
message: "Select an organization for your new project",
|
|
1046
|
-
options: [...orgs.map((org) => ({
|
|
1047
|
-
value: org.id,
|
|
1048
|
-
label: `${org.name} (${org.slug})`
|
|
1049
|
-
})), {
|
|
1050
|
-
value: CREATE_NEW_ORG,
|
|
1051
|
-
label: "Create a new organization"
|
|
1052
|
-
}]
|
|
1053
|
-
});
|
|
1054
|
-
if (p.isCancel(selected)) return onCancel();
|
|
1055
|
-
if (selected === CREATE_NEW_ORG) return promptCreateOrganization(token);
|
|
1056
|
-
const org = orgs.find((o) => o.id === selected);
|
|
1057
|
-
await setActiveOrganization(token, org.id);
|
|
1058
|
-
return org.id;
|
|
1059
|
-
}
|
|
1060
|
-
async function promptCreateOrganization(token) {
|
|
1061
|
-
const orgName = await p.text({
|
|
1062
|
-
message: "Organization name",
|
|
1063
|
-
placeholder: "My Company",
|
|
1064
|
-
validate: (value) => {
|
|
1065
|
-
if (!value.trim()) return "Organization name is required";
|
|
1066
|
-
}
|
|
1067
|
-
});
|
|
1068
|
-
if (p.isCancel(orgName)) return onCancel();
|
|
1069
|
-
const org = await createOrganization(token, orgName, slugify(orgName, {
|
|
1070
|
-
lower: true,
|
|
1071
|
-
strict: true
|
|
1072
|
-
}));
|
|
1073
|
-
p.log.success(`Created organization: ${org.name}`);
|
|
1074
|
-
return org.id;
|
|
1075
|
-
}
|
|
1076
|
-
async function init() {
|
|
1077
|
-
p.intro(`Camox v${ownPkg.version}`);
|
|
1078
|
-
const stored = readAuthToken();
|
|
1079
|
-
if (stored) p.log.info(`Welcome back, ${stored.name}!`);
|
|
1080
|
-
p.log.info("Let's create your Camox application.");
|
|
1081
|
-
const name = await p.text({
|
|
1082
|
-
message: "Project display name",
|
|
1083
|
-
placeholder: "My Website",
|
|
1084
|
-
validate: (value) => {
|
|
1085
|
-
if (!value.trim()) return "Project name is required";
|
|
1086
|
-
}
|
|
1087
|
-
});
|
|
1088
|
-
if (p.isCancel(name)) return onCancel();
|
|
1089
|
-
const auth = await getOrAuthenticate();
|
|
1090
|
-
const orgId = await selectOrCreateOrganization(auth.token);
|
|
1091
|
-
let projectSlug;
|
|
1092
|
-
while (true) {
|
|
1093
|
-
const slugInput = await p.text({
|
|
1094
|
-
message: "Project slug",
|
|
1095
|
-
initialValue: slugify(name, {
|
|
1096
|
-
lower: true,
|
|
1097
|
-
strict: true
|
|
1098
|
-
}) || "my-site",
|
|
1099
|
-
validate: (value) => {
|
|
1100
|
-
if (!value.trim()) return "Slug is required";
|
|
1101
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value)) return "Slug must be lowercase alphanumeric with hyphens";
|
|
1102
|
-
}
|
|
1103
|
-
});
|
|
1104
|
-
if (p.isCancel(slugInput)) return onCancel();
|
|
1105
|
-
const s = p.spinner();
|
|
1106
|
-
s.start("Checking slug availability...");
|
|
1107
|
-
const { available } = await checkSlugAvailability(auth.token, slugInput);
|
|
1108
|
-
if (available) {
|
|
1109
|
-
s.stop("Slug is available!");
|
|
1110
|
-
projectSlug = slugInput;
|
|
1111
|
-
break;
|
|
1112
|
-
}
|
|
1113
|
-
s.stop(`Slug "${slugInput}" is already taken. Please choose another.`);
|
|
1114
|
-
}
|
|
1115
|
-
const projectPath = await p.text({
|
|
1116
|
-
message: "Project path",
|
|
1117
|
-
initialValue: `./${projectSlug}`,
|
|
1118
|
-
validate: (value) => {
|
|
1119
|
-
if (!value.trim()) return "Path is required";
|
|
1120
|
-
const resolved = path.resolve(value);
|
|
1121
|
-
if (fs.existsSync(resolved) && fs.readdirSync(resolved).length > 0) return "Directory is not empty";
|
|
1122
|
-
}
|
|
1123
|
-
});
|
|
1124
|
-
if (p.isCancel(projectPath)) return onCancel();
|
|
1125
|
-
const resolvedPath = projectPath;
|
|
1126
|
-
const targetDir = path.resolve(resolvedPath);
|
|
1127
|
-
const s0 = p.spinner();
|
|
1128
|
-
s0.start("Creating project...");
|
|
1129
|
-
let project;
|
|
1130
|
-
try {
|
|
1131
|
-
project = await createProject(auth.token, name, projectSlug, orgId);
|
|
1132
|
-
s0.stop(`Project created with slug: ${project.slug}`);
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
s0.stop("Failed to create project.");
|
|
1135
|
-
p.log.error(err instanceof Error ? err.message : "Unknown error");
|
|
1136
|
-
process.exit(1);
|
|
1137
|
-
}
|
|
1138
|
-
const selected = await p.select({
|
|
1139
|
-
message: "Which package manager?",
|
|
1140
|
-
options: [
|
|
1141
|
-
{
|
|
1142
|
-
value: "pnpm",
|
|
1143
|
-
label: "pnpm (recommended)"
|
|
1144
|
-
},
|
|
1145
|
-
{
|
|
1146
|
-
value: "bun",
|
|
1147
|
-
label: "bun"
|
|
1148
|
-
},
|
|
1149
|
-
{
|
|
1150
|
-
value: "npm",
|
|
1151
|
-
label: "npm"
|
|
1152
|
-
}
|
|
1153
|
-
]
|
|
1154
|
-
});
|
|
1155
|
-
if (p.isCancel(selected)) return onCancel();
|
|
1156
|
-
const pm = selected;
|
|
1157
|
-
const s = p.spinner();
|
|
1158
|
-
s.start("Scaffolding project...");
|
|
1159
|
-
copyDir(path.resolve(__dirname, "..", "templates", "default"), targetDir, { "{{projectName}}": name });
|
|
1160
|
-
createAgentSkillLinks(targetDir);
|
|
1161
|
-
const viteConfigPath = path.join(targetDir, "vite.config.ts");
|
|
1162
|
-
let viteConfig = fs.readFileSync(viteConfigPath, "utf-8");
|
|
1163
|
-
viteConfig = viteConfig.replace(/"[^"]*"(,?)[ \t]*\/\/[ \t]*camox-cli:replace-slug.*$/gm, `"${project.slug}"$1`);
|
|
1164
|
-
viteConfig = viteConfig.replace(/^[ \t]*\/\/[ \t]*camox-cli:dev-only-start[ \t]*\r?\n[\s\S]*?^[ \t]*\/\/[ \t]*camox-cli:dev-only-end[ \t]*\r?\n/gm, "");
|
|
1165
|
-
fs.writeFileSync(viteConfigPath, viteConfig);
|
|
1166
|
-
const pkgPath = path.join(targetDir, "package.json");
|
|
1167
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
1168
|
-
pkg.name = project.slug;
|
|
1169
|
-
delete pkg.version;
|
|
1170
|
-
pkg.dependencies.camox = `^${ownPkg.version}`;
|
|
1171
|
-
pkg.packageManager = packageManagerVersions[pm];
|
|
1172
|
-
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
1173
|
-
if (pm === "pnpm") fs.writeFileSync(path.join(targetDir, "pnpm-workspace.yaml"), PNPM_WORKSPACE);
|
|
1174
|
-
fs.writeFileSync(path.join(targetDir, ".gitignore"), `node_modules
|
|
1175
|
-
.DS_Store
|
|
1176
|
-
dist
|
|
1177
|
-
dist-ssr
|
|
1178
|
-
*.local
|
|
1179
|
-
count.txt
|
|
1180
|
-
.env
|
|
1181
|
-
.nitro
|
|
1182
|
-
.tanstack
|
|
1183
|
-
.output
|
|
1184
|
-
.vinxi
|
|
1185
|
-
node_modules
|
|
1186
|
-
|
|
1187
|
-
# Auto generated by Camox
|
|
1188
|
-
src/camox/app.ts
|
|
1189
|
-
src/routes/(camox)/
|
|
1190
|
-
|
|
1191
|
-
# Auto generated by Tanstack Router
|
|
1192
|
-
src/routeTree.gen.ts
|
|
1193
|
-
`);
|
|
1194
|
-
s.stop("Project scaffolded!");
|
|
1195
|
-
const { install: installCmd, dev: devCmd } = pmCommands[pm];
|
|
1196
|
-
const s2 = p.spinner();
|
|
1197
|
-
s2.start(`Running ${installCmd.display}...`);
|
|
1198
|
-
try {
|
|
1199
|
-
await runPackageManagerCommand(installCmd, targetDir);
|
|
1200
|
-
s2.stop("Dependencies installed!");
|
|
1201
|
-
} catch (err) {
|
|
1202
|
-
s2.stop("Install failed.");
|
|
1203
|
-
p.log.error(getCommandFailureMessage(pm, err));
|
|
1204
|
-
p.outro(`To finish setup:\n cd ${resolvedPath}\n ${installCmd.display}\n ${devCmd.display}`);
|
|
1205
|
-
process.exit(1);
|
|
1206
|
-
}
|
|
1207
|
-
p.log.info(`Starting dev server... (Ctrl+C to stop)`);
|
|
1208
|
-
let child;
|
|
1209
|
-
try {
|
|
1210
|
-
child = await startPackageManagerCommand(devCmd, targetDir);
|
|
1211
|
-
} catch (err) {
|
|
1212
|
-
p.log.error(getCommandFailureMessage(pm, err));
|
|
1213
|
-
p.outro(`To start the dev server:\n cd ${resolvedPath}\n ${devCmd.display}`);
|
|
1214
|
-
process.exit(1);
|
|
1215
|
-
}
|
|
1216
|
-
const sigintHandler = () => {
|
|
1217
|
-
if (child.pid) try {
|
|
1218
|
-
process.kill(-child.pid, "SIGTERM");
|
|
1219
|
-
} catch {}
|
|
1220
|
-
};
|
|
1221
|
-
process.on("SIGINT", sigintHandler);
|
|
1222
|
-
process.on("exit", sigintHandler);
|
|
1223
|
-
child.on("close", () => {
|
|
1224
|
-
process.removeListener("SIGINT", sigintHandler);
|
|
1225
|
-
process.removeListener("exit", sigintHandler);
|
|
1226
|
-
p.outro(`To restart the dev server:\n cd ${resolvedPath}\n ${devCmd.display}`);
|
|
1227
|
-
process.exit(0);
|
|
1228
|
-
});
|
|
955
|
+
const parser$9 = command("init", object({ command: constant("init") }));
|
|
956
|
+
const handler$9 = init;
|
|
957
|
+
const DASHBOARD_URL = process.env.CAMOX_URL || "https://app.camox.dev";
|
|
958
|
+
/**
|
|
959
|
+
* Projects are created from the dashboard, which hosts them, so `camox init` (and
|
|
960
|
+
* `npm create camox`, which runs it) only says where to go. It exits non-zero, since it created
|
|
961
|
+
* nothing.
|
|
962
|
+
*/
|
|
963
|
+
function init() {
|
|
964
|
+
p.intro("camox init");
|
|
965
|
+
p.log.info(`Projects are now created from the Camox dashboard: ${DASHBOARD_URL}`);
|
|
966
|
+
p.outro("Nothing was created.");
|
|
967
|
+
process.exit(1);
|
|
1229
968
|
}
|
|
1230
969
|
//#endregion
|
|
1231
970
|
//#region src/commands/layouts.ts
|
|
1232
|
-
const
|
|
1233
|
-
const
|
|
1234
|
-
const productionFlag$1 = option("--production");
|
|
1235
|
-
const parser$5 = command("layouts", or(command("list", object({
|
|
971
|
+
const jsonFlag$2 = option("--json");
|
|
972
|
+
const list$2 = command("list", object({
|
|
1236
973
|
command: constant("layouts.list"),
|
|
1237
974
|
cwd: cwdFlag,
|
|
1238
|
-
project: projectFlag
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
975
|
+
project: projectFlag,
|
|
976
|
+
json: jsonFlag$2
|
|
977
|
+
}), { description: message`List layout metadata. Use layouts get --layout-id NAME with a returned layoutId to discover its blocks.` });
|
|
978
|
+
const get$1 = command("get", object({
|
|
979
|
+
command: constant("layouts.get"),
|
|
980
|
+
layoutId: option("--layout-id", string({ metavar: "NAME" })),
|
|
981
|
+
cwd: cwdFlag,
|
|
982
|
+
project: projectFlag,
|
|
983
|
+
locale: localeFlag,
|
|
984
|
+
json: jsonFlag$2
|
|
985
|
+
}), { description: message`Inspect a layout and its blocks, including shared navigation and footer IDs for blocks get and blocks edit. A layout is named by the layoutId its code gives it, as listed by layouts list.` });
|
|
986
|
+
const parser$8 = command("layouts", or(list$2, get$1));
|
|
987
|
+
async function handler$8(args) {
|
|
1243
988
|
const outputMode = args.json ? "json" : "auto";
|
|
1244
989
|
const cwd = args.cwd;
|
|
1245
990
|
const projectFlag = args.project;
|
|
1246
|
-
const
|
|
991
|
+
const locale = args.locale;
|
|
1247
992
|
switch (args.command) {
|
|
1248
993
|
case "layouts.list": return dispatch({
|
|
1249
994
|
toolName: "listLayouts",
|
|
1250
995
|
args: {},
|
|
1251
996
|
cwd,
|
|
1252
997
|
projectFlag,
|
|
1253
|
-
|
|
998
|
+
locale,
|
|
999
|
+
outputMode
|
|
1000
|
+
});
|
|
1001
|
+
case "layouts.get": return dispatch({
|
|
1002
|
+
toolName: "getLayout",
|
|
1003
|
+
args: { layoutId: args.layoutId },
|
|
1004
|
+
cwd,
|
|
1005
|
+
projectFlag,
|
|
1006
|
+
locale,
|
|
1254
1007
|
outputMode
|
|
1255
1008
|
});
|
|
1256
1009
|
}
|
|
1257
1010
|
}
|
|
1258
1011
|
//#endregion
|
|
1012
|
+
//#region src/commands/locales.ts
|
|
1013
|
+
const jsonFlag$1 = option("--json");
|
|
1014
|
+
const tagFlag = option("--tag", string({ metavar: "TAG" }), { description: message`A BCP 47 language tag, such as de, pt-BR or fr-CA.` });
|
|
1015
|
+
const list$1 = command("list", object({
|
|
1016
|
+
command: constant("locales.list"),
|
|
1017
|
+
cwd: cwdFlag,
|
|
1018
|
+
project: projectFlag,
|
|
1019
|
+
json: jsonFlag$1
|
|
1020
|
+
}), { description: message`List the draft's locales, the default locale first. Pass a locale's tag to --locale on pages, blocks and layouts commands to read or write its content.` });
|
|
1021
|
+
const add = command("add", object({
|
|
1022
|
+
command: constant("locales.add"),
|
|
1023
|
+
tag: tagFlag,
|
|
1024
|
+
cwd: cwdFlag,
|
|
1025
|
+
project: projectFlag,
|
|
1026
|
+
json: jsonFlag$1
|
|
1027
|
+
}), { description: message`Add a locale. Every page, block, repeatable item, synced block and collection record of the default locale gets a copy in it, keeping its id, which Camox translates in the background. Publishing puts the locale on Live. Example: camox locales add --tag de.` });
|
|
1028
|
+
const remove = command("remove", object({
|
|
1029
|
+
command: constant("locales.remove"),
|
|
1030
|
+
tag: tagFlag,
|
|
1031
|
+
cwd: cwdFlag,
|
|
1032
|
+
project: projectFlag,
|
|
1033
|
+
json: jsonFlag$1
|
|
1034
|
+
}), { description: message`Remove a locale with all of its content. Publishing deletes them from Live for good. The default locale can't be removed.` });
|
|
1035
|
+
const setDefaultTag = command("set-default-tag", object({
|
|
1036
|
+
command: constant("locales.set-default-tag"),
|
|
1037
|
+
tag: tagFlag,
|
|
1038
|
+
cwd: cwdFlag,
|
|
1039
|
+
project: projectFlag,
|
|
1040
|
+
json: jsonFlag$1
|
|
1041
|
+
}), { description: message`Correct the default locale's tag, such as from en to en-GB, on Live and in every draft at once. Only possible while it's the only locale.` });
|
|
1042
|
+
const parser$7 = command("locales", or(list$1, add, remove, setDefaultTag));
|
|
1043
|
+
const TOOLS = {
|
|
1044
|
+
"locales.list": "listLocales",
|
|
1045
|
+
"locales.add": "addLocale",
|
|
1046
|
+
"locales.remove": "removeLocale",
|
|
1047
|
+
"locales.set-default-tag": "setDefaultLocaleTag"
|
|
1048
|
+
};
|
|
1049
|
+
async function handler$7(args) {
|
|
1050
|
+
const outputMode = args.json ? "json" : "auto";
|
|
1051
|
+
return dispatch({
|
|
1052
|
+
toolName: TOOLS[args.command],
|
|
1053
|
+
args: "tag" in args ? { tag: args.tag } : {},
|
|
1054
|
+
cwd: args.cwd,
|
|
1055
|
+
projectFlag: args.project,
|
|
1056
|
+
outputMode
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
//#endregion
|
|
1259
1060
|
//#region src/commands/login.ts
|
|
1260
|
-
const parser$
|
|
1261
|
-
const handler$
|
|
1061
|
+
const parser$6 = command("login", object({ command: constant("login") }));
|
|
1062
|
+
const handler$6 = login;
|
|
1262
1063
|
async function login() {
|
|
1263
1064
|
p.intro("camox login");
|
|
1264
1065
|
try {
|
|
@@ -1272,11 +1073,11 @@ async function login() {
|
|
|
1272
1073
|
}
|
|
1273
1074
|
//#endregion
|
|
1274
1075
|
//#region src/commands/logout.ts
|
|
1275
|
-
const parser$
|
|
1076
|
+
const parser$5 = command("logout", object({
|
|
1276
1077
|
command: constant("logout"),
|
|
1277
1078
|
cwd: cwdFlag
|
|
1278
1079
|
}));
|
|
1279
|
-
const handler$
|
|
1080
|
+
const handler$5 = logout;
|
|
1280
1081
|
function logout(args = {}) {
|
|
1281
1082
|
p.intro("camox logout");
|
|
1282
1083
|
let authenticationUrl = null;
|
|
@@ -1289,121 +1090,99 @@ function logout(args = {}) {
|
|
|
1289
1090
|
}
|
|
1290
1091
|
if (!(error instanceof RuntimeNotFoundError)) throw error;
|
|
1291
1092
|
}
|
|
1292
|
-
const token = authenticationUrl ? readAuthTokenForUrl(authenticationUrl) : readAuthToken();
|
|
1093
|
+
const token = authenticationUrl ? readAuthTokenForUrl(authenticationUrl, args.cwd) : readAuthToken();
|
|
1293
1094
|
if (!token) {
|
|
1294
1095
|
log.error("Not logged in.");
|
|
1295
1096
|
return;
|
|
1296
1097
|
}
|
|
1297
|
-
if (authenticationUrl) removeAuthTokenForUrl(authenticationUrl);
|
|
1098
|
+
if (authenticationUrl) removeAuthTokenForUrl(authenticationUrl, args.cwd);
|
|
1298
1099
|
else removeAuthToken();
|
|
1299
1100
|
p.log.success(`Logged out from ${token.name}.`);
|
|
1300
1101
|
}
|
|
1301
1102
|
//#endregion
|
|
1302
1103
|
//#region src/commands/pages.ts
|
|
1303
|
-
const projectFlag = optional(option("--project", string({ metavar: "SLUG" })));
|
|
1304
1104
|
const jsonFlag = option("--json");
|
|
1305
|
-
const productionFlag = option("--production");
|
|
1306
|
-
const liveFlag = option("--live");
|
|
1307
1105
|
const list = command("list", object({
|
|
1308
1106
|
command: constant("pages.list"),
|
|
1309
1107
|
cwd: cwdFlag,
|
|
1310
1108
|
project: projectFlag,
|
|
1311
|
-
|
|
1109
|
+
locale: localeFlag,
|
|
1312
1110
|
json: jsonFlag
|
|
1313
1111
|
}));
|
|
1314
1112
|
const get = command("get", object({
|
|
1315
1113
|
command: constant("pages.get"),
|
|
1316
|
-
id: optional(option("--id",
|
|
1114
|
+
id: optional(option("--id", uuidv7())),
|
|
1317
1115
|
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1318
|
-
live: liveFlag,
|
|
1319
1116
|
cwd: cwdFlag,
|
|
1320
1117
|
project: projectFlag,
|
|
1321
|
-
|
|
1118
|
+
locale: localeFlag,
|
|
1322
1119
|
json: jsonFlag
|
|
1323
1120
|
}));
|
|
1324
|
-
const
|
|
1325
|
-
command: constant("pages.publish"),
|
|
1326
|
-
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1327
|
-
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1328
|
-
noLayout: option("--no-layout"),
|
|
1329
|
-
cwd: cwdFlag,
|
|
1330
|
-
project: projectFlag,
|
|
1331
|
-
production: productionFlag,
|
|
1332
|
-
json: jsonFlag
|
|
1333
|
-
}));
|
|
1334
|
-
const unpublish = command("unpublish", object({
|
|
1335
|
-
command: constant("pages.unpublish"),
|
|
1336
|
-
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1337
|
-
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1338
|
-
cwd: cwdFlag,
|
|
1339
|
-
project: projectFlag,
|
|
1340
|
-
production: productionFlag,
|
|
1341
|
-
json: jsonFlag
|
|
1342
|
-
}));
|
|
1343
|
-
const discardChanges = command("discard-changes", object({
|
|
1344
|
-
command: constant("pages.discard-changes"),
|
|
1345
|
-
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1346
|
-
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1347
|
-
cwd: cwdFlag,
|
|
1348
|
-
project: projectFlag,
|
|
1349
|
-
production: productionFlag,
|
|
1350
|
-
json: jsonFlag
|
|
1351
|
-
}));
|
|
1352
|
-
const parser$2 = command("pages", or(list, get, command("create", object({
|
|
1121
|
+
const create = command("create", object({
|
|
1353
1122
|
command: constant("pages.create"),
|
|
1354
1123
|
nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
|
|
1355
1124
|
pathSegment: option("--path-segment", string({ metavar: "SEGMENT" })),
|
|
1356
|
-
layoutId: option("--layout-id",
|
|
1357
|
-
parentPageId: optional(option("--parent-page-id",
|
|
1125
|
+
layoutId: option("--layout-id", string({ metavar: "NAME" })),
|
|
1126
|
+
parentPageId: optional(option("--parent-page-id", uuidv7())),
|
|
1358
1127
|
cwd: cwdFlag,
|
|
1359
1128
|
project: projectFlag,
|
|
1360
|
-
|
|
1129
|
+
locale: localeFlag,
|
|
1361
1130
|
json: jsonFlag
|
|
1362
|
-
}))
|
|
1131
|
+
}));
|
|
1132
|
+
const update = command("update", object({
|
|
1363
1133
|
command: constant("pages.update"),
|
|
1364
|
-
id: optional(option("--id",
|
|
1365
|
-
min: 1,
|
|
1366
|
-
metavar: "ID"
|
|
1367
|
-
}))),
|
|
1134
|
+
id: optional(option("--id", uuidv7())),
|
|
1368
1135
|
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1369
|
-
metaTitle: optional(option("--meta-title", string({ metavar: "TEXT" }), { description: message`Set the SEO title. An empty string clears it. Disables automatic SEO for both fields.` })),
|
|
1370
|
-
metaDescription: optional(option("--meta-description", string({ metavar: "TEXT" }), { description: message`Set the SEO description. An empty string clears it. Disables automatic SEO for both fields.` })),
|
|
1371
|
-
aiSeo: optional(option("--ai-seo", choice(["on", "off"]), { description: message`Enable or disable automatic SEO. Enabling schedules asynchronous generation; it cannot be combined with manual metadata. Disabling preserves current metadata.` })),
|
|
1372
1136
|
nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
|
|
1373
1137
|
pathSegment: optional(option("--path-segment", string({ metavar: "SEGMENT" }))),
|
|
1374
|
-
parentPageId: optional(option("--parent-page-id",
|
|
1138
|
+
parentPageId: optional(option("--parent-page-id", uuidv7())),
|
|
1139
|
+
cwd: cwdFlag,
|
|
1140
|
+
project: projectFlag,
|
|
1141
|
+
locale: localeFlag,
|
|
1142
|
+
json: jsonFlag
|
|
1143
|
+
}), { description: message`Update a page. Pass exactly one of --id or --path and at least one update field. Omitted fields are preserved. Example: camox pages update --path /about --nickname "About". Edit SEO metadata with pages set-seo.` });
|
|
1144
|
+
const setSeo = command("set-seo", object({
|
|
1145
|
+
command: constant("pages.set-seo"),
|
|
1146
|
+
id: optional(option("--id", uuidv7())),
|
|
1147
|
+
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1148
|
+
metaTitle: optional(option("--meta-title", string({ metavar: "TEXT" }), { description: message`Set the SEO title. Makes both fields manual.` })),
|
|
1149
|
+
metaDescription: optional(option("--meta-description", string({ metavar: "TEXT" }), { description: message`Set the SEO description. An empty string clears it. Makes both fields manual.` })),
|
|
1150
|
+
aiSeo: optional(option("--ai-seo", choice(["on", "off"]), { description: message`on makes both fields generated again and schedules asynchronous generation; it cannot be combined with --meta-title or --meta-description. off keeps the current values as manual.` })),
|
|
1375
1151
|
cwd: cwdFlag,
|
|
1376
1152
|
project: projectFlag,
|
|
1377
|
-
|
|
1153
|
+
locale: localeFlag,
|
|
1378
1154
|
json: jsonFlag
|
|
1379
|
-
}), { description: message`
|
|
1155
|
+
}), { description: message`Set a page's SEO metadata. The meta title and description are one group: both generated by AI, or both manual. Writing one field makes both fields manual, keeping the current value of the other. Pass exactly one of --id or --path and at least one of --meta-title, --meta-description or --ai-seo. An empty meta title blocks publishing. Example: camox pages set-seo --path /about --meta-title "About us" --meta-description "Meet the team". Read metadata with pages get.` });
|
|
1156
|
+
const setLayout = command("set-layout", object({
|
|
1380
1157
|
command: constant("pages.set-layout"),
|
|
1381
|
-
id: option("--id",
|
|
1382
|
-
layoutId: option("--layout-id",
|
|
1158
|
+
id: option("--id", uuidv7()),
|
|
1159
|
+
layoutId: option("--layout-id", string({ metavar: "NAME" })),
|
|
1383
1160
|
cwd: cwdFlag,
|
|
1384
1161
|
project: projectFlag,
|
|
1385
|
-
|
|
1162
|
+
locale: localeFlag,
|
|
1386
1163
|
json: jsonFlag
|
|
1387
|
-
}))
|
|
1164
|
+
}));
|
|
1165
|
+
const del = command("delete", object({
|
|
1388
1166
|
command: constant("pages.delete"),
|
|
1389
|
-
id: option("--id",
|
|
1167
|
+
id: option("--id", uuidv7()),
|
|
1390
1168
|
cwd: cwdFlag,
|
|
1391
1169
|
project: projectFlag,
|
|
1392
|
-
|
|
1170
|
+
locale: localeFlag,
|
|
1393
1171
|
json: jsonFlag
|
|
1394
|
-
}))
|
|
1395
|
-
|
|
1172
|
+
}));
|
|
1173
|
+
const parser$4 = command("pages", or(list, get, create, update, setSeo, setLayout, del));
|
|
1174
|
+
async function handler$4(args) {
|
|
1396
1175
|
const outputMode = args.json ? "json" : "auto";
|
|
1397
1176
|
const cwd = args.cwd;
|
|
1398
1177
|
const projectFlag = args.project;
|
|
1399
|
-
const
|
|
1178
|
+
const locale = args.locale;
|
|
1400
1179
|
switch (args.command) {
|
|
1401
1180
|
case "pages.list": return dispatch({
|
|
1402
1181
|
toolName: "listPages",
|
|
1403
1182
|
args: {},
|
|
1404
1183
|
cwd,
|
|
1405
1184
|
projectFlag,
|
|
1406
|
-
|
|
1185
|
+
locale,
|
|
1407
1186
|
outputMode
|
|
1408
1187
|
});
|
|
1409
1188
|
case "pages.get":
|
|
@@ -1416,13 +1195,10 @@ async function handler$2(args) {
|
|
|
1416
1195
|
}
|
|
1417
1196
|
return dispatch({
|
|
1418
1197
|
toolName: "getPage",
|
|
1419
|
-
args: {
|
|
1420
|
-
...args.id != null ? { id: args.id } : { path: args.path },
|
|
1421
|
-
source: args.live ? "live" : "draft"
|
|
1422
|
-
},
|
|
1198
|
+
args: args.id != null ? { id: args.id } : { path: args.path },
|
|
1423
1199
|
cwd,
|
|
1424
1200
|
projectFlag,
|
|
1425
|
-
|
|
1201
|
+
locale,
|
|
1426
1202
|
outputMode
|
|
1427
1203
|
});
|
|
1428
1204
|
case "pages.create": return dispatch({
|
|
@@ -1435,7 +1211,7 @@ async function handler$2(args) {
|
|
|
1435
1211
|
},
|
|
1436
1212
|
cwd,
|
|
1437
1213
|
projectFlag,
|
|
1438
|
-
|
|
1214
|
+
locale,
|
|
1439
1215
|
outputMode
|
|
1440
1216
|
});
|
|
1441
1217
|
case "pages.update":
|
|
@@ -1446,21 +1222,11 @@ async function handler$2(args) {
|
|
|
1446
1222
|
});
|
|
1447
1223
|
process.exit(2);
|
|
1448
1224
|
}
|
|
1449
|
-
if (args.aiSeo === "on" && (args.metaTitle !== void 0 || args.metaDescription !== void 0)) {
|
|
1450
|
-
printError({
|
|
1451
|
-
code: "INVALID_ARGS",
|
|
1452
|
-
message: "Cannot enable automatic SEO alongside manual metadata."
|
|
1453
|
-
});
|
|
1454
|
-
process.exit(2);
|
|
1455
|
-
}
|
|
1456
1225
|
if ([
|
|
1457
1226
|
args.nickname,
|
|
1458
1227
|
args.pathSegment,
|
|
1459
|
-
args.parentPageId
|
|
1460
|
-
|
|
1461
|
-
args.metaDescription,
|
|
1462
|
-
args.aiSeo
|
|
1463
|
-
].every((value) => value === void 0)) {
|
|
1228
|
+
args.parentPageId
|
|
1229
|
+
].every((v) => v === void 0)) {
|
|
1464
1230
|
printError({
|
|
1465
1231
|
code: "INVALID_ARGS",
|
|
1466
1232
|
message: "Pass at least one field to update."
|
|
@@ -1471,38 +1237,16 @@ async function handler$2(args) {
|
|
|
1471
1237
|
toolName: "updatePage",
|
|
1472
1238
|
args: {
|
|
1473
1239
|
...args.id != null ? { id: args.id } : { path: args.path },
|
|
1474
|
-
metaTitle: args.metaTitle,
|
|
1475
|
-
metaDescription: args.metaDescription,
|
|
1476
|
-
aiSeoEnabled: args.aiSeo === void 0 ? void 0 : args.aiSeo === "on",
|
|
1477
1240
|
nickname: args.nickname,
|
|
1478
1241
|
pathSegment: args.pathSegment,
|
|
1479
1242
|
parentPageId: args.parentPageId
|
|
1480
1243
|
},
|
|
1481
1244
|
cwd,
|
|
1482
1245
|
projectFlag,
|
|
1483
|
-
|
|
1246
|
+
locale,
|
|
1484
1247
|
outputMode
|
|
1485
1248
|
});
|
|
1486
|
-
case "pages.set-
|
|
1487
|
-
toolName: "setPageLayout",
|
|
1488
|
-
args: {
|
|
1489
|
-
id: args.id,
|
|
1490
|
-
layoutId: args.layoutId
|
|
1491
|
-
},
|
|
1492
|
-
cwd,
|
|
1493
|
-
projectFlag,
|
|
1494
|
-
production,
|
|
1495
|
-
outputMode
|
|
1496
|
-
});
|
|
1497
|
-
case "pages.delete": return dispatch({
|
|
1498
|
-
toolName: "deletePage",
|
|
1499
|
-
args: { id: args.id },
|
|
1500
|
-
cwd,
|
|
1501
|
-
projectFlag,
|
|
1502
|
-
production,
|
|
1503
|
-
outputMode
|
|
1504
|
-
});
|
|
1505
|
-
case "pages.publish":
|
|
1249
|
+
case "pages.set-seo": {
|
|
1506
1250
|
if (args.id == null === (args.path == null)) {
|
|
1507
1251
|
printError({
|
|
1508
1252
|
code: "INVALID_ARGS",
|
|
@@ -1510,77 +1254,258 @@ async function handler$2(args) {
|
|
|
1510
1254
|
});
|
|
1511
1255
|
process.exit(2);
|
|
1512
1256
|
}
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
args: {
|
|
1516
|
-
...args.id != null ? { id: args.id } : { path: args.path },
|
|
1517
|
-
alsoPublishLayout: !args.noLayout
|
|
1518
|
-
},
|
|
1519
|
-
cwd,
|
|
1520
|
-
projectFlag,
|
|
1521
|
-
production,
|
|
1522
|
-
outputMode
|
|
1523
|
-
});
|
|
1524
|
-
case "pages.unpublish":
|
|
1525
|
-
if (args.id == null === (args.path == null)) {
|
|
1257
|
+
const writesField = args.metaTitle !== void 0 || args.metaDescription !== void 0;
|
|
1258
|
+
if (!writesField && args.aiSeo === void 0) {
|
|
1526
1259
|
printError({
|
|
1527
1260
|
code: "INVALID_ARGS",
|
|
1528
|
-
message: "Pass
|
|
1261
|
+
message: "Pass at least one of --meta-title, --meta-description or --ai-seo."
|
|
1529
1262
|
});
|
|
1530
1263
|
process.exit(2);
|
|
1531
1264
|
}
|
|
1532
|
-
|
|
1533
|
-
toolName: "unpublishPage",
|
|
1534
|
-
args: args.id != null ? { id: args.id } : { path: args.path },
|
|
1535
|
-
cwd,
|
|
1536
|
-
projectFlag,
|
|
1537
|
-
production,
|
|
1538
|
-
outputMode
|
|
1539
|
-
});
|
|
1540
|
-
case "pages.discard-changes":
|
|
1541
|
-
if (args.id == null === (args.path == null)) {
|
|
1265
|
+
if (args.aiSeo === "on" && writesField) {
|
|
1542
1266
|
printError({
|
|
1543
1267
|
code: "INVALID_ARGS",
|
|
1544
|
-
message: "
|
|
1268
|
+
message: "--ai-seo on cannot be combined with --meta-title or --meta-description."
|
|
1545
1269
|
});
|
|
1546
1270
|
process.exit(2);
|
|
1547
1271
|
}
|
|
1548
1272
|
return dispatch({
|
|
1549
|
-
toolName: "
|
|
1550
|
-
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
|
+
},
|
|
1551
1280
|
cwd,
|
|
1552
1281
|
projectFlag,
|
|
1553
|
-
|
|
1282
|
+
locale,
|
|
1554
1283
|
outputMode
|
|
1555
1284
|
});
|
|
1285
|
+
}
|
|
1286
|
+
case "pages.set-layout": return dispatch({
|
|
1287
|
+
toolName: "setPageLayout",
|
|
1288
|
+
args: {
|
|
1289
|
+
id: args.id,
|
|
1290
|
+
layoutId: args.layoutId
|
|
1291
|
+
},
|
|
1292
|
+
cwd,
|
|
1293
|
+
projectFlag,
|
|
1294
|
+
locale,
|
|
1295
|
+
outputMode
|
|
1296
|
+
});
|
|
1297
|
+
case "pages.delete": return dispatch({
|
|
1298
|
+
toolName: "deletePage",
|
|
1299
|
+
args: { id: args.id },
|
|
1300
|
+
cwd,
|
|
1301
|
+
projectFlag,
|
|
1302
|
+
locale,
|
|
1303
|
+
outputMode
|
|
1304
|
+
});
|
|
1556
1305
|
}
|
|
1557
1306
|
}
|
|
1558
1307
|
//#endregion
|
|
1308
|
+
//#region src/commands/preview.ts
|
|
1309
|
+
const parser$3 = command("preview", object({
|
|
1310
|
+
command: constant("preview"),
|
|
1311
|
+
cwd: cwdFlag,
|
|
1312
|
+
url: option("--url", string()),
|
|
1313
|
+
json: option("--json")
|
|
1314
|
+
}), { description: message`Create a single-use sign-in URL for local verification. Open it in your automation browser within three minutes. The URL is a credential; do not share it.` });
|
|
1315
|
+
async function handler$3(args) {
|
|
1316
|
+
try {
|
|
1317
|
+
const url = new URL(args.url);
|
|
1318
|
+
if (!isLoopbackUrl(url)) throw new Error("Preview requires an http(s) loopback URL without credentials.");
|
|
1319
|
+
const runtime = loadRuntime(args.cwd);
|
|
1320
|
+
const auth = readAuthTokenForUrl(runtime.authenticationUrl, args.cwd);
|
|
1321
|
+
if (!auth?.email) {
|
|
1322
|
+
printError({
|
|
1323
|
+
code: "NOT_AUTHENTICATED",
|
|
1324
|
+
message: `Run camox login for ${runtime.authenticationUrl} first.`
|
|
1325
|
+
});
|
|
1326
|
+
process.exitCode = 2;
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
const target = {
|
|
1330
|
+
projectUuid: runtime.projectId,
|
|
1331
|
+
apiUrl: normalizeUrl(runtime.apiUrl)
|
|
1332
|
+
};
|
|
1333
|
+
printResult({
|
|
1334
|
+
url: await createPreviewSignInUrl(url.href, target, auth.token),
|
|
1335
|
+
projectId: target.projectUuid,
|
|
1336
|
+
apiUrl: target.apiUrl,
|
|
1337
|
+
warning: "Single-use sign-in credential, valid for three minutes. Open in your verification browser; do not share or commit."
|
|
1338
|
+
}, args.json ? "json" : "auto");
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
printError({
|
|
1341
|
+
code: "PREVIEW_FAILED",
|
|
1342
|
+
message: error instanceof Error ? error.message : "Could not create preview."
|
|
1343
|
+
});
|
|
1344
|
+
process.exitCode = 2;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
//#endregion
|
|
1348
|
+
//#region src/lib/publish-preview.ts
|
|
1349
|
+
function where(owner) {
|
|
1350
|
+
if (!owner) return "";
|
|
1351
|
+
switch (owner.kind) {
|
|
1352
|
+
case "page": return ` on page ${owner.path}`;
|
|
1353
|
+
case "layout": return ` in layout ${owner.name}`;
|
|
1354
|
+
case "syncedBlock": return ` in synced block ${owner.title}`;
|
|
1355
|
+
case "collection": return ` in collection ${owner.title}`;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
const subject = (item) => `${item.entity} "${item.label}"${where(item.owner)}`;
|
|
1359
|
+
const when = (at) => at === null ? "" : ` on ${new Date(at).toLocaleString()}`;
|
|
1360
|
+
function conflictLine(conflict) {
|
|
1361
|
+
if (conflict.liveVersion === null) return `${subject(conflict)}: deleted on Live`;
|
|
1362
|
+
if (conflict.release) return `${subject(conflict)}: changed by a release${when(conflict.publishedAt)}`;
|
|
1363
|
+
const by = conflict.publishedBy?.name ?? "someone";
|
|
1364
|
+
return `${subject(conflict)}: published by ${by}${when(conflict.publishedAt)}`;
|
|
1365
|
+
}
|
|
1366
|
+
const LOST_REASONS = {
|
|
1367
|
+
parentGone: "what it belongs to is gone from Live and the draft",
|
|
1368
|
+
writtenAfterDeletion: "published after the draft deleted what it belongs to"
|
|
1369
|
+
};
|
|
1370
|
+
function section(title, lines) {
|
|
1371
|
+
if (lines.length === 0) return [];
|
|
1372
|
+
return [
|
|
1373
|
+
`${title} (${lines.length})`,
|
|
1374
|
+
...lines.map((line) => ` - ${line}`),
|
|
1375
|
+
""
|
|
1376
|
+
];
|
|
1377
|
+
}
|
|
1378
|
+
const changeLine = (change) => `${change.change} ${subject(change)}`;
|
|
1379
|
+
/**
|
|
1380
|
+
* The draft's changes, under the locale each is in once the draft changes more than its default
|
|
1381
|
+
* locale: the default locale first, then the others, then what every locale shares, like files.
|
|
1382
|
+
*/
|
|
1383
|
+
function changesSection(preview) {
|
|
1384
|
+
const { changes, defaultLocale } = preview;
|
|
1385
|
+
const tags = new Set(changes.map((change) => change.locale));
|
|
1386
|
+
if ([...tags].every((tag) => tag === null || tag === defaultLocale)) return section("Changes", changes.map(changeLine));
|
|
1387
|
+
const rank = (tag) => tag === defaultLocale ? 0 : tag === null ? 2 : 1;
|
|
1388
|
+
const ordered = [...tags].sort((a, b) => rank(a) - rank(b) || (a ?? "").localeCompare(b ?? ""));
|
|
1389
|
+
return [
|
|
1390
|
+
`Changes (${changes.length})`,
|
|
1391
|
+
...ordered.flatMap((tag) => [` ${tag ?? "Every locale"}`, ...changes.filter((change) => change.locale === tag).map((change) => ` - ${changeLine(change)}`)]),
|
|
1392
|
+
""
|
|
1393
|
+
];
|
|
1394
|
+
}
|
|
1395
|
+
/** The preview as text, in the order ADR 0009 lists it, ending with the token. */
|
|
1396
|
+
function formatPreview(preview) {
|
|
1397
|
+
const lines = [
|
|
1398
|
+
...section("Blockers: fix these in your development draft before publishing", preview.blockers.map((problem) => `${subject(problem)}: ${problem.message}`)),
|
|
1399
|
+
...section("Conflicts: Live changed these since your draft did; publishing overwrites Live's version", preview.conflicts.map(conflictLine)),
|
|
1400
|
+
...section("Lost changes: publishing drops these", preview.lost.map((lost) => `${subject(lost)}: ${LOST_REASONS[lost.reason]}`)),
|
|
1401
|
+
...section("Warnings: already on Live, not blocking", preview.warnings.map((problem) => `${subject(problem)}: ${problem.message}`)),
|
|
1402
|
+
...changesSection(preview)
|
|
1403
|
+
];
|
|
1404
|
+
if (preview.changes.length === 0) lines.push("Your development draft has no changes.", "");
|
|
1405
|
+
lines.push(`Token: ${preview.token}`, ...nextStep(preview));
|
|
1406
|
+
return `${lines.join("\n")}\n`;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* What to do next: fix the blockers, or publish when there is something to publish, waiting for
|
|
1410
|
+
* running translations or publishing anyway.
|
|
1411
|
+
*/
|
|
1412
|
+
function nextStep(preview) {
|
|
1413
|
+
if (preview.blockers.length > 0) return ["Publishing is blocked until the blockers are fixed."];
|
|
1414
|
+
if (preview.changes.length === 0) return [];
|
|
1415
|
+
const publish = `camox publish --confirm ${preview.token}`;
|
|
1416
|
+
if (preview.translating.length > 0) return [`Translations into ${preview.translating.join(", ")} are still running: publish once they finish, or publish anyway and Camox finishes them in a draft of its own.`, `Publish anyway with: ${publish} --anyway`];
|
|
1417
|
+
return [`Publish with: ${publish}`];
|
|
1418
|
+
}
|
|
1419
|
+
//#endregion
|
|
1420
|
+
//#region src/commands/publish.ts
|
|
1421
|
+
const parser$2 = command("publish", object({
|
|
1422
|
+
command: constant("publish"),
|
|
1423
|
+
confirm: optional(option("--confirm", string({ metavar: "TOKEN" }), { description: message`Publish, with the token \`camox publish\` printed with the list you reviewed.` })),
|
|
1424
|
+
anyway: option("--anyway", { description: message`With --confirm, publish while translations are still running: Camox finishes them in a draft of its own and publishes it once they're done.` }),
|
|
1425
|
+
cwd: cwdFlag,
|
|
1426
|
+
project: projectFlag,
|
|
1427
|
+
json: option("--json")
|
|
1428
|
+
}), { description: message`List what publishing your development draft to Live would do, with a token; publish with --confirm <token>.` });
|
|
1429
|
+
async function handler$2(args) {
|
|
1430
|
+
const outputMode = args.json ? "json" : "auto";
|
|
1431
|
+
const target = {
|
|
1432
|
+
cwd: args.cwd,
|
|
1433
|
+
projectFlag: args.project
|
|
1434
|
+
};
|
|
1435
|
+
if (args.confirm === void 0) {
|
|
1436
|
+
printPreview(await callToolResult({
|
|
1437
|
+
...target,
|
|
1438
|
+
toolName: "previewPublish",
|
|
1439
|
+
args: {}
|
|
1440
|
+
}), outputMode);
|
|
1441
|
+
process.exit(0);
|
|
1442
|
+
}
|
|
1443
|
+
const result = await callToolResult({
|
|
1444
|
+
...target,
|
|
1445
|
+
toolName: "publishDraft",
|
|
1446
|
+
args: {
|
|
1447
|
+
token: args.confirm,
|
|
1448
|
+
...args.anyway && { publishAnyway: true }
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
if (result.published !== false) {
|
|
1452
|
+
if (isJsonMode(outputMode)) printResult(result, outputMode);
|
|
1453
|
+
else process.stdout.write("Published your development draft to Live.\n");
|
|
1454
|
+
process.exit(0);
|
|
1455
|
+
}
|
|
1456
|
+
if (result.reason === "empty") {
|
|
1457
|
+
if (isJsonMode(outputMode)) printResult(result, outputMode);
|
|
1458
|
+
else process.stdout.write("Your development draft has no changes: nothing to publish.\n");
|
|
1459
|
+
process.exit(0);
|
|
1460
|
+
}
|
|
1461
|
+
if (result.reason === "discarded") {
|
|
1462
|
+
printError({
|
|
1463
|
+
code: DRAFT_ENDED_ERROR_CODE,
|
|
1464
|
+
message: `${result.message} Nothing was published.`
|
|
1465
|
+
});
|
|
1466
|
+
process.exit(1);
|
|
1467
|
+
}
|
|
1468
|
+
printError({
|
|
1469
|
+
code: NOT_PUBLISHED_CODES[result.reason],
|
|
1470
|
+
message: `${result.message} Nothing was published.`
|
|
1471
|
+
});
|
|
1472
|
+
printPreview(result.preview, outputMode);
|
|
1473
|
+
process.exit(1);
|
|
1474
|
+
}
|
|
1475
|
+
function printPreview(preview, outputMode) {
|
|
1476
|
+
if (isJsonMode(outputMode)) return printResult(preview, outputMode);
|
|
1477
|
+
process.stdout.write(formatPreview(preview));
|
|
1478
|
+
}
|
|
1479
|
+
//#endregion
|
|
1559
1480
|
//#region src/commands/release.ts
|
|
1560
|
-
const parser$1 = command("release", object({
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1481
|
+
const parser$1 = command("release", object({
|
|
1482
|
+
command: constant("release"),
|
|
1483
|
+
dryRun: option("--dry-run", { description: message`Build the website as a release would, without logging in or syncing its definitions to Live.` })
|
|
1484
|
+
}));
|
|
1485
|
+
function handler$1(args) {
|
|
1486
|
+
p.intro(args.dryRun ? "camox release --dry-run" : "camox release");
|
|
1487
|
+
if (!args.dryRun && !process.env.CAMOX_DEPLOY_TOKEN) {
|
|
1488
|
+
p.log.error("CAMOX_DEPLOY_TOKEN is required to release to Live.");
|
|
1565
1489
|
process.exit(1);
|
|
1566
1490
|
}
|
|
1567
|
-
p.log.info("Building and syncing
|
|
1568
|
-
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"], {
|
|
1569
1494
|
stdio: "inherit",
|
|
1570
1495
|
env: {
|
|
1571
1496
|
...process.env,
|
|
1572
|
-
CAMOX_INTERNAL_RELEASE: "1"
|
|
1497
|
+
CAMOX_INTERNAL_RELEASE: args.dryRun ? "dry-run" : "1"
|
|
1573
1498
|
}
|
|
1574
1499
|
});
|
|
1575
1500
|
if (result.error) {
|
|
1576
|
-
p.log.error(`Could not start the
|
|
1501
|
+
p.log.error(`Could not start the release build: ${result.error.message}`);
|
|
1577
1502
|
process.exit(1);
|
|
1578
1503
|
}
|
|
1579
1504
|
if (result.status !== 0) {
|
|
1580
|
-
p.log.error("
|
|
1505
|
+
p.log.error(args.dryRun ? "Dry run failed." : "Release failed.");
|
|
1581
1506
|
process.exit(result.status ?? 1);
|
|
1582
1507
|
}
|
|
1583
|
-
p.outro("
|
|
1508
|
+
p.outro(args.dryRun ? "Dry run built successfully." : "Release prepared successfully.");
|
|
1584
1509
|
process.exit(0);
|
|
1585
1510
|
}
|
|
1586
1511
|
//#endregion
|
|
@@ -1588,9 +1513,37 @@ function handler$1() {
|
|
|
1588
1513
|
const parser = command("status", object({
|
|
1589
1514
|
command: constant("status"),
|
|
1590
1515
|
cwd: cwdFlag,
|
|
1591
|
-
|
|
1516
|
+
project: projectFlag,
|
|
1592
1517
|
json: option("--json")
|
|
1593
1518
|
}));
|
|
1519
|
+
async function developmentDraftStatus(params) {
|
|
1520
|
+
try {
|
|
1521
|
+
const project = await getProject(params.token, params.project, params.apiUrl);
|
|
1522
|
+
return {
|
|
1523
|
+
exists: true,
|
|
1524
|
+
changeCount: (await getDevelopmentDraft({
|
|
1525
|
+
...params,
|
|
1526
|
+
projectId: project.id
|
|
1527
|
+
})).changeCount
|
|
1528
|
+
};
|
|
1529
|
+
} catch (err) {
|
|
1530
|
+
if (isNoDevelopmentDraft(err)) return {
|
|
1531
|
+
exists: false,
|
|
1532
|
+
changeCount: 0
|
|
1533
|
+
};
|
|
1534
|
+
return {
|
|
1535
|
+
exists: null,
|
|
1536
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
function describeDraft(draft, authenticationUrl) {
|
|
1541
|
+
if (!draft) return `(sign in to see it — run \`camox login\` for ${authenticationUrl})`;
|
|
1542
|
+
if (draft.exists === null) return `(couldn't be checked: ${draft.error})`;
|
|
1543
|
+
if (!draft.exists) return "no development draft yet — run your site locally once to create it";
|
|
1544
|
+
if (draft.changeCount === 0) return "no changes";
|
|
1545
|
+
return `${draft.changeCount} change${draft.changeCount === 1 ? "" : "s"} — \`camox publish\` lists them`;
|
|
1546
|
+
}
|
|
1594
1547
|
async function handler(args) {
|
|
1595
1548
|
let runtime;
|
|
1596
1549
|
try {
|
|
@@ -1612,11 +1565,18 @@ async function handler(args) {
|
|
|
1612
1565
|
}
|
|
1613
1566
|
throw err;
|
|
1614
1567
|
}
|
|
1615
|
-
const
|
|
1616
|
-
const
|
|
1568
|
+
const project = args.project?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectId;
|
|
1569
|
+
const token = readAuthTokenForUrl(runtime.authenticationUrl, args.cwd);
|
|
1570
|
+
const developmentDraft = token ? await developmentDraftStatus({
|
|
1571
|
+
token: token.token,
|
|
1572
|
+
apiUrl: runtime.apiUrl,
|
|
1573
|
+
project,
|
|
1574
|
+
disableTelemetry: runtime.disableTelemetry
|
|
1575
|
+
}) : null;
|
|
1617
1576
|
const status = {
|
|
1618
|
-
|
|
1619
|
-
|
|
1577
|
+
project,
|
|
1578
|
+
draft: DEVELOPMENT_DRAFT,
|
|
1579
|
+
developmentDraft,
|
|
1620
1580
|
apiUrl: runtime.apiUrl,
|
|
1621
1581
|
authenticationUrl: runtime.authenticationUrl,
|
|
1622
1582
|
authenticated: token !== null,
|
|
@@ -1630,8 +1590,8 @@ async function handler(args) {
|
|
|
1630
1590
|
process.exit(0);
|
|
1631
1591
|
}
|
|
1632
1592
|
const lines = [
|
|
1633
|
-
`project: ${status.
|
|
1634
|
-
`
|
|
1593
|
+
`project: ${status.project}`,
|
|
1594
|
+
`draft: ${describeDraft(developmentDraft, status.authenticationUrl)}`,
|
|
1635
1595
|
`api: ${status.apiUrl}`,
|
|
1636
1596
|
`auth: ${status.authenticationUrl}`,
|
|
1637
1597
|
status.user ? `signed in: ${status.user.name} <${status.user.email}>` : `signed in: (no token for ${status.authenticationUrl} — run \`camox login\`)`
|
|
@@ -1641,25 +1601,29 @@ async function handler(args) {
|
|
|
1641
1601
|
}
|
|
1642
1602
|
//#endregion
|
|
1643
1603
|
//#region src/index.ts
|
|
1644
|
-
const
|
|
1645
|
-
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),
|
|
1646
1606
|
metadata: {
|
|
1647
1607
|
name: "camox",
|
|
1648
1608
|
brief: message`Camox CLI`
|
|
1649
1609
|
}
|
|
1650
|
-
})
|
|
1610
|
+
});
|
|
1611
|
+
const result = runSync(program, { help: "both" });
|
|
1651
1612
|
switch (result.command) {
|
|
1652
1613
|
case "init":
|
|
1653
|
-
|
|
1614
|
+
handler$9();
|
|
1654
1615
|
break;
|
|
1655
1616
|
case "login":
|
|
1656
|
-
await handler$
|
|
1617
|
+
await handler$6();
|
|
1657
1618
|
break;
|
|
1658
1619
|
case "logout":
|
|
1659
|
-
handler$
|
|
1620
|
+
handler$5(result);
|
|
1660
1621
|
break;
|
|
1661
1622
|
case "release":
|
|
1662
|
-
handler$1();
|
|
1623
|
+
handler$1(result);
|
|
1624
|
+
break;
|
|
1625
|
+
case "preview":
|
|
1626
|
+
await handler$3(result);
|
|
1663
1627
|
break;
|
|
1664
1628
|
case "status":
|
|
1665
1629
|
await handler(result);
|
|
@@ -1668,12 +1632,10 @@ switch (result.command) {
|
|
|
1668
1632
|
case "pages.get":
|
|
1669
1633
|
case "pages.create":
|
|
1670
1634
|
case "pages.update":
|
|
1635
|
+
case "pages.set-seo":
|
|
1671
1636
|
case "pages.set-layout":
|
|
1672
1637
|
case "pages.delete":
|
|
1673
|
-
|
|
1674
|
-
case "pages.unpublish":
|
|
1675
|
-
case "pages.discard-changes":
|
|
1676
|
-
await handler$2(result);
|
|
1638
|
+
await handler$4(result);
|
|
1677
1639
|
break;
|
|
1678
1640
|
case "blocks.types":
|
|
1679
1641
|
case "blocks.describe":
|
|
@@ -1683,22 +1645,28 @@ switch (result.command) {
|
|
|
1683
1645
|
case "blocks.edit":
|
|
1684
1646
|
case "blocks.move":
|
|
1685
1647
|
case "blocks.delete":
|
|
1686
|
-
await handler$
|
|
1648
|
+
await handler$12(result);
|
|
1687
1649
|
break;
|
|
1688
1650
|
case "files.upload":
|
|
1689
1651
|
case "files.update":
|
|
1690
1652
|
case "files.get":
|
|
1691
1653
|
case "files.list":
|
|
1692
|
-
await handler$
|
|
1654
|
+
await handler$10(result);
|
|
1693
1655
|
break;
|
|
1694
1656
|
case "layouts.list":
|
|
1695
|
-
|
|
1696
|
-
break;
|
|
1697
|
-
case "env.check":
|
|
1698
|
-
case "env.push":
|
|
1699
|
-
case "env.pull":
|
|
1657
|
+
case "layouts.get":
|
|
1700
1658
|
await handler$8(result);
|
|
1701
1659
|
break;
|
|
1660
|
+
case "locales.list":
|
|
1661
|
+
case "locales.add":
|
|
1662
|
+
case "locales.remove":
|
|
1663
|
+
case "locales.set-default-tag":
|
|
1664
|
+
await handler$7(result);
|
|
1665
|
+
break;
|
|
1666
|
+
case "publish":
|
|
1667
|
+
await handler$2(result);
|
|
1668
|
+
break;
|
|
1669
|
+
case "discard": await handler$11(result);
|
|
1702
1670
|
}
|
|
1703
1671
|
//#endregion
|
|
1704
1672
|
export {};
|