@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,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 }
@@ -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
+ }
@@ -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
+ }