camox 0.36.2 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/vite/definitionsSync.js +13 -12
- package/dist/features/vite/devAuthentication.js +188 -0
- package/dist/features/vite/runtimeDev.js +3 -1
- package/dist/features/vite/vite.d.ts +2 -3
- package/dist/features/vite/vite.js +29 -39
- package/dist/lib/api-client-server.js +2 -1
- package/package.json +4 -24
- package/skills/camox/SKILL.md +17 -0
- package/skills/{camox-block/SKILL.md → camox/references/block-definitions.md} +2 -7
- package/skills/camox/references/cli-blocks.md +78 -0
- package/skills/camox/references/cli-common.md +48 -0
- package/skills/camox/references/cli-environments.md +37 -0
- package/skills/camox/references/cli-pages.md +53 -0
- package/skills/{camox-layout/SKILL.md → camox/references/layout-definitions.md} +2 -7
- package/dist/features/vite/skillGeneration.js +0 -104
- package/skills/camox-cli/SKILL.md +0 -268
|
@@ -9,17 +9,17 @@ const SYNC_DEBOUNCE_DELAY_MS = 100;
|
|
|
9
9
|
* This is the core sync logic, independent of ViteDevServer.
|
|
10
10
|
*/
|
|
11
11
|
function throwIfSyncAuthError(error) {
|
|
12
|
-
if (error instanceof Error && error.name === "ORPCError" && error.message.toLowerCase().includes("unauthorized")) throw new Error("[camox] Definition sync failed: invalid
|
|
12
|
+
if (error instanceof Error && error.name === "ORPCError" && error.message.toLowerCase().includes("unauthorized")) throw new Error("[camox] Definition sync failed: invalid or missing credentials.");
|
|
13
13
|
}
|
|
14
14
|
function isNotFoundError(error) {
|
|
15
15
|
return error instanceof Error && error.name === "ORPCError" && error.message.toLowerCase().includes("not found");
|
|
16
16
|
}
|
|
17
17
|
function throwUnknownEnvironmentError(environmentName) {
|
|
18
|
-
throw new Error(`[camox] Environment "${environmentName}" does not exist.
|
|
18
|
+
throw new Error(`[camox] Environment "${environmentName}" does not exist. Run the dev server while authenticated to create your personal environment first.`);
|
|
19
19
|
}
|
|
20
20
|
async function syncDefinitionsToApi(options) {
|
|
21
|
-
const { camoxApp, projectSlug, apiUrl,
|
|
22
|
-
const client = createServerApiClient(apiUrl, environmentName);
|
|
21
|
+
const { camoxApp, projectSlug, apiUrl, environmentName, autoCreate, authToken, deployToken, logger } = options;
|
|
22
|
+
const client = createServerApiClient(apiUrl, environmentName, authToken);
|
|
23
23
|
const blocks = camoxApp.getBlocks();
|
|
24
24
|
const layoutDefinitions = camoxApp.getSerializableLayoutDefinitions();
|
|
25
25
|
const typesUsedInLayouts = /* @__PURE__ */ new Set();
|
|
@@ -40,7 +40,7 @@ async function syncDefinitionsToApi(options) {
|
|
|
40
40
|
try {
|
|
41
41
|
const result = await client.blockDefinitions.sync({
|
|
42
42
|
projectSlug,
|
|
43
|
-
|
|
43
|
+
deployToken,
|
|
44
44
|
autoCreate,
|
|
45
45
|
definitions
|
|
46
46
|
});
|
|
@@ -64,7 +64,7 @@ async function syncDefinitionsToApi(options) {
|
|
|
64
64
|
try {
|
|
65
65
|
layoutSyncResults = await client.layouts.sync({
|
|
66
66
|
projectSlug,
|
|
67
|
-
|
|
67
|
+
deployToken,
|
|
68
68
|
autoCreate,
|
|
69
69
|
layouts: layoutDefinitions
|
|
70
70
|
});
|
|
@@ -99,7 +99,7 @@ async function syncDefinitionsToApi(options) {
|
|
|
99
99
|
if (initialPage) try {
|
|
100
100
|
const result = await client.projects.initializeContent({
|
|
101
101
|
projectSlug,
|
|
102
|
-
|
|
102
|
+
deployToken,
|
|
103
103
|
layoutId: initialPage.layoutId,
|
|
104
104
|
blocks: initialPage.blocks
|
|
105
105
|
});
|
|
@@ -140,10 +140,10 @@ async function ssrLoadModule(server, modulePath) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
async function syncDefinitions(server, options) {
|
|
143
|
-
const { projectSlug,
|
|
143
|
+
const { projectSlug, apiUrl, environmentName, autoCreate, authToken, deployToken } = options;
|
|
144
144
|
const blocksDirs = [path.resolve(server.config.root, "src/blocks"), path.resolve(server.config.root, "src/camox/blocks")];
|
|
145
145
|
const layoutsDirs = [path.resolve(server.config.root, "src/layouts"), path.resolve(server.config.root, "src/camox/layouts")];
|
|
146
|
-
const client = createServerApiClient(apiUrl, environmentName);
|
|
146
|
+
const client = createServerApiClient(apiUrl, environmentName, authToken);
|
|
147
147
|
async function performInitialSync() {
|
|
148
148
|
const appModule = server.moduleGraph.getModuleById(CAMOX_APP_PATH);
|
|
149
149
|
if (appModule) server.moduleGraph.invalidateModule(appModule);
|
|
@@ -156,9 +156,10 @@ async function syncDefinitions(server, options) {
|
|
|
156
156
|
camoxApp: camoxModule.camoxApp,
|
|
157
157
|
projectSlug,
|
|
158
158
|
apiUrl,
|
|
159
|
-
syncSecret,
|
|
160
159
|
environmentName,
|
|
161
160
|
autoCreate,
|
|
161
|
+
authToken,
|
|
162
|
+
deployToken,
|
|
162
163
|
logger: server.config.logger
|
|
163
164
|
});
|
|
164
165
|
}
|
|
@@ -177,7 +178,7 @@ async function syncDefinitions(server, options) {
|
|
|
177
178
|
try {
|
|
178
179
|
result = await client.blockDefinitions.upsert({
|
|
179
180
|
projectSlug,
|
|
180
|
-
|
|
181
|
+
deployToken,
|
|
181
182
|
blockId: block._internal.id,
|
|
182
183
|
title: block._internal.title,
|
|
183
184
|
description: block._internal.description,
|
|
@@ -199,7 +200,7 @@ async function syncDefinitions(server, options) {
|
|
|
199
200
|
try {
|
|
200
201
|
result = await client.blockDefinitions.delete({
|
|
201
202
|
projectSlug,
|
|
202
|
-
|
|
203
|
+
deployToken,
|
|
203
204
|
blockId
|
|
204
205
|
});
|
|
205
206
|
} catch (error) {
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { verifyOneTimeToken, writeAuthTokenForUrl } from "@camox/cli/auth";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
//#region src/features/vite/devAuthentication.ts
|
|
5
|
+
const CALLBACK_PATH = "/__camox/auth/callback";
|
|
6
|
+
const READY_PATH = "/__camox/auth/ready";
|
|
7
|
+
const AUTH_REQUEST_TTL_MS = 600 * 1e3;
|
|
8
|
+
function normalizeUrl(url) {
|
|
9
|
+
return url.replace(/\/+$/, "");
|
|
10
|
+
}
|
|
11
|
+
function isLoopbackHostname(hostname) {
|
|
12
|
+
const normalizedHostname = hostname.toLowerCase();
|
|
13
|
+
if (normalizedHostname === "localhost" || normalizedHostname.endsWith(".localhost")) return true;
|
|
14
|
+
if (normalizedHostname === "[::1]" || normalizedHostname === "::1") return true;
|
|
15
|
+
return /^127(?:\.\d{1,3}){3}$/.test(normalizedHostname);
|
|
16
|
+
}
|
|
17
|
+
function getRequestOrigin(req, server) {
|
|
18
|
+
const host = req.headers.host;
|
|
19
|
+
if (!host) return null;
|
|
20
|
+
try {
|
|
21
|
+
const protocol = server.config.server.https ? "https" : "http";
|
|
22
|
+
const origin = new URL(`${protocol}://${host}`);
|
|
23
|
+
if (!isLoopbackHostname(origin.hostname)) return null;
|
|
24
|
+
return origin.origin;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getReturnPath(req, origin) {
|
|
30
|
+
const url = new URL(req.url ?? "/", origin);
|
|
31
|
+
return `${url.pathname}${url.search}`;
|
|
32
|
+
}
|
|
33
|
+
function redirect(res, location) {
|
|
34
|
+
res.writeHead(302, {
|
|
35
|
+
"Cache-Control": "no-store",
|
|
36
|
+
Location: location
|
|
37
|
+
});
|
|
38
|
+
res.end();
|
|
39
|
+
}
|
|
40
|
+
function sendUnavailable(res) {
|
|
41
|
+
res.writeHead(503, {
|
|
42
|
+
"Cache-Control": "no-store",
|
|
43
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
44
|
+
"Retry-After": "1"
|
|
45
|
+
});
|
|
46
|
+
res.end(JSON.stringify({ error: "Camox authentication required" }));
|
|
47
|
+
}
|
|
48
|
+
function sendRestartingPage(res, returnPath) {
|
|
49
|
+
const serializedReturnPath = JSON.stringify(returnPath).replaceAll("<", "\\u003c");
|
|
50
|
+
res.writeHead(200, {
|
|
51
|
+
"Cache-Control": "no-store",
|
|
52
|
+
"Content-Type": "text/html; charset=utf-8"
|
|
53
|
+
});
|
|
54
|
+
res.end(`<!doctype html>
|
|
55
|
+
<html lang="en">
|
|
56
|
+
<head>
|
|
57
|
+
<meta charset="utf-8" />
|
|
58
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
59
|
+
<title>Connecting to Camox…</title>
|
|
60
|
+
<style>
|
|
61
|
+
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }
|
|
62
|
+
body { align-items: center; background: #09090b; color: #fafafa; display: flex; justify-content: center; margin: 0; min-height: 100vh; }
|
|
63
|
+
main { align-items: center; display: flex; flex-direction: column; gap: 16px; text-align: center; }
|
|
64
|
+
.spinner { animation: spin 800ms linear infinite; border: 2px solid #3f3f46; border-radius: 999px; border-top-color: #fafafa; height: 24px; width: 24px; }
|
|
65
|
+
p { color: #a1a1aa; margin: 0; }
|
|
66
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
67
|
+
</style>
|
|
68
|
+
</head>
|
|
69
|
+
<body>
|
|
70
|
+
<main><div class="spinner"></div><p>Starting your Camox site…</p></main>
|
|
71
|
+
<script>
|
|
72
|
+
const returnPath = ${serializedReturnPath};
|
|
73
|
+
async function waitUntilReady() {
|
|
74
|
+
try {
|
|
75
|
+
const response = await fetch("${READY_PATH}", { cache: "no-store" });
|
|
76
|
+
if (response.ok) {
|
|
77
|
+
window.location.replace(returnPath);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
window.setTimeout(waitUntilReady, 250);
|
|
82
|
+
}
|
|
83
|
+
window.setTimeout(waitUntilReady, 250);
|
|
84
|
+
<\/script>
|
|
85
|
+
</body>
|
|
86
|
+
</html>`);
|
|
87
|
+
}
|
|
88
|
+
function installDevAuthenticationMiddleware(server, options) {
|
|
89
|
+
const pendingAuthentications = /* @__PURE__ */ new Map();
|
|
90
|
+
let loggedAuthorizationUrl = false;
|
|
91
|
+
server.middlewares.use((req, res, next) => {
|
|
92
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
93
|
+
if (requestUrl.pathname === READY_PATH) {
|
|
94
|
+
if (!options.isAuthenticated) {
|
|
95
|
+
sendUnavailable(res);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
res.writeHead(204, { "Cache-Control": "no-store" });
|
|
99
|
+
res.end();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (options.isAuthenticated) {
|
|
103
|
+
next();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (requestUrl.pathname === CALLBACK_PATH) {
|
|
107
|
+
if (req.method !== "GET") {
|
|
108
|
+
res.writeHead(405, { Allow: "GET" });
|
|
109
|
+
res.end();
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
handleAuthenticationCallback(req, res, server, options, pendingAuthentications);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const acceptsHtml = req.headers.accept?.includes("text/html") ?? false;
|
|
116
|
+
if (req.method !== "GET" || !acceptsHtml) {
|
|
117
|
+
sendUnavailable(res);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const origin = getRequestOrigin(req, server);
|
|
121
|
+
if (!origin) {
|
|
122
|
+
res.writeHead(400, {
|
|
123
|
+
"Cache-Control": "no-store",
|
|
124
|
+
"Content-Type": "text/plain; charset=utf-8"
|
|
125
|
+
});
|
|
126
|
+
res.end("Open this Camox development server through localhost to authenticate.");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const now = Date.now();
|
|
130
|
+
for (const [state, pending] of pendingAuthentications) if (pending.expiresAt < now) pendingAuthentications.delete(state);
|
|
131
|
+
const state = randomBytes(32).toString("hex");
|
|
132
|
+
const callbackUrl = new URL(CALLBACK_PATH, origin);
|
|
133
|
+
callbackUrl.searchParams.set("state", state);
|
|
134
|
+
pendingAuthentications.set(state, {
|
|
135
|
+
callbackUrl: callbackUrl.toString(),
|
|
136
|
+
expiresAt: now + AUTH_REQUEST_TTL_MS,
|
|
137
|
+
returnPath: getReturnPath(req, origin)
|
|
138
|
+
});
|
|
139
|
+
const authorizationUrl = new URL("/dev-authorize", `${normalizeUrl(options.authenticationUrl)}/`);
|
|
140
|
+
authorizationUrl.searchParams.set("callback", callbackUrl.toString());
|
|
141
|
+
if (!loggedAuthorizationUrl) {
|
|
142
|
+
server.config.logger.info(`Camox authentication required: ${authorizationUrl}`, { timestamp: true });
|
|
143
|
+
loggedAuthorizationUrl = true;
|
|
144
|
+
}
|
|
145
|
+
redirect(res, authorizationUrl.toString());
|
|
146
|
+
});
|
|
147
|
+
const authenticationLayer = server.middlewares.stack.at(-1);
|
|
148
|
+
const nitroMiddlewareIndex = server.middlewares.stack.findIndex((layer) => typeof layer.handle === "function" && layer.handle.name === "nitroDevMiddlewarePre");
|
|
149
|
+
if (authenticationLayer && nitroMiddlewareIndex >= 0) {
|
|
150
|
+
server.middlewares.stack.pop();
|
|
151
|
+
server.middlewares.stack.splice(nitroMiddlewareIndex, 0, authenticationLayer);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function handleAuthenticationCallback(req, res, server, options, pendingAuthentications) {
|
|
155
|
+
const requestUrl = new URL(req.url ?? "/", "http://localhost");
|
|
156
|
+
const state = requestUrl.searchParams.get("state");
|
|
157
|
+
const ott = requestUrl.searchParams.get("ott");
|
|
158
|
+
const pending = state ? pendingAuthentications.get(state) : void 0;
|
|
159
|
+
if (!state || !ott || !pending || pending.expiresAt < Date.now()) {
|
|
160
|
+
if (state) pendingAuthentications.delete(state);
|
|
161
|
+
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
162
|
+
res.end("This Camox authentication request is invalid or has expired. Reload the local site.");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const authToken = await verifyOneTimeToken(options.apiUrl, ott);
|
|
167
|
+
writeAuthTokenForUrl(options.authenticationUrl, authToken);
|
|
168
|
+
pendingAuthentications.delete(state);
|
|
169
|
+
server.config.logger.info(`Authenticated with Camox as ${authToken.email || authToken.name}.`, { timestamp: true });
|
|
170
|
+
res.once("finish", () => {
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
server.restart().catch((error) => {
|
|
173
|
+
server.config.logger.error(`Unable to restart the Camox development server: ${String(error)}`, { timestamp: true });
|
|
174
|
+
});
|
|
175
|
+
}, 25);
|
|
176
|
+
});
|
|
177
|
+
sendRestartingPage(res, pending.returnPath);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
server.config.logger.error(`Camox authentication failed: ${String(error)}`, { timestamp: true });
|
|
180
|
+
const authorizationUrl = new URL("/dev-authorize", `${normalizeUrl(options.authenticationUrl)}/`);
|
|
181
|
+
authorizationUrl.searchParams.set("callback", pending.callbackUrl);
|
|
182
|
+
authorizationUrl.searchParams.set("error", "callback_failed");
|
|
183
|
+
redirect(res, authorizationUrl.toString());
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
export { installDevAuthenticationMiddleware };
|
|
@@ -50,7 +50,7 @@ function resolveSdkAlias(id, importer) {
|
|
|
50
50
|
].find((candidate) => existsSync(candidate));
|
|
51
51
|
}
|
|
52
52
|
function resolveCamoxSourceImport(id) {
|
|
53
|
-
|
|
53
|
+
const sourceImport = {
|
|
54
54
|
"camox/createApp": resolve(sdkRoot, "src/core/createApp.ts"),
|
|
55
55
|
"camox/createBlock": resolve(sdkRoot, "src/core/createBlock.tsx"),
|
|
56
56
|
"camox/createLayout": resolve(sdkRoot, "src/core/createLayout.tsx"),
|
|
@@ -64,6 +64,8 @@ function resolveCamoxSourceImport(id) {
|
|
|
64
64
|
"camox/_internal/studioServer": resolve(sdkRoot, "src/features/runtime/studioServer.tsx"),
|
|
65
65
|
"camox/_internal/runtime": resolve(sdkRoot, "src/features/runtime/runtime.ts")
|
|
66
66
|
}[id];
|
|
67
|
+
if (!sourceImport || !existsSync(sourceImport)) return;
|
|
68
|
+
return sourceImport;
|
|
67
69
|
}
|
|
68
70
|
function generateVirtualCamoxApp() {
|
|
69
71
|
return `import { createApp } from "camox/createApp";
|
|
@@ -6,6 +6,7 @@ interface CamoxNitro {
|
|
|
6
6
|
hook: (name: "compiled", callback: (nitro: CamoxNitro) => void) => void;
|
|
7
7
|
};
|
|
8
8
|
options: {
|
|
9
|
+
noExternals?: boolean | (string | RegExp)[];
|
|
9
10
|
output: {
|
|
10
11
|
publicDir: string;
|
|
11
12
|
serverDir: string;
|
|
@@ -22,15 +23,13 @@ type CamoxVitePlugin = Plugin & {
|
|
|
22
23
|
interface CamoxPluginOptions {
|
|
23
24
|
/** Stable, human-readable slug identifying this project (e.g. "prestigious-impala-84") */
|
|
24
25
|
projectSlug: string;
|
|
25
|
-
/** Secret used to authenticate definition sync requests with the API */
|
|
26
|
-
syncSecret: string;
|
|
27
26
|
/** Disable PostHog telemetry collection (default: false) */
|
|
28
27
|
disableTelemetry?: boolean;
|
|
29
28
|
/** Internal options (intended for Camox contributors in development, not for public use) */
|
|
30
29
|
_internal?: {
|
|
31
30
|
/** URL of the Camox API backend, used for data fetching */apiUrl?: string; /** URL of the Camox authentication backend (default: https://app.camox.dev) */
|
|
32
31
|
authenticationUrl?: string; /** Show Tanstack query devtools (default: false) */
|
|
33
|
-
enableTanstackDevtools?: boolean; /** Disable automatic code generation (route files
|
|
32
|
+
enableTanstackDevtools?: boolean; /** Disable automatic code generation (route files and app file) (default: false) */
|
|
34
33
|
disableCodeGen?: boolean; /** Mount the Camox-owned dev runtime at this base path. Use "/" for root mode. */
|
|
35
34
|
runtimeBasePath?: string;
|
|
36
35
|
};
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { generateAppFile, watchAppFile } from "./appGeneration.js";
|
|
2
2
|
import { watchNewBlockFiles } from "./blockBoilerplate.js";
|
|
3
|
+
import { installDevAuthenticationMiddleware } from "./devAuthentication.js";
|
|
3
4
|
import { syncDefinitions, syncDefinitionsToApi } from "./definitionsSync.js";
|
|
4
5
|
import { cleanupGeneratedRouteFiles } from "./routeGeneration.js";
|
|
5
6
|
import { installRuntimeNitroRoutes, loadRuntimeDevModule, resolveRuntimeDevId } from "./runtimeDev.js";
|
|
6
|
-
import { generateSkillFiles, watchSkillFiles } from "./skillGeneration.js";
|
|
7
7
|
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
8
|
import { dirname, join, resolve } from "node:path";
|
|
10
9
|
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { readAuthTokenForUrl } from "@camox/cli/auth";
|
|
11
11
|
import { createServer } from "vite-plus";
|
|
12
|
-
import { z } from "zod";
|
|
13
12
|
|
|
14
13
|
//#region src/features/vite/vite.ts
|
|
15
14
|
const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
@@ -24,22 +23,6 @@ const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0virtual:camox/studio-client-url";
|
|
|
24
23
|
const PRODUCTION_API_URL = "https://api.camox.dev";
|
|
25
24
|
/** Authentication URL to use for Camox authentication (production Camox Dashboard) */
|
|
26
25
|
const DEFAULT_AUTHENTICATION_URL = "https://app.camox.dev";
|
|
27
|
-
const authTokenSchema = z.object({
|
|
28
|
-
token: z.string(),
|
|
29
|
-
name: z.string(),
|
|
30
|
-
email: z.string()
|
|
31
|
-
});
|
|
32
|
-
const authFileSchema = z.record(z.string(), authTokenSchema);
|
|
33
|
-
function readAuthEmail(authenticationUrl) {
|
|
34
|
-
const authFile = join(homedir(), ".camox", "auth.json");
|
|
35
|
-
const key = authenticationUrl.replace(/\/+$/, "");
|
|
36
|
-
try {
|
|
37
|
-
const raw = JSON.parse(readFileSync(authFile, "utf-8"));
|
|
38
|
-
return authFileSchema.parse(raw)[key]?.email ?? null;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
26
|
/**
|
|
44
27
|
* Drop a sidecar at `<root>/node_modules/.camox/runtime.json` so the `camox`
|
|
45
28
|
* CLI can pick up the same projectSlug / apiUrl / authenticationUrl the
|
|
@@ -54,18 +37,6 @@ function writeRuntimeSidecar(root, data) {
|
|
|
54
37
|
mkdirSync(dir, { recursive: true });
|
|
55
38
|
writeFileSync(join(dir, "runtime.json"), `${JSON.stringify(data, null, 2)}\n`);
|
|
56
39
|
}
|
|
57
|
-
function resolveEnvironmentName(command, authenticationUrl) {
|
|
58
|
-
if (command === "serve") {
|
|
59
|
-
const email = readAuthEmail(authenticationUrl);
|
|
60
|
-
if (!email) throw new Error("Camox: not authenticated. Run `npx camox login` to create your personal dev environment.");
|
|
61
|
-
return `dev:${email}`;
|
|
62
|
-
}
|
|
63
|
-
const envFromProcess = process.env.CAMOX_ENV;
|
|
64
|
-
if (envFromProcess) return envFromProcess;
|
|
65
|
-
const email = readAuthEmail(authenticationUrl);
|
|
66
|
-
const suggestion = email ? ` CAMOX_ENV=dev:${email} (your personal dev environment)\n CAMOX_ENV=production (release build)` : ` CAMOX_ENV=production (release build)\n\nIf you want to build against a dev environment, run \`npx camox login\` first.`;
|
|
67
|
-
throw new Error(`Camox: CAMOX_ENV is required on build. Set it to one of:\n${suggestion}`);
|
|
68
|
-
}
|
|
69
40
|
function camox(options) {
|
|
70
41
|
const apiUrl = options._internal?.apiUrl ?? PRODUCTION_API_URL;
|
|
71
42
|
const authenticationUrl = options._internal?.authenticationUrl ?? DEFAULT_AUTHENTICATION_URL;
|
|
@@ -73,8 +44,10 @@ function camox(options) {
|
|
|
73
44
|
const disableCodeGen = options._internal?.disableCodeGen ?? false;
|
|
74
45
|
const runtimeBasePath = options._internal?.runtimeBasePath;
|
|
75
46
|
let isBuild = false;
|
|
47
|
+
let isRelease = false;
|
|
76
48
|
let resolvedConfig;
|
|
77
49
|
let environmentName;
|
|
50
|
+
let localAuth = null;
|
|
78
51
|
return {
|
|
79
52
|
name: "camox",
|
|
80
53
|
enforce: "pre",
|
|
@@ -103,6 +76,10 @@ function camox(options) {
|
|
|
103
76
|
return loadRuntimeDevModule(id, { runtimeBasePath });
|
|
104
77
|
},
|
|
105
78
|
nitro: { setup(nitro) {
|
|
79
|
+
if (nitro.options.noExternals !== true) {
|
|
80
|
+
const noExternals = Array.isArray(nitro.options.noExternals) ? nitro.options.noExternals : [];
|
|
81
|
+
nitro.options.noExternals = [...noExternals, "camox"];
|
|
82
|
+
}
|
|
106
83
|
installRuntimeNitroRoutes(nitro, { runtimeBasePath });
|
|
107
84
|
nitro.hooks.hook("compiled", ({ options }) => {
|
|
108
85
|
const serverAssets = join(options.output.serverDir, "assets");
|
|
@@ -112,7 +89,13 @@ function camox(options) {
|
|
|
112
89
|
} },
|
|
113
90
|
config(_config, env) {
|
|
114
91
|
isBuild = env.command === "build";
|
|
115
|
-
|
|
92
|
+
isRelease = isBuild && process.env.CAMOX_INTERNAL_RELEASE === "1";
|
|
93
|
+
if (isRelease) environmentName = "production";
|
|
94
|
+
else {
|
|
95
|
+
localAuth = readAuthTokenForUrl(authenticationUrl);
|
|
96
|
+
if (isBuild && !localAuth) throw new Error("Camox: not authenticated. Run `npx camox login` before building the site.");
|
|
97
|
+
environmentName = localAuth ? `dev:${localAuth.email}` : "dev:unauthenticated";
|
|
98
|
+
}
|
|
116
99
|
return {
|
|
117
100
|
build: { emitAssets: true },
|
|
118
101
|
define: {
|
|
@@ -220,32 +203,39 @@ function camox(options) {
|
|
|
220
203
|
if (!disableCodeGen) {
|
|
221
204
|
generateAppFile(config.root);
|
|
222
205
|
cleanupGeneratedRouteFiles(routesDir);
|
|
223
|
-
generateSkillFiles(config.root);
|
|
224
206
|
}
|
|
225
207
|
if (disableCodeGen) config.logger.warn("⚠️ Code generation is disabled (_internal.disableCodeGen). This option is only meant for momentary debugging — do not deploy or commit your app with it enabled.", { timestamp: true });
|
|
226
208
|
const mode = config.command === "serve" ? "Running" : "Building";
|
|
227
|
-
|
|
209
|
+
const environmentLabel = localAuth || isRelease ? environmentName : "authentication required";
|
|
210
|
+
config.logger.info(`${mode} Camox app (${environmentLabel})`, { timestamp: true });
|
|
228
211
|
},
|
|
229
212
|
configureServer(server) {
|
|
230
213
|
const routesDir = resolve(server.config.root, "src/routes");
|
|
214
|
+
installDevAuthenticationMiddleware(server, {
|
|
215
|
+
apiUrl,
|
|
216
|
+
authenticationUrl,
|
|
217
|
+
isAuthenticated: localAuth !== null
|
|
218
|
+
});
|
|
231
219
|
if (!disableCodeGen) {
|
|
232
220
|
watchAppFile(server, server.config.root);
|
|
233
221
|
cleanupGeneratedRouteFiles(routesDir);
|
|
234
|
-
watchSkillFiles(server, server.config.root);
|
|
235
222
|
watchNewBlockFiles(server);
|
|
236
223
|
}
|
|
237
224
|
server.httpServer?.once("listening", () => {
|
|
225
|
+
if (!localAuth) return;
|
|
238
226
|
syncDefinitions(server, {
|
|
239
227
|
projectSlug: options.projectSlug,
|
|
240
|
-
syncSecret: options.syncSecret,
|
|
241
228
|
apiUrl,
|
|
242
229
|
environmentName,
|
|
243
|
-
autoCreate: true
|
|
230
|
+
autoCreate: true,
|
|
231
|
+
authToken: localAuth?.token
|
|
244
232
|
});
|
|
245
233
|
});
|
|
246
234
|
},
|
|
247
235
|
async closeBundle() {
|
|
248
|
-
if (!
|
|
236
|
+
if (!isRelease) return;
|
|
237
|
+
const deployToken = process.env.CAMOX_DEPLOY_TOKEN;
|
|
238
|
+
if (!deployToken) throw new Error("Camox: CAMOX_DEPLOY_TOKEN is required to release to production.");
|
|
249
239
|
const camoxAppPath = "./src/camox/app.ts";
|
|
250
240
|
const tempServer = await createServer({
|
|
251
241
|
configFile: false,
|
|
@@ -262,9 +252,9 @@ function camox(options) {
|
|
|
262
252
|
camoxApp: camoxModule.camoxApp,
|
|
263
253
|
projectSlug: options.projectSlug,
|
|
264
254
|
apiUrl,
|
|
265
|
-
syncSecret: options.syncSecret,
|
|
266
255
|
environmentName,
|
|
267
256
|
autoCreate: false,
|
|
257
|
+
deployToken,
|
|
268
258
|
logger: resolvedConfig.logger
|
|
269
259
|
});
|
|
270
260
|
} finally {
|
|
@@ -2,9 +2,10 @@ import { createORPCClient } from "@orpc/client";
|
|
|
2
2
|
import { RPCLink } from "@orpc/client/fetch";
|
|
3
3
|
|
|
4
4
|
//#region src/lib/api-client-server.ts
|
|
5
|
-
function createServerApiClient(apiUrl, environmentName) {
|
|
5
|
+
function createServerApiClient(apiUrl, environmentName, authToken) {
|
|
6
6
|
const headers = {};
|
|
7
7
|
if (environmentName) headers["x-environment-name"] = environmentName;
|
|
8
|
+
if (authToken) headers.Authorization = `Bearer ${authToken}`;
|
|
8
9
|
return createORPCClient(new RPCLink({
|
|
9
10
|
url: `${apiUrl}/rpc`,
|
|
10
11
|
headers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "camox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"camox": "./bin/camox.mjs"
|
|
@@ -13,103 +13,83 @@
|
|
|
13
13
|
"type": "module",
|
|
14
14
|
"exports": {
|
|
15
15
|
"./createApp": {
|
|
16
|
-
"source": "./src/core/createApp.ts",
|
|
17
16
|
"types": "./dist/core/createApp.d.ts",
|
|
18
17
|
"import": "./dist/core/createApp.js"
|
|
19
18
|
},
|
|
20
19
|
"./createBlock": {
|
|
21
|
-
"source": "./src/core/createBlock.tsx",
|
|
22
20
|
"types": "./dist/core/createBlock.d.ts",
|
|
23
21
|
"import": "./dist/core/createBlock.js"
|
|
24
22
|
},
|
|
25
23
|
"./createLayout": {
|
|
26
|
-
"source": "./src/core/createLayout.tsx",
|
|
27
24
|
"types": "./dist/core/createLayout.d.ts",
|
|
28
25
|
"import": "./dist/core/createLayout.js"
|
|
29
26
|
},
|
|
30
27
|
"./document": {
|
|
31
|
-
"source": "./src/core/defineDocument.ts",
|
|
32
28
|
"types": "./dist/core/defineDocument.d.ts",
|
|
33
29
|
"import": "./dist/core/defineDocument.js"
|
|
34
30
|
},
|
|
35
31
|
"./CamoxPreview": {
|
|
36
|
-
"source": "./src/features/preview/CamoxPreview.tsx",
|
|
37
32
|
"types": "./dist/features/preview/CamoxPreview.d.ts",
|
|
38
33
|
"import": "./dist/features/preview/CamoxPreview.js"
|
|
39
34
|
},
|
|
40
35
|
"./CamoxContent": {
|
|
41
|
-
"source": "./src/features/content/CamoxContent.tsx",
|
|
42
36
|
"types": "./dist/features/content/CamoxContent.d.ts",
|
|
43
37
|
"import": "./dist/features/content/CamoxContent.js"
|
|
44
38
|
},
|
|
45
39
|
"./CamoxProvider": {
|
|
46
|
-
"source": "./src/features/provider/CamoxProvider.tsx",
|
|
47
40
|
"types": "./dist/features/provider/CamoxProvider.d.ts",
|
|
48
41
|
"import": "./dist/features/provider/CamoxProvider.js"
|
|
49
42
|
},
|
|
50
43
|
"./CamoxStudio": {
|
|
51
|
-
"source": "./src/features/studio/CamoxStudio.tsx",
|
|
52
44
|
"types": "./dist/features/studio/CamoxStudio.d.ts",
|
|
53
45
|
"import": "./dist/features/studio/CamoxStudio.js"
|
|
54
46
|
},
|
|
55
47
|
"./navigation": {
|
|
56
|
-
"source": "./src/features/navigation/navigation.tsx",
|
|
57
48
|
"types": "./dist/features/navigation/navigation.d.ts",
|
|
58
49
|
"import": "./dist/features/navigation/navigation.js"
|
|
59
50
|
},
|
|
60
51
|
"./vite": {
|
|
61
|
-
"source": "./src/features/vite/vite.ts",
|
|
62
52
|
"types": "./dist/features/vite/vite.d.ts",
|
|
63
53
|
"import": "./dist/features/vite/vite.js"
|
|
64
54
|
},
|
|
65
55
|
"./metadata": {
|
|
66
|
-
"source": "./src/features/metadata/sitemap.ts",
|
|
67
56
|
"types": "./dist/features/metadata/sitemap.d.ts",
|
|
68
57
|
"import": "./dist/features/metadata/sitemap.js"
|
|
69
58
|
},
|
|
70
59
|
"./_internal/pageServer": {
|
|
71
|
-
"source": "./src/features/runtime/pageServer.tsx",
|
|
72
60
|
"types": "./dist/features/runtime/pageServer.d.ts",
|
|
73
61
|
"import": "./dist/features/runtime/pageServer.js"
|
|
74
62
|
},
|
|
75
63
|
"./_internal/pageClient": {
|
|
76
|
-
"source": "./src/features/runtime/pageClient.tsx",
|
|
77
64
|
"types": "./dist/features/runtime/pageClient.d.ts",
|
|
78
65
|
"import": "./dist/features/runtime/pageClient.js"
|
|
79
66
|
},
|
|
80
67
|
"./_internal/studioClient": {
|
|
81
|
-
"source": "./src/features/runtime/studioClient.tsx",
|
|
82
68
|
"types": "./dist/features/runtime/studioClient.d.ts",
|
|
83
69
|
"import": "./dist/features/runtime/studioClient.js"
|
|
84
70
|
},
|
|
85
71
|
"./_internal/studioServer": {
|
|
86
|
-
"source": "./src/features/runtime/studioServer.tsx",
|
|
87
72
|
"types": "./dist/features/runtime/studioServer.d.ts",
|
|
88
73
|
"import": "./dist/features/runtime/studioServer.js"
|
|
89
74
|
},
|
|
90
75
|
"./_internal/runtime": {
|
|
91
|
-
"source": "./src/features/runtime/runtime.ts",
|
|
92
76
|
"types": "./dist/features/runtime/runtime.d.ts",
|
|
93
77
|
"import": "./dist/features/runtime/runtime.js"
|
|
94
78
|
},
|
|
95
79
|
"./_internal/imageResponse": {
|
|
96
80
|
"workerd": {
|
|
97
|
-
"source": "./src/features/og/imageResponse.workerd.ts",
|
|
98
81
|
"types": "./dist/features/og/imageResponse.workerd.d.ts",
|
|
99
82
|
"import": "./dist/features/og/imageResponse.workerd.js"
|
|
100
83
|
},
|
|
101
84
|
"worker": {
|
|
102
|
-
"source": "./src/features/og/imageResponse.workerd.ts",
|
|
103
85
|
"types": "./dist/features/og/imageResponse.workerd.d.ts",
|
|
104
86
|
"import": "./dist/features/og/imageResponse.workerd.js"
|
|
105
87
|
},
|
|
106
88
|
"deno": {
|
|
107
|
-
"source": "./src/features/og/imageResponse.workerd.ts",
|
|
108
89
|
"types": "./dist/features/og/imageResponse.workerd.d.ts",
|
|
109
90
|
"import": "./dist/features/og/imageResponse.workerd.js"
|
|
110
91
|
},
|
|
111
92
|
"default": {
|
|
112
|
-
"source": "./src/features/og/imageResponse.node.ts",
|
|
113
93
|
"types": "./dist/features/og/imageResponse.node.d.ts",
|
|
114
94
|
"import": "./dist/features/og/imageResponse.node.js"
|
|
115
95
|
}
|
|
@@ -157,9 +137,9 @@
|
|
|
157
137
|
"streamdown": "^2.5.0",
|
|
158
138
|
"unhead": "^3.1.7",
|
|
159
139
|
"zod": "^4.4.3",
|
|
160
|
-
"@camox/
|
|
161
|
-
"@camox/
|
|
162
|
-
"@camox/
|
|
140
|
+
"@camox/api-contract": "0.38.0",
|
|
141
|
+
"@camox/cli": "0.38.0",
|
|
142
|
+
"@camox/ui": "0.38.0"
|
|
163
143
|
},
|
|
164
144
|
"devDependencies": {
|
|
165
145
|
"@babel/core": "^7.29.0",
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: camox
|
|
3
|
+
description: Use when working in a Camox codebase with Block Definitions, Layout Definitions, Pages, Block content, the Camox CLI, publishing, or Environments. Applies to code-defined site structure and agent-driven content changes.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Camox
|
|
7
|
+
|
|
8
|
+
Load only the references needed for the current task. Do not preload every reference.
|
|
9
|
+
|
|
10
|
+
- To create or change a **Block Definition** in `src/blocks/`, read [references/block-definitions.md](references/block-definitions.md).
|
|
11
|
+
- To create or change a **Layout Definition** in `src/layouts/`, read [references/layout-definitions.md](references/layout-definitions.md).
|
|
12
|
+
- For any CLI operation, first read [references/cli-common.md](references/cli-common.md), then read only the relevant command reference:
|
|
13
|
+
- To inspect, create, edit, move, or delete Block instances or their Content, read [references/cli-blocks.md](references/cli-blocks.md).
|
|
14
|
+
- To inspect, create, update, publish, or assign Layouts to Pages, read [references/cli-pages.md](references/cli-pages.md).
|
|
15
|
+
- To target production or replicate content between Environments, read [references/cli-environments.md](references/cli-environments.md).
|
|
16
|
+
|
|
17
|
+
A request can cross boundaries. For example, adding a new kind of section to a Page may require the Block Definition reference followed by the CLI common and Block references. Load the additional reference only once that need is established.
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: camox-block
|
|
3
|
-
description: "How to create Camox block definition files. Use this skill whenever the user wants to create a new block for their Camox website, add a page section/component, build a reusable content block, or asks about the block definition API. Trigger on mentions of blocks, sections, page components, content types, or any request to add new visual sections to a Camox site — even if they don't say 'block' explicitly."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
1
|
# Creating Camox Block Definitions
|
|
7
2
|
|
|
8
3
|
A block is a reusable page section (hero, testimonial, gallery, footer...). Users compose pages by assembling blocks. This skill covers creating block **definitions** — the template that describes a block's schema and rendering. Not the content (an instance of a block).
|
|
9
4
|
|
|
10
|
-
## After defining the block:
|
|
5
|
+
## After defining the block: place it when requested
|
|
11
6
|
|
|
12
|
-
A definition only adds a new _type_ of block to the catalog — it doesn't put one on any
|
|
7
|
+
A definition only adds a new _type_ of block to the catalog — it doesn't put one on any Page. Users almost never just want "a definition"; they want a section that actually shows up on the site. Once the Block Definition exists and the dev server has picked it up, follow the umbrella skill's CLI routing to create a Block on the relevant Page. For shared chrome, follow the Layout Definition reference instead. If the user asks for "a hero on the homepage", define the Block Definition and then create the hero Block on the home Page. Don't stop after the definition unless the user explicitly requested only the reusable type.
|
|
13
8
|
|
|
14
9
|
## Quick Start
|
|
15
10
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Managing Blocks with the Camox CLI
|
|
2
|
+
|
|
3
|
+
Read [cli-common.md](cli-common.md) first. Replace `{{CAMOX_CMD}}` as described there and verify commands with `--help`.
|
|
4
|
+
|
|
5
|
+
Use these commands for Block instances and their Content. To change which fields or rendering a Block type provides, use the Block Definition reference instead.
|
|
6
|
+
|
|
7
|
+
## Inspect before changing
|
|
8
|
+
|
|
9
|
+
Look up the Page before creating or moving Blocks so you have its `id`, current Blocks, and any sibling ids. Describe the Block type before constructing Content:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
{{CAMOX_CMD}} pages get --path /
|
|
13
|
+
{{CAMOX_CMD}} blocks describe --type hero
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Create and position a Block
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}'
|
|
20
|
+
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --position first
|
|
21
|
+
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --before-id 174
|
|
22
|
+
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --after-id 174
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`blocks create` and `blocks move` accept the same positioning flags. Pass at most one:
|
|
26
|
+
|
|
27
|
+
| Flag | Meaning |
|
|
28
|
+
| ------------------------- | ----------------------------------------------- |
|
|
29
|
+
| `--position first` | Put the Block first. |
|
|
30
|
+
| `--position last` | Put the Block last; this is the create default. |
|
|
31
|
+
| `--after-id <ID>` | Put it immediately after a sibling Block. |
|
|
32
|
+
| `--before-id <ID>` | Put it immediately before a sibling Block. |
|
|
33
|
+
| `--after-position <KEY>` | Insert after a known fractional-index key. |
|
|
34
|
+
| `--before-position <KEY>` | Insert before a known fractional-index key. |
|
|
35
|
+
|
|
36
|
+
Prefer `--position`, `--after-id`, and `--before-id`. A move requires one positioning flag.
|
|
37
|
+
|
|
38
|
+
## Edit Block Content
|
|
39
|
+
|
|
40
|
+
Content patches merge ordinary fields, so send only fields that should change:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
{{CAMOX_CMD}} pages get --path /pricing
|
|
44
|
+
{{CAMOX_CMD}} blocks edit --id 314 --content '{"headline": "New headline"}'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Edit repeatable Content safely
|
|
48
|
+
|
|
49
|
+
A repeatable field's array is replacement-shaped: omitting an existing item deletes it and cascades to its file references, settings, and nested items. Fetch the Block, preserve every existing item with `_itemId`, and add changes only to the intended entries:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
{{CAMOX_CMD}} blocks get --id 99
|
|
53
|
+
|
|
54
|
+
{{CAMOX_CMD}} blocks edit --id 99 --content '{
|
|
55
|
+
"items": [
|
|
56
|
+
{"_itemId": 401},
|
|
57
|
+
{"_itemId": 402, "answer": "Updated answer."},
|
|
58
|
+
{"_itemId": 403}
|
|
59
|
+
]
|
|
60
|
+
}'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The response's `repeatableItems` contains each item's `id`, `fieldName`, `parentItemId`, and Content. Use the same `_itemId` pattern recursively for nested repeatables. Fetch multiple Blocks together when useful:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
{{CAMOX_CMD}} blocks get-many --id 99 --id 100 --id 101
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`blocks get-many` returns the same bundle shape as `blocks get`, in requested-id order.
|
|
70
|
+
|
|
71
|
+
## Draft and live reads
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
{{CAMOX_CMD}} blocks get --id 314 # draft
|
|
75
|
+
{{CAMOX_CMD}} blocks get --id 314 --live # published snapshot
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Publish or discard at the Page level; read [cli-pages.md](cli-pages.md) when the task includes review or publication.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Camox CLI common guidance
|
|
2
|
+
|
|
3
|
+
Use the Camox CLI for CRUD operations on Pages, Block instances, Layout assignments, metadata, drafts, and published content. Use the code references instead when changing Block Definitions or Layout Definitions in `src/blocks/` or `src/layouts/`.
|
|
4
|
+
|
|
5
|
+
If a change should be visible without a code deployment, it is usually a CLI content operation. Some requests require both code and CLI work: define the new type first, wait for the dev server to discover it, then create or assign its content through the CLI.
|
|
6
|
+
|
|
7
|
+
## Run the installed CLI
|
|
8
|
+
|
|
9
|
+
Detect the package manager from the closest `package.json#packageManager` or lockfile, then use its local Camox binary:
|
|
10
|
+
|
|
11
|
+
| Package manager | Command |
|
|
12
|
+
| --------------- | ------------ |
|
|
13
|
+
| pnpm | `pnpm camox` |
|
|
14
|
+
| Bun | `bunx camox` |
|
|
15
|
+
| npm | `npx camox` |
|
|
16
|
+
|
|
17
|
+
In the examples below, replace `{{CAMOX_CMD}}` with that command.
|
|
18
|
+
|
|
19
|
+
## Discover the current command surface
|
|
20
|
+
|
|
21
|
+
The CLI evolves with the installed Camox version. Do not guess commands or flags from memory. Run `--help` at the root, command group, and subcommand before executing a write:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
{{CAMOX_CMD}} --help
|
|
25
|
+
{{CAMOX_CMD}} pages --help
|
|
26
|
+
{{CAMOX_CMD}} pages create --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Treat the installed CLI's output as authoritative when it differs from an example in these references.
|
|
30
|
+
|
|
31
|
+
## Draft-first workflow
|
|
32
|
+
|
|
33
|
+
By default, reads and writes operate on the draft in the current developer's isolated dev Environment. Make content changes in draft, summarize them for review, and publish only after the user asks or approves.
|
|
34
|
+
|
|
35
|
+
- Omit `--live` while editing. `--live` only reads the published snapshot.
|
|
36
|
+
- Omit `--production` unless the user explicitly asks to operate on the production Environment.
|
|
37
|
+
- Publishing a draft and replicating an Environment are different operations. Read the Environment reference before using `env push` or `env pull`.
|
|
38
|
+
|
|
39
|
+
## Write grounded content
|
|
40
|
+
|
|
41
|
+
Before writing Content:
|
|
42
|
+
|
|
43
|
+
1. Inspect existing Pages, Blocks, and Block type descriptions to learn the site's voice, positioning, names, and factual claims.
|
|
44
|
+
2. Use facts and copy supplied by the user. Research external facts with an available retrieval tool; ask when a material fact cannot be established.
|
|
45
|
+
3. Never invent filler copy, statistics, testimonials, company details, asset filenames, or URLs.
|
|
46
|
+
4. Leave `File`, `Embed`, or `Image` values empty when no real asset is available, and tell the user what remains to be supplied.
|
|
47
|
+
|
|
48
|
+
After writes, report the affected Pages and Blocks and the notable content or structural changes.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Managing Environments with the Camox CLI
|
|
2
|
+
|
|
3
|
+
Read [cli-common.md](cli-common.md) first. Replace `{{CAMOX_CMD}}` as described there and verify commands with `--help`.
|
|
4
|
+
|
|
5
|
+
Every Project has an isolated dev Environment for each developer and a shared production Environment. Each Environment independently contains draft and live sources.
|
|
6
|
+
|
|
7
|
+
## Target production per command
|
|
8
|
+
|
|
9
|
+
Commands target the dev Environment by default. Use `--production` only when the user explicitly asks to operate on production:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
{{CAMOX_CMD}} pages list --production
|
|
13
|
+
{{CAMOX_CMD}} blocks edit --id <ID> --production
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`--production` selects the Environment; it does not select published content. Combine it with `--live` to read production's published snapshot, or use `pages publish --production` to publish a production draft.
|
|
17
|
+
|
|
18
|
+
## Replicate an entire Environment
|
|
19
|
+
|
|
20
|
+
Use the `env` group when the user wants to copy all dev content to production or replace dev with production. Do not emulate replication with a series of resource commands.
|
|
21
|
+
|
|
22
|
+
Inspect compatibility first:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
{{CAMOX_CMD}} env check
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`env check` reports push and pull compatibility and identifies Block Definition or Layout divergences. Resolve every reported divergence before replication; incompatible replication fails with `FAILED_PRECONDITION` and the same reasons.
|
|
29
|
+
|
|
30
|
+
After explicit user authorization for the requested direction:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
{{CAMOX_CMD}} env push --yes # replace production with dev
|
|
34
|
+
{{CAMOX_CMD}} env pull --yes # replace dev with production
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Both operations replace every Page, Block, and file in the target Environment and cannot be undone. They require `--yes` or `-y`. Confirm the direction from the user's request; never infer push versus pull.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Managing Pages with the Camox CLI
|
|
2
|
+
|
|
3
|
+
Read [cli-common.md](cli-common.md) first. Replace `{{CAMOX_CMD}}` as described there and verify commands with `--help`.
|
|
4
|
+
|
|
5
|
+
Use these commands for Page paths and metadata, Layout assignments, draft review, and publishing. Use [cli-blocks.md](cli-blocks.md) when changing the ordered Blocks or Content on a Page.
|
|
6
|
+
|
|
7
|
+
## Create a Page with an existing Layout
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
{{CAMOX_CMD}} layouts list
|
|
11
|
+
{{CAMOX_CMD}} pages create --path-segment about --layout-id 39
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use `--parent-page-id <ID>` to nest the Page under another Page. Assigning an existing Layout is a CLI content operation; creating or changing a Layout Definition is a code operation covered by the Layout Definition reference.
|
|
15
|
+
|
|
16
|
+
## Review draft and live state
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
{{CAMOX_CMD}} pages get --path /pricing # draft
|
|
20
|
+
{{CAMOX_CMD}} pages get --path /pricing --live # published snapshot
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Live reads fail when the Page or Block has never been published. `--live` is not a write target and does not publish anything.
|
|
24
|
+
|
|
25
|
+
## Publish after approval
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
{{CAMOX_CMD}} pages publish --path /pricing
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`pages publish` accepts exactly one of `--id` or `--path`. It publishes the Page's current draft and, by default, its Layout. Publishing the Layout is a no-op when it has no pending changes. Use `--no-layout` only when the user explicitly does not want pending Layout changes published with the Page:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
{{CAMOX_CMD}} pages publish --path /pricing --no-layout
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Do not publish merely because a draft edit succeeded. Summarize the draft and wait for the user to ask or approve publication.
|
|
38
|
+
|
|
39
|
+
## Unpublish or discard drafts
|
|
40
|
+
|
|
41
|
+
Remove a Page from live without deleting its draft:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
{{CAMOX_CMD}} pages unpublish --path /pricing
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Reset a draft to its current live snapshot without changing live:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
{{CAMOX_CMD}} pages discard-changes --path /pricing
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Both commands accept exactly one of `--id` or `--path`. Discarding fails if the Page has never been published.
|
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: camox-layout
|
|
3
|
-
description: "How to create and edit Camox layout definition files. Use this skill whenever the user wants to create a new layout for their Camox website, modify an existing layout, wrap pages in shared structure (navbar + footer), customize meta titles or OG images, or asks about the layout definition API. Trigger on mentions of layouts, page wrappers, page templates, shared page structure, navbar/footer placement, or any request to define how pages are structured — even if they don't say 'layout' explicitly."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
1
|
# Creating Camox Layout Definitions
|
|
7
2
|
|
|
8
3
|
A layout wraps pages in shared structure — a navbar at the top, a footer at the bottom, consistent styling. Each page in the CMS is assigned a layout. This skill covers creating layout **definitions** — the template that describes which blocks surround page content and how to render them.
|
|
9
4
|
|
|
10
|
-
## After defining the layout:
|
|
5
|
+
## After defining the layout: assign it when requested
|
|
11
6
|
|
|
12
|
-
A definition only adds a new
|
|
7
|
+
A definition only adds a new Layout to the catalog — it doesn't apply it to any Page, and existing Pages keep their current Layout. If the user wanted "the about Page to use the new marketing Layout" or "all docs Pages to use this Layout", follow the umbrella skill's CLI routing to update the relevant Pages after the dev server has picked up the definition. Don't stop after the definition unless the user explicitly requested only a reusable Layout Definition.
|
|
13
8
|
|
|
14
9
|
## Quick Start
|
|
15
10
|
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { writeIfChanged } from "./utils.js";
|
|
2
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
//#region src/features/vite/skillGeneration.ts
|
|
7
|
-
const NOTICE = `<!-- AUTO-GENERATED BY CAMOX — DO NOT EDIT
|
|
8
|
-
This file is managed by the Camox Vite plugin.
|
|
9
|
-
Any manual edits will be automatically reverted by the dev server. -->`;
|
|
10
|
-
function injectNoticeAfterFrontmatter(source) {
|
|
11
|
-
if (!source.startsWith("---\n")) return `${NOTICE}\n\n${source}`;
|
|
12
|
-
const closeIdx = source.indexOf("\n---\n", 4);
|
|
13
|
-
if (closeIdx === -1) return `${NOTICE}\n\n${source}`;
|
|
14
|
-
const endOfClose = closeIdx + 5;
|
|
15
|
-
return `${source.slice(0, endOfClose)}\n${NOTICE}\n${source.slice(endOfClose)}`;
|
|
16
|
-
}
|
|
17
|
-
const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
18
|
-
const camoxCmdByPm = {
|
|
19
|
-
pnpm: "pnpm camox",
|
|
20
|
-
bun: "bunx camox",
|
|
21
|
-
npm: "npx camox"
|
|
22
|
-
};
|
|
23
|
-
function detectPackageManagerInDir(dir) {
|
|
24
|
-
const packageJsonPath = resolve(dir, "package.json");
|
|
25
|
-
if (existsSync(packageJsonPath)) try {
|
|
26
|
-
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
27
|
-
const name = (typeof pkg.packageManager === "string" ? pkg.packageManager : "").split("@", 1)[0];
|
|
28
|
-
if (name === "pnpm" || name === "bun" || name === "npm") return name;
|
|
29
|
-
} catch {}
|
|
30
|
-
if (existsSync(resolve(dir, "pnpm-lock.yaml"))) return "pnpm";
|
|
31
|
-
if (existsSync(resolve(dir, "bun.lock")) || existsSync(resolve(dir, "bun.lockb"))) return "bun";
|
|
32
|
-
if (existsSync(resolve(dir, "package-lock.json"))) return "npm";
|
|
33
|
-
}
|
|
34
|
-
function detectPackageManager(appRoot) {
|
|
35
|
-
let dir = resolve(appRoot);
|
|
36
|
-
while (true) {
|
|
37
|
-
const found = detectPackageManagerInDir(dir);
|
|
38
|
-
if (found) return found;
|
|
39
|
-
const parent = dirname(dir);
|
|
40
|
-
if (parent === dir) return "npm";
|
|
41
|
-
dir = parent;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
function getSkillNames() {
|
|
45
|
-
const skillsDir = resolve(sdkRoot, "skills");
|
|
46
|
-
return readdirSync(skillsDir).filter((name) => lstatSync(resolve(skillsDir, name)).isDirectory());
|
|
47
|
-
}
|
|
48
|
-
function generateSkillContent(skillName, appRoot) {
|
|
49
|
-
const raw = readFileSync(resolve(sdkRoot, `skills/${skillName}/SKILL.md`), "utf-8");
|
|
50
|
-
const pm = detectPackageManager(appRoot);
|
|
51
|
-
return injectNoticeAfterFrontmatter(raw.replaceAll("{{PM_NAME}}", pm).replaceAll("{{CAMOX_CMD}}", camoxCmdByPm[pm]));
|
|
52
|
-
}
|
|
53
|
-
function getSkillFileEntries(appRoot) {
|
|
54
|
-
return getSkillNames().map((name) => ({
|
|
55
|
-
path: resolve(appRoot, `.agents/skills/${name}/SKILL.md`),
|
|
56
|
-
content: generateSkillContent(name, appRoot)
|
|
57
|
-
}));
|
|
58
|
-
}
|
|
59
|
-
function generateSkillFiles(appRoot) {
|
|
60
|
-
for (const name of getSkillNames()) mkdirSync(resolve(appRoot, `.agents/skills/${name}`), { recursive: true });
|
|
61
|
-
for (const entry of getSkillFileEntries(appRoot)) writeIfChanged(entry.path, entry.content);
|
|
62
|
-
ensureClaudeSkillSymlinks(appRoot);
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* For each skill directory in `.agents/skills/`, ensure a corresponding
|
|
66
|
-
* symlink exists in `.claude/skills/` pointing to it via a relative path.
|
|
67
|
-
*/
|
|
68
|
-
function ensureClaudeSkillSymlinks(appRoot) {
|
|
69
|
-
const agentsSkillsDir = resolve(appRoot, ".agents/skills");
|
|
70
|
-
const claudeSkillsDir = resolve(appRoot, ".claude/skills");
|
|
71
|
-
mkdirSync(claudeSkillsDir, { recursive: true });
|
|
72
|
-
for (const name of readdirSync(agentsSkillsDir)) {
|
|
73
|
-
if (!lstatSync(join(agentsSkillsDir, name)).isDirectory()) continue;
|
|
74
|
-
const symlinkPath = join(claudeSkillsDir, name);
|
|
75
|
-
const targetRelative = join("../../.agents/skills", name);
|
|
76
|
-
try {
|
|
77
|
-
if (readlinkSync(symlinkPath) === targetRelative) continue;
|
|
78
|
-
} catch {}
|
|
79
|
-
symlinkSync(targetRelative, symlinkPath);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
function watchSkillFiles(server, appRoot) {
|
|
83
|
-
const entries = getSkillFileEntries(appRoot);
|
|
84
|
-
const expectedByPath = new Map(entries.map((e) => [e.path, e.content]));
|
|
85
|
-
server.watcher.on("change", (changedPath) => {
|
|
86
|
-
const expected = expectedByPath.get(changedPath);
|
|
87
|
-
if (!expected) return;
|
|
88
|
-
if (readFileSync(changedPath, "utf-8") !== expected) {
|
|
89
|
-
const rel = relative(server.config.root, changedPath);
|
|
90
|
-
server.config.logger.warn(`Camox skill file "${rel}" was modified — reverting to generated version.`, { timestamp: true });
|
|
91
|
-
writeFileSync(changedPath, expected, "utf-8");
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
server.watcher.on("unlink", (deletedPath) => {
|
|
95
|
-
const expected = expectedByPath.get(deletedPath);
|
|
96
|
-
if (!expected) return;
|
|
97
|
-
const rel = relative(server.config.root, deletedPath);
|
|
98
|
-
server.config.logger.warn(`Camox skill file "${rel}" was deleted — recreating.`, { timestamp: true });
|
|
99
|
-
writeFileSync(deletedPath, expected, "utf-8");
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
//#endregion
|
|
104
|
-
export { generateSkillFiles, watchSkillFiles };
|
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: camox-cli
|
|
3
|
-
description: "How to read or modify Camox website content via the CLI: pages, routes, block instances, copy, images/files, meta fields, layout assignments, draft/live state, publishing, and dev/prod sync. Use when the user wants to add, remove, rename, reorder, review, publish, or change anything visible on the site, including sections, heroes, footers, headlines, SEO titles, social previews, or what appears at a URL. Trigger on requests like 'add a page', 'change the headline', 'move this section', 'what's on /about', 'show me the live page', 'publish this page', 'fix the meta title', 'push to production', 'pull from production', or 'sync dev with prod', even if they do not mention Camox, CMS, page, block, layout, environment, or CLI."
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Using the Camox CLI
|
|
7
|
-
|
|
8
|
-
The Camox CLI is the right tool for any **CRUD operation on CMS content** — pages, block instances, layout assignments, meta fields. Reach for it before writing a custom script or asking the user to click through the dashboard. The `camox` binary is already installed in this project (it ships with the `camox` package).
|
|
9
|
-
|
|
10
|
-
## When to use this skill vs. `camox-block` / `camox-layout`
|
|
11
|
-
|
|
12
|
-
This skill (`camox-cli`) is for **content** — the actual pages, the actual block instances on those pages, the wording and images visitors see. If the user wants to add a section to a specific page, change copy, reorder blocks, create a new route, swap which layout a page uses, or fix a meta title, you're editing content and you want the CLI.
|
|
13
|
-
|
|
14
|
-
The `camox-block` skill is for **block definitions** — the `.tsx` files in `src/blocks/` that define what _kinds_ of blocks exist (their schema, fields, and rendering). Reach for it only when the user wants to introduce a new type of section that doesn't exist yet (e.g. "we need a pricing-table section and we've never built one"), or change the schema or rendering of an existing block type.
|
|
15
|
-
|
|
16
|
-
The `camox-layout` skill is for **layout definitions** — the `.tsx` files in `src/layouts/` that define the shared shells around page content (which navbar/footer blocks wrap pages, how meta titles are built, OG images, etc.). Reach for it when the user wants a new kind of page wrapper, a different shared structure, or to change how titles or social previews are constructed. _Assigning_ an existing layout to a page is content — that's the CLI.
|
|
17
|
-
|
|
18
|
-
Rule of thumb: if the change should be visible on the live site without a code deploy, it's content → use the CLI. If it requires editing source files in `src/blocks/` or `src/layouts/`, it's a definition → use `camox-block` or `camox-layout`. Some requests need both (e.g. "add a pricing section the site has never had before" = define the block via `camox-block`, then create an instance on a page via the CLI).
|
|
19
|
-
|
|
20
|
-
## Running the CLI
|
|
21
|
-
|
|
22
|
-
This project uses **{{PM_NAME}}**. Always invoke the CLI as:
|
|
23
|
-
|
|
24
|
-
```sh
|
|
25
|
-
{{CAMOX_CMD}} <command> [options]
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
For example: `{{CAMOX_CMD}} pages list`, `{{CAMOX_CMD}} blocks types`.
|
|
29
|
-
|
|
30
|
-
## Discover commands with `--help`
|
|
31
|
-
|
|
32
|
-
The CLI surface evolves. **Don't guess command names or flags from memory — ask the CLI.** Run `--help` at the root and on every subcommand before invoking it, and treat the output as authoritative.
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
# top-level: lists command groups (pages, blocks, layouts, ...)
|
|
36
|
-
{{CAMOX_CMD}} --help
|
|
37
|
-
|
|
38
|
-
# a group: lists its subcommands
|
|
39
|
-
{{CAMOX_CMD}} pages --help
|
|
40
|
-
|
|
41
|
-
# a subcommand: lists its required and optional flags
|
|
42
|
-
{{CAMOX_CMD}} pages create --help
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
The CLI is organised into command groups around the resource types (pages, blocks, layouts, …). Use `--help` to discover what each group supports — the exact set of subcommands and flags is the CLI's responsibility, not this skill's.
|
|
46
|
-
|
|
47
|
-
## Common recipes
|
|
48
|
-
|
|
49
|
-
Most content tasks fit into a small number of shapes. Use these as the entry-point template, then verify exact flags with `--help`. Always look up the page (`pages get`) before you create or move blocks — you'll need its `id` and the `id` / `type` of any sibling block you're positioning relative to.
|
|
50
|
-
|
|
51
|
-
### Add a new block at a specific spot on a page
|
|
52
|
-
|
|
53
|
-
```sh
|
|
54
|
-
# 1) Find the page and the sibling you'll position relative to.
|
|
55
|
-
{{CAMOX_CMD}} pages get --path /
|
|
56
|
-
# read the response to grab pageId and the id of the existing first block
|
|
57
|
-
|
|
58
|
-
# 2) See what fields the block type accepts.
|
|
59
|
-
{{CAMOX_CMD}} blocks describe --type hero
|
|
60
|
-
|
|
61
|
-
# 3) Create the new block at the desired position.
|
|
62
|
-
# At the very top of the page:
|
|
63
|
-
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --position first
|
|
64
|
-
# Or right before / after a known sibling:
|
|
65
|
-
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --before-id 174
|
|
66
|
-
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}' --after-id 174
|
|
67
|
-
# Or appended to the end (default):
|
|
68
|
-
{{CAMOX_CMD}} blocks create --page-id 25 --type hero --content '{...}'
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
See **Block positioning** below for the full set of options.
|
|
72
|
-
|
|
73
|
-
### Update copy in an existing block
|
|
74
|
-
|
|
75
|
-
```sh
|
|
76
|
-
# 1) Locate the block on the page.
|
|
77
|
-
{{CAMOX_CMD}} pages get --path /pricing
|
|
78
|
-
# note the block id you want to edit
|
|
79
|
-
|
|
80
|
-
# 2) Patch only the fields you want to change — content is merged.
|
|
81
|
-
{{CAMOX_CMD}} blocks edit --id 314 --content '{"headline": "New headline"}'
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Update one item inside a repeatable field (one FAQ answer, one feature title, …)
|
|
85
|
-
|
|
86
|
-
Repeatable fields are an array of items. `blocks edit` treats the items array as the new truth: any existing item whose `_itemId` is not in your patch is **deleted** (cascading to its file references, settings, and child items). To change one item without losing the rest, fetch the block first to learn every item's id, then patch with every `_itemId` present.
|
|
87
|
-
|
|
88
|
-
```sh
|
|
89
|
-
# 1) Fetch the block's raw items (the `id` of each is its `_itemId`).
|
|
90
|
-
{{CAMOX_CMD}} blocks get --id 99
|
|
91
|
-
# in the response, `repeatableItems` lists each item with `id`, `fieldName`,
|
|
92
|
-
# `parentItemId`, and `content`. Pick the id you want to edit.
|
|
93
|
-
|
|
94
|
-
# 2) Send every item back. Empty `{"_itemId": N}` entries preserve items as-is;
|
|
95
|
-
# the entry you want to change carries the field overrides.
|
|
96
|
-
{{CAMOX_CMD}} blocks edit --id 99 --content '{
|
|
97
|
-
"items": [
|
|
98
|
-
{"_itemId": 401},
|
|
99
|
-
{"_itemId": 402, "answer": "Updated answer."},
|
|
100
|
-
{"_itemId": 403}
|
|
101
|
-
]
|
|
102
|
-
}'
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Same pattern for nested repeatables: each child item also has an `id` you pass back as `_itemId` inside the parent item's array field.
|
|
106
|
-
|
|
107
|
-
When you need to edit repeatable fields in several blocks, prefer fetching them together first:
|
|
108
|
-
|
|
109
|
-
```sh
|
|
110
|
-
{{CAMOX_CMD}} blocks get-many --id 99 --id 100 --id 101
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
`blocks get-many` returns the same bundle shape as `blocks get` for each block, in the same order as the requested ids.
|
|
114
|
-
|
|
115
|
-
### Create a new page using an existing layout
|
|
116
|
-
|
|
117
|
-
```sh
|
|
118
|
-
# 1) List layouts and pick one.
|
|
119
|
-
{{CAMOX_CMD}} layouts list
|
|
120
|
-
|
|
121
|
-
# 2) Create the page under that layout.
|
|
122
|
-
{{CAMOX_CMD}} pages create --path-segment about --layout-id 39
|
|
123
|
-
# Use --parent-page-id <ID> to nest the page under another route.
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Review a draft and publish it to live
|
|
127
|
-
|
|
128
|
-
Agents should work on **drafts** first, get the changes reviewed, and only then publish. Draft is the default source for reads and the default target for writes, so do not add `--live` while editing.
|
|
129
|
-
|
|
130
|
-
```sh
|
|
131
|
-
# 1) Inspect the draft the agent will edit/review.
|
|
132
|
-
{{CAMOX_CMD}} pages get --path /pricing
|
|
133
|
-
{{CAMOX_CMD}} blocks get --id 314
|
|
134
|
-
|
|
135
|
-
# 2) Inspect the currently published snapshot when you need a before/after.
|
|
136
|
-
{{CAMOX_CMD}} pages get --path /pricing --live
|
|
137
|
-
{{CAMOX_CMD}} blocks get --id 314 --live
|
|
138
|
-
|
|
139
|
-
# 3) After review/approval, publish the draft page to live.
|
|
140
|
-
{{CAMOX_CMD}} pages publish --path /pricing
|
|
141
|
-
|
|
142
|
-
# Use --no-layout only when you explicitly do not want to publish pending
|
|
143
|
-
# changes to the page's layout alongside the page.
|
|
144
|
-
{{CAMOX_CMD}} pages publish --path /pricing --no-layout
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
`pages publish` accepts exactly one of `--id` or `--path`. It publishes the page's current draft. By default it also publishes the page's layout; that layout publish is a no-op when there are no pending layout changes.
|
|
148
|
-
|
|
149
|
-
To remove a published page from live without deleting its draft:
|
|
150
|
-
|
|
151
|
-
```sh
|
|
152
|
-
{{CAMOX_CMD}} pages unpublish --path /pricing
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
To throw away unpublished draft edits and reset the draft to the current live snapshot:
|
|
156
|
-
|
|
157
|
-
```sh
|
|
158
|
-
{{CAMOX_CMD}} pages discard-changes --path /pricing
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
`pages unpublish` and `pages discard-changes` accept exactly one of `--id` or `--path`. `pages unpublish` leaves the draft untouched. `pages discard-changes` does not change live content and fails when the page has never been published.
|
|
162
|
-
|
|
163
|
-
`--live` is for **reading** the published snapshot. It is not a write target and it is not how you publish. Live reads error when the page or block has never been published.
|
|
164
|
-
|
|
165
|
-
### Promote dev to production (or pull production back into dev)
|
|
166
|
-
|
|
167
|
-
The `env` group replicates content in bulk between your dev environment and production — the same two operations the studio's environment menu exposes. This is separate from draft/live publishing. Use it when the user wants to copy an environment's content to the other environment, or wipe dev and start from production.
|
|
168
|
-
|
|
169
|
-
```sh
|
|
170
|
-
# Inspect both directions before committing. Shows whether push/pull are
|
|
171
|
-
# currently safe, and lists any block-definition / layout divergences if not.
|
|
172
|
-
{{CAMOX_CMD}} env check
|
|
173
|
-
|
|
174
|
-
# Overwrite production with your dev environment.
|
|
175
|
-
{{CAMOX_CMD}} env push --yes
|
|
176
|
-
|
|
177
|
-
# Overwrite your dev environment with production.
|
|
178
|
-
{{CAMOX_CMD}} env pull --yes
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
`--yes` (or `-y`) is required on `push` / `pull` — both replace every page, block, and file in the target environment and the change cannot be undone. See **Environments, drafts, and live** below for when to use this vs `pages publish`, `--live`, and `--production`.
|
|
182
|
-
|
|
183
|
-
## Block positioning
|
|
184
|
-
|
|
185
|
-
`blocks create` and `blocks move` accept the same set of positioning flags. **Pass at most one** — combining them is rejected. `move` requires one (use `--position last` to send a block to the end); `create` defaults to appending at the end if you pass none.
|
|
186
|
-
|
|
187
|
-
| Flag | Meaning |
|
|
188
|
-
| ------------------------- | -------------------------------------------------------------------- |
|
|
189
|
-
| `--position first` | Place at the very top of the page. |
|
|
190
|
-
| `--position last` | Place at the very bottom of the page (also the create-time default). |
|
|
191
|
-
| `--after-id <ID>` | Place immediately after the sibling block with that id. |
|
|
192
|
-
| `--before-id <ID>` | Place immediately before the sibling block with that id. |
|
|
193
|
-
| `--after-position <KEY>` | Low-level: insert after a known fractional-index key. |
|
|
194
|
-
| `--before-position <KEY>` | Low-level: insert before a known fractional-index key. |
|
|
195
|
-
|
|
196
|
-
Prefer the high-level flags (`--position`, `--after-id`, `--before-id`) — they read naturally and don't require you to know the fractional-index format. The `--after-position` / `--before-position` flags exist for cases where you already have a key in hand (e.g. piping output between commands).
|
|
197
|
-
|
|
198
|
-
## Environments, drafts, and live
|
|
199
|
-
|
|
200
|
-
Every project has two environments: a per-user, isolated **dev** environment and the shared **production** environment. Inside each environment, content has a **draft** working copy and a **live** published snapshot.
|
|
201
|
-
|
|
202
|
-
### Draft-first workflow
|
|
203
|
-
|
|
204
|
-
By default, reads and writes operate on the draft. This is intentional: agents should make content changes in draft, summarize what changed, let the user review, and publish only after approval.
|
|
205
|
-
|
|
206
|
-
```sh
|
|
207
|
-
{{CAMOX_CMD}} pages get --path /about # draft
|
|
208
|
-
{{CAMOX_CMD}} blocks edit --id 314 --content '{"headline": "New headline"}'
|
|
209
|
-
{{CAMOX_CMD}} pages publish --path /about # after review
|
|
210
|
-
{{CAMOX_CMD}} pages discard-changes --path /about # reset draft to live
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
Use `--live` only to compare against the published snapshot:
|
|
214
|
-
|
|
215
|
-
```sh
|
|
216
|
-
{{CAMOX_CMD}} pages get --path /about --live
|
|
217
|
-
{{CAMOX_CMD}} blocks get --id 314 --live
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Per-command: the `--production` flag
|
|
221
|
-
|
|
222
|
-
By default, every command runs against the dev environment. Pass `--production` to target the production environment instead, one command at a time:
|
|
223
|
-
|
|
224
|
-
```sh
|
|
225
|
-
{{CAMOX_CMD}} pages list --production
|
|
226
|
-
{{CAMOX_CMD}} blocks edit --id <ID> --production
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
`--production` selects the environment; it does not mean "published". Production still has draft and live state. To read production's published snapshot, combine `--production` with `--live`. To publish a production draft, use `pages publish --production`.
|
|
230
|
-
|
|
231
|
-
Only use `--production` when the user has explicitly asked to operate on the production environment. For everything else — exploration, tentative edits, anything you'd want to be able to throw away — stay on the default dev environment.
|
|
232
|
-
|
|
233
|
-
### Bulk replication: the `env` group
|
|
234
|
-
|
|
235
|
-
When the user wants to **copy dev to production** (or do the inverse — wipe dev and start from production), use the env group instead of running many commands with `--production`:
|
|
236
|
-
|
|
237
|
-
```sh
|
|
238
|
-
{{CAMOX_CMD}} env check # report compatibility of both push and pull
|
|
239
|
-
{{CAMOX_CMD}} env push --yes # overwrite production with dev
|
|
240
|
-
{{CAMOX_CMD}} env pull --yes # overwrite dev with production
|
|
241
|
-
```
|
|
242
|
-
|
|
243
|
-
`env check` returns `{ push: { compatible, reasons }, pull: { compatible, reasons } }`. The `reasons` array is empty when that direction is safe; otherwise each entry names a divergence (a block type or layout that exists in one env but not the other, or a block-definition schema mismatch). Fix the divergence first — replicate will refuse with `FAILED_PRECONDITION` and the same reasons in `data.reasons` if you don't.
|
|
244
|
-
|
|
245
|
-
`env push` and `env pull` require `--yes` (or `-y`) because they replace the target environment's entire content. Without it the command refuses and prints what it would have done.
|
|
246
|
-
|
|
247
|
-
## Don't write slop — build understanding first
|
|
248
|
-
|
|
249
|
-
Anything you create with this CLI ends up on a real website read by real people. **Never invent generic filler copy** ("Welcome to our amazing platform", "Lorem ipsum"-grade headlines, plausible-sounding-but-fabricated stats, made-up testimonials, fake company names). That kind of content is worse than nothing — it ships, it gets indexed, and the user has to clean it up.
|
|
250
|
-
|
|
251
|
-
Before writing any block content:
|
|
252
|
-
|
|
253
|
-
1. **Read what already exists.** Use the CLI to list pages and inspect existing blocks (`{{CAMOX_CMD}} pages list`, `{{CAMOX_CMD}} pages get …`, `{{CAMOX_CMD}} blocks describe …`, etc. — discover the exact commands via `--help`). The site's voice, product positioning, naming, and recurring claims are usually already established somewhere; mirror them. A new "About" block on a site that already has hero/feature copy should sound like a continuation of that copy, not a fresh marketing draft.
|
|
254
|
-
2. **Pull real facts from the right source.** If the user gave you the content, use it verbatim. If the content describes something external (a person, company, product, paper, event, library), use web search or whatever fetch tools you have to look up actual details before writing. If you genuinely can't get a fact, ask the user — don't paper over it with a guess.
|
|
255
|
-
3. **For `File` and `Embed` fields, do not guess URLs.** These point at real assets (PDFs, videos, embeds). A hallucinated URL produces a broken link or, worse, a link to someone else's content. Leave the URL field blank and tell the user the asset still needs to be supplied — the CMS will treat the empty value correctly, and `toMarkdown` will skip the line.
|
|
256
|
-
4. **Same for `Image`.** Don't fabricate filenames. If you don't have a real uploaded asset to reference, leave it empty and flag it.
|
|
257
|
-
|
|
258
|
-
Short version: if you're tempted to "make something up that sounds about right", stop and either go find the real thing or hand the gap back to the user.
|
|
259
|
-
|
|
260
|
-
## Workflow
|
|
261
|
-
|
|
262
|
-
1. If the task matches one of the **Common recipes** above, start from that template — it's almost always the right shape.
|
|
263
|
-
2. Otherwise, run `{{CAMOX_CMD}} --help` to find the right command group, then `{{CAMOX_CMD}} <group> --help` and `{{CAMOX_CMD}} <group> <command> --help` to confirm the exact flags.
|
|
264
|
-
3. Before writing content, read existing pages/blocks and gather any external facts you need (see above).
|
|
265
|
-
4. Work on draft first. Omit `--live` while editing; use `--live` only for reading the published snapshot for comparison.
|
|
266
|
-
5. Get the draft changes reviewed. Summarize the affected page(s), block ids, and notable copy/structure changes.
|
|
267
|
-
6. Publish with `{{CAMOX_CMD}} pages publish --id <ID>` or `{{CAMOX_CMD}} pages publish --path <PATH>` only after the user asks or approves.
|
|
268
|
-
7. Add `--production` only when the user has asked to operate on the production environment.
|