@open-product-primer/cli 0.4.0 → 0.5.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/lib/detect.js +4 -0
- package/dist/lib/install-agent.d.ts +4 -1
- package/dist/lib/install-agent.js +107 -3
- package/package.json +1 -1
package/dist/lib/detect.js
CHANGED
|
@@ -68,6 +68,10 @@ function detectAvailableAgents(projectRoot) {
|
|
|
68
68
|
detected.push('claude');
|
|
69
69
|
if (fs.existsSync(path.join(projectRoot, '.cursor')))
|
|
70
70
|
detected.push('cursor');
|
|
71
|
+
if (fs.existsSync(path.join(projectRoot, 'AGENTS.md')))
|
|
72
|
+
detected.push('codex');
|
|
73
|
+
if (fs.existsSync(path.join(projectRoot, 'GEMINI.md')))
|
|
74
|
+
detected.push('gemini');
|
|
71
75
|
return detected;
|
|
72
76
|
}
|
|
73
77
|
function writeAgentsToConfig(agents, projectRoot) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type Agent = 'claude' | 'cursor';
|
|
1
|
+
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini';
|
|
2
2
|
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
4
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
@@ -7,3 +7,6 @@ export declare const CLAUDE_SKILLS: Record<string, string>;
|
|
|
7
7
|
export declare const CLAUDE_COMMANDS: Record<string, string>;
|
|
8
8
|
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
9
9
|
export declare const CURSOR_COMMANDS: Record<string, string>;
|
|
10
|
+
export declare function writeAgentInstructionFile(filePath: string, section: string): void;
|
|
11
|
+
export declare function codexInstructions(): string;
|
|
12
|
+
export declare function geminiInstructions(): string;
|
|
@@ -40,12 +40,15 @@ exports.CURSOR_COMMANDS = exports.CURSOR_SKILLS = exports.CLAUDE_COMMANDS = expo
|
|
|
40
40
|
exports.promptFrameworkSelection = promptFrameworkSelection;
|
|
41
41
|
exports.promptAgentSelection = promptAgentSelection;
|
|
42
42
|
exports.installAgentSkills = installAgentSkills;
|
|
43
|
+
exports.writeAgentInstructionFile = writeAgentInstructionFile;
|
|
44
|
+
exports.codexInstructions = codexInstructions;
|
|
45
|
+
exports.geminiInstructions = geminiInstructions;
|
|
43
46
|
const path = __importStar(require("path"));
|
|
44
47
|
const fs = __importStar(require("fs"));
|
|
45
48
|
const chalk_1 = __importDefault(require("chalk"));
|
|
46
49
|
const scaffold_1 = require("./scaffold");
|
|
47
50
|
const detect_1 = require("./detect");
|
|
48
|
-
exports.SUPPORTED_AGENTS = ['claude', 'cursor'];
|
|
51
|
+
exports.SUPPORTED_AGENTS = ['claude', 'cursor', 'codex', 'gemini'];
|
|
49
52
|
async function promptFrameworkSelection(projectRoot) {
|
|
50
53
|
const configPath = path.join(projectRoot, '.claude', 'hooks', 'config.json');
|
|
51
54
|
if (fs.existsSync(configPath)) {
|
|
@@ -81,6 +84,8 @@ async function promptAgentSelection(projectRoot) {
|
|
|
81
84
|
choices: [
|
|
82
85
|
{ name: 'Claude Code', value: 'claude', checked: detected.includes('claude') },
|
|
83
86
|
{ name: 'Cursor', value: 'cursor', checked: detected.includes('cursor') },
|
|
87
|
+
{ name: 'Codex', value: 'codex', checked: detected.includes('codex') },
|
|
88
|
+
{ name: 'Gemini CLI', value: 'gemini', checked: detected.includes('gemini') },
|
|
84
89
|
],
|
|
85
90
|
});
|
|
86
91
|
}
|
|
@@ -131,6 +136,16 @@ function installAgentSkills(agent, projectRoot, framework = 'openspec') {
|
|
|
131
136
|
console.log(chalk_1.default.dim(' .claude/ created — Claude Code will discover these files automatically.'));
|
|
132
137
|
}
|
|
133
138
|
}
|
|
139
|
+
else if (agent === 'codex') {
|
|
140
|
+
const agentsFile = path.join(projectRoot, 'AGENTS.md');
|
|
141
|
+
writeAgentInstructionFile(agentsFile, codexInstructions());
|
|
142
|
+
console.log(chalk_1.default.green('✓') + ' AGENTS.md (oprim section written)');
|
|
143
|
+
}
|
|
144
|
+
else if (agent === 'gemini') {
|
|
145
|
+
const geminiFile = path.join(projectRoot, 'GEMINI.md');
|
|
146
|
+
writeAgentInstructionFile(geminiFile, geminiInstructions());
|
|
147
|
+
console.log(chalk_1.default.green('✓') + ' GEMINI.md (oprim section written)');
|
|
148
|
+
}
|
|
134
149
|
else if (agent === 'cursor') {
|
|
135
150
|
const cursorDir = path.join(projectRoot, '.cursor');
|
|
136
151
|
const dirCreated = !fs.existsSync(cursorDir);
|
|
@@ -290,7 +305,7 @@ After receiving the title, validate: if fewer than 4 words OR fewer than 25 char
|
|
|
290
305
|
- If "y": proceed with the original title
|
|
291
306
|
|
|
292
307
|
### 2. Assign the next BET ID
|
|
293
|
-
Scan \`oprim/bets/\` for directories matching \`BET-(\\d+)
|
|
308
|
+
Scan \`oprim/bets/\` for directories matching \`BET-(\\d+)\`. Extract all integers. Assign max+1, zero-padded to 3 digits. Default \`001\` if none.
|
|
294
309
|
|
|
295
310
|
### 3. Check sequence.yaml exists
|
|
296
311
|
If \`oprim/sequence.yaml\` not found: report and stop — advise \`oprim init\`.
|
|
@@ -541,7 +556,7 @@ function pdrInlineContent() {
|
|
|
541
556
|
return `Create a new PDR in \`oprim/decisions/\`. Scan for \`PDR-(\\d+)-\` to assign next ID (zero-padded, default 001). 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.`;
|
|
542
557
|
}
|
|
543
558
|
function betInlineContent() {
|
|
544
|
-
return `Create a new bet in \`oprim/bets/\`. Before asking for the title, show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan \`BET-(\\d+)
|
|
559
|
+
return `Create a new bet in \`oprim/bets/\`. Before asking for the title, show: "Naming tip: verb + object [for context] — Good: 'Improve bet naming for scannability' / Bad: 'Naming'". Scan \`BET-(\\d+)\` dirs for next ID (zero-padded, default 001). Check \`oprim/sequence.yaml\` exists (stop if not — advise oprim init). After receiving the title, validate: if fewer than 4 words OR fewer than 25 characters, warn "this title may be too vague", suggest a reformulation, and ask "Proceed anyway? (y/N)" — if "n", prompt for a revised title. Gather: decision (default Build now), owner, review date, why-now, alternatives, expected outcomes, kill criteria, PDR links. Write \`oprim/bets/BET-NNN/bet-decision.md\` with an inline naming tip comment in the header. Append entry to sequence.yaml backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`. Then ask: "Do you want to scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\` from the discovery template (sections: Problem Framing, User Research Signals, Competitive Context, Open Questions); if "n" or Enter, skip silently. Report what was created.`;
|
|
545
560
|
}
|
|
546
561
|
function criteriaInlineContent() {
|
|
547
562
|
return `Add metrics to \`oprim/bets/BET-NNN/criteria.yaml\`. Verify bet dir exists. Gather: metric ID, name, baseline, target, timeframe, launch date, segment. Ask source type (amplitude or bigquery). Amplitude: event, aggregation, denominator_event. BigQuery: table, metric_column, filter, aggregation, denominator_query. If file exists: append to metrics list (never overwrite). If not: create. Ask if adding more metrics. Report what was created.`;
|
|
@@ -707,3 +722,92 @@ Validate the primer sequencing board and suggest rebalancing if needed.
|
|
|
707
722
|
6. **Suggest moves** — recommend bets to defer to \`next\` or \`later\` to resolve violations
|
|
708
723
|
`;
|
|
709
724
|
}
|
|
725
|
+
// ─── Instruction-file helpers (Codex / Gemini CLI) ────────────────────────────
|
|
726
|
+
const OPRIM_START = '<!-- oprim:start -->';
|
|
727
|
+
const OPRIM_END = '<!-- oprim:end -->';
|
|
728
|
+
function writeAgentInstructionFile(filePath, section) {
|
|
729
|
+
const delimited = `${OPRIM_START}\n${section}\n${OPRIM_END}`;
|
|
730
|
+
if (!fs.existsSync(filePath)) {
|
|
731
|
+
fs.writeFileSync(filePath, delimited + '\n', 'utf-8');
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const existing = fs.readFileSync(filePath, 'utf-8');
|
|
735
|
+
const startIdx = existing.indexOf(OPRIM_START);
|
|
736
|
+
const endIdx = existing.indexOf(OPRIM_END);
|
|
737
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
738
|
+
const before = existing.slice(0, startIdx);
|
|
739
|
+
const after = existing.slice(endIdx + OPRIM_END.length);
|
|
740
|
+
fs.writeFileSync(filePath, before + delimited + after, 'utf-8');
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
744
|
+
fs.writeFileSync(filePath, existing + separator + delimited + '\n', 'utf-8');
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function oprimWorkflowsInline() {
|
|
748
|
+
return `
|
|
749
|
+
## oprim workflows
|
|
750
|
+
|
|
751
|
+
### Bet authoring (oprim-bet)
|
|
752
|
+
Create a new bet in \`oprim/bets/\` and register it on the sequencing board.
|
|
753
|
+
|
|
754
|
+
1. Show naming tip: "verb + object [for context] — e.g. 'Improve bet naming for scannability'"
|
|
755
|
+
2. Ask for the bet title. Validate: fewer than 4 words OR fewer than 25 chars → warn, suggest reformulation, ask "Proceed anyway? (y/N)".
|
|
756
|
+
3. Assign next BET ID: scan \`oprim/bets/BET-(\\d+)\` dirs, max+1 zero-padded to 3 digits (default 001).
|
|
757
|
+
4. Check \`oprim/sequence.yaml\` exists — stop if not, advise \`oprim init\`.
|
|
758
|
+
5. Gather: decision (default Build now), owner, review date (YYYY-MM-DD), why now, alternatives, expected outcomes, kill criteria, PDR links.
|
|
759
|
+
6. Write \`oprim/bets/BET-NNN/bet-decision.md\` with all fields.
|
|
760
|
+
7. Append to \`oprim/sequence.yaml\` backlog: \`{id, title, blocked_by: [], unlocks: [], requires_pdrs: []}\`.
|
|
761
|
+
8. Ask: "Scaffold a discovery.md now? (y/N)" — if "y", write \`oprim/bets/BET-NNN/discovery.md\`.
|
|
762
|
+
9. Report what was created.
|
|
763
|
+
|
|
764
|
+
### Criteria authoring (oprim-criteria)
|
|
765
|
+
Create or append to \`oprim/bets/BET-NNN/criteria.yaml\`.
|
|
766
|
+
|
|
767
|
+
1. Ask which bet (e.g. BET-042). Verify dir exists.
|
|
768
|
+
2. Gather: metric ID (snake_case), name, baseline, target, timeframe, launch date, segment.
|
|
769
|
+
3. Ask source type (amplitude / bigquery).
|
|
770
|
+
- Amplitude: event, aggregation (unique_users/event_count/property_sum), denominator_event.
|
|
771
|
+
- BigQuery: table, metric_column, filter, aggregation, denominator_query.
|
|
772
|
+
4. If file exists: append to \`metrics\` list (never overwrite). If not: create.
|
|
773
|
+
5. Ask if adding more metrics.
|
|
774
|
+
6. Report what was created.
|
|
775
|
+
|
|
776
|
+
### PDR authoring (oprim-pdr)
|
|
777
|
+
Create a new Product Decision Record in \`oprim/decisions/\`.
|
|
778
|
+
|
|
779
|
+
1. Ask for decision title.
|
|
780
|
+
2. Assign next PDR ID: scan \`oprim/decisions/PDR-(\\d+)-\`, max+1 zero-padded to 3 digits (default 001).
|
|
781
|
+
3. Gather: context, decision, alternatives, consequences, evidence, related bets/specs.
|
|
782
|
+
4. Ask if superseding an existing PDR.
|
|
783
|
+
5. Write \`oprim/decisions/PDR-NNN-<slug>.md\`. If superseding, update old PDR Status.
|
|
784
|
+
6. Report what was created.
|
|
785
|
+
|
|
786
|
+
### KPI review (oprim-review)
|
|
787
|
+
Create a KPI review artifact in \`oprim/reviews/\`.
|
|
788
|
+
|
|
789
|
+
1. Ask which bet (e.g. BET-042).
|
|
790
|
+
2. Read \`oprim/bets/BET-NNN/criteria.yaml\` for pre-fill. Check \`oprim/bets/BET-NNN/measurements/\` for \`run-*.yaml\` — use most recent if present.
|
|
791
|
+
3. If no run result, ask for each metric's actual value.
|
|
792
|
+
4. Status: actual >= target → hit; actual < target → missed; not provided → pending.
|
|
793
|
+
5. Ask reviewer name and decision quality notes.
|
|
794
|
+
6. Write \`oprim/reviews/YYYY-MM-DD-BET-NNN-kpi.md\`.
|
|
795
|
+
7. Report what was created.
|
|
796
|
+
|
|
797
|
+
### Bet archiving (oprim-archive)
|
|
798
|
+
Archive a completed bet.
|
|
799
|
+
|
|
800
|
+
1. Ask for bet ID (accept bet-005, 005, 5, BET-005 — normalize to BET-NNN).
|
|
801
|
+
2. Verify \`oprim/bets/BET-NNN/\` exists.
|
|
802
|
+
3. Check \`oprim/sequence.yaml\` for entries where \`blocked_by\` or \`unlocks\` reference the target bet — warn if found, ask "Archive anyway? (y/N)".
|
|
803
|
+
4. Move directory: \`oprim/bets/BET-NNN → oprim/bets/archived/BET-NNN\`.
|
|
804
|
+
5. Remove the bet entry from \`oprim/sequence.yaml\`.
|
|
805
|
+
6. Report what was done.
|
|
806
|
+
`;
|
|
807
|
+
}
|
|
808
|
+
function codexInstructions() {
|
|
809
|
+
return oprimWorkflowsInline();
|
|
810
|
+
}
|
|
811
|
+
function geminiInstructions() {
|
|
812
|
+
return oprimWorkflowsInline();
|
|
813
|
+
}
|