@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.
- package/bin/scalebun.js +2 -0
- package/lib/api/client.d.ts +52 -0
- package/lib/api/client.d.ts.map +1 -0
- package/lib/api/client.js +335 -0
- package/lib/api/client.js.map +1 -0
- package/lib/commands/ci-init.d.ts +3 -0
- package/lib/commands/ci-init.d.ts.map +1 -0
- package/lib/commands/ci-init.js +348 -0
- package/lib/commands/ci-init.js.map +1 -0
- package/lib/commands/doctor.d.ts +31 -0
- package/lib/commands/doctor.d.ts.map +1 -0
- package/lib/commands/doctor.js +559 -0
- package/lib/commands/doctor.js.map +1 -0
- package/lib/commands/launch.d.ts +3 -0
- package/lib/commands/launch.d.ts.map +1 -0
- package/lib/commands/launch.js +23 -0
- package/lib/commands/launch.js.map +1 -0
- package/lib/commands/link.d.ts +16 -0
- package/lib/commands/link.d.ts.map +1 -0
- package/lib/commands/link.js +164 -0
- package/lib/commands/link.js.map +1 -0
- package/lib/commands/login.d.ts +12 -0
- package/lib/commands/login.d.ts.map +1 -0
- package/lib/commands/login.js +124 -0
- package/lib/commands/login.js.map +1 -0
- package/lib/commands/migrate-codepush.d.ts +13 -0
- package/lib/commands/migrate-codepush.d.ts.map +1 -0
- package/lib/commands/migrate-codepush.js +98 -0
- package/lib/commands/migrate-codepush.js.map +1 -0
- package/lib/commands/ota-bundle.d.ts +22 -0
- package/lib/commands/ota-bundle.d.ts.map +1 -0
- package/lib/commands/ota-bundle.js +253 -0
- package/lib/commands/ota-bundle.js.map +1 -0
- package/lib/commands/ota-channels.d.ts +3 -0
- package/lib/commands/ota-channels.d.ts.map +1 -0
- package/lib/commands/ota-channels.js +231 -0
- package/lib/commands/ota-channels.js.map +1 -0
- package/lib/commands/ota-delete-bundle.d.ts +16 -0
- package/lib/commands/ota-delete-bundle.d.ts.map +1 -0
- package/lib/commands/ota-delete-bundle.js +96 -0
- package/lib/commands/ota-delete-bundle.js.map +1 -0
- package/lib/commands/ota-inspect.d.ts +3 -0
- package/lib/commands/ota-inspect.d.ts.map +1 -0
- package/lib/commands/ota-inspect.js +258 -0
- package/lib/commands/ota-inspect.js.map +1 -0
- package/lib/commands/ota-keys.d.ts +91 -0
- package/lib/commands/ota-keys.d.ts.map +1 -0
- package/lib/commands/ota-keys.js +765 -0
- package/lib/commands/ota-keys.js.map +1 -0
- package/lib/commands/ota-promote.d.ts +3 -0
- package/lib/commands/ota-promote.d.ts.map +1 -0
- package/lib/commands/ota-promote.js +194 -0
- package/lib/commands/ota-promote.js.map +1 -0
- package/lib/commands/ota-publish.d.ts +26 -0
- package/lib/commands/ota-publish.d.ts.map +1 -0
- package/lib/commands/ota-publish.js +892 -0
- package/lib/commands/ota-publish.js.map +1 -0
- package/lib/commands/ota-rollout.d.ts +3 -0
- package/lib/commands/ota-rollout.d.ts.map +1 -0
- package/lib/commands/ota-rollout.js +265 -0
- package/lib/commands/ota-rollout.js.map +1 -0
- package/lib/commands/ota-sign.d.ts +19 -0
- package/lib/commands/ota-sign.d.ts.map +1 -0
- package/lib/commands/ota-sign.js +245 -0
- package/lib/commands/ota-sign.js.map +1 -0
- package/lib/commands/push-apns.d.ts +3 -0
- package/lib/commands/push-apns.d.ts.map +1 -0
- package/lib/commands/push-apns.js +166 -0
- package/lib/commands/push-apns.js.map +1 -0
- package/lib/commands/quickstart.d.ts +4 -0
- package/lib/commands/quickstart.d.ts.map +1 -0
- package/lib/commands/quickstart.js +672 -0
- package/lib/commands/quickstart.js.map +1 -0
- package/lib/commands/shell.d.ts +28 -0
- package/lib/commands/shell.d.ts.map +1 -0
- package/lib/commands/shell.js +512 -0
- package/lib/commands/shell.js.map +1 -0
- package/lib/commands/tls-pins.d.ts +27 -0
- package/lib/commands/tls-pins.d.ts.map +1 -0
- package/lib/commands/tls-pins.js +209 -0
- package/lib/commands/tls-pins.js.map +1 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +233 -0
- package/lib/index.js.map +1 -0
- package/lib/ui/banner.d.ts +23 -0
- package/lib/ui/banner.d.ts.map +1 -0
- package/lib/ui/banner.js +162 -0
- package/lib/ui/banner.js.map +1 -0
- package/lib/ui/graphics.d.ts +18 -0
- package/lib/ui/graphics.d.ts.map +1 -0
- package/lib/ui/graphics.js +134 -0
- package/lib/ui/graphics.js.map +1 -0
- package/lib/ui/mark.d.ts +4 -0
- package/lib/ui/mark.d.ts.map +1 -0
- package/lib/ui/mark.js +21 -0
- package/lib/ui/mark.js.map +1 -0
- package/lib/ui/menu.d.ts +109 -0
- package/lib/ui/menu.d.ts.map +1 -0
- package/lib/ui/menu.js +314 -0
- package/lib/ui/menu.js.map +1 -0
- package/lib/ui/sixel.d.ts +6 -0
- package/lib/ui/sixel.d.ts.map +1 -0
- package/lib/ui/sixel.js +9 -0
- package/lib/ui/sixel.js.map +1 -0
- package/lib/utils/apps.d.ts +29 -0
- package/lib/utils/apps.d.ts.map +1 -0
- package/lib/utils/apps.js +19 -0
- package/lib/utils/apps.js.map +1 -0
- package/lib/utils/browser.d.ts +9 -0
- package/lib/utils/browser.d.ts.map +1 -0
- package/lib/utils/browser.js +39 -0
- package/lib/utils/browser.js.map +1 -0
- package/lib/utils/bundle.d.ts +153 -0
- package/lib/utils/bundle.d.ts.map +1 -0
- package/lib/utils/bundle.js +459 -0
- package/lib/utils/bundle.js.map +1 -0
- package/lib/utils/bundleStore.d.ts +128 -0
- package/lib/utils/bundleStore.d.ts.map +1 -0
- package/lib/utils/bundleStore.js +250 -0
- package/lib/utils/bundleStore.js.map +1 -0
- package/lib/utils/context.d.ts +88 -0
- package/lib/utils/context.d.ts.map +1 -0
- package/lib/utils/context.js +247 -0
- package/lib/utils/context.js.map +1 -0
- package/lib/utils/credentials.d.ts +59 -0
- package/lib/utils/credentials.d.ts.map +1 -0
- package/lib/utils/credentials.js +147 -0
- package/lib/utils/credentials.js.map +1 -0
- package/lib/utils/guide.d.ts +63 -0
- package/lib/utils/guide.d.ts.map +1 -0
- package/lib/utils/guide.js +191 -0
- package/lib/utils/guide.js.map +1 -0
- package/lib/utils/hash.d.ts +3 -0
- package/lib/utils/hash.d.ts.map +1 -0
- package/lib/utils/hash.js +44 -0
- package/lib/utils/hash.js.map +1 -0
- package/lib/utils/native-id.d.ts +17 -0
- package/lib/utils/native-id.d.ts.map +1 -0
- package/lib/utils/native-id.js +105 -0
- package/lib/utils/native-id.js.map +1 -0
- package/lib/utils/output.d.ts +96 -0
- package/lib/utils/output.d.ts.map +1 -0
- package/lib/utils/output.js +135 -0
- package/lib/utils/output.js.map +1 -0
- package/lib/utils/prompt.d.ts +106 -0
- package/lib/utils/prompt.d.ts.map +1 -0
- package/lib/utils/prompt.js +457 -0
- package/lib/utils/prompt.js.map +1 -0
- package/lib/utils/provenance.d.ts +30 -0
- package/lib/utils/provenance.d.ts.map +1 -0
- package/lib/utils/provenance.js +157 -0
- package/lib/utils/provenance.js.map +1 -0
- package/lib/utils/resolve.d.ts +81 -0
- package/lib/utils/resolve.d.ts.map +1 -0
- package/lib/utils/resolve.js +139 -0
- package/lib/utils/resolve.js.map +1 -0
- package/lib/utils/sdkBin.d.ts +18 -0
- package/lib/utils/sdkBin.d.ts.map +1 -0
- package/lib/utils/sdkBin.js +76 -0
- package/lib/utils/sdkBin.js.map +1 -0
- package/lib/utils/signer.d.ts +94 -0
- package/lib/utils/signer.d.ts.map +1 -0
- package/lib/utils/signer.js +230 -0
- package/lib/utils/signer.js.map +1 -0
- package/lib/utils/signing.d.ts +102 -0
- package/lib/utils/signing.d.ts.map +1 -0
- package/lib/utils/signing.js +229 -0
- package/lib/utils/signing.js.map +1 -0
- package/lib/utils/transportSecurity.d.ts +36 -0
- package/lib/utils/transportSecurity.d.ts.map +1 -0
- package/lib/utils/transportSecurity.js +74 -0
- package/lib/utils/transportSecurity.js.map +1 -0
- package/lib/utils/trustAnchor.d.ts +109 -0
- package/lib/utils/trustAnchor.d.ts.map +1 -0
- package/lib/utils/trustAnchor.js +169 -0
- package/lib/utils/trustAnchor.js.map +1 -0
- package/lib/utils/ui.d.ts +12 -0
- package/lib/utils/ui.d.ts.map +1 -0
- package/lib/utils/ui.js +60 -0
- package/lib/utils/ui.js.map +1 -0
- package/lib/utils/validate.d.ts +62 -0
- package/lib/utils/validate.d.ts.map +1 -0
- package/lib/utils/validate.js +189 -0
- package/lib/utils/validate.js.map +1 -0
- package/lib/vendor/ota-protocol/base64.d.ts +11 -0
- package/lib/vendor/ota-protocol/base64.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/base64.js +39 -0
- package/lib/vendor/ota-protocol/base64.js.map +1 -0
- package/lib/vendor/ota-protocol/codec.d.ts +26 -0
- package/lib/vendor/ota-protocol/codec.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/codec.js +349 -0
- package/lib/vendor/ota-protocol/codec.js.map +1 -0
- package/lib/vendor/ota-protocol/hex.d.ts +8 -0
- package/lib/vendor/ota-protocol/hex.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/hex.js +31 -0
- package/lib/vendor/ota-protocol/hex.js.map +1 -0
- package/lib/vendor/ota-protocol/index.d.ts +17 -0
- package/lib/vendor/ota-protocol/index.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/index.js +41 -0
- package/lib/vendor/ota-protocol/index.js.map +1 -0
- package/lib/vendor/ota-protocol/limits.d.ts +39 -0
- package/lib/vendor/ota-protocol/limits.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/limits.js +42 -0
- package/lib/vendor/ota-protocol/limits.js.map +1 -0
- package/lib/vendor/ota-protocol/types.d.ts +73 -0
- package/lib/vendor/ota-protocol/types.d.ts.map +1 -0
- package/lib/vendor/ota-protocol/types.js +24 -0
- package/lib/vendor/ota-protocol/types.js.map +1 -0
- 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
|