@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 +233 -189
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
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.
|
|
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
|
-
...
|
|
647
|
-
})).addSuccess(
|
|
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
|
-
...
|
|
684
|
-
})).addSuccess(
|
|
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
|
-
|
|
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
|
-
...
|
|
899
|
-
})).addSuccess(
|
|
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
|
-
...
|
|
960
|
-
})).addSuccess(
|
|
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
|
-
...
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
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
|
|
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
|
-
|
|
1529
|
-
const envUrl = yield* runtime.getEnv("
|
|
1515
|
+
getWebUrl: Effect.gen(function* () {
|
|
1516
|
+
const envUrl = yield* runtime.getEnv("BETTER_UPDATE_WEB_URL");
|
|
1530
1517
|
if (envUrl) return normalizeUrl(envUrl);
|
|
1531
|
-
const
|
|
1532
|
-
if (typeof
|
|
1533
|
-
return
|
|
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
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
-
|
|
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
|
|
2033
|
+
const api = yield* apiClient;
|
|
2034
|
+
const items = yield* drainCursor((cursor) => api.branches.list({ urlParams: {
|
|
1987
2035
|
projectId,
|
|
1988
|
-
|
|
1989
|
-
|
|
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) =>
|
|
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
|
|
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) =>
|
|
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 =
|
|
2289
|
+
const p12Path = path.join(options.tempDir, "signing.p12");
|
|
2231
2290
|
const profileFilename = `${resolved.provisioningProfile.uuid ?? "profile"}.mobileprovision`;
|
|
2232
|
-
const profilePath =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3295
|
-
|
|
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
|
-
"
|
|
3298
|
-
"
|
|
3299
|
-
"
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
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 [
|
|
3739
|
+
const [items, branches] = yield* Effect.all([drainCursor((cursor) => api.channels.list({ urlParams: {
|
|
3720
3740
|
projectId,
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
} }), api.branches.list({ urlParams: {
|
|
3741
|
+
limit: 100,
|
|
3742
|
+
...cursor ? { cursor } : {}
|
|
3743
|
+
} })), drainCursor((cursor) => api.branches.list({ urlParams: {
|
|
3724
3744
|
projectId,
|
|
3725
|
-
|
|
3726
|
-
|
|
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
|
|
4915
|
+
const webUrl = yield* configStore.getWebUrl;
|
|
4898
4916
|
const loginServer = yield* Effect.acquireRelease(Effect.sync(createBrowserLoginServer), (server) => Effect.sync(server.stop));
|
|
4899
|
-
const loginUrl = `${
|
|
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
|
|
4975
|
+
description: "List projects (most recently active first)"
|
|
4958
4976
|
},
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
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
|
-
"
|
|
5015
|
+
"Last activity"
|
|
4973
5016
|
], items.map((project) => [
|
|
4974
5017
|
project.id,
|
|
4975
5018
|
project.name,
|
|
4976
5019
|
project.slug,
|
|
4977
|
-
project.
|
|
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
|
-
|
|
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
|
|
5222
|
+
const branches = yield* drainCursor((cursor) => api.branches.list({ urlParams: {
|
|
5178
5223
|
projectId,
|
|
5179
|
-
|
|
5180
|
-
|
|
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) {
|