@onmyway133/asc-cli 1.0.1

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.
Files changed (55) hide show
  1. package/README.md +299 -0
  2. package/bun.lock +231 -0
  3. package/dist/index.js +179 -0
  4. package/docs/API_COVERAGE.md +355 -0
  5. package/docs/openapi/latest.json +230597 -0
  6. package/docs/openapi/paths.txt +1208 -0
  7. package/openapi-ts.config.ts +25 -0
  8. package/package.json +32 -0
  9. package/scripts/gen-paths-index.py +24 -0
  10. package/src/api/client.ts +132 -0
  11. package/src/api/generated/client/client.gen.ts +298 -0
  12. package/src/api/generated/client/index.ts +25 -0
  13. package/src/api/generated/client/types.gen.ts +214 -0
  14. package/src/api/generated/client/utils.gen.ts +316 -0
  15. package/src/api/generated/client.gen.ts +16 -0
  16. package/src/api/generated/core/auth.gen.ts +41 -0
  17. package/src/api/generated/core/bodySerializer.gen.ts +82 -0
  18. package/src/api/generated/core/params.gen.ts +169 -0
  19. package/src/api/generated/core/pathSerializer.gen.ts +171 -0
  20. package/src/api/generated/core/queryKeySerializer.gen.ts +117 -0
  21. package/src/api/generated/core/serverSentEvents.gen.ts +242 -0
  22. package/src/api/generated/core/types.gen.ts +104 -0
  23. package/src/api/generated/core/utils.gen.ts +140 -0
  24. package/src/api/generated/index.ts +4 -0
  25. package/src/api/generated/sdk.gen.ts +11701 -0
  26. package/src/api/generated/types.gen.ts +92035 -0
  27. package/src/api/hey-api-client.ts +20 -0
  28. package/src/api/types.ts +160 -0
  29. package/src/auth/credentials.ts +125 -0
  30. package/src/auth/jwt.ts +118 -0
  31. package/src/commands/app-info.ts +110 -0
  32. package/src/commands/apps.ts +44 -0
  33. package/src/commands/auth.ts +327 -0
  34. package/src/commands/availability.ts +52 -0
  35. package/src/commands/beta-review.ts +145 -0
  36. package/src/commands/builds.ts +97 -0
  37. package/src/commands/game-center.ts +114 -0
  38. package/src/commands/iap.ts +105 -0
  39. package/src/commands/metadata.ts +81 -0
  40. package/src/commands/pricing.ts +110 -0
  41. package/src/commands/reports.ts +116 -0
  42. package/src/commands/reviews.ts +93 -0
  43. package/src/commands/screenshots.ts +139 -0
  44. package/src/commands/signing.ts +214 -0
  45. package/src/commands/subscriptions.ts +144 -0
  46. package/src/commands/testflight.ts +110 -0
  47. package/src/commands/versions.ts +76 -0
  48. package/src/commands/xcode-cloud.ts +207 -0
  49. package/src/index.ts +1661 -0
  50. package/src/utils/help-spec.ts +835 -0
  51. package/src/utils/output.ts +79 -0
  52. package/tests/auth.test.ts +105 -0
  53. package/tests/client.test.ts +22 -0
  54. package/tests/output.test.ts +36 -0
  55. package/tsconfig.json +15 -0
@@ -0,0 +1,139 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import { readFileSync } from "fs"
3
+ import { detectFormat, printJSON, printSuccess, printTable } from "../utils/output.ts"
4
+
5
+ // ---- App Screenshots ----
6
+
7
+ export async function screenshotSetsList(opts: {
8
+ versionLocalizationId: string
9
+ output?: string
10
+ }): Promise<void> {
11
+ const items = await ascFetchAll<{
12
+ id: string
13
+ attributes: {
14
+ screenshotDisplayType?: string
15
+ appScreenshots?: { meta?: { total?: number } }
16
+ }
17
+ }>(`/v1/appStoreVersionLocalizations/${opts.versionLocalizationId}/appScreenshotSets`, {
18
+ params: { "fields[appScreenshotSets]": "screenshotDisplayType,appScreenshots" },
19
+ })
20
+ const fmt = detectFormat(opts.output)
21
+ if (fmt === "json") { printJSON(items); return }
22
+ printTable(
23
+ ["Set ID", "Display Type"],
24
+ items.map(s => [s.id, s.attributes.screenshotDisplayType ?? "-"]),
25
+ `Screenshot Sets (${items.length})`,
26
+ )
27
+ }
28
+
29
+ export async function screenshotsList(opts: {
30
+ setId: string
31
+ output?: string
32
+ }): Promise<void> {
33
+ const items = await ascFetchAll<{
34
+ id: string
35
+ attributes: {
36
+ fileName?: string
37
+ fileSize?: number
38
+ sourceFileChecksum?: string
39
+ assetDeliveryState?: { state?: string }
40
+ imageAsset?: { templateUrl?: string; width?: number; height?: number }
41
+ }
42
+ }>(`/v1/appScreenshotSets/${opts.setId}/appScreenshots`)
43
+ const fmt = detectFormat(opts.output)
44
+ if (fmt === "json") { printJSON(items); return }
45
+ printTable(
46
+ ["ID", "File Name", "State"],
47
+ items.map(s => [
48
+ s.id,
49
+ s.attributes.fileName ?? "-",
50
+ s.attributes.assetDeliveryState?.state ?? "-",
51
+ ]),
52
+ `Screenshots (${items.length})`,
53
+ )
54
+ }
55
+
56
+ export async function screenshotCreate(opts: {
57
+ setId: string
58
+ filePath: string
59
+ }): Promise<void> {
60
+ const fileData = readFileSync(opts.filePath)
61
+ const fileName = opts.filePath.split("/").pop() ?? "screenshot.png"
62
+ const fileSize = fileData.length
63
+
64
+ // Step 1: Reserve asset
65
+ const reserve = await ascFetch<{
66
+ data: {
67
+ id: string
68
+ attributes: {
69
+ uploadOperations?: Array<{
70
+ url: string
71
+ method: string
72
+ offset: number
73
+ length: number
74
+ requestHeaders?: Array<{ name: string; value: string }>
75
+ }>
76
+ }
77
+ }
78
+ }>("/v1/appScreenshots", {
79
+ method: "POST",
80
+ body: {
81
+ data: {
82
+ type: "appScreenshots",
83
+ attributes: { fileName, fileSize },
84
+ relationships: {
85
+ appScreenshotSet: { data: { type: "appScreenshotSets", id: opts.setId } },
86
+ },
87
+ },
88
+ },
89
+ })
90
+
91
+ const screenshotId = reserve.data.data.id
92
+ const uploads = reserve.data.data.attributes.uploadOperations ?? []
93
+
94
+ // Step 2: Upload parts
95
+ for (const op of uploads) {
96
+ const chunk = fileData.subarray(op.offset, op.offset + op.length)
97
+ const headers: Record<string, string> = { "Content-Type": "image/png" }
98
+ for (const h of op.requestHeaders ?? []) headers[h.name] = h.value
99
+ await fetch(op.url, { method: op.method, headers, body: chunk })
100
+ }
101
+
102
+ // Step 3: Commit
103
+ await ascFetch(`/v1/appScreenshots/${screenshotId}`, {
104
+ method: "PATCH",
105
+ body: {
106
+ data: {
107
+ type: "appScreenshots",
108
+ id: screenshotId,
109
+ attributes: { uploaded: true },
110
+ },
111
+ },
112
+ })
113
+
114
+ printSuccess(`Screenshot uploaded: ${screenshotId}`)
115
+ }
116
+
117
+ export async function screenshotDelete(screenshotId: string): Promise<void> {
118
+ await ascFetch(`/v1/appScreenshots/${screenshotId}`, { method: "DELETE" })
119
+ printSuccess(`Screenshot ${screenshotId} deleted`)
120
+ }
121
+
122
+ // ---- App Preview Sets ----
123
+
124
+ export async function previewSetsList(opts: {
125
+ versionLocalizationId: string
126
+ output?: string
127
+ }): Promise<void> {
128
+ const items = await ascFetchAll<{
129
+ id: string
130
+ attributes: { previewType?: string }
131
+ }>(`/v1/appStoreVersionLocalizations/${opts.versionLocalizationId}/appPreviewSets`)
132
+ const fmt = detectFormat(opts.output)
133
+ if (fmt === "json") { printJSON(items); return }
134
+ printTable(
135
+ ["Set ID", "Preview Type"],
136
+ items.map(s => [s.id, s.attributes.previewType ?? "-"]),
137
+ `Preview Sets (${items.length})`,
138
+ )
139
+ }
@@ -0,0 +1,214 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import { detectFormat, formatDate, printJSON, printSuccess, printTable } from "../utils/output.ts"
3
+
4
+ // ---- Certificates ----
5
+
6
+ export async function certificatesList(opts: {
7
+ type?: string
8
+ output?: string
9
+ } = {}): Promise<void> {
10
+ const params: Record<string, string> = {
11
+ "fields[certificates]": "certificateType,displayName,expirationDate,serialNumber,platform",
12
+ }
13
+ if (opts.type) params["filter[certificateType]"] = opts.type
14
+ const items = await ascFetchAll<{
15
+ id: string
16
+ attributes: {
17
+ certificateType?: string
18
+ displayName?: string
19
+ expirationDate?: string
20
+ serialNumber?: string
21
+ platform?: string
22
+ }
23
+ }>("/v1/certificates", { params })
24
+ const fmt = detectFormat(opts.output)
25
+ if (fmt === "json") { printJSON(items); return }
26
+ printTable(
27
+ ["ID", "Name", "Type", "Expires", "Serial"],
28
+ items.map(c => [
29
+ c.id,
30
+ c.attributes.displayName ?? "-",
31
+ c.attributes.certificateType ?? "-",
32
+ formatDate(c.attributes.expirationDate ?? ""),
33
+ c.attributes.serialNumber ?? "-",
34
+ ]),
35
+ `Certificates (${items.length})`,
36
+ )
37
+ }
38
+
39
+ export async function certificateGet(certId: string, opts: { output?: string } = {}): Promise<void> {
40
+ const result = await ascFetch<{
41
+ data: {
42
+ id: string
43
+ attributes: {
44
+ certificateType?: string
45
+ displayName?: string
46
+ expirationDate?: string
47
+ serialNumber?: string
48
+ platform?: string
49
+ certificateContent?: string
50
+ }
51
+ }
52
+ }>(`/v1/certificates/${certId}`)
53
+ const fmt = detectFormat(opts.output)
54
+ if (fmt === "json") { printJSON(result.data.data); return }
55
+ const a = result.data.data.attributes
56
+ console.log(`ID: ${result.data.data.id}`)
57
+ console.log(`Name: ${a.displayName ?? "-"}`)
58
+ console.log(`Type: ${a.certificateType ?? "-"}`)
59
+ console.log(`Expires: ${formatDate(a.expirationDate ?? "")}`)
60
+ console.log(`Serial: ${a.serialNumber ?? "-"}`)
61
+ console.log(`Platform: ${a.platform ?? "-"}`)
62
+ }
63
+
64
+ export async function certificateRevoke(certId: string): Promise<void> {
65
+ await ascFetch(`/v1/certificates/${certId}`, { method: "DELETE" })
66
+ printSuccess(`Certificate ${certId} revoked`)
67
+ }
68
+
69
+ // ---- Devices ----
70
+
71
+ export async function devicesList(opts: {
72
+ platform?: string
73
+ status?: string
74
+ output?: string
75
+ } = {}): Promise<void> {
76
+ const params: Record<string, string> = {
77
+ "fields[devices]": "name,deviceClass,model,udid,platform,status,addedDate",
78
+ }
79
+ if (opts.platform) params["filter[platform]"] = opts.platform
80
+ if (opts.status) params["filter[status]"] = opts.status
81
+ const items = await ascFetchAll<{
82
+ id: string
83
+ attributes: {
84
+ name?: string
85
+ deviceClass?: string
86
+ model?: string
87
+ udid?: string
88
+ platform?: string
89
+ status?: string
90
+ addedDate?: string
91
+ }
92
+ }>("/v1/devices", { params })
93
+ const fmt = detectFormat(opts.output)
94
+ if (fmt === "json") { printJSON(items); return }
95
+ printTable(
96
+ ["ID", "Name", "Model", "Platform", "Status", "UDID"],
97
+ items.map(d => [
98
+ d.id,
99
+ d.attributes.name ?? "-",
100
+ d.attributes.model ?? d.attributes.deviceClass ?? "-",
101
+ d.attributes.platform ?? "-",
102
+ d.attributes.status ?? "-",
103
+ d.attributes.udid ?? "-",
104
+ ]),
105
+ `Devices (${items.length})`,
106
+ )
107
+ }
108
+
109
+ export async function deviceRegister(opts: {
110
+ name: string
111
+ udid: string
112
+ platform: string
113
+ }): Promise<void> {
114
+ const result = await ascFetch<{ data: { id: string } }>("/v1/devices", {
115
+ method: "POST",
116
+ body: {
117
+ data: {
118
+ type: "devices",
119
+ attributes: { name: opts.name, udid: opts.udid, platform: opts.platform },
120
+ },
121
+ },
122
+ })
123
+ printSuccess(`Device registered: ${result.data.data.id} (${opts.udid})`)
124
+ }
125
+
126
+ // ---- Bundle IDs ----
127
+
128
+ export async function bundleIdsList(opts: {
129
+ platform?: string
130
+ identifier?: string
131
+ output?: string
132
+ } = {}): Promise<void> {
133
+ const params: Record<string, string> = {
134
+ "fields[bundleIds]": "name,identifier,platform,seedId",
135
+ }
136
+ if (opts.platform) params["filter[platform]"] = opts.platform
137
+ if (opts.identifier) params["filter[identifier]"] = opts.identifier
138
+ const items = await ascFetchAll<{
139
+ id: string
140
+ attributes: { name?: string; identifier?: string; platform?: string; seedId?: string }
141
+ }>("/v1/bundleIds", { params })
142
+ const fmt = detectFormat(opts.output)
143
+ if (fmt === "json") { printJSON(items); return }
144
+ printTable(
145
+ ["ID", "Name", "Identifier", "Platform", "Seed ID"],
146
+ items.map(b => [
147
+ b.id,
148
+ b.attributes.name ?? "-",
149
+ b.attributes.identifier ?? "-",
150
+ b.attributes.platform ?? "-",
151
+ b.attributes.seedId ?? "-",
152
+ ]),
153
+ `Bundle IDs (${items.length})`,
154
+ )
155
+ }
156
+
157
+ export async function bundleIdRegister(opts: {
158
+ name: string
159
+ identifier: string
160
+ platform: string
161
+ }): Promise<void> {
162
+ const result = await ascFetch<{ data: { id: string } }>("/v1/bundleIds", {
163
+ method: "POST",
164
+ body: {
165
+ data: {
166
+ type: "bundleIds",
167
+ attributes: { name: opts.name, identifier: opts.identifier, platform: opts.platform },
168
+ },
169
+ },
170
+ })
171
+ printSuccess(`Bundle ID registered: ${result.data.data.id} (${opts.identifier})`)
172
+ }
173
+
174
+ // ---- Provisioning Profiles ----
175
+
176
+ export async function profilesList(opts: {
177
+ type?: string
178
+ name?: string
179
+ output?: string
180
+ } = {}): Promise<void> {
181
+ const params: Record<string, string> = {
182
+ "fields[profiles]": "name,profileType,profileState,expirationDate,uuid",
183
+ }
184
+ if (opts.type) params["filter[profileType]"] = opts.type
185
+ if (opts.name) params["filter[name]"] = opts.name
186
+ const items = await ascFetchAll<{
187
+ id: string
188
+ attributes: {
189
+ name?: string
190
+ profileType?: string
191
+ profileState?: string
192
+ expirationDate?: string
193
+ uuid?: string
194
+ }
195
+ }>("/v1/profiles", { params })
196
+ const fmt = detectFormat(opts.output)
197
+ if (fmt === "json") { printJSON(items); return }
198
+ printTable(
199
+ ["ID", "Name", "Type", "State", "Expires"],
200
+ items.map(p => [
201
+ p.id,
202
+ p.attributes.name ?? "-",
203
+ p.attributes.profileType ?? "-",
204
+ p.attributes.profileState ?? "-",
205
+ formatDate(p.attributes.expirationDate ?? ""),
206
+ ]),
207
+ `Provisioning Profiles (${items.length})`,
208
+ )
209
+ }
210
+
211
+ export async function profileDelete(profileId: string): Promise<void> {
212
+ await ascFetch(`/v1/profiles/${profileId}`, { method: "DELETE" })
213
+ printSuccess(`Profile ${profileId} deleted`)
214
+ }
@@ -0,0 +1,144 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import { detectFormat, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
3
+
4
+ // ---- Subscription Groups ----
5
+
6
+ export async function subscriptionGroupsList(opts: { appId: string; output?: string }): Promise<void> {
7
+ const items = await ascFetchAll<{
8
+ id: string
9
+ attributes: { referenceName?: string; subscriptions?: number }
10
+ }>(`/v1/apps/${opts.appId}/subscriptionGroups`, {
11
+ params: { "fields[subscriptionGroups]": "referenceName,subscriptions" },
12
+ })
13
+ const fmt = detectFormat(opts.output)
14
+ if (fmt === "json") { printJSON(items); return }
15
+ printTable(
16
+ ["Group ID", "Reference Name"],
17
+ items.map(g => [g.id, g.attributes.referenceName ?? "-"]),
18
+ `Subscription Groups (${items.length})`,
19
+ )
20
+ }
21
+
22
+ export async function subscriptionGroupCreate(opts: {
23
+ appId: string
24
+ referenceName: string
25
+ }): Promise<void> {
26
+ const result = await ascFetch<{ data: { id: string } }>("/v1/subscriptionGroups", {
27
+ method: "POST",
28
+ body: {
29
+ data: {
30
+ type: "subscriptionGroups",
31
+ attributes: { referenceName: opts.referenceName },
32
+ relationships: { app: { data: { type: "apps", id: opts.appId } } },
33
+ },
34
+ },
35
+ })
36
+ printSuccess(`Created subscription group: ${result.data.data.id}`)
37
+ }
38
+
39
+ // ---- Subscriptions ----
40
+
41
+ export async function subscriptionsList(opts: { groupId: string; output?: string }): Promise<void> {
42
+ const items = await ascFetchAll<{
43
+ id: string
44
+ attributes: {
45
+ name?: string
46
+ productId?: string
47
+ subscriptionPeriod?: string
48
+ state?: string
49
+ familySharable?: boolean
50
+ }
51
+ }>(`/v1/subscriptionGroups/${opts.groupId}/subscriptions`, {
52
+ params: { "fields[subscriptions]": "name,productId,subscriptionPeriod,state,familySharable" },
53
+ })
54
+ const fmt = detectFormat(opts.output)
55
+ if (fmt === "json") { printJSON(items); return }
56
+ printTable(
57
+ ["ID", "Name", "Product ID", "Period", "State"],
58
+ items.map(s => [
59
+ s.id,
60
+ truncate(s.attributes.name ?? "-", 30),
61
+ s.attributes.productId ?? "-",
62
+ s.attributes.subscriptionPeriod ?? "-",
63
+ s.attributes.state ?? "-",
64
+ ]),
65
+ `Subscriptions (${items.length})`,
66
+ )
67
+ }
68
+
69
+ export async function subscriptionGet(subId: string, opts: { output?: string } = {}): Promise<void> {
70
+ const result = await ascFetch<{
71
+ data: {
72
+ id: string
73
+ attributes: {
74
+ name?: string
75
+ productId?: string
76
+ subscriptionPeriod?: string
77
+ state?: string
78
+ familySharable?: boolean
79
+ reviewNote?: string
80
+ availableInAllTerritories?: boolean
81
+ groupLevel?: number
82
+ }
83
+ }
84
+ }>(`/v1/subscriptions/${subId}`)
85
+ const fmt = detectFormat(opts.output)
86
+ if (fmt === "json") { printJSON(result.data.data); return }
87
+ const a = result.data.data.attributes
88
+ console.log(`ID: ${result.data.data.id}`)
89
+ console.log(`Name: ${a.name ?? "-"}`)
90
+ console.log(`Product ID: ${a.productId ?? "-"}`)
91
+ console.log(`Period: ${a.subscriptionPeriod ?? "-"}`)
92
+ console.log(`State: ${a.state ?? "-"}`)
93
+ console.log(`Group Level: ${a.groupLevel ?? "-"}`)
94
+ console.log(`Family Share: ${a.familySharable ?? false}`)
95
+ console.log(`All Territ.: ${a.availableInAllTerritories ?? false}`)
96
+ }
97
+
98
+ export async function subscriptionCreate(opts: {
99
+ groupId: string
100
+ name: string
101
+ productId: string
102
+ period: string
103
+ }): Promise<void> {
104
+ const result = await ascFetch<{ data: { id: string } }>("/v1/subscriptions", {
105
+ method: "POST",
106
+ body: {
107
+ data: {
108
+ type: "subscriptions",
109
+ attributes: {
110
+ name: opts.name,
111
+ productId: opts.productId,
112
+ subscriptionPeriod: opts.period,
113
+ },
114
+ relationships: {
115
+ group: { data: { type: "subscriptionGroups", id: opts.groupId } },
116
+ },
117
+ },
118
+ },
119
+ })
120
+ printSuccess(`Created subscription: ${result.data.data.id} (${opts.productId})`)
121
+ }
122
+
123
+ export async function subscriptionPricesList(opts: {
124
+ subId: string
125
+ territory?: string
126
+ output?: string
127
+ }): Promise<void> {
128
+ const params: Record<string, string> = {
129
+ "fields[subscriptionPrices]": "startDate,territory",
130
+ include: "subscriptionPricePoint",
131
+ }
132
+ if (opts.territory) params["filter[territory]"] = opts.territory
133
+ const items = await ascFetchAll<{
134
+ id: string
135
+ attributes: { startDate?: string; territory?: unknown }
136
+ }>(`/v1/subscriptions/${opts.subId}/prices`, { params })
137
+ const fmt = detectFormat(opts.output)
138
+ if (fmt === "json") { printJSON(items); return }
139
+ printTable(
140
+ ["Price ID", "Territory", "Start Date"],
141
+ items.map(p => [p.id, String(p.attributes.territory ?? "-"), p.attributes.startDate ?? "ongoing"]),
142
+ `Subscription Prices (${items.length})`,
143
+ )
144
+ }
@@ -0,0 +1,110 @@
1
+ import * as p from "@clack/prompts"
2
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
3
+ import type { BetaGroup, BetaTester } from "../api/types.ts"
4
+ import { detectFormat, formatDate, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
5
+
6
+ export async function testflightGroupsList(opts: {
7
+ appId?: string
8
+ output?: string
9
+ } = {}): Promise<void> {
10
+ let groups: BetaGroup[]
11
+ if (opts.appId) {
12
+ groups = await ascFetchAll<BetaGroup>(`/v1/apps/${opts.appId}/betaGroups`)
13
+ } else {
14
+ groups = await ascFetchAll<BetaGroup>("/v1/betaGroups")
15
+ }
16
+
17
+ const fmt = detectFormat(opts.output)
18
+ if (fmt === "json") {
19
+ printJSON(groups.map((g) => ({ id: g.id, ...g.attributes })))
20
+ return
21
+ }
22
+
23
+ printTable(
24
+ ["ID", "Name", "Internal", "Public Link"],
25
+ groups.map((g) => [
26
+ g.id,
27
+ g.attributes.name,
28
+ g.attributes.isInternalGroup ? "yes" : "no",
29
+ g.attributes.publicLink ?? "-",
30
+ ]),
31
+ `Beta Groups (${groups.length})`,
32
+ )
33
+ }
34
+
35
+ export async function testflightTestersList(opts: {
36
+ groupId: string
37
+ output?: string
38
+ }): Promise<void> {
39
+ const testers = await ascFetchAll<BetaTester>(
40
+ `/v1/betaGroups/${opts.groupId}/betaTesters`,
41
+ {
42
+ params: { "fields[betaTesters]": "firstName,lastName,email,inviteType,state" },
43
+ },
44
+ )
45
+
46
+ const fmt = detectFormat(opts.output)
47
+ if (fmt === "json") {
48
+ printJSON(testers.map((t) => ({ id: t.id, ...t.attributes })))
49
+ return
50
+ }
51
+
52
+ printTable(
53
+ ["ID", "Name", "Email", "State"],
54
+ testers.map((t) => [
55
+ t.id,
56
+ [t.attributes.firstName, t.attributes.lastName].filter(Boolean).join(" ") || "-",
57
+ t.attributes.email,
58
+ t.attributes.state ?? "-",
59
+ ]),
60
+ `Testers (${testers.length})`,
61
+ )
62
+ }
63
+
64
+ export async function testflightTestersAdd(opts: {
65
+ groupId: string
66
+ email?: string
67
+ firstName?: string
68
+ lastName?: string
69
+ }): Promise<void> {
70
+ let email = opts.email
71
+ if (!email) {
72
+ const val = await p.text({ message: "Tester email:" })
73
+ if (p.isCancel(val)) { p.cancel("Cancelled"); process.exit(0) }
74
+ email = val as string
75
+ }
76
+
77
+ // Create tester + add to group
78
+ const res = await ascFetch<BetaTester>("/v1/betaTesters", {
79
+ method: "POST",
80
+ body: {
81
+ data: {
82
+ type: "betaTesters",
83
+ attributes: {
84
+ email,
85
+ firstName: opts.firstName,
86
+ lastName: opts.lastName,
87
+ },
88
+ relationships: {
89
+ betaGroups: {
90
+ data: [{ type: "betaGroups", id: opts.groupId }],
91
+ },
92
+ },
93
+ },
94
+ },
95
+ })
96
+ printSuccess(`Tester ${email} added (ID: ${res.data.id})`)
97
+ }
98
+
99
+ export async function testflightTestersRemove(opts: {
100
+ groupId: string
101
+ testerId: string
102
+ }): Promise<void> {
103
+ await ascFetch(`/v1/betaGroups/${opts.groupId}/relationships/betaTesters`, {
104
+ method: "DELETE",
105
+ body: {
106
+ data: [{ type: "betaTesters", id: opts.testerId }],
107
+ },
108
+ })
109
+ printSuccess(`Tester ${opts.testerId} removed from group`)
110
+ }
@@ -0,0 +1,76 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import type { AppStoreVersion, Platform } from "../api/types.ts"
3
+ import { detectFormat, formatDate, printJSON, printSuccess, printTable } from "../utils/output.ts"
4
+
5
+ export async function versionsList(opts: {
6
+ appId: string
7
+ platform?: string
8
+ output?: string
9
+ }): Promise<void> {
10
+ const params: Record<string, string> = {
11
+ "fields[appStoreVersions]": "platform,versionString,appStoreState,createdDate",
12
+ }
13
+ if (opts.platform) params["filter[platform]"] = opts.platform.toUpperCase()
14
+
15
+ const versions = await ascFetchAll<AppStoreVersion>(
16
+ `/v1/apps/${opts.appId}/appStoreVersions`,
17
+ { params },
18
+ )
19
+
20
+ const fmt = detectFormat(opts.output)
21
+ if (fmt === "json") {
22
+ printJSON(versions.map((v) => ({ id: v.id, ...v.attributes })))
23
+ return
24
+ }
25
+
26
+ printTable(
27
+ ["ID", "Version", "Platform", "State", "Created"],
28
+ versions.map((v) => [
29
+ v.id,
30
+ v.attributes.versionString,
31
+ v.attributes.platform,
32
+ v.attributes.appStoreState,
33
+ formatDate(v.attributes.createdDate),
34
+ ]),
35
+ `Versions (${versions.length})`,
36
+ )
37
+ }
38
+
39
+ export async function versionsCreate(opts: {
40
+ appId: string
41
+ version: string
42
+ platform: string
43
+ }): Promise<void> {
44
+ const platform = opts.platform.toUpperCase() as Platform
45
+ const res = await ascFetch<AppStoreVersion>("/v1/appStoreVersions", {
46
+ method: "POST",
47
+ body: {
48
+ data: {
49
+ type: "appStoreVersions",
50
+ attributes: {
51
+ platform,
52
+ versionString: opts.version,
53
+ },
54
+ relationships: {
55
+ app: { data: { type: "apps", id: opts.appId } },
56
+ },
57
+ },
58
+ },
59
+ })
60
+ printSuccess(`Created version ${opts.version} (${platform}) — ID: ${res.data.id}`)
61
+ }
62
+
63
+ export async function versionsSubmit(versionId: string): Promise<void> {
64
+ await ascFetch("/v1/appStoreVersionSubmissions", {
65
+ method: "POST",
66
+ body: {
67
+ data: {
68
+ type: "appStoreVersionSubmissions",
69
+ relationships: {
70
+ appStoreVersion: { data: { type: "appStoreVersions", id: versionId } },
71
+ },
72
+ },
73
+ },
74
+ })
75
+ printSuccess(`Version ${versionId} submitted for review`)
76
+ }