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.
@@ -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
+ }