import { readFile, readdir, writeFile, access } from 'node:fs/promises' import { join, resolve } from 'node:path' const root = resolve(new URL('..', import.meta.url).pathname) const sourceRoot = resolve(process.env.FREECAD_SOURCE_DIR || join(root, '.cache/freecad/FreeCAD')) const outputPath = resolve(process.env.FREECAD_INVENTORY_FILE || join(root, 'config/freecad-source-inventory.json')) const baselineCommit = '0108fd4b4850cc46e625b60e53cea7a7bbe69f8d' const cmakeAliases = { AddonManager: 'ADDONMGR', JtReader: 'JTREADER', MeshPart: 'MESH_PART', PartDesign: 'PART_DESIGN', TechDraw: 'TECHDRAW', ReverseEngineering: 'REVERSEENGINEERING', OpenSCAD: 'OPENSCAD', } const read = async (path) => readFile(path, 'utf8') const exists = async (path) => access(path).then(() => true).catch(() => false) const moduleRoot = join(sourceRoot, 'src/Mod') if (!(await exists(moduleRoot))) throw new Error(`FreeCAD source directory is unavailable: ${moduleRoot}`) const buildOptions = await read(join(sourceRoot, 'cMake/FreeCAD_Helpers/InitializeFreeCADBuildOptions.cmake')) const optionDefaults = new Map([...buildOptions.matchAll(/option\(BUILD_([A-Z0-9_]+)[^\n]*?\s(ON|OFF)\)/g)].map((match) => [match[1], match[2] === 'ON'])) const moduleNames = (await readdir(moduleRoot, { withFileTypes: true })).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort() const commandPattern = /(?:addCommand|addCommand\s*\(|registerCommand)\s*\(\s*["']([^"']+)["']/g const commandIds = (text) => [...text.matchAll(commandPattern)].map((match) => match[1]).filter((value, index, values) => values.indexOf(value) === index).sort() const modules = [] for (const name of moduleNames) { const modulePath = join(moduleRoot, name) const option = cmakeAliases[name] || name.toUpperCase() const cmakePath = join(modulePath, 'CMakeLists.txt') const appPath = join(modulePath, 'App') const guiPath = join(modulePath, 'Gui') const cmake = await exists(cmakePath) ? await read(cmakePath) : '' const pythonFiles = [] const collectPython = async (directory) => { if (!(await exists(directory))) return for (const entry of await readdir(directory, { withFileTypes: true })) { const path = join(directory, entry.name) if (entry.isDirectory()) await collectPython(path) else if (entry.name.endsWith('.py')) pythonFiles.push(path) } } await collectPython(modulePath) const commandNames = [] for (const path of pythonFiles) { const text = await read(path) commandNames.push(...commandIds(text)) } modules.push({ name, sourcePath: `src/Mod/${name}`, cmakeOption: `BUILD_${option}`, cmakeDefault: optionDefaults.has(option) ? optionDefaults.get(option) : null, hasCMake: Boolean(cmake), hasApp: await exists(appPath), hasGui: await exists(guiPath), pythonFileCount: pythonFiles.length, commandIds: [...new Set(commandNames)].sort(), runtimeStatus: 'requires-freecad-reference-probe', }) } const inventory = { schemaVersion: 1, baseline: { freecadVersion: '1.1.1', commit: baselineCommit, sourcePath: '.cache/freecad/FreeCAD' }, generatedBy: 'scripts/generate-freecad-source-inventory.mjs', moduleCount: modules.length, modules, } if (modules.length !== 34) throw new Error(`Expected 34 FreeCAD modules, found ${modules.length}.`) await writeFile(outputPath, `${JSON.stringify(inventory, null, 2)}\n`, 'utf8') console.log(JSON.stringify({ status: 'source-inventory-generated', output: outputPath, moduleCount: modules.length, commandCount: modules.reduce((total, module) => total + module.commandIds.length, 0) }, null, 2))