@better-update/cli 0.31.1 → 0.32.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 +219 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -34,7 +34,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
34
34
|
|
|
35
35
|
//#endregion
|
|
36
36
|
//#region package.json
|
|
37
|
-
var version = "0.
|
|
37
|
+
var version = "0.32.0";
|
|
38
38
|
|
|
39
39
|
//#endregion
|
|
40
40
|
//#region src/lib/interactive-mode.ts
|
|
@@ -1297,8 +1297,8 @@ var ChannelsGroup = class extends HttpApiGroup.make("channels").add(HttpApiEndpo
|
|
|
1297
1297
|
//#endregion
|
|
1298
1298
|
//#region ../../packages/api/src/domain/device.ts
|
|
1299
1299
|
const DeviceClass = Schema.Literal("IPHONE", "IPAD", "MAC", "UNKNOWN");
|
|
1300
|
-
const IDENTIFIER_PATTERN = /^(?:[A-Fa-f0-9]{40}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{16}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12})$/u;
|
|
1301
|
-
const DeviceIdentifier = Schema.String.pipe(Schema.pattern(IDENTIFIER_PATTERN, { message: () => "Identifier must be an Apple UDID: 40 hex chars, 8-16 hex, or UUID (8-4-4-4-12 hex)" }));
|
|
1300
|
+
const IDENTIFIER_PATTERN$1 = /^(?:[A-Fa-f0-9]{40}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{16}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12})$/u;
|
|
1301
|
+
const DeviceIdentifier = Schema.String.pipe(Schema.pattern(IDENTIFIER_PATTERN$1, { message: () => "Identifier must be an Apple UDID: 40 hex chars, 8-16 hex, or UUID (8-4-4-4-12 hex)" }));
|
|
1302
1302
|
var Device = class extends Schema.Class("Device")({
|
|
1303
1303
|
id: Id,
|
|
1304
1304
|
organizationId: Id,
|
|
@@ -1324,6 +1324,34 @@ const UpdateDeviceBody = Schema.Struct({
|
|
|
1324
1324
|
enabled: Schema.optional(Schema.Boolean),
|
|
1325
1325
|
appleTeamId: Schema.optional(Schema.NullOr(Id))
|
|
1326
1326
|
});
|
|
1327
|
+
/**
|
|
1328
|
+
* One device in an App Store Connect snapshot, as reconciled by `syncDevices`.
|
|
1329
|
+
* `appleDevicePortalId` is the device's id on Apple's portal — its presence is
|
|
1330
|
+
* what the dashboard surfaces as "synced".
|
|
1331
|
+
*/
|
|
1332
|
+
const SyncDeviceEntry = Schema.Struct({
|
|
1333
|
+
identifier: DeviceIdentifier,
|
|
1334
|
+
name: Name120,
|
|
1335
|
+
deviceClass: DeviceClass,
|
|
1336
|
+
appleDevicePortalId: Schema.String
|
|
1337
|
+
});
|
|
1338
|
+
/**
|
|
1339
|
+
* Reconcile the org's device roster for one Apple team against a snapshot of
|
|
1340
|
+
* App Store Connect devices: link portal ids onto existing rows and import any
|
|
1341
|
+
* device that only exists on Apple. `appleTeamId` is the internal team Id (UUID).
|
|
1342
|
+
*/
|
|
1343
|
+
const SyncDevicesBody = Schema.Struct({
|
|
1344
|
+
appleTeamId: Id,
|
|
1345
|
+
devices: Schema.Array(SyncDeviceEntry)
|
|
1346
|
+
});
|
|
1347
|
+
const SyncDevicesResult = Schema.Struct({
|
|
1348
|
+
/** Devices that existed only on Apple and were imported locally. */
|
|
1349
|
+
created: Schema.Number,
|
|
1350
|
+
/** Local devices that gained (or changed) their Apple portal id. */
|
|
1351
|
+
linked: Schema.Number,
|
|
1352
|
+
/** Local devices already in sync — nothing to do. */
|
|
1353
|
+
unchanged: Schema.Number
|
|
1354
|
+
});
|
|
1327
1355
|
const DeleteDeviceResult = DeletedResult;
|
|
1328
1356
|
const DeviceSortColumn = Schema.Literal("name", "createdAt", "deviceClass");
|
|
1329
1357
|
const DeviceSort = sortParam(DeviceSortColumn);
|
|
@@ -1374,6 +1402,9 @@ var DevicesGroup = class extends HttpApiGroup.make("devices").add(HttpApiEndpoin
|
|
|
1374
1402
|
}))).add(HttpApiEndpoint.del("delete")`/api/devices/${idParam}`.addSuccess(DeleteDeviceResult).annotateContext(OpenApi.annotations({
|
|
1375
1403
|
title: "Delete device",
|
|
1376
1404
|
description: "Remove a registered device from the organization"
|
|
1405
|
+
}))).add(HttpApiEndpoint.post("syncDevices", "/api/devices/sync").setPayload(SyncDevicesBody).addSuccess(SyncDevicesResult).annotateContext(OpenApi.annotations({
|
|
1406
|
+
title: "Sync devices with App Store Connect",
|
|
1407
|
+
description: "Reconcile the org's device roster for one Apple team against an App Store Connect snapshot: link Apple portal ids onto existing devices and import devices that only exist on Apple"
|
|
1377
1408
|
}))).add(HttpApiEndpoint.post("createRegistrationRequest", "/api/devices/registration-requests").setPayload(CreateRegistrationRequestBody).addSuccess(DeviceRegistrationRequest, { status: 201 }).annotateContext(OpenApi.annotations({
|
|
1378
1409
|
title: "Create device registration request",
|
|
1379
1410
|
description: "Generate a URL + QR code for self-service device enrollment via Safari on iOS"
|
|
@@ -19357,12 +19388,13 @@ const toAscDevice = (value) => {
|
|
|
19357
19388
|
if (!isRecord$1(value)) return null;
|
|
19358
19389
|
const { id, attributes } = value;
|
|
19359
19390
|
if (typeof id !== "string" || !isRecord$1(attributes)) return null;
|
|
19360
|
-
const { udid, name } = attributes;
|
|
19391
|
+
const { udid, name, deviceClass } = attributes;
|
|
19361
19392
|
if (typeof udid !== "string" || typeof name !== "string") return null;
|
|
19362
19393
|
return {
|
|
19363
19394
|
id,
|
|
19364
19395
|
udid,
|
|
19365
|
-
name
|
|
19396
|
+
name,
|
|
19397
|
+
deviceClass: typeof deviceClass === "string" ? deviceClass : null
|
|
19366
19398
|
};
|
|
19367
19399
|
};
|
|
19368
19400
|
const extractList = (body, map) => {
|
|
@@ -19373,6 +19405,18 @@ const extractSingle = (body, map) => {
|
|
|
19373
19405
|
if (!isRecord$1(body)) return null;
|
|
19374
19406
|
return map(body["data"]);
|
|
19375
19407
|
};
|
|
19408
|
+
/**
|
|
19409
|
+
* App Store Connect paginates list responses (default 200/page) and returns the
|
|
19410
|
+
* absolute URL of the next page under `links.next`. Strip the base so it can be
|
|
19411
|
+
* fed back into `fetchRaw`; return null when there is no further page.
|
|
19412
|
+
*/
|
|
19413
|
+
const nextPagePath = (body) => {
|
|
19414
|
+
if (!isRecord$1(body)) return null;
|
|
19415
|
+
const { links } = body;
|
|
19416
|
+
if (!isRecord$1(links) || typeof links["next"] !== "string") return null;
|
|
19417
|
+
const { next } = links;
|
|
19418
|
+
return next.startsWith(API_BASE) ? next.slice(37) : next;
|
|
19419
|
+
};
|
|
19376
19420
|
const malformed = (resource) => new AscApiError({
|
|
19377
19421
|
status: 500,
|
|
19378
19422
|
message: `Malformed ${resource} response`,
|
|
@@ -19420,7 +19464,29 @@ const createBundleId = (credentials, params) => withJwt(credentials, (jwt) => Ef
|
|
|
19420
19464
|
return resource;
|
|
19421
19465
|
}));
|
|
19422
19466
|
const listDevices = (credentials) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
19423
|
-
|
|
19467
|
+
const devices = [];
|
|
19468
|
+
let path = "/v1/devices?limit=200";
|
|
19469
|
+
while (path !== null) {
|
|
19470
|
+
const body = yield* fetchRaw(jwt, path);
|
|
19471
|
+
devices.push(...extractList(body, toAscDevice));
|
|
19472
|
+
path = nextPagePath(body);
|
|
19473
|
+
}
|
|
19474
|
+
return devices;
|
|
19475
|
+
}));
|
|
19476
|
+
const createDevice = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
19477
|
+
const resource = extractSingle(yield* fetchRaw(jwt, "/v1/devices", {
|
|
19478
|
+
method: "POST",
|
|
19479
|
+
body: JSON.stringify({ data: {
|
|
19480
|
+
type: "devices",
|
|
19481
|
+
attributes: {
|
|
19482
|
+
name: params.name,
|
|
19483
|
+
udid: params.udid,
|
|
19484
|
+
platform: "IOS"
|
|
19485
|
+
}
|
|
19486
|
+
} })
|
|
19487
|
+
}), toAscDevice);
|
|
19488
|
+
if (resource === null) return yield* malformed("device");
|
|
19489
|
+
return resource;
|
|
19424
19490
|
}));
|
|
19425
19491
|
const createProvisioningProfile = (credentials, params) => withJwt(credentials, (jwt) => Effect.gen(function* () {
|
|
19426
19492
|
const relationships = {
|
|
@@ -30143,6 +30209,7 @@ const listDevicesCommand = defineCommand({
|
|
|
30143
30209
|
"Class",
|
|
30144
30210
|
"UDID",
|
|
30145
30211
|
"Team",
|
|
30212
|
+
"Synced",
|
|
30146
30213
|
"Enabled"
|
|
30147
30214
|
], items.map((device) => [
|
|
30148
30215
|
device.id,
|
|
@@ -30150,6 +30217,7 @@ const listDevicesCommand = defineCommand({
|
|
|
30150
30217
|
device.deviceClass,
|
|
30151
30218
|
device.identifier,
|
|
30152
30219
|
device.appleTeamId ?? "—",
|
|
30220
|
+
device.appleDevicePortalId === null ? "no" : "yes",
|
|
30153
30221
|
device.enabled ? "yes" : "no"
|
|
30154
30222
|
]));
|
|
30155
30223
|
return {
|
|
@@ -30190,6 +30258,150 @@ const renameDeviceCommand = defineCommand({
|
|
|
30190
30258
|
}))
|
|
30191
30259
|
});
|
|
30192
30260
|
|
|
30261
|
+
//#endregion
|
|
30262
|
+
//#region src/commands/devices/sync.ts
|
|
30263
|
+
const IDENTIFIER_PATTERN = /^(?:[A-Fa-f0-9]{40}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{16}|[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12})$/u;
|
|
30264
|
+
const APPLE_DEVICE_CLASS = {
|
|
30265
|
+
IPHONE: "IPHONE",
|
|
30266
|
+
IPAD: "IPAD",
|
|
30267
|
+
MAC: "MAC"
|
|
30268
|
+
};
|
|
30269
|
+
const toDeviceClass = (raw) => raw === null ? "UNKNOWN" : APPLE_DEVICE_CLASS[raw] ?? "UNKNOWN";
|
|
30270
|
+
const ascErrorMessage = (error) => error._tag === "AscApiError" ? error.message : `Apple request failed: ${String(error.cause)}`;
|
|
30271
|
+
const LIST_LIMIT = 100;
|
|
30272
|
+
/**
|
|
30273
|
+
* Resolve which ASC key authenticates the sync and which internal team it
|
|
30274
|
+
* targets. Either flag suffices: an ASC key already carries its team, and a team
|
|
30275
|
+
* resolves to the ASC key uploaded for it.
|
|
30276
|
+
*/
|
|
30277
|
+
const resolveTarget = (api, args) => Effect.gen(function* () {
|
|
30278
|
+
const keyArg = args["asc-api-key-id"];
|
|
30279
|
+
const teamArg = args["apple-team-id"];
|
|
30280
|
+
const ascKeys = yield* api.ascApiKeys.list();
|
|
30281
|
+
if (keyArg !== void 0) {
|
|
30282
|
+
const match = ascKeys.items.find((key) => key.id === keyArg);
|
|
30283
|
+
if (match === void 0) return yield* new InvalidArgumentError({ message: `ASC API key "${keyArg}" not found in this organization.` });
|
|
30284
|
+
const appleTeamId = teamArg ?? match.appleTeamId;
|
|
30285
|
+
if (!appleTeamId) return yield* new InvalidArgumentError({ message: `ASC API key "${keyArg}" is not linked to an Apple team; pass --apple-team-id <uuid>.` });
|
|
30286
|
+
return {
|
|
30287
|
+
ascApiKeyId: keyArg,
|
|
30288
|
+
appleTeamId
|
|
30289
|
+
};
|
|
30290
|
+
}
|
|
30291
|
+
if (teamArg !== void 0) {
|
|
30292
|
+
const match = ascKeys.items.find((key) => key.appleTeamId === teamArg);
|
|
30293
|
+
if (match === void 0) return yield* new InvalidArgumentError({ message: `No ASC API key found for team "${teamArg}". Upload one with \`better-update credentials upload-asc-key\`.` });
|
|
30294
|
+
return {
|
|
30295
|
+
ascApiKeyId: match.id,
|
|
30296
|
+
appleTeamId: teamArg
|
|
30297
|
+
};
|
|
30298
|
+
}
|
|
30299
|
+
return yield* new InvalidArgumentError({ message: "Pass --apple-team-id <uuid> or --asc-api-key-id <id> to choose what to sync." });
|
|
30300
|
+
});
|
|
30301
|
+
const listAllLocalDevices = (api, appleTeamId) => Effect.gen(function* () {
|
|
30302
|
+
const items = [];
|
|
30303
|
+
let page = 1;
|
|
30304
|
+
let total = Number.POSITIVE_INFINITY;
|
|
30305
|
+
while (items.length < total) {
|
|
30306
|
+
const result = yield* api.devices.list({ urlParams: {
|
|
30307
|
+
appleTeamId,
|
|
30308
|
+
page,
|
|
30309
|
+
limit: LIST_LIMIT
|
|
30310
|
+
} });
|
|
30311
|
+
({total} = result);
|
|
30312
|
+
if (result.items.length === 0) break;
|
|
30313
|
+
items.push(...result.items);
|
|
30314
|
+
page += 1;
|
|
30315
|
+
}
|
|
30316
|
+
return items;
|
|
30317
|
+
});
|
|
30318
|
+
const syncDeviceCommand = defineCommand({
|
|
30319
|
+
meta: {
|
|
30320
|
+
name: "sync",
|
|
30321
|
+
description: "Sync devices with Apple App Store Connect: register local-only devices on Apple and import devices already registered there"
|
|
30322
|
+
},
|
|
30323
|
+
args: {
|
|
30324
|
+
"apple-team-id": {
|
|
30325
|
+
type: "string",
|
|
30326
|
+
description: "Internal team Id (UUID) to sync; derived from --asc-api-key-id if omitted"
|
|
30327
|
+
},
|
|
30328
|
+
"asc-api-key-id": {
|
|
30329
|
+
type: "string",
|
|
30330
|
+
description: "ASC API key to authenticate with; derived from --apple-team-id if omitted"
|
|
30331
|
+
},
|
|
30332
|
+
push: {
|
|
30333
|
+
type: "boolean",
|
|
30334
|
+
default: true,
|
|
30335
|
+
description: "Register local-only devices on Apple",
|
|
30336
|
+
negativeDescription: "Skip registering local devices on Apple (--no-push)"
|
|
30337
|
+
},
|
|
30338
|
+
pull: {
|
|
30339
|
+
type: "boolean",
|
|
30340
|
+
default: true,
|
|
30341
|
+
description: "Import Apple-registered devices into better-update",
|
|
30342
|
+
negativeDescription: "Skip importing Apple devices (--no-pull)"
|
|
30343
|
+
}
|
|
30344
|
+
},
|
|
30345
|
+
run: async ({ args }) => runEffect(Effect.gen(function* () {
|
|
30346
|
+
const api = yield* apiClient;
|
|
30347
|
+
const target = yield* resolveTarget(api, args);
|
|
30348
|
+
const creds = yield* fetchAscCredentials(api, target.ascApiKeyId);
|
|
30349
|
+
const ascCreds = {
|
|
30350
|
+
keyId: creds.keyId,
|
|
30351
|
+
issuerId: creds.issuerId,
|
|
30352
|
+
p8Pem: creds.p8Pem
|
|
30353
|
+
};
|
|
30354
|
+
const appleDevices = yield* listDevices(ascCreds);
|
|
30355
|
+
const local = yield* listAllLocalDevices(api, target.appleTeamId);
|
|
30356
|
+
const localUdids = new Set(local.map((device) => device.identifier.toLowerCase()));
|
|
30357
|
+
const pushed = [];
|
|
30358
|
+
const pushFailures = [];
|
|
30359
|
+
if (args.push) {
|
|
30360
|
+
const appleUdids = new Set(appleDevices.map((device) => device.udid.toLowerCase()));
|
|
30361
|
+
const toPush = local.filter((device) => !appleUdids.has(device.identifier.toLowerCase()));
|
|
30362
|
+
for (const device of toPush) {
|
|
30363
|
+
const result = yield* Effect.either(createDevice(ascCreds, {
|
|
30364
|
+
name: device.name,
|
|
30365
|
+
udid: device.identifier
|
|
30366
|
+
}));
|
|
30367
|
+
if (Either.isRight(result)) pushed.push(result.right);
|
|
30368
|
+
else pushFailures.push({
|
|
30369
|
+
identifier: device.identifier,
|
|
30370
|
+
message: ascErrorMessage(result.left)
|
|
30371
|
+
});
|
|
30372
|
+
}
|
|
30373
|
+
}
|
|
30374
|
+
const reconcileEntries = [...appleDevices, ...pushed].filter((device) => args.pull || localUdids.has(device.udid.toLowerCase())).filter((device) => IDENTIFIER_PATTERN.test(device.udid) && device.name.length > 0).map((device) => ({
|
|
30375
|
+
identifier: device.udid,
|
|
30376
|
+
name: device.name.slice(0, 120),
|
|
30377
|
+
deviceClass: toDeviceClass(device.deviceClass),
|
|
30378
|
+
appleDevicePortalId: device.id
|
|
30379
|
+
}));
|
|
30380
|
+
const summary = reconcileEntries.length > 0 ? yield* api.devices.syncDevices({ payload: {
|
|
30381
|
+
appleTeamId: target.appleTeamId,
|
|
30382
|
+
devices: reconcileEntries
|
|
30383
|
+
} }) : {
|
|
30384
|
+
created: 0,
|
|
30385
|
+
linked: 0,
|
|
30386
|
+
unchanged: 0
|
|
30387
|
+
};
|
|
30388
|
+
yield* printHumanKeyValue([
|
|
30389
|
+
["Apple devices", String(appleDevices.length + pushed.length)],
|
|
30390
|
+
["Pushed to Apple", String(pushed.length)],
|
|
30391
|
+
["Imported locally", String(summary.created)],
|
|
30392
|
+
["Linked (portal id set)", String(summary.linked)],
|
|
30393
|
+
["Already synced", String(summary.unchanged)]
|
|
30394
|
+
]);
|
|
30395
|
+
for (const failure of pushFailures) yield* printHuman(`⚠ Could not push ${failure.identifier} to Apple: ${failure.message}`);
|
|
30396
|
+
return {
|
|
30397
|
+
appleTeamId: target.appleTeamId,
|
|
30398
|
+
pushed: pushed.length,
|
|
30399
|
+
...summary,
|
|
30400
|
+
pushFailures
|
|
30401
|
+
};
|
|
30402
|
+
}), { json: "value" })
|
|
30403
|
+
});
|
|
30404
|
+
|
|
30193
30405
|
//#endregion
|
|
30194
30406
|
//#region src/commands/devices/view.ts
|
|
30195
30407
|
const viewDeviceCommand = defineCommand({
|
|
@@ -30230,6 +30442,7 @@ const devicesCommand = defineCommand({
|
|
|
30230
30442
|
add: addDeviceCommand,
|
|
30231
30443
|
list: listDevicesCommand,
|
|
30232
30444
|
view: viewDeviceCommand,
|
|
30445
|
+
sync: syncDeviceCommand,
|
|
30233
30446
|
rename: renameDeviceCommand,
|
|
30234
30447
|
enable: enableDeviceCommand,
|
|
30235
30448
|
disable: disableDeviceCommand,
|