climaybe 2.4.2 → 3.0.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.
@@ -0,0 +1,210 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { watchTree } from './watch.js';
4
+ import { join } from 'node:path';
5
+ import pc from 'picocolors';
6
+ import { readConfig } from './config.js';
7
+ import { buildScripts } from './build-scripts.js';
8
+ import { runShopify } from './shopify-cli.js';
9
+
10
+ function getPackageDir() {
11
+ return process.env.CLIMAYBE_PACKAGE_DIR || process.cwd();
12
+ }
13
+
14
+ function binPath(binName) {
15
+ // Resolve to this package's bundled .bin (works for npx + installed package).
16
+ return join(getPackageDir(), 'node_modules', '.bin', binName);
17
+ }
18
+
19
+ function spawnLogged(command, args, { name, cwd = process.cwd(), env = process.env } = {}) {
20
+ const child = spawn(command, args, {
21
+ cwd,
22
+ env,
23
+ stdio: 'inherit',
24
+ shell: process.platform === 'win32',
25
+ });
26
+
27
+ child.on('exit', (code, signal) => {
28
+ if (signal) {
29
+ console.log(pc.yellow(`\n ${name} exited with signal ${signal}\n`));
30
+ return;
31
+ }
32
+ if (code && code !== 0) {
33
+ console.log(pc.red(`\n ${name} exited with code ${code}\n`));
34
+ }
35
+ });
36
+
37
+ return child;
38
+ }
39
+
40
+ function runTailwind(args, { cwd = process.cwd(), env = process.env, name = 'tailwind' } = {}) {
41
+ return spawnLogged(
42
+ 'npx',
43
+ ['-y', '--package', '@tailwindcss/cli@latest', '--package', 'tailwindcss@latest', 'tailwindcss', ...args],
44
+ { name, cwd, env }
45
+ );
46
+ }
47
+
48
+ function safeClose(w) {
49
+ if (!w) return;
50
+ try {
51
+ w.close?.();
52
+ } catch {
53
+ // ignore
54
+ }
55
+ }
56
+
57
+ function safeKill(child) {
58
+ if (!child || typeof child.kill !== 'function') return;
59
+ try {
60
+ child.kill('SIGTERM');
61
+ } catch {
62
+ // ignore
63
+ }
64
+ }
65
+
66
+ export function serveShopify({ cwd = process.cwd() } = {}) {
67
+ const config = readConfig(cwd) || {};
68
+ const store = config.default_store || config.store || '';
69
+ const args = ['theme', 'dev', '--theme-editor-sync'];
70
+ if (store) args.push(`--store=${store}`);
71
+ return runShopify(args, { cwd, name: 'shopify' });
72
+ }
73
+
74
+ export function serveAssets({ cwd = process.cwd(), includeThemeCheck = true } = {}) {
75
+ const env = { ...process.env, NODE_ENV: 'production' };
76
+ const styleEntrypoint = join(cwd, '_styles', 'main.css');
77
+ const tailwind = existsSync(styleEntrypoint)
78
+ ? runTailwind(['-i', '_styles/main.css', '-o', 'assets/style.css', '--watch'], { cwd, env, name: 'tailwind' })
79
+ : null;
80
+
81
+ // Optional dev MCP (non-blocking if missing)
82
+ const devMcp = spawnLogged('npx', ['-y', '@shopify/dev-mcp@latest'], { name: 'dev-mcp', cwd });
83
+
84
+ const scriptsDir = join(cwd, '_scripts');
85
+ if (existsSync(scriptsDir)) {
86
+ try {
87
+ buildScripts({ cwd });
88
+ console.log(pc.green(' scripts built (initial)'));
89
+ } catch (err) {
90
+ console.log(pc.red(` initial scripts build failed: ${err.message}`));
91
+ }
92
+ }
93
+ const scriptsWatch = existsSync(scriptsDir)
94
+ ? watchTree({
95
+ rootDir: scriptsDir,
96
+ ignore: (p) => p.includes('node_modules') || p.includes('/assets/') || p.includes('/.git/'),
97
+ debounceMs: 300,
98
+ onChange: () => {
99
+ try {
100
+ buildScripts({ cwd });
101
+ console.log(pc.green(' scripts rebuilt'));
102
+ } catch (err) {
103
+ console.log(pc.red(` scripts build failed: ${err.message}`));
104
+ }
105
+ },
106
+ })
107
+ : null;
108
+
109
+ let themeCheckRunning = false;
110
+ let themeCheckQueued = false;
111
+ const runThemeCheck = () => {
112
+ if (themeCheckRunning) {
113
+ themeCheckQueued = true;
114
+ return;
115
+ }
116
+ themeCheckRunning = true;
117
+ const child = runShopify(['theme', 'check'], { cwd, name: 'theme-check' });
118
+ child.on('exit', () => {
119
+ themeCheckRunning = false;
120
+ if (themeCheckQueued) {
121
+ themeCheckQueued = false;
122
+ runThemeCheck();
123
+ }
124
+ });
125
+ };
126
+
127
+ const themeCheckWatch =
128
+ includeThemeCheck
129
+ ? watchTree({
130
+ rootDir: cwd,
131
+ ignore: (p) =>
132
+ p.includes('/node_modules/') ||
133
+ p.includes('/assets/') ||
134
+ p.includes('/.git/') ||
135
+ p.includes('/_scripts/') ||
136
+ p.includes('/_styles/'),
137
+ debounceMs: 800,
138
+ onChange: () => {
139
+ runThemeCheck();
140
+ },
141
+ })
142
+ : null;
143
+
144
+ if (includeThemeCheck) {
145
+ runThemeCheck();
146
+ }
147
+
148
+ const cleanup = () => {
149
+ safeClose(scriptsWatch);
150
+ safeClose(themeCheckWatch);
151
+ safeKill(tailwind);
152
+ safeKill(devMcp);
153
+ };
154
+
155
+ return { tailwind, devMcp, scriptsWatch, themeCheckWatch, cleanup };
156
+ }
157
+
158
+ export function serveAll({ cwd = process.cwd(), includeThemeCheck = true } = {}) {
159
+ // Keep Shopify CLI in the foreground (real TTY), and run watchers in background.
160
+ const assets = serveAssets({ cwd, includeThemeCheck });
161
+ const shopify = serveShopify({ cwd });
162
+
163
+ const cleanup = () => {
164
+ assets.cleanup?.();
165
+ safeKill(shopify);
166
+ };
167
+
168
+ const handleSignal = () => cleanup();
169
+ process.once('SIGINT', handleSignal);
170
+ process.once('SIGTERM', handleSignal);
171
+
172
+ shopify.on('exit', () => {
173
+ cleanup();
174
+ });
175
+
176
+ return { shopify, ...assets, cleanup };
177
+ }
178
+
179
+ export function lintAll({ cwd = process.cwd() } = {}) {
180
+ // Keep these intentionally simple wrappers; users can run the underlying tools directly if desired.
181
+ const eslint = spawnLogged(binPath('eslint'), ['./assets/*.js', '--config', '.config/eslint.config.mjs'], {
182
+ name: 'eslint',
183
+ cwd,
184
+ });
185
+ const stylelint = spawnLogged(
186
+ binPath('stylelint'),
187
+ ['./assets/*.css', '--config', '.config/.stylelintrc.json'],
188
+ { name: 'stylelint', cwd }
189
+ );
190
+ const themeCheck = runShopify(['theme', 'check'], { cwd, name: 'theme-check' });
191
+ return { eslint, stylelint, themeCheck };
192
+ }
193
+
194
+ export function buildAll({ cwd = process.cwd() } = {}) {
195
+ const env = { ...process.env, NODE_ENV: 'production' };
196
+ let scriptsOk = true;
197
+ try {
198
+ buildScripts({ cwd });
199
+ } catch (err) {
200
+ console.log(pc.red(`\n build-scripts failed: ${err.message}\n`));
201
+ scriptsOk = false;
202
+ }
203
+ const tailwind = runTailwind(['-i', '_styles/main.css', '-o', 'assets/style.css', '--minify'], {
204
+ cwd,
205
+ env,
206
+ name: 'tailwind',
207
+ });
208
+ return { scriptsOk, tailwind };
209
+ }
210
+
@@ -1,5 +1,6 @@
1
1
  import prompts from 'prompts';
2
2
  import pc from 'picocolors';
3
+ import { basename } from 'node:path';
3
4
 
4
5
  /**
5
6
  * Extract the subdomain (storeKey) from a Shopify domain.
@@ -42,13 +43,13 @@ export async function promptStore(defaultDomain = '') {
42
43
  const { domain } = await prompts({
43
44
  type: 'text',
44
45
  name: 'domain',
45
- message: 'Store URL',
46
+ message: 'Store name or domain',
46
47
  initial: defaultDomain,
47
48
  validate: (v) => {
48
- if (v.trim().length === 0) return 'Store URL is required';
49
+ if (v.trim().length === 0) return 'Store name is required';
49
50
  const normalized = normalizeDomain(v);
50
51
  if (!normalized || !isValidShopifyDomain(normalized)) {
51
- return 'Enter a valid Shopify domain (e.g. voldt-staging.myshopify.com)';
52
+ return 'Enter a valid store name or domain (e.g. voldt-staging or voldt-staging.myshopify.com)';
52
53
  }
53
54
  return true;
54
55
  },
@@ -149,14 +150,14 @@ export async function promptBuildWorkflows() {
149
150
  }
150
151
 
151
152
  /**
152
- * Ask whether to scaffold the local theme dev kit files (scripts, lint, ignores, editor tasks).
153
+ * Ask whether to scaffold local theme dev-kit files (configs, ignores, editor tasks).
153
154
  */
154
155
  export async function promptDevKit() {
155
156
  const { enableDevKit } = await prompts({
156
157
  type: 'confirm',
157
158
  name: 'enableDevKit',
158
159
  message:
159
- 'Install Electric Maybe theme dev kit? (local scripts/watch/lint configs, ignores, and optional VS Code tasks)',
160
+ 'Install Electric Maybe theme dev kit? (local dev config files, ignore defaults, and optional VS Code tasks)',
160
161
  initial: true,
161
162
  });
162
163
 
@@ -170,7 +171,7 @@ export async function promptVSCodeDevTasks() {
170
171
  const { enableVSCodeTasks } = await prompts({
171
172
  type: 'confirm',
172
173
  name: 'enableVSCodeTasks',
173
- message: 'Add VS Code tasks.json to auto-run Shopify + Tailwind local dev tasks?',
174
+ message: 'Add VS Code tasks.json to auto-run climaybe local dev commands (Shopify + assets watch)?',
174
175
  initial: true,
175
176
  });
176
177
 
@@ -206,6 +207,28 @@ export async function promptCursorSkills() {
206
207
  return !!enableCursorSkills;
207
208
  }
208
209
 
210
+ /**
211
+ * Prompt for package.json name when creating a new package.json.
212
+ */
213
+ export async function promptProjectName(cwd = process.cwd()) {
214
+ const suggested = basename(cwd).trim().toLowerCase().replace(/\s+/g, '-');
215
+ const { projectName } = await prompts({
216
+ type: 'text',
217
+ name: 'projectName',
218
+ message: 'Project name for package.json',
219
+ initial: suggested || 'shopify-theme',
220
+ validate: (v) => {
221
+ const name = String(v || '').trim();
222
+ if (!name) return 'Project name is required';
223
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
224
+ return 'Use lowercase letters, numbers, dot, underscore, or hyphen';
225
+ }
226
+ return true;
227
+ },
228
+ });
229
+ return String(projectName || '').trim();
230
+ }
231
+
209
232
  /**
210
233
  * Prompt for a single new store (used by add-store command).
211
234
  * Takes existing aliases to prevent duplicates.
@@ -0,0 +1,30 @@
1
+ import { spawn } from 'node:child_process';
2
+ import pc from 'picocolors';
3
+
4
+ function spawnInherit(cmd, args, { cwd = process.cwd(), name = cmd } = {}) {
5
+ const child = spawn(cmd, args, {
6
+ cwd,
7
+ stdio: 'inherit',
8
+ shell: process.platform === 'win32',
9
+ });
10
+ child.on('exit', (code) => {
11
+ if (code && code !== 0) {
12
+ console.log(pc.red(`\n ${name} exited with code ${code}\n`));
13
+ }
14
+ });
15
+ return child;
16
+ }
17
+
18
+ /**
19
+ * Run Shopify CLI, falling back to npx when `shopify` isn't available.
20
+ * @param {string[]} args e.g. ['theme','check']
21
+ */
22
+ export function runShopify(args, { cwd = process.cwd(), name = 'shopify' } = {}) {
23
+ const child = spawnInherit('shopify', args, { cwd, name });
24
+ child.on('error', (err) => {
25
+ if (err?.code !== 'ENOENT') return;
26
+ spawnInherit('npx', ['-y', '@shopify/cli@latest', ...args], { cwd, name: 'shopify(npx)' });
27
+ });
28
+ return child;
29
+ }
30
+
@@ -1,24 +1,12 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join, dirname } from 'node:path';
3
- import { readPkg, writePkg } from './config.js';
3
+ import { readPkg, writePkg, writeClimaybeConfig } from './config.js';
4
+ import { getLatestTagVersion } from './git.js';
4
5
 
5
6
  const DEV_KIT_FILES = {
6
- 'nodemon.json': `{
7
- "watch": ["_scripts"],
8
- "ext": "js",
9
- "exec": "npm run scripts:build --silent",
10
- "quiet": true,
11
- "no-colours": true,
12
- "ignore": ["node_modules/**/*", "assets/**/*", "**/*.min.js"],
13
- "delay": "500",
14
- "polling": false,
15
- "legacyWatch": false,
16
- "restartable": "rs"
17
- }
18
- `,
19
7
  '.theme-check.yml': `root: .
20
8
 
21
- extends: :nothing
9
+ extends: theme-check:recommended
22
10
 
23
11
  ignore:
24
12
  - node_modules/*
@@ -80,7 +68,7 @@ const VSCODE_TASKS_CONTENT = `{
80
68
  {
81
69
  "label": "Shopify",
82
70
  "type": "shell",
83
- "command": "yarn shopify:serve",
71
+ "command": "climaybe serve:shopify",
84
72
  "isBackground": true,
85
73
  "presentation": {
86
74
  "echo": true,
@@ -96,7 +84,7 @@ const VSCODE_TASKS_CONTENT = `{
96
84
  {
97
85
  "label": "Tailwind",
98
86
  "type": "shell",
99
- "command": "yarn tailwind:watch",
87
+ "command": "climaybe serve:assets",
100
88
  "isBackground": true,
101
89
  "presentation": {
102
90
  "echo": true,
@@ -122,39 +110,13 @@ const VSCODE_TASKS_CONTENT = `{
122
110
 
123
111
  const GITIGNORE_BLOCK = `# climaybe: theme dev kit (managed)
124
112
  .vscode
113
+ node_modules/
125
114
  assets/style.css
126
115
  assets/index.js
127
116
  .shopify
128
117
  .vercel
129
118
  `;
130
119
 
131
- const PACKAGE_MERGES = {
132
- scripts: {
133
- 'shopify:serve': 'shopify theme dev --theme-editor-sync --store=$npm_package_config_store',
134
- 'shopify:populate': 'shopify populate --store=$npm_package_config_store',
135
- 'scripts:build': 'node build-scripts.js',
136
- 'scripts:watch': 'nodemon',
137
- 'tailwind:watch':
138
- `concurrently --kill-others --max-restarts 3 "NODE_ENV=production NODE_OPTIONS='--max-old-space-size=512' ` +
139
- `npx @tailwindcss/cli -i _styles/main.css -o assets/style.css --watch" "NODE_OPTIONS='--max-old-space-size=256' ` +
140
- `npm run scripts:watch" "npx -y @shopify/dev-mcp@latest"`,
141
- 'tailwind:build': 'NODE_ENV=production npx @tailwindcss/cli -i _styles/main.css -o assets/style.css --minify && npm run scripts:build',
142
- 'lint:liquid': 'shopify theme check',
143
- 'lint:js': 'eslint ./assets/*.js --config .config/eslint.config.mjs',
144
- 'lint:css': 'node_modules/.bin/stylelint ./assets/*.css --config .config/.stylelintrc.json',
145
- release: 'node .sys/scripts/release.js',
146
- },
147
- devDependencies: {
148
- '@shopify/prettier-plugin-liquid': '^1.6.3',
149
- '@tailwindcss/cli': '^4.1.17',
150
- concurrently: '^8.2.2',
151
- nodemon: '^3.0.2',
152
- prettier: '^3.4.2',
153
- stylelint: '^16.9.0',
154
- eslint: '^9.11.0',
155
- },
156
- };
157
-
158
120
  function ensureParent(path) {
159
121
  mkdirSync(dirname(path), { recursive: true });
160
122
  }
@@ -171,12 +133,32 @@ function mergeGitignore(cwd = process.cwd()) {
171
133
  writeFileSync(path, `${next}\n`, 'utf-8');
172
134
  }
173
135
 
174
- function mergePackageJson(defaultStoreDomain = '', cwd = process.cwd()) {
175
- const pkg = readPkg(cwd) || { name: 'shopify-theme', version: '1.0.0', private: true };
176
- pkg.config = { ...(pkg.config || {}) };
177
- if (!pkg.config.store && defaultStoreDomain) pkg.config.store = defaultStoreDomain;
178
- pkg.scripts = { ...(pkg.scripts || {}), ...PACKAGE_MERGES.scripts };
179
- pkg.devDependencies = { ...(pkg.devDependencies || {}), ...PACKAGE_MERGES.devDependencies };
136
+ function mergePackageJson({ packageName = 'shopify-theme', cwd = process.cwd() } = {}) {
137
+ let pkg = readPkg(cwd);
138
+ if (!pkg) {
139
+ let version = '0.1.0';
140
+ try {
141
+ const fromTags = getLatestTagVersion(cwd);
142
+ if (fromTags) version = fromTags;
143
+ } catch {
144
+ // not a git repo or no semver tags found
145
+ }
146
+ pkg = { name: packageName, version, private: true };
147
+ }
148
+ if (!pkg.description) {
149
+ pkg.description = 'Customizable modular development environment for blazing-fast Shopify theme creation';
150
+ }
151
+ if (!pkg.author) {
152
+ pkg.author = 'Electric Maybe <hello@electricmaybe.com>';
153
+ }
154
+ pkg.devDependencies = { ...(pkg.devDependencies || {}) };
155
+
156
+ // Ensure teammates can run climaybe + Tailwind after plain npm install.
157
+ const cliVersion = process.env.CLIMAYBE_PACKAGE_VERSION;
158
+ const climaybeRange = /^\d+\.\d+\.\d+/.test(String(cliVersion || '')) ? `^${cliVersion}` : 'latest';
159
+ if (!pkg.devDependencies.climaybe) pkg.devDependencies.climaybe = climaybeRange;
160
+ if (!pkg.devDependencies.tailwindcss) pkg.devDependencies.tailwindcss = 'latest';
161
+
180
162
  writePkg(pkg, cwd);
181
163
  }
182
164
 
@@ -186,7 +168,12 @@ export function getDevKitExistingFiles({ includeVSCodeTasks = true, cwd = proces
186
168
  return paths.filter((p) => existsSync(join(cwd, p)));
187
169
  }
188
170
 
189
- export function scaffoldThemeDevKit({ includeVSCodeTasks = true, defaultStoreDomain = '', cwd = process.cwd() } = {}) {
171
+ export function scaffoldThemeDevKit({
172
+ includeVSCodeTasks = true,
173
+ defaultStoreDomain = '',
174
+ packageName = 'shopify-theme',
175
+ cwd = process.cwd(),
176
+ } = {}) {
190
177
  for (const [rel, content] of Object.entries(DEV_KIT_FILES)) {
191
178
  const dest = join(cwd, rel);
192
179
  ensureParent(dest);
@@ -198,5 +185,18 @@ export function scaffoldThemeDevKit({ includeVSCodeTasks = true, defaultStoreDom
198
185
  writeFileSync(dest, VSCODE_TASKS_CONTENT, 'utf-8');
199
186
  }
200
187
  mergeGitignore(cwd);
201
- mergePackageJson(defaultStoreDomain, cwd);
188
+ mergePackageJson({ packageName, cwd });
189
+
190
+ // New source-of-truth config file for climaybe (local dev + CI).
191
+ // We intentionally keep package.json changes minimal (no script injection).
192
+ writeClimaybeConfig(
193
+ {
194
+ port: 9295,
195
+ default_store: defaultStoreDomain || undefined,
196
+ dev_kit: true,
197
+ vscode_tasks: includeVSCodeTasks,
198
+ project_type: 'theme',
199
+ },
200
+ cwd
201
+ );
202
202
  }
@@ -0,0 +1,80 @@
1
+ import { existsSync, readdirSync, watch } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ function debounce(fn, ms) {
5
+ let t = null;
6
+ return (...args) => {
7
+ if (t) clearTimeout(t);
8
+ t = setTimeout(() => fn(...args), ms);
9
+ };
10
+ }
11
+
12
+ function listDirsRecursively(rootDir) {
13
+ const dirs = [];
14
+ if (!existsSync(rootDir)) return dirs;
15
+
16
+ const stack = [rootDir];
17
+ while (stack.length) {
18
+ const dir = stack.pop();
19
+ dirs.push(dir);
20
+ let entries = [];
21
+ try {
22
+ entries = readdirSync(dir, { withFileTypes: true });
23
+ } catch {
24
+ continue;
25
+ }
26
+ for (const e of entries) {
27
+ if (!e.isDirectory()) continue;
28
+ const full = join(dir, e.name);
29
+ stack.push(full);
30
+ }
31
+ }
32
+ return dirs;
33
+ }
34
+
35
+ export function watchTree({ rootDir, ignore = () => false, onChange, debounceMs = 300 } = {}) {
36
+ if (!rootDir) throw new Error('watchTree: rootDir is required');
37
+ if (typeof onChange !== 'function') throw new Error('watchTree: onChange is required');
38
+ if (!existsSync(rootDir)) return { close: () => {} };
39
+
40
+ const watchers = new Map();
41
+ const debounced = debounce(onChange, debounceMs);
42
+
43
+ function ensureWatched(dir) {
44
+ if (watchers.has(dir)) return;
45
+ try {
46
+ const w = watch(dir, { persistent: true }, (eventType, filename) => {
47
+ const name = typeof filename === 'string' ? filename : '';
48
+ const full = name ? join(dir, name) : dir;
49
+ if (ignore(full)) return;
50
+ debounced(full, eventType);
51
+ // Best-effort: new dirs can appear; rescan on any event.
52
+ rescan();
53
+ });
54
+ watchers.set(dir, w);
55
+ } catch {
56
+ // ignore unwatcheable dirs
57
+ }
58
+ }
59
+
60
+ function rescan() {
61
+ const dirs = listDirsRecursively(rootDir);
62
+ for (const d of dirs) ensureWatched(d);
63
+ }
64
+
65
+ rescan();
66
+
67
+ return {
68
+ close() {
69
+ for (const w of watchers.values()) {
70
+ try {
71
+ w.close();
72
+ } catch {
73
+ // ignore
74
+ }
75
+ }
76
+ watchers.clear();
77
+ },
78
+ };
79
+ }
80
+
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, copyFileSync, rmSync } from 'node:f
2
2
  import { join, dirname } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import pc from 'picocolors';
5
- import { installBuildScript, removeBuildScript } from './build-workflows.js';
5
+ import { ensureBuildWorkflowDefaults } from './build-workflows.js';
6
6
 
7
7
  const __dirname = dirname(fileURLToPath(import.meta.url));
8
8
  const TEMPLATES_DIR = join(__dirname, '..', 'workflows');
@@ -110,9 +110,7 @@ export function scaffoldWorkflows(mode = 'single', options = {}, cwd = process.c
110
110
  for (const f of listYmls(buildDir)) {
111
111
  copyWorkflow(buildDir, f, dest);
112
112
  }
113
- installBuildScript(cwd);
114
- } else {
115
- removeBuildScript(cwd);
113
+ ensureBuildWorkflowDefaults(cwd);
116
114
  }
117
115
 
118
116
  const total = readdirSync(dest).filter((f) => f.endsWith('.yml')).length;
@@ -119,9 +119,18 @@ jobs:
119
119
  COMMITS=$(git log --pretty=format:"%s" -n 30 "$TAG_NAME" 2>/dev/null || true)
120
120
  fi
121
121
 
122
+ # Filter out noisy system-generated sync/merge commits so release notes stay readable.
123
+ FILTERED_COMMITS=$(printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | grep -Eiv \
124
+ '^Sync main → staging-|^Sync main -> staging-|^Merge pull request #|^Merge branch |^Merge live-.* into main \[hotfix-backport\]$|^chore: sync root to stores/.+ \[root-to-stores\]$|^chore: keep store root JSONs from stores/.+ \[stores-to-root\]$|^Update from Shopify for theme ')
125
+ if [ -z "$FILTERED_COMMITS" ]; then
126
+ NOISY_ONLY_COMMITS=true
127
+ else
128
+ NOISY_ONLY_COMMITS=false
129
+ fi
130
+
122
131
  # Score how many commit subjects follow conventional-commit style.
123
- TOTAL_COUNT=$(printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
124
- CONVENTIONAL_COUNT=$(printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | grep -Eic '^(feat|fix|refactor|perf|docs|style|test|build|ci|chore|revert)(\([^)]+\))?!?: ')
132
+ TOTAL_COUNT=$(printf '%s\n' "$FILTERED_COMMITS" | sed '/^[[:space:]]*$/d' | wc -l | tr -d ' ')
133
+ CONVENTIONAL_COUNT=$(printf '%s\n' "$FILTERED_COMMITS" | sed '/^[[:space:]]*$/d' | grep -Eic '^(feat|fix|refactor|perf|docs|style|test|build|ci|chore|revert)(\([^)]+\))?!?: ')
125
134
  if [ -z "$TOTAL_COUNT" ] || [ "$TOTAL_COUNT" -eq 0 ]; then
126
135
  TOTAL_COUNT=1
127
136
  fi
@@ -139,8 +148,8 @@ jobs:
139
148
  } > "$NOTES_FILE"
140
149
 
141
150
  # If commit quality is low and Gemini is available, ask AI to summarize merchant-facing notes.
142
- if [ "$QUALITY" -lt 50 ] && [ -n "$GEMINI_API_KEY" ]; then
143
- printf '%s\n' "$COMMITS" > .sys/release-notes/commits-for-ai.txt
151
+ if [ "$QUALITY" -lt 50 ] && [ -n "$GEMINI_API_KEY" ] && [ "$NOISY_ONLY_COMMITS" != "true" ]; then
152
+ printf '%s\n' "$FILTERED_COMMITS" > .sys/release-notes/commits-for-ai.txt
144
153
 
145
154
  PROMPT=$(cat <<'PROMPT_EOF'
146
155
  You generate release notes for a Shopify theme.
@@ -182,12 +191,20 @@ jobs:
182
191
  else
183
192
  echo "## Changes" >> "$NOTES_FILE"
184
193
  echo >> "$NOTES_FILE"
185
- printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | sed 's/^/- /' >> "$NOTES_FILE"
194
+ if [ "$NOISY_ONLY_COMMITS" = "true" ]; then
195
+ echo "- No notable merchant-facing changes in this release." >> "$NOTES_FILE"
196
+ else
197
+ printf '%s\n' "$FILTERED_COMMITS" | sed '/^[[:space:]]*$/d' | sed 's/^/- /' >> "$NOTES_FILE"
198
+ fi
186
199
  fi
187
200
  else
188
201
  echo "## Changes" >> "$NOTES_FILE"
189
202
  echo >> "$NOTES_FILE"
190
- printf '%s\n' "$COMMITS" | sed '/^[[:space:]]*$/d' | sed 's/^/- /' >> "$NOTES_FILE"
203
+ if [ "$NOISY_ONLY_COMMITS" = "true" ]; then
204
+ echo "- No notable merchant-facing changes in this release." >> "$NOTES_FILE"
205
+ else
206
+ printf '%s\n' "$FILTERED_COMMITS" | sed '/^[[:space:]]*$/d' | sed 's/^/- /' >> "$NOTES_FILE"
207
+ fi
191
208
  fi
192
209
 
193
210
  echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT
@@ -22,18 +22,33 @@ jobs:
22
22
  uses: actions/setup-node@v4
23
23
  with:
24
24
  node-version: "24"
25
- cache: "npm"
26
25
 
27
- - name: Install dependencies
28
- run: npm ci
26
+ - name: Detect build entrypoints
27
+ id: detect
28
+ run: |
29
+ HAS_SCRIPTS=false
30
+ if ls _scripts/*.js >/dev/null 2>&1; then
31
+ HAS_SCRIPTS=true
32
+ fi
33
+ HAS_STYLES=false
34
+ if [ -f "_styles/main.css" ]; then
35
+ HAS_STYLES=true
36
+ fi
37
+ echo "has_scripts=$HAS_SCRIPTS" >> $GITHUB_OUTPUT
38
+ echo "has_styles=$HAS_STYLES" >> $GITHUB_OUTPUT
29
39
 
30
40
  - name: Build scripts
31
- run: node .climaybe/build-scripts.js
41
+ if: steps.detect.outputs.has_scripts == 'true'
42
+ run: npx -y climaybe@latest build-scripts
32
43
 
33
44
  - name: Run Tailwind build
34
45
  id: build
35
46
  run: |
36
- NODE_ENV=production npx @tailwindcss/cli -i _styles/main.css -o assets/style.css --minify
47
+ if [ "${{ steps.detect.outputs.has_styles }}" = "true" ]; then
48
+ NODE_ENV=production npx -y climaybe@latest build
49
+ else
50
+ echo "No _styles/main.css found; skipping Tailwind build."
51
+ fi
37
52
  echo "success=true" >> $GITHUB_OUTPUT
38
53
 
39
54
  - name: Commit and push changes
@@ -43,7 +58,12 @@ jobs:
43
58
  BRANCH_NAME=${{ github.head_ref || github.ref_name }}
44
59
  git fetch origin "$BRANCH_NAME" || echo "Branch does not exist yet"
45
60
  git checkout -B "$BRANCH_NAME" "origin/$BRANCH_NAME" 2>/dev/null || git checkout -B "$BRANCH_NAME"
46
- git add -f assets/index.js assets/style.css
61
+ if ls assets/*.js >/dev/null 2>&1; then
62
+ git add -f assets/*.js
63
+ fi
64
+ if [ -f "assets/style.css" ]; then
65
+ git add -f assets/style.css
66
+ fi
47
67
  if git diff --staged --quiet; then
48
68
  echo "No changes to commit"
49
69
  else