@cmssy/cli 9.4.0 → 9.5.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 (35) hide show
  1. package/assets/init/astro/env.example +7 -0
  2. package/assets/init/astro/src/cmssy/blocks.ts +12 -0
  3. package/assets/init/astro/src/cmssy/editor.tsx +9 -0
  4. package/assets/init/astro/src/cmssy/hero.tsx +17 -0
  5. package/assets/init/astro/src/cmssy.config.ts +9 -0
  6. package/assets/init/astro/src/components/Blocks.tsx +37 -0
  7. package/assets/init/astro/src/middleware.ts +6 -0
  8. package/assets/init/astro/src/pages/[...path].astro +38 -0
  9. package/assets/init/astro/src/pages/cmssy-edit/[...path].astro +33 -0
  10. package/assets/init/astro/src/pages/robots.txt.ts +5 -0
  11. package/assets/init/astro/src/pages/sitemap.xml.ts +5 -0
  12. package/assets/init/next/app/[[...path]]/page.tsx +16 -0
  13. package/assets/init/next/app/api/draft/route.ts +4 -0
  14. package/assets/init/next/app/cmssy-edit/[[...path]]/page.tsx +13 -0
  15. package/assets/init/next/app/robots.ts +4 -0
  16. package/assets/init/next/app/sitemap.ts +7 -0
  17. package/assets/init/next/blocks/hero/Hero.tsx +17 -0
  18. package/assets/init/next/blocks/hero/block.ts +17 -0
  19. package/assets/init/next/cmssy/blocks.ts +5 -0
  20. package/assets/init/next/cmssy/editable-layout.tsx +12 -0
  21. package/assets/init/next/cmssy/editor.tsx +10 -0
  22. package/assets/init/next/cmssy.config.ts +14 -0
  23. package/assets/init/next/env.example +7 -0
  24. package/assets/init/next/proxy.ts +13 -0
  25. package/assets/init/remix/app/cmssy/blocks.ts +12 -0
  26. package/assets/init/remix/app/cmssy/editor.tsx +14 -0
  27. package/assets/init/remix/app/cmssy/hero.tsx +17 -0
  28. package/assets/init/remix/app/routes/page.tsx +67 -0
  29. package/assets/init/remix/app/routes/robots.ts +4 -0
  30. package/assets/init/remix/app/routes/sitemap.ts +4 -0
  31. package/assets/init/remix/app/routes.ts +10 -0
  32. package/assets/init/remix/cmssy.config.ts +9 -0
  33. package/assets/init/remix/env.example +7 -0
  34. package/dist/index.js +333 -52
  35. package/package.json +6 -4
@@ -0,0 +1,7 @@
1
+ # Settings → Headless in the cmssy dashboard
2
+ CMSSY_ORG_SLUG=
3
+ CMSSY_WORKSPACE_SLUG=
4
+
5
+ # Also Settings → Headless. The editor proves itself with this secret, so an
6
+ # unverified ?cmssyEdit=1 cannot open the door.
7
+ CMSSY_DRAFT_SECRET=
@@ -0,0 +1,12 @@
1
+ import { defineBlock } from "@cmssy/react";
2
+ import { Hero, heroProps } from "./hero";
3
+
4
+ export const heroBlock = defineBlock({
5
+ type: "hero",
6
+ label: "Hero",
7
+ component: Hero,
8
+ props: heroProps,
9
+ });
10
+
11
+ export const blocks = [heroBlock];
12
+ export default blocks;
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { CmssyLazyEditor } from "@cmssy/react/client";
3
+
4
+ // The edit bridge is a client island: the editor talks to the page over
5
+ // postMessage, and that protocol lives in @cmssy/core - not in React and not in
6
+ // Next. That is why the same bridge works here.
7
+ export default function CmssyEditor(props: Record<string, unknown>) {
8
+ return <CmssyLazyEditor {...(props as never)} load={() => import("./blocks")} />;
9
+ }
@@ -0,0 +1,17 @@
1
+ import { fields, type BlockProps } from "@cmssy/react";
2
+
3
+ // The schema lives next to the component that reads it, and types it. Rename a
4
+ // field here and this file stops compiling - it cannot render an empty block.
5
+ export const heroProps = {
6
+ title: fields.text({ label: "Title", required: true }),
7
+ subtitle: fields.text({ label: "Subtitle" }),
8
+ };
9
+
10
+ export function Hero({ content }: BlockProps<typeof heroProps>) {
11
+ return (
12
+ <section className="hero">
13
+ <h1>{content.title}</h1>
14
+ {content.subtitle ? <p>{content.subtitle}</p> : null}
15
+ </section>
16
+ );
17
+ }
@@ -0,0 +1,9 @@
1
+ import { defineCmssyConfig } from "@cmssy/astro";
2
+
3
+ // Pass the env raw: a `?? ""` fallback turns a missing variable into an empty
4
+ // one, and the error then surfaces somewhere unrelated.
5
+ export const cmssy = defineCmssyConfig({
6
+ org: process.env.CMSSY_ORG_SLUG,
7
+ workspaceSlug: process.env.CMSSY_WORKSPACE_SLUG,
8
+ draftSecret: process.env.CMSSY_DRAFT_SECRET,
9
+ });
@@ -0,0 +1,37 @@
1
+ import { CmssyBlock, buildBlockMap, buildBlockContext } from "@cmssy/react";
2
+ import type { CmssyPageData } from "@cmssy/core";
3
+ import { blocks } from "../cmssy/blocks";
4
+
5
+ /**
6
+ * The public page: React blocks rendered on the server by Astro, with no client
7
+ * JS at all. The visitor gets HTML; the framework stays out of the render path.
8
+ */
9
+ export function Blocks({
10
+ page,
11
+ locale,
12
+ defaultLocale,
13
+ enabledLocales,
14
+ }: {
15
+ page: CmssyPageData;
16
+ locale: string;
17
+ defaultLocale: string;
18
+ enabledLocales: string[];
19
+ }) {
20
+ const blockMap = buildBlockMap(blocks);
21
+ const context = buildBlockContext(locale, defaultLocale, enabledLocales);
22
+
23
+ return (
24
+ <>
25
+ {(page.blocks ?? []).map((block) => (
26
+ <CmssyBlock
27
+ key={block.id}
28
+ block={block}
29
+ blockMap={blockMap}
30
+ locale={locale}
31
+ defaultLocale={defaultLocale}
32
+ context={context}
33
+ />
34
+ ))}
35
+ </>
36
+ );
37
+ }
@@ -0,0 +1,6 @@
1
+ import { cmssyMiddleware } from "@cmssy/astro";
2
+ import { cmssy } from "./cmssy.config";
3
+
4
+ // The whole adapter. It resolves the language, routes a verified editor request
5
+ // to /cmssy-edit, and applies the CSP that lets the admin frame this site.
6
+ export const onRequest = cmssyMiddleware(cmssy);
@@ -0,0 +1,38 @@
1
+ ---
2
+ import { loadCmssyPage } from "@cmssy/astro";
3
+ import { cmssy } from "../cmssy.config";
4
+ import { Blocks } from "../components/Blocks";
5
+
6
+ export const prerender = false;
7
+
8
+ const { page, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
9
+ cmssy,
10
+ Astro.request,
11
+ Astro.url,
12
+ );
13
+
14
+ if (!page) {
15
+ Astro.response.status = 404;
16
+ }
17
+ ---
18
+
19
+ <html lang={locale}>
20
+ <head>
21
+ <meta charset="utf-8" />
22
+ <title>{page?.seoTitle?.[locale] ?? page?.displayName?.[locale] ?? "cmssy on Astro"}</title>
23
+ </head>
24
+ <body>
25
+ {
26
+ page ? (
27
+ <Blocks
28
+ page={page}
29
+ locale={locale}
30
+ defaultLocale={defaultLocale}
31
+ enabledLocales={enabledLocales}
32
+ />
33
+ ) : (
34
+ <main><h1>Not found</h1></main>
35
+ )
36
+ }
37
+ </body>
38
+ </html>
@@ -0,0 +1,33 @@
1
+ ---
2
+ import { loadCmssyPage } from "@cmssy/astro";
3
+ import { cmssy } from "../../cmssy.config";
4
+ import CmssyEditor from "../../cmssy/editor";
5
+
6
+ // The editor iframe lands here (the middleware rewrites verified requests). It
7
+ // must be dynamic: a prerendered page never sees the query string.
8
+ export const prerender = false;
9
+
10
+ const { page, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
11
+ cmssy,
12
+ Astro.request,
13
+ Astro.url,
14
+ );
15
+ const editorOrigin = cmssy.editorOrigin ?? "*";
16
+ ---
17
+
18
+ <html lang={locale}>
19
+ <head>
20
+ <meta charset="utf-8" />
21
+ <title>cmssy editor</title>
22
+ </head>
23
+ <body>
24
+ <CmssyEditor
25
+ client:load
26
+ page={page}
27
+ locale={locale}
28
+ defaultLocale={defaultLocale}
29
+ enabledLocales={enabledLocales}
30
+ edit={{ editorOrigin }}
31
+ />
32
+ </body>
33
+ </html>
@@ -0,0 +1,5 @@
1
+ import { createCmssyRobots } from "@cmssy/astro";
2
+ import { cmssy } from "../cmssy.config";
3
+
4
+ export const prerender = false;
5
+ export const GET = createCmssyRobots(cmssy);
@@ -0,0 +1,5 @@
1
+ import { createCmssySitemap } from "@cmssy/astro";
2
+ import { cmssy } from "../cmssy.config";
3
+
4
+ export const prerender = false;
5
+ export const GET = createCmssySitemap(cmssy);
@@ -0,0 +1,16 @@
1
+ import { buildCmssyMetadata, createCmssyPage } from "@cmssy/next/server";
2
+ import { cmssy } from "@/cmssy.config";
3
+ import { blocks } from "@/cmssy/blocks";
4
+ import { CmssyEditor } from "@/cmssy/editor";
5
+
6
+ type PageProps = { params: Promise<{ path?: string[] }> };
7
+
8
+ export async function generateMetadata({ params }: PageProps) {
9
+ const { path } = await params;
10
+ // As routed, prefix and all: the prefix IS the language. Strip it and every
11
+ // translated page gets the default language's title - and a canonical
12
+ // pointing at the default language's URL, which reads as "duplicate".
13
+ return buildCmssyMetadata(cmssy, path);
14
+ }
15
+
16
+ export default createCmssyPage(cmssy, blocks, { editor: CmssyEditor });
@@ -0,0 +1,4 @@
1
+ import { createDraftRoute } from "@cmssy/next/server";
2
+ import { cmssy } from "@/cmssy.config";
3
+
4
+ export const GET = createDraftRoute(cmssy);
@@ -0,0 +1,13 @@
1
+ import { createCmssyEditPage } from "@cmssy/next/server";
2
+ import { cmssy } from "@/cmssy.config";
3
+ import { blocks } from "@/cmssy/blocks";
4
+ import { CmssyEditor } from "@/cmssy/editor";
5
+
6
+ // The route the middleware rewrites a verified editor request onto. The public
7
+ // pages stay static, and a static page never sees the query string that would
8
+ // put it in edit mode - which is why the editor needs a route of its own.
9
+ //
10
+ // Delete this file and the editor preview goes blank.
11
+ export const dynamic = "force-dynamic";
12
+
13
+ export default createCmssyEditPage(cmssy, blocks, { editor: CmssyEditor });
@@ -0,0 +1,4 @@
1
+ import { createCmssyRobots } from "@cmssy/next/server";
2
+ import { cmssy } from "@/cmssy.config";
3
+
4
+ export default createCmssyRobots(cmssy);
@@ -0,0 +1,7 @@
1
+ import { createCmssySitemap } from "@cmssy/next/server";
2
+ import { cmssy } from "@/cmssy.config";
3
+
4
+ // One entry per language version of every published page, with hreflang and
5
+ // x-default. Rendering products or categories from model records? They are not
6
+ // pages - add them through `extra`.
7
+ export default createCmssySitemap(cmssy);
@@ -0,0 +1,17 @@
1
+ import { fields, type BlockProps } from "@cmssy/react";
2
+
3
+ // The schema lives next to the component that reads it, and types it. Rename a
4
+ // field here and this file stops compiling - it cannot render an empty block.
5
+ export const heroProps = {
6
+ heading: fields.text({ label: "Heading", required: true }),
7
+ text: fields.textarea({ label: "Text" }),
8
+ };
9
+
10
+ export default function Hero({ content }: BlockProps<typeof heroProps>) {
11
+ return (
12
+ <section>
13
+ <h1>{content.heading}</h1>
14
+ {content.text ? <p>{content.text}</p> : null}
15
+ </section>
16
+ );
17
+ }
@@ -0,0 +1,17 @@
1
+ import { defineBlock } from "@cmssy/react";
2
+ import Hero, { heroProps } from "./Hero";
3
+
4
+ export const heroBlock = defineBlock({
5
+ type: "hero",
6
+ label: "Hero",
7
+ component: Hero,
8
+ props: heroProps,
9
+ // A loader runs SERVER-SIDE before the block renders, so a block can read data
10
+ // (products, posts) the same way a page can. Import it dynamically: it may
11
+ // touch the cmssy config, which must never reach the browser.
12
+ //
13
+ // loader: async () => {
14
+ // const { loadPosts } = await import("./load-posts");
15
+ // return { posts: await loadPosts() };
16
+ // },
17
+ });
@@ -0,0 +1,5 @@
1
+ import { heroBlock } from "@/blocks/hero/block";
2
+
3
+ // Every block your app renders. The editor loads this list lazily; the server
4
+ // renders from it directly.
5
+ export const blocks = [heroBlock];
@@ -0,0 +1,12 @@
1
+ "use client";
2
+
3
+ import {
4
+ CmssyLazyLayout,
5
+ type CmssyLazyLayoutProps,
6
+ } from "@cmssy/react/client";
7
+
8
+ // Mounts the layout blocks through the edit bridge, so the header and the footer
9
+ // are editable in the editor rather than markup it can select and cannot fill.
10
+ export function EditableLayout(props: Omit<CmssyLazyLayoutProps, "load">) {
11
+ return <CmssyLazyLayout {...props} load={() => import("./blocks")} />;
12
+ }
@@ -0,0 +1,10 @@
1
+ "use client";
2
+
3
+ import { CmssyLazyEditor } from "@cmssy/react/client";
4
+ import type { CmssyEditorProps } from "@cmssy/next";
5
+
6
+ // The block registry is loaded lazily ON THE CLIENT, so block loaders - which
7
+ // run server-side and read the config - never reach the browser bundle.
8
+ export function CmssyEditor(props: CmssyEditorProps) {
9
+ return <CmssyLazyEditor {...props} load={() => import("./blocks")} />;
10
+ }
@@ -0,0 +1,14 @@
1
+ import { defineCmssyConfig } from "@cmssy/next";
2
+
3
+ // Pass process.env raw: defineCmssyConfig validates at startup and names any
4
+ // variable you are missing. A `?? ""` fallback would hide that, and the error
5
+ // would surface later, somewhere unrelated.
6
+ //
7
+ // This module reads server env. Never import a VALUE from it (or from a module
8
+ // that imports it) in a "use client" component - types are erased, values drag
9
+ // process.env into the browser bundle.
10
+ export const cmssy = defineCmssyConfig({
11
+ org: process.env.CMSSY_ORG_SLUG,
12
+ workspaceSlug: process.env.CMSSY_WORKSPACE_SLUG,
13
+ draftSecret: process.env.CMSSY_DRAFT_SECRET,
14
+ });
@@ -0,0 +1,7 @@
1
+ # `npx @cmssy/cli link --token cs_...` writes all three into .env.local for you.
2
+ # Your organization slug (Settings -> Headless in the cmssy dashboard)
3
+ CMSSY_ORG_SLUG=
4
+ # Your workspace slug (Settings -> Headless in the cmssy dashboard)
5
+ CMSSY_WORKSPACE_SLUG=
6
+ # Draft/preview secret, generated per workspace (Settings -> Headless)
7
+ CMSSY_DRAFT_SECRET=
@@ -0,0 +1,13 @@
1
+ import { createCmssyProxy } from "@cmssy/next/middleware";
2
+ import { cmssy } from "@/cmssy.config";
3
+
4
+ // Resolves the language, sends verified editor traffic to /cmssy-edit (carrying
5
+ // that language and the edit flag), and lets the cmssy admin frame this site.
6
+ // The order matters, which is why it lives in the preset and not here.
7
+ export const proxy = createCmssyProxy(cmssy);
8
+
9
+ // Next parses this at compile time, so the matcher has to be a literal - an
10
+ // imported constant is rejected. Skips Next internals, API routes and files.
11
+ export const config = {
12
+ matcher: ["/((?!_next/|api/|.*\\..*).*)"],
13
+ };
@@ -0,0 +1,12 @@
1
+ import { defineBlock } from "@cmssy/react";
2
+ import { Hero, heroProps } from "./hero";
3
+
4
+ export const heroBlock = defineBlock({
5
+ type: "hero",
6
+ label: "Hero",
7
+ component: Hero,
8
+ props: heroProps,
9
+ });
10
+
11
+ export const blocks = [heroBlock];
12
+ export default blocks;
@@ -0,0 +1,14 @@
1
+ import { CmssyLazyEditor } from "@cmssy/react/client";
2
+ import type { CmssyPageData } from "@cmssy/core";
3
+
4
+ // The edit bridge: the editor talks to the page over postMessage, and that
5
+ // protocol lives in @cmssy/core - not in React and not in a framework.
6
+ export function CmssyEditor(props: {
7
+ page: CmssyPageData | null;
8
+ locale: string;
9
+ defaultLocale: string;
10
+ enabledLocales: string[];
11
+ edit: { editorOrigin: string | string[] };
12
+ }) {
13
+ return <CmssyLazyEditor {...props} load={() => import("./blocks")} />;
14
+ }
@@ -0,0 +1,17 @@
1
+ import { fields, type BlockProps } from "@cmssy/react";
2
+
3
+ // The schema lives next to the component that reads it, and types it. Rename a
4
+ // field here and this file stops compiling - it cannot render an empty block.
5
+ export const heroProps = {
6
+ title: fields.text({ label: "Title", required: true }),
7
+ subtitle: fields.text({ label: "Subtitle" }),
8
+ };
9
+
10
+ export function Hero({ content }: BlockProps<typeof heroProps>) {
11
+ return (
12
+ <section>
13
+ <h1>{content.title}</h1>
14
+ {content.subtitle ? <p>{content.subtitle}</p> : null}
15
+ </section>
16
+ );
17
+ }
@@ -0,0 +1,67 @@
1
+ import { CmssyBlock, buildBlockContext, buildBlockMap } from "@cmssy/react";
2
+ import { createCmssyHeaders, createCmssyLoader } from "@cmssy/remix";
3
+ import { cmssy } from "../../cmssy.config";
4
+ import { blocks } from "../cmssy/blocks";
5
+ import { CmssyEditor } from "../cmssy/editor";
6
+ import type { Route } from "./+types/page";
7
+
8
+ export const loader = createCmssyLoader(cmssy);
9
+
10
+ // Without these the admin cannot frame the site, and the editor shows an empty
11
+ // box with no error anywhere.
12
+ export const headers = createCmssyHeaders(cmssy);
13
+
14
+ export default function CmssyPage({ loaderData }: Route.ComponentProps) {
15
+ const {
16
+ page,
17
+ locale,
18
+ defaultLocale,
19
+ enabledLocales,
20
+ isEdit,
21
+ editorOrigin,
22
+ diagnostics,
23
+ } = loaderData;
24
+
25
+ if (diagnostics) {
26
+ return <div dangerouslySetInnerHTML={{ __html: diagnostics }} />;
27
+ }
28
+
29
+ // A verified editor request renders the same page through the edit bridge.
30
+ // No separate route: a React Router page always sees its query string.
31
+ if (isEdit) {
32
+ return (
33
+ <CmssyEditor
34
+ page={page}
35
+ locale={locale}
36
+ defaultLocale={defaultLocale}
37
+ enabledLocales={enabledLocales}
38
+ edit={{ editorOrigin }}
39
+ />
40
+ );
41
+ }
42
+
43
+ if (!page)
44
+ return (
45
+ <main>
46
+ <h1>Not found</h1>
47
+ </main>
48
+ );
49
+
50
+ const blockMap = buildBlockMap(blocks);
51
+ const context = buildBlockContext(locale, defaultLocale, enabledLocales);
52
+
53
+ return (
54
+ <main>
55
+ {(page.blocks ?? []).map((block) => (
56
+ <CmssyBlock
57
+ key={block.id}
58
+ block={block}
59
+ blockMap={blockMap}
60
+ locale={locale}
61
+ defaultLocale={defaultLocale}
62
+ context={context}
63
+ />
64
+ ))}
65
+ </main>
66
+ );
67
+ }
@@ -0,0 +1,4 @@
1
+ import { createCmssyRobots } from "@cmssy/remix";
2
+ import { cmssy } from "../../cmssy.config";
3
+
4
+ export const loader = createCmssyRobots(cmssy);
@@ -0,0 +1,4 @@
1
+ import { createCmssySitemap } from "@cmssy/remix";
2
+ import { cmssy } from "../../cmssy.config";
3
+
4
+ export const loader = createCmssySitemap(cmssy);
@@ -0,0 +1,10 @@
1
+ import { type RouteConfig, index, route } from "@react-router/dev/routes";
2
+
3
+ // The splat does not match "/", so the homepage needs its own entry - it is the
4
+ // same route module, mounted twice.
5
+ export default [
6
+ route("sitemap.xml", "routes/sitemap.ts"),
7
+ route("robots.txt", "routes/robots.ts"),
8
+ index("routes/page.tsx"),
9
+ route("*", "routes/page.tsx", { id: "cmssy-catch-all" }),
10
+ ] satisfies RouteConfig;
@@ -0,0 +1,9 @@
1
+ import { defineCmssyConfig } from "@cmssy/remix";
2
+
3
+ // Pass the env raw: a `?? ""` fallback turns a missing variable into an empty
4
+ // one, and the error then surfaces somewhere unrelated.
5
+ export const cmssy = defineCmssyConfig({
6
+ org: process.env.CMSSY_ORG_SLUG,
7
+ workspaceSlug: process.env.CMSSY_WORKSPACE_SLUG,
8
+ draftSecret: process.env.CMSSY_DRAFT_SECRET,
9
+ });
@@ -0,0 +1,7 @@
1
+ # Settings → Headless in the cmssy dashboard
2
+ CMSSY_ORG_SLUG=
3
+ CMSSY_WORKSPACE_SLUG=
4
+
5
+ # Also Settings → Headless. The editor proves itself with this secret, so an
6
+ # unverified ?cmssyEdit=1 cannot open the door.
7
+ CMSSY_DRAFT_SECRET=
package/dist/index.js CHANGED
@@ -3,14 +3,16 @@
3
3
  // src/index.ts
4
4
  import { createInterface } from "readline/promises";
5
5
 
6
- // src/link.ts
7
- import { existsSync, readFileSync, writeFileSync } from "fs";
8
- import { join } from "path";
6
+ // src/init.ts
9
7
  import {
10
- buildEditorUrl,
11
- checkDraftSecret,
12
- checkWorkspaceReachable
13
- } from "@cmssy/core/preflight";
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";
14
16
 
15
17
  // src/admin-client.ts
16
18
  var DEFAULT_ADMIN_API_URL = "https://api.cmssy.io/graphql";
@@ -120,6 +122,298 @@ async function setPreviewUrl(previewUrl, options) {
120
122
  );
121
123
  }
122
124
 
125
+ // src/format.ts
126
+ var GREEN = "\x1B[32m";
127
+ var RED = "\x1B[31m";
128
+ var YELLOW = "\x1B[33m";
129
+ var CYAN = "\x1B[36m";
130
+ var RESET = "\x1B[0m";
131
+ function useColor(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
132
+ return isTty && env.NO_COLOR === void 0;
133
+ }
134
+ function paint(text, color, colored) {
135
+ return colored ? `${color}${text}${RESET}` : text;
136
+ }
137
+ function formatResult(result, colored = useColor()) {
138
+ if (result.status === "ok") {
139
+ return `${paint("\u2713", GREEN, colored)} ${result.message}`;
140
+ }
141
+ if (result.status === "unknown") {
142
+ return `${paint("?", YELLOW, colored)} ${result.message}`;
143
+ }
144
+ const fix = result.fix ? `
145
+ fix: ${result.fix}` : "";
146
+ return `${paint("\u2717", RED, colored)} ${result.message}${fix}`;
147
+ }
148
+ function formatEditorLink(url, colored = useColor()) {
149
+ return `
150
+ Edit this site visually:
151
+ ${paint(url, CYAN, colored)}
152
+ `;
153
+ }
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
+ );
160
+ var FRAMEWORKS = [
161
+ {
162
+ name: "next",
163
+ label: "Next.js",
164
+ createCommand: "npx create-next-app@latest",
165
+ detect: ["next"],
166
+ dependencies: ["@cmssy/next", "@cmssy/react"],
167
+ files: [
168
+ "cmssy.config.ts",
169
+ "proxy.ts",
170
+ "cmssy/blocks.ts",
171
+ "cmssy/editor.tsx",
172
+ "cmssy/editable-layout.tsx",
173
+ "blocks/hero/block.ts",
174
+ "blocks/hero/Hero.tsx",
175
+ "app/[[...path]]/page.tsx",
176
+ "app/cmssy-edit/[[...path]]/page.tsx",
177
+ "app/api/draft/route.ts",
178
+ "app/robots.ts",
179
+ "app/sitemap.ts"
180
+ ]
181
+ },
182
+ {
183
+ name: "astro",
184
+ label: "Astro",
185
+ createCommand: "npm create astro@latest",
186
+ detect: ["astro"],
187
+ dependencies: ["@cmssy/astro", "@cmssy/react", "@cmssy/core"],
188
+ files: [
189
+ "src/cmssy.config.ts",
190
+ "src/middleware.ts",
191
+ "src/cmssy/blocks.ts",
192
+ "src/cmssy/editor.tsx",
193
+ "src/cmssy/hero.tsx",
194
+ "src/components/Blocks.tsx",
195
+ "src/pages/[...path].astro",
196
+ "src/pages/cmssy-edit/[...path].astro",
197
+ "src/pages/robots.txt.ts",
198
+ "src/pages/sitemap.xml.ts"
199
+ ]
200
+ },
201
+ {
202
+ name: "remix",
203
+ label: "React Router 7 / Remix",
204
+ createCommand: "npx create-react-router@latest",
205
+ detect: ["react-router", "@react-router/dev", "@remix-run/react"],
206
+ dependencies: ["@cmssy/remix", "@cmssy/react", "@cmssy/core"],
207
+ files: [
208
+ "cmssy.config.ts",
209
+ "app/routes.ts",
210
+ "app/cmssy/blocks.ts",
211
+ "app/cmssy/editor.tsx",
212
+ "app/cmssy/hero.tsx",
213
+ "app/routes/page.tsx",
214
+ "app/routes/robots.ts",
215
+ "app/routes/sitemap.ts"
216
+ ]
217
+ }
218
+ ];
219
+ function readPackageJson(root) {
220
+ const path = join(root, "package.json");
221
+ if (!existsSync(path)) {
222
+ throw new CliError(
223
+ `no package.json in ${root}`,
224
+ "cmssy init wires cmssy into an EXISTING app - create one first, then rerun inside it"
225
+ );
226
+ }
227
+ try {
228
+ return JSON.parse(readFileSync(path, "utf8"));
229
+ } catch {
230
+ throw new CliError(`${path} is not valid JSON`);
231
+ }
232
+ }
233
+ function detectFramework(pkg) {
234
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
235
+ const match = FRAMEWORKS.find(
236
+ (framework) => framework.detect.some((name) => deps[name] !== void 0)
237
+ );
238
+ if (!match) {
239
+ const hints = FRAMEWORKS.map(
240
+ (framework) => `${framework.label}: create the app with \`${framework.createCommand}\`, then rerun cmssy init inside it`
241
+ );
242
+ throw new CliError(
243
+ "no supported framework in package.json (looked for next, astro, react-router)",
244
+ hints.join("\n ")
245
+ );
246
+ }
247
+ return match;
248
+ }
249
+ function frameworkFiles(framework, root) {
250
+ const srcPrefix = framework.name === "next" && existsSync(join(root, "src", "app")) ? "src/" : "";
251
+ return [
252
+ { asset: "env.example", target: ".env.example" },
253
+ ...framework.files.map((path) => ({
254
+ asset: path,
255
+ target: framework.name === "next" ? `${srcPrefix}${path}` : path
256
+ }))
257
+ ];
258
+ }
259
+ function cliVersion() {
260
+ const pkg = JSON.parse(readFileSync(CLI_PACKAGE_JSON, "utf8"));
261
+ return pkg.version;
262
+ }
263
+ function addDependencies(root, pkg, framework) {
264
+ const present = { ...pkg.dependencies, ...pkg.devDependencies };
265
+ const missing = framework.dependencies.filter(
266
+ (name) => present[name] === void 0
267
+ );
268
+ if (missing.length === 0) return [];
269
+ const range = `^${cliVersion()}`;
270
+ const dependencies = { ...pkg.dependencies };
271
+ for (const name of missing) dependencies[name] = range;
272
+ pkg.dependencies = Object.fromEntries(
273
+ Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
274
+ );
275
+ writeFileSync(
276
+ join(root, "package.json"),
277
+ `${JSON.stringify(pkg, null, 2)}
278
+ `
279
+ );
280
+ return missing.map((name) => `${name}@${range}`);
281
+ }
282
+ function detectInstallCommand(root) {
283
+ if (existsSync(join(root, "pnpm-lock.yaml")) || existsSync(join(root, "pnpm-workspace.yaml"))) {
284
+ return "pnpm install";
285
+ }
286
+ if (existsSync(join(root, "yarn.lock"))) return "yarn";
287
+ if (existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb"))) {
288
+ return "bun install";
289
+ }
290
+ return "npm install";
291
+ }
292
+ function frameworkNotes(framework, root, skipped) {
293
+ const notes = [];
294
+ 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))) {
298
+ notes.push({
299
+ status: "unknown",
300
+ message: `${home} conflicts with the cmssy catch-all route - delete it and the cmssy page serves /`
301
+ });
302
+ }
303
+ }
304
+ if (framework.name === "astro") {
305
+ notes.push({
306
+ status: "unknown",
307
+ message: "the cmssy wiring needs the React integration and a server adapter - run: npx astro add react node"
308
+ });
309
+ if (existsSync(join(root, "src/pages/index.astro"))) {
310
+ notes.push({
311
+ status: "unknown",
312
+ message: "src/pages/index.astro shadows the cmssy catch-all for / - delete it and the cmssy page serves /"
313
+ });
314
+ }
315
+ }
316
+ if (framework.name === "remix" && skipped.includes("app/routes.ts")) {
317
+ notes.push({
318
+ status: "unknown",
319
+ message: "app/routes.ts already existed - mount routes/page.tsx (index + splat), routes/robots.ts and routes/sitemap.ts there yourself, or rerun with --force"
320
+ });
321
+ }
322
+ return notes;
323
+ }
324
+ function runInit(options, deps) {
325
+ const { log } = deps;
326
+ try {
327
+ const root = resolve(deps.cwd, options.dir ?? ".");
328
+ if (!existsSync(root)) {
329
+ throw new CliError(
330
+ `${root} does not exist`,
331
+ "pass --dir with the app's directory, or run cmssy init inside it"
332
+ );
333
+ }
334
+ const pkg = readPackageJson(root);
335
+ const framework = detectFramework(pkg);
336
+ log(
337
+ formatResult({
338
+ status: "ok",
339
+ message: `detected ${framework.label} - wiring cmssy into ${root}`
340
+ })
341
+ );
342
+ const written = [];
343
+ const skipped = [];
344
+ for (const file of frameworkFiles(framework, root)) {
345
+ const target = join(root, file.target);
346
+ if (existsSync(target) && !options.force) {
347
+ skipped.push(file.target);
348
+ log(
349
+ formatResult({
350
+ status: "unknown",
351
+ message: `${file.target} exists, skipped (--force overwrites)`
352
+ })
353
+ );
354
+ continue;
355
+ }
356
+ mkdirSync(dirname(target), { recursive: true });
357
+ copyFileSync(join(ASSETS_DIR, framework.name, file.asset), target);
358
+ written.push(file.target);
359
+ log(formatResult({ status: "ok", message: `wrote ${file.target}` }));
360
+ }
361
+ const added = addDependencies(root, pkg, framework);
362
+ if (added.length > 0) {
363
+ log(
364
+ formatResult({
365
+ status: "ok",
366
+ message: `added ${added.join(", ")} to package.json`
367
+ })
368
+ );
369
+ }
370
+ for (const note of frameworkNotes(framework, root, skipped)) {
371
+ log(formatResult(note));
372
+ }
373
+ log("");
374
+ log(
375
+ `${written.length} file${written.length === 1 ? "" : "s"} written, ${skipped.length} skipped.`
376
+ );
377
+ log("");
378
+ log("Next steps:");
379
+ let step = 1;
380
+ if (added.length > 0) log(` ${step++}. ${detectInstallCommand(root)}`);
381
+ log(` ${step++}. npx @cmssy/cli link --token cs_...`);
382
+ const registry = frameworkFiles(framework, root).find(
383
+ (file) => file.target.endsWith("cmssy/blocks.ts")
384
+ );
385
+ log(` ${step}. add your blocks to ${registry?.target} and publish a page`);
386
+ return 0;
387
+ } catch (error) {
388
+ if (error instanceof CliError) {
389
+ log(
390
+ formatResult({
391
+ status: "fail",
392
+ message: error.message,
393
+ fix: error.fix
394
+ })
395
+ );
396
+ return 1;
397
+ }
398
+ log(
399
+ formatResult({
400
+ status: "fail",
401
+ message: error instanceof Error ? error.message : String(error)
402
+ })
403
+ );
404
+ return 1;
405
+ }
406
+ }
407
+
408
+ // src/link.ts
409
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
410
+ import { join as join2 } from "path";
411
+ import {
412
+ buildEditorUrl,
413
+ checkDraftSecret,
414
+ checkWorkspaceReachable
415
+ } from "@cmssy/core/preflight";
416
+
123
417
  // src/env-file.ts
124
418
  function parseEnvFile(content) {
125
419
  const vars = {};
@@ -179,43 +473,13 @@ function mergeEnvContent(existing, updates) {
179
473
  `;
180
474
  }
181
475
 
182
- // src/format.ts
183
- var GREEN = "\x1B[32m";
184
- var RED = "\x1B[31m";
185
- var YELLOW = "\x1B[33m";
186
- var CYAN = "\x1B[36m";
187
- var RESET = "\x1B[0m";
188
- function useColor(env = process.env, isTty = Boolean(process.stdout.isTTY)) {
189
- return isTty && env.NO_COLOR === void 0;
190
- }
191
- function paint(text, color, colored) {
192
- return colored ? `${color}${text}${RESET}` : text;
193
- }
194
- function formatResult(result, colored = useColor()) {
195
- if (result.status === "ok") {
196
- return `${paint("\u2713", GREEN, colored)} ${result.message}`;
197
- }
198
- if (result.status === "unknown") {
199
- return `${paint("?", YELLOW, colored)} ${result.message}`;
200
- }
201
- const fix = result.fix ? `
202
- fix: ${result.fix}` : "";
203
- return `${paint("\u2717", RED, colored)} ${result.message}${fix}`;
204
- }
205
- function formatEditorLink(url, colored = useColor()) {
206
- return `
207
- Edit this site visually:
208
- ${paint(url, CYAN, colored)}
209
- `;
210
- }
211
-
212
476
  // src/link.ts
213
477
  var ENV_FILES = [".env.local", ".env"];
214
478
  function loadEnvFiles(cwd, env) {
215
479
  for (const file of ENV_FILES) {
216
- const path = join(cwd, file);
217
- if (!existsSync(path)) continue;
218
- applyEnv(parseEnvFile(readFileSync(path, "utf8")), env);
480
+ const path = join2(cwd, file);
481
+ if (!existsSync2(path)) continue;
482
+ applyEnv(parseEnvFile(readFileSync2(path, "utf8")), env);
219
483
  }
220
484
  }
221
485
  function resolveToken(options, deps) {
@@ -298,9 +562,9 @@ function resolvePreviewUrl(options) {
298
562
  return origin;
299
563
  }
300
564
  function writeEnvLocal(cwd, updates) {
301
- const path = join(cwd, ".env.local");
302
- const existing = existsSync(path) ? readFileSync(path, "utf8") : null;
303
- writeFileSync(path, mergeEnvContent(existing, updates));
565
+ const path = join2(cwd, ".env.local");
566
+ const existing = existsSync2(path) ? readFileSync2(path, "utf8") : null;
567
+ writeFileSync2(path, mergeEnvContent(existing, updates));
304
568
  }
305
569
  async function runLink(options, deps) {
306
570
  const { cwd, env, log } = deps;
@@ -395,22 +659,23 @@ async function runLink(options, deps) {
395
659
  }
396
660
 
397
661
  // src/index.ts
398
- var USAGE = "usage: cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]";
662
+ var USAGE = [
663
+ "usage: cmssy <command>",
664
+ " cmssy init [--dir <path>] [--force]",
665
+ " cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]"
666
+ ].join("\n");
399
667
  function flagValue(args, name) {
400
668
  const index = args.findIndex((arg) => arg === name);
401
669
  if (index !== -1) return args[index + 1];
402
670
  return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
403
671
  }
404
- async function main() {
405
- const [command, ...args] = process.argv.slice(2);
406
- if (command !== "link") {
407
- console.error(USAGE);
408
- process.exitCode = command === void 0 || command === "--help" ? 0 : 1;
409
- return;
410
- }
672
+ function hasFlag(args, name) {
673
+ return args.includes(name);
674
+ }
675
+ async function runLinkCommand(args) {
411
676
  const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
412
677
  try {
413
- process.exitCode = await runLink(
678
+ return await runLink(
414
679
  {
415
680
  token: flagValue(args, "--token"),
416
681
  workspace: flagValue(args, "--workspace"),
@@ -429,6 +694,22 @@ async function main() {
429
694
  rl?.close();
430
695
  }
431
696
  }
697
+ async function main() {
698
+ const [command, ...args] = process.argv.slice(2);
699
+ if (command === "init") {
700
+ process.exitCode = runInit(
701
+ { dir: flagValue(args, "--dir"), force: hasFlag(args, "--force") },
702
+ { cwd: process.cwd(), log: (line) => console.log(line) }
703
+ );
704
+ return;
705
+ }
706
+ if (command === "link") {
707
+ process.exitCode = await runLinkCommand(args);
708
+ return;
709
+ }
710
+ console.error(USAGE);
711
+ process.exitCode = command === void 0 || command === "--help" ? 0 : 1;
712
+ }
432
713
  main().catch((error) => {
433
714
  console.error(
434
715
  `cmssy: ${error instanceof Error ? error.message : String(error)}`
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "9.4.0",
4
- "description": "One command to connect an app to a cmssy workspace: `cmssy link` writes .env.local, sets the preview URL and verifies the editor wiring.",
3
+ "version": "9.5.0",
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",
7
7
  "cli",
8
+ "init",
8
9
  "link"
9
10
  ],
10
11
  "homepage": "https://cmssy.com",
@@ -22,10 +23,11 @@
22
23
  "cmssy": "./dist/index.js"
23
24
  },
24
25
  "files": [
25
- "dist"
26
+ "dist",
27
+ "assets"
26
28
  ],
27
29
  "dependencies": {
28
- "@cmssy/core": "9.4.0"
30
+ "@cmssy/core": "9.5.0"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@types/node": "^20.0.0",