climaybe 3.0.9 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,150 +1,170 @@
1
1
  ---
2
- description: Schema design rules — minimal settings, translation keys, no redundancy
2
+ description: Schema design rules and schema builder _schemas/, partials, dynamic generation, inline markers
3
3
  globs:
4
4
  - "sections/**/*.liquid"
5
5
  - "**/sections/*.liquid"
6
6
  - "blocks/**/*.liquid"
7
7
  - "**/blocks/*.liquid"
8
+ - "_schemas/**/*.js"
9
+ - "_schemas/**/*.json"
8
10
  alwaysApply: false
9
11
  ---
10
- # Schema Design Rules for Electric Maybe Shopify Theme
12
+ # Schema Design Rules
11
13
 
12
- ## Core Principles
14
+ ## Schema Builder (`climaybe build-schemas`)
13
15
 
14
- ### 1. **Start Simple, Expand Gradually**
16
+ This project uses the climaybe schema builder to generate schemas from JavaScript/JSON source files. Schemas are defined in `_schemas/` and injected into `sections/*.liquid` and `blocks/*.liquid` at build time.
17
+
18
+ ### How it works
19
+
20
+ A section or block file contains an inline-comment marker at the end:
21
+
22
+ ```liquid
23
+ <section class="hero">{{ section.settings.title }}</section>
24
+
25
+ {% # schema 'hero-banner' %}
26
+ ```
27
+
28
+ `{% # ... %}` is a Liquid inline comment — Shopify ignores it. The builder finds the marker, resolves `_schemas/hero-banner.js` (or `.json`), and writes the generated `{% schema %}...{% endschema %}` block below it. On rebuild, only the generated block is replaced. Everything above the marker (including theme editor changes) is preserved.
29
+
30
+ ### Directory structure
31
+
32
+ ```
33
+ _schemas/ ← schema definitions (JS or JSON)
34
+ ├── partials/ ← shared settings, fieldsets, factory functions
35
+ │ ├── link.js
36
+ │ ├── create-links.js
37
+ │ └── spacing.js
38
+ ├── hero-banner.js ← referenced by sections via marker
39
+ ├── landing-page.js
40
+ └── page-schema.js
41
+ ```
42
+
43
+ ### When creating or editing schemas
44
+
45
+ 1. **Define schemas in `_schemas/`**, not inline in section files. The `{% schema %}...{% endschema %}` block in `sections/` is generated output — never edit it directly.
46
+
47
+ 2. **Use JS files** (CommonJS `module.exports`) when you need imports, computation, or comments. Use JSON for simple static schemas.
48
+
49
+ 3. **Extract shared settings into `_schemas/partials/`** — these are imported via `require()` and are never referenced directly by section markers.
50
+
51
+ 4. **Spread partials into settings arrays:**
52
+ ```js
53
+ const linkSettings = require('./partials/link.js');
54
+ const spacingSettings = require('./partials/spacing.js');
55
+
56
+ module.exports = {
57
+ name: 'Featured Collection',
58
+ settings: [
59
+ { type: 'text', id: 'title', label: 'Heading' },
60
+ ...linkSettings,
61
+ ...spacingSettings,
62
+ ],
63
+ };
64
+ ```
65
+
66
+ 5. **Use factory functions for repeated field groups:**
67
+ ```js
68
+ const createLinks = require('./partials/create-links.js');
69
+
70
+ module.exports = {
71
+ name: 'Hero',
72
+ settings: [
73
+ { type: 'text', id: 'heading', label: 'Heading' },
74
+ ...createLinks(3),
75
+ ],
76
+ };
77
+ ```
78
+
79
+ 6. **Use function exports for per-section overrides:**
80
+ ```js
81
+ module.exports = function (filename, content) {
82
+ return {
83
+ name: content.name || filename.replace('.liquid', ''),
84
+ settings: [/* ... */],
85
+ };
86
+ };
87
+ ```
88
+
89
+ The function receives `(filename, inlineContent)`. Inline content comes from a second inline comment in the section file:
90
+ ```liquid
91
+ {% # schema 'page-schema' %}
92
+ {% # { "name": "About Us" } %}
93
+ ```
94
+
95
+ 7. **For static exports**, inline JSON is shallow-merged on top (inline wins):
96
+ ```liquid
97
+ {% # schema 'newsletter' %}
98
+ {% # { "name": "Footer Newsletter" } %}
99
+ ```
100
+
101
+ ### Commands
102
+
103
+ ```bash
104
+ npx climaybe build-schemas # generate schemas in sections/
105
+ npx climaybe build-schemas --dry-run # preview without writing
106
+ npx climaybe build-schemas --list # list schema files and markers
107
+ ```
108
+
109
+ Schemas also rebuild automatically during `climaybe serve` (watch mode, tagged `[schema]` in console).
110
+
111
+ ### Rules for AI assistants
112
+
113
+ - When asked to create a new section or block with a schema, create the schema definition in `_schemas/` and add the `{% # schema 'name' %}` marker to the liquid file. Do not write raw `{% schema %}` JSON.
114
+ - When asked to edit a schema, edit the file in `_schemas/`, not the generated block in `sections/` or `blocks/`.
115
+ - When adding fields that are reused across schemas (links, spacing, colors), extract them into `_schemas/partials/` and spread them.
116
+ - When duplicating a schema for a variant section, use a shared schema file instead. Add the same marker to both sections.
117
+ - After editing `_schemas/` files, remind the user to run `npx climaybe build-schemas` (or note that `climaybe serve` rebuilds automatically).
118
+
119
+ ---
120
+
121
+ ## Schema Design Principles
122
+
123
+ ### 1. Start Simple, Expand Gradually
15
124
  - Begin with minimal, essential settings only
16
125
  - Add complexity only when there's a clear, demonstrated need
17
- - Ask for permission before adding multiple settings at once
18
126
 
19
- ### 2. **Avoid Redundancy**
127
+ ### 2. Avoid Redundancy
20
128
  - If text content exists in translation files (`en.default.json`), don't duplicate it in schema
21
129
  - Use translation keys for all text content, not schema text inputs
22
130
  - Schema should focus on functionality, not content
23
131
 
24
- ### 3. **Question Every Setting**
132
+ ### 3. Question Every Setting
25
133
  Before adding any schema setting, ask:
26
134
  - Is this truly necessary for the user?
27
135
  - Can this be handled by translation files instead?
28
136
  - Will this setting be used by most merchants?
29
137
  - Does this add value or just complexity?
30
138
 
31
- ### 4. **Limit Settings Per Section**
139
+ ### 4. Limit Settings Per Section
32
140
  - **Maximum 5 settings per section** (excluding headers)
33
141
  - If more are needed, consider splitting into multiple sections
34
142
  - Group related settings under headers
35
143
 
36
144
  ## Schema Setting Guidelines
37
145
 
38
- ### **Essential Settings (Always Include)**
146
+ ### Essential Settings (Always Include)
39
147
  - Link list selectors for menus
40
148
  - Image upload fields for key visuals
41
149
  - Color scheme selectors (if theme supports multiple schemes)
42
150
 
43
- ### **Optional Settings (Add Only When Needed)**
151
+ ### Optional Settings (Add Only When Needed)
44
152
  - Layout toggles (show/hide elements)
45
153
  - Spacing adjustments
46
154
  - Text alignment options
47
155
  - Background color/image options
48
156
 
49
- ### **Avoid These Settings**
157
+ ### Avoid These Settings
50
158
  - Text content inputs (use translation files)
51
159
  - Toggle switches for every element
52
160
  - Complex layout selectors
53
161
  - Settings that duplicate existing functionality
54
162
 
55
- ## Implementation Rules
56
-
57
- ### **Before Adding Settings**
58
- 1. Check if the functionality can be achieved through:
59
- - Translation files
60
- - CSS classes
61
- - Existing theme settings
62
- - Liquid conditionals
63
-
64
- 2. Consider the user experience:
65
- - Will merchants actually use this setting?
66
- - Is it intuitive and discoverable?
67
- - Does it solve a real problem?
68
-
69
- 3. Evaluate maintenance cost:
70
- - Will this setting need updates?
71
- - Does it create technical debt?
72
- - Is it worth the complexity?
73
-
74
- ### **When Adding Multiple Settings**
75
- - **Ask for permission first**
76
- - Explain the reasoning for each setting
77
- - Consider if they can be grouped or simplified
78
- - Test with a small subset first
79
-
80
- ## Examples
81
-
82
- ### ✅ Good Schema (Simple)
83
- ```json
84
- {
85
- "settings": [
86
- {
87
- "type": "link_list",
88
- "id": "main_menu",
89
- "label": "Main menu"
90
- },
91
- {
92
- "type": "image_picker",
93
- "id": "logo",
94
- "label": "Logo"
95
- }
96
- ]
97
- }
98
- ```
99
-
100
- ### ❌ Bad Schema (Over-engineered)
101
- ```json
102
- {
103
- "settings": [
104
- {
105
- "type": "link_list",
106
- "id": "main_menu",
107
- "label": "Main menu"
108
- },
109
- {
110
- "type": "checkbox",
111
- "id": "show_menu",
112
- "label": "Show menu",
113
- "default": true
114
- },
115
- {
116
- "type": "select",
117
- "id": "menu_style",
118
- "label": "Menu style",
119
- "options": [
120
- {"value": "horizontal", "label": "Horizontal"},
121
- {"value": "vertical", "label": "Vertical"},
122
- {"value": "dropdown", "label": "Dropdown"}
123
- ]
124
- },
125
- {
126
- "type": "range",
127
- "id": "menu_spacing",
128
- "min": 0,
129
- "max": 50,
130
- "step": 5,
131
- "label": "Menu spacing"
132
- }
133
- ]
134
- }
135
- ```
136
-
137
163
  ## Review Process
138
164
 
139
165
  Before committing any schema changes:
140
- 1. **Count the settings** - Keep under 5 per section
141
- 2. **Check for redundancy** - Ensure no duplicate functionality
142
- 3. **Verify necessity** - Each setting should solve a real problem
143
- 4. **Consider alternatives** - Could this be handled differently?
144
- 5. **Get approval** - For multiple settings or complex changes
145
-
146
- ## Remember
147
- - **Simplicity is a feature**
148
- - **Less is more**
149
- - **When in doubt, leave it out**
150
- - **Ask before expanding**
166
+ 1. **Count the settings** keep under 5 per section
167
+ 2. **Check for redundancy** ensure no duplicate functionality
168
+ 3. **Verify necessity** each setting should solve a real problem
169
+ 4. **Check partials** could this field group be extracted and shared?
170
+ 5. **Consider alternatives** could this be handled via translations, CSS, or existing settings?
@@ -11,11 +11,13 @@ alwaysApply: false
11
11
 
12
12
  Every section must include:
13
13
 
14
- - `{% schema %}` tag with valid JSON
14
+ - Schema definition — either a `{% schema %}` tag with valid JSON, or a `{% # schema 'name' %}` marker referencing a schema from `_schemas/` (see `schemas.mdc` for the schema builder)
15
15
  - Proper HTML semantic structure
16
16
  - CSS scoping with section classes
17
17
  - Translation keys for all text
18
18
 
19
+ When using the schema builder, add the inline-comment marker at the end of the section file and define the schema in `_schemas/`. The generated `{% schema %}...{% endschema %}` block is build output — do not edit it directly.
20
+
19
21
  ## Section Patterns
20
22
 
21
23
  _(To be filled in on review.)_
@@ -12,9 +12,9 @@ Builds a changelog or release notes from commits since a ref (e.g. last tag), gr
12
12
  When grouping and labeling commits, read and apply:
13
13
 
14
14
  1. `.cursor/rules/00-rule-index.mdc` — rule index
15
- 2. `.cursor/rules/commit-rules.mdc` — commit types (🔨 fix, 🚀 feat, ♻️ refactor, 🎨 style, etc.) and format
15
+ 2. `.cursor/rules/commit-rules.mdc` — commit types (fix, feat, refactor, style, etc.) and format
16
16
 
17
- Use the same type labels and emoji for changelog sections so the output matches how the team writes commits.
17
+ Use the same type labels for changelog sections so the output matches how the team writes commits.
18
18
 
19
19
  ## Workflow
20
20
 
@@ -23,8 +23,8 @@ Use the same type labels and emoji for changelog sections so the output matches
23
23
  3. **Group by type** — Map each commit to a type from commit-rules.mdc (fix, feat, refactor, style, remove, wip, docs, ai, chore, upgrade). Ignore merge commits. Put "unknown" or "other" for non-matching messages.
24
24
  4. **Format** — Output markdown:
25
25
  - Optional title (e.g. "Changelog since v1.2.0")
26
- - Sections by type (e.g. "## 🚀 Features", "## 🔨 Fixes")
27
- - Under each section: list of commit descriptions (one line each, link to commit if desired). Strip emoji+type prefix for readability if you want, or keep for consistency.
26
+ - Sections by type (e.g. "## Features", "## Fixes")
27
+ - Under each section: list of commit descriptions (one line each, link to commit if desired).
28
28
  5. **Optional** — Add "Breaking changes" subsection if any commit message indicates breaking change (e.g. `!` or "BREAKING CHANGE").
29
29
 
30
30
  ## Output Format
@@ -32,15 +32,15 @@ Use the same type labels and emoji for changelog sections so the output matches
32
32
  ```markdown
33
33
  # Changelog (since [ref])
34
34
 
35
- ## 🚀 Features
35
+ ## Features
36
36
  - add product quick view modal
37
37
  - implement infinite scroll for collections
38
38
 
39
- ## 🔨 Fixes
39
+ ## Fixes
40
40
  - resolve cart total calculation error
41
41
  - modal close button not working
42
42
 
43
- ## ♻️ Refactor
43
+ ## Refactor
44
44
  - optimize product card rendering
45
45
  ```
46
46
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: commit-in-groups
3
- description: Groups current working tree changes into logical commits and suggests commit messages per project convention. Use when the user says "/commit-in-groups", "commit in groups", "group my commits", "suggest separate commits", or "split my changes into commits". Follows commit-rules.mdc (emoji + type + description).
3
+ description: Groups current working tree changes into logical commits and suggests commit messages per project convention. Use when the user says "/commit-in-groups", "commit in groups", "group my commits", "suggest separate commits", or "split my changes into commits". Follows commit-rules.mdc (type + description).
4
4
  ---
5
5
 
6
6
  # Commit in Groups
@@ -14,7 +14,7 @@ Analyzes uncommitted changes, groups them into logical commits, and suggests one
14
14
  Before suggesting any commit messages, read and apply:
15
15
 
16
16
  1. `.cursor/rules/00-rule-index.mdc` — rule index
17
- 2. `.cursor/rules/commit-rules.mdc` — commit format (emoji + type + description), types (fix, feat, refactor, style, etc.), imperative mood, optional scope
17
+ 2. `.cursor/rules/commit-rules.mdc` — commit format (type + description), types (fix, feat, refactor, style, etc.), imperative mood, optional scope
18
18
 
19
19
  Use the exact format and types from commit-rules; no period at end; lowercase description.
20
20
 
@@ -27,8 +27,8 @@ Use the exact format and types from commit-rules; no period at end; lowercase de
27
27
  - Refactor in one area, fix in another → separate commits
28
28
  - Locale/schema/docs can be separate if they form a clear unit
29
29
  - When in doubt, split rather than combine
30
- 3. **Assign type** — For each group, pick the best type from commit-rules (🔨 fix, 🚀 feat, ♻️ refactor, 🎨 style, 🗑️ remove, 📝 docs, 🔧 chore, etc.) and optional scope (e.g. `sections`, `cart`, `locales`).
31
- 4. **Write messages** — One line per commit: `<emoji> <type>(scope): <description>`. Imperative, lowercase, no period. Multi-line body only if the change is complex and warrants bullets.
30
+ 3. **Assign type** — For each group, pick the best type from commit-rules (fix, feat, refactor, style, remove, docs, chore, etc.) and optional scope (e.g. `sections`, `cart`, `locales`).
31
+ 4. **Write messages** — One line per commit: `<type>(scope): <description>`. Imperative, lowercase, no period. Multi-line body only if the change is complex and warrants bullets.
32
32
  5. **Output and execute** — List the suggested commits (message + files per commit), then **run the commits yourself**: for each group, run `git add` on the listed files and `git commit -m "..."` with the exact message. Do not only suggest commands; execute them so the working tree is left with the changes committed. Summarize what was committed at the end.
33
33
 
34
34
  ## Output Format
@@ -38,11 +38,11 @@ Show the plan briefly, then run it:
38
38
  ```markdown
39
39
  ## Suggested commits
40
40
 
41
- **Commit 1:** `🚀 feat(sections): add featured collection section`
41
+ **Commit 1:** `feat(sections): add featured collection section`
42
42
  - `sections/s--featured-collection.liquid`
43
43
  - `snippets/m--product-card.liquid`
44
44
 
45
- **Commit 2:** `🎨 style(cart): adjust drawer spacing`
45
+ **Commit 2:** `style(cart): adjust drawer spacing`
46
46
  - `sections/cart--drawer.liquid`
47
47
  - `assets/style.css`
48
48
  ```
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import { appInitCommand } from './commands/app-init.js';
12
12
  import { migrateLegacyConfigCommand } from './commands/migrate-legacy-config.js';
13
13
  import { buildScriptsCommand } from './commands/build-scripts.js';
14
14
  import { createEntrypointsCommand } from './commands/create-entrypoints.js';
15
+ import { buildSchemasCommand } from './commands/build-schemas.js';
15
16
  import { serveAll, serveAssets, serveShopify, lintAll, buildAll } from './lib/dev-runtime.js';
16
17
 
17
18
  /**
@@ -84,6 +85,13 @@ function registerThemeCommands(cmd) {
84
85
  .description('Create _scripts/main.js and _styles/main.css (optional)')
85
86
  .action(createEntrypointsCommand);
86
87
 
88
+ cmd
89
+ .command('build-schemas')
90
+ .description('Generate schemas from _schemas/ JS/JSON into sections/ and blocks/ (uses inline-comment markers)')
91
+ .option('--dry-run', 'Show what would be injected without writing files')
92
+ .option('--list', 'List available schema files and section references')
93
+ .action(buildSchemasCommand);
94
+
87
95
  cmd
88
96
  .command('update')
89
97
  .alias('update-workflows')
@@ -1,25 +1,29 @@
1
1
  import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs';
2
2
  import { join, basename, dirname, normalize } from 'node:path';
3
3
 
4
- function extractImports(content) {
4
+ function extractImportRecords(content) {
5
5
  const imports = [];
6
6
  // Supports compact imports (import{a}from"./x"), multiline forms,
7
7
  // and import attributes (with { type: "json" }).
8
8
  const fromImportRegex =
9
- /(^|\n)\s*import(?:\s+type)?\s*[\s\S]*?\s*\bfrom\b\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
9
+ /(^|\n)\s*import(?:\s+type)?\s*([\s\S]*?)\s*\bfrom\b\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
10
10
  const sideEffectImportRegex = /(^|\n)\s*import\s*['"]([^'"]+)['"](?:\s+with\s*\{[\s\S]*?\})?\s*;?/g;
11
11
  let match;
12
12
 
13
13
  while ((match = fromImportRegex.exec(content)) !== null) {
14
- imports.push(match[2]);
14
+ imports.push({ importPath: match[3], hasBindings: Boolean(match[2]?.trim()) });
15
15
  }
16
16
  while ((match = sideEffectImportRegex.exec(content)) !== null) {
17
- imports.push(match[2]);
17
+ imports.push({ importPath: match[2], hasBindings: false });
18
18
  }
19
19
 
20
20
  return imports;
21
21
  }
22
22
 
23
+ function extractImports(content) {
24
+ return extractImportRecords(content).map((record) => record.importPath);
25
+ }
26
+
23
27
  function stripModuleSyntax(content) {
24
28
  // Remove import statements (including multiline/compact forms and import attributes).
25
29
  let cleaned = content.replace(
@@ -58,7 +62,7 @@ function minifyScriptContent(content) {
58
62
  return minified;
59
63
  }
60
64
 
61
- function processScriptFile({ scriptsDir, filePath, processedFiles, minify = false }) {
65
+ function processScriptFile({ scriptsDir, filePath, processedFiles, minify = false, isolateFiles = new Set() }) {
62
66
  if (processedFiles.has(filePath)) return '';
63
67
  processedFiles.add(filePath);
64
68
 
@@ -78,11 +82,20 @@ function processScriptFile({ scriptsDir, filePath, processedFiles, minify = fals
78
82
  for (const importPath of imports) {
79
83
  const resolvedImport = resolveImportPath(filePath, importPath);
80
84
  if (!resolvedImport) continue;
81
- importedContent += processScriptFile({ scriptsDir, filePath: resolvedImport, processedFiles, minify });
85
+ importedContent += processScriptFile({
86
+ scriptsDir,
87
+ filePath: resolvedImport,
88
+ processedFiles,
89
+ minify,
90
+ isolateFiles
91
+ });
82
92
  }
83
93
 
84
94
  content = stripModuleSyntax(content);
85
95
  if (minify) content = minifyScriptContent(content);
96
+ if (isolateFiles.has(filePath)) {
97
+ content = `(function () {\n${content.trim()}\n})();`;
98
+ }
86
99
 
87
100
  return importedContent + '\n' + content;
88
101
  }
@@ -113,6 +126,39 @@ function collectImportedFiles({ scriptsDir, entryFile, seen = new Set() }) {
113
126
  return seen;
114
127
  }
115
128
 
129
+ function collectFilesToIsolate({ scriptsDir, entryFile }) {
130
+ const seen = new Set();
131
+ const importedWithBindings = new Set();
132
+
133
+ function visit(filePath) {
134
+ if (seen.has(filePath)) return;
135
+ seen.add(filePath);
136
+
137
+ const fullPath = join(scriptsDir, filePath);
138
+ if (!existsSync(fullPath)) return;
139
+ const content = readFileSync(fullPath, 'utf8');
140
+ const imports = extractImportRecords(content);
141
+
142
+ for (const record of imports) {
143
+ const resolved = resolveImportPath(filePath, record.importPath);
144
+ if (!resolved) continue;
145
+ if (record.hasBindings) importedWithBindings.add(resolved);
146
+ visit(resolved);
147
+ }
148
+ }
149
+
150
+ visit(entryFile);
151
+
152
+ const isolateFiles = new Set();
153
+ for (const file of seen) {
154
+ if (file !== entryFile && !importedWithBindings.has(file)) {
155
+ isolateFiles.add(file);
156
+ }
157
+ }
158
+
159
+ return isolateFiles;
160
+ }
161
+
116
162
  function listTopLevelEntrypoints(scriptsDir) {
117
163
  if (!existsSync(scriptsDir)) return [];
118
164
  return readdirSync(scriptsDir, { withFileTypes: true })
@@ -134,7 +180,14 @@ function buildSingleEntrypoint({ cwd, entryFile, minify = false }) {
134
180
  }
135
181
 
136
182
  const processedFiles = new Set();
137
- let finalContent = processScriptFile({ scriptsDir, filePath: entryFile, processedFiles, minify });
183
+ const isolateFiles = collectFilesToIsolate({ scriptsDir, entryFile });
184
+ let finalContent = processScriptFile({
185
+ scriptsDir,
186
+ filePath: entryFile,
187
+ processedFiles,
188
+ minify,
189
+ isolateFiles
190
+ });
138
191
  finalContent = stripModuleSyntax(finalContent);
139
192
  if (minify) finalContent = minifyScriptContent(finalContent);
140
193
 
@@ -5,6 +5,7 @@ import { isAbsolute, join, relative } from 'node:path';
5
5
  import pc from 'picocolors';
6
6
  import { readConfig } from './config.js';
7
7
  import { buildScripts } from './build-scripts.js';
8
+ import { buildSchemas } from './schema-builder.js';
8
9
  import { runShopify } from './shopify-cli.js';
9
10
 
10
11
  function tagLabel(tag, color = (s) => s) {
@@ -260,6 +261,49 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
260
261
  })
261
262
  : null;
262
263
 
264
+ const schemasDir = join(cwd, '_schemas');
265
+ const hasSchemas = existsSync(schemasDir);
266
+ if (hasSchemas) {
267
+ try {
268
+ const result = buildSchemas({ cwd });
269
+ if (result.processed.length > 0) {
270
+ writeTaggedLine('schema', pc.green, `built ${result.processed.length} file(s) (initial)`);
271
+ }
272
+ if (result.errors.length > 0) {
273
+ for (const e of result.errors) {
274
+ writeTaggedLine('schema', pc.green, `error: ${e.section} — ${e.error}`, process.stderr);
275
+ }
276
+ }
277
+ } catch (err) {
278
+ writeTaggedLine('schema', pc.green, `initial build failed: ${err.message}`, process.stderr);
279
+ }
280
+ } else {
281
+ writeTaggedLine('schema', pc.green, 'skipped (missing _schemas/)');
282
+ }
283
+
284
+ const schemasWatch = hasSchemas
285
+ ? watchTree({
286
+ rootDir: schemasDir,
287
+ ignore: (p) => p.includes('node_modules') || p.includes('/.git/'),
288
+ debounceMs: 300,
289
+ onChange: () => {
290
+ try {
291
+ const result = buildSchemas({ cwd });
292
+ if (result.processed.length > 0) {
293
+ writeTaggedLine('schema', pc.green, `rebuilt ${result.processed.length} file(s)`);
294
+ }
295
+ if (result.errors.length > 0) {
296
+ for (const e of result.errors) {
297
+ writeTaggedLine('schema', pc.green, `error: ${e.section} — ${e.error}`, process.stderr);
298
+ }
299
+ }
300
+ } catch (err) {
301
+ writeTaggedLine('schema', pc.green, `build failed: ${err.message}`, process.stderr);
302
+ }
303
+ },
304
+ })
305
+ : null;
306
+
263
307
  let themeCheckRunning = false;
264
308
  let themeCheckQueued = false;
265
309
  const runThemeCheck = () => {
@@ -287,7 +331,8 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
287
331
  p.includes('/assets/') ||
288
332
  p.includes('/.git/') ||
289
333
  p.includes('/_scripts/') ||
290
- p.includes('/_styles/'),
334
+ p.includes('/_styles/') ||
335
+ p.includes('/_schemas/'),
291
336
  debounceMs: 800,
292
337
  onChange: () => {
293
338
  runThemeCheck();
@@ -304,12 +349,13 @@ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = false } =
304
349
 
305
350
  const cleanup = () => {
306
351
  safeClose(scriptsWatch);
352
+ safeClose(schemasWatch);
307
353
  safeClose(themeCheckWatch);
308
354
  safeKill(tailwind);
309
355
  safeKill(devMcp);
310
356
  };
311
357
 
312
- return { tailwind, devMcp, scriptsWatch, themeCheckWatch, cleanup };
358
+ return { tailwind, devMcp, scriptsWatch, schemasWatch, themeCheckWatch, cleanup };
313
359
  }
314
360
 
315
361
  export function serveAll({ cwd = process.cwd(), includeThemeCheck = false } = {}) {
@@ -355,6 +401,24 @@ export function lintAll({ cwd = process.cwd() } = {}) {
355
401
 
356
402
  export function buildAll({ cwd = process.cwd() } = {}) {
357
403
  const env = { ...process.env, NODE_ENV: 'production' };
404
+
405
+ let schemasOk = true;
406
+ try {
407
+ const schemaResult = buildSchemas({ cwd });
408
+ if (schemaResult.processed.length > 0) {
409
+ writeTaggedLine('schema', pc.green, `built ${schemaResult.processed.length} section(s)`);
410
+ }
411
+ if (schemaResult.errors.length > 0) {
412
+ for (const e of schemaResult.errors) {
413
+ writeTaggedLine('schema', pc.green, `error: ${e.section} — ${e.error}`, process.stderr);
414
+ }
415
+ schemasOk = false;
416
+ }
417
+ } catch (err) {
418
+ console.log(pc.red(`\n build-schemas failed: ${err.message}\n`));
419
+ schemasOk = false;
420
+ }
421
+
358
422
  let scriptsOk = true;
359
423
  try {
360
424
  buildScripts({ cwd });
@@ -367,6 +431,6 @@ export function buildAll({ cwd = process.cwd() } = {}) {
367
431
  env,
368
432
  name: 'tailwind',
369
433
  });
370
- return { scriptsOk, tailwind };
434
+ return { schemasOk, scriptsOk, tailwind };
371
435
  }
372
436