Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
vendor/** linguist-vendored whitespace=-trailing-space,-space-before-tab
vendor/**/*.wasm binary
vendor/**/*.woff2 binary
4 changes: 4 additions & 0 deletions .github/codeql-config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
name: CodeFlow CodeQL configuration

paths-ignore:
- vendor/**
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ jobs:
with:
languages: javascript-typescript
build-mode: none
config-file: ./.github/codeql-config.yml

- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v4
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Run test suite
run: node --test tests/*.test.mjs tests/*.smoke.js
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Braedon Saunders

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ git clone https://github.com/braedonsaunders/codeflow.git
open index.html
```

No build process. No npm install. It is a single `index.html` app that loads pinned browser dependencies from CDNs.
No build process. No npm install. Clone the whole repository: `index.html` loads pinned,
checked-in browser dependencies from `vendor/`, so a fresh local launch works without a network connection.

### Option 3: Analyze Local Files
You can now analyze code directly from your local machine without uploading to GitHub:
Expand Down Expand Up @@ -236,6 +237,10 @@ CodeFlow extracts functions and analyzes dependencies for:
| Clojure | `.clj`, `.cljs`, `.cljc` |
| Elm | `.elm` |
| VBA | `.vba`, `.bas`, `.cls`, `.xlsm`, `.xlsb`, `.xlam` |
| Delphi / FreePascal / Object Pascal | `.pas`, `.pp`, `.dpr`, `.dpk`, `.lpr`, `.inc` |

Pascal support is heuristic. It recognizes routines and `uses` unit dependencies while filtering
strings and Pascal comment forms; unsupported language features fall back gracefully.

---

Expand Down Expand Up @@ -308,12 +313,19 @@ For larger repositories or team usage, we recommend using GitHub App authenticat
│ │ │
│ ┌────────▼────────┐ │
│ │ React App │ │
│ │ (Single File) │ │
│ │ (Static Assets) │ │
│ └─────────────────┘ │
└─────────────────────────────────────────────────┘
```

**Zero build dependencies to install.** Everything runs from pinned CDNs:
**Zero build dependencies to install.** Browser libraries and required Tree-sitter WASM files are
pinned under `vendor/`, with source URLs, SHA-256 digests, and license notices. To refresh them:

```bash
node scripts/vendor-browser-deps.mjs
```

The vendored runtime includes:
- React 18
- D3.js 7
- Babel (for JSX)
Expand All @@ -329,10 +341,10 @@ We love contributions! Here's how:
3. Test locally (just open in browser)
4. Submit a PR

If you're editing the markdown / wiki-link parser, Node.js unit tests live under `tests/` and run with no dependencies:
Node.js unit tests live under `tests/` and run with no dependencies:

```bash
node --test tests/
node --test tests/*.test.mjs tests/*.smoke.js
```

`tests/verify-brain-vault.mjs` is an optional end-to-end script that always verifies the bundled fixtures and will also scan a real local vault when you explicitly set `BRAIN_VAULT=/path/to/vault`.
Expand All @@ -352,10 +364,10 @@ node --test tests/
> CodeFlow runs entirely in your browser. It calls the GitHub API directly from your browser and processes everything client-side.

**Q: Is my code safe?**
> Yes. Your code is fetched directly from GitHub to your browser. Nothing is sent to any server we control. Check the source — it's one file!
> Yes. Your code is fetched directly from GitHub to your browser. Nothing is sent to any server we control. The app and its pinned browser dependencies are checked into this repository for inspection.

**Q: Can I use it offline?**
> Yes. With the local file analysis feature, you can analyze code from your computer without any internet connection. Click the "Open Folder" button and select your files. All processing happens entirely in your browser.
> Yes. Clone the complete repository, open `index.html`, then use the local file analysis feature. All required browser and parser assets are checked in, so even a cold first load works without internet. GitHub URL analysis still requires access to GitHub's API.

**Q: Why is analysis slow?**
> We make individual API calls for each file to get content. With a token, you get higher rate limits and faster analysis.
Expand Down Expand Up @@ -422,7 +434,7 @@ If you find CodeFlow useful, please star the repo.

## License

MIT License — use it however you want.
[MIT License](./LICENSE) — use it however you want.

---

Expand Down
16 changes: 16 additions & 0 deletions card/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ A GitHub Action that drops a slick auto-updating SVG card on your README — hea

The card uses the **same analyzer** as the codeflow web app. There's no separate parser, no version drift — the Action reads codeflow's `index.html` and runs its analyzer in a Node `vm`.

## Headless analysis

Use the same pipeline without rendering a card, writing history, committing files, or posting PR comments:

```bash
node card/analyze.js --path /path/to/repository --exclude 'vendor/**' --exclude '*.min.js'
```

The command writes only JSON to stdout. Its versioned envelope contains the analyzer's full `data`
object and the card-ready `snapshot`; errors go to stderr. It is also available programmatically:

```js
const { analyze } = require('./card/analyze.js');
const result = await analyze({ repoRoot: '/path/to/repository', exclude: ['vendor/**'] });
```

## Quick start

Drop this file in `.github/workflows/codeflow-card.yml`:
Expand Down
64 changes: 64 additions & 0 deletions card/analyze.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env node

'use strict';

const path = require('path');

const { analyze } = require('./lib/analysis.js');

function usage() {
return [
'Usage: node card/analyze.js [--path <directory>] [--exclude <glob> ...]',
'',
'Writes a versioned CodeFlow analysis envelope to stdout as JSON.',
'Repeat --exclude to omit multiple files or directory patterns.',
].join('\n');
}

function readValue(argv, index, flag) {
if (index + 1 >= argv.length) throw new Error(flag + ' requires a value');
return argv[index + 1];
}

function parseArgs(argv) {
const parsed = { repoRoot: process.cwd(), exclude: [], help: false };
for (let index = 0; index < argv.length; index++) {
const arg = argv[index];
if (arg === '--help' || arg === '-h') {
parsed.help = true;
} else if (arg === '--path') {
parsed.repoRoot = readValue(argv, index, '--path');
index++;
} else if (arg.startsWith('--path=')) {
parsed.repoRoot = arg.slice('--path='.length);
} else if (arg === '--exclude') {
parsed.exclude.push(readValue(argv, index, '--exclude'));
index++;
} else if (arg.startsWith('--exclude=')) {
parsed.exclude.push(arg.slice('--exclude='.length));
} else {
throw new Error('Unknown argument: ' + arg);
}
}
parsed.repoRoot = path.resolve(parsed.repoRoot);
return parsed;
}

async function main(argv) {
const parsed = parseArgs(argv || process.argv.slice(2));
if (parsed.help) {
process.stdout.write(usage() + '\n');
return;
}
const result = await analyze({ repoRoot: parsed.repoRoot, exclude: parsed.exclude });
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
}

if (require.main === module) {
main().catch((error) => {
process.stderr.write('[codeflow-analyze] error: ' + (error.stack || error.message || error) + '\n');
process.exitCode = 1;
});
}

module.exports = { analyze, main, parseArgs, usage };
39 changes: 9 additions & 30 deletions card/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,8 @@ const fs = require('fs');
const path = require('path');

const { loadInputs } = require('./lib/inputs.js');
const { loadAnalyzer, locateIndexHtml } = require('./lib/analyzer.js');
const { buildAnalyzed } = require('./lib/collect.js');
const { compileExcludePatterns } = require('./lib/exclude.js');
const { readState, appendRun, writeState, snapshotFromAnalysis } = require('./lib/state.js');
const { analyze } = require('./lib/analysis.js');
const { readState, appendRun, writeState } = require('./lib/state.js');
const { renderCard } = require('./render/card.js');
const { renderReceiptMarkdown } = require('./render/receipt-md.js');
const { commitAndPush } = require('./lib/git.js');
Expand Down Expand Up @@ -47,29 +45,6 @@ async function run() {
const repoRoot = process.env.GITHUB_WORKSPACE || process.cwd();
log('analyzing ' + repoRoot);

const actionDir = __dirname;
const indexHtmlPath = locateIndexHtml(actionDir, repoRoot);
log('analyzer source: ' + indexHtmlPath);

const { Parser, buildAnalysisData, calcBlast, calcHealth } = loadAnalyzer(indexHtmlPath);

const excludePatterns = compileExcludePatterns(inputs.exclude);
if (excludePatterns.length > 0) {
log('exclude patterns: ' + excludePatterns.map((p) => p.raw).join(', '));
}

const { analyzed, allFns } = await buildAnalyzed(repoRoot, Parser, excludePatterns);
log('collected ' + analyzed.length + ' files (' + allFns.length + ' functions)');

const data = await buildAnalysisData({
analyzed,
allFns,
excludePatterns: excludePatterns.map((p) => p.raw),
progress: () => {},
yieldFn: async () => {},
});
log('analysis: files=' + data.stats.files + ' fns=' + data.stats.functions + ' loc=' + data.stats.loc);

const event = loadEvent();
const sha = process.env.GITHUB_SHA || null;
const actor = process.env.GITHUB_ACTOR || null;
Expand All @@ -78,9 +53,13 @@ async function run() {
(event && event.number) ||
null;
const ctx = { sha, actor, pr: prNumber };

const snapshot = snapshotFromAnalysis(data, { calcBlast, calcHealth }, ctx);
log('grade=' + (snapshot.grade || '?') + ' score=' + (snapshot.score == null ? '?' : snapshot.score));
const { data, snapshot } = await analyze({
repoRoot,
actionDir: __dirname,
exclude: inputs.exclude,
context: ctx,
progress: log,
});

const stateAbs = path.resolve(repoRoot, inputs.state);
const state = readState(stateAbs);
Expand Down
62 changes: 62 additions & 0 deletions card/lib/analysis.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Shared side-effect-free analysis pipeline used by the Action and headless CLI.

'use strict';

const path = require('path');

const { loadAnalyzer, locateIndexHtml } = require('./analyzer.js');
const { buildAnalyzed } = require('./collect.js');
const { compileExcludePatterns } = require('./exclude.js');
const { snapshotFromAnalysis } = require('./state.js');

const HEADLESS_SCHEMA_VERSION = 1;

function normalizeExcludeInput(exclude) {
if (Array.isArray(exclude)) return exclude.join(',');
return exclude == null ? '' : String(exclude);
}

async function analyze(options) {
const opts = options || {};
const repoRoot = path.resolve(opts.repoRoot || process.cwd());
const actionDir = path.resolve(opts.actionDir || path.join(__dirname, '..'));
const progress = typeof opts.progress === 'function' ? opts.progress : () => {};
const indexHtmlPath = opts.indexHtmlPath || locateIndexHtml(actionDir, repoRoot);

progress('analyzer source: ' + indexHtmlPath);
const { Parser, buildAnalysisData, calcBlast, calcHealth } = loadAnalyzer(indexHtmlPath);
const excludePatterns = compileExcludePatterns(normalizeExcludeInput(opts.exclude));
if (excludePatterns.length > 0) {
progress('exclude patterns: ' + excludePatterns.map((pattern) => pattern.raw).join(', '));
}

const { analyzed, allFns } = await buildAnalyzed(repoRoot, Parser, excludePatterns);
Comment thread
braedonsaunders marked this conversation as resolved.
progress('collected ' + analyzed.length + ' files (' + allFns.length + ' functions)');

const data = await buildAnalysisData({
analyzed,
allFns,
excludePatterns: excludePatterns.map((pattern) => pattern.raw),
progress: (message) => progress(message),
yieldFn: async () => {},
});
progress(
'analysis: files=' + data.stats.files +
' fns=' + data.stats.functions +
' loc=' + data.stats.loc
);

const snapshot = snapshotFromAnalysis(
data,
{ calcBlast, calcHealth },
opts.context || {}
);
progress(
'grade=' + (snapshot.grade || '?') +
' score=' + (snapshot.score == null ? '?' : snapshot.score)
);

return { schemaVersion: HEADLESS_SCHEMA_VERSION, data, snapshot };
}

module.exports = { analyze, HEADLESS_SCHEMA_VERSION, normalizeExcludeInput };
1 change: 1 addition & 0 deletions card/lib/collect.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const { matchesExcludePattern } = require('./exclude.js');
const DEFAULT_IGNORES = new Set([
'.git',
'node_modules',
'vendor',
'.next',
'.nuxt',
'dist',
Expand Down
Loading
Loading