@cmssy/cli 14.0.0 → 14.2.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.
@@ -2,6 +2,7 @@ import {
2
2
  CmssyBlock,
3
3
  buildBlockContext,
4
4
  buildBlockMap,
5
+ type CmssyBlockPage,
5
6
  type CmssyLayoutGroup,
6
7
  } from "@cmssy/react";
7
8
  import { CmssyLazyLayout } from "@cmssy/react/client";
@@ -12,6 +13,7 @@ import { blocks } from "./blocks";
12
13
  export interface LayoutSlotProps {
13
14
  groups: CmssyLayoutGroup[];
14
15
  position: CmssyRegion<typeof layout>;
16
+ page: CmssyBlockPage;
15
17
  locale: string;
16
18
  defaultLocale: string;
17
19
  enabledLocales: string[];
@@ -23,6 +25,7 @@ export interface LayoutSlotProps {
23
25
  export default function LayoutSlot({
24
26
  groups,
25
27
  position,
28
+ page,
26
29
  locale,
27
30
  defaultLocale,
28
31
  enabledLocales,
@@ -35,6 +38,7 @@ export default function LayoutSlot({
35
38
  <CmssyLazyLayout
36
39
  groups={groups}
37
40
  position={position}
41
+ page={page}
38
42
  locale={locale}
39
43
  defaultLocale={defaultLocale}
40
44
  enabledLocales={enabledLocales}
@@ -50,7 +54,14 @@ export default function LayoutSlot({
50
54
  if (!group) return null;
51
55
 
52
56
  const blockMap = buildBlockMap(blocks);
53
- const context = buildBlockContext(locale, defaultLocale, enabledLocales);
57
+ const context = buildBlockContext(
58
+ locale,
59
+ defaultLocale,
60
+ enabledLocales,
61
+ false,
62
+ undefined,
63
+ { page },
64
+ );
54
65
 
55
66
  return (
56
67
  <>
@@ -11,7 +11,7 @@ import LayoutSlot from "../cmssy/layout-slot";
11
11
 
12
12
  export const prerender = false;
13
13
 
14
- const { page, layouts, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
14
+ const { page, layouts, pageContext, locale, defaultLocale, enabledLocales } = await loadCmssyPage(
15
15
  cmssy,
16
16
  Astro.request,
17
17
  Astro.url,
@@ -33,6 +33,7 @@ const { data: blockData, content: blockContent } = await resolveEditorBlockData(
33
33
 
34
34
  const layoutSlot = {
35
35
  groups: layouts,
36
+ page: pageContext,
36
37
  blocks,
37
38
  locale,
38
39
  defaultLocale,
@@ -54,6 +55,7 @@ const [header, footer] = await Promise.all([
54
55
  <LayoutSlot
55
56
  groups={layouts}
56
57
  position="header"
58
+ page={pageContext}
57
59
  locale={locale}
58
60
  defaultLocale={defaultLocale}
59
61
  enabledLocales={enabledLocales}
@@ -77,6 +79,7 @@ const [header, footer] = await Promise.all([
77
79
  <LayoutSlot
78
80
  groups={layouts}
79
81
  position="footer"
82
+ page={pageContext}
80
83
  locale={locale}
81
84
  defaultLocale={defaultLocale}
82
85
  enabledLocales={enabledLocales}
@@ -10,6 +10,7 @@ export const prerender = false;
10
10
  const {
11
11
  page,
12
12
  layouts,
13
+ pageContext,
13
14
  locale,
14
15
  defaultLocale,
15
16
  enabledLocales,
@@ -29,6 +30,7 @@ const editorOrigin = resolveEditorOrigin(cmssy.editorOrigin);
29
30
  client:load
30
31
  groups={layouts}
31
32
  position="header"
33
+ page={pageContext}
32
34
  locale={locale}
33
35
  defaultLocale={defaultLocale}
34
36
  enabledLocales={enabledLocales}
@@ -48,6 +50,7 @@ const editorOrigin = resolveEditorOrigin(cmssy.editorOrigin);
48
50
  client:load
49
51
  groups={layouts}
50
52
  position="footer"
53
+ page={pageContext}
51
54
  locale={locale}
52
55
  defaultLocale={defaultLocale}
53
56
  enabledLocales={enabledLocales}
@@ -2,6 +2,7 @@ import {
2
2
  CmssyBlock,
3
3
  buildBlockContext,
4
4
  buildBlockMap,
5
+ type CmssyBlockPage,
5
6
  type CmssyLayoutGroup,
6
7
  } from "@cmssy/react";
7
8
  import { CmssyLazyLayout } from "@cmssy/react/client";
@@ -12,6 +13,7 @@ import { blocks } from "./blocks";
12
13
  export interface LayoutSlotProps {
13
14
  groups: CmssyLayoutGroup[];
14
15
  position: CmssyRegion<typeof layout>;
16
+ page: CmssyBlockPage;
15
17
  locale: string;
16
18
  defaultLocale: string;
17
19
  enabledLocales: string[];
@@ -23,6 +25,7 @@ export interface LayoutSlotProps {
23
25
  export function LayoutSlot({
24
26
  groups,
25
27
  position,
28
+ page,
26
29
  locale,
27
30
  defaultLocale,
28
31
  enabledLocales,
@@ -35,6 +38,7 @@ export function LayoutSlot({
35
38
  <CmssyLazyLayout
36
39
  groups={groups}
37
40
  position={position}
41
+ page={page}
38
42
  locale={locale}
39
43
  defaultLocale={defaultLocale}
40
44
  enabledLocales={enabledLocales}
@@ -50,7 +54,14 @@ export function LayoutSlot({
50
54
  if (!group) return null;
51
55
 
52
56
  const blockMap = buildBlockMap(blocks);
53
- const context = buildBlockContext(locale, defaultLocale, enabledLocales);
57
+ const context = buildBlockContext(
58
+ locale,
59
+ defaultLocale,
60
+ enabledLocales,
61
+ false,
62
+ undefined,
63
+ { page },
64
+ );
54
65
 
55
66
  return (
56
67
  <>
@@ -45,6 +45,7 @@ export async function loader(args: Route.LoaderArgs) {
45
45
 
46
46
  const slotOptions = {
47
47
  groups: data.layouts,
48
+ page: data.pageContext,
48
49
  blocks,
49
50
  locale: data.locale,
50
51
  defaultLocale: data.defaultLocale,
@@ -72,6 +73,7 @@ export default function CmssyPage({ loaderData }: Route.ComponentProps) {
72
73
  const {
73
74
  page,
74
75
  layouts,
76
+ pageContext,
75
77
  locale,
76
78
  defaultLocale,
77
79
  enabledLocales,
@@ -92,6 +94,7 @@ export default function CmssyPage({ loaderData }: Route.ComponentProps) {
92
94
  <LayoutSlot
93
95
  groups={layouts}
94
96
  position={position}
97
+ page={pageContext}
95
98
  locale={locale}
96
99
  defaultLocale={defaultLocale}
97
100
  enabledLocales={enabledLocales}
package/dist/index.js CHANGED
@@ -13,6 +13,10 @@ var BAD_TOKEN_HINT = {
13
13
  message: "the cmssy API rejected the token",
14
14
  fix: "create an API token in the cmssy dashboard (Settings \u2192 API Tokens) and pass it via --token or CMSSY_API_TOKEN"
15
15
  };
16
+ var NEEDS_MANIFEST_WRITE_HINT = {
17
+ message: "the token's user cannot write this workspace's block manifest",
18
+ fix: "the token's user needs the PAGES_EDIT permission in the selected workspace"
19
+ };
16
20
  var NEEDS_PAGES_EDIT_HINT = {
17
21
  message: "the token's user cannot read this workspace's settings",
18
22
  fix: "the token's user needs the PAGES_EDIT permission in the selected workspace"
@@ -68,8 +72,11 @@ async function adminGraphql(query, variables, options) {
68
72
  `the cmssy API rejected the request - ${errors.map((error) => error.message ?? "GraphQL error").join("; ")}`
69
73
  );
70
74
  }
71
- if (!response.ok || envelope?.data === void 0) {
72
- throw new CliError(`the cmssy API responded with HTTP ${response.status}`);
75
+ if (!response.ok || envelope?.data == null) {
76
+ throw new CliError(
77
+ `the cmssy API returned no data (HTTP ${response.status})`,
78
+ "check that CMSSY_API_URL points at the cmssy admin API (https://api.cmssy.io/graphql), not the delivery endpoint"
79
+ );
73
80
  }
74
81
  return envelope.data;
75
82
  }
@@ -95,6 +102,14 @@ var UPDATE_PREVIEW_URL_MUTATION = `mutation CliSetPreviewUrl($input: UpdateSiteC
95
102
  }
96
103
  }
97
104
  }`;
105
+ var SAVE_BLOCK_MANIFEST_MUTATION = `mutation CliSaveBlockManifest($blocks: JSON!, $regions: JSON) {
106
+ blockManifest {
107
+ save(blocks: $blocks, regions: $regions) {
108
+ hash
109
+ updatedAt
110
+ }
111
+ }
112
+ }`;
98
113
  async function fetchMyWorkspaces(options) {
99
114
  const data = await adminGraphql(WORKSPACES_MINE_QUERY, {}, options);
100
115
  return data.workspace.mine;
@@ -114,6 +129,31 @@ async function setPreviewUrl(previewUrl, options) {
114
129
  { ...options, denied: NEEDS_PAGES_EDIT_HINT }
115
130
  );
116
131
  }
132
+ var BLOCK_MANIFEST_HASH_QUERY = `query CliBlockManifestHash {
133
+ blockManifest {
134
+ get {
135
+ hash
136
+ }
137
+ }
138
+ }`;
139
+ async function fetchBlockManifestHash(options) {
140
+ const data = await adminGraphql(
141
+ BLOCK_MANIFEST_HASH_QUERY,
142
+ {},
143
+ {
144
+ ...options,
145
+ denied: NEEDS_MANIFEST_WRITE_HINT
146
+ }
147
+ );
148
+ return data.blockManifest.get?.hash ?? null;
149
+ }
150
+ async function saveBlockManifest(manifest, options) {
151
+ const data = await adminGraphql(SAVE_BLOCK_MANIFEST_MUTATION, manifest, {
152
+ ...options,
153
+ denied: NEEDS_MANIFEST_WRITE_HINT
154
+ });
155
+ return data.blockManifest.save;
156
+ }
117
157
 
118
158
  // src/format.ts
119
159
  var GREEN = "\x1B[32m";
@@ -601,6 +641,25 @@ function runAddBlock(options, deps) {
601
641
  }
602
642
  }
603
643
 
644
+ // src/args.ts
645
+ function flagValue(args, name) {
646
+ const index = args.findIndex((arg) => arg === name);
647
+ if (index !== -1) {
648
+ const value = args[index + 1];
649
+ if (value === void 0 || value.startsWith("--")) {
650
+ throw new CliError(
651
+ `${name} needs a value`,
652
+ `pass ${name} <value>, or ${name}=<value>`
653
+ );
654
+ }
655
+ return value;
656
+ }
657
+ return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
658
+ }
659
+ function hasFlag(args, name) {
660
+ return args.includes(name);
661
+ }
662
+
604
663
  // src/init.ts
605
664
  import {
606
665
  copyFileSync as copyFileSync2,
@@ -1464,8 +1523,345 @@ async function runLink(options, deps) {
1464
1523
  }
1465
1524
  }
1466
1525
 
1526
+ // src/sync-manifest.ts
1527
+ import { existsSync as existsSync7 } from "fs";
1528
+ import { isAbsolute, join as join8 } from "path";
1529
+ import {
1530
+ buildBlockManifest
1531
+ } from "@cmssy/core";
1532
+
1533
+ // src/site-modules.ts
1534
+ import { mkdirSync as mkdirSync3, rmSync, writeFileSync as writeFileSync5 } from "fs";
1535
+ import { basename, join as join7, resolve as resolve3 } from "path";
1536
+ import { pathToFileURL } from "url";
1537
+ import { build } from "esbuild";
1538
+ var RUNTIME_ONLY_MODULES = ["server-only", "client-only"];
1539
+ var ASSET_EXTENSIONS = [
1540
+ ".module.css",
1541
+ ".module.scss",
1542
+ ".module.sass",
1543
+ ".module.less",
1544
+ ".css",
1545
+ ".scss",
1546
+ ".sass",
1547
+ ".less",
1548
+ ".png",
1549
+ ".jpg",
1550
+ ".jpeg",
1551
+ ".gif",
1552
+ ".svg",
1553
+ ".webp",
1554
+ ".avif",
1555
+ ".ico",
1556
+ ".woff",
1557
+ ".woff2",
1558
+ ".ttf",
1559
+ ".otf",
1560
+ ".mp4",
1561
+ ".webm",
1562
+ ".mp3",
1563
+ ".md",
1564
+ ".mdx"
1565
+ ];
1566
+ var assetLoaders = Object.fromEntries(
1567
+ ASSET_EXTENSIONS.map((extension) => [extension, "empty"])
1568
+ );
1569
+ var runtimeOnlyModules = {
1570
+ name: "cmssy-runtime-only-modules",
1571
+ setup(api) {
1572
+ const filter = new RegExp(`^(${RUNTIME_ONLY_MODULES.join("|")})$`);
1573
+ api.onResolve({ filter }, (args) => ({
1574
+ path: args.path,
1575
+ namespace: "cmssy-empty"
1576
+ }));
1577
+ api.onLoad({ filter: /.*/, namespace: "cmssy-empty" }, () => ({
1578
+ contents: "export {};",
1579
+ loader: "js"
1580
+ }));
1581
+ }
1582
+ };
1583
+ async function loadSiteModule(cwd, entry) {
1584
+ const entryPath = resolve3(cwd, entry);
1585
+ let code;
1586
+ try {
1587
+ const result = await build({
1588
+ absWorkingDir: cwd,
1589
+ entryPoints: [entryPath],
1590
+ bundle: true,
1591
+ platform: "node",
1592
+ format: "esm",
1593
+ target: "node20",
1594
+ write: false,
1595
+ packages: "external",
1596
+ jsx: "automatic",
1597
+ loader: assetLoaders,
1598
+ define: { "import.meta.env": "process.env" },
1599
+ plugins: [runtimeOnlyModules],
1600
+ logLevel: "silent"
1601
+ });
1602
+ code = result.outputFiles[0]?.text ?? "";
1603
+ } catch (error) {
1604
+ const messages = error && typeof error === "object" && "errors" in error ? error.errors.map(
1605
+ (issue) => issue.location ? `${issue.location.file}:${issue.location.line} ${issue.text}` : issue.text
1606
+ ) : [error instanceof Error ? error.message : String(error)];
1607
+ throw new CliError(`could not compile ${entry}`, messages.join("\n "));
1608
+ }
1609
+ const dir = join7(cwd, "node_modules", ".cache", "cmssy");
1610
+ const file = join7(
1611
+ dir,
1612
+ `${basename(entry).replace(/\.[^.]+$/, "")}.${process.pid}.${Date.now()}.mjs`
1613
+ );
1614
+ try {
1615
+ mkdirSync3(dir, { recursive: true });
1616
+ writeFileSync5(file, code);
1617
+ } catch (error) {
1618
+ throw new CliError(
1619
+ `could not write the compiled ${entry} to ${dir}`,
1620
+ error instanceof Error ? error.message : String(error)
1621
+ );
1622
+ }
1623
+ try {
1624
+ return await import(pathToFileURL(file).href);
1625
+ } catch (error) {
1626
+ throw new CliError(
1627
+ `could not load ${entry}`,
1628
+ error instanceof Error ? error.message : String(error)
1629
+ );
1630
+ } finally {
1631
+ rmSync(file, { force: true });
1632
+ }
1633
+ }
1634
+
1635
+ // src/sync-manifest.ts
1636
+ var BLOCKS_CANDIDATES = [
1637
+ "cmssy/blocks.ts",
1638
+ "cmssy/blocks.tsx",
1639
+ "src/cmssy/blocks.ts",
1640
+ "src/cmssy/blocks.tsx",
1641
+ "app/cmssy/blocks.ts",
1642
+ "app/cmssy/blocks.tsx"
1643
+ ];
1644
+ var CONFIG_CANDIDATES = [
1645
+ "cmssy.config.ts",
1646
+ "cmssy.config.mts",
1647
+ "src/cmssy.config.ts",
1648
+ "src/cmssy.config.mts",
1649
+ "app/cmssy.config.ts"
1650
+ ];
1651
+ var SYNC_MANIFEST_USAGE = [
1652
+ " cmssy sync-manifest [--blocks <path>] [--config <path>] [--token <cs_...>]",
1653
+ " [--org <slug>] [--workspace <slug>] [--dry-run] [--help]"
1654
+ ];
1655
+ function findModule(cwd, requested, candidates, flag) {
1656
+ if (requested?.trim()) {
1657
+ const path = requested.trim();
1658
+ if (!existsSync7(isAbsolute(path) ? path : join8(cwd, path))) {
1659
+ throw new CliError(`${path} does not exist`, `check the ${flag} path`);
1660
+ }
1661
+ return path;
1662
+ }
1663
+ const found = candidates.find(
1664
+ (candidate) => existsSync7(join8(cwd, candidate))
1665
+ );
1666
+ if (!found) {
1667
+ throw new CliError(
1668
+ `no ${candidates[0]} found in ${cwd}`,
1669
+ `looked for ${candidates.join(", ")} - pass ${flag} <path> to name the file`
1670
+ );
1671
+ }
1672
+ return found;
1673
+ }
1674
+ function isBlockSource(value) {
1675
+ return !!value && typeof value === "object" && typeof value.type === "string" && !!value.props && typeof value.props === "object";
1676
+ }
1677
+ function readBlocks(module, path) {
1678
+ const exported = module.blocks ?? module.default;
1679
+ if (!Array.isArray(exported)) {
1680
+ throw new CliError(
1681
+ `${path} does not export a \`blocks\` array`,
1682
+ "export const blocks = [heroBlock, ...] - the same list the app renders from"
1683
+ );
1684
+ }
1685
+ const invalid = exported.findIndex((block) => !isBlockSource(block));
1686
+ if (invalid !== -1) {
1687
+ throw new CliError(
1688
+ `${path}: blocks[${invalid}] is not a block definition`,
1689
+ "every entry must come from defineBlock({ type, props, component })"
1690
+ );
1691
+ }
1692
+ const category = typeof module.category === "string" ? module.category : void 0;
1693
+ return { blocks: exported, category };
1694
+ }
1695
+ function isLayout(value) {
1696
+ return !!value && typeof value === "object" && Array.isArray(value.regions);
1697
+ }
1698
+ function readConfig(module, path) {
1699
+ const cmssy = module.cmssy;
1700
+ const site = cmssy && typeof cmssy === "object" ? cmssy : null;
1701
+ const layout = isLayout(module.layout) ? module.layout : isLayout(site?.layout) ? site.layout : null;
1702
+ if (!layout && module.layout !== void 0) {
1703
+ throw new CliError(
1704
+ `${path} exports \`layout\`, but it is not a defineCmssyLayout() result`,
1705
+ "export const layout = defineCmssyLayout({ regions: [...] })"
1706
+ );
1707
+ }
1708
+ return {
1709
+ layout,
1710
+ org: typeof site?.org === "string" ? site.org : void 0,
1711
+ workspaceSlug: typeof site?.workspaceSlug === "string" ? site.workspaceSlug : void 0
1712
+ };
1713
+ }
1714
+ function resolveSlug(flag, fromConfig, fromEnv, variable, flagName) {
1715
+ const value = flag?.trim() || fromConfig?.trim() || fromEnv?.trim();
1716
+ if (!value) {
1717
+ throw new CliError(
1718
+ `no workspace to push to: ${variable} is not set`,
1719
+ `run cmssy link first, or pass ${flagName} <slug>`
1720
+ );
1721
+ }
1722
+ return value;
1723
+ }
1724
+ function resolveToken2(options, env) {
1725
+ const token = options.token?.trim() || env.CMSSY_API_TOKEN?.trim();
1726
+ if (!token) {
1727
+ throw new CliError(
1728
+ "no API token given",
1729
+ "create an API token in the cmssy dashboard (Settings \u2192 API Tokens) and set CMSSY_API_TOKEN in the deploy environment, or pass --token cs_..."
1730
+ );
1731
+ }
1732
+ return token;
1733
+ }
1734
+ function matchWorkspace(workspaces, org, slug) {
1735
+ const match = workspaces.find(
1736
+ (workspace) => workspace.slug === slug && (workspace.organizationSlug ?? null) === org
1737
+ );
1738
+ if (!match) {
1739
+ const known = workspaces.map(
1740
+ (workspace) => `${workspace.organizationSlug ?? "?"}/${workspace.slug}`
1741
+ ).join(", ");
1742
+ throw new CliError(
1743
+ `the token's user is not a member of ${org}/${slug}`,
1744
+ known ? `the token can reach: ${known} - check CMSSY_ORG_SLUG and CMSSY_WORKSPACE_SLUG` : "the token's user belongs to no workspace - check the token"
1745
+ );
1746
+ }
1747
+ return match;
1748
+ }
1749
+ function describe(manifest) {
1750
+ const lines = [
1751
+ ` blocks: ${manifest.blocks.map((block) => block.type).join(", ") || "-"}`
1752
+ ];
1753
+ if (!manifest.regions) {
1754
+ lines.push(" regions: none declared - the stored regions are kept");
1755
+ return lines;
1756
+ }
1757
+ const regions = manifest.regions.map((region) => {
1758
+ const settings = region.settings ? Object.keys(region.settings) : [];
1759
+ return settings.length ? `${region.id} (${settings.join(", ")})` : region.id;
1760
+ });
1761
+ lines.push(` regions: ${regions.join(", ") || "-"}`);
1762
+ return lines;
1763
+ }
1764
+ async function collectManifest(options, deps) {
1765
+ const load = deps.load ?? loadSiteModule;
1766
+ const blocksPath = findModule(
1767
+ deps.cwd,
1768
+ options.blocks,
1769
+ BLOCKS_CANDIDATES,
1770
+ "--blocks"
1771
+ );
1772
+ const configPath = findModule(
1773
+ deps.cwd,
1774
+ options.config,
1775
+ CONFIG_CANDIDATES,
1776
+ "--config"
1777
+ );
1778
+ const { blocks, category } = readBlocks(
1779
+ await load(deps.cwd, blocksPath),
1780
+ blocksPath
1781
+ );
1782
+ const config = readConfig(await load(deps.cwd, configPath), configPath);
1783
+ if (blocks.length === 0) {
1784
+ throw new CliError(
1785
+ `${blocksPath} exports an empty \`blocks\` array`,
1786
+ "register at least one block - cmssy add block <name>"
1787
+ );
1788
+ }
1789
+ const manifest = buildBlockManifest(blocks, {
1790
+ category,
1791
+ regions: config.layout?.regions
1792
+ });
1793
+ return {
1794
+ manifest,
1795
+ blocksPath,
1796
+ configPath,
1797
+ org: config.org,
1798
+ workspaceSlug: config.workspaceSlug
1799
+ };
1800
+ }
1801
+ async function runSyncManifest(options, deps) {
1802
+ if (options.help) {
1803
+ deps.log("usage:");
1804
+ for (const line of SYNC_MANIFEST_USAGE) deps.log(line);
1805
+ return 0;
1806
+ }
1807
+ try {
1808
+ loadEnvFiles(deps.cwd, deps.env);
1809
+ const collected = await collectManifest(options, deps);
1810
+ const { manifest } = collected;
1811
+ if (options.dryRun) {
1812
+ deps.log(JSON.stringify(manifest, null, 2));
1813
+ return 0;
1814
+ }
1815
+ const token = resolveToken2(options, deps.env);
1816
+ const org = resolveSlug(
1817
+ options.org,
1818
+ collected.org,
1819
+ deps.env.CMSSY_ORG_SLUG,
1820
+ "CMSSY_ORG_SLUG",
1821
+ "--org"
1822
+ );
1823
+ const slug = resolveSlug(
1824
+ options.workspace,
1825
+ collected.workspaceSlug,
1826
+ deps.env.CMSSY_WORKSPACE_SLUG,
1827
+ "CMSSY_WORKSPACE_SLUG",
1828
+ "--workspace"
1829
+ );
1830
+ const request2 = {
1831
+ token,
1832
+ apiUrl: deps.env.CMSSY_API_URL,
1833
+ fetch: deps.fetch
1834
+ };
1835
+ const workspace = matchWorkspace(
1836
+ await fetchMyWorkspaces(request2),
1837
+ org,
1838
+ slug
1839
+ );
1840
+ const scoped = { ...request2, workspaceId: workspace.id };
1841
+ const before = await fetchBlockManifestHash(scoped);
1842
+ const saved = await saveBlockManifest(manifest, scoped);
1843
+ const unchanged = before === saved.hash;
1844
+ const count = `${manifest.blocks.length} block${manifest.blocks.length === 1 ? "" : "s"}${manifest.regions ? ` and ${manifest.regions.length} region${manifest.regions.length === 1 ? "" : "s"}` : ""}`;
1845
+ deps.log(
1846
+ unchanged ? `cmssy: ${org}/${slug} already has this manifest - ${count} unchanged (${collected.blocksPath}, ${collected.configPath})` : `cmssy: pushed ${count} to ${org}/${slug} (${collected.blocksPath}, ${collected.configPath})`
1847
+ );
1848
+ for (const line of describe(manifest)) deps.log(line);
1849
+ deps.log(
1850
+ unchanged ? ` manifest ${saved.hash.slice(0, 12)} - last updated ${saved.updatedAt}` : ` manifest ${saved.hash.slice(0, 12)} - updated ${saved.updatedAt}`
1851
+ );
1852
+ return 0;
1853
+ } catch (error) {
1854
+ if (error instanceof CliError) {
1855
+ deps.log(`cmssy: ${error.message}`);
1856
+ if (error.fix) deps.log(` ${error.fix}`);
1857
+ return 1;
1858
+ }
1859
+ throw error;
1860
+ }
1861
+ }
1862
+
1467
1863
  // src/types-command.ts
1468
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync7, readdirSync, writeFileSync as writeFileSync5 } from "fs";
1864
+ import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync, writeFileSync as writeFileSync6 } from "fs";
1469
1865
  import { dirname as dirname3, relative, resolve as resolvePath } from "path";
1470
1866
  import { DEFAULT_CMSSY_API_URL } from "@cmssy/core";
1471
1867
 
@@ -1774,7 +2170,7 @@ function describeDrift(previous, next) {
1774
2170
  report("fields", declared(body(previous), fields), declared(body(next), fields));
1775
2171
  return lines.length ? lines : ["the generated output differs"];
1776
2172
  }
1777
- function resolve3(value, fromEnv, variable) {
2173
+ function resolve4(value, fromEnv, variable) {
1778
2174
  const resolved = value?.trim() || fromEnv?.trim();
1779
2175
  if (!resolved) {
1780
2176
  throw new CliError(
@@ -1856,8 +2252,8 @@ function syncOperations(options, deps) {
1856
2252
  return 1;
1857
2253
  }
1858
2254
  try {
1859
- mkdirSync3(dirname3(outPath), { recursive: true });
1860
- writeFileSync5(outPath, source);
2255
+ mkdirSync4(dirname3(outPath), { recursive: true });
2256
+ writeFileSync6(outPath, source);
1861
2257
  } catch (error) {
1862
2258
  throw new CliError(
1863
2259
  `could not write ${shown}: ${error.message}`,
@@ -1874,8 +2270,8 @@ async function runTypes(options, deps) {
1874
2270
  try {
1875
2271
  loadEnvFiles(deps.cwd, deps.env);
1876
2272
  operationsStatus = syncOperations(options, deps);
1877
- const org = resolve3(options.org, deps.env.CMSSY_ORG_SLUG, "CMSSY_ORG_SLUG");
1878
- const workspace = resolve3(
2273
+ const org = resolve4(options.org, deps.env.CMSSY_ORG_SLUG, "CMSSY_ORG_SLUG");
2274
+ const workspace = resolve4(
1879
2275
  options.workspace,
1880
2276
  deps.env.CMSSY_WORKSPACE_SLUG,
1881
2277
  "CMSSY_WORKSPACE_SLUG"
@@ -1922,8 +2318,8 @@ async function runTypes(options, deps) {
1922
2318
  deps.log(" run `cmssy types` and commit the result");
1923
2319
  return 1;
1924
2320
  }
1925
- mkdirSync3(dirname3(outPath), { recursive: true });
1926
- writeFileSync5(outPath, source);
2321
+ mkdirSync4(dirname3(outPath), { recursive: true });
2322
+ writeFileSync6(outPath, source);
1927
2323
  const fieldCount = models.reduce(
1928
2324
  (total, model) => total + model.fields.length,
1929
2325
  0
@@ -1952,16 +2348,9 @@ var USAGE = [
1952
2348
  " cmssy add block <name> [--dir <path>]",
1953
2349
  " cmssy link [--token <cs_...>] [--workspace <slug>] [--preview-url <url>]",
1954
2350
  " cmssy types [--out <path>] [--operations-out <path>] [--no-operations]",
1955
- " [--check] [--org <slug>] [--workspace <slug>]"
2351
+ " [--check] [--org <slug>] [--workspace <slug>]",
2352
+ ...SYNC_MANIFEST_USAGE
1956
2353
  ].join("\n");
1957
- function flagValue(args, name) {
1958
- const index = args.findIndex((arg) => arg === name);
1959
- if (index !== -1) return args[index + 1];
1960
- return args.find((arg) => arg.startsWith(`${name}=`))?.slice(name.length + 1);
1961
- }
1962
- function hasFlag(args, name) {
1963
- return args.includes(name);
1964
- }
1965
2354
  async function runLinkCommand(args) {
1966
2355
  const rl = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : null;
1967
2356
  try {
@@ -2029,6 +2418,26 @@ async function main() {
2029
2418
  );
2030
2419
  return;
2031
2420
  }
2421
+ if (command === "sync-manifest") {
2422
+ process.exitCode = await runSyncManifest(
2423
+ {
2424
+ help: hasFlag(args, "--help"),
2425
+ blocks: flagValue(args, "--blocks"),
2426
+ config: flagValue(args, "--config"),
2427
+ token: flagValue(args, "--token"),
2428
+ org: flagValue(args, "--org"),
2429
+ workspace: flagValue(args, "--workspace"),
2430
+ dryRun: hasFlag(args, "--dry-run")
2431
+ },
2432
+ {
2433
+ cwd: process.cwd(),
2434
+ env: process.env,
2435
+ log: (line) => console.log(line),
2436
+ fetch: globalThis.fetch
2437
+ }
2438
+ );
2439
+ return;
2440
+ }
2032
2441
  console.error(USAGE);
2033
2442
  process.exitCode = command === void 0 || command === "--help" ? 0 : 1;
2034
2443
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cmssy/cli",
3
- "version": "14.0.0",
3
+ "version": "14.2.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,8 @@
27
27
  "assets"
28
28
  ],
29
29
  "dependencies": {
30
- "@cmssy/core": "14.0.0"
30
+ "esbuild": "^0.27.7",
31
+ "@cmssy/core": "14.2.0"
31
32
  },
32
33
  "devDependencies": {
33
34
  "@types/node": "^20.0.0",