@cms-lab/cli 1.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Afaq Rashid
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # cms-lab
2
+
3
+ CLI for catching CMS-driven Next.js failures before deploy.
4
+
5
+ ```sh
6
+ npx @cms-lab/cli scan
7
+ ```
8
+
9
+ ## Commands
10
+
11
+ ```sh
12
+ cms-lab init
13
+ cms-lab doctor
14
+ cms-lab doctor --debug --verbose 2
15
+ cms-lab scan --report
16
+ cms-lab scan --markdown
17
+ cms-lab scan --junit
18
+ cms-lab scan --slack-webhook "$CMS_LAB_SLACK_WEBHOOK"
19
+ cms-lab scan --debug
20
+ cms-lab scan --json
21
+ cms-lab scan --json --include-sensitive-output
22
+ cms-lab scan --max-warnings 0
23
+ cms-lab scan --strict
24
+ cms-lab explain CMS-ROUTE-404
25
+ ```
26
+
27
+ ## Scope
28
+
29
+ cms-lab supports Next.js App Router projects using Prismic, Strapi, Directus,
30
+ and WordPress. It validates configured CMS route mappings, route reachability,
31
+ UID gaps, SEO metadata, image alt text, and project-specific required fields.
32
+
33
+ Debug logs use stderr and support `--debug` plus `--verbose <0|1|2|3>`, so JSON
34
+ scan output remains clean on stdout. Raw CMS document `data` is redacted from
35
+ `--json` output, along with document URLs, UIDs, and absolute project paths,
36
+ unless `--include-sensitive-output` is passed explicitly.
37
+
38
+ CI strictness can be raised with `--fail-on warning`, `--max-warnings <count>`,
39
+ `--max-info <count>`, or `--strict`.
40
+
41
+ Exports include local HTML (`--report`), Markdown (`--markdown`), JUnit XML
42
+ (`--junit`), and redacted Slack incoming webhook summaries
43
+ (`--slack-webhook <url>`). Slack notifications send counts and diagnostic codes
44
+ only, never raw CMS payloads, local paths, or webhook URLs.
45
+
46
+ ## Config
47
+
48
+ Create `cms-lab.config.ts` in the target Next.js project. Import
49
+ `defineConfig` from `@cms-lab/core`.
50
+
51
+ See the repository README for full setup, CI, and live Prismic testing.
52
+
53
+ ## Open Source
54
+
55
+ MIT licensed. See the repository [license](https://github.com/i-afaqrashid/cms-lab/blob/main/LICENSE), [contributing guide](https://github.com/i-afaqrashid/cms-lab/blob/main/CONTRIBUTING.md), and [support guide](https://github.com/i-afaqrashid/cms-lab/blob/main/SUPPORT.md).
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runCli
4
+ } from "./chunk-JHH7PD4C.js";
5
+
6
+ // src/bin.ts
7
+ var exitCode = await runCli(process.argv.slice(2));
8
+ process.exitCode = exitCode;
@@ -0,0 +1,1134 @@
1
+ // src/index.ts
2
+ import { Command, CommanderError } from "commander";
3
+ import { access, mkdir, writeFile } from "fs/promises";
4
+ import { dirname, resolve } from "path";
5
+ import { performance } from "perf_hooks";
6
+ import {
7
+ CmsFetchError,
8
+ ConfigLoadError,
9
+ SiteUnreachableError,
10
+ explainDiagnostic,
11
+ loadCmsLabConfig,
12
+ scanDocuments
13
+ } from "@cms-lab/core";
14
+ import { fetchDirectusDocuments as defaultFetchDirectusDocuments } from "@cms-lab/directus";
15
+ import { detectNextProject } from "@cms-lab/next";
16
+ import { fetchPrismicDocuments as defaultFetchPrismicDocuments } from "@cms-lab/prismic";
17
+ import { renderHtmlReport } from "@cms-lab/reporter";
18
+ import { fetchStrapiDocuments as defaultFetchStrapiDocuments } from "@cms-lab/strapi";
19
+ import { fetchWordPressDocuments as defaultFetchWordPressDocuments } from "@cms-lab/wordpress";
20
+
21
+ // src/exporters.ts
22
+ function renderMarkdownSummary(result, status) {
23
+ const lines = [
24
+ `# cms-lab scan ${status}`,
25
+ "",
26
+ "| Errors | Warnings | Info | Documents |",
27
+ "| ---: | ---: | ---: | ---: |",
28
+ `| ${result.summary.errors} | ${result.summary.warnings} | ${result.summary.info} | ${result.documents.length} |`,
29
+ ""
30
+ ];
31
+ if (result.diagnostics.length === 0) {
32
+ lines.push("No diagnostics found.", "");
33
+ return lines.join("\n");
34
+ }
35
+ lines.push("## Diagnostics", "");
36
+ lines.push("| Severity | Code | Message | Source |");
37
+ lines.push("| --- | --- | --- | --- |");
38
+ for (const diagnostic of result.diagnostics) {
39
+ lines.push(
40
+ `| ${escapeMarkdown(diagnostic.severity)} | ${escapeMarkdown(diagnostic.code)} | ${escapeMarkdown(diagnostic.message)} | ${escapeMarkdown(diagnostic.source ?? "")} |`
41
+ );
42
+ }
43
+ lines.push("");
44
+ return lines.join("\n");
45
+ }
46
+ function renderJUnitReport(result) {
47
+ const diagnostics = result.diagnostics.length > 0 ? result.diagnostics : [
48
+ {
49
+ severity: "info",
50
+ code: "CMS-LAB-PASS",
51
+ message: "cms-lab scan passed with no diagnostics"
52
+ }
53
+ ];
54
+ const failures = result.diagnostics.filter(
55
+ (diagnostic) => diagnostic.severity === "error"
56
+ );
57
+ return [
58
+ '<?xml version="1.0" encoding="UTF-8"?>',
59
+ `<testsuite name="cms-lab" tests="${diagnostics.length}" failures="${failures.length}" errors="0">`,
60
+ ...diagnostics.map((diagnostic) => renderJUnitTestcase(diagnostic)),
61
+ "</testsuite>",
62
+ ""
63
+ ].join("\n");
64
+ }
65
+ function renderSlackPayload(result, status) {
66
+ const summary = `${result.summary.errors} ${plural(result.summary.errors, "error")}, ${result.summary.warnings} ${plural(result.summary.warnings, "warning")}, ${result.summary.info} ${plural(result.summary.info, "info item")}, ${result.documents.length} ${plural(result.documents.length, "document")}`;
67
+ const codes = [
68
+ ...new Set(result.diagnostics.map((diagnostic) => diagnostic.code))
69
+ ].slice(0, 5).join(", ");
70
+ const suffix = codes ? ` Top codes: ${codes}.` : "";
71
+ return {
72
+ text: `cms-lab scan ${status}: ${summary}.${suffix}`
73
+ };
74
+ }
75
+ async function postSlackPayload(options) {
76
+ const response = await (options.fetchImpl ?? fetch)(options.webhookUrl, {
77
+ method: "POST",
78
+ headers: { "content-type": "application/json" },
79
+ body: JSON.stringify(options.payload)
80
+ });
81
+ if (!response.ok) {
82
+ throw new Error(`Slack webhook returned HTTP ${response.status}`);
83
+ }
84
+ }
85
+ function renderJUnitTestcase(diagnostic) {
86
+ const classname = `cms-lab.${groupForDiagnostic(diagnostic)}`;
87
+ const name = diagnostic.code;
88
+ if (diagnostic.severity === "error") {
89
+ return ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(name)}"><failure message="${escapeXml(diagnostic.message)}">${escapeXml(diagnostic.message)}</failure></testcase>`;
90
+ }
91
+ return ` <testcase classname="${escapeXml(classname)}" name="${escapeXml(name)}"><system-out>${escapeXml(diagnostic.message)}</system-out></testcase>`;
92
+ }
93
+ function groupForDiagnostic(diagnostic) {
94
+ if (diagnostic.code.startsWith("CMS-ROUTE") || diagnostic.code === "CMS-UID-MISSING") {
95
+ return "routes";
96
+ }
97
+ if (diagnostic.code.startsWith("CMS-FIELD")) {
98
+ return "fields";
99
+ }
100
+ if (diagnostic.code.startsWith("SEO-")) {
101
+ return "seo";
102
+ }
103
+ if (diagnostic.code.startsWith("A11Y-")) {
104
+ return "a11y";
105
+ }
106
+ return "other";
107
+ }
108
+ function escapeMarkdown(value) {
109
+ return value.replaceAll("|", "\\|").replaceAll("\n", " ");
110
+ }
111
+ function escapeXml(value) {
112
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
113
+ }
114
+ function plural(value, singular) {
115
+ return value === 1 ? singular : `${singular}s`;
116
+ }
117
+
118
+ // src/output.ts
119
+ import pc from "picocolors";
120
+ function formatPrettyResult(result, options = {}) {
121
+ const color = options.color ?? false;
122
+ const paint = color ? pc : noColor;
123
+ const failOn = options.failOn ?? "error";
124
+ const thresholdFailure = thresholdFailureMessage(result, options);
125
+ const lines = [];
126
+ lines.push(paint.bold("cms-lab"));
127
+ lines.push(`project ${result.project.framework} ${result.project.router}`);
128
+ lines.push(`documents ${result.documents.length}`);
129
+ lines.push("");
130
+ appendGroup(
131
+ lines,
132
+ "errors",
133
+ result.diagnostics.filter((diagnostic) => diagnostic.severity === "error"),
134
+ paint.red
135
+ );
136
+ appendGroup(
137
+ lines,
138
+ "warnings",
139
+ result.diagnostics.filter(
140
+ (diagnostic) => diagnostic.severity === "warning"
141
+ ),
142
+ paint.yellow
143
+ );
144
+ appendGroup(
145
+ lines,
146
+ "info",
147
+ result.diagnostics.filter((diagnostic) => diagnostic.severity === "info"),
148
+ paint.cyan
149
+ );
150
+ lines.push("");
151
+ lines.push("summary");
152
+ lines.push(` errors ${result.summary.errors}`);
153
+ lines.push(` warnings ${result.summary.warnings}`);
154
+ lines.push(` info ${result.summary.info}`);
155
+ lines.push("");
156
+ if (result.summary.errors > 0 && failOn !== "never") {
157
+ lines.push(
158
+ paint.red(
159
+ `scan failed - ${result.summary.errors} ${plural2(result.summary.errors, "error")}`
160
+ )
161
+ );
162
+ } else if (failOn === "warning" && result.summary.warnings > 0) {
163
+ lines.push(
164
+ paint.yellow(
165
+ `scan failed - ${result.summary.warnings} ${plural2(result.summary.warnings, "warning")}`
166
+ )
167
+ );
168
+ } else if (thresholdFailure) {
169
+ lines.push(paint.red(thresholdFailure));
170
+ } else if (failOn === "never" && (result.summary.errors > 0 || result.summary.warnings > 0)) {
171
+ lines.push(
172
+ paint.cyan("scan completed - diagnostics ignored by --fail-on never")
173
+ );
174
+ } else {
175
+ lines.push(paint.green("scan passed"));
176
+ }
177
+ if (options.hints && result.diagnostics.length > 0) {
178
+ lines.push("");
179
+ lines.push("next");
180
+ lines.push(` cms-lab explain ${result.diagnostics[0].code}`);
181
+ }
182
+ return `${lines.join("\n")}
183
+ `;
184
+ }
185
+ function thresholdFailureMessage(result, options) {
186
+ if (options.maxWarnings !== void 0 && result.summary.warnings > options.maxWarnings) {
187
+ return `scan failed - ${result.summary.warnings} ${plural2(result.summary.warnings, "warning")} exceed --max-warnings ${options.maxWarnings}`;
188
+ }
189
+ if (options.maxInfo !== void 0 && result.summary.info > options.maxInfo) {
190
+ return `scan failed - ${result.summary.info} ${plural2(result.summary.info, "info item")} exceed --max-info ${options.maxInfo}`;
191
+ }
192
+ return void 0;
193
+ }
194
+ function appendGroup(lines, title, diagnostics, color) {
195
+ if (diagnostics.length === 0) {
196
+ return;
197
+ }
198
+ lines.push(title);
199
+ for (const diagnostic of diagnostics) {
200
+ const location = diagnostic.path ? ` ${diagnostic.path}` : "";
201
+ const source = diagnostic.source ? ` (${diagnostic.source})` : "";
202
+ lines.push(
203
+ ` ${color(diagnostic.code)}${location} - ${diagnostic.message}${source}`
204
+ );
205
+ }
206
+ lines.push("");
207
+ }
208
+ function plural2(value, singular) {
209
+ return value === 1 ? singular : `${singular}s`;
210
+ }
211
+ var noColor = {
212
+ bold: (value) => value,
213
+ red: (value) => value,
214
+ yellow: (value) => value,
215
+ cyan: (value) => value,
216
+ green: (value) => value
217
+ };
218
+
219
+ // src/index.ts
220
+ async function runCli(argv, dependencies = {}) {
221
+ let exitCode = 0;
222
+ const program = new Command().name("cms-lab").description("Catch CMS bugs before deploy.").version("1.0.1").exitOverride().configureOutput({
223
+ writeOut: (text) => writeStdout(dependencies, text),
224
+ writeErr: (text) => writeStderr(dependencies, text)
225
+ });
226
+ program.addHelpText(
227
+ "after",
228
+ `
229
+ Examples:
230
+ cms-lab init
231
+ cms-lab doctor --config cms-lab.config.ts
232
+ cms-lab scan --ci --report
233
+ cms-lab explain CMS-ROUTE-404
234
+ `
235
+ );
236
+ program.command("scan").description("Scan CMS content against configured site routes.").option("--url <url>", "Override config.site.url").option("--config <path>", "Path to cms-lab config file").option("--json", "Print ScanResult JSON").option("--ci", "Use stable CI-friendly terminal output").option(
237
+ "--fail-on <level>",
238
+ "Exit with code 1 on error, warning, or never",
239
+ "error"
240
+ ).option(
241
+ "--max-warnings <count>",
242
+ "Exit with code 1 when warnings exceed this count"
243
+ ).option(
244
+ "--max-info <count>",
245
+ "Exit with code 1 when info diagnostics exceed this count"
246
+ ).option(
247
+ "--strict",
248
+ "Fail on warnings and info diagnostics. Equivalent to --fail-on warning --max-info 0."
249
+ ).option(
250
+ "--report [path]",
251
+ "Write an HTML report. Defaults to .cms-lab/report.html"
252
+ ).option(
253
+ "--markdown [path]",
254
+ "Write a Markdown summary. Defaults to .cms-lab/summary.md"
255
+ ).option(
256
+ "--junit [path]",
257
+ "Write a JUnit XML report. Defaults to .cms-lab/junit.xml"
258
+ ).option(
259
+ "--slack-webhook <url>",
260
+ "Post a redacted scan summary to a Slack incoming webhook"
261
+ ).option(
262
+ "--notify-on <mode>",
263
+ "When to notify Slack: always, failure, or diagnostics",
264
+ "failure"
265
+ ).option(
266
+ "--include-sensitive-output",
267
+ "Include raw CMS document data and local project paths in --json output"
268
+ ).option(
269
+ "--type <type>",
270
+ "Limit scan to a CMS content type. Repeatable; comma-separated values are allowed.",
271
+ collectOption,
272
+ []
273
+ ).option(
274
+ "--only <check>",
275
+ "Run only a check group. Repeatable; comma-separated values are allowed.",
276
+ collectOption,
277
+ []
278
+ ).option(
279
+ "--skip <check>",
280
+ "Skip a check group. Repeatable; comma-separated values are allowed.",
281
+ collectOption,
282
+ []
283
+ ).option("--timeout <ms>", "Per-route HTTP timeout in milliseconds").option("--concurrency <count>", "Maximum concurrent route probes").option("--retries <count>", "Retry transient route probe failures", "1").option("--debug", "Write debug logs to stderr").option("--verbose <level>", "Debug verbosity level: 0, 1, 2, or 3").option("--no-color", "Disable ANSI color in terminal output").addHelpText(
284
+ "after",
285
+ `
286
+ Examples:
287
+ cms-lab scan
288
+ cms-lab scan --url https://staging.example.com
289
+ cms-lab scan --ci --report
290
+ cms-lab scan --json --include-sensitive-output
291
+ cms-lab scan --only routes,fields --fail-on warning
292
+ `
293
+ ).action(async (options) => {
294
+ exitCode = await runScan(options, dependencies);
295
+ });
296
+ program.command("doctor").description(
297
+ "Validate cms-lab config, project, site, and CMS connectivity."
298
+ ).option("--url <url>", "Override config.site.url").option("--config <path>", "Path to cms-lab config file").option("--timeout <ms>", "HTTP timeout in milliseconds").option("--retries <count>", "Retry transient connectivity failures", "1").option("--debug", "Write debug logs to stderr").option("--verbose <level>", "Debug verbosity level: 0, 1, 2, or 3").addHelpText(
299
+ "after",
300
+ `
301
+ Examples:
302
+ cms-lab doctor
303
+ cms-lab doctor --config cms-lab.config.ts
304
+ cms-lab doctor --url https://staging.example.com --debug
305
+ `
306
+ ).action(async (options) => {
307
+ exitCode = await runDoctor(options, dependencies);
308
+ });
309
+ program.command("explain").argument("<code>", "Diagnostic code to explain").description("Explain a cms-lab diagnostic code.").addHelpText(
310
+ "after",
311
+ `
312
+ Examples:
313
+ cms-lab explain CMS-ROUTE-404
314
+ cms-lab explain SEO-META-MISSING
315
+ `
316
+ ).action((code) => {
317
+ exitCode = runExplain(code, dependencies);
318
+ });
319
+ program.command("init").description("Create a starter cms-lab.config.ts file.").option("--config <path>", "Config file path", "cms-lab.config.ts").option("--force", "Overwrite an existing config file").option("--repository <name>", "Prismic repository name", "my-repo").option("--url <url>", "Site URL", "http://localhost:3000").addHelpText(
320
+ "after",
321
+ `
322
+ Examples:
323
+ cms-lab init
324
+ cms-lab init --repository my-prismic-repo --url http://localhost:3000
325
+ cms-lab init --config cms-lab.config.ts --force
326
+ `
327
+ ).action(async (options) => {
328
+ exitCode = await runInit(options, dependencies);
329
+ });
330
+ try {
331
+ await program.parseAsync(argv, { from: "user" });
332
+ return exitCode;
333
+ } catch (error) {
334
+ if (error instanceof CommanderError) {
335
+ return error.exitCode ?? 2;
336
+ }
337
+ writeStderr(dependencies, `Unexpected error: ${messageFrom(error)}
338
+ `);
339
+ return 2;
340
+ }
341
+ }
342
+ async function runScan(options, dependencies) {
343
+ const cwd = dependencies.cwd ?? process.cwd();
344
+ let debug = createNoopDebugLogger();
345
+ let endTotal = () => {
346
+ };
347
+ try {
348
+ debug = createDebugLogger(
349
+ "scan",
350
+ options.debug,
351
+ options.verbose,
352
+ dependencies
353
+ );
354
+ endTotal = debug.time("total", 2);
355
+ const timeoutMs = parseTimeout(options.timeout);
356
+ const concurrency = parseConcurrency(options.concurrency);
357
+ const retries = parseRetries(options.retries);
358
+ const strict = Boolean(options.strict);
359
+ const failOn = strict ? "warning" : parseFailOn(options.failOn);
360
+ const maxWarnings = parseOptionalThreshold(
361
+ options.maxWarnings,
362
+ "--max-warnings"
363
+ );
364
+ const maxInfo = parseOptionalThreshold(
365
+ options.maxInfo ?? (strict ? "0" : void 0),
366
+ "--max-info"
367
+ );
368
+ const notifyOn = parseNotifyOn(options.notifyOn);
369
+ const filters = {
370
+ types: splitList(options.type),
371
+ only: parseCheckGroups(options.only),
372
+ skip: parseCheckGroups(options.skip)
373
+ };
374
+ debug.log(1, `cwd ${cwd}`);
375
+ debug.log(
376
+ 2,
377
+ `options ${describeScanOptions(
378
+ { ...options, failOn, maxWarnings, maxInfo },
379
+ filters
380
+ )}`
381
+ );
382
+ const endConfig = debug.time("config", 2);
383
+ const loaded = await loadCmsLabConfig({ cwd, configPath: options.config });
384
+ endConfig();
385
+ const config = {
386
+ ...loaded.config,
387
+ site: {
388
+ ...loaded.config.site,
389
+ url: options.url ?? loaded.config.site.url
390
+ }
391
+ };
392
+ debug.log(1, `config ${loaded.configFile ?? "inline"}`);
393
+ debug.log(1, `site ${config.site.url}`);
394
+ debug.log(1, `cms ${describeCms(config.cms)}`);
395
+ if (config.framework.router !== "app") {
396
+ throw new ConfigLoadError(
397
+ "cms-lab only supports Next.js App Router projects"
398
+ );
399
+ }
400
+ const endProject = debug.time("project detection", 2);
401
+ const project = await detectNextProject(cwd);
402
+ endProject();
403
+ if (project.router !== "app") {
404
+ throw new ConfigLoadError(
405
+ "cms-lab only supports Next.js App Router projects"
406
+ );
407
+ }
408
+ debug.log(
409
+ 1,
410
+ `project next ${project.router} appDir=${project.appDir ?? "none"}`
411
+ );
412
+ const endCms = debug.time("cms fetch", 2);
413
+ const documents = await fetchCmsDocuments(config.cms, dependencies);
414
+ endCms();
415
+ debug.log(1, `documents ${documents.length}`);
416
+ debug.log(3, `document types ${describeDocumentTypes(documents)}`);
417
+ assertTypeFilterMatched(documents, filters.types);
418
+ const endScan = debug.time("scan", 2);
419
+ const result = await scanDocuments({
420
+ config,
421
+ project,
422
+ documents,
423
+ fetch: dependencies.fetch,
424
+ timeoutMs,
425
+ concurrency,
426
+ retries,
427
+ filters
428
+ });
429
+ endScan();
430
+ debug.log(
431
+ 1,
432
+ `summary errors=${result.summary.errors} warnings=${result.summary.warnings} info=${result.summary.info}`
433
+ );
434
+ const exitCode = exitCodeForResult(result, {
435
+ failOn,
436
+ maxWarnings,
437
+ maxInfo
438
+ });
439
+ const status = exitCode === 0 ? "passed" : "failed";
440
+ const endReport = debug.time("exports", 2);
441
+ await maybeWriteReport(options.report, result, cwd);
442
+ await maybeWriteMarkdown(options.markdown, result, status, cwd);
443
+ await maybeWriteJUnit(options.junit, result, cwd);
444
+ const slackSent = await maybePostSlack({
445
+ webhookUrl: options.slackWebhook,
446
+ notifyOn,
447
+ result,
448
+ status,
449
+ fetchImpl: dependencies.fetch
450
+ });
451
+ endReport();
452
+ if (options.report) {
453
+ debug.log(1, `report ${reportPathFromOption(options.report, cwd)}`);
454
+ }
455
+ if (options.markdown) {
456
+ debug.log(1, `markdown ${markdownPathFromOption(options.markdown, cwd)}`);
457
+ }
458
+ if (options.junit) {
459
+ debug.log(1, `junit ${junitPathFromOption(options.junit, cwd)}`);
460
+ }
461
+ if (slackSent) {
462
+ debug.log(1, "slack webhook sent");
463
+ } else if (options.slackWebhook) {
464
+ debug.log(1, "slack webhook skipped");
465
+ }
466
+ if (options.json) {
467
+ writeStdout(
468
+ dependencies,
469
+ `${JSON.stringify(jsonOutputResult(result, options), null, 2)}
470
+ `
471
+ );
472
+ } else {
473
+ writeStdout(
474
+ dependencies,
475
+ formatPrettyResult(result, {
476
+ color: shouldUseColor(options, dependencies),
477
+ failOn,
478
+ maxWarnings,
479
+ maxInfo,
480
+ hints: !options.ci
481
+ })
482
+ );
483
+ if (options.report) {
484
+ writeStdout(
485
+ dependencies,
486
+ `report ${reportPathFromOption(options.report, cwd)}
487
+ `
488
+ );
489
+ }
490
+ if (options.markdown) {
491
+ writeStdout(
492
+ dependencies,
493
+ `markdown ${markdownPathFromOption(options.markdown, cwd)}
494
+ `
495
+ );
496
+ }
497
+ if (options.junit) {
498
+ writeStdout(
499
+ dependencies,
500
+ `junit ${junitPathFromOption(options.junit, cwd)}
501
+ `
502
+ );
503
+ }
504
+ }
505
+ endTotal();
506
+ return exitCode;
507
+ } catch (error) {
508
+ endTotal();
509
+ debug.log(1, `error ${safeMessageFrom(error)}`);
510
+ if (error instanceof ConfigLoadError) {
511
+ writeStderr(
512
+ dependencies,
513
+ `Config error: ${redactSensitive(error.message)}
514
+ `
515
+ );
516
+ return 2;
517
+ }
518
+ if (error instanceof CmsFetchError) {
519
+ writeStderr(
520
+ dependencies,
521
+ `CMS error: ${redactSensitive(error.message)}
522
+ `
523
+ );
524
+ return 3;
525
+ }
526
+ if (error instanceof SiteUnreachableError) {
527
+ writeStderr(
528
+ dependencies,
529
+ `Site error: ${redactSensitive(error.message)}
530
+ `
531
+ );
532
+ return 4;
533
+ }
534
+ writeStderr(dependencies, `Unexpected error: ${safeMessageFrom(error)}
535
+ `);
536
+ return 2;
537
+ }
538
+ }
539
+ async function runDoctor(options, dependencies) {
540
+ const cwd = dependencies.cwd ?? process.cwd();
541
+ let debug = createNoopDebugLogger();
542
+ let endTotal = () => {
543
+ };
544
+ try {
545
+ debug = createDebugLogger(
546
+ "doctor",
547
+ options.debug,
548
+ options.verbose,
549
+ dependencies
550
+ );
551
+ endTotal = debug.time("total", 2);
552
+ const timeoutMs = parseTimeout(options.timeout) ?? 5e3;
553
+ const retries = parseRetries(options.retries) ?? 1;
554
+ debug.log(1, `cwd ${cwd}`);
555
+ debug.log(
556
+ 2,
557
+ `options timeout=${timeoutMs} retries=${retries} url=${options.url ?? "config"}`
558
+ );
559
+ const endConfig = debug.time("config", 2);
560
+ const loaded = await loadCmsLabConfig({ cwd, configPath: options.config });
561
+ endConfig();
562
+ const config = {
563
+ ...loaded.config,
564
+ site: {
565
+ ...loaded.config.site,
566
+ url: options.url ?? loaded.config.site.url
567
+ }
568
+ };
569
+ debug.log(1, `config ${loaded.configFile ?? "inline"}`);
570
+ debug.log(1, `site ${config.site.url}`);
571
+ debug.log(1, `cms ${describeCms(config.cms)}`);
572
+ writeStdout(
573
+ dependencies,
574
+ `config ok${loaded.configFile ? ` - ${loaded.configFile}` : ""}
575
+ `
576
+ );
577
+ if (config.framework.router !== "app") {
578
+ throw new ConfigLoadError(
579
+ "cms-lab only supports Next.js App Router projects"
580
+ );
581
+ }
582
+ const endProject = debug.time("project detection", 2);
583
+ const project = await detectNextProject(cwd);
584
+ endProject();
585
+ if (project.router !== "app") {
586
+ throw new ConfigLoadError(
587
+ "cms-lab only supports Next.js App Router projects"
588
+ );
589
+ }
590
+ debug.log(
591
+ 1,
592
+ `project next ${project.router} appDir=${project.appDir ?? "none"}`
593
+ );
594
+ writeStdout(
595
+ dependencies,
596
+ `next app ok - ${project.appDir ?? project.rootDir}
597
+ `
598
+ );
599
+ const endSite = debug.time("site probe", 2);
600
+ await fetchSite(config.site.url, dependencies.fetch, timeoutMs, retries);
601
+ endSite();
602
+ writeStdout(dependencies, `site ok - ${config.site.url}
603
+ `);
604
+ const endCms = debug.time("cms fetch", 2);
605
+ const documents = await fetchCmsDocuments(config.cms, dependencies);
606
+ endCms();
607
+ debug.log(1, `documents ${documents.length}`);
608
+ debug.log(3, `document types ${describeDocumentTypes(documents)}`);
609
+ writeStdout(
610
+ dependencies,
611
+ `cms ok - ${documents.length} ${plural3(documents.length, "document")}
612
+ `
613
+ );
614
+ endTotal();
615
+ return 0;
616
+ } catch (error) {
617
+ endTotal();
618
+ debug.log(1, `error ${safeMessageFrom(error)}`);
619
+ if (error instanceof ConfigLoadError) {
620
+ writeStderr(
621
+ dependencies,
622
+ `Config error: ${redactSensitive(error.message)}
623
+ `
624
+ );
625
+ return 2;
626
+ }
627
+ if (error instanceof CmsFetchError) {
628
+ writeStderr(
629
+ dependencies,
630
+ `CMS error: ${redactSensitive(error.message)}
631
+ `
632
+ );
633
+ return 3;
634
+ }
635
+ if (error instanceof SiteUnreachableError) {
636
+ writeStderr(
637
+ dependencies,
638
+ `Site error: ${redactSensitive(error.message)}
639
+ `
640
+ );
641
+ return 4;
642
+ }
643
+ writeStderr(dependencies, `Unexpected error: ${safeMessageFrom(error)}
644
+ `);
645
+ return 2;
646
+ }
647
+ }
648
+ function runExplain(code, dependencies) {
649
+ const explanation = explainDiagnostic(code);
650
+ if (!explanation) {
651
+ writeStderr(dependencies, `Unknown diagnostic code: ${code}
652
+ `);
653
+ return 2;
654
+ }
655
+ writeStdout(
656
+ dependencies,
657
+ [
658
+ `${explanation.code} (${explanation.severity})`,
659
+ explanation.title,
660
+ "",
661
+ `Meaning: ${explanation.meaning}`,
662
+ `Fix: ${explanation.fix}`,
663
+ ""
664
+ ].join("\n")
665
+ );
666
+ return 0;
667
+ }
668
+ async function runInit(options, dependencies) {
669
+ const cwd = dependencies.cwd ?? process.cwd();
670
+ const target = resolve(cwd, options.config ?? "cms-lab.config.ts");
671
+ try {
672
+ if (!options.force && await fileExists(target)) {
673
+ writeStderr(
674
+ dependencies,
675
+ `Config error: ${target} already exists. Use --force to overwrite it.
676
+ `
677
+ );
678
+ return 2;
679
+ }
680
+ await mkdir(dirname(target), { recursive: true });
681
+ await writeFile(
682
+ target,
683
+ starterConfig({
684
+ repository: options.repository ?? "my-repo",
685
+ url: options.url ?? "http://localhost:3000"
686
+ })
687
+ );
688
+ writeStdout(dependencies, `created ${target}
689
+ `);
690
+ return 0;
691
+ } catch (error) {
692
+ writeStderr(dependencies, `Config error: ${messageFrom(error)}
693
+ `);
694
+ return 2;
695
+ }
696
+ }
697
+ function writeStdout(dependencies, text) {
698
+ if (dependencies.stdout) {
699
+ dependencies.stdout(text);
700
+ return;
701
+ }
702
+ process.stdout.write(text);
703
+ }
704
+ function writeStderr(dependencies, text) {
705
+ if (dependencies.stderr) {
706
+ dependencies.stderr(text);
707
+ return;
708
+ }
709
+ process.stderr.write(text);
710
+ }
711
+ function messageFrom(error) {
712
+ return error instanceof Error ? error.message : String(error);
713
+ }
714
+ function safeMessageFrom(error) {
715
+ return redactSensitive(messageFrom(error));
716
+ }
717
+ function redactSensitive(value) {
718
+ return value.replaceAll(/(access_token=)[^&\s]+/gi, "$1[redacted]").replaceAll(/([?&](?:token|password|secret)=)[^&\s]+/gi, "$1[redacted]").replaceAll(/\bBearer\s+[-._~+/=a-z0-9]+/gi, "Bearer [redacted]").replaceAll(/(https?:\/\/)([^:\s/@]+):([^@\s/]+)@/gi, "$1[redacted]@");
719
+ }
720
+ function jsonOutputResult(result, options) {
721
+ if (options.includeSensitiveOutput) {
722
+ return result;
723
+ }
724
+ return {
725
+ ...result,
726
+ project: {
727
+ framework: result.project.framework,
728
+ router: result.project.router,
729
+ rootDir: "[redacted: pass --include-sensitive-output to emit raw project paths]",
730
+ ...result.project.appDir ? {
731
+ appDir: "[redacted: pass --include-sensitive-output to emit raw project paths]"
732
+ } : {},
733
+ ...result.project.pagesDir ? {
734
+ pagesDir: "[redacted: pass --include-sensitive-output to emit raw project paths]"
735
+ } : {}
736
+ },
737
+ documents: result.documents.map((document) => ({
738
+ id: document.id,
739
+ type: document.type,
740
+ status: document.status,
741
+ data: "[redacted: pass --include-sensitive-output to emit raw CMS data]"
742
+ }))
743
+ };
744
+ }
745
+ function collectOption(value, previous) {
746
+ return [...previous, value];
747
+ }
748
+ function splitList(values) {
749
+ return [
750
+ ...new Set(
751
+ (values ?? []).flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean)
752
+ )
753
+ ];
754
+ }
755
+ function createNoopDebugLogger() {
756
+ return {
757
+ log: () => {
758
+ },
759
+ time: () => () => {
760
+ }
761
+ };
762
+ }
763
+ function createDebugLogger(command, debug, verbose, dependencies) {
764
+ const level = parseVerbosity(debug, verbose);
765
+ return {
766
+ log: (messageLevel, message) => {
767
+ if (level < messageLevel) {
768
+ return;
769
+ }
770
+ writeStderr(dependencies, `[cms-lab:debug] ${command} ${message}
771
+ `);
772
+ },
773
+ time: (label, messageLevel = 2) => {
774
+ if (level < messageLevel) {
775
+ return () => {
776
+ };
777
+ }
778
+ const startedAt = performance.now();
779
+ let ended = false;
780
+ return () => {
781
+ if (ended) {
782
+ return;
783
+ }
784
+ ended = true;
785
+ writeStderr(
786
+ dependencies,
787
+ `[cms-lab:debug] ${command} timing ${label} ${formatDuration(
788
+ performance.now() - startedAt
789
+ )}
790
+ `
791
+ );
792
+ };
793
+ }
794
+ };
795
+ }
796
+ function parseVerbosity(debug, verbose) {
797
+ if (verbose === void 0) {
798
+ return debug ? 1 : 0;
799
+ }
800
+ if (verbose === "0" || verbose === "1" || verbose === "2" || verbose === "3") {
801
+ return Number(verbose);
802
+ }
803
+ throw new ConfigLoadError("--verbose must be one of: 0, 1, 2, 3");
804
+ }
805
+ function shouldUseColor(options, dependencies) {
806
+ if (options.ci || options.color === false) {
807
+ return false;
808
+ }
809
+ const env = dependencies.env ?? process.env;
810
+ if (env.NO_COLOR || env.TERM === "dumb") {
811
+ return false;
812
+ }
813
+ return dependencies.isStdoutTTY ?? Boolean(process.stdout.isTTY);
814
+ }
815
+ function describeScanOptions(options, filters) {
816
+ return [
817
+ `json=${Boolean(options.json)}`,
818
+ `ci=${Boolean(options.ci)}`,
819
+ `report=${Boolean(options.report)}`,
820
+ `markdown=${Boolean(options.markdown)}`,
821
+ `junit=${Boolean(options.junit)}`,
822
+ `slackWebhook=${Boolean(options.slackWebhook)}`,
823
+ `notifyOn=${options.notifyOn ?? "failure"}`,
824
+ `failOn=${options.failOn}`,
825
+ `maxWarnings=${options.maxWarnings ?? "none"}`,
826
+ `maxInfo=${options.maxInfo ?? "none"}`,
827
+ `timeout=${options.timeout ?? "default"}`,
828
+ `concurrency=${options.concurrency ?? "default"}`,
829
+ `retries=${options.retries ?? "1"}`,
830
+ `types=${formatList(filters.types)}`,
831
+ `only=${formatList(filters.only)}`,
832
+ `skip=${formatList(filters.skip)}`
833
+ ].join(" ");
834
+ }
835
+ function describeCms(config) {
836
+ if (config.provider === "prismic") {
837
+ return `prismic repository=${config.repositoryName}`;
838
+ }
839
+ if (config.provider === "strapi") {
840
+ return `strapi url=${safeUrl(config.url)} collections=${formatList(
841
+ config.collections.map((collection) => collection.endpoint)
842
+ )}`;
843
+ }
844
+ if (config.provider === "directus") {
845
+ return `directus url=${safeUrl(config.url)} collections=${formatList(
846
+ config.collections.map((collection) => collection.collection)
847
+ )}`;
848
+ }
849
+ return `wordpress url=${safeUrl(config.url)} contentTypes=${formatList(
850
+ config.contentTypes?.map((contentType) => contentType.endpoint) ?? [
851
+ "pages",
852
+ "posts"
853
+ ]
854
+ )}`;
855
+ }
856
+ function describeDocumentTypes(documents) {
857
+ const counts = /* @__PURE__ */ new Map();
858
+ for (const document of documents) {
859
+ counts.set(document.type, (counts.get(document.type) ?? 0) + 1);
860
+ }
861
+ return [...counts.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([type, count]) => `${type}=${count}`).join(", ") || "none";
862
+ }
863
+ function safeUrl(value) {
864
+ try {
865
+ const url = new URL(value);
866
+ url.username = "";
867
+ url.password = "";
868
+ url.search = "";
869
+ url.hash = "";
870
+ return url.toString().replace(/\/$/, "");
871
+ } catch {
872
+ return "<invalid-url>";
873
+ }
874
+ }
875
+ function formatList(values) {
876
+ return values.length > 0 ? values.join(",") : "none";
877
+ }
878
+ function formatDuration(ms) {
879
+ return `${ms.toFixed(1)}ms`;
880
+ }
881
+ async function fetchCmsDocuments(config, dependencies) {
882
+ if (dependencies.fetchCmsDocuments) {
883
+ return dependencies.fetchCmsDocuments(config);
884
+ }
885
+ if (config.provider === "prismic") {
886
+ const loadDocuments = dependencies.fetchPrismicDocuments ?? ((cmsConfig) => {
887
+ if (cmsConfig.provider !== "prismic") {
888
+ throw new ConfigLoadError(
889
+ `fetchPrismicDocuments cannot load ${cmsConfig.provider}`
890
+ );
891
+ }
892
+ return defaultFetchPrismicDocuments(cmsConfig, {
893
+ fetch: dependencies.fetch
894
+ });
895
+ });
896
+ return loadDocuments(config);
897
+ }
898
+ if (config.provider === "strapi") {
899
+ return defaultFetchStrapiDocuments(config, { fetch: dependencies.fetch });
900
+ }
901
+ if (config.provider === "directus") {
902
+ return defaultFetchDirectusDocuments(config, { fetch: dependencies.fetch });
903
+ }
904
+ return defaultFetchWordPressDocuments(config, { fetch: dependencies.fetch });
905
+ }
906
+ function assertTypeFilterMatched(documents, types) {
907
+ if (types.length === 0) {
908
+ return;
909
+ }
910
+ if (documents.some((document) => types.includes(document.type))) {
911
+ return;
912
+ }
913
+ const availableTypes = [
914
+ ...new Set(documents.map((document) => document.type))
915
+ ].sort().join(", ");
916
+ throw new ConfigLoadError(
917
+ `No CMS documents matched --type ${types.join(", ")}. Available types: ${availableTypes || "none"}`
918
+ );
919
+ }
920
+ function parseCheckGroups(values) {
921
+ const allowed = /* @__PURE__ */ new Set(["routes", "seo", "a11y", "images", "fields"]);
922
+ const groups = splitList(values);
923
+ for (const group of groups) {
924
+ if (!allowed.has(group)) {
925
+ throw new ConfigLoadError(
926
+ `Unknown check group "${group}". Expected one of: ${[...allowed].join(", ")}`
927
+ );
928
+ }
929
+ }
930
+ return groups;
931
+ }
932
+ function parseTimeout(value) {
933
+ if (!value) {
934
+ return void 0;
935
+ }
936
+ const timeout = Number(value);
937
+ if (!Number.isInteger(timeout) || timeout <= 0) {
938
+ throw new ConfigLoadError("--timeout must be a positive integer");
939
+ }
940
+ return timeout;
941
+ }
942
+ function parseConcurrency(value) {
943
+ if (!value) {
944
+ return void 0;
945
+ }
946
+ const concurrency = Number(value);
947
+ if (!Number.isInteger(concurrency) || concurrency <= 0) {
948
+ throw new ConfigLoadError("--concurrency must be a positive integer");
949
+ }
950
+ return concurrency;
951
+ }
952
+ function parseRetries(value) {
953
+ if (!value) {
954
+ return void 0;
955
+ }
956
+ const retries = Number(value);
957
+ if (!Number.isInteger(retries) || retries < 0) {
958
+ throw new ConfigLoadError("--retries must be a non-negative integer");
959
+ }
960
+ return retries;
961
+ }
962
+ function parseOptionalThreshold(value, flag) {
963
+ if (value === void 0) {
964
+ return void 0;
965
+ }
966
+ const threshold = Number(value);
967
+ if (!Number.isInteger(threshold) || threshold < 0) {
968
+ throw new ConfigLoadError(`${flag} must be a non-negative integer`);
969
+ }
970
+ return threshold;
971
+ }
972
+ async function fetchSite(url, fetchImpl, timeoutMs, retries) {
973
+ let lastError;
974
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
975
+ const controller = new AbortController();
976
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
977
+ try {
978
+ const response = await (fetchImpl ?? fetch)(url, {
979
+ method: "GET",
980
+ signal: controller.signal
981
+ });
982
+ if (!response.ok) {
983
+ lastError = new Error(`Site ${url} returned HTTP ${response.status}`);
984
+ if (attempt >= retries) {
985
+ break;
986
+ }
987
+ continue;
988
+ }
989
+ return;
990
+ } catch (error) {
991
+ lastError = error;
992
+ if (attempt >= retries) {
993
+ break;
994
+ }
995
+ } finally {
996
+ clearTimeout(timeout);
997
+ }
998
+ }
999
+ throw new SiteUnreachableError(messageFrom(lastError));
1000
+ }
1001
+ function parseFailOn(value) {
1002
+ const level = value ?? "error";
1003
+ if (level === "error" || level === "warning" || level === "never") {
1004
+ return level;
1005
+ }
1006
+ throw new ConfigLoadError("--fail-on must be one of: error, warning, never");
1007
+ }
1008
+ function exitCodeForResult(result, options) {
1009
+ const { failOn, maxWarnings, maxInfo } = options;
1010
+ if (failOn === "never") {
1011
+ return thresholdExceeded(result, maxWarnings, maxInfo) ? 1 : 0;
1012
+ }
1013
+ if (result.summary.errors > 0) {
1014
+ return 1;
1015
+ }
1016
+ if (failOn === "warning" && result.summary.warnings > 0) {
1017
+ return 1;
1018
+ }
1019
+ if (thresholdExceeded(result, maxWarnings, maxInfo)) {
1020
+ return 1;
1021
+ }
1022
+ return 0;
1023
+ }
1024
+ function thresholdExceeded(result, maxWarnings, maxInfo) {
1025
+ return maxWarnings !== void 0 && result.summary.warnings > maxWarnings || maxInfo !== void 0 && result.summary.info > maxInfo;
1026
+ }
1027
+ async function maybeWriteReport(report, result, cwd) {
1028
+ if (!report) {
1029
+ return;
1030
+ }
1031
+ const path = reportPathFromOption(report, cwd);
1032
+ await mkdir(dirname(path), { recursive: true });
1033
+ await writeFile(path, renderHtmlReport(result), "utf8");
1034
+ }
1035
+ function reportPathFromOption(report, cwd) {
1036
+ return resolve(
1037
+ cwd,
1038
+ typeof report === "string" ? report : ".cms-lab/report.html"
1039
+ );
1040
+ }
1041
+ async function maybeWriteMarkdown(markdown, result, status, cwd) {
1042
+ if (!markdown) {
1043
+ return;
1044
+ }
1045
+ const path = markdownPathFromOption(markdown, cwd);
1046
+ await mkdir(dirname(path), { recursive: true });
1047
+ await writeFile(path, renderMarkdownSummary(result, status), "utf8");
1048
+ }
1049
+ function markdownPathFromOption(markdown, cwd) {
1050
+ return resolve(
1051
+ cwd,
1052
+ typeof markdown === "string" ? markdown : ".cms-lab/summary.md"
1053
+ );
1054
+ }
1055
+ async function maybeWriteJUnit(junit, result, cwd) {
1056
+ if (!junit) {
1057
+ return;
1058
+ }
1059
+ const path = junitPathFromOption(junit, cwd);
1060
+ await mkdir(dirname(path), { recursive: true });
1061
+ await writeFile(path, renderJUnitReport(result), "utf8");
1062
+ }
1063
+ function junitPathFromOption(junit, cwd) {
1064
+ return resolve(cwd, typeof junit === "string" ? junit : ".cms-lab/junit.xml");
1065
+ }
1066
+ async function maybePostSlack(options) {
1067
+ if (!options.webhookUrl) {
1068
+ return false;
1069
+ }
1070
+ if (!shouldNotify(options.notifyOn, options.result, options.status)) {
1071
+ return false;
1072
+ }
1073
+ await postSlackPayload({
1074
+ webhookUrl: options.webhookUrl,
1075
+ fetchImpl: options.fetchImpl,
1076
+ payload: renderSlackPayload(options.result, options.status)
1077
+ });
1078
+ return true;
1079
+ }
1080
+ function shouldNotify(notifyOn, result, status) {
1081
+ if (notifyOn === "always") {
1082
+ return true;
1083
+ }
1084
+ if (notifyOn === "failure") {
1085
+ return status === "failed";
1086
+ }
1087
+ return result.diagnostics.length > 0;
1088
+ }
1089
+ function parseNotifyOn(value) {
1090
+ const notifyOn = value ?? "failure";
1091
+ if (notifyOn === "always" || notifyOn === "failure" || notifyOn === "diagnostics") {
1092
+ return notifyOn;
1093
+ }
1094
+ throw new ConfigLoadError(
1095
+ "--notify-on must be one of: always, failure, diagnostics"
1096
+ );
1097
+ }
1098
+ async function fileExists(path) {
1099
+ try {
1100
+ await access(path);
1101
+ return true;
1102
+ } catch {
1103
+ return false;
1104
+ }
1105
+ }
1106
+ function plural3(value, singular) {
1107
+ return value === 1 ? singular : `${singular}s`;
1108
+ }
1109
+ function starterConfig(options) {
1110
+ return `import { defineConfig } from "@cms-lab/core";
1111
+
1112
+ export default defineConfig({
1113
+ site: { url: ${JSON.stringify(options.url)} },
1114
+ framework: { type: "next", router: "app" },
1115
+ cms: {
1116
+ provider: "prismic",
1117
+ repositoryName: ${JSON.stringify(options.repository)},
1118
+ accessToken: process.env.PRISMIC_ACCESS_TOKEN,
1119
+ },
1120
+ routes: [
1121
+ { type: "page", pattern: "/:uid", getPath: (doc) => \`/\${doc.uid}\` },
1122
+ {
1123
+ type: "blog_post",
1124
+ pattern: "/blog/:uid",
1125
+ getPath: (doc) => \`/blog/\${doc.uid}\`,
1126
+ },
1127
+ ],
1128
+ });
1129
+ `;
1130
+ }
1131
+
1132
+ export {
1133
+ runCli
1134
+ };
@@ -0,0 +1,15 @@
1
+ import { FetchLike, CmsProviderConfig, CMSDocument } from '@cms-lab/core';
2
+
3
+ type CliDependencies = {
4
+ cwd?: string;
5
+ stdout?: (text: string) => void;
6
+ stderr?: (text: string) => void;
7
+ env?: Record<string, string | undefined>;
8
+ isStdoutTTY?: boolean;
9
+ fetch?: FetchLike;
10
+ fetchCmsDocuments?: (config: CmsProviderConfig) => Promise<CMSDocument[]>;
11
+ fetchPrismicDocuments?: (config: CmsProviderConfig) => Promise<CMSDocument[]>;
12
+ };
13
+ declare function runCli(argv: string[], dependencies?: CliDependencies): Promise<number>;
14
+
15
+ export { type CliDependencies, runCli };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ runCli
3
+ } from "./chunk-JHH7PD4C.js";
4
+ export {
5
+ runCli
6
+ };
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@cms-lab/cli",
3
+ "version": "1.0.1",
4
+ "type": "module",
5
+ "description": "Catch CMS bugs before deploy.",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/i-afaqrashid/cms-lab.git",
10
+ "directory": "packages/cli"
11
+ },
12
+ "homepage": "https://cms-lab.dev",
13
+ "bugs": {
14
+ "url": "https://github.com/i-afaqrashid/cms-lab/issues"
15
+ },
16
+ "keywords": [
17
+ "cms",
18
+ "nextjs",
19
+ "prismic",
20
+ "strapi",
21
+ "directus",
22
+ "wordpress",
23
+ "cli",
24
+ "testing"
25
+ ],
26
+ "bin": {
27
+ "cms-lab": "./dist/bin.js"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/index.d.ts",
32
+ "import": "./dist/index.js"
33
+ }
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "README.md"
38
+ ],
39
+ "engines": {
40
+ "node": ">=20.10"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "commander": "^14.0.2",
47
+ "picocolors": "^1.1.1",
48
+ "@cms-lab/core": "1.0.1",
49
+ "@cms-lab/directus": "1.0.1",
50
+ "@cms-lab/next": "1.0.1",
51
+ "@cms-lab/reporter": "1.0.1",
52
+ "@cms-lab/strapi": "1.0.1",
53
+ "@cms-lab/wordpress": "1.0.1",
54
+ "@cms-lab/prismic": "1.0.1"
55
+ },
56
+ "author": "Afaq Rashid",
57
+ "scripts": {
58
+ "build": "tsup src/index.ts src/bin.ts --format esm --dts --clean --tsconfig ../../tsconfig.base.json"
59
+ }
60
+ }