@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.
- package/CHANGELOG.md +169 -0
- package/README.md +79 -2
- package/README.tr.md +6 -3
- package/index.js +36 -3
- package/package.json +3 -3
- package/tools/code-intel/index.js +665 -0
- package/tools/code-intel/kotlin.js +159 -0
- package/tools/code-intel/lsp-client.js +422 -0
- package/tools/code-intel/pool.js +273 -0
- package/tools/code-intel/positions.js +195 -0
- package/tools/code-intel/swift.js +249 -0
- package/tools/pass-kit/index.js +432 -0
- package/tools/pass-kit/sign.js +255 -0
- package/tools/pass-kit/spec.js +317 -0
- package/tools/pass-kit/validate.js +329 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* validate.js - every check that can be made without signing anything.
|
|
3
|
+
*
|
|
4
|
+
* Pure: it takes a parsed pass.json and a list of file names, and returns
|
|
5
|
+
* findings. No I/O, so it is the same function whether it is checking a draft
|
|
6
|
+
* before a build or an existing `.pkpass` after one.
|
|
7
|
+
*
|
|
8
|
+
* Findings are graded because the consequences genuinely differ:
|
|
9
|
+
*
|
|
10
|
+
* error Wallet refuses the pass, or the pass is wrong about itself.
|
|
11
|
+
* warning Wallet accepts it and quietly does less than intended - the
|
|
12
|
+
* enhanced layout falls back, a localized string never resolves, a
|
|
13
|
+
* renamed tag is ignored. This grade is the point of the file.
|
|
14
|
+
* note Harmless, but somebody meant something by it.
|
|
15
|
+
*
|
|
16
|
+
* @module tools/pass-kit/validate
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
STYLES,
|
|
21
|
+
REQUIRED_TOP_LEVEL,
|
|
22
|
+
ASSETS,
|
|
23
|
+
ALL_IMAGE_BASENAMES,
|
|
24
|
+
STYLE_REQUIRED_KEYS,
|
|
25
|
+
TRANSIT_TYPES,
|
|
26
|
+
BARCODE_FORMATS,
|
|
27
|
+
FIELD_BUCKETS,
|
|
28
|
+
SEMANTIC_ENUMS,
|
|
29
|
+
SEMANTIC_TYPES,
|
|
30
|
+
RENAMED_SEMANTICS,
|
|
31
|
+
ENHANCED_BOARDING_PASS_TAGS,
|
|
32
|
+
checkSemanticValue,
|
|
33
|
+
} from "./spec.js";
|
|
34
|
+
|
|
35
|
+
const HEX_COLOR = /^#[0-9a-fA-F]{6}$/;
|
|
36
|
+
const RGB_COLOR = /^rgb\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}\s*\)$/;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {object} pass parsed pass.json
|
|
40
|
+
* @param {string[]} files file names present beside it, e.g. ["icon.png", "en.lproj/pass.strings"]
|
|
41
|
+
* @param {object} [opts]
|
|
42
|
+
* @returns {{style: string|null, errors: object[], warnings: object[], notes: object[], ok: boolean}}
|
|
43
|
+
*/
|
|
44
|
+
export function validatePass(pass, files = [], opts = {}) {
|
|
45
|
+
const errors = [];
|
|
46
|
+
const warnings = [];
|
|
47
|
+
const notes = [];
|
|
48
|
+
const add = (list, where, message) => list.push({ where, message });
|
|
49
|
+
|
|
50
|
+
if (!pass || typeof pass !== "object") {
|
|
51
|
+
return { style: null, errors: [{ where: "pass.json", message: "not a JSON object" }], warnings, notes, ok: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const key of REQUIRED_TOP_LEVEL) {
|
|
55
|
+
if (pass[key] === undefined || pass[key] === "") add(errors, key, "required top-level key is missing");
|
|
56
|
+
}
|
|
57
|
+
if (pass.formatVersion !== undefined && pass.formatVersion !== 1) {
|
|
58
|
+
add(errors, "formatVersion", `must be 1, got ${JSON.stringify(pass.formatVersion)}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const present = STYLES.filter((s) => pass[s] !== undefined);
|
|
62
|
+
let style = null;
|
|
63
|
+
if (present.length === 0) {
|
|
64
|
+
add(errors, "pass.json", `no style dictionary: exactly one of ${STYLES.join(", ")} is required`);
|
|
65
|
+
} else if (present.length > 1) {
|
|
66
|
+
add(errors, "pass.json", `${present.length} style dictionaries (${present.join(", ")}); a pass has exactly one`);
|
|
67
|
+
} else {
|
|
68
|
+
style = present[0];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (style) {
|
|
72
|
+
const body = pass[style] || {};
|
|
73
|
+
for (const key of STYLE_REQUIRED_KEYS[style] || []) {
|
|
74
|
+
if (body[key] === undefined) add(errors, `${style}.${key}`, "required for this style");
|
|
75
|
+
}
|
|
76
|
+
if (style === "boardingPass" && body.transitType && !TRANSIT_TYPES.includes(body.transitType)) {
|
|
77
|
+
add(errors, "boardingPass.transitType", `"${body.transitType}" is not one of ${TRANSIT_TYPES.join(", ")}`);
|
|
78
|
+
}
|
|
79
|
+
for (const bucket of Object.keys(body)) {
|
|
80
|
+
if (!FIELD_BUCKETS.includes(bucket) && bucket !== "transitType") {
|
|
81
|
+
add(notes, `${style}.${bucket}`, "not a field bucket Apple defines; it will be ignored");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const bucket of FIELD_BUCKETS) {
|
|
85
|
+
const rows = body[bucket];
|
|
86
|
+
if (rows === undefined) continue;
|
|
87
|
+
if (!Array.isArray(rows)) {
|
|
88
|
+
add(errors, `${style}.${bucket}`, "must be an array");
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
rows.forEach((f, i) => {
|
|
92
|
+
const at = `${style}.${bucket}[${i}]`;
|
|
93
|
+
if (!f || typeof f !== "object") return add(errors, at, "must be an object");
|
|
94
|
+
if (typeof f.key !== "string" || !f.key) add(errors, at, "`key` is required");
|
|
95
|
+
if (f.value === undefined) add(errors, at, "`value` is required");
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
checkColors(pass, errors);
|
|
101
|
+
checkBarcodes(pass, errors, warnings);
|
|
102
|
+
checkSemantics(pass, style, errors, warnings, notes, opts);
|
|
103
|
+
checkWebService(pass, errors, warnings);
|
|
104
|
+
checkAssets(style, files, errors, warnings, notes);
|
|
105
|
+
checkLocalization(pass, style, files, warnings);
|
|
106
|
+
checkLabels(pass, style, files, warnings);
|
|
107
|
+
|
|
108
|
+
return { style, errors, warnings, notes, ok: errors.length === 0 };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function checkColors(pass, errors) {
|
|
112
|
+
for (const key of ["foregroundColor", "backgroundColor", "labelColor", "stripColor"]) {
|
|
113
|
+
const v = pass[key];
|
|
114
|
+
if (v === undefined) continue;
|
|
115
|
+
if (typeof v !== "string" || (!HEX_COLOR.test(v) && !RGB_COLOR.test(v))) {
|
|
116
|
+
errors.push({ where: key, message: `must be #RRGGBB or rgb(r,g,b), got ${JSON.stringify(v)}` });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function checkBarcodes(pass, errors, warnings) {
|
|
122
|
+
const list = pass.barcodes;
|
|
123
|
+
if (list === undefined) {
|
|
124
|
+
if (pass.barcode === undefined) {
|
|
125
|
+
warnings.push({ where: "barcodes", message: "no barcode: the pass will scan nowhere" });
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(list)) return errors.push({ where: "barcodes", message: "must be an array" });
|
|
130
|
+
list.forEach((b, i) => {
|
|
131
|
+
const at = `barcodes[${i}]`;
|
|
132
|
+
if (!b || typeof b !== "object") return errors.push({ where: at, message: "must be an object" });
|
|
133
|
+
if (!BARCODE_FORMATS.includes(b.format)) {
|
|
134
|
+
errors.push({ where: `${at}.format`, message: `"${b.format}" is not one of ${BARCODE_FORMATS.join(", ")}` });
|
|
135
|
+
}
|
|
136
|
+
if (typeof b.message !== "string" || !b.message) {
|
|
137
|
+
errors.push({ where: `${at}.message`, message: "required" });
|
|
138
|
+
}
|
|
139
|
+
if (typeof b.messageEncoding !== "string" || !b.messageEncoding) {
|
|
140
|
+
errors.push({ where: `${at}.messageEncoding`, message: 'required, usually "iso-8859-1"' });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function checkSemantics(pass, style, errors, warnings, notes, opts) {
|
|
146
|
+
// Semantics live at the top level and may also appear per field. Both are
|
|
147
|
+
// checked; the top-level block is the one the enhanced layouts read.
|
|
148
|
+
const blocks = [{ at: "semantics", value: pass.semantics }];
|
|
149
|
+
if (style && pass[style]) {
|
|
150
|
+
for (const bucket of FIELD_BUCKETS) {
|
|
151
|
+
(pass[style][bucket] || []).forEach((f, i) => {
|
|
152
|
+
if (f && f.semantics) blocks.push({ at: `${style}.${bucket}[${i}].semantics`, value: f.semantics });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for (const { at, value } of blocks) {
|
|
158
|
+
if (value === undefined) continue;
|
|
159
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
160
|
+
errors.push({ where: at, message: "must be an object" });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
for (const [key, v] of Object.entries(value)) {
|
|
164
|
+
const renamedTo = RENAMED_SEMANTICS[key];
|
|
165
|
+
if (renamedTo) {
|
|
166
|
+
warnings.push({
|
|
167
|
+
where: `${at}.${key}`,
|
|
168
|
+
message: `renamed to "${renamedTo}" in iOS 26. The old spelling is not rejected, it is ignored - so whatever it drives silently does not appear`,
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const allowed = SEMANTIC_ENUMS[key];
|
|
173
|
+
if (allowed) {
|
|
174
|
+
const values = Array.isArray(v) ? v : [v];
|
|
175
|
+
for (const one of values) {
|
|
176
|
+
if (!allowed.includes(one)) {
|
|
177
|
+
errors.push({
|
|
178
|
+
where: `${at}.${key}`,
|
|
179
|
+
message: `"${one}" is not a value Apple defines. One unknown value here does not raise an error: Wallet declines the enhanced layout and falls back silently. Allowed: ${allowed.join(", ")}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const type = SEMANTIC_TYPES[key];
|
|
186
|
+
if (!type) {
|
|
187
|
+
notes.push({ where: `${at}.${key}`, message: "not a semantic tag this validator knows; it may be new or misspelled" });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const bad = checkSemanticValue(key, type, v);
|
|
191
|
+
if (bad) errors.push({ where: `${at}.${key}`, message: bad });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const top = pass.semantics || {};
|
|
196
|
+
// `original*` is the schedule and `current*` is the live value. Rewriting
|
|
197
|
+
// `original*` on an update destroys the only record of what changed, which is
|
|
198
|
+
// what the delay notifications are computed from.
|
|
199
|
+
for (const pair of [
|
|
200
|
+
["originalDepartureDate", "currentDepartureDate"],
|
|
201
|
+
["originalArrivalDate", "currentArrivalDate"],
|
|
202
|
+
["originalBoardingDate", "currentBoardingDate"],
|
|
203
|
+
]) {
|
|
204
|
+
if (top[pair[1]] !== undefined && top[pair[0]] === undefined) {
|
|
205
|
+
warnings.push({
|
|
206
|
+
where: `semantics.${pair[1]}`,
|
|
207
|
+
message: `set without ${pair[0]}. The original is the schedule and the current is the live value; with no original there is nothing to compare against`,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (style === "boardingPass" && opts.enhanced !== false) {
|
|
213
|
+
const missing = ENHANCED_BOARDING_PASS_TAGS.filter((t) => top[t] === undefined);
|
|
214
|
+
if (missing.length && missing.length < ENHANCED_BOARDING_PASS_TAGS.length) {
|
|
215
|
+
warnings.push({
|
|
216
|
+
where: "semantics",
|
|
217
|
+
message: `an enhanced boarding pass wants ${missing.length} more tag(s): ${missing.join(", ")}. Missing them is not an error - the pass renders the older layout instead, with nothing saying why`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function checkWebService(pass, errors, warnings) {
|
|
224
|
+
const hasUrl = typeof pass.webServiceURL === "string" && pass.webServiceURL !== "";
|
|
225
|
+
const hasToken = typeof pass.authenticationToken === "string" && pass.authenticationToken !== "";
|
|
226
|
+
if (hasUrl !== hasToken) {
|
|
227
|
+
errors.push({
|
|
228
|
+
where: hasUrl ? "authenticationToken" : "webServiceURL",
|
|
229
|
+
message: "webServiceURL and authenticationToken go together; one without the other disables updates",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
if (hasUrl && !/^https:\/\//i.test(pass.webServiceURL)) {
|
|
233
|
+
errors.push({ where: "webServiceURL", message: "must be https" });
|
|
234
|
+
}
|
|
235
|
+
if (hasToken && pass.authenticationToken.length < 16) {
|
|
236
|
+
warnings.push({
|
|
237
|
+
where: "authenticationToken",
|
|
238
|
+
message: "shorter than 16 characters; Apple requires at least 16",
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function checkAssets(style, files, errors, warnings, notes) {
|
|
244
|
+
if (!style) return;
|
|
245
|
+
const names = new Set(files.map((f) => f.split("/").pop()));
|
|
246
|
+
const has = (base) => names.has(`${base}.png`) || names.has(`${base}@2x.png`) || names.has(`${base}@3x.png`);
|
|
247
|
+
const contract = ASSETS[style] || { required: [], optional: [] };
|
|
248
|
+
|
|
249
|
+
for (const base of contract.required) {
|
|
250
|
+
if (!has(base)) {
|
|
251
|
+
// Tagged, because the pass.json-only caller has no files to check and has
|
|
252
|
+
// to drop these. It used to drop them by matching the message text, which
|
|
253
|
+
// stops working the moment the wording is improved - silently, and in the
|
|
254
|
+
// direction of reporting missing images that were never claimed present.
|
|
255
|
+
errors.push({ where: `${base}.png`, message: `required for a ${style}`, kind: "asset" });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
for (const base of ALL_IMAGE_BASENAMES) {
|
|
259
|
+
if (!has(base)) continue;
|
|
260
|
+
if (contract.required.includes(base) || contract.optional.includes(base)) continue;
|
|
261
|
+
notes.push({ where: `${base}.png`, message: `a ${style} does not use this image; it is weight in the manifest` });
|
|
262
|
+
}
|
|
263
|
+
// The 1x file carries NO suffix. `footer@1x.png` is not the 1x footer - it is
|
|
264
|
+
// a file Wallet never looks for, hashed into the manifest for nothing.
|
|
265
|
+
for (const n of names) {
|
|
266
|
+
if (/@1x\.png$/.test(n)) {
|
|
267
|
+
warnings.push({
|
|
268
|
+
where: n,
|
|
269
|
+
message: `the 1x variant has no suffix. Wallet looks for "${n.replace("@1x", "")}" and will not find this file`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
for (const base of contract.required) {
|
|
274
|
+
if (has(base) && !names.has(`${base}@2x.png`)) {
|
|
275
|
+
notes.push({ where: `${base}@2x.png`, message: "no @2x variant; it will be upscaled on every modern device" });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function checkLocalization(pass, style, files, warnings) {
|
|
281
|
+
const lprojs = new Set(
|
|
282
|
+
files.filter((f) => f.includes(".lproj/")).map((f) => f.split(".lproj/")[0].split("/").pop()),
|
|
283
|
+
);
|
|
284
|
+
if (lprojs.size === 0) return;
|
|
285
|
+
const strings = new Set(files.filter((f) => f.endsWith(".lproj/pass.strings")).map((f) => f.split(".lproj/")[0].split("/").pop()));
|
|
286
|
+
for (const l of lprojs) {
|
|
287
|
+
if (!strings.has(l)) {
|
|
288
|
+
warnings.push({ where: `${l}.lproj`, message: "no pass.strings; every label in this language falls back" });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Labels, which are the quietest way a pass ends up half-translated.
|
|
295
|
+
*
|
|
296
|
+
* Deliberately NOT inside checkLocalization: this first lived there, behind its
|
|
297
|
+
* early return, and therefore never ran on a pass with no .lproj folder at all -
|
|
298
|
+
* which is the case where a key-shaped label is worst, because it renders as the
|
|
299
|
+
* key. A test caught it.
|
|
300
|
+
*/
|
|
301
|
+
function checkLabels(pass, style, files, warnings) {
|
|
302
|
+
if (!style || !pass[style]) return;
|
|
303
|
+
const labels = [];
|
|
304
|
+
for (const bucket of FIELD_BUCKETS) {
|
|
305
|
+
(pass[style][bucket] || []).forEach((f) => {
|
|
306
|
+
if (f && typeof f.label === "string" && f.label) labels.push(f.label);
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
if (labels.length === 0) return;
|
|
310
|
+
|
|
311
|
+
const keyish = labels.filter((l) => /^[a-z][A-Za-z0-9_]*$/.test(l) || /_/.test(l));
|
|
312
|
+
const hasStrings = files.some((f) => f.endsWith(".lproj/pass.strings"));
|
|
313
|
+
|
|
314
|
+
if (keyish.length > 0 && !hasStrings) {
|
|
315
|
+
warnings.push({
|
|
316
|
+
where: `${style} labels`,
|
|
317
|
+
message: `${keyish.length} label(s) look like localization keys (${keyish.slice(0, 4).map((l) => JSON.stringify(l)).join(", ")}) but no .lproj/pass.strings was found. An unresolved key renders as the key itself`,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (labels.length >= 2 && keyish.length > 0 && keyish.length < labels.length) {
|
|
323
|
+
const literals = labels.filter((l) => !keyish.includes(l)).slice(0, 4);
|
|
324
|
+
warnings.push({
|
|
325
|
+
where: `${style} labels`,
|
|
326
|
+
message: `${keyish.length} of ${labels.length} labels look like localization keys and the rest look like literal text (e.g. ${literals.map((l) => JSON.stringify(l)).join(", ")}). Mixed labels localize the keys and leave the literals in one language`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|