@nivustec/proteus 1.0.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.
@@ -0,0 +1,157 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "fs";
2
+ import { execSync } from "child_process";
3
+ import { glob } from "glob";
4
+ import { parseAndInject } from "./parser.js";
5
+ export async function injectTestIds(config, specificFiles) {
6
+ const stats = {
7
+ filesProcessed: 0,
8
+ totalInjected: 0,
9
+ errors: 0,
10
+ errorMessages: [],
11
+ };
12
+ let filesToProcess = [];
13
+ // If we have specific files, process ONLY them (ignore include/exclude)
14
+ if (specificFiles && specificFiles.length > 0) {
15
+ filesToProcess = specificFiles.filter((file) => {
16
+ const exists = existsSync(file);
17
+ return exists;
18
+ });
19
+ }
20
+ else {
21
+ // If there are no specific files, use include/exclude
22
+ for (const pattern of config.include || []) {
23
+ const matches = await glob(pattern, {
24
+ ignore: config.exclude,
25
+ nodir: true,
26
+ });
27
+ filesToProcess.push(...matches);
28
+ }
29
+ filesToProcess = [...new Set(filesToProcess)];
30
+ }
31
+ for (const filePath of filesToProcess) {
32
+ const code = readFileSync(filePath, "utf-8");
33
+ if (config.verbose) {
34
+ console.log("🌊 Proteus is transforming your components...");
35
+ }
36
+ const result = parseAndInject(filePath, code, config);
37
+ if (result.error) {
38
+ stats.errors++;
39
+ stats.errorMessages.push(`Error processing ${filePath}: ${result.error.message}`);
40
+ }
41
+ if (result.injectedCount > 0) {
42
+ writeFileSync(filePath, result.code);
43
+ stats.totalInjected += result.injectedCount;
44
+ }
45
+ stats.filesProcessed++;
46
+ }
47
+ return stats;
48
+ }
49
+ export async function injectStagedFiles() {
50
+ const stats = {
51
+ filesProcessed: 0,
52
+ totalInjected: 0,
53
+ errors: 0,
54
+ errorMessages: [],
55
+ };
56
+ try {
57
+ const stagedBackup = execSync("git diff --cached --name-only", {
58
+ encoding: "utf8",
59
+ })
60
+ .split("\n")
61
+ .filter(Boolean);
62
+ execSync("git reset --mixed", { stdio: "pipe" });
63
+ const filesToProcess = stagedBackup.filter((file) => file.match(/\.(jsx|tsx)$/) &&
64
+ !file.includes("node_modules") &&
65
+ existsSync(file));
66
+ const cfg = loadConfig(undefined, true);
67
+ cfg.verbose = false;
68
+ if (cfg.verbose) {
69
+ console.log(`🔧 Files to process: ${filesToProcess.length} files`);
70
+ }
71
+ if (filesToProcess.length === 0) {
72
+ if (stagedBackup.length > 0) {
73
+ execSync(`git add ${stagedBackup.join(" ")}`, { stdio: "pipe" });
74
+ }
75
+ if (cfg.verbose) {
76
+ console.log("💡 No React files in commit");
77
+ }
78
+ return stats;
79
+ }
80
+ const config = cfg;
81
+ for (const filePath of filesToProcess) {
82
+ const code = readFileSync(filePath, "utf-8");
83
+ const result = parseAndInject(filePath, code, config);
84
+ if (result.error) {
85
+ stats.errors++;
86
+ stats.errorMessages.push(`Error processing ${filePath}: ${result.error.message}`);
87
+ }
88
+ if (result.injectedCount > 0) {
89
+ writeFileSync(filePath, result.code);
90
+ if (config.verbose) {
91
+ console.log("🌊 Proteus is transforming your components...");
92
+ console.log(`✅ Injected ${result.injectedCount} test IDs in ${filePath}`);
93
+ }
94
+ stats.totalInjected += result.injectedCount;
95
+ }
96
+ stats.filesProcessed++;
97
+ }
98
+ // 4. Re-stage all files (including modifications)
99
+ if (stagedBackup.length > 0) {
100
+ execSync(`git add ${stagedBackup.join(" ")}`, { stdio: "pipe" });
101
+ }
102
+ if (config.verbose) {
103
+ if (stats.totalInjected > 0) {
104
+ console.log(`🎉 Added ${stats.totalInjected} test IDs to the commit`);
105
+ console.log("💡 Review the changes with 'git diff --cached' before committing");
106
+ }
107
+ else {
108
+ console.log("✅ All files already have data-testid");
109
+ }
110
+ }
111
+ return stats;
112
+ }
113
+ catch (error) {
114
+ stats.errors++;
115
+ stats.errorMessages.push(error instanceof Error ? error.message : String(error));
116
+ return stats;
117
+ }
118
+ }
119
+ export function setupGitHooks() {
120
+ const packageJsonPath = "./package.json";
121
+ const huskyDir = "./.husky";
122
+ if (existsSync(huskyDir)) {
123
+ try {
124
+ const preCommitPath = `${huskyDir}/pre-commit`;
125
+ const script = `#!/usr/bin/env sh\n. "$(dirname "$0")/_/husky.sh"\nproteus pre-commit\n`;
126
+ writeFileSync(preCommitPath, script, { encoding: "utf8" });
127
+ console.log("✓ Husky pre-commit configured at .husky/pre-commit");
128
+ return;
129
+ }
130
+ catch (error) {
131
+ console.error("Error configuring Husky pre-commit:", error);
132
+ }
133
+ }
134
+ if (!existsSync(packageJsonPath)) {
135
+ console.warn("Warning: package.json not found, cannot setup Git hooks");
136
+ return;
137
+ }
138
+ try {
139
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
140
+ if (!packageJson.scripts) {
141
+ packageJson.scripts = {};
142
+ }
143
+ packageJson.scripts["proteus:pre-commit"] = "proteus pre-commit";
144
+ if (!packageJson.husky)
145
+ packageJson.husky = {};
146
+ if (!packageJson.husky.hooks)
147
+ packageJson.husky.hooks = {};
148
+ packageJson.husky.hooks["pre-commit"] = "npm run proteus:pre-commit";
149
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
150
+ console.log("✓ Legacy Git hooks configured in package.json");
151
+ console.log("💡 Added pre-commit hook for automatic data-testid injection");
152
+ }
153
+ catch (error) {
154
+ console.error("Error setting up Git hooks:", error);
155
+ }
156
+ }
157
+ import { loadConfig } from "./config.js";
@@ -0,0 +1,8 @@
1
+ import { ProteuConfig } from "./types.js";
2
+ interface InjectionResult {
3
+ code: string;
4
+ injectedCount: number;
5
+ error?: Error;
6
+ }
7
+ export declare function parseAndInject(filePath: string, code: string, config: ProteuConfig): InjectionResult;
8
+ export {};
package/dist/parser.js ADDED
@@ -0,0 +1,402 @@
1
+ import { parse } from "@babel/parser";
2
+ import _traverse from "@babel/traverse";
3
+ import * as t from "@babel/types";
4
+ import { generateTestId, shortStableHash } from "./utils.js";
5
+ // Normalize ESM/CJS interop from @babel/traverse
6
+ const traverse = _traverse.default;
7
+ // Check if we're inside a forwardRef function
8
+ function isInsideForwardRef(path) {
9
+ let current = path;
10
+ while (current) {
11
+ // Check if parent is a CallExpression with forwardRef
12
+ if (current.isCallExpression() &&
13
+ current.node.callee &&
14
+ ((current.node.callee.type === "Identifier" && current.node.callee.name === "forwardRef") ||
15
+ (current.node.callee.type === "MemberExpression" &&
16
+ current.node.callee.property &&
17
+ current.node.callee.property.name === "forwardRef"))) {
18
+ return true;
19
+ }
20
+ current = current.parentPath;
21
+ }
22
+ return false;
23
+ }
24
+ // Check if element has ref={ref} attribute (forwarded ref)
25
+ function hasForwardedRef(node) {
26
+ return node.attributes.some((attr) => t.isJSXAttribute(attr) &&
27
+ t.isJSXIdentifier(attr.name) &&
28
+ attr.name.name === "ref" &&
29
+ attr.value &&
30
+ t.isJSXExpressionContainer(attr.value) &&
31
+ t.isIdentifier(attr.value.expression) &&
32
+ attr.value.expression.name === "ref");
33
+ }
34
+ // Check if element has {...props} spread
35
+ function hasPropsSpread(node) {
36
+ return node.attributes.some((attr) => t.isJSXSpreadAttribute(attr) &&
37
+ t.isIdentifier(attr.argument) &&
38
+ attr.argument.name === "props");
39
+ }
40
+ // Parse the source, traverse JSX, and inject data-testid attributes
41
+ export function parseAndInject(filePath, code, config) {
42
+ let injectedCount = 0;
43
+ try {
44
+ const ast = parse(code, {
45
+ sourceType: "module",
46
+ plugins: ["jsx", "typescript"],
47
+ tokens: true,
48
+ });
49
+ const lines = code.split("\n");
50
+ let hasModifications = false;
51
+ let elementsFound = 0;
52
+ traverse(ast, {
53
+ JSXOpeningElement(path) {
54
+ elementsFound++;
55
+ const node = path.node;
56
+ if (!node.loc)
57
+ return;
58
+ const lineNumber = node.loc.start.line;
59
+ const elementName = getElementName(node);
60
+ if (!elementName || isExcludedElement(elementName))
61
+ return;
62
+ const hasTestId = node.attributes.some((attr) => t.isJSXAttribute(attr) &&
63
+ t.isJSXIdentifier(attr.name) &&
64
+ attr.name.name === "data-testid");
65
+ if (hasTestId)
66
+ return;
67
+ // SKIP if this element is a reusable component wrapper:
68
+ // - Inside forwardRef
69
+ // - Has ref={ref}
70
+ // - Has {...props}
71
+ if (config.detectReusableComponents) {
72
+ const insideForwardRef = isInsideForwardRef(path);
73
+ const hasRef = hasForwardedRef(node);
74
+ const hasSpread = hasPropsSpread(node);
75
+ if (insideForwardRef && hasRef && hasSpread) {
76
+ if (config.verbose) {
77
+ console.log(`⏭️ Skipping reusable component wrapper at ${filePath}:${lineNumber}`);
78
+ }
79
+ return; // Skip this element, but continue processing others
80
+ }
81
+ }
82
+ const mapInfo = getMapInfo(path);
83
+ const isDynamic = isInsideMapFunction(path);
84
+ const descriptor = getFunctionalDescriptor(node, path);
85
+ const componentPath = getComponentPath(path);
86
+ const classNameHint = getStaticClassName(node);
87
+ const siblingPos = getSiblingPosition(path, elementName);
88
+ const elementInfo = {
89
+ elementName,
90
+ fileName: filePath,
91
+ lineNumber,
92
+ key: mapInfo.key,
93
+ index: mapInfo.index,
94
+ isDynamic,
95
+ descriptor,
96
+ componentPath,
97
+ };
98
+ const built = buildAttributeForElement(config, elementInfo, mapInfo, classNameHint, siblingPos);
99
+ // Insert just before the closing token of the opening tag (supports multi-line and self-closing)
100
+ const endLineIndex = node.loc.end.line - 1;
101
+ const endCol = node.loc.end.column;
102
+ if (endLineIndex >= lines.length)
103
+ return;
104
+ const endLine = lines[endLineIndex];
105
+ const isSelf = node.selfClosing === true;
106
+ const insertCol = isSelf ? Math.max(0, endCol - 2) : Math.max(0, endCol - 1);
107
+ lines[endLineIndex] =
108
+ endLine.slice(0, insertCol) +
109
+ ` ${built.attributeText}` +
110
+ endLine.slice(insertCol);
111
+ hasModifications = true;
112
+ injectedCount++;
113
+ },
114
+ });
115
+ // Quiet by default; injector controls logging verbosity
116
+ return {
117
+ code: hasModifications ? lines.join("\n") : code,
118
+ injectedCount,
119
+ };
120
+ }
121
+ catch (error) {
122
+ return { code, injectedCount: 0, error: error };
123
+ }
124
+ }
125
+ // Helpers
126
+ function getElementName(node) {
127
+ if (t.isJSXIdentifier(node.name)) {
128
+ return node.name.name;
129
+ }
130
+ else if (t.isJSXMemberExpression(node.name)) {
131
+ let name = "";
132
+ let current = node.name;
133
+ while (t.isJSXMemberExpression(current)) {
134
+ name =
135
+ "." +
136
+ (t.isJSXIdentifier(current.property) ? current.property.name : "") +
137
+ name;
138
+ current = current.object;
139
+ }
140
+ if (t.isJSXIdentifier(current)) {
141
+ name = current.name + name;
142
+ }
143
+ return name;
144
+ }
145
+ return "";
146
+ }
147
+ function isExcludedElement(elementName) {
148
+ // Self-closing, non-semantic tags we never instrument
149
+ const excluded = ["br", "hr", "meta", "link", "script", "style"];
150
+ return excluded.includes(elementName.toLowerCase());
151
+ }
152
+ function isInsideMapFunction(path) {
153
+ let current = path.parentPath;
154
+ while (current) {
155
+ if (current.isCallExpression() &&
156
+ current.node.callee.type === "MemberExpression" &&
157
+ current.node.callee.property.type === "Identifier" &&
158
+ current.node.callee.property.name === "map") {
159
+ return true;
160
+ }
161
+ current = current.parentPath;
162
+ }
163
+ return false;
164
+ }
165
+ // Returns map key/index info for elements within Array.prototype.map
166
+ function getMapInfo(path) {
167
+ let current = path.parentPath;
168
+ while (current) {
169
+ if (current.isCallExpression() &&
170
+ current.node.callee.type === "MemberExpression" &&
171
+ current.node.callee.property.type === "Identifier" &&
172
+ current.node.callee.property.name === "map") {
173
+ // Try current opening element
174
+ if (path.isJSXOpeningElement()) {
175
+ const keyAttr = path.node.attributes.find((attr) => t.isJSXAttribute(attr) &&
176
+ t.isJSXIdentifier(attr.name) &&
177
+ attr.name.name === "key");
178
+ if (keyAttr && t.isJSXAttribute(keyAttr) && keyAttr.value) {
179
+ if (t.isStringLiteral(keyAttr.value)) {
180
+ return { key: keyAttr.value.value };
181
+ }
182
+ else if (t.isJSXExpressionContainer(keyAttr.value)) {
183
+ const expr = keyAttr.value.expression;
184
+ const keyExpr = expressionToText(expr);
185
+ if (keyExpr)
186
+ return { key: keyExpr };
187
+ }
188
+ }
189
+ }
190
+ // Try nearest ancestor JSXElement's openingElement (e.g., wrapper with key)
191
+ let ancestor = path.parentPath;
192
+ while (ancestor) {
193
+ if (ancestor.node && ancestor.node.type === "JSXElement") {
194
+ const opening = ancestor.node.openingElement;
195
+ const keyAttr = opening.attributes.find((attr) => t.isJSXAttribute(attr) &&
196
+ t.isJSXIdentifier(attr.name) &&
197
+ attr.name.name === "key");
198
+ if (keyAttr && keyAttr.value) {
199
+ if (t.isStringLiteral(keyAttr.value)) {
200
+ return { key: keyAttr.value.value };
201
+ }
202
+ if (t.isJSXExpressionContainer(keyAttr.value)) {
203
+ const keyExpr = expressionToText(keyAttr.value.expression);
204
+ if (keyExpr)
205
+ return { key: keyExpr };
206
+ }
207
+ }
208
+ }
209
+ ancestor = ancestor.parentPath;
210
+ }
211
+ break;
212
+ }
213
+ current = current.parentPath;
214
+ }
215
+ return {};
216
+ }
217
+ // Stringify simple identifiers/member-expressions (e.g., item.id)
218
+ function expressionToText(expr) {
219
+ if (!expr)
220
+ return undefined;
221
+ if (expr.type === "Identifier")
222
+ return expr.name;
223
+ if (expr.type === "MemberExpression") {
224
+ const obj = expressionToText(expr.object);
225
+ const prop = expr.property && expr.property.name ? expr.property.name : undefined;
226
+ if (obj && prop)
227
+ return `${obj}.${prop}`;
228
+ }
229
+ return undefined;
230
+ }
231
+ // Derive a semantic descriptor from attributes or children (e.g., placeholder, alt, or text)
232
+ function getFunctionalDescriptor(node, path) {
233
+ // Prefer explicit semantics on common elements
234
+ const attrValue = (name) => {
235
+ const attr = node.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === name);
236
+ if (!attr || !attr.value)
237
+ return undefined;
238
+ if (t.isStringLiteral(attr.value))
239
+ return attr.value.value.toLowerCase();
240
+ return undefined;
241
+ };
242
+ const name = t.isJSXIdentifier(node.name) ? node.name.name.toLowerCase() : "";
243
+ if (name === "input") {
244
+ const placeholder = attrValue("placeholder");
245
+ if (placeholder)
246
+ return `input-${placeholder.replace(/\s+/g, "-")}`;
247
+ const type = attrValue("type");
248
+ if (type)
249
+ return `input-${type}`;
250
+ }
251
+ if (name === "img") {
252
+ const alt = attrValue("alt");
253
+ if (alt)
254
+ return `img-${alt.replace(/\s+/g, "-")}`;
255
+ }
256
+ // Infer from children (text or simple expressions)
257
+ const parentEl = path.parentPath && path.parentPath.parent;
258
+ if (parentEl && parentEl.type === "JSXElement") {
259
+ const children = parentEl.children || [];
260
+ for (const c of children) {
261
+ if (c.type === "JSXText") {
262
+ const text = c.value.trim();
263
+ if (text)
264
+ return text.toLowerCase().replace(/\s+/g, "-");
265
+ }
266
+ if (c.type === "JSXExpressionContainer") {
267
+ const d = inferDescriptorFromExpression(c.expression);
268
+ if (d)
269
+ return d;
270
+ }
271
+ }
272
+ }
273
+ return undefined;
274
+ }
275
+ // Resolve nearest component/function name to build an informative base id
276
+ function getComponentPath(path) {
277
+ const names = [];
278
+ let current = path;
279
+ while (current) {
280
+ if (current.isFunctionDeclaration() || current.isFunctionExpression() || current.isArrowFunctionExpression()) {
281
+ const id = current.node.id;
282
+ if (id && id.name) {
283
+ names.unshift(id.name);
284
+ break;
285
+ }
286
+ }
287
+ if (current.isVariableDeclarator() && current.node.id.type === "Identifier") {
288
+ names.unshift(current.node.id.name);
289
+ break;
290
+ }
291
+ current = current.parentPath;
292
+ }
293
+ return names.length ? names : undefined;
294
+ }
295
+ // Read static className literals to refine role inference
296
+ function getStaticClassName(node) {
297
+ const cls = node.attributes.find((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === "className");
298
+ if (!cls || !cls.value)
299
+ return undefined;
300
+ if (t.isStringLiteral(cls.value))
301
+ return cls.value.value;
302
+ return undefined;
303
+ }
304
+ // If multiple identical sibling tags exist, return this element's position (1-based)
305
+ function getSiblingPosition(path, elementName) {
306
+ const parent = path.parentPath && path.parentPath.parent;
307
+ if (!parent || parent.type !== 'JSXElement')
308
+ return undefined;
309
+ const children = parent.children || [];
310
+ let indices = [];
311
+ for (let i = 0; i < children.length; i++) {
312
+ const c = children[i];
313
+ if (c.type === 'JSXElement') {
314
+ const open = c.openingElement;
315
+ const name = getElementName(open);
316
+ if (name === elementName)
317
+ indices.push(i);
318
+ }
319
+ }
320
+ if (indices.length <= 1)
321
+ return undefined;
322
+ const myIdx = children.findIndex((c) => c === path.parentPath.node);
323
+ const pos = indices.indexOf(myIdx);
324
+ if (pos === -1)
325
+ return undefined;
326
+ return { index: pos, total: indices.length };
327
+ }
328
+ // Extract a concise name from simple expressions inside JSX
329
+ function inferDescriptorFromExpression(expr) {
330
+ if (!expr)
331
+ return undefined;
332
+ if (expr.type === "MemberExpression") {
333
+ const prop = expr.property;
334
+ if (prop && prop.type === "Identifier")
335
+ return prop.name.toLowerCase();
336
+ }
337
+ if (expr.type === "CallExpression") {
338
+ for (const arg of expr.arguments || []) {
339
+ const d = inferDescriptorFromExpression(arg);
340
+ if (d)
341
+ return d;
342
+ }
343
+ }
344
+ return undefined;
345
+ }
346
+ // Map raw tag name to a coarse role
347
+ function mapRole(tag) {
348
+ const name = tag.toLowerCase();
349
+ if (name === "div" || name === "section" || name === "article" || name === "span")
350
+ return "container";
351
+ if (name === "img")
352
+ return "image";
353
+ return name;
354
+ }
355
+ // Build the data-testid attribute text based on strategy, map context, sibling index, and descriptors
356
+ function buildAttributeForElement(config, info, mapInfo, classNameHint, siblingPos) {
357
+ if (config.strategy === "functional") {
358
+ const component = (info.componentPath && info.componentPath[0])
359
+ ? info.componentPath[0].toLowerCase()
360
+ : info.fileName.replace(/\.(tsx|jsx|ts|js)$/, "").toLowerCase();
361
+ let role = mapRole(info.elementName);
362
+ if (classNameHint) {
363
+ if (/\bitem\b/.test(classNameHint))
364
+ role = "item";
365
+ else if (/\bproduct-list\b/.test(classNameHint))
366
+ role = "list";
367
+ else if (/\bheader\b/.test(classNameHint))
368
+ role = "header";
369
+ else if (/\badd-to-cart\b/.test(classNameHint))
370
+ role = "button";
371
+ }
372
+ const descriptor = info.descriptor ? info.descriptor.replace(/\s+/g, "-") : undefined;
373
+ // Add sibling index when multiple identical tags under same parent
374
+ const siblingIndexPart = siblingPos && siblingPos.total > 1 ? String(siblingPos.index + 1) : undefined;
375
+ const base = ["qa_" + component, role, siblingIndexPart, descriptor]
376
+ .filter(Boolean)
377
+ .join("_");
378
+ // In functional strategy, always append map key for elements inside map
379
+ if (info.isDynamic && mapInfo && mapInfo.key) {
380
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/.test(mapInfo.key)) {
381
+ const exprAttr = `data-testid={\`${base}_\${${mapInfo.key}}\`}`;
382
+ return { attributeText: exprAttr, idForRecord: `${base}_${mapInfo.key}` };
383
+ }
384
+ return { attributeText: `data-testid="${base}_${mapInfo.key}"`, idForRecord: `${base}_${mapInfo.key}` };
385
+ }
386
+ if (info.isDynamic && typeof mapInfo.index === "number") {
387
+ return { attributeText: `data-testid="${base}_${mapInfo.index}"`, idForRecord: `${base}_${mapInfo.index}` };
388
+ }
389
+ // Ensure uniqueness for repeated tags without descriptors
390
+ const unique = shortStableHash(`${info.fileName}:${info.lineNumber}:${role}:${descriptor || ''}`);
391
+ return { attributeText: `data-testid="${base}_${unique}"`, idForRecord: `${base}_${unique}` };
392
+ }
393
+ // safe-hash fallback
394
+ const id = generateTestId(config, info);
395
+ if (info.isDynamic && mapInfo && mapInfo.key) {
396
+ if (/^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/.test(mapInfo.key)) {
397
+ return { attributeText: `data-testid={\`${id}_\${${mapInfo.key}}\`}`, idForRecord: `${id}_${mapInfo.key}` };
398
+ }
399
+ return { attributeText: `data-testid="${id}_${mapInfo.key}"`, idForRecord: `${id}_${mapInfo.key}` };
400
+ }
401
+ return { attributeText: `data-testid="${id}"`, idForRecord: id };
402
+ }
@@ -0,0 +1,15 @@
1
+ interface ProteusCLIArgs {
2
+ files?: string[];
3
+ include?: string[];
4
+ exclude?: string[];
5
+ strategy?: "safe-hash" | "functional";
6
+ json_output: boolean;
7
+ }
8
+ /**
9
+ * Executes the Proteus CLI as a child process and returns the parsed JSON output.
10
+ * This function is designed to be universally compatible with any agent.
11
+ * @param args - The arguments to pass to the 'proteus inject' command.
12
+ * @returns A promise that resolves with the JSON output from the CLI.
13
+ */
14
+ export declare function executeProteusCLI(args: ProteusCLIArgs): Promise<any>;
15
+ export {};
@@ -0,0 +1,52 @@
1
+ import { spawn } from "child_process";
2
+ /**
3
+ * Executes the Proteus CLI as a child process and returns the parsed JSON output.
4
+ * This function is designed to be universally compatible with any agent.
5
+ * @param args - The arguments to pass to the 'proteus inject' command.
6
+ * @returns A promise that resolves with the JSON output from the CLI.
7
+ */
8
+ export async function executeProteusCLI(args) {
9
+ return new Promise((resolve, reject) => {
10
+ const cliArgs = ["inject"];
11
+ // Ensure json_output is always true for tool calling
12
+ if (args.json_output) {
13
+ cliArgs.push("--json");
14
+ }
15
+ else {
16
+ reject(new Error("The 'json_output' argument must be true for tool calling."));
17
+ return;
18
+ }
19
+ if (args.files && args.files.length > 0)
20
+ cliArgs.push(...args.files);
21
+ if (args.include && args.include.length > 0)
22
+ cliArgs.push("--include", ...args.include);
23
+ if (args.exclude && args.exclude.length > 0)
24
+ cliArgs.push("--exclude", ...args.exclude);
25
+ if (args.strategy)
26
+ cliArgs.push("--strategy", args.strategy);
27
+ const proteusProcess = spawn("proteus", cliArgs, { shell: true });
28
+ let stdoutData = "";
29
+ let stderrData = "";
30
+ proteusProcess.stdout.on("data", (data) => {
31
+ stdoutData += data.toString();
32
+ });
33
+ proteusProcess.stderr.on("data", (data) => {
34
+ stderrData += data.toString();
35
+ });
36
+ proteusProcess.on("close", (code) => {
37
+ if (code !== 0) {
38
+ return reject(new Error(`Proteus CLI exited with code ${code}: ${stderrData}`));
39
+ }
40
+ try {
41
+ const jsonOutput = JSON.parse(stdoutData);
42
+ resolve(jsonOutput);
43
+ }
44
+ catch (error) {
45
+ reject(new Error(`Failed to parse Proteus CLI JSON output: ${error}`));
46
+ }
47
+ });
48
+ proteusProcess.on("error", (err) => {
49
+ reject(new Error(`Failed to start Proteus CLI process: ${err.message}`));
50
+ });
51
+ });
52
+ }
@@ -0,0 +1,21 @@
1
+ export interface ProteuConfig {
2
+ injectOnCommit?: boolean;
3
+ injectOnSave?: boolean;
4
+ include?: string[];
5
+ exclude?: string[];
6
+ strategy?: "safe-hash" | "functional";
7
+ verbose?: boolean;
8
+ json?: boolean;
9
+ detectReusableComponents?: boolean;
10
+ autoExcludePatterns?: string[];
11
+ }
12
+ export interface ElementInfo {
13
+ elementName: string;
14
+ fileName: string;
15
+ lineNumber: number;
16
+ key?: string;
17
+ index?: number;
18
+ isDynamic?: boolean;
19
+ componentPath?: string[];
20
+ descriptor?: string;
21
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ // Lean types only; remove unused records
@@ -0,0 +1,4 @@
1
+ import { ElementInfo, ProteuConfig } from "./types.js";
2
+ export declare function generateTestId(config: ProteuConfig, info: ElementInfo): string;
3
+ export declare function shortStableHash(input: string): string;
4
+ export declare function normalizePath(path: string): string;