climaybe 1.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 +204 -0
- package/bin/cli.js +5 -0
- package/package.json +31 -0
- package/src/commands/add-store.js +55 -0
- package/src/commands/init.js +78 -0
- package/src/commands/switch.js +27 -0
- package/src/commands/sync.js +42 -0
- package/src/commands/update-workflows.js +18 -0
- package/src/index.js +44 -0
- package/src/lib/config.js +92 -0
- package/src/lib/git.js +90 -0
- package/src/lib/prompts.js +117 -0
- package/src/lib/store-sync.js +142 -0
- package/src/lib/workflows.js +99 -0
- package/src/workflows/multi/hotfix-backport.yml +100 -0
- package/src/workflows/multi/main-to-staging-stores.yml +118 -0
- package/src/workflows/multi/pr-to-live.yml +71 -0
- package/src/workflows/multi/root-to-stores.yml +84 -0
- package/src/workflows/multi/stores-to-root.yml +89 -0
- package/src/workflows/shared/ai-changelog.yml +108 -0
- package/src/workflows/shared/version-bump.yml +122 -0
- package/src/workflows/single/nightly-hotfix.yml +73 -0
- package/src/workflows/single/post-merge-tag.yml +96 -0
- package/src/workflows/single/release-pr-check.yml +136 -0
package/README.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# climaybe
|
|
2
|
+
|
|
3
|
+
Shopify CI/CD CLI — scaffolds GitHub Actions workflows, branch strategy, and store config for single-store and multi-store theme repositories.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g climaybe
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
cd your-shopify-theme-repo
|
|
15
|
+
climaybe init
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The interactive setup will ask for your store URL(s) and configure everything automatically.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### `climaybe init`
|
|
23
|
+
|
|
24
|
+
Interactive setup that configures your repo for CI/CD.
|
|
25
|
+
|
|
26
|
+
1. Prompts for your store URL (e.g., `voldt-staging.myshopify.com`)
|
|
27
|
+
2. Extracts subdomain as alias, lets you override
|
|
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)
|
|
33
|
+
|
|
34
|
+
### `climaybe add-store`
|
|
35
|
+
|
|
36
|
+
Add a new store to an existing setup.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
climaybe add-store
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- Prompts for new store URL + alias
|
|
43
|
+
- Creates `staging-<alias>` and `live-<alias>` branches
|
|
44
|
+
- Creates `stores/<alias>/` directory structure
|
|
45
|
+
- If store count goes from 1 to 2+, automatically migrates from single to multi-store mode
|
|
46
|
+
|
|
47
|
+
### `climaybe switch <alias>`
|
|
48
|
+
|
|
49
|
+
Switch your local dev environment to a specific store (multi-store only).
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
climaybe switch voldt-norway
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Copies `stores/<alias>/` JSON files to the repo root so you can preview that store locally.
|
|
56
|
+
|
|
57
|
+
### `climaybe sync [alias]`
|
|
58
|
+
|
|
59
|
+
Sync root JSON files back to a store directory (multi-store only).
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
climaybe sync voldt-norway
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
If no alias is given, syncs to the default store.
|
|
66
|
+
|
|
67
|
+
### `climaybe update-workflows`
|
|
68
|
+
|
|
69
|
+
Refresh GitHub Actions workflows from the latest bundled templates.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
climaybe update-workflows
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Useful after updating the CLI to get the latest workflow improvements.
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
The CLI writes config into the `config` field of your `package.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"config": {
|
|
84
|
+
"port": 9295,
|
|
85
|
+
"default_store": "voldt-staging.myshopify.com",
|
|
86
|
+
"stores": {
|
|
87
|
+
"voldt-staging": "voldt-staging.myshopify.com",
|
|
88
|
+
"voldt-norway": "voldt-norway.myshopify.com"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Workflows read this config at runtime — no hardcoded values in YAML files.
|
|
95
|
+
|
|
96
|
+
## Branch Strategy
|
|
97
|
+
|
|
98
|
+
### Single-store
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
staging → main
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- `staging` — development branch
|
|
105
|
+
- `main` — production branch
|
|
106
|
+
|
|
107
|
+
### Multi-store
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
staging → main → staging-<store> → live-<store>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
- `staging` — development branch
|
|
114
|
+
- `main` — shared codebase (not live)
|
|
115
|
+
- `staging-<store>` — per-store staging with store-specific JSON data
|
|
116
|
+
- `live-<store>` — per-store production
|
|
117
|
+
|
|
118
|
+
## Workflows
|
|
119
|
+
|
|
120
|
+
### Shared (both modes)
|
|
121
|
+
|
|
122
|
+
| Workflow | Purpose |
|
|
123
|
+
|----------|---------|
|
|
124
|
+
| `ai-changelog.yml` | Reusable workflow. Sends commits to Gemini API, returns classified changelog. |
|
|
125
|
+
| `version-bump.yml` | Reusable workflow. Bumps version in `settings_schema.json`, creates git tag. |
|
|
126
|
+
|
|
127
|
+
### Single-store
|
|
128
|
+
|
|
129
|
+
| Workflow | Trigger | What it does |
|
|
130
|
+
|----------|---------|-------------|
|
|
131
|
+
| `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 |
|
|
133
|
+
| `nightly-hotfix.yml` | Cron 02:00 US Eastern | Tags untagged hotfix commits with patch version |
|
|
134
|
+
|
|
135
|
+
### Multi-store (additional)
|
|
136
|
+
|
|
137
|
+
| Workflow | Trigger | What it does |
|
|
138
|
+
|----------|---------|-------------|
|
|
139
|
+
| `main-to-staging-stores.yml` | Push to `main` | Opens PRs to each `staging-<alias>` branch |
|
|
140
|
+
| `stores-to-root.yml` | Push to `staging-*` | Copies `stores/<alias>/` JSONs to repo root |
|
|
141
|
+
| `pr-to-live.yml` | After stores-to-root | Opens PR from `staging-<alias>` to `live-<alias>` |
|
|
142
|
+
| `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` |
|
|
144
|
+
|
|
145
|
+
## Versioning
|
|
146
|
+
|
|
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
|
|
150
|
+
|
|
151
|
+
All version bumps update `config/settings_schema.json` automatically.
|
|
152
|
+
|
|
153
|
+
## File Sync Rules (Multi-store)
|
|
154
|
+
|
|
155
|
+
**Synced between root and `stores/<alias>/`:**
|
|
156
|
+
- `config/settings_data.json`
|
|
157
|
+
- `templates/*.json`
|
|
158
|
+
- `sections/*.json`
|
|
159
|
+
|
|
160
|
+
**NOT synced (travels via branch history):**
|
|
161
|
+
- `config/settings_schema.json`
|
|
162
|
+
- `locales/*.json`
|
|
163
|
+
|
|
164
|
+
## Recursive Trigger Prevention
|
|
165
|
+
|
|
166
|
+
- Hotfix backport commits contain `[hotfix-backport]` in the message
|
|
167
|
+
- Store sync commits contain `[stores-to-root]` or `[root-to-stores]`
|
|
168
|
+
- Version bump commits contain `chore(release): bump version`
|
|
169
|
+
- All workflows check for these flags and skip accordingly
|
|
170
|
+
|
|
171
|
+
## GitHub Secrets
|
|
172
|
+
|
|
173
|
+
Add the following secret to your GitHub repository:
|
|
174
|
+
|
|
175
|
+
| Secret | Required | Description |
|
|
176
|
+
|--------|----------|-------------|
|
|
177
|
+
| `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
|
|
178
|
+
|
|
179
|
+
## Directory Structure (Multi-store)
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
├── assets/
|
|
183
|
+
├── config/
|
|
184
|
+
├── layout/
|
|
185
|
+
├── locales/
|
|
186
|
+
├── sections/
|
|
187
|
+
├── snippets/
|
|
188
|
+
├── templates/
|
|
189
|
+
├── stores/
|
|
190
|
+
│ ├── voldt-staging/
|
|
191
|
+
│ │ ├── config/settings_data.json
|
|
192
|
+
│ │ ├── templates/*.json
|
|
193
|
+
│ │ └── sections/*.json
|
|
194
|
+
│ └── voldt-norway/
|
|
195
|
+
│ ├── config/settings_data.json
|
|
196
|
+
│ ├── templates/*.json
|
|
197
|
+
│ └── sections/*.json
|
|
198
|
+
├── package.json
|
|
199
|
+
└── .github/workflows/
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
MIT — Electric Maybe
|
package/bin/cli.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "climaybe",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config for single-store and multi-store theme repos",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"climaybe": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"shopify",
|
|
18
|
+
"ci",
|
|
19
|
+
"cd",
|
|
20
|
+
"theme",
|
|
21
|
+
"workflow",
|
|
22
|
+
"github-actions"
|
|
23
|
+
],
|
|
24
|
+
"author": "Electric Maybe",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"commander": "^13.1.0",
|
|
28
|
+
"picocolors": "^1.1.1",
|
|
29
|
+
"prompts": "^2.4.2"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { promptNewStore } from '../lib/prompts.js';
|
|
3
|
+
import { readConfig, addStoreToConfig, getStoreAliases, getMode } from '../lib/config.js';
|
|
4
|
+
import { createStoreBranches } from '../lib/git.js';
|
|
5
|
+
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
6
|
+
import { createStoreDirectories } from '../lib/store-sync.js';
|
|
7
|
+
|
|
8
|
+
export async function addStoreCommand() {
|
|
9
|
+
console.log(pc.bold('\n climaybe — Add Store\n'));
|
|
10
|
+
|
|
11
|
+
// Guard: config must exist
|
|
12
|
+
const config = readConfig();
|
|
13
|
+
if (!config?.stores) {
|
|
14
|
+
console.log(pc.red(' No climaybe config found. Run "climaybe init" first.\n'));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const existingAliases = getStoreAliases();
|
|
19
|
+
const previousMode = getMode();
|
|
20
|
+
|
|
21
|
+
// Prompt for new store
|
|
22
|
+
const store = await promptNewStore(existingAliases);
|
|
23
|
+
if (!store) return;
|
|
24
|
+
|
|
25
|
+
// Add to config
|
|
26
|
+
addStoreToConfig(store.alias, store.domain);
|
|
27
|
+
console.log(pc.green(` Added store: ${store.alias} → ${store.domain}`));
|
|
28
|
+
|
|
29
|
+
const newMode = getMode();
|
|
30
|
+
|
|
31
|
+
// Create store branches + directories
|
|
32
|
+
createStoreBranches(store.alias);
|
|
33
|
+
createStoreDirectories(store.alias);
|
|
34
|
+
|
|
35
|
+
// Handle single → multi migration
|
|
36
|
+
if (previousMode === 'single' && newMode === 'multi') {
|
|
37
|
+
console.log(pc.cyan('\n Migrating from single-store to multi-store mode...'));
|
|
38
|
+
|
|
39
|
+
// Create branches for the original store too
|
|
40
|
+
const originalAlias = existingAliases[0];
|
|
41
|
+
createStoreBranches(originalAlias);
|
|
42
|
+
createStoreDirectories(originalAlias);
|
|
43
|
+
|
|
44
|
+
// Re-scaffold workflows for multi mode
|
|
45
|
+
scaffoldWorkflows('multi');
|
|
46
|
+
console.log(pc.green(' Migration complete — workflows updated to multi-store mode.'));
|
|
47
|
+
} else if (newMode === 'multi') {
|
|
48
|
+
// Already multi, just make sure workflows are current
|
|
49
|
+
scaffoldWorkflows('multi');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
console.log(pc.bold(pc.green('\n Store added successfully!\n')));
|
|
53
|
+
console.log(pc.dim(` New branches: staging-${store.alias}, live-${store.alias}`));
|
|
54
|
+
console.log(pc.dim(` Store dir: stores/${store.alias}/\n`));
|
|
55
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { promptStoreLoop } from '../lib/prompts.js';
|
|
3
|
+
import { readConfig, writeConfig } from '../lib/config.js';
|
|
4
|
+
import { ensureGitRepo, ensureInitialCommit, ensureStagingBranch, createStoreBranches } from '../lib/git.js';
|
|
5
|
+
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
6
|
+
import { createStoreDirectories } from '../lib/store-sync.js';
|
|
7
|
+
|
|
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
|
+
|
|
20
|
+
// 1. Collect stores from user
|
|
21
|
+
const stores = await promptStoreLoop();
|
|
22
|
+
const mode = stores.length > 1 ? 'multi' : 'single';
|
|
23
|
+
|
|
24
|
+
console.log(pc.dim(`\n Mode: ${mode}-store (${stores.length} store(s))`));
|
|
25
|
+
|
|
26
|
+
// 2. Build config
|
|
27
|
+
const config = {
|
|
28
|
+
port: 9295,
|
|
29
|
+
default_store: stores[0].domain,
|
|
30
|
+
stores: {},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
for (const s of stores) {
|
|
34
|
+
config.stores[s.alias] = s.domain;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 3. Write package.json config
|
|
38
|
+
writeConfig(config);
|
|
39
|
+
console.log(pc.green(' Updated package.json config.'));
|
|
40
|
+
|
|
41
|
+
// 4. Ensure git repo
|
|
42
|
+
ensureGitRepo();
|
|
43
|
+
ensureInitialCommit();
|
|
44
|
+
|
|
45
|
+
// 5. Create branches
|
|
46
|
+
if (mode === 'single') {
|
|
47
|
+
ensureStagingBranch();
|
|
48
|
+
} else {
|
|
49
|
+
// Multi-store: staging branch + per-store branches
|
|
50
|
+
ensureStagingBranch();
|
|
51
|
+
for (const s of stores) {
|
|
52
|
+
createStoreBranches(s.alias);
|
|
53
|
+
createStoreDirectories(s.alias);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 6. Scaffold workflows
|
|
58
|
+
scaffoldWorkflows(mode);
|
|
59
|
+
|
|
60
|
+
// Done
|
|
61
|
+
console.log(pc.bold(pc.green('\n Setup complete!\n')));
|
|
62
|
+
|
|
63
|
+
if (mode === 'single') {
|
|
64
|
+
console.log(pc.dim(' Branches: main, staging'));
|
|
65
|
+
console.log(pc.dim(' Workflow: staging → main with versioning + nightly hotfix tagging'));
|
|
66
|
+
} else {
|
|
67
|
+
console.log(pc.dim(' Branches: main, staging'));
|
|
68
|
+
for (const s of stores) {
|
|
69
|
+
console.log(pc.dim(` staging-${s.alias}, live-${s.alias}`));
|
|
70
|
+
}
|
|
71
|
+
console.log(pc.dim(' Workflow: staging → main → staging-<store> → live-<store>'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
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'));
|
|
77
|
+
console.log(pc.dim(' 3. Tag your first release: git tag v1.0.0\n'));
|
|
78
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getStoreAliases, getMode } from '../lib/config.js';
|
|
3
|
+
import { storesToRoot } from '../lib/store-sync.js';
|
|
4
|
+
|
|
5
|
+
export async function switchCommand(alias) {
|
|
6
|
+
console.log(pc.bold('\n climaybe — Switch Store\n'));
|
|
7
|
+
|
|
8
|
+
const mode = getMode();
|
|
9
|
+
if (mode !== 'multi') {
|
|
10
|
+
console.log(pc.yellow(' Switch is only available in multi-store mode.\n'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const aliases = getStoreAliases();
|
|
15
|
+
if (!aliases.includes(alias)) {
|
|
16
|
+
console.log(pc.red(` Unknown store alias: "${alias}"`));
|
|
17
|
+
console.log(pc.dim(` Available: ${aliases.join(', ')}\n`));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ok = storesToRoot(alias);
|
|
22
|
+
if (ok) {
|
|
23
|
+
console.log(pc.bold(pc.green(`\n Switched to store: ${alias}\n`)));
|
|
24
|
+
console.log(pc.dim(' Root JSON files now reflect this store\'s data.'));
|
|
25
|
+
console.log(pc.dim(' Use "climaybe sync" to write changes back.\n'));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getStoreAliases, getMode, readConfig } from '../lib/config.js';
|
|
3
|
+
import { rootToStores } from '../lib/store-sync.js';
|
|
4
|
+
|
|
5
|
+
export async function syncCommand(alias) {
|
|
6
|
+
console.log(pc.bold('\n climaybe — Sync to Store\n'));
|
|
7
|
+
|
|
8
|
+
const mode = getMode();
|
|
9
|
+
if (mode !== 'multi') {
|
|
10
|
+
console.log(pc.yellow(' Sync is only available in multi-store mode.\n'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const aliases = getStoreAliases();
|
|
15
|
+
|
|
16
|
+
// If no alias provided, use default store
|
|
17
|
+
if (!alias) {
|
|
18
|
+
const config = readConfig();
|
|
19
|
+
const defaultDomain = config?.default_store;
|
|
20
|
+
// Find alias by domain
|
|
21
|
+
alias = aliases.find((a) => config.stores[a] === defaultDomain);
|
|
22
|
+
|
|
23
|
+
if (!alias) {
|
|
24
|
+
console.log(pc.red(' No alias provided and no default store found.'));
|
|
25
|
+
console.log(pc.dim(` Usage: climaybe sync <alias>`));
|
|
26
|
+
console.log(pc.dim(` Available: ${aliases.join(', ')}\n`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
console.log(pc.dim(` Using default store: ${alias}`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!aliases.includes(alias)) {
|
|
33
|
+
console.log(pc.red(` Unknown store alias: "${alias}"`));
|
|
34
|
+
console.log(pc.dim(` Available: ${aliases.join(', ')}\n`));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ok = rootToStores(alias);
|
|
39
|
+
if (ok) {
|
|
40
|
+
console.log(pc.bold(pc.green(`\n Synced root → stores/${alias}/\n`)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getMode, readConfig } from '../lib/config.js';
|
|
3
|
+
import { scaffoldWorkflows } from '../lib/workflows.js';
|
|
4
|
+
|
|
5
|
+
export async function updateWorkflowsCommand() {
|
|
6
|
+
console.log(pc.bold('\n climaybe — Update Workflows\n'));
|
|
7
|
+
|
|
8
|
+
const config = readConfig();
|
|
9
|
+
if (!config?.stores) {
|
|
10
|
+
console.log(pc.red(' No climaybe config found. Run "climaybe init" first.\n'));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const mode = getMode();
|
|
15
|
+
scaffoldWorkflows(mode);
|
|
16
|
+
|
|
17
|
+
console.log(pc.bold(pc.green('\n Workflows updated!\n')));
|
|
18
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { initCommand } from './commands/init.js';
|
|
3
|
+
import { addStoreCommand } from './commands/add-store.js';
|
|
4
|
+
import { switchCommand } from './commands/switch.js';
|
|
5
|
+
import { syncCommand } from './commands/sync.js';
|
|
6
|
+
import { updateWorkflowsCommand } from './commands/update-workflows.js';
|
|
7
|
+
|
|
8
|
+
export function run(argv) {
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('climaybe')
|
|
13
|
+
.description('Shopify CI/CD CLI — scaffolds workflows, branch strategy, and store config')
|
|
14
|
+
.version('1.0.0');
|
|
15
|
+
|
|
16
|
+
program
|
|
17
|
+
.command('init')
|
|
18
|
+
.description('Initialize CI/CD setup for a Shopify theme repo')
|
|
19
|
+
.action(initCommand);
|
|
20
|
+
|
|
21
|
+
program
|
|
22
|
+
.command('add-store')
|
|
23
|
+
.description('Add a new store to an existing multi-store config')
|
|
24
|
+
.action(addStoreCommand);
|
|
25
|
+
|
|
26
|
+
program
|
|
27
|
+
.command('switch')
|
|
28
|
+
.argument('<alias>', 'Store alias to switch to')
|
|
29
|
+
.description('Switch local dev environment to a specific store')
|
|
30
|
+
.action(switchCommand);
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.command('sync')
|
|
34
|
+
.argument('[alias]', 'Store alias to sync to (defaults to active store)')
|
|
35
|
+
.description('Sync root JSON files back to a store directory')
|
|
36
|
+
.action(syncCommand);
|
|
37
|
+
|
|
38
|
+
program
|
|
39
|
+
.command('update-workflows')
|
|
40
|
+
.description('Refresh GitHub Actions workflows from latest bundled templates')
|
|
41
|
+
.action(updateWorkflowsCommand);
|
|
42
|
+
|
|
43
|
+
program.parse(argv);
|
|
44
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const PKG = 'package.json';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve absolute path to the target repo's package.json.
|
|
8
|
+
* Defaults to cwd.
|
|
9
|
+
*/
|
|
10
|
+
function pkgPath(cwd = process.cwd()) {
|
|
11
|
+
return join(cwd, PKG);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read the full package.json from a target repo.
|
|
16
|
+
* Returns null if it doesn't exist.
|
|
17
|
+
*/
|
|
18
|
+
export function readPkg(cwd = process.cwd()) {
|
|
19
|
+
const p = pkgPath(cwd);
|
|
20
|
+
if (!existsSync(p)) return null;
|
|
21
|
+
return JSON.parse(readFileSync(p, 'utf-8'));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write the full package.json object back to disk.
|
|
26
|
+
*/
|
|
27
|
+
export function writePkg(pkg, cwd = process.cwd()) {
|
|
28
|
+
writeFileSync(pkgPath(cwd), JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Read the climaybe config section from package.json.
|
|
33
|
+
* Returns null if package.json or config section doesn't exist.
|
|
34
|
+
*/
|
|
35
|
+
export function readConfig(cwd = process.cwd()) {
|
|
36
|
+
const pkg = readPkg(cwd);
|
|
37
|
+
return pkg?.config ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Write/merge the climaybe config section into package.json.
|
|
42
|
+
* Creates package.json if it doesn't exist.
|
|
43
|
+
*/
|
|
44
|
+
export function writeConfig(config, cwd = process.cwd()) {
|
|
45
|
+
let pkg = readPkg(cwd);
|
|
46
|
+
if (!pkg) {
|
|
47
|
+
pkg = {
|
|
48
|
+
name: 'shopify-theme',
|
|
49
|
+
version: '1.0.0',
|
|
50
|
+
private: true,
|
|
51
|
+
config: {},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
pkg.config = { ...pkg.config, ...config };
|
|
55
|
+
writePkg(pkg, cwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the list of store aliases from config.
|
|
60
|
+
*/
|
|
61
|
+
export function getStoreAliases(cwd = process.cwd()) {
|
|
62
|
+
const config = readConfig(cwd);
|
|
63
|
+
if (!config?.stores) return [];
|
|
64
|
+
return Object.keys(config.stores);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Determine the current mode: 'single' or 'multi'.
|
|
69
|
+
*/
|
|
70
|
+
export function getMode(cwd = process.cwd()) {
|
|
71
|
+
const aliases = getStoreAliases(cwd);
|
|
72
|
+
return aliases.length > 1 ? 'multi' : 'single';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Add a store entry to the config.
|
|
77
|
+
* Returns the updated config.
|
|
78
|
+
*/
|
|
79
|
+
export function addStoreToConfig(alias, domain, cwd = process.cwd()) {
|
|
80
|
+
const config = readConfig(cwd) || { port: 9295, stores: {} };
|
|
81
|
+
if (!config.stores) config.stores = {};
|
|
82
|
+
|
|
83
|
+
config.stores[alias] = domain;
|
|
84
|
+
|
|
85
|
+
// Set as default if it's the first store
|
|
86
|
+
if (!config.default_store) {
|
|
87
|
+
config.default_store = domain;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
writeConfig(config, cwd);
|
|
91
|
+
return readConfig(cwd);
|
|
92
|
+
}
|
package/src/lib/git.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
|
|
4
|
+
const exec = (cmd, cwd = process.cwd()) =>
|
|
5
|
+
execSync(cmd, { cwd, encoding: 'utf-8', stdio: 'pipe' }).trim();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if current directory is a git repo.
|
|
9
|
+
*/
|
|
10
|
+
export function isGitRepo(cwd = process.cwd()) {
|
|
11
|
+
try {
|
|
12
|
+
exec('git rev-parse --is-inside-work-tree', cwd);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the current branch name.
|
|
21
|
+
*/
|
|
22
|
+
export function currentBranch(cwd = process.cwd()) {
|
|
23
|
+
return exec('git rev-parse --abbrev-ref HEAD', cwd);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Check if a local branch exists.
|
|
28
|
+
*/
|
|
29
|
+
export function branchExists(name, cwd = process.cwd()) {
|
|
30
|
+
try {
|
|
31
|
+
exec(`git rev-parse --verify ${name}`, cwd);
|
|
32
|
+
return true;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new branch from current HEAD (does not checkout).
|
|
40
|
+
*/
|
|
41
|
+
export function createBranch(name, cwd = process.cwd()) {
|
|
42
|
+
if (branchExists(name, cwd)) {
|
|
43
|
+
console.log(pc.yellow(` Branch "${name}" already exists, skipping.`));
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
exec(`git branch ${name}`, cwd);
|
|
47
|
+
console.log(pc.green(` Created branch: ${name}`));
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create staging and live branches for a store alias.
|
|
53
|
+
*/
|
|
54
|
+
export function createStoreBranches(alias, cwd = process.cwd()) {
|
|
55
|
+
const staging = `staging-${alias}`;
|
|
56
|
+
const live = `live-${alias}`;
|
|
57
|
+
createBranch(staging, cwd);
|
|
58
|
+
createBranch(live, cwd);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Ensure the staging branch exists (for single-store mode).
|
|
63
|
+
*/
|
|
64
|
+
export function ensureStagingBranch(cwd = process.cwd()) {
|
|
65
|
+
createBranch('staging', cwd);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Create an initial commit if the repo has no commits.
|
|
70
|
+
*/
|
|
71
|
+
export function ensureInitialCommit(cwd = process.cwd()) {
|
|
72
|
+
try {
|
|
73
|
+
exec('git rev-parse HEAD', cwd);
|
|
74
|
+
} catch {
|
|
75
|
+
// No commits yet — create an initial one
|
|
76
|
+
exec('git add -A', cwd);
|
|
77
|
+
exec('git commit -m "chore: initial commit" --allow-empty', cwd);
|
|
78
|
+
console.log(pc.green(' Created initial commit.'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Initialize a git repo if not already one.
|
|
84
|
+
*/
|
|
85
|
+
export function ensureGitRepo(cwd = process.cwd()) {
|
|
86
|
+
if (!isGitRepo(cwd)) {
|
|
87
|
+
exec('git init', cwd);
|
|
88
|
+
console.log(pc.green(' Initialized git repository.'));
|
|
89
|
+
}
|
|
90
|
+
}
|