@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,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configures the @hey-api/client-fetch generated client with JWT authentication.
|
|
3
|
+
* Import this file before using any functions from src/api/generated/sdk.gen.ts
|
|
4
|
+
* to ensure all SDK calls include the Authorization header.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { client } from "./generated/client.gen.ts"
|
|
8
|
+
import { getActiveProfile } from "../auth/credentials.ts"
|
|
9
|
+
import { generateJWT } from "../auth/jwt.ts"
|
|
10
|
+
|
|
11
|
+
client.interceptors.request.use(async (request) => {
|
|
12
|
+
const profile = getActiveProfile()
|
|
13
|
+
if (profile) {
|
|
14
|
+
const token = await generateJWT(profile)
|
|
15
|
+
request.headers.set("Authorization", `Bearer ${token}`)
|
|
16
|
+
}
|
|
17
|
+
return request
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export { client }
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Core App Store Connect API types (JSON:API format)
|
|
2
|
+
|
|
3
|
+
export interface JsonApiResource<T> {
|
|
4
|
+
type: string
|
|
5
|
+
id: string
|
|
6
|
+
attributes: T
|
|
7
|
+
relationships?: Record<string, JsonApiRelationship>
|
|
8
|
+
links?: Record<string, string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface JsonApiRelationship {
|
|
12
|
+
data?: { type: string; id: string } | Array<{ type: string; id: string }>
|
|
13
|
+
links?: { self?: string; related?: string }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface JsonApiResponse<T> {
|
|
17
|
+
data: T
|
|
18
|
+
included?: JsonApiResource<unknown>[]
|
|
19
|
+
links?: { self?: string; next?: string; first?: string; last?: string }
|
|
20
|
+
meta?: { paging?: { total?: number; limit?: number } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface JsonApiError {
|
|
24
|
+
id?: string
|
|
25
|
+
status: string
|
|
26
|
+
code: string
|
|
27
|
+
title: string
|
|
28
|
+
detail?: string
|
|
29
|
+
source?: { pointer?: string; parameter?: string }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface JsonApiErrorResponse {
|
|
33
|
+
errors: JsonApiError[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- Apps ----
|
|
37
|
+
export interface AppAttributes {
|
|
38
|
+
name: string
|
|
39
|
+
bundleId: string
|
|
40
|
+
sku: string
|
|
41
|
+
primaryLocale: string
|
|
42
|
+
isOrEverWasMadeForKids: boolean
|
|
43
|
+
subscriptionStatusUrl?: string
|
|
44
|
+
contentRightsDeclaration?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type App = JsonApiResource<AppAttributes>
|
|
48
|
+
|
|
49
|
+
// ---- App Store Versions ----
|
|
50
|
+
export interface AppStoreVersionAttributes {
|
|
51
|
+
platform: "IOS" | "MAC_OS" | "TV_OS" | "VISION_OS"
|
|
52
|
+
versionString: string
|
|
53
|
+
appStoreState:
|
|
54
|
+
| "ACCEPTED"
|
|
55
|
+
| "DEVELOPER_REMOVED_FROM_SALE"
|
|
56
|
+
| "DEVELOPER_REJECTED"
|
|
57
|
+
| "IN_REVIEW"
|
|
58
|
+
| "INVALID_BINARY"
|
|
59
|
+
| "METADATA_REJECTED"
|
|
60
|
+
| "PENDING_APPLE_RELEASE"
|
|
61
|
+
| "PENDING_CONTRACT"
|
|
62
|
+
| "PENDING_DEVELOPER_RELEASE"
|
|
63
|
+
| "PREPARE_FOR_SUBMISSION"
|
|
64
|
+
| "PREORDER_READY_FOR_SALE"
|
|
65
|
+
| "PROCESSING_FOR_APP_STORE"
|
|
66
|
+
| "READY_FOR_DISTRIBUTION"
|
|
67
|
+
| "READY_FOR_REVIEW"
|
|
68
|
+
| "READY_FOR_SALE"
|
|
69
|
+
| "REJECTED"
|
|
70
|
+
| "REMOVED_FROM_SALE"
|
|
71
|
+
| "WAITING_FOR_EXPORT_COMPLIANCE"
|
|
72
|
+
| "WAITING_FOR_REVIEW"
|
|
73
|
+
releaseType?: "MANUAL" | "AFTER_APPROVAL" | "SCHEDULED"
|
|
74
|
+
createdDate?: string
|
|
75
|
+
downloadable?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type AppStoreVersion = JsonApiResource<AppStoreVersionAttributes>
|
|
79
|
+
|
|
80
|
+
// ---- Builds ----
|
|
81
|
+
export interface BuildAttributes {
|
|
82
|
+
version: string
|
|
83
|
+
uploadedDate?: string
|
|
84
|
+
expirationDate?: string
|
|
85
|
+
expired?: boolean
|
|
86
|
+
minOsVersion?: string
|
|
87
|
+
lsMinimumSystemVersion?: string
|
|
88
|
+
computedMinMacOsVersion?: string
|
|
89
|
+
iconAssetToken?: { templateUrl: string }
|
|
90
|
+
processingState?: "PROCESSING" | "FAILED" | "INVALID" | "VALID"
|
|
91
|
+
buildAudienceType?: string
|
|
92
|
+
usesNonExemptEncryption?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type Build = JsonApiResource<BuildAttributes>
|
|
96
|
+
|
|
97
|
+
// ---- Beta Groups ----
|
|
98
|
+
export interface BetaGroupAttributes {
|
|
99
|
+
name: string
|
|
100
|
+
createdDate?: string
|
|
101
|
+
isInternalGroup: boolean
|
|
102
|
+
hasAccessToAllBuilds?: boolean
|
|
103
|
+
publicLinkEnabled?: boolean
|
|
104
|
+
publicLinkId?: string
|
|
105
|
+
publicLinkLimitEnabled?: boolean
|
|
106
|
+
publicLinkLimit?: number
|
|
107
|
+
publicLink?: string
|
|
108
|
+
feedbackEnabled?: boolean
|
|
109
|
+
iosBuildsAvailableForAppleSiliconMac?: boolean
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type BetaGroup = JsonApiResource<BetaGroupAttributes>
|
|
113
|
+
|
|
114
|
+
// ---- Beta Testers ----
|
|
115
|
+
export interface BetaTesterAttributes {
|
|
116
|
+
firstName?: string
|
|
117
|
+
lastName?: string
|
|
118
|
+
email: string
|
|
119
|
+
inviteType?: string
|
|
120
|
+
state?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type BetaTester = JsonApiResource<BetaTesterAttributes>
|
|
124
|
+
|
|
125
|
+
// ---- App Store Version Localizations ----
|
|
126
|
+
export interface AppStoreVersionLocalizationAttributes {
|
|
127
|
+
locale: string
|
|
128
|
+
description?: string
|
|
129
|
+
keywords?: string
|
|
130
|
+
marketingUrl?: string
|
|
131
|
+
promotionalText?: string
|
|
132
|
+
supportUrl?: string
|
|
133
|
+
whatsNew?: string
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type AppStoreVersionLocalization = JsonApiResource<AppStoreVersionLocalizationAttributes>
|
|
137
|
+
|
|
138
|
+
// ---- Customer Reviews ----
|
|
139
|
+
export interface CustomerReviewAttributes {
|
|
140
|
+
rating: number
|
|
141
|
+
title?: string
|
|
142
|
+
body?: string
|
|
143
|
+
reviewerNickname?: string
|
|
144
|
+
createdDate?: string
|
|
145
|
+
territory?: string
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type CustomerReview = JsonApiResource<CustomerReviewAttributes>
|
|
149
|
+
|
|
150
|
+
// ---- Review Responses ----
|
|
151
|
+
export interface CustomerReviewResponseAttributes {
|
|
152
|
+
responseBody: string
|
|
153
|
+
lastModifiedDate?: string
|
|
154
|
+
state?: "PUBLISHED" | "PENDING_PUBLISH"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export type CustomerReviewResponse = JsonApiResource<CustomerReviewResponseAttributes>
|
|
158
|
+
|
|
159
|
+
// ---- Platform ----
|
|
160
|
+
export type Platform = "IOS" | "MAC_OS" | "TV_OS" | "VISION_OS"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs"
|
|
2
|
+
import { homedir } from "os"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
|
|
5
|
+
export interface Profile {
|
|
6
|
+
name: string
|
|
7
|
+
keyId: string
|
|
8
|
+
issuerId: string
|
|
9
|
+
privateKeyPath?: string
|
|
10
|
+
privateKeyPem?: string
|
|
11
|
+
isDefault: boolean
|
|
12
|
+
vendorNumber?: string
|
|
13
|
+
storedInKeychain?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CredentialsFile {
|
|
17
|
+
activeProfile: string
|
|
18
|
+
profiles: Profile[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getAscDir(): string {
|
|
22
|
+
return join(process.env["HOME"] ?? homedir(), ".asc")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getCredentialsPath(): string {
|
|
26
|
+
return join(getAscDir(), "credentials.json")
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureAscDir(): void {
|
|
30
|
+
const dir = getAscDir()
|
|
31
|
+
if (!existsSync(dir)) {
|
|
32
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function loadCredentials(): CredentialsFile {
|
|
37
|
+
const path = getCredentialsPath()
|
|
38
|
+
if (!existsSync(path)) {
|
|
39
|
+
return { activeProfile: "default", profiles: [] }
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const raw = readFileSync(path, "utf8")
|
|
43
|
+
return JSON.parse(raw) as CredentialsFile
|
|
44
|
+
} catch {
|
|
45
|
+
return { activeProfile: "default", profiles: [] }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function saveCredentials(creds: CredentialsFile): void {
|
|
50
|
+
ensureAscDir()
|
|
51
|
+
const path = getCredentialsPath()
|
|
52
|
+
writeFileSync(path, JSON.stringify(creds, null, 2), "utf8")
|
|
53
|
+
chmodSync(path, 0o600)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getActiveProfile(): Profile | undefined {
|
|
57
|
+
// Env var override
|
|
58
|
+
const envKeyId = process.env["ASC_KEY_ID"]
|
|
59
|
+
const envIssuerId = process.env["ASC_ISSUER_ID"]
|
|
60
|
+
const envPrivateKeyPath = process.env["ASC_PRIVATE_KEY_PATH"]
|
|
61
|
+
const envPrivateKey = process.env["ASC_PRIVATE_KEY"]
|
|
62
|
+
|
|
63
|
+
if (envKeyId && envIssuerId && (envPrivateKeyPath || envPrivateKey)) {
|
|
64
|
+
return {
|
|
65
|
+
name: "env",
|
|
66
|
+
keyId: envKeyId,
|
|
67
|
+
issuerId: envIssuerId,
|
|
68
|
+
privateKeyPath: envPrivateKeyPath,
|
|
69
|
+
privateKeyPem: envPrivateKey,
|
|
70
|
+
isDefault: true,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const creds = loadCredentials()
|
|
75
|
+
const name = process.env["ASC_PROFILE"] ?? creds.activeProfile ?? "default"
|
|
76
|
+
return creds.profiles.find((p) => p.name === name)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function addOrUpdateProfile(profile: Profile): void {
|
|
80
|
+
const creds = loadCredentials()
|
|
81
|
+
const idx = creds.profiles.findIndex((p) => p.name === profile.name)
|
|
82
|
+
if (idx >= 0) {
|
|
83
|
+
creds.profiles[idx] = profile
|
|
84
|
+
} else {
|
|
85
|
+
creds.profiles.push(profile)
|
|
86
|
+
}
|
|
87
|
+
// If first profile, set as active
|
|
88
|
+
if (creds.profiles.length === 1) {
|
|
89
|
+
creds.activeProfile = profile.name
|
|
90
|
+
}
|
|
91
|
+
saveCredentials(creds)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function removeProfile(name: string): boolean {
|
|
95
|
+
const creds = loadCredentials()
|
|
96
|
+
const idx = creds.profiles.findIndex((p) => p.name === name)
|
|
97
|
+
if (idx < 0) return false
|
|
98
|
+
creds.profiles.splice(idx, 1)
|
|
99
|
+
if (creds.activeProfile === name) {
|
|
100
|
+
creds.activeProfile = creds.profiles[0]?.name ?? "default"
|
|
101
|
+
}
|
|
102
|
+
saveCredentials(creds)
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function setActiveProfile(name: string): boolean {
|
|
107
|
+
const creds = loadCredentials()
|
|
108
|
+
if (!creds.profiles.find((p) => p.name === name)) return false
|
|
109
|
+
creds.activeProfile = name
|
|
110
|
+
saveCredentials(creds)
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function updateProfile(name: string, updates: Partial<Omit<Profile, "name">>): boolean {
|
|
115
|
+
const creds = loadCredentials()
|
|
116
|
+
const idx = creds.profiles.findIndex((p) => p.name === name)
|
|
117
|
+
if (idx < 0) return false
|
|
118
|
+
creds.profiles[idx] = { ...creds.profiles[idx]!, ...updates }
|
|
119
|
+
saveCredentials(creds)
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function credentialsPath(): string {
|
|
124
|
+
return getCredentialsPath()
|
|
125
|
+
}
|
package/src/auth/jwt.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { importPKCS8, SignJWT } from "jose"
|
|
2
|
+
import { existsSync, readFileSync } from "fs"
|
|
3
|
+
import { spawnSync } from "child_process"
|
|
4
|
+
import type { Profile } from "./credentials.ts"
|
|
5
|
+
|
|
6
|
+
interface CachedToken {
|
|
7
|
+
token: string
|
|
8
|
+
expiresAt: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Per-profile in-memory cache
|
|
12
|
+
const tokenCache = new Map<string, CachedToken>()
|
|
13
|
+
|
|
14
|
+
const KEYCHAIN_SERVICE = "asc-cli"
|
|
15
|
+
|
|
16
|
+
export async function generateJWT(profile: Profile): Promise<string> {
|
|
17
|
+
const cacheKey = `${profile.name}:${profile.keyId}`
|
|
18
|
+
const cached = tokenCache.get(cacheKey)
|
|
19
|
+
// Reuse if more than 2 minutes remain
|
|
20
|
+
if (cached && cached.expiresAt - Date.now() > 120_000) {
|
|
21
|
+
return cached.token
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pem = await resolvePrivateKey(profile)
|
|
25
|
+
const privateKey = await importPKCS8(pem, "ES256")
|
|
26
|
+
|
|
27
|
+
const now = Math.floor(Date.now() / 1000)
|
|
28
|
+
const exp = now + 20 * 60 // 20 minutes max (Apple limit)
|
|
29
|
+
|
|
30
|
+
const token = await new SignJWT({})
|
|
31
|
+
.setProtectedHeader({ alg: "ES256", kid: profile.keyId, typ: "JWT" })
|
|
32
|
+
.setIssuer(profile.issuerId)
|
|
33
|
+
.setIssuedAt(now)
|
|
34
|
+
.setExpirationTime(exp)
|
|
35
|
+
.setAudience("appstoreconnect-v1")
|
|
36
|
+
.sign(privateKey)
|
|
37
|
+
|
|
38
|
+
tokenCache.set(cacheKey, { token, expiresAt: exp * 1000 })
|
|
39
|
+
return token
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function resolvePrivateKey(profile: Profile): Promise<string> {
|
|
43
|
+
// 1. Try keychain
|
|
44
|
+
const keychainKey = loadFromKeychain(profile.name)
|
|
45
|
+
if (keychainKey) return keychainKey
|
|
46
|
+
|
|
47
|
+
// 2. Inline PEM
|
|
48
|
+
if (profile.privateKeyPem) {
|
|
49
|
+
return profile.privateKeyPem
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 3. File path
|
|
53
|
+
if (profile.privateKeyPath) {
|
|
54
|
+
const expanded = profile.privateKeyPath.trim().replace(/^~/, process.env["HOME"] ?? "")
|
|
55
|
+
if (!existsSync(expanded)) {
|
|
56
|
+
throw new Error(`Private key file not found: ${expanded}`)
|
|
57
|
+
}
|
|
58
|
+
return readFileSync(expanded, "utf8")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error(`No private key found for profile "${profile.name}". Run: asc auth login`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function loadFromKeychain(profileName: string): string | undefined {
|
|
65
|
+
try {
|
|
66
|
+
const result = spawnSync(
|
|
67
|
+
"security",
|
|
68
|
+
["find-generic-password", "-a", profileName, "-s", KEYCHAIN_SERVICE, "-w"],
|
|
69
|
+
{ encoding: "utf8" },
|
|
70
|
+
)
|
|
71
|
+
if (result.status !== 0 || !result.stdout) return undefined
|
|
72
|
+
const stored = result.stdout.trim()
|
|
73
|
+
if (!stored) return undefined
|
|
74
|
+
// Decode from base64 (PEM is stored encoded to survive newlines in the -w arg)
|
|
75
|
+
const decoded = Buffer.from(stored, "base64").toString("utf8")
|
|
76
|
+
return decoded.includes("BEGIN") ? decoded : stored // fallback for legacy plain-text entries
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function saveToKeychain(profileName: string, pem: string): boolean {
|
|
83
|
+
try {
|
|
84
|
+
// Base64-encode so the multi-line PEM survives as a single-line -w argument
|
|
85
|
+
const encoded = Buffer.from(pem).toString("base64")
|
|
86
|
+
const result = spawnSync(
|
|
87
|
+
"security",
|
|
88
|
+
["add-generic-password", "-U", "-a", profileName, "-s", KEYCHAIN_SERVICE, "-w", encoded],
|
|
89
|
+
{ encoding: "utf8" },
|
|
90
|
+
)
|
|
91
|
+
return result.status === 0
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function deleteFromKeychain(profileName: string): void {
|
|
98
|
+
try {
|
|
99
|
+
spawnSync(
|
|
100
|
+
"security",
|
|
101
|
+
["delete-generic-password", "-a", profileName, "-s", KEYCHAIN_SERVICE],
|
|
102
|
+
{ encoding: "utf8" },
|
|
103
|
+
)
|
|
104
|
+
} catch {
|
|
105
|
+
// ignore — item may not exist
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function clearTokenCache(profileName?: string): void {
|
|
110
|
+
if (profileName) {
|
|
111
|
+
for (const key of tokenCache.keys()) {
|
|
112
|
+
if (key.startsWith(`${profileName}:`)) tokenCache.delete(key)
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
tokenCache.clear()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import { detectFormat, printJSON, printTable } from "../utils/output.ts"
|
|
3
|
+
|
|
4
|
+
// ---- App Info (content ratings, age ratings, primary/secondary categories) ----
|
|
5
|
+
|
|
6
|
+
export async function appInfoGet(appId: string, opts: { output?: string } = {}): Promise<void> {
|
|
7
|
+
const result = await ascFetch<{
|
|
8
|
+
data: Array<{
|
|
9
|
+
id: string
|
|
10
|
+
attributes: {
|
|
11
|
+
appStoreState?: string
|
|
12
|
+
appStoreAgeRating?: string
|
|
13
|
+
brazilAgeRating?: string
|
|
14
|
+
kidsAgeBand?: string
|
|
15
|
+
}
|
|
16
|
+
}>
|
|
17
|
+
}>(`/v1/apps/${appId}/appInfos`, {
|
|
18
|
+
params: {
|
|
19
|
+
include: "primaryCategory,secondaryCategory",
|
|
20
|
+
"fields[appInfos]": "appStoreState,appStoreAgeRating,brazilAgeRating,kidsAgeBand",
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
const fmt = detectFormat(opts.output)
|
|
24
|
+
if (fmt === "json") { printJSON(result.data); return }
|
|
25
|
+
for (const info of result.data.data) {
|
|
26
|
+
const a = info.attributes
|
|
27
|
+
console.log(`Info ID: ${info.id}`)
|
|
28
|
+
console.log(`State: ${a.appStoreState ?? "-"}`)
|
|
29
|
+
console.log(`Age Rating: ${a.appStoreAgeRating ?? "-"}`)
|
|
30
|
+
console.log(`Brazil Rating: ${a.brazilAgeRating ?? "-"}`)
|
|
31
|
+
console.log(`Kids Band: ${a.kidsAgeBand ?? "-"}`)
|
|
32
|
+
console.log("")
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function appInfoUpdate(opts: {
|
|
37
|
+
infoId: string
|
|
38
|
+
primaryCategoryId?: string
|
|
39
|
+
secondaryCategoryId?: string
|
|
40
|
+
}): Promise<void> {
|
|
41
|
+
const relationships: Record<string, unknown> = {}
|
|
42
|
+
if (opts.primaryCategoryId) {
|
|
43
|
+
relationships["primaryCategory"] = { data: { type: "appCategories", id: opts.primaryCategoryId } }
|
|
44
|
+
}
|
|
45
|
+
if (opts.secondaryCategoryId) {
|
|
46
|
+
relationships["secondaryCategory"] = { data: { type: "appCategories", id: opts.secondaryCategoryId } }
|
|
47
|
+
}
|
|
48
|
+
await ascFetch(`/v1/appInfos/${opts.infoId}`, {
|
|
49
|
+
method: "PATCH",
|
|
50
|
+
body: {
|
|
51
|
+
data: {
|
|
52
|
+
type: "appInfos",
|
|
53
|
+
id: opts.infoId,
|
|
54
|
+
relationships,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
console.log(`App info ${opts.infoId} updated`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function appCategoriesList(opts: {
|
|
62
|
+
platform?: string
|
|
63
|
+
output?: string
|
|
64
|
+
} = {}): Promise<void> {
|
|
65
|
+
const params: Record<string, string> = {}
|
|
66
|
+
if (opts.platform) params["filter[platforms]"] = opts.platform
|
|
67
|
+
const items = await ascFetchAll<{
|
|
68
|
+
id: string
|
|
69
|
+
attributes: { platforms?: string[] }
|
|
70
|
+
}>("/v1/appCategories", { params })
|
|
71
|
+
const fmt = detectFormat(opts.output)
|
|
72
|
+
if (fmt === "json") { printJSON(items); return }
|
|
73
|
+
printTable(
|
|
74
|
+
["Category ID", "Platforms"],
|
|
75
|
+
items.map(c => [c.id, (c.attributes.platforms ?? []).join(", ")]),
|
|
76
|
+
`App Categories (${items.length})`,
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function ageRatingDeclarationUpdate(opts: {
|
|
81
|
+
declarationId: string
|
|
82
|
+
alcoholTobaccoOrDrugUseOrReferences?: string
|
|
83
|
+
contests?: string
|
|
84
|
+
gambling?: boolean
|
|
85
|
+
gamblingAndContests?: boolean
|
|
86
|
+
horrorOrFearThemes?: string
|
|
87
|
+
kidsAgeBand?: string
|
|
88
|
+
matureOrSuggestiveThemes?: string
|
|
89
|
+
medicalOrTreatmentInformation?: string
|
|
90
|
+
profanityOrCrudeHumor?: string
|
|
91
|
+
sexualContentGraphicAndNudity?: string
|
|
92
|
+
sexualContentOrNudity?: string
|
|
93
|
+
unrestrictedWebAccess?: boolean
|
|
94
|
+
violenceCartoonOrFantasy?: string
|
|
95
|
+
violenceRealisticProlongedGraphicOrSadistic?: string
|
|
96
|
+
violenceRealistic?: string
|
|
97
|
+
}): Promise<void> {
|
|
98
|
+
const { declarationId, ...attributes } = opts
|
|
99
|
+
await ascFetch(`/v1/ageRatingDeclarations/${declarationId}`, {
|
|
100
|
+
method: "PATCH",
|
|
101
|
+
body: {
|
|
102
|
+
data: {
|
|
103
|
+
type: "ageRatingDeclarations",
|
|
104
|
+
id: declarationId,
|
|
105
|
+
attributes,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
})
|
|
109
|
+
console.log(`Age rating declaration ${declarationId} updated`)
|
|
110
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import type { App } from "../api/types.ts"
|
|
3
|
+
import { detectFormat, formatDate, printJSON, printTable, truncate } from "../utils/output.ts"
|
|
4
|
+
|
|
5
|
+
export async function appsList(opts: { limit?: number; output?: string } = {}): Promise<void> {
|
|
6
|
+
const apps = await ascFetchAll<App>("/v1/apps", {
|
|
7
|
+
params: { "fields[apps]": "name,bundleId,sku,primaryLocale" },
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
const fmt = detectFormat(opts.output)
|
|
11
|
+
if (fmt === "json") {
|
|
12
|
+
printJSON(apps.map((a) => ({ id: a.id, ...a.attributes })))
|
|
13
|
+
return
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
printTable(
|
|
17
|
+
["ID", "Name", "Bundle ID", "SKU", "Locale"],
|
|
18
|
+
apps.map((a) => [
|
|
19
|
+
a.id,
|
|
20
|
+
truncate(a.attributes.name, 40),
|
|
21
|
+
a.attributes.bundleId,
|
|
22
|
+
a.attributes.sku ?? "-",
|
|
23
|
+
a.attributes.primaryLocale,
|
|
24
|
+
]),
|
|
25
|
+
`Apps (${apps.length})`,
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function appsGet(appId: string, opts: { output?: string } = {}): Promise<void> {
|
|
30
|
+
const res = await ascFetch<App>(`/v1/apps/${appId}`)
|
|
31
|
+
const app = res.data
|
|
32
|
+
|
|
33
|
+
const fmt = detectFormat(opts.output)
|
|
34
|
+
if (fmt === "json") {
|
|
35
|
+
printJSON({ id: app.id, ...app.attributes })
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log(`ID: ${app.id}`)
|
|
40
|
+
console.log(`Name: ${app.attributes.name}`)
|
|
41
|
+
console.log(`Bundle ID: ${app.attributes.bundleId}`)
|
|
42
|
+
console.log(`SKU: ${app.attributes.sku ?? "-"}`)
|
|
43
|
+
console.log(`Primary Locale: ${app.attributes.primaryLocale}`)
|
|
44
|
+
}
|