diff --git a/.agents-docs/AGENTS-architecture.md b/.agents-docs/AGENTS-architecture.md index 65896a5..5c5aba2 100644 --- a/.agents-docs/AGENTS-architecture.md +++ b/.agents-docs/AGENTS-architecture.md @@ -6,7 +6,7 @@ ``` plan2code/ ├── src/ # Source workflow prompts (9 markdown files) -│ └── plan2code-3b-review-references/ # Reference files for review skill +│ └── plan2code-review-references/ # Reference files for review skill │ ├── verification-protocol.md # Deep verification + confidence calibration │ ├── dimensions.md # 11 dimensions with detailed checklists │ └── false-positives.md # Known false-positive patterns @@ -17,6 +17,9 @@ plan2code/ │ ├── src/ # TypeScript source │ │ └── prompts/ # Internal AI prompt templates (no char limit) │ └── dist/ # Built output (tsup) +├── src/statusline-claude/ # Claude CLI status line (Node.js, zero deps, single file) +│ ├── statusline.js # Self-contained: config, git, formatters, render +│ └── statusline-config.json # Default config template ├── scripts/ # Development scripts │ └── validate-char-count.js # Pre-commit character count validator ├── dist/ # Generated distribution files (auto-generated) @@ -41,6 +44,7 @@ plan2code/ | `scripts/validate-char-count.js` | Pre-commit validator ensuring all source prompts ≤ 11,000 chars | | `version.json` | Version metadata (name, version, description) | | `QUICK-REFERENCE.md` | User quick-reference card | +| `src/statusline-claude/` | Claude CLI status bar (included in `A` Install All + dev tools; also via Custom → S) | ## Workflow Prompts (in `src/`) @@ -53,7 +57,7 @@ plan2code/ | `plan2code-1b-revise-plan.md` | 1b | Mid-implementation revisions | | `plan2code-2-document.md` | 2 | Create implementation specs | | `plan2code-3-implement.md` | 3 | Execute implementation (phase by phase) | -| `plan2code-3b-review.md` | 3b | Post-implementation comprehensive review | +| `plan2code-review.md` | review | Post-implementation comprehensive review | | `plan2code-4-finalize.md` | 4 | Validate, summarize, feedback, archive (7 steps) | ## Naming Convention @@ -71,11 +75,35 @@ Examples: Some workflows use companion reference files for depth that exceeds the 11k char limit. The orchestrator (main workflow file) loads them via `Read` directives during execution. -**Pattern:** `src/-references/` (e.g., `plan2code-3b-review-references/`) +**Pattern:** `src/-references/` (e.g., `plan2code-review-references/`) **How the installer handles them:** - **Skill-directory platforms** (Claude Code, Agents, Crush, Devin): reference files are nested as `/references/`. Read paths use canonical `references/.md`. -- **Flat-file platforms** (Windsurf, Cursor, Copilot, Continue): reference files are placed as a sibling directory. The installer rewrites Read paths to the sibling directory name (e.g., `plan2code-3b-review-references/.md`). +- **Flat-file platforms** (Windsurf, Cursor, Copilot, Continue): reference files are placed as a sibling directory. The installer rewrites Read paths to the sibling directory name (e.g., `plan2code-review-references/.md`). - **TOML platforms** (Gemini CLI): reference files are skipped — TOML embeds content inline, so Read directives won't resolve. The orchestrator's inline fallback text covers this. -Reference files are NOT subject to the 11,000 character limit. Currently only the review workflow (Step 3b) uses this pattern — it serves as the POC for potential adoption by other workflows. +Reference files are NOT subject to the 11,000 character limit. Currently only the review workflow uses this pattern — it serves as the POC for potential adoption by other workflows. + +## Status Line + +Optional Claude Code status bar living in `src/statusline-claude/`. Three-line bar (icon + content per line) showing model, project, branch, uncommitted diff stats, session duration + cost, context window usage, and plan/quota usage. + +**Design constraints:** +- **Zero runtime dependencies** — `statusline.js` is self-contained (config loader, git helpers, formatters, render). Copied verbatim to `~/.claude/plan2code-statusline.js` on install; no bundler step. +- **Stdin-driven** — all data comes from Claude Code's stdin JSON (`model`, `workspace`, `context_window`, `rate_limits`, `cost`). No API calls, no auth, no background processes. +- **Silent failure** — outer `try/catch` around `main()` plus `process.exit(0)` on missing stdin guarantees the script never crashes the CLI. All git ops are timeout-bounded (1.5s) and non-git workspaces short-circuit via `fs.existsSync('.git')`. +- **Atomic settings writes** — installer writes `~/.claude/settings.json` via temp file + rename so a crash never leaves the file truncated. +- **Custom-config respect** — installer detects non-plan2code `statusLine` entries, prompts before replacing, and backs up to `statusline-previous.json`. Uninstall only removes `settings.statusLine` if it points to the plan2code bundle. + +**Layout:** + +``` +src/statusline-claude/ +├── statusline.js # Self-contained: config, git, formatters, render +├── statusline-config.json # Default config template +└── README.md # User docs: install, config, debugging +``` + +**Adaptive plan-usage display:** the formatter auto-selects between `5h/7d` rate-limit percentages (Pro/Max/Teams — when `rate_limits` present in stdin) and `Nk in · Nk out` session-token counts (Bedrock/Vertex/PAYG — when `rate_limits` absent). Segment is hidden when neither shape is available. + +**Installer integration** lives in `install.js` under the `STATUS LINE INSTALLATION` section (`installStatusLine`, `uninstallStatusLine`). Included in `A` (Install All + dev tools); also available individually via Custom → `S`. diff --git a/.agents-docs/AGENTS-development-commands.md b/.agents-docs/AGENTS-development-commands.md index e6538a2..202a7c0 100644 --- a/.agents-docs/AGENTS-development-commands.md +++ b/.agents-docs/AGENTS-development-commands.md @@ -25,8 +25,9 @@ cd plan2code-metrics && npm run build # Build the CLI | Option | Action | |--------|--------| -| `I` | Install Plan2Code for all platforms + loop CLI | -| `U` | Uninstall Plan2Code files + loop CLI (confirmation required) | +| `I` | Install Plan2Code workflow prompts for all platforms, plus `plan2code-loop` CLI | +| `A` | Everything in `I` plus `plan2code-bot`, `plan2code-metrics` (dev tools), and Claude Code status line | +| `U` | Uninstall Plan2Code files: prompts + `plan2code-loop` + `plan2code-metrics` + `plan2code-bot` + Claude Code status line (confirmation required) | | `C` | Open CUSTOM sub-menu | | `Q` | Quit | @@ -37,6 +38,8 @@ cd plan2code-metrics && npm run build # Build the CLI | `L` | Show local (per-project) install instructions | | `O` | Install plan2code-loop CLI only | | `M` | Install plan2code-metrics CLI only | +| `S` | Install Claude Code status line only | +| `B` | Install plan2code-bot CLI only | | `Q` | Return to main menu | ## How the Installer Works @@ -58,7 +61,7 @@ cd plan2code-metrics && npm run build # Build the CLI | VS Code Copilot | `.prompt.md` | — | — | YAML frontmatter | | Codeium | `.md` | — | — | YAML frontmatter | | Claude Code (Skills) | `SKILL.md` in subdir | `.claude/skills//` | `~/.claude/skills//` | YAML frontmatter + `disable-model-invocation: true` | -| Agent Skills (Amp · Devin · Gemini CLI · OpenCode) | `SKILL.md` in subdir | `.agents/skills//` | `~/.agents/skills//` | YAML frontmatter (no disable flag) | +| Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed) | `SKILL.md` in subdir | `.agents/skills//` | `~/.agents/skills//` | YAML frontmatter (no disable flag) | | Crush | `SKILL.md` in subdir | — (global only) | `~/.config/crush/skills//` (Unix) / `%LOCALAPPDATA%\crush\skills\\` (Windows) | YAML frontmatter | | Gemini CLI (TOML) | `.toml` | `.gemini/commands/` | `~/.gemini/commands/` | None (TOML fields: `description`, `prompt`) | | Pi (pi.dev) | `.md` | `.pi/prompts/` | `~/.pi/agent/prompts/` | YAML frontmatter (`description`) | diff --git a/AGENTS.md b/AGENTS.md index d4515c6..54fea51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,10 +1,10 @@ # AGENTS.md -This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, Devin, and other AI coding assistants when working with code in this repository. +This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, Devin, Zed, and other AI coding assistants when working with code in this repository. ## Project Overview -Plan2Code is a structured 4-step workflow methodology for AI-assisted software development. It provides prompt templates that can be installed globally or per-project for various AI coding tools (Claude Code, Cursor, Copilot, Continue, Windsurf, Codeium, Devin). +Plan2Code is a structured 4-step workflow methodology for AI-assisted software development. It provides prompt templates that can be installed globally or per-project for various AI coding tools (Claude Code, Cursor, Copilot, Continue, Windsurf, Codeium, Devin, Zed). **Version:** Check `version.json` for current version **Author:** Justin Parker @@ -32,6 +32,12 @@ Recursive self-improvement toolchain (`plan2code-metrics/`) for collecting run m Details: [Plan2Code Metrics](./.agents-docs/AGENTS-plan2code-metrics.md) +## Plan2Code Status Line (Claude CLI) + +Optional CLI status bar (`src/statusline-claude/`) for Claude Code. Displays model, project, branch, context usage, usage stats (rate limits or token counts), git diff stats, and duration. Reads data directly from Claude Code's stdin JSON — no API calls, no auth, no background processes. Included in `Install All + dev tools` (`A`); also available via `install.js` Custom → S (opt-in). + +Details: [Architecture](./.agents-docs/AGENTS-architecture.md) (see Status Line section) + ## Development Commands Build commands, installer menu options, how the installer generates platform-specific files, platform format table, and how to edit workflow prompts. diff --git a/CHANGELOG.md b/CHANGELOG.md index 800d0a4..2f2b034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,54 @@ All notable changes to Plan2Code will be documented in this file. +## v1.15.2 + +### 🔧 Changed + +- **Review workflow renamed** — `/plan2code-3b-review` → `/plan2code-review` + - Removed `3b` step-number prefix; review is now a standalone utility workflow (like `init` and `quick-task`) + - Source file: `src/plan2code-review.md` (was `plan2code-3b-review.md`) + - Reference directory: `src/plan2code-review-references/` (was `plan2code-3b-review-references/`) + - Updated all docs, installer config, architecture docs, and cross-references + +## v1.15.1 + +### 🔧 Changed + +- **Zed agent support** — Added Zed to all agent support documentation + - `README.md` Supported Platforms list + - `AGENTS.md` intro and Project Overview + - `.agents-docs/AGENTS-development-commands.md` Platform-Specific File Formats table + - `install.js` Agent Skills platform label + - `src/plan2code-init.md` AGENTS.md template text + +## v1.15.0 + +### ✨ Added + +- **Claude CLI status line** — Persistent three-line status bar for Claude Code, displaying model, project, git branch, uncommitted diff stats, session duration, context window usage bar, and plan/quota usage. Reads all data from Claude Code's stdin JSON — no API calls, no auth, no background processes. + - `src/statusline-claude/statusline.js` — self-contained: stdin parsing, config loader, ANSI formatting, orchestration + - `src/statusline-claude/statusline-config.json` — distributed default config + - `src/statusline-claude/README.md` — user docs: install, config, usage modes, troubleshooting + - **Planny mascot icons** on each line (`╭─╮`, `│★│`, `╰─╯`) in brand colors, with monochrome fallback + - **Context window bar** (12-cell `▰▱`) with configurable `autocompactBuffer` (default 33000 tokens) so the percentage reflects *usable* context, not the raw window + - **Adaptive plan usage segment** — auto-detects data shape: + - Pro/Max/Teams (rate_limits present) → `5h: NN% · 7d: NN%` + - Bedrock / Vertex / PAYG (no rate_limits) → `NNk in · NNk out` session tokens + - Segment hidden when neither is available + - **Real uncommitted diff stats** via `git diff HEAD --numstat` → `+NN -NN` + - **Color-coded thresholds** — green / yellow / red for context bar and rate limits + - **Separator line** rendered at fixed 55-character width for consistent alignment + - **Session cost display** — Estimated session cost shown next to duration (e.g. `49m ($4.62)`) via new `items.sessionCost` config (default on); reads `cost.total_cost_usd` from Claude Code stdin; hidden when zero or unavailable + - **Compact mode redesigned** — `"compact": true` now renders two content lines (no mascot icons, no separator) instead of one cramped single line. The single-line variant truncated in narrow terminals — the exact case compact mode was meant to help. Default remains `false`. + - **Silent failure** on all errors — never crashes, never blocks the CLI; 1.5s per-call timeouts on git operations; non-git workspaces short-circuit without spawning subprocesses +- **Installer integration** — Status line included in `Install All + dev tools` (`A`); also available via `Custom → S` (opt-in). + - Copies `statusline.js` verbatim to `~/.claude/plan2code-statusline.js` (single-file design, no bundling) + - Registers in `~/.claude/settings.json` under `statusLine` via atomic temp-file + rename write + - Preserves existing `statusline-config.json` on reinstall + - Detects non-plan2code custom `statusLine` configs — prompts before replacing; auto-backs up to `statusline-previous.json` + - Uninstall (`U`) removes bundled script + `settings.json` entry; preserves `statusline-config.json`. Only removes `settings.statusLine` if it points to the plan2code bundle — non-plan2code entries are left intact. + ## v1.14.0 ### ✨ Added diff --git a/QUICK-REFERENCE.md b/QUICK-REFERENCE.md index f8302ca..b66f89e 100644 --- a/QUICK-REFERENCE.md +++ b/QUICK-REFERENCE.md @@ -7,11 +7,11 @@ | Init | /plan2code-init | None | AGENTS.md file | | Update | /plan2code-init-update | AGENTS.md | Updated AGENTS.md | | 0 | /plan2code-quick-task | Requirements | Conversational plan | +| review | /plan2code-review | Scope guidance | Review findings + fixes | | 1 | /plan2code-1-plan | Requirements | PLAN-CONVERSATION-.md + PLAN-DRAFT-.md | | 1b | /plan2code-1b-revise-plan | Specs + changes | Updated specs | | 2 | /plan2code-2-document | PLAN-DRAFT.md | overview.md + Phase files | | 3 | /plan2code-3-implement | overview.md | Implemented code | -| 3b | /plan2code-3b-review | Scope guidance | Review findings + fixes | | 4 | /plan2code-4-finalize | overview.md | Archived specs | ## File Structure diff --git a/README.md b/README.md index 5770f25..fbe6186 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ A structured 4-step workflow for developing features and projects with AI assist | `/plan2code-init` | Generate AGENTS.md file for new/existing projects | | `/plan2code-init-update` | Update AGENTS.md with new learnings from coding sessions | | `/plan2code-quick-task` | Small, quick tasks that don't need full workflow | +| `/plan2code-review` | Post-phase code review (after key features or milestones)| | `/plan2code-1-plan` | Starting a new feature (full planning) | | `/plan2code-1b-revise-plan` | Requirements change mid-implementation | | `/plan2code-2-document` | After planning, create implementation specs | | `/plan2code-3-implement` | Execute implementation (one phase per conversation) | -| `/plan2code-3b-review` | Post-phase code review (after key features or milestones)| | `/plan2code-4-finalize` | All phases complete, ready to archive | **Key Rules:** @@ -68,6 +68,7 @@ The install script requires **Node.js** (v14 or later). If you don't have Node.j - Amp - OpenCode - Devin +- Zed ### Install via npx (Recommended — No Clone Required) @@ -134,6 +135,12 @@ SELECT OPTION (I, U, C, Q) [I]: --- +## Status Line — Claude CLI (Optional) + +A persistent three-line status bar for Claude Code showing model, project, git branch, uncommitted diff stats, context usage, and plan quota. Included in `node install.js` → `A` (Install All + dev tools), or available individually via Custom → Status Line. See [src/statusline-claude/README.md](src/statusline-claude/README.md) for details. + +--- + ## Important: Start Fresh Conversations **Start a new conversation/chat session before each step.** This includes: @@ -233,7 +240,7 @@ The `overview.md` includes a "Parallel Execution Groups" section that identifies --- -### Step 3b: Review Mode 🔬 (Optional) +### Review Mode 🔬 (Optional) **Purpose:** Comprehensive post-implementation code review with adaptive scope and spec compliance checking. @@ -282,7 +289,7 @@ After running `node install.js`, use the slash commands directly in your AI tool /plan2code-1-plan # Start planning a new feature /plan2code-2-document # Create implementation docs from plan /plan2code-3-implement # Begin/continue implementation -/plan2code-3b-review # Post-phase code review (optional) +/plan2code-review # Post-phase code review (optional) /plan2code-4-finalize # Wrap up after all phases complete ``` @@ -422,7 +429,7 @@ The `[/]` status enables parallel execution - multiple agents can work on differ | Step 1 (Plan) | None (describe your feature/project) | | Step 2 (Document) | `specs//PLAN-DRAFT-.md` or planning conversation | | Step 3 (Implement) | `specs//overview.md` (auto-detects phase) | -| Step 3b (Review) | Optional: scope guidance (e.g., "review last 2 phases", "just the auth module", "whole PR"). Auto-detects changes and specs if no scope given. | +| Review (Optional) | Scope guidance (e.g., "review last 2 phases", "just the auth module", "whole PR"). Auto-detects changes and specs if no scope given. | | Step 4 (Finalize) | `specs//overview.md` | --- diff --git a/docs/index.html b/docs/index.html index 6a47e00..e747022 100644 --- a/docs/index.html +++ b/docs/index.html @@ -988,7 +988,7 @@
-
3b
+
review
🔬

Review

@@ -1112,7 +1112,7 @@
  • /plan2code-1-plan
  • /plan2code-2-document
  • /plan2code-3-implement (per phase)
  • -
  • /plan2code-3b-review (optional)
  • +
  • /plan2code-review (optional)
  • /plan2code-4-finalize

  • diff --git a/install.js b/install.js index 9e31669..732475e 100644 --- a/install.js +++ b/install.js @@ -167,11 +167,12 @@ const SOURCE_PROMPTS = [ description: 'Execute implementation phase by phase' }, { - source: 'plan2code-3b-review.md', - stepNumber: '3b', + source: 'plan2code-review.md', + stepNumber: 'review', name: 'review', displayName: 'Review Mode', - description: 'Comprehensive post-implementation review with spec compliance checking' + description: 'Comprehensive post-implementation review with spec compliance checking', + isUtility: true }, { source: 'plan2code-4-finalize.md', @@ -195,12 +196,19 @@ function generateSkillName(prompt) { return generateFilename(prompt, '').replace(/--+/g, '-'); } +// Helper function to generate step label for descriptions +function generateStepLabel(prompt) { + if (prompt.stepNumber === 'init') return 'Init'; + if (prompt.stepNumber === 'review') return 'Review'; + return `Step ${prompt.stepNumber}`; +} + // Helper function to generate YAML frontmatter for SKILL.md files function generateSkillHeader(prompt, disableModelInvocation = false) { const lines = [ '---', `name: ${generateSkillName(prompt)}`, - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - user-initiated workflow step. Do not invoke autonomously."`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - user-initiated workflow step. Do not invoke autonomously."`, ]; if (disableModelInvocation) lines.push('disable-model-invocation: true'); lines.push('---'); @@ -209,7 +217,7 @@ function generateSkillHeader(prompt, disableModelInvocation = false) { // Helper function to generate complete .toml file content for Gemini CLI function generateTomlContent(prompt, sourceContent) { - const stepLabel = prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`; + const stepLabel = generateStepLabel(prompt); const desc = `Plan2Code ${stepLabel}: ${prompt.displayName} - user-initiated ${prompt.description || 'workflow step'}`; return `description = "${desc}"\nprompt = '''\n${sourceContent}\n'''\n`; } @@ -235,7 +243,7 @@ const LOCAL_DESTINATIONS = [ header: (prompt) => [ '---', 'mode: agent', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -245,7 +253,7 @@ const LOCAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.agent.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -256,7 +264,7 @@ const LOCAL_DESTINATIONS = [ header: (prompt) => [ '---', `name: ${prompt.name}`, - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -266,7 +274,7 @@ const LOCAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -276,7 +284,7 @@ const LOCAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -286,7 +294,7 @@ const LOCAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -296,7 +304,7 @@ const LOCAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -306,7 +314,7 @@ const LOCAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -344,7 +352,7 @@ const GLOBAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -361,7 +369,7 @@ const GLOBAL_DESTINATIONS = [ header: (prompt) => [ '---', `name: ${prompt.name}`, - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -371,7 +379,7 @@ const GLOBAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -382,7 +390,7 @@ const GLOBAL_DESTINATIONS = [ header: (prompt) => [ '---', 'agent: agent', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -392,7 +400,7 @@ const GLOBAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -402,7 +410,7 @@ const GLOBAL_DESTINATIONS = [ filePattern: (prompt) => generateFilename(prompt, '.md'), header: (prompt) => [ '---', - `description: "Plan2Code ${prompt.stepNumber === 'init' ? 'Init' : `Step ${prompt.stepNumber}`}: ${prompt.displayName} - ${prompt.description}"`, + `description: "Plan2Code ${generateStepLabel(prompt)}: ${prompt.displayName} - ${prompt.description}"`, '---' ].join('\n') }, @@ -413,10 +421,10 @@ const GLOBAL_DESTINATIONS = [ header: (prompt) => generateSkillHeader(prompt, true), }, { - name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode)', + name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed)', dir: '.agents/skills', type: 'skill', - header: (prompt) => generateSkillHeader(prompt, false), + header: (prompt) => generateSkillHeader(prompt, true), }, { name: 'Crush', @@ -460,10 +468,6 @@ function getCrushSkillsDir() { // Helper function to get Agent Skills global directory based on platform function getAgentSkillsDir() { const homedir = os.homedir(); - if (process.platform === 'win32') { - const localAppData = process.env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local'); - return path.join(localAppData, 'agents', 'skills'); - } return path.join(homedir, '.agents', 'skills'); } @@ -562,15 +566,13 @@ const INSTALL_TARGETS = [ icon: '◉ ' }, { - name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode)', + name: 'Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed)', id: 'agent-skills', dir: getAgentSkillsDir, sourceDir: '.agents/skills', type: 'skill', filePattern: /^plan2code-/, - displayPath: process.platform === 'win32' - ? '%LOCALAPPDATA%\\agents\\skills' - : '~/.agents/skills', + displayPath: '~/.agents/skills', icon: '◉ ' }, { @@ -713,19 +715,8 @@ function deleteDirectory(dirPath) { if (!fs.existsSync(dirPath)) { return; } - try { - const files = fs.readdirSync(dirPath); - for (const file of files) { - const filePath = path.join(dirPath, file); - const stat = fs.statSync(filePath); - if (stat.isDirectory()) { - deleteDirectory(filePath); - } else { - fs.unlinkSync(filePath); - } - } - fs.rmdirSync(dirPath); + fs.rmSync(dirPath, { recursive: true, force: true }); } catch (err) { console.error(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Could not delete directory ${dirPath}: ${err.message}`); } @@ -866,7 +857,7 @@ function inlineReferenceContent(sourceContent, srcRefDir) { } /** - * Copy a reference directory (e.g., plan2code-3b-review-references/) to a destination. + * Copy a reference directory (e.g., plan2code-review-references/) to a destination. * Used by syncPrompts() to distribute reference files alongside orchestrator files. */ function copyReferenceDirectory(srcRefDir, destRefDir, stats, quiet = false) { @@ -970,7 +961,7 @@ function syncPrompts(quiet = false) { continue; } - // Detect reference directory (e.g., plan2code-3b-review-references/) + // Detect reference directory (e.g., plan2code-review-references/) const refDirName = prompt.source.replace(/\.md$/, '-references'); const srcRefDir = path.join(SRC_DIR, refDirName); const hasRefDir = fs.existsSync(srcRefDir); @@ -1102,13 +1093,12 @@ function cleanLegacyTargets() { /** * Execute installation */ -function install(targets = null) { +async function install(targets = null) { const projectRoot = __dirname; const homeDir = os.homedir(); const targetList = targets || getTargets(); let totalInstalled = 0; - let totalSkipped = 0; let totalErrors = 0; displaySectionHeader(' INSTALLING FILES '); @@ -1286,9 +1276,7 @@ function install(targets = null) { console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${statusColor}Status:${COLORS.RESET} ${statusColor}${statusText}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Files Installed: ${COLORS.GREEN}${totalInstalled}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - if (totalSkipped > 0) { - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Files Skipped: ${COLORS.YELLOW}${totalSkipped}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - } + if (totalErrors > 0) { console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} Errors: ${COLORS.RED}${totalErrors}${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); } @@ -1559,12 +1547,12 @@ function runInteractive() { rl.close(); const loopResult = installPlan2CodeLoop(); console.log(''); - const installResult = install(); + const installResult = await install(); const exitCode = loopResult !== 0 ? loopResult : installResult; process.exit(exitCode); } - // A path — Install Everything (prompts + loop + bot + metrics) + // A path — Install Everything (prompts + loop + bot + metrics + Claude status line) if (input === 'A') { rl.close(); const loopResult = installPlan2CodeLoop(); @@ -1573,8 +1561,10 @@ function runInteractive() { console.log(''); const metricsResult = installPlan2CodeMetrics(); console.log(''); - const installResult = install(); - const exitCode = loopResult !== 0 ? loopResult : installResult !== 0 ? installResult : botResult !== 0 ? botResult : metricsResult; + const statusLineResult = await installStatusLine(); + console.log(''); + const installResult = await install(); + const exitCode = loopResult !== 0 ? loopResult : botResult !== 0 ? botResult : metricsResult !== 0 ? metricsResult : statusLineResult !== 0 ? statusLineResult : installResult; process.exit(exitCode); } @@ -1599,7 +1589,8 @@ function runInteractive() { const loopResult = uninstallPlan2CodeLoop(); const metricsResult = uninstallPlan2CodeMetrics(); const botResult = uninstallPlan2CodeBot(); - const exitCode = uninstallResult !== 0 ? uninstallResult : loopResult !== 0 ? loopResult : metricsResult !== 0 ? metricsResult : botResult; + const statusLineResult = uninstallStatusLine(); + const exitCode = uninstallResult !== 0 ? uninstallResult : loopResult !== 0 ? loopResult : metricsResult !== 0 ? metricsResult : botResult !== 0 ? botResult : statusLineResult; process.exit(exitCode); } else { console.log(`\n${COLORS.YELLOW}${SYMBOLS.WARNING} CANCELLED${COLORS.RESET}\n`); @@ -1617,11 +1608,12 @@ function runInteractive() { console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}L.${COLORS.RESET} ${COLORS.BLUE}LOCAL${COLORS.RESET} Show local (project) install instructions`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}O.${COLORS.RESET} ${COLORS.GREEN}LOOP CLI${COLORS.RESET} Install plan2code-loop CLI only`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}M.${COLORS.RESET} ${COLORS.GREEN}METRICS${COLORS.RESET} Install plan2code-metrics CLI only`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}S.${COLORS.RESET} ${COLORS.GREEN}STATUS${COLORS.RESET} Install Claude Code status line`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}B.${COLORS.RESET} ${COLORS.GREEN}BOT${COLORS.RESET} Install plan2code-bot CLI only`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}Q.${COLORS.RESET} ${COLORS.DIM}BACK${COLORS.RESET} Return to main menu`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(`${COLORS.CYAN}╚════════════════════════════════════════════════════════════════╝${COLORS.RESET}`); console.log(''); - const customAnswer = await question(`${COLORS.CYAN}${SYMBOLS.SELECT} SELECT OPTION${COLORS.RESET} (L, O, M, B, Q) [Q]: `); + const customAnswer = await question(`${COLORS.CYAN}${SYMBOLS.SELECT} SELECT OPTION${COLORS.RESET} (L, O, M, S, B, Q) [Q]: `); const customInput = customAnswer.trim().toUpperCase() || 'Q'; // C > L — show local install instructions @@ -1644,6 +1636,13 @@ function runInteractive() { process.exit(result); } + // C > S — install status line + if (customInput === 'S') { + rl.close(); + const result = await installStatusLine(); + process.exit(result); + } + // C > B — install bot CLI only if (customInput === 'B') { rl.close(); @@ -1678,7 +1677,7 @@ function runInteractive() { // PLAN2CODE-LOOP INSTALLATION // ============================================================================ -const { execSync, spawn } = require('child_process'); +const { execSync } = require('child_process'); /** * Build plan2code-loop package @@ -1870,27 +1869,13 @@ function removeGlobalSymlink() { for (const file of filesToRemove) { try { fs.lstatSync(file.path); - // File exists, try to remove it - try { - // Try rmdirSync first for junctions/symlinks - fs.rmdirSync(file.path); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } catch (rmdirErr) { - // Fall back to unlinkSync for regular files - try { - fs.unlinkSync(file.path); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } catch (unlinkErr) { - // Last resort: rmSync - fs.rmSync(file.path, { force: true, recursive: true }); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } - } + fs.rmSync(file.path, { force: true, recursive: true }); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; } catch (err) { - // File doesn't exist, skip it + if (err.code !== 'ENOENT') { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${err.message}`); + } } } @@ -1904,6 +1889,7 @@ function removeGlobalSymlink() { return false; } } + /** * Install plan2code-loop (build + symlink) */ @@ -2119,23 +2105,13 @@ function removeMetricsGlobalSymlink() { for (const file of filesToRemove) { try { fs.lstatSync(file.path); - try { - fs.rmdirSync(file.path); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } catch (rmdirErr) { - try { - fs.unlinkSync(file.path); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } catch (unlinkErr) { - fs.rmSync(file.path, { force: true, recursive: true }); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } - } + fs.rmSync(file.path, { force: true, recursive: true }); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; } catch (err) { - // File doesn't exist, skip it + if (err.code !== 'ENOENT') { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${err.message}`); + } } } @@ -2191,6 +2167,234 @@ function uninstallPlan2CodeMetrics() { return 0; } +// ============================================================================ +// STATUS LINE INSTALLATION +// ============================================================================ + +/** + * Status line source layout — single-file design, no bundling step. + * `statusline.js` is self-contained (config loader, formatters, entry point) + * and copied verbatim to ~/.claude/plan2code-statusline.js on install. + */ +function getStatusLineSrcDir() { + return path.join(__dirname, 'src', 'statusline-claude'); +} + +/** + * Install status line (copy + configure settings). + * Async because it may prompt when a custom (non-plan2code) statusLine exists. + */ +async function installStatusLine({ quiet = false } = {}) { + if (!quiet) { + displaySectionHeader('CLAUDE STATUS LINE', '[ INSTALL ]'); + } else { + console.log(` ${COLORS.CYAN}${SYMBOLS.ACTIVE} INSTALLING${COLORS.RESET} Claude Status Line...`); + } + + const srcDir = getStatusLineSrcDir(); + if (!fs.existsSync(srcDir)) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} src/statusline-claude directory not found`); + return 1; + } + + const homeDir = os.homedir(); + const claudeDir = path.join(homeDir, '.claude'); + + try { + fs.mkdirSync(claudeDir, { recursive: true }); + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Cannot create ~/.claude/: ${error.message}`); + return 1; + } + + // Copy script directly from src (no bundling — statusline.js is self-contained) + const statuslineDest = path.join(claudeDir, 'plan2code-statusline.js'); + const configDest = path.join(claudeDir, 'statusline-config.json'); + + try { + fs.copyFileSync(path.join(srcDir, 'statusline.js'), statuslineDest); + if (!quiet) console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Installed: ~/.claude/plan2code-statusline.js`); + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to copy script: ${error.message}`); + return 1; + } + + // Preserve existing user config + if (!fs.existsSync(configDest)) { + try { + fs.copyFileSync(path.join(srcDir, 'statusline-config.json'), configDest); + if (!quiet) console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: ~/.claude/statusline-config.json`); + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to copy config: ${error.message}`); + return 1; + } + } else { + if (!quiet) console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Preserved existing: ~/.claude/statusline-config.json${COLORS.RESET}`); + } + + // Make executable on non-Windows + if (process.platform !== 'win32') { + try { + fs.chmodSync(statuslineDest, 0o755); + } catch {} + } + + // Configure Claude Code settings.json + let settingsOk = false; + const settingsPath = path.join(claudeDir, 'settings.json'); + const expectedCommand = `node "${statuslineDest.replace(/\\/g, '/')}"`; + + try { + let settings = {}; + if (fs.existsSync(settingsPath)) { + settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + } + + // Detect existing statusLine — skip prompt if ours or absent + if (settings.statusLine && settings.statusLine.command) { + const existingCmd = settings.statusLine.command; + const isPlan2Code = existingCmd.includes('plan2code-statusline'); + + if (!isPlan2Code) { + // Custom statusLine detected — prompt before overwriting + console.log(` ${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} Existing statusLine detected in settings.json:`); + console.log(` ${COLORS.DIM} ${existingCmd}${COLORS.RESET}`); + + const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise(resolve => { + rl2.question(` ${COLORS.CYAN}Replace with Plan2Code status line?${COLORS.RESET} (Y/n): `, resolve); + }); + rl2.close(); + + if (answer.trim().toLowerCase() === 'n') { + console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Skipped — scripts installed but settings.json unchanged${COLORS.RESET}`); + console.log(''); + return 0; + } + + // Backup existing config before replacing + const backupPath = path.join(claudeDir, 'statusline-previous.json'); + fs.writeFileSync(backupPath, JSON.stringify({ statusLine: settings.statusLine }, null, 2)); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Backed up existing config: ~/.claude/statusline-previous.json`); + } + } + + settings.statusLine = { + type: 'command', + command: expectedCommand, + }; + // Atomic write: temp file + rename so a crash/power-loss never leaves + // settings.json truncated. + const tmpSettings = settingsPath + '.tmp'; + fs.writeFileSync(tmpSettings, JSON.stringify(settings, null, 2)); + fs.renameSync(tmpSettings, settingsPath); + if (!quiet) console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Configured: ~/.claude/settings.json (statusLine)`); + settingsOk = true; + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to update settings.json: ${error.message}`); + } + + // Clean up legacy filenames from previous installs + for (const legacy of ['statusline.js', 'plan-usage.js', 'plan2code-plan-usage.js', 'statusline-cache.json']) { + const legacyPath = path.join(claudeDir, legacy); + try { + if (fs.existsSync(legacyPath)) { + fs.unlinkSync(legacyPath); + if (!quiet) console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Removed legacy: ${legacy}${COLORS.RESET}`); + } + } catch {} + } + + if (settingsOk) { + if (!quiet) { + console.log(''); + console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} Claude Status Line installed successfully!${COLORS.RESET}`); + console.log(''); + console.log(`${COLORS.CYAN}Usage:${COLORS.RESET}`); + console.log(` ${COLORS.DIM}Restart Claude Code to see the status bar${COLORS.RESET}`); + console.log(` ${COLORS.DIM}Config: ~/.claude/statusline-config.json${COLORS.RESET}`); + console.log(''); + } else { + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Claude Status Line → ${COLORS.DIM}~/.claude/plan2code-statusline.js${COLORS.RESET}`); + } + } else { + console.log(''); + console.log(` ${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} Scripts installed but settings.json not updated.`); + console.log(` ${COLORS.DIM}Add statusLine config to ~/.claude/settings.json manually.${COLORS.RESET}`); + console.log(''); + } + + return settingsOk ? 0 : 1; +} + +/** + * Uninstall status line + */ +function uninstallStatusLine() { + displaySectionHeader('CLAUDE STATUS LINE', '[ UNINSTALL ]'); + + const homeDir = os.homedir(); + const claudeDir = path.join(homeDir, '.claude'); + const settingsPath = path.join(claudeDir, 'settings.json'); + + // Remove statusLine from settings.json (only if it points to our bundle) + try { + if (fs.existsSync(settingsPath)) { + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); + if (settings.statusLine) { + const existingCmd = settings.statusLine.command || ''; + if (existingCmd.includes('plan2code-statusline')) { + delete settings.statusLine; + const tmpSettings = settingsPath + '.tmp'; + fs.writeFileSync(tmpSettings, JSON.stringify(settings, null, 2)); + fs.renameSync(tmpSettings, settingsPath); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed statusLine from settings.json`); + } else { + console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Preserved non-plan2code statusLine in settings.json${COLORS.RESET}`); + console.log(` ${COLORS.DIM} ${existingCmd}${COLORS.RESET}`); + } + } else { + console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No statusLine config in settings.json${COLORS.RESET}`); + } + } + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to update settings.json: ${error.message}`); + } + + // Remove script files (preserve config) + const filesToRemove = [ + { path: path.join(claudeDir, 'plan2code-statusline.js'), name: 'plan2code-statusline.js' }, + { path: path.join(claudeDir, 'plan2code-plan-usage.js'), name: 'plan2code-plan-usage.js' }, + { path: path.join(claudeDir, 'statusline.js'), name: 'statusline.js (legacy)' }, + { path: path.join(claudeDir, 'plan-usage.js'), name: 'plan-usage.js (legacy)' }, + { path: path.join(claudeDir, 'statusline-cache.json'), name: 'statusline-cache.json' }, + ]; + + let removedCount = 0; + for (const file of filesToRemove) { + try { + if (fs.existsSync(file.path)) { + fs.unlinkSync(file.path); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; + } + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${error.message}`); + } + } + + if (removedCount === 0) { + console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No status line files found${COLORS.RESET}`); + } + + console.log(` ${COLORS.DIM}${SYMBOLS.INFO} Preserved: statusline-config.json${COLORS.RESET}`); + console.log(''); + console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} Status line uninstalled${COLORS.RESET}`); + console.log(''); + + return 0; +} + // ============================================================================ // PLAN2CODE-BOT INSTALLATION // ============================================================================ @@ -2362,23 +2566,13 @@ function removeBotGlobalSymlink() { for (const file of filesToRemove) { try { fs.lstatSync(file.path); - try { - fs.rmdirSync(file.path); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } catch (rmdirErr) { - try { - fs.unlinkSync(file.path); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } catch (unlinkErr) { - fs.rmSync(file.path, { force: true, recursive: true }); - console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); - removedCount++; - } - } + fs.rmSync(file.path, { force: true, recursive: true }); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; } catch (err) { - // File doesn't exist, skip it + if (err.code !== 'ENOENT') { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to remove ${file.name}: ${err.message}`); + } } } diff --git a/package.json b/package.json index b52ed7b..1c064bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plan2code", - "version": "1.14.0", + "version": "1.15.2", "private": true, "bin": { "plan2code": "./install.js" diff --git a/plan2code-metrics/README.md b/plan2code-metrics/README.md index 17da052..b69c577 100644 --- a/plan2code-metrics/README.md +++ b/plan2code-metrics/README.md @@ -6,7 +6,7 @@ Recursive self-improvement toolchain for plan2code contributors. Collects metric ```bash # Install (one-time, from the plan2code root) -node install.js # → "I" (Install All) includes metrics +node install.js # → "A" (Install All + dev tools) includes metrics # → or "M" (Metrics only) under the CUSTOM sub-menu # After finishing any project spec (steps 1-4): @@ -78,7 +78,7 @@ From the plan2code repo root: node install.js ``` -Choose **I** (Install All) to install prompts, loop CLI, and metrics together. Or choose **C** (Custom) then **M** (Metrics) to install just the metrics CLI. +Choose **A** (Install All + dev tools) to install prompts, loop CLI, bot, metrics, and status line together. Or choose **C** (Custom) then **M** (Metrics) to install just the metrics CLI. The installer handles `npm install`, `npm run build`, and global linking automatically. After install, `plan2code-metrics` is available from any directory. diff --git a/scripts/validate-char-count.js b/scripts/validate-char-count.js index e864aca..0c373e6 100644 --- a/scripts/validate-char-count.js +++ b/scripts/validate-char-count.js @@ -7,7 +7,7 @@ const CHAR_LIMIT = 11000; const srcDir = path.join(__dirname, '..', 'src'); // Note: readdirSync is non-recursive, so files in subdirectories like -// plan2code-3b-review-references/ are automatically excluded from +// plan2code-review-references/ are automatically excluded from // the character limit check. Reference files have no char limit. const files = fs.readdirSync(srcDir) .filter(f => f.startsWith('plan2code-') && f.endsWith('.md')) diff --git a/src/plan2code-3-implement.md b/src/plan2code-3-implement.md index 982e5c9..dbeed4c 100644 --- a/src/plan2code-3-implement.md +++ b/src/plan2code-3-implement.md @@ -204,7 +204,7 @@ On user "approved": 4. **If more phases:** "NEXT STEP: Start NEW conversation and run: `/plan2code-3-implement`" 5. **If final phase:** "NEXT STEP: Start NEW conversation and run: `/plan2code-4-finalize`" 6. Mention `/plan2code-1b-revise-plan` option -7. Suggest: "Optional: run `/plan2code-3b-review` for a post-phase code review -- recommended after key features or milestones." +7. Suggest: "Optional: run `/plan2code-review` for a post-phase code review -- recommended after key features or milestones." Planny (continuing): ``` diff --git a/src/plan2code-init.md b/src/plan2code-init.md index 8130baf..39a4d2a 100644 --- a/src/plan2code-init.md +++ b/src/plan2code-init.md @@ -10,7 +10,7 @@ Start all CREATE AGENTS MODE responses with '💡' ╰───╯ ``` -Analyze this codebase and create `AGENTS.md` to guide future AI coding agents (Claude Code, Codex, Gemini CLI, Devin, etc.). +Analyze this codebase and create `AGENTS.md` to guide future AI coding agents (Claude Code, Codex, Gemini CLI, Devin, Zed, etc.). ## Content @@ -31,7 +31,7 @@ Prefix the file with: ``` # AGENTS.md -This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, Devin, and other AI coding assistants when working with code in this repository. +This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, Devin, Zed, and other AI coding assistants when working with code in this repository. ``` --- diff --git a/src/plan2code-3b-review-references/dimensions.md b/src/plan2code-review-references/dimensions.md similarity index 99% rename from src/plan2code-3b-review-references/dimensions.md rename to src/plan2code-review-references/dimensions.md index fcac42b..b3ef761 100644 --- a/src/plan2code-3b-review-references/dimensions.md +++ b/src/plan2code-review-references/dimensions.md @@ -1,5 +1,5 @@ # Dimension Checklists -> Part of plan2code-3b-review — loaded during Step 3 (Analyze). +> Part of plan2code-review — loaded during Step 3 (Analyze). Non-obvious check items, anti-patterns, and "don't flag" guidance for each review dimension. Focus on what LLMs commonly miss. diff --git a/src/plan2code-3b-review-references/false-positives.md b/src/plan2code-review-references/false-positives.md similarity index 98% rename from src/plan2code-3b-review-references/false-positives.md rename to src/plan2code-review-references/false-positives.md index cfc93a7..44d0cb5 100644 --- a/src/plan2code-3b-review-references/false-positives.md +++ b/src/plan2code-review-references/false-positives.md @@ -1,5 +1,5 @@ # False-Positive Catalog -> Part of plan2code-3b-review — loaded before presenting findings in Step 3 (Analyze). +> Part of plan2code-review — loaded before presenting findings in Step 3 (Analyze). Check every finding against this catalog before presenting it. These patterns represent common categories of false findings that waste reviewer and developer time. Each entry includes the bias, a concrete example, why it's wrong, and how to verify before flagging. diff --git a/src/plan2code-3b-review-references/verification-protocol.md b/src/plan2code-review-references/verification-protocol.md similarity index 98% rename from src/plan2code-3b-review-references/verification-protocol.md rename to src/plan2code-review-references/verification-protocol.md index a90bd8d..ad84a2f 100644 --- a/src/plan2code-3b-review-references/verification-protocol.md +++ b/src/plan2code-review-references/verification-protocol.md @@ -1,5 +1,5 @@ # Verification Protocol -> Part of plan2code-3b-review — loaded during Step 3 (Analyze). +> Part of plan2code-review — loaded during Step 3 (Analyze). Apply this protocol to every finding before presenting it. Findings that fail verification are dropped. No exceptions. diff --git a/src/plan2code-3b-review.md b/src/plan2code-review.md similarity index 100% rename from src/plan2code-3b-review.md rename to src/plan2code-review.md diff --git a/src/statusline-claude/README.md b/src/statusline-claude/README.md new file mode 100644 index 0000000..365af66 --- /dev/null +++ b/src/statusline-claude/README.md @@ -0,0 +1,167 @@ +# Plan2Code Status Line (Claude CLI) + +A persistent three-line status bar for Claude Code that displays model info, project context, context window usage, usage stats (rate limits or token counts), and git diff stats. + +## Example Output + +**Pro / Max / Teams** — rate limits segment: + +``` +╭─╮ Opus 4.6 │ plan2code │ feature/statusline │ +12 -3 +│★│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ +╰─╯ 3h 5m ($4.62) │ ▰▰▰▰▰▱▱▱▱▱▱▱ 42% │ 5h: 28% · 7d: 61% +``` + +**Enterprise / Bedrock / Vertex / PAYG** — session token counts (no `rate_limits` in stdin): + +``` +╭─╮ Sonnet 4.5 │ plan2code │ main │ +12 -3 +│★│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ +╰─╯ 2m ($0.18) │ ▰▰▱▱▱▱▱▱▱▱▱▱ 18% │ 88k in · 3k out +``` + +**Line 1:** Planny icon | Model name | Project name | Git branch | Uncommitted changes +**Line 2:** Planny icon | Gray separator +**Line 3:** Planny icon | Session duration + cost | Context window bar | Usage (rate limits or token counts) + +## Installation + +### Via install.js (Recommended) + +```bash +node install.js +# Select A (Install All + dev tools) or C (Custom) → S (Status Line) +``` + +This automatically: +1. Copies `statusline.js` → `~/.claude/plan2code-statusline.js` +2. Creates default config at `~/.claude/statusline-config.json` (preserves existing) +3. Registers the status line in `~/.claude/settings.json` (atomic write) + +### Manual Setup + +1. Copy `statusline.js` → `~/.claude/plan2code-statusline.js` +2. Copy `statusline-config.json` → `~/.claude/statusline-config.json` +3. Add to `~/.claude/settings.json`: + ```json + { + "statusLine": { + "type": "command", + "command": "node /path/to/home/.claude/plan2code-statusline.js" + } + } + ``` + +## Configuration + +Edit `~/.claude/statusline-config.json`: + +```json +{ + "autocompactBuffer": 33000, + "color": true, + "compact": false, + "items": { + "model": true, + "project": true, + "branch": true, + "contextBar": true, + "planUsage": true, + "linesChanged": true, + "duration": true, + "sessionCost": true + } +} +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `autocompactBuffer` | `33000` | Autocompact buffer size in tokens. Context bar scales usable window = total − buffer. Adjust to match your model's autocompact buffer. | +| `color` | `true` | Enable ANSI color output. Set `false` for monochrome. The `NO_COLOR` environment variable (per [no-color.org](https://no-color.org)) also disables color. | +| `compact` | `false` | Two-line mode — drops the mascot icons and the separator line. Goes from 3 lines to 2, no horizontal truncation. | +| `items.model` | `true` | Show model name (Opus, Sonnet, Haiku) | +| `items.project` | `true` | Show project directory name | +| `items.branch` | `true` | Show current git branch (falls back to short SHA when detached HEAD) | +| `items.contextBar` | `true` | Show context window usage bar with percentage | +| `items.planUsage` | `true` | Show usage info: rate limits (Pro/Max) or token counts (Bedrock/Vertex/PAYG) | +| `items.linesChanged` | `true` | Show uncommitted git diff stats (+added -removed) | +| `items.duration` | `true` | Show session duration | +| `items.sessionCost` | `true` | Append estimated session cost in parens next to duration (e.g. `49m ($4.62)`). Reads `cost.total_cost_usd` from Claude Code stdin — client-side estimate, not billing-exact. Session-scoped only; monthly enterprise totals are not exposed by Claude Code. Hidden when cost is 0 or unavailable. | + +Malformed config falls back to defaults silently. Type errors on individual keys also fall back to defaults. + +## Platform Support + +| Platform | Status | Notes | +|----------|--------|-------| +| Claude Code | Supported | Full feature set via `settings.json` registration | +| Copilot CLI | Planned | Deferred — CLI lacks status line script support as of v0.0.421 | +| Others | Not supported | Codex CLI, Gemini CLI have built-in status, not scriptable | + +## Usage Display + +The usage segment reads data directly from Claude Code's stdin JSON — no API calls, no auth needed, no background processes. + +Two display modes are automatically selected based on the data present in stdin: + +| User Type | Condition | Display Format | Data Source | +|-----------|-----------|----------------|-------------| +| Pro/Max/Teams | `rate_limits` present in stdin | `5h: 28% · 7d: 61%` | `rate_limits.five_hour.used_percentage`, `rate_limits.seven_day.used_percentage` | +| Bedrock/Vertex/PAYG | No `rate_limits` in stdin | `12k in · 4k out` | `context_window.total_input_tokens`, `context_window.total_output_tokens` | + +### Color Thresholds + +- Rate limits: green < 60%, yellow 60–79%, red 80%+ +- Token counts: neutral/white (no thresholds) +- Context bar: green < 50%, yellow 50–69%, red 70%+ + +## Debugging + +The bundled script accepts two diagnostic flags for local testing: + +```bash +# Print parsed stdin and merged config to stderr +echo '{"model":{"display_name":"Opus 4.6"}}' | node ~/.claude/plan2code-statusline.js --debug + +# Load a fixture file instead of reading from stdin +node ~/.claude/plan2code-statusline.js --fixture ./fixture.json +node ~/.claude/plan2code-statusline.js --fixture ./fixture.json --debug +``` + +Fixture format = whatever Claude Code sends on stdin (JSON with `model`, `workspace`, `context_window`, `rate_limits`, `cost.total_duration_ms`). + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Garbled ANSI codes in output | Set `"color": false` in config, or export `NO_COLOR=1` | +| Status line not appearing | Verify `~/.claude/settings.json` has `statusLine` key with correct path to `plan2code-statusline.js` | +| Slow execution (>100ms) | Git calls are timeout-bounded to 1.5s each; non-repos short-circuit via `.git` directory check. If still slow, disable `branch` or `linesChanged`. | +| Usage segment not showing | Rate limits require Pro/Max/Teams plan. Token counts require at least one assistant message. Use `--debug` to inspect stdin. | +| Wrong context bar percentage | Adjust `autocompactBuffer` to match your model. Default 33000 tokens. | +| Three lines take too much space | Set `"compact": true` to drop the mascot icons + separator line (3 lines → 2). | + +## Architecture + +``` +src/statusline-claude/ +├── statusline.js # Self-contained: config, git, formatters, render +└── statusline-config.json # Default config template +``` + +Single-file design — `statusline.js` is copied verbatim to `~/.claude/plan2code-statusline.js` on install. No bundler, no runtime dependencies. + +## Uninstalling + +### Via install.js + +```bash +node install.js +# Select U (Uninstall) → confirms removal +``` + +### Manual Removal + +1. Remove `statusLine` key from `~/.claude/settings.json` +2. Delete `~/.claude/plan2code-statusline.js` +3. Optionally delete `~/.claude/statusline-config.json` diff --git a/src/statusline-claude/statusline-config.json b/src/statusline-claude/statusline-config.json new file mode 100644 index 0000000..bfef506 --- /dev/null +++ b/src/statusline-claude/statusline-config.json @@ -0,0 +1,15 @@ +{ + "autocompactBuffer": 33000, + "color": true, + "compact": false, + "items": { + "model": true, + "project": true, + "branch": true, + "contextBar": true, + "planUsage": true, + "linesChanged": true, + "duration": true, + "sessionCost": true + } +} diff --git a/src/statusline-claude/statusline.js b/src/statusline-claude/statusline.js new file mode 100644 index 0000000..937daf4 --- /dev/null +++ b/src/statusline-claude/statusline.js @@ -0,0 +1,425 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { execFileSync } = require('child_process'); + +// ============================================================================ +// Constants +// ============================================================================ + +const CONTEXT_BAR_LEN = 12; +const CONTEXT_THRESHOLDS = { green: 50, yellow: 70 }; +const RATE_LIMIT_THRESHOLDS = { green: 60, yellow: 80 }; +const GIT_TIMEOUT_MS = 1500; +const DEFAULT_WINDOW_SIZE = 200000; +const SEPARATOR_WIDTH = 55; + +// ============================================================================ +// Config +// ============================================================================ + +const DEFAULTS = { + autocompactBuffer: 33000, + color: true, + compact: false, + items: { + model: true, + project: true, + branch: true, + contextBar: true, + planUsage: true, + linesChanged: true, + duration: true, + sessionCost: true, + }, +}; + +function loadConfig() { + const configPath = path.join(os.homedir(), '.claude', 'statusline-config.json'); + let merged = { ...DEFAULTS, items: { ...DEFAULTS.items } }; + + try { + const raw = fs.readFileSync(configPath, 'utf8'); + const user = JSON.parse(raw); + merged = { + ...DEFAULTS, + ...user, + items: { ...DEFAULTS.items, ...(user.items || {}) }, + }; + } catch { + // Fall through to defaults + } + + // Type validation with fallback + if (!Number.isInteger(merged.autocompactBuffer) || merged.autocompactBuffer < 0 || merged.autocompactBuffer >= 1_000_000) { + merged.autocompactBuffer = DEFAULTS.autocompactBuffer; + } + if (typeof merged.color !== 'boolean') merged.color = DEFAULTS.color; + if (typeof merged.compact !== 'boolean') merged.compact = DEFAULTS.compact; + for (const key of Object.keys(DEFAULTS.items)) { + if (typeof merged.items[key] !== 'boolean') merged.items[key] = DEFAULTS.items[key]; + } + + // NO_COLOR convention (https://no-color.org) — env trumps config + if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') { + merged.color = false; + } + + return merged; +} + +// ============================================================================ +// Colors +// ============================================================================ + +const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`; +const C = { + reset: '\x1b[0m', + bold: '\x1b[1m', + dim: '\x1b[2m', + green: '\x1b[32m', + yellow: '\x1b[33m', + red: '\x1b[31m', + cyan: '\x1b[36m', + white: '\x1b[37m', + gray: '\x1b[90m', + // Plan2Code brand + teal: rgb(190, 192, 200), // Silver — repo + sky: rgb(120, 195, 255), // Light sky blue — branch + muted: rgb(40, 120, 200), // Mid blue — model + label: rgb(130, 135, 150), // Mid gray — field labels (5h, 7d) + // Bar & quota thresholds + barGreen: rgb(50, 220, 100), + barYellow: rgb(220, 180, 50), + barRed: rgb(220, 80, 70), + // Planny mascot + sGreen: rgb(60, 200, 120), + sBlue: rgb(50, 100, 200), + sEye: rgb(255, 255, 255), +}; + +const SEP = (config) => config.color ? `${C.gray} │ ${C.reset}` : ' │ '; +const DOT = (config) => config.color ? `${C.gray} · ${C.reset}` : ' · '; + +// ============================================================================ +// Git helpers +// ============================================================================ + +function gitExec(args, cwd) { + try { + return execFileSync('git', args, { + cwd, + timeout: GIT_TIMEOUT_MS, + encoding: 'utf8', + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }).trim(); + } catch { + return null; + } +} + +function isGitRepo(cwd) { + try { + // Walk up parent dirs (bounded) — monorepos open Claude Code in a subdir + // where .git lives at the repo root. .git can be a directory or a file. + let dir = cwd; + for (let i = 0; i < 10; i++) { + if (fs.existsSync(path.join(dir, '.git'))) return true; + const parent = path.dirname(dir); + if (parent === dir) return false; + dir = parent; + } + return false; + } catch { + return false; + } +} + +function getGitBranch(stdinData) { + const cwd = stdinData?.workspace?.project_dir || process.cwd(); + if (!isGitRepo(cwd)) return ''; + const branch = gitExec(['symbolic-ref', '--short', 'HEAD'], cwd); + if (branch) return branch; + // Detached HEAD fallback — show short SHA so branch segment isn't empty + const sha = gitExec(['rev-parse', '--short', 'HEAD'], cwd); + return sha || ''; +} + +// ============================================================================ +// Formatters +// ============================================================================ + +function getModelName(stdinData) { + const raw = stdinData?.model; + if (!raw) return ''; + + if (typeof raw === 'object' && raw.display_name) { + return raw.display_name.replace(/ context\)/gi, ')'); + } + + const model = typeof raw === 'object' ? raw.id : raw; + if (!model || typeof model !== 'string') return ''; + + const claudeMatch = model.match(/^claude-(\w+)-/); + if (claudeMatch) { + return claudeMatch[1].charAt(0).toUpperCase() + claudeMatch[1].slice(1); + } + + const gptMatch = model.match(/^(gpt-[\w.]+)/i); + if (gptMatch) { + return gptMatch[1].toUpperCase(); + } + + return model; +} + +function getProjectName(stdinData) { + const projectDir = stdinData?.workspace?.project_dir; + if (!projectDir || typeof projectDir !== 'string') return ''; + return path.basename(projectDir); +} + +function calculateContextPercent(stdinData, config) { + const cw = stdinData?.context_window; + if (!cw) return null; + const windowSize = cw.context_window_size || DEFAULT_WINDOW_SIZE; + const buffer = config?.autocompactBuffer ?? DEFAULTS.autocompactBuffer; + const usableTokens = windowSize - buffer; + if (usableTokens <= 0) return 100; + const rawUsedPct = cw.used_percentage ?? 0; + const rawUsedTokens = (rawUsedPct / 100) * windowSize; + return Math.round(Math.min(100, (rawUsedTokens / usableTokens) * 100)); +} + +function formatContextBar(percent, config) { + if (percent == null) return null; + + const clamped = Math.max(0, Math.min(100, percent)); + const filled = Math.round((clamped / 100) * CONTEXT_BAR_LEN); + const filledStr = '▰'.repeat(filled); + const emptyStr = '▱'.repeat(CONTEXT_BAR_LEN - filled); + + if (!config.color) return `${filledStr}${emptyStr} ${clamped}%`; + + const barColor = clamped >= CONTEXT_THRESHOLDS.yellow ? C.barRed + : clamped >= CONTEXT_THRESHOLDS.green ? C.barYellow + : C.barGreen; + return `${barColor}${filledStr}${C.gray}${emptyStr}${C.reset} ${barColor}${clamped}%${C.reset}`; +} + +function formatLinesChanged(stdinData, config) { + const cwd = stdinData?.workspace?.project_dir || process.cwd(); + if (!isGitRepo(cwd)) return null; + const raw = gitExec(['diff', 'HEAD', '--numstat'], cwd); + if (raw == null) return null; + + let added = 0, removed = 0; + for (const line of raw.split('\n')) { + if (!line) continue; + const [a, r] = line.split('\t'); + if (a !== '-') added += parseInt(a, 10) || 0; + if (r !== '-') removed += parseInt(r, 10) || 0; + } + if (added === 0 && removed === 0) return null; + if (!config.color) return `+${added} -${removed}`; + return `${C.green}+${added}${C.reset} ${C.red}-${removed}${C.reset}`; +} + +function formatSessionCost(stdinData) { + const usd = stdinData?.cost?.total_cost_usd; + if (usd == null || isNaN(usd) || usd <= 0) return null; + if (usd < 0.01) return '<$0.01'; + if (usd < 100) return `$${usd.toFixed(2)}`; + return `$${Math.round(usd)}`; +} + +function formatDuration(stdinData, config) { + const ms = stdinData?.cost?.total_duration_ms; + if (!ms) return null; + + const totalSec = Math.round(ms / 1000); + let text; + if (totalSec < 60) text = `${totalSec}s`; + else { + const hours = Math.floor(totalSec / 3600); + const mins = Math.floor((totalSec % 3600) / 60); + text = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; + } + + // Append session cost in parens when enabled: "49m ($4.62)" + if (config.items.sessionCost) { + const cost = formatSessionCost(stdinData); + if (cost) { + text = config.color + ? `${C.label}${text} (${cost})${C.reset}` + : `${text} (${cost})`; + return text; + } + } + return config.color ? `${C.label}${text}${C.reset}` : text; +} + +function colorize(text, percent, thresholds, config) { + if (!config.color || percent == null) return text; + let code; + if (percent >= thresholds.yellow) code = C.barRed; + else if (percent >= thresholds.green) code = C.barYellow; + else code = C.barGreen; + return `${code}${text}${C.reset}`; +} + +function formatTokenCount(n) { + if (n == null || isNaN(n) || n < 0) return '0'; + if (n < 1000) return String(Math.round(n)); + if (n < 999500) return Math.round(n / 1000) + 'k'; + return (n / 1_000_000).toFixed(1) + 'M'; +} + +function formatRateLimits(stdinData, config) { + if (!config.items.planUsage) return null; + const fiveHour = stdinData?.rate_limits?.five_hour?.used_percentage; + const sevenDay = stdinData?.rate_limits?.seven_day?.used_percentage; + if (fiveHour == null && sevenDay == null) return null; + + const parts = []; + if (fiveHour != null) { + const pct = Math.round(fiveHour); + const val = colorize(`${pct}%`, pct, RATE_LIMIT_THRESHOLDS, config); + parts.push(config.color ? `${C.label}5h:${C.reset} ${val}` : `5h: ${pct}%`); + } + if (sevenDay != null) { + const pct = Math.round(sevenDay); + const val = colorize(`${pct}%`, pct, RATE_LIMIT_THRESHOLDS, config); + parts.push(config.color ? `${C.label}7d:${C.reset} ${val}` : `7d: ${pct}%`); + } + return parts.join(DOT(config)); +} + +function formatTokenUsage(stdinData, config) { + if (!config.items.planUsage) return null; + const inp = stdinData?.context_window?.total_input_tokens; + const out = stdinData?.context_window?.total_output_tokens; + if ((!inp || inp === 0) && (!out || out === 0)) return null; + const inStr = formatTokenCount(inp || 0); + const outStr = formatTokenCount(out || 0); + if (!config.color) return `${inStr} in${DOT(config)}${outStr} out`; + return `${C.white}${inStr}${C.reset} ${C.label}in${C.reset}${DOT(config)}${C.white}${outStr}${C.reset} ${C.label}out${C.reset}`; +} + +function formatLine1(stdinData, config) { + const segments = []; + + if (config.items.model) { + const model = getModelName(stdinData); + if (model) segments.push(config.color ? `${C.muted}${model}${C.reset}` : model); + } + + if (config.items.project) { + const project = getProjectName(stdinData); + if (project) segments.push(config.color ? `${C.bold}${C.teal}${project}${C.reset}` : project); + } + + if (config.items.branch) { + const branch = getGitBranch(stdinData); + if (branch) segments.push(config.color ? `${C.sky}${branch}${C.reset}` : branch); + } + + if (config.items.linesChanged) { + const lines = formatLinesChanged(stdinData, config); + if (lines) segments.push(lines); + } + + return segments.join(SEP(config)); +} + +function formatLine2(stdinData, config) { + const segments = []; + + if (config.items.duration) { + const dur = formatDuration(stdinData, config); + if (dur) segments.push(dur); + } + + if (config.items.contextBar) { + const percent = calculateContextPercent(stdinData, config); + const bar = formatContextBar(percent, config); + if (bar) segments.push(bar); + } + + if (config.items.planUsage) { + const usage = formatRateLimits(stdinData, config) || formatTokenUsage(stdinData, config); + if (usage) segments.push(usage); + } + + return segments.join(SEP(config)); +} + +// ============================================================================ +// Main +// ============================================================================ + +function loadStdin() { + // --fixture for local diagnostics (bypasses stdin) + const fixtureIdx = process.argv.indexOf('--fixture'); + if (fixtureIdx !== -1 && process.argv[fixtureIdx + 1]) { + try { + return JSON.parse(fs.readFileSync(process.argv[fixtureIdx + 1], 'utf8')); + } catch (err) { + process.stderr.write(`[statusline] fixture load failed: ${err.message}\n`); + process.exit(1); + } + } + try { + return JSON.parse(fs.readFileSync(0, 'utf8')); + } catch { + return null; + } +} + +function main() { + const debug = process.argv.includes('--debug'); + const stdinData = loadStdin(); + if (!stdinData) process.exit(0); + + const config = loadConfig(); + + if (debug) { + process.stderr.write('[statusline debug] parsed stdin:\n'); + process.stderr.write(JSON.stringify(stdinData, null, 2) + '\n'); + process.stderr.write('[statusline debug] merged config:\n'); + process.stderr.write(JSON.stringify(config, null, 2) + '\n'); + } + + const line1 = formatLine1(stdinData, config); + const line2 = formatLine2(stdinData, config); + if (!line1 && !line2) return; + + if (config.compact) { + if (line1) process.stdout.write(line1 + '\n'); + if (line2) process.stdout.write(line2 + '\n'); + return; + } + + if (config.color) { + const ico1 = `${C.sGreen}╭─╮${C.reset} `; + const ico2 = `${C.sGreen}│${C.sEye}★${C.sGreen}│${C.reset} `; + const ico3 = `${C.sGreen}╰─╯${C.reset} `; + const pad = `${C.gray}${'┄'.repeat(SEPARATOR_WIDTH)}${C.reset}`; + process.stdout.write(ico1 + line1 + '\n' + ico2 + pad + '\n' + ico3 + line2 + '\n'); + } else { + const ico1 = '╭─╮ '; + const ico2 = '│★│ '; + const ico3 = '╰─╯ '; + const pad = '┄'.repeat(SEPARATOR_WIDTH); + process.stdout.write(ico1 + line1 + '\n' + ico2 + pad + '\n' + ico3 + line2 + '\n'); + } +} + +try { + main(); +} catch { + process.exit(0); +} diff --git a/version.json b/version.json index c26660a..ff71f06 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "name": "Plan2Code", - "version": "1.14.0", + "version": "1.15.2", "description": "A structured 4-step workflow methodology for AI-assisted software development", "keywords": [ "ai", @@ -17,7 +17,7 @@ "url": "https://github.com/jparkerweb/plan2code" }, "homepage": "https://plan2code.jparkerweb.com", - "releaseDate": "2026-05-22", + "releaseDate": "2026-06-01", "mode": "utility" }