@nomac/cli 0.1.3 → 0.2.1

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.1.3",
3
+ "version": "0.2.1",
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/src/cli.mjs CHANGED
@@ -50,7 +50,12 @@ export async function main(argv) {
50
50
  }
51
51
  case "whoami": {
52
52
  const me = await api("GET", "/api/v1/me");
53
- console.log(JSON.stringify(me, null, 2));
53
+ const o = me.org ?? {};
54
+ console.log(`org: ${o.name ?? "?"} (${o.id ?? "?"})`);
55
+ console.log(`plan: ${o.tier ?? "?"}`);
56
+ console.log(`projects: ${me.projects ?? 0}`);
57
+ console.log(`connections: ${me.connections ?? 0}`);
58
+ console.log(`auth: ${me.auth?.via ?? "?"}${me.auth?.key_id ? ` (${me.auth.key_id})` : ""}`);
54
59
  return;
55
60
  }
56
61
  case undefined:
@@ -79,14 +84,55 @@ async function push() {
79
84
  console.log("packing working tree (respecting .gitignore, secrets excluded)…");
80
85
  const { tarball, files } = await packTarball(process.cwd());
81
86
  console.log(`${files.length} files, ${(tarball.length / 1024).toFixed(0)} KB compressed`);
82
- const res = await api("POST", `/api/v1/projects/${state.project_id}/snapshots`, {
83
- raw: tarball,
84
- });
87
+
88
+ // preferred: direct-to-storage upload (no ~4MB API body limit), then confirm.
89
+ // Servers without the uploads route get the legacy inline body instead.
90
+ let grant = null;
91
+ try {
92
+ grant = await api("POST", `/api/v1/projects/${state.project_id}/snapshots/uploads`);
93
+ } catch {
94
+ grant = null;
95
+ }
96
+ let res;
97
+ if (grant) {
98
+ if (tarball.length > grant.max_bytes) {
99
+ throw new CliError(
100
+ "tarball too large",
101
+ `Packed source is ${(tarball.length / 1048576).toFixed(1)} MB; the limit is ${(grant.max_bytes / 1048576).toFixed(0)} MB. Check .gitignore: build artifacts are recreated on the build VM.`,
102
+ );
103
+ }
104
+ const up = await fetch(grant.upload_url, {
105
+ method: "PUT",
106
+ headers: { "content-type": "application/gzip" },
107
+ body: tarball,
108
+ });
109
+ if (!up.ok) throw new CliError(`upload failed (HTTP ${up.status})`);
110
+ res = await api("POST", `/api/v1/projects/${state.project_id}/snapshots`, {
111
+ body: { upload_key: grant.upload_key },
112
+ });
113
+ } else {
114
+ res = await api("POST", `/api/v1/projects/${state.project_id}/snapshots`, {
115
+ raw: tarball,
116
+ });
117
+ }
85
118
  console.log(`✅ snapshot ${res.snapshot_id} (commit ${res.commit.slice(0, 8)})`);
86
119
  const d = res.detected ?? {};
120
+ const framework = d.project_kind
121
+ ? ` · ${d.project_kind}${d.flutter_sdk_version ? ` ${d.flutter_sdk_version}` : ""}`
122
+ : "";
87
123
  console.log(
88
- ` detected: ${d.xcodeproj ?? "?"} · scheme ${d.scheme ?? "?"} · ${d.bundle_id ?? "no bundle id"}${d.marketing_version ? ` · v${d.marketing_version}` : ""}`,
124
+ ` detected: ${d.xcodeproj ?? "?"} · scheme ${d.scheme ?? "?"} · ${d.bundle_id ?? "no bundle id"}${d.marketing_version ? ` · v${d.marketing_version}` : ""}${framework}`,
89
125
  );
126
+ if (res.lint) {
127
+ const icon = res.lint.grade === "green" ? "🟢" : res.lint.grade === "yellow" ? "🟡" : "🔴";
128
+ const n = res.lint.findings ?? 0;
129
+ console.log(
130
+ ` review lint: ${icon} ${res.lint.grade}${n ? ` — ${n} finding${n === 1 ? "" : "s"}` : ""}`,
131
+ );
132
+ if (res.lint.grade === "red") {
133
+ console.log(" ⚠ red findings will block publish — fix them before `nomac build`/publish.");
134
+ }
135
+ }
90
136
  if (res.warning) console.log(`⚠ ${res.warning}`);
91
137
  saveProjectState({ ...state, last_snapshot: res.snapshot_id });
92
138
  }
package/src/mcp.mjs CHANGED
@@ -2,6 +2,7 @@
2
2
  // Thin wrappers over the nomac REST API; API-key auth (nomac login / env).
3
3
  // Works in every MCP client day one; the hosted transport is mcp.nomac.app.
4
4
 
5
+ import { randomUUID } from "node:crypto";
5
6
  import { createInterface } from "node:readline";
6
7
  import { api } from "./api.mjs";
7
8
  import { loadProjectState, saveProjectState } from "./config.mjs";
@@ -210,10 +211,16 @@ export const TOOLS = [
210
211
  },
211
212
  handler: async (args) => {
212
213
  const project_id = await resolveProjectId(args);
214
+ const mode = args?.confirm ? "confirm" : "stage";
215
+ // Idempotency keys identify one tool invocation, not every publish for
216
+ // this project forever. A later invocation must re-run mutable lint and
217
+ // App Store preflight checks; retries inside this invocation reuse this
218
+ // single key.
219
+ const idempotencyKey = `mcp-pub-${project_id}-${mode}-${randomUUID()}`;
213
220
  return text(
214
221
  await api("POST", "/api/v1/publish", {
215
222
  body: { project_id, confirm: args?.confirm ?? false, force: args?.force ?? false },
216
- idempotencyKey: `mcp-pub-${project_id}-${args?.confirm ? "confirm" : "stage"}`,
223
+ idempotencyKey,
217
224
  }),
218
225
  );
219
226
  },