@daemux/store-automator 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.md +11 -13
  3. package/bin/cli.mjs +69 -10
  4. package/package.json +7 -8
  5. package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
  6. package/plugins/store-automator/agents/app-designer.md +320 -0
  7. package/plugins/store-automator/agents/appstore-meta-creator.md +37 -1
  8. package/plugins/store-automator/agents/appstore-reviewer.md +66 -5
  9. package/plugins/store-automator/agents/architect.md +144 -0
  10. package/plugins/store-automator/agents/developer.md +249 -0
  11. package/plugins/store-automator/agents/devops.md +396 -0
  12. package/plugins/store-automator/agents/product-manager.md +258 -0
  13. package/plugins/store-automator/agents/reviewer.md +386 -0
  14. package/plugins/store-automator/agents/simplifier.md +192 -0
  15. package/plugins/store-automator/agents/tester.md +284 -0
  16. package/scripts/check_changed.sh +23 -0
  17. package/scripts/check_google_play.py +139 -0
  18. package/scripts/codemagic-setup.mjs +44 -0
  19. package/scripts/generate.sh +107 -0
  20. package/scripts/manage_version_ios.py +168 -0
  21. package/src/codemagic-api.mjs +73 -0
  22. package/src/codemagic-setup.mjs +164 -0
  23. package/src/github-setup.mjs +52 -0
  24. package/src/install.mjs +32 -7
  25. package/src/prompt.mjs +7 -2
  26. package/src/templates.mjs +13 -0
  27. package/src/uninstall.mjs +37 -21
  28. package/templates/CLAUDE.md.template +298 -193
  29. package/templates/ci.config.yaml.template +14 -1
  30. package/templates/codemagic.template.yaml +15 -6
  31. package/templates/fastlane/android/Fastfile.template +11 -4
  32. package/templates/fastlane/ios/Fastfile.template +27 -11
  33. package/templates/fastlane/ios/Snapfile.template +3 -1
  34. package/templates/github/workflows/codemagic-trigger.yml +68 -0
  35. package/templates/scripts/create_app_record.py +172 -0
  36. package/templates/scripts/generate.sh +6 -0
  37. package/plugins/store-automator/agents/appstore-media-designer.md +0 -261
  38. package/src/dependency-check.mjs +0 -26
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Automatic iOS version management via App Store Connect API.
4
+ Queries the latest app version and decides whether to create a new version or reuse existing.
5
+
6
+ State logic:
7
+ READY_FOR_SALE -> create new version (auto-increment patch)
8
+ PREPARE_FOR_SUBMISSION -> reuse existing version
9
+ WAITING_FOR_REVIEW -> reuse existing version
10
+ IN_REVIEW -> reuse existing version
11
+ REJECTED -> reuse existing version
12
+ PENDING_DEVELOPER_RELEASE -> error (must manually release first)
13
+ No versions exist -> create version 1.0.0
14
+
15
+ Version increment uses base-10 rollover: 1.0.9 -> 1.1.0, 1.9.9 -> 2.0.0
16
+
17
+ Required env vars:
18
+ APP_STORE_CONNECT_KEY_IDENTIFIER - Key ID from App Store Connect
19
+ APP_STORE_CONNECT_ISSUER_ID - Issuer ID from App Store Connect
20
+ APP_STORE_CONNECT_PRIVATE_KEY - Contents of the P8 key file
21
+ BUNDLE_ID - App bundle identifier
22
+ """
23
+ import os
24
+ import sys
25
+ import json
26
+ import time
27
+
28
+ try:
29
+ import jwt
30
+ import requests
31
+ except ImportError:
32
+ print("Installing dependencies...", file=sys.stderr)
33
+ import subprocess
34
+ subprocess.check_call([
35
+ sys.executable, "-m", "pip", "install",
36
+ "PyJWT", "cryptography", "requests"
37
+ ], stdout=subprocess.DEVNULL)
38
+ import jwt
39
+ import requests
40
+
41
+ BASE_URL = "https://api.appstoreconnect.apple.com/v1"
42
+
43
+
44
+ def get_jwt_token(key_id, issuer_id, private_key):
45
+ payload = {
46
+ "iss": issuer_id,
47
+ "exp": int(time.time()) + 1200,
48
+ "aud": "appstoreconnect-v1",
49
+ }
50
+ return jwt.encode(payload, private_key, algorithm="ES256", headers={"kid": key_id})
51
+
52
+
53
+ def get_app_id(headers, bundle_id):
54
+ resp = requests.get(
55
+ f"{BASE_URL}/apps",
56
+ params={"filter[bundleId]": bundle_id},
57
+ headers=headers,
58
+ timeout=(10, 30),
59
+ )
60
+ resp.raise_for_status()
61
+ data = resp.json().get("data", [])
62
+ if not data:
63
+ print(f"ERROR: No app found for bundle ID {bundle_id}", file=sys.stderr)
64
+ sys.exit(1)
65
+ return data[0]["id"]
66
+
67
+
68
+ def get_versions(headers, app_id):
69
+ resp = requests.get(
70
+ f"{BASE_URL}/apps/{app_id}/appStoreVersions",
71
+ headers=headers,
72
+ timeout=(10, 30),
73
+ )
74
+ resp.raise_for_status()
75
+ return resp.json().get("data", [])
76
+
77
+
78
+ def increment_version(version_str):
79
+ parts = version_str.split(".")
80
+ major = int(parts[0])
81
+ minor = int(parts[1]) if len(parts) > 1 else 0
82
+ micro = int(parts[2]) if len(parts) > 2 else 0
83
+ micro += 1
84
+ if micro >= 10:
85
+ micro, minor = 0, minor + 1
86
+ if minor >= 10:
87
+ minor, major = 0, major + 1
88
+ return f"{major}.{minor}.{micro}"
89
+
90
+
91
+ def create_version(headers, app_id, version_string):
92
+ resp = requests.post(
93
+ f"{BASE_URL}/appStoreVersions",
94
+ json={
95
+ "data": {
96
+ "type": "appStoreVersions",
97
+ "attributes": {
98
+ "platform": "IOS",
99
+ "versionString": version_string,
100
+ "releaseType": "AFTER_APPROVAL",
101
+ },
102
+ "relationships": {
103
+ "app": {"data": {"type": "apps", "id": app_id}}
104
+ },
105
+ }
106
+ },
107
+ headers=headers,
108
+ timeout=(10, 30),
109
+ )
110
+ resp.raise_for_status()
111
+ return resp.json()["data"]
112
+
113
+
114
+ def main():
115
+ key_id = os.environ.get("APP_STORE_CONNECT_KEY_IDENTIFIER", "")
116
+ issuer_id = os.environ.get("APP_STORE_CONNECT_ISSUER_ID", "")
117
+ private_key = os.environ.get("APP_STORE_CONNECT_PRIVATE_KEY", "")
118
+ bundle_id = os.environ.get("BUNDLE_ID", "")
119
+
120
+ if not all([key_id, issuer_id, private_key, bundle_id]):
121
+ print("ERROR: Missing required environment variables", file=sys.stderr)
122
+ sys.exit(1)
123
+
124
+ token = get_jwt_token(key_id, issuer_id, private_key)
125
+ headers = {
126
+ "Authorization": f"Bearer {token}",
127
+ "Content-Type": "application/json",
128
+ }
129
+
130
+ app_id = get_app_id(headers, bundle_id)
131
+ versions = get_versions(headers, app_id)
132
+
133
+ new_version = None
134
+ if not versions:
135
+ new_version = "1.0.0"
136
+ else:
137
+ latest = max(versions, key=lambda x: x["attributes"]["createdDate"])
138
+ state = latest["attributes"]["appStoreState"]
139
+ current_version = latest["attributes"]["versionString"]
140
+
141
+ if state == "PENDING_DEVELOPER_RELEASE":
142
+ print(
143
+ "ERROR: App is Pending Developer Release. Publish it first.",
144
+ file=sys.stderr,
145
+ )
146
+ sys.exit(1)
147
+ elif state == "READY_FOR_SALE":
148
+ new_version = increment_version(current_version)
149
+ else:
150
+ result = {
151
+ "version": current_version,
152
+ "version_id": latest["id"],
153
+ "state": state,
154
+ }
155
+
156
+ if new_version:
157
+ created = create_version(headers, app_id, new_version)
158
+ result = {
159
+ "version": new_version,
160
+ "version_id": created["id"],
161
+ "state": "PREPARE_FOR_SUBMISSION",
162
+ }
163
+
164
+ print(json.dumps(result))
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
@@ -0,0 +1,73 @@
1
+ const BASE_URL = 'https://api.codemagic.io';
2
+
3
+ async function cmFetch(token, method, path, body) {
4
+ const url = `${BASE_URL}${path}`;
5
+ const headers = {
6
+ 'x-auth-token': token,
7
+ 'Content-Type': 'application/json',
8
+ };
9
+
10
+ let response;
11
+ try {
12
+ response = await fetch(url, {
13
+ method,
14
+ headers,
15
+ body: body ? JSON.stringify(body) : undefined,
16
+ });
17
+ } catch (err) {
18
+ throw new Error(`Network error: ${err.message}.`);
19
+ }
20
+
21
+ if (response.status === 401) {
22
+ throw new Error('Authentication failed. Check your CM_API_TOKEN.');
23
+ }
24
+ if (response.status === 403) {
25
+ throw new Error('Permission denied. Check API token permissions.');
26
+ }
27
+ if (response.status >= 500) {
28
+ throw new Error(`Codemagic API error (${response.status}). Try again later.`);
29
+ }
30
+ if (!response.ok) {
31
+ throw new Error(`Codemagic API error (${response.status}).`);
32
+ }
33
+
34
+ const text = await response.text();
35
+ if (!text) return {};
36
+ return JSON.parse(text);
37
+ }
38
+
39
+ export async function listApps(token) {
40
+ const data = await cmFetch(token, 'GET', '/apps');
41
+ return data.applications || [];
42
+ }
43
+
44
+ export async function findAppByRepo(token, repoUrl) {
45
+ const apps = await listApps(token);
46
+ const normalized = normalizeRepoUrl(repoUrl);
47
+ return apps.find((app) => {
48
+ const appRepo = app.repository?.url || app.repositoryUrl || '';
49
+ return normalizeRepoUrl(appRepo) === normalized;
50
+ }) || null;
51
+ }
52
+
53
+ export async function addApp(token, repoUrl) {
54
+ return cmFetch(token, 'POST', '/apps', { repositoryUrl: repoUrl });
55
+ }
56
+
57
+ export async function startBuild(token, appId, workflowId, branch) {
58
+ return cmFetch(token, 'POST', '/builds', { appId, workflowId, branch });
59
+ }
60
+
61
+ export async function getBuildStatus(token, buildId) {
62
+ const data = await cmFetch(token, 'GET', `/builds/${buildId}`);
63
+ return data.build || data;
64
+ }
65
+
66
+ export function normalizeRepoUrl(url) {
67
+ return url
68
+ .replace(/^git@github\.com:/, 'https://github.com/')
69
+ .replace(/^ssh:\/\/git@github\.com\//, 'https://github.com/')
70
+ .replace(/\.git$/, '')
71
+ .replace(/\/$/, '')
72
+ .toLowerCase();
73
+ }
@@ -0,0 +1,164 @@
1
+ import { findAppByRepo, addApp, startBuild, getBuildStatus, normalizeRepoUrl } from './codemagic-api.mjs';
2
+ import { exec } from './utils.mjs';
3
+ import { execFileSync } from 'child_process';
4
+
5
+ const POLL_INTERVAL_MS = 30_000;
6
+ const POLL_TIMEOUT_MS = 15 * 60 * 1000;
7
+ const TERMINAL_STATUSES = new Set(['finished', 'failed', 'canceled']);
8
+
9
+ function resolveToken(tokenArg) {
10
+ const token = process.env.CM_API_TOKEN || tokenArg;
11
+ if (!token) {
12
+ console.error('Codemagic API token required. Set CM_API_TOKEN or pass --token=...');
13
+ process.exit(1);
14
+ }
15
+ return token;
16
+ }
17
+
18
+ function resolveRepoUrl() {
19
+ const url = exec('git remote get-url origin');
20
+ if (!url) {
21
+ console.error('Not a git repository. Run from your project root.');
22
+ process.exit(1);
23
+ }
24
+ return normalizeRepoUrl(url);
25
+ }
26
+
27
+ function sleep(ms) {
28
+ return new Promise((resolve) => setTimeout(resolve, ms));
29
+ }
30
+
31
+ function extractOwnerRepo(repoUrl) {
32
+ const match = repoUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
33
+ return match ? { owner: match[1], repo: match[2] } : null;
34
+ }
35
+
36
+ function setupWebhook(repoUrl, appId) {
37
+ const ownerRepo = extractOwnerRepo(repoUrl);
38
+ if (!ownerRepo) {
39
+ console.log('Warning: Cannot extract owner/repo from URL. Skipping webhook setup.');
40
+ return false;
41
+ }
42
+
43
+ const { owner, repo } = ownerRepo;
44
+ const webhookUrl = `https://api.codemagic.io/hooks/${appId}`;
45
+
46
+ if (!exec('which gh')) {
47
+ console.log('');
48
+ console.log('GitHub CLI not found. Webhook not created.');
49
+ console.log('To enable auto-triggering, manually create webhook:');
50
+ console.log(` Repository: ${owner}/${repo}`);
51
+ console.log(` URL: ${webhookUrl}`);
52
+ console.log(` Content type: application/json`);
53
+ console.log(` Events: push, pull_request, create`);
54
+ return false;
55
+ }
56
+
57
+ try {
58
+ const payload = JSON.stringify({
59
+ name: 'web',
60
+ active: true,
61
+ events: ['push', 'pull_request', 'create'],
62
+ config: { url: webhookUrl, content_type: 'json' },
63
+ });
64
+
65
+ execFileSync('gh', ['api', `repos/${owner}/${repo}/hooks`, '-X', 'POST', '--input', '-'], {
66
+ input: payload,
67
+ encoding: 'utf8',
68
+ stdio: ['pipe', 'pipe', 'pipe'],
69
+ });
70
+ console.log(`Webhook created: ${owner}/${repo} → Codemagic`);
71
+ return true;
72
+ } catch (error) {
73
+ const errorMsg = String(error?.message || error);
74
+ if (errorMsg.includes('422') || errorMsg.includes('already exists')) {
75
+ console.log('Webhook already exists.');
76
+ return true;
77
+ }
78
+ console.log(`Warning: Cannot create webhook (${errorMsg})`);
79
+ console.log('Create manually in GitHub repository settings.');
80
+ return false;
81
+ }
82
+ }
83
+
84
+ async function pollBuildStatus(token, buildId) {
85
+ console.log(`Polling build ${buildId} (every 30s, max 15 min)...`);
86
+ const start = Date.now();
87
+
88
+ while (Date.now() - start < POLL_TIMEOUT_MS) {
89
+ await sleep(POLL_INTERVAL_MS);
90
+
91
+ const build = await getBuildStatus(token, buildId);
92
+ const status = build.status || 'unknown';
93
+ console.log(` Build status: ${status}`);
94
+
95
+ if (TERMINAL_STATUSES.has(status)) {
96
+ return status;
97
+ }
98
+ }
99
+
100
+ console.log('Build still running. Check dashboard.');
101
+ return 'timeout';
102
+ }
103
+
104
+ export async function runCodemagicSetup(options) {
105
+ const {
106
+ tokenArg = '',
107
+ branch = 'main',
108
+ workflowId = 'default',
109
+ trigger = false,
110
+ wait = false,
111
+ } = options;
112
+
113
+ const token = resolveToken(tokenArg);
114
+ const repoUrl = resolveRepoUrl();
115
+
116
+ console.log(`Repository: ${repoUrl}`);
117
+ console.log('Checking Codemagic for existing app...');
118
+
119
+ let app = await findAppByRepo(token, repoUrl);
120
+
121
+ if (app) {
122
+ console.log(`App already registered: ${app.appName || app._id}`);
123
+ } else {
124
+ console.log('App not found. Adding to Codemagic...');
125
+ app = await addApp(token, repoUrl);
126
+ console.log(`App added: ${app.appName || app._id}`);
127
+
128
+ console.log('Setting up GitHub webhook...');
129
+ setupWebhook(repoUrl, app._id);
130
+ }
131
+
132
+ const appId = app._id;
133
+
134
+ if (!trigger) {
135
+ console.log('\nSetup complete. Use --trigger to start a build.\n');
136
+ console.log('To enable GitHub Actions auto-trigger:');
137
+ console.log(' 1. Fill codemagic.app_id in ci.config.yaml');
138
+ console.log(' 2. Run: npx @daemux/store-automator --github-setup');
139
+ return { appId, buildId: null, status: null };
140
+ }
141
+
142
+ console.log(`\nTriggering build on branch "${branch}" (workflow: ${workflowId})...`);
143
+ const buildResult = await startBuild(token, appId, workflowId, branch);
144
+ const buildId = buildResult.buildId;
145
+ console.log(`Build started: ${buildId}`);
146
+
147
+ if (!wait) {
148
+ console.log('Use --wait to poll until completion.');
149
+ return { appId, buildId, status: null };
150
+ }
151
+
152
+ const status = await pollBuildStatus(token, buildId);
153
+
154
+ if (status === 'finished') {
155
+ console.log('Build finished successfully.');
156
+ } else if (status === 'failed') {
157
+ console.error('Build failed.');
158
+ process.exit(1);
159
+ } else if (status === 'canceled') {
160
+ console.log('Build was canceled.');
161
+ }
162
+
163
+ return { appId, buildId, status };
164
+ }
@@ -0,0 +1,52 @@
1
+ import { exec } from './utils.mjs';
2
+ import { execFileSync } from 'child_process';
3
+
4
+ function resolveToken(tokenArg) {
5
+ const token = process.env.CM_API_TOKEN || tokenArg;
6
+ if (!token) {
7
+ console.error('Codemagic API token required. Set CM_API_TOKEN or pass --token=...');
8
+ process.exit(1);
9
+ }
10
+ return token;
11
+ }
12
+
13
+ function checkGhCli() {
14
+ const ghPath = exec('which gh');
15
+ if (!ghPath) {
16
+ console.error('GitHub CLI (gh) is required but not found.');
17
+ console.error('Install it: https://cli.github.com/');
18
+ process.exit(1);
19
+ }
20
+
21
+ const authStatus = exec('gh auth status 2>&1');
22
+ if (!authStatus || authStatus.includes('not logged')) {
23
+ console.error('GitHub CLI is not authenticated. Run: gh auth login');
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ function setGitHubSecret(name, value) {
29
+ try {
30
+ execFileSync('gh', ['secret', 'set', name, '--body', value], {
31
+ encoding: 'utf8',
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ });
34
+ console.log(` Secret ${name} set successfully.`);
35
+ } catch (err) {
36
+ console.error(`Failed to set GitHub secret: ${name}`);
37
+ if (err.stderr) console.error(err.stderr.trim());
38
+ process.exit(1);
39
+ }
40
+ }
41
+
42
+ export async function runGitHubSetup(options) {
43
+ checkGhCli();
44
+ const token = resolveToken(options.tokenArg);
45
+
46
+ console.log('Configuring GitHub repository secret...');
47
+ setGitHubSecret('CM_API_TOKEN', token);
48
+
49
+ console.log('\nGitHub Actions setup complete.');
50
+ console.log('Next: Fill codemagic.app_id in ci.config.yaml.');
51
+ console.log('GitHub Actions will trigger configured Codemagic workflows on push to main.');
52
+ }
package/src/install.mjs CHANGED
@@ -1,14 +1,13 @@
1
1
  import { existsSync, rmSync, cpSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { execSync } from 'node:child_process';
4
+ import { execSync, execFileSync } from 'node:child_process';
5
5
  import {
6
6
  MARKETPLACE_DIR, KNOWN_MP_PATH, CACHE_DIR,
7
7
  MARKETPLACE_NAME, PLUGIN_REF,
8
8
  getPackageDir, exec, ensureDir, ensureFile, readJson, writeJson,
9
9
  } from './utils.mjs';
10
10
  import { injectEnvVars, injectStatusLine } from './settings.mjs';
11
- import { ensureClaudePlugin } from './dependency-check.mjs';
12
11
  import { promptForTokens } from './prompt.mjs';
13
12
  import { getMcpServers, writeMcpJson } from './mcp-setup.mjs';
14
13
  import { installClaudeMd, installCiTemplates, installFirebaseTemplates } from './templates.mjs';
@@ -62,7 +61,7 @@ function registerMarketplace() {
62
61
  data = {};
63
62
  }
64
63
  data[MARKETPLACE_NAME] = {
65
- source: { source: 'github', repo: 'daemux/store-automator' },
64
+ source: { source: 'github', repo: 'daemux/daemux-plugins' },
66
65
  installLocation: MARKETPLACE_DIR,
67
66
  lastUpdated: new Date().toISOString(),
68
67
  };
@@ -93,13 +92,29 @@ function printSummary(scope, oldVersion, newVersion) {
93
92
  }
94
93
  }
95
94
 
95
+ function setupGitHubActions(codemagicToken) {
96
+ if (!codemagicToken) return false;
97
+
98
+ try {
99
+ execFileSync('which', ['gh'], { encoding: 'utf8', stdio: 'pipe' });
100
+ const authStatus = execFileSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: 'pipe' });
101
+ if (authStatus.includes('not logged')) return false;
102
+
103
+ execFileSync('gh', ['secret', 'set', 'CM_API_TOKEN', '--body', codemagicToken], {
104
+ encoding: 'utf8',
105
+ stdio: 'pipe',
106
+ });
107
+ return true;
108
+ } catch {
109
+ return false;
110
+ }
111
+ }
112
+
96
113
  export async function runInstall(scope, isPostinstall = false) {
97
114
  checkClaudeCli();
98
115
 
99
116
  console.log('Installing/updating Daemux Store Automator...');
100
117
 
101
- ensureClaudePlugin();
102
-
103
118
  const tokens = await promptForTokens();
104
119
 
105
120
  const projectDir = process.cwd();
@@ -132,10 +147,20 @@ export async function runInstall(scope, isPostinstall = false) {
132
147
  injectEnvVars(settingsPath);
133
148
  injectStatusLine(settingsPath);
134
149
 
150
+ const ghConfigured = setupGitHubActions(tokens.codemagicToken);
151
+ if (ghConfigured) {
152
+ console.log('GitHub Actions: CM_API_TOKEN secret configured.');
153
+ } else if (tokens.codemagicToken) {
154
+ console.log('GitHub Actions: secret not set (gh CLI unavailable or not authenticated).');
155
+ }
156
+
135
157
  printSummary(scope, oldVersion, newVersion);
136
158
  console.log('');
137
159
  console.log('Next steps:');
138
- console.log(' 1. Fill ci.config.yaml with your app details');
160
+ console.log(' 1. Fill ci.config.yaml (including codemagic.app_id)');
139
161
  console.log(' 2. Add creds/AuthKey.p8 and creds/play-service-account.json');
140
- console.log(' 3. Start Claude Code and begin developing your app');
162
+ console.log(' 3. Start Claude Code');
163
+ if (!ghConfigured) {
164
+ console.log(' Note: For auto-trigger, install gh CLI and run "gh auth login"');
165
+ }
141
166
  }
package/src/prompt.mjs CHANGED
@@ -16,7 +16,7 @@ export async function promptForTokens() {
16
16
  if (!isInteractive()) {
17
17
  console.log('Non-interactive terminal detected, skipping token prompts.');
18
18
  console.log('Run "npx store-automator" manually to configure MCP tokens.');
19
- return { stitchApiKey: '', cloudflareToken: '', cloudflareAccountId: '' };
19
+ return { stitchApiKey: '', cloudflareToken: '', cloudflareAccountId: '', codemagicToken: '' };
20
20
  }
21
21
 
22
22
  const rl = createInterface({
@@ -48,7 +48,12 @@ export async function promptForTokens() {
48
48
  );
49
49
  }
50
50
 
51
- return { stitchApiKey, cloudflareToken, cloudflareAccountId };
51
+ const codemagicToken = await ask(
52
+ rl,
53
+ 'Codemagic API Token (CM_API_TOKEN for CI/CD builds): '
54
+ );
55
+
56
+ return { stitchApiKey, cloudflareToken, cloudflareAccountId, codemagicToken };
52
57
  } finally {
53
58
  rl.close();
54
59
  }
package/src/templates.mjs CHANGED
@@ -10,6 +10,10 @@ const FILE_COPIES = [
10
10
 
11
11
  const DIR_COPIES = ['scripts', 'fastlane', 'web'];
12
12
 
13
+ const DIR_COPIES_MAPPED = [
14
+ ['github', '.github'],
15
+ ];
16
+
13
17
  const FIREBASE_COPIES = [
14
18
  ['firebase/firestore.rules.template', 'backend/firestore.rules'],
15
19
  ['firebase/storage.rules.template', 'backend/storage.rules'],
@@ -60,6 +64,15 @@ export function installCiTemplates(projectDir, packageDir) {
60
64
  true,
61
65
  );
62
66
  }
67
+
68
+ for (const [src, dest] of DIR_COPIES_MAPPED) {
69
+ copyIfMissing(
70
+ join(templateDir, src),
71
+ join(projectDir, dest),
72
+ `${dest}/`,
73
+ true,
74
+ );
75
+ }
63
76
  }
64
77
 
65
78
  export function installFirebaseTemplates(projectDir, packageDir) {
package/src/uninstall.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, rmSync, unlinkSync } from 'node:fs';
1
+ import { existsSync, rmSync, unlinkSync, readdirSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
4
  import { execSync } from 'node:child_process';
@@ -58,6 +58,31 @@ function removeCiTemplates(projectDir) {
58
58
  rmSync(dirPath, { recursive: true, force: true });
59
59
  }
60
60
  }
61
+
62
+ removeGitHubWorkflow(projectDir);
63
+ }
64
+
65
+ function isDirEmpty(dirPath) {
66
+ if (!existsSync(dirPath)) return true;
67
+ return readdirSync(dirPath).length === 0;
68
+ }
69
+
70
+ function removeGitHubWorkflow(projectDir) {
71
+ const workflowFile = join(projectDir, '.github', 'workflows', 'codemagic-trigger.yml');
72
+ removeFileIfExists(workflowFile, '.github/workflows/codemagic-trigger.yml');
73
+
74
+ const workflowsDir = join(projectDir, '.github', 'workflows');
75
+ const githubDir = join(projectDir, '.github');
76
+
77
+ if (isDirEmpty(workflowsDir)) {
78
+ rmSync(workflowsDir, { recursive: true, force: true });
79
+ console.log('Removed empty .github/workflows/ directory.');
80
+ }
81
+
82
+ if (isDirEmpty(githubDir)) {
83
+ rmSync(githubDir, { recursive: true, force: true });
84
+ console.log('Removed empty .github/ directory.');
85
+ }
61
86
  }
62
87
 
63
88
  export async function runUninstall(scope) {
@@ -72,29 +97,20 @@ export async function runUninstall(scope) {
72
97
  unregisterMarketplace();
73
98
  }
74
99
 
75
- const baseDir = scope === 'user'
76
- ? join(homedir(), '.claude')
77
- : join(process.cwd(), '.claude');
78
-
79
- const projectDir = process.cwd();
80
- const scopeLabel = scope === 'user' ? 'global' : 'project';
100
+ const isGlobal = scope === 'user';
101
+ const baseDir = isGlobal ? join(homedir(), '.claude') : join(process.cwd(), '.claude');
102
+ const scopeLabel = isGlobal ? 'global' : 'project';
81
103
 
82
104
  removeFileIfExists(join(baseDir, 'CLAUDE.md'), `${scopeLabel} CLAUDE.md`);
83
- removeCiTemplates(projectDir);
84
- removeMcpServers(projectDir);
105
+ removeCiTemplates(process.cwd());
106
+ removeMcpServers(process.cwd());
85
107
 
86
- const settingsPath = join(baseDir, 'settings.json');
87
108
  console.log(`Cleaning ${scopeLabel} settings...`);
88
- removeEnvVars(settingsPath);
89
- removeStatusLine(settingsPath);
109
+ removeEnvVars(join(baseDir, 'settings.json'));
110
+ removeStatusLine(join(baseDir, 'settings.json'));
90
111
 
91
- console.log('');
92
- if (scope === 'user') {
93
- console.log('Done! store-automator uninstalled globally.');
94
- } else {
95
- console.log('Done! store-automator uninstalled from this project.');
96
- console.log('');
97
- console.log(`Note: Marketplace files remain in ${MARKETPLACE_DIR}`);
98
- console.log('Run with --global --uninstall to remove marketplace completely.');
99
- }
112
+ console.log(isGlobal
113
+ ? '\nDone! store-automator uninstalled globally.'
114
+ : `\nDone! store-automator uninstalled from this project.\n\nNote: Marketplace files remain in ${MARKETPLACE_DIR}\nRun with --global --uninstall to remove marketplace completely.`
115
+ );
100
116
  }