climaybe 1.0.0 → 1.3.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.
Files changed (32) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -12
  3. package/package.json +45 -2
  4. package/src/commands/add-store.js +77 -4
  5. package/src/commands/init.js +150 -16
  6. package/src/commands/update-workflows.js +4 -2
  7. package/src/index.js +15 -3
  8. package/src/lib/config.js +17 -0
  9. package/src/lib/git.js +21 -3
  10. package/src/lib/github-secrets.js +263 -0
  11. package/src/lib/prompts.js +116 -6
  12. package/src/lib/workflows.js +23 -3
  13. package/src/workflows/build/build-pipeline.yml +57 -0
  14. package/src/workflows/build/create-release.yml +52 -0
  15. package/src/workflows/build/reusable-build.yml +52 -0
  16. package/src/workflows/multi/main-to-staging-stores.yml +44 -3
  17. package/src/workflows/multi/multistore-hotfix-to-main.yml +84 -0
  18. package/src/workflows/multi/pr-to-live.yml +82 -8
  19. package/src/workflows/multi/stores-to-root.yml +10 -3
  20. package/src/workflows/preview/pr-close.yml +63 -0
  21. package/src/workflows/preview/pr-update.yml +120 -0
  22. package/src/workflows/preview/reusable-cleanup-themes.yml +71 -0
  23. package/src/workflows/preview/reusable-comment-on-pr.yml +76 -0
  24. package/src/workflows/preview/reusable-extract-pr-number.yml +35 -0
  25. package/src/workflows/preview/reusable-rename-theme.yml +73 -0
  26. package/src/workflows/preview/reusable-share-theme.yml +94 -0
  27. package/src/workflows/shared/ai-changelog.yml +82 -24
  28. package/src/workflows/shared/version-bump.yml +22 -7
  29. package/src/workflows/single/nightly-hotfix.yml +38 -2
  30. package/src/workflows/single/post-merge-tag.yml +68 -15
  31. package/src/workflows/single/release-pr-check.yml +16 -6
  32. package/src/workflows/multi/hotfix-backport.yml +0 -100
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Electric Maybe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -26,10 +26,12 @@ Interactive setup that configures your repo for CI/CD.
26
26
  1. Prompts for your store URL (e.g., `voldt-staging.myshopify.com`)
27
27
  2. Extracts subdomain as alias, lets you override
28
28
  3. Asks if you want to add more stores
29
- 4. Based on store count, sets up **single-store** or **multi-store** mode
30
- 5. Writes `package.json` config
31
- 6. Scaffolds GitHub Actions workflows
32
- 7. Creates git branches and store directories (multi-store)
29
+ 4. Asks whether to enable optional **preview + cleanup** workflows
30
+ 5. Asks whether to enable optional **build + Lighthouse** workflows
31
+ 6. Based on store count, sets up **single-store** or **multi-store** mode
32
+ 7. Writes `package.json` config
33
+ 8. Scaffolds GitHub Actions workflows
34
+ 9. Creates git branches and store directories (multi-store)
33
35
 
34
36
  ### `climaybe add-store`
35
37
 
@@ -83,6 +85,8 @@ The CLI writes config into the `config` field of your `package.json`:
83
85
  "config": {
84
86
  "port": 9295,
85
87
  "default_store": "voldt-staging.myshopify.com",
88
+ "preview_workflows": true,
89
+ "build_workflows": true,
86
90
  "stores": {
87
91
  "voldt-staging": "voldt-staging.myshopify.com",
88
92
  "voldt-norway": "voldt-norway.myshopify.com"
@@ -115,6 +119,8 @@ staging → main → staging-<store> → live-<store>
115
119
  - `staging-<store>` — per-store staging with store-specific JSON data
116
120
  - `live-<store>` — per-store production
117
121
 
122
+ Direct pushes to `staging-<store>` or `live-<store>` are automatically synced back to `main` (no PR; multistore-hotfix-to-main merges the branch into main).
123
+
118
124
  ## Workflows
119
125
 
120
126
  ### Shared (both modes)
@@ -129,27 +135,55 @@ staging → main → staging-<store> → live-<store>
129
135
  | Workflow | Trigger | What it does |
130
136
  |----------|---------|-------------|
131
137
  | `release-pr-check.yml` | PR from `staging` to `main` | Generates changelog, creates pre-release patch tag, posts PR comment |
132
- | `post-merge-tag.yml` | Push to `main` (merged PR) | Detects release version from PR title, bumps minor version |
138
+ | `post-merge-tag.yml` | Push to `main` (merged PR) | Staging→main: minor bump (e.g. v3.2.0). Hotfix sync: patch bump (e.g. v3.2.1) |
133
139
  | `nightly-hotfix.yml` | Cron 02:00 US Eastern | Tags untagged hotfix commits with patch version |
134
140
 
135
141
  ### Multi-store (additional)
136
142
 
137
143
  | Workflow | Trigger | What it does |
138
144
  |----------|---------|-------------|
139
- | `main-to-staging-stores.yml` | Push to `main` | Opens PRs to each `staging-<alias>` branch |
145
+ | `main-to-staging-stores.yml` (main-to-staging-&lt;store&gt;) | Push to `main` | Opens PRs from main to each `staging-<alias>` branch |
140
146
  | `stores-to-root.yml` | Push to `staging-*` | Copies `stores/<alias>/` JSONs to repo root |
141
147
  | `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
142
148
  | `root-to-stores.yml` | Push to `live-*` (hotfix) | Syncs root JSONs back to `stores/<alias>/` |
143
- | `hotfix-backport.yml` | After root-to-stores | Creates backport PR to `main` |
149
+ | `multistore-hotfix-to-main.yml` | Push to `staging-*` or `live-*` (and after root-to-stores) | Automatically merges store branch into main (no PR); everything is synced back to main |
150
+
151
+ ### Optional preview + cleanup package
152
+
153
+ Enabled via `climaybe init` prompt (`Enable preview + cleanup workflows?`).
154
+
155
+ | Workflow | Trigger | What it does |
156
+ |----------|---------|-------------|
157
+ | `pr-update.yml` | PR opened/synchronize/reopened | Shares draft theme, renames with `-PR<number>`, comments preview + customize URLs |
158
+ | `pr-close.yml` | PR closed | Deletes matching preview themes and comments deleted count + names |
159
+ | `reusable-share-theme.yml` | workflow_call | Shares Shopify draft theme and returns `theme_id` |
160
+ | `reusable-rename-theme.yml` | workflow_call | Renames shared theme to include `PR<number>` (fails job on rename failure) |
161
+ | `reusable-comment-on-pr.yml` | workflow_call | Posts preview comment including Customize URL |
162
+ | `reusable-cleanup-themes.yml` | workflow_call | Deletes preview themes by PR number and exposes cleanup outputs |
163
+ | `reusable-extract-pr-number.yml` | workflow_call | Extracts padded/unpadded PR number outputs for naming and API-safe usage |
164
+
165
+ ### Optional build + Lighthouse package
166
+
167
+ Enabled via `climaybe init` prompt (`Enable build + Lighthouse workflows?`).
168
+
169
+ | Workflow | Trigger | What it does |
170
+ |----------|---------|-------------|
171
+ | `build-pipeline.yml` | Push to `main/staging/develop` | Runs reusable build and Lighthouse checks (when required secrets exist) |
172
+ | `reusable-build.yml` | workflow_call | Runs Node build + Tailwind compile, then commits compiled assets when changed |
173
+ | `create-release.yml` | Push tag `v*` | Builds release archive and creates GitHub Release using `release-notes.md` |
144
174
 
145
175
  ## Versioning
146
176
 
147
- - **Release merge** (`staging` `main`): Minor bump (e.g., `v3.1.x` `v3.2.0`)
148
- - **Hotfix** (direct commit to `main` or `live-*`): Patch bump (e.g., `v3.2.0` → `v3.2.1`)
149
- - **PR title convention**: `Release v3.2` the workflow extracts the version from this
177
+ - **Version format**: Always three-part (e.g. `v3.2.0`). No two-part tags.
178
+ - **Release merge** (`staging` `main`): Minor bump (e.g. `v3.1.12` → `v3.2.0`)
179
+ - **Hotfix sync** (`staging-<store>` or `live-<store>` main via multistore-hotfix-to-main): Patch bump runs immediately (e.g. `v3.2.0` → `v3.2.1`)
180
+ - **Other hotfixes** (direct commit to `main`): Patch bump via nightly workflow or manual run
181
+ - **PR title convention**: `Release v3.2` or `Release v3.2.0` — the workflow normalizes to three-part
150
182
 
151
183
  All version bumps update `config/settings_schema.json` automatically.
152
184
 
185
+ **Full specification:** For detailed versioning rules, local dev flow, hotfix behavior, and alignment with the external CI/CD doc, see **[CI/CD Reference](docs/CI_CD_REFERENCE.md)**.
186
+
153
187
  ## File Sync Rules (Multi-store)
154
188
 
155
189
  **Synced between root and `stores/<alias>/`:**
@@ -163,18 +197,27 @@ All version bumps update `config/settings_schema.json` automatically.
163
197
 
164
198
  ## Recursive Trigger Prevention
165
199
 
166
- - Hotfix backport commits contain `[hotfix-backport]` in the message
200
+ - Hotfix sync merge commits (multistore-hotfix-to-main) contain `[hotfix-backport]` in the message
167
201
  - Store sync commits contain `[stores-to-root]` or `[root-to-stores]`
168
202
  - Version bump commits contain `chore(release): bump version`
169
203
  - All workflows check for these flags and skip accordingly
170
204
 
171
205
  ## GitHub Secrets
172
206
 
173
- Add the following secret to your GitHub repository:
207
+ Add the following secrets to your GitHub repository (or use **GitLab CI/CD variables** if you use GitLab). You can configure them during `climaybe init` via the GitHub or GitLab CLI.
174
208
 
175
209
  | Secret | Required | Description |
176
210
  |--------|----------|-------------|
177
211
  | `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
212
+ | `SHOPIFY_STORE_URL` | Set from config | Store URL is set automatically from the store domain(s) you add during init (no prompt). |
213
+ | `SHOPIFY_CLI_THEME_TOKEN` | Yes* | Theme access token for preview workflows (required when preview is enabled). |
214
+ | `SHOP_ACCESS_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
215
+ | `LHCI_GITHUB_APP_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
216
+ | `SHOP_PASSWORD` | Optional | Used by Lighthouse action when your store requires password auth |
217
+
218
+ **Store URL:** During `climaybe init` (or `add-store`), store URL secret(s) are set from your configured store domain(s); you are only prompted for the theme token.
219
+
220
+ **Multi-store:** Per-store secrets `SHOPIFY_STORE_URL_<ALIAS>` and `SHOPIFY_CLI_THEME_TOKEN_<ALIAS>` — the URL is set from config; you must provide the theme token per store. `<ALIAS>` is uppercase with hyphens as underscores (e.g. `voldt-norway` → `SHOPIFY_STORE_URL_VOLDT_NORWAY`).
178
221
 
179
222
  ## Directory Structure (Multi-store)
180
223
 
@@ -199,6 +242,15 @@ Add the following secret to your GitHub repository:
199
242
  └── .github/workflows/
200
243
  ```
201
244
 
245
+ ## Releases and versioning
246
+
247
+ - **Branch:** Single default branch `main`. Feature branches open as PRs into `main`.
248
+ - **Versioning:** [SemVer](https://semver.org/). Versions are **bumped automatically** when PRs are merged to `main` using [conventional commits](https://www.conventionalcommits.org/): `fix:` → patch, `feat:` → minor, `BREAKING CHANGE` or `feat!:` → major.
249
+ - **Flow:** Merge to `main` → [Release version](.github/workflows/release-version.yml) runs semantic-release (bumps `package.json`, pushes tag) → tag push triggers [Release](.github/workflows/release.yml) (tests + publish to npm). Requires `NPM_TOKEN` secret for npm publish.
250
+ - **CI:** Every PR and push to `main` runs tests on Node 20 and 22 ([CI workflow](.github/workflows/ci.yml)).
251
+
252
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for branch, PR, and conventional-commit details.
253
+
202
254
  ## License
203
255
 
204
256
  MIT — Electric Maybe
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "description": "Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config for single-store and multi-store theme repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,9 +23,52 @@
23
23
  ],
24
24
  "author": "Electric Maybe",
25
25
  "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/electricmaybe/climaybe.git"
29
+ },
30
+ "bugs": "https://github.com/electricmaybe/climaybe/issues",
31
+ "homepage": "https://github.com/electricmaybe/climaybe#readme",
32
+ "scripts": {
33
+ "test": "node scripts/run-tests.js 2>&1 | tee test.log",
34
+ "prepare": "husky"
35
+ },
36
+ "release": {
37
+ "branches": [
38
+ "main"
39
+ ],
40
+ "plugins": [
41
+ "@semantic-release/commit-analyzer",
42
+ "@semantic-release/release-notes-generator",
43
+ [
44
+ "@semantic-release/npm",
45
+ {
46
+ "npmPublish": false
47
+ }
48
+ ],
49
+ [
50
+ "@semantic-release/git",
51
+ {
52
+ "assets": [
53
+ "package.json",
54
+ "package-lock.json"
55
+ ],
56
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
57
+ }
58
+ ]
59
+ ]
60
+ },
26
61
  "dependencies": {
27
- "commander": "^13.1.0",
62
+ "commander": "^14.0.3",
28
63
  "picocolors": "^1.1.1",
29
64
  "prompts": "^2.4.2"
65
+ },
66
+ "devDependencies": {
67
+ "@commitlint/cli": "^20.4.4",
68
+ "@commitlint/config-conventional": "^20.4.4",
69
+ "@semantic-release/git": "^10.0.1",
70
+ "@semantic-release/npm": "^13.1.5",
71
+ "husky": "^9.1.7",
72
+ "semantic-release": "^25.0.3"
30
73
  }
31
74
  }
@@ -1,9 +1,21 @@
1
1
  import pc from 'picocolors';
2
- import { promptNewStore } from '../lib/prompts.js';
3
- import { readConfig, addStoreToConfig, getStoreAliases, getMode } from '../lib/config.js';
2
+ import { promptNewStore, promptConfigureCISecrets, promptUpdateExistingSecrets, promptSecretValue } from '../lib/prompts.js';
3
+ import { readConfig, addStoreToConfig, getStoreAliases, getMode, isPreviewWorkflowsEnabled, isBuildWorkflowsEnabled } from '../lib/config.js';
4
4
  import { createStoreBranches } from '../lib/git.js';
5
5
  import { scaffoldWorkflows } from '../lib/workflows.js';
6
6
  import { createStoreDirectories } from '../lib/store-sync.js';
7
+ import {
8
+ isGhAvailable,
9
+ hasGitHubRemote,
10
+ isGlabAvailable,
11
+ hasGitLabRemote,
12
+ listGitHubSecrets,
13
+ listGitLabVariables,
14
+ getStoreUrlSecretForNewStore,
15
+ getSecretsToPromptForNewStore,
16
+ setSecret,
17
+ setGitLabVariable,
18
+ } from '../lib/github-secrets.js';
7
19
 
8
20
  export async function addStoreCommand() {
9
21
  console.log(pc.bold('\n climaybe — Add Store\n'));
@@ -17,6 +29,8 @@ export async function addStoreCommand() {
17
29
 
18
30
  const existingAliases = getStoreAliases();
19
31
  const previousMode = getMode();
32
+ const includePreview = isPreviewWorkflowsEnabled();
33
+ const includeBuild = isBuildWorkflowsEnabled();
20
34
 
21
35
  // Prompt for new store
22
36
  const store = await promptNewStore(existingAliases);
@@ -42,14 +56,73 @@ export async function addStoreCommand() {
42
56
  createStoreDirectories(originalAlias);
43
57
 
44
58
  // Re-scaffold workflows for multi mode
45
- scaffoldWorkflows('multi');
59
+ scaffoldWorkflows('multi', { includePreview, includeBuild });
46
60
  console.log(pc.green(' Migration complete — workflows updated to multi-store mode.'));
47
61
  } else if (newMode === 'multi') {
48
62
  // Already multi, just make sure workflows are current
49
- scaffoldWorkflows('multi');
63
+ scaffoldWorkflows('multi', { includePreview, includeBuild });
50
64
  }
51
65
 
52
66
  console.log(pc.bold(pc.green('\n Store added successfully!\n')));
53
67
  console.log(pc.dim(` New branches: staging-${store.alias}, live-${store.alias}`));
54
68
  console.log(pc.dim(` Store dir: stores/${store.alias}/\n`));
69
+
70
+ // If preview workflows are on, offer to set this store's CI secrets (multi-store uses per-store secrets)
71
+ if (includePreview) {
72
+ const ciHost = await promptConfigureCISecrets();
73
+ if (ciHost !== 'skip') {
74
+ const setter =
75
+ ciHost === 'github'
76
+ ? { check: isGhAvailable, checkRemote: hasGitHubRemote, set: setSecret, name: 'GitHub' }
77
+ : { check: isGlabAvailable, checkRemote: hasGitLabRemote, set: setGitLabVariable, name: 'GitLab' };
78
+
79
+ if (!setter.check()) {
80
+ console.log(pc.yellow(` ${setter.name} CLI is not installed or not logged in. Add secrets manually in repo Settings.`));
81
+ } else if (!setter.checkRemote()) {
82
+ console.log(pc.yellow(' No ' + setter.name + ' remote (origin). Add secrets manually after pushing.'));
83
+ } else {
84
+ let setCount = 0;
85
+ const { name: urlName, value: urlValue } = getStoreUrlSecretForNewStore(store);
86
+ try {
87
+ await setter.set(urlName, urlValue);
88
+ console.log(pc.green(` Set ${urlName} (from store config).`));
89
+ setCount++;
90
+ } catch (err) {
91
+ console.log(pc.red(` Failed to set ${urlName}: ${err.message}`));
92
+ }
93
+
94
+ const secretsToPrompt = getSecretsToPromptForNewStore(store);
95
+ const existingNames = ciHost === 'github' ? listGitHubSecrets() : listGitLabVariables();
96
+ const namesWeWillPrompt = new Set(secretsToPrompt.map((s) => s.name));
97
+ const alreadySet = existingNames.filter((n) => namesWeWillPrompt.has(n));
98
+ if (alreadySet.length > 0) {
99
+ const doUpdate = await promptUpdateExistingSecrets(alreadySet);
100
+ if (!doUpdate) {
101
+ if (setCount > 0) {
102
+ console.log(pc.green(`\n Done. ${setCount} secret(s) set for store "${store.alias}".\n`));
103
+ }
104
+ return;
105
+ }
106
+ }
107
+ const total = secretsToPrompt.length;
108
+ console.log(pc.cyan(`\n Configure ${total} secret(s) for store "${store.alias}" (theme token required).\n`));
109
+ for (let i = 0; i < secretsToPrompt.length; i++) {
110
+ const secret = secretsToPrompt[i];
111
+ const value = await promptSecretValue(secret, i, total);
112
+ if (value) {
113
+ try {
114
+ await setter.set(secret.name, value);
115
+ console.log(pc.green(` Set ${secret.name}.`));
116
+ setCount++;
117
+ } catch (err) {
118
+ console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
119
+ }
120
+ }
121
+ }
122
+ if (setCount > 0) {
123
+ console.log(pc.green(`\n Done. ${setCount} secret(s) set for store "${store.alias}".\n`));
124
+ }
125
+ }
126
+ }
127
+ }
55
128
  }
@@ -1,25 +1,40 @@
1
+ import prompts from 'prompts';
1
2
  import pc from 'picocolors';
2
- import { promptStoreLoop } from '../lib/prompts.js';
3
+ import {
4
+ promptStoreLoop,
5
+ promptPreviewWorkflows,
6
+ promptBuildWorkflows,
7
+ promptConfigureCISecrets,
8
+ promptUpdateExistingSecrets,
9
+ promptSecretValue,
10
+ } from '../lib/prompts.js';
3
11
  import { readConfig, writeConfig } from '../lib/config.js';
4
12
  import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches } from '../lib/git.js';
5
13
  import { scaffoldWorkflows } from '../lib/workflows.js';
6
14
  import { createStoreDirectories } from '../lib/store-sync.js';
15
+ import {
16
+ isGhAvailable,
17
+ hasGitHubRemote,
18
+ isGlabAvailable,
19
+ hasGitLabRemote,
20
+ listGitHubSecrets,
21
+ listGitLabVariables,
22
+ getStoreUrlSecretsFromConfig,
23
+ getSecretsToPrompt,
24
+ setSecret,
25
+ setGitLabVariable,
26
+ } from '../lib/github-secrets.js';
7
27
 
8
- export async function initCommand() {
9
- console.log(pc.bold('\n climaybe Shopify CI/CD Setup\n'));
10
-
11
- // Guard: check if already initialized
12
- const existing = readConfig();
13
- if (existing?.stores && Object.keys(existing.stores).length > 0) {
14
- console.log(pc.yellow(' This repo already has a climaybe config.'));
15
- console.log(pc.dim(' Use "climaybe add-store" to add more stores.'));
16
- console.log(pc.dim(' Use "climaybe update-workflows" to refresh workflows.\n'));
17
- return;
18
- }
19
-
28
+ /**
29
+ * Run the full init flow: prompts, config write, git, branches, workflows.
30
+ * Used by both init (when not already inited or user confirms reinit) and reinit.
31
+ */
32
+ async function runInitFlow() {
20
33
  // 1. Collect stores from user
21
34
  const stores = await promptStoreLoop();
22
35
  const mode = stores.length > 1 ? 'multi' : 'single';
36
+ const enablePreviewWorkflows = await promptPreviewWorkflows();
37
+ const enableBuildWorkflows = await promptBuildWorkflows();
23
38
 
24
39
  console.log(pc.dim(`\n Mode: ${mode}-store (${stores.length} store(s))`));
25
40
 
@@ -27,6 +42,8 @@ export async function initCommand() {
27
42
  const config = {
28
43
  port: 9295,
29
44
  default_store: stores[0].domain,
45
+ preview_workflows: enablePreviewWorkflows,
46
+ build_workflows: enableBuildWorkflows,
30
47
  stores: {},
31
48
  };
32
49
 
@@ -55,7 +72,10 @@ export async function initCommand() {
55
72
  }
56
73
 
57
74
  // 6. Scaffold workflows
58
- scaffoldWorkflows(mode);
75
+ scaffoldWorkflows(mode, {
76
+ includePreview: enablePreviewWorkflows,
77
+ includeBuild: enableBuildWorkflows,
78
+ });
59
79
 
60
80
  // Done
61
81
  console.log(pc.bold(pc.green('\n Setup complete!\n')));
@@ -70,9 +90,123 @@ export async function initCommand() {
70
90
  }
71
91
  console.log(pc.dim(' Workflow: staging → main → staging-<store> → live-<store>'));
72
92
  }
93
+ console.log(pc.dim(` Preview workflows: ${enablePreviewWorkflows ? 'enabled' : 'disabled'}`));
94
+ console.log(pc.dim(` Build workflows: ${enableBuildWorkflows ? 'enabled' : 'disabled'}`));
73
95
 
74
96
  console.log(pc.dim('\n Next steps:'));
75
- console.log(pc.dim(' 1. Add GEMINI_API_KEY to your GitHub repo secrets'));
76
- console.log(pc.dim(' 2. Push to GitHub and start using the branching workflow'));
97
+ console.log(pc.dim(' 1. Add GEMINI_API_KEY to your CI secrets (or configure below)'));
98
+ console.log(pc.dim(' 2. Push to GitHub/GitLab and start using the branching workflow'));
77
99
  console.log(pc.dim(' 3. Tag your first release: git tag v1.0.0\n'));
100
+
101
+ const ciHost = await promptConfigureCISecrets();
102
+ if (ciHost === 'skip') return;
103
+
104
+ const secretsToPrompt = getSecretsToPrompt({
105
+ enablePreviewWorkflows,
106
+ enableBuildWorkflows,
107
+ mode,
108
+ stores,
109
+ });
110
+ const total = secretsToPrompt.length;
111
+ const setter =
112
+ ciHost === 'github'
113
+ ? { check: isGhAvailable, checkRemote: hasGitHubRemote, set: setSecret, name: 'GitHub' }
114
+ : { check: isGlabAvailable, checkRemote: hasGitLabRemote, set: setGitLabVariable, name: 'GitLab' };
115
+
116
+ if (!setter.check()) {
117
+ const installUrl = ciHost === 'github' ? 'https://cli.github.com/' : 'https://gitlab.com/gitlab-org/cli';
118
+ console.log(pc.yellow(` ${setter.name} CLI is not installed or not logged in.`));
119
+ console.log(pc.dim(` Install: ${installUrl} — then run ${ciHost === 'github' ? 'gh' : 'glab'} auth login`));
120
+ console.log(
121
+ pc.dim(
122
+ ciHost === 'github'
123
+ ? ' You can add secrets later in the repo: Settings → Secrets and variables → Actions.\n'
124
+ : ' You can add variables later in the repo: Settings → CI/CD → Variables.\n'
125
+ )
126
+ );
127
+ return;
128
+ }
129
+ if (!setter.checkRemote()) {
130
+ console.log(pc.yellow(' This repo has no ' + setter.name + ' remote (origin).'));
131
+ console.log(pc.dim(' Add a remote and push first, then add secrets/variables in the repo Settings.\n'));
132
+ return;
133
+ }
134
+
135
+ const existingNames =
136
+ ciHost === 'github' ? listGitHubSecrets() : listGitLabVariables();
137
+ const namesWeWillPrompt = new Set(secretsToPrompt.map((s) => s.name));
138
+ const alreadySet = existingNames.filter((n) => namesWeWillPrompt.has(n));
139
+ if (alreadySet.length > 0) {
140
+ const doUpdate = await promptUpdateExistingSecrets(alreadySet);
141
+ if (!doUpdate) {
142
+ console.log(pc.dim('\n Skipping. Existing secrets left unchanged.\n'));
143
+ return;
144
+ }
145
+ }
146
+
147
+ let setCount = 0;
148
+
149
+ // Set store URL(s) from config (domains already added during init) — no prompt
150
+ const storeUrlSecrets = getStoreUrlSecretsFromConfig({
151
+ enablePreviewWorkflows,
152
+ enableBuildWorkflows,
153
+ mode,
154
+ stores,
155
+ });
156
+ for (const { name, value } of storeUrlSecrets) {
157
+ try {
158
+ await setter.set(name, value);
159
+ console.log(pc.green(` Set ${name} (from store config).`));
160
+ setCount++;
161
+ } catch (err) {
162
+ console.log(pc.red(` Failed to set ${name}: ${err.message}`));
163
+ }
164
+ }
165
+
166
+ console.log(pc.cyan(`\n Configure ${total} ${setter.name} secret(s)/variable(s). Leave optional ones blank to skip.\n`));
167
+ for (let i = 0; i < secretsToPrompt.length; i++) {
168
+ const secret = secretsToPrompt[i];
169
+ const value = await promptSecretValue(secret, i, total);
170
+ if (value) {
171
+ try {
172
+ await setter.set(secret.name, value);
173
+ console.log(pc.green(` Set ${secret.name}.`));
174
+ setCount++;
175
+ } catch (err) {
176
+ console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
177
+ }
178
+ }
179
+ }
180
+ if (setCount > 0) {
181
+ console.log(pc.green(`\n Done. ${setCount} secret(s) set for this repository.\n`));
182
+ }
183
+ }
184
+
185
+ export async function initCommand() {
186
+ console.log(pc.bold('\n climaybe — Shopify CI/CD Setup\n'));
187
+
188
+ const existing = readConfig();
189
+ const alreadyInited = existing?.stores && Object.keys(existing.stores).length > 0;
190
+
191
+ if (alreadyInited) {
192
+ const { reinit } = await prompts({
193
+ type: 'confirm',
194
+ name: 'reinit',
195
+ message: 'This repo already has a climaybe config. Reinitialize? This will remove your current stores and workflow settings.',
196
+ initial: false,
197
+ });
198
+ if (!reinit) {
199
+ console.log(pc.dim(' Use "climaybe add-store" to add more stores.'));
200
+ console.log(pc.dim(' Use "climaybe update-workflows" to refresh workflows.'));
201
+ console.log(pc.dim(' Use "climaybe reinit" to reinitialize from scratch.\n'));
202
+ return;
203
+ }
204
+ }
205
+
206
+ await runInitFlow();
207
+ }
208
+
209
+ export async function reinitCommand() {
210
+ console.log(pc.bold('\n climaybe — Reinitialize CI/CD Setup\n'));
211
+ await runInitFlow();
78
212
  }
@@ -1,5 +1,5 @@
1
1
  import pc from 'picocolors';
2
- import { getMode, readConfig } from '../lib/config.js';
2
+ import { getMode, isBuildWorkflowsEnabled, isPreviewWorkflowsEnabled, readConfig } from '../lib/config.js';
3
3
  import { scaffoldWorkflows } from '../lib/workflows.js';
4
4
 
5
5
  export async function updateWorkflowsCommand() {
@@ -12,7 +12,9 @@ export async function updateWorkflowsCommand() {
12
12
  }
13
13
 
14
14
  const mode = getMode();
15
- scaffoldWorkflows(mode);
15
+ const includePreview = isPreviewWorkflowsEnabled();
16
+ const includeBuild = isBuildWorkflowsEnabled();
17
+ scaffoldWorkflows(mode, { includePreview, includeBuild });
16
18
 
17
19
  console.log(pc.bold(pc.green('\n Workflows updated!\n')));
18
20
  }
package/src/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { Command } from 'commander';
2
- import { initCommand } from './commands/init.js';
2
+ import { initCommand, reinitCommand } from './commands/init.js';
3
3
  import { addStoreCommand } from './commands/add-store.js';
4
4
  import { switchCommand } from './commands/switch.js';
5
5
  import { syncCommand } from './commands/sync.js';
6
6
  import { updateWorkflowsCommand } from './commands/update-workflows.js';
7
7
 
8
- export function run(argv) {
8
+ /**
9
+ * Create the CLI program (for testing and for run).
10
+ */
11
+ export function createProgram() {
9
12
  const program = new Command();
10
13
 
11
14
  program
@@ -18,6 +21,11 @@ export function run(argv) {
18
21
  .description('Initialize CI/CD setup for a Shopify theme repo')
19
22
  .action(initCommand);
20
23
 
24
+ program
25
+ .command('reinit')
26
+ .description('Reinitialize CI/CD setup (removes existing config and re-scaffolds workflows)')
27
+ .action(reinitCommand);
28
+
21
29
  program
22
30
  .command('add-store')
23
31
  .description('Add a new store to an existing multi-store config')
@@ -40,5 +48,9 @@ export function run(argv) {
40
48
  .description('Refresh GitHub Actions workflows from latest bundled templates')
41
49
  .action(updateWorkflowsCommand);
42
50
 
43
- program.parse(argv);
51
+ return program;
52
+ }
53
+
54
+ export function run(argv) {
55
+ createProgram().parse(argv);
44
56
  }
package/src/lib/config.js CHANGED
@@ -14,6 +14,7 @@ function pkgPath(cwd = process.cwd()) {
14
14
  /**
15
15
  * Read the full package.json from a target repo.
16
16
  * Returns null if it doesn't exist.
17
+ * @public (used by tests)
17
18
  */
18
19
  export function readPkg(cwd = process.cwd()) {
19
20
  const p = pkgPath(cwd);
@@ -72,6 +73,22 @@ export function getMode(cwd = process.cwd()) {
72
73
  return aliases.length > 1 ? 'multi' : 'single';
73
74
  }
74
75
 
76
+ /**
77
+ * Whether optional preview/cleanup workflows are enabled.
78
+ */
79
+ export function isPreviewWorkflowsEnabled(cwd = process.cwd()) {
80
+ const config = readConfig(cwd);
81
+ return config?.preview_workflows === true;
82
+ }
83
+
84
+ /**
85
+ * Whether optional build/Lighthouse workflows are enabled.
86
+ */
87
+ export function isBuildWorkflowsEnabled(cwd = process.cwd()) {
88
+ const config = readConfig(cwd);
89
+ return config?.build_workflows === true;
90
+ }
91
+
75
92
  /**
76
93
  * Add a store entry to the config.
77
94
  * Returns the updated config.
package/src/lib/git.js CHANGED
@@ -4,6 +4,11 @@ import pc from 'picocolors';
4
4
  const exec = (cmd, cwd = process.cwd()) =>
5
5
  execSync(cmd, { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
6
6
 
7
+ function isExpectedGitError(err, patterns) {
8
+ const text = [err.message, err.stderr].filter(Boolean).join(' ');
9
+ return patterns.some((p) => text.includes(p));
10
+ }
11
+
7
12
  /**
8
13
  * Check if current directory is a git repo.
9
14
  */
@@ -11,7 +16,10 @@ export function isGitRepo(cwd = process.cwd()) {
11
16
  try {
12
17
  exec('git rev-parse --is-inside-work-tree', cwd);
13
18
  return true;
14
- } catch {
19
+ } catch (err) {
20
+ if (!isExpectedGitError(err, ['not a git repository'])) {
21
+ console.error(err);
22
+ }
15
23
  return false;
16
24
  }
17
25
  }
@@ -30,7 +38,10 @@ export function branchExists(name, cwd = process.cwd()) {
30
38
  try {
31
39
  exec(`git rev-parse --verify ${name}`, cwd);
32
40
  return true;
33
- } catch {
41
+ } catch (err) {
42
+ if (!isExpectedGitError(err, ['Needed a single revision'])) {
43
+ console.error(err);
44
+ }
34
45
  return false;
35
46
  }
36
47
  }
@@ -71,7 +82,14 @@ export function ensureStagingBranch(cwd = process.cwd()) {
71
82
  export function ensureInitialCommit(cwd = process.cwd()) {
72
83
  try {
73
84
  exec('git rev-parse HEAD', cwd);
74
- } catch {
85
+ } catch (err) {
86
+ const expectedNoCommit = isExpectedGitError(err, [
87
+ 'unknown revision',
88
+ 'path not in the working tree',
89
+ ]);
90
+ if (!expectedNoCommit) {
91
+ console.error(err);
92
+ }
75
93
  // No commits yet — create an initial one
76
94
  exec('git add -A', cwd);
77
95
  exec('git commit -m "chore: initial commit" --allow-empty', cwd);