@open-product-primer/cli 2.8.0 → 2.9.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/dist/cli.js CHANGED
@@ -13,6 +13,9 @@ const migrate_1 = require("./commands/migrate");
13
13
  const measure_1 = require("./commands/measure");
14
14
  const ovw_1 = require("./commands/ovw");
15
15
  const context_1 = require("./commands/context");
16
+ const list_1 = require("./commands/list");
17
+ const show_1 = require("./commands/show");
18
+ const status_1 = require("./commands/status");
16
19
  const package_json_1 = __importDefault(require("../package.json"));
17
20
  const program = new commander_1.Command();
18
21
  program
@@ -27,4 +30,7 @@ program.addCommand((0, migrate_1.migrateCommand)());
27
30
  program.addCommand((0, measure_1.measureCommand)());
28
31
  program.addCommand((0, ovw_1.ovwCommand)());
29
32
  program.addCommand((0, context_1.contextCommand)());
33
+ program.addCommand((0, list_1.listCommand)());
34
+ program.addCommand((0, show_1.showCommand)());
35
+ program.addCommand((0, status_1.statusCommand)());
30
36
  program.parse();
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function listCommand(): Command;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listCommand = listCommand;
7
+ const commander_1 = require("commander");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const artifacts_1 = require("../lib/artifacts");
10
+ function printTable(label, rows, columns) {
11
+ console.log(chalk_1.default.bold.cyan(`\n── ${label} ──`));
12
+ if (rows.length === 0) {
13
+ console.log(' (none)');
14
+ return;
15
+ }
16
+ for (const row of rows) {
17
+ console.log(` ${columns(row).join(' ')}`);
18
+ }
19
+ }
20
+ function listCommand() {
21
+ return new commander_1.Command('list')
22
+ .description('Enumerate oprim artifacts (bets, decisions, notes) by type')
23
+ .option('-b, --bets', 'list bets')
24
+ .option('-d, --decisions', 'list decisions (PDRs)')
25
+ .option('-n, --notes', 'list notes')
26
+ .option('--json', 'print machine-readable JSON instead of the human-readable table')
27
+ .action((opts) => {
28
+ const projectRoot = process.cwd();
29
+ const anyTypeRequested = !!(opts.bets || opts.decisions || opts.notes);
30
+ const showBets = anyTypeRequested ? !!opts.bets : true;
31
+ const showDecisions = !!opts.decisions;
32
+ const showNotes = !!opts.notes;
33
+ const bets = showBets ? (0, artifacts_1.listBets)(projectRoot) : undefined;
34
+ const decisions = showDecisions ? (0, artifacts_1.listDecisions)(projectRoot) : undefined;
35
+ const notes = showNotes ? (0, artifacts_1.listNotes)(projectRoot) : undefined;
36
+ if (opts.json) {
37
+ const result = {};
38
+ if (bets)
39
+ result.bets = bets;
40
+ if (decisions)
41
+ result.decisions = decisions;
42
+ if (notes)
43
+ result.notes = notes;
44
+ console.log(JSON.stringify(result));
45
+ return;
46
+ }
47
+ if (bets)
48
+ printTable('BETS', bets, (b) => [b.id, b.status ?? '-', b.title]);
49
+ if (decisions)
50
+ printTable('DECISIONS', decisions, (d) => [d.id, d.status ?? '-', d.title]);
51
+ if (notes)
52
+ printTable('NOTES', notes, (n) => [n.id, n.tags.join(',') || '-', n.title]);
53
+ });
54
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function showCommand(): Command;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.showCommand = showCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const spec_delta_1 = require("../lib/spec-delta");
45
+ const artifacts_1 = require("../lib/artifacts");
46
+ function extractLinks(content) {
47
+ const match = content.match(/^##\s*Links\s*\n([\s\S]*?)(?=\n##\s|$)/m);
48
+ return match ? match[1].trim() : null;
49
+ }
50
+ function resolveBet(projectRoot, idInput) {
51
+ for (const sub of ['pending', 'archived']) {
52
+ const betsDir = path.join(projectRoot, 'oprim', 'bets', sub);
53
+ const resolved = (0, spec_delta_1.resolveBetDirectory)(betsDir, idInput);
54
+ if (!resolved)
55
+ continue;
56
+ const decisionPath = path.join(betsDir, resolved, 'bet-decision.md');
57
+ if (!fs.existsSync(decisionPath))
58
+ continue;
59
+ const content = fs.readFileSync(decisionPath, 'utf-8');
60
+ const { title, status } = (0, artifacts_1.parseBetDecision)(content);
61
+ return {
62
+ type: 'bet',
63
+ id: resolved.match(/^BET-\d+/)?.[0] ?? resolved,
64
+ path: path.relative(projectRoot, decisionPath),
65
+ fields: { title, status, links: extractLinks(content) },
66
+ content,
67
+ };
68
+ }
69
+ return null;
70
+ }
71
+ function resolveDecision(projectRoot, idInput) {
72
+ const filePath = (0, artifacts_1.resolvePdrPath)(projectRoot, idInput);
73
+ if (!filePath)
74
+ return null;
75
+ const content = fs.readFileSync(filePath, 'utf-8');
76
+ const { id, title, status } = (0, artifacts_1.parsePdrFile)(content);
77
+ if (!id)
78
+ return null;
79
+ return {
80
+ type: 'decision',
81
+ id,
82
+ path: path.relative(projectRoot, filePath),
83
+ fields: { title, status },
84
+ content,
85
+ };
86
+ }
87
+ function resolveNote(projectRoot, idInput) {
88
+ const filePath = (0, artifacts_1.resolveNotePath)(projectRoot, idInput);
89
+ if (!filePath)
90
+ return null;
91
+ const content = fs.readFileSync(filePath, 'utf-8');
92
+ const { title, tags } = (0, artifacts_1.parseNoteFrontmatter)(content);
93
+ const id = path.basename(filePath).match(/^(NOTE-\d+)/)?.[1] ?? idInput;
94
+ return {
95
+ type: 'note',
96
+ id,
97
+ path: path.relative(projectRoot, filePath),
98
+ fields: { title, tags },
99
+ content,
100
+ };
101
+ }
102
+ function resolveArtifact(projectRoot, idInput) {
103
+ const normalized = idInput.trim().toUpperCase();
104
+ if (normalized.startsWith('BET'))
105
+ return resolveBet(projectRoot, idInput);
106
+ if (normalized.startsWith('PDR'))
107
+ return resolveDecision(projectRoot, idInput);
108
+ if (normalized.startsWith('NOTE'))
109
+ return resolveNote(projectRoot, idInput);
110
+ return null;
111
+ }
112
+ function showCommand() {
113
+ return new commander_1.Command('show')
114
+ .description('Show a single oprim artifact (bet, decision, or note) resolved by ID')
115
+ .argument('<id>', 'artifact ID, e.g. BET-030, PDR-005, or NOTE-012')
116
+ .option('--json', 'print machine-readable JSON instead of the human-readable view')
117
+ .action((idInput, opts) => {
118
+ const projectRoot = process.cwd();
119
+ const result = resolveArtifact(projectRoot, idInput);
120
+ if (!result) {
121
+ console.error(`${idInput} was not found — checked bets, decisions, and notes.`);
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ if (opts.json) {
126
+ console.log(JSON.stringify({ type: result.type, id: result.id, path: result.path, ...result.fields, content: result.content }));
127
+ return;
128
+ }
129
+ console.log(chalk_1.default.bold(`${result.id}`) + chalk_1.default.dim(` (${result.type}) ${result.path}`));
130
+ for (const [key, value] of Object.entries(result.fields)) {
131
+ if (value === null || value === undefined || value === '')
132
+ continue;
133
+ console.log(chalk_1.default.dim(`${key}:`) + ` ${Array.isArray(value) ? value.join(', ') : value}`);
134
+ }
135
+ console.log('');
136
+ console.log(result.content);
137
+ });
138
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function statusCommand(): Command;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.statusCommand = statusCommand;
40
+ const commander_1 = require("commander");
41
+ const path = __importStar(require("path"));
42
+ const fs = __importStar(require("fs"));
43
+ const chalk_1 = __importDefault(require("chalk"));
44
+ const yaml = __importStar(require("js-yaml"));
45
+ function statusCommand() {
46
+ return new commander_1.Command('status')
47
+ .description('Show sequencing board state (now/next/later/backlog) and WIP limit usage')
48
+ .option('--json', 'print machine-readable JSON instead of the human-readable report')
49
+ .action((opts) => {
50
+ const projectRoot = process.cwd();
51
+ const sequencePath = path.join(projectRoot, 'oprim', 'sequence.yaml');
52
+ if (!fs.existsSync(sequencePath)) {
53
+ console.error("No oprim/sequence.yaml found — run 'oprim init' first");
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ let board;
58
+ try {
59
+ board = yaml.load(fs.readFileSync(sequencePath, 'utf-8')) ?? {};
60
+ }
61
+ catch {
62
+ console.error('Failed to parse oprim/sequence.yaml');
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const now = board.now ?? [];
67
+ const next = board.next ?? [];
68
+ const later = board.later ?? [];
69
+ const backlog = board.backlog ?? [];
70
+ if (opts.json) {
71
+ console.log(JSON.stringify({ wip_limits: board.wip_limits ?? {}, now, next, later, backlog }));
72
+ return;
73
+ }
74
+ console.log(chalk_1.default.bold('oprim status') + '\n');
75
+ const wipLimit = board.wip_limits?.now;
76
+ const wipLine = wipLimit !== undefined ? `${now.length}/${wipLimit}` : `${now.length}`;
77
+ console.log(chalk_1.default.dim(`WIP (now): ${wipLine}`));
78
+ const lanes = [
79
+ ['now', now],
80
+ ['next', next],
81
+ ['later', later],
82
+ ['backlog', backlog],
83
+ ];
84
+ for (const [label, bets] of lanes) {
85
+ console.log(chalk_1.default.bold.cyan(`\n── ${label.toUpperCase()} ──`));
86
+ if (bets.length === 0) {
87
+ console.log(' (empty)');
88
+ continue;
89
+ }
90
+ for (const bet of bets) {
91
+ console.log(` ${bet.id} ${bet.title}`);
92
+ }
93
+ }
94
+ });
95
+ }
@@ -0,0 +1,44 @@
1
+ export interface BetSummary {
2
+ id: string;
3
+ title: string;
4
+ status: string | null;
5
+ path: string;
6
+ }
7
+ export interface DecisionSummary {
8
+ id: string;
9
+ title: string;
10
+ status: string | null;
11
+ path: string;
12
+ }
13
+ export interface NoteSummary {
14
+ id: string;
15
+ title: string;
16
+ tags: string[];
17
+ path: string;
18
+ }
19
+ /** Parses a bet-decision.md body for its title and `- Decision: <status>` line. */
20
+ export declare function parseBetDecision(content: string): {
21
+ title: string;
22
+ status: string | null;
23
+ };
24
+ /** Enumerates bets under oprim/bets/pending/ and oprim/bets/archived/. */
25
+ export declare function listBets(projectRoot: string): BetSummary[];
26
+ /** Parses a PDR file for its id, title, and status (ported from decisionsViewScriptTemplate). */
27
+ export declare function parsePdrFile(content: string): {
28
+ id: string | null;
29
+ title: string;
30
+ status: string | null;
31
+ };
32
+ /** Enumerates decisions (PDRs) under oprim/decisions/. */
33
+ export declare function listDecisions(projectRoot: string): DecisionSummary[];
34
+ /** Parses a note's frontmatter (title, tags) — falls back to empty values if frontmatter is absent/unparsable. */
35
+ export declare function parseNoteFrontmatter(content: string): {
36
+ title: string;
37
+ tags: string[];
38
+ };
39
+ /** Enumerates notes under oprim/notes/. */
40
+ export declare function listNotes(projectRoot: string): NoteSummary[];
41
+ /** Resolves a PDR ID (accepting `pdr-005`, `005`, `5`, or `PDR-005`) to its file path under oprim/decisions/. */
42
+ export declare function resolvePdrPath(projectRoot: string, idInput: string): string | null;
43
+ /** Resolves a note ID (accepting `note-005`, `005`, `5`, or `NOTE-005`) to its file path under oprim/notes/. */
44
+ export declare function resolveNotePath(projectRoot: string, idInput: string): string | null;
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.parseBetDecision = parseBetDecision;
37
+ exports.listBets = listBets;
38
+ exports.parsePdrFile = parsePdrFile;
39
+ exports.listDecisions = listDecisions;
40
+ exports.parseNoteFrontmatter = parseNoteFrontmatter;
41
+ exports.listNotes = listNotes;
42
+ exports.resolvePdrPath = resolvePdrPath;
43
+ exports.resolveNotePath = resolveNotePath;
44
+ const path = __importStar(require("path"));
45
+ const fs = __importStar(require("fs"));
46
+ const yaml = __importStar(require("js-yaml"));
47
+ function extractBetId(dirName) {
48
+ return dirName.match(/^BET-\d+/)?.[0] ?? dirName;
49
+ }
50
+ /** Parses a bet-decision.md body for its title and `- Decision: <status>` line. */
51
+ function parseBetDecision(content) {
52
+ const titleMatch = content.match(/^#\s*Decision:\s*BET-\d+\s+(.*)$/m);
53
+ const statusMatch = content.match(/^-\s*Decision:\s*(.+)$/m);
54
+ return {
55
+ title: titleMatch ? titleMatch[1].trim() : '',
56
+ status: statusMatch ? statusMatch[1].trim() : null,
57
+ };
58
+ }
59
+ /** Enumerates bets under oprim/bets/pending/ and oprim/bets/archived/. */
60
+ function listBets(projectRoot) {
61
+ const results = [];
62
+ for (const sub of ['pending', 'archived']) {
63
+ const dir = path.join(projectRoot, 'oprim', 'bets', sub);
64
+ if (!fs.existsSync(dir))
65
+ continue;
66
+ const entries = fs.readdirSync(dir, { withFileTypes: true }).filter((e) => e.isDirectory());
67
+ for (const entry of entries) {
68
+ const decisionPath = path.join(dir, entry.name, 'bet-decision.md');
69
+ if (!fs.existsSync(decisionPath))
70
+ continue;
71
+ const content = fs.readFileSync(decisionPath, 'utf-8');
72
+ const { title, status } = parseBetDecision(content);
73
+ results.push({
74
+ id: extractBetId(entry.name),
75
+ title,
76
+ status,
77
+ path: path.relative(projectRoot, decisionPath),
78
+ });
79
+ }
80
+ }
81
+ return results;
82
+ }
83
+ /** Parses a PDR file for its id, title, and status (ported from decisionsViewScriptTemplate). */
84
+ function parsePdrFile(content) {
85
+ const idMatch = content.match(/^#\s*(PDR-\d+)[:\s]*(.*)$/m);
86
+ const id = idMatch ? idMatch[1] : null;
87
+ const title = idMatch ? idMatch[2].trim() : '';
88
+ const statusMatch = content.match(/^##\s*Status\s*\n(.+)$/m) ?? content.match(/^Status:\s*(.+)$/m);
89
+ const status = statusMatch ? statusMatch[1].trim() : null;
90
+ return { id, title, status };
91
+ }
92
+ /** Enumerates decisions (PDRs) under oprim/decisions/. */
93
+ function listDecisions(projectRoot) {
94
+ const dir = path.join(projectRoot, 'oprim', 'decisions');
95
+ if (!fs.existsSync(dir))
96
+ return [];
97
+ const results = [];
98
+ const files = fs.readdirSync(dir).filter((f) => /^PDR-\d+.*\.md$/.test(f));
99
+ for (const file of files) {
100
+ const filePath = path.join(dir, file);
101
+ const content = fs.readFileSync(filePath, 'utf-8');
102
+ const { id, title, status } = parsePdrFile(content);
103
+ if (!id)
104
+ continue;
105
+ results.push({ id, title, status, path: path.relative(projectRoot, filePath) });
106
+ }
107
+ return results;
108
+ }
109
+ /** Parses a note's frontmatter (title, tags) — falls back to empty values if frontmatter is absent/unparsable. */
110
+ function parseNoteFrontmatter(content) {
111
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
112
+ if (!fmMatch)
113
+ return { title: '', tags: [] };
114
+ try {
115
+ const fm = (yaml.load(fmMatch[1]) ?? {});
116
+ return { title: fm.title ?? '', tags: fm.tags ?? [] };
117
+ }
118
+ catch {
119
+ return { title: '', tags: [] };
120
+ }
121
+ }
122
+ /** Enumerates notes under oprim/notes/. */
123
+ function listNotes(projectRoot) {
124
+ const dir = path.join(projectRoot, 'oprim', 'notes');
125
+ if (!fs.existsSync(dir))
126
+ return [];
127
+ const results = [];
128
+ const files = fs.readdirSync(dir).filter((f) => /^NOTE-\d+.*\.md$/.test(f));
129
+ for (const file of files) {
130
+ const idMatch = file.match(/^(NOTE-\d+)/);
131
+ if (!idMatch)
132
+ continue;
133
+ const filePath = path.join(dir, file);
134
+ const content = fs.readFileSync(filePath, 'utf-8');
135
+ const { title, tags } = parseNoteFrontmatter(content);
136
+ results.push({ id: idMatch[1], title, tags, path: path.relative(projectRoot, filePath) });
137
+ }
138
+ return results;
139
+ }
140
+ function normalizeArtifactId(prefix, input) {
141
+ const digits = input.replace(/[^0-9]/g, '');
142
+ return `${prefix}-${digits.padStart(3, '0')}`;
143
+ }
144
+ function resolveByPrefix(dir, id) {
145
+ if (!fs.existsSync(dir))
146
+ return null;
147
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
148
+ const exact = files.find((f) => f === `${id}.md`);
149
+ if (exact)
150
+ return path.join(dir, exact);
151
+ const slugMatches = files.filter((f) => f.startsWith(`${id}-`));
152
+ if (slugMatches.length === 1)
153
+ return path.join(dir, slugMatches[0]);
154
+ return null;
155
+ }
156
+ /** Resolves a PDR ID (accepting `pdr-005`, `005`, `5`, or `PDR-005`) to its file path under oprim/decisions/. */
157
+ function resolvePdrPath(projectRoot, idInput) {
158
+ return resolveByPrefix(path.join(projectRoot, 'oprim', 'decisions'), normalizeArtifactId('PDR', idInput));
159
+ }
160
+ /** Resolves a note ID (accepting `note-005`, `005`, `5`, or `NOTE-005`) to its file path under oprim/notes/. */
161
+ function resolveNotePath(projectRoot, idInput) {
162
+ return resolveByPrefix(path.join(projectRoot, 'oprim', 'notes'), normalizeArtifactId('NOTE', idInput));
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-product-primer/cli",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Open Product Primer CLI — product decisions, sequencing, and KPI tracking for repositories",
5
5
  "keywords": [
6
6
  "product",