@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,114 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import { detectFormat, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
|
|
3
|
+
|
|
4
|
+
// ---- Achievements ----
|
|
5
|
+
|
|
6
|
+
export async function achievementsList(opts: {
|
|
7
|
+
appId: string
|
|
8
|
+
output?: string
|
|
9
|
+
}): Promise<void> {
|
|
10
|
+
const items = await ascFetchAll<{
|
|
11
|
+
id: string
|
|
12
|
+
attributes: {
|
|
13
|
+
referenceName?: string
|
|
14
|
+
points?: number
|
|
15
|
+
showBeforeEarned?: boolean
|
|
16
|
+
repeatable?: boolean
|
|
17
|
+
archived?: boolean
|
|
18
|
+
}
|
|
19
|
+
}>(`/v1/apps/${opts.appId}/gameCenterAchievements`, {
|
|
20
|
+
params: { "fields[gameCenterAchievements]": "referenceName,points,showBeforeEarned,repeatable,archived" },
|
|
21
|
+
})
|
|
22
|
+
const fmt = detectFormat(opts.output)
|
|
23
|
+
if (fmt === "json") { printJSON(items); return }
|
|
24
|
+
printTable(
|
|
25
|
+
["ID", "Reference Name", "Points", "Repeatable"],
|
|
26
|
+
items.map(a => [
|
|
27
|
+
a.id,
|
|
28
|
+
a.attributes.referenceName ?? "-",
|
|
29
|
+
String(a.attributes.points ?? "-"),
|
|
30
|
+
String(a.attributes.repeatable ?? "-"),
|
|
31
|
+
]),
|
|
32
|
+
`Game Center Achievements (${items.length})`,
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function achievementGet(achievementId: string, opts: { output?: string } = {}): Promise<void> {
|
|
37
|
+
const result = await ascFetch<{
|
|
38
|
+
data: {
|
|
39
|
+
id: string
|
|
40
|
+
attributes: {
|
|
41
|
+
referenceName?: string
|
|
42
|
+
points?: number
|
|
43
|
+
showBeforeEarned?: boolean
|
|
44
|
+
repeatable?: boolean
|
|
45
|
+
archived?: boolean
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}>(`/v1/gameCenterAchievements/${achievementId}`)
|
|
49
|
+
const fmt = detectFormat(opts.output)
|
|
50
|
+
if (fmt === "json") { printJSON(result.data.data); return }
|
|
51
|
+
const a = result.data.data.attributes
|
|
52
|
+
console.log(`ID: ${result.data.data.id}`)
|
|
53
|
+
console.log(`Reference Name: ${a.referenceName ?? "-"}`)
|
|
54
|
+
console.log(`Points: ${a.points ?? "-"}`)
|
|
55
|
+
console.log(`Repeatable: ${a.repeatable ?? false}`)
|
|
56
|
+
console.log(`Show Before Earn:${a.showBeforeEarned ?? false}`)
|
|
57
|
+
console.log(`Archived: ${a.archived ?? false}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- Leaderboards ----
|
|
61
|
+
|
|
62
|
+
export async function leaderboardsList(opts: {
|
|
63
|
+
appId: string
|
|
64
|
+
output?: string
|
|
65
|
+
}): Promise<void> {
|
|
66
|
+
const items = await ascFetchAll<{
|
|
67
|
+
id: string
|
|
68
|
+
attributes: {
|
|
69
|
+
referenceName?: string
|
|
70
|
+
defaultFormatter?: unknown
|
|
71
|
+
archived?: boolean
|
|
72
|
+
}
|
|
73
|
+
}>(`/v1/apps/${opts.appId}/gameCenterLeaderboards`, {
|
|
74
|
+
params: { "fields[gameCenterLeaderboards]": "referenceName,defaultFormatter,archived" },
|
|
75
|
+
})
|
|
76
|
+
const fmt = detectFormat(opts.output)
|
|
77
|
+
if (fmt === "json") { printJSON(items); return }
|
|
78
|
+
printTable(
|
|
79
|
+
["ID", "Reference Name", "Archived"],
|
|
80
|
+
items.map(l => [l.id, l.attributes.referenceName ?? "-", String(l.attributes.archived ?? false)]),
|
|
81
|
+
`Leaderboards (${items.length})`,
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ---- Leaderboard Sets ----
|
|
86
|
+
|
|
87
|
+
export async function leaderboardSetsList(opts: {
|
|
88
|
+
appId: string
|
|
89
|
+
output?: string
|
|
90
|
+
}): Promise<void> {
|
|
91
|
+
const items = await ascFetchAll<{
|
|
92
|
+
id: string
|
|
93
|
+
attributes: { referenceName?: string; archived?: boolean }
|
|
94
|
+
}>(`/v1/apps/${opts.appId}/gameCenterLeaderboardSets`)
|
|
95
|
+
const fmt = detectFormat(opts.output)
|
|
96
|
+
if (fmt === "json") { printJSON(items); return }
|
|
97
|
+
printTable(
|
|
98
|
+
["ID", "Reference Name", "Archived"],
|
|
99
|
+
items.map(s => [s.id, s.attributes.referenceName ?? "-", String(s.attributes.archived ?? false)]),
|
|
100
|
+
`Leaderboard Sets (${items.length})`,
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- Game Center Enable ----
|
|
105
|
+
|
|
106
|
+
export async function gameCenterEnableCheck(appId: string, opts: { output?: string } = {}): Promise<void> {
|
|
107
|
+
const result = await ascFetch<{
|
|
108
|
+
data: { id: string; attributes: Record<string, unknown> }
|
|
109
|
+
}>(`/v1/apps/${appId}/gameCenterEnabledVersions`)
|
|
110
|
+
const fmt = detectFormat(opts.output)
|
|
111
|
+
if (fmt === "json") { printJSON(result.data); return }
|
|
112
|
+
console.log(`Game Center status for app ${appId}:`)
|
|
113
|
+
console.log(JSON.stringify(result.data.data, null, 2))
|
|
114
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import { detectFormat, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
|
|
3
|
+
|
|
4
|
+
// ---- In-App Purchases (non-subscription consumables, non-consumables, non-renewing subscriptions) ----
|
|
5
|
+
|
|
6
|
+
export async function iapList(opts: { appId: string; output?: string }): Promise<void> {
|
|
7
|
+
const items = await ascFetchAll<{
|
|
8
|
+
id: string
|
|
9
|
+
attributes: {
|
|
10
|
+
name?: string
|
|
11
|
+
productId?: string
|
|
12
|
+
inAppPurchaseType?: string
|
|
13
|
+
state?: string
|
|
14
|
+
}
|
|
15
|
+
}>(`/v1/apps/${opts.appId}/inAppPurchasesV2`, {
|
|
16
|
+
params: { "fields[inAppPurchasesV2]": "name,productId,inAppPurchaseType,state" },
|
|
17
|
+
})
|
|
18
|
+
const fmt = detectFormat(opts.output)
|
|
19
|
+
if (fmt === "json") { printJSON(items); return }
|
|
20
|
+
printTable(
|
|
21
|
+
["ID", "Name", "Product ID", "Type", "State"],
|
|
22
|
+
items.map(i => [
|
|
23
|
+
i.id,
|
|
24
|
+
truncate(i.attributes.name ?? "-", 30),
|
|
25
|
+
i.attributes.productId ?? "-",
|
|
26
|
+
i.attributes.inAppPurchaseType ?? "-",
|
|
27
|
+
i.attributes.state ?? "-",
|
|
28
|
+
]),
|
|
29
|
+
`In-App Purchases (${items.length})`,
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function iapGet(iapId: string, opts: { output?: string } = {}): Promise<void> {
|
|
34
|
+
const result = await ascFetch<{
|
|
35
|
+
data: {
|
|
36
|
+
id: string
|
|
37
|
+
attributes: {
|
|
38
|
+
name?: string
|
|
39
|
+
productId?: string
|
|
40
|
+
inAppPurchaseType?: string
|
|
41
|
+
state?: string
|
|
42
|
+
reviewNote?: string
|
|
43
|
+
familySharable?: boolean
|
|
44
|
+
contentHosting?: boolean
|
|
45
|
+
availableInAllTerritories?: boolean
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}>(`/v1/inAppPurchasesV2/${iapId}`)
|
|
49
|
+
const fmt = detectFormat(opts.output)
|
|
50
|
+
if (fmt === "json") { printJSON(result.data); return }
|
|
51
|
+
const a = result.data.data.attributes
|
|
52
|
+
console.log(`ID: ${result.data.data.id}`)
|
|
53
|
+
console.log(`Name: ${a.name ?? "-"}`)
|
|
54
|
+
console.log(`Product ID: ${a.productId ?? "-"}`)
|
|
55
|
+
console.log(`Type: ${a.inAppPurchaseType ?? "-"}`)
|
|
56
|
+
console.log(`State: ${a.state ?? "-"}`)
|
|
57
|
+
console.log(`Reviewable: ${a.reviewNote ?? "(no review note)"}`)
|
|
58
|
+
console.log(`Family Share: ${a.familySharable ?? false}`)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function iapCreate(opts: {
|
|
62
|
+
appId: string
|
|
63
|
+
name: string
|
|
64
|
+
productId: string
|
|
65
|
+
type: string
|
|
66
|
+
}): Promise<void> {
|
|
67
|
+
const result = await ascFetch<{ data: { id: string } }>("/v1/inAppPurchasesV2", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
body: {
|
|
70
|
+
data: {
|
|
71
|
+
type: "inAppPurchasesV2",
|
|
72
|
+
attributes: {
|
|
73
|
+
name: opts.name,
|
|
74
|
+
productId: opts.productId,
|
|
75
|
+
inAppPurchaseType: opts.type,
|
|
76
|
+
},
|
|
77
|
+
relationships: {
|
|
78
|
+
app: { data: { type: "apps", id: opts.appId } },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
printSuccess(`Created IAP: ${result.data.data.id} (${opts.productId})`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function iapDelete(iapId: string): Promise<void> {
|
|
87
|
+
await ascFetch(`/v1/inAppPurchasesV2/${iapId}`, { method: "DELETE" })
|
|
88
|
+
printSuccess(`Deleted IAP ${iapId}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function iapLocalesList(iapId: string, opts: { output?: string } = {}): Promise<void> {
|
|
92
|
+
const items = await ascFetchAll<{
|
|
93
|
+
id: string
|
|
94
|
+
attributes: { locale?: string; name?: string; description?: string }
|
|
95
|
+
}>(`/v1/inAppPurchasesV2/${iapId}/iapPriceSchedule`, {
|
|
96
|
+
params: { "fields[inAppPurchaseLocalizations]": "locale,name,description" },
|
|
97
|
+
})
|
|
98
|
+
const fmt = detectFormat(opts.output)
|
|
99
|
+
if (fmt === "json") { printJSON(items); return }
|
|
100
|
+
printTable(
|
|
101
|
+
["Locale", "Name", "Description"],
|
|
102
|
+
items.map(l => [l.attributes.locale ?? l.id, l.attributes.name ?? "-", truncate(l.attributes.description ?? "-", 40)]),
|
|
103
|
+
`IAP Localizations (${items.length})`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import type { AppStoreVersionLocalization } from "../api/types.ts"
|
|
3
|
+
import { detectFormat, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
|
|
4
|
+
|
|
5
|
+
export async function metadataList(opts: {
|
|
6
|
+
versionId: string
|
|
7
|
+
output?: string
|
|
8
|
+
}): Promise<void> {
|
|
9
|
+
const locs = await ascFetchAll<AppStoreVersionLocalization>(
|
|
10
|
+
`/v1/appStoreVersions/${opts.versionId}/appStoreVersionLocalizations`,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
const fmt = detectFormat(opts.output)
|
|
14
|
+
if (fmt === "json") {
|
|
15
|
+
printJSON(locs.map((l) => ({ id: l.id, ...l.attributes })))
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
printTable(
|
|
20
|
+
["ID", "Locale", "What's New", "Description"],
|
|
21
|
+
locs.map((l) => [
|
|
22
|
+
l.id,
|
|
23
|
+
l.attributes.locale,
|
|
24
|
+
truncate(l.attributes.whatsNew ?? "-", 50),
|
|
25
|
+
truncate(l.attributes.description ?? "-", 50),
|
|
26
|
+
]),
|
|
27
|
+
`Localizations (${locs.length})`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function metadataUpdate(opts: {
|
|
32
|
+
versionId: string
|
|
33
|
+
locale: string
|
|
34
|
+
whatsNew?: string
|
|
35
|
+
description?: string
|
|
36
|
+
keywords?: string
|
|
37
|
+
promotionalText?: string
|
|
38
|
+
}): Promise<void> {
|
|
39
|
+
// Find existing localization for this locale
|
|
40
|
+
const locs = await ascFetchAll<AppStoreVersionLocalization>(
|
|
41
|
+
`/v1/appStoreVersions/${opts.versionId}/appStoreVersionLocalizations`,
|
|
42
|
+
{ params: { "filter[locale]": opts.locale } },
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const attributes: Record<string, string> = {}
|
|
46
|
+
if (opts.whatsNew !== undefined) attributes["whatsNew"] = opts.whatsNew
|
|
47
|
+
if (opts.description !== undefined) attributes["description"] = opts.description
|
|
48
|
+
if (opts.keywords !== undefined) attributes["keywords"] = opts.keywords
|
|
49
|
+
if (opts.promotionalText !== undefined) attributes["promotionalText"] = opts.promotionalText
|
|
50
|
+
|
|
51
|
+
if (locs.length > 0 && locs[0]) {
|
|
52
|
+
await ascFetch(`/v1/appStoreVersionLocalizations/${locs[0].id}`, {
|
|
53
|
+
method: "PATCH",
|
|
54
|
+
body: {
|
|
55
|
+
data: {
|
|
56
|
+
type: "appStoreVersionLocalizations",
|
|
57
|
+
id: locs[0].id,
|
|
58
|
+
attributes,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
printSuccess(`Metadata updated for locale "${opts.locale}"`)
|
|
63
|
+
} else {
|
|
64
|
+
// Create new localization
|
|
65
|
+
await ascFetch("/v1/appStoreVersionLocalizations", {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: {
|
|
68
|
+
data: {
|
|
69
|
+
type: "appStoreVersionLocalizations",
|
|
70
|
+
attributes: { locale: opts.locale, ...attributes },
|
|
71
|
+
relationships: {
|
|
72
|
+
appStoreVersion: {
|
|
73
|
+
data: { type: "appStoreVersions", id: opts.versionId },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
printSuccess(`Localization created for locale "${opts.locale}"`)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import { detectFormat, formatDate, printJSON, printSuccess, printTable } from "../utils/output.ts"
|
|
3
|
+
|
|
4
|
+
export async function pricingScheduleGet(appId: string, opts: { output?: string } = {}): Promise<void> {
|
|
5
|
+
// Fetch price schedule with related resources included
|
|
6
|
+
const result = await ascFetch<{
|
|
7
|
+
data: { id: string; type: string }
|
|
8
|
+
included?: Array<{ type: string; id: string; attributes?: Record<string, unknown> }>
|
|
9
|
+
}>(`/v1/apps/${appId}/appPriceSchedule`, {
|
|
10
|
+
params: {
|
|
11
|
+
include: "manualPrices,automaticPrices,baseTerritory",
|
|
12
|
+
"fields[territories]": "currency,name",
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
const body = result.data
|
|
16
|
+
const fmt = detectFormat(opts.output)
|
|
17
|
+
if (fmt === "json") { printJSON(body); return }
|
|
18
|
+
console.log(`Schedule ID: ${body.data.id}`)
|
|
19
|
+
const included = body.included ?? []
|
|
20
|
+
const base = included.find(r => r.type === "territories")
|
|
21
|
+
if (base) {
|
|
22
|
+
const attrs = base.attributes as { currency?: string; name?: string } | undefined
|
|
23
|
+
console.log(`Base Territory: ${attrs?.name ?? base.id} (${attrs?.currency ?? "?"})`)
|
|
24
|
+
}
|
|
25
|
+
const manual = included.filter(r => r.type === "appPrices")
|
|
26
|
+
if (manual.length) {
|
|
27
|
+
printTable(
|
|
28
|
+
["Price ID", "Territory", "Start Date"],
|
|
29
|
+
manual.map(p => {
|
|
30
|
+
const a = p.attributes as { territory?: string; startDate?: string } | undefined
|
|
31
|
+
return [p.id, a?.territory ?? "-", formatDate(a?.startDate ?? "")]
|
|
32
|
+
}),
|
|
33
|
+
`Manual Prices (${manual.length})`,
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function pricingScheduleSet(opts: {
|
|
39
|
+
appId: string
|
|
40
|
+
baseTerritory: string
|
|
41
|
+
pricePointId: string
|
|
42
|
+
}): Promise<void> {
|
|
43
|
+
await ascFetch("/v1/appPriceSchedules", {
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: {
|
|
46
|
+
data: {
|
|
47
|
+
type: "appPriceSchedules",
|
|
48
|
+
relationships: {
|
|
49
|
+
app: { data: { type: "apps", id: opts.appId } },
|
|
50
|
+
manualPrices: { data: [{ type: "appPrices", id: "${price}" }] },
|
|
51
|
+
baseTerritory: { data: { type: "territories", id: opts.baseTerritory } },
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
included: [
|
|
55
|
+
{
|
|
56
|
+
type: "appPrices",
|
|
57
|
+
id: "${price}",
|
|
58
|
+
attributes: { startDate: null },
|
|
59
|
+
relationships: {
|
|
60
|
+
appPricePoint: { data: { type: "appPricePoints", id: opts.pricePointId } },
|
|
61
|
+
territory: { data: { type: "territories", id: opts.baseTerritory } },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
printSuccess(`Price schedule updated for app ${opts.appId}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function pricingPointsList(opts: {
|
|
71
|
+
appId: string
|
|
72
|
+
territory?: string
|
|
73
|
+
output?: string
|
|
74
|
+
}): Promise<void> {
|
|
75
|
+
const params: Record<string, string> = {
|
|
76
|
+
"fields[appPricePoints]": "customerPrice,proceeds,territory",
|
|
77
|
+
limit: "200",
|
|
78
|
+
}
|
|
79
|
+
if (opts.territory) params["filter[territory]"] = opts.territory
|
|
80
|
+
const items = await ascFetchAll<{
|
|
81
|
+
id: string
|
|
82
|
+
attributes: { customerPrice?: string; proceeds?: string; territory?: unknown }
|
|
83
|
+
}>(`/v1/apps/${opts.appId}/appPricePoints`, { params })
|
|
84
|
+
const fmt = detectFormat(opts.output)
|
|
85
|
+
if (fmt === "json") { printJSON(items); return }
|
|
86
|
+
printTable(
|
|
87
|
+
["ID", "Customer Price", "Proceeds", "Territory"],
|
|
88
|
+
items.map(p => [
|
|
89
|
+
p.id,
|
|
90
|
+
p.attributes.customerPrice ?? "-",
|
|
91
|
+
p.attributes.proceeds ?? "-",
|
|
92
|
+
String(p.attributes.territory ?? "-"),
|
|
93
|
+
]),
|
|
94
|
+
`Price Points (${items.length})`,
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function territoriesList(opts: { output?: string } = {}): Promise<void> {
|
|
99
|
+
const items = await ascFetchAll<{
|
|
100
|
+
id: string
|
|
101
|
+
attributes: { currency?: string; name?: string }
|
|
102
|
+
}>("/v1/territories")
|
|
103
|
+
const fmt = detectFormat(opts.output)
|
|
104
|
+
if (fmt === "json") { printJSON(items); return }
|
|
105
|
+
printTable(
|
|
106
|
+
["Territory Code", "Name", "Currency"],
|
|
107
|
+
items.map(t => [t.id, t.attributes.name ?? "-", t.attributes.currency ?? "-"]),
|
|
108
|
+
`Territories (${items.length})`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import { detectFormat, formatDate, printJSON, printTable } from "../utils/output.ts"
|
|
3
|
+
|
|
4
|
+
// ---- Sales and Finance Reports ----
|
|
5
|
+
|
|
6
|
+
export async function salesReportDownload(opts: {
|
|
7
|
+
vendorNumber: string
|
|
8
|
+
frequency: string
|
|
9
|
+
reportDate: string
|
|
10
|
+
reportType?: string
|
|
11
|
+
reportSubType?: string
|
|
12
|
+
output?: string
|
|
13
|
+
}): Promise<void> {
|
|
14
|
+
const params: Record<string, string> = {
|
|
15
|
+
"filter[vendorNumber]": opts.vendorNumber,
|
|
16
|
+
"filter[frequency]": opts.frequency,
|
|
17
|
+
"filter[reportDate]": opts.reportDate,
|
|
18
|
+
"filter[reportType]": opts.reportType ?? "SALES",
|
|
19
|
+
"filter[reportSubType]": opts.reportSubType ?? "SUMMARY",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Sales reports return gzipped TSV, not JSON:API
|
|
23
|
+
const profile = (await import("../auth/credentials.ts")).getActiveProfile()
|
|
24
|
+
if (!profile) throw new Error("No active profile. Run: asc auth login")
|
|
25
|
+
const token = await (await import("../auth/jwt.ts")).generateJWT(profile)
|
|
26
|
+
|
|
27
|
+
const url = new URL("https://api.appstoreconnect.apple.com/v1/salesReports")
|
|
28
|
+
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v)
|
|
29
|
+
|
|
30
|
+
const res = await fetch(url.toString(), {
|
|
31
|
+
headers: { Authorization: `Bearer ${token}`, "Accept-Encoding": "gzip" },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const err = await res.json() as { errors?: Array<{ detail?: string }> }
|
|
36
|
+
const msg = err.errors?.[0]?.detail ?? `HTTP ${res.status}`
|
|
37
|
+
throw new Error(msg)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const text = await res.text()
|
|
41
|
+
const fmt = detectFormat(opts.output)
|
|
42
|
+
if (fmt === "json") {
|
|
43
|
+
// Parse TSV to JSON
|
|
44
|
+
const lines = text.trim().split("\n")
|
|
45
|
+
const headers = (lines[0] ?? "").split("\t")
|
|
46
|
+
const rows = lines.slice(1).map(line => {
|
|
47
|
+
const cols = line.split("\t")
|
|
48
|
+
return Object.fromEntries(headers.map((h, i) => [h, cols[i] ?? ""]))
|
|
49
|
+
})
|
|
50
|
+
console.log(JSON.stringify(rows, null, 2))
|
|
51
|
+
} else {
|
|
52
|
+
process.stdout.write(text)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function financeReportDownload(opts: {
|
|
57
|
+
vendorNumber: string
|
|
58
|
+
regionCode: string
|
|
59
|
+
reportDate: string
|
|
60
|
+
output?: string
|
|
61
|
+
}): Promise<void> {
|
|
62
|
+
const profile = (await import("../auth/credentials.ts")).getActiveProfile()
|
|
63
|
+
if (!profile) throw new Error("No active profile. Run: asc auth login")
|
|
64
|
+
const token = await (await import("../auth/jwt.ts")).generateJWT(profile)
|
|
65
|
+
|
|
66
|
+
const url = new URL("https://api.appstoreconnect.apple.com/v1/financeReports")
|
|
67
|
+
url.searchParams.set("filter[vendorNumber]", opts.vendorNumber)
|
|
68
|
+
url.searchParams.set("filter[regionCode]", opts.regionCode)
|
|
69
|
+
url.searchParams.set("filter[reportDate]", opts.reportDate)
|
|
70
|
+
url.searchParams.set("filter[reportType]", "FINANCIAL")
|
|
71
|
+
|
|
72
|
+
const res = await fetch(url.toString(), { headers: { Authorization: `Bearer ${token}` } })
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
const err = await res.json() as { errors?: Array<{ detail?: string }> }
|
|
76
|
+
const msg = err.errors?.[0]?.detail ?? `HTTP ${res.status}`
|
|
77
|
+
throw new Error(msg)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
process.stdout.write(await res.text())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function analyticsReportsList(opts: {
|
|
84
|
+
appId: string
|
|
85
|
+
output?: string
|
|
86
|
+
}): Promise<void> {
|
|
87
|
+
const items = await ascFetchAll<{
|
|
88
|
+
id: string
|
|
89
|
+
attributes: { name?: string; category?: string }
|
|
90
|
+
}>(`/v1/apps/${opts.appId}/analyticsReportRequests`)
|
|
91
|
+
const fmt = detectFormat(opts.output)
|
|
92
|
+
if (fmt === "json") { printJSON(items); return }
|
|
93
|
+
printTable(
|
|
94
|
+
["Request ID", "State"],
|
|
95
|
+
items.map(r => [r.id, String(r.attributes.name ?? r.id)]),
|
|
96
|
+
`Analytics Report Requests (${items.length})`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function analyticsReportRequest(opts: {
|
|
101
|
+
appId: string
|
|
102
|
+
category: string
|
|
103
|
+
}): Promise<void> {
|
|
104
|
+
const result = await ascFetch<{ data: { id: string } }>("/v1/analyticsReportRequests", {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: {
|
|
107
|
+
data: {
|
|
108
|
+
type: "analyticsReportRequests",
|
|
109
|
+
attributes: { accessType: opts.category },
|
|
110
|
+
relationships: { app: { data: { type: "apps", id: opts.appId } } },
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
})
|
|
114
|
+
console.log(`Analytics report request created: ${result.data.data.id}`)
|
|
115
|
+
console.log("Check back later with: asc reports analytics-list --app-id <id>")
|
|
116
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ascFetch, ascFetchAll } from "../api/client.ts"
|
|
2
|
+
import type { CustomerReview, CustomerReviewResponse } from "../api/types.ts"
|
|
3
|
+
import { detectFormat, formatDate, printJSON, printSuccess, printTable, truncate } from "../utils/output.ts"
|
|
4
|
+
|
|
5
|
+
export async function reviewsList(opts: {
|
|
6
|
+
appId: string
|
|
7
|
+
output?: string
|
|
8
|
+
}): Promise<void> {
|
|
9
|
+
const reviews = await ascFetchAll<CustomerReview>(
|
|
10
|
+
`/v1/apps/${opts.appId}/customerReviews`,
|
|
11
|
+
{
|
|
12
|
+
params: {
|
|
13
|
+
"fields[customerReviews]": "rating,title,body,reviewerNickname,createdDate,territory",
|
|
14
|
+
"sort": "-createdDate",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const fmt = detectFormat(opts.output)
|
|
20
|
+
if (fmt === "json") {
|
|
21
|
+
printJSON(reviews.map((r) => ({ id: r.id, ...r.attributes })))
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
printTable(
|
|
26
|
+
["ID", "Rating", "Title", "Body", "Date"],
|
|
27
|
+
reviews.map((r) => [
|
|
28
|
+
r.id,
|
|
29
|
+
"★".repeat(r.attributes.rating) + "☆".repeat(5 - r.attributes.rating),
|
|
30
|
+
truncate(r.attributes.title ?? "-", 30),
|
|
31
|
+
truncate(r.attributes.body ?? "-", 50),
|
|
32
|
+
formatDate(r.attributes.createdDate),
|
|
33
|
+
]),
|
|
34
|
+
`Reviews (${reviews.length})`,
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function reviewsGet(reviewId: string, opts: { output?: string } = {}): Promise<void> {
|
|
39
|
+
const res = await ascFetch<CustomerReview>(`/v1/customerReviews/${reviewId}`)
|
|
40
|
+
const r = res.data
|
|
41
|
+
|
|
42
|
+
const fmt = detectFormat(opts.output)
|
|
43
|
+
if (fmt === "json") {
|
|
44
|
+
printJSON({ id: r.id, ...r.attributes })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(`ID: ${r.id}`)
|
|
49
|
+
console.log(`Rating: ${"★".repeat(r.attributes.rating)}${"☆".repeat(5 - r.attributes.rating)}`)
|
|
50
|
+
console.log(`Title: ${r.attributes.title ?? "-"}`)
|
|
51
|
+
console.log(`Body: ${r.attributes.body ?? "-"}`)
|
|
52
|
+
console.log(`Author: ${r.attributes.reviewerNickname ?? "-"}`)
|
|
53
|
+
console.log(`Date: ${formatDate(r.attributes.createdDate)}`)
|
|
54
|
+
console.log(`Territory:${r.attributes.territory ?? "-"}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function reviewsRespond(opts: {
|
|
58
|
+
reviewId: string
|
|
59
|
+
message: string
|
|
60
|
+
}): Promise<void> {
|
|
61
|
+
// Check if a response already exists
|
|
62
|
+
const existingRes = await ascFetch<CustomerReviewResponse | null>(
|
|
63
|
+
`/v1/customerReviews/${opts.reviewId}/response`,
|
|
64
|
+
).catch(() => null)
|
|
65
|
+
|
|
66
|
+
if (existingRes?.data) {
|
|
67
|
+
await ascFetch(`/v1/customerReviewResponses/${existingRes.data.id}`, {
|
|
68
|
+
method: "PATCH",
|
|
69
|
+
body: {
|
|
70
|
+
data: {
|
|
71
|
+
type: "customerReviewResponses",
|
|
72
|
+
id: existingRes.data.id,
|
|
73
|
+
attributes: { responseBody: opts.message },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
})
|
|
77
|
+
printSuccess("Review response updated")
|
|
78
|
+
} else {
|
|
79
|
+
await ascFetch("/v1/customerReviewResponses", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: {
|
|
82
|
+
data: {
|
|
83
|
+
type: "customerReviewResponses",
|
|
84
|
+
attributes: { responseBody: opts.message },
|
|
85
|
+
relationships: {
|
|
86
|
+
review: { data: { type: "customerReviews", id: opts.reviewId } },
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
printSuccess("Review response posted")
|
|
92
|
+
}
|
|
93
|
+
}
|