swiftpatch-cli 1.0.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/README.md +275 -0
- package/bin/orbit-plus +3 -0
- package/bin/swiftpatch +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4942 -0
- package/dist/index.js.map +1 -0
- package/package.json +89 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4942 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/lib/config.ts
|
|
22
|
+
import Conf from "conf";
|
|
23
|
+
var store, config;
|
|
24
|
+
var init_config = __esm({
|
|
25
|
+
"src/lib/config.ts"() {
|
|
26
|
+
"use strict";
|
|
27
|
+
init_esm_shims();
|
|
28
|
+
store = new Conf({
|
|
29
|
+
projectName: "swiftpatch-cli-config"
|
|
30
|
+
});
|
|
31
|
+
config = {
|
|
32
|
+
get(key) {
|
|
33
|
+
return store.get(key);
|
|
34
|
+
},
|
|
35
|
+
set(key, value) {
|
|
36
|
+
store.set(key, value);
|
|
37
|
+
},
|
|
38
|
+
delete(key) {
|
|
39
|
+
store.delete(key);
|
|
40
|
+
},
|
|
41
|
+
getAll() {
|
|
42
|
+
return store.store;
|
|
43
|
+
},
|
|
44
|
+
clear() {
|
|
45
|
+
store.clear();
|
|
46
|
+
},
|
|
47
|
+
get path() {
|
|
48
|
+
return store.path;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/lib/api.ts
|
|
55
|
+
var api_exports = {};
|
|
56
|
+
__export(api_exports, {
|
|
57
|
+
api: () => api
|
|
58
|
+
});
|
|
59
|
+
import axios from "axios";
|
|
60
|
+
function warnIfInsecure(url) {
|
|
61
|
+
if (url.startsWith("http://") && !url.includes("localhost") && !url.includes("127.0.0.1")) {
|
|
62
|
+
console.warn(
|
|
63
|
+
"\x1B[33m\u26A0 Warning: API URL uses HTTP instead of HTTPS. This is insecure for production use.\x1B[0m"
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function normalizeIds(obj) {
|
|
68
|
+
if (obj === null || obj === void 0) return obj;
|
|
69
|
+
if (Array.isArray(obj)) return obj.map(normalizeIds);
|
|
70
|
+
if (typeof obj === "object") {
|
|
71
|
+
const result = {};
|
|
72
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
73
|
+
if (key === "_id") {
|
|
74
|
+
result["id"] = value;
|
|
75
|
+
} else if (key === "__v") {
|
|
76
|
+
continue;
|
|
77
|
+
} else {
|
|
78
|
+
result[key] = normalizeIds(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
return obj;
|
|
84
|
+
}
|
|
85
|
+
var API_BASE_URL, ApiClient, api;
|
|
86
|
+
var init_api = __esm({
|
|
87
|
+
"src/lib/api.ts"() {
|
|
88
|
+
"use strict";
|
|
89
|
+
init_esm_shims();
|
|
90
|
+
init_auth();
|
|
91
|
+
init_config();
|
|
92
|
+
API_BASE_URL = process.env.SWIFTPATCH_API_URL || "https://swiftpatch.hyperbrainlabs.com/api/v1";
|
|
93
|
+
ApiClient = class {
|
|
94
|
+
client;
|
|
95
|
+
constructor() {
|
|
96
|
+
const baseURL = config.get("apiUrl") || API_BASE_URL;
|
|
97
|
+
warnIfInsecure(baseURL);
|
|
98
|
+
this.client = axios.create({
|
|
99
|
+
baseURL,
|
|
100
|
+
timeout: 3e4,
|
|
101
|
+
headers: {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
"User-Agent": "swiftpatch-cli/1.0.0"
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.client.interceptors.request.use((reqConfig) => {
|
|
107
|
+
const authHeaders = auth.getAuthHeader();
|
|
108
|
+
Object.assign(reqConfig.headers, authHeaders);
|
|
109
|
+
return reqConfig;
|
|
110
|
+
});
|
|
111
|
+
this.client.interceptors.response.use(
|
|
112
|
+
(response) => response,
|
|
113
|
+
(error) => {
|
|
114
|
+
if (error.response) {
|
|
115
|
+
const data = error.response.data;
|
|
116
|
+
const message = data?.error?.message || error.message;
|
|
117
|
+
if (error.response.status === 401) {
|
|
118
|
+
throw new Error("Authentication failed. Please run: swiftpatch login");
|
|
119
|
+
}
|
|
120
|
+
throw new Error(message);
|
|
121
|
+
}
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
// AUTH
|
|
127
|
+
async createLoginSession() {
|
|
128
|
+
const { data } = await this.client.post("/auth/cli/session");
|
|
129
|
+
return normalizeIds(data.data);
|
|
130
|
+
}
|
|
131
|
+
async checkLoginSession(sessionId) {
|
|
132
|
+
const { data } = await this.client.get(`/auth/cli/session/${sessionId}`);
|
|
133
|
+
return normalizeIds(data.data);
|
|
134
|
+
}
|
|
135
|
+
// USER
|
|
136
|
+
async getCurrentUser() {
|
|
137
|
+
const { data } = await this.client.get("/auth/me");
|
|
138
|
+
return normalizeIds(data.data);
|
|
139
|
+
}
|
|
140
|
+
async getOrganizations() {
|
|
141
|
+
const { data } = await this.client.get("/orgs");
|
|
142
|
+
return normalizeIds(data.data);
|
|
143
|
+
}
|
|
144
|
+
// APPS (org-scoped)
|
|
145
|
+
async getApps(orgId) {
|
|
146
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps`);
|
|
147
|
+
return normalizeIds(data.data);
|
|
148
|
+
}
|
|
149
|
+
async getApp(orgId, appId) {
|
|
150
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}`);
|
|
151
|
+
return normalizeIds(data.data);
|
|
152
|
+
}
|
|
153
|
+
async createApp(orgId, params) {
|
|
154
|
+
const { data } = await this.client.post(`/orgs/${orgId}/apps`, params);
|
|
155
|
+
return normalizeIds(data.data);
|
|
156
|
+
}
|
|
157
|
+
async updateApp(orgId, appId, params) {
|
|
158
|
+
const { data } = await this.client.patch(`/orgs/${orgId}/apps/${appId}`, params);
|
|
159
|
+
return normalizeIds(data.data);
|
|
160
|
+
}
|
|
161
|
+
async deleteApp(orgId, appId) {
|
|
162
|
+
await this.client.delete(`/orgs/${orgId}/apps/${appId}`);
|
|
163
|
+
}
|
|
164
|
+
// RELEASES (org-scoped)
|
|
165
|
+
async getReleases(orgId, appId, params) {
|
|
166
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/releases`, { params });
|
|
167
|
+
const normalized = normalizeIds(data);
|
|
168
|
+
return {
|
|
169
|
+
releases: normalized.data || [],
|
|
170
|
+
pagination: normalized.pagination
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async getRelease(orgId, appId, releaseId) {
|
|
174
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/releases/${releaseId}`);
|
|
175
|
+
return normalizeIds(data.data?.release || data.data);
|
|
176
|
+
}
|
|
177
|
+
async createRelease(orgId, appId, params) {
|
|
178
|
+
const { data } = await this.client.post(`/orgs/${orgId}/apps/${appId}/releases`, params);
|
|
179
|
+
return normalizeIds(data.data?.release || data.data);
|
|
180
|
+
}
|
|
181
|
+
async completeUpload(orgId, appId, releaseId, params) {
|
|
182
|
+
const { data } = await this.client.post(
|
|
183
|
+
`/orgs/${orgId}/apps/${appId}/releases/${releaseId}/upload-complete`,
|
|
184
|
+
params
|
|
185
|
+
);
|
|
186
|
+
return normalizeIds(data.data?.release || data.data);
|
|
187
|
+
}
|
|
188
|
+
async publishRelease(orgId, appId, releaseId, params) {
|
|
189
|
+
const { data } = await this.client.post(
|
|
190
|
+
`/orgs/${orgId}/apps/${appId}/releases/${releaseId}/publish`,
|
|
191
|
+
params
|
|
192
|
+
);
|
|
193
|
+
return normalizeIds(data.data?.release || data.data);
|
|
194
|
+
}
|
|
195
|
+
async updateRollout(orgId, appId, releaseId, rolloutPercent) {
|
|
196
|
+
const { data } = await this.client.patch(
|
|
197
|
+
`/orgs/${orgId}/apps/${appId}/releases/${releaseId}/rollout`,
|
|
198
|
+
{ rolloutPercent }
|
|
199
|
+
);
|
|
200
|
+
return normalizeIds(data.data?.release || data.data);
|
|
201
|
+
}
|
|
202
|
+
async disableRelease(orgId, appId, releaseId, reason) {
|
|
203
|
+
const { data } = await this.client.post(
|
|
204
|
+
`/orgs/${orgId}/apps/${appId}/releases/${releaseId}/disable`,
|
|
205
|
+
{ reason }
|
|
206
|
+
);
|
|
207
|
+
return normalizeIds(data.data?.release || data.data);
|
|
208
|
+
}
|
|
209
|
+
async rollbackRelease(orgId, appId, releaseId, reason) {
|
|
210
|
+
const { data } = await this.client.post(
|
|
211
|
+
`/orgs/${orgId}/apps/${appId}/releases/${releaseId}/rollback`,
|
|
212
|
+
{ reason }
|
|
213
|
+
);
|
|
214
|
+
return normalizeIds(data.data?.release || data.data);
|
|
215
|
+
}
|
|
216
|
+
// CHANNELS (org-scoped)
|
|
217
|
+
async getChannels(orgId, appId) {
|
|
218
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/channels`);
|
|
219
|
+
return normalizeIds(data.data);
|
|
220
|
+
}
|
|
221
|
+
async createChannel(orgId, appId, params) {
|
|
222
|
+
const { data } = await this.client.post(`/orgs/${orgId}/apps/${appId}/channels`, params);
|
|
223
|
+
return normalizeIds(data.data);
|
|
224
|
+
}
|
|
225
|
+
async updateChannel(orgId, appId, channelId, params) {
|
|
226
|
+
const { data } = await this.client.patch(`/orgs/${orgId}/apps/${appId}/channels/${channelId}`, params);
|
|
227
|
+
return normalizeIds(data.data);
|
|
228
|
+
}
|
|
229
|
+
async deleteChannel(orgId, appId, channelId) {
|
|
230
|
+
await this.client.delete(`/orgs/${orgId}/apps/${appId}/channels/${channelId}`);
|
|
231
|
+
}
|
|
232
|
+
// ANALYTICS (org-scoped)
|
|
233
|
+
async getAnalytics(orgId, appId, params) {
|
|
234
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/analytics/overview`, { params });
|
|
235
|
+
return normalizeIds(data.data);
|
|
236
|
+
}
|
|
237
|
+
// ── AI ENDPOINTS ────────────────────────────────────────────────
|
|
238
|
+
getBaseUrl() {
|
|
239
|
+
return this.client.defaults.baseURL || API_BASE_URL;
|
|
240
|
+
}
|
|
241
|
+
getAuthHeaders() {
|
|
242
|
+
return auth.getAuthHeader();
|
|
243
|
+
}
|
|
244
|
+
async aiRiskAssessment(orgId, appId, releaseId) {
|
|
245
|
+
const { data } = await this.client.post(
|
|
246
|
+
`/orgs/${orgId}/apps/${appId}/releases/${releaseId}/ai/risk-assessment`
|
|
247
|
+
);
|
|
248
|
+
return normalizeIds(data.data);
|
|
249
|
+
}
|
|
250
|
+
async aiCrashGroups(orgId, appId, params) {
|
|
251
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/ai/crash-groups`, { params });
|
|
252
|
+
return normalizeIds(data.data);
|
|
253
|
+
}
|
|
254
|
+
async aiInsights(orgId, appId, params) {
|
|
255
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/ai/insights`, { params });
|
|
256
|
+
return normalizeIds(data.data);
|
|
257
|
+
}
|
|
258
|
+
// ── CLAUDE AI KEY (platform-provisioned) ──────────────────────
|
|
259
|
+
async getClaudeApiKey() {
|
|
260
|
+
const { data } = await this.client.get("/ai/claude-key");
|
|
261
|
+
return normalizeIds(data.data);
|
|
262
|
+
}
|
|
263
|
+
// ── CLI ENDPOINTS ──────────────────────────────────────────────
|
|
264
|
+
/**
|
|
265
|
+
* Generate a signed S3 URL for bundle upload.
|
|
266
|
+
* Uses /cli/gen-signed-url (user auth) or /cli/ci/gen-signed-url (CI token auth).
|
|
267
|
+
*/
|
|
268
|
+
async generateSignedUrl(params, ciToken) {
|
|
269
|
+
const headers = {};
|
|
270
|
+
const endpoint = ciToken ? "/cli/ci/gen-signed-url" : "/cli/gen-signed-url";
|
|
271
|
+
if (ciToken) {
|
|
272
|
+
headers["x-ci-token"] = ciToken;
|
|
273
|
+
}
|
|
274
|
+
const { data } = await this.client.post(endpoint, params, { headers });
|
|
275
|
+
return normalizeIds(data.data);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Promote a published bundle to a release.
|
|
279
|
+
*/
|
|
280
|
+
async promoteBundle(params, ciToken) {
|
|
281
|
+
const { data } = await this.client.post("/cli/ci/promote", params, {
|
|
282
|
+
headers: { "x-ci-token": ciToken }
|
|
283
|
+
});
|
|
284
|
+
return normalizeIds(data.data);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Update an existing release via CLI.
|
|
288
|
+
*/
|
|
289
|
+
async updateReleaseCli(params, ciToken) {
|
|
290
|
+
const { data } = await this.client.post("/cli/ci/update-release", params, {
|
|
291
|
+
headers: { "x-ci-token": ciToken }
|
|
292
|
+
});
|
|
293
|
+
return normalizeIds(data.data);
|
|
294
|
+
}
|
|
295
|
+
// ── CI TOKENS ─────────────────────────────────────────────────
|
|
296
|
+
async getCITokens(orgId, appId) {
|
|
297
|
+
const { data } = await this.client.get(`/orgs/${orgId}/apps/${appId}/ci-tokens`);
|
|
298
|
+
return normalizeIds(data.data);
|
|
299
|
+
}
|
|
300
|
+
async createCIToken(orgId, appId, name) {
|
|
301
|
+
const { data } = await this.client.post(`/orgs/${orgId}/apps/${appId}/ci-tokens`, { name });
|
|
302
|
+
return normalizeIds(data.data);
|
|
303
|
+
}
|
|
304
|
+
async deleteCIToken(orgId, appId, tokenId) {
|
|
305
|
+
await this.client.delete(`/orgs/${orgId}/apps/${appId}/ci-tokens/${tokenId}`);
|
|
306
|
+
}
|
|
307
|
+
async regenerateCIToken(orgId, appId, tokenId) {
|
|
308
|
+
const { data } = await this.client.post(`/orgs/${orgId}/apps/${appId}/ci-tokens/${tokenId}/regenerate`);
|
|
309
|
+
return normalizeIds(data.data);
|
|
310
|
+
}
|
|
311
|
+
// ── WEBHOOKS ──────────────────────────────────────────────────
|
|
312
|
+
async getWebhooks(orgId) {
|
|
313
|
+
const { data } = await this.client.get(`/orgs/${orgId}/webhooks`);
|
|
314
|
+
return normalizeIds(data.data);
|
|
315
|
+
}
|
|
316
|
+
async createWebhook(orgId, params) {
|
|
317
|
+
const { data } = await this.client.post(`/orgs/${orgId}/webhooks`, params);
|
|
318
|
+
return normalizeIds(data.data);
|
|
319
|
+
}
|
|
320
|
+
async updateWebhook(orgId, webhookId, params) {
|
|
321
|
+
const { data } = await this.client.patch(`/orgs/${orgId}/webhooks/${webhookId}`, params);
|
|
322
|
+
return normalizeIds(data.data);
|
|
323
|
+
}
|
|
324
|
+
async deleteWebhook(orgId, webhookId) {
|
|
325
|
+
await this.client.delete(`/orgs/${orgId}/webhooks/${webhookId}`);
|
|
326
|
+
}
|
|
327
|
+
async testWebhook(orgId, webhookId) {
|
|
328
|
+
const { data } = await this.client.post(`/orgs/${orgId}/webhooks/${webhookId}/test`);
|
|
329
|
+
return normalizeIds(data.data);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
api = new ApiClient();
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// src/lib/auth.ts
|
|
337
|
+
import Conf2 from "conf";
|
|
338
|
+
import crypto from "crypto";
|
|
339
|
+
import os from "os";
|
|
340
|
+
function deriveEncryptionKey() {
|
|
341
|
+
const material = [
|
|
342
|
+
os.hostname(),
|
|
343
|
+
os.userInfo().username,
|
|
344
|
+
os.homedir(),
|
|
345
|
+
"swiftpatch-cli"
|
|
346
|
+
].join(":");
|
|
347
|
+
return crypto.createHash("sha256").update(material).digest("hex");
|
|
348
|
+
}
|
|
349
|
+
function createStore() {
|
|
350
|
+
try {
|
|
351
|
+
return new Conf2({
|
|
352
|
+
projectName: "swiftpatch-cli",
|
|
353
|
+
encryptionKey: deriveEncryptionKey()
|
|
354
|
+
});
|
|
355
|
+
} catch {
|
|
356
|
+
const fresh = new Conf2({
|
|
357
|
+
projectName: "swiftpatch-cli",
|
|
358
|
+
encryptionKey: deriveEncryptionKey()
|
|
359
|
+
});
|
|
360
|
+
fresh.clear();
|
|
361
|
+
return fresh;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
var store2, auth;
|
|
365
|
+
var init_auth = __esm({
|
|
366
|
+
"src/lib/auth.ts"() {
|
|
367
|
+
"use strict";
|
|
368
|
+
init_esm_shims();
|
|
369
|
+
store2 = createStore();
|
|
370
|
+
auth = {
|
|
371
|
+
isLoggedIn() {
|
|
372
|
+
return !!(store2.get("token") || store2.get("apiKey"));
|
|
373
|
+
},
|
|
374
|
+
getToken() {
|
|
375
|
+
return store2.get("token") || store2.get("apiKey");
|
|
376
|
+
},
|
|
377
|
+
getAuthHeader() {
|
|
378
|
+
const token = this.getToken();
|
|
379
|
+
if (!token) return {};
|
|
380
|
+
return { Authorization: `Bearer ${token}` };
|
|
381
|
+
},
|
|
382
|
+
saveToken(token) {
|
|
383
|
+
store2.set("token", token);
|
|
384
|
+
store2.delete("apiKey");
|
|
385
|
+
},
|
|
386
|
+
async loginWithApiKey(apiKey) {
|
|
387
|
+
const originalToken = store2.get("token");
|
|
388
|
+
const originalApiKey = store2.get("apiKey");
|
|
389
|
+
store2.set("apiKey", apiKey);
|
|
390
|
+
store2.delete("token");
|
|
391
|
+
try {
|
|
392
|
+
const { api: api2 } = await Promise.resolve().then(() => (init_api(), api_exports));
|
|
393
|
+
await api2.getCurrentUser();
|
|
394
|
+
return true;
|
|
395
|
+
} catch (error) {
|
|
396
|
+
if (originalToken) store2.set("token", originalToken);
|
|
397
|
+
else store2.delete("token");
|
|
398
|
+
if (originalApiKey) store2.set("apiKey", originalApiKey);
|
|
399
|
+
else store2.delete("apiKey");
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
},
|
|
403
|
+
saveUser(user) {
|
|
404
|
+
store2.set("user", user);
|
|
405
|
+
},
|
|
406
|
+
getUser() {
|
|
407
|
+
return store2.get("user");
|
|
408
|
+
},
|
|
409
|
+
logout() {
|
|
410
|
+
store2.delete("token");
|
|
411
|
+
store2.delete("apiKey");
|
|
412
|
+
store2.delete("user");
|
|
413
|
+
},
|
|
414
|
+
getClaudeApiKey() {
|
|
415
|
+
return store2.get("claudeApiKey");
|
|
416
|
+
},
|
|
417
|
+
setClaudeApiKey(key) {
|
|
418
|
+
store2.set("claudeApiKey", key);
|
|
419
|
+
},
|
|
420
|
+
deleteClaudeApiKey() {
|
|
421
|
+
store2.delete("claudeApiKey");
|
|
422
|
+
},
|
|
423
|
+
getConfigPath() {
|
|
424
|
+
return store2.path;
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// src/index.ts
|
|
431
|
+
init_esm_shims();
|
|
432
|
+
|
|
433
|
+
// src/cli.ts
|
|
434
|
+
init_esm_shims();
|
|
435
|
+
import { Command as Command47 } from "commander";
|
|
436
|
+
import chalk45 from "chalk";
|
|
437
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
438
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
439
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
440
|
+
|
|
441
|
+
// src/commands/login.ts
|
|
442
|
+
init_esm_shims();
|
|
443
|
+
init_auth();
|
|
444
|
+
init_api();
|
|
445
|
+
import { Command } from "commander";
|
|
446
|
+
import chalk2 from "chalk";
|
|
447
|
+
import inquirer from "inquirer";
|
|
448
|
+
import open from "open";
|
|
449
|
+
import ora from "ora";
|
|
450
|
+
|
|
451
|
+
// src/utils/logger.ts
|
|
452
|
+
init_esm_shims();
|
|
453
|
+
import chalk from "chalk";
|
|
454
|
+
import figures from "figures";
|
|
455
|
+
var logger = {
|
|
456
|
+
info(message) {
|
|
457
|
+
console.log(chalk.blue(figures.info) + " " + message);
|
|
458
|
+
},
|
|
459
|
+
success(message) {
|
|
460
|
+
console.log(chalk.green(figures.tick) + " " + message);
|
|
461
|
+
},
|
|
462
|
+
warning(message) {
|
|
463
|
+
console.log(chalk.yellow(figures.warning) + " " + message);
|
|
464
|
+
},
|
|
465
|
+
error(message) {
|
|
466
|
+
console.log(chalk.red(figures.cross) + " " + message);
|
|
467
|
+
},
|
|
468
|
+
debug(message) {
|
|
469
|
+
if (process.env.DEBUG) {
|
|
470
|
+
console.log(chalk.gray(figures.pointer) + " " + chalk.gray(message));
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
blank() {
|
|
474
|
+
console.log("");
|
|
475
|
+
},
|
|
476
|
+
divider() {
|
|
477
|
+
console.log(chalk.gray("\u2500".repeat(50)));
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
// src/commands/login.ts
|
|
482
|
+
var loginCommand = new Command("login").description("Authenticate with SwiftPatch").option("-k, --api-key <key>", "Login with API key (for CI/CD)").option("-i, --interactive", "Force interactive login").action(async (options) => {
|
|
483
|
+
const existingToken = auth.getToken();
|
|
484
|
+
if (existingToken && !options.interactive && !options.apiKey) {
|
|
485
|
+
const user = await api.getCurrentUser().catch(() => null);
|
|
486
|
+
if (user) {
|
|
487
|
+
logger.info(`Already logged in as ${chalk2.cyan(user.email)}`);
|
|
488
|
+
const { reauth } = await inquirer.prompt([
|
|
489
|
+
{
|
|
490
|
+
type: "confirm",
|
|
491
|
+
name: "reauth",
|
|
492
|
+
message: "Do you want to log in with a different account?",
|
|
493
|
+
default: false
|
|
494
|
+
}
|
|
495
|
+
]);
|
|
496
|
+
if (!reauth) return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (options.apiKey) {
|
|
500
|
+
const spinner = ora("Validating API key...").start();
|
|
501
|
+
try {
|
|
502
|
+
const isValid = await auth.loginWithApiKey(options.apiKey);
|
|
503
|
+
if (isValid) {
|
|
504
|
+
spinner.succeed("Logged in with API key");
|
|
505
|
+
const user = await api.getCurrentUser();
|
|
506
|
+
logger.success(`Authenticated as ${chalk2.cyan(user.email)}`);
|
|
507
|
+
} else {
|
|
508
|
+
spinner.fail("Invalid API key");
|
|
509
|
+
process.exit(1);
|
|
510
|
+
}
|
|
511
|
+
} catch (error) {
|
|
512
|
+
spinner.fail(`Login failed: ${error.message}`);
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const { method } = await inquirer.prompt([
|
|
518
|
+
{
|
|
519
|
+
type: "list",
|
|
520
|
+
name: "method",
|
|
521
|
+
message: "How would you like to authenticate?",
|
|
522
|
+
choices: [
|
|
523
|
+
{ name: "Login with browser (recommended)", value: "browser" },
|
|
524
|
+
{ name: "Enter API key", value: "apikey" }
|
|
525
|
+
]
|
|
526
|
+
}
|
|
527
|
+
]);
|
|
528
|
+
if (method === "browser") {
|
|
529
|
+
await browserLogin();
|
|
530
|
+
} else {
|
|
531
|
+
await apiKeyLogin();
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
async function browserLogin() {
|
|
535
|
+
const spinner = ora("Opening browser...").start();
|
|
536
|
+
try {
|
|
537
|
+
const { sessionId, loginUrl } = await api.createLoginSession();
|
|
538
|
+
spinner.text = "Waiting for authentication...";
|
|
539
|
+
await open(loginUrl);
|
|
540
|
+
logger.info(`
|
|
541
|
+
If browser doesn't open, visit:
|
|
542
|
+
${chalk2.cyan(loginUrl)}
|
|
543
|
+
`);
|
|
544
|
+
const token = await pollForToken(sessionId, spinner);
|
|
545
|
+
auth.saveToken(token);
|
|
546
|
+
spinner.succeed("Login successful!");
|
|
547
|
+
const user = await api.getCurrentUser();
|
|
548
|
+
console.log("");
|
|
549
|
+
logger.success(`Welcome, ${chalk2.bold(user.name)}!`);
|
|
550
|
+
logger.info(`Email: ${chalk2.cyan(user.email)}`);
|
|
551
|
+
const orgs = await api.getOrganizations();
|
|
552
|
+
if (orgs.length > 0) {
|
|
553
|
+
logger.info(`Organizations: ${orgs.map((o) => chalk2.cyan(o.name)).join(", ")}`);
|
|
554
|
+
}
|
|
555
|
+
} catch (error) {
|
|
556
|
+
spinner.fail(`Login failed: ${error.message}`);
|
|
557
|
+
process.exit(1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
async function pollForToken(sessionId, spinner) {
|
|
561
|
+
const maxAttempts = 60;
|
|
562
|
+
let attempts = 0;
|
|
563
|
+
while (attempts < maxAttempts) {
|
|
564
|
+
try {
|
|
565
|
+
const result = await api.checkLoginSession(sessionId);
|
|
566
|
+
if (result.status === "completed" && result.token) {
|
|
567
|
+
return result.token;
|
|
568
|
+
}
|
|
569
|
+
if (result.status === "expired") {
|
|
570
|
+
throw new Error("Login session expired. Please try again.");
|
|
571
|
+
}
|
|
572
|
+
} catch (error) {
|
|
573
|
+
if (error.message.includes("expired")) {
|
|
574
|
+
throw error;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
await sleep(5e3);
|
|
578
|
+
attempts++;
|
|
579
|
+
const remainingSeconds = (maxAttempts - attempts) * 5;
|
|
580
|
+
const remainingMinutes = Math.floor(remainingSeconds / 60);
|
|
581
|
+
const remainingSecs = remainingSeconds % 60;
|
|
582
|
+
spinner.text = `Waiting for authentication... (${remainingMinutes}m ${remainingSecs}s remaining)`;
|
|
583
|
+
}
|
|
584
|
+
throw new Error("Login timed out. Please try again.");
|
|
585
|
+
}
|
|
586
|
+
async function apiKeyLogin() {
|
|
587
|
+
const { apiKey } = await inquirer.prompt([
|
|
588
|
+
{
|
|
589
|
+
type: "password",
|
|
590
|
+
name: "apiKey",
|
|
591
|
+
message: "Enter your API key:",
|
|
592
|
+
mask: "*",
|
|
593
|
+
validate: (input) => {
|
|
594
|
+
if (!input) return "API key is required";
|
|
595
|
+
if (!input.startsWith("sp_")) return "Invalid API key format";
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
]);
|
|
600
|
+
const spinner = ora("Validating API key...").start();
|
|
601
|
+
try {
|
|
602
|
+
await auth.loginWithApiKey(apiKey);
|
|
603
|
+
spinner.succeed("Login successful!");
|
|
604
|
+
const user = await api.getCurrentUser();
|
|
605
|
+
logger.success(`Authenticated as ${chalk2.cyan(user.email)}`);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
spinner.fail(`Login failed: ${error.message}`);
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
function sleep(ms) {
|
|
612
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/commands/logout.ts
|
|
616
|
+
init_esm_shims();
|
|
617
|
+
init_auth();
|
|
618
|
+
import { Command as Command2 } from "commander";
|
|
619
|
+
import inquirer2 from "inquirer";
|
|
620
|
+
var logoutCommand = new Command2("logout").description("Log out from SwiftPatch").option("-f, --force", "Logout without confirmation").action(async (options) => {
|
|
621
|
+
if (!auth.isLoggedIn()) {
|
|
622
|
+
logger.info("You are not logged in");
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
if (!options.force) {
|
|
626
|
+
const { confirm } = await inquirer2.prompt([
|
|
627
|
+
{
|
|
628
|
+
type: "confirm",
|
|
629
|
+
name: "confirm",
|
|
630
|
+
message: "Are you sure you want to log out?",
|
|
631
|
+
default: false
|
|
632
|
+
}
|
|
633
|
+
]);
|
|
634
|
+
if (!confirm) {
|
|
635
|
+
logger.info("Logout cancelled");
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
auth.logout();
|
|
640
|
+
logger.success("Logged out successfully");
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// src/commands/whoami.ts
|
|
644
|
+
init_esm_shims();
|
|
645
|
+
init_auth();
|
|
646
|
+
init_api();
|
|
647
|
+
import { Command as Command3 } from "commander";
|
|
648
|
+
import chalk3 from "chalk";
|
|
649
|
+
var whoamiCommand = new Command3("whoami").description("Show current authenticated user").action(async () => {
|
|
650
|
+
if (!auth.isLoggedIn()) {
|
|
651
|
+
logger.error("Not logged in. Run " + chalk3.cyan("swiftpatch login") + " first.");
|
|
652
|
+
process.exit(1);
|
|
653
|
+
}
|
|
654
|
+
try {
|
|
655
|
+
const user = await api.getCurrentUser();
|
|
656
|
+
const orgs = await api.getOrganizations();
|
|
657
|
+
console.log("");
|
|
658
|
+
console.log(chalk3.bold(" Current User"));
|
|
659
|
+
console.log("");
|
|
660
|
+
console.log(` Name: ${chalk3.cyan(user.name)}`);
|
|
661
|
+
console.log(` Email: ${chalk3.cyan(user.email)}`);
|
|
662
|
+
console.log(` ID: ${chalk3.gray(user.id)}`);
|
|
663
|
+
console.log("");
|
|
664
|
+
if (orgs.length > 0) {
|
|
665
|
+
console.log(chalk3.bold(" Organizations"));
|
|
666
|
+
console.log("");
|
|
667
|
+
orgs.forEach((org) => {
|
|
668
|
+
console.log(` \u2022 ${chalk3.cyan(org.name)} ${chalk3.gray(`(${org.slug})`)}`);
|
|
669
|
+
console.log(` Plan: ${org.plan} | Apps: ${org.usage.appsCount}`);
|
|
670
|
+
});
|
|
671
|
+
console.log("");
|
|
672
|
+
}
|
|
673
|
+
} catch (error) {
|
|
674
|
+
logger.error(`Failed to get user info: ${error.message}`);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// src/commands/apps/index.ts
|
|
680
|
+
init_esm_shims();
|
|
681
|
+
import { Command as Command9 } from "commander";
|
|
682
|
+
|
|
683
|
+
// src/commands/apps/list.ts
|
|
684
|
+
init_esm_shims();
|
|
685
|
+
init_api();
|
|
686
|
+
import { Command as Command4 } from "commander";
|
|
687
|
+
import chalk7 from "chalk";
|
|
688
|
+
import ora2 from "ora";
|
|
689
|
+
|
|
690
|
+
// src/utils/auth-guard.ts
|
|
691
|
+
init_esm_shims();
|
|
692
|
+
init_auth();
|
|
693
|
+
import chalk4 from "chalk";
|
|
694
|
+
async function requireAuth() {
|
|
695
|
+
if (!auth.isLoggedIn()) {
|
|
696
|
+
logger.error("You must be logged in to run this command");
|
|
697
|
+
console.log("");
|
|
698
|
+
console.log("Run " + chalk4.cyan("swiftpatch login") + " to authenticate");
|
|
699
|
+
console.log("");
|
|
700
|
+
process.exit(1);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/utils/org-resolver.ts
|
|
705
|
+
init_esm_shims();
|
|
706
|
+
init_api();
|
|
707
|
+
init_config();
|
|
708
|
+
import chalk5 from "chalk";
|
|
709
|
+
import inquirer3 from "inquirer";
|
|
710
|
+
async function resolveOrgId(explicitOrgId) {
|
|
711
|
+
if (explicitOrgId) {
|
|
712
|
+
return explicitOrgId;
|
|
713
|
+
}
|
|
714
|
+
const defaultOrg = config.get("defaultOrg");
|
|
715
|
+
if (defaultOrg) {
|
|
716
|
+
return defaultOrg;
|
|
717
|
+
}
|
|
718
|
+
const orgs = await api.getOrganizations();
|
|
719
|
+
if (orgs.length === 0) {
|
|
720
|
+
logger.error("No organizations found. Please create one at https://app.swiftpatch.io");
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
723
|
+
if (orgs.length === 1) {
|
|
724
|
+
return orgs[0].id;
|
|
725
|
+
}
|
|
726
|
+
const { selectedOrg } = await inquirer3.prompt([
|
|
727
|
+
{
|
|
728
|
+
type: "list",
|
|
729
|
+
name: "selectedOrg",
|
|
730
|
+
message: "Select organization:",
|
|
731
|
+
choices: orgs.map((org) => ({
|
|
732
|
+
name: `${org.name} ${chalk5.gray(`(${org.slug})`)}`,
|
|
733
|
+
value: org.id
|
|
734
|
+
}))
|
|
735
|
+
}
|
|
736
|
+
]);
|
|
737
|
+
const { saveDefault } = await inquirer3.prompt([
|
|
738
|
+
{
|
|
739
|
+
type: "confirm",
|
|
740
|
+
name: "saveDefault",
|
|
741
|
+
message: "Save as default organization?",
|
|
742
|
+
default: true
|
|
743
|
+
}
|
|
744
|
+
]);
|
|
745
|
+
if (saveDefault) {
|
|
746
|
+
config.set("defaultOrg", selectedOrg);
|
|
747
|
+
logger.info(`Default organization saved. Change with: ${chalk5.cyan("swiftpatch config set defaultOrg <org-id>")}`);
|
|
748
|
+
}
|
|
749
|
+
return selectedOrg;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// src/utils/table.ts
|
|
753
|
+
init_esm_shims();
|
|
754
|
+
import Table from "cli-table3";
|
|
755
|
+
import chalk6 from "chalk";
|
|
756
|
+
function createTable(options) {
|
|
757
|
+
const tableConfig = {
|
|
758
|
+
head: options.head.map((h) => chalk6.bold(h)),
|
|
759
|
+
style: {
|
|
760
|
+
head: [],
|
|
761
|
+
border: []
|
|
762
|
+
},
|
|
763
|
+
chars: {
|
|
764
|
+
"top": "\u2500",
|
|
765
|
+
"top-mid": "\u252C",
|
|
766
|
+
"top-left": "\u250C",
|
|
767
|
+
"top-right": "\u2510",
|
|
768
|
+
"bottom": "\u2500",
|
|
769
|
+
"bottom-mid": "\u2534",
|
|
770
|
+
"bottom-left": "\u2514",
|
|
771
|
+
"bottom-right": "\u2518",
|
|
772
|
+
"left": "\u2502",
|
|
773
|
+
"left-mid": "\u251C",
|
|
774
|
+
"mid": "\u2500",
|
|
775
|
+
"mid-mid": "\u253C",
|
|
776
|
+
"right": "\u2502",
|
|
777
|
+
"right-mid": "\u2524",
|
|
778
|
+
"middle": "\u2502"
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
if (options.colWidths) {
|
|
782
|
+
tableConfig.colWidths = options.colWidths;
|
|
783
|
+
}
|
|
784
|
+
const table = new Table(tableConfig);
|
|
785
|
+
for (const row of options.rows) {
|
|
786
|
+
table.push(row.map((v) => v == null ? "" : String(v)));
|
|
787
|
+
}
|
|
788
|
+
return table.toString();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// src/commands/apps/list.ts
|
|
792
|
+
var listAppsCommand = new Command4("list").description("List all apps").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
|
|
793
|
+
await requireAuth();
|
|
794
|
+
const orgId = await resolveOrgId(options.org);
|
|
795
|
+
const spinner = ora2("Fetching apps...").start();
|
|
796
|
+
try {
|
|
797
|
+
const apps = await api.getApps(orgId);
|
|
798
|
+
spinner.stop();
|
|
799
|
+
if (options.json) {
|
|
800
|
+
console.log(JSON.stringify(apps, null, 2));
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (apps.length === 0) {
|
|
804
|
+
console.log("");
|
|
805
|
+
console.log(chalk7.gray(" No apps found."));
|
|
806
|
+
console.log("");
|
|
807
|
+
console.log(" Create one with: " + chalk7.cyan("swiftpatch apps create"));
|
|
808
|
+
console.log("");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
console.log("");
|
|
812
|
+
console.log(chalk7.bold(" Your Apps"));
|
|
813
|
+
console.log("");
|
|
814
|
+
const table = createTable({
|
|
815
|
+
head: ["Name", "Platform", "Deployment Key", "Releases"],
|
|
816
|
+
rows: apps.map((app) => [
|
|
817
|
+
chalk7.cyan(app.name || ""),
|
|
818
|
+
app.platform || "",
|
|
819
|
+
chalk7.gray((app.deploymentKey || "").slice(0, 20) + "..."),
|
|
820
|
+
String(app.stats?.totalReleases ?? 0)
|
|
821
|
+
])
|
|
822
|
+
});
|
|
823
|
+
console.log(table);
|
|
824
|
+
console.log("");
|
|
825
|
+
} catch (error) {
|
|
826
|
+
spinner.fail(`Failed to fetch apps: ${error.message}`);
|
|
827
|
+
process.exit(1);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// src/commands/apps/create.ts
|
|
832
|
+
init_esm_shims();
|
|
833
|
+
init_api();
|
|
834
|
+
import { Command as Command5 } from "commander";
|
|
835
|
+
import chalk8 from "chalk";
|
|
836
|
+
import ora3 from "ora";
|
|
837
|
+
var createAppCommand = new Command5("create").description("Create a new app").option("-n, --name <name>", "App name").option("-p, --platform <platform>", "Platform (ios, android, or both)").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
|
|
838
|
+
await requireAuth();
|
|
839
|
+
const orgId = await resolveOrgId(options.org);
|
|
840
|
+
let appName = options.name;
|
|
841
|
+
if (!appName) {
|
|
842
|
+
const inquirer24 = (await import("inquirer")).default;
|
|
843
|
+
const { name } = await inquirer24.prompt([
|
|
844
|
+
{
|
|
845
|
+
type: "input",
|
|
846
|
+
name: "name",
|
|
847
|
+
message: "App name:",
|
|
848
|
+
validate: (input) => {
|
|
849
|
+
if (!input.trim()) return "App name is required";
|
|
850
|
+
if (input.length < 2) return "Name must be at least 2 characters";
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
]);
|
|
855
|
+
appName = name;
|
|
856
|
+
}
|
|
857
|
+
let platform = options.platform?.toUpperCase();
|
|
858
|
+
if (!platform || !["IOS", "ANDROID", "BOTH"].includes(platform)) {
|
|
859
|
+
const inquirer24 = (await import("inquirer")).default;
|
|
860
|
+
const { selectedPlatform } = await inquirer24.prompt([
|
|
861
|
+
{
|
|
862
|
+
type: "list",
|
|
863
|
+
name: "selectedPlatform",
|
|
864
|
+
message: "Select platform:",
|
|
865
|
+
choices: [
|
|
866
|
+
{ name: "iOS", value: "IOS" },
|
|
867
|
+
{ name: "Android", value: "ANDROID" },
|
|
868
|
+
{ name: "Both (iOS & Android)", value: "BOTH" }
|
|
869
|
+
]
|
|
870
|
+
}
|
|
871
|
+
]);
|
|
872
|
+
platform = selectedPlatform;
|
|
873
|
+
}
|
|
874
|
+
const spinner = ora3("Creating app...").start();
|
|
875
|
+
try {
|
|
876
|
+
const app = await api.createApp(orgId, {
|
|
877
|
+
name: appName,
|
|
878
|
+
platform
|
|
879
|
+
});
|
|
880
|
+
spinner.succeed("App created!");
|
|
881
|
+
if (options.json) {
|
|
882
|
+
console.log(JSON.stringify(app, null, 2));
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
console.log("");
|
|
886
|
+
console.log(chalk8.bold(" App Details"));
|
|
887
|
+
console.log("");
|
|
888
|
+
console.log(` Name: ${chalk8.cyan(app.name)}`);
|
|
889
|
+
console.log(` ID: ${chalk8.gray(app.id)}`);
|
|
890
|
+
console.log(` Platform: ${app.platform}`);
|
|
891
|
+
console.log(` Deployment Key: ${chalk8.gray(app.deploymentKey)}`);
|
|
892
|
+
console.log("");
|
|
893
|
+
console.log(chalk8.gray(" Add this to your React Native app:"));
|
|
894
|
+
console.log(` ${chalk8.cyan(`deploymentKey: "${app.deploymentKey}"`)}`);
|
|
895
|
+
console.log("");
|
|
896
|
+
} catch (error) {
|
|
897
|
+
spinner.fail(`Failed to create app: ${error.message}`);
|
|
898
|
+
process.exit(1);
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// src/commands/apps/info.ts
|
|
903
|
+
init_esm_shims();
|
|
904
|
+
init_api();
|
|
905
|
+
import { Command as Command6 } from "commander";
|
|
906
|
+
import chalk9 from "chalk";
|
|
907
|
+
import ora4 from "ora";
|
|
908
|
+
var infoAppCommand = new Command6("info").description("Show app details").argument("<app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
909
|
+
await requireAuth();
|
|
910
|
+
const orgId = await resolveOrgId(options.org);
|
|
911
|
+
const spinner = ora4("Fetching app details...").start();
|
|
912
|
+
try {
|
|
913
|
+
const app = await api.getApp(orgId, appId);
|
|
914
|
+
spinner.stop();
|
|
915
|
+
if (options.json) {
|
|
916
|
+
console.log(JSON.stringify(app, null, 2));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
console.log("");
|
|
920
|
+
console.log(chalk9.bold(" App Details"));
|
|
921
|
+
console.log("");
|
|
922
|
+
console.log(` Name: ${chalk9.cyan(app.name)}`);
|
|
923
|
+
console.log(` ID: ${chalk9.gray(app.id)}`);
|
|
924
|
+
console.log(` Slug: ${chalk9.gray(app.slug)}`);
|
|
925
|
+
console.log(` Platform: ${app.platform}`);
|
|
926
|
+
console.log(` Deployment Key: ${chalk9.gray(app.deploymentKey)}`);
|
|
927
|
+
console.log(` Signing: ${app.signingEnabled ? chalk9.green("Enabled") : chalk9.gray("Disabled")}`);
|
|
928
|
+
const orgDisplay = typeof app.organizationId === "object" && app.organizationId?.name ? `${app.organizationId.name} (${app.organizationId.id || app.organizationId._id})` : app.organizationId;
|
|
929
|
+
console.log(` Organization: ${chalk9.gray(orgDisplay)}`);
|
|
930
|
+
if (app.stats) {
|
|
931
|
+
console.log("");
|
|
932
|
+
console.log(chalk9.bold(" Stats"));
|
|
933
|
+
console.log(` Total Releases: ${app.stats.totalReleases}`);
|
|
934
|
+
console.log(` Total Installs: ${app.stats.totalInstalls}`);
|
|
935
|
+
console.log(` Active Devices: ${app.stats.activeDevices}`);
|
|
936
|
+
}
|
|
937
|
+
console.log("");
|
|
938
|
+
} catch (error) {
|
|
939
|
+
spinner.fail(`Failed to fetch app: ${error.message}`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
// src/commands/apps/update.ts
|
|
945
|
+
init_esm_shims();
|
|
946
|
+
init_api();
|
|
947
|
+
import { Command as Command7 } from "commander";
|
|
948
|
+
import chalk10 from "chalk";
|
|
949
|
+
import inquirer4 from "inquirer";
|
|
950
|
+
import ora5 from "ora";
|
|
951
|
+
var updateAppCommand = new Command7("update").description("Update app settings").argument("<app-id>", "App ID").option("-n, --name <name>", "New app name").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
952
|
+
await requireAuth();
|
|
953
|
+
const orgId = await resolveOrgId(options.org);
|
|
954
|
+
const spinner = ora5("Fetching app details...").start();
|
|
955
|
+
const app = await api.getApp(orgId, appId);
|
|
956
|
+
spinner.stop();
|
|
957
|
+
let name = options.name;
|
|
958
|
+
if (!name) {
|
|
959
|
+
const answers = await inquirer4.prompt([
|
|
960
|
+
{
|
|
961
|
+
type: "input",
|
|
962
|
+
name: "name",
|
|
963
|
+
message: "New app name:",
|
|
964
|
+
default: app.name,
|
|
965
|
+
validate: (input) => {
|
|
966
|
+
if (!input.trim()) return "App name is required";
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
]);
|
|
971
|
+
name = answers.name;
|
|
972
|
+
}
|
|
973
|
+
const updateSpinner = ora5("Updating app...").start();
|
|
974
|
+
try {
|
|
975
|
+
const updated = await api.updateApp(orgId, appId, { name });
|
|
976
|
+
updateSpinner.succeed("App updated!");
|
|
977
|
+
if (options.json) {
|
|
978
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
console.log("");
|
|
982
|
+
console.log(` Name: ${chalk10.cyan(updated.name)}`);
|
|
983
|
+
console.log(` Slug: ${chalk10.gray(updated.slug)}`);
|
|
984
|
+
console.log("");
|
|
985
|
+
} catch (error) {
|
|
986
|
+
updateSpinner.fail(`Failed to update app: ${error.message}`);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
// src/commands/apps/delete.ts
|
|
992
|
+
init_esm_shims();
|
|
993
|
+
init_api();
|
|
994
|
+
import { Command as Command8 } from "commander";
|
|
995
|
+
import chalk11 from "chalk";
|
|
996
|
+
import inquirer5 from "inquirer";
|
|
997
|
+
import ora6 from "ora";
|
|
998
|
+
var deleteAppCommand = new Command8("delete").description("Delete an app").argument("<app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-y, --yes", "Skip confirmation").action(async (appId, options) => {
|
|
999
|
+
await requireAuth();
|
|
1000
|
+
const orgId = await resolveOrgId(options.org);
|
|
1001
|
+
const app = await api.getApp(orgId, appId);
|
|
1002
|
+
if (!options.yes) {
|
|
1003
|
+
console.log("");
|
|
1004
|
+
logger.warning(`You are about to delete: ${chalk11.bold(app.name)}`);
|
|
1005
|
+
console.log(chalk11.gray(" This action cannot be undone."));
|
|
1006
|
+
console.log("");
|
|
1007
|
+
const { confirm } = await inquirer5.prompt([
|
|
1008
|
+
{
|
|
1009
|
+
type: "input",
|
|
1010
|
+
name: "confirm",
|
|
1011
|
+
message: `Type "${app.name}" to confirm deletion:`,
|
|
1012
|
+
validate: (input) => {
|
|
1013
|
+
if (input !== app.name) return `Please type "${app.name}" to confirm`;
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
]);
|
|
1018
|
+
if (confirm !== app.name) {
|
|
1019
|
+
logger.info("Deletion cancelled");
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const spinner = ora6("Deleting app...").start();
|
|
1024
|
+
try {
|
|
1025
|
+
await api.deleteApp(orgId, appId);
|
|
1026
|
+
spinner.succeed(`App "${app.name}" deleted`);
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
spinner.fail(`Failed to delete app: ${error.message}`);
|
|
1029
|
+
process.exit(1);
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
// src/commands/apps/index.ts
|
|
1034
|
+
var appsCommands = new Command9("apps").description("Manage apps");
|
|
1035
|
+
appsCommands.addCommand(listAppsCommand);
|
|
1036
|
+
appsCommands.addCommand(createAppCommand);
|
|
1037
|
+
appsCommands.addCommand(infoAppCommand);
|
|
1038
|
+
appsCommands.addCommand(updateAppCommand);
|
|
1039
|
+
appsCommands.addCommand(deleteAppCommand);
|
|
1040
|
+
|
|
1041
|
+
// src/commands/release.ts
|
|
1042
|
+
init_esm_shims();
|
|
1043
|
+
init_api();
|
|
1044
|
+
import { Command as Command10 } from "commander";
|
|
1045
|
+
import chalk12 from "chalk";
|
|
1046
|
+
import inquirer6 from "inquirer";
|
|
1047
|
+
import ora7 from "ora";
|
|
1048
|
+
import path4 from "path";
|
|
1049
|
+
import fs6 from "fs-extra";
|
|
1050
|
+
|
|
1051
|
+
// src/lib/bundler.ts
|
|
1052
|
+
init_esm_shims();
|
|
1053
|
+
import { spawn } from "child_process";
|
|
1054
|
+
import path2 from "path";
|
|
1055
|
+
import fs from "fs-extra";
|
|
1056
|
+
import os2 from "os";
|
|
1057
|
+
var bundler = {
|
|
1058
|
+
async bundle(options) {
|
|
1059
|
+
const {
|
|
1060
|
+
platform,
|
|
1061
|
+
entryFile = "index.js",
|
|
1062
|
+
dev = false,
|
|
1063
|
+
minify = true,
|
|
1064
|
+
sourcemap = false
|
|
1065
|
+
} = options;
|
|
1066
|
+
const tempDir = path2.join(os2.tmpdir(), `swiftpatch-bundle-${Date.now()}`);
|
|
1067
|
+
await fs.ensureDir(tempDir);
|
|
1068
|
+
const bundlePath = path2.join(tempDir, `index.${platform}.bundle`);
|
|
1069
|
+
const sourcemapPath = sourcemap ? path2.join(tempDir, `index.${platform}.bundle.map`) : void 0;
|
|
1070
|
+
const args = [
|
|
1071
|
+
"react-native",
|
|
1072
|
+
"bundle",
|
|
1073
|
+
"--platform",
|
|
1074
|
+
platform,
|
|
1075
|
+
"--entry-file",
|
|
1076
|
+
entryFile,
|
|
1077
|
+
"--bundle-output",
|
|
1078
|
+
bundlePath,
|
|
1079
|
+
"--dev",
|
|
1080
|
+
String(dev),
|
|
1081
|
+
"--minify",
|
|
1082
|
+
String(minify),
|
|
1083
|
+
"--reset-cache"
|
|
1084
|
+
];
|
|
1085
|
+
if (sourcemap && sourcemapPath) {
|
|
1086
|
+
args.push("--sourcemap-output", sourcemapPath);
|
|
1087
|
+
}
|
|
1088
|
+
await runCommand("npx", args);
|
|
1089
|
+
const stats = await fs.stat(bundlePath);
|
|
1090
|
+
return {
|
|
1091
|
+
bundlePath,
|
|
1092
|
+
bundleSize: stats.size,
|
|
1093
|
+
sourcemapPath
|
|
1094
|
+
};
|
|
1095
|
+
},
|
|
1096
|
+
async bundleWithConfig(configPath) {
|
|
1097
|
+
const config2 = await fs.readJson(configPath);
|
|
1098
|
+
return this.bundle(config2);
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
function runCommand(command, args) {
|
|
1102
|
+
return new Promise((resolve, reject) => {
|
|
1103
|
+
const child = spawn(command, args, {
|
|
1104
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1105
|
+
shell: true
|
|
1106
|
+
});
|
|
1107
|
+
let stderr = "";
|
|
1108
|
+
child.stdout?.on("data", () => {
|
|
1109
|
+
});
|
|
1110
|
+
child.stderr?.on("data", (data) => {
|
|
1111
|
+
stderr += data.toString();
|
|
1112
|
+
});
|
|
1113
|
+
child.on("close", (code) => {
|
|
1114
|
+
if (code === 0) {
|
|
1115
|
+
resolve();
|
|
1116
|
+
} else {
|
|
1117
|
+
reject(new Error(`Bundle failed: ${stderr}`));
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
child.on("error", (error) => {
|
|
1121
|
+
reject(error);
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/lib/uploader.ts
|
|
1127
|
+
init_esm_shims();
|
|
1128
|
+
import axios2 from "axios";
|
|
1129
|
+
import fs2 from "fs-extra";
|
|
1130
|
+
var uploader = {
|
|
1131
|
+
async uploadBundle(uploadUrl, bundlePath, onProgress) {
|
|
1132
|
+
const fileSize = (await fs2.stat(bundlePath)).size;
|
|
1133
|
+
const fileStream = fs2.createReadStream(bundlePath);
|
|
1134
|
+
let uploadedBytes = 0;
|
|
1135
|
+
fileStream.on("data", (chunk) => {
|
|
1136
|
+
uploadedBytes += chunk.length;
|
|
1137
|
+
const percent = Math.round(uploadedBytes / fileSize * 100);
|
|
1138
|
+
onProgress?.(percent);
|
|
1139
|
+
});
|
|
1140
|
+
await axios2.put(uploadUrl, fileStream, {
|
|
1141
|
+
headers: {
|
|
1142
|
+
"Content-Type": "application/javascript",
|
|
1143
|
+
"Content-Length": fileSize
|
|
1144
|
+
},
|
|
1145
|
+
maxContentLength: Infinity,
|
|
1146
|
+
maxBodyLength: Infinity
|
|
1147
|
+
});
|
|
1148
|
+
},
|
|
1149
|
+
/**
|
|
1150
|
+
* Upload a ZIP file to S3 via presigned URL (for publish-bundle).
|
|
1151
|
+
*/
|
|
1152
|
+
async uploadZip(uploadUrl, zipPath, onProgress) {
|
|
1153
|
+
const fileSize = (await fs2.stat(zipPath)).size;
|
|
1154
|
+
const fileStream = fs2.createReadStream(zipPath);
|
|
1155
|
+
let uploadedBytes = 0;
|
|
1156
|
+
fileStream.on("data", (chunk) => {
|
|
1157
|
+
uploadedBytes += chunk.length;
|
|
1158
|
+
const percent = Math.round(uploadedBytes / fileSize * 100);
|
|
1159
|
+
onProgress?.(percent);
|
|
1160
|
+
});
|
|
1161
|
+
await axios2.put(uploadUrl, fileStream, {
|
|
1162
|
+
headers: {
|
|
1163
|
+
"Content-Type": "application/zip",
|
|
1164
|
+
"Content-Length": fileSize
|
|
1165
|
+
},
|
|
1166
|
+
maxContentLength: Infinity,
|
|
1167
|
+
maxBodyLength: Infinity
|
|
1168
|
+
});
|
|
1169
|
+
},
|
|
1170
|
+
async uploadWithRetry(uploadUrl, bundlePath, onProgress, maxRetries = 3) {
|
|
1171
|
+
let lastError = null;
|
|
1172
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1173
|
+
try {
|
|
1174
|
+
await this.uploadBundle(uploadUrl, bundlePath, onProgress);
|
|
1175
|
+
return;
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
lastError = error;
|
|
1178
|
+
if (attempt < maxRetries) {
|
|
1179
|
+
await sleep2(1e3 * Math.pow(2, attempt));
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
throw lastError || new Error("Upload failed");
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
function sleep2(ms) {
|
|
1187
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/lib/hash.ts
|
|
1191
|
+
init_esm_shims();
|
|
1192
|
+
import crypto2 from "crypto";
|
|
1193
|
+
import fs3 from "fs-extra";
|
|
1194
|
+
import { readFileSync } from "fs";
|
|
1195
|
+
async function hashBundle(bundlePath) {
|
|
1196
|
+
return new Promise((resolve, reject) => {
|
|
1197
|
+
const hash = crypto2.createHash("sha256");
|
|
1198
|
+
const stream = fs3.createReadStream(bundlePath);
|
|
1199
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1200
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
1201
|
+
stream.on("error", reject);
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
function calculateZipHash(filePath) {
|
|
1205
|
+
try {
|
|
1206
|
+
const fileBuffer = readFileSync(filePath);
|
|
1207
|
+
return crypto2.createHash("sha256").update(fileBuffer).digest("hex");
|
|
1208
|
+
} catch {
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
async function hashFile(filePath) {
|
|
1213
|
+
return new Promise((resolve, reject) => {
|
|
1214
|
+
const hash = crypto2.createHash("sha256");
|
|
1215
|
+
const stream = fs3.createReadStream(filePath);
|
|
1216
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1217
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
1218
|
+
stream.on("error", reject);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/lib/signing.ts
|
|
1223
|
+
init_esm_shims();
|
|
1224
|
+
import crypto3 from "crypto";
|
|
1225
|
+
import fs4 from "fs-extra";
|
|
1226
|
+
import path3 from "path";
|
|
1227
|
+
import jwt from "jsonwebtoken";
|
|
1228
|
+
var BUNDLE_EXTENSION = ".swiftpatchsigned";
|
|
1229
|
+
async function signBundle(bundlePath, privateKeyPath) {
|
|
1230
|
+
const absolutePath = path3.resolve(privateKeyPath);
|
|
1231
|
+
if (!await fs4.pathExists(absolutePath)) {
|
|
1232
|
+
throw new Error(`Private key not found: ${absolutePath}`);
|
|
1233
|
+
}
|
|
1234
|
+
const privateKey = await fs4.readFile(absolutePath, "utf-8");
|
|
1235
|
+
const bundleContent = await fs4.readFile(bundlePath);
|
|
1236
|
+
const sign = crypto3.createSign("RSA-SHA256");
|
|
1237
|
+
sign.update(bundleContent);
|
|
1238
|
+
sign.end();
|
|
1239
|
+
const signature = sign.sign(privateKey, "base64");
|
|
1240
|
+
return signature;
|
|
1241
|
+
}
|
|
1242
|
+
async function signBundleDirectory(bundlePath, privateKeyPath) {
|
|
1243
|
+
if (!privateKeyPath) return;
|
|
1244
|
+
let privateKey;
|
|
1245
|
+
try {
|
|
1246
|
+
privateKey = await fs4.readFile(path3.resolve(privateKeyPath));
|
|
1247
|
+
} catch {
|
|
1248
|
+
throw new Error(`The path specified for the signing key ("${privateKeyPath}") was not valid.`);
|
|
1249
|
+
}
|
|
1250
|
+
const signedFilePath = path3.join(bundlePath, BUNDLE_EXTENSION);
|
|
1251
|
+
const fileHashMap = await generatePackageManifest(bundlePath, bundlePath);
|
|
1252
|
+
const packageHash = await computePackageHash(fileHashMap);
|
|
1253
|
+
const payload = { packageHash };
|
|
1254
|
+
try {
|
|
1255
|
+
const signedJwt = jwt.sign(payload, privateKey, { algorithm: "RS256" });
|
|
1256
|
+
await fs4.writeFile(signedFilePath, signedJwt);
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
throw new Error(`Error signing bundle: ${err.message}`);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
async function generatePackageManifest(directoryPath, basePath) {
|
|
1262
|
+
const fileHashMap = /* @__PURE__ */ new Map();
|
|
1263
|
+
const filePathList = await getFilePathsInDir(directoryPath);
|
|
1264
|
+
if (!filePathList || filePathList.length === 0) {
|
|
1265
|
+
throw new Error("Error: Can't sign the release because no files were found.");
|
|
1266
|
+
}
|
|
1267
|
+
for (const filePath of filePathList) {
|
|
1268
|
+
const relativePath = normalizePath(path3.relative(basePath, filePath));
|
|
1269
|
+
if (!isIgnored(relativePath)) {
|
|
1270
|
+
const hash = await hashFile(filePath);
|
|
1271
|
+
fileHashMap.set(relativePath, hash);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return fileHashMap;
|
|
1275
|
+
}
|
|
1276
|
+
async function computePackageHash(fileHashMap) {
|
|
1277
|
+
let entries = [];
|
|
1278
|
+
fileHashMap.forEach((hash, name) => {
|
|
1279
|
+
entries.push(name + ":" + hash);
|
|
1280
|
+
});
|
|
1281
|
+
entries = entries.sort();
|
|
1282
|
+
return crypto3.createHash("sha256").update(JSON.stringify(entries)).digest("hex");
|
|
1283
|
+
}
|
|
1284
|
+
async function generateKeyPair(outputDir) {
|
|
1285
|
+
const { publicKey, privateKey } = crypto3.generateKeyPairSync("rsa", {
|
|
1286
|
+
modulusLength: 2048,
|
|
1287
|
+
publicKeyEncoding: {
|
|
1288
|
+
type: "spki",
|
|
1289
|
+
format: "pem"
|
|
1290
|
+
},
|
|
1291
|
+
privateKeyEncoding: {
|
|
1292
|
+
type: "pkcs8",
|
|
1293
|
+
format: "pem"
|
|
1294
|
+
}
|
|
1295
|
+
});
|
|
1296
|
+
const publicKeyPath = path3.join(outputDir, "swiftpatch-public.pem");
|
|
1297
|
+
const privateKeyPath = path3.join(outputDir, "swiftpatch-private.pem");
|
|
1298
|
+
await fs4.ensureDir(outputDir);
|
|
1299
|
+
await fs4.writeFile(publicKeyPath, publicKey);
|
|
1300
|
+
await fs4.writeFile(privateKeyPath, privateKey, { mode: 384 });
|
|
1301
|
+
return { publicKeyPath, privateKeyPath };
|
|
1302
|
+
}
|
|
1303
|
+
async function readBundleSignature(bundlePath) {
|
|
1304
|
+
const signedFilePath = path3.join(bundlePath, BUNDLE_EXTENSION);
|
|
1305
|
+
if (!await fs4.pathExists(signedFilePath)) {
|
|
1306
|
+
return null;
|
|
1307
|
+
}
|
|
1308
|
+
const signature = await fs4.readFile(signedFilePath, "utf-8");
|
|
1309
|
+
return signature.trim();
|
|
1310
|
+
}
|
|
1311
|
+
function normalizePath(filePath) {
|
|
1312
|
+
return filePath.replace(/\\/g, "/");
|
|
1313
|
+
}
|
|
1314
|
+
async function getFilePathsInDir(dir) {
|
|
1315
|
+
const stats = await fs4.stat(dir);
|
|
1316
|
+
if (stats.isDirectory()) {
|
|
1317
|
+
let files = [];
|
|
1318
|
+
for (const file of await fs4.readdir(dir)) {
|
|
1319
|
+
files = files.concat(await getFilePathsInDir(path3.join(dir, file)));
|
|
1320
|
+
}
|
|
1321
|
+
return files;
|
|
1322
|
+
} else {
|
|
1323
|
+
return [dir];
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function isIgnored(relativeFilePath) {
|
|
1327
|
+
const MACOSX = "__MACOSX/";
|
|
1328
|
+
const DS_STORE = ".DS_Store";
|
|
1329
|
+
const CODEPUSH_META = ".codepushrelease";
|
|
1330
|
+
return relativeFilePath.startsWith(MACOSX) || relativeFilePath === DS_STORE || relativeFilePath.endsWith("/" + DS_STORE) || relativeFilePath === CODEPUSH_META || relativeFilePath.endsWith("/" + CODEPUSH_META);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// src/utils/detect.ts
|
|
1334
|
+
init_esm_shims();
|
|
1335
|
+
import fs5 from "fs-extra";
|
|
1336
|
+
async function detectAppInfo(platform) {
|
|
1337
|
+
try {
|
|
1338
|
+
if (platform === "ios") {
|
|
1339
|
+
return await detectIosVersion();
|
|
1340
|
+
} else {
|
|
1341
|
+
return await detectAndroidVersion();
|
|
1342
|
+
}
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
async function detectIosVersion() {
|
|
1348
|
+
try {
|
|
1349
|
+
const pkg = await fs5.readJson("./package.json");
|
|
1350
|
+
if (pkg.version) {
|
|
1351
|
+
return { version: pkg.version };
|
|
1352
|
+
}
|
|
1353
|
+
} catch {
|
|
1354
|
+
}
|
|
1355
|
+
return null;
|
|
1356
|
+
}
|
|
1357
|
+
async function detectAndroidVersion() {
|
|
1358
|
+
try {
|
|
1359
|
+
const pkg = await fs5.readJson("./package.json");
|
|
1360
|
+
if (pkg.version) {
|
|
1361
|
+
return { version: pkg.version };
|
|
1362
|
+
}
|
|
1363
|
+
} catch {
|
|
1364
|
+
}
|
|
1365
|
+
return null;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// src/commands/release.ts
|
|
1369
|
+
var releaseCommand = new Command10("release").description("Bundle and publish a new release (deprecated: use publish-bundle + release-bundle)").option("-a, --app <app-id>", "App ID or slug").option("-o, --org <org-id>", "Organization ID").option("-p, --platform <platform>", "Target platform (ios or android)").option("-t, --target <versions...>", "Target binary version(s)").option("-c, --channel <channel>", "Release channel", "production").option("-m, --mandatory", "Mark as mandatory update", false).option("-r, --rollout <percent>", "Rollout percentage", "100").option("-n, --note <note>", "Release notes").option("--bundle-path <path>", "Path to pre-built bundle").option("--dry-run", "Simulate release without uploading", false).option("-y, --yes", "Skip confirmation prompts", false).option("--sign", "Sign the bundle", false).option("--private-key <path>", "Path to private key for signing").action(async (options) => {
|
|
1370
|
+
await requireAuth();
|
|
1371
|
+
const orgId = await resolveOrgId(options.org);
|
|
1372
|
+
console.log("");
|
|
1373
|
+
logger.warning(
|
|
1374
|
+
chalk12.yellow('The "release" command is deprecated. Use "publish-bundle" + "release-bundle" instead.')
|
|
1375
|
+
);
|
|
1376
|
+
console.log("");
|
|
1377
|
+
logger.info(chalk12.bold("SwiftPatch Release"));
|
|
1378
|
+
console.log("");
|
|
1379
|
+
let appId = options.app;
|
|
1380
|
+
if (!appId) {
|
|
1381
|
+
const configApp = await detectAppFromConfig();
|
|
1382
|
+
if (configApp) {
|
|
1383
|
+
appId = configApp;
|
|
1384
|
+
logger.info(`Using app from config: ${chalk12.cyan(appId)}`);
|
|
1385
|
+
} else {
|
|
1386
|
+
const apps = await api.getApps(orgId);
|
|
1387
|
+
if (apps.length === 0) {
|
|
1388
|
+
logger.error("No apps found. Create one with: swiftpatch apps create");
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
const { selectedApp } = await inquirer6.prompt([
|
|
1392
|
+
{
|
|
1393
|
+
type: "list",
|
|
1394
|
+
name: "selectedApp",
|
|
1395
|
+
message: "Select app:",
|
|
1396
|
+
choices: apps.map((app2) => ({
|
|
1397
|
+
name: `${app2.name} (${app2.platform})`,
|
|
1398
|
+
value: app2.id
|
|
1399
|
+
}))
|
|
1400
|
+
}
|
|
1401
|
+
]);
|
|
1402
|
+
appId = selectedApp;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
const app = await api.getApp(orgId, appId);
|
|
1406
|
+
logger.info(`App: ${chalk12.cyan(app.name)}`);
|
|
1407
|
+
let platform = options.platform;
|
|
1408
|
+
if (!platform) {
|
|
1409
|
+
if (app.platform === "BOTH") {
|
|
1410
|
+
const { selectedPlatform } = await inquirer6.prompt([
|
|
1411
|
+
{
|
|
1412
|
+
type: "list",
|
|
1413
|
+
name: "selectedPlatform",
|
|
1414
|
+
message: "Select platform:",
|
|
1415
|
+
choices: [
|
|
1416
|
+
{ name: "iOS", value: "ios" },
|
|
1417
|
+
{ name: "Android", value: "android" }
|
|
1418
|
+
]
|
|
1419
|
+
}
|
|
1420
|
+
]);
|
|
1421
|
+
platform = selectedPlatform;
|
|
1422
|
+
} else {
|
|
1423
|
+
platform = app.platform.toLowerCase();
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
logger.info(`Platform: ${chalk12.cyan(platform)}`);
|
|
1427
|
+
let targetVersions = options.target;
|
|
1428
|
+
if (!targetVersions || targetVersions.length === 0) {
|
|
1429
|
+
const detectedVersion = await detectAppInfo(platform);
|
|
1430
|
+
const { version: version2 } = await inquirer6.prompt([
|
|
1431
|
+
{
|
|
1432
|
+
type: "input",
|
|
1433
|
+
name: "version",
|
|
1434
|
+
message: "Target binary version:",
|
|
1435
|
+
default: detectedVersion?.version || "1.0.0",
|
|
1436
|
+
validate: (input) => {
|
|
1437
|
+
if (!input) return "Version is required";
|
|
1438
|
+
if (!/^\d+\.\d+\.\d+/.test(input)) return "Invalid version format (use semver)";
|
|
1439
|
+
return true;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
]);
|
|
1443
|
+
targetVersions = [version2];
|
|
1444
|
+
}
|
|
1445
|
+
logger.info(`Target version: ${chalk12.cyan(targetVersions.join(", "))}`);
|
|
1446
|
+
let releaseNote = options.note;
|
|
1447
|
+
if (!releaseNote && !options.yes) {
|
|
1448
|
+
const { note } = await inquirer6.prompt([
|
|
1449
|
+
{
|
|
1450
|
+
type: "input",
|
|
1451
|
+
name: "note",
|
|
1452
|
+
message: "Release notes (optional):"
|
|
1453
|
+
}
|
|
1454
|
+
]);
|
|
1455
|
+
releaseNote = note || void 0;
|
|
1456
|
+
}
|
|
1457
|
+
const rolloutPercent = parseInt(options.rollout || "100", 10);
|
|
1458
|
+
let bundlePath = options.bundlePath;
|
|
1459
|
+
if (!bundlePath) {
|
|
1460
|
+
const spinner = ora7("Bundling JavaScript...").start();
|
|
1461
|
+
try {
|
|
1462
|
+
const result = await bundler.bundle({
|
|
1463
|
+
platform,
|
|
1464
|
+
entryFile: "index.js",
|
|
1465
|
+
dev: false
|
|
1466
|
+
});
|
|
1467
|
+
bundlePath = result.bundlePath;
|
|
1468
|
+
spinner.succeed(`Bundle created: ${chalk12.gray(formatBytes(result.bundleSize))}`);
|
|
1469
|
+
} catch (error) {
|
|
1470
|
+
spinner.fail(`Bundling failed: ${error.message}`);
|
|
1471
|
+
process.exit(1);
|
|
1472
|
+
}
|
|
1473
|
+
} else {
|
|
1474
|
+
if (!await fs6.pathExists(bundlePath)) {
|
|
1475
|
+
logger.error(`Bundle not found: ${bundlePath}`);
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
logger.info(`Using existing bundle: ${chalk12.gray(bundlePath)}`);
|
|
1479
|
+
}
|
|
1480
|
+
const spinner2 = ora7("Computing bundle hash...").start();
|
|
1481
|
+
const bundleHash = await hashBundle(bundlePath);
|
|
1482
|
+
const bundleSize = (await fs6.stat(bundlePath)).size;
|
|
1483
|
+
spinner2.succeed(`Bundle hash: ${chalk12.gray(bundleHash.slice(0, 16))}...`);
|
|
1484
|
+
let signature;
|
|
1485
|
+
if (options.sign || app.signingEnabled) {
|
|
1486
|
+
const spinner3 = ora7("Signing bundle...").start();
|
|
1487
|
+
try {
|
|
1488
|
+
const privateKeyPath = options.privateKey || "./swiftpatch-private.pem";
|
|
1489
|
+
signature = await signBundle(bundlePath, privateKeyPath);
|
|
1490
|
+
spinner3.succeed("Bundle signed");
|
|
1491
|
+
} catch (error) {
|
|
1492
|
+
spinner3.fail(`Signing failed: ${error.message}`);
|
|
1493
|
+
if (app.signingEnabled) {
|
|
1494
|
+
logger.error("Bundle signing is required for this app");
|
|
1495
|
+
process.exit(1);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
console.log("");
|
|
1500
|
+
console.log(chalk12.bold(" Release Summary"));
|
|
1501
|
+
console.log("");
|
|
1502
|
+
console.log(` App: ${chalk12.cyan(app.name)}`);
|
|
1503
|
+
console.log(` Platform: ${chalk12.cyan(platform)}`);
|
|
1504
|
+
console.log(` Target: ${chalk12.cyan(targetVersions.join(", "))}`);
|
|
1505
|
+
console.log(` Channel: ${chalk12.cyan(options.channel || "production")}`);
|
|
1506
|
+
console.log(` Rollout: ${chalk12.cyan(rolloutPercent + "%")}`);
|
|
1507
|
+
console.log(` Mandatory: ${options.mandatory ? chalk12.yellow("Yes") : chalk12.gray("No")}`);
|
|
1508
|
+
console.log(` Bundle: ${chalk12.gray(formatBytes(bundleSize))}`);
|
|
1509
|
+
console.log(` Hash: ${chalk12.gray(bundleHash.slice(0, 16))}...`);
|
|
1510
|
+
if (signature) {
|
|
1511
|
+
console.log(` Signed: ${chalk12.green("Yes")}`);
|
|
1512
|
+
}
|
|
1513
|
+
if (releaseNote) {
|
|
1514
|
+
console.log(` Notes: ${chalk12.gray(releaseNote.slice(0, 50))}${releaseNote.length > 50 ? "..." : ""}`);
|
|
1515
|
+
}
|
|
1516
|
+
console.log("");
|
|
1517
|
+
if (options.dryRun) {
|
|
1518
|
+
logger.info("Dry run - skipping upload");
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (!options.yes) {
|
|
1522
|
+
const { confirm } = await inquirer6.prompt([
|
|
1523
|
+
{
|
|
1524
|
+
type: "confirm",
|
|
1525
|
+
name: "confirm",
|
|
1526
|
+
message: "Publish this release?",
|
|
1527
|
+
default: true
|
|
1528
|
+
}
|
|
1529
|
+
]);
|
|
1530
|
+
if (!confirm) {
|
|
1531
|
+
logger.info("Release cancelled");
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
const spinner4 = ora7("Creating release...").start();
|
|
1536
|
+
try {
|
|
1537
|
+
const release = await api.createRelease(orgId, app.id, {
|
|
1538
|
+
version: targetVersions[0],
|
|
1539
|
+
platform: platform.toUpperCase(),
|
|
1540
|
+
targetVersions,
|
|
1541
|
+
isMandatory: options.mandatory || false,
|
|
1542
|
+
releaseNote
|
|
1543
|
+
});
|
|
1544
|
+
spinner4.text = "Uploading bundle...";
|
|
1545
|
+
await uploader.uploadBundle(release.uploadUrl, bundlePath, (progress) => {
|
|
1546
|
+
spinner4.text = `Uploading bundle... ${progress}%`;
|
|
1547
|
+
});
|
|
1548
|
+
spinner4.text = "Processing...";
|
|
1549
|
+
await api.completeUpload(orgId, app.id, release.id, {
|
|
1550
|
+
bundleHash,
|
|
1551
|
+
bundleSize,
|
|
1552
|
+
signature
|
|
1553
|
+
});
|
|
1554
|
+
const finalRelease = await waitForProcessing(orgId, app.id, release.id, spinner4);
|
|
1555
|
+
spinner4.text = "Publishing...";
|
|
1556
|
+
await api.publishRelease(orgId, app.id, release.id, { rolloutPercent });
|
|
1557
|
+
spinner4.succeed("Release published!");
|
|
1558
|
+
console.log("");
|
|
1559
|
+
console.log(chalk12.green(" Release published successfully!"));
|
|
1560
|
+
console.log("");
|
|
1561
|
+
console.log(` Version: ${chalk12.cyan(finalRelease.version)}`);
|
|
1562
|
+
console.log(` Release ID: ${chalk12.gray(release.id)}`);
|
|
1563
|
+
console.log(` Status: ${chalk12.green("RELEASED")}`);
|
|
1564
|
+
console.log(` Rollout: ${chalk12.cyan(rolloutPercent + "%")}`);
|
|
1565
|
+
console.log("");
|
|
1566
|
+
console.log(` View in dashboard: ${chalk12.cyan(`https://app.swiftpatch.io/apps/${app.id}/releases/${release.id}`)}`);
|
|
1567
|
+
console.log("");
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
spinner4.fail(`Release failed: ${error.message}`);
|
|
1570
|
+
process.exit(1);
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
async function detectAppFromConfig() {
|
|
1574
|
+
const configPaths = [
|
|
1575
|
+
"./swiftpatch.config.js",
|
|
1576
|
+
"./swiftpatch.config.json",
|
|
1577
|
+
"./.swiftpatchrc"
|
|
1578
|
+
];
|
|
1579
|
+
for (const configPath of configPaths) {
|
|
1580
|
+
if (await fs6.pathExists(configPath)) {
|
|
1581
|
+
try {
|
|
1582
|
+
const config2 = configPath.endsWith(".js") ? (await import(path4.resolve(configPath))).default : await fs6.readJson(configPath);
|
|
1583
|
+
return config2.appId || config2.app;
|
|
1584
|
+
} catch {
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
return void 0;
|
|
1590
|
+
}
|
|
1591
|
+
async function waitForProcessing(orgId, appId, releaseId, spinner) {
|
|
1592
|
+
const maxAttempts = 12;
|
|
1593
|
+
let attempts = 0;
|
|
1594
|
+
while (attempts < maxAttempts) {
|
|
1595
|
+
const release2 = await api.getRelease(orgId, appId, releaseId);
|
|
1596
|
+
if (release2.status === "READY" || release2.status === "RELEASED") {
|
|
1597
|
+
return release2;
|
|
1598
|
+
}
|
|
1599
|
+
if (release2.status === "FAILED") {
|
|
1600
|
+
throw new Error(release2.statusMessage || "Processing failed");
|
|
1601
|
+
}
|
|
1602
|
+
if (attempts >= 3 && release2.status === "PROCESSING") {
|
|
1603
|
+
return release2;
|
|
1604
|
+
}
|
|
1605
|
+
await sleep3(5e3);
|
|
1606
|
+
attempts++;
|
|
1607
|
+
spinner.text = `Processing... (${attempts * 5}s)`;
|
|
1608
|
+
}
|
|
1609
|
+
const release = await api.getRelease(orgId, appId, releaseId);
|
|
1610
|
+
if (release.status === "PROCESSING") {
|
|
1611
|
+
return release;
|
|
1612
|
+
}
|
|
1613
|
+
throw new Error("Processing timed out");
|
|
1614
|
+
}
|
|
1615
|
+
function formatBytes(bytes) {
|
|
1616
|
+
if (bytes < 1024) return bytes + " B";
|
|
1617
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
1618
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
1619
|
+
}
|
|
1620
|
+
function sleep3(ms) {
|
|
1621
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// src/commands/releases/index.ts
|
|
1625
|
+
init_esm_shims();
|
|
1626
|
+
import { Command as Command16 } from "commander";
|
|
1627
|
+
|
|
1628
|
+
// src/commands/releases/list.ts
|
|
1629
|
+
init_esm_shims();
|
|
1630
|
+
init_api();
|
|
1631
|
+
import { Command as Command11 } from "commander";
|
|
1632
|
+
import chalk13 from "chalk";
|
|
1633
|
+
import ora8 from "ora";
|
|
1634
|
+
var listReleasesCommand = new Command11("list").description("List releases for an app").argument("<app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("--page <page>", "Page number", "1").option("--limit <limit>", "Items per page", "20").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
1635
|
+
await requireAuth();
|
|
1636
|
+
const orgId = await resolveOrgId(options.org);
|
|
1637
|
+
const spinner = ora8("Fetching releases...").start();
|
|
1638
|
+
try {
|
|
1639
|
+
const result = await api.getReleases(orgId, appId, {
|
|
1640
|
+
page: parseInt(options.page, 10),
|
|
1641
|
+
limit: parseInt(options.limit, 10)
|
|
1642
|
+
});
|
|
1643
|
+
spinner.stop();
|
|
1644
|
+
if (options.json) {
|
|
1645
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
if (result.releases.length === 0) {
|
|
1649
|
+
console.log("");
|
|
1650
|
+
console.log(chalk13.gray(" No releases found."));
|
|
1651
|
+
console.log("");
|
|
1652
|
+
console.log(" Publish one with: " + chalk13.cyan("swiftpatch release"));
|
|
1653
|
+
console.log("");
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
console.log("");
|
|
1657
|
+
console.log(chalk13.bold(" Releases"));
|
|
1658
|
+
console.log("");
|
|
1659
|
+
const table = createTable({
|
|
1660
|
+
head: ["Version", "Platform", "Status", "Rollout", "Mandatory", "Created"],
|
|
1661
|
+
rows: result.releases.map((r) => [
|
|
1662
|
+
chalk13.cyan(r.version),
|
|
1663
|
+
r.platform,
|
|
1664
|
+
formatStatus(r.status),
|
|
1665
|
+
`${r.rolloutPercent}%`,
|
|
1666
|
+
r.isMandatory ? chalk13.yellow("Yes") : chalk13.gray("No"),
|
|
1667
|
+
new Date(r.createdAt).toLocaleDateString()
|
|
1668
|
+
])
|
|
1669
|
+
});
|
|
1670
|
+
console.log(table);
|
|
1671
|
+
if (result.pagination) {
|
|
1672
|
+
console.log("");
|
|
1673
|
+
console.log(chalk13.gray(
|
|
1674
|
+
` Page ${result.pagination.page} of ${result.pagination.totalPages} (${result.pagination.total} total)`
|
|
1675
|
+
));
|
|
1676
|
+
}
|
|
1677
|
+
console.log("");
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
spinner.fail(`Failed to fetch releases: ${error.message}`);
|
|
1680
|
+
process.exit(1);
|
|
1681
|
+
}
|
|
1682
|
+
});
|
|
1683
|
+
function formatStatus(status) {
|
|
1684
|
+
switch (status) {
|
|
1685
|
+
case "RELEASED":
|
|
1686
|
+
return chalk13.green(status);
|
|
1687
|
+
case "ROLLED_BACK":
|
|
1688
|
+
return chalk13.red(status);
|
|
1689
|
+
case "PROCESSING":
|
|
1690
|
+
return chalk13.yellow(status);
|
|
1691
|
+
case "READY":
|
|
1692
|
+
return chalk13.blue(status);
|
|
1693
|
+
case "FAILED":
|
|
1694
|
+
return chalk13.red(status);
|
|
1695
|
+
default:
|
|
1696
|
+
return chalk13.gray(status);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/commands/releases/info.ts
|
|
1701
|
+
init_esm_shims();
|
|
1702
|
+
init_api();
|
|
1703
|
+
import { Command as Command12 } from "commander";
|
|
1704
|
+
import chalk14 from "chalk";
|
|
1705
|
+
import ora9 from "ora";
|
|
1706
|
+
import inquirer7 from "inquirer";
|
|
1707
|
+
var infoReleaseCommand = new Command12("info").description("Show release details").argument("<release-id>", "Release ID").option("-a, --app <app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (releaseId, options) => {
|
|
1708
|
+
await requireAuth();
|
|
1709
|
+
const orgId = await resolveOrgId(options.org);
|
|
1710
|
+
let appId = options.app;
|
|
1711
|
+
if (!appId) {
|
|
1712
|
+
const apps = await api.getApps(orgId);
|
|
1713
|
+
const { selectedApp } = await inquirer7.prompt([
|
|
1714
|
+
{
|
|
1715
|
+
type: "list",
|
|
1716
|
+
name: "selectedApp",
|
|
1717
|
+
message: "Select app:",
|
|
1718
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
1719
|
+
}
|
|
1720
|
+
]);
|
|
1721
|
+
appId = selectedApp;
|
|
1722
|
+
}
|
|
1723
|
+
const spinner = ora9("Fetching release details...").start();
|
|
1724
|
+
try {
|
|
1725
|
+
const release = await api.getRelease(orgId, appId, releaseId);
|
|
1726
|
+
spinner.stop();
|
|
1727
|
+
if (options.json) {
|
|
1728
|
+
console.log(JSON.stringify(release, null, 2));
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
console.log("");
|
|
1732
|
+
console.log(chalk14.bold(" Release Details"));
|
|
1733
|
+
console.log("");
|
|
1734
|
+
console.log(` Version: ${chalk14.cyan(release.version)}`);
|
|
1735
|
+
console.log(` ID: ${chalk14.gray(release.id)}`);
|
|
1736
|
+
console.log(` Platform: ${release.platform}`);
|
|
1737
|
+
console.log(` Status: ${formatStatus2(release.status)}`);
|
|
1738
|
+
console.log(` Rollout: ${chalk14.cyan(release.rolloutPercent + "%")}`);
|
|
1739
|
+
console.log(` Mandatory: ${release.isMandatory ? chalk14.yellow("Yes") : chalk14.gray("No")}`);
|
|
1740
|
+
console.log(` Bundle Size: ${chalk14.gray(formatBytes2(release.bundleSize))}`);
|
|
1741
|
+
console.log(` Bundle Hash: ${chalk14.gray(release.bundleHash?.slice(0, 16) || "N/A")}...`);
|
|
1742
|
+
console.log(` Created: ${new Date(release.createdAt).toLocaleString()}`);
|
|
1743
|
+
if (release.releasedAt) {
|
|
1744
|
+
console.log(` Released: ${new Date(release.releasedAt).toLocaleString()}`);
|
|
1745
|
+
}
|
|
1746
|
+
if (release.releaseNote) {
|
|
1747
|
+
console.log("");
|
|
1748
|
+
console.log(chalk14.bold(" Release Notes"));
|
|
1749
|
+
console.log(` ${release.releaseNote}`);
|
|
1750
|
+
}
|
|
1751
|
+
console.log("");
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
spinner.fail(`Failed to fetch release: ${error.message}`);
|
|
1754
|
+
process.exit(1);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
function formatStatus2(status) {
|
|
1758
|
+
switch (status) {
|
|
1759
|
+
case "RELEASED":
|
|
1760
|
+
return chalk14.green(status);
|
|
1761
|
+
case "ROLLED_BACK":
|
|
1762
|
+
return chalk14.red(status);
|
|
1763
|
+
case "PROCESSING":
|
|
1764
|
+
return chalk14.yellow(status);
|
|
1765
|
+
default:
|
|
1766
|
+
return chalk14.gray(status);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function formatBytes2(bytes) {
|
|
1770
|
+
if (!bytes) return "N/A";
|
|
1771
|
+
if (bytes < 1024) return bytes + " B";
|
|
1772
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
1773
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/commands/releases/rollout.ts
|
|
1777
|
+
init_esm_shims();
|
|
1778
|
+
init_api();
|
|
1779
|
+
import { Command as Command13 } from "commander";
|
|
1780
|
+
import chalk15 from "chalk";
|
|
1781
|
+
import inquirer8 from "inquirer";
|
|
1782
|
+
import ora10 from "ora";
|
|
1783
|
+
var rolloutCommand = new Command13("rollout").description("Update rollout percentage for a release").argument("<release-id>", "Release ID").option("-a, --app <app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-p, --percent <percent>", "Rollout percentage (0-100)").action(async (releaseId, options) => {
|
|
1784
|
+
await requireAuth();
|
|
1785
|
+
const orgId = await resolveOrgId(options.org);
|
|
1786
|
+
let appId = options.app;
|
|
1787
|
+
if (!appId) {
|
|
1788
|
+
const apps = await api.getApps(orgId);
|
|
1789
|
+
const { selectedApp } = await inquirer8.prompt([
|
|
1790
|
+
{
|
|
1791
|
+
type: "list",
|
|
1792
|
+
name: "selectedApp",
|
|
1793
|
+
message: "Select app:",
|
|
1794
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
1795
|
+
}
|
|
1796
|
+
]);
|
|
1797
|
+
appId = selectedApp;
|
|
1798
|
+
}
|
|
1799
|
+
let rolloutPercent = options.percent ? parseInt(options.percent, 10) : null;
|
|
1800
|
+
if (rolloutPercent !== null && (isNaN(rolloutPercent) || rolloutPercent < 0 || rolloutPercent > 100)) {
|
|
1801
|
+
console.error(chalk15.red("Rollout percentage must be a number between 0 and 100"));
|
|
1802
|
+
process.exit(1);
|
|
1803
|
+
}
|
|
1804
|
+
if (rolloutPercent === null) {
|
|
1805
|
+
const release = await api.getRelease(orgId, appId, releaseId);
|
|
1806
|
+
const { percent } = await inquirer8.prompt([
|
|
1807
|
+
{
|
|
1808
|
+
type: "number",
|
|
1809
|
+
name: "percent",
|
|
1810
|
+
message: `Current rollout: ${release.rolloutPercent}%. New percentage:`,
|
|
1811
|
+
default: release.rolloutPercent,
|
|
1812
|
+
validate: (input) => {
|
|
1813
|
+
if (input < 0 || input > 100) return "Must be between 0 and 100";
|
|
1814
|
+
return true;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
]);
|
|
1818
|
+
rolloutPercent = percent;
|
|
1819
|
+
}
|
|
1820
|
+
const spinner = ora10("Updating rollout...").start();
|
|
1821
|
+
try {
|
|
1822
|
+
const release = await api.updateRollout(orgId, appId, releaseId, rolloutPercent);
|
|
1823
|
+
spinner.succeed(`Rollout updated to ${chalk15.cyan(rolloutPercent + "%")}`);
|
|
1824
|
+
console.log("");
|
|
1825
|
+
console.log(` Release: ${chalk15.gray(release.version)}`);
|
|
1826
|
+
console.log(` Status: ${chalk15.green(release.status)}`);
|
|
1827
|
+
console.log(` Rollout: ${chalk15.cyan(release.rolloutPercent + "%")}`);
|
|
1828
|
+
console.log("");
|
|
1829
|
+
} catch (error) {
|
|
1830
|
+
spinner.fail(`Failed to update rollout: ${error.message}`);
|
|
1831
|
+
process.exit(1);
|
|
1832
|
+
}
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
// src/commands/releases/rollback.ts
|
|
1836
|
+
init_esm_shims();
|
|
1837
|
+
init_api();
|
|
1838
|
+
import { Command as Command14 } from "commander";
|
|
1839
|
+
import chalk16 from "chalk";
|
|
1840
|
+
import inquirer9 from "inquirer";
|
|
1841
|
+
import ora11 from "ora";
|
|
1842
|
+
var rollbackCommand = new Command14("rollback").description("Rollback (disable) a release").argument("<release-id>", "Release ID").option("-a, --app <app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-r, --reason <reason>", "Rollback reason").option("-y, --yes", "Skip confirmation").action(async (releaseId, options) => {
|
|
1843
|
+
await requireAuth();
|
|
1844
|
+
const orgId = await resolveOrgId(options.org);
|
|
1845
|
+
let appId = options.app;
|
|
1846
|
+
if (!appId) {
|
|
1847
|
+
const apps = await api.getApps(orgId);
|
|
1848
|
+
const { selectedApp } = await inquirer9.prompt([
|
|
1849
|
+
{
|
|
1850
|
+
type: "list",
|
|
1851
|
+
name: "selectedApp",
|
|
1852
|
+
message: "Select app:",
|
|
1853
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
1854
|
+
}
|
|
1855
|
+
]);
|
|
1856
|
+
appId = selectedApp;
|
|
1857
|
+
}
|
|
1858
|
+
const release = await api.getRelease(orgId, appId, releaseId);
|
|
1859
|
+
console.log("");
|
|
1860
|
+
logger.warning("You are about to rollback this release:");
|
|
1861
|
+
console.log("");
|
|
1862
|
+
console.log(` Version: ${chalk16.cyan(release.version)}`);
|
|
1863
|
+
console.log(` Platform: ${release.platform}`);
|
|
1864
|
+
console.log(` Status: ${release.status}`);
|
|
1865
|
+
console.log(` Rollout: ${release.rolloutPercent}%`);
|
|
1866
|
+
console.log("");
|
|
1867
|
+
if (!options.yes) {
|
|
1868
|
+
const { confirm } = await inquirer9.prompt([
|
|
1869
|
+
{
|
|
1870
|
+
type: "confirm",
|
|
1871
|
+
name: "confirm",
|
|
1872
|
+
message: "Are you sure you want to rollback this release?",
|
|
1873
|
+
default: false
|
|
1874
|
+
}
|
|
1875
|
+
]);
|
|
1876
|
+
if (!confirm) {
|
|
1877
|
+
logger.info("Rollback cancelled");
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
let reason = options.reason;
|
|
1882
|
+
if (!reason && !options.yes) {
|
|
1883
|
+
const { inputReason } = await inquirer9.prompt([
|
|
1884
|
+
{
|
|
1885
|
+
type: "input",
|
|
1886
|
+
name: "inputReason",
|
|
1887
|
+
message: "Reason for rollback (optional):"
|
|
1888
|
+
}
|
|
1889
|
+
]);
|
|
1890
|
+
reason = inputReason || void 0;
|
|
1891
|
+
}
|
|
1892
|
+
const spinner = ora11("Rolling back release...").start();
|
|
1893
|
+
try {
|
|
1894
|
+
await api.rollbackRelease(orgId, appId, releaseId, reason);
|
|
1895
|
+
spinner.succeed("Release rolled back");
|
|
1896
|
+
console.log("");
|
|
1897
|
+
console.log(chalk16.yellow(" Release has been disabled"));
|
|
1898
|
+
console.log(chalk16.gray(" Users will no longer receive this update"));
|
|
1899
|
+
console.log("");
|
|
1900
|
+
} catch (error) {
|
|
1901
|
+
spinner.fail(`Rollback failed: ${error.message}`);
|
|
1902
|
+
process.exit(1);
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
// src/commands/releases/disable.ts
|
|
1907
|
+
init_esm_shims();
|
|
1908
|
+
init_api();
|
|
1909
|
+
import { Command as Command15 } from "commander";
|
|
1910
|
+
import chalk17 from "chalk";
|
|
1911
|
+
import inquirer10 from "inquirer";
|
|
1912
|
+
import ora12 from "ora";
|
|
1913
|
+
var disableCommand = new Command15("disable").description("Disable a release (stops serving to new devices)").argument("<release-id>", "Release ID").option("-a, --app <app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-r, --reason <reason>", "Reason for disabling").option("-y, --yes", "Skip confirmation").action(async (releaseId, options) => {
|
|
1914
|
+
await requireAuth();
|
|
1915
|
+
const orgId = await resolveOrgId(options.org);
|
|
1916
|
+
let appId = options.app;
|
|
1917
|
+
if (!appId) {
|
|
1918
|
+
const apps = await api.getApps(orgId);
|
|
1919
|
+
const { selectedApp } = await inquirer10.prompt([
|
|
1920
|
+
{
|
|
1921
|
+
type: "list",
|
|
1922
|
+
name: "selectedApp",
|
|
1923
|
+
message: "Select app:",
|
|
1924
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
1925
|
+
}
|
|
1926
|
+
]);
|
|
1927
|
+
appId = selectedApp;
|
|
1928
|
+
}
|
|
1929
|
+
const release = await api.getRelease(orgId, appId, releaseId);
|
|
1930
|
+
console.log("");
|
|
1931
|
+
logger.warning("You are about to disable this release:");
|
|
1932
|
+
console.log("");
|
|
1933
|
+
console.log(` Version: ${chalk17.cyan(release.version)}`);
|
|
1934
|
+
console.log(` Platform: ${release.platform}`);
|
|
1935
|
+
console.log(` Status: ${release.status}`);
|
|
1936
|
+
console.log(` Rollout: ${release.rolloutPercent}%`);
|
|
1937
|
+
console.log("");
|
|
1938
|
+
if (!options.yes) {
|
|
1939
|
+
const { confirm } = await inquirer10.prompt([
|
|
1940
|
+
{
|
|
1941
|
+
type: "confirm",
|
|
1942
|
+
name: "confirm",
|
|
1943
|
+
message: "Are you sure you want to disable this release?",
|
|
1944
|
+
default: false
|
|
1945
|
+
}
|
|
1946
|
+
]);
|
|
1947
|
+
if (!confirm) {
|
|
1948
|
+
logger.info("Cancelled");
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
let reason = options.reason;
|
|
1953
|
+
if (!reason && !options.yes) {
|
|
1954
|
+
const { inputReason } = await inquirer10.prompt([
|
|
1955
|
+
{
|
|
1956
|
+
type: "input",
|
|
1957
|
+
name: "inputReason",
|
|
1958
|
+
message: "Reason for disabling (optional):"
|
|
1959
|
+
}
|
|
1960
|
+
]);
|
|
1961
|
+
reason = inputReason || void 0;
|
|
1962
|
+
}
|
|
1963
|
+
const spinner = ora12("Disabling release...").start();
|
|
1964
|
+
try {
|
|
1965
|
+
await api.disableRelease(orgId, appId, releaseId, reason);
|
|
1966
|
+
spinner.succeed("Release disabled");
|
|
1967
|
+
console.log("");
|
|
1968
|
+
console.log(chalk17.yellow(" Release has been disabled"));
|
|
1969
|
+
console.log(chalk17.gray(" New devices will not receive this update"));
|
|
1970
|
+
console.log(chalk17.gray(" Devices that already installed it are unaffected"));
|
|
1971
|
+
console.log("");
|
|
1972
|
+
} catch (error) {
|
|
1973
|
+
spinner.fail(`Failed to disable release: ${error.message}`);
|
|
1974
|
+
process.exit(1);
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
|
|
1978
|
+
// src/commands/releases/index.ts
|
|
1979
|
+
var releasesCommands = new Command16("releases").description("Manage releases");
|
|
1980
|
+
releasesCommands.addCommand(listReleasesCommand);
|
|
1981
|
+
releasesCommands.addCommand(infoReleaseCommand);
|
|
1982
|
+
releasesCommands.addCommand(rolloutCommand);
|
|
1983
|
+
releasesCommands.addCommand(rollbackCommand);
|
|
1984
|
+
releasesCommands.addCommand(disableCommand);
|
|
1985
|
+
|
|
1986
|
+
// src/commands/channels/index.ts
|
|
1987
|
+
init_esm_shims();
|
|
1988
|
+
import { Command as Command21 } from "commander";
|
|
1989
|
+
|
|
1990
|
+
// src/commands/channels/list.ts
|
|
1991
|
+
init_esm_shims();
|
|
1992
|
+
init_api();
|
|
1993
|
+
import { Command as Command17 } from "commander";
|
|
1994
|
+
import chalk18 from "chalk";
|
|
1995
|
+
import ora13 from "ora";
|
|
1996
|
+
var listChannelsCommand = new Command17("list").description("List channels for an app").argument("<app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
1997
|
+
await requireAuth();
|
|
1998
|
+
const orgId = await resolveOrgId(options.org);
|
|
1999
|
+
const spinner = ora13("Fetching channels...").start();
|
|
2000
|
+
try {
|
|
2001
|
+
const channels = await api.getChannels(orgId, appId);
|
|
2002
|
+
spinner.stop();
|
|
2003
|
+
if (options.json) {
|
|
2004
|
+
console.log(JSON.stringify(channels, null, 2));
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
if (channels.length === 0) {
|
|
2008
|
+
console.log("");
|
|
2009
|
+
console.log(chalk18.gray(" No channels found."));
|
|
2010
|
+
console.log("");
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
console.log("");
|
|
2014
|
+
console.log(chalk18.bold(" Channels"));
|
|
2015
|
+
console.log("");
|
|
2016
|
+
const table = createTable({
|
|
2017
|
+
head: ["Name", "Slug", "Created"],
|
|
2018
|
+
rows: channels.map((ch) => [
|
|
2019
|
+
chalk18.cyan(ch.name),
|
|
2020
|
+
chalk18.gray(ch.slug),
|
|
2021
|
+
new Date(ch.createdAt).toLocaleDateString()
|
|
2022
|
+
])
|
|
2023
|
+
});
|
|
2024
|
+
console.log(table);
|
|
2025
|
+
console.log("");
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
spinner.fail(`Failed to fetch channels: ${error.message}`);
|
|
2028
|
+
process.exit(1);
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
// src/commands/channels/create.ts
|
|
2033
|
+
init_esm_shims();
|
|
2034
|
+
init_api();
|
|
2035
|
+
import { Command as Command18 } from "commander";
|
|
2036
|
+
import chalk19 from "chalk";
|
|
2037
|
+
import inquirer11 from "inquirer";
|
|
2038
|
+
import ora14 from "ora";
|
|
2039
|
+
var createChannelCommand = new Command18("create").description("Create a new channel").argument("<app-id>", "App ID").option("-n, --name <name>", "Channel name").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
2040
|
+
await requireAuth();
|
|
2041
|
+
const orgId = await resolveOrgId(options.org);
|
|
2042
|
+
let channelName = options.name;
|
|
2043
|
+
if (!channelName) {
|
|
2044
|
+
const { name } = await inquirer11.prompt([
|
|
2045
|
+
{
|
|
2046
|
+
type: "input",
|
|
2047
|
+
name: "name",
|
|
2048
|
+
message: "Channel name:",
|
|
2049
|
+
validate: (input) => {
|
|
2050
|
+
if (!input.trim()) return "Channel name is required";
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
]);
|
|
2055
|
+
channelName = name;
|
|
2056
|
+
}
|
|
2057
|
+
const spinner = ora14("Creating channel...").start();
|
|
2058
|
+
try {
|
|
2059
|
+
const channel = await api.createChannel(orgId, appId, { name: channelName });
|
|
2060
|
+
spinner.succeed("Channel created!");
|
|
2061
|
+
if (options.json) {
|
|
2062
|
+
console.log(JSON.stringify(channel, null, 2));
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
console.log("");
|
|
2066
|
+
console.log(` Name: ${chalk19.cyan(channel.name)}`);
|
|
2067
|
+
console.log(` Slug: ${chalk19.gray(channel.slug)}`);
|
|
2068
|
+
console.log("");
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
spinner.fail(`Failed to create channel: ${error.message}`);
|
|
2071
|
+
process.exit(1);
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
|
|
2075
|
+
// src/commands/channels/update.ts
|
|
2076
|
+
init_esm_shims();
|
|
2077
|
+
init_api();
|
|
2078
|
+
import { Command as Command19 } from "commander";
|
|
2079
|
+
import chalk20 from "chalk";
|
|
2080
|
+
import inquirer12 from "inquirer";
|
|
2081
|
+
import ora15 from "ora";
|
|
2082
|
+
var updateChannelCommand = new Command19("update").description("Update a channel").argument("<app-id>", "App ID").argument("<channel-id>", "Channel ID").option("-n, --name <name>", "New channel name").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, channelId, options) => {
|
|
2083
|
+
await requireAuth();
|
|
2084
|
+
const orgId = await resolveOrgId(options.org);
|
|
2085
|
+
let channelName = options.name;
|
|
2086
|
+
if (!channelName) {
|
|
2087
|
+
const answers = await inquirer12.prompt([
|
|
2088
|
+
{
|
|
2089
|
+
type: "input",
|
|
2090
|
+
name: "name",
|
|
2091
|
+
message: "New channel name:",
|
|
2092
|
+
validate: (input) => {
|
|
2093
|
+
if (!input.trim()) return "Channel name is required";
|
|
2094
|
+
return true;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
]);
|
|
2098
|
+
channelName = answers.name;
|
|
2099
|
+
}
|
|
2100
|
+
const spinner = ora15("Updating channel...").start();
|
|
2101
|
+
try {
|
|
2102
|
+
const channel = await api.updateChannel(orgId, appId, channelId, { name: channelName });
|
|
2103
|
+
spinner.succeed("Channel updated!");
|
|
2104
|
+
if (options.json) {
|
|
2105
|
+
console.log(JSON.stringify(channel, null, 2));
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
console.log("");
|
|
2109
|
+
console.log(` Name: ${chalk20.cyan(channel.name)}`);
|
|
2110
|
+
console.log(` Slug: ${chalk20.gray(channel.slug)}`);
|
|
2111
|
+
console.log("");
|
|
2112
|
+
} catch (error) {
|
|
2113
|
+
spinner.fail(`Failed to update channel: ${error.message}`);
|
|
2114
|
+
process.exit(1);
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
// src/commands/channels/delete.ts
|
|
2119
|
+
init_esm_shims();
|
|
2120
|
+
init_api();
|
|
2121
|
+
import { Command as Command20 } from "commander";
|
|
2122
|
+
import chalk21 from "chalk";
|
|
2123
|
+
import inquirer13 from "inquirer";
|
|
2124
|
+
import ora16 from "ora";
|
|
2125
|
+
var deleteChannelCommand = new Command20("delete").description("Delete a channel").argument("<app-id>", "App ID").argument("<channel-id>", "Channel ID").option("-o, --org <org-id>", "Organization ID").option("-y, --yes", "Skip confirmation").action(async (appId, channelId, options) => {
|
|
2126
|
+
await requireAuth();
|
|
2127
|
+
const orgId = await resolveOrgId(options.org);
|
|
2128
|
+
if (!options.yes) {
|
|
2129
|
+
const { confirm } = await inquirer13.prompt([
|
|
2130
|
+
{
|
|
2131
|
+
type: "confirm",
|
|
2132
|
+
name: "confirm",
|
|
2133
|
+
message: "Are you sure you want to delete this channel? This cannot be undone.",
|
|
2134
|
+
default: false
|
|
2135
|
+
}
|
|
2136
|
+
]);
|
|
2137
|
+
if (!confirm) {
|
|
2138
|
+
console.log(chalk21.gray(" Cancelled"));
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const spinner = ora16("Deleting channel...").start();
|
|
2143
|
+
try {
|
|
2144
|
+
await api.deleteChannel(orgId, appId, channelId);
|
|
2145
|
+
spinner.succeed("Channel deleted");
|
|
2146
|
+
console.log("");
|
|
2147
|
+
} catch (error) {
|
|
2148
|
+
spinner.fail(`Failed to delete channel: ${error.message}`);
|
|
2149
|
+
process.exit(1);
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
// src/commands/channels/index.ts
|
|
2154
|
+
var channelsCommands = new Command21("channels").description("Manage channels");
|
|
2155
|
+
channelsCommands.addCommand(listChannelsCommand);
|
|
2156
|
+
channelsCommands.addCommand(createChannelCommand);
|
|
2157
|
+
channelsCommands.addCommand(updateChannelCommand);
|
|
2158
|
+
channelsCommands.addCommand(deleteChannelCommand);
|
|
2159
|
+
|
|
2160
|
+
// src/commands/ci-tokens/index.ts
|
|
2161
|
+
init_esm_shims();
|
|
2162
|
+
import { Command as Command26 } from "commander";
|
|
2163
|
+
|
|
2164
|
+
// src/commands/ci-tokens/list.ts
|
|
2165
|
+
init_esm_shims();
|
|
2166
|
+
init_api();
|
|
2167
|
+
import { Command as Command22 } from "commander";
|
|
2168
|
+
import chalk22 from "chalk";
|
|
2169
|
+
import inquirer14 from "inquirer";
|
|
2170
|
+
import ora17 from "ora";
|
|
2171
|
+
var listCITokensCommand = new Command22("list").description("List CI tokens for an app").argument("[app-id]", "App ID").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
2172
|
+
await requireAuth();
|
|
2173
|
+
const orgId = await resolveOrgId(options.org);
|
|
2174
|
+
if (!appId) {
|
|
2175
|
+
const apps = await api.getApps(orgId);
|
|
2176
|
+
const { selectedApp } = await inquirer14.prompt([
|
|
2177
|
+
{
|
|
2178
|
+
type: "list",
|
|
2179
|
+
name: "selectedApp",
|
|
2180
|
+
message: "Select app:",
|
|
2181
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
2182
|
+
}
|
|
2183
|
+
]);
|
|
2184
|
+
appId = selectedApp;
|
|
2185
|
+
}
|
|
2186
|
+
const spinner = ora17("Fetching CI tokens...").start();
|
|
2187
|
+
try {
|
|
2188
|
+
const tokens = await api.getCITokens(orgId, appId);
|
|
2189
|
+
spinner.stop();
|
|
2190
|
+
if (options.json) {
|
|
2191
|
+
console.log(JSON.stringify(tokens, null, 2));
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
if (tokens.length === 0) {
|
|
2195
|
+
console.log("");
|
|
2196
|
+
console.log(chalk22.gray(" No CI tokens found."));
|
|
2197
|
+
console.log(chalk22.gray(" Create one with: swiftpatch ci-tokens create"));
|
|
2198
|
+
console.log("");
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
console.log("");
|
|
2202
|
+
console.log(chalk22.bold(" CI Tokens"));
|
|
2203
|
+
console.log("");
|
|
2204
|
+
const table = createTable({
|
|
2205
|
+
head: ["Name", "Prefix", "Status", "Uses", "Last Used", "Created"],
|
|
2206
|
+
rows: tokens.map((t) => [
|
|
2207
|
+
chalk22.cyan(t.name),
|
|
2208
|
+
chalk22.gray(t.tokenPrefix + "..."),
|
|
2209
|
+
t.isActive ? chalk22.green("Active") : chalk22.red("Revoked"),
|
|
2210
|
+
String(t.usageCount),
|
|
2211
|
+
t.lastUsedAt ? new Date(t.lastUsedAt).toLocaleDateString() : chalk22.gray("Never"),
|
|
2212
|
+
new Date(t.createdAt).toLocaleDateString()
|
|
2213
|
+
])
|
|
2214
|
+
});
|
|
2215
|
+
console.log(table);
|
|
2216
|
+
console.log("");
|
|
2217
|
+
} catch (error) {
|
|
2218
|
+
spinner.fail(`Failed to fetch CI tokens: ${error.message}`);
|
|
2219
|
+
process.exit(1);
|
|
2220
|
+
}
|
|
2221
|
+
});
|
|
2222
|
+
|
|
2223
|
+
// src/commands/ci-tokens/create.ts
|
|
2224
|
+
init_esm_shims();
|
|
2225
|
+
init_api();
|
|
2226
|
+
import { Command as Command23 } from "commander";
|
|
2227
|
+
import chalk23 from "chalk";
|
|
2228
|
+
import inquirer15 from "inquirer";
|
|
2229
|
+
import ora18 from "ora";
|
|
2230
|
+
var createCITokenCommand = new Command23("create").description("Create a new CI token").argument("[app-id]", "App ID").option("-n, --name <name>", "Token name").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
2231
|
+
await requireAuth();
|
|
2232
|
+
const orgId = await resolveOrgId(options.org);
|
|
2233
|
+
if (!appId) {
|
|
2234
|
+
const apps = await api.getApps(orgId);
|
|
2235
|
+
const { selectedApp } = await inquirer15.prompt([
|
|
2236
|
+
{
|
|
2237
|
+
type: "list",
|
|
2238
|
+
name: "selectedApp",
|
|
2239
|
+
message: "Select app:",
|
|
2240
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
2241
|
+
}
|
|
2242
|
+
]);
|
|
2243
|
+
appId = selectedApp;
|
|
2244
|
+
}
|
|
2245
|
+
let tokenName = options.name;
|
|
2246
|
+
if (!tokenName) {
|
|
2247
|
+
const answers = await inquirer15.prompt([
|
|
2248
|
+
{
|
|
2249
|
+
type: "input",
|
|
2250
|
+
name: "name",
|
|
2251
|
+
message: 'Token name (e.g., "GitHub Actions", "Jenkins"):',
|
|
2252
|
+
validate: (input) => {
|
|
2253
|
+
if (!input.trim()) return "Token name is required";
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
]);
|
|
2258
|
+
tokenName = answers.name;
|
|
2259
|
+
}
|
|
2260
|
+
const spinner = ora18("Creating CI token...").start();
|
|
2261
|
+
try {
|
|
2262
|
+
const result = await api.createCIToken(orgId, appId, tokenName);
|
|
2263
|
+
spinner.succeed("CI token created!");
|
|
2264
|
+
if (options.json) {
|
|
2265
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
console.log("");
|
|
2269
|
+
console.log(chalk23.bold(" Your CI Token (shown only once):"));
|
|
2270
|
+
console.log("");
|
|
2271
|
+
console.log(` ${chalk23.green(result.token)}`);
|
|
2272
|
+
console.log("");
|
|
2273
|
+
logger.warning("Save this token now. It will not be shown again.");
|
|
2274
|
+
console.log("");
|
|
2275
|
+
console.log(chalk23.gray(" Usage in CI/CD:"));
|
|
2276
|
+
console.log(chalk23.gray(" $ swiftpatch publish-bundle -p ios --ci-token <token>"));
|
|
2277
|
+
console.log(chalk23.gray(" $ swiftpatch release-bundle --hash <hash> --app-version 1.0.0 --ci-token <token>"));
|
|
2278
|
+
console.log("");
|
|
2279
|
+
console.log(chalk23.gray(" Or set as environment variable:"));
|
|
2280
|
+
console.log(chalk23.gray(" $ export SWIFTPATCH_CI_TOKEN=<token>"));
|
|
2281
|
+
console.log("");
|
|
2282
|
+
} catch (error) {
|
|
2283
|
+
spinner.fail(`Failed to create CI token: ${error.message}`);
|
|
2284
|
+
process.exit(1);
|
|
2285
|
+
}
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2288
|
+
// src/commands/ci-tokens/delete.ts
|
|
2289
|
+
init_esm_shims();
|
|
2290
|
+
init_api();
|
|
2291
|
+
import { Command as Command24 } from "commander";
|
|
2292
|
+
import chalk24 from "chalk";
|
|
2293
|
+
import inquirer16 from "inquirer";
|
|
2294
|
+
import ora19 from "ora";
|
|
2295
|
+
var deleteCITokenCommand = new Command24("delete").description("Revoke a CI token").argument("<token-id>", "Token ID").option("-a, --app <app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-y, --yes", "Skip confirmation").action(async (tokenId, options) => {
|
|
2296
|
+
await requireAuth();
|
|
2297
|
+
const orgId = await resolveOrgId(options.org);
|
|
2298
|
+
let appId = options.app;
|
|
2299
|
+
if (!appId) {
|
|
2300
|
+
const apps = await api.getApps(orgId);
|
|
2301
|
+
const { selectedApp } = await inquirer16.prompt([
|
|
2302
|
+
{
|
|
2303
|
+
type: "list",
|
|
2304
|
+
name: "selectedApp",
|
|
2305
|
+
message: "Select app:",
|
|
2306
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
2307
|
+
}
|
|
2308
|
+
]);
|
|
2309
|
+
appId = selectedApp;
|
|
2310
|
+
}
|
|
2311
|
+
if (!options.yes) {
|
|
2312
|
+
const { confirm } = await inquirer16.prompt([
|
|
2313
|
+
{
|
|
2314
|
+
type: "confirm",
|
|
2315
|
+
name: "confirm",
|
|
2316
|
+
message: "Are you sure you want to revoke this CI token? Any pipelines using it will stop working.",
|
|
2317
|
+
default: false
|
|
2318
|
+
}
|
|
2319
|
+
]);
|
|
2320
|
+
if (!confirm) {
|
|
2321
|
+
console.log(chalk24.gray(" Cancelled"));
|
|
2322
|
+
return;
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
const spinner = ora19("Revoking CI token...").start();
|
|
2326
|
+
try {
|
|
2327
|
+
await api.deleteCIToken(orgId, appId, tokenId);
|
|
2328
|
+
spinner.succeed("CI token revoked");
|
|
2329
|
+
console.log("");
|
|
2330
|
+
} catch (error) {
|
|
2331
|
+
spinner.fail(`Failed to revoke CI token: ${error.message}`);
|
|
2332
|
+
process.exit(1);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
// src/commands/ci-tokens/regenerate.ts
|
|
2337
|
+
init_esm_shims();
|
|
2338
|
+
init_api();
|
|
2339
|
+
import { Command as Command25 } from "commander";
|
|
2340
|
+
import chalk25 from "chalk";
|
|
2341
|
+
import inquirer17 from "inquirer";
|
|
2342
|
+
import ora20 from "ora";
|
|
2343
|
+
var regenerateCITokenCommand = new Command25("regenerate").description("Regenerate a CI token (revokes old one)").argument("<token-id>", "Token ID").option("-a, --app <app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-y, --yes", "Skip confirmation").option("--json", "Output as JSON").action(async (tokenId, options) => {
|
|
2344
|
+
await requireAuth();
|
|
2345
|
+
const orgId = await resolveOrgId(options.org);
|
|
2346
|
+
let appId = options.app;
|
|
2347
|
+
if (!appId) {
|
|
2348
|
+
const apps = await api.getApps(orgId);
|
|
2349
|
+
const { selectedApp } = await inquirer17.prompt([
|
|
2350
|
+
{
|
|
2351
|
+
type: "list",
|
|
2352
|
+
name: "selectedApp",
|
|
2353
|
+
message: "Select app:",
|
|
2354
|
+
choices: apps.map((app) => ({ name: app.name, value: app.id }))
|
|
2355
|
+
}
|
|
2356
|
+
]);
|
|
2357
|
+
appId = selectedApp;
|
|
2358
|
+
}
|
|
2359
|
+
if (!options.yes) {
|
|
2360
|
+
const { confirm } = await inquirer17.prompt([
|
|
2361
|
+
{
|
|
2362
|
+
type: "confirm",
|
|
2363
|
+
name: "confirm",
|
|
2364
|
+
message: "This will revoke the old token. Pipelines using it will need to be updated. Continue?",
|
|
2365
|
+
default: false
|
|
2366
|
+
}
|
|
2367
|
+
]);
|
|
2368
|
+
if (!confirm) {
|
|
2369
|
+
console.log(chalk25.gray(" Cancelled"));
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
const spinner = ora20("Regenerating CI token...").start();
|
|
2374
|
+
try {
|
|
2375
|
+
const result = await api.regenerateCIToken(orgId, appId, tokenId);
|
|
2376
|
+
spinner.succeed("CI token regenerated!");
|
|
2377
|
+
if (options.json) {
|
|
2378
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
console.log("");
|
|
2382
|
+
console.log(chalk25.bold(" Your new CI Token (shown only once):"));
|
|
2383
|
+
console.log("");
|
|
2384
|
+
console.log(` ${chalk25.green(result.token)}`);
|
|
2385
|
+
console.log("");
|
|
2386
|
+
logger.warning("Save this token now. It will not be shown again.");
|
|
2387
|
+
logger.warning("The old token has been revoked.");
|
|
2388
|
+
console.log("");
|
|
2389
|
+
} catch (error) {
|
|
2390
|
+
spinner.fail(`Failed to regenerate CI token: ${error.message}`);
|
|
2391
|
+
process.exit(1);
|
|
2392
|
+
}
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
// src/commands/ci-tokens/index.ts
|
|
2396
|
+
var ciTokensCommands = new Command26("ci-tokens").description("Manage CI/CD tokens");
|
|
2397
|
+
ciTokensCommands.addCommand(listCITokensCommand);
|
|
2398
|
+
ciTokensCommands.addCommand(createCITokenCommand);
|
|
2399
|
+
ciTokensCommands.addCommand(deleteCITokenCommand);
|
|
2400
|
+
ciTokensCommands.addCommand(regenerateCITokenCommand);
|
|
2401
|
+
|
|
2402
|
+
// src/commands/webhooks/index.ts
|
|
2403
|
+
init_esm_shims();
|
|
2404
|
+
import { Command as Command32 } from "commander";
|
|
2405
|
+
|
|
2406
|
+
// src/commands/webhooks/list.ts
|
|
2407
|
+
init_esm_shims();
|
|
2408
|
+
init_api();
|
|
2409
|
+
import { Command as Command27 } from "commander";
|
|
2410
|
+
import chalk26 from "chalk";
|
|
2411
|
+
import ora21 from "ora";
|
|
2412
|
+
var listWebhooksCommand = new Command27("list").description("List webhooks").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
|
|
2413
|
+
await requireAuth();
|
|
2414
|
+
const orgId = await resolveOrgId(options.org);
|
|
2415
|
+
const spinner = ora21("Fetching webhooks...").start();
|
|
2416
|
+
try {
|
|
2417
|
+
const webhooks = await api.getWebhooks(orgId);
|
|
2418
|
+
spinner.stop();
|
|
2419
|
+
if (options.json) {
|
|
2420
|
+
console.log(JSON.stringify(webhooks, null, 2));
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
if (webhooks.length === 0) {
|
|
2424
|
+
console.log("");
|
|
2425
|
+
console.log(chalk26.gray(" No webhooks configured."));
|
|
2426
|
+
console.log(chalk26.gray(" Create one with: swiftpatch webhooks create"));
|
|
2427
|
+
console.log("");
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
console.log("");
|
|
2431
|
+
console.log(chalk26.bold(" Webhooks"));
|
|
2432
|
+
console.log("");
|
|
2433
|
+
const table = createTable({
|
|
2434
|
+
head: ["ID", "URL", "Events", "Status", "Deliveries"],
|
|
2435
|
+
rows: webhooks.map((w) => [
|
|
2436
|
+
chalk26.gray(w.id.slice(0, 8) + "..."),
|
|
2437
|
+
chalk26.cyan(w.url.length > 40 ? w.url.slice(0, 40) + "..." : w.url),
|
|
2438
|
+
w.events.join(", "),
|
|
2439
|
+
w.isActive ? chalk26.green("Active") : chalk26.red("Inactive"),
|
|
2440
|
+
w.stats ? `${w.stats.successfulDeliveries}/${w.stats.totalDeliveries}` : "0/0"
|
|
2441
|
+
])
|
|
2442
|
+
});
|
|
2443
|
+
console.log(table);
|
|
2444
|
+
console.log("");
|
|
2445
|
+
} catch (error) {
|
|
2446
|
+
spinner.fail(`Failed to fetch webhooks: ${error.message}`);
|
|
2447
|
+
process.exit(1);
|
|
2448
|
+
}
|
|
2449
|
+
});
|
|
2450
|
+
|
|
2451
|
+
// src/commands/webhooks/create.ts
|
|
2452
|
+
init_esm_shims();
|
|
2453
|
+
init_api();
|
|
2454
|
+
import { Command as Command28 } from "commander";
|
|
2455
|
+
import chalk27 from "chalk";
|
|
2456
|
+
import inquirer18 from "inquirer";
|
|
2457
|
+
import ora22 from "ora";
|
|
2458
|
+
var WEBHOOK_EVENTS = [
|
|
2459
|
+
"release.published",
|
|
2460
|
+
"release.disabled",
|
|
2461
|
+
"update.success",
|
|
2462
|
+
"update.failed",
|
|
2463
|
+
"app.created",
|
|
2464
|
+
"app.deleted"
|
|
2465
|
+
];
|
|
2466
|
+
var createWebhookCommand = new Command28("create").description("Create a webhook").option("-u, --url <url>", "Webhook URL").option("-e, --events <events...>", "Events to subscribe to").option("-s, --secret <secret>", "Webhook signing secret").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (options) => {
|
|
2467
|
+
await requireAuth();
|
|
2468
|
+
const orgId = await resolveOrgId(options.org);
|
|
2469
|
+
let url = options.url;
|
|
2470
|
+
if (!url) {
|
|
2471
|
+
const answers = await inquirer18.prompt([
|
|
2472
|
+
{
|
|
2473
|
+
type: "input",
|
|
2474
|
+
name: "url",
|
|
2475
|
+
message: "Webhook URL:",
|
|
2476
|
+
validate: (input) => {
|
|
2477
|
+
if (!input.trim()) return "URL is required";
|
|
2478
|
+
try {
|
|
2479
|
+
new URL(input);
|
|
2480
|
+
return true;
|
|
2481
|
+
} catch {
|
|
2482
|
+
return "Invalid URL";
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
]);
|
|
2487
|
+
url = answers.url;
|
|
2488
|
+
}
|
|
2489
|
+
let events = options.events;
|
|
2490
|
+
if (!events || events.length === 0) {
|
|
2491
|
+
const answers = await inquirer18.prompt([
|
|
2492
|
+
{
|
|
2493
|
+
type: "checkbox",
|
|
2494
|
+
name: "events",
|
|
2495
|
+
message: "Select events to subscribe to:",
|
|
2496
|
+
choices: WEBHOOK_EVENTS,
|
|
2497
|
+
validate: (input) => {
|
|
2498
|
+
if (input.length === 0) return "Select at least one event";
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
]);
|
|
2503
|
+
events = answers.events;
|
|
2504
|
+
}
|
|
2505
|
+
const spinner = ora22("Creating webhook...").start();
|
|
2506
|
+
try {
|
|
2507
|
+
const webhook = await api.createWebhook(orgId, {
|
|
2508
|
+
url,
|
|
2509
|
+
events,
|
|
2510
|
+
secret: options.secret
|
|
2511
|
+
});
|
|
2512
|
+
spinner.succeed("Webhook created!");
|
|
2513
|
+
if (options.json) {
|
|
2514
|
+
console.log(JSON.stringify(webhook, null, 2));
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
console.log("");
|
|
2518
|
+
console.log(` ID: ${chalk27.gray(webhook.id)}`);
|
|
2519
|
+
console.log(` URL: ${chalk27.cyan(webhook.url)}`);
|
|
2520
|
+
console.log(` Events: ${webhook.events.join(", ")}`);
|
|
2521
|
+
if (webhook.secret) {
|
|
2522
|
+
console.log(` Secret: ${chalk27.green(webhook.secret)}`);
|
|
2523
|
+
console.log(chalk27.yellow(" Save this secret \u2014 it will not be shown again."));
|
|
2524
|
+
}
|
|
2525
|
+
console.log("");
|
|
2526
|
+
} catch (error) {
|
|
2527
|
+
spinner.fail(`Failed to create webhook: ${error.message}`);
|
|
2528
|
+
process.exit(1);
|
|
2529
|
+
}
|
|
2530
|
+
});
|
|
2531
|
+
|
|
2532
|
+
// src/commands/webhooks/update.ts
|
|
2533
|
+
init_esm_shims();
|
|
2534
|
+
init_api();
|
|
2535
|
+
import { Command as Command29 } from "commander";
|
|
2536
|
+
import chalk28 from "chalk";
|
|
2537
|
+
import ora23 from "ora";
|
|
2538
|
+
var updateWebhookCommand = new Command29("update").description("Update a webhook").argument("<webhook-id>", "Webhook ID").option("-u, --url <url>", "New URL").option("-e, --events <events...>", "New events list").option("-s, --secret <secret>", "New signing secret").option("--enable", "Enable webhook").option("--disable", "Disable webhook").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (webhookId, options) => {
|
|
2539
|
+
await requireAuth();
|
|
2540
|
+
const orgId = await resolveOrgId(options.org);
|
|
2541
|
+
const params = {};
|
|
2542
|
+
if (options.url) params.url = options.url;
|
|
2543
|
+
if (options.events) params.events = options.events;
|
|
2544
|
+
if (options.secret) params.secret = options.secret;
|
|
2545
|
+
if (options.enable) params.isActive = true;
|
|
2546
|
+
if (options.disable) params.isActive = false;
|
|
2547
|
+
if (Object.keys(params).length === 0) {
|
|
2548
|
+
console.log(chalk28.yellow(" No changes specified. Use --url, --events, --secret, --enable, or --disable"));
|
|
2549
|
+
process.exit(1);
|
|
2550
|
+
}
|
|
2551
|
+
const spinner = ora23("Updating webhook...").start();
|
|
2552
|
+
try {
|
|
2553
|
+
const webhook = await api.updateWebhook(orgId, webhookId, params);
|
|
2554
|
+
spinner.succeed("Webhook updated!");
|
|
2555
|
+
if (options.json) {
|
|
2556
|
+
console.log(JSON.stringify(webhook, null, 2));
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
console.log("");
|
|
2560
|
+
console.log(` URL: ${chalk28.cyan(webhook.url)}`);
|
|
2561
|
+
console.log(` Events: ${webhook.events.join(", ")}`);
|
|
2562
|
+
console.log(` Status: ${webhook.isActive ? chalk28.green("Active") : chalk28.red("Inactive")}`);
|
|
2563
|
+
console.log("");
|
|
2564
|
+
} catch (error) {
|
|
2565
|
+
spinner.fail(`Failed to update webhook: ${error.message}`);
|
|
2566
|
+
process.exit(1);
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
|
|
2570
|
+
// src/commands/webhooks/delete.ts
|
|
2571
|
+
init_esm_shims();
|
|
2572
|
+
init_api();
|
|
2573
|
+
import { Command as Command30 } from "commander";
|
|
2574
|
+
import chalk29 from "chalk";
|
|
2575
|
+
import inquirer19 from "inquirer";
|
|
2576
|
+
import ora24 from "ora";
|
|
2577
|
+
var deleteWebhookCommand = new Command30("delete").description("Delete a webhook").argument("<webhook-id>", "Webhook ID").option("-o, --org <org-id>", "Organization ID").option("-y, --yes", "Skip confirmation").action(async (webhookId, options) => {
|
|
2578
|
+
await requireAuth();
|
|
2579
|
+
const orgId = await resolveOrgId(options.org);
|
|
2580
|
+
if (!options.yes) {
|
|
2581
|
+
const { confirm } = await inquirer19.prompt([
|
|
2582
|
+
{
|
|
2583
|
+
type: "confirm",
|
|
2584
|
+
name: "confirm",
|
|
2585
|
+
message: "Are you sure you want to delete this webhook?",
|
|
2586
|
+
default: false
|
|
2587
|
+
}
|
|
2588
|
+
]);
|
|
2589
|
+
if (!confirm) {
|
|
2590
|
+
console.log(chalk29.gray(" Cancelled"));
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
}
|
|
2594
|
+
const spinner = ora24("Deleting webhook...").start();
|
|
2595
|
+
try {
|
|
2596
|
+
await api.deleteWebhook(orgId, webhookId);
|
|
2597
|
+
spinner.succeed("Webhook deleted");
|
|
2598
|
+
console.log("");
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
spinner.fail(`Failed to delete webhook: ${error.message}`);
|
|
2601
|
+
process.exit(1);
|
|
2602
|
+
}
|
|
2603
|
+
});
|
|
2604
|
+
|
|
2605
|
+
// src/commands/webhooks/test.ts
|
|
2606
|
+
init_esm_shims();
|
|
2607
|
+
init_api();
|
|
2608
|
+
import { Command as Command31 } from "commander";
|
|
2609
|
+
import ora25 from "ora";
|
|
2610
|
+
var testWebhookCommand = new Command31("test").description("Send a test event to a webhook").argument("<webhook-id>", "Webhook ID").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (webhookId, options) => {
|
|
2611
|
+
await requireAuth();
|
|
2612
|
+
const orgId = await resolveOrgId(options.org);
|
|
2613
|
+
const spinner = ora25("Sending test event...").start();
|
|
2614
|
+
try {
|
|
2615
|
+
const result = await api.testWebhook(orgId, webhookId);
|
|
2616
|
+
if (result.success) {
|
|
2617
|
+
spinner.succeed(`Test event delivered (status ${result.statusCode})`);
|
|
2618
|
+
} else {
|
|
2619
|
+
spinner.fail(`Test event failed: ${result.error || "Unknown error"}`);
|
|
2620
|
+
}
|
|
2621
|
+
if (options.json) {
|
|
2622
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
console.log("");
|
|
2626
|
+
} catch (error) {
|
|
2627
|
+
spinner.fail(`Failed to send test event: ${error.message}`);
|
|
2628
|
+
process.exit(1);
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
|
|
2632
|
+
// src/commands/webhooks/index.ts
|
|
2633
|
+
var webhooksCommands = new Command32("webhooks").description("Manage webhooks");
|
|
2634
|
+
webhooksCommands.addCommand(listWebhooksCommand);
|
|
2635
|
+
webhooksCommands.addCommand(createWebhookCommand);
|
|
2636
|
+
webhooksCommands.addCommand(updateWebhookCommand);
|
|
2637
|
+
webhooksCommands.addCommand(deleteWebhookCommand);
|
|
2638
|
+
webhooksCommands.addCommand(testWebhookCommand);
|
|
2639
|
+
|
|
2640
|
+
// src/commands/analytics.ts
|
|
2641
|
+
init_esm_shims();
|
|
2642
|
+
init_api();
|
|
2643
|
+
import { Command as Command33 } from "commander";
|
|
2644
|
+
import chalk30 from "chalk";
|
|
2645
|
+
import ora26 from "ora";
|
|
2646
|
+
var analyticsCommand = new Command33("analytics").description("View analytics for an app").argument("<app-id>", "App ID").option("-o, --org <org-id>", "Organization ID").option("-r, --release <release-id>", "Filter by release").option("-d, --days <days>", "Time range in days", "7").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
2647
|
+
await requireAuth();
|
|
2648
|
+
const orgId = await resolveOrgId(options.org);
|
|
2649
|
+
const spinner = ora26("Fetching analytics...").start();
|
|
2650
|
+
try {
|
|
2651
|
+
const analytics = await api.getAnalytics(orgId, appId, {
|
|
2652
|
+
days: parseInt(options.days, 10),
|
|
2653
|
+
releaseId: options.release
|
|
2654
|
+
});
|
|
2655
|
+
spinner.stop();
|
|
2656
|
+
if (options.json) {
|
|
2657
|
+
console.log(JSON.stringify(analytics, null, 2));
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
console.log("");
|
|
2661
|
+
console.log(chalk30.bold(" Analytics Overview"));
|
|
2662
|
+
console.log("");
|
|
2663
|
+
console.log(` Total Updates: ${chalk30.cyan((analytics.totalUpdates ?? 0).toLocaleString())}`);
|
|
2664
|
+
console.log(` Success Rate: ${chalk30.green((analytics.successRate ?? 0).toFixed(1) + "%")}`);
|
|
2665
|
+
console.log(` Avg Patch Size: ${chalk30.cyan(formatBytes3(analytics.avgPatchSize ?? 0))}`);
|
|
2666
|
+
console.log(` Bandwidth Saved: ${chalk30.green(formatBytes3(analytics.bandwidthSaved ?? 0))}`);
|
|
2667
|
+
console.log(` Bandwidth Used: ${chalk30.gray(formatBytes3(analytics.totalBandwidthUsed ?? 0))}`);
|
|
2668
|
+
console.log("");
|
|
2669
|
+
} catch (error) {
|
|
2670
|
+
spinner.fail(`Failed to fetch analytics: ${error.message}`);
|
|
2671
|
+
process.exit(1);
|
|
2672
|
+
}
|
|
2673
|
+
});
|
|
2674
|
+
function formatBytes3(bytes) {
|
|
2675
|
+
if (bytes < 1024) return bytes + " B";
|
|
2676
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
2677
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
2678
|
+
}
|
|
2679
|
+
|
|
2680
|
+
// src/commands/ai.ts
|
|
2681
|
+
init_esm_shims();
|
|
2682
|
+
init_api();
|
|
2683
|
+
import { Command as Command37 } from "commander";
|
|
2684
|
+
import chalk35 from "chalk";
|
|
2685
|
+
import ora30 from "ora";
|
|
2686
|
+
|
|
2687
|
+
// src/commands/ai-doctor.ts
|
|
2688
|
+
init_esm_shims();
|
|
2689
|
+
import { Command as Command34 } from "commander";
|
|
2690
|
+
import chalk32 from "chalk";
|
|
2691
|
+
import ora27 from "ora";
|
|
2692
|
+
import path5 from "path";
|
|
2693
|
+
import fs7 from "fs-extra";
|
|
2694
|
+
|
|
2695
|
+
// src/lib/claude.ts
|
|
2696
|
+
init_esm_shims();
|
|
2697
|
+
init_auth();
|
|
2698
|
+
init_api();
|
|
2699
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
2700
|
+
var CLAUDE_MODEL = "claude-sonnet-4-5-20250929";
|
|
2701
|
+
var MAX_TOKENS = 4096;
|
|
2702
|
+
var TIMEOUT_MS = 12e4;
|
|
2703
|
+
async function resolveApiKey() {
|
|
2704
|
+
const envKey = process.env.SWIFTPATCH_CLAUDE_API_KEY;
|
|
2705
|
+
if (envKey && isValidClaudeKey(envKey)) {
|
|
2706
|
+
return envKey;
|
|
2707
|
+
}
|
|
2708
|
+
const storedKey = auth.getClaudeApiKey();
|
|
2709
|
+
if (storedKey && isValidClaudeKey(storedKey)) {
|
|
2710
|
+
return storedKey;
|
|
2711
|
+
}
|
|
2712
|
+
if (auth.isLoggedIn()) {
|
|
2713
|
+
try {
|
|
2714
|
+
const { apiKey } = await api.getClaudeApiKey();
|
|
2715
|
+
if (apiKey && isValidClaudeKey(apiKey)) {
|
|
2716
|
+
return apiKey;
|
|
2717
|
+
}
|
|
2718
|
+
} catch {
|
|
2719
|
+
logger.debug("Platform Claude key not available, skipping");
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
return null;
|
|
2723
|
+
}
|
|
2724
|
+
function isValidClaudeKey(key) {
|
|
2725
|
+
return typeof key === "string" && key.startsWith("sk-ant-") && key.length > 20;
|
|
2726
|
+
}
|
|
2727
|
+
async function createClient() {
|
|
2728
|
+
const apiKey = await resolveApiKey();
|
|
2729
|
+
if (!apiKey) {
|
|
2730
|
+
throw new Error(
|
|
2731
|
+
"No Claude API key configured.\n\n Set one with:\n swiftpatch config set claudeApiKey <your-key>\n\n Or set the SWIFTPATCH_CLAUDE_API_KEY environment variable.\n\n Get a key at: https://console.anthropic.com/settings/keys"
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
return new Anthropic({
|
|
2735
|
+
apiKey,
|
|
2736
|
+
timeout: TIMEOUT_MS,
|
|
2737
|
+
maxRetries: 2
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
async function claudeChat(messages, options = {}) {
|
|
2741
|
+
const client = await createClient();
|
|
2742
|
+
const response = await client.messages.create({
|
|
2743
|
+
model: CLAUDE_MODEL,
|
|
2744
|
+
max_tokens: options.maxTokens || MAX_TOKENS,
|
|
2745
|
+
...options.system ? { system: options.system } : {},
|
|
2746
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {},
|
|
2747
|
+
messages
|
|
2748
|
+
});
|
|
2749
|
+
const textBlock = response.content.find((block) => block.type === "text");
|
|
2750
|
+
if (!textBlock || textBlock.type !== "text") {
|
|
2751
|
+
throw new Error("Unexpected response format from Claude");
|
|
2752
|
+
}
|
|
2753
|
+
return textBlock.text;
|
|
2754
|
+
}
|
|
2755
|
+
async function claudeStream(messages, options = {}) {
|
|
2756
|
+
const client = await createClient();
|
|
2757
|
+
let fullText = "";
|
|
2758
|
+
const stream = client.messages.stream({
|
|
2759
|
+
model: CLAUDE_MODEL,
|
|
2760
|
+
max_tokens: options.maxTokens || MAX_TOKENS,
|
|
2761
|
+
...options.system ? { system: options.system } : {},
|
|
2762
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {},
|
|
2763
|
+
messages
|
|
2764
|
+
});
|
|
2765
|
+
for await (const event of stream) {
|
|
2766
|
+
if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
|
|
2767
|
+
process.stdout.write(event.delta.text);
|
|
2768
|
+
fullText += event.delta.text;
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
return fullText;
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
// src/utils/consent.ts
|
|
2775
|
+
init_esm_shims();
|
|
2776
|
+
init_config();
|
|
2777
|
+
import chalk31 from "chalk";
|
|
2778
|
+
import inquirer20 from "inquirer";
|
|
2779
|
+
var SCOPE_DESCRIPTIONS = {
|
|
2780
|
+
"project-files": "Project config files (package.json, build.gradle, Podfile)",
|
|
2781
|
+
"git-history": "Git commit messages, file change names, and diff stats",
|
|
2782
|
+
"git-diff": "Code diff content from your changed JS/TS files",
|
|
2783
|
+
"project-structure": "Project directory structure and file existence checks"
|
|
2784
|
+
};
|
|
2785
|
+
async function requireConsent(scopes, options = {}) {
|
|
2786
|
+
if (options.yes) {
|
|
2787
|
+
return true;
|
|
2788
|
+
}
|
|
2789
|
+
const consentedScopes = config.get("aiConsentScopes");
|
|
2790
|
+
if (consentedScopes) {
|
|
2791
|
+
const allConsented = scopes.every((s) => consentedScopes.includes(s));
|
|
2792
|
+
if (allConsented) {
|
|
2793
|
+
return true;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
console.log("");
|
|
2797
|
+
console.log(chalk31.bold(" Data Access Notice"));
|
|
2798
|
+
console.log("");
|
|
2799
|
+
console.log(" This command will read the following local data and send");
|
|
2800
|
+
console.log(" a summary to Claude AI (Anthropic) for analysis:");
|
|
2801
|
+
console.log("");
|
|
2802
|
+
for (const scope of scopes) {
|
|
2803
|
+
console.log(` ${chalk31.cyan("\u2022")} ${SCOPE_DESCRIPTIONS[scope]}`);
|
|
2804
|
+
}
|
|
2805
|
+
console.log("");
|
|
2806
|
+
console.log(chalk31.gray(" No source code is stored. Data is sent via encrypted HTTPS."));
|
|
2807
|
+
console.log(chalk31.gray(" See: https://docs.swiftpatch.io/privacy/ai"));
|
|
2808
|
+
console.log("");
|
|
2809
|
+
const { consent } = await inquirer20.prompt([
|
|
2810
|
+
{
|
|
2811
|
+
type: "confirm",
|
|
2812
|
+
name: "consent",
|
|
2813
|
+
message: "Allow SwiftPatch AI to analyze this data?",
|
|
2814
|
+
default: true
|
|
2815
|
+
}
|
|
2816
|
+
]);
|
|
2817
|
+
if (!consent) {
|
|
2818
|
+
logger.info("AI analysis cancelled. No data was sent.");
|
|
2819
|
+
return false;
|
|
2820
|
+
}
|
|
2821
|
+
const { remember } = await inquirer20.prompt([
|
|
2822
|
+
{
|
|
2823
|
+
type: "confirm",
|
|
2824
|
+
name: "remember",
|
|
2825
|
+
message: "Remember this choice for future runs?",
|
|
2826
|
+
default: true
|
|
2827
|
+
}
|
|
2828
|
+
]);
|
|
2829
|
+
if (remember) {
|
|
2830
|
+
const existing = config.get("aiConsentScopes") || [];
|
|
2831
|
+
const merged = [.../* @__PURE__ */ new Set([...existing, ...scopes])];
|
|
2832
|
+
config.set("aiConsentScopes", merged);
|
|
2833
|
+
logger.info(`Consent saved. Reset with: ${chalk31.cyan("swiftpatch config delete aiConsentScopes")}`);
|
|
2834
|
+
}
|
|
2835
|
+
return true;
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
// src/commands/ai-doctor.ts
|
|
2839
|
+
async function collectDiagnostics(projectRoot) {
|
|
2840
|
+
const diag = {
|
|
2841
|
+
projectRoot,
|
|
2842
|
+
packageJson: null,
|
|
2843
|
+
reactNativeVersion: null,
|
|
2844
|
+
nativeDeps: [],
|
|
2845
|
+
assets: [],
|
|
2846
|
+
hermesEnabled: false,
|
|
2847
|
+
hasSigningKey: false,
|
|
2848
|
+
podfileLockHash: null,
|
|
2849
|
+
gradleDeps: [],
|
|
2850
|
+
warnings: []
|
|
2851
|
+
};
|
|
2852
|
+
const pkgPath = path5.join(projectRoot, "package.json");
|
|
2853
|
+
if (await fs7.pathExists(pkgPath)) {
|
|
2854
|
+
try {
|
|
2855
|
+
diag.packageJson = await fs7.readJson(pkgPath);
|
|
2856
|
+
const deps = { ...diag.packageJson?.dependencies, ...diag.packageJson?.devDependencies };
|
|
2857
|
+
diag.reactNativeVersion = deps?.["react-native"] || null;
|
|
2858
|
+
const nativeModulePatterns = [
|
|
2859
|
+
"react-native-",
|
|
2860
|
+
"@react-native/",
|
|
2861
|
+
"@react-native-community/",
|
|
2862
|
+
"expo-",
|
|
2863
|
+
"react-native-reanimated",
|
|
2864
|
+
"react-native-gesture-handler",
|
|
2865
|
+
"react-native-screens",
|
|
2866
|
+
"react-native-svg",
|
|
2867
|
+
"lottie-react-native",
|
|
2868
|
+
"@shopify/react-native-skia"
|
|
2869
|
+
];
|
|
2870
|
+
for (const [dep] of Object.entries(deps || {})) {
|
|
2871
|
+
if (nativeModulePatterns.some((p) => dep.startsWith(p) || dep === p)) {
|
|
2872
|
+
diag.nativeDeps.push(`${dep}@${deps[dep]}`);
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
} catch {
|
|
2876
|
+
diag.warnings.push("Failed to parse package.json");
|
|
2877
|
+
}
|
|
2878
|
+
} else {
|
|
2879
|
+
diag.warnings.push("No package.json found \u2014 are you in a React Native project root?");
|
|
2880
|
+
}
|
|
2881
|
+
const assetDirs = ["assets", "src/assets", "app/assets", "src/images"];
|
|
2882
|
+
for (const dir of assetDirs) {
|
|
2883
|
+
const fullDir = path5.join(projectRoot, dir);
|
|
2884
|
+
if (await fs7.pathExists(fullDir)) {
|
|
2885
|
+
try {
|
|
2886
|
+
const files = await fs7.readdir(fullDir);
|
|
2887
|
+
for (const file of files.slice(0, 50)) {
|
|
2888
|
+
try {
|
|
2889
|
+
const stat = await fs7.stat(path5.join(fullDir, file));
|
|
2890
|
+
if (stat.isFile() && stat.size > 100 * 1024) {
|
|
2891
|
+
diag.assets.push({ name: `${dir}/${file}`, sizeKB: Math.round(stat.size / 1024) });
|
|
2892
|
+
}
|
|
2893
|
+
} catch {
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
} catch {
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
const gradlePath = path5.join(projectRoot, "android/app/build.gradle");
|
|
2901
|
+
if (await fs7.pathExists(gradlePath)) {
|
|
2902
|
+
try {
|
|
2903
|
+
const gradle = await fs7.readFile(gradlePath, "utf-8");
|
|
2904
|
+
diag.hermesEnabled = gradle.includes("hermesEnabled") && !gradle.includes("hermesEnabled: false");
|
|
2905
|
+
const depMatches = gradle.match(/implementation\s+['"]([^'"]+)['"]/g);
|
|
2906
|
+
if (depMatches) {
|
|
2907
|
+
diag.gradleDeps = depMatches.slice(0, 20).map((m) => m.replace(/implementation\s+['"]/, "").replace(/['"]/, ""));
|
|
2908
|
+
}
|
|
2909
|
+
} catch {
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
const gradlePropsPath = path5.join(projectRoot, "android/gradle.properties");
|
|
2913
|
+
if (await fs7.pathExists(gradlePropsPath)) {
|
|
2914
|
+
try {
|
|
2915
|
+
const props = await fs7.readFile(gradlePropsPath, "utf-8");
|
|
2916
|
+
if (props.includes("newArchEnabled=true")) {
|
|
2917
|
+
diag.warnings.push("New Architecture is enabled \u2014 ensure your OTA bundle is compatible");
|
|
2918
|
+
}
|
|
2919
|
+
} catch {
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
const signingPaths = [
|
|
2923
|
+
path5.join(projectRoot, "swiftpatch-private.pem"),
|
|
2924
|
+
path5.join(projectRoot, ".swiftpatch/private.pem"),
|
|
2925
|
+
path5.join(projectRoot, "keys/swiftpatch-private.pem")
|
|
2926
|
+
];
|
|
2927
|
+
for (const p of signingPaths) {
|
|
2928
|
+
if (await fs7.pathExists(p)) {
|
|
2929
|
+
diag.hasSigningKey = true;
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
const podfileLock = path5.join(projectRoot, "ios/Podfile.lock");
|
|
2934
|
+
if (await fs7.pathExists(podfileLock)) {
|
|
2935
|
+
try {
|
|
2936
|
+
const stat = await fs7.stat(podfileLock);
|
|
2937
|
+
diag.podfileLockHash = `modified:${stat.mtimeMs}`;
|
|
2938
|
+
} catch {
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
return diag;
|
|
2942
|
+
}
|
|
2943
|
+
var aiDoctorCommand = new Command34("doctor").description("AI-powered pre-publish diagnostics for your React Native project").option("-d, --dir <path>", "Project root directory", process.cwd()).option("-y, --yes", "Skip consent prompt (for CI/CD)").option("--json", "Output raw diagnostics as JSON").action(async (options) => {
|
|
2944
|
+
const projectRoot = path5.resolve(options.dir);
|
|
2945
|
+
if (!await fs7.pathExists(path5.join(projectRoot, "package.json"))) {
|
|
2946
|
+
logger.error("No package.json found. Run this from your React Native project root.");
|
|
2947
|
+
process.exit(1);
|
|
2948
|
+
}
|
|
2949
|
+
const consented = await requireConsent(
|
|
2950
|
+
["project-files", "project-structure"],
|
|
2951
|
+
{ yes: options.yes }
|
|
2952
|
+
);
|
|
2953
|
+
if (!consented) {
|
|
2954
|
+
process.exit(0);
|
|
2955
|
+
}
|
|
2956
|
+
const spinner = ora27("Scanning project...").start();
|
|
2957
|
+
let diag;
|
|
2958
|
+
try {
|
|
2959
|
+
diag = await collectDiagnostics(projectRoot);
|
|
2960
|
+
spinner.succeed("Project scanned");
|
|
2961
|
+
} catch (error) {
|
|
2962
|
+
spinner.fail(`Scan failed: ${error.message}`);
|
|
2963
|
+
process.exit(1);
|
|
2964
|
+
}
|
|
2965
|
+
if (options.json) {
|
|
2966
|
+
console.log(JSON.stringify(diag, null, 2));
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
console.log("");
|
|
2970
|
+
console.log(chalk32.bold(" Project Diagnostics"));
|
|
2971
|
+
console.log("");
|
|
2972
|
+
if (diag.reactNativeVersion) {
|
|
2973
|
+
console.log(` React Native: ${chalk32.cyan(diag.reactNativeVersion)}`);
|
|
2974
|
+
}
|
|
2975
|
+
console.log(` Native deps: ${chalk32.cyan(String(diag.nativeDeps.length))}`);
|
|
2976
|
+
console.log(` Hermes: ${diag.hermesEnabled ? chalk32.green("Enabled") : chalk32.yellow("Disabled")}`);
|
|
2977
|
+
console.log(` Signing key: ${diag.hasSigningKey ? chalk32.green("Found") : chalk32.yellow("Not found")}`);
|
|
2978
|
+
if (diag.assets.length > 0) {
|
|
2979
|
+
console.log("");
|
|
2980
|
+
console.log(chalk32.bold(" Large Assets (>100KB):"));
|
|
2981
|
+
for (const asset of diag.assets.slice(0, 10)) {
|
|
2982
|
+
const color = asset.sizeKB > 500 ? chalk32.red : chalk32.yellow;
|
|
2983
|
+
console.log(` ${color(`${asset.sizeKB}KB`)} ${asset.name}`);
|
|
2984
|
+
}
|
|
2985
|
+
if (diag.assets.length > 10) {
|
|
2986
|
+
console.log(chalk32.gray(` ... and ${diag.assets.length - 10} more`));
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
if (diag.warnings.length > 0) {
|
|
2990
|
+
console.log("");
|
|
2991
|
+
for (const w of diag.warnings) {
|
|
2992
|
+
logger.warning(w);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
const aiSpinner = ora27("Analyzing with Claude AI...").start();
|
|
2996
|
+
try {
|
|
2997
|
+
const systemPrompt = "You are SwiftPatch Doctor, an expert in React Native OTA (Over-The-Air) updates. Analyze the project diagnostics and provide a concise, actionable report. Focus on:\n1. Native dependency risks: identify deps that require a full app store release if changed\n2. Bundle size concerns: large assets, unnecessary imports\n3. Hermes compatibility: is the config optimal for OTA?\n4. Signing: is bundle signing configured?\n5. Overall OTA readiness score (1-10)\n\nFormat your response with clear sections using markdown-style headers (##). Be direct and specific. Do not explain what OTA is. The user is a developer.";
|
|
2998
|
+
const diagnosticsSummary = JSON.stringify({
|
|
2999
|
+
reactNativeVersion: diag.reactNativeVersion,
|
|
3000
|
+
nativeDeps: diag.nativeDeps,
|
|
3001
|
+
largeAssets: diag.assets,
|
|
3002
|
+
hermesEnabled: diag.hermesEnabled,
|
|
3003
|
+
hasSigningKey: diag.hasSigningKey,
|
|
3004
|
+
hasPodfileLock: !!diag.podfileLockHash,
|
|
3005
|
+
gradleDeps: diag.gradleDeps,
|
|
3006
|
+
localWarnings: diag.warnings
|
|
3007
|
+
}, null, 2);
|
|
3008
|
+
const analysis = await claudeChat(
|
|
3009
|
+
[{ role: "user", content: `Analyze this React Native project for OTA update readiness:
|
|
3010
|
+
|
|
3011
|
+
${diagnosticsSummary}` }],
|
|
3012
|
+
{ system: systemPrompt, temperature: 0.3 }
|
|
3013
|
+
);
|
|
3014
|
+
aiSpinner.succeed("AI analysis complete");
|
|
3015
|
+
console.log("");
|
|
3016
|
+
console.log(chalk32.bold(" AI Analysis"));
|
|
3017
|
+
console.log("");
|
|
3018
|
+
for (const line of analysis.split("\n")) {
|
|
3019
|
+
console.log(` ${line}`);
|
|
3020
|
+
}
|
|
3021
|
+
console.log("");
|
|
3022
|
+
} catch (error) {
|
|
3023
|
+
aiSpinner.fail(`AI analysis failed: ${error.message}`);
|
|
3024
|
+
console.log("");
|
|
3025
|
+
console.log(chalk32.gray(" Local diagnostics above are still valid."));
|
|
3026
|
+
console.log("");
|
|
3027
|
+
process.exit(1);
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
|
|
3031
|
+
// src/commands/ai-explain.ts
|
|
3032
|
+
init_esm_shims();
|
|
3033
|
+
import { Command as Command35 } from "commander";
|
|
3034
|
+
import chalk33 from "chalk";
|
|
3035
|
+
import ora28 from "ora";
|
|
3036
|
+
import { execFile } from "child_process";
|
|
3037
|
+
import { promisify } from "util";
|
|
3038
|
+
var execFileAsync = promisify(execFile);
|
|
3039
|
+
var MAX_CONTEXT_BYTES = 12e3;
|
|
3040
|
+
async function gitCommand(args, cwd) {
|
|
3041
|
+
try {
|
|
3042
|
+
const { stdout } = await execFileAsync("git", args, {
|
|
3043
|
+
cwd,
|
|
3044
|
+
maxBuffer: 512 * 1024,
|
|
3045
|
+
timeout: 15e3
|
|
3046
|
+
});
|
|
3047
|
+
return stdout.trim();
|
|
3048
|
+
} catch {
|
|
3049
|
+
return null;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
async function getLastReleaseTag(cwd) {
|
|
3053
|
+
const patterns = ["swiftpatch-*", "release-*", "v*"];
|
|
3054
|
+
for (const pattern of patterns) {
|
|
3055
|
+
const tag = await gitCommand(
|
|
3056
|
+
["describe", "--tags", "--match", pattern, "--abbrev=0", "HEAD~1"],
|
|
3057
|
+
cwd
|
|
3058
|
+
);
|
|
3059
|
+
if (tag) return tag;
|
|
3060
|
+
}
|
|
3061
|
+
const anyTag = await gitCommand(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd);
|
|
3062
|
+
return anyTag;
|
|
3063
|
+
}
|
|
3064
|
+
var aiExplainCommand = new Command35("explain").description("Generate a changelog from git history using AI").option("-f, --from <ref>", "Start ref (tag, commit, or branch). Defaults to last tag.").option("-t, --to <ref>", "End ref", "HEAD").option("--format <type>", "Output format: markdown, plain, json", "markdown").option("--max-commits <n>", "Maximum commits to analyze", "50").option("-y, --yes", "Skip consent prompt (for CI/CD)").option("--json", "Output structured JSON").action(async (options) => {
|
|
3065
|
+
const cwd = process.cwd();
|
|
3066
|
+
const isGit = await gitCommand(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
3067
|
+
if (isGit !== "true") {
|
|
3068
|
+
logger.error("Not a git repository. Run this from your project root.");
|
|
3069
|
+
process.exit(1);
|
|
3070
|
+
}
|
|
3071
|
+
const consented = await requireConsent(
|
|
3072
|
+
["git-history"],
|
|
3073
|
+
{ yes: options.yes }
|
|
3074
|
+
);
|
|
3075
|
+
if (!consented) {
|
|
3076
|
+
process.exit(0);
|
|
3077
|
+
}
|
|
3078
|
+
const spinner = ora28("Gathering git history...").start();
|
|
3079
|
+
let fromRef = options.from;
|
|
3080
|
+
if (!fromRef) {
|
|
3081
|
+
fromRef = await getLastReleaseTag(cwd);
|
|
3082
|
+
if (!fromRef) {
|
|
3083
|
+
fromRef = `HEAD~${Math.min(parseInt(options.maxCommits, 10), 50)}`;
|
|
3084
|
+
spinner.text = `No release tags found. Using last ${options.maxCommits} commits...`;
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
const toRef = options.to;
|
|
3088
|
+
const maxCommits = Math.min(Math.max(parseInt(options.maxCommits, 10) || 50, 1), 100);
|
|
3089
|
+
const logOutput = await gitCommand(
|
|
3090
|
+
["log", `${fromRef}..${toRef}`, `--max-count=${maxCommits}`, "--pretty=format:%h|%s|%an|%ad", "--date=short"],
|
|
3091
|
+
cwd
|
|
3092
|
+
);
|
|
3093
|
+
if (!logOutput) {
|
|
3094
|
+
spinner.fail("No commits found in the specified range.");
|
|
3095
|
+
console.log("");
|
|
3096
|
+
console.log(chalk33.gray(` Range: ${fromRef}..${toRef}`));
|
|
3097
|
+
console.log("");
|
|
3098
|
+
process.exit(1);
|
|
3099
|
+
}
|
|
3100
|
+
const diffStat = await gitCommand(
|
|
3101
|
+
["diff", "--stat", `${fromRef}..${toRef}`],
|
|
3102
|
+
cwd
|
|
3103
|
+
);
|
|
3104
|
+
const changedFiles = await gitCommand(
|
|
3105
|
+
["diff", "--name-only", `${fromRef}..${toRef}`],
|
|
3106
|
+
cwd
|
|
3107
|
+
);
|
|
3108
|
+
spinner.succeed(`Found ${logOutput.split("\n").length} commits (${fromRef}..${toRef})`);
|
|
3109
|
+
const truncatedLog = logOutput.slice(0, MAX_CONTEXT_BYTES);
|
|
3110
|
+
const truncatedStat = (diffStat || "").slice(0, MAX_CONTEXT_BYTES / 2);
|
|
3111
|
+
const truncatedFiles = (changedFiles || "").slice(0, MAX_CONTEXT_BYTES / 2);
|
|
3112
|
+
const outputFormat = options.json ? "json" : options.format;
|
|
3113
|
+
const systemPrompt = "You are a senior developer writing release notes for a React Native app that uses SwiftPatch for OTA updates. Generate a clear, professional changelog from the provided git history.\n\nRules:\n- Group changes by category: Features, Bug Fixes, Improvements, Breaking Changes\n- Only include Breaking Changes if there are actual breaking API or behavior changes\n- Omit empty categories\n- Write for end-users and fellow developers, not bots\n- Be concise: one line per change\n- Highlight any changes that affect native code (these CANNOT be delivered via OTA)\n" + (outputFormat === "json" ? '- Return valid JSON with structure: { "summary": "...", "categories": [{ "name": "...", "changes": ["..."] }], "nativeChanges": boolean, "otaSafe": boolean }' : outputFormat === "markdown" ? "- Use markdown formatting with ## headers" : "- Use plain text formatting, no markdown");
|
|
3114
|
+
const userMessage = `Generate a changelog for these changes:
|
|
3115
|
+
|
|
3116
|
+
## Commits
|
|
3117
|
+
${truncatedLog}
|
|
3118
|
+
|
|
3119
|
+
## Changed Files
|
|
3120
|
+
${truncatedFiles}
|
|
3121
|
+
|
|
3122
|
+
## Diff Stats
|
|
3123
|
+
${truncatedStat}`;
|
|
3124
|
+
if (outputFormat === "json") {
|
|
3125
|
+
const aiSpinner = ora28("Generating changelog with AI...").start();
|
|
3126
|
+
try {
|
|
3127
|
+
const result = await claudeChat(
|
|
3128
|
+
[{ role: "user", content: userMessage }],
|
|
3129
|
+
{ system: systemPrompt, temperature: 0.3 }
|
|
3130
|
+
);
|
|
3131
|
+
aiSpinner.succeed("Changelog generated");
|
|
3132
|
+
console.log("");
|
|
3133
|
+
console.log(result);
|
|
3134
|
+
console.log("");
|
|
3135
|
+
} catch (error) {
|
|
3136
|
+
aiSpinner.fail(`Failed: ${error.message}`);
|
|
3137
|
+
process.exit(1);
|
|
3138
|
+
}
|
|
3139
|
+
} else {
|
|
3140
|
+
console.log("");
|
|
3141
|
+
process.stdout.write(chalk33.dim(""));
|
|
3142
|
+
try {
|
|
3143
|
+
await claudeStream(
|
|
3144
|
+
[{ role: "user", content: userMessage }],
|
|
3145
|
+
{ system: systemPrompt, temperature: 0.3 }
|
|
3146
|
+
);
|
|
3147
|
+
console.log("");
|
|
3148
|
+
console.log("");
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
console.log("");
|
|
3151
|
+
logger.error(`Failed: ${error.message}`);
|
|
3152
|
+
process.exit(1);
|
|
3153
|
+
}
|
|
3154
|
+
}
|
|
3155
|
+
});
|
|
3156
|
+
|
|
3157
|
+
// src/commands/ai-review.ts
|
|
3158
|
+
init_esm_shims();
|
|
3159
|
+
import { Command as Command36 } from "commander";
|
|
3160
|
+
import chalk34 from "chalk";
|
|
3161
|
+
import ora29 from "ora";
|
|
3162
|
+
import { execFile as execFile2 } from "child_process";
|
|
3163
|
+
import { promisify as promisify2 } from "util";
|
|
3164
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3165
|
+
var MAX_DIFF_BYTES = 15e3;
|
|
3166
|
+
var HIGH_RISK_PATTERNS = [
|
|
3167
|
+
"android/",
|
|
3168
|
+
"ios/",
|
|
3169
|
+
"Podfile",
|
|
3170
|
+
"build.gradle",
|
|
3171
|
+
"AndroidManifest.xml",
|
|
3172
|
+
"Info.plist",
|
|
3173
|
+
"settings.gradle",
|
|
3174
|
+
".pbxproj",
|
|
3175
|
+
"Podfile.lock"
|
|
3176
|
+
];
|
|
3177
|
+
var SENSITIVE_PATTERNS = [
|
|
3178
|
+
"auth",
|
|
3179
|
+
"login",
|
|
3180
|
+
"token",
|
|
3181
|
+
"payment",
|
|
3182
|
+
"checkout",
|
|
3183
|
+
"security",
|
|
3184
|
+
"crypto",
|
|
3185
|
+
"encrypt",
|
|
3186
|
+
"password",
|
|
3187
|
+
"secret",
|
|
3188
|
+
"api-key",
|
|
3189
|
+
"apiKey",
|
|
3190
|
+
"credential"
|
|
3191
|
+
];
|
|
3192
|
+
async function gitCommand2(args, cwd) {
|
|
3193
|
+
try {
|
|
3194
|
+
const { stdout } = await execFileAsync2("git", args, {
|
|
3195
|
+
cwd,
|
|
3196
|
+
maxBuffer: 1024 * 1024,
|
|
3197
|
+
timeout: 15e3
|
|
3198
|
+
});
|
|
3199
|
+
return stdout.trim();
|
|
3200
|
+
} catch {
|
|
3201
|
+
return null;
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
async function analyzeChangesLocally(fromRef, toRef, cwd) {
|
|
3205
|
+
const result = {
|
|
3206
|
+
nativeFileChanges: [],
|
|
3207
|
+
sensitiveFileChanges: [],
|
|
3208
|
+
totalFilesChanged: 0,
|
|
3209
|
+
linesAdded: 0,
|
|
3210
|
+
linesRemoved: 0,
|
|
3211
|
+
hasNativeChanges: false,
|
|
3212
|
+
hasSensitiveChanges: false,
|
|
3213
|
+
changedFiles: [],
|
|
3214
|
+
diffExcerpt: ""
|
|
3215
|
+
};
|
|
3216
|
+
const filesOutput = await gitCommand2(
|
|
3217
|
+
["diff", "--name-only", `${fromRef}..${toRef}`],
|
|
3218
|
+
cwd
|
|
3219
|
+
);
|
|
3220
|
+
if (!filesOutput) return result;
|
|
3221
|
+
const files = filesOutput.split("\n").filter(Boolean);
|
|
3222
|
+
result.changedFiles = files;
|
|
3223
|
+
result.totalFilesChanged = files.length;
|
|
3224
|
+
for (const file of files) {
|
|
3225
|
+
const lower = file.toLowerCase();
|
|
3226
|
+
if (HIGH_RISK_PATTERNS.some((p) => lower.includes(p.toLowerCase()))) {
|
|
3227
|
+
result.nativeFileChanges.push(file);
|
|
3228
|
+
result.hasNativeChanges = true;
|
|
3229
|
+
}
|
|
3230
|
+
if (SENSITIVE_PATTERNS.some((p) => lower.includes(p))) {
|
|
3231
|
+
result.sensitiveFileChanges.push(file);
|
|
3232
|
+
result.hasSensitiveChanges = true;
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
const statsOutput = await gitCommand2(
|
|
3236
|
+
["diff", "--shortstat", `${fromRef}..${toRef}`],
|
|
3237
|
+
cwd
|
|
3238
|
+
);
|
|
3239
|
+
if (statsOutput) {
|
|
3240
|
+
const addMatch = statsOutput.match(/(\d+) insertion/);
|
|
3241
|
+
const delMatch = statsOutput.match(/(\d+) deletion/);
|
|
3242
|
+
result.linesAdded = addMatch ? parseInt(addMatch[1], 10) : 0;
|
|
3243
|
+
result.linesRemoved = delMatch ? parseInt(delMatch[1], 10) : 0;
|
|
3244
|
+
}
|
|
3245
|
+
const jsDiff = await gitCommand2(
|
|
3246
|
+
[
|
|
3247
|
+
"diff",
|
|
3248
|
+
`${fromRef}..${toRef}`,
|
|
3249
|
+
"--",
|
|
3250
|
+
"*.ts",
|
|
3251
|
+
"*.tsx",
|
|
3252
|
+
"*.js",
|
|
3253
|
+
"*.jsx",
|
|
3254
|
+
":!node_modules",
|
|
3255
|
+
":!*.lock",
|
|
3256
|
+
":!dist/",
|
|
3257
|
+
":!build/"
|
|
3258
|
+
],
|
|
3259
|
+
cwd
|
|
3260
|
+
);
|
|
3261
|
+
if (jsDiff) {
|
|
3262
|
+
result.diffExcerpt = jsDiff.slice(0, MAX_DIFF_BYTES);
|
|
3263
|
+
}
|
|
3264
|
+
return result;
|
|
3265
|
+
}
|
|
3266
|
+
var aiReviewCommand = new Command36("review").description("AI-powered safety review of changes before OTA deployment").option("-f, --from <ref>", "Start ref (tag, commit, or branch). Defaults to last tag.").option("-t, --to <ref>", "End ref", "HEAD").option("-y, --yes", "Skip consent prompt (for CI/CD)").option("--json", "Output as JSON").action(async (options) => {
|
|
3267
|
+
const cwd = process.cwd();
|
|
3268
|
+
const isGit = await gitCommand2(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
3269
|
+
if (isGit !== "true") {
|
|
3270
|
+
logger.error("Not a git repository. Run this from your project root.");
|
|
3271
|
+
process.exit(1);
|
|
3272
|
+
}
|
|
3273
|
+
const consented = await requireConsent(
|
|
3274
|
+
["git-history", "git-diff"],
|
|
3275
|
+
{ yes: options.yes }
|
|
3276
|
+
);
|
|
3277
|
+
if (!consented) {
|
|
3278
|
+
process.exit(0);
|
|
3279
|
+
}
|
|
3280
|
+
const spinner = ora29("Analyzing changes...").start();
|
|
3281
|
+
let fromRef = options.from;
|
|
3282
|
+
if (!fromRef) {
|
|
3283
|
+
fromRef = await gitCommand2(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd);
|
|
3284
|
+
if (!fromRef) {
|
|
3285
|
+
fromRef = "HEAD~10";
|
|
3286
|
+
spinner.text = "No tags found. Comparing against last 10 commits...";
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
const toRef = options.to;
|
|
3290
|
+
let analysis;
|
|
3291
|
+
try {
|
|
3292
|
+
analysis = await analyzeChangesLocally(fromRef, toRef, cwd);
|
|
3293
|
+
} catch (error) {
|
|
3294
|
+
spinner.fail(`Analysis failed: ${error.message}`);
|
|
3295
|
+
process.exit(1);
|
|
3296
|
+
}
|
|
3297
|
+
if (analysis.totalFilesChanged === 0) {
|
|
3298
|
+
spinner.succeed("No changes found");
|
|
3299
|
+
console.log("");
|
|
3300
|
+
console.log(chalk34.gray(` Range: ${fromRef}..${toRef}`));
|
|
3301
|
+
console.log(chalk34.green(" Nothing to review \u2014 working tree is clean."));
|
|
3302
|
+
console.log("");
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
spinner.succeed(`Found ${analysis.totalFilesChanged} changed files (+${analysis.linesAdded} -${analysis.linesRemoved})`);
|
|
3306
|
+
if (analysis.hasNativeChanges) {
|
|
3307
|
+
console.log("");
|
|
3308
|
+
logger.warning(chalk34.bold("Native file changes detected \u2014 these CANNOT be delivered via OTA:"));
|
|
3309
|
+
for (const f of analysis.nativeFileChanges) {
|
|
3310
|
+
console.log(` ${chalk34.red("!")} ${f}`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (analysis.hasSensitiveChanges) {
|
|
3314
|
+
console.log("");
|
|
3315
|
+
logger.warning(chalk34.bold("Sensitive file changes detected \u2014 review carefully:"));
|
|
3316
|
+
for (const f of analysis.sensitiveFileChanges) {
|
|
3317
|
+
console.log(` ${chalk34.yellow("!")} ${f}`);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
const aiSpinner = ora29("Running AI safety review...").start();
|
|
3321
|
+
try {
|
|
3322
|
+
const systemPrompt = "You are SwiftPatch Review, an expert in React Native OTA update safety. Analyze the code diff and provide a safety assessment for shipping this as an OTA update.\n\nYour response MUST include:\n1. **OTA Safety Score** (1-10, where 10 is perfectly safe)\n2. **Verdict**: SAFE, CAUTION, or UNSAFE\n3. **Risk Summary**: 1-2 sentences about the overall risk\n4. **Issues Found**: List specific concerns (if any)\n5. **Recommendation**: Should this be shipped as OTA or requires app store release?\n\nRisk factors to check:\n- Native code changes (cannot be OTA updated)\n- Authentication/security logic changes\n- API endpoint changes (URL, headers, auth)\n- Error handling removal or weakening\n- Removal of safety checks or validation\n- Large structural refactors that may introduce regressions\n- Third-party SDK version changes\n\nBe concise and specific. Developers are your audience.";
|
|
3323
|
+
const contextData = JSON.stringify({
|
|
3324
|
+
range: `${fromRef}..${toRef}`,
|
|
3325
|
+
totalFilesChanged: analysis.totalFilesChanged,
|
|
3326
|
+
linesAdded: analysis.linesAdded,
|
|
3327
|
+
linesRemoved: analysis.linesRemoved,
|
|
3328
|
+
nativeFileChanges: analysis.nativeFileChanges,
|
|
3329
|
+
sensitiveFileChanges: analysis.sensitiveFileChanges,
|
|
3330
|
+
changedFiles: analysis.changedFiles.slice(0, 50)
|
|
3331
|
+
}, null, 2);
|
|
3332
|
+
const userMessage = `Review this OTA update for safety:
|
|
3333
|
+
|
|
3334
|
+
## Change Summary
|
|
3335
|
+
${contextData}
|
|
3336
|
+
|
|
3337
|
+
## Code Diff (JS/TS only)
|
|
3338
|
+
\`\`\`diff
|
|
3339
|
+
${analysis.diffExcerpt}
|
|
3340
|
+
\`\`\``;
|
|
3341
|
+
const result = await claudeChat(
|
|
3342
|
+
[{ role: "user", content: userMessage }],
|
|
3343
|
+
{ system: systemPrompt, temperature: 0.2 }
|
|
3344
|
+
);
|
|
3345
|
+
aiSpinner.succeed("Safety review complete");
|
|
3346
|
+
if (options.json) {
|
|
3347
|
+
console.log(JSON.stringify({
|
|
3348
|
+
range: `${fromRef}..${toRef}`,
|
|
3349
|
+
localAnalysis: {
|
|
3350
|
+
totalFilesChanged: analysis.totalFilesChanged,
|
|
3351
|
+
linesAdded: analysis.linesAdded,
|
|
3352
|
+
linesRemoved: analysis.linesRemoved,
|
|
3353
|
+
hasNativeChanges: analysis.hasNativeChanges,
|
|
3354
|
+
hasSensitiveChanges: analysis.hasSensitiveChanges,
|
|
3355
|
+
nativeFileChanges: analysis.nativeFileChanges,
|
|
3356
|
+
sensitiveFileChanges: analysis.sensitiveFileChanges
|
|
3357
|
+
},
|
|
3358
|
+
aiReview: result
|
|
3359
|
+
}, null, 2));
|
|
3360
|
+
return;
|
|
3361
|
+
}
|
|
3362
|
+
console.log("");
|
|
3363
|
+
console.log(chalk34.bold(" AI Safety Review"));
|
|
3364
|
+
console.log("");
|
|
3365
|
+
for (const line of result.split("\n")) {
|
|
3366
|
+
console.log(` ${line}`);
|
|
3367
|
+
}
|
|
3368
|
+
console.log("");
|
|
3369
|
+
} catch (error) {
|
|
3370
|
+
aiSpinner.fail(`AI review failed: ${error.message}`);
|
|
3371
|
+
console.log("");
|
|
3372
|
+
console.log(chalk34.gray(" Local analysis above is still valid."));
|
|
3373
|
+
console.log("");
|
|
3374
|
+
process.exit(1);
|
|
3375
|
+
}
|
|
3376
|
+
});
|
|
3377
|
+
|
|
3378
|
+
// src/commands/ai.ts
|
|
3379
|
+
var aiCommand = new Command37("ai").description("AI-powered release intelligence");
|
|
3380
|
+
aiCommand.addCommand(aiDoctorCommand);
|
|
3381
|
+
aiCommand.addCommand(aiExplainCommand);
|
|
3382
|
+
aiCommand.addCommand(aiReviewCommand);
|
|
3383
|
+
aiCommand.command("ask").description("Ask AI about your releases, crashes, or deployment strategy").argument("<question>", "Your question").option("-o, --org <org-id>", "Organization ID").option("-a, --app <app-id>", "App ID for context").action(async (question, options) => {
|
|
3384
|
+
await requireAuth();
|
|
3385
|
+
const orgId = await resolveOrgId(options.org);
|
|
3386
|
+
process.stdout.write(chalk35.dim("AI: "));
|
|
3387
|
+
const controller = new AbortController();
|
|
3388
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
3389
|
+
try {
|
|
3390
|
+
const response = await fetch(
|
|
3391
|
+
`${api.getBaseUrl()}/orgs/${orgId}/ai/chat`,
|
|
3392
|
+
{
|
|
3393
|
+
method: "POST",
|
|
3394
|
+
headers: {
|
|
3395
|
+
"Content-Type": "application/json",
|
|
3396
|
+
...api.getAuthHeaders()
|
|
3397
|
+
},
|
|
3398
|
+
body: JSON.stringify({
|
|
3399
|
+
message: question,
|
|
3400
|
+
context: {
|
|
3401
|
+
page: "cli",
|
|
3402
|
+
...options.app ? { entityType: "app", entityId: options.app } : {}
|
|
3403
|
+
}
|
|
3404
|
+
}),
|
|
3405
|
+
signal: controller.signal
|
|
3406
|
+
}
|
|
3407
|
+
);
|
|
3408
|
+
if (!response.ok) {
|
|
3409
|
+
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
|
|
3410
|
+
}
|
|
3411
|
+
if (!response.body) {
|
|
3412
|
+
throw new Error("No response body");
|
|
3413
|
+
}
|
|
3414
|
+
const reader = response.body.getReader();
|
|
3415
|
+
const decoder = new TextDecoder();
|
|
3416
|
+
let buffer = "";
|
|
3417
|
+
while (true) {
|
|
3418
|
+
const { done, value } = await reader.read();
|
|
3419
|
+
if (done) break;
|
|
3420
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3421
|
+
const lines = buffer.split("\n");
|
|
3422
|
+
buffer = lines.pop() || "";
|
|
3423
|
+
for (const line of lines) {
|
|
3424
|
+
if (line.startsWith("data: ")) {
|
|
3425
|
+
const data = line.slice(6);
|
|
3426
|
+
if (data === "[DONE]") break;
|
|
3427
|
+
process.stdout.write(data);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
console.log("");
|
|
3432
|
+
console.log("");
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
console.log("");
|
|
3435
|
+
if (error.name === "AbortError") {
|
|
3436
|
+
console.error(chalk35.red("AI request timed out after 60 seconds."));
|
|
3437
|
+
} else {
|
|
3438
|
+
console.error(chalk35.red(`Failed: ${error.message}`));
|
|
3439
|
+
}
|
|
3440
|
+
process.exit(1);
|
|
3441
|
+
} finally {
|
|
3442
|
+
clearTimeout(timeout);
|
|
3443
|
+
}
|
|
3444
|
+
});
|
|
3445
|
+
aiCommand.command("risk-score").description("Get AI risk assessment for a release").requiredOption("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").requiredOption("-r, --release <release-id>", "Release ID").option("--json", "Output as JSON").action(async (options) => {
|
|
3446
|
+
await requireAuth();
|
|
3447
|
+
const orgId = options.org;
|
|
3448
|
+
const spinner = ora30("Assessing release risk...").start();
|
|
3449
|
+
try {
|
|
3450
|
+
const assessment = await api.aiRiskAssessment(orgId, options.app, options.release);
|
|
3451
|
+
spinner.stop();
|
|
3452
|
+
if (options.json) {
|
|
3453
|
+
console.log(JSON.stringify(assessment, null, 2));
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
const levelColors = {
|
|
3457
|
+
LOW: chalk35.green,
|
|
3458
|
+
MEDIUM: chalk35.yellow,
|
|
3459
|
+
HIGH: chalk35.hex("#f97316"),
|
|
3460
|
+
CRITICAL: chalk35.red
|
|
3461
|
+
};
|
|
3462
|
+
const colorFn = levelColors[assessment.level] || chalk35.white;
|
|
3463
|
+
console.log("");
|
|
3464
|
+
console.log(chalk35.bold(" Risk Assessment"));
|
|
3465
|
+
console.log("");
|
|
3466
|
+
console.log(` Score: ${colorFn(String(assessment.score) + "/100")}`);
|
|
3467
|
+
console.log(` Level: ${colorFn(assessment.level)}`);
|
|
3468
|
+
console.log("");
|
|
3469
|
+
console.log(chalk35.bold(" Risk Factors:"));
|
|
3470
|
+
for (const factor of assessment.factors) {
|
|
3471
|
+
const bar = "\u2588".repeat(factor.impact) + "\u2591".repeat(10 - factor.impact);
|
|
3472
|
+
console.log(` ${bar} ${factor.factor}`);
|
|
3473
|
+
console.log(` ${chalk35.gray(factor.description)}`);
|
|
3474
|
+
}
|
|
3475
|
+
console.log("");
|
|
3476
|
+
console.log(chalk35.bold(" Recommendation:"));
|
|
3477
|
+
console.log(` ${assessment.recommendation}`);
|
|
3478
|
+
console.log("");
|
|
3479
|
+
} catch (error) {
|
|
3480
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
3481
|
+
process.exit(1);
|
|
3482
|
+
}
|
|
3483
|
+
});
|
|
3484
|
+
aiCommand.command("crashes").description("List AI-grouped crash reports for an app").requiredOption("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").option("-s, --status <status>", "Filter by status (OPEN, RESOLVED, IGNORED)", "OPEN").option("-l, --limit <limit>", "Maximum number of results", "10").option("--json", "Output as JSON").action(async (options) => {
|
|
3485
|
+
await requireAuth();
|
|
3486
|
+
const spinner = ora30("Fetching crash groups...").start();
|
|
3487
|
+
try {
|
|
3488
|
+
const groups = await api.aiCrashGroups(options.org, options.app, {
|
|
3489
|
+
status: options.status,
|
|
3490
|
+
limit: parseInt(options.limit, 10)
|
|
3491
|
+
});
|
|
3492
|
+
spinner.stop();
|
|
3493
|
+
if (options.json) {
|
|
3494
|
+
console.log(JSON.stringify(groups, null, 2));
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
if (groups.length === 0) {
|
|
3498
|
+
console.log(chalk35.green("\n No crash groups found. Your app is running smoothly!\n"));
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
console.log("");
|
|
3502
|
+
console.log(chalk35.bold(` Crash Groups (${groups.length})`));
|
|
3503
|
+
console.log("");
|
|
3504
|
+
const severityColors = {
|
|
3505
|
+
CRITICAL: chalk35.red,
|
|
3506
|
+
HIGH: chalk35.hex("#f97316"),
|
|
3507
|
+
MEDIUM: chalk35.yellow,
|
|
3508
|
+
LOW: chalk35.blue
|
|
3509
|
+
};
|
|
3510
|
+
for (const group of groups) {
|
|
3511
|
+
const colorFn = severityColors[group.severity] || chalk35.white;
|
|
3512
|
+
console.log(` ${colorFn(`[${group.severity}]`)} ${group.title}`);
|
|
3513
|
+
console.log(` ${chalk35.gray(`${group.occurrenceCount} occurrences \xB7 ${group.affectedDevices} devices \xB7 ${group.category}`)}`);
|
|
3514
|
+
if (group.aiAnalysis) {
|
|
3515
|
+
console.log(` ${chalk35.cyan("AI:")} ${group.aiAnalysis.rootCause.slice(0, 100)}${group.aiAnalysis.rootCause.length > 100 ? "..." : ""}`);
|
|
3516
|
+
}
|
|
3517
|
+
console.log("");
|
|
3518
|
+
}
|
|
3519
|
+
} catch (error) {
|
|
3520
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
3521
|
+
process.exit(1);
|
|
3522
|
+
}
|
|
3523
|
+
});
|
|
3524
|
+
aiCommand.command("insights").description("View AI-generated insights for an app").requiredOption("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").option("-s, --status <status>", "Filter by status (ACTIVE, ACKNOWLEDGED, DISMISSED)", "ACTIVE").option("--json", "Output as JSON").action(async (options) => {
|
|
3525
|
+
await requireAuth();
|
|
3526
|
+
const spinner = ora30("Fetching AI insights...").start();
|
|
3527
|
+
try {
|
|
3528
|
+
const insights = await api.aiInsights(options.org, options.app, {
|
|
3529
|
+
status: options.status
|
|
3530
|
+
});
|
|
3531
|
+
spinner.stop();
|
|
3532
|
+
if (options.json) {
|
|
3533
|
+
console.log(JSON.stringify(insights, null, 2));
|
|
3534
|
+
return;
|
|
3535
|
+
}
|
|
3536
|
+
if (insights.length === 0) {
|
|
3537
|
+
console.log(chalk35.green("\n No active insights. All systems nominal!\n"));
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
3540
|
+
console.log("");
|
|
3541
|
+
console.log(chalk35.bold(` AI Insights (${insights.length})`));
|
|
3542
|
+
console.log("");
|
|
3543
|
+
const priorityColors = {
|
|
3544
|
+
CRITICAL: chalk35.red,
|
|
3545
|
+
HIGH: chalk35.hex("#f97316"),
|
|
3546
|
+
MEDIUM: chalk35.yellow,
|
|
3547
|
+
LOW: chalk35.blue,
|
|
3548
|
+
INFO: chalk35.gray
|
|
3549
|
+
};
|
|
3550
|
+
for (const insight of insights) {
|
|
3551
|
+
const colorFn = priorityColors[insight.priority] || chalk35.white;
|
|
3552
|
+
console.log(` ${colorFn(`[${insight.priority}]`)} ${insight.title}`);
|
|
3553
|
+
console.log(` ${chalk35.gray(insight.summary)}`);
|
|
3554
|
+
if (insight.actionItems.length > 0) {
|
|
3555
|
+
for (const item of insight.actionItems) {
|
|
3556
|
+
console.log(` ${chalk35.cyan("\u2192")} ${item}`);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
console.log("");
|
|
3560
|
+
}
|
|
3561
|
+
} catch (error) {
|
|
3562
|
+
spinner.fail(`Failed: ${error.message}`);
|
|
3563
|
+
process.exit(1);
|
|
3564
|
+
}
|
|
3565
|
+
});
|
|
3566
|
+
|
|
3567
|
+
// src/commands/config.ts
|
|
3568
|
+
init_esm_shims();
|
|
3569
|
+
init_config();
|
|
3570
|
+
init_auth();
|
|
3571
|
+
import { Command as Command38 } from "commander";
|
|
3572
|
+
import chalk36 from "chalk";
|
|
3573
|
+
var VALID_KEYS = [
|
|
3574
|
+
"apiUrl",
|
|
3575
|
+
"defaultOrg",
|
|
3576
|
+
"defaultApp",
|
|
3577
|
+
"defaultPlatform",
|
|
3578
|
+
"defaultChannel",
|
|
3579
|
+
"aiConsentScopes"
|
|
3580
|
+
];
|
|
3581
|
+
var SECRET_KEYS = ["claudeApiKey"];
|
|
3582
|
+
var ALL_KEYS = [...VALID_KEYS, ...SECRET_KEYS];
|
|
3583
|
+
function isSecretKey(key) {
|
|
3584
|
+
return SECRET_KEYS.includes(key);
|
|
3585
|
+
}
|
|
3586
|
+
function maskSecret(value) {
|
|
3587
|
+
if (value.length <= 8) return "****";
|
|
3588
|
+
return value.slice(0, 7) + "..." + "*".repeat(8);
|
|
3589
|
+
}
|
|
3590
|
+
var configCommands = new Command38("config").description("CLI configuration");
|
|
3591
|
+
configCommands.command("set").description("Set a config value").argument("<key>", "Config key").argument("<value>", "Config value").action((key, value) => {
|
|
3592
|
+
if (!ALL_KEYS.includes(key)) {
|
|
3593
|
+
logger.error(`Invalid config key: ${key}`);
|
|
3594
|
+
console.log("");
|
|
3595
|
+
console.log("Valid keys: " + ALL_KEYS.map((k) => chalk36.cyan(k)).join(", "));
|
|
3596
|
+
console.log("");
|
|
3597
|
+
process.exit(1);
|
|
3598
|
+
}
|
|
3599
|
+
if (isSecretKey(key)) {
|
|
3600
|
+
if (key === "claudeApiKey") {
|
|
3601
|
+
if (!value.startsWith("sk-ant-") || value.length <= 20) {
|
|
3602
|
+
logger.error('Invalid Claude API key format. Keys start with "sk-ant-"');
|
|
3603
|
+
console.log("");
|
|
3604
|
+
console.log(" Get a key at: " + chalk36.cyan("https://console.anthropic.com/settings/keys"));
|
|
3605
|
+
console.log("");
|
|
3606
|
+
process.exit(1);
|
|
3607
|
+
}
|
|
3608
|
+
auth.setClaudeApiKey(value);
|
|
3609
|
+
logger.success(`Set ${chalk36.cyan(key)} = ${chalk36.gray(maskSecret(value))} (encrypted)`);
|
|
3610
|
+
}
|
|
3611
|
+
return;
|
|
3612
|
+
}
|
|
3613
|
+
config.set(key, value);
|
|
3614
|
+
logger.success(`Set ${chalk36.cyan(key)} = ${chalk36.gray(value)}`);
|
|
3615
|
+
});
|
|
3616
|
+
configCommands.command("get").description("Get a config value").argument("<key>", "Config key").action((key) => {
|
|
3617
|
+
if (isSecretKey(key)) {
|
|
3618
|
+
if (key === "claudeApiKey") {
|
|
3619
|
+
const value2 = auth.getClaudeApiKey();
|
|
3620
|
+
if (!value2) {
|
|
3621
|
+
logger.info(`${key} is not set`);
|
|
3622
|
+
} else {
|
|
3623
|
+
console.log(`${chalk36.cyan(key)} = ${maskSecret(value2)} (encrypted)`);
|
|
3624
|
+
}
|
|
3625
|
+
}
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
const value = config.get(key);
|
|
3629
|
+
if (value === void 0) {
|
|
3630
|
+
logger.info(`${key} is not set`);
|
|
3631
|
+
} else {
|
|
3632
|
+
console.log(`${chalk36.cyan(key)} = ${value}`);
|
|
3633
|
+
}
|
|
3634
|
+
});
|
|
3635
|
+
configCommands.command("delete").description("Delete a config value").argument("<key>", "Config key").action((key) => {
|
|
3636
|
+
if (!ALL_KEYS.includes(key)) {
|
|
3637
|
+
logger.error(`Invalid config key: ${key}`);
|
|
3638
|
+
process.exit(1);
|
|
3639
|
+
}
|
|
3640
|
+
if (isSecretKey(key)) {
|
|
3641
|
+
if (key === "claudeApiKey") {
|
|
3642
|
+
auth.deleteClaudeApiKey();
|
|
3643
|
+
logger.success(`Deleted ${chalk36.cyan(key)}`);
|
|
3644
|
+
}
|
|
3645
|
+
return;
|
|
3646
|
+
}
|
|
3647
|
+
config.delete(key);
|
|
3648
|
+
logger.success(`Deleted ${chalk36.cyan(key)}`);
|
|
3649
|
+
});
|
|
3650
|
+
configCommands.command("list").description("List all config values").action(() => {
|
|
3651
|
+
const all = config.getAll();
|
|
3652
|
+
const plainKeys = Object.keys(all);
|
|
3653
|
+
const hasClaudeKey = !!auth.getClaudeApiKey();
|
|
3654
|
+
if (plainKeys.length === 0 && !hasClaudeKey) {
|
|
3655
|
+
console.log("");
|
|
3656
|
+
console.log(chalk36.gray(" No config values set."));
|
|
3657
|
+
console.log("");
|
|
3658
|
+
console.log(" Set a value with: " + chalk36.cyan("swiftpatch config set <key> <value>"));
|
|
3659
|
+
console.log("");
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3662
|
+
console.log("");
|
|
3663
|
+
console.log(chalk36.bold(" CLI Configuration"));
|
|
3664
|
+
console.log("");
|
|
3665
|
+
plainKeys.forEach((key) => {
|
|
3666
|
+
console.log(` ${chalk36.cyan(key)} = ${all[key]}`);
|
|
3667
|
+
});
|
|
3668
|
+
if (hasClaudeKey) {
|
|
3669
|
+
const key = auth.getClaudeApiKey();
|
|
3670
|
+
console.log(` ${chalk36.cyan("claudeApiKey")} = ${maskSecret(key)} ${chalk36.gray("(encrypted)")}`);
|
|
3671
|
+
}
|
|
3672
|
+
console.log("");
|
|
3673
|
+
console.log(chalk36.gray(` Config path: ${config.path}`));
|
|
3674
|
+
console.log("");
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3677
|
+
// src/commands/publish-bundle.ts
|
|
3678
|
+
init_esm_shims();
|
|
3679
|
+
init_api();
|
|
3680
|
+
import { Command as Command39 } from "commander";
|
|
3681
|
+
import chalk37 from "chalk";
|
|
3682
|
+
import ora31 from "ora";
|
|
3683
|
+
import path9 from "path";
|
|
3684
|
+
import fs11 from "fs-extra";
|
|
3685
|
+
import os3 from "os";
|
|
3686
|
+
|
|
3687
|
+
// src/lib/archive.ts
|
|
3688
|
+
init_esm_shims();
|
|
3689
|
+
import archiver from "archiver";
|
|
3690
|
+
import fs8 from "fs";
|
|
3691
|
+
import path6 from "path";
|
|
3692
|
+
function createZip(inputPath, outputPath) {
|
|
3693
|
+
return new Promise((resolve, reject) => {
|
|
3694
|
+
const zipPath = path6.join(outputPath, "build.zip");
|
|
3695
|
+
const output = fs8.createWriteStream(zipPath);
|
|
3696
|
+
const archive = archiver("zip", { zlib: { level: 9 } });
|
|
3697
|
+
output.on("close", () => resolve());
|
|
3698
|
+
archive.on("error", (err) => reject(err));
|
|
3699
|
+
archive.pipe(output);
|
|
3700
|
+
archive.directory(inputPath, "build");
|
|
3701
|
+
archive.finalize();
|
|
3702
|
+
});
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
// src/lib/artifacts.ts
|
|
3706
|
+
init_esm_shims();
|
|
3707
|
+
import fs9 from "fs-extra";
|
|
3708
|
+
import path7 from "path";
|
|
3709
|
+
async function keepArtifacts(contentRootPath, platform, variant) {
|
|
3710
|
+
const artifactsDir = path7.join(process.cwd(), "swiftpatch-artifacts", platform, variant);
|
|
3711
|
+
await fs9.ensureDir(artifactsDir);
|
|
3712
|
+
const bundlesDir = path7.join(contentRootPath, "bundles");
|
|
3713
|
+
const sourcemapsDir = path7.join(contentRootPath, "sourcemaps");
|
|
3714
|
+
if (await fs9.pathExists(bundlesDir)) {
|
|
3715
|
+
await copyDirRecursive(bundlesDir, path7.join(artifactsDir, "bundles"));
|
|
3716
|
+
}
|
|
3717
|
+
if (await fs9.pathExists(sourcemapsDir)) {
|
|
3718
|
+
await copyDirRecursive(sourcemapsDir, path7.join(artifactsDir, "sourcemaps"));
|
|
3719
|
+
}
|
|
3720
|
+
}
|
|
3721
|
+
async function copyDirRecursive(srcDir, destDir) {
|
|
3722
|
+
await fs9.ensureDir(destDir);
|
|
3723
|
+
const entries = await fs9.readdir(srcDir);
|
|
3724
|
+
for (const entry of entries) {
|
|
3725
|
+
const srcPath = path7.join(srcDir, entry);
|
|
3726
|
+
const destPath = path7.join(destDir, entry);
|
|
3727
|
+
const stat = await fs9.stat(srcPath);
|
|
3728
|
+
if (stat.isDirectory()) {
|
|
3729
|
+
await copyDirRecursive(srcPath, destPath);
|
|
3730
|
+
} else {
|
|
3731
|
+
await fs9.copyFile(srcPath, destPath);
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
|
|
3736
|
+
// src/lib/react-native-utils.ts
|
|
3737
|
+
init_esm_shims();
|
|
3738
|
+
import * as path8 from "path";
|
|
3739
|
+
import * as fs10 from "fs";
|
|
3740
|
+
import * as childProcess from "child_process";
|
|
3741
|
+
import { coerce, compare } from "semver";
|
|
3742
|
+
function findUpwardReactNativePackageJson(startDir = process.cwd()) {
|
|
3743
|
+
let current = startDir;
|
|
3744
|
+
while (current !== path8.parse(current).root) {
|
|
3745
|
+
const candidate = path8.join(current, "node_modules", "react-native", "package.json");
|
|
3746
|
+
if (fs10.existsSync(candidate)) return candidate;
|
|
3747
|
+
current = path8.dirname(current);
|
|
3748
|
+
}
|
|
3749
|
+
return null;
|
|
3750
|
+
}
|
|
3751
|
+
function getReactNativeVersion() {
|
|
3752
|
+
const rnPackageJsonPath = findUpwardReactNativePackageJson();
|
|
3753
|
+
if (!rnPackageJsonPath) return null;
|
|
3754
|
+
const rnPackageJson = JSON.parse(fs10.readFileSync(rnPackageJsonPath, "utf-8"));
|
|
3755
|
+
return rnPackageJson.version;
|
|
3756
|
+
}
|
|
3757
|
+
function directoryExistsSync(dirname3) {
|
|
3758
|
+
try {
|
|
3759
|
+
return fs10.statSync(dirname3).isDirectory();
|
|
3760
|
+
} catch (err) {
|
|
3761
|
+
if (err.code !== "ENOENT") throw err;
|
|
3762
|
+
}
|
|
3763
|
+
return false;
|
|
3764
|
+
}
|
|
3765
|
+
function getReactNativePackagePath() {
|
|
3766
|
+
const rnPackageJsonPath = findUpwardReactNativePackageJson();
|
|
3767
|
+
if (rnPackageJsonPath) return path8.dirname(rnPackageJsonPath);
|
|
3768
|
+
const result = childProcess.spawnSync("node", [
|
|
3769
|
+
"--print",
|
|
3770
|
+
"require.resolve('react-native/package.json')"
|
|
3771
|
+
]);
|
|
3772
|
+
const packagePath = path8.dirname(result.stdout.toString().trim());
|
|
3773
|
+
if (result.status === 0 && directoryExistsSync(packagePath)) return packagePath;
|
|
3774
|
+
return path8.join("node_modules", "react-native");
|
|
3775
|
+
}
|
|
3776
|
+
function resolvePackageDirFromCwd(packageName) {
|
|
3777
|
+
const result = childProcess.spawnSync("node", [
|
|
3778
|
+
"--print",
|
|
3779
|
+
`require.resolve('${packageName}/package.json')`
|
|
3780
|
+
]);
|
|
3781
|
+
if (result.status !== 0) return null;
|
|
3782
|
+
const resolved = result.stdout.toString().trim();
|
|
3783
|
+
if (!resolved) return null;
|
|
3784
|
+
const packageDir = path8.dirname(resolved);
|
|
3785
|
+
return directoryExistsSync(packageDir) ? packageDir : null;
|
|
3786
|
+
}
|
|
3787
|
+
function isValidPlatform(platform) {
|
|
3788
|
+
return platform?.toLowerCase() === "android" || platform?.toLowerCase() === "ios";
|
|
3789
|
+
}
|
|
3790
|
+
function fileExists(filePath) {
|
|
3791
|
+
try {
|
|
3792
|
+
return fs10.statSync(filePath).isFile();
|
|
3793
|
+
} catch {
|
|
3794
|
+
return false;
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
function getCliPath() {
|
|
3798
|
+
return path8.join(getReactNativePackagePath(), "cli.js");
|
|
3799
|
+
}
|
|
3800
|
+
async function runReactNativeBundleCommand(bundleName, entryFile, outputFolder, platform, sourcemap, devMode) {
|
|
3801
|
+
fs10.mkdirSync(path8.join(outputFolder, "bundles"), { recursive: true });
|
|
3802
|
+
if (sourcemap) {
|
|
3803
|
+
fs10.mkdirSync(path8.join(outputFolder, "sourcemaps"), { recursive: true });
|
|
3804
|
+
}
|
|
3805
|
+
const reactNativeBundleArgs = [
|
|
3806
|
+
getCliPath(),
|
|
3807
|
+
"bundle",
|
|
3808
|
+
"--dev",
|
|
3809
|
+
devMode,
|
|
3810
|
+
"--assets-dest",
|
|
3811
|
+
path8.join(outputFolder, "bundles"),
|
|
3812
|
+
"--bundle-output",
|
|
3813
|
+
path8.join(outputFolder, "bundles", bundleName),
|
|
3814
|
+
"--entry-file",
|
|
3815
|
+
entryFile,
|
|
3816
|
+
"--platform",
|
|
3817
|
+
platform,
|
|
3818
|
+
...sourcemap ? ["--sourcemap-output", path8.join(outputFolder, "sourcemaps", bundleName + ".map")] : []
|
|
3819
|
+
];
|
|
3820
|
+
logger.info('Running "react-native bundle" command');
|
|
3821
|
+
const reactNativeBundleProcess = childProcess.spawn("node", reactNativeBundleArgs);
|
|
3822
|
+
return new Promise((resolve, reject) => {
|
|
3823
|
+
reactNativeBundleProcess.stdout.on("data", (data) => {
|
|
3824
|
+
console.log(data.toString().trim());
|
|
3825
|
+
});
|
|
3826
|
+
reactNativeBundleProcess.stderr.on("data", (data) => {
|
|
3827
|
+
logger.error(data.toString().trim());
|
|
3828
|
+
});
|
|
3829
|
+
reactNativeBundleProcess.on("close", (exitCode, signal) => {
|
|
3830
|
+
if (exitCode !== 0) {
|
|
3831
|
+
reject(
|
|
3832
|
+
new Error(`"react-native bundle" command failed (exitCode=${exitCode}, signal=${signal}).`)
|
|
3833
|
+
);
|
|
3834
|
+
}
|
|
3835
|
+
resolve();
|
|
3836
|
+
});
|
|
3837
|
+
});
|
|
3838
|
+
}
|
|
3839
|
+
async function runHermesEmitBinaryCommand(bundleName, outputFolder, hermesLogs = false, hermescPath, sourcemap = false) {
|
|
3840
|
+
const hermesArgs = [
|
|
3841
|
+
...sourcemap ? ["--output-source-map"] : [],
|
|
3842
|
+
"--emit-binary",
|
|
3843
|
+
"--out",
|
|
3844
|
+
path8.join(outputFolder, bundleName + ".hbc"),
|
|
3845
|
+
path8.join(outputFolder, "bundles", bundleName)
|
|
3846
|
+
];
|
|
3847
|
+
logger.info("Converting JS bundle to Hermes bytecode");
|
|
3848
|
+
const hermesCommand = await getHermesCommand();
|
|
3849
|
+
logger.info(`Hermesc path: ${hermescPath || hermesCommand}`);
|
|
3850
|
+
const hermesProcess = childProcess.spawn(hermescPath || hermesCommand, hermesArgs);
|
|
3851
|
+
let logFile = null;
|
|
3852
|
+
let isWarned = false;
|
|
3853
|
+
if (hermesLogs) {
|
|
3854
|
+
logFile = fs10.createWriteStream("output.log", { flags: "a" });
|
|
3855
|
+
}
|
|
3856
|
+
return new Promise((resolve, reject) => {
|
|
3857
|
+
hermesProcess.stdout.on("data", (data) => {
|
|
3858
|
+
logger.info(data.toString().trim());
|
|
3859
|
+
});
|
|
3860
|
+
hermesProcess.stderr.on("data", (data) => {
|
|
3861
|
+
if (isWarned) {
|
|
3862
|
+
if (hermesLogs && logFile) logFile.write(data.toString().trim());
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
isWarned = true;
|
|
3866
|
+
logger.warning("Hermes command executed with warnings. Use --hermes-logs for full logs.");
|
|
3867
|
+
});
|
|
3868
|
+
hermesProcess.on("close", (exitCode, signal) => {
|
|
3869
|
+
if (hermesLogs && logFile) {
|
|
3870
|
+
logger.success("Done writing logs in output.log file.");
|
|
3871
|
+
logFile.end();
|
|
3872
|
+
}
|
|
3873
|
+
if (exitCode !== 0) {
|
|
3874
|
+
reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`));
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
const source = path8.join(outputFolder, bundleName + ".hbc");
|
|
3878
|
+
const destination = path8.join(outputFolder, "bundles", bundleName);
|
|
3879
|
+
fs10.copyFile(source, destination, (err) => {
|
|
3880
|
+
if (err) {
|
|
3881
|
+
reject(new Error(`Copying Hermes output failed: ${err.message}`));
|
|
3882
|
+
return;
|
|
3883
|
+
}
|
|
3884
|
+
fs10.unlink(source, (unlinkErr) => {
|
|
3885
|
+
if (unlinkErr) {
|
|
3886
|
+
reject(unlinkErr);
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
resolve();
|
|
3890
|
+
});
|
|
3891
|
+
});
|
|
3892
|
+
});
|
|
3893
|
+
});
|
|
3894
|
+
}
|
|
3895
|
+
function getHermesOSBin() {
|
|
3896
|
+
switch (process.platform) {
|
|
3897
|
+
case "win32":
|
|
3898
|
+
return "win64-bin";
|
|
3899
|
+
case "darwin":
|
|
3900
|
+
return "osx-bin";
|
|
3901
|
+
default:
|
|
3902
|
+
return "linux64-bin";
|
|
3903
|
+
}
|
|
3904
|
+
}
|
|
3905
|
+
function getHermesOSExe() {
|
|
3906
|
+
const versionObj = coerce(getReactNativeVersion());
|
|
3907
|
+
if (!versionObj?.version) {
|
|
3908
|
+
throw new Error("Unable to determine React Native version");
|
|
3909
|
+
}
|
|
3910
|
+
const react63orAbove = compare(versionObj.version, "0.63.0") !== -1;
|
|
3911
|
+
const hermesExecutableName = react63orAbove ? "hermesc" : "hermes";
|
|
3912
|
+
return process.platform === "win32" ? hermesExecutableName + ".exe" : hermesExecutableName;
|
|
3913
|
+
}
|
|
3914
|
+
async function getHermesCommand() {
|
|
3915
|
+
const bundledHermesEngine = path8.join(
|
|
3916
|
+
getReactNativePackagePath(),
|
|
3917
|
+
"sdks",
|
|
3918
|
+
"hermesc",
|
|
3919
|
+
getHermesOSBin(),
|
|
3920
|
+
getHermesOSExe()
|
|
3921
|
+
);
|
|
3922
|
+
if (fileExists(bundledHermesEngine)) return bundledHermesEngine;
|
|
3923
|
+
const hermesEnginePackageDir = resolvePackageDirFromCwd("hermes-engine");
|
|
3924
|
+
if (hermesEnginePackageDir) {
|
|
3925
|
+
const hermesEngine2 = path8.join(hermesEnginePackageDir, getHermesOSBin(), getHermesOSExe());
|
|
3926
|
+
if (fileExists(hermesEngine2)) return hermesEngine2;
|
|
3927
|
+
}
|
|
3928
|
+
const hermesEngine = path8.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe());
|
|
3929
|
+
if (fileExists(hermesEngine)) return hermesEngine;
|
|
3930
|
+
const hermesCompilerPackageDir = resolvePackageDirFromCwd("hermes-compiler");
|
|
3931
|
+
if (hermesCompilerPackageDir) {
|
|
3932
|
+
const hermesCompiler2 = path8.join(hermesCompilerPackageDir, "hermesc", getHermesOSBin(), getHermesOSExe());
|
|
3933
|
+
if (fileExists(hermesCompiler2)) return hermesCompiler2;
|
|
3934
|
+
}
|
|
3935
|
+
const hermesCompiler = path8.join("node_modules", "hermes-compiler", "hermesc", getHermesOSBin(), getHermesOSExe());
|
|
3936
|
+
if (fileExists(hermesCompiler)) return hermesCompiler;
|
|
3937
|
+
const hermesVmPackageDir = resolvePackageDirFromCwd("hermesvm");
|
|
3938
|
+
if (hermesVmPackageDir) {
|
|
3939
|
+
return path8.join(hermesVmPackageDir, getHermesOSBin(), "hermes");
|
|
3940
|
+
}
|
|
3941
|
+
return path8.join("node_modules", "hermesvm", getHermesOSBin(), "hermes");
|
|
3942
|
+
}
|
|
3943
|
+
|
|
3944
|
+
// src/commands/publish-bundle.ts
|
|
3945
|
+
var publishBundleCommand = new Command39("publish-bundle").description("Bundle, compile Hermes, sign, and upload a JS bundle to SwiftPatch").requiredOption("-p, --platform <platform>", "Target platform (ios or android)").option("-a, --app <app-id>", "App ID or slug").option("-o, --org <org-id>", "Organization ID").option("-e, --entry-file <path>", "Entry file for React Native bundle", "index.js").option("-n, --release-note <note>", "Release notes", "").option("-k, --private-key <path>", "Path to RSA private key for bundle signing").option("--dev", "Create a development bundle", false).option("--sourcemap", "Generate sourcemaps", false).option("--hermes", "Compile to Hermes bytecode", false).option("--hermesc-path <path>", "Custom path to hermesc binary").option("--hermes-logs", "Output Hermes compilation logs to file", false).option("--keep-artifacts", "Save intermediate build artifacts for debugging", false).option("--ci-token <token>", "CI token for authentication (alternative to login)").action(async (options) => {
|
|
3946
|
+
const platform = options.platform.toLowerCase();
|
|
3947
|
+
if (!isValidPlatform(platform)) {
|
|
3948
|
+
logger.error('Invalid platform. Must be "ios" or "android".');
|
|
3949
|
+
process.exit(1);
|
|
3950
|
+
}
|
|
3951
|
+
const ciToken = options.ciToken || process.env.SWIFTPATCH_CI_TOKEN;
|
|
3952
|
+
if (!ciToken) {
|
|
3953
|
+
await requireAuth();
|
|
3954
|
+
}
|
|
3955
|
+
const orgId = ciToken ? void 0 : await resolveOrgId(options.org);
|
|
3956
|
+
console.log("");
|
|
3957
|
+
logger.info(chalk37.bold("SwiftPatch Publish Bundle"));
|
|
3958
|
+
console.log("");
|
|
3959
|
+
let appSlug = options.app;
|
|
3960
|
+
let resolvedAppId = "";
|
|
3961
|
+
if (!appSlug && !ciToken) {
|
|
3962
|
+
const apps = await api.getApps(orgId);
|
|
3963
|
+
if (apps.length === 0) {
|
|
3964
|
+
logger.error("No apps found. Create one with: swiftpatch apps create");
|
|
3965
|
+
process.exit(1);
|
|
3966
|
+
}
|
|
3967
|
+
if (apps.length === 1) {
|
|
3968
|
+
appSlug = apps[0].slug || apps[0].id;
|
|
3969
|
+
resolvedAppId = apps[0].id;
|
|
3970
|
+
logger.info(`App: ${chalk37.cyan(apps[0].name)}`);
|
|
3971
|
+
} else {
|
|
3972
|
+
const inquirer24 = (await import("inquirer")).default;
|
|
3973
|
+
const { selectedApp } = await inquirer24.prompt([
|
|
3974
|
+
{
|
|
3975
|
+
type: "list",
|
|
3976
|
+
name: "selectedApp",
|
|
3977
|
+
message: "Select app:",
|
|
3978
|
+
choices: apps.map((app) => ({
|
|
3979
|
+
name: `${app.name} (${app.platform})`,
|
|
3980
|
+
value: app.slug || app.id
|
|
3981
|
+
}))
|
|
3982
|
+
}
|
|
3983
|
+
]);
|
|
3984
|
+
appSlug = selectedApp;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
const uploadPath = appSlug || resolvedAppId || "";
|
|
3988
|
+
const entryFile = options.entryFile || "index.js";
|
|
3989
|
+
const bundleName = platform === "ios" ? "main.jsbundle" : "index.android.bundle";
|
|
3990
|
+
const contentRootPath = path9.join(os3.tmpdir(), `swiftpatch-publish-${Date.now()}`);
|
|
3991
|
+
await fs11.ensureDir(contentRootPath);
|
|
3992
|
+
try {
|
|
3993
|
+
const spinner = ora31("Bundling JavaScript...").start();
|
|
3994
|
+
try {
|
|
3995
|
+
await runReactNativeBundleCommand(
|
|
3996
|
+
bundleName,
|
|
3997
|
+
entryFile,
|
|
3998
|
+
contentRootPath,
|
|
3999
|
+
platform,
|
|
4000
|
+
options.sourcemap || false,
|
|
4001
|
+
options.dev || false
|
|
4002
|
+
);
|
|
4003
|
+
spinner.succeed("JavaScript bundled");
|
|
4004
|
+
} catch (error) {
|
|
4005
|
+
spinner.fail(`Bundling failed: ${error.message}`);
|
|
4006
|
+
process.exit(1);
|
|
4007
|
+
}
|
|
4008
|
+
if (options.keepArtifacts) {
|
|
4009
|
+
await keepArtifacts(contentRootPath, platform, "normal");
|
|
4010
|
+
}
|
|
4011
|
+
if (options.hermes) {
|
|
4012
|
+
const hermesSpinner = ora31("Compiling to Hermes bytecode...").start();
|
|
4013
|
+
try {
|
|
4014
|
+
await runHermesEmitBinaryCommand(
|
|
4015
|
+
bundleName,
|
|
4016
|
+
contentRootPath,
|
|
4017
|
+
options.hermesLogs || false,
|
|
4018
|
+
options.hermescPath,
|
|
4019
|
+
options.sourcemap || false
|
|
4020
|
+
);
|
|
4021
|
+
hermesSpinner.succeed("Hermes bytecode compiled");
|
|
4022
|
+
} catch (error) {
|
|
4023
|
+
hermesSpinner.fail(`Hermes compilation failed: ${error.message}`);
|
|
4024
|
+
process.exit(1);
|
|
4025
|
+
}
|
|
4026
|
+
if (options.keepArtifacts) {
|
|
4027
|
+
await keepArtifacts(contentRootPath, platform, "hermes");
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
if (options.privateKey) {
|
|
4031
|
+
const signSpinner = ora31("Signing bundle...").start();
|
|
4032
|
+
try {
|
|
4033
|
+
const bundlesDir2 = path9.join(contentRootPath, "bundles");
|
|
4034
|
+
await signBundleDirectory(bundlesDir2, options.privateKey);
|
|
4035
|
+
signSpinner.succeed("Bundle signed with JWT RS256");
|
|
4036
|
+
} catch (error) {
|
|
4037
|
+
signSpinner.fail(`Signing failed: ${error.message}`);
|
|
4038
|
+
process.exit(1);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
const zipSpinner = ora31("Creating ZIP archive...").start();
|
|
4042
|
+
const bundlesDir = path9.join(contentRootPath, "bundles");
|
|
4043
|
+
await createZip(bundlesDir, contentRootPath);
|
|
4044
|
+
const zipPath = path9.join(contentRootPath, "build.zip");
|
|
4045
|
+
if (!await fs11.pathExists(zipPath)) {
|
|
4046
|
+
zipSpinner.fail("ZIP creation failed: build.zip not found");
|
|
4047
|
+
process.exit(1);
|
|
4048
|
+
}
|
|
4049
|
+
const zipSize = (await fs11.stat(zipPath)).size;
|
|
4050
|
+
zipSpinner.succeed(`ZIP archive created: ${chalk37.gray(formatBytes4(zipSize))}`);
|
|
4051
|
+
const hashSpinner = ora31("Computing ZIP hash...").start();
|
|
4052
|
+
const hash = calculateZipHash(zipPath);
|
|
4053
|
+
if (!hash) {
|
|
4054
|
+
hashSpinner.fail("Failed to compute ZIP hash");
|
|
4055
|
+
process.exit(1);
|
|
4056
|
+
}
|
|
4057
|
+
hashSpinner.succeed(`ZIP hash: ${chalk37.gray(hash.slice(0, 16))}...`);
|
|
4058
|
+
let bundleSignature;
|
|
4059
|
+
if (options.privateKey) {
|
|
4060
|
+
const bundlesDir2 = path9.join(contentRootPath, "bundles");
|
|
4061
|
+
const sig = await readBundleSignature(bundlesDir2);
|
|
4062
|
+
if (sig) bundleSignature = sig;
|
|
4063
|
+
}
|
|
4064
|
+
const urlSpinner = ora31("Requesting upload URL...").start();
|
|
4065
|
+
try {
|
|
4066
|
+
const { url } = await api.generateSignedUrl(
|
|
4067
|
+
{
|
|
4068
|
+
hash,
|
|
4069
|
+
uploadPath,
|
|
4070
|
+
platform,
|
|
4071
|
+
releaseNote: options.releaseNote || "",
|
|
4072
|
+
signature: bundleSignature
|
|
4073
|
+
},
|
|
4074
|
+
ciToken
|
|
4075
|
+
);
|
|
4076
|
+
urlSpinner.succeed("Upload URL received");
|
|
4077
|
+
const uploadSpinner = ora31("Uploading bundle...").start();
|
|
4078
|
+
await uploader.uploadZip(url, zipPath, (percent) => {
|
|
4079
|
+
uploadSpinner.text = `Uploading bundle... ${percent}%`;
|
|
4080
|
+
});
|
|
4081
|
+
uploadSpinner.succeed("Bundle uploaded");
|
|
4082
|
+
console.log("");
|
|
4083
|
+
console.log(chalk37.green(" Bundle published successfully!"));
|
|
4084
|
+
console.log("");
|
|
4085
|
+
console.log(` Hash: ${chalk37.cyan(hash)}`);
|
|
4086
|
+
console.log(` Platform: ${chalk37.cyan(platform)}`);
|
|
4087
|
+
console.log(` Size: ${chalk37.gray(formatBytes4(zipSize))}`);
|
|
4088
|
+
if (options.hermes) {
|
|
4089
|
+
console.log(` Hermes: ${chalk37.green("Yes")}`);
|
|
4090
|
+
}
|
|
4091
|
+
if (options.privateKey) {
|
|
4092
|
+
console.log(` Signed: ${chalk37.green("Yes")}`);
|
|
4093
|
+
}
|
|
4094
|
+
console.log("");
|
|
4095
|
+
console.log(
|
|
4096
|
+
chalk37.gray(" Use this hash to create a release:")
|
|
4097
|
+
);
|
|
4098
|
+
console.log(
|
|
4099
|
+
` ${chalk37.cyan(`swiftpatch release-bundle --hash ${hash} --app-version <version>`)}`
|
|
4100
|
+
);
|
|
4101
|
+
console.log("");
|
|
4102
|
+
} catch (error) {
|
|
4103
|
+
urlSpinner.fail(`Upload failed: ${error.message}`);
|
|
4104
|
+
process.exit(1);
|
|
4105
|
+
}
|
|
4106
|
+
} finally {
|
|
4107
|
+
await fs11.remove(contentRootPath).catch(() => {
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
function formatBytes4(bytes) {
|
|
4112
|
+
if (bytes < 1024) return bytes + " B";
|
|
4113
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
|
4114
|
+
return (bytes / 1024 / 1024).toFixed(1) + " MB";
|
|
4115
|
+
}
|
|
4116
|
+
|
|
4117
|
+
// src/commands/release-bundle.ts
|
|
4118
|
+
init_esm_shims();
|
|
4119
|
+
init_api();
|
|
4120
|
+
import { Command as Command40 } from "commander";
|
|
4121
|
+
import chalk38 from "chalk";
|
|
4122
|
+
import ora32 from "ora";
|
|
4123
|
+
var releaseBundleCommand = new Command40("release-bundle").description("Promote a published bundle to a release (requires CI token)").requiredOption("--hash <hash>", "Bundle hash from publish-bundle output").requiredOption("--app-version <version>", "Target binary app version (semver)").option("--project-id <id>", "Project/App ID").option("-n, --release-note <note>", "Release notes", "").option("-m, --mandatory", "Mark as mandatory update", false).option("--paused", "Create release in paused state", false).option("--ci-token <token>", "CI token for authentication").action(async (options) => {
|
|
4124
|
+
const ciToken = options.ciToken || process.env.SWIFTPATCH_CI_TOKEN;
|
|
4125
|
+
if (!ciToken) {
|
|
4126
|
+
logger.error("CI token required. Provide --ci-token or set SWIFTPATCH_CI_TOKEN env var.");
|
|
4127
|
+
process.exit(1);
|
|
4128
|
+
}
|
|
4129
|
+
if (!options.projectId) {
|
|
4130
|
+
logger.error("--project-id is required for release-bundle.");
|
|
4131
|
+
process.exit(1);
|
|
4132
|
+
}
|
|
4133
|
+
console.log("");
|
|
4134
|
+
logger.info(chalk38.bold("SwiftPatch Release Bundle"));
|
|
4135
|
+
console.log("");
|
|
4136
|
+
const spinner = ora32("Promoting bundle to release...").start();
|
|
4137
|
+
try {
|
|
4138
|
+
const result = await api.promoteBundle(
|
|
4139
|
+
{
|
|
4140
|
+
projectId: options.projectId,
|
|
4141
|
+
hash: options.hash,
|
|
4142
|
+
appVersion: options.appVersion,
|
|
4143
|
+
releaseNote: options.releaseNote || "",
|
|
4144
|
+
isMandatory: options.mandatory || false,
|
|
4145
|
+
isPaused: options.paused || false
|
|
4146
|
+
},
|
|
4147
|
+
ciToken
|
|
4148
|
+
);
|
|
4149
|
+
spinner.succeed("Bundle promoted to release!");
|
|
4150
|
+
console.log("");
|
|
4151
|
+
console.log(chalk38.green(" Release created successfully!"));
|
|
4152
|
+
console.log("");
|
|
4153
|
+
console.log(` Hash: ${chalk38.cyan(options.hash.slice(0, 16))}...`);
|
|
4154
|
+
console.log(` App Version: ${chalk38.cyan(options.appVersion)}`);
|
|
4155
|
+
console.log(` Mandatory: ${options.mandatory ? chalk38.yellow("Yes") : chalk38.gray("No")}`);
|
|
4156
|
+
console.log(` Paused: ${options.paused ? chalk38.yellow("Yes") : chalk38.gray("No")}`);
|
|
4157
|
+
if (result?.id) {
|
|
4158
|
+
console.log(` Release ID: ${chalk38.gray(result.id)}`);
|
|
4159
|
+
}
|
|
4160
|
+
console.log("");
|
|
4161
|
+
} catch (error) {
|
|
4162
|
+
spinner.fail(`Release failed: ${error.message}`);
|
|
4163
|
+
process.exit(1);
|
|
4164
|
+
}
|
|
4165
|
+
});
|
|
4166
|
+
|
|
4167
|
+
// src/commands/update-release.ts
|
|
4168
|
+
init_esm_shims();
|
|
4169
|
+
init_api();
|
|
4170
|
+
import { Command as Command41 } from "commander";
|
|
4171
|
+
import chalk39 from "chalk";
|
|
4172
|
+
import ora33 from "ora";
|
|
4173
|
+
var updateReleaseCommand = new Command41("update-release").description("Update an existing release by bundle hash (requires CI token)").requiredOption("--hash <hash>", "Bundle hash of the release to update").option("--project-id <id>", "Project/App ID").option("-n, --release-note <note>", "Updated release notes").option("-m, --mandatory", "Mark as mandatory update").option("--paused", "Pause the release").option("--rolled-back", "Mark release as rolled back").option("-r, --rollout <percent>", "Rollout percentage (1-100)").option("--ci-token <token>", "CI token for authentication").action(async (options) => {
|
|
4174
|
+
const ciToken = options.ciToken || process.env.SWIFTPATCH_CI_TOKEN;
|
|
4175
|
+
if (!ciToken) {
|
|
4176
|
+
logger.error("CI token required. Provide --ci-token or set SWIFTPATCH_CI_TOKEN env var.");
|
|
4177
|
+
process.exit(1);
|
|
4178
|
+
}
|
|
4179
|
+
if (!options.projectId) {
|
|
4180
|
+
logger.error("--project-id is required for update-release.");
|
|
4181
|
+
process.exit(1);
|
|
4182
|
+
}
|
|
4183
|
+
console.log("");
|
|
4184
|
+
logger.info(chalk39.bold("SwiftPatch Update Release"));
|
|
4185
|
+
console.log("");
|
|
4186
|
+
const spinner = ora33("Updating release...").start();
|
|
4187
|
+
try {
|
|
4188
|
+
const params = {
|
|
4189
|
+
projectId: options.projectId,
|
|
4190
|
+
hash: options.hash
|
|
4191
|
+
};
|
|
4192
|
+
if (options.releaseNote !== void 0) params.releaseNote = options.releaseNote;
|
|
4193
|
+
if (options.mandatory !== void 0) params.isMandatory = options.mandatory;
|
|
4194
|
+
if (options.paused !== void 0) params.isPaused = options.paused;
|
|
4195
|
+
if (options.rolledBack !== void 0) params.isRolledBack = options.rolledBack;
|
|
4196
|
+
if (options.rollout !== void 0) params.rolloutPercent = parseInt(options.rollout, 10);
|
|
4197
|
+
const result = await api.updateReleaseCli(params, ciToken);
|
|
4198
|
+
spinner.succeed("Release updated!");
|
|
4199
|
+
console.log("");
|
|
4200
|
+
console.log(chalk39.green(" Release updated successfully!"));
|
|
4201
|
+
console.log("");
|
|
4202
|
+
console.log(` Hash: ${chalk39.cyan(options.hash.slice(0, 16))}...`);
|
|
4203
|
+
if (options.releaseNote) {
|
|
4204
|
+
console.log(` Note: ${chalk39.gray(options.releaseNote.slice(0, 60))}`);
|
|
4205
|
+
}
|
|
4206
|
+
if (options.mandatory !== void 0) {
|
|
4207
|
+
console.log(` Mandatory: ${options.mandatory ? chalk39.yellow("Yes") : chalk39.gray("No")}`);
|
|
4208
|
+
}
|
|
4209
|
+
if (options.paused !== void 0) {
|
|
4210
|
+
console.log(` Paused: ${options.paused ? chalk39.yellow("Yes") : chalk39.gray("No")}`);
|
|
4211
|
+
}
|
|
4212
|
+
if (options.rolledBack !== void 0) {
|
|
4213
|
+
console.log(` Rolled Back: ${options.rolledBack ? chalk39.yellow("Yes") : chalk39.gray("No")}`);
|
|
4214
|
+
}
|
|
4215
|
+
if (options.rollout !== void 0) {
|
|
4216
|
+
console.log(` Rollout: ${chalk39.cyan(options.rollout + "%")}`);
|
|
4217
|
+
}
|
|
4218
|
+
if (result?.id) {
|
|
4219
|
+
console.log(` Release ID: ${chalk39.gray(result.id)}`);
|
|
4220
|
+
}
|
|
4221
|
+
console.log("");
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
spinner.fail(`Update failed: ${error.message}`);
|
|
4224
|
+
process.exit(1);
|
|
4225
|
+
}
|
|
4226
|
+
});
|
|
4227
|
+
|
|
4228
|
+
// src/commands/generate-key-pair.ts
|
|
4229
|
+
init_esm_shims();
|
|
4230
|
+
import { Command as Command42 } from "commander";
|
|
4231
|
+
import chalk40 from "chalk";
|
|
4232
|
+
import ora34 from "ora";
|
|
4233
|
+
import path10 from "path";
|
|
4234
|
+
var generateKeyPairCommand = new Command42("generate-key-pair").description("Generate RSA 2048-bit key pair for bundle signing").option("-o, --output <dir>", "Output directory for key files", ".").action(async (options) => {
|
|
4235
|
+
console.log("");
|
|
4236
|
+
logger.info(chalk40.bold("SwiftPatch Generate Key Pair"));
|
|
4237
|
+
console.log("");
|
|
4238
|
+
const outputDir = path10.resolve(options.output || ".");
|
|
4239
|
+
const spinner = ora34("Generating RSA 2048-bit key pair...").start();
|
|
4240
|
+
try {
|
|
4241
|
+
const { publicKeyPath, privateKeyPath } = await generateKeyPair(outputDir);
|
|
4242
|
+
spinner.succeed("Key pair generated!");
|
|
4243
|
+
console.log("");
|
|
4244
|
+
console.log(chalk40.green(" Key pair created successfully!"));
|
|
4245
|
+
console.log("");
|
|
4246
|
+
console.log(` Public key: ${chalk40.cyan(publicKeyPath)}`);
|
|
4247
|
+
console.log(` Private key: ${chalk40.cyan(privateKeyPath)}`);
|
|
4248
|
+
console.log("");
|
|
4249
|
+
console.log(chalk40.gray(" Usage:"));
|
|
4250
|
+
console.log(chalk40.gray(" 1. Upload the public key to your app settings in the SwiftPatch dashboard"));
|
|
4251
|
+
console.log(chalk40.gray(" 2. Use --private-key flag when publishing bundles:"));
|
|
4252
|
+
console.log(` ${chalk40.cyan(`swiftpatch publish-bundle --private-key ${privateKeyPath} -p ios`)}`);
|
|
4253
|
+
console.log("");
|
|
4254
|
+
console.log(chalk40.yellow(" Important: Keep your private key secure and never commit it to version control."));
|
|
4255
|
+
console.log(chalk40.yellow(" Add it to .gitignore:"));
|
|
4256
|
+
console.log(` ${chalk40.gray('echo "swiftpatch-private.pem" >> .gitignore')}`);
|
|
4257
|
+
console.log("");
|
|
4258
|
+
} catch (error) {
|
|
4259
|
+
spinner.fail(`Key generation failed: ${error.message}`);
|
|
4260
|
+
process.exit(1);
|
|
4261
|
+
}
|
|
4262
|
+
});
|
|
4263
|
+
|
|
4264
|
+
// src/commands/deploy.ts
|
|
4265
|
+
init_esm_shims();
|
|
4266
|
+
init_api();
|
|
4267
|
+
import { Command as Command43 } from "commander";
|
|
4268
|
+
import chalk41 from "chalk";
|
|
4269
|
+
import inquirer21 from "inquirer";
|
|
4270
|
+
import ora35 from "ora";
|
|
4271
|
+
import fs12 from "fs-extra";
|
|
4272
|
+
import path11 from "path";
|
|
4273
|
+
var deployCommand = new Command43("deploy").description("Bundle, publish, and release in one step").option("-p, --platform <platform>", "Target platform (ios or android)").option("-a, --app <app-id>", "App ID or slug").option("-o, --org <org-id>", "Organization ID").option("--app-version <version>", "Target app version (semver)").option("-n, --release-note <note>", "Release notes").option("-m, --mandatory", "Mark as mandatory update").option("--paused", "Create release in paused state").option("--hermes", "Compile to Hermes bytecode").option("--ci-token <token>", "CI token for authentication").option("-e, --entry-file <path>", "Entry file (default: index.js)").option("--dev", "Dev bundle").option("--sourcemap", "Generate sourcemaps").option("-k, --private-key <path>", "Private key path for signing").option("-y, --yes", "Skip confirmations").action(async (options) => {
|
|
4274
|
+
const ciToken = options.ciToken || process.env.SWIFTPATCH_CI_TOKEN;
|
|
4275
|
+
if (!ciToken) {
|
|
4276
|
+
await requireAuth();
|
|
4277
|
+
}
|
|
4278
|
+
const orgId = ciToken ? void 0 : await resolveOrgId(options.org);
|
|
4279
|
+
let platform = options.platform;
|
|
4280
|
+
if (!platform) {
|
|
4281
|
+
const hasIos = await fs12.pathExists("./ios");
|
|
4282
|
+
const hasAndroid = await fs12.pathExists("./android");
|
|
4283
|
+
if (hasIos && !hasAndroid) {
|
|
4284
|
+
platform = "ios";
|
|
4285
|
+
} else if (hasAndroid && !hasIos) {
|
|
4286
|
+
platform = "android";
|
|
4287
|
+
} else {
|
|
4288
|
+
const answers = await inquirer21.prompt([
|
|
4289
|
+
{
|
|
4290
|
+
type: "list",
|
|
4291
|
+
name: "platform",
|
|
4292
|
+
message: "Select platform:",
|
|
4293
|
+
choices: ["ios", "android"]
|
|
4294
|
+
}
|
|
4295
|
+
]);
|
|
4296
|
+
platform = answers.platform;
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
let appVersion = options.appVersion;
|
|
4300
|
+
if (!appVersion) {
|
|
4301
|
+
try {
|
|
4302
|
+
const pkg = await fs12.readJson("./package.json");
|
|
4303
|
+
appVersion = pkg.version;
|
|
4304
|
+
} catch {
|
|
4305
|
+
}
|
|
4306
|
+
if (!appVersion) {
|
|
4307
|
+
const answers = await inquirer21.prompt([
|
|
4308
|
+
{
|
|
4309
|
+
type: "input",
|
|
4310
|
+
name: "version",
|
|
4311
|
+
message: "App version (semver):",
|
|
4312
|
+
validate: (v) => /^\d+\.\d+\.\d+/.test(v) || "Must be valid semver (e.g., 1.0.0)"
|
|
4313
|
+
}
|
|
4314
|
+
]);
|
|
4315
|
+
appVersion = answers.version;
|
|
4316
|
+
} else if (!options.yes) {
|
|
4317
|
+
console.log(chalk41.gray(` Detected version: ${appVersion}`));
|
|
4318
|
+
}
|
|
4319
|
+
}
|
|
4320
|
+
const releaseNote = options.releaseNote || "";
|
|
4321
|
+
let uploadPath = options.app || "";
|
|
4322
|
+
if (!uploadPath && orgId) {
|
|
4323
|
+
const apps = await api.getApps(orgId);
|
|
4324
|
+
if (apps.length === 1) {
|
|
4325
|
+
uploadPath = apps[0].slug || apps[0].id;
|
|
4326
|
+
if (!options.yes) console.log(chalk41.gray(` Using app: ${apps[0].name}`));
|
|
4327
|
+
} else if (apps.length > 1) {
|
|
4328
|
+
const { selectedApp } = await inquirer21.prompt([
|
|
4329
|
+
{
|
|
4330
|
+
type: "list",
|
|
4331
|
+
name: "selectedApp",
|
|
4332
|
+
message: "Select app:",
|
|
4333
|
+
choices: apps.map((a) => ({ name: `${a.name} (${a.platform})`, value: a.slug || a.id }))
|
|
4334
|
+
}
|
|
4335
|
+
]);
|
|
4336
|
+
uploadPath = selectedApp;
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
console.log("");
|
|
4340
|
+
console.log(chalk41.bold(" Deploy"));
|
|
4341
|
+
console.log(chalk41.gray(` Platform: ${platform} \xB7 Version: ${appVersion}`));
|
|
4342
|
+
console.log("");
|
|
4343
|
+
const bundleSpinner = ora35("Bundling JavaScript...").start();
|
|
4344
|
+
try {
|
|
4345
|
+
const entryFile = options.entryFile || "index.js";
|
|
4346
|
+
const outputDir = path11.join(".swiftpatch-tmp", platform);
|
|
4347
|
+
await fs12.ensureDir(outputDir);
|
|
4348
|
+
const bundleName = platform === "ios" ? "main.jsbundle" : "index.android.bundle";
|
|
4349
|
+
await runReactNativeBundleCommand(
|
|
4350
|
+
bundleName,
|
|
4351
|
+
entryFile,
|
|
4352
|
+
outputDir,
|
|
4353
|
+
platform,
|
|
4354
|
+
options.sourcemap || false,
|
|
4355
|
+
options.dev || false
|
|
4356
|
+
);
|
|
4357
|
+
if (options.hermes) {
|
|
4358
|
+
bundleSpinner.text = "Compiling to Hermes bytecode...";
|
|
4359
|
+
}
|
|
4360
|
+
bundleSpinner.succeed("Bundle created");
|
|
4361
|
+
} catch (error) {
|
|
4362
|
+
bundleSpinner.fail(`Bundle failed: ${error.message}`);
|
|
4363
|
+
process.exit(1);
|
|
4364
|
+
}
|
|
4365
|
+
const archiveSpinner = ora35("Creating archive...").start();
|
|
4366
|
+
let zipPath;
|
|
4367
|
+
let bundleHash;
|
|
4368
|
+
try {
|
|
4369
|
+
const outputDir = path11.join(".swiftpatch-tmp", platform);
|
|
4370
|
+
const zipOutputDir = path11.join(".swiftpatch-tmp", "zip");
|
|
4371
|
+
await fs12.ensureDir(zipOutputDir);
|
|
4372
|
+
await createZip(outputDir, zipOutputDir);
|
|
4373
|
+
zipPath = path11.join(zipOutputDir, "build.zip");
|
|
4374
|
+
bundleHash = calculateZipHash(zipPath);
|
|
4375
|
+
if (!bundleHash) throw new Error("Failed to compute bundle hash");
|
|
4376
|
+
archiveSpinner.succeed(`Archive created (hash: ${bundleHash.slice(0, 12)}...)`);
|
|
4377
|
+
} catch (error) {
|
|
4378
|
+
archiveSpinner.fail(`Archive failed: ${error.message}`);
|
|
4379
|
+
process.exit(1);
|
|
4380
|
+
}
|
|
4381
|
+
const uploadSpinner = ora35("Uploading bundle...").start();
|
|
4382
|
+
try {
|
|
4383
|
+
const { url } = await api.generateSignedUrl(
|
|
4384
|
+
{ hash: bundleHash, uploadPath, platform, releaseNote },
|
|
4385
|
+
ciToken
|
|
4386
|
+
);
|
|
4387
|
+
await uploader.uploadZip(url, zipPath, (progress) => {
|
|
4388
|
+
uploadSpinner.text = `Uploading bundle... ${progress}%`;
|
|
4389
|
+
});
|
|
4390
|
+
uploadSpinner.succeed("Bundle uploaded");
|
|
4391
|
+
} catch (error) {
|
|
4392
|
+
uploadSpinner.fail(`Upload failed: ${error.message}`);
|
|
4393
|
+
process.exit(1);
|
|
4394
|
+
}
|
|
4395
|
+
if (!ciToken) {
|
|
4396
|
+
console.log("");
|
|
4397
|
+
logger.warning("Deploy requires a CI token to create the release.");
|
|
4398
|
+
console.log(chalk41.gray(" Bundle was uploaded. Promote it manually:"));
|
|
4399
|
+
console.log(chalk41.cyan(` $ swiftpatch release-bundle --hash ${bundleHash} --app-version ${appVersion} --ci-token <token>`));
|
|
4400
|
+
console.log("");
|
|
4401
|
+
} else {
|
|
4402
|
+
const releaseSpinner = ora35("Creating release...").start();
|
|
4403
|
+
try {
|
|
4404
|
+
await api.promoteBundle(
|
|
4405
|
+
{
|
|
4406
|
+
projectId: uploadPath,
|
|
4407
|
+
hash: bundleHash,
|
|
4408
|
+
appVersion,
|
|
4409
|
+
releaseNote,
|
|
4410
|
+
isMandatory: options.mandatory || false,
|
|
4411
|
+
isPaused: options.paused || false
|
|
4412
|
+
},
|
|
4413
|
+
ciToken
|
|
4414
|
+
);
|
|
4415
|
+
releaseSpinner.succeed("Release created!");
|
|
4416
|
+
console.log("");
|
|
4417
|
+
console.log(chalk41.bold(" Deploy Complete"));
|
|
4418
|
+
console.log(` Hash: ${chalk41.gray(bundleHash)}`);
|
|
4419
|
+
console.log(` Version: ${chalk41.cyan(appVersion)}`);
|
|
4420
|
+
console.log(` Platform: ${platform}`);
|
|
4421
|
+
console.log(` Mandatory: ${options.mandatory ? chalk41.yellow("Yes") : "No"}`);
|
|
4422
|
+
console.log(` Status: ${options.paused ? chalk41.yellow("Paused") : chalk41.green("Released")}`);
|
|
4423
|
+
console.log("");
|
|
4424
|
+
} catch (error) {
|
|
4425
|
+
releaseSpinner.fail(`Release failed: ${error.message}`);
|
|
4426
|
+
process.exit(1);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
await fs12.remove(".swiftpatch-tmp").catch(() => {
|
|
4430
|
+
});
|
|
4431
|
+
});
|
|
4432
|
+
|
|
4433
|
+
// src/commands/status.ts
|
|
4434
|
+
init_esm_shims();
|
|
4435
|
+
init_api();
|
|
4436
|
+
import { Command as Command44 } from "commander";
|
|
4437
|
+
import chalk42 from "chalk";
|
|
4438
|
+
import inquirer22 from "inquirer";
|
|
4439
|
+
import ora36 from "ora";
|
|
4440
|
+
var statusCommand = new Command44("status").description("Show app status at a glance").argument("[app-id]", "App ID").option("-o, --org <org-id>", "Organization ID").option("--json", "Output as JSON").action(async (appId, options) => {
|
|
4441
|
+
await requireAuth();
|
|
4442
|
+
const orgId = await resolveOrgId(options.org);
|
|
4443
|
+
if (!appId) {
|
|
4444
|
+
const apps = await api.getApps(orgId);
|
|
4445
|
+
if (apps.length === 0) {
|
|
4446
|
+
console.log("");
|
|
4447
|
+
console.log(chalk42.gray(" No apps found. Create one with: swiftpatch apps create"));
|
|
4448
|
+
console.log("");
|
|
4449
|
+
return;
|
|
4450
|
+
}
|
|
4451
|
+
const { selectedApp } = await inquirer22.prompt([
|
|
4452
|
+
{
|
|
4453
|
+
type: "list",
|
|
4454
|
+
name: "selectedApp",
|
|
4455
|
+
message: "Select app:",
|
|
4456
|
+
choices: apps.map((app) => ({ name: `${app.name} (${app.platform})`, value: app.id }))
|
|
4457
|
+
}
|
|
4458
|
+
]);
|
|
4459
|
+
appId = selectedApp;
|
|
4460
|
+
}
|
|
4461
|
+
const spinner = ora36("Fetching status...").start();
|
|
4462
|
+
try {
|
|
4463
|
+
const [app, releasesResult, channels, analytics] = await Promise.all([
|
|
4464
|
+
api.getApp(orgId, appId),
|
|
4465
|
+
api.getReleases(orgId, appId, { page: 1, limit: 5 }),
|
|
4466
|
+
api.getChannels(orgId, appId),
|
|
4467
|
+
api.getAnalytics(orgId, appId, { days: 7 }).catch(() => null)
|
|
4468
|
+
]);
|
|
4469
|
+
spinner.stop();
|
|
4470
|
+
if (options.json) {
|
|
4471
|
+
console.log(JSON.stringify({ app, releases: releasesResult.releases, channels, analytics }, null, 2));
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
console.log("");
|
|
4475
|
+
console.log(chalk42.bold(` ${app.name}`));
|
|
4476
|
+
console.log(chalk42.gray(` ${app.slug} \xB7 ${app.platform} \xB7 ${app.signingEnabled ? "Signing enabled" : "No signing"}`));
|
|
4477
|
+
console.log("");
|
|
4478
|
+
if (app.stats) {
|
|
4479
|
+
console.log(chalk42.bold(" Overview"));
|
|
4480
|
+
console.log(` Releases: ${chalk42.cyan(String(app.stats.totalReleases))}`);
|
|
4481
|
+
console.log(` Total Installs: ${chalk42.cyan(String(app.stats.totalInstalls))}`);
|
|
4482
|
+
console.log(` Active Devices: ${chalk42.green(String(app.stats.activeDevices))}`);
|
|
4483
|
+
console.log("");
|
|
4484
|
+
}
|
|
4485
|
+
if (analytics) {
|
|
4486
|
+
console.log(chalk42.bold(" Last 7 Days"));
|
|
4487
|
+
console.log(` Updates: ${chalk42.cyan(String(analytics.totalUpdates))}`);
|
|
4488
|
+
console.log(` Success Rate: ${analytics.successRate >= 95 ? chalk42.green(analytics.successRate + "%") : chalk42.yellow(analytics.successRate + "%")}`);
|
|
4489
|
+
if (analytics.failedUpdates > 0) {
|
|
4490
|
+
console.log(` Failed: ${chalk42.red(String(analytics.failedUpdates))}`);
|
|
4491
|
+
}
|
|
4492
|
+
console.log("");
|
|
4493
|
+
}
|
|
4494
|
+
const releases = releasesResult.releases;
|
|
4495
|
+
if (releases.length > 0) {
|
|
4496
|
+
console.log(chalk42.bold(" Recent Releases"));
|
|
4497
|
+
for (const r of releases) {
|
|
4498
|
+
const statusColor = r.status === "RELEASED" ? chalk42.green : r.status === "DISABLED" ? chalk42.red : r.status === "READY" ? chalk42.blue : chalk42.gray;
|
|
4499
|
+
console.log(
|
|
4500
|
+
` ${statusColor("\u25CF")} v${r.version} (build ${r.buildNumber}) \xB7 ${r.platform} \xB7 ${statusColor(r.status)} \xB7 ${r.rolloutPercent}% rollout`
|
|
4501
|
+
);
|
|
4502
|
+
}
|
|
4503
|
+
console.log("");
|
|
4504
|
+
}
|
|
4505
|
+
if (channels.length > 0) {
|
|
4506
|
+
console.log(chalk42.bold(" Channels"));
|
|
4507
|
+
console.log(` ${channels.map((c) => chalk42.cyan(c.name)).join(", ")}`);
|
|
4508
|
+
console.log("");
|
|
4509
|
+
}
|
|
4510
|
+
console.log(chalk42.gray(` Deployment Key: ${app.deploymentKey}`));
|
|
4511
|
+
console.log("");
|
|
4512
|
+
} catch (error) {
|
|
4513
|
+
spinner.fail(`Failed to fetch status: ${error.message}`);
|
|
4514
|
+
process.exit(1);
|
|
4515
|
+
}
|
|
4516
|
+
});
|
|
4517
|
+
|
|
4518
|
+
// src/commands/init.ts
|
|
4519
|
+
init_esm_shims();
|
|
4520
|
+
init_api();
|
|
4521
|
+
init_auth();
|
|
4522
|
+
init_config();
|
|
4523
|
+
import { Command as Command45 } from "commander";
|
|
4524
|
+
import chalk43 from "chalk";
|
|
4525
|
+
import inquirer23 from "inquirer";
|
|
4526
|
+
import ora37 from "ora";
|
|
4527
|
+
import fs13 from "fs-extra";
|
|
4528
|
+
import path12 from "path";
|
|
4529
|
+
var initCommand = new Command45("init").description("Initialize SwiftPatch in your React Native project").option("-y, --yes", "Accept defaults").action(async (options) => {
|
|
4530
|
+
console.log("");
|
|
4531
|
+
console.log(chalk43.bold(" SwiftPatch Setup"));
|
|
4532
|
+
console.log("");
|
|
4533
|
+
const hasPackageJson = await fs13.pathExists("./package.json");
|
|
4534
|
+
if (!hasPackageJson) {
|
|
4535
|
+
logger.error("No package.json found. Run this command in your React Native project root.");
|
|
4536
|
+
process.exit(1);
|
|
4537
|
+
}
|
|
4538
|
+
const pkg = await fs13.readJson("./package.json");
|
|
4539
|
+
const hasRN = pkg.dependencies?.["react-native"] || pkg.devDependencies?.["react-native"];
|
|
4540
|
+
if (!hasRN) {
|
|
4541
|
+
logger.warning("react-native not found in dependencies. Is this a React Native project?");
|
|
4542
|
+
if (!options.yes) {
|
|
4543
|
+
const { proceed } = await inquirer23.prompt([
|
|
4544
|
+
{ type: "confirm", name: "proceed", message: "Continue anyway?", default: false }
|
|
4545
|
+
]);
|
|
4546
|
+
if (!proceed) return;
|
|
4547
|
+
}
|
|
4548
|
+
} else {
|
|
4549
|
+
logger.success(`React Native project detected (${hasRN})`);
|
|
4550
|
+
}
|
|
4551
|
+
const hasSDK = pkg.dependencies?.["@swiftpatch/react-native"] || pkg.devDependencies?.["@swiftpatch/react-native"];
|
|
4552
|
+
if (!hasSDK) {
|
|
4553
|
+
logger.warning("@swiftpatch/react-native SDK not installed");
|
|
4554
|
+
console.log(chalk43.gray(" Install with: npm install @swiftpatch/react-native"));
|
|
4555
|
+
console.log("");
|
|
4556
|
+
} else {
|
|
4557
|
+
logger.success(`SwiftPatch SDK installed (${hasSDK})`);
|
|
4558
|
+
}
|
|
4559
|
+
if (!auth.isLoggedIn()) {
|
|
4560
|
+
logger.warning("Not logged in");
|
|
4561
|
+
console.log(chalk43.gray(" Run: swiftpatch login"));
|
|
4562
|
+
console.log("");
|
|
4563
|
+
process.exit(1);
|
|
4564
|
+
}
|
|
4565
|
+
logger.success("Authenticated");
|
|
4566
|
+
const orgId = await resolveOrgId();
|
|
4567
|
+
const hasIos = await fs13.pathExists("./ios");
|
|
4568
|
+
const hasAndroid = await fs13.pathExists("./android");
|
|
4569
|
+
const platforms = [];
|
|
4570
|
+
if (hasIos) platforms.push("ios");
|
|
4571
|
+
if (hasAndroid) platforms.push("android");
|
|
4572
|
+
if (platforms.length > 0) {
|
|
4573
|
+
logger.success(`Platforms detected: ${platforms.join(", ")}`);
|
|
4574
|
+
}
|
|
4575
|
+
const spinner = ora37("Fetching apps...").start();
|
|
4576
|
+
const apps = await api.getApps(orgId);
|
|
4577
|
+
spinner.stop();
|
|
4578
|
+
let selectedApp;
|
|
4579
|
+
if (apps.length > 0 && !options.yes) {
|
|
4580
|
+
const { action } = await inquirer23.prompt([
|
|
4581
|
+
{
|
|
4582
|
+
type: "list",
|
|
4583
|
+
name: "action",
|
|
4584
|
+
message: "Link to an existing app or create a new one?",
|
|
4585
|
+
choices: [
|
|
4586
|
+
{ name: "Link existing app", value: "link" },
|
|
4587
|
+
{ name: "Create new app", value: "create" }
|
|
4588
|
+
]
|
|
4589
|
+
}
|
|
4590
|
+
]);
|
|
4591
|
+
if (action === "link") {
|
|
4592
|
+
const { appChoice } = await inquirer23.prompt([
|
|
4593
|
+
{
|
|
4594
|
+
type: "list",
|
|
4595
|
+
name: "appChoice",
|
|
4596
|
+
message: "Select app:",
|
|
4597
|
+
choices: apps.map((a) => ({ name: `${a.name} (${a.platform})`, value: a }))
|
|
4598
|
+
}
|
|
4599
|
+
]);
|
|
4600
|
+
selectedApp = appChoice;
|
|
4601
|
+
} else {
|
|
4602
|
+
selectedApp = await createApp(orgId, pkg, platforms);
|
|
4603
|
+
}
|
|
4604
|
+
} else if (apps.length === 0) {
|
|
4605
|
+
console.log(chalk43.gray(" No apps found. Creating one..."));
|
|
4606
|
+
selectedApp = await createApp(orgId, pkg, platforms);
|
|
4607
|
+
} else {
|
|
4608
|
+
selectedApp = apps[0];
|
|
4609
|
+
}
|
|
4610
|
+
let defaultPlatform;
|
|
4611
|
+
if (hasIos && !hasAndroid) defaultPlatform = "ios";
|
|
4612
|
+
if (hasAndroid && !hasIos) defaultPlatform = "android";
|
|
4613
|
+
const rcConfig = {
|
|
4614
|
+
appId: selectedApp.id,
|
|
4615
|
+
app: selectedApp.slug,
|
|
4616
|
+
platform: defaultPlatform || (selectedApp.platform === "BOTH" ? void 0 : selectedApp.platform.toLowerCase()),
|
|
4617
|
+
deploymentKey: selectedApp.deploymentKey
|
|
4618
|
+
};
|
|
4619
|
+
const rcPath = path12.resolve(".swiftpatchrc");
|
|
4620
|
+
await fs13.writeJson(rcPath, rcConfig, { spaces: 2 });
|
|
4621
|
+
logger.success("Created .swiftpatchrc");
|
|
4622
|
+
config.set("defaultOrg", orgId);
|
|
4623
|
+
config.set("defaultApp", selectedApp.id);
|
|
4624
|
+
if (defaultPlatform) config.set("defaultPlatform", defaultPlatform);
|
|
4625
|
+
logger.success("Saved defaults to CLI config");
|
|
4626
|
+
console.log("");
|
|
4627
|
+
console.log(chalk43.bold(" Setup Complete"));
|
|
4628
|
+
console.log("");
|
|
4629
|
+
console.log(` App: ${chalk43.cyan(selectedApp.name)}`);
|
|
4630
|
+
console.log(` Platform: ${selectedApp.platform}`);
|
|
4631
|
+
console.log(` Deployment Key: ${chalk43.gray(selectedApp.deploymentKey)}`);
|
|
4632
|
+
console.log(` Config: ${chalk43.gray(rcPath)}`);
|
|
4633
|
+
console.log("");
|
|
4634
|
+
console.log(chalk43.bold(" Next Steps"));
|
|
4635
|
+
console.log(chalk43.gray(" 1. Add the deployment key to your app (Info.plist / strings.xml)"));
|
|
4636
|
+
console.log(chalk43.gray(" 2. Wrap your app with <SwiftPatchProvider />"));
|
|
4637
|
+
console.log(chalk43.gray(" 3. Run: swiftpatch deploy -p ios --ci-token <token>"));
|
|
4638
|
+
console.log("");
|
|
4639
|
+
});
|
|
4640
|
+
async function createApp(orgId, pkg, platforms) {
|
|
4641
|
+
const { name } = await inquirer23.prompt([
|
|
4642
|
+
{
|
|
4643
|
+
type: "input",
|
|
4644
|
+
name: "name",
|
|
4645
|
+
message: "App name:",
|
|
4646
|
+
default: pkg.name || path12.basename(process.cwd())
|
|
4647
|
+
}
|
|
4648
|
+
]);
|
|
4649
|
+
let platform;
|
|
4650
|
+
if (platforms.length === 2) {
|
|
4651
|
+
platform = "BOTH";
|
|
4652
|
+
} else if (platforms.length === 1) {
|
|
4653
|
+
platform = platforms[0].toUpperCase();
|
|
4654
|
+
} else {
|
|
4655
|
+
const { p } = await inquirer23.prompt([
|
|
4656
|
+
{
|
|
4657
|
+
type: "list",
|
|
4658
|
+
name: "p",
|
|
4659
|
+
message: "Platform:",
|
|
4660
|
+
choices: [
|
|
4661
|
+
{ name: "iOS", value: "IOS" },
|
|
4662
|
+
{ name: "Android", value: "ANDROID" },
|
|
4663
|
+
{ name: "Both", value: "BOTH" }
|
|
4664
|
+
]
|
|
4665
|
+
}
|
|
4666
|
+
]);
|
|
4667
|
+
platform = p;
|
|
4668
|
+
}
|
|
4669
|
+
const createSpinner = ora37("Creating app...").start();
|
|
4670
|
+
const app = await api.createApp(orgId, { name, platform });
|
|
4671
|
+
createSpinner.succeed(`App created: ${app.name}`);
|
|
4672
|
+
return app;
|
|
4673
|
+
}
|
|
4674
|
+
|
|
4675
|
+
// src/commands/doctor.ts
|
|
4676
|
+
init_esm_shims();
|
|
4677
|
+
init_api();
|
|
4678
|
+
init_auth();
|
|
4679
|
+
init_config();
|
|
4680
|
+
import { Command as Command46 } from "commander";
|
|
4681
|
+
import chalk44 from "chalk";
|
|
4682
|
+
import fs14 from "fs-extra";
|
|
4683
|
+
var doctorCommand = new Command46("doctor").description("Diagnose your SwiftPatch setup").action(async () => {
|
|
4684
|
+
console.log("");
|
|
4685
|
+
console.log(chalk44.bold(" SwiftPatch Doctor"));
|
|
4686
|
+
console.log("");
|
|
4687
|
+
let issues = 0;
|
|
4688
|
+
const nodeVersion = process.version;
|
|
4689
|
+
const major = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
4690
|
+
if (major >= 18) {
|
|
4691
|
+
logger.success(`Node.js ${nodeVersion}`);
|
|
4692
|
+
} else {
|
|
4693
|
+
logger.error(`Node.js ${nodeVersion} \u2014 requires >= 18.0.0`);
|
|
4694
|
+
issues++;
|
|
4695
|
+
}
|
|
4696
|
+
const hasPackageJson = await fs14.pathExists("./package.json");
|
|
4697
|
+
if (hasPackageJson) {
|
|
4698
|
+
const pkg = await fs14.readJson("./package.json");
|
|
4699
|
+
const rnVersion = pkg.dependencies?.["react-native"] || pkg.devDependencies?.["react-native"];
|
|
4700
|
+
if (rnVersion) {
|
|
4701
|
+
logger.success(`React Native ${rnVersion}`);
|
|
4702
|
+
} else {
|
|
4703
|
+
logger.warning("react-native not found in dependencies");
|
|
4704
|
+
issues++;
|
|
4705
|
+
}
|
|
4706
|
+
const sdkVersion = pkg.dependencies?.["@swiftpatch/react-native"] || pkg.devDependencies?.["@swiftpatch/react-native"];
|
|
4707
|
+
if (sdkVersion) {
|
|
4708
|
+
logger.success(`@swiftpatch/react-native ${sdkVersion}`);
|
|
4709
|
+
} else {
|
|
4710
|
+
logger.error("@swiftpatch/react-native SDK not installed");
|
|
4711
|
+
console.log(chalk44.gray(" Fix: npm install @swiftpatch/react-native"));
|
|
4712
|
+
issues++;
|
|
4713
|
+
}
|
|
4714
|
+
} else {
|
|
4715
|
+
logger.warning("No package.json found (not in a project directory?)");
|
|
4716
|
+
}
|
|
4717
|
+
const hasIos = await fs14.pathExists("./ios");
|
|
4718
|
+
const hasAndroid = await fs14.pathExists("./android");
|
|
4719
|
+
if (hasIos || hasAndroid) {
|
|
4720
|
+
const platforms = [hasIos && "iOS", hasAndroid && "Android"].filter(Boolean).join(", ");
|
|
4721
|
+
logger.success(`Platforms: ${platforms}`);
|
|
4722
|
+
} else if (hasPackageJson) {
|
|
4723
|
+
logger.warning("No ios/ or android/ directories found");
|
|
4724
|
+
issues++;
|
|
4725
|
+
}
|
|
4726
|
+
if (auth.isLoggedIn()) {
|
|
4727
|
+
logger.success("Authenticated");
|
|
4728
|
+
try {
|
|
4729
|
+
const user = await api.getCurrentUser();
|
|
4730
|
+
logger.success(`Connected as ${user.name} (${user.email})`);
|
|
4731
|
+
} catch (error) {
|
|
4732
|
+
logger.error(`API connection failed: ${error.message}`);
|
|
4733
|
+
issues++;
|
|
4734
|
+
}
|
|
4735
|
+
} else {
|
|
4736
|
+
logger.error("Not authenticated");
|
|
4737
|
+
console.log(chalk44.gray(" Fix: swiftpatch login"));
|
|
4738
|
+
issues++;
|
|
4739
|
+
}
|
|
4740
|
+
const customApiUrl = config.get("apiUrl");
|
|
4741
|
+
if (customApiUrl) {
|
|
4742
|
+
logger.info(`Custom API URL: ${customApiUrl}`);
|
|
4743
|
+
if (customApiUrl.startsWith("http://") && !customApiUrl.includes("localhost")) {
|
|
4744
|
+
logger.warning("API URL uses HTTP \u2014 consider HTTPS for production");
|
|
4745
|
+
issues++;
|
|
4746
|
+
}
|
|
4747
|
+
} else {
|
|
4748
|
+
logger.success("API URL: default (production)");
|
|
4749
|
+
}
|
|
4750
|
+
const defaultOrg = config.get("defaultOrg");
|
|
4751
|
+
const defaultApp = config.get("defaultApp");
|
|
4752
|
+
if (defaultOrg) {
|
|
4753
|
+
logger.success(`Default org: ${defaultOrg}`);
|
|
4754
|
+
} else {
|
|
4755
|
+
logger.info("No default org set (will prompt each time)");
|
|
4756
|
+
}
|
|
4757
|
+
if (defaultApp) {
|
|
4758
|
+
logger.success(`Default app: ${defaultApp}`);
|
|
4759
|
+
} else {
|
|
4760
|
+
logger.info("No default app set");
|
|
4761
|
+
}
|
|
4762
|
+
const hasRc = await fs14.pathExists("./.swiftpatchrc");
|
|
4763
|
+
if (hasRc) {
|
|
4764
|
+
try {
|
|
4765
|
+
const rc = await fs14.readJson("./.swiftpatchrc");
|
|
4766
|
+
logger.success(`.swiftpatchrc found (app: ${rc.app || rc.appId || "unknown"})`);
|
|
4767
|
+
} catch {
|
|
4768
|
+
logger.warning(".swiftpatchrc exists but is invalid JSON");
|
|
4769
|
+
issues++;
|
|
4770
|
+
}
|
|
4771
|
+
} else {
|
|
4772
|
+
logger.info("No .swiftpatchrc (run: swiftpatch init)");
|
|
4773
|
+
}
|
|
4774
|
+
if (hasPackageJson) {
|
|
4775
|
+
try {
|
|
4776
|
+
const pkg = await fs14.readJson("./package.json");
|
|
4777
|
+
const rnVersion = pkg.dependencies?.["react-native"];
|
|
4778
|
+
if (rnVersion) {
|
|
4779
|
+
const cleanVersion = rnVersion.replace(/[\^~>=<]/g, "");
|
|
4780
|
+
const [, minor] = cleanVersion.split(".");
|
|
4781
|
+
if (parseInt(minor) >= 70) {
|
|
4782
|
+
logger.success("Hermes likely enabled (RN 0.70+)");
|
|
4783
|
+
} else {
|
|
4784
|
+
logger.info("Hermes may need to be manually enabled");
|
|
4785
|
+
}
|
|
4786
|
+
}
|
|
4787
|
+
} catch {
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
4790
|
+
console.log("");
|
|
4791
|
+
if (issues === 0) {
|
|
4792
|
+
console.log(chalk44.green.bold(" All checks passed!"));
|
|
4793
|
+
} else {
|
|
4794
|
+
console.log(chalk44.yellow.bold(` ${issues} issue${issues > 1 ? "s" : ""} found`));
|
|
4795
|
+
}
|
|
4796
|
+
console.log("");
|
|
4797
|
+
});
|
|
4798
|
+
|
|
4799
|
+
// src/cli.ts
|
|
4800
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
4801
|
+
var version = "1.0.0";
|
|
4802
|
+
try {
|
|
4803
|
+
const pkg = JSON.parse(readFileSync3(join2(__dirname2, "../package.json"), "utf-8"));
|
|
4804
|
+
version = pkg.version;
|
|
4805
|
+
} catch {
|
|
4806
|
+
}
|
|
4807
|
+
var cli = new Command47();
|
|
4808
|
+
cli.name("swiftpatch").description(chalk45.bold("SwiftPatch CLI") + " - Ship React Native updates instantly").version(version, "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command").addHelpText("after", `
|
|
4809
|
+
${chalk45.bold("Quick Start:")}
|
|
4810
|
+
$ swiftpatch init ${chalk45.gray("# Set up SwiftPatch in your project")}
|
|
4811
|
+
$ swiftpatch deploy -p ios ${chalk45.gray("# Bundle, upload, and release")}
|
|
4812
|
+
$ swiftpatch status ${chalk45.gray("# View app status at a glance")}
|
|
4813
|
+
|
|
4814
|
+
${chalk45.bold("Examples:")}
|
|
4815
|
+
${chalk45.gray("# Login to SwiftPatch")}
|
|
4816
|
+
$ swiftpatch login
|
|
4817
|
+
|
|
4818
|
+
${chalk45.gray("# Publish a bundle")}
|
|
4819
|
+
$ swiftpatch publish-bundle -p ios --hermes
|
|
4820
|
+
|
|
4821
|
+
${chalk45.gray("# Promote a bundle to a release")}
|
|
4822
|
+
$ swiftpatch release-bundle --hash <hash> --app-version 1.0.0 --ci-token <token>
|
|
4823
|
+
|
|
4824
|
+
${chalk45.gray("# One-step deploy (bundle + upload + release)")}
|
|
4825
|
+
$ swiftpatch deploy -p ios --ci-token <token>
|
|
4826
|
+
|
|
4827
|
+
${chalk45.gray("# List all apps")}
|
|
4828
|
+
$ swiftpatch apps list
|
|
4829
|
+
|
|
4830
|
+
${chalk45.gray("# Update rollout percentage")}
|
|
4831
|
+
$ swiftpatch releases rollout <release-id> --percent 50
|
|
4832
|
+
|
|
4833
|
+
${chalk45.gray("# Manage CI tokens")}
|
|
4834
|
+
$ swiftpatch ci-tokens create
|
|
4835
|
+
|
|
4836
|
+
${chalk45.gray("# Diagnose setup issues")}
|
|
4837
|
+
$ swiftpatch doctor
|
|
4838
|
+
|
|
4839
|
+
${chalk45.bold("Documentation:")}
|
|
4840
|
+
${chalk45.cyan("https://docs.swiftpatch.io/cli")}
|
|
4841
|
+
`);
|
|
4842
|
+
cli.addCommand(initCommand);
|
|
4843
|
+
cli.addCommand(doctorCommand);
|
|
4844
|
+
cli.addCommand(statusCommand);
|
|
4845
|
+
cli.addCommand(loginCommand);
|
|
4846
|
+
cli.addCommand(logoutCommand);
|
|
4847
|
+
cli.addCommand(whoamiCommand);
|
|
4848
|
+
cli.addCommand(appsCommands);
|
|
4849
|
+
cli.addCommand(deployCommand);
|
|
4850
|
+
cli.addCommand(releaseCommand);
|
|
4851
|
+
cli.addCommand(publishBundleCommand);
|
|
4852
|
+
cli.addCommand(releaseBundleCommand);
|
|
4853
|
+
cli.addCommand(updateReleaseCommand);
|
|
4854
|
+
cli.addCommand(generateKeyPairCommand);
|
|
4855
|
+
cli.addCommand(releasesCommands);
|
|
4856
|
+
cli.addCommand(channelsCommands);
|
|
4857
|
+
cli.addCommand(ciTokensCommands);
|
|
4858
|
+
cli.addCommand(webhooksCommands);
|
|
4859
|
+
cli.addCommand(analyticsCommand);
|
|
4860
|
+
cli.addCommand(aiCommand);
|
|
4861
|
+
cli.addCommand(configCommands);
|
|
4862
|
+
cli.action(() => {
|
|
4863
|
+
console.log("");
|
|
4864
|
+
console.log(chalk45.bold.cyan(" SwiftPatch CLI"));
|
|
4865
|
+
console.log(chalk45.gray(` v${version}`));
|
|
4866
|
+
console.log("");
|
|
4867
|
+
console.log(" Run " + chalk45.cyan("swiftpatch --help") + " for usage information");
|
|
4868
|
+
console.log("");
|
|
4869
|
+
});
|
|
4870
|
+
|
|
4871
|
+
// src/utils/update-checker.ts
|
|
4872
|
+
init_esm_shims();
|
|
4873
|
+
function checkForUpdates() {
|
|
4874
|
+
try {
|
|
4875
|
+
import("update-notifier").then(({ default: updateNotifier }) => {
|
|
4876
|
+
import("fs-extra").then(({ default: fs15 }) => {
|
|
4877
|
+
import("url").then(({ fileURLToPath: fileURLToPath3 }) => {
|
|
4878
|
+
import("path").then(({ dirname: dirname3, join: join3 }) => {
|
|
4879
|
+
try {
|
|
4880
|
+
const __dirname3 = dirname3(fileURLToPath3(import.meta.url));
|
|
4881
|
+
const pkg = JSON.parse(
|
|
4882
|
+
fs15.readFileSync(join3(__dirname3, "../../package.json"), "utf-8")
|
|
4883
|
+
);
|
|
4884
|
+
const notifier = updateNotifier({ pkg, updateCheckInterval: 1e3 * 60 * 60 * 24 });
|
|
4885
|
+
notifier.notify();
|
|
4886
|
+
} catch {
|
|
4887
|
+
}
|
|
4888
|
+
});
|
|
4889
|
+
});
|
|
4890
|
+
});
|
|
4891
|
+
}).catch(() => {
|
|
4892
|
+
});
|
|
4893
|
+
} catch {
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
// src/utils/errors.ts
|
|
4898
|
+
init_esm_shims();
|
|
4899
|
+
import chalk46 from "chalk";
|
|
4900
|
+
var CLIError = class extends Error {
|
|
4901
|
+
constructor(message, code, suggestion) {
|
|
4902
|
+
super(message);
|
|
4903
|
+
this.code = code;
|
|
4904
|
+
this.suggestion = suggestion;
|
|
4905
|
+
this.name = "CLIError";
|
|
4906
|
+
}
|
|
4907
|
+
};
|
|
4908
|
+
function handleError(error) {
|
|
4909
|
+
console.log("");
|
|
4910
|
+
if (error instanceof CLIError) {
|
|
4911
|
+
logger.error(error.message);
|
|
4912
|
+
if (error.suggestion) {
|
|
4913
|
+
console.log("");
|
|
4914
|
+
console.log(chalk46.gray("Suggestion: ") + error.suggestion);
|
|
4915
|
+
}
|
|
4916
|
+
} else if (error instanceof Error) {
|
|
4917
|
+
logger.error(error.message);
|
|
4918
|
+
if (process.env.DEBUG) {
|
|
4919
|
+
console.log("");
|
|
4920
|
+
console.log(chalk46.gray(error.stack || ""));
|
|
4921
|
+
}
|
|
4922
|
+
} else {
|
|
4923
|
+
logger.error("An unexpected error occurred");
|
|
4924
|
+
}
|
|
4925
|
+
console.log("");
|
|
4926
|
+
console.log(chalk46.gray("For help, run: swiftpatch --help"));
|
|
4927
|
+
console.log(chalk46.gray("Report issues: https://github.com/codewprincee/cli/issues"));
|
|
4928
|
+
console.log("");
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
// src/index.ts
|
|
4932
|
+
async function main() {
|
|
4933
|
+
try {
|
|
4934
|
+
checkForUpdates();
|
|
4935
|
+
await cli.parseAsync(process.argv);
|
|
4936
|
+
} catch (error) {
|
|
4937
|
+
handleError(error);
|
|
4938
|
+
process.exit(1);
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
main();
|
|
4942
|
+
//# sourceMappingURL=index.js.map
|