@daemux/store-automator 0.10.9 → 0.10.11

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.
@@ -5,14 +5,14 @@
5
5
  },
6
6
  "metadata": {
7
7
  "description": "App Store & Google Play automation for Flutter apps",
8
- "version": "0.10.9"
8
+ "version": "0.10.11"
9
9
  },
10
10
  "plugins": [
11
11
  {
12
12
  "name": "store-automator",
13
13
  "source": "./plugins/store-automator",
14
14
  "description": "3 agents for app store publishing: reviewer, meta-creator, media-designer",
15
- "version": "0.10.9",
15
+ "version": "0.10.11",
16
16
  "keywords": [
17
17
  "flutter",
18
18
  "app-store",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemux/store-automator",
3
- "version": "0.10.9",
3
+ "version": "0.10.11",
4
4
  "description": "Full App Store & Google Play automation for Flutter apps with Claude Code agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "store-automator",
3
- "version": "0.10.9",
3
+ "version": "0.10.11",
4
4
  "description": "App Store & Google Play automation agents for Flutter app publishing",
5
5
  "author": {
6
6
  "name": "Daemux"
package/src/ci-config.mjs CHANGED
@@ -3,6 +3,35 @@ import { join } from 'node:path';
3
3
 
4
4
  const CI_CONFIG_FILE = 'ci.config.yaml';
5
5
 
6
+ const KEY_TO_CAMEL = {
7
+ 'app.name': 'appName',
8
+ 'app.bundle_id': 'bundleId',
9
+ 'app.package_name': 'packageName',
10
+ 'app.sku': 'sku',
11
+ 'app.apple_id': 'appleId',
12
+ 'credentials.apple.key_id': 'keyId',
13
+ 'credentials.apple.issuer_id': 'issuerId',
14
+ 'credentials.android.keystore_password': 'keystorePassword',
15
+ 'ios.primary_category': 'primaryCategory',
16
+ 'ios.secondary_category': 'secondaryCategory',
17
+ 'ios.price_tier': 'priceTier',
18
+ 'ios.submit_for_review': 'submitForReview',
19
+ 'ios.automatic_release': 'automaticRelease',
20
+ 'android.track': 'track',
21
+ 'android.rollout_fraction': 'rolloutFraction',
22
+ 'android.in_app_update_priority': 'inAppUpdatePriority',
23
+ 'web.domain': 'domain',
24
+ 'web.cloudflare_project_name': 'cfProjectName',
25
+ 'web.tagline': 'tagline',
26
+ 'web.primary_color': 'primaryColor',
27
+ 'web.secondary_color': 'secondaryColor',
28
+ 'web.company_name': 'companyName',
29
+ 'web.contact_email': 'contactEmail',
30
+ 'web.support_email': 'supportEmail',
31
+ 'web.jurisdiction': 'jurisdiction',
32
+ 'metadata.languages': 'languages',
33
+ };
34
+
6
35
  const FIELD_PATTERNS = {
7
36
  'app.name': { regex: /^( name: ).*$/m, replacement: (v) => ` name: "${v}"` },
8
37
  'app.bundle_id': { regex: /^( bundle_id: ).*$/m, replacement: (v) => ` bundle_id: "${v}"` },
@@ -80,26 +109,28 @@ function extractFieldValue(content, regex) {
80
109
  return raw;
81
110
  }
82
111
 
83
- export function isPlaceholder(value) {
84
- if (value === undefined || value === null || value === '') return true;
85
- const s = String(value);
86
- if (s.startsWith('REPLACE_WITH_')) return true;
87
- if (s.startsWith('yourapp')) return true;
88
- if (s.startsWith('com.yourcompany.')) return true;
89
- if (s === 'your@email.com') return true;
90
- return false;
91
- }
112
+ export { isPlaceholder } from './guide.mjs';
92
113
 
93
114
  export function readCiConfig(projectDir) {
94
115
  const configPath = join(projectDir, CI_CONFIG_FILE);
95
116
  if (!existsSync(configPath)) return {};
96
117
  const content = readFileSync(configPath, 'utf-8');
97
- const config = {};
118
+ const raw = {};
98
119
  for (const [key, { regex }] of Object.entries(FIELD_PATTERNS)) {
99
120
  const val = extractFieldValue(content, regex);
100
- if (val !== undefined) config[key] = val;
121
+ if (val !== undefined) raw[key] = val;
122
+ }
123
+ raw['metadata.languages'] = extractLanguages(content);
124
+
125
+ const config = {};
126
+ for (const [dotKey, value] of Object.entries(raw)) {
127
+ const camelKey = KEY_TO_CAMEL[dotKey];
128
+ if (camelKey) {
129
+ config[camelKey] = value;
130
+ } else {
131
+ config[dotKey] = value;
132
+ }
101
133
  }
102
- config['metadata.languages'] = extractLanguages(content);
103
134
  return config;
104
135
  }
105
136
 
@@ -178,11 +209,13 @@ export function writeMatchConfig(projectDir, { deployKeyPath, gitUrl }) {
178
209
  const guRegex = /^(\s*git_url:\s*)"[^"]*"/m;
179
210
 
180
211
  if (deployKeyPath && dpRegex.test(content)) {
181
- content = content.replace(dpRegex, `$1"${deployKeyPath}"`);
212
+ const safe = deployKeyPath.replace(/\$/g, '$$$$');
213
+ content = content.replace(dpRegex, `$1"${safe}"`);
182
214
  changed = true;
183
215
  }
184
216
  if (gitUrl && guRegex.test(content)) {
185
- content = content.replace(guRegex, `$1"${gitUrl}"`);
217
+ const safe = gitUrl.replace(/\$/g, '$$$$');
218
+ content = content.replace(guRegex, `$1"${safe}"`);
186
219
  changed = true;
187
220
  }
188
221
  if (changed) writeFileSync(configPath, content, 'utf-8');
package/src/guide.mjs CHANGED
@@ -1,4 +1,3 @@
1
- import { createInterface } from 'node:readline';
2
1
  import { existsSync } from 'node:fs';
3
2
 
4
3
  const BOLD = '\x1b[1m';
@@ -7,14 +6,18 @@ const YELLOW = '\x1b[33m';
7
6
  const GREEN = '\x1b[32m';
8
7
  const RESET = '\x1b[0m';
9
8
 
10
- function isNonInteractive() {
9
+ export function isNonInteractive() {
11
10
  return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
12
11
  }
13
12
 
14
- export function printGuide(title, steps) {
13
+ export function printSectionHeader(title) {
15
14
  console.log('');
16
15
  console.log(`${BOLD}${CYAN}=== ${title} ===${RESET}`);
17
16
  console.log('');
17
+ }
18
+
19
+ export function printGuide(title, steps) {
20
+ printSectionHeader(title);
18
21
  for (let i = 0; i < steps.length; i++) {
19
22
  console.log(` ${i + 1}. ${steps[i]}`);
20
23
  }
@@ -43,7 +46,9 @@ export function verifyFileExists(filePath, description) {
43
46
  return false;
44
47
  }
45
48
 
46
- export async function runGuide(rl, guide) {
49
+ export async function runGuide(rl, guide, { skip = false } = {}) {
50
+ if (skip || isNonInteractive()) return 'skip';
51
+
47
52
  printGuide(guide.title, guide.steps);
48
53
 
49
54
  if (guide.verifyPath) {
@@ -61,8 +66,9 @@ export async function runGuide(rl, guide) {
61
66
  return 'yes';
62
67
  }
63
68
 
64
- export function askQuestion(rl, question, defaultValue) {
69
+ export function askQuestion(rl, question, defaultValue, { skipIfFilled = false } = {}) {
65
70
  if (isNonInteractive()) return Promise.resolve(defaultValue || '');
71
+ if (skipIfFilled && defaultValue) return Promise.resolve(defaultValue);
66
72
 
67
73
  const suffix = defaultValue ? ` [${defaultValue}]` : '';
68
74
  return new Promise((resolve) => {
@@ -72,11 +78,24 @@ export function askQuestion(rl, question, defaultValue) {
72
78
  });
73
79
  }
74
80
 
81
+ export function getDefault(key, cliFlags, currentConfig) {
82
+ if (cliFlags[key] !== undefined) return cliFlags[key];
83
+ const configVal = currentConfig[key];
84
+ if (!isPlaceholder(configVal)) return configVal;
85
+ return '';
86
+ }
87
+
88
+ export function getDefaultWithFallback(key, cliFlags, currentConfig, fallback) {
89
+ const val = getDefault(key, cliFlags, currentConfig);
90
+ return val !== '' ? val : fallback;
91
+ }
92
+
75
93
  export function isPlaceholder(value) {
76
94
  if (value === undefined || value === null || value === '') return true;
77
95
  const s = String(value);
78
96
  if (s.startsWith('REPLACE_WITH_')) return true;
79
97
  if (s.startsWith('yourapp')) return true;
80
98
  if (s.startsWith('com.yourcompany.')) return true;
99
+ if (s === 'your@email.com') return true;
81
100
  return false;
82
101
  }
@@ -11,7 +11,7 @@ export function installGitHubActionsPath(projectDir, packageDir, cliTokens) {
11
11
  });
12
12
 
13
13
  const wrote = writeMatchConfig(projectDir, {
14
- deployKeyPath: cliTokens.matchDeployKey,
14
+ deployKeyPath: cliTokens.matchDeployKeyPath,
15
15
  gitUrl: cliTokens.matchGitUrl,
16
16
  });
17
17
  if (wrote) console.log('Match credentials written to ci.config.yaml');
package/src/install.mjs CHANGED
@@ -11,7 +11,7 @@ import { injectEnvVars, injectStatusLine } from './settings.mjs';
11
11
  import { promptAll } from './prompt.mjs';
12
12
  import { getMcpServers, writeMcpJson } from './mcp-setup.mjs';
13
13
  import { installClaudeMd, installCiTemplates, installFirebaseTemplates } from './templates.mjs';
14
- import { readCiConfig, writeCiFields, writeCiLanguages, isPlaceholder } from './ci-config.mjs';
14
+ import { readCiConfig, writeCiFields, writeCiLanguages, writeMatchConfig, isPlaceholder } from './ci-config.mjs';
15
15
  import { installGitHubActionsPath } from './install-paths.mjs';
16
16
 
17
17
  function checkClaudeCli() {
@@ -152,16 +152,13 @@ function printNextSteps(prompted) {
152
152
  }
153
153
  }
154
154
 
155
- function isNonInteractive() {
156
- return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
157
- }
158
-
159
155
  export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
160
156
  checkClaudeCli();
161
157
 
162
158
  console.log('Installing/updating Daemux Store Automator...');
163
159
 
164
160
  const isGitHubActions = Boolean(cliTokens.githubActions);
161
+ const nonInteractive = Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
165
162
  const projectDir = process.cwd();
166
163
  const oldVersion = readMarketplaceVersion();
167
164
  const packageDir = getPackageDir();
@@ -184,11 +181,21 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
184
181
  // 4. Run interactive prompts (or use CLI flags / skip in non-interactive)
185
182
  let prompted;
186
183
  if (isGitHubActions) {
187
- prompted = { bundleId: cliTokens.bundleId ?? '' };
188
- } else if (isNonInteractive()) {
184
+ prompted = {
185
+ bundleId: cliTokens.bundleId ?? '',
186
+ matchDeployKeyPath: cliTokens.matchDeployKey,
187
+ matchGitUrl: cliTokens.matchGitUrl,
188
+ };
189
+ } else if (nonInteractive) {
189
190
  prompted = { ...cliTokens };
190
191
  } else {
191
- prompted = await promptAll(cliTokens, currentConfig, projectDir);
192
+ const { createInterface } = await import('node:readline');
193
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
194
+ try {
195
+ prompted = await promptAll(rl, cliTokens, currentConfig, projectDir);
196
+ } finally {
197
+ rl.close();
198
+ }
192
199
  }
193
200
 
194
201
  // 5. Write all prompted values to ci.config.yaml
@@ -196,6 +203,14 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
196
203
  const wrote = writeCiFields(projectDir, ciFields);
197
204
  if (wrote) console.log('Configuration written to ci.config.yaml');
198
205
 
206
+ if (prompted.matchDeployKeyPath || prompted.matchGitUrl) {
207
+ const wroteMatch = writeMatchConfig(projectDir, {
208
+ deployKeyPath: prompted.matchDeployKeyPath,
209
+ gitUrl: prompted.matchGitUrl,
210
+ });
211
+ if (wroteMatch) console.log('Match credentials written to ci.config.yaml');
212
+ }
213
+
199
214
  // 6. Handle languages separately
200
215
  if (prompted.languages) {
201
216
  const langStr = Array.isArray(prompted.languages)
@@ -220,7 +235,7 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
220
235
 
221
236
  installClaudeMd(join(baseDir, 'CLAUDE.md'), packageDir, prompted.appName);
222
237
 
223
- installGitHubActionsPath(projectDir, packageDir, cliTokens);
238
+ installGitHubActionsPath(projectDir, packageDir, prompted);
224
239
 
225
240
  const scopeLabel = scope === 'user' ? 'global' : 'project';
226
241
  console.log(`Configuring ${scopeLabel} settings...`);
@@ -229,14 +244,14 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
229
244
  injectStatusLine(settingsPath);
230
245
 
231
246
  // 8. Run post-install guides (interactive only)
232
- if (!isGitHubActions && !isNonInteractive()) {
247
+ if (!isGitHubActions && !nonInteractive) {
233
248
  const { createInterface } = await import('node:readline');
234
- const { runPostInstallGuides } = await import('./prompts/store-settings.mjs');
235
- const rl = createInterface({ input: process.stdin, output: process.stdout });
249
+ const guideRl = createInterface({ input: process.stdin, output: process.stdout });
236
250
  try {
237
- await runPostInstallGuides(rl);
251
+ const { runPostInstallGuides } = await import('./prompts/store-settings.mjs');
252
+ await runPostInstallGuides(guideRl, currentConfig);
238
253
  } finally {
239
- rl.close();
254
+ guideRl.close();
240
255
  }
241
256
  }
242
257
 
package/src/prompt.mjs CHANGED
@@ -1,53 +1,36 @@
1
- import { createInterface } from 'node:readline';
2
1
  import { promptAppIdentity } from './prompts/app-identity.mjs';
3
2
  import { promptCredentials } from './prompts/credentials.mjs';
4
3
  import { promptStoreSettings, promptWebSettings } from './prompts/store-settings.mjs';
5
- import { askQuestion } from './guide.mjs';
6
-
7
- function isInteractive() {
8
- return Boolean(process.stdin.isTTY);
9
- }
10
-
11
- function isNonInteractive() {
12
- return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
13
- }
14
-
15
- function allPromptsProvided(cliFlags) {
16
- return (
17
- cliFlags.bundleId !== undefined &&
18
- cliFlags.stitchApiKey !== undefined &&
19
- cliFlags.cloudflareToken !== undefined &&
20
- cliFlags.cloudflareAccountId !== undefined
21
- );
22
- }
4
+ import { askQuestion, getDefault, printSectionHeader } from './guide.mjs';
23
5
 
24
6
  async function promptMcpTokens(rl, cliFlags, currentConfig) {
7
+ const stitchVal = getDefault('stitchApiKey', cliFlags, currentConfig) || '';
8
+ const cfTokenVal = getDefault('cloudflareToken', cliFlags, currentConfig) || '';
9
+ const cfAcctVal = getDefault('cloudflareAccountId', cliFlags, currentConfig) || '';
10
+
25
11
  const result = {
26
- stitchApiKey: cliFlags.stitchApiKey ?? '',
27
- cloudflareToken: cliFlags.cloudflareToken ?? '',
28
- cloudflareAccountId: cliFlags.cloudflareAccountId ?? '',
12
+ stitchApiKey: stitchVal,
13
+ cloudflareToken: cfTokenVal,
14
+ cloudflareAccountId: cfAcctVal,
29
15
  };
30
16
 
31
- const allProvided = (
32
- cliFlags.stitchApiKey !== undefined &&
33
- cliFlags.cloudflareToken !== undefined &&
34
- cliFlags.cloudflareAccountId !== undefined
35
- );
36
-
37
- if (allProvided) return result;
17
+ if (result.stitchApiKey && result.cloudflareToken && result.cloudflareAccountId) {
18
+ return result;
19
+ }
38
20
 
21
+ printSectionHeader('MCP Tokens (Optional)');
39
22
  console.log('Press Enter to skip any token you do not have yet.');
40
23
  console.log('');
41
24
 
42
- if (cliFlags.stitchApiKey === undefined) {
25
+ if (!result.stitchApiKey) {
43
26
  result.stitchApiKey = await askQuestion(rl, 'Stitch MCP API Key', currentConfig.stitchApiKey || '');
44
27
  }
45
28
 
46
- if (cliFlags.cloudflareToken === undefined) {
29
+ if (!result.cloudflareToken) {
47
30
  result.cloudflareToken = await askQuestion(rl, 'Cloudflare API Token', currentConfig.cloudflareToken || '');
48
31
  }
49
32
 
50
- if (result.cloudflareToken && cliFlags.cloudflareAccountId === undefined) {
33
+ if (result.cloudflareToken && !result.cloudflareAccountId) {
51
34
  result.cloudflareAccountId = await askQuestion(
52
35
  rl, 'Cloudflare Account ID', currentConfig.cloudflareAccountId || ''
53
36
  );
@@ -57,56 +40,11 @@ async function promptMcpTokens(rl, cliFlags, currentConfig) {
57
40
  }
58
41
 
59
42
  export async function promptAll(rl, cliFlags, currentConfig, projectDir) {
60
- console.log('');
61
- console.log('\x1b[1m\x1b[36m=== Store Automator — Interactive Setup ===\x1b[0m');
62
-
63
43
  const identity = await promptAppIdentity(rl, cliFlags, currentConfig);
64
-
65
44
  const credentials = await promptCredentials(rl, cliFlags, currentConfig, projectDir);
66
-
67
45
  const storeSettings = await promptStoreSettings(rl, cliFlags, currentConfig);
68
-
69
46
  const webSettings = await promptWebSettings(rl, cliFlags, currentConfig);
70
-
71
- console.log('');
72
- console.log('\x1b[1m\x1b[36m=== MCP Tokens (Optional) ===\x1b[0m');
73
- console.log('');
74
47
  const mcpTokens = await promptMcpTokens(rl, cliFlags, currentConfig);
75
48
 
76
49
  return { ...identity, ...credentials, ...storeSettings, ...webSettings, ...mcpTokens };
77
50
  }
78
-
79
- export async function promptForTokens(cliTokens = {}) {
80
- const result = {
81
- bundleId: cliTokens.bundleId ?? '',
82
- stitchApiKey: cliTokens.stitchApiKey ?? '',
83
- cloudflareToken: cliTokens.cloudflareToken ?? '',
84
- cloudflareAccountId: cliTokens.cloudflareAccountId ?? '',
85
- };
86
-
87
- if (allPromptsProvided(cliTokens)) {
88
- console.log('All configuration provided via CLI flags, skipping prompts.');
89
- return result;
90
- }
91
-
92
- if (!isInteractive() || isNonInteractive()) {
93
- console.log('Non-interactive terminal detected, skipping prompts.');
94
- console.log('Run "npx store-automator" manually to configure.');
95
- return result;
96
- }
97
-
98
- const rl = createInterface({ input: process.stdin, output: process.stdout });
99
-
100
- try {
101
- const all = await promptAll(rl, cliTokens, {}, process.cwd());
102
- return {
103
- ...all,
104
- bundleId: all.bundleId || result.bundleId,
105
- stitchApiKey: all.stitchApiKey || result.stitchApiKey,
106
- cloudflareToken: all.cloudflareToken || result.cloudflareToken,
107
- cloudflareAccountId: all.cloudflareAccountId || result.cloudflareAccountId,
108
- };
109
- } finally {
110
- rl.close();
111
- }
112
- }
@@ -1,61 +1,30 @@
1
- import { askQuestion, isPlaceholder } from '../guide.mjs';
1
+ import { askQuestion, getDefault, printSectionHeader } from '../guide.mjs';
2
2
 
3
3
  const FIELDS = [
4
- {
5
- key: 'appName',
6
- label: 'App Name (display name)',
7
- configKey: 'app.name',
8
- flag: 'appName',
9
- },
10
- {
11
- key: 'bundleId',
12
- label: 'Bundle ID (e.g. com.company.app)',
13
- configKey: 'bundle_id',
14
- flag: 'bundleId',
15
- },
16
- {
17
- key: 'packageName',
18
- label: 'Android Package Name',
19
- configKey: 'android.package_name',
20
- flag: 'packageName',
21
- },
22
- {
23
- key: 'sku',
24
- label: 'App Store Connect SKU',
25
- configKey: 'ios.sku',
26
- flag: 'sku',
27
- },
28
- {
29
- key: 'appleId',
30
- label: 'Apple Developer Account Email',
31
- configKey: 'ios.apple_id',
32
- flag: 'appleId',
33
- },
4
+ { key: 'appName', label: 'App Name (display name)' },
5
+ { key: 'bundleId', label: 'Bundle ID (e.g. com.company.app)' },
6
+ { key: 'packageName', label: 'Android Package Name' },
7
+ { key: 'sku', label: 'App Store Connect SKU' },
8
+ { key: 'appleId', label: 'Apple Developer Account Email' },
34
9
  ];
35
10
 
36
- function getDefault(key, cliFlags, currentConfig) {
37
- if (cliFlags[key] !== undefined) return cliFlags[key];
38
- const configVal = currentConfig[key];
39
- if (!isPlaceholder(configVal)) return configVal;
40
- return '';
41
- }
42
-
43
11
  export async function promptAppIdentity(rl, cliFlags, currentConfig) {
44
- console.log('');
45
- console.log('\x1b[1m\x1b[36m=== App Identity ===\x1b[0m');
46
- console.log('');
12
+ const allFilled = FIELDS.every((f) => !!getDefault(f.key, cliFlags, currentConfig));
13
+
14
+ if (!allFilled) {
15
+ printSectionHeader('App Identity');
16
+ }
47
17
 
48
18
  const result = {};
49
19
 
50
20
  for (const field of FIELDS) {
51
- const flagVal = cliFlags[field.flag];
52
- if (flagVal !== undefined) {
21
+ const flagVal = getDefault(field.key, cliFlags, currentConfig);
22
+ if (flagVal) {
53
23
  result[field.key] = flagVal;
54
24
  continue;
55
25
  }
56
26
 
57
- const defaultVal = getDefault(field.key, cliFlags, currentConfig);
58
- result[field.key] = await askQuestion(rl, field.label, defaultVal);
27
+ result[field.key] = await askQuestion(rl, field.label, '');
59
28
  }
60
29
 
61
30
  if (!result.packageName && result.bundleId) {
@@ -1,14 +1,12 @@
1
1
  import { join } from 'node:path';
2
- import { runGuide, askQuestion, isPlaceholder } from '../guide.mjs';
3
-
4
- function getDefault(key, cliFlags, currentConfig) {
5
- if (cliFlags[key] !== undefined) return cliFlags[key];
6
- const configVal = currentConfig[key];
7
- if (!isPlaceholder(configVal)) return configVal;
8
- return '';
9
- }
2
+ import { existsSync } from 'node:fs';
3
+ import { runGuide, askQuestion, getDefault, printSectionHeader } from '../guide.mjs';
10
4
 
11
5
  async function promptAppStoreApiKey(rl, cliFlags, currentConfig, projectDir) {
6
+ const keyId = getDefault('keyId', cliFlags, currentConfig);
7
+ const issuerId = getDefault('issuerId', cliFlags, currentConfig);
8
+ const allFilled = !!(keyId && issuerId);
9
+
12
10
  await runGuide(rl, {
13
11
  title: 'Create App Store Connect API Key',
14
12
  steps: [
@@ -21,24 +19,18 @@ async function promptAppStoreApiKey(rl, cliFlags, currentConfig, projectDir) {
21
19
  verifyPath: join(projectDir, 'creds', 'AuthKey.p8'),
22
20
  verifyDescription: 'App Store Connect API Key (.p8)',
23
21
  confirmQuestion: 'Have you created and saved the API key?',
24
- });
22
+ }, { skip: allFilled });
25
23
 
26
- const keyId = await askQuestion(
27
- rl,
28
- 'App Store Connect Key ID',
29
- getDefault('keyId', cliFlags, currentConfig)
30
- );
31
-
32
- const issuerId = await askQuestion(
33
- rl,
34
- 'App Store Connect Issuer ID',
35
- getDefault('issuerId', cliFlags, currentConfig)
36
- );
37
-
38
- return { keyId, issuerId };
24
+ return {
25
+ keyId: await askQuestion(rl, 'App Store Connect Key ID', keyId, { skipIfFilled: !!keyId }),
26
+ issuerId: await askQuestion(rl, 'App Store Connect Issuer ID', issuerId, { skipIfFilled: !!issuerId }),
27
+ };
39
28
  }
40
29
 
41
30
  async function promptPlayServiceAccount(rl, projectDir) {
31
+ const jsonPath = join(projectDir, 'creds', 'play-service-account.json');
32
+ const fileExists = existsSync(jsonPath);
33
+
42
34
  await runGuide(rl, {
43
35
  title: 'Create Google Play Service Account',
44
36
  steps: [
@@ -48,13 +40,15 @@ async function promptPlayServiceAccount(rl, projectDir) {
48
40
  'Download the JSON key file',
49
41
  'Save to creds/play-service-account.json in your project',
50
42
  ],
51
- verifyPath: join(projectDir, 'creds', 'play-service-account.json'),
43
+ verifyPath: jsonPath,
52
44
  verifyDescription: 'Google Play Service Account JSON',
53
45
  confirmQuestion: 'Have you set up the service account?',
54
- });
46
+ }, { skip: fileExists });
55
47
  }
56
48
 
57
49
  async function promptAndroidKeystore(rl, cliFlags, currentConfig) {
50
+ const keystorePassword = getDefault('keystorePassword', cliFlags, currentConfig);
51
+
58
52
  await runGuide(rl, {
59
53
  title: 'Set up Android Keystore',
60
54
  steps: [
@@ -64,18 +58,18 @@ async function promptAndroidKeystore(rl, cliFlags, currentConfig) {
64
58
  'Keep the keystore file safe — you cannot replace it once uploaded to Google Play',
65
59
  ],
66
60
  confirmQuestion: 'Have you created or located your Android keystore?',
67
- });
61
+ }, { skip: !!keystorePassword });
68
62
 
69
- const keystorePassword = await askQuestion(
70
- rl,
71
- 'Keystore password',
72
- getDefault('keystorePassword', cliFlags, currentConfig)
63
+ return await askQuestion(
64
+ rl, 'Keystore password', keystorePassword, { skipIfFilled: !!keystorePassword }
73
65
  );
74
-
75
- return keystorePassword;
76
66
  }
77
67
 
78
68
  async function promptMatchSigning(rl, cliFlags, currentConfig) {
69
+ const matchDeployKeyPath = getDefault('matchDeployKeyPath', cliFlags, currentConfig) || 'creds/match_deploy_key';
70
+ const matchGitUrl = getDefault('matchGitUrl', cliFlags, currentConfig);
71
+ const allFilled = !!(matchDeployKeyPath && matchGitUrl);
72
+
79
73
  await runGuide(rl, {
80
74
  title: 'Set up Match Code Signing',
81
75
  steps: [
@@ -84,39 +78,37 @@ async function promptMatchSigning(rl, cliFlags, currentConfig) {
84
78
  'Add the public key as a deploy key to the certificates repo (with write access)',
85
79
  ],
86
80
  confirmQuestion: 'Have you set up the certificates repository and deploy key?',
87
- });
88
-
89
- const matchDeployKeyPath = await askQuestion(
90
- rl,
91
- 'Path to Match deploy key',
92
- getDefault('matchDeployKeyPath', cliFlags, currentConfig) || 'creds/match_deploy_key'
93
- );
94
-
95
- const matchGitUrl = await askQuestion(
96
- rl,
97
- 'Match certificates Git URL (SSH)',
98
- getDefault('matchGitUrl', cliFlags, currentConfig)
99
- );
100
-
101
- return { matchDeployKeyPath, matchGitUrl };
81
+ }, { skip: allFilled });
82
+
83
+ return {
84
+ matchDeployKeyPath: await askQuestion(
85
+ rl, 'Path to Match deploy key', matchDeployKeyPath, { skipIfFilled: !!matchDeployKeyPath }
86
+ ),
87
+ matchGitUrl: await askQuestion(
88
+ rl, 'Match certificates Git URL (SSH)', matchGitUrl, { skipIfFilled: !!matchGitUrl }
89
+ ),
90
+ };
102
91
  }
103
92
 
104
93
  export async function promptCredentials(rl, cliFlags, currentConfig, projectDir) {
105
- console.log('');
106
- console.log('\x1b[1m\x1b[36m=== Credentials Setup ===\x1b[0m');
107
- console.log('');
94
+ const keyId = getDefault('keyId', cliFlags, currentConfig);
95
+ const issuerId = getDefault('issuerId', cliFlags, currentConfig);
96
+ const keystorePassword = getDefault('keystorePassword', cliFlags, currentConfig);
97
+ const matchDeployKeyPath = getDefault('matchDeployKeyPath', cliFlags, currentConfig) || 'creds/match_deploy_key';
98
+ const matchGitUrl = getDefault('matchGitUrl', cliFlags, currentConfig);
99
+ const jsonPath = join(projectDir, 'creds', 'play-service-account.json');
100
+ const jsonExists = existsSync(jsonPath);
108
101
 
109
- const { keyId, issuerId } = await promptAppStoreApiKey(
110
- rl, cliFlags, currentConfig, projectDir
111
- );
112
-
113
- await promptPlayServiceAccount(rl, projectDir);
102
+ const allFilled = !!(keyId && issuerId && keystorePassword && matchDeployKeyPath && matchGitUrl && jsonExists);
114
103
 
115
- const keystorePassword = await promptAndroidKeystore(rl, cliFlags, currentConfig);
104
+ if (!allFilled) {
105
+ printSectionHeader('Credentials Setup');
106
+ }
116
107
 
117
- const { matchDeployKeyPath, matchGitUrl } = await promptMatchSigning(
118
- rl, cliFlags, currentConfig
119
- );
108
+ const apiKey = await promptAppStoreApiKey(rl, cliFlags, currentConfig, projectDir);
109
+ await promptPlayServiceAccount(rl, projectDir);
110
+ const ksPassword = await promptAndroidKeystore(rl, cliFlags, currentConfig);
111
+ const match = await promptMatchSigning(rl, cliFlags, currentConfig);
120
112
 
121
- return { keyId, issuerId, keystorePassword, matchDeployKeyPath, matchGitUrl };
113
+ return { ...apiKey, keystorePassword: ksPassword, ...match };
122
114
  }
@@ -1,18 +1,20 @@
1
- import { runGuide, askQuestion, isPlaceholder } from '../guide.mjs';
1
+ import {
2
+ runGuide, askQuestion, getDefault, getDefaultWithFallback, printSectionHeader,
3
+ } from '../guide.mjs';
2
4
 
3
- function getDefault(key, cliFlags, currentConfig) {
4
- if (cliFlags[key] !== undefined) return cliFlags[key];
5
- const configVal = currentConfig[key];
6
- if (!isPlaceholder(configVal)) return configVal;
7
- return '';
8
- }
5
+ const IOS_KEYS = ['primaryCategory', 'secondaryCategory', 'priceTier', 'submitForReview', 'automaticRelease'];
6
+ const ANDROID_KEYS = ['track', 'rolloutFraction', 'inAppUpdatePriority'];
9
7
 
10
- function getDefaultWithFallback(key, cliFlags, currentConfig, fallback) {
11
- const val = getDefault(key, cliFlags, currentConfig);
12
- return val !== '' ? val : fallback;
8
+ function allKeysFilled(keys, cliFlags, currentConfig) {
9
+ return keys.every((k) => {
10
+ const val = getDefault(k, cliFlags, currentConfig);
11
+ return val !== '' && val !== undefined;
12
+ });
13
13
  }
14
14
 
15
15
  async function promptIosSettings(rl, cliFlags, currentConfig) {
16
+ const iosFilled = allKeysFilled(IOS_KEYS, cliFlags, currentConfig);
17
+
16
18
  await runGuide(rl, {
17
19
  title: 'Create App in App Store Connect',
18
20
  steps: [
@@ -22,36 +24,31 @@ async function promptIosSettings(rl, cliFlags, currentConfig) {
22
24
  'Save the app',
23
25
  ],
24
26
  confirmQuestion: 'Have you created the app in App Store Connect?',
25
- });
27
+ }, { skip: iosFilled });
26
28
 
29
+ const primaryCat = getDefault('primaryCategory', cliFlags, currentConfig);
27
30
  const primaryCategory = await askQuestion(
28
- rl,
29
- 'iOS Primary Category (e.g. GAMES, UTILITIES)',
30
- getDefault('primaryCategory', cliFlags, currentConfig)
31
+ rl, 'iOS Primary Category (e.g. GAMES, UTILITIES)', primaryCat, { skipIfFilled: !!primaryCat }
31
32
  );
32
33
 
34
+ const secCat = getDefault('secondaryCategory', cliFlags, currentConfig);
33
35
  const secondaryCategory = await askQuestion(
34
- rl,
35
- 'iOS Secondary Category (optional)',
36
- getDefault('secondaryCategory', cliFlags, currentConfig)
36
+ rl, 'iOS Secondary Category (optional)', secCat, { skipIfFilled: !!secCat }
37
37
  );
38
38
 
39
+ const priceVal = getDefaultWithFallback('priceTier', cliFlags, currentConfig, '0');
39
40
  const priceTier = await askQuestion(
40
- rl,
41
- 'iOS Price Tier',
42
- getDefaultWithFallback('priceTier', cliFlags, currentConfig, '0')
41
+ rl, 'iOS Price Tier', priceVal, { skipIfFilled: !!priceVal }
43
42
  );
44
43
 
44
+ const submitVal = getDefaultWithFallback('submitForReview', cliFlags, currentConfig, 'true');
45
45
  const submitForReview = await askQuestion(
46
- rl,
47
- 'Auto-submit for review? (true/false)',
48
- getDefaultWithFallback('submitForReview', cliFlags, currentConfig, 'true')
46
+ rl, 'Auto-submit for review? (true/false)', submitVal, { skipIfFilled: !!submitVal }
49
47
  );
50
48
 
49
+ const autoVal = getDefaultWithFallback('automaticRelease', cliFlags, currentConfig, 'true');
51
50
  const automaticRelease = await askQuestion(
52
- rl,
53
- 'Automatic release after approval? (true/false)',
54
- getDefaultWithFallback('automaticRelease', cliFlags, currentConfig, 'true')
51
+ rl, 'Automatic release after approval? (true/false)', autoVal, { skipIfFilled: !!autoVal }
55
52
  );
56
53
 
57
54
  return {
@@ -64,6 +61,8 @@ async function promptIosSettings(rl, cliFlags, currentConfig) {
64
61
  }
65
62
 
66
63
  async function promptAndroidSettings(rl, cliFlags, currentConfig) {
64
+ const androidFilled = allKeysFilled(ANDROID_KEYS, cliFlags, currentConfig);
65
+
67
66
  await runGuide(rl, {
68
67
  title: 'Create App in Google Play Console',
69
68
  steps: [
@@ -73,24 +72,21 @@ async function promptAndroidSettings(rl, cliFlags, currentConfig) {
73
72
  'Complete the declarations and create',
74
73
  ],
75
74
  confirmQuestion: 'Have you created the app in Google Play Console?',
76
- });
75
+ }, { skip: androidFilled });
77
76
 
77
+ const trackVal = getDefaultWithFallback('track', cliFlags, currentConfig, 'internal');
78
78
  const track = await askQuestion(
79
- rl,
80
- 'Android release track (internal/alpha/beta/production)',
81
- getDefaultWithFallback('track', cliFlags, currentConfig, 'internal')
79
+ rl, 'Android release track (internal/alpha/beta/production)', trackVal, { skipIfFilled: !!trackVal }
82
80
  );
83
81
 
82
+ const rolloutVal = getDefaultWithFallback('rolloutFraction', cliFlags, currentConfig, '1.0');
84
83
  const rolloutFraction = await askQuestion(
85
- rl,
86
- 'Rollout fraction (0.0 - 1.0)',
87
- getDefaultWithFallback('rolloutFraction', cliFlags, currentConfig, '1.0')
84
+ rl, 'Rollout fraction (0.0 - 1.0)', rolloutVal, { skipIfFilled: !!rolloutVal }
88
85
  );
89
86
 
87
+ const priorityVal = getDefaultWithFallback('inAppUpdatePriority', cliFlags, currentConfig, '3');
90
88
  const inAppUpdatePriority = await askQuestion(
91
- rl,
92
- 'In-app update priority (0-5)',
93
- getDefaultWithFallback('inAppUpdatePriority', cliFlags, currentConfig, '3')
89
+ rl, 'In-app update priority (0-5)', priorityVal, { skipIfFilled: !!priorityVal }
94
90
  );
95
91
 
96
92
  return {
@@ -101,19 +97,23 @@ async function promptAndroidSettings(rl, cliFlags, currentConfig) {
101
97
  }
102
98
 
103
99
  async function promptMetadataSettings(rl, cliFlags, currentConfig) {
100
+ const langVal = getDefaultWithFallback('languages', cliFlags, currentConfig, 'en-US');
104
101
  const languages = await askQuestion(
105
- rl,
106
- 'Metadata languages (comma-separated, e.g. en-US,de-DE)',
107
- getDefaultWithFallback('languages', cliFlags, currentConfig, 'en-US')
102
+ rl, 'Metadata languages (comma-separated, e.g. en-US,de-DE)', langVal, { skipIfFilled: !!langVal }
108
103
  );
109
104
 
110
105
  return { languages: languages.split(',').map((l) => l.trim()).filter(Boolean) };
111
106
  }
112
107
 
113
108
  export async function promptStoreSettings(rl, cliFlags, currentConfig) {
114
- console.log('');
115
- console.log('\x1b[1m\x1b[36m=== Store Settings ===\x1b[0m');
116
- console.log('');
109
+ const iosFilled = allKeysFilled(IOS_KEYS, cliFlags, currentConfig);
110
+ const androidFilled = allKeysFilled(ANDROID_KEYS, cliFlags, currentConfig);
111
+ const langVal = getDefaultWithFallback('languages', cliFlags, currentConfig, 'en-US');
112
+ const allFilled = iosFilled && androidFilled && !!langVal;
113
+
114
+ if (!allFilled) {
115
+ printSectionHeader('Store Settings');
116
+ }
117
117
 
118
118
  const ios = await promptIosSettings(rl, cliFlags, currentConfig);
119
119
  const android = await promptAndroidSettings(rl, cliFlags, currentConfig);
@@ -122,10 +122,17 @@ export async function promptStoreSettings(rl, cliFlags, currentConfig) {
122
122
  return { ...ios, ...android, ...metadata };
123
123
  }
124
124
 
125
+ const WEB_KEYS = [
126
+ 'domain', 'cfProjectName', 'tagline', 'primaryColor', 'secondaryColor',
127
+ 'companyName', 'contactEmail', 'supportEmail', 'jurisdiction',
128
+ ];
129
+
125
130
  export async function promptWebSettings(rl, cliFlags, currentConfig) {
126
- console.log('');
127
- console.log('\x1b[1m\x1b[36m=== Web Settings ===\x1b[0m');
128
- console.log('');
131
+ const webFilled = WEB_KEYS.every((k) => !!getDefault(k, cliFlags, currentConfig));
132
+
133
+ if (!webFilled) {
134
+ printSectionHeader('Web Settings');
135
+ }
129
136
 
130
137
  const fields = [
131
138
  { key: 'domain', label: 'Domain (e.g. example.com)' },
@@ -141,16 +148,17 @@ export async function promptWebSettings(rl, cliFlags, currentConfig) {
141
148
 
142
149
  const result = {};
143
150
  for (const field of fields) {
144
- result[field.key] = await askQuestion(
145
- rl,
146
- field.label,
147
- getDefault(field.key, cliFlags, currentConfig)
148
- );
151
+ const val = getDefault(field.key, cliFlags, currentConfig);
152
+ result[field.key] = await askQuestion(rl, field.label, val, { skipIfFilled: !!val });
149
153
  }
150
154
  return result;
151
155
  }
152
156
 
153
- export async function runPostInstallGuides(rl) {
157
+ export async function runPostInstallGuides(rl, currentConfig = {}) {
158
+ const hasRepo = !!currentConfig._githubRepoConfigured;
159
+ const hasSecrets = !!currentConfig._githubSecretsConfigured;
160
+ const hasFirebase = !!currentConfig._firebaseConfigured;
161
+
154
162
  await runGuide(rl, {
155
163
  title: 'Create Private GitHub Repository',
156
164
  steps: [
@@ -159,7 +167,7 @@ export async function runPostInstallGuides(rl) {
159
167
  'Push your project to the repository',
160
168
  ],
161
169
  confirmQuestion: 'Have you created the GitHub repository?',
162
- });
170
+ }, { skip: hasRepo });
163
171
 
164
172
  await runGuide(rl, {
165
173
  title: 'Set GitHub Actions Secrets',
@@ -170,7 +178,7 @@ export async function runPostInstallGuides(rl) {
170
178
  'Upload creds/ files as secrets if using CI',
171
179
  ],
172
180
  confirmQuestion: 'Have you configured GitHub Actions secrets?',
173
- });
181
+ }, { skip: hasSecrets });
174
182
 
175
183
  await runGuide(rl, {
176
184
  title: 'Create Firebase Project (optional)',
@@ -181,5 +189,5 @@ export async function runPostInstallGuides(rl) {
181
189
  'Download google-services.json and GoogleService-Info.plist',
182
190
  ],
183
191
  confirmQuestion: 'Have you set up Firebase? (skip if not needed)',
184
- });
192
+ }, { skip: hasFirebase });
185
193
  }
@@ -44,6 +44,8 @@ fi
44
44
  # --- Upload metadata via Fastlane ---
45
45
  echo "Uploading Android metadata..."
46
46
 
47
+ cd "$APP_ROOT/android"
48
+
47
49
  set +e
48
50
  FASTLANE_OUTPUT=$(PACKAGE_NAME="$PACKAGE_NAME" \
49
51
  GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH="$SA_FULL_PATH" \