@nomac/cli 0.2.3 → 0.2.4
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/package.json +1 -1
- package/skill/SKILL.md +51 -5
- package/src/lifecycle-schemas.json +215 -0
- package/src/mcp.mjs +29 -3
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -18,9 +18,9 @@ via MCP tools (preferred) or the `nomac` CLI.
|
|
|
18
18
|
## Setup check
|
|
19
19
|
|
|
20
20
|
1. MCP tools available (`connect_status`, `push_project`, `build`, …)? Use them.
|
|
21
|
-
2. Otherwise: `npx @nomac/cli whoami`. If not logged in,
|
|
22
|
-
|
|
23
|
-
`claude mcp add nomac -- npx @nomac/cli mcp`.
|
|
21
|
+
2. Otherwise: `npx @nomac/cli whoami`. If not logged in, run
|
|
22
|
+
`npx @nomac/cli login`; give the human the device URL and code to approve.
|
|
23
|
+
Then `claude mcp add nomac -- npx @nomac/cli mcp`.
|
|
24
24
|
3. `connect_status` must show a healthy Apple connection. If not, the human
|
|
25
25
|
connects at the dashboard (60 seconds, one .p8 upload). Don't guess —
|
|
26
26
|
verify, then proceed.
|
|
@@ -50,8 +50,11 @@ status # poll ~30s until ready | failed
|
|
|
50
50
|
(in the schema). It runs in the background: poll `get_screenshot_upload`
|
|
51
51
|
until complete before publishing. A rolled-back or paused replacement
|
|
52
52
|
needs the returned recovery instructions before starting another upload.
|
|
53
|
-
3. `publish`
|
|
54
|
-
|
|
53
|
+
3. `publish` without confirm stages real Apple drafts/build attachments but
|
|
54
|
+
does not submit. Fix the returned blockers; retain the exact build/submission IDs.
|
|
55
|
+
4. `publish confirm=true` sends the app to Apple. Ensure the human authorized
|
|
56
|
+
this app/version; honor authorization already given. New versions use manual
|
|
57
|
+
release, while existing versions retain their Apple release policy.
|
|
55
58
|
|
|
56
59
|
If metadata or publish returns pending, keep its `request_key` and repeat
|
|
57
60
|
the original arguments with that key. `get_store_operation` recovers the key
|
|
@@ -60,6 +63,49 @@ do not use one to bypass an unconfirmed Apple request. Staging and confirmation
|
|
|
60
63
|
remain separate invocations. For screenshots, keep the original `request_key`
|
|
61
64
|
too, and use `get_screenshot_upload` to find or poll the existing operation.
|
|
62
65
|
|
|
66
|
+
## Fix review rejections and release
|
|
67
|
+
|
|
68
|
+
- `get_review` lists live Apple submissions, including ones created outside
|
|
69
|
+
nomac. Pass `asc_submission_id` for all item states and exact version/build IDs.
|
|
70
|
+
- Apple does not expose App Review messages through its public API. Use the
|
|
71
|
+
returned App Store Connect handoff to have the human provide the actual text.
|
|
72
|
+
Never invent a rejection reason from a status code or public customer review.
|
|
73
|
+
- Fix metadata/screenshots on the exact `version_string`. A binary change needs
|
|
74
|
+
push/build, then `publish` with the exact `build_id` and `asc_submission_id`.
|
|
75
|
+
`resolve_rejection:true` explicitly asserts that this version's issue is fixed.
|
|
76
|
+
- `prepare_review_reply` prepares your authored text but always returns `sent:false`.
|
|
77
|
+
The human sends it and any message attachments in App Store Connect. Changing
|
|
78
|
+
review_contact.notes is not a reply; sending a reply is not resubmission.
|
|
79
|
+
- `update_review` supports `resolve_item`, `remove_item`, `cancel`, `resubmit`,
|
|
80
|
+
and `release`. Preview without confirm; confirm the exact requested action.
|
|
81
|
+
Fix issues before marking items resolved. Every rejected item must be resolved
|
|
82
|
+
or removed before resubmitting. Removed items cannot be added back to that submission.
|
|
83
|
+
- Poll `get_review`; approval and release are separate. Release requires an
|
|
84
|
+
app-version item in Pending Developer Release. Apple/account/legal decisions
|
|
85
|
+
and unsupported product edits still require the relevant Apple workflow.
|
|
86
|
+
|
|
87
|
+
## External TestFlight
|
|
88
|
+
|
|
89
|
+
`ready` from a nomac build means internal TestFlight readiness. External testing
|
|
90
|
+
requires its own review and distribution:
|
|
91
|
+
|
|
92
|
+
1. `get_testflight` lists groups, test information and recent Apple builds. Use
|
|
93
|
+
`asc_build_id` for a specific build's beta-review/external state, or `group_id`
|
|
94
|
+
to list testers. Never infer external approval from internal readiness.
|
|
95
|
+
2. `manage_testflight action=metadata` sets app_info (description/feedbackEmail/URLs),
|
|
96
|
+
review_contact (Apple contact/demo fields), locale, and build whats_new.
|
|
97
|
+
3. Select/create an external group. `invite_tester`/`remove_tester` use an explicit
|
|
98
|
+
email/tester ID and group_id. `update_group` can enable an optional public link.
|
|
99
|
+
4. `distribute` requires asc_build_id and explicit group_ids. It submits beta
|
|
100
|
+
review if needed, then assigns those groups. auto_notify defaults false;
|
|
101
|
+
`notify` starts testing after approval. `remove_build` affects one group;
|
|
102
|
+
`expire` stops testing for the build across all groups.
|
|
103
|
+
5. All actions preview without confirm and apply with confirm:true. Pending
|
|
104
|
+
requests use their original key/body; get_store_operation recovers them.
|
|
105
|
+
6. Relay an accurate human-provided uses_non_exempt_encryption with action=encryption
|
|
106
|
+
when appropriate. Never guess. Rejected beta messages and compliance documents
|
|
107
|
+
require App Store Connect; do not blindly resubmit an unchanged rejected binary.
|
|
108
|
+
|
|
63
109
|
## When stuck
|
|
64
110
|
|
|
65
111
|
`report_issue` with the failing `ref_id` files a support ticket with full
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
{
|
|
2
|
+
"update_review": {
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"asc_submission_id": {
|
|
7
|
+
"type": "string",
|
|
8
|
+
"minLength": 1
|
|
9
|
+
},
|
|
10
|
+
"action": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": [
|
|
13
|
+
"resolve_item",
|
|
14
|
+
"remove_item",
|
|
15
|
+
"cancel",
|
|
16
|
+
"resubmit",
|
|
17
|
+
"release"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"item_id": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"minLength": 1
|
|
23
|
+
},
|
|
24
|
+
"confirm": {
|
|
25
|
+
"default": false,
|
|
26
|
+
"type": "boolean"
|
|
27
|
+
},
|
|
28
|
+
"force": {
|
|
29
|
+
"default": false,
|
|
30
|
+
"type": "boolean"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": [
|
|
34
|
+
"asc_submission_id",
|
|
35
|
+
"action"
|
|
36
|
+
],
|
|
37
|
+
"additionalProperties": false
|
|
38
|
+
},
|
|
39
|
+
"prepare_review_reply": {
|
|
40
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"asc_submission_id": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"minLength": 1
|
|
46
|
+
},
|
|
47
|
+
"reply": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"minLength": 1,
|
|
50
|
+
"maxLength": 4000
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"required": [
|
|
54
|
+
"asc_submission_id",
|
|
55
|
+
"reply"
|
|
56
|
+
],
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
},
|
|
59
|
+
"manage_testflight": {
|
|
60
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": {
|
|
63
|
+
"action": {
|
|
64
|
+
"type": "string",
|
|
65
|
+
"enum": [
|
|
66
|
+
"metadata",
|
|
67
|
+
"create_group",
|
|
68
|
+
"update_group",
|
|
69
|
+
"invite_tester",
|
|
70
|
+
"remove_tester",
|
|
71
|
+
"remove_build",
|
|
72
|
+
"distribute",
|
|
73
|
+
"notify",
|
|
74
|
+
"expire",
|
|
75
|
+
"encryption"
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
"asc_build_id": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"minLength": 1
|
|
81
|
+
},
|
|
82
|
+
"group_ids": {
|
|
83
|
+
"minItems": 1,
|
|
84
|
+
"maxItems": 100,
|
|
85
|
+
"type": "array",
|
|
86
|
+
"items": {
|
|
87
|
+
"type": "string",
|
|
88
|
+
"minLength": 1
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"group_id": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"minLength": 1
|
|
94
|
+
},
|
|
95
|
+
"name": {
|
|
96
|
+
"type": "string",
|
|
97
|
+
"minLength": 1,
|
|
98
|
+
"maxLength": 100
|
|
99
|
+
},
|
|
100
|
+
"email": {
|
|
101
|
+
"type": "string",
|
|
102
|
+
"format": "email",
|
|
103
|
+
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
|
|
104
|
+
},
|
|
105
|
+
"first_name": {
|
|
106
|
+
"type": "string",
|
|
107
|
+
"maxLength": 100
|
|
108
|
+
},
|
|
109
|
+
"last_name": {
|
|
110
|
+
"type": "string",
|
|
111
|
+
"maxLength": 100
|
|
112
|
+
},
|
|
113
|
+
"tester_id": {
|
|
114
|
+
"type": "string",
|
|
115
|
+
"minLength": 1
|
|
116
|
+
},
|
|
117
|
+
"public_link_enabled": {
|
|
118
|
+
"type": "boolean"
|
|
119
|
+
},
|
|
120
|
+
"public_link_limit": {
|
|
121
|
+
"type": "integer",
|
|
122
|
+
"minimum": 1,
|
|
123
|
+
"maximum": 10000
|
|
124
|
+
},
|
|
125
|
+
"auto_notify": {
|
|
126
|
+
"default": false,
|
|
127
|
+
"type": "boolean"
|
|
128
|
+
},
|
|
129
|
+
"uses_non_exempt_encryption": {
|
|
130
|
+
"type": "boolean"
|
|
131
|
+
},
|
|
132
|
+
"locale": {
|
|
133
|
+
"default": "en-US",
|
|
134
|
+
"type": "string",
|
|
135
|
+
"minLength": 2,
|
|
136
|
+
"maxLength": 35
|
|
137
|
+
},
|
|
138
|
+
"app_info": {
|
|
139
|
+
"type": "object",
|
|
140
|
+
"properties": {
|
|
141
|
+
"description": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"maxLength": 4000
|
|
144
|
+
},
|
|
145
|
+
"feedbackEmail": {
|
|
146
|
+
"type": "string",
|
|
147
|
+
"format": "email",
|
|
148
|
+
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
|
|
149
|
+
},
|
|
150
|
+
"marketingUrl": {
|
|
151
|
+
"type": "string",
|
|
152
|
+
"maxLength": 255,
|
|
153
|
+
"format": "uri"
|
|
154
|
+
},
|
|
155
|
+
"privacyPolicyUrl": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"maxLength": 255,
|
|
158
|
+
"format": "uri"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"additionalProperties": false
|
|
162
|
+
},
|
|
163
|
+
"review_contact": {
|
|
164
|
+
"type": "object",
|
|
165
|
+
"properties": {
|
|
166
|
+
"contactFirstName": {
|
|
167
|
+
"type": "string",
|
|
168
|
+
"maxLength": 100
|
|
169
|
+
},
|
|
170
|
+
"contactLastName": {
|
|
171
|
+
"type": "string",
|
|
172
|
+
"maxLength": 100
|
|
173
|
+
},
|
|
174
|
+
"contactPhone": {
|
|
175
|
+
"type": "string",
|
|
176
|
+
"maxLength": 100
|
|
177
|
+
},
|
|
178
|
+
"contactEmail": {
|
|
179
|
+
"type": "string",
|
|
180
|
+
"format": "email",
|
|
181
|
+
"pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
|
|
182
|
+
},
|
|
183
|
+
"demoAccountRequired": {
|
|
184
|
+
"type": "boolean"
|
|
185
|
+
},
|
|
186
|
+
"demoAccountName": {
|
|
187
|
+
"type": "string",
|
|
188
|
+
"maxLength": 100
|
|
189
|
+
},
|
|
190
|
+
"demoAccountPassword": {
|
|
191
|
+
"type": "string",
|
|
192
|
+
"maxLength": 100
|
|
193
|
+
},
|
|
194
|
+
"notes": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"maxLength": 4000
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"additionalProperties": false
|
|
200
|
+
},
|
|
201
|
+
"whats_new": {
|
|
202
|
+
"type": "string",
|
|
203
|
+
"maxLength": 4000
|
|
204
|
+
},
|
|
205
|
+
"confirm": {
|
|
206
|
+
"default": false,
|
|
207
|
+
"type": "boolean"
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"required": [
|
|
211
|
+
"action"
|
|
212
|
+
],
|
|
213
|
+
"additionalProperties": false
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/mcp.mjs
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// Works in every MCP client day one; the hosted transport is mcp.nomac.app.
|
|
4
4
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
const lifecycleSchemas = JSON.parse(readFileSync(new URL("./lifecycle-schemas.json", import.meta.url), "utf8"));
|
|
6
8
|
import { createInterface } from "node:readline";
|
|
7
9
|
import { api } from "./api.mjs";
|
|
8
10
|
import { loadProjectState, saveProjectState } from "./config.mjs";
|
|
@@ -236,18 +238,21 @@ export const TOOLS = [
|
|
|
236
238
|
},
|
|
237
239
|
{
|
|
238
240
|
name: "get_store_operation",
|
|
239
|
-
description: "Read saved publish/metadata requests and recover a lost response or request_key. Resume pending work by repeating the original
|
|
241
|
+
description: "Read saved publish/metadata/review/TestFlight requests and recover a lost response or request_key. Resume pending work by repeating the original tool arguments with its returned request_key. This status call performs no Apple writes.",
|
|
240
242
|
inputSchema: { type: "object", properties: { project_id: { type: "string" }, request_key: { type: "string" } }, additionalProperties: false },
|
|
241
243
|
handler: async args => text(await api("GET", `/api/v1/projects/${await resolveProjectId(args)}/operations${args?.request_key ? `?request_key=${encodeURIComponent(args.request_key)}` : ""}`)),
|
|
242
244
|
},
|
|
243
245
|
{
|
|
244
246
|
name: "publish",
|
|
245
247
|
description:
|
|
246
|
-
"Submit for App Store review (3-step reviewSubmissions).
|
|
248
|
+
"Submit for App Store review (3-step reviewSubmissions). Requires confirm:true with authorization for this app/version; run once without confirm first to see the staged result + any Apple blockers. Costs nothing; the review decision takes ~1-3 days. On pending, repeat unchanged arguments with the returned request_key; get_store_operation recovers a lost response.",
|
|
247
249
|
inputSchema: {
|
|
248
250
|
type: "object",
|
|
249
251
|
properties: {
|
|
250
252
|
project_id: { type: "string" },
|
|
253
|
+
build_id: { type: "string", description: "Exact nomac release build to attach" },
|
|
254
|
+
asc_submission_id: { type: "string", description: "Exact Apple submission from get_review" },
|
|
255
|
+
resolve_rejection: { type: "boolean", default: false, description: "Assert this version rejection was fixed; mark its item ready for review" },
|
|
251
256
|
confirm: { type: "boolean", default: false },
|
|
252
257
|
request_key: { type: "string", description: "Only supply to resume an interrupted invocation with its original unchanged arguments" },
|
|
253
258
|
force: { type: "boolean", default: false, description: "override a red lint gate (not recommended)" },
|
|
@@ -264,12 +269,33 @@ export const TOOLS = [
|
|
|
264
269
|
const idempotencyKey = args?.request_key ?? `mcp-pub-${project_id}-${mode}-${randomUUID()}`;
|
|
265
270
|
return text(
|
|
266
271
|
await api("POST", "/api/v1/publish", {
|
|
267
|
-
body: { project_id, confirm: args?.confirm ?? false, force: args?.force ?? false },
|
|
272
|
+
body: { project_id, ...(args?.build_id ? { build_id: args.build_id } : {}), ...(args?.asc_submission_id ? { asc_submission_id: args.asc_submission_id } : {}), ...(args?.resolve_rejection !== undefined ? { resolve_rejection: args.resolve_rejection } : {}), confirm: args?.confirm ?? false, force: args?.force ?? false },
|
|
268
273
|
idempotencyKey,
|
|
269
274
|
}),
|
|
270
275
|
);
|
|
271
276
|
},
|
|
272
277
|
},
|
|
278
|
+
...[
|
|
279
|
+
["get_review", "reviews", "asc_submission_id", "Read live App Review submissions including those created outside nomac. Pass asc_submission_id for all items, version/build/contact details. Reviewer messages require the App Store Connect handoff in the result."],
|
|
280
|
+
["get_testflight", "testflight", "asc_build_id", "Read external/internal groups and test metadata. Pass asc_build_id for exact beta review and distribution state. Internal readiness does not imply external approval."],
|
|
281
|
+
].map(([name, path, filter, description]) => ({
|
|
282
|
+
name, description,
|
|
283
|
+
inputSchema: { type: "object", properties: { project_id: { type: "string" }, [filter]: { type: "string" }, ...(name === "get_testflight" ? { group_id: { type: "string" } } : {}) }, additionalProperties: false },
|
|
284
|
+
handler: async args => text(await api("GET", `/api/v1/projects/${encodeURIComponent(await resolveProjectId(args))}/${path}${args?.[filter] || args?.group_id ? `?${new URLSearchParams({ ...(args?.[filter] ? { [filter]: args[filter] } : {}), ...(args?.group_id ? { group_id: args.group_id } : {}) })}` : ""}`)),
|
|
285
|
+
})),
|
|
286
|
+
...[
|
|
287
|
+
["update_review", "reviews", "Resolve a fixed rejected item, remove an item, cancel review, resubmit ready items or release an approved version. Use exact IDs from get_review. Preview without confirm; confirm:true performs the action. Removal cannot be undone in that submission. Fix actual issues before resolving. On pending, repeat unchanged arguments with request_key; get_store_operation recovers it."],
|
|
288
|
+
["manage_testflight", "testflight", "Set beta metadata/contact/demo credentials, create/update external groups/public links, invite/remove an explicit tester, distribute a specific Apple build to explicit groups and Beta App Review, notify after approval, expire testing, or relay a human-provided encryption answer. Preview without confirm; confirm:true applies. auto_notify defaults false. Never guess attestations. On pending, repeat unchanged arguments with request_key."],
|
|
289
|
+
["prepare_review_reply", "reviews/reply", "Prepare YOUR authored reply with an App Store Connect handoff. Always returns sent:false: Apple's public API cannot read or send reviewer messages. The human must paste and send it. This does not change reviewer notes or public customer reviews."],
|
|
290
|
+
].map(([name, path, description]) => ({
|
|
291
|
+
name, description,
|
|
292
|
+
inputSchema: { ...lifecycleSchemas[name], properties: { project_id: { type: "string" }, ...lifecycleSchemas[name].properties, ...(name !== "prepare_review_reply" ? { request_key: { type: "string" } } : {}) } },
|
|
293
|
+
handler: async args => {
|
|
294
|
+
const projectId = await resolveProjectId(args);
|
|
295
|
+
const { project_id: _project, request_key, ...body } = args;
|
|
296
|
+
return text(await api("POST", `/api/v1/projects/${encodeURIComponent(projectId)}/${path}`, { body, idempotencyKey: request_key ?? randomUUID() }));
|
|
297
|
+
},
|
|
298
|
+
})),
|
|
273
299
|
{
|
|
274
300
|
name: "get_feedback",
|
|
275
301
|
description:
|