prd-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/prd.js +118 -0
- package/lib/commands/add-project.js +163 -0
- package/lib/commands/dashboard.js +173 -0
- package/lib/commands/init.js +221 -0
- package/lib/config.js +93 -0
- package/lib/parser.js +154 -0
- package/package.json +41 -0
- package/templates/PROCESS.md +707 -0
- package/templates/README.md.tmpl +76 -0
- package/templates/RULES.md.tmpl +53 -0
- package/templates/backlog.md.tmpl +6 -0
- package/test/sarah-test.js +237 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nimrod Margalit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Product OS Framework
|
|
2
|
+
|
|
3
|
+
An AI-native product development lifecycle for [Cursor](https://cursor.com). From idea to production with quality gates, slash commands, and real-time visibility.
|
|
4
|
+
|
|
5
|
+
## What It Does
|
|
6
|
+
|
|
7
|
+
Product OS gives your team a structured workflow for building features:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
PM creates & verifies PRD → Dev builds & tests → Team releases & documents
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
**Four slash commands in Cursor:**
|
|
14
|
+
|
|
15
|
+
| Command | Owner | What Happens |
|
|
16
|
+
|---------|-------|--------------|
|
|
17
|
+
| `/create` | PM | Define → Verify → Extract Features → PRD approved |
|
|
18
|
+
| `/verify` | PM | Quality check any PRD |
|
|
19
|
+
| `/dev` | Developer | Kickoff → Code → Test → PR ready |
|
|
20
|
+
| `/release` | Team | Review → Merge → Deploy → Document |
|
|
21
|
+
|
|
22
|
+
**Plus a CLI dashboard:**
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
$ prd
|
|
26
|
+
|
|
27
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
|
+
📊 PRODUCT STORIES DASHBOARD
|
|
29
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Total Stories: 5
|
|
33
|
+
Average Progress: 30%
|
|
34
|
+
Blocked: 0
|
|
35
|
+
|
|
36
|
+
delivery-api (4 stories)
|
|
37
|
+
US-001 ✅ Done P0
|
|
38
|
+
Real-time order tracking
|
|
39
|
+
████████████ 100%
|
|
40
|
+
|
|
41
|
+
US-002 🔨 In Dev P1
|
|
42
|
+
Restaurant menu management
|
|
43
|
+
██████░░░░░░ 50%
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
### 1. Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g prd-cli
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Create Your Workspace
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
prd init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Answer the prompts:
|
|
61
|
+
- Workspace name (e.g., `product-docs`)
|
|
62
|
+
- First project name (e.g., `my-app`)
|
|
63
|
+
- Path to dev repo (optional)
|
|
64
|
+
|
|
65
|
+
This creates your product docs folder, templates, and a `.code-workspace` file.
|
|
66
|
+
|
|
67
|
+
### 3. Open in Cursor
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
cursor your-workspace.code-workspace
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Both your product docs and dev repo are connected automatically.
|
|
74
|
+
|
|
75
|
+
### 4. Create Your First Story
|
|
76
|
+
|
|
77
|
+
In Cursor chat:
|
|
78
|
+
```
|
|
79
|
+
/create "User authentication with OAuth" @my-app/backlog.md
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The AI agent walks you through defining requirements, verifying quality, and extracting features.
|
|
83
|
+
|
|
84
|
+
### 5. View Dashboard
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
prd
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## How It Works
|
|
91
|
+
|
|
92
|
+
### For Product Managers
|
|
93
|
+
|
|
94
|
+
1. **`/create`** - Interactive PRD creation with AI-powered refinement
|
|
95
|
+
2. **`/verify`** - Automated quality checks for your PRD
|
|
96
|
+
3. **`prd dashboard`** - See all stories across all projects
|
|
97
|
+
|
|
98
|
+
### For Developers
|
|
99
|
+
|
|
100
|
+
1. **`/dev US-001`** - AI reads the PRD + RULES.md, generates tasks, builds iteratively
|
|
101
|
+
2. **`/release US-001`** - Merge, deploy, document
|
|
102
|
+
|
|
103
|
+
### Workspace Structure
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
your-product-docs/
|
|
107
|
+
├── .prd.config.json # Workspace config
|
|
108
|
+
├── PROCESS.md # The methodology
|
|
109
|
+
├── README.md # Project registry
|
|
110
|
+
└── my-app/ # Project folder
|
|
111
|
+
├── backlog.md # Story tracking
|
|
112
|
+
├── RULES.md # Technical standards
|
|
113
|
+
├── US-001.md # Story files
|
|
114
|
+
└── US-002.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each project folder contains stories (`US-*.md`) with YAML frontmatter that powers the dashboard and slash commands.
|
|
118
|
+
|
|
119
|
+
## CLI Commands
|
|
120
|
+
|
|
121
|
+
| Command | Description |
|
|
122
|
+
|---------|-------------|
|
|
123
|
+
| `prd init` | Create a new product workspace |
|
|
124
|
+
| `prd add-project` | Add another project |
|
|
125
|
+
| `prd` or `prd dashboard` | Full stories dashboard |
|
|
126
|
+
| `prd list` | Compact list view |
|
|
127
|
+
| `prd stats` | Quick statistics |
|
|
128
|
+
| `prd dashboard --status done` | Filter by status |
|
|
129
|
+
| `prd dashboard --phase dev` | Filter by phase |
|
|
130
|
+
|
|
131
|
+
## Key Concepts
|
|
132
|
+
|
|
133
|
+
### Quality Gates
|
|
134
|
+
Every PRD goes through automated verification before development starts. Gaps are identified and suggested fixes offered.
|
|
135
|
+
|
|
136
|
+
### RULES.md
|
|
137
|
+
Each project has a `RULES.md` defining technical standards (tech stack, patterns, testing). AI agents read this during `/dev` to ensure consistent implementation.
|
|
138
|
+
|
|
139
|
+
### YAML Frontmatter
|
|
140
|
+
Stories include metadata that powers the dashboard:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
---
|
|
144
|
+
id: US-001
|
|
145
|
+
project: my-app
|
|
146
|
+
status: dev
|
|
147
|
+
phase: development
|
|
148
|
+
progress: 50
|
|
149
|
+
priority: P0
|
|
150
|
+
---
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Global Story IDs
|
|
154
|
+
Stories use globally unique IDs (`US-001`, `US-002`, ...) across all projects. No conflicts, easy cross-referencing.
|
|
155
|
+
|
|
156
|
+
## Built For
|
|
157
|
+
|
|
158
|
+
- **Cursor IDE** - Slash commands powered by AI agents
|
|
159
|
+
- **Small teams** - PMs + Developers working together
|
|
160
|
+
- **Multiple projects** - One workspace, many products
|
|
161
|
+
- **AI-native development** - Quality gates designed for LLM-assisted workflows
|
|
162
|
+
|
|
163
|
+
## Industry Alignment
|
|
164
|
+
|
|
165
|
+
| Product OS | Maps To |
|
|
166
|
+
|------------|---------|
|
|
167
|
+
| `/create` + `/verify` | **Dual-Track Agile** Discovery / **Shape Up** Shaping |
|
|
168
|
+
| `/dev` | **Dual-Track Agile** Delivery / **Shape Up** Building |
|
|
169
|
+
| `/release` | **Lean** Measure / **Shape Up** Shipping |
|
|
170
|
+
|
|
171
|
+
## Contributing
|
|
172
|
+
|
|
173
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
[MIT](LICENSE)
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
**Product OS Framework** - Built by [Nimrod Margalit](https://github.com/nimidev)
|
package/bin/prd.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Product OS Framework CLI
|
|
5
|
+
* AI-native product development lifecycle for Cursor
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { Command } = require('commander');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { findWorkspaceRoot, loadConfig } = require('../lib/config');
|
|
11
|
+
const { dashboard } = require('../lib/commands/dashboard');
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('prd')
|
|
17
|
+
.description('Product OS Framework - AI-native product development lifecycle for Cursor')
|
|
18
|
+
.version('1.0.0');
|
|
19
|
+
|
|
20
|
+
// --- prd init ---
|
|
21
|
+
program
|
|
22
|
+
.command('init')
|
|
23
|
+
.description('Create a new product workspace')
|
|
24
|
+
.action(async () => {
|
|
25
|
+
const { init } = require('../lib/commands/init');
|
|
26
|
+
await init();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// --- prd add-project ---
|
|
30
|
+
program
|
|
31
|
+
.command('add-project')
|
|
32
|
+
.description('Add a new project to the workspace')
|
|
33
|
+
.action(async () => {
|
|
34
|
+
const { addProject } = require('../lib/commands/add-project');
|
|
35
|
+
await addProject();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// --- prd dashboard ---
|
|
39
|
+
program
|
|
40
|
+
.command('dashboard')
|
|
41
|
+
.description('Display product stories dashboard')
|
|
42
|
+
.option('-p, --path <path>', 'Path to product-docs workspace')
|
|
43
|
+
.option('-s, --status <status>', 'Filter by status (backlog, dev, done, etc.)')
|
|
44
|
+
.option('--phase <phase>', 'Filter by phase (create, dev, release)')
|
|
45
|
+
.option('-c, --compact', 'Compact output')
|
|
46
|
+
.action((options) => {
|
|
47
|
+
const docsPath = resolveDocsPath(options.path);
|
|
48
|
+
dashboard(docsPath, options);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// --- prd list ---
|
|
52
|
+
program
|
|
53
|
+
.command('list')
|
|
54
|
+
.description('List all stories (compact view)')
|
|
55
|
+
.option('-p, --path <path>', 'Path to product-docs workspace')
|
|
56
|
+
.action((options) => {
|
|
57
|
+
const docsPath = resolveDocsPath(options.path);
|
|
58
|
+
dashboard(docsPath, { ...options, compact: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// --- prd stats ---
|
|
62
|
+
program
|
|
63
|
+
.command('stats')
|
|
64
|
+
.description('Show statistics summary')
|
|
65
|
+
.option('-p, --path <path>', 'Path to product-docs workspace')
|
|
66
|
+
.action((options) => {
|
|
67
|
+
const chalk = require('chalk');
|
|
68
|
+
const { scanAllStories, getStats } = require('../lib/parser');
|
|
69
|
+
|
|
70
|
+
const docsPath = resolveDocsPath(options.path);
|
|
71
|
+
try {
|
|
72
|
+
const data = scanAllStories(docsPath);
|
|
73
|
+
const stats = getStats(data.stories);
|
|
74
|
+
|
|
75
|
+
console.log('\n' + chalk.bold.cyan('📊 Statistics'));
|
|
76
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
77
|
+
console.log(`Total: ${chalk.green.bold(stats.total)}`);
|
|
78
|
+
console.log(`Progress: ${chalk.yellow.bold(stats.avgProgress + '%')}`);
|
|
79
|
+
console.log(`Blocked: ${stats.blocked > 0 ? chalk.red.bold(stats.blocked) : chalk.gray('0')}`);
|
|
80
|
+
console.log('');
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error(chalk.red('Error:'), err.message);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// --- Default: show dashboard if no command given ---
|
|
88
|
+
if (process.argv.length === 2) {
|
|
89
|
+
const docsPath = resolveDocsPath();
|
|
90
|
+
if (docsPath) {
|
|
91
|
+
dashboard(docsPath);
|
|
92
|
+
} else {
|
|
93
|
+
const chalk = require('chalk');
|
|
94
|
+
console.log('\n' + chalk.bold.cyan('Product OS Framework'));
|
|
95
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
96
|
+
console.log(chalk.white('No product workspace found.\n'));
|
|
97
|
+
console.log(chalk.white('Get started:'));
|
|
98
|
+
console.log(chalk.cyan(' prd init') + chalk.gray(' Create a new workspace'));
|
|
99
|
+
console.log(chalk.cyan(' prd dashboard') + chalk.gray(' View stories dashboard'));
|
|
100
|
+
console.log(chalk.cyan(' prd --help') + chalk.gray(' See all commands\n'));
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
program.parse();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the product-docs workspace path
|
|
108
|
+
* Priority: explicit --path > auto-detect via config > cwd
|
|
109
|
+
*/
|
|
110
|
+
function resolveDocsPath(explicitPath) {
|
|
111
|
+
if (explicitPath) return path.resolve(explicitPath);
|
|
112
|
+
|
|
113
|
+
const root = findWorkspaceRoot(process.cwd());
|
|
114
|
+
if (root) return root;
|
|
115
|
+
|
|
116
|
+
// Fallback: current directory
|
|
117
|
+
return process.cwd();
|
|
118
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prd add-project - Add a new project to the workspace
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const { loadConfig, saveConfig, CONFIG_FILENAME } = require('../config');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Update .code-workspace file to include new project's dev repo
|
|
13
|
+
*/
|
|
14
|
+
function updateWorkspaceFile(workspaceRoot, projects) {
|
|
15
|
+
// Find .code-workspace files in parent directory
|
|
16
|
+
const parentDir = path.dirname(workspaceRoot);
|
|
17
|
+
const wsFiles = fs.readdirSync(parentDir).filter(f => f.endsWith('.code-workspace'));
|
|
18
|
+
|
|
19
|
+
for (const wsFile of wsFiles) {
|
|
20
|
+
const wsPath = path.join(parentDir, wsFile);
|
|
21
|
+
try {
|
|
22
|
+
const ws = JSON.parse(fs.readFileSync(wsPath, 'utf-8'));
|
|
23
|
+
|
|
24
|
+
// Add any new project repos that aren't already in the workspace
|
|
25
|
+
for (const [name, proj] of Object.entries(projects)) {
|
|
26
|
+
if (!proj.repoPath) continue;
|
|
27
|
+
const relPath = path.relative(parentDir, proj.repoPath);
|
|
28
|
+
const alreadyIncluded = ws.folders.some(f => f.path === relPath || f.name === name);
|
|
29
|
+
if (!alreadyIncluded) {
|
|
30
|
+
ws.folders.push({ name, path: relPath });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fs.writeFileSync(wsPath, JSON.stringify(ws, null, 2) + '\n', 'utf-8');
|
|
35
|
+
return wsPath;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
// Skip malformed workspace files
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Main add-project command
|
|
45
|
+
*/
|
|
46
|
+
async function addProject(options = {}) {
|
|
47
|
+
const config = loadConfig();
|
|
48
|
+
if (!config) {
|
|
49
|
+
console.log(chalk.red('\nNo Product OS workspace found.'));
|
|
50
|
+
console.log(chalk.gray(`Run ${chalk.cyan('prd init')} to create one.\n`));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const workspaceRoot = config._root;
|
|
55
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates');
|
|
56
|
+
|
|
57
|
+
console.log(chalk.gray('\nAdding a new project to your workspace.\n'));
|
|
58
|
+
|
|
59
|
+
const answers = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'input',
|
|
62
|
+
name: 'projectName',
|
|
63
|
+
message: 'Project name:',
|
|
64
|
+
validate: (v) => {
|
|
65
|
+
if (!v.trim()) return 'Project name is required';
|
|
66
|
+
if (/[^a-zA-Z0-9_-]/.test(v.trim())) return 'Use only letters, numbers, hyphens, underscores';
|
|
67
|
+
if (config.projects[v.trim()]) return 'Project already exists';
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
type: 'input',
|
|
73
|
+
name: 'description',
|
|
74
|
+
message: 'Project description (optional):',
|
|
75
|
+
default: ''
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'confirm',
|
|
79
|
+
name: 'hasRepo',
|
|
80
|
+
message: 'Do you have an existing dev repo for this project?',
|
|
81
|
+
default: false
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
type: 'input',
|
|
85
|
+
name: 'repoPath',
|
|
86
|
+
message: 'Path to the dev repo:',
|
|
87
|
+
when: (a) => a.hasRepo,
|
|
88
|
+
validate: (v) => {
|
|
89
|
+
const resolved = path.resolve(v);
|
|
90
|
+
if (!fs.existsSync(resolved)) return `Path not found: ${resolved}`;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const projName = answers.projectName.trim();
|
|
97
|
+
const projDesc = answers.description.trim();
|
|
98
|
+
const repoPath = answers.repoPath ? path.resolve(answers.repoPath) : null;
|
|
99
|
+
|
|
100
|
+
// Create project directory
|
|
101
|
+
const projDir = path.join(workspaceRoot, projName);
|
|
102
|
+
fs.mkdirSync(projDir, { recursive: true });
|
|
103
|
+
|
|
104
|
+
// Create backlog
|
|
105
|
+
const backlogTmpl = path.join(templatesDir, 'backlog.md.tmpl');
|
|
106
|
+
if (fs.existsSync(backlogTmpl)) {
|
|
107
|
+
let content = fs.readFileSync(backlogTmpl, 'utf-8');
|
|
108
|
+
content = content.replace(/\{\{projectName\}\}/g, projName);
|
|
109
|
+
fs.writeFileSync(path.join(projDir, 'backlog.md'), content, 'utf-8');
|
|
110
|
+
} else {
|
|
111
|
+
fs.writeFileSync(path.join(projDir, 'backlog.md'),
|
|
112
|
+
`# ${projName} - Backlog\n\n| ID | Title | Status | Priority |\n|----|-------|--------|----------|\n`, 'utf-8');
|
|
113
|
+
}
|
|
114
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${projName}/backlog.md`));
|
|
115
|
+
|
|
116
|
+
// Create RULES.md
|
|
117
|
+
const rulesTmpl = path.join(templatesDir, 'RULES.md.tmpl');
|
|
118
|
+
if (fs.existsSync(rulesTmpl)) {
|
|
119
|
+
let content = fs.readFileSync(rulesTmpl, 'utf-8');
|
|
120
|
+
content = content.replace(/\{\{projectName\}\}/g, projName);
|
|
121
|
+
fs.writeFileSync(path.join(projDir, 'RULES.md'), content, 'utf-8');
|
|
122
|
+
} else {
|
|
123
|
+
fs.writeFileSync(path.join(projDir, 'RULES.md'),
|
|
124
|
+
`# ${projName} - Technical Standards\n\nDocument your project's tech stack, patterns, and conventions here.\n`, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${projName}/RULES.md`));
|
|
127
|
+
|
|
128
|
+
// Update config
|
|
129
|
+
config.projects[projName] = {
|
|
130
|
+
description: projDesc,
|
|
131
|
+
repoPath
|
|
132
|
+
};
|
|
133
|
+
saveConfig(config);
|
|
134
|
+
console.log(chalk.green(' ✅ Updated ') + chalk.white(CONFIG_FILENAME));
|
|
135
|
+
|
|
136
|
+
// Update README project table
|
|
137
|
+
const readmePath = path.join(workspaceRoot, 'README.md');
|
|
138
|
+
if (fs.existsSync(readmePath)) {
|
|
139
|
+
let readme = fs.readFileSync(readmePath, 'utf-8');
|
|
140
|
+
const tableRow = `| [${projName}](./${projName}/) | ${projDesc || ''} |`;
|
|
141
|
+
// Insert before the empty line after the last table row
|
|
142
|
+
readme = readme.replace(
|
|
143
|
+
/(## Projects\n\n\|[^\n]+\n\|[^\n]+\n)([\s\S]*?)(\n\n)/,
|
|
144
|
+
(match, header, rows, gap) => `${header}${rows}\n${tableRow}${gap}`
|
|
145
|
+
);
|
|
146
|
+
fs.writeFileSync(readmePath, readme, 'utf-8');
|
|
147
|
+
console.log(chalk.green(' ✅ Updated ') + chalk.white('README.md'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Update workspace file
|
|
151
|
+
if (repoPath) {
|
|
152
|
+
const wsFile = updateWorkspaceFile(workspaceRoot, config.projects);
|
|
153
|
+
if (wsFile) {
|
|
154
|
+
console.log(chalk.green(' ✅ Updated ') + chalk.white(path.basename(wsFile)));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
console.log('\n' + chalk.bold.green(`🎉 Project "${projName}" added!\n`));
|
|
159
|
+
console.log(chalk.white('Create stories in Cursor:'));
|
|
160
|
+
console.log(chalk.cyan(` /create "feature" @${projName}/backlog.md\n`));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = { addProject };
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard command - Rich terminal output
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { scanAllStories, getStats } = require('../parser');
|
|
7
|
+
|
|
8
|
+
const STATUS_CONFIG = {
|
|
9
|
+
backlog: { icon: '📋', color: 'gray', label: 'Backlog' },
|
|
10
|
+
create: { icon: '✏️', color: 'blue', label: 'Creating' },
|
|
11
|
+
approved: { icon: '✅', color: 'green', label: 'Approved' },
|
|
12
|
+
dev: { icon: '🔨', color: 'yellow', label: 'In Dev' },
|
|
13
|
+
in_progress: { icon: '🔄', color: 'yellow', label: 'In Progress' },
|
|
14
|
+
testing: { icon: '🧪', color: 'cyan', label: 'Testing' },
|
|
15
|
+
review: { icon: '👀', color: 'magenta', label: 'Review' },
|
|
16
|
+
release: { icon: '🚀', color: 'green', label: 'Release' },
|
|
17
|
+
done: { icon: '✅', color: 'green', label: 'Done' },
|
|
18
|
+
blocked: { icon: '🚫', color: 'red', label: 'Blocked' }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PRIORITY_COLORS = {
|
|
22
|
+
P0: 'red',
|
|
23
|
+
P1: 'yellow',
|
|
24
|
+
P2: 'blue',
|
|
25
|
+
P3: 'gray'
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function formatProgressBar(progress) {
|
|
29
|
+
const [completed, total] = progress.split('/').map(Number);
|
|
30
|
+
if (total === 0) return chalk.gray('N/A');
|
|
31
|
+
|
|
32
|
+
const percentage = Math.round((completed / total) * 100);
|
|
33
|
+
const barLength = 12;
|
|
34
|
+
const filledLength = Math.round((completed / total) * barLength);
|
|
35
|
+
const emptyLength = barLength - filledLength;
|
|
36
|
+
|
|
37
|
+
const filled = '█'.repeat(filledLength);
|
|
38
|
+
const empty = '░'.repeat(emptyLength);
|
|
39
|
+
|
|
40
|
+
let color = 'red';
|
|
41
|
+
if (percentage >= 80) color = 'green';
|
|
42
|
+
else if (percentage >= 50) color = 'yellow';
|
|
43
|
+
|
|
44
|
+
return chalk[color](filled) + chalk.gray(empty) + chalk.white(` ${percentage}%`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatStatus(status) {
|
|
48
|
+
const config = STATUS_CONFIG[status] || { icon: '❓', color: 'gray', label: status };
|
|
49
|
+
return config.icon + ' ' + chalk[config.color](config.label);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function formatPriority(priority) {
|
|
53
|
+
const color = PRIORITY_COLORS[priority] || 'gray';
|
|
54
|
+
return chalk[color].bold(priority);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function displaySummary(stats) {
|
|
58
|
+
console.log('\n' + chalk.bold.cyan('━'.repeat(60)));
|
|
59
|
+
console.log(chalk.bold.cyan(' 📊 PRODUCT STORIES DASHBOARD'));
|
|
60
|
+
console.log(chalk.bold.cyan('━'.repeat(60)) + '\n');
|
|
61
|
+
|
|
62
|
+
console.log(chalk.white.bold('Summary:'));
|
|
63
|
+
console.log(` Total Stories: ${chalk.green.bold(stats.total)}`);
|
|
64
|
+
console.log(` Average Progress: ${chalk.yellow.bold(stats.avgProgress + '%')}`);
|
|
65
|
+
console.log(` Blocked: ${stats.blocked > 0 ? chalk.red.bold(stats.blocked) : chalk.gray('0')}`);
|
|
66
|
+
|
|
67
|
+
console.log('\n' + chalk.white.bold('By Status:'));
|
|
68
|
+
Object.entries(stats.byStatus).forEach(([status, count]) => {
|
|
69
|
+
const config = STATUS_CONFIG[status] || { icon: '❓', color: 'gray' };
|
|
70
|
+
console.log(` ${config.icon} ${chalk[config.color](status.padEnd(12))} ${count}`);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
console.log('\n' + chalk.white.bold('By Priority:'));
|
|
74
|
+
Object.entries(stats.byPriority).forEach(([priority, count]) => {
|
|
75
|
+
console.log(` ${formatPriority(priority)} ${count}`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
console.log('\n' + chalk.gray('─'.repeat(60)) + '\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function displayProjects(projects, options = {}) {
|
|
82
|
+
for (const project of projects) {
|
|
83
|
+
console.log(chalk.bold.white(`\n${project.name}`) + chalk.gray(` (${project.storyCount} stories)`));
|
|
84
|
+
console.log(chalk.gray('─'.repeat(60)));
|
|
85
|
+
|
|
86
|
+
const stories = project.stories.sort((a, b) => a.id.localeCompare(b.id));
|
|
87
|
+
|
|
88
|
+
let filtered = stories;
|
|
89
|
+
if (options.status) {
|
|
90
|
+
filtered = filtered.filter(s => s.status === options.status);
|
|
91
|
+
}
|
|
92
|
+
if (options.phase) {
|
|
93
|
+
filtered = filtered.filter(s => s.phase === options.phase);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (filtered.length === 0) {
|
|
97
|
+
console.log(chalk.gray(' No stories match filters\n'));
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const story of filtered) {
|
|
102
|
+
const statusDisplay = formatStatus(story.status);
|
|
103
|
+
const priorityDisplay = formatPriority(story.priority);
|
|
104
|
+
const progressBar = formatProgressBar(story.progress);
|
|
105
|
+
const blockerIndicator = story.blockers.length > 0 ? chalk.red.bold(' 🚫') : '';
|
|
106
|
+
|
|
107
|
+
console.log(` ${chalk.cyan.bold(story.id)} ${statusDisplay} ${priorityDisplay}${blockerIndicator}`);
|
|
108
|
+
console.log(` ${chalk.white(story.title)}`);
|
|
109
|
+
console.log(` ${progressBar} ${chalk.gray(story.progress + ' AC')}`);
|
|
110
|
+
|
|
111
|
+
if (story.blockers.length > 0) {
|
|
112
|
+
console.log(` ${chalk.red('Blocked by:')} ${story.blockers.join(', ')}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function displayCompact(projects) {
|
|
121
|
+
for (const project of projects) {
|
|
122
|
+
console.log(chalk.bold.white(`\n${project.name}`));
|
|
123
|
+
|
|
124
|
+
const stories = project.stories.sort((a, b) => a.id.localeCompare(b.id));
|
|
125
|
+
|
|
126
|
+
for (const story of stories) {
|
|
127
|
+
const config = STATUS_CONFIG[story.status] || { icon: '❓' };
|
|
128
|
+
const [completed, total] = story.progress.split('/').map(Number);
|
|
129
|
+
const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
|
|
130
|
+
const blockerIndicator = story.blockers.length > 0 ? chalk.red(' 🚫') : '';
|
|
131
|
+
|
|
132
|
+
console.log(` ${config.icon} ${chalk.cyan(story.id)} ${chalk.gray(story.title)} ${chalk.yellow(percentage + '%')}${blockerIndicator}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
console.log('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Main dashboard command
|
|
140
|
+
* @param {string} docsPath - Path to product-docs workspace
|
|
141
|
+
* @param {Object} options
|
|
142
|
+
*/
|
|
143
|
+
function dashboard(docsPath, options = {}) {
|
|
144
|
+
try {
|
|
145
|
+
const data = scanAllStories(docsPath);
|
|
146
|
+
const stats = getStats(data.stories);
|
|
147
|
+
|
|
148
|
+
if (data.stories.length === 0) {
|
|
149
|
+
console.log(chalk.yellow('\nNo stories found in ' + docsPath));
|
|
150
|
+
console.log(chalk.gray('Create your first story in Cursor: /create "feature" @project/backlog.md\n'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!options.compact) {
|
|
155
|
+
displaySummary(stats);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (options.compact) {
|
|
159
|
+
displayCompact(data.projects);
|
|
160
|
+
} else {
|
|
161
|
+
displayProjects(data.projects, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(chalk.gray(`Last updated: ${data.lastUpdated.toLocaleString()}`));
|
|
165
|
+
console.log('');
|
|
166
|
+
|
|
167
|
+
} catch (err) {
|
|
168
|
+
console.error(chalk.red('Error:'), err.message);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { dashboard };
|