@open-product-primer/cli 2.4.0 → 2.5.1
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/dist/commands/init.js +1 -0
- package/dist/commands/update.js +1 -0
- package/dist/lib/install-agent.js +3 -3
- package/dist/lib/templates.d.ts +1 -0
- package/dist/lib/templates.js +80 -1
- package/dist/workflows/pdr.cursor-command.md +1 -1
- package/dist/workflows/pdr.inline.md +2 -1
- package/dist/workflows/pdr.template.md +3 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -90,6 +90,7 @@ function initCommand() {
|
|
|
90
90
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'discovery.md'), templates_1.discoveryTemplate);
|
|
91
91
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'templates', 'note.md'), noteContent);
|
|
92
92
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
93
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-decisions-view.js'), templates_1.decisionsViewScriptTemplate);
|
|
93
94
|
if (okfEnabled) {
|
|
94
95
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'index.md'), (0, templates_1.indexTemplate)(projectName));
|
|
95
96
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -71,6 +71,7 @@ function updateCommand() {
|
|
|
71
71
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
72
72
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
73
73
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
74
|
+
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-decisions-view.js'), templates_1.decisionsViewScriptTemplate);
|
|
74
75
|
const configPath = path.join(primerDir, 'config.yaml');
|
|
75
76
|
if (fs.existsSync(configPath)) {
|
|
76
77
|
const existingConfig = fs.readFileSync(configPath, 'utf-8');
|
|
@@ -90,8 +90,8 @@ async function promptFrameworkSelection(projectRoot) {
|
|
|
90
90
|
return select({
|
|
91
91
|
message: 'Which speccing framework does this project use?',
|
|
92
92
|
choices: [
|
|
93
|
-
{ name: '
|
|
94
|
-
{ name: '
|
|
93
|
+
{ name: 'Native (oprim-authored specs, recommended)', value: 'native' },
|
|
94
|
+
{ name: 'OpenSpec', value: 'openspec' },
|
|
95
95
|
{ name: 'None', value: 'none' },
|
|
96
96
|
],
|
|
97
97
|
});
|
|
@@ -129,7 +129,7 @@ async function promptAgentSelection(projectRoot) {
|
|
|
129
129
|
}
|
|
130
130
|
async function promptPdrSurfacing() {
|
|
131
131
|
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
|
132
|
-
return confirm({ message: 'Enable proactive PDR surfacing in skills? (
|
|
132
|
+
return confirm({ message: 'Enable proactive PDR surfacing in skills? (Y/n)', default: true });
|
|
133
133
|
}
|
|
134
134
|
async function promptOkfFrontmatter() {
|
|
135
135
|
const { confirm } = await Promise.resolve().then(() => __importStar(require('@inquirer/prompts')));
|
package/dist/lib/templates.d.ts
CHANGED
|
@@ -9,4 +9,5 @@ export declare const noteTemplate = "# Note: <Note title>\n\n<Capture the observ
|
|
|
9
9
|
export declare const criteriaTemplate = "metrics:\n - id: metric_id\n name: \"Metric name\"\n baseline: 0.00\n target: 0.00\n timeframe: \"30 days post-launch\"\n launch_date: \"YYYY-MM-DD\"\n source:\n type: amplitude\n definition:\n event: event_name\n aggregation: unique_users\n denominator_event: null\n segment: null\n";
|
|
10
10
|
export declare const discoveryTemplate = "# Discovery: BET-XXX <Bet title>\n\n## Problem Framing\n- **Problem statement**: <What problem are we solving and for whom?>\n- **Evidence this is real**: <Data, support tickets, user quotes, etc.>\n- **Why it matters**: <Business or user impact if left unsolved>\n\n## User Research Signals\n- **Research conducted**: <Interviews, surveys, usability tests, etc.>\n- **Key findings**: <What did we learn?>\n- **Assumptions to validate**: <What are we still unsure about?>\n\n## Competitive Context\n- **How others solve this**: <Competitor or adjacent solutions>\n- **Our differentiation**: <Why our approach is better or different>\n- **Gaps / opportunities**: <What's underserved in the market?>\n\n## Open Questions\n- [ ] <Question 1 \u2014 what needs to be answered before committing?>\n- [ ] <Question 2>\n- [ ] <Question 3>\n";
|
|
11
11
|
export declare const sequenceViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst SEQUENCE_PATH = path.join(process.cwd(), 'oprim/sequence.yaml');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/sequence-view.md');\nconst MAX_LABEL_LEN = 40;\n\nfunction parseSequenceYaml(text) {\n const BUCKETS = ['now', 'next', 'later', 'backlog'];\n const result = Object.fromEntries(BUCKETS.map(b => [b, []]));\n let section = null, item = null, listField = null;\n\n for (const raw of text.split('\\n')) {\n const trimmed = raw.trim();\n if (!trimmed || trimmed.startsWith('#')) continue;\n const indent = raw.length - raw.trimStart().length;\n\n if (indent === 0) {\n const m = trimmed.match(/^(\\w+):/);\n if (m) { section = BUCKETS.includes(m[1]) ? m[1] : null; item = null; listField = null; }\n continue;\n }\n\n if (!section) continue;\n\n if (indent === 2 && trimmed.startsWith('- ')) {\n item = {}; result[section].push(item); listField = null;\n const rest = trimmed.slice(2);\n const m = rest.match(/^(\\w+):\\s*(.*)/);\n if (m) item[m[1]] = m[2].replace(/^['\"]|['\"]$/g, '').trim();\n continue;\n }\n\n if (indent === 4 && item) {\n const m = trimmed.match(/^(\\w+):\\s*(.*)/);\n if (!m) continue;\n const [, key, val] = m;\n const v = val.trim();\n if (v === '[]') { item[key] = []; listField = null; }\n else if (v === '') { item[key] = []; listField = key; }\n else { item[key] = v.replace(/^['\"]|['\"]$/g, ''); listField = null; }\n continue;\n }\n\n if (indent === 6 && item && listField && trimmed.startsWith('- ')) {\n item[listField].push(trimmed.slice(2).trim());\n }\n }\n return result;\n}\n\nfunction nodeId(betId) { return betId.replace(/-/g, ''); }\n\nfunction truncate(title) {\n return title.length > MAX_LABEL_LEN ? title.slice(0, MAX_LABEL_LEN - 3) + '...' : title;\n}\n\nfunction generateMermaidBlock(seq) {\n const lines = ['```mermaid', 'graph TD'];\n const ACTIVE = [['now', 'Now'], ['next', 'Next'], ['later', 'Later']];\n\n for (const [key, label] of ACTIVE) {\n const bets = seq[key] || [];\n if (!bets.length) continue;\n lines.push(' subgraph ' + label);\n for (const bet of bets) {\n lines.push(' ' + nodeId(bet.id) + '[\"' + bet.id + ': ' + truncate(bet.title) + '\"]');\n }\n lines.push(' end');\n }\n\n for (const [key] of ACTIVE) {\n for (const bet of (seq[key] || [])) {\n for (const blocker of (bet.blocked_by || [])) {\n lines.push(' ' + nodeId(bet.id) + ' --> ' + nodeId(blocker));\n }\n }\n }\n\n lines.push('```');\n return lines.join('\\n');\n}\n\nfunction generateBacklogSection(seq) {\n const backlog = seq.backlog || [];\n if (!backlog.length) return '';\n return '\\n\\n### Backlog\\n' + backlog.map(b => '- **' + b.id + '**: ' + b.title).join('\\n');\n}\n\nfunction main() {\n const seq = parseSequenceYaml(fs.readFileSync(SEQUENCE_PATH, 'utf8'));\n const header = [\n '<!-- Auto-generated from oprim/sequence.yaml. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-sequence-view.js -->',\n '',\n '# Sequencing Board',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateMermaidBlock(seq) + generateBacklogSection(seq) + '\\n');\n console.log('Written: oprim/sequence-view.md');\n}\n\nmain();\n";
|
|
12
|
+
export declare const decisionsViewScriptTemplate = "#!/usr/bin/env node\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst DECISIONS_DIR = path.join(process.cwd(), 'oprim/decisions');\nconst OUTPUT_PATH = path.join(process.cwd(), 'oprim/decisions-view.md');\n\nfunction parsePdrFile(text) {\n const idMatch = text.match(/^#\\s*(PDR-\\d+)[:\\s]*(.*)$/m);\n const id = idMatch ? idMatch[1] : null;\n const title = idMatch ? idMatch[2].trim() : '';\n\n const statusMatch = text.match(/^##\\s*Status\\s*\\n(.+)$/m) || text.match(/^Status:\\s*(.+)$/m);\n const statusLine = statusMatch ? statusMatch[1].trim() : '';\n const supersededMatch = statusLine.match(/Superseded by (PDR-\\d+)/);\n\n return {\n id,\n title,\n status: statusLine,\n supersededBy: supersededMatch ? supersededMatch[1] : null,\n };\n}\n\nfunction readPdrs() {\n if (!fs.existsSync(DECISIONS_DIR)) return [];\n const files = fs.readdirSync(DECISIONS_DIR).filter(f => /^PDR-\\d+.*\\.md$/.test(f));\n return files\n .map(f => ({ file: f, ...parsePdrFile(fs.readFileSync(path.join(DECISIONS_DIR, f), 'utf8')) }))\n .filter(pdr => pdr.id);\n}\n\nfunction generateBody(pdrs) {\n if (pdrs.length === 0) {\n return 'No decisions yet. Run `/oprim:pdr` to record your first product decision.\\n';\n }\n\n const byId = Object.fromEntries(pdrs.map(p => [p.id, p]));\n const supersededOf = {};\n for (const pdr of pdrs) {\n if (pdr.supersededBy && byId[pdr.supersededBy]) {\n (supersededOf[pdr.supersededBy] = supersededOf[pdr.supersededBy] || []).push(pdr);\n }\n }\n\n const current = pdrs.filter(p => !p.supersededBy);\n if (current.length === 0) {\n return 'No current decisions \u2014 all recorded PDRs have been superseded.\\n';\n }\n\n const lines = ['## Current decisions', ''];\n for (const pdr of current) {\n lines.push('- **' + pdr.id + '**: ' + pdr.title);\n for (const old of (supersededOf[pdr.id] || [])) {\n lines.push(' - supersedes [' + old.id + '](decisions/' + old.file + '): ' + old.title);\n }\n }\n\n return lines.join('\\n') + '\\n';\n}\n\nfunction main() {\n const pdrs = readPdrs();\n const header = [\n '<!-- Auto-generated from oprim/decisions/. Do not edit directly. -->',\n '<!-- Regenerate by running: node oprim/scripts/generate-decisions-view.js -->',\n '',\n '# Current Decisions',\n '',\n '',\n ].join('\\n');\n fs.writeFileSync(OUTPUT_PATH, header + generateBody(pdrs));\n console.log('Written: oprim/decisions-view.md');\n}\n\nmain();\n";
|
|
12
13
|
export declare const kpiReviewTemplate = "# KPI Review: BET-XXX\n\n**Review date:** YYYY-MM-DD\n**Reviewed by:** <name>\n\n| Metric | Baseline | Target | Actual | Status |\n|--------|----------|--------|--------|--------|\n| <metric name> | - | - | - | pending |\n\n## Decision quality\n<Did outcomes validate the decision? What would you do differently?>\n\n## Actions\n- [ ] Update bet-decision outcome section\n- [ ] Update affected PDRs\n- [ ] Re-sequence impacted bets\n";
|
package/dist/lib/templates.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.kpiReviewTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
3
|
+
exports.kpiReviewTemplate = exports.decisionsViewScriptTemplate = exports.sequenceViewScriptTemplate = exports.discoveryTemplate = exports.criteriaTemplate = exports.noteTemplate = exports.betDecisionTemplate = exports.pdrTemplate = exports.sequenceTemplate = void 0;
|
|
4
4
|
exports.configTemplate = configTemplate;
|
|
5
5
|
exports.okfFrontmatter = okfFrontmatter;
|
|
6
6
|
exports.noteMinimalFrontmatter = noteMinimalFrontmatter;
|
|
@@ -283,6 +283,85 @@ function main() {
|
|
|
283
283
|
console.log('Written: oprim/sequence-view.md');
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
+
main();
|
|
287
|
+
`;
|
|
288
|
+
exports.decisionsViewScriptTemplate = `#!/usr/bin/env node
|
|
289
|
+
'use strict';
|
|
290
|
+
|
|
291
|
+
const fs = require('fs');
|
|
292
|
+
const path = require('path');
|
|
293
|
+
|
|
294
|
+
const DECISIONS_DIR = path.join(process.cwd(), 'oprim/decisions');
|
|
295
|
+
const OUTPUT_PATH = path.join(process.cwd(), 'oprim/decisions-view.md');
|
|
296
|
+
|
|
297
|
+
function parsePdrFile(text) {
|
|
298
|
+
const idMatch = text.match(/^#\\s*(PDR-\\d+)[:\\s]*(.*)$/m);
|
|
299
|
+
const id = idMatch ? idMatch[1] : null;
|
|
300
|
+
const title = idMatch ? idMatch[2].trim() : '';
|
|
301
|
+
|
|
302
|
+
const statusMatch = text.match(/^##\\s*Status\\s*\\n(.+)$/m) || text.match(/^Status:\\s*(.+)$/m);
|
|
303
|
+
const statusLine = statusMatch ? statusMatch[1].trim() : '';
|
|
304
|
+
const supersededMatch = statusLine.match(/Superseded by (PDR-\\d+)/);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
id,
|
|
308
|
+
title,
|
|
309
|
+
status: statusLine,
|
|
310
|
+
supersededBy: supersededMatch ? supersededMatch[1] : null,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function readPdrs() {
|
|
315
|
+
if (!fs.existsSync(DECISIONS_DIR)) return [];
|
|
316
|
+
const files = fs.readdirSync(DECISIONS_DIR).filter(f => /^PDR-\\d+.*\\.md$/.test(f));
|
|
317
|
+
return files
|
|
318
|
+
.map(f => ({ file: f, ...parsePdrFile(fs.readFileSync(path.join(DECISIONS_DIR, f), 'utf8')) }))
|
|
319
|
+
.filter(pdr => pdr.id);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function generateBody(pdrs) {
|
|
323
|
+
if (pdrs.length === 0) {
|
|
324
|
+
return 'No decisions yet. Run \`/oprim:pdr\` to record your first product decision.\\n';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const byId = Object.fromEntries(pdrs.map(p => [p.id, p]));
|
|
328
|
+
const supersededOf = {};
|
|
329
|
+
for (const pdr of pdrs) {
|
|
330
|
+
if (pdr.supersededBy && byId[pdr.supersededBy]) {
|
|
331
|
+
(supersededOf[pdr.supersededBy] = supersededOf[pdr.supersededBy] || []).push(pdr);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const current = pdrs.filter(p => !p.supersededBy);
|
|
336
|
+
if (current.length === 0) {
|
|
337
|
+
return 'No current decisions — all recorded PDRs have been superseded.\\n';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const lines = ['## Current decisions', ''];
|
|
341
|
+
for (const pdr of current) {
|
|
342
|
+
lines.push('- **' + pdr.id + '**: ' + pdr.title);
|
|
343
|
+
for (const old of (supersededOf[pdr.id] || [])) {
|
|
344
|
+
lines.push(' - supersedes [' + old.id + '](decisions/' + old.file + '): ' + old.title);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return lines.join('\\n') + '\\n';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function main() {
|
|
352
|
+
const pdrs = readPdrs();
|
|
353
|
+
const header = [
|
|
354
|
+
'<!-- Auto-generated from oprim/decisions/. Do not edit directly. -->',
|
|
355
|
+
'<!-- Regenerate by running: node oprim/scripts/generate-decisions-view.js -->',
|
|
356
|
+
'',
|
|
357
|
+
'# Current Decisions',
|
|
358
|
+
'',
|
|
359
|
+
'',
|
|
360
|
+
].join('\\n');
|
|
361
|
+
fs.writeFileSync(OUTPUT_PATH, header + generateBody(pdrs));
|
|
362
|
+
console.log('Written: oprim/decisions-view.md');
|
|
363
|
+
}
|
|
364
|
+
|
|
286
365
|
main();
|
|
287
366
|
`;
|
|
288
367
|
exports.kpiReviewTemplate = `# KPI Review: BET-XXX
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Create a new PDR in `oprim/decisions/`. Scan for `PDR-(\d+)-` to assign next ID (zero-padded, default 001). Read `oprim/config.yaml`'s `rules.pdr` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write `oprim/decisions/PDR-NNN-<slug>.md`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created.
|
|
1
|
+
Create a new PDR in `oprim/decisions/`. Scan for `PDR-(\d+)-` to assign next ID (zero-padded, default 001). Read `oprim/config.yaml`'s `rules.pdr` — if non-empty, apply it as additional guidance and reflect it in the generated content; if empty, behavior is unchanged. Gather: title, context, decision, alternatives, consequences, evidence, related bets/specs. Ask if superseding an existing PDR. Write `oprim/decisions/PDR-NNN-<slug>.md`. If superseding: update old PDR Status to "Superseded by PDR-NNN". Report what was created. Then run `node oprim/scripts/generate-decisions-view.js` from the project root to update `oprim/decisions-view.md`.
|
|
@@ -7,4 +7,5 @@ Create a new Product Decision Record in `oprim/decisions/`.
|
|
|
7
7
|
3. Gather: context, decision, alternatives, consequences, evidence, related bets/specs.
|
|
8
8
|
4. Ask if superseding an existing PDR.
|
|
9
9
|
5. Write `oprim/decisions/PDR-NNN-<slug>.md`. If superseding, update old PDR Status.
|
|
10
|
-
6. Report what was created.
|
|
10
|
+
6. Report what was created.
|
|
11
|
+
7. Run `node oprim/scripts/generate-decisions-view.js` from the project root to update `oprim/decisions-view.md`.
|
|
@@ -65,3 +65,6 @@ Proposed
|
|
|
65
65
|
Read the superseded file → replace Status value with `Superseded by PDR-NNN` → write back.
|
|
66
66
|
|
|
67
67
|
### 7. Report what was created
|
|
68
|
+
|
|
69
|
+
### 8. Regenerate the decisions rollup view
|
|
70
|
+
Run `node oprim/scripts/generate-decisions-view.js` from the project root to update `oprim/decisions-view.md`.
|