@open-product-primer/cli 1.2.0 → 2.1.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/README.md +6 -0
- package/dist/cli.js +2 -0
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/context.js +188 -0
- package/dist/commands/doctor.js +49 -0
- package/dist/commands/init.js +2 -3
- package/dist/commands/update.js +29 -2
- package/dist/lib/config-merge.d.ts +10 -0
- package/dist/lib/config-merge.js +57 -0
- package/dist/lib/install-agent.d.ts +2 -0
- package/dist/lib/install-agent.js +293 -44
- package/dist/lib/integrity.js +2 -1
- package/dist/lib/remote-context.d.ts +50 -0
- package/dist/lib/remote-context.js +299 -0
- package/dist/lib/templates.d.ts +1 -1
- package/dist/lib/templates.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,5 +27,11 @@ Bin aliases: `open-product-primer`, `oprim`.
|
|
|
27
27
|
| `oprim update` | Refresh `/oprim:*` assistant commands and skills |
|
|
28
28
|
| `oprim doctor` | Verify scaffold, integrations, and measurement env |
|
|
29
29
|
| `oprim measure` | Run KPI measurement pipeline for a bet |
|
|
30
|
+
| `oprim context` | Print resolved remote context content (`--source <name>` to scope to one) |
|
|
31
|
+
| `oprim context init` | Declare the current project a citable remote context (`--description`); prefer the guided `/oprim:context-init` skill in Claude Code |
|
|
32
|
+
| `oprim context register` | Register a remote context source (`--git <url>` or `--local <path>`, `--name`, optional `--description`) |
|
|
33
|
+
| `oprim context list` | List every registered source's name, kind, and description without fully resolving any of them |
|
|
34
|
+
|
|
35
|
+
A remote context is an oprim workspace bundled somewhere outside the current project's directory — a remote git repo or a local sibling directory — that this project can reference read-only. Content resolves fresh on demand (never a persistent clone you have to `git pull` yourself); `oprim context list` is the cheap way to see what's registered before pulling full content.
|
|
30
36
|
|
|
31
37
|
Full documentation: [github.com/eshraw/open-product-primer](https://github.com/eshraw/open-product-primer).
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ const doctor_1 = require("./commands/doctor");
|
|
|
11
11
|
const migrate_1 = require("./commands/migrate");
|
|
12
12
|
const measure_1 = require("./commands/measure");
|
|
13
13
|
const ovw_1 = require("./commands/ovw");
|
|
14
|
+
const context_1 = require("./commands/context");
|
|
14
15
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
15
16
|
const program = new commander_1.Command();
|
|
16
17
|
program
|
|
@@ -23,4 +24,5 @@ program.addCommand((0, doctor_1.doctorCommand)());
|
|
|
23
24
|
program.addCommand((0, migrate_1.migrateCommand)());
|
|
24
25
|
program.addCommand((0, measure_1.measureCommand)());
|
|
25
26
|
program.addCommand((0, ovw_1.ovwCommand)());
|
|
27
|
+
program.addCommand((0, context_1.contextCommand)());
|
|
26
28
|
program.parse();
|
|
@@ -0,0 +1,188 @@
|
|
|
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.contextCommand = contextCommand;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
43
|
+
const remote_context_1 = require("../lib/remote-context");
|
|
44
|
+
const scaffold_1 = require("../lib/scaffold");
|
|
45
|
+
function sourceKind(source) {
|
|
46
|
+
return (0, remote_context_1.isGitSource)(source) ? 'git' : 'path';
|
|
47
|
+
}
|
|
48
|
+
function initSubcommand() {
|
|
49
|
+
return new commander_1.Command('init')
|
|
50
|
+
.description('Declare the current project as a citable remote context')
|
|
51
|
+
.option('--description <text>', 'canonical description of what this context contains')
|
|
52
|
+
.action((opts) => {
|
|
53
|
+
const projectRoot = process.cwd();
|
|
54
|
+
if ((0, scaffold_1.fileExists)((0, remote_context_1.identityFilePath)(projectRoot))) {
|
|
55
|
+
console.log(chalk_1.default.yellow('A remote context identity already exists at ') + chalk_1.default.cyan('.oprim-context/context.yaml'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const name = path.basename(projectRoot);
|
|
59
|
+
(0, remote_context_1.writeIdentity)(projectRoot, { name, version: '1', description: opts.description });
|
|
60
|
+
console.log(chalk_1.default.green('✓') + ' .oprim-context/context.yaml created');
|
|
61
|
+
console.log(` name: ${name}`);
|
|
62
|
+
if (opts.description) {
|
|
63
|
+
console.log(` description: ${opts.description}`);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(chalk_1.default.yellow(' no description set') +
|
|
67
|
+
' — other projects referencing this context via ' +
|
|
68
|
+
chalk_1.default.cyan('oprim context list') +
|
|
69
|
+
' will see it as description-less.');
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function registerSubcommand() {
|
|
74
|
+
return new commander_1.Command('register')
|
|
75
|
+
.description('Register a remote context source in the current project')
|
|
76
|
+
.option('--git <url>', 'git remote URL of the remote context')
|
|
77
|
+
.option('--local <path>', 'local filesystem path of the remote context')
|
|
78
|
+
.requiredOption('--name <name>', 'name to register this source under')
|
|
79
|
+
.option('--description <text>', 'local note about why this source was registered')
|
|
80
|
+
.action((opts) => {
|
|
81
|
+
const projectRoot = process.cwd();
|
|
82
|
+
if ((opts.git && opts.local) || (!opts.git && !opts.local)) {
|
|
83
|
+
console.error(chalk_1.default.red('Exactly one of --git or --local is required.'));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
|
|
87
|
+
if ((0, remote_context_1.findSourceByName)(config, opts.name)) {
|
|
88
|
+
console.error(chalk_1.default.red(`A source named "${opts.name}" is already registered.`));
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
const source = opts.git
|
|
92
|
+
? { name: opts.name, git: opts.git, description: opts.description }
|
|
93
|
+
: { name: opts.name, path: opts.local, description: opts.description };
|
|
94
|
+
config.enabled = true;
|
|
95
|
+
config.sources.push(source);
|
|
96
|
+
(0, remote_context_1.writeRemoteContextConfig)(projectRoot, config);
|
|
97
|
+
console.log(chalk_1.default.green('✓') + ` Registered "${opts.name}" (${sourceKind(source)})`);
|
|
98
|
+
const result = (0, remote_context_1.resolveIdentityOnly)(source);
|
|
99
|
+
if (result.error) {
|
|
100
|
+
console.log(chalk_1.default.yellow(' Could not confirm this source yet: ') +
|
|
101
|
+
result.error +
|
|
102
|
+
chalk_1.default.dim(' — try `oprim context list` or `oprim doctor` to retry later.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (result.nameMismatch) {
|
|
106
|
+
console.log(chalk_1.default.yellow(` Warning: declared name "${result.nameMismatch.declared}" does not match resolved identity name "${result.nameMismatch.resolved}".`));
|
|
107
|
+
}
|
|
108
|
+
if (result.identity?.description) {
|
|
109
|
+
console.log(` ${chalk_1.default.dim('description:')} ${result.identity.description}`);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
console.log(chalk_1.default.dim(' (no canonical description set on this source)'));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function listSubcommand() {
|
|
117
|
+
return new commander_1.Command('list')
|
|
118
|
+
.description('List every registered remote context source, without fully resolving any of them')
|
|
119
|
+
.action(() => {
|
|
120
|
+
const projectRoot = process.cwd();
|
|
121
|
+
const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
|
|
122
|
+
if (!config.enabled || config.sources.length === 0) {
|
|
123
|
+
console.log('No remote contexts configured.');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
for (const source of config.sources) {
|
|
127
|
+
const kind = sourceKind(source);
|
|
128
|
+
console.log(`${chalk_1.default.bold(source.name)} ${chalk_1.default.dim(`(${kind})`)}`);
|
|
129
|
+
const result = (0, remote_context_1.resolveIdentityOnly)(source);
|
|
130
|
+
if (result.error) {
|
|
131
|
+
console.log(' ' + chalk_1.default.red(`unresolved: ${result.error}`));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const desc = result.identity?.description;
|
|
135
|
+
console.log(' ' + (desc ? desc : chalk_1.default.dim('no description set')));
|
|
136
|
+
if (result.nameMismatch) {
|
|
137
|
+
console.log(chalk_1.default.yellow(` name mismatch: declared "${result.nameMismatch.declared}", resolved "${result.nameMismatch.resolved}"`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (source.description) {
|
|
141
|
+
console.log(' ' + chalk_1.default.dim(`local note: ${source.description}`));
|
|
142
|
+
}
|
|
143
|
+
console.log('');
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function contextCommand() {
|
|
148
|
+
const cmd = new commander_1.Command('context')
|
|
149
|
+
.description('Print resolved remote context content')
|
|
150
|
+
.option('--source <name>', 'limit output to a single named source')
|
|
151
|
+
.action((opts) => {
|
|
152
|
+
const projectRoot = process.cwd();
|
|
153
|
+
const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
|
|
154
|
+
if (!config.enabled || config.sources.length === 0) {
|
|
155
|
+
console.log('No remote contexts configured.');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
let sources = config.sources;
|
|
159
|
+
if (opts.source) {
|
|
160
|
+
const match = (0, remote_context_1.findSourceByName)(config, opts.source);
|
|
161
|
+
if (!match) {
|
|
162
|
+
console.error(chalk_1.default.red(`No source named "${opts.source}" is registered.`));
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
sources = [match];
|
|
166
|
+
}
|
|
167
|
+
for (const source of sources) {
|
|
168
|
+
console.log(chalk_1.default.bold(`═══ ${source.name} ═══`));
|
|
169
|
+
const result = (0, remote_context_1.resolveFull)(source);
|
|
170
|
+
if (result.error) {
|
|
171
|
+
console.log(chalk_1.default.red(` unresolved: ${result.error}`));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (result.stale) {
|
|
175
|
+
console.log(chalk_1.default.yellow(' (stale — last successful fetch could not be refreshed)'));
|
|
176
|
+
}
|
|
177
|
+
if (result.nameMismatch) {
|
|
178
|
+
console.log(chalk_1.default.yellow(` name mismatch: declared "${result.nameMismatch.declared}", resolved "${result.nameMismatch.resolved}"`));
|
|
179
|
+
}
|
|
180
|
+
console.log((0, remote_context_1.assembleOprimWorkspaceContent)(result.workspaceRoot));
|
|
181
|
+
console.log('');
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
cmd.addCommand(initSubcommand());
|
|
185
|
+
cmd.addCommand(registerSubcommand());
|
|
186
|
+
cmd.addCommand(listSubcommand());
|
|
187
|
+
return cmd;
|
|
188
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -44,6 +44,7 @@ const chalk_1 = __importDefault(require("chalk"));
|
|
|
44
44
|
const detect_1 = require("../lib/detect");
|
|
45
45
|
const measure_1 = require("../lib/measure");
|
|
46
46
|
const integrity_1 = require("../lib/integrity");
|
|
47
|
+
const remote_context_1 = require("../lib/remote-context");
|
|
47
48
|
const AGENT_DIRS = {
|
|
48
49
|
claude: '.claude',
|
|
49
50
|
cursor: '.cursor',
|
|
@@ -103,6 +104,52 @@ function checkClaudeHooks(projectRoot, checks) {
|
|
|
103
104
|
required: false,
|
|
104
105
|
});
|
|
105
106
|
}
|
|
107
|
+
// bet-026 — validates each configured remote_context.sources entry using the identity-only
|
|
108
|
+
// fetch (cheap: single file, not a full workspace pull), plus a separate "ever fully
|
|
109
|
+
// resolved" nudge that only applies to git sources (local paths have no persistent cache).
|
|
110
|
+
function checkRemoteContexts(projectRoot, checks) {
|
|
111
|
+
const config = (0, remote_context_1.readRemoteContextConfig)(projectRoot);
|
|
112
|
+
if (!config.enabled || config.sources.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
for (const source of config.sources) {
|
|
115
|
+
const kind = (0, remote_context_1.isGitSource)(source) ? 'git' : 'path';
|
|
116
|
+
const result = (0, remote_context_1.resolveIdentityOnly)(source);
|
|
117
|
+
if (result.error) {
|
|
118
|
+
checks.push({
|
|
119
|
+
name: `remote_context: ${source.name} (${kind})`,
|
|
120
|
+
pass: false,
|
|
121
|
+
note: `${kind === 'git' ? 'Remote unreachable' : 'Path not found'}: ${result.error} — run 'oprim context list' to retry`,
|
|
122
|
+
required: false,
|
|
123
|
+
});
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (!result.identity) {
|
|
127
|
+
checks.push({
|
|
128
|
+
name: `remote_context: ${source.name} (${kind})`,
|
|
129
|
+
pass: false,
|
|
130
|
+
note: `Missing remote context identity — ask the source project to run 'oprim context init'`,
|
|
131
|
+
required: false,
|
|
132
|
+
});
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
checks.push({
|
|
136
|
+
name: `remote_context: ${source.name} (${kind})`,
|
|
137
|
+
pass: true,
|
|
138
|
+
note: result.nameMismatch
|
|
139
|
+
? `Name mismatch: declared "${result.nameMismatch.declared}", resolved "${result.nameMismatch.resolved}"`
|
|
140
|
+
: undefined,
|
|
141
|
+
required: false,
|
|
142
|
+
});
|
|
143
|
+
if ((0, remote_context_1.isGitSource)(source) && !(0, remote_context_1.hasEverFullyResolved)(source)) {
|
|
144
|
+
checks.push({
|
|
145
|
+
name: `remote_context: ${source.name} never fully resolved`,
|
|
146
|
+
pass: false,
|
|
147
|
+
note: `Run 'oprim context --source ${source.name}' to pull its content for the first time`,
|
|
148
|
+
required: false,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
106
153
|
function doctorCommand() {
|
|
107
154
|
return new commander_1.Command('doctor')
|
|
108
155
|
.description('Check oprim install health and integration readiness')
|
|
@@ -209,6 +256,8 @@ function doctorCommand() {
|
|
|
209
256
|
(0, integrity_1.checkSequenceIntegrity)(projectRoot, checks);
|
|
210
257
|
// ── Skill version drift checks ────────────────────────────────────────────
|
|
211
258
|
(0, integrity_1.checkSkillVersionDrift)(projectRoot, checks);
|
|
259
|
+
// ── Remote context checks ─────────────────────────────────────────────────
|
|
260
|
+
checkRemoteContexts(projectRoot, checks);
|
|
212
261
|
// ── Agent environment checks ──────────────────────────────────────────────
|
|
213
262
|
const configAgents = (0, detect_1.readAgentsFromConfig)(projectRoot);
|
|
214
263
|
if (configAgents !== null) {
|
package/dist/commands/init.js
CHANGED
|
@@ -61,6 +61,7 @@ function initCommand() {
|
|
|
61
61
|
console.log(chalk_1.default.green('✓') + ' Graphify detected');
|
|
62
62
|
console.log('');
|
|
63
63
|
const okfEnabled = await (0, install_agent_1.promptOkfFrontmatter)();
|
|
64
|
+
const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
64
65
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
65
66
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'decisions'));
|
|
66
67
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'bets'));
|
|
@@ -68,7 +69,7 @@ function initCommand() {
|
|
|
68
69
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'notes'));
|
|
69
70
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'templates'));
|
|
70
71
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
71
|
-
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled));
|
|
72
|
+
const configWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'config.yaml'), (0, templates_1.configTemplate)(projectName, openspec.detected, graphify.detected, okfEnabled, specFramework));
|
|
72
73
|
const sequenceWritten = (0, scaffold_1.writeFileIfAbsent)(path.join(primerDir, 'sequence.yaml'), templates_1.sequenceTemplate);
|
|
73
74
|
const pdrContent = okfEnabled ? (0, templates_1.okfFrontmatter)('pdr', '<Decision title>') + templates_1.pdrTemplate : templates_1.pdrTemplate;
|
|
74
75
|
const betContent = okfEnabled
|
|
@@ -136,10 +137,8 @@ function initCommand() {
|
|
|
136
137
|
' after configuring an AI tool to install /oprim:* skills.');
|
|
137
138
|
}
|
|
138
139
|
else {
|
|
139
|
-
let specFramework = 'openspec';
|
|
140
140
|
let pdrSurfacing = false;
|
|
141
141
|
if (selectedAgents.includes('claude')) {
|
|
142
|
-
specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
143
142
|
pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
144
143
|
}
|
|
145
144
|
console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
|
package/dist/commands/update.js
CHANGED
|
@@ -45,6 +45,19 @@ const install_agent_1 = require("../lib/install-agent");
|
|
|
45
45
|
const detect_1 = require("../lib/detect");
|
|
46
46
|
const scaffold_1 = require("../lib/scaffold");
|
|
47
47
|
const templates_1 = require("../lib/templates");
|
|
48
|
+
const config_merge_1 = require("../lib/config-merge");
|
|
49
|
+
// bet-023 — persist the resolved spec_framework into oprim/config.yaml, inserting it only
|
|
50
|
+
// when the key is missing (never overwrites an already-persisted choice).
|
|
51
|
+
function persistSpecFramework(configPath, framework) {
|
|
52
|
+
if (!fs.existsSync(configPath))
|
|
53
|
+
return;
|
|
54
|
+
const existing = fs.readFileSync(configPath, 'utf-8');
|
|
55
|
+
const { content, changed } = (0, config_merge_1.mergeSpecFramework)(existing, framework);
|
|
56
|
+
if (changed) {
|
|
57
|
+
fs.writeFileSync(configPath, content, 'utf-8');
|
|
58
|
+
console.log(chalk_1.default.green('✓') + ` oprim/config.yaml — integrations.spec_framework set to ${framework}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
48
61
|
function updateCommand() {
|
|
49
62
|
return new commander_1.Command('update')
|
|
50
63
|
.description('Refresh /oprim:* assistant commands and skills from package templates')
|
|
@@ -58,13 +71,23 @@ function updateCommand() {
|
|
|
58
71
|
const primerDir = path.join(projectRoot, 'oprim');
|
|
59
72
|
(0, scaffold_1.ensureDir)(path.join(primerDir, 'scripts'));
|
|
60
73
|
(0, scaffold_1.writeFile)(path.join(primerDir, 'scripts', 'generate-sequence-view.js'), templates_1.sequenceViewScriptTemplate);
|
|
74
|
+
const configPath = path.join(primerDir, 'config.yaml');
|
|
75
|
+
if (fs.existsSync(configPath)) {
|
|
76
|
+
const existingConfig = fs.readFileSync(configPath, 'utf-8');
|
|
77
|
+
const { content: mergedConfig, changed } = (0, config_merge_1.mergeConfigSchema)(existingConfig);
|
|
78
|
+
if (changed) {
|
|
79
|
+
fs.writeFileSync(configPath, mergedConfig, 'utf-8');
|
|
80
|
+
console.log(chalk_1.default.green('✓') + ' oprim/config.yaml — schema updated with new keys (existing values preserved)');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
61
83
|
if (configAgents !== null && configAgents.length > 0) {
|
|
62
|
-
let specFramework =
|
|
84
|
+
let specFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
|
|
63
85
|
let pdrSurfacing = false;
|
|
64
86
|
if (configAgents.includes('claude')) {
|
|
65
87
|
specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
66
88
|
pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
67
89
|
}
|
|
90
|
+
persistSpecFramework(configPath, specFramework);
|
|
68
91
|
for (const agent of configAgents) {
|
|
69
92
|
(0, install_agent_1.installAgentSkills)(agent, projectRoot, specFramework, pdrSurfacing);
|
|
70
93
|
}
|
|
@@ -75,6 +98,7 @@ function updateCommand() {
|
|
|
75
98
|
const legacyAgents = [];
|
|
76
99
|
if (fs.existsSync(path.join(projectRoot, '.claude'))) {
|
|
77
100
|
const specFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
101
|
+
persistSpecFramework(configPath, specFramework);
|
|
78
102
|
const pdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
79
103
|
(0, install_agent_1.installAgentSkills)('claude', projectRoot, specFramework, pdrSurfacing);
|
|
80
104
|
legacyAgents.push('claude');
|
|
@@ -99,22 +123,25 @@ function updateCommand() {
|
|
|
99
123
|
default: false,
|
|
100
124
|
});
|
|
101
125
|
if (!addMore) {
|
|
126
|
+
persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
|
|
102
127
|
console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
|
|
103
128
|
return;
|
|
104
129
|
}
|
|
105
130
|
console.log('');
|
|
106
131
|
const selected = await (0, install_agent_1.promptAgentSelection)(projectRoot);
|
|
107
132
|
if (selected.length === 0) {
|
|
133
|
+
persistSpecFramework(configPath, (0, install_agent_1.resolveSpecFramework)(projectRoot));
|
|
108
134
|
console.log('\n' + chalk_1.default.yellow('No agents selected.'));
|
|
109
135
|
console.log('\nRun ' + chalk_1.default.cyan('oprim doctor') + ' to verify your setup.');
|
|
110
136
|
return;
|
|
111
137
|
}
|
|
112
|
-
let addSpecFramework =
|
|
138
|
+
let addSpecFramework = (0, install_agent_1.resolveSpecFramework)(projectRoot);
|
|
113
139
|
let addPdrSurfacing = false;
|
|
114
140
|
if (selected.includes('claude')) {
|
|
115
141
|
addSpecFramework = await (0, install_agent_1.promptFrameworkSelection)(projectRoot);
|
|
116
142
|
addPdrSurfacing = await (0, install_agent_1.promptPdrSurfacing)();
|
|
117
143
|
}
|
|
144
|
+
persistSpecFramework(configPath, addSpecFramework);
|
|
118
145
|
console.log('\n' + chalk_1.default.bold('Installing agent skills...'));
|
|
119
146
|
for (const agent of selected) {
|
|
120
147
|
(0, install_agent_1.installAgentSkills)(agent, projectRoot, addSpecFramework, addPdrSurfacing);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function mergeConfigSchema(existingContent: string): {
|
|
2
|
+
content: string;
|
|
3
|
+
changed: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare function readSpecFramework(content: string): string | null;
|
|
6
|
+
export declare function deriveDefaultSpecFramework(content: string): string;
|
|
7
|
+
export declare function mergeSpecFramework(existingContent: string, framework: string): {
|
|
8
|
+
content: string;
|
|
9
|
+
changed: boolean;
|
|
10
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mergeConfigSchema = mergeConfigSchema;
|
|
4
|
+
exports.readSpecFramework = readSpecFramework;
|
|
5
|
+
exports.deriveDefaultSpecFramework = deriveDefaultSpecFramework;
|
|
6
|
+
exports.mergeSpecFramework = mergeSpecFramework;
|
|
7
|
+
const CONFIG_SCHEMA_FIELDS = [
|
|
8
|
+
{ key: 'context', block: 'context: ""\n' },
|
|
9
|
+
{ key: 'rules', block: 'rules: {}\n' },
|
|
10
|
+
// bet-026 — supersedes the old inert `store:` key (BET-025). `store` is intentionally left
|
|
11
|
+
// out of this table going forward: a project that already has it keeps it untouched (this
|
|
12
|
+
// merge only adds missing keys, never removes existing ones), but new/updated configs only
|
|
13
|
+
// ever gain `remote_context`.
|
|
14
|
+
{ key: 'remote_context', block: 'remote_context:\n enabled: false\n sources: []\n' },
|
|
15
|
+
];
|
|
16
|
+
function existingTopLevelKeys(content) {
|
|
17
|
+
const keys = new Set();
|
|
18
|
+
for (const line of content.split('\n')) {
|
|
19
|
+
const match = line.match(/^([A-Za-z_][\w-]*):/);
|
|
20
|
+
if (match)
|
|
21
|
+
keys.add(match[1]);
|
|
22
|
+
}
|
|
23
|
+
return keys;
|
|
24
|
+
}
|
|
25
|
+
function mergeConfigSchema(existingContent) {
|
|
26
|
+
const present = existingTopLevelKeys(existingContent);
|
|
27
|
+
const missing = CONFIG_SCHEMA_FIELDS.filter((field) => !present.has(field.key));
|
|
28
|
+
if (missing.length === 0)
|
|
29
|
+
return { content: existingContent, changed: false };
|
|
30
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
31
|
+
const additions = missing.map((field) => field.block).join('');
|
|
32
|
+
return { content: existingContent + separator + additions, changed: true };
|
|
33
|
+
}
|
|
34
|
+
// bet-023 — integrations.spec_framework is nested under `integrations:` rather than a
|
|
35
|
+
// top-level key, and its default depends on the project's existing openspec state, so it
|
|
36
|
+
// can't reuse the static CONFIG_SCHEMA_FIELDS table above. Handled as its own one-off per
|
|
37
|
+
// the bet's design (falls back to a minimal merge rather than depending on the generic
|
|
38
|
+
// top-level mechanism, which doesn't support nested keys or computed defaults).
|
|
39
|
+
function readSpecFramework(content) {
|
|
40
|
+
const match = content.match(/^\s*spec_framework:\s*(\S+)/m);
|
|
41
|
+
return match ? match[1] : null;
|
|
42
|
+
}
|
|
43
|
+
function deriveDefaultSpecFramework(content) {
|
|
44
|
+
return /^\s*openspec:\n\s*enabled:\s*true/m.test(content) ? 'openspec' : 'none';
|
|
45
|
+
}
|
|
46
|
+
function mergeSpecFramework(existingContent, framework) {
|
|
47
|
+
if (readSpecFramework(existingContent))
|
|
48
|
+
return { content: existingContent, changed: false };
|
|
49
|
+
const lines = existingContent.split('\n');
|
|
50
|
+
const integrationsIndex = lines.findIndex((line) => line === 'integrations:');
|
|
51
|
+
if (integrationsIndex === -1) {
|
|
52
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
53
|
+
return { content: existingContent + separator + `integrations:\n spec_framework: ${framework}\n`, changed: true };
|
|
54
|
+
}
|
|
55
|
+
lines.splice(integrationsIndex + 1, 0, ` spec_framework: ${framework}`);
|
|
56
|
+
return { content: lines.join('\n'), changed: true };
|
|
57
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type Agent = 'claude' | 'cursor' | 'codex' | 'gemini' | 'poolside';
|
|
2
2
|
export declare const SUPPORTED_AGENTS: readonly Agent[];
|
|
3
3
|
export declare function promptFrameworkSelection(projectRoot: string): Promise<string>;
|
|
4
|
+
export declare function resolveSpecFramework(projectRoot: string): string;
|
|
4
5
|
export declare function promptAgentSelection(projectRoot: string): Promise<string[]>;
|
|
5
6
|
export declare function promptPdrSurfacing(): Promise<boolean>;
|
|
6
7
|
export declare function promptOkfFrontmatter(): Promise<boolean>;
|
|
@@ -11,6 +12,7 @@ export declare const CLAUDE_COMMANDS: Record<string, string>;
|
|
|
11
12
|
export declare const POOLSIDE_SKILLS: Record<string, string>;
|
|
12
13
|
export declare const CURSOR_SKILLS: Record<string, string>;
|
|
13
14
|
export declare const CURSOR_COMMANDS: Record<string, string>;
|
|
15
|
+
export declare function specAuthoringSkill(): string;
|
|
14
16
|
export declare function writeAgentInstructionFile(filePath: string, section: string): void;
|
|
15
17
|
export declare function codexInstructions(): string;
|
|
16
18
|
export declare function geminiInstructions(): string;
|