@scalebun-release/cli 2.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 (210) hide show
  1. package/bin/scalebun.js +2 -0
  2. package/lib/api/client.d.ts +52 -0
  3. package/lib/api/client.d.ts.map +1 -0
  4. package/lib/api/client.js +335 -0
  5. package/lib/api/client.js.map +1 -0
  6. package/lib/commands/ci-init.d.ts +3 -0
  7. package/lib/commands/ci-init.d.ts.map +1 -0
  8. package/lib/commands/ci-init.js +348 -0
  9. package/lib/commands/ci-init.js.map +1 -0
  10. package/lib/commands/doctor.d.ts +31 -0
  11. package/lib/commands/doctor.d.ts.map +1 -0
  12. package/lib/commands/doctor.js +559 -0
  13. package/lib/commands/doctor.js.map +1 -0
  14. package/lib/commands/launch.d.ts +3 -0
  15. package/lib/commands/launch.d.ts.map +1 -0
  16. package/lib/commands/launch.js +23 -0
  17. package/lib/commands/launch.js.map +1 -0
  18. package/lib/commands/link.d.ts +16 -0
  19. package/lib/commands/link.d.ts.map +1 -0
  20. package/lib/commands/link.js +164 -0
  21. package/lib/commands/link.js.map +1 -0
  22. package/lib/commands/login.d.ts +12 -0
  23. package/lib/commands/login.d.ts.map +1 -0
  24. package/lib/commands/login.js +124 -0
  25. package/lib/commands/login.js.map +1 -0
  26. package/lib/commands/migrate-codepush.d.ts +13 -0
  27. package/lib/commands/migrate-codepush.d.ts.map +1 -0
  28. package/lib/commands/migrate-codepush.js +98 -0
  29. package/lib/commands/migrate-codepush.js.map +1 -0
  30. package/lib/commands/ota-bundle.d.ts +22 -0
  31. package/lib/commands/ota-bundle.d.ts.map +1 -0
  32. package/lib/commands/ota-bundle.js +253 -0
  33. package/lib/commands/ota-bundle.js.map +1 -0
  34. package/lib/commands/ota-channels.d.ts +3 -0
  35. package/lib/commands/ota-channels.d.ts.map +1 -0
  36. package/lib/commands/ota-channels.js +231 -0
  37. package/lib/commands/ota-channels.js.map +1 -0
  38. package/lib/commands/ota-delete-bundle.d.ts +16 -0
  39. package/lib/commands/ota-delete-bundle.d.ts.map +1 -0
  40. package/lib/commands/ota-delete-bundle.js +96 -0
  41. package/lib/commands/ota-delete-bundle.js.map +1 -0
  42. package/lib/commands/ota-inspect.d.ts +3 -0
  43. package/lib/commands/ota-inspect.d.ts.map +1 -0
  44. package/lib/commands/ota-inspect.js +258 -0
  45. package/lib/commands/ota-inspect.js.map +1 -0
  46. package/lib/commands/ota-keys.d.ts +91 -0
  47. package/lib/commands/ota-keys.d.ts.map +1 -0
  48. package/lib/commands/ota-keys.js +765 -0
  49. package/lib/commands/ota-keys.js.map +1 -0
  50. package/lib/commands/ota-promote.d.ts +3 -0
  51. package/lib/commands/ota-promote.d.ts.map +1 -0
  52. package/lib/commands/ota-promote.js +194 -0
  53. package/lib/commands/ota-promote.js.map +1 -0
  54. package/lib/commands/ota-publish.d.ts +26 -0
  55. package/lib/commands/ota-publish.d.ts.map +1 -0
  56. package/lib/commands/ota-publish.js +892 -0
  57. package/lib/commands/ota-publish.js.map +1 -0
  58. package/lib/commands/ota-rollout.d.ts +3 -0
  59. package/lib/commands/ota-rollout.d.ts.map +1 -0
  60. package/lib/commands/ota-rollout.js +265 -0
  61. package/lib/commands/ota-rollout.js.map +1 -0
  62. package/lib/commands/ota-sign.d.ts +19 -0
  63. package/lib/commands/ota-sign.d.ts.map +1 -0
  64. package/lib/commands/ota-sign.js +245 -0
  65. package/lib/commands/ota-sign.js.map +1 -0
  66. package/lib/commands/push-apns.d.ts +3 -0
  67. package/lib/commands/push-apns.d.ts.map +1 -0
  68. package/lib/commands/push-apns.js +166 -0
  69. package/lib/commands/push-apns.js.map +1 -0
  70. package/lib/commands/quickstart.d.ts +4 -0
  71. package/lib/commands/quickstart.d.ts.map +1 -0
  72. package/lib/commands/quickstart.js +672 -0
  73. package/lib/commands/quickstart.js.map +1 -0
  74. package/lib/commands/shell.d.ts +28 -0
  75. package/lib/commands/shell.d.ts.map +1 -0
  76. package/lib/commands/shell.js +512 -0
  77. package/lib/commands/shell.js.map +1 -0
  78. package/lib/commands/tls-pins.d.ts +27 -0
  79. package/lib/commands/tls-pins.d.ts.map +1 -0
  80. package/lib/commands/tls-pins.js +209 -0
  81. package/lib/commands/tls-pins.js.map +1 -0
  82. package/lib/index.d.ts +2 -0
  83. package/lib/index.d.ts.map +1 -0
  84. package/lib/index.js +233 -0
  85. package/lib/index.js.map +1 -0
  86. package/lib/ui/banner.d.ts +23 -0
  87. package/lib/ui/banner.d.ts.map +1 -0
  88. package/lib/ui/banner.js +162 -0
  89. package/lib/ui/banner.js.map +1 -0
  90. package/lib/ui/graphics.d.ts +18 -0
  91. package/lib/ui/graphics.d.ts.map +1 -0
  92. package/lib/ui/graphics.js +134 -0
  93. package/lib/ui/graphics.js.map +1 -0
  94. package/lib/ui/mark.d.ts +4 -0
  95. package/lib/ui/mark.d.ts.map +1 -0
  96. package/lib/ui/mark.js +21 -0
  97. package/lib/ui/mark.js.map +1 -0
  98. package/lib/ui/menu.d.ts +109 -0
  99. package/lib/ui/menu.d.ts.map +1 -0
  100. package/lib/ui/menu.js +314 -0
  101. package/lib/ui/menu.js.map +1 -0
  102. package/lib/ui/sixel.d.ts +6 -0
  103. package/lib/ui/sixel.d.ts.map +1 -0
  104. package/lib/ui/sixel.js +9 -0
  105. package/lib/ui/sixel.js.map +1 -0
  106. package/lib/utils/apps.d.ts +29 -0
  107. package/lib/utils/apps.d.ts.map +1 -0
  108. package/lib/utils/apps.js +19 -0
  109. package/lib/utils/apps.js.map +1 -0
  110. package/lib/utils/browser.d.ts +9 -0
  111. package/lib/utils/browser.d.ts.map +1 -0
  112. package/lib/utils/browser.js +39 -0
  113. package/lib/utils/browser.js.map +1 -0
  114. package/lib/utils/bundle.d.ts +153 -0
  115. package/lib/utils/bundle.d.ts.map +1 -0
  116. package/lib/utils/bundle.js +459 -0
  117. package/lib/utils/bundle.js.map +1 -0
  118. package/lib/utils/bundleStore.d.ts +128 -0
  119. package/lib/utils/bundleStore.d.ts.map +1 -0
  120. package/lib/utils/bundleStore.js +250 -0
  121. package/lib/utils/bundleStore.js.map +1 -0
  122. package/lib/utils/context.d.ts +88 -0
  123. package/lib/utils/context.d.ts.map +1 -0
  124. package/lib/utils/context.js +247 -0
  125. package/lib/utils/context.js.map +1 -0
  126. package/lib/utils/credentials.d.ts +59 -0
  127. package/lib/utils/credentials.d.ts.map +1 -0
  128. package/lib/utils/credentials.js +147 -0
  129. package/lib/utils/credentials.js.map +1 -0
  130. package/lib/utils/guide.d.ts +63 -0
  131. package/lib/utils/guide.d.ts.map +1 -0
  132. package/lib/utils/guide.js +191 -0
  133. package/lib/utils/guide.js.map +1 -0
  134. package/lib/utils/hash.d.ts +3 -0
  135. package/lib/utils/hash.d.ts.map +1 -0
  136. package/lib/utils/hash.js +44 -0
  137. package/lib/utils/hash.js.map +1 -0
  138. package/lib/utils/native-id.d.ts +17 -0
  139. package/lib/utils/native-id.d.ts.map +1 -0
  140. package/lib/utils/native-id.js +105 -0
  141. package/lib/utils/native-id.js.map +1 -0
  142. package/lib/utils/output.d.ts +96 -0
  143. package/lib/utils/output.d.ts.map +1 -0
  144. package/lib/utils/output.js +135 -0
  145. package/lib/utils/output.js.map +1 -0
  146. package/lib/utils/prompt.d.ts +106 -0
  147. package/lib/utils/prompt.d.ts.map +1 -0
  148. package/lib/utils/prompt.js +457 -0
  149. package/lib/utils/prompt.js.map +1 -0
  150. package/lib/utils/provenance.d.ts +30 -0
  151. package/lib/utils/provenance.d.ts.map +1 -0
  152. package/lib/utils/provenance.js +157 -0
  153. package/lib/utils/provenance.js.map +1 -0
  154. package/lib/utils/resolve.d.ts +81 -0
  155. package/lib/utils/resolve.d.ts.map +1 -0
  156. package/lib/utils/resolve.js +139 -0
  157. package/lib/utils/resolve.js.map +1 -0
  158. package/lib/utils/sdkBin.d.ts +18 -0
  159. package/lib/utils/sdkBin.d.ts.map +1 -0
  160. package/lib/utils/sdkBin.js +76 -0
  161. package/lib/utils/sdkBin.js.map +1 -0
  162. package/lib/utils/signer.d.ts +94 -0
  163. package/lib/utils/signer.d.ts.map +1 -0
  164. package/lib/utils/signer.js +230 -0
  165. package/lib/utils/signer.js.map +1 -0
  166. package/lib/utils/signing.d.ts +102 -0
  167. package/lib/utils/signing.d.ts.map +1 -0
  168. package/lib/utils/signing.js +229 -0
  169. package/lib/utils/signing.js.map +1 -0
  170. package/lib/utils/transportSecurity.d.ts +36 -0
  171. package/lib/utils/transportSecurity.d.ts.map +1 -0
  172. package/lib/utils/transportSecurity.js +74 -0
  173. package/lib/utils/transportSecurity.js.map +1 -0
  174. package/lib/utils/trustAnchor.d.ts +109 -0
  175. package/lib/utils/trustAnchor.d.ts.map +1 -0
  176. package/lib/utils/trustAnchor.js +169 -0
  177. package/lib/utils/trustAnchor.js.map +1 -0
  178. package/lib/utils/ui.d.ts +12 -0
  179. package/lib/utils/ui.d.ts.map +1 -0
  180. package/lib/utils/ui.js +60 -0
  181. package/lib/utils/ui.js.map +1 -0
  182. package/lib/utils/validate.d.ts +62 -0
  183. package/lib/utils/validate.d.ts.map +1 -0
  184. package/lib/utils/validate.js +189 -0
  185. package/lib/utils/validate.js.map +1 -0
  186. package/lib/vendor/ota-protocol/base64.d.ts +11 -0
  187. package/lib/vendor/ota-protocol/base64.d.ts.map +1 -0
  188. package/lib/vendor/ota-protocol/base64.js +39 -0
  189. package/lib/vendor/ota-protocol/base64.js.map +1 -0
  190. package/lib/vendor/ota-protocol/codec.d.ts +26 -0
  191. package/lib/vendor/ota-protocol/codec.d.ts.map +1 -0
  192. package/lib/vendor/ota-protocol/codec.js +349 -0
  193. package/lib/vendor/ota-protocol/codec.js.map +1 -0
  194. package/lib/vendor/ota-protocol/hex.d.ts +8 -0
  195. package/lib/vendor/ota-protocol/hex.d.ts.map +1 -0
  196. package/lib/vendor/ota-protocol/hex.js +31 -0
  197. package/lib/vendor/ota-protocol/hex.js.map +1 -0
  198. package/lib/vendor/ota-protocol/index.d.ts +17 -0
  199. package/lib/vendor/ota-protocol/index.d.ts.map +1 -0
  200. package/lib/vendor/ota-protocol/index.js +41 -0
  201. package/lib/vendor/ota-protocol/index.js.map +1 -0
  202. package/lib/vendor/ota-protocol/limits.d.ts +39 -0
  203. package/lib/vendor/ota-protocol/limits.d.ts.map +1 -0
  204. package/lib/vendor/ota-protocol/limits.js +42 -0
  205. package/lib/vendor/ota-protocol/limits.js.map +1 -0
  206. package/lib/vendor/ota-protocol/types.d.ts +73 -0
  207. package/lib/vendor/ota-protocol/types.d.ts.map +1 -0
  208. package/lib/vendor/ota-protocol/types.js +24 -0
  209. package/lib/vendor/ota-protocol/types.js.map +1 -0
  210. package/package.json +37 -0
@@ -0,0 +1,672 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerAppsCommand = registerAppsCommand;
40
+ exports.registerQuickstartCommand = registerQuickstartCommand;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const readline = __importStar(require("readline"));
44
+ const chalk_1 = __importDefault(require("chalk"));
45
+ const credentials_1 = require("../utils/credentials");
46
+ const client_1 = require("../api/client");
47
+ const crypto = __importStar(require("crypto"));
48
+ const apps_1 = require("../utils/apps");
49
+ const trustAnchor_1 = require("../utils/trustAnchor");
50
+ const ota_keys_1 = require("./ota-keys");
51
+ const login_1 = require("./login");
52
+ const doctor_1 = require("./doctor");
53
+ const ota_keys_2 = require("./ota-keys");
54
+ const signing_1 = require("../utils/signing");
55
+ const sdkBin_1 = require("../utils/sdkBin");
56
+ const context_1 = require("../utils/context");
57
+ const output_1 = require("../utils/output");
58
+ /** client key with the middle masked — enough to recognize, useless to steal. */
59
+ function maskKey(key) {
60
+ return key.length <= 16 ? key : `${key.slice(0, 12)}…${key.slice(-4)}`;
61
+ }
62
+ function registerAppsCommand(program) {
63
+ program
64
+ .command('apps')
65
+ .description('List every app you can publish to, with the IDs the other commands need')
66
+ .option('--keys', 'Also show each app\'s environments and SDK client keys (masked)')
67
+ .option('--json', 'Machine-readable output')
68
+ .action(async (opts) => {
69
+ const creds = (0, credentials_1.readCredentials)();
70
+ if (!creds) {
71
+ console.error('❌ Not logged in. Run `scalebun login` first.');
72
+ process.exit(1);
73
+ }
74
+ const client = new client_1.ApiClient(creds);
75
+ let entries;
76
+ try {
77
+ entries = await (0, apps_1.fetchAllApps)(client);
78
+ }
79
+ catch (err) {
80
+ console.error(`❌ Could not list apps: ${err.message}`);
81
+ process.exit(1);
82
+ return;
83
+ }
84
+ // Machine mode short-circuits before the grouped, coloured rendering —
85
+ // and before the per-app key fetches, which are N+1 requests nobody
86
+ // scripting this wants to pay for unless they asked with --keys.
87
+ // isJson(), not opts.json: commander does not pass root options down to
88
+ // subcommand handlers, and because the root also declares --json it
89
+ // consumes the flag before this handler ever sees it. output.ts reads it
90
+ // once from raw argv and is the single authority.
91
+ if ((0, output_1.isJson)()) {
92
+ (0, output_1.emit)(() => { }, {
93
+ ok: true,
94
+ count: entries.length,
95
+ apps: entries.map((e) => ({
96
+ id: e.app.id,
97
+ name: e.app.name,
98
+ platform: e.app.platform ?? null,
99
+ orgId: e.orgId,
100
+ orgName: e.orgName,
101
+ projectName: e.projectName,
102
+ })),
103
+ });
104
+ return;
105
+ }
106
+ if (!entries.length) {
107
+ console.log('\nNo apps yet. Create one in the dashboard (it takes a minute), then re-run this.\n');
108
+ return;
109
+ }
110
+ console.log('');
111
+ let lastGroup = '';
112
+ for (const e of entries) {
113
+ const group = `${e.orgName} / ${e.projectName}`;
114
+ if (group !== lastGroup) {
115
+ console.log(chalk_1.default.bold(` ${group}`));
116
+ lastGroup = group;
117
+ }
118
+ console.log(` • ${e.app.name.padEnd(24)} ${chalk_1.default.dim(e.app.platform ?? '?')} app-id: ${chalk_1.default.cyan(e.app.id)}`);
119
+ if (opts.keys) {
120
+ try {
121
+ const envs = await client.get(`/apps/${e.app.id}/environments`);
122
+ for (const env of envs ?? []) {
123
+ const keys = await client.get(`/environments/${env.id}/sdk-keys`);
124
+ const active = (keys ?? []).filter((k) => k.status === 'active');
125
+ const label = `${env.name}${env.type ? ` (${env.type})` : ''}`;
126
+ if (!active.length) {
127
+ console.log(` ${label}: ${chalk_1.default.dim('no active client key')}`);
128
+ }
129
+ for (const k of active) {
130
+ console.log(` ${label}: ${chalk_1.default.green(maskKey(k.clientKey))} ${chalk_1.default.dim(k.name ?? '')}`);
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ console.log(chalk_1.default.dim(' (could not load environments/keys)'));
136
+ }
137
+ }
138
+ }
139
+ console.log('');
140
+ console.log(chalk_1.default.dim(' Next: scalebun ota publish --app-id <id> --channel production --platform android'));
141
+ console.log('');
142
+ });
143
+ }
144
+ function registerQuickstartCommand(program) {
145
+ program
146
+ .command('quickstart')
147
+ .description('Guided walkthrough: login → pick your app → keys → signing → native wiring → health check → first release')
148
+ .option('--app-id <id>', 'Skip app selection and use this app')
149
+ .action(async (opts) => {
150
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
151
+ // Resolves '' on EOF rather than never settling.
152
+ //
153
+ // THIS IS WHY QUICKSTART DIED AT "Which app?". The old form was
154
+ // `new Promise((r) => rl.question(q, r))`: if stdin reaches EOF — piped
155
+ // input, a closed handle, a terminal that hands the process no tty — the
156
+ // callback never fires, the promise never settles, and with nothing left
157
+ // holding the event loop open node simply exits. Mid-step, no error, no
158
+ // message: the shell prompt comes back and whatever the user typed next
159
+ // lands in the shell instead. utils/prompt.ts already guards this exact
160
+ // case; this one did not.
161
+ //
162
+ // Resolving '' hands control back to each call site's own default (the
163
+ // app picker reads '' as 1, the [Y/n] prompts as yes), so an EOF now
164
+ // continues the walkthrough instead of killing it.
165
+ // Tracked, because `rl.question()` on an ALREADY-closed interface throws
166
+ // ERR_USE_AFTER_CLOSE synchronously — so guarding only the close-while-
167
+ // waiting case turned the silent exit into a crash at the next prompt.
168
+ let rlClosed = false;
169
+ rl.on('close', () => {
170
+ rlClosed = true;
171
+ });
172
+ const ask = (q) => new Promise((resolve) => {
173
+ if (rlClosed) {
174
+ resolve('');
175
+ return;
176
+ }
177
+ let answered = false;
178
+ const onClose = () => {
179
+ if (!answered)
180
+ resolve('');
181
+ };
182
+ rl.once('close', onClose);
183
+ try {
184
+ rl.question(q, (answer) => {
185
+ answered = true;
186
+ rl.removeListener('close', onClose);
187
+ resolve(answer);
188
+ });
189
+ }
190
+ catch {
191
+ // Closed between the check above and here.
192
+ rl.removeListener('close', onClose);
193
+ resolve('');
194
+ }
195
+ });
196
+ const step = (n, title) => console.log(chalk_1.default.bold.cyan(`\n── Step ${n}/7 · ${title} ${'─'.repeat(Math.max(1, 44 - title.length))}`));
197
+ try {
198
+ console.log(chalk_1.default.bold('\n⚡ Scalebun quickstart — from zero to your first OTA update.\n'));
199
+ console.log(chalk_1.default.dim(' Everything below runs against your real account. Ctrl+C any time.'));
200
+ // Say it once, at the top. Without this the walkthrough looks like it is
201
+ // racing through the prompts on its own, which is exactly what it is doing.
202
+ if (!process.stdin.isTTY) {
203
+ console.log(chalk_1.default.yellow(' stdin is not interactive — every prompt below takes its default.'));
204
+ console.log(chalk_1.default.dim(' Pass --app-id to choose the app explicitly.'));
205
+ }
206
+ // ── 1. Login ────────────────────────────────────────────────────────
207
+ step(1, 'Sign in');
208
+ let creds = (0, credentials_1.readCredentials)();
209
+ if (creds) {
210
+ try {
211
+ const me = await new client_1.ApiClient(creds).get('/auth/me');
212
+ console.log(`✅ Already signed in as ${me.email} (${creds.apiUrl})`);
213
+ }
214
+ catch {
215
+ console.log('⚠️ Stored token no longer works — signing in again.');
216
+ creds = null;
217
+ }
218
+ }
219
+ if (!creds) {
220
+ await (0, login_1.runLogin)({});
221
+ creds = (0, credentials_1.readCredentials)();
222
+ if (!creds)
223
+ process.exit(1);
224
+ }
225
+ const client = new client_1.ApiClient(creds);
226
+ // ── 2. Pick the app ─────────────────────────────────────────────────
227
+ step(2, 'Pick your app');
228
+ let appId = opts.appId ?? null;
229
+ let appLabel = appId ?? '';
230
+ // Needed to write the native trust anchor in step 4. It is NOT the appId:
231
+ // the device verifier compares the signed release's projectId against the
232
+ // embedded one and refuses on PROJECT_MISMATCH.
233
+ let projectId = null;
234
+ if (!appId) {
235
+ const entries = await (0, apps_1.fetchAllApps)(client);
236
+ if (!entries.length) {
237
+ console.log('❌ Your account has no apps yet.');
238
+ console.log(' Create one in the dashboard (Organization → Project → App), then re-run quickstart.');
239
+ process.exit(1);
240
+ }
241
+ if (entries.length === 1) {
242
+ appId = entries[0].app.id;
243
+ appLabel = entries[0].app.name;
244
+ projectId = entries[0].projectId;
245
+ console.log(`✅ Using your only app: ${appLabel} (${appId})`);
246
+ }
247
+ else {
248
+ entries.forEach((e, i) => console.log(` ${i + 1}. ${e.orgName} / ${e.projectName} / ${chalk_1.default.bold(e.app.name)} ${chalk_1.default.dim(e.app.id)}`));
249
+ const n = parseInt(await ask('\nWhich app? [1]: '), 10) || 1;
250
+ const chosen = entries[Math.min(Math.max(n, 1), entries.length) - 1];
251
+ appId = chosen.app.id;
252
+ appLabel = chosen.app.name;
253
+ projectId = chosen.projectId;
254
+ console.log(`✅ ${chosen.app.name} (${appId})`);
255
+ }
256
+ }
257
+ // `--app-id` skips the picker above, so projectId is still unknown — and
258
+ // without it step 4 cannot write the trust anchor, which is the one thing
259
+ // the user most needs done for them. Look it up rather than silently
260
+ // degrading the flow for anyone who passed the flag.
261
+ if (appId && !projectId) {
262
+ try {
263
+ const found = (await (0, apps_1.fetchAllApps)(client)).find((e) => e.app.id === appId);
264
+ if (found) {
265
+ projectId = found.projectId;
266
+ if (!appLabel || appLabel === appId)
267
+ appLabel = found.app.name;
268
+ }
269
+ }
270
+ catch { /* non-fatal: step 4 says why the anchor was skipped */ }
271
+ }
272
+ // Bind the choice to this directory, so the rest of quickstart — and
273
+ // every command the user runs afterwards — stops needing --app-id.
274
+ // Without this the guided flow ends by printing commands that all
275
+ // carry a pasted cuid, which is exactly the friction `link` removes.
276
+ {
277
+ const root = (0, context_1.findProjectRoot)() ?? process.cwd();
278
+ try {
279
+ (0, context_1.writeProjectConfig)(root, {
280
+ appId: appId,
281
+ ...(appLabel ? { appName: appLabel } : {}),
282
+ channel: 'production',
283
+ });
284
+ console.log(chalk_1.default.dim(` Linked this project — ${context_1.PROJECT_FILE} written, --app-id no longer needed here.`));
285
+ }
286
+ catch {
287
+ // A read-only checkout is not a reason to abandon the walkthrough;
288
+ // every later step still works with the id held in memory.
289
+ console.log(chalk_1.default.dim(' (could not write scalebun.json here — continuing)'));
290
+ }
291
+ }
292
+ // ── 3. SDK client key ───────────────────────────────────────────────
293
+ step(3, 'SDK client key');
294
+ console.log(chalk_1.default.dim(' The client key is what your app ships with — it scopes every SDK call to this app.'));
295
+ let clientKeyForSnippet = null;
296
+ try {
297
+ const envs = await client.get(`/apps/${appId}/environments`);
298
+ for (const env of envs ?? []) {
299
+ const keys = await client.get(`/environments/${env.id}/sdk-keys`);
300
+ const active = (keys ?? []).filter((k) => k.status === 'active');
301
+ console.log(` ${env.name}: ${active.length ? active.map((k) => maskKey(k.clientKey)).join(', ') : chalk_1.default.dim('no active key')}`);
302
+ if (active.length && !clientKeyForSnippet)
303
+ clientKeyForSnippet = active[0].clientKey;
304
+ }
305
+ if (!clientKeyForSnippet && envs?.length) {
306
+ const yn = (await ask('\n No active client key. Create one now? [Y/n]: ')).trim().toLowerCase();
307
+ if (yn !== 'n') {
308
+ const made = await client.post(`/environments/${envs[0].id}/sdk-keys`, {
309
+ name: 'quickstart key',
310
+ });
311
+ clientKeyForSnippet = made.clientKey;
312
+ console.log(` 🔑 Created: ${made.clientKey}`);
313
+ if (made.secretKey) {
314
+ console.log(chalk_1.default.yellow(` Secret key (shown ONCE, store it now): ${made.secretKey}`));
315
+ }
316
+ }
317
+ }
318
+ }
319
+ catch (err) {
320
+ console.log(` ⚠️ Could not read keys (${err.message}) — grab the client key from the dashboard instead.`);
321
+ }
322
+ console.log('\n Wire the SDK in your app entry point:\n');
323
+ console.log(chalk_1.default.green(' import { ScaleBunProvider } from \'@scalebun/react-native\';\n'));
324
+ console.log(chalk_1.default.green(' <ScaleBunProvider config={{'));
325
+ console.log(chalk_1.default.green(` clientKey: '${clientKeyForSnippet ?? '<your client key>'}',`));
326
+ console.log(chalk_1.default.green(' }}>'));
327
+ console.log(chalk_1.default.green(' <App />'));
328
+ console.log(chalk_1.default.green(' </ScaleBunProvider>'));
329
+ // ── 4. Bundle signing key ───────────────────────────────────────────
330
+ // Default is YES: quickstart is where NEW apps are born, and new apps
331
+ // should come out signed. Existing apps are never touched by this —
332
+ // signing only binds where a key is pinned in the app's own config.
333
+ step(4, 'Bundle signing (recommended)');
334
+ const keyPath = path.resolve('.scalebun', 'ota-signing-key.pem');
335
+ // Drives the closing advice in step 7: an app with no signing key can
336
+ // publish, but only on SHA-256 integrity, and nothing stops a release
337
+ // the customer's key never authorised.
338
+ let signingReady = false;
339
+ if (fs.existsSync(keyPath)) {
340
+ // A local key file is not the same as a registered key.
341
+ //
342
+ // This branch used to print "already present" and skip the entire
343
+ // step — so a run that died at registration (a 403, a dropped
344
+ // connection) left a private key on disk that the server had never
345
+ // seen, and every re-run cheerfully confirmed it was fine. The next
346
+ // publish was the first thing to disagree, by which point nothing on
347
+ // screen connected the two. Check the server, not the filesystem.
348
+ console.log(`✅ Signing key already present: ${keyPath}`);
349
+ signingReady = true;
350
+ const localKeyId = (0, ota_keys_2.readLocalKeyId)(path.dirname(keyPath));
351
+ try {
352
+ const rows = await client.get(`/saas/apps/${appId}/ota/signing-keys`);
353
+ const match = localKeyId
354
+ ? rows.find((r) => r.keyId === localKeyId && !r.revokedAt)
355
+ : undefined;
356
+ if (match) {
357
+ console.log(chalk_1.default.dim(` Registered with this app as "${localKeyId}".`));
358
+ }
359
+ else if (localKeyId) {
360
+ console.log(chalk_1.default.yellow(` ⚠️ "${localKeyId}" is NOT registered with this app.`));
361
+ console.log(chalk_1.default.dim(' Publishing will work, but the release cannot be promoted until it is.'));
362
+ console.log(chalk_1.default.dim(` scalebun ota keys register --key-id ${localKeyId} --public-key ${path.join(path.dirname(keyPath), 'ota-signing-key.pub.pem')}`));
363
+ }
364
+ else {
365
+ console.log(chalk_1.default.dim(' No local key id recorded — run `scalebun ota keys list` to check.'));
366
+ }
367
+ }
368
+ catch (err) {
369
+ const msg = String(err?.message ?? err ?? '').trim();
370
+ console.log(chalk_1.default.dim(` Could not confirm registration: ${msg || 'no detail returned'}. Check with \`scalebun ota keys list\`.`));
371
+ }
372
+ }
373
+ else {
374
+ const yn = (await ask(' No signing key. Create one? [Y/n]: ')).trim().toLowerCase();
375
+ if (yn !== 'n') {
376
+ // Let the operator name the key. It is the label they will see in the
377
+ // dashboard and the id every signed release carries, and it outlives
378
+ // this setup by years — `quickstart-2026-09` says nothing about which
379
+ // app, environment or team it belongs to once there are several.
380
+ const defaultKeyId = `${(appLabel || 'app').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 24) || 'app'}-${new Date().toISOString().slice(0, 7)}`;
381
+ console.log('');
382
+ const rawKeyId = (await ask(` Name this signing key [${defaultKeyId}]: `)).trim();
383
+ const keyId = (rawKeyId || defaultKeyId).replace(/[^A-Za-z0-9._-]+/g, '-');
384
+ if (rawKeyId && keyId !== rawKeyId) {
385
+ console.log(chalk_1.default.dim(` Using "${keyId}" — a key id may only contain letters, digits, dot, dash or underscore.`));
386
+ }
387
+ const { privatePem, publicPem, info } = (0, signing_1.generateRsaKeyPair)();
388
+ fs.mkdirSync(path.dirname(keyPath), { recursive: true });
389
+ fs.writeFileSync(keyPath, privatePem, { mode: 0o600 });
390
+ // Same hardening as `ota keys generate` — this inline path used to skip
391
+ // both, leaving the key world-readable on Windows and its git hygiene
392
+ // to a warning line.
393
+ (0, credentials_1.restrictToCurrentUser)(keyPath, 'your OTA signing private key');
394
+ fs.writeFileSync(path.join(path.dirname(keyPath), 'ota-signing-key.pub.pem'), publicPem);
395
+ fs.writeFileSync(path.join(path.dirname(keyPath), ota_keys_2.KEY_ID_FILE), keyId + '\n');
396
+ const gitignoreOutcome = (0, ota_keys_2.ensureKeyDirGitignored)(path.dirname(keyPath));
397
+ // Registration is guarded because the private key is ALREADY on disk
398
+ // by this point. Unguarded, a refusal here threw out of the action
399
+ // handler as an unhandled rejection — which prints next to nothing —
400
+ // and left a local key the server had never seen. The next publish
401
+ // then failed for a reason nothing on screen had explained.
402
+ let keyRow = null;
403
+ try {
404
+ keyRow = await client.post(`/saas/apps/${appId}/ota/signing-keys`, {
405
+ publicKey: publicPem,
406
+ keyId,
407
+ algorithm: 'RSA_SHA256',
408
+ name: keyId,
409
+ });
410
+ }
411
+ catch (err) {
412
+ const msg = String(err?.message ?? err ?? '').trim();
413
+ const status = typeof err?.status === 'number' ? err.status : undefined;
414
+ console.log(chalk_1.default.yellow(` ⚠️ The key was generated but NOT registered${status ? ` (HTTP ${status})` : ''}.`));
415
+ // Never print an empty reason. A blank line here is what made this
416
+ // failure look like a crash rather than a refusal.
417
+ console.log(chalk_1.default.dim(` ${msg || 'the server refused the request and returned no detail'}`));
418
+ if (status === 403) {
419
+ console.log(chalk_1.default.dim(' A 403 here usually means the workspace is suspended or its plan lapsed.'));
420
+ console.log(chalk_1.default.dim(' Check billing in the dashboard, then re-run quickstart.'));
421
+ }
422
+ else if (status === 401) {
423
+ console.log(chalk_1.default.dim(' Run `scalebun login` and re-run quickstart.'));
424
+ }
425
+ console.log(chalk_1.default.dim(` Your private key is safe at ${keyPath}. Register it later with:`));
426
+ console.log(chalk_1.default.dim(` scalebun ota keys register --key-id ${keyId} --public-key ${path.join(path.dirname(keyPath), 'ota-signing-key.pub.pem')}`));
427
+ }
428
+ // Registering only says "trust this key". Until possession is proven
429
+ // the key is not ACTIVE and every promote is refused as high-risk
430
+ // (`signing_key_ownership_unverified`) — so a guided setup that stops
431
+ // at registration hands the user a key that cannot ship anything. We
432
+ // are holding the private half right here; finish it.
433
+ if (keyRow?.id) {
434
+ try {
435
+ const ok = await (0, ota_keys_1.proveOwnership)(client, appId, keyRow.id, crypto.createPrivateKey(privatePem));
436
+ console.log(ok
437
+ ? ' ✅ Ownership proven — the key is ACTIVE.'
438
+ : ' ⚠️ Ownership not confirmed; run `scalebun ota keys verify`.');
439
+ }
440
+ catch (err) {
441
+ console.log(chalk_1.default.yellow(` ⚠️ Could not prove ownership: ${err.message}`));
442
+ console.log(chalk_1.default.dim(' Finish it with: scalebun ota keys verify'));
443
+ }
444
+ }
445
+ signingReady = true;
446
+ console.log(` 🔏 RSA-${info.modulusBits} key generated (${keyPath}) and public half registered.`);
447
+ console.log(` Key ID: ${keyId} Fingerprint: SHA-256:${info.fingerprintDisplay.slice(0, 23)}…`);
448
+ console.log(` Git: ${gitignoreOutcome}`);
449
+ // Write the trust anchor rather than describing it.
450
+ //
451
+ // This step used to print the path and stop, leaving the most
452
+ // error-prone action to be done by hand: a JSON file, in the right
453
+ // directory, with the right projectId and the PEM inlined. Get the
454
+ // projectId wrong — appId is the easy wrong guess, both are `cm…`
455
+ // cuids — and the device downloads the release, refuses it with
456
+ // PROJECT_MISMATCH, and reverts. The CLI knows both ids.
457
+ // Embed an UNUSED standby key alongside the active one.
458
+ //
459
+ // Trust only changes at store speed — the anchor is compiled in, so a
460
+ // key the binary does not already carry can never be added over the
461
+ // air. With a single key, rotating (or recovering from a leak) means
462
+ // shipping a native build and waiting for adoption while still signing
463
+ // with the key you are trying to retire. A standby the device already
464
+ // trusts turns that into: start signing with it.
465
+ //
466
+ // It is deliberately NOT registered with the server. Pre-trusted but
467
+ // unusable is the point: nothing can sign with it until you choose to
468
+ // register it, so it costs nothing to carry and is there when needed.
469
+ //
470
+ // Resolved HERE, above the per-platform writes, because it used to be
471
+ // generated inside the Android branch — so an iOS app got the active
472
+ // key and no standby, and the rotation path the standby exists for
473
+ // did not exist on iOS at all.
474
+ const standbyId = `${keyId}-standby`;
475
+ const standbyPath = path.join(path.dirname(keyPath), 'ota-standby-key.pem');
476
+ const standbyPubPath = path.join(path.dirname(keyPath), 'ota-standby-key.pub.pem');
477
+ let standbyPem = null;
478
+ let standbyIsNew = false;
479
+ if (projectId) {
480
+ try {
481
+ if (!fs.existsSync(standbyPath)) {
482
+ // Asked, not assumed — but the default is yes, and saying no
483
+ // has a cost that only shows up much later: the anchor is
484
+ // compiled into the binary, so a key the app does not already
485
+ // carry can never be added over the air. With one key, rotating
486
+ // (or recovering from a leak) means shipping a native build and
487
+ // waiting for adoption. `doctor` warns about exactly this.
488
+ const sb = (await ask(' Add a standby key for rotation? [Y/n]: ')).trim().toLowerCase();
489
+ if (sb !== 'n') {
490
+ const standby = (0, signing_1.generateRsaKeyPair)();
491
+ fs.writeFileSync(standbyPath, standby.privatePem, { mode: 0o600 });
492
+ (0, credentials_1.restrictToCurrentUser)(standbyPath, 'your OTA standby signing key');
493
+ fs.writeFileSync(standbyPubPath, standby.publicPem);
494
+ standbyPem = standby.publicPem;
495
+ standbyIsNew = true;
496
+ }
497
+ }
498
+ else if (fs.existsSync(standbyPubPath)) {
499
+ // Already generated on an earlier run. Re-read it so a project
500
+ // that gains an iOS app later still gets the standby embedded.
501
+ standbyPem = fs.readFileSync(standbyPubPath, 'utf8');
502
+ }
503
+ }
504
+ catch (err) {
505
+ console.log(chalk_1.default.yellow(` ⚠️ Could not prepare the standby key: ${err.message}`));
506
+ }
507
+ }
508
+ /** Write active + standby into one platform's anchor. */
509
+ const embedKeys = (write) => {
510
+ const res = write({
511
+ cwd: process.cwd(),
512
+ projectId: projectId,
513
+ keyId,
514
+ publicKeyPem: publicPem,
515
+ });
516
+ if (!standbyPem)
517
+ return res;
518
+ return write({
519
+ cwd: process.cwd(),
520
+ projectId: projectId,
521
+ keyId: standbyId,
522
+ publicKeyPem: standbyPem,
523
+ });
524
+ };
525
+ if (!projectId) {
526
+ console.log(chalk_1.default.dim(' Trust anchor skipped — project id could not be resolved.'));
527
+ }
528
+ else if (!(0, trustAnchor_1.hasAndroidProject)(process.cwd())) {
529
+ console.log(chalk_1.default.dim(' No android/app directory — skipping the Android trust anchor.'));
530
+ }
531
+ else {
532
+ try {
533
+ const res = embedKeys(trustAnchor_1.writeAndroidTrustAnchor);
534
+ console.log(` ✅ Android trust anchor written: ${res.path}`);
535
+ console.log(chalk_1.default.dim(` projectId ${projectId} · trusts ${res.keyIds.join(', ')}` +
536
+ (res.merged ? ' (existing keys preserved)' : '')));
537
+ console.log(chalk_1.default.dim(' Rebuild the app so the device enforces this key.'));
538
+ }
539
+ catch (err) {
540
+ console.log(chalk_1.default.yellow(` ⚠️ Could not write the trust anchor: ${err.message}`));
541
+ }
542
+ }
543
+ // iOS, written rather than described — same reasoning as Android
544
+ // above, plus one failure Android does not have. The file can be
545
+ // correct and still not ship, because Xcode copies only what the
546
+ // target references; the device then builds an empty registry and
547
+ // refuses every release. So the one step the CLI cannot do is the
548
+ // one it has to be loudest about.
549
+ if (!projectId) {
550
+ // Already reported above; nothing useful to add per-platform.
551
+ }
552
+ else if (!(0, trustAnchor_1.hasIosProject)(process.cwd())) {
553
+ console.log(chalk_1.default.dim(' No ios/<App> directory — skipping the iOS trust anchor.'));
554
+ }
555
+ else {
556
+ try {
557
+ const ios = embedKeys(trustAnchor_1.writeIosTrustAnchor);
558
+ console.log(` ✅ iOS trust anchor written: ${ios.path}`);
559
+ console.log(chalk_1.default.dim(` projectId ${projectId} · trusts ${ios.keyIds.join(', ')}` +
560
+ (ios.merged ? ' (existing keys preserved)' : '')));
561
+ if (!ios.inXcodeResources) {
562
+ console.log(chalk_1.default.yellow(` ⚠️ Not yet in the Xcode project. Open the workspace, drag`));
563
+ console.log(chalk_1.default.yellow(` ${trustAnchor_1.IOS_ANCHOR_FILENAME} into the app group and tick your app target`));
564
+ console.log(chalk_1.default.yellow(' so it lands in Build Phases → Copy Bundle Resources.'));
565
+ console.log(chalk_1.default.dim(' Until then the app ships without it and refuses every'));
566
+ console.log(chalk_1.default.dim(' release with UNKNOWN_KEY_ID.'));
567
+ }
568
+ else {
569
+ console.log(chalk_1.default.dim(' Already referenced by the Xcode project — rebuild to enforce it.'));
570
+ }
571
+ }
572
+ catch (err) {
573
+ console.log(chalk_1.default.yellow(` ⚠️ Could not write the iOS trust anchor: ${err.message}`));
574
+ }
575
+ }
576
+ // Printed once, after both platforms: the standby is one key pair
577
+ // embedded in whichever anchors exist, not a per-platform artifact.
578
+ if (standbyIsNew) {
579
+ console.log(` ✅ Standby key embedded too: ${standbyId}`);
580
+ console.log(chalk_1.default.dim(' Not registered, so nothing can sign with it yet. To rotate later:'));
581
+ console.log(chalk_1.default.dim(` scalebun ota keys register --key-id ${standbyId} --public-key ${standbyPubPath}`));
582
+ console.log(chalk_1.default.yellow(` Keep ${standbyPath} offline — it is your recovery key.`));
583
+ }
584
+ console.log(chalk_1.default.dim(' The device verifies every release against this key and refuses the rest.'));
585
+ }
586
+ else {
587
+ console.log(chalk_1.default.dim(' Skipped — bundles will ship on SHA-256 integrity alone (unsigned).'));
588
+ }
589
+ }
590
+ // ── 5. Native wiring ────────────────────────────────────────────────
591
+ // The step this flow always needed: everything before here is account
592
+ // plumbing, but an app whose MainApplication/AppDelegate never resolves
593
+ // the OTA slot installs updates that NEVER RUN. `init` does the wiring;
594
+ // doctor (next step) verifies it. Check first, offer to fix — and if
595
+ // the SDK bin isn't resolvable from here, say exactly what to run
596
+ // instead of failing the whole guided flow.
597
+ step(5, 'Wire the native app for OTA');
598
+ const platforms = ['android', 'ios'].filter((p) => fs.existsSync(path.resolve(p)));
599
+ if (!platforms.length) {
600
+ console.log(chalk_1.default.dim(' No android/ or ios/ directory here — run quickstart from your app root'));
601
+ console.log(chalk_1.default.dim(' to wire the native side, or run `scalebun init android` / `init ios` there.'));
602
+ }
603
+ else {
604
+ const sdkBin = (0, sdkBin_1.resolveSdkBin)();
605
+ if (!sdkBin) {
606
+ console.log(' ⚠️ @scalebun/react-native is not resolvable from this directory, so the');
607
+ console.log(' wiring step cannot run. After installing the SDK, wire it with:');
608
+ for (const p of platforms)
609
+ console.log(chalk_1.default.green(` npx scalebun init ${p}`));
610
+ }
611
+ else {
612
+ for (const p of platforms) {
613
+ // --check verifies without touching files (exit 1 = not wired).
614
+ const wired = (0, sdkBin_1.runSdkBin)(sdkBin, ['init', p, '--check']) === 0;
615
+ if (wired) {
616
+ console.log(` ✅ ${p}: already wired for OTA`);
617
+ continue;
618
+ }
619
+ const yn = (await ask(` ${p} is not wired. Wire it now? [Y/n]: `)).trim().toLowerCase();
620
+ if (yn !== 'n') {
621
+ const code = (0, sdkBin_1.runSdkBin)(sdkBin, ['init', p]);
622
+ console.log(code === 0
623
+ ? ` ✅ ${p}: wired — rebuild the app before testing OTA`
624
+ : ` ⚠️ ${p}: init exited ${code} — fix the message above, or wire manually (doctor prints the exact code)`);
625
+ }
626
+ else {
627
+ console.log(chalk_1.default.dim(` Skipped — updates will install but never run on ${p} until it is wired.`));
628
+ }
629
+ }
630
+ }
631
+ }
632
+ // ── 6. Project health ───────────────────────────────────────────────
633
+ step(6, 'Project health check');
634
+ console.log(chalk_1.default.dim(' Running `scalebun doctor` on the current directory…'));
635
+ await (0, doctor_1.runDoctor)(); // exits on blocking failures, same as the standalone command
636
+ // ── 7. First release ────────────────────────────────────────────────
637
+ step(7, 'Ship your first update');
638
+ console.log(' From your app\'s directory:\n');
639
+ console.log(chalk_1.default.green(` scalebun ota publish --app-id ${appId} --channel production --platform android \\`));
640
+ console.log(chalk_1.default.green(' --release-note "First OTA release"'));
641
+ console.log('\n Then manage it:\n');
642
+ console.log(` ${chalk_1.default.cyan('scalebun ota releases')} --app-id ${appId} ${chalk_1.default.dim('# live state per channel')}`);
643
+ console.log(` ${chalk_1.default.cyan('scalebun ota rollout')} --app-id ${appId} --percent 10 ${chalk_1.default.dim('# staged rollout')}`);
644
+ console.log(` ${chalk_1.default.cyan('scalebun ota pause')} --app-id ${appId} ${chalk_1.default.dim('# stop new deliveries')}`);
645
+ console.log(` ${chalk_1.default.cyan('scalebun ota rollback')} --app-id ${appId} ${chalk_1.default.dim('# revert every device')}`);
646
+ console.log(` ${chalk_1.default.cyan('scalebun ci init')} ${chalk_1.default.dim('# generate a CI workflow (GitHub/GitLab)')}`);
647
+ console.log(`\n Dashboard: releases, adoption and rollback analytics live under ${chalk_1.default.bold('OTA')} for ${chalk_1.default.bold(appLabel || appId)}.`);
648
+ // Say what to do next, and be specific about the cost of skipping it.
649
+ // Without a signing key a release still ships — SHA-256 proves the bytes
650
+ // arrived intact, but nothing proves WHO produced them, so a device will
651
+ // accept any bundle whose hash matches what the server declared.
652
+ if (!signingReady) {
653
+ console.log(chalk_1.default.yellow(`\n ⚠️ No signing key is set up for this app.`));
654
+ console.log(' Releases will ship on SHA-256 integrity alone: the bytes are checked,');
655
+ console.log(' but nothing proves who signed them. Set one up next — one command,');
656
+ console.log(' and it registers and proves ownership for you:');
657
+ console.log(chalk_1.default.green(`\n scalebun ota keys generate --app-id ${appId}`));
658
+ console.log(chalk_1.default.dim('\n Then rebuild the app: the public key is compiled into the binary,'));
659
+ console.log(chalk_1.default.dim(' so it cannot be delivered over the air.'));
660
+ }
661
+ console.log('');
662
+ }
663
+ catch (err) {
664
+ console.error(`\n❌ Quickstart stopped: ${err?.message ?? err}`);
665
+ process.exit(1);
666
+ }
667
+ finally {
668
+ rl.close();
669
+ }
670
+ });
671
+ }
672
+ //# sourceMappingURL=quickstart.js.map