@better-update/cli 0.6.3 → 0.7.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/dist/index.mjs CHANGED
@@ -19,6 +19,15 @@ import { cancel, isCancel, password } from "@clack/prompts";
19
19
  //#region \0rolldown/runtime.js
20
20
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
21
21
 
22
+ //#endregion
23
+ //#region package.json
24
+ var version = "0.7.0";
25
+
26
+ //#endregion
27
+ //#region ../../packages/type-guards/src/index.ts
28
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
29
+ const asRecord = (value) => isRecord(value) ? value : void 0;
30
+
22
31
  //#endregion
23
32
  //#region src/lib/exit-codes.ts
24
33
  var AuthRequiredError = class extends Data.TaggedError("AuthRequiredError") {};
@@ -57,11 +66,6 @@ const formatCause = (cause) => {
57
66
  return String(cause);
58
67
  };
59
68
 
60
- //#endregion
61
- //#region src/lib/record.ts
62
- const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
63
- const asRecord = (value) => isRecord(value) ? value : void 0;
64
-
65
69
  //#endregion
66
70
  //#region src/lib/app-json.ts
67
71
  const readAppJson = Effect.gen(function* () {
@@ -135,6 +139,14 @@ const PaginationParams = Schema.Struct({
135
139
  page: Schema.optional(Schema.NumberFromString),
136
140
  limit: Schema.optional(Schema.NumberFromString)
137
141
  });
142
+ const CursorPaginationParams = Schema.Struct({
143
+ cursor: Schema.optional(Schema.String),
144
+ limit: Schema.optional(Schema.NumberFromString)
145
+ });
146
+ const cursorPageResult = (itemSchema) => Schema.Struct({
147
+ items: Schema.Array(itemSchema),
148
+ nextCursor: Schema.NullOr(Schema.String)
149
+ });
138
150
  const UpdateRolloutBody = Schema.Struct({ percentage: Schema.Number.pipe(Schema.int(), Schema.between(1, 100)) });
139
151
  const UploadHeaders = Schema.Record({
140
152
  key: Schema.String,
@@ -634,18 +646,11 @@ var AuditLog = class extends Schema.Class("AuditLog")({
634
646
  //#region ../../packages/api/src/groups/audit-logs.ts
635
647
  var AuditLogsGroup = class extends HttpApiGroup.make("audit-logs").add(HttpApiEndpoint.get("list", "/api/audit-logs").setUrlParams(Schema.Struct({
636
648
  projectId: Schema.optional(Schema.String),
637
- action: Schema.optional(Schema.String),
638
649
  resourceType: Schema.optional(Schema.String),
639
- actorId: Schema.optional(Schema.String),
640
650
  from: Schema.optional(Schema.String),
641
651
  to: Schema.optional(Schema.String),
642
- ...PaginationParams.fields
643
- })).addSuccess(Schema.Struct({
644
- items: Schema.Array(AuditLog),
645
- total: Schema.Number,
646
- page: Schema.Number,
647
- limit: Schema.Number
648
- })).annotateContext(OpenApi.annotations({
652
+ ...CursorPaginationParams.fields
653
+ })).addSuccess(cursorPageResult(AuditLog)).annotateContext(OpenApi.annotations({
649
654
  title: "List audit logs",
650
655
  description: "List audit log entries with optional filters"
651
656
  }))).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -676,13 +681,8 @@ var BranchesGroup = class extends HttpApiGroup.make("branches").add(HttpApiEndpo
676
681
  description: "Create a new branch within a project"
677
682
  }))).add(HttpApiEndpoint.get("list", "/api/branches").setUrlParams(Schema.Struct({
678
683
  projectId: Id,
679
- ...PaginationParams.fields
680
- })).addSuccess(Schema.Struct({
681
- items: Schema.Array(Branch),
682
- total: Schema.Number,
683
- page: Schema.Number,
684
- limit: Schema.Number
685
- })).annotateContext(OpenApi.annotations({
684
+ ...CursorPaginationParams.fields
685
+ })).addSuccess(cursorPageResult(Branch)).annotateContext(OpenApi.annotations({
686
686
  title: "List branches",
687
687
  description: "List all branches for a project"
688
688
  }))).add(HttpApiEndpoint.patch("rename")`/api/branches/${idParam$8}`.setPayload(UpdateBranchBody).addSuccess(Branch).addError(Conflict).annotateContext(OpenApi.annotations({
@@ -852,15 +852,17 @@ const InstallLinkResult = Schema.Struct({
852
852
  //#region ../../packages/api/src/domain/build-compatibility.ts
853
853
  var BuildCompatibilityChannel = class extends Schema.Class("BuildCompatibilityChannel")({
854
854
  channelId: Id,
855
- channelName: Schema.String,
856
855
  updateCount: Schema.Number,
857
856
  latestUpdateId: Schema.NullOr(Id),
858
857
  latestUpdateMessage: Schema.NullOr(Schema.String),
859
- latestUpdateCreatedAt: Schema.NullOr(DateTimeString),
858
+ latestUpdateCreatedAt: Schema.NullOr(DateTimeString)
859
+ }) {};
860
+ var CompatibilityChannelInfo = class extends Schema.Class("CompatibilityChannelInfo")({
861
+ channelId: Id,
862
+ channelName: Schema.String,
860
863
  isPaused: Schema.Boolean,
861
864
  rolloutActive: Schema.Boolean
862
865
  }) {};
863
- var BuildCompatibilityRow = class extends BuildWithArtifact.extend("BuildCompatibilityRow")({ channels: Schema.Array(BuildCompatibilityChannel) }) {};
864
866
  var MissingRuntimeVersionBuild = class extends Schema.Class("MissingRuntimeVersionBuild")({
865
867
  channelId: Id,
866
868
  channelName: Schema.String,
@@ -873,7 +875,11 @@ var MissingRuntimeVersionBuild = class extends Schema.Class("MissingRuntimeVersi
873
875
  rolloutActive: Schema.Boolean
874
876
  }) {};
875
877
  const BuildCompatibilityMatrixResult = Schema.Struct({
876
- rows: Schema.Array(BuildCompatibilityRow),
878
+ channels: Schema.Array(CompatibilityChannelInfo),
879
+ channelStatusByKey: Schema.Record({
880
+ key: Schema.String,
881
+ value: Schema.Array(BuildCompatibilityChannel)
882
+ }),
877
883
  missingRuntimeVersions: Schema.Array(MissingRuntimeVersionBuild)
878
884
  });
879
885
 
@@ -891,13 +897,8 @@ var BuildsGroup = class extends HttpApiGroup.make("builds").add(HttpApiEndpoint.
891
897
  platform: Schema.optional(Platform),
892
898
  profile: Schema.optional(Schema.String),
893
899
  runtimeVersion: Schema.optional(Schema.String),
894
- ...PaginationParams.fields
895
- })).addSuccess(Schema.Struct({
896
- items: Schema.Array(BuildWithArtifact),
897
- total: Schema.Number,
898
- page: Schema.Number,
899
- limit: Schema.Number
900
- })).annotateContext(OpenApi.annotations({
900
+ ...CursorPaginationParams.fields
901
+ })).addSuccess(cursorPageResult(BuildWithArtifact)).annotateContext(OpenApi.annotations({
901
902
  title: "List builds",
902
903
  description: "List builds for a project with optional filters"
903
904
  }))).add(HttpApiEndpoint.get("compatibilityMatrix", "/api/builds/compatibility-matrix").setUrlParams(Schema.Struct({ projectId: Id })).addSuccess(BuildCompatibilityMatrixResult).annotateContext(OpenApi.annotations({
@@ -952,13 +953,8 @@ var ChannelsGroup = class extends HttpApiGroup.make("channels").add(HttpApiEndpo
952
953
  description: "Relink channel to a different branch"
953
954
  }))).add(HttpApiEndpoint.get("list", "/api/channels").setUrlParams(Schema.Struct({
954
955
  projectId: Id,
955
- ...PaginationParams.fields
956
- })).addSuccess(Schema.Struct({
957
- items: Schema.Array(Channel),
958
- total: Schema.Number,
959
- page: Schema.Number,
960
- limit: Schema.Number
961
- })).annotateContext(OpenApi.annotations({
956
+ ...CursorPaginationParams.fields
957
+ })).addSuccess(cursorPageResult(Channel)).annotateContext(OpenApi.annotations({
962
958
  title: "List channels",
963
959
  description: "List all channels for a project"
964
960
  }))).add(HttpApiEndpoint.post("pause")`/api/channels/${idParam$6}/pause`.addSuccess(Channel).annotateContext(OpenApi.annotations({
@@ -1019,10 +1015,10 @@ const UpdateDeviceBody = Schema.Struct({
1019
1015
  });
1020
1016
  const DeleteDeviceResult = Schema.Struct({ deleted: Schema.Number });
1021
1017
  const ListDevicesParams = Schema.Struct({
1022
- ...PaginationParams.fields,
1023
- search: Schema.optional(Schema.String),
1018
+ ...CursorPaginationParams.fields,
1024
1019
  deviceClass: Schema.optional(DeviceClass),
1025
- appleTeamId: Schema.optional(Id)
1020
+ appleTeamId: Schema.optional(Id),
1021
+ query: Schema.optional(Schema.String)
1026
1022
  });
1027
1023
  var DeviceRegistrationRequest = class extends Schema.Class("DeviceRegistrationRequest")({
1028
1024
  id: Id,
@@ -1053,12 +1049,7 @@ const idParam$5 = HttpApiSchema.param("id", Schema.String);
1053
1049
  var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoint.post("register", "/api/devices").setPayload(RegisterDeviceBody).addSuccess(Device, { status: 201 }).annotateContext(OpenApi.annotations({
1054
1050
  title: "Register device",
1055
1051
  description: "Register an Apple device UDID in the caller's active organization"
1056
- }))).add(HttpApiEndpoint.get("list", "/api/devices").setUrlParams(ListDevicesParams).addSuccess(Schema.Struct({
1057
- items: Schema.Array(Device),
1058
- total: Schema.Number,
1059
- page: Schema.Number,
1060
- limit: Schema.Number
1061
- })).annotateContext(OpenApi.annotations({
1052
+ }))).add(HttpApiEndpoint.get("list", "/api/devices").setUrlParams(ListDevicesParams).addSuccess(cursorPageResult(Device)).annotateContext(OpenApi.annotations({
1062
1053
  title: "List devices",
1063
1054
  description: "List registered Apple devices in the caller's active organization"
1064
1055
  }))).add(HttpApiEndpoint.get("get")`/api/devices/${idParam$5}`.addSuccess(Device).annotateContext(OpenApi.annotations({
@@ -1141,12 +1132,7 @@ var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoi
1141
1132
  projectId: Id,
1142
1133
  environment: Schema.optional(Schema.String),
1143
1134
  ...PaginationParams.fields
1144
- })).addSuccess(Schema.Struct({
1145
- items: Schema.Array(EnvVar),
1146
- total: Schema.Number,
1147
- page: Schema.Number,
1148
- limit: Schema.Number
1149
- })).annotateContext(OpenApi.annotations({
1135
+ })).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).annotateContext(OpenApi.annotations({
1150
1136
  title: "List environment variables",
1151
1137
  description: "List environment variables with optional filters"
1152
1138
  }))).add(HttpApiEndpoint.get("get")`/api/env-vars/${idParam$4}`.addSuccess(EnvVar).annotateContext(OpenApi.annotations({
@@ -1265,8 +1251,17 @@ var Project = class extends Schema.Class("Project")({
1265
1251
  name: Schema.String,
1266
1252
  slug: Schema.String,
1267
1253
  createdAt: DateTimeString,
1268
- lastActivityAt: DateTimeString
1254
+ lastActivityAt: DateTimeString,
1255
+ branchCount: Schema.Number,
1256
+ channelCount: Schema.Number,
1257
+ updateCount: Schema.Number
1269
1258
  }) {};
1259
+ const ProjectSort = Schema.Literal("lastActivityAt", "name");
1260
+ const ListProjectsParams = Schema.Struct({
1261
+ ...PaginationParams.fields,
1262
+ query: Schema.optional(Schema.String),
1263
+ sort: Schema.optional(ProjectSort)
1264
+ });
1270
1265
  const CreateProjectBody = Schema.Struct({
1271
1266
  name: Schema.String.pipe(Schema.minLength(1)),
1272
1267
  slug: Schema.String.pipe(Schema.minLength(1))
@@ -1281,7 +1276,7 @@ const slugParam = HttpApiSchema.param("slug", Schema.String);
1281
1276
  var ProjectsGroup = class extends HttpApiGroup.make("projects").add(HttpApiEndpoint.post("create", "/api/projects").setPayload(CreateProjectBody).addSuccess(Project, { status: 201 }).annotateContext(OpenApi.annotations({
1282
1277
  title: "Create project",
1283
1278
  description: "Create a new project in the caller's active organization"
1284
- }))).add(HttpApiEndpoint.get("list", "/api/projects").setUrlParams(PaginationParams).addSuccess(Schema.Struct({
1279
+ }))).add(HttpApiEndpoint.get("list", "/api/projects").setUrlParams(ListProjectsParams).addSuccess(Schema.Struct({
1285
1280
  items: Schema.Array(Project),
1286
1281
  total: Schema.Number,
1287
1282
  page: Schema.Number,
@@ -1380,13 +1375,9 @@ var UpdatesGroup = class extends HttpApiGroup.make("updates").add(HttpApiEndpoin
1380
1375
  }))).add(HttpApiEndpoint.get("list", "/api/updates").setUrlParams(Schema.Struct({
1381
1376
  projectId: Id,
1382
1377
  branchId: Schema.optional(Id),
1383
- ...PaginationParams.fields
1384
- })).addSuccess(Schema.Struct({
1385
- items: Schema.Array(Update),
1386
- total: Schema.Number,
1387
- page: Schema.Number,
1388
- limit: Schema.Number
1389
- })).annotateContext(OpenApi.annotations({
1378
+ platform: Schema.optional(Platform),
1379
+ ...CursorPaginationParams.fields
1380
+ })).addSuccess(cursorPageResult(Update)).annotateContext(OpenApi.annotations({
1390
1381
  title: "List updates",
1391
1382
  description: "List updates for a project, optionally filtered by branch"
1392
1383
  }))).add(HttpApiEndpoint.del("deleteGroup")`/api/updates/${groupIdParam}`.addSuccess(DeleteUpdateResult).annotateContext(OpenApi.annotations({
@@ -1497,7 +1488,7 @@ const AuthStoreLive = Layer.effect(AuthStore, Effect.gen(function* () {
1497
1488
  //#endregion
1498
1489
  //#region src/services/config-store.ts
1499
1490
  const DEFAULT_BASE_URL = "https://graph.better-update.dev";
1500
- const DEFAULT_ACCOUNTS_URL = "https://accounts.better-update.dev";
1491
+ const DEFAULT_WEB_URL = "https://better-update.dev";
1501
1492
  var ConfigStoreParseError = class extends Data.TaggedError("ConfigStoreParseError") {};
1502
1493
  const normalizeUrl = (value) => value.replace(/\/$/, "");
1503
1494
  var ConfigStore = class extends Context.Tag("cli/ConfigStore")() {};
@@ -1521,12 +1512,12 @@ const ConfigStoreLive = Layer.effect(ConfigStore, Effect.gen(function* () {
1521
1512
  if (typeof baseUrl === "string") return normalizeUrl(baseUrl);
1522
1513
  return DEFAULT_BASE_URL;
1523
1514
  }),
1524
- getAccountsUrl: Effect.gen(function* () {
1525
- const envUrl = yield* runtime.getEnv("BETTER_UPDATE_ACCOUNTS_URL");
1515
+ getWebUrl: Effect.gen(function* () {
1516
+ const envUrl = yield* runtime.getEnv("BETTER_UPDATE_WEB_URL");
1526
1517
  if (envUrl) return normalizeUrl(envUrl);
1527
- const accountsUrl = (yield* readConfig)?.["accountsUrl"];
1528
- if (typeof accountsUrl === "string") return normalizeUrl(accountsUrl);
1529
- return DEFAULT_ACCOUNTS_URL;
1518
+ const webUrl = (yield* readConfig)?.["webUrl"];
1519
+ if (typeof webUrl === "string") return normalizeUrl(webUrl);
1520
+ return DEFAULT_WEB_URL;
1530
1521
  })
1531
1522
  };
1532
1523
  }));
@@ -1552,11 +1543,20 @@ const ApiClientLive = Layer.effect(ApiClientService, Effect.gen(function* () {
1552
1543
 
1553
1544
  //#endregion
1554
1545
  //#region ../../packages/safe-json/src/index.ts
1555
- var SafeJsonParseError = class extends Data.TaggedError("SafeJsonParseError") {};
1556
- const safeJsonParse = (text) => Effect.runSync(Effect.orElseSucceed(Effect.try({
1557
- try: () => JSON.parse(text),
1558
- catch: () => new SafeJsonParseError({ message: "Invalid JSON" })
1559
- }), () => null));
1546
+ const parseJsonResult = (text) => {
1547
+ try {
1548
+ return {
1549
+ ok: true,
1550
+ value: JSON.parse(text)
1551
+ };
1552
+ } catch {
1553
+ return { ok: false };
1554
+ }
1555
+ };
1556
+ const safeJsonParse = (text) => {
1557
+ const result = parseJsonResult(text);
1558
+ return result.ok ? result.value : null;
1559
+ };
1560
1560
 
1561
1561
  //#endregion
1562
1562
  //#region src/services/apple-session-store.ts
@@ -1894,6 +1894,43 @@ const analyticsCommand = defineCommand({
1894
1894
  }
1895
1895
  });
1896
1896
 
1897
+ //#endregion
1898
+ //#region src/lib/cli-schemas.ts
1899
+ const RolloutPercentage = Schema.Number.pipe(Schema.int(), Schema.between(1, 100)).annotations({
1900
+ message: () => "Rollout percentage must be between 1 and 100.",
1901
+ identifier: "RolloutPercentage"
1902
+ });
1903
+ const KeyValuePair = Schema.Struct({
1904
+ key: Schema.String,
1905
+ value: Schema.String
1906
+ });
1907
+ const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
1908
+ strict: true,
1909
+ decode: (input, _options, ast) => {
1910
+ const eqIndex = input.indexOf("=");
1911
+ if (eqIndex <= 0) return ParseResult.fail(new ParseResult.Type(ast, input, "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)"));
1912
+ return ParseResult.succeed({
1913
+ key: input.slice(0, eqIndex),
1914
+ value: input.slice(eqIndex + 1)
1915
+ });
1916
+ },
1917
+ encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`)
1918
+ });
1919
+ const parseRolloutPercentage = (raw, flag) => Effect.try({
1920
+ try: () => Schema.decodeUnknownSync(RolloutPercentage)(Number(raw)),
1921
+ catch: () => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })
1922
+ });
1923
+ const parseKeyValue = (raw) => Effect.try({
1924
+ try: () => Schema.decodeUnknownSync(KeyValueFromString)(raw),
1925
+ catch: () => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })
1926
+ });
1927
+ const parseLimit = (raw, defaultValue) => {
1928
+ if (raw === void 0) return Effect.succeed(defaultValue);
1929
+ const parsed = Number(raw);
1930
+ if (!Number.isInteger(parsed) || parsed < 1) return Effect.fail(new InvalidArgumentError({ message: `--limit must be a positive integer, got "${raw}".` }));
1931
+ return Effect.succeed(parsed);
1932
+ };
1933
+
1897
1934
  //#endregion
1898
1935
  //#region src/commands/audit-logs/list.ts
1899
1936
  const listCommand$7 = defineCommand({
@@ -1902,18 +1939,10 @@ const listCommand$7 = defineCommand({
1902
1939
  description: "List audit log entries"
1903
1940
  },
1904
1941
  args: {
1905
- action: {
1906
- type: "string",
1907
- description: "Filter by action"
1908
- },
1909
1942
  "resource-type": {
1910
1943
  type: "string",
1911
1944
  description: "Filter by resource type"
1912
1945
  },
1913
- "actor-id": {
1914
- type: "string",
1915
- description: "Filter by actor ID"
1916
- },
1917
1946
  from: {
1918
1947
  type: "string",
1919
1948
  description: "ISO timestamp lower bound"
@@ -1921,20 +1950,23 @@ const listCommand$7 = defineCommand({
1921
1950
  to: {
1922
1951
  type: "string",
1923
1952
  description: "ISO timestamp upper bound"
1953
+ },
1954
+ limit: {
1955
+ type: "string",
1956
+ default: "100",
1957
+ description: "Max rows (default 100)"
1924
1958
  }
1925
1959
  },
1926
1960
  run: async ({ args }) => runEffect(Effect.gen(function* () {
1961
+ const limit = yield* parseLimit(args.limit, 100);
1927
1962
  const api = yield* apiClient;
1928
1963
  const filters = {};
1929
- if (args.action) filters["action"] = args.action;
1930
1964
  if (args["resource-type"]) filters["resourceType"] = args["resource-type"];
1931
- if (args["actor-id"]) filters["actorId"] = args["actor-id"];
1932
1965
  if (args.from) filters["from"] = args.from;
1933
1966
  if (args.to) filters["to"] = args.to;
1934
1967
  const { items } = yield* api["audit-logs"].list({ urlParams: {
1935
1968
  ...filters,
1936
- page: 1,
1937
- limit: 100
1969
+ limit
1938
1970
  } });
1939
1971
  if (items.length === 0) {
1940
1972
  yield* Console.log("No audit log entries found.");
@@ -1970,6 +2002,25 @@ const auditLogsCommand = defineCommand({
1970
2002
  subCommands: { list: listCommand$7 }
1971
2003
  });
1972
2004
 
2005
+ //#endregion
2006
+ //#region src/lib/drain-cursor.ts
2007
+ const PAGE_SIZE = 100;
2008
+ const MAX_PAGES = 100;
2009
+ /**
2010
+ * Drain a cursor-paginated list endpoint into a single array. CLI commands
2011
+ * that resolve names → IDs (e.g. branch lookup) need the full set, not a
2012
+ * page slice.
2013
+ */
2014
+ const drainCursor = (fetchPage) => {
2015
+ const loop = (accumulator, cursor, pages) => fetchPage(cursor).pipe(Effect.flatMap((page) => {
2016
+ const next = [...accumulator, ...page.items];
2017
+ const { nextCursor } = page;
2018
+ const reachedLimit = pages + 1 >= MAX_PAGES || next.length >= PAGE_SIZE * MAX_PAGES;
2019
+ return nextCursor === null || reachedLimit ? Effect.succeed(next) : loop(next, nextCursor, pages + 1);
2020
+ }));
2021
+ return loop([], void 0, 0);
2022
+ };
2023
+
1973
2024
  //#endregion
1974
2025
  //#region src/commands/branches.ts
1975
2026
  const listCommand$6 = defineCommand({
@@ -1979,11 +2030,12 @@ const listCommand$6 = defineCommand({
1979
2030
  },
1980
2031
  run: async () => runEffect(Effect.gen(function* () {
1981
2032
  const projectId = yield* readProjectId;
1982
- const { items } = yield* (yield* apiClient).branches.list({ urlParams: {
2033
+ const api = yield* apiClient;
2034
+ const items = yield* drainCursor((cursor) => api.branches.list({ urlParams: {
1983
2035
  projectId,
1984
- page: 1,
1985
- limit: 1e3
1986
- } });
2036
+ limit: 100,
2037
+ ...cursor ? { cursor } : {}
2038
+ } }));
1987
2039
  if (items.length === 0) {
1988
2040
  yield* Console.log("No branches found.");
1989
2041
  return;
@@ -2166,14 +2218,25 @@ const findAndroidArtifact = ({ projectRoot, format, flavor, buildType, minMtimeM
2166
2218
  //#endregion
2167
2219
  //#region ../../packages/encoding/src/index.ts
2168
2220
  const asUint8Array = (data) => data instanceof Uint8Array ? data : new Uint8Array(data);
2169
- const toBinaryString = (data) => [...asUint8Array(data)].map((byte) => String.fromCodePoint(byte)).join("");
2221
+ const toBinaryString = (data) => Array.from(asUint8Array(data), (byte) => String.fromCodePoint(byte)).join("");
2222
+ const padBase64 = (value) => value.padEnd(value.length + (4 - value.length % 4) % 4, "=");
2223
+ const BASE64_PATTERN = /^[A-Za-z0-9+/]*={0,2}$/u;
2224
+ const HEX_PATTERN = /^(?:[0-9A-Fa-f]{2})*$/u;
2225
+ const normalizeBase64 = (value) => {
2226
+ const compact = value.replaceAll(/\s+/gu, "");
2227
+ if (!BASE64_PATTERN.test(compact) || compact.length % 4 === 1) throw new RangeError("Invalid base64 string");
2228
+ return padBase64(compact);
2229
+ };
2170
2230
  const toBase64 = (data) => btoa(toBinaryString(data));
2171
2231
  const fromBase64 = (str) => {
2172
- const binary = atob(str);
2173
- return new Uint8Array(Array.from({ length: binary.length }, (_, idx) => binary.codePointAt(idx) ?? 0));
2232
+ const binary = atob(normalizeBase64(str));
2233
+ return Uint8Array.from(binary, (char) => char.codePointAt(0) ?? 0);
2174
2234
  };
2175
2235
  const toBase64Url = (data) => toBase64(data).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
2176
- const fromHex = (hex) => new Uint8Array((hex.match(/.{2}/g) ?? []).map((byte) => Number.parseInt(byte, 16)));
2236
+ const fromHex = (hex) => {
2237
+ if (!HEX_PATTERN.test(hex)) throw new RangeError("Invalid hex string");
2238
+ return Uint8Array.from(hex.match(/.{2}/gu) ?? [], (byte) => Number.parseInt(byte, 16));
2239
+ };
2177
2240
 
2178
2241
  //#endregion
2179
2242
  //#region src/lib/credentials-downloader.ts
@@ -2223,9 +2286,9 @@ const downloadIosCredentials = (api, options) => Effect.gen(function* () {
2223
2286
  message: "Server returned non-iOS credentials for an iOS build request",
2224
2287
  hint: bindHint
2225
2288
  }));
2226
- const p12Path = `${options.tempDir}/signing.p12`;
2289
+ const p12Path = path.join(options.tempDir, "signing.p12");
2227
2290
  const profileFilename = `${resolved.provisioningProfile.uuid ?? "profile"}.mobileprovision`;
2228
- const profilePath = `${options.tempDir}/${profileFilename}`;
2291
+ const profilePath = path.join(options.tempDir, profileFilename);
2229
2292
  yield* fs.writeFile(p12Path, fromBase64(resolved.distributionCertificate.p12Base64));
2230
2293
  yield* fs.writeFile(profilePath, fromBase64(resolved.provisioningProfile.mobileprovisionBase64));
2231
2294
  return {
@@ -2249,7 +2312,7 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
2249
2312
  message: "Server returned non-Android credentials for an Android build request",
2250
2313
  hint: androidBindHint
2251
2314
  }));
2252
- const keystorePath = `${options.tempDir}/upload.keystore`;
2315
+ const keystorePath = path.join(options.tempDir, "upload.keystore");
2253
2316
  yield* fs.writeFile(keystorePath, fromBase64(resolved.keystore.keystoreBase64));
2254
2317
  return {
2255
2318
  keystorePath,
@@ -3283,23 +3346,23 @@ const compatibilityMatrixCommand = defineCommand({
3283
3346
  run: async () => runEffect(Effect.gen(function* () {
3284
3347
  const projectId = yield* readProjectId;
3285
3348
  const result = yield* (yield* apiClient).builds.compatibilityMatrix({ urlParams: { projectId } });
3286
- if (result.rows.length === 0 && result.missingRuntimeVersions.length === 0) {
3349
+ const matrixKeys = Object.keys(result.channelStatusByKey);
3350
+ if (matrixKeys.length === 0 && result.missingRuntimeVersions.length === 0) {
3287
3351
  yield* Console.log("No compatibility data found.");
3288
3352
  return;
3289
3353
  }
3290
- if (result.rows.length > 0) {
3291
- yield* Console.log("Build-to-Channel Compatibility:");
3354
+ const channelLookup = Object.fromEntries(result.channels.map((channel) => [channel.channelId, channel.channelName]));
3355
+ if (matrixKeys.length > 0) {
3356
+ yield* Console.log("Channel Status by (Platform / Runtime Version):");
3292
3357
  yield* printTable([
3293
- "Build ID",
3294
- "Platform",
3295
- "Runtime Version",
3296
- "Channels"
3297
- ], result.rows.map((row) => [
3298
- row.id,
3299
- row.platform,
3300
- row.runtimeVersion ?? "-",
3301
- row.channels.map((channel) => channel.channelName).join(", ") || "-"
3302
- ]));
3358
+ "Platform / Runtime",
3359
+ "Channel",
3360
+ "Updates"
3361
+ ], matrixKeys.flatMap((key) => (result.channelStatusByKey[key] ?? []).filter((entry) => entry.updateCount > 0).map((entry) => [
3362
+ key,
3363
+ channelLookup[entry.channelId] ?? entry.channelId,
3364
+ String(entry.updateCount)
3365
+ ])));
3303
3366
  }
3304
3367
  if (result.missingRuntimeVersions.length > 0) {
3305
3368
  yield* Console.log("\nMissing Runtime Versions:");
@@ -3389,43 +3452,6 @@ const installLinkCommand = defineCommand({
3389
3452
  }))
3390
3453
  });
3391
3454
 
3392
- //#endregion
3393
- //#region src/lib/cli-schemas.ts
3394
- const RolloutPercentage = Schema.Number.pipe(Schema.int(), Schema.between(1, 100)).annotations({
3395
- message: () => "Rollout percentage must be between 1 and 100.",
3396
- identifier: "RolloutPercentage"
3397
- });
3398
- const KeyValuePair = Schema.Struct({
3399
- key: Schema.String,
3400
- value: Schema.String
3401
- });
3402
- const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
3403
- strict: true,
3404
- decode: (input, _options, ast) => {
3405
- const eqIndex = input.indexOf("=");
3406
- if (eqIndex <= 0) return ParseResult.fail(new ParseResult.Type(ast, input, "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)"));
3407
- return ParseResult.succeed({
3408
- key: input.slice(0, eqIndex),
3409
- value: input.slice(eqIndex + 1)
3410
- });
3411
- },
3412
- encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`)
3413
- });
3414
- const parseRolloutPercentage = (raw, flag) => Effect.try({
3415
- try: () => Schema.decodeUnknownSync(RolloutPercentage)(Number(raw)),
3416
- catch: () => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })
3417
- });
3418
- const parseKeyValue = (raw) => Effect.try({
3419
- try: () => Schema.decodeUnknownSync(KeyValueFromString)(raw),
3420
- catch: () => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })
3421
- });
3422
- const parseLimit = (raw, defaultValue) => {
3423
- if (raw === void 0) return Effect.succeed(defaultValue);
3424
- const parsed = Number(raw);
3425
- if (!Number.isInteger(parsed) || parsed < 1) return Effect.fail(new InvalidArgumentError({ message: `--limit must be a positive integer, got "${raw}".` }));
3426
- return Effect.succeed(parsed);
3427
- };
3428
-
3429
3455
  //#endregion
3430
3456
  //#region src/commands/builds/list.ts
3431
3457
  const listCommand$5 = defineCommand({
@@ -3453,7 +3479,6 @@ const listCommand$5 = defineCommand({
3453
3479
  const { items } = yield* api.builds.list({ urlParams: {
3454
3480
  projectId,
3455
3481
  ...platformFilter,
3456
- page: 1,
3457
3482
  limit
3458
3483
  } });
3459
3484
  yield* printTable([
@@ -3660,13 +3685,12 @@ const createCommand$2 = defineCommand({
3660
3685
  run: async ({ args }) => runEffect(Effect.gen(function* () {
3661
3686
  const projectId = yield* readProjectId;
3662
3687
  const api = yield* apiClient;
3663
- const { items: branches } = yield* api.branches.list({ urlParams: {
3664
- projectId,
3665
- page: 1,
3666
- limit: 1e3
3667
- } });
3668
3688
  const branchId = yield* resolveNamedResourceId$1({
3669
- items: branches,
3689
+ items: yield* drainCursor((cursor) => api.branches.list({ urlParams: {
3690
+ projectId,
3691
+ limit: 100,
3692
+ ...cursor ? { cursor } : {}
3693
+ } })),
3670
3694
  kind: "Branch",
3671
3695
  name: args.branch
3672
3696
  });
@@ -3712,15 +3736,15 @@ const listCommand$4 = defineCommand({
3712
3736
  run: async () => runEffect(Effect.gen(function* () {
3713
3737
  const projectId = yield* readProjectId;
3714
3738
  const api = yield* apiClient;
3715
- const [{ items }, { items: branches }] = yield* Effect.all([api.channels.list({ urlParams: {
3739
+ const [items, branches] = yield* Effect.all([drainCursor((cursor) => api.channels.list({ urlParams: {
3716
3740
  projectId,
3717
- page: 1,
3718
- limit: 1e3
3719
- } }), api.branches.list({ urlParams: {
3741
+ limit: 100,
3742
+ ...cursor ? { cursor } : {}
3743
+ } })), drainCursor((cursor) => api.branches.list({ urlParams: {
3720
3744
  projectId,
3721
- page: 1,
3722
- limit: 1e3
3723
- } })]);
3745
+ limit: 100,
3746
+ ...cursor ? { cursor } : {}
3747
+ } }))]);
3724
3748
  if (items.length === 0) {
3725
3749
  yield* Console.log("No channels found.");
3726
3750
  return;
@@ -3826,13 +3850,12 @@ const createCommand$1 = defineCommand({
3826
3850
  const percentage = yield* parseRolloutPercentage(args.percentage, "percentage");
3827
3851
  const projectId = yield* readProjectId;
3828
3852
  const api = yield* apiClient;
3829
- const { items: branches } = yield* api.branches.list({ urlParams: {
3830
- projectId,
3831
- page: 1,
3832
- limit: 1e3
3833
- } });
3834
3853
  const newBranchId = yield* resolveNamedResourceId$1({
3835
- items: branches,
3854
+ items: yield* drainCursor((cursor) => api.branches.list({ urlParams: {
3855
+ projectId,
3856
+ limit: 100,
3857
+ ...cursor ? { cursor } : {}
3858
+ } })),
3836
3859
  kind: "Branch",
3837
3860
  name: args.branch
3838
3861
  });
@@ -3931,13 +3954,12 @@ const updateCommand$1 = defineCommand({
3931
3954
  run: async ({ args }) => runEffect(Effect.gen(function* () {
3932
3955
  const projectId = yield* readProjectId;
3933
3956
  const api = yield* apiClient;
3934
- const { items: branches } = yield* api.branches.list({ urlParams: {
3935
- projectId,
3936
- page: 1,
3937
- limit: 1e3
3938
- } });
3939
3957
  const branchId = yield* resolveNamedResourceId$1({
3940
- items: branches,
3958
+ items: yield* drainCursor((cursor) => api.branches.list({ urlParams: {
3959
+ projectId,
3960
+ limit: 100,
3961
+ ...cursor ? { cursor } : {}
3962
+ } })),
3941
3963
  kind: "Branch",
3942
3964
  name: args.branch
3943
3965
  });
@@ -4890,9 +4912,9 @@ const openBrowser = (url) => Effect.gen(function* () {
4890
4912
  const browserLogin = Effect.scoped(Effect.gen(function* () {
4891
4913
  const configStore = yield* ConfigStore;
4892
4914
  const authStore = yield* AuthStore;
4893
- const accountsUrl = yield* configStore.getAccountsUrl;
4915
+ const webUrl = yield* configStore.getWebUrl;
4894
4916
  const loginServer = yield* Effect.acquireRelease(Effect.sync(createBrowserLoginServer), (server) => Effect.sync(server.stop));
4895
- const loginUrl = `${accountsUrl}/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
4917
+ const loginUrl = `${webUrl}/auth/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
4896
4918
  yield* Console.log("Opening browser for better-update login...");
4897
4919
  yield* Console.log("");
4898
4920
  yield* openBrowser(loginUrl);
@@ -4950,12 +4972,37 @@ const logoutCommand = defineCommand({
4950
4972
  const listCommand$1 = defineCommand({
4951
4973
  meta: {
4952
4974
  name: "list",
4953
- description: "List all projects"
4975
+ description: "List projects (most recently active first)"
4954
4976
  },
4955
- run: async () => runEffect(Effect.gen(function* () {
4956
- const { items } = yield* (yield* apiClient).projects.list({ urlParams: {
4957
- page: 1,
4958
- limit: 1e3
4977
+ args: {
4978
+ query: {
4979
+ type: "string",
4980
+ description: "Substring search on name or slug"
4981
+ },
4982
+ sort: {
4983
+ type: "string",
4984
+ description: "Sort key: lastActivityAt (default) or name",
4985
+ default: "lastActivityAt"
4986
+ },
4987
+ limit: {
4988
+ type: "string",
4989
+ description: "Page size (default 50, max 100)",
4990
+ default: "50"
4991
+ },
4992
+ page: {
4993
+ type: "string",
4994
+ description: "1-based page number",
4995
+ default: "1"
4996
+ }
4997
+ },
4998
+ run: async ({ args }) => runEffect(Effect.gen(function* () {
4999
+ const api = yield* apiClient;
5000
+ const sort = args.sort === "name" ? "name" : "lastActivityAt";
5001
+ const { items, total, page } = yield* api.projects.list({ urlParams: {
5002
+ page: Number(args.page),
5003
+ limit: Number(args.limit),
5004
+ sort,
5005
+ ...args.query ? { query: args.query } : {}
4959
5006
  } });
4960
5007
  if (items.length === 0) {
4961
5008
  yield* Console.log("No projects found.");
@@ -4965,13 +5012,14 @@ const listCommand$1 = defineCommand({
4965
5012
  "ID",
4966
5013
  "Name",
4967
5014
  "Slug",
4968
- "Created"
5015
+ "Last activity"
4969
5016
  ], items.map((project) => [
4970
5017
  project.id,
4971
5018
  project.name,
4972
5019
  project.slug,
4973
- project.createdAt
5020
+ project.lastActivityAt
4974
5021
  ]));
5022
+ yield* Console.log(`Page ${page} · ${items.length} of ${total} project(s)`);
4975
5023
  }))
4976
5024
  });
4977
5025
  const createCommand = defineCommand({
@@ -5114,7 +5162,8 @@ const statusCommand = defineCommand({
5114
5162
  yield* Console.log("");
5115
5163
  yield* Console.log("Builds");
5116
5164
  yield* Console.log("------");
5117
- yield* printKeyValue([["Total", String(builds.total)]]);
5165
+ const moreSuffix = builds.nextCursor === null ? "" : "+";
5166
+ yield* printKeyValue([["Recent", `${String(builds.items.length)}${moreSuffix}`]]);
5118
5167
  }))
5119
5168
  });
5120
5169
 
@@ -5170,11 +5219,11 @@ const listCommand = defineCommand({
5170
5219
  const limit = yield* parseLimit(args.limit, 20);
5171
5220
  const projectId = yield* readProjectId;
5172
5221
  const api = yield* apiClient;
5173
- const { items: branches } = yield* api.branches.list({ urlParams: {
5222
+ const branches = yield* drainCursor((cursor) => api.branches.list({ urlParams: {
5174
5223
  projectId,
5175
- page: 1,
5176
- limit: 1e3
5177
- } });
5224
+ limit: 100,
5225
+ ...cursor ? { cursor } : {}
5226
+ } }));
5178
5227
  const branchId = args.branch ? yield* resolveNamedResourceId({
5179
5228
  items: branches,
5180
5229
  kind: "Branch",
@@ -5183,7 +5232,6 @@ const listCommand = defineCommand({
5183
5232
  const { items } = yield* api.updates.list({ urlParams: {
5184
5233
  projectId,
5185
5234
  ...branchId === void 0 ? {} : { branchId },
5186
- page: 1,
5187
5235
  limit
5188
5236
  } });
5189
5237
  if (items.length === 0) {
@@ -5963,7 +6011,7 @@ const updateCommand = defineCommand({
5963
6011
  await runMain(defineCommand({
5964
6012
  meta: {
5965
6013
  name: "better-update",
5966
- version: "0.1.0",
6014
+ version,
5967
6015
  description: "Publish OTA updates and builds for Expo apps"
5968
6016
  },
5969
6017
  subCommands: {