@daemux/store-automator 0.3.0 → 0.5.1
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 +15 -5
- package/src/uninstall.mjs +37 -21
- package/templates/CLAUDE.md.template +293 -223
- 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 -195
- package/src/dependency-check.mjs +0 -26
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
default_platform(:android)
|
|
2
2
|
|
|
3
|
+
# Resolve paths absolutely to avoid symlink-relative breakage.
|
|
4
|
+
# CM_BUILD_DIR is set by Codemagic CI; locally we derive from __FILE__.
|
|
5
|
+
ROOT_DIR = ENV.fetch("CM_BUILD_DIR", File.expand_path("../..", __FILE__))
|
|
6
|
+
APP_ROOT = ENV.fetch("APP_ROOT", "app")
|
|
7
|
+
|
|
3
8
|
def metadata_changed?(path)
|
|
4
9
|
!sh("git diff --name-only HEAD~1 -- #{path}").strip.empty?
|
|
5
10
|
rescue StandardError
|
|
@@ -9,14 +14,14 @@ end
|
|
|
9
14
|
platform :android do
|
|
10
15
|
lane :deploy_android do
|
|
11
16
|
upload_to_play_store(
|
|
12
|
-
aab: "
|
|
17
|
+
aab: "#{ROOT_DIR}/#{APP_ROOT}/build/app/outputs/bundle/release/app-release.aab",
|
|
13
18
|
track: ENV.fetch("TRACK", "internal"),
|
|
14
19
|
json_key: ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH"],
|
|
15
20
|
skip_upload_metadata: !metadata_changed?("fastlane/metadata/android/"),
|
|
16
21
|
skip_upload_screenshots: !metadata_changed?("fastlane/screenshots/android/"),
|
|
17
22
|
skip_upload_images: !metadata_changed?("fastlane/screenshots/android/"),
|
|
18
23
|
skip_upload_changelogs: false,
|
|
19
|
-
metadata_path: "
|
|
24
|
+
metadata_path: "#{ROOT_DIR}/fastlane/metadata/android",
|
|
20
25
|
rollout: ENV.fetch("ROLLOUT_FRACTION", "").empty? ? nil : ENV["ROLLOUT_FRACTION"].to_f,
|
|
21
26
|
in_app_update_priority: ENV.fetch("IN_APP_UPDATE_PRIORITY", "3").to_i
|
|
22
27
|
)
|
|
@@ -26,11 +31,13 @@ platform :android do
|
|
|
26
31
|
manage_google_iap(
|
|
27
32
|
json_key: ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON_PATH"],
|
|
28
33
|
package_name: ENV["PACKAGE_NAME"],
|
|
29
|
-
config_path: "
|
|
34
|
+
config_path: "#{ROOT_DIR}/fastlane/iap_config.json"
|
|
30
35
|
)
|
|
31
36
|
end
|
|
32
37
|
|
|
33
38
|
lane :update_data_safety do
|
|
34
|
-
|
|
39
|
+
script = "#{ROOT_DIR}/scripts/update_data_safety.py"
|
|
40
|
+
csv = "#{ROOT_DIR}/fastlane/data_safety.csv"
|
|
41
|
+
sh("python3", script) if File.exist?(csv)
|
|
35
42
|
end
|
|
36
43
|
end
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
default_platform(:ios)
|
|
2
2
|
|
|
3
|
+
# Resolve paths absolutely to avoid symlink-relative breakage.
|
|
4
|
+
# CM_BUILD_DIR is set by Codemagic CI; locally we derive from __FILE__.
|
|
5
|
+
ROOT_DIR = ENV.fetch("CM_BUILD_DIR", File.expand_path("../..", __FILE__))
|
|
6
|
+
APP_ROOT = ENV.fetch("APP_ROOT", "app")
|
|
7
|
+
|
|
3
8
|
def metadata_changed?(path)
|
|
4
9
|
!sh("git diff --name-only HEAD~1 -- #{path}").strip.empty?
|
|
5
10
|
rescue StandardError
|
|
@@ -7,28 +12,39 @@ rescue StandardError
|
|
|
7
12
|
end
|
|
8
13
|
|
|
9
14
|
def asc_api_key
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
key_path = ENV["FASTLANE_API_KEY_PATH"]
|
|
16
|
+
if key_path && File.exist?(key_path)
|
|
17
|
+
app_store_connect_api_key(
|
|
18
|
+
key_id: ENV["APP_STORE_CONNECT_KEY_IDENTIFIER"],
|
|
19
|
+
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
|
|
20
|
+
key_filepath: key_path,
|
|
21
|
+
is_key_content_base64: false
|
|
22
|
+
)
|
|
23
|
+
else
|
|
24
|
+
app_store_connect_api_key(
|
|
25
|
+
key_id: ENV["APP_STORE_CONNECT_KEY_IDENTIFIER"],
|
|
26
|
+
issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
|
|
27
|
+
key_content: ENV["APP_STORE_CONNECT_PRIVATE_KEY"],
|
|
28
|
+
is_key_content_base64: false
|
|
29
|
+
)
|
|
30
|
+
end
|
|
16
31
|
end
|
|
17
32
|
|
|
18
33
|
platform :ios do
|
|
19
34
|
lane :deploy_ios do
|
|
20
35
|
deliver(
|
|
21
36
|
api_key: asc_api_key,
|
|
22
|
-
|
|
37
|
+
app_identifier: ENV["BUNDLE_ID"],
|
|
38
|
+
ipa: Dir.glob("#{ROOT_DIR}/#{APP_ROOT}/build/ios/ipa/*.ipa").first,
|
|
23
39
|
skip_metadata: !metadata_changed?("fastlane/metadata/ios/"),
|
|
24
40
|
skip_screenshots: !metadata_changed?("fastlane/screenshots/ios/"),
|
|
25
41
|
sync_screenshots: true,
|
|
26
|
-
metadata_path: "
|
|
27
|
-
screenshots_path: "
|
|
42
|
+
metadata_path: "#{ROOT_DIR}/fastlane/metadata/ios",
|
|
43
|
+
screenshots_path: "#{ROOT_DIR}/fastlane/screenshots/ios",
|
|
28
44
|
submit_for_review: ENV.fetch("SUBMIT_FOR_REVIEW", "true") == "true",
|
|
29
45
|
automatic_release: ENV.fetch("AUTOMATIC_RELEASE", "true") == "true",
|
|
30
46
|
force: true,
|
|
31
|
-
app_rating_config_path: "
|
|
47
|
+
app_rating_config_path: "#{ROOT_DIR}/fastlane/app_rating_config.json",
|
|
32
48
|
price_tier: ENV.fetch("PRICE_TIER", "0").to_i,
|
|
33
49
|
run_precheck_before_submit: true,
|
|
34
50
|
precheck_include_in_app_purchases: false,
|
|
@@ -41,7 +57,7 @@ platform :ios do
|
|
|
41
57
|
manage_iap(
|
|
42
58
|
api_key: asc_api_key,
|
|
43
59
|
app_id: ENV["BUNDLE_ID"],
|
|
44
|
-
config_path: "
|
|
60
|
+
config_path: "#{ROOT_DIR}/fastlane/iap_config.json"
|
|
45
61
|
)
|
|
46
62
|
end
|
|
47
63
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
root_dir = ENV.fetch("CM_BUILD_DIR", File.expand_path("../..", __FILE__))
|
|
2
|
+
|
|
1
3
|
devices([
|
|
2
4
|
"iPhone 16 Pro Max",
|
|
3
5
|
"iPhone 16 Pro",
|
|
@@ -21,6 +23,6 @@ languages([
|
|
|
21
23
|
])
|
|
22
24
|
|
|
23
25
|
scheme(ENV.fetch("APP_SCHEME", "Runner"))
|
|
24
|
-
output_directory("
|
|
26
|
+
output_directory("#{root_dir}/fastlane/screenshots/ios")
|
|
25
27
|
clear_previous_screenshots(false)
|
|
26
28
|
concurrent_simulators(true)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: Trigger Codemagic Builds
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
trigger:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- name: Checkout config
|
|
12
|
+
uses: actions/checkout@v4
|
|
13
|
+
with:
|
|
14
|
+
sparse-checkout: ci.config.yaml
|
|
15
|
+
sparse-checkout-cone-mode: false
|
|
16
|
+
|
|
17
|
+
- name: Install yq
|
|
18
|
+
run: |
|
|
19
|
+
if ! command -v yq &> /dev/null; then
|
|
20
|
+
sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64
|
|
21
|
+
sudo chmod +x /usr/local/bin/yq
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
- name: Read Codemagic config
|
|
25
|
+
id: config
|
|
26
|
+
run: |
|
|
27
|
+
if [ ! -f ci.config.yaml ]; then
|
|
28
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
29
|
+
echo "ci.config.yaml not found - skipping trigger."
|
|
30
|
+
exit 0
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
APP_ID=$(yq -r '.codemagic.app_id // ""' ci.config.yaml)
|
|
34
|
+
if [ -z "$APP_ID" ] || [ "$APP_ID" = "null" ]; then
|
|
35
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
36
|
+
echo "app_id not configured - skipping trigger."
|
|
37
|
+
exit 0
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
echo "app_id=$APP_ID" >> "$GITHUB_OUTPUT"
|
|
41
|
+
|
|
42
|
+
WORKFLOWS=$(yq -r '.codemagic.workflows[]' ci.config.yaml)
|
|
43
|
+
DELIMITER="EOF_$(date +%s%N)"
|
|
44
|
+
echo "workflows<<$DELIMITER" >> "$GITHUB_OUTPUT"
|
|
45
|
+
echo "$WORKFLOWS" >> "$GITHUB_OUTPUT"
|
|
46
|
+
echo "$DELIMITER" >> "$GITHUB_OUTPUT"
|
|
47
|
+
|
|
48
|
+
- name: Trigger Codemagic builds
|
|
49
|
+
if: steps.config.outputs.skip != 'true'
|
|
50
|
+
env:
|
|
51
|
+
CM_TOKEN: ${{ secrets.CM_API_TOKEN }}
|
|
52
|
+
APP_ID: ${{ steps.config.outputs.app_id }}
|
|
53
|
+
BRANCH: ${{ github.ref_name }}
|
|
54
|
+
run: |
|
|
55
|
+
echo "${{ steps.config.outputs.workflows }}" | while IFS= read -r workflow; do
|
|
56
|
+
[ -z "$workflow" ] && continue
|
|
57
|
+
if ! echo "$workflow" | grep -Eq '^[a-zA-Z0-9_-]+$'; then
|
|
58
|
+
echo "ERROR: Invalid workflow name: $workflow"
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
echo "Triggering: $workflow"
|
|
62
|
+
curl --max-time 30 --retry 2 -sf -X POST https://api.codemagic.io/builds \
|
|
63
|
+
-H "Content-Type: application/json" \
|
|
64
|
+
-H "x-auth-token: $CM_TOKEN" \
|
|
65
|
+
-d "{\"appId\": \"$APP_ID\", \"workflowId\": \"$workflow\", \"branch\": \"$BRANCH\"}" \
|
|
66
|
+
&& echo " -> success" \
|
|
67
|
+
|| echo " -> failed"
|
|
68
|
+
done
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Create an App Store Connect app record via the direct API.
|
|
4
|
+
|
|
5
|
+
Looks up the Bundle ID resource, then creates a new app record
|
|
6
|
+
with the specified name, SKU, and primary locale.
|
|
7
|
+
|
|
8
|
+
Required env vars:
|
|
9
|
+
APP_STORE_CONNECT_KEY_IDENTIFIER - Key ID from App Store Connect
|
|
10
|
+
APP_STORE_CONNECT_ISSUER_ID - Issuer ID from App Store Connect
|
|
11
|
+
APP_STORE_CONNECT_PRIVATE_KEY - Contents of the P8 key file
|
|
12
|
+
BUNDLE_ID - App bundle identifier (e.g. com.daemux.gigachat)
|
|
13
|
+
APP_NAME - Display name for the app
|
|
14
|
+
SKU - Unique SKU string for the app
|
|
15
|
+
|
|
16
|
+
Exit codes:
|
|
17
|
+
0 - App record created successfully
|
|
18
|
+
1 - Any failure (missing env vars, API error, etc.)
|
|
19
|
+
"""
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import jwt
|
|
27
|
+
import requests
|
|
28
|
+
except ImportError:
|
|
29
|
+
print("Installing dependencies...", file=sys.stderr)
|
|
30
|
+
import subprocess
|
|
31
|
+
subprocess.check_call(
|
|
32
|
+
[sys.executable, "-m", "pip", "install", "PyJWT", "cryptography", "requests"],
|
|
33
|
+
stdout=subprocess.DEVNULL,
|
|
34
|
+
)
|
|
35
|
+
import jwt
|
|
36
|
+
import requests
|
|
37
|
+
|
|
38
|
+
BASE_URL = "https://api.appstoreconnect.apple.com/v1"
|
|
39
|
+
TIMEOUT = (10, 30)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_jwt_token(key_id: str, issuer_id: str, private_key: str) -> str:
|
|
43
|
+
"""Generate a signed JWT for App Store Connect API authentication."""
|
|
44
|
+
now = int(time.time())
|
|
45
|
+
payload = {
|
|
46
|
+
"iss": issuer_id,
|
|
47
|
+
"iat": now,
|
|
48
|
+
"exp": now + 1200,
|
|
49
|
+
"aud": "appstoreconnect-v1",
|
|
50
|
+
}
|
|
51
|
+
return jwt.encode(
|
|
52
|
+
payload, private_key, algorithm="ES256", headers={"kid": key_id}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def lookup_bundle_id_resource(headers: dict, bundle_id: str) -> str:
|
|
57
|
+
"""Look up the Bundle ID resource ID for the given identifier."""
|
|
58
|
+
resp = requests.get(
|
|
59
|
+
f"{BASE_URL}/bundleIds",
|
|
60
|
+
params={"filter[identifier]": bundle_id},
|
|
61
|
+
headers=headers,
|
|
62
|
+
timeout=TIMEOUT,
|
|
63
|
+
)
|
|
64
|
+
resp.raise_for_status()
|
|
65
|
+
data = resp.json().get("data", [])
|
|
66
|
+
if not data:
|
|
67
|
+
print(
|
|
68
|
+
f"ERROR: No Bundle ID found for identifier '{bundle_id}'. "
|
|
69
|
+
"Register it in App Store Connect > Certificates, Identifiers & Profiles first.",
|
|
70
|
+
file=sys.stderr,
|
|
71
|
+
)
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
return data[0]["id"]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def create_app_record(
|
|
77
|
+
headers: dict,
|
|
78
|
+
bundle_id: str,
|
|
79
|
+
bundle_id_resource_id: str,
|
|
80
|
+
app_name: str,
|
|
81
|
+
sku: str,
|
|
82
|
+
) -> dict:
|
|
83
|
+
"""Create a new app record in App Store Connect."""
|
|
84
|
+
payload = {
|
|
85
|
+
"data": {
|
|
86
|
+
"type": "apps",
|
|
87
|
+
"attributes": {
|
|
88
|
+
"bundleId": bundle_id,
|
|
89
|
+
"name": app_name,
|
|
90
|
+
"sku": sku,
|
|
91
|
+
"primaryLocale": "en-US",
|
|
92
|
+
},
|
|
93
|
+
"relationships": {
|
|
94
|
+
"bundleId": {
|
|
95
|
+
"data": {
|
|
96
|
+
"type": "bundleIds",
|
|
97
|
+
"id": bundle_id_resource_id,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
resp = requests.post(
|
|
104
|
+
f"{BASE_URL}/apps",
|
|
105
|
+
json=payload,
|
|
106
|
+
headers=headers,
|
|
107
|
+
timeout=TIMEOUT,
|
|
108
|
+
)
|
|
109
|
+
if resp.status_code == 409:
|
|
110
|
+
print(
|
|
111
|
+
f"ERROR: An app with bundle ID '{bundle_id}' already exists.",
|
|
112
|
+
file=sys.stderr,
|
|
113
|
+
)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
if not resp.ok:
|
|
116
|
+
errors = resp.json().get("errors", [])
|
|
117
|
+
for err in errors:
|
|
118
|
+
print(f"ERROR: {err.get('detail', err.get('title', 'Unknown'))}", file=sys.stderr)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
return resp.json()["data"]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def main() -> None:
|
|
124
|
+
key_id = os.environ.get("APP_STORE_CONNECT_KEY_IDENTIFIER", "")
|
|
125
|
+
issuer_id = os.environ.get("APP_STORE_CONNECT_ISSUER_ID", "")
|
|
126
|
+
private_key = os.environ.get("APP_STORE_CONNECT_PRIVATE_KEY", "")
|
|
127
|
+
bundle_id = os.environ.get("BUNDLE_ID", "")
|
|
128
|
+
app_name = os.environ.get("APP_NAME", "")
|
|
129
|
+
sku = os.environ.get("SKU", "")
|
|
130
|
+
|
|
131
|
+
missing = []
|
|
132
|
+
if not key_id:
|
|
133
|
+
missing.append("APP_STORE_CONNECT_KEY_IDENTIFIER")
|
|
134
|
+
if not issuer_id:
|
|
135
|
+
missing.append("APP_STORE_CONNECT_ISSUER_ID")
|
|
136
|
+
if not private_key:
|
|
137
|
+
missing.append("APP_STORE_CONNECT_PRIVATE_KEY")
|
|
138
|
+
if not bundle_id:
|
|
139
|
+
missing.append("BUNDLE_ID")
|
|
140
|
+
if not app_name:
|
|
141
|
+
missing.append("APP_NAME")
|
|
142
|
+
if not sku:
|
|
143
|
+
missing.append("SKU")
|
|
144
|
+
|
|
145
|
+
if missing:
|
|
146
|
+
print(f"ERROR: Missing required environment variables: {', '.join(missing)}", file=sys.stderr)
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
token = get_jwt_token(key_id, issuer_id, private_key)
|
|
150
|
+
headers = {
|
|
151
|
+
"Authorization": f"Bearer {token}",
|
|
152
|
+
"Content-Type": "application/json",
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
print(f"Looking up Bundle ID resource for '{bundle_id}'...")
|
|
156
|
+
bundle_id_resource_id = lookup_bundle_id_resource(headers, bundle_id)
|
|
157
|
+
print(f"Found Bundle ID resource: {bundle_id_resource_id}")
|
|
158
|
+
|
|
159
|
+
print(f"Creating app record '{app_name}' (SKU: {sku})...")
|
|
160
|
+
app_data = create_app_record(headers, bundle_id, bundle_id_resource_id, app_name, sku)
|
|
161
|
+
|
|
162
|
+
result = {
|
|
163
|
+
"app_id": app_data["id"],
|
|
164
|
+
"name": app_data["attributes"]["name"],
|
|
165
|
+
"bundle_id": app_data["attributes"]["bundleId"],
|
|
166
|
+
"sku": app_data["attributes"]["sku"],
|
|
167
|
+
}
|
|
168
|
+
print(f"App record created successfully: {json.dumps(result)}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main()
|
|
@@ -54,6 +54,9 @@ PRICE_TIER=$(yq -r '.ios.price_tier' "$CONFIG")
|
|
|
54
54
|
SUBMIT_REVIEW=$(yq -r '.ios.submit_for_review' "$CONFIG")
|
|
55
55
|
AUTO_RELEASE=$(yq -r '.ios.automatic_release' "$CONFIG")
|
|
56
56
|
|
|
57
|
+
# Read flutter_root (defaults to "." if not set)
|
|
58
|
+
APP_ROOT=$(yq -r '.flutter_root // "."' "$CONFIG")
|
|
59
|
+
|
|
57
60
|
# Read credentials from ci.config.yaml
|
|
58
61
|
P8_KEY_PATH=$(yq -r '.credentials.apple.p8_key_path' "$CONFIG")
|
|
59
62
|
APPLE_KEY_ID=$(yq -r '.credentials.apple.key_id' "$CONFIG")
|
|
@@ -72,6 +75,7 @@ validate_value "ios.primary_category" "$PRIMARY_CAT"
|
|
|
72
75
|
validate_value "ios.secondary_category" "$SECONDARY_CAT"
|
|
73
76
|
validate_value "credentials.apple.key_id" "$APPLE_KEY_ID"
|
|
74
77
|
validate_value "credentials.apple.issuer_id" "$APPLE_ISSUER_ID"
|
|
78
|
+
validate_value "flutter_root" "$APP_ROOT"
|
|
75
79
|
|
|
76
80
|
# Generate codemagic.yaml from template
|
|
77
81
|
sed \
|
|
@@ -93,9 +97,11 @@ sed \
|
|
|
93
97
|
-e "s|\${APPLE_ISSUER_ID}|$APPLE_ISSUER_ID|g" \
|
|
94
98
|
-e "s|\${GOOGLE_SA_JSON_PATH}|$GOOGLE_SA_JSON_PATH|g" \
|
|
95
99
|
-e "s|\${KEYSTORE_PASSWORD}|$KEYSTORE_PASSWORD|g" \
|
|
100
|
+
-e "s|\${APP_ROOT}|$APP_ROOT|g" \
|
|
96
101
|
"$TEMPLATE" > codemagic.yaml
|
|
97
102
|
|
|
98
103
|
echo "Generated codemagic.yaml for $APP_NAME ($BUNDLE_ID)"
|
|
99
104
|
echo " iOS: bundle_id=$BUNDLE_ID, category=$PRIMARY_CAT"
|
|
100
105
|
echo " Android: package=$PACKAGE_NAME, track=$TRACK"
|
|
106
|
+
echo " Flutter: root=$APP_ROOT"
|
|
101
107
|
echo " Credentials: P8=$P8_KEY_PATH, SA=$GOOGLE_SA_JSON_PATH"
|
|
@@ -1,195 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: appstore-media-designer
|
|
3
|
-
description: "Creates ASO-optimized app store screenshots for Apple App Store and Google Play. All designs created in Stitch MCP. Researches competitors for inspiration. 5 screenshots per device, all sizes."
|
|
4
|
-
model: opus
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
You are a senior app store creative designer and ASO (App Store Optimization) specialist. You create high-converting, guideline-compliant screenshots that maximize organic downloads from store search results.
|
|
8
|
-
|
|
9
|
-
## Critical ASO Context
|
|
10
|
-
|
|
11
|
-
Screenshots are the #1 conversion factor in app store search results. Users see screenshots as **small thumbnails** in search results, NOT full-size. Your designs MUST convert at thumbnail size:
|
|
12
|
-
|
|
13
|
-
- **Headlines must be BIG** — max 2 lines, large bold text readable at thumbnail size
|
|
14
|
-
- **Short, selling copy** — benefit-focused, not feature descriptions
|
|
15
|
-
- **Visual clarity** — clean layouts that communicate instantly, no clutter
|
|
16
|
-
- **First screenshot is everything** — 80% of users decide from the first screenshot alone
|
|
17
|
-
|
|
18
|
-
## Workflow
|
|
19
|
-
|
|
20
|
-
1. READ the app source code (lib/ directory) to understand all screens and features
|
|
21
|
-
2. READ any existing Stitch designs in the same project (the app design was created here first)
|
|
22
|
-
3. READ ci.config.yaml for app identity and branding info
|
|
23
|
-
4. **RESEARCH competitors** — search for the biggest competitors in the same app category, study their screenshot strategies, note what works (headlines, layouts, colors)
|
|
24
|
-
5. PLAN 5 screenshot scenes optimized for ASO conversion
|
|
25
|
-
6. USE Stitch MCP to create ALL screenshots in the **same Stitch project** as the app design
|
|
26
|
-
7. EXPORT and SAVE screenshots to fastlane/screenshots/ in the correct directory structure
|
|
27
|
-
8. Verify all required sizes, formats, and file names are present
|
|
28
|
-
|
|
29
|
-
## Competitor Research (MANDATORY)
|
|
30
|
-
|
|
31
|
-
Before designing screenshots, research the top 5-10 competitors in your app's category:
|
|
32
|
-
|
|
33
|
-
1. Use web search to find the top apps in the category on both App Store and Google Play
|
|
34
|
-
2. Study their screenshot strategies: headline styles, colors, layouts, number of screenshots
|
|
35
|
-
3. Note common patterns that successful apps use
|
|
36
|
-
4. Identify opportunities to differentiate while following proven patterns
|
|
37
|
-
5. Document findings briefly before starting design
|
|
38
|
-
|
|
39
|
-
## Screenshot Strategy: 5 Scenes
|
|
40
|
-
|
|
41
|
-
For every app, create exactly 5 screenshot scenes:
|
|
42
|
-
|
|
43
|
-
| Scene | Purpose | Headline Strategy |
|
|
44
|
-
|-------|---------|-------------------|
|
|
45
|
-
| 01_hero | Most impressive feature/screen — this is the MONEY SHOT | Bold value proposition, max 5 words, answers "what does this app do?" |
|
|
46
|
-
| 02_feature1 | Primary feature in action | Benefit headline: what the user GETS |
|
|
47
|
-
| 03_feature2 | Secondary differentiating feature | What makes this app DIFFERENT |
|
|
48
|
-
| 04_social | Social proof, results, or key metric | Trust/credibility headline |
|
|
49
|
-
| 05_settings | Customization, extras, or final CTA | "And more..." or urgency headline |
|
|
50
|
-
|
|
51
|
-
### Headline Rules (CRITICAL for ASO)
|
|
52
|
-
|
|
53
|
-
- **MAX 2 lines of text** — never more
|
|
54
|
-
- **BIG font size** — must be readable when the screenshot is thumbnail-sized in search results
|
|
55
|
-
- **Short selling text** — 3-6 words per headline, not feature descriptions
|
|
56
|
-
- **Action/benefit words** — "Unlock", "Transform", "Discover", "Create", "Save", "Get"
|
|
57
|
-
- **No filler words** — every word must earn its place
|
|
58
|
-
- Examples of GOOD headlines: "Chat Smarter, Not Harder", "Your AI Assistant", "Unlimited Creativity"
|
|
59
|
-
- Examples of BAD headlines: "Advanced AI-powered conversational interface with real-time responses"
|
|
60
|
-
|
|
61
|
-
### Scene Design Rules
|
|
62
|
-
|
|
63
|
-
- Headlines placed at TOP of the screenshot — big, bold, high contrast
|
|
64
|
-
- Background: solid color or gradient from the app's color palette
|
|
65
|
-
- App screen mockup placed centrally, occupying 55-65% of the image area
|
|
66
|
-
- Device frame is OPTIONAL — frameless looks more modern and gives more screen space
|
|
67
|
-
- Consistent typography and color scheme across all 5 scenes
|
|
68
|
-
- The app UI shown must represent realistic app content
|
|
69
|
-
- Clean, modern, minimal style — Apple/Google design quality
|
|
70
|
-
|
|
71
|
-
## All Screenshots Created in Stitch MCP
|
|
72
|
-
|
|
73
|
-
**MANDATORY: ALL screenshots are designed entirely in Stitch MCP. No simulator screenshots, no mobile-mcp, no external tools.**
|
|
74
|
-
|
|
75
|
-
### Design Process
|
|
76
|
-
|
|
77
|
-
1. **Use the existing Stitch project** — screenshots go in the SAME project where the app design was created
|
|
78
|
-
2. For each of the 5 scenes, create a design in Stitch MCP with a detailed prompt
|
|
79
|
-
3. Generate at EVERY required device dimension (see sizes below)
|
|
80
|
-
4. Export each design as PNG and save to the correct directory path
|
|
81
|
-
|
|
82
|
-
### Stitch Design Prompt Template
|
|
83
|
-
|
|
84
|
-
For each scene, use a detailed prompt like:
|
|
85
|
-
|
|
86
|
-
```
|
|
87
|
-
App store screenshot for a [app category] app called "[App Name]".
|
|
88
|
-
|
|
89
|
-
LAYOUT:
|
|
90
|
-
- Top 30%: Large headline "[HEADLINE TEXT]" in bold [font], [color] text, left-aligned or centered
|
|
91
|
-
- Optional small subheadline below in lighter weight
|
|
92
|
-
- Center/bottom 65%: [Device type] showing the app's [specific screen] with [describe UI content in detail]
|
|
93
|
-
- Background: [gradient/solid color matching app theme]
|
|
94
|
-
|
|
95
|
-
STYLE:
|
|
96
|
-
- Clean, modern, minimal — premium App Store quality
|
|
97
|
-
- No device frame / thin device frame (choose one)
|
|
98
|
-
- High contrast between text and background
|
|
99
|
-
- [App name]'s color palette: primary [#hex], accent [#hex], background [#hex]
|
|
100
|
-
|
|
101
|
-
DIMENSIONS: [width] x [height] pixels
|
|
102
|
-
FORMAT: PNG, RGB color space
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
### Device Sizes to Generate
|
|
106
|
-
|
|
107
|
-
For EACH of the 5 scenes, generate at ALL these sizes:
|
|
108
|
-
|
|
109
|
-
**Apple App Store (required):**
|
|
110
|
-
- iPhone 6.7": 1290 x 2796 px
|
|
111
|
-
- iPad Pro 12.9": 2048 x 2732 px
|
|
112
|
-
- iPad Pro 13": 2064 x 2752 px
|
|
113
|
-
|
|
114
|
-
**Google Play (required):**
|
|
115
|
-
- Phone: 1080 x 1920 px
|
|
116
|
-
- 7" Tablet: 1200 x 1920 px
|
|
117
|
-
- 10" Tablet: 1920 x 1200 px (landscape)
|
|
118
|
-
|
|
119
|
-
**Google Play extras (required):**
|
|
120
|
-
- Feature Graphic: 1024 x 500 px (landscape banner — app name + tagline + brand colors)
|
|
121
|
-
- Icon: 512 x 512 px
|
|
122
|
-
|
|
123
|
-
## Directory Structure
|
|
124
|
-
|
|
125
|
-
Save all exported screenshots to:
|
|
126
|
-
|
|
127
|
-
### iOS
|
|
128
|
-
```
|
|
129
|
-
fastlane/screenshots/ios/
|
|
130
|
-
en-US/
|
|
131
|
-
iPhone 6.7/
|
|
132
|
-
01_hero.png, 02_feature1.png, 03_feature2.png, 04_social.png, 05_settings.png
|
|
133
|
-
iPad Pro 12.9/
|
|
134
|
-
01_hero.png, 02_feature1.png, 03_feature2.png, 04_social.png, 05_settings.png
|
|
135
|
-
iPad Pro 13/
|
|
136
|
-
01_hero.png, 02_feature1.png, 03_feature2.png, 04_social.png, 05_settings.png
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### Android
|
|
140
|
-
```
|
|
141
|
-
fastlane/screenshots/android/
|
|
142
|
-
en-US/
|
|
143
|
-
phoneScreenshots/
|
|
144
|
-
01_hero.png, 02_feature1.png, 03_feature2.png, 04_social.png, 05_settings.png
|
|
145
|
-
sevenInchScreenshots/
|
|
146
|
-
01_hero.png, 02_feature1.png, 03_feature2.png, 04_social.png, 05_settings.png
|
|
147
|
-
tenInchScreenshots/
|
|
148
|
-
01_hero.png, 02_feature1.png, 03_feature2.png, 04_social.png, 05_settings.png
|
|
149
|
-
featureGraphic.png
|
|
150
|
-
icon.png
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
## Apple App Store Rules
|
|
154
|
-
- Must show app UI (Stitch-designed screens representing real app features)
|
|
155
|
-
- No photographs of people holding physical devices
|
|
156
|
-
- Format: .png only
|
|
157
|
-
- Max 10 per device class per locale
|
|
158
|
-
- Text overlays allowed, app UI must be prominent
|
|
159
|
-
- No misleading content
|
|
160
|
-
- Portrait orientation
|
|
161
|
-
|
|
162
|
-
## Google Play Rules
|
|
163
|
-
- Screenshots must accurately depict the app experience
|
|
164
|
-
- Device frames optional
|
|
165
|
-
- Feature graphic: landscape 1024x500, displayed at top of store listing
|
|
166
|
-
- Text must be readable
|
|
167
|
-
- No excessive text overlaying the UI
|
|
168
|
-
|
|
169
|
-
## Fallback: If Stitch MCP is unavailable
|
|
170
|
-
|
|
171
|
-
Only if Stitch MCP tools are not available, save a design specification to `fastlane/screenshots/design-spec.json` with brand colors, scene descriptions, and headline text for each scene. This spec can be used to create screenshots manually.
|
|
172
|
-
|
|
173
|
-
## Output Verification Checklist
|
|
174
|
-
|
|
175
|
-
After creating all screenshots, verify:
|
|
176
|
-
|
|
177
|
-
- [ ] iPhone 6.7" — 5 screenshots at 1290x2796, .png
|
|
178
|
-
- [ ] iPad Pro 12.9" — 5 screenshots at 2048x2732, .png
|
|
179
|
-
- [ ] iPad Pro 13" — 5 screenshots at 2064x2752, .png
|
|
180
|
-
- [ ] Android phone — 5 screenshots at 1080x1920, .png
|
|
181
|
-
- [ ] Android 7" tablet — 5 screenshots at 1200x1920, .png
|
|
182
|
-
- [ ] Android 10" tablet — 5 screenshots at 1920x1200, .png
|
|
183
|
-
- [ ] Feature graphic — 1024x500, .png
|
|
184
|
-
- [ ] Icon — 512x512, .png
|
|
185
|
-
- [ ] Headlines are BIG and readable at thumbnail size
|
|
186
|
-
- [ ] Max 2 lines of headline text per screenshot
|
|
187
|
-
- [ ] Consistent color scheme and typography across all scenes
|
|
188
|
-
- [ ] App UI is prominent and represents the actual app
|
|
189
|
-
- [ ] No photographs of people holding physical devices
|
|
190
|
-
|
|
191
|
-
## Output Footer
|
|
192
|
-
|
|
193
|
-
```
|
|
194
|
-
NEXT: appstore-reviewer to verify screenshot compliance.
|
|
195
|
-
```
|
package/src/dependency-check.mjs
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { exec } from './utils.mjs';
|
|
2
|
-
|
|
3
|
-
export function isClaudePluginInstalled() {
|
|
4
|
-
const result = exec('npm ls -g @daemux/claude-plugin --depth=0');
|
|
5
|
-
return result !== null && result.includes('@daemux/claude-plugin');
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function installClaudePlugin() {
|
|
9
|
-
console.log('Installing @daemux/claude-plugin globally...');
|
|
10
|
-
const result = exec('npm install -g @daemux/claude-plugin');
|
|
11
|
-
if (result === null) {
|
|
12
|
-
console.log('Warning: could not install @daemux/claude-plugin globally.');
|
|
13
|
-
console.log('Run manually: npm install -g @daemux/claude-plugin');
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
console.log('@daemux/claude-plugin installed successfully.');
|
|
17
|
-
return true;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function ensureClaudePlugin() {
|
|
21
|
-
if (isClaudePluginInstalled()) {
|
|
22
|
-
console.log('@daemux/claude-plugin is already installed globally.');
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
return installClaudePlugin();
|
|
26
|
-
}
|