@graph8/sdk 0.358.0 → 0.362.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 (110) hide show
  1. package/blocks/_foundation/README.md +108 -0
  2. package/blocks/_foundation/block.json +160 -12
  3. package/blocks/_foundation/components/api-error.tsx +62 -18
  4. package/blocks/_foundation/components/empty-state.tsx +39 -7
  5. package/blocks/_foundation/components/screen/bar-list.tsx +71 -0
  6. package/blocks/_foundation/components/screen/detail-panel.tsx +114 -0
  7. package/blocks/_foundation/components/screen/entity-avatar.tsx +56 -0
  8. package/blocks/_foundation/components/screen/form-sheet.tsx +73 -0
  9. package/blocks/_foundation/components/screen/index.ts +21 -0
  10. package/blocks/_foundation/components/screen/key-value-list.tsx +65 -0
  11. package/blocks/_foundation/components/screen/screen-layout.tsx +124 -0
  12. package/blocks/_foundation/components/screen/section-card.tsx +47 -0
  13. package/blocks/_foundation/components/screen/sparkline.tsx +77 -0
  14. package/blocks/_foundation/components/screen/split-view.tsx +125 -0
  15. package/blocks/_foundation/components/screen/stat-strip.tsx +121 -0
  16. package/blocks/_foundation/components/screen/status-pill.tsx +104 -0
  17. package/blocks/_foundation/components/screen/time-series-chart.tsx +114 -0
  18. package/blocks/_foundation/components/screen/toolbar.tsx +144 -0
  19. package/blocks/_foundation/hooks/use-url-state.ts +74 -0
  20. package/blocks/appointments/README.md +21 -1
  21. package/blocks/appointments/block.json +39 -2
  22. package/blocks/appointments/components/event-type-related.tsx +75 -0
  23. package/blocks/appointments/components/meeting-list.tsx +8 -4
  24. package/blocks/appointments/components/meetings-screen.tsx +154 -0
  25. package/blocks/appointments/components/scheduling-screen.tsx +121 -0
  26. package/blocks/appointments/hooks/use-event-types.ts +17 -0
  27. package/blocks/block.types.ts +9 -0
  28. package/blocks/deals/README.md +34 -3
  29. package/blocks/deals/block.json +39 -4
  30. package/blocks/deals/components/deal-detail.tsx +4 -2
  31. package/blocks/deals/components/deal-related.tsx +87 -0
  32. package/blocks/deals/components/deals-screen.tsx +293 -0
  33. package/blocks/deals/components/pipeline-board.tsx +4 -2
  34. package/blocks/deals/hooks/use-default-owner.ts +60 -0
  35. package/blocks/deals/lib/deals-types.ts +14 -0
  36. package/blocks/forms/README.md +12 -0
  37. package/blocks/forms/block.json +9 -1
  38. package/blocks/forms/components/forms-screen.tsx +350 -0
  39. package/blocks/forms/components/submissions-table.tsx +13 -5
  40. package/blocks/lifecycle/README.md +16 -0
  41. package/blocks/lifecycle/block.json +23 -1
  42. package/blocks/lifecycle/components/account-related.tsx +66 -0
  43. package/blocks/lifecycle/components/lifecycle-screen.tsx +147 -0
  44. package/blocks/newsletter/README.md +13 -0
  45. package/blocks/newsletter/block.json +9 -1
  46. package/blocks/newsletter/components/newsletter-screen.tsx +402 -0
  47. package/blocks/newsletter/components/subscriber-table.tsx +12 -6
  48. package/blocks/nurture/README.md +12 -0
  49. package/blocks/nurture/block.json +9 -1
  50. package/blocks/nurture/components/nurture-screen.tsx +279 -0
  51. package/blocks/pages/README.md +12 -0
  52. package/blocks/pages/block.json +9 -1
  53. package/blocks/pages/components/content-library.tsx +37 -7
  54. package/blocks/pages/components/page-list.tsx +4 -2
  55. package/blocks/pages/components/pages-screen.tsx +154 -0
  56. package/blocks/quotes/README.md +17 -0
  57. package/blocks/quotes/block.json +26 -2
  58. package/blocks/quotes/components/quote-list.tsx +7 -3
  59. package/blocks/quotes/components/quote-related.tsx +120 -0
  60. package/blocks/quotes/components/quotes-screen.tsx +169 -0
  61. package/dist/cli.js +197 -37
  62. package/dist/index.browser.js +1 -1
  63. package/dist/index.browser.js.map +1 -1
  64. package/dist/index.browser.mjs +1 -1
  65. package/dist/index.browser.mjs.map +1 -1
  66. package/dist/index.d.mts +93 -7
  67. package/dist/index.d.ts +93 -7
  68. package/dist/index.js +1 -1
  69. package/dist/index.js.map +1 -1
  70. package/dist/index.mjs +1 -1
  71. package/dist/index.mjs.map +1 -1
  72. package/dist/react.browser.js +1 -1
  73. package/dist/react.browser.js.map +1 -1
  74. package/dist/react.browser.mjs +1 -1
  75. package/dist/react.browser.mjs.map +1 -1
  76. package/dist/react.d.mts +90 -4
  77. package/dist/react.d.ts +90 -4
  78. package/dist/react.js +1 -1
  79. package/dist/react.js.map +1 -1
  80. package/dist/react.mjs +1 -1
  81. package/dist/react.mjs.map +1 -1
  82. package/dist/types/blocks/create.d.ts +15 -1
  83. package/dist/types/blocks/doctor.d.ts +2 -1
  84. package/dist/types/blocks/flows.d.ts +20 -0
  85. package/dist/types/blocks/registry.d.ts +15 -0
  86. package/dist/types/core.d.ts +3 -2
  87. package/dist/types/react.d.ts +2 -0
  88. package/dist/types/studio.d.ts +48 -0
  89. package/dist/types/version.d.ts +2 -2
  90. package/docs/api/adCampaigns.d.ts +4 -0
  91. package/docs/api/adCampaigns.md +4 -0
  92. package/docs/api/gtmCampaigns.d.ts +4 -0
  93. package/docs/api/gtmCampaigns.md +2 -0
  94. package/docs/api/gtmContext.d.ts +5 -1
  95. package/docs/api/gtmContext.md +15 -7
  96. package/docs/api/integrations.d.ts +29 -4
  97. package/docs/api/integrations.md +14 -0
  98. package/docs/sdk-clients.md +1 -1
  99. package/package.json +1 -1
  100. package/stacks/stack.schema.json +6 -1
  101. package/styles/styles.css +1 -1
  102. package/templates/next-app/README.md.tmpl +16 -0
  103. package/templates/next-app/app/globals.css.tmpl +105 -0
  104. package/templates/next-app/app/layout.tsx.tmpl +11 -2
  105. package/templates/next-app/components/app-shell.tsx.tmpl +238 -23
  106. package/templates/next-app/components/shell/connection-status.tsx.tmpl +109 -0
  107. package/templates/next-app/components/shell/nav-model.ts.tmpl +65 -0
  108. package/templates/next-app/components/shell/quick-jump.tsx.tmpl +131 -0
  109. package/templates/next-app/components/shell/theme-toggle.tsx.tmpl +40 -0
  110. package/templates/next-app/lib/nav.ts.tmpl +1 -1
package/dist/cli.js CHANGED
@@ -36,8 +36,8 @@ __export(cli_exports, {
36
36
  });
37
37
  module.exports = __toCommonJS(cli_exports);
38
38
  var import_node_child_process = require("child_process");
39
- var import_node_fs5 = __toESM(require("fs"));
40
- var import_node_path5 = __toESM(require("path"));
39
+ var import_node_fs6 = __toESM(require("fs"));
40
+ var import_node_path6 = __toESM(require("path"));
41
41
 
42
42
  // src/agentSetup.ts
43
43
  var ALL_TARGETS = ["claude", "codex", "cursor"];
@@ -119,9 +119,9 @@ ${section}
119
119
  }
120
120
  function planAgentSetup(targets, read, catalog) {
121
121
  const plans = [];
122
- const add = (target, path6, next) => {
123
- const current = read(path6);
124
- plans.push({ target, path: path6, content: next, action: current === null ? "create" : current === next ? "unchanged" : "update" });
122
+ const add = (target, path7, next) => {
123
+ const current = read(path7);
124
+ plans.push({ target, path: path7, content: next, action: current === null ? "create" : current === next ? "unchanged" : "update" });
125
125
  };
126
126
  for (const target of targets) {
127
127
  if (target === "claude") add(target, ".claude/skills/graph8-sdk/SKILL.md", claudeSkill(catalog));
@@ -158,7 +158,7 @@ var import_node_fs2 = __toESM(require("fs"));
158
158
  var import_node_path2 = __toESM(require("path"));
159
159
 
160
160
  // src/version.ts
161
- var SDK_VERSION = "0.358.0";
161
+ var SDK_VERSION = "0.362.0";
162
162
  var SDK_CLIENT_ID = `js-sdk/${SDK_VERSION}`;
163
163
 
164
164
  // src/blocks/registry.ts
@@ -220,11 +220,12 @@ function loadRegistry(blocksRoot) {
220
220
  summary: typeof raw.summary === "string" ? raw.summary : "",
221
221
  status: raw.status === "planned" ? "planned" : "available",
222
222
  blocks: arr(raw.blocks),
223
- nav: arr(raw.nav)
223
+ nav: arr(raw.nav),
224
+ ...typeof raw.home === "string" ? { home: raw.home } : {}
224
225
  });
225
226
  }
226
227
  }
227
- return { blocks, stacks };
228
+ return { blocks, stacks, stacksRoot: stacksDir };
228
229
  }
229
230
  function findBlock(reg, id) {
230
231
  return reg.blocks.find((b) => b.id === id);
@@ -262,8 +263,19 @@ function searchStacks(reg, text) {
262
263
  if (!needle) return [];
263
264
  return reg.stacks.filter((s) => [s.id, s.title, s.summary].some((x) => x.toLowerCase().includes(needle)));
264
265
  }
266
+ function screenNames(b) {
267
+ return b.components.map((c) => c.name).filter((n) => n.endsWith("Screen"));
268
+ }
265
269
  function blockSummary(b) {
266
- return { id: b.id, title: b.title, summary: b.summary, dependsOn: b.dependsOn, components: b.components.map((c) => c.name) };
270
+ const screens = screenNames(b);
271
+ return {
272
+ id: b.id,
273
+ title: b.title,
274
+ summary: b.summary,
275
+ dependsOn: b.dependsOn,
276
+ components: b.components.map((c) => c.name),
277
+ ...screens.length ? { screens } : {}
278
+ };
267
279
  }
268
280
  function stackStatus(reg, s) {
269
281
  const missing = s.blocks.filter((id) => !findBlock(reg, id));
@@ -280,7 +292,8 @@ function catalogSection(reg) {
280
292
  const lines = ["### graph8 blocks", "", "Ready-made, copy-in UI + hooks per graph8 feature. Pick from this list instead of reading every README:", ""];
281
293
  for (const b of reg.blocks) {
282
294
  const deps = b.dependsOn.length ? ` (needs ${b.dependsOn.join(", ")})` : "";
283
- lines.push(`- block \`${b.id}\`: ${b.summary}${deps}`);
295
+ const screens = screenNames(b);
296
+ lines.push(`- block \`${b.id}\`: ${b.summary}${deps}${screens.length ? ` Screens (start here): ${screens.join(", ")}.` : ""}`);
284
297
  }
285
298
  for (const s of reg.stacks) {
286
299
  const { missing } = stackStatus(reg, s);
@@ -715,6 +728,8 @@ function stackProblem(reg, stack) {
715
728
  ].join("\n");
716
729
  }
717
730
  const seen = /* @__PURE__ */ new Set();
731
+ const homeIssue = homeProblem(stack);
732
+ if (homeIssue) return homeIssue;
718
733
  for (const n of stack.nav) {
719
734
  const block = findBlock(reg, n.block);
720
735
  if (!stack.blocks.includes(n.block)) return `stack "${stack.id}" nav "${n.label}" uses block "${n.block}" which the stack does not include.`;
@@ -722,31 +737,77 @@ function stackProblem(reg, stack) {
722
737
  if (/[\[\]]/.test(n.path)) return `stack "${stack.id}" nav "${n.label}" uses ${n.path}: dynamic segments cannot be a sidebar link.`;
723
738
  if (seen.has(n.path)) return `stack "${stack.id}" has two nav entries at ${n.path}.`;
724
739
  seen.add(n.path);
740
+ if (n.page && (!reg.stacksRoot || !import_node_fs3.default.existsSync(import_node_path3.default.join(reg.stacksRoot, stack.id, n.page)))) {
741
+ return `stack "${stack.id}" nav "${n.label}" uses page ${n.page}, which does not exist under stacks/${stack.id}/.`;
742
+ }
725
743
  }
726
744
  return null;
727
745
  }
746
+ function homeProblem(stack) {
747
+ if (!stack.home) return null;
748
+ if (!stack.nav.some((n) => n.path === stack.home)) return `stack "${stack.id}" has home ${stack.home}, which is not one of its nav paths.`;
749
+ if (stack.home !== "/" && stack.nav.some((n) => n.path === "/")) return `stack "${stack.id}" has home ${stack.home} but also a nav entry at "/": drop one.`;
750
+ return null;
751
+ }
752
+ function stackPageFile(reg, stack, n) {
753
+ return n.page && reg.stacksRoot ? import_node_path3.default.join(reg.stacksRoot, stack.id, n.page) : null;
754
+ }
755
+ function homeRedirect(stack) {
756
+ if (stack.home) return stack.home === "/" ? null : stack.home;
757
+ return stack.nav.length && !stack.nav.some((n) => n.path === "/") ? stack.nav[0].path : null;
758
+ }
728
759
  function renderPages(reg, stack, templates) {
729
760
  const pageTpl = import_node_fs3.default.readFileSync(import_node_path3.default.join(templates, "_pages", "route-page.tsx.tmpl"), "utf8");
730
761
  const files = stack.nav.map((n) => {
731
762
  const block = findBlock(reg, n.block);
732
763
  const comp = block.components.find((c) => c.name === n.component);
733
- const content = render(pageTpl, {
764
+ const custom = stackPageFile(reg, stack, n);
765
+ const vars = {
734
766
  stackId: stack.id,
735
767
  block: block.id,
736
768
  blockDir: `components/g8/${block.dir}`,
737
769
  component: n.component,
738
770
  importPath: componentImportPath(block, comp.file),
739
771
  propsJson: JSON.stringify(n.props ?? {}, null, 2)
740
- });
741
- return { rel: pageFileFor(n.path), content };
772
+ };
773
+ return { rel: pageFileFor(n.path), content: render(custom ? import_node_fs3.default.readFileSync(custom, "utf8") : pageTpl, vars) };
742
774
  });
743
- if (stack.nav.length && !stack.nav.some((n) => n.path === "/")) {
775
+ const to = homeRedirect(stack);
776
+ if (to) {
744
777
  const redirect = import_node_fs3.default.readFileSync(import_node_path3.default.join(templates, "_pages", "redirect-page.tsx.tmpl"), "utf8");
745
- files.push({ rel: "app/page.tsx", content: render(redirect, { firstPathJson: JSON.stringify(stack.nav[0].path) }) });
778
+ files.push({ rel: "app/page.tsx", content: render(redirect, { firstPathJson: JSON.stringify(to) }) });
746
779
  }
747
780
  return files;
748
781
  }
749
- function peerDependenciesFor(blocks, io) {
782
+ function navUsesExtras(stack) {
783
+ return stack.nav.some((n) => n.icon || n.group || n.description);
784
+ }
785
+ function navParts(stack) {
786
+ if (!navUsesExtras(stack)) {
787
+ return { navImports: "", navType: "{ label: string; path: string }[]", navJson: JSON.stringify(stack.nav.map((n) => ({ label: n.label, path: n.path })), null, 2) };
788
+ }
789
+ const icons = [...new Set(stack.nav.flatMap((n) => n.icon ? [n.icon] : []))].sort();
790
+ const named = [...icons, "type LucideIcon"].join(", ");
791
+ const items = stack.nav.map((n) => ({
792
+ label: n.label,
793
+ path: n.path,
794
+ ...n.icon ? { icon: `__ICON__${n.icon}` } : {},
795
+ ...n.group ? { group: n.group } : {},
796
+ ...n.description ? { description: n.description } : {}
797
+ }));
798
+ return {
799
+ navImports: [
800
+ `import { ${named} } from "lucide-react";`,
801
+ "",
802
+ "export type NavItem = { label: string; path: string; icon?: LucideIcon; group?: string; description?: string };",
803
+ "",
804
+ ""
805
+ ].join("\n"),
806
+ navType: "NavItem[]",
807
+ navJson: JSON.stringify(items, null, 2).replace(/"__ICON__(\w+)"/g, "$1")
808
+ };
809
+ }
810
+ function peerDependenciesFor(blocks, io, stack) {
750
811
  const peers = {};
751
812
  for (const b of blocks) for (const [name, range] of Object.entries(b.peerDependencies)) peers[name] ??= range;
752
813
  const usesUi = blocks.some((b) => walk(b.root).some((f) => /\.(tsx?|jsx?)$/.test(f) && /["']@graph8\/sdk\/ui["'/]/.test(import_node_fs3.default.readFileSync(import_node_path3.default.join(b.root, f), "utf8"))));
@@ -754,8 +815,17 @@ function peerDependenciesFor(blocks, io) {
754
815
  const sdk = JSON.parse(import_node_fs3.default.readFileSync(io.sdkPackageJson, "utf8"));
755
816
  for (const [name, range] of Object.entries(sdk.peerDependencies ?? {})) peers[name] ??= range;
756
817
  }
818
+ if (navUsesExtras(stack)) peers["lucide-react"] ??= lucideRange(io);
757
819
  return peers;
758
820
  }
821
+ function lucideRange(io) {
822
+ try {
823
+ const sdk = JSON.parse(import_node_fs3.default.readFileSync(io.sdkPackageJson ?? "", "utf8"));
824
+ return sdk.peerDependencies?.["lucide-react"] ?? ">=0.400.0";
825
+ } catch {
826
+ return ">=0.400.0";
827
+ }
828
+ }
759
829
  function mergedPackageJson(rendered, peers) {
760
830
  const pkg = JSON.parse(rendered);
761
831
  for (const [name, range] of Object.entries(peers)) pkg.dependencies[name] ??= range;
@@ -773,14 +843,14 @@ function planCreate(reg, stack, appName, pm, io) {
773
843
  stackId: stack.id,
774
844
  title: stack.title,
775
845
  titleJson: JSON.stringify(stack.title),
776
- navJson: JSON.stringify(stack.nav.map((n) => ({ label: n.label, path: n.path })), null, 2),
846
+ ...navParts(stack),
777
847
  devCommand,
778
848
  screens: stack.nav.map((n) => `- ${n.label}: \`${n.path}\` (${n.block} block, \`${n.component}\`)`).join("\n") || "- (none yet: add a block with `npx @graph8/sdk add <block>`)"
779
849
  };
780
850
  const files = [];
781
851
  for (const rel of walk(templates).filter((f) => !f.startsWith("_pages/"))) {
782
852
  let content = render(import_node_fs3.default.readFileSync(import_node_path3.default.join(templates, rel), "utf8"), vars);
783
- if (rel === "package.json.tmpl") content = mergedPackageJson(content, peerDependenciesFor(blocks, io));
853
+ if (rel === "package.json.tmpl") content = mergedPackageJson(content, peerDependenciesFor(blocks, io, stack));
784
854
  files.push({ rel: outputName(rel), content });
785
855
  }
786
856
  files.push(...renderPages(reg, stack, templates));
@@ -859,8 +929,8 @@ function finish(opts, reg, stack, blocks, abs, pm, io, cwd) {
859
929
  }
860
930
 
861
931
  // src/blocks/doctor.ts
862
- var import_node_fs4 = __toESM(require("fs"));
863
- var import_node_path4 = __toESM(require("path"));
932
+ var import_node_fs5 = __toESM(require("fs"));
933
+ var import_node_path5 = __toESM(require("path"));
864
934
 
865
935
  // src/generated/billable.ts
866
936
  var BILLABLE_ROUTES = {
@@ -1680,7 +1750,7 @@ function warnIfTargetOrgIgnored(apiKey, headers) {
1680
1750
  );
1681
1751
  }
1682
1752
  }
1683
- async function request(baseUrl, path6, apiKey, opts = {}) {
1753
+ async function request(baseUrl, path7, apiKey, opts = {}) {
1684
1754
  const {
1685
1755
  method = "GET",
1686
1756
  body,
@@ -1693,7 +1763,7 @@ async function request(baseUrl, path6, apiKey, opts = {}) {
1693
1763
  fetchImpl = fetch,
1694
1764
  sleepImpl = defaultSleep
1695
1765
  } = opts;
1696
- const url = baseUrl.replace(/\/+$/, "") + path6 + buildQuery(query);
1766
+ const url = baseUrl.replace(/\/+$/, "") + path7 + buildQuery(query);
1697
1767
  const finalHeaders = {
1698
1768
  "Content-Type": "application/json",
1699
1769
  Authorization: `Bearer ${apiKey}`,
@@ -5173,14 +5243,89 @@ function createApiClient(apiKey, apiUrl) {
5173
5243
  return client;
5174
5244
  }
5175
5245
 
5246
+ // src/blocks/flows.ts
5247
+ var import_node_fs4 = __toESM(require("fs"));
5248
+ var import_node_path4 = __toESM(require("path"));
5249
+ var IMPORT_RE2 = /(?:^|\n)\s*(?:import|export)\s[^'"`;]*?from\s*["']([^"']+)["']|(?:^|\n)\s*import\s*["']([^"']+)["']|import\(\s*["']([^"']+)["']\s*\)/g;
5250
+ var EXTENSIONS = ["", ".tsx", ".ts", "/index.tsx", "/index.ts"];
5251
+ function specifiers(source) {
5252
+ return [...source.matchAll(IMPORT_RE2)].map((m) => m[1] ?? m[2] ?? m[3]);
5253
+ }
5254
+ function resolveFile(base) {
5255
+ for (const ext of EXTENSIONS) {
5256
+ const candidate = base + ext;
5257
+ if (import_node_fs4.default.existsSync(candidate) && import_node_fs4.default.statSync(candidate).isFile()) return candidate;
5258
+ }
5259
+ return null;
5260
+ }
5261
+ function resolveImport(spec, from, reg) {
5262
+ if (spec.startsWith(".")) return resolveFile(import_node_path4.default.resolve(import_node_path4.default.dirname(from), spec));
5263
+ const g8 = /^@\/g8\/([^/]+)\/(.+)$/.exec(spec);
5264
+ if (!g8) return null;
5265
+ const owner = findBlock(reg, g8[1]);
5266
+ if (owner) return resolveFile(import_node_path4.default.join(owner.root, "components", g8[2])) ?? resolveFile(import_node_path4.default.join(owner.root, g8[2]));
5267
+ const foundation = findBlock(reg, "foundation");
5268
+ return foundation ? resolveFile(import_node_path4.default.join(foundation.root, g8[1], g8[2])) : null;
5269
+ }
5270
+ function resolveInstalled(spec, reg) {
5271
+ const m = /^@\/components\/g8\/([^/]+)\/(.+)$/.exec(spec);
5272
+ const block = m && reg.blocks.find((b) => b.dir === m[1]);
5273
+ if (!m || !block) return null;
5274
+ const files = [...block.components, ...block.hooks, ...block.lib].map((x) => x.file);
5275
+ const hit = files.find((f) => (installedRel(f) ?? f).replace(/\.(tsx|ts|jsx|js)$/, "") === m[2]);
5276
+ return hit ? import_node_path4.default.join(block.root, hit) : null;
5277
+ }
5278
+ function seeds(reg, stack) {
5279
+ const out = [];
5280
+ for (const n of stack.nav) {
5281
+ const comp = findBlock(reg, n.block)?.components.find((c) => c.name === n.component);
5282
+ const block = findBlock(reg, n.block);
5283
+ if (!block || !comp) continue;
5284
+ const pageFile = n.page && reg.stacksRoot ? import_node_path4.default.join(reg.stacksRoot, stack.id, n.page) : null;
5285
+ if (!pageFile || !import_node_fs4.default.existsSync(pageFile)) {
5286
+ out.push(import_node_path4.default.join(block.root, comp.file));
5287
+ continue;
5288
+ }
5289
+ const source = import_node_fs4.default.readFileSync(pageFile, "utf8");
5290
+ if (source.includes("{{importPath}}")) out.push(import_node_path4.default.join(block.root, comp.file));
5291
+ for (const spec of specifiers(source)) {
5292
+ const hit = resolveInstalled(spec, reg);
5293
+ if (hit) out.push(hit);
5294
+ }
5295
+ }
5296
+ return out;
5297
+ }
5298
+ function reachedFiles(reg, stack) {
5299
+ const seen = /* @__PURE__ */ new Set();
5300
+ const queue = seeds(reg, stack);
5301
+ while (queue.length) {
5302
+ const file = queue.pop();
5303
+ if (seen.has(file)) continue;
5304
+ seen.add(file);
5305
+ if (!/\.(tsx?|jsx?)$/.test(file)) continue;
5306
+ for (const spec of specifiers(import_node_fs4.default.readFileSync(file, "utf8"))) {
5307
+ const next = resolveImport(spec, file, reg);
5308
+ if (next && !seen.has(next)) queue.push(next);
5309
+ }
5310
+ }
5311
+ return seen;
5312
+ }
5313
+ function unreachedComponents(reg, stack) {
5314
+ const reached = reachedFiles(reg, stack);
5315
+ const blocks = resolveWithDeps(reg, stack.blocks).filter((b) => b.id !== "foundation" && stack.blocks.includes(b.id));
5316
+ return blocks.flatMap((b) => b.components.filter((c) => !reached.has(import_node_path4.default.join(b.root, c.file))).map((c) => ({ block: b.id, component: c.name })));
5317
+ }
5318
+
5176
5319
  // src/blocks/doctor.ts
5177
- var DOCTOR_USAGE = `Usage: npx @graph8/sdk doctor [--stack <id>] [--blocks a,b] [--dir <path>]
5320
+ var DOCTOR_USAGE = `Usage: npx @graph8/sdk doctor [--stack <id>] [--blocks a,b] [--dir <path>] [--strict]
5178
5321
 
5179
5322
  Check that this project is ready for graph8 blocks:
5180
5323
  - G8_API_KEY (env or .env.local) is valid
5181
5324
  - the key's scopes cover the blocks you chose
5182
5325
  - the organization's subscription is active
5183
5326
  - a checklist of in-app setup each block needs
5327
+ With --stack it also lists "Flows": block components that no page of the stack reaches
5328
+ (a warning; --strict makes them fail the run).
5184
5329
  With no --stack/--blocks, the blocks recorded in graph8.json are checked.
5185
5330
  Optional G8_API_URL points at a non-default API. Exit code 0 when all checks
5186
5331
  pass, 1 otherwise.`;
@@ -5200,6 +5345,7 @@ ${DOCTOR_USAGE}`);
5200
5345
  if (flag === "--stack") o.stack = value();
5201
5346
  else if (flag === "--blocks") o.blocks.push(...value().split(",").map((s) => s.trim()).filter(Boolean));
5202
5347
  else if (flag === "--dir") o.dir = value();
5348
+ else if (arg === "--strict") o.strict = true;
5203
5349
  else throw new Error(`unknown option ${arg}
5204
5350
 
5205
5351
  ${DOCTOR_USAGE}`);
@@ -5218,9 +5364,9 @@ function parseEnvFile(text) {
5218
5364
  function readSetting(name, root, env) {
5219
5365
  if (env[name]) return env[name];
5220
5366
  for (const f of [".env.local"]) {
5221
- const file = import_node_path4.default.join(root, f);
5222
- if (import_node_fs4.default.existsSync(file)) {
5223
- const v = parseEnvFile(import_node_fs4.default.readFileSync(file, "utf8"))[name];
5367
+ const file = import_node_path5.default.join(root, f);
5368
+ if (import_node_fs5.default.existsSync(file)) {
5369
+ const v = parseEnvFile(import_node_fs5.default.readFileSync(file, "utf8"))[name];
5224
5370
  if (v) return v;
5225
5371
  }
5226
5372
  }
@@ -5241,7 +5387,7 @@ function selectBlocks(opts, reg, root) {
5241
5387
  }
5242
5388
  if (!ids.length) {
5243
5389
  try {
5244
- const ledger = JSON.parse(import_node_fs4.default.readFileSync(import_node_path4.default.join(root, "graph8.json"), "utf8"));
5390
+ const ledger = JSON.parse(import_node_fs5.default.readFileSync(import_node_path5.default.join(root, "graph8.json"), "utf8"));
5245
5391
  ids = Object.keys(ledger.blocks ?? {});
5246
5392
  } catch {
5247
5393
  ids = [];
@@ -5255,7 +5401,7 @@ function errorText(e, key) {
5255
5401
  return `${status ? `HTTP ${status}: ` : ""}${msg}`.split(key).join("[redacted]");
5256
5402
  }
5257
5403
  async function runDoctor(opts, reg, io, cwd) {
5258
- const root = import_node_path4.default.resolve(cwd, opts.dir);
5404
+ const root = import_node_path5.default.resolve(cwd, opts.dir);
5259
5405
  let blocks;
5260
5406
  try {
5261
5407
  blocks = selectBlocks(opts, reg, root);
@@ -5281,6 +5427,7 @@ async function runDoctor(opts, reg, io, cwd) {
5281
5427
  await checkKey(call, blocks, key, line);
5282
5428
  await checkSubscription(call, key, line);
5283
5429
  }
5430
+ if (opts.stack) printFlows(opts, reg, io.out, line);
5284
5431
  printChecklist(blocks, io.out);
5285
5432
  io.out(failed ? "\nSome checks failed." : "\nAll checks passed.");
5286
5433
  return failed ? 1 : 0;
@@ -5318,6 +5465,18 @@ async function checkSubscription(call, key, line) {
5318
5465
  else line(false, `could not read subscription status (${errorText(e, key)})`);
5319
5466
  }
5320
5467
  }
5468
+ function printFlows(opts, reg, out, line) {
5469
+ const stack = reg.stacks.find((s) => s.id === opts.stack);
5470
+ if (!stack) return;
5471
+ const missing = unreachedComponents(reg, stack);
5472
+ out("\nFlows (block components a page of this stack reaches):");
5473
+ if (!missing.length) return line(true, "every component of the stack's blocks is reachable from a page");
5474
+ for (const m of missing) {
5475
+ const text = `(${m.block}) ${m.component} is not reached by any page of "${stack.id}": wire it into a page or a Screen slot`;
5476
+ if (opts.strict) line(false, text);
5477
+ else out(`warn ${text}`);
5478
+ }
5479
+ }
5321
5480
  function printChecklist(blocks, out) {
5322
5481
  const items = blocks.flatMap((b) => b.prerequisites.filter((p) => p.where === "app").map((p) => ({ block: b.id, p })));
5323
5482
  if (!items.length) return;
@@ -5349,7 +5508,7 @@ Run a command with --help for its options.
5349
5508
 
5350
5509
  ${INIT_AGENT_USAGE}`;
5351
5510
  function registryFor(io) {
5352
- const root = io.blocksRoot ?? process.env.G8_BLOCKS_DIR ?? import_node_path5.default.resolve(__dirname, "..", "blocks");
5511
+ const root = io.blocksRoot ?? process.env.G8_BLOCKS_DIR ?? import_node_path6.default.resolve(__dirname, "..", "blocks");
5353
5512
  return loadRegistry(root);
5354
5513
  }
5355
5514
  function defaultSpawn(cmd, args, cwd) {
@@ -5362,6 +5521,7 @@ function printCatalog(blocks, stacks, io, empty) {
5362
5521
  if (blocks.length) io.out("Blocks");
5363
5522
  for (const b of blocks) {
5364
5523
  io.out(` ${b.id} ${b.summary}`);
5524
+ if (b.screens?.length) io.out(` screens (start here): ${b.screens.join(", ")}`);
5365
5525
  if (b.dependsOn.length) io.out(` depends on: ${b.dependsOn.join(", ")}`);
5366
5526
  }
5367
5527
  if (stacks.length) io.out(`${blocks.length ? "\n" : ""}Stacks`);
@@ -5413,13 +5573,13 @@ function runCreateCommand(rest, io) {
5413
5573
  if (wantsHelp(rest)) return io.out(CREATE_USAGE), 0;
5414
5574
  try {
5415
5575
  const opts = parseCreateArgs(rest);
5416
- const packageRoot = import_node_path5.default.resolve(__dirname, "..");
5576
+ const packageRoot = import_node_path6.default.resolve(__dirname, "..");
5417
5577
  const cio = {
5418
5578
  out: io.out,
5419
5579
  err: io.err,
5420
5580
  spawn: io.spawn ?? defaultSpawn,
5421
- templatesRoot: io.templatesRoot ?? process.env.G8_TEMPLATES_DIR ?? import_node_path5.default.join(packageRoot, "templates"),
5422
- sdkPackageJson: import_node_path5.default.join(packageRoot, "package.json"),
5581
+ templatesRoot: io.templatesRoot ?? process.env.G8_TEMPLATES_DIR ?? import_node_path6.default.join(packageRoot, "templates"),
5582
+ sdkPackageJson: import_node_path6.default.join(packageRoot, "package.json"),
5423
5583
  userAgent: io.userAgent ?? process.env.npm_config_user_agent
5424
5584
  };
5425
5585
  return runCreate(opts, registryFor(io), cio, io.cwd);
@@ -5465,8 +5625,8 @@ ${USAGE}`);
5465
5625
  return 1;
5466
5626
  }
5467
5627
  const read = (rel) => {
5468
- const full = import_node_path5.default.join(io.cwd, rel);
5469
- return import_node_fs5.default.existsSync(full) ? import_node_fs5.default.readFileSync(full, "utf8") : null;
5628
+ const full = import_node_path6.default.join(io.cwd, rel);
5629
+ return import_node_fs6.default.existsSync(full) ? import_node_fs6.default.readFileSync(full, "utf8") : null;
5470
5630
  };
5471
5631
  let plans;
5472
5632
  try {
@@ -5477,9 +5637,9 @@ ${USAGE}`);
5477
5637
  }
5478
5638
  for (const plan of plans) {
5479
5639
  if (!options.dryRun && plan.action !== "unchanged") {
5480
- const full = import_node_path5.default.join(io.cwd, plan.path);
5481
- import_node_fs5.default.mkdirSync(import_node_path5.default.dirname(full), { recursive: true });
5482
- import_node_fs5.default.writeFileSync(full, plan.content, "utf8");
5640
+ const full = import_node_path6.default.join(io.cwd, plan.path);
5641
+ import_node_fs6.default.mkdirSync(import_node_path6.default.dirname(full), { recursive: true });
5642
+ import_node_fs6.default.writeFileSync(full, plan.content, "utf8");
5483
5643
  }
5484
5644
  io.out(`${options.dryRun ? "would " : ""}${plan.action === "unchanged" ? "keep" : plan.action} ${plan.path} (${plan.target})`);
5485
5645
  }
@@ -991,7 +991,7 @@ var EXTERNAL_ROUTES = {
991
991
  };
992
992
 
993
993
  // src/version.ts
994
- var SDK_VERSION = "0.358.0";
994
+ var SDK_VERSION = "0.362.0";
995
995
  var SDK_CLIENT_ID = `js-sdk/${SDK_VERSION}`;
996
996
 
997
997
  // src/http.ts