@open-product-primer/cli 2.0.0 → 2.2.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 +4 -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/validate.d.ts +2 -0
- package/dist/commands/validate.js +131 -0
- package/dist/lib/config-merge.js +5 -1
- package/dist/lib/install-agent.js +78 -938
- package/dist/lib/remote-context.d.ts +50 -0
- package/dist/lib/remote-context.js +299 -0
- package/dist/lib/spec-delta.d.ts +30 -0
- package/dist/lib/spec-delta.js +216 -0
- package/dist/lib/templates.js +2 -1
- package/dist/lib/validate-checks.d.ts +13 -0
- package/dist/lib/validate-checks.js +145 -0
- package/dist/lib/workflow-renderer.d.ts +19 -0
- package/dist/lib/workflow-renderer.js +103 -0
- package/dist/lib/workflow-schema.d.ts +37 -0
- package/dist/lib/workflow-schema.js +126 -0
- package/dist/workflows/archive.inline.md +10 -0
- package/dist/workflows/archive.schema.yaml +13 -0
- package/dist/workflows/archive.template.md +106 -0
- package/dist/workflows/bet.cursor-command.md +1 -0
- package/dist/workflows/bet.inline.md +13 -0
- package/dist/workflows/bet.schema.yaml +14 -0
- package/dist/workflows/bet.template.md +114 -0
- package/dist/workflows/context.schema.yaml +13 -0
- package/dist/workflows/context.template.md +34 -0
- package/dist/workflows/criteria.cursor-command.md +1 -0
- package/dist/workflows/criteria.inline.md +11 -0
- package/dist/workflows/criteria.schema.yaml +14 -0
- package/dist/workflows/criteria.template.md +52 -0
- package/dist/workflows/note.cursor-command.md +1 -0
- package/dist/workflows/note.inline.md +11 -0
- package/dist/workflows/note.schema.yaml +14 -0
- package/dist/workflows/note.template.md +66 -0
- package/dist/workflows/pdr.cursor-command.md +1 -0
- package/dist/workflows/pdr.inline.md +10 -0
- package/dist/workflows/pdr.schema.yaml +14 -0
- package/dist/workflows/pdr.template.md +67 -0
- package/dist/workflows/promote.native.template.md +29 -0
- package/dist/workflows/promote.none.template.md +25 -0
- package/dist/workflows/promote.openspec.template.md +40 -0
- package/dist/workflows/promote.schema.yaml +14 -0
- package/dist/workflows/review.cursor-command.md +1 -0
- package/dist/workflows/review.inline.md +11 -0
- package/dist/workflows/review.schema.yaml +14 -0
- package/dist/workflows/review.template.md +69 -0
- package/dist/workflows/sequence.cursor-command.md +12 -0
- package/dist/workflows/sequence.inline.md +10 -0
- package/dist/workflows/sequence.schema.yaml +13 -0
- package/dist/workflows/sequence.template.md +88 -0
- package/dist/workflows/spec-authoring.schema.yaml +13 -0
- package/dist/workflows/spec-authoring.template.md +68 -0
- package/package.json +2 -2
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
|
@@ -8,9 +8,11 @@ const commander_1 = require("commander");
|
|
|
8
8
|
const init_1 = require("./commands/init");
|
|
9
9
|
const update_1 = require("./commands/update");
|
|
10
10
|
const doctor_1 = require("./commands/doctor");
|
|
11
|
+
const validate_1 = require("./commands/validate");
|
|
11
12
|
const migrate_1 = require("./commands/migrate");
|
|
12
13
|
const measure_1 = require("./commands/measure");
|
|
13
14
|
const ovw_1 = require("./commands/ovw");
|
|
15
|
+
const context_1 = require("./commands/context");
|
|
14
16
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
15
17
|
const program = new commander_1.Command();
|
|
16
18
|
program
|
|
@@ -20,7 +22,9 @@ program
|
|
|
20
22
|
program.addCommand((0, init_1.initCommand)());
|
|
21
23
|
program.addCommand((0, update_1.updateCommand)());
|
|
22
24
|
program.addCommand((0, doctor_1.doctorCommand)());
|
|
25
|
+
program.addCommand((0, validate_1.validateCommand)());
|
|
23
26
|
program.addCommand((0, migrate_1.migrateCommand)());
|
|
24
27
|
program.addCommand((0, measure_1.measureCommand)());
|
|
25
28
|
program.addCommand((0, ovw_1.ovwCommand)());
|
|
29
|
+
program.addCommand((0, context_1.contextCommand)());
|
|
26
30
|
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) {
|
|
@@ -0,0 +1,131 @@
|
|
|
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.validateCommand = validateCommand;
|
|
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 integrity_1 = require("../lib/integrity");
|
|
45
|
+
const validate_checks_1 = require("../lib/validate-checks");
|
|
46
|
+
const spec_delta_1 = require("../lib/spec-delta");
|
|
47
|
+
function runChecks(projectRoot) {
|
|
48
|
+
const checks = [];
|
|
49
|
+
(0, integrity_1.checkSequenceIntegrity)(projectRoot, checks);
|
|
50
|
+
(0, integrity_1.checkSkillVersionDrift)(projectRoot, checks);
|
|
51
|
+
(0, validate_checks_1.checkBetDefinitionOfDone)(projectRoot, checks);
|
|
52
|
+
(0, validate_checks_1.checkSpecDeltaDrift)(projectRoot, checks);
|
|
53
|
+
(0, validate_checks_1.checkCrossBetConflicts)(projectRoot, checks);
|
|
54
|
+
return checks;
|
|
55
|
+
}
|
|
56
|
+
function printReport(checks) {
|
|
57
|
+
console.log(chalk_1.default.bold('oprim validate') + '\n');
|
|
58
|
+
for (const check of checks) {
|
|
59
|
+
const icon = check.pass ? chalk_1.default.green('✓') : check.required ? chalk_1.default.red('✗') : chalk_1.default.yellow('○');
|
|
60
|
+
const label = check.pass ? chalk_1.default.white(check.name) : chalk_1.default.gray(check.name);
|
|
61
|
+
const note = check.note ? chalk_1.default.dim(` (${check.note})`) : '';
|
|
62
|
+
console.log(` ${icon} ${label}${note}`);
|
|
63
|
+
}
|
|
64
|
+
const passed = checks.filter((c) => c.pass).length;
|
|
65
|
+
console.log(`\n${passed}/${checks.length} checks passed.`);
|
|
66
|
+
}
|
|
67
|
+
function runDiff(projectRoot, betIdInput) {
|
|
68
|
+
const betsDir = path.join(projectRoot, 'oprim', 'bets');
|
|
69
|
+
const resolvedDir = (0, spec_delta_1.resolveBetDirectory)(betsDir, betIdInput);
|
|
70
|
+
if (!resolvedDir) {
|
|
71
|
+
console.error(chalk_1.default.red(`Bet ${betIdInput} was not found in oprim/bets/.`));
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const specsDir = path.join(betsDir, resolvedDir, 'specs');
|
|
76
|
+
if (!fs.existsSync(specsDir)) {
|
|
77
|
+
console.log(`${resolvedDir} has no spec deltas to preview.`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const capEntries = fs.readdirSync(specsDir, { withFileTypes: true }).filter((e) => e.isDirectory());
|
|
81
|
+
const capsWithDelta = capEntries.filter((e) => fs.existsSync(path.join(specsDir, e.name, 'spec.md')));
|
|
82
|
+
if (capsWithDelta.length === 0) {
|
|
83
|
+
console.log(`${resolvedDir} has no spec deltas to preview.`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
for (const capEntry of capsWithDelta) {
|
|
87
|
+
const deltaContent = fs.readFileSync(path.join(specsDir, capEntry.name, 'spec.md'), 'utf-8');
|
|
88
|
+
const currentTruthPath = path.join(projectRoot, 'oprim', 'specs', capEntry.name, 'spec.md');
|
|
89
|
+
const currentTruth = fs.existsSync(currentTruthPath) ? fs.readFileSync(currentTruthPath, 'utf-8') : null;
|
|
90
|
+
console.log(chalk_1.default.bold(`═══ ${capEntry.name} ═══`));
|
|
91
|
+
try {
|
|
92
|
+
const result = (0, spec_delta_1.foldDelta)(currentTruth, deltaContent);
|
|
93
|
+
console.log(result.content);
|
|
94
|
+
for (const note of result.notes) {
|
|
95
|
+
console.log(chalk_1.default.yellow(` note: ${note}`));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.error(chalk_1.default.red(err.message));
|
|
100
|
+
process.exitCode = 1;
|
|
101
|
+
}
|
|
102
|
+
console.log('');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function validateCommand() {
|
|
106
|
+
return new commander_1.Command('validate')
|
|
107
|
+
.description('CI-gateable validation of oprim board and spec-delta integrity')
|
|
108
|
+
.option('--json', 'print machine-readable JSON instead of the human-readable report')
|
|
109
|
+
.option('--strict', 'exit non-zero if any check fails, not only required ones')
|
|
110
|
+
.option('--diff <bet-id>', "preview a bet's spec-delta merge result without writing files")
|
|
111
|
+
.action((opts) => {
|
|
112
|
+
const projectRoot = process.cwd();
|
|
113
|
+
if (opts.diff) {
|
|
114
|
+
runDiff(projectRoot, opts.diff);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const checks = runChecks(projectRoot);
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
console.log(JSON.stringify({ checks }));
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
printReport(checks);
|
|
123
|
+
}
|
|
124
|
+
const requiredFailed = checks.some((c) => c.required && !c.pass);
|
|
125
|
+
const anyFailed = checks.some((c) => !c.pass);
|
|
126
|
+
const shouldFail = opts.strict ? anyFailed : requiredFailed;
|
|
127
|
+
if (shouldFail) {
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
package/dist/lib/config-merge.js
CHANGED
|
@@ -7,7 +7,11 @@ exports.mergeSpecFramework = mergeSpecFramework;
|
|
|
7
7
|
const CONFIG_SCHEMA_FIELDS = [
|
|
8
8
|
{ key: 'context', block: 'context: ""\n' },
|
|
9
9
|
{ key: 'rules', block: 'rules: {}\n' },
|
|
10
|
-
|
|
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' },
|
|
11
15
|
];
|
|
12
16
|
function existingTopLevelKeys(content) {
|
|
13
17
|
const keys = new Set();
|