@nomac/cli 0.11.10 → 0.11.11
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/src/hosted-sessions.mjs +19 -8
- package/src/mcp.mjs +2 -2
- package/src/store-upload.mjs +6 -5
package/package.json
CHANGED
package/src/hosted-sessions.mjs
CHANGED
|
@@ -308,14 +308,17 @@ export function projectLine({ path, on_drive }) {
|
|
|
308
308
|
return on_drive ? `Project: ${short}, on your drive: kept when the Mac stops, and the next session builds only what changed.`
|
|
309
309
|
: `Project: ${short}, deleted when the Mac stops: nomac pull what you need first.`;
|
|
310
310
|
}
|
|
311
|
+
/** "Mac: macOS 26.6.2, Xcode 27.0 (default) and Xcode 26.6 (/Applications/Xcode-26.6.app), iOS 27.0 Simulator", or "". */
|
|
312
|
+
export function macLine(image) {
|
|
313
|
+
if (!image?.macos) return "";
|
|
314
|
+
// An image with a second Xcode names it and its path, as the API's image line does.
|
|
315
|
+
const others = (image.xcodes ?? []).filter(x => !x.default);
|
|
316
|
+
const xcode = others.length ? `Xcode ${image.xcode} (default) and ${others.map(x => `Xcode ${x.version} (${x.path})`).join(" and ")}` : `Xcode ${image.xcode}`;
|
|
317
|
+
return `Mac: macOS ${image.macos.split(" ")[0]}, ${xcode}${(image.simulator_runtimes ?? []).map(r => `, ${r} Simulator`).join("")}`;
|
|
318
|
+
}
|
|
311
319
|
export function statusSummary(session, image) {
|
|
312
320
|
const lines = [`${session.id}: ${session.state}`];
|
|
313
|
-
if (image
|
|
314
|
-
// An image with a second Xcode names it and its path, as the API's image line does.
|
|
315
|
-
const others = (image.xcodes ?? []).filter(x => !x.default);
|
|
316
|
-
const xcode = others.length ? `Xcode ${image.xcode} (default) and ${others.map(x => `Xcode ${x.version} (${x.path})`).join(" and ")}` : `Xcode ${image.xcode}`;
|
|
317
|
-
lines.push(`Mac: macOS ${image.macos.split(" ")[0]}, ${xcode}${(image.simulator_runtimes ?? []).map(r => `, ${r} Simulator`).join("")}`);
|
|
318
|
-
}
|
|
321
|
+
if (macLine(image)) lines.push(macLine(image));
|
|
319
322
|
if (["ready", "provisioning", "queued"].includes(session.state)) lines.push(`Cost so far: ${usd(session.cost?.accrued_microunits)} ($0.80/hour, billed by the minute while ready). Credit left: nomac credits`);
|
|
320
323
|
const a = session.activity ?? {};
|
|
321
324
|
if (session.state === "ready") {
|
|
@@ -351,6 +354,8 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
351
354
|
if (values.help) return print(command === "desktop" ? DESKTOP_HELP : HELP);
|
|
352
355
|
const wait = ["start", "attach", "stop"].includes(command) ? integer(values.wait, command === "stop" ? 60 : 300, 0, command === "stop" ? 300 : 600) : 0;
|
|
353
356
|
if (command === "start" && values["request-key"] !== undefined && !validRequestKey(values["request-key"])) throw new CliError("Use a request key of 8–128 letters, digits, underscores, periods, colons or hyphens.");
|
|
357
|
+
// The image the last read reported (macOS, Xcodes, Simulators), for start's summary.
|
|
358
|
+
let lastImage;
|
|
354
359
|
const output = result => {
|
|
355
360
|
// Sync, pull and the other commands answer with their own result, which has no session.
|
|
356
361
|
if (values.json || !["start", "attach", "stop"].includes(command)) { print(JSON.stringify(result, null, 2)); return result; }
|
|
@@ -364,7 +369,9 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
364
369
|
: ["stopped", "failed"].includes(result.session.state) ? `${result.session.drive ? "The drive saves in the background (nomac drive shows it). " : ""}Start another: nomac start`
|
|
365
370
|
: result.session.state === "stopping" ? "Stopping. Check: nomac status"
|
|
366
371
|
: "Check: nomac status";
|
|
367
|
-
|
|
372
|
+
// A ready Mac says what is on it, as nomac status does.
|
|
373
|
+
const mac = result.session.state === "ready" && macLine(lastImage) ? `${macLine(lastImage)}\n` : "";
|
|
374
|
+
print(`${result.attached ? `${result.attached}\n` : ""}${result.session.id}: ${result.session.state}\n${mac}${next}\n${usage}${result.warning ? `\n${result.warning}` : ""}`);
|
|
368
375
|
return result;
|
|
369
376
|
};
|
|
370
377
|
const saveView = (local, view) => {
|
|
@@ -373,7 +380,11 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
373
380
|
Object.assign(local, { id: view.id, state: view.state, snapshot: view }); store.save(local);
|
|
374
381
|
return view;
|
|
375
382
|
};
|
|
376
|
-
const refresh = async local =>
|
|
383
|
+
const refresh = async local => {
|
|
384
|
+
const answer = await request("GET", `/api/v1/mac/sessions/${sessionId(local.id)}`);
|
|
385
|
+
if (answer.image) lastImage = answer.image;
|
|
386
|
+
return saveView(local, answer.session);
|
|
387
|
+
};
|
|
377
388
|
async function ready(local) {
|
|
378
389
|
await refresh(local);
|
|
379
390
|
if (local.state !== "ready") throw new CliError("Mac is not ready.", `Session ${local.id} is ${local.state}. Run nomac session status --json.`, undefined, "session_not_ready");
|
package/src/mcp.mjs
CHANGED
|
@@ -70,7 +70,7 @@ export const TOOLS = [
|
|
|
70
70
|
handler: async args => text(await api("GET", `/api/v1/store-capabilities?${new URLSearchParams(Object.entries(args ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]))}`)),
|
|
71
71
|
},
|
|
72
72
|
...[
|
|
73
|
-
["read_store", "read", "Read a discovered Apple GET operation. Start with an apps_* relationship and scope:[]; reuse the returned resource scopes. Paginate with next_cursor and unchanged query. Raw IDs are accepted only for global lookups. Reads live Apple state
|
|
73
|
+
["read_store", "read", "Read a discovered Apple GET operation. Start with an apps_* relationship and scope:[]; reuse the returned resource scopes. Paginate with next_cursor and unchanged query. Raw IDs are accepted only for global lookups. Reads live Apple state and analytics report URLs; a file's upload ranges come only in the answer to its reservation, so use upload_store_asset."],
|
|
74
74
|
["set_subscription_prices", "subscription-prices", "Sets a subscription's price in every territory from one base price point, using Apple's equalized prices (what App Store Connect's all-territories pricing does): territories already at that price are skipped, the others are set one by one, about 40 per call within 35 seconds. Call again (a new request_key, or none) until remaining is 0; a repeat changes nothing. Apple's refusal for a territory is listed in failed (UNSUPPORTED_TERRITORY: set the subscription's availability there first). Without confirm it returns a preview with the counts. Prices come from the human."],
|
|
75
75
|
["update_store", "write", "Execute a discovered Apple write using its exact JSON:API body. Copy scope and relationship references from read_store. Preview by default; confirm:true changes real Apple resources. Use owner-authorized prices and attestations. On pending, retry identical arguments with request_key; get_store_operation recovers lost keys. Use publish/update_review/manage_testflight for their dedicated lifecycle actions."],
|
|
76
76
|
].map(([name, route, description]) => ({
|
|
@@ -260,7 +260,7 @@ export const TOOLS = [
|
|
|
260
260
|
{
|
|
261
261
|
name: "upload_screenshots",
|
|
262
262
|
description:
|
|
263
|
-
"Start a screenshot replacement for one display type and iOS version. PNGs are validated and prior images are backed up. Reuse request_key and the original body when retrying. Poll get_screenshot_upload until complete, rolled_back or needs_attention; accepted means still in progress. images: [{filename, data: base64 PNG}].",
|
|
263
|
+
"Start a screenshot replacement for one display type and iOS version. PNGs are validated and prior images are backed up. Reuse request_key and the original body when retrying. Poll get_screenshot_upload until complete, rolled_back or needs_attention; accepted means still in progress. images: [{filename, link}] with a create_mac_download_link url for each PNG on the Mac (any size; preferred), or [{filename, data: base64 PNG}] for small images (a request is capped near 4.5 MB in all).",
|
|
264
264
|
inputSchema: {
|
|
265
265
|
type: "object",
|
|
266
266
|
properties: {
|
package/src/store-upload.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Shared by the hosted upload endpoint and the published CLI (copied by the
|
|
2
|
-
// schema generator). Provider upload URLs come only from
|
|
3
|
-
// app-scoped
|
|
2
|
+
// schema generator). Provider upload URLs come only from the authenticated,
|
|
3
|
+
// app-scoped answer to the reservation; Nomac credentials never go to those URLs.
|
|
4
4
|
import { createHash, randomUUID } from 'node:crypto';
|
|
5
5
|
import { open } from 'node:fs/promises';
|
|
6
6
|
import { basename } from 'node:path';
|
|
@@ -82,8 +82,9 @@ export async function uploadStoreAsset(args, call) {
|
|
|
82
82
|
stage = 'upload';
|
|
83
83
|
const completed = ['UPLOAD_COMPLETE', 'COMPLETE', 'FAILED'].includes(attributes.assetDeliveryState?.state);
|
|
84
84
|
if (!completed) {
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
// Apple lists the upload ranges in its answer to the reservation only (a later read has none).
|
|
86
|
+
const operations = reservation.upload_operations?.length ? reservation.upload_operations : attributes.uploadOperations;
|
|
87
|
+
if (!Array.isArray(operations) || !operations.length) throw new Error(`Apple gives a file's upload links only in its answer to the reservation, and this request has none (that answer was lost). Delete the reservation (update_store with ${args.operation.replace(/_createInstance$/, '_deleteInstance')} and this scope), then upload again with a new request_key.`);
|
|
87
88
|
let end = 0;
|
|
88
89
|
for (const op of [...operations].sort((a, b) => a.offset - b.offset)) {
|
|
89
90
|
if (op.method !== 'PUT' || !Number.isSafeInteger(op.offset) || !Number.isSafeInteger(op.length) || op.length < 1 || op.offset !== end || op.offset + op.length > size) throw new Error('Apple returned invalid or incomplete upload byte ranges.');
|
|
@@ -105,7 +106,7 @@ export async function uploadStoreAsset(args, call) {
|
|
|
105
106
|
const part = await source(op.offset, op.length);
|
|
106
107
|
try {
|
|
107
108
|
const response = await fetch(op.url, { method: 'PUT', headers, body: part, duplex: 'half', redirect: 'error', signal: AbortSignal.timeout(120_000) });
|
|
108
|
-
if (!response.ok) throw new Error(`Apple asset upload returned HTTP ${response.status}. Retry with this same request key
|
|
109
|
+
if (!response.ok) throw new Error(`Apple asset upload returned HTTP ${response.status}. Retry with this same request key; if it fails again, its upload links have expired: delete the reservation (update_store with ${args.operation.replace(/_createInstance$/, '_deleteInstance')} and this scope) and upload again with a new request_key.`);
|
|
109
110
|
await response.body?.cancel();
|
|
110
111
|
} finally { if (typeof part.destroy === 'function') part.destroy(); }
|
|
111
112
|
}
|