@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,327 @@
1
+ import * as p from "@clack/prompts"
2
+ import chalk from "chalk"
3
+ import { existsSync, readFileSync } from "fs"
4
+ import {
5
+ addOrUpdateProfile,
6
+ credentialsPath,
7
+ getActiveProfile,
8
+ loadCredentials,
9
+ removeProfile,
10
+ saveCredentials,
11
+ setActiveProfile,
12
+ updateProfile,
13
+ type Profile,
14
+ } from "../auth/credentials.ts"
15
+ import { deleteFromKeychain, generateJWT, saveToKeychain } from "../auth/jwt.ts"
16
+ import { printError, printSuccess, printTable } from "../utils/output.ts"
17
+
18
+ export async function authLogin(args: {
19
+ name?: string
20
+ keyId?: string
21
+ issuerId?: string
22
+ privateKey?: string
23
+ bypassKeychain?: boolean
24
+ }): Promise<void> {
25
+ p.intro(chalk.bold("App Store Connect — Login"))
26
+
27
+ let name = args.name
28
+ let keyId = args.keyId
29
+ let issuerId = args.issuerId
30
+ let privateKey = args.privateKey
31
+
32
+ // Interactive prompts for missing values
33
+ if (!keyId) {
34
+ const val = await p.text({ message: "Key ID (from App Store Connect API Keys):" })
35
+ if (p.isCancel(val)) { p.cancel("Cancelled"); process.exit(0) }
36
+ keyId = val as string
37
+ }
38
+
39
+ if (!issuerId) {
40
+ const val = await p.text({ message: "Issuer ID:" })
41
+ if (p.isCancel(val)) { p.cancel("Cancelled"); process.exit(0) }
42
+ issuerId = val as string
43
+ }
44
+
45
+ if (!privateKey) {
46
+ const val = await p.text({
47
+ message: "Path to private key (.p8 file):",
48
+ placeholder: "~/.asc/AuthKey_XXXXXX.p8",
49
+ })
50
+ if (p.isCancel(val)) { p.cancel("Cancelled"); process.exit(0) }
51
+ privateKey = val as string
52
+ }
53
+
54
+ if (!name) {
55
+ const existing = loadCredentials()
56
+ const defaultName = existing.profiles.length === 0 ? "default" : undefined
57
+ const val = await p.text({
58
+ message: "Profile name:",
59
+ placeholder: defaultName ?? "work",
60
+ defaultValue: defaultName,
61
+ })
62
+ if (p.isCancel(val)) { p.cancel("Cancelled"); process.exit(0) }
63
+ name = (val as string) || "default"
64
+ }
65
+
66
+ // Expand ~ in path
67
+ const expandedPath = privateKey.trim().replace(/^~/, process.env["HOME"] ?? "")
68
+ if (!existsSync(expandedPath)) {
69
+ printError(new Error(`Private key file not found: ${expandedPath}`))
70
+ process.exit(1)
71
+ }
72
+
73
+ const spinner = p.spinner()
74
+ spinner.start("Validating credentials…")
75
+
76
+ const profile: Profile = {
77
+ name,
78
+ keyId,
79
+ issuerId,
80
+ privateKeyPath: privateKey,
81
+ isDefault: loadCredentials().profiles.length === 0,
82
+ }
83
+
84
+ // Try storing in keychain
85
+ let storedInKeychain = false
86
+ if (!args.bypassKeychain) {
87
+ const pem = readFileSync(expandedPath, "utf8")
88
+ storedInKeychain = saveToKeychain(name, pem)
89
+ if (storedInKeychain) {
90
+ // Don't need path if stored in keychain
91
+ profile.storedInKeychain = true
92
+ }
93
+ }
94
+
95
+ // Validate by generating JWT
96
+ try {
97
+ await generateJWT(profile)
98
+ spinner.stop("Credentials validated")
99
+ } catch (err) {
100
+ spinner.stop("Validation failed")
101
+ printError(err)
102
+ process.exit(1)
103
+ }
104
+
105
+ addOrUpdateProfile(profile)
106
+
107
+ const keychainNote = storedInKeychain ? chalk.dim(" (private key stored in Keychain)") : ""
108
+ printSuccess(`Profile "${chalk.cyan(name)}" saved${keychainNote}`)
109
+ console.log(chalk.dim(` Credentials: ${credentialsPath()}`))
110
+ p.outro("You're all set! Run " + chalk.cyan("asc apps list") + " to get started.")
111
+ }
112
+
113
+ export function authList(): void {
114
+ const creds = loadCredentials()
115
+ if (creds.profiles.length === 0) {
116
+ console.log(chalk.dim("No profiles configured. Run: asc auth login"))
117
+ return
118
+ }
119
+ printTable(
120
+ ["Profile", "Key ID", "Issuer ID", "Active"],
121
+ creds.profiles.map((p) => [
122
+ p.name,
123
+ p.keyId,
124
+ p.issuerId,
125
+ creds.activeProfile === p.name ? chalk.green("✓") : "",
126
+ ]),
127
+ "Profiles",
128
+ )
129
+ }
130
+
131
+ export async function authUse(name?: string): Promise<void> {
132
+ const creds = loadCredentials()
133
+ if (creds.profiles.length === 0) {
134
+ console.log(chalk.dim("No profiles. Run: asc auth login"))
135
+ return
136
+ }
137
+
138
+ let profileName = name
139
+ if (!profileName) {
140
+ const choice = await p.select({
141
+ message: "Select profile to activate:",
142
+ options: creds.profiles.map((pr) => ({
143
+ value: pr.name,
144
+ label: pr.name,
145
+ hint: pr.name === creds.activeProfile ? "active" : undefined,
146
+ })),
147
+ })
148
+ if (p.isCancel(choice)) { p.cancel("Cancelled"); process.exit(0) }
149
+ profileName = choice as string
150
+ }
151
+
152
+ if (!setActiveProfile(profileName)) {
153
+ printError(new Error(`Profile "${profileName}" not found`))
154
+ process.exit(1)
155
+ }
156
+ printSuccess(`Active profile set to "${chalk.cyan(profileName)}"`)
157
+ }
158
+
159
+ export async function authStatus(opts: { validate?: boolean } = {}): Promise<void> {
160
+ const profile = getActiveProfile()
161
+ if (!profile) {
162
+ console.log(chalk.dim("No active profile. Run: asc auth login"))
163
+ return
164
+ }
165
+
166
+ console.log(chalk.bold("Active profile:"), chalk.cyan(profile.name))
167
+ console.log(" Key ID: ", profile.keyId)
168
+ console.log(" Issuer ID: ", profile.issuerId)
169
+ if (profile.privateKeyPath) {
170
+ console.log(" Key path: ", profile.privateKeyPath)
171
+ }
172
+ if (profile.storedInKeychain) {
173
+ console.log(" Keychain: ", chalk.green("yes"))
174
+ }
175
+
176
+ if (opts.validate) {
177
+ const spinner = p.spinner()
178
+ spinner.start("Validating token…")
179
+ try {
180
+ await generateJWT(profile)
181
+ spinner.stop(chalk.green("Token generated successfully"))
182
+ } catch (err) {
183
+ spinner.stop(chalk.red("Token generation failed"))
184
+ printError(err)
185
+ }
186
+ }
187
+ }
188
+
189
+ export async function authLogout(name?: string): Promise<void> {
190
+ const creds = loadCredentials()
191
+ let profileName = name
192
+
193
+ if (!profileName) {
194
+ if (creds.profiles.length === 0) {
195
+ console.log(chalk.dim("No profiles configured."))
196
+ return
197
+ }
198
+ const choice = await p.select({
199
+ message: "Select profile to remove:",
200
+ options: creds.profiles.map((pr) => ({
201
+ value: pr.name,
202
+ label: pr.name,
203
+ hint: pr.name === creds.activeProfile ? "active" : undefined,
204
+ })),
205
+ })
206
+ if (p.isCancel(choice)) { p.cancel("Cancelled"); process.exit(0) }
207
+ profileName = choice as string
208
+ }
209
+
210
+ const confirm = await p.confirm({
211
+ message: `Remove profile "${profileName}"?`,
212
+ })
213
+ if (p.isCancel(confirm) || !confirm) {
214
+ p.cancel("Cancelled")
215
+ return
216
+ }
217
+
218
+ deleteFromKeychain(profileName)
219
+ if (removeProfile(profileName)) {
220
+ printSuccess(`Profile "${chalk.cyan(profileName)}" removed`)
221
+ } else {
222
+ printError(new Error(`Profile "${profileName}" not found`))
223
+ }
224
+ }
225
+
226
+ // ---- Profile management (dedicated subcommand) ----
227
+
228
+ export function profileView(name?: string): void {
229
+ const creds = loadCredentials()
230
+ const profileName = name ?? creds.activeProfile
231
+ const profile = creds.profiles.find((p) => p.name === profileName)
232
+ if (!profile) {
233
+ printError(new Error(`Profile "${profileName}" not found. Run: asc auth list`))
234
+ process.exit(1)
235
+ }
236
+ const isActive = creds.activeProfile === profile.name
237
+ console.log(chalk.bold(`Profile: ${chalk.cyan(profile.name)}`) + (isActive ? chalk.green(" ← active") : ""))
238
+ console.log(` Key ID: ${profile.keyId}`)
239
+ console.log(` Issuer ID: ${profile.issuerId}`)
240
+ if (profile.privateKeyPath) {
241
+ console.log(` Key path: ${profile.privateKeyPath}`)
242
+ }
243
+ console.log(` In Keychain: ${profile.storedInKeychain ? chalk.green("yes") : "no"}`)
244
+ if (profile.vendorNumber) {
245
+ console.log(` Vendor Number: ${profile.vendorNumber}`)
246
+ }
247
+ console.log(chalk.dim(`\n Credentials file: ${credentialsPath()}`))
248
+ }
249
+
250
+ export async function profileUpdate(
251
+ name: string | undefined,
252
+ updates: {
253
+ keyId?: string
254
+ issuerId?: string
255
+ privateKey?: string
256
+ vendorNumber?: string
257
+ bypassKeychain?: boolean
258
+ renameTo?: string
259
+ },
260
+ ): Promise<void> {
261
+ const creds = loadCredentials()
262
+ const resolvedName = name ?? creds.activeProfile
263
+ const existing = creds.profiles.find((p) => p.name === resolvedName)
264
+ if (!existing) {
265
+ printError(new Error(`Profile "${resolvedName}" not found. Run: asc auth list`))
266
+ process.exit(1)
267
+ }
268
+
269
+ const changes: Partial<Omit<Profile, "name">> = {}
270
+ if (updates.keyId) changes.keyId = updates.keyId
271
+ if (updates.issuerId) changes.issuerId = updates.issuerId
272
+ if (updates.vendorNumber) changes.vendorNumber = updates.vendorNumber
273
+
274
+ if (updates.privateKey) {
275
+ const expandedPath = updates.privateKey.trim().replace(/^~/, process.env["HOME"] ?? "")
276
+ if (!existsSync(expandedPath)) {
277
+ printError(new Error(`Private key file not found: ${expandedPath}`))
278
+ process.exit(1)
279
+ }
280
+ changes.privateKeyPath = updates.privateKey
281
+
282
+ if (!updates.bypassKeychain) {
283
+ const pem = readFileSync(expandedPath, "utf8")
284
+ const stored = saveToKeychain(resolvedName, pem)
285
+ if (stored) {
286
+ changes.storedInKeychain = true
287
+ }
288
+ }
289
+ }
290
+
291
+ if (updates.renameTo) {
292
+ if (creds.profiles.find((p) => p.name === updates.renameTo)) {
293
+ printError(new Error(`Profile "${updates.renameTo}" already exists`))
294
+ process.exit(1)
295
+ }
296
+ // Move keychain entry to new name
297
+ if (existing.storedInKeychain) {
298
+ const { loadFromKeychain, saveToKeychain: save, deleteFromKeychain } = await import("../auth/jwt.ts")
299
+ const pem = loadFromKeychain(resolvedName)
300
+ if (pem) {
301
+ save(updates.renameTo, pem)
302
+ deleteFromKeychain(resolvedName)
303
+ }
304
+ }
305
+ // Rename in credentials file
306
+ const idx = creds.profiles.findIndex((p) => p.name === resolvedName)
307
+ creds.profiles[idx] = { ...creds.profiles[idx]!, ...changes, name: updates.renameTo }
308
+ if (creds.activeProfile === resolvedName) creds.activeProfile = updates.renameTo
309
+ saveCredentials(creds)
310
+ printSuccess(`Profile "${chalk.cyan(resolvedName)}" renamed to "${chalk.cyan(updates.renameTo)}"`)
311
+ profileView(updates.renameTo)
312
+ return
313
+ }
314
+
315
+ if (Object.keys(changes).length === 0) {
316
+ console.log(chalk.dim("Nothing to update. Provide at least one flag."))
317
+ return
318
+ }
319
+
320
+ updateProfile(resolvedName, changes)
321
+ printSuccess(`Profile "${chalk.cyan(resolvedName)}" updated`)
322
+ profileView(resolvedName)
323
+ }
324
+
325
+ export async function profileDelete(name?: string): Promise<void> {
326
+ return authLogout(name)
327
+ }
@@ -0,0 +1,52 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import { detectFormat, printJSON, printTable } from "../utils/output.ts"
3
+
4
+ export async function availabilityGet(appId: string, opts: { output?: string } = {}): Promise<void> {
5
+ const result = await ascFetch<{
6
+ data: { id: string; attributes: { availableInNewTerritories?: boolean } }
7
+ included?: Array<{ type: string; id: string; attributes?: { currency?: string; name?: string } }>
8
+ }>(`/v1/apps/${appId}/appAvailabilityV2`, {
9
+ params: {
10
+ include: "availableTerritories",
11
+ "fields[territories]": "currency,name",
12
+ },
13
+ })
14
+ const body = result.data
15
+ const fmt = detectFormat(opts.output)
16
+ if (fmt === "json") { printJSON(body); return }
17
+ console.log(`App ID: ${appId}`)
18
+ console.log(`Available in new territories: ${body.data.attributes?.availableInNewTerritories ?? "-"}`)
19
+ const territories = (body.included ?? []).filter(r => r.type === "territories")
20
+ if (territories.length) {
21
+ console.log(`\nEnabled Territories (${territories.length}):`)
22
+ printTable(
23
+ ["Territory Code", "Name", "Currency"],
24
+ territories.map(t => [t.id, t.attributes?.name ?? "-", t.attributes?.currency ?? "-"]),
25
+ )
26
+ }
27
+ }
28
+
29
+ export async function availabilityTerritoriesSet(opts: {
30
+ appId: string
31
+ territories: string[]
32
+ availableInNewTerritories?: boolean
33
+ }): Promise<void> {
34
+ await ascFetch(`/v1/appAvailabilities`, {
35
+ method: "POST",
36
+ body: {
37
+ data: {
38
+ type: "appAvailabilities",
39
+ attributes: {
40
+ availableInNewTerritories: opts.availableInNewTerritories ?? true,
41
+ },
42
+ relationships: {
43
+ app: { data: { type: "apps", id: opts.appId } },
44
+ availableTerritories: {
45
+ data: opts.territories.map(t => ({ type: "territories", id: t })),
46
+ },
47
+ },
48
+ },
49
+ },
50
+ })
51
+ console.log(`Availability updated for app ${opts.appId}`)
52
+ }
@@ -0,0 +1,145 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import { detectFormat, formatDate, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
3
+
4
+ // ---- Beta App Review Submissions ----
5
+
6
+ export async function betaReviewSubmissionCreate(buildId: string): Promise<void> {
7
+ const result = await ascFetch<{ data: { id: string; attributes: { betaReviewState?: string } } }>(
8
+ "/v1/betaAppReviewSubmissions",
9
+ {
10
+ method: "POST",
11
+ body: {
12
+ data: {
13
+ type: "betaAppReviewSubmissions",
14
+ relationships: {
15
+ build: { data: { type: "builds", id: buildId } },
16
+ },
17
+ },
18
+ },
19
+ },
20
+ )
21
+ const state = result.data.data.attributes.betaReviewState
22
+ printSuccess(`Beta review submitted for build ${buildId} (state: ${state ?? "WAITING_FOR_REVIEW"})`)
23
+ }
24
+
25
+ export async function betaReviewSubmissionGet(buildId: string, opts: { output?: string } = {}): Promise<void> {
26
+ const items = await ascFetchAll<{
27
+ id: string
28
+ attributes: { betaReviewState?: string }
29
+ relationships?: { build?: { data?: { id: string } } }
30
+ }>("/v1/betaAppReviewSubmissions", {
31
+ params: { "filter[build]": buildId },
32
+ })
33
+ const fmt = detectFormat(opts.output)
34
+ if (fmt === "json") { printJSON(items); return }
35
+ if (items.length === 0) {
36
+ console.log(`No beta review submission found for build ${buildId}`)
37
+ return
38
+ }
39
+ for (const s of items) {
40
+ console.log(`Submission ID: ${s.id}`)
41
+ console.log(`State: ${s.attributes.betaReviewState ?? "-"}`)
42
+ }
43
+ }
44
+
45
+ // ---- Beta App Review Details ----
46
+
47
+ export async function betaReviewDetailGet(appId: string, opts: { output?: string } = {}): Promise<void> {
48
+ const result = await ascFetch<{
49
+ data: {
50
+ id: string
51
+ attributes: {
52
+ contactFirstName?: string
53
+ contactLastName?: string
54
+ contactPhone?: string
55
+ contactEmail?: string
56
+ demoAccountName?: string
57
+ demoAccountPassword?: string
58
+ demoAccountRequired?: boolean
59
+ notes?: string
60
+ }
61
+ }
62
+ }>(`/v1/apps/${appId}/betaAppReviewDetail`)
63
+ const fmt = detectFormat(opts.output)
64
+ if (fmt === "json") { printJSON(result.data.data); return }
65
+ const a = result.data.data.attributes
66
+ console.log(`Review Detail ID: ${result.data.data.id}`)
67
+ console.log(`Contact: ${a.contactFirstName ?? ""} ${a.contactLastName ?? ""}`)
68
+ console.log(`Contact Email: ${a.contactEmail ?? "-"}`)
69
+ console.log(`Contact Phone: ${a.contactPhone ?? "-"}`)
70
+ console.log(`Demo Account: ${a.demoAccountRequired ? "Required" : "Not required"}`)
71
+ if (a.demoAccountRequired) {
72
+ console.log(`Demo Username: ${a.demoAccountName ?? "-"}`)
73
+ }
74
+ if (a.notes) console.log(`Notes: ${a.notes}`)
75
+ }
76
+
77
+ export async function betaReviewDetailUpdate(opts: {
78
+ appId: string
79
+ contactFirstName?: string
80
+ contactLastName?: string
81
+ contactEmail?: string
82
+ contactPhone?: string
83
+ demoAccountName?: string
84
+ demoAccountPassword?: string
85
+ demoAccountRequired?: boolean
86
+ notes?: string
87
+ }): Promise<void> {
88
+ // First get the detail ID
89
+ const current = await ascFetch<{ data: { id: string } }>(`/v1/apps/${opts.appId}/betaAppReviewDetail`)
90
+ const detailId = current.data.data.id
91
+ const { appId, ...attributes } = opts
92
+ await ascFetch(`/v1/betaAppReviewDetails/${detailId}`, {
93
+ method: "PATCH",
94
+ body: {
95
+ data: {
96
+ type: "betaAppReviewDetails",
97
+ id: detailId,
98
+ attributes,
99
+ },
100
+ },
101
+ })
102
+ printSuccess(`Beta review details updated for app ${appId}`)
103
+ }
104
+
105
+ // ---- Build Beta Details ----
106
+
107
+ export async function buildBetaDetailUpdate(opts: {
108
+ buildId: string
109
+ autoNotifyEnabled?: boolean
110
+ }): Promise<void> {
111
+ const current = await ascFetch<{
112
+ data: { id: string }
113
+ }>(`/v1/builds/${opts.buildId}/buildBetaDetail`)
114
+ const detailId = current.data.data.id
115
+ await ascFetch(`/v1/buildBetaDetails/${detailId}`, {
116
+ method: "PATCH",
117
+ body: {
118
+ data: {
119
+ type: "buildBetaDetails",
120
+ id: detailId,
121
+ attributes: { autoNotifyEnabled: opts.autoNotifyEnabled },
122
+ },
123
+ },
124
+ })
125
+ printSuccess(`Build beta detail updated for build ${opts.buildId}`)
126
+ }
127
+
128
+ // ---- Beta Build Localizations ----
129
+
130
+ export async function betaBuildLocalizationsList(opts: {
131
+ buildId: string
132
+ output?: string
133
+ }): Promise<void> {
134
+ const items = await ascFetchAll<{
135
+ id: string
136
+ attributes: { locale?: string; whatsNew?: string }
137
+ }>(`/v1/builds/${opts.buildId}/betaBuildLocalizations`)
138
+ const fmt = detectFormat(opts.output)
139
+ if (fmt === "json") { printJSON(items); return }
140
+ printTable(
141
+ ["ID", "Locale", "What's New"],
142
+ items.map(l => [l.id, l.attributes.locale ?? "-", truncate(l.attributes.whatsNew ?? "-", 60)]),
143
+ `Beta Build Localizations (${items.length})`,
144
+ )
145
+ }
@@ -0,0 +1,97 @@
1
+ import { ascFetch, ascFetchAll } from "../api/client.ts"
2
+ import type { Build } from "../api/types.ts"
3
+ import { detectFormat, formatDate, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
4
+
5
+ export async function buildsList(opts: {
6
+ appId?: string
7
+ version?: string
8
+ platform?: string
9
+ output?: string
10
+ } = {}): Promise<void> {
11
+ const params: Record<string, string> = {
12
+ "fields[builds]": "version,uploadedDate,processingState,expired",
13
+ "limit": "200",
14
+ }
15
+ if (opts.appId) params["filter[app]"] = opts.appId
16
+ if (opts.version) params["filter[version]"] = opts.version
17
+ if (opts.platform) params["filter[platform]"] = opts.platform.toUpperCase()
18
+
19
+ const builds = await ascFetchAll<Build>("/v1/builds", { params })
20
+
21
+ const fmt = detectFormat(opts.output)
22
+ if (fmt === "json") {
23
+ printJSON(builds.map((b) => ({ id: b.id, ...b.attributes })))
24
+ return
25
+ }
26
+
27
+ printTable(
28
+ ["ID", "Version", "Uploaded", "State", "Expired"],
29
+ builds.map((b) => [
30
+ b.id,
31
+ b.attributes.version,
32
+ formatDate(b.attributes.uploadedDate),
33
+ b.attributes.processingState ?? "-",
34
+ b.attributes.expired ? "yes" : "no",
35
+ ]),
36
+ `Builds (${builds.length})`,
37
+ )
38
+ }
39
+
40
+ export async function buildsGet(buildId: string, opts: { output?: string } = {}): Promise<void> {
41
+ const res = await ascFetch<Build>(`/v1/builds/${buildId}`)
42
+ const b = res.data
43
+
44
+ const fmt = detectFormat(opts.output)
45
+ if (fmt === "json") {
46
+ printJSON({ id: b.id, ...b.attributes })
47
+ return
48
+ }
49
+
50
+ console.log(`ID: ${b.id}`)
51
+ console.log(`Version: ${b.attributes.version}`)
52
+ console.log(`Uploaded: ${formatDate(b.attributes.uploadedDate)}`)
53
+ console.log(`Processing State: ${b.attributes.processingState ?? "-"}`)
54
+ console.log(`Expired: ${b.attributes.expired ? "yes" : "no"}`)
55
+ console.log(`Min OS: ${b.attributes.minOsVersion ?? "-"}`)
56
+ }
57
+
58
+ export async function buildsUpdateBetaNotes(opts: {
59
+ buildId: string
60
+ locale: string
61
+ notes: string
62
+ }): Promise<void> {
63
+ // First get existing localizations
64
+ const res = await ascFetchAll<{ type: string; id: string; attributes: { locale: string; whatsNew?: string } }>(
65
+ `/v1/builds/${opts.buildId}/betaBuildLocalizations`,
66
+ )
67
+
68
+ const existing = res.find((l) => l.attributes.locale === opts.locale)
69
+
70
+ if (existing) {
71
+ await ascFetch(`/v1/betaBuildLocalizations/${existing.id}`, {
72
+ method: "PATCH",
73
+ body: {
74
+ data: {
75
+ type: "betaBuildLocalizations",
76
+ id: existing.id,
77
+ attributes: { whatsNew: opts.notes },
78
+ },
79
+ },
80
+ })
81
+ printSuccess(`Beta notes updated for locale "${opts.locale}"`)
82
+ } else {
83
+ await ascFetch("/v1/betaBuildLocalizations", {
84
+ method: "POST",
85
+ body: {
86
+ data: {
87
+ type: "betaBuildLocalizations",
88
+ attributes: { locale: opts.locale, whatsNew: opts.notes },
89
+ relationships: {
90
+ build: { data: { type: "builds", id: opts.buildId } },
91
+ },
92
+ },
93
+ },
94
+ })
95
+ printSuccess(`Beta notes created for locale "${opts.locale}"`)
96
+ }
97
+ }