ubriot 0.2.1 → 0.2.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # ubriot
2
2
 
3
- Mobile release CLI. Ship iOS and Android native builds, submit to TestFlight and Google Play, and push OTA updates — all from one command.
3
+ Mobile release CLI. Ship iOS and Android native builds, submit to TestFlight and Google Play, and push OTA updates, all from one command.
4
4
 
5
5
  ## Install
6
6
 
package/dist/api.js CHANGED
@@ -7,13 +7,50 @@ exports.makeClient = makeClient;
7
7
  exports.handleError = handleError;
8
8
  const axios_1 = __importDefault(require("axios"));
9
9
  const config_1 = require("./config");
10
+ async function refreshAccessToken() {
11
+ const cfg = (0, config_1.readConfig)();
12
+ if (!cfg.refreshToken)
13
+ return null;
14
+ try {
15
+ const { data } = await axios_1.default.post(`${cfg.apiBase}/auth/refresh`, {
16
+ refreshToken: cfg.refreshToken,
17
+ });
18
+ const token = data?.user?.token;
19
+ if (!token)
20
+ return null;
21
+ (0, config_1.writeConfig)({ token, refreshToken: data?.refreshToken ?? cfg.refreshToken });
22
+ return token;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
10
28
  function makeClient() {
11
29
  const cfg = (0, config_1.readConfig)();
12
- return axios_1.default.create({
30
+ const client = axios_1.default.create({
13
31
  baseURL: cfg.apiBase,
14
32
  headers: cfg.token ? { Authorization: `Bearer ${cfg.token}` } : {},
15
- timeout: 30000,
33
+ timeout: 120000,
34
+ });
35
+ // Expired access token: refresh once and retry the original request.
36
+ client.interceptors.response.use((res) => res, async (error) => {
37
+ const original = error.config;
38
+ const status = error.response?.status;
39
+ const isAuthCall = typeof original?.url === "string" && original.url.includes("/auth/");
40
+ if (status !== 401 || !original || original._retry || isAuthCall) {
41
+ return Promise.reject(error);
42
+ }
43
+ original._retry = true;
44
+ const newToken = await refreshAccessToken();
45
+ if (!newToken) {
46
+ console.error("Session expired. Run: ubriot login");
47
+ process.exit(1);
48
+ }
49
+ original.headers = original.headers ?? {};
50
+ original.headers.Authorization = `Bearer ${newToken}`;
51
+ return client(original);
16
52
  });
53
+ return client;
17
54
  }
18
55
  function handleError(err) {
19
56
  if (axios_1.default.isAxiosError(err)) {
package/dist/config.js CHANGED
@@ -11,6 +11,7 @@ const CONFIG_FILE = (0, path_1.join)(CONFIG_DIR, "config.json");
11
11
  const defaults = {
12
12
  apiBase: "https://api.ubriot.dev/api/v1",
13
13
  token: null,
14
+ refreshToken: null,
14
15
  email: null,
15
16
  };
16
17
  function readConfig() {
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ const child_process_1 = require("child_process");
42
42
  const fs_1 = require("fs");
43
43
  const path_1 = require("path");
44
44
  const crypto_1 = require("crypto");
45
- commander_1.program.name("ubriot").description("Ubriot — self-hosted Expo release infrastructure").version("0.1.0");
45
+ commander_1.program.name("ubriot").description("Ubriot, self-hosted Expo release infrastructure").version("0.1.0");
46
46
  const MIME = {
47
47
  ".avif": "image/avif",
48
48
  ".bundle": "application/javascript",
@@ -89,6 +89,37 @@ function readExpoConfigFromPath(configPath) {
89
89
  const configJson = readJsonFile(configPath);
90
90
  return configJson.expo ?? configJson;
91
91
  }
92
+ /**
93
+ * Derive the Apple capabilities an app needs straight from its Expo config, so
94
+ * users don't have to remember flags like `--ios-capability PUSH_NOTIFICATIONS`.
95
+ * Reads plugins + ios.entitlements + ios.infoPlist for clear, unambiguous signals.
96
+ */
97
+ function detectIosCapabilities(config) {
98
+ const caps = new Set();
99
+ const plugins = Array.isArray(config?.plugins) ? config.plugins : [];
100
+ const pluginNames = plugins.map((p) => (Array.isArray(p) ? p[0] : p)).filter((p) => typeof p === "string");
101
+ const ent = config?.ios?.entitlements ?? {};
102
+ // Push notifications: expo-notifications plugin or an aps-environment entitlement.
103
+ if (pluginNames.some((p) => p.includes("expo-notifications")) || ent["aps-environment"]) {
104
+ caps.add("PUSH_NOTIFICATIONS");
105
+ }
106
+ // Sign in with Apple.
107
+ if (ent["com.apple.developer.applesignin"])
108
+ caps.add("APPLE_ID_AUTH");
109
+ // Associated Domains (universal links).
110
+ if (config?.ios?.associatedDomains?.length || ent["com.apple.developer.associated-domains"]) {
111
+ caps.add("ASSOCIATED_DOMAINS");
112
+ }
113
+ // HealthKit.
114
+ if (pluginNames.some((p) => p.includes("health")) || ent["com.apple.developer.healthkit"]) {
115
+ caps.add("HEALTHKIT");
116
+ }
117
+ // iCloud.
118
+ if (ent["com.apple.developer.icloud-services"] || ent["com.apple.developer.icloud-container-identifiers"]) {
119
+ caps.add("ICLOUD");
120
+ }
121
+ return [...caps];
122
+ }
92
123
  function hasExpoIdentity(config) {
93
124
  return Boolean(config?.slug ||
94
125
  config?.name ||
@@ -564,7 +595,7 @@ commander_1.program
564
595
  const token = data?.user?.token;
565
596
  if (!token)
566
597
  throw new Error("No token in response");
567
- (0, config_1.writeConfig)({ token, email });
598
+ (0, config_1.writeConfig)({ token, refreshToken: data?.refreshToken ?? null, email });
568
599
  console.log(`Logged in as ${email}`);
569
600
  }
570
601
  catch (err) {
@@ -574,8 +605,18 @@ commander_1.program
574
605
  commander_1.program
575
606
  .command("logout")
576
607
  .description("Clear saved credentials")
577
- .action(() => {
578
- (0, config_1.writeConfig)({ token: null, email: null });
608
+ .action(async () => {
609
+ const cfg = (0, config_1.readConfig)();
610
+ if (cfg.refreshToken) {
611
+ // Best-effort server-side revoke; ignore failures.
612
+ try {
613
+ await (0, api_1.makeClient)().post("/auth/logout", { refreshToken: cfg.refreshToken });
614
+ }
615
+ catch {
616
+ // ignore
617
+ }
618
+ }
619
+ (0, config_1.writeConfig)({ token: null, refreshToken: null, email: null });
579
620
  console.log("Logged out.");
580
621
  });
581
622
  commander_1.program
@@ -680,7 +721,7 @@ apps
680
721
  if (opts.noAutoSubmitGooglePlay)
681
722
  body.autoSubmitGooglePlay = false;
682
723
  if (!Object.keys(body).length) {
683
- console.log("Nothing to update — pass --ios-bundle, --android-id, --asc-app-id, --google-play-track, and/or auto-submit flags");
724
+ console.log("Nothing to update, pass --ios-bundle, --android-id, --asc-app-id, --google-play-track, and/or auto-submit flags");
684
725
  return;
685
726
  }
686
727
  const { data } = await (0, api_1.makeClient)().patch(`/ubriot/apps/${opts.app}`, body);
@@ -738,12 +779,30 @@ commander_1.program
738
779
  }
739
780
  // EAS-style auto-provisioning: ensure a provisioning profile exists for iOS.
740
781
  if (normalizedPlatform === "ios") {
741
- const provisionExplicit = Boolean(opts.reprovisionIos || opts.iosCapability?.length);
782
+ // Auto-detect capabilities from the local Expo config so users don't have
783
+ // to pass --ios-capability by hand. Explicit flags still win / extend.
784
+ let capabilities = opts.iosCapability ?? [];
785
+ try {
786
+ const cfgPath = opts.expoConfig
787
+ ? (0, path_1.resolve)(opts.expoConfig)
788
+ : opts.expoProjectRoot
789
+ ? (0, path_1.join)((0, path_1.resolve)(opts.expoProjectRoot), "app.json")
790
+ : "app.json";
791
+ const localCfg = (0, fs_1.existsSync)((0, path_1.resolve)(cfgPath)) ? readExpoConfigFromPath((0, path_1.resolve)(cfgPath)) : readExpoConfig();
792
+ const detected = detectIosCapabilities(localCfg);
793
+ const added = detected.filter((c) => !capabilities.includes(c));
794
+ if (added.length) {
795
+ capabilities = [...capabilities, ...added];
796
+ console.log(`Auto-detected iOS capabilities from app config: ${added.join(", ")}`);
797
+ }
798
+ }
799
+ catch { /* detection is best-effort; fall back to explicit flags */ }
800
+ const provisionExplicit = Boolean(opts.reprovisionIos || capabilities.length);
742
801
  const provisionBody = {};
743
802
  if (opts.reprovisionIos)
744
803
  provisionBody.force = true;
745
- if (opts.iosCapability?.length)
746
- provisionBody.capabilities = opts.iosCapability;
804
+ if (capabilities.length)
805
+ provisionBody.capabilities = capabilities;
747
806
  try {
748
807
  const { data: prov } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/provision-ios`, provisionBody);
749
808
  if (prov?.status === "provisioned") {
@@ -754,7 +813,7 @@ commander_1.program
754
813
  catch (provErr) {
755
814
  const msg = provErr?.response?.data?.message ?? provErr?.message ?? "";
756
815
  // When reprovisioning or capabilities were explicitly requested, a provisioning
757
- // failure means the build is doomed to fail signing — stop here instead.
816
+ // failure means the build is doomed to fail signing, stop here instead.
758
817
  if (provisionExplicit)
759
818
  throw new Error(`iOS provisioning failed: ${msg}`);
760
819
  console.log(`⚠ Auto-provisioning skipped: ${msg}`);
@@ -766,7 +825,7 @@ commander_1.program
766
825
  repoUrl,
767
826
  branch,
768
827
  });
769
- console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}) — status: ${data.status}`);
828
+ console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
770
829
  const finished = await waitForBuild(client, data.id, pollIntervalMs, timeoutMs);
771
830
  if (opts.submit === false)
772
831
  return;
@@ -823,7 +882,7 @@ builds
823
882
  repoUrl: opts.repo,
824
883
  branch: opts.branch,
825
884
  });
826
- console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}) — status: ${data.status}`);
885
+ console.log(`Queued build #${data.id} (${data.platform} / ${data.profile}), status: ${data.status}`);
827
886
  if (!shouldWait) {
828
887
  if (data.platform === "ios") {
829
888
  console.log(`To submit after it succeeds: ubriot builds submit-testflight ${data.id}`);
@@ -950,8 +1009,8 @@ builds
950
1009
  if (data.playSubmissionStatus === "failed") {
951
1010
  throw new Error(`Google Play submit for build #${buildId} failed${data.playSubmissionError ? `: ${data.playSubmissionError}` : ""}`);
952
1011
  }
953
- const asc = data.ascSubmissionStatus === "succeeded" ? " — TestFlight submission succeeded" : "";
954
- const play = data.playSubmissionStatus === "succeeded" ? ` — Google Play submission succeeded${data.playTrack ? ` (${data.playTrack})` : ""}` : "";
1012
+ const asc = data.ascSubmissionStatus === "succeeded" ? ", TestFlight submission succeeded" : "";
1013
+ const play = data.playSubmissionStatus === "succeeded" ? `, Google Play submission succeeded${data.playTrack ? ` (${data.playTrack})` : ""}` : "";
955
1014
  console.log(`Build #${buildId} succeeded${asc}${play}.`);
956
1015
  return;
957
1016
  }
@@ -996,7 +1055,7 @@ builds
996
1055
  (0, config_1.requireAuth)();
997
1056
  try {
998
1057
  const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-testflight`);
999
- console.log(`Build #${buildId} queued for TestFlight submit — ascSubmissionStatus: ${data.ascSubmissionStatus}`);
1058
+ console.log(`Build #${buildId} queued for TestFlight submit, ascSubmissionStatus: ${data.ascSubmissionStatus}`);
1000
1059
  }
1001
1060
  catch (err) {
1002
1061
  (0, api_1.handleError)(err);
@@ -1023,7 +1082,7 @@ builds
1023
1082
  (0, config_1.requireAuth)();
1024
1083
  try {
1025
1084
  const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-google-play`, { track: opts.track });
1026
- console.log(`Build #${buildId} queued for Google Play submit — playSubmissionStatus: ${data.playSubmissionStatus}`);
1085
+ console.log(`Build #${buildId} queued for Google Play submit, playSubmissionStatus: ${data.playSubmissionStatus}`);
1027
1086
  }
1028
1087
  catch (err) {
1029
1088
  (0, api_1.handleError)(err);
@@ -1109,7 +1168,7 @@ creds
1109
1168
  payload: { serviceAccountJsonBase64 },
1110
1169
  });
1111
1170
  console.log(`Google Play service account credential created: #${data.id} "${data.name}"`);
1112
- console.log(`Ready to ship — run: ubriot ship android`);
1171
+ console.log(`Ready to ship, run: ubriot ship android`);
1113
1172
  }
1114
1173
  catch (err) {
1115
1174
  (0, api_1.handleError)(err);
@@ -1192,6 +1251,27 @@ creds
1192
1251
  (0, api_1.handleError)(err);
1193
1252
  }
1194
1253
  });
1254
+ creds
1255
+ .command("copy")
1256
+ .description("Copy a credential to another app in the same team (reuses team-wide keys like APNs/ASC without re-uploading the file)")
1257
+ .requiredOption("--from <credId>", "Source credential id (see `credentials list`)")
1258
+ .requiredOption("--to-app <appIdOrSlug>", "Destination app id or slug")
1259
+ .option("--name <name>", "Name for the copied credential")
1260
+ .option("--bundle-id <id>", "Override bundle id (APNs topic); defaults to the destination app's bundle id")
1261
+ .action(async (opts) => {
1262
+ (0, config_1.requireAuth)();
1263
+ try {
1264
+ const { data } = await (0, api_1.makeClient)().post(`/ubriot/credentials/${opts.from}/copy`, {
1265
+ toApp: opts.toApp,
1266
+ name: opts.name,
1267
+ bundleId: opts.bundleId,
1268
+ });
1269
+ console.log(`Copied credential → #${data.id} "${data.name}" (${data.kind}) on app ${data.toAppId}`);
1270
+ }
1271
+ catch (err) {
1272
+ (0, api_1.handleError)(err);
1273
+ }
1274
+ });
1195
1275
  creds
1196
1276
  .command("upload-ios-cert")
1197
1277
  .description("Upload an iOS distribution certificate (.p12)")
@@ -1296,7 +1376,7 @@ creds
1296
1376
  },
1297
1377
  });
1298
1378
  console.log(`Apple ID credential created: #${data.id} "${data.name}"`);
1299
- console.log(`Ready to ship — run: ubriot ship ios`);
1379
+ console.log(`Ready to ship, run: ubriot ship ios`);
1300
1380
  }
1301
1381
  catch (err) {
1302
1382
  (0, api_1.handleError)(err);
@@ -1304,7 +1384,7 @@ creds
1304
1384
  });
1305
1385
  creds
1306
1386
  .command("setup-testflight")
1307
- .description("Interactive guided setup for TestFlight submission (like EAS — walks you through it)")
1387
+ .description("Interactive guided setup for TestFlight submission (like EAS, walks you through it)")
1308
1388
  .requiredOption("--app <appId>", "App ID (run 'ubriot apps list' if unsure)")
1309
1389
  .action(async (opts) => {
1310
1390
  (0, config_1.requireAuth)();
@@ -1314,8 +1394,8 @@ creds
1314
1394
  console.log(`\n🚀 Setting up TestFlight submission for: ${app.name}`);
1315
1395
  console.log(` Bundle ID: ${app.iosBundleIdentifier || "(not set)"}\n`);
1316
1396
  console.log("We need your Apple credentials to submit to TestFlight.");
1317
- console.log("You only do this ONCE — the same credentials work for all your apps.");
1318
- console.log("Apple requires a special one-time password for scripts — your regular");
1397
+ console.log("You only do this ONCE, the same credentials work for all your apps.");
1398
+ console.log("Apple requires a special one-time password for scripts, your regular");
1319
1399
  console.log("Apple ID password won't work here (Apple blocks it for security).\n");
1320
1400
  // 2. Get Apple ID
1321
1401
  const appleId = await (0, readline_1.readlineSync)("Enter your Apple developer email: ");
@@ -1336,7 +1416,7 @@ Next, generate an app-specific password:
1336
1416
  const appSpecificPassword = await (0, readline_1.readlineSync)("Paste the app-specific password here: ", true);
1337
1417
  if (!appSpecificPassword.match(/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/)) {
1338
1418
  console.log("\n⚠ That doesn't look like the expected format (xxxx-xxxx-xxxx-xxxx).");
1339
- console.log(" Saving anyway — if the format was wrong the submit step will tell you.\n");
1419
+ console.log(" Saving anyway, if the format was wrong the submit step will tell you.\n");
1340
1420
  }
1341
1421
  // 5. Save credential
1342
1422
  const { data: cred } = await (0, api_1.makeClient)().post(`/ubriot/apps/${opts.app}/credentials`, {
@@ -1346,7 +1426,7 @@ Next, generate an app-specific password:
1346
1426
  payload: { appleId, appSpecificPassword },
1347
1427
  });
1348
1428
  console.log(`\n✅ Apple credentials saved (#${cred.id}).`);
1349
- console.log(` These will be used automatically for all your apps — no setup needed for future apps.`);
1429
+ console.log(` These will be used automatically for all your apps, no setup needed for future apps.`);
1350
1430
  console.log(`\nTo build and submit to TestFlight, run:`);
1351
1431
  console.log(` ubriot ship ios\n`);
1352
1432
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ubriot",
3
- "version": "0.2.1",
4
- "description": "CI/CD CLI for Expo and React Native — ship iOS, Android, and OTA updates from your terminal.",
3
+ "version": "0.2.2",
4
+ "description": "CI/CD CLI for Expo and React Native, ship iOS, Android, and OTA updates from your terminal.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://ubriot.dev",
7
7
  "repository": {