@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.
- package/README.md +299 -0
- package/bun.lock +231 -0
- package/dist/index.js +179 -0
- package/docs/API_COVERAGE.md +355 -0
- package/docs/openapi/latest.json +230597 -0
- package/docs/openapi/paths.txt +1208 -0
- package/openapi-ts.config.ts +25 -0
- package/package.json +32 -0
- package/scripts/gen-paths-index.py +24 -0
- package/src/api/client.ts +132 -0
- package/src/api/generated/client/client.gen.ts +298 -0
- package/src/api/generated/client/index.ts +25 -0
- package/src/api/generated/client/types.gen.ts +214 -0
- package/src/api/generated/client/utils.gen.ts +316 -0
- package/src/api/generated/client.gen.ts +16 -0
- package/src/api/generated/core/auth.gen.ts +41 -0
- package/src/api/generated/core/bodySerializer.gen.ts +82 -0
- package/src/api/generated/core/params.gen.ts +169 -0
- package/src/api/generated/core/pathSerializer.gen.ts +171 -0
- package/src/api/generated/core/queryKeySerializer.gen.ts +117 -0
- package/src/api/generated/core/serverSentEvents.gen.ts +242 -0
- package/src/api/generated/core/types.gen.ts +104 -0
- package/src/api/generated/core/utils.gen.ts +140 -0
- package/src/api/generated/index.ts +4 -0
- package/src/api/generated/sdk.gen.ts +11701 -0
- package/src/api/generated/types.gen.ts +92035 -0
- package/src/api/hey-api-client.ts +20 -0
- package/src/api/types.ts +160 -0
- package/src/auth/credentials.ts +125 -0
- package/src/auth/jwt.ts +118 -0
- package/src/commands/app-info.ts +110 -0
- package/src/commands/apps.ts +44 -0
- package/src/commands/auth.ts +327 -0
- package/src/commands/availability.ts +52 -0
- package/src/commands/beta-review.ts +145 -0
- package/src/commands/builds.ts +97 -0
- package/src/commands/game-center.ts +114 -0
- package/src/commands/iap.ts +105 -0
- package/src/commands/metadata.ts +81 -0
- package/src/commands/pricing.ts +110 -0
- package/src/commands/reports.ts +116 -0
- package/src/commands/reviews.ts +93 -0
- package/src/commands/screenshots.ts +139 -0
- package/src/commands/signing.ts +214 -0
- package/src/commands/subscriptions.ts +144 -0
- package/src/commands/testflight.ts +110 -0
- package/src/commands/versions.ts +76 -0
- package/src/commands/xcode-cloud.ts +207 -0
- package/src/index.ts +1661 -0
- package/src/utils/help-spec.ts +835 -0
- package/src/utils/output.ts +79 -0
- package/tests/auth.test.ts +105 -0
- package/tests/client.test.ts +22 -0
- package/tests/output.test.ts +36 -0
- 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
|
+
}
|