From c69bea3777bc28726db72b4974be87513c605e7e Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 23 Nov 2025 18:24:12 -0700 Subject: [PATCH 01/24] docs: update README with comprehensive command documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add table of contents for better navigation • Document all implemented commands (commit, init, config) • Include installation, quick start, and configuration sections • Add examples and usage patterns for each command • Mark planned features section for future commands --- README.md | 199 +++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 191 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dce4762..29ffffd 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,187 @@ -# `@labcatr/labcommitr` +# Labcommitr -A solution for building standardized git commits! +A CLI tool for creating standardized Git commits with customizable workflows and presets. **Labcommitr** is used internally for all @labcatr projects. However, feel free to use it for your own projects! -## Plans +## Table of Contents -**Command**: `labcommitr` (alias `lc`) +- [Installation](#installation) +- [Quick Start](#quick-start) +- [Commands](#commands) + - [commit](#commit) + - [init](#init) + - [config](#config) +- [Configuration](#configuration) +- [Development & Testing](#development--testing) +- [Contributing](#contributing) -### Commands +## Installation -`labcommitr commit` : Start a `labcommitr` commit builder. +```bash +npm install -g @labcatr/labcommitr +# or +pnpm add -g @labcatr/labcommitr +``` + +After installation, use either `labcommitr` or `lab` to run commands. + +## Quick Start + +1. **Initialize configuration** in your project: + ```bash + lab init + ``` + +2. **Create your first commit**: + ```bash + lab commit + ``` + +3. **Or commit quickly with flags**: + ```bash + lab commit -t feat -m "add new feature" + ``` + +## Commands + +### commit + +Create a standardized commit following your project's configuration. + +**Usage:** +```bash +lab commit [options] +lab c [options] # Short alias +``` + +**Options:** +- `-t, --type ` - Commit type (e.g., `feat`, `fix`, `docs`) +- `-s, --scope ` - Commit scope (e.g., `api`, `auth`, `ui`) +- `-m, --message ` - Commit subject/message +- `-b, --body ` - Commit body/description +- `--no-verify` - Bypass Git hooks + +**Examples:** +```bash +# Interactive commit (prompts for missing fields) +lab commit + +# Quick commit with type and message +lab commit -t feat -m "add user authentication" + +# Full commit with all fields +lab commit -t feat -s api -m "add endpoint" -b "Implements REST endpoint for user data" + +# Commit without running Git hooks +lab commit -t fix -m "fix bug" --no-verify +``` + +**Notes:** +- Messages and body with spaces must be quoted +- If all required fields are provided via flags, the commit is created immediately +- If any required fields are missing, an interactive prompt guides you through completion +- Supports keyboard shortcuts for faster navigation (see configuration) + +--- + +### init + +Initialize Labcommitr configuration in your project. This creates a `.labcommitr.config.yaml` file with your chosen preset and preferences. + +**Usage:** +```bash +lab init [options] +``` + +**Options:** +- `-f, --force` - Overwrite existing configuration file +- `--preset ` - Use a specific preset without prompts (options: `conventional`, `gitmoji`, `angular`, `minimal`) + +**Examples:** +```bash +# Interactive setup (recommended) +lab init + +# Overwrite existing configuration +lab init --force + +# Use a specific preset without prompts +lab init --preset conventional +``` -`labcommitr init`, `-i`: Create a file called `.labcommitrc` in the root directory of the current git repo. +**What it does:** +- Guides you through selecting a commit convention preset +- Configures emoji support based on terminal capabilities +- Sets up auto-staging preferences +- Generates `.labcommitr.config.yaml` with default shortcuts enabled +- Validates the generated configuration -`labcommitr go [...message]`: Quickly submit a commit of the specified type with a message. If a message is not specified, a generic one will be generated for you (it is not good practice, however its BLAZINGLY FAST). +**Presets available:** +- **Conventional Commits** - Popular across open-source and personal projects +- **Angular Convention** - Strict format used by Angular and enterprise teams (includes `perf`, `build`, `ci` types) +- **Gitmoji** - Emoji-based commits for visual clarity +- **Minimal** - Start with basics, customize everything yourself later + +--- + +### config + +Manage and inspect Labcommitr configuration. + +**Usage:** +```bash +lab config +``` + +**Subcommands:** + +#### show + +Display the currently loaded configuration with source information. + +**Usage:** +```bash +lab config show [options] +``` + +**Options:** +- `-p, --path ` - Start configuration search from a specific directory + +**Examples:** +```bash +# Show current configuration +lab config show + +# Show configuration from a specific directory +lab config show --path /path/to/project +``` + +**What it shows:** +- Configuration source (file path or "defaults") +- Emoji mode status +- Full configuration in JSON format +- Helpful warnings if using default configuration + +--- + +## Configuration + +Labcommitr uses a `.labcommitr.config.yaml` file in your project root. The configuration file supports: + +- **Commit types** - Define custom commit types with descriptions +- **Format options** - Configure scope, body, and emoji requirements +- **Keyboard shortcuts** - Enable and customize shortcuts for faster navigation +- **Git integration** - Auto-staging and commit signing preferences + +See [`CONFIG_SCHEMA.md`](CONFIG_SCHEMA.md) for complete configuration documentation. + +**Configuration discovery:** +- Searches from current directory up to project root +- Falls back to global configuration if available +- Uses sensible defaults if no configuration found + +--- ## Development & Testing @@ -37,3 +204,19 @@ pnpm run test:sandbox:clean ``` See [`scripts/README.md`](scripts/README.md) for complete testing documentation. + +--- + +## Contributing + +Contributions are welcome! Please ensure your commits follow the project's commit message format (which you can set up using `lab init`). + +For development guidelines, see [`DEVELOPMENT_GUIDELINES.md`](DEVELOPMENT_GUIDELINES.md). + +--- + +## Planned Features + +The following commands are planned but not yet implemented: + +- `lab go [...message]` - Quickly submit a commit of the specified type with a message. If a message is not specified, a generic one will be generated (fast but not recommended for production use). From d7fad7c94e5eab08ade4e7d570f1a90a386e6fde Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 23 Nov 2025 19:54:11 -0700 Subject: [PATCH 02/24] feat: add preview and revert commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add preview command for browsing commit history • Add revert command with commit workflow integration • Implement commit history pagination (50 commits per batch, max 100) • Add merge commit handling with parent selection • Add commit message parsing for revert templates • Integrate revert with existing commit workflow • Add conflict resolution support (--continue, --abort) • Register preview and revert commands in main program --- src/cli/commands/preview/index.ts | 238 ++++++++++ src/cli/commands/preview/prompts.ts | 338 ++++++++++++++ src/cli/commands/preview/types.ts | 14 + src/cli/commands/revert/index.ts | 538 ++++++++++++++++++++++ src/cli/commands/revert/prompts.ts | 161 +++++++ src/cli/commands/revert/types.ts | 13 + src/cli/commands/shared/commit-parser.ts | 99 ++++ src/cli/commands/shared/git-operations.ts | 288 ++++++++++++ src/cli/commands/shared/types.ts | 86 ++++ src/cli/program.ts | 12 +- 10 files changed, 1786 insertions(+), 1 deletion(-) create mode 100644 src/cli/commands/preview/index.ts create mode 100644 src/cli/commands/preview/prompts.ts create mode 100644 src/cli/commands/preview/types.ts create mode 100644 src/cli/commands/revert/index.ts create mode 100644 src/cli/commands/revert/prompts.ts create mode 100644 src/cli/commands/revert/types.ts create mode 100644 src/cli/commands/shared/commit-parser.ts create mode 100644 src/cli/commands/shared/git-operations.ts create mode 100644 src/cli/commands/shared/types.ts diff --git a/src/cli/commands/preview/index.ts b/src/cli/commands/preview/index.ts new file mode 100644 index 0000000..6543e74 --- /dev/null +++ b/src/cli/commands/preview/index.ts @@ -0,0 +1,238 @@ +/** + * Preview Command + * + * Browse and inspect commit history without modifying the repository + */ + +import { Command } from "commander"; +import { Logger } from "../../../lib/logger.js"; +import { + isGitRepository, + getCurrentBranch, + fetchCommits, + getCommitDetails, + getCommitDiff, +} from "../shared/git-operations.js"; +import type { CommitInfo } from "../shared/types.js"; +import { + displayCommitList, + displayCommitDetails, + displayHelp, + waitForDetailAction, + waitForListAction, +} from "./prompts.js"; +import { textColors } from "../init/colors.js"; + +/** + * Clear terminal screen + */ +function clearTerminal(): void { + if (process.stdout.isTTY) { + process.stdout.write("\x1B[2J"); + process.stdout.write("\x1B[H"); + } +} + +/** + * Preview action handler + */ +async function previewAction(options: { + limit?: number; + branch?: string; +}): Promise { + try { + // Check git repository + if (!isGitRepository()) { + Logger.error("Not a git repository"); + console.error("\n Initialize git first: git init\n"); + process.exit(1); + } + + const currentBranch = getCurrentBranch(); + if (!currentBranch) { + Logger.error("Could not determine current branch"); + process.exit(1); + } + + const branch = options.branch || currentBranch; + const maxCommits = Math.min(options.limit || 50, 100); + const pageSize = 10; + + // Initial fetch + let allCommits: CommitInfo[] = []; + let totalFetched = 0; + let hasMore = true; + let currentPage = 0; + + const loadMoreCommits = async (): Promise => { + if (totalFetched >= maxCommits) { + hasMore = false; + return; + } + + const remaining = maxCommits - totalFetched; + const toFetch = Math.min(remaining, 50); + const newCommits = fetchCommits(toFetch, branch); + allCommits = [...allCommits, ...newCommits]; + totalFetched = allCommits.length; + hasMore = newCommits.length === 50 && totalFetched < maxCommits; + }; + + // Load initial batch + await loadMoreCommits(); + + if (allCommits.length === 0) { + console.log("\n No commits found in current branch.\n"); + process.exit(0); + } + + // Main loop + let exit = false; + let viewingDetails = false; + let currentDetailCommit: CommitInfo | null = null; + + while (!exit) { + clearTerminal(); + + if (viewingDetails && currentDetailCommit) { + // Detail view + displayCommitDetails(currentDetailCommit); + console.log( + ` ${textColors.white("Press")} ${textColors.brightYellow("b")} ${textColors.white("for body,")} ${textColors.brightYellow("f")} ${textColors.white("for files,")} ${textColors.brightYellow("d")} ${textColors.white("for diff,")} ${textColors.brightYellow("r")} ${textColors.white("to revert,")} ${textColors.brightYellow("←")} ${textColors.white("to go back")}`, + ); + + const action = await waitForDetailAction(); + + switch (action) { + case "back": + viewingDetails = false; + currentDetailCommit = null; + break; + case "body": + // Body is already shown in details + break; + case "files": + // Files are already shown in details + break; + case "diff": + clearTerminal(); + console.log( + `\n${textColors.brightWhite("Diff for commit")} ${currentDetailCommit.shortHash}:\n`, + ); + const diff = getCommitDiff(currentDetailCommit.hash); + console.log(diff); + console.log( + `\n${textColors.white("Press any key to go back...")}`, + ); + await new Promise((resolve) => { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.once("data", () => { + process.stdin.setRawMode(false); + process.stdin.pause(); + resolve(null); + }); + }); + break; + case "revert": + // Switch to revert command + console.log("\n Switching to revert command...\n"); + exit = true; + // Import and call revert with this commit + const revertModule = await import("../revert/index.js"); + await revertModule.revertCommit(currentDetailCommit.hash); + break; + case "help": + displayHelp(); + await new Promise((resolve) => { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.once("data", () => { + process.stdin.setRawMode(false); + process.stdin.pause(); + resolve(null); + }); + }); + break; + case "exit": + exit = true; + break; + } + } else { + // List view + const startIndex = currentPage * pageSize; + const endIndex = Math.min(startIndex + pageSize, allCommits.length); + const pageCommits = allCommits.slice(startIndex, endIndex); + const maxIndex = pageCommits.length - 1; + + displayCommitList(pageCommits, startIndex, totalFetched, hasMore); + + const action = await waitForListAction(maxIndex, hasMore); + + if (typeof action === "number") { + // View commit details + const commit = pageCommits[action]; + // Load full details if not already loaded + if (!commit.body || !commit.fileStats) { + try { + const fullDetails = getCommitDetails(commit.hash); + // Update the commit in our array + const index = allCommits.findIndex((c) => c.hash === commit.hash); + if (index >= 0) { + allCommits[index] = fullDetails; + } + currentDetailCommit = fullDetails; + } catch (error) { + Logger.error(`Failed to load commit details: ${error}`); + continue; + } + } else { + currentDetailCommit = commit; + } + viewingDetails = true; + } else if (action === "next") { + // Load next batch + if (hasMore) { + console.log("\n Loading next batch..."); + await loadMoreCommits(); + if (!hasMore) { + console.log(" Maximum commits loaded (100)."); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + } else if (action === "help") { + displayHelp(); + await new Promise((resolve) => { + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.once("data", () => { + process.stdin.setRawMode(false); + process.stdin.pause(); + resolve(null); + }); + }); + } else if (action === "exit") { + exit = true; + } + } + } + + console.log("\n Exiting preview.\n"); + } catch (error: unknown) { + Logger.error("Failed to preview commits"); + if (error instanceof Error) { + console.error(`\n ${error.message}\n`); + } + process.exit(1); + } +} + +/** + * Preview command + */ +export const previewCommand = new Command("preview") + .description("Browse and inspect commit history") + .option("-l, --limit ", "Maximum commits to fetch (default: 50, max: 100)", "50") + .option("-b, --branch ", "Branch to preview (default: current branch)") + .action(previewAction); + diff --git a/src/cli/commands/preview/prompts.ts b/src/cli/commands/preview/prompts.ts new file mode 100644 index 0000000..edd7afc --- /dev/null +++ b/src/cli/commands/preview/prompts.ts @@ -0,0 +1,338 @@ +/** + * Preview Command Prompts + * + * Interactive prompts for browsing commit history + */ + +import { select, isCancel } from "@clack/prompts"; +import { labelColors, textColors } from "../init/colors.js"; +import type { CommitInfo } from "../shared/types.js"; +import { getCommitDetails, getCommitDiff } from "../shared/git-operations.js"; +import readline from "readline"; + +/** + * Create compact color-coded label + */ +function label( + text: string, + color: "magenta" | "cyan" | "blue" | "yellow" | "green", +): string { + const colorFn = { + magenta: labelColors.bgBrightMagenta, + cyan: labelColors.bgBrightCyan, + blue: labelColors.bgBrightBlue, + yellow: labelColors.bgBrightYellow, + green: labelColors.bgBrightGreen, + }[color]; + + const width = 7; + const textLength = Math.min(text.length, width); + const padding = width - textLength; + const leftPad = Math.ceil(padding / 2); + const rightPad = padding - leftPad; + const centeredText = + " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + + return colorFn(` ${centeredText} `); +} + +/** + * Display commit list + */ +export function displayCommitList( + commits: CommitInfo[], + startIndex: number, + totalFetched: number, + hasMore: boolean, +): void { + console.log(); + console.log( + `${label("preview", "cyan")} ${textColors.pureWhite("Commit History")}`, + ); + console.log(); + + if (commits.length === 0) { + console.log(" No commits found."); + return; + } + + // Display commits with number shortcuts + const displayCount = Math.min(commits.length, 10); + for (let i = 0; i < displayCount; i++) { + const commit = commits[i]; + const number = i.toString(); + const mergeIndicator = commit.isMerge ? " [Merge]" : ""; + const truncatedSubject = + commit.subject.length > 50 + ? commit.subject.substring(0, 47) + "..." + : commit.subject; + + console.log( + ` ${textColors.brightCyan(`[${number}]`)} ${textColors.brightWhite(commit.shortHash)} ${truncatedSubject}${mergeIndicator}`, + ); + console.log( + ` ${textColors.white(commit.author.name)} • ${textColors.white(commit.date.relative)}`, + ); + } + + // Pagination info + const endIndex = startIndex + displayCount; + console.log(); + if (hasMore) { + console.log( + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+ (press ${textColors.brightYellow("n")} for next batch)`, + ); + } else { + console.log( + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}`, + ); + } + console.log(); + console.log( + ` ${textColors.white("Press")} ${textColors.brightCyan("0-9")} ${textColors.white("to view details,")} ${textColors.brightYellow("n")} ${textColors.white("for next batch,")} ${textColors.brightYellow("?")} ${textColors.white("for help, or")} ${textColors.brightYellow("Esc")} ${textColors.white("to exit")}`, + ); +} + +/** + * Display commit details + */ +export function displayCommitDetails(commit: CommitInfo): void { + console.log(); + console.log( + `${label("detail", "green")} ${textColors.pureWhite("Commit Details")}`, + ); + console.log(); + console.log(` ${textColors.brightWhite("Hash:")} ${commit.hash}`); + console.log(` ${textColors.brightWhite("Subject:")} ${commit.subject}`); + console.log(); + console.log(` ${textColors.brightWhite("Author:")} ${commit.author.name} <${commit.author.email}>`); + console.log(` ${textColors.brightWhite("Date:")} ${commit.date.absolute}`); + console.log(` ${textColors.brightWhite("Relative:")} ${commit.date.relative}`); + console.log(); + + if (commit.parents.length > 0) { + console.log(` ${textColors.brightWhite("Parents:")}`); + commit.parents.forEach((parent) => { + console.log(` ${parent.substring(0, 7)}`); + }); + console.log(); + } + + if (commit.isMerge) { + console.log(` ${textColors.brightYellow("⚠ This is a merge commit")}`); + console.log(); + } + + if (commit.fileStats) { + console.log(` ${textColors.brightWhite("File Statistics:")}`); + console.log(` Files changed: ${commit.fileStats.filesChanged}`); + if (commit.fileStats.additions !== undefined) { + console.log(` Additions: ${textColors.gitAdded(`+${commit.fileStats.additions}`)}`); + } + if (commit.fileStats.deletions !== undefined) { + console.log(` Deletions: ${textColors.gitDeleted(`-${commit.fileStats.deletions}`)}`); + } + console.log(); + } + + if (commit.body) { + console.log(` ${textColors.brightWhite("Body:")}`); + const bodyLines = commit.body.split("\n"); + bodyLines.forEach((line) => { + console.log(` ${line}`); + }); + console.log(); + } else { + console.log(` ${textColors.white("Body:")} No body`); + console.log(); + } + + if (commit.files && commit.files.length > 0) { + console.log(` ${textColors.brightWhite("Changed Files:")}`); + commit.files.slice(0, 20).forEach((file) => { + console.log(` ${file}`); + }); + if (commit.files.length > 20) { + console.log(` ... and ${commit.files.length - 20} more`); + } + console.log(); + } +} + +/** + * Display help + */ +export function displayHelp(): void { + console.log(); + console.log( + `${label("help", "yellow")} ${textColors.pureWhite("Keyboard Shortcuts")}`, + ); + console.log(); + console.log(` ${textColors.brightCyan("0-9")} View commit details`); + console.log(` ${textColors.brightYellow("↑/↓")} Navigate list`); + console.log(` ${textColors.brightYellow("n")} Jump to next batch`); + console.log(` ${textColors.brightYellow("b")} View/toggle body`); + console.log(` ${textColors.brightYellow("f")} View/toggle files`); + console.log(` ${textColors.brightYellow("d")} View diff`); + console.log(` ${textColors.brightYellow("r")} Revert this commit`); + console.log(` ${textColors.brightYellow("←/Esc")} Back to list`); + console.log(` ${textColors.brightYellow("?")} Show this help`); + console.log(` ${textColors.brightYellow("q")} Exit`); + console.log(); +} + +/** + * Wait for user input in detail view + */ +export async function waitForDetailAction(): Promise< + "back" | "body" | "files" | "diff" | "revert" | "help" | "exit" +> { + return new Promise((resolve) => { + const stdin = process.stdin; + const wasRaw = stdin.isRaw; + + if (!wasRaw) { + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + } + + readline.emitKeypressEvents(stdin); + + const onKeypress = (char: string, key: readline.Key) => { + if (key.name === "escape" || (key.ctrl && key.name === "c")) { + cleanup(); + resolve("exit"); + return; + } + + if (key.name === "left" || (key.name === "escape" && !key.ctrl)) { + cleanup(); + resolve("back"); + return; + } + + if (char === "b" || char === "B") { + cleanup(); + resolve("body"); + return; + } + + if (char === "f" || char === "F") { + cleanup(); + resolve("files"); + return; + } + + if (char === "d" || char === "D") { + cleanup(); + resolve("diff"); + return; + } + + if (char === "r" || char === "R") { + cleanup(); + resolve("revert"); + return; + } + + if (char === "?") { + cleanup(); + resolve("help"); + return; + } + + if (char === "q" || char === "Q") { + cleanup(); + resolve("exit"); + return; + } + }; + + const cleanup = () => { + stdin.removeListener("keypress", onKeypress); + if (!wasRaw) { + stdin.setRawMode(false); + stdin.pause(); + } + }; + + stdin.on("keypress", onKeypress); + }); +} + +/** + * Wait for user input in list view + */ +export async function waitForListAction( + maxIndex: number, + hasMore: boolean, +): Promise { + return new Promise((resolve) => { + const stdin = process.stdin; + const wasRaw = stdin.isRaw; + + if (!wasRaw) { + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + } + + readline.emitKeypressEvents(stdin); + + const onKeypress = (char: string, key: readline.Key) => { + if (key.name === "escape" || (key.ctrl && key.name === "c")) { + cleanup(); + resolve("exit"); + return; + } + + if (key.name === "up" || key.name === "down") { + // Let user navigate with arrows (we'll handle this in the main loop) + return; + } + + // Number keys 0-9 + if (/^[0-9]$/.test(char)) { + const num = parseInt(char, 10); + if (num <= maxIndex) { + cleanup(); + resolve(num); + return; + } + } + + // Next batch + if ((char === "n" || char === "N") && hasMore) { + cleanup(); + resolve("next"); + return; + } + + // Help + if (char === "?") { + cleanup(); + resolve("help"); + return; + } + + // Exit + if (char === "q" || char === "Q") { + cleanup(); + resolve("exit"); + return; + } + }; + + const cleanup = () => { + stdin.removeListener("keypress", onKeypress); + if (!wasRaw) { + stdin.setRawMode(false); + stdin.pause(); + } + }; + + stdin.on("keypress", onKeypress); + }); +} + diff --git a/src/cli/commands/preview/types.ts b/src/cli/commands/preview/types.ts new file mode 100644 index 0000000..7104bed --- /dev/null +++ b/src/cli/commands/preview/types.ts @@ -0,0 +1,14 @@ +/** + * Preview Command Types + */ + +export interface PreviewState { + commits: import("../shared/types.js").CommitInfo[]; + currentPage: number; + pageSize: number; + totalFetched: number; + maxCommits: number; + hasMore: boolean; + currentIndex: number; +} + diff --git a/src/cli/commands/revert/index.ts b/src/cli/commands/revert/index.ts new file mode 100644 index 0000000..5d5e331 --- /dev/null +++ b/src/cli/commands/revert/index.ts @@ -0,0 +1,538 @@ +/** + * Revert Command + * + * Select and revert commits using the project's commit workflow + */ + +import { Command } from "commander"; +import { Logger } from "../../../lib/logger.js"; +import { loadConfig } from "../../../lib/config/index.js"; +import { + isGitRepository, + getCurrentBranch, + fetchCommits, + getCommitDetails, + isMergeCommit, + getMergeParents, + hasUncommittedChanges, +} from "../shared/git-operations.js"; +import { parseCommitMessage, generateRevertSubject } from "../shared/commit-parser.js"; +import type { CommitInfo } from "../shared/types.js"; +import { + displayRevertCommitList, + promptMergeParent, + displayRevertConfirmation, + promptRevertConfirmation, +} from "./prompts.js"; +import { + promptType, + promptScope, + promptSubject, + promptBody, + displayPreview, +} from "../commit/prompts.js"; +import { formatCommitMessage } from "../commit/formatter.js"; +import { createCommit } from "../commit/git.js"; +import { spawnSync } from "child_process"; +import readline from "readline"; +import { textColors, success, attention } from "../init/colors.js"; + +/** + * Clear terminal screen + */ +function clearTerminal(): void { + if (process.stdout.isTTY) { + process.stdout.write("\x1B[2J"); + process.stdout.write("\x1B[H"); + } +} + +/** + * Execute git revert command + */ +function execGitRevert(hash: string, parentNumber?: number): void { + const args = ["revert", "--no-edit"]; // We'll create our own commit message + + if (parentNumber !== undefined) { + args.push("-m", parentNumber.toString()); + } + + args.push(hash); + + const result = spawnSync("git", args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.status !== 0) { + const stderr = result.stderr?.toString() || "Unknown error"; + throw new Error(`Git revert failed: ${stderr}`); + } +} + +/** + * Check if revert is in progress + */ +function isRevertInProgress(): boolean { + try { + const result = spawnSync("git", ["status"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + return result.stdout?.toString().includes("revert") || false; + } catch { + return false; + } +} + +/** + * Continue revert after conflict resolution + */ +function continueRevert(): void { + const result = spawnSync("git", ["revert", "--continue"], { + encoding: "utf-8", + stdio: "inherit", + }); + + if (result.status !== 0) { + throw new Error("Failed to continue revert"); + } +} + +/** + * Abort revert + */ +function abortRevert(): void { + const result = spawnSync("git", ["revert", "--abort"], { + encoding: "utf-8", + stdio: "inherit", + }); + + if (result.status !== 0) { + throw new Error("Failed to abort revert"); + } +} + +/** + * Revert a specific commit (called from preview or directly) + */ +export async function revertCommit( + commitHash: string, + options?: { noEdit?: boolean; parentNumber?: number }, +): Promise { + try { + // Load config + const configResult = await loadConfig(); + if (configResult.source === "defaults") { + Logger.error("Configuration not found"); + console.error("\n Run 'lab init' to create configuration file.\n"); + process.exit(1); + } + + const config = configResult.config; + + // Get commit details + const commit = getCommitDetails(commitHash); + + // Check if merge commit + let parentNumber = options?.parentNumber; + if (commit.isMerge && parentNumber === undefined) { + const parents = getMergeParents(commitHash); + if (parents.length > 1) { + clearTerminal(); + parentNumber = await promptMergeParent(parents); + } else { + parentNumber = 1; // Default to first parent + } + } + + // Show confirmation + clearTerminal(); + displayRevertConfirmation(commit); + + let useWorkflow = !options?.noEdit; + if (useWorkflow) { + const confirmResult = await promptRevertConfirmation(); + if (confirmResult === "cancel") { + console.log("\n Revert cancelled.\n"); + process.exit(0); + } + useWorkflow = confirmResult === "edit"; + } + + // Execute revert using commit workflow + if (useWorkflow) { + // Parse original commit message + const parsed = parseCommitMessage(commit.subject); + const parsedBody = commit.body ? parseCommitMessage(commit.body) : null; + + // Determine type + let type: string; + let emoji: string | undefined; + const revertType = config.types.find((t) => t.id === "revert"); + if (revertType) { + type = "revert"; + emoji = revertType.emoji; + } else { + // Let user select type + clearTerminal(); + const typeResult = await promptType(config); + type = typeResult.type; + emoji = typeResult.emoji; + } + + // Determine scope + let scope: string | undefined; + if (parsed.scope) { + // Pre-fill with extracted scope + scope = await promptScope(config, type, undefined, parsed.scope); + } else { + scope = await promptScope(config, type); + } + + // Determine subject + const maxLength = config.format.subject_max_length; + const defaultSubject = parsed.parseSuccess + ? generateRevertSubject(parsed.subject, maxLength) + : generateRevertSubject(commit.subject, maxLength); + + clearTerminal(); + let subject = await promptSubject(config, undefined, defaultSubject); + + // Determine body + let body: string | undefined; + if (config.format.body.required) { + const defaultBody = `This reverts commit ${commit.hash}.`; + body = await promptBody(config, undefined, defaultBody); + } else { + // Optional body - pre-fill but allow skip + const defaultBody = `This reverts commit ${commit.hash}.`; + body = await promptBody(config, undefined, defaultBody); + } + + // Preview + clearTerminal(); + let formattedMessage = formatCommitMessage( + config, + type, + emoji, + scope, + subject, + ); + + let action: "commit" | "edit-type" | "edit-scope" | "edit-subject" | "edit-body" | "cancel"; + + do { + // Regenerate formatted message with current values + formattedMessage = formatCommitMessage( + config, + type, + emoji, + scope, + subject, + ); + + action = await displayPreview(formattedMessage, body, config); + + if (action === "edit-type") { + const typeResult = await promptType(config, undefined, type); + type = typeResult.type; + emoji = typeResult.emoji; + const isScopeRequired = config.validation.require_scope_for.includes(type); + if (isScopeRequired && !scope) { + scope = await promptScope(config, type, undefined, scope); + } + } else if (action === "edit-scope") { + scope = await promptScope(config, type, undefined, scope); + } else if (action === "edit-subject") { + subject = await promptSubject(config, undefined, subject); + } else if (action === "edit-body") { + body = await promptBody(config, body); + } else if (action === "cancel") { + console.log("\n Revert cancelled.\n"); + process.exit(0); + } + + if (action !== "commit") { + clearTerminal(); + } + } while (action !== "commit"); + + // Execute revert + console.log(); + console.log("◐ Reverting commit..."); + + try { + // First, do the git revert (this stages the changes) + execGitRevert(commitHash, parentNumber); + + // Now amend the commit with our formatted message + const args = ["commit", "--amend", "-m", formattedMessage]; + if (body) { + args.push("-m", body); + } + if (config.advanced.git.sign_commits) { + args.push("-S"); + } + + const amendResult = spawnSync("git", args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (amendResult.status !== 0) { + throw new Error(`Failed to amend commit: ${amendResult.stderr?.toString() || "Unknown error"}`); + } + + // Get commit hash + const hashResult = spawnSync("git", ["rev-parse", "HEAD"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + const revertHash = hashResult.stdout?.toString().trim().substring(0, 7) || "unknown"; + + console.log(`${success("✓")} Revert commit created successfully!`); + console.log(` ${revertHash} ${formattedMessage}`); + } catch (error: unknown) { + // Check if it's a conflict + if (error instanceof Error && error.message.includes("conflict")) { + console.log(); + console.log( + `${attention("⚠ Conflicts detected during revert.")}`, + ); + console.log(); + console.log(" Resolve conflicts manually, then:"); + console.log(` ${textColors.brightCyan("lab revert --continue")} - Continue after resolution`); + console.log(` ${textColors.brightCyan("lab revert --abort")} - Abort revert`); + process.exit(1); + } + throw error; + } + } else { + // Use Git's default revert message + console.log(); + console.log("◐ Reverting commit..."); + + try { + execGitRevert(commitHash, parentNumber); + const hashResult = spawnSync("git", ["rev-parse", "HEAD"], { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + const revertHash = hashResult.stdout?.toString().trim().substring(0, 7) || "unknown"; + + console.log(`${success("✓")} Revert commit created successfully!`); + console.log(` ${revertHash}`); + } catch (error: unknown) { + if (error instanceof Error && error.message.includes("conflict")) { + console.log(); + console.log( + `${attention("⚠ Conflicts detected during revert.")}`, + ); + console.log(); + console.log(" Resolve conflicts manually, then:"); + console.log(` ${textColors.brightCyan("lab revert --continue")} - Continue after resolution`); + console.log(` ${textColors.brightCyan("lab revert --abort")} - Abort revert`); + process.exit(1); + } + throw error; + } + } + } catch (error: unknown) { + Logger.error("Failed to revert commit"); + if (error instanceof Error) { + console.error(`\n ${error.message}\n`); + } + process.exit(1); + } +} + +/** + * Revert action handler + */ +async function revertAction(options: { + limit?: number; + branch?: string; + noEdit?: boolean; + continue?: boolean; + abort?: boolean; +}): Promise { + try { + // Handle continue/abort flags + if (options.continue) { + continueRevert(); + return; + } + + if (options.abort) { + abortRevert(); + return; + } + + // Check git repository + if (!isGitRepository()) { + Logger.error("Not a git repository"); + console.error("\n Initialize git first: git init\n"); + process.exit(1); + } + + // Check for uncommitted changes + if (hasUncommittedChanges()) { + console.log(); + console.log( + `${attention("⚠ You have uncommitted changes.")}`, + ); + console.log(" Revert may cause conflicts."); + console.log(); + } + + const currentBranch = getCurrentBranch(); + if (!currentBranch) { + Logger.error("Could not determine current branch"); + process.exit(1); + } + + const branch = options.branch || currentBranch; + const maxCommits = Math.min(parseInt(options.limit?.toString() || "50", 10), 100); + const pageSize = 10; + + // Initial fetch + let allCommits: CommitInfo[] = []; + let totalFetched = 0; + let hasMore = true; + + const loadMoreCommits = async (): Promise => { + if (totalFetched >= maxCommits) { + hasMore = false; + return; + } + + const remaining = maxCommits - totalFetched; + const toFetch = Math.min(remaining, 50); + const newCommits = fetchCommits(toFetch, branch); + allCommits = [...allCommits, ...newCommits]; + totalFetched = allCommits.length; + hasMore = newCommits.length === 50 && totalFetched < maxCommits; + }; + + await loadMoreCommits(); + + if (allCommits.length === 0) { + console.log("\n No commits found in current branch.\n"); + process.exit(0); + } + + // Interactive selection + let selectedCommit: CommitInfo | null = null; + let currentPage = 0; + + while (!selectedCommit) { + clearTerminal(); + const startIndex = currentPage * pageSize; + const endIndex = Math.min(startIndex + pageSize, allCommits.length); + const pageCommits = allCommits.slice(startIndex, endIndex); + + displayRevertCommitList(pageCommits, startIndex, totalFetched, hasMore); + + console.log( + ` ${textColors.white("Press")} ${textColors.brightCyan("0-9")} ${textColors.white("to select commit,")} ${textColors.brightYellow("n")} ${textColors.white("for next batch,")} ${textColors.brightYellow("Esc")} ${textColors.white("to cancel")}`, + ); + + // Wait for input + const stdin = process.stdin; + const wasRaw = stdin.isRaw; + + if (!wasRaw) { + stdin.setRawMode(true); + stdin.resume(); + stdin.setEncoding("utf8"); + } + + readline.emitKeypressEvents(stdin); + + const selection = await new Promise((resolve) => { + const onKeypress = (char: string, key: readline.Key) => { + if (key.name === "escape" || (key.ctrl && key.name === "c")) { + cleanup(); + resolve("cancel"); + return; + } + + if (/^[0-9]$/.test(char)) { + const num = parseInt(char, 10); + if (num < pageCommits.length) { + cleanup(); + resolve(num); + return; + } + } + + if ((char === "n" || char === "N") && hasMore) { + cleanup(); + resolve("next"); + return; + } + }; + + const cleanup = () => { + stdin.removeListener("keypress", onKeypress); + if (!wasRaw) { + stdin.setRawMode(false); + stdin.pause(); + } + }; + + stdin.on("keypress", onKeypress); + }); + + if (selection === "cancel") { + console.log("\n Revert cancelled.\n"); + process.exit(0); + } else if (selection === "next") { + if (hasMore) { + console.log("\n Loading next batch..."); + await loadMoreCommits(); + if (!hasMore) { + console.log(" Maximum commits loaded (100)."); + await new Promise((resolve) => setTimeout(resolve, 1000)); + } + } + } else if (typeof selection === "number") { + selectedCommit = pageCommits[selection]; + // Load full details if needed + if (!selectedCommit.body || !selectedCommit.fileStats) { + try { + selectedCommit = getCommitDetails(selectedCommit.hash); + } catch (error) { + Logger.error(`Failed to load commit details: ${error}`); + selectedCommit = null; + continue; + } + } + } + } + + if (selectedCommit) { + await revertCommit(selectedCommit.hash, { noEdit: options.noEdit }); + } + } catch (error: unknown) { + Logger.error("Failed to revert commit"); + if (error instanceof Error) { + console.error(`\n ${error.message}\n`); + } + process.exit(1); + } +} + +/** + * Revert command + */ +export const revertCommand = new Command("revert") + .description("Revert a commit using the project's commit workflow") + .option("-l, --limit ", "Maximum commits to fetch (default: 50, max: 100)", "50") + .option("-b, --branch ", "Branch to revert from (default: current branch)") + .option("--no-edit", "Skip commit message editing (use Git defaults)") + .option("--continue", "Continue revert after conflict resolution") + .option("--abort", "Abort revert in progress") + .action(revertAction); + diff --git a/src/cli/commands/revert/prompts.ts b/src/cli/commands/revert/prompts.ts new file mode 100644 index 0000000..8e530ca --- /dev/null +++ b/src/cli/commands/revert/prompts.ts @@ -0,0 +1,161 @@ +/** + * Revert Command Prompts + * + * Interactive prompts for reverting commits + */ + +import { select, confirm, isCancel } from "@clack/prompts"; +import { labelColors, textColors, success, attention } from "../init/colors.js"; +import type { CommitInfo, MergeParent } from "../shared/types.js"; + +/** + * Create compact color-coded label + */ +function label( + text: string, + color: "magenta" | "cyan" | "blue" | "yellow" | "green", +): string { + const colorFn = { + magenta: labelColors.bgBrightMagenta, + cyan: labelColors.bgBrightCyan, + blue: labelColors.bgBrightBlue, + yellow: labelColors.bgBrightYellow, + green: labelColors.bgBrightGreen, + }[color]; + + const width = 7; + const textLength = Math.min(text.length, width); + const padding = width - textLength; + const leftPad = Math.ceil(padding / 2); + const rightPad = padding - leftPad; + const centeredText = + " ".repeat(leftPad) + text.substring(0, textLength) + " ".repeat(rightPad); + + return colorFn(` ${centeredText} `); +} + +/** + * Handle prompt cancellation + */ +function handleCancel(value: unknown): void { + if (isCancel(value)) { + console.log("\nRevert cancelled."); + process.exit(0); + } +} + +/** + * Display commit list for revert + */ +export function displayRevertCommitList( + commits: CommitInfo[], + startIndex: number, + totalFetched: number, + hasMore: boolean, +): void { + console.log(); + console.log( + `${label("revert", "yellow")} ${textColors.pureWhite("Select Commit to Revert")}`, + ); + console.log(); + + if (commits.length === 0) { + console.log(" No commits found."); + return; + } + + const displayCount = Math.min(commits.length, 10); + for (let i = 0; i < displayCount; i++) { + const commit = commits[i]; + const number = i.toString(); + const mergeIndicator = commit.isMerge ? " [Merge]" : ""; + const truncatedSubject = + commit.subject.length > 50 + ? commit.subject.substring(0, 47) + "..." + : commit.subject; + + console.log( + ` ${textColors.brightCyan(`[${number}]`)} ${textColors.brightWhite(commit.shortHash)} ${truncatedSubject}${mergeIndicator}`, + ); + console.log( + ` ${textColors.white(commit.author.name)} • ${textColors.white(commit.date.relative)}`, + ); + } + + console.log(); + if (hasMore) { + console.log( + ` Showing commits ${startIndex + 1}-${startIndex + displayCount} of ${totalFetched}+ (press ${textColors.brightYellow("n")} for next batch)`, + ); + } else { + console.log( + ` Showing commits ${startIndex + 1}-${startIndex + displayCount} of ${totalFetched}`, + ); + } + console.log(); +} + +/** + * Prompt for merge commit parent selection + */ +export async function promptMergeParent( + parents: MergeParent[], +): Promise { + const options = parents.map((parent) => ({ + value: parent.number.toString(), + label: `Parent ${parent.number}${parent.branch ? `: ${parent.branch}` : ""} (${parent.shortHash})${parent.number === 1 ? " [mainline, default]" : ""}`, + })); + + const selected = await select({ + message: `${label("parent", "blue")} ${textColors.pureWhite("Select parent to revert to:")}`, + options, + initialValue: "1", // Default to parent 1 + }); + + handleCancel(selected); + return parseInt(selected as string, 10); +} + +/** + * Display revert confirmation + */ +export function displayRevertConfirmation(commit: CommitInfo): void { + console.log(); + console.log( + `${label("confirm", "green")} ${textColors.pureWhite("Revert Confirmation")}`, + ); + console.log(); + console.log(` ${textColors.brightWhite("Reverting commit:")} ${commit.shortHash}`); + console.log(` ${textColors.brightWhite("Original:")} ${commit.subject}`); + console.log(); + console.log( + ` ${attention("This will create a new commit that undoes these changes.")}`, + ); + console.log(); +} + +/** + * Prompt for revert confirmation + */ +export async function promptRevertConfirmation(): Promise<"confirm" | "edit" | "cancel"> { + const confirmed = await confirm({ + message: `${label("confirm", "green")} ${textColors.pureWhite("Proceed with revert?")}`, + initialValue: true, + }); + + handleCancel(confirmed); + + if (confirmed) { + // Ask if user wants to edit commit message + const edit = await confirm({ + message: `${label("edit", "yellow")} ${textColors.pureWhite("Edit commit message before reverting?")}`, + initialValue: false, + }); + + handleCancel(edit); + return edit ? "edit" : "confirm"; + } + + return "cancel"; +} + diff --git a/src/cli/commands/revert/types.ts b/src/cli/commands/revert/types.ts new file mode 100644 index 0000000..de4c42c --- /dev/null +++ b/src/cli/commands/revert/types.ts @@ -0,0 +1,13 @@ +/** + * Revert Command Types + */ + +import type { CommitInfo, MergeParent } from "../shared/types.js"; + +export interface RevertState { + selectedCommit: CommitInfo; + parentNumber?: number; // For merge commits (1, 2, etc.) + useCommitWorkflow: boolean; // true unless --no-edit + conflictDetected: boolean; +} + diff --git a/src/cli/commands/shared/commit-parser.ts b/src/cli/commands/shared/commit-parser.ts new file mode 100644 index 0000000..d28b8fc --- /dev/null +++ b/src/cli/commands/shared/commit-parser.ts @@ -0,0 +1,99 @@ +/** + * Commit Message Parser + * + * Parses commit messages to extract type, scope, and subject + * for use in revert commit templates + */ + +import type { ParsedCommit } from "./types.js"; + +/** + * Parse commit message following conventional commits format + * Format: {emoji}{type}({scope}): {subject} + */ +export function parseCommitMessage(message: string): ParsedCommit { + if (!message || !message.trim()) { + return { + subject: message || "", + parseSuccess: false, + }; + } + + // Remove leading/trailing whitespace + const trimmed = message.trim(); + + // Try to match: {emoji}{type}({scope}): {subject} + // Emoji is optional, scope is optional + const pattern1 = /^(?:\p{Emoji}*\s*)?(\w+)(?:\(([^)]+)\))?:\s*(.+)$/u; + const match1 = trimmed.match(pattern1); + + if (match1) { + const [, type, scope, subject] = match1; + return { + type: type.toLowerCase(), + scope: scope || undefined, + subject: subject.trim(), + parseSuccess: true, + }; + } + + // Try to match: {type}({scope}): {subject} (no emoji) + const pattern2 = /^(\w+)(?:\(([^)]+)\))?:\s*(.+)$/; + const match2 = trimmed.match(pattern2); + + if (match2) { + const [, type, scope, subject] = match2; + return { + type: type.toLowerCase(), + scope: scope || undefined, + subject: subject.trim(), + parseSuccess: true, + }; + } + + // Try to match: {type}: {subject} (no scope) + const pattern3 = /^(\w+):\s*(.+)$/; + const match3 = trimmed.match(pattern3); + + if (match3) { + const [, type, subject] = match3; + return { + type: type.toLowerCase(), + subject: subject.trim(), + parseSuccess: true, + }; + } + + // If no pattern matches, return entire message as subject + return { + subject: trimmed, + parseSuccess: false, + }; +} + +/** + * Generate revert subject following industry standards + */ +export function generateRevertSubject( + originalSubject: string, + maxLength: number, +): string { + // Industry standard: Revert "original subject" + let base = `Revert "${originalSubject}"`; + + // Handle quotes: if double quotes in subject, use single quotes + if (originalSubject.includes('"')) { + base = `Revert '${originalSubject}'`; + } + + // Truncate if too long + if (base.length > maxLength) { + // Reserve space for "Revert \"...\"" + const availableLength = maxLength - 15; // "Revert \"...\"" + const truncated = originalSubject.substring(0, Math.max(0, availableLength)); + return `Revert "${truncated}..."`; + } + + return base; +} + diff --git a/src/cli/commands/shared/git-operations.ts b/src/cli/commands/shared/git-operations.ts new file mode 100644 index 0000000..fdc429e --- /dev/null +++ b/src/cli/commands/shared/git-operations.ts @@ -0,0 +1,288 @@ +/** + * Shared Git Operations + * + * Common Git operations used by preview and revert commands + */ + +import { spawnSync } from "child_process"; +import { Logger } from "../../../lib/logger.js"; +import type { CommitInfo, MergeParent } from "./types.js"; + +/** + * Execute git command and return stdout + */ +function execGit(args: string[]): string { + try { + const result = spawnSync("git", args, { + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.error) { + throw result.error; + } + + if (result.status !== 0) { + const stderr = result.stderr?.toString() || "Unknown error"; + const error = new Error(stderr); + (error as any).code = result.status; + throw error; + } + + return result.stdout?.toString().trim() || ""; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : String(error); + Logger.error(`Git command failed: git ${args.join(" ")}`); + Logger.error(errorMessage); + throw error; + } +} + +/** + * Check if current directory is a git repository + */ +export function isGitRepository(): boolean { + try { + execGit(["rev-parse", "--git-dir"]); + return true; + } catch { + return false; + } +} + +/** + * Get current branch name + */ +export function getCurrentBranch(): string | null { + try { + return execGit(["rev-parse", "--abbrev-ref", "HEAD"]); + } catch { + return null; + } +} + +/** + * Format relative time (e.g., "2 hours ago") + */ +function formatRelativeTime(date: Date): string { + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSecs = Math.floor(diffMs / 1000); + const diffMins = Math.floor(diffSecs / 60); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + const diffWeeks = Math.floor(diffDays / 7); + const diffMonths = Math.floor(diffDays / 30); + const diffYears = Math.floor(diffDays / 365); + + if (diffSecs < 60) return `${diffSecs} second${diffSecs !== 1 ? "s" : ""} ago`; + if (diffMins < 60) return `${diffMins} minute${diffMins !== 1 ? "s" : ""} ago`; + if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`; + if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`; + if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks !== 1 ? "s" : ""} ago`; + if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? "s" : ""} ago`; + return `${diffYears} year${diffYears !== 1 ? "s" : ""} ago`; +} + +/** + * Fetch commits from git log + */ +export function fetchCommits( + limit: number, + branch?: string, +): CommitInfo[] { + const args = [ + "log", + "--max-count", + limit.toString(), + "--format=%H|%s|%an|%ae|%ai|%P", + "--date=iso", + ]; + + if (branch) { + args.push(branch); + } + + const output = execGit(args); + if (!output) return []; + + const commits: CommitInfo[] = []; + const lines = output.split("\n").filter((l) => l.trim()); + + for (const line of lines) { + const parts = line.split("|"); + if (parts.length < 6) continue; + + const [hash, subject, authorName, authorEmail, dateStr, parentsStr] = parts; + const shortHash = hash.substring(0, 7); + const parents = parentsStr ? parentsStr.trim().split(/\s+/) : []; + const isMerge = parents.length > 1; + const date = new Date(dateStr); + + commits.push({ + hash, + shortHash, + subject: subject || "(no subject)", + body: null, // Will be fetched lazily + author: { + name: authorName || "Unknown", + email: authorEmail || "", + }, + date: { + absolute: date.toISOString(), + relative: formatRelativeTime(date), + }, + parents, + isMerge, + }); + } + + return commits; +} + +/** + * Get detailed commit information + */ +export function getCommitDetails(hash: string): CommitInfo { + // Get basic info + const logOutput = execGit([ + "log", + "-1", + "--format=%H|%s|%an|%ae|%ai|%P", + "--date=iso", + hash, + ]); + + if (!logOutput) { + throw new Error(`Commit ${hash} not found`); + } + + const parts = logOutput.split("|"); + if (parts.length < 6) { + throw new Error(`Invalid commit format: ${hash}`); + } + + const [fullHash, subject, authorName, authorEmail, dateStr, parentsStr] = parts; + const shortHash = fullHash.substring(0, 7); + const parents = parentsStr ? parentsStr.trim().split(/\s+/) : []; + const isMerge = parents.length > 1; + const date = new Date(dateStr); + + // Get body + const bodyOutput = execGit(["log", "-1", "--format=%B", hash]); + const body = bodyOutput.trim() || null; + + // Get file stats + const statOutput = execGit(["show", "--stat", "--format=", hash]); + let fileStats: CommitInfo["fileStats"] | undefined; + if (statOutput) { + const statLines = statOutput.split("\n").filter((l) => l.trim()); + const lastLine = statLines[statLines.length - 1]; + const match = lastLine.match(/(\d+) file(?:s)? changed(?:, (\d+) insertion(?:s)?)?(?:, (\d+) deletion(?:s)?)?/); + if (match) { + fileStats = { + filesChanged: parseInt(match[1], 10) || 0, + additions: match[2] ? parseInt(match[2], 10) : undefined, + deletions: match[3] ? parseInt(match[3], 10) : undefined, + }; + } + } + + // Get changed files + const filesOutput = execGit(["show", "--name-only", "--format=", hash]); + const files = filesOutput + ? filesOutput.split("\n").filter((l) => l.trim()) + : undefined; + + return { + hash: fullHash, + shortHash, + subject: subject || "(no subject)", + body, + author: { + name: authorName || "Unknown", + email: authorEmail || "", + }, + date: { + absolute: date.toISOString(), + relative: formatRelativeTime(date), + }, + parents, + isMerge, + fileStats, + files, + }; +} + +/** + * Check if commit is a merge commit + */ +export function isMergeCommit(hash: string): boolean { + try { + const parents = execGit(["log", "-1", "--format=%P", hash]); + return parents.trim().split(/\s+/).length > 1; + } catch { + return false; + } +} + +/** + * Get merge commit parents + */ +export function getMergeParents(hash: string): MergeParent[] { + try { + const parentsStr = execGit(["log", "-1", "--format=%P", hash]); + const parentHashes = parentsStr.trim().split(/\s+/).filter((h) => h); + + return parentHashes.map((parentHash, index) => { + const shortHash = parentHash.substring(0, 7); + // Try to get branch name + let branch: string | undefined; + try { + const branchOutput = execGit([ + "branch", + "--contains", + parentHash, + "--format=%(refname:short)", + ]); + const branches = branchOutput.split("\n").filter((b) => b.trim()); + branch = branches[0] || undefined; + } catch { + // Branch name not available + } + + return { + number: index + 1, + branch, + shortHash, + hash: parentHash, + }; + }); + } catch { + return []; + } +} + +/** + * Get commit diff + */ +export function getCommitDiff(hash: string): string { + try { + return execGit(["show", hash]); + } catch { + return ""; + } +} + +/** + * Check if there are uncommitted changes + */ +export function hasUncommittedChanges(): boolean { + try { + const status = execGit(["status", "--porcelain"]); + return status.trim().length > 0; + } catch { + return false; + } +} + diff --git a/src/cli/commands/shared/types.ts b/src/cli/commands/shared/types.ts new file mode 100644 index 0000000..7898745 --- /dev/null +++ b/src/cli/commands/shared/types.ts @@ -0,0 +1,86 @@ +/** + * Shared Types for Preview and Revert Commands + * + * Common type definitions used by both preview and revert commands + */ + +/** + * Commit information structure + */ +export interface CommitInfo { + /** Full commit hash */ + hash: string; + /** Short hash (7 characters) */ + shortHash: string; + /** Full subject line */ + subject: string; + /** Commit body or null if no body */ + body: string | null; + /** Author information */ + author: { + name: string; + email: string; + }; + /** Date information */ + date: { + absolute: string; // ISO format + relative: string; // "2 hours ago" + }; + /** Parent commit hashes */ + parents: string[]; + /** True if this is a merge commit */ + isMerge: boolean; + /** File statistics (optional) */ + fileStats?: { + filesChanged: number; + additions?: number; + deletions?: number; + }; + /** Changed file paths (optional, lazy-loaded) */ + files?: string[]; +} + +/** + * Commit batch for pagination + */ +export interface CommitBatch { + /** Commits in this batch */ + commits: CommitInfo[]; + /** 0-based index of first commit */ + startIndex: number; + /** Total commits fetched so far */ + totalFetched: number; + /** More commits available beyond max */ + hasMore: boolean; +} + +/** + * Parsed commit message components + */ +export interface ParsedCommit { + /** Extracted type from original commit */ + type?: string; + /** Extracted scope from original commit */ + scope?: string; + /** Original subject */ + subject: string; + /** Original body */ + body?: string; + /** Whether parsing succeeded */ + parseSuccess: boolean; +} + +/** + * Merge commit parent information + */ +export interface MergeParent { + /** Parent number (1, 2, etc.) */ + number: number; + /** Branch name if available */ + branch?: string; + /** Short hash */ + shortHash: string; + /** Full hash */ + hash: string; +} + diff --git a/src/cli/program.ts b/src/cli/program.ts index 23ae0ee..2ab5ba9 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -17,6 +17,9 @@ import { dirname, join } from "path"; import { configCommand } from "./commands/config.js"; import { initCommand } from "./commands/init/index.js"; import { commitCommand } from "./commands/commit.js"; +import { previewCommand } from "./commands/preview/index.js"; +import { revertCommand } from "./commands/revert/index.js"; +import { testCommand } from "./commands/test/index.js"; // Get package.json for version info const __filename = fileURLToPath(import.meta.url); @@ -45,6 +48,9 @@ program program.addCommand(configCommand); program.addCommand(initCommand); program.addCommand(commitCommand); +program.addCommand(previewCommand); +program.addCommand(revertCommand); +program.addCommand(testCommand); // Customize help text program.addHelpText( @@ -53,7 +59,11 @@ program.addHelpText( Examples: $ labcommitr init Initialize config in current project $ lab commit Create a standardized commit (interactive) - $ lab config show Display current configuration + $ lab preview Browse commit history + $ lab revert Revert a commit using commit workflow + $ lab config show Display current configuration + $ lab test setup Set up test environment + $ lab test shell Open shell in test environment Documentation: https://github.com/labcatr/labcommitr#readme From 2c84fa69408d35d9a210e52eeaff6b26e4601d59 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 23 Nov 2025 19:54:14 -0700 Subject: [PATCH 03/24] feat: implement new testing architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add lab test command with scenario management • Implement 5 test scenarios (existing-project, with-changes, with-history, with-merge, with-conflicts) • Add scenario generators for automated test environment setup • Add state management for test environment tracking • Implement test shell command for interactive testing • Add test reset, clean, status, and list-scenarios commands • Replace bash-based sandbox scripts with TypeScript implementation • Support automated commit history generation for preview/revert testing --- src/cli/commands/test/index.ts | 289 +++++++++++++++++ src/cli/commands/test/scenario-generator.ts | 343 ++++++++++++++++++++ src/cli/commands/test/scenarios.ts | 71 ++++ src/cli/commands/test/state-manager.ts | 84 +++++ src/cli/commands/test/types.ts | 27 ++ 5 files changed, 814 insertions(+) create mode 100644 src/cli/commands/test/index.ts create mode 100644 src/cli/commands/test/scenario-generator.ts create mode 100644 src/cli/commands/test/scenarios.ts create mode 100644 src/cli/commands/test/state-manager.ts create mode 100644 src/cli/commands/test/types.ts diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts new file mode 100644 index 0000000..461b6a1 --- /dev/null +++ b/src/cli/commands/test/index.ts @@ -0,0 +1,289 @@ +/** + * Test Command + * + * Manages test environment for testing Labcommitr commands + */ + +import { Command } from "commander"; +import { Logger } from "../../../lib/logger.js"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import { existsSync } from "fs"; +import { spawnSync } from "child_process"; +import { generateScenario } from "./scenario-generator.js"; +import { + getSandboxPath, + loadState, + saveState, + clearState, + isSandboxValid, +} from "./state-manager.js"; +import { SCENARIOS, DEFAULT_SCENARIO, listScenarios, getScenario } from "./scenarios.js"; +import type { ScenarioName } from "./types.js"; +import { textColors, success, attention } from "../init/colors.js"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const PROJECT_ROOT = join(__dirname, "../../../../"); + +/** + * Build project if needed + */ +async function ensureBuilt(): Promise { + const distPath = join(PROJECT_ROOT, "dist", "index.js"); + if (!existsSync(distPath)) { + console.log("◐ Building project..."); + const result = spawnSync("pnpm", ["run", "build"], { + cwd: PROJECT_ROOT, + stdio: "inherit", + }); + if (result.status !== 0) { + throw new Error("Build failed"); + } + } +} + +/** + * Setup scenario + */ +async function setupAction(options: { scenario?: string }): Promise { + try { + await ensureBuilt(); + + const scenarioName = (options.scenario || DEFAULT_SCENARIO) as ScenarioName; + const scenario = getScenario(scenarioName); + + if (!scenario) { + Logger.error(`Invalid scenario: ${scenarioName}`); + console.error("\n Available scenarios:"); + listScenarios().forEach((s) => { + console.error(` • ${s.name} - ${s.description}`); + }); + console.error(); + process.exit(1); + } + + const sandboxPath = getSandboxPath(PROJECT_ROOT); + + console.log(); + console.log( + `${textColors.brightCyan("◐")} Setting up scenario: ${textColors.brightWhite(scenarioName)}`, + ); + console.log(` ${scenario.description}`); + console.log(); + + // Generate scenario + await generateScenario(sandboxPath, scenarioName); + + // Save state + saveState(sandboxPath, scenarioName); + + console.log(); + console.log(`${success("✓")} Test environment ready!`); + console.log(); + console.log(` ${textColors.brightWhite("Sandbox:")} ${sandboxPath}`); + console.log(` ${textColors.brightWhite("Scenario:")} ${scenarioName}`); + console.log(); + console.log( + ` ${textColors.white("Run commands with:")} ${textColors.brightCyan("lab test shell")}`, + ); + console.log(); + } catch (error: unknown) { + Logger.error("Failed to setup test environment"); + if (error instanceof Error) { + console.error(`\n ${error.message}\n`); + } + process.exit(1); + } +} + +/** + * Reset scenario + */ +async function resetAction(): Promise { + try { + const sandboxPath = getSandboxPath(PROJECT_ROOT); + const state = loadState(sandboxPath); + + if (!state || !state.scenario) { + Logger.error("No active test environment found"); + console.error("\n Run 'lab test setup' first.\n"); + process.exit(1); + } + + console.log(); + console.log( + `${textColors.brightCyan("◐")} Resetting scenario: ${textColors.brightWhite(state.scenario)}`, + ); + console.log(); + + // Regenerate scenario + await generateScenario(sandboxPath, state.scenario); + + console.log(); + console.log(`${success("✓")} Scenario reset complete!`); + console.log(); + } catch (error: unknown) { + Logger.error("Failed to reset test environment"); + if (error instanceof Error) { + console.error(`\n ${error.message}\n`); + } + process.exit(1); + } +} + +/** + * Clean sandbox + */ +async function cleanAction(): Promise { + const sandboxPath = getSandboxPath(PROJECT_ROOT); + + if (!existsSync(sandboxPath)) { + console.log("\n No test environment to clean.\n"); + return; + } + + const { rmSync } = await import("fs"); + rmSync(sandboxPath, { recursive: true, force: true }); + + console.log(); + console.log(`${success("✓")} Test environment removed`); + console.log(); +} + +/** + * Show status + */ +async function statusAction(): Promise { + const sandboxPath = getSandboxPath(PROJECT_ROOT); + const state = loadState(sandboxPath); + + console.log(); + + if (!state || !isSandboxValid(sandboxPath)) { + console.log(" No active test environment."); + console.log(); + console.log(` Run ${textColors.brightCyan("lab test setup")} to create one.`); + console.log(); + return; + } + + const scenario = getScenario(state.scenario!); + + console.log(` ${textColors.brightWhite("Scenario:")} ${state.scenario}`); + if (scenario) { + console.log(` ${textColors.brightWhite("Description:")} ${scenario.description}`); + } + console.log(` ${textColors.brightWhite("Sandbox:")} ${sandboxPath}`); + console.log(); + + // Show git status + const { execSync } = await import("child_process"); + try { + const gitStatus = execSync("git status --porcelain", { + cwd: sandboxPath, + encoding: "utf-8", + }).trim(); + + if (gitStatus) { + const lines = gitStatus.split("\n").length; + console.log(` ${textColors.brightWhite("Uncommitted changes:")} ${lines} file(s)`); + } else { + console.log(` ${textColors.brightWhite("Working directory:")} clean`); + } + } catch { + // Git status failed + } + + console.log(); +} + +/** + * List scenarios + */ +function listScenariosAction(): void { + console.log(); + console.log(` ${textColors.brightWhite("Available scenarios:")}`); + console.log(); + + listScenarios().forEach((scenario) => { + console.log(` ${textColors.brightCyan("•")} ${textColors.brightWhite(scenario.name)}`); + console.log(` ${textColors.white(scenario.description)}`); + console.log(); + }); +} + +/** + * Open shell in test environment + */ +function shellAction(): void { + const sandboxPath = getSandboxPath(PROJECT_ROOT); + + if (!isSandboxValid(sandboxPath)) { + Logger.error("No active test environment found"); + console.error("\n Run 'lab test setup' first.\n"); + process.exit(1); + } + + console.log(); + console.log( + `${textColors.brightCyan("◐")} Opening shell in test environment...`, + ); + console.log(); + console.log( + ` ${textColors.white("Sandbox:")} ${sandboxPath}`, + ); + console.log( + ` ${textColors.white("Exit with:")} ${textColors.brightCyan("exit")} or ${textColors.brightCyan("Ctrl+D")}`, + ); + console.log(); + + // Spawn shell + const shell = process.env.SHELL || "/bin/bash"; + spawnSync(shell, [], { + cwd: sandboxPath, + stdio: "inherit", + env: { + ...process.env, + PS1: `[lab-test] ${process.env.PS1 || "$ "}`, + }, + }); +} + +/** + * Test command + */ +export const testCommand = new Command("test") + .description("Manage test environment for testing Labcommitr commands") + .addCommand( + new Command("setup") + .description("Set up test environment with specified scenario") + .option("-s, --scenario ", "Scenario name", DEFAULT_SCENARIO) + .action(setupAction), + ) + .addCommand( + new Command("reset") + .description("Reset current scenario to initial state") + .action(resetAction), + ) + .addCommand( + new Command("clean") + .description("Remove test environment") + .action(cleanAction), + ) + .addCommand( + new Command("status") + .description("Show current test environment status") + .action(statusAction), + ) + .addCommand( + new Command("list-scenarios") + .description("List all available scenarios") + .action(listScenariosAction), + ) + .addCommand( + new Command("shell") + .description("Open interactive shell in test environment") + .action(shellAction), + ); + diff --git a/src/cli/commands/test/scenario-generator.ts b/src/cli/commands/test/scenario-generator.ts new file mode 100644 index 0000000..ab7771e --- /dev/null +++ b/src/cli/commands/test/scenario-generator.ts @@ -0,0 +1,343 @@ +/** + * Scenario Generator + * + * Generates test scenarios with appropriate git repository states + */ + +import { spawnSync } from "child_process"; +import { mkdirSync, writeFileSync, existsSync } from "fs"; +import { join } from "path"; +import type { ScenarioName } from "./types.js"; +import { loadConfig } from "../../../lib/config/index.js"; + +/** + * Execute git command in sandbox + */ +function execGit(sandboxPath: string, args: string[]): void { + const result = spawnSync("git", args, { + cwd: sandboxPath, + encoding: "utf-8", + stdio: ["ignore", "pipe", "pipe"], + }); + + if (result.status !== 0) { + throw new Error( + `Git command failed: git ${args.join(" ")}\n${result.stderr}`, + ); + } +} + +/** + * Initialize git repository + */ +function initGit(sandboxPath: string): void { + execGit(sandboxPath, ["init", "--initial-branch=main"]); + execGit(sandboxPath, ["config", "user.name", "Test User"]); + execGit(sandboxPath, ["config", "user.email", "test@example.com"]); +} + +/** + * Create initial commit structure + */ +function createInitialStructure(sandboxPath: string): void { + // Create README + writeFileSync( + join(sandboxPath, "README.md"), + "# Test Repository\n\nThis is a test repository for Labcommitr.\n", + ); + + // Create package.json + writeFileSync( + join(sandboxPath, "package.json"), + JSON.stringify( + { + name: "test-project", + version: "1.0.0", + description: "Test project for Labcommitr", + }, + null, + 2, + ), + ); + + execGit(sandboxPath, ["add", "."]); + execGit(sandboxPath, ["commit", "-m", "Initial commit", "--no-verify"]); +} + +/** + * Generate commit history + */ +function generateCommitHistory( + sandboxPath: string, + count: number, + includeMerges: boolean = false, +): void { + const commitTypes = ["feat", "fix", "docs", "refactor", "test", "chore"]; + const scopes = ["api", "ui", "auth", "db", "config", null]; + const subjects = [ + "add new feature", + "fix bug", + "update documentation", + "refactor code", + "add tests", + "update dependencies", + "improve performance", + "fix typo", + "update config", + "add validation", + ]; + + // Create some base files first + mkdirSync(join(sandboxPath, "src"), { recursive: true }); + mkdirSync(join(sandboxPath, "lib"), { recursive: true }); + mkdirSync(join(sandboxPath, "docs"), { recursive: true }); + + for (let i = 0; i < count; i++) { + const type = commitTypes[i % commitTypes.length]; + const scope = scopes[i % scopes.length]; + const subject = subjects[i % subjects.length]; + + // Create or modify a file + const fileNum = (i % 10) + 1; + const fileName = `file-${fileNum}.ts`; + const filePath = join(sandboxPath, "src", fileName); + + writeFileSync( + filePath, + `// File ${fileNum}\n// Commit ${i + 1}\nexport const value${i} = ${i};\n`, + ); + + execGit(sandboxPath, ["add", filePath]); + + // Create commit message + let commitMessage = `${type}`; + if (scope) { + commitMessage += `(${scope})`; + } + commitMessage += `: ${subject} ${i + 1}`; + + // Add body occasionally + if (i % 5 === 0) { + commitMessage += `\n\nThis commit includes additional changes.\n- Change 1\n- Change 2`; + } + + execGit(sandboxPath, [ + "commit", + "-m", + commitMessage, + "--no-verify", + "--allow-empty", + ]); + + // Create merge commits occasionally + if (includeMerges && i > 0 && i % 10 === 0) { + const branchName = `feature-${i}`; + execGit(sandboxPath, ["checkout", "-b", branchName]); + + // Make a commit on branch + writeFileSync( + join(sandboxPath, "src", `branch-${i}.ts`), + `// Branch file ${i}\n`, + ); + execGit(sandboxPath, ["add", join("src", `branch-${i}.ts`)]); + execGit(sandboxPath, [ + "commit", + "-m", + `feat: add feature ${i}`, + "--no-verify", + ]); + + // Merge back to main + execGit(sandboxPath, ["checkout", "main"]); + execGit(sandboxPath, [ + "merge", + "--no-ff", + "-m", + `Merge branch '${branchName}'`, + branchName, + "--no-verify", + ]); + } + } +} + +/** + * Create uncommitted changes + */ +function createUncommittedChanges(sandboxPath: string): void { + // Modified files + for (let i = 1; i <= 4; i++) { + const filePath = join(sandboxPath, "src", `component-${String.fromCharCode(96 + i)}.ts`); + writeFileSync( + filePath, + `// Component ${String.fromCharCode(96 + i)}\nexport class Component${String.fromCharCode(96 + i).toUpperCase()} {}\n// Modified\n`, + ); + } + + // Added files + for (let i = 1; i <= 3; i++) { + const filePath = join(sandboxPath, "src", `service-${String.fromCharCode(96 + i)}.ts`); + writeFileSync( + filePath, + `// New service ${String.fromCharCode(96 + i)}\nexport class Service${String.fromCharCode(96 + i).toUpperCase()} {}\n`, + ); + } + + // Deleted files (mark for deletion) + for (let i = 1; i <= 2; i++) { + const filePath = join(sandboxPath, "lib", `old-util-${i}.js`); + if (!existsSync(filePath)) { + writeFileSync(filePath, `// Old utility ${i}\n`); + execGit(sandboxPath, ["add", filePath]); + execGit(sandboxPath, [ + "commit", + "-m", + `chore: add old util ${i}`, + "--no-verify", + ]); + } + execGit(sandboxPath, ["rm", filePath]); + } + + // Renamed files + mkdirSync(join(sandboxPath, "lib"), { recursive: true }); + const renames = [ + ["helpers.ts", "helper-functions.ts"], + ["constants.ts", "app-constants.ts"], + ]; + + for (const [oldName, newName] of renames) { + const oldPath = join(sandboxPath, "lib", oldName); + if (!existsSync(oldPath)) { + writeFileSync(oldPath, `// ${oldName}\n`); + execGit(sandboxPath, ["add", oldPath]); + execGit(sandboxPath, [ + "commit", + "-m", + `chore: add ${oldName}`, + "--no-verify", + ]); + } + execGit(sandboxPath, ["mv", oldPath, join(sandboxPath, "lib", newName)]); + } +} + +/** + * Create conflict state + */ +function createConflictState(sandboxPath: string): void { + // Create a file and commit it + const conflictFile = join(sandboxPath, "conflict.ts"); + writeFileSync(conflictFile, "// Original content\n"); + execGit(sandboxPath, ["add", conflictFile]); + execGit(sandboxPath, [ + "commit", + "-m", + "feat: add conflict file", + "--no-verify", + ]); + + // Create a branch and modify + execGit(sandboxPath, ["checkout", "-b", "feature-branch"]); + writeFileSync(conflictFile, "// Modified on branch\n"); + execGit(sandboxPath, ["add", conflictFile]); + execGit(sandboxPath, [ + "commit", + "-m", + "feat: modify on branch", + "--no-verify", + ]); + + // Switch back and modify + execGit(sandboxPath, ["checkout", "main"]); + writeFileSync(conflictFile, "// Modified on main\n"); + execGit(sandboxPath, ["add", conflictFile]); + execGit(sandboxPath, [ + "commit", + "-m", + "feat: modify on main", + "--no-verify", + ]); + + // Attempt merge to create conflict + try { + execGit(sandboxPath, ["merge", "feature-branch", "--no-commit"]); + } catch { + // Conflict expected + } +} + +/** + * Copy config file to sandbox + */ +async function copyConfig(sandboxPath: string): Promise { + const configResult = await loadConfig(); + if (configResult.source !== "defaults" && configResult.config) { + // Get config from project root + const { readFileSync } = await import("fs"); + const { fileURLToPath } = await import("url"); + const { dirname, join } = await import("path"); + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const projectRoot = join(__dirname, "../../../../"); + const configPath = join(projectRoot, ".labcommitr.config.yaml"); + + if (existsSync(configPath)) { + const configContent = readFileSync(configPath, "utf-8"); + writeFileSync(join(sandboxPath, ".labcommitr.config.yaml"), configContent); + } + } +} + +/** + * Generate scenario + */ +export async function generateScenario( + sandboxPath: string, + scenario: ScenarioName, +): Promise { + // Clean and initialize + if (existsSync(join(sandboxPath, ".git"))) { + execGit(sandboxPath, ["rm", "-rf", ".git"]); + } + + initGit(sandboxPath); + createInitialStructure(sandboxPath); + + // Generate based on scenario + switch (scenario) { + case "existing-project": + // History + changes, no config + generateCommitHistory(sandboxPath, 25); + createUncommittedChanges(sandboxPath); + // No config file + break; + + case "with-changes": + // History + changes + config + generateCommitHistory(sandboxPath, 25); + createUncommittedChanges(sandboxPath); + await copyConfig(sandboxPath); + break; + + case "with-history": + // Extensive history + config + generateCommitHistory(sandboxPath, 100); + await copyConfig(sandboxPath); + break; + + case "with-merge": + // History with merges + config + generateCommitHistory(sandboxPath, 50, true); + await copyConfig(sandboxPath); + break; + + case "with-conflicts": + // History + conflict state + config + generateCommitHistory(sandboxPath, 20); + createConflictState(sandboxPath); + await copyConfig(sandboxPath); + break; + } +} + diff --git a/src/cli/commands/test/scenarios.ts b/src/cli/commands/test/scenarios.ts new file mode 100644 index 0000000..29b00ee --- /dev/null +++ b/src/cli/commands/test/scenarios.ts @@ -0,0 +1,71 @@ +/** + * Test Scenario Definitions + * + * Defines all available test scenarios and their metadata + */ + +import type { ScenarioMetadata, ScenarioName } from "./types.js"; + +export const SCENARIOS: Record = { + "existing-project": { + name: "existing-project", + description: + "Existing project with history and uncommitted changes, no config file. Use for testing adding Labcommitr to an existing project.", + hasHistory: true, + hasChanges: true, + hasConfig: false, + hasConflicts: false, + hasMerges: false, + }, + "with-changes": { + name: "with-changes", + description: + "Project with history, uncommitted changes, and config file. Use for testing commit command with various file states.", + hasHistory: true, + hasChanges: true, + hasConfig: true, + hasConflicts: false, + hasMerges: false, + }, + "with-history": { + name: "with-history", + description: + "Project with extensive commit history (100+ commits) and config file. Use for testing preview and revert commands.", + hasHistory: true, + hasChanges: false, + hasConfig: true, + hasConflicts: false, + hasMerges: false, + }, + "with-merge": { + name: "with-merge", + description: + "Project with merge commits and config file. Use for testing revert command with merge commit handling.", + hasHistory: true, + hasChanges: false, + hasConfig: true, + hasConflicts: false, + hasMerges: true, + }, + "with-conflicts": { + name: "with-conflicts", + description: + "Project in conflict state with config file. Use for testing conflict resolution workflows.", + hasHistory: true, + hasChanges: false, + hasConfig: true, + hasConflicts: true, + hasMerges: false, + }, +}; + +export const DEFAULT_SCENARIO: ScenarioName = "with-changes"; + +export function getScenario(name: string): ScenarioMetadata | null { + return SCENARIOS[name as ScenarioName] || null; +} + +export function listScenarios(): ScenarioMetadata[] { + return Object.values(SCENARIOS); +} + diff --git a/src/cli/commands/test/state-manager.ts b/src/cli/commands/test/state-manager.ts new file mode 100644 index 0000000..6ae0b9c --- /dev/null +++ b/src/cli/commands/test/state-manager.ts @@ -0,0 +1,84 @@ +/** + * Test State Manager + * + * Manages test environment state and metadata + */ + +import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs"; +import { join } from "path"; +import type { TestState, ScenarioName } from "./types.js"; + +const STATE_FILE = ".test-state.json"; + +/** + * Get sandbox path + */ +export function getSandboxPath(projectRoot: string): string { + return join(projectRoot, ".sandbox", "test"); +} + +/** + * Get state file path + */ +function getStateFilePath(sandboxPath: string): string { + return join(sandboxPath, STATE_FILE); +} + +/** + * Load test state + */ +export function loadState(sandboxPath: string): TestState | null { + const statePath = getStateFilePath(sandboxPath); + if (!existsSync(statePath)) { + return null; + } + + try { + const content = readFileSync(statePath, "utf-8"); + const state = JSON.parse(content) as TestState; + return state; + } catch { + return null; + } +} + +/** + * Save test state + */ +export function saveState( + sandboxPath: string, + scenario: ScenarioName, +): void { + mkdirSync(sandboxPath, { recursive: true }); + + const state: TestState = { + scenario, + sandboxPath, + isActive: true, + }; + + const statePath = getStateFilePath(sandboxPath); + writeFileSync(statePath, JSON.stringify(state, null, 2)); +} + +/** + * Clear test state + */ +export function clearState(sandboxPath: string): void { + const statePath = getStateFilePath(sandboxPath); + if (existsSync(statePath)) { + unlinkSync(statePath); + } +} + +/** + * Check if sandbox exists and is valid + */ +export function isSandboxValid(sandboxPath: string): boolean { + return ( + existsSync(sandboxPath) && + existsSync(join(sandboxPath, ".git")) && + existsSync(getStateFilePath(sandboxPath)) + ); +} + diff --git a/src/cli/commands/test/types.ts b/src/cli/commands/test/types.ts new file mode 100644 index 0000000..6fda8b0 --- /dev/null +++ b/src/cli/commands/test/types.ts @@ -0,0 +1,27 @@ +/** + * Test Command Types + */ + +export type ScenarioName = + | "existing-project" + | "with-changes" + | "with-history" + | "with-merge" + | "with-conflicts"; + +export interface ScenarioMetadata { + name: ScenarioName; + description: string; + hasHistory: boolean; + hasChanges: boolean; + hasConfig: boolean; + hasConflicts: boolean; + hasMerges: boolean; +} + +export interface TestState { + scenario: ScenarioName | null; + sandboxPath: string; + isActive: boolean; +} + From 43c0756ca46566c843162d99152e1bcc49d6503b Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 23 Nov 2025 19:54:15 -0700 Subject: [PATCH 04/24] refactor: remove legacy testing scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Remove labcommitr-sandbox.sh bash script • Remove reset-sandbox.sh helper script • Remove legacy npm test scripts (test:sandbox, test:sandbox:bare, etc.) • Clean up scripts directory (now empty, removed) • Testing now handled by lab test command --- package.json | 6 +- scripts/README.md | 407 ------------------------- scripts/labcommitr-sandbox.sh | 557 ---------------------------------- scripts/reset-sandbox.sh | 38 --- 4 files changed, 1 insertion(+), 1007 deletions(-) delete mode 100644 scripts/README.md delete mode 100755 scripts/labcommitr-sandbox.sh delete mode 100755 scripts/reset-sandbox.sh diff --git a/package.json b/package.json index e0a57f1..c184b16 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,7 @@ "format": "pnpm run format:code", "format:ci": "pnpm run format:code", "format:code": "prettier -w \"**/*\" --ignore-unknown --cache", - "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format", - "test:sandbox": "bash scripts/labcommitr-sandbox.sh", - "test:sandbox:bare": "bash scripts/labcommitr-sandbox.sh --no-config", - "test:sandbox:reset": "bash scripts/labcommitr-sandbox.sh --reset", - "test:sandbox:clean": "bash scripts/labcommitr-sandbox.sh --clean" + "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format" }, "type": "module", "bin": { diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100644 index 478c245..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,407 +0,0 @@ -# Labcommitr Testing Sandbox - -A testing environment for safely experimenting with Labcommitr commands without affecting your real repository. - -## TLDR; Quick Start - -```bash -# Create sandbox with config (if available in project root) -pnpm run test:sandbox - -# Create sandbox without config (start from scratch) -pnpm run test:sandbox:bare - -# Enter sandbox and test -cd .sandbox/*/ -node ../../dist/index.js commit - -# Quick reset from within sandbox (easiest!) -bash reset-sandbox.sh # Reset, remove config -bash reset-sandbox.sh --preserve-config # Reset, keep config - -# Quick reset from project root -pnpm run test:sandbox:reset # Reset, remove config -bash scripts/labcommitr-sandbox.sh --reset --preserve-config # Reset, keep config - -# Full recreation (slower, completely fresh) -pnpm run test:sandbox - -# Clean up (remove sandbox) -pnpm run test:sandbox:clean -``` - ---- - -## Table of Contents - -- [Overview](#overview) -- [Quick Start](#tldr-quick-start) -- [Usage](#usage) - - [Creating a Sandbox](#creating-a-sandbox) - - [Testing Commands](#testing-commands) - - [Resetting the Sandbox](#resetting-the-sandbox) - - [Cleaning Up](#cleaning-up) -- [Sandbox Contents](#sandbox-contents) -- [Testing Scenarios](#testing-scenarios) -- [Troubleshooting](#troubleshooting) -- [Safety Guarantees](#safety-guarantees) - ---- - -## Overview - -The testing sandbox creates an isolated git repository with pre-configured file states to test Labcommitr's commit command. Each sandbox gets a randomized scientific name (e.g., `quark`, `photon`, `neutron`) and is stored in `.sandbox/` directory. - -**Key Features:** -- ✅ Completely isolated from your real repository -- ✅ Pre-populated with various git file states (modified, added, deleted, renamed, copied) -- ✅ Automatically copies your project's `.labcommitr.config.yaml` if it exists -- ✅ Safe to delete anytime -- ✅ Quick reset option for iterative testing - ---- - -## Usage - -### Creating a Sandbox - -You have two options for creating a sandbox: - -**Option 1: With Config (Default)** -```bash -# Using npm script (recommended) -pnpm run test:sandbox - -# Or direct script execution -bash scripts/labcommitr-sandbox.sh -``` - -This will: -1. Create a new sandbox directory in `.sandbox//` -2. Initialize a git repository -3. Copy your project's `.labcommitr.config.yaml` if it exists (ready to test immediately) -4. Create test files with various git states -5. Stage all changes ready for testing - -**Option 2: Without Config (Start from Scratch)** -```bash -# Using npm script (recommended) -pnpm run test:sandbox:bare - -# Or direct script execution -bash scripts/labcommitr-sandbox.sh --no-config -``` - -This will: -1. Create a new sandbox directory in `.sandbox//` -2. Initialize a git repository -3. **Skip copying config** (sandbox starts without configuration) -4. Create test files with various git states -5. Stage all changes ready for testing - -After creating a bare sandbox, you can set up configuration: -```bash -cd .sandbox/*/ -lab init # Interactive setup (or lab init --force to overwrite if config exists) -``` - -**Note:** If a sandbox already exists, it will be completely recreated (full reset). - -### Testing Commands - -Once the sandbox is created, navigate to it and test Labcommitr: - -```bash -# Find your sandbox (it has a random scientific name) -cd .sandbox/*/ - -# Test commit command (recommended method) -node ../../dist/index.js commit - -# Alternative: If you've linked globally -lab commit -``` - -**⚠️ Important:** Do NOT use `npx lab commit` - it will use the wrong 'lab' package (Node.js test framework). - -### Resetting the Sandbox - -You have multiple reset options depending on your needs: - -**Quick Reset from Within Sandbox** (easiest for iterative testing) -```bash -# From within the sandbox directory -cd .sandbox/*/ - -# Reset (removes config file) -bash reset-sandbox.sh - -# Reset and preserve config file -bash reset-sandbox.sh --preserve-config -``` -- Can be run from within the sandbox directory -- Faster (keeps repository structure) -- Resets git state and re-applies test file changes -- Option to preserve `.labcommitr.config.yaml` file - -**Quick Reset from Project Root** -```bash -# Reset (removes config file) -pnpm run test:sandbox:reset - -# Reset and preserve config file -bash scripts/labcommitr-sandbox.sh --reset --preserve-config -``` -- Must be run from project root -- Same functionality as reset from within sandbox - -**Full Recreation** (slower but completely fresh) -```bash -pnpm run test:sandbox -# or -bash scripts/labcommitr-sandbox.sh -``` -- Removes entire sandbox and recreates from scratch -- Ensures all file types are properly staged -- Use this if quick reset doesn't work or you want a clean slate - -### Cleaning Up - -To completely remove the sandbox: - -```bash -pnpm run test:sandbox:clean -# or -bash scripts/labcommitr-sandbox.sh --clean -``` - -This removes the sandbox directory entirely. The `.sandbox/` base directory is also removed if empty. - ---- - -## Sandbox Contents - -Each sandbox contains a git repository with the following pre-staged file states: - -### File States (17 total files staged) - -- **Modified (4 files)**: `src/component-{a,b,c,d}.ts` - Files with changes -- **Added (4 files)**: `src/service-{a,b,c}.ts`, `docs/guide.md` - New files -- **Deleted (4 files)**: `utils/old-util-{1,2,3,4}.js` - Files marked for deletion -- **Renamed (4 files)**: `lib/{helpers→helper-functions, constants→app-constants, types→type-definitions, config→configuration}.ts` - Files moved/renamed -- **Copied (4 files)**: `src/model-{1,2}-backup.ts`, `lib/model-{3,4}-copy.ts` - Files copied (Git detects with `-C50` flag) -- **Pre-staged (1 file)**: `pre-staged.ts` - Already staged file for testing - -### Directory Structure - -``` -.sandbox/ -└── / # e.g., quark, photon, neutron - ├── .labcommitr.config.yaml # Copied from project root (if exists) - ├── README.md - ├── package.json - ├── src/ - │ ├── component-{a,b,c,d}.ts - │ ├── service-{a,b,c}.ts - │ ├── model-{1,2,3,4}.ts - │ └── model-{1,2}-backup.ts - ├── docs/ - │ └── guide.md - ├── lib/ - │ ├── helper-functions.ts - │ ├── app-constants.ts - │ ├── type-definitions.ts - │ ├── configuration.ts - │ └── model-{3,4}-copy.ts - ├── utils/ - └── pre-staged.ts -``` - ---- - -## Testing Scenarios - -### Test Different Configurations - -1. **Modify config in sandbox:** - ```bash - cd .sandbox/*/ - # Edit .labcommitr.config.yaml - # Test with different settings: - # - auto_stage: true vs false - # - Different commit types - # - Validation rules - # - editor_preference: "auto" | "inline" | "editor" - ``` - -2. **Test auto-stage behavior:** - - Set `auto_stage: true` - tool should stage files automatically - - Set `auto_stage: false` - tool should only commit already-staged files - -3. **Test validation rules:** - - Try invalid commit types - - Test scope requirements - - Test subject length limits - -4. **Test editor preferences:** - - `inline`: Type body directly in terminal - - `editor`: Opens your default editor - - `auto`: Detects available editor automatically - -### Verify Commit Results - -```bash -# Check git log -git log --oneline -5 - -# See last commit details -git show HEAD - -# Check git status -git status -git status --porcelain # Compact format - -# See staged files -git diff --cached --name-only -``` - ---- - -## Troubleshooting - -### Files Don't Appear Correctly - -If git status doesn't show the expected file states: - -1. **Full recreation:** - ```bash - pnpm run test:sandbox - ``` - -2. **Check git status manually:** - ```bash - cd .sandbox/*/ - git status - git status --porcelain - ``` - -### Config Not Found - -If you see "No config file found" or created a bare sandbox: - -1. **Create config in sandbox (recommended):** - ```bash - cd .sandbox/*/ - lab init # Interactive setup - ``` - -2. **Or create config in project root first, then recreate sandbox:** - ```bash - # From project root - lab init - # Then recreate sandbox to copy the config - pnpm run test:sandbox - ``` - -3. **To overwrite existing config in sandbox:** - ```bash - cd .sandbox/*/ - lab init --force # Overwrites existing config - ``` - -### Reset Not Working - -If quick reset fails: - -1. **Use full recreation instead:** - ```bash - pnpm run test:sandbox - ``` - -2. **Or manually reset:** - ```bash - cd .sandbox/*/ - git reset --hard HEAD - git clean -fd - ``` - -### Can't Find Sandbox - -Sandbox location is randomized. To find it: - -```bash -# List all sandboxes -ls -la .sandbox/ - -# Or use find -find .sandbox -name ".git" -type d -``` - ---- - -## Safety Guarantees - -The sandbox is **100% safe**: - -1. **No push to remote**: Sandbox is completely separate, no remote configured -2. **Isolated**: No connection to your real repository -3. **Easy cleanup**: Delete directory when done (`pnpm run test:sandbox:clean`) -4. **No side effects**: Changes only exist in test environment -5. **Git-ignored**: `.sandbox/` is in `.gitignore`, won't be committed - ---- - -## Pro Tips - -1. **Keep sandbox open in separate terminal** for quick iteration -2. **Use quick reset** (`--reset`) for faster testing cycles -3. **Use bare sandbox** (`--no-config`) to test the full `lab init` flow -4. **Test both `auto_stage: true` and `false`** configurations -5. **Test editor preferences** (`inline`, `editor`, `auto`) -6. **Test validation rules** by intentionally breaking them -7. **Check git log** after commits to verify message formatting -8. **Use `lab init --force`** in sandbox to test different presets and configurations - ---- - -## Script Options - -The `labcommitr-sandbox.sh` script supports the following options: - -```bash -# Create or recreate sandbox with config (default) -bash scripts/labcommitr-sandbox.sh - -# Create sandbox without config (start from scratch) -bash scripts/labcommitr-sandbox.sh --no-config - -# Quick reset (faster, keeps repo structure) -bash scripts/labcommitr-sandbox.sh --reset - -# Quick reset with config preservation -bash scripts/labcommitr-sandbox.sh --reset --preserve-config - -# Remove sandbox completely -bash scripts/labcommitr-sandbox.sh --clean - -# Show help -bash scripts/labcommitr-sandbox.sh --help -``` - -**Note:** The script can detect if it's being run from within a sandbox directory and will automatically use that sandbox for reset operations. - -### Reset Script (Within Sandbox) - -Each sandbox includes a `reset-sandbox.sh` script for convenience: - -```bash -# From within sandbox directory -bash reset-sandbox.sh # Reset, remove config -bash reset-sandbox.sh --preserve-config # Reset, keep config -``` - ---- - -**Last Updated**: January 2025 -**Script Location**: `scripts/labcommitr-sandbox.sh` -**Sandbox Location**: `.sandbox//` diff --git a/scripts/labcommitr-sandbox.sh b/scripts/labcommitr-sandbox.sh deleted file mode 100755 index a52ddc8..0000000 --- a/scripts/labcommitr-sandbox.sh +++ /dev/null @@ -1,557 +0,0 @@ -#!/bin/bash - -# Labcommitr Testing Sandbox -# Creates an isolated git repository for testing Labcommitr commands -# Safe to use - nothing affects your real repository - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -SANDBOX_BASE="$PROJECT_ROOT/.sandbox" - -# Colors for output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Scientific names for fun repository names -SCIENTIFIC_NAMES=( - "quark" "photon" "neutron" "electron" "atom" "molecule" - "proton" "boson" "fermion" "quantum" "plasma" "ion" - "catalyst" "enzyme" "polymer" "crystal" "isotope" "nucleus" - "chromosome" "genome" "protein" "dna" "rna" "enzyme" - "nebula" "galaxy" "asteroid" "comet" "pulsar" "quasar" -) - -# Function to generate random scientific name -generate_sandbox_name() { - local random_index=$((RANDOM % ${#SCIENTIFIC_NAMES[@]})) - echo "${SCIENTIFIC_NAMES[$random_index]}" -} - -# Function to find existing sandbox -find_existing_sandbox() { - if [ -d "$SANDBOX_BASE" ]; then - local dirs=("$SANDBOX_BASE"/*) - if [ -d "${dirs[0]}" ]; then - echo "${dirs[0]}" - return 0 - fi - fi - return 1 -} - -# Function to detect if we're in a sandbox directory -detect_sandbox_dir() { - local current_dir="$(pwd)" - local abs_current_dir="$(cd "$current_dir" && pwd)" - - # Check if current directory is within .sandbox/ - if [[ "$abs_current_dir" == *"/.sandbox/"* ]]; then - # Find the .sandbox base directory - local sandbox_base="${abs_current_dir%/.sandbox/*}/.sandbox" - - # Check if .sandbox directory exists - if [ ! -d "$sandbox_base" ]; then - return 1 - fi - - # Get the path after .sandbox/ - local relative_path="${abs_current_dir#${sandbox_base}/}" - - # Extract the first directory name (sandbox name) - local sandbox_name="${relative_path%%/*}" - - # Construct full sandbox path - local sandbox_dir="$sandbox_base/$sandbox_name" - - # Verify it's actually a git repository (sandbox marker) - if [ -d "$sandbox_dir/.git" ]; then - echo "$sandbox_dir" - return 0 - fi - fi - return 1 -} - -# Function to display usage -show_usage() { - echo "Usage: $0 [OPTIONS]" - echo "" - echo "Options:" - echo " --reset Quick reset: reset git state without full recreation" - echo " --preserve-config Preserve .labcommitr.config.yaml during reset (use with --reset)" - echo " --clean Remove sandbox directory entirely" - echo " --no-config Create sandbox without copying config (start from scratch)" - echo " --help Show this help message" - echo "" - echo "Examples:" - echo " $0 # Create or recreate sandbox (with config if available)" - echo " $0 --no-config # Create sandbox without config (run 'lab init' yourself)" - echo " $0 --reset # Quick reset (faster, keeps repo structure)" - echo " $0 --reset --preserve-config # Quick reset, preserve config file" - echo " $0 --clean # Remove sandbox completely" - echo "" - echo "Note: Can be run from within sandbox directory or project root" -} - -# Parse arguments -RESET_MODE=false -CLEAN_MODE=false -NO_CONFIG=false -PRESERVE_CONFIG=false - -while [[ $# -gt 0 ]]; do - case $1 in - --reset) - RESET_MODE=true - shift - ;; - --preserve-config) - PRESERVE_CONFIG=true - shift - ;; - --clean) - CLEAN_MODE=true - shift - ;; - --no-config) - NO_CONFIG=true - shift - ;; - --help|-h) - show_usage - exit 0 - ;; - *) - echo -e "${RED}Error: Unknown option: $1${NC}" - show_usage - exit 1 - ;; - esac -done - -# Handle clean mode -if [ "$CLEAN_MODE" = true ]; then - SANDBOX_DIR=$(find_existing_sandbox) - if [ -n "$SANDBOX_DIR" ]; then - echo -e "${YELLOW}Removing sandbox: $SANDBOX_DIR${NC}" - rm -rf "$SANDBOX_DIR" - echo -e "${GREEN}✓${NC} Sandbox removed" - - # Clean up base directory if empty - if [ -d "$SANDBOX_BASE" ] && [ -z "$(ls -A "$SANDBOX_BASE")" ]; then - rmdir "$SANDBOX_BASE" - fi - else - echo -e "${YELLOW}No sandbox found to clean${NC}" - fi - exit 0 -fi - -# Handle reset mode -if [ "$RESET_MODE" = true ]; then - # Try to detect if we're in a sandbox directory first - DETECTED_SANDBOX=$(detect_sandbox_dir) - if [ -n "$DETECTED_SANDBOX" ] && [ -d "$DETECTED_SANDBOX" ]; then - SANDBOX_DIR="$DETECTED_SANDBOX" - else - # Fall back to finding existing sandbox from project root - SANDBOX_DIR=$(find_existing_sandbox) - fi - - if [ -z "$SANDBOX_DIR" ] || [ ! -d "$SANDBOX_DIR" ]; then - echo -e "${RED}Error: No existing sandbox found. Run without --reset to create one.${NC}" - exit 1 - fi - - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BLUE} Labcommitr Testing Sandbox - Quick Reset${NC}" - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo "" - echo -e "${GREEN}✓${NC} Resetting sandbox: $SANDBOX_DIR" - cd "$SANDBOX_DIR" - - # Preserve config if requested - CONFIG_BACKUP="" - if [ "$PRESERVE_CONFIG" = true ] && [ -f ".labcommitr.config.yaml" ]; then - CONFIG_BACKUP=$(mktemp) - cp ".labcommitr.config.yaml" "$CONFIG_BACKUP" - echo -e "${GREEN}✓${NC} Preserving config file..." - fi - - # Reset git state - git reset --hard HEAD 2>/dev/null || true - git clean -fd 2>/dev/null || true - - # Restore config if preserved - if [ -n "$CONFIG_BACKUP" ] && [ -f "$CONFIG_BACKUP" ]; then - cp "$CONFIG_BACKUP" ".labcommitr.config.yaml" - rm "$CONFIG_BACKUP" - echo -e "${GREEN}✓${NC} Config file restored" - fi - - # Ensure directories exist (they might have been removed by git clean) - mkdir -p src docs lib utils - - # Re-apply changes (same as modification phase) - echo -e "${GREEN}✓${NC} Re-applying test file states..." - - # Modify 4 files - cat >> src/component-a.ts << 'EOF' -export function newFeatureA() { return 'new'; } -EOF - cat >> src/component-b.ts << 'EOF' -export function newFeatureB() { return 'new'; } -EOF - cat >> src/component-c.ts << 'EOF' -export function newFeatureC() { return 'new'; } -EOF - cat >> src/component-d.ts << 'EOF' -export function newFeatureD() { return 'new'; } -EOF - git add src/component-a.ts src/component-b.ts src/component-c.ts src/component-d.ts - - # Add 4 new files - echo "# New service A" > src/service-a.ts - echo "export class ServiceA {}" >> src/service-a.ts - git add src/service-a.ts - - echo "# New service B" > src/service-b.ts - echo "export class ServiceB {}" >> src/service-b.ts - git add src/service-b.ts - - echo "# New service C" > src/service-c.ts - echo "export class ServiceC {}" >> src/service-c.ts - git add src/service-c.ts - - echo "# New service D" > docs/guide.md - echo "# User Guide" >> docs/guide.md - git add docs/guide.md - - # Delete 4 files - git rm -f utils/old-util-1.js utils/old-util-2.js utils/old-util-3.js utils/old-util-4.js 2>/dev/null || true - - # Rename 4 files - git mv -f lib/helpers.ts lib/helper-functions.ts 2>/dev/null || true - git mv -f lib/constants.ts lib/app-constants.ts 2>/dev/null || true - git mv -f lib/types.ts lib/type-definitions.ts 2>/dev/null || true - git mv -f lib/config.ts lib/configuration.ts 2>/dev/null || true - - # Copy 4 files - cp src/model-1.ts src/model-1-backup.ts - echo "" >> src/model-1-backup.ts - echo "// Backup copy" >> src/model-1-backup.ts - git add src/model-1-backup.ts - - cp src/model-2.ts src/model-2-backup.ts - echo "" >> src/model-2-backup.ts - echo "// Backup copy" >> src/model-2-backup.ts - git add src/model-2-backup.ts - - cp src/model-3.ts lib/model-3-copy.ts - echo "" >> lib/model-3-copy.ts - echo "// Copy in lib directory" >> lib/model-3-copy.ts - git add lib/model-3-copy.ts - - cp src/model-4.ts lib/model-4-copy.ts - echo "" >> lib/model-4-copy.ts - echo "// Copy in lib directory" >> lib/model-4-copy.ts - git add lib/model-4-copy.ts - - # Pre-staged file - echo "# Pre-staged file" > pre-staged.ts - git add pre-staged.ts - - # Always ensure reset script is present and up-to-date - if [ -f "$SCRIPT_DIR/reset-sandbox.sh" ]; then - cp "$SCRIPT_DIR/reset-sandbox.sh" "reset-sandbox.sh" - chmod +x "reset-sandbox.sh" - echo -e "${GREEN}✓${NC} Reset script updated" - fi - - echo "" - echo -e "${GREEN}✓${NC} Sandbox reset complete!" - echo "" - echo -e "${YELLOW}Sandbox location:${NC} $SANDBOX_DIR" - echo "" - echo -e "${YELLOW}To test:${NC}" - echo " cd $SANDBOX_DIR" - echo " node $PROJECT_ROOT/dist/index.js commit" - echo "" - echo -e "${YELLOW}To reset again (from within sandbox):${NC}" - echo " bash $SCRIPT_DIR/labcommitr-sandbox.sh --reset" - echo " bash $SCRIPT_DIR/labcommitr-sandbox.sh --reset --preserve-config" - echo "" - echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - exit 0 -fi - -# Normal mode: create or recreate sandbox -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -echo -e "${BLUE} Labcommitr Testing Sandbox${NC}" -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -echo "" - -# Generate sandbox name -SANDBOX_NAME=$(generate_sandbox_name) -SANDBOX_DIR="$SANDBOX_BASE/$SANDBOX_NAME" - -# Clean up existing sandbox if it exists -if [ -d "$SANDBOX_DIR" ]; then - echo -e "${YELLOW}⚠ Cleaning up existing sandbox...${NC}" - echo -e "${YELLOW} (This ensures all file types are properly staged)${NC}" - rm -rf "$SANDBOX_DIR" -fi - -# Create sandbox directory -echo -e "${GREEN}✓${NC} Creating sandbox: $SANDBOX_NAME" -mkdir -p "$SANDBOX_DIR" -cd "$SANDBOX_DIR" - -# Initialize git repository -echo -e "${GREEN}✓${NC} Initializing git repository..." -git init --initial-branch=main -git config user.name "Test User" -git config user.email "test@example.com" - -# Copy config file if it exists (unless --no-config flag is set) -if [ "$NO_CONFIG" = false ]; then - if [ -f "$PROJECT_ROOT/.labcommitr.config.yaml" ]; then - echo -e "${GREEN}✓${NC} Copying config file..." - cp "$PROJECT_ROOT/.labcommitr.config.yaml" "$SANDBOX_DIR/.labcommitr.config.yaml" - else - echo -e "${YELLOW}⚠ No config file found. Run 'lab init' in sandbox after setup.${NC}" - fi -else - echo -e "${YELLOW}⚠ Sandbox created without config. Run 'lab init' to set up configuration.${NC}" -fi - -# Copy reset script for convenience -if [ -f "$SCRIPT_DIR/reset-sandbox.sh" ]; then - cp "$SCRIPT_DIR/reset-sandbox.sh" "$SANDBOX_DIR/reset-sandbox.sh" - chmod +x "$SANDBOX_DIR/reset-sandbox.sh" -fi - -# Create initial commit -echo -e "${GREEN}✓${NC} Creating initial commit structure..." -cat > README.md << 'EOF' -# Test Repository - -This is a sandbox repository for testing the labcommitr commit command. -Safe to experiment with - nothing affects your real repository. -EOF - -cat > package.json << 'EOF' -{ - "name": "test-project", - "version": "1.0.0", - "description": "Test project for commit command" -} -EOF - -git add . -git commit -m "Initial commit" --no-verify - -# Create various file states for testing -echo -e "${GREEN}✓${NC} Creating test files with various states..." - -# Create directory structure -mkdir -p src docs lib utils - -# ============================================================================ -# SETUP PHASE: Create base files that will be modified/deleted/renamed/copied -# ============================================================================ - -# Files for modification (4 files) -echo "# Component A" > src/component-a.ts -echo "export class ComponentA {}" >> src/component-a.ts -git add src/component-a.ts -git commit -m "Add component A" --no-verify - -echo "# Component B" > src/component-b.ts -echo "export class ComponentB {}" >> src/component-b.ts -git add src/component-b.ts -git commit -m "Add component B" --no-verify - -echo "# Component C" > src/component-c.ts -echo "export class ComponentC {}" >> src/component-c.ts -git add src/component-c.ts -git commit -m "Add component C" --no-verify - -echo "# Component D" > src/component-d.ts -echo "export class ComponentD {}" >> src/component-d.ts -git add src/component-d.ts -git commit -m "Add component D" --no-verify - -# Files for deletion (4 files) -echo "# Old utility 1" > utils/old-util-1.js -git add utils/old-util-1.js -git commit -m "Add old util 1" --no-verify - -echo "# Old utility 2" > utils/old-util-2.js -git add utils/old-util-2.js -git commit -m "Add old util 2" --no-verify - -echo "# Old utility 3" > utils/old-util-3.js -git add utils/old-util-3.js -git commit -m "Add old util 3" --no-verify - -echo "# Old utility 4" > utils/old-util-4.js -git add utils/old-util-4.js -git commit -m "Add old util 4" --no-verify - -# Files for renaming (4 files) -echo "# Helper functions" > lib/helpers.ts -git add lib/helpers.ts -git commit -m "Add helpers" --no-verify - -echo "# Constants" > lib/constants.ts -git add lib/constants.ts -git commit -m "Add constants" --no-verify - -echo "# Types" > lib/types.ts -git add lib/types.ts -git commit -m "Add types" --no-verify - -echo "# Config" > lib/config.ts -git add lib/config.ts -git commit -m "Add config" --no-verify - -# Files for copying (4 files - will copy these) -echo "# Original model 1" > src/model-1.ts -git add src/model-1.ts -git commit -m "Add model 1" --no-verify - -echo "# Original model 2" > src/model-2.ts -git add src/model-2.ts -git commit -m "Add model 2" --no-verify - -echo "# Original model 3" > src/model-3.ts -git add src/model-3.ts -git commit -m "Add model 3" --no-verify - -echo "# Original model 4" > src/model-4.ts -git add src/model-4.ts -git commit -m "Add model 4" --no-verify - -# ============================================================================ -# MODIFICATION PHASE: Apply changes for testing -# ============================================================================ - -# Modify 4 files (M - Modified) and STAGE them -# IMPORTANT: Modify first, then stage all at once to ensure they're staged -cat >> src/component-a.ts << 'EOF' -export function newFeatureA() { return 'new'; } -EOF - -cat >> src/component-b.ts << 'EOF' -export function newFeatureB() { return 'new'; } -EOF - -cat >> src/component-c.ts << 'EOF' -export function newFeatureC() { return 'new'; } -EOF - -cat >> src/component-d.ts << 'EOF' -export function newFeatureD() { return 'new'; } -EOF - -# Stage all modified files together -git add src/component-a.ts src/component-b.ts src/component-c.ts src/component-d.ts - -# Add 4 new files (A - Added) and STAGE them -echo "# New service A" > src/service-a.ts -echo "export class ServiceA {}" >> src/service-a.ts -git add src/service-a.ts - -echo "# New service B" > src/service-b.ts -echo "export class ServiceB {}" >> src/service-b.ts -git add src/service-b.ts - -echo "# New service C" > src/service-c.ts -echo "export class ServiceC {}" >> src/service-c.ts -git add src/service-c.ts - -echo "# New service D" > docs/guide.md -echo "# User Guide" >> docs/guide.md -git add docs/guide.md - -# Delete 4 files (D - Deleted) -git rm utils/old-util-1.js -git rm utils/old-util-2.js -git rm utils/old-util-3.js -git rm utils/old-util-4.js - -# Rename 4 files (R - Renamed) -git mv lib/helpers.ts lib/helper-functions.ts -git mv lib/constants.ts lib/app-constants.ts -git mv lib/types.ts lib/type-definitions.ts -git mv lib/config.ts lib/configuration.ts - -# Copy 4 files (C - Copied) -# IMPORTANT: For Git to detect copies, source files must exist in previous commits -# and copies must have sufficient content (Git needs similarity threshold) -# We'll add more content to ensure detection works -cp src/model-1.ts src/model-1-backup.ts -# Add a comment to make it a proper copy (but still similar enough) -echo "" >> src/model-1-backup.ts -echo "// Backup copy" >> src/model-1-backup.ts -git add src/model-1-backup.ts - -cp src/model-2.ts src/model-2-backup.ts -echo "" >> src/model-2-backup.ts -echo "// Backup copy" >> src/model-2-backup.ts -git add src/model-2-backup.ts - -cp src/model-3.ts lib/model-3-copy.ts -echo "" >> lib/model-3-copy.ts -echo "// Copy in lib directory" >> lib/model-3-copy.ts -git add lib/model-3-copy.ts - -cp src/model-4.ts lib/model-4-copy.ts -echo "" >> lib/model-4-copy.ts -echo "// Copy in lib directory" >> lib/model-4-copy.ts -git add lib/model-4-copy.ts - -# Create one staged file for testing already-staged scenario -echo "# Pre-staged file" > pre-staged.ts -git add pre-staged.ts - -echo "" -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -echo -e "${GREEN}✓${NC} Sandbox ready: $SANDBOX_NAME" -echo "" -echo -e "${YELLOW}Sandbox location:${NC} $SANDBOX_DIR" -echo "" -echo -e "${YELLOW}Current repository state (all staged):${NC}" -echo " • Modified files (4): src/component-{a,b,c,d}.ts" -echo " • Added files (4): src/service-{a,b,c}.ts, docs/guide.md" -echo " • Deleted files (4): utils/old-util-{1,2,3,4}.js" -echo " • Renamed files (4): lib/{helpers→helper-functions, constants→app-constants, types→type-definitions, config→configuration}.ts" -echo " • Copied files (4): src/model-{1,2}-backup.ts, lib/model-{3,4}-copy.ts" -echo " • Pre-staged file: pre-staged.ts" -echo "" -echo -e "${YELLOW}To test:${NC}" -echo " cd $SANDBOX_DIR" -echo " node $PROJECT_ROOT/dist/index.js commit" -echo "" -echo -e "${YELLOW}To reset (from within sandbox):${NC}" -echo " bash reset-sandbox.sh # Reset, remove config" -echo " bash reset-sandbox.sh --preserve-config # Reset, keep config" -echo "" -echo -e "${YELLOW}To reset (from project root):${NC}" -echo " pnpm run test:sandbox:reset # Reset, remove config" -echo " bash $SCRIPT_DIR/labcommitr-sandbox.sh --reset --preserve-config # Reset, keep config" -echo "" -echo -e "${YELLOW}To reset (full recreation):${NC}" -echo " pnpm run test:sandbox" -echo "" -echo -e "${YELLOW}To clean up:${NC}" -echo " pnpm run test:sandbox:clean" -echo "" -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - diff --git a/scripts/reset-sandbox.sh b/scripts/reset-sandbox.sh deleted file mode 100755 index 56ab68d..0000000 --- a/scripts/reset-sandbox.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/bash - -# Quick reset script for sandbox repositories -# Can be run from within a sandbox directory -# Usage: bash reset-sandbox.sh [--preserve-config] - -# Check if we're in a sandbox directory -CURRENT_DIR="$(pwd)" -if [[ "$CURRENT_DIR" != *"/.sandbox/"* ]]; then - echo "Error: This script must be run from within a sandbox directory" - echo "Current directory: $CURRENT_DIR" - echo "" - echo "To reset from project root, use:" - echo " pnpm run test:sandbox:reset" - exit 1 -fi - -# Find the project root by looking for .sandbox parent -# Current dir is something like: /path/to/project/.sandbox/atom -# We need to go up to find the project root -SANDBOX_DIR="$CURRENT_DIR" -PROJECT_ROOT="${SANDBOX_DIR%/.sandbox/*}" -SCRIPT_DIR="$PROJECT_ROOT/scripts" - -# Verify the script exists -if [ ! -f "$SCRIPT_DIR/labcommitr-sandbox.sh" ]; then - echo "Error: Could not find labcommitr-sandbox.sh script" - echo "Expected at: $SCRIPT_DIR/labcommitr-sandbox.sh" - exit 1 -fi - -# Call the main sandbox script with reset flag -if [ "$1" = "--preserve-config" ]; then - bash "$SCRIPT_DIR/labcommitr-sandbox.sh" --reset --preserve-config -else - bash "$SCRIPT_DIR/labcommitr-sandbox.sh" --reset -fi - From 58a970e2b554486583f4ba5d257aad973e4e92d0 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 23 Nov 2025 19:54:18 -0700 Subject: [PATCH 05/24] docs: reorganize documentation structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Create docs/ directory for architecture and development documentation • Move ARCHITECTURE_DECISIONS.md to docs/ • Move CONFIG_SCHEMA.md to docs/ • Move DEVELOPMENT_GUIDELINES.md to docs/ • Move DEVELOPMENT_PROGRESS.md to docs/ • Move REQUIREMENTS.md to docs/ • Move COMMIT_COMMAND_SPECIFICATION.md to docs/ • Keep TESTING.md in root as user-facing documentation • Update .gitignore to allow docs/*.md and TESTING.md • Update README.md references to use docs/ paths • Keep project root clean with only user-facing docs --- .gitignore | 6 +- README.md | 48 +- TESTING.md | 519 +++++++++++++++++++ docs/ARCHITECTURE_DECISIONS.md | 215 ++++++++ docs/COMMIT_COMMAND_SPECIFICATION.md | 692 ++++++++++++++++++++++++++ docs/CONFIG_SCHEMA.md | 364 ++++++++++++++ docs/DEVELOPMENT_GUIDELINES.md | 711 +++++++++++++++++++++++++++ docs/DEVELOPMENT_PROGRESS.md | 384 +++++++++++++++ docs/REQUIREMENTS.md | 123 +++++ 9 files changed, 3048 insertions(+), 14 deletions(-) create mode 100644 TESTING.md create mode 100644 docs/ARCHITECTURE_DECISIONS.md create mode 100644 docs/COMMIT_COMMAND_SPECIFICATION.md create mode 100644 docs/CONFIG_SCHEMA.md create mode 100644 docs/DEVELOPMENT_GUIDELINES.md create mode 100644 docs/DEVELOPMENT_PROGRESS.md create mode 100644 docs/REQUIREMENTS.md diff --git a/.gitignore b/.gitignore index 8efb2a1..658662e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ coverage/ *.lcov # Testing -# Sandbox directory for testing Labcommitr commands (scripts in scripts/ are tracked) +# Sandbox directory for testing Labcommitr commands .sandbox/ test-results/ @@ -75,10 +75,12 @@ Thumbs.db rust-src/ ### Documentation -# Ignore all .md files except README.md, CHANGELOG.md, and .changeset/*.md (local reference only) +# Ignore all .md files except README.md, CHANGELOG.md, TESTING.md, docs/*.md, and .changeset/*.md *.md !README.md !CHANGELOG.md +!TESTING.md +!docs/*.md !.changeset/*.md ### Labcommitr Configuration diff --git a/README.md b/README.md index 29ffffd..bc4dffe 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Labcommitr uses a `.labcommitr.config.yaml` file in your project root. The confi - **Keyboard shortcuts** - Enable and customize shortcuts for faster navigation - **Git integration** - Auto-staging and commit signing preferences -See [`CONFIG_SCHEMA.md`](CONFIG_SCHEMA.md) for complete configuration documentation. +See [`docs/CONFIG_SCHEMA.md`](docs/CONFIG_SCHEMA.md) for complete configuration documentation. **Configuration discovery:** - Searches from current directory up to project root @@ -185,25 +185,49 @@ See [`CONFIG_SCHEMA.md`](CONFIG_SCHEMA.md) for complete configuration documentat ## Development & Testing -### Testing Sandbox +### Testing Environment -For safe testing of Labcommitr commands without affecting your real repository, use the testing sandbox: +For safe testing of Labcommitr commands without affecting your real repository, use the built-in testing environment: ```bash -# Create sandbox with config (if available) -pnpm run test:sandbox +# Set up test environment (default scenario) +lab test setup -# Create sandbox without config (start from scratch) -pnpm run test:sandbox:bare +# Open shell in test environment +lab test shell -# Quick reset for iterative testing -pnpm run test:sandbox:reset +# Run commands normally +lab commit +lab preview +lab revert + +# Reset environment for another test +lab test reset # Clean up -pnpm run test:sandbox:clean +lab test clean +``` + +**Available Scenarios:** +- `existing-project` - Test adding Labcommitr to existing project +- `with-changes` - Test commit command with various file states (default) +- `with-history` - Test preview and revert with rich history +- `with-merge` - Test revert with merge commits +- `with-conflicts` - Test conflict resolution workflows + +**Examples:** +```bash +# Set up specific scenario +lab test setup --scenario with-history + +# List all scenarios +lab test list-scenarios + +# Check current status +lab test status ``` -See [`scripts/README.md`](scripts/README.md) for complete testing documentation. +See [`TESTING.md`](TESTING.md) for complete testing documentation. --- @@ -211,7 +235,7 @@ See [`scripts/README.md`](scripts/README.md) for complete testing documentation. Contributions are welcome! Please ensure your commits follow the project's commit message format (which you can set up using `lab init`). -For development guidelines, see [`DEVELOPMENT_GUIDELINES.md`](DEVELOPMENT_GUIDELINES.md). +For development guidelines, see [`docs/DEVELOPMENT_GUIDELINES.md`](docs/DEVELOPMENT_GUIDELINES.md). --- diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..59a4757 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,519 @@ +# Labcommitr Testing Environment + +A simple, flexible testing environment for testing Labcommitr commands in isolated git repositories. + +## Quick Start + +```bash +# Set up test environment (default scenario) +lab test setup + +# Open shell in test environment +lab test shell + +# Run commands normally +lab commit +lab preview +lab revert + +# Exit shell when done +exit +``` + +--- + +## Table of Contents + +- [Overview](#overview) +- [Quick Start](#quick-start) +- [Scenarios](#scenarios) +- [Commands](#commands) +- [Workflows](#workflows) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +The testing environment provides isolated git repositories with predefined states (scenarios) for testing Labcommitr commands. Each scenario represents a different git repository state that you can use to test various commands and workflows. + +**Key Features:** +- ✅ Simple command interface +- ✅ Multiple scenarios for different testing needs +- ✅ Real-world git states (no artificial staging) +- ✅ Quick reset and scenario switching +- ✅ Safe and isolated (doesn't affect your real repository) + +**Sandbox Location:** +- Single sandbox: `.sandbox/test/` +- Predictable location (easy to find) +- Git-ignored (won't be committed) + +--- + +## Scenarios + +Scenarios represent different git repository states. Each scenario is designed to test specific commands or workflows. + +### `existing-project` + +**Purpose:** Test adding Labcommitr to an existing project + +**State:** +- Pre-existing commit history (20-30 commits) +- Uncommitted changes (modified, added, deleted, renamed files) +- Changes are **not staged** (natural git state) +- **No** `.labcommitr.config.yaml` file + +**Use Cases:** +- Test `lab init` on existing project +- Test first commit after adding Labcommitr +- Test config creation workflow + +**Setup:** +```bash +lab test setup --scenario existing-project +``` + +--- + +### `with-changes` + +**Purpose:** Test commit command with various file states + +**State:** +- Pre-existing commit history (20-30 commits) +- Uncommitted changes (modified, added, deleted, renamed files) +- Changes are **not staged** (natural git state) +- `.labcommitr.config.yaml` file present + +**Use Cases:** +- Test `lab commit` with various file states +- Test auto-stage behavior (if enabled in config) +- Test commit message prompts +- Test validation rules + +**Setup:** +```bash +lab test setup --scenario with-changes +``` + +**Default Scenario:** This is the default scenario if none is specified. + +--- + +### `with-history` + +**Purpose:** Test preview and revert commands with rich history + +**State:** +- Extensive commit history (100+ commits) +- Varied commit messages (feat, fix, docs, refactor, etc.) +- Commits with and without bodies +- `.labcommitr.config.yaml` file present +- No uncommitted changes +- Clean working directory + +**Use Cases:** +- Test `lab preview` pagination +- Test `lab preview` detail view +- Test `lab preview` navigation +- Test `lab revert` commit selection +- Test `lab revert` workflow + +**Setup:** +```bash +lab test setup --scenario with-history +``` + +--- + +### `with-merge` + +**Purpose:** Test revert with merge commits + +**State:** +- Git repository with merge commits +- Multiple branches merged into main +- Merge commits with multiple parents +- `.labcommitr.config.yaml` file present +- No uncommitted changes +- Clean working directory + +**Use Cases:** +- Test `lab revert` with merge commits +- Test parent selection for merge commits +- Test merge commit handling + +**Setup:** +```bash +lab test setup --scenario with-merge +``` + +--- + +### `with-conflicts` + +**Purpose:** Test conflict resolution workflows + +**State:** +- Git repository in conflict state +- Unmerged files (conflict markers present) +- Revert operation in progress (optional) +- `.labcommitr.config.yaml` file present +- Conflict state ready for resolution + +**Use Cases:** +- Test `lab revert --continue` after conflict resolution +- Test `lab revert --abort` to cancel revert +- Test conflict resolution workflow + +**Setup:** +```bash +lab test setup --scenario with-conflicts +``` + +--- + +## Commands + +### `lab test setup [--scenario ]` + +Set up test environment with specified scenario. + +**Options:** +- `-s, --scenario ` - Scenario name (default: `with-changes`) + +**Examples:** +```bash +# Set up default scenario (with-changes) +lab test setup + +# Set up specific scenario +lab test setup --scenario existing-project +lab test setup --scenario with-history +lab test setup --scenario with-merge +``` + +**What it does:** +- Builds project if needed +- Creates/updates sandbox in `.sandbox/test/` +- Generates scenario with appropriate git state +- Sets up config file (if scenario requires it) +- Ready for testing + +--- + +### `lab test shell` + +Open interactive shell in test environment. + +**Examples:** +```bash +lab test shell +``` + +**What it does:** +- Opens shell in test environment directory +- Changes working directory to sandbox +- You can run commands normally (`lab commit`, `lab preview`, etc.) +- Exit with `exit` or `Ctrl+D` + +**Note:** Make sure you've run `lab test setup` first. + +--- + +### `lab test reset` + +Reset current scenario to initial state. + +**Examples:** +```bash +lab test reset +``` + +**What it does:** +- Resets current scenario to initial state +- Keeps same scenario active +- Fast reset (preserves repo structure) +- Ready for testing again + +--- + +### `lab test clean` + +Remove test environment completely. + +**Examples:** +```bash +lab test clean +``` + +**What it does:** +- Removes sandbox directory +- Cleans up all test artifacts +- Returns to clean state + +--- + +### `lab test status` + +Show current test environment status. + +**Examples:** +```bash +lab test status +``` + +**What it shows:** +- Current scenario name +- Scenario description +- Sandbox location +- Git status summary +- Uncommitted changes count + +--- + +### `lab test list-scenarios` + +List all available scenarios. + +**Examples:** +```bash +lab test list-scenarios +``` + +**What it shows:** +- All available scenarios +- Description for each scenario +- Use cases for each scenario + +--- + +## Workflows + +### Workflow 1: Testing Commit Command + +```bash +# Set up environment +lab test setup --scenario with-changes + +# Enter test environment +lab test shell + +# Test commit +lab commit +# [interactive commit prompts] + +# Exit and reset for another test +exit +lab test reset +lab test shell +lab commit -t feat -m "quick commit" +``` + +--- + +### Workflow 2: Testing Preview Command + +```bash +# Set up environment with history +lab test setup --scenario with-history + +# Enter test environment +lab test shell + +# Test preview +lab preview +# [browse 100+ commits] +# [test pagination, detail view, navigation] +``` + +--- + +### Workflow 3: Testing Revert Command + +```bash +# Set up environment +lab test setup --scenario with-history + +# Enter test environment +lab test shell + +# Test revert +lab revert +# [select commit to revert] +# [go through revert workflow] + +# Test revert with merge commits +exit +lab test setup --scenario with-merge +lab test shell +lab revert +# [select merge commit] +# [select parent] +# [complete revert] +``` + +--- + +### Workflow 4: Testing Init on Existing Project + +```bash +# Set up environment +lab test setup --scenario existing-project + +# Enter test environment +lab test shell + +# Test init workflow +lab init +# [interactive init prompts] +# [config file created] + +# Test first commit +lab commit +# [commit with new config] +``` + +--- + +### Workflow 5: Real-World End-to-End Workflow + +```bash +# Set up environment +lab test setup --scenario with-changes + +# Enter test environment +lab test shell + +# Real workflow +lab commit -t feat -s api -m "add new endpoint" +lab preview +lab commit -t fix -m "fix bug in endpoint" +lab preview +lab revert +# [select commit to revert] +# [complete revert workflow] +lab preview +# [verify revert commit] +``` + +--- + +### Workflow 6: Testing Conflict Resolution + +```bash +# Set up environment +lab test setup --scenario with-conflicts + +# Enter test environment +lab test shell + +# Test abort +lab revert --abort +# [revert cancelled] + +# Reset and test continue +exit +lab test reset +lab test shell +# [manually resolve conflicts] +lab revert --continue +# [revert completed] +``` + +--- + +## Troubleshooting + +### "No active test environment found" + +**Problem:** You're trying to use test commands but no environment is set up. + +**Solution:** +```bash +lab test setup +``` + +--- + +### "Invalid scenario" + +**Problem:** You specified a scenario name that doesn't exist. + +**Solution:** +```bash +# List available scenarios +lab test list-scenarios + +# Use correct scenario name +lab test setup --scenario with-changes +``` + +--- + +### Build Required + +**Problem:** Project needs to be built before testing. + +**Solution:** +The `lab test setup` command automatically builds the project if needed. If you encounter build issues: + +```bash +# Build manually +pnpm run build + +# Then set up test environment +lab test setup +``` + +--- + +### Sandbox Location + +**Problem:** Can't find sandbox or want to access it directly. + +**Solution:** +- Sandbox is always at: `.sandbox/test/` +- Use `lab test shell` to enter it +- Or navigate manually: `cd .sandbox/test/` + +--- + +### Reset Not Working + +**Problem:** Reset doesn't restore scenario properly. + +**Solution:** +```bash +# Clean and recreate +lab test clean +lab test setup --scenario +``` + +--- + +## Tips + +1. **Use `lab test shell`** - Easiest way to test commands in the environment +2. **Check status** - Use `lab test status` to see current state +3. **Quick reset** - Use `lab test reset` for fast iteration +4. **Test workflows** - Chain multiple commands to test real-world usage +5. **Switch scenarios** - Use different scenarios for different testing needs + +--- + +## Safety + +The test environment is **100% safe**: +- ✅ Isolated from your real repository +- ✅ No remote configured (can't push) +- ✅ Easy cleanup (`lab test clean`) +- ✅ Git-ignored (won't be committed) + +--- + +**Last Updated:** January 2025 +**Sandbox Location:** `.sandbox/test/` + diff --git a/docs/ARCHITECTURE_DECISIONS.md b/docs/ARCHITECTURE_DECISIONS.md new file mode 100644 index 0000000..32c04f9 --- /dev/null +++ b/docs/ARCHITECTURE_DECISIONS.md @@ -0,0 +1,215 @@ +# Architecture Decision Records (ADRs) + +## Overview + +This document tracks all major architectural decisions made during Labcommitr development. Each decision is recorded with context, alternatives considered, and consequences to ensure future developers understand the reasoning behind current implementations. + +--- + +## ADR-001: Configuration Schema Design + +**Date**: 2025-01-16 +**Status**: Accepted +**Participants**: Development Team + +### Context +Need to define the configuration file structure that will be used throughout the application. The schema must be: +- Easy for users to understand and modify +- Flexible enough to handle future requirements +- Validatable with clear error messages +- Backward compatible as features evolve + +### Decision +Use YAML-based configuration with the following principles: +- Only `types` array is required (zero-config philosophy) +- All other fields have sensible defaults +- Dynamic emoji support with automatic terminal detection +- Template-based message formatting +- Extensible validation system + +### Alternatives Considered +1. **JSON Configuration**: Rejected due to lack of comments and reduced readability +2. **JavaScript Configuration**: Rejected due to security concerns and complexity +3. **Multiple Config Files**: Rejected due to increased complexity and user confusion +4. **Everything Required**: Rejected as it violates zero-config principle + +### Consequences +- **Positive**: Users can start with minimal configuration +- **Positive**: Easy to extend without breaking existing configs +- **Positive**: Clear separation between required and optional settings +- **Negative**: More complex default merging logic required +- **Negative**: Need to maintain backward compatibility as schema evolves + +--- + +## ADR-002: Dynamic Emoji Fallback Strategy + +**Date**: 2025-01-16 +**Status**: Accepted +**Participants**: Development Team + +### Context +Terminal emoji support varies widely across platforms and terminals. Need a strategy that: +- Provides great experience when emojis are supported +- Falls back gracefully when they're not +- Doesn't require system modifications +- Maintains consistent message formatting + +### Decision +Implement dynamic emoji detection with automatic fallback: +- Detect terminal emoji support at runtime +- Use emoji when supported, fall back to text when not +- Same configuration works across all terminals +- Allow manual override via configuration + +### Alternatives Considered +1. **Always Use Emojis**: Rejected due to poor experience on unsupported terminals +2. **Never Use Emojis**: Rejected as it limits visual enhancement capabilities +3. **Install Emoji Fonts**: Rejected due to security/permission concerns +4. **ASCII Art Fallbacks**: Rejected due to complexity and inconsistent spacing + +### Consequences +- **Positive**: Optimal experience on all terminals without user intervention +- **Positive**: No system modifications required +- **Positive**: Configuration is portable across environments +- **Negative**: Requires emoji detection library and logic +- **Negative**: More complex template processing + +--- + +## ADR-003: Modular Architecture with Plugin Patterns + +**Date**: 2025-01-16 +**Status**: Accepted +**Participants**: Development Team + +### Context +Project requirements are expected to evolve significantly. Architecture must be flexible enough to: +- Add new configuration fields without code changes +- Extend validation rules easily +- Support new command types +- Allow customization of core behaviors + +### Decision +Use modular architecture with plugin patterns: +- Interface-based design for all major components +- Registry patterns for extensible functionality +- Dependency injection for loose coupling +- Configuration-driven behavior where possible + +### Alternatives Considered +1. **Monolithic Architecture**: Rejected due to inflexibility for future changes +2. **Microservice Architecture**: Rejected as overkill for CLI tool +3. **Event-Driven Architecture**: Rejected due to added complexity for limited benefit + +### Consequences +- **Positive**: Easy to extend and modify functionality +- **Positive**: Components are testable in isolation +- **Positive**: Clear separation of concerns +- **Negative**: More initial complexity in setup +- **Negative**: Requires more careful interface design + +--- + +## ADR-004: Version Control and Development Workflow + +**Date**: 2025-01-16 +**Status**: Accepted +**Participants**: Development Team + +### Context +Need to establish development practices that ensure: +- High code quality and maintainability +- Clear change tracking and history +- Easy rollback of problematic changes +- Professional development standards + +### Decision +Implement strict version control practices: +- Feature branches for all major implementations +- Small, atomic commits with descriptive messages +- Mandatory commit message format with bullet points +- No direct commits to main branch +- Comprehensive testing before merges + +### Alternatives Considered +1. **Trunk-Based Development**: Rejected due to risk of unstable main branch +2. **GitFlow**: Rejected as too complex for project size +3. **Squash Merging**: Rejected as it loses detailed change history + +### Consequences +- **Positive**: Clear change history and easy debugging +- **Positive**: Safe rollback of individual changes +- **Positive**: Professional development practices +- **Negative**: Requires discipline in commit practices +- **Negative**: Slightly more overhead in branching workflow + +--- + +## ADR-005: Zero-Config Philosophy with Sensible Defaults + +**Date**: 2025-01-16 +**Status**: Accepted +**Participants**: Development Team + +### Context +Tool should be immediately usable while still allowing full customization. Need to balance: +- Ease of initial use (zero configuration) +- Power user customization capabilities +- Clear upgrade path from simple to complex usage + +### Decision +Implement zero-config philosophy: +- Only commit types are required in configuration +- All other settings have sensible defaults +- Users can progressively add customization as needed +- Full power available when desired + +### Alternatives Considered +1. **Everything Explicit**: Rejected due to poor initial user experience +2. **Wizard-Based Setup**: Rejected as it adds friction to getting started +3. **Multiple Configuration Levels**: Rejected due to complexity + +### Consequences +- **Positive**: Excellent onboarding experience +- **Positive**: Progressive disclosure of complexity +- **Positive**: Tool works immediately after installation +- **Negative**: More complex default merging logic +- **Negative**: Need to choose defaults carefully + +--- + +## Template for Future ADRs + +```markdown +## ADR-XXX: [Decision Title] + +**Date**: [YYYY-MM-DD] +**Status**: [Proposed | Accepted | Rejected | Superseded] +**Participants**: [Who was involved in the decision] + +### Context +[Describe the situation that requires a decision] + +### Decision +[State the decision that was made] + +### Alternatives Considered +[List other options that were considered and why they were rejected] + +### Consequences +[Describe the positive and negative consequences of this decision] +``` + +--- + +## Decision Status Definitions + +- **Proposed**: Decision is under consideration +- **Accepted**: Decision is approved and should be implemented +- **Rejected**: Decision was considered but not adopted +- **Superseded**: Decision was replaced by a later decision + +--- + +**Note**: All architectural decisions should be recorded here to maintain institutional knowledge and provide context for future development decisions. diff --git a/docs/COMMIT_COMMAND_SPECIFICATION.md b/docs/COMMIT_COMMAND_SPECIFICATION.md new file mode 100644 index 0000000..fabd5ee --- /dev/null +++ b/docs/COMMIT_COMMAND_SPECIFICATION.md @@ -0,0 +1,692 @@ +# Commit Command Implementation Specification + +**Command:** `lab commit` (alias: `lab c`) + +## Overview + +Interactive commit creation with standardized formatting based on project configuration. Validates input against config rules, stages files per configuration, and creates formatted git commits. + +--- + +## Workflow Sequence + +``` +1. Load Configuration + ↓ +2. Early File Check/Stage (BEFORE prompts) + ↓ +3. Display Staged Files Verification + ↓ +4. Collect Commit Data (prompts) + ↓ +5. Preview Commit Message + ↓ +6. Confirm & Execute Commit + ↓ +7. Cleanup (if cancelled/failed) +``` + +--- + +## Phase 1: Pre-Prompt File Handling + +### 1.1 Load Configuration +- Load `.labcommitr.config.yaml` from project root +- Validate configuration +- Extract: `auto_stage`, `emoji_enabled`, `types`, validation rules + +### 1.2 Check Git Repository +- Verify current directory is a git repository +- If not: Error and exit with message: `"Not a git repository"` + +### 1.3 File Staging Logic + +#### Scenario A: `auto_stage: false` (default) + +**Check:** +```bash +git status --porcelain --staged +``` + +**If empty:** +``` +✗ Error: No files staged for commit + + Nothing has been staged. Please stage files first: + • Use 'git add ' to stage specific files + • Use 'git add -u' to stage all modified files + • Or enable auto_stage in your config + +[Abort immediately - no prompts shown] +``` + +**If files exist:** +- Continue to file verification display +- Track: `alreadyStagedFiles[]` for cleanup reference + +#### Scenario B: `auto_stage: true` + +**Check git status:** +```bash +git status --porcelain +``` + +**Case 1: All unstaged tracked files** +``` +◐ Staging files... +✓ Staged 3 files + +[Continue to verification] +``` + +**Case 2: Mixed state (some already staged, some unstaged)** +``` +◐ Checking git status... + Found 1 file already staged, 2 files unstaged + +◐ Staging remaining files... +✓ Staged 2 additional files + +[Continue to verification] +``` + +**Case 3: Nothing to stage** +``` +◐ Checking git status... +⚠ No modified files to stage + + All files are already committed or there are no changes. + Nothing to commit. + +[Abort immediately] +``` + +**Case 4: Only untracked files** +``` +◐ Checking git status... +⚠ No tracked files to stage + + Only untracked files exist. Stage them manually with 'git add ' + +[Abort immediately] +``` + +**Action:** +- Execute: `git add -u` (stages modified/deleted tracked files only) +- Track what we staged: `newlyStagedFiles[]` +- Preserve already staged files (don't touch them) + +--- + +## Phase 2: File Verification Display + +### 2.1 Collect File Information + +**Commands:** +```bash +# Get file status and paths +git diff --cached --name-status + +# Get line statistics +git diff --cached --numstat +``` + +**Parse output:** +- Status codes: `M` (modified), `A` (added), `D` (deleted), `R` (renamed), `C` (copied) +- Paths: relative to repo root +- Statistics: additions (+), deletions (-) per file + +### 2.2 Display Format + +**Standard display (no mixed state):** +``` +[files] Files to be committed (4 files): + + Modified (2): + M src/auth/login.ts (+45 -12 lines) + M src/auth/types.ts (+23 -8 lines) + + Deleted (1): + D old/auth.ts (-120 lines) + + Added (1): + A tests/auth.test.ts (+67 lines) + +───────────────────────────────────────────── + Press Enter to continue, Ctrl+C to cancel +``` + +**Mixed state display (when auto_stage: true):** +``` +[files] Files to be committed (3 files): + + Already staged (1 file): + M src/auth/login.ts (+45 -12 lines) + + Auto-staged (2 files): + M src/auth/types.ts (+23 -8 lines) + A tests/auth.test.ts (+67 lines) + +───────────────────────────────────────────── + Press Enter to continue, Ctrl+C to cancel +``` + +**Rules:** +- Group files by status (Modified, Added, Deleted, Renamed, Copied) +- Show line statistics in format: `(+additions -deletions lines)` +- Separate "already staged" from "auto-staged" when applicable +- Show total count in header +- Wait for user confirmation before proceeding + +--- + +## Phase 3: Prompt Collection + +### 3.1 Prompt Order +1. Type selection +2. Scope input (conditional) +3. Subject input +4. Body input (conditional) + +### 3.2 Prompt 1: Type Selection + +**Label:** `[type]` (magenta) + +**Display:** +``` +[type] Select commit type: + ◯ feat A new feature for the user + ◯ fix A bug fix for the user + ◯ docs Documentation changes + ◯ refactor Code refactoring without changing functionality + ◯ test Adding or updating tests + ◯ style Code style changes (formatting, semicolons, etc.) + ◯ chore Maintenance tasks, build changes, etc. + +↑/↓ to navigate, Enter to select, type to filter +``` + +**Rules:** +- NO emojis in type list (even if `emoji_enabled: true`) +- Show only: `id` and `description` from config +- Support search/filter (built into @clack/prompts) +- Validate selected type exists in config (if provided via CLI flag) + +**CLI Flag Support:** +- If `--type ` provided: validate it exists in config, skip prompt +- If invalid: Error and list available types + +**Output:** +- Store: `selectedType` (string), `selectedTypeEmoji` (string | undefined) + +### 3.3 Prompt 2: Scope Input + +**Label:** `[scope]` (blue) + +**Conditional Logic:** + +**Case 1: Required by validation rules** +```typescript +if (config.validation.require_scope_for.includes(selectedType)) { + // Scope is required +} +``` + +**Case 2: Not required** +- Allow empty/optional scope + +**Display Options:** + +**Option A: Allowed scopes list exists** +``` +[scope] Enter scope (required for 'feat'): + ◯ auth + ◯ api + ◯ ui + ◯ db + ◯ (custom) Type a custom scope + +↑/↓ to navigate, Enter to select, type to filter +``` + +**Option B: Free-form input** +``` +[scope] Enter scope (optional): _ +``` + +**Validation:** +- If `allowed_scopes` is non-empty: validate input against list +- If invalid: Show error, allow retry +- If required and empty: Show error, require input + +**Error Display:** +``` +[scope] invalid-scope + +⚠ Invalid scope: 'invalid-scope' + Allowed scopes: auth, api, ui, db + +[scope] Enter scope (optional): _ +``` + +**Output:** +- Store: `scope` (string | undefined) + +### 3.4 Prompt 3: Subject Input + +**Label:** `[subject]` (cyan) + +**Display:** +``` +[subject] Enter commit subject (max 50 chars): _ +``` + +**Real-time Validation (after input):** + +**Too Short:** +``` +[subject] hi + +⚠ Subject too short (2 characters) + Minimum length: 3 characters + +[subject] Enter commit subject: _ +``` + +**Too Long:** +``` +[subject] add comprehensive login system with oauth and 2fa + +⚠ Subject too long (58 characters) + Maximum length: 50 characters + Please shorten your message + +[subject] Enter commit subject: _ +``` + +**Prohibited Words:** +``` +[subject] add temp hack fixme + +⚠ Subject contains prohibited words: temp, hack, fixme + Please rephrase your commit message + +[subject] Enter commit subject: _ +``` + +**Multiple Errors:** +``` +[subject] hi fixme + +⚠ Validation failed: + • Subject too short (2 characters, minimum: 3) + • Subject contains prohibited word: fixme + +[subject] Enter commit subject: _ +``` + +**Validation Rules:** +- Check: `config.validation.subject_min_length` +- Check: `config.format.subject_max_length` +- Check: `config.validation.prohibited_words` (case-insensitive) +- Show all errors at once +- Allow retry until valid + +**Output:** +- Store: `subject` (string) + +### 3.5 Prompt 4: Body Input + +**Label:** `[body]` (yellow) + +**Conditional Display:** + +**If required (`config.format.body.required === true`):** +``` +[body] Enter commit body (required, min 20 chars): + (Press 'e' to open editor) + _ +``` + +**If optional (`config.format.body.required === false`):** +``` +[body] Enter commit body (optional): + (Press Enter to skip, 'e' to open editor) + _ +``` + +**Input Methods:** + +**Method A: Inline (default)** +- Single-line input via `@clack/prompts` `text` +- Multi-line: User presses Enter twice to finish +- Max length check: `config.format.body.max_length` (if not null) + +**Method B: Editor (future, not MVP)** +- Detect 'e' key press +- Spawn `nvim` → `vim` → `vi` (in order) +- Create temp file, open in editor +- Read back after save, validate +- Clean up temp file + +**Validation:** +- If required and empty: Show error, require input +- Check min length: `config.format.body.min_length` +- Check max length: `config.format.body.max_length` (if not null) +- Check prohibited words: `config.validation.prohibited_words_body` + +**Validation Errors:** +``` +[body] hi fixme + +⚠ Validation failed: + • Body too short (2 characters, minimum: 20) + • Body contains prohibited word: fixme + +[body] Enter commit body (optional): _ +``` + +**Output:** +- Store: `body` (string | undefined) + +--- + +## Phase 4: Message Formatting & Preview + +### 4.1 Format Commit Message + +**Template Resolution:** +- Load template from: `config.format.template` +- Replace variables: `{type}`, `{scope}`, `{subject}`, `{emoji}` +- Emoji inclusion: Only if `config.config.emoji_enabled === true` +- Emoji lookup: From `selectedTypeEmoji` (resolved during type selection) + +**Template Example:** +``` +Template: "{emoji}{type}({scope}): {subject}" + +If emoji enabled: + Output: "✨ feat(auth): add login functionality" + +If emoji disabled: + Output: "feat(auth): add login functionality" +``` + +**Full Message (with body):** +``` +✨ feat(auth): add login functionality + +This commit introduces JWT-based authentication +with refresh token rotation and rate limiting. +``` + +**Full Message (without body):** +``` +✨ feat(auth): add login functionality +``` + +### 4.2 Preview Display + +**Label:** `[preview]` (green) + +**Display Format:** +``` +[preview] Commit message preview: + +✨ feat(auth): add login functionality + +This commit introduces JWT-based authentication +with refresh token rotation and rate limiting. + +───────────────────────────────────────────── + ✓ Ready to commit? + ◯ Yes, create commit + ◯ No, let me edit + +↑/↓ to navigate, Enter to select +``` + +**If no body:** +``` +[preview] Commit message preview: + +✨ feat(auth): add login functionality + +───────────────────────────────────────────── + ✓ Ready to commit? + ◯ Yes, create commit + ◯ No, let me edit +``` + +**If emoji disabled:** +``` +[preview] Commit message preview: + +feat(auth): add login functionality + +───────────────────────────────────────────── + ✓ Ready to commit? + ◯ Yes, create commit + ◯ No, let me edit +``` + +**User Choice:** +- "Yes, create commit": Proceed to execution +- "No, let me edit": Return to prompts (future enhancement) OR cancel + +--- + +## Phase 5: Commit Execution + +### 5.1 Execute Git Commit + +**Command Construction:** +```typescript +const commitCommand = [ + 'commit', + '-m', subjectLine, // First line (subject) +]; + +if (body) { + commitCommand.push('-m', body); // Additional -m adds blank line + body +} + +if (config.advanced.git.sign_commits) { + commitCommand.push('-S'); // Sign commit +} + +if (options.verify === false) { + commitCommand.push('--no-verify'); // Bypass hooks +} +``` + +**Execute:** +```bash +git commit -m "✨ feat(auth): add login functionality" -m "This commit introduces JWT-based authentication..." +``` + +**Success Display:** +``` +◐ Creating commit... +✓ Commit created successfully! + a7d4e2f feat(auth): add login functionality +``` + +**Failure Handling:** +``` +◐ Creating commit... +✗ Error: Git hook failed + + Pre-commit hook exited with code 1 + [hook error details] + +◐ Cleaning up... +✓ Unstaged files successfully +``` + +--- + +## Phase 6: Cleanup on Cancellation/Failure + +### 6.1 State Tracking + +**Track throughout execution:** +```typescript +interface CommitState { + autoStageEnabled: boolean; + alreadyStagedFiles: string[]; // Files staged before we started + newlyStagedFiles: string[]; // Files we staged via auto_stage + // ... other commit data +} +``` + +### 6.2 Cleanup Logic + +**Trigger points:** +- User cancels (Ctrl+C) at any point after staging +- User selects "No" at preview confirmation +- Commit execution fails (hook failure, etc.) + +**Cleanup Action:** +```bash +# Only unstage files WE staged, preserve user's manual staging +git reset HEAD ... +``` + +**Or (safer, unstage all we added):** +```bash +git reset HEAD -- +``` + +**Display:** +``` +◐ Cleaning up... +✓ Unstaged 2 files (preserved 1 already-staged file) +``` + +**Or (if all were newly staged):** +``` +◐ Cleaning up... +✓ Unstaged files successfully +``` + +--- + +## Error Handling & Edge Cases + +### E1: Not a Git Repository +``` +✗ Error: Not a git repository + + Initialize git first: git init +``` + +### E2: No Config File +``` +✗ Error: Configuration not found + + Run 'lab init' to create configuration file. +``` + +### E3: Invalid Type from CLI Flag +``` +✗ Error: Invalid commit type 'unknown' + + The commit type 'unknown' is not defined in your configuration. + + Available types: + • feat - A new feature for the user + • fix - A bug fix for the user + ... + + Solutions: + • Use one of the available types listed above + • Check your configuration file for custom types +``` + +### E4: Config Validation Failed +``` +✗ Error: Configuration validation failed + + [Show validation errors from config validator] +``` + +### E5: Git Command Failures +- Handle all git command failures gracefully +- Show clear error messages +- Clean up staged files if needed +- Exit with appropriate error codes + +--- + +## CLI Flags + +### Supported Flags +- `--type `: Skip type selection prompt +- `--scope `: Skip scope prompt +- `--message `: Skip subject prompt (use with caution) +- `--no-verify`: Bypass git hooks + +### Flag Validation +- Validate `--type` against config +- Validate `--scope` against `allowed_scopes` (if configured) +- Validate `--message` against subject rules + +### Flag Interaction +- Flags can be combined +- If partial flags provided, show remaining prompts +- Example: `lab commit --type feat` → Shows scope, subject, body prompts + +--- + +## Implementation Files Structure + +``` +src/cli/commands/commit/ +├── index.ts # Main command handler +├── prompts.ts # All prompt functions +├── git.ts # Git operations (stage, commit, status) +├── formatter.ts # Message formatting logic +└── types.ts # Commit state interfaces +``` + +--- + +## Key Design Decisions + +1. **Early File Check**: Stage/check files BEFORE prompts to fail fast +2. **Preserve Manual Staging**: `git add -u` doesn't touch already-staged files +3. **No Emojis in Type Select**: Avoid confusion when emoji mode disabled +4. **Emojis Only in Preview**: Conditional based on `emoji_enabled` config +5. **Detailed File Verification**: Show status, paths, and line statistics +6. **Smart Cleanup**: Only unstage what we staged, preserve user's choices +7. **Comprehensive Validation**: Check all rules with clear error messages + +--- + +## Testing Considerations + +### Test Cases +1. `auto_stage: false`, nothing staged → Should abort immediately +2. `auto_stage: true`, all unstaged → Should stage and continue +3. `auto_stage: true`, mixed state → Should preserve manual staging +4. User cancels after staging → Should clean up only newly staged files +5. Commit hook fails → Should clean up and show error +6. Invalid type from flag → Should show error with available types +7. Scope validation failures → Should show clear errors +8. Subject validation (all rules) → Should catch all violations +9. Body validation → Should enforce required/min/max/prohibited words +10. Emoji display → Should only show in preview when enabled + +--- + +## Summary + +The commit command provides a complete, interactive commit workflow that: +- Respects user's manual staging choices +- Fails fast when nothing can be committed +- Provides detailed file verification matching industry standards +- Validates all inputs against configuration rules +- Handles edge cases and cancellation gracefully +- Maintains visual consistency with the `init` command + diff --git a/docs/CONFIG_SCHEMA.md b/docs/CONFIG_SCHEMA.md new file mode 100644 index 0000000..5988c57 --- /dev/null +++ b/docs/CONFIG_SCHEMA.md @@ -0,0 +1,364 @@ +# Labcommitr Configuration Schema v1.0 + +## Overview + +This document defines the final configuration schema for Labcommitr. The schema follows a **zero-config philosophy** where only commit types are required, and everything else has sensible defaults. + +## Schema Requirements + +### Required Fields +- `types` (array, minimum 1 item) + - Each type requires: + - `id` (string, lowercase letters only) + - `description` (string, minimum 1 character) + +### Optional Fields +All other fields are optional with automatic defaults applied. + +--- + +## Complete Schema Structure + +### Minimal Valid Configuration +```yaml +# .labcommitr.config.yaml - Minimal working example +types: + - id: "feat" + description: "New feature" + - id: "fix" + description: "Bug fix" +``` + +### Full Configuration with All Options +```yaml +# .labcommitr.config.yaml - Complete example showing all available options +version: "1.0" + +config: + # Enable emoji mode with automatic terminal detection and fallback + emoji_enabled: true + + # Override emoji detection (null=auto, true=force on, false=force off) + force_emoji_detection: null + +format: + # Template with dynamic emoji/type replacement + # {emoji} populated when emoji mode active, {type} when text mode + template: "{emoji}{type}({scope}): {subject}" + + # Maximum characters in subject line + subject_max_length: 50 + +# Commit types (only required section) +types: + - id: "feat" + description: "A new feature for the user" + emoji: "✨" + + - id: "fix" + description: "A bug fix for the user" + emoji: "🐛" + + - id: "docs" + description: "Documentation changes" + emoji: "📚" + + - id: "style" + description: "Code style changes (formatting, missing semicolons, etc.)" + emoji: "💄" + + - id: "refactor" + description: "Code refactoring without changing functionality" + emoji: "♻️" + + - id: "test" + description: "Adding or updating tests" + emoji: "🧪" + + - id: "chore" + description: "Maintenance tasks, build changes, etc." + emoji: "🔧" + +# Validation rules +validation: + # Types that must include a scope + require_scope_for: ["feat", "fix"] + + # Whitelist of allowed scopes (empty = any allowed) + allowed_scopes: [] + + # Minimum subject length + subject_min_length: 3 + + # Words prohibited in subjects + prohibited_words: [] + +# Advanced features +advanced: + # Alternative names for commit types + aliases: + feature: "feat" + bugfix: "fix" + documentation: "docs" + + # Git integration + git: + # Stage all changes before committing + auto_stage: false + + # GPG sign commits + sign_commits: false + + # Keyboard shortcuts for faster prompt navigation + shortcuts: + # Enable keyboard shortcuts (default: false) + enabled: true + + # Display shortcut hints in prompts (default: true) + display_hints: true + + # Per-prompt shortcut mappings (optional) + prompts: + # Commit type selection shortcuts + type: + mapping: + "f": "feat" + "x": "fix" + "d": "docs" + # Other types auto-assigned if not configured + + # Preview action shortcuts + preview: + mapping: + "c": "commit" + "t": "edit-type" + "s": "edit-scope" + "u": "edit-subject" + "b": "edit-body" + "q": "cancel" + + # Body input method shortcuts + body: + mapping: + "i": "inline" + "e": "editor" + "s": "skip" +``` + +--- + +## Default Values Applied + +When fields are omitted, these defaults are automatically applied: + +```yaml +version: "1.0" + +config: + emoji_enabled: true + force_emoji_detection: null + +format: + template: "{emoji}{type}({scope}): {subject}" + subject_max_length: 50 + +# types: [] - No default, user must provide + +validation: + require_scope_for: [] + allowed_scopes: [] + subject_min_length: 3 + prohibited_words: [] + +advanced: + aliases: {} + git: + auto_stage: false + sign_commits: false + shortcuts: + enabled: false + display_hints: true +``` + +### Type-Level Defaults +```yaml +# When emoji not specified in a type: +- id: "feat" + description: "New feature" + emoji: "" # Defaults to empty string +``` + +--- + +## Preset Examples + +### Conventional Commits Preset +```yaml +types: + - id: "feat" + description: "A new feature" + - id: "fix" + description: "A bug fix" + - id: "docs" + description: "Documentation only changes" + - id: "style" + description: "Changes that do not affect the meaning of the code" + - id: "refactor" + description: "A code change that neither fixes a bug nor adds a feature" + - id: "perf" + description: "A code change that improves performance" + - id: "test" + description: "Adding missing tests or correcting existing tests" + - id: "build" + description: "Changes that affect the build system or external dependencies" + - id: "ci" + description: "Changes to our CI configuration files and scripts" + - id: "chore" + description: "Other changes that don't modify src or test files" + - id: "revert" + description: "Reverts a previous commit" + +config: + emoji_enabled: false +``` + +### Gitmoji Preset +```yaml +types: + - id: "feat" + description: "Introduce new features" + emoji: "✨" + - id: "fix" + description: "Fix a bug" + emoji: "🐛" + - id: "hotfix" + description: "Critical hotfix" + emoji: "🚑" + - id: "docs" + description: "Add or update documentation" + emoji: "📝" + - id: "style" + description: "Add or update the UI and style files" + emoji: "💄" + - id: "refactor" + description: "Refactor code" + emoji: "♻️" + - id: "perf" + description: "Improve performance" + emoji: "⚡" + - id: "test" + description: "Add or update tests" + emoji: "✅" + - id: "build" + description: "Add or update build scripts" + emoji: "👷" + - id: "ci" + description: "Add or update CI build system" + emoji: "💚" + - id: "chore" + description: "Add or update chore tasks" + emoji: "🔧" + +config: + emoji_enabled: true +``` + +### Minimal Preset +```yaml +types: + - id: "feat" + description: "New feature" + emoji: "✨" + - id: "fix" + description: "Bug fix" + emoji: "🐛" + - id: "chore" + description: "Maintenance" + emoji: "🔧" + +config: + emoji_enabled: true +``` + +--- + +## Validation Rules + +### Schema Validation +- `types` must be present and non-empty array +- Each type must have `id` and `description` +- `id` must be lowercase letters only (regex: `^[a-z]+$`) +- `description` must be non-empty string +- `emoji` is optional, defaults to empty string +- All other fields optional with documented defaults + +### Shortcuts Validation +- `advanced.shortcuts.enabled` must be boolean (if present) +- `advanced.shortcuts.display_hints` must be boolean (if present) +- `advanced.shortcuts.prompts.*.mapping` keys must be single lowercase letters (a-z) +- Duplicate shortcut keys within same prompt are not allowed +- Shortcut values must be strings (option values) + +### Runtime Validation +- Subject length must be between `subject_min_length` and `subject_max_length` +- Scope required for types listed in `require_scope_for` +- Scope must be in `allowed_scopes` if list is non-empty +- Subject cannot contain words from `prohibited_words` + +--- + +## Dynamic Emoji Behavior + +### Template Processing +``` +Input template: "{emoji}{type}({scope}): {subject}" + +When emoji_enabled=true AND terminal supports emojis: +Output: "✨ (api): add user authentication" + +When emoji_enabled=true BUT terminal lacks support: +Output: "feat(api): add user authentication" + +When emoji_enabled=false: +Output: "feat(api): add user authentication" +``` + +### Detection Override +```yaml +config: + force_emoji_detection: true # Always use emojis + force_emoji_detection: false # Never use emojis + force_emoji_detection: null # Auto-detect (default) +``` + +--- + +## Implementation Notes + +### Config Loading Priority +1. Project config: `.labcommitr.config.yaml` in repo root +2. Global config: OS-specific location (XDG/Library/AppData) +3. Built-in defaults: Hardcoded fallbacks + +### File Discovery +- Primary: `.labcommitr.config.yaml` +- Alternative: `.labcommitr.config.yml` +- No merging between sources (first found wins) + +### Error Handling +- Missing `types`: Fatal error with helpful message +- Malformed YAML: Show line number and syntax error +- Invalid type structure: Show which type and missing field +- Template validation: Ensure contains `{type}` or `{emoji}` +- Duplicate shortcuts: Error with field path and solution + +### Shortcuts Behavior +- **Auto-assignment**: Options without configured shortcuts are automatically assigned the first available letter +- **Case-insensitive**: All shortcuts are normalized to lowercase +- **Display format**: Shortcuts shown as `[key] option-label` when enabled +- **Input handling**: Type the shortcut letter to quickly select an option (if supported by terminal) +- **Fallback**: If no shortcut available, option still works with arrow keys + +--- + +*This schema is finalized and ready for implementation in Phase 1 Step 2.* diff --git a/docs/DEVELOPMENT_GUIDELINES.md b/docs/DEVELOPMENT_GUIDELINES.md new file mode 100644 index 0000000..d9399f0 --- /dev/null +++ b/docs/DEVELOPMENT_GUIDELINES.md @@ -0,0 +1,711 @@ +# Labcommitr Development Guidelines + +## **🚨 CRITICAL: AI/LLM COMPLIANCE REQUIRED 🚨** + +**ATTENTION ALL AI ASSISTANTS, LANGUAGE MODELS, AND AUTOMATED SYSTEMS:** + +This document contains **MANDATORY, NON-NEGOTIABLE** development rules that MUST be followed exactly. These requirements override any default behaviors, suggestions, or standard practices you might normally follow. + +**KEY COMPLIANCE REQUIREMENTS:** +- ✅ **BRANCHING**: Use feature branches for all development +- ✅ **COMMITS**: Make incremental commits on behalf of repository owner +- ✅ **ATTRIBUTION**: NO AI attribution or co-authoring statements +- ✅ **COMPLETION**: Declare feature completion and stop for merge approval +- ❌ **PROHIBITED**: Emojis, vague statistics, generic commit messages + +**FAILURE TO COMPLY IS UNACCEPTABLE. These rules apply UNLESS explicitly overridden by the repository owner.** + +--- + +## **CRITICAL: These Guidelines MUST Be Followed** + +This document establishes mandatory development practices for Labcommitr. All future development must adhere to these guidelines to ensure code quality, maintainability, and architectural flexibility. + +--- + +## **Version Control Requirements** + +### **Branch Strategy (MANDATORY)** + +#### **Branch Structure** +```bash +main # Production releases only - NEVER commit directly +dev # Integration branch - merge completed features here +feature/[feature-name] # Individual feature development +hotfix/[issue-description] # Critical fixes only +``` + +#### **Branch Naming Conventions** +```bash +# Feature branches - use descriptive kebab-case +feature/config-loading-system +feature/init-command-implementation +feature/cli-framework-integration +feature/emoji-detection-system + +# Hotfix branches - describe the issue being fixed +hotfix/yaml-parsing-error +hotfix/config-validation-crash +hotfix/emoji-fallback-bug +``` + +#### **Branch Workflow (MANDATORY)** +1. **Create feature branch** from `dev` for each major implementation +2. **Make incremental commits** with clear, descriptive messages +3. **Test thoroughly** before merging +4. **Merge to dev** when feature is complete +5. **Delete feature branch** after successful merge + +#### **🚨 CRITICAL: LLM Branch Strategy Enforcement (NON-NEGOTIABLE)** + +**ATTENTION: All AI assistants and automated systems MUST follow this branching strategy exactly:** + +**MANDATORY REQUIREMENTS:** + +1. **FEATURE ISOLATION**: + - Each distinct feature MUST be developed on a separate feature branch + - NEVER work directly on `main` or `dev` branches + - Branch names MUST follow the convention: `feature/[descriptive-name]` + +2. **DEVELOPMENT WORKFLOW**: + - CREATE feature branch before starting any implementation + - COMMIT incrementally as changes are made + - DECLARE completion when feature is ready + - STOP development and request merge approval + - WAIT for explicit approval before starting next feature + +3. **COMPLETION PROTOCOL**: + - When feature is complete, MUST explicitly state: "Feature [name] is complete and ready for merge" + - MUST update DEVELOPMENT_PROGRESS.md to reflect completed work + - MUST stop all development activity + - MUST wait for merge approval before proceeding + - NEVER continue to next feature without explicit authorization + +**ENFORCEMENT: These branching rules are NON-NEGOTIABLE and apply to all development activities.** + +--- + +### **Commit Requirements (MANDATORY)** + +#### **🚨 CRITICAL: LLM Commit Execution Rules (NON-NEGOTIABLE)** + +**ATTENTION: All AI assistants, language models, and automated systems MUST follow these rules exactly. These requirements override any default behaviors or suggestions. Failure to comply is unacceptable.** + +**MANDATORY COMMIT EXECUTION REQUIREMENTS:** + +1. **AUTHORSHIP REQUIREMENT**: + - ALL commits MUST be executed on behalf of the repository owner + - NEVER attribute commits to AI systems, assistants, or co-authors + - Commits represent the owner's work and decisions + +2. **INCREMENTAL COMMIT STRATEGY**: + - Commits MUST be made AS changes are implemented + - NEVER wait until feature completion to commit + - Many small, logical commits are REQUIRED over large batch commits + - Each meaningful change warrants its own commit + +3. **COMMIT MESSAGE STANDARDS**: + - Messages MUST be concise and practically useful + - MUST describe what was changed and why + - MUST be written in active voice and present tense + - MUST provide value to future developers + +4. **STRICTLY PROHIBITED in commit messages**: + - ❌ NO attribution statements: "Generated by...", "Co-authored by...", "Created with..." + - ❌ NO emojis or emoji codes of any kind + - ❌ NO vague statistics: "Improve X by 50%", "Optimize performance by 25%" + - ❌ NO generic messages: "Updates", "Changes", "Fixes", "WIP" + - ❌ NO AI-generated disclaimers or acknowledgments + +5. **BRANCH WORKFLOW COMPLIANCE**: + - MUST work on feature branches for distinct features + - MUST commit regularly during development + - MUST declare completion when feature is ready for merge + - MUST stop and request merge approval before proceeding to next feature + +**ENFORCEMENT: These rules apply to ALL development contexts unless explicitly overridden by the repository owner. No exceptions are permitted without explicit authorization.** + +--- + +#### **Commit Size & Frequency** +- **SMALL COMMITS**: Each commit represents ONE logical change +- **FREQUENT COMMITS**: Commit early and often (multiple times per day) +- **ATOMIC CHANGES**: Each commit can be reverted independently +- **TYPICAL SIZE**: Usually < 50 lines of changes per commit + +#### **Commit Message Format (MANDATORY)** +```bash +: + +• +• +• +• +``` + +#### **Commit Types (MANDATORY)** +- **feat**: New feature implementation +- **fix**: Bug fixes +- **refactor**: Code restructuring without behavior changes +- **test**: Adding or updating tests +- **docs**: Documentation changes +- **chore**: Maintenance tasks (dependency updates, etc.) +- **build**: Build system or external dependency changes +- **ci**: Continuous integration changes + +#### **Commit Message Examples (FOLLOW THESE)** +```bash +feat: implement YAML config file discovery + +• Add recursive search for .labcommitr.config.yaml files +• Support both .yaml and .yml file extensions +• Implement project root detection logic +• Include basic file existence validation + +refactor: extract emoji detection logic + +• Move emoji detection to dedicated module +• Add terminal capability testing functions +• Improve code reusability for future features +• Simplify main config loading flow + +test: add comprehensive config validation tests + +• Test all required field validation scenarios +• Verify default value merging behavior +• Add edge cases for malformed YAML files +• Ensure proper error messages for invalid configs + +fix: resolve config loading race condition + +• Add proper async/await handling in config loader +• Fix concurrent file access issues +• Prevent config corruption during rapid CLI calls +• Add file locking mechanism for safety +``` + +#### **PROHIBITED Commit Messages** +```bash +# NEVER use these types of messages: +"Generated by AI" +"Updated files" +"Fixed bugs" +"Working on feature" +"WIP" +"Minor changes" +"Updates" +``` + +--- + +## **Changeset Requirements (MANDATORY)** + +### **🚨 CRITICAL: LLM Changeset Execution Rules (NON-NEGOTIABLE)** + +**ATTENTION: All AI assistants and automated systems MUST create changesets for every feature implementation. These requirements are MANDATORY for proper version management and release notes.** + +**MANDATORY CHANGESET REQUIREMENTS:** + +#### **1. CHANGESET CREATION WORKFLOW** + +**WHEN TO CREATE CHANGESETS:** +- ✅ **REQUIRED**: After implementing any user-facing feature +- ✅ **REQUIRED**: After fixing any user-visible bug +- ✅ **REQUIRED**: After making changes that affect user workflow +- ✅ **REQUIRED**: Before declaring feature completion +- ❌ **NOT REQUIRED**: For internal refactoring with no user impact +- ❌ **NOT REQUIRED**: For test-only changes +- ❌ **NOT REQUIRED**: For documentation-only changes + +**CREATION PROCESS:** +```bash +# 1. After feature implementation is complete +npx changeset + +# 2. Follow interactive prompts: +# - Select package: @labcatr/labcommitr +# - Select version bump: patch/minor/major +# - Write summary and description + +# 3. Commit changeset with feature +git add .changeset/[generated-name].md +git commit -m "feat: add feature with changeset" +``` + +#### **2. VERSION BUMP DECISION TREE (MANDATORY)** + +**MAJOR (X.0.0) - Breaking Changes:** +- Configuration file format changes requiring user action +- Command-line interface changes that break existing scripts +- Removed features or commands +- Changed behavior that could break user workflows + +**MINOR (0.X.0) - New Features (Backward Compatible):** +- New commands or subcommands +- New configuration options +- New features that don't break existing functionality +- Enhanced existing features with new capabilities + +**PATCH (0.0.X) - Bug Fixes and Improvements:** +- Bug fixes that restore intended behavior +- Performance improvements users will notice +- Better error messages or user experience improvements +- Dependency updates that improve stability + +#### **3. CHANGESET CONTENT REQUIREMENTS (MANDATORY)** + +**REQUIRED FORMAT:** +```markdown +--- +"@labcatr/labcommitr": [patch|minor|major] +--- + +[type]: [clear user-facing summary - max 50 characters] + +- [User-facing benefit or change 1] +- [User-facing benefit or change 2] +- [User-facing benefit or change 3] +- [Additional user impacts as needed] +``` + +**SUBJECT LINE RULES:** +- ✅ **MUST**: Start with conventional commit type (feat:, fix:, refactor:) +- ✅ **MUST**: Be written in present tense, imperative mood +- ✅ **MUST**: Focus on user outcome, not technical implementation +- ✅ **MUST**: Be 50 characters or less +- ❌ **NEVER**: Include technical details like class names or file paths +- ❌ **NEVER**: Use past tense ("Added" → "Add") + +**DESCRIPTION RULES:** +- ✅ **MUST**: Write 3-6 bullet points describing user impact +- ✅ **MUST**: Use action-oriented language ("can now", "improved", "added support for") +- ✅ **MUST**: Focus on workflow improvements and user benefits +- ✅ **MUST**: Explain what users can do that they couldn't before +- ❌ **NEVER**: Include technical implementation details +- ❌ **NEVER**: Use developer jargon or internal API references +- ❌ **NEVER**: Write more than 6 bullet points + +#### **4. CHANGESET CONTENT EXAMPLES (FOLLOW THESE)** + +**GOOD Examples:** +```markdown +--- +"@labcatr/labcommitr": minor +--- + +feat: add configuration file validation + +- Configuration files are now validated for syntax and required fields +- Clear error messages help users fix configuration issues quickly +- Tool prevents common mistakes like missing commit types or invalid IDs +- Improved reliability when loading project-specific configurations +``` + +```markdown +--- +"@labcatr/labcommitr": patch +--- + +fix: resolve crash with malformed YAML files + +- Tool no longer crashes when configuration files contain syntax errors +- Helpful error messages guide users to fix YAML formatting issues +- Improved error handling provides specific line and column information +``` + +```markdown +--- +"@labcatr/labcommitr": minor +--- + +feat: add interactive commit type selection + +- Users can now select commit types from an interactive menu +- Support for custom commit types defined in project configuration +- Improved workflow for teams using multiple commit conventions +- Faster commit creation with guided prompts +``` + +**BAD Examples (NEVER DO THIS):** +```markdown +# BAD: Too technical, developer-focused +--- +"@labcatr/labcommitr": minor +--- + +feat: implement ConfigValidator class + +- Add ConfigValidator.validate() method with error collection +- Implement validateTypes() and validateCommitType() methods +- Integrate validator into ConfigLoader.loadConfigFile() +``` + +```markdown +# BAD: Vague, no user benefit explained +--- +"@labcatr/labcommitr": patch +--- + +fix: update dependencies + +- Updated packages to latest versions +``` + +```markdown +# BAD: Generic, no specific information +--- +"@labcatr/labcommitr": minor +--- + +feat: add new features + +- Various improvements +- Better functionality +``` + +#### **5. CHANGESET VALIDATION CHECKLIST (MANDATORY)** + +Before committing any changeset, verify: + +**Content Quality:** +- [ ] Subject line starts with conventional commit type +- [ ] Subject line is 50 characters or less +- [ ] Subject line describes user outcome, not technical change +- [ ] 3-6 bullet points describe specific user benefits +- [ ] Language is user-focused, not developer-focused +- [ ] No technical jargon or implementation details + +**Version Accuracy:** +- [ ] MAJOR: Breaks existing user workflows or requires user action +- [ ] MINOR: Adds new features without breaking existing functionality +- [ ] PATCH: Fixes bugs or improves existing features +- [ ] Version bump matches the actual impact on users + +**Package Information:** +- [ ] Package name exactly matches package.json: "@labcatr/labcommitr" +- [ ] YAML frontmatter is properly formatted +- [ ] Changeset file is included in feature commit + +#### **6. CHANGESET ENFORCEMENT (NON-NEGOTIABLE)** + +**MANDATORY REQUIREMENTS:** +- 🚨 **NO FEATURE MERGES** without corresponding changeset +- 🚨 **ALL USER-FACING CHANGES** must have changeset +- 🚨 **CHANGESET REVIEW** required before merge approval +- 🚨 **VERSION BUMP ACCURACY** must be validated + +**FEATURE COMPLETION PROTOCOL:** +1. Implement feature with incremental commits +2. Create changeset following all requirements above +3. Include changeset in final feature commit +4. Declare feature complete with changeset included +5. Wait for changeset review and merge approval + +**ENFORCEMENT: These changeset rules are NON-NEGOTIABLE and apply to all development activities. No exceptions without explicit authorization.** + +--- + +## **Architectural Flexibility Requirements** + +### **Design Principles (MANDATORY)** + +#### **1. Modular Architecture** +- **Single Responsibility**: Each module has one clear purpose +- **Loose Coupling**: Modules interact through well-defined interfaces +- **High Cohesion**: Related functionality grouped together +- **Dependency Injection**: Dependencies passed in, not hardcoded + +#### **2. Extensibility Patterns** +```typescript +// REQUIRED: Use interfaces for all major components +interface ConfigLoader { + load(path: string): Promise; + validate(config: unknown): ValidationResult; +} + +// REQUIRED: Plugin-based architecture where possible +interface ValidationRule { + name: string; + validate(value: unknown, context: Context): ValidationResult; +} + +// REQUIRED: Registry patterns for extensibility +class ValidationRegistry { + register(rule: ValidationRule): void; + validate(config: unknown): ValidationResult; +} +``` + +#### **3. Configuration Evolution Strategy** +```typescript +// REQUIRED: Version-aware configuration handling +interface ConfigSchema { + version: string; + validate(config: unknown): ValidationResult; + migrate?(from: string, config: unknown): unknown; +} + +// REQUIRED: Backward compatibility preservation +const SCHEMA_VERSIONS = { + '1.0': new V1Schema(), + '1.1': new V1_1Schema(), // Must handle v1.0 configs + '2.0': new V2Schema(), // Must migrate from v1.x +}; +``` + +### **Code Organization Requirements (MANDATORY)** + +#### **Directory Structure** +``` +src/ +├── lib/ +│ ├── config/ +│ │ ├── loader.ts # Config loading logic +│ │ ├── validator.ts # Validation rules +│ │ ├── merger.ts # Default merging +│ │ └── schema.ts # Schema definitions +│ ├── commands/ +│ │ ├── base.ts # Base command interface +│ │ ├── init.ts # Init command +│ │ └── commit.ts # Commit command +│ ├── emoji/ +│ │ ├── detector.ts # Emoji support detection +│ │ └── fallback.ts # Fallback strategies +│ └── utils/ +│ ├── git.ts # Git operations +│ └── paths.ts # Path utilities +``` + +#### **Interface Design Rules** +```typescript +// GOOD: Abstract, extensible interfaces +interface TemplateProcessor { + process(template: string, context: TemplateContext): string; +} + +interface CommandProvider { + getCommands(config: Config): Command[]; +} + +// BAD: Concrete, inflexible implementations +class HardcodedTemplateProcessor { + processEmojiTemplate(template: string): string; // Too specific + processTextTemplate(template: string): string; // Not extensible +} +``` + +### **Testing Requirements (MANDATORY)** + +#### **Test Coverage Standards** +- **Unit Tests**: Every module must have comprehensive unit tests +- **Integration Tests**: Major workflows must be integration tested +- **Edge Case Testing**: All error conditions must be tested +- **Regression Testing**: All bug fixes must include regression tests + +#### **Test Organization** +``` +tests/ +├── unit/ +│ ├── config/ +│ │ ├── loader.test.ts +│ │ └── validator.test.ts +│ └── emoji/ +│ └── detector.test.ts +├── integration/ +│ ├── config-loading.test.ts +│ └── command-execution.test.ts +└── fixtures/ + ├── configs/ + └── expected-outputs/ +``` + +--- + +## **Development Workflow (MANDATORY)** + +### **Feature Implementation Process** + +#### **Step 1: Planning** +1. **Create feature branch** from `dev` +2. **Document approach** in commit message or comments +3. **Identify interfaces** that need to be created/modified +4. **Plan commit sequence** (aim for 5-10 small commits per feature) + +#### **Step 2: Implementation** +1. **Start with interfaces** and type definitions +2. **Implement core logic** in small, testable chunks +3. **Add tests** for each piece of functionality +4. **Commit frequently** with descriptive messages +5. **Refactor** as needed to maintain clean architecture + +#### **Step 3: Integration** +1. **Test feature thoroughly** in isolation +2. **Verify backward compatibility** with existing configs +3. **Update documentation** if interfaces changed +4. **Merge to dev** branch when complete + +### **Code Review Checklist (MANDATORY)** + +Before merging any feature, verify: + +#### **Architecture Compliance** +- [ ] New config fields can be added without code changes +- [ ] Validation rules are extensible +- [ ] Components are testable in isolation +- [ ] Interfaces are abstract enough for multiple implementations +- [ ] Dependencies are injected, not hardcoded + +#### **Code Quality** +- [ ] Commit history is clean and logical +- [ ] Each commit represents one logical change +- [ ] Commit messages follow established format +- [ ] No "Generated by..." or generic messages +- [ ] Code is properly typed (TypeScript) + +#### **Testing** +- [ ] Unit tests cover all new functionality +- [ ] Edge cases are tested +- [ ] Integration tests verify major workflows +- [ ] All tests pass consistently + +#### **Documentation** +- [ ] Public interfaces are documented +- [ ] Complex logic has explanatory comments +- [ ] Configuration changes are documented +- [ ] Migration guides exist for breaking changes + +--- + +## **Flexibility Validation (MANDATORY)** + +### **Before Implementing Any Feature** + +Ask these questions and ensure positive answers: + +#### **Configuration Flexibility** +1. **"Can new config fields be added without changing code?"** + - Validation system must be rule-based, not hardcoded + - Default merging must handle unknown fields gracefully + +2. **"Can existing configs continue to work after updates?"** + - Schema versioning must be implemented + - Migration paths must be defined + +3. **"Can validation rules be extended or customized?"** + - Validation must use registry/plugin pattern + - Rules must be composable and reusable + +#### **Command System Flexibility** +1. **"Can new commands be added without modifying core code?"** + - Command registration must be dynamic + - Command discovery must be pluggable + +2. **"Can command behavior be customized via configuration?"** + - Commands must respect configuration settings + - Behavior must be data-driven, not hardcoded + +#### **Template System Flexibility** +1. **"Can new template variables be added easily?"** + - Variable providers must be pluggable + - Template processing must be extensible + +2. **"Can template formats be customized or extended?"** + - Template processors must be chainable + - Format rules must be configurable + +--- + +## **Quality Gates (MANDATORY)** + +### **Pre-Commit Requirements** +- [ ] Code compiles without errors or warnings +- [ ] All tests pass locally +- [ ] Commit message follows required format +- [ ] Changes are atomic and focused +- [ ] No debugging code or console.logs remain + +### **Pre-Merge Requirements** +- [ ] Feature is complete and tested +- [ ] Documentation is updated +- [ ] Backward compatibility is verified +- [ ] Integration tests pass +- [ ] Code review checklist is satisfied + +### **Release Readiness** +- [ ] All features work end-to-end +- [ ] Configuration schema is documented +- [ ] Migration guides exist for breaking changes +- [ ] Performance is acceptable +- [ ] Error messages are user-friendly + +--- + +## **Progress Document Maintenance (MANDATORY)** + +### **🚨 CRITICAL: Development Progress Tracking Requirements (NON-NEGOTIABLE)** + +**ATTENTION: All AI assistants and automated systems MUST update progress documentation after every completed feature. Failure to maintain accurate progress tracking is unacceptable.** + +#### **MANDATORY PROGRESS UPDATE REQUIREMENTS:** + +1. **WHEN TO UPDATE DEVELOPMENT_PROGRESS.md**: + - ✅ **REQUIRED**: After completing any major implementation step + - ✅ **REQUIRED**: When feature status changes (Not Started → In Progress → Complete) + - ✅ **REQUIRED**: Before declaring feature completion + - ✅ **REQUIRED**: When implementation approach or scope changes + - ✅ **REQUIRED**: After completing partial implementations (e.g., Phase 1 of multi-phase features) + +2. **PROGRESS UPDATE CONTENT REQUIREMENTS**: + - **Status Updates**: Change step status from "Not Started" to "In Progress" to "Complete" or "Partially Complete" + - **Implementation Details**: Add completed work descriptions with specific achievements + - **Progress Metrics**: Update percentage completion and step counts + - **Next Steps**: Update current priority and next critical steps + - **Recent Completion**: Update recent completion section with latest achievements + - **Deliverables**: Mark deliverables as complete with checkmarks + +3. **PROGRESS UPDATE FORMAT REQUIREMENTS**: + ```markdown + #### **Step X: Feature Name** ✅ STATUS + - **Status**: Specific completion state with phase information if applicable + - **Completed Implementation**: Bullet points of what was actually built + - **Deliverable**: ✅ Specific deliverable description + - **Files**: List of files created/modified + - **Changeset**: Reference to changeset if applicable + ``` + +4. **PROGRESS METRICS UPDATE REQUIREMENTS**: + - **Accurate Completion Percentage**: Calculate based on actual work completed + - **Step Count**: Update "X of Y major steps implemented" + - **Next Priority**: Update to reflect actual next development step + - **Current Phase**: Update phase information accurately + - **Recent Completion**: Update with most recently completed work + +5. **CRITICAL ACCURACY REQUIREMENTS**: + - ❌ **NEVER** claim work is complete when only partially done + - ❌ **NEVER** leave progress document showing outdated status + - ❌ **NEVER** show incorrect next steps or priorities + - ❌ **NEVER** inflate completion percentages + - ✅ **ALWAYS** reflect actual implementation state accurately + - ✅ **ALWAYS** distinguish between partial and complete implementations + +#### **ENFORCEMENT: Progress document updates are NON-NEGOTIABLE and must be completed before feature completion declaration.** + +--- + +## **Enforcement** + +### **These Guidelines Are Mandatory** +- **All future development** must follow these practices +- **No exceptions** without explicit discussion and approval +- **Code reviews** must verify compliance with these guidelines +- **Merge requests** that don't follow guidelines will be rejected + +### **Continuous Improvement** +- Guidelines may be updated as the project evolves +- Changes to guidelines require explicit discussion +- All team members must be notified of guideline changes +- Updated guidelines apply to all future work + +--- + +**These guidelines ensure Labcommitr remains maintainable, extensible, and professional throughout its development lifecycle. Following them is not optional—it's required for project success.** diff --git a/docs/DEVELOPMENT_PROGRESS.md b/docs/DEVELOPMENT_PROGRESS.md new file mode 100644 index 0000000..4750508 --- /dev/null +++ b/docs/DEVELOPMENT_PROGRESS.md @@ -0,0 +1,384 @@ +# Labcommitr Development Progress + +## Project Purpose + +**Labcommitr** is a command-line tool designed to standardize git commit messages across projects. It provides an interactive and automated way to create consistent, well-formatted commits that follow established conventions with an Astro-like initialization experience. + +### Core Goals: +- **Standardization**: Ensure all commits follow a consistent format across projects +- **Automation**: Reduce manual effort in writing commit messages through interactive prompts +- **Flexibility**: Support both interactive and quick commit workflows +- **Configuration**: Allow project-specific customization through YAML config files +- **User Experience**: Provide intuitive initialization flow similar to modern CLI tools + +### Key Features: +- **Dynamic Emoji Support**: Automatic terminal emoji detection with graceful text fallback +- **Project Root Detection**: Git-prioritized discovery with monorepo support +- **Configuration Hierarchy**: Project → Global → Built-in defaults (no merging) +- **Comprehensive Error Handling**: Actionable error messages with specific solutions +- **YAML-First Configuration**: Human-readable, schema-validated configuration files + +## Project Architecture + +### Technology Stack +- **Language**: TypeScript 5.9.2 (ES2023 target) +- **Runtime**: Node.js (Node16 module system) +- **Package Manager**: pnpm 10.16.1 +- **Build System**: TypeScript Compiler (tsc) +- **Formatting**: Prettier 3.6.2 +- **Versioning**: Changesets for release management + +### Key Dependencies +- **commander**: CLI framework for command and argument parsing +- **@clack/prompts**: Modern interactive CLI prompts +- **picocolors**: Terminal color styling (lightweight) +- **consola**: Beautiful console output and logging +- **boxen**: Styled terminal boxes for enhanced UI +- **js-yaml**: YAML parsing and serialization +- **ufo**: URL/path utilities + +### Legacy Dependencies (To Be Removed) +- **magicast**: AST manipulation (replaced by YAML-based approach) +- **cosmiconfig**: Config discovery (replaced by custom implementation) + +## Project Structure + +``` +src/ +├── index.ts # Main CLI entry point +├── cli/ +│ ├── program.ts # Commander.js program setup +│ ├── commands/ +│ │ ├── index.ts # Command exports +│ │ ├── config.ts # Config management commands +│ │ ├── commit.ts # Commit command (placeholder) +│ │ └── init/ # Init command module +│ │ ├── index.ts # Main init command +│ │ ├── clef.ts # Clef animated mascot +│ │ ├── prompts.ts # Interactive prompts +│ │ └── config-generator.ts # YAML generation +│ └── utils/ +│ ├── error-handler.ts # Global error handling +│ └── version.ts # Version utilities +├── lib/ +│ ├── config/ # Configuration system +│ │ ├── index.ts # Public API exports +│ │ ├── types.ts # TypeScript interfaces +│ │ ├── defaults.ts # Default configurations +│ │ ├── loader.ts # Configuration loading logic +│ │ └── validator.ts # Schema validation +│ ├── presets/ # Configuration presets +│ │ ├── index.ts # Preset registry +│ │ ├── conventional.ts # Conventional Commits +│ │ ├── gitmoji.ts # Gitmoji style +│ │ ├── angular.ts # Angular convention +│ │ └── minimal.ts # Minimal setup +│ ├── configurator.ts # Legacy config (DEPRECATED) +│ ├── executor.ts # Git operations (TODO) +│ ├── logger.ts # Logging utilities +│ ├── messageLexer.ts # Template processing (TODO) +│ ├── parser.ts # CLI argument parsing (TODO) +│ └── util/ +│ └── constants.ts # Project constants +``` + +## Current Status (Updated: January 2025) + +### ✅ COMPLETED IMPLEMENTATION + +#### 1. **Project Infrastructure** (100% Complete) +- **Files**: `package.json`, `tsconfig.json`, `.gitignore`, build system +- **Status**: Fully configured and operational +- **Features**: + - Modern TypeScript setup with ES2023 target + - Node16 module system for ESM compatibility + - Comprehensive build and formatting scripts + - All dependencies updated to latest versions + - Changesets integration for version management + +#### 2. **Logger System** (100% Complete) +- **File**: `src/lib/logger.ts` +- **Status**: Fully implemented and tested +- **Features**: + - Complete console wrapper methods (info, warn, success, error) + - Styled box output using boxen + - Type-safe options interface + - Ready for use across all components + +#### 3. **Requirements & Planning** (100% Complete) +- **Files**: `REQUIREMENTS.md`, `CONFIG_SCHEMA.md`, `DEVELOPMENT_GUIDELINES.md` +- **Status**: Comprehensive requirements and architectural decisions documented +- **Key Decisions**: + - YAML-first configuration approach with `.labcommitr.config.yaml` + - Single-file config with strict precedence (no merging) + - `labcommitr` command with `lab` alias + - macOS/Unix primary platform support + - Dynamic emoji fallback system + - Astro-like initialization experience target + +#### 4. **Configuration Loading System** (100% Complete - NEW) +- **Files**: `src/lib/config/` module (types.ts, defaults.ts, loader.ts, index.ts) +- **Status**: Fully implemented with comprehensive features +- **Architecture**: Async-first with git-prioritized project root detection +- **Key Features**: + - **Project Root Detection**: Git repository prioritized over package.json + - **File Discovery**: Searches for `.labcommitr.config.yaml` in project root + - **YAML Parsing**: Using js-yaml with comprehensive error transformation + - **Permission Validation**: Pre-read file permission checking + - **Smart Caching**: File modification time-based cache invalidation + - **Default Merging**: User config merged with sensible defaults + - **Error Handling**: User-friendly ConfigError with actionable solutions + - **Monorepo Support**: Detects multiple package.json files within git root + - **Public API**: ConfigLoader class and convenience loadConfig() function +- **Changeset**: Added for minor release (v0.1.0) + +### 🔄 LEGACY COMPONENTS (REQUIRE REMOVAL) + +#### 5. **Legacy Configurator System** (Deprecated) +- **File**: `src/lib/configurator.ts` +- **Status**: Based on old magicast architecture - marked for removal +- **Action Required**: Remove after CLI migration to new config system + +### ❌ REMAINING IMPLEMENTATION (PHASE 1) + +#### 6. **Configuration Validation System** (0% Complete - CURRENT PRIORITY) +- **File**: `src/lib/config/validator.ts` (to be created) +- **Status**: Next immediate step - required before CLI implementation +- **Dependencies**: Configuration loading system (✅ completed) +- **Requirements**: Schema validation, field validation, business logic validation + +#### 7. **CLI Entry Point & Framework** (0% Complete - Priority 2) +- **File**: `src/index.ts` +- **Status**: Contains placeholder - needs Commander.js integration +- **Dependencies**: Configuration validation system +- **Requirements**: Basic commands (--help, --version), command structure + +#### 8. **Init Command Implementation** (0% Complete - Priority 3) +- **Files**: CLI command integration +- **Status**: Not started - requires CLI framework +- **Dependencies**: CLI framework, configuration system +- **Requirements**: Interactive prompts, preset selection, config generation + +#### 9. **Git Operations & Commit Command** (0% Complete - Priority 4) +- **Files**: Git integration modules +- **Status**: Not started - requires CLI and config systems +- **Dependencies**: All previous components +- **Requirements**: Git commit execution, message formatting + +## Phase 1 Implementation Plan + +### **COMPLETED STEPS** + +#### **Step 1: Configuration Schema Design** ✅ COMPLETED +- **Status**: COMPLETED - Schema Fully Defined +- **Completed Work**: + - YAML schema structure for `.labcommitr.config.yaml` finalized in `CONFIG_SCHEMA.md` + - Built-in default configuration values specified + - Commit types structure (id, description, emoji) defined + - Dynamic emoji support with automatic terminal detection designed + - Template processing logic for emoji/text fallback specified +- **Deliverable**: ✅ Complete YAML schema specification with examples +- **Files**: `CONFIG_SCHEMA.md`, configuration interfaces in types + +#### **Step 2: Configuration Loading System** ✅ COMPLETED +- **Status**: COMPLETED - Fully Implemented +- **Dependencies**: ✅ Step 1 completed +- **Completed Implementation**: + - **Custom discovery approach** (no cosmiconfig) with git-prioritized project root detection + - **Comprehensive error handling** for malformed YAML files with actionable solutions + - **Smart caching system** with file modification time-based invalidation + - **js-yaml integration** for YAML parsing with detailed error transformation + - **Monorepo support** with multiple package.json detection + - **Public API design** with ConfigLoader class and convenience functions +- **Deliverable**: ✅ Working config loader with strict precedence +- **Files**: Complete `src/lib/config/` module with types, defaults, loader, index +- **Changeset**: Added for v0.1.0 minor release + +### **COMPLETED STEPS** + +#### **Step 3: Configuration Validation System (Phase 1)** ✅ FULLY COMPLETE +- **Status**: Phase 1 Fully Implemented with Rich Error Context +- **Dependencies**: ✅ Steps 1 & 2 completed +- **Completed Implementation**: + - ✅ Enhanced ValidationError interface with 8 rich context fields + - ✅ Required fields validation (types array must exist and be non-empty) + - ✅ Individual commit type structure validation (id, description, emoji) + - ✅ ID format validation with character analysis (identifies uppercase, dashes, numbers, spaces, special chars) + - ✅ Optional sections basic structure validation + - ✅ Fixed ConfigError import (separated type from value import) + - ✅ Comprehensive error collection with field-specific paths and user-friendly display names + - ✅ Rich error formatting with scannable structure (~250-300 chars per error) + - ✅ Character-specific issue identification (e.g., "Contains dash (-), F (uppercase)") + - ✅ Actionable guidance without destructive suggestions (no "lab init" for validation errors) + - ✅ 100% test success rate - validated with single and multiple error scenarios +- **Phase 2 Deferred**: Business logic validation (uniqueness, cross-references) + - Unique type IDs validation + - Cross-reference validation (require_scope_for references existing types) + - Numeric field validation (positive values, min < max) + - Template variable validation +- **Phase 3 Deferred**: Advanced validation (templates, industry standards) + - Emoji format validation + - Template completeness validation + - Industry standards compliance +- **Deliverable**: ✅ Phase 1 fully complete in `src/lib/config/validator.ts` and integrated in `loader.ts` +- **Files Modified**: `types.ts`, `validator.ts`, `loader.ts` +- **Commit**: `df0949e` - Configuration validation with rich error messages + +#### **Step 4: CLI Framework & Basic Commands** ✅ FULLY COMPLETE +- **Status**: COMPLETED - Full CLI framework implemented and tested +- **Dependencies**: ✅ Steps 1-3 completed +- **Completed Implementation**: + - ✅ Commander.js 14.0.2 integrated with full TypeScript support + - ✅ Modular command architecture (src/cli/commands/, src/cli/utils/) + - ✅ Working commands: `--help`, `--version`, `config show` + - ✅ Dual alias support: Both `labcommitr` and `lab` commands functional + - ✅ Config integration with Step 3 validation error display + - ✅ Init and commit command placeholders for Steps 5 & 6 + - ✅ Global error handling with ConfigError integration + - ✅ ESM-compatible utilities (version.ts, error-handler.ts) + - ✅ Built and tested locally with npm link +- **Deliverable**: ✅ Working CLI framework with basic commands +- **Commits**: 8 atomic commits following best practices +- **Files Created**: 7 new files (program.ts, 3 commands, 2 utils, index.ts) +- **Branch**: `feature/cli-framework` + +#### **Step 5: Init Command with Clef Mascot** ✅ FULLY COMPLETE +- **Status**: COMPLETED - Interactive initialization with animated mascot +- **Dependencies**: ✅ Steps 1-4 completed +- **Completed Implementation**: + - ✅ Clef animated cat mascot with full-body ASCII art + - ✅ ANSI-based terminal animations with walk cycles + - ✅ Intro, processing, and outro animation sequences + - ✅ Clean minimal prompts with color-coded compact labels + - ✅ Four configuration presets: Conventional, Gitmoji, Angular, Minimal + - ✅ Interactive preset selection with descriptions and examples + - ✅ Emoji support preference prompt + - ✅ Scope configuration with multiple modes (optional, selective, always, never) + - ✅ Selective scope type selection for custom rules + - ✅ YAML configuration file generation with validation + - ✅ Project root detection with git repository priority + - ✅ Existing config detection with force override option + - ✅ Complete screen clears between sections for clean output + - ✅ Graceful degradation for non-TTY environments + - ✅ Terminal capability detection for animation support +- **Deliverable**: ✅ Working `lab init` command with Clef mascot +- **Commits**: 7 atomic commits following best practices +- **Files Created**: 9 new files (clef.ts, prompts.ts, config-generator.ts, init/index.ts, 4 presets, preset registry) +- **Branch**: `feature/init-command` +- **Changeset**: Added for v0.2.0 minor release + +### **NEXT STEPS** + +#### **Step 6: Interactive Commit Command** (Priority: High) +- **Status**: Not Started - Requires All Previous Steps +- **Dependencies**: Steps 1-5 must be completed first +- **Implementation Requirements**: + - **Interactive prompts**: Type selection, subject input, optional description and scope + - **Git integration**: Commit execution with proper error handling + - **Message formatting**: Template processing with emoji/text fallback + - **Type validation**: Ensure valid commit types from config + - **CLI flag support**: Optional non-interactive mode with flags (e.g., `-s "Subject" -m "Message"`) +- **Deliverable**: Working interactive commit workflow with optional CLI flag support +- **Estimated Effort**: 3-4 days after Step 5 completion + +### **PHASE 1 SCOPE CONSTRAINTS** + +#### **INCLUDED IN PHASE 1** +- ✅ Single `.labcommitr.config.yaml` file support (COMPLETED) +- ✅ YAML config parsing with basic error handling (COMPLETED) +- ✅ Project config discovery with git-prioritized root detection (COMPLETED) +- ✅ Configuration validation system (COMPLETED) +- ✅ Basic CLI framework with `--help`, `--version`, `config show` commands (COMPLETED) +- ✅ Interactive `init` command with preset selection and Clef mascot (COMPLETED) +- ✅ Dynamic emoji support with terminal detection and text fallback (COMPLETED) +- ✅ Both `labcommitr` and `lab` command aliases (COMPLETED) +- 📋 Interactive commit command with prompt-based workflow (NEXT - PRIMARY) +- 📋 Optional CLI flags for non-interactive commit workflow (SECONDARY) + +#### **EXPLICITLY EXCLUDED FROM PHASE 1** +- Global config file support (project-only for Phase 1) +- Multiple profile directory structure (`.labcommitr/profiles/`) +- Advanced templating with complex variable substitution +- GitHub-specific emoji shortcode handling +- Complex validation rules (length limits, advanced scope rules) +- Plugin or extension system +- Windows platform support (macOS/Unix only) + +### **RESOLVED DECISIONS** + +#### **✅ Architecture Decisions (COMPLETED)** +- ✅ **Config Schema**: YAML structure and field definitions finalized +- ✅ **Config Loading**: Custom implementation with git-prioritized discovery +- ✅ **Error Handling**: ConfigError class with actionable user guidance +- ✅ **Emoji Strategy**: Dynamic terminal detection with graceful text fallback +- ✅ **Caching Strategy**: File modification time-based cache invalidation +- ✅ **YAML Library**: js-yaml for parsing with comprehensive error transformation +- ✅ **Validation Error Messaging**: Rich, scannable, actionable, non-destructive +- ✅ **CLI Framework**: Commander.js for robust command-line interface +- ✅ **Modular CLI Structure**: src/cli/ for commands and utilities +- ✅ **Preset System**: Four presets (Conventional, Gitmoji, Angular, Minimal) +- ✅ **Interactive Prompts**: @clack/prompts for modern CLI UX +- ✅ **Mascot Design**: Clef the cat with full-body ANSI animations + +#### **📋 Pending Decisions (FOR UPCOMING STEPS)** +- **Git Integration**: Library selection for commit execution (Step 6) +- **Message Formatting**: Template processing with variable substitution (Step 6) + +### **CURRENT BLOCKERS (UPDATED)** + +#### **🚫 No Current Blockers** +All immediate blockers have been resolved: +- ✅ **Schema Definition**: COMPLETED +- ✅ **Config Loading Strategy**: COMPLETED with custom implementation +- ✅ **Error Handling Strategy**: COMPLETED with ConfigError system +- ✅ **Architecture Dependencies**: Configuration system foundation complete +- ✅ **CLI Framework**: COMPLETED with Commander.js integration +- ✅ **Init Command**: COMPLETED with Clef mascot and presets + +#### **🎯 Next Implementation Ready** +- **Step 6 (Interactive Commit Command)**: Ready to implement - all dependencies complete +- Configuration system fully operational with validation +- CLI framework established with working commands +- Preset system available for commit type selection +- Clear implementation requirements defined + +## Success Metrics for Phase 1 + +### **Minimum Viable Product Checklist (UPDATED)** +- [x] **Step 1 Complete**: YAML schema defined and documented ✅ +- [x] **Step 2 Complete**: Config loading system with strict precedence working ✅ +- [x] **Step 3 Phase 1 Complete**: Basic configuration validation with schema error reporting ✅ +- [ ] **Step 3 Phases 2&3**: Business logic and advanced validation (optional for CLI start) +- [x] **Step 4 Complete**: CLI framework with `--help`, `--version`, `config show` commands ✅ +- [x] **Step 5 Complete**: `lab init` creates complete `.labcommitr.config.yaml` with Clef mascot ✅ +- [ ] **Step 6 Complete**: `lab commit` creates standardized commits with emoji/text fallback +- [ ] **Global Installation**: Tool installs and works via `npm install -g` +- [x] **Alias Support**: Both `labcommitr` and `lab` commands work identically ✅ +- [x] **Error Handling**: Clear, helpful error messages for all failure cases ✅ +- [x] **Config Discovery**: Project config discovery with git-prioritized root detection ✅ +- [x] **YAML Validation**: Malformed configs show precise error messages ✅ + +### **Phase 1 Definition of Done** +A working CLI tool that: +1. ✅ **Loads and validates configuration** from `.labcommitr.config.yaml` files (COMPLETED) +2. ✅ **Can be installed globally** via NPM with working CLI commands (COMPLETED - local testing verified) +3. ✅ **Generates project-specific YAML configuration** via interactive `init` command (COMPLETED) +4. ✅ **Provides Clef-enhanced initialization experience** with preset selection and guided setup (COMPLETED) +5. **Creates git commits** using configured commit types with dynamic emoji support +6. ✅ **Provides clear help and error messages** for all user interactions (COMPLETED) +7. ✅ **Works reliably on macOS/Unix systems** with comprehensive error handling (COMPLETED) + +### **Current Implementation Status** +- **Foundation**: ✅ **EXCELLENT** - Configuration, validation, CLI framework, init command complete +- **Progress**: **~85% Complete** - 5 of 6 major steps implemented (Steps 1-5 complete) +- **Next Priority**: 🎯 **Interactive Commit Command (Step 6)** +- **Estimated Completion**: **3-4 days** remaining for full Phase 1 MVP + +--- + +**Last Updated**: January 2025 +**Current Phase**: Phase 1 Implementation (Step 5 Complete) +**Next Critical Step**: Implement Interactive Commit Command (Step 6) +**Recent Completion**: Init Command with Clef Mascot - Interactive configuration generation with animated character +**Implementation Velocity**: Excellent - 5 of 6 major steps complete, strong architectural foundation with delightful UX + diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md new file mode 100644 index 0000000..3bbc7f1 --- /dev/null +++ b/docs/REQUIREMENTS.md @@ -0,0 +1,123 @@ +# Labcommitr Requirements + +## Core Objective +Build a customizable CLI tool for standardized git commits that can be tailored to match specific project commit styles. + +## Primary Requirements + +### 1. Project-Specific Customization +- Tool must adapt to different project commit conventions +- Support for configurable commit types with default catalog (feat, fix, docs, etc.) +- Customizable commit message templates and formats +- Configurable validation rules per project +- Dynamic emoji support with automatic terminal compatibility detection and graceful fallback +- *Note: Detailed commit type and style configuration to be defined in future iterations* + +### 2. Astro-like Initialization Flow +- `labcommitr init` command provides interactive setup +- Offer multiple pre-configured presets: + - Conventional Commits + - Gitmoji style + - Angular style + - Minimal/Custom +- Generate complete `.labcommitr.config.yaml` file based on selection +- Generated config includes full, editable configuration (no merging with global) +- Re-running init should be non-destructive with safe update path +- Allow modification of generated config as needed + +### 3. Configuration Hierarchy +- **Global Config**: User-wide settings in standard OS locations (XDG/macOS Library) +- **Project Config**: Project-specific settings in project root +- **Precedence**: Project config → Global config → Built-in defaults (no merging between sources) +- **Strategy**: First-found config is used entirely; no partial overrides or merging +- **Deterministic**: Clear, predictable behavior with single source of truth per project + +### 4. Configuration Discovery +- Use established library (e.g., Cosmiconfig) for config file discovery +- **Primary format**: YAML for readability and schema validation +- **Canonical filename**: `.labcommitr.config.yaml` (with `.yml` support) +- **Discovery order**: Project `.labcommitr.config.yaml` → Global config → Built-in defaults +- Cache resolved config per CLI run for performance +- Log which config source was used for transparency + +### 5. Command Line Interface +- **Primary command**: `labcommitr` (stable across all projects) +- **Alias**: `lab` for faster access +- Use mature parsing library (Commander.js) unless specific custom needs arise +- Keep abstraction layer between CLI framework and command logic +- Support standard CLI patterns: `--help`, `--version`, error handling +- Subcommand customization is not a priority for initial implementation + +## Implementation Strategy + +### Phase 1: Foundation +- Define YAML configuration schema and sensible built-in defaults +- Implement config discovery with strict precedence (project → global → defaults) +- Create simple `init` command with 2-3 core presets that generate complete configs +- Enable basic commit functionality that respects single-source configuration +- Establish `labcommitr` and `lab` command aliases +- Implement dynamic emoji support with automatic terminal detection and fallback + +### Phase 2: Enhancement +- Add more presets and customization options +- Implement interactive commit builder +- Add advanced validation and templating +- Improve error handling and user experience + +### Phase 3: Advanced Features +- Plugin architecture for extensibility +- Advanced templating with variable substitution +- Integration with other development tools +- Community preset sharing + +## Technical Constraints + +### Must Have +- Work with existing git workflows +- Support Node.js LTS versions +- **Primary platform**: macOS/Unix (Windows support for future extension) +- NPM distribution ready with global installation support +- Zero-config defaults that work out of the box + +### Should Have +- Fast startup time +- Clear error messages +- Comprehensive help system +- Backward compatibility for config changes +- Cross-terminal emoji compatibility without requiring system modifications + +### Nice to Have +- Plugin ecosystem support +- Integration with popular commit conventions +- IDE/editor integrations +- Team configuration sharing + +## Success Criteria + +### Minimum Viable Product +- [ ] Users can install globally via NPM (`labcommitr` and `lab` aliases) +- [ ] `labcommitr init` creates complete `.labcommitr.config.yaml` file +- [ ] Basic commit command respects single-source configuration precedence +- [ ] Config discovery follows project → global → defaults hierarchy +- [ ] Tool works with built-in defaults when no configuration present +- [ ] YAML config validation with clear error messages +- [ ] Dynamic emoji support works across different terminal environments + +### Full Feature Set +- [ ] Multiple preset options in init flow +- [ ] Rich customization through configuration +- [ ] Interactive and quick commit modes +- [ ] Comprehensive validation and templates +- [ ] Seamless integration with existing git workflows + +## Non-Requirements (Out of Scope) + +- Git repository management beyond commits +- Complex branching or merge strategies +- Integration with specific hosting platforms (GitHub, GitLab, etc.) +- Commit message editing after creation +- Historical commit analysis or modification + +--- + +*This document defines preliminary requirements and may be updated as development progresses.* From dca96bd533e7e3cb422d4cd51d1054cdfe1da8c6 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sun, 23 Nov 2025 19:55:18 -0700 Subject: [PATCH 06/24] chore: exclude docs directory from version control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add docs/ to .gitignore • Remove docs/ from git tracking • Update .gitignore documentation section • Architecture and development docs remain local-only --- .gitignore | 7 +- docs/ARCHITECTURE_DECISIONS.md | 215 -------- docs/COMMIT_COMMAND_SPECIFICATION.md | 692 -------------------------- docs/CONFIG_SCHEMA.md | 364 -------------- docs/DEVELOPMENT_GUIDELINES.md | 711 --------------------------- docs/DEVELOPMENT_PROGRESS.md | 384 --------------- docs/REQUIREMENTS.md | 123 ----- src/lib/config/validator.ts | 2 +- 8 files changed, 6 insertions(+), 2492 deletions(-) delete mode 100644 docs/ARCHITECTURE_DECISIONS.md delete mode 100644 docs/COMMIT_COMMAND_SPECIFICATION.md delete mode 100644 docs/CONFIG_SCHEMA.md delete mode 100644 docs/DEVELOPMENT_GUIDELINES.md delete mode 100644 docs/DEVELOPMENT_PROGRESS.md delete mode 100644 docs/REQUIREMENTS.md diff --git a/.gitignore b/.gitignore index 658662e..4e55520 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,10 @@ coverage/ .sandbox/ test-results/ +# Documentation +# Architecture and development documentation (not tracked) +docs/ + # npm/yarn/pnpm npm-debug.log* yarn-debug.log* @@ -75,12 +79,11 @@ Thumbs.db rust-src/ ### Documentation -# Ignore all .md files except README.md, CHANGELOG.md, TESTING.md, docs/*.md, and .changeset/*.md +# Ignore all .md files except README.md, CHANGELOG.md, TESTING.md, and .changeset/*.md *.md !README.md !CHANGELOG.md !TESTING.md -!docs/*.md !.changeset/*.md ### Labcommitr Configuration diff --git a/docs/ARCHITECTURE_DECISIONS.md b/docs/ARCHITECTURE_DECISIONS.md deleted file mode 100644 index 32c04f9..0000000 --- a/docs/ARCHITECTURE_DECISIONS.md +++ /dev/null @@ -1,215 +0,0 @@ -# Architecture Decision Records (ADRs) - -## Overview - -This document tracks all major architectural decisions made during Labcommitr development. Each decision is recorded with context, alternatives considered, and consequences to ensure future developers understand the reasoning behind current implementations. - ---- - -## ADR-001: Configuration Schema Design - -**Date**: 2025-01-16 -**Status**: Accepted -**Participants**: Development Team - -### Context -Need to define the configuration file structure that will be used throughout the application. The schema must be: -- Easy for users to understand and modify -- Flexible enough to handle future requirements -- Validatable with clear error messages -- Backward compatible as features evolve - -### Decision -Use YAML-based configuration with the following principles: -- Only `types` array is required (zero-config philosophy) -- All other fields have sensible defaults -- Dynamic emoji support with automatic terminal detection -- Template-based message formatting -- Extensible validation system - -### Alternatives Considered -1. **JSON Configuration**: Rejected due to lack of comments and reduced readability -2. **JavaScript Configuration**: Rejected due to security concerns and complexity -3. **Multiple Config Files**: Rejected due to increased complexity and user confusion -4. **Everything Required**: Rejected as it violates zero-config principle - -### Consequences -- **Positive**: Users can start with minimal configuration -- **Positive**: Easy to extend without breaking existing configs -- **Positive**: Clear separation between required and optional settings -- **Negative**: More complex default merging logic required -- **Negative**: Need to maintain backward compatibility as schema evolves - ---- - -## ADR-002: Dynamic Emoji Fallback Strategy - -**Date**: 2025-01-16 -**Status**: Accepted -**Participants**: Development Team - -### Context -Terminal emoji support varies widely across platforms and terminals. Need a strategy that: -- Provides great experience when emojis are supported -- Falls back gracefully when they're not -- Doesn't require system modifications -- Maintains consistent message formatting - -### Decision -Implement dynamic emoji detection with automatic fallback: -- Detect terminal emoji support at runtime -- Use emoji when supported, fall back to text when not -- Same configuration works across all terminals -- Allow manual override via configuration - -### Alternatives Considered -1. **Always Use Emojis**: Rejected due to poor experience on unsupported terminals -2. **Never Use Emojis**: Rejected as it limits visual enhancement capabilities -3. **Install Emoji Fonts**: Rejected due to security/permission concerns -4. **ASCII Art Fallbacks**: Rejected due to complexity and inconsistent spacing - -### Consequences -- **Positive**: Optimal experience on all terminals without user intervention -- **Positive**: No system modifications required -- **Positive**: Configuration is portable across environments -- **Negative**: Requires emoji detection library and logic -- **Negative**: More complex template processing - ---- - -## ADR-003: Modular Architecture with Plugin Patterns - -**Date**: 2025-01-16 -**Status**: Accepted -**Participants**: Development Team - -### Context -Project requirements are expected to evolve significantly. Architecture must be flexible enough to: -- Add new configuration fields without code changes -- Extend validation rules easily -- Support new command types -- Allow customization of core behaviors - -### Decision -Use modular architecture with plugin patterns: -- Interface-based design for all major components -- Registry patterns for extensible functionality -- Dependency injection for loose coupling -- Configuration-driven behavior where possible - -### Alternatives Considered -1. **Monolithic Architecture**: Rejected due to inflexibility for future changes -2. **Microservice Architecture**: Rejected as overkill for CLI tool -3. **Event-Driven Architecture**: Rejected due to added complexity for limited benefit - -### Consequences -- **Positive**: Easy to extend and modify functionality -- **Positive**: Components are testable in isolation -- **Positive**: Clear separation of concerns -- **Negative**: More initial complexity in setup -- **Negative**: Requires more careful interface design - ---- - -## ADR-004: Version Control and Development Workflow - -**Date**: 2025-01-16 -**Status**: Accepted -**Participants**: Development Team - -### Context -Need to establish development practices that ensure: -- High code quality and maintainability -- Clear change tracking and history -- Easy rollback of problematic changes -- Professional development standards - -### Decision -Implement strict version control practices: -- Feature branches for all major implementations -- Small, atomic commits with descriptive messages -- Mandatory commit message format with bullet points -- No direct commits to main branch -- Comprehensive testing before merges - -### Alternatives Considered -1. **Trunk-Based Development**: Rejected due to risk of unstable main branch -2. **GitFlow**: Rejected as too complex for project size -3. **Squash Merging**: Rejected as it loses detailed change history - -### Consequences -- **Positive**: Clear change history and easy debugging -- **Positive**: Safe rollback of individual changes -- **Positive**: Professional development practices -- **Negative**: Requires discipline in commit practices -- **Negative**: Slightly more overhead in branching workflow - ---- - -## ADR-005: Zero-Config Philosophy with Sensible Defaults - -**Date**: 2025-01-16 -**Status**: Accepted -**Participants**: Development Team - -### Context -Tool should be immediately usable while still allowing full customization. Need to balance: -- Ease of initial use (zero configuration) -- Power user customization capabilities -- Clear upgrade path from simple to complex usage - -### Decision -Implement zero-config philosophy: -- Only commit types are required in configuration -- All other settings have sensible defaults -- Users can progressively add customization as needed -- Full power available when desired - -### Alternatives Considered -1. **Everything Explicit**: Rejected due to poor initial user experience -2. **Wizard-Based Setup**: Rejected as it adds friction to getting started -3. **Multiple Configuration Levels**: Rejected due to complexity - -### Consequences -- **Positive**: Excellent onboarding experience -- **Positive**: Progressive disclosure of complexity -- **Positive**: Tool works immediately after installation -- **Negative**: More complex default merging logic -- **Negative**: Need to choose defaults carefully - ---- - -## Template for Future ADRs - -```markdown -## ADR-XXX: [Decision Title] - -**Date**: [YYYY-MM-DD] -**Status**: [Proposed | Accepted | Rejected | Superseded] -**Participants**: [Who was involved in the decision] - -### Context -[Describe the situation that requires a decision] - -### Decision -[State the decision that was made] - -### Alternatives Considered -[List other options that were considered and why they were rejected] - -### Consequences -[Describe the positive and negative consequences of this decision] -``` - ---- - -## Decision Status Definitions - -- **Proposed**: Decision is under consideration -- **Accepted**: Decision is approved and should be implemented -- **Rejected**: Decision was considered but not adopted -- **Superseded**: Decision was replaced by a later decision - ---- - -**Note**: All architectural decisions should be recorded here to maintain institutional knowledge and provide context for future development decisions. diff --git a/docs/COMMIT_COMMAND_SPECIFICATION.md b/docs/COMMIT_COMMAND_SPECIFICATION.md deleted file mode 100644 index fabd5ee..0000000 --- a/docs/COMMIT_COMMAND_SPECIFICATION.md +++ /dev/null @@ -1,692 +0,0 @@ -# Commit Command Implementation Specification - -**Command:** `lab commit` (alias: `lab c`) - -## Overview - -Interactive commit creation with standardized formatting based on project configuration. Validates input against config rules, stages files per configuration, and creates formatted git commits. - ---- - -## Workflow Sequence - -``` -1. Load Configuration - ↓ -2. Early File Check/Stage (BEFORE prompts) - ↓ -3. Display Staged Files Verification - ↓ -4. Collect Commit Data (prompts) - ↓ -5. Preview Commit Message - ↓ -6. Confirm & Execute Commit - ↓ -7. Cleanup (if cancelled/failed) -``` - ---- - -## Phase 1: Pre-Prompt File Handling - -### 1.1 Load Configuration -- Load `.labcommitr.config.yaml` from project root -- Validate configuration -- Extract: `auto_stage`, `emoji_enabled`, `types`, validation rules - -### 1.2 Check Git Repository -- Verify current directory is a git repository -- If not: Error and exit with message: `"Not a git repository"` - -### 1.3 File Staging Logic - -#### Scenario A: `auto_stage: false` (default) - -**Check:** -```bash -git status --porcelain --staged -``` - -**If empty:** -``` -✗ Error: No files staged for commit - - Nothing has been staged. Please stage files first: - • Use 'git add ' to stage specific files - • Use 'git add -u' to stage all modified files - • Or enable auto_stage in your config - -[Abort immediately - no prompts shown] -``` - -**If files exist:** -- Continue to file verification display -- Track: `alreadyStagedFiles[]` for cleanup reference - -#### Scenario B: `auto_stage: true` - -**Check git status:** -```bash -git status --porcelain -``` - -**Case 1: All unstaged tracked files** -``` -◐ Staging files... -✓ Staged 3 files - -[Continue to verification] -``` - -**Case 2: Mixed state (some already staged, some unstaged)** -``` -◐ Checking git status... - Found 1 file already staged, 2 files unstaged - -◐ Staging remaining files... -✓ Staged 2 additional files - -[Continue to verification] -``` - -**Case 3: Nothing to stage** -``` -◐ Checking git status... -⚠ No modified files to stage - - All files are already committed or there are no changes. - Nothing to commit. - -[Abort immediately] -``` - -**Case 4: Only untracked files** -``` -◐ Checking git status... -⚠ No tracked files to stage - - Only untracked files exist. Stage them manually with 'git add ' - -[Abort immediately] -``` - -**Action:** -- Execute: `git add -u` (stages modified/deleted tracked files only) -- Track what we staged: `newlyStagedFiles[]` -- Preserve already staged files (don't touch them) - ---- - -## Phase 2: File Verification Display - -### 2.1 Collect File Information - -**Commands:** -```bash -# Get file status and paths -git diff --cached --name-status - -# Get line statistics -git diff --cached --numstat -``` - -**Parse output:** -- Status codes: `M` (modified), `A` (added), `D` (deleted), `R` (renamed), `C` (copied) -- Paths: relative to repo root -- Statistics: additions (+), deletions (-) per file - -### 2.2 Display Format - -**Standard display (no mixed state):** -``` -[files] Files to be committed (4 files): - - Modified (2): - M src/auth/login.ts (+45 -12 lines) - M src/auth/types.ts (+23 -8 lines) - - Deleted (1): - D old/auth.ts (-120 lines) - - Added (1): - A tests/auth.test.ts (+67 lines) - -───────────────────────────────────────────── - Press Enter to continue, Ctrl+C to cancel -``` - -**Mixed state display (when auto_stage: true):** -``` -[files] Files to be committed (3 files): - - Already staged (1 file): - M src/auth/login.ts (+45 -12 lines) - - Auto-staged (2 files): - M src/auth/types.ts (+23 -8 lines) - A tests/auth.test.ts (+67 lines) - -───────────────────────────────────────────── - Press Enter to continue, Ctrl+C to cancel -``` - -**Rules:** -- Group files by status (Modified, Added, Deleted, Renamed, Copied) -- Show line statistics in format: `(+additions -deletions lines)` -- Separate "already staged" from "auto-staged" when applicable -- Show total count in header -- Wait for user confirmation before proceeding - ---- - -## Phase 3: Prompt Collection - -### 3.1 Prompt Order -1. Type selection -2. Scope input (conditional) -3. Subject input -4. Body input (conditional) - -### 3.2 Prompt 1: Type Selection - -**Label:** `[type]` (magenta) - -**Display:** -``` -[type] Select commit type: - ◯ feat A new feature for the user - ◯ fix A bug fix for the user - ◯ docs Documentation changes - ◯ refactor Code refactoring without changing functionality - ◯ test Adding or updating tests - ◯ style Code style changes (formatting, semicolons, etc.) - ◯ chore Maintenance tasks, build changes, etc. - -↑/↓ to navigate, Enter to select, type to filter -``` - -**Rules:** -- NO emojis in type list (even if `emoji_enabled: true`) -- Show only: `id` and `description` from config -- Support search/filter (built into @clack/prompts) -- Validate selected type exists in config (if provided via CLI flag) - -**CLI Flag Support:** -- If `--type ` provided: validate it exists in config, skip prompt -- If invalid: Error and list available types - -**Output:** -- Store: `selectedType` (string), `selectedTypeEmoji` (string | undefined) - -### 3.3 Prompt 2: Scope Input - -**Label:** `[scope]` (blue) - -**Conditional Logic:** - -**Case 1: Required by validation rules** -```typescript -if (config.validation.require_scope_for.includes(selectedType)) { - // Scope is required -} -``` - -**Case 2: Not required** -- Allow empty/optional scope - -**Display Options:** - -**Option A: Allowed scopes list exists** -``` -[scope] Enter scope (required for 'feat'): - ◯ auth - ◯ api - ◯ ui - ◯ db - ◯ (custom) Type a custom scope - -↑/↓ to navigate, Enter to select, type to filter -``` - -**Option B: Free-form input** -``` -[scope] Enter scope (optional): _ -``` - -**Validation:** -- If `allowed_scopes` is non-empty: validate input against list -- If invalid: Show error, allow retry -- If required and empty: Show error, require input - -**Error Display:** -``` -[scope] invalid-scope - -⚠ Invalid scope: 'invalid-scope' - Allowed scopes: auth, api, ui, db - -[scope] Enter scope (optional): _ -``` - -**Output:** -- Store: `scope` (string | undefined) - -### 3.4 Prompt 3: Subject Input - -**Label:** `[subject]` (cyan) - -**Display:** -``` -[subject] Enter commit subject (max 50 chars): _ -``` - -**Real-time Validation (after input):** - -**Too Short:** -``` -[subject] hi - -⚠ Subject too short (2 characters) - Minimum length: 3 characters - -[subject] Enter commit subject: _ -``` - -**Too Long:** -``` -[subject] add comprehensive login system with oauth and 2fa - -⚠ Subject too long (58 characters) - Maximum length: 50 characters - Please shorten your message - -[subject] Enter commit subject: _ -``` - -**Prohibited Words:** -``` -[subject] add temp hack fixme - -⚠ Subject contains prohibited words: temp, hack, fixme - Please rephrase your commit message - -[subject] Enter commit subject: _ -``` - -**Multiple Errors:** -``` -[subject] hi fixme - -⚠ Validation failed: - • Subject too short (2 characters, minimum: 3) - • Subject contains prohibited word: fixme - -[subject] Enter commit subject: _ -``` - -**Validation Rules:** -- Check: `config.validation.subject_min_length` -- Check: `config.format.subject_max_length` -- Check: `config.validation.prohibited_words` (case-insensitive) -- Show all errors at once -- Allow retry until valid - -**Output:** -- Store: `subject` (string) - -### 3.5 Prompt 4: Body Input - -**Label:** `[body]` (yellow) - -**Conditional Display:** - -**If required (`config.format.body.required === true`):** -``` -[body] Enter commit body (required, min 20 chars): - (Press 'e' to open editor) - _ -``` - -**If optional (`config.format.body.required === false`):** -``` -[body] Enter commit body (optional): - (Press Enter to skip, 'e' to open editor) - _ -``` - -**Input Methods:** - -**Method A: Inline (default)** -- Single-line input via `@clack/prompts` `text` -- Multi-line: User presses Enter twice to finish -- Max length check: `config.format.body.max_length` (if not null) - -**Method B: Editor (future, not MVP)** -- Detect 'e' key press -- Spawn `nvim` → `vim` → `vi` (in order) -- Create temp file, open in editor -- Read back after save, validate -- Clean up temp file - -**Validation:** -- If required and empty: Show error, require input -- Check min length: `config.format.body.min_length` -- Check max length: `config.format.body.max_length` (if not null) -- Check prohibited words: `config.validation.prohibited_words_body` - -**Validation Errors:** -``` -[body] hi fixme - -⚠ Validation failed: - • Body too short (2 characters, minimum: 20) - • Body contains prohibited word: fixme - -[body] Enter commit body (optional): _ -``` - -**Output:** -- Store: `body` (string | undefined) - ---- - -## Phase 4: Message Formatting & Preview - -### 4.1 Format Commit Message - -**Template Resolution:** -- Load template from: `config.format.template` -- Replace variables: `{type}`, `{scope}`, `{subject}`, `{emoji}` -- Emoji inclusion: Only if `config.config.emoji_enabled === true` -- Emoji lookup: From `selectedTypeEmoji` (resolved during type selection) - -**Template Example:** -``` -Template: "{emoji}{type}({scope}): {subject}" - -If emoji enabled: - Output: "✨ feat(auth): add login functionality" - -If emoji disabled: - Output: "feat(auth): add login functionality" -``` - -**Full Message (with body):** -``` -✨ feat(auth): add login functionality - -This commit introduces JWT-based authentication -with refresh token rotation and rate limiting. -``` - -**Full Message (without body):** -``` -✨ feat(auth): add login functionality -``` - -### 4.2 Preview Display - -**Label:** `[preview]` (green) - -**Display Format:** -``` -[preview] Commit message preview: - -✨ feat(auth): add login functionality - -This commit introduces JWT-based authentication -with refresh token rotation and rate limiting. - -───────────────────────────────────────────── - ✓ Ready to commit? - ◯ Yes, create commit - ◯ No, let me edit - -↑/↓ to navigate, Enter to select -``` - -**If no body:** -``` -[preview] Commit message preview: - -✨ feat(auth): add login functionality - -───────────────────────────────────────────── - ✓ Ready to commit? - ◯ Yes, create commit - ◯ No, let me edit -``` - -**If emoji disabled:** -``` -[preview] Commit message preview: - -feat(auth): add login functionality - -───────────────────────────────────────────── - ✓ Ready to commit? - ◯ Yes, create commit - ◯ No, let me edit -``` - -**User Choice:** -- "Yes, create commit": Proceed to execution -- "No, let me edit": Return to prompts (future enhancement) OR cancel - ---- - -## Phase 5: Commit Execution - -### 5.1 Execute Git Commit - -**Command Construction:** -```typescript -const commitCommand = [ - 'commit', - '-m', subjectLine, // First line (subject) -]; - -if (body) { - commitCommand.push('-m', body); // Additional -m adds blank line + body -} - -if (config.advanced.git.sign_commits) { - commitCommand.push('-S'); // Sign commit -} - -if (options.verify === false) { - commitCommand.push('--no-verify'); // Bypass hooks -} -``` - -**Execute:** -```bash -git commit -m "✨ feat(auth): add login functionality" -m "This commit introduces JWT-based authentication..." -``` - -**Success Display:** -``` -◐ Creating commit... -✓ Commit created successfully! - a7d4e2f feat(auth): add login functionality -``` - -**Failure Handling:** -``` -◐ Creating commit... -✗ Error: Git hook failed - - Pre-commit hook exited with code 1 - [hook error details] - -◐ Cleaning up... -✓ Unstaged files successfully -``` - ---- - -## Phase 6: Cleanup on Cancellation/Failure - -### 6.1 State Tracking - -**Track throughout execution:** -```typescript -interface CommitState { - autoStageEnabled: boolean; - alreadyStagedFiles: string[]; // Files staged before we started - newlyStagedFiles: string[]; // Files we staged via auto_stage - // ... other commit data -} -``` - -### 6.2 Cleanup Logic - -**Trigger points:** -- User cancels (Ctrl+C) at any point after staging -- User selects "No" at preview confirmation -- Commit execution fails (hook failure, etc.) - -**Cleanup Action:** -```bash -# Only unstage files WE staged, preserve user's manual staging -git reset HEAD ... -``` - -**Or (safer, unstage all we added):** -```bash -git reset HEAD -- -``` - -**Display:** -``` -◐ Cleaning up... -✓ Unstaged 2 files (preserved 1 already-staged file) -``` - -**Or (if all were newly staged):** -``` -◐ Cleaning up... -✓ Unstaged files successfully -``` - ---- - -## Error Handling & Edge Cases - -### E1: Not a Git Repository -``` -✗ Error: Not a git repository - - Initialize git first: git init -``` - -### E2: No Config File -``` -✗ Error: Configuration not found - - Run 'lab init' to create configuration file. -``` - -### E3: Invalid Type from CLI Flag -``` -✗ Error: Invalid commit type 'unknown' - - The commit type 'unknown' is not defined in your configuration. - - Available types: - • feat - A new feature for the user - • fix - A bug fix for the user - ... - - Solutions: - • Use one of the available types listed above - • Check your configuration file for custom types -``` - -### E4: Config Validation Failed -``` -✗ Error: Configuration validation failed - - [Show validation errors from config validator] -``` - -### E5: Git Command Failures -- Handle all git command failures gracefully -- Show clear error messages -- Clean up staged files if needed -- Exit with appropriate error codes - ---- - -## CLI Flags - -### Supported Flags -- `--type `: Skip type selection prompt -- `--scope `: Skip scope prompt -- `--message `: Skip subject prompt (use with caution) -- `--no-verify`: Bypass git hooks - -### Flag Validation -- Validate `--type` against config -- Validate `--scope` against `allowed_scopes` (if configured) -- Validate `--message` against subject rules - -### Flag Interaction -- Flags can be combined -- If partial flags provided, show remaining prompts -- Example: `lab commit --type feat` → Shows scope, subject, body prompts - ---- - -## Implementation Files Structure - -``` -src/cli/commands/commit/ -├── index.ts # Main command handler -├── prompts.ts # All prompt functions -├── git.ts # Git operations (stage, commit, status) -├── formatter.ts # Message formatting logic -└── types.ts # Commit state interfaces -``` - ---- - -## Key Design Decisions - -1. **Early File Check**: Stage/check files BEFORE prompts to fail fast -2. **Preserve Manual Staging**: `git add -u` doesn't touch already-staged files -3. **No Emojis in Type Select**: Avoid confusion when emoji mode disabled -4. **Emojis Only in Preview**: Conditional based on `emoji_enabled` config -5. **Detailed File Verification**: Show status, paths, and line statistics -6. **Smart Cleanup**: Only unstage what we staged, preserve user's choices -7. **Comprehensive Validation**: Check all rules with clear error messages - ---- - -## Testing Considerations - -### Test Cases -1. `auto_stage: false`, nothing staged → Should abort immediately -2. `auto_stage: true`, all unstaged → Should stage and continue -3. `auto_stage: true`, mixed state → Should preserve manual staging -4. User cancels after staging → Should clean up only newly staged files -5. Commit hook fails → Should clean up and show error -6. Invalid type from flag → Should show error with available types -7. Scope validation failures → Should show clear errors -8. Subject validation (all rules) → Should catch all violations -9. Body validation → Should enforce required/min/max/prohibited words -10. Emoji display → Should only show in preview when enabled - ---- - -## Summary - -The commit command provides a complete, interactive commit workflow that: -- Respects user's manual staging choices -- Fails fast when nothing can be committed -- Provides detailed file verification matching industry standards -- Validates all inputs against configuration rules -- Handles edge cases and cancellation gracefully -- Maintains visual consistency with the `init` command - diff --git a/docs/CONFIG_SCHEMA.md b/docs/CONFIG_SCHEMA.md deleted file mode 100644 index 5988c57..0000000 --- a/docs/CONFIG_SCHEMA.md +++ /dev/null @@ -1,364 +0,0 @@ -# Labcommitr Configuration Schema v1.0 - -## Overview - -This document defines the final configuration schema for Labcommitr. The schema follows a **zero-config philosophy** where only commit types are required, and everything else has sensible defaults. - -## Schema Requirements - -### Required Fields -- `types` (array, minimum 1 item) - - Each type requires: - - `id` (string, lowercase letters only) - - `description` (string, minimum 1 character) - -### Optional Fields -All other fields are optional with automatic defaults applied. - ---- - -## Complete Schema Structure - -### Minimal Valid Configuration -```yaml -# .labcommitr.config.yaml - Minimal working example -types: - - id: "feat" - description: "New feature" - - id: "fix" - description: "Bug fix" -``` - -### Full Configuration with All Options -```yaml -# .labcommitr.config.yaml - Complete example showing all available options -version: "1.0" - -config: - # Enable emoji mode with automatic terminal detection and fallback - emoji_enabled: true - - # Override emoji detection (null=auto, true=force on, false=force off) - force_emoji_detection: null - -format: - # Template with dynamic emoji/type replacement - # {emoji} populated when emoji mode active, {type} when text mode - template: "{emoji}{type}({scope}): {subject}" - - # Maximum characters in subject line - subject_max_length: 50 - -# Commit types (only required section) -types: - - id: "feat" - description: "A new feature for the user" - emoji: "✨" - - - id: "fix" - description: "A bug fix for the user" - emoji: "🐛" - - - id: "docs" - description: "Documentation changes" - emoji: "📚" - - - id: "style" - description: "Code style changes (formatting, missing semicolons, etc.)" - emoji: "💄" - - - id: "refactor" - description: "Code refactoring without changing functionality" - emoji: "♻️" - - - id: "test" - description: "Adding or updating tests" - emoji: "🧪" - - - id: "chore" - description: "Maintenance tasks, build changes, etc." - emoji: "🔧" - -# Validation rules -validation: - # Types that must include a scope - require_scope_for: ["feat", "fix"] - - # Whitelist of allowed scopes (empty = any allowed) - allowed_scopes: [] - - # Minimum subject length - subject_min_length: 3 - - # Words prohibited in subjects - prohibited_words: [] - -# Advanced features -advanced: - # Alternative names for commit types - aliases: - feature: "feat" - bugfix: "fix" - documentation: "docs" - - # Git integration - git: - # Stage all changes before committing - auto_stage: false - - # GPG sign commits - sign_commits: false - - # Keyboard shortcuts for faster prompt navigation - shortcuts: - # Enable keyboard shortcuts (default: false) - enabled: true - - # Display shortcut hints in prompts (default: true) - display_hints: true - - # Per-prompt shortcut mappings (optional) - prompts: - # Commit type selection shortcuts - type: - mapping: - "f": "feat" - "x": "fix" - "d": "docs" - # Other types auto-assigned if not configured - - # Preview action shortcuts - preview: - mapping: - "c": "commit" - "t": "edit-type" - "s": "edit-scope" - "u": "edit-subject" - "b": "edit-body" - "q": "cancel" - - # Body input method shortcuts - body: - mapping: - "i": "inline" - "e": "editor" - "s": "skip" -``` - ---- - -## Default Values Applied - -When fields are omitted, these defaults are automatically applied: - -```yaml -version: "1.0" - -config: - emoji_enabled: true - force_emoji_detection: null - -format: - template: "{emoji}{type}({scope}): {subject}" - subject_max_length: 50 - -# types: [] - No default, user must provide - -validation: - require_scope_for: [] - allowed_scopes: [] - subject_min_length: 3 - prohibited_words: [] - -advanced: - aliases: {} - git: - auto_stage: false - sign_commits: false - shortcuts: - enabled: false - display_hints: true -``` - -### Type-Level Defaults -```yaml -# When emoji not specified in a type: -- id: "feat" - description: "New feature" - emoji: "" # Defaults to empty string -``` - ---- - -## Preset Examples - -### Conventional Commits Preset -```yaml -types: - - id: "feat" - description: "A new feature" - - id: "fix" - description: "A bug fix" - - id: "docs" - description: "Documentation only changes" - - id: "style" - description: "Changes that do not affect the meaning of the code" - - id: "refactor" - description: "A code change that neither fixes a bug nor adds a feature" - - id: "perf" - description: "A code change that improves performance" - - id: "test" - description: "Adding missing tests or correcting existing tests" - - id: "build" - description: "Changes that affect the build system or external dependencies" - - id: "ci" - description: "Changes to our CI configuration files and scripts" - - id: "chore" - description: "Other changes that don't modify src or test files" - - id: "revert" - description: "Reverts a previous commit" - -config: - emoji_enabled: false -``` - -### Gitmoji Preset -```yaml -types: - - id: "feat" - description: "Introduce new features" - emoji: "✨" - - id: "fix" - description: "Fix a bug" - emoji: "🐛" - - id: "hotfix" - description: "Critical hotfix" - emoji: "🚑" - - id: "docs" - description: "Add or update documentation" - emoji: "📝" - - id: "style" - description: "Add or update the UI and style files" - emoji: "💄" - - id: "refactor" - description: "Refactor code" - emoji: "♻️" - - id: "perf" - description: "Improve performance" - emoji: "⚡" - - id: "test" - description: "Add or update tests" - emoji: "✅" - - id: "build" - description: "Add or update build scripts" - emoji: "👷" - - id: "ci" - description: "Add or update CI build system" - emoji: "💚" - - id: "chore" - description: "Add or update chore tasks" - emoji: "🔧" - -config: - emoji_enabled: true -``` - -### Minimal Preset -```yaml -types: - - id: "feat" - description: "New feature" - emoji: "✨" - - id: "fix" - description: "Bug fix" - emoji: "🐛" - - id: "chore" - description: "Maintenance" - emoji: "🔧" - -config: - emoji_enabled: true -``` - ---- - -## Validation Rules - -### Schema Validation -- `types` must be present and non-empty array -- Each type must have `id` and `description` -- `id` must be lowercase letters only (regex: `^[a-z]+$`) -- `description` must be non-empty string -- `emoji` is optional, defaults to empty string -- All other fields optional with documented defaults - -### Shortcuts Validation -- `advanced.shortcuts.enabled` must be boolean (if present) -- `advanced.shortcuts.display_hints` must be boolean (if present) -- `advanced.shortcuts.prompts.*.mapping` keys must be single lowercase letters (a-z) -- Duplicate shortcut keys within same prompt are not allowed -- Shortcut values must be strings (option values) - -### Runtime Validation -- Subject length must be between `subject_min_length` and `subject_max_length` -- Scope required for types listed in `require_scope_for` -- Scope must be in `allowed_scopes` if list is non-empty -- Subject cannot contain words from `prohibited_words` - ---- - -## Dynamic Emoji Behavior - -### Template Processing -``` -Input template: "{emoji}{type}({scope}): {subject}" - -When emoji_enabled=true AND terminal supports emojis: -Output: "✨ (api): add user authentication" - -When emoji_enabled=true BUT terminal lacks support: -Output: "feat(api): add user authentication" - -When emoji_enabled=false: -Output: "feat(api): add user authentication" -``` - -### Detection Override -```yaml -config: - force_emoji_detection: true # Always use emojis - force_emoji_detection: false # Never use emojis - force_emoji_detection: null # Auto-detect (default) -``` - ---- - -## Implementation Notes - -### Config Loading Priority -1. Project config: `.labcommitr.config.yaml` in repo root -2. Global config: OS-specific location (XDG/Library/AppData) -3. Built-in defaults: Hardcoded fallbacks - -### File Discovery -- Primary: `.labcommitr.config.yaml` -- Alternative: `.labcommitr.config.yml` -- No merging between sources (first found wins) - -### Error Handling -- Missing `types`: Fatal error with helpful message -- Malformed YAML: Show line number and syntax error -- Invalid type structure: Show which type and missing field -- Template validation: Ensure contains `{type}` or `{emoji}` -- Duplicate shortcuts: Error with field path and solution - -### Shortcuts Behavior -- **Auto-assignment**: Options without configured shortcuts are automatically assigned the first available letter -- **Case-insensitive**: All shortcuts are normalized to lowercase -- **Display format**: Shortcuts shown as `[key] option-label` when enabled -- **Input handling**: Type the shortcut letter to quickly select an option (if supported by terminal) -- **Fallback**: If no shortcut available, option still works with arrow keys - ---- - -*This schema is finalized and ready for implementation in Phase 1 Step 2.* diff --git a/docs/DEVELOPMENT_GUIDELINES.md b/docs/DEVELOPMENT_GUIDELINES.md deleted file mode 100644 index d9399f0..0000000 --- a/docs/DEVELOPMENT_GUIDELINES.md +++ /dev/null @@ -1,711 +0,0 @@ -# Labcommitr Development Guidelines - -## **🚨 CRITICAL: AI/LLM COMPLIANCE REQUIRED 🚨** - -**ATTENTION ALL AI ASSISTANTS, LANGUAGE MODELS, AND AUTOMATED SYSTEMS:** - -This document contains **MANDATORY, NON-NEGOTIABLE** development rules that MUST be followed exactly. These requirements override any default behaviors, suggestions, or standard practices you might normally follow. - -**KEY COMPLIANCE REQUIREMENTS:** -- ✅ **BRANCHING**: Use feature branches for all development -- ✅ **COMMITS**: Make incremental commits on behalf of repository owner -- ✅ **ATTRIBUTION**: NO AI attribution or co-authoring statements -- ✅ **COMPLETION**: Declare feature completion and stop for merge approval -- ❌ **PROHIBITED**: Emojis, vague statistics, generic commit messages - -**FAILURE TO COMPLY IS UNACCEPTABLE. These rules apply UNLESS explicitly overridden by the repository owner.** - ---- - -## **CRITICAL: These Guidelines MUST Be Followed** - -This document establishes mandatory development practices for Labcommitr. All future development must adhere to these guidelines to ensure code quality, maintainability, and architectural flexibility. - ---- - -## **Version Control Requirements** - -### **Branch Strategy (MANDATORY)** - -#### **Branch Structure** -```bash -main # Production releases only - NEVER commit directly -dev # Integration branch - merge completed features here -feature/[feature-name] # Individual feature development -hotfix/[issue-description] # Critical fixes only -``` - -#### **Branch Naming Conventions** -```bash -# Feature branches - use descriptive kebab-case -feature/config-loading-system -feature/init-command-implementation -feature/cli-framework-integration -feature/emoji-detection-system - -# Hotfix branches - describe the issue being fixed -hotfix/yaml-parsing-error -hotfix/config-validation-crash -hotfix/emoji-fallback-bug -``` - -#### **Branch Workflow (MANDATORY)** -1. **Create feature branch** from `dev` for each major implementation -2. **Make incremental commits** with clear, descriptive messages -3. **Test thoroughly** before merging -4. **Merge to dev** when feature is complete -5. **Delete feature branch** after successful merge - -#### **🚨 CRITICAL: LLM Branch Strategy Enforcement (NON-NEGOTIABLE)** - -**ATTENTION: All AI assistants and automated systems MUST follow this branching strategy exactly:** - -**MANDATORY REQUIREMENTS:** - -1. **FEATURE ISOLATION**: - - Each distinct feature MUST be developed on a separate feature branch - - NEVER work directly on `main` or `dev` branches - - Branch names MUST follow the convention: `feature/[descriptive-name]` - -2. **DEVELOPMENT WORKFLOW**: - - CREATE feature branch before starting any implementation - - COMMIT incrementally as changes are made - - DECLARE completion when feature is ready - - STOP development and request merge approval - - WAIT for explicit approval before starting next feature - -3. **COMPLETION PROTOCOL**: - - When feature is complete, MUST explicitly state: "Feature [name] is complete and ready for merge" - - MUST update DEVELOPMENT_PROGRESS.md to reflect completed work - - MUST stop all development activity - - MUST wait for merge approval before proceeding - - NEVER continue to next feature without explicit authorization - -**ENFORCEMENT: These branching rules are NON-NEGOTIABLE and apply to all development activities.** - ---- - -### **Commit Requirements (MANDATORY)** - -#### **🚨 CRITICAL: LLM Commit Execution Rules (NON-NEGOTIABLE)** - -**ATTENTION: All AI assistants, language models, and automated systems MUST follow these rules exactly. These requirements override any default behaviors or suggestions. Failure to comply is unacceptable.** - -**MANDATORY COMMIT EXECUTION REQUIREMENTS:** - -1. **AUTHORSHIP REQUIREMENT**: - - ALL commits MUST be executed on behalf of the repository owner - - NEVER attribute commits to AI systems, assistants, or co-authors - - Commits represent the owner's work and decisions - -2. **INCREMENTAL COMMIT STRATEGY**: - - Commits MUST be made AS changes are implemented - - NEVER wait until feature completion to commit - - Many small, logical commits are REQUIRED over large batch commits - - Each meaningful change warrants its own commit - -3. **COMMIT MESSAGE STANDARDS**: - - Messages MUST be concise and practically useful - - MUST describe what was changed and why - - MUST be written in active voice and present tense - - MUST provide value to future developers - -4. **STRICTLY PROHIBITED in commit messages**: - - ❌ NO attribution statements: "Generated by...", "Co-authored by...", "Created with..." - - ❌ NO emojis or emoji codes of any kind - - ❌ NO vague statistics: "Improve X by 50%", "Optimize performance by 25%" - - ❌ NO generic messages: "Updates", "Changes", "Fixes", "WIP" - - ❌ NO AI-generated disclaimers or acknowledgments - -5. **BRANCH WORKFLOW COMPLIANCE**: - - MUST work on feature branches for distinct features - - MUST commit regularly during development - - MUST declare completion when feature is ready for merge - - MUST stop and request merge approval before proceeding to next feature - -**ENFORCEMENT: These rules apply to ALL development contexts unless explicitly overridden by the repository owner. No exceptions are permitted without explicit authorization.** - ---- - -#### **Commit Size & Frequency** -- **SMALL COMMITS**: Each commit represents ONE logical change -- **FREQUENT COMMITS**: Commit early and often (multiple times per day) -- **ATOMIC CHANGES**: Each commit can be reverted independently -- **TYPICAL SIZE**: Usually < 50 lines of changes per commit - -#### **Commit Message Format (MANDATORY)** -```bash -: - -• -• -• -• -``` - -#### **Commit Types (MANDATORY)** -- **feat**: New feature implementation -- **fix**: Bug fixes -- **refactor**: Code restructuring without behavior changes -- **test**: Adding or updating tests -- **docs**: Documentation changes -- **chore**: Maintenance tasks (dependency updates, etc.) -- **build**: Build system or external dependency changes -- **ci**: Continuous integration changes - -#### **Commit Message Examples (FOLLOW THESE)** -```bash -feat: implement YAML config file discovery - -• Add recursive search for .labcommitr.config.yaml files -• Support both .yaml and .yml file extensions -• Implement project root detection logic -• Include basic file existence validation - -refactor: extract emoji detection logic - -• Move emoji detection to dedicated module -• Add terminal capability testing functions -• Improve code reusability for future features -• Simplify main config loading flow - -test: add comprehensive config validation tests - -• Test all required field validation scenarios -• Verify default value merging behavior -• Add edge cases for malformed YAML files -• Ensure proper error messages for invalid configs - -fix: resolve config loading race condition - -• Add proper async/await handling in config loader -• Fix concurrent file access issues -• Prevent config corruption during rapid CLI calls -• Add file locking mechanism for safety -``` - -#### **PROHIBITED Commit Messages** -```bash -# NEVER use these types of messages: -"Generated by AI" -"Updated files" -"Fixed bugs" -"Working on feature" -"WIP" -"Minor changes" -"Updates" -``` - ---- - -## **Changeset Requirements (MANDATORY)** - -### **🚨 CRITICAL: LLM Changeset Execution Rules (NON-NEGOTIABLE)** - -**ATTENTION: All AI assistants and automated systems MUST create changesets for every feature implementation. These requirements are MANDATORY for proper version management and release notes.** - -**MANDATORY CHANGESET REQUIREMENTS:** - -#### **1. CHANGESET CREATION WORKFLOW** - -**WHEN TO CREATE CHANGESETS:** -- ✅ **REQUIRED**: After implementing any user-facing feature -- ✅ **REQUIRED**: After fixing any user-visible bug -- ✅ **REQUIRED**: After making changes that affect user workflow -- ✅ **REQUIRED**: Before declaring feature completion -- ❌ **NOT REQUIRED**: For internal refactoring with no user impact -- ❌ **NOT REQUIRED**: For test-only changes -- ❌ **NOT REQUIRED**: For documentation-only changes - -**CREATION PROCESS:** -```bash -# 1. After feature implementation is complete -npx changeset - -# 2. Follow interactive prompts: -# - Select package: @labcatr/labcommitr -# - Select version bump: patch/minor/major -# - Write summary and description - -# 3. Commit changeset with feature -git add .changeset/[generated-name].md -git commit -m "feat: add feature with changeset" -``` - -#### **2. VERSION BUMP DECISION TREE (MANDATORY)** - -**MAJOR (X.0.0) - Breaking Changes:** -- Configuration file format changes requiring user action -- Command-line interface changes that break existing scripts -- Removed features or commands -- Changed behavior that could break user workflows - -**MINOR (0.X.0) - New Features (Backward Compatible):** -- New commands or subcommands -- New configuration options -- New features that don't break existing functionality -- Enhanced existing features with new capabilities - -**PATCH (0.0.X) - Bug Fixes and Improvements:** -- Bug fixes that restore intended behavior -- Performance improvements users will notice -- Better error messages or user experience improvements -- Dependency updates that improve stability - -#### **3. CHANGESET CONTENT REQUIREMENTS (MANDATORY)** - -**REQUIRED FORMAT:** -```markdown ---- -"@labcatr/labcommitr": [patch|minor|major] ---- - -[type]: [clear user-facing summary - max 50 characters] - -- [User-facing benefit or change 1] -- [User-facing benefit or change 2] -- [User-facing benefit or change 3] -- [Additional user impacts as needed] -``` - -**SUBJECT LINE RULES:** -- ✅ **MUST**: Start with conventional commit type (feat:, fix:, refactor:) -- ✅ **MUST**: Be written in present tense, imperative mood -- ✅ **MUST**: Focus on user outcome, not technical implementation -- ✅ **MUST**: Be 50 characters or less -- ❌ **NEVER**: Include technical details like class names or file paths -- ❌ **NEVER**: Use past tense ("Added" → "Add") - -**DESCRIPTION RULES:** -- ✅ **MUST**: Write 3-6 bullet points describing user impact -- ✅ **MUST**: Use action-oriented language ("can now", "improved", "added support for") -- ✅ **MUST**: Focus on workflow improvements and user benefits -- ✅ **MUST**: Explain what users can do that they couldn't before -- ❌ **NEVER**: Include technical implementation details -- ❌ **NEVER**: Use developer jargon or internal API references -- ❌ **NEVER**: Write more than 6 bullet points - -#### **4. CHANGESET CONTENT EXAMPLES (FOLLOW THESE)** - -**GOOD Examples:** -```markdown ---- -"@labcatr/labcommitr": minor ---- - -feat: add configuration file validation - -- Configuration files are now validated for syntax and required fields -- Clear error messages help users fix configuration issues quickly -- Tool prevents common mistakes like missing commit types or invalid IDs -- Improved reliability when loading project-specific configurations -``` - -```markdown ---- -"@labcatr/labcommitr": patch ---- - -fix: resolve crash with malformed YAML files - -- Tool no longer crashes when configuration files contain syntax errors -- Helpful error messages guide users to fix YAML formatting issues -- Improved error handling provides specific line and column information -``` - -```markdown ---- -"@labcatr/labcommitr": minor ---- - -feat: add interactive commit type selection - -- Users can now select commit types from an interactive menu -- Support for custom commit types defined in project configuration -- Improved workflow for teams using multiple commit conventions -- Faster commit creation with guided prompts -``` - -**BAD Examples (NEVER DO THIS):** -```markdown -# BAD: Too technical, developer-focused ---- -"@labcatr/labcommitr": minor ---- - -feat: implement ConfigValidator class - -- Add ConfigValidator.validate() method with error collection -- Implement validateTypes() and validateCommitType() methods -- Integrate validator into ConfigLoader.loadConfigFile() -``` - -```markdown -# BAD: Vague, no user benefit explained ---- -"@labcatr/labcommitr": patch ---- - -fix: update dependencies - -- Updated packages to latest versions -``` - -```markdown -# BAD: Generic, no specific information ---- -"@labcatr/labcommitr": minor ---- - -feat: add new features - -- Various improvements -- Better functionality -``` - -#### **5. CHANGESET VALIDATION CHECKLIST (MANDATORY)** - -Before committing any changeset, verify: - -**Content Quality:** -- [ ] Subject line starts with conventional commit type -- [ ] Subject line is 50 characters or less -- [ ] Subject line describes user outcome, not technical change -- [ ] 3-6 bullet points describe specific user benefits -- [ ] Language is user-focused, not developer-focused -- [ ] No technical jargon or implementation details - -**Version Accuracy:** -- [ ] MAJOR: Breaks existing user workflows or requires user action -- [ ] MINOR: Adds new features without breaking existing functionality -- [ ] PATCH: Fixes bugs or improves existing features -- [ ] Version bump matches the actual impact on users - -**Package Information:** -- [ ] Package name exactly matches package.json: "@labcatr/labcommitr" -- [ ] YAML frontmatter is properly formatted -- [ ] Changeset file is included in feature commit - -#### **6. CHANGESET ENFORCEMENT (NON-NEGOTIABLE)** - -**MANDATORY REQUIREMENTS:** -- 🚨 **NO FEATURE MERGES** without corresponding changeset -- 🚨 **ALL USER-FACING CHANGES** must have changeset -- 🚨 **CHANGESET REVIEW** required before merge approval -- 🚨 **VERSION BUMP ACCURACY** must be validated - -**FEATURE COMPLETION PROTOCOL:** -1. Implement feature with incremental commits -2. Create changeset following all requirements above -3. Include changeset in final feature commit -4. Declare feature complete with changeset included -5. Wait for changeset review and merge approval - -**ENFORCEMENT: These changeset rules are NON-NEGOTIABLE and apply to all development activities. No exceptions without explicit authorization.** - ---- - -## **Architectural Flexibility Requirements** - -### **Design Principles (MANDATORY)** - -#### **1. Modular Architecture** -- **Single Responsibility**: Each module has one clear purpose -- **Loose Coupling**: Modules interact through well-defined interfaces -- **High Cohesion**: Related functionality grouped together -- **Dependency Injection**: Dependencies passed in, not hardcoded - -#### **2. Extensibility Patterns** -```typescript -// REQUIRED: Use interfaces for all major components -interface ConfigLoader { - load(path: string): Promise; - validate(config: unknown): ValidationResult; -} - -// REQUIRED: Plugin-based architecture where possible -interface ValidationRule { - name: string; - validate(value: unknown, context: Context): ValidationResult; -} - -// REQUIRED: Registry patterns for extensibility -class ValidationRegistry { - register(rule: ValidationRule): void; - validate(config: unknown): ValidationResult; -} -``` - -#### **3. Configuration Evolution Strategy** -```typescript -// REQUIRED: Version-aware configuration handling -interface ConfigSchema { - version: string; - validate(config: unknown): ValidationResult; - migrate?(from: string, config: unknown): unknown; -} - -// REQUIRED: Backward compatibility preservation -const SCHEMA_VERSIONS = { - '1.0': new V1Schema(), - '1.1': new V1_1Schema(), // Must handle v1.0 configs - '2.0': new V2Schema(), // Must migrate from v1.x -}; -``` - -### **Code Organization Requirements (MANDATORY)** - -#### **Directory Structure** -``` -src/ -├── lib/ -│ ├── config/ -│ │ ├── loader.ts # Config loading logic -│ │ ├── validator.ts # Validation rules -│ │ ├── merger.ts # Default merging -│ │ └── schema.ts # Schema definitions -│ ├── commands/ -│ │ ├── base.ts # Base command interface -│ │ ├── init.ts # Init command -│ │ └── commit.ts # Commit command -│ ├── emoji/ -│ │ ├── detector.ts # Emoji support detection -│ │ └── fallback.ts # Fallback strategies -│ └── utils/ -│ ├── git.ts # Git operations -│ └── paths.ts # Path utilities -``` - -#### **Interface Design Rules** -```typescript -// GOOD: Abstract, extensible interfaces -interface TemplateProcessor { - process(template: string, context: TemplateContext): string; -} - -interface CommandProvider { - getCommands(config: Config): Command[]; -} - -// BAD: Concrete, inflexible implementations -class HardcodedTemplateProcessor { - processEmojiTemplate(template: string): string; // Too specific - processTextTemplate(template: string): string; // Not extensible -} -``` - -### **Testing Requirements (MANDATORY)** - -#### **Test Coverage Standards** -- **Unit Tests**: Every module must have comprehensive unit tests -- **Integration Tests**: Major workflows must be integration tested -- **Edge Case Testing**: All error conditions must be tested -- **Regression Testing**: All bug fixes must include regression tests - -#### **Test Organization** -``` -tests/ -├── unit/ -│ ├── config/ -│ │ ├── loader.test.ts -│ │ └── validator.test.ts -│ └── emoji/ -│ └── detector.test.ts -├── integration/ -│ ├── config-loading.test.ts -│ └── command-execution.test.ts -└── fixtures/ - ├── configs/ - └── expected-outputs/ -``` - ---- - -## **Development Workflow (MANDATORY)** - -### **Feature Implementation Process** - -#### **Step 1: Planning** -1. **Create feature branch** from `dev` -2. **Document approach** in commit message or comments -3. **Identify interfaces** that need to be created/modified -4. **Plan commit sequence** (aim for 5-10 small commits per feature) - -#### **Step 2: Implementation** -1. **Start with interfaces** and type definitions -2. **Implement core logic** in small, testable chunks -3. **Add tests** for each piece of functionality -4. **Commit frequently** with descriptive messages -5. **Refactor** as needed to maintain clean architecture - -#### **Step 3: Integration** -1. **Test feature thoroughly** in isolation -2. **Verify backward compatibility** with existing configs -3. **Update documentation** if interfaces changed -4. **Merge to dev** branch when complete - -### **Code Review Checklist (MANDATORY)** - -Before merging any feature, verify: - -#### **Architecture Compliance** -- [ ] New config fields can be added without code changes -- [ ] Validation rules are extensible -- [ ] Components are testable in isolation -- [ ] Interfaces are abstract enough for multiple implementations -- [ ] Dependencies are injected, not hardcoded - -#### **Code Quality** -- [ ] Commit history is clean and logical -- [ ] Each commit represents one logical change -- [ ] Commit messages follow established format -- [ ] No "Generated by..." or generic messages -- [ ] Code is properly typed (TypeScript) - -#### **Testing** -- [ ] Unit tests cover all new functionality -- [ ] Edge cases are tested -- [ ] Integration tests verify major workflows -- [ ] All tests pass consistently - -#### **Documentation** -- [ ] Public interfaces are documented -- [ ] Complex logic has explanatory comments -- [ ] Configuration changes are documented -- [ ] Migration guides exist for breaking changes - ---- - -## **Flexibility Validation (MANDATORY)** - -### **Before Implementing Any Feature** - -Ask these questions and ensure positive answers: - -#### **Configuration Flexibility** -1. **"Can new config fields be added without changing code?"** - - Validation system must be rule-based, not hardcoded - - Default merging must handle unknown fields gracefully - -2. **"Can existing configs continue to work after updates?"** - - Schema versioning must be implemented - - Migration paths must be defined - -3. **"Can validation rules be extended or customized?"** - - Validation must use registry/plugin pattern - - Rules must be composable and reusable - -#### **Command System Flexibility** -1. **"Can new commands be added without modifying core code?"** - - Command registration must be dynamic - - Command discovery must be pluggable - -2. **"Can command behavior be customized via configuration?"** - - Commands must respect configuration settings - - Behavior must be data-driven, not hardcoded - -#### **Template System Flexibility** -1. **"Can new template variables be added easily?"** - - Variable providers must be pluggable - - Template processing must be extensible - -2. **"Can template formats be customized or extended?"** - - Template processors must be chainable - - Format rules must be configurable - ---- - -## **Quality Gates (MANDATORY)** - -### **Pre-Commit Requirements** -- [ ] Code compiles without errors or warnings -- [ ] All tests pass locally -- [ ] Commit message follows required format -- [ ] Changes are atomic and focused -- [ ] No debugging code or console.logs remain - -### **Pre-Merge Requirements** -- [ ] Feature is complete and tested -- [ ] Documentation is updated -- [ ] Backward compatibility is verified -- [ ] Integration tests pass -- [ ] Code review checklist is satisfied - -### **Release Readiness** -- [ ] All features work end-to-end -- [ ] Configuration schema is documented -- [ ] Migration guides exist for breaking changes -- [ ] Performance is acceptable -- [ ] Error messages are user-friendly - ---- - -## **Progress Document Maintenance (MANDATORY)** - -### **🚨 CRITICAL: Development Progress Tracking Requirements (NON-NEGOTIABLE)** - -**ATTENTION: All AI assistants and automated systems MUST update progress documentation after every completed feature. Failure to maintain accurate progress tracking is unacceptable.** - -#### **MANDATORY PROGRESS UPDATE REQUIREMENTS:** - -1. **WHEN TO UPDATE DEVELOPMENT_PROGRESS.md**: - - ✅ **REQUIRED**: After completing any major implementation step - - ✅ **REQUIRED**: When feature status changes (Not Started → In Progress → Complete) - - ✅ **REQUIRED**: Before declaring feature completion - - ✅ **REQUIRED**: When implementation approach or scope changes - - ✅ **REQUIRED**: After completing partial implementations (e.g., Phase 1 of multi-phase features) - -2. **PROGRESS UPDATE CONTENT REQUIREMENTS**: - - **Status Updates**: Change step status from "Not Started" to "In Progress" to "Complete" or "Partially Complete" - - **Implementation Details**: Add completed work descriptions with specific achievements - - **Progress Metrics**: Update percentage completion and step counts - - **Next Steps**: Update current priority and next critical steps - - **Recent Completion**: Update recent completion section with latest achievements - - **Deliverables**: Mark deliverables as complete with checkmarks - -3. **PROGRESS UPDATE FORMAT REQUIREMENTS**: - ```markdown - #### **Step X: Feature Name** ✅ STATUS - - **Status**: Specific completion state with phase information if applicable - - **Completed Implementation**: Bullet points of what was actually built - - **Deliverable**: ✅ Specific deliverable description - - **Files**: List of files created/modified - - **Changeset**: Reference to changeset if applicable - ``` - -4. **PROGRESS METRICS UPDATE REQUIREMENTS**: - - **Accurate Completion Percentage**: Calculate based on actual work completed - - **Step Count**: Update "X of Y major steps implemented" - - **Next Priority**: Update to reflect actual next development step - - **Current Phase**: Update phase information accurately - - **Recent Completion**: Update with most recently completed work - -5. **CRITICAL ACCURACY REQUIREMENTS**: - - ❌ **NEVER** claim work is complete when only partially done - - ❌ **NEVER** leave progress document showing outdated status - - ❌ **NEVER** show incorrect next steps or priorities - - ❌ **NEVER** inflate completion percentages - - ✅ **ALWAYS** reflect actual implementation state accurately - - ✅ **ALWAYS** distinguish between partial and complete implementations - -#### **ENFORCEMENT: Progress document updates are NON-NEGOTIABLE and must be completed before feature completion declaration.** - ---- - -## **Enforcement** - -### **These Guidelines Are Mandatory** -- **All future development** must follow these practices -- **No exceptions** without explicit discussion and approval -- **Code reviews** must verify compliance with these guidelines -- **Merge requests** that don't follow guidelines will be rejected - -### **Continuous Improvement** -- Guidelines may be updated as the project evolves -- Changes to guidelines require explicit discussion -- All team members must be notified of guideline changes -- Updated guidelines apply to all future work - ---- - -**These guidelines ensure Labcommitr remains maintainable, extensible, and professional throughout its development lifecycle. Following them is not optional—it's required for project success.** diff --git a/docs/DEVELOPMENT_PROGRESS.md b/docs/DEVELOPMENT_PROGRESS.md deleted file mode 100644 index 4750508..0000000 --- a/docs/DEVELOPMENT_PROGRESS.md +++ /dev/null @@ -1,384 +0,0 @@ -# Labcommitr Development Progress - -## Project Purpose - -**Labcommitr** is a command-line tool designed to standardize git commit messages across projects. It provides an interactive and automated way to create consistent, well-formatted commits that follow established conventions with an Astro-like initialization experience. - -### Core Goals: -- **Standardization**: Ensure all commits follow a consistent format across projects -- **Automation**: Reduce manual effort in writing commit messages through interactive prompts -- **Flexibility**: Support both interactive and quick commit workflows -- **Configuration**: Allow project-specific customization through YAML config files -- **User Experience**: Provide intuitive initialization flow similar to modern CLI tools - -### Key Features: -- **Dynamic Emoji Support**: Automatic terminal emoji detection with graceful text fallback -- **Project Root Detection**: Git-prioritized discovery with monorepo support -- **Configuration Hierarchy**: Project → Global → Built-in defaults (no merging) -- **Comprehensive Error Handling**: Actionable error messages with specific solutions -- **YAML-First Configuration**: Human-readable, schema-validated configuration files - -## Project Architecture - -### Technology Stack -- **Language**: TypeScript 5.9.2 (ES2023 target) -- **Runtime**: Node.js (Node16 module system) -- **Package Manager**: pnpm 10.16.1 -- **Build System**: TypeScript Compiler (tsc) -- **Formatting**: Prettier 3.6.2 -- **Versioning**: Changesets for release management - -### Key Dependencies -- **commander**: CLI framework for command and argument parsing -- **@clack/prompts**: Modern interactive CLI prompts -- **picocolors**: Terminal color styling (lightweight) -- **consola**: Beautiful console output and logging -- **boxen**: Styled terminal boxes for enhanced UI -- **js-yaml**: YAML parsing and serialization -- **ufo**: URL/path utilities - -### Legacy Dependencies (To Be Removed) -- **magicast**: AST manipulation (replaced by YAML-based approach) -- **cosmiconfig**: Config discovery (replaced by custom implementation) - -## Project Structure - -``` -src/ -├── index.ts # Main CLI entry point -├── cli/ -│ ├── program.ts # Commander.js program setup -│ ├── commands/ -│ │ ├── index.ts # Command exports -│ │ ├── config.ts # Config management commands -│ │ ├── commit.ts # Commit command (placeholder) -│ │ └── init/ # Init command module -│ │ ├── index.ts # Main init command -│ │ ├── clef.ts # Clef animated mascot -│ │ ├── prompts.ts # Interactive prompts -│ │ └── config-generator.ts # YAML generation -│ └── utils/ -│ ├── error-handler.ts # Global error handling -│ └── version.ts # Version utilities -├── lib/ -│ ├── config/ # Configuration system -│ │ ├── index.ts # Public API exports -│ │ ├── types.ts # TypeScript interfaces -│ │ ├── defaults.ts # Default configurations -│ │ ├── loader.ts # Configuration loading logic -│ │ └── validator.ts # Schema validation -│ ├── presets/ # Configuration presets -│ │ ├── index.ts # Preset registry -│ │ ├── conventional.ts # Conventional Commits -│ │ ├── gitmoji.ts # Gitmoji style -│ │ ├── angular.ts # Angular convention -│ │ └── minimal.ts # Minimal setup -│ ├── configurator.ts # Legacy config (DEPRECATED) -│ ├── executor.ts # Git operations (TODO) -│ ├── logger.ts # Logging utilities -│ ├── messageLexer.ts # Template processing (TODO) -│ ├── parser.ts # CLI argument parsing (TODO) -│ └── util/ -│ └── constants.ts # Project constants -``` - -## Current Status (Updated: January 2025) - -### ✅ COMPLETED IMPLEMENTATION - -#### 1. **Project Infrastructure** (100% Complete) -- **Files**: `package.json`, `tsconfig.json`, `.gitignore`, build system -- **Status**: Fully configured and operational -- **Features**: - - Modern TypeScript setup with ES2023 target - - Node16 module system for ESM compatibility - - Comprehensive build and formatting scripts - - All dependencies updated to latest versions - - Changesets integration for version management - -#### 2. **Logger System** (100% Complete) -- **File**: `src/lib/logger.ts` -- **Status**: Fully implemented and tested -- **Features**: - - Complete console wrapper methods (info, warn, success, error) - - Styled box output using boxen - - Type-safe options interface - - Ready for use across all components - -#### 3. **Requirements & Planning** (100% Complete) -- **Files**: `REQUIREMENTS.md`, `CONFIG_SCHEMA.md`, `DEVELOPMENT_GUIDELINES.md` -- **Status**: Comprehensive requirements and architectural decisions documented -- **Key Decisions**: - - YAML-first configuration approach with `.labcommitr.config.yaml` - - Single-file config with strict precedence (no merging) - - `labcommitr` command with `lab` alias - - macOS/Unix primary platform support - - Dynamic emoji fallback system - - Astro-like initialization experience target - -#### 4. **Configuration Loading System** (100% Complete - NEW) -- **Files**: `src/lib/config/` module (types.ts, defaults.ts, loader.ts, index.ts) -- **Status**: Fully implemented with comprehensive features -- **Architecture**: Async-first with git-prioritized project root detection -- **Key Features**: - - **Project Root Detection**: Git repository prioritized over package.json - - **File Discovery**: Searches for `.labcommitr.config.yaml` in project root - - **YAML Parsing**: Using js-yaml with comprehensive error transformation - - **Permission Validation**: Pre-read file permission checking - - **Smart Caching**: File modification time-based cache invalidation - - **Default Merging**: User config merged with sensible defaults - - **Error Handling**: User-friendly ConfigError with actionable solutions - - **Monorepo Support**: Detects multiple package.json files within git root - - **Public API**: ConfigLoader class and convenience loadConfig() function -- **Changeset**: Added for minor release (v0.1.0) - -### 🔄 LEGACY COMPONENTS (REQUIRE REMOVAL) - -#### 5. **Legacy Configurator System** (Deprecated) -- **File**: `src/lib/configurator.ts` -- **Status**: Based on old magicast architecture - marked for removal -- **Action Required**: Remove after CLI migration to new config system - -### ❌ REMAINING IMPLEMENTATION (PHASE 1) - -#### 6. **Configuration Validation System** (0% Complete - CURRENT PRIORITY) -- **File**: `src/lib/config/validator.ts` (to be created) -- **Status**: Next immediate step - required before CLI implementation -- **Dependencies**: Configuration loading system (✅ completed) -- **Requirements**: Schema validation, field validation, business logic validation - -#### 7. **CLI Entry Point & Framework** (0% Complete - Priority 2) -- **File**: `src/index.ts` -- **Status**: Contains placeholder - needs Commander.js integration -- **Dependencies**: Configuration validation system -- **Requirements**: Basic commands (--help, --version), command structure - -#### 8. **Init Command Implementation** (0% Complete - Priority 3) -- **Files**: CLI command integration -- **Status**: Not started - requires CLI framework -- **Dependencies**: CLI framework, configuration system -- **Requirements**: Interactive prompts, preset selection, config generation - -#### 9. **Git Operations & Commit Command** (0% Complete - Priority 4) -- **Files**: Git integration modules -- **Status**: Not started - requires CLI and config systems -- **Dependencies**: All previous components -- **Requirements**: Git commit execution, message formatting - -## Phase 1 Implementation Plan - -### **COMPLETED STEPS** - -#### **Step 1: Configuration Schema Design** ✅ COMPLETED -- **Status**: COMPLETED - Schema Fully Defined -- **Completed Work**: - - YAML schema structure for `.labcommitr.config.yaml` finalized in `CONFIG_SCHEMA.md` - - Built-in default configuration values specified - - Commit types structure (id, description, emoji) defined - - Dynamic emoji support with automatic terminal detection designed - - Template processing logic for emoji/text fallback specified -- **Deliverable**: ✅ Complete YAML schema specification with examples -- **Files**: `CONFIG_SCHEMA.md`, configuration interfaces in types - -#### **Step 2: Configuration Loading System** ✅ COMPLETED -- **Status**: COMPLETED - Fully Implemented -- **Dependencies**: ✅ Step 1 completed -- **Completed Implementation**: - - **Custom discovery approach** (no cosmiconfig) with git-prioritized project root detection - - **Comprehensive error handling** for malformed YAML files with actionable solutions - - **Smart caching system** with file modification time-based invalidation - - **js-yaml integration** for YAML parsing with detailed error transformation - - **Monorepo support** with multiple package.json detection - - **Public API design** with ConfigLoader class and convenience functions -- **Deliverable**: ✅ Working config loader with strict precedence -- **Files**: Complete `src/lib/config/` module with types, defaults, loader, index -- **Changeset**: Added for v0.1.0 minor release - -### **COMPLETED STEPS** - -#### **Step 3: Configuration Validation System (Phase 1)** ✅ FULLY COMPLETE -- **Status**: Phase 1 Fully Implemented with Rich Error Context -- **Dependencies**: ✅ Steps 1 & 2 completed -- **Completed Implementation**: - - ✅ Enhanced ValidationError interface with 8 rich context fields - - ✅ Required fields validation (types array must exist and be non-empty) - - ✅ Individual commit type structure validation (id, description, emoji) - - ✅ ID format validation with character analysis (identifies uppercase, dashes, numbers, spaces, special chars) - - ✅ Optional sections basic structure validation - - ✅ Fixed ConfigError import (separated type from value import) - - ✅ Comprehensive error collection with field-specific paths and user-friendly display names - - ✅ Rich error formatting with scannable structure (~250-300 chars per error) - - ✅ Character-specific issue identification (e.g., "Contains dash (-), F (uppercase)") - - ✅ Actionable guidance without destructive suggestions (no "lab init" for validation errors) - - ✅ 100% test success rate - validated with single and multiple error scenarios -- **Phase 2 Deferred**: Business logic validation (uniqueness, cross-references) - - Unique type IDs validation - - Cross-reference validation (require_scope_for references existing types) - - Numeric field validation (positive values, min < max) - - Template variable validation -- **Phase 3 Deferred**: Advanced validation (templates, industry standards) - - Emoji format validation - - Template completeness validation - - Industry standards compliance -- **Deliverable**: ✅ Phase 1 fully complete in `src/lib/config/validator.ts` and integrated in `loader.ts` -- **Files Modified**: `types.ts`, `validator.ts`, `loader.ts` -- **Commit**: `df0949e` - Configuration validation with rich error messages - -#### **Step 4: CLI Framework & Basic Commands** ✅ FULLY COMPLETE -- **Status**: COMPLETED - Full CLI framework implemented and tested -- **Dependencies**: ✅ Steps 1-3 completed -- **Completed Implementation**: - - ✅ Commander.js 14.0.2 integrated with full TypeScript support - - ✅ Modular command architecture (src/cli/commands/, src/cli/utils/) - - ✅ Working commands: `--help`, `--version`, `config show` - - ✅ Dual alias support: Both `labcommitr` and `lab` commands functional - - ✅ Config integration with Step 3 validation error display - - ✅ Init and commit command placeholders for Steps 5 & 6 - - ✅ Global error handling with ConfigError integration - - ✅ ESM-compatible utilities (version.ts, error-handler.ts) - - ✅ Built and tested locally with npm link -- **Deliverable**: ✅ Working CLI framework with basic commands -- **Commits**: 8 atomic commits following best practices -- **Files Created**: 7 new files (program.ts, 3 commands, 2 utils, index.ts) -- **Branch**: `feature/cli-framework` - -#### **Step 5: Init Command with Clef Mascot** ✅ FULLY COMPLETE -- **Status**: COMPLETED - Interactive initialization with animated mascot -- **Dependencies**: ✅ Steps 1-4 completed -- **Completed Implementation**: - - ✅ Clef animated cat mascot with full-body ASCII art - - ✅ ANSI-based terminal animations with walk cycles - - ✅ Intro, processing, and outro animation sequences - - ✅ Clean minimal prompts with color-coded compact labels - - ✅ Four configuration presets: Conventional, Gitmoji, Angular, Minimal - - ✅ Interactive preset selection with descriptions and examples - - ✅ Emoji support preference prompt - - ✅ Scope configuration with multiple modes (optional, selective, always, never) - - ✅ Selective scope type selection for custom rules - - ✅ YAML configuration file generation with validation - - ✅ Project root detection with git repository priority - - ✅ Existing config detection with force override option - - ✅ Complete screen clears between sections for clean output - - ✅ Graceful degradation for non-TTY environments - - ✅ Terminal capability detection for animation support -- **Deliverable**: ✅ Working `lab init` command with Clef mascot -- **Commits**: 7 atomic commits following best practices -- **Files Created**: 9 new files (clef.ts, prompts.ts, config-generator.ts, init/index.ts, 4 presets, preset registry) -- **Branch**: `feature/init-command` -- **Changeset**: Added for v0.2.0 minor release - -### **NEXT STEPS** - -#### **Step 6: Interactive Commit Command** (Priority: High) -- **Status**: Not Started - Requires All Previous Steps -- **Dependencies**: Steps 1-5 must be completed first -- **Implementation Requirements**: - - **Interactive prompts**: Type selection, subject input, optional description and scope - - **Git integration**: Commit execution with proper error handling - - **Message formatting**: Template processing with emoji/text fallback - - **Type validation**: Ensure valid commit types from config - - **CLI flag support**: Optional non-interactive mode with flags (e.g., `-s "Subject" -m "Message"`) -- **Deliverable**: Working interactive commit workflow with optional CLI flag support -- **Estimated Effort**: 3-4 days after Step 5 completion - -### **PHASE 1 SCOPE CONSTRAINTS** - -#### **INCLUDED IN PHASE 1** -- ✅ Single `.labcommitr.config.yaml` file support (COMPLETED) -- ✅ YAML config parsing with basic error handling (COMPLETED) -- ✅ Project config discovery with git-prioritized root detection (COMPLETED) -- ✅ Configuration validation system (COMPLETED) -- ✅ Basic CLI framework with `--help`, `--version`, `config show` commands (COMPLETED) -- ✅ Interactive `init` command with preset selection and Clef mascot (COMPLETED) -- ✅ Dynamic emoji support with terminal detection and text fallback (COMPLETED) -- ✅ Both `labcommitr` and `lab` command aliases (COMPLETED) -- 📋 Interactive commit command with prompt-based workflow (NEXT - PRIMARY) -- 📋 Optional CLI flags for non-interactive commit workflow (SECONDARY) - -#### **EXPLICITLY EXCLUDED FROM PHASE 1** -- Global config file support (project-only for Phase 1) -- Multiple profile directory structure (`.labcommitr/profiles/`) -- Advanced templating with complex variable substitution -- GitHub-specific emoji shortcode handling -- Complex validation rules (length limits, advanced scope rules) -- Plugin or extension system -- Windows platform support (macOS/Unix only) - -### **RESOLVED DECISIONS** - -#### **✅ Architecture Decisions (COMPLETED)** -- ✅ **Config Schema**: YAML structure and field definitions finalized -- ✅ **Config Loading**: Custom implementation with git-prioritized discovery -- ✅ **Error Handling**: ConfigError class with actionable user guidance -- ✅ **Emoji Strategy**: Dynamic terminal detection with graceful text fallback -- ✅ **Caching Strategy**: File modification time-based cache invalidation -- ✅ **YAML Library**: js-yaml for parsing with comprehensive error transformation -- ✅ **Validation Error Messaging**: Rich, scannable, actionable, non-destructive -- ✅ **CLI Framework**: Commander.js for robust command-line interface -- ✅ **Modular CLI Structure**: src/cli/ for commands and utilities -- ✅ **Preset System**: Four presets (Conventional, Gitmoji, Angular, Minimal) -- ✅ **Interactive Prompts**: @clack/prompts for modern CLI UX -- ✅ **Mascot Design**: Clef the cat with full-body ANSI animations - -#### **📋 Pending Decisions (FOR UPCOMING STEPS)** -- **Git Integration**: Library selection for commit execution (Step 6) -- **Message Formatting**: Template processing with variable substitution (Step 6) - -### **CURRENT BLOCKERS (UPDATED)** - -#### **🚫 No Current Blockers** -All immediate blockers have been resolved: -- ✅ **Schema Definition**: COMPLETED -- ✅ **Config Loading Strategy**: COMPLETED with custom implementation -- ✅ **Error Handling Strategy**: COMPLETED with ConfigError system -- ✅ **Architecture Dependencies**: Configuration system foundation complete -- ✅ **CLI Framework**: COMPLETED with Commander.js integration -- ✅ **Init Command**: COMPLETED with Clef mascot and presets - -#### **🎯 Next Implementation Ready** -- **Step 6 (Interactive Commit Command)**: Ready to implement - all dependencies complete -- Configuration system fully operational with validation -- CLI framework established with working commands -- Preset system available for commit type selection -- Clear implementation requirements defined - -## Success Metrics for Phase 1 - -### **Minimum Viable Product Checklist (UPDATED)** -- [x] **Step 1 Complete**: YAML schema defined and documented ✅ -- [x] **Step 2 Complete**: Config loading system with strict precedence working ✅ -- [x] **Step 3 Phase 1 Complete**: Basic configuration validation with schema error reporting ✅ -- [ ] **Step 3 Phases 2&3**: Business logic and advanced validation (optional for CLI start) -- [x] **Step 4 Complete**: CLI framework with `--help`, `--version`, `config show` commands ✅ -- [x] **Step 5 Complete**: `lab init` creates complete `.labcommitr.config.yaml` with Clef mascot ✅ -- [ ] **Step 6 Complete**: `lab commit` creates standardized commits with emoji/text fallback -- [ ] **Global Installation**: Tool installs and works via `npm install -g` -- [x] **Alias Support**: Both `labcommitr` and `lab` commands work identically ✅ -- [x] **Error Handling**: Clear, helpful error messages for all failure cases ✅ -- [x] **Config Discovery**: Project config discovery with git-prioritized root detection ✅ -- [x] **YAML Validation**: Malformed configs show precise error messages ✅ - -### **Phase 1 Definition of Done** -A working CLI tool that: -1. ✅ **Loads and validates configuration** from `.labcommitr.config.yaml` files (COMPLETED) -2. ✅ **Can be installed globally** via NPM with working CLI commands (COMPLETED - local testing verified) -3. ✅ **Generates project-specific YAML configuration** via interactive `init` command (COMPLETED) -4. ✅ **Provides Clef-enhanced initialization experience** with preset selection and guided setup (COMPLETED) -5. **Creates git commits** using configured commit types with dynamic emoji support -6. ✅ **Provides clear help and error messages** for all user interactions (COMPLETED) -7. ✅ **Works reliably on macOS/Unix systems** with comprehensive error handling (COMPLETED) - -### **Current Implementation Status** -- **Foundation**: ✅ **EXCELLENT** - Configuration, validation, CLI framework, init command complete -- **Progress**: **~85% Complete** - 5 of 6 major steps implemented (Steps 1-5 complete) -- **Next Priority**: 🎯 **Interactive Commit Command (Step 6)** -- **Estimated Completion**: **3-4 days** remaining for full Phase 1 MVP - ---- - -**Last Updated**: January 2025 -**Current Phase**: Phase 1 Implementation (Step 5 Complete) -**Next Critical Step**: Implement Interactive Commit Command (Step 6) -**Recent Completion**: Init Command with Clef Mascot - Interactive configuration generation with animated character -**Implementation Velocity**: Excellent - 5 of 6 major steps complete, strong architectural foundation with delightful UX - diff --git a/docs/REQUIREMENTS.md b/docs/REQUIREMENTS.md deleted file mode 100644 index 3bbc7f1..0000000 --- a/docs/REQUIREMENTS.md +++ /dev/null @@ -1,123 +0,0 @@ -# Labcommitr Requirements - -## Core Objective -Build a customizable CLI tool for standardized git commits that can be tailored to match specific project commit styles. - -## Primary Requirements - -### 1. Project-Specific Customization -- Tool must adapt to different project commit conventions -- Support for configurable commit types with default catalog (feat, fix, docs, etc.) -- Customizable commit message templates and formats -- Configurable validation rules per project -- Dynamic emoji support with automatic terminal compatibility detection and graceful fallback -- *Note: Detailed commit type and style configuration to be defined in future iterations* - -### 2. Astro-like Initialization Flow -- `labcommitr init` command provides interactive setup -- Offer multiple pre-configured presets: - - Conventional Commits - - Gitmoji style - - Angular style - - Minimal/Custom -- Generate complete `.labcommitr.config.yaml` file based on selection -- Generated config includes full, editable configuration (no merging with global) -- Re-running init should be non-destructive with safe update path -- Allow modification of generated config as needed - -### 3. Configuration Hierarchy -- **Global Config**: User-wide settings in standard OS locations (XDG/macOS Library) -- **Project Config**: Project-specific settings in project root -- **Precedence**: Project config → Global config → Built-in defaults (no merging between sources) -- **Strategy**: First-found config is used entirely; no partial overrides or merging -- **Deterministic**: Clear, predictable behavior with single source of truth per project - -### 4. Configuration Discovery -- Use established library (e.g., Cosmiconfig) for config file discovery -- **Primary format**: YAML for readability and schema validation -- **Canonical filename**: `.labcommitr.config.yaml` (with `.yml` support) -- **Discovery order**: Project `.labcommitr.config.yaml` → Global config → Built-in defaults -- Cache resolved config per CLI run for performance -- Log which config source was used for transparency - -### 5. Command Line Interface -- **Primary command**: `labcommitr` (stable across all projects) -- **Alias**: `lab` for faster access -- Use mature parsing library (Commander.js) unless specific custom needs arise -- Keep abstraction layer between CLI framework and command logic -- Support standard CLI patterns: `--help`, `--version`, error handling -- Subcommand customization is not a priority for initial implementation - -## Implementation Strategy - -### Phase 1: Foundation -- Define YAML configuration schema and sensible built-in defaults -- Implement config discovery with strict precedence (project → global → defaults) -- Create simple `init` command with 2-3 core presets that generate complete configs -- Enable basic commit functionality that respects single-source configuration -- Establish `labcommitr` and `lab` command aliases -- Implement dynamic emoji support with automatic terminal detection and fallback - -### Phase 2: Enhancement -- Add more presets and customization options -- Implement interactive commit builder -- Add advanced validation and templating -- Improve error handling and user experience - -### Phase 3: Advanced Features -- Plugin architecture for extensibility -- Advanced templating with variable substitution -- Integration with other development tools -- Community preset sharing - -## Technical Constraints - -### Must Have -- Work with existing git workflows -- Support Node.js LTS versions -- **Primary platform**: macOS/Unix (Windows support for future extension) -- NPM distribution ready with global installation support -- Zero-config defaults that work out of the box - -### Should Have -- Fast startup time -- Clear error messages -- Comprehensive help system -- Backward compatibility for config changes -- Cross-terminal emoji compatibility without requiring system modifications - -### Nice to Have -- Plugin ecosystem support -- Integration with popular commit conventions -- IDE/editor integrations -- Team configuration sharing - -## Success Criteria - -### Minimum Viable Product -- [ ] Users can install globally via NPM (`labcommitr` and `lab` aliases) -- [ ] `labcommitr init` creates complete `.labcommitr.config.yaml` file -- [ ] Basic commit command respects single-source configuration precedence -- [ ] Config discovery follows project → global → defaults hierarchy -- [ ] Tool works with built-in defaults when no configuration present -- [ ] YAML config validation with clear error messages -- [ ] Dynamic emoji support works across different terminal environments - -### Full Feature Set -- [ ] Multiple preset options in init flow -- [ ] Rich customization through configuration -- [ ] Interactive and quick commit modes -- [ ] Comprehensive validation and templates -- [ ] Seamless integration with existing git workflows - -## Non-Requirements (Out of Scope) - -- Git repository management beyond commits -- Complex branching or merge strategies -- Integration with specific hosting platforms (GitHub, GitLab, etc.) -- Commit message editing after creation -- Historical commit analysis or modification - ---- - -*This document defines preliminary requirements and may be updated as development progresses.* diff --git a/src/lib/config/validator.ts b/src/lib/config/validator.ts index a58052a..f7ded4b 100644 --- a/src/lib/config/validator.ts +++ b/src/lib/config/validator.ts @@ -1,7 +1,7 @@ /** * Configuration validation system for labcommitr * - * Implements incremental validation following the CONFIG_SCHEMA.md specification. + * Implements incremental validation following the docs/CONFIG_SCHEMA.md specification. * Phase 1: Basic schema validation (required fields, types, structure) * Phase 2: Business logic validation (uniqueness, cross-references) * Phase 3: Advanced validation (templates, industry standards) From dd152af07e352de8d17e086f5ee87f5eea354c17 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 15:27:33 -0700 Subject: [PATCH 07/24] fix: enable next batch pagination in preview command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Fix fetchCommits to support pagination with excludeHash parameter • Fix currentPage increment when pressing n for next batch • Ensure hasMorePages correctly indicates available pages • Fix pagination to fetch commits starting from last seen commit --- src/cli/commands/preview/index.ts | 35 +++++++++++++++++++---- src/cli/commands/shared/git-operations.ts | 14 ++++++++- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/preview/index.ts b/src/cli/commands/preview/index.ts index 6543e74..8d3ad12 100644 --- a/src/cli/commands/preview/index.ts +++ b/src/cli/commands/preview/index.ts @@ -72,7 +72,11 @@ async function previewAction(options: { const remaining = maxCommits - totalFetched; const toFetch = Math.min(remaining, 50); - const newCommits = fetchCommits(toFetch, branch); + + // Get the last commit hash we've already fetched to exclude it from next fetch + const lastHash = allCommits.length > 0 ? allCommits[allCommits.length - 1].hash : undefined; + + const newCommits = fetchCommits(toFetch, branch, lastHash); allCommits = [...allCommits, ...newCommits]; totalFetched = allCommits.length; hasMore = newCommits.length === 50 && totalFetched < maxCommits; @@ -165,9 +169,13 @@ async function previewAction(options: { const pageCommits = allCommits.slice(startIndex, endIndex); const maxIndex = pageCommits.length - 1; - displayCommitList(pageCommits, startIndex, totalFetched, hasMore); + // Check if there are more pages to show (either already loaded or can be fetched) + const hasMorePages = (currentPage + 1) * pageSize < allCommits.length || hasMore; + const hasPreviousPage = currentPage > 0; - const action = await waitForListAction(maxIndex, hasMore); + displayCommitList(pageCommits, startIndex, totalFetched, hasMore, hasPreviousPage, hasMorePages); + + const action = await waitForListAction(maxIndex, hasMorePages, hasPreviousPage); if (typeof action === "number") { // View commit details @@ -190,16 +198,31 @@ async function previewAction(options: { currentDetailCommit = commit; } viewingDetails = true; + } else if (action === "previous") { + // Move to previous page + if (currentPage > 0) { + currentPage--; + } } else if (action === "next") { - // Load next batch - if (hasMore) { + // Move to next page + const nextPageStart = (currentPage + 1) * pageSize; + + // If we need more commits and they're available, load them + if (nextPageStart >= allCommits.length && hasMore) { console.log("\n Loading next batch..."); await loadMoreCommits(); - if (!hasMore) { + if (!hasMore && nextPageStart >= allCommits.length) { console.log(" Maximum commits loaded (100)."); await new Promise((resolve) => setTimeout(resolve, 1000)); + // Don't increment page if we can't show it + continue; } } + + // Increment page if we have commits to show + if (nextPageStart < allCommits.length) { + currentPage++; + } } else if (action === "help") { displayHelp(); await new Promise((resolve) => { diff --git a/src/cli/commands/shared/git-operations.ts b/src/cli/commands/shared/git-operations.ts index fdc429e..22af224 100644 --- a/src/cli/commands/shared/git-operations.ts +++ b/src/cli/commands/shared/git-operations.ts @@ -90,6 +90,7 @@ function formatRelativeTime(date: Date): string { export function fetchCommits( limit: number, branch?: string, + excludeHash?: string, ): CommitInfo[] { const args = [ "log", @@ -99,7 +100,18 @@ export function fetchCommits( "--date=iso", ]; - if (branch) { + // To get commits older than excludeHash, we fetch starting from excludeHash's parent + // excludeHash^ means "the parent of excludeHash" (which is older in the history) + // This gives us commits that are older than the last one we've seen + if (excludeHash) { + // Fetch from the parent of the last commit we've seen + // This will get commits older than excludeHash + const startPoint = `${excludeHash}^`; + args.push(startPoint); + // If branch is specified, we still want to limit to that branch + // But since we're starting from an older commit, we'll naturally get commits on the same branch + // (unless there are merges, but that's fine - we want all commits) + } else if (branch) { args.push(branch); } From b732a0041dc9aa24a81c9371716e5c7f72dabf0b Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 15:27:36 -0700 Subject: [PATCH 08/24] feat: add previous batch navigation to preview command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add p key for navigating to previous batch • Update displayCommitList to show previous/next hints conditionally • Fix navigation hints to only appear when actions are available • Remove arrow key navigation (↑/↓) and help text • Use hasMorePages instead of hasMore for navigation hints --- src/cli/commands/preview/prompts.ts | 55 ++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/src/cli/commands/preview/prompts.ts b/src/cli/commands/preview/prompts.ts index edd7afc..3c3dbae 100644 --- a/src/cli/commands/preview/prompts.ts +++ b/src/cli/commands/preview/prompts.ts @@ -44,6 +44,8 @@ export function displayCommitList( startIndex: number, totalFetched: number, hasMore: boolean, + hasPreviousPage: boolean = false, + hasMorePages: boolean = false, ): void { console.log(); console.log( @@ -78,18 +80,42 @@ export function displayCommitList( // Pagination info const endIndex = startIndex + displayCount; console.log(); + const paginationHints: string[] = []; + if (hasPreviousPage) { + paginationHints.push(`${textColors.brightYellow("p")} for previous batch`); + } + if (hasMorePages) { + paginationHints.push(`${textColors.brightYellow("n")} for next batch`); + } + const paginationText = paginationHints.length > 0 + ? ` (press ${paginationHints.join(", ")})` + : ""; + if (hasMore) { console.log( - ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+ (press ${textColors.brightYellow("n")} for next batch)`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+${paginationText}`, ); } else { console.log( - ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}${paginationText}`, ); } console.log(); + + // Build navigation hints + const navHints: string[] = []; + navHints.push(`${textColors.brightCyan("0-9")} ${textColors.white("to view details")}`); + if (hasPreviousPage) { + navHints.push(`${textColors.brightYellow("p")} ${textColors.white("for previous batch")}`); + } + if (hasMorePages) { + navHints.push(`${textColors.brightYellow("n")} ${textColors.white("for next batch")}`); + } + navHints.push(`${textColors.brightYellow("?")} ${textColors.white("for help")}`); + navHints.push(`${textColors.brightYellow("Esc")} ${textColors.white("to exit")}`); + console.log( - ` ${textColors.white("Press")} ${textColors.brightCyan("0-9")} ${textColors.white("to view details,")} ${textColors.brightYellow("n")} ${textColors.white("for next batch,")} ${textColors.brightYellow("?")} ${textColors.white("for help, or")} ${textColors.brightYellow("Esc")} ${textColors.white("to exit")}`, + ` ${textColors.white("Press")} ${navHints.join(`, `)}`, ); } @@ -169,7 +195,7 @@ export function displayHelp(): void { ); console.log(); console.log(` ${textColors.brightCyan("0-9")} View commit details`); - console.log(` ${textColors.brightYellow("↑/↓")} Navigate list`); + console.log(` ${textColors.brightYellow("p")} Jump to previous batch`); console.log(` ${textColors.brightYellow("n")} Jump to next batch`); console.log(` ${textColors.brightYellow("b")} View/toggle body`); console.log(` ${textColors.brightYellow("f")} View/toggle files`); @@ -266,8 +292,9 @@ export async function waitForDetailAction(): Promise< */ export async function waitForListAction( maxIndex: number, - hasMore: boolean, -): Promise { + hasMorePages: boolean, + hasPreviousPage: boolean = false, +): Promise { return new Promise((resolve) => { const stdin = process.stdin; const wasRaw = stdin.isRaw; @@ -287,11 +314,6 @@ export async function waitForListAction( return; } - if (key.name === "up" || key.name === "down") { - // Let user navigate with arrows (we'll handle this in the main loop) - return; - } - // Number keys 0-9 if (/^[0-9]$/.test(char)) { const num = parseInt(char, 10); @@ -302,8 +324,15 @@ export async function waitForListAction( } } - // Next batch - if ((char === "n" || char === "N") && hasMore) { + // Previous batch - allow if there's a previous page + if ((char === "p" || char === "P") && hasPreviousPage) { + cleanup(); + resolve("previous"); + return; + } + + // Next batch - allow if there are more pages to show + if ((char === "n" || char === "N") && hasMorePages) { cleanup(); resolve("next"); return; From 7cd66a2a8b738195475c4f1cbf9f832a6a8f4a9a Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 15:27:39 -0700 Subject: [PATCH 09/24] feat: apply preview navigation improvements to revert command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add p key for previous batch navigation • Fix pagination to properly increment currentPage • Update navigation hints to only show when actions available • Use hasMorePages for accurate navigation hint display • Fix fetchCommits pagination with excludeHash parameter --- src/cli/commands/revert/index.ts | 68 +++++++++++++++++++++++++++--- src/cli/commands/revert/prompts.ts | 19 ++++++++- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/src/cli/commands/revert/index.ts b/src/cli/commands/revert/index.ts index 5d5e331..9a136f7 100644 --- a/src/cli/commands/revert/index.ts +++ b/src/cli/commands/revert/index.ts @@ -376,6 +376,17 @@ async function revertAction(options: { process.exit(1); } + // Check for config if --no-edit is not used (config needed for commit workflow) + if (!options.noEdit) { + const configResult = await loadConfig(); + if (configResult.source === "defaults") { + Logger.error("Configuration not found"); + console.error("\n Run 'lab init' to create configuration file."); + console.error(" Or use --no-edit to use Git's default revert message.\n"); + process.exit(1); + } + } + // Check for uncommitted changes if (hasUncommittedChanges()) { console.log(); @@ -409,7 +420,11 @@ async function revertAction(options: { const remaining = maxCommits - totalFetched; const toFetch = Math.min(remaining, 50); - const newCommits = fetchCommits(toFetch, branch); + + // Get the last commit hash we've already fetched to exclude it from next fetch + const lastHash = allCommits.length > 0 ? allCommits[allCommits.length - 1].hash : undefined; + + const newCommits = fetchCommits(toFetch, branch, lastHash); allCommits = [...allCommits, ...newCommits]; totalFetched = allCommits.length; hasMore = newCommits.length === 50 && totalFetched < maxCommits; @@ -432,10 +447,25 @@ async function revertAction(options: { const endIndex = Math.min(startIndex + pageSize, allCommits.length); const pageCommits = allCommits.slice(startIndex, endIndex); - displayRevertCommitList(pageCommits, startIndex, totalFetched, hasMore); + // Check if there are more pages to show (either already loaded or can be fetched) + const hasMorePages = (currentPage + 1) * pageSize < allCommits.length || hasMore; + const hasPreviousPage = currentPage > 0; + + displayRevertCommitList(pageCommits, startIndex, totalFetched, hasMore, hasPreviousPage, hasMorePages); + // Build navigation hints + const navHints: string[] = []; + navHints.push(`${textColors.brightCyan("0-9")} ${textColors.white("to select commit")}`); + if (hasPreviousPage) { + navHints.push(`${textColors.brightYellow("p")} ${textColors.white("for previous batch")}`); + } + if (hasMorePages) { + navHints.push(`${textColors.brightYellow("n")} ${textColors.white("for next batch")}`); + } + navHints.push(`${textColors.brightYellow("Esc")} ${textColors.white("to cancel")}`); + console.log( - ` ${textColors.white("Press")} ${textColors.brightCyan("0-9")} ${textColors.white("to select commit,")} ${textColors.brightYellow("n")} ${textColors.white("for next batch,")} ${textColors.brightYellow("Esc")} ${textColors.white("to cancel")}`, + ` ${textColors.white("Press")} ${navHints.join(`, `)}`, ); // Wait for input @@ -450,7 +480,7 @@ async function revertAction(options: { readline.emitKeypressEvents(stdin); - const selection = await new Promise((resolve) => { + const selection = await new Promise((resolve) => { const onKeypress = (char: string, key: readline.Key) => { if (key.name === "escape" || (key.ctrl && key.name === "c")) { cleanup(); @@ -467,7 +497,15 @@ async function revertAction(options: { } } - if ((char === "n" || char === "N") && hasMore) { + // Previous batch - allow if there's a previous page + if ((char === "p" || char === "P") && hasPreviousPage) { + cleanup(); + resolve("previous"); + return; + } + + // Next batch - allow if there are more pages to show + if ((char === "n" || char === "N") && hasMorePages) { cleanup(); resolve("next"); return; @@ -488,15 +526,31 @@ async function revertAction(options: { if (selection === "cancel") { console.log("\n Revert cancelled.\n"); process.exit(0); + } else if (selection === "previous") { + // Move to previous page + if (currentPage > 0) { + currentPage--; + } } else if (selection === "next") { - if (hasMore) { + // Move to next page + const nextPageStart = (currentPage + 1) * pageSize; + + // If we need more commits and they're available, load them + if (nextPageStart >= allCommits.length && hasMore) { console.log("\n Loading next batch..."); await loadMoreCommits(); - if (!hasMore) { + if (!hasMore && nextPageStart >= allCommits.length) { console.log(" Maximum commits loaded (100)."); await new Promise((resolve) => setTimeout(resolve, 1000)); + // Don't increment page if we can't show it + continue; } } + + // Increment page if we have commits to show + if (nextPageStart < allCommits.length) { + currentPage++; + } } else if (typeof selection === "number") { selectedCommit = pageCommits[selection]; // Load full details if needed diff --git a/src/cli/commands/revert/prompts.ts b/src/cli/commands/revert/prompts.ts index 8e530ca..332baba 100644 --- a/src/cli/commands/revert/prompts.ts +++ b/src/cli/commands/revert/prompts.ts @@ -52,6 +52,8 @@ export function displayRevertCommitList( startIndex: number, totalFetched: number, hasMore: boolean, + hasPreviousPage: boolean = false, + hasMorePages: boolean = false, ): void { console.log(); console.log( @@ -82,14 +84,27 @@ export function displayRevertCommitList( ); } + // Pagination info + const endIndex = startIndex + displayCount; console.log(); + const paginationHints: string[] = []; + if (hasPreviousPage) { + paginationHints.push(`${textColors.brightYellow("p")} for previous batch`); + } + if (hasMorePages) { + paginationHints.push(`${textColors.brightYellow("n")} for next batch`); + } + const paginationText = paginationHints.length > 0 + ? ` (press ${paginationHints.join(", ")})` + : ""; + if (hasMore) { console.log( - ` Showing commits ${startIndex + 1}-${startIndex + displayCount} of ${totalFetched}+ (press ${textColors.brightYellow("n")} for next batch)`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+${paginationText}`, ); } else { console.log( - ` Showing commits ${startIndex + 1}-${startIndex + displayCount} of ${totalFetched}`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}${paginationText}`, ); } console.log(); From e9565a2d320b7c4c0d52cf36dadf0fe0a3e47719 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 15:27:45 -0700 Subject: [PATCH 10/24] fix: ensure lib directory exists in test scenario generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add explicit lib directory check before creating files to rename • Add error handling for file operations in rename logic • Verify file is actually a file before reading during rename • Fix ENOENT error when generating scenarios with renamed files --- src/cli/commands/test/scenario-generator.ts | 142 ++++++++++++++++---- 1 file changed, 116 insertions(+), 26 deletions(-) diff --git a/src/cli/commands/test/scenario-generator.ts b/src/cli/commands/test/scenario-generator.ts index ab7771e..04d1b26 100644 --- a/src/cli/commands/test/scenario-generator.ts +++ b/src/cli/commands/test/scenario-generator.ts @@ -5,8 +5,8 @@ */ import { spawnSync } from "child_process"; -import { mkdirSync, writeFileSync, existsSync } from "fs"; -import { join } from "path"; +import { mkdirSync, writeFileSync, existsSync, rmSync } from "fs"; +import { join, relative } from "path"; import type { ScenarioName } from "./types.js"; import { loadConfig } from "../../../lib/config/index.js"; @@ -20,9 +20,19 @@ function execGit(sandboxPath: string, args: string[]): void { stdio: ["ignore", "pipe", "pipe"], }); + if (result.error) { + throw new Error( + `Failed to execute git command: git ${args.join(" ")}\n${result.error.message}`, + ); + } + if (result.status !== 0) { + // With encoding: "utf-8", stderr/stdout are strings or null + const stderr = result.stderr || ""; + const stdout = result.stdout || ""; + const errorMessage = stderr || stdout || `Command exited with status ${result.status}`; throw new Error( - `Git command failed: git ${args.join(" ")}\n${result.stderr}`, + `Git command failed: git ${args.join(" ")}\n${errorMessage}`, ); } } @@ -31,6 +41,11 @@ function execGit(sandboxPath: string, args: string[]): void { * Initialize git repository */ function initGit(sandboxPath: string): void { + // Ensure directory exists and is accessible + if (!existsSync(sandboxPath)) { + throw new Error(`Sandbox directory does not exist: ${sandboxPath}`); + } + execGit(sandboxPath, ["init", "--initial-branch=main"]); execGit(sandboxPath, ["config", "user.name", "Test User"]); execGit(sandboxPath, ["config", "user.email", "test@example.com"]); @@ -164,7 +179,7 @@ function generateCommitHistory( /** * Create uncommitted changes */ -function createUncommittedChanges(sandboxPath: string): void { +async function createUncommittedChanges(sandboxPath: string): Promise { // Modified files for (let i = 1; i <= 4; i++) { const filePath = join(sandboxPath, "src", `component-${String.fromCharCode(96 + i)}.ts`); @@ -183,42 +198,113 @@ function createUncommittedChanges(sandboxPath: string): void { ); } + // Ensure lib directory exists + mkdirSync(join(sandboxPath, "lib"), { recursive: true }); + // Deleted files (mark for deletion) + // Create both files first, then commit them together, then remove them + const filesToDelete = []; for (let i = 1; i <= 2; i++) { - const filePath = join(sandboxPath, "lib", `old-util-${i}.js`); + const fileName = `old-util-${i}.js`; + const filePath = join(sandboxPath, "lib", fileName); + const relativePath = `lib/${fileName}`; + + // Create file if it doesn't exist if (!existsSync(filePath)) { writeFileSync(filePath, `// Old utility ${i}\n`); - execGit(sandboxPath, ["add", filePath]); - execGit(sandboxPath, [ - "commit", - "-m", - `chore: add old util ${i}`, - "--no-verify", - ]); + filesToDelete.push({ filePath, relativePath }); + } + } + + // Add and commit all files together + if (filesToDelete.length > 0) { + const relativePaths = filesToDelete.map(f => f.relativePath); + execGit(sandboxPath, ["add", ...relativePaths]); + execGit(sandboxPath, [ + "commit", + "-m", + "chore: add old util files for deletion test", + "--no-verify", + ]); + } + + // Now remove each file (they should all exist at this point) + // Use git rm to stage the deletion, then unstage it to create unstaged deletion + for (const { filePath, relativePath } of filesToDelete) { + // Verify file still exists before git rm + if (existsSync(filePath)) { + // Stage the deletion + execGit(sandboxPath, ["rm", relativePath]); + // Unstage it so it becomes an unstaged change (natural git state) + execGit(sandboxPath, ["reset", "HEAD", "--", relativePath]); } - execGit(sandboxPath, ["rm", filePath]); } // Renamed files - mkdirSync(join(sandboxPath, "lib"), { recursive: true }); const renames = [ ["helpers.ts", "helper-functions.ts"], ["constants.ts", "app-constants.ts"], ]; + // Ensure lib directory exists (in case it was removed) + const libDir = join(sandboxPath, "lib"); + if (!existsSync(libDir)) { + mkdirSync(libDir, { recursive: true }); + } + + // Create all files first, then commit them together + const filesToRename = []; for (const [oldName, newName] of renames) { const oldPath = join(sandboxPath, "lib", oldName); + const oldRelativePath = `lib/${oldName}`; + const newRelativePath = `lib/${newName}`; + if (!existsSync(oldPath)) { writeFileSync(oldPath, `// ${oldName}\n`); - execGit(sandboxPath, ["add", oldPath]); - execGit(sandboxPath, [ - "commit", - "-m", - `chore: add ${oldName}`, - "--no-verify", - ]); + filesToRename.push({ oldPath, oldRelativePath, newRelativePath }); + } + } + + // Add and commit all files together + if (filesToRename.length > 0) { + const relativePaths = filesToRename.map(f => f.oldRelativePath); + execGit(sandboxPath, ["add", ...relativePaths]); + execGit(sandboxPath, [ + "commit", + "-m", + "chore: add files for rename test", + "--no-verify", + ]); + } + + // Now rename each file (they should all exist at this point) + // For unstaged renames, we manually move the file (not git mv) so git sees it as + // an unstaged deletion of old file and unstaged addition of new file + const { readFileSync, unlinkSync, statSync } = await import("fs"); + for (const { oldPath, oldRelativePath, newRelativePath } of filesToRename) { + const newPath = join(sandboxPath, newRelativePath); + // Verify file exists and is a file (not a directory) before renaming + if (existsSync(oldPath)) { + try { + const stats = statSync(oldPath); + if (!stats.isFile()) { + continue; // Skip if it's not a file + } + // Manually move the file (not git mv) to create unstaged rename + const content = readFileSync(oldPath, "utf-8"); + // Ensure new file's directory exists + const newDir = join(sandboxPath, "lib"); + if (!existsSync(newDir)) { + mkdirSync(newDir, { recursive: true }); + } + writeFileSync(newPath, content); + // Delete the old file (this creates an unstaged deletion) + unlinkSync(oldPath); + } catch (error) { + // Skip this file if there's an error reading/writing + continue; + } } - execGit(sandboxPath, ["mv", oldPath, join(sandboxPath, "lib", newName)]); } } @@ -296,9 +382,13 @@ export async function generateScenario( sandboxPath: string, scenario: ScenarioName, ): Promise { + // Ensure sandbox directory exists + mkdirSync(sandboxPath, { recursive: true }); + // Clean and initialize - if (existsSync(join(sandboxPath, ".git"))) { - execGit(sandboxPath, ["rm", "-rf", ".git"]); + const gitDir = join(sandboxPath, ".git"); + if (existsSync(gitDir)) { + rmSync(gitDir, { recursive: true, force: true }); } initGit(sandboxPath); @@ -309,14 +399,14 @@ export async function generateScenario( case "existing-project": // History + changes, no config generateCommitHistory(sandboxPath, 25); - createUncommittedChanges(sandboxPath); + await createUncommittedChanges(sandboxPath); // No config file break; case "with-changes": // History + changes + config generateCommitHistory(sandboxPath, 25); - createUncommittedChanges(sandboxPath); + await createUncommittedChanges(sandboxPath); await copyConfig(sandboxPath); break; From 80d189d05fa2beeb680b07d745ffebaada4e2e53 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:15:56 -0700 Subject: [PATCH 11/24] fix: exclude subject line from commit body extraction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Split commit message by first blank line to separate subject and body • Only return content after blank line as body • Prevents subject line from appearing in body section --- src/cli/commands/shared/git-operations.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cli/commands/shared/git-operations.ts b/src/cli/commands/shared/git-operations.ts index 22af224..676014f 100644 --- a/src/cli/commands/shared/git-operations.ts +++ b/src/cli/commands/shared/git-operations.ts @@ -180,9 +180,17 @@ export function getCommitDetails(hash: string): CommitInfo { const isMerge = parents.length > 1; const date = new Date(dateStr); - // Get body + // Get body - need to exclude the subject line const bodyOutput = execGit(["log", "-1", "--format=%B", hash]); - const body = bodyOutput.trim() || null; + let body: string | null = null; + + if (bodyOutput) { + const trimmed = bodyOutput.trim(); + // Split by first blank line (subject is first line, body is after) + const parts = trimmed.split(/\n\n/, 2); + // If there's content after the first blank line, that's the body + body = parts.length > 1 && parts[1].trim() ? parts[1].trim() : null; + } // Get file stats const statOutput = execGit(["show", "--stat", "--format=", hash]); From a123eec3278b8c366bea9650e87d998340260e62 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:15:59 -0700 Subject: [PATCH 12/24] feat: add toggle functionality for body and files in preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add showBody and showFiles toggle state variables • Update displayCommitDetails to accept visibility parameters • Implement b key to toggle body visibility • Implement f key to toggle files visibility • Reset toggles when viewing new commit or returning to list • Update prompt text to indicate toggle behavior • Fixes issue where pressing b/f caused repeated rendering --- src/cli/commands/preview/index.ts | 16 +++++++--- src/cli/commands/preview/prompts.ts | 46 +++++++++++++++++------------ 2 files changed, 39 insertions(+), 23 deletions(-) diff --git a/src/cli/commands/preview/index.ts b/src/cli/commands/preview/index.ts index 8d3ad12..9c74ea7 100644 --- a/src/cli/commands/preview/index.ts +++ b/src/cli/commands/preview/index.ts @@ -94,15 +94,17 @@ async function previewAction(options: { let exit = false; let viewingDetails = false; let currentDetailCommit: CommitInfo | null = null; + let showBody = true; // Toggle state for body visibility + let showFiles = true; // Toggle state for files visibility while (!exit) { clearTerminal(); if (viewingDetails && currentDetailCommit) { // Detail view - displayCommitDetails(currentDetailCommit); + displayCommitDetails(currentDetailCommit, showBody, showFiles); console.log( - ` ${textColors.white("Press")} ${textColors.brightYellow("b")} ${textColors.white("for body,")} ${textColors.brightYellow("f")} ${textColors.white("for files,")} ${textColors.brightYellow("d")} ${textColors.white("for diff,")} ${textColors.brightYellow("r")} ${textColors.white("to revert,")} ${textColors.brightYellow("←")} ${textColors.white("to go back")}`, + ` ${textColors.white("Press")} ${textColors.brightYellow("b")} ${textColors.white("to toggle body,")} ${textColors.brightYellow("f")} ${textColors.white("to toggle files,")} ${textColors.brightYellow("d")} ${textColors.white("for diff,")} ${textColors.brightYellow("r")} ${textColors.white("to revert,")} ${textColors.brightYellow("←")} ${textColors.white("to go back")}`, ); const action = await waitForDetailAction(); @@ -111,12 +113,16 @@ async function previewAction(options: { case "back": viewingDetails = false; currentDetailCommit = null; + showBody = true; // Reset toggles when going back + showFiles = true; break; case "body": - // Body is already shown in details + // Toggle body visibility + showBody = !showBody; break; case "files": - // Files are already shown in details + // Toggle files visibility + showFiles = !showFiles; break; case "diff": clearTerminal(); @@ -198,6 +204,8 @@ async function previewAction(options: { currentDetailCommit = commit; } viewingDetails = true; + showBody = true; // Reset toggles when viewing new commit + showFiles = true; } else if (action === "previous") { // Move to previous page if (currentPage > 0) { diff --git a/src/cli/commands/preview/prompts.ts b/src/cli/commands/preview/prompts.ts index 3c3dbae..73745b7 100644 --- a/src/cli/commands/preview/prompts.ts +++ b/src/cli/commands/preview/prompts.ts @@ -122,7 +122,11 @@ export function displayCommitList( /** * Display commit details */ -export function displayCommitDetails(commit: CommitInfo): void { +export function displayCommitDetails( + commit: CommitInfo, + showBody: boolean = true, + showFiles: boolean = true, +): void { console.log(); console.log( `${label("detail", "green")} ${textColors.pureWhite("Commit Details")}`, @@ -161,27 +165,31 @@ export function displayCommitDetails(commit: CommitInfo): void { console.log(); } - if (commit.body) { - console.log(` ${textColors.brightWhite("Body:")}`); - const bodyLines = commit.body.split("\n"); - bodyLines.forEach((line) => { - console.log(` ${line}`); - }); - console.log(); - } else { - console.log(` ${textColors.white("Body:")} No body`); - console.log(); + if (showBody) { + if (commit.body) { + console.log(` ${textColors.brightWhite("Body:")}`); + const bodyLines = commit.body.split("\n"); + bodyLines.forEach((line) => { + console.log(` ${line}`); + }); + console.log(); + } else { + console.log(` ${textColors.white("Body:")} No body`); + console.log(); + } } - if (commit.files && commit.files.length > 0) { - console.log(` ${textColors.brightWhite("Changed Files:")}`); - commit.files.slice(0, 20).forEach((file) => { - console.log(` ${file}`); - }); - if (commit.files.length > 20) { - console.log(` ... and ${commit.files.length - 20} more`); + if (showFiles) { + if (commit.files && commit.files.length > 0) { + console.log(` ${textColors.brightWhite("Changed Files:")}`); + commit.files.slice(0, 20).forEach((file) => { + console.log(` ${file}`); + }); + if (commit.files.length > 20) { + console.log(` ... and ${commit.files.length - 20} more`); + } + console.log(); } - console.log(); } } From 50ee6229308ab081d608593073d06380bcf4043a Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:16:02 -0700 Subject: [PATCH 13/24] fix: move config existence check before Clef intro animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Perform early validation before any UI/animation • Check for existing config immediately after project root detection • Only show Clef intro if initialization will proceed • Provides better UX by failing fast with clear error message --- src/cli/commands/init/index.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts index e228add..c368469 100644 --- a/src/cli/commands/init/index.ts +++ b/src/cli/commands/init/index.ts @@ -78,10 +78,7 @@ async function initAction(options: { preset?: string; }): Promise { try { - // Intro: Clef introduces herself - await clef.intro(); - // Screen is now completely clear - + // Early validation: Check prerequisites before any UI // Detect project root const projectRoot = await detectProjectRoot(); if (!projectRoot) { @@ -89,13 +86,17 @@ async function initAction(options: { process.exit(1); } - // Check for existing config + // Check for existing config (must happen before any UI/animation) if (configExists(projectRoot) && !options.force) { Logger.error("Configuration already exists. Use --force to overwrite."); Logger.info(`File: ${path.join(projectRoot, ".labcommitr.config.yaml")}`); process.exit(1); } + // Intro: Clef introduces herself (only if we're proceeding) + await clef.intro(); + // Screen is now completely clear + // Prompts: Clean labels, no cat // Note: @clack/prompts clears each prompt after selection (their default behavior) const presetId = options.preset || (await promptPreset()); From f81536f30f1b001bf6b5bc512319b2f01386f075 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:16:05 -0700 Subject: [PATCH 14/24] feat: make test commands development-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Create separate dev entrypoint (program-dev.ts, index-dev.ts) • Remove test command from production program.ts • Add files field to package.json to exclude test commands • Create .npmignore to exclude dev-only files from published package • Update test command error messages to reference dev CLI • Add dev:cli script for running development CLI • Test commands only available when running from source repository --- .npmignore | 32 +++++++++++++++ package.json | 17 +++++++- src/cli/commands/test/index.ts | 8 ++-- src/cli/program-dev.ts | 75 ++++++++++++++++++++++++++++++++++ src/cli/program.ts | 6 +-- src/index-dev.ts | 46 +++++++++++++++++++++ 6 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 .npmignore create mode 100644 src/cli/program-dev.ts create mode 100644 src/index-dev.ts diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..5a01cf7 --- /dev/null +++ b/.npmignore @@ -0,0 +1,32 @@ +# Exclude development-only files from published package +dist/cli/commands/test/ +dist/cli/program-dev.js +dist/cli/program-dev.d.ts +dist/cli/program-dev.js.map +dist/index-dev.js +dist/index-dev.d.ts +dist/index-dev.js.map + +# Exclude source files +src/ +*.ts +!*.d.ts + +# Exclude development files +.git/ +.sandbox/ +docs/ +*.md +!README.md +!CHANGELOG.md +!TESTING.md +!.changeset/*.md + +# Exclude build artifacts +*.map +tsconfig.json +.prettierrc* + +# Exclude test and development scripts +scripts/ + diff --git a/package.json b/package.json index c184b16..f1e0120 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "format": "pnpm run format:code", "format:ci": "pnpm run format:code", "format:code": "prettier -w \"**/*\" --ignore-unknown --cache", - "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format" + "version": "changeset version && pnpm install --no-frozen-lockfile && pnpm run format", + "dev:cli": "node dist/index-dev.js" }, "type": "module", "bin": { @@ -43,6 +44,20 @@ "typescript": "^5.9.2", "ufo": "^1.6.1" }, + "files": [ + "dist/index.js", + "dist/index.d.ts", + "dist/cli/program.js", + "dist/cli/program.d.ts", + "dist/cli/commands/commit", + "dist/cli/commands/config", + "dist/cli/commands/init", + "dist/cli/commands/preview", + "dist/cli/commands/revert", + "dist/cli/commands/shared", + "dist/cli/utils", + "dist/lib" + ], "publishConfig": { "access": "public" }, diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index 461b6a1..c26de8c 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -85,7 +85,7 @@ async function setupAction(options: { scenario?: string }): Promise { console.log(` ${textColors.brightWhite("Scenario:")} ${scenarioName}`); console.log(); console.log( - ` ${textColors.white("Run commands with:")} ${textColors.brightCyan("lab test shell")}`, + ` ${textColors.white("Run commands with:")} ${textColors.brightCyan("pnpm run dev:cli test shell")}`, ); console.log(); } catch (error: unknown) { @@ -107,7 +107,7 @@ async function resetAction(): Promise { if (!state || !state.scenario) { Logger.error("No active test environment found"); - console.error("\n Run 'lab test setup' first.\n"); + console.error("\n Run 'pnpm run dev:cli test setup' first.\n"); process.exit(1); } @@ -163,7 +163,7 @@ async function statusAction(): Promise { if (!state || !isSandboxValid(sandboxPath)) { console.log(" No active test environment."); console.log(); - console.log(` Run ${textColors.brightCyan("lab test setup")} to create one.`); + console.log(` Run ${textColors.brightCyan("pnpm run dev:cli test setup")} to create one.`); console.log(); return; } @@ -221,7 +221,7 @@ function shellAction(): void { if (!isSandboxValid(sandboxPath)) { Logger.error("No active test environment found"); - console.error("\n Run 'lab test setup' first.\n"); + console.error("\n Run 'pnpm run dev:cli test setup' first.\n"); process.exit(1); } diff --git a/src/cli/program-dev.ts b/src/cli/program-dev.ts new file mode 100644 index 0000000..9ccd61a --- /dev/null +++ b/src/cli/program-dev.ts @@ -0,0 +1,75 @@ +/** + * Commander.js program configuration (Development) + * + * This module sets up the CLI program structure for development use, + * including test commands that are not available in production builds. + * + * This file is only used during development and is not included in + * the published package. + */ + +import { Command } from "commander"; +import { readFileSync } from "fs"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; + +// Commands +import { configCommand } from "./commands/config.js"; +import { initCommand } from "./commands/init/index.js"; +import { commitCommand } from "./commands/commit.js"; +import { previewCommand } from "./commands/preview/index.js"; +import { revertCommand } from "./commands/revert/index.js"; +import { testCommand } from "./commands/test/index.js"; + +// Get package.json for version info +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const packageJsonPath = join(__dirname, "../../package.json"); +const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8")); + +/** + * Development CLI program instance (includes test commands) + */ +export const program = new Command(); + +// Program metadata +program + .name("labcommitr") + .description( + "A CLI tool for standardized git commits with customizable workflows", + ) + .version(packageJson.version, "-v, --version", "Display version number") + .helpOption("-h, --help", "Display help information"); + +// Global options (future: --verbose, --no-emoji, etc.) +// program.option('--verbose', 'Enable verbose logging'); + +// Register commands (including test command for development) +program.addCommand(configCommand); +program.addCommand(initCommand); +program.addCommand(commitCommand); +program.addCommand(previewCommand); +program.addCommand(revertCommand); +program.addCommand(testCommand); + +// Customize help text +program.addHelpText( + "after", + ` +Examples: + $ labcommitr init Initialize config in current project + $ lab commit Create a standardized commit (interactive) + $ lab preview Browse commit history + $ lab revert Revert a commit using commit workflow + $ lab config show Display current configuration + $ lab test setup Set up test environment + $ lab test shell Open shell in test environment + +Documentation: + https://github.com/labcatr/labcommitr#readme +`, +); + +// Error on unknown commands +program.showSuggestionAfterError(true); + diff --git a/src/cli/program.ts b/src/cli/program.ts index 2ab5ba9..8550e0d 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -19,7 +19,7 @@ import { initCommand } from "./commands/init/index.js"; import { commitCommand } from "./commands/commit.js"; import { previewCommand } from "./commands/preview/index.js"; import { revertCommand } from "./commands/revert/index.js"; -import { testCommand } from "./commands/test/index.js"; +// Note: testCommand is only available in dev entrypoint (program-dev.ts) // Get package.json for version info const __filename = fileURLToPath(import.meta.url); @@ -50,7 +50,7 @@ program.addCommand(initCommand); program.addCommand(commitCommand); program.addCommand(previewCommand); program.addCommand(revertCommand); -program.addCommand(testCommand); +// Note: testCommand is only registered in dev entrypoint (program-dev.ts) // Customize help text program.addHelpText( @@ -62,8 +62,6 @@ Examples: $ lab preview Browse commit history $ lab revert Revert a commit using commit workflow $ lab config show Display current configuration - $ lab test setup Set up test environment - $ lab test shell Open shell in test environment Documentation: https://github.com/labcatr/labcommitr#readme diff --git a/src/index-dev.ts b/src/index-dev.ts new file mode 100644 index 0000000..5715b5c --- /dev/null +++ b/src/index-dev.ts @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +/** + * Labcommitr CLI Entry Point (Development) + * + * This file serves as the development entry point for the labcommitr CLI tool. + * It includes test commands that are not available in production builds. + * + * This file is only used during development and is not included in + * the published package. + */ + +import { program } from "./cli/program-dev.js"; +import { handleCliError } from "./cli/utils/error-handler.js"; + +/** + * Main CLI execution (Development) + * Parses process arguments and executes the appropriate command + */ +async function main(): Promise { + try { + await program.parseAsync(process.argv); + } catch (error: unknown) { + // Check if error is about too many arguments (likely unquoted message/body) + if ( + error instanceof Error && + error.message.includes("too many arguments") + ) { + console.error("\n✗ Error: Too many arguments"); + console.error("\n Your message or body contains spaces and needs to be quoted."); + console.error("\n Fix: Use quotes around values with spaces:"); + console.error(` • Message: -m "your message here"`); + console.error(` • Body: -b "your body here"`); + console.error( + ` • Example: lab commit -t feat -m "add feature" -b "detailed description"\n`, + ); + process.exit(1); + } + handleCliError(error); + process.exit(1); + } +} + +// Execute CLI +main(); + From 5731b7714a64e2b7c23abeb469cd63f2c43dcb41 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:16:09 -0700 Subject: [PATCH 15/24] docs: update documentation for dev-only test commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Update README.md to show dev CLI usage for test commands • Update TESTING.md with all examples using pnpm run dev:cli • Add clear notes that test commands are development-only • Include alternative usage with node dist/index-dev.js • Update all workflow examples and troubleshooting sections --- README.md | 24 +++++++---- TESTING.md | 115 +++++++++++++++++++++++++++++++---------------------- 2 files changed, 84 insertions(+), 55 deletions(-) diff --git a/README.md b/README.md index bc4dffe..0e3d1df 100644 --- a/README.md +++ b/README.md @@ -187,25 +187,33 @@ See [`docs/CONFIG_SCHEMA.md`](docs/CONFIG_SCHEMA.md) for complete configuration ### Testing Environment +**Note:** Test commands are **development-only** and are not available in the published package. They can only be used when running from the source repository. + For safe testing of Labcommitr commands without affecting your real repository, use the built-in testing environment: ```bash # Set up test environment (default scenario) -lab test setup +pnpm run dev:cli test setup # Open shell in test environment -lab test shell +pnpm run dev:cli test shell -# Run commands normally +# Run commands normally (use regular lab command in shell) lab commit lab preview lab revert # Reset environment for another test -lab test reset +pnpm run dev:cli test reset # Clean up -lab test clean +pnpm run dev:cli test clean +``` + +**Alternative:** You can also use `node dist/index-dev.js` instead of `pnpm run dev:cli`: +```bash +node dist/index-dev.js test setup +node dist/index-dev.js test shell ``` **Available Scenarios:** @@ -218,13 +226,13 @@ lab test clean **Examples:** ```bash # Set up specific scenario -lab test setup --scenario with-history +pnpm run dev:cli test setup --scenario with-history # List all scenarios -lab test list-scenarios +pnpm run dev:cli test list-scenarios # Check current status -lab test status +pnpm run dev:cli test status ``` See [`TESTING.md`](TESTING.md) for complete testing documentation. diff --git a/TESTING.md b/TESTING.md index 59a4757..67b1bfb 100644 --- a/TESTING.md +++ b/TESTING.md @@ -2,16 +2,18 @@ A simple, flexible testing environment for testing Labcommitr commands in isolated git repositories. +**Note:** Test commands are **development-only** and are not available in the published package. They can only be used when running from the source repository. + ## Quick Start ```bash # Set up test environment (default scenario) -lab test setup +pnpm run dev:cli test setup # Open shell in test environment -lab test shell +pnpm run dev:cli test shell -# Run commands normally +# Run commands normally (use regular lab command in shell) lab commit lab preview lab revert @@ -20,6 +22,12 @@ lab revert exit ``` +**Alternative:** You can also use `node dist/index-dev.js` instead of `pnpm run dev:cli`: +```bash +node dist/index-dev.js test setup +node dist/index-dev.js test shell +``` + --- ## Table of Contents @@ -72,7 +80,7 @@ Scenarios represent different git repository states. Each scenario is designed t **Setup:** ```bash -lab test setup --scenario existing-project +pnpm run dev:cli test setup --scenario existing-project ``` --- @@ -95,7 +103,7 @@ lab test setup --scenario existing-project **Setup:** ```bash -lab test setup --scenario with-changes +pnpm run dev:cli test setup --scenario with-changes ``` **Default Scenario:** This is the default scenario if none is specified. @@ -123,7 +131,7 @@ lab test setup --scenario with-changes **Setup:** ```bash -lab test setup --scenario with-history +pnpm run dev:cli test setup --scenario with-history ``` --- @@ -147,7 +155,7 @@ lab test setup --scenario with-history **Setup:** ```bash -lab test setup --scenario with-merge +pnpm run dev:cli test setup --scenario with-merge ``` --- @@ -170,7 +178,7 @@ lab test setup --scenario with-merge **Setup:** ```bash -lab test setup --scenario with-conflicts +pnpm run dev:cli test setup --scenario with-conflicts ``` --- @@ -181,18 +189,20 @@ lab test setup --scenario with-conflicts Set up test environment with specified scenario. +**Note:** This command is only available in development. Use `pnpm run dev:cli test setup` or `node dist/index-dev.js test setup`. + **Options:** - `-s, --scenario ` - Scenario name (default: `with-changes`) **Examples:** ```bash # Set up default scenario (with-changes) -lab test setup +pnpm run dev:cli test setup # Set up specific scenario -lab test setup --scenario existing-project -lab test setup --scenario with-history -lab test setup --scenario with-merge +pnpm run dev:cli test setup --scenario existing-project +pnpm run dev:cli test setup --scenario with-history +pnpm run dev:cli test setup --scenario with-merge ``` **What it does:** @@ -208,9 +218,11 @@ lab test setup --scenario with-merge Open interactive shell in test environment. +**Note:** This command is only available in development. Use `pnpm run dev:cli test shell` or `node dist/index-dev.js test shell`. + **Examples:** ```bash -lab test shell +pnpm run dev:cli test shell ``` **What it does:** @@ -219,7 +231,7 @@ lab test shell - You can run commands normally (`lab commit`, `lab preview`, etc.) - Exit with `exit` or `Ctrl+D` -**Note:** Make sure you've run `lab test setup` first. +**Note:** Make sure you've run `pnpm run dev:cli test setup` first. --- @@ -227,9 +239,11 @@ lab test shell Reset current scenario to initial state. +**Note:** This command is only available in development. Use `pnpm run dev:cli test reset` or `node dist/index-dev.js test reset`. + **Examples:** ```bash -lab test reset +pnpm run dev:cli test reset ``` **What it does:** @@ -244,9 +258,11 @@ lab test reset Remove test environment completely. +**Note:** This command is only available in development. Use `pnpm run dev:cli test clean` or `node dist/index-dev.js test clean`. + **Examples:** ```bash -lab test clean +pnpm run dev:cli test clean ``` **What it does:** @@ -260,9 +276,11 @@ lab test clean Show current test environment status. +**Note:** This command is only available in development. Use `pnpm run dev:cli test status` or `node dist/index-dev.js test status`. + **Examples:** ```bash -lab test status +pnpm run dev:cli test status ``` **What it shows:** @@ -278,9 +296,11 @@ lab test status List all available scenarios. +**Note:** This command is only available in development. Use `pnpm run dev:cli test list-scenarios` or `node dist/index-dev.js test list-scenarios`. + **Examples:** ```bash -lab test list-scenarios +pnpm run dev:cli test list-scenarios ``` **What it shows:** @@ -296,10 +316,10 @@ lab test list-scenarios ```bash # Set up environment -lab test setup --scenario with-changes +pnpm run dev:cli test setup --scenario with-changes # Enter test environment -lab test shell +pnpm run dev:cli test shell # Test commit lab commit @@ -307,8 +327,8 @@ lab commit # Exit and reset for another test exit -lab test reset -lab test shell +pnpm run dev:cli test reset +pnpm run dev:cli test shell lab commit -t feat -m "quick commit" ``` @@ -318,10 +338,10 @@ lab commit -t feat -m "quick commit" ```bash # Set up environment with history -lab test setup --scenario with-history +pnpm run dev:cli test setup --scenario with-history # Enter test environment -lab test shell +pnpm run dev:cli test shell # Test preview lab preview @@ -335,10 +355,10 @@ lab preview ```bash # Set up environment -lab test setup --scenario with-history +pnpm run dev:cli test setup --scenario with-history # Enter test environment -lab test shell +pnpm run dev:cli test shell # Test revert lab revert @@ -347,8 +367,8 @@ lab revert # Test revert with merge commits exit -lab test setup --scenario with-merge -lab test shell +pnpm run dev:cli test setup --scenario with-merge +pnpm run dev:cli test shell lab revert # [select merge commit] # [select parent] @@ -361,10 +381,10 @@ lab revert ```bash # Set up environment -lab test setup --scenario existing-project +pnpm run dev:cli test setup --scenario existing-project # Enter test environment -lab test shell +pnpm run dev:cli test shell # Test init workflow lab init @@ -382,10 +402,10 @@ lab commit ```bash # Set up environment -lab test setup --scenario with-changes +pnpm run dev:cli test setup --scenario with-changes # Enter test environment -lab test shell +pnpm run dev:cli test shell # Real workflow lab commit -t feat -s api -m "add new endpoint" @@ -405,10 +425,10 @@ lab preview ```bash # Set up environment -lab test setup --scenario with-conflicts +pnpm run dev:cli test setup --scenario with-conflicts # Enter test environment -lab test shell +pnpm run dev:cli test shell # Test abort lab revert --abort @@ -416,8 +436,8 @@ lab revert --abort # Reset and test continue exit -lab test reset -lab test shell +pnpm run dev:cli test reset +pnpm run dev:cli test shell # [manually resolve conflicts] lab revert --continue # [revert completed] @@ -433,7 +453,7 @@ lab revert --continue **Solution:** ```bash -lab test setup +pnpm run dev:cli test setup ``` --- @@ -445,10 +465,10 @@ lab test setup **Solution:** ```bash # List available scenarios -lab test list-scenarios +pnpm run dev:cli test list-scenarios # Use correct scenario name -lab test setup --scenario with-changes +pnpm run dev:cli test setup --scenario with-changes ``` --- @@ -458,14 +478,14 @@ lab test setup --scenario with-changes **Problem:** Project needs to be built before testing. **Solution:** -The `lab test setup` command automatically builds the project if needed. If you encounter build issues: +The `pnpm run dev:cli test setup` command automatically builds the project if needed. If you encounter build issues: ```bash # Build manually pnpm run build # Then set up test environment -lab test setup +pnpm run dev:cli test setup ``` --- @@ -476,7 +496,7 @@ lab test setup **Solution:** - Sandbox is always at: `.sandbox/test/` -- Use `lab test shell` to enter it +- Use `pnpm run dev:cli test shell` to enter it - Or navigate manually: `cd .sandbox/test/` --- @@ -488,19 +508,20 @@ lab test setup **Solution:** ```bash # Clean and recreate -lab test clean -lab test setup --scenario +pnpm run dev:cli test clean +pnpm run dev:cli test setup --scenario ``` --- ## Tips -1. **Use `lab test shell`** - Easiest way to test commands in the environment -2. **Check status** - Use `lab test status` to see current state -3. **Quick reset** - Use `lab test reset` for fast iteration +1. **Use `pnpm run dev:cli test shell`** - Easiest way to test commands in the environment +2. **Check status** - Use `pnpm run dev:cli test status` to see current state +3. **Quick reset** - Use `pnpm run dev:cli test reset` for fast iteration 4. **Test workflows** - Chain multiple commands to test real-world usage 5. **Switch scenarios** - Use different scenarios for different testing needs +6. **Dev-only access** - Test commands are only available when running from source repository --- From 4597502e853a19599746193715be683901f14d3f Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:22:27 -0700 Subject: [PATCH 16/24] chore: add changeset for preview body extraction fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Document fix for excluding subject line from commit body • Patch version bump for bug fix --- .changeset/fix-preview-body-extraction.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/fix-preview-body-extraction.md diff --git a/.changeset/fix-preview-body-extraction.md b/.changeset/fix-preview-body-extraction.md new file mode 100644 index 0000000..2634cdb --- /dev/null +++ b/.changeset/fix-preview-body-extraction.md @@ -0,0 +1,11 @@ +--- +"@labcatr/labcommitr": patch +--- + +fix: exclude subject line from commit body extraction + +- Split commit message by first blank line to separate subject and body +- Only return content after blank line as body in preview command +- Prevents subject line from appearing in body section +- Fixes incorrect display where commit subject was shown as part of body + From 8a8d29c1101382affe8fd039d94b41fbbc6423a3 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:22:30 -0700 Subject: [PATCH 17/24] chore: add changeset for preview toggle functionality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Document new toggle feature for body and files in preview • Minor version bump for new feature • Includes b and f key toggles for visibility control --- .changeset/add-preview-toggle-functionality.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/add-preview-toggle-functionality.md diff --git a/.changeset/add-preview-toggle-functionality.md b/.changeset/add-preview-toggle-functionality.md new file mode 100644 index 0000000..6151f8a --- /dev/null +++ b/.changeset/add-preview-toggle-functionality.md @@ -0,0 +1,14 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: add toggle functionality for body and files in preview + +- Add toggle state for body and files visibility in commit detail view +- Implement `b` key to toggle body visibility on/off +- Implement `f` key to toggle files visibility on/off +- Reset toggles when viewing new commit or returning to list +- Update prompt text to indicate toggle behavior +- Fixes issue where pressing `b`/`f` caused repeated rendering +- Improves UX by allowing users to hide/show sections as needed + From 43df95c21b06dd487179f2f865bdf88ada2b2df6 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:22:33 -0700 Subject: [PATCH 18/24] chore: add changeset for init config check timing fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Document fix for config check before Clef animation • Patch version bump for UX improvement • Prevents unnecessary animation when config exists --- .changeset/fix-init-config-check-timing.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/fix-init-config-check-timing.md diff --git a/.changeset/fix-init-config-check-timing.md b/.changeset/fix-init-config-check-timing.md new file mode 100644 index 0000000..34c7891 --- /dev/null +++ b/.changeset/fix-init-config-check-timing.md @@ -0,0 +1,12 @@ +--- +"@labcatr/labcommitr": patch +--- + +fix: move config existence check before Clef intro animation + +- Perform early validation before any UI/animation in init command +- Check for existing config immediately after project root detection +- Only show Clef intro animation if initialization will proceed +- Provides better UX by failing fast with clear error message +- Prevents unnecessary animation when config already exists + From c435d38f8bc24184545c8cc8254a065b9a929019 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:49:52 -0700 Subject: [PATCH 19/24] feat: add init command alias and improve help text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add 'i' alias for init command for faster access • Update help examples to use 'lab' instead of 'labcommitr' consistently • Add concise examples showing both full commands and aliases • Add note clarifying both 'lab' and 'labcommitr' can be used • Update README to document init|i alias • Remove duplicate pagination text from preview and revert commands • Improve help text clarity and consistency across all commands --- .../add-command-aliases-and-help-improvements.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/add-command-aliases-and-help-improvements.md diff --git a/.changeset/add-command-aliases-and-help-improvements.md b/.changeset/add-command-aliases-and-help-improvements.md new file mode 100644 index 0000000..6e8463b --- /dev/null +++ b/.changeset/add-command-aliases-and-help-improvements.md @@ -0,0 +1,14 @@ +--- +"@labcatr/labcommitr": minor +--- + +feat: add init command alias and improve help text + +- Add `i` alias for `init` command for faster access +- Update help examples to use `lab` instead of `labcommitr` consistently +- Add concise examples showing both full commands and aliases +- Add note clarifying both `lab` and `labcommitr` can be used +- Update README to document `init|i` alias +- Remove duplicate pagination text from preview and revert commands +- Improve help text clarity and consistency across all commands + From ac6232957f58350505952b8809edf6e114bc5f48 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:50:00 -0700 Subject: [PATCH 20/24] refactor: improve help text examples and consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Update examples to use 'lab' instead of 'labcommitr' consistently • Add concise examples showing both full commands and aliases • Add note clarifying both 'lab' and 'labcommitr' can be used • Remove unnecessary examples to keep help text focused --- src/cli/program-dev.ts | 11 ++++++----- src/cli/program.ts | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/cli/program-dev.ts b/src/cli/program-dev.ts index 9ccd61a..cad45f5 100644 --- a/src/cli/program-dev.ts +++ b/src/cli/program-dev.ts @@ -57,14 +57,15 @@ program.addHelpText( "after", ` Examples: - $ labcommitr init Initialize config in current project - $ lab commit Create a standardized commit (interactive) - $ lab preview Browse commit history - $ lab revert Revert a commit using commit workflow - $ lab config show Display current configuration + $ lab init Initialize config in current project + $ lab commit Create a standardized commit (interactive) + $ lab i (alias for init) + $ lab c (alias for commit) $ lab test setup Set up test environment $ lab test shell Open shell in test environment +Note: You can use either 'lab' or 'labcommitr' to run commands. + Documentation: https://github.com/labcatr/labcommitr#readme `, diff --git a/src/cli/program.ts b/src/cli/program.ts index 8550e0d..b10f0bc 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -57,11 +57,12 @@ program.addHelpText( "after", ` Examples: - $ labcommitr init Initialize config in current project - $ lab commit Create a standardized commit (interactive) - $ lab preview Browse commit history - $ lab revert Revert a commit using commit workflow - $ lab config show Display current configuration + $ lab init Initialize config in current project + $ lab commit Create a standardized commit (interactive) + $ lab i (alias for init) + $ lab c (alias for commit) + +Note: You can use either 'lab' or 'labcommitr' to run commands. Documentation: https://github.com/labcatr/labcommitr#readme From af75ab5420a2db2c87e1ddfcb78716931e02513c Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:50:02 -0700 Subject: [PATCH 21/24] feat: add 'i' alias for init command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add 'i' alias to init command for faster access • Update README to document init|i alias usage • Maintains consistency with existing commit|c alias --- README.md | 1 + src/cli/commands/init/index.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 0e3d1df..6b36474 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ Initialize Labcommitr configuration in your project. This creates a `.labcommitr **Usage:** ```bash lab init [options] +lab i [options] # Short alias ``` **Options:** diff --git a/src/cli/commands/init/index.ts b/src/cli/commands/init/index.ts index c368469..e07f433 100644 --- a/src/cli/commands/init/index.ts +++ b/src/cli/commands/init/index.ts @@ -62,6 +62,7 @@ function configExists(projectRoot: string): boolean { */ export const initCommand = new Command("init") .description("Initialize labcommitr configuration in your project") + .alias("i") .option("-f, --force", "Overwrite existing configuration") .option( "--preset ", From 0ddbe05f357023d3db432dce051b9e05fa611658 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 17:50:04 -0700 Subject: [PATCH 22/24] fix: remove duplicate pagination text from commit lists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Remove redundant pagination hints from preview command commit list • Remove redundant pagination hints from revert command commit list • Navigation hints are already displayed separately below the list • Prevents duplicate information and improves readability --- src/cli/commands/preview/prompts.ts | 14 ++------------ src/cli/commands/revert/prompts.ts | 14 ++------------ 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/cli/commands/preview/prompts.ts b/src/cli/commands/preview/prompts.ts index 73745b7..651a91d 100644 --- a/src/cli/commands/preview/prompts.ts +++ b/src/cli/commands/preview/prompts.ts @@ -80,24 +80,14 @@ export function displayCommitList( // Pagination info const endIndex = startIndex + displayCount; console.log(); - const paginationHints: string[] = []; - if (hasPreviousPage) { - paginationHints.push(`${textColors.brightYellow("p")} for previous batch`); - } - if (hasMorePages) { - paginationHints.push(`${textColors.brightYellow("n")} for next batch`); - } - const paginationText = paginationHints.length > 0 - ? ` (press ${paginationHints.join(", ")})` - : ""; if (hasMore) { console.log( - ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+${paginationText}`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+`, ); } else { console.log( - ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}${paginationText}`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}`, ); } console.log(); diff --git a/src/cli/commands/revert/prompts.ts b/src/cli/commands/revert/prompts.ts index 332baba..3c3165f 100644 --- a/src/cli/commands/revert/prompts.ts +++ b/src/cli/commands/revert/prompts.ts @@ -87,24 +87,14 @@ export function displayRevertCommitList( // Pagination info const endIndex = startIndex + displayCount; console.log(); - const paginationHints: string[] = []; - if (hasPreviousPage) { - paginationHints.push(`${textColors.brightYellow("p")} for previous batch`); - } - if (hasMorePages) { - paginationHints.push(`${textColors.brightYellow("n")} for next batch`); - } - const paginationText = paginationHints.length > 0 - ? ` (press ${paginationHints.join(", ")})` - : ""; if (hasMore) { console.log( - ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+${paginationText}`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}+`, ); } else { console.log( - ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}${paginationText}`, + ` Showing commits ${startIndex + 1}-${endIndex} of ${totalFetched}`, ); } console.log(); From e7b1d00bc7f7a2a70bd26e03e3a5deacdd7bb2b6 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 18:00:45 -0700 Subject: [PATCH 23/24] docs: add preview and revert command documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Add comprehensive documentation for preview command • Add comprehensive documentation for revert command • Update table of contents to include new commands • Document interactive features and keyboard shortcuts • Include usage examples and options for both commands --- README.md | 144 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6b36474..f8b6e6f 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ A CLI tool for creating standardized Git commits with customizable workflows and - [commit](#commit) - [init](#init) - [config](#config) + - [preview](#preview) + - [revert](#revert) - [Configuration](#configuration) - [Development & Testing](#development--testing) - [Contributing](#contributing) @@ -166,6 +168,114 @@ lab config show --path /path/to/project --- +### preview + +Browse and inspect commit history interactively without modifying your repository. + +**Usage:** +```bash +lab preview [options] +``` + +**Options:** +- `-l, --limit ` - Maximum commits to fetch (default: 50, max: 100) +- `-b, --branch ` - Branch to preview (default: current branch) + +**Examples:** +```bash +# Browse commits on current branch +lab preview + +# Preview commits from a specific branch +lab preview --branch main + +# Limit the number of commits fetched +lab preview --limit 25 +``` + +**Interactive Features:** +- **Commit List View:** + - Navigate through commits with pagination (10 per page) + - Press `0-9` to view details of a specific commit + - Press `n` to load next batch (if available) + - Press `p` to go to previous batch (if available) + - Press `Esc` to exit + +- **Commit Detail View:** + - View full commit information (hash, author, date, subject, body, files) + - Press `b` to toggle body visibility + - Press `f` to toggle changed files visibility + - Press `d` to view diff + - Press `r` to revert this commit (switches to revert command) + - Press `←` or `Esc` to go back to list + - Press `?` for help + +**Notes:** +- Read-only operation - does not modify your repository +- Fetches commits in batches of 50 (up to 100 total) +- Works on current branch by default +- No configuration file required (read-only operation) + +--- + +### revert + +Revert a commit using the project's commit workflow. Select a commit interactively and create a revert commit following your project's commit message format. + +**Usage:** +```bash +lab revert [options] +``` + +**Options:** +- `-l, --limit ` - Maximum commits to fetch (default: 50, max: 100) +- `-b, --branch ` - Branch to revert from (default: current branch) +- `--no-edit` - Skip commit message editing (use Git's default revert message) +- `--continue` - Continue revert after conflict resolution +- `--abort` - Abort revert in progress + +**Examples:** +```bash +# Interactive revert (uses commit workflow) +lab revert + +# Revert from specific branch +lab revert --branch main + +# Revert without using commit workflow +lab revert --no-edit + +# Continue after resolving conflicts +lab revert --continue + +# Abort a revert in progress +lab revert --abort +``` + +**Interactive Features:** +- **Commit Selection:** + - Browse commits with pagination (10 per page) + - Press `0-9` to select a commit to revert + - Press `n` to load next batch (if available) + - Press `p` to go to previous batch (if available) + - Press `Esc` to cancel + +- **Revert Workflow:** + - Shows commit details before reverting + - For merge commits, prompts to select parent + - Uses your project's commit workflow to create revert commit message + - Allows editing commit message before finalizing + - Handles conflicts with `--continue` and `--abort` options + +**Notes:** +- Requires `.labcommitr.config.yaml` (unless using `--no-edit`) +- Creates a new commit that undoes the selected commit +- For merge commits, you'll be prompted to select which parent to revert to +- If conflicts occur, resolve them manually and use `--continue` +- Use `--abort` to cancel a revert in progress + +--- + ## Configuration Labcommitr uses a `.labcommitr.config.yaml` file in your project root. The configuration file supports: @@ -242,14 +352,38 @@ See [`TESTING.md`](TESTING.md) for complete testing documentation. ## Contributing -Contributions are welcome! Please ensure your commits follow the project's commit message format (which you can set up using `lab init`). +Contributions are welcome! We appreciate your interest in improving Labcommitr. + +### How to Contribute + +Before implementing any changes, please follow this process: + +1. **Open an issue** describing your proposed change + - Clearly explain what you want to add or modify + - Provide **use cases** that demonstrate the value of your change + - Include **justification** for why this change would benefit users + - Discuss potential implementation approaches if relevant -For development guidelines, see [`docs/DEVELOPMENT_GUIDELINES.md`](docs/DEVELOPMENT_GUIDELINES.md). +2. **Wait for review and discussion** + - Maintainers will review your proposal + - Community feedback is encouraged + - We'll discuss whether the change aligns with project goals + +3. **Proceed with implementation** (if approved) + - Once the proposal is accepted, you can start implementing + - Follow the project's development guidelines + - Ensure your commits follow the project's commit message format (you can set up using `lab init`) + +### Development Guidelines + +For detailed development guidelines, coding standards, and architecture information, see [`docs/DEVELOPMENT_GUIDELINES.md`](docs/DEVELOPMENT_GUIDELINES.md). + +### Questions? + +If you have questions or need clarification, feel free to open a discussion or issue. --- ## Planned Features -The following commands are planned but not yet implemented: - -- `lab go [...message]` - Quickly submit a commit of the specified type with a message. If a message is not specified, a generic one will be generated (fast but not recommended for production use). +_No planned features at this time. Check back later or open an issue to suggest new features!_ From 32a7e66984572c03445214aa237f745dd5cd0142 Mon Sep 17 00:00:00 2001 From: Satanshu Mishra Date: Sat, 29 Nov 2025 18:10:58 -0700 Subject: [PATCH 24/24] fix: Token Update --- .github/workflows/label.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index a493bf4..c80e759 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -15,5 +15,5 @@ jobs: steps: - uses: actions/labeler@v4 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + repo-token: "${{ secrets.LAB_ACTIONS_TOKEN }}" sync-labels: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 41d8fb1..244b0d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,6 @@ jobs: title: "[ci] release" env: # Uses built-in GITHUB_TOKEN (automatically available, no secret needed) - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.LAB_ACTIONS_TOKEN }} # Needs access to publish to npm NPM_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }}