mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 10:23:23 -07:00
v1.15.2 — add Claude CLI status line, Zed support, rename 3b-review to review
- Status line: new src/statusline-claude/ (Planny box-star mascot icon), install/uninstall wiring in install.js, Install All (A) + Custom (S) - Zed agent support across README, AGENTS, docs, init prompt, installer - Rename /plan2code-3b-review to /plan2code-review (now a standalone utility workflow); update file, reference dir, and all cross-references - install.js: fs.rmSync simplifications, async install, summary cleanup - Bump to v1.15.2; CHANGELOG entries for v1.15.0/1/2
This commit is contained in:
@@ -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/<source-filename-without-extension>-references/` (e.g., `plan2code-3b-review-references/`)
|
||||
**Pattern:** `src/<source-filename-without-extension>-references/` (e.g., `plan2code-review-references/`)
|
||||
|
||||
**How the installer handles them:**
|
||||
- **Skill-directory platforms** (Claude Code, Agents, Crush, Devin): reference files are nested as `<skill-name>/references/`. Read paths use canonical `references/<file>.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/<file>.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/<file>.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`.
|
||||
|
||||
@@ -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/<skill-name>/` | `~/.claude/skills/<skill-name>/` | YAML frontmatter + `disable-model-invocation: true` |
|
||||
| Agent Skills (Amp · Devin · Gemini CLI · OpenCode) | `SKILL.md` in subdir | `.agents/skills/<skill-name>/` | `~/.agents/skills/<skill-name>/` | YAML frontmatter (no disable flag) |
|
||||
| Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed) | `SKILL.md` in subdir | `.agents/skills/<skill-name>/` | `~/.agents/skills/<skill-name>/` | YAML frontmatter (no disable flag) |
|
||||
| Crush | `SKILL.md` in subdir | — (global only) | `~/.config/crush/skills/<skill-name>/` (Unix) / `%LOCALAPPDATA%\crush\skills\<skill-name>\` (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`) |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
+1
-1
@@ -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-<date>.md + PLAN-DRAFT-<date>.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
|
||||
|
||||
@@ -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/<feature>/PLAN-DRAFT-<date>.md` or planning conversation |
|
||||
| Step 3 (Implement) | `specs/<feature>/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/<feature>/overview.md` |
|
||||
|
||||
---
|
||||
|
||||
+2
-2
@@ -988,7 +988,7 @@
|
||||
<section class="step">
|
||||
<div class="step-content">
|
||||
<div class="step-header">
|
||||
<div class="step-number headline">3b</div>
|
||||
<div class="step-number headline">review</div>
|
||||
<div class="step-icon" style="color: var(--accent);">🔬</div>
|
||||
</div>
|
||||
<h2 class="step-title subheadline">Review</h2>
|
||||
@@ -1112,7 +1112,7 @@
|
||||
<li><span class="code-line">/plan2code-1-plan</span></li>
|
||||
<li><span class="code-line">/plan2code-2-document</span></li>
|
||||
<li><span class="code-line">/plan2code-3-implement</span> <span class="gray">(per phase)</span></li>
|
||||
<li><span class="code-line">/plan2code-3b-review</span> <span class="gray">(optional)</span></li>
|
||||
<li><span class="code-line">/plan2code-review</span> <span class="gray">(optional)</span></li>
|
||||
<li><span class="code-line">/plan2code-4-finalize</span></li>
|
||||
</ul>
|
||||
<br>
|
||||
|
||||
+301
-107
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plan2code",
|
||||
"version": "1.14.0",
|
||||
"version": "1.15.2",
|
||||
"private": true,
|
||||
"bin": {
|
||||
"plan2code": "./install.js"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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):
|
||||
```
|
||||
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 <path> 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);
|
||||
}
|
||||
+2
-2
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user