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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# {{workspaceName}}
|
|
2
|
+
|
|
3
|
+
Product documentation workspace powered by [Product OS Framework](https://github.com/nimidev/product-OS-framework).
|
|
4
|
+
|
|
5
|
+
## Projects
|
|
6
|
+
|
|
7
|
+
| Project | Description |
|
|
8
|
+
|---------|-------------|
|
|
9
|
+
| [{{projectName}}](./{{projectName}}/) | {{projectDescription}} |
|
|
10
|
+
|
|
11
|
+
## Story ID Registry
|
|
12
|
+
|
|
13
|
+
**Next Available ID: {{nextId}}**
|
|
14
|
+
|
|
15
|
+
When creating a new story, use the next available ID. The `/create` command in Cursor handles this automatically.
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### For Product Managers
|
|
20
|
+
|
|
21
|
+
1. Open the workspace file in Cursor (`.code-workspace`)
|
|
22
|
+
2. Create a story: `/create "feature description" @{{projectName}}/backlog.md`
|
|
23
|
+
3. Refine requirements through conversation with the AI agent
|
|
24
|
+
4. Approve the PRD when ready
|
|
25
|
+
|
|
26
|
+
### For Developers
|
|
27
|
+
|
|
28
|
+
1. Open the workspace file in Cursor (`.code-workspace`)
|
|
29
|
+
2. Start development: `/dev US-{ID}`
|
|
30
|
+
3. The agent reads RULES.md and follows project standards
|
|
31
|
+
4. Ship it: `/release US-{ID}`
|
|
32
|
+
|
|
33
|
+
### Dashboard
|
|
34
|
+
|
|
35
|
+
View all stories from any terminal:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
prd # Full dashboard
|
|
39
|
+
prd list # Compact view
|
|
40
|
+
prd stats # Quick numbers
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Process
|
|
44
|
+
|
|
45
|
+
See [PROCESS.md](./PROCESS.md) for the complete product development workflow.
|
|
46
|
+
|
|
47
|
+
## Story File Format
|
|
48
|
+
|
|
49
|
+
Stories use YAML frontmatter for metadata:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
---
|
|
53
|
+
id: US-001
|
|
54
|
+
project: {{projectName}}
|
|
55
|
+
status: create
|
|
56
|
+
phase: planning
|
|
57
|
+
progress: 0
|
|
58
|
+
priority: P1
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
# Story Title
|
|
62
|
+
|
|
63
|
+
## Goal
|
|
64
|
+
What problem does this solve?
|
|
65
|
+
|
|
66
|
+
## Acceptance Criteria
|
|
67
|
+
1. ...
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Adding a New Project
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
prd add-project
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This creates the project folder, backlog, RULES.md, and updates the workspace.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# {{projectName}} - Technical Standards
|
|
2
|
+
|
|
3
|
+
This file defines the technical standards and patterns for the {{projectName}} project.
|
|
4
|
+
AI agents read this file during `/dev` to ensure consistent implementation.
|
|
5
|
+
|
|
6
|
+
## Technology Stack
|
|
7
|
+
|
|
8
|
+
<!-- List your tech stack with versions -->
|
|
9
|
+
- Language:
|
|
10
|
+
- Framework:
|
|
11
|
+
- Database:
|
|
12
|
+
- Testing:
|
|
13
|
+
|
|
14
|
+
## Code Organization
|
|
15
|
+
|
|
16
|
+
<!-- Describe your folder structure and naming conventions -->
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
src/
|
|
20
|
+
├── ...
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Patterns & Conventions
|
|
24
|
+
|
|
25
|
+
<!-- Document coding patterns the team follows -->
|
|
26
|
+
- Naming conventions:
|
|
27
|
+
- Error handling:
|
|
28
|
+
- Logging:
|
|
29
|
+
|
|
30
|
+
## Testing Requirements
|
|
31
|
+
|
|
32
|
+
<!-- Define test coverage expectations -->
|
|
33
|
+
- Unit tests: Required for all business logic
|
|
34
|
+
- Integration tests: Required for API endpoints
|
|
35
|
+
- E2E tests: Critical paths only
|
|
36
|
+
|
|
37
|
+
## Performance Standards
|
|
38
|
+
|
|
39
|
+
<!-- Set performance targets -->
|
|
40
|
+
- Response time:
|
|
41
|
+
- Bundle size:
|
|
42
|
+
|
|
43
|
+
## Security Practices
|
|
44
|
+
|
|
45
|
+
<!-- Document security requirements -->
|
|
46
|
+
- Authentication:
|
|
47
|
+
- Authorization:
|
|
48
|
+
- Data handling:
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
> **Tip:** Fill this out based on your project's actual tech stack.
|
|
53
|
+
> The more specific you are, the better the AI agent will implement features.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* End-to-end test: Simulates "Sarah" - a new PM setting up the framework.
|
|
4
|
+
* Run from a clean directory to test the full flow.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
|
|
11
|
+
const TEST_DIR = '/tmp/sarah-test';
|
|
12
|
+
const WORKSPACE_NAME = 'food-delivery-docs';
|
|
13
|
+
const PROJECT_NAME = 'delivery-api';
|
|
14
|
+
|
|
15
|
+
// Import framework modules directly
|
|
16
|
+
const { CONFIG_FILENAME, loadConfig, saveConfig } = require('../lib/config');
|
|
17
|
+
const { scanAllStories, getStats } = require('../lib/parser');
|
|
18
|
+
const { dashboard } = require('../lib/commands/dashboard');
|
|
19
|
+
|
|
20
|
+
function assert(condition, message) {
|
|
21
|
+
if (!condition) {
|
|
22
|
+
console.log(chalk.red(` ❌ FAIL: ${message}`));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
console.log(chalk.green(` ✅ ${message}`));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function header(text) {
|
|
29
|
+
console.log('\n' + chalk.bold.cyan(`─── ${text} ───`));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- Setup ---
|
|
33
|
+
console.log(chalk.bold.white('\n🧪 Sarah Test: End-to-End Framework Test\n'));
|
|
34
|
+
|
|
35
|
+
// Clean up
|
|
36
|
+
if (fs.existsSync(TEST_DIR)) {
|
|
37
|
+
fs.rmSync(TEST_DIR, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
fs.mkdirSync(TEST_DIR, { recursive: true });
|
|
40
|
+
|
|
41
|
+
// Create fake dev repo
|
|
42
|
+
const devRepoPath = path.join(TEST_DIR, 'delivery-api');
|
|
43
|
+
fs.mkdirSync(path.join(devRepoPath, 'src'), { recursive: true });
|
|
44
|
+
fs.writeFileSync(path.join(devRepoPath, 'package.json'), '{"name":"delivery-api"}');
|
|
45
|
+
fs.writeFileSync(path.join(devRepoPath, 'src', 'index.js'), '// app entry');
|
|
46
|
+
|
|
47
|
+
// --- Step 1: Simulate prd init ---
|
|
48
|
+
header('Step 1: prd init');
|
|
49
|
+
|
|
50
|
+
const templatesDir = path.join(__dirname, '..', 'templates');
|
|
51
|
+
const wsDir = path.join(TEST_DIR, WORKSPACE_NAME);
|
|
52
|
+
|
|
53
|
+
// Create workspace directory
|
|
54
|
+
fs.mkdirSync(wsDir, { recursive: true });
|
|
55
|
+
fs.mkdirSync(path.join(wsDir, PROJECT_NAME), { recursive: true });
|
|
56
|
+
|
|
57
|
+
// Create config
|
|
58
|
+
const config = {
|
|
59
|
+
name: WORKSPACE_NAME,
|
|
60
|
+
nextId: 1,
|
|
61
|
+
ignoreDirs: ['.git', 'node_modules', 'scripts', 'docs'],
|
|
62
|
+
projects: {
|
|
63
|
+
[PROJECT_NAME]: {
|
|
64
|
+
description: 'Backend API for food delivery platform',
|
|
65
|
+
repoPath: devRepoPath
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
processVersion: '2.0'
|
|
69
|
+
};
|
|
70
|
+
fs.writeFileSync(path.join(wsDir, CONFIG_FILENAME), JSON.stringify(config, null, 2) + '\n');
|
|
71
|
+
assert(fs.existsSync(path.join(wsDir, CONFIG_FILENAME)), 'Config created');
|
|
72
|
+
|
|
73
|
+
// Copy PROCESS.md
|
|
74
|
+
fs.copyFileSync(
|
|
75
|
+
path.join(templatesDir, 'PROCESS.md'),
|
|
76
|
+
path.join(wsDir, 'PROCESS.md')
|
|
77
|
+
);
|
|
78
|
+
assert(fs.existsSync(path.join(wsDir, 'PROCESS.md')), 'PROCESS.md created');
|
|
79
|
+
|
|
80
|
+
// Create README from template
|
|
81
|
+
let readme = fs.readFileSync(path.join(templatesDir, 'README.md.tmpl'), 'utf-8');
|
|
82
|
+
readme = readme.replace(/\{\{workspaceName\}\}/g, WORKSPACE_NAME);
|
|
83
|
+
readme = readme.replace(/\{\{projectName\}\}/g, PROJECT_NAME);
|
|
84
|
+
readme = readme.replace(/\{\{projectDescription\}\}/g, 'Backend API for food delivery platform');
|
|
85
|
+
readme = readme.replace(/\{\{nextId\}\}/g, 'US-001');
|
|
86
|
+
fs.writeFileSync(path.join(wsDir, 'README.md'), readme);
|
|
87
|
+
assert(fs.existsSync(path.join(wsDir, 'README.md')), 'README.md created');
|
|
88
|
+
|
|
89
|
+
// Create backlog from template
|
|
90
|
+
let backlog = fs.readFileSync(path.join(templatesDir, 'backlog.md.tmpl'), 'utf-8');
|
|
91
|
+
backlog = backlog.replace(/\{\{projectName\}\}/g, PROJECT_NAME);
|
|
92
|
+
fs.writeFileSync(path.join(wsDir, PROJECT_NAME, 'backlog.md'), backlog);
|
|
93
|
+
assert(fs.existsSync(path.join(wsDir, PROJECT_NAME, 'backlog.md')), 'backlog.md created');
|
|
94
|
+
|
|
95
|
+
// Create RULES.md from template
|
|
96
|
+
let rules = fs.readFileSync(path.join(templatesDir, 'RULES.md.tmpl'), 'utf-8');
|
|
97
|
+
rules = rules.replace(/\{\{projectName\}\}/g, PROJECT_NAME);
|
|
98
|
+
fs.writeFileSync(path.join(wsDir, PROJECT_NAME, 'RULES.md'), rules);
|
|
99
|
+
assert(fs.existsSync(path.join(wsDir, PROJECT_NAME, 'RULES.md')), 'RULES.md created');
|
|
100
|
+
|
|
101
|
+
// Create .code-workspace file
|
|
102
|
+
const workspace = {
|
|
103
|
+
folders: [
|
|
104
|
+
{ name: 'product-docs', path: WORKSPACE_NAME },
|
|
105
|
+
{ name: PROJECT_NAME, path: PROJECT_NAME }
|
|
106
|
+
],
|
|
107
|
+
settings: {}
|
|
108
|
+
};
|
|
109
|
+
const wsFilePath = path.join(TEST_DIR, `${WORKSPACE_NAME}.code-workspace`);
|
|
110
|
+
fs.writeFileSync(wsFilePath, JSON.stringify(workspace, null, 2) + '\n');
|
|
111
|
+
assert(fs.existsSync(wsFilePath), '.code-workspace file created');
|
|
112
|
+
|
|
113
|
+
// --- Step 2: Simulate creating stories (what /create would do in Cursor) ---
|
|
114
|
+
header('Step 2: Create stories (simulating /create in Cursor)');
|
|
115
|
+
|
|
116
|
+
function createStory(id, title, priority, status) {
|
|
117
|
+
const story = `---
|
|
118
|
+
id: US-${String(id).padStart(3, '0')}
|
|
119
|
+
project: ${PROJECT_NAME}
|
|
120
|
+
status: ${status}
|
|
121
|
+
phase: ${status === 'done' ? 'deployed' : status === 'dev' ? 'development' : 'planning'}
|
|
122
|
+
progress: ${status === 'done' ? '100' : status === 'dev' ? '50' : '0'}
|
|
123
|
+
priority: ${priority}
|
|
124
|
+
created: 2026-02-09
|
|
125
|
+
updated: 2026-02-09
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
# US-${String(id).padStart(3, '0')}: ${title}
|
|
129
|
+
|
|
130
|
+
## Goal
|
|
131
|
+
${title} for the food delivery platform.
|
|
132
|
+
|
|
133
|
+
## Acceptance Criteria
|
|
134
|
+
1. Feature works as expected
|
|
135
|
+
2. Tests pass
|
|
136
|
+
3. Documentation updated
|
|
137
|
+
`;
|
|
138
|
+
fs.writeFileSync(path.join(wsDir, PROJECT_NAME, `US-${String(id).padStart(3, '0')}.md`), story);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
createStory(1, 'Real-time order tracking', 'P0', 'done');
|
|
142
|
+
createStory(2, 'Restaurant menu management', 'P1', 'dev');
|
|
143
|
+
createStory(3, 'Payment processing with Stripe', 'P0', 'create');
|
|
144
|
+
createStory(4, 'Push notifications for order updates', 'P1', 'backlog');
|
|
145
|
+
|
|
146
|
+
assert(fs.existsSync(path.join(wsDir, PROJECT_NAME, 'US-001.md')), 'US-001.md created');
|
|
147
|
+
assert(fs.existsSync(path.join(wsDir, PROJECT_NAME, 'US-004.md')), 'US-004.md created');
|
|
148
|
+
|
|
149
|
+
// --- Step 3: Test dashboard ---
|
|
150
|
+
header('Step 3: prd dashboard');
|
|
151
|
+
|
|
152
|
+
const data = scanAllStories(wsDir);
|
|
153
|
+
assert(data.stories.length === 4, `Found ${data.stories.length} stories (expected 4)`);
|
|
154
|
+
assert(data.projects.length === 1, `Found ${data.projects.length} project(s) (expected 1)`);
|
|
155
|
+
assert(data.projects[0].name === PROJECT_NAME, `Project name: ${data.projects[0].name}`);
|
|
156
|
+
|
|
157
|
+
const stats = getStats(data.stories);
|
|
158
|
+
assert(stats.total === 4, `Total stories: ${stats.total}`);
|
|
159
|
+
assert(stats.byStatus.done === 1, `Done: ${stats.byStatus.done}`);
|
|
160
|
+
assert(stats.byStatus.dev === 1, `Dev: ${stats.byStatus.dev}`);
|
|
161
|
+
assert(stats.byStatus.create === 1, `Create: ${stats.byStatus.create}`);
|
|
162
|
+
assert(stats.byStatus.backlog === 1, `Backlog: ${stats.byStatus.backlog}`);
|
|
163
|
+
assert(stats.avgProgress === 38, `Avg progress: ${stats.avgProgress}% (expected 38%)`);
|
|
164
|
+
|
|
165
|
+
// Display the actual dashboard
|
|
166
|
+
console.log('');
|
|
167
|
+
dashboard(wsDir);
|
|
168
|
+
|
|
169
|
+
// --- Step 4: Simulate add-project ---
|
|
170
|
+
header('Step 4: Add second project');
|
|
171
|
+
|
|
172
|
+
const proj2 = 'customer-app';
|
|
173
|
+
fs.mkdirSync(path.join(wsDir, proj2), { recursive: true });
|
|
174
|
+
|
|
175
|
+
let backlog2 = fs.readFileSync(path.join(templatesDir, 'backlog.md.tmpl'), 'utf-8');
|
|
176
|
+
backlog2 = backlog2.replace(/\{\{projectName\}\}/g, proj2);
|
|
177
|
+
fs.writeFileSync(path.join(wsDir, proj2, 'backlog.md'), backlog2);
|
|
178
|
+
|
|
179
|
+
let rules2 = fs.readFileSync(path.join(templatesDir, 'RULES.md.tmpl'), 'utf-8');
|
|
180
|
+
rules2 = rules2.replace(/\{\{projectName\}\}/g, proj2);
|
|
181
|
+
fs.writeFileSync(path.join(wsDir, proj2, 'RULES.md'), rules2);
|
|
182
|
+
|
|
183
|
+
createStoryInProject(5, 'Order placement flow', 'P0', 'create', proj2);
|
|
184
|
+
|
|
185
|
+
function createStoryInProject(id, title, priority, status, project) {
|
|
186
|
+
const story = `---
|
|
187
|
+
id: US-${String(id).padStart(3, '0')}
|
|
188
|
+
project: ${project}
|
|
189
|
+
status: ${status}
|
|
190
|
+
phase: planning
|
|
191
|
+
progress: 0
|
|
192
|
+
priority: ${priority}
|
|
193
|
+
created: 2026-02-09
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
# US-${String(id).padStart(3, '0')}: ${title}
|
|
197
|
+
|
|
198
|
+
## Goal
|
|
199
|
+
${title}.
|
|
200
|
+
|
|
201
|
+
## Acceptance Criteria
|
|
202
|
+
1. Feature works as expected
|
|
203
|
+
`;
|
|
204
|
+
fs.writeFileSync(path.join(wsDir, project, `US-${String(id).padStart(3, '0')}.md`), story);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
assert(fs.existsSync(path.join(wsDir, proj2, 'US-005.md')), 'customer-app/US-005.md created');
|
|
208
|
+
|
|
209
|
+
// --- Step 5: Multi-project dashboard ---
|
|
210
|
+
header('Step 5: Multi-project dashboard');
|
|
211
|
+
|
|
212
|
+
const data2 = scanAllStories(wsDir);
|
|
213
|
+
assert(data2.stories.length === 5, `Found ${data2.stories.length} stories across all projects`);
|
|
214
|
+
assert(data2.projects.length === 2, `Found ${data2.projects.length} projects`);
|
|
215
|
+
|
|
216
|
+
console.log('');
|
|
217
|
+
dashboard(wsDir);
|
|
218
|
+
|
|
219
|
+
// --- Step 6: Config auto-detection ---
|
|
220
|
+
header('Step 6: Config auto-detection');
|
|
221
|
+
|
|
222
|
+
const loadedConfig = loadConfig(wsDir);
|
|
223
|
+
assert(loadedConfig !== null, 'Config loaded from workspace');
|
|
224
|
+
assert(loadedConfig.name === WORKSPACE_NAME, `Workspace name: ${loadedConfig.name}`);
|
|
225
|
+
assert(loadedConfig.projects[PROJECT_NAME] !== undefined, `Project "${PROJECT_NAME}" in config`);
|
|
226
|
+
|
|
227
|
+
// --- Summary ---
|
|
228
|
+
header('TEST RESULTS');
|
|
229
|
+
console.log(chalk.bold.green('\n ✅ ALL TESTS PASSED!\n'));
|
|
230
|
+
console.log(chalk.white(' Sarah\'s workspace:'));
|
|
231
|
+
console.log(chalk.gray(` ${wsDir}/`));
|
|
232
|
+
console.log(chalk.gray(` ├── .prd.config.json`));
|
|
233
|
+
console.log(chalk.gray(` ├── PROCESS.md`));
|
|
234
|
+
console.log(chalk.gray(` ├── README.md`));
|
|
235
|
+
console.log(chalk.gray(` ├── delivery-api/ (4 stories)`));
|
|
236
|
+
console.log(chalk.gray(` └── customer-app/ (1 story)\n`));
|
|
237
|
+
console.log(chalk.gray(` Workspace: ${wsFilePath}\n`));
|