@better-update/cli 0.6.4 → 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
@@ -21,7 +21,12 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
21
21
 
22
22
  //#endregion
23
23
  //#region package.json
24
- var version = "0.6.4";
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;
25
30
 
26
31
  //#endregion
27
32
  //#region src/lib/exit-codes.ts
@@ -61,11 +66,6 @@ const formatCause = (cause) => {
61
66
  return String(cause);
62
67
  };
63
68
 
64
- //#endregion
65
- //#region src/lib/record.ts
66
- const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
67
- const asRecord = (value) => isRecord(value) ? value : void 0;
68
-
69
69
  //#endregion
70
70
  //#region src/lib/app-json.ts
71
71
  const readAppJson = Effect.gen(function* () {
@@ -139,6 +139,14 @@ const PaginationParams = Schema.Struct({
139
139
  page: Schema.optional(Schema.NumberFromString),
140
140
  limit: Schema.optional(Schema.NumberFromString)
141
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
+ });
142
150
  const UpdateRolloutBody = Schema.Struct({ percentage: Schema.Number.pipe(Schema.int(), Schema.between(1, 100)) });
143
151
  const UploadHeaders = Schema.Record({
144
152
  key: Schema.String,
@@ -638,18 +646,11 @@ var AuditLog = class extends Schema.Class("AuditLog")({
638
646
  //#region ../../packages/api/src/groups/audit-logs.ts
639
647
  var AuditLogsGroup = class extends HttpApiGroup.make("audit-logs").add(HttpApiEndpoint.get("list", "/api/audit-logs").setUrlParams(Schema.Struct({
640
648
  projectId: Schema.optional(Schema.String),
641
- action: Schema.optional(Schema.String),
642
649
  resourceType: Schema.optional(Schema.String),
643
- actorId: Schema.optional(Schema.String),
644
650
  from: Schema.optional(Schema.String),
645
651
  to: Schema.optional(Schema.String),
646
- ...PaginationParams.fields
647
- })).addSuccess(Schema.Struct({
648
- items: Schema.Array(AuditLog),
649
- total: Schema.Number,
650
- page: Schema.Number,
651
- limit: Schema.Number
652
- })).annotateContext(OpenApi.annotations({
652
+ ...CursorPaginationParams.fields
653
+ })).addSuccess(cursorPageResult(AuditLog)).annotateContext(OpenApi.annotations({
653
654
  title: "List audit logs",
654
655
  description: "List audit log entries with optional filters"
655
656
  }))).addError(Forbidden).annotateContext(OpenApi.annotations({
@@ -680,13 +681,8 @@ var BranchesGroup = class extends HttpApiGroup.make("branches").add(HttpApiEndpo
680
681
  description: "Create a new branch within a project"
681
682
  }))).add(HttpApiEndpoint.get("list", "/api/branches").setUrlParams(Schema.Struct({
682
683
  projectId: Id,
683
- ...PaginationParams.fields
684
- })).addSuccess(Schema.Struct({
685
- items: Schema.Array(Branch),
686
- total: Schema.Number,
687
- page: Schema.Number,
688
- limit: Schema.Number
689
- })).annotateContext(OpenApi.annotations({
684
+ ...CursorPaginationParams.fields
685
+ })).addSuccess(cursorPageResult(Branch)).annotateContext(OpenApi.annotations({
690
686
  title: "List branches",
691
687
  description: "List all branches for a project"
692
688
  }))).add(HttpApiEndpoint.patch("rename")`/api/branches/${idParam$8}`.setPayload(UpdateBranchBody).addSuccess(Branch).addError(Conflict).annotateContext(OpenApi.annotations({
@@ -856,15 +852,17 @@ const InstallLinkResult = Schema.Struct({
856
852
  //#region ../../packages/api/src/domain/build-compatibility.ts
857
853
  var BuildCompatibilityChannel = class extends Schema.Class("BuildCompatibilityChannel")({
858
854
  channelId: Id,
859
- channelName: Schema.String,
860
855
  updateCount: Schema.Number,
861
856
  latestUpdateId: Schema.NullOr(Id),
862
857
  latestUpdateMessage: Schema.NullOr(Schema.String),
863
- 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,
864
863
  isPaused: Schema.Boolean,
865
864
  rolloutActive: Schema.Boolean
866
865
  }) {};
867
- var BuildCompatibilityRow = class extends BuildWithArtifact.extend("BuildCompatibilityRow")({ channels: Schema.Array(BuildCompatibilityChannel) }) {};
868
866
  var MissingRuntimeVersionBuild = class extends Schema.Class("MissingRuntimeVersionBuild")({
869
867
  channelId: Id,
870
868
  channelName: Schema.String,
@@ -877,7 +875,11 @@ var MissingRuntimeVersionBuild = class extends Schema.Class("MissingRuntimeVersi
877
875
  rolloutActive: Schema.Boolean
878
876
  }) {};
879
877
  const BuildCompatibilityMatrixResult = Schema.Struct({
880
- rows: Schema.Array(BuildCompatibilityRow),
878
+ channels: Schema.Array(CompatibilityChannelInfo),
879
+ channelStatusByKey: Schema.Record({
880
+ key: Schema.String,
881
+ value: Schema.Array(BuildCompatibilityChannel)
882
+ }),
881
883
  missingRuntimeVersions: Schema.Array(MissingRuntimeVersionBuild)
882
884
  });
883
885
 
@@ -895,13 +897,8 @@ var BuildsGroup = class extends HttpApiGroup.make("builds").add(HttpApiEndpoint.
895
897
  platform: Schema.optional(Platform),
896
898
  profile: Schema.optional(Schema.String),
897
899
  runtimeVersion: Schema.optional(Schema.String),
898
- ...PaginationParams.fields
899
- })).addSuccess(Schema.Struct({
900
- items: Schema.Array(BuildWithArtifact),
901
- total: Schema.Number,
902
- page: Schema.Number,
903
- limit: Schema.Number
904
- })).annotateContext(OpenApi.annotations({
900
+ ...CursorPaginationParams.fields
901
+ })).addSuccess(cursorPageResult(BuildWithArtifact)).annotateContext(OpenApi.annotations({
905
902
  title: "List builds",
906
903
  description: "List builds for a project with optional filters"
907
904
  }))).add(HttpApiEndpoint.get("compatibilityMatrix", "/api/builds/compatibility-matrix").setUrlParams(Schema.Struct({ projectId: Id })).addSuccess(BuildCompatibilityMatrixResult).annotateContext(OpenApi.annotations({
@@ -956,13 +953,8 @@ var ChannelsGroup = class extends HttpApiGroup.make("channels").add(HttpApiEndpo
956
953
  description: "Relink channel to a different branch"
957
954
  }))).add(HttpApiEndpoint.get("list", "/api/channels").setUrlParams(Schema.Struct({
958
955
  projectId: Id,
959
- ...PaginationParams.fields
960
- })).addSuccess(Schema.Struct({
961
- items: Schema.Array(Channel),
962
- total: Schema.Number,
963
- page: Schema.Number,
964
- limit: Schema.Number
965
- })).annotateContext(OpenApi.annotations({
956
+ ...CursorPaginationParams.fields
957
+ })).addSuccess(cursorPageResult(Channel)).annotateContext(OpenApi.annotations({
966
958
  title: "List channels",
967
959
  description: "List all channels for a project"
968
960
  }))).add(HttpApiEndpoint.post("pause")`/api/channels/${idParam$6}/pause`.addSuccess(Channel).annotateContext(OpenApi.annotations({
@@ -1023,10 +1015,10 @@ const UpdateDeviceBody = Schema.Struct({
1023
1015
  });
1024
1016
  const DeleteDeviceResult = Schema.Struct({ deleted: Schema.Number });
1025
1017
  const ListDevicesParams = Schema.Struct({
1026
- ...PaginationParams.fields,
1027
- search: Schema.optional(Schema.String),
1018
+ ...CursorPaginationParams.fields,
1028
1019
  deviceClass: Schema.optional(DeviceClass),
1029
- appleTeamId: Schema.optional(Id)
1020
+ appleTeamId: Schema.optional(Id),
1021
+ query: Schema.optional(Schema.String)
1030
1022
  });
1031
1023
  var DeviceRegistrationRequest = class extends Schema.Class("DeviceRegistrationRequest")({
1032
1024
  id: Id,
@@ -1057,12 +1049,7 @@ const idParam$5 = HttpApiSchema.param("id", Schema.String);
1057
1049
  var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoint.post("register", "/api/devices").setPayload(RegisterDeviceBody).addSuccess(Device, { status: 201 }).annotateContext(OpenApi.annotations({
1058
1050
  title: "Register device",
1059
1051
  description: "Register an Apple device UDID in the caller's active organization"
1060
- }))).add(HttpApiEndpoint.get("list", "/api/devices").setUrlParams(ListDevicesParams).addSuccess(Schema.Struct({
1061
- items: Schema.Array(Device),
1062
- total: Schema.Number,
1063
- page: Schema.Number,
1064
- limit: Schema.Number
1065
- })).annotateContext(OpenApi.annotations({
1052
+ }))).add(HttpApiEndpoint.get("list", "/api/devices").setUrlParams(ListDevicesParams).addSuccess(cursorPageResult(Device)).annotateContext(OpenApi.annotations({
1066
1053
  title: "List devices",
1067
1054
  description: "List registered Apple devices in the caller's active organization"
1068
1055
  }))).add(HttpApiEndpoint.get("get")`/api/devices/${idParam$5}`.addSuccess(Device).annotateContext(OpenApi.annotations({
@@ -1145,12 +1132,7 @@ var EnvVarsGroup = class extends HttpApiGroup.make("env-vars").add(HttpApiEndpoi
1145
1132
  projectId: Id,
1146
1133
  environment: Schema.optional(Schema.String),
1147
1134
  ...PaginationParams.fields
1148
- })).addSuccess(Schema.Struct({
1149
- items: Schema.Array(EnvVar),
1150
- total: Schema.Number,
1151
- page: Schema.Number,
1152
- limit: Schema.Number
1153
- })).annotateContext(OpenApi.annotations({
1135
+ })).addSuccess(Schema.Struct({ items: Schema.Array(EnvVar) })).annotateContext(OpenApi.annotations({
1154
1136
  title: "List environment variables",
1155
1137
  description: "List environment variables with optional filters"
1156
1138
  }))).add(HttpApiEndpoint.get("get")`/api/env-vars/${idParam$4}`.addSuccess(EnvVar).annotateContext(OpenApi.annotations({
@@ -1269,8 +1251,17 @@ var Project = class extends Schema.Class("Project")({
1269
1251
  name: Schema.String,
1270
1252
  slug: Schema.String,
1271
1253
  createdAt: DateTimeString,
1272
- lastActivityAt: DateTimeString
1254
+ lastActivityAt: DateTimeString,
1255
+ branchCount: Schema.Number,
1256
+ channelCount: Schema.Number,
1257
+ updateCount: Schema.Number
1273
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
+ });
1274
1265
  const CreateProjectBody = Schema.Struct({
1275
1266
  name: Schema.String.pipe(Schema.minLength(1)),
1276
1267
  slug: Schema.String.pipe(Schema.minLength(1))
@@ -1285,7 +1276,7 @@ const slugParam = HttpApiSchema.param("slug", Schema.String);
1285
1276
  var ProjectsGroup = class extends HttpApiGroup.make("projects").add(HttpApiEndpoint.post("create", "/api/projects").setPayload(CreateProjectBody).addSuccess(Project, { status: 201 }).annotateContext(OpenApi.annotations({
1286
1277
  title: "Create project",
1287
1278
  description: "Create a new project in the caller's active organization"
1288
- }))).add(HttpApiEndpoint.get("list", "/api/projects").setUrlParams(PaginationParams).addSuccess(Schema.Struct({
1279
+ }))).add(HttpApiEndpoint.get("list", "/api/projects").setUrlParams(ListProjectsParams).addSuccess(Schema.Struct({
1289
1280
  items: Schema.Array(Project),
1290
1281
  total: Schema.Number,
1291
1282
  page: Schema.Number,
@@ -1384,13 +1375,9 @@ var UpdatesGroup = class extends HttpApiGroup.make("updates").add(HttpApiEndpoin
1384
1375
  }))).add(HttpApiEndpoint.get("list", "/api/updates").setUrlParams(Schema.Struct({
1385
1376
  projectId: Id,
1386
1377
  branchId: Schema.optional(Id),
1387
- ...PaginationParams.fields
1388
- })).addSuccess(Schema.Struct({
1389
- items: Schema.Array(Update),
1390
- total: Schema.Number,
1391
- page: Schema.Number,
1392
- limit: Schema.Number
1393
- })).annotateContext(OpenApi.annotations({
1378
+ platform: Schema.optional(Platform),
1379
+ ...CursorPaginationParams.fields
1380
+ })).addSuccess(cursorPageResult(Update)).annotateContext(OpenApi.annotations({
1394
1381
  title: "List updates",
1395
1382
  description: "List updates for a project, optionally filtered by branch"
1396
1383
  }))).add(HttpApiEndpoint.del("deleteGroup")`/api/updates/${groupIdParam}`.addSuccess(DeleteUpdateResult).annotateContext(OpenApi.annotations({
@@ -1501,7 +1488,7 @@ const AuthStoreLive = Layer.effect(AuthStore, Effect.gen(function* () {
1501
1488
  //#endregion
1502
1489
  //#region src/services/config-store.ts
1503
1490
  const DEFAULT_BASE_URL = "https://graph.better-update.dev";
1504
- const DEFAULT_ACCOUNTS_URL = "https://accounts.better-update.dev";
1491
+ const DEFAULT_WEB_URL = "https://better-update.dev";
1505
1492
  var ConfigStoreParseError = class extends Data.TaggedError("ConfigStoreParseError") {};
1506
1493
  const normalizeUrl = (value) => value.replace(/\/$/, "");
1507
1494
  var ConfigStore = class extends Context.Tag("cli/ConfigStore")() {};
@@ -1525,12 +1512,12 @@ const ConfigStoreLive = Layer.effect(ConfigStore, Effect.gen(function* () {
1525
1512
  if (typeof baseUrl === "string") return normalizeUrl(baseUrl);
1526
1513
  return DEFAULT_BASE_URL;
1527
1514
  }),
1528
- getAccountsUrl: Effect.gen(function* () {
1529
- const envUrl = yield* runtime.getEnv("BETTER_UPDATE_ACCOUNTS_URL");
1515
+ getWebUrl: Effect.gen(function* () {
1516
+ const envUrl = yield* runtime.getEnv("BETTER_UPDATE_WEB_URL");
1530
1517
  if (envUrl) return normalizeUrl(envUrl);
1531
- const accountsUrl = (yield* readConfig)?.["accountsUrl"];
1532
- if (typeof accountsUrl === "string") return normalizeUrl(accountsUrl);
1533
- return DEFAULT_ACCOUNTS_URL;
1518
+ const webUrl = (yield* readConfig)?.["webUrl"];
1519
+ if (typeof webUrl === "string") return normalizeUrl(webUrl);
1520
+ return DEFAULT_WEB_URL;
1534
1521
  })
1535
1522
  };
1536
1523
  }));
@@ -1556,11 +1543,20 @@ const ApiClientLive = Layer.effect(ApiClientService, Effect.gen(function* () {
1556
1543
 
1557
1544
  //#endregion
1558
1545
  //#region ../../packages/safe-json/src/index.ts
1559
- var SafeJsonParseError = class extends Data.TaggedError("SafeJsonParseError") {};
1560
- const safeJsonParse = (text) => Effect.runSync(Effect.orElseSucceed(Effect.try({
1561
- try: () => JSON.parse(text),
1562
- catch: () => new SafeJsonParseError({ message: "Invalid JSON" })
1563
- }), () => 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
+ };
1564
1560
 
1565
1561
  //#endregion
1566
1562
  //#region src/services/apple-session-store.ts
@@ -1898,6 +1894,43 @@ const analyticsCommand = defineCommand({
1898
1894
  }
1899
1895
  });
1900
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
+
1901
1934
  //#endregion
1902
1935
  //#region src/commands/audit-logs/list.ts
1903
1936
  const listCommand$7 = defineCommand({
@@ -1906,18 +1939,10 @@ const listCommand$7 = defineCommand({
1906
1939
  description: "List audit log entries"
1907
1940
  },
1908
1941
  args: {
1909
- action: {
1910
- type: "string",
1911
- description: "Filter by action"
1912
- },
1913
1942
  "resource-type": {
1914
1943
  type: "string",
1915
1944
  description: "Filter by resource type"
1916
1945
  },
1917
- "actor-id": {
1918
- type: "string",
1919
- description: "Filter by actor ID"
1920
- },
1921
1946
  from: {
1922
1947
  type: "string",
1923
1948
  description: "ISO timestamp lower bound"
@@ -1925,20 +1950,23 @@ const listCommand$7 = defineCommand({
1925
1950
  to: {
1926
1951
  type: "string",
1927
1952
  description: "ISO timestamp upper bound"
1953
+ },
1954
+ limit: {
1955
+ type: "string",
1956
+ default: "100",
1957
+ description: "Max rows (default 100)"
1928
1958
  }
1929
1959
  },
1930
1960
  run: async ({ args }) => runEffect(Effect.gen(function* () {
1961
+ const limit = yield* parseLimit(args.limit, 100);
1931
1962
  const api = yield* apiClient;
1932
1963
  const filters = {};
1933
- if (args.action) filters["action"] = args.action;
1934
1964
  if (args["resource-type"]) filters["resourceType"] = args["resource-type"];
1935
- if (args["actor-id"]) filters["actorId"] = args["actor-id"];
1936
1965
  if (args.from) filters["from"] = args.from;
1937
1966
  if (args.to) filters["to"] = args.to;
1938
1967
  const { items } = yield* api["audit-logs"].list({ urlParams: {
1939
1968
  ...filters,
1940
- page: 1,
1941
- limit: 100
1969
+ limit
1942
1970
  } });
1943
1971
  if (items.length === 0) {
1944
1972
  yield* Console.log("No audit log entries found.");
@@ -1974,6 +2002,25 @@ const auditLogsCommand = defineCommand({
1974
2002
  subCommands: { list: listCommand$7 }
1975
2003
  });
1976
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
+
1977
2024
  //#endregion
1978
2025
  //#region src/commands/branches.ts
1979
2026
  const listCommand$6 = defineCommand({
@@ -1983,11 +2030,12 @@ const listCommand$6 = defineCommand({
1983
2030
  },
1984
2031
  run: async () => runEffect(Effect.gen(function* () {
1985
2032
  const projectId = yield* readProjectId;
1986
- const { items } = yield* (yield* apiClient).branches.list({ urlParams: {
2033
+ const api = yield* apiClient;
2034
+ const items = yield* drainCursor((cursor) => api.branches.list({ urlParams: {
1987
2035
  projectId,
1988
- page: 1,
1989
- limit: 1e3
1990
- } });
2036
+ limit: 100,
2037
+ ...cursor ? { cursor } : {}
2038
+ } }));
1991
2039
  if (items.length === 0) {
1992
2040
  yield* Console.log("No branches found.");
1993
2041
  return;
@@ -2170,14 +2218,25 @@ const findAndroidArtifact = ({ projectRoot, format, flavor, buildType, minMtimeM
2170
2218
  //#endregion
2171
2219
  //#region ../../packages/encoding/src/index.ts
2172
2220
  const asUint8Array = (data) => data instanceof Uint8Array ? data : new Uint8Array(data);
2173
- 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
+ };
2174
2230
  const toBase64 = (data) => btoa(toBinaryString(data));
2175
2231
  const fromBase64 = (str) => {
2176
- const binary = atob(str);
2177
- 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);
2178
2234
  };
2179
2235
  const toBase64Url = (data) => toBase64(data).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, "");
2180
- 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
+ };
2181
2240
 
2182
2241
  //#endregion
2183
2242
  //#region src/lib/credentials-downloader.ts
@@ -2227,9 +2286,9 @@ const downloadIosCredentials = (api, options) => Effect.gen(function* () {
2227
2286
  message: "Server returned non-iOS credentials for an iOS build request",
2228
2287
  hint: bindHint
2229
2288
  }));
2230
- const p12Path = `${options.tempDir}/signing.p12`;
2289
+ const p12Path = path.join(options.tempDir, "signing.p12");
2231
2290
  const profileFilename = `${resolved.provisioningProfile.uuid ?? "profile"}.mobileprovision`;
2232
- const profilePath = `${options.tempDir}/${profileFilename}`;
2291
+ const profilePath = path.join(options.tempDir, profileFilename);
2233
2292
  yield* fs.writeFile(p12Path, fromBase64(resolved.distributionCertificate.p12Base64));
2234
2293
  yield* fs.writeFile(profilePath, fromBase64(resolved.provisioningProfile.mobileprovisionBase64));
2235
2294
  return {
@@ -2253,7 +2312,7 @@ const downloadAndroidCredentials = (api, options) => Effect.gen(function* () {
2253
2312
  message: "Server returned non-Android credentials for an Android build request",
2254
2313
  hint: androidBindHint
2255
2314
  }));
2256
- const keystorePath = `${options.tempDir}/upload.keystore`;
2315
+ const keystorePath = path.join(options.tempDir, "upload.keystore");
2257
2316
  yield* fs.writeFile(keystorePath, fromBase64(resolved.keystore.keystoreBase64));
2258
2317
  return {
2259
2318
  keystorePath,
@@ -3287,23 +3346,23 @@ const compatibilityMatrixCommand = defineCommand({
3287
3346
  run: async () => runEffect(Effect.gen(function* () {
3288
3347
  const projectId = yield* readProjectId;
3289
3348
  const result = yield* (yield* apiClient).builds.compatibilityMatrix({ urlParams: { projectId } });
3290
- 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) {
3291
3351
  yield* Console.log("No compatibility data found.");
3292
3352
  return;
3293
3353
  }
3294
- if (result.rows.length > 0) {
3295
- 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):");
3296
3357
  yield* printTable([
3297
- "Build ID",
3298
- "Platform",
3299
- "Runtime Version",
3300
- "Channels"
3301
- ], result.rows.map((row) => [
3302
- row.id,
3303
- row.platform,
3304
- row.runtimeVersion ?? "-",
3305
- row.channels.map((channel) => channel.channelName).join(", ") || "-"
3306
- ]));
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
+ ])));
3307
3366
  }
3308
3367
  if (result.missingRuntimeVersions.length > 0) {
3309
3368
  yield* Console.log("\nMissing Runtime Versions:");
@@ -3393,43 +3452,6 @@ const installLinkCommand = defineCommand({
3393
3452
  }))
3394
3453
  });
3395
3454
 
3396
- //#endregion
3397
- //#region src/lib/cli-schemas.ts
3398
- const RolloutPercentage = Schema.Number.pipe(Schema.int(), Schema.between(1, 100)).annotations({
3399
- message: () => "Rollout percentage must be between 1 and 100.",
3400
- identifier: "RolloutPercentage"
3401
- });
3402
- const KeyValuePair = Schema.Struct({
3403
- key: Schema.String,
3404
- value: Schema.String
3405
- });
3406
- const KeyValueFromString = Schema.transformOrFail(Schema.String, KeyValuePair, {
3407
- strict: true,
3408
- decode: (input, _options, ast) => {
3409
- const eqIndex = input.indexOf("=");
3410
- if (eqIndex <= 0) return ParseResult.fail(new ParseResult.Type(ast, input, "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)"));
3411
- return ParseResult.succeed({
3412
- key: input.slice(0, eqIndex),
3413
- value: input.slice(eqIndex + 1)
3414
- });
3415
- },
3416
- encode: ({ key, value }) => ParseResult.succeed(`${key}=${value}`)
3417
- });
3418
- const parseRolloutPercentage = (raw, flag) => Effect.try({
3419
- try: () => Schema.decodeUnknownSync(RolloutPercentage)(Number(raw)),
3420
- catch: () => new InvalidArgumentError({ message: `--${flag} must be an integer between 1 and 100, got "${raw}".` })
3421
- });
3422
- const parseKeyValue = (raw) => Effect.try({
3423
- try: () => Schema.decodeUnknownSync(KeyValueFromString)(raw),
3424
- catch: () => new InvalidArgumentError({ message: "Invalid format. Use KEY=VALUE (e.g. API_KEY=abc123)" })
3425
- });
3426
- const parseLimit = (raw, defaultValue) => {
3427
- if (raw === void 0) return Effect.succeed(defaultValue);
3428
- const parsed = Number(raw);
3429
- if (!Number.isInteger(parsed) || parsed < 1) return Effect.fail(new InvalidArgumentError({ message: `--limit must be a positive integer, got "${raw}".` }));
3430
- return Effect.succeed(parsed);
3431
- };
3432
-
3433
3455
  //#endregion
3434
3456
  //#region src/commands/builds/list.ts
3435
3457
  const listCommand$5 = defineCommand({
@@ -3457,7 +3479,6 @@ const listCommand$5 = defineCommand({
3457
3479
  const { items } = yield* api.builds.list({ urlParams: {
3458
3480
  projectId,
3459
3481
  ...platformFilter,
3460
- page: 1,
3461
3482
  limit
3462
3483
  } });
3463
3484
  yield* printTable([
@@ -3664,13 +3685,12 @@ const createCommand$2 = defineCommand({
3664
3685
  run: async ({ args }) => runEffect(Effect.gen(function* () {
3665
3686
  const projectId = yield* readProjectId;
3666
3687
  const api = yield* apiClient;
3667
- const { items: branches } = yield* api.branches.list({ urlParams: {
3668
- projectId,
3669
- page: 1,
3670
- limit: 1e3
3671
- } });
3672
3688
  const branchId = yield* resolveNamedResourceId$1({
3673
- items: branches,
3689
+ items: yield* drainCursor((cursor) => api.branches.list({ urlParams: {
3690
+ projectId,
3691
+ limit: 100,
3692
+ ...cursor ? { cursor } : {}
3693
+ } })),
3674
3694
  kind: "Branch",
3675
3695
  name: args.branch
3676
3696
  });
@@ -3716,15 +3736,15 @@ const listCommand$4 = defineCommand({
3716
3736
  run: async () => runEffect(Effect.gen(function* () {
3717
3737
  const projectId = yield* readProjectId;
3718
3738
  const api = yield* apiClient;
3719
- const [{ items }, { items: branches }] = yield* Effect.all([api.channels.list({ urlParams: {
3739
+ const [items, branches] = yield* Effect.all([drainCursor((cursor) => api.channels.list({ urlParams: {
3720
3740
  projectId,
3721
- page: 1,
3722
- limit: 1e3
3723
- } }), api.branches.list({ urlParams: {
3741
+ limit: 100,
3742
+ ...cursor ? { cursor } : {}
3743
+ } })), drainCursor((cursor) => api.branches.list({ urlParams: {
3724
3744
  projectId,
3725
- page: 1,
3726
- limit: 1e3
3727
- } })]);
3745
+ limit: 100,
3746
+ ...cursor ? { cursor } : {}
3747
+ } }))]);
3728
3748
  if (items.length === 0) {
3729
3749
  yield* Console.log("No channels found.");
3730
3750
  return;
@@ -3830,13 +3850,12 @@ const createCommand$1 = defineCommand({
3830
3850
  const percentage = yield* parseRolloutPercentage(args.percentage, "percentage");
3831
3851
  const projectId = yield* readProjectId;
3832
3852
  const api = yield* apiClient;
3833
- const { items: branches } = yield* api.branches.list({ urlParams: {
3834
- projectId,
3835
- page: 1,
3836
- limit: 1e3
3837
- } });
3838
3853
  const newBranchId = yield* resolveNamedResourceId$1({
3839
- items: branches,
3854
+ items: yield* drainCursor((cursor) => api.branches.list({ urlParams: {
3855
+ projectId,
3856
+ limit: 100,
3857
+ ...cursor ? { cursor } : {}
3858
+ } })),
3840
3859
  kind: "Branch",
3841
3860
  name: args.branch
3842
3861
  });
@@ -3935,13 +3954,12 @@ const updateCommand$1 = defineCommand({
3935
3954
  run: async ({ args }) => runEffect(Effect.gen(function* () {
3936
3955
  const projectId = yield* readProjectId;
3937
3956
  const api = yield* apiClient;
3938
- const { items: branches } = yield* api.branches.list({ urlParams: {
3939
- projectId,
3940
- page: 1,
3941
- limit: 1e3
3942
- } });
3943
3957
  const branchId = yield* resolveNamedResourceId$1({
3944
- items: branches,
3958
+ items: yield* drainCursor((cursor) => api.branches.list({ urlParams: {
3959
+ projectId,
3960
+ limit: 100,
3961
+ ...cursor ? { cursor } : {}
3962
+ } })),
3945
3963
  kind: "Branch",
3946
3964
  name: args.branch
3947
3965
  });
@@ -4894,9 +4912,9 @@ const openBrowser = (url) => Effect.gen(function* () {
4894
4912
  const browserLogin = Effect.scoped(Effect.gen(function* () {
4895
4913
  const configStore = yield* ConfigStore;
4896
4914
  const authStore = yield* AuthStore;
4897
- const accountsUrl = yield* configStore.getAccountsUrl;
4915
+ const webUrl = yield* configStore.getWebUrl;
4898
4916
  const loginServer = yield* Effect.acquireRelease(Effect.sync(createBrowserLoginServer), (server) => Effect.sync(server.stop));
4899
- const loginUrl = `${accountsUrl}/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
4917
+ const loginUrl = `${webUrl}/auth/cli-login?callbackUrl=${encodeURIComponent(loginServer.callbackUrl)}`;
4900
4918
  yield* Console.log("Opening browser for better-update login...");
4901
4919
  yield* Console.log("");
4902
4920
  yield* openBrowser(loginUrl);
@@ -4954,12 +4972,37 @@ const logoutCommand = defineCommand({
4954
4972
  const listCommand$1 = defineCommand({
4955
4973
  meta: {
4956
4974
  name: "list",
4957
- description: "List all projects"
4975
+ description: "List projects (most recently active first)"
4958
4976
  },
4959
- run: async () => runEffect(Effect.gen(function* () {
4960
- const { items } = yield* (yield* apiClient).projects.list({ urlParams: {
4961
- page: 1,
4962
- 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 } : {}
4963
5006
  } });
4964
5007
  if (items.length === 0) {
4965
5008
  yield* Console.log("No projects found.");
@@ -4969,13 +5012,14 @@ const listCommand$1 = defineCommand({
4969
5012
  "ID",
4970
5013
  "Name",
4971
5014
  "Slug",
4972
- "Created"
5015
+ "Last activity"
4973
5016
  ], items.map((project) => [
4974
5017
  project.id,
4975
5018
  project.name,
4976
5019
  project.slug,
4977
- project.createdAt
5020
+ project.lastActivityAt
4978
5021
  ]));
5022
+ yield* Console.log(`Page ${page} · ${items.length} of ${total} project(s)`);
4979
5023
  }))
4980
5024
  });
4981
5025
  const createCommand = defineCommand({
@@ -5118,7 +5162,8 @@ const statusCommand = defineCommand({
5118
5162
  yield* Console.log("");
5119
5163
  yield* Console.log("Builds");
5120
5164
  yield* Console.log("------");
5121
- yield* printKeyValue([["Total", String(builds.total)]]);
5165
+ const moreSuffix = builds.nextCursor === null ? "" : "+";
5166
+ yield* printKeyValue([["Recent", `${String(builds.items.length)}${moreSuffix}`]]);
5122
5167
  }))
5123
5168
  });
5124
5169
 
@@ -5174,11 +5219,11 @@ const listCommand = defineCommand({
5174
5219
  const limit = yield* parseLimit(args.limit, 20);
5175
5220
  const projectId = yield* readProjectId;
5176
5221
  const api = yield* apiClient;
5177
- const { items: branches } = yield* api.branches.list({ urlParams: {
5222
+ const branches = yield* drainCursor((cursor) => api.branches.list({ urlParams: {
5178
5223
  projectId,
5179
- page: 1,
5180
- limit: 1e3
5181
- } });
5224
+ limit: 100,
5225
+ ...cursor ? { cursor } : {}
5226
+ } }));
5182
5227
  const branchId = args.branch ? yield* resolveNamedResourceId({
5183
5228
  items: branches,
5184
5229
  kind: "Branch",
@@ -5187,7 +5232,6 @@ const listCommand = defineCommand({
5187
5232
  const { items } = yield* api.updates.list({ urlParams: {
5188
5233
  projectId,
5189
5234
  ...branchId === void 0 ? {} : { branchId },
5190
- page: 1,
5191
5235
  limit
5192
5236
  } });
5193
5237
  if (items.length === 0) {