@stag-build/phonebook 0.1.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +283 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +82 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/commands/doctor.d.ts +33 -0
  7. package/dist/commands/doctor.js +690 -0
  8. package/dist/commands/doctor.js.map +1 -0
  9. package/dist/commands/init.d.ts +27 -0
  10. package/dist/commands/init.js +415 -0
  11. package/dist/commands/init.js.map +1 -0
  12. package/dist/config.d.ts +33 -0
  13. package/dist/config.js +21 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/engines/android.d.ts +70 -0
  16. package/dist/engines/android.js +265 -0
  17. package/dist/engines/android.js.map +1 -0
  18. package/dist/engines/git.d.ts +4 -0
  19. package/dist/engines/git.js +13 -0
  20. package/dist/engines/git.js.map +1 -0
  21. package/dist/engines/ios.d.ts +55 -0
  22. package/dist/engines/ios.js +195 -0
  23. package/dist/engines/ios.js.map +1 -0
  24. package/dist/errors.d.ts +12 -0
  25. package/dist/errors.js +69 -0
  26. package/dist/errors.js.map +1 -0
  27. package/dist/gradle/catalog.d.ts +110 -0
  28. package/dist/gradle/catalog.js +413 -0
  29. package/dist/gradle/catalog.js.map +1 -0
  30. package/dist/ios/snapshotTestClass.d.ts +59 -0
  31. package/dist/ios/snapshotTestClass.js +195 -0
  32. package/dist/ios/snapshotTestClass.js.map +1 -0
  33. package/dist/manifest.d.ts +32 -0
  34. package/dist/manifest.js +23 -0
  35. package/dist/manifest.js.map +1 -0
  36. package/dist/mcp/server.d.ts +1 -0
  37. package/dist/mcp/server.js +226 -0
  38. package/dist/mcp/server.js.map +1 -0
  39. package/dist/naming.d.ts +18 -0
  40. package/dist/naming.js +34 -0
  41. package/dist/naming.js.map +1 -0
  42. package/dist/scan/android.d.ts +7 -0
  43. package/dist/scan/android.js +222 -0
  44. package/dist/scan/android.js.map +1 -0
  45. package/dist/scan/hints.d.ts +25 -0
  46. package/dist/scan/hints.js +338 -0
  47. package/dist/scan/hints.js.map +1 -0
  48. package/dist/scan/ios.d.ts +7 -0
  49. package/dist/scan/ios.js +201 -0
  50. package/dist/scan/ios.js.map +1 -0
  51. package/dist/scan/types.d.ts +52 -0
  52. package/dist/scan/types.js +7 -0
  53. package/dist/scan/types.js.map +1 -0
  54. package/dist/site/build.d.ts +17 -0
  55. package/dist/site/build.js +0 -0
  56. package/dist/site/build.js.map +1 -0
  57. package/dist/versions.d.ts +71 -0
  58. package/dist/versions.js +334 -0
  59. package/dist/versions.js.map +1 -0
  60. package/package.json +54 -0
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Scans a project's .swift files for a `class X: SnapshotTest` subclass,
3
+ * skipping build-output/dependency directories (.build, DerivedData, Pods,
4
+ * .git) and any path segment named "checkouts" — SPM package sources
5
+ * (including SnapshotPreviews' own SnapshotTest subclasses, if any) must not
6
+ * count towards this check.
7
+ */
8
+ export declare function findSnapshotTestSubclass(projectDir: string): Promise<{
9
+ className: string;
10
+ relativePath: string;
11
+ importsSnapshottingTests: boolean;
12
+ } | undefined>;
13
+ /**
14
+ * Reads the project's .pbxproj text: from the configured `ios.project` path
15
+ * when given, otherwise by discovering the first *.xcodeproj directly under
16
+ * projectDir (mirroring how `doctor` locates it for a workspace-only config).
17
+ */
18
+ export declare function readPbxprojText(projectDir: string, projectPath?: string): Promise<string>;
19
+ /** A PBXNativeTarget whose body references SnapshottingTests (i.e. links the product). */
20
+ export interface SnapshottingTestsTarget {
21
+ id: string;
22
+ name: string;
23
+ body: string;
24
+ }
25
+ /**
26
+ * Finds every PBXNativeTarget in the pbxproj that links the SnapshottingTests
27
+ * product. In practice there's exactly one (the app-hosted unit-test target);
28
+ * returned as a list to stay defensive.
29
+ */
30
+ export declare function findSnapshottingTestsTargets(pbxproj: string): SnapshottingTestsTarget[];
31
+ /**
32
+ * Heuristically checks whether the pbxproj target that links
33
+ * SnapshottingTests has TEST_HOST/BUNDLE_LOADER set in any of its build
34
+ * configurations (i.e. is hosted in the app, as SnapshotPreviews requires).
35
+ * Returns a note string when the wiring is present but hosting can't be
36
+ * confirmed; undefined otherwise (including when parsing fails — this is a
37
+ * best-effort heuristic, not a hard failure).
38
+ */
39
+ export declare function findMissingTestHostNote(pbxproj: string): string | undefined;
40
+ /** Where to put the missing SnapshotTest subclass, derived from the pbxproj. */
41
+ export interface SnapshotTestClassLocation {
42
+ /** Name of the PBXNativeTarget that links SnapshottingTests. */
43
+ targetName: string;
44
+ /**
45
+ * Folder path (relative to the project, "/"-joined) covered by a
46
+ * PBXFileSystemSynchronizedRootGroup tied to the target, if the project
47
+ * uses Xcode's synchronized groups (objectVersion >= ~77). When set,
48
+ * simply creating a .swift file in this folder is enough — Xcode picks it
49
+ * up automatically, no project-file edit required.
50
+ */
51
+ synchronizedFolder?: string;
52
+ }
53
+ /**
54
+ * Identifies the target that links SnapshottingTests and, for a project
55
+ * using filesystem-synchronized groups, the folder a new source file can be
56
+ * dropped into to be picked up automatically. Best-effort/heuristic — returns
57
+ * undefined if the target can't be identified or parsing fails.
58
+ */
59
+ export declare function findSnapshotTestClassLocation(pbxproj: string): SnapshotTestClassLocation | undefined;
@@ -0,0 +1,195 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { join, relative } from 'node:path';
3
+ /**
4
+ * Shared iOS SnapshotTest-subclass discovery: scanning project sources for an
5
+ * existing subclass, and (heuristically) parsing a .pbxproj to find which
6
+ * target links SnapshottingTests, whether it's properly hosted (TEST_HOST),
7
+ * and — for projects using Xcode's filesystem-synchronized groups — which
8
+ * folder a new file should be dropped into.
9
+ *
10
+ * Used by both `doctor` (to report on the setup) and `init --write-snapshot-class`
11
+ * (to safely write the missing file). Lives outside doctor.ts/init.ts to avoid a
12
+ * circular import between those two modules.
13
+ */
14
+ const SNAPSHOT_TEST_SUBCLASS_PATTERN = /class\s+(\w+)\s*:\s*SnapshotTest\b/;
15
+ const SNAPSHOT_TEST_SCAN_SKIP_DIRS = new Set(['.build', 'DerivedData', 'Pods', '.git']);
16
+ async function readTextIfExists(path) {
17
+ try {
18
+ return await readFile(path, 'utf8');
19
+ }
20
+ catch {
21
+ return '';
22
+ }
23
+ }
24
+ /**
25
+ * Scans a project's .swift files for a `class X: SnapshotTest` subclass,
26
+ * skipping build-output/dependency directories (.build, DerivedData, Pods,
27
+ * .git) and any path segment named "checkouts" — SPM package sources
28
+ * (including SnapshotPreviews' own SnapshotTest subclasses, if any) must not
29
+ * count towards this check.
30
+ */
31
+ export async function findSnapshotTestSubclass(projectDir) {
32
+ async function walk(dir) {
33
+ let entries;
34
+ try {
35
+ entries = await readdir(dir, { withFileTypes: true });
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ for (const entry of entries) {
41
+ const full = join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ if (SNAPSHOT_TEST_SCAN_SKIP_DIRS.has(entry.name) || entry.name === 'checkouts')
44
+ continue;
45
+ const found = await walk(full);
46
+ if (found)
47
+ return found;
48
+ continue;
49
+ }
50
+ if (!entry.isFile() || !entry.name.endsWith('.swift'))
51
+ continue;
52
+ const text = await readTextIfExists(full);
53
+ const match = text.match(SNAPSHOT_TEST_SUBCLASS_PATTERN);
54
+ if (match) {
55
+ return {
56
+ className: match[1],
57
+ relativePath: relative(projectDir, full),
58
+ // The SnapshotTest base class lives in the SnapshottingTests module;
59
+ // any other import (e.g. SnapshotPreviews) fails to compile.
60
+ importsSnapshottingTests: /^\s*import\s+SnapshottingTests\b/m.test(text),
61
+ };
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+ return walk(projectDir);
67
+ }
68
+ /**
69
+ * Extracts the bodies of every object with the given `isa` from a raw
70
+ * .pbxproj text, keyed by object id. Minimal/defensive regex parsing — no
71
+ * plist library — matched against the standard pbxproj indentation.
72
+ */
73
+ function extractPbxprojObjects(pbxproj, isa) {
74
+ const map = new Map();
75
+ const re = new RegExp(`\\n\\t\\t([0-9A-F]{24}) [^\\n]*=\\s*\\{\\n\\t\\t\\tisa = ${isa};([\\s\\S]*?)\\n\\t\\t\\};`, 'g');
76
+ let match;
77
+ while ((match = re.exec(pbxproj))) {
78
+ map.set(match[1], match[2]);
79
+ }
80
+ return map;
81
+ }
82
+ function unquote(value) {
83
+ const trimmed = value.trim();
84
+ return trimmed.startsWith('"') && trimmed.endsWith('"') ? trimmed.slice(1, -1) : trimmed;
85
+ }
86
+ /**
87
+ * Reads the project's .pbxproj text: from the configured `ios.project` path
88
+ * when given, otherwise by discovering the first *.xcodeproj directly under
89
+ * projectDir (mirroring how `doctor` locates it for a workspace-only config).
90
+ */
91
+ export async function readPbxprojText(projectDir, projectPath) {
92
+ if (projectPath) {
93
+ return readTextIfExists(join(projectPath, 'project.pbxproj'));
94
+ }
95
+ try {
96
+ const texts = [];
97
+ for (const entry of await readdir(projectDir)) {
98
+ if (entry.endsWith('.xcodeproj')) {
99
+ const text = await readTextIfExists(join(projectDir, entry, 'project.pbxproj'));
100
+ if (text)
101
+ texts.push(text);
102
+ }
103
+ }
104
+ return texts.join('\n');
105
+ }
106
+ catch {
107
+ return '';
108
+ }
109
+ }
110
+ /**
111
+ * Finds every PBXNativeTarget in the pbxproj that links the SnapshottingTests
112
+ * product. In practice there's exactly one (the app-hosted unit-test target);
113
+ * returned as a list to stay defensive.
114
+ */
115
+ export function findSnapshottingTestsTargets(pbxproj) {
116
+ const targets = extractPbxprojObjects(pbxproj, 'PBXNativeTarget');
117
+ const result = [];
118
+ for (const [id, body] of targets) {
119
+ if (!body.includes('SnapshottingTests'))
120
+ continue;
121
+ const nameMatch = body.match(/\n\t\t\tname = ([^\n;]+);/);
122
+ result.push({ id, name: nameMatch ? unquote(nameMatch[1]) : id, body });
123
+ }
124
+ return result;
125
+ }
126
+ /**
127
+ * Heuristically checks whether the pbxproj target that links
128
+ * SnapshottingTests has TEST_HOST/BUNDLE_LOADER set in any of its build
129
+ * configurations (i.e. is hosted in the app, as SnapshotPreviews requires).
130
+ * Returns a note string when the wiring is present but hosting can't be
131
+ * confirmed; undefined otherwise (including when parsing fails — this is a
132
+ * best-effort heuristic, not a hard failure).
133
+ */
134
+ export function findMissingTestHostNote(pbxproj) {
135
+ if (!pbxproj.includes('SnapshottingTests'))
136
+ return undefined;
137
+ try {
138
+ const targets = findSnapshottingTestsTargets(pbxproj);
139
+ const configLists = extractPbxprojObjects(pbxproj, 'XCConfigurationList');
140
+ const buildConfigs = extractPbxprojObjects(pbxproj, 'XCBuildConfiguration');
141
+ for (const target of targets) {
142
+ const confListMatch = target.body.match(/buildConfigurationList = ([0-9A-F]{24})/);
143
+ const confListBody = confListMatch ? configLists.get(confListMatch[1]) : undefined;
144
+ if (!confListBody)
145
+ continue;
146
+ const buildConfigIds = [...confListBody.matchAll(/([0-9A-F]{24}) \/\*/g)].map((m) => m[1]);
147
+ if (buildConfigIds.length === 0)
148
+ continue;
149
+ const hasHost = buildConfigIds.some((id) => {
150
+ const cfgBody = buildConfigs.get(id);
151
+ return cfgBody !== undefined && (cfgBody.includes('TEST_HOST') || cfgBody.includes('BUNDLE_LOADER'));
152
+ });
153
+ if (!hasHost) {
154
+ return 'the target linking SnapshottingTests appears to have no TEST_HOST; SnapshotPreviews must run in a test target hosted in the app';
155
+ }
156
+ }
157
+ }
158
+ catch {
159
+ return undefined;
160
+ }
161
+ return undefined;
162
+ }
163
+ /**
164
+ * Identifies the target that links SnapshottingTests and, for a project
165
+ * using filesystem-synchronized groups, the folder a new source file can be
166
+ * dropped into to be picked up automatically. Best-effort/heuristic — returns
167
+ * undefined if the target can't be identified or parsing fails.
168
+ */
169
+ export function findSnapshotTestClassLocation(pbxproj) {
170
+ try {
171
+ const targets = findSnapshottingTestsTargets(pbxproj);
172
+ if (targets.length === 0)
173
+ return undefined;
174
+ const target = targets[0];
175
+ const syncGroups = extractPbxprojObjects(pbxproj, 'PBXFileSystemSynchronizedRootGroup');
176
+ const groupsMatch = target.body.match(/fileSystemSynchronizedGroups = \(([\s\S]*?)\)/);
177
+ let synchronizedFolder;
178
+ if (groupsMatch) {
179
+ const groupIds = [...groupsMatch[1].matchAll(/([0-9A-F]{24})/g)].map((m) => m[1]);
180
+ for (const gid of groupIds) {
181
+ const groupBody = syncGroups.get(gid);
182
+ const pathMatch = groupBody?.match(/\n\t\t\tpath = ([^\n;]+);/);
183
+ if (pathMatch) {
184
+ synchronizedFolder = unquote(pathMatch[1]);
185
+ break;
186
+ }
187
+ }
188
+ }
189
+ return { targetName: target.name, synchronizedFolder };
190
+ }
191
+ catch {
192
+ return undefined;
193
+ }
194
+ }
195
+ //# sourceMappingURL=snapshotTestClass.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshotTestClass.js","sourceRoot":"","sources":["../../src/ios/snapshotTestClass.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export declare const SCHEMA_VERSION = 1;
2
+ export type Platform = 'android' | 'ios';
3
+ export interface ManifestEntry {
4
+ /** Display name of the component, e.g. "Button" */
5
+ component: string;
6
+ /** Display name of the state, e.g. "Disabled" */
7
+ state: string;
8
+ /** Gradle module / Xcode target the preview lives in */
9
+ module: string;
10
+ /** Path of the source file containing the preview, relative to the project root, when known */
11
+ sourceFile?: string;
12
+ /** Fully qualified original preview identifier (FQN of the preview function / test) */
13
+ previewName: string;
14
+ /** Image path relative to the bundle root, e.g. "images/ab12cd.png" */
15
+ image: string;
16
+ width?: number;
17
+ height?: number;
18
+ theme?: 'light' | 'dark';
19
+ device?: string;
20
+ tags?: string[];
21
+ }
22
+ export interface Manifest {
23
+ schemaVersion: typeof SCHEMA_VERSION;
24
+ platform: Platform;
25
+ app: {
26
+ name: string;
27
+ commit?: string;
28
+ generatedAt: string;
29
+ };
30
+ entries: ManifestEntry[];
31
+ }
32
+ export declare function validateManifest(data: unknown): Manifest;
@@ -0,0 +1,23 @@
1
+ export const SCHEMA_VERSION = 1;
2
+ export function validateManifest(data) {
3
+ const m = data;
4
+ if (!m || typeof m !== 'object')
5
+ throw new Error('manifest.json is not an object');
6
+ if (m.schemaVersion !== SCHEMA_VERSION) {
7
+ throw new Error(`Unsupported manifest schemaVersion ${m.schemaVersion}, expected ${SCHEMA_VERSION}`);
8
+ }
9
+ if (m.platform !== 'android' && m.platform !== 'ios') {
10
+ throw new Error(`Unknown platform "${m.platform}"`);
11
+ }
12
+ if (!Array.isArray(m.entries))
13
+ throw new Error('manifest.entries is not an array');
14
+ for (const e of m.entries) {
15
+ for (const key of ['component', 'state', 'module', 'previewName', 'image']) {
16
+ if (typeof e[key] !== 'string' || e[key].length === 0) {
17
+ throw new Error(`manifest entry missing "${key}": ${JSON.stringify(e)}`);
18
+ }
19
+ }
20
+ }
21
+ return m;
22
+ }
23
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAmChC,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,MAAM,CAAC,GAAG,IAAgB,CAAC;IAC3B,IAAI,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACnF,IAAI,CAAC,CAAC,aAAa,KAAK,cAAc,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC,aAAa,cAAc,cAAc,EAAE,CAAC,CAAC;IACvG,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CAAC,qBAAqB,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC;IACtD,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,CAAC,CAAC;IACnF,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;QAC1B,KAAK,MAAM,GAAG,IAAI,CAAC,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,CAAU,EAAE,CAAC;YACpF,IAAI,OAAO,CAAC,CAAC,GAAG,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACtD,MAAM,IAAI,KAAK,CAAC,2BAA2B,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YAC3E,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1 @@
1
+ export declare function runMcpServer(): Promise<void>;
@@ -0,0 +1,226 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { z } from 'zod';
7
+ import { loadConfig } from '../config.js';
8
+ import { collectDoctorChecks } from '../commands/doctor.js';
9
+ import { generateAndroid } from '../engines/android.js';
10
+ import { generateIos } from '../engines/ios.js';
11
+ import { buildSite } from '../site/build.js';
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ async function readPackageVersion() {
14
+ try {
15
+ const raw = await readFile(resolve(__dirname, '../../package.json'), 'utf8');
16
+ const pkg = JSON.parse(raw);
17
+ return pkg.version ?? '0.0.0';
18
+ }
19
+ catch {
20
+ return '0.0.0';
21
+ }
22
+ }
23
+ function errorResult(message) {
24
+ return { isError: true, content: [{ type: 'text', text: message }] };
25
+ }
26
+ function textResult(text) {
27
+ return { content: [{ type: 'text', text }] };
28
+ }
29
+ function summarizeCoverage(report) {
30
+ const { stats } = report;
31
+ const withoutPreview = stats.components - stats.withPreview;
32
+ const orphanCount = report.orphanPreviews.length;
33
+ return [
34
+ `Components: ${stats.components}`,
35
+ `With preview: ${stats.withPreview}`,
36
+ `Without preview: ${withoutPreview}`,
37
+ `With dark preview: ${stats.withDarkPreview}`,
38
+ orphanCount > 0
39
+ ? `Orphan previews (could not be matched to a component in the same file): ${orphanCount}`
40
+ : `Orphan previews (unmatched to a component): ${orphanCount}`,
41
+ ].join('\n');
42
+ }
43
+ const HINT_LIST_CAP = 40;
44
+ function summarizeHints(report) {
45
+ const allPreviews = [
46
+ ...report.components.flatMap((c) => c.previews),
47
+ ...report.orphanPreviews,
48
+ ];
49
+ const entries = allPreviews.flatMap((p) => (p.hints ?? []).map((h) => ({
50
+ file: p.file,
51
+ line: p.line,
52
+ functionName: p.name,
53
+ hint: h,
54
+ })));
55
+ if (entries.length === 0) {
56
+ return 'Configuration hints (0)';
57
+ }
58
+ const shown = entries.slice(0, HINT_LIST_CAP);
59
+ const lines = shown.map((e) => `${e.file}:${e.line} ${e.functionName} [${e.hint.rule}] ${e.hint.message} -> ${e.hint.suggestion}`);
60
+ const remaining = entries.length - shown.length;
61
+ if (remaining > 0) {
62
+ lines.push(`... and ${remaining} more (see JSON)`);
63
+ }
64
+ return [`Configuration hints (${entries.length})`, ...lines].join('\n');
65
+ }
66
+ const NAMING_GUIDANCE = `Naming convention (see docs/naming-convention.md):
67
+ 1. A display name containing "/" splits into component/state (e.g. "Button/Enabled").
68
+ 2. A display name without "/" becomes the state; the component comes from the function name.
69
+ 3. No display name: the function name (minus a leading/trailing "Preview") is the component, and the state is "Default".
70
+ Component names are camel-case-spaced ("UserCard" -> "User Card", acronym runs kept together: "URLBar" -> "URL Bar").
71
+ Dark mode: Android infers "Dark" state from uiMode = UI_MODE_NIGHT_YES and strips a trailing Dark/Night suffix from the function name; iOS has no such inference, so give the preview an explicit "Component/Dark" name and add .preferredColorScheme(.dark).
72
+
73
+ Configuration hints: analyze_coverage also flags previews whose name implies a configuration (landscape, dark, tablet, RTL, large font, ...) that the annotation doesn't actually declare — e.g. a function named "...Landscape" with a plain @Preview() renders portrait. Fix by adding the declared trait the hint suggests (device/orientation spec, uiMode, locale, fontScale, traits: for iOS, etc.), or renaming if the config was never intended.
74
+
75
+ Canvas size (Android): preview dimensions come only from the @Preview annotation — widthDp/heightDp or device = "spec:width=..dp,height=..dp,...". Wrapping the composable in Modifier.size(...)/width(...)/height(...) in the preview body does NOT resize the canvas: the content is sized inside a canvas that stays the default phone size, so wide or large content is clipped, not fit. Correct: @Preview(widthDp = 891, heightDp = 411) fun ... { LoginContentLandscape() } — no size Modifier needed.
76
+
77
+ Canvas size (iOS): preview dimensions come from traits: (e.g. .fixedLayout(width:height:)) or .previewDevice on the #Preview, not from a frame modifier in the view body.
78
+
79
+ Snapshot test class (iOS): the SnapshotTest subclass that records previews must live in the app-hosted unit-test target's own folder (the target linking SnapshottingTests, hosted via TEST_HOST/BUNDLE_LOADER) — run \`phonebook doctor\` to see which target and where. In a project using Xcode's filesystem-synchronized groups, just creating the file in that folder is enough (Xcode picks it up automatically); \`phonebook init --write-snapshot-class\` can do this for you when that condition holds.`;
80
+ function androidTemplate(component, states) {
81
+ const name = component || 'Component';
82
+ const stateList = states.length > 0 ? states : ['Default'];
83
+ const blocks = stateList
84
+ .filter((s) => s.toLowerCase() !== 'dark')
85
+ .map((state) => `@Preview(name = "${name}/${state}")
86
+ @Composable
87
+ private fun ${name}${state}Preview() {
88
+ ${name}(/* TODO: pass ${state} state props */)
89
+ }`)
90
+ .join('\n\n');
91
+ const dark = `@Preview(name = "${name}/Dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
92
+ @Composable
93
+ private fun ${name}DarkPreview() {
94
+ ${name}(/* TODO: pass default props */)
95
+ }`;
96
+ return `${blocks}\n\n${dark}`;
97
+ }
98
+ function iosTemplate(component, states) {
99
+ const name = component || 'Component';
100
+ const stateList = states.length > 0 ? states : ['Default'];
101
+ const blocks = stateList
102
+ .filter((s) => s.toLowerCase() !== 'dark')
103
+ .map((state) => `#Preview("${name}/${state}", traits: .sizeThatFitsLayout) {
104
+ ${name}(/* TODO: pass ${state} state props */)
105
+ }`)
106
+ .join('\n\n');
107
+ const dark = `#Preview("${name}/Dark", traits: .sizeThatFitsLayout) {
108
+ ${name}(/* TODO: pass default props */)
109
+ .preferredColorScheme(.dark)
110
+ }`;
111
+ return `${blocks}\n\n${dark}`;
112
+ }
113
+ export async function runMcpServer() {
114
+ const version = await readPackageVersion();
115
+ const server = new McpServer({ name: 'phonebook', version });
116
+ server.registerTool('analyze_coverage', {
117
+ description: 'Scan the codebase for UI components and the previews that cover them: which have previews, which states/themes are missing. Read-only.',
118
+ inputSchema: {
119
+ dir: z.string().default('.').describe('Project directory containing phonebook.config.json'),
120
+ },
121
+ }, async ({ dir }) => {
122
+ let projectDir;
123
+ let platform;
124
+ let modules;
125
+ try {
126
+ const loaded = await loadConfig(dir);
127
+ projectDir = loaded.projectDir;
128
+ platform = loaded.config.platform;
129
+ modules = loaded.config.android?.modules ?? [':app'];
130
+ }
131
+ catch (err) {
132
+ return errorResult(`${err.message}\nRun \`phonebook init\` first to scaffold phonebook.config.json.`);
133
+ }
134
+ let report;
135
+ try {
136
+ if (platform === 'android') {
137
+ const { scanAndroid } = await import('../scan/android.js');
138
+ report = await scanAndroid(projectDir, modules);
139
+ }
140
+ else {
141
+ const { scanIos } = await import('../scan/ios.js');
142
+ report = await scanIos(projectDir);
143
+ }
144
+ }
145
+ catch (err) {
146
+ return errorResult(`Failed to analyze coverage: ${err.message}`);
147
+ }
148
+ const summary = summarizeCoverage(report);
149
+ const hints = summarizeHints(report);
150
+ return textResult(`${summary}\n\n${hints}\n\n${JSON.stringify(report, null, 2)}`);
151
+ });
152
+ server.registerTool('check_setup', {
153
+ description: "Check that the project is correctly set up for `phonebook generate` (same checks as `phonebook doctor`): libraries wired, test target present, JDK/Xcode/simulator available.",
154
+ inputSchema: {
155
+ dir: z.string().default('.').describe('Project directory containing phonebook.config.json'),
156
+ },
157
+ }, async ({ dir }) => {
158
+ const { lines, ok } = await collectDoctorChecks(dir);
159
+ const text = [...lines, ok ? 'READY' : 'NOT READY'].join('\n');
160
+ return textResult(text);
161
+ });
162
+ server.registerTool('get_preview_guidance', {
163
+ description: 'Return the preview naming convention plus a ready-to-paste preview code template for a component, so any agent writes consistent previews.',
164
+ inputSchema: {
165
+ platform: z.enum(['android', 'ios']).describe('Target platform'),
166
+ component: z.string().optional().describe('Component name, e.g. "Button"'),
167
+ states: z.array(z.string()).optional().describe('State names, e.g. ["Enabled", "Disabled"]'),
168
+ },
169
+ }, async ({ platform, component, states }) => {
170
+ const name = component ?? 'Component';
171
+ const stateList = states && states.length > 0 ? states : ['Default'];
172
+ const template = platform === 'android' ? androidTemplate(name, stateList) : iosTemplate(name, stateList);
173
+ const text = `${NAMING_GUIDANCE}\n\nTemplate for ${name} (${platform}):\n\n${template}`;
174
+ return textResult(text);
175
+ });
176
+ server.registerTool('run_generate', {
177
+ description: 'Run the platform engine to render all previews and produce a bundle (manifest + images), same as `phonebook generate`.',
178
+ inputSchema: {
179
+ dir: z.string().default('.').describe('Project directory containing phonebook.config.json'),
180
+ },
181
+ }, async ({ dir }) => {
182
+ let projectDir;
183
+ try {
184
+ const loaded = await loadConfig(dir);
185
+ projectDir = loaded.projectDir;
186
+ const outputDir = resolve(projectDir, loaded.config.output ?? 'phonebook-out');
187
+ const manifest = loaded.config.platform === 'android'
188
+ ? await generateAndroid(loaded.config, projectDir, outputDir, { quiet: true })
189
+ : await generateIos(loaded.config, projectDir, outputDir, { quiet: true });
190
+ const componentStates = manifest.entries.map((e) => `${e.component}/${e.state}`).sort();
191
+ const text = [
192
+ `Recorded ${manifest.entries.length} previews -> ${outputDir}`,
193
+ '',
194
+ ...componentStates,
195
+ ].join('\n');
196
+ return textResult(text);
197
+ }
198
+ catch (err) {
199
+ return errorResult(`${err.message}\nRun the check_setup tool (or \`phonebook doctor\`) to diagnose the project setup.`);
200
+ }
201
+ });
202
+ server.registerTool('run_build', {
203
+ description: 'Build the static gallery site from a bundle, same as `phonebook build <bundle>`.',
204
+ inputSchema: {
205
+ bundle: z.string().describe('Bundle directory produced by run_generate / `phonebook generate`'),
206
+ output: z
207
+ .string()
208
+ .optional()
209
+ .describe('Site output directory (default: the bundle directory itself, reusing its images)'),
210
+ },
211
+ }, async ({ bundle, output }) => {
212
+ try {
213
+ // Default to building in place so the site reuses the bundle's images
214
+ // instead of duplicating every screenshot into a sibling directory.
215
+ const outDir = output ? resolve(output) : resolve(bundle);
216
+ const count = await buildSite(resolve(bundle), outDir);
217
+ return textResult(`Built gallery with ${count} screenshots -> ${outDir}/index.html`);
218
+ }
219
+ catch (err) {
220
+ return errorResult(err.message);
221
+ }
222
+ });
223
+ const transport = new StdioServerTransport();
224
+ await server.connect(transport);
225
+ }
226
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Naming convention: turn a raw preview identifier into { component, state }.
3
+ *
4
+ * Precedence:
5
+ * 1. An explicit preview display name containing "/" splits into component/state
6
+ * (e.g. @Preview(name = "Button/Disabled")).
7
+ * 2. An explicit preview display name without "/" becomes the state; the
8
+ * component is derived from the function name.
9
+ * 3. Otherwise the function name minus a leading/trailing "Preview" is the
10
+ * component and the state is "Default".
11
+ */
12
+ export interface ComponentState {
13
+ component: string;
14
+ state: string;
15
+ }
16
+ export declare function parsePreviewName(functionName: string, displayName?: string): ComponentState;
17
+ /** "PrimaryButton" -> "Primary Button"; leaves acronym runs intact ("URLBar" -> "URL Bar") */
18
+ export declare function spaceCamelCase(s: string): string;
package/dist/naming.js ADDED
@@ -0,0 +1,34 @@
1
+ export function parsePreviewName(functionName, displayName) {
2
+ const component = stripPreview(functionName);
3
+ const name = displayName?.trim();
4
+ if (name) {
5
+ const slash = name.indexOf('/');
6
+ if (slash > 0 && slash < name.length - 1) {
7
+ return {
8
+ component: spaceCamelCase(name.slice(0, slash).trim()),
9
+ state: name.slice(slash + 1).trim(),
10
+ };
11
+ }
12
+ return { component, state: name };
13
+ }
14
+ return { component, state: 'Default' };
15
+ }
16
+ function stripPreview(functionName) {
17
+ let n = functionName;
18
+ if (n.endsWith('Preview'))
19
+ n = n.slice(0, -'Preview'.length);
20
+ else if (n.startsWith('Preview'))
21
+ n = n.slice('Preview'.length);
22
+ if (n.length === 0)
23
+ n = functionName;
24
+ return spaceCamelCase(n);
25
+ }
26
+ /** "PrimaryButton" -> "Primary Button"; leaves acronym runs intact ("URLBar" -> "URL Bar") */
27
+ export function spaceCamelCase(s) {
28
+ return s
29
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
30
+ .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
31
+ .replace(/_/g, ' ')
32
+ .trim();
33
+ }
34
+ //# sourceMappingURL=naming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,gBAAgB,CAAC,YAAoB,EAAE,WAAoB;IACzE,MAAM,SAAS,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC;IAC7C,MAAM,IAAI,GAAG,WAAW,EAAE,IAAI,EAAE,CAAC;IACjC,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAChC,IAAI,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzC,OAAO;gBACL,SAAS,EAAE,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;gBACtD,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE;aACpC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACpC,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AACzC,CAAC;AAED,SAAS,YAAY,CAAC,YAAoB;IACxC,IAAI,CAAC,GAAG,YAAY,CAAC;IACrB,IAAI,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;SACxD,IAAI,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IAChE,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC;QAAE,CAAC,GAAG,YAAY,CAAC;IACrC,OAAO,cAAc,CAAC,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,cAAc,CAAC,CAAS;IACtC,OAAO,CAAC;SACL,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC;SACtC,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC;SACzC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC;SAClB,IAAI,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { CoverageReport } from './types.js';
2
+ /**
3
+ * Regex-based heuristic scanner for Jetpack Compose `@Composable` / `@Preview`
4
+ * functions. Walks each module's `src/main` tree and matches previews to
5
+ * components by file + name-prefix.
6
+ */
7
+ export declare function scanAndroid(projectDir: string, modules: string[]): Promise<CoverageReport>;