codetraxis 1.0.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/README.md +245 -0
- package/bin/cli.js +225 -0
- package/client/dist/assets/axios-vendor-B_3Om2-t.js +6 -0
- package/client/dist/assets/babel-vendor-B4E1Dfn4.js +818 -0
- package/client/dist/assets/index-BHUsNYUv.css +1 -0
- package/client/dist/assets/index-BMv7qJps.js +193 -0
- package/client/dist/assets/monaco-vendor-Bo5GWDbL.js +11 -0
- package/client/dist/assets/react-vendor-BhKDh-5n.js +49 -0
- package/client/dist/assets/redux-vendor-D-3X9xqH.js +9 -0
- package/client/dist/index.html +18 -0
- package/package.json +68 -0
- package/server/dist/index.js +62 -0
- package/server/dist/routes/agent.route.js +94 -0
- package/server/dist/routes/file.route.js +58 -0
- package/server/dist/routes/tree.route.js +13 -0
- package/server/dist/utils/agent/agentInstaller.js +1027 -0
- package/server/dist/utils/agent/debugHub.js +50 -0
- package/server/dist/utils/file/buildTree.js +79 -0
- package/server/dist/utils/file/gitignoreLoader.js +38 -0
- package/server/dist/utils/git/getGitStatus.js +58 -0
- package/server/dist/utils/watcher/gitignoreWatcher.js +22 -0
- package/server/dist/utils/watcher/watcherService.js +117 -0
|
@@ -0,0 +1,1027 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.detectProjectKind = detectProjectKind;
|
|
40
|
+
exports.buildInstallPlan = buildInstallPlan;
|
|
41
|
+
exports.installAgent = installAgent;
|
|
42
|
+
exports.findEntryFile = findEntryFile;
|
|
43
|
+
exports.removeAgent = removeAgent;
|
|
44
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
45
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
46
|
+
const parser_1 = require("@babel/parser");
|
|
47
|
+
const t = __importStar(require("@babel/types"));
|
|
48
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
49
|
+
// Helpers
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
async function readJson(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(await promises_1.default.readFile(filePath, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async function fileExists(p) {
|
|
60
|
+
try {
|
|
61
|
+
await promises_1.default.access(p);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
69
|
+
// Step 1 — detect project kind
|
|
70
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
async function detectProjectKind(targetDir) {
|
|
72
|
+
const pkg = await readJson(node_path_1.default.join(targetDir, "package.json"));
|
|
73
|
+
if (!pkg)
|
|
74
|
+
return "unknown";
|
|
75
|
+
const deps = {
|
|
76
|
+
...(pkg.dependencies ?? {}),
|
|
77
|
+
...(pkg.devDependencies ?? {}),
|
|
78
|
+
};
|
|
79
|
+
if (deps["next"])
|
|
80
|
+
return "nextjs";
|
|
81
|
+
if (deps["expo"] || deps["@expo/cli"] || deps["expo-router"])
|
|
82
|
+
return "expo";
|
|
83
|
+
if (deps["react-native"] && !deps["expo"])
|
|
84
|
+
return "react-native";
|
|
85
|
+
if (deps["vite"] || deps["@vitejs/plugin-react"] || deps["@vitejs/plugin-react-swc"])
|
|
86
|
+
return "vite-react";
|
|
87
|
+
if (deps["react-scripts"])
|
|
88
|
+
return "cra-react";
|
|
89
|
+
if (deps["webpack"] || deps["@webpack-cli/generators"])
|
|
90
|
+
return "webpack-react";
|
|
91
|
+
if (await fileExists(node_path_1.default.join(targetDir, "index.html")) && !deps["react"])
|
|
92
|
+
return "plain-web";
|
|
93
|
+
if (deps["react"])
|
|
94
|
+
return "webpack-react";
|
|
95
|
+
return "unknown";
|
|
96
|
+
}
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
// Step 2 — semantic bootstrap scan
|
|
99
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
100
|
+
const WEB_BOOTSTRAP_PATTERNS = [
|
|
101
|
+
/createRoot\s*\(/,
|
|
102
|
+
/ReactDOM\.render\s*\(/,
|
|
103
|
+
/hydrateRoot\s*\(/,
|
|
104
|
+
];
|
|
105
|
+
const RN_BOOTSTRAP_PATTERNS = [
|
|
106
|
+
/registerRootComponent\s*\(/,
|
|
107
|
+
/AppRegistry\.registerComponent\s*\(/,
|
|
108
|
+
/AppRegistry\.registerHeadlessTask\s*\(/,
|
|
109
|
+
];
|
|
110
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", ".next", ".expo", "coverage", "__tests__"]);
|
|
111
|
+
async function scanDirForPattern(dir, patterns, maxDepth = 3, _depth = 0) {
|
|
112
|
+
if (_depth > maxDepth)
|
|
113
|
+
return null;
|
|
114
|
+
let entries;
|
|
115
|
+
try {
|
|
116
|
+
entries = await promises_1.default.readdir(dir, { withFileTypes: true });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (SKIP_DIRS.has(entry.name))
|
|
123
|
+
continue;
|
|
124
|
+
const full = node_path_1.default.join(dir, entry.name);
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
const found = await scanDirForPattern(full, patterns, maxDepth, _depth + 1);
|
|
127
|
+
if (found)
|
|
128
|
+
return found;
|
|
129
|
+
}
|
|
130
|
+
else if (/\.(tsx?|jsx?)$/.test(entry.name) && !/\.(test|spec|stories)\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
131
|
+
let source;
|
|
132
|
+
try {
|
|
133
|
+
source = await promises_1.default.readFile(full, "utf-8");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
for (const pattern of patterns) {
|
|
139
|
+
if (pattern.test(source)) {
|
|
140
|
+
return { file: full, reason: `matches ${pattern}`, confidence: 0.9 };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// Step 3 — per-framework adapters
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
150
|
+
async function resolveVitePlan(targetDir) {
|
|
151
|
+
for (const htmlPath of ["index.html", "public/index.html"]) {
|
|
152
|
+
const htmlFile = node_path_1.default.join(targetDir, htmlPath);
|
|
153
|
+
if (!(await fileExists(htmlFile)))
|
|
154
|
+
continue;
|
|
155
|
+
const html = await promises_1.default.readFile(htmlFile, "utf-8");
|
|
156
|
+
const match = html.match(/<script[^>]+type=["']module["'][^>]+src=["']([^"']+)["']/i) ??
|
|
157
|
+
html.match(/<script[^>]+src=["']([^"']+)["'][^>]+type=["']module["']/i);
|
|
158
|
+
if (match) {
|
|
159
|
+
const scriptFile = node_path_1.default.join(targetDir, match[1].replace(/^\//, ""));
|
|
160
|
+
if (await fileExists(scriptFile)) {
|
|
161
|
+
return { strategy: "inject-import", targetFile: scriptFile, confidence: 0.95,
|
|
162
|
+
reason: `Vite: index.html <script type="module" src="${match[1]}">` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// index.html exists but no module script → HTML patch
|
|
166
|
+
return { strategy: "patch-html", htmlFile, scriptSrc: "/codetraxisAgent.js",
|
|
167
|
+
confidence: 0.6, reason: "Vite: index.html found, no module entry detected" };
|
|
168
|
+
}
|
|
169
|
+
// Fallback: semantic scan
|
|
170
|
+
const found = await scanDirForPattern(targetDir, WEB_BOOTSTRAP_PATTERNS);
|
|
171
|
+
if (found)
|
|
172
|
+
return { strategy: "inject-import", targetFile: found.file, confidence: found.confidence,
|
|
173
|
+
reason: `Vite fallback: ${found.reason}` };
|
|
174
|
+
return { strategy: "manual", reason: "Vite project: no entry file found" };
|
|
175
|
+
}
|
|
176
|
+
async function resolveNextjsPlan(targetDir) {
|
|
177
|
+
const candidates = [
|
|
178
|
+
"src/app/layout.tsx", "src/app/layout.jsx",
|
|
179
|
+
"app/layout.tsx", "app/layout.jsx",
|
|
180
|
+
"src/pages/_app.tsx", "src/pages/_app.jsx",
|
|
181
|
+
"pages/_app.tsx", "pages/_app.jsx",
|
|
182
|
+
];
|
|
183
|
+
for (const c of candidates) {
|
|
184
|
+
const full = node_path_1.default.join(targetDir, c);
|
|
185
|
+
if (await fileExists(full)) {
|
|
186
|
+
return { strategy: "inject-import", targetFile: full, confidence: 0.9, reason: `Next.js: found ${c}` };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { strategy: "manual", reason: "Next.js: no _app / layout file found" };
|
|
190
|
+
}
|
|
191
|
+
async function resolveExpoPlan(targetDir) {
|
|
192
|
+
const pkg = await readJson(node_path_1.default.join(targetDir, "package.json"));
|
|
193
|
+
const mainField = pkg?.main && typeof pkg.main === "string" ? pkg.main : null;
|
|
194
|
+
// ── 1. expo-router: "main" === "expo-router/entry" or expo-router dep ────
|
|
195
|
+
// Entry point is the root layout: app/_layout.tsx (or app/_layout.jsx, etc.)
|
|
196
|
+
const isExpoRouter = mainField === "expo-router/entry" ||
|
|
197
|
+
!!pkg?.dependencies?.["expo-router"] ||
|
|
198
|
+
!!pkg?.devDependencies?.["expo-router"];
|
|
199
|
+
if (isExpoRouter) {
|
|
200
|
+
const routerLayouts = [
|
|
201
|
+
"app/_layout.tsx", "app/_layout.jsx", "app/_layout.ts", "app/_layout.js",
|
|
202
|
+
"src/app/_layout.tsx", "src/app/_layout.jsx",
|
|
203
|
+
];
|
|
204
|
+
for (const c of routerLayouts) {
|
|
205
|
+
const full = node_path_1.default.join(targetDir, c);
|
|
206
|
+
if (await fileExists(full)) {
|
|
207
|
+
return { strategy: "inject-import", targetFile: full, confidence: 0.95,
|
|
208
|
+
reason: `Expo Router: found ${c}` };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// expo-router detected but no _layout — fall through to other methods
|
|
212
|
+
}
|
|
213
|
+
// ── 2. package.json "main" — file path (not a node module) ───────────────
|
|
214
|
+
if (mainField && !mainField.includes("/node_modules") && !mainField.startsWith("@") && !mainField.includes("expo-router")) {
|
|
215
|
+
const mainFile = node_path_1.default.join(targetDir, mainField);
|
|
216
|
+
if (await fileExists(mainFile)) {
|
|
217
|
+
return { strategy: "inject-import", targetFile: mainFile, confidence: 0.95,
|
|
218
|
+
reason: `Expo: package.json "main": "${mainField}"` };
|
|
219
|
+
}
|
|
220
|
+
// Try resolving without/with extension
|
|
221
|
+
const base = mainFile.replace(/\.[jt]sx?$/, "");
|
|
222
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
223
|
+
const candidate = base + ext;
|
|
224
|
+
if (await fileExists(candidate)) {
|
|
225
|
+
return { strategy: "inject-import", targetFile: candidate, confidence: 0.9,
|
|
226
|
+
reason: `Expo: package.json "main" resolved to ${node_path_1.default.basename(candidate)}` };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// ── 3. app.json / app.config.js: expo.entryPoint ─────────────────────────
|
|
231
|
+
for (const cfgName of ["app.json", "app.config.json"]) {
|
|
232
|
+
const appJson = await readJson(node_path_1.default.join(targetDir, cfgName));
|
|
233
|
+
if (appJson) {
|
|
234
|
+
const expo = (appJson.expo ?? appJson);
|
|
235
|
+
if (typeof expo.entryPoint === "string") {
|
|
236
|
+
const ep = node_path_1.default.join(targetDir, expo.entryPoint);
|
|
237
|
+
if (await fileExists(ep)) {
|
|
238
|
+
return { strategy: "inject-import", targetFile: ep, confidence: 0.9,
|
|
239
|
+
reason: `Expo: ${cfgName} "entryPoint": "${expo.entryPoint}"` };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
// ── 4. Semantic scan: registerRootComponent / AppRegistry ─────────────────
|
|
245
|
+
const found = await scanDirForPattern(targetDir, RN_BOOTSTRAP_PATTERNS);
|
|
246
|
+
if (found)
|
|
247
|
+
return { strategy: "inject-import", targetFile: found.file, confidence: found.confidence,
|
|
248
|
+
reason: `Expo semantic: ${found.reason}` };
|
|
249
|
+
// ── 5. Well-known root-level entry candidates for bare Expo projects ──────
|
|
250
|
+
const EXPO_ROOT_CANDIDATES = [
|
|
251
|
+
"index.ts", "index.tsx", "index.js", "index.jsx",
|
|
252
|
+
"App.tsx", "App.jsx", "App.ts", "App.js",
|
|
253
|
+
];
|
|
254
|
+
for (const c of EXPO_ROOT_CANDIDATES) {
|
|
255
|
+
const full = node_path_1.default.join(targetDir, c);
|
|
256
|
+
if (await fileExists(full)) {
|
|
257
|
+
return { strategy: "inject-import", targetFile: full, confidence: 0.6,
|
|
258
|
+
reason: `Expo root fallback: found ${c}` };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { strategy: "manual", reason: "Expo: no entry file found. Add import \"./codetraxisAgent\" manually to your root component or index file." };
|
|
262
|
+
}
|
|
263
|
+
async function resolveReactNativePlan(targetDir) {
|
|
264
|
+
const pkg = await readJson(node_path_1.default.join(targetDir, "package.json"));
|
|
265
|
+
const mainField = pkg?.main && typeof pkg.main === "string" ? pkg.main : null;
|
|
266
|
+
if (mainField && !mainField.includes("expo-router")) {
|
|
267
|
+
const mainFile = node_path_1.default.join(targetDir, mainField);
|
|
268
|
+
if (await fileExists(mainFile)) {
|
|
269
|
+
return { strategy: "inject-import", targetFile: mainFile, confidence: 0.95,
|
|
270
|
+
reason: `React Native: package.json "main": "${mainField}"` };
|
|
271
|
+
}
|
|
272
|
+
const base = mainFile.replace(/\.[jt]sx?$/, "");
|
|
273
|
+
for (const ext of [".tsx", ".ts", ".jsx", ".js"]) {
|
|
274
|
+
const candidate = base + ext;
|
|
275
|
+
if (await fileExists(candidate)) {
|
|
276
|
+
return { strategy: "inject-import", targetFile: candidate, confidence: 0.9,
|
|
277
|
+
reason: `React Native: package.json "main" resolved to ${node_path_1.default.basename(candidate)}` };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const found = await scanDirForPattern(targetDir, RN_BOOTSTRAP_PATTERNS);
|
|
282
|
+
if (found)
|
|
283
|
+
return { strategy: "inject-import", targetFile: found.file, confidence: found.confidence,
|
|
284
|
+
reason: `React Native semantic: ${found.reason}` };
|
|
285
|
+
// Root-level candidates
|
|
286
|
+
for (const c of ["index.ts", "index.tsx", "index.js", "index.jsx", "App.tsx", "App.jsx"]) {
|
|
287
|
+
const full = node_path_1.default.join(targetDir, c);
|
|
288
|
+
if (await fileExists(full)) {
|
|
289
|
+
return { strategy: "inject-import", targetFile: full, confidence: 0.6,
|
|
290
|
+
reason: `React Native root fallback: found ${c}` };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return resolveCandidateFallback(targetDir);
|
|
294
|
+
}
|
|
295
|
+
async function resolveCraPlan(targetDir) {
|
|
296
|
+
const found = await scanDirForPattern(targetDir, WEB_BOOTSTRAP_PATTERNS);
|
|
297
|
+
if (found)
|
|
298
|
+
return { strategy: "inject-import", targetFile: found.file, confidence: found.confidence,
|
|
299
|
+
reason: `CRA: ${found.reason}` };
|
|
300
|
+
const htmlFile = node_path_1.default.join(targetDir, "public", "index.html");
|
|
301
|
+
if (await fileExists(htmlFile)) {
|
|
302
|
+
return { strategy: "patch-html", htmlFile, scriptSrc: "/codetraxisAgent.js",
|
|
303
|
+
confidence: 0.5, reason: "CRA: public/index.html" };
|
|
304
|
+
}
|
|
305
|
+
return { strategy: "manual", reason: "CRA: no bootstrap file found" };
|
|
306
|
+
}
|
|
307
|
+
async function resolveGenericReactPlan(targetDir) {
|
|
308
|
+
const found = await scanDirForPattern(targetDir, [...WEB_BOOTSTRAP_PATTERNS, ...RN_BOOTSTRAP_PATTERNS]);
|
|
309
|
+
if (found)
|
|
310
|
+
return { strategy: "inject-import", targetFile: found.file, confidence: found.confidence,
|
|
311
|
+
reason: `Generic React: ${found.reason}` };
|
|
312
|
+
return resolveCandidateFallback(targetDir);
|
|
313
|
+
}
|
|
314
|
+
// Last resort — classic name list
|
|
315
|
+
const ENTRY_CANDIDATES = [
|
|
316
|
+
"src/main.tsx", "src/main.jsx", "src/main.ts", "src/main.js",
|
|
317
|
+
"src/index.tsx", "src/index.jsx", "src/index.ts", "src/index.js",
|
|
318
|
+
"src/App.tsx", "src/App.jsx",
|
|
319
|
+
"main.tsx", "main.jsx", "main.ts", "main.js",
|
|
320
|
+
"index.tsx", "index.jsx", "index.ts", "index.js",
|
|
321
|
+
"App.tsx", "App.jsx",
|
|
322
|
+
];
|
|
323
|
+
async function resolveCandidateFallback(targetDir) {
|
|
324
|
+
for (const candidate of ENTRY_CANDIDATES) {
|
|
325
|
+
const full = node_path_1.default.join(targetDir, candidate);
|
|
326
|
+
if (await fileExists(full)) {
|
|
327
|
+
return { strategy: "inject-import", targetFile: full, confidence: 0.4, reason: `Fallback candidate: ${candidate}` };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { strategy: "manual", reason: "No entry file found by any method" };
|
|
331
|
+
}
|
|
332
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
333
|
+
// Step 4 — build install plan
|
|
334
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
335
|
+
async function buildInstallPlan(targetDir) {
|
|
336
|
+
const kind = await detectProjectKind(targetDir);
|
|
337
|
+
let plan;
|
|
338
|
+
switch (kind) {
|
|
339
|
+
case "vite-react":
|
|
340
|
+
plan = await resolveVitePlan(targetDir);
|
|
341
|
+
break;
|
|
342
|
+
case "nextjs":
|
|
343
|
+
plan = await resolveNextjsPlan(targetDir);
|
|
344
|
+
break;
|
|
345
|
+
case "expo":
|
|
346
|
+
plan = await resolveExpoPlan(targetDir);
|
|
347
|
+
break;
|
|
348
|
+
case "react-native":
|
|
349
|
+
plan = await resolveReactNativePlan(targetDir);
|
|
350
|
+
break;
|
|
351
|
+
case "cra-react":
|
|
352
|
+
case "webpack-react":
|
|
353
|
+
plan = await resolveCraPlan(targetDir);
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
plan = await resolveGenericReactPlan(targetDir);
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
return { kind, plan };
|
|
360
|
+
}
|
|
361
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
362
|
+
// Step 5 — AST-based import injection
|
|
363
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
364
|
+
const AGENT_IMPORT_MARKER = "./codetraxisAgent";
|
|
365
|
+
function injectImportAst(source, importPath) {
|
|
366
|
+
let ast;
|
|
367
|
+
try {
|
|
368
|
+
ast = (0, parser_1.parse)(source, {
|
|
369
|
+
sourceType: "module",
|
|
370
|
+
plugins: ["typescript", "jsx", "decorators-legacy", "classProperties"],
|
|
371
|
+
errorRecovery: true,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return injectImportLineBased(source, importPath);
|
|
376
|
+
}
|
|
377
|
+
const importLine = `import "${importPath}"; // codetraxis agent\n`;
|
|
378
|
+
// Find end position of the last ImportDeclaration
|
|
379
|
+
let lastImportEnd = -1;
|
|
380
|
+
for (const node of ast.program.body) {
|
|
381
|
+
if (t.isImportDeclaration(node) && node.end != null) {
|
|
382
|
+
lastImportEnd = node.end;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (lastImportEnd < 0)
|
|
386
|
+
return importLine + source;
|
|
387
|
+
return source.slice(0, lastImportEnd) + "\n" + importLine + source.slice(lastImportEnd);
|
|
388
|
+
}
|
|
389
|
+
function injectImportLineBased(source, importPath) {
|
|
390
|
+
const lines = source.split("\n");
|
|
391
|
+
let lastImportIdx = -1;
|
|
392
|
+
for (let i = 0; i < lines.length; i++) {
|
|
393
|
+
if (/^\s*(import\s|export\s+\*|\/\/\s*@ts-nocheck)/.test(lines[i])
|
|
394
|
+
|| /^\s*const\s+\w+\s*=\s*require\(/.test(lines[i])) {
|
|
395
|
+
lastImportIdx = i;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const importLine = `import "${importPath}"; // codetraxis agent`;
|
|
399
|
+
if (lastImportIdx >= 0) {
|
|
400
|
+
lines.splice(lastImportIdx + 1, 0, importLine);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
lines.unshift(importLine);
|
|
404
|
+
}
|
|
405
|
+
return lines.join("\n");
|
|
406
|
+
}
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
408
|
+
// Step 6 — apply plan (main export)
|
|
409
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
410
|
+
async function installAgent(targetDir) {
|
|
411
|
+
try {
|
|
412
|
+
const { kind, plan } = await buildInstallPlan(targetDir);
|
|
413
|
+
if (plan.strategy === "manual") {
|
|
414
|
+
return { success: false, alreadyInstalled: false, projectKind: kind, strategy: "manual", error: plan.reason };
|
|
415
|
+
}
|
|
416
|
+
if (plan.strategy === "patch-html") {
|
|
417
|
+
return {
|
|
418
|
+
success: false, alreadyInstalled: false, projectKind: kind, strategy: "patch-html",
|
|
419
|
+
error: `HTML patching not yet supported. Add <script src="${plan.scriptSrc}"> manually to ${plan.htmlFile}`,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
// strategy === "inject-import"
|
|
423
|
+
const targetFile = plan.targetFile;
|
|
424
|
+
// Place the agent file at the project root (or src/ if entry is under src/).
|
|
425
|
+
// Never place it inside app/ (expo-router would try to route it).
|
|
426
|
+
const relToRoot = node_path_1.default.relative(targetDir, node_path_1.default.dirname(targetFile));
|
|
427
|
+
const agentDir = relToRoot.startsWith("app") || relToRoot.startsWith("src/app")
|
|
428
|
+
// expo-router: entry is in app/ — put agent at project root instead
|
|
429
|
+
? targetDir
|
|
430
|
+
: node_path_1.default.dirname(targetFile);
|
|
431
|
+
const agentFile = node_path_1.default.join(agentDir, "codetraxisAgent", "index.ts");
|
|
432
|
+
// Import path from the entry file to the agent folder's index
|
|
433
|
+
let agentImportPath = AGENT_IMPORT_MARKER; // default: "./codetraxisAgent"
|
|
434
|
+
if (agentDir !== node_path_1.default.dirname(targetFile)) {
|
|
435
|
+
// entry is in app/, agent is at root → relative path from entry to root
|
|
436
|
+
const rel = node_path_1.default.relative(node_path_1.default.dirname(targetFile), agentDir);
|
|
437
|
+
agentImportPath = (rel.startsWith(".") ? rel : "./" + rel) + "/codetraxisAgent";
|
|
438
|
+
}
|
|
439
|
+
const entrySource = await promises_1.default.readFile(targetFile, "utf-8");
|
|
440
|
+
const alreadyInstalled = entrySource.includes("codetraxisAgent");
|
|
441
|
+
// Always overwrite the agent folder so the latest version is always present.
|
|
442
|
+
await writeAgentFiles(agentDir, process.env.PORT ?? "3333", kind);
|
|
443
|
+
// Only inject the import if it's not there yet.
|
|
444
|
+
if (!alreadyInstalled) {
|
|
445
|
+
await promises_1.default.writeFile(targetFile, injectImportAst(entrySource, agentImportPath), "utf-8");
|
|
446
|
+
}
|
|
447
|
+
return { success: true, alreadyInstalled, entryFile: targetFile, agentFile, projectKind: kind, strategy: plan.strategy };
|
|
448
|
+
}
|
|
449
|
+
catch (err) {
|
|
450
|
+
return { success: false, alreadyInstalled: false, error: err instanceof Error ? err.message : String(err) };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
454
|
+
// Public helper used by status route
|
|
455
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
456
|
+
async function findEntryFile(targetDir) {
|
|
457
|
+
const { plan } = await buildInstallPlan(targetDir);
|
|
458
|
+
return plan.strategy === "inject-import" ? plan.targetFile : null;
|
|
459
|
+
}
|
|
460
|
+
async function removeAgent(targetDir) {
|
|
461
|
+
try {
|
|
462
|
+
const entryFile = await findEntryFile(targetDir);
|
|
463
|
+
if (!entryFile) {
|
|
464
|
+
return { success: false, error: "Entry file not found" };
|
|
465
|
+
}
|
|
466
|
+
const source = await promises_1.default.readFile(entryFile, "utf-8");
|
|
467
|
+
if (!source.includes("codetraxisAgent")) {
|
|
468
|
+
return { success: false, error: "Agent import not found in entry file" };
|
|
469
|
+
}
|
|
470
|
+
// Удаляем строку импорта агента
|
|
471
|
+
const cleaned = source
|
|
472
|
+
.split("\n")
|
|
473
|
+
.filter(line => !line.includes("codetraxisAgent"))
|
|
474
|
+
.join("\n");
|
|
475
|
+
await promises_1.default.writeFile(entryFile, cleaned, "utf-8");
|
|
476
|
+
// Ищем папку агента (может быть в той же папке что entry, или в корне проекта)
|
|
477
|
+
const agentDirCandidates = [
|
|
478
|
+
node_path_1.default.join(node_path_1.default.dirname(entryFile), "codetraxisAgent"),
|
|
479
|
+
node_path_1.default.join(targetDir, "codetraxisAgent"),
|
|
480
|
+
];
|
|
481
|
+
let deletedAgentFile;
|
|
482
|
+
for (const candidate of agentDirCandidates) {
|
|
483
|
+
if (await fileExists(candidate)) {
|
|
484
|
+
await promises_1.default.rm(candidate, { recursive: true, force: true });
|
|
485
|
+
deletedAgentFile = candidate;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// Обратная совместимость: старый монолитный файл codetraxisAgent.ts
|
|
490
|
+
if (!deletedAgentFile) {
|
|
491
|
+
const legacyCandidates = [
|
|
492
|
+
node_path_1.default.join(node_path_1.default.dirname(entryFile), "codetraxisAgent.ts"),
|
|
493
|
+
node_path_1.default.join(targetDir, "codetraxisAgent.ts"),
|
|
494
|
+
];
|
|
495
|
+
for (const candidate of legacyCandidates) {
|
|
496
|
+
if (await fileExists(candidate)) {
|
|
497
|
+
await promises_1.default.unlink(candidate);
|
|
498
|
+
deletedAgentFile = candidate;
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return { success: true, entryFile, agentFile: deletedAgentFile };
|
|
504
|
+
}
|
|
505
|
+
catch (err) {
|
|
506
|
+
return { success: false, error: err instanceof Error ? err.message : String(err) };
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
510
|
+
// Agent source — modular architecture
|
|
511
|
+
//
|
|
512
|
+
// Generates a codetraxisAgent/ folder with:
|
|
513
|
+
// shared.ts — bridge (WS transport + helpers)
|
|
514
|
+
// interceptors/consoleInterceptor.ts
|
|
515
|
+
// interceptors/fetchInterceptor.ts
|
|
516
|
+
// interceptors/xhrInterceptor.ts — for React Native & raw XHR users
|
|
517
|
+
// index.ts — wires everything together
|
|
518
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
519
|
+
async function writeAgentFiles(agentParentDir, serverPort, kind) {
|
|
520
|
+
const agentDir = node_path_1.default.join(agentParentDir, "codetraxisAgent");
|
|
521
|
+
const interceptorsDir = node_path_1.default.join(agentDir, "interceptors");
|
|
522
|
+
await promises_1.default.mkdir(interceptorsDir, { recursive: true });
|
|
523
|
+
await promises_1.default.writeFile(node_path_1.default.join(agentDir, "shared.ts"), buildSharedSource(serverPort), "utf-8");
|
|
524
|
+
await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "consoleInterceptor.ts"), buildConsoleInterceptorSource(), "utf-8");
|
|
525
|
+
await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "fetchInterceptor.ts"), buildFetchInterceptorSource(), "utf-8");
|
|
526
|
+
await promises_1.default.writeFile(node_path_1.default.join(interceptorsDir, "xhrInterceptor.ts"), buildXhrInterceptorSource(), "utf-8");
|
|
527
|
+
await promises_1.default.writeFile(node_path_1.default.join(agentDir, "index.ts"), buildAgentIndexSource(kind), "utf-8");
|
|
528
|
+
}
|
|
529
|
+
// ─── shared.ts ────────────────────────────────────────────────────────────────
|
|
530
|
+
function buildSharedSource(serverPort) {
|
|
531
|
+
return `/**
|
|
532
|
+
* codetraxis agent — shared bridge (auto-generated, do not edit).
|
|
533
|
+
*/
|
|
534
|
+
|
|
535
|
+
export type TreeViewerEvent = Record<string, unknown>;
|
|
536
|
+
|
|
537
|
+
export interface TreeViewerBridge {
|
|
538
|
+
send: (event: TreeViewerEvent) => void;
|
|
539
|
+
uid: () => string;
|
|
540
|
+
safeSerialize: (value: unknown, depth?: number) => unknown;
|
|
541
|
+
truncate: (value: string, max?: number) => string;
|
|
542
|
+
isWeb: boolean;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
export function createTreeViewerBridge(serverPort: string): TreeViewerBridge {
|
|
546
|
+
const g = globalThis as Record<string, any>;
|
|
547
|
+
|
|
548
|
+
const isWeb: boolean =
|
|
549
|
+
typeof g.document !== "undefined" &&
|
|
550
|
+
typeof g.addEventListener === "function";
|
|
551
|
+
|
|
552
|
+
let ws: WebSocket | null = null;
|
|
553
|
+
let wsReady = false;
|
|
554
|
+
const queue: string[] = [];
|
|
555
|
+
|
|
556
|
+
const getHost = (): string => {
|
|
557
|
+
if (typeof g.__TREE_VIEWER_HOST__ === "string" && g.__TREE_VIEWER_HOST__) {
|
|
558
|
+
return g.__TREE_VIEWER_HOST__ as string;
|
|
559
|
+
}
|
|
560
|
+
if (isWeb && g.location?.hostname) return g.location.hostname as string;
|
|
561
|
+
return "localhost";
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const connect = () => {
|
|
565
|
+
try {
|
|
566
|
+
const WS =
|
|
567
|
+
typeof WebSocket !== "undefined"
|
|
568
|
+
? WebSocket
|
|
569
|
+
: (g.WebSocket as typeof WebSocket | undefined);
|
|
570
|
+
if (!WS) return;
|
|
571
|
+
|
|
572
|
+
ws = new WS(\`ws://\${getHost()}:${serverPort}/agent\`);
|
|
573
|
+
ws.onopen = () => {
|
|
574
|
+
wsReady = true;
|
|
575
|
+
while (queue.length > 0) {
|
|
576
|
+
const item = queue.shift();
|
|
577
|
+
if (item && ws) ws.send(item);
|
|
578
|
+
}
|
|
579
|
+
};
|
|
580
|
+
ws.onclose = () => { wsReady = false; setTimeout(connect, 3000); };
|
|
581
|
+
ws.onerror = () => { wsReady = false; };
|
|
582
|
+
} catch { /* unavailable */ }
|
|
583
|
+
};
|
|
584
|
+
connect();
|
|
585
|
+
|
|
586
|
+
const truncate = (value: string, max = 500000): string =>
|
|
587
|
+
value.length > max ? \`\${value.slice(0, max)}…[truncated]\` : value;
|
|
588
|
+
|
|
589
|
+
const safeSerialize = (value: unknown, depth = 0): unknown => {
|
|
590
|
+
if (depth > 4) return "[depth limit]";
|
|
591
|
+
if (value == null) return value;
|
|
592
|
+
if (typeof value === "function") return \`[Function: \${(value as Function).name || "anonymous"}]\`;
|
|
593
|
+
if (typeof value === "symbol") return value.toString();
|
|
594
|
+
if (typeof value !== "object") return value;
|
|
595
|
+
if (value instanceof Error) return { __error: true, name: value.name, message: value.message, stack: value.stack };
|
|
596
|
+
if (Array.isArray(value)) return value.slice(0, 1000).map(i => safeSerialize(i, depth + 1));
|
|
597
|
+
|
|
598
|
+
const seen = new WeakSet<object>();
|
|
599
|
+
const walk = (obj: object, d: number): Record<string, unknown> | string => {
|
|
600
|
+
if (seen.has(obj)) return "[circular]";
|
|
601
|
+
seen.add(obj);
|
|
602
|
+
const r: Record<string, unknown> = {};
|
|
603
|
+
let n = 0;
|
|
604
|
+
for (const k in obj as Record<string, unknown>) {
|
|
605
|
+
if (n++ > 500) { r["..."] = "[truncated]"; break; }
|
|
606
|
+
try { r[k] = safeSerialize((obj as Record<string, unknown>)[k], d + 1); }
|
|
607
|
+
catch { r[k] = "[unserializable]"; }
|
|
608
|
+
}
|
|
609
|
+
return r;
|
|
610
|
+
};
|
|
611
|
+
return walk(value as object, depth);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
const uid = () =>
|
|
615
|
+
\`\${Math.random().toString(36).slice(2)}\${Date.now().toString(36)}\`;
|
|
616
|
+
|
|
617
|
+
const send = (event: TreeViewerEvent) => {
|
|
618
|
+
try {
|
|
619
|
+
const payload = JSON.stringify(event);
|
|
620
|
+
if (wsReady && ws) { ws.send(payload); }
|
|
621
|
+
else { queue.push(payload); if (queue.length > 200) queue.shift(); }
|
|
622
|
+
} catch { /* ignore */ }
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
return { send, uid, safeSerialize, truncate, isWeb };
|
|
626
|
+
}
|
|
627
|
+
`;
|
|
628
|
+
}
|
|
629
|
+
// ─── consoleInterceptor.ts ────────────────────────────────────────────────────
|
|
630
|
+
function buildConsoleInterceptorSource() {
|
|
631
|
+
return `/**
|
|
632
|
+
* codetraxis agent — console interceptor (auto-generated, do not edit).
|
|
633
|
+
*/
|
|
634
|
+
import type { TreeViewerBridge } from "../shared";
|
|
635
|
+
|
|
636
|
+
const INSTALLED_KEY = "__tv_console_installed__";
|
|
637
|
+
|
|
638
|
+
export function setupConsoleInterceptor(bridge: TreeViewerBridge): void {
|
|
639
|
+
const g = globalThis as Record<string, any>;
|
|
640
|
+
if (g[INSTALLED_KEY]) return;
|
|
641
|
+
g[INSTALLED_KEY] = true;
|
|
642
|
+
|
|
643
|
+
const original = {
|
|
644
|
+
log: console.log.bind(console),
|
|
645
|
+
info: console.info.bind(console),
|
|
646
|
+
warn: console.warn.bind(console),
|
|
647
|
+
error: console.error.bind(console),
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
(["log", "info", "warn", "error"] as const).forEach(level => {
|
|
651
|
+
// @ts-ignore
|
|
652
|
+
console[level] = (...args: unknown[]) => {
|
|
653
|
+
original[level](...args);
|
|
654
|
+
bridge.send({
|
|
655
|
+
id: bridge.uid(),
|
|
656
|
+
type: "console",
|
|
657
|
+
level,
|
|
658
|
+
args: args.map(a => bridge.safeSerialize(a)),
|
|
659
|
+
timestamp: Date.now(),
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
// ─── fetchInterceptor.ts ──────────────────────────────────────────────────────
|
|
667
|
+
function buildFetchInterceptorSource() {
|
|
668
|
+
return `/**
|
|
669
|
+
* codetraxis agent — fetch interceptor (auto-generated, do not edit).
|
|
670
|
+
*/
|
|
671
|
+
import type { TreeViewerBridge } from "../shared";
|
|
672
|
+
|
|
673
|
+
const INSTALLED_KEY = "__tv_fetch_installed__";
|
|
674
|
+
|
|
675
|
+
function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string> | undefined {
|
|
676
|
+
if (!headers) return undefined;
|
|
677
|
+
const result: Record<string, string> = {};
|
|
678
|
+
try {
|
|
679
|
+
if (headers instanceof Headers) {
|
|
680
|
+
headers.forEach((v, k) => { result[k] = v; });
|
|
681
|
+
} else if (Array.isArray(headers)) {
|
|
682
|
+
(headers as [string, string][]).forEach(([k, v]) => { result[k] = v; });
|
|
683
|
+
} else {
|
|
684
|
+
Object.assign(result, headers as Record<string, string>);
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
} catch { return undefined; }
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
export function setupFetchInterceptor(bridge: TreeViewerBridge): void {
|
|
691
|
+
const g = globalThis as Record<string, any>;
|
|
692
|
+
if (g[INSTALLED_KEY]) return;
|
|
693
|
+
g[INSTALLED_KEY] = true;
|
|
694
|
+
|
|
695
|
+
const originalFetch =
|
|
696
|
+
typeof globalThis.fetch === "function"
|
|
697
|
+
? globalThis.fetch.bind(globalThis)
|
|
698
|
+
: null;
|
|
699
|
+
if (!originalFetch) return;
|
|
700
|
+
|
|
701
|
+
globalThis.fetch = async function tvFetch(
|
|
702
|
+
input: RequestInfo | URL,
|
|
703
|
+
init?: RequestInit,
|
|
704
|
+
): Promise<Response> {
|
|
705
|
+
const url =
|
|
706
|
+
typeof input === "string" ? input
|
|
707
|
+
: input instanceof URL ? input.href
|
|
708
|
+
: (input as Request).url;
|
|
709
|
+
|
|
710
|
+
const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
|
|
711
|
+
const id = bridge.uid();
|
|
712
|
+
const start = Date.now();
|
|
713
|
+
|
|
714
|
+
const requestHeaders = normalizeHeaders(
|
|
715
|
+
init?.headers ?? (input instanceof Request ? input.headers : undefined),
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
let requestBody: string | undefined;
|
|
719
|
+
const rawBody = init?.body;
|
|
720
|
+
if (rawBody != null) {
|
|
721
|
+
try {
|
|
722
|
+
requestBody =
|
|
723
|
+
typeof rawBody === "string" ? bridge.truncate(rawBody)
|
|
724
|
+
: rawBody instanceof URLSearchParams ? bridge.truncate(rawBody.toString())
|
|
725
|
+
: "[binary]";
|
|
726
|
+
} catch { requestBody = "[unserializable-body]"; }
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
bridge.send({ id, type: "network", transport: "fetch", method, url,
|
|
730
|
+
requestHeaders, requestBody, state: "pending", timestamp: start });
|
|
731
|
+
|
|
732
|
+
try {
|
|
733
|
+
const response = await originalFetch(input, init);
|
|
734
|
+
|
|
735
|
+
let responseHeaders: Record<string, string> | undefined;
|
|
736
|
+
try {
|
|
737
|
+
responseHeaders = {};
|
|
738
|
+
response.headers.forEach((v, k) => { responseHeaders![k] = v; });
|
|
739
|
+
} catch { /* ignore */ }
|
|
740
|
+
|
|
741
|
+
let responseBody: string | undefined;
|
|
742
|
+
try { responseBody = bridge.truncate(await response.clone().text()); }
|
|
743
|
+
catch { responseBody = "[binary]"; }
|
|
744
|
+
|
|
745
|
+
bridge.send({ id, type: "network", transport: "fetch", method, url,
|
|
746
|
+
status: response.status, requestHeaders, requestBody,
|
|
747
|
+
responseHeaders, responseBody,
|
|
748
|
+
state: response.ok ? "success" : "error",
|
|
749
|
+
duration: Date.now() - start, timestamp: start });
|
|
750
|
+
|
|
751
|
+
return response;
|
|
752
|
+
} catch (error) {
|
|
753
|
+
bridge.send({ id, type: "network", transport: "fetch", method, url,
|
|
754
|
+
requestHeaders, requestBody, state: "error",
|
|
755
|
+
duration: Date.now() - start, timestamp: start,
|
|
756
|
+
error: bridge.safeSerialize(error) });
|
|
757
|
+
throw error;
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
`;
|
|
762
|
+
}
|
|
763
|
+
// ─── xhrInterceptor.ts ────────────────────────────────────────────────────────
|
|
764
|
+
function buildXhrInterceptorSource() {
|
|
765
|
+
return `/**
|
|
766
|
+
* codetraxis agent — XHR interceptor (auto-generated, do not edit).
|
|
767
|
+
* Patches XMLHttpRequest.prototype so existing references (e.g. axios
|
|
768
|
+
* internals captured at import time) are also intercepted.
|
|
769
|
+
* Works in browser and React Native (which ships its own XHR polyfill).
|
|
770
|
+
*/
|
|
771
|
+
import type { TreeViewerBridge } from "../shared";
|
|
772
|
+
|
|
773
|
+
const INSTALLED_KEY = "__tv_xhr_installed__";
|
|
774
|
+
|
|
775
|
+
export function setupXhrInterceptor(bridge: TreeViewerBridge): void {
|
|
776
|
+
if (typeof XMLHttpRequest === "undefined") return;
|
|
777
|
+
const g = globalThis as Record<string, any>;
|
|
778
|
+
if (g[INSTALLED_KEY]) return;
|
|
779
|
+
g[INSTALLED_KEY] = true;
|
|
780
|
+
|
|
781
|
+
try {
|
|
782
|
+
const proto = XMLHttpRequest.prototype;
|
|
783
|
+
|
|
784
|
+
const _origOpen = proto.open;
|
|
785
|
+
proto.open = function(this: XMLHttpRequest, method: string, url: string, ...rest: unknown[]) {
|
|
786
|
+
(this as any).__tv_method = method.toUpperCase();
|
|
787
|
+
(this as any).__tv_url = String(url);
|
|
788
|
+
(this as any).__tv_reqHeaders = undefined;
|
|
789
|
+
return (_origOpen as Function).apply(this, [method, url, ...rest]);
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const _origSetHeader = proto.setRequestHeader;
|
|
793
|
+
proto.setRequestHeader = function(this: XMLHttpRequest, name: string, value: string) {
|
|
794
|
+
if (!(this as any).__tv_reqHeaders) (this as any).__tv_reqHeaders = {} as Record<string, string>;
|
|
795
|
+
(this as any).__tv_reqHeaders[name] = value;
|
|
796
|
+
return _origSetHeader.apply(this, [name, value]);
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
const _origSend = proto.send;
|
|
800
|
+
proto.send = function(this: XMLHttpRequest, body?: Document | XMLHttpRequestBodyInit | null) {
|
|
801
|
+
const id = bridge.uid();
|
|
802
|
+
const method = (this as any).__tv_method ?? "GET";
|
|
803
|
+
const url = (this as any).__tv_url ?? "";
|
|
804
|
+
const reqHeaders = (this as any).__tv_reqHeaders as Record<string, string> | undefined;
|
|
805
|
+
const start = Date.now();
|
|
806
|
+
|
|
807
|
+
let requestBody: string | undefined;
|
|
808
|
+
if (body != null) {
|
|
809
|
+
try {
|
|
810
|
+
requestBody =
|
|
811
|
+
typeof body === "string" ? bridge.truncate(body)
|
|
812
|
+
: body instanceof URLSearchParams ? bridge.truncate(body.toString())
|
|
813
|
+
: "[binary]";
|
|
814
|
+
} catch { /* ignore */ }
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
bridge.send({ id, type: "network", transport: "xhr", method, url,
|
|
818
|
+
requestBody, requestHeaders: reqHeaders, state: "pending", timestamp: start });
|
|
819
|
+
|
|
820
|
+
this.addEventListener("load", () => {
|
|
821
|
+
let responseBody: string | undefined;
|
|
822
|
+
try { responseBody = bridge.truncate(this.responseText); } catch { /* binary */ }
|
|
823
|
+
|
|
824
|
+
let responseHeaders: Record<string, string> | undefined;
|
|
825
|
+
try {
|
|
826
|
+
const raw = this.getAllResponseHeaders();
|
|
827
|
+
if (raw) {
|
|
828
|
+
responseHeaders = {};
|
|
829
|
+
raw.trim().split(/\\r?\\n/).forEach(line => {
|
|
830
|
+
const idx = line.indexOf(": ");
|
|
831
|
+
if (idx > 0) responseHeaders![line.slice(0, idx).toLowerCase()] = line.slice(idx + 2);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
} catch { /* ignore */ }
|
|
835
|
+
|
|
836
|
+
bridge.send({ id, type: "network", transport: "xhr", method, url,
|
|
837
|
+
status: this.status, requestBody, requestHeaders: reqHeaders,
|
|
838
|
+
responseBody, responseHeaders,
|
|
839
|
+
state: this.status >= 200 && this.status < 400 ? "success" : "error",
|
|
840
|
+
duration: Date.now() - start, timestamp: start });
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
this.addEventListener("error", () => {
|
|
844
|
+
bridge.send({ id, type: "network", transport: "xhr", method, url,
|
|
845
|
+
requestBody, requestHeaders: reqHeaders,
|
|
846
|
+
state: "error", duration: Date.now() - start, timestamp: start });
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
return _origSend.apply(this, [body]);
|
|
850
|
+
};
|
|
851
|
+
} catch { /* not available */ }
|
|
852
|
+
}
|
|
853
|
+
`;
|
|
854
|
+
}
|
|
855
|
+
// ─── index.ts ─────────────────────────────────────────────────────────────────
|
|
856
|
+
function buildAgentIndexSource(kind) {
|
|
857
|
+
const isRn = kind === "expo" || kind === "react-native";
|
|
858
|
+
return `/**
|
|
859
|
+
* codetraxis debug agent — entry point (auto-generated, do not edit).
|
|
860
|
+
*/
|
|
861
|
+
import { createTreeViewerBridge } from "./shared";
|
|
862
|
+
import { setupConsoleInterceptor } from "./interceptors/consoleInterceptor";
|
|
863
|
+
import { setupFetchInterceptor } from "./interceptors/fetchInterceptor";
|
|
864
|
+
import { setupXhrInterceptor } from "./interceptors/xhrInterceptor";
|
|
865
|
+
|
|
866
|
+
export const treeViewerBridge = createTreeViewerBridge("__PORT__");
|
|
867
|
+
|
|
868
|
+
setupConsoleInterceptor(treeViewerBridge);
|
|
869
|
+
setupFetchInterceptor(treeViewerBridge);
|
|
870
|
+
${isRn ? "setupXhrInterceptor(treeViewerBridge); // React Native uses XHR under the hood" : "setupXhrInterceptor(treeViewerBridge);"}
|
|
871
|
+
|
|
872
|
+
// ─── Auto-attach default axios instance ──────────────────────────────────────
|
|
873
|
+
// If the project uses axios, we attach interceptors to the default instance.
|
|
874
|
+
// For axios.create() instances, call attachAxios(instance) manually.
|
|
875
|
+
try {
|
|
876
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
877
|
+
const axiosModule = require("axios");
|
|
878
|
+
const axiosInstance = axiosModule?.default ?? axiosModule;
|
|
879
|
+
if (axiosInstance?.interceptors) {
|
|
880
|
+
attachAxios(axiosInstance);
|
|
881
|
+
}
|
|
882
|
+
} catch { /* axios not installed — skip */ }
|
|
883
|
+
|
|
884
|
+
// ─── attachAxios — for axios.create() instances ───────────────────────────────
|
|
885
|
+
// Usage: import { attachAxios } from "./codetraxisAgent";
|
|
886
|
+
// attachAxios(myAxiosInstance);
|
|
887
|
+
export function attachAxios(instance: any): void {
|
|
888
|
+
if (!instance?.interceptors) return;
|
|
889
|
+
|
|
890
|
+
const INSTALLED_KEY = "__tv_axios_installed__";
|
|
891
|
+
if (instance[INSTALLED_KEY]) return;
|
|
892
|
+
instance[INSTALLED_KEY] = true;
|
|
893
|
+
|
|
894
|
+
const REQ_ID_KEY = "__tv_req_id__";
|
|
895
|
+
const REQ_START_KEY = "__tv_req_start__";
|
|
896
|
+
|
|
897
|
+
const joinUrl = (base?: string, url?: string) => {
|
|
898
|
+
if (!base) return url || "";
|
|
899
|
+
if (!url) return base;
|
|
900
|
+
try { return new URL(url, base).toString(); } catch { return \`\${base}\${url}\`; }
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const normalizeHeaders = (h: unknown): Record<string, string> | undefined => {
|
|
904
|
+
if (!h) return undefined;
|
|
905
|
+
try {
|
|
906
|
+
if (typeof (h as any).toJSON === "function") return (h as any).toJSON() as Record<string, string>;
|
|
907
|
+
return { ...(h as Record<string, string>) };
|
|
908
|
+
} catch { return undefined; }
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
instance.interceptors.request.use(
|
|
912
|
+
(config: any) => {
|
|
913
|
+
const id = treeViewerBridge.uid();
|
|
914
|
+
const start = Date.now();
|
|
915
|
+
config[REQ_ID_KEY] = id;
|
|
916
|
+
config[REQ_START_KEY] = start;
|
|
917
|
+
treeViewerBridge.send({
|
|
918
|
+
id, type: "network", transport: "axios",
|
|
919
|
+
method: (config.method || "get").toUpperCase(),
|
|
920
|
+
url: joinUrl(config.baseURL, config.url),
|
|
921
|
+
requestHeaders: normalizeHeaders(config.headers),
|
|
922
|
+
requestBody: treeViewerBridge.safeSerialize(config.data),
|
|
923
|
+
state: "pending", timestamp: start,
|
|
924
|
+
});
|
|
925
|
+
return config;
|
|
926
|
+
},
|
|
927
|
+
(error: any) => {
|
|
928
|
+
treeViewerBridge.send({
|
|
929
|
+
id: treeViewerBridge.uid(), type: "network", transport: "axios",
|
|
930
|
+
state: "error", timestamp: Date.now(),
|
|
931
|
+
error: treeViewerBridge.safeSerialize(error),
|
|
932
|
+
});
|
|
933
|
+
return Promise.reject(error);
|
|
934
|
+
},
|
|
935
|
+
);
|
|
936
|
+
|
|
937
|
+
instance.interceptors.response.use(
|
|
938
|
+
(response: any) => {
|
|
939
|
+
const id = response.config[REQ_ID_KEY] || treeViewerBridge.uid();
|
|
940
|
+
const start = response.config[REQ_START_KEY] || Date.now();
|
|
941
|
+
treeViewerBridge.send({
|
|
942
|
+
id, type: "network", transport: "axios",
|
|
943
|
+
method: (response.config.method || "get").toUpperCase(),
|
|
944
|
+
url: joinUrl(response.config.baseURL, response.config.url),
|
|
945
|
+
status: response.status,
|
|
946
|
+
requestHeaders: normalizeHeaders(response.config.headers),
|
|
947
|
+
requestBody: treeViewerBridge.safeSerialize(response.config.data),
|
|
948
|
+
responseHeaders: normalizeHeaders(response.headers),
|
|
949
|
+
responseBody: treeViewerBridge.safeSerialize(response.data),
|
|
950
|
+
state: "success", duration: Date.now() - start, timestamp: start,
|
|
951
|
+
});
|
|
952
|
+
return response;
|
|
953
|
+
},
|
|
954
|
+
(error: any) => {
|
|
955
|
+
const cfg = error?.config || {};
|
|
956
|
+
const id = cfg[REQ_ID_KEY] || treeViewerBridge.uid();
|
|
957
|
+
const start = cfg[REQ_START_KEY] || Date.now();
|
|
958
|
+
treeViewerBridge.send({
|
|
959
|
+
id, type: "network", transport: "axios",
|
|
960
|
+
method: (cfg.method || "get").toUpperCase(),
|
|
961
|
+
url: joinUrl(cfg.baseURL, cfg.url),
|
|
962
|
+
status: error?.response?.status,
|
|
963
|
+
requestHeaders: normalizeHeaders(cfg.headers),
|
|
964
|
+
requestBody: treeViewerBridge.safeSerialize(cfg.data),
|
|
965
|
+
responseHeaders: normalizeHeaders(error?.response?.headers),
|
|
966
|
+
responseBody: treeViewerBridge.safeSerialize(error?.response?.data),
|
|
967
|
+
state: "error", duration: Date.now() - start, timestamp: start,
|
|
968
|
+
error: treeViewerBridge.safeSerialize({ message: error?.message, code: error?.code, name: error?.name }),
|
|
969
|
+
});
|
|
970
|
+
return Promise.reject(error);
|
|
971
|
+
},
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// ─── attachSocketIO — for socket.io-client instances ─────────────────────────
|
|
976
|
+
// React Native uses socket.io which doesn't go through globalThis.WebSocket.
|
|
977
|
+
// Usage: import { attachSocketIO } from "./codetraxisAgent";
|
|
978
|
+
// attachSocketIO(socket); // call after getChatSocket() or io()
|
|
979
|
+
export function attachSocketIO(socket: any): void {
|
|
980
|
+
if (!socket) return;
|
|
981
|
+
|
|
982
|
+
const INSTALLED_KEY = "__tv_sio_installed__";
|
|
983
|
+
if (socket[INSTALLED_KEY]) return;
|
|
984
|
+
socket[INSTALLED_KEY] = true;
|
|
985
|
+
|
|
986
|
+
const url: string = socket.io?.uri ?? socket.nsp ?? "socket.io";
|
|
987
|
+
|
|
988
|
+
// ── Incoming events ──────────────────────────────────────────────────────
|
|
989
|
+
socket.onAny((event: string, ...args: unknown[]) => {
|
|
990
|
+
treeViewerBridge.send({
|
|
991
|
+
id: treeViewerBridge.uid(),
|
|
992
|
+
type: "network",
|
|
993
|
+
transport: "websocket",
|
|
994
|
+
url,
|
|
995
|
+
method: "MESSAGE",
|
|
996
|
+
responseBody: treeViewerBridge.truncate(
|
|
997
|
+
JSON.stringify({ event, data: args.length === 1 ? args[0] : args }),
|
|
998
|
+
),
|
|
999
|
+
state: "success",
|
|
1000
|
+
timestamp: Date.now(),
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
// ── Outgoing events ──────────────────────────────────────────────────────
|
|
1005
|
+
const origEmit = socket.emit.bind(socket);
|
|
1006
|
+
socket.emit = (event: string, ...args: unknown[]) => {
|
|
1007
|
+
if (!["ping", "pong"].includes(event)) {
|
|
1008
|
+
treeViewerBridge.send({
|
|
1009
|
+
id: treeViewerBridge.uid(),
|
|
1010
|
+
type: "network",
|
|
1011
|
+
transport: "websocket",
|
|
1012
|
+
url,
|
|
1013
|
+
method: "SEND",
|
|
1014
|
+
requestBody: treeViewerBridge.truncate(
|
|
1015
|
+
JSON.stringify({ event, data: args[0] }),
|
|
1016
|
+
),
|
|
1017
|
+
state: "success",
|
|
1018
|
+
timestamp: Date.now(),
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
return origEmit(event, ...args);
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
export {};
|
|
1026
|
+
`.replace("__PORT__", "${process.env.PORT ?? '3333'}");
|
|
1027
|
+
}
|