diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c552595..6f8047d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -36,6 +36,11 @@ jobs: - name: Generate version file run: bun run generate:version + - name: Examples up-to-date + run: | + bun run generate:examples + git diff --exit-code -- "examples/*.md" + - name: Lint run: bun run lint diff --git a/README.md b/README.md index bdf0259..707a9b4 100644 --- a/README.md +++ b/README.md @@ -18,18 +18,20 @@ Or run instantly: npx fln . -o codebase.md ``` -Works with **Claude**, **ChatGPT**, **Gemini**, **Grok**, **Cursor**, **Copilot**, and any AI tool. +Works with **Claude**, **ChatGPT**, **Gemini**, **Grok**, **Cursor**, **Copilot**, and *any* AI tool. + +**`fln`** (short for *flatten*) is **language-agnostic** by design: TypeScript, Python, Java, Go, Rust, Bash, SQL, mixed monorepos — it treats everything as plain text, detects project metadata from common manifests (`package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml`, `CMakeLists.txt`, `vcpkg.json`), respects `.gitignore`, and skips binaries by default. ## Why fln exists If you use LLMs for real projects, you’ve hit these limits: -- **Context windows** — large projects don’t fit -- **Upload friction** — dozens of files, every session -- **Partial understanding** — AI sees fragments, not architecture -- **Manual prep** — repeating the same setup again and again +- **Context windows** — large projects don’t fit. +- **Upload friction** — selecting dozens of files for every session. +- **Partial understanding** — AI sees fragments, not the architecture. +- **Manual prep** — repeating the same setup context again and again. -**fln removes that overhead.** +**`fln` removes that overhead.** It turns your project into a single, structured snapshot that LLMs can actually reason about. ## What fln enables @@ -37,39 +39,42 @@ It turns your project into a single, structured snapshot that LLMs can actually **→ Full-context refactoring** Ask architectural questions that are impossible file-by-file: > “Where is the real coupling here?” -> “What should be split into modules?” +> “What should be split into modules?” **→ Instant onboarding** -One markdown file instead of “start by opening these 12 folders”. +One markdown file instead of “start by opening these 12 folders”. Perfect for reading code on a tablet or onboarding new developers without an IDE. **→ Project-level code reviews** Let AI detect patterns, inconsistencies, and risks across the entire codebase. -**→ Reliable documentation snapshots** -Generate auditable, deterministic snapshots for docs, reviews, or CI. +**→ Auditable Snapshots** +Create a single, clean artifact of your codebase state for security reviews, compliance audits, or legal records without granting full repo access. + +**→ Dataset Preparation** +Generate clean, formatted data for RAG pipelines and fine-tuning custom models. **→ LLM-friendly diffs** -Flatten → commit → flatten again. See how the *whole project* changed. +Flatten → commit → flatten again. See how the *whole project* changed structurally. ## Compatible with your AI workflow -- **Claude** — ideal for large architectural prompts (200K+ tokens) -- **Gemini** — push massive codebases into 1M token windows -- **ChatGPT** — single-shot analysis without attachments -- **Cursor / Windsurf** — reference the full project in prompts -- **GitHub Copilot** — better context → better suggestions -- **Local LLMs** — datasets for RAG and fine-tuning +- **Claude** — ideal for large architectural prompts (200K+ tokens). +- **Gemini** — push massive codebases into 1M token windows. +- **ChatGPT** — single-shot analysis without attachments. +- **Cursor / Windsurf** — reference the full project in prompts. +- **GitHub Copilot** — better context → better suggestions. +- **Local LLMs** — datasets for RAG and fine-tuning. ## Built for real projects -- ⚡ **Fast parallel scanning** — thousands of files in seconds -- 🎯 **Smart filtering** — respects `.gitignore`, excludes binaries, configurable size limits -- 📁 **Intentional file order** — entry points and configs first, not alphabetical noise -- 🔄 **Auto-detection** — skips files previously generated by fln -- 📐 **Deterministic output** — same input → same snapshot -- 🧠 **Project metadata detection** — name & version from ecosystem-native manifests -- 🛠️ **Developer-friendly** — Markdown for humans, JSON for tooling, dry-run mode for safety -- 🔒 **No surprises** — runs locally, no data leaves your machine +- ⚡ **Fast parallel scanning** — thousands of files in seconds. +- 🎯 **Smart filtering** — respects `.gitignore`, excludes binaries, configurable size limits. +- 📁 **Intentional file order** — entry points and configs first, not alphabetical noise. +- 🔄 **Auto-detection** — skips files previously generated by `fln`. +- 📐 **Deterministic output** — same input → same snapshot. +- 🧠 **Project metadata detection** — name & version from ecosystem-native manifests. +- 🛠️ **Developer-friendly** — `Markdown` for humans, `JSON` for tooling, `--dry-run` mode for safety. +- 🔒 **No surprises** — runs locally, no data leaves your machine. Zero dependencies on external services. Zero tracking. Just a tool that does its job. @@ -171,6 +176,56 @@ fln . --dry-run +## CI/CD & Automation + +Integrate `fln` into your pipeline to keep your codebase “AI-ready” automatically. + +### GitHub Actions: Auto-generate Snapshots + +Generate a fresh `codebase.md` artifact on every push. Download it anytime to chat with LLMs about the *exact* state of your main branch or a specific PR without manual scanning. + +Create `.github/workflows/codebase-snapshot.yaml`: + +```yaml +name: Snapshot Codebase + +on: + push: + branches: [ "main" ] + pull_request: + +jobs: + snapshot: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + + - name: Generate Snapshot + # Generates codebase.md without installing fln globally + run: npx fln . -o codebase.md --no-ansi + + - name: Upload Artifact + uses: actions/upload-artifact@v6 + with: + name: codebase-snapshot + path: codebase.md + retention-days: 7 +``` + +### Git Hooks: Pre-commit Context Guard + +Prevent accidental “context bloat” (e.g., committing large datasets or wrong lockfiles) by failing commits if the flattened codebase exceeds a specific size. This ensures your project always fits within LLM context windows. + +Add to your pre-commit hook (e.g., via `husky` or `lint-staged`): + +```bash +# Fails the commit if the flattened codebase exceeds 5MB (configurable) +# --dry-run ensures no files are written to disk +npx fln . --dry-run --max-total-size 5mb +``` + ## JavaScript API ```typescript @@ -249,38 +304,13 @@ All CLI options are available via `FlnOptions`. ## Preview -````md - - -# Codebase Snapshot: my-app - -Generated: 2026-02-03 09:41 -Files: 42 | Directories: 8 +Full real outputs are provided below. Each example is a compact project in [`examples/`](examples/). `fln` outputs the directory tree and file contents with **entry points and configs first** (intentional file order): ---- - -## Directory Tree -```text -├── src/ -│ ├── cli/ -│ │ └── main.ts -│ └── core/ -│ └── scanTree.ts -… -``` - ---- - -## Source Files - -### src/core/scanTree.ts -```ts -export function scanTree(…) { - // … -} -``` -… -```` +- [TypeScript](examples/ts-app.md) +- [Python](examples/python-app.md) +- [Java](examples/java-app.md) +- [Go](examples/go-app.md) +- [Rust](examples/rust-app.md) ## Support this project @@ -293,7 +323,7 @@ If it saves you time or improves your AI workflow: ## Contributing PRs and issues are welcome. -See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and guidelines. +See [`CONTRIBUTING.md`](CONTRIBUTING.md) for setup and guidelines. ## License diff --git a/eslint.config.js b/eslint.config.js index 7753a58..c4dd582 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1 +1,7 @@ -export { bun as default } from "@nesvet/eslint-config";// eslint-disable-line import/no-default-export +import { bun } from "@nesvet/eslint-config"; + + +export default [// eslint-disable-line import/no-default-export + { ignores: [ "examples/**" ] }, + ...bun +]; diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..5de2d5c --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Examples + +Subfolders here are minimal sample projects in various languages, used to demonstrate `fln` output. Each subfolder has a corresponding generated snapshot `examples/.md` next to it. + +The `*.md` files in this directory are produced by a script. After changing any example’s code, regenerate them from the repository root: + +```bash +bun run generate:examples +``` diff --git a/examples/go-app.md b/examples/go-app.md new file mode 100644 index 0000000..70b1941 --- /dev/null +++ b/examples/go-app.md @@ -0,0 +1,166 @@ + + +# Codebase Snapshot: go-app + +Generated: 2026-01-01 00:00 +Files: 7 | Directories: 8 + +--- + +## Directory Tree +```text +├── README.md +├── go.mod +├── cmd +│ └── app +│ └── main.go +└── internal + ├── config + │ └── config.go + ├── formatter + │ └── formatter.go + ├── processor + │ └── processor.go + └── reader + └── reader.go +``` + +--- + +## Source Files + +### README.md +```md +# go-app + +Go app example. +``` + +### go.mod +```mod +module example.com/go-app + +go 1.22 +``` + +### cmd/app/main.go +```go +package main + +import ( + "fmt" + + "example.com/go-app/internal/config" + "example.com/go-app/internal/formatter" + "example.com/go-app/internal/processor" + "example.com/go-app/internal/reader" +) + +func main() { + appConfig := config.LoadConfig() + lines := reader.ReadLines(appConfig.InputPath) + report := processor.BuildReport(appConfig.ProjectName, lines, appConfig.MinLineLength) + fmt.Println(formatter.FormatReport(report)) +} +``` + +### internal/config/config.go +```go +package config + +type Config struct { + ProjectName string + InputPath string + MinLineLength int +} + +func LoadConfig() Config { + return Config{ + ProjectName: "go-app", + InputPath: "sample.txt", + MinLineLength: 3, + } +} +``` + +### internal/formatter/formatter.go +```go +package formatter + +import ( + "strconv" + "strings" + + "example.com/go-app/internal/processor" +) + +func FormatReport(report processor.Report) string { + lines := []string{ + "Project: " + report.ProjectName, + "Lines: " + formatCount(report.LineCount), + "Filtered:", + } + for _, line := range report.FilteredLines { + lines = append(lines, "- "+line) + } + + return strings.Join(lines, "\n") +} + +func formatCount(value int) string { + return strconv.Itoa(value) +} +``` + +### internal/processor/processor.go +```go +package processor + +type Report struct { + ProjectName string + LineCount int + FilteredLines []string +} + +func BuildReport(projectName string, lines []string, minLineLength int) Report { + filteredLines := make([]string, 0, len(lines)) + for _, line := range lines { + if len(line) >= minLineLength { + filteredLines = append(filteredLines, line) + } + } + + return Report{ + ProjectName: projectName, + LineCount: len(lines), + FilteredLines: filteredLines, + } +} +``` + +### internal/reader/reader.go +```go +package reader + +import ( + "os" + "strings" +) + +func ReadLines(path string) []string { + content, err := os.ReadFile(path) + if err != nil { + return []string{"alpha", "beta", "gamma", "delta"} + } + + lines := strings.Split(string(content), "\n") + result := make([]string, 0, len(lines)) + for _, line := range lines { + if line != "" { + result = append(result, line) + } + } + + return result +} +``` diff --git a/examples/go-app/README.md b/examples/go-app/README.md new file mode 100644 index 0000000..c4f0250 --- /dev/null +++ b/examples/go-app/README.md @@ -0,0 +1,3 @@ +# go-app + +Go app example. diff --git a/examples/go-app/cmd/app/main.go b/examples/go-app/cmd/app/main.go new file mode 100644 index 0000000..8be08dc --- /dev/null +++ b/examples/go-app/cmd/app/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + + "example.com/go-app/internal/config" + "example.com/go-app/internal/formatter" + "example.com/go-app/internal/processor" + "example.com/go-app/internal/reader" +) + +func main() { + appConfig := config.LoadConfig() + lines := reader.ReadLines(appConfig.InputPath) + report := processor.BuildReport(appConfig.ProjectName, lines, appConfig.MinLineLength) + fmt.Println(formatter.FormatReport(report)) +} diff --git a/examples/go-app/go.mod b/examples/go-app/go.mod new file mode 100644 index 0000000..0846999 --- /dev/null +++ b/examples/go-app/go.mod @@ -0,0 +1,3 @@ +module example.com/go-app + +go 1.22 diff --git a/examples/go-app/internal/config/config.go b/examples/go-app/internal/config/config.go new file mode 100644 index 0000000..f99560e --- /dev/null +++ b/examples/go-app/internal/config/config.go @@ -0,0 +1,15 @@ +package config + +type Config struct { + ProjectName string + InputPath string + MinLineLength int +} + +func LoadConfig() Config { + return Config{ + ProjectName: "go-app", + InputPath: "sample.txt", + MinLineLength: 3, + } +} diff --git a/examples/go-app/internal/formatter/formatter.go b/examples/go-app/internal/formatter/formatter.go new file mode 100644 index 0000000..10d2691 --- /dev/null +++ b/examples/go-app/internal/formatter/formatter.go @@ -0,0 +1,25 @@ +package formatter + +import ( + "strconv" + "strings" + + "example.com/go-app/internal/processor" +) + +func FormatReport(report processor.Report) string { + lines := []string{ + "Project: " + report.ProjectName, + "Lines: " + formatCount(report.LineCount), + "Filtered:", + } + for _, line := range report.FilteredLines { + lines = append(lines, "- "+line) + } + + return strings.Join(lines, "\n") +} + +func formatCount(value int) string { + return strconv.Itoa(value) +} diff --git a/examples/go-app/internal/processor/processor.go b/examples/go-app/internal/processor/processor.go new file mode 100644 index 0000000..fa12f7c --- /dev/null +++ b/examples/go-app/internal/processor/processor.go @@ -0,0 +1,22 @@ +package processor + +type Report struct { + ProjectName string + LineCount int + FilteredLines []string +} + +func BuildReport(projectName string, lines []string, minLineLength int) Report { + filteredLines := make([]string, 0, len(lines)) + for _, line := range lines { + if len(line) >= minLineLength { + filteredLines = append(filteredLines, line) + } + } + + return Report{ + ProjectName: projectName, + LineCount: len(lines), + FilteredLines: filteredLines, + } +} diff --git a/examples/go-app/internal/reader/reader.go b/examples/go-app/internal/reader/reader.go new file mode 100644 index 0000000..48e2a5d --- /dev/null +++ b/examples/go-app/internal/reader/reader.go @@ -0,0 +1,23 @@ +package reader + +import ( + "os" + "strings" +) + +func ReadLines(path string) []string { + content, err := os.ReadFile(path) + if err != nil { + return []string{"alpha", "beta", "gamma", "delta"} + } + + lines := strings.Split(string(content), "\n") + result := make([]string, 0, len(lines)) + for _, line := range lines { + if line != "" { + result = append(result, line) + } + } + + return result +} diff --git a/examples/java-app.md b/examples/java-app.md new file mode 100644 index 0000000..262162b --- /dev/null +++ b/examples/java-app.md @@ -0,0 +1,161 @@ + + +# Codebase Snapshot: java-app + +Generated: 2026-01-01 00:00 +Files: 7 | Directories: 6 + +--- + +## Directory Tree +```text +├── README.md +├── pom.xml +└── src + └── main + └── java + └── example + └── javaapp + ├── Main.java + ├── Config.java + ├── Formatter.java + ├── Processor.java + └── Reader.java +``` + +--- + +## Source Files + +### README.md +```md +# java-app + +Java app example. +``` + +### pom.xml +```xml + + + 4.0.0 + + example + java-app + 0.1.0 + jar + Java app example project. + + + 17 + 17 + UTF-8 + + +``` + +### src/main/java/example/javaapp/Main.java +```java +package example.javaapp; + +public final class Main { + + private Main() {} + + public static void main(String[] args) { + AppConfig config = Config.loadConfig(); + var lines = Reader.readLines(config.inputPath()); + var report = Processor.buildReport(config.projectName(), lines, config.minLineLength()); + System.out.println(Formatter.formatReport(report)); + } +} +``` + +### src/main/java/example/javaapp/Config.java +```java +package example.javaapp; + +public record AppConfig(String projectName, String inputPath, int minLineLength) { + + public static AppConfig loadConfig() { + return new AppConfig("java-app", "sample.txt", 3); + } +} +``` + +### src/main/java/example/javaapp/Formatter.java +```java +package example.javaapp; + +import java.util.StringJoiner; + +import example.javaapp.Processor.Report; + +public final class Formatter { + + private Formatter() {} + + public static String formatReport(Report report) { + StringJoiner joiner = new StringJoiner("\n"); + joiner.add("Project: " + report.projectName()); + joiner.add("Lines: " + report.lineCount()); + joiner.add("Filtered:"); + for (String line : report.filteredLines()) { + joiner.add("- " + line); + } + return joiner.toString(); + } +} +``` + +### src/main/java/example/javaapp/Processor.java +```java +package example.javaapp; + +import java.util.List; +import java.util.stream.Collectors; + +public final class Processor { + + private Processor() {} + + public record Report(String projectName, int lineCount, List filteredLines) {} + + public static Report buildReport(String projectName, List lines, int minLineLength) { + List filteredLines = lines.stream() + .filter(line -> line.length() >= minLineLength) + .collect(Collectors.toList()); + return new Report(projectName, lines.size(), filteredLines); + } +} +``` + +### src/main/java/example/javaapp/Reader.java +```java +package example.javaapp; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public final class Reader { + + private Reader() {} + + public static List readLines(String path) { + try { + String content = Files.readString(Path.of(path)); + return Arrays.stream(content.split("\n")) + .filter(line -> !line.trim().isEmpty()) + .collect(Collectors.toList()); + } catch (IOException e) { + return List.of("alpha", "beta", "gamma", "delta"); + } + } +} +``` diff --git a/examples/java-app/README.md b/examples/java-app/README.md new file mode 100644 index 0000000..d6add4c --- /dev/null +++ b/examples/java-app/README.md @@ -0,0 +1,3 @@ +# java-app + +Java app example. diff --git a/examples/java-app/pom.xml b/examples/java-app/pom.xml new file mode 100644 index 0000000..d989f5a --- /dev/null +++ b/examples/java-app/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + example + java-app + 0.1.0 + jar + Java app example project. + + + 17 + 17 + UTF-8 + + diff --git a/examples/java-app/src/main/java/example/javaapp/Config.java b/examples/java-app/src/main/java/example/javaapp/Config.java new file mode 100644 index 0000000..b024f5c --- /dev/null +++ b/examples/java-app/src/main/java/example/javaapp/Config.java @@ -0,0 +1,8 @@ +package example.javaapp; + +public record AppConfig(String projectName, String inputPath, int minLineLength) { + + public static AppConfig loadConfig() { + return new AppConfig("java-app", "sample.txt", 3); + } +} diff --git a/examples/java-app/src/main/java/example/javaapp/Formatter.java b/examples/java-app/src/main/java/example/javaapp/Formatter.java new file mode 100644 index 0000000..62a7434 --- /dev/null +++ b/examples/java-app/src/main/java/example/javaapp/Formatter.java @@ -0,0 +1,21 @@ +package example.javaapp; + +import java.util.StringJoiner; + +import example.javaapp.Processor.Report; + +public final class Formatter { + + private Formatter() {} + + public static String formatReport(Report report) { + StringJoiner joiner = new StringJoiner("\n"); + joiner.add("Project: " + report.projectName()); + joiner.add("Lines: " + report.lineCount()); + joiner.add("Filtered:"); + for (String line : report.filteredLines()) { + joiner.add("- " + line); + } + return joiner.toString(); + } +} diff --git a/examples/java-app/src/main/java/example/javaapp/Main.java b/examples/java-app/src/main/java/example/javaapp/Main.java new file mode 100644 index 0000000..8d9566a --- /dev/null +++ b/examples/java-app/src/main/java/example/javaapp/Main.java @@ -0,0 +1,13 @@ +package example.javaapp; + +public final class Main { + + private Main() {} + + public static void main(String[] args) { + AppConfig config = Config.loadConfig(); + var lines = Reader.readLines(config.inputPath()); + var report = Processor.buildReport(config.projectName(), lines, config.minLineLength()); + System.out.println(Formatter.formatReport(report)); + } +} diff --git a/examples/java-app/src/main/java/example/javaapp/Processor.java b/examples/java-app/src/main/java/example/javaapp/Processor.java new file mode 100644 index 0000000..154b314 --- /dev/null +++ b/examples/java-app/src/main/java/example/javaapp/Processor.java @@ -0,0 +1,18 @@ +package example.javaapp; + +import java.util.List; +import java.util.stream.Collectors; + +public final class Processor { + + private Processor() {} + + public record Report(String projectName, int lineCount, List filteredLines) {} + + public static Report buildReport(String projectName, List lines, int minLineLength) { + List filteredLines = lines.stream() + .filter(line -> line.length() >= minLineLength) + .collect(Collectors.toList()); + return new Report(projectName, lines.size(), filteredLines); + } +} diff --git a/examples/java-app/src/main/java/example/javaapp/Reader.java b/examples/java-app/src/main/java/example/javaapp/Reader.java new file mode 100644 index 0000000..0175e91 --- /dev/null +++ b/examples/java-app/src/main/java/example/javaapp/Reader.java @@ -0,0 +1,24 @@ +package example.javaapp; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public final class Reader { + + private Reader() {} + + public static List readLines(String path) { + try { + String content = Files.readString(Path.of(path)); + return Arrays.stream(content.split("\n")) + .filter(line -> !line.trim().isEmpty()) + .collect(Collectors.toList()); + } catch (IOException e) { + return List.of("alpha", "beta", "gamma", "delta"); + } + } +} diff --git a/examples/python-app.md b/examples/python-app.md new file mode 100644 index 0000000..ee11fd0 --- /dev/null +++ b/examples/python-app.md @@ -0,0 +1,154 @@ + + +# Codebase Snapshot: python-app + +Generated: 2026-01-01 00:00 +Files: 8 | Directories: 3 + +--- + +## Directory Tree +```text +├── README.md +├── pyproject.toml +└── src + └── python_app + ├── main.py + ├── config.py + ├── __init__.py + ├── formatter.py + ├── processor.py + └── reader.py +``` + +--- + +## Source Files + +### README.md +```md +# python-app + +Python app example. +``` + +### pyproject.toml +```toml +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-app" +version = "0.1.0" +description = "Compact example project for fln." +readme = "README.md" +requires-python = ">=3.10" + +[tool.setuptools.packages.find] +where = ["src"] +``` + +### src/python_app/main.py +```py +from .config import loadConfig +from .formatter import formatReport +from .processor import buildReport +from .reader import readLines + + +def run() -> str: + config = loadConfig() + lines = readLines(config.inputPath) + report = buildReport(config.projectName, lines, config.minLineLength) + return formatReport(report) + + +def main() -> None: + print(run()) + + +if __name__ == "__main__": + main() +``` + +### src/python_app/config.py +```py +from dataclasses import dataclass + + +@dataclass(frozen=True) +class AppConfig: + projectName: str + inputPath: str + minLineLength: int + + +def loadConfig() -> AppConfig: + return AppConfig( + projectName="python-app", + inputPath="sample.txt", + minLineLength=3 + ) +``` + +### src/python_app/__init__.py +```py +from .main import run + +__all__ = ["run"] +``` + +### src/python_app/formatter.py +```py +from .processor import Report + + +def formatReport(report: Report) -> str: + lines = [ + f"Project: {report.projectName}", + f"Lines: {report.lineCount}", + "Filtered:" + ] + lines.extend([f"- {line}" for line in report.filteredLines]) + return "\n".join(lines) +``` + +### src/python_app/processor.py +```py +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Report: + projectName: str + lineCount: int + filteredLines: list[str] + + +def buildReport(projectName: str, lines: list[str], minLineLength: int) -> Report: + filteredLines = [line for line in lines if len(line) >= minLineLength] + return Report( + projectName=projectName, + lineCount=len(lines), + filteredLines=filteredLines + ) +``` + +### src/python_app/reader.py +```py +from pathlib import Path + + +def readLines(path: str) -> list[str]: + filePath = Path(path) + if filePath.is_file(): + return filePath.read_text(encoding="utf-8").splitlines() + + return [ + "alpha", + "beta", + "gamma", + "delta" + ] +``` diff --git a/examples/python-app/README.md b/examples/python-app/README.md new file mode 100644 index 0000000..49567eb --- /dev/null +++ b/examples/python-app/README.md @@ -0,0 +1,3 @@ +# python-app + +Python app example. diff --git a/examples/python-app/pyproject.toml b/examples/python-app/pyproject.toml new file mode 100644 index 0000000..575a372 --- /dev/null +++ b/examples/python-app/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools>=68.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "python-app" +version = "0.1.0" +description = "Compact example project for fln." +readme = "README.md" +requires-python = ">=3.10" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/examples/python-app/src/python_app/__init__.py b/examples/python-app/src/python_app/__init__.py new file mode 100644 index 0000000..9c4f0db --- /dev/null +++ b/examples/python-app/src/python_app/__init__.py @@ -0,0 +1,3 @@ +from .main import run + +__all__ = ["run"] diff --git a/examples/python-app/src/python_app/config.py b/examples/python-app/src/python_app/config.py new file mode 100644 index 0000000..6d4d7f0 --- /dev/null +++ b/examples/python-app/src/python_app/config.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class AppConfig: + projectName: str + inputPath: str + minLineLength: int + + +def loadConfig() -> AppConfig: + return AppConfig( + projectName="python-app", + inputPath="sample.txt", + minLineLength=3 + ) diff --git a/examples/python-app/src/python_app/formatter.py b/examples/python-app/src/python_app/formatter.py new file mode 100644 index 0000000..4e9c141 --- /dev/null +++ b/examples/python-app/src/python_app/formatter.py @@ -0,0 +1,11 @@ +from .processor import Report + + +def formatReport(report: Report) -> str: + lines = [ + f"Project: {report.projectName}", + f"Lines: {report.lineCount}", + "Filtered:" + ] + lines.extend([f"- {line}" for line in report.filteredLines]) + return "\n".join(lines) diff --git a/examples/python-app/src/python_app/main.py b/examples/python-app/src/python_app/main.py new file mode 100644 index 0000000..f8cdc45 --- /dev/null +++ b/examples/python-app/src/python_app/main.py @@ -0,0 +1,19 @@ +from .config import loadConfig +from .formatter import formatReport +from .processor import buildReport +from .reader import readLines + + +def run() -> str: + config = loadConfig() + lines = readLines(config.inputPath) + report = buildReport(config.projectName, lines, config.minLineLength) + return formatReport(report) + + +def main() -> None: + print(run()) + + +if __name__ == "__main__": + main() diff --git a/examples/python-app/src/python_app/processor.py b/examples/python-app/src/python_app/processor.py new file mode 100644 index 0000000..4fa1293 --- /dev/null +++ b/examples/python-app/src/python_app/processor.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Report: + projectName: str + lineCount: int + filteredLines: list[str] + + +def buildReport(projectName: str, lines: list[str], minLineLength: int) -> Report: + filteredLines = [line for line in lines if len(line) >= minLineLength] + return Report( + projectName=projectName, + lineCount=len(lines), + filteredLines=filteredLines + ) diff --git a/examples/python-app/src/python_app/reader.py b/examples/python-app/src/python_app/reader.py new file mode 100644 index 0000000..042f960 --- /dev/null +++ b/examples/python-app/src/python_app/reader.py @@ -0,0 +1,14 @@ +from pathlib import Path + + +def readLines(path: str) -> list[str]: + filePath = Path(path) + if filePath.is_file(): + return filePath.read_text(encoding="utf-8").splitlines() + + return [ + "alpha", + "beta", + "gamma", + "delta" + ] diff --git a/examples/rust-app.md b/examples/rust-app.md new file mode 100644 index 0000000..c7cee7e --- /dev/null +++ b/examples/rust-app.md @@ -0,0 +1,147 @@ + + +# Codebase Snapshot: rust-app + +Generated: 2026-01-01 00:00 +Files: 8 | Directories: 2 + +--- + +## Directory Tree +```text +├── README.md +├── Cargo.toml +└── src + ├── lib.rs + ├── main.rs + ├── config.rs + ├── formatter.rs + ├── processor.rs + └── reader.rs +``` + +--- + +## Source Files + +### README.md +```md +# rust-app + +Rust app example. +``` + +### Cargo.toml +```toml +[package] +name = "rust-app" +version = "0.1.0" +edition = "2021" +description = "Compact Rust example project for fln." + +[dependencies] +``` + +### src/lib.rs +```rs +pub mod config; +pub mod formatter; +pub mod processor; +pub mod reader; +``` + +### src/main.rs +```rs +use rust_app::config::loadConfig; +use rust_app::formatter::formatReport; +use rust_app::processor::buildReport; +use rust_app::reader::readLines; + +fn main() { + let config = loadConfig(); + let lines = readLines(&config.inputPath); + let report = buildReport(config.projectName, lines, config.minLineLength); + println!("{}", formatReport(report)); +} +``` + +### src/config.rs +```rs +pub struct AppConfig { + pub projectName: String, + pub inputPath: String, + pub minLineLength: usize, +} + +pub fn loadConfig() -> AppConfig { + AppConfig { + projectName: "rust-app".to_string(), + inputPath: "sample.txt".to_string(), + minLineLength: 3, + } +} +``` + +### src/formatter.rs +```rs +use crate::processor::Report; + +pub fn formatReport(report: Report) -> String { + let mut lines = vec![ + format!("Project: {}", report.projectName), + format!("Lines: {}", report.lineCount), + "Filtered:".to_string(), + ]; + for line in report.filteredLines { + lines.push(format!("- {}", line)); + } + + lines.join("\n") +} +``` + +### src/processor.rs +```rs +pub struct Report { + pub projectName: String, + pub lineCount: usize, + pub filteredLines: Vec, +} + +pub fn buildReport(projectName: String, lines: Vec, minLineLength: usize) -> Report { + let filteredLines = lines + .iter() + .filter(|line| line.len() >= minLineLength) + .map(|line| line.to_string()) + .collect::>(); + + Report { + projectName, + lineCount: lines.len(), + filteredLines, + } +} +``` + +### src/reader.rs +```rs +use std::fs; + +pub fn readLines(path: &str) -> Vec { + let content = fs::read_to_string(path); + if let Ok(text) = content { + return text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.to_string()) + .collect(); + } + + vec![ + "alpha".to_string(), + "beta".to_string(), + "gamma".to_string(), + "delta".to_string(), + ] +} +``` diff --git a/examples/rust-app/Cargo.toml b/examples/rust-app/Cargo.toml new file mode 100644 index 0000000..91f7745 --- /dev/null +++ b/examples/rust-app/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "rust-app" +version = "0.1.0" +edition = "2021" +description = "Compact Rust example project for fln." + +[dependencies] diff --git a/examples/rust-app/README.md b/examples/rust-app/README.md new file mode 100644 index 0000000..5ca6587 --- /dev/null +++ b/examples/rust-app/README.md @@ -0,0 +1,3 @@ +# rust-app + +Rust app example. diff --git a/examples/rust-app/src/config.rs b/examples/rust-app/src/config.rs new file mode 100644 index 0000000..4a4561f --- /dev/null +++ b/examples/rust-app/src/config.rs @@ -0,0 +1,13 @@ +pub struct AppConfig { + pub projectName: String, + pub inputPath: String, + pub minLineLength: usize, +} + +pub fn loadConfig() -> AppConfig { + AppConfig { + projectName: "rust-app".to_string(), + inputPath: "sample.txt".to_string(), + minLineLength: 3, + } +} diff --git a/examples/rust-app/src/formatter.rs b/examples/rust-app/src/formatter.rs new file mode 100644 index 0000000..1a76c5f --- /dev/null +++ b/examples/rust-app/src/formatter.rs @@ -0,0 +1,14 @@ +use crate::processor::Report; + +pub fn formatReport(report: Report) -> String { + let mut lines = vec![ + format!("Project: {}", report.projectName), + format!("Lines: {}", report.lineCount), + "Filtered:".to_string(), + ]; + for line in report.filteredLines { + lines.push(format!("- {}", line)); + } + + lines.join("\n") +} diff --git a/examples/rust-app/src/lib.rs b/examples/rust-app/src/lib.rs new file mode 100644 index 0000000..5a21712 --- /dev/null +++ b/examples/rust-app/src/lib.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod formatter; +pub mod processor; +pub mod reader; diff --git a/examples/rust-app/src/main.rs b/examples/rust-app/src/main.rs new file mode 100644 index 0000000..4d135db --- /dev/null +++ b/examples/rust-app/src/main.rs @@ -0,0 +1,11 @@ +use rust_app::config::loadConfig; +use rust_app::formatter::formatReport; +use rust_app::processor::buildReport; +use rust_app::reader::readLines; + +fn main() { + let config = loadConfig(); + let lines = readLines(&config.inputPath); + let report = buildReport(config.projectName, lines, config.minLineLength); + println!("{}", formatReport(report)); +} diff --git a/examples/rust-app/src/processor.rs b/examples/rust-app/src/processor.rs new file mode 100644 index 0000000..62e5e78 --- /dev/null +++ b/examples/rust-app/src/processor.rs @@ -0,0 +1,19 @@ +pub struct Report { + pub projectName: String, + pub lineCount: usize, + pub filteredLines: Vec, +} + +pub fn buildReport(projectName: String, lines: Vec, minLineLength: usize) -> Report { + let filteredLines = lines + .iter() + .filter(|line| line.len() >= minLineLength) + .map(|line| line.to_string()) + .collect::>(); + + Report { + projectName, + lineCount: lines.len(), + filteredLines, + } +} diff --git a/examples/rust-app/src/reader.rs b/examples/rust-app/src/reader.rs new file mode 100644 index 0000000..fb45524 --- /dev/null +++ b/examples/rust-app/src/reader.rs @@ -0,0 +1,19 @@ +use std::fs; + +pub fn readLines(path: &str) -> Vec { + let content = fs::read_to_string(path); + if let Ok(text) = content { + return text + .lines() + .filter(|line| !line.trim().is_empty()) + .map(|line| line.to_string()) + .collect(); + } + + vec![ + "alpha".to_string(), + "beta".to_string(), + "gamma".to_string(), + "delta".to_string(), + ] +} diff --git a/examples/ts-app.md b/examples/ts-app.md new file mode 100644 index 0000000..faa991e --- /dev/null +++ b/examples/ts-app.md @@ -0,0 +1,143 @@ + + +# Codebase Snapshot: ts-app + +Generated: 2026-01-01 00:00 +Files: 8 | Directories: 2 + +--- + +## Directory Tree +```text +├── README.md +├── package.json +├── tsconfig.json +└── src + ├── index.ts + ├── config.ts + ├── formatter.ts + ├── processor.ts + └── reader.ts +``` + +--- + +## Source Files + +### README.md +```md +# ts-app + +TypeScript app example. +``` + +### package.json +```json +{ + "name": "ts-app", + "version": "0.1.0", + "type": "module", + "description": "Compact TypeScript example project for fln.", + "scripts": { + "start": "node ./dist/index.js" + } +} +``` + +### tsconfig.json +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "strict": true, + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ] +} +``` + +### src/index.ts +```ts +import { loadConfig } from "./config"; +import { formatReport } from "./formatter"; +import { buildReport } from "./processor"; +import { readLines } from "./reader"; + +async function main(): Promise { + const config = loadConfig(); + const lines = await readLines(config.inputPath); + console.log(formatReport(buildReport(config.projectName, lines, config.minLineLength))); +} + +void main(); +``` + +### src/config.ts +```ts +export type AppConfig = { + projectName: string; + inputPath: string; + minLineLength: number; +}; + +export const loadConfig = (): AppConfig => ({ + projectName: "ts-app", + inputPath: "sample.txt", + minLineLength: 3 +}); +``` + +### src/formatter.ts +```ts +import type { Report } from "./processor"; + +export const formatReport = (report: Report): string => { + const lines = [ + `Project: ${report.projectName}`, + `Lines: ${report.lineCount}`, + "Filtered:" + ]; + lines.push(...report.filteredLines.map((line) => `- ${line}`)); + return lines.join("\n"); +}; +``` + +### src/processor.ts +```ts +export type Report = { + projectName: string; + lineCount: number; + filteredLines: string[]; +}; + +export const buildReport = ( + projectName: string, + lines: string[], + minLineLength: number +): Report => { + const filteredLines = lines.filter((line) => line.length >= minLineLength); + return { + projectName, + lineCount: lines.length, + filteredLines + }; +}; +``` + +### src/reader.ts +```ts +import { readFile } from "node:fs/promises"; + +export const readLines = async (path: string): Promise => { + try { + const content = await readFile(path, "utf-8"); + return content.split("\n").filter((line) => line.trim().length > 0); + } catch { + return [ "alpha", "beta", "gamma", "delta" ]; + } +}; +``` diff --git a/examples/ts-app/README.md b/examples/ts-app/README.md new file mode 100644 index 0000000..9b6a599 --- /dev/null +++ b/examples/ts-app/README.md @@ -0,0 +1,3 @@ +# ts-app + +TypeScript app example. diff --git a/examples/ts-app/package.json b/examples/ts-app/package.json new file mode 100644 index 0000000..aa45b0c --- /dev/null +++ b/examples/ts-app/package.json @@ -0,0 +1,9 @@ +{ + "name": "ts-app", + "version": "0.1.0", + "type": "module", + "description": "Compact TypeScript example project for fln.", + "scripts": { + "start": "node ./dist/index.js" + } +} diff --git a/examples/ts-app/src/config.ts b/examples/ts-app/src/config.ts new file mode 100644 index 0000000..e47f3c6 --- /dev/null +++ b/examples/ts-app/src/config.ts @@ -0,0 +1,11 @@ +export type AppConfig = { + projectName: string; + inputPath: string; + minLineLength: number; +}; + +export const loadConfig = (): AppConfig => ({ + projectName: "ts-app", + inputPath: "sample.txt", + minLineLength: 3 +}); diff --git a/examples/ts-app/src/formatter.ts b/examples/ts-app/src/formatter.ts new file mode 100644 index 0000000..ca8f3cc --- /dev/null +++ b/examples/ts-app/src/formatter.ts @@ -0,0 +1,11 @@ +import type { Report } from "./processor"; + +export const formatReport = (report: Report): string => { + const lines = [ + `Project: ${report.projectName}`, + `Lines: ${report.lineCount}`, + "Filtered:" + ]; + lines.push(...report.filteredLines.map((line) => `- ${line}`)); + return lines.join("\n"); +}; diff --git a/examples/ts-app/src/index.ts b/examples/ts-app/src/index.ts new file mode 100644 index 0000000..1d07397 --- /dev/null +++ b/examples/ts-app/src/index.ts @@ -0,0 +1,12 @@ +import { loadConfig } from "./config"; +import { formatReport } from "./formatter"; +import { buildReport } from "./processor"; +import { readLines } from "./reader"; + +async function main(): Promise { + const config = loadConfig(); + const lines = await readLines(config.inputPath); + console.log(formatReport(buildReport(config.projectName, lines, config.minLineLength))); +} + +void main(); diff --git a/examples/ts-app/src/processor.ts b/examples/ts-app/src/processor.ts new file mode 100644 index 0000000..8aabe92 --- /dev/null +++ b/examples/ts-app/src/processor.ts @@ -0,0 +1,18 @@ +export type Report = { + projectName: string; + lineCount: number; + filteredLines: string[]; +}; + +export const buildReport = ( + projectName: string, + lines: string[], + minLineLength: number +): Report => { + const filteredLines = lines.filter((line) => line.length >= minLineLength); + return { + projectName, + lineCount: lines.length, + filteredLines + }; +}; diff --git a/examples/ts-app/src/reader.ts b/examples/ts-app/src/reader.ts new file mode 100644 index 0000000..16c046c --- /dev/null +++ b/examples/ts-app/src/reader.ts @@ -0,0 +1,10 @@ +import { readFile } from "node:fs/promises"; + +export const readLines = async (path: string): Promise => { + try { + const content = await readFile(path, "utf-8"); + return content.split("\n").filter((line) => line.trim().length > 0); + } catch { + return [ "alpha", "beta", "gamma", "delta" ]; + } +}; diff --git a/examples/ts-app/tsconfig.json b/examples/ts-app/tsconfig.json new file mode 100644 index 0000000..3f49e62 --- /dev/null +++ b/examples/ts-app/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler", + "strict": true, + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/package.json b/package.json index 7ad5b32..38bcb71 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,8 @@ "build:npm": "tsc -p tsconfig.build.json", "clean": "rm -rf fln dist", "dev": "bun run src/cli/main.ts", - "generate:version": "bun run generate-version.ts", + "generate:examples": "bun run scripts/generate-examples.ts", + "generate:version": "bun run scripts/generate-version.ts", "install:system": "bun run build && echo 'Requesting sudo to move binary to /usr/local/bin...' && sudo mv ./fln /usr/local/bin/fln && echo 'Installed successfully. Run \"fln --help\" to test.'", "install:user": "bun run build && mkdir -p \"$HOME/.local/bin\" && mv ./fln \"$HOME/.local/bin/fln\" && echo 'Installed successfully. Run \"fln --help\" to test.'", "lint": "eslint .", diff --git a/scripts/generate-examples.ts b/scripts/generate-examples.ts new file mode 100644 index 0000000..a0f2f3a --- /dev/null +++ b/scripts/generate-examples.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env bun + +import { readdirSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { fln } from "../src/api/fln"; + + +const root = resolve(import.meta.dir, ".."); +const examplesDir = join(root, "examples"); + +const entries = readdirSync(examplesDir, { withFileTypes: true }); +const names = entries.filter(entry => entry.isDirectory()).map(entry => entry.name); + +for (const name of names) { + await fln({ + rootDirectory: join(examplesDir, name), + outputFile: join(examplesDir, `${name}.md`), + generatedDate: "2026-01-01 00:00" + }); + + console.info(`Generated ${join("examples", `${name}.md`)}`); +} + +console.info(`Done. ${names.length} snapshot(s) written.`); diff --git a/generate-version.ts b/scripts/generate-version.ts similarity index 51% rename from generate-version.ts rename to scripts/generate-version.ts index 8bbae23..d147b9a 100644 --- a/generate-version.ts +++ b/scripts/generate-version.ts @@ -1,17 +1,20 @@ #!/usr/bin/env bun import { writeFileSync } from "node:fs"; -import packageJson from "./package.json" with { type: "json" }; +import { resolve } from "node:path"; +import packageJson from "../package.json" with { type: "json" }; +const root = resolve(import.meta.dir, ".."); + const code = // eslint-disable-next-line @stylistic/indent -`// Auto-generated by generate-version.ts +`// Auto-generated by scripts/generate-version.ts // Do not edit manually export const VERSION = "${packageJson.version}"; `; -writeFileSync("src/version.ts", code, "utf8"); +writeFileSync(resolve(root, "src/version.ts"), code, "utf8"); console.info(`✓ Generated version.ts with ${packageJson.version}`); diff --git a/tests/index.ts b/tests/index.ts deleted file mode 100644 index d0ab9de..0000000 --- a/tests/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./end-to-end.test"; -export * from "./size.test"; diff --git a/tsconfig.json b/tsconfig.json index 868bf52..b49b970 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,5 +18,6 @@ "$infra": [ "./src/infra/index.ts" ], "$version": [ "./src/version.ts" ] } - } + }, + "exclude": [ "examples" ] }