Onboarding Generator
/onboarding - analyzes a repository and generates a structured ONBOARDING.md (architecture, dev setup, key commands, test workflow, glossary). Runs an inventory script to build a language-aware structural map, reads only the files that map surfaces, and writes prose with strict voice rules so the output reads like a knowledgeable teammate rather than generated documentation.
Tags
README
Onboarding Generator
Generates a structured ONBOARDING.md for any repository from a language-aware code inventory. Ships a thin /onboarding slash command, a skill with strict voice rules, and the inventory script the skill depends on.
The output is orientation for contributors (human or agent), not end-user documentation. Always regenerates from scratch.
What This Module Provides
| Source | Target | Purpose |
|---|---|---|
commands/onboarding.md |
commands/onboarding.md |
/onboarding slash command entry point |
skills/onboarding/SKILL.md |
skills/onboarding/SKILL.md |
Voice rules, section contract, sanity checklist |
scripts/inventory.mjs |
scripts/inventory.mjs |
Node script producing JSON inventory (languages, frameworks, entry points, scripts, docs, infra, monorepo) |
The Output: ONBOARDING.md
Six sections, in this exact order:
- Overview - one paragraph, three sentences max.
- Architecture - concrete files and directories, ASCII diagram when two or more pieces interact.
- Dev Setup - prerequisites, install, env (link to
.env.example), first-run command. - Key Commands - short table of the commands a working engineer actually types.
- Test Workflow - unit / integration / e2e layout, commands, pre-push expectation.
- Glossary - five to fifteen project-specific terms, one sentence each.
Voice Rules
The skill enforces a specific voice. Output should read like a knowledgeable teammate explaining the repo over coffee:
- Direct, no hedges ("probably", "might", "should usually" are banned).
- Concrete over abstract - name files and directories, not "the middleware layer".
- Match codebase formality - read
CLAUDE.md/README.mdfirst, mirror the tone. - Never include secrets - link to
.env.example, never paste values. - Link instead of duplicate - if an answer lives in
docs/architecture.md, point there. - No preamble ("Welcome!") or filler close ("That should get you started!").
Full list in skills/onboarding/SKILL.md.
Usage
/onboarding # Inventory the current repo, write ONBOARDING.md at the root
/onboarding --dry-run # Print the markdown to stdout, do not write the file
/onboarding path/to/repo # Inventory a specific path
You can also run the inventory script directly to inspect the JSON:
node ~/.claude/scripts/inventory.mjs . --pretty
Stack Coverage
The inventory script detects:
- Languages: TypeScript, JavaScript, Python, Go, Rust, Ruby, Java, PHP, Swift, C/C++, Shell.
- JS frameworks: Vite, Webpack, Next.js, Nuxt, Remix, SvelteKit, Astro, Angular, React, Vue, Svelte, Solid.
- Styling / UI: Tailwind, styled-components.
- Backend / runtimes: Express, Fastify, Hono, Cloudflare Workers.
- Testing: Vitest, Jest, Playwright, Cypress, pytest, RSpec,
go test,cargo test. - Chrome extensions: detects
manifest.jsonwith MV2/MV3 structure, surfaces background / content / popup / options entry points. - Monorepos: npm/pnpm/yarn workspaces, turborepo, nx, lerna.
- Package managers: pnpm, npm, yarn, bun, poetry, pipenv, cargo, bundler.
- Infrastructure: Docker, docker-compose, Cloudflare Workers, Vercel, Netlify, GitHub Actions, CircleCI, Fly.io, Render, Heroku, Supabase, Terraform, Husky.
The script is bounded (depth 4, ignores node_modules, .git, dist, build output, caches). It never reads file contents beyond the manifests and config files it explicitly opens.
Manual Installation
mkdir -p ~/.claude/commands
mkdir -p ~/.claude/skills/onboarding
mkdir -p ~/.claude/scripts
cp modules/onboarding/commands/onboarding.md \
~/.claude/commands/onboarding.md
cp modules/onboarding/skills/onboarding/SKILL.md \
~/.claude/skills/onboarding/SKILL.md
cp modules/onboarding/scripts/inventory.mjs \
~/.claude/scripts/inventory.mjs
Requires node on $PATH. No other dependencies.
Design Notes
- Always regenerates from scratch. No diffing against a prior
ONBOARDING.md. Regeneration keeps the voice consistent and avoids fossilized claims that survive a refactor. - Inventory first, then read. The skill reads only the files the inventory surfaces - entry points, top-level docs, env example. It never globs the whole tree, never reads lockfiles, never opens
node_modules. - Separation of concerns. The command is a thin argument parser and runner. The skill holds the voice rules and section contract. The inventory script is pure structural analysis with no prose logic.
- No overlap with
/docupdate./docupdateaudits existing documentation for drift./onboardingwrites a single new file from scratch. They are complementary.
Source
Pattern adapted from EveryInc's compound-engineering plugin (skills/onboarding/SKILL.md, skills/onboarding/scripts/inventory.mjs). The original is Ruby-aware; this implementation adds explicit support for Vite, Next.js, Chrome extensions, and monorepo tooling (turborepo, nx, pnpm workspaces) that appear across the portfolio.
Will install
| Path | Action | Target | Type |
|---|---|---|---|
commands/onboarding.md | → | commands/onboarding.md | command |
skills/onboarding/SKILL.md | → | skills/onboarding/SKILL.md | skill |
scripts/inventory.mjs | → | scripts/inventory.mjs | script |
Dependencies
No dependencies.
Required by
No other module depends on this one.
Included in presets
Install this module
Agent prompt
Recommended for agent users -- hands the whole install off to your assistant.
Fetch https://cd23a9be.ccgm-site.pages.dev/modules/onboarding.md and install this module into my Claude Code setup.
Native plugin marketplace
One command via the native plugin marketplace -- additive, does not merge settings.json.
claude plugin install onboarding@ccgm
The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.
Files
command (1)
commands/onboarding.md
---
description: Generate a structured ONBOARDING.md for the current repo from a code inventory. Architecture, dev setup, key commands, test workflow, glossary. Always regenerates from scratch.
allowed-tools: Bash, Read, Write, Edit, Glob, Grep
argument-hint: "[path] [--dry-run]"
---
# /onboarding - Structured ONBOARDING.md Generator
Analyze the current repository and write `ONBOARDING.md` at the repo root. The file covers architecture, dev setup, key commands, test workflow, and a project glossary - sized for a new engineer (or a fresh Claude session) to get productive in under ten minutes.
Always regenerates from scratch. Never diffs against an existing ONBOARDING.md.
Follow the detailed instructions in `~/.claude/skills/onboarding/SKILL.md`. This command is the thin entry point; the skill holds the voice rules and the section contract.
---
## Phase 0: Parse Arguments
Extract from `$ARGUMENTS`:
- **Positional path** (optional): target repo root. Defaults to `$PWD`.
- **`--dry-run`**: print the generated markdown to stdout; do not write `ONBOARDING.md`.
If a path argument is present and is not a directory, stop and tell the user.
---
## Phase 1: Run the Inventory
Run the inventory script and capture the JSON output:
```bash
node ~/.claude/scripts/inventory.mjs "$TARGET_ROOT" --pretty
```
Read the returned JSON. If the script errors, or the output shows `languages: []` and `frameworks: []` and `scripts: {}`, STOP. Tell the user the repo looks too sparse for a meaningful onboarding doc and ask what they want covered instead.
Highlight inventory `notes[]` to the user if any are non-empty (for example "No README detected at repo root").
---
## Phase 2: Read What the Inventory Surfaces
Read only the files the inventory points at. The target set is:
- Every entry in `docs[]` (READMEs, architecture notes, CLAUDE.md, AGENTS.md, subdocs).
- The first one or two `entryPoints[]` that look like a human would start there.
- The `package.json` (or Cargo.toml / pyproject.toml / go.mod) for the scripts and dependency context already summarized in the inventory.
- The `envExample` file if present.
Do NOT glob the whole tree. Do NOT read lockfiles, generated output, node_modules, or .git.
For a monorepo, pick two or three representative workspace packages to sketch in Architecture. Do not enumerate every package.
---
## Phase 3: Write the Six Sections
Follow the section contract in the skill exactly. The output has six sections in this order:
1. **Overview** - one paragraph, three sentences max.
2. **Architecture** - concrete files and directories, ASCII diagram when two or more pieces interact.
3. **Dev Setup** - prerequisites, install, env (link to `.env.example`), first-run command.
4. **Key Commands** - short table of the commands a working engineer actually types.
5. **Test Workflow** - unit / integration / e2e layout, commands, pre-push expectation.
6. **Glossary** - five to fifteen project-specific terms, one sentence each.
Honor every voice rule from `~/.claude/skills/onboarding/SKILL.md`:
- Direct, no hedges, concrete over abstract.
- Match the formality of the existing `CLAUDE.md` / `README.md` tone.
- Never paste secrets, keys, or production URLs. Link to the env example.
- Link to existing docs instead of duplicating their content.
- No "Welcome!" preamble, no "That should get you started!" closer.
---
## Phase 4: Sanity Check
Before writing the file:
- [ ] Six sections present in the required order.
- [ ] No secrets or real URLs.
- [ ] No hedge words ("probably", "might", "should usually", "in most cases").
- [ ] File paths referenced exist in `entryPoints[]` or `docs[]` from the inventory.
- [ ] Under ~300 lines.
- [ ] Opens with `# Onboarding` or `# {name} Onboarding`, not "Welcome" or "This document...".
If any check fails, edit the draft before writing.
---
## Phase 5: Write or Print
If `--dry-run` was passed, print the markdown to stdout and stop.
Otherwise:
```bash
# Overwrite; regeneration is always from scratch.
# (Write tool, not bash heredoc - preserves exact content.)
```
Write the content to `${TARGET_ROOT}/ONBOARDING.md` using the Write tool.
Report to the user:
- File written (absolute path).
- Line count.
- Any inventory `notes[]` that were non-empty (gaps the user should know about).
Stop. Do not follow up with "Would you like me to..." - the file is the deliverable.
skill (1)
skills/onboarding/SKILL.md
---
name: onboarding
description: >
Generate a structured ONBOARDING.md for the current repository. Runs an inventory script to build a language-aware structural map (languages, frameworks, entry points, scripts, docs, infrastructure, monorepo layout), reads only the files the inventory surfaces, then writes prose that reads like a knowledgeable teammate over coffee - not generated documentation. Always regenerates from scratch; no diffing against the previous file.
Triggers: onboarding doc, write ONBOARDING.md, generate project onboarding, new engineer guide, fresh session primer.
---
# onboarding - Structured ONBOARDING.md Generator
Produce a single file at the repo root: `ONBOARDING.md`. The file is the deliverable. The skill does not modify any other documentation.
## When to Run
- A new engineer (or a fresh Claude session) will read this repo for the first time.
- The repo has grown enough that `README.md` alone no longer answers "how do I ship a change here today?"
- The project just crossed a refactor boundary and the old onboarding notes are stale.
- The user asks for "the five-minute tour" or an orientation doc.
Do NOT run for:
- Single-file utilities, one-off scripts, or projects without a meaningful internal structure.
- Private forks of well-documented upstreams (point the user at the upstream docs instead).
- Monorepo workspaces that already have per-package READMEs covering the same ground.
## Phase 0: Run the Inventory
Always start with the inventory script. It is cheap and the writer depends on its output.
```bash
node ~/.claude/scripts/inventory.mjs "$PWD" --pretty
```
If the command errors or returns a shallow inventory (no languages, no frameworks, no scripts), STOP and ask the user what kind of repo this is. Do not guess. A wrong onboarding doc is worse than no onboarding doc.
The inventory JSON contains:
- `name`, `description`, `languages`, `frameworks`, `packageManager`
- `monorepo` (isMonorepo, tool, workspaces)
- `entryPoints` (web, node-main, bin, ext-background, ext-content, python, go, rust)
- `scripts` (package.json scripts plus Makefile targets)
- `docs` (README, CLAUDE.md, docs/*, ARCHITECTURE.md, etc.)
- `infrastructure` (docker, cloudflare-workers, github-actions, husky, ...)
- `testRunners`, `envExample`, `topLevelDirs`, `notes`
## Phase 1: Read Only What the Inventory Surfaces
Read the specific files the inventory points at:
- Every doc in `docs[]` (they often already contain the answer; link, do not duplicate).
- `package.json` (or equivalent manifest) for the `scripts` and dependency context.
- `CLAUDE.md` and/or `AGENTS.md` at the repo root (the tone and project conventions live there).
- One or two entry points so architecture claims are grounded in real code.
- The env example file if present (for the "Setup" section).
Do NOT glob-read every source file. Do NOT read lockfiles. Do NOT read generated build output.
If the inventory lists a monorepo, pick the two or three workspace packages that look like the primary entry points (by scripts, entry points, or dependency surface) and sketch them. Do not enumerate every package.
## Phase 2: Write ONBOARDING.md
Always regenerate from scratch. Never diff against a prior ONBOARDING.md - regeneration keeps the voice consistent and avoids fossilized claims. If a prior ONBOARDING.md exists, overwrite it.
The output has six sections in this exact order:
1. **Overview** - one paragraph: what this repo is, who uses it, what it ships. Three sentences maximum.
2. **Architecture** - key components and how they connect. Use a small ASCII diagram when more than two pieces interact. Name concrete files and directories.
3. **Dev Setup** - prerequisites (languages, runtime versions), install command, env vars (link to `.env.example` - never paste values), first-run command.
4. **Key Commands** - a short table of the scripts a working engineer actually types. Trim noise (leave out `postinstall`, `prepare`, `lint:fix` unless meaningful). If a command has an obvious gotcha, note it in one clause.
5. **Test Workflow** - how tests are organized (unit / integration / e2e), how to run each tier, what counts as "tests pass before push."
6. **Glossary** - five to fifteen project-specific terms. Definitions in one sentence each. No generic terminology (do not define "TypeScript"). Lucas-specific portfolio terms like `clone`, `workspace`, `agent-id` go here when the repo uses them.
Link to files with relative paths. Use `file.ts:42` when pointing at a specific line.
## Voice Rules
These are non-negotiable. The output should read like a knowledgeable teammate explaining the repo over coffee.
- **Direct**: "Run `npm run dev`" not "You can run `npm run dev` to start the development server."
- **Cut hedges**: no "probably", "might", "should", "in most cases." If the claim needs a hedge, it is not confident enough to include.
- **Concrete over abstract**: "The auth middleware sets `req.user` from the JWT in `Authorization`" not "Authentication is handled by the middleware layer."
- **Match codebase formality**: if `CLAUDE.md` is terse and jokey, match it. If it is formal, match that. Read the existing tone before writing.
- **Never include secrets**: no example keys, no sample tokens, no production URLs. Link to `.env.example` and move on.
- **Link instead of duplicate**: if the answer lives in `docs/architecture.md`, point there. The goal is orientation, not a second copy of the docs.
- **No preamble**: the file opens with `# Onboarding` (or `# {name} Onboarding`) and goes straight to the Overview. No "Welcome!" No "This document will help you..."
- **No filler summaries**: do not close with "That should get you started!" End on the last useful sentence.
## Anti-Patterns to Refuse
Refuse to produce output in these shapes, even if the user asks. Say what you are doing differently and continue.
- A bulleted list of every file in the repo. Onboarding is orientation, not inventory.
- A rewrite of the README. If the README covers the answer, link to it.
- Step-by-step tutorials for how to use the product. Onboarding is for contributors, not end users.
- Marketing prose ("our cutting-edge platform"). This is an internal doc for engineers.
- Duplicate `.env` values. Ever. Link to the example file.
## Phase 3: Sanity Check
After writing, re-read the file once before finalizing:
- [ ] Every section is present in the required order.
- [ ] No secrets, keys, or real URLs appear.
- [ ] No hedges slipped in ("probably", "might", "should usually").
- [ ] Every file path referenced actually exists (cross-check against inventory `docs[]` / `entryPoints[]`).
- [ ] The doc is under ~300 lines. If longer, cut the Architecture diagram or the Glossary.
- [ ] Opening sentence does not start with "Welcome" or "This document."
Then write to `ONBOARDING.md` at the repo root. Report file path and line count. Done.
## Notes for the Calling Agent
- The skill is idempotent. Running it twice regenerates the same shape.
- The inventory script is deliberately bounded (depth 4, ignores build output). For a very large monorepo (dozens of workspaces), let the inventory finish, then pick a small, interesting subset rather than re-expanding the scan.
- If the user wants a preview, pass `--dry-run` to the containing command - the skill prints the draft to stdout and skips the write.
script (1)
scripts/inventory.mjs
#!/usr/bin/env node
// inventory.mjs - Language-aware structural map of a repository.
//
// Reads a repo root (default: cwd) and prints a JSON object to stdout
// describing languages, frameworks, entry points, scripts, existing docs,
// infrastructure, and monorepo structure.
//
// The /onboarding command consumes this JSON. It is intentionally cheap:
// directory walks are bounded by depth, every glob is scoped, binary and
// lock files are skipped. The script never reads file contents beyond the
// set of manifests and config files it explicitly opens.
//
// Usage:
// node inventory.mjs # inventory cwd
// node inventory.mjs /path/to/repo # inventory another path
// node inventory.mjs --pretty # pretty-print JSON
//
// Exit codes:
// 0 - ok (JSON printed)
// 1 - fatal error (root path missing, etc.)
import { readFileSync, existsSync, statSync, readdirSync } from "node:fs";
import { join, basename, relative, resolve } from "node:path";
// ---------- Config ----------
const MAX_DEPTH = 4;
const IGNORED_DIRS = new Set([
"node_modules",
".git",
".next",
".nuxt",
".svelte-kit",
".turbo",
".cache",
".vercel",
".wrangler",
"dist",
"build",
"out",
"coverage",
".venv",
"venv",
"__pycache__",
".pytest_cache",
"target",
"vendor",
"tmp",
]);
// ---------- CLI ----------
const args = process.argv.slice(2);
const pretty = args.includes("--pretty");
const positional = args.filter((a) => !a.startsWith("--"));
const rootArg = positional[0] || process.cwd();
const ROOT = resolve(rootArg);
if (!existsSync(ROOT) || !statSync(ROOT).isDirectory()) {
process.stderr.write(`inventory: not a directory: ${ROOT}\n`);
process.exit(1);
}
// ---------- Helpers ----------
function safeRead(path) {
try {
return readFileSync(path, "utf8");
} catch {
return null;
}
}
function safeJson(path) {
const raw = safeRead(path);
if (!raw) return null;
try {
return JSON.parse(raw);
} catch {
return null;
}
}
function exists(rel) {
return existsSync(join(ROOT, rel));
}
function firstExisting(paths) {
for (const p of paths) if (exists(p)) return p;
return null;
}
function listTopLevelDirs() {
try {
return readdirSync(ROOT, { withFileTypes: true })
.filter((d) => d.isDirectory() && !IGNORED_DIRS.has(d.name) && !d.name.startsWith("."))
.map((d) => d.name)
.sort();
} catch {
return [];
}
}
function walk(dir, depth, out, filter) {
if (depth > MAX_DEPTH) return;
let entries;
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
if (IGNORED_DIRS.has(e.name)) continue;
const full = join(dir, e.name);
if (e.isDirectory()) {
walk(full, depth + 1, out, filter);
} else if (e.isFile() && filter(e.name, full)) {
out.push(relative(ROOT, full));
}
}
}
function globFiles(filter) {
const out = [];
walk(ROOT, 0, out, filter);
return out.sort();
}
// ---------- Detection ----------
function detectLanguages() {
const langs = new Set();
if (exists("package.json")) langs.add("javascript");
if (exists("tsconfig.json") || globFiles((n) => n.endsWith(".ts") || n.endsWith(".tsx")).length > 0) {
langs.add("typescript");
}
if (exists("Cargo.toml")) langs.add("rust");
if (exists("pyproject.toml") || exists("requirements.txt") || exists("setup.py")) langs.add("python");
if (exists("go.mod")) langs.add("go");
if (exists("Gemfile")) langs.add("ruby");
if (exists("composer.json")) langs.add("php");
if (exists("pom.xml") || exists("build.gradle") || exists("build.gradle.kts")) langs.add("java");
if (exists("Package.swift")) langs.add("swift");
if (globFiles((n) => n === "CMakeLists.txt" || n.endsWith(".cpp") || n.endsWith(".cc")).length > 0) {
langs.add("cpp");
}
if (globFiles((n) => n.endsWith(".sh")).length > 0) langs.add("shell");
return [...langs].sort();
}
function detectFrameworks(pkg) {
const fw = new Set();
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
// Build tools
if (deps["vite"]) fw.add("vite");
if (deps["webpack"]) fw.add("webpack");
if (deps["next"]) fw.add("next.js");
if (deps["nuxt"] || deps["nuxt3"]) fw.add("nuxt");
if (deps["@remix-run/react"] || deps["@remix-run/node"]) fw.add("remix");
if (deps["@sveltejs/kit"]) fw.add("sveltekit");
if (deps["astro"]) fw.add("astro");
if (deps["@angular/core"]) fw.add("angular");
// UI
if (deps["react"]) fw.add("react");
if (deps["react-dom"]) fw.add("react-dom");
if (deps["vue"]) fw.add("vue");
if (deps["svelte"]) fw.add("svelte");
if (deps["solid-js"]) fw.add("solid");
// Styling
if (deps["tailwindcss"]) fw.add("tailwind");
if (deps["styled-components"]) fw.add("styled-components");
// Backend / runtimes
if (deps["express"]) fw.add("express");
if (deps["fastify"]) fw.add("fastify");
if (deps["hono"]) fw.add("hono");
if (deps["wrangler"] || exists("wrangler.toml") || exists("wrangler.jsonc")) fw.add("cloudflare-workers");
if (deps["@cloudflare/workers-types"]) fw.add("cloudflare-workers");
// Testing
if (deps["vitest"]) fw.add("vitest");
if (deps["jest"]) fw.add("jest");
if (deps["@playwright/test"] || deps["playwright"]) fw.add("playwright");
if (deps["cypress"]) fw.add("cypress");
// Backend-as-a-service
if (deps["@supabase/supabase-js"] || exists("supabase")) fw.add("supabase");
if (deps["firebase"] || deps["firebase-admin"]) fw.add("firebase");
// Chrome extension detection (root, plus common CRX manifest homes)
const manifestCandidates = ["manifest.json", "public/manifest.json", "static/manifest.json", "src/manifest.json"];
for (const mp of manifestCandidates) {
if (!exists(mp)) continue;
const m = safeJson(join(ROOT, mp));
if (m && (m.manifest_version || m.background || m.content_scripts)) {
fw.add("chrome-extension");
break;
}
}
// Python frameworks
const reqs = safeRead("requirements.txt") || "";
const pyproject = safeRead("pyproject.toml") || "";
const pyBlob = reqs + "\n" + pyproject;
if (/\bdjango\b/i.test(pyBlob)) fw.add("django");
if (/\bflask\b/i.test(pyBlob)) fw.add("flask");
if (/\bfastapi\b/i.test(pyBlob)) fw.add("fastapi");
return [...fw].sort();
}
function detectPackageManager() {
if (exists("pnpm-lock.yaml")) return "pnpm";
if (exists("yarn.lock")) return "yarn";
if (exists("bun.lockb") || exists("bun.lock")) return "bun";
if (exists("package-lock.json")) return "npm";
if (exists("poetry.lock")) return "poetry";
if (exists("Pipfile.lock")) return "pipenv";
if (exists("Cargo.lock")) return "cargo";
if (exists("Gemfile.lock")) return "bundler";
return null;
}
function detectMonorepo(pkg) {
const out = { isMonorepo: false, tool: null, workspaces: [] };
if (pkg?.workspaces) {
out.isMonorepo = true;
out.tool = exists("pnpm-workspace.yaml") ? "pnpm" : "npm/yarn";
const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
out.workspaces = ws;
} else if (exists("pnpm-workspace.yaml")) {
out.isMonorepo = true;
out.tool = "pnpm";
const raw = safeRead("pnpm-workspace.yaml") || "";
out.workspaces = raw
.split("\n")
.map((l) => l.trim())
.filter((l) => l.startsWith("- "))
.map((l) => l.slice(2).replace(/^["']|["']$/g, ""));
} else if (exists("turbo.json") || exists("nx.json") || exists("lerna.json")) {
out.isMonorepo = true;
out.tool = exists("turbo.json") ? "turborepo" : exists("nx.json") ? "nx" : "lerna";
}
return out;
}
function detectEntryPoints(pkg) {
const entries = [];
// Package-declared entries
if (pkg?.main) entries.push({ kind: "node-main", path: pkg.main });
if (pkg?.module) entries.push({ kind: "esm-main", path: pkg.module });
if (pkg?.bin) {
if (typeof pkg.bin === "string") entries.push({ kind: "bin", path: pkg.bin });
else for (const [name, p] of Object.entries(pkg.bin)) entries.push({ kind: `bin:${name}`, path: p });
}
// Vite / Next / app shells
const webEntries = [
"index.html",
"src/main.ts",
"src/main.tsx",
"src/main.js",
"src/main.jsx",
"src/index.ts",
"src/index.tsx",
"src/index.js",
"src/index.jsx",
"app/page.tsx",
"app/page.js",
"pages/index.tsx",
"pages/index.js",
"app/layout.tsx",
];
for (const p of webEntries) {
if (exists(p)) entries.push({ kind: "web", path: p });
}
// Chrome extension (root, plus common CRX manifest homes)
const extManifestCandidates = ["manifest.json", "public/manifest.json", "static/manifest.json", "src/manifest.json"];
for (const mp of extManifestCandidates) {
if (!exists(mp)) continue;
const m = safeJson(join(ROOT, mp));
if (!m) continue;
if (m.background?.service_worker) entries.push({ kind: "ext-background", path: m.background.service_worker });
if (m.background?.scripts) for (const s of m.background.scripts) entries.push({ kind: "ext-background", path: s });
if (m.content_scripts) {
for (const cs of m.content_scripts) for (const s of cs.js || []) entries.push({ kind: "ext-content", path: s });
}
if (m.action?.default_popup) entries.push({ kind: "ext-popup", path: m.action.default_popup });
if (m.options_page) entries.push({ kind: "ext-options", path: m.options_page });
break;
}
// Python
if (exists("manage.py")) entries.push({ kind: "python", path: "manage.py" });
if (exists("main.py")) entries.push({ kind: "python", path: "main.py" });
if (exists("app.py")) entries.push({ kind: "python", path: "app.py" });
// Go
if (exists("main.go")) entries.push({ kind: "go", path: "main.go" });
if (exists("cmd")) entries.push({ kind: "go", path: "cmd/" });
// Rust
if (exists("src/main.rs")) entries.push({ kind: "rust-bin", path: "src/main.rs" });
if (exists("src/lib.rs")) entries.push({ kind: "rust-lib", path: "src/lib.rs" });
return entries;
}
function detectScripts(pkg) {
const scripts = {};
if (pkg?.scripts) Object.assign(scripts, pkg.scripts);
// Makefile targets
if (exists("Makefile")) {
const raw = safeRead("Makefile") || "";
const targets = [...raw.matchAll(/^([a-zA-Z][a-zA-Z0-9_-]*):/gm)].map((m) => m[1]);
if (targets.length) scripts.__makefile__ = targets;
}
return scripts;
}
function detectDocs() {
const candidates = [
"README.md",
"README.rst",
"CONTRIBUTING.md",
"CHANGELOG.md",
"ARCHITECTURE.md",
"CLAUDE.md",
"AGENTS.md",
"ONBOARDING.md",
"SETUP.md",
"docs",
".github/CONTRIBUTING.md",
];
const out = [];
for (const p of candidates) if (exists(p)) out.push(p);
// Subdocs
if (exists("docs")) {
const docFiles = globFiles((n, full) => {
if (!full.includes(`${ROOT}/docs/`)) return false;
return n.endsWith(".md") || n.endsWith(".mdx") || n.endsWith(".rst");
}).slice(0, 20);
out.push(...docFiles);
}
return out;
}
function detectInfrastructure() {
const infra = [];
const checks = [
["Dockerfile", "docker"],
["docker-compose.yml", "docker-compose"],
["docker-compose.yaml", "docker-compose"],
[".dockerignore", "docker"],
["wrangler.toml", "cloudflare-workers"],
["wrangler.jsonc", "cloudflare-workers"],
["vercel.json", "vercel"],
["netlify.toml", "netlify"],
[".github/workflows", "github-actions"],
[".circleci/config.yml", "circleci"],
["fly.toml", "fly.io"],
["render.yaml", "render"],
["supabase", "supabase"],
["terraform", "terraform"],
["Procfile", "heroku"],
[".husky", "husky"],
];
for (const [p, label] of checks) if (exists(p)) infra.push(label);
return [...new Set(infra)].sort();
}
function detectTestRunners(pkg) {
const runners = [];
const deps = { ...(pkg?.dependencies || {}), ...(pkg?.devDependencies || {}) };
if (deps.vitest) runners.push("vitest");
if (deps.jest) runners.push("jest");
if (deps.mocha) runners.push("mocha");
if (deps.ava) runners.push("ava");
if (deps["@playwright/test"] || deps.playwright) runners.push("playwright");
if (deps.cypress) runners.push("cypress");
if (exists("pytest.ini") || /\bpytest\b/.test(safeRead("pyproject.toml") || "")) runners.push("pytest");
if (exists(".rspec") || exists("spec")) runners.push("rspec");
if (exists("go.mod")) runners.push("go test");
if (exists("Cargo.toml")) runners.push("cargo test");
return [...new Set(runners)];
}
// ---------- Build inventory ----------
const pkg = safeJson(join(ROOT, "package.json"));
const topLevelDirs = listTopLevelDirs();
const languages = detectLanguages();
const frameworks = detectFrameworks(pkg);
const packageManager = detectPackageManager();
const monorepo = detectMonorepo(pkg);
const entryPoints = detectEntryPoints(pkg);
const scripts = detectScripts(pkg);
const docs = detectDocs();
const infrastructure = detectInfrastructure();
const testRunners = detectTestRunners(pkg);
const manifestName = pkg?.name || basename(ROOT);
const envExample = firstExisting([".env.example", ".env.template", ".env.sample"]);
const nodeVersionFile = firstExisting([".nvmrc", ".node-version"]);
const pythonVersionFile = firstExisting([".python-version"]);
const inventory = {
root: ROOT,
name: manifestName,
description: pkg?.description || null,
languages,
frameworks,
packageManager,
nodeVersion: nodeVersionFile ? (safeRead(join(ROOT, nodeVersionFile)) || "").trim() : null,
pythonVersion: pythonVersionFile ? (safeRead(join(ROOT, pythonVersionFile)) || "").trim() : null,
monorepo,
entryPoints,
scripts,
docs,
infrastructure,
testRunners,
envExample,
topLevelDirs,
notes: [],
};
// Sanity notes the writer may surface to the user.
if (!inventory.docs.includes("README.md") && !inventory.docs.includes("README.rst")) {
inventory.notes.push("No README detected at repo root.");
}
if (languages.length === 0) {
inventory.notes.push("No known language manifests detected; inventory may be shallow.");
}
if (monorepo.isMonorepo && monorepo.workspaces.length === 0) {
inventory.notes.push("Monorepo tooling detected but no workspace globs resolved.");
}
process.stdout.write(pretty ? JSON.stringify(inventory, null, 2) : JSON.stringify(inventory));
process.stdout.write("\n");