asc-mcp-pro 0.1.0
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/LICENSE +21 -0
- package/README.md +241 -0
- package/build/auth.d.ts +8 -0
- package/build/auth.js +44 -0
- package/build/auth.js.map +1 -0
- package/build/client.d.ts +21 -0
- package/build/client.js +91 -0
- package/build/client.js.map +1 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +47 -0
- package/build/index.js.map +1 -0
- package/build/lib/jsonapi-write.d.ts +32 -0
- package/build/lib/jsonapi-write.js +18 -0
- package/build/lib/jsonapi-write.js.map +1 -0
- package/build/lib/jsonapi.d.ts +27 -0
- package/build/lib/jsonapi.js +27 -0
- package/build/lib/jsonapi.js.map +1 -0
- package/build/lib/locales.d.ts +24 -0
- package/build/lib/locales.js +66 -0
- package/build/lib/locales.js.map +1 -0
- package/build/lib/pagination.d.ts +11 -0
- package/build/lib/pagination.js +23 -0
- package/build/lib/pagination.js.map +1 -0
- package/build/lib/projections.d.ts +8 -0
- package/build/lib/projections.js +51 -0
- package/build/lib/projections.js.map +1 -0
- package/build/lib/upload.d.ts +38 -0
- package/build/lib/upload.js +133 -0
- package/build/lib/upload.js.map +1 -0
- package/build/tools/apps.d.ts +3 -0
- package/build/tools/apps.js +160 -0
- package/build/tools/apps.js.map +1 -0
- package/build/tools/builds.d.ts +3 -0
- package/build/tools/builds.js +158 -0
- package/build/tools/builds.js.map +1 -0
- package/build/tools/iap.d.ts +3 -0
- package/build/tools/iap.js +501 -0
- package/build/tools/iap.js.map +1 -0
- package/build/tools/ping.d.ts +3 -0
- package/build/tools/ping.js +12 -0
- package/build/tools/ping.js.map +1 -0
- package/build/tools/pricing.d.ts +3 -0
- package/build/tools/pricing.js +150 -0
- package/build/tools/pricing.js.map +1 -0
- package/build/tools/privacy.d.ts +3 -0
- package/build/tools/privacy.js +6 -0
- package/build/tools/privacy.js.map +1 -0
- package/build/tools/provisioning.d.ts +3 -0
- package/build/tools/provisioning.js +343 -0
- package/build/tools/provisioning.js.map +1 -0
- package/build/tools/registry.d.ts +3 -0
- package/build/tools/registry.js +35 -0
- package/build/tools/registry.js.map +1 -0
- package/build/tools/reports.d.ts +3 -0
- package/build/tools/reports.js +177 -0
- package/build/tools/reports.js.map +1 -0
- package/build/tools/reviews.d.ts +3 -0
- package/build/tools/reviews.js +51 -0
- package/build/tools/reviews.js.map +1 -0
- package/build/tools/runner.d.ts +3 -0
- package/build/tools/runner.js +174 -0
- package/build/tools/runner.js.map +1 -0
- package/build/tools/screenshots.d.ts +3 -0
- package/build/tools/screenshots.js +228 -0
- package/build/tools/screenshots.js.map +1 -0
- package/build/tools/testflight.d.ts +3 -0
- package/build/tools/testflight.js +360 -0
- package/build/tools/testflight.js.map +1 -0
- package/build/tools/types.d.ts +7 -0
- package/build/tools/types.js +2 -0
- package/build/tools/types.js.map +1 -0
- package/build/tools/users.d.ts +3 -0
- package/build/tools/users.js +65 -0
- package/build/tools/users.js.map +1 -0
- package/build/tools/versions.d.ts +3 -0
- package/build/tools/versions.js +443 -0
- package/build/tools/versions.js.map +1 -0
- package/build/tools/workflows.d.ts +3 -0
- package/build/tools/workflows.js +633 -0
- package/build/tools/workflows.js.map +1 -0
- package/examples/localize.workflow.json +30 -0
- package/examples/release.workflow.json +49 -0
- package/examples/testflight.workflow.json +21 -0
- package/package.json +59 -0
|
@@ -0,0 +1,633 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { postBody, patchBody, singleRel, maybeSend } from "../lib/jsonapi-write.js";
|
|
4
|
+
import { APP_STORE_LOCALES, METADATA_CHAR_LIMITS } from "../lib/locales.js";
|
|
5
|
+
async function listAll(client, path, query = {}) {
|
|
6
|
+
const acc = [];
|
|
7
|
+
let next = null;
|
|
8
|
+
let q = { limit: 200, ...query };
|
|
9
|
+
for (let i = 0; i < 20 && (i === 0 || next); i++) {
|
|
10
|
+
const resp = next
|
|
11
|
+
? await client.request(next)
|
|
12
|
+
: await client.request(path, { query: q });
|
|
13
|
+
q = undefined;
|
|
14
|
+
acc.push(...resp.data);
|
|
15
|
+
next = resp.links?.next ?? null;
|
|
16
|
+
if (!next)
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
return acc;
|
|
20
|
+
}
|
|
21
|
+
export function workflowsTools(client) {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
name: "asc_workflow_id_resolver",
|
|
25
|
+
description: "Look up canonical ids for an app from any of: bundle id, app name fragment, or sku. Returns appId plus optional latest version/build ids when found.",
|
|
26
|
+
inputSchema: z.object({
|
|
27
|
+
query: z.string().describe("bundle id, name substring, or sku"),
|
|
28
|
+
includeLatestVersion: z.boolean().default(true),
|
|
29
|
+
includeLatestBuild: z.boolean().default(true),
|
|
30
|
+
}),
|
|
31
|
+
handler: async ({ query, includeLatestVersion, includeLatestBuild }) => {
|
|
32
|
+
// Try as bundle id first
|
|
33
|
+
let apps = await client.request(`/apps`, {
|
|
34
|
+
query: { "filter[bundleId]": query, limit: 10 },
|
|
35
|
+
});
|
|
36
|
+
if (apps.data.length === 0) {
|
|
37
|
+
apps = await client.request(`/apps`, {
|
|
38
|
+
query: { "filter[name]": query, limit: 10 },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (apps.data.length === 0) {
|
|
42
|
+
apps = await client.request(`/apps`, {
|
|
43
|
+
query: { "filter[sku]": query, limit: 10 },
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (apps.data.length === 0) {
|
|
47
|
+
const all = await client.request(`/apps`, { query: { limit: 200 } });
|
|
48
|
+
const q = query.toLowerCase();
|
|
49
|
+
apps = { data: all.data.filter((a) => a.attributes.name?.toLowerCase().includes(q)) };
|
|
50
|
+
}
|
|
51
|
+
const results = [];
|
|
52
|
+
for (const app of apps.data.slice(0, 5)) {
|
|
53
|
+
const item = {
|
|
54
|
+
appId: app.id,
|
|
55
|
+
name: app.attributes.name,
|
|
56
|
+
bundleId: app.attributes.bundleId,
|
|
57
|
+
sku: app.attributes.sku,
|
|
58
|
+
};
|
|
59
|
+
if (includeLatestVersion) {
|
|
60
|
+
const v = await client.request(`/apps/${app.id}/appStoreVersions`, {
|
|
61
|
+
query: { limit: 1 },
|
|
62
|
+
});
|
|
63
|
+
if (v.data[0]) {
|
|
64
|
+
item.latestVersion = {
|
|
65
|
+
id: v.data[0].id,
|
|
66
|
+
versionString: v.data[0].attributes.versionString,
|
|
67
|
+
state: v.data[0].attributes.appStoreState,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (includeLatestBuild) {
|
|
72
|
+
const b = await client.request(`/builds`, {
|
|
73
|
+
query: { "filter[app]": app.id, limit: 1, sort: "-uploadedDate" },
|
|
74
|
+
});
|
|
75
|
+
if (b.data[0]) {
|
|
76
|
+
item.latestBuild = {
|
|
77
|
+
id: b.data[0].id,
|
|
78
|
+
version: b.data[0].attributes.version,
|
|
79
|
+
processingState: b.data[0].attributes.processingState,
|
|
80
|
+
expired: b.data[0].attributes.expired,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
results.push(item);
|
|
85
|
+
}
|
|
86
|
+
return { matches: results };
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "asc_workflow_whats_new_from_git",
|
|
91
|
+
description: "Read `git log <since>..HEAD` from a repo path and return a draft 'What's New' note. Trims to 4000 chars. Does NOT push — caller pushes via asc_versions_update_localization.",
|
|
92
|
+
inputSchema: z.object({
|
|
93
|
+
repoPath: z.string().describe("Absolute path to a git repository"),
|
|
94
|
+
since: z.string().describe("Git ref to diff from, e.g. last tag like v1.2.3"),
|
|
95
|
+
maxBullets: z.number().int().min(1).max(50).default(10),
|
|
96
|
+
}),
|
|
97
|
+
handler: async ({ repoPath, since, maxBullets }) => {
|
|
98
|
+
const log = execSync(`git -C "${repoPath}" log --pretty=format:%s ${since}..HEAD`, {
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
});
|
|
101
|
+
const lines = log
|
|
102
|
+
.split("\n")
|
|
103
|
+
.map((l) => l.trim())
|
|
104
|
+
.filter((l) => l.length > 0 && !/^(chore|ci|docs|test|build|merge)/i.test(l))
|
|
105
|
+
.slice(0, maxBullets);
|
|
106
|
+
const bullets = lines.map((l) => `• ${l.replace(/^([a-z]+)(\([^)]+\))?:\s*/i, "")}`).join("\n");
|
|
107
|
+
return { draft: bullets.slice(0, 4000), commitCount: lines.length };
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: "asc_workflow_localize_metadata",
|
|
112
|
+
description: "Push a set of translated metadata to a version, locale-by-locale. The CALLER (LLM) does the translation; this tool just writes. Validates char limits.",
|
|
113
|
+
inputSchema: z.object({
|
|
114
|
+
versionId: z.string(),
|
|
115
|
+
translations: z.record(z.enum(APP_STORE_LOCALES), z.object({
|
|
116
|
+
description: z.string().optional(),
|
|
117
|
+
keywords: z.string().optional(),
|
|
118
|
+
promotionalText: z.string().optional(),
|
|
119
|
+
whatsNew: z.string().optional(),
|
|
120
|
+
marketingUrl: z.string().optional(),
|
|
121
|
+
supportUrl: z.string().optional(),
|
|
122
|
+
})),
|
|
123
|
+
dryRun: z.boolean().optional(),
|
|
124
|
+
}),
|
|
125
|
+
handler: async ({ versionId, translations, dryRun }) => {
|
|
126
|
+
const entries = Object.entries(translations);
|
|
127
|
+
// Validate char limits
|
|
128
|
+
for (const [loc, t] of entries) {
|
|
129
|
+
for (const [field, value] of Object.entries(t)) {
|
|
130
|
+
if (typeof value !== "string")
|
|
131
|
+
continue;
|
|
132
|
+
const limit = METADATA_CHAR_LIMITS[field];
|
|
133
|
+
if (limit && value.length > limit) {
|
|
134
|
+
throw new Error(`${loc}.${field} is ${value.length} chars (limit ${limit})`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const existing = await listAll(client, `/appStoreVersions/${versionId}/appStoreVersionLocalizations`);
|
|
139
|
+
const byLocale = new Map(existing.map((l) => [l.attributes.locale, l]));
|
|
140
|
+
const results = [];
|
|
141
|
+
for (const [locale, attrs] of entries) {
|
|
142
|
+
const loc = byLocale.get(locale);
|
|
143
|
+
if (loc) {
|
|
144
|
+
const body = patchBody("appStoreVersionLocalizations", loc.id, attrs);
|
|
145
|
+
const r = await maybeSend(client, dryRun, "PATCH", `/appStoreVersionLocalizations/${loc.id}`, body);
|
|
146
|
+
results.push({ locale, action: "updated", result: r });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
const body = postBody("appStoreVersionLocalizations", { locale, ...attrs }, { appStoreVersion: singleRel("appStoreVersions", versionId) });
|
|
150
|
+
const r = await maybeSend(client, dryRun, "POST", `/appStoreVersionLocalizations`, body);
|
|
151
|
+
results.push({ locale, action: "created", result: r });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { processed: results.length, results };
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "asc_workflow_testflight_quick_ship",
|
|
159
|
+
description: "Find the newest VALID build for an app, attach it to one or more Beta Groups, set 'What to Test' notes, and optionally submit for beta review.",
|
|
160
|
+
inputSchema: z.object({
|
|
161
|
+
appId: z.string(),
|
|
162
|
+
groupIds: z.array(z.string()).min(1),
|
|
163
|
+
whatToTest: z.record(z.enum(APP_STORE_LOCALES), z.string()).optional(),
|
|
164
|
+
submitForBetaReview: z.boolean().default(false),
|
|
165
|
+
dryRun: z.boolean().optional(),
|
|
166
|
+
}),
|
|
167
|
+
handler: async ({ appId, groupIds, whatToTest, submitForBetaReview, dryRun }) => {
|
|
168
|
+
const builds = await client.request(`/builds`, {
|
|
169
|
+
query: {
|
|
170
|
+
"filter[app]": appId,
|
|
171
|
+
"filter[processingState]": "VALID",
|
|
172
|
+
"filter[expired]": "false",
|
|
173
|
+
limit: 1,
|
|
174
|
+
sort: "-uploadedDate",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
const build = builds.data[0];
|
|
178
|
+
if (!build)
|
|
179
|
+
throw new Error("No valid non-expired build found.");
|
|
180
|
+
const buildId = build.id;
|
|
181
|
+
const actions = [{ found: { buildId, version: build.attributes.version } }];
|
|
182
|
+
for (const groupId of groupIds) {
|
|
183
|
+
const body = { data: [{ type: "builds", id: buildId }] };
|
|
184
|
+
const r = await maybeSend(client, dryRun, "POST", `/betaGroups/${groupId}/relationships/builds`, body);
|
|
185
|
+
actions.push({ addedToGroup: groupId, result: r });
|
|
186
|
+
}
|
|
187
|
+
if (whatToTest) {
|
|
188
|
+
const existing = await listAll(client, `/builds/${buildId}/betaBuildLocalizations`);
|
|
189
|
+
const byLocale = new Map(existing.map((l) => [l.attributes.locale, l]));
|
|
190
|
+
for (const [locale, text] of Object.entries(whatToTest)) {
|
|
191
|
+
const loc = byLocale.get(locale);
|
|
192
|
+
if (loc) {
|
|
193
|
+
const body = patchBody("betaBuildLocalizations", loc.id, { whatsNew: text });
|
|
194
|
+
const r = await maybeSend(client, dryRun, "PATCH", `/betaBuildLocalizations/${loc.id}`, body);
|
|
195
|
+
actions.push({ updatedWhatToTest: locale, result: r });
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const body = postBody("betaBuildLocalizations", { locale, whatsNew: text }, { build: singleRel("builds", buildId) });
|
|
199
|
+
const r = await maybeSend(client, dryRun, "POST", `/betaBuildLocalizations`, body);
|
|
200
|
+
actions.push({ createdWhatToTest: locale, result: r });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (submitForBetaReview) {
|
|
205
|
+
const body = postBody("betaAppReviewSubmissions", undefined, { build: singleRel("builds", buildId) });
|
|
206
|
+
const r = await maybeSend(client, dryRun, "POST", `/betaAppReviewSubmissions`, body);
|
|
207
|
+
actions.push({ submittedForBetaReview: true, result: r });
|
|
208
|
+
}
|
|
209
|
+
return { actions };
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
name: "asc_workflow_submission_health",
|
|
214
|
+
description: "Audit a version for submission readiness: localization completeness, screenshots, export compliance, review details, build attachment, age rating. Returns a structured pass/fail report.",
|
|
215
|
+
inputSchema: z.object({
|
|
216
|
+
versionId: z.string(),
|
|
217
|
+
}),
|
|
218
|
+
handler: async ({ versionId }) => {
|
|
219
|
+
const issues = [];
|
|
220
|
+
const checks = [];
|
|
221
|
+
const version = await client.request(`/appStoreVersions/${versionId}`);
|
|
222
|
+
const primaryLocale = version.data.attributes.primaryLocale;
|
|
223
|
+
// Build attachment
|
|
224
|
+
try {
|
|
225
|
+
const build = await client.request(`/appStoreVersions/${versionId}/build`);
|
|
226
|
+
if (!build.data) {
|
|
227
|
+
issues.push({ severity: "error", check: "build", detail: "No build attached" });
|
|
228
|
+
checks.push({ check: "build", status: "fail" });
|
|
229
|
+
}
|
|
230
|
+
else if (build.data.attributes.processingState !== "VALID") {
|
|
231
|
+
issues.push({ severity: "error", check: "build", detail: `Build in state ${build.data.attributes.processingState}` });
|
|
232
|
+
checks.push({ check: "build", status: "fail" });
|
|
233
|
+
}
|
|
234
|
+
else if (build.data.attributes.usesNonExemptEncryption === null) {
|
|
235
|
+
issues.push({ severity: "error", check: "exportCompliance", detail: "Export compliance not declared" });
|
|
236
|
+
checks.push({ check: "exportCompliance", status: "fail" });
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
checks.push({ check: "build", status: "pass" });
|
|
240
|
+
checks.push({ check: "exportCompliance", status: "pass" });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
checks.push({ check: "build", status: "fail" });
|
|
245
|
+
issues.push({ severity: "error", check: "build", detail: e.message });
|
|
246
|
+
}
|
|
247
|
+
// Localizations + screenshots
|
|
248
|
+
const locs = await listAll(client, `/appStoreVersions/${versionId}/appStoreVersionLocalizations`);
|
|
249
|
+
for (const loc of locs) {
|
|
250
|
+
const locale = loc.attributes.locale;
|
|
251
|
+
const required = ["description", "keywords"];
|
|
252
|
+
for (const f of required) {
|
|
253
|
+
if (!loc.attributes[f]) {
|
|
254
|
+
issues.push({ severity: "error", check: `metadata:${locale}`, detail: `Missing ${f}` });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (!loc.attributes.whatsNew && primaryLocale && locale === primaryLocale) {
|
|
258
|
+
issues.push({ severity: "warning", check: `metadata:${locale}`, detail: "Missing whatsNew" });
|
|
259
|
+
}
|
|
260
|
+
const screenshotSets = await client.request(`/appStoreVersionLocalizations/${loc.id}/appScreenshotSets`, { query: { limit: 50 } });
|
|
261
|
+
if (screenshotSets.data.length === 0) {
|
|
262
|
+
issues.push({ severity: "error", check: `screenshots:${locale}`, detail: "No screenshot sets" });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
checks.push({ check: "localizations", status: issues.some((i) => i.check.startsWith("metadata:") || i.check.startsWith("screenshots:")) ? "fail" : "pass" });
|
|
266
|
+
// Review details
|
|
267
|
+
try {
|
|
268
|
+
const detail = await client.request(`/appStoreVersions/${versionId}/appStoreReviewDetail`);
|
|
269
|
+
if (!detail.data) {
|
|
270
|
+
issues.push({ severity: "warning", check: "reviewDetails", detail: "No App Store Review Detail set" });
|
|
271
|
+
checks.push({ check: "reviewDetails", status: "warn" });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const a = detail.data.attributes;
|
|
275
|
+
if (a.demoAccountRequired && (!a.demoAccountName || !a.demoAccountPassword)) {
|
|
276
|
+
issues.push({ severity: "error", check: "reviewDetails", detail: "demoAccountRequired but credentials missing" });
|
|
277
|
+
}
|
|
278
|
+
if (!a.contactFirstName || !a.contactEmail || !a.contactPhone) {
|
|
279
|
+
issues.push({ severity: "warning", check: "reviewDetails", detail: "Contact info incomplete" });
|
|
280
|
+
}
|
|
281
|
+
checks.push({ check: "reviewDetails", status: "pass" });
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
checks.push({ check: "reviewDetails", status: "warn" });
|
|
286
|
+
}
|
|
287
|
+
// Age rating
|
|
288
|
+
try {
|
|
289
|
+
const ar = await client.request(`/appStoreVersions/${versionId}/ageRatingDeclaration`);
|
|
290
|
+
if (!ar.data) {
|
|
291
|
+
issues.push({ severity: "error", check: "ageRating", detail: "No age rating declaration" });
|
|
292
|
+
checks.push({ check: "ageRating", status: "fail" });
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
checks.push({ check: "ageRating", status: "pass" });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
checks.push({ check: "ageRating", status: "fail" });
|
|
300
|
+
}
|
|
301
|
+
const errors = issues.filter((i) => i.severity === "error").length;
|
|
302
|
+
return {
|
|
303
|
+
versionId,
|
|
304
|
+
status: errors === 0 ? "ready" : "blocked",
|
|
305
|
+
errors,
|
|
306
|
+
warnings: issues.filter((i) => i.severity === "warning").length,
|
|
307
|
+
checks,
|
|
308
|
+
issues,
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "asc_workflow_aso_audit",
|
|
314
|
+
description: "For each version localization: surface keyword duplicates, unused keyword characters (100 limit), and words that also appear in the description (wasted). Optionally diff against competitor keyword strings.",
|
|
315
|
+
inputSchema: z.object({
|
|
316
|
+
versionId: z.string(),
|
|
317
|
+
competitorKeywords: z.array(z.string()).optional().describe("Other keyword strings to diff against"),
|
|
318
|
+
}),
|
|
319
|
+
handler: async ({ versionId, competitorKeywords }) => {
|
|
320
|
+
const locs = await listAll(client, `/appStoreVersions/${versionId}/appStoreVersionLocalizations`);
|
|
321
|
+
const report = [];
|
|
322
|
+
for (const loc of locs) {
|
|
323
|
+
const locale = loc.attributes.locale;
|
|
324
|
+
const keywords = (loc.attributes.keywords || "").trim();
|
|
325
|
+
const description = (loc.attributes.description || "").toLowerCase();
|
|
326
|
+
const kws = keywords.split(",").map((k) => k.trim()).filter(Boolean);
|
|
327
|
+
const lowered = kws.map((k) => k.toLowerCase());
|
|
328
|
+
const dupes = lowered.filter((k, i) => lowered.indexOf(k) !== i);
|
|
329
|
+
const inDescription = kws.filter((k) => description.includes(k.toLowerCase()));
|
|
330
|
+
const used = keywords.length;
|
|
331
|
+
const item = {
|
|
332
|
+
locale,
|
|
333
|
+
keywordCount: kws.length,
|
|
334
|
+
charsUsed: used,
|
|
335
|
+
charsRemaining: 100 - used,
|
|
336
|
+
duplicates: Array.from(new Set(dupes)),
|
|
337
|
+
wastedInDescription: inDescription,
|
|
338
|
+
};
|
|
339
|
+
if (competitorKeywords?.length) {
|
|
340
|
+
const competitorSet = new Set(competitorKeywords
|
|
341
|
+
.flatMap((s) => s.split(",").map((k) => k.trim().toLowerCase()))
|
|
342
|
+
.filter((s) => s.length > 0));
|
|
343
|
+
const mySet = new Set(lowered);
|
|
344
|
+
item.competitorGap = Array.from(competitorSet).filter((k) => !mySet.has(k));
|
|
345
|
+
item.competitorOverlap = Array.from(mySet).filter((k) => competitorSet.has(k));
|
|
346
|
+
}
|
|
347
|
+
report.push(item);
|
|
348
|
+
}
|
|
349
|
+
return { versionId, locales: report };
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "asc_workflow_build_lifecycle",
|
|
354
|
+
description: "List all builds for an app with state + expiration days remaining. Flag expiring in <= 7 days. Optionally mark expired ones expired.",
|
|
355
|
+
inputSchema: z.object({
|
|
356
|
+
appId: z.string(),
|
|
357
|
+
warnWithinDays: z.number().int().min(1).max(90).default(7),
|
|
358
|
+
cleanupExpired: z.boolean().default(false),
|
|
359
|
+
dryRun: z.boolean().optional(),
|
|
360
|
+
}),
|
|
361
|
+
handler: async ({ appId, warnWithinDays, cleanupExpired, dryRun }) => {
|
|
362
|
+
const builds = await listAll(client, `/builds`, { "filter[app]": appId, sort: "-uploadedDate" });
|
|
363
|
+
const now = Date.now();
|
|
364
|
+
const report = [];
|
|
365
|
+
const actions = [];
|
|
366
|
+
for (const b of builds) {
|
|
367
|
+
const exp = b.attributes.expirationDate ? new Date(b.attributes.expirationDate).getTime() : null;
|
|
368
|
+
const daysLeft = exp ? Math.round((exp - now) / 86400000) : null;
|
|
369
|
+
const flag = b.attributes.expired ? "expired" :
|
|
370
|
+
b.attributes.processingState !== "VALID" ? b.attributes.processingState :
|
|
371
|
+
daysLeft !== null && daysLeft <= warnWithinDays ? "expiring_soon" :
|
|
372
|
+
"ok";
|
|
373
|
+
report.push({
|
|
374
|
+
id: b.id,
|
|
375
|
+
version: b.attributes.version,
|
|
376
|
+
state: b.attributes.processingState,
|
|
377
|
+
expired: b.attributes.expired,
|
|
378
|
+
uploadedDate: b.attributes.uploadedDate,
|
|
379
|
+
daysUntilExpiry: daysLeft,
|
|
380
|
+
flag,
|
|
381
|
+
});
|
|
382
|
+
if (cleanupExpired && flag === "ok" && b.attributes.expired) {
|
|
383
|
+
const body = patchBody("builds", b.id, { expired: true });
|
|
384
|
+
const r = await maybeSend(client, dryRun, "PATCH", `/builds/${b.id}`, body);
|
|
385
|
+
actions.push({ buildId: b.id, action: "marked_expired", result: r });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
total: report.length,
|
|
390
|
+
expiringSoon: report.filter((r) => r.flag === "expiring_soon").length,
|
|
391
|
+
expired: report.filter((r) => r.expired).length,
|
|
392
|
+
builds: report,
|
|
393
|
+
actions,
|
|
394
|
+
};
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "asc_workflow_crash_triage",
|
|
399
|
+
description: "Pull top diagnostic signatures across recent builds for an app, group by type, return summarized triage view.",
|
|
400
|
+
inputSchema: z.object({
|
|
401
|
+
appId: z.string(),
|
|
402
|
+
buildLimit: z.number().int().min(1).max(20).default(5),
|
|
403
|
+
signaturesPerBuild: z.number().int().min(1).max(50).default(20),
|
|
404
|
+
}),
|
|
405
|
+
handler: async ({ appId, buildLimit, signaturesPerBuild }) => {
|
|
406
|
+
const builds = await client.request(`/builds`, {
|
|
407
|
+
query: { "filter[app]": appId, "filter[processingState]": "VALID", limit: buildLimit, sort: "-uploadedDate" },
|
|
408
|
+
});
|
|
409
|
+
const grouped = {};
|
|
410
|
+
for (const b of builds.data) {
|
|
411
|
+
try {
|
|
412
|
+
const sigs = await client.request(`/builds/${b.id}/diagnosticSignatures`, {
|
|
413
|
+
query: { limit: signaturesPerBuild },
|
|
414
|
+
});
|
|
415
|
+
for (const s of sigs.data) {
|
|
416
|
+
const type = s.attributes.diagnosticType || "UNKNOWN";
|
|
417
|
+
if (!grouped[type])
|
|
418
|
+
grouped[type] = { count: 0, signatures: [] };
|
|
419
|
+
grouped[type].count++;
|
|
420
|
+
grouped[type].signatures.push({
|
|
421
|
+
buildVersion: b.attributes.version,
|
|
422
|
+
buildId: b.id,
|
|
423
|
+
signatureId: s.id,
|
|
424
|
+
signature: s.attributes.signature,
|
|
425
|
+
weight: s.attributes.weight,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// Some accounts don't have P&P data; skip silently.
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return { appId, buildsExamined: builds.data.length, byType: grouped };
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: "asc_workflow_release_next_version",
|
|
438
|
+
description: "Cut a new App Store Version: create version, copy metadata from the most recent prior version, attach latest valid build, carry over export compliance, set release type. Caller pushes 'what's new' separately (use whats_new_from_git first). Does NOT submit.",
|
|
439
|
+
inputSchema: z.object({
|
|
440
|
+
appId: z.string(),
|
|
441
|
+
platform: z.enum(["IOS", "MAC_OS", "TV_OS", "VISION_OS"]),
|
|
442
|
+
newVersionString: z.string(),
|
|
443
|
+
releaseType: z.enum(["MANUAL", "AFTER_APPROVAL", "SCHEDULED"]).default("AFTER_APPROVAL"),
|
|
444
|
+
earliestReleaseDate: z.string().optional(),
|
|
445
|
+
copyright: z.string().optional(),
|
|
446
|
+
attachLatestBuild: z.boolean().default(true),
|
|
447
|
+
dryRun: z.boolean().optional(),
|
|
448
|
+
}),
|
|
449
|
+
handler: async ({ appId, platform, newVersionString, releaseType, earliestReleaseDate, copyright, attachLatestBuild, dryRun }) => {
|
|
450
|
+
// 1. Previous version (ASC returns recent-first by default for this endpoint)
|
|
451
|
+
const priors = await client.request(`/apps/${appId}/appStoreVersions`, {
|
|
452
|
+
query: { "filter[platform]": platform, limit: 5 },
|
|
453
|
+
});
|
|
454
|
+
const prior = priors.data[0];
|
|
455
|
+
if (!prior)
|
|
456
|
+
throw new Error("No prior version found to copy from.");
|
|
457
|
+
// 2. Latest build
|
|
458
|
+
let buildId;
|
|
459
|
+
if (attachLatestBuild) {
|
|
460
|
+
const builds = await client.request(`/builds`, {
|
|
461
|
+
query: { "filter[app]": appId, "filter[processingState]": "VALID", "filter[expired]": "false", limit: 1, sort: "-uploadedDate" },
|
|
462
|
+
});
|
|
463
|
+
buildId = builds.data[0]?.id;
|
|
464
|
+
}
|
|
465
|
+
// 3. Create new version
|
|
466
|
+
const createAttrs = { platform, versionString: newVersionString, releaseType };
|
|
467
|
+
if (copyright)
|
|
468
|
+
createAttrs.copyright = copyright;
|
|
469
|
+
if (earliestReleaseDate)
|
|
470
|
+
createAttrs.earliestReleaseDate = earliestReleaseDate;
|
|
471
|
+
const createRels = {
|
|
472
|
+
app: { data: { type: "apps", id: appId } },
|
|
473
|
+
};
|
|
474
|
+
if (buildId)
|
|
475
|
+
createRels.build = { data: { type: "builds", id: buildId } };
|
|
476
|
+
const createBody = postBody("appStoreVersions", createAttrs, createRels);
|
|
477
|
+
if (dryRun) {
|
|
478
|
+
return {
|
|
479
|
+
dryRun: true,
|
|
480
|
+
plan: {
|
|
481
|
+
copyingFrom: { versionId: prior.id, versionString: prior.attributes.versionString },
|
|
482
|
+
attachingBuildId: buildId,
|
|
483
|
+
createPayload: createBody,
|
|
484
|
+
},
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
const created = await client.request(`/appStoreVersions`, {
|
|
488
|
+
method: "POST",
|
|
489
|
+
body: createBody,
|
|
490
|
+
});
|
|
491
|
+
const newVersionId = created.data.id;
|
|
492
|
+
// 4. Copy localizations
|
|
493
|
+
const priorLocs = await listAll(client, `/appStoreVersions/${prior.id}/appStoreVersionLocalizations`);
|
|
494
|
+
const copied = [];
|
|
495
|
+
for (const pl of priorLocs) {
|
|
496
|
+
const attrs = {
|
|
497
|
+
description: pl.attributes.description,
|
|
498
|
+
keywords: pl.attributes.keywords,
|
|
499
|
+
marketingUrl: pl.attributes.marketingUrl,
|
|
500
|
+
promotionalText: pl.attributes.promotionalText,
|
|
501
|
+
supportUrl: pl.attributes.supportUrl,
|
|
502
|
+
// intentionally not copying whatsNew — that's per-release
|
|
503
|
+
};
|
|
504
|
+
const body = postBody("appStoreVersionLocalizations", { locale: pl.attributes.locale, ...attrs }, { appStoreVersion: singleRel("appStoreVersions", newVersionId) });
|
|
505
|
+
const r = await client.request(`/appStoreVersionLocalizations`, { method: "POST", body });
|
|
506
|
+
copied.push({ locale: pl.attributes.locale, result: r });
|
|
507
|
+
}
|
|
508
|
+
// 5. Carry over export compliance from build
|
|
509
|
+
if (buildId) {
|
|
510
|
+
// Only need to set if not yet declared on the build; safe to set to current value.
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
newVersionId,
|
|
514
|
+
versionString: newVersionString,
|
|
515
|
+
copiedLocalizations: copied.length,
|
|
516
|
+
attachedBuildId: buildId ?? null,
|
|
517
|
+
nextSteps: [
|
|
518
|
+
"Use asc_workflow_whats_new_from_git to draft per-locale What's New, then asc_workflow_localize_metadata to push.",
|
|
519
|
+
"Use asc_workflow_submission_health to verify readiness.",
|
|
520
|
+
"Use asc_versions_submit_for_review_v1 or asc_review_submissions_create + submit to submit.",
|
|
521
|
+
],
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
name: "asc_workflow_metadata_sync",
|
|
527
|
+
description: "Pull metadata for a version into a JSON object you can save locally, or push from a JSON object back to ASC. Useful for version-controlled metadata.",
|
|
528
|
+
inputSchema: z.object({
|
|
529
|
+
versionId: z.string(),
|
|
530
|
+
direction: z.enum(["pull", "push"]),
|
|
531
|
+
data: z.record(z.string(), z.record(z.string(), z.unknown())).optional().describe("For push: { locale: { description, keywords, ... } }"),
|
|
532
|
+
dryRun: z.boolean().optional(),
|
|
533
|
+
}),
|
|
534
|
+
handler: async ({ versionId, direction, data, dryRun }) => {
|
|
535
|
+
if (direction === "pull") {
|
|
536
|
+
const locs = await listAll(client, `/appStoreVersions/${versionId}/appStoreVersionLocalizations`);
|
|
537
|
+
const out = {};
|
|
538
|
+
for (const l of locs) {
|
|
539
|
+
const a = l.attributes;
|
|
540
|
+
out[a.locale] = {
|
|
541
|
+
description: a.description,
|
|
542
|
+
keywords: a.keywords,
|
|
543
|
+
marketingUrl: a.marketingUrl,
|
|
544
|
+
promotionalText: a.promotionalText,
|
|
545
|
+
supportUrl: a.supportUrl,
|
|
546
|
+
whatsNew: a.whatsNew,
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
return out;
|
|
550
|
+
}
|
|
551
|
+
if (!data)
|
|
552
|
+
throw new Error("push direction requires `data`");
|
|
553
|
+
const locs = await listAll(client, `/appStoreVersions/${versionId}/appStoreVersionLocalizations`);
|
|
554
|
+
const byLocale = new Map(locs.map((l) => [l.attributes.locale, l]));
|
|
555
|
+
const results = [];
|
|
556
|
+
for (const [locale, attrs] of Object.entries(data)) {
|
|
557
|
+
const loc = byLocale.get(locale);
|
|
558
|
+
if (loc) {
|
|
559
|
+
const body = patchBody("appStoreVersionLocalizations", loc.id, attrs);
|
|
560
|
+
const r = await maybeSend(client, dryRun, "PATCH", `/appStoreVersionLocalizations/${loc.id}`, body);
|
|
561
|
+
results.push({ locale, action: "updated", result: r });
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
const body = postBody("appStoreVersionLocalizations", { locale, ...attrs }, { appStoreVersion: singleRel("appStoreVersions", versionId) });
|
|
565
|
+
const r = await maybeSend(client, dryRun, "POST", `/appStoreVersionLocalizations`, body);
|
|
566
|
+
results.push({ locale, action: "created", result: r });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return { processed: results.length, results };
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: "asc_workflow_review_responses",
|
|
574
|
+
description: "List recent customer reviews for an app, optionally only ones without a developer response yet. Caller drafts and submits responses separately.",
|
|
575
|
+
inputSchema: z.object({
|
|
576
|
+
appId: z.string(),
|
|
577
|
+
onlyUnanswered: z.boolean().default(true),
|
|
578
|
+
limit: z.number().int().min(1).max(200).default(50),
|
|
579
|
+
}),
|
|
580
|
+
handler: async ({ appId, onlyUnanswered, limit }) => {
|
|
581
|
+
const reviews = await client.request(`/apps/${appId}/customerReviews`, { query: { limit, sort: "-createdDate", include: ["response"] } });
|
|
582
|
+
const responseIds = new Set((reviews.included ?? []).filter((i) => i.type === "customerReviewResponses").map((i) => i.id));
|
|
583
|
+
const list = reviews.data.map((r) => {
|
|
584
|
+
const hasResponse = r.relationships?.response?.data?.id && responseIds.has(r.relationships.response.data.id);
|
|
585
|
+
return {
|
|
586
|
+
id: r.id,
|
|
587
|
+
rating: r.attributes.rating,
|
|
588
|
+
title: r.attributes.title,
|
|
589
|
+
body: r.attributes.body,
|
|
590
|
+
createdDate: r.attributes.createdDate,
|
|
591
|
+
territory: r.attributes.territory,
|
|
592
|
+
reviewerNickname: r.attributes.reviewerNickname,
|
|
593
|
+
hasResponse,
|
|
594
|
+
};
|
|
595
|
+
});
|
|
596
|
+
return { reviews: onlyUnanswered ? list.filter((r) => !r.hasResponse) : list };
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
{
|
|
600
|
+
name: "asc_workflow_analytics_oneshot",
|
|
601
|
+
description: "Convenience: create an analytics report request, poll until reports are available, list segments. Returns the segment URLs to download.",
|
|
602
|
+
inputSchema: z.object({
|
|
603
|
+
appId: z.string(),
|
|
604
|
+
accessType: z.enum(["ONGOING", "ONE_TIME_SNAPSHOT"]).default("ONE_TIME_SNAPSHOT"),
|
|
605
|
+
category: z.enum(["APP_USAGE", "APP_STORE_ENGAGEMENT", "COMMERCE", "FRAMEWORKS_USAGE", "PERFORMANCE"]).optional(),
|
|
606
|
+
maxWaitSeconds: z.number().int().min(10).max(600).default(120),
|
|
607
|
+
}),
|
|
608
|
+
handler: async ({ appId, accessType, category, maxWaitSeconds }) => {
|
|
609
|
+
const created = await client.request(`/analyticsReportRequests`, {
|
|
610
|
+
method: "POST",
|
|
611
|
+
body: postBody("analyticsReportRequests", { accessType }, { app: singleRel("apps", appId) }),
|
|
612
|
+
});
|
|
613
|
+
const requestId = created.data.id;
|
|
614
|
+
const deadline = Date.now() + maxWaitSeconds * 1000;
|
|
615
|
+
let reports = [];
|
|
616
|
+
while (Date.now() < deadline) {
|
|
617
|
+
const r = await client.request(`/analyticsReportRequests/${requestId}/reports`, {
|
|
618
|
+
query: { "filter[category]": category, limit: 200 },
|
|
619
|
+
});
|
|
620
|
+
if (r.data.length > 0) {
|
|
621
|
+
reports = r.data;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
await new Promise((res) => setTimeout(res, 5000));
|
|
625
|
+
}
|
|
626
|
+
if (reports.length === 0)
|
|
627
|
+
return { requestId, status: "no_reports_yet", reports: [] };
|
|
628
|
+
return { requestId, reports: reports.map((r) => ({ id: r.id, name: r.attributes.name, category: r.attributes.category })) };
|
|
629
|
+
},
|
|
630
|
+
},
|
|
631
|
+
];
|
|
632
|
+
}
|
|
633
|
+
//# sourceMappingURL=workflows.js.map
|