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,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prd init - Interactive workspace scaffolding
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const inquirer = require('inquirer');
|
|
9
|
+
const { CONFIG_FILENAME } = require('../config');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Read a template file and replace {{placeholders}}
|
|
13
|
+
*/
|
|
14
|
+
function renderTemplate(templatePath, vars) {
|
|
15
|
+
let content = fs.readFileSync(templatePath, 'utf-8');
|
|
16
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
17
|
+
content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value);
|
|
18
|
+
}
|
|
19
|
+
return content;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a .code-workspace file
|
|
24
|
+
*/
|
|
25
|
+
function createWorkspaceFile(workspacePath, workspaceName, projects) {
|
|
26
|
+
const folders = [
|
|
27
|
+
{ name: 'product-docs', path: path.basename(workspacePath) }
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
for (const [name, proj] of Object.entries(projects)) {
|
|
31
|
+
if (proj.repoPath) {
|
|
32
|
+
const relPath = path.relative(path.dirname(workspacePath), proj.repoPath);
|
|
33
|
+
folders.push({ name, path: relPath });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const workspace = {
|
|
38
|
+
folders,
|
|
39
|
+
settings: {}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const wsFilePath = path.join(
|
|
43
|
+
path.dirname(workspacePath),
|
|
44
|
+
`${workspaceName.replace(/\s+/g, '-').toLowerCase()}.code-workspace`
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(wsFilePath, JSON.stringify(workspace, null, 2) + '\n', 'utf-8');
|
|
48
|
+
return wsFilePath;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Main init command
|
|
53
|
+
*/
|
|
54
|
+
async function init(options = {}) {
|
|
55
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates');
|
|
56
|
+
|
|
57
|
+
console.log('\n' + chalk.bold.cyan('Welcome to Product OS Framework!'));
|
|
58
|
+
console.log(chalk.gray('Let\'s set up your product workspace.\n'));
|
|
59
|
+
|
|
60
|
+
// --- Prompts ---
|
|
61
|
+
const answers = await inquirer.prompt([
|
|
62
|
+
{
|
|
63
|
+
type: 'input',
|
|
64
|
+
name: 'workspaceName',
|
|
65
|
+
message: 'Workspace name:',
|
|
66
|
+
default: 'product-docs',
|
|
67
|
+
validate: (v) => v.trim().length > 0 || 'Name is required'
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
type: 'input',
|
|
71
|
+
name: 'projectName',
|
|
72
|
+
message: 'Your first project name:',
|
|
73
|
+
validate: (v) => {
|
|
74
|
+
if (!v.trim()) return 'Project name is required';
|
|
75
|
+
if (/[^a-zA-Z0-9_-]/.test(v.trim())) return 'Use only letters, numbers, hyphens, underscores';
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
type: 'input',
|
|
81
|
+
name: 'projectDescription',
|
|
82
|
+
message: 'Project description (optional):',
|
|
83
|
+
default: ''
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'confirm',
|
|
87
|
+
name: 'hasRepo',
|
|
88
|
+
message: 'Do you have an existing dev repo for this project?',
|
|
89
|
+
default: false
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'input',
|
|
93
|
+
name: 'repoPath',
|
|
94
|
+
message: 'Path to the dev repo:',
|
|
95
|
+
when: (a) => a.hasRepo,
|
|
96
|
+
validate: (v) => {
|
|
97
|
+
const resolved = path.resolve(v);
|
|
98
|
+
if (!fs.existsSync(resolved)) return `Path not found: ${resolved}`;
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const wsName = answers.workspaceName.trim();
|
|
105
|
+
const projName = answers.projectName.trim();
|
|
106
|
+
const projDesc = answers.projectDescription.trim();
|
|
107
|
+
const repoPath = answers.repoPath ? path.resolve(answers.repoPath) : null;
|
|
108
|
+
|
|
109
|
+
// --- Determine target directory ---
|
|
110
|
+
const targetDir = path.resolve(process.cwd(), wsName);
|
|
111
|
+
|
|
112
|
+
if (fs.existsSync(targetDir)) {
|
|
113
|
+
const { overwrite } = await inquirer.prompt([{
|
|
114
|
+
type: 'confirm',
|
|
115
|
+
name: 'overwrite',
|
|
116
|
+
message: `Directory "${wsName}" already exists. Continue and add files?`,
|
|
117
|
+
default: false
|
|
118
|
+
}]);
|
|
119
|
+
if (!overwrite) {
|
|
120
|
+
console.log(chalk.yellow('\nAborted.\n'));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(chalk.gray('\nCreating workspace...\n'));
|
|
126
|
+
|
|
127
|
+
// --- Create directories ---
|
|
128
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
129
|
+
fs.mkdirSync(path.join(targetDir, projName), { recursive: true });
|
|
130
|
+
|
|
131
|
+
// --- Create config ---
|
|
132
|
+
const projects = {};
|
|
133
|
+
projects[projName] = {
|
|
134
|
+
description: projDesc,
|
|
135
|
+
repoPath: repoPath || null
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const config = {
|
|
139
|
+
name: wsName,
|
|
140
|
+
nextId: 1,
|
|
141
|
+
ignoreDirs: ['.git', 'node_modules', 'scripts', 'docs'],
|
|
142
|
+
projects,
|
|
143
|
+
processVersion: '2.0'
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
fs.writeFileSync(
|
|
147
|
+
path.join(targetDir, CONFIG_FILENAME),
|
|
148
|
+
JSON.stringify(config, null, 2) + '\n',
|
|
149
|
+
'utf-8'
|
|
150
|
+
);
|
|
151
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${wsName}/${CONFIG_FILENAME}`));
|
|
152
|
+
|
|
153
|
+
// --- Copy PROCESS.md ---
|
|
154
|
+
const processTemplate = path.join(templatesDir, 'PROCESS.md');
|
|
155
|
+
if (fs.existsSync(processTemplate)) {
|
|
156
|
+
fs.copyFileSync(processTemplate, path.join(targetDir, 'PROCESS.md'));
|
|
157
|
+
} else {
|
|
158
|
+
// Fallback: minimal PROCESS.md
|
|
159
|
+
fs.writeFileSync(path.join(targetDir, 'PROCESS.md'),
|
|
160
|
+
'# Product Development Process v2.0\n\nSee the Product OS Framework documentation for the full process.\n', 'utf-8');
|
|
161
|
+
}
|
|
162
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${wsName}/PROCESS.md`));
|
|
163
|
+
|
|
164
|
+
// --- Create README.md ---
|
|
165
|
+
const readmeContent = renderTemplate(path.join(templatesDir, 'README.md.tmpl'), {
|
|
166
|
+
workspaceName: wsName,
|
|
167
|
+
projectName: projName,
|
|
168
|
+
projectDescription: projDesc || 'No description yet',
|
|
169
|
+
nextId: 'US-001'
|
|
170
|
+
});
|
|
171
|
+
fs.writeFileSync(path.join(targetDir, 'README.md'), readmeContent, 'utf-8');
|
|
172
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${wsName}/README.md`));
|
|
173
|
+
|
|
174
|
+
// --- Create project backlog ---
|
|
175
|
+
const backlogContent = renderTemplate(path.join(templatesDir, 'backlog.md.tmpl'), {
|
|
176
|
+
projectName: projName
|
|
177
|
+
});
|
|
178
|
+
fs.writeFileSync(path.join(targetDir, projName, 'backlog.md'), backlogContent, 'utf-8');
|
|
179
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${wsName}/${projName}/backlog.md`));
|
|
180
|
+
|
|
181
|
+
// --- Create project RULES.md ---
|
|
182
|
+
const rulesContent = renderTemplate(path.join(templatesDir, 'RULES.md.tmpl'), {
|
|
183
|
+
projectName: projName
|
|
184
|
+
});
|
|
185
|
+
fs.writeFileSync(path.join(targetDir, projName, 'RULES.md'), rulesContent, 'utf-8');
|
|
186
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(`${wsName}/${projName}/RULES.md`));
|
|
187
|
+
|
|
188
|
+
// --- Create .code-workspace file ---
|
|
189
|
+
const wsFilePath = createWorkspaceFile(targetDir, wsName, projects);
|
|
190
|
+
console.log(chalk.green(' ✅ Created ') + chalk.white(path.basename(wsFilePath)));
|
|
191
|
+
|
|
192
|
+
// --- Create .gitignore ---
|
|
193
|
+
fs.writeFileSync(path.join(targetDir, '.gitignore'), 'node_modules/\n.DS_Store\n', 'utf-8');
|
|
194
|
+
|
|
195
|
+
// --- Done! ---
|
|
196
|
+
console.log('\n' + chalk.bold.green('🎉 Done! Your product workspace is ready.\n'));
|
|
197
|
+
|
|
198
|
+
const wsFileRelative = path.relative(process.cwd(), wsFilePath);
|
|
199
|
+
|
|
200
|
+
console.log(chalk.white.bold('Next steps:\n'));
|
|
201
|
+
|
|
202
|
+
const wsFileAbsolute = path.resolve(wsFilePath);
|
|
203
|
+
|
|
204
|
+
console.log(chalk.cyan(' 1. Open in Cursor:'));
|
|
205
|
+
console.log(chalk.white(` Open Cursor → File → Open Workspace from File`));
|
|
206
|
+
console.log(chalk.white(` Navigate to: ${chalk.bold(wsFileAbsolute)}`));
|
|
207
|
+
console.log(chalk.gray(` (This connects your product docs${repoPath ? ' and dev repo' : ''} in one workspace)\n`));
|
|
208
|
+
|
|
209
|
+
console.log(chalk.cyan(' 2. Create your first story (in Cursor chat):'));
|
|
210
|
+
console.log(chalk.white(` /create "feature description" @${projName}/backlog.md\n`));
|
|
211
|
+
|
|
212
|
+
console.log(chalk.cyan(' 3. View dashboard (in any terminal):'));
|
|
213
|
+
console.log(chalk.white(` cd ${wsName} && prd\n`));
|
|
214
|
+
|
|
215
|
+
if (!repoPath) {
|
|
216
|
+
console.log(chalk.gray(' ℹ️ No dev repo linked yet. When development starts, run:'));
|
|
217
|
+
console.log(chalk.gray(` prd add-project ${projName} --link /path/to/repo\n`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { init };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config file loader for Product OS Framework
|
|
3
|
+
* Reads .prd.config.json from the workspace root
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
const CONFIG_FILENAME = '.prd.config.json';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find the workspace root by walking up from cwd looking for .prd.config.json
|
|
13
|
+
* @param {string} startDir - Directory to start searching from
|
|
14
|
+
* @returns {string|null} Path to workspace root, or null
|
|
15
|
+
*/
|
|
16
|
+
function findWorkspaceRoot(startDir) {
|
|
17
|
+
let dir = path.resolve(startDir || process.cwd());
|
|
18
|
+
const root = path.parse(dir).root;
|
|
19
|
+
|
|
20
|
+
while (dir !== root) {
|
|
21
|
+
if (fs.existsSync(path.join(dir, CONFIG_FILENAME))) {
|
|
22
|
+
return dir;
|
|
23
|
+
}
|
|
24
|
+
dir = path.dirname(dir);
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Load config from workspace root
|
|
31
|
+
* @param {string} workspacePath - Path to workspace (or auto-detect)
|
|
32
|
+
* @returns {Object} Config object with defaults applied
|
|
33
|
+
*/
|
|
34
|
+
function loadConfig(workspacePath) {
|
|
35
|
+
const root = workspacePath || findWorkspaceRoot(process.cwd());
|
|
36
|
+
if (!root) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const configPath = path.join(root, CONFIG_FILENAME);
|
|
41
|
+
if (!fs.existsSync(configPath)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
47
|
+
const config = JSON.parse(raw);
|
|
48
|
+
config._root = root;
|
|
49
|
+
config._configPath = configPath;
|
|
50
|
+
return applyDefaults(config);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
console.error(`Error reading ${CONFIG_FILENAME}:`, err.message);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Save config to workspace root
|
|
59
|
+
* @param {Object} config - Config object to save
|
|
60
|
+
*/
|
|
61
|
+
function saveConfig(config) {
|
|
62
|
+
const configPath = config._configPath;
|
|
63
|
+
if (!configPath) {
|
|
64
|
+
throw new Error('Cannot save config: no _configPath set');
|
|
65
|
+
}
|
|
66
|
+
const toWrite = { ...config };
|
|
67
|
+
delete toWrite._root;
|
|
68
|
+
delete toWrite._configPath;
|
|
69
|
+
fs.writeFileSync(configPath, JSON.stringify(toWrite, null, 2) + '\n', 'utf-8');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Apply default values to config
|
|
74
|
+
* @param {Object} config
|
|
75
|
+
* @returns {Object}
|
|
76
|
+
*/
|
|
77
|
+
function applyDefaults(config) {
|
|
78
|
+
return {
|
|
79
|
+
name: config.name || 'Product Docs',
|
|
80
|
+
nextId: config.nextId || 1,
|
|
81
|
+
ignoreDirs: config.ignoreDirs || ['.git', 'node_modules', 'scripts', 'docs'],
|
|
82
|
+
projects: config.projects || {},
|
|
83
|
+
processVersion: config.processVersion || '2.0',
|
|
84
|
+
...config
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
CONFIG_FILENAME,
|
|
90
|
+
findWorkspaceRoot,
|
|
91
|
+
loadConfig,
|
|
92
|
+
saveConfig
|
|
93
|
+
};
|
package/lib/parser.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story Parser - Reusable core module
|
|
3
|
+
* Parses US-*.md files with YAML frontmatter
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const matter = require('gray-matter');
|
|
9
|
+
const { loadConfig } = require('./config');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse a single story file
|
|
13
|
+
* @param {string} filePath - Path to US-*.md file
|
|
14
|
+
* @returns {Object} Story metadata
|
|
15
|
+
*/
|
|
16
|
+
function parseStory(filePath) {
|
|
17
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
18
|
+
const { data, content: body } = matter(content);
|
|
19
|
+
|
|
20
|
+
// Extract title from first # heading
|
|
21
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
22
|
+
const title = titleMatch ? titleMatch[1] : 'Untitled';
|
|
23
|
+
|
|
24
|
+
// Normalize progress to string format "X/Y"
|
|
25
|
+
let progress = data.progress || '0/0';
|
|
26
|
+
if (typeof progress === 'number') {
|
|
27
|
+
progress = `${progress}/100`;
|
|
28
|
+
} else if (typeof progress !== 'string') {
|
|
29
|
+
progress = String(progress);
|
|
30
|
+
}
|
|
31
|
+
if (!progress.includes('/')) {
|
|
32
|
+
progress = `${progress}/100`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
id: data.id || path.basename(filePath, '.md'),
|
|
37
|
+
title,
|
|
38
|
+
project: data.project || 'unknown',
|
|
39
|
+
status: data.status || 'backlog',
|
|
40
|
+
phase: data.phase || 'create',
|
|
41
|
+
progress,
|
|
42
|
+
priority: data.priority || 'P2',
|
|
43
|
+
assignee: data.assignee || 'unassigned',
|
|
44
|
+
blockers: Array.isArray(data.blockers) ? data.blockers : [],
|
|
45
|
+
dependencies: Array.isArray(data.dependencies) ? data.dependencies : [],
|
|
46
|
+
created: data.created || null,
|
|
47
|
+
updated: data.updated || null,
|
|
48
|
+
filePath
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Scan all stories in a product-docs workspace
|
|
54
|
+
* @param {string} productDocsPath - Path to workspace root
|
|
55
|
+
* @returns {Object} { projects: [], stories: [], lastUpdated: Date }
|
|
56
|
+
*/
|
|
57
|
+
function scanAllStories(productDocsPath) {
|
|
58
|
+
if (!fs.existsSync(productDocsPath)) {
|
|
59
|
+
throw new Error(`Path does not exist: ${productDocsPath}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Load config for ignore list, or use defaults
|
|
63
|
+
const config = loadConfig(productDocsPath);
|
|
64
|
+
const ignoreDirs = config
|
|
65
|
+
? config.ignoreDirs
|
|
66
|
+
: ['.git', 'node_modules', 'scripts', 'cli', 'docs'];
|
|
67
|
+
|
|
68
|
+
const stories = [];
|
|
69
|
+
const projects = {};
|
|
70
|
+
|
|
71
|
+
const entries = fs.readdirSync(productDocsPath, { withFileTypes: true });
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!entry.isDirectory()) continue;
|
|
75
|
+
|
|
76
|
+
const projectName = entry.name;
|
|
77
|
+
|
|
78
|
+
// Skip ignored directories
|
|
79
|
+
if (projectName.startsWith('.') || ignoreDirs.includes(projectName)) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const projectPath = path.join(productDocsPath, projectName);
|
|
84
|
+
const files = fs.readdirSync(projectPath);
|
|
85
|
+
const storyFiles = files.filter(f => /^US-\d+\.md$/.test(f));
|
|
86
|
+
|
|
87
|
+
if (storyFiles.length === 0) continue;
|
|
88
|
+
|
|
89
|
+
projects[projectName] = {
|
|
90
|
+
name: projectName,
|
|
91
|
+
path: projectPath,
|
|
92
|
+
storyCount: storyFiles.length,
|
|
93
|
+
stories: []
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const file of storyFiles) {
|
|
97
|
+
try {
|
|
98
|
+
const story = parseStory(path.join(projectPath, file));
|
|
99
|
+
stories.push(story);
|
|
100
|
+
projects[projectName].stories.push(story);
|
|
101
|
+
} catch (err) {
|
|
102
|
+
// Silently skip unparseable files in production
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
projects: Object.values(projects),
|
|
109
|
+
stories,
|
|
110
|
+
lastUpdated: new Date()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get summary statistics
|
|
116
|
+
* @param {Array} stories
|
|
117
|
+
* @returns {Object}
|
|
118
|
+
*/
|
|
119
|
+
function getStats(stories) {
|
|
120
|
+
const stats = {
|
|
121
|
+
total: stories.length,
|
|
122
|
+
byStatus: {},
|
|
123
|
+
byPhase: {},
|
|
124
|
+
byPriority: {},
|
|
125
|
+
blocked: 0,
|
|
126
|
+
avgProgress: 0
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let totalProgress = 0;
|
|
130
|
+
|
|
131
|
+
for (const story of stories) {
|
|
132
|
+
stats.byStatus[story.status] = (stats.byStatus[story.status] || 0) + 1;
|
|
133
|
+
stats.byPhase[story.phase] = (stats.byPhase[story.phase] || 0) + 1;
|
|
134
|
+
stats.byPriority[story.priority] = (stats.byPriority[story.priority] || 0) + 1;
|
|
135
|
+
|
|
136
|
+
if (story.blockers && story.blockers.length > 0) {
|
|
137
|
+
stats.blocked++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const [completed, total] = story.progress.split('/').map(Number);
|
|
141
|
+
if (total > 0) {
|
|
142
|
+
totalProgress += (completed / total) * 100;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
stats.avgProgress = stories.length > 0 ? Math.round(totalProgress / stories.length) : 0;
|
|
147
|
+
return stats;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
parseStory,
|
|
152
|
+
scanAllStories,
|
|
153
|
+
getStats
|
|
154
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "prd-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Product OS Framework - AI-native product development lifecycle for Cursor",
|
|
5
|
+
"main": "lib/parser.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"prd": "./bin/prd.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test/run.js",
|
|
11
|
+
"link": "npm link",
|
|
12
|
+
"unlink": "npm unlink"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"product-management",
|
|
16
|
+
"prd",
|
|
17
|
+
"cursor",
|
|
18
|
+
"ai-native",
|
|
19
|
+
"developer-tools",
|
|
20
|
+
"product-development",
|
|
21
|
+
"agile",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"author": "Nimrod Margalit",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/nimidev/product-OS-framework.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/nimidev/product-OS-framework#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=14.0.0"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"chalk": "^4.1.2",
|
|
36
|
+
"cli-table3": "^0.6.3",
|
|
37
|
+
"commander": "^12.0.0",
|
|
38
|
+
"gray-matter": "^4.0.3",
|
|
39
|
+
"inquirer": "^8.2.6"
|
|
40
|
+
}
|
|
41
|
+
}
|