@mmerterden/multi-agent-toolkit-mcp 3.9.0 → 3.11.0

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.
@@ -0,0 +1,249 @@
1
+ /**
2
+ * swift.js - sourcekit-lsp, and an honest account of what it can answer.
3
+ *
4
+ * CAPABILITY IS NOT CAPABILITY. `initialize` returns `referencesProvider: true`
5
+ * whether or not references will ever be non-empty. Measured on this machine:
6
+ * against a package root, `textDocument/references` answered `[]` at 694ms and
7
+ * `[3]` at 5.8s, with nothing changed in between except that the background
8
+ * index had finished. Against a real 658-file package with dependencies it was
9
+ * still `[]` at 70s, having spent that time resolving and fetching. So the
10
+ * useful question is never "does the server say it supports references" but
11
+ * "has the index for this root settled", and that is what `indexReport` answers
12
+ * and what the reference tools wait on.
13
+ *
14
+ * WORKSPACE ROOT decides everything else. sourcekit-lsp takes its compiler
15
+ * arguments from a build system, and which one it finds determines whether the
16
+ * answers are semantic or a guess. The resolution order below is by strength of
17
+ * evidence, and whichever one matched is returned in every result, because a
18
+ * caller reading "0 references" deserves to know it came from a fallback.
19
+ *
20
+ * @module tools/code-intel/swift
21
+ */
22
+
23
+ import { existsSync, readdirSync, statSync } from "node:fs";
24
+ import { dirname, join, basename } from "node:path";
25
+ import { homedir } from "node:os";
26
+ import { execFileSync } from "node:child_process";
27
+
28
+ /** Directories a root walk must not climb into or count. */
29
+ const SKIP_DIRS = new Set([".git", ".build", "build", "DerivedData", "Pods", "node_modules"]);
30
+
31
+ let cachedBin;
32
+
33
+ /**
34
+ * Where sourcekit-lsp is.
35
+ *
36
+ * PATH before `xcrun` on purpose: it is what lets a test put a fake server on
37
+ * PATH and have it used, and on a real machine `/usr/bin/sourcekit-lsp` is on
38
+ * PATH anyway. `SOURCEKIT_LSP_PATH` wins over both.
39
+ */
40
+ export function resolveSwiftBin() {
41
+ if (cachedBin !== undefined) return cachedBin;
42
+ const explicit = process.env.SOURCEKIT_LSP_PATH;
43
+ if (explicit && existsSync(explicit)) return (cachedBin = explicit);
44
+ const onPath = which("sourcekit-lsp");
45
+ if (onPath) return (cachedBin = onPath);
46
+ try {
47
+ const found = execFileSync("xcrun", ["--find", "sourcekit-lsp"], {
48
+ encoding: "utf8",
49
+ stdio: ["ignore", "pipe", "ignore"],
50
+ timeout: 10000,
51
+ }).trim();
52
+ if (found && existsSync(found)) return (cachedBin = found);
53
+ } catch {
54
+ /* no Xcode */
55
+ }
56
+ return (cachedBin = null);
57
+ }
58
+
59
+ function which(cmd) {
60
+ try {
61
+ const p = execFileSync("/usr/bin/which", [cmd], {
62
+ encoding: "utf8",
63
+ stdio: ["ignore", "pipe", "ignore"],
64
+ timeout: 5000,
65
+ }).trim();
66
+ return p && existsSync(p) ? p : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /** For tests, which change PATH between cases. */
73
+ export function resetSwiftBinCache() {
74
+ cachedBin = undefined;
75
+ }
76
+
77
+ /**
78
+ * The strongest build-settings source at or above `file`.
79
+ *
80
+ * @returns {{root: string, source: string}}
81
+ */
82
+ export function resolveWorkspaceRoot(file, explicitRoot) {
83
+ if (explicitRoot) return { root: explicitRoot, source: classify(explicitRoot) };
84
+ let dir = statSafe(file)?.isDirectory() ? file : dirname(file);
85
+ const seen = [];
86
+ for (let i = 0; i < 40 && dir && dir !== "/"; i++) {
87
+ seen.push(dir);
88
+ const s = classify(dir);
89
+ if (s !== "none") return { root: dir, source: s };
90
+ dir = dirname(dir);
91
+ }
92
+ const git = gitRoot(file);
93
+ if (git) return { root: git, source: "fallback" };
94
+ return { root: seen[0] || dirname(file), source: "fallback" };
95
+ }
96
+
97
+ function classify(dir) {
98
+ if (existsSync(join(dir, "buildServer.json"))) return "buildServer";
99
+ if (existsSync(join(dir, "Package.swift"))) return "swiftpm";
100
+ if (existsSync(join(dir, "compile_commands.json"))) return "compilationDatabase";
101
+ if (existsSync(join(dir, "compile_flags.txt"))) return "compilationDatabase";
102
+ let entries;
103
+ try {
104
+ entries = readdirSync(dir);
105
+ } catch {
106
+ return "none";
107
+ }
108
+ if (entries.some((e) => e.endsWith(".xcworkspace") || e.endsWith(".xcodeproj"))) return "xcode";
109
+ return "none";
110
+ }
111
+
112
+ function statSafe(p) {
113
+ try {
114
+ return statSync(p);
115
+ } catch {
116
+ return null;
117
+ }
118
+ }
119
+
120
+ function gitRoot(file) {
121
+ try {
122
+ return execFileSync("git", ["-C", dirname(file), "rev-parse", "--show-toplevel"], {
123
+ encoding: "utf8",
124
+ stdio: ["ignore", "pipe", "ignore"],
125
+ timeout: 5000,
126
+ }).trim();
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /**
133
+ * What an answer from this root is worth, before any question is asked.
134
+ *
135
+ * `semantic` is the headline: true when cross-file answers can be trusted,
136
+ * false when the server will still reply but from fallback arguments. Nothing
137
+ * here starts a server - it is the tool a caller runs BECAUSE something is
138
+ * missing, so it must never be the thing that fails.
139
+ */
140
+ export function indexReport(root, source) {
141
+ const spm = [
142
+ join(root, ".build", "index-build"),
143
+ join(root, ".build", "index-store"),
144
+ ].filter(existsSync);
145
+ const derived = derivedDataStore(root);
146
+ const built = spm.length > 0 || Boolean(derived);
147
+
148
+ const semantic = source === "swiftpm" || source === "buildServer" || source === "compilationDatabase";
149
+ const report = {
150
+ root,
151
+ buildSettingsSource: source,
152
+ semantic,
153
+ indexStore: derived || spm[0] || null,
154
+ indexBuilt: built,
155
+ remedy: [],
156
+ };
157
+ if (derived) {
158
+ const units = countUnits(join(derived, "v5", "units"));
159
+ if (units) {
160
+ report.unitCount = units.count;
161
+ report.newestUnit = new Date(units.newest).toISOString();
162
+ }
163
+ }
164
+ if (source === "xcode") {
165
+ report.remedy.push(
166
+ "This root is an Xcode project with no buildServer.json, so sourcekit-lsp falls back to default compiler arguments: cross-module answers and diagnostics will be wrong rather than missing. `brew install xcode-build-server && xcode-build-server config -project <X>.xcodeproj -scheme <S>` fixes it. Nothing here writes that file for you.",
167
+ );
168
+ }
169
+ if (source === "fallback") {
170
+ report.remedy.push(
171
+ "No build system was found at or above this file. Point --workspace_root at the package or project root.",
172
+ );
173
+ }
174
+ if (semantic && !built) {
175
+ report.remedy.push(
176
+ "No index on disk yet. The first cross-file question builds one in the background; on a package with dependencies that is minutes, not seconds.",
177
+ );
178
+ }
179
+ return report;
180
+ }
181
+
182
+ /**
183
+ * Xcode writes its index under DerivedData, in `<name>-<hash>`.
184
+ *
185
+ * `<name>` is the PROJECT's name, not the directory's, and the two differ often
186
+ * enough that assuming they match is the failure mode this whole file exists to
187
+ * avoid. Measured on a checkout whose directory name and `.xcodeproj` name had
188
+ * nothing in common: reported as having no index at all, while an 18,205-unit
189
+ * store sat under the project's name, and the remedy printed underneath told
190
+ * the caller to build one. So the candidate names come from the project and
191
+ * workspace files in the root, with the directory name last.
192
+ *
193
+ * Several folders can share a prefix - the same project checked out twice. The
194
+ * newest store wins, because the question being asked is about freshness.
195
+ */
196
+ function derivedDataStore(root) {
197
+ const dd = join(homedir(), "Library", "Developer", "Xcode", "DerivedData");
198
+ let entries;
199
+ try {
200
+ entries = readdirSync(dd);
201
+ } catch {
202
+ return null;
203
+ }
204
+ const names = new Set([basename(root)]);
205
+ try {
206
+ for (const e of readdirSync(root)) {
207
+ const m = /^(.+)\.(xcodeproj|xcworkspace)$/.exec(e);
208
+ if (m) names.add(m[1]);
209
+ }
210
+ } catch {
211
+ /* unreadable root: the directory name is still a candidate */
212
+ }
213
+ let best = null;
214
+ for (const c of entries) {
215
+ const dash = c.lastIndexOf("-");
216
+ if (dash <= 0 || !names.has(c.slice(0, dash))) continue;
217
+ const store = join(dd, c, "Index.noindex", "DataStore");
218
+ if (!existsSync(store)) continue;
219
+ const s = statSafe(join(store, "v5", "units")) || statSafe(store);
220
+ const at = s ? s.mtimeMs : 0;
221
+ if (!best || at > best.at) best = { store, at };
222
+ }
223
+ return best ? best.store : null;
224
+ }
225
+
226
+ /**
227
+ * How many index units there are, and when one was last written.
228
+ *
229
+ * Freshness is taken from the DIRECTORY's mtime, not from a sample of the files
230
+ * in it. A store here holds 34,380 units: stat-ing all of them costs 1.8s, and
231
+ * stat-ing the first 200 of a readdir costs nothing but answers about an
232
+ * arbitrary 200 - directory order is a hash, not a timeline, so the number it
233
+ * produces is the newest of whichever units happened to come first. It was
234
+ * correct on the measured store by luck and nothing made that visible. A unit
235
+ * is written as a new file, which bumps the directory, so one syscall answers
236
+ * exactly: measured at 2ms from the true maximum over all 34,380.
237
+ */
238
+ function countUnits(dir) {
239
+ let entries;
240
+ try {
241
+ entries = readdirSync(dir);
242
+ } catch {
243
+ return null;
244
+ }
245
+ const s = statSafe(dir);
246
+ return { count: entries.length, newest: s ? s.mtimeMs : Date.now() };
247
+ }
248
+
249
+ export { SKIP_DIRS };
@@ -0,0 +1,432 @@
1
+ /**
2
+ * pass-kit - build, sign, validate and inspect Apple Wallet passes.
3
+ *
4
+ * GENERIC BY CONSTRUCTION. No airline, no brand, no project appears anywhere in
5
+ * this directory. All five of Apple's styles are first-class, the asset
6
+ * contract is a table per style, and every identifier - pass type, team,
7
+ * organization, colours, copy - is caller input. The only thing that is not
8
+ * input is the specification itself.
9
+ *
10
+ * NO SECRETS. A literal passphrase is not an accepted parameter. The caller
11
+ * names an environment variable or a keychain entry, and the value reaches
12
+ * openssl through `-passin env:` so it never appears in `ps`, never lands in a
13
+ * log, and is scrubbed out of any error text on the way back.
14
+ *
15
+ * OFFLINE. Nothing here contacts Apple or anything else.
16
+ *
17
+ * @module tools/pass-kit
18
+ */
19
+
20
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
21
+ import { basename, dirname, isAbsolute, join } from "node:path";
22
+ import { tmpdir } from "node:os";
23
+ import { validatePass } from "./validate.js";
24
+ import { STYLES, ASSETS, SCALES } from "./spec.js";
25
+ import {
26
+ ERROR_PREFIX,
27
+ listPassFiles,
28
+ buildManifest,
29
+ writeManifest,
30
+ resolvePassphrase,
31
+ inspectCertificate,
32
+ signManifest,
33
+ verifySignature,
34
+ archive,
35
+ unpack,
36
+ } from "./sign.js";
37
+
38
+ const CERT_ARGS = {
39
+ certificate: { type: "string", description: "Pass Type ID certificate, PEM" },
40
+ private_key: { type: "string", description: "Private key for that certificate, PEM" },
41
+ wwdr_certificate: { type: "string", description: "Apple WWDR intermediate certificate, PEM" },
42
+ passphrase_env: {
43
+ type: "string",
44
+ description:
45
+ "NAME of an environment variable holding the private-key passphrase. Not the passphrase itself - a literal is deliberately not accepted",
46
+ },
47
+ keychain_account: {
48
+ type: "string",
49
+ description: "Keychain account to read the passphrase from, instead of passphrase_env",
50
+ },
51
+ keychain_service: { type: "string", description: 'Keychain service (default "pkpass")' },
52
+ };
53
+
54
+ export const PASS_TOOLS = [
55
+ {
56
+ name: "pass_validate",
57
+ description:
58
+ "Check a pass against Apple's rules without building or signing anything: required keys, the one style dictionary, field shapes, colours, barcodes, the per-style image contract, localization consistency, and semantic tags. Grades findings by consequence - an error is a pass Wallet refuses, a warning is a pass Wallet accepts while quietly doing less than intended. One unknown PKPassengerCapability value is the canonical example: no error anywhere, and the enhanced layout silently falls back.",
59
+ inputSchema: {
60
+ type: "object",
61
+ properties: {
62
+ pass_json: { type: "string", description: "Path to a pass.json" },
63
+ pass_dir: { type: "string", description: "Path to a .pass directory - checks its images and localizations too" },
64
+ pkpass: { type: "string", description: "Path to a built .pkpass - unpacked and checked, signature included" },
65
+ },
66
+ },
67
+ },
68
+ {
69
+ name: "pass_build",
70
+ description:
71
+ "Assemble, sign and archive a .pkpass. Takes a pass.json and an asset directory, writes the manifest (SHA-1 per file, as Apple specifies), signs it with openssl into a detached PKCS#7 signature, and zips the result. Validates first and refuses on an error-grade finding unless told otherwise. Offline, deterministic, and never accepts a literal passphrase.",
72
+ inputSchema: {
73
+ type: "object",
74
+ properties: {
75
+ pass_json: { type: "string", description: "Path to the pass.json to build from" },
76
+ assets_dir: { type: "string", description: "Directory holding the images and any xx.lproj folders" },
77
+ output: { type: "string", description: "Path to write the .pkpass to" },
78
+ ...CERT_ARGS,
79
+ overrides: {
80
+ type: "object",
81
+ description:
82
+ "Top-level keys to merge into pass.json before building - serialNumber, relevantDate, barcodes and so on. The file on disk is not modified",
83
+ },
84
+ allow_errors: {
85
+ type: "boolean",
86
+ description: "Build even when validation reports errors (default false)",
87
+ },
88
+ },
89
+ required: ["pass_json", "assets_dir", "output"],
90
+ },
91
+ },
92
+ {
93
+ name: "pass_inspect",
94
+ description:
95
+ "Open an existing .pkpass and report what is actually in it: style, top-level keys, semantic tags, images and their scales, localizations, whether the manifest matches the files, and the certificate chain that signed it. Read-only, and it never needs the private key.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ pkpass: { type: "string", description: "Path to the .pkpass to open" },
100
+ show_pass_json: { type: "boolean", description: "Include the full pass.json in the result" },
101
+ },
102
+ required: ["pkpass"],
103
+ },
104
+ },
105
+ {
106
+ name: "pass_certificates",
107
+ description:
108
+ "Report the signing material: which certificate this is, its pass type and team identifier, when it expires and whether it already has. An expired Pass Type ID certificate still builds, still signs and still verifies - Wallet is the first thing that refuses it, and it does not say why. Never reads or reports key material.",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ certificate: { type: "string", description: "Pass Type ID certificate, PEM" },
113
+ wwdr_certificate: { type: "string", description: "Apple WWDR intermediate certificate, PEM" },
114
+ passphrase_env: { type: "string", description: "Name of an env var to check is set (its value is never read out)" },
115
+ keychain_account: { type: "string", description: "Keychain account to check exists" },
116
+ keychain_service: { type: "string", description: 'Keychain service (default "pkpass")' },
117
+ },
118
+ required: ["certificate"],
119
+ },
120
+ },
121
+ ];
122
+
123
+ export const PASS_READ_ONLY = ["pass_validate", "pass_inspect", "pass_certificates"];
124
+
125
+ export const PASS_OUTPUT_SCHEMAS = {
126
+ pass_validate: {
127
+ type: "object",
128
+ properties: {
129
+ style: { type: "string" },
130
+ ok: { type: "boolean" },
131
+ errors: { type: "array", items: { type: "object" } },
132
+ warnings: { type: "array", items: { type: "object" } },
133
+ notes: { type: "array", items: { type: "object" } },
134
+ checked: { type: "object" },
135
+ },
136
+ required: ["ok", "errors", "warnings", "notes"],
137
+ },
138
+ pass_certificates: {
139
+ type: "object",
140
+ properties: {
141
+ certificate: { type: "object" },
142
+ wwdr: { type: "object" },
143
+ passphrase: { type: "object" },
144
+ usable: { type: "boolean" },
145
+ },
146
+ required: ["certificate", "usable"],
147
+ },
148
+ };
149
+
150
+ function requireAbs(p, label) {
151
+ if (!p) return `${label} is required`;
152
+ if (!isAbsolute(p)) return `${label} must be an absolute path, got "${p}"`;
153
+ if (!existsSync(p)) return `no such ${label}: ${p}`;
154
+ return null;
155
+ }
156
+
157
+ function readJson(p) {
158
+ try {
159
+ return { value: JSON.parse(readFileSync(p, "utf8")) };
160
+ } catch (e) {
161
+ return { error: `${ERROR_PREFIX}${p} is not readable JSON: ${e.message}` };
162
+ }
163
+ }
164
+
165
+ function validateTool(args) {
166
+ if (args.pkpass) {
167
+ const bad = requireAbs(args.pkpass, "pkpass");
168
+ if (bad) return `${ERROR_PREFIX}${bad}`;
169
+ const un = unpack(args.pkpass);
170
+ if (un.error) return un.error;
171
+ try {
172
+ const passPath = join(un.dir, "pass.json");
173
+ if (!existsSync(passPath)) return `${ERROR_PREFIX}${args.pkpass} contains no pass.json`;
174
+ const doc = readJson(passPath);
175
+ if (doc.error) return doc.error;
176
+ const files = listPassFiles(un.dir);
177
+ const report = validatePass(doc.value, files);
178
+ const sig = existsSync(join(un.dir, "signature"))
179
+ ? verifySignature({
180
+ signaturePath: join(un.dir, "signature"),
181
+ manifestPath: join(un.dir, "manifest.json"),
182
+ })
183
+ : { valid: false, reason: "no signature file" };
184
+ const manifestCheck = checkManifest(un.dir, files);
185
+ return {
186
+ ...report,
187
+ checked: { source: args.pkpass, files: files.length, signature: sig, manifest: manifestCheck },
188
+ };
189
+ } finally {
190
+ rmSync(un.dir, { recursive: true, force: true });
191
+ }
192
+ }
193
+
194
+ const dir = args.pass_dir;
195
+ if (dir) {
196
+ const bad = requireAbs(dir, "pass_dir");
197
+ if (bad) return `${ERROR_PREFIX}${bad}`;
198
+ const passPath = join(dir, "pass.json");
199
+ if (!existsSync(passPath)) return `${ERROR_PREFIX}${dir} contains no pass.json`;
200
+ const doc = readJson(passPath);
201
+ if (doc.error) return doc.error;
202
+ const files = listPassFiles(dir);
203
+ return { ...validatePass(doc.value, files), checked: { source: dir, files: files.length } };
204
+ }
205
+
206
+ const p = args.pass_json;
207
+ const bad = requireAbs(p, "pass_json");
208
+ if (bad) return `${ERROR_PREFIX}${bad}`;
209
+ const doc = readJson(p);
210
+ if (doc.error) return doc.error;
211
+ const report = validatePass(doc.value, []);
212
+ // Asset findings against an empty file list would be noise, not findings.
213
+ return {
214
+ ...report,
215
+ errors: report.errors.filter((e) => e.kind !== "asset"),
216
+ checked: { source: p, files: 0, note: "pass.json only - give pass_dir or pkpass to check images too" },
217
+ };
218
+ }
219
+
220
+ function checkManifest(dir, files) {
221
+ const manifestPath = join(dir, "manifest.json");
222
+ if (!existsSync(manifestPath)) return { present: false };
223
+ const doc = readJson(manifestPath);
224
+ if (doc.error) return { present: true, valid: false, reason: "unreadable" };
225
+ const expected = buildManifest(dir, files);
226
+ const missing = Object.keys(expected).filter((f) => !(f in doc.value));
227
+ const extra = Object.keys(doc.value).filter((f) => !(f in expected));
228
+ const mismatched = Object.keys(expected).filter((f) => f in doc.value && doc.value[f] !== expected[f]);
229
+ return {
230
+ present: true,
231
+ valid: missing.length === 0 && extra.length === 0 && mismatched.length === 0,
232
+ entries: Object.keys(doc.value).length,
233
+ ...(missing.length ? { missing } : {}),
234
+ ...(extra.length ? { extra } : {}),
235
+ ...(mismatched.length ? { mismatched } : {}),
236
+ };
237
+ }
238
+
239
+ function buildTool(args) {
240
+ for (const [p, label] of [
241
+ [args.pass_json, "pass_json"],
242
+ [args.assets_dir, "assets_dir"],
243
+ [args.certificate, "certificate"],
244
+ [args.private_key, "private_key"],
245
+ [args.wwdr_certificate, "wwdr_certificate"],
246
+ ]) {
247
+ const bad = requireAbs(p, label);
248
+ if (bad) return `${ERROR_PREFIX}${bad}`;
249
+ }
250
+ if (!args.output || !isAbsolute(args.output)) {
251
+ return `${ERROR_PREFIX}output must be an absolute path`;
252
+ }
253
+
254
+ const pass = readJson(args.pass_json);
255
+ if (pass.error) return pass.error;
256
+ const doc = { ...pass.value, ...(args.overrides || {}) };
257
+
258
+ const cert = inspectCertificate(args.certificate);
259
+ if (cert.expired) {
260
+ return `${ERROR_PREFIX}the Pass Type ID certificate expired on ${cert.notAfter}. A pass signed with it builds, signs and verifies, and Wallet then refuses it without saying why. Renew it before building.`;
261
+ }
262
+
263
+ const secret = resolvePassphrase(args);
264
+ if (secret.error) return secret.error;
265
+
266
+ const staging = mkdtempSync(join(tmpdir(), "pass-build-"));
267
+ try {
268
+ cpSync(args.assets_dir, staging, { recursive: true });
269
+ rmSync(join(staging, "pass.json"), { force: true });
270
+ rmSync(join(staging, "manifest.json"), { force: true });
271
+ rmSync(join(staging, "signature"), { force: true });
272
+ writeFileSync(join(staging, "pass.json"), JSON.stringify(doc, null, 2));
273
+
274
+ const files = listPassFiles(staging);
275
+ const report = validatePass(doc, files);
276
+ if (!report.ok && !args.allow_errors) {
277
+ return {
278
+ built: false,
279
+ reason: "validation failed - pass allow_errors: true to build anyway",
280
+ ...report,
281
+ };
282
+ }
283
+
284
+ const manifest = buildManifest(staging, files);
285
+ const manifestPath = writeManifest(staging, manifest);
286
+ const signed = signManifest({
287
+ manifestPath,
288
+ signaturePath: join(staging, "signature"),
289
+ certificate: args.certificate,
290
+ privateKey: args.private_key,
291
+ wwdr: args.wwdr_certificate,
292
+ passphrase: secret.value,
293
+ });
294
+ if (signed.error) return signed.error;
295
+
296
+ mkdirSync(dirname(args.output), { recursive: true });
297
+ rmSync(args.output, { force: true });
298
+ const zipped = archive(staging, args.output);
299
+ if (zipped.error) return zipped.error;
300
+
301
+ return {
302
+ built: true,
303
+ output: args.output,
304
+ bytes: zipped.bytes,
305
+ style: report.style,
306
+ files: files.length + 2,
307
+ manifestEntries: Object.keys(manifest).length,
308
+ passphraseSource: secret.source,
309
+ certificate: { passTypeIdentifier: cert.passTypeIdentifier, teamIdentifier: cert.teamIdentifier, notAfter: cert.notAfter, daysLeft: cert.daysLeft },
310
+ warnings: report.warnings,
311
+ notes: report.notes,
312
+ ...(report.ok ? {} : { errors: report.errors, builtWithErrors: true }),
313
+ };
314
+ } catch (e) {
315
+ return `${ERROR_PREFIX}build failed: ${e.message}`;
316
+ } finally {
317
+ rmSync(staging, { recursive: true, force: true });
318
+ }
319
+ }
320
+
321
+ function inspectTool(args) {
322
+ const bad = requireAbs(args.pkpass, "pkpass");
323
+ if (bad) return `${ERROR_PREFIX}${bad}`;
324
+ const un = unpack(args.pkpass);
325
+ if (un.error) return un.error;
326
+ try {
327
+ const passPath = join(un.dir, "pass.json");
328
+ if (!existsSync(passPath)) return `${ERROR_PREFIX}${args.pkpass} contains no pass.json`;
329
+ const doc = readJson(passPath);
330
+ if (doc.error) return doc.error;
331
+ const pass = doc.value;
332
+ const files = listPassFiles(un.dir);
333
+ const style = STYLES.find((s) => pass[s] !== undefined) || null;
334
+
335
+ const images = {};
336
+ for (const f of files) {
337
+ const m = /^([a-zA-Z]+)(@2x|@3x)?\.png$/.exec(f.split("/").pop() || "");
338
+ if (!m) continue;
339
+ images[m[1]] = images[m[1]] || [];
340
+ images[m[1]].push(m[2] || "1x");
341
+ }
342
+ const localizations = [
343
+ ...new Set(files.filter((f) => f.includes(".lproj/")).map((f) => f.split(".lproj/")[0].split("/").pop())),
344
+ ];
345
+
346
+ const sig = existsSync(join(un.dir, "signature"))
347
+ ? verifySignature({ signaturePath: join(un.dir, "signature"), manifestPath: join(un.dir, "manifest.json") })
348
+ : { valid: false, reason: "no signature file" };
349
+
350
+ return {
351
+ pkpass: args.pkpass,
352
+ bytes: statSync(args.pkpass).size,
353
+ style,
354
+ passTypeIdentifier: pass.passTypeIdentifier,
355
+ teamIdentifier: pass.teamIdentifier,
356
+ organizationName: pass.organizationName,
357
+ serialNumber: pass.serialNumber,
358
+ formatVersion: pass.formatVersion,
359
+ topLevelKeys: Object.keys(pass).sort(),
360
+ semanticTags: Object.keys(pass.semantics || {}).sort(),
361
+ barcodes: (pass.barcodes || []).map((b) => b.format),
362
+ updates: pass.webServiceURL ? { webServiceURL: pass.webServiceURL, hasAuthenticationToken: Boolean(pass.authenticationToken) } : null,
363
+ images,
364
+ localizations,
365
+ files: files.length,
366
+ manifest: checkManifest(un.dir, files),
367
+ signature: sig,
368
+ ...(args.show_pass_json ? { passJson: pass } : {}),
369
+ };
370
+ } finally {
371
+ rmSync(un.dir, { recursive: true, force: true });
372
+ }
373
+ }
374
+
375
+ function certificatesTool(args) {
376
+ const bad = requireAbs(args.certificate, "certificate");
377
+ if (bad) return `${ERROR_PREFIX}${bad}`;
378
+ const cert = inspectCertificate(args.certificate);
379
+ const wwdr = args.wwdr_certificate ? inspectCertificate(args.wwdr_certificate) : null;
380
+
381
+ let passphrase = { configured: false, note: "give passphrase_env or keychain_account to check one" };
382
+ if (args.passphrase_env || args.keychain_account) {
383
+ const r = resolvePassphrase(args);
384
+ // The VALUE is never reported, only whether one was found.
385
+ passphrase = r.error
386
+ ? { configured: false, reason: r.error.replace(ERROR_PREFIX, "") }
387
+ : { configured: true, source: r.source };
388
+ }
389
+
390
+ const usable = Boolean(cert && !cert.error && cert.expired === false);
391
+ const advice = [];
392
+ if (cert.expired) {
393
+ advice.push(
394
+ `The certificate expired on ${cert.notAfter}. A pass signed with it builds, signs and verifies; Wallet refuses it and says nothing. Renew it at developer.apple.com.`,
395
+ );
396
+ } else if (cert.daysLeft !== null && cert.daysLeft < 30) {
397
+ advice.push(`The certificate expires in ${cert.daysLeft} day(s).`);
398
+ }
399
+ if (!wwdr) advice.push("Give wwdr_certificate to check the intermediate too; signing needs it.");
400
+ if (!passphrase.configured) {
401
+ advice.push(
402
+ "No passphrase source configured. Name an environment variable (passphrase_env) or store one in the keychain; a literal passphrase is deliberately not an accepted input.",
403
+ );
404
+ }
405
+
406
+ return { certificate: cert, wwdr, passphrase, usable, advice };
407
+ }
408
+
409
+ /**
410
+ * @param {string} name
411
+ * @param {object} args
412
+ */
413
+ export async function handlePass(name, args = {}) {
414
+ try {
415
+ switch (name) {
416
+ case "pass_validate":
417
+ return validateTool(args);
418
+ case "pass_build":
419
+ return buildTool(args);
420
+ case "pass_inspect":
421
+ return inspectTool(args);
422
+ case "pass_certificates":
423
+ return certificatesTool(args);
424
+ default:
425
+ return null;
426
+ }
427
+ } catch (e) {
428
+ return `${ERROR_PREFIX}${name}: ${e?.message || String(e)}`;
429
+ }
430
+ }
431
+
432
+ export { STYLES, ASSETS, SCALES };