@nomac/cli 0.5.6 → 0.5.7
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/cli.mjs +2 -0
- package/src/desktop.mjs +26 -0
- package/src/hosted-sessions.mjs +8 -1
- package/src/mac-tools.mjs +7 -0
- package/src/mac-transport.mjs +3 -1
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -13,6 +13,7 @@ Nomac 2.0 cloud Macs:
|
|
|
13
13
|
nomac exec --request-key KEY -- command args
|
|
14
14
|
nomac job JOB_ID inspect a submitted command
|
|
15
15
|
nomac output JOB_ID read resumable command output
|
|
16
|
+
nomac desktop the Mac's full desktop in your browser
|
|
16
17
|
nomac start --help session limits and usage
|
|
17
18
|
|
|
18
19
|
Existing Mac workspaces:
|
|
@@ -66,6 +67,7 @@ export async function main(argv) {
|
|
|
66
67
|
case "output":
|
|
67
68
|
case "sync":
|
|
68
69
|
case "ssh":
|
|
70
|
+
case "desktop":
|
|
69
71
|
case "pull":
|
|
70
72
|
case "stop":
|
|
71
73
|
case "session": {
|
package/src/desktop.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { loadApiUrl } from "./config.mjs";
|
|
2
|
+
import { openUrl } from "./mac-credits.mjs";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `nomac desktop`: the Mac's full macOS desktop in the browser. The page on
|
|
6
|
+
* nomac.app connects through the gateway to the VM's own display, so there is
|
|
7
|
+
* nothing to install and no password to type. macOS has no RDP.
|
|
8
|
+
*/
|
|
9
|
+
export const DESKTOP_HELP = `nomac desktop [--no-open] [--json]
|
|
10
|
+
Open the Mac's full macOS desktop in your browser: mouse and keyboard,
|
|
11
|
+
nothing to install. Sign in to nomac.app if asked. (macOS has no RDP.)
|
|
12
|
+
The session stays busy while the desktop is open; the page disconnects after
|
|
13
|
+
15 minutes without input. Credit is used while the Mac is ready: nomac stop when done.`;
|
|
14
|
+
|
|
15
|
+
export const desktopUrl = id => `${loadApiUrl()}/mac/desktop/${encodeURIComponent(id)}`;
|
|
16
|
+
|
|
17
|
+
export function desktop(local, { values = {}, print = console.log, open = openUrl } = {}) {
|
|
18
|
+
const url = desktopUrl(local.id);
|
|
19
|
+
const opened = values["no-open"] ? false : open(url);
|
|
20
|
+
if (values.json) return print(JSON.stringify({ session_id: local.id, url, opened_in_browser: opened }));
|
|
21
|
+
print([
|
|
22
|
+
`Mac desktop: ${url}`,
|
|
23
|
+
opened ? "Opened in your browser. Sign in to nomac.app if asked." : "Open this link in your browser and sign in to nomac.app.",
|
|
24
|
+
"Credit is used while the Mac is ready: nomac stop when done.",
|
|
25
|
+
].join("\n"));
|
|
26
|
+
}
|
package/src/hosted-sessions.mjs
CHANGED
|
@@ -7,12 +7,14 @@ import { sessionStore } from "./session-store.mjs";
|
|
|
7
7
|
import { forwardingArg, runProcess, sshArgs, workspaceCommand } from "./session-ssh.mjs";
|
|
8
8
|
import { pullProject, syncProject } from "./session-transfer.mjs";
|
|
9
9
|
import { macRequest, sessionId, transportOptions } from "./mac-transport.mjs";
|
|
10
|
+
import { desktop, DESKTOP_HELP } from "./desktop.mjs";
|
|
10
11
|
|
|
11
12
|
const HELP = `Nomac cloud Mac sessions
|
|
12
13
|
nomac start [--wait 300] [--max-duration 7200] [--idle-timeout 600] [--max-cost 5] [--json]
|
|
13
14
|
nomac sync . [--dry-run] upload changed files
|
|
14
15
|
nomac ssh [--forward 8080:3000] full Mac shell and port forwarding
|
|
15
16
|
nomac ssh -- command args run any command over SSH
|
|
17
|
+
nomac desktop [--no-open] [--json] the full macOS desktop in your browser
|
|
16
18
|
nomac exec --request-key KEY -- command args
|
|
17
19
|
nomac job JOB_ID reconcile a submitted command
|
|
18
20
|
nomac output JOB_ID [--cursor 0] [--stream stdout|stderr]
|
|
@@ -33,6 +35,7 @@ const options = {
|
|
|
33
35
|
"max-cost": { type: "string" }, "request-key": { type: "string" }, timeout: { type: "string" },
|
|
34
36
|
overwrite: { type: "boolean" }, "dry-run": { type: "boolean" }, from: { type: "string" },
|
|
35
37
|
forward: { type: "string", multiple: true }, cursor: { type: "string" }, stream: { type: "string" },
|
|
38
|
+
"no-open": { type: "boolean" },
|
|
36
39
|
};
|
|
37
40
|
function budget(value = "5") {
|
|
38
41
|
if (!/^(?:0|[1-9][0-9]{0,5})(?:\.[0-9]{1,6})?$/.test(value)) throw new CliError("--max-cost must be a positive USD amount, for example 5 or 2.50.");
|
|
@@ -69,7 +72,7 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
69
72
|
try { parsed = parseArgs({ args: argv, options, allowPositionals: true }); }
|
|
70
73
|
catch (error) { throw new CliError(error.message); }
|
|
71
74
|
const { values, positionals } = parsed;
|
|
72
|
-
if (values.help) return print(HELP);
|
|
75
|
+
if (values.help) return print(command === "desktop" ? DESKTOP_HELP : HELP);
|
|
73
76
|
const wait = ["start", "stop"].includes(command) ? integer(values.wait, command === "start" ? 300 : 60, 0, command === "start" ? 600 : 300) : 0;
|
|
74
77
|
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.");
|
|
75
78
|
const output = result => {
|
|
@@ -122,6 +125,10 @@ export async function hostedSessionCommand(command, argv, { store = sessionStore
|
|
|
122
125
|
}
|
|
123
126
|
return output(await hostedRequest(local.id, method, path, params, { request, transport }));
|
|
124
127
|
}
|
|
128
|
+
if (command === "desktop") {
|
|
129
|
+
if (positionals.length) throw new CliError("Usage: nomac desktop [--no-open] [--json]");
|
|
130
|
+
return desktop(await ready(localSession()), { values, print });
|
|
131
|
+
}
|
|
125
132
|
if (command === "ssh") {
|
|
126
133
|
if (values.json) throw new CliError("SSH streams bytes. Use session status --json for metadata.");
|
|
127
134
|
const local = await ready(localSession());
|
package/src/mac-tools.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { api } from "./api.mjs";
|
|
2
2
|
import { openUrl } from "./mac-credits.mjs";
|
|
3
|
+
import { desktopUrl } from "./desktop.mjs";
|
|
3
4
|
const text = value => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
|
|
4
5
|
const session = { type: "string", pattern: "^ses_[A-Za-z0-9_-]{1,80}$" };
|
|
5
6
|
const job = { type: "string", pattern: "^job_[a-f0-9]{40}$" };
|
|
@@ -16,6 +17,12 @@ export const MAC_TOOLS = [
|
|
|
16
17
|
{ name: "start_mac", description: "Allocate a full cloud Mac VM, billed from prepaid credit at $0.80 an hour by the minute. If it returns insufficient_credit, call get_mac_checkout, have the human pay, then retry with the same request_key. Supply a stable request_key; SSH public_key is optional for HTTPS-only agents; reuse both after a lost response. Inspect the returned offer and limits. Poll get_mac_session until ready. No project pipeline is imposed; stop_mac deletes the VM and files.", inputSchema: schema({ public_key: { type: "string" }, request_key: key, limits: { type: "object", properties: { max_duration_seconds: { type: "integer", minimum: 60, maximum: 28800 }, idle_timeout_seconds: { type: "integer", minimum: 60, maximum: 3600 }, spend_cap_microunits: { type: "string", pattern: "^[1-9][0-9]{0,17}$" } }, additionalProperties: false } }, ["request_key"]), handler: async ({ request_key, ...body }) => text(await api("POST", "/api/v1/mac/sessions", { body, idempotencyKey: request_key })) },
|
|
17
18
|
{ name: "list_mac_sessions", description: "Find existing Mac sessions, including after losing a response. Shows state, cost, limits and cleanup.", inputSchema: schema({}), handler: async () => text(await api("GET", "/api/v1/mac/sessions")) },
|
|
18
19
|
{ name: "get_mac_session", description: "Read live Mac allocation state, cost, funded time and cleanup confirmation. queued/provisioning means wait; stopping is not yet confirmed cleanup.", inputSchema: schema({ session_id: session }, ["session_id"]), handler: async args => text(await api("GET", base(args))) },
|
|
20
|
+
{ name: "open_mac_desktop", description: "Open the Mac's full macOS desktop in the human's browser on this machine, with mouse and keyboard and nothing to install. Use it when the human wants to see or click something on the Mac, for example Xcode, the Simulator or a sign-in window. The session must be ready. If opened_in_browser is false, give the human the returned url; it opens after they sign in to nomac.app. The session stays busy while the desktop is open and the page disconnects after 15 minutes without input. To show the iOS Simulator there, boot a device with simctl, then run open \"$(xcode-select -p)/Applications/Simulator.app\" with exec_mac. macOS has no RDP.", inputSchema: schema({ session_id: session, open: { type: "boolean", default: true, description: "Open the link in this machine's browser." } }, ["session_id"]), handler: async ({ session_id, open = true }) => {
|
|
21
|
+
const view = await api("GET", base({ session_id }));
|
|
22
|
+
if (view?.session?.state !== "ready") return text({ error: { code: "session_not_ready", message: "The Mac is not ready. Wait until get_mac_session shows ready, then call open_mac_desktop again." } });
|
|
23
|
+
const url = desktopUrl(session_id), opened = open ? openUrl(url) : false;
|
|
24
|
+
return text({ session_id, url, opened_in_browser: opened, message: opened ? "The desktop opened in the human's browser; they may need to sign in to nomac.app." : "Give the human this link; it opens after they sign in to nomac.app." });
|
|
25
|
+
} },
|
|
19
26
|
{ name: "stop_mac", description: "Stop billing by shutting down the VM and deleting its files. Retrieve files first. Poll get_mac_session until cleanup_confirmed; stopping only acknowledges the request.", inputSchema: schema({ session_id: session }, ["session_id"]), handler: async args => text(await api("DELETE", base(args))) },
|
|
20
27
|
{ name: "exec_mac", description: "Run any argv command in your cloud Mac workspace. Returns a durable job ID; poll get_mac_job and read_mac_output. Reuse request_key and identical arguments after lost responses; never submit a new key blindly. Use bash -lc explicitly for shell syntax. timeout_seconds defaults to 300 and caps at 600; raise it for slow work such as a first simulator boot, which needs about 90 seconds. A command (argv plus env) can be at most 64 KiB; to put larger files on the Mac, fetch them there (git clone, curl) or write them in parts of up to about 40 KiB (e.g. base64 -d >> file).", inputSchema: schema({ session_id: session, request_key: key, argv: { type: "array", items: { type: "string" }, minItems: 1 }, timeout_seconds: { type: "integer", minimum: 1, maximum: 600 } }, ["session_id", "request_key", "argv"]), handler: async ({ session_id, request_key, ...body }) => text(await api("POST", `${base({ session_id })}/jobs`, { body, idempotencyKey: request_key })) },
|
|
21
28
|
{ name: "get_mac_job", description: "Inspect the existing command without rerunning it. Retain its job ID until completion and retrieve output before stopping the VM.", inputSchema: schema({ session_id: session, job_id: job }, ["session_id", "job_id"]), handler: async args => text(await api("GET", `${base(args)}/jobs/${encodeURIComponent(args.job_id)}`)) },
|
package/src/mac-transport.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import { getProxyForUrl } from "proxy-from-env";
|
|
|
5
5
|
import WebSocket, { createWebSocketStream } from "ws";
|
|
6
6
|
import { CliError } from "./api.mjs";
|
|
7
7
|
import { clientHeaders } from "./client-info.mjs";
|
|
8
|
+
// Bytes in flight per relay hop. 64 KiB capped a ~190 ms transatlantic path near 0.3 MB/s.
|
|
9
|
+
const RELAY_BUFFER = 1024 * 1024;
|
|
8
10
|
|
|
9
11
|
export function sessionId(value) {
|
|
10
12
|
const id = String(value ?? "").replace(/^nomac-/, "");
|
|
@@ -105,7 +107,7 @@ export function tunnel(value, { input = process.stdin, output = process.stdout,
|
|
|
105
107
|
handshakeTimeout: 15_000, perMessageDeflate: false, maxPayload: 1024 * 1024,
|
|
106
108
|
followRedirects: false,
|
|
107
109
|
});
|
|
108
|
-
const stream = createWebSocketStream(ws, { highWaterMark:
|
|
110
|
+
const stream = createWebSocketStream(ws, { highWaterMark: RELAY_BUFFER });
|
|
109
111
|
let failure, closedCode, settled = false;
|
|
110
112
|
const finish = () => {
|
|
111
113
|
if (settled) return;
|