climaybe 2.4.2 → 3.0.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.
- 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/init.js +52 -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 +188 -0
- package/src/lib/prompts.js +29 -6
- package/src/lib/shopify-cli.js +30 -0
- package/src/lib/theme-dev-kit.js +33 -51
- 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 -5
- 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
package/README.md
CHANGED
|
@@ -13,10 +13,10 @@ Built by [Electric Maybe](https://electricmaybe.com) — a Shopify-focused produ
|
|
|
13
13
|
|
|
14
14
|
- **`climaybe theme <command>`** — canonical commands for theme repos (workflows, stores, branches).
|
|
15
15
|
- **Same commands at the top level** — `climaybe init` is the same as `climaybe theme init` (backward compatible).
|
|
16
|
-
- **`climaybe app init`** — app repos only: writes `project_type: "app"` in `
|
|
16
|
+
- **`climaybe app init`** — app repos only: writes `project_type: "app"` in `climaybe.config.json`, optional commitlint + Cursor bundle. Does **not** install theme GitHub Actions or store/branch setup.
|
|
17
17
|
- **`climaybe setup-commitlint`** and **`climaybe add-cursor`** — always at the top level (stack-agnostic).
|
|
18
18
|
|
|
19
|
-
Theme-only commands refuse to run when `
|
|
19
|
+
Theme-only commands refuse to run when `climaybe.config.json` → `project_type` is **`app`**.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -64,7 +64,7 @@ Interactive setup that configures your repo for CI/CD.
|
|
|
64
64
|
6. Asks whether to enable **commitlint + Husky** (enforce [conventional commits](https://www.conventionalcommits.org/) on `git commit`)
|
|
65
65
|
7. Asks whether to install the **Cursor bundle** (`.cursor/rules/`, `.cursor/skills/`, `.cursor/agents/`) — Electric Maybe conventions for themes and AI workflows
|
|
66
66
|
8. Based on store count, sets up **single-store** or **multi-store** mode
|
|
67
|
-
9. Writes `
|
|
67
|
+
9. Writes `climaybe.config.json`
|
|
68
68
|
10. Scaffolds GitHub Actions workflows
|
|
69
69
|
11. Creates git branches and store directories (multi-store)
|
|
70
70
|
12. Optionally installs commitlint, Husky, and the Cursor bundle (rules, skills, agents)
|
|
@@ -145,21 +145,19 @@ The previous command name `add-cursor-skill` still works as an alias. Re-running
|
|
|
145
145
|
|
|
146
146
|
## Configuration
|
|
147
147
|
|
|
148
|
-
The CLI writes config into
|
|
148
|
+
The CLI writes config into `climaybe.config.json`:
|
|
149
149
|
|
|
150
150
|
```json
|
|
151
151
|
{
|
|
152
|
-
"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
161
|
-
"voldt-norway": "voldt-norway.myshopify.com"
|
|
162
|
-
}
|
|
152
|
+
"port": 9295,
|
|
153
|
+
"default_store": "voldt-staging.myshopify.com",
|
|
154
|
+
"preview_workflows": true,
|
|
155
|
+
"build_workflows": true,
|
|
156
|
+
"commitlint": true,
|
|
157
|
+
"cursor_skills": true,
|
|
158
|
+
"stores": {
|
|
159
|
+
"voldt-staging": "voldt-staging.myshopify.com",
|
|
160
|
+
"voldt-norway": "voldt-norway.myshopify.com"
|
|
163
161
|
}
|
|
164
162
|
}
|
|
165
163
|
```
|
|
@@ -235,27 +233,28 @@ Enabled via `climaybe init` prompt (`Enable preview + cleanup workflows?`; defau
|
|
|
235
233
|
|
|
236
234
|
Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`; default: yes).
|
|
237
235
|
|
|
238
|
-
When enabled,
|
|
239
|
-
- `_scripts/main.
|
|
240
|
-
- `
|
|
236
|
+
When enabled, builds are **resilient**:
|
|
237
|
+
- If `_scripts/*.js` or `_styles/main.css` are missing, the build workflow **skips** those steps and continues.
|
|
238
|
+
- `init` may offer to create entrypoints; **default answer is No**.
|
|
241
239
|
|
|
242
|
-
`
|
|
243
|
-
- `assets/`
|
|
244
|
-
|
|
245
|
-
`climaybe` auto-installs the shared build script at `.climaybe/build-scripts.js` during workflow scaffolding.
|
|
240
|
+
Build workflows run bundling via `npx -y climaybe@latest build-scripts` and Tailwind via `npx -y climaybe@latest build` (no per-repo build script is installed).
|
|
246
241
|
|
|
247
242
|
| Workflow | Trigger | What it does |
|
|
248
243
|
|----------|---------|-------------|
|
|
249
244
|
| `build-pipeline.yml` | Push to any branch | Runs reusable build and Lighthouse checks (when required secrets exist) |
|
|
250
245
|
| `reusable-build.yml` | workflow_call | Runs Node build + Tailwind compile, then commits compiled assets when changed |
|
|
251
|
-
| `create-release.yml` | Push tag `v*`, or **workflow_run** after Post-Merge Tag / Nightly Hotfix Tag succeed on `main` | Builds release archive and creates GitHub Release notes from commits since the previous tag. If
|
|
246
|
+
| `create-release.yml` | Push tag `v*`, or **workflow_run** after Post-Merge Tag / Nightly Hotfix Tag succeed on `main` | Builds release archive and creates GitHub Release notes from commits since the previous tag. It filters repetitive automation subjects (main→staging syncs, store/root sync chores, bot merge noise) before generating notes. If remaining subjects are low-signal and `GEMINI_API_KEY` exists, it uses Gemini to generate cleaner merchant-facing notes. Also covers tags created by workflows with `GITHUB_TOKEN`, which may not trigger tag-push workflows. |
|
|
252
247
|
|
|
253
248
|
### Optional theme dev kit package
|
|
254
249
|
|
|
255
250
|
During `climaybe init`, you can enable the Electric Maybe theme dev kit (default: yes). This installs local
|
|
256
|
-
dev
|
|
257
|
-
`.lighthouserc.js`),
|
|
258
|
-
|
|
251
|
+
dev config defaults (`.theme-check.yml`, `.shopifyignore`, `.prettierrc`,
|
|
252
|
+
`.lighthouserc.js`), writes `climaybe.config.json`, appends a managed `.gitignore` block, and optionally adds
|
|
253
|
+
`.vscode/tasks.json` (default: yes) wired to run `climaybe` dev commands.
|
|
254
|
+
|
|
255
|
+
You can create optional build entrypoints later with:
|
|
256
|
+
|
|
257
|
+
`climaybe create-entrypoints`
|
|
259
258
|
|
|
260
259
|
If these files already exist, `init` warns that they will be replaced.
|
|
261
260
|
|
package/bin/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
3.0.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "climaybe",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Shopify CLI by Electric Maybe for theme CI/CD workflows, branch orchestration, app setup, and dev tooling",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -78,6 +78,14 @@
|
|
|
78
78
|
]
|
|
79
79
|
},
|
|
80
80
|
"dependencies": {
|
|
81
|
+
"@shopify/prettier-plugin-liquid": "^1.6.3",
|
|
82
|
+
"@tailwindcss/cli": "^4.1.17",
|
|
83
|
+
"@tailwindcss/typography": "^0.5.16",
|
|
84
|
+
"eslint": "^9.11.0",
|
|
85
|
+
"prettier": "^3.4.2",
|
|
86
|
+
"stylelint": "^16.9.0",
|
|
87
|
+
"stylelint-config-standard": "^36.0.1",
|
|
88
|
+
"tailwindcss": "^4.1.18",
|
|
81
89
|
"commander": "^14.0.3",
|
|
82
90
|
"picocolors": "^1.1.1",
|
|
83
91
|
"prompts": "^2.4.2"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import prompts from 'prompts';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
-
import { readConfig, writeConfig } from '../lib/config.js';
|
|
3
|
+
import { readConfig, readPkg, writeConfig } from '../lib/config.js';
|
|
4
4
|
import { getDevKitExistingFiles, scaffoldThemeDevKit } from '../lib/theme-dev-kit.js';
|
|
5
|
-
import { promptVSCodeDevTasks } from '../lib/prompts.js';
|
|
5
|
+
import { promptProjectName, promptVSCodeDevTasks } from '../lib/prompts.js';
|
|
6
6
|
|
|
7
7
|
export async function addDevKitCommand() {
|
|
8
8
|
console.log(pc.bold('\n climaybe — Add theme dev kit\n'));
|
|
@@ -25,12 +25,14 @@ export async function addDevKitCommand() {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const config = readConfig() || {};
|
|
28
|
+
const projectName = !readPkg() ? await promptProjectName() : undefined;
|
|
28
29
|
scaffoldThemeDevKit({
|
|
29
30
|
includeVSCodeTasks,
|
|
30
31
|
defaultStoreDomain: config.default_store || '',
|
|
32
|
+
packageName: projectName || undefined,
|
|
31
33
|
});
|
|
32
34
|
writeConfig({ dev_kit: true, vscode_tasks: includeVSCodeTasks });
|
|
33
35
|
|
|
34
36
|
console.log(pc.green(' Theme dev kit installed.'));
|
|
35
|
-
console.log(pc.dim(' Added
|
|
37
|
+
console.log(pc.dim(' Added local dev-kit config files and ignore defaults.\n'));
|
|
36
38
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
-
import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue
|
|
2
|
+
import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue } from '../lib/prompts.js';
|
|
3
3
|
import {
|
|
4
4
|
readConfig,
|
|
5
5
|
addStoreToConfig,
|
|
@@ -119,23 +119,38 @@ export async function addStoreCommand() {
|
|
|
119
119
|
console.log(pc.cyan(`\n Configure ${total} secret(s) for store "${store.alias}" (theme token required).\n`));
|
|
120
120
|
for (let i = 0; i < secretsToPrompt.length; i++) {
|
|
121
121
|
const secret = secretsToPrompt[i];
|
|
122
|
-
const value = await promptSecretValue(secret, i, total);
|
|
123
|
-
if (!value) continue;
|
|
124
|
-
|
|
125
122
|
const isThemeToken = secret.name === 'SHOPIFY_THEME_ACCESS_TOKEN' || secret.name.startsWith('SHOPIFY_THEME_ACCESS_TOKEN_');
|
|
126
123
|
if (isThemeToken && store.domain) {
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
// Theme tokens are required and must validate; keep prompting until valid + set.
|
|
125
|
+
while (true) {
|
|
126
|
+
const value = await promptSecretValue(secret, i, total);
|
|
127
|
+
if (!value) {
|
|
128
|
+
console.log(pc.red(' Theme access token is required.'));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
129
131
|
const result = await validateThemeAccessToken(store.domain, value);
|
|
130
132
|
if (!result.ok) {
|
|
131
133
|
console.log(pc.red(` Token test failed: ${result.error}`));
|
|
132
|
-
console.log(pc.dim('
|
|
134
|
+
console.log(pc.dim(' Please try again with a valid Theme Access token.'));
|
|
133
135
|
continue;
|
|
134
136
|
}
|
|
135
137
|
console.log(pc.green(' Token validated against store.'));
|
|
138
|
+
try {
|
|
139
|
+
await setter.set(secret.name, value);
|
|
140
|
+
console.log(pc.green(` Set ${secret.name}.`));
|
|
141
|
+
setCount++;
|
|
142
|
+
break;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
|
|
145
|
+
console.log(pc.dim(' Please try entering the token again.'));
|
|
146
|
+
}
|
|
136
147
|
}
|
|
148
|
+
continue;
|
|
137
149
|
}
|
|
138
150
|
|
|
151
|
+
const value = await promptSecretValue(secret, i, total);
|
|
152
|
+
if (!value) continue;
|
|
153
|
+
|
|
139
154
|
try {
|
|
140
155
|
await setter.set(secret.name, value);
|
|
141
156
|
console.log(pc.green(` Set ${secret.name}.`));
|
package/src/commands/app-init.js
CHANGED
|
@@ -26,7 +26,7 @@ async function runAppInitFlow() {
|
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
writeConfig(config, process.cwd(), { defaultPackageName: 'shopify-app' });
|
|
29
|
-
console.log(pc.green(' Updated
|
|
29
|
+
console.log(pc.green(' Updated climaybe config.'));
|
|
30
30
|
|
|
31
31
|
ensureGitRepo();
|
|
32
32
|
ensureInitialCommit();
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { requireThemeProject } from '../lib/theme-guard.js';
|
|
3
|
+
import { buildScripts } from '../lib/build-scripts.js';
|
|
4
|
+
|
|
5
|
+
export async function buildScriptsCommand() {
|
|
6
|
+
console.log(pc.bold('\n climaybe — Build scripts\n'));
|
|
7
|
+
if (!requireThemeProject()) return;
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
if (global.gc) global.gc();
|
|
11
|
+
const { bundles } = buildScripts({ cwd: process.cwd() });
|
|
12
|
+
if (!bundles || bundles.length === 0) {
|
|
13
|
+
console.log(pc.yellow(' No _scripts/*.js entrypoints found; nothing to build.'));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const totalFiles = bundles.reduce((sum, b) => sum + (b.fileCount || 0), 0);
|
|
17
|
+
console.log(pc.green(` Scripts built (${bundles.length} bundle(s), ${totalFiles} files total)`));
|
|
18
|
+
for (const b of bundles) {
|
|
19
|
+
console.log(pc.dim(` - ${b.entryFile} → ${b.outputPath}`));
|
|
20
|
+
}
|
|
21
|
+
if (global.gc) global.gc();
|
|
22
|
+
} catch (err) {
|
|
23
|
+
console.log(pc.red(` Build error: ${err.message}`));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { requireThemeProject } from '../lib/theme-guard.js';
|
|
5
|
+
|
|
6
|
+
const MAIN_JS = `// climaybe entrypoint: global theme JS
|
|
7
|
+
// Add imports here for code you want on every page.
|
|
8
|
+
// Top-level files in _scripts/ (besides main.js) are treated as separate bundles.
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
const MAIN_CSS = `@import "tailwindcss";
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
export async function createEntrypointsCommand() {
|
|
15
|
+
console.log(pc.bold('\n climaybe — Create entrypoints\n'));
|
|
16
|
+
if (!requireThemeProject()) return;
|
|
17
|
+
|
|
18
|
+
const scriptsDir = join(process.cwd(), '_scripts');
|
|
19
|
+
const stylesDir = join(process.cwd(), '_styles');
|
|
20
|
+
const assetsDir = join(process.cwd(), 'assets');
|
|
21
|
+
|
|
22
|
+
mkdirSync(scriptsDir, { recursive: true });
|
|
23
|
+
mkdirSync(stylesDir, { recursive: true });
|
|
24
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
const mainJsPath = join(scriptsDir, 'main.js');
|
|
27
|
+
const mainCssPath = join(stylesDir, 'main.css');
|
|
28
|
+
|
|
29
|
+
if (!existsSync(mainJsPath)) {
|
|
30
|
+
writeFileSync(mainJsPath, MAIN_JS, 'utf-8');
|
|
31
|
+
console.log(pc.green(' Created _scripts/main.js'));
|
|
32
|
+
} else {
|
|
33
|
+
console.log(pc.dim(' _scripts/main.js already exists (skipped)'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!existsSync(mainCssPath)) {
|
|
37
|
+
writeFileSync(mainCssPath, MAIN_CSS, 'utf-8');
|
|
38
|
+
console.log(pc.green(' Created _styles/main.css'));
|
|
39
|
+
} else {
|
|
40
|
+
console.log(pc.dim(' _styles/main.css already exists (skipped)'));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(pc.dim('\n Next: run `climaybe build` or `climaybe serve`.\n'));
|
|
44
|
+
}
|
|
45
|
+
|
package/src/commands/init.js
CHANGED
|
@@ -6,20 +6,20 @@ import {
|
|
|
6
6
|
promptBuildWorkflows,
|
|
7
7
|
promptDevKit,
|
|
8
8
|
promptVSCodeDevTasks,
|
|
9
|
+
promptProjectName,
|
|
9
10
|
promptCommitlint,
|
|
10
11
|
promptCursorSkills,
|
|
11
12
|
promptConfigureCISecrets,
|
|
12
13
|
promptUpdateExistingSecrets,
|
|
13
14
|
promptSecretValue,
|
|
14
|
-
promptTestThemeToken,
|
|
15
15
|
} from '../lib/prompts.js';
|
|
16
|
-
import { readConfig, writeConfig, getProjectType } from '../lib/config.js';
|
|
16
|
+
import { readConfig, writeConfig, getProjectType, readPkg } from '../lib/config.js';
|
|
17
17
|
import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches, getSuggestedTagForRelease } from '../lib/git.js';
|
|
18
18
|
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
19
19
|
import { createStoreDirectories } from '../lib/store-sync.js';
|
|
20
20
|
import { scaffoldCommitlint } from '../lib/commit-tooling.js';
|
|
21
21
|
import { scaffoldCursorBundle } from '../lib/cursor-bundle.js';
|
|
22
|
-
import { getMissingBuildWorkflowRequirements,
|
|
22
|
+
import { getMissingBuildWorkflowRequirements, ensureBuildWorkflowDefaults } from '../lib/build-workflows.js';
|
|
23
23
|
import { getDevKitExistingFiles, scaffoldThemeDevKit } from '../lib/theme-dev-kit.js';
|
|
24
24
|
import {
|
|
25
25
|
isGhAvailable,
|
|
@@ -48,23 +48,36 @@ async function runInitFlow() {
|
|
|
48
48
|
const enableBuildWorkflows = await promptBuildWorkflows();
|
|
49
49
|
const enableDevKit = await promptDevKit();
|
|
50
50
|
const enableVSCodeTasks = enableDevKit ? await promptVSCodeDevTasks() : false;
|
|
51
|
+
const projectName = enableDevKit && !readPkg() ? await promptProjectName() : undefined;
|
|
51
52
|
const enableCommitlint = await promptCommitlint();
|
|
52
53
|
const enableCursorSkills = await promptCursorSkills();
|
|
53
54
|
|
|
54
55
|
console.log(pc.dim(`\n Mode: ${mode}-store (${stores.length} store(s))`));
|
|
55
56
|
|
|
57
|
+
let missingBuildFiles = [];
|
|
56
58
|
if (enableBuildWorkflows) {
|
|
57
59
|
ensureBuildWorkflowDefaults();
|
|
58
|
-
|
|
60
|
+
missingBuildFiles = getMissingBuildWorkflowRequirements();
|
|
59
61
|
if (missingBuildFiles.length > 0) {
|
|
60
62
|
console.log(pc.red('\n Build workflows are enabled, but required files are missing:'));
|
|
61
63
|
for (const req of missingBuildFiles) {
|
|
62
64
|
const expected = req.kind === 'dir' ? `${req.path}/` : req.path;
|
|
63
65
|
console.log(pc.red(` - ${expected}`));
|
|
64
66
|
}
|
|
65
|
-
console.log(pc.dim(
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
console.log(pc.dim('\n Build workflows will be scaffolded, but build steps will be skipped until entrypoints exist.'));
|
|
68
|
+
const { createNow } = await prompts({
|
|
69
|
+
type: 'confirm',
|
|
70
|
+
name: 'createNow',
|
|
71
|
+
message: 'Create _scripts/main.js and _styles/main.css now?',
|
|
72
|
+
initial: false,
|
|
73
|
+
});
|
|
74
|
+
if (createNow) {
|
|
75
|
+
// Lazy import to avoid circular deps and keep init lean.
|
|
76
|
+
const { createEntrypointsCommand } = await import('./create-entrypoints.js');
|
|
77
|
+
await createEntrypointsCommand();
|
|
78
|
+
} else {
|
|
79
|
+
console.log(pc.dim(' Skipping entrypoint creation (default).'));
|
|
80
|
+
}
|
|
68
81
|
}
|
|
69
82
|
}
|
|
70
83
|
|
|
@@ -75,6 +88,7 @@ async function runInitFlow() {
|
|
|
75
88
|
default_store: stores[0].domain,
|
|
76
89
|
preview_workflows: enablePreviewWorkflows,
|
|
77
90
|
build_workflows: enableBuildWorkflows,
|
|
91
|
+
build_entrypoints_ready: enableBuildWorkflows ? missingBuildFiles?.length === 0 : undefined,
|
|
78
92
|
dev_kit: enableDevKit,
|
|
79
93
|
vscode_tasks: enableVSCodeTasks,
|
|
80
94
|
commitlint: enableCommitlint,
|
|
@@ -86,9 +100,9 @@ async function runInitFlow() {
|
|
|
86
100
|
config.stores[s.alias] = s.domain;
|
|
87
101
|
}
|
|
88
102
|
|
|
89
|
-
// 3. Write
|
|
103
|
+
// 3. Write climaybe config
|
|
90
104
|
writeConfig(config);
|
|
91
|
-
console.log(pc.green(' Updated
|
|
105
|
+
console.log(pc.green(' Updated climaybe config.'));
|
|
92
106
|
|
|
93
107
|
// 4. Ensure git repo
|
|
94
108
|
ensureGitRepo();
|
|
@@ -141,8 +155,9 @@ async function runInitFlow() {
|
|
|
141
155
|
scaffoldThemeDevKit({
|
|
142
156
|
includeVSCodeTasks: enableVSCodeTasks,
|
|
143
157
|
defaultStoreDomain: stores[0]?.domain || '',
|
|
158
|
+
packageName: projectName || undefined,
|
|
144
159
|
});
|
|
145
|
-
console.log(pc.green(' Theme dev kit installed (
|
|
160
|
+
console.log(pc.green(' Theme dev kit installed (local config + ignore defaults).'));
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
// Done
|
|
@@ -245,26 +260,45 @@ async function runInitFlow() {
|
|
|
245
260
|
console.log(pc.cyan(`\n Configure ${totalToPrompt} ${setter.name} secret(s)/variable(s). Leave optional ones blank to skip.\n`));
|
|
246
261
|
for (let i = 0; i < toPrompt.length; i++) {
|
|
247
262
|
const secret = toPrompt[i];
|
|
248
|
-
const value = await promptSecretValue(secret, i, totalToPrompt);
|
|
249
|
-
if (!value) continue;
|
|
250
|
-
|
|
251
263
|
const isThemeToken =
|
|
252
264
|
secret.name === 'SHOPIFY_THEME_ACCESS_TOKEN' || secret.name.startsWith('SHOPIFY_THEME_ACCESS_TOKEN_');
|
|
253
265
|
const storeUrl = isThemeToken ? getStoreUrlForThemeTokenSecret(secret.name, stores) : null;
|
|
254
266
|
|
|
255
|
-
if (
|
|
256
|
-
|
|
257
|
-
|
|
267
|
+
if (isThemeToken) {
|
|
268
|
+
if (!storeUrl) {
|
|
269
|
+
console.log(pc.red(` Could not resolve store URL for required token ${secret.name}.`));
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
// Theme tokens are required and must validate; keep prompting until valid + set.
|
|
273
|
+
while (true) {
|
|
274
|
+
const value = await promptSecretValue(secret, i, totalToPrompt);
|
|
275
|
+
if (!value) {
|
|
276
|
+
console.log(pc.red(' Theme access token is required.'));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
258
279
|
const result = await validateThemeAccessToken(storeUrl, value);
|
|
259
280
|
if (!result.ok) {
|
|
260
281
|
console.log(pc.red(` Token test failed: ${result.error}`));
|
|
261
|
-
console.log(pc.dim('
|
|
282
|
+
console.log(pc.dim(' Please try again with a valid Theme Access token.'));
|
|
262
283
|
continue;
|
|
263
284
|
}
|
|
264
285
|
console.log(pc.green(' Token validated against store.'));
|
|
286
|
+
try {
|
|
287
|
+
await setter.set(secret.name, value);
|
|
288
|
+
console.log(pc.green(` Set ${secret.name}.`));
|
|
289
|
+
setCount++;
|
|
290
|
+
break;
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
|
|
293
|
+
console.log(pc.dim(' Please try entering the token again.'));
|
|
294
|
+
}
|
|
265
295
|
}
|
|
296
|
+
continue;
|
|
266
297
|
}
|
|
267
298
|
|
|
299
|
+
const value = await promptSecretValue(secret, i, totalToPrompt);
|
|
300
|
+
if (!value) continue;
|
|
301
|
+
|
|
268
302
|
try {
|
|
269
303
|
await setter.set(secret.name, value);
|
|
270
304
|
console.log(pc.green(` Set ${secret.name}.`));
|
|
@@ -284,7 +318,7 @@ export async function initCommand() {
|
|
|
284
318
|
if (getProjectType() === 'app') {
|
|
285
319
|
console.log(pc.red(' This repo is configured as a Shopify app (project_type: app).'));
|
|
286
320
|
console.log(pc.dim(' Use: npx climaybe app init'));
|
|
287
|
-
console.log(pc.dim(' To switch to theme CI/CD, remove or edit
|
|
321
|
+
console.log(pc.dim(' To switch to theme CI/CD, remove or edit climaybe.config.json → project_type first.\n'));
|
|
288
322
|
return;
|
|
289
323
|
}
|
|
290
324
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { migrateLegacyPackageConfigToClimaybe, readClimaybeConfig, readPkg, writePkg } from '../lib/config.js';
|
|
6
|
+
import { updateWorkflowsCommand } from './update-workflows.js';
|
|
7
|
+
import { requireThemeProject } from '../lib/theme-guard.js';
|
|
8
|
+
import { getDevKitExistingFiles, scaffoldThemeDevKit } from '../lib/theme-dev-kit.js';
|
|
9
|
+
|
|
10
|
+
const LEGACY_SCRIPT_KEYS = [
|
|
11
|
+
'shopify:serve',
|
|
12
|
+
'shopify:serve:sync',
|
|
13
|
+
'shopify:populate',
|
|
14
|
+
'scripts:build',
|
|
15
|
+
'scripts:watch',
|
|
16
|
+
'tailwind:watch',
|
|
17
|
+
'tailwind:build',
|
|
18
|
+
'dev',
|
|
19
|
+
'dev:theme',
|
|
20
|
+
'dev:assets',
|
|
21
|
+
'dev:no-sync',
|
|
22
|
+
'lint:liquid',
|
|
23
|
+
'lint:js',
|
|
24
|
+
'lint:css',
|
|
25
|
+
'release',
|
|
26
|
+
'prepare',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const LEGACY_DEP_NAMES = [
|
|
30
|
+
'concurrently',
|
|
31
|
+
'nodemon',
|
|
32
|
+
'tailwindcss',
|
|
33
|
+
'@tailwindcss/cli',
|
|
34
|
+
'@tailwindcss/typography',
|
|
35
|
+
'eslint',
|
|
36
|
+
'@eslint/js',
|
|
37
|
+
'@eslint/create-config',
|
|
38
|
+
'stylelint',
|
|
39
|
+
'stylelint-config-standard',
|
|
40
|
+
'prettier',
|
|
41
|
+
'@shopify/prettier-plugin-liquid',
|
|
42
|
+
'@commitlint/cli',
|
|
43
|
+
'@commitlint/config-conventional',
|
|
44
|
+
'husky',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function cleanupLegacyFiles(cwd) {
|
|
48
|
+
const candidates = [
|
|
49
|
+
'.climaybe/build-scripts.js',
|
|
50
|
+
'build-scripts.js',
|
|
51
|
+
'.climaybe/dev-with-sync.sh',
|
|
52
|
+
'.climaybe/dev.sh',
|
|
53
|
+
];
|
|
54
|
+
const removed = [];
|
|
55
|
+
for (const rel of candidates) {
|
|
56
|
+
const abs = join(cwd, rel);
|
|
57
|
+
if (!existsSync(abs)) continue;
|
|
58
|
+
rmSync(abs, { force: true });
|
|
59
|
+
removed.push(rel);
|
|
60
|
+
}
|
|
61
|
+
return removed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cleanupLegacyPackageJson(cwd) {
|
|
65
|
+
const pkg = readPkg(cwd);
|
|
66
|
+
if (!pkg || typeof pkg !== 'object') return { removedScripts: [], removedDeps: [] };
|
|
67
|
+
|
|
68
|
+
const removedScripts = [];
|
|
69
|
+
if (pkg.scripts && typeof pkg.scripts === 'object') {
|
|
70
|
+
for (const key of LEGACY_SCRIPT_KEYS) {
|
|
71
|
+
if (key in pkg.scripts) {
|
|
72
|
+
delete pkg.scripts[key];
|
|
73
|
+
removedScripts.push(key);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (Object.keys(pkg.scripts).length === 0) delete pkg.scripts;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const removedDeps = [];
|
|
80
|
+
for (const depField of ['dependencies', 'devDependencies']) {
|
|
81
|
+
const deps = pkg[depField];
|
|
82
|
+
if (!deps || typeof deps !== 'object') continue;
|
|
83
|
+
for (const name of LEGACY_DEP_NAMES) {
|
|
84
|
+
if (name in deps) {
|
|
85
|
+
delete deps[name];
|
|
86
|
+
removedDeps.push(`${depField}:${name}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (Object.keys(deps).length === 0) delete pkg[depField];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
writePkg(pkg, cwd);
|
|
93
|
+
return { removedScripts, removedDeps };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function migrateLegacyConfigCommand(options = {}) {
|
|
97
|
+
const overwrite = options.overwrite === true;
|
|
98
|
+
const updateWorkflows = options.updateWorkflows !== false;
|
|
99
|
+
const yes = options.yes === true;
|
|
100
|
+
|
|
101
|
+
console.log(pc.bold('\n climaybe — Migrate legacy config\n'));
|
|
102
|
+
|
|
103
|
+
if (!requireThemeProject()) return;
|
|
104
|
+
|
|
105
|
+
const legacy = readPkg()?.config ?? null;
|
|
106
|
+
if (!legacy || typeof legacy !== 'object') {
|
|
107
|
+
console.log(pc.yellow(' No legacy package.json config found (package.json → config).'));
|
|
108
|
+
console.log(pc.dim(' Nothing to migrate.\n'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const existing = readClimaybeConfig();
|
|
113
|
+
if (existing && !overwrite) {
|
|
114
|
+
const ok = yes
|
|
115
|
+
? true
|
|
116
|
+
: (
|
|
117
|
+
await prompts({
|
|
118
|
+
type: 'confirm',
|
|
119
|
+
name: 'ok',
|
|
120
|
+
message: 'A climaybe.config.json already exists. Overwrite it from package.json config?',
|
|
121
|
+
initial: false,
|
|
122
|
+
})
|
|
123
|
+
).ok;
|
|
124
|
+
if (!ok) {
|
|
125
|
+
console.log(pc.dim(' Cancelled.\n'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const did = migrateLegacyPackageConfigToClimaybe({ overwrite: true });
|
|
131
|
+
if (!did) {
|
|
132
|
+
console.log(pc.yellow(' Migration skipped (no legacy config, or config already present).'));
|
|
133
|
+
console.log(pc.dim(' Tip: run with --overwrite to force rewrite.\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log(pc.green(' Migrated package.json config → climaybe.config.json'));
|
|
138
|
+
|
|
139
|
+
// Clean legacy dev system artifacts (sample-style package scripts, shims, old .climaybe scripts).
|
|
140
|
+
const cleanup = yes
|
|
141
|
+
? true
|
|
142
|
+
: (
|
|
143
|
+
await prompts({
|
|
144
|
+
type: 'confirm',
|
|
145
|
+
name: 'cleanup',
|
|
146
|
+
message: 'Remove legacy dev scripts/deps from package.json and delete old local shims?',
|
|
147
|
+
initial: true,
|
|
148
|
+
})
|
|
149
|
+
).cleanup;
|
|
150
|
+
if (cleanup) {
|
|
151
|
+
const removedFiles = cleanupLegacyFiles(process.cwd());
|
|
152
|
+
const { removedScripts, removedDeps } = cleanupLegacyPackageJson(process.cwd());
|
|
153
|
+
if (removedFiles.length > 0) {
|
|
154
|
+
console.log(pc.green(` Removed legacy files: ${removedFiles.join(', ')}`));
|
|
155
|
+
}
|
|
156
|
+
if (removedScripts.length > 0) {
|
|
157
|
+
console.log(pc.green(` Removed legacy package.json scripts: ${removedScripts.join(', ')}`));
|
|
158
|
+
}
|
|
159
|
+
if (removedDeps.length > 0) {
|
|
160
|
+
console.log(pc.green(` Removed legacy package.json deps: ${removedDeps.join(', ')}`));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Reinstall dev kit so tasks/theme-check config match the new climaybe runtime.
|
|
165
|
+
const reinstallDevKit = yes
|
|
166
|
+
? true
|
|
167
|
+
: (
|
|
168
|
+
await prompts({
|
|
169
|
+
type: 'confirm',
|
|
170
|
+
name: 'reinstallDevKit',
|
|
171
|
+
message: 'Install/refresh climaybe dev kit files (.vscode/tasks.json, etc.)?',
|
|
172
|
+
initial: true,
|
|
173
|
+
})
|
|
174
|
+
).reinstallDevKit;
|
|
175
|
+
if (reinstallDevKit) {
|
|
176
|
+
const existing = getDevKitExistingFiles({ includeVSCodeTasks: true });
|
|
177
|
+
if (existing.length > 0) {
|
|
178
|
+
console.log(pc.yellow(' Some dev kit files already exist and will be replaced:'));
|
|
179
|
+
for (const path of existing) console.log(pc.yellow(` - ${path}`));
|
|
180
|
+
const ok = yes
|
|
181
|
+
? true
|
|
182
|
+
: (
|
|
183
|
+
await prompts({
|
|
184
|
+
type: 'confirm',
|
|
185
|
+
name: 'ok',
|
|
186
|
+
message: 'Replace these files?',
|
|
187
|
+
initial: true,
|
|
188
|
+
})
|
|
189
|
+
).ok;
|
|
190
|
+
if (!ok) {
|
|
191
|
+
console.log(pc.dim(' Skipped dev kit refresh.\n'));
|
|
192
|
+
} else {
|
|
193
|
+
const legacy = readPkg()?.config ?? {};
|
|
194
|
+
scaffoldThemeDevKit({
|
|
195
|
+
includeVSCodeTasks: true,
|
|
196
|
+
defaultStoreDomain: legacy.default_store || legacy.store || '',
|
|
197
|
+
});
|
|
198
|
+
console.log(pc.green(' Dev kit refreshed.'));
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
const legacy = readPkg()?.config ?? {};
|
|
202
|
+
scaffoldThemeDevKit({
|
|
203
|
+
includeVSCodeTasks: true,
|
|
204
|
+
defaultStoreDomain: legacy.default_store || legacy.store || '',
|
|
205
|
+
});
|
|
206
|
+
console.log(pc.green(' Dev kit installed.'));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (updateWorkflows) {
|
|
211
|
+
console.log(pc.dim('\n Updating workflows to use climaybe.config.json...\n'));
|
|
212
|
+
await updateWorkflowsCommand();
|
|
213
|
+
} else {
|
|
214
|
+
console.log(pc.dim(' Skipped workflow update (pass --update-workflows to refresh).\n'));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|