@camox/cli 0.42.0 → 0.43.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 +125 -0
- package/dist/index.mjs +426 -45
- package/package.json +3 -2
- package/templates/default/package.json +1 -0
- package/templates/default/scripts/README.md +35 -0
- package/templates/default/scripts/production-smoke.mjs +124 -0
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Camox CLI
|
|
2
|
+
|
|
3
|
+
## Runtime lookup and monorepos
|
|
4
|
+
|
|
5
|
+
Runtime-backed commands (`status`, `pages`, `blocks`, `layouts`, `env`, `files`,
|
|
6
|
+
and `logout`) accept `--cwd PATH`:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
camox pages list --cwd apps/site
|
|
10
|
+
camox status --cwd apps/site
|
|
11
|
+
camox files upload --cwd apps/site --file ./hero.webp --alt "Product screenshot"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The CLI searches for `node_modules/.camox/runtime.json` starting in that directory
|
|
15
|
+
and walking upward. Without `--cwd`, lookup starts in the shell's current directory.
|
|
16
|
+
There is no workspace or child-directory discovery. Run the app's dev server or
|
|
17
|
+
build once to generate its runtime file.
|
|
18
|
+
|
|
19
|
+
Relative `--cwd` paths resolve against the shell's current directory. This option
|
|
20
|
+
only affects runtime lookup: other paths, such as `--file ./hero.webp`, still
|
|
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.
|
|
24
|
+
|
|
25
|
+
## Page SEO
|
|
26
|
+
|
|
27
|
+
Use `pages get` to read metadata and `pages update` to edit it:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
camox pages get --path /about --json
|
|
31
|
+
camox pages update --path /about --meta-title "About us" --meta-description "Meet the team."
|
|
32
|
+
camox pages update --id 42 --meta-description ""
|
|
33
|
+
camox pages update --id 42 --ai-seo on
|
|
34
|
+
camox pages update --id 42 --ai-seo off
|
|
35
|
+
camox pages get --path /about --live
|
|
36
|
+
camox pages publish --path /about
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
- Updates require exactly one of `--id` or `--path` and at least one update field.
|
|
40
|
+
Omitted fields are preserved; empty strings clear metadata.
|
|
41
|
+
- Setting either metadata field disables automatic SEO for **both** fields.
|
|
42
|
+
Manual metadata cannot be combined with `--ai-seo on`.
|
|
43
|
+
- `--ai-seo off` preserves current metadata. `--ai-seo on` schedules asynchronous
|
|
44
|
+
generation; the returned page contains current values, not necessarily generated
|
|
45
|
+
values. Use `pages get` to inspect them later.
|
|
46
|
+
- Changes affect the draft only. Publish separately to update live metadata.
|
|
47
|
+
`pages get --live` reads the published snapshot; without it, reads use the draft.
|
|
48
|
+
- Commands support `--project SLUG`, `--production`, and `--json`, with the usual
|
|
49
|
+
development-environment default and structured JSON output/errors. `--production`
|
|
50
|
+
selects the environment; it does not publish the draft.
|
|
51
|
+
|
|
52
|
+
See `camox pages update --help` for options and examples.
|
|
53
|
+
|
|
54
|
+
## Media
|
|
55
|
+
|
|
56
|
+
Run `camox login` and start your project's dev server (or build once) so the CLI
|
|
57
|
+
can discover the project's runtime configuration.
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
# Upload a local file with manual alt text.
|
|
61
|
+
camox files upload --file ./hero.webp --alt "Product screenshot" --json
|
|
62
|
+
|
|
63
|
+
# Download a remote file and store a copy in Camox.
|
|
64
|
+
camox files upload --url https://example.com/hero.webp --alt "Product screenshot"
|
|
65
|
+
|
|
66
|
+
# Optionally override the uploaded filename.
|
|
67
|
+
camox files upload --url https://example.com/download --filename hero.webp --ai-metadata off
|
|
68
|
+
|
|
69
|
+
# Inspect the returned ID and URL; use the ID in your block's media field.
|
|
70
|
+
camox files list
|
|
71
|
+
camox files get --id 123
|
|
72
|
+
|
|
73
|
+
camox files update --id 123 --filename hero.webp
|
|
74
|
+
camox files update --id 123 --alt "Updated description"
|
|
75
|
+
camox files update --id 123 --alt ""
|
|
76
|
+
camox files update --id 123 --ai-metadata on
|
|
77
|
+
|
|
78
|
+
# Replace the underlying file everywhere it is referenced, preserving its ID.
|
|
79
|
+
camox files update --id 123 --file ./new-hero.webp
|
|
80
|
+
camox files update --id 123 --url https://example.com/new-hero.webp
|
|
81
|
+
|
|
82
|
+
# Replace content and override metadata together.
|
|
83
|
+
camox files update --id 123 --file ./new-hero.webp --filename hero.webp --alt "New screenshot"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- Upload requires exactly one of `--file` or `--url`. URLs are copied, not linked.
|
|
87
|
+
- `--alt` disables AI metadata, including when its value is an empty string.
|
|
88
|
+
It cannot be combined with `--ai-metadata on`. Manual edits are protected from
|
|
89
|
+
already-running AI jobs.
|
|
90
|
+
- Without metadata flags, raster-image uploads use automatic metadata. Other
|
|
91
|
+
file types do not. AI may update both the filename and alt text asynchronously.
|
|
92
|
+
- `--ai-metadata on` schedules generation for raster images; success does not mean
|
|
93
|
+
generation has completed. Use `files get` to inspect the current record.
|
|
94
|
+
- Update requires at least one of `--file`, `--url`, `--filename`, `--alt` or
|
|
95
|
+
`--ai-metadata`. `--file` and `--url` are mutually exclusive; without either,
|
|
96
|
+
only metadata changes. Renaming alone preserves the file ID, URL and AI setting.
|
|
97
|
+
- Replacing content preserves the file ID and all `_fileId` references in the
|
|
98
|
+
selected environment. The URL changes to avoid stale caches. Direct URLs in
|
|
99
|
+
stored block/repeatable-item content are migrated within that environment;
|
|
100
|
+
URLs hardcoded outside Camox cannot be rewritten.
|
|
101
|
+
- Replacements preserve omitted alt text and AI settings. The filename defaults
|
|
102
|
+
to the new source filename; `--filename` overrides it. Binary metadata and
|
|
103
|
+
explicit metadata changes are saved together. If AI remains enabled, raster
|
|
104
|
+
replacements schedule generation on the original file ID; other types skip
|
|
105
|
+
generation. Disable AI to prevent future automatic renaming or alt changes.
|
|
106
|
+
- Files are shared assets, not page drafts: replacing one affects every reference
|
|
107
|
+
to it in the selected environment. No page publish step is required.
|
|
108
|
+
- Failed replacements leave the original file unchanged and remove the unused
|
|
109
|
+
uploaded binary. After a network timeout, use `files get --id ID` before retrying
|
|
110
|
+
because the server may already have completed the operation.
|
|
111
|
+
- All commands support `--project SLUG`, `--production`, and `--json`. The default
|
|
112
|
+
environment is `dev:<your-email>`, not production. IDs are scoped to the selected
|
|
113
|
+
project and environment.
|
|
114
|
+
- Uploads return the stored file record, including `id`, `url`, `mimeType`, `alt`,
|
|
115
|
+
and `aiMetadataEnabled`. Output is JSON; errors are structured JSON on stderr.
|
|
116
|
+
- Files are limited to 100 MiB. URL downloads allow at most five redirects and
|
|
117
|
+
have a two-minute timeout; uploads have a separate two-minute timeout.
|
|
118
|
+
Downloads stream through a temporary file that is removed afterward.
|
|
119
|
+
- Only HTTP(S) URLs without embedded credentials are supported. Signed query
|
|
120
|
+
URLs work and are not included in download errors. Camox credentials are never
|
|
121
|
+
sent to the source URL. Remote authentication flags and batch uploads are not
|
|
122
|
+
supported.
|
|
123
|
+
|
|
124
|
+
See `camox files --help`, `camox files upload --help`, and
|
|
125
|
+
`camox files update --help` for command options.
|
package/dist/index.mjs
CHANGED
|
@@ -12,9 +12,11 @@ import { execSync, spawn, spawnSync } from "node:child_process";
|
|
|
12
12
|
import http from "node:http";
|
|
13
13
|
import * as p from "@clack/prompts";
|
|
14
14
|
import { log } from "@clack/prompts";
|
|
15
|
-
import fs from "node:fs";
|
|
16
|
-
import
|
|
15
|
+
import fs, { openAsBlob } from "node:fs";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
import path, { basename, extname, join } from "node:path";
|
|
17
18
|
import { ZodError, z } from "zod";
|
|
19
|
+
import { mkdtemp, open, rm, stat } from "node:fs/promises";
|
|
18
20
|
import { fileURLToPath } from "node:url";
|
|
19
21
|
import slugify from "slugify";
|
|
20
22
|
//#region src/lib/auth.ts
|
|
@@ -188,11 +190,17 @@ const SIDECAR = path.join("node_modules", ".camox", "runtime.json");
|
|
|
188
190
|
var RuntimeNotFoundError = class extends Error {
|
|
189
191
|
cwd;
|
|
190
192
|
constructor(cwd) {
|
|
191
|
-
super(`No
|
|
193
|
+
super(`No Camox runtime found. Looked for "${SIDECAR}" starting from ${cwd} and its parent directories. Change into your app directory or pass --cwd <app-path>. If you're already targeting the correct app, run its dev server or build once to generate the runtime file.`);
|
|
192
194
|
this.cwd = cwd;
|
|
193
195
|
this.name = "RuntimeNotFoundError";
|
|
194
196
|
}
|
|
195
197
|
};
|
|
198
|
+
var RuntimeDirectoryError = class extends Error {
|
|
199
|
+
constructor(directory) {
|
|
200
|
+
super(`Runtime lookup directory "${directory}" does not exist, is not a directory, or cannot be accessed. Check --cwd <app-path>.`);
|
|
201
|
+
this.name = "RuntimeDirectoryError";
|
|
202
|
+
}
|
|
203
|
+
};
|
|
196
204
|
var RuntimeMalformedError = class extends Error {
|
|
197
205
|
constructor(file, reason) {
|
|
198
206
|
super(`Camox runtime sidecar at ${file} is malformed (${reason}). Re-run your project's dev server to regenerate it.`);
|
|
@@ -205,7 +213,13 @@ var RuntimeMalformedError = class extends Error {
|
|
|
205
213
|
* that tell the user how to fix it.
|
|
206
214
|
*/
|
|
207
215
|
function loadRuntime(cwd = process.cwd()) {
|
|
208
|
-
let dir = cwd;
|
|
216
|
+
let dir = path.resolve(cwd);
|
|
217
|
+
try {
|
|
218
|
+
if (!fs.statSync(dir).isDirectory()) throw new RuntimeDirectoryError(dir);
|
|
219
|
+
} catch {
|
|
220
|
+
throw new RuntimeDirectoryError(dir);
|
|
221
|
+
}
|
|
222
|
+
const start = dir;
|
|
209
223
|
while (true) {
|
|
210
224
|
const candidate = path.join(dir, SIDECAR);
|
|
211
225
|
if (fs.existsSync(candidate)) {
|
|
@@ -223,7 +237,7 @@ function loadRuntime(cwd = process.cwd()) {
|
|
|
223
237
|
}
|
|
224
238
|
}
|
|
225
239
|
const parent = path.dirname(dir);
|
|
226
|
-
if (parent === dir) throw new RuntimeNotFoundError(
|
|
240
|
+
if (parent === dir) throw new RuntimeNotFoundError(start);
|
|
227
241
|
dir = parent;
|
|
228
242
|
}
|
|
229
243
|
}
|
|
@@ -239,7 +253,7 @@ function compact(input) {
|
|
|
239
253
|
for (const [k, v] of Object.entries(input)) if (v !== void 0) out[k] = v;
|
|
240
254
|
return out;
|
|
241
255
|
}
|
|
242
|
-
function fail(err, code) {
|
|
256
|
+
function fail$1(err, code) {
|
|
243
257
|
printError(err);
|
|
244
258
|
process.exit(code);
|
|
245
259
|
}
|
|
@@ -247,7 +261,7 @@ async function resolveProjectId(token, slug, apiUrl) {
|
|
|
247
261
|
try {
|
|
248
262
|
return (await getProjectBySlug(token, slug, apiUrl)).id;
|
|
249
263
|
} catch (err) {
|
|
250
|
-
return fail({
|
|
264
|
+
return fail$1({
|
|
251
265
|
code: "PROJECT_LOOKUP_FAILED",
|
|
252
266
|
message: `Could not load project "${slug}".`,
|
|
253
267
|
details: err instanceof Error ? err.message : String(err)
|
|
@@ -258,7 +272,7 @@ async function callRemote(params) {
|
|
|
258
272
|
try {
|
|
259
273
|
return await callTool(params);
|
|
260
274
|
} catch (err) {
|
|
261
|
-
return fail(asCliError(err), 1);
|
|
275
|
+
return fail$1(asCliError(err), 1);
|
|
262
276
|
}
|
|
263
277
|
}
|
|
264
278
|
/**
|
|
@@ -272,12 +286,16 @@ async function callRemote(params) {
|
|
|
272
286
|
* from the sidecar — it's derived from auth (`dev:<email>`) by default,
|
|
273
287
|
* with `--production` as the only opt-in for prod.
|
|
274
288
|
*/
|
|
275
|
-
async function
|
|
289
|
+
async function resolveCommandContext(opts) {
|
|
276
290
|
let runtime;
|
|
277
291
|
try {
|
|
278
|
-
runtime = loadRuntime();
|
|
292
|
+
runtime = loadRuntime(opts.cwd);
|
|
279
293
|
} catch (err) {
|
|
280
|
-
if (err instanceof
|
|
294
|
+
if (err instanceof RuntimeDirectoryError) return fail$1({
|
|
295
|
+
code: "INVALID_CWD",
|
|
296
|
+
message: err.message
|
|
297
|
+
}, 2);
|
|
298
|
+
if (err instanceof RuntimeNotFoundError || err instanceof RuntimeMalformedError) return fail$1({
|
|
281
299
|
code: "RUNTIME_NOT_FOUND",
|
|
282
300
|
message: err.message
|
|
283
301
|
}, 2);
|
|
@@ -285,24 +303,29 @@ async function dispatch(opts) {
|
|
|
285
303
|
}
|
|
286
304
|
const slug = opts.projectFlag?.trim() || process.env.CAMOX_PROJECT?.trim() || runtime.projectSlug;
|
|
287
305
|
const token = readAuthTokenForUrl(runtime.authenticationUrl);
|
|
288
|
-
if (!token) return fail({
|
|
306
|
+
if (!token) return fail$1({
|
|
289
307
|
code: "NOT_AUTHENTICATED",
|
|
290
308
|
message: `No stored credentials for ${runtime.authenticationUrl}. Run \`camox login\` against this backend first.`
|
|
291
309
|
}, 2);
|
|
292
|
-
if (!opts.production && !token.email) return fail({
|
|
310
|
+
if (!opts.production && !token.email) return fail$1({
|
|
293
311
|
code: "AUTH_INCOMPLETE",
|
|
294
312
|
message: "Auth token is missing an email. Run `camox login` to refresh credentials."
|
|
295
313
|
}, 2);
|
|
296
314
|
const environmentName = opts.production ? "production" : `dev:${token.email}`;
|
|
297
315
|
const projectId = await resolveProjectId(token.token, slug, runtime.apiUrl);
|
|
298
|
-
|
|
316
|
+
return {
|
|
299
317
|
token: token.token,
|
|
300
318
|
apiUrl: runtime.apiUrl,
|
|
301
319
|
environmentName,
|
|
302
320
|
projectId,
|
|
303
|
-
name: opts.toolName,
|
|
304
|
-
args: compact(opts.args),
|
|
305
321
|
disableTelemetry: runtime.disableTelemetry
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
async function dispatch(opts) {
|
|
325
|
+
const response = await callRemote({
|
|
326
|
+
...await resolveCommandContext(opts),
|
|
327
|
+
name: opts.toolName,
|
|
328
|
+
args: compact(opts.args)
|
|
306
329
|
});
|
|
307
330
|
if (!response.ok) {
|
|
308
331
|
printError(response.error);
|
|
@@ -312,19 +335,24 @@ async function dispatch(opts) {
|
|
|
312
335
|
process.exit(0);
|
|
313
336
|
}
|
|
314
337
|
//#endregion
|
|
338
|
+
//#region src/lib/runtime-options.ts
|
|
339
|
+
const cwdFlag = optional(option("--cwd", string({ metavar: "PATH" }), { description: message`Start runtime lookup in this directory and walk upward. Relative to the shell's current directory; does not change how other file paths resolve.` }));
|
|
340
|
+
//#endregion
|
|
315
341
|
//#region src/commands/blocks.ts
|
|
316
342
|
const projectFlag$3 = optional(option("--project", string({ metavar: "SLUG" })));
|
|
317
343
|
const jsonFlag$3 = option("--json");
|
|
318
344
|
const productionFlag$2 = option("--production");
|
|
319
345
|
const liveFlag$1 = option("--live");
|
|
320
|
-
const parser$
|
|
346
|
+
const parser$9 = command("blocks", or(command("types", object({
|
|
321
347
|
command: constant("blocks.types"),
|
|
348
|
+
cwd: cwdFlag,
|
|
322
349
|
project: projectFlag$3,
|
|
323
350
|
production: productionFlag$2,
|
|
324
351
|
json: jsonFlag$3
|
|
325
352
|
})), command("describe", object({
|
|
326
353
|
command: constant("blocks.describe"),
|
|
327
354
|
type: multiple(option("--type", string({ metavar: "TYPE" })), { min: 1 }),
|
|
355
|
+
cwd: cwdFlag,
|
|
328
356
|
project: projectFlag$3,
|
|
329
357
|
production: productionFlag$2,
|
|
330
358
|
json: jsonFlag$3
|
|
@@ -332,6 +360,7 @@ const parser$8 = command("blocks", or(command("types", object({
|
|
|
332
360
|
command: constant("blocks.get"),
|
|
333
361
|
id: option("--id", integer({ metavar: "ID" })),
|
|
334
362
|
live: liveFlag$1,
|
|
363
|
+
cwd: cwdFlag,
|
|
335
364
|
project: projectFlag$3,
|
|
336
365
|
production: productionFlag$2,
|
|
337
366
|
json: jsonFlag$3
|
|
@@ -339,6 +368,7 @@ const parser$8 = command("blocks", or(command("types", object({
|
|
|
339
368
|
command: constant("blocks.get-many"),
|
|
340
369
|
id: multiple(option("--id", integer({ metavar: "ID" })), { min: 1 }),
|
|
341
370
|
live: liveFlag$1,
|
|
371
|
+
cwd: cwdFlag,
|
|
342
372
|
project: projectFlag$3,
|
|
343
373
|
production: productionFlag$2,
|
|
344
374
|
json: jsonFlag$3
|
|
@@ -353,6 +383,7 @@ const parser$8 = command("blocks", or(command("types", object({
|
|
|
353
383
|
beforeId: optional(option("--before-id", integer({ metavar: "ID" }))),
|
|
354
384
|
afterPosition: optional(option("--after-position", string({ metavar: "POS" }))),
|
|
355
385
|
beforePosition: optional(option("--before-position", string({ metavar: "POS" }))),
|
|
386
|
+
cwd: cwdFlag,
|
|
356
387
|
project: projectFlag$3,
|
|
357
388
|
production: productionFlag$2,
|
|
358
389
|
json: jsonFlag$3
|
|
@@ -361,6 +392,7 @@ const parser$8 = command("blocks", or(command("types", object({
|
|
|
361
392
|
id: option("--id", integer({ metavar: "ID" })),
|
|
362
393
|
content: optional(option("--content", string({ metavar: "JSON" }))),
|
|
363
394
|
settings: optional(option("--settings", string({ metavar: "JSON" }))),
|
|
395
|
+
cwd: cwdFlag,
|
|
364
396
|
project: projectFlag$3,
|
|
365
397
|
production: productionFlag$2,
|
|
366
398
|
json: jsonFlag$3
|
|
@@ -372,12 +404,14 @@ const parser$8 = command("blocks", or(command("types", object({
|
|
|
372
404
|
beforeId: optional(option("--before-id", integer({ metavar: "ID" }))),
|
|
373
405
|
afterPosition: optional(option("--after-position", string({ metavar: "POS" }))),
|
|
374
406
|
beforePosition: optional(option("--before-position", string({ metavar: "POS" }))),
|
|
407
|
+
cwd: cwdFlag,
|
|
375
408
|
project: projectFlag$3,
|
|
376
409
|
production: productionFlag$2,
|
|
377
410
|
json: jsonFlag$3
|
|
378
411
|
})), command("delete", object({
|
|
379
412
|
command: constant("blocks.delete"),
|
|
380
413
|
id: option("--id", integer({ metavar: "ID" })),
|
|
414
|
+
cwd: cwdFlag,
|
|
381
415
|
project: projectFlag$3,
|
|
382
416
|
production: productionFlag$2,
|
|
383
417
|
json: jsonFlag$3
|
|
@@ -408,8 +442,9 @@ function collectPositioningArgs(args) {
|
|
|
408
442
|
toolArgs
|
|
409
443
|
};
|
|
410
444
|
}
|
|
411
|
-
async function handler$
|
|
445
|
+
async function handler$9(args) {
|
|
412
446
|
const outputMode = args.json ? "json" : "auto";
|
|
447
|
+
const cwd = args.cwd;
|
|
413
448
|
const projectFlag = args.project;
|
|
414
449
|
const production = args.production;
|
|
415
450
|
try {
|
|
@@ -417,6 +452,7 @@ async function handler$8(args) {
|
|
|
417
452
|
case "blocks.types": return dispatch({
|
|
418
453
|
toolName: "listBlockTypes",
|
|
419
454
|
args: {},
|
|
455
|
+
cwd,
|
|
420
456
|
projectFlag,
|
|
421
457
|
production,
|
|
422
458
|
outputMode
|
|
@@ -424,6 +460,7 @@ async function handler$8(args) {
|
|
|
424
460
|
case "blocks.describe": return dispatch({
|
|
425
461
|
toolName: "describeBlockTypes",
|
|
426
462
|
args: { types: [...args.type] },
|
|
463
|
+
cwd,
|
|
427
464
|
projectFlag,
|
|
428
465
|
production,
|
|
429
466
|
outputMode
|
|
@@ -434,6 +471,7 @@ async function handler$8(args) {
|
|
|
434
471
|
id: args.id,
|
|
435
472
|
source: args.live ? "live" : "draft"
|
|
436
473
|
},
|
|
474
|
+
cwd,
|
|
437
475
|
projectFlag,
|
|
438
476
|
production,
|
|
439
477
|
outputMode
|
|
@@ -444,6 +482,7 @@ async function handler$8(args) {
|
|
|
444
482
|
ids: [...args.id],
|
|
445
483
|
source: args.live ? "live" : "draft"
|
|
446
484
|
},
|
|
485
|
+
cwd,
|
|
447
486
|
projectFlag,
|
|
448
487
|
production,
|
|
449
488
|
outputMode
|
|
@@ -468,6 +507,7 @@ async function handler$8(args) {
|
|
|
468
507
|
settings,
|
|
469
508
|
...toolArgs
|
|
470
509
|
},
|
|
510
|
+
cwd,
|
|
471
511
|
projectFlag,
|
|
472
512
|
production,
|
|
473
513
|
outputMode
|
|
@@ -483,6 +523,7 @@ async function handler$8(args) {
|
|
|
483
523
|
content,
|
|
484
524
|
settings
|
|
485
525
|
},
|
|
526
|
+
cwd,
|
|
486
527
|
projectFlag,
|
|
487
528
|
production,
|
|
488
529
|
outputMode
|
|
@@ -510,6 +551,7 @@ async function handler$8(args) {
|
|
|
510
551
|
id: args.id,
|
|
511
552
|
...toolArgs
|
|
512
553
|
},
|
|
554
|
+
cwd,
|
|
513
555
|
projectFlag,
|
|
514
556
|
production,
|
|
515
557
|
outputMode
|
|
@@ -518,6 +560,7 @@ async function handler$8(args) {
|
|
|
518
560
|
case "blocks.delete": return dispatch({
|
|
519
561
|
toolName: "deleteBlock",
|
|
520
562
|
args: { id: args.id },
|
|
563
|
+
cwd,
|
|
521
564
|
projectFlag,
|
|
522
565
|
production,
|
|
523
566
|
outputMode
|
|
@@ -533,28 +576,33 @@ async function handler$8(args) {
|
|
|
533
576
|
const projectFlag$2 = optional(option("--project", string({ metavar: "SLUG" })));
|
|
534
577
|
const jsonFlag$2 = option("--json");
|
|
535
578
|
const yesFlag = option("--yes", "-y");
|
|
536
|
-
const parser$
|
|
579
|
+
const parser$8 = command("env", or(command("check", object({
|
|
537
580
|
command: constant("env.check"),
|
|
581
|
+
cwd: cwdFlag,
|
|
538
582
|
project: projectFlag$2,
|
|
539
583
|
json: jsonFlag$2
|
|
540
584
|
})), command("push", object({
|
|
541
585
|
command: constant("env.push"),
|
|
542
586
|
yes: yesFlag,
|
|
587
|
+
cwd: cwdFlag,
|
|
543
588
|
project: projectFlag$2,
|
|
544
589
|
json: jsonFlag$2
|
|
545
590
|
})), command("pull", object({
|
|
546
591
|
command: constant("env.pull"),
|
|
547
592
|
yes: yesFlag,
|
|
593
|
+
cwd: cwdFlag,
|
|
548
594
|
project: projectFlag$2,
|
|
549
595
|
json: jsonFlag$2
|
|
550
596
|
}))));
|
|
551
|
-
async function handler$
|
|
597
|
+
async function handler$8(args) {
|
|
552
598
|
const outputMode = args.json ? "json" : "auto";
|
|
599
|
+
const cwd = args.cwd;
|
|
553
600
|
const projectFlag = args.project;
|
|
554
601
|
switch (args.command) {
|
|
555
602
|
case "env.check": return dispatch({
|
|
556
603
|
toolName: "checkEnvironmentCompatibility",
|
|
557
604
|
args: {},
|
|
605
|
+
cwd,
|
|
558
606
|
projectFlag,
|
|
559
607
|
outputMode
|
|
560
608
|
});
|
|
@@ -571,12 +619,265 @@ async function handler$7(args) {
|
|
|
571
619
|
return dispatch({
|
|
572
620
|
toolName: "replicateEnvironment",
|
|
573
621
|
args: { direction: isPush ? "push" : "pull" },
|
|
622
|
+
cwd,
|
|
574
623
|
projectFlag,
|
|
575
624
|
outputMode
|
|
576
625
|
});
|
|
577
626
|
}
|
|
578
627
|
}
|
|
579
628
|
}
|
|
629
|
+
const TIMEOUT_MS = 12e4;
|
|
630
|
+
const MAX_REDIRECTS = 5;
|
|
631
|
+
function fail(code, message) {
|
|
632
|
+
throw Object.assign(new Error(message), { __cliError: {
|
|
633
|
+
code,
|
|
634
|
+
message
|
|
635
|
+
} });
|
|
636
|
+
}
|
|
637
|
+
function validateMetadata(options) {
|
|
638
|
+
if (options.alt !== void 0 && options.aiMetadata === "on") fail("INVALID_ARGS", "--alt cannot be combined with --ai-metadata on.");
|
|
639
|
+
}
|
|
640
|
+
function validateUpload(options) {
|
|
641
|
+
if (options.file !== void 0 === (options.url !== void 0)) fail("INVALID_ARGS", "Pass exactly one of --file or --url.");
|
|
642
|
+
validateMetadata(options);
|
|
643
|
+
validateFilename(options.filename);
|
|
644
|
+
}
|
|
645
|
+
function validateFilename(filename) {
|
|
646
|
+
if (filename !== void 0 && (!filename.trim() || /[\\/]/.test(filename) || filename.split("").some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) || filename === "." || filename === "..")) fail("INVALID_ARGS", "--filename must be a non-empty filename without directories or control characters.");
|
|
647
|
+
}
|
|
648
|
+
function httpUrl(value) {
|
|
649
|
+
let url;
|
|
650
|
+
try {
|
|
651
|
+
url = new URL(value);
|
|
652
|
+
} catch {
|
|
653
|
+
return fail("DOWNLOAD_FAILED", "Invalid media URL.");
|
|
654
|
+
}
|
|
655
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password) fail("DOWNLOAD_FAILED", "Media URLs must use HTTP(S) without embedded credentials.");
|
|
656
|
+
return url;
|
|
657
|
+
}
|
|
658
|
+
const MIME_TYPES = {
|
|
659
|
+
".jpg": "image/jpeg",
|
|
660
|
+
".jpeg": "image/jpeg",
|
|
661
|
+
".png": "image/png",
|
|
662
|
+
".webp": "image/webp",
|
|
663
|
+
".gif": "image/gif",
|
|
664
|
+
".avif": "image/avif",
|
|
665
|
+
".svg": "image/svg+xml",
|
|
666
|
+
".ico": "image/x-icon",
|
|
667
|
+
".bmp": "image/bmp",
|
|
668
|
+
".pdf": "application/pdf",
|
|
669
|
+
".mp4": "video/mp4",
|
|
670
|
+
".webm": "video/webm",
|
|
671
|
+
".mov": "video/quicktime",
|
|
672
|
+
".mp3": "audio/mpeg",
|
|
673
|
+
".wav": "audio/wav",
|
|
674
|
+
".ogg": "audio/ogg",
|
|
675
|
+
".txt": "text/plain",
|
|
676
|
+
".json": "application/json"
|
|
677
|
+
};
|
|
678
|
+
function mimeType(filename) {
|
|
679
|
+
return MIME_TYPES[extname(filename).toLowerCase()] ?? "application/octet-stream";
|
|
680
|
+
}
|
|
681
|
+
async function download(source, destination) {
|
|
682
|
+
try {
|
|
683
|
+
let url = httpUrl(source);
|
|
684
|
+
const signal = AbortSignal.timeout(TIMEOUT_MS);
|
|
685
|
+
for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
|
|
686
|
+
const response = await fetch(url, {
|
|
687
|
+
redirect: "manual",
|
|
688
|
+
signal
|
|
689
|
+
});
|
|
690
|
+
if ([
|
|
691
|
+
301,
|
|
692
|
+
302,
|
|
693
|
+
303,
|
|
694
|
+
307,
|
|
695
|
+
308
|
|
696
|
+
].includes(response.status)) {
|
|
697
|
+
await response.body?.cancel();
|
|
698
|
+
const location = response.headers.get("location");
|
|
699
|
+
if (!location || redirects === MAX_REDIRECTS) fail("DOWNLOAD_FAILED", "Missing redirect location or too many redirects.");
|
|
700
|
+
url = httpUrl(new URL(location, url).href);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
if (!response.ok || !response.body) {
|
|
704
|
+
await response.body?.cancel();
|
|
705
|
+
fail("DOWNLOAD_FAILED", `Media download failed (HTTP ${response.status}).`);
|
|
706
|
+
}
|
|
707
|
+
if (Number(response.headers.get("content-length")) > 104857600) {
|
|
708
|
+
await response.body.cancel();
|
|
709
|
+
fail("FILE_TOO_LARGE", "File exceeds the 100 MiB limit.");
|
|
710
|
+
}
|
|
711
|
+
const file = await open(destination, "w");
|
|
712
|
+
let size = 0;
|
|
713
|
+
try {
|
|
714
|
+
for await (const chunk of response.body) {
|
|
715
|
+
size += chunk.byteLength;
|
|
716
|
+
if (size > 104857600) fail("FILE_TOO_LARGE", "File exceeds the 100 MiB limit.");
|
|
717
|
+
await file.writeFile(chunk);
|
|
718
|
+
}
|
|
719
|
+
} finally {
|
|
720
|
+
await file.close();
|
|
721
|
+
}
|
|
722
|
+
const filename = basename(decodeURIComponent(url.pathname)) || "upload";
|
|
723
|
+
return {
|
|
724
|
+
filename,
|
|
725
|
+
mimeType: response.headers.get("content-type")?.split(";")[0]?.trim() || mimeType(filename)
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return fail("DOWNLOAD_FAILED", "Too many redirects.");
|
|
729
|
+
} catch (error) {
|
|
730
|
+
if (error && typeof error === "object" && "__cliError" in error) throw error;
|
|
731
|
+
return fail("DOWNLOAD_FAILED", "Could not download media (network, timeout, or local I/O failure).");
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
async function uploadFile(context, options, targetId) {
|
|
735
|
+
validateUpload(options);
|
|
736
|
+
let temporaryDirectory;
|
|
737
|
+
try {
|
|
738
|
+
let path = options.file;
|
|
739
|
+
let filename = path ? basename(path) : "upload";
|
|
740
|
+
let type = mimeType(filename);
|
|
741
|
+
if (options.url !== void 0) {
|
|
742
|
+
temporaryDirectory = await mkdtemp(join(tmpdir(), "camox-upload-"));
|
|
743
|
+
path = join(temporaryDirectory, "download");
|
|
744
|
+
const downloaded = await download(options.url, path);
|
|
745
|
+
filename = downloaded.filename;
|
|
746
|
+
type = downloaded.mimeType;
|
|
747
|
+
}
|
|
748
|
+
if (!path) fail("INVALID_ARGS", "Provide a file path.");
|
|
749
|
+
const info = await stat(path);
|
|
750
|
+
if (!info.isFile()) fail("INVALID_ARGS", "Upload source must be a regular file.");
|
|
751
|
+
if (info.size > 104857600) fail("FILE_TOO_LARGE", "File exceeds the 100 MiB limit.");
|
|
752
|
+
const body = new FormData();
|
|
753
|
+
if (type === "application/octet-stream") type = mimeType(options.filename ?? filename);
|
|
754
|
+
body.set("file", await openAsBlob(path, { type }), options.filename ?? filename);
|
|
755
|
+
body.set("projectId", String(context.projectId));
|
|
756
|
+
if (options.alt !== void 0) body.set("alt", options.alt);
|
|
757
|
+
if (options.aiMetadata !== void 0 || options.alt !== void 0) body.set("aiMetadataEnabled", String(options.alt === void 0 && options.aiMetadata === "on"));
|
|
758
|
+
const headers = {
|
|
759
|
+
Authorization: `Bearer ${context.token}`,
|
|
760
|
+
"x-camox-client": "cli",
|
|
761
|
+
"x-environment-name": context.environmentName
|
|
762
|
+
};
|
|
763
|
+
if (context.disableTelemetry) headers["x-camox-telemetry-disabled"] = "1";
|
|
764
|
+
let response;
|
|
765
|
+
try {
|
|
766
|
+
const endpoint = targetId === void 0 ? "/files/upload" : `/files/${targetId}/content`;
|
|
767
|
+
response = await fetch(`${context.apiUrl}${endpoint}`, {
|
|
768
|
+
method: "POST",
|
|
769
|
+
headers,
|
|
770
|
+
body,
|
|
771
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
772
|
+
redirect: "error"
|
|
773
|
+
});
|
|
774
|
+
} catch {
|
|
775
|
+
return fail("UPLOAD_FAILED", targetId === void 0 ? "Media upload failed (network or timeout). Check files list before retrying." : `File replacement failed (network or timeout). Check files get --id ${targetId} before retrying.`);
|
|
776
|
+
}
|
|
777
|
+
if (!response.ok) fail("UPLOAD_FAILED", `${targetId === void 0 ? "Media upload" : "File replacement"} failed (HTTP ${response.status}).`);
|
|
778
|
+
return await response.json();
|
|
779
|
+
} finally {
|
|
780
|
+
if (temporaryDirectory) await rm(temporaryDirectory, {
|
|
781
|
+
recursive: true,
|
|
782
|
+
force: true
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/commands/files.ts
|
|
788
|
+
const common = {
|
|
789
|
+
cwd: cwdFlag,
|
|
790
|
+
project: optional(option("--project", string({ metavar: "SLUG" }))),
|
|
791
|
+
production: option("--production"),
|
|
792
|
+
json: option("--json")
|
|
793
|
+
};
|
|
794
|
+
const metadata = {
|
|
795
|
+
alt: optional(option("--alt", string({ metavar: "TEXT" }), { description: message`Alt text (including an empty string). Disables automatic metadata.` })),
|
|
796
|
+
aiMetadata: optional(option("--ai-metadata", choice(["on", "off"]), { description: message`Enable or disable automatic metadata. Cannot enable alongside --alt. Enabling schedules generation for raster images.` }))
|
|
797
|
+
};
|
|
798
|
+
const parser$7 = command("files", or(command("upload", object({
|
|
799
|
+
command: constant("files.upload"),
|
|
800
|
+
file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Local file to upload. Pass exactly one of --file or --url.` })),
|
|
801
|
+
url: optional(option("--url", string({ metavar: "URL" }), { description: message`HTTP(S) URL to download and copy into Camox, not an external reference.` })),
|
|
802
|
+
filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Override the local basename or final URL path filename.` })),
|
|
803
|
+
...metadata,
|
|
804
|
+
...common
|
|
805
|
+
}), { description: message`Upload media (maximum 100 MiB). Example: camox files upload --url https://example.com/hero.webp --alt "Product screenshot". Returns the stored file ID and URL. Without metadata options, raster images use automatic metadata.` }), command("update", object({
|
|
806
|
+
command: constant("files.update"),
|
|
807
|
+
id: option("--id", integer({
|
|
808
|
+
min: 1,
|
|
809
|
+
metavar: "ID"
|
|
810
|
+
})),
|
|
811
|
+
file: optional(option("--file", string({ metavar: "PATH" }), { description: message`Replace file content from a local path, keeping its ID and references. Mutually exclusive with --url.` })),
|
|
812
|
+
url: optional(option("--url", string({ metavar: "URL" }), { description: message`Download and replace file content from an HTTP(S) URL, keeping its ID and references. Mutually exclusive with --file.` })),
|
|
813
|
+
filename: optional(option("--filename", string({ metavar: "NAME" }), { description: message`Set the filename. Metadata-only renames preserve the URL; replacement uses the source filename unless overridden. Disable AI to prevent future automatic renaming.` })),
|
|
814
|
+
...metadata,
|
|
815
|
+
...common
|
|
816
|
+
}), { description: message`Update metadata or replace file content with --file or --url (maximum 100 MiB). Replacing affects every reference to this file in the selected environment; its ID stays unchanged but its URL changes. Omitted alt text and AI settings are preserved. Example: camox files update --id 123 --file ./hero.jpg --alt "Updated description". Use --alt "" for intentionally empty alt text.` }), command("get", object({
|
|
817
|
+
command: constant("files.get"),
|
|
818
|
+
id: option("--id", integer({
|
|
819
|
+
min: 1,
|
|
820
|
+
metavar: "ID"
|
|
821
|
+
})),
|
|
822
|
+
...common
|
|
823
|
+
}), { description: message`Inspect a file in the selected project and environment.` }), command("list", object({
|
|
824
|
+
command: constant("files.list"),
|
|
825
|
+
...common
|
|
826
|
+
}), { description: message`List files in the selected project and environment.` })), { description: message`Upload and manage media. Defaults to your development environment; --production explicitly targets production.` });
|
|
827
|
+
async function handler$7(args) {
|
|
828
|
+
const outputMode = args.json ? "json" : "auto";
|
|
829
|
+
const options = {
|
|
830
|
+
cwd: args.cwd,
|
|
831
|
+
projectFlag: args.project,
|
|
832
|
+
production: args.production,
|
|
833
|
+
outputMode
|
|
834
|
+
};
|
|
835
|
+
try {
|
|
836
|
+
if (args.command === "files.upload") {
|
|
837
|
+
validateUpload(args);
|
|
838
|
+
printResult(await uploadFile(await resolveCommandContext(options), args), outputMode);
|
|
839
|
+
process.exit(0);
|
|
840
|
+
}
|
|
841
|
+
if (args.command === "files.list") return dispatch({
|
|
842
|
+
...options,
|
|
843
|
+
toolName: "listFiles",
|
|
844
|
+
args: {}
|
|
845
|
+
});
|
|
846
|
+
if (args.command === "files.get") return dispatch({
|
|
847
|
+
...options,
|
|
848
|
+
toolName: "getFile",
|
|
849
|
+
args: { id: args.id }
|
|
850
|
+
});
|
|
851
|
+
if (args.file !== void 0 || args.url !== void 0) {
|
|
852
|
+
validateUpload(args);
|
|
853
|
+
printResult(await uploadFile(await resolveCommandContext(options), args, args.id), outputMode);
|
|
854
|
+
process.exit(0);
|
|
855
|
+
}
|
|
856
|
+
validateMetadata(args);
|
|
857
|
+
validateFilename(args.filename);
|
|
858
|
+
if (args.alt === void 0 && args.aiMetadata === void 0 && args.filename === void 0) {
|
|
859
|
+
printError({
|
|
860
|
+
code: "INVALID_ARGS",
|
|
861
|
+
message: "Pass --file, --url, --filename, --alt or --ai-metadata."
|
|
862
|
+
});
|
|
863
|
+
process.exit(2);
|
|
864
|
+
}
|
|
865
|
+
return dispatch({
|
|
866
|
+
...options,
|
|
867
|
+
toolName: "updateFile",
|
|
868
|
+
args: {
|
|
869
|
+
id: args.id,
|
|
870
|
+
alt: args.alt,
|
|
871
|
+
filename: args.filename,
|
|
872
|
+
aiMetadataEnabled: args.aiMetadata === void 0 ? void 0 : args.aiMetadata === "on"
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
} catch (err) {
|
|
876
|
+
const error = asCliError(err);
|
|
877
|
+
printError(error);
|
|
878
|
+
process.exit(error.code === "INVALID_ARGS" ? 2 : 1);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
580
881
|
//#endregion
|
|
581
882
|
//#region src/lib/agent-skills.ts
|
|
582
883
|
function createAgentSkillLinks(targetDir) {
|
|
@@ -584,8 +885,10 @@ function createAgentSkillLinks(targetDir) {
|
|
|
584
885
|
const claudeSkillsDir = path.join(targetDir, ".claude", "skills");
|
|
585
886
|
fs.mkdirSync(agentsSkillsDir, { recursive: true });
|
|
586
887
|
fs.mkdirSync(claudeSkillsDir, { recursive: true });
|
|
587
|
-
|
|
588
|
-
|
|
888
|
+
for (const skill of ["camox", "camoxify"]) {
|
|
889
|
+
fs.symlinkSync(`../../node_modules/camox/skills/${skill}`, path.join(agentsSkillsDir, skill));
|
|
890
|
+
fs.symlinkSync(`../../.agents/skills/${skill}`, path.join(claudeSkillsDir, skill));
|
|
891
|
+
}
|
|
589
892
|
}
|
|
590
893
|
//#endregion
|
|
591
894
|
//#region src/lib/utils.ts
|
|
@@ -931,18 +1234,21 @@ const jsonFlag$1 = option("--json");
|
|
|
931
1234
|
const productionFlag$1 = option("--production");
|
|
932
1235
|
const parser$5 = command("layouts", or(command("list", object({
|
|
933
1236
|
command: constant("layouts.list"),
|
|
1237
|
+
cwd: cwdFlag,
|
|
934
1238
|
project: projectFlag$1,
|
|
935
1239
|
production: productionFlag$1,
|
|
936
1240
|
json: jsonFlag$1
|
|
937
1241
|
}))));
|
|
938
1242
|
async function handler$5(args) {
|
|
939
1243
|
const outputMode = args.json ? "json" : "auto";
|
|
1244
|
+
const cwd = args.cwd;
|
|
940
1245
|
const projectFlag = args.project;
|
|
941
1246
|
const production = args.production;
|
|
942
1247
|
switch (args.command) {
|
|
943
1248
|
case "layouts.list": return dispatch({
|
|
944
1249
|
toolName: "listLayouts",
|
|
945
1250
|
args: {},
|
|
1251
|
+
cwd,
|
|
946
1252
|
projectFlag,
|
|
947
1253
|
production,
|
|
948
1254
|
outputMode
|
|
@@ -966,15 +1272,18 @@ async function login() {
|
|
|
966
1272
|
}
|
|
967
1273
|
//#endregion
|
|
968
1274
|
//#region src/commands/logout.ts
|
|
969
|
-
const parser$3 = command("logout", object({
|
|
1275
|
+
const parser$3 = command("logout", object({
|
|
1276
|
+
command: constant("logout"),
|
|
1277
|
+
cwd: cwdFlag
|
|
1278
|
+
}));
|
|
970
1279
|
const handler$3 = logout;
|
|
971
|
-
function logout() {
|
|
1280
|
+
function logout(args = {}) {
|
|
972
1281
|
p.intro("camox logout");
|
|
973
1282
|
let authenticationUrl = null;
|
|
974
1283
|
try {
|
|
975
|
-
authenticationUrl = loadRuntime().authenticationUrl;
|
|
1284
|
+
authenticationUrl = loadRuntime(args.cwd).authenticationUrl;
|
|
976
1285
|
} catch (error) {
|
|
977
|
-
if (error instanceof RuntimeMalformedError) {
|
|
1286
|
+
if (error instanceof RuntimeMalformedError || error instanceof RuntimeDirectoryError || args.cwd !== void 0 && error instanceof RuntimeNotFoundError) {
|
|
978
1287
|
log.error(error.message);
|
|
979
1288
|
return;
|
|
980
1289
|
}
|
|
@@ -997,6 +1306,7 @@ const productionFlag = option("--production");
|
|
|
997
1306
|
const liveFlag = option("--live");
|
|
998
1307
|
const list = command("list", object({
|
|
999
1308
|
command: constant("pages.list"),
|
|
1309
|
+
cwd: cwdFlag,
|
|
1000
1310
|
project: projectFlag,
|
|
1001
1311
|
production: productionFlag,
|
|
1002
1312
|
json: jsonFlag
|
|
@@ -1006,6 +1316,7 @@ const get = command("get", object({
|
|
|
1006
1316
|
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1007
1317
|
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1008
1318
|
live: liveFlag,
|
|
1319
|
+
cwd: cwdFlag,
|
|
1009
1320
|
project: projectFlag,
|
|
1010
1321
|
production: productionFlag,
|
|
1011
1322
|
json: jsonFlag
|
|
@@ -1015,6 +1326,7 @@ const publish = command("publish", object({
|
|
|
1015
1326
|
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1016
1327
|
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1017
1328
|
noLayout: option("--no-layout"),
|
|
1329
|
+
cwd: cwdFlag,
|
|
1018
1330
|
project: projectFlag,
|
|
1019
1331
|
production: productionFlag,
|
|
1020
1332
|
json: jsonFlag
|
|
@@ -1023,6 +1335,7 @@ const unpublish = command("unpublish", object({
|
|
|
1023
1335
|
command: constant("pages.unpublish"),
|
|
1024
1336
|
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1025
1337
|
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1338
|
+
cwd: cwdFlag,
|
|
1026
1339
|
project: projectFlag,
|
|
1027
1340
|
production: productionFlag,
|
|
1028
1341
|
json: jsonFlag
|
|
@@ -1031,6 +1344,7 @@ const discardChanges = command("discard-changes", object({
|
|
|
1031
1344
|
command: constant("pages.discard-changes"),
|
|
1032
1345
|
id: optional(option("--id", integer({ metavar: "ID" }))),
|
|
1033
1346
|
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1347
|
+
cwd: cwdFlag,
|
|
1034
1348
|
project: projectFlag,
|
|
1035
1349
|
production: productionFlag,
|
|
1036
1350
|
json: jsonFlag
|
|
@@ -1041,40 +1355,53 @@ const parser$2 = command("pages", or(list, get, command("create", object({
|
|
|
1041
1355
|
pathSegment: option("--path-segment", string({ metavar: "SEGMENT" })),
|
|
1042
1356
|
layoutId: option("--layout-id", integer({ metavar: "ID" })),
|
|
1043
1357
|
parentPageId: optional(option("--parent-page-id", integer({ metavar: "ID" }))),
|
|
1358
|
+
cwd: cwdFlag,
|
|
1044
1359
|
project: projectFlag,
|
|
1045
1360
|
production: productionFlag,
|
|
1046
1361
|
json: jsonFlag
|
|
1047
1362
|
})), command("update", object({
|
|
1048
1363
|
command: constant("pages.update"),
|
|
1049
|
-
id: option("--id", integer({
|
|
1364
|
+
id: optional(option("--id", integer({
|
|
1365
|
+
min: 1,
|
|
1366
|
+
metavar: "ID"
|
|
1367
|
+
}))),
|
|
1368
|
+
path: optional(option("--path", string({ metavar: "PATH" }))),
|
|
1369
|
+
metaTitle: optional(option("--meta-title", string({ metavar: "TEXT" }), { description: message`Set the SEO title. An empty string clears it. Disables automatic SEO for both fields.` })),
|
|
1370
|
+
metaDescription: optional(option("--meta-description", string({ metavar: "TEXT" }), { description: message`Set the SEO description. An empty string clears it. Disables automatic SEO for both fields.` })),
|
|
1371
|
+
aiSeo: optional(option("--ai-seo", choice(["on", "off"]), { description: message`Enable or disable automatic SEO. Enabling schedules asynchronous generation; it cannot be combined with manual metadata. Disabling preserves current metadata.` })),
|
|
1050
1372
|
nickname: optional(option("--nickname", string({ metavar: "TEXT" }))),
|
|
1051
1373
|
pathSegment: optional(option("--path-segment", string({ metavar: "SEGMENT" }))),
|
|
1052
1374
|
parentPageId: optional(option("--parent-page-id", integer({ metavar: "ID" }))),
|
|
1375
|
+
cwd: cwdFlag,
|
|
1053
1376
|
project: projectFlag,
|
|
1054
1377
|
production: productionFlag,
|
|
1055
1378
|
json: jsonFlag
|
|
1056
|
-
})), command("set-layout", object({
|
|
1379
|
+
}), { description: message`Update a page draft, including in --production. Pass exactly one of --id or --path and at least one update field. Omitted fields are preserved. Publish separately with pages publish. Example: camox pages update --path /about --meta-title "About us" --meta-description "Meet the team". Read metadata with pages get; add --live for published metadata.` }), command("set-layout", object({
|
|
1057
1380
|
command: constant("pages.set-layout"),
|
|
1058
1381
|
id: option("--id", integer({ metavar: "ID" })),
|
|
1059
1382
|
layoutId: option("--layout-id", integer({ metavar: "ID" })),
|
|
1383
|
+
cwd: cwdFlag,
|
|
1060
1384
|
project: projectFlag,
|
|
1061
1385
|
production: productionFlag,
|
|
1062
1386
|
json: jsonFlag
|
|
1063
1387
|
})), command("delete", object({
|
|
1064
1388
|
command: constant("pages.delete"),
|
|
1065
1389
|
id: option("--id", integer({ metavar: "ID" })),
|
|
1390
|
+
cwd: cwdFlag,
|
|
1066
1391
|
project: projectFlag,
|
|
1067
1392
|
production: productionFlag,
|
|
1068
1393
|
json: jsonFlag
|
|
1069
1394
|
})), publish, unpublish, discardChanges));
|
|
1070
1395
|
async function handler$2(args) {
|
|
1071
1396
|
const outputMode = args.json ? "json" : "auto";
|
|
1397
|
+
const cwd = args.cwd;
|
|
1072
1398
|
const projectFlag = args.project;
|
|
1073
1399
|
const production = args.production;
|
|
1074
1400
|
switch (args.command) {
|
|
1075
1401
|
case "pages.list": return dispatch({
|
|
1076
1402
|
toolName: "listPages",
|
|
1077
1403
|
args: {},
|
|
1404
|
+
cwd,
|
|
1078
1405
|
projectFlag,
|
|
1079
1406
|
production,
|
|
1080
1407
|
outputMode
|
|
@@ -1093,6 +1420,7 @@ async function handler$2(args) {
|
|
|
1093
1420
|
...args.id != null ? { id: args.id } : { path: args.path },
|
|
1094
1421
|
source: args.live ? "live" : "draft"
|
|
1095
1422
|
},
|
|
1423
|
+
cwd,
|
|
1096
1424
|
projectFlag,
|
|
1097
1425
|
production,
|
|
1098
1426
|
outputMode
|
|
@@ -1105,28 +1433,63 @@ async function handler$2(args) {
|
|
|
1105
1433
|
layoutId: args.layoutId,
|
|
1106
1434
|
parentPageId: args.parentPageId
|
|
1107
1435
|
},
|
|
1436
|
+
cwd,
|
|
1108
1437
|
projectFlag,
|
|
1109
1438
|
production,
|
|
1110
1439
|
outputMode
|
|
1111
1440
|
});
|
|
1112
|
-
case "pages.update":
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1441
|
+
case "pages.update":
|
|
1442
|
+
if (args.id == null === (args.path == null)) {
|
|
1443
|
+
printError({
|
|
1444
|
+
code: "INVALID_ARGS",
|
|
1445
|
+
message: "Pass exactly one of --id or --path."
|
|
1446
|
+
});
|
|
1447
|
+
process.exit(2);
|
|
1448
|
+
}
|
|
1449
|
+
if (args.aiSeo === "on" && (args.metaTitle !== void 0 || args.metaDescription !== void 0)) {
|
|
1450
|
+
printError({
|
|
1451
|
+
code: "INVALID_ARGS",
|
|
1452
|
+
message: "Cannot enable automatic SEO alongside manual metadata."
|
|
1453
|
+
});
|
|
1454
|
+
process.exit(2);
|
|
1455
|
+
}
|
|
1456
|
+
if ([
|
|
1457
|
+
args.nickname,
|
|
1458
|
+
args.pathSegment,
|
|
1459
|
+
args.parentPageId,
|
|
1460
|
+
args.metaTitle,
|
|
1461
|
+
args.metaDescription,
|
|
1462
|
+
args.aiSeo
|
|
1463
|
+
].every((value) => value === void 0)) {
|
|
1464
|
+
printError({
|
|
1465
|
+
code: "INVALID_ARGS",
|
|
1466
|
+
message: "Pass at least one field to update."
|
|
1467
|
+
});
|
|
1468
|
+
process.exit(2);
|
|
1469
|
+
}
|
|
1470
|
+
return dispatch({
|
|
1471
|
+
toolName: "updatePage",
|
|
1472
|
+
args: {
|
|
1473
|
+
...args.id != null ? { id: args.id } : { path: args.path },
|
|
1474
|
+
metaTitle: args.metaTitle,
|
|
1475
|
+
metaDescription: args.metaDescription,
|
|
1476
|
+
aiSeoEnabled: args.aiSeo === void 0 ? void 0 : args.aiSeo === "on",
|
|
1477
|
+
nickname: args.nickname,
|
|
1478
|
+
pathSegment: args.pathSegment,
|
|
1479
|
+
parentPageId: args.parentPageId
|
|
1480
|
+
},
|
|
1481
|
+
cwd,
|
|
1482
|
+
projectFlag,
|
|
1483
|
+
production,
|
|
1484
|
+
outputMode
|
|
1485
|
+
});
|
|
1124
1486
|
case "pages.set-layout": return dispatch({
|
|
1125
1487
|
toolName: "setPageLayout",
|
|
1126
1488
|
args: {
|
|
1127
1489
|
id: args.id,
|
|
1128
1490
|
layoutId: args.layoutId
|
|
1129
1491
|
},
|
|
1492
|
+
cwd,
|
|
1130
1493
|
projectFlag,
|
|
1131
1494
|
production,
|
|
1132
1495
|
outputMode
|
|
@@ -1134,6 +1497,7 @@ async function handler$2(args) {
|
|
|
1134
1497
|
case "pages.delete": return dispatch({
|
|
1135
1498
|
toolName: "deletePage",
|
|
1136
1499
|
args: { id: args.id },
|
|
1500
|
+
cwd,
|
|
1137
1501
|
projectFlag,
|
|
1138
1502
|
production,
|
|
1139
1503
|
outputMode
|
|
@@ -1152,6 +1516,7 @@ async function handler$2(args) {
|
|
|
1152
1516
|
...args.id != null ? { id: args.id } : { path: args.path },
|
|
1153
1517
|
alsoPublishLayout: !args.noLayout
|
|
1154
1518
|
},
|
|
1519
|
+
cwd,
|
|
1155
1520
|
projectFlag,
|
|
1156
1521
|
production,
|
|
1157
1522
|
outputMode
|
|
@@ -1167,6 +1532,7 @@ async function handler$2(args) {
|
|
|
1167
1532
|
return dispatch({
|
|
1168
1533
|
toolName: "unpublishPage",
|
|
1169
1534
|
args: args.id != null ? { id: args.id } : { path: args.path },
|
|
1535
|
+
cwd,
|
|
1170
1536
|
projectFlag,
|
|
1171
1537
|
production,
|
|
1172
1538
|
outputMode
|
|
@@ -1182,6 +1548,7 @@ async function handler$2(args) {
|
|
|
1182
1548
|
return dispatch({
|
|
1183
1549
|
toolName: "discardPageChanges",
|
|
1184
1550
|
args: args.id != null ? { id: args.id } : { path: args.path },
|
|
1551
|
+
cwd,
|
|
1185
1552
|
projectFlag,
|
|
1186
1553
|
production,
|
|
1187
1554
|
outputMode
|
|
@@ -1220,14 +1587,22 @@ function handler$1() {
|
|
|
1220
1587
|
//#region src/commands/status.ts
|
|
1221
1588
|
const parser = command("status", object({
|
|
1222
1589
|
command: constant("status"),
|
|
1590
|
+
cwd: cwdFlag,
|
|
1223
1591
|
production: option("--production"),
|
|
1224
1592
|
json: option("--json")
|
|
1225
1593
|
}));
|
|
1226
1594
|
async function handler(args) {
|
|
1227
1595
|
let runtime;
|
|
1228
1596
|
try {
|
|
1229
|
-
runtime = loadRuntime();
|
|
1597
|
+
runtime = loadRuntime(args.cwd);
|
|
1230
1598
|
} catch (err) {
|
|
1599
|
+
if (err instanceof RuntimeDirectoryError) {
|
|
1600
|
+
printError({
|
|
1601
|
+
code: "INVALID_CWD",
|
|
1602
|
+
message: err.message
|
|
1603
|
+
});
|
|
1604
|
+
process.exit(2);
|
|
1605
|
+
}
|
|
1231
1606
|
if (err instanceof RuntimeNotFoundError || err instanceof RuntimeMalformedError) {
|
|
1232
1607
|
printError({
|
|
1233
1608
|
code: "RUNTIME_NOT_FOUND",
|
|
@@ -1267,7 +1642,7 @@ async function handler(args) {
|
|
|
1267
1642
|
//#endregion
|
|
1268
1643
|
//#region src/index.ts
|
|
1269
1644
|
const result = runSync(defineProgram({
|
|
1270
|
-
parser: or(parser$6, parser$4, parser$3, parser$1, parser, parser$2, parser$
|
|
1645
|
+
parser: or(parser$6, parser$4, parser$3, parser$1, parser, parser$2, parser$9, parser$5, parser$8, parser$7),
|
|
1271
1646
|
metadata: {
|
|
1272
1647
|
name: "camox",
|
|
1273
1648
|
brief: message`Camox CLI`
|
|
@@ -1281,7 +1656,7 @@ switch (result.command) {
|
|
|
1281
1656
|
await handler$4();
|
|
1282
1657
|
break;
|
|
1283
1658
|
case "logout":
|
|
1284
|
-
handler$3();
|
|
1659
|
+
handler$3(result);
|
|
1285
1660
|
break;
|
|
1286
1661
|
case "release":
|
|
1287
1662
|
handler$1();
|
|
@@ -1308,7 +1683,13 @@ switch (result.command) {
|
|
|
1308
1683
|
case "blocks.edit":
|
|
1309
1684
|
case "blocks.move":
|
|
1310
1685
|
case "blocks.delete":
|
|
1311
|
-
await handler$
|
|
1686
|
+
await handler$9(result);
|
|
1687
|
+
break;
|
|
1688
|
+
case "files.upload":
|
|
1689
|
+
case "files.update":
|
|
1690
|
+
case "files.get":
|
|
1691
|
+
case "files.list":
|
|
1692
|
+
await handler$7(result);
|
|
1312
1693
|
break;
|
|
1313
1694
|
case "layouts.list":
|
|
1314
1695
|
await handler$5(result);
|
|
@@ -1316,7 +1697,7 @@ switch (result.command) {
|
|
|
1316
1697
|
case "env.check":
|
|
1317
1698
|
case "env.push":
|
|
1318
1699
|
case "env.pull":
|
|
1319
|
-
await handler$
|
|
1700
|
+
await handler$8(result);
|
|
1320
1701
|
break;
|
|
1321
1702
|
}
|
|
1322
1703
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camox/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"bin": {
|
|
6
6
|
"camox": "./dist/index.mjs"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"@types/node": "^24.12.4",
|
|
32
32
|
"@typescript/native-preview": "7.0.0-dev.20260412.1",
|
|
33
33
|
"vite-plus": "^0.1.21",
|
|
34
|
-
"@camox/api-contract": "0.
|
|
34
|
+
"@camox/api-contract": "0.43.0"
|
|
35
35
|
},
|
|
36
36
|
"nx": {
|
|
37
37
|
"tags": [
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"build": "node scripts/sync-templates.mjs && vp pack",
|
|
43
43
|
"dev": "node scripts/sync-templates.mjs --watch --build",
|
|
44
44
|
"lint": "vp lint",
|
|
45
|
+
"test": "vp pack && node --test test/*.test.mjs",
|
|
45
46
|
"check": "tsgo --noEmit && vp lint --fix"
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Production runtime smoke test
|
|
2
|
+
|
|
3
|
+
From the repository root:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pnpm exec nx run camox:build
|
|
7
|
+
pnpm --filter @camox/template-default test:production
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
The test builds a temporary copy of the starter using Nitro's Node middleware
|
|
11
|
+
preset, moves only `.output` into an isolated deployment, removes build-time
|
|
12
|
+
`node_modules` links, and serves a real HTTP request. It asserts that React/Base UI
|
|
13
|
+
can render HTML and Camox's OG renderer can produce a PNG. No Camox API, real auth,
|
|
14
|
+
application-level dependency tracing overrides, or direct Takumi dependency is
|
|
15
|
+
needed. A temporary home directory supplies fake build authentication without
|
|
16
|
+
modifying the developer's credentials. CI runs this test after building the SDK.
|
|
17
|
+
|
|
18
|
+
## Issue #106 investigation
|
|
19
|
+
|
|
20
|
+
- **Takumi:** the original `createRequire(import.meta.url).resolve(...)` survives
|
|
21
|
+
bundling, but the WASM package/asset is absent from the deployment. The isolated
|
|
22
|
+
smoke test reproduced `Cannot find module '@takumi-rs/wasm/takumi_wasm_bg.wasm'`
|
|
23
|
+
before the fix. Camox now uses a static module import so Nitro's WASM loader
|
|
24
|
+
owns asset inclusion, including when dependencies are fully bundled. The
|
|
25
|
+
server-only guard in `createLayout` prevents this import reaching client builds.
|
|
26
|
+
- **tslib:** a package containing only `package.json` and `tslib.es6.mjs`, as
|
|
27
|
+
reported, reproduces Node's missing `tslib/modules/index.js` error: Node selects
|
|
28
|
+
that entry from the package exports. The complete upstream package is valid;
|
|
29
|
+
the partial traced copy is not. The current starter build did not independently
|
|
30
|
+
reproduce that partial copy. Camox now adds `tslib` to Nitro's `noExternals`,
|
|
31
|
+
keeping its existing ESM alias and removing reliance on partial package tracing.
|
|
32
|
+
Plugin tests cover preservation of user options, including `noExternals: true`.
|
|
33
|
+
|
|
34
|
+
These are SDK integration fixes, not starter workarounds. Applications should not
|
|
35
|
+
need to add Takumi as a direct dependency or configure `traceDeps`/`noExternals`.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { cp, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const temp = await mkdtemp(join(tmpdir(), "camox-production-"));
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const root = join(temp, "app");
|
|
14
|
+
const home = join(temp, "home");
|
|
15
|
+
await mkdir(join(home, ".camox"), { recursive: true });
|
|
16
|
+
await writeFile(
|
|
17
|
+
join(home, ".camox/auth.json"),
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
"http://localhost:3274": { token: "smoke", name: "Smoke", email: "smoke@example.com" },
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
await mkdir(root);
|
|
23
|
+
await mkdir(join(temp, "node_modules"));
|
|
24
|
+
// Nitro resolves its builder from the project root. Match Vite+'s own Vite
|
|
25
|
+
// dependency without relying on workspace hoisting in this temporary app.
|
|
26
|
+
const vitePackage = createRequire(import.meta.resolve("vite-plus")).resolve("vite/package.json");
|
|
27
|
+
await symlink(dirname(vitePackage), join(temp, "node_modules/vite"), "dir");
|
|
28
|
+
for (const file of ["src", "vite.config.ts", "tsconfig.json", "package.json"]) {
|
|
29
|
+
await cp(join(appRoot, file), join(root, file), { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
await symlink(join(appRoot, "node_modules"), join(root, "node_modules"), "dir");
|
|
32
|
+
const configPath = join(root, "vite.config.ts");
|
|
33
|
+
await writeFile(
|
|
34
|
+
configPath,
|
|
35
|
+
(await readFile(configPath, "utf8")).replace(
|
|
36
|
+
"nitro()",
|
|
37
|
+
'nitro({ routes: { "/smoke": "./routes/smoke.ts" } })',
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
await mkdir(join(root, "routes"), { recursive: true });
|
|
41
|
+
await writeFile(
|
|
42
|
+
join(root, "routes/smoke.ts"),
|
|
43
|
+
`import { Dialog } from "@base-ui/react/dialog";
|
|
44
|
+
import { createElement } from "react";
|
|
45
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
46
|
+
import { ImageResponse } from "camox/_internal/imageResponse";
|
|
47
|
+
|
|
48
|
+
export default async () => {
|
|
49
|
+
const element = createElement("div", { style: { color: "black", background: "white" } }, "Production smoke");
|
|
50
|
+
const image = new ImageResponse(element, { width: 320, height: 160 });
|
|
51
|
+
const bytes = new Uint8Array(await image.arrayBuffer());
|
|
52
|
+
const html = renderToStaticMarkup(createElement(Dialog.Root, null,
|
|
53
|
+
createElement(Dialog.Trigger, null, "Production smoke")));
|
|
54
|
+
return Response.json({ html, png: Array.from(bytes.slice(0, 8)) });
|
|
55
|
+
};
|
|
56
|
+
`,
|
|
57
|
+
);
|
|
58
|
+
const build = spawnSync(
|
|
59
|
+
process.execPath,
|
|
60
|
+
[
|
|
61
|
+
"--input-type=module",
|
|
62
|
+
"-e",
|
|
63
|
+
`
|
|
64
|
+
const { createBuilder } = await import(${JSON.stringify(import.meta.resolve("vite-plus"))});
|
|
65
|
+
const builder = await createBuilder({ root: ${JSON.stringify(root)} });
|
|
66
|
+
await builder.buildApp();
|
|
67
|
+
`,
|
|
68
|
+
],
|
|
69
|
+
{
|
|
70
|
+
cwd: root,
|
|
71
|
+
env: {
|
|
72
|
+
...process.env,
|
|
73
|
+
HOME: home,
|
|
74
|
+
CAMOX_INTERNAL_RELEASE: "0",
|
|
75
|
+
NITRO_PRESET: "node-middleware",
|
|
76
|
+
},
|
|
77
|
+
encoding: "utf8",
|
|
78
|
+
timeout: 180_000,
|
|
79
|
+
killSignal: "SIGKILL",
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
assert.equal(build.status, 0, `${build.stdout}\n${build.stderr}`);
|
|
83
|
+
|
|
84
|
+
// Move only the deployment artifact away from all workspace node_modules.
|
|
85
|
+
const deployment = join(temp, "deployment");
|
|
86
|
+
await cp(join(root, ".output"), deployment, { recursive: true });
|
|
87
|
+
await rm(root, { recursive: true, force: true });
|
|
88
|
+
await rm(join(temp, "node_modules"), { recursive: true, force: true });
|
|
89
|
+
const runtime = spawnSync(
|
|
90
|
+
process.execPath,
|
|
91
|
+
[
|
|
92
|
+
"--input-type=module",
|
|
93
|
+
"-e",
|
|
94
|
+
`
|
|
95
|
+
import assert from "node:assert/strict";
|
|
96
|
+
import { createServer } from "node:http";
|
|
97
|
+
const { middleware } = await import("./server/index.mjs");
|
|
98
|
+
const server = createServer(middleware);
|
|
99
|
+
await new Promise(resolve => server.listen(0, "127.0.0.1", resolve));
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch("http://127.0.0.1:" + server.address().port + "/smoke");
|
|
102
|
+
assert.equal(response.status, 200, await response.clone().text());
|
|
103
|
+
const result = await response.json();
|
|
104
|
+
assert.match(result.html, /Production smoke/);
|
|
105
|
+
assert.deepEqual(result.png, [137, 80, 78, 71, 13, 10, 26, 10]);
|
|
106
|
+
} finally {
|
|
107
|
+
server.closeAllConnections();
|
|
108
|
+
server.close();
|
|
109
|
+
}
|
|
110
|
+
`,
|
|
111
|
+
],
|
|
112
|
+
{
|
|
113
|
+
cwd: deployment,
|
|
114
|
+
env: { ...process.env, NODE_PATH: "", NODE_ENV: "production" },
|
|
115
|
+
encoding: "utf8",
|
|
116
|
+
timeout: 30_000,
|
|
117
|
+
killSignal: "SIGKILL",
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
assert.equal(runtime.status, 0, `${runtime.stdout}\n${runtime.stderr}`);
|
|
121
|
+
console.log("Production server rendered HTML and a Takumi PNG from an isolated deployment.");
|
|
122
|
+
} finally {
|
|
123
|
+
await rm(temp, { recursive: true, force: true });
|
|
124
|
+
}
|