ubriot 0.2.0 → 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
- CI/CD CLI for Expo and React Native. 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
 
@@ -29,6 +29,7 @@ ubriot update # publish an OTA update
29
29
  | `ubriot push setup --app <name>` | Set up native push notifications |
30
30
  | `ubriot apps list` | List your release apps |
31
31
  | `ubriot builds list --app <name>` | List recent builds |
32
+ | `ubriot builds watch <buildId>` | Watch a build live: streaming logs + status until it finishes |
32
33
 
33
34
  ## Documentation
34
35
 
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
@@ -1,5 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
3
36
  Object.defineProperty(exports, "__esModule", { value: true });
4
37
  const commander_1 = require("commander");
5
38
  const readline_1 = require("./readline");
@@ -9,7 +42,7 @@ const child_process_1 = require("child_process");
9
42
  const fs_1 = require("fs");
10
43
  const path_1 = require("path");
11
44
  const crypto_1 = require("crypto");
12
- 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");
13
46
  const MIME = {
14
47
  ".avif": "image/avif",
15
48
  ".bundle": "application/javascript",
@@ -56,6 +89,37 @@ function readExpoConfigFromPath(configPath) {
56
89
  const configJson = readJsonFile(configPath);
57
90
  return configJson.expo ?? configJson;
58
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
+ }
59
123
  function hasExpoIdentity(config) {
60
124
  return Boolean(config?.slug ||
61
125
  config?.name ||
@@ -531,7 +595,7 @@ commander_1.program
531
595
  const token = data?.user?.token;
532
596
  if (!token)
533
597
  throw new Error("No token in response");
534
- (0, config_1.writeConfig)({ token, email });
598
+ (0, config_1.writeConfig)({ token, refreshToken: data?.refreshToken ?? null, email });
535
599
  console.log(`Logged in as ${email}`);
536
600
  }
537
601
  catch (err) {
@@ -541,8 +605,18 @@ commander_1.program
541
605
  commander_1.program
542
606
  .command("logout")
543
607
  .description("Clear saved credentials")
544
- .action(() => {
545
- (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 });
546
620
  console.log("Logged out.");
547
621
  });
548
622
  commander_1.program
@@ -567,6 +641,7 @@ apps
567
641
  .option("--android-id <id>", "Android application ID")
568
642
  .option("--ios-bundle <id>", "iOS bundle identifier")
569
643
  .option("--google-play-track <track>", "Default Google Play track", "internal")
644
+ .option("--expo-project-root <path>", "Path to the Expo app inside the repo (monorepos), e.g. products/myapp/mobile")
570
645
  .action(async (opts) => {
571
646
  (0, config_1.requireAuth)();
572
647
  try {
@@ -578,6 +653,7 @@ apps
578
653
  androidApplicationId: opts.androidId,
579
654
  iosBundleIdentifier: opts.iosBundle,
580
655
  googlePlayTrack: opts.googlePlayTrack,
656
+ expoProjectRoot: opts.expoProjectRoot,
581
657
  });
582
658
  console.log(`Created app: ${data.name} (id: ${data.id}, slug: ${data.slug})`);
583
659
  }
@@ -611,6 +687,7 @@ apps
611
687
  .description("Update release app settings (TestFlight mapping, auto-submit)")
612
688
  .requiredOption("--app <appId>", "Release app id")
613
689
  .option("--ios-bundle <id>", "iOS bundle identifier")
690
+ .option("--expo-project-root <path>", "Path to the Expo app inside the repo (monorepos)")
614
691
  .option("--android-id <id>", "Android application ID / package name")
615
692
  .option("--asc-app-id <id>", "App Store Connect app id (from ASC app URL)")
616
693
  .option("--clear-asc-app-id", "Remove stored ASC app id (use bundle id lookup)", false)
@@ -625,6 +702,8 @@ apps
625
702
  const body = {};
626
703
  if (opts.iosBundle !== undefined)
627
704
  body.iosBundleIdentifier = opts.iosBundle || null;
705
+ if (opts.expoProjectRoot !== undefined)
706
+ body.expoProjectRoot = opts.expoProjectRoot;
628
707
  if (opts.androidId !== undefined)
629
708
  body.androidApplicationId = opts.androidId || null;
630
709
  if (opts.clearAscAppId)
@@ -642,7 +721,7 @@ apps
642
721
  if (opts.noAutoSubmitGooglePlay)
643
722
  body.autoSubmitGooglePlay = false;
644
723
  if (!Object.keys(body).length) {
645
- 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");
646
725
  return;
647
726
  }
648
727
  const { data } = await (0, api_1.makeClient)().patch(`/ubriot/apps/${opts.app}`, body);
@@ -660,8 +739,11 @@ commander_1.program
660
739
  .option("--profile <profile>", "Build profile", "production")
661
740
  .option("--repo <url>", "Override repo URL")
662
741
  .option("--branch <branch>", "Branch to build")
742
+ .option("--local [path]", "Build from a local directory instead of cloning from remote (path defaults to CWD)")
663
743
  .option("--track <track>", "Google Play track for Android", "internal")
664
744
  .option("--no-submit", "Build only; do not submit to TestFlight / Google Play")
745
+ .option("--reprovision-ios", "Force-regenerate the iOS provisioning profile before building", false)
746
+ .option("--ios-capability <type...>", "Apple capability to ensure on the bundle ID (e.g. HEALTHKIT); regenerates the profile if newly added")
665
747
  .option("--poll-interval <seconds>", "Polling interval while waiting", "15")
666
748
  .option("--timeout <minutes>", "Maximum wait time for the build and store submit", "180")
667
749
  .action(async (platform, opts) => {
@@ -679,13 +761,71 @@ commander_1.program
679
761
  const client = (0, api_1.makeClient)();
680
762
  const pollIntervalMs = parsePositiveNumber(opts.pollInterval, 15, "--poll-interval") * 1000;
681
763
  const timeoutMs = parsePositiveNumber(opts.timeout, 180, "--timeout") * 60 * 1000;
764
+ // --local builds from a file:// path so the worker clones locally without needing a remote push
765
+ let repoUrl = opts.repo;
766
+ let branch = opts.branch;
767
+ if (opts.local !== undefined) {
768
+ const { resolve } = await Promise.resolve().then(() => __importStar(require("path")));
769
+ const { execSync } = await Promise.resolve().then(() => __importStar(require("child_process")));
770
+ const localPath = opts.local === true ? process.cwd() : resolve(opts.local);
771
+ repoUrl = `file://${localPath}`;
772
+ if (!branch) {
773
+ try {
774
+ branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: localPath, stdio: "pipe" }).toString().trim();
775
+ }
776
+ catch { }
777
+ }
778
+ console.log(`Building from local path: ${localPath} (branch: ${branch ?? "default"})`);
779
+ }
780
+ // EAS-style auto-provisioning: ensure a provisioning profile exists for iOS.
781
+ if (normalizedPlatform === "ios") {
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);
801
+ const provisionBody = {};
802
+ if (opts.reprovisionIos)
803
+ provisionBody.force = true;
804
+ if (capabilities.length)
805
+ provisionBody.capabilities = capabilities;
806
+ try {
807
+ const { data: prov } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/provision-ios`, provisionBody);
808
+ if (prov?.status === "provisioned") {
809
+ const caps = prov.capabilitiesAdded?.length ? ` [capabilities added: ${prov.capabilitiesAdded.join(", ")}]` : "";
810
+ console.log(`Auto-provisioned iOS signing: ${prov.profileName} (${prov.bundleId})${caps}`);
811
+ }
812
+ }
813
+ catch (provErr) {
814
+ const msg = provErr?.response?.data?.message ?? provErr?.message ?? "";
815
+ // When reprovisioning or capabilities were explicitly requested, a provisioning
816
+ // failure means the build is doomed to fail signing, stop here instead.
817
+ if (provisionExplicit)
818
+ throw new Error(`iOS provisioning failed: ${msg}`);
819
+ console.log(`⚠ Auto-provisioning skipped: ${msg}`);
820
+ }
821
+ }
682
822
  const { data } = await client.post(`/ubriot/apps/${encodeURIComponent(app)}/builds`, {
683
823
  platform: normalizedPlatform,
684
824
  profile: opts.profile,
685
- repoUrl: opts.repo,
686
- branch: opts.branch,
825
+ repoUrl,
826
+ branch,
687
827
  });
688
- 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}`);
689
829
  const finished = await waitForBuild(client, data.id, pollIntervalMs, timeoutMs);
690
830
  if (opts.submit === false)
691
831
  return;
@@ -742,7 +882,7 @@ builds
742
882
  repoUrl: opts.repo,
743
883
  branch: opts.branch,
744
884
  });
745
- 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}`);
746
886
  if (!shouldWait) {
747
887
  if (data.platform === "ios") {
748
888
  console.log(`To submit after it succeeds: ubriot builds submit-testflight ${data.id}`);
@@ -818,6 +958,70 @@ builds
818
958
  (0, api_1.handleError)(err);
819
959
  }
820
960
  });
961
+ builds
962
+ .command("watch <buildId>")
963
+ .description("Watch a build live: streams logs and status changes until the build and any store submission finish")
964
+ .option("--poll-interval <seconds>", "Polling interval while watching", "5")
965
+ .option("--timeout <minutes>", "Maximum watch time", "180")
966
+ .option("--no-logs", "Only print status changes; skip streaming build logs")
967
+ .action(async (buildId, opts) => {
968
+ (0, config_1.requireAuth)();
969
+ try {
970
+ const client = (0, api_1.makeClient)();
971
+ const intervalMs = parsePositiveNumber(opts.pollInterval, 5, "--poll-interval") * 1000;
972
+ const timeoutMs = parsePositiveNumber(opts.timeout, 180, "--timeout") * 60 * 1000;
973
+ const deadline = Date.now() + timeoutMs;
974
+ let lastStatusLine = "";
975
+ let lastLogId = 0;
976
+ while (Date.now() <= deadline) {
977
+ const { data } = await client.get(`/ubriot/builds/${buildId}`);
978
+ if (opts.logs !== false) {
979
+ // The logs endpoint has no incremental cursor, so fetch all and print only ids we haven't seen.
980
+ try {
981
+ const { data: logs } = await client.get(`/ubriot/builds/${buildId}/logs`);
982
+ for (const l of logs) {
983
+ if (l.id <= lastLogId)
984
+ continue;
985
+ lastLogId = l.id;
986
+ const message = l.message.endsWith("\n") ? l.message : `${l.message}\n`;
987
+ process.stdout.write(`[${l.phase ?? "-"}] ${message}`);
988
+ }
989
+ }
990
+ catch {
991
+ // Logs are best-effort; a transient fetch error shouldn't kill the watch.
992
+ }
993
+ }
994
+ const statusLine = buildStatusLine(data);
995
+ if (statusLine !== lastStatusLine) {
996
+ console.log(`── ${statusLine}`);
997
+ lastStatusLine = statusLine;
998
+ }
999
+ const buildDone = BUILD_TERMINAL_STATUSES.has(data.status);
1000
+ const ascPending = data.ascSubmissionStatus != null && !ASC_TERMINAL_STATUSES.has(data.ascSubmissionStatus);
1001
+ const playPending = data.playSubmissionStatus != null && !PLAY_TERMINAL_STATUSES.has(data.playSubmissionStatus);
1002
+ if (buildDone && !ascPending && !playPending) {
1003
+ if (data.status !== "succeeded") {
1004
+ throw new Error(`Build #${buildId} ${data.status}${data.errorMessage ? `: ${data.errorMessage}` : ""}`);
1005
+ }
1006
+ if (data.ascSubmissionStatus === "failed") {
1007
+ throw new Error(`TestFlight submit for build #${buildId} failed${data.ascSubmissionError ? `: ${data.ascSubmissionError}` : ""}`);
1008
+ }
1009
+ if (data.playSubmissionStatus === "failed") {
1010
+ throw new Error(`Google Play submit for build #${buildId} failed${data.playSubmissionError ? `: ${data.playSubmissionError}` : ""}`);
1011
+ }
1012
+ const asc = data.ascSubmissionStatus === "succeeded" ? ", TestFlight submission succeeded" : "";
1013
+ const play = data.playSubmissionStatus === "succeeded" ? `, Google Play submission succeeded${data.playTrack ? ` (${data.playTrack})` : ""}` : "";
1014
+ console.log(`Build #${buildId} succeeded${asc}${play}.`);
1015
+ return;
1016
+ }
1017
+ await sleep(intervalMs);
1018
+ }
1019
+ throw new Error(`Timed out watching build #${buildId} after ${opts.timeout} minutes`);
1020
+ }
1021
+ catch (err) {
1022
+ (0, api_1.handleError)(err);
1023
+ }
1024
+ });
821
1025
  builds
822
1026
  .command("cancel <buildId>")
823
1027
  .description("Cancel a queued/running build")
@@ -851,7 +1055,7 @@ builds
851
1055
  (0, config_1.requireAuth)();
852
1056
  try {
853
1057
  const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-testflight`);
854
- console.log(`Build #${buildId} queued for TestFlight submit — ascSubmissionStatus: ${data.ascSubmissionStatus}`);
1058
+ console.log(`Build #${buildId} queued for TestFlight submit, ascSubmissionStatus: ${data.ascSubmissionStatus}`);
855
1059
  }
856
1060
  catch (err) {
857
1061
  (0, api_1.handleError)(err);
@@ -878,7 +1082,7 @@ builds
878
1082
  (0, config_1.requireAuth)();
879
1083
  try {
880
1084
  const { data } = await (0, api_1.makeClient)().post(`/ubriot/builds/${buildId}/submit-google-play`, { track: opts.track });
881
- 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}`);
882
1086
  }
883
1087
  catch (err) {
884
1088
  (0, api_1.handleError)(err);
@@ -964,7 +1168,7 @@ creds
964
1168
  payload: { serviceAccountJsonBase64 },
965
1169
  });
966
1170
  console.log(`Google Play service account credential created: #${data.id} "${data.name}"`);
967
- console.log(`Ready to ship — run: ubriot ship android`);
1171
+ console.log(`Ready to ship, run: ubriot ship android`);
968
1172
  }
969
1173
  catch (err) {
970
1174
  (0, api_1.handleError)(err);
@@ -1047,6 +1251,27 @@ creds
1047
1251
  (0, api_1.handleError)(err);
1048
1252
  }
1049
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
+ });
1050
1275
  creds
1051
1276
  .command("upload-ios-cert")
1052
1277
  .description("Upload an iOS distribution certificate (.p12)")
@@ -1151,7 +1376,7 @@ creds
1151
1376
  },
1152
1377
  });
1153
1378
  console.log(`Apple ID credential created: #${data.id} "${data.name}"`);
1154
- console.log(`Ready to ship — run: ubriot ship ios`);
1379
+ console.log(`Ready to ship, run: ubriot ship ios`);
1155
1380
  }
1156
1381
  catch (err) {
1157
1382
  (0, api_1.handleError)(err);
@@ -1159,7 +1384,7 @@ creds
1159
1384
  });
1160
1385
  creds
1161
1386
  .command("setup-testflight")
1162
- .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)")
1163
1388
  .requiredOption("--app <appId>", "App ID (run 'ubriot apps list' if unsure)")
1164
1389
  .action(async (opts) => {
1165
1390
  (0, config_1.requireAuth)();
@@ -1169,8 +1394,8 @@ creds
1169
1394
  console.log(`\n🚀 Setting up TestFlight submission for: ${app.name}`);
1170
1395
  console.log(` Bundle ID: ${app.iosBundleIdentifier || "(not set)"}\n`);
1171
1396
  console.log("We need your Apple credentials to submit to TestFlight.");
1172
- console.log("You only do this ONCE — the same credentials work for all your apps.");
1173
- 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");
1174
1399
  console.log("Apple ID password won't work here (Apple blocks it for security).\n");
1175
1400
  // 2. Get Apple ID
1176
1401
  const appleId = await (0, readline_1.readlineSync)("Enter your Apple developer email: ");
@@ -1191,7 +1416,7 @@ Next, generate an app-specific password:
1191
1416
  const appSpecificPassword = await (0, readline_1.readlineSync)("Paste the app-specific password here: ", true);
1192
1417
  if (!appSpecificPassword.match(/^[a-z]{4}-[a-z]{4}-[a-z]{4}-[a-z]{4}$/)) {
1193
1418
  console.log("\n⚠ That doesn't look like the expected format (xxxx-xxxx-xxxx-xxxx).");
1194
- 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");
1195
1420
  }
1196
1421
  // 5. Save credential
1197
1422
  const { data: cred } = await (0, api_1.makeClient)().post(`/ubriot/apps/${opts.app}/credentials`, {
@@ -1201,7 +1426,7 @@ Next, generate an app-specific password:
1201
1426
  payload: { appleId, appSpecificPassword },
1202
1427
  });
1203
1428
  console.log(`\n✅ Apple credentials saved (#${cred.id}).`);
1204
- 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.`);
1205
1430
  console.log(`\nTo build and submit to TestFlight, run:`);
1206
1431
  console.log(` ubriot ship ios\n`);
1207
1432
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ubriot",
3
- "version": "0.2.0",
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": {