@cmssy/cli 9.7.2 → 9.9.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.
Files changed (2) hide show
  1. package/dist/index.js +342 -40
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,16 +3,9 @@
3
3
  // src/index.ts
4
4
  import { createInterface } from "readline/promises";
5
5
 
6
- // src/init.ts
7
- import {
8
- copyFileSync,
9
- existsSync,
10
- mkdirSync,
11
- readFileSync,
12
- writeFileSync
13
- } from "fs";
14
- import { dirname, join, resolve } from "path";
15
- import { fileURLToPath } from "url";
6
+ // src/add-block.ts
7
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
8
+ import { dirname, join as join2, resolve } from "path";
16
9
 
17
10
  // src/admin-client.ts
18
11
  var DEFAULT_ADMIN_API_URL = "https://api.cmssy.io/graphql";
@@ -151,12 +144,17 @@ Edit this site visually:
151
144
  ${paint(url, CYAN, colored)}
152
145
  `;
153
146
  }
147
+ function formatDraftPreviewLink(draftUrl, exitUrl, colored = useColor()) {
148
+ return `
149
+ Preview drafts without the editor (the /api/draft path also works on your local dev server):
150
+ ${paint(draftUrl, CYAN, colored)}
151
+ exit draft mode: ${paint(exitUrl, CYAN, colored)}
152
+ `;
153
+ }
154
154
 
155
- // src/init.ts
156
- var ASSETS_DIR = fileURLToPath(new URL("../assets/init", import.meta.url));
157
- var CLI_PACKAGE_JSON = fileURLToPath(
158
- new URL("../package.json", import.meta.url)
159
- );
155
+ // src/framework.ts
156
+ import { existsSync, readFileSync } from "fs";
157
+ import { join } from "path";
160
158
  var FRAMEWORKS = [
161
159
  {
162
160
  name: "next",
@@ -246,8 +244,272 @@ function detectFramework(pkg) {
246
244
  }
247
245
  return match;
248
246
  }
247
+ function nextSrcPrefix(root) {
248
+ return existsSync(join(root, "src", "app")) ? "src/" : "";
249
+ }
250
+
251
+ // src/add-block.ts
252
+ var BLOCK_NAME_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
253
+ function capitalize(word) {
254
+ return word.charAt(0).toUpperCase() + word.slice(1);
255
+ }
256
+ function blockNames(name) {
257
+ const words = name.split("-");
258
+ const pascal = words.map(capitalize).join("");
259
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
260
+ return {
261
+ type: name,
262
+ label: words.map(capitalize).join(" "),
263
+ pascal,
264
+ camel,
265
+ block: `${camel}Block`,
266
+ props: `${camel}Props`
267
+ };
268
+ }
269
+ function nextBlockDefinition(names) {
270
+ return `import { defineBlock } from "@cmssy/react";
271
+ import ${names.pascal}, { ${names.props} } from "./${names.pascal}";
272
+
273
+ export const ${names.block} = defineBlock({
274
+ type: "${names.type}",
275
+ label: "${names.label}",
276
+ component: ${names.pascal},
277
+ props: ${names.props},
278
+ });
279
+ `;
280
+ }
281
+ function nextComponent(names) {
282
+ return `import { fields, type BlockProps } from "@cmssy/react";
283
+
284
+ export const ${names.props} = {
285
+ heading: fields.text({ label: "Heading", required: true }),
286
+ text: fields.textarea({ label: "Text" }),
287
+ };
288
+
289
+ export default function ${names.pascal}({ content }: BlockProps<typeof ${names.props}>) {
290
+ return (
291
+ <section>
292
+ <h2>{content.heading}</h2>
293
+ {content.text ? <p>{content.text}</p> : null}
294
+ </section>
295
+ );
296
+ }
297
+ `;
298
+ }
299
+ function singleFileComponent(names) {
300
+ return `import { fields, type BlockProps } from "@cmssy/react";
301
+
302
+ export const ${names.props} = {
303
+ heading: fields.text({ label: "Heading", required: true }),
304
+ text: fields.textarea({ label: "Text" }),
305
+ };
306
+
307
+ export function ${names.pascal}({ content }: BlockProps<typeof ${names.props}>) {
308
+ return (
309
+ <section>
310
+ <h2>{content.heading}</h2>
311
+ {content.text ? <p>{content.text}</p> : null}
312
+ </section>
313
+ );
314
+ }
315
+ `;
316
+ }
317
+ function insertAfterImports(source, line) {
318
+ const imports = [...source.matchAll(/^import .*$/gm)];
319
+ const last = imports[imports.length - 1];
320
+ if (last === void 0) return `${line}
321
+ ${source}`;
322
+ const end = (last.index ?? 0) + last[0].length;
323
+ return `${source.slice(0, end)}
324
+ ${line}${source.slice(end)}`;
325
+ }
326
+ function appendToBlocksArray(source, identifier) {
327
+ const match = source.match(/export const blocks = \[([\s\S]*?)\]/);
328
+ if (!match) {
329
+ throw new CliError(
330
+ "could not find `export const blocks = [...]` in the block registry",
331
+ `add the block yourself: import it and append ${identifier} to the blocks array`
332
+ );
333
+ }
334
+ const inner = match[1] ?? "";
335
+ let updated;
336
+ if (inner.trim() === "") {
337
+ updated = identifier;
338
+ } else if (inner.includes("\n")) {
339
+ const body = inner.replace(/[\s,]*$/, "");
340
+ const indent = /\n(\s*)\S/.exec(inner)?.[1] ?? " ";
341
+ updated = `${body},
342
+ ${indent}${identifier},
343
+ `;
344
+ } else {
345
+ updated = `${inner.trim().replace(/,$/, "")}, ${identifier}`;
346
+ }
347
+ return source.replace(match[0], `export const blocks = [${updated}]`);
348
+ }
349
+ function frameworkLayout(framework, root, names) {
350
+ if (framework.name === "next") {
351
+ const prefix = nextSrcPrefix(root);
352
+ const componentPath2 = `${prefix}blocks/${names.type}/${names.pascal}.tsx`;
353
+ return {
354
+ registryPath: `${prefix}cmssy/blocks.ts`,
355
+ componentPath: componentPath2,
356
+ files: [
357
+ {
358
+ path: `${prefix}blocks/${names.type}/block.ts`,
359
+ content: nextBlockDefinition(names)
360
+ },
361
+ { path: componentPath2, content: nextComponent(names) }
362
+ ],
363
+ registerInRegistry: (source) => appendToBlocksArray(
364
+ insertAfterImports(
365
+ source,
366
+ `import { ${names.block} } from "@/blocks/${names.type}/block";`
367
+ ),
368
+ names.block
369
+ )
370
+ };
371
+ }
372
+ const base = framework.name === "astro" ? "src/cmssy" : "app/cmssy";
373
+ const componentPath = `${base}/${names.type}.tsx`;
374
+ return {
375
+ registryPath: `${base}/blocks.ts`,
376
+ componentPath,
377
+ files: [{ path: componentPath, content: singleFileComponent(names) }],
378
+ registerInRegistry: (source) => {
379
+ let updated = insertAfterImports(
380
+ source,
381
+ `import { ${names.pascal}, ${names.props} } from "./${names.type}";`
382
+ );
383
+ if (!/import \{[^}]*defineBlock[^}]*\} from "@cmssy\/react"/.test(updated)) {
384
+ updated = insertAfterImports(
385
+ updated,
386
+ `import { defineBlock } from "@cmssy/react";`
387
+ );
388
+ }
389
+ const definition = `export const ${names.block} = defineBlock({
390
+ type: "${names.type}",
391
+ label: "${names.label}",
392
+ component: ${names.pascal},
393
+ props: ${names.props},
394
+ });
395
+
396
+ `;
397
+ const anchor = updated.indexOf("export const blocks =");
398
+ if (anchor === -1) {
399
+ throw new CliError(
400
+ "could not find `export const blocks = [...]` in the block registry",
401
+ `add the block yourself: define ${names.block} with defineBlock and append it to the blocks array`
402
+ );
403
+ }
404
+ updated = `${updated.slice(0, anchor)}${definition}${updated.slice(anchor)}`;
405
+ return appendToBlocksArray(updated, names.block);
406
+ }
407
+ };
408
+ }
409
+ function runAddBlock(options, deps) {
410
+ const { log } = deps;
411
+ try {
412
+ if (!BLOCK_NAME_PATTERN.test(options.name)) {
413
+ throw new CliError(
414
+ `"${options.name}" is not a valid block name`,
415
+ "use kebab-case: cmssy add block pricing-table"
416
+ );
417
+ }
418
+ const root = resolve(deps.cwd, options.dir ?? ".");
419
+ if (!existsSync2(root)) {
420
+ throw new CliError(
421
+ `${root} does not exist`,
422
+ "pass --dir with the app's directory, or run cmssy add block inside it"
423
+ );
424
+ }
425
+ const framework = detectFramework(readPackageJson(root));
426
+ const names = blockNames(options.name);
427
+ const layout = frameworkLayout(framework, root, names);
428
+ const registryFile = join2(root, layout.registryPath);
429
+ if (!existsSync2(registryFile)) {
430
+ throw new CliError(
431
+ `no block registry at ${layout.registryPath}`,
432
+ "run cmssy init first - it wires the registry the editor loads blocks from"
433
+ );
434
+ }
435
+ const registry = readFileSync2(registryFile, "utf8");
436
+ if (registry.includes(names.block) || registry.includes(`type: "${names.type}"`)) {
437
+ throw new CliError(
438
+ `block "${names.type}" is already registered in ${layout.registryPath}`
439
+ );
440
+ }
441
+ for (const file of layout.files) {
442
+ if (existsSync2(join2(root, file.path))) {
443
+ throw new CliError(
444
+ `${file.path} already exists`,
445
+ "pick another name or remove the existing block first"
446
+ );
447
+ }
448
+ }
449
+ const updatedRegistry = layout.registerInRegistry(registry, names);
450
+ log(
451
+ formatResult({
452
+ status: "ok",
453
+ message: `detected ${framework.label} - scaffolding block "${names.type}"`
454
+ })
455
+ );
456
+ for (const file of layout.files) {
457
+ const target = join2(root, file.path);
458
+ mkdirSync(dirname(target), { recursive: true });
459
+ writeFileSync(target, file.content);
460
+ log(formatResult({ status: "ok", message: `wrote ${file.path}` }));
461
+ }
462
+ writeFileSync(registryFile, updatedRegistry);
463
+ log(
464
+ formatResult({
465
+ status: "ok",
466
+ message: `registered ${names.block} in ${layout.registryPath}`
467
+ })
468
+ );
469
+ log("");
470
+ log("Next steps:");
471
+ log(` 1. edit ${layout.componentPath} - define the fields and markup`);
472
+ log(
473
+ ` 2. restart your dev server - the editor picks up "${names.type}" from the next handshake`
474
+ );
475
+ return 0;
476
+ } catch (error) {
477
+ if (error instanceof CliError) {
478
+ log(
479
+ formatResult({
480
+ status: "fail",
481
+ message: error.message,
482
+ fix: error.fix
483
+ })
484
+ );
485
+ return 1;
486
+ }
487
+ log(
488
+ formatResult({
489
+ status: "fail",
490
+ message: error instanceof Error ? error.message : String(error)
491
+ })
492
+ );
493
+ return 1;
494
+ }
495
+ }
496
+
497
+ // src/init.ts
498
+ import {
499
+ copyFileSync,
500
+ existsSync as existsSync3,
501
+ mkdirSync as mkdirSync2,
502
+ readFileSync as readFileSync3,
503
+ writeFileSync as writeFileSync2
504
+ } from "fs";
505
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
506
+ import { fileURLToPath } from "url";
507
+ var ASSETS_DIR = fileURLToPath(new URL("../assets/init", import.meta.url));
508
+ var CLI_PACKAGE_JSON = fileURLToPath(
509
+ new URL("../package.json", import.meta.url)
510
+ );
249
511
  function frameworkFiles(framework, root) {
250
- const srcPrefix = framework.name === "next" && existsSync(join(root, "src", "app")) ? "src/" : "";
512
+ const srcPrefix = framework.name === "next" ? nextSrcPrefix(root) : "";
251
513
  return [
252
514
  { asset: "env.example", target: ".env.example" },
253
515
  ...framework.files.map((path) => ({
@@ -257,7 +519,7 @@ function frameworkFiles(framework, root) {
257
519
  ];
258
520
  }
259
521
  function cliVersion() {
260
- const pkg = JSON.parse(readFileSync(CLI_PACKAGE_JSON, "utf8"));
522
+ const pkg = JSON.parse(readFileSync3(CLI_PACKAGE_JSON, "utf8"));
261
523
  return pkg.version;
262
524
  }
263
525
  function addDependencies(root, pkg, framework) {
@@ -272,19 +534,19 @@ function addDependencies(root, pkg, framework) {
272
534
  pkg.dependencies = Object.fromEntries(
273
535
  Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
274
536
  );
275
- writeFileSync(
276
- join(root, "package.json"),
537
+ writeFileSync2(
538
+ join3(root, "package.json"),
277
539
  `${JSON.stringify(pkg, null, 2)}
278
540
  `
279
541
  );
280
542
  return missing.map((name) => `${name}@${range}`);
281
543
  }
282
544
  function detectInstallCommand(root) {
283
- if (existsSync(join(root, "pnpm-lock.yaml")) || existsSync(join(root, "pnpm-workspace.yaml"))) {
545
+ if (existsSync3(join3(root, "pnpm-lock.yaml")) || existsSync3(join3(root, "pnpm-workspace.yaml"))) {
284
546
  return "pnpm install";
285
547
  }
286
- if (existsSync(join(root, "yarn.lock"))) return "yarn";
287
- if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) {
548
+ if (existsSync3(join3(root, "yarn.lock"))) return "yarn";
549
+ if (existsSync3(join3(root, "bun.lock")) || existsSync3(join3(root, "bun.lockb"))) {
288
550
  return "bun install";
289
551
  }
290
552
  return "npm install";
@@ -292,9 +554,8 @@ function detectInstallCommand(root) {
292
554
  function frameworkNotes(framework, root, skipped) {
293
555
  const notes = [];
294
556
  if (framework.name === "next") {
295
- const srcPrefix = existsSync(join(root, "src", "app")) ? "src/" : "";
296
- const home = `${srcPrefix}app/page.tsx`;
297
- if (existsSync(join(root, home))) {
557
+ const home = `${nextSrcPrefix(root)}app/page.tsx`;
558
+ if (existsSync3(join3(root, home))) {
298
559
  notes.push({
299
560
  status: "unknown",
300
561
  message: `${home} conflicts with the cmssy catch-all route - delete it and the cmssy page serves /`
@@ -306,7 +567,7 @@ function frameworkNotes(framework, root, skipped) {
306
567
  status: "unknown",
307
568
  message: "the cmssy wiring needs the React integration and a server adapter - run: npx astro add react node"
308
569
  });
309
- if (existsSync(join(root, "src/pages/index.astro"))) {
570
+ if (existsSync3(join3(root, "src/pages/index.astro"))) {
310
571
  notes.push({
311
572
  status: "unknown",
312
573
  message: "src/pages/index.astro shadows the cmssy catch-all for / - delete it and the cmssy page serves /"
@@ -324,8 +585,8 @@ function frameworkNotes(framework, root, skipped) {
324
585
  function runInit(options, deps) {
325
586
  const { log } = deps;
326
587
  try {
327
- const root = resolve(deps.cwd, options.dir ?? ".");
328
- if (!existsSync(root)) {
588
+ const root = resolve2(deps.cwd, options.dir ?? ".");
589
+ if (!existsSync3(root)) {
329
590
  throw new CliError(
330
591
  `${root} does not exist`,
331
592
  "pass --dir with the app's directory, or run cmssy init inside it"
@@ -342,8 +603,8 @@ function runInit(options, deps) {
342
603
  const written = [];
343
604
  const skipped = [];
344
605
  for (const file of frameworkFiles(framework, root)) {
345
- const target = join(root, file.target);
346
- if (existsSync(target) && !options.force) {
606
+ const target = join3(root, file.target);
607
+ if (existsSync3(target) && !options.force) {
347
608
  skipped.push(file.target);
348
609
  log(
349
610
  formatResult({
@@ -353,8 +614,8 @@ function runInit(options, deps) {
353
614
  );
354
615
  continue;
355
616
  }
356
- mkdirSync(dirname(target), { recursive: true });
357
- copyFileSync(join(ASSETS_DIR, framework.name, file.asset), target);
617
+ mkdirSync2(dirname2(target), { recursive: true });
618
+ copyFileSync(join3(ASSETS_DIR, framework.name, file.asset), target);
358
619
  written.push(file.target);
359
620
  log(formatResult({ status: "ok", message: `wrote ${file.target}` }));
360
621
  }
@@ -406,8 +667,8 @@ function runInit(options, deps) {
406
667
  }
407
668
 
408
669
  // src/link.ts
409
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
410
- import { join as join2 } from "path";
670
+ import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
671
+ import { join as join4 } from "path";
411
672
  import {
412
673
  buildEditorUrl,
413
674
  checkDraftSecret,
@@ -477,9 +738,9 @@ function mergeEnvContent(existing, updates) {
477
738
  var ENV_FILES = [".env.local", ".env"];
478
739
  function loadEnvFiles(cwd, env) {
479
740
  for (const file of ENV_FILES) {
480
- const path = join2(cwd, file);
481
- if (!existsSync2(path)) continue;
482
- applyEnv(parseEnvFile(readFileSync2(path, "utf8")), env);
741
+ const path = join4(cwd, file);
742
+ if (!existsSync4(path)) continue;
743
+ applyEnv(parseEnvFile(readFileSync4(path, "utf8")), env);
483
744
  }
484
745
  }
485
746
  function resolveToken(options, deps) {
@@ -496,6 +757,24 @@ function describeWorkspace(workspace) {
496
757
  const org = workspace.organizationSlug ?? "?";
497
758
  return `${workspace.name} (${org}/${workspace.slug})`;
498
759
  }
760
+ function draftRouteBase(previewUrl) {
761
+ try {
762
+ const base = new URL(previewUrl);
763
+ const basePath = base.pathname.replace(/\/+$/, "");
764
+ return `${base.origin}${basePath}/api/draft`;
765
+ } catch {
766
+ return null;
767
+ }
768
+ }
769
+ function buildDraftPreviewUrls(previewUrl, draftSecret) {
770
+ const base = draftRouteBase(previewUrl);
771
+ if (!base) return null;
772
+ const params = new URLSearchParams({ secret: draftSecret, redirect: "/" });
773
+ return {
774
+ draftUrl: `${base}?${params.toString()}`,
775
+ exitUrl: `${base}?disable=1`
776
+ };
777
+ }
499
778
  async function selectWorkspace(workspaces, options, deps) {
500
779
  if (workspaces.length === 0) {
501
780
  throw new CliError(
@@ -562,9 +841,9 @@ function resolvePreviewUrl(options) {
562
841
  return origin;
563
842
  }
564
843
  function writeEnvLocal(cwd, updates) {
565
- const path = join2(cwd, ".env.local");
566
- const existing = existsSync2(path) ? readFileSync2(path, "utf8") : null;
567
- writeFileSync2(path, mergeEnvContent(existing, updates));
844
+ const path = join4(cwd, ".env.local");
845
+ const existing = existsSync4(path) ? readFileSync4(path, "utf8") : null;
846
+ writeFileSync3(path, mergeEnvContent(existing, updates));
568
847
  }
569
848
  async function runLink(options, deps) {
570
849
  const { cwd, env, log } = deps;
@@ -636,6 +915,15 @@ async function runLink(options, deps) {
636
915
  const secretResult = await checkDraftSecret(preflight);
637
916
  log(formatResult(secretResult));
638
917
  log(formatEditorLink(buildEditorUrl(preflight)));
918
+ if (reachable.previewUrl && secretResult.status !== "fail") {
919
+ const draftUrls = buildDraftPreviewUrls(
920
+ reachable.previewUrl,
921
+ draftSecret
922
+ );
923
+ if (draftUrls) {
924
+ log(formatDraftPreviewLink(draftUrls.draftUrl, draftUrls.exitUrl));
925
+ }
926
+ }
639
927
  return reachable.status === "fail" || secretResult.status === "fail" ? 1 : 0;
640
928
  } catch (error) {
641
929
  if (error instanceof CliError) {
@@ -662,6 +950,7 @@ async function runLink(options, deps) {
662
950
  var USAGE = [
663
951
  "usage: cmssy <command>",
664
952
  " cmssy init [--dir <path>] [--force]",
953
+ " cmssy add block <name> [--dir <path>]",
665
954
  " cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]"
666
955
  ].join("\n");
667
956
  function flagValue(args, name) {
@@ -703,6 +992,19 @@ async function main() {
703
992
  );
704
993
  return;
705
994
  }
995
+ if (command === "add") {
996
+ const [kind, name, ...rest] = args;
997
+ if (kind !== "block" || name === void 0 || name.startsWith("--")) {
998
+ console.error(USAGE);
999
+ process.exitCode = 1;
1000
+ return;
1001
+ }
1002
+ process.exitCode = runAddBlock(
1003
+ { name, dir: flagValue(rest, "--dir") },
1004
+ { cwd: process.cwd(), log: (line) => console.log(line) }
1005
+ );
1006
+ return;
1007
+ }
706
1008
  if (command === "link") {
707
1009
  process.exitCode = await runLinkCommand(args);
708
1010
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "9.7.2",
3
+ "version": "9.9.0",
4
4
  "description": "The cmssy CLI: `cmssy init` wires cmssy into an existing Next.js, Astro or React Router app; `cmssy link` connects it to a workspace and verifies the editor wiring.",
5
5
  "keywords": [
6
6
  "cmssy",
@@ -27,7 +27,7 @@
27
27
  "assets"
28
28
  ],
29
29
  "dependencies": {
30
- "@cmssy/core": "9.7.2"
30
+ "@cmssy/core": "9.9.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^20.0.0",