@ceraph/react-native-mcp 0.2.2 → 0.3.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/LICENSE +116 -15
- package/README.md +79 -77
- package/assets/default.png +0 -0
- package/dist/app-lifecycle.d.ts +50 -0
- package/dist/app-lifecycle.js +487 -0
- package/dist/camera-image-writer.d.ts +43 -0
- package/dist/camera-image-writer.js +280 -0
- package/dist/camera-registry-sync.d.ts +18 -0
- package/dist/camera-registry-sync.js +117 -0
- package/dist/cli.d.ts +0 -7
- package/dist/cli.js +41 -9
- package/dist/device-autonomy.d.ts +30 -0
- package/dist/device-autonomy.js +117 -0
- package/dist/error-parser.d.ts +6 -26
- package/dist/error-parser.js +4 -74
- package/dist/expo-manager.d.ts +2 -74
- package/dist/expo-manager.js +11 -125
- package/dist/index.d.ts +0 -7
- package/dist/index.js +1266 -56
- package/dist/init/ast-camera.d.ts +29 -0
- package/dist/init/ast-camera.js +267 -0
- package/dist/init/ast-layout.d.ts +15 -0
- package/dist/init/ast-layout.js +167 -0
- package/dist/init/claude-hook-constants.d.ts +9 -0
- package/dist/init/claude-hook-constants.js +91 -0
- package/dist/init/lan-ip.d.ts +11 -0
- package/dist/init/lan-ip.js +51 -0
- package/dist/init/monorepo.d.ts +13 -0
- package/dist/init/monorepo.js +185 -0
- package/dist/init/oauth.d.ts +52 -0
- package/dist/init/oauth.js +220 -0
- package/dist/init/package-manager.d.ts +11 -0
- package/dist/init/package-manager.js +60 -0
- package/dist/init/prompt.d.ts +12 -0
- package/dist/init/prompt.js +68 -0
- package/dist/init/shell-profile.d.ts +22 -0
- package/dist/init/shell-profile.js +85 -0
- package/dist/init/steps.d.ts +135 -0
- package/dist/init/steps.js +399 -0
- package/dist/init/url-scheme.d.ts +42 -0
- package/dist/init/url-scheme.js +187 -0
- package/dist/init/walkthrough.d.ts +76 -0
- package/dist/init/walkthrough.js +340 -0
- package/dist/init.d.ts +7 -7
- package/dist/init.js +280 -120
- package/dist/iproxy-manager.d.ts +32 -0
- package/dist/iproxy-manager.js +216 -0
- package/dist/mac-caffeinate.d.ts +10 -0
- package/dist/mac-caffeinate.js +56 -0
- package/dist/permission-interceptor.d.ts +29 -0
- package/dist/permission-interceptor.js +185 -0
- package/dist/prebuild-detector.d.ts +0 -30
- package/dist/prebuild-detector.js +1 -42
- package/dist/preflight.d.ts +34 -0
- package/dist/preflight.js +847 -0
- package/dist/screen.d.ts +132 -43
- package/dist/screen.js +668 -94
- package/dist/shim/boot.d.ts +41 -0
- package/dist/shim/boot.js +141 -0
- package/dist/shim/camera.d.ts +22 -0
- package/dist/shim/camera.js +62 -0
- package/dist/shim/config.d.ts +6 -0
- package/dist/shim/config.js +56 -0
- package/dist/shim/deep-link.d.ts +1 -0
- package/dist/shim/deep-link.js +25 -0
- package/dist/shim/dev-guard.d.ts +1 -0
- package/dist/shim/dev-guard.js +3 -0
- package/dist/shim/error-handler.d.ts +20 -0
- package/dist/shim/error-handler.js +66 -0
- package/dist/shim/fetch-interceptor.d.ts +13 -0
- package/dist/shim/fetch-interceptor.js +93 -0
- package/dist/shim/index.d.ts +6 -0
- package/dist/shim/index.js +6 -0
- package/dist/shim/keep-awake.d.ts +13 -0
- package/dist/shim/keep-awake.js +118 -0
- package/dist/shim/reload.d.ts +23 -0
- package/dist/shim/reload.js +76 -0
- package/dist/shim/signal-capture.d.ts +11 -0
- package/dist/shim/signal-capture.js +15 -0
- package/dist/shim/signal-transport.d.ts +17 -0
- package/dist/shim/signal-transport.js +43 -0
- package/dist/signal-listener.d.ts +27 -0
- package/dist/signal-listener.js +135 -0
- package/dist/simulator-boot.d.ts +52 -0
- package/dist/simulator-boot.js +227 -0
- package/dist/target.d.ts +48 -0
- package/dist/target.js +267 -0
- package/dist/uninstall/cli-runner.d.ts +32 -0
- package/dist/uninstall/cli-runner.js +223 -0
- package/dist/uninstall/footprint.d.ts +40 -0
- package/dist/uninstall/footprint.js +288 -0
- package/dist/uninstall/mcp-tools.d.ts +14 -0
- package/dist/uninstall/mcp-tools.js +175 -0
- package/dist/uninstall/revert-auth.d.ts +22 -0
- package/dist/uninstall/revert-auth.js +31 -0
- package/dist/uninstall/revert-boot.d.ts +24 -0
- package/dist/uninstall/revert-boot.js +242 -0
- package/dist/uninstall/revert-camera.d.ts +12 -0
- package/dist/uninstall/revert-camera.js +199 -0
- package/dist/uninstall/revert-ceraph-dir.d.ts +27 -0
- package/dist/uninstall/revert-ceraph-dir.js +38 -0
- package/dist/uninstall/revert-claude-hooks.d.ts +19 -0
- package/dist/uninstall/revert-claude-hooks.js +191 -0
- package/dist/uninstall/revert-gitignore.d.ts +17 -0
- package/dist/uninstall/revert-gitignore.js +43 -0
- package/dist/uninstall/revert-mcp-clients.d.ts +57 -0
- package/dist/uninstall/revert-mcp-clients.js +194 -0
- package/dist/uninstall/revert-package.d.ts +34 -0
- package/dist/uninstall/revert-package.js +98 -0
- package/dist/uninstall/revert-scheme.d.ts +36 -0
- package/dist/uninstall/revert-scheme.js +139 -0
- package/dist/uninstall/revert-signal-host-env.d.ts +31 -0
- package/dist/uninstall/revert-signal-host-env.js +61 -0
- package/dist/uninstall/walkthrough.d.ts +80 -0
- package/dist/uninstall/walkthrough.js +1244 -0
- package/dist/utils/atomic-write.d.ts +1 -0
- package/dist/utils/atomic-write.js +30 -0
- package/dist/wait-for-device.d.ts +68 -0
- package/dist/wait-for-device.js +368 -0
- package/dist/wda-manager.d.ts +38 -0
- package/dist/wda-manager.js +186 -0
- package/dist/wda-simulator.d.ts +28 -0
- package/dist/wda-simulator.js +257 -0
- package/package.json +38 -5
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface CameraReplacementEdit {
|
|
2
|
+
filePath: string;
|
|
3
|
+
relPath: string;
|
|
4
|
+
line: number;
|
|
5
|
+
suggestedKey: string;
|
|
6
|
+
alreadyHasImageKey: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ScanCameraViewsResult {
|
|
9
|
+
edits: CameraReplacementEdit[];
|
|
10
|
+
filesScanned: number;
|
|
11
|
+
}
|
|
12
|
+
export declare function suggestImageKey(absPath: string): string;
|
|
13
|
+
export declare function scanCameraViews(projectDir: string): Promise<ScanCameraViewsResult>;
|
|
14
|
+
export interface ApplyEditsResult {
|
|
15
|
+
filesEdited: string[];
|
|
16
|
+
totalReplacements: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ApplyEditsOptions {
|
|
19
|
+
omitImageKey?: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function applyCameraEdits(projectDir: string, edits: CameraReplacementEdit[], options?: ApplyEditsOptions): Promise<ApplyEditsResult>;
|
|
22
|
+
export interface SetImageKeyResult {
|
|
23
|
+
applied: boolean;
|
|
24
|
+
previousValue?: string | null;
|
|
25
|
+
reason?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare function setImageKeyOnExistingTag(filePath: string, line: number, imageKey: string): Promise<SetImageKeyResult>;
|
|
28
|
+
export declare function snapshotFile(filePath: string): Promise<string | null>;
|
|
29
|
+
export declare function restoreFile(filePath: string, snapshot: string): Promise<void>;
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join, relative, sep } from "node:path";
|
|
3
|
+
import { IndentationText, Project, SyntaxKind, Node, } from "ts-morph";
|
|
4
|
+
const SCAN_ROOTS = ["app", "src", "screens"];
|
|
5
|
+
const SCAN_EXTENSIONS = new Set([".tsx", ".jsx"]);
|
|
6
|
+
const SKIP_DIRS = new Set([
|
|
7
|
+
"node_modules",
|
|
8
|
+
"dist",
|
|
9
|
+
"build",
|
|
10
|
+
".git",
|
|
11
|
+
".ceraph",
|
|
12
|
+
".rn-mcp-cache",
|
|
13
|
+
".expo",
|
|
14
|
+
".next",
|
|
15
|
+
"ios",
|
|
16
|
+
"android",
|
|
17
|
+
]);
|
|
18
|
+
const SHIM_IMPORT_MODULE = "@ceraph/react-native-mcp/shim";
|
|
19
|
+
const SHIM_IMPORT_NAME = "CeraphCamera";
|
|
20
|
+
export function suggestImageKey(absPath) {
|
|
21
|
+
const file = basename(absPath).replace(/\.(tsx|jsx)$/i, "");
|
|
22
|
+
const parent = basename(dirname(absPath));
|
|
23
|
+
const generic = new Set(["page", "index", "screen", "route", "layout"]);
|
|
24
|
+
const stem = generic.has(file.toLowerCase()) ? parent : file;
|
|
25
|
+
const kebab = stem
|
|
26
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
27
|
+
.toLowerCase()
|
|
28
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
29
|
+
.replace(/^-+|-+$/g, "");
|
|
30
|
+
return kebab.length > 0 ? kebab : "camera";
|
|
31
|
+
}
|
|
32
|
+
async function listCandidateFiles(projectDir) {
|
|
33
|
+
const out = [];
|
|
34
|
+
async function walk(dir) {
|
|
35
|
+
let entries;
|
|
36
|
+
try {
|
|
37
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const abs = join(dir, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
if (SKIP_DIRS.has(entry.name))
|
|
46
|
+
continue;
|
|
47
|
+
await walk(abs);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!entry.isFile())
|
|
51
|
+
continue;
|
|
52
|
+
const dot = entry.name.lastIndexOf(".");
|
|
53
|
+
if (dot < 0)
|
|
54
|
+
continue;
|
|
55
|
+
const ext = entry.name.slice(dot).toLowerCase();
|
|
56
|
+
if (!SCAN_EXTENSIONS.has(ext))
|
|
57
|
+
continue;
|
|
58
|
+
out.push(abs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const sub of SCAN_ROOTS) {
|
|
62
|
+
await walk(join(projectDir, sub));
|
|
63
|
+
}
|
|
64
|
+
for (const candidate of ["App.tsx", "App.jsx"]) {
|
|
65
|
+
out.push(join(projectDir, candidate));
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function buildProject(filePaths) {
|
|
70
|
+
const project = new Project({
|
|
71
|
+
useInMemoryFileSystem: false,
|
|
72
|
+
skipAddingFilesFromTsConfig: true,
|
|
73
|
+
skipFileDependencyResolution: true,
|
|
74
|
+
skipLoadingLibFiles: true,
|
|
75
|
+
compilerOptions: { allowJs: true, jsx: 1 },
|
|
76
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
77
|
+
});
|
|
78
|
+
for (const path of filePaths) {
|
|
79
|
+
try {
|
|
80
|
+
project.addSourceFileAtPath(path);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return project;
|
|
86
|
+
}
|
|
87
|
+
function listOpeningElements(source, name) {
|
|
88
|
+
const out = [];
|
|
89
|
+
source.forEachDescendant((node) => {
|
|
90
|
+
if (Node.isJsxOpeningElement(node) || Node.isJsxSelfClosingElement(node)) {
|
|
91
|
+
const tag = node.getTagNameNode();
|
|
92
|
+
if (tag.getText() === name) {
|
|
93
|
+
out.push(node);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
function hasImageKeyAttribute(node) {
|
|
100
|
+
const attrs = node.getAttributes();
|
|
101
|
+
for (const attr of attrs) {
|
|
102
|
+
if (Node.isJsxAttribute(attr)) {
|
|
103
|
+
const nameNode = attr.getNameNode();
|
|
104
|
+
if (nameNode.getText() === "imageKey")
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
export async function scanCameraViews(projectDir) {
|
|
111
|
+
const candidates = await listCandidateFiles(projectDir);
|
|
112
|
+
const project = buildProject(candidates);
|
|
113
|
+
const edits = [];
|
|
114
|
+
for (const source of project.getSourceFiles()) {
|
|
115
|
+
const tags = listOpeningElements(source, "CameraView");
|
|
116
|
+
if (tags.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
for (const tag of tags) {
|
|
119
|
+
const start = tag.getStart();
|
|
120
|
+
const lineCol = source.getLineAndColumnAtPos(start);
|
|
121
|
+
edits.push({
|
|
122
|
+
filePath: source.getFilePath(),
|
|
123
|
+
relPath: relative(projectDir, source.getFilePath())
|
|
124
|
+
.split(sep)
|
|
125
|
+
.join("/"),
|
|
126
|
+
line: lineCol.line,
|
|
127
|
+
suggestedKey: suggestImageKey(source.getFilePath()),
|
|
128
|
+
alreadyHasImageKey: hasImageKeyAttribute(tag),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
edits.sort((a, b) => a.relPath === b.relPath ? a.line - b.line : a.relPath < b.relPath ? -1 : 1);
|
|
133
|
+
return { edits, filesScanned: project.getSourceFiles().length };
|
|
134
|
+
}
|
|
135
|
+
export async function applyCameraEdits(projectDir, edits, options = {}) {
|
|
136
|
+
const byFile = new Map();
|
|
137
|
+
for (const edit of edits) {
|
|
138
|
+
const list = byFile.get(edit.filePath) ?? [];
|
|
139
|
+
list.push(edit);
|
|
140
|
+
byFile.set(edit.filePath, list);
|
|
141
|
+
}
|
|
142
|
+
const project = buildProject(Array.from(byFile.keys()));
|
|
143
|
+
const filesEdited = [];
|
|
144
|
+
let totalReplacements = 0;
|
|
145
|
+
for (const [filePath, fileEdits] of byFile) {
|
|
146
|
+
const source = project.getSourceFile(filePath);
|
|
147
|
+
if (!source)
|
|
148
|
+
continue;
|
|
149
|
+
let replacementsThisFile = 0;
|
|
150
|
+
for (const edit of fileEdits) {
|
|
151
|
+
const tags = listOpeningElements(source, "CameraView");
|
|
152
|
+
const target = tags.find((t) => {
|
|
153
|
+
const pos = source.getLineAndColumnAtPos(t.getStart());
|
|
154
|
+
return pos.line === edit.line;
|
|
155
|
+
});
|
|
156
|
+
if (!target)
|
|
157
|
+
continue;
|
|
158
|
+
target.getTagNameNode().replaceWithText("CeraphCamera");
|
|
159
|
+
if (Node.isJsxOpeningElement(target)) {
|
|
160
|
+
const parent = target.getParentIfKind(SyntaxKind.JsxElement);
|
|
161
|
+
if (parent) {
|
|
162
|
+
const closing = parent.getClosingElement();
|
|
163
|
+
closing.getTagNameNode().replaceWithText("CeraphCamera");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!edit.alreadyHasImageKey && !options.omitImageKey) {
|
|
167
|
+
target.addAttribute({
|
|
168
|
+
name: "imageKey",
|
|
169
|
+
initializer: `"${edit.suggestedKey}"`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
replacementsThisFile++;
|
|
173
|
+
totalReplacements++;
|
|
174
|
+
}
|
|
175
|
+
const existingShimImport = source.getImportDeclaration(SHIM_IMPORT_MODULE);
|
|
176
|
+
if (existingShimImport) {
|
|
177
|
+
const named = existingShimImport.getNamedImports();
|
|
178
|
+
if (!named.some((n) => n.getName() === SHIM_IMPORT_NAME)) {
|
|
179
|
+
existingShimImport.addNamedImport(SHIM_IMPORT_NAME);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
source.addImportDeclaration({
|
|
184
|
+
moduleSpecifier: SHIM_IMPORT_MODULE,
|
|
185
|
+
namedImports: [SHIM_IMPORT_NAME],
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const expoCameraImport = source.getImportDeclaration("expo-camera");
|
|
189
|
+
if (expoCameraImport) {
|
|
190
|
+
const remainingTags = listOpeningElements(source, "CameraView");
|
|
191
|
+
if (remainingTags.length === 0) {
|
|
192
|
+
const named = expoCameraImport.getNamedImports();
|
|
193
|
+
const cameraViewSpec = named.find((n) => n.getName() === "CameraView");
|
|
194
|
+
if (cameraViewSpec) {
|
|
195
|
+
cameraViewSpec.remove();
|
|
196
|
+
const remaining = expoCameraImport.getNamedImports();
|
|
197
|
+
const hasDefault = expoCameraImport.getDefaultImport() != null;
|
|
198
|
+
if (remaining.length === 0 && !hasDefault) {
|
|
199
|
+
expoCameraImport.remove();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (replacementsThisFile > 0) {
|
|
205
|
+
const updated = source.getFullText();
|
|
206
|
+
await writeFile(filePath, updated, "utf-8");
|
|
207
|
+
filesEdited.push(filePath);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { filesEdited, totalReplacements };
|
|
211
|
+
}
|
|
212
|
+
export async function setImageKeyOnExistingTag(filePath, line, imageKey) {
|
|
213
|
+
const project = buildProject([filePath]);
|
|
214
|
+
const source = project.getSourceFile(filePath);
|
|
215
|
+
if (!source) {
|
|
216
|
+
return { applied: false, reason: `File not found: ${filePath}` };
|
|
217
|
+
}
|
|
218
|
+
const tags = listOpeningElements(source, "CeraphCamera");
|
|
219
|
+
const target = tags.find((t) => {
|
|
220
|
+
const pos = source.getLineAndColumnAtPos(t.getStart());
|
|
221
|
+
return pos.line === line;
|
|
222
|
+
});
|
|
223
|
+
if (!target) {
|
|
224
|
+
return {
|
|
225
|
+
applied: false,
|
|
226
|
+
reason: `No <CeraphCamera> at ${filePath}:${line}`,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
let previousValue = null;
|
|
230
|
+
let existingFound = false;
|
|
231
|
+
for (const attr of target.getAttributes()) {
|
|
232
|
+
if (Node.isJsxAttribute(attr)) {
|
|
233
|
+
const nameNode = attr.getNameNode();
|
|
234
|
+
if (nameNode.getText() === "imageKey") {
|
|
235
|
+
existingFound = true;
|
|
236
|
+
const init = attr.getInitializer();
|
|
237
|
+
if (init && Node.isStringLiteral(init)) {
|
|
238
|
+
previousValue = init.getLiteralText();
|
|
239
|
+
}
|
|
240
|
+
attr.setInitializer(`"${imageKey}"`);
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!existingFound) {
|
|
246
|
+
target.addAttribute({
|
|
247
|
+
name: "imageKey",
|
|
248
|
+
initializer: `"${imageKey}"`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (previousValue === imageKey) {
|
|
252
|
+
return { applied: false, previousValue, reason: "imageKey unchanged" };
|
|
253
|
+
}
|
|
254
|
+
await writeFile(filePath, source.getFullText(), "utf-8");
|
|
255
|
+
return { applied: true, previousValue };
|
|
256
|
+
}
|
|
257
|
+
export async function snapshotFile(filePath) {
|
|
258
|
+
try {
|
|
259
|
+
return await readFile(filePath, "utf-8");
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
export async function restoreFile(filePath, snapshot) {
|
|
266
|
+
await writeFile(filePath, snapshot, "utf-8");
|
|
267
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type RootComponentTarget = {
|
|
2
|
+
kind: "expo-router";
|
|
3
|
+
filePath: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: "bare-rn";
|
|
6
|
+
filePath: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function detectRootComponent(projectDir: string): Promise<RootComponentTarget | null>;
|
|
9
|
+
export interface InjectInstallCeraphResult {
|
|
10
|
+
applied: boolean;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function injectInstallCeraph(target: RootComponentTarget): Promise<InjectInstallCeraphResult>;
|
|
14
|
+
export declare function snapshotLayout(target: RootComponentTarget): Promise<string | null>;
|
|
15
|
+
export declare function restoreLayout(target: RootComponentTarget, snapshot: string): Promise<void>;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { IndentationText, Project, Node, } from "ts-morph";
|
|
4
|
+
const SHIM_MODULE = "@ceraph/react-native-mcp/shim";
|
|
5
|
+
const INSTALL_CALL = "installCeraph";
|
|
6
|
+
async function fileExists(path) {
|
|
7
|
+
try {
|
|
8
|
+
await readFile(path, "utf-8");
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function detectRootComponent(projectDir) {
|
|
16
|
+
const expo = join(projectDir, "app", "_layout.tsx");
|
|
17
|
+
if (await fileExists(expo)) {
|
|
18
|
+
return { kind: "expo-router", filePath: expo };
|
|
19
|
+
}
|
|
20
|
+
const expoJsx = join(projectDir, "app", "_layout.jsx");
|
|
21
|
+
if (await fileExists(expoJsx)) {
|
|
22
|
+
return { kind: "expo-router", filePath: expoJsx };
|
|
23
|
+
}
|
|
24
|
+
for (const name of ["App.tsx", "App.jsx"]) {
|
|
25
|
+
const p = join(projectDir, name);
|
|
26
|
+
if (await fileExists(p)) {
|
|
27
|
+
return { kind: "bare-rn", filePath: p };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function findDefaultExportFunction(source) {
|
|
33
|
+
for (const fn of source.getFunctions()) {
|
|
34
|
+
if (fn.isDefaultExport())
|
|
35
|
+
return fn;
|
|
36
|
+
}
|
|
37
|
+
const exportAssignment = source
|
|
38
|
+
.getStatements()
|
|
39
|
+
.find((s) => Node.isExportAssignment(s));
|
|
40
|
+
if (exportAssignment && Node.isExportAssignment(exportAssignment)) {
|
|
41
|
+
const expr = exportAssignment.getExpression();
|
|
42
|
+
if (Node.isIdentifier(expr)) {
|
|
43
|
+
const symbol = expr.getSymbol();
|
|
44
|
+
if (symbol) {
|
|
45
|
+
const decl = symbol.getDeclarations()[0];
|
|
46
|
+
if (decl && Node.isVariableDeclaration(decl)) {
|
|
47
|
+
const init = decl.getInitializer();
|
|
48
|
+
if (init && Node.isArrowFunction(init))
|
|
49
|
+
return init;
|
|
50
|
+
if (init && Node.isFunctionExpression(init))
|
|
51
|
+
return init;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (Node.isArrowFunction(expr))
|
|
56
|
+
return expr;
|
|
57
|
+
if (Node.isFunctionExpression(expr))
|
|
58
|
+
return expr;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
function ensureUseEffectImport(source) {
|
|
63
|
+
const reactImport = source.getImportDeclaration("react");
|
|
64
|
+
if (!reactImport) {
|
|
65
|
+
source.addImportDeclaration({
|
|
66
|
+
moduleSpecifier: "react",
|
|
67
|
+
namedImports: ["useEffect"],
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const named = reactImport.getNamedImports();
|
|
72
|
+
if (!named.some((n) => n.getName() === "useEffect")) {
|
|
73
|
+
reactImport.addNamedImport("useEffect");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function ensureShimImport(source) {
|
|
77
|
+
const existing = source.getImportDeclaration(SHIM_MODULE);
|
|
78
|
+
if (!existing) {
|
|
79
|
+
source.addImportDeclaration({
|
|
80
|
+
moduleSpecifier: SHIM_MODULE,
|
|
81
|
+
namedImports: [INSTALL_CALL],
|
|
82
|
+
});
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const named = existing.getNamedImports();
|
|
86
|
+
if (!named.some((n) => n.getName() === INSTALL_CALL)) {
|
|
87
|
+
existing.addNamedImport(INSTALL_CALL);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function bodyAlreadyHasInstallCeraph(fn) {
|
|
91
|
+
const body = fn.getBody();
|
|
92
|
+
if (!body)
|
|
93
|
+
return false;
|
|
94
|
+
if (!Node.isBlock(body))
|
|
95
|
+
return false;
|
|
96
|
+
const text = body.getText();
|
|
97
|
+
return text.includes(`${INSTALL_CALL}(`);
|
|
98
|
+
}
|
|
99
|
+
function insertInstallCeraphHook(fn) {
|
|
100
|
+
if (bodyAlreadyHasInstallCeraph(fn))
|
|
101
|
+
return false;
|
|
102
|
+
if (Node.isArrowFunction(fn)) {
|
|
103
|
+
const body = fn.getBody();
|
|
104
|
+
if (!Node.isBlock(body)) {
|
|
105
|
+
const expressionText = body.getText();
|
|
106
|
+
body.replaceWithText(`{ return ${expressionText}; }`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const block = fn.getBody();
|
|
110
|
+
if (!block || !Node.isBlock(block))
|
|
111
|
+
return false;
|
|
112
|
+
block.insertStatements(0, "useEffect(() => { installCeraph(); }, []);");
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
export async function injectInstallCeraph(target) {
|
|
116
|
+
const project = new Project({
|
|
117
|
+
useInMemoryFileSystem: false,
|
|
118
|
+
skipAddingFilesFromTsConfig: true,
|
|
119
|
+
skipFileDependencyResolution: true,
|
|
120
|
+
skipLoadingLibFiles: true,
|
|
121
|
+
compilerOptions: { allowJs: true, jsx: 1 },
|
|
122
|
+
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
123
|
+
});
|
|
124
|
+
const source = project.addSourceFileAtPath(target.filePath);
|
|
125
|
+
const fn = findDefaultExportFunction(source);
|
|
126
|
+
if (!fn) {
|
|
127
|
+
return {
|
|
128
|
+
applied: false,
|
|
129
|
+
reason: "Could not locate the default-exported function component. " +
|
|
130
|
+
`Manually add the following near the top of your root component in ${target.filePath}:\n` +
|
|
131
|
+
` import { installCeraph } from "${SHIM_MODULE}";\n` +
|
|
132
|
+
` useEffect(() => { installCeraph(); }, []);`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (bodyAlreadyHasInstallCeraph(fn)) {
|
|
136
|
+
ensureShimImport(source);
|
|
137
|
+
ensureUseEffectImport(source);
|
|
138
|
+
const updated = source.getFullText();
|
|
139
|
+
await writeFile(target.filePath, updated, "utf-8");
|
|
140
|
+
return { applied: true, reason: "Already wired — imports checked." };
|
|
141
|
+
}
|
|
142
|
+
ensureShimImport(source);
|
|
143
|
+
ensureUseEffectImport(source);
|
|
144
|
+
const inserted = insertInstallCeraphHook(fn);
|
|
145
|
+
if (!inserted) {
|
|
146
|
+
return {
|
|
147
|
+
applied: false,
|
|
148
|
+
reason: "Default export is not a block-bodied function. " +
|
|
149
|
+
"Manually add `useEffect(() => { installCeraph(); }, [])` at the top " +
|
|
150
|
+
"of the component body.",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
const updated = source.getFullText();
|
|
154
|
+
await writeFile(target.filePath, updated, "utf-8");
|
|
155
|
+
return { applied: true };
|
|
156
|
+
}
|
|
157
|
+
export async function snapshotLayout(target) {
|
|
158
|
+
try {
|
|
159
|
+
return await readFile(target.filePath, "utf-8");
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export async function restoreLayout(target, snapshot) {
|
|
166
|
+
await writeFile(target.filePath, snapshot, "utf-8");
|
|
167
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const HOOK_SCRIPT = "#!/bin/bash\n# rn-error-notify.sh \u2014 Injected by @ceraph/react-native-mcp init\n# Reads .rn-errors.json and injects runtime errors into Claude's context.\n\nERROR_FILE=\"$CLAUDE_PROJECT_DIR/mobile/.rn-errors.json\"\n\n# Also check project root if mobile/ doesn't exist\nif [ ! -f \"$ERROR_FILE\" ]; then\n ERROR_FILE=\"$CLAUDE_PROJECT_DIR/.rn-errors.json\"\nfi\n\nif [ ! -f \"$ERROR_FILE\" ]; then\n exit 0\nfi\n\nERROR_COUNT=$(jq -r '.errors | length' \"$ERROR_FILE\" 2>/dev/null)\n\nif [ \"$ERROR_COUNT\" = \"0\" ] || [ -z \"$ERROR_COUNT\" ]; then\n exit 0\nfi\n\necho \"REACT NATIVE RUNTIME ERROR DETECTED:\"\necho \"\"\njq -r '.errors[] | \"Error: \\(.message)\\nStack: \\(.stack)\\nTime: \\(.timestamp)\\n---\"' \"$ERROR_FILE\" 2>/dev/null\necho \"\"\necho \"Use rn_get_errors for full details. Fix the error and rebuild.\"\n";
|
|
2
|
+
export declare const FLOW_PROGRESS_HOOK_SCRIPT = "#!/bin/bash\n# rn-flow-progress-notify.sh \u2014 Injected by @ceraph/react-native-mcp init\n# Reads .rn-flow-progress.json and injects a one-line heartbeat into\n# Claude's context. HEARTBEAT ONLY \u2014 no step-level failure details.\n\nENTRY_FILE=\"$CLAUDE_PROJECT_DIR/mobile/.rn-flow-progress.json\"\nif [ ! -f \"$ENTRY_FILE\" ]; then\n ENTRY_FILE=\"$CLAUDE_PROJECT_DIR/.rn-flow-progress.json\"\nfi\nif [ ! -f \"$ENTRY_FILE\" ]; then\n exit 0\nfi\n\n# If jq isn't installed, exit quietly \u2014 the heartbeat is best-effort.\nif ! command -v jq >/dev/null 2>&1; then\n exit 0\nfi\n\nENTRY=$(jq -r '.entry' \"$ENTRY_FILE\" 2>/dev/null)\nif [ \"$ENTRY\" = \"null\" ] || [ -z \"$ENTRY\" ]; then\n exit 0\nfi\n\n# jq '// \"\"' (works in jq 1.4+; \"// empty\" needed 1.5+ and would have\n# silently aborted older shells) makes jq emit an empty string for\n# missing fields rather than the literal \"null\". Defaults below keep\n# set -e shells happy in the arithmetic + awk expansions even when\n# the entry is partial (transient state if the file is read\n# mid-write).\nFLOW=$(jq -r '.entry.flow // \"\"' \"$ENTRY_FILE\" 2>/dev/null)\nINDEX=$(jq -r '.entry.index // \"\"' \"$ENTRY_FILE\" 2>/dev/null)\nTOTAL=$(jq -r '.entry.total // \"\"' \"$ENTRY_FILE\" 2>/dev/null)\nSUCCESS=$(jq -r '.entry.success // \"\"' \"$ENTRY_FILE\" 2>/dev/null)\nDURATION=$(jq -r '.entry.durationMs // \"\"' \"$ENTRY_FILE\" 2>/dev/null)\n\nINDEX=${INDEX:-0}\nTOTAL=${TOTAL:-0}\nDURATION=${DURATION:-0}\nFLOW=${FLOW:-unknown}\n\nDISPLAY_INDEX=$((INDEX + 1))\nif [ \"$SUCCESS\" = \"true\" ]; then\n STATUS=\"passed\"\nelse\n STATUS=\"failed\"\nfi\n\necho \"Flow $DISPLAY_INDEX/$TOTAL \u2014 $FLOW: $STATUS in $(awk \"BEGIN {printf \\\"%.1f\\\", $DURATION/1000}\")s\"\necho \"(Wait for the end-of-run summary before making any code changes \u2014 the flows in this run reference your pre-edit code.)\"\n";
|
|
3
|
+
export declare const ERROR_HOOK_REL_PATH: readonly [".claude", "hooks", "rn-error-notify.sh"];
|
|
4
|
+
export declare const FLOW_PROGRESS_HOOK_REL_PATH: readonly [".claude", "hooks", "rn-flow-progress-notify.sh"];
|
|
5
|
+
export declare const ERROR_HOOK_MATCHER = ".rn-errors.json";
|
|
6
|
+
export declare const FLOW_PROGRESS_HOOK_MATCHER = ".rn-flow-progress.json";
|
|
7
|
+
export declare const ERROR_HOOK_COMMAND = "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rn-error-notify.sh";
|
|
8
|
+
export declare const FLOW_PROGRESS_HOOK_COMMAND = "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rn-flow-progress-notify.sh";
|
|
9
|
+
export declare const CLAUDE_SETTINGS_REL_PATHS: readonly [readonly [".claude", "settings.json"], readonly [".claude", "settings.local.json"]];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export const HOOK_SCRIPT = `#!/bin/bash
|
|
2
|
+
# rn-error-notify.sh — Injected by @ceraph/react-native-mcp init
|
|
3
|
+
# Reads .rn-errors.json and injects runtime errors into Claude's context.
|
|
4
|
+
|
|
5
|
+
ERROR_FILE="\$CLAUDE_PROJECT_DIR/mobile/.rn-errors.json"
|
|
6
|
+
|
|
7
|
+
# Also check project root if mobile/ doesn't exist
|
|
8
|
+
if [ ! -f "\$ERROR_FILE" ]; then
|
|
9
|
+
ERROR_FILE="\$CLAUDE_PROJECT_DIR/.rn-errors.json"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [ ! -f "\$ERROR_FILE" ]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
ERROR_COUNT=\$(jq -r '.errors | length' "\$ERROR_FILE" 2>/dev/null)
|
|
17
|
+
|
|
18
|
+
if [ "\$ERROR_COUNT" = "0" ] || [ -z "\$ERROR_COUNT" ]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo "REACT NATIVE RUNTIME ERROR DETECTED:"
|
|
23
|
+
echo ""
|
|
24
|
+
jq -r '.errors[] | "Error: \\(.message)\\nStack: \\(.stack)\\nTime: \\(.timestamp)\\n---"' "\$ERROR_FILE" 2>/dev/null
|
|
25
|
+
echo ""
|
|
26
|
+
echo "Use rn_get_errors for full details. Fix the error and rebuild."
|
|
27
|
+
`;
|
|
28
|
+
export const FLOW_PROGRESS_HOOK_SCRIPT = `#!/bin/bash
|
|
29
|
+
# rn-flow-progress-notify.sh — Injected by @ceraph/react-native-mcp init
|
|
30
|
+
# Reads .rn-flow-progress.json and injects a one-line heartbeat into
|
|
31
|
+
# Claude's context. HEARTBEAT ONLY — no step-level failure details.
|
|
32
|
+
|
|
33
|
+
ENTRY_FILE="\$CLAUDE_PROJECT_DIR/mobile/.rn-flow-progress.json"
|
|
34
|
+
if [ ! -f "\$ENTRY_FILE" ]; then
|
|
35
|
+
ENTRY_FILE="\$CLAUDE_PROJECT_DIR/.rn-flow-progress.json"
|
|
36
|
+
fi
|
|
37
|
+
if [ ! -f "\$ENTRY_FILE" ]; then
|
|
38
|
+
exit 0
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# If jq isn't installed, exit quietly — the heartbeat is best-effort.
|
|
42
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
ENTRY=\$(jq -r '.entry' "\$ENTRY_FILE" 2>/dev/null)
|
|
47
|
+
if [ "\$ENTRY" = "null" ] || [ -z "\$ENTRY" ]; then
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# jq '// ""' (works in jq 1.4+; "// empty" needed 1.5+ and would have
|
|
52
|
+
# silently aborted older shells) makes jq emit an empty string for
|
|
53
|
+
# missing fields rather than the literal "null". Defaults below keep
|
|
54
|
+
# set -e shells happy in the arithmetic + awk expansions even when
|
|
55
|
+
# the entry is partial (transient state if the file is read
|
|
56
|
+
# mid-write).
|
|
57
|
+
FLOW=\$(jq -r '.entry.flow // ""' "\$ENTRY_FILE" 2>/dev/null)
|
|
58
|
+
INDEX=\$(jq -r '.entry.index // ""' "\$ENTRY_FILE" 2>/dev/null)
|
|
59
|
+
TOTAL=\$(jq -r '.entry.total // ""' "\$ENTRY_FILE" 2>/dev/null)
|
|
60
|
+
SUCCESS=\$(jq -r '.entry.success // ""' "\$ENTRY_FILE" 2>/dev/null)
|
|
61
|
+
DURATION=\$(jq -r '.entry.durationMs // ""' "\$ENTRY_FILE" 2>/dev/null)
|
|
62
|
+
|
|
63
|
+
INDEX=\${INDEX:-0}
|
|
64
|
+
TOTAL=\${TOTAL:-0}
|
|
65
|
+
DURATION=\${DURATION:-0}
|
|
66
|
+
FLOW=\${FLOW:-unknown}
|
|
67
|
+
|
|
68
|
+
DISPLAY_INDEX=\$((INDEX + 1))
|
|
69
|
+
if [ "\$SUCCESS" = "true" ]; then
|
|
70
|
+
STATUS="passed"
|
|
71
|
+
else
|
|
72
|
+
STATUS="failed"
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
echo "Flow \$DISPLAY_INDEX/\$TOTAL — \$FLOW: \$STATUS in \$(awk "BEGIN {printf \\"%.1f\\", \$DURATION/1000}")s"
|
|
76
|
+
echo "(Wait for the end-of-run summary before making any code changes — the flows in this run reference your pre-edit code.)"
|
|
77
|
+
`;
|
|
78
|
+
export const ERROR_HOOK_REL_PATH = [".claude", "hooks", "rn-error-notify.sh"];
|
|
79
|
+
export const FLOW_PROGRESS_HOOK_REL_PATH = [
|
|
80
|
+
".claude",
|
|
81
|
+
"hooks",
|
|
82
|
+
"rn-flow-progress-notify.sh",
|
|
83
|
+
];
|
|
84
|
+
export const ERROR_HOOK_MATCHER = ".rn-errors.json";
|
|
85
|
+
export const FLOW_PROGRESS_HOOK_MATCHER = ".rn-flow-progress.json";
|
|
86
|
+
export const ERROR_HOOK_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/rn-error-notify.sh';
|
|
87
|
+
export const FLOW_PROGRESS_HOOK_COMMAND = '"$CLAUDE_PROJECT_DIR"/.claude/hooks/rn-flow-progress-notify.sh';
|
|
88
|
+
export const CLAUDE_SETTINGS_REL_PATHS = [
|
|
89
|
+
[".claude", "settings.json"],
|
|
90
|
+
[".claude", "settings.local.json"],
|
|
91
|
+
];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type NetworkInterfaceInfo } from "node:os";
|
|
2
|
+
export interface LanIpCandidate {
|
|
3
|
+
interfaceName: string;
|
|
4
|
+
address: string;
|
|
5
|
+
kind: "wifi" | "usb-ethernet" | "other";
|
|
6
|
+
}
|
|
7
|
+
export interface DetectMacLanIpDeps {
|
|
8
|
+
networkInterfaces?: () => Record<string, NetworkInterfaceInfo[] | undefined>;
|
|
9
|
+
}
|
|
10
|
+
export declare function detectMacLanIpCandidates(deps?: DetectMacLanIpDeps): LanIpCandidate[];
|
|
11
|
+
export declare function detectMacLanIp(deps?: DetectMacLanIpDeps): LanIpCandidate | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { networkInterfaces } from "node:os";
|
|
2
|
+
const WIFI_INTERFACE_NAMES = new Set(["en0"]);
|
|
3
|
+
const USB_ETHERNET_INTERFACE_NAMES = new Set(["en5", "en6"]);
|
|
4
|
+
function classifyCandidate(ifName, info) {
|
|
5
|
+
if (info.family !== "IPv4")
|
|
6
|
+
return null;
|
|
7
|
+
if (info.internal)
|
|
8
|
+
return null;
|
|
9
|
+
if (info.address.startsWith("127."))
|
|
10
|
+
return null;
|
|
11
|
+
if (info.address.startsWith("169.254."))
|
|
12
|
+
return null;
|
|
13
|
+
let kind;
|
|
14
|
+
if (WIFI_INTERFACE_NAMES.has(ifName))
|
|
15
|
+
kind = "wifi";
|
|
16
|
+
else if (USB_ETHERNET_INTERFACE_NAMES.has(ifName))
|
|
17
|
+
kind = "usb-ethernet";
|
|
18
|
+
else
|
|
19
|
+
return null;
|
|
20
|
+
return { interfaceName: ifName, address: info.address, kind };
|
|
21
|
+
}
|
|
22
|
+
export function detectMacLanIpCandidates(deps = {}) {
|
|
23
|
+
const reader = deps.networkInterfaces ?? networkInterfaces;
|
|
24
|
+
const ifs = reader();
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const wifi = [];
|
|
27
|
+
const usb = [];
|
|
28
|
+
const names = Object.keys(ifs).sort();
|
|
29
|
+
for (const name of names) {
|
|
30
|
+
const infos = ifs[name];
|
|
31
|
+
if (!infos)
|
|
32
|
+
continue;
|
|
33
|
+
for (const info of infos) {
|
|
34
|
+
const candidate = classifyCandidate(name, info);
|
|
35
|
+
if (!candidate)
|
|
36
|
+
continue;
|
|
37
|
+
if (seen.has(candidate.address))
|
|
38
|
+
continue;
|
|
39
|
+
seen.add(candidate.address);
|
|
40
|
+
if (candidate.kind === "wifi")
|
|
41
|
+
wifi.push(candidate);
|
|
42
|
+
else
|
|
43
|
+
usb.push(candidate);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...wifi, ...usb];
|
|
47
|
+
}
|
|
48
|
+
export function detectMacLanIp(deps = {}) {
|
|
49
|
+
const candidates = detectMacLanIpCandidates(deps);
|
|
50
|
+
return candidates[0] ?? null;
|
|
51
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MonorepoCandidate {
|
|
2
|
+
absPath: string;
|
|
3
|
+
relPath: string;
|
|
4
|
+
signals: ReadonlyArray<"expo" | "react-native" | "app.json">;
|
|
5
|
+
}
|
|
6
|
+
export interface MonorepoDetectionResult {
|
|
7
|
+
kind: "none" | "single" | "multi";
|
|
8
|
+
matches: MonorepoCandidate[];
|
|
9
|
+
rootIsRnApp: boolean;
|
|
10
|
+
isMonorepo: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function readWorkspaceGlobs(projectDir: string): Promise<string[] | null>;
|
|
13
|
+
export declare function detectMonorepoSubpackages(projectDir: string): Promise<MonorepoDetectionResult>;
|