diff --git a/.claude/agents/action-planning-agent.md b/.claude/agents/action-planning-agent.md index 441989d0..fcac40b9 100644 --- a/.claude/agents/action-planning-agent.md +++ b/.claude/agents/action-planning-agent.md @@ -213,7 +213,11 @@ Generate individual `.task/IMPL-*.json` files with the following structure: ``` **Field Descriptions**: -- `id`: Task identifier (format: `IMPL-N`) +- `id`: Task identifier + - Single module format: `IMPL-N` (e.g., IMPL-001, IMPL-002) + - Multi-module format: `IMPL-{prefix}{seq}` (e.g., IMPL-A1, IMPL-B1, IMPL-C1) + - Prefix: A, B, C... (assigned by module detection order) + - Sequence: 1, 2, 3... (per-module increment) - `title`: Descriptive task name summarizing the work - `status`: Task state - `pending` (not started), `active` (in progress), `completed` (done), `blocked` (waiting on dependencies) - `context_package_path`: Path to smart context package containing project structure, dependencies, and brainstorming artifacts catalog @@ -225,7 +229,8 @@ Generate individual `.task/IMPL-*.json` files with the following structure: "meta": { "type": "feature|bugfix|refactor|test-gen|test-fix|docs", "agent": "@code-developer|@action-planning-agent|@test-fix-agent|@universal-executor", - "execution_group": "parallel-abc123|null" + "execution_group": "parallel-abc123|null", + "module": "frontend|backend|shared|null" } } ``` @@ -234,6 +239,7 @@ Generate individual `.task/IMPL-*.json` files with the following structure: - `type`: Task category - `feature` (new functionality), `bugfix` (fix defects), `refactor` (restructure code), `test-gen` (generate tests), `test-fix` (fix failing tests), `docs` (documentation) - `agent`: Assigned agent for execution - `execution_group`: Parallelization group ID (tasks with same ID can run concurrently) or `null` for sequential tasks +- `module`: Module identifier for multi-module projects (e.g., `frontend`, `backend`, `shared`) or `null` for single-module **Test Task Extensions** (for type="test-gen" or type="test-fix"): @@ -604,10 +610,42 @@ Agent determines CLI tool usage per-step based on user semantics and task nature - Analysis results (technical approach, architecture decisions) - Brainstorming artifacts (role analyses, guidance specifications) +**Multi-Module Format** (when modules detected): + +When multiple modules are detected (frontend/backend, etc.), organize IMPL_PLAN.md by module: + +```markdown +# Implementation Plan + +## Module A: Frontend (N tasks) +### IMPL-A1: [Task Title] +[Task details...] + +### IMPL-A2: [Task Title] +[Task details...] + +## Module B: Backend (N tasks) +### IMPL-B1: [Task Title] +[Task details...] + +### IMPL-B2: [Task Title] +[Task details...] + +## Cross-Module Dependencies +- IMPL-A1 → IMPL-B1 (Frontend depends on Backend API) +- IMPL-A2 → IMPL-B2 (UI state depends on Backend service) +``` + +**Cross-Module Dependency Notation**: +- During parallel planning, use `CROSS::{module}::{pattern}` format +- Example: `depends_on: ["CROSS::B::api-endpoint"]` +- Integration phase resolves to actual task IDs: `CROSS::B::api → IMPL-B1` + ### 2.3 TODO_LIST.md Structure Generate at `.workflow/active/{session_id}/TODO_LIST.md`: +**Single Module Format**: ```markdown # Tasks: {Session Topic} @@ -621,26 +659,50 @@ Generate at `.workflow/active/{session_id}/TODO_LIST.md`: - `- [x]` = Completed task ``` +**Multi-Module Format** (hierarchical by module): +```markdown +# Tasks: {Session Topic} + +## Module A (Frontend) +- [ ] **IMPL-A1**: [Task Title] → [📋](./.task/IMPL-A1.json) +- [ ] **IMPL-A2**: [Task Title] → [📋](./.task/IMPL-A2.json) + +## Module B (Backend) +- [ ] **IMPL-B1**: [Task Title] → [📋](./.task/IMPL-B1.json) +- [ ] **IMPL-B2**: [Task Title] → [📋](./.task/IMPL-B2.json) + +## Cross-Module Dependencies +- IMPL-A1 → IMPL-B1 (Frontend depends on Backend API) + +## Status Legend +- `- [ ]` = Pending task +- `- [x]` = Completed task +``` + **Linking Rules**: - Todo items → task JSON: `[📋](./.task/IMPL-XXX.json)` - Completed tasks → summaries: `[✅](./.summaries/IMPL-XXX-summary.md)` -- Consistent ID schemes: IMPL-XXX +- Consistent ID schemes: `IMPL-N` (single) or `IMPL-{prefix}{seq}` (multi-module) ### 2.4 Complexity-Based Structure Selection Use `analysis_results.complexity` or task count to determine structure: -**Simple Tasks** (≤5 tasks): -- Flat structure: IMPL_PLAN.md + TODO_LIST.md + task JSONs -- All tasks at same level +**Single Module Mode**: +- **Simple Tasks** (≤5 tasks): Flat structure +- **Medium Tasks** (6-12 tasks): Flat structure +- **Complex Tasks** (>12 tasks): Re-scope required (maximum 12 tasks hard limit) -**Medium Tasks** (6-12 tasks): -- Flat structure: IMPL_PLAN.md + TODO_LIST.md + task JSONs -- All tasks at same level +**Multi-Module Mode** (N+1 parallel planning): +- **Per-module limit**: ≤9 tasks per module +- **Total limit**: Sum of all module tasks ≤27 (3 modules × 9 tasks) +- **Task ID format**: `IMPL-{prefix}{seq}` (e.g., IMPL-A1, IMPL-B1) +- **Structure**: Hierarchical by module in IMPL_PLAN.md and TODO_LIST.md -**Complex Tasks** (>12 tasks): -- **Re-scope required**: Maximum 12 tasks hard limit -- If analysis_results contains >12 tasks, consolidate or request re-scoping +**Multi-Module Detection Triggers**: +- Explicit frontend/backend separation (`src/frontend`, `src/backend`) +- Monorepo structure (`packages/*`, `apps/*`) +- Context-package dependency clustering (2+ distinct module groups) --- @@ -685,8 +747,10 @@ Use `analysis_results.complexity` or task count to determine structure: ### 3.3 File Organization - Session naming: `WFS-[topic-slug]` -- Task IDs: IMPL-XXX (flat structure only) -- Directory structure: flat task organization +- Task IDs: + - Single module: `IMPL-N` (e.g., IMPL-001, IMPL-002) + - Multi-module: `IMPL-{prefix}{seq}` (e.g., IMPL-A1, IMPL-B1) +- Directory structure: flat task organization (all tasks in `.task/`) ### 3.4 Document Standards diff --git a/.claude/commands/workflow/tools/task-generate-agent.md b/.claude/commands/workflow/tools/task-generate-agent.md index 94cff40d..d8ad0cad 100644 --- a/.claude/commands/workflow/tools/task-generate-agent.md +++ b/.claude/commands/workflow/tools/task-generate-agent.md @@ -14,8 +14,8 @@ Generate implementation planning documents (IMPL_PLAN.md, task JSONs, TODO_LIST. ## Core Philosophy - **Planning Only**: Generate planning documents (IMPL_PLAN.md, task JSONs, TODO_LIST.md) - does NOT implement code - **Agent-Driven Document Generation**: Delegate plan generation to action-planning-agent +- **N+1 Parallel Planning**: Auto-detect multi-module projects, enable parallel planning (2+1 or 3+1 mode) - **Progressive Loading**: Load context incrementally (Core → Selective → On-Demand) due to analysis.md file size -- **Two-Phase Flow**: Discovery (context gathering) → Output (planning document generation) - **Memory-First**: Reuse loaded documents from conversation memory - **Smart Selection**: Load synthesis_output OR guidance + relevant role analyses, NOT all role analyses - **MCP-Enhanced**: Use MCP tools for advanced code analysis and research @@ -28,22 +28,38 @@ Input Parsing: ├─ Parse flags: --session └─ Validation: session_id REQUIRED -Phase 1: Context Preparation (Command) +Phase 1: Context Preparation & Module Detection (Command) ├─ Assemble session paths (metadata, context package, output dirs) - └─ Provide metadata (session_id, execution_mode, mcp_capabilities) + ├─ Provide metadata (session_id, execution_mode, mcp_capabilities) + ├─ Auto-detect modules from context-package + directory structure + └─ Decision: + ├─ modules.length == 1 → Single Agent Mode (Phase 2A) + └─ modules.length >= 2 → Parallel Mode (Phase 2B + Phase 3) -Phase 2: Planning Document Generation (Agent) +Phase 2A: Single Agent Planning (Original Flow) ├─ Load context package (progressive loading strategy) ├─ Generate Task JSON Files (.task/IMPL-*.json) ├─ Create IMPL_PLAN.md └─ Generate TODO_LIST.md + +Phase 2B: N Parallel Planning (Multi-Module) + ├─ Launch N action-planning-agents simultaneously (one per module) + ├─ Each agent generates module-scoped tasks (IMPL-{prefix}{seq}.json) + ├─ Task ID format: IMPL-A1, IMPL-A2... / IMPL-B1, IMPL-B2... + └─ Each module limited to ≤9 tasks + +Phase 3: Integration (+1 Coordinator, Multi-Module Only) + ├─ Collect all module task JSONs + ├─ Resolve cross-module dependencies (CROSS::{module}::{pattern} → actual ID) + ├─ Generate unified IMPL_PLAN.md (grouped by module) + └─ Generate TODO_LIST.md (hierarchical: module → tasks) ``` ## Document Generation Lifecycle -### Phase 1: Context Preparation (Command Responsibility) +### Phase 1: Context Preparation & Module Detection (Command Responsibility) -**Command prepares session paths and metadata for planning document generation.** +**Command prepares session paths, metadata, and detects module structure.** **Session Path Structure**: ``` @@ -52,8 +68,12 @@ Phase 2: Planning Document Generation (Agent) ├── .process/ │ └── context-package.json # Context package with artifact catalog ├── .task/ # Output: Task JSON files -├── IMPL_PLAN.md # Output: Implementation plan -└── TODO_LIST.md # Output: TODO list +│ ├── IMPL-A1.json # Multi-module: prefixed by module +│ ├── IMPL-A2.json +│ ├── IMPL-B1.json +│ └── ... +├── IMPL_PLAN.md # Output: Implementation plan (grouped by module) +└── TODO_LIST.md # Output: TODO list (hierarchical) ``` **Command Preparation**: @@ -66,9 +86,40 @@ Phase 2: Planning Document Generation (Agent) - `session_id` - `mcp_capabilities` (available MCP tools) +3. **Auto Module Detection** (determines single vs parallel mode): + ```javascript + function autoDetectModules(contextPackage, projectRoot) { + // Priority 1: Explicit frontend/backend separation + if (exists('src/frontend') && exists('src/backend')) { + return [ + { name: 'frontend', prefix: 'A', paths: ['src/frontend'] }, + { name: 'backend', prefix: 'B', paths: ['src/backend'] } + ]; + } + + // Priority 2: Monorepo structure + if (exists('packages/*') || exists('apps/*')) { + return detectMonorepoModules(); // Returns 2-3 main packages + } + + // Priority 3: Context-package dependency clustering + const modules = clusterByDependencies(contextPackage.dependencies?.internal); + if (modules.length >= 2) return modules.slice(0, 3); + + // Default: Single module (original flow) + return [{ name: 'main', prefix: '', paths: ['.'] }]; + } + ``` + +**Decision Logic**: +- `modules.length == 1` → Phase 2A (Single Agent, original flow) +- `modules.length >= 2` → Phase 2B + Phase 3 (N+1 Parallel) + **Note**: CLI tool usage is now determined semantically by action-planning-agent based on user's task description, not by flags. -### Phase 2: Planning Document Generation (Agent Responsibility) +### Phase 2A: Single Agent Planning (Original Flow) + +**Condition**: `modules.length == 1` (no multi-module detected) **Purpose**: Generate IMPL_PLAN.md, task JSONs, and TODO_LIST.md - planning documents only, NOT code implementation. @@ -148,4 +199,93 @@ Hard Constraints: ) ``` -、 \ No newline at end of file +### Phase 2B: N Parallel Planning (Multi-Module) + +**Condition**: `modules.length >= 2` (multi-module detected) + +**Purpose**: Launch N action-planning-agents simultaneously, one per module, for parallel task generation. + +**Parallel Agent Invocation**: +```javascript +// Launch N agents in parallel (one per module) +const planningTasks = modules.map(module => + Task( + subagent_type="action-planning-agent", + description=`Plan ${module.name} module`, + prompt=` +## SCOPE +- Module: ${module.name} (${module.type}) +- Focus Paths: ${module.paths.join(', ')} +- Task ID Prefix: IMPL-${module.prefix} +- Task Limit: ≤9 tasks +- Other Modules: ${otherModules.join(', ')} +- Cross-module deps format: "CROSS::{module}::{pattern}" + +## SESSION PATHS +Input: + - Context Package: .workflow/active/{session-id}/.process/context-package.json +Output: + - Task Dir: .workflow/active/{session-id}/.task/ + +## INSTRUCTIONS +- Generate tasks ONLY for ${module.name} module +- Use task ID format: IMPL-${module.prefix}1, IMPL-${module.prefix}2, ... +- For cross-module dependencies, use: depends_on: ["CROSS::B::api-endpoint"] +- Maximum 9 tasks per module + ` + ) +); + +// Execute all in parallel +await Promise.all(planningTasks); +``` + +**Output Structure** (direct to .task/): +``` +.task/ +├── IMPL-A1.json # Module A (e.g., frontend) +├── IMPL-A2.json +├── IMPL-B1.json # Module B (e.g., backend) +├── IMPL-B2.json +└── IMPL-C1.json # Module C (e.g., shared) +``` + +**Task ID Naming**: +- Format: `IMPL-{prefix}{seq}.json` +- Prefix: A, B, C... (assigned by detection order) +- Sequence: 1, 2, 3... (per-module increment) + +### Phase 3: Integration (+1 Coordinator, Multi-Module Only) + +**Condition**: Only executed when `modules.length >= 2` + +**Purpose**: Collect all module tasks, resolve cross-module dependencies, generate unified documents. + +**Integration Logic**: +```javascript +// 1. Collect all module task JSONs +const allTasks = glob('.task/IMPL-*.json').map(loadJson); + +// 2. Resolve cross-module dependencies +for (const task of allTasks) { + if (task.depends_on) { + task.depends_on = task.depends_on.map(dep => { + if (dep.startsWith('CROSS::')) { + // CROSS::B::api-endpoint → find matching IMPL-B* task + const [, targetModule, pattern] = dep.match(/CROSS::(\w+)::(.+)/); + return findTaskByModuleAndPattern(allTasks, targetModule, pattern); + } + return dep; + }); + } +} + +// 3. Generate unified IMPL_PLAN.md (grouped by module) +generateIMPL_PLAN(allTasks, modules); + +// 4. Generate TODO_LIST.md (hierarchical structure) +generateTODO_LIST(allTasks, modules); +``` + +**Note**: IMPL_PLAN.md and TODO_LIST.md structure definitions are in `action-planning-agent.md`. + diff --git a/ccw/README.md b/ccw/README.md new file mode 100644 index 00000000..cb9df69f --- /dev/null +++ b/ccw/README.md @@ -0,0 +1,121 @@ +# CCW - Claude Code Workflow CLI + +A command-line tool for viewing workflow sessions and code review results from the Claude Code Workflow system. + +## Installation + +```bash +# Install globally +npm install -g ccw + +# Or install from local source +cd path/to/ccw +npm install +npm link +``` + +## Usage + +### View Dashboard + +```bash +# Open workflow dashboard in browser +ccw view + +# Specify project path +ccw view -p /path/to/project + +# Generate dashboard without opening browser +ccw view --no-browser + +# Custom output path +ccw view -o report.html +``` + +## Features + +### Workflow Dashboard +- **Active Sessions**: View all active workflow sessions with task progress +- **Archived Sessions**: Browse completed/archived sessions +- **Task Tracking**: See individual task status (pending/in_progress/completed) +- **Progress Bars**: Visual progress indicators for each session + +### Review Integration +- **Code Review Findings**: View results from `review-module-cycle` +- **Severity Distribution**: Critical/High/Medium/Low finding counts +- **Dimension Analysis**: Findings by review dimension (Security, Architecture, Quality, etc.) +- **Tabbed Interface**: Switch between Workflow and Reviews tabs + +## Dashboard Data Sources + +The CLI reads data from the `.workflow/` directory structure: + +``` +.workflow/ +├── active/ +│ └── WFS-{session-id}/ +│ ├── workflow-session.json # Session metadata +│ ├── .task/ +│ │ └── IMPL-*.json # Task definitions +│ └── .review/ +│ ├── review-progress.json # Review progress +│ └── dimensions/ +│ └── *.json # Dimension findings +└── archives/ + └── WFS-{session-id}/ # Archived sessions +``` + +## Bundled Templates + +The CLI includes bundled dashboard templates: +- `workflow-dashboard.html` - Workflow session and task visualization +- `review-cycle-dashboard.html` - Code review findings display + +No external template installation required - templates are included in the npm package. + +## Requirements + +- Node.js >= 16.0.0 +- npm or yarn + +## Integration with Claude Code Workflow + +This CLI is a standalone tool that works with the Claude Code Workflow system: + +1. **Install CCW CLI** (via npm) + - `npm install -g ccw` + - Provides `ccw view` command for dashboard viewing + - Templates are bundled - no additional installation required + +2. **Optional: Install Claude Code Workflow** (via `Install-Claude.ps1`) + - Provides workflow commands, agents, and automation + - CCW will automatically detect and display workflow sessions + +## Options + +| Option | Description | +|--------|-------------| +| `-p, --path ` | Path to project directory (default: current directory) | +| `--no-browser` | Generate dashboard without opening browser | +| `-o, --output ` | Custom output path for HTML file | +| `-V, --version` | Display version number | +| `-h, --help` | Display help information | + +## Development + +```bash +# Clone and install dependencies +git clone +cd ccw +npm install + +# Link for local testing +npm link + +# Test the CLI +ccw view -p /path/to/test/project +``` + +## License + +MIT diff --git a/ccw/bin/ccw.js b/ccw/bin/ccw.js new file mode 100644 index 00000000..fab6b0f4 --- /dev/null +++ b/ccw/bin/ccw.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +/** + * CCW CLI - Claude Code Workflow Dashboard + * Entry point for global CLI installation + */ + +import { run } from '../src/cli.js'; + +run(process.argv); diff --git a/ccw/package-lock.json b/ccw/package-lock.json new file mode 100644 index 00000000..b16d1748 --- /dev/null +++ b/ccw/package-lock.json @@ -0,0 +1,1914 @@ +{ + "name": "ccw", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ccw", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "boxen": "^7.1.0", + "chalk": "^5.3.0", + "commander": "^11.0.0", + "figlet": "^1.7.0", + "glob": "^10.3.0", + "gradient-string": "^2.0.2", + "inquirer": "^9.2.0", + "open": "^9.1.0", + "ora": "^7.0.0" + }, + "bin": { + "ccw": "bin/ccw.js" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/tinycolor2": { + "version": "1.4.6", + "resolved": "https://registry.npmjs.org/@types/tinycolor2/-/tinycolor2-1.4.6.tgz", + "integrity": "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==", + "license": "MIT" + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", + "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-align/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/ansi-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-align/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boxen": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", + "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.1", + "camelcase": "^7.0.1", + "chalk": "^5.2.0", + "cli-boxes": "^3.0.0", + "string-width": "^5.1.2", + "type-fest": "^2.13.0", + "widest-line": "^4.0.1", + "wrap-ansi": "^8.1.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bplist-parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", + "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.44" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bundle-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", + "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "license": "MIT", + "dependencies": { + "run-applescript": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/cli-boxes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", + "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/default-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", + "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "license": "MIT", + "dependencies": { + "bundle-name": "^3.0.0", + "default-browser-id": "^3.0.0", + "execa": "^7.1.1", + "titleize": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", + "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "license": "MIT", + "dependencies": { + "bplist-parser": "^0.2.0", + "untildify": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/figlet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.9.4.tgz", + "integrity": "sha512-uN6QE+TrzTAHC1IWTyrc4FfGo2KH/82J8Jl1tyKB7+z5DBit/m3D++Iu5lg91qJMnQQ3vpJrj5gxcK/pk4R9tQ==", + "license": "MIT", + "dependencies": { + "commander": "^14.0.0" + }, + "bin": { + "figlet": "bin/index.js" + }, + "engines": { + "node": ">= 17.0.0" + } + }, + "node_modules/figlet/node_modules/commander": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", + "integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gradient-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/gradient-string/-/gradient-string-2.0.2.tgz", + "integrity": "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "tinygradient": "^1.1.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gradient-string/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/gradient-string/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "9.3.8", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz", + "integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==", + "license": "MIT", + "dependencies": { + "@inquirer/external-editor": "^1.0.2", + "@inquirer/figures": "^1.0.3", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/inquirer/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/inquirer/node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/inquirer/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-wsl/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", + "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "license": "MIT", + "dependencies": { + "default-browser": "^4.0.0", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", + "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.9.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.3.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "string-width": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/ora/node_modules/string-width": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", + "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^10.2.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/run-applescript": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", + "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", + "license": "MIT", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/run-applescript/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/run-applescript/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-applescript/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/run-applescript/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-applescript/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/run-applescript/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinygradient": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/tinygradient/-/tinygradient-1.1.5.tgz", + "integrity": "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw==", + "license": "MIT", + "dependencies": { + "@types/tinycolor2": "^1.4.0", + "tinycolor2": "^1.0.0" + } + }, + "node_modules/titleize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", + "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/widest-line": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", + "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", + "license": "MIT", + "dependencies": { + "string-width": "^5.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/ccw/package.json b/ccw/package.json new file mode 100644 index 00000000..ad551032 --- /dev/null +++ b/ccw/package.json @@ -0,0 +1,47 @@ +{ + "name": "ccw", + "version": "1.0.0", + "description": "Claude Code Workflow CLI - Dashboard viewer for workflow sessions and reviews", + "type": "module", + "main": "src/index.js", + "bin": { + "ccw": "./bin/ccw.js" + }, + "scripts": { + "test": "node --test", + "lint": "eslint src/" + }, + "keywords": [ + "claude", + "workflow", + "cli", + "dashboard", + "code-review" + ], + "author": "Claude Code Workflow", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + }, + "dependencies": { + "commander": "^11.0.0", + "open": "^9.1.0", + "chalk": "^5.3.0", + "glob": "^10.3.0", + "inquirer": "^9.2.0", + "ora": "^7.0.0", + "figlet": "^1.7.0", + "boxen": "^7.1.0", + "gradient-string": "^2.0.2" + }, + "files": [ + "bin/", + "src/", + "README.md", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "https://github.com/claude-code-workflow/ccw" + } +} diff --git a/ccw/src/cli.js b/ccw/src/cli.js new file mode 100644 index 00000000..574f2b1b --- /dev/null +++ b/ccw/src/cli.js @@ -0,0 +1,82 @@ +import { Command } from 'commander'; +import { viewCommand } from './commands/view.js'; +import { installCommand } from './commands/install.js'; +import { uninstallCommand } from './commands/uninstall.js'; +import { listCommand } from './commands/list.js'; +import { readFileSync, existsSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** + * Load package.json with error handling + * @returns {Object} - Package info with version + */ +function loadPackageInfo() { + const pkgPath = join(__dirname, '../package.json'); + + try { + if (!existsSync(pkgPath)) { + console.error('Fatal Error: package.json not found.'); + console.error(`Expected location: ${pkgPath}`); + process.exit(1); + } + + const content = readFileSync(pkgPath, 'utf8'); + return JSON.parse(content); + } catch (error) { + if (error instanceof SyntaxError) { + console.error('Fatal Error: package.json contains invalid JSON.'); + console.error(`Parse error: ${error.message}`); + } else { + console.error('Fatal Error: Could not read package.json.'); + console.error(`Error: ${error.message}`); + } + process.exit(1); + } +} + +const pkg = loadPackageInfo(); + +export function run(argv) { + const program = new Command(); + + program + .name('ccw') + .description('Claude Code Workflow CLI - Dashboard and workflow tools') + .version(pkg.version); + + // View command + program + .command('view') + .description('Open workflow dashboard in browser') + .option('-p, --path ', 'Path to project directory', '.') + .option('--no-browser', 'Generate dashboard without opening browser') + .option('-o, --output ', 'Output path for generated HTML') + .action(viewCommand); + + // Install command + program + .command('install') + .description('Install Claude Code Workflow to your system') + .option('-m, --mode ', 'Installation mode: Global or Path') + .option('-p, --path ', 'Installation path (for Path mode)') + .option('-f, --force', 'Force installation without prompts') + .action(installCommand); + + // Uninstall command + program + .command('uninstall') + .description('Uninstall Claude Code Workflow') + .action(uninstallCommand); + + // List command + program + .command('list') + .description('List all installed Claude Code Workflow instances') + .action(listCommand); + + program.parse(argv); +} diff --git a/ccw/src/commands/install.js b/ccw/src/commands/install.js new file mode 100644 index 00000000..69fe10cd --- /dev/null +++ b/ccw/src/commands/install.js @@ -0,0 +1,309 @@ +import { existsSync, mkdirSync, readdirSync, statSync, copyFileSync, readFileSync, writeFileSync } from 'fs'; +import { join, dirname, basename, relative } from 'path'; +import { homedir } from 'os'; +import { fileURLToPath } from 'url'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import { showHeader, showBanner, createSpinner, success, info, warning, error, summaryBox, step, divider } from '../utils/ui.js'; +import { createManifest, addFileEntry, addDirectoryEntry, saveManifest, findManifest, getAllManifests } from '../core/manifest.js'; +import { validatePath } from '../utils/path-resolver.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Source directories to install +const SOURCE_DIRS = ['.claude', '.codex', '.gemini', '.qwen']; + +// Get package root directory (ccw/src/commands -> ccw) +function getPackageRoot() { + return join(__dirname, '..', '..'); +} + +// Get source installation directory (parent of ccw) +function getSourceDir() { + return join(getPackageRoot(), '..'); +} + +/** + * Install command handler + * @param {Object} options - Command options + */ +export async function installCommand(options) { + const version = getVersion(); + + // Show beautiful header + showHeader(version); + + // Check for existing installations + const existingManifests = getAllManifests(); + if (existingManifests.length > 0 && !options.force) { + info('Existing installations detected:'); + console.log(''); + existingManifests.forEach((m, i) => { + console.log(chalk.gray(` ${i + 1}. ${m.installation_mode} - ${m.installation_path}`)); + console.log(chalk.gray(` Installed: ${new Date(m.installation_date).toLocaleDateString()}`)); + }); + console.log(''); + + const { proceed } = await inquirer.prompt([{ + type: 'confirm', + name: 'proceed', + message: 'Continue with new installation?', + default: true + }]); + + if (!proceed) { + info('Installation cancelled'); + return; + } + } + + // Interactive mode selection + const mode = options.mode || await selectMode(); + + let installPath; + if (mode === 'Global') { + installPath = homedir(); + info(`Global installation to: ${installPath}`); + } else { + const inputPath = options.path || await selectPath(); + + // Validate the installation path + const pathValidation = validatePath(inputPath, { mustExist: true }); + if (!pathValidation.valid) { + error(`Invalid installation path: ${pathValidation.error}`); + process.exit(1); + } + + installPath = pathValidation.path; + info(`Path installation to: ${installPath}`); + } + + // Validate source directories exist + const sourceDir = getSourceDir(); + const availableDirs = SOURCE_DIRS.filter(dir => existsSync(join(sourceDir, dir))); + + if (availableDirs.length === 0) { + error('No source directories found to install.'); + error(`Expected directories in: ${sourceDir}`); + process.exit(1); + } + + console.log(''); + info(`Found ${availableDirs.length} directories to install: ${availableDirs.join(', ')}`); + divider(); + + // Check for existing installation at target path + const existingManifest = findManifest(installPath, mode); + if (existingManifest) { + warning('Existing installation found at this location'); + const { backup } = await inquirer.prompt([{ + type: 'confirm', + name: 'backup', + message: 'Create backup before reinstalling?', + default: true + }]); + + if (backup) { + await createBackup(installPath, existingManifest); + } + } + + // Create manifest + const manifest = createManifest(mode, installPath); + + // Perform installation + console.log(''); + const spinner = createSpinner('Installing files...').start(); + + let totalFiles = 0; + let totalDirs = 0; + + try { + for (const dir of availableDirs) { + const srcPath = join(sourceDir, dir); + const destPath = join(installPath, dir); + + spinner.text = `Installing ${dir}...`; + + const { files, directories } = await copyDirectory(srcPath, destPath, manifest); + totalFiles += files; + totalDirs += directories; + } + + // Create version.json + const versionPath = join(installPath, '.claude', 'version.json'); + if (existsSync(dirname(versionPath))) { + const versionInfo = { + version: version, + installedAt: new Date().toISOString(), + mode: mode, + installer: 'ccw' + }; + writeFileSync(versionPath, JSON.stringify(versionInfo, null, 2), 'utf8'); + addFileEntry(manifest, versionPath); + totalFiles++; + } + + spinner.succeed('Installation complete!'); + + } catch (err) { + spinner.fail('Installation failed'); + error(err.message); + process.exit(1); + } + + // Save manifest + const manifestPath = saveManifest(manifest); + + // Show summary + console.log(''); + summaryBox({ + title: ' Installation Summary ', + lines: [ + chalk.green.bold('✓ Installation Successful'), + '', + chalk.white(`Mode: ${chalk.cyan(mode)}`), + chalk.white(`Path: ${chalk.cyan(installPath)}`), + '', + chalk.gray(`Files installed: ${totalFiles}`), + chalk.gray(`Directories created: ${totalDirs}`), + '', + chalk.gray(`Manifest: ${basename(manifestPath)}`), + ], + borderColor: 'green' + }); + + // Show next steps + console.log(''); + info('Next steps:'); + console.log(chalk.gray(' 1. Restart Claude Code or your IDE')); + console.log(chalk.gray(' 2. Run: ccw view - to open the workflow dashboard')); + console.log(chalk.gray(' 3. Run: ccw uninstall - to remove this installation')); + console.log(''); +} + +/** + * Interactive mode selection + * @returns {Promise} - Selected mode + */ +async function selectMode() { + const { mode } = await inquirer.prompt([{ + type: 'list', + name: 'mode', + message: 'Select installation mode:', + choices: [ + { + name: `${chalk.cyan('Global')} - Install to home directory (recommended)`, + value: 'Global' + }, + { + name: `${chalk.yellow('Path')} - Install to specific project path`, + value: 'Path' + } + ] + }]); + + return mode; +} + +/** + * Interactive path selection + * @returns {Promise} - Selected path + */ +async function selectPath() { + const { path } = await inquirer.prompt([{ + type: 'input', + name: 'path', + message: 'Enter installation path:', + default: process.cwd(), + validate: (input) => { + if (!input) return 'Path is required'; + if (!existsSync(input)) { + return `Path does not exist: ${input}`; + } + return true; + } + }]); + + return path; +} + +/** + * Create backup of existing installation + * @param {string} installPath - Installation path + * @param {Object} manifest - Existing manifest + */ +async function createBackup(installPath, manifest) { + const spinner = createSpinner('Creating backup...').start(); + + try { + const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').split('.')[0]; + const backupDir = join(installPath, `.claude-backup-${timestamp}`); + + mkdirSync(backupDir, { recursive: true }); + + // Copy existing .claude directory + const claudeDir = join(installPath, '.claude'); + if (existsSync(claudeDir)) { + await copyDirectory(claudeDir, join(backupDir, '.claude')); + } + + spinner.succeed(`Backup created: ${backupDir}`); + } catch (err) { + spinner.warn(`Backup failed: ${err.message}`); + } +} + +/** + * Copy directory recursively + * @param {string} src - Source directory + * @param {string} dest - Destination directory + * @param {Object} manifest - Manifest to track files (optional) + * @returns {Object} - Count of files and directories + */ +async function copyDirectory(src, dest, manifest = null) { + let files = 0; + let directories = 0; + + // Create destination directory + if (!existsSync(dest)) { + mkdirSync(dest, { recursive: true }); + directories++; + if (manifest) addDirectoryEntry(manifest, dest); + } + + const entries = readdirSync(src); + + for (const entry of entries) { + const srcPath = join(src, entry); + const destPath = join(dest, entry); + const stat = statSync(srcPath); + + if (stat.isDirectory()) { + const result = await copyDirectory(srcPath, destPath, manifest); + files += result.files; + directories += result.directories; + } else { + copyFileSync(srcPath, destPath); + files++; + if (manifest) addFileEntry(manifest, destPath); + } + } + + return { files, directories }; +} + +/** + * Get package version + * @returns {string} - Version string + */ +function getVersion() { + try { + const pkgPath = join(getPackageRoot(), 'package.json'); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); + return pkg.version || '1.0.0'; + } catch { + return '1.0.0'; + } +} diff --git a/ccw/src/commands/list.js b/ccw/src/commands/list.js new file mode 100644 index 00000000..6949b4ca --- /dev/null +++ b/ccw/src/commands/list.js @@ -0,0 +1,37 @@ +import chalk from 'chalk'; +import { showBanner, divider, info } from '../utils/ui.js'; +import { getAllManifests } from '../core/manifest.js'; + +/** + * List command handler - shows all installations + */ +export async function listCommand() { + showBanner(); + console.log(chalk.cyan.bold(' Installed Claude Code Workflow Instances\n')); + + const manifests = getAllManifests(); + + if (manifests.length === 0) { + info('No installations found.'); + console.log(''); + console.log(chalk.gray(' Run: ccw install - to install Claude Code Workflow')); + console.log(''); + return; + } + + manifests.forEach((m, i) => { + const modeColor = m.installation_mode === 'Global' ? chalk.cyan : chalk.yellow; + + console.log(chalk.white.bold(` ${i + 1}. `) + modeColor.bold(m.installation_mode)); + console.log(chalk.gray(` Path: ${m.installation_path}`)); + console.log(chalk.gray(` Date: ${new Date(m.installation_date).toLocaleDateString()}`)); + console.log(chalk.gray(` Version: ${m.application_version}`)); + console.log(chalk.gray(` Files: ${m.files_count}`)); + console.log(chalk.gray(` Dirs: ${m.directories_count}`)); + console.log(''); + }); + + divider(); + console.log(chalk.gray(' Run: ccw uninstall - to remove an installation')); + console.log(''); +} diff --git a/ccw/src/commands/uninstall.js b/ccw/src/commands/uninstall.js new file mode 100644 index 00000000..0a7518f7 --- /dev/null +++ b/ccw/src/commands/uninstall.js @@ -0,0 +1,238 @@ +import { existsSync, unlinkSync, rmdirSync, readdirSync, statSync } from 'fs'; +import { join, dirname, basename } from 'path'; +import inquirer from 'inquirer'; +import chalk from 'chalk'; +import { showBanner, createSpinner, success, info, warning, error, summaryBox, divider } from '../utils/ui.js'; +import { getAllManifests, deleteManifest } from '../core/manifest.js'; + +/** + * Uninstall command handler + * @param {Object} options - Command options + */ +export async function uninstallCommand(options) { + showBanner(); + console.log(chalk.cyan.bold(' Uninstall Claude Code Workflow\n')); + + // Get all manifests + const manifests = getAllManifests(); + + if (manifests.length === 0) { + warning('No installations found.'); + info('Nothing to uninstall.'); + return; + } + + // Display installations + console.log(chalk.white.bold(' Found installations:\n')); + + manifests.forEach((m, i) => { + const modeColor = m.installation_mode === 'Global' ? chalk.cyan : chalk.yellow; + console.log(chalk.white(` ${i + 1}. `) + modeColor.bold(m.installation_mode)); + console.log(chalk.gray(` Path: ${m.installation_path}`)); + console.log(chalk.gray(` Date: ${new Date(m.installation_date).toLocaleDateString()}`)); + console.log(chalk.gray(` Version: ${m.application_version}`)); + console.log(chalk.gray(` Files: ${m.files_count} | Dirs: ${m.directories_count}`)); + console.log(''); + }); + + divider(); + + // Select installation to uninstall + let selectedManifest; + + if (manifests.length === 1) { + const { confirm } = await inquirer.prompt([{ + type: 'confirm', + name: 'confirm', + message: `Uninstall ${manifests[0].installation_mode} installation at ${manifests[0].installation_path}?`, + default: false + }]); + + if (!confirm) { + info('Uninstall cancelled'); + return; + } + + selectedManifest = manifests[0]; + } else { + const choices = manifests.map((m, i) => ({ + name: `${m.installation_mode} - ${m.installation_path}`, + value: i + })); + + choices.push({ name: chalk.gray('Cancel'), value: -1 }); + + const { selection } = await inquirer.prompt([{ + type: 'list', + name: 'selection', + message: 'Select installation to uninstall:', + choices + }]); + + if (selection === -1) { + info('Uninstall cancelled'); + return; + } + + selectedManifest = manifests[selection]; + + // Confirm selection + const { confirm } = await inquirer.prompt([{ + type: 'confirm', + name: 'confirm', + message: `Are you sure you want to uninstall ${selectedManifest.installation_mode} installation?`, + default: false + }]); + + if (!confirm) { + info('Uninstall cancelled'); + return; + } + } + + console.log(''); + + // Perform uninstallation + const spinner = createSpinner('Removing files...').start(); + + let removedFiles = 0; + let removedDirs = 0; + let failedFiles = []; + + try { + // Remove files first (in reverse order to handle nested files) + const files = [...(selectedManifest.files || [])].reverse(); + + for (const fileEntry of files) { + const filePath = fileEntry.path; + spinner.text = `Removing: ${basename(filePath)}`; + + try { + if (existsSync(filePath)) { + unlinkSync(filePath); + removedFiles++; + } + } catch (err) { + failedFiles.push({ path: filePath, error: err.message }); + } + } + + // Remove directories (in reverse order to remove nested dirs first) + const directories = [...(selectedManifest.directories || [])].reverse(); + + // Sort by path length (deepest first) + directories.sort((a, b) => b.path.length - a.path.length); + + for (const dirEntry of directories) { + const dirPath = dirEntry.path; + spinner.text = `Removing directory: ${basename(dirPath)}`; + + try { + if (existsSync(dirPath)) { + // Only remove if empty + const contents = readdirSync(dirPath); + if (contents.length === 0) { + rmdirSync(dirPath); + removedDirs++; + } + } + } catch (err) { + // Ignore directory removal errors (might not be empty) + } + } + + // Try to clean up parent directories if empty + const installPath = selectedManifest.installation_path; + for (const dir of ['.claude', '.codex', '.gemini', '.qwen']) { + const dirPath = join(installPath, dir); + try { + if (existsSync(dirPath)) { + await removeEmptyDirs(dirPath); + } + } catch { + // Ignore + } + } + + spinner.succeed('Uninstall complete!'); + + } catch (err) { + spinner.fail('Uninstall failed'); + error(err.message); + return; + } + + // Delete manifest + deleteManifest(selectedManifest.manifest_file); + + // Show summary + console.log(''); + + if (failedFiles.length > 0) { + summaryBox({ + title: ' Uninstall Summary ', + lines: [ + chalk.yellow.bold('⚠ Partially Completed'), + '', + chalk.white(`Files removed: ${chalk.green(removedFiles)}`), + chalk.white(`Directories removed: ${chalk.green(removedDirs)}`), + chalk.white(`Failed: ${chalk.red(failedFiles.length)}`), + '', + chalk.gray('Some files could not be removed.'), + chalk.gray('They may be in use or require elevated permissions.'), + ], + borderColor: 'yellow' + }); + + if (process.env.DEBUG) { + console.log(''); + console.log(chalk.gray('Failed files:')); + failedFiles.forEach(f => { + console.log(chalk.red(` ${f.path}: ${f.error}`)); + }); + } + } else { + summaryBox({ + title: ' Uninstall Summary ', + lines: [ + chalk.green.bold('✓ Successfully Uninstalled'), + '', + chalk.white(`Files removed: ${chalk.green(removedFiles)}`), + chalk.white(`Directories removed: ${chalk.green(removedDirs)}`), + '', + chalk.gray('Manifest removed'), + ], + borderColor: 'green' + }); + } + + console.log(''); +} + +/** + * Recursively remove empty directories + * @param {string} dirPath - Directory path + */ +async function removeEmptyDirs(dirPath) { + if (!existsSync(dirPath)) return; + + const stat = statSync(dirPath); + if (!stat.isDirectory()) return; + + let files = readdirSync(dirPath); + + // Recursively check subdirectories + for (const file of files) { + const filePath = join(dirPath, file); + if (statSync(filePath).isDirectory()) { + await removeEmptyDirs(filePath); + } + } + + // Re-check after processing subdirectories + files = readdirSync(dirPath); + if (files.length === 0) { + rmdirSync(dirPath); + } +} + diff --git a/ccw/src/commands/view.js b/ccw/src/commands/view.js new file mode 100644 index 00000000..8a30e14f --- /dev/null +++ b/ccw/src/commands/view.js @@ -0,0 +1,132 @@ +import { scanSessions } from '../core/session-scanner.js'; +import { aggregateData } from '../core/data-aggregator.js'; +import { generateDashboard } from '../core/dashboard-generator.js'; +import { launchBrowser, isHeadlessEnvironment } from '../utils/browser-launcher.js'; +import { resolvePath, ensureDir, getWorkflowDir, validatePath, validateOutputPath } from '../utils/path-resolver.js'; +import chalk from 'chalk'; +import { writeFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; + +/** + * View command handler - generates and opens workflow dashboard + * @param {Object} options - Command options + */ +export async function viewCommand(options) { + // Validate project path + const pathValidation = validatePath(options.path, { mustExist: true }); + if (!pathValidation.valid) { + console.error(chalk.red(`\n Error: ${pathValidation.error}\n`)); + process.exit(1); + } + + const workingDir = pathValidation.path; + const workflowDir = join(workingDir, '.workflow'); + + console.log(chalk.blue.bold('\n CCW Dashboard Generator\n')); + console.log(chalk.gray(` Project: ${workingDir}`)); + console.log(chalk.gray(` Workflow: ${workflowDir}\n`)); + + // Check if .workflow directory exists + if (!existsSync(workflowDir)) { + console.log(chalk.yellow(' No .workflow directory found.')); + console.log(chalk.gray(' This project may not have any workflow sessions yet.\n')); + + // Still generate an empty dashboard + const emptyData = { + generatedAt: new Date().toISOString(), + activeSessions: [], + archivedSessions: [], + reviewData: null, + statistics: { + totalSessions: 0, + activeSessions: 0, + totalTasks: 0, + completedTasks: 0, + reviewFindings: 0 + } + }; + + await generateAndOpen(emptyData, workflowDir, options); + return; + } + + try { + // Step 1: Scan for sessions + console.log(chalk.cyan(' Scanning sessions...')); + const sessions = await scanSessions(workflowDir); + console.log(chalk.green(` Found ${sessions.active.length} active, ${sessions.archived.length} archived sessions`)); + + if (sessions.hasReviewData) { + console.log(chalk.magenta(' Review data detected - will include Reviews tab')); + } + + // Step 2: Aggregate all data + console.log(chalk.cyan(' Aggregating data...')); + const dashboardData = await aggregateData(sessions, workflowDir); + + // Log statistics + const stats = dashboardData.statistics; + console.log(chalk.gray(` Tasks: ${stats.completedTasks}/${stats.totalTasks} completed`)); + if (stats.reviewFindings > 0) { + console.log(chalk.gray(` Review findings: ${stats.reviewFindings}`)); + } + + // Step 3 & 4: Generate and open + await generateAndOpen(dashboardData, workflowDir, options); + + } catch (error) { + console.error(chalk.red(`\n Error: ${error.message}\n`)); + if (process.env.DEBUG) { + console.error(error.stack); + } + process.exit(1); + } +} + +/** + * Generate dashboard and optionally open in browser + * @param {Object} data - Dashboard data + * @param {string} workflowDir - Path to .workflow + * @param {Object} options - Command options + */ +async function generateAndOpen(data, workflowDir, options) { + // Step 3: Generate dashboard HTML + console.log(chalk.cyan(' Generating dashboard...')); + const html = await generateDashboard(data); + + // Step 4: Validate and write dashboard file + let outputPath; + if (options.output) { + const outputValidation = validateOutputPath(options.output, workflowDir); + if (!outputValidation.valid) { + console.error(chalk.red(`\n Error: ${outputValidation.error}\n`)); + process.exit(1); + } + outputPath = outputValidation.path; + } else { + outputPath = join(workflowDir, 'dashboard.html'); + } + + ensureDir(dirname(outputPath)); + writeFileSync(outputPath, html, 'utf8'); + console.log(chalk.green(` Dashboard saved: ${outputPath}`)); + + // Step 5: Open in browser (unless --no-browser or headless environment) + if (options.browser !== false) { + if (isHeadlessEnvironment()) { + console.log(chalk.yellow('\n Running in CI/headless environment - skipping browser launch')); + console.log(chalk.gray(` Open manually: file://${outputPath.replace(/\\/g, '/')}\n`)); + } else { + console.log(chalk.cyan(' Opening in browser...')); + try { + await launchBrowser(outputPath); + console.log(chalk.green.bold('\n Dashboard opened in browser!\n')); + } catch (error) { + console.log(chalk.yellow(`\n Could not open browser: ${error.message}`)); + console.log(chalk.gray(` Open manually: file://${outputPath.replace(/\\/g, '/')}\n`)); + } + } + } else { + console.log(chalk.gray(`\n Open in browser: file://${outputPath.replace(/\\/g, '/')}\n`)); + } +} diff --git a/ccw/src/core/dashboard-generator.js b/ccw/src/core/dashboard-generator.js new file mode 100644 index 00000000..b3608861 --- /dev/null +++ b/ccw/src/core/dashboard-generator.js @@ -0,0 +1,577 @@ +import { readFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +// Bundled template paths +const WORKFLOW_TEMPLATE = join(__dirname, '../templates/workflow-dashboard.html'); +const REVIEW_TEMPLATE = join(__dirname, '../templates/review-cycle-dashboard.html'); + +/** + * Generate dashboard HTML from aggregated data + * Uses bundled templates from ccw package + * @param {Object} data - Aggregated dashboard data + * @returns {Promise} - Generated HTML + */ +export async function generateDashboard(data) { + // Use bundled workflow template + if (existsSync(WORKFLOW_TEMPLATE)) { + return generateFromBundledTemplate(data, WORKFLOW_TEMPLATE); + } + + // Fallback to inline dashboard if template missing + return generateInlineDashboard(data); +} + +/** + * Generate dashboard using bundled template + * @param {Object} data - Dashboard data + * @param {string} templatePath - Path to workflow-dashboard.html + * @returns {string} - Generated HTML + */ +function generateFromBundledTemplate(data, templatePath) { + let html = readFileSync(templatePath, 'utf8'); + + // Prepare workflow data for injection + const workflowData = { + activeSessions: data.activeSessions, + archivedSessions: data.archivedSessions + }; + + // Inject workflow data + html = html.replace('{{WORKFLOW_DATA}}', JSON.stringify(workflowData, null, 2)); + + // If we have review data, add a review tab + if (data.reviewData && data.reviewData.totalFindings > 0) { + html = injectReviewTab(html, data.reviewData); + } + + return html; +} + +/** + * Inject review tab into existing dashboard + * @param {string} html - Base dashboard HTML + * @param {Object} reviewData - Review data to display + * @returns {string} - Modified HTML with review tab + */ +function injectReviewTab(html, reviewData) { + // Add review tab button in header controls + const tabButtonHtml = ` + + `; + + // Insert after filter-group + html = html.replace( + '\n \n ', + ` +
+ ${tabButtonHtml} +
+ + ` + ); + + // Add review section before closing container + const reviewSectionHtml = generateReviewSection(reviewData); + + html = html.replace( + '\n\n + ${hasReviews ? '' : ''} + + + +
+
+
+
${stats.totalSessions}
+
Total Sessions
+
+
+
${stats.activeSessions}
+
Active Sessions
+
+
+
${stats.totalTasks}
+
Total Tasks
+
+
+
${stats.completedTasks}
+
Completed Tasks
+
+
+ +
+

Active Sessions

+
+ ${data.activeSessions.length === 0 + ? '
No active sessions
' + : data.activeSessions.map(s => renderSessionCard(s, true)).join('')} +
+
+ +
+

Archived Sessions

+
+ ${data.archivedSessions.length === 0 + ? '
No archived sessions
' + : data.archivedSessions.map(s => renderSessionCard(s, false)).join('')} +
+
+
+ + ${hasReviews ? renderReviewTab(data.reviewData) : ''} + + + + + + +`; +} + +/** + * Render a session card + * @param {Object} session - Session data + * @param {boolean} isActive - Whether session is active + * @returns {string} - HTML string + */ +function renderSessionCard(session, isActive) { + const completedTasks = isActive + ? session.tasks.filter(t => t.status === 'completed').length + : session.taskCount; + const totalTasks = isActive ? session.tasks.length : session.taskCount; + const progress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0; + + const tasksHtml = isActive && session.tasks.length > 0 + ? session.tasks.map(t => ` +
+
${t.title}
+ ${t.task_id} +
+ `).join('') + : ''; + + return ` +
+
${session.session_id}
+
+ ${session.project ? `
${session.project}
` : ''} +
${session.created_at} | ${completedTasks}/${totalTasks} tasks
+
+ ${totalTasks > 0 ? ` +
+
+
+ ` : ''} + ${tasksHtml} +
+ `; +} + +/** + * Render review tab HTML + * @param {Object} reviewData - Review data + * @returns {string} - HTML string + */ +function renderReviewTab(reviewData) { + const { severityDistribution, dimensionSummary } = reviewData; + + return ` + + `; +} diff --git a/ccw/src/core/data-aggregator.js b/ccw/src/core/data-aggregator.js new file mode 100644 index 00000000..5de5c075 --- /dev/null +++ b/ccw/src/core/data-aggregator.js @@ -0,0 +1,288 @@ +import { glob } from 'glob'; +import { readFileSync, existsSync } from 'fs'; +import { join, basename } from 'path'; + +/** + * Aggregate all data for dashboard rendering + * @param {Object} sessions - Scanned sessions from session-scanner + * @param {string} workflowDir - Path to .workflow directory + * @returns {Promise} - Aggregated dashboard data + */ +export async function aggregateData(sessions, workflowDir) { + const data = { + generatedAt: new Date().toISOString(), + activeSessions: [], + archivedSessions: [], + reviewData: null, + statistics: { + totalSessions: 0, + activeSessions: 0, + totalTasks: 0, + completedTasks: 0, + reviewFindings: 0 + } + }; + + // Process active sessions + for (const session of sessions.active) { + const sessionData = await processSession(session, true); + data.activeSessions.push(sessionData); + data.statistics.totalTasks += sessionData.tasks.length; + data.statistics.completedTasks += sessionData.tasks.filter(t => t.status === 'completed').length; + } + + // Process archived sessions + for (const session of sessions.archived) { + const sessionData = await processSession(session, false); + data.archivedSessions.push(sessionData); + data.statistics.totalTasks += sessionData.taskCount || 0; + data.statistics.completedTasks += sessionData.taskCount || 0; + } + + // Aggregate review data if present + if (sessions.hasReviewData) { + data.reviewData = await aggregateReviewData(sessions.active); + data.statistics.reviewFindings = data.reviewData.totalFindings; + } + + data.statistics.totalSessions = sessions.active.length + sessions.archived.length; + data.statistics.activeSessions = sessions.active.length; + + return data; +} + +/** + * Process a single session, loading tasks and review info + * @param {Object} session - Session object from scanner + * @param {boolean} isActive - Whether session is active + * @returns {Promise} - Processed session data + */ +async function processSession(session, isActive) { + const result = { + session_id: session.session_id, + project: session.project || session.session_id, + status: session.status || (isActive ? 'active' : 'archived'), + created_at: formatDate(session.created_at), + archived_at: formatDate(session.archived_at), + path: session.path, + tasks: [], + taskCount: 0, + hasReview: false, + reviewSummary: null + }; + + // Load tasks for active sessions (full details) + if (isActive) { + const taskDir = join(session.path, '.task'); + if (existsSync(taskDir)) { + const taskFiles = await safeGlob('IMPL-*.json', taskDir); + for (const taskFile of taskFiles) { + try { + const taskData = JSON.parse(readFileSync(join(taskDir, taskFile), 'utf8')); + result.tasks.push({ + task_id: taskData.id || basename(taskFile, '.json'), + title: taskData.title || 'Untitled Task', + status: taskData.status || 'pending', + type: taskData.meta?.type || 'task' + }); + } catch { + // Skip invalid task files + } + } + // Sort tasks by ID + result.tasks.sort((a, b) => sortTaskIds(a.task_id, b.task_id)); + } + result.taskCount = result.tasks.length; + + // Check for review data + const reviewDir = join(session.path, '.review'); + if (existsSync(reviewDir)) { + result.hasReview = true; + result.reviewSummary = loadReviewSummary(reviewDir); + } + } else { + // For archived, just count tasks + const taskDir = join(session.path, '.task'); + if (existsSync(taskDir)) { + const taskFiles = await safeGlob('IMPL-*.json', taskDir); + result.taskCount = taskFiles.length; + } + } + + return result; +} + +/** + * Aggregate review data from all active sessions with reviews + * @param {Array} activeSessions - Active session objects + * @returns {Promise} - Aggregated review data + */ +async function aggregateReviewData(activeSessions) { + const reviewData = { + totalFindings: 0, + severityDistribution: { critical: 0, high: 0, medium: 0, low: 0 }, + dimensionSummary: {}, + sessions: [] + }; + + for (const session of activeSessions) { + const reviewDir = join(session.path, '.review'); + if (!existsSync(reviewDir)) continue; + + const reviewProgress = loadReviewProgress(reviewDir); + const dimensionData = await loadDimensionData(reviewDir); + + if (reviewProgress || dimensionData.length > 0) { + const sessionReview = { + session_id: session.session_id, + progress: reviewProgress, + dimensions: dimensionData, + findings: [] + }; + + // Collect and count findings + for (const dim of dimensionData) { + if (dim.findings && Array.isArray(dim.findings)) { + for (const finding of dim.findings) { + const severity = (finding.severity || 'low').toLowerCase(); + if (reviewData.severityDistribution.hasOwnProperty(severity)) { + reviewData.severityDistribution[severity]++; + } + reviewData.totalFindings++; + sessionReview.findings.push({ + ...finding, + dimension: dim.name + }); + } + } + + // Track dimension summary + if (!reviewData.dimensionSummary[dim.name]) { + reviewData.dimensionSummary[dim.name] = { count: 0, sessions: [] }; + } + reviewData.dimensionSummary[dim.name].count += dim.findings?.length || 0; + reviewData.dimensionSummary[dim.name].sessions.push(session.session_id); + } + + reviewData.sessions.push(sessionReview); + } + } + + return reviewData; +} + +/** + * Load review progress from review-progress.json + * @param {string} reviewDir - Path to .review directory + * @returns {Object|null} + */ +function loadReviewProgress(reviewDir) { + const progressFile = join(reviewDir, 'review-progress.json'); + if (!existsSync(progressFile)) return null; + try { + return JSON.parse(readFileSync(progressFile, 'utf8')); + } catch { + return null; + } +} + +/** + * Load review summary from review-state.json + * @param {string} reviewDir - Path to .review directory + * @returns {Object|null} + */ +function loadReviewSummary(reviewDir) { + const stateFile = join(reviewDir, 'review-state.json'); + if (!existsSync(stateFile)) return null; + try { + const state = JSON.parse(readFileSync(stateFile, 'utf8')); + return { + phase: state.phase || 'unknown', + severityDistribution: state.severity_distribution || {}, + criticalFiles: (state.critical_files || []).slice(0, 3), + status: state.status || 'in_progress' + }; + } catch { + return null; + } +} + +/** + * Load dimension data from .review/dimensions/ + * @param {string} reviewDir - Path to .review directory + * @returns {Promise} + */ +async function loadDimensionData(reviewDir) { + const dimensionsDir = join(reviewDir, 'dimensions'); + if (!existsSync(dimensionsDir)) return []; + + const dimensions = []; + const dimFiles = await safeGlob('*.json', dimensionsDir); + + for (const file of dimFiles) { + try { + const data = JSON.parse(readFileSync(join(dimensionsDir, file), 'utf8')); + dimensions.push({ + name: basename(file, '.json'), + findings: Array.isArray(data) ? data : (data.findings || []), + status: data.status || 'completed' + }); + } catch { + // Skip invalid dimension files + } + } + + return dimensions; +} + +/** + * Safe glob wrapper that returns empty array on error + * @param {string} pattern - Glob pattern + * @param {string} cwd - Current working directory + * @returns {Promise} + */ +async function safeGlob(pattern, cwd) { + try { + return await glob(pattern, { cwd, absolute: false }); + } catch { + return []; + } +} + +/** + * Format date for display + * @param {string|null} dateStr - ISO date string + * @returns {string} + */ +function formatDate(dateStr) { + if (!dateStr) return 'N/A'; + try { + const date = new Date(dateStr); + return date.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch { + return dateStr; + } +} + +/** + * Sort task IDs numerically (IMPL-1, IMPL-2, IMPL-1.1, etc.) + * @param {string} a - First task ID + * @param {string} b - Second task ID + * @returns {number} + */ +function sortTaskIds(a, b) { + const parseId = (id) => { + const match = id.match(/IMPL-(\d+)(?:\.(\d+))?/); + if (!match) return [0, 0]; + return [parseInt(match[1]), parseInt(match[2] || 0)]; + }; + const [a1, a2] = parseId(a); + const [b1, b2] = parseId(b); + return a1 - b1 || a2 - b2; +} diff --git a/ccw/src/core/manifest.js b/ccw/src/core/manifest.js new file mode 100644 index 00000000..40979299 --- /dev/null +++ b/ccw/src/core/manifest.js @@ -0,0 +1,201 @@ +import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync, statSync } from 'fs'; +import { join, dirname } from 'path'; +import { homedir } from 'os'; + +// Manifest directory location +const MANIFEST_DIR = join(homedir(), '.claude-manifests'); + +/** + * Ensure manifest directory exists + */ +function ensureManifestDir() { + if (!existsSync(MANIFEST_DIR)) { + mkdirSync(MANIFEST_DIR, { recursive: true }); + } +} + +/** + * Create a new installation manifest + * @param {string} mode - Installation mode (Global/Path) + * @param {string} installPath - Installation path + * @returns {Object} - New manifest object + */ +export function createManifest(mode, installPath) { + ensureManifestDir(); + + const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').split('.')[0]; + const modePrefix = mode === 'Global' ? 'manifest-global' : 'manifest-path'; + const manifestId = `${modePrefix}-${timestamp}`; + + return { + manifest_id: manifestId, + version: '1.0', + installation_mode: mode, + installation_path: installPath, + installation_date: new Date().toISOString(), + installer_version: '1.0.0', + files: [], + directories: [] + }; +} + +/** + * Add file entry to manifest + * @param {Object} manifest - Manifest object + * @param {string} filePath - File path + */ +export function addFileEntry(manifest, filePath) { + manifest.files.push({ + path: filePath, + type: 'File', + timestamp: new Date().toISOString() + }); +} + +/** + * Add directory entry to manifest + * @param {Object} manifest - Manifest object + * @param {string} dirPath - Directory path + */ +export function addDirectoryEntry(manifest, dirPath) { + manifest.directories.push({ + path: dirPath, + type: 'Directory', + timestamp: new Date().toISOString() + }); +} + +/** + * Save manifest to disk + * @param {Object} manifest - Manifest object + * @returns {string} - Path to saved manifest + */ +export function saveManifest(manifest) { + ensureManifestDir(); + + // Remove old manifests for same path and mode + removeOldManifests(manifest.installation_path, manifest.installation_mode); + + const manifestPath = join(MANIFEST_DIR, `${manifest.manifest_id}.json`); + writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8'); + + return manifestPath; +} + +/** + * Remove old manifests for the same installation path and mode + * @param {string} installPath - Installation path + * @param {string} mode - Installation mode + */ +function removeOldManifests(installPath, mode) { + if (!existsSync(MANIFEST_DIR)) return; + + const normalizedPath = installPath.toLowerCase().replace(/[\\/]+$/, ''); + + try { + const files = readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json')); + + for (const file of files) { + try { + const filePath = join(MANIFEST_DIR, file); + const content = JSON.parse(readFileSync(filePath, 'utf8')); + + const manifestPath = (content.installation_path || '').toLowerCase().replace(/[\\/]+$/, ''); + const manifestMode = content.installation_mode || 'Global'; + + if (manifestPath === normalizedPath && manifestMode === mode) { + unlinkSync(filePath); + } + } catch { + // Skip invalid manifest files + } + } + } catch { + // Ignore errors + } +} + +/** + * Get all installation manifests + * @returns {Array} - Array of manifest objects + */ +export function getAllManifests() { + if (!existsSync(MANIFEST_DIR)) return []; + + const manifests = []; + + try { + const files = readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json')); + + for (const file of files) { + try { + const filePath = join(MANIFEST_DIR, file); + const content = JSON.parse(readFileSync(filePath, 'utf8')); + + // Try to read version.json for application version + let appVersion = 'unknown'; + try { + const versionPath = join(content.installation_path, '.claude', 'version.json'); + if (existsSync(versionPath)) { + const versionInfo = JSON.parse(readFileSync(versionPath, 'utf8')); + appVersion = versionInfo.version || 'unknown'; + } + } catch { + // Ignore + } + + manifests.push({ + ...content, + manifest_file: filePath, + application_version: appVersion, + files_count: content.files?.length || 0, + directories_count: content.directories?.length || 0 + }); + } catch { + // Skip invalid manifest files + } + } + + // Sort by installation date (newest first) + manifests.sort((a, b) => new Date(b.installation_date) - new Date(a.installation_date)); + + } catch { + // Ignore errors + } + + return manifests; +} + +/** + * Find manifest for a specific path and mode + * @param {string} installPath - Installation path + * @param {string} mode - Installation mode + * @returns {Object|null} - Manifest or null + */ +export function findManifest(installPath, mode) { + const manifests = getAllManifests(); + const normalizedPath = installPath.toLowerCase().replace(/[\\/]+$/, ''); + + return manifests.find(m => { + const manifestPath = (m.installation_path || '').toLowerCase().replace(/[\\/]+$/, ''); + return manifestPath === normalizedPath && m.installation_mode === mode; + }) || null; +} + +/** + * Delete a manifest file + * @param {string} manifestFile - Path to manifest file + */ +export function deleteManifest(manifestFile) { + if (existsSync(manifestFile)) { + unlinkSync(manifestFile); + } +} + +/** + * Get manifest directory path + * @returns {string} + */ +export function getManifestDir() { + return MANIFEST_DIR; +} diff --git a/ccw/src/core/session-scanner.js b/ccw/src/core/session-scanner.js new file mode 100644 index 00000000..14de994e --- /dev/null +++ b/ccw/src/core/session-scanner.js @@ -0,0 +1,159 @@ +import { glob } from 'glob'; +import { readFileSync, existsSync, statSync, readdirSync } from 'fs'; +import { join, basename } from 'path'; + +/** + * Scan .workflow directory for active and archived sessions + * @param {string} workflowDir - Path to .workflow directory + * @returns {Promise<{active: Array, archived: Array, hasReviewData: boolean}>} + */ +export async function scanSessions(workflowDir) { + const result = { + active: [], + archived: [], + hasReviewData: false + }; + + if (!existsSync(workflowDir)) { + return result; + } + + // Scan active sessions + const activeDir = join(workflowDir, 'active'); + if (existsSync(activeDir)) { + const activeSessions = await findWfsSessions(activeDir); + for (const sessionName of activeSessions) { + const sessionPath = join(activeDir, sessionName); + const sessionData = readSessionData(sessionPath); + if (sessionData) { + result.active.push({ + ...sessionData, + path: sessionPath, + isActive: true + }); + // Check for review data + if (existsSync(join(sessionPath, '.review'))) { + result.hasReviewData = true; + } + } + } + } + + // Scan archived sessions + const archivesDir = join(workflowDir, 'archives'); + if (existsSync(archivesDir)) { + const archivedSessions = await findWfsSessions(archivesDir); + for (const sessionName of archivedSessions) { + const sessionPath = join(archivesDir, sessionName); + const sessionData = readSessionData(sessionPath); + if (sessionData) { + result.archived.push({ + ...sessionData, + path: sessionPath, + isActive: false + }); + } + } + } + + // Sort by creation date (newest first) + result.active.sort((a, b) => new Date(b.created_at || 0) - new Date(a.created_at || 0)); + result.archived.sort((a, b) => new Date(b.archived_at || b.created_at || 0) - new Date(a.archived_at || a.created_at || 0)); + + return result; +} + +/** + * Find WFS-* directories in a given path + * @param {string} dir - Directory to search + * @returns {Promise} - Array of session directory names + */ +async function findWfsSessions(dir) { + try { + // Use glob for cross-platform pattern matching + const sessions = await glob('WFS-*', { + cwd: dir, + onlyDirectories: true, + absolute: false + }); + return sessions; + } catch { + // Fallback: manual directory listing + try { + const entries = readdirSync(dir, { withFileTypes: true }); + return entries + .filter(e => e.isDirectory() && e.name.startsWith('WFS-')) + .map(e => e.name); + } catch { + return []; + } + } +} + +/** + * Read session data from workflow-session.json or create minimal from directory + * @param {string} sessionPath - Path to session directory + * @returns {Object|null} - Session data object or null if invalid + */ +function readSessionData(sessionPath) { + const sessionFile = join(sessionPath, 'workflow-session.json'); + + if (existsSync(sessionFile)) { + try { + const data = JSON.parse(readFileSync(sessionFile, 'utf8')); + return { + session_id: data.session_id || basename(sessionPath), + project: data.project || data.description || '', + status: data.status || 'active', + created_at: data.created_at || data.initialized_at || null, + archived_at: data.archived_at || null, + type: data.type || 'workflow' + }; + } catch { + // Fall through to minimal session + } + } + + // Fallback: create minimal session from directory info + try { + const stats = statSync(sessionPath); + return { + session_id: basename(sessionPath), + project: '', + status: 'unknown', + created_at: stats.birthtime.toISOString(), + archived_at: null, + type: 'workflow' + }; + } catch { + return null; + } +} + +/** + * Check if session has review data + * @param {string} sessionPath - Path to session directory + * @returns {boolean} + */ +export function hasReviewData(sessionPath) { + const reviewDir = join(sessionPath, '.review'); + return existsSync(reviewDir); +} + +/** + * Get list of task files in session + * @param {string} sessionPath - Path to session directory + * @returns {Promise} + */ +export async function getTaskFiles(sessionPath) { + const taskDir = join(sessionPath, '.task'); + if (!existsSync(taskDir)) { + return []; + } + + try { + return await glob('IMPL-*.json', { cwd: taskDir, absolute: false }); + } catch { + return []; + } +} diff --git a/ccw/src/index.js b/ccw/src/index.js new file mode 100644 index 00000000..5881089e --- /dev/null +++ b/ccw/src/index.js @@ -0,0 +1,9 @@ +/** + * CCW - Claude Code Workflow CLI + * Main exports for programmatic usage + */ + +export { run } from './cli.js'; +export { scanSessions } from './core/session-scanner.js'; +export { aggregateData } from './core/data-aggregator.js'; +export { generateDashboard } from './core/dashboard-generator.js'; diff --git a/ccw/src/templates/review-cycle-dashboard.html b/ccw/src/templates/review-cycle-dashboard.html new file mode 100644 index 00000000..b308a748 --- /dev/null +++ b/ccw/src/templates/review-cycle-dashboard.html @@ -0,0 +1,2816 @@ + + + + + + Code Review Dashboard - {{SESSION_ID}} + + + +
+
+

🔍 Code Review Dashboard

+
+ 📋 Session: Loading... + 🆔 Review ID: Loading... + 🕒 Last Updated: Loading... +
+ +
+ + +
+ 0 findings selected + + +
+ + + +
+
+ + +
+
+

Fix Progress

+ PLANNING +
+ + +
+
+ + + + + + + + + +
+ + +
+
+

Review Progress

+ LOADING +
+
+
+
+
+ Initializing... +
+
+ + +
+
+
🔴
+
0
+
Critical
+
+
+
🟠
+
0
+
High
+
+
+
🟡
+
0
+
Medium
+
+
+
đŸŸĸ
+
0
+
Low
+
+
+ + +
+
Findings by Dimension
+ + + + + + + + + + + + + + + +
DimensionCriticalHighMediumLowTotalStatus
+
+ + +
+ + + + + + + + +
+ + +
+
+
đŸŽ¯ Advanced Filters & Sort
+ +
+
+ +
+ Severity: +
+ + + + +
+
+ + +
+ Sort: + + +
+ + +
+ Select: + + + +
+
+
+ + +
+
+

Findings (0)

+
+
+
+
âŗ
+

Loading findings...

+
+
+
+
+ + +
+
+
+
Finding Details
+ +
+
+ +
+
+ + +
+
+
+
📜 Fix History
+ +
+
+ +
+
+ + + + + + diff --git a/ccw/src/templates/workflow-dashboard.html b/ccw/src/templates/workflow-dashboard.html new file mode 100644 index 00000000..96744194 --- /dev/null +++ b/ccw/src/templates/workflow-dashboard.html @@ -0,0 +1,664 @@ + + + + + + Workflow Dashboard - Task Board + + + +
+
+

🚀 Workflow Dashboard

+

Task Board - Active and Archived Sessions

+ +
+ + +
+ + + +
+
+
+ +
+
+
0
+
Total Sessions
+
+
+
0
+
Active Sessions
+
+
+
0
+
Total Tasks
+
+
+
0
+
Completed Tasks
+
+
+ +
+
+

📋 Active Sessions

+
+
+
+ +
+
+

đŸ“Ļ Archived Sessions

+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/ccw/src/utils/browser-launcher.js b/ccw/src/utils/browser-launcher.js new file mode 100644 index 00000000..ba2a9d05 --- /dev/null +++ b/ccw/src/utils/browser-launcher.js @@ -0,0 +1,49 @@ +import open from 'open'; +import { platform } from 'os'; +import { resolve } from 'path'; + +/** + * Launch a file in the default browser + * Cross-platform compatible (Windows/macOS/Linux) + * @param {string} filePath - Path to HTML file + * @returns {Promise} + */ +export async function launchBrowser(filePath) { + const absolutePath = resolve(filePath); + + // Construct file:// URL based on platform + let url; + if (platform() === 'win32') { + // Windows: file:///C:/path/to/file.html + url = `file:///${absolutePath.replace(/\\/g, '/')}`; + } else { + // Unix: file:///path/to/file.html + url = `file://${absolutePath}`; + } + + try { + // Use the 'open' package which handles cross-platform browser launching + await open(url); + } catch (error) { + // Fallback: try opening the file path directly + try { + await open(absolutePath); + } catch (fallbackError) { + throw new Error(`Failed to open browser: ${error.message}`); + } + } +} + +/** + * Check if we're running in a headless/CI environment + * @returns {boolean} + */ +export function isHeadlessEnvironment() { + return !!( + process.env.CI || + process.env.CONTINUOUS_INTEGRATION || + process.env.GITHUB_ACTIONS || + process.env.GITLAB_CI || + process.env.JENKINS_URL + ); +} diff --git a/ccw/src/utils/file-utils.js b/ccw/src/utils/file-utils.js new file mode 100644 index 00000000..c2132064 --- /dev/null +++ b/ccw/src/utils/file-utils.js @@ -0,0 +1,48 @@ +import { readFileSync, existsSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +/** + * Safely read a JSON file + * @param {string} filePath - Path to JSON file + * @returns {Object|null} - Parsed JSON or null on error + */ +export function readJsonFile(filePath) { + if (!existsSync(filePath)) return null; + try { + return JSON.parse(readFileSync(filePath, 'utf8')); + } catch { + return null; + } +} + +/** + * Safely read a text file + * @param {string} filePath - Path to text file + * @returns {string|null} - File contents or null on error + */ +export function readTextFile(filePath) { + if (!existsSync(filePath)) return null; + try { + return readFileSync(filePath, 'utf8'); + } catch { + return null; + } +} + +/** + * Write content to a file + * @param {string} filePath - Path to file + * @param {string} content - Content to write + */ +export function writeTextFile(filePath, content) { + writeFileSync(filePath, content, 'utf8'); +} + +/** + * Check if a path exists + * @param {string} filePath - Path to check + * @returns {boolean} + */ +export function pathExists(filePath) { + return existsSync(filePath); +} diff --git a/ccw/src/utils/path-resolver.js b/ccw/src/utils/path-resolver.js new file mode 100644 index 00000000..d48e2e40 --- /dev/null +++ b/ccw/src/utils/path-resolver.js @@ -0,0 +1,195 @@ +import { resolve, join, relative, isAbsolute } from 'path'; +import { existsSync, mkdirSync, realpathSync, statSync } from 'fs'; +import { homedir } from 'os'; + +/** + * Resolve a path, handling ~ for home directory + * @param {string} inputPath - Path to resolve + * @returns {string} - Absolute path + */ +export function resolvePath(inputPath) { + if (!inputPath) return process.cwd(); + + // Handle ~ for home directory + if (inputPath.startsWith('~')) { + return join(homedir(), inputPath.slice(1)); + } + + return resolve(inputPath); +} + +/** + * Validate and sanitize a user-provided path + * Prevents path traversal attacks and validates path is within allowed boundaries + * @param {string} inputPath - User-provided path + * @param {Object} options - Validation options + * @param {string} options.baseDir - Base directory to restrict paths within (optional) + * @param {boolean} options.mustExist - Whether path must exist (default: false) + * @param {boolean} options.allowHome - Whether to allow home directory paths (default: true) + * @returns {Object} - { valid: boolean, path: string|null, error: string|null } + */ +export function validatePath(inputPath, options = {}) { + const { baseDir = null, mustExist = false, allowHome = true } = options; + + // Check for empty/null input + if (!inputPath || typeof inputPath !== 'string') { + return { valid: false, path: null, error: 'Path is required' }; + } + + // Trim whitespace + const trimmedPath = inputPath.trim(); + + // Check for suspicious patterns (null bytes, control characters) + if (/[\x00-\x1f]/.test(trimmedPath)) { + return { valid: false, path: null, error: 'Path contains invalid characters' }; + } + + // Resolve the path + let resolvedPath; + try { + resolvedPath = resolvePath(trimmedPath); + } catch (err) { + return { valid: false, path: null, error: `Invalid path: ${err.message}` }; + } + + // Check if path exists when required + if (mustExist && !existsSync(resolvedPath)) { + return { valid: false, path: null, error: `Path does not exist: ${resolvedPath}` }; + } + + // Get real path if it exists (resolves symlinks) + let realPath = resolvedPath; + if (existsSync(resolvedPath)) { + try { + realPath = realpathSync(resolvedPath); + } catch (err) { + return { valid: false, path: null, error: `Cannot resolve path: ${err.message}` }; + } + } + + // Check if within base directory when specified + if (baseDir) { + const resolvedBase = resolvePath(baseDir); + const relativePath = relative(resolvedBase, realPath); + + // Path traversal detection: relative path should not start with '..' + if (relativePath.startsWith('..') || isAbsolute(relativePath)) { + return { + valid: false, + path: null, + error: `Path must be within ${resolvedBase}` + }; + } + } + + // Check home directory restriction + if (!allowHome) { + const home = homedir(); + if (realPath === home || realPath.startsWith(home + '/') || realPath.startsWith(home + '\\')) { + // This is fine, we're just checking if it's explicitly the home dir itself + } + } + + return { valid: true, path: realPath, error: null }; +} + +/** + * Validate output file path for writing + * @param {string} outputPath - Output file path + * @param {string} defaultDir - Default directory if path is relative + * @returns {Object} - { valid: boolean, path: string|null, error: string|null } + */ +export function validateOutputPath(outputPath, defaultDir = process.cwd()) { + if (!outputPath || typeof outputPath !== 'string') { + return { valid: false, path: null, error: 'Output path is required' }; + } + + const trimmedPath = outputPath.trim(); + + // Check for suspicious patterns + if (/[\x00-\x1f]/.test(trimmedPath)) { + return { valid: false, path: null, error: 'Output path contains invalid characters' }; + } + + // Resolve the path + let resolvedPath; + try { + resolvedPath = isAbsolute(trimmedPath) ? trimmedPath : join(defaultDir, trimmedPath); + resolvedPath = resolve(resolvedPath); + } catch (err) { + return { valid: false, path: null, error: `Invalid output path: ${err.message}` }; + } + + // Ensure it's not a directory + if (existsSync(resolvedPath)) { + try { + const stat = statSync(resolvedPath); + if (stat.isDirectory()) { + return { valid: false, path: null, error: 'Output path is a directory, expected a file' }; + } + } catch { + // Ignore stat errors + } + } + + return { valid: true, path: resolvedPath, error: null }; +} + +/** + * Get potential template locations + * @returns {string[]} - Array of existing template directories + */ +export function getTemplateLocations() { + const locations = [ + join(homedir(), '.claude', 'templates'), + join(process.cwd(), '.claude', 'templates') + ]; + + return locations.filter(loc => existsSync(loc)); +} + +/** + * Find a template file in known locations + * @param {string} templateName - Name of template file (e.g., 'workflow-dashboard.html') + * @returns {string|null} - Path to template or null if not found + */ +export function findTemplate(templateName) { + const locations = getTemplateLocations(); + + for (const loc of locations) { + const templatePath = join(loc, templateName); + if (existsSync(templatePath)) { + return templatePath; + } + } + + return null; +} + +/** + * Ensure directory exists, creating if necessary + * @param {string} dirPath - Directory path to ensure + */ +export function ensureDir(dirPath) { + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }); + } +} + +/** + * Get the .workflow directory path from project path + * @param {string} projectPath - Path to project + * @returns {string} - Path to .workflow directory + */ +export function getWorkflowDir(projectPath) { + return join(resolvePath(projectPath), '.workflow'); +} + +/** + * Normalize path for display (handle Windows backslashes) + * @param {string} filePath - Path to normalize + * @returns {string} + */ +export function normalizePathForDisplay(filePath) { + return filePath.replace(/\\/g, '/'); +} diff --git a/ccw/src/utils/ui.js b/ccw/src/utils/ui.js new file mode 100644 index 00000000..b226baa3 --- /dev/null +++ b/ccw/src/utils/ui.js @@ -0,0 +1,148 @@ +import chalk from 'chalk'; +import figlet from 'figlet'; +import boxen from 'boxen'; +import gradient from 'gradient-string'; +import ora from 'ora'; + +// Custom gradient colors +const claudeGradient = gradient(['#00d4ff', '#00ff88']); +const codeGradient = gradient(['#00ff88', '#ffff00']); +const workflowGradient = gradient(['#ffff00', '#ff8800']); + +/** + * Display ASCII art banner + */ +export function showBanner() { + console.log(''); + + // CLAUDE in cyan gradient + try { + const claudeText = figlet.textSync('Claude', { font: 'Standard' }); + console.log(claudeGradient(claudeText)); + } catch { + console.log(chalk.cyan.bold(' Claude')); + } + + // CODE in green gradient + try { + const codeText = figlet.textSync('Code', { font: 'Standard' }); + console.log(codeGradient(codeText)); + } catch { + console.log(chalk.green.bold(' Code')); + } + + // WORKFLOW in yellow gradient + try { + const workflowText = figlet.textSync('Workflow', { font: 'Standard' }); + console.log(workflowGradient(workflowText)); + } catch { + console.log(chalk.yellow.bold(' Workflow')); + } + + console.log(''); +} + +/** + * Display header with version info + * @param {string} version - Version number + * @param {string} mode - Installation mode + */ +export function showHeader(version, mode = '') { + showBanner(); + + const versionText = version ? `v${version}` : ''; + const modeText = mode ? ` (${mode})` : ''; + + console.log(boxen( + chalk.cyan.bold('Claude Code Workflow System') + '\n' + + chalk.gray(`Installer ${versionText}${modeText}`) + '\n\n' + + chalk.white('Unified workflow system with comprehensive coordination'), + { + padding: 1, + margin: { top: 0, bottom: 1, left: 2, right: 2 }, + borderStyle: 'round', + borderColor: 'cyan' + } + )); +} + +/** + * Create a spinner + * @param {string} text - Spinner text + * @returns {ora.Ora} + */ +export function createSpinner(text) { + return ora({ + text, + color: 'cyan', + spinner: 'dots' + }); +} + +/** + * Display success message + * @param {string} message + */ +export function success(message) { + console.log(chalk.green('✓') + ' ' + chalk.green(message)); +} + +/** + * Display info message + * @param {string} message + */ +export function info(message) { + console.log(chalk.cyan('ℹ') + ' ' + chalk.cyan(message)); +} + +/** + * Display warning message + * @param {string} message + */ +export function warning(message) { + console.log(chalk.yellow('⚠') + ' ' + chalk.yellow(message)); +} + +/** + * Display error message + * @param {string} message + */ +export function error(message) { + console.log(chalk.red('✖') + ' ' + chalk.red(message)); +} + +/** + * Display step message + * @param {number} step - Step number + * @param {number} total - Total steps + * @param {string} message - Step message + */ +export function step(stepNum, total, message) { + console.log(chalk.gray(`[${stepNum}/${total}]`) + ' ' + chalk.white(message)); +} + +/** + * Display summary box + * @param {Object} options + * @param {string} options.title - Box title + * @param {string[]} options.lines - Content lines + * @param {string} options.borderColor - Border color + */ +export function summaryBox({ title, lines, borderColor = 'green' }) { + const content = lines.join('\n'); + console.log(boxen(content, { + title, + titleAlignment: 'center', + padding: 1, + margin: { top: 1, bottom: 1, left: 2, right: 2 }, + borderStyle: 'round', + borderColor + })); +} + +/** + * Display a divider line + */ +export function divider() { + console.log(chalk.gray('─'.repeat(60))); +}