climaybe 2.4.1 → 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 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 `package.json` config, optional commitlint + Cursor bundle. Does **not** install theme GitHub Actions or store/branch setup.
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 `package.json` → `config.project_type` is **`app`**.
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 `package.json` config
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 the `config` field of your `package.json`:
148
+ The CLI writes config into `climaybe.config.json`:
149
149
 
150
150
  ```json
151
151
  {
152
- "config": {
153
- "port": 9295,
154
- "default_store": "voldt-staging.myshopify.com",
155
- "preview_workflows": true,
156
- "build_workflows": true,
157
- "commitlint": true,
158
- "cursor_skills": true,
159
- "stores": {
160
- "voldt-staging": "voldt-staging.myshopify.com",
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
  ```
@@ -205,17 +203,17 @@ Direct pushes to `staging-<store>` or `live-<store>` are automatically synced ba
205
203
  |----------|---------|-------------|
206
204
  | `release-pr-check.yml` | PR from `staging` to `main` | Finds latest tag on main, AI changelog to PR head, creates pre-release patch tag (e.g. v3.1.13) to lock state; posts changelog comment |
207
205
  | `post-merge-tag.yml` | Push to `main` (merged PR) | Staging→main only: minor bump from latest tag (e.g. v3.1.13 → v3.2.0). No version in PR title |
208
- | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Collects commits since latest tag (incl. hotfix backports), AI changelog, patch bump and tag |
206
+ | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Collects commits since latest tag (incl. hotfix backports), ignores no-op/empty-tree commits, generates AI changelog, patch bump and tag |
209
207
 
210
208
  ### Multi-store (additional)
211
209
 
212
210
  | Workflow | Trigger | What it does |
213
211
  |----------|---------|-------------|
214
- | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Merges main into each `staging-<alias>`; root JSONs ignored. For hotfix-backport: if source is `staging-<alias>`, that same staging branch is skipped; if source is `live-<alias>`, `staging-<alias>` is also synced. Skips only on pure store-sync. |
212
+ | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Merges main into each `staging-<alias>`; root JSONs ignored. Skips no-op sync when branch tree already matches main. For hotfix-backport: if source is `staging-<alias>`, that same staging branch is skipped; if source is `live-<alias>`, `staging-<alias>` is also synced. Skips only on pure store-sync. |
215
213
  | `stores-to-root.yml` | Push to `staging-*` | From main merge: stores→root. From elsewhere (e.g. Shopify): root→stores |
216
214
  | `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
217
215
  | `root-to-stores.yml` | Push to `live-*` | From main merge: stores→root. From elsewhere: root→stores (same as stores-to-root on staging-*) |
218
- | `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Merges store branch into main (no PR). Skips when push is a merge from main (avoids loop) |
216
+ | `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Merges store branch into main (no PR). Skips when push is a merge from main (avoids loop) and skips no-op backports when source and main trees are identical |
219
217
 
220
218
  ### Optional preview + cleanup package
221
219
 
@@ -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, `init` validates required theme files and exits with an error if any are missing:
239
- - `_scripts/main.js`
240
- - `_styles/main.css`
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
- `init` auto-creates:
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 commit 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. |
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 scripts/config defaults (`nodemon.json`, `.theme-check.yml`, `.shopifyignore`, `.prettierrc`,
257
- `.lighthouserc.js`), merges matching `package.json` scripts/dependencies, appends a managed `.gitignore` block,
258
- and optionally adds `.vscode/tasks.json` (default: yes).
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
 
@@ -268,7 +267,7 @@ You can install/update this later with:
268
267
  - **Version format**: Always three-part (e.g. `v3.2.0`). No version in code or PR title; the system infers from tags.
269
268
  - **No tags yet?** The system uses `theme_version` from `config/settings_schema.json` (`theme_info`), creates that tag on main (e.g. `v1.0.0`), and continues from there.
270
269
  - **Staging → main**: On PR, a pre-release patch tag (e.g. v3.1.13) locks the current minor line; on merge, **minor** bump (e.g. v3.1.13 → v3.2.0).
271
- - **Non-staging to main** (hotfix backports, direct commits): **Patch** bump only, via **nightly workflow** at 02:00 US Eastern (not at commit time).
270
+ - **Non-staging to main** (hotfix backports, direct commits): **Patch** bump only, via **nightly workflow** at 02:00 US Eastern (not at commit time). No-op/empty-tree commits are ignored.
272
271
  - **Version bump runs only on main** (post-merge-tag and nightly-hotfix). Main-to-staging-stores merges main into each `staging-<alias>` on every push (version bumps and hotfixes). For hotfix-backport, only a `staging-<alias> -> main` source skips syncing back to the same staging branch; a `live-<alias> -> main` source still syncs into `staging-<alias>`.
273
272
  - Version bumps update `config/settings_schema.json` and, when present, `package.json` `version`.
274
273
  - **Safety**: The version-bump workflow fails if the new tag would not be **strictly higher** than the latest merged release tag (semver), so the release line cannot step backward.
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 2.4.1
1
+ 3.0.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "2.4.1",
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 scripts/configs for local serve/watch/lint and ignore defaults.\n'));
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, promptTestThemeToken } from '../lib/prompts.js';
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
- const doTest = await promptTestThemeToken();
128
- if (doTest) {
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(' Secret not set. You can add it later in repo Settings → Secrets.'));
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}.`));
@@ -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 package.json config.'));
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
+
@@ -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, getBuildScriptRelativePath, ensureBuildWorkflowDefaults } from '../lib/build-workflows.js';
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
- const missingBuildFiles = getMissingBuildWorkflowRequirements();
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(`\n climaybe will auto-install ${getBuildScriptRelativePath()} during workflow scaffolding.`));
66
- console.log(pc.dim(' Add the missing files above, then run init again.\n'));
67
- return;
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 package.json config
103
+ // 3. Write climaybe config
90
104
  writeConfig(config);
91
- console.log(pc.green(' Updated package.json config.'));
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 (scripts/watch/lint + ignore defaults).'));
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 (storeUrl) {
256
- const doTest = await promptTestThemeToken();
257
- if (doTest) {
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(' Secret not set. You can add it later in repo Settings → Secrets.'));
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 package.json → config first.\n'));
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