@daemux/store-automator 0.10.8 → 0.10.9

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.8"
8
+ "version": "0.10.9"
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.8",
15
+ "version": "0.10.9",
16
16
  "keywords": [
17
17
  "flutter",
18
18
  "app-store",
package/bin/cli.mjs CHANGED
@@ -23,7 +23,32 @@ const valueFlags = {
23
23
  '--cloudflare-token=': 'cloudflareToken',
24
24
  '--cloudflare-account-id=': 'cloudflareAccountId',
25
25
  '--bundle-id=': 'bundleId',
26
+ '--app-name=': 'appName',
27
+ '--key-id=': 'keyId',
28
+ '--issuer-id=': 'issuerId',
29
+ '--keystore-password=': 'keystorePassword',
30
+ '--sku=': 'sku',
31
+ '--apple-id=': 'appleId',
32
+ '--primary-category=': 'primaryCategory',
33
+ '--secondary-category=': 'secondaryCategory',
34
+ '--price-tier=': 'priceTier',
35
+ '--submit-for-review=': 'submitForReview',
36
+ '--automatic-release=': 'automaticRelease',
37
+ '--track=': 'track',
38
+ '--rollout-fraction=': 'rolloutFraction',
39
+ '--in-app-update-priority=': 'inAppUpdatePriority',
40
+ '--domain=': 'domain',
41
+ '--cf-project-name=': 'cfProjectName',
42
+ '--tagline=': 'tagline',
43
+ '--primary-color=': 'primaryColor',
44
+ '--secondary-color=': 'secondaryColor',
45
+ '--company-name=': 'companyName',
46
+ '--contact-email=': 'contactEmail',
47
+ '--support-email=': 'supportEmail',
48
+ '--jurisdiction=': 'jurisdiction',
49
+ '--languages=': 'languages',
26
50
  '--match-deploy-key=': 'matchDeployKey',
51
+ '--match-deploy-key-path=': 'matchDeployKeyPath',
27
52
  '--match-git-url=': 'matchGitUrl',
28
53
  };
29
54
 
@@ -60,8 +85,44 @@ Options:
60
85
  -v, --version Show version number
61
86
  -h, --help Show help
62
87
 
63
- App Configuration:
88
+ App Identity:
89
+ --app-name=NAME App display name
64
90
  --bundle-id=ID Bundle ID / Package Name (e.g., com.company.app)
91
+ --sku=SKU App Store Connect SKU
92
+ --apple-id=EMAIL Apple Developer Account Email
93
+
94
+ Credentials:
95
+ --key-id=ID App Store Connect Key ID
96
+ --issuer-id=ID App Store Connect Issuer ID
97
+ --keystore-password=PASS Android keystore password
98
+ --match-deploy-key-path=PATH Path to Match deploy key
99
+ --match-git-url=URL Match certificates Git URL (SSH)
100
+
101
+ iOS Store Settings:
102
+ --primary-category=CAT iOS primary category (e.g., UTILITIES)
103
+ --secondary-category=CAT iOS secondary category
104
+ --price-tier=N iOS price tier (0 = free)
105
+ --submit-for-review=BOOL Auto-submit for review (true/false)
106
+ --automatic-release=BOOL Auto-release after approval (true/false)
107
+
108
+ Android Store Settings:
109
+ --track=TRACK Android release track (internal/alpha/beta/production)
110
+ --rollout-fraction=N Rollout fraction (0.0-1.0)
111
+ --in-app-update-priority=N In-app update priority (0-5)
112
+
113
+ Web Settings:
114
+ --domain=DOMAIN Web domain (e.g., myapp-pages.pages.dev)
115
+ --cf-project-name=NAME Cloudflare Pages project name
116
+ --tagline=TEXT App tagline
117
+ --primary-color=HEX Primary color (e.g., #2563EB)
118
+ --secondary-color=HEX Secondary color
119
+ --company-name=NAME Company name
120
+ --contact-email=EMAIL Contact email
121
+ --support-email=EMAIL Support email
122
+ --jurisdiction=TEXT Legal jurisdiction
123
+
124
+ Languages:
125
+ --languages=LANGS Comma-separated language codes (e.g., en-US,de-DE)
65
126
 
66
127
  MCP Token Flags (skip interactive prompts):
67
128
  --stitch-key=KEY Stitch MCP API key
@@ -74,17 +135,9 @@ GitHub Actions CI mode:
74
135
  --match-git-url=URL Git URL for Match certificates repo
75
136
 
76
137
  Examples:
77
- npx @daemux/store-automator Install for project
78
- npx @daemux/store-automator -g Install globally
79
- npx @daemux/store-automator -u Uninstall from project
80
- npx @daemux/store-automator -g -u Uninstall globally
81
-
82
- GitHub Actions install:
83
- npx @daemux/store-automator --github-actions --bundle-id=ID --match-deploy-key=PATH --match-git-url=URL
84
-
85
- Non-interactive install:
86
- npx @daemux/store-automator --bundle-id=ID --stitch-key=KEY
87
- npx @daemux/store-automator --cloudflare-token=TOKEN --cloudflare-account-id=ID`);
138
+ npx @daemux/store-automator
139
+ npx @daemux/store-automator --app-name="My App" --bundle-id=com.company.app
140
+ npx @daemux/store-automator --github-actions --bundle-id=ID --match-deploy-key=PATH --match-git-url=URL`);
88
141
  process.exit(0);
89
142
  case '-v':
90
143
  case '--version':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemux/store-automator",
3
- "version": "0.10.8",
3
+ "version": "0.10.9",
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.8",
3
+ "version": "0.10.9",
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
@@ -2,51 +2,196 @@ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
 
4
4
  const CI_CONFIG_FILE = 'ci.config.yaml';
5
+
5
6
  const FIELD_PATTERNS = {
6
- bundle_id: /^(\s*bundle_id:\s*)"[^"]*"/m,
7
- package_name: /^(\s*package_name:\s*)"[^"]*"/m,
8
- deploy_key_path: /^(\s*deploy_key_path:\s*)"[^"]*"/m,
9
- git_url: /^(\s*git_url:\s*)"[^"]*"/m,
7
+ 'app.name': { regex: /^( name: ).*$/m, replacement: (v) => ` name: "${v}"` },
8
+ 'app.bundle_id': { regex: /^( bundle_id: ).*$/m, replacement: (v) => ` bundle_id: "${v}"` },
9
+ 'app.package_name': { regex: /^( package_name: ).*$/m, replacement: (v) => ` package_name: "${v}"` },
10
+ 'app.sku': { regex: /^( sku: ).*$/m, replacement: (v) => ` sku: "${v}"` },
11
+ 'app.apple_id': { regex: /^( apple_id: ).*$/m, replacement: (v) => ` apple_id: "${v}"` },
12
+ 'credentials.apple.key_id': {
13
+ regex: /^( key_id: ).*$/m, replacement: (v) => ` key_id: "${v}"`,
14
+ },
15
+ 'credentials.apple.issuer_id': {
16
+ regex: /^( issuer_id: ).*$/m, replacement: (v) => ` issuer_id: "${v}"`,
17
+ },
18
+ 'credentials.android.keystore_password': {
19
+ regex: /^( keystore_password: ).*$/m, replacement: (v) => ` keystore_password: "${v}"`,
20
+ },
21
+ 'ios.primary_category': {
22
+ regex: /^( primary_category: ).*$/m, replacement: (v) => ` primary_category: "${v}"`,
23
+ },
24
+ 'ios.secondary_category': {
25
+ regex: /^( secondary_category: ).*$/m, replacement: (v) => ` secondary_category: "${v}"`,
26
+ },
27
+ 'ios.price_tier': { regex: /^( price_tier: ).*$/m, replacement: (v) => ` price_tier: ${v}` },
28
+ 'ios.submit_for_review': {
29
+ regex: /^( submit_for_review: ).*$/m, replacement: (v) => ` submit_for_review: ${v}`,
30
+ },
31
+ 'ios.automatic_release': {
32
+ regex: /^( automatic_release: ).*$/m, replacement: (v) => ` automatic_release: ${v}`,
33
+ },
34
+ 'android.track': { regex: /^( track: ).*$/m, replacement: (v) => ` track: "${v}"` },
35
+ 'android.rollout_fraction': {
36
+ regex: /^( rollout_fraction: ).*$/m, replacement: (v) => ` rollout_fraction: "${v}"`,
37
+ },
38
+ 'android.in_app_update_priority': {
39
+ regex: /^( in_app_update_priority: ).*$/m, replacement: (v) => ` in_app_update_priority: ${v}`,
40
+ },
41
+ 'web.domain': { regex: /^( domain: ).*$/m, replacement: (v) => ` domain: "${v}"` },
42
+ 'web.cloudflare_project_name': {
43
+ regex: /^( cloudflare_project_name: ).*$/m, replacement: (v) => ` cloudflare_project_name: "${v}"`,
44
+ },
45
+ 'web.tagline': { regex: /^( tagline: ).*$/m, replacement: (v) => ` tagline: "${v}"` },
46
+ 'web.primary_color': {
47
+ regex: /^( primary_color: ).*$/m, replacement: (v) => ` primary_color: "${v}"`,
48
+ },
49
+ 'web.secondary_color': {
50
+ regex: /^( secondary_color: ).*$/m, replacement: (v) => ` secondary_color: "${v}"`,
51
+ },
52
+ 'web.company_name': {
53
+ regex: /^( company_name: ).*$/m, replacement: (v) => ` company_name: "${v}"`,
54
+ },
55
+ 'web.contact_email': {
56
+ regex: /^( contact_email: ).*$/m, replacement: (v) => ` contact_email: "${v}"`,
57
+ },
58
+ 'web.support_email': {
59
+ regex: /^( support_email: ).*$/m, replacement: (v) => ` support_email: "${v}"`,
60
+ },
61
+ 'web.jurisdiction': {
62
+ regex: /^( jurisdiction: ).*$/m, replacement: (v) => ` jurisdiction: "${v}"`,
63
+ },
64
+ 'web.app_store_url': {
65
+ regex: /^( app_store_url: ).*$/m, replacement: (v) => ` app_store_url: "${v}"`,
66
+ },
67
+ 'web.google_play_url': {
68
+ regex: /^( google_play_url: ).*$/m, replacement: (v) => ` google_play_url: "${v}"`,
69
+ },
10
70
  };
11
71
 
12
- function writeCiField(projectDir, field, value) {
13
- const configPath = join(projectDir, CI_CONFIG_FILE);
14
- if (!existsSync(configPath)) return false;
72
+ function extractFieldValue(content, regex) {
73
+ const match = content.match(regex);
74
+ if (!match) return undefined;
75
+ const line = match[0];
76
+ const colonIdx = line.indexOf(':');
77
+ if (colonIdx < 0) return undefined;
78
+ const raw = line.slice(colonIdx + 1).trim();
79
+ if (raw.length >= 2 && raw[0] === '"' && raw[raw.length - 1] === '"') return raw.slice(1, -1);
80
+ return raw;
81
+ }
15
82
 
16
- try {
17
- const pattern = FIELD_PATTERNS[field];
18
- const content = readFileSync(configPath, 'utf8');
19
- if (!pattern.test(content)) return false;
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
+ }
20
92
 
21
- const safeValue = value.replace(/\$/g, '$$$$');
22
- const updated = content.replace(pattern, `$1"${safeValue}"`);
23
- if (updated === content) return false;
93
+ export function readCiConfig(projectDir) {
94
+ const configPath = join(projectDir, CI_CONFIG_FILE);
95
+ if (!existsSync(configPath)) return {};
96
+ const content = readFileSync(configPath, 'utf-8');
97
+ const config = {};
98
+ for (const [key, { regex }] of Object.entries(FIELD_PATTERNS)) {
99
+ const val = extractFieldValue(content, regex);
100
+ if (val !== undefined) config[key] = val;
101
+ }
102
+ config['metadata.languages'] = extractLanguages(content);
103
+ return config;
104
+ }
24
105
 
25
- writeFileSync(configPath, updated, 'utf8');
26
- return true;
27
- } catch {
28
- return false;
106
+ function extractLanguages(content) {
107
+ const lines = content.split('\n');
108
+ const langIdx = lines.findIndex((l) => /^\s*languages:\s*$/.test(l));
109
+ if (langIdx < 0) return [];
110
+ const langs = [];
111
+ for (let i = langIdx + 1; i < lines.length; i++) {
112
+ const match = lines[i].match(/^\s+-\s+(.+)$/);
113
+ if (!match) break;
114
+ langs.push(match[1].trim());
115
+ }
116
+ return langs;
117
+ }
118
+
119
+ export function writeCiFields(projectDir, fields) {
120
+ const configPath = join(projectDir, CI_CONFIG_FILE);
121
+ if (!existsSync(configPath)) return false;
122
+ let content = readFileSync(configPath, 'utf-8');
123
+ let changed = false;
124
+ for (const [key, value] of Object.entries(fields)) {
125
+ if (value === undefined || value === null) continue;
126
+ const pattern = FIELD_PATTERNS[key];
127
+ if (!pattern) continue;
128
+ const safeValue = String(value).replace(/\$/g, '$$$$');
129
+ const updated = content.replace(pattern.regex, pattern.replacement(safeValue));
130
+ if (updated !== content) {
131
+ content = updated;
132
+ changed = true;
133
+ }
29
134
  }
135
+ if (changed) writeFileSync(configPath, content, 'utf-8');
136
+ return changed;
137
+ }
138
+
139
+ export function writeCiLanguages(projectDir, languagesStr) {
140
+ const configPath = join(projectDir, CI_CONFIG_FILE);
141
+ if (!existsSync(configPath)) return false;
142
+ const langs = languagesStr
143
+ .split(',')
144
+ .map((s) => s.trim())
145
+ .filter(Boolean);
146
+ if (langs.length === 0) return false;
147
+
148
+ const yamlLines = langs.map((l) => ` - ${l}`).join('\n');
149
+ const content = readFileSync(configPath, 'utf-8');
150
+ const updated = content.replace(
151
+ /^( languages:\s*)\n(?:\s+-\s+.+\n?)*/m,
152
+ `$1\n${yamlLines}\n`
153
+ );
154
+ if (updated === content) return false;
155
+ writeFileSync(configPath, updated, 'utf-8');
156
+ return true;
30
157
  }
31
158
 
32
159
  export function writeCiBundleId(projectDir, bundleId) {
33
- return writeCiField(projectDir, 'bundle_id', bundleId);
160
+ return writeCiFields(projectDir, {
161
+ 'app.bundle_id': bundleId,
162
+ });
34
163
  }
35
164
 
36
165
  export function writeCiPackageName(projectDir, packageName) {
37
- return writeCiField(projectDir, 'package_name', packageName);
166
+ return writeCiFields(projectDir, {
167
+ 'app.package_name': packageName,
168
+ });
38
169
  }
39
170
 
40
171
  export function writeMatchConfig(projectDir, { deployKeyPath, gitUrl }) {
41
- const wrote1 = writeCiField(projectDir, 'deploy_key_path', deployKeyPath);
42
- const wrote2 = writeCiField(projectDir, 'git_url', gitUrl);
43
- return wrote1 || wrote2;
172
+ const configPath = join(projectDir, CI_CONFIG_FILE);
173
+ if (!existsSync(configPath)) return false;
174
+ let content = readFileSync(configPath, 'utf-8');
175
+ let changed = false;
176
+
177
+ const dpRegex = /^(\s*deploy_key_path:\s*)"[^"]*"/m;
178
+ const guRegex = /^(\s*git_url:\s*)"[^"]*"/m;
179
+
180
+ if (deployKeyPath && dpRegex.test(content)) {
181
+ content = content.replace(dpRegex, `$1"${deployKeyPath}"`);
182
+ changed = true;
183
+ }
184
+ if (gitUrl && guRegex.test(content)) {
185
+ content = content.replace(guRegex, `$1"${gitUrl}"`);
186
+ changed = true;
187
+ }
188
+ if (changed) writeFileSync(configPath, content, 'utf-8');
189
+ return changed;
44
190
  }
45
191
 
46
192
  export function readFlutterRoot(projectDir) {
47
193
  const configPath = join(projectDir, CI_CONFIG_FILE);
48
194
  if (!existsSync(configPath)) return '.';
49
-
50
195
  try {
51
196
  const content = readFileSync(configPath, 'utf8');
52
197
  const match = content.match(/^flutter_root:\s*"([^"]*)"/m);
package/src/guide.mjs ADDED
@@ -0,0 +1,82 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { existsSync } from 'node:fs';
3
+
4
+ const BOLD = '\x1b[1m';
5
+ const CYAN = '\x1b[36m';
6
+ const YELLOW = '\x1b[33m';
7
+ const GREEN = '\x1b[32m';
8
+ const RESET = '\x1b[0m';
9
+
10
+ function isNonInteractive() {
11
+ return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
12
+ }
13
+
14
+ export function printGuide(title, steps) {
15
+ console.log('');
16
+ console.log(`${BOLD}${CYAN}=== ${title} ===${RESET}`);
17
+ console.log('');
18
+ for (let i = 0; i < steps.length; i++) {
19
+ console.log(` ${i + 1}. ${steps[i]}`);
20
+ }
21
+ console.log('');
22
+ }
23
+
24
+ export function askConfirmation(rl, question) {
25
+ if (isNonInteractive()) return Promise.resolve('skip');
26
+
27
+ return new Promise((resolve) => {
28
+ rl.question(`${question} (y/n/skip): `, (answer) => {
29
+ const val = answer.trim().toLowerCase();
30
+ if (val === 'y' || val === 'yes') return resolve('yes');
31
+ if (val === 's' || val === 'skip') return resolve('skip');
32
+ resolve('no');
33
+ });
34
+ });
35
+ }
36
+
37
+ export function verifyFileExists(filePath, description) {
38
+ if (existsSync(filePath)) {
39
+ console.log(`${GREEN} Found: ${description} (${filePath})${RESET}`);
40
+ return true;
41
+ }
42
+ console.log(`${YELLOW} Warning: ${description} not found at ${filePath}${RESET}`);
43
+ return false;
44
+ }
45
+
46
+ export async function runGuide(rl, guide) {
47
+ printGuide(guide.title, guide.steps);
48
+
49
+ if (guide.verifyPath) {
50
+ verifyFileExists(guide.verifyPath, guide.verifyDescription || guide.verifyPath);
51
+ }
52
+
53
+ if (guide.confirmQuestion) {
54
+ const answer = await askConfirmation(rl, guide.confirmQuestion);
55
+ if (answer === 'no') {
56
+ console.log(`${YELLOW} Skipped. You can complete this step later.${RESET}`);
57
+ }
58
+ return answer;
59
+ }
60
+
61
+ return 'yes';
62
+ }
63
+
64
+ export function askQuestion(rl, question, defaultValue) {
65
+ if (isNonInteractive()) return Promise.resolve(defaultValue || '');
66
+
67
+ const suffix = defaultValue ? ` [${defaultValue}]` : '';
68
+ return new Promise((resolve) => {
69
+ rl.question(`${question}${suffix}: `, (answer) => {
70
+ resolve(answer.trim() || defaultValue || '');
71
+ });
72
+ });
73
+ }
74
+
75
+ export function isPlaceholder(value) {
76
+ if (value === undefined || value === null || value === '') return true;
77
+ const s = String(value);
78
+ if (s.startsWith('REPLACE_WITH_')) return true;
79
+ if (s.startsWith('yourapp')) return true;
80
+ if (s.startsWith('com.yourcompany.')) return true;
81
+ return false;
82
+ }
package/src/install.mjs CHANGED
@@ -8,10 +8,10 @@ import {
8
8
  getPackageDir, exec, ensureDir, ensureFile, readJson, writeJson,
9
9
  } from './utils.mjs';
10
10
  import { injectEnvVars, injectStatusLine } from './settings.mjs';
11
- import { promptForTokens } from './prompt.mjs';
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 { writeCiBundleId, writeCiPackageName } from './ci-config.mjs';
14
+ import { readCiConfig, writeCiFields, writeCiLanguages, isPlaceholder } from './ci-config.mjs';
15
15
  import { installGitHubActionsPath } from './install-paths.mjs';
16
16
 
17
17
  function checkClaudeCli() {
@@ -94,13 +94,66 @@ function printSummary(scope, oldVersion, newVersion) {
94
94
  }
95
95
  }
96
96
 
97
- function printNextSteps() {
97
+ function mapPromptsToCiFields(prompted) {
98
+ return {
99
+ 'app.name': prompted.appName,
100
+ 'app.bundle_id': prompted.bundleId,
101
+ 'app.package_name': prompted.packageName,
102
+ 'app.sku': prompted.sku,
103
+ 'app.apple_id': prompted.appleId,
104
+ 'credentials.apple.key_id': prompted.keyId,
105
+ 'credentials.apple.issuer_id': prompted.issuerId,
106
+ 'credentials.android.keystore_password': prompted.keystorePassword,
107
+ 'ios.primary_category': prompted.primaryCategory,
108
+ 'ios.secondary_category': prompted.secondaryCategory,
109
+ 'ios.price_tier': prompted.priceTier,
110
+ 'ios.submit_for_review': prompted.submitForReview,
111
+ 'ios.automatic_release': prompted.automaticRelease,
112
+ 'android.track': prompted.track,
113
+ 'android.rollout_fraction': prompted.rolloutFraction,
114
+ 'android.in_app_update_priority': prompted.inAppUpdatePriority,
115
+ 'web.domain': prompted.domain,
116
+ 'web.cloudflare_project_name': prompted.cfProjectName,
117
+ 'web.tagline': prompted.tagline,
118
+ 'web.primary_color': prompted.primaryColor,
119
+ 'web.secondary_color': prompted.secondaryColor,
120
+ 'web.company_name': prompted.companyName,
121
+ 'web.contact_email': prompted.contactEmail,
122
+ 'web.support_email': prompted.supportEmail,
123
+ 'web.jurisdiction': prompted.jurisdiction,
124
+ };
125
+ }
126
+
127
+ function printNextSteps(prompted) {
128
+ const missing = [];
129
+
130
+ if (isPlaceholder(prompted.bundleId)) {
131
+ missing.push('Set bundle ID in ci.config.yaml');
132
+ }
133
+ if (isPlaceholder(prompted.keyId)) {
134
+ missing.push('Add App Store Connect credentials (key_id, issuer_id, AuthKey.p8)');
135
+ }
136
+ if (!existsSync(join(process.cwd(), 'creds', 'play-service-account.json'))) {
137
+ missing.push('Add creds/play-service-account.json for Google Play');
138
+ }
139
+ if (isPlaceholder(prompted.matchGitUrl)) {
140
+ missing.push('Configure Match code signing (match_git_url, deploy key)');
141
+ }
142
+
98
143
  console.log('');
99
- console.log('Next steps:');
100
- console.log(' 1. Fill ci.config.yaml with credentials');
101
- console.log(' 2. Add creds/AuthKey.p8 and creds/play-service-account.json');
102
- console.log(' 3. Set MATCH_PASSWORD secret in GitHub repository settings');
103
- console.log(' 4. Start Claude Code');
144
+ if (missing.length === 0) {
145
+ console.log('All configuration complete! Start Claude Code.');
146
+ } else {
147
+ console.log('Next steps:');
148
+ for (let i = 0; i < missing.length; i++) {
149
+ console.log(` ${i + 1}. ${missing[i]}`);
150
+ }
151
+ console.log(` ${missing.length + 1}. Start Claude Code`);
152
+ }
153
+ }
154
+
155
+ function isNonInteractive() {
156
+ return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
104
157
  }
105
158
 
106
159
  export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
@@ -109,21 +162,11 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
109
162
  console.log('Installing/updating Daemux Store Automator...');
110
163
 
111
164
  const isGitHubActions = Boolean(cliTokens.githubActions);
112
-
113
- const tokens = isGitHubActions
114
- ? { bundleId: cliTokens.bundleId ?? '' }
115
- : await promptForTokens(cliTokens);
116
-
117
165
  const projectDir = process.cwd();
118
-
119
- if (!isGitHubActions) {
120
- const servers = getMcpServers(tokens);
121
- writeMcpJson(projectDir, servers);
122
- }
123
-
124
166
  const oldVersion = readMarketplaceVersion();
125
167
  const packageDir = getPackageDir();
126
168
 
169
+ // 1. Copy plugin files + register marketplace
127
170
  copyPluginFiles(packageDir);
128
171
  clearCache();
129
172
  registerMarketplace();
@@ -131,21 +174,51 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
131
174
 
132
175
  const newVersion = readMarketplaceVersion('unknown');
133
176
 
177
+ // 2. Install CI templates (creates ci.config.yaml if missing)
178
+ installCiTemplates(projectDir, packageDir);
179
+ installFirebaseTemplates(projectDir, packageDir);
180
+
181
+ // 3. Read current ci.config.yaml values
182
+ const currentConfig = readCiConfig(projectDir);
183
+
184
+ // 4. Run interactive prompts (or use CLI flags / skip in non-interactive)
185
+ let prompted;
186
+ if (isGitHubActions) {
187
+ prompted = { bundleId: cliTokens.bundleId ?? '' };
188
+ } else if (isNonInteractive()) {
189
+ prompted = { ...cliTokens };
190
+ } else {
191
+ prompted = await promptAll(cliTokens, currentConfig, projectDir);
192
+ }
193
+
194
+ // 5. Write all prompted values to ci.config.yaml
195
+ const ciFields = mapPromptsToCiFields(prompted);
196
+ const wrote = writeCiFields(projectDir, ciFields);
197
+ if (wrote) console.log('Configuration written to ci.config.yaml');
198
+
199
+ // 6. Handle languages separately
200
+ if (prompted.languages) {
201
+ const langStr = Array.isArray(prompted.languages)
202
+ ? prompted.languages.join(',')
203
+ : prompted.languages;
204
+ if (writeCiLanguages(projectDir, langStr)) {
205
+ console.log('Languages updated in ci.config.yaml');
206
+ }
207
+ }
208
+
209
+ // 7. Configure MCP, CLAUDE.md, settings
210
+ if (!isGitHubActions) {
211
+ const servers = getMcpServers(prompted);
212
+ writeMcpJson(projectDir, servers);
213
+ }
214
+
134
215
  const baseDir = scope === 'user'
135
216
  ? join(homedir(), '.claude')
136
217
  : join(process.cwd(), '.claude');
137
218
 
138
219
  ensureDir(baseDir);
139
220
 
140
- installClaudeMd(join(baseDir, 'CLAUDE.md'), packageDir);
141
- installCiTemplates(projectDir, packageDir);
142
- installFirebaseTemplates(projectDir, packageDir);
143
-
144
- if (tokens.bundleId) {
145
- const written = writeCiBundleId(projectDir, tokens.bundleId);
146
- if (written) console.log(`Bundle ID set in ci.config.yaml: ${tokens.bundleId}`);
147
- writeCiPackageName(projectDir, tokens.bundleId);
148
- }
221
+ installClaudeMd(join(baseDir, 'CLAUDE.md'), packageDir, prompted.appName);
149
222
 
150
223
  installGitHubActionsPath(projectDir, packageDir, cliTokens);
151
224
 
@@ -155,6 +228,19 @@ export async function runInstall(scope, isPostinstall = false, cliTokens = {}) {
155
228
  injectEnvVars(settingsPath);
156
229
  injectStatusLine(settingsPath);
157
230
 
231
+ // 8. Run post-install guides (interactive only)
232
+ if (!isGitHubActions && !isNonInteractive()) {
233
+ 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 });
236
+ try {
237
+ await runPostInstallGuides(rl);
238
+ } finally {
239
+ rl.close();
240
+ }
241
+ }
242
+
243
+ // 9. Summary + dynamic next steps
158
244
  printSummary(scope, oldVersion, newVersion);
159
- printNextSteps();
245
+ printNextSteps(prompted);
160
246
  }
package/src/prompt.mjs CHANGED
@@ -1,30 +1,79 @@
1
1
  import { createInterface } from 'node:readline';
2
+ import { promptAppIdentity } from './prompts/app-identity.mjs';
3
+ import { promptCredentials } from './prompts/credentials.mjs';
4
+ import { promptStoreSettings, promptWebSettings } from './prompts/store-settings.mjs';
5
+ import { askQuestion } from './guide.mjs';
2
6
 
3
7
  function isInteractive() {
4
8
  return Boolean(process.stdin.isTTY);
5
9
  }
6
10
 
7
- function ask(rl, question) {
8
- return new Promise((resolve) => {
9
- rl.question(question, (answer) => {
10
- resolve(answer.trim());
11
- });
12
- });
11
+ function isNonInteractive() {
12
+ return Boolean(process.env.npm_config_yes) || process.argv.includes('--postinstall');
13
13
  }
14
14
 
15
- function allTokensProvided(cliTokens) {
15
+ function allPromptsProvided(cliFlags) {
16
16
  return (
17
- cliTokens.stitchApiKey !== undefined &&
18
- cliTokens.cloudflareToken !== undefined &&
19
- cliTokens.cloudflareAccountId !== undefined
17
+ cliFlags.bundleId !== undefined &&
18
+ cliFlags.stitchApiKey !== undefined &&
19
+ cliFlags.cloudflareToken !== undefined &&
20
+ cliFlags.cloudflareAccountId !== undefined
20
21
  );
21
22
  }
22
23
 
23
- function allPromptsProvided(cliTokens) {
24
- return (
25
- cliTokens.bundleId !== undefined &&
26
- allTokensProvided(cliTokens)
24
+ async function promptMcpTokens(rl, cliFlags, currentConfig) {
25
+ const result = {
26
+ stitchApiKey: cliFlags.stitchApiKey ?? '',
27
+ cloudflareToken: cliFlags.cloudflareToken ?? '',
28
+ cloudflareAccountId: cliFlags.cloudflareAccountId ?? '',
29
+ };
30
+
31
+ const allProvided = (
32
+ cliFlags.stitchApiKey !== undefined &&
33
+ cliFlags.cloudflareToken !== undefined &&
34
+ cliFlags.cloudflareAccountId !== undefined
27
35
  );
36
+
37
+ if (allProvided) return result;
38
+
39
+ console.log('Press Enter to skip any token you do not have yet.');
40
+ console.log('');
41
+
42
+ if (cliFlags.stitchApiKey === undefined) {
43
+ result.stitchApiKey = await askQuestion(rl, 'Stitch MCP API Key', currentConfig.stitchApiKey || '');
44
+ }
45
+
46
+ if (cliFlags.cloudflareToken === undefined) {
47
+ result.cloudflareToken = await askQuestion(rl, 'Cloudflare API Token', currentConfig.cloudflareToken || '');
48
+ }
49
+
50
+ if (result.cloudflareToken && cliFlags.cloudflareAccountId === undefined) {
51
+ result.cloudflareAccountId = await askQuestion(
52
+ rl, 'Cloudflare Account ID', currentConfig.cloudflareAccountId || ''
53
+ );
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ 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
+ const identity = await promptAppIdentity(rl, cliFlags, currentConfig);
64
+
65
+ const credentials = await promptCredentials(rl, cliFlags, currentConfig, projectDir);
66
+
67
+ const storeSettings = await promptStoreSettings(rl, cliFlags, currentConfig);
68
+
69
+ 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
+ const mcpTokens = await promptMcpTokens(rl, cliFlags, currentConfig);
75
+
76
+ return { ...identity, ...credentials, ...storeSettings, ...webSettings, ...mcpTokens };
28
77
  }
29
78
 
30
79
  export async function promptForTokens(cliTokens = {}) {
@@ -40,61 +89,23 @@ export async function promptForTokens(cliTokens = {}) {
40
89
  return result;
41
90
  }
42
91
 
43
- if (!isInteractive()) {
92
+ if (!isInteractive() || isNonInteractive()) {
44
93
  console.log('Non-interactive terminal detected, skipping prompts.');
45
94
  console.log('Run "npx store-automator" manually to configure.');
46
95
  return result;
47
96
  }
48
97
 
49
- const rl = createInterface({
50
- input: process.stdin,
51
- output: process.stdout,
52
- });
53
-
54
- console.log('');
98
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
55
99
 
56
100
  try {
57
- if (cliTokens.bundleId === undefined) {
58
- console.log('App Configuration');
59
- console.log('');
60
- result.bundleId = await ask(
61
- rl,
62
- 'Bundle ID / Package Name (e.g., com.company.app): '
63
- );
64
- console.log('');
65
- }
66
-
67
- if (allTokensProvided(cliTokens)) {
68
- console.log('All MCP tokens provided via CLI flags.');
69
- return result;
70
- }
71
-
72
- console.log('MCP Server Configuration');
73
- console.log('Press Enter to skip any token you do not have yet.');
74
- console.log('');
75
-
76
- if (cliTokens.stitchApiKey === undefined) {
77
- result.stitchApiKey = await ask(
78
- rl,
79
- 'Stitch MCP API Key (STITCH_API_KEY value): '
80
- );
81
- }
82
-
83
- if (cliTokens.cloudflareToken === undefined) {
84
- result.cloudflareToken = await ask(
85
- rl,
86
- 'Cloudflare API Token: '
87
- );
88
- }
89
-
90
- if (result.cloudflareToken && cliTokens.cloudflareAccountId === undefined) {
91
- result.cloudflareAccountId = await ask(
92
- rl,
93
- 'Cloudflare Account ID: '
94
- );
95
- }
96
-
97
- return result;
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
+ };
98
109
  } finally {
99
110
  rl.close();
100
111
  }
@@ -0,0 +1,66 @@
1
+ import { askQuestion, isPlaceholder } from '../guide.mjs';
2
+
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
+ },
34
+ ];
35
+
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
+ export async function promptAppIdentity(rl, cliFlags, currentConfig) {
44
+ console.log('');
45
+ console.log('\x1b[1m\x1b[36m=== App Identity ===\x1b[0m');
46
+ console.log('');
47
+
48
+ const result = {};
49
+
50
+ for (const field of FIELDS) {
51
+ const flagVal = cliFlags[field.flag];
52
+ if (flagVal !== undefined) {
53
+ result[field.key] = flagVal;
54
+ continue;
55
+ }
56
+
57
+ const defaultVal = getDefault(field.key, cliFlags, currentConfig);
58
+ result[field.key] = await askQuestion(rl, field.label, defaultVal);
59
+ }
60
+
61
+ if (!result.packageName && result.bundleId) {
62
+ result.packageName = result.bundleId;
63
+ }
64
+
65
+ return result;
66
+ }
@@ -0,0 +1,122 @@
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
+ }
10
+
11
+ async function promptAppStoreApiKey(rl, cliFlags, currentConfig, projectDir) {
12
+ await runGuide(rl, {
13
+ title: 'Create App Store Connect API Key',
14
+ steps: [
15
+ 'Log in to App Store Connect (appstoreconnect.apple.com)',
16
+ 'Go to Users and Access -> Integrations -> Team Keys',
17
+ 'Click + to generate a new key with "App Manager" role',
18
+ 'Download the .p8 file',
19
+ `Save to creds/AuthKey.p8 in your project`,
20
+ ],
21
+ verifyPath: join(projectDir, 'creds', 'AuthKey.p8'),
22
+ verifyDescription: 'App Store Connect API Key (.p8)',
23
+ confirmQuestion: 'Have you created and saved the API key?',
24
+ });
25
+
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 };
39
+ }
40
+
41
+ async function promptPlayServiceAccount(rl, projectDir) {
42
+ await runGuide(rl, {
43
+ title: 'Create Google Play Service Account',
44
+ steps: [
45
+ 'Go to Google Play Console -> Setup -> API access',
46
+ 'Create a service account or link existing one',
47
+ 'Grant "Release manager" permission to the service account',
48
+ 'Download the JSON key file',
49
+ 'Save to creds/play-service-account.json in your project',
50
+ ],
51
+ verifyPath: join(projectDir, 'creds', 'play-service-account.json'),
52
+ verifyDescription: 'Google Play Service Account JSON',
53
+ confirmQuestion: 'Have you set up the service account?',
54
+ });
55
+ }
56
+
57
+ async function promptAndroidKeystore(rl, cliFlags, currentConfig) {
58
+ await runGuide(rl, {
59
+ title: 'Set up Android Keystore',
60
+ steps: [
61
+ 'If you don\'t have a keystore, run:',
62
+ ' keytool -genkey -v -keystore creds/upload-keystore.jks \\',
63
+ ' -keyalg RSA -keysize 2048 -validity 10000',
64
+ 'Keep the keystore file safe — you cannot replace it once uploaded to Google Play',
65
+ ],
66
+ confirmQuestion: 'Have you created or located your Android keystore?',
67
+ });
68
+
69
+ const keystorePassword = await askQuestion(
70
+ rl,
71
+ 'Keystore password',
72
+ getDefault('keystorePassword', cliFlags, currentConfig)
73
+ );
74
+
75
+ return keystorePassword;
76
+ }
77
+
78
+ async function promptMatchSigning(rl, cliFlags, currentConfig) {
79
+ await runGuide(rl, {
80
+ title: 'Set up Match Code Signing',
81
+ steps: [
82
+ 'Create a PRIVATE Git repository for certificates (e.g. github.com/yourorg/certificates)',
83
+ 'Generate an SSH deploy key: ssh-keygen -t ed25519 -f creds/match_deploy_key',
84
+ 'Add the public key as a deploy key to the certificates repo (with write access)',
85
+ ],
86
+ 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 };
102
+ }
103
+
104
+ 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('');
108
+
109
+ const { keyId, issuerId } = await promptAppStoreApiKey(
110
+ rl, cliFlags, currentConfig, projectDir
111
+ );
112
+
113
+ await promptPlayServiceAccount(rl, projectDir);
114
+
115
+ const keystorePassword = await promptAndroidKeystore(rl, cliFlags, currentConfig);
116
+
117
+ const { matchDeployKeyPath, matchGitUrl } = await promptMatchSigning(
118
+ rl, cliFlags, currentConfig
119
+ );
120
+
121
+ return { keyId, issuerId, keystorePassword, matchDeployKeyPath, matchGitUrl };
122
+ }
@@ -0,0 +1,185 @@
1
+ import { runGuide, askQuestion, isPlaceholder } from '../guide.mjs';
2
+
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
+ }
9
+
10
+ function getDefaultWithFallback(key, cliFlags, currentConfig, fallback) {
11
+ const val = getDefault(key, cliFlags, currentConfig);
12
+ return val !== '' ? val : fallback;
13
+ }
14
+
15
+ async function promptIosSettings(rl, cliFlags, currentConfig) {
16
+ await runGuide(rl, {
17
+ title: 'Create App in App Store Connect',
18
+ steps: [
19
+ 'Go to App Store Connect -> My Apps -> +',
20
+ 'Select your Bundle ID',
21
+ 'Fill in app name and primary language',
22
+ 'Save the app',
23
+ ],
24
+ confirmQuestion: 'Have you created the app in App Store Connect?',
25
+ });
26
+
27
+ const primaryCategory = await askQuestion(
28
+ rl,
29
+ 'iOS Primary Category (e.g. GAMES, UTILITIES)',
30
+ getDefault('primaryCategory', cliFlags, currentConfig)
31
+ );
32
+
33
+ const secondaryCategory = await askQuestion(
34
+ rl,
35
+ 'iOS Secondary Category (optional)',
36
+ getDefault('secondaryCategory', cliFlags, currentConfig)
37
+ );
38
+
39
+ const priceTier = await askQuestion(
40
+ rl,
41
+ 'iOS Price Tier',
42
+ getDefaultWithFallback('priceTier', cliFlags, currentConfig, '0')
43
+ );
44
+
45
+ const submitForReview = await askQuestion(
46
+ rl,
47
+ 'Auto-submit for review? (true/false)',
48
+ getDefaultWithFallback('submitForReview', cliFlags, currentConfig, 'true')
49
+ );
50
+
51
+ const automaticRelease = await askQuestion(
52
+ rl,
53
+ 'Automatic release after approval? (true/false)',
54
+ getDefaultWithFallback('automaticRelease', cliFlags, currentConfig, 'true')
55
+ );
56
+
57
+ return {
58
+ primaryCategory,
59
+ secondaryCategory,
60
+ priceTier: Number(priceTier) || 0,
61
+ submitForReview: submitForReview === 'true',
62
+ automaticRelease: automaticRelease === 'true',
63
+ };
64
+ }
65
+
66
+ async function promptAndroidSettings(rl, cliFlags, currentConfig) {
67
+ await runGuide(rl, {
68
+ title: 'Create App in Google Play Console',
69
+ steps: [
70
+ 'Go to Google Play Console -> All apps -> Create app',
71
+ 'Enter app name and default language',
72
+ 'Select app type (App) and free/paid',
73
+ 'Complete the declarations and create',
74
+ ],
75
+ confirmQuestion: 'Have you created the app in Google Play Console?',
76
+ });
77
+
78
+ const track = await askQuestion(
79
+ rl,
80
+ 'Android release track (internal/alpha/beta/production)',
81
+ getDefaultWithFallback('track', cliFlags, currentConfig, 'internal')
82
+ );
83
+
84
+ const rolloutFraction = await askQuestion(
85
+ rl,
86
+ 'Rollout fraction (0.0 - 1.0)',
87
+ getDefaultWithFallback('rolloutFraction', cliFlags, currentConfig, '1.0')
88
+ );
89
+
90
+ const inAppUpdatePriority = await askQuestion(
91
+ rl,
92
+ 'In-app update priority (0-5)',
93
+ getDefaultWithFallback('inAppUpdatePriority', cliFlags, currentConfig, '3')
94
+ );
95
+
96
+ return {
97
+ track,
98
+ rolloutFraction: Number(rolloutFraction) || 1.0,
99
+ inAppUpdatePriority: Number(inAppUpdatePriority) || 3,
100
+ };
101
+ }
102
+
103
+ async function promptMetadataSettings(rl, cliFlags, currentConfig) {
104
+ const languages = await askQuestion(
105
+ rl,
106
+ 'Metadata languages (comma-separated, e.g. en-US,de-DE)',
107
+ getDefaultWithFallback('languages', cliFlags, currentConfig, 'en-US')
108
+ );
109
+
110
+ return { languages: languages.split(',').map((l) => l.trim()).filter(Boolean) };
111
+ }
112
+
113
+ export async function promptStoreSettings(rl, cliFlags, currentConfig) {
114
+ console.log('');
115
+ console.log('\x1b[1m\x1b[36m=== Store Settings ===\x1b[0m');
116
+ console.log('');
117
+
118
+ const ios = await promptIosSettings(rl, cliFlags, currentConfig);
119
+ const android = await promptAndroidSettings(rl, cliFlags, currentConfig);
120
+ const metadata = await promptMetadataSettings(rl, cliFlags, currentConfig);
121
+
122
+ return { ...ios, ...android, ...metadata };
123
+ }
124
+
125
+ export async function promptWebSettings(rl, cliFlags, currentConfig) {
126
+ console.log('');
127
+ console.log('\x1b[1m\x1b[36m=== Web Settings ===\x1b[0m');
128
+ console.log('');
129
+
130
+ const fields = [
131
+ { key: 'domain', label: 'Domain (e.g. example.com)' },
132
+ { key: 'cfProjectName', label: 'Cloudflare Pages project name' },
133
+ { key: 'tagline', label: 'App tagline' },
134
+ { key: 'primaryColor', label: 'Primary color (hex, e.g. #4A90D9)' },
135
+ { key: 'secondaryColor', label: 'Secondary color (hex)' },
136
+ { key: 'companyName', label: 'Company name' },
137
+ { key: 'contactEmail', label: 'Contact email' },
138
+ { key: 'supportEmail', label: 'Support email' },
139
+ { key: 'jurisdiction', label: 'Legal jurisdiction (e.g. Delaware, USA)' },
140
+ ];
141
+
142
+ const result = {};
143
+ for (const field of fields) {
144
+ result[field.key] = await askQuestion(
145
+ rl,
146
+ field.label,
147
+ getDefault(field.key, cliFlags, currentConfig)
148
+ );
149
+ }
150
+ return result;
151
+ }
152
+
153
+ export async function runPostInstallGuides(rl) {
154
+ await runGuide(rl, {
155
+ title: 'Create Private GitHub Repository',
156
+ steps: [
157
+ 'Go to github.com/new',
158
+ 'Create a PRIVATE repository',
159
+ 'Push your project to the repository',
160
+ ],
161
+ confirmQuestion: 'Have you created the GitHub repository?',
162
+ });
163
+
164
+ await runGuide(rl, {
165
+ title: 'Set GitHub Actions Secrets',
166
+ steps: [
167
+ 'Go to your repo -> Settings -> Secrets -> Actions',
168
+ 'Add secret MATCH_PASSWORD (your match encryption password)',
169
+ 'Add secret KEYSTORE_PASSWORD (your Android keystore password)',
170
+ 'Upload creds/ files as secrets if using CI',
171
+ ],
172
+ confirmQuestion: 'Have you configured GitHub Actions secrets?',
173
+ });
174
+
175
+ await runGuide(rl, {
176
+ title: 'Create Firebase Project (optional)',
177
+ steps: [
178
+ 'Go to console.firebase.google.com',
179
+ 'Create a new project or select existing',
180
+ 'Add your iOS and Android apps',
181
+ 'Download google-services.json and GoogleService-Info.plist',
182
+ ],
183
+ confirmQuestion: 'Have you set up Firebase? (skip if not needed)',
184
+ });
185
+ }
package/src/templates.mjs CHANGED
@@ -46,13 +46,18 @@ function copyIfMissing(srcPath, destPath, label, isDirectory) {
46
46
  }
47
47
  }
48
48
 
49
- export function installClaudeMd(targetPath, packageDir) {
49
+ export function installClaudeMd(targetPath, packageDir, appName) {
50
50
  const template = join(packageDir, 'templates', 'CLAUDE.md.template');
51
51
  if (!existsSync(template)) return;
52
52
  const action = existsSync(targetPath) ? 'Updating' : 'Installing';
53
53
  console.log(`${action} CLAUDE.md...`);
54
54
  ensureDir(join(targetPath, '..'));
55
- copyFileSync(template, targetPath);
55
+
56
+ let content = readFileSync(template, 'utf8');
57
+ if (appName) {
58
+ content = content.replace(/\{APP_NAME\}/g, appName);
59
+ }
60
+ writeFileSync(targetPath, content, 'utf8');
56
61
  }
57
62
 
58
63
  export function installCiTemplates(projectDir, packageDir) {
@@ -91,6 +91,7 @@ platform :ios do
91
91
  skip_binary_upload: true,
92
92
  skip_metadata: false,
93
93
  skip_screenshots: false,
94
+ overwrite_screenshots: true,
94
95
  run_precheck_before_submit: false,
95
96
  submit_for_review: false
96
97
  )
@@ -92,6 +92,22 @@ jobs:
92
92
  with:
93
93
  channel: stable
94
94
 
95
+ - name: Cache Flutter
96
+ uses: actions/cache@v4
97
+ with:
98
+ path: /Users/runner/.pub-cache
99
+ key: pub-cache-${{ hashFiles('app/pubspec.lock') }}
100
+ restore-keys: |
101
+ pub-cache-
102
+
103
+ - name: Cache CocoaPods
104
+ uses: actions/cache@v4
105
+ with:
106
+ path: app/ios/Pods
107
+ key: pods-${{ hashFiles('app/ios/Podfile.lock') }}
108
+ restore-keys: |
109
+ pods-
110
+
95
111
  - name: Install yq
96
112
  run: brew install yq
97
113
 
@@ -16,34 +16,10 @@ if [ ! -d "$METADATA_DIR" ] && [ ! -d "$SCREENSHOTS_DIR" ]; then
16
16
  ci_skip "No iOS metadata directories found"
17
17
  fi
18
18
 
19
- HASH_DIRS=()
20
19
  for dir in "$METADATA_DIR" "$SCREENSHOTS_DIR"; do
21
- [ -d "$dir" ] && HASH_DIRS+=("$dir") && echo " Found: $dir"
20
+ [ -d "$dir" ] && echo " Found: $dir"
22
21
  done
23
22
 
24
- # --- Hash-based change detection ---
25
- HASH=$(
26
- find "${HASH_DIRS[@]}" -type f ! -name '.DS_Store' -print0 \
27
- | LC_ALL=C sort -z \
28
- | xargs -0 shasum -a 256 2>/dev/null \
29
- | shasum -a 256 \
30
- | cut -d' ' -f1
31
- )
32
-
33
- STATE_DIR="$PROJECT_ROOT/.ci-state"
34
- mkdir -p "$STATE_DIR"
35
- STATE_FILE="$STATE_DIR/ios-metadata-hash"
36
-
37
- if [ -f "$STATE_FILE" ]; then
38
- STORED_HASH=$(cat "$STATE_FILE")
39
- if [ "$HASH" = "$STORED_HASH" ]; then
40
- ci_skip "iOS metadata unchanged since last upload"
41
- fi
42
- echo "Changes detected (old: ${STORED_HASH:0:12}..., new: ${HASH:0:12}...)"
43
- else
44
- echo "No cached hash found. First run — will upload metadata."
45
- fi
46
-
47
23
  # --- Validate Apple credentials ---
48
24
  if [ -z "$P8_KEY_PATH" ] || [ -z "$APPLE_KEY_ID" ] || [ -z "$APPLE_ISSUER_ID" ]; then
49
25
  echo "ERROR: Apple credentials not configured in ci.config.yaml" >&2
@@ -70,6 +46,4 @@ cd "$APP_ROOT/ios"
70
46
 
71
47
  bundle exec fastlane upload_metadata_ios
72
48
 
73
- # --- Update hash on success ---
74
- echo "$HASH" > "$STATE_FILE"
75
49
  ci_done "iOS metadata uploaded to App Store Connect"