sdk-release-kit 0.2.0__py3-none-any.whl

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 (39) hide show
  1. sdk_release_kit/__init__.py +3 -0
  2. sdk_release_kit/_data/examples/custom.example.yaml +18 -0
  3. sdk_release_kit/_data/examples/keys.example.yaml +18 -0
  4. sdk_release_kit/_data/examples/outputs.example.yaml +19 -0
  5. sdk_release_kit/_data/examples/repos.example.yaml +33 -0
  6. sdk_release_kit/_data/recipes/sdks.yaml +222 -0
  7. sdk_release_kit/_data/web/app.js +305 -0
  8. sdk_release_kit/_data/web/index.html +142 -0
  9. sdk_release_kit/_data/web/styles.css +93 -0
  10. sdk_release_kit/builders/__init__.py +24 -0
  11. sdk_release_kit/builders/android.py +86 -0
  12. sdk_release_kit/builders/base.py +217 -0
  13. sdk_release_kit/builders/flutter.py +16 -0
  14. sdk_release_kit/builders/generic.py +56 -0
  15. sdk_release_kit/builders/ios.py +204 -0
  16. sdk_release_kit/builders/reactnative.py +32 -0
  17. sdk_release_kit/cli.py +198 -0
  18. sdk_release_kit/mcp_server.py +240 -0
  19. sdk_release_kit/models.py +306 -0
  20. sdk_release_kit/paths.py +85 -0
  21. sdk_release_kit/pipeline.py +446 -0
  22. sdk_release_kit/recipes.py +110 -0
  23. sdk_release_kit/runlog.py +62 -0
  24. sdk_release_kit/server.py +331 -0
  25. sdk_release_kit/store/__init__.py +29 -0
  26. sdk_release_kit/store/_yaml.py +46 -0
  27. sdk_release_kit/store/custom.py +34 -0
  28. sdk_release_kit/store/keys.py +111 -0
  29. sdk_release_kit/store/outputs.py +15 -0
  30. sdk_release_kit/store/repos.py +15 -0
  31. sdk_release_kit/uploaders/__init__.py +20 -0
  32. sdk_release_kit/uploaders/onedrive.py +129 -0
  33. sdk_release_kit/uploaders/play.py +108 -0
  34. sdk_release_kit/uploaders/testflight.py +105 -0
  35. sdk_release_kit-0.2.0.dist-info/METADATA +307 -0
  36. sdk_release_kit-0.2.0.dist-info/RECORD +39 -0
  37. sdk_release_kit-0.2.0.dist-info/WHEEL +4 -0
  38. sdk_release_kit-0.2.0.dist-info/entry_points.txt +2 -0
  39. sdk_release_kit-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """SDK Release Kit: build mobile SDK demo apps and packages, then publish them."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,18 @@
1
+ # Custom data: free-form YAML. Recipes read it through ${custom.some.key}.
2
+ # Unknown keys are kept as written, so teams and AI agents can add their own.
3
+
4
+ # Apple Developer team used for archive and export (leave empty for automatic).
5
+ ios_team_id: ""
6
+
7
+ # Store identifiers of the demo apps, used for Play and TestFlight uploads.
8
+ app_ids:
9
+ android_native: com.example.paymentsdk.demo
10
+ ios_native: com.example.paymentsdk.demoapp
11
+ flutter_android: com.example.paymentsdk.flutter.demoapp
12
+ flutter_ios: com.example.paymentsdk.flutter.demoapp
13
+ rn_android: com.example.paymentsdk.reactnative.demoapp
14
+ rn_ios: com.example.paymentsdk.reactnative.demoapp
15
+
16
+ # Anything else your team wants to keep with the tool, for example:
17
+ # notify:
18
+ # slack_webhook_env: SLACK_WEBHOOK_URL
@@ -0,0 +1,18 @@
1
+ # Keys: PATHS to key files and REFERENCES to secrets. Never put a password in this file.
2
+ # A secret reference is {source: env|keychain|none, name: ...}.
3
+ # env: read from the environment variable `name`
4
+ # keychain: read from the OS keychain, service "sdk-release-kit", account `name`
5
+ # (set with `sdk-release-kit keys set <name>` or the Keys page)
6
+ schema_version: 1
7
+ android:
8
+ keystore_path: ~/path/to/release.keystore
9
+ key_alias: release
10
+ store_password: {source: keychain, name: android.store_password}
11
+ key_password: {source: keychain, name: android.key_password}
12
+ inject_signing: true
13
+ play:
14
+ json_key_path: ~/path/to/play-service-account.json
15
+ testflight:
16
+ key_id: {source: env, name: ASC_KEY_ID}
17
+ issuer_id: {source: env, name: ASC_ISSUER_ID}
18
+ key_path: ~/path/to/AuthKey_XXXXXXXXXX.p8
@@ -0,0 +1,19 @@
1
+ # Outputs: where artifacts go. Not committed anywhere.
2
+ schema_version: 1
3
+ staging: ~/sdk-release-kit/staging
4
+ onedrive:
5
+ # Any locally synced folder works (OneDrive, Google Drive, Dropbox, an SMB mount).
6
+ enabled: false
7
+ root: ~/path/to/OneDrive/Mobile SDK Builds
8
+ demos_android: android
9
+ demos_ios: ios
10
+ packages: packages
11
+ copy_untargeted: false
12
+ play:
13
+ enabled: false
14
+ track: internal
15
+ release_status: completed # use "draft" for an app that has never been published
16
+ method: auto # auto | api | fastlane
17
+ testflight:
18
+ enabled: false
19
+ method: altool # altool | fastlane
@@ -0,0 +1,33 @@
1
+ # Repositories: recipe id -> checkout on THIS machine. Not committed anywhere.
2
+ # Copied to <settings dir>/repos.yaml by `sdk-release-kit init`.
3
+ schema_version: 1
4
+ repos:
5
+ android-current:
6
+ path: ~/path/to/payment-sdk-android
7
+ enabled: true
8
+ android-legacy:
9
+ path: ~/path/to/payment-sdk-android-legacy
10
+ enabled: false
11
+ ios-current:
12
+ path: ~/path/to/payment-sdk-ios
13
+ enabled: false
14
+ ios-legacy:
15
+ path: ~/path/to/payment-sdk-ios-legacy
16
+ enabled: false
17
+ flutter:
18
+ path: ~/path/to/payment-sdk-flutter
19
+ enabled: false
20
+ reactnative-current:
21
+ path: ~/path/to/payment-sdk-react-native
22
+ enabled: false
23
+ reactnative-legacy:
24
+ path: ~/path/to/payment-sdk-react-native-legacy
25
+ enabled: false
26
+ tooling:
27
+ # Android Studio's bundled JDK on macOS is usually:
28
+ # /Applications/Android Studio.app/Contents/jbr/Contents/Home
29
+ java_home: null
30
+ android_sdk: ~/Library/Android/sdk
31
+ flutter_bin: null # e.g. ~/path/to/flutter/bin
32
+ developer_dir: null # e.g. /Applications/Xcode.app/Contents/Developer
33
+ extra_path: []
@@ -0,0 +1,222 @@
1
+ # Portable build recipes. Shipped in git.
2
+ #
3
+ # Rules (see AGENTS.md):
4
+ # - Paths here are RELATIVE to the SDK checkout. Absolute paths belong in user settings.
5
+ # - No secrets. Identifiers that differ per team come from Custom data via ${custom.key:-default}.
6
+ # - Adjust workdirs, workspaces and schemes to your repositories, or override them per machine
7
+ # with a file in <settings dir>/recipes.d/ (deep-merged over this file).
8
+ #
9
+ # Step types: run, gradle, flutter, xcode-simulator, xcode-ipa, xcframework, npm-pack, source-zip.
10
+ # Optional on any step: name, workdir, env, when: [onedrive|play|testflight], host: any|macos,
11
+ # collect: [{glob, kind, target, root, required}].
12
+
13
+ schema_version: 1
14
+
15
+ # Shared-folder targets for demo apps: <onedrive root>/<outputs.onedrive[folder]>/<name>
16
+ onedrive_targets:
17
+ android-native-current: {folder: demos_android, name: native-current.apk}
18
+ android-native-legacy: {folder: demos_android, name: native-legacy.apk}
19
+ android-flutter: {folder: demos_android, name: flutter.apk}
20
+ android-rn-current: {folder: demos_android, name: rn-current.apk}
21
+ android-rn-legacy: {folder: demos_android, name: rn-legacy.apk}
22
+ ios-native-current: {folder: demos_ios, name: native-current.app}
23
+ ios-native-legacy: {folder: demos_ios, name: native-legacy.app}
24
+ ios-flutter: {folder: demos_ios, name: flutter.app}
25
+ ios-rn-current: {folder: demos_ios, name: rn-current.app}
26
+ ios-rn-legacy: {folder: demos_ios, name: rn-legacy.app}
27
+
28
+ recipes:
29
+ # ------------------------------------------------------------------------------- Android
30
+ android-current:
31
+ platform: android
32
+ label: Android native SDK (current)
33
+ version: {file: gradle.properties, regex: '^\s*VERSION_NAME\s*=\s*(\S+)'}
34
+ store:
35
+ play: true
36
+ android_package: "${custom.app_ids.android_native:-com.example.paymentsdk.demo}"
37
+ demos:
38
+ - type: gradle
39
+ name: demo APK + AAB
40
+ tasks: [":app:assembleRelease", ":app:bundleRelease"]
41
+ collect:
42
+ - {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-native-current}
43
+ - {glob: "app/build/outputs/bundle/release/*.aab", kind: aab}
44
+ packages:
45
+ - type: gradle
46
+ name: AARs to Maven local
47
+ tasks: [publishToMavenLocal]
48
+ sign: false
49
+ collect:
50
+ - {glob: "*/build/outputs/aar/*-release.aar", kind: aar}
51
+
52
+ android-legacy:
53
+ platform: android
54
+ label: Android native SDK (legacy)
55
+ version: {file: gradle.properties, regex: '^\s*VERSION_NAME\s*=\s*(\S+)'}
56
+ store:
57
+ play: false # same applicationId as android-current: shared folder only
58
+ android_package: "${custom.app_ids.android_native:-com.example.paymentsdk.demo}"
59
+ demos:
60
+ - type: gradle
61
+ name: demo APK + AAB
62
+ tasks: [":app:assembleRelease", ":app:bundleRelease"]
63
+ collect:
64
+ - {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-native-legacy}
65
+ - {glob: "app/build/outputs/bundle/release/*.aab", kind: aab}
66
+ packages:
67
+ - type: gradle
68
+ name: AARs to Maven local
69
+ tasks: [publishToMavenLocal]
70
+ sign: false
71
+ collect:
72
+ - {glob: "*/build/outputs/aar/*-release.aar", kind: aar}
73
+
74
+ # ----------------------------------------------------------------------------------- iOS
75
+ ios-current:
76
+ platform: ios
77
+ label: iOS native SDK (current)
78
+ store:
79
+ testflight: true
80
+ ios_bundle_id: "${custom.app_ids.ios_native:-com.example.paymentsdk.demoapp}"
81
+ demos:
82
+ - type: xcode-simulator
83
+ name: simulator .app
84
+ workspace: DemoApp/DemoApp.xcworkspace
85
+ scheme: DemoApp
86
+ pod_install: false
87
+ target: ios-native-current
88
+ - type: xcode-ipa
89
+ name: archive + IPA for TestFlight
90
+ when: [testflight]
91
+ workspace: DemoApp/DemoApp.xcworkspace
92
+ scheme: DemoApp
93
+ team_id: "${custom.ios_team_id:-}"
94
+ packages:
95
+ - type: xcframework
96
+ workspace: DemoApp/DemoApp.xcworkspace
97
+ scheme: PaymentSDK
98
+ framework_name: PaymentSDK
99
+
100
+ ios-legacy:
101
+ platform: ios
102
+ label: iOS native SDK (legacy)
103
+ store:
104
+ testflight: false # same bundle id as ios-current: shared folder only
105
+ ios_bundle_id: "${custom.app_ids.ios_native:-com.example.paymentsdk.demoapp}"
106
+ demos:
107
+ - type: xcode-simulator
108
+ name: simulator .app
109
+ workspace: DemoApp/DemoApp.xcworkspace
110
+ scheme: DemoApp
111
+ target: ios-native-legacy
112
+ packages:
113
+ - type: xcframework
114
+ workspace: DemoApp/DemoApp.xcworkspace
115
+ scheme: PaymentSDK
116
+ framework_name: PaymentSDK
117
+
118
+ # ------------------------------------------------------------------------------- Flutter
119
+ flutter:
120
+ platform: flutter
121
+ label: Flutter plugin
122
+ version: {file: pubspec.yaml, regex: '^version:\s*(\S+)'}
123
+ store:
124
+ play: true
125
+ testflight: true
126
+ android_package: "${custom.app_ids.flutter_android:-com.example.paymentsdk.flutter.demoapp}"
127
+ ios_bundle_id: "${custom.app_ids.flutter_ios:-com.example.paymentsdk.flutter.demoapp}"
128
+ demos:
129
+ - {type: flutter, name: pub get, workdir: example, args: [pub, get]}
130
+ - type: flutter
131
+ name: demo APK
132
+ workdir: example
133
+ args: [build, apk, --release]
134
+ collect:
135
+ - {glob: "build/app/outputs/flutter-apk/app-release.apk", kind: apk, target: android-flutter}
136
+ - type: flutter
137
+ name: demo AAB
138
+ workdir: example
139
+ when: [play]
140
+ args: [build, appbundle, --release]
141
+ collect:
142
+ - {glob: "build/app/outputs/bundle/release/*.aab", kind: aab}
143
+ - type: flutter
144
+ name: simulator .app
145
+ workdir: example
146
+ host: macos
147
+ args: [build, ios, --simulator, --debug]
148
+ collect:
149
+ - {glob: "build/ios/iphonesimulator/Runner.app", kind: app, target: ios-flutter}
150
+ - type: flutter
151
+ name: IPA for TestFlight
152
+ workdir: example
153
+ host: macos
154
+ when: [testflight]
155
+ args: [build, ipa, --release, --export-method, app-store]
156
+ collect:
157
+ - {glob: "build/ios/ipa/*.ipa", kind: ipa}
158
+ packages:
159
+ - {type: source-zip, name: package sources}
160
+
161
+ # -------------------------------------------------------------------------- React Native
162
+ reactnative-current:
163
+ platform: reactnative
164
+ label: React Native SDK (current)
165
+ version: {file: package.json, regex: '"version"\s*:\s*"([^"]+)"'}
166
+ store:
167
+ play: true
168
+ testflight: true
169
+ android_package: "${custom.app_ids.rn_android:-com.example.paymentsdk.reactnative.demoapp}"
170
+ ios_bundle_id: "${custom.app_ids.rn_ios:-com.example.paymentsdk.reactnative.demoapp}"
171
+ demos:
172
+ - {type: run, name: install demo deps, workdir: SimpleIntegration, cmd: [npm, install]}
173
+ - type: gradle
174
+ name: demo APK + AAB
175
+ workdir: SimpleIntegration/android
176
+ tasks: [assembleRelease, bundleRelease]
177
+ collect:
178
+ - {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-rn-current}
179
+ - {glob: "app/build/outputs/bundle/release/*.aab", kind: aab}
180
+ - type: xcode-simulator
181
+ name: simulator .app
182
+ workdir: SimpleIntegration/ios
183
+ workspace: SimpleIntegration.xcworkspace
184
+ scheme: SimpleIntegration
185
+ pod_install: true
186
+ target: ios-rn-current
187
+ - type: xcode-ipa
188
+ name: archive + IPA for TestFlight
189
+ when: [testflight]
190
+ workdir: SimpleIntegration/ios
191
+ workspace: SimpleIntegration.xcworkspace
192
+ scheme: SimpleIntegration
193
+ team_id: "${custom.ios_team_id:-}"
194
+ packages:
195
+ - {type: npm-pack, name: npm pack}
196
+
197
+ reactnative-legacy:
198
+ platform: reactnative
199
+ label: React Native SDK (legacy)
200
+ version: {file: package.json, regex: '"version"\s*:\s*"([^"]+)"'}
201
+ store:
202
+ play: false # duplicates of reactnative-current ids: shared folder only
203
+ testflight: false
204
+ android_package: "${custom.app_ids.rn_android:-com.example.paymentsdk.reactnative.demoapp}"
205
+ ios_bundle_id: "${custom.app_ids.rn_ios:-com.example.paymentsdk.reactnative.demoapp}"
206
+ demos:
207
+ - {type: run, name: install demo deps, workdir: SimpleIntegration, cmd: [npm, install]}
208
+ - type: gradle
209
+ name: demo APK
210
+ workdir: SimpleIntegration/android
211
+ tasks: [assembleRelease]
212
+ collect:
213
+ - {glob: "app/build/outputs/apk/release/*.apk", kind: apk, target: android-rn-legacy}
214
+ - type: xcode-simulator
215
+ name: simulator .app
216
+ workdir: SimpleIntegration/ios
217
+ workspace: SimpleIntegration.xcworkspace
218
+ scheme: SimpleIntegration
219
+ pod_install: true
220
+ target: ios-rn-legacy
221
+ packages:
222
+ - {type: npm-pack, name: npm pack}
@@ -0,0 +1,305 @@
1
+ // SDK Release Kit Settings UI. Plain JS, no build step.
2
+ "use strict";
3
+
4
+ const TOKEN = document.querySelector('meta[name="srk-token"]').content;
5
+ const $ = (sel, root = document) => root.querySelector(sel);
6
+ const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
7
+ let STATUS = null;
8
+ let REPOS = null;
9
+ let OUTPUTS = null;
10
+ let KEYS = null;
11
+
12
+ async function api(method, path, body) {
13
+ const res = await fetch(path, {
14
+ method,
15
+ headers: { "Content-Type": "application/json", "X-SRK-Token": TOKEN },
16
+ body: body === undefined ? undefined : JSON.stringify(body),
17
+ });
18
+ if (!res.ok) {
19
+ let detail = res.statusText;
20
+ try { detail = (await res.json()).detail; } catch (_) { /* not JSON */ }
21
+ throw new Error(typeof detail === "string" ? detail : JSON.stringify(detail, null, 2));
22
+ }
23
+ return res.json();
24
+ }
25
+
26
+ function toast(msg, isError = false) {
27
+ const el = $("#toast");
28
+ el.textContent = msg;
29
+ el.className = "toast" + (isError ? " err" : "");
30
+ el.hidden = false;
31
+ clearTimeout(toast._t);
32
+ toast._t = setTimeout(() => (el.hidden = true), isError ? 8000 : 2500);
33
+ }
34
+
35
+ function guard(fn) {
36
+ return async (...args) => {
37
+ try { await fn(...args); } catch (e) { toast(e.message, true); }
38
+ };
39
+ }
40
+
41
+ const getPath = (obj, dotted) => dotted.split(".").reduce((o, k) => (o == null ? o : o[k]), obj);
42
+ function setPath(obj, dotted, value) {
43
+ const parts = dotted.split(".");
44
+ let o = obj;
45
+ for (const p of parts.slice(0, -1)) o = o[p] ??= {};
46
+ o[parts.at(-1)] = value;
47
+ }
48
+ const nullIfEmpty = (v) => (v === "" ? null : v);
49
+
50
+ // Tabs -------------------------------------------------------------------------------------
51
+ $$("#tabs button").forEach((btn) =>
52
+ btn.addEventListener("click", () => {
53
+ $$("#tabs button").forEach((b) => b.classList.toggle("active", b === btn));
54
+ $$(".tab").forEach((t) => (t.hidden = t.id !== "tab-" + btn.dataset.tab));
55
+ try { localStorage.setItem("srk-tab", btn.dataset.tab); } catch (_) { /* storage blocked */ }
56
+ })
57
+ );
58
+
59
+ // Repositories -----------------------------------------------------------------------------
60
+ function renderRepos() {
61
+ const tbody = $("#repos-table tbody");
62
+ tbody.innerHTML = "";
63
+ const ids = [...new Set([...Object.keys(STATUS.recipes), ...Object.keys(REPOS.repos)])];
64
+ for (const id of ids) {
65
+ const entry = REPOS.repos[id] || { path: "", enabled: false };
66
+ const recipe = STATUS.recipes[id];
67
+ const tr = document.createElement("tr");
68
+ tr.dataset.id = id;
69
+ tr.innerHTML = `
70
+ <td><input type="checkbox" class="enabled"></td>
71
+ <td><strong></strong><div class="muted small"></div></td>
72
+ <td class="path"><input class="path-input" placeholder="~/path/to/checkout"></td>
73
+ <td class="check"></td>`;
74
+ $(".enabled", tr).checked = !!entry.enabled;
75
+ $("strong", tr).textContent = id;
76
+ $(".small", tr).textContent = recipe ? `${recipe.platform}. ${recipe.label}` : "not in recipes (kept)";
77
+ const input = $(".path-input", tr);
78
+ input.value = entry.path || "";
79
+ const check = guard(async () => {
80
+ const cell = $(".check", tr);
81
+ if (!input.value.trim()) { cell.textContent = ""; return; }
82
+ const r = await api("POST", "/api/validate/path", { path: input.value });
83
+ cell.textContent = r.ok ? "✓ " + r.detail : "✗ " + r.detail;
84
+ cell.className = "check " + (r.ok ? "ok" : "bad");
85
+ });
86
+ input.addEventListener("change", check);
87
+ check();
88
+ tbody.appendChild(tr);
89
+ }
90
+ const t = REPOS.tooling || {};
91
+ for (const input of $$("#tooling-form input")) {
92
+ const v = t[input.name];
93
+ input.value = Array.isArray(v) ? v.join(", ") : v || "";
94
+ }
95
+ }
96
+
97
+ $("#save-repos").addEventListener("click", guard(async () => {
98
+ const repos = { ...REPOS.repos };
99
+ for (const tr of $$("#repos-table tbody tr")) {
100
+ repos[tr.dataset.id] = {
101
+ ...(repos[tr.dataset.id] || {}),
102
+ path: $(".path-input", tr).value.trim(),
103
+ enabled: $(".enabled", tr).checked,
104
+ };
105
+ }
106
+ const tooling = { ...(REPOS.tooling || {}) };
107
+ for (const input of $$("#tooling-form input")) {
108
+ tooling[input.name] = input.name === "extra_path"
109
+ ? input.value.split(",").map((s) => s.trim()).filter(Boolean)
110
+ : nullIfEmpty(input.value.trim());
111
+ }
112
+ REPOS = await api("PUT", "/api/settings/repos", { repos, tooling });
113
+ renderRepos();
114
+ renderRunSdks();
115
+ toast("Repositories saved");
116
+ }));
117
+
118
+ // Outputs ----------------------------------------------------------------------------------
119
+ function renderOutputs() {
120
+ for (const el of $$("#tab-outputs [data-k]")) {
121
+ const v = getPath(OUTPUTS, el.dataset.k);
122
+ if (el.type === "checkbox") el.checked = !!v;
123
+ else el.value = v ?? "";
124
+ }
125
+ }
126
+
127
+ $("#save-outputs").addEventListener("click", guard(async () => {
128
+ const body = structuredClone(OUTPUTS);
129
+ for (const el of $$("#tab-outputs [data-k]")) {
130
+ setPath(body, el.dataset.k, el.type === "checkbox" ? el.checked : nullIfEmpty(el.value.trim()));
131
+ }
132
+ OUTPUTS = await api("PUT", "/api/settings/outputs", body);
133
+ renderOutputs();
134
+ renderRunDestinations();
135
+ toast("Outputs saved");
136
+ }));
137
+
138
+ // Keys -------------------------------------------------------------------------------------
139
+ const SECRET_LABELS = {
140
+ "android.store_password": "Keystore password",
141
+ "android.key_password": "Key password (empty uses keystore password)",
142
+ "testflight.key_id": "ASC key id",
143
+ "testflight.issuer_id": "ASC issuer id",
144
+ };
145
+
146
+ function renderKeys() {
147
+ const { settings, status } = KEYS;
148
+ const chips = $("#keys-status");
149
+ chips.innerHTML = "";
150
+ for (const [name, st] of Object.entries(status)) {
151
+ const c = document.createElement("span");
152
+ c.className = "chip " + (st.configured ? "ok" : "bad");
153
+ c.textContent = `${name}: ${st.configured ? "configured" : "missing"}`;
154
+ c.title = st.detail;
155
+ chips.appendChild(c);
156
+ }
157
+ for (const el of $$("#tab-keys [data-key]")) {
158
+ const v = getPath(settings, el.dataset.key);
159
+ if (el.type === "checkbox") el.checked = !!v;
160
+ else el.value = v ?? "";
161
+ }
162
+ for (const box of $$("#tab-keys [data-secret]")) {
163
+ const field = box.dataset.secret;
164
+ const ref = getPath(settings, field) || { source: "none", name: "" };
165
+ const st = status[field] || {};
166
+ box.innerHTML = `
167
+ <div class="muted">${SECRET_LABELS[field]} <span class="${st.configured ? "ok" : "bad"}">(${st.configured ? "configured" : "missing"})</span></div>
168
+ <div class="row">
169
+ <select class="src"><option value="keychain">OS keychain</option><option value="env">Environment variable</option><option value="none">Not set</option></select>
170
+ <input class="ref-name" placeholder="entry or variable name">
171
+ </div>
172
+ <div class="row keychain-row">
173
+ <input class="value" type="password" autocomplete="new-password" placeholder="new value (stored in keychain, never shown)">
174
+ <button class="store">Store in keychain</button>
175
+ </div>`;
176
+ $(".src", box).value = ref.source;
177
+ $(".ref-name", box).value = ref.name;
178
+ const syncRow = () => ($(".keychain-row", box).hidden = $(".src", box).value !== "keychain");
179
+ $(".src", box).addEventListener("change", syncRow);
180
+ syncRow();
181
+ $(".store", box).addEventListener("click", guard(async () => {
182
+ const value = $(".value", box).value;
183
+ if (!value) { toast("Enter a value first", true); return; }
184
+ KEYS = await api("POST", "/api/keys/secret", { field, value });
185
+ renderKeys();
186
+ toast("Stored in keychain");
187
+ }));
188
+ }
189
+ }
190
+
191
+ $$("#tab-keys [data-upload]").forEach((input) =>
192
+ input.addEventListener("change", guard(async () => {
193
+ const file = input.files[0];
194
+ if (!file) return;
195
+ const buf = new Uint8Array(await file.arrayBuffer());
196
+ let bin = "";
197
+ for (let i = 0; i < buf.length; i += 0x8000) bin += String.fromCharCode(...buf.subarray(i, i + 0x8000));
198
+ KEYS = await api("POST", "/api/keys/upload", {
199
+ field: input.dataset.upload, filename: file.name, content_b64: btoa(bin),
200
+ });
201
+ input.value = "";
202
+ renderKeys();
203
+ toast(`${file.name} saved to the private keys folder`);
204
+ }))
205
+ );
206
+
207
+ $("#save-keys").addEventListener("click", guard(async () => {
208
+ const body = structuredClone(KEYS.settings);
209
+ for (const el of $$("#tab-keys [data-key]")) {
210
+ setPath(body, el.dataset.key, el.type === "checkbox" ? el.checked : nullIfEmpty(el.value.trim()));
211
+ }
212
+ for (const box of $$("#tab-keys [data-secret]")) {
213
+ setPath(body, box.dataset.secret, {
214
+ source: $(".src", box).value, name: $(".ref-name", box).value.trim(),
215
+ });
216
+ }
217
+ KEYS = await api("PUT", "/api/settings/keys", body);
218
+ renderKeys();
219
+ toast("Keys saved");
220
+ }));
221
+
222
+ // Custom -----------------------------------------------------------------------------------
223
+ $("#save-custom").addEventListener("click", guard(async () => {
224
+ const r = await api("PUT", "/api/settings/custom", { text: $("#custom-text").value });
225
+ $("#custom-text").value = r.text;
226
+ toast(`Custom data saved (${r.keys.length} top-level keys)`);
227
+ }));
228
+
229
+ // Run --------------------------------------------------------------------------------------
230
+ function renderRunSdks() {
231
+ const box = $("#run-sdks");
232
+ box.innerHTML = "";
233
+ for (const id of Object.keys(STATUS.recipes)) {
234
+ const entry = REPOS.repos[id];
235
+ const ready = entry && entry.enabled && entry.path;
236
+ const label = document.createElement("label");
237
+ label.className = "inline";
238
+ label.innerHTML = `<input type="checkbox" name="sdk"> <span></span>`;
239
+ $("input", label).value = id;
240
+ $("input", label).checked = !!ready;
241
+ $("span", label).textContent = id + (ready ? "" : " (no repo)");
242
+ box.appendChild(label);
243
+ }
244
+ }
245
+
246
+ function renderRunDestinations() {
247
+ for (const el of $$('input[name="dest"]')) el.checked = !!(OUTPUTS[el.value] || {}).enabled;
248
+ }
249
+
250
+ async function startRun(dryRun) {
251
+ const body = {
252
+ sdks: $$('input[name="sdk"]:checked').map((e) => e.value),
253
+ artifacts: $('input[name="artifacts"]:checked').value,
254
+ destinations: $$('input[name="dest"]:checked').map((e) => e.value),
255
+ dry_run: dryRun,
256
+ skip_upload: $("#skip-upload").checked,
257
+ };
258
+ if (!body.sdks.length) { toast("Select at least one SDK", true); return; }
259
+ const { id } = await api("POST", "/api/runs", body);
260
+ const logEl = $("#run-log");
261
+ logEl.textContent = "";
262
+ $("#run-state").textContent = dryRun ? "planning..." : "running...";
263
+ $$("#dry-run, #start-run").forEach((b) => (b.disabled = true));
264
+ try {
265
+ const res = await fetch(`/api/runs/${id}/log`, { headers: { "X-SRK-Token": TOKEN } });
266
+ const reader = res.body.getReader();
267
+ const dec = new TextDecoder();
268
+ for (;;) {
269
+ const { value, done } = await reader.read();
270
+ if (done) break;
271
+ const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 8;
272
+ logEl.textContent += dec.decode(value, { stream: true });
273
+ if (atBottom) logEl.scrollTop = logEl.scrollHeight;
274
+ }
275
+ const st = await api("GET", `/api/runs/${id}`);
276
+ const ok = st.result && (st.result.dry_run || st.result.ok);
277
+ $("#run-state").textContent = st.error ? "error" : dryRun ? "plan ready" : ok ? "finished" : "finished with problems";
278
+ } finally {
279
+ $$("#dry-run, #start-run").forEach((b) => (b.disabled = false));
280
+ }
281
+ }
282
+ $("#dry-run").addEventListener("click", guard(() => startRun(true)));
283
+ $("#start-run").addEventListener("click", guard(() => startRun(false)));
284
+
285
+ // Boot -------------------------------------------------------------------------------------
286
+ guard(async () => {
287
+ [STATUS, REPOS, OUTPUTS, KEYS] = await Promise.all([
288
+ api("GET", "/api/status"),
289
+ api("GET", "/api/settings/repos"),
290
+ api("GET", "/api/settings/outputs"),
291
+ api("GET", "/api/settings/keys"),
292
+ ]);
293
+ $("#version").textContent = "v" + STATUS.version;
294
+ $("#repos-file").textContent = STATUS.files.repos;
295
+ if (STATUS.recipe_error) toast(STATUS.recipe_error, true);
296
+ $("#custom-text").value = (await api("GET", "/api/settings/custom")).text;
297
+ renderRepos();
298
+ renderOutputs();
299
+ renderKeys();
300
+ renderRunSdks();
301
+ renderRunDestinations();
302
+ let tab = null;
303
+ try { tab = localStorage.getItem("srk-tab"); } catch (_) { /* storage blocked */ }
304
+ if (tab) $(`#tabs button[data-tab="${tab}"]`)?.click();
305
+ })();