@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nomac/cli",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Ship iOS apps to TestFlight and the App Store without a Mac — built for agents.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
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, ask the human for an API
22
- key from nomac.app → API keys, then `npx @nomac/cli login <nmk_…>` and
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` (no confirm) staged dry-run; fix any Apple blockers it returns.
54
- 4. Ask the human explicitly before `publish confirm=true` it's irreversible.
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 publish/set_metadata arguments with its returned request_key. This status call performs no Apple writes.",
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). IRREVERSIBLE once confirmed requires confirm:true; 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.",
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: