climaybe 3.0.4 → 3.0.6

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
@@ -304,14 +304,16 @@ Add the following secrets to your GitHub repository (or use **GitLab CI/CD varia
304
304
 
305
305
  | Secret | Required | Description |
306
306
  |--------|----------|-------------|
307
- | `GEMINI_API_KEY` | Yes | Google Gemini API key for changelog generation |
307
+ | `GEMINI_API_KEY` | Optional | Google Gemini API key for AI-generated release notes fallback |
308
308
  | `SHOPIFY_STORE_URL` | Set from config | Store URL is set automatically from the store domain(s) you add during init (no prompt). |
309
- | `SHOPIFY_THEME_ACCESS_TOKEN` | Yes* | Theme access token for preview workflows (required when preview is enabled). |
309
+ | `SHOPIFY_THEME_ACCESS_TOKEN` | Optional* | Theme access token for preview workflows (needed only when you want preview theme publish/cleanup to run). |
310
310
  | `SHOP_ACCESS_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
311
311
  | `LHCI_GITHUB_APP_TOKEN` | Optional* | Required only when optional build workflows are enabled (Lighthouse) |
312
312
  | `SHOP_PASSWORD` | Optional | Used by Lighthouse action when your store requires password auth |
313
313
 
314
- **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.
314
+ **Prompting behavior:** During `climaybe init` (or `add-store`), every GitHub/GitLab secret prompt is skippable. Add values later in CI settings if you prefer.
315
+
316
+ **Store URL:** During `climaybe init` (or `add-store`), store URL secret(s) are set from your configured store domain(s); theme tokens are optional prompts.
315
317
 
316
318
  **Multi-store:** Per-store secrets `SHOPIFY_STORE_URL_<ALIAS>` and `SHOPIFY_THEME_ACCESS_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`). Preview and cleanup: for PRs targeting **main**, **staging**, or **develop** the workflows use the **default store** (from `config.default_store` or first in `config.stores`); for PRs targeting **staging-&lt;alias&gt;** or **live-&lt;alias&gt;** they use that store’s credentials. Set either the plain `SHOPIFY_*` secrets or the `_<ALIAS>` pair for each store you use in preview.
317
319
 
package/bin/version.txt CHANGED
@@ -1 +1 @@
1
- 3.0.4
1
+ 3.0.6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "climaybe",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
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": {
@@ -88,7 +88,13 @@ export async function addStoreCommand() {
88
88
  : { check: isGlabAvailable, checkRemote: hasGitLabRemote, set: setGitLabVariable, name: 'GitLab' };
89
89
 
90
90
  if (!setter.check()) {
91
- console.log(pc.yellow(` ${setter.name} CLI is not installed or not logged in. Add secrets manually in repo Settings.`));
91
+ console.log(
92
+ pc.yellow(
93
+ ciHost === 'github'
94
+ ? ' GitHub CLI is not available (tried gh and npx gh) or not logged in. Add secrets manually in repo Settings.'
95
+ : ` ${setter.name} CLI is not installed or not logged in. Add secrets manually in repo Settings.`
96
+ )
97
+ );
92
98
  } else if (!setter.checkRemote()) {
93
99
  console.log(pc.yellow(' No ' + setter.name + ' remote (origin). Add secrets manually after pushing.'));
94
100
  } else {
@@ -116,22 +122,22 @@ export async function addStoreCommand() {
116
122
  }
117
123
  }
118
124
  const total = secretsToPrompt.length;
119
- console.log(pc.cyan(`\n Configure ${total} secret(s) for store "${store.alias}" (theme token required).\n`));
125
+ console.log(pc.cyan(`\n Configure ${total} secret(s) for store "${store.alias}" (all optional).\n`));
120
126
  for (let i = 0; i < secretsToPrompt.length; i++) {
121
127
  const secret = secretsToPrompt[i];
122
128
  const isThemeToken = secret.name === 'SHOPIFY_THEME_ACCESS_TOKEN' || secret.name.startsWith('SHOPIFY_THEME_ACCESS_TOKEN_');
123
129
  if (isThemeToken && store.domain) {
124
- // Theme tokens are required and must validate; keep prompting until valid + set.
130
+ // Theme tokens are optional; if provided, keep prompting until valid + set.
125
131
  while (true) {
126
132
  const value = await promptSecretValue(secret, i, total);
127
133
  if (!value) {
128
- console.log(pc.red(' Theme access token is required.'));
129
- continue;
134
+ console.log(pc.dim(` Skipped ${secret.name}.`));
135
+ break;
130
136
  }
131
137
  const result = await validateThemeAccessToken(store.domain, value);
132
138
  if (!result.ok) {
133
139
  console.log(pc.red(` Token test failed: ${result.error}`));
134
- console.log(pc.dim(' Please try again with a valid Theme Access token.'));
140
+ console.log(pc.dim(' Enter a valid token, or leave blank to skip.'));
135
141
  continue;
136
142
  }
137
143
  console.log(pc.green(' Token validated against store.'));
@@ -142,7 +148,7 @@ export async function addStoreCommand() {
142
148
  break;
143
149
  } catch (err) {
144
150
  console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
145
- console.log(pc.dim(' Please try entering the token again.'));
151
+ console.log(pc.dim(' Enter again to retry, or leave blank to skip.'));
146
152
  }
147
153
  }
148
154
  continue;
@@ -6,6 +6,8 @@ import {
6
6
  currentBranch,
7
7
  ensureStagingBranch,
8
8
  createStoreBranches,
9
+ hasOriginRemote,
10
+ pushBranchesToOrigin,
9
11
  } from '../lib/git.js';
10
12
 
11
13
  /**
@@ -36,15 +38,26 @@ export async function ensureBranchesCommand() {
36
38
  console.log(pc.dim(` Mode: ${mode}-store (${aliases.length} store(s))\n`));
37
39
 
38
40
  ensureStagingBranch();
41
+ const branchesToPush = ['staging'];
39
42
  for (const alias of aliases) {
40
43
  createStoreBranches(alias);
44
+ branchesToPush.push(`staging-${alias}`, `live-${alias}`);
41
45
  }
42
46
 
43
47
  console.log(pc.bold(pc.green('\n Branches ensured.\n')));
44
- console.log(pc.dim(' Push them so CI can run:'));
45
- console.log(pc.dim(' git push origin staging'));
46
- for (const alias of aliases) {
47
- console.log(pc.dim(` git push origin staging-${alias} live-${alias}`));
48
+ if (hasOriginRemote()) {
49
+ try {
50
+ pushBranchesToOrigin(branchesToPush);
51
+ console.log(pc.green(' Pushed ensured branches to origin.\n'));
52
+ } catch (err) {
53
+ console.log(pc.yellow(` Could not push branches automatically: ${err.message}`));
54
+ console.log(pc.dim(' Push them manually so CI can run:'));
55
+ console.log(pc.dim(' git push origin --all\n'));
56
+ }
57
+ } else {
58
+ console.log(pc.dim(' No origin remote found.'));
59
+ console.log(pc.dim(' Push them after adding a remote so CI can run:'));
60
+ console.log(pc.dim(' git remote add origin <url>'));
61
+ console.log(pc.dim(' git push origin --all\n'));
48
62
  }
49
- console.log(pc.dim(' Or push all at once: git push origin --all\n'));
50
63
  }
@@ -223,7 +223,13 @@ async function runInitFlow() {
223
223
 
224
224
  if (!setter.check()) {
225
225
  const installUrl = ciHost === 'github' ? 'https://cli.github.com/' : 'https://gitlab.com/gitlab-org/cli';
226
- console.log(pc.yellow(` ${setter.name} CLI is not installed or not logged in.`));
226
+ console.log(
227
+ pc.yellow(
228
+ ciHost === 'github'
229
+ ? ' GitHub CLI is not available (tried gh and npx gh) or not logged in.'
230
+ : ` ${setter.name} CLI is not installed or not logged in.`
231
+ )
232
+ );
227
233
  console.log(pc.dim(` Install: ${installUrl} — then run ${ciHost === 'github' ? 'gh' : 'glab'} auth login`));
228
234
  console.log(
229
235
  pc.dim(
@@ -283,23 +289,24 @@ async function runInitFlow() {
283
289
 
284
290
  if (isThemeToken) {
285
291
  if (!storeUrl) {
286
- console.log(pc.red(` Could not resolve store URL for required token ${secret.name}.`));
287
- continue;
292
+ console.log(pc.yellow(` Could not resolve store URL for ${secret.name}, skipping token validation.`));
288
293
  }
289
- // Theme tokens are required and must validate; keep prompting until valid + set.
294
+ // Theme tokens are optional during setup; if provided, keep prompting until valid + set.
290
295
  while (true) {
291
296
  const value = await promptSecretValue(secret, i, totalToPrompt);
292
297
  if (!value) {
293
- console.log(pc.red(' Theme access token is required.'));
294
- continue;
298
+ console.log(pc.dim(` Skipped ${secret.name}.`));
299
+ break;
295
300
  }
296
- const result = await validateThemeAccessToken(storeUrl, value);
297
- if (!result.ok) {
298
- console.log(pc.red(` Token test failed: ${result.error}`));
299
- console.log(pc.dim(' Please try again with a valid Theme Access token.'));
300
- continue;
301
+ if (storeUrl) {
302
+ const result = await validateThemeAccessToken(storeUrl, value);
303
+ if (!result.ok) {
304
+ console.log(pc.red(` Token test failed: ${result.error}`));
305
+ console.log(pc.dim(' Enter a valid token, or leave blank to skip.'));
306
+ continue;
307
+ }
308
+ console.log(pc.green(' Token validated against store.'));
301
309
  }
302
- console.log(pc.green(' Token validated against store.'));
303
310
  try {
304
311
  await setter.set(secret.name, value);
305
312
  console.log(pc.green(` Set ${secret.name}.`));
@@ -307,7 +314,7 @@ async function runInitFlow() {
307
314
  break;
308
315
  } catch (err) {
309
316
  console.log(pc.red(` Failed to set ${secret.name}: ${err.message}`));
310
- console.log(pc.dim(' Please try entering the token again.'));
317
+ console.log(pc.dim(' Enter again to retry, or leave blank to skip.'));
311
318
  }
312
319
  }
313
320
  continue;
package/src/lib/git.js CHANGED
@@ -107,6 +107,28 @@ export function ensureGitRepo(cwd = process.cwd()) {
107
107
  }
108
108
  }
109
109
 
110
+ /**
111
+ * Check if origin remote exists.
112
+ */
113
+ export function hasOriginRemote(cwd = process.cwd()) {
114
+ try {
115
+ exec('git remote get-url origin', cwd);
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Push branches to origin if remote exists.
124
+ */
125
+ export function pushBranchesToOrigin(branches = [], cwd = process.cwd()) {
126
+ if (!branches.length) return true;
127
+ const unique = [...new Set(branches)];
128
+ exec(`git push origin ${unique.join(' ')}`, cwd);
129
+ return true;
130
+ }
131
+
110
132
  /**
111
133
  * Get the latest tag version (e.g. "1.2.3") from v* tags, or null if none.
112
134
  * Sorts by version so v2.0.0 > v1.9.9.
@@ -1,6 +1,20 @@
1
1
  import { spawn, spawnSync, execSync } from 'node:child_process';
2
2
  import pc from 'picocolors';
3
3
 
4
+ function resolveGhInvocation(cwd = process.cwd()) {
5
+ const direct = spawnSync('gh', ['--version'], { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
6
+ if (direct.status === 0) return { command: 'gh', prefix: [] };
7
+
8
+ const viaNpx = spawnSync('npx', ['--yes', 'gh', '--version'], {
9
+ cwd,
10
+ encoding: 'utf-8',
11
+ stdio: ['pipe', 'pipe', 'pipe'],
12
+ });
13
+ if (viaNpx.status === 0) return { command: 'npx', prefix: ['--yes', 'gh'] };
14
+
15
+ return null;
16
+ }
17
+
4
18
  /**
5
19
  * Secret/variable definitions for CI (GitHub Actions or GitLab CI).
6
20
  * condition: 'always' = required for core workflows; 'preview' | 'build' = only when that feature is enabled.
@@ -8,7 +22,7 @@ import pc from 'picocolors';
8
22
  export const SECRET_DEFINITIONS = [
9
23
  {
10
24
  name: 'GEMINI_API_KEY',
11
- required: true,
25
+ required: false,
12
26
  condition: 'always',
13
27
  description: 'Google Gemini API key for AI-generated changelogs on release',
14
28
  whereToGet:
@@ -24,7 +38,7 @@ export const SECRET_DEFINITIONS = [
24
38
  },
25
39
  {
26
40
  name: 'SHOPIFY_THEME_ACCESS_TOKEN',
27
- required: true,
41
+ required: false,
28
42
  condition: 'preview',
29
43
  description: 'Theme access token so CI can push preview themes (password from Shopify Theme Access app)',
30
44
  whereToGet:
@@ -60,8 +74,13 @@ export const SECRET_DEFINITIONS = [
60
74
  */
61
75
  export function isGhAvailable() {
62
76
  try {
63
- execSync('gh auth status', { encoding: 'utf-8', stdio: 'pipe' });
64
- return true;
77
+ const gh = resolveGhInvocation();
78
+ if (!gh) return false;
79
+ const result = spawnSync(gh.command, [...gh.prefix, 'auth', 'status'], {
80
+ encoding: 'utf-8',
81
+ stdio: ['pipe', 'pipe', 'pipe'],
82
+ });
83
+ return result.status === 0;
65
84
  } catch {
66
85
  return false;
67
86
  }
@@ -99,10 +118,12 @@ export function getGitHubRepoSpec(cwd = process.cwd()) {
99
118
  */
100
119
  export function listGitHubSecrets(cwd = process.cwd()) {
101
120
  try {
121
+ const gh = resolveGhInvocation(cwd);
122
+ if (!gh) return [];
102
123
  const repo = getGitHubRepoSpec(cwd);
103
- const args = ['secret', 'list', '--json', 'name'];
124
+ const args = [...gh.prefix, 'secret', 'list', '--json', 'name'];
104
125
  if (repo) args.push('-R', repo);
105
- const result = spawnSync('gh', args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
126
+ const result = spawnSync(gh.command, args, { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
106
127
  const out = (result.stdout || '').trim();
107
128
  const data = JSON.parse(out || '[]');
108
129
  return Array.isArray(data) ? data.map((s) => s.name).filter(Boolean) : [];
@@ -131,10 +152,15 @@ export function listGitLabVariables(cwd = process.cwd()) {
131
152
  */
132
153
  export function setSecret(name, value, cwd = process.cwd()) {
133
154
  return new Promise((resolve, reject) => {
155
+ const gh = resolveGhInvocation(cwd);
156
+ if (!gh) {
157
+ reject(new Error('GitHub CLI is not available (tried gh and npx gh)'));
158
+ return;
159
+ }
134
160
  const repo = getGitHubRepoSpec(cwd);
135
- const args = ['secret', 'set', name];
161
+ const args = [...gh.prefix, 'secret', 'set', name];
136
162
  if (repo) args.push('-R', repo);
137
- const child = spawn('gh', args, {
163
+ const child = spawn(gh.command, args, {
138
164
  stdio: ['pipe', 'inherit', 'inherit'],
139
165
  cwd,
140
166
  });
@@ -295,7 +321,7 @@ export function getSecretsToPrompt({ enablePreviewWorkflows, enableBuildWorkflow
295
321
  if (enablePreviewWorkflows) {
296
322
  list.push({
297
323
  name: `SHOPIFY_THEME_ACCESS_TOKEN_${suffix}`,
298
- required: true,
324
+ required: false,
299
325
  description: `Store ${store.alias}: Theme access token (password from Theme Access app)`,
300
326
  whereToGet: 'Theme Access app in Shopify — the password it gives is the token for this store.',
301
327
  });
@@ -338,7 +364,7 @@ export function getSecretsToPromptForNewStore(store) {
338
364
  return [
339
365
  {
340
366
  name: `SHOPIFY_THEME_ACCESS_TOKEN_${suffix}`,
341
- required: true,
367
+ required: false,
342
368
  description: `Store ${store.alias}: Theme access token (password from Theme Access app)`,
343
369
  whereToGet: 'Theme Access app in Shopify — the password it gives is the token for this store.',
344
370
  },