diff --git a/.github/workflows/readme-automation.yml b/.github/workflows/readme-automation.yml new file mode 100644 index 00000000..fa375529 --- /dev/null +++ b/.github/workflows/readme-automation.yml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com +# SPDX-License-Identifier: MIT +--- +# yamllint disable rule:line-length +name: readme-automation + +'on': + workflow_dispatch: + push: + paths: + - "src/eoc.js" +concurrency: + group: readmeautomation-${{ github.ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ubuntu-24.04 + + steps: + - name: Check out repo + uses: actions/checkout@v6 + - name: Use Node + uses: actions/setup-node@v6 + with: + node-version: "18.x" + check-latest: false + - name: Clean install + run: npm ci + - name: Install global + run: npm install -g + - name: Run readme_automation script + run: |- + node scripts/readme_automation.js + - name: Create Pull Request + uses: peter-evans/create-pull-request@v8 + with: + commit-message: "chore(readme): insert sections into README from CLI help" + branch: chore/readme-automation + title: "chore(readme): insert sections into README from CLI help" + body: | + This PR was automatically generated by GitHub Actions. + + - Insert sections into README from CLI help output + - Keeps documentation in sync with the CLI diff --git a/README.md b/README.md index 751862b9..07b41012 100644 --- a/README.md +++ b/README.md @@ -115,42 +115,30 @@ That's it. You can also do many other things with `eoc` commands (the flow is explained in [this blog post][blog]): -* `register` finds necessary `.eo` files and registers them in a JSON catalog -* `assemble` parses `.eo` files into `.xmir`, optimizes them, - and pulls foreign EO objects -* `transpile` converts `.xmir` files to the target programming -language (Java by default) -* `compile` converts target language sources (e.g., `.java`) -to binaries (e.g., `.class`) -* `link` puts all binaries together into a single executable binary -* `dataize` dataizes a single object from the executable binary -* `test` dataizes all visible unit tests -* `lint` finds style-related errors in EO and XMIR files -* `jeo:disassemble` converts Java `.class` files to `.xmir` -(via [jeo](https://github.com/objectionary/jeo-maven-plugin)) -* `jeo:assemble` converts `.xmir` files to Java `.class` files -(via [jeo](https://github.com/objectionary/jeo-maven-plugin)) - -There are also commands that help manipulate with XMIR and EO sources -(the list is not completed, while some of them are not implemented as of yet): - -* `audit` inspects all required packages and reports their status -* `foreign` inspects all objects found in the program after the `assemble` step -* `sodg` generates `.sodg` from `.xmir`, further rederable as XML or [Dot][dot] -* `print` generates `.eo` files from `.xmir` files -* `generate_comments` generates `.json` files with LLM-generated - documentation for `.eo` structures -* `docs` generates HTML documentation from `.xmir` files -* `latex` generates `.tex` files from `.eo` sources -* `fmt` formats `.eo` files in the source directory -* `normalize` normalizes `.eo` files via phi-calculus rewriting using - [phino](https://github.com/objectionary/phino) (must be installed separately); - original files are saved to `.eoc/before-normalize/` for debugging -* ~~`translate` converts Java/C++/Python/etc. program to EO program~~ -* ~~`demu` removes `cage` and `memory` objects~~ -* ~~`dejump` removes `goto` objects~~ -* ~~`infer` suggests object names where it's possible to infer them~~ -* ~~`flatten` moves inner objects to upper level~~ + +* `audit` Inspect all packages and report their status +* `foreign` Inspect and print the list of foreign objects +* `clean` Delete all temporary files +* `register` Register all visible EO source files +* `parse` Parse EO files into XMIR +* `assemble` Parse EO files into XMIR and join them with required dependencies +* `sodg` Generate SODG files from XMIR +* `print` Generate EO files from XMIR files +* `lint` Lint XMIR files and fail if any issues inside +* `resolve` Resolve all the dependencies required for compilation +* `transpile` Convert EO files into target language +* `compile` Compile target language sources into binaries +* `link` Link together all binaries into a single executable binary +* `dataize` Run the single executable binary and dataize an object +* `test` Run all visible unit tests +* `docs` Generate documentation from XMIR files +* `generate_comments` Generate documentation with LLM +* `jeo:disassemble` Disassemble .class files to .xmir files +* `jeo:assemble` Assemble .xmir files to .class files +* `latex` Generate LaTeX files from EO sources +* `normalize` Normalize EO files using phi-calculus normalization via phino +* `fmt` Format EO files in the source directory + This command line toolkit simply integrates other tools available in the [@objectionary](https://github.com/objectionary) GitHub organization. diff --git a/scripts/readme_automation.js b/scripts/readme_automation.js new file mode 100644 index 00000000..1970b1b9 --- /dev/null +++ b/scripts/readme_automation.js @@ -0,0 +1,50 @@ +#!/usr/bin/env node +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ + +const assert = require('assert'); +const { commandsDescription } = require("../src/eoc"); + +function escapeRegex(str) { + return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function updateSection(sectionName, newContent, readMeContent) { + const start = ``; + const end = ``; + const regex = new RegExp( + `(${escapeRegex(start)})([\\s\\S]*?)(${escapeRegex(end)})`, + "g" + ); + return readMeContent.replace(regex, `$1\n${newContent}$3`); +} + +function bulletListTemplate(rows) { + return `${rows.map(([cmd, desc]) => `* \`${cmd}\` ${desc}`).join("\n") }\n`; +} + +function modifyReadme(commands,fs) { + assert.ok(commands.length > 0,'Commands should have rows') + const commandsMarkdown = bulletListTemplate(commands); + assert.ok(commandsMarkdown.length > 0,"commandsMarkdown result should have text") + let readMeContent = fs.readFileSync('README.md', "utf8"); + assert.ok(readMeContent.length > 0,"readMeContent should have text") + readMeContent = updateSection('commands', commandsMarkdown, readMeContent); + assert.ok(readMeContent.length > 0,"readMeContent should sill have text after modification") + fs.writeFileSync('README.md',readMeContent); +} + +function syncCommandsToReadme(fs) +{ + modifyReadme(commandsDescription(),fs); +} + +module.exports = { + bulletListTemplate, updateSection, modifyReadme, syncCommandsToReadme +}; + +if (require.main === module) { + syncCommandsToReadme(require("fs")); +}; diff --git a/src/eoc.js b/src/eoc.js index 352be112..1a15cbbd 100755 --- a/src/eoc.js +++ b/src/eoc.js @@ -394,12 +394,19 @@ program.command('fmt') }); }); -try { - program.parse(process.argv); -} catch (e) { - console.error(e.message); - console.debug(e.stack); - process.exit(1); +if (require.main === module) { + try { + program.parse(process.argv); + } catch (e) { + console.error(e.message); + console.debug(e.stack); + process.exit(1); + } +} + +module.exports.commandsDescription = function commandsDescription() { + return program.commands + .map(c => [c.name(),c.description()]); } /** diff --git a/test/test_eoc.js b/test/test_eoc.js index ef403f9f..cbc449c1 100644 --- a/test/test_eoc.js +++ b/test/test_eoc.js @@ -20,6 +20,14 @@ describe('eoc', () => { assert(stdout.includes(version.when)); done(); }); + it('can get commands description from eoc as a module', (done) => { + const commandsDescriptionList = require('../src/eoc').commandsDescription(); + assert(commandsDescriptionList.length > 0,"commandsDescriptionList should have more then one element"); + assert(commandsDescriptionList[0].length == 2,"commandsDescriptionList element should have 2 values"); + assert(commandsDescriptionList[0][0].length > 0,"First value of commandsDescriptionList element should have non-zero length"); + assert(commandsDescriptionList[0][1].length > 0,"Second value of commandsDescriptionList element should have non-zero length"); + done(); + }); }); describe('eoc', () => { diff --git a/test/test_readme_automation.js b/test/test_readme_automation.js new file mode 100644 index 00000000..dcac222d --- /dev/null +++ b/test/test_readme_automation.js @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2022-2026 Objectionary.com + * SPDX-License-Identifier: MIT + */ + +const assert = require("assert"); +const path = require("path"); +const { updateSection, bulletListTemplate, modifyReadme, syncCommandsToReadme} = require(path.join(__dirname, "../scripts/readme_automation.js")); + +const readme = `before\n\nold content\n\n\nkeep\n\nafter` +const commandsList = [['one','a'],['two','b'],['three','c']]; +const keywords = ["keep","after","before","",""]; +const fsMockProto = {readMeContent:null, readFileSync(path,options){ return readme; }, writeFileSync(path,data){ this.readMeContent = data; }}; + + +describe("readme_automation scripts", () => { + it("updateSection replaces only the requested section", () => { + const readmeUpdated = updateSection("commands", "new content", readme); + assert.ok(!readmeUpdated.includes("old content"),"Update README should no have 'old content'"); + assert.ok(keywords.every(sub => readmeUpdated.includes(sub)),"Update README should have all keywords"); + assert.ok(readmeUpdated.includes("new content"),"Update README should have `new content`"); + }); + it("bulletListTemplate renders a list", async () => { + const bulletListMarkdown = bulletListTemplate(commandsList); + assert.strictEqual(bulletListMarkdown, "* `one` a\n* `two` b\n* `three` c\n"); + }); + it("call modifyReadme with fs mock", async () => { + const fsMock = {...fsMockProto}; + modifyReadme(commandsList,fsMock); + assert.notEqual(fsMock.readMeContent,null,"README file should have some content"); + assert.ok(!fsMock.readMeContent.includes("old content"),"README file should no have 'old content'"); + assert.ok(keywords.every(sub => fsMock.readMeContent.includes(sub)),"README file should have all keywords"); + assert.ok(["* `one` a","* `two` b","* `three` c"].every(sub => fsMock.readMeContent.includes(sub))); + }); + it("test the whole workflow with fs mock", async () => { + const fsMock = {...fsMockProto}; + syncCommandsToReadme(fsMock); + assert.notEqual(fsMock.readMeContent,null,"README file should have some content"); + }); +});