recon-generate 0.0.4 → 0.0.5

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/dist/generator.js CHANGED
@@ -195,7 +195,7 @@ class ReconGenerator {
195
195
  return kind;
196
196
  }
197
197
  async findSourceContracts() {
198
- var _a;
198
+ var _a, _b;
199
199
  const contracts = [];
200
200
  const outDir = this.outDir();
201
201
  try {
@@ -224,7 +224,12 @@ class ReconGenerator {
224
224
  const lowerName = String(contractName).toLowerCase();
225
225
  const isGeneratedMock = this.generatedMocks.has(String(contractName));
226
226
  const contractKind = await this.getContractKind(sourcePath, String(contractName));
227
- if (contractKind === 'interface' || contractKind === 'library') {
227
+ // Check if contract is explicitly included or mocked - if so, don't filter by kind
228
+ const isExplicitlyIncluded = this.options.include &&
229
+ (this.options.include.contractOnly.has(String(contractName)) ||
230
+ this.options.include.functions.has(String(contractName)));
231
+ const isExplicitlyMocked = (_b = this.options.mocks) === null || _b === void 0 ? void 0 : _b.has(String(contractName));
232
+ if ((contractKind === 'interface' || contractKind === 'library') && !isExplicitlyIncluded && !isExplicitlyMocked) {
228
233
  this.logDebug('Skipping non-contract artifact', { contractName, sourcePath, contractKind });
229
234
  continue;
230
235
  }
@@ -388,7 +393,10 @@ class ReconGenerator {
388
393
  if (!tempSrc) {
389
394
  return;
390
395
  }
391
- const mocksDir = path.join(this.options.suiteDir, 'mocks');
396
+ const suiteDirAbs = path.isAbsolute(this.options.suiteDir)
397
+ ? this.options.suiteDir
398
+ : path.join(this.foundryRoot, this.options.suiteDir);
399
+ const mocksDir = path.join(suiteDirAbs, 'mocks');
392
400
  await fs.mkdir(mocksDir, { recursive: true });
393
401
  const tempFiles = await fs.readdir(tempSrc);
394
402
  for (const f of tempFiles) {
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ const path = __importStar(require("path"));
39
39
  const case_1 = require("case");
40
40
  const generator_1 = require("./generator");
41
41
  const utils_1 = require("./utils");
42
+ const link_1 = require("./link");
42
43
  function parseFilter(input) {
43
44
  if (!input)
44
45
  return undefined;
@@ -119,54 +120,87 @@ async function main() {
119
120
  .option('--list', 'List available contracts/functions (after filters) and exit')
120
121
  .option('--force', 'Replace existing generated suite output (under --output). Does not rebuild .recon/out.')
121
122
  .option('--force-build', 'Delete .recon/out to force a fresh forge build before generating')
123
+ .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)');
124
+ program
125
+ .command('link')
126
+ .description('Link library addresses into echidna/medusa configs via crytic-compile')
127
+ .option('--echidna-config <path>', 'Path to echidna yaml (defaults based on --name)')
128
+ .option('--medusa-config <path>', 'Path to medusa json (defaults based on --name)')
129
+ .option('--name <suite>', 'Suite name to pick config defaults (echidna-<name>.yaml / medusa-<name>.json)')
122
130
  .option('--foundry-config <path>', 'Path to foundry.toml (defaults to ./foundry.toml)')
123
- .parse(process.argv);
124
- const opts = program.opts();
125
- const workspaceRoot = process.cwd();
126
- const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
127
- const foundryRoot = path.dirname(foundryConfig);
128
- const includeFilter = parseFilter(opts.include);
129
- const excludeFilter = parseFilter(opts.exclude);
130
- const adminFilter = parseFilter(opts.admin);
131
- const mockSet = opts.mock
132
- ? new Set(String(opts.mock).split(',').map((s) => s.trim()).filter(Boolean))
133
- : undefined;
134
- const dynamicDeploySet = opts.dynamicDeploy
135
- ? new Set(String(opts.dynamicDeploy).split(',').map((s) => s.trim()).filter(Boolean))
136
- : undefined;
137
- const suiteRaw = opts.name ? String(opts.name).trim() : '';
138
- const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
139
- const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
140
- const suiteFolderName = suiteSnake ? `recon-${suiteSnake}` : 'recon';
141
- const baseOut = opts.output ? String(opts.output) : await (0, utils_1.getTestFolder)(foundryRoot);
142
- const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.join(foundryRoot, baseOut);
143
- const suiteDir = path.join(baseOutPath, suiteFolderName);
144
- const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
145
- const reconPath = opts.recon
146
- ? (path.isAbsolute(opts.recon) ? opts.recon : path.join(workspaceRoot, opts.recon))
147
- : (await (0, utils_1.fileExists)(path.join(foundryRoot, reconDefaultName))
148
- ? path.join(foundryRoot, reconDefaultName)
149
- : path.join(foundryRoot, '.recon', reconDefaultName));
150
- const generator = new generator_1.ReconGenerator(foundryRoot, {
151
- suiteDir,
152
- reconConfigPath: reconPath,
153
- foundryConfigPath: foundryConfig,
154
- include: includeFilter,
155
- exclude: excludeFilter,
156
- admin: adminFilter ? adminFilter.functions : undefined,
157
- debug: !!opts.debug,
158
- force: !!opts.force,
159
- forceBuild: !!opts.forceBuild,
160
- mocks: mockSet,
161
- dynamicDeploy: dynamicDeploySet,
162
- suiteNameSnake: suiteSnake,
163
- suiteNamePascal: suitePascal,
131
+ .action(async (opts) => {
132
+ const workspaceRoot = process.cwd();
133
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
134
+ const foundryRoot = path.dirname(foundryConfig);
135
+ const suiteRaw = opts.name ? String(opts.name).trim() : '';
136
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
137
+ const suffix = suiteSnake ? `-${suiteSnake}` : '';
138
+ const echidnaDefault = `echidna${suffix}.yaml`;
139
+ const medusaDefault = `medusa${suffix}.json`;
140
+ const echidnaConfigOpt = opts.echidnaConfig || echidnaDefault;
141
+ const medusaConfigOpt = opts.medusaConfig || medusaDefault;
142
+ const echidnaConfigPath = path.isAbsolute(echidnaConfigOpt)
143
+ ? echidnaConfigOpt
144
+ : path.join(foundryRoot, echidnaConfigOpt);
145
+ const medusaConfigPath = path.isAbsolute(medusaConfigOpt)
146
+ ? medusaConfigOpt
147
+ : path.join(foundryRoot, medusaConfigOpt);
148
+ await (0, link_1.runLink)(foundryRoot, echidnaConfigPath, medusaConfigPath);
164
149
  });
165
- if (opts.list) {
166
- await generator.listAvailable();
167
- return;
168
- }
169
- await generator.run();
150
+ program
151
+ .action(async (opts) => {
152
+ const workspaceRoot = process.cwd();
153
+ const foundryConfig = (0, utils_1.getFoundryConfigPath)(workspaceRoot, opts.foundryConfig);
154
+ const foundryRoot = path.dirname(foundryConfig);
155
+ const includeFilter = parseFilter(opts.include);
156
+ const excludeFilter = parseFilter(opts.exclude);
157
+ const adminFilter = parseFilter(opts.admin);
158
+ const mockSet = opts.mock
159
+ ? new Set(String(opts.mock).split(',').map((s) => s.trim()).filter(Boolean))
160
+ : undefined;
161
+ if (includeFilter && mockSet && mockSet.size > 0) {
162
+ for (const m of mockSet) {
163
+ includeFilter.contractOnly.add(`${m}Mock`);
164
+ }
165
+ }
166
+ const dynamicDeploySet = opts.dynamicDeploy
167
+ ? new Set(String(opts.dynamicDeploy).split(',').map((s) => s.trim()).filter(Boolean))
168
+ : undefined;
169
+ const suiteRaw = opts.name ? String(opts.name).trim() : '';
170
+ const suiteSnake = suiteRaw ? (0, case_1.snake)(suiteRaw) : '';
171
+ const suitePascal = suiteRaw ? (0, case_1.pascal)(suiteRaw) : '';
172
+ const suiteFolderName = suiteSnake ? `recon-${suiteSnake}` : 'recon';
173
+ const baseOut = opts.output ? String(opts.output) : await (0, utils_1.getTestFolder)(foundryRoot);
174
+ const baseOutPath = path.isAbsolute(baseOut) ? baseOut : path.join(foundryRoot, baseOut);
175
+ const suiteDir = path.join(baseOutPath, suiteFolderName);
176
+ const reconDefaultName = suiteSnake ? `recon-${suiteSnake}.json` : 'recon.json';
177
+ const reconPath = opts.recon
178
+ ? (path.isAbsolute(opts.recon) ? opts.recon : path.join(workspaceRoot, opts.recon))
179
+ : (await (0, utils_1.fileExists)(path.join(foundryRoot, reconDefaultName))
180
+ ? path.join(foundryRoot, reconDefaultName)
181
+ : path.join(foundryRoot, '.recon', reconDefaultName));
182
+ const generator = new generator_1.ReconGenerator(foundryRoot, {
183
+ suiteDir,
184
+ reconConfigPath: reconPath,
185
+ foundryConfigPath: foundryConfig,
186
+ include: includeFilter,
187
+ exclude: excludeFilter,
188
+ admin: adminFilter ? adminFilter.functions : undefined,
189
+ debug: !!opts.debug,
190
+ force: !!opts.force,
191
+ forceBuild: !!opts.forceBuild,
192
+ mocks: mockSet,
193
+ dynamicDeploy: dynamicDeploySet,
194
+ suiteNameSnake: suiteSnake,
195
+ suiteNamePascal: suitePascal,
196
+ });
197
+ if (opts.list) {
198
+ await generator.listAvailable();
199
+ return;
200
+ }
201
+ await generator.run();
202
+ });
203
+ await program.parseAsync(process.argv);
170
204
  }
171
205
  main().catch(err => {
172
206
  console.error('recon-generate failed:', err.message || err);
package/dist/link.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function runLink(foundryRoot: string, echidnaConfigPath: string, medusaConfigPath: string): Promise<void>;
package/dist/link.js ADDED
@@ -0,0 +1,137 @@
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.runLink = runLink;
40
+ const child_process_1 = require("child_process");
41
+ const fs = __importStar(require("fs/promises"));
42
+ const yaml_1 = __importDefault(require("yaml"));
43
+ const utils_1 = require("./utils");
44
+ const generateHexAddress = (index) => {
45
+ return `0xf${(index + 1).toString().padStart(2, '0')}`;
46
+ };
47
+ const parseLibrariesFromOutput = (output) => {
48
+ const usesPattern = /^\s+uses: \[(.*?)\]/gm;
49
+ const matches = [...(0, utils_1.stripAnsiCodes)(output).matchAll(usesPattern)];
50
+ const allLibraries = [];
51
+ for (const match of matches) {
52
+ if (match[1]) {
53
+ const libs = match[1]
54
+ .split(',')
55
+ .map(lib => lib.trim().replace(/["'\s]/g, ''))
56
+ .filter(lib => lib.length > 0);
57
+ for (const lib of libs) {
58
+ if (!allLibraries.includes(lib)) {
59
+ allLibraries.push(lib);
60
+ }
61
+ }
62
+ }
63
+ }
64
+ return allLibraries;
65
+ };
66
+ const runCryticCompile = (cwd) => {
67
+ return new Promise((resolve, reject) => {
68
+ (0, child_process_1.exec)('crytic-compile . --foundry-compile-all --print-libraries', { cwd, env: { ...process.env, PATH: (0, utils_1.getEnvPath)() } }, (error, stdout, stderr) => {
69
+ if (error) {
70
+ reject(new Error(stderr || error.message));
71
+ return;
72
+ }
73
+ resolve(stdout || '');
74
+ });
75
+ });
76
+ };
77
+ const formatEchidnaYaml = (config, libraries) => {
78
+ const cfg = { ...config };
79
+ delete cfg.cryticArgs;
80
+ delete cfg.deployContracts;
81
+ let out = yaml_1.default.stringify(cfg, { indent: 2 });
82
+ if (libraries.length === 0) {
83
+ out += 'cryticArgs: []\n';
84
+ out += 'deployContracts: []\n';
85
+ return out;
86
+ }
87
+ const libraryArgs = libraries.map((lib, index) => `(${lib},${generateHexAddress(index)})`).join(',');
88
+ out += `cryticArgs: ["--compile-libraries=${libraryArgs}","--foundry-compile-all"]\n`;
89
+ out += 'deployContracts:\n';
90
+ libraries.forEach((lib, index) => {
91
+ out += ` - ["${generateHexAddress(index)}", "${lib}"]\n`;
92
+ });
93
+ return out;
94
+ };
95
+ const updateEchidnaConfig = async (configPath, libraries) => {
96
+ const content = await fs.readFile(configPath, 'utf8');
97
+ const parsed = yaml_1.default.parse(content) || {};
98
+ const updated = formatEchidnaYaml(parsed, libraries);
99
+ await fs.writeFile(configPath, updated, 'utf8');
100
+ };
101
+ const updateMedusaConfig = async (configPath, libraries) => {
102
+ const content = await fs.readFile(configPath, 'utf8');
103
+ const parsed = JSON.parse(content);
104
+ if (!parsed.compilation)
105
+ parsed.compilation = {};
106
+ if (!parsed.compilation.platformConfig)
107
+ parsed.compilation.platformConfig = {};
108
+ if (libraries.length > 0) {
109
+ const libraryArgs = libraries.map((lib, index) => `(${lib},${generateHexAddress(index)})`).join(',');
110
+ parsed.compilation.platformConfig.args = ['--compile-libraries', libraryArgs, '--foundry-compile-all'];
111
+ }
112
+ else {
113
+ delete parsed.compilation.platformConfig.args;
114
+ }
115
+ await fs.writeFile(configPath, JSON.stringify(parsed, null, 2), 'utf8');
116
+ };
117
+ async function runLink(foundryRoot, echidnaConfigPath, medusaConfigPath) {
118
+ const output = await runCryticCompile(foundryRoot);
119
+ const libraries = parseLibrariesFromOutput(output);
120
+ console.log('Detected libraries:', libraries.length ? libraries.join(', ') : '(none)');
121
+ try {
122
+ await fs.access(echidnaConfigPath);
123
+ await updateEchidnaConfig(echidnaConfigPath, libraries);
124
+ console.log(`Updated echidna config at ${echidnaConfigPath}`);
125
+ }
126
+ catch (e) {
127
+ throw new Error(`Failed to update echidna config: ${e}`);
128
+ }
129
+ try {
130
+ await fs.access(medusaConfigPath);
131
+ await updateMedusaConfig(medusaConfigPath, libraries);
132
+ console.log(`Updated medusa config at ${medusaConfigPath}`);
133
+ }
134
+ catch (e) {
135
+ throw new Error(`Failed to update medusa config: ${e}`);
136
+ }
137
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recon-generate",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "CLI to scaffold Recon fuzzing suite inside Foundry projects",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -22,7 +22,8 @@
22
22
  "abi-to-mock": "^1.0.4",
23
23
  "case": "^1.6.3",
24
24
  "commander": "^14.0.2",
25
- "handlebars": "^4.7.8"
25
+ "handlebars": "^4.7.8",
26
+ "yaml": "^2.8.2"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@types/node": "^24.0.4",