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.
- package/README.md +25 -26
- package/bin/version.txt +1 -1
- package/package.json +9 -1
- package/src/commands/add-dev-kit.js +5 -3
- package/src/commands/add-store.js +22 -7
- package/src/commands/app-init.js +1 -1
- package/src/commands/build-scripts.js +27 -0
- package/src/commands/create-entrypoints.js +45 -0
- package/src/commands/ensure-branches.js +4 -9
- package/src/commands/init.js +69 -18
- package/src/commands/migrate-legacy-config.js +217 -0
- package/src/index.js +37 -2
- package/src/lib/build-scripts.js +153 -0
- package/src/lib/build-workflows.js +3 -43
- package/src/lib/config.js +78 -8
- package/src/lib/dev-runtime.js +210 -0
- package/src/lib/prompts.js +29 -6
- package/src/lib/shopify-cli.js +30 -0
- package/src/lib/theme-dev-kit.js +52 -52
- package/src/lib/watch.js +80 -0
- package/src/lib/workflows.js +2 -4
- package/src/workflows/build/create-release.yml +23 -6
- package/src/workflows/build/reusable-build.yml +26 -6
- package/src/workflows/multi/main-to-staging-stores.yml +4 -3
- package/src/workflows/multi/multistore-hotfix-to-main.yml +3 -3
- package/src/workflows/multi/pr-to-live.yml +3 -3
- package/src/workflows/preview/pr-close.yml +4 -4
- package/src/workflows/preview/pr-update.yml +4 -4
|
@@ -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
|
+
|
package/src/lib/prompts.js
CHANGED
|
@@ -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
|
|
46
|
+
message: 'Store name or domain',
|
|
46
47
|
initial: defaultDomain,
|
|
47
48
|
validate: (v) => {
|
|
48
|
-
if (v.trim().length === 0) return 'Store
|
|
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
|
|
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
|
|
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
|
|
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 +
|
|
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
|
+
|
package/src/lib/theme-dev-kit.js
CHANGED
|
@@ -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: :
|
|
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": "
|
|
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": "
|
|
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(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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({
|
|
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(
|
|
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
|
}
|
package/src/lib/watch.js
ADDED
|
@@ -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
|
+
|
package/src/lib/workflows.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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' "$
|
|
124
|
-
CONVENTIONAL_COUNT=$(printf '%s\n' "$
|
|
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' "$
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|