@mmerterden/dev-toolkit-mcp 2.24.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/CHANGELOG.md +708 -0
- package/LICENSE +21 -0
- package/README.md +387 -0
- package/index.js +1277 -0
- package/package.json +80 -0
- package/tools/design-check/content-cardinality.js +204 -0
- package/tools/design-check/geometry.js +140 -0
- package/tools/design-check/index.js +213 -0
- package/tools/design-check/mock-detect.js +213 -0
- package/tools/design-check/report.js +590 -0
- package/tools/design-check/scan.js +91 -0
- package/tools/design-check/scenario-inventory.js +598 -0
- package/tools/design-check/visual-compare.js +961 -0
- package/tools/ios-app-store-audit/context.js +180 -0
- package/tools/ios-app-store-audit/data/apple-required-sdks.json +32 -0
- package/tools/ios-app-store-audit/data/debug-tools-blocklist.json +133 -0
- package/tools/ios-app-store-audit/index.js +164 -0
- package/tools/ios-app-store-audit/models.js +47 -0
- package/tools/ios-app-store-audit/rules/asset-validation.js +72 -0
- package/tools/ios-app-store-audit/rules/binary-size.js +70 -0
- package/tools/ios-app-store-audit/rules/code-signing.js +95 -0
- package/tools/ios-app-store-audit/rules/dead-reference.js +131 -0
- package/tools/ios-app-store-audit/rules/debug-tool-leak.js +185 -0
- package/tools/ios-app-store-audit/rules/duplicate-resource.js +130 -0
- package/tools/ios-app-store-audit/rules/embedded-sdk.js +126 -0
- package/tools/ios-app-store-audit/rules/entitlement.js +105 -0
- package/tools/ios-app-store-audit/rules/extension-signing.js +105 -0
- package/tools/ios-app-store-audit/rules/info-plist.js +158 -0
- package/tools/ios-app-store-audit/rules/ipv6-compliance.js +101 -0
- package/tools/ios-app-store-audit/rules/privacy-manifest.js +121 -0
- package/tools/ios-app-store-audit/rules/production-hygiene.js +237 -0
- package/tools/ios-app-store-audit/rules/provisioning-profile.js +127 -0
- package/tools/ios-app-store-audit/rules/required-reason-api.js +123 -0
- package/tools/ios-app-store-audit/rules/sdk-floor.js +104 -0
- package/tools/ios-app-store-audit/rules/swift-abi.js +64 -0
- package/tools/ios-app-store-audit/rules/team-id.js +62 -0
- package/tools/ios-testflight/index.js +479 -0
- package/ui-tree-dumper.swift +122 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design_mock_detect - decide whether a mobile project can be launched in a
|
|
3
|
+
* mock/stub mode so its screens can be captured deterministically.
|
|
4
|
+
*
|
|
5
|
+
* Fully generic: no project, owner, or repo name is hardcoded. Detection is
|
|
6
|
+
* signal-based (naming conventions + DI patterns + well-known switch NAMES),
|
|
7
|
+
* and the caller may inject project-specific switch keys via opts.extraKeys.
|
|
8
|
+
* Scanning uses ripgrep when available (fast, .gitignore-aware) and falls back
|
|
9
|
+
* to a bounded node walk otherwise.
|
|
10
|
+
*
|
|
11
|
+
* returns:
|
|
12
|
+
* {
|
|
13
|
+
* supported: true | false | "debug-only",
|
|
14
|
+
* platform: "ios" | "android" | "unknown",
|
|
15
|
+
* mechanism: "userdefaults-launcharg" | "interceptor" | "debug-di" | null,
|
|
16
|
+
* activation: { kind, key?, value?, launchArg?/intentExtra?, buildConfig },
|
|
17
|
+
* variantsHint: [ ... ],
|
|
18
|
+
* evidence: [ { signal, file, line, snippet } ],
|
|
19
|
+
* reason: <human string when unsupported>
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { readFileSync, existsSync } from "fs";
|
|
24
|
+
import { basename } from "path";
|
|
25
|
+
|
|
26
|
+
import { hasRg, rg, walk, rel, detectPlatform, SRC_EXT } from "./scan.js";
|
|
27
|
+
|
|
28
|
+
const KNOWN_MOCK_KEYS = [
|
|
29
|
+
"debugMockMode", "mockServiceIsActive", "mock-service-is-active",
|
|
30
|
+
"traceweave.mock.enabled", "isMockEnabled", "isMockMode", "useMockData",
|
|
31
|
+
"useMockServices", "MOCK_MODE", "UITEST_MOCK", "mockEnabled",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
function hasLaunchableAndroidApp(repoPath) {
|
|
35
|
+
if (hasRg()) {
|
|
36
|
+
try {
|
|
37
|
+
const app = rg("com\\.android\\.application|applicationId\\s+[\"']", repoPath,
|
|
38
|
+
{ globs: ["-g", "*.gradle", "-g", "*.gradle.kts"] });
|
|
39
|
+
if (app.length) return true;
|
|
40
|
+
const launcher = rg("android\\.intent\\.category\\.LAUNCHER", repoPath, { globs: ["-g", "AndroidManifest.xml"] });
|
|
41
|
+
return launcher.length > 0;
|
|
42
|
+
} catch { /* fall through */ }
|
|
43
|
+
}
|
|
44
|
+
let launchable = false;
|
|
45
|
+
walk(repoPath, (f) => {
|
|
46
|
+
const b = basename(f);
|
|
47
|
+
if (b === "build.gradle" || b === "build.gradle.kts" || b === "AndroidManifest.xml") {
|
|
48
|
+
let t = ""; try { t = readFileSync(f, "utf-8"); } catch { return; }
|
|
49
|
+
if (/com\.android\.application|applicationId\s+["']|android\.intent\.category\.LAUNCHER/.test(t)) launchable = true;
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
return launchable;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Core signal scan. Populates `state` with runtimeKey/interceptor/debugDI/etc.
|
|
56
|
+
function scanWithRg(repoPath, keys, state) {
|
|
57
|
+
const keyAlt = keys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
58
|
+
// 1. known/injected runtime switch keys
|
|
59
|
+
for (const hit of rg(`["'](?:${keyAlt})["']`, repoPath)) {
|
|
60
|
+
const km = /["']([^"']+)["']/.exec(hit.text);
|
|
61
|
+
const k = km ? km[1] : "?";
|
|
62
|
+
if (/traceweave\.mock|URLProtocol|interceptor/i.test(hit.text) || k.includes("traceweave")) state.interceptor = true;
|
|
63
|
+
state.runtimeKey = state.runtimeKey || k;
|
|
64
|
+
state.push(`runtime-switch:${k}`, hit, repoPath);
|
|
65
|
+
}
|
|
66
|
+
// 2. generic AppStorage/UserDefaults "mock" bool key
|
|
67
|
+
if (!state.runtimeKey) {
|
|
68
|
+
for (const hit of rg(`(?:@AppStorage|forKey|defaults\\.\\w+)\\s*\\(?\\s*["'][A-Za-z0-9_.\\-]*[Mm]ock[A-Za-z0-9_.\\-]*["']`, repoPath)) {
|
|
69
|
+
const km = /["']([A-Za-z0-9_.\-]*[Mm]ock[A-Za-z0-9_.\-]*)["']/.exec(hit.text);
|
|
70
|
+
if (km) { state.runtimeKey = km[1]; state.push(`runtime-switch:${km[1]}`, hit, repoPath); break; }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 2b. launch-argument mock (ProcessInfo.processInfo.arguments.contains) + arg constants
|
|
74
|
+
if (!state.runtimeKey) {
|
|
75
|
+
const la = rg("ProcessInfo\\.processInfo\\.arguments\\.contains", repoPath);
|
|
76
|
+
if (la.length) {
|
|
77
|
+
const argHits = rg("static let \\w+\\s*=\\s*\"(-[A-Za-z]*[Mm]ock[A-Za-z0-9]*)\"", repoPath);
|
|
78
|
+
const am = argHits.length ? /"(-[A-Za-z0-9]+)"/.exec(argHits[0].text) : null;
|
|
79
|
+
state.runtimeKey = am ? am[1] : "-Mock";
|
|
80
|
+
state.launchArg = true;
|
|
81
|
+
state.push("runtime-launcharg", la[0], repoPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// 2c. constant-defined UserDefaults mock key (string literal with 'mock', not inline @AppStorage)
|
|
85
|
+
if (!state.runtimeKey) {
|
|
86
|
+
const ck = rg("static let \\w+\\s*=\\s*\"[A-Za-z0-9_.\\-]*[Mm]ock[A-Za-z0-9_.\\-]*\"", repoPath);
|
|
87
|
+
if (ck.length) {
|
|
88
|
+
const cm = /"([A-Za-z0-9_.\-]*[Mm]ock[A-Za-z0-9_.\-]*)"/.exec(ck[0].text);
|
|
89
|
+
if (cm) { state.runtimeKey = cm[1]; state.push(`runtime-switch:${cm[1]}`, ck[0], repoPath); }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// 3. mock type declarations
|
|
93
|
+
const typeHits = rg(`\\b(?:class|struct|final class|object|enum)\\s+(?:Mock[A-Za-z0-9]+|[A-Za-z0-9]+Mock(?:Service|NetworkService|Repository|Provider|DataSource))\\b`, repoPath);
|
|
94
|
+
state.mockTypeCount = typeHits.length;
|
|
95
|
+
typeHits.slice(0, 8).forEach((h) => {
|
|
96
|
+
const tm = /\s(Mock[A-Za-z0-9]+|[A-Za-z0-9]+Mock(?:Service|NetworkService|Repository|Provider|DataSource))\b/.exec(h.text);
|
|
97
|
+
state.push(`mock-type:${tm ? tm[1] : "Mock"}`, h, repoPath);
|
|
98
|
+
});
|
|
99
|
+
// 4. #if DEBUG return Mock (compile-time DI)
|
|
100
|
+
const diHits = rg(`#if\\s+DEBUG.{0,400}?return\\s+Mock[A-Za-z0-9]+`, repoPath, { multiline: true });
|
|
101
|
+
if (diHits.length) { state.debugDI = true; state.push("debug-di", diHits[0], repoPath); }
|
|
102
|
+
// 5. MockData fixtures → variant hints
|
|
103
|
+
for (const f of rg("**/MockData/**/*.json", repoPath, { files: true, globs: [] })) {
|
|
104
|
+
const b = basename(f.file);
|
|
105
|
+
const vm = /([A-Za-z0-9]+)_mock\.json$|mock_([A-Za-z0-9]+)\.json$/i.exec(b);
|
|
106
|
+
state.variants.add((vm ? (vm[1] || vm[2]) : b.replace(/\.json$/i, "")).toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function scanWithWalk(repoPath, keys, state) {
|
|
111
|
+
const keyRegexes = keys.map((k) => ({ k, re: new RegExp(`["']${k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["']`) }));
|
|
112
|
+
const genericRe = /(?:@AppStorage|forKey|defaults\.\w+)\s*\(?\s*["']([A-Za-z0-9_.\-]*[Mm]ock[A-Za-z0-9_.\-]*)["']/;
|
|
113
|
+
const typeRe = /\b(?:class|struct|final class|object|enum)\s+(Mock[A-Za-z0-9]+|[A-Za-z0-9]+Mock(?:Service|NetworkService|Repository|Provider|DataSource))\b/;
|
|
114
|
+
const diRe = /#if\s+DEBUG[\s\S]{0,400}?\breturn\s+Mock[A-Za-z0-9]+/;
|
|
115
|
+
const varRe = /([A-Za-z0-9]+)_mock\.json$|mock_([A-Za-z0-9]+)\.json$/i;
|
|
116
|
+
const laUseRe = /ProcessInfo\.processInfo\.arguments\.contains/;
|
|
117
|
+
const laConstRe = /static let \w+\s*=\s*"(-[A-Za-z]*[Mm]ock[A-Za-z0-9]*)"/;
|
|
118
|
+
const constKeyRe = /static let \w+\s*=\s*"([A-Za-z0-9_.\-]*[Mm]ock[A-Za-z0-9_.\-]*)"/;
|
|
119
|
+
walk(repoPath, (file) => {
|
|
120
|
+
const b = basename(file);
|
|
121
|
+
const ext = "." + (b.split(".").pop() || "");
|
|
122
|
+
if (ext === ".json" && /\/MockData\//.test(file)) {
|
|
123
|
+
const m = varRe.exec(b);
|
|
124
|
+
state.variants.add((m ? (m[1] || m[2]) : b.replace(/\.json$/i, "")).toLowerCase());
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!SRC_EXT.has(ext)) return;
|
|
128
|
+
let txt = ""; try { txt = readFileSync(file, "utf-8"); } catch { return; }
|
|
129
|
+
if (!/[Mm]ock/.test(txt)) return;
|
|
130
|
+
const mkHit = (idx) => { const line = txt.slice(0, idx).split("\n").length; return { file, line, text: (txt.split("\n")[line - 1] || "").trim() }; };
|
|
131
|
+
for (const { k, re } of keyRegexes) {
|
|
132
|
+
const m = re.exec(txt);
|
|
133
|
+
if (m) { if (/traceweave\.mock|URLProtocol|interceptor/i.test(txt) || k.includes("traceweave")) state.interceptor = true; state.runtimeKey = state.runtimeKey || k; state.push(`runtime-switch:${k}`, mkHit(m.index), repoPath); }
|
|
134
|
+
}
|
|
135
|
+
if (!state.runtimeKey) { const gm = genericRe.exec(txt); if (gm) { state.runtimeKey = gm[1]; state.push(`runtime-switch:${gm[1]}`, mkHit(gm.index), repoPath); } }
|
|
136
|
+
// launch-argument mock (ProcessInfo.arguments.contains) + arg constant (may be in different files)
|
|
137
|
+
if (laUseRe.test(txt)) state.launchArgUsed = true;
|
|
138
|
+
if (!state.launchArgConst) { const lm = laConstRe.exec(txt); if (lm) { state.launchArgConst = lm[1]; state.launchArgHit = mkHit(lm.index); } }
|
|
139
|
+
if (!state.constKeyCand) { const cm = constKeyRe.exec(txt); if (cm && !cm[1].startsWith("-")) { state.constKeyCand = cm[1]; state.constKeyHit = mkHit(cm.index); } }
|
|
140
|
+
const dm = diRe.exec(txt); if (dm) { state.debugDI = true; state.push("debug-di", mkHit(dm.index), repoPath); }
|
|
141
|
+
const tm = typeRe.exec(txt); if (tm) { state.mockTypeCount++; if (state.mockTypeCount <= 8) state.push(`mock-type:${tm[1]}`, mkHit(tm.index), repoPath); }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function detectMock({ repoPath, platform, extraKeys = [] } = {}) {
|
|
146
|
+
if (!repoPath || !existsSync(repoPath)) {
|
|
147
|
+
return { supported: false, platform: "unknown", mechanism: null, reason: `repoPath not found: ${repoPath}`, evidence: [] };
|
|
148
|
+
}
|
|
149
|
+
const plat = platform || detectPlatform(repoPath);
|
|
150
|
+
const keys = [...new Set([...KNOWN_MOCK_KEYS, ...extraKeys])];
|
|
151
|
+
|
|
152
|
+
const evidence = [];
|
|
153
|
+
const state = {
|
|
154
|
+
runtimeKey: null, launchArg: false, interceptor: false, debugDI: false, mockTypeCount: 0,
|
|
155
|
+
variants: new Set(),
|
|
156
|
+
push(signal, hit, rp) { evidence.push({ signal, file: rel(rp, hit.file), line: hit.line, snippet: (hit.text || "").slice(0, 160) }); },
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
let scanned = false;
|
|
160
|
+
if (hasRg()) { try { scanWithRg(repoPath, keys, state); scanned = true; } catch { /* fall back */ } }
|
|
161
|
+
if (!scanned) scanWithWalk(repoPath, keys, state);
|
|
162
|
+
|
|
163
|
+
// Resolve launch-argument / constant-defined mock switches (either scan sets the flags).
|
|
164
|
+
if (!state.runtimeKey) {
|
|
165
|
+
if (state.launchArgUsed && state.launchArgConst) {
|
|
166
|
+
state.runtimeKey = state.launchArgConst; state.launchArg = true;
|
|
167
|
+
if (state.launchArgHit) state.push(`runtime-launcharg:${state.launchArgConst}`, state.launchArgHit, repoPath);
|
|
168
|
+
} else if (state.constKeyCand) {
|
|
169
|
+
state.runtimeKey = state.constKeyCand;
|
|
170
|
+
if (state.constKeyHit) state.push(`runtime-switch:${state.constKeyCand}`, state.constKeyHit, repoPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const variantsHint = [...state.variants].slice(0, 40);
|
|
175
|
+
|
|
176
|
+
// If platform is still unknown, infer it from the evidence file extensions so
|
|
177
|
+
// the activation (iOS launch arg vs Android intent extra) is never mis-picked.
|
|
178
|
+
let effPlat = plat;
|
|
179
|
+
if (effPlat === "unknown") {
|
|
180
|
+
const exts = evidence.map((e) => (e.file || "").split(".").pop());
|
|
181
|
+
if (exts.includes("swift")) effPlat = "ios";
|
|
182
|
+
else if (exts.includes("kt") || exts.includes("kts")) effPlat = "android";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (effPlat === "android" && !hasLaunchableAndroidApp(repoPath)) {
|
|
186
|
+
return { supported: false, platform: effPlat, mechanism: null,
|
|
187
|
+
reason: "No launchable Android application module found (library set only) - nothing to run in mock mode.",
|
|
188
|
+
variantsHint, evidence };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (state.runtimeKey) {
|
|
192
|
+
const isFlag = state.launchArg || state.runtimeKey.startsWith("-");
|
|
193
|
+
const kind = state.interceptor ? "interceptor" : (isFlag ? "launch-argument" : "userdefaults-launcharg");
|
|
194
|
+
return {
|
|
195
|
+
supported: true, platform: effPlat, mechanism: kind,
|
|
196
|
+
activation: effPlat === "android"
|
|
197
|
+
? { kind, key: state.runtimeKey, value: "true", intentExtra: `--ez ${state.runtimeKey.replace(/^-/, "")} true`, buildConfig: "debug" }
|
|
198
|
+
: { kind, key: state.runtimeKey, value: isFlag ? "(flag)" : "YES", launchArg: isFlag ? state.runtimeKey : `-${state.runtimeKey} YES`, buildConfig: "Debug" },
|
|
199
|
+
variantsHint, evidence,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (state.debugDI || state.mockTypeCount > 0) {
|
|
203
|
+
return {
|
|
204
|
+
supported: "debug-only", platform: effPlat, mechanism: "debug-di",
|
|
205
|
+
activation: { kind: "build-config", buildConfig: effPlat === "android" ? "debug" : "Debug" },
|
|
206
|
+
note: "Mocks are compiled into the Debug build via #if DEBUG DI. No runtime switch → variants cannot be toggled at launch; only the default Debug state is comparable.",
|
|
207
|
+
variantsHint, evidence,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { supported: false, platform: effPlat, mechanism: null,
|
|
211
|
+
reason: "No mock switch, mock service naming, or MockData fixtures detected - project has no mock support this tool can drive.",
|
|
212
|
+
variantsHint, evidence };
|
|
213
|
+
}
|