From 340b4a989ce3ce95627957e287e87b50818536fe Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:06:53 +0530 Subject: [PATCH 01/12] feat(cli): add CLI MVP --- package.json | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 package.json diff --git a/package.json b/package.json new file mode 100644 index 0000000..506f0fe --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "beforecode", + "version": "0.1.0", + "description": "Spec-first project planning toolkit for humans and AI coding agents.", + "type": "module", + "bin": { + "beforecode": "./bin/beforecode.js" + }, + "scripts": { + "start": "node ./bin/beforecode.js", + "check": "node ./bin/beforecode.js check", + "score": "node ./bin/beforecode.js score" + }, + "files": [ + "bin", + "src", + "templates", + "checklists", + "prompts", + "docs/cli-usage.md", + "README.md", + "LICENSE" + ], + "keywords": [ + "prd", + "documentation", + "project-planning", + "software-architecture", + "ai-coding", + "qa" + ], + "author": "Mohit Kumar", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } +} From eeccbda09044f66275c478375e36ed41164c3877 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:07:06 +0530 Subject: [PATCH 02/12] feat(cli): add executable entrypoint --- bin/beforecode.js | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 bin/beforecode.js diff --git a/bin/beforecode.js b/bin/beforecode.js new file mode 100644 index 0000000..bcd8e21 --- /dev/null +++ b/bin/beforecode.js @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +import { runCli } from "../src/cli.js"; + +runCli(process.argv.slice(2)).catch((error) => { + console.error(`BeforeCode error: ${error.message}`); + process.exit(1); +}); From 4d7e8b5312285c50dc5b63763983ed6a2d28fa83 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:07:22 +0530 Subject: [PATCH 03/12] feat(cli): add project type maps --- src/project-types.js | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/project-types.js diff --git a/src/project-types.js b/src/project-types.js new file mode 100644 index 0000000..0768e5c --- /dev/null +++ b/src/project-types.js @@ -0,0 +1,49 @@ +export const PROJECT_TYPES = { + small: ["project-brief", "prd", "build-plan", "qa-test-plan"], + portfolio: ["project-brief", "ux-flows", "ui-system", "build-plan", "qa-test-plan"], + saas: [ + "project-brief", + "research-report", + "prd", + "ux-flows", + "trd", + "database-schema", + "api-documentation", + "permission-matrix", + "qa-test-plan", + "implementation-plan", + "deployment-plan" + ], + crm: [ + "business-requirements", + "research-report", + "prd", + "srs", + "permission-matrix", + "database-schema", + "api-documentation", + "qa-test-plan", + "implementation-plan" + ], + "ai-agent": [ + "project-brief", + "research-report", + "prd", + "trd", + "database-schema", + "api-documentation", + "permission-matrix", + "qa-test-plan", + "implementation-plan", + "deployment-plan" + ], + opensource: ["project-brief", "prd", "trd", "build-plan", "qa-test-plan", "deployment-plan"] +}; + +export function getProjectDocs(type) { + return PROJECT_TYPES[type] || null; +} + +export function getProjectTypes() { + return Object.keys(PROJECT_TYPES); +} From f888c5f2a9ffa660ae7c1498f403c7906f737515 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:07:29 +0530 Subject: [PATCH 04/12] feat(cli): add template map --- src/template-map.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/template-map.js diff --git a/src/template-map.js b/src/template-map.js new file mode 100644 index 0000000..826b3ac --- /dev/null +++ b/src/template-map.js @@ -0,0 +1,27 @@ +export const TEMPLATE_FILES = { + "project-brief": "project-brief.md", + "research-report": "research-report.md", + "business-requirements": "business-requirements.md", + prd: "prd.md", + srs: "srs.md", + "release-scope": "release-scope.md", + "ux-flows": "ux-flows.md", + "ui-system": "ui-system.md", + "permission-matrix": "permission-matrix.md", + trd: "trd.md", + "database-schema": "database-schema.md", + "api-documentation": "api-documentation.md", + "decision-record": "decision-record.md", + "implementation-plan": "implementation-plan.md", + "build-plan": "build-plan.md", + "qa-test-plan": "qa-test-plan.md", + "deployment-plan": "deployment-plan.md" +}; + +export function getTemplateFile(name) { + return TEMPLATE_FILES[name] || null; +} + +export function listTemplates() { + return Object.keys(TEMPLATE_FILES); +} From 70a8c0c683e6f8bf58be526a07321aa58955d75f Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:07:35 +0530 Subject: [PATCH 05/12] feat(cli): add path utilities --- src/paths.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/paths.js diff --git a/src/paths.js b/src/paths.js new file mode 100644 index 0000000..7fe09cc --- /dev/null +++ b/src/paths.js @@ -0,0 +1,11 @@ +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SRC_DIR = dirname(fileURLToPath(import.meta.url)); +export const PACKAGE_ROOT = resolve(SRC_DIR, ".."); +export const TEMPLATE_DIR = join(PACKAGE_ROOT, "templates"); +export const PROMPT_DIR = join(PACKAGE_ROOT, "prompts"); + +export function projectPath(...parts) { + return resolve(process.cwd(), ...parts); +} From bf896e62c7407b98a5c43c08bdb94db6202cd19b Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:07:44 +0530 Subject: [PATCH 06/12] feat(cli): add filesystem helpers --- src/fs-utils.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/fs-utils.js diff --git a/src/fs-utils.js b/src/fs-utils.js new file mode 100644 index 0000000..299e5f2 --- /dev/null +++ b/src/fs-utils.js @@ -0,0 +1,34 @@ +import { copyFile, mkdir, readFile, writeFile, access } from "node:fs/promises"; +import { constants } from "node:fs"; +import { dirname } from "node:path"; + +export async function ensureDir(path) { + await mkdir(path, { recursive: true }); +} + +export async function exists(path) { + try { + await access(path, constants.F_OK); + return true; + } catch { + return false; + } +} + +export async function copyFileSafe(source, target, { force = false } = {}) { + if (!force && await exists(target)) { + return { copied: false, skipped: true, target }; + } + await ensureDir(dirname(target)); + await copyFile(source, target); + return { copied: true, skipped: false, target }; +} + +export async function readText(path) { + return readFile(path, "utf8"); +} + +export async function writeJson(path, data) { + await ensureDir(dirname(path)); + await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf8"); +} From a81b63f77246ee6a49f15cd2b1e97cc5e3d83fd8 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:07:49 +0530 Subject: [PATCH 07/12] feat(cli): add argument parser --- src/args.js | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/args.js diff --git a/src/args.js b/src/args.js new file mode 100644 index 0000000..e16c7bd --- /dev/null +++ b/src/args.js @@ -0,0 +1,19 @@ +export function parseArgs(args) { + const result = { _: [] }; + for (let i = 0; i < args.length; i += 1) { + const arg = args[i]; + if (arg.startsWith("--")) { + const key = arg.slice(2); + const next = args[i + 1]; + if (!next || next.startsWith("--")) { + result[key] = true; + } else { + result[key] = next; + i += 1; + } + } else { + result._.push(arg); + } + } + return result; +} From 26112a9349a31be2243c637e40226b3a597dfd59 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:08:28 +0530 Subject: [PATCH 08/12] feat(cli): add command runner --- src/cli.js | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/cli.js diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..271e005 --- /dev/null +++ b/src/cli.js @@ -0,0 +1,108 @@ +import { join } from "node:path"; +import { parseArgs } from "./args.js"; +import { TEMPLATE_DIR, projectPath } from "./paths.js"; +import { copyFileSafe, ensureDir, exists, writeJson } from "./fs-utils.js"; +import { getProjectDocs, getProjectTypes } from "./project-types.js"; +import { getTemplateFile, listTemplates } from "./template-map.js"; + +const VERSION = "0.1.0"; + +export async function runCli(argv) { + const options = parseArgs(argv); + const command = options._[0] || "help"; + + if (options.version || command === "version") return printVersion(); + if (options.help || command === "help") return printHelp(); + if (command === "init") return initCommand(options); + if (command === "add") return addCommand(options); + if (command === "check") return checkCommand(options); + if (command === "score") return scoreCommand(options); + if (command === "list") return listCommand(); + + throw new Error(`Unknown command: ${command}`); +} + +function printVersion() { + console.log(`beforecode ${VERSION}`); +} + +function printHelp() { + console.log(`BeforeCode CLI\n\nUsage:\n beforecode init --type saas\n beforecode add prd\n beforecode check --type saas\n beforecode score --type saas\n beforecode list\n\nProject types:\n ${getProjectTypes().join(", ")}\n`); +} + +function listCommand() { + console.log("Project types:"); + for (const type of getProjectTypes()) console.log(`- ${type}`); + console.log("\nTemplates:"); + for (const item of listTemplates()) console.log(`- ${item}`); +} + +async function initCommand(options) { + const type = options.type || "small"; + const docs = getProjectDocs(type); + if (!docs) throw new Error(`Unknown project type: ${type}`); + + const docsDir = projectPath(options.docs || "docs"); + await ensureDir(docsDir); + + const copied = []; + const skipped = []; + + for (const name of docs) { + const file = getTemplateFile(name); + const source = join(TEMPLATE_DIR, file); + const target = join(docsDir, numberedName(copied.length + skipped.length + 1, file)); + const result = await copyFileSafe(source, target, { force: Boolean(options.force) }); + if (result.copied) copied.push(target); + if (result.skipped) skipped.push(target); + } + + await writeJson(projectPath(".beforecoderc.json"), { + projectType: type, + docsPath: options.docs || "docs", + generatedBy: "beforecode", + version: VERSION + }); + + console.log(`BeforeCode initialized for ${type}.`); + console.log(`Copied: ${copied.length}`); + if (skipped.length) console.log(`Skipped existing files: ${skipped.length}`); +} + +async function addCommand(options) { + const name = options._[1]; + if (!name) throw new Error("Missing template name. Example: beforecode add prd"); + const file = getTemplateFile(name); + if (!file) throw new Error(`Unknown template: ${name}`); + const docsDir = projectPath(options.docs || "docs"); + const source = join(TEMPLATE_DIR, file); + const target = join(docsDir, file); + const result = await copyFileSafe(source, target, { force: Boolean(options.force) }); + console.log(result.copied ? `Added ${file}` : `Skipped existing ${file}`); +} + +async function checkCommand(options) { + const type = options.type || "small"; + const docs = getProjectDocs(type); + if (!docs) throw new Error(`Unknown project type: ${type}`); + const docsDir = projectPath(options.docs || "docs"); + let present = 0; + console.log(`BeforeCode check: ${type}\n`); + for (const name of docs) { + const file = getTemplateFile(name); + const expected = join(docsDir, file); + const found = await exists(expected) || await exists(join(docsDir, numberedName(docs.indexOf(name) + 1, file))); + if (found) present += 1; + console.log(`${found ? "✓" : "✗"} ${file}`); + } + const score = Math.round((present / docs.length) * 100); + console.log(`\nReadiness: ${score}%`); +} + +async function scoreCommand(options) { + return checkCommand(options); +} + +function numberedName(index, file) { + return `${String(index).padStart(2, "0")}-${file}`; +} From 9c161b5c095732aaa0243795790dc345214398c0 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:08:38 +0530 Subject: [PATCH 09/12] docs(cli): add CLI PRD --- docs/cli-prd.md | 54 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 docs/cli-prd.md diff --git a/docs/cli-prd.md b/docs/cli-prd.md new file mode 100644 index 0000000..5486e9e --- /dev/null +++ b/docs/cli-prd.md @@ -0,0 +1,54 @@ +# BeforeCode CLI PRD + +## Overview + +BeforeCode CLI turns the Markdown toolkit into a project-integrated generator. + +Users can run one command inside any software project and generate the right documentation set for the project type. + +## Goal + +Make BeforeCode usable without manual copying from GitHub. + +## Target Users + +- Developers starting a new project +- Existing project maintainers adding documentation +- AI coding users preparing source-of-truth docs +- Students creating structured project documentation + +## MVP Commands + +| Command | Purpose | +|---|---| +| `beforecode init --type saas` | Generate a documentation set for a project type | +| `beforecode add prd` | Add one template to the project | +| `beforecode check --type saas` | Check required docs for a project type | +| `beforecode score --type saas` | Show readiness score | +| `beforecode list` | Show supported project types and templates | + +## Supported Project Types + +- small +- portfolio +- saas +- crm +- ai-agent +- opensource + +## Success Criteria + +- CLI can run with Node 18 or later +- CLI can generate docs into a local `docs/` folder +- CLI does not overwrite files unless `--force` is used +- CLI creates `.beforecoderc.json` +- CLI can check missing docs +- CLI works with `npx beforecode` after package publishing + +## Non-goals for MVP + +- No web UI +- No remote API +- No AI generation inside CLI +- No npm publishing automation yet +- No TypeScript build step yet From edd238b796c73b0924a76a2398323b90c58e61b5 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:08:56 +0530 Subject: [PATCH 10/12] docs(cli): add CLI TRD --- docs/cli-trd.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docs/cli-trd.md diff --git a/docs/cli-trd.md b/docs/cli-trd.md new file mode 100644 index 0000000..bfb8108 --- /dev/null +++ b/docs/cli-trd.md @@ -0,0 +1,51 @@ +# BeforeCode CLI TRD + +## Overview + +The CLI is a zero-dependency Node.js tool that copies BeforeCode templates into a project. + +## Runtime + +- Node.js 18+ +- ECMAScript modules +- Built-in filesystem APIs + +## Entry Point + +```text +bin/beforecode.js +``` + +## Source Layout + +```text +src/cli.js +src/project-types.js +src/template-map.js +src/fs-utils.js +src/args.js +src/paths.js +``` + +## Commands + +- `init` generates a docs set +- `add` copies one template +- `check` checks required docs +- `score` shows readiness score +- `list` shows supported options + +## Safety Rules + +- Do not overwrite files by default +- Use `--force` to replace existing files +- Generate `.beforecoderc.json` for future checks + +## Future Scope + +- Interactive prompts +- Template variables +- Better scoring +- Test suite +- npm publishing +- AI handoff generation From de4098f93726542afbbf2a848cc495b4489cd941 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:09:03 +0530 Subject: [PATCH 11/12] docs(cli): add CLI usage guide --- docs/cli-usage.md | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 docs/cli-usage.md diff --git a/docs/cli-usage.md b/docs/cli-usage.md new file mode 100644 index 0000000..b8c1e7b --- /dev/null +++ b/docs/cli-usage.md @@ -0,0 +1,61 @@ +# CLI Usage + +## Install locally from the repository + +```bash +npm install +node ./bin/beforecode.js help +``` + +## Generate docs for a SaaS project + +```bash +beforecode init --type saas +``` + +This creates a `docs/` folder with the recommended SaaS documentation set. + +## Generate docs in a custom folder + +```bash +beforecode init --type ai-agent --docs project-docs +``` + +## Add one template + +```bash +beforecode add prd +beforecode add api-documentation +beforecode add qa-test-plan +``` + +## Check readiness + +```bash +beforecode check --type saas +``` + +## Score readiness + +```bash +beforecode score --type saas +``` + +## List options + +```bash +beforecode list +``` + +## Supported project types + +- small +- portfolio +- saas +- crm +- ai-agent +- opensource + +## Notes + +The CLI does not overwrite files by default. Use `--force` only when you intentionally want to replace existing generated files. From 8b6d2c01dd50881a4e8ee5524c90f12beec9c1c9 Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Sat, 20 Jun 2026 00:30:29 +0530 Subject: [PATCH 12/12] cli polish --- .github/workflows/cli-test.yml | 28 +++++++ bin/beforecode.js | 0 docs/cli-prd.md | 6 ++ docs/cli-roadmap.md | 29 +++++++ docs/cli-trd.md | 29 ++++--- docs/cli-usage.md | 48 +++++++++--- package.json | 9 ++- src/args.js | 31 +++++--- src/cli.js | 135 +++++++++++---------------------- src/commands/add.js | 27 +++++++ src/commands/check.js | 29 +++++++ src/commands/handoff.js | 19 +++++ src/commands/init.js | 26 +++++++ src/commands/list.js | 24 ++++++ src/commands/score.js | 33 ++++++++ src/core/checker.js | 34 +++++++++ src/core/config.js | 35 +++++++++ src/core/generator.js | 115 ++++++++++++++++++++++++++++ src/core/handoff.js | 85 +++++++++++++++++++++ src/core/scorer.js | 30 ++++++++ src/data/project-types.js | 100 ++++++++++++++++++++++++ src/data/templates.js | 27 +++++++ src/fs-utils.js | 34 ++++++--- src/paths.js | 5 +- src/project-types.js | 50 +----------- src/template-map.js | 28 +------ tests/add-check-score.test.js | 52 +++++++++++++ tests/cli-help.test.js | 16 ++++ tests/init-command.test.js | 40 ++++++++++ 29 files changed, 913 insertions(+), 211 deletions(-) create mode 100644 .github/workflows/cli-test.yml mode change 100644 => 100755 bin/beforecode.js create mode 100644 docs/cli-roadmap.md create mode 100644 src/commands/add.js create mode 100644 src/commands/check.js create mode 100644 src/commands/handoff.js create mode 100644 src/commands/init.js create mode 100644 src/commands/list.js create mode 100644 src/commands/score.js create mode 100644 src/core/checker.js create mode 100644 src/core/config.js create mode 100644 src/core/generator.js create mode 100644 src/core/handoff.js create mode 100644 src/core/scorer.js create mode 100644 src/data/project-types.js create mode 100644 src/data/templates.js create mode 100644 tests/add-check-score.test.js create mode 100644 tests/cli-help.test.js create mode 100644 tests/init-command.test.js diff --git a/.github/workflows/cli-test.yml b/.github/workflows/cli-test.yml new file mode 100644 index 0000000..126160f --- /dev/null +++ b/.github/workflows/cli-test.yml @@ -0,0 +1,28 @@ +name: CLI test + +on: + pull_request: + paths: + - "bin/**" + - "src/**" + - "tests/**" + - "package.json" + - ".github/workflows/cli-test.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + cli: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: "20" + - run: npm test + - run: node ./bin/beforecode.js help + - run: node ./bin/beforecode.js list + - run: node ./bin/beforecode.js init --type saas --docs tmp-docs + - run: node ./bin/beforecode.js check --type saas --docs tmp-docs diff --git a/bin/beforecode.js b/bin/beforecode.js old mode 100644 new mode 100755 diff --git a/docs/cli-prd.md b/docs/cli-prd.md index 5486e9e..6418624 100644 --- a/docs/cli-prd.md +++ b/docs/cli-prd.md @@ -25,6 +25,7 @@ Make BeforeCode usable without manual copying from GitHub. | `beforecode add prd` | Add one template to the project | | `beforecode check --type saas` | Check required docs for a project type | | `beforecode score --type saas` | Show readiness score | +| `beforecode handoff` | Generate AI handoff files | | `beforecode list` | Show supported project types and templates | ## Supported Project Types @@ -33,6 +34,8 @@ Make BeforeCode usable without manual copying from GitHub. - portfolio - saas - crm +- ecommerce +- mobile - ai-agent - opensource @@ -41,8 +44,11 @@ Make BeforeCode usable without manual copying from GitHub. - CLI can run with Node 18 or later - CLI can generate docs into a local `docs/` folder - CLI does not overwrite files unless `--force` is used +- CLI supports `--dry-run` - CLI creates `.beforecoderc.json` - CLI can check missing docs +- CLI can generate a basic AI handoff package +- CLI can run tests with Node built-in test runner - CLI works with `npx beforecode` after package publishing ## Non-goals for MVP diff --git a/docs/cli-roadmap.md b/docs/cli-roadmap.md new file mode 100644 index 0000000..92c40f8 --- /dev/null +++ b/docs/cli-roadmap.md @@ -0,0 +1,29 @@ +# CLI Roadmap + +## v0.1 + +- Generate docs by project type +- Add one template +- Check missing docs +- Show readiness score +- Generate basic AI handoff files +- Add Node test workflow + +## v0.2 + +- Interactive prompts +- Template variable injection +- More project-specific template packs +- Better config editing + +## v0.3 + +- Section-level readiness checks +- Traceability report +- AI handoff profiles for common coding tools + +## v1.0 + +- Stable npm package +- Versioned template packs +- Strong docs site integration diff --git a/docs/cli-trd.md b/docs/cli-trd.md index bfb8108..59e85ba 100644 --- a/docs/cli-trd.md +++ b/docs/cli-trd.md @@ -2,13 +2,14 @@ ## Overview -The CLI is a zero-dependency Node.js tool that copies BeforeCode templates into a project. +The CLI is a zero-dependency Node.js tool that copies BeforeCode templates into a project and checks documentation readiness. ## Runtime - Node.js 18+ - ECMAScript modules - Built-in filesystem APIs +- Node built-in test runner ## Entry Point @@ -20,10 +21,11 @@ bin/beforecode.js ```text src/cli.js -src/project-types.js -src/template-map.js -src/fs-utils.js +src/commands/ +src/core/ +src/data/ src/args.js +src/fs-utils.js src/paths.js ``` @@ -33,19 +35,28 @@ src/paths.js - `add` copies one template - `check` checks required docs - `score` shows readiness score +- `handoff` creates AI handoff files - `list` shows supported options ## Safety Rules - Do not overwrite files by default - Use `--force` to replace existing files +- Use `--dry-run` to preview changes - Generate `.beforecoderc.json` for future checks +## Test Strategy + +Use Node's built-in test runner. + +```bash +npm test +``` + ## Future Scope - Interactive prompts -- Template variables -- Better scoring -- Test suite -- npm publishing -- AI handoff generation +- Template variables beyond basic fields +- Section-level scoring +- Traceability scoring +- npm publishing workflow diff --git a/docs/cli-usage.md b/docs/cli-usage.md index b8c1e7b..4c6b5fd 100644 --- a/docs/cli-usage.md +++ b/docs/cli-usage.md @@ -1,26 +1,39 @@ # CLI Usage -## Install locally from the repository +BeforeCode CLI generates project documentation inside an existing or new software project. + +## Local usage from the repository ```bash npm install node ./bin/beforecode.js help +node ./bin/beforecode.js list ``` -## Generate docs for a SaaS project +## Generate a docs set ```bash beforecode init --type saas ``` -This creates a `docs/` folder with the recommended SaaS documentation set. - -## Generate docs in a custom folder +Use a custom docs folder: ```bash beforecode init --type ai-agent --docs project-docs ``` +Preview without writing files: + +```bash +beforecode init --type saas --dry-run +``` + +Replace existing generated files intentionally: + +```bash +beforecode init --type saas --force +``` + ## Add one template ```bash @@ -35,16 +48,31 @@ beforecode add qa-test-plan beforecode check --type saas ``` +If `.beforecoderc.json` exists, the CLI can read the project type and docs path automatically: + +```bash +beforecode check +``` + ## Score readiness ```bash beforecode score --type saas ``` -## List options +The MVP score is based on whether required files exist. Later versions can inspect sections and traceability. + +## Generate AI handoff files ```bash -beforecode list +beforecode handoff +``` + +This creates: + +```text +AGENTS.md +docs/ai-handoff.md ``` ## Supported project types @@ -53,9 +81,11 @@ beforecode list - portfolio - saas - crm +- ecommerce +- mobile - ai-agent - opensource -## Notes +## Safe behavior -The CLI does not overwrite files by default. Use `--force` only when you intentionally want to replace existing generated files. +The CLI does not overwrite files by default. Use `--force` only when replacing existing generated files is intentional. diff --git a/package.json b/package.json index 506f0fe..d8ae685 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "beforecode": "./bin/beforecode.js" }, "scripts": { - "start": "node ./bin/beforecode.js", - "check": "node ./bin/beforecode.js check", - "score": "node ./bin/beforecode.js score" + "cli": "node ./bin/beforecode.js", + "test": "node --test", + "test:cli": "node ./bin/beforecode.js help && node ./bin/beforecode.js list" }, "files": [ "bin", @@ -27,7 +27,8 @@ "project-planning", "software-architecture", "ai-coding", - "qa" + "qa", + "cli" ], "author": "Mohit Kumar", "license": "MIT", diff --git a/src/args.js b/src/args.js index e16c7bd..24e77cf 100644 --- a/src/args.js +++ b/src/args.js @@ -1,19 +1,30 @@ export function parseArgs(args) { const result = { _: [] }; + for (let i = 0; i < args.length; i += 1) { const arg = args[i]; - if (arg.startsWith("--")) { - const key = arg.slice(2); - const next = args[i + 1]; - if (!next || next.startsWith("--")) { - result[key] = true; - } else { - result[key] = next; - i += 1; - } - } else { + + if (!arg.startsWith("--")) { result._.push(arg); + continue; + } + + const raw = arg.slice(2); + const equalsIndex = raw.indexOf("="); + + if (equalsIndex !== -1) { + result[raw.slice(0, equalsIndex)] = raw.slice(equalsIndex + 1); + continue; + } + + const next = args[i + 1]; + if (!next || next.startsWith("--")) { + result[raw] = true; + } else { + result[raw] = next; + i += 1; } } + return result; } diff --git a/src/cli.js b/src/cli.js index 271e005..43a9833 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,23 +1,32 @@ -import { join } from "node:path"; import { parseArgs } from "./args.js"; -import { TEMPLATE_DIR, projectPath } from "./paths.js"; -import { copyFileSafe, ensureDir, exists, writeJson } from "./fs-utils.js"; -import { getProjectDocs, getProjectTypes } from "./project-types.js"; -import { getTemplateFile, listTemplates } from "./template-map.js"; +import { getProjectTypes } from "./data/project-types.js"; +import { initCommand } from "./commands/init.js"; +import { addCommand } from "./commands/add.js"; +import { checkCommand } from "./commands/check.js"; +import { scoreCommand } from "./commands/score.js"; +import { handoffCommand } from "./commands/handoff.js"; +import { listCommand } from "./commands/list.js"; const VERSION = "0.1.0"; -export async function runCli(argv) { +export async function runCli(argv, context = { cwd: process.cwd() }) { const options = parseArgs(argv); const command = options._[0] || "help"; - if (options.version || command === "version") return printVersion(); - if (options.help || command === "help") return printHelp(); - if (command === "init") return initCommand(options); - if (command === "add") return addCommand(options); - if (command === "check") return checkCommand(options); - if (command === "score") return scoreCommand(options); - if (command === "list") return listCommand(); + if (options.version || command === "version") { + return printVersion(); + } + + if (options.help || command === "help") { + return printHelp(); + } + + if (command === "init") return initCommand(options, context); + if (command === "add") return addCommand(options, context); + if (command === "check") return checkCommand(options, context); + if (command === "score") return scoreCommand(options, context); + if (command === "handoff") return handoffCommand(options, context); + if (command === "list") return listCommand(options, context); throw new Error(`Unknown command: ${command}`); } @@ -27,82 +36,26 @@ function printVersion() { } function printHelp() { - console.log(`BeforeCode CLI\n\nUsage:\n beforecode init --type saas\n beforecode add prd\n beforecode check --type saas\n beforecode score --type saas\n beforecode list\n\nProject types:\n ${getProjectTypes().join(", ")}\n`); -} - -function listCommand() { - console.log("Project types:"); - for (const type of getProjectTypes()) console.log(`- ${type}`); - console.log("\nTemplates:"); - for (const item of listTemplates()) console.log(`- ${item}`); -} - -async function initCommand(options) { - const type = options.type || "small"; - const docs = getProjectDocs(type); - if (!docs) throw new Error(`Unknown project type: ${type}`); - - const docsDir = projectPath(options.docs || "docs"); - await ensureDir(docsDir); - - const copied = []; - const skipped = []; - - for (const name of docs) { - const file = getTemplateFile(name); - const source = join(TEMPLATE_DIR, file); - const target = join(docsDir, numberedName(copied.length + skipped.length + 1, file)); - const result = await copyFileSafe(source, target, { force: Boolean(options.force) }); - if (result.copied) copied.push(target); - if (result.skipped) skipped.push(target); - } - - await writeJson(projectPath(".beforecoderc.json"), { - projectType: type, - docsPath: options.docs || "docs", - generatedBy: "beforecode", - version: VERSION - }); - - console.log(`BeforeCode initialized for ${type}.`); - console.log(`Copied: ${copied.length}`); - if (skipped.length) console.log(`Skipped existing files: ${skipped.length}`); -} - -async function addCommand(options) { - const name = options._[1]; - if (!name) throw new Error("Missing template name. Example: beforecode add prd"); - const file = getTemplateFile(name); - if (!file) throw new Error(`Unknown template: ${name}`); - const docsDir = projectPath(options.docs || "docs"); - const source = join(TEMPLATE_DIR, file); - const target = join(docsDir, file); - const result = await copyFileSafe(source, target, { force: Boolean(options.force) }); - console.log(result.copied ? `Added ${file}` : `Skipped existing ${file}`); -} - -async function checkCommand(options) { - const type = options.type || "small"; - const docs = getProjectDocs(type); - if (!docs) throw new Error(`Unknown project type: ${type}`); - const docsDir = projectPath(options.docs || "docs"); - let present = 0; - console.log(`BeforeCode check: ${type}\n`); - for (const name of docs) { - const file = getTemplateFile(name); - const expected = join(docsDir, file); - const found = await exists(expected) || await exists(join(docsDir, numberedName(docs.indexOf(name) + 1, file))); - if (found) present += 1; - console.log(`${found ? "✓" : "✗"} ${file}`); - } - const score = Math.round((present / docs.length) * 100); - console.log(`\nReadiness: ${score}%`); -} - -async function scoreCommand(options) { - return checkCommand(options); -} - -function numberedName(index, file) { - return `${String(index).padStart(2, "0")}-${file}`; + console.log(`BeforeCode CLI + +Usage: + beforecode init --type saas + beforecode init --type ai-agent --docs project-docs + beforecode add prd + beforecode check --type saas + beforecode score --type saas + beforecode handoff + beforecode list + +Options: + --type Project type + --docs Documentation folder + --name Project name + --force Replace existing generated files + --dry-run Preview changes without writing files + --version Show version + +Project types: + ${getProjectTypes().join(", ")} +`); } diff --git a/src/commands/add.js b/src/commands/add.js new file mode 100644 index 0000000..88bd9f8 --- /dev/null +++ b/src/commands/add.js @@ -0,0 +1,27 @@ +import { resolveOptions } from "../core/config.js"; +import { addTemplate } from "../core/generator.js"; + +export async function addCommand(options, context) { + const templateName = options._[1]; + + if (!templateName) { + throw new Error("Missing template name. Example: beforecode add prd"); + } + + const resolved = await resolveOptions(options, context.cwd); + const result = await addTemplate({ + cwd: context.cwd, + templateName, + docsPath: resolved.docsPath, + force: resolved.force, + dryRun: resolved.dryRun + }); + + if (result.dryRun) { + console.log(`would create ${result.target}`); + } else if (result.created) { + console.log(`Added ${templateName}`); + } else { + console.log(`Skipped existing ${templateName}`); + } +} diff --git a/src/commands/check.js b/src/commands/check.js new file mode 100644 index 0000000..1499ebd --- /dev/null +++ b/src/commands/check.js @@ -0,0 +1,29 @@ +import { resolveOptions } from "../core/config.js"; +import { checkProject } from "../core/checker.js"; + +export async function checkCommand(options, context) { + const resolved = await resolveOptions(options, context.cwd); + const result = await checkProject({ + cwd: context.cwd, + projectType: resolved.projectType, + docsPath: resolved.docsPath + }); + + console.log(`BeforeCode check: ${result.label}`); + console.log(""); + + for (const item of result.results) { + console.log(`${item.found ? "✓" : "✗"} ${item.file}`); + } + + if (result.missing.length) { + console.log(""); + console.log("Missing:"); + for (const item of result.missing) { + console.log(`- ${item.file}`); + } + } + + console.log(""); + console.log(`Readiness: ${result.score}%`); +} diff --git a/src/commands/handoff.js b/src/commands/handoff.js new file mode 100644 index 0000000..6d78c82 --- /dev/null +++ b/src/commands/handoff.js @@ -0,0 +1,19 @@ +import { resolveOptions } from "../core/config.js"; +import { generateHandoff } from "../core/handoff.js"; + +export async function handoffCommand(options, context) { + const resolved = await resolveOptions(options, context.cwd); + const result = await generateHandoff({ cwd: context.cwd, ...resolved }); + + if (result.dryRun) { + console.log("BeforeCode handoff dry run"); + for (const file of result.files) { + console.log(`would create ${file.path}`); + } + return; + } + + console.log("BeforeCode AI handoff generated"); + console.log(`Created: ${result.created.length}`); + console.log(`Skipped: ${result.skipped.length}`); +} diff --git a/src/commands/init.js b/src/commands/init.js new file mode 100644 index 0000000..4258c6b --- /dev/null +++ b/src/commands/init.js @@ -0,0 +1,26 @@ +import { resolveOptions } from "../core/config.js"; +import { initProject } from "../core/generator.js"; + +export async function initCommand(options, context) { + const resolved = await resolveOptions(options, context.cwd); + const result = await initProject({ cwd: context.cwd, ...resolved }); + + if (result.dryRun) { + console.log(`BeforeCode dry run: ${result.label}`); + for (const item of result.planned) { + console.log(`would create ${relative(context.cwd, item.target)}`); + } + return; + } + + console.log("BeforeCode initialized"); + console.log(`Project type: ${result.label}`); + console.log(`Docs path: ${result.docsPath}`); + console.log(`Created: ${result.created.length}`); + console.log(`Skipped: ${result.skipped.length}`); + console.log("Config: .beforecoderc.json"); +} + +function relative(cwd, file) { + return file.replace(`${cwd}/`, "").replace(`${cwd}\\`, ""); +} diff --git a/src/commands/list.js b/src/commands/list.js new file mode 100644 index 0000000..e2aa068 --- /dev/null +++ b/src/commands/list.js @@ -0,0 +1,24 @@ +import { getProjectTypes, getProjectType } from "../data/project-types.js"; +import { listTemplates } from "../data/templates.js"; + +export function listCommand(options) { + const mode = options._[1] || "all"; + + if (mode === "all" || mode === "types") { + console.log("Project types:"); + for (const type of getProjectTypes()) { + console.log(`- ${type}: ${getProjectType(type).label}`); + } + } + + if (mode === "all") { + console.log(""); + } + + if (mode === "all" || mode === "templates") { + console.log("Templates:"); + for (const template of listTemplates()) { + console.log(`- ${template}`); + } + } +} diff --git a/src/commands/score.js b/src/commands/score.js new file mode 100644 index 0000000..d125c95 --- /dev/null +++ b/src/commands/score.js @@ -0,0 +1,33 @@ +import { resolveOptions } from "../core/config.js"; +import { scoreProject } from "../core/scorer.js"; + +export async function scoreCommand(options, context) { + const resolved = await resolveOptions(options, context.cwd); + const result = await scoreProject({ + cwd: context.cwd, + projectType: resolved.projectType, + docsPath: resolved.docsPath + }); + + console.log(`BeforeCode readiness score: ${result.label}`); + console.log(""); + + for (const [category, score] of Object.entries(result.categories)) { + console.log(`${label(category)}: ${score}%`); + } + + console.log(""); + console.log(`Overall: ${result.score}%`); + + if (result.score < 70) { + console.log("Status: Needs more documentation before full build."); + } else if (result.score < 90) { + console.log("Status: Good start, but review missing areas."); + } else { + console.log("Status: Ready for implementation review."); + } +} + +function label(value) { + return value.replace("-", " ").replace(/\b\w/g, (char) => char.toUpperCase()); +} diff --git a/src/core/checker.js b/src/core/checker.js new file mode 100644 index 0000000..24f3c11 --- /dev/null +++ b/src/core/checker.js @@ -0,0 +1,34 @@ +import { join } from "node:path"; +import { readDirSafe } from "../fs-utils.js"; +import { getProjectDocs, getProjectType } from "../data/project-types.js"; +import { getTemplateFile } from "../data/templates.js"; + +export async function checkProject({ cwd, projectType, docsPath }) { + const docs = getProjectDocs(projectType); + const typeConfig = getProjectType(projectType); + + if (!docs) { + throw new Error(`Unknown project type: ${projectType}`); + } + + const files = await readDirSafe(join(cwd, docsPath)); + const results = docs.map((templateName) => { + const file = getTemplateFile(templateName); + const found = files.some((existing) => existing === file || existing.endsWith(`-${file}`)); + return { templateName, file, found }; + }); + + const present = results.filter((item) => item.found); + const missing = results.filter((item) => !item.found); + const score = Math.round((present.length / results.length) * 100); + + return { + projectType, + label: typeConfig.label, + docsPath, + results, + present, + missing, + score + }; +} diff --git a/src/core/config.js b/src/core/config.js new file mode 100644 index 0000000..6c0cb2f --- /dev/null +++ b/src/core/config.js @@ -0,0 +1,35 @@ +import { join } from "node:path"; +import { exists, readText, writeText } from "../fs-utils.js"; + +export const CONFIG_FILE = ".beforecoderc.json"; + +export async function readConfig(cwd) { + const path = join(cwd, CONFIG_FILE); + + if (!await exists(path)) { + return null; + } + + try { + return JSON.parse(await readText(path)); + } catch { + throw new Error(`${CONFIG_FILE} exists but is not valid JSON.`); + } +} + +export async function writeConfig(cwd, config, { force = true } = {}) { + const path = join(cwd, CONFIG_FILE); + return writeText(path, `${JSON.stringify(config, null, 2)}\n`, { force }); +} + +export async function resolveOptions(options, cwd) { + const config = await readConfig(cwd); + + return { + projectName: options.name || config?.projectName || "Untitled Project", + projectType: options.type || config?.projectType || "small", + docsPath: options.docs || config?.docsPath || "docs", + force: Boolean(options.force), + dryRun: Boolean(options["dry-run"]) + }; +} diff --git a/src/core/generator.js b/src/core/generator.js new file mode 100644 index 0000000..8120ed2 --- /dev/null +++ b/src/core/generator.js @@ -0,0 +1,115 @@ +import { join } from "node:path"; +import { TEMPLATE_DIR } from "../paths.js"; +import { ensureDir, exists, readText, writeText } from "../fs-utils.js"; +import { getProjectDocs, getProjectType } from "../data/project-types.js"; +import { getTemplateFile } from "../data/templates.js"; +import { writeConfig } from "./config.js"; + +const VERSION = "0.1.0"; + +export async function initProject({ cwd, projectName, projectType, docsPath, force = false, dryRun = false }) { + const docs = getProjectDocs(projectType); + const typeConfig = getProjectType(projectType); + + if (!docs) { + throw new Error(`Unknown project type: ${projectType}`); + } + + const docsDir = join(cwd, docsPath); + const planned = docs.map((templateName, index) => { + const file = getTemplateFile(templateName); + return { + templateName, + file, + target: join(docsDir, numberedName(index + 1, file)) + }; + }); + + if (dryRun) { + return { + dryRun: true, + projectType, + label: typeConfig.label, + docsPath, + planned, + created: [], + skipped: [] + }; + } + + await ensureDir(docsDir); + + const created = []; + const skipped = []; + + for (const item of planned) { + if (!force && await exists(item.target)) { + skipped.push(item.target); + continue; + } + + const source = join(TEMPLATE_DIR, item.file); + const content = renderTemplate(await readText(source), { + projectName, + projectType, + docsPath, + templateName: item.templateName + }); + + await writeText(item.target, content, { force: true }); + created.push(item.target); + } + + await writeConfig(cwd, { + projectName, + projectType, + docsPath, + createdBy: "beforecode", + version: VERSION, + documents: planned.map((item) => item.target.split(/[\\/]/).pop()) + }); + + return { + dryRun: false, + projectType, + label: typeConfig.label, + docsPath, + planned, + created, + skipped + }; +} + +export async function addTemplate({ cwd, templateName, docsPath, force = false, dryRun = false }) { + const file = getTemplateFile(templateName); + + if (!file) { + throw new Error(`Unknown template: ${templateName}`); + } + + const target = join(cwd, docsPath, file); + const source = join(TEMPLATE_DIR, file); + + if (dryRun) { + return { dryRun: true, target, created: false, skipped: false }; + } + + if (!force && await exists(target)) { + return { dryRun: false, target, created: false, skipped: true }; + } + + await writeText(target, await readText(source), { force: true }); + return { dryRun: false, target, created: true, skipped: false }; +} + +function numberedName(index, file) { + return `${String(index).padStart(2, "0")}-${file}`; +} + +function renderTemplate(content, values) { + return content + .replaceAll("{{projectName}}", values.projectName) + .replaceAll("{{projectType}}", values.projectType) + .replaceAll("{{docsPath}}", values.docsPath) + .replaceAll("{{templateName}}", values.templateName); +} diff --git a/src/core/handoff.js b/src/core/handoff.js new file mode 100644 index 0000000..bfb7e06 --- /dev/null +++ b/src/core/handoff.js @@ -0,0 +1,85 @@ +import { join } from "node:path"; +import { writeText } from "../fs-utils.js"; + +export async function generateHandoff({ cwd, projectName, projectType, docsPath, force = false, dryRun = false }) { + const files = [ + { + path: join(cwd, "AGENTS.md"), + content: agentsContent({ projectName, projectType, docsPath }) + }, + { + path: join(cwd, docsPath, "ai-handoff.md"), + content: handoffContent({ projectName, projectType, docsPath }) + } + ]; + + if (dryRun) { + return { dryRun: true, files, created: [], skipped: [] }; + } + + const created = []; + const skipped = []; + + for (const file of files) { + const result = await writeText(file.path, file.content, { force }); + if (result.written) created.push(file.path); + if (result.skipped) skipped.push(file.path); + } + + return { dryRun: false, files, created, skipped }; +} + +function agentsContent({ projectName, projectType, docsPath }) { + return `# AGENTS.md + +## Project + +${projectName} + +## Project type + +${projectType} + +## Source of truth + +Read the approved documents in \`${docsPath}/\` before changing code. + +## Required workflow + +1. Inspect the repository. +2. Read product and technical docs. +3. Map work to requirements. +4. Keep changes inside the requested scope. +5. Run available checks. +6. Report what changed, how it was verified, and what remains. + +## Approval required + +Ask before destructive data changes, production deployment, new external services, permission changes, or undocumented scope expansion. +`; +} + +function handoffContent({ projectName, projectType, docsPath }) { + return `# AI Handoff + +## Project + +${projectName} + +## Project type + +${projectType} + +## Documents + +Use \`${docsPath}/\` as the source-of-truth folder. + +## Build instruction + +Implement only the current approved scope. If requirements conflict, stop and report the conflict before coding. + +## Verification + +After implementation, report tests run, files changed, risks, and any requirement that could not be completed. +`; +} diff --git a/src/core/scorer.js b/src/core/scorer.js new file mode 100644 index 0000000..9a106b7 --- /dev/null +++ b/src/core/scorer.js @@ -0,0 +1,30 @@ +import { checkProject } from "./checker.js"; + +const CATEGORIES = { + product: ["project-brief.md", "research-report.md", "business-requirements.md", "prd.md", "srs.md"], + experience: ["ux-flows.md", "ui-system.md", "permission-matrix.md"], + engineering: ["trd.md", "database-schema.md", "api-documentation.md"], + quality: ["qa-test-plan.md"], + delivery: ["implementation-plan.md", "build-plan.md", "deployment-plan.md"] +}; + +export async function scoreProject(options) { + const check = await checkProject(options); + const categories = {}; + + for (const [name, files] of Object.entries(CATEGORIES)) { + const relevant = check.results.filter((item) => files.includes(item.file)); + + if (!relevant.length) { + continue; + } + + const present = relevant.filter((item) => item.found).length; + categories[name] = Math.round((present / relevant.length) * 100); + } + + return { + ...check, + categories + }; +} diff --git a/src/data/project-types.js b/src/data/project-types.js new file mode 100644 index 0000000..788a930 --- /dev/null +++ b/src/data/project-types.js @@ -0,0 +1,100 @@ +export const PROJECT_TYPES = { + small: { + label: "Small app", + documents: ["project-brief", "prd", "build-plan", "qa-test-plan"] + }, + portfolio: { + label: "Portfolio", + documents: ["project-brief", "ux-flows", "ui-system", "build-plan", "qa-test-plan"] + }, + saas: { + label: "SaaS product", + documents: [ + "project-brief", + "research-report", + "prd", + "ux-flows", + "trd", + "database-schema", + "api-documentation", + "permission-matrix", + "qa-test-plan", + "implementation-plan", + "deployment-plan" + ] + }, + crm: { + label: "CRM or operations system", + documents: [ + "business-requirements", + "research-report", + "prd", + "srs", + "permission-matrix", + "database-schema", + "api-documentation", + "qa-test-plan", + "implementation-plan" + ] + }, + ecommerce: { + label: "E-commerce or marketplace", + documents: [ + "project-brief", + "research-report", + "business-requirements", + "prd", + "ux-flows", + "trd", + "database-schema", + "api-documentation", + "permission-matrix", + "qa-test-plan", + "deployment-plan" + ] + }, + mobile: { + label: "Mobile app", + documents: [ + "project-brief", + "prd", + "ux-flows", + "api-documentation", + "permission-matrix", + "qa-test-plan", + "implementation-plan", + "deployment-plan" + ] + }, + "ai-agent": { + label: "AI agent", + documents: [ + "project-brief", + "research-report", + "prd", + "trd", + "database-schema", + "api-documentation", + "permission-matrix", + "qa-test-plan", + "implementation-plan", + "deployment-plan" + ] + }, + opensource: { + label: "Open-source tool", + documents: ["project-brief", "prd", "trd", "build-plan", "qa-test-plan", "deployment-plan"] + } +}; + +export function getProjectType(type) { + return PROJECT_TYPES[type] || null; +} + +export function getProjectDocs(type) { + return getProjectType(type)?.documents || null; +} + +export function getProjectTypes() { + return Object.keys(PROJECT_TYPES); +} diff --git a/src/data/templates.js b/src/data/templates.js new file mode 100644 index 0000000..550f26a --- /dev/null +++ b/src/data/templates.js @@ -0,0 +1,27 @@ +export const TEMPLATE_FILES = { + "project-brief": "project-brief.md", + "research-report": "research-report.md", + "business-requirements": "business-requirements.md", + prd: "prd.md", + srs: "srs.md", + "release-scope": "release-scope.md", + "ux-flows": "ux-flows.md", + "ui-system": "ui-system.md", + "permission-matrix": "permission-matrix.md", + trd: "trd.md", + "database-schema": "database-schema.md", + "api-documentation": "api-documentation.md", + "decision-record": "decision-record.md", + "implementation-plan": "implementation-plan.md", + "build-plan": "build-plan.md", + "qa-test-plan": "qa-test-plan.md", + "deployment-plan": "deployment-plan.md" +}; + +export function getTemplateFile(name) { + return TEMPLATE_FILES[name] || null; +} + +export function listTemplates() { + return Object.keys(TEMPLATE_FILES).sort(); +} diff --git a/src/fs-utils.js b/src/fs-utils.js index 299e5f2..735d344 100644 --- a/src/fs-utils.js +++ b/src/fs-utils.js @@ -1,4 +1,4 @@ -import { copyFile, mkdir, readFile, writeFile, access } from "node:fs/promises"; +import { access, copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import { constants } from "node:fs"; import { dirname } from "node:path"; @@ -15,20 +15,34 @@ export async function exists(path) { } } +export async function readText(path) { + return readFile(path, "utf8"); +} + +export async function readDirSafe(path) { + try { + return await readdir(path); + } catch { + return []; + } +} + +export async function writeText(path, content, { force = false } = {}) { + if (!force && await exists(path)) { + return { written: false, skipped: true, path }; + } + + await ensureDir(dirname(path)); + await writeFile(path, content, "utf8"); + return { written: true, skipped: false, path }; +} + export async function copyFileSafe(source, target, { force = false } = {}) { if (!force && await exists(target)) { return { copied: false, skipped: true, target }; } + await ensureDir(dirname(target)); await copyFile(source, target); return { copied: true, skipped: false, target }; } - -export async function readText(path) { - return readFile(path, "utf8"); -} - -export async function writeJson(path, data) { - await ensureDir(dirname(path)); - await writeFile(path, `${JSON.stringify(data, null, 2)}\n`, "utf8"); -} diff --git a/src/paths.js b/src/paths.js index 7fe09cc..9388a88 100644 --- a/src/paths.js +++ b/src/paths.js @@ -2,10 +2,11 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const SRC_DIR = dirname(fileURLToPath(import.meta.url)); + export const PACKAGE_ROOT = resolve(SRC_DIR, ".."); export const TEMPLATE_DIR = join(PACKAGE_ROOT, "templates"); export const PROMPT_DIR = join(PACKAGE_ROOT, "prompts"); -export function projectPath(...parts) { - return resolve(process.cwd(), ...parts); +export function projectPath(cwd, ...parts) { + return resolve(cwd, ...parts); } diff --git a/src/project-types.js b/src/project-types.js index 0768e5c..a2774e2 100644 --- a/src/project-types.js +++ b/src/project-types.js @@ -1,49 +1 @@ -export const PROJECT_TYPES = { - small: ["project-brief", "prd", "build-plan", "qa-test-plan"], - portfolio: ["project-brief", "ux-flows", "ui-system", "build-plan", "qa-test-plan"], - saas: [ - "project-brief", - "research-report", - "prd", - "ux-flows", - "trd", - "database-schema", - "api-documentation", - "permission-matrix", - "qa-test-plan", - "implementation-plan", - "deployment-plan" - ], - crm: [ - "business-requirements", - "research-report", - "prd", - "srs", - "permission-matrix", - "database-schema", - "api-documentation", - "qa-test-plan", - "implementation-plan" - ], - "ai-agent": [ - "project-brief", - "research-report", - "prd", - "trd", - "database-schema", - "api-documentation", - "permission-matrix", - "qa-test-plan", - "implementation-plan", - "deployment-plan" - ], - opensource: ["project-brief", "prd", "trd", "build-plan", "qa-test-plan", "deployment-plan"] -}; - -export function getProjectDocs(type) { - return PROJECT_TYPES[type] || null; -} - -export function getProjectTypes() { - return Object.keys(PROJECT_TYPES); -} +export { PROJECT_TYPES, getProjectDocs, getProjectType, getProjectTypes } from "./data/project-types.js"; diff --git a/src/template-map.js b/src/template-map.js index 826b3ac..2b79f9b 100644 --- a/src/template-map.js +++ b/src/template-map.js @@ -1,27 +1 @@ -export const TEMPLATE_FILES = { - "project-brief": "project-brief.md", - "research-report": "research-report.md", - "business-requirements": "business-requirements.md", - prd: "prd.md", - srs: "srs.md", - "release-scope": "release-scope.md", - "ux-flows": "ux-flows.md", - "ui-system": "ui-system.md", - "permission-matrix": "permission-matrix.md", - trd: "trd.md", - "database-schema": "database-schema.md", - "api-documentation": "api-documentation.md", - "decision-record": "decision-record.md", - "implementation-plan": "implementation-plan.md", - "build-plan": "build-plan.md", - "qa-test-plan": "qa-test-plan.md", - "deployment-plan": "deployment-plan.md" -}; - -export function getTemplateFile(name) { - return TEMPLATE_FILES[name] || null; -} - -export function listTemplates() { - return Object.keys(TEMPLATE_FILES); -} +export { TEMPLATE_FILES, getTemplateFile, listTemplates } from "./data/templates.js"; diff --git a/tests/add-check-score.test.js b/tests/add-check-score.test.js new file mode 100644 index 0000000..8a3e917 --- /dev/null +++ b/tests/add-check-score.test.js @@ -0,0 +1,52 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +test("add creates one template and check reports readiness", () => { + const cwd = mkdtempSync(join(tmpdir(), "beforecode-add-")); + + try { + execFileSync("node", [process.cwd() + "/bin/beforecode.js", "add", "prd"], { + cwd, + encoding: "utf8" + }); + + assert.ok(existsSync(join(cwd, "docs", "prd.md"))); + + const check = execFileSync("node", [process.cwd() + "/bin/beforecode.js", "check", "--type", "small"], { + cwd, + encoding: "utf8" + }); + + assert.match(check, /prd.md/); + assert.match(check, /Readiness:/); + + const score = execFileSync("node", [process.cwd() + "/bin/beforecode.js", "score", "--type", "small"], { + cwd, + encoding: "utf8" + }); + + assert.match(score, /Overall:/); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); + +test("handoff creates AGENTS.md", () => { + const cwd = mkdtempSync(join(tmpdir(), "beforecode-handoff-")); + + try { + execFileSync("node", [process.cwd() + "/bin/beforecode.js", "handoff", "--name", "Demo"], { + cwd, + encoding: "utf8" + }); + + assert.ok(existsSync(join(cwd, "AGENTS.md"))); + assert.ok(existsSync(join(cwd, "docs", "ai-handoff.md"))); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); diff --git a/tests/cli-help.test.js b/tests/cli-help.test.js new file mode 100644 index 0000000..182a71f --- /dev/null +++ b/tests/cli-help.test.js @@ -0,0 +1,16 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; + +test("help command prints usage", () => { + const output = execFileSync("node", ["./bin/beforecode.js", "help"], { encoding: "utf8" }); + assert.match(output, /BeforeCode CLI/); + assert.match(output, /beforecode init --type saas/); +}); + +test("list command prints project types", () => { + const output = execFileSync("node", ["./bin/beforecode.js", "list"], { encoding: "utf8" }); + assert.match(output, /Project types:/); + assert.match(output, /saas/); + assert.match(output, /Templates:/); +}); diff --git a/tests/init-command.test.js b/tests/init-command.test.js new file mode 100644 index 0000000..5b0472c --- /dev/null +++ b/tests/init-command.test.js @@ -0,0 +1,40 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { execFileSync } from "node:child_process"; +import { mkdtempSync, existsSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +test("init creates SaaS docs and config", () => { + const cwd = mkdtempSync(join(tmpdir(), "beforecode-init-")); + + try { + const output = execFileSync("node", [process.cwd() + "/bin/beforecode.js", "init", "--type", "saas"], { + cwd, + encoding: "utf8" + }); + + assert.match(output, /BeforeCode initialized/); + assert.ok(existsSync(join(cwd, "docs", "01-project-brief.md"))); + assert.ok(existsSync(join(cwd, "docs", "03-prd.md"))); + assert.ok(existsSync(join(cwd, ".beforecoderc.json"))); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +}); + +test("dry run does not create files", () => { + const cwd = mkdtempSync(join(tmpdir(), "beforecode-dry-")); + + try { + const output = execFileSync("node", [process.cwd() + "/bin/beforecode.js", "init", "--type", "small", "--dry-run"], { + cwd, + encoding: "utf8" + }); + + assert.match(output, /dry run/); + assert.equal(existsSync(join(cwd, "docs")), false); + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +});