@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/README.md +11 -13
- package/bin/cli.mjs +69 -10
- package/package.json +7 -8
- package/plugins/store-automator/.claude-plugin/plugin.json +1 -1
- package/plugins/store-automator/agents/app-designer.md +320 -0
- package/plugins/store-automator/agents/appstore-meta-creator.md +37 -1
- package/plugins/store-automator/agents/appstore-reviewer.md +66 -5
- package/plugins/store-automator/agents/architect.md +144 -0
- package/plugins/store-automator/agents/developer.md +249 -0
- package/plugins/store-automator/agents/devops.md +396 -0
- package/plugins/store-automator/agents/product-manager.md +258 -0
- package/plugins/store-automator/agents/reviewer.md +386 -0
- package/plugins/store-automator/agents/simplifier.md +192 -0
- package/plugins/store-automator/agents/tester.md +284 -0
- package/scripts/check_changed.sh +23 -0
- package/scripts/check_google_play.py +139 -0
- package/scripts/codemagic-setup.mjs +44 -0
- package/scripts/generate.sh +107 -0
- package/scripts/manage_version_ios.py +168 -0
- package/src/codemagic-api.mjs +73 -0
- package/src/codemagic-setup.mjs +164 -0
- package/src/github-setup.mjs +52 -0
- package/src/install.mjs +32 -7
- package/src/prompt.mjs +7 -2
- package/src/templates.mjs +13 -0
- package/src/uninstall.mjs +37 -21
- package/templates/CLAUDE.md.template +298 -193
- package/templates/ci.config.yaml.template +14 -1
- package/templates/codemagic.template.yaml +15 -6
- package/templates/fastlane/android/Fastfile.template +11 -4
- package/templates/fastlane/ios/Fastfile.template +27 -11
- package/templates/fastlane/ios/Snapfile.template +3 -1
- package/templates/github/workflows/codemagic-trigger.yml +68 -0
- package/templates/scripts/create_app_record.py +172 -0
- package/templates/scripts/generate.sh +6 -0
- package/plugins/store-automator/agents/appstore-media-designer.md +0 -261
- 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/
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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(
|
|
84
|
-
removeMcpServers(
|
|
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(
|
|
89
|
-
removeStatusLine(
|
|
109
|
+
removeEnvVars(join(baseDir, 'settings.json'));
|
|
110
|
+
removeStatusLine(join(baseDir, 'settings.json'));
|
|
90
111
|
|
|
91
|
-
console.log(
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
}
|