@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 CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  ## Runtime lookup and monorepos
4
4
 
5
- Runtime-backed commands (`status`, `pages`, `blocks`, `layouts`, `env`, `files`,
6
- `preview`, and `logout`) accept `--cwd PATH`:
5
+ Runtime-backed commands (`status`, `pages`, `blocks`, `layouts`, `locales`, `files`,
6
+ `publish`, `discard`, `preview`, and `logout`) accept `--cwd PATH`:
7
7
 
8
8
  ```sh
9
9
  camox pages list --cwd apps/site
@@ -19,12 +19,51 @@ build once to generate its runtime file.
19
19
  Relative `--cwd` paths resolve against the shell's current directory. This option
20
20
  only affects runtime lookup: other paths, such as `--file ./hero.webp`, still
21
21
  resolve against the shell's current directory. Missing or non-directory paths fail
22
- instead of falling back to the shell's directory. Environment selection is unchanged:
23
- your development environment by default, with explicit `--production` where supported.
22
+ instead of falling back to the shell's directory.
24
23
 
25
- ## Authenticated local draft preview
24
+ ## Your development draft
26
25
 
27
- CLI login does not sign in a fresh browser. To verify draft writes without publishing:
26
+ Every command reads and writes your development draft: it shows Live wherever
27
+ it changes nothing, and publishing it lands on Live. No command takes a draft
28
+ flag. Dev mode creates the draft on its first sync after `camox login`, so run
29
+ your site locally once (e.g. `pnpm dev`) before using the CLI; until then,
30
+ commands fail with `NO_DEVELOPMENT_DRAFT`.
31
+
32
+ Agents in parallel worktrees share their developer's development draft, and
33
+ publish or discard it together.
34
+
35
+ ## Publishing and discarding
36
+
37
+ ```sh
38
+ camox status # whether your development draft exists, and how many changes it holds
39
+ camox publish # what publishing would do, with a token
40
+ camox publish --confirm TOKEN # publish to Live
41
+ camox discard # empty your development draft (asks first; --yes skips the question)
42
+ ```
43
+
44
+ `camox publish` lists, in order:
45
+
46
+ - **Blockers**, which prevent publishing until they're fixed in the draft. They
47
+ include definitions that differ from Live's: your local code changed a block,
48
+ layout or collection, and only a release (`camox release`) can ship it to Live.
49
+ - **Conflicts**: content Live changed or deleted since your draft changed it,
50
+ with who published it and when. Publishing overwrites Live's version.
51
+ - **Lost changes**, which publishing drops because what they belong to is gone.
52
+ - **Warnings**: the same problems already on Live, which don't block.
53
+ - **Changes**: everything your draft creates, changes or deletes.
54
+
55
+ The token records the conflicts the list showed. `camox publish --confirm TOKEN`
56
+ publishes the whole draft, which then stays open, emptied. If Live changed since
57
+ the list was printed, nothing is published: the command prints the new list and
58
+ token, fails with `CONFLICTS_CHANGED`, and you review it before confirming again.
59
+
60
+ `camox discard` empties your development draft without touching Live. It asks
61
+ for confirmation in a terminal; elsewhere it fails with `CONFIRMATION_REQUIRED`
62
+ unless you pass `--yes`.
63
+
64
+ ## Authenticated local preview
65
+
66
+ CLI login does not sign in a fresh browser. To verify writes in your development draft:
28
67
 
29
68
  ```sh
30
69
  camox preview --url http://localhost:3000/about --json
@@ -32,46 +71,87 @@ camox preview --cwd apps/site --url http://localhost:3000/about --json
32
71
  ```
33
72
 
34
73
  Open the returned `url` in the browser you use for verification. Wait for
35
- `[data-camox-preview="ready"]`, which identifies the project and developer
36
- environment after authenticated draft rendering. Errors appear as
74
+ `[data-camox-preview="ready"]`, which identifies the project and your
75
+ development draft after authenticated rendering. Errors appear as
37
76
  `[data-camox-preview="error"]`; expired or consumed links require a fresh command.
38
77
  The CLI and SDK must both support the preview handoff.
39
78
 
40
79
  The URL transfers your signed-in session: treat it as a credential, do not share
41
80
  or commit it, and use it within three minutes. Only HTTP(S) loopback URLs are
42
- accepted. The destination must match the CLI's project, backend, and developer
43
- environment. There is no production mode or automatic default-browser opening.
81
+ accepted. The destination must match the CLI's project and backend. There is no
82
+ automatic default-browser opening.
44
83
  The local dev server must already be running.
45
84
 
46
85
  ## Page SEO
47
86
 
48
- Use `pages get` to read metadata and `pages update` to edit it:
87
+ Use `pages get` to read metadata and `pages set-seo` to edit it:
49
88
 
50
89
  ```sh
51
90
  camox pages get --path /about --json
52
- camox pages update --path /about --meta-title "About us" --meta-description "Meet the team."
53
- camox pages update --id 42 --meta-description ""
54
- camox pages update --id 42 --ai-seo on
55
- camox pages update --id 42 --ai-seo off
56
- camox pages get --path /about --live
57
- camox pages publish --path /about
91
+ camox pages set-seo --path /about --meta-title "About us" --meta-description "Meet the team."
92
+ # Page and block IDs are UUIDv7s; anything else is rejected.
93
+ PAGE_ID=0199f1c2-7b3e-7a10-9c4d-5e6f7a8b9c0a
94
+ camox pages set-seo --id "$PAGE_ID" --meta-description ""
95
+ camox pages set-seo --id "$PAGE_ID" --ai-seo on
96
+ camox pages set-seo --id "$PAGE_ID" --ai-seo off
58
97
  ```
59
98
 
60
- - Updates require exactly one of `--id` or `--path` and at least one update field.
61
- Omitted fields are preserved; empty strings clear metadata.
62
- - Setting either metadata field disables automatic SEO for **both** fields.
63
- Manual metadata cannot be combined with `--ai-seo on`.
64
- - `--ai-seo off` preserves current metadata. `--ai-seo on` schedules asynchronous
99
+ - `set-seo` requires exactly one of `--id` or `--path` and at least one of
100
+ `--meta-title`, `--meta-description` or `--ai-seo`. An empty meta title blocks
101
+ publishing.
102
+ - The meta title and description are one group: both generated or both manual.
103
+ Writing either field makes **both** manual, keeping the current value of the
104
+ other. Manual metadata cannot be combined with `--ai-seo on`.
105
+ - `--ai-seo off` keeps current metadata as manual. `--ai-seo on` schedules asynchronous
65
106
  generation; the returned page contains current values, not necessarily generated
66
107
  values. Use `pages get` to inspect them later.
67
- - Changes affect the draft only. Publish separately to update live metadata.
68
- `pages get --live` reads the published snapshot; without it, reads use the draft.
69
- - Commands support `--project SLUG`, `--production`, and `--json`, with the usual
70
- development-environment default and structured JSON output/errors. `--production`
71
- selects the environment; it does not publish the draft.
108
+ - Changes are visible in your development draft right away, and reach Live
109
+ when you publish it.
110
+ - Commands support `--project PROJECT` (a Project ID or Project Slug) and
111
+ `--json`, with structured JSON output/errors.
72
112
 
73
113
  See `camox pages update --help` for options and examples.
74
114
 
115
+ ## Shared navigation and footer content
116
+
117
+ `pages get` shows page-owned blocks. To find layout-owned blocks:
118
+
119
+ ```sh
120
+ camox layouts list
121
+ camox layouts get --layout-id default --json
122
+ BLOCK_ID=0199f1c2-7b3e-7a10-9c4d-5e6f7a8b9c0b
123
+ camox blocks get --id "$BLOCK_ID"
124
+ camox blocks edit --id "$BLOCK_ID" --content '{"title": "Updated title"}'
125
+ ```
126
+
127
+ A layout is identified by the name its code gives it: the `layoutId` from
128
+ `layouts list`, which is also a page's `layoutId`. `layouts get` returns
129
+ `blocks` with IDs, types, layout slot, and content, along with ordered
130
+ `layout.beforeBlockIds` and `layout.afterBlockIds`. Use a returned block `id`
131
+ with `blocks get` and `blocks edit`, inspecting its schema and current content
132
+ before editing.
133
+
134
+ Commands accept `--cwd`, `--project`, and `--json`.
135
+ Keep the project consistent when using discovered IDs.
136
+ Editing a shared layout block changes it for all pages using that layout.
137
+
138
+ ## Locales
139
+
140
+ ```sh
141
+ camox locales list
142
+ camox locales add --tag de
143
+ camox pages get --path /pricing --locale de
144
+ camox locales remove --tag de
145
+ camox locales set-default-tag --tag en-GB
146
+ ```
147
+
148
+ Locales are added and removed in your development draft, like content. Camox
149
+ translates the default locale into every other locale of the draft, when a
150
+ locale is added and after every change to the default locale. Content commands
151
+ (`pages`, `blocks`, `layouts get`) take `--locale <TAG>` and act on the default
152
+ locale without it. Correcting the default locale's tag applies at once
153
+ and is only possible while it's the only locale.
154
+
75
155
  ## Media
76
156
 
77
157
  Run `camox login` and start your project's dev server (or build once) so the CLI
@@ -88,24 +168,27 @@ camox files upload --url https://example.com/hero.webp --alt "Product screenshot
88
168
  camox files upload --url https://example.com/download --filename hero.webp --ai-metadata off
89
169
 
90
170
  # Inspect the returned ID and URL; use the ID in your block's media field.
171
+ # File IDs are UUIDv7s; anything else is rejected.
91
172
  camox files list
92
- camox files get --id 123
173
+ FILE_ID=0199f1c2-7b3e-7a10-9c4d-5e6f7a8b9c0d
174
+ camox files get --id "$FILE_ID"
93
175
 
94
- camox files update --id 123 --filename hero.webp
95
- camox files update --id 123 --alt "Updated description"
96
- camox files update --id 123 --alt ""
97
- camox files update --id 123 --ai-metadata on
176
+ camox files update --id "$FILE_ID" --filename hero.webp
177
+ camox files update --id "$FILE_ID" --alt "Updated description"
178
+ camox files update --id "$FILE_ID" --alt ""
179
+ camox files update --id "$FILE_ID" --ai-metadata on
98
180
 
99
181
  # Replace the underlying file everywhere it is referenced, preserving its ID.
100
- camox files update --id 123 --file ./new-hero.webp
101
- camox files update --id 123 --url https://example.com/new-hero.webp
182
+ camox files update --id "$FILE_ID" --file ./new-hero.webp
183
+ camox files update --id "$FILE_ID" --url https://example.com/new-hero.webp
102
184
 
103
185
  # Replace content and override metadata together.
104
- camox files update --id 123 --file ./new-hero.webp --filename hero.webp --alt "New screenshot"
186
+ camox files update --id "$FILE_ID" --file ./new-hero.webp --filename hero.webp --alt "New screenshot"
105
187
  ```
106
188
 
107
189
  - Upload requires exactly one of `--file` or `--url`. URLs are copied, not linked.
108
- - `--alt` disables AI metadata, including when its value is an empty string.
190
+ - Alt text and filename are one group: both generated or both manual. `--alt`
191
+ (even an empty string) makes both manual, keeping the current filename.
109
192
  It cannot be combined with `--ai-metadata on`. Manual edits are protected from
110
193
  already-running AI jobs.
111
194
  - Without metadata flags, raster-image uploads use automatic metadata. Other
@@ -114,26 +197,26 @@ camox files update --id 123 --file ./new-hero.webp --filename hero.webp --alt "N
114
197
  generation has completed. Use `files get` to inspect the current record.
115
198
  - Update requires at least one of `--file`, `--url`, `--filename`, `--alt` or
116
199
  `--ai-metadata`. `--file` and `--url` are mutually exclusive; without either,
117
- only metadata changes. Renaming alone preserves the file ID, URL and AI setting.
118
- - Replacing content preserves the file ID and all `_fileId` references in the
119
- selected environment. The URL changes to avoid stale caches. Direct URLs in
120
- stored block/repeatable-item content are migrated within that environment;
200
+ only metadata changes. Renaming alone preserves the file ID and URL and makes
201
+ both alt text and filename manual.
202
+ - Replacing content preserves the file ID and all `_fileId` references in your
203
+ development draft. The URL changes to avoid stale caches. Direct URLs in
204
+ stored block/repeatable-item content are migrated within that draft;
121
205
  URLs hardcoded outside Camox cannot be rewritten.
122
206
  - Replacements preserve omitted alt text and AI settings. The filename defaults
123
207
  to the new source filename; `--filename` overrides it. Binary metadata and
124
208
  explicit metadata changes are saved together. If AI remains enabled, raster
125
209
  replacements schedule generation on the original file ID; other types skip
126
210
  generation. Disable AI to prevent future automatic renaming or alt changes.
127
- - Files are shared assets, not page drafts: replacing one affects every reference
128
- to it in the selected environment. No page publish step is required.
211
+ - Files are shared assets: replacing one affects every reference
212
+ to it in your development draft.
129
213
  - Failed replacements leave the original file unchanged and remove the unused
130
214
  uploaded binary. After a network timeout, use `files get --id ID` before retrying
131
215
  because the server may already have completed the operation.
132
- - All commands support `--project SLUG`, `--production`, and `--json`. The default
133
- environment is `dev:<your-email>`, not production. IDs are scoped to the selected
134
- project and environment.
216
+ - All commands support `--project PROJECT` (a Project ID or Project Slug) and
217
+ `--json`. IDs are scoped to the selected project.
135
218
  - Uploads return the stored file record, including `id`, `url`, `mimeType`, `alt`,
136
- and `aiMetadataEnabled`. Output is JSON; errors are structured JSON on stderr.
219
+ and `aiMetadata`. Output is JSON; errors are structured JSON on stderr.
137
220
  - Files are limited to 100 MiB. URL downloads allow at most five redirects and
138
221
  have a two-minute timeout; uploads have a separate two-minute timeout.
139
222
  Downloads stream through a temporary file that is removed afterward.
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env node
2
+ import { t as installLocalhostDns } from "./local-dns-DQIT1ucr.mjs";
3
+ import { createORPCClient } from "@orpc/client";
4
+ import { RPCLink } from "@orpc/client/fetch";
5
+ //#region ../api-contract/src/drafts.ts
6
+ /** Request header naming the draft a request reads and writes (ADR 0011). Absent means Live. */
7
+ const DRAFT_HEADER = "x-camox-draft";
8
+ /**
9
+ * The `x-camox-draft` value naming the caller's own development draft in the project a request
10
+ * acts on (ADR 0006, ADR 0011). Dev mode and the CLI always send it.
11
+ */
12
+ const DEVELOPMENT_DRAFT = "development";
13
+ /**
14
+ * oRPC error code for a request naming the development draft before dev mode created it: the
15
+ * first dev-mode sync after login does, so the site has to run locally once first.
16
+ */
17
+ const NO_DEVELOPMENT_DRAFT_ERROR = "NO_DEVELOPMENT_DRAFT";
18
+ /**
19
+ * The oRPC error code a request naming an ended draft fails with. Its `data` is an `EndedDraft`,
20
+ * so a page can render Live and say who published or discarded the draft.
21
+ */
22
+ const DRAFT_ENDED_ERROR_CODE = "DRAFT_ENDED";
23
+ /**
24
+ * Why the publish tool published nothing of a draft with changes, and returned a fresh preview to
25
+ * review again, by the error code of each: Live changed since the preview, the draft has blockers,
26
+ * or translations are still running.
27
+ */
28
+ const NOT_PUBLISHED_CODES = {
29
+ conflictsChanged: "CONFLICTS_CHANGED",
30
+ blocked: "PUBLISH_BLOCKED",
31
+ translating: "TRANSLATIONS_PENDING"
32
+ };
33
+ //#endregion
34
+ //#region src/lib/api.ts
35
+ const CAMOX_API_URL = process.env.CAMOX_API_URL || "https://api.camox.dev";
36
+ installLocalhostDns(new URL(CAMOX_API_URL).hostname);
37
+ function authHeaders(token) {
38
+ return {
39
+ Authorization: `Bearer ${token}`,
40
+ "Content-Type": "application/json"
41
+ };
42
+ }
43
+ /**
44
+ * Headers of every request a command sends about a project: the CLI works only in the
45
+ * caller's development draft (ADR 0010).
46
+ */
47
+ function developmentDraftHeaders(token, disableTelemetry) {
48
+ const headers = {
49
+ Authorization: `Bearer ${token}`,
50
+ "x-camox-client": "cli",
51
+ [DRAFT_HEADER]: DEVELOPMENT_DRAFT
52
+ };
53
+ if (disableTelemetry) headers["x-camox-telemetry-disabled"] = "1";
54
+ return headers;
55
+ }
56
+ function createRpcClient(token, apiUrl = CAMOX_API_URL, options = {}) {
57
+ installLocalhostDns(new URL(apiUrl).hostname);
58
+ const headers = options.developmentDraft ? developmentDraftHeaders(token, options.disableTelemetry) : {
59
+ Authorization: `Bearer ${token}`,
60
+ "x-camox-client": "cli"
61
+ };
62
+ const link = new RPCLink({
63
+ url: `${apiUrl}/rpc`,
64
+ headers
65
+ });
66
+ return createORPCClient(link);
67
+ }
68
+ async function verifySession(token) {
69
+ return (await fetch(`${CAMOX_API_URL}/api/auth/get-session`, {
70
+ method: "GET",
71
+ headers: authHeaders(token)
72
+ })).ok;
73
+ }
74
+ const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
75
+ /**
76
+ * Load a project named by its Project ID or its Project Slug. A UUID-shaped name is a
77
+ * Project ID, which the API validates; anything else is a slug.
78
+ */
79
+ async function getProject(token, project, apiUrl) {
80
+ const client = createRpcClient(token, apiUrl, { developmentDraft: true });
81
+ return UUID_SHAPE.test(project) ? client.projects.getByUuid({ projectUuid: project }) : client.projects.getBySlug({ slug: project });
82
+ }
83
+ /**
84
+ * The caller's development draft in a project, summarized: it fails with
85
+ * `NO_DEVELOPMENT_DRAFT` until dev mode created it.
86
+ */
87
+ async function getDevelopmentDraft(params) {
88
+ return createRpcClient(params.token, params.apiUrl, {
89
+ developmentDraft: true,
90
+ disableTelemetry: params.disableTelemetry
91
+ }).drafts.get({ projectId: params.projectId });
92
+ }
93
+ async function callTool(params) {
94
+ return await createRpcClient(params.token, params.apiUrl, {
95
+ developmentDraft: true,
96
+ disableTelemetry: params.disableTelemetry
97
+ }).agent.callTool({
98
+ projectId: params.projectId,
99
+ name: params.name,
100
+ arguments: params.args
101
+ });
102
+ }
103
+ //#endregion
104
+ export { verifySession as a, NOT_PUBLISHED_CODES as c, getProject as i, NO_DEVELOPMENT_DRAFT_ERROR as l, developmentDraftHeaders as n, DEVELOPMENT_DRAFT as o, getDevelopmentDraft as r, DRAFT_ENDED_ERROR_CODE as s, callTool as t };
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { a as verifySession } from "./api-DNIQC1ea.mjs";
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { t as installLocalhostDns } from "./local-dns-DQIT1ucr.mjs";
2
3
  import fs from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
@@ -21,6 +22,38 @@ function localAuthFile(cwd) {
21
22
  function normalizeUrl(url) {
22
23
  return url.replace(/\/+$/, "");
23
24
  }
25
+ function isLoopbackUrl(url) {
26
+ return ["http:", "https:"].includes(url.protocol) && ([
27
+ "localhost",
28
+ "127.0.0.1",
29
+ "[::1]"
30
+ ].includes(url.hostname) || url.hostname.endsWith(".localhost")) && !url.username && !url.password;
31
+ }
32
+ async function createPreviewSignInUrl(destination, target, authToken, signal = AbortSignal.timeout(15e3)) {
33
+ const url = new URL(destination);
34
+ if (!isLoopbackUrl(url)) throw new Error("Preview requires an http(s) loopback URL without credentials.");
35
+ installLocalhostDns(new URL(target.apiUrl).hostname);
36
+ url.searchParams.set("camox-preview", JSON.stringify({
37
+ ...target,
38
+ apiUrl: normalizeUrl(target.apiUrl)
39
+ }));
40
+ const response = await fetch(`${normalizeUrl(target.apiUrl)}/api/auth/cross-domain/studio-authorize`, {
41
+ method: "POST",
42
+ headers: {
43
+ Authorization: `Bearer ${authToken}`,
44
+ "Content-Type": "application/json"
45
+ },
46
+ body: JSON.stringify({ callback: url.href }),
47
+ redirect: "error",
48
+ signal
49
+ });
50
+ if (!response.ok) throw new Error(`Could not create preview sign-in (${response.status}). Check your session with camox login.`);
51
+ const data = await response.json();
52
+ const token = typeof data.url === "string" ? URL.parse(data.url)?.searchParams.get("ott") : null;
53
+ if (!token) throw new Error("Authentication backend returned no one-time token.");
54
+ url.searchParams.set("ott", token);
55
+ return url.href;
56
+ }
24
57
  function readAllTokens(file) {
25
58
  if (!fs.existsSync(file)) return {};
26
59
  const entries = JSON.parse(fs.readFileSync(file, "utf-8"));
@@ -65,6 +98,7 @@ function removeAuthTokenForUrl(authenticationUrl, cwd = process.cwd()) {
65
98
  fs.rmSync(file, { force: true });
66
99
  }
67
100
  async function verifyOneTimeToken(apiUrl, token) {
101
+ installLocalhostDns(new URL(apiUrl).hostname);
68
102
  const res = await fetch(`${normalizeUrl(apiUrl)}/api/auth/one-time-token/verify`, {
69
103
  method: "POST",
70
104
  headers: { "Content-Type": "application/json" },
@@ -83,4 +117,4 @@ async function verifyOneTimeToken(apiUrl, token) {
83
117
  };
84
118
  }
85
119
  //#endregion
86
- export { writeAuthTokenForUrl as a, verifyOneTimeToken as i, readAuthTokenForUrl as n, removeAuthTokenForUrl as r, normalizeUrl as t };
120
+ export { removeAuthTokenForUrl as a, readAuthTokenForUrl as i, isLoopbackUrl as n, verifyOneTimeToken as o, normalizeUrl as r, writeAuthTokenForUrl as s, createPreviewSignInUrl as t };
package/dist/index.d.mts CHANGED
@@ -1,2 +1,2 @@
1
1
 
2
- export { };
2
+ export {}