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