@runcontext/cli 0.1.1 → 0.2.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/index.js CHANGED
@@ -1,16 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command9 } from "commander";
4
+ import { Command as Command11 } from "commander";
5
5
 
6
- // src/commands/build.ts
6
+ // src/commands/lint.ts
7
7
  import { Command } from "commander";
8
+ import chalk2 from "chalk";
8
9
  import path from "path";
9
- import fs from "fs";
10
10
  import {
11
- loadConfig,
12
11
  compile,
13
- emitManifest,
12
+ loadConfig,
14
13
  LintEngine,
15
14
  ALL_RULES
16
15
  } from "@runcontext/core";
@@ -22,502 +21,659 @@ function formatDiagnostics(diagnostics) {
22
21
  return chalk.green("No issues found.");
23
22
  }
24
23
  const lines = [];
25
- let errorCount = 0;
26
- let warningCount = 0;
27
24
  for (const d of diagnostics) {
28
- const location = `${d.source.file}:${d.source.line}:${d.source.col}`;
29
- const severityLabel = d.severity === "error" ? chalk.red("error") : chalk.yellow("warning");
30
- if (d.severity === "error") {
31
- errorCount++;
32
- } else {
33
- warningCount++;
34
- }
35
- lines.push(` ${location} ${severityLabel} ${chalk.dim(d.ruleId)} ${d.message}`);
25
+ const icon = d.severity === "error" ? chalk.red("error") : chalk.yellow("warning");
26
+ const loc = chalk.gray(
27
+ `${d.location.file}:${d.location.line}:${d.location.column}`
28
+ );
29
+ const rule = chalk.gray(`[${d.ruleId}]`);
30
+ const fixTag = d.fixable ? chalk.blue(" (fixable)") : "";
31
+ lines.push(` ${icon} ${d.message} ${rule}${fixTag}`);
32
+ lines.push(` ${loc}`);
36
33
  }
34
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
35
+ const warnCount = diagnostics.filter((d) => d.severity === "warning").length;
37
36
  lines.push("");
38
37
  const parts = [];
39
- if (errorCount > 0) {
40
- parts.push(chalk.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`));
41
- }
42
- if (warningCount > 0) {
43
- parts.push(chalk.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`));
44
- }
38
+ if (errorCount > 0) parts.push(chalk.red(`${errorCount} error(s)`));
39
+ if (warnCount > 0) parts.push(chalk.yellow(`${warnCount} warning(s)`));
45
40
  lines.push(parts.join(", "));
46
41
  return lines.join("\n");
47
42
  }
43
+ function formatTierScore(score) {
44
+ const lines = [];
45
+ const tierColor = getTierColor(score.tier);
46
+ lines.push(
47
+ `${chalk.bold(score.model)}: ${tierColor(score.tier.toUpperCase())}`
48
+ );
49
+ lines.push("");
50
+ lines.push(formatTierSection("Bronze", score.bronze.passed, score.bronze.checks));
51
+ lines.push(formatTierSection("Silver", score.silver.passed, score.silver.checks));
52
+ lines.push(formatTierSection("Gold", score.gold.passed, score.gold.checks));
53
+ return lines.join("\n");
54
+ }
55
+ function formatTierSection(label, passed, checks) {
56
+ const lines = [];
57
+ const status = passed ? chalk.green("PASS") : chalk.red("FAIL");
58
+ lines.push(` ${label}: ${status}`);
59
+ for (const check of checks) {
60
+ const icon = check.passed ? chalk.green(" +") : chalk.red(" -");
61
+ lines.push(` ${icon} ${check.label}`);
62
+ if (check.detail && !check.passed) {
63
+ lines.push(chalk.gray(` ${check.detail}`));
64
+ }
65
+ }
66
+ return lines.join("\n");
67
+ }
68
+ function getTierColor(tier) {
69
+ switch (tier) {
70
+ case "gold":
71
+ return chalk.yellow;
72
+ case "silver":
73
+ return chalk.white;
74
+ case "bronze":
75
+ return chalk.hex("#CD7F32");
76
+ default:
77
+ return chalk.gray;
78
+ }
79
+ }
80
+ function formatError(message) {
81
+ return chalk.red(`Error: ${message}`);
82
+ }
83
+ function formatSuccess(message) {
84
+ return chalk.green(message);
85
+ }
48
86
 
49
87
  // src/formatters/json.ts
50
- function formatDiagnosticsJson(diagnostics) {
51
- return JSON.stringify(diagnostics, null, 2);
88
+ function formatJson(data) {
89
+ return JSON.stringify(data, null, 2);
52
90
  }
53
91
 
54
- // src/commands/build.ts
55
- var buildCommand = new Command("build").description("Compile context files and emit manifest").option("--format <format>", "output format for diagnostics (pretty|json)", "pretty").action(async (opts) => {
92
+ // src/commands/lint.ts
93
+ var lintCommand = new Command("lint").description("Run all lint rules against context files").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (opts) => {
56
94
  try {
57
- const config = await loadConfig(process.cwd());
58
- const rootDir = config.paths?.rootDir || process.cwd();
59
- const contextDir = path.resolve(rootDir, config.paths?.contextDir || "context");
60
- const distDir = path.resolve(rootDir, config.paths?.distDir || "dist");
61
- const { graph, diagnostics: compileDiags } = await compile({ contextDir, config });
62
- const engine = new LintEngine(config.lint?.rules);
95
+ const config = loadConfig(process.cwd());
96
+ const contextDir = opts.contextDir ? path.resolve(opts.contextDir) : path.resolve(config.context_dir);
97
+ const { graph, diagnostics: compileDiags } = await compile({
98
+ contextDir,
99
+ config
100
+ });
101
+ const overrides = config.lint?.severity_overrides;
102
+ const engine = new LintEngine(overrides);
63
103
  for (const rule of ALL_RULES) {
64
104
  engine.register(rule);
65
105
  }
66
106
  const lintDiags = engine.run(graph);
67
107
  const allDiags = [...compileDiags, ...lintDiags];
68
- if (allDiags.length > 0) {
69
- const output = opts.format === "json" ? formatDiagnosticsJson(allDiags) : formatDiagnostics(allDiags);
70
- console.error(output);
108
+ if (config.minimum_tier) {
109
+ const tierOrder = ["none", "bronze", "silver", "gold"];
110
+ const minIdx = tierOrder.indexOf(config.minimum_tier);
111
+ for (const [modelName, score] of graph.tiers) {
112
+ const actualIdx = tierOrder.indexOf(score.tier);
113
+ if (actualIdx < minIdx) {
114
+ allDiags.push({
115
+ ruleId: "tier/minimum-tier",
116
+ severity: "error",
117
+ message: `Model "${modelName}" is tier "${score.tier}" but minimum_tier is "${config.minimum_tier}"`,
118
+ location: { file: `model:${modelName}`, line: 1, column: 1 },
119
+ fixable: false
120
+ });
121
+ }
122
+ }
123
+ }
124
+ if (opts.format === "json") {
125
+ console.log(formatJson(allDiags));
126
+ } else {
127
+ console.log(formatDiagnostics(allDiags));
71
128
  }
72
- const manifest = emitManifest(graph, config);
73
- fs.mkdirSync(distDir, { recursive: true });
74
- const manifestPath = path.join(distDir, "context.manifest.json");
75
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
76
- const summary = [
77
- `Built manifest: ${manifest.concepts.length} concepts`,
78
- `${manifest.products.length} products`,
79
- `${manifest.policies.length} policies`,
80
- `${manifest.entities.length} entities`,
81
- `${manifest.terms.length} terms`,
82
- `${manifest.owners.length} owners`
83
- ].join(", ");
84
- console.log(summary);
85
- console.log(`Manifest written to ${manifestPath}`);
86
129
  const hasErrors = allDiags.some((d) => d.severity === "error");
87
130
  if (hasErrors) {
88
131
  process.exit(1);
89
132
  }
90
133
  } catch (err) {
91
- console.error("Build failed:", err.message);
134
+ console.error(chalk2.red(`Lint failed: ${err.message}`));
92
135
  process.exit(1);
93
136
  }
94
137
  });
95
138
 
96
- // src/commands/lint.ts
139
+ // src/commands/build.ts
97
140
  import { Command as Command2 } from "commander";
141
+ import chalk3 from "chalk";
98
142
  import path2 from "path";
99
- import {
100
- loadConfig as loadConfig2,
101
- compile as compile2,
102
- LintEngine as LintEngine2,
103
- ALL_RULES as ALL_RULES2
104
- } from "@runcontext/core";
105
- var lintCommand = new Command2("lint").description("Lint context files and report diagnostics").option("--format <format>", "output format (pretty|json)", "pretty").option("--fix", "apply autofixes (placeholder)").action(async (opts) => {
106
- if (opts.fix) {
107
- console.log("Use 'context fix' for autofixes.");
108
- return;
109
- }
143
+ import fs from "fs";
144
+ import { compile as compile2, loadConfig as loadConfig2, emitManifest } from "@runcontext/core";
145
+ var buildCommand = new Command2("build").description("Compile context files and emit manifest JSON").option("--context-dir <path>", "Path to context directory").option("--output-dir <path>", "Path to output directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (opts) => {
110
146
  try {
111
- const config = await loadConfig2(process.cwd());
112
- const rootDir = config.paths?.rootDir || process.cwd();
113
- const contextDir = path2.resolve(rootDir, config.paths?.contextDir || "context");
114
- const { graph, diagnostics: compileDiags } = await compile2({ contextDir, config });
115
- const engine = new LintEngine2(config.lint?.rules);
116
- for (const rule of ALL_RULES2) {
117
- engine.register(rule);
118
- }
119
- const lintDiags = engine.run(graph);
120
- const allDiags = [...compileDiags, ...lintDiags];
121
- const output = opts.format === "json" ? formatDiagnosticsJson(allDiags) : formatDiagnostics(allDiags);
122
- console.log(output);
123
- const hasErrors = allDiags.some((d) => d.severity === "error");
124
- if (hasErrors) {
147
+ const config = loadConfig2(process.cwd());
148
+ const contextDir = opts.contextDir ? path2.resolve(opts.contextDir) : path2.resolve(config.context_dir);
149
+ const outputDir = opts.outputDir ? path2.resolve(opts.outputDir) : path2.resolve(config.output_dir);
150
+ const { graph, diagnostics } = await compile2({ contextDir, config });
151
+ const errors = diagnostics.filter((d) => d.severity === "error");
152
+ if (errors.length > 0) {
153
+ if (opts.format === "json") {
154
+ console.log(formatJson({ success: false, errors }));
155
+ } else {
156
+ console.error(
157
+ chalk3.red(`Build failed with ${errors.length} error(s):`)
158
+ );
159
+ for (const e of errors) {
160
+ console.error(chalk3.red(` - ${e.message} [${e.ruleId}]`));
161
+ }
162
+ }
125
163
  process.exit(1);
126
164
  }
165
+ const manifest = emitManifest(graph, config);
166
+ fs.mkdirSync(outputDir, { recursive: true });
167
+ const outputPath = path2.join(outputDir, "contextkit-manifest.json");
168
+ fs.writeFileSync(outputPath, JSON.stringify(manifest, null, 2), "utf-8");
169
+ if (opts.format === "json") {
170
+ console.log(formatJson({ success: true, outputPath, manifest }));
171
+ } else {
172
+ console.log(formatSuccess(`Manifest written to ${outputPath}`));
173
+ }
127
174
  } catch (err) {
128
- console.error("Lint failed:", err.message);
175
+ console.error(formatError(err.message));
129
176
  process.exit(1);
130
177
  }
131
178
  });
132
179
 
133
- // src/commands/init.ts
180
+ // src/commands/tier.ts
134
181
  import { Command as Command3 } from "commander";
135
- import fs2 from "fs";
182
+ import chalk4 from "chalk";
136
183
  import path3 from "path";
137
- var SAMPLE_CONCEPT = `kind: concept
138
- id: example-concept
139
- definition: An example concept to get you started.
140
- owner: example-team
141
- tags:
142
- - example
143
- status: draft
144
- `;
145
- var SAMPLE_OWNER = `kind: owner
146
- id: example-team
147
- displayName: Example Team
148
- email: team@example.com
149
- `;
150
- function generateConfig(projectName) {
151
- return `project:
152
- id: ${projectName}
153
- displayName: "${projectName}"
154
- version: "0.1.0"
155
-
156
- paths:
157
- contextDir: context
158
- distDir: dist
159
-
160
- lint:
161
- defaultSeverity: warning
162
- `;
163
- }
164
- var initCommand = new Command3("init").description("Create a new ContextKit project").option("--name <name>", "project name (defaults to directory basename)").action((opts) => {
165
- const cwd = process.cwd();
166
- const projectName = opts.name || path3.basename(cwd);
167
- const dirs = [
168
- "context/concepts",
169
- "context/products",
170
- "context/policies",
171
- "context/entities",
172
- "context/owners",
173
- "context/terms"
174
- ];
175
- const files = [
176
- { path: "context/concepts/example-concept.ctx.yaml", content: SAMPLE_CONCEPT },
177
- { path: "context/owners/example-team.owner.yaml", content: SAMPLE_OWNER },
178
- { path: "contextkit.config.yaml", content: generateConfig(projectName) }
179
- ];
180
- const created = [];
181
- for (const dir of dirs) {
182
- const fullPath = path3.join(cwd, dir);
183
- fs2.mkdirSync(fullPath, { recursive: true });
184
- created.push(dir + "/");
185
- }
186
- for (const file of files) {
187
- const fullPath = path3.join(cwd, file.path);
188
- if (!fs2.existsSync(fullPath)) {
189
- fs2.writeFileSync(fullPath, file.content, "utf-8");
190
- created.push(file.path);
184
+ import { compile as compile3, loadConfig as loadConfig3, computeTier } from "@runcontext/core";
185
+ var tierCommand = new Command3("tier").description("Show tier scorecard for one or all models").argument("[model-name]", "Specific model name to check").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (modelName, opts) => {
186
+ try {
187
+ const config = loadConfig3(process.cwd());
188
+ const contextDir = opts.contextDir ? path3.resolve(opts.contextDir) : path3.resolve(config.context_dir);
189
+ const { graph } = await compile3({ contextDir, config });
190
+ let scores;
191
+ if (modelName) {
192
+ if (!graph.models.has(modelName)) {
193
+ console.error(formatError(`Model '${modelName}' not found.`));
194
+ const available = [...graph.models.keys()].join(", ");
195
+ if (available) {
196
+ console.error(chalk4.gray(`Available models: ${available}`));
197
+ }
198
+ process.exit(1);
199
+ }
200
+ scores = [computeTier(modelName, graph)];
191
201
  } else {
192
- console.log(` Skipped (already exists): ${file.path}`);
202
+ scores = [...graph.models.keys()].map(
203
+ (name) => computeTier(name, graph)
204
+ );
193
205
  }
194
- }
195
- console.log(`Initialized ContextKit project "${projectName}":`);
196
- for (const item of created) {
197
- console.log(` created ${item}`);
206
+ if (scores.length === 0) {
207
+ console.log(
208
+ opts.format === "json" ? formatJson([]) : chalk4.yellow("No models found.")
209
+ );
210
+ return;
211
+ }
212
+ if (opts.format === "json") {
213
+ console.log(formatJson(scores));
214
+ } else {
215
+ for (const score of scores) {
216
+ console.log(formatTierScore(score));
217
+ console.log("");
218
+ }
219
+ }
220
+ } catch (err) {
221
+ console.error(formatError(err.message));
222
+ process.exit(1);
198
223
  }
199
224
  });
200
225
 
201
226
  // src/commands/explain.ts
202
227
  import { Command as Command4 } from "commander";
203
- import fs3 from "fs";
228
+ import chalk5 from "chalk";
204
229
  import path4 from "path";
205
- import chalk2 from "chalk";
206
- var explainCommand = new Command4("explain").description("Look up a node by ID in the manifest").argument("<id>", "node ID to look up").option("--manifest <path>", "path to manifest file", "dist/context.manifest.json").action((id, opts) => {
207
- const manifestPath = path4.resolve(process.cwd(), opts.manifest);
208
- if (!fs3.existsSync(manifestPath)) {
209
- console.error(`Manifest not found at ${manifestPath}. Run 'context build' first.`);
210
- process.exit(1);
211
- }
212
- let manifest;
230
+ import { compile as compile4, loadConfig as loadConfig4 } from "@runcontext/core";
231
+ var explainCommand = new Command4("explain").description("Look up models, terms, or owners by name and show details").argument("<name>", "Name of a model, term, or owner to look up").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").action(async (name, opts) => {
213
232
  try {
214
- const raw = fs3.readFileSync(manifestPath, "utf-8");
215
- manifest = JSON.parse(raw);
216
- } catch {
217
- console.error(`Failed to read manifest at ${manifestPath}`);
218
- process.exit(1);
219
- }
220
- const entry = manifest.indexes?.byId?.[id];
221
- if (!entry) {
222
- console.error(`Node "${id}" not found in manifest.`);
223
- process.exit(1);
224
- }
225
- const { kind, index } = entry;
226
- const collection = manifest[kind + "s"];
227
- if (!collection || !collection[index]) {
228
- console.error(`Node "${id}" not found in "${kind}s" collection.`);
229
- process.exit(1);
230
- }
231
- const node = collection[index];
232
- console.log(chalk2.bold(`${kind}: ${id}`));
233
- console.log("");
234
- const fields = Object.entries(node).filter(
235
- ([key]) => key !== "id"
236
- );
237
- for (const [key, value] of fields) {
238
- if (value === void 0 || value === null) continue;
239
- if (Array.isArray(value)) {
240
- console.log(` ${chalk2.dim(key + ":")} ${value.join(", ")}`);
241
- } else if (typeof value === "object") {
242
- console.log(` ${chalk2.dim(key + ":")} ${JSON.stringify(value)}`);
233
+ const config = loadConfig4(process.cwd());
234
+ const contextDir = opts.contextDir ? path4.resolve(opts.contextDir) : path4.resolve(config.context_dir);
235
+ const { graph } = await compile4({ contextDir, config });
236
+ const results = [];
237
+ if (graph.models.has(name)) {
238
+ results.push({ type: "model", name, data: graph.models.get(name) });
239
+ }
240
+ if (graph.terms.has(name)) {
241
+ results.push({ type: "term", name, data: graph.terms.get(name) });
242
+ }
243
+ if (graph.owners.has(name)) {
244
+ results.push({ type: "owner", name, data: graph.owners.get(name) });
245
+ }
246
+ if (graph.governance.has(name)) {
247
+ results.push({
248
+ type: "governance",
249
+ name,
250
+ data: graph.governance.get(name)
251
+ });
252
+ }
253
+ if (graph.rules.has(name)) {
254
+ results.push({ type: "rules", name, data: graph.rules.get(name) });
255
+ }
256
+ if (graph.lineage.has(name)) {
257
+ results.push({ type: "lineage", name, data: graph.lineage.get(name) });
258
+ }
259
+ if (results.length === 0) {
260
+ console.error(formatError(`No matching entity found for '${name}'.`));
261
+ process.exit(1);
262
+ }
263
+ if (opts.format === "json") {
264
+ console.log(formatJson(results));
243
265
  } else {
244
- console.log(` ${chalk2.dim(key + ":")} ${String(value)}`);
266
+ for (const result of results) {
267
+ console.log(chalk5.bold(`${result.type}: ${result.name}`));
268
+ console.log(chalk5.gray("---"));
269
+ console.log(JSON.stringify(result.data, null, 2));
270
+ console.log("");
271
+ }
245
272
  }
273
+ } catch (err) {
274
+ console.error(formatError(err.message));
275
+ process.exit(1);
246
276
  }
247
277
  });
248
278
 
249
279
  // src/commands/fix.ts
250
280
  import { Command as Command5 } from "commander";
281
+ import chalk6 from "chalk";
251
282
  import path5 from "path";
252
- import fs4 from "fs";
253
- import chalk3 from "chalk";
283
+ import fs2 from "fs";
254
284
  import {
255
- loadConfig as loadConfig3,
256
- compile as compile3,
257
- LintEngine as LintEngine3,
258
- ALL_RULES as ALL_RULES3,
285
+ compile as compile5,
286
+ loadConfig as loadConfig5,
287
+ LintEngine as LintEngine2,
288
+ ALL_RULES as ALL_RULES2,
259
289
  applyFixes
260
290
  } from "@runcontext/core";
261
- var fixCommand = new Command5("fix").description("Apply autofixes to context files").option("--write", "write fixes to disk (default: dry-run)").option("--format <format>", "output format for diagnostics (pretty|json)", "pretty").action(async (opts) => {
291
+ var fixCommand = new Command5("fix").description("Auto-fix lint issues").option("--context-dir <path>", "Path to context directory").option("--format <type>", "Output format: pretty or json", "pretty").option("--dry-run", "Show what would be fixed without writing files").action(async (opts) => {
262
292
  try {
263
- const config = await loadConfig3(process.cwd());
264
- const rootDir = config.paths?.rootDir || process.cwd();
265
- const contextDir = path5.resolve(rootDir, config.paths?.contextDir || "context");
266
- const { graph, diagnostics: compileDiags } = await compile3({ contextDir, config });
267
- const engine = new LintEngine3(config.lint?.rules);
268
- for (const rule of ALL_RULES3) {
293
+ const config = loadConfig5(process.cwd());
294
+ const contextDir = opts.contextDir ? path5.resolve(opts.contextDir) : path5.resolve(config.context_dir);
295
+ const { graph } = await compile5({ contextDir, config });
296
+ const overrides = config.lint?.severity_overrides;
297
+ const engine = new LintEngine2(overrides);
298
+ for (const rule of ALL_RULES2) {
269
299
  engine.register(rule);
270
300
  }
271
- const lintDiags = engine.run(graph);
272
- const allDiags = [...compileDiags, ...lintDiags];
273
- const fixableDiags = allDiags.filter((d) => d.fixable && d.fix);
274
- const unfixableDiags = allDiags.filter((d) => !d.fixable || !d.fix);
275
- if (fixableDiags.length === 0) {
276
- console.log(chalk3.green("No fixable issues found."));
277
- if (unfixableDiags.length > 0) {
278
- console.log("");
279
- console.log(chalk3.yellow(`${unfixableDiags.length} unfixable issue(s) remain:`));
280
- const output = opts.format === "json" ? formatDiagnosticsJson(unfixableDiags) : formatDiagnostics(unfixableDiags);
281
- console.log(output);
282
- const hasErrors = unfixableDiags.some((d) => d.severity === "error");
283
- if (hasErrors) {
284
- process.exit(1);
285
- }
301
+ const diagnostics = engine.run(graph);
302
+ const fixable = diagnostics.filter((d) => d.fixable);
303
+ if (fixable.length === 0) {
304
+ if (opts.format === "json") {
305
+ console.log(formatJson({ fixedFiles: [], fixCount: 0 }));
306
+ } else {
307
+ console.log(chalk6.green("No fixable issues found."));
286
308
  }
287
309
  return;
288
310
  }
289
- const results = applyFixes(fixableDiags);
290
- if (opts.write) {
291
- for (const result of results) {
292
- fs4.writeFileSync(result.file, result.newContent, "utf-8");
311
+ const readFile = (filePath) => fs2.readFileSync(filePath, "utf-8");
312
+ const fixedFiles = applyFixes(fixable, readFile);
313
+ if (opts.dryRun) {
314
+ if (opts.format === "json") {
315
+ const entries = [...fixedFiles.entries()].map(([file, content]) => ({
316
+ file,
317
+ content
318
+ }));
319
+ console.log(
320
+ formatJson({ dryRun: true, fixCount: fixable.length, entries })
321
+ );
322
+ } else {
323
+ console.log(
324
+ chalk6.yellow(`Dry run: ${fixable.length} issue(s) would be fixed in ${fixedFiles.size} file(s):`)
325
+ );
326
+ for (const file of fixedFiles.keys()) {
327
+ console.log(chalk6.gray(` ${file}`));
328
+ }
293
329
  }
294
- const totalEdits = results.reduce((sum, r) => sum + r.editsApplied, 0);
330
+ return;
331
+ }
332
+ for (const [file, content] of fixedFiles) {
333
+ fs2.writeFileSync(file, content, "utf-8");
334
+ }
335
+ if (opts.format === "json") {
295
336
  console.log(
296
- chalk3.green(`Fixed ${totalEdits} issue(s) in ${results.length} file(s).`)
337
+ formatJson({
338
+ fixedFiles: [...fixedFiles.keys()],
339
+ fixCount: fixable.length
340
+ })
297
341
  );
298
- if (unfixableDiags.length > 0) {
299
- console.log("");
300
- console.log(chalk3.yellow(`${unfixableDiags.length} unfixable issue(s) remain:`));
301
- const output = opts.format === "json" ? formatDiagnosticsJson(unfixableDiags) : formatDiagnostics(unfixableDiags);
302
- console.log(output);
303
- }
304
342
  } else {
305
- console.log(chalk3.cyan("Dry run \u2014 no files changed. Use --write to apply fixes.\n"));
306
- console.log(chalk3.bold(`${fixableDiags.length} fixable issue(s) found:
307
- `));
308
- for (const diag of fixableDiags) {
309
- const location = `${diag.source.file}:${diag.source.line}:${diag.source.col}`;
310
- const severityLabel = diag.severity === "error" ? chalk3.red("error") : chalk3.yellow("warning");
311
- console.log(` ${location} ${severityLabel} ${chalk3.dim(diag.ruleId)} ${diag.message}`);
312
- if (diag.fix) {
313
- console.log(` ${chalk3.green("fix:")} ${diag.fix.description}`);
314
- }
315
- }
316
- console.log("");
317
343
  console.log(
318
- `Would fix ${results.reduce((s, r) => s + r.editsApplied, 0)} issue(s) in ${results.length} file(s).`
344
+ formatSuccess(
345
+ `Fixed ${fixable.length} issue(s) in ${fixedFiles.size} file(s).`
346
+ )
319
347
  );
320
- if (unfixableDiags.length > 0) {
321
- console.log("");
322
- console.log(chalk3.yellow(`${unfixableDiags.length} unfixable issue(s) would remain.`));
323
- }
324
- }
325
- const hasUnfixableErrors = unfixableDiags.some((d) => d.severity === "error");
326
- if (hasUnfixableErrors) {
327
- process.exit(1);
328
348
  }
329
349
  } catch (err) {
330
- console.error("Fix failed:", err.message);
350
+ console.error(formatError(err.message));
331
351
  process.exit(1);
332
352
  }
333
353
  });
334
354
 
335
355
  // src/commands/dev.ts
336
356
  import { Command as Command6 } from "commander";
357
+ import chalk7 from "chalk";
337
358
  import path6 from "path";
338
- import chalk4 from "chalk";
339
- import { watch } from "chokidar";
340
359
  import {
341
- loadConfig as loadConfig4,
342
- compile as compile4,
343
- LintEngine as LintEngine4,
344
- ALL_RULES as ALL_RULES4
360
+ compile as compile6,
361
+ loadConfig as loadConfig6,
362
+ LintEngine as LintEngine3,
363
+ ALL_RULES as ALL_RULES3
345
364
  } from "@runcontext/core";
346
- var devCommand = new Command6("dev").description("Watch context files and rebuild on change").action(async () => {
365
+ async function runLint(contextDir) {
366
+ const config = loadConfig6(process.cwd());
367
+ const { graph, diagnostics: compileDiags } = await compile6({
368
+ contextDir,
369
+ config
370
+ });
371
+ const overrides = config.lint?.severity_overrides;
372
+ const engine = new LintEngine3(overrides);
373
+ for (const rule of ALL_RULES3) {
374
+ engine.register(rule);
375
+ }
376
+ const lintDiags = engine.run(graph);
377
+ const allDiags = [...compileDiags, ...lintDiags];
378
+ console.clear();
379
+ console.log(chalk7.gray(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Linting...`));
380
+ console.log(formatDiagnostics(allDiags));
381
+ console.log("");
382
+ }
383
+ var devCommand = new Command6("dev").description("Watch mode \u2014 re-run lint on file changes").option("--context-dir <path>", "Path to context directory").action(async (opts) => {
347
384
  try {
348
- const config = await loadConfig4(process.cwd());
349
- const rootDir = config.paths?.rootDir || process.cwd();
350
- const contextDir = path6.resolve(rootDir, config.paths?.contextDir || "context");
351
- const watchPattern = path6.join(contextDir, "**/*.{yaml,yml}");
352
- console.log(chalk4.cyan(`Watching ${watchPattern} for changes...
353
- `));
354
- await runBuild(contextDir, config);
385
+ const config = loadConfig6(process.cwd());
386
+ const contextDir = opts.contextDir ? path6.resolve(opts.contextDir) : path6.resolve(config.context_dir);
387
+ console.log(chalk7.blue(`Watching ${contextDir} for changes...`));
388
+ console.log(chalk7.gray("Press Ctrl+C to stop.\n"));
389
+ await runLint(contextDir);
390
+ const { watch } = await import("chokidar");
355
391
  let debounceTimer = null;
356
- const watcher = watch(watchPattern, {
357
- ignoreInitial: true,
358
- persistent: true
392
+ const watcher = watch(contextDir, {
393
+ ignored: /(^|[/\\])\../,
394
+ // ignore dotfiles
395
+ persistent: true,
396
+ ignoreInitial: true
359
397
  });
360
398
  watcher.on("all", (_event, _filePath) => {
361
- if (debounceTimer) {
362
- clearTimeout(debounceTimer);
363
- }
399
+ if (debounceTimer) clearTimeout(debounceTimer);
364
400
  debounceTimer = setTimeout(async () => {
365
- debounceTimer = null;
366
- await runBuild(contextDir, config);
367
- }, 100);
368
- });
369
- watcher.on("error", (error) => {
370
- console.error(chalk4.red(`Watcher error: ${error.message}`));
401
+ try {
402
+ await runLint(contextDir);
403
+ } catch (err) {
404
+ console.error(
405
+ chalk7.red(`Lint error: ${err.message}`)
406
+ );
407
+ }
408
+ }, 300);
371
409
  });
372
- const cleanup = () => {
373
- console.log(chalk4.dim("\nStopping watch mode..."));
374
- watcher.close().then(() => {
375
- process.exit(0);
376
- });
377
- };
378
- process.on("SIGINT", cleanup);
379
- process.on("SIGTERM", cleanup);
380
410
  } catch (err) {
381
- console.error("Dev mode failed:", err.message);
411
+ console.error(chalk7.red(`Dev mode failed: ${err.message}`));
382
412
  process.exit(1);
383
413
  }
384
414
  });
385
- async function runBuild(contextDir, config) {
386
- const separator = chalk4.dim("\u2500".repeat(60));
387
- const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
388
- console.log(separator);
389
- console.log(chalk4.bold(`[${timestamp}] Rebuilding...
390
- `));
415
+
416
+ // src/commands/init.ts
417
+ import { Command as Command7 } from "commander";
418
+ import chalk8 from "chalk";
419
+ import path7 from "path";
420
+ import fs3 from "fs";
421
+ var EXAMPLE_OSI = `version: "1.0"
422
+
423
+ semantic_model:
424
+ - name: example-model
425
+ description: An example semantic model
426
+ ai_context:
427
+ instructions: "Use this model for general analytics queries"
428
+ synonyms: ["example", "sample model"]
429
+
430
+ datasets:
431
+ - name: example_table
432
+ source: warehouse.public.example_table
433
+ primary_key: [id]
434
+ description: "Example table"
435
+ fields:
436
+ - name: id
437
+ expression:
438
+ dialects:
439
+ - dialect: ANSI_SQL
440
+ expression: id
441
+ description: "Primary key"
442
+ type: number
443
+ - name: name
444
+ expression:
445
+ dialects:
446
+ - dialect: ANSI_SQL
447
+ expression: name
448
+ description: "Name field"
449
+ type: string
450
+ `;
451
+ var EXAMPLE_GOVERNANCE = `model: example-model
452
+ owner: data-team
453
+ classification: internal
454
+ security:
455
+ pii: false
456
+ access_level: internal
457
+ datasets:
458
+ example_table:
459
+ grain: one row per example entity
460
+ fields:
461
+ id:
462
+ description: "Primary key"
463
+ name:
464
+ description: "Name field"
465
+ `;
466
+ var EXAMPLE_TERM = `glossary:
467
+ - term: Example Term
468
+ definition: A sample glossary term to demonstrate the format
469
+ aliases: ["sample term"]
470
+ owner: data-team
471
+ `;
472
+ var EXAMPLE_OWNER = `team: data-team
473
+ name: Data Team
474
+ email: data-team@example.com
475
+ slack: "#data-team"
476
+ members:
477
+ - name: Jane Doe
478
+ role: lead
479
+ `;
480
+ var EXAMPLE_CONFIG = `context_dir: context
481
+ output_dir: dist
482
+ minimum_tier: bronze
483
+ `;
484
+ var initCommand = new Command7("init").description("Scaffold a v0.2 ContextKit project structure").option("--dir <path>", "Root directory for the project", ".").action(async (opts) => {
391
485
  try {
392
- const { graph, diagnostics: compileDiags } = await compile4({ contextDir, config });
393
- const engine = new LintEngine4(config.lint?.rules);
394
- for (const rule of ALL_RULES4) {
395
- engine.register(rule);
486
+ const rootDir = path7.resolve(opts.dir);
487
+ const contextDir = path7.join(rootDir, "context");
488
+ const dirs = [
489
+ path7.join(contextDir, "models"),
490
+ path7.join(contextDir, "governance"),
491
+ path7.join(contextDir, "glossary"),
492
+ path7.join(contextDir, "owners")
493
+ ];
494
+ for (const dir of dirs) {
495
+ fs3.mkdirSync(dir, { recursive: true });
396
496
  }
397
- const lintDiags = engine.run(graph);
398
- const allDiags = [...compileDiags, ...lintDiags];
399
- if (allDiags.length === 0) {
400
- console.log(chalk4.green("No issues found.\n"));
401
- return;
402
- }
403
- let errorCount = 0;
404
- let warningCount = 0;
405
- let fixableCount = 0;
406
- for (const d of allDiags) {
407
- if (d.severity === "error") errorCount++;
408
- else warningCount++;
409
- if (d.fixable) fixableCount++;
410
- const location = `${d.source.file}:${d.source.line}:${d.source.col}`;
411
- const severityLabel = d.severity === "error" ? chalk4.red("error") : chalk4.yellow("warning");
412
- console.log(` ${location} ${severityLabel} ${chalk4.dim(d.ruleId)} ${d.message}`);
497
+ const files = [
498
+ {
499
+ path: path7.join(contextDir, "models", "example-model.osi.yaml"),
500
+ content: EXAMPLE_OSI
501
+ },
502
+ {
503
+ path: path7.join(
504
+ contextDir,
505
+ "governance",
506
+ "example-model.governance.yaml"
507
+ ),
508
+ content: EXAMPLE_GOVERNANCE
509
+ },
510
+ {
511
+ path: path7.join(contextDir, "glossary", "glossary.term.yaml"),
512
+ content: EXAMPLE_TERM
513
+ },
514
+ {
515
+ path: path7.join(contextDir, "owners", "data-team.owner.yaml"),
516
+ content: EXAMPLE_OWNER
517
+ },
518
+ {
519
+ path: path7.join(rootDir, "contextkit.config.yaml"),
520
+ content: EXAMPLE_CONFIG
521
+ }
522
+ ];
523
+ let created = 0;
524
+ let skipped = 0;
525
+ for (const file of files) {
526
+ if (fs3.existsSync(file.path)) {
527
+ console.log(chalk8.gray(` skip ${path7.relative(rootDir, file.path)} (exists)`));
528
+ skipped++;
529
+ } else {
530
+ fs3.writeFileSync(file.path, file.content, "utf-8");
531
+ console.log(chalk8.green(` create ${path7.relative(rootDir, file.path)}`));
532
+ created++;
533
+ }
413
534
  }
414
535
  console.log("");
415
- const parts = [];
416
- if (errorCount > 0) {
417
- parts.push(chalk4.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`));
418
- }
419
- if (warningCount > 0) {
420
- parts.push(chalk4.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`));
421
- }
422
- if (fixableCount > 0) {
423
- parts.push(chalk4.cyan(`${fixableCount} fixable`));
424
- }
425
- console.log(parts.join(", ") + "\n");
536
+ console.log(
537
+ formatSuccess(
538
+ `Initialized ContextKit project: ${created} file(s) created, ${skipped} skipped.`
539
+ )
540
+ );
541
+ console.log("");
542
+ console.log(chalk8.gray("Next steps:"));
543
+ console.log(chalk8.gray(" 1. Edit the example files in context/"));
544
+ console.log(chalk8.gray(" 2. Run: context lint"));
545
+ console.log(chalk8.gray(" 3. Run: context build"));
426
546
  } catch (err) {
427
- console.error(chalk4.red(`Build error: ${err.message}
428
- `));
547
+ console.error(formatError(err.message));
548
+ process.exit(1);
429
549
  }
430
- }
550
+ });
431
551
 
432
552
  // src/commands/site.ts
433
- import { readFile } from "fs/promises";
434
- import { resolve } from "path";
435
- import { Command as Command7 } from "commander";
436
- import { generateSite } from "@runcontext/site";
437
- var siteCommand = new Command7("site").description("Site generator commands");
438
- siteCommand.command("build").description("Build the context documentation site").option("--manifest <path>", "Path to manifest file", "dist/context.manifest.json").option("--output <dir>", "Output directory", "dist/site").option("--title <title>", "Site title").option("--base-path <path>", "Base path for links (e.g., /docs)").action(async (opts) => {
439
- const manifestPath = resolve(opts.manifest);
440
- let manifestJson;
441
- try {
442
- manifestJson = await readFile(manifestPath, "utf-8");
443
- } catch {
444
- console.error(`Error: Could not read manifest file at ${manifestPath}`);
445
- console.error('Run "context build" first to generate the manifest.');
446
- process.exitCode = 1;
447
- return;
448
- }
449
- let manifest;
553
+ import { Command as Command8 } from "commander";
554
+ import chalk9 from "chalk";
555
+ import path8 from "path";
556
+ import { compile as compile7, loadConfig as loadConfig7, emitManifest as emitManifest2 } from "@runcontext/core";
557
+ var siteCommand = new Command8("site").description("Build a static documentation site from compiled context").option("--context-dir <path>", "Path to context directory").option("--output-dir <path>", "Path to site output directory").action(async (opts) => {
450
558
  try {
451
- manifest = JSON.parse(manifestJson);
452
- } catch {
453
- console.error(`Error: Invalid JSON in manifest file at ${manifestPath}`);
454
- process.exitCode = 1;
455
- return;
559
+ const config = loadConfig7(process.cwd());
560
+ const contextDir = opts.contextDir ? path8.resolve(opts.contextDir) : path8.resolve(config.context_dir);
561
+ const { graph } = await compile7({ contextDir, config });
562
+ const manifest = emitManifest2(graph, config);
563
+ let buildSite;
564
+ try {
565
+ const siteModule = await import("@runcontext/site");
566
+ buildSite = siteModule.buildSite;
567
+ } catch {
568
+ }
569
+ if (!buildSite) {
570
+ console.log(
571
+ chalk9.yellow(
572
+ "Site generator is not yet available. Install @runcontext/site to enable this command."
573
+ )
574
+ );
575
+ process.exit(0);
576
+ }
577
+ const outputDir = opts.outputDir ? path8.resolve(opts.outputDir) : path8.resolve(config.site?.base_path ?? "site");
578
+ await buildSite(manifest, config, outputDir);
579
+ console.log(chalk9.green(`Site built to ${outputDir}`));
580
+ } catch (err) {
581
+ console.error(formatError(err.message));
582
+ process.exit(1);
456
583
  }
457
- const outputDir = resolve(opts.output);
458
- console.log(`Building site from ${manifestPath}...`);
459
- await generateSite({
460
- manifest,
461
- outputDir,
462
- title: opts.title,
463
- basePath: opts.basePath
464
- });
465
- console.log(`Site generated at ${outputDir}`);
466
584
  });
467
585
 
468
586
  // src/commands/serve.ts
469
- import { Command as Command8 } from "commander";
470
- import fs5 from "fs";
471
- import path7 from "path";
472
- var serveCommand = new Command8("serve").description("Start the MCP server").option("--stdio", "Use stdio transport (default)").option("--http <port>", "Use HTTP/SSE transport on the given port", parseInt).option("--manifest <path>", "Path to manifest file", "dist/context.manifest.json").action(async (opts) => {
587
+ import { Command as Command9 } from "commander";
588
+ import chalk10 from "chalk";
589
+ var serveCommand = new Command9("serve").description("Start the MCP server (stdio transport)").option("--context-dir <path>", "Path to context directory").action(async (opts) => {
473
590
  try {
474
- const manifestPath = path7.resolve(process.cwd(), opts.manifest);
475
- if (!fs5.existsSync(manifestPath)) {
476
- console.error(`Manifest not found: ${manifestPath}`);
477
- console.error('Run "context build" first to generate the manifest.');
591
+ let startServer;
592
+ try {
593
+ const mcpModule = await import("@runcontext/mcp");
594
+ startServer = mcpModule.startServer;
595
+ } catch {
596
+ }
597
+ if (!startServer) {
598
+ console.log(
599
+ chalk10.yellow(
600
+ "MCP server is not available. Install @runcontext/mcp to enable this command."
601
+ )
602
+ );
478
603
  process.exit(1);
479
604
  }
480
- const manifestData = JSON.parse(fs5.readFileSync(manifestPath, "utf-8"));
481
- const { createContextMcpServer } = await import("@runcontext/mcp");
482
- const server = createContextMcpServer(manifestData);
483
- if (opts.http) {
484
- const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
485
- const { createMcpExpressApp } = await import("@modelcontextprotocol/sdk/server/express.js");
486
- const { randomUUID } = await import("crypto");
487
- const app = createMcpExpressApp();
488
- const transport = new StreamableHTTPServerTransport({
489
- sessionIdGenerator: () => randomUUID()
490
- });
491
- app.all("/mcp", (req, res) => {
492
- transport.handleRequest(req, res);
493
- });
494
- await server.connect(transport);
495
- const port = opts.http;
496
- app.listen(port, () => {
497
- console.log(`ContextKit MCP server listening on http://127.0.0.1:${port}/mcp`);
498
- });
605
+ console.log(chalk10.blue("Starting MCP server (stdio transport)..."));
606
+ await startServer({
607
+ contextDir: opts.contextDir,
608
+ rootDir: process.cwd()
609
+ });
610
+ } catch (err) {
611
+ console.error(formatError(err.message));
612
+ process.exit(1);
613
+ }
614
+ });
615
+
616
+ // src/commands/validate-osi.ts
617
+ import { Command as Command10 } from "commander";
618
+ import chalk11 from "chalk";
619
+ import path9 from "path";
620
+ import { parseFile, osiDocumentSchema } from "@runcontext/core";
621
+ var validateOsiCommand = new Command10("validate-osi").description("Validate a single OSI file against the schema").argument("<file>", "Path to the OSI YAML file").option("--format <type>", "Output format: pretty or json", "pretty").action(async (file, opts) => {
622
+ try {
623
+ const filePath = path9.resolve(file);
624
+ const parsed = await parseFile(filePath, "model");
625
+ const result = osiDocumentSchema.safeParse(parsed.data);
626
+ if (result.success) {
627
+ if (opts.format === "json") {
628
+ console.log(
629
+ formatJson({
630
+ valid: true,
631
+ file: filePath,
632
+ data: result.data
633
+ })
634
+ );
635
+ } else {
636
+ console.log(formatSuccess(`${filePath} is valid.`));
637
+ }
499
638
  } else {
500
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
501
- const transport = new StdioServerTransport();
502
- await server.connect(transport);
503
- console.error("ContextKit MCP server running on stdio");
639
+ const issues = result.error.issues.map((issue) => ({
640
+ path: issue.path.join("."),
641
+ message: issue.message
642
+ }));
643
+ if (opts.format === "json") {
644
+ console.log(
645
+ formatJson({
646
+ valid: false,
647
+ file: filePath,
648
+ issues
649
+ })
650
+ );
651
+ } else {
652
+ console.error(chalk11.red(`Validation failed for ${filePath}:`));
653
+ for (const issue of issues) {
654
+ console.error(chalk11.red(` ${issue.path}: ${issue.message}`));
655
+ }
656
+ }
657
+ process.exit(1);
504
658
  }
505
659
  } catch (err) {
506
- console.error("Failed to start MCP server:", err.message);
660
+ console.error(formatError(err.message));
507
661
  process.exit(1);
508
662
  }
509
663
  });
510
664
 
511
665
  // src/index.ts
512
- var program = new Command9();
513
- program.name("context").version("0.1.0").description("ContextKit \u2014 Git-native institutional context compiler");
514
- program.addCommand(buildCommand);
666
+ var program = new Command11();
667
+ program.name("context").description("ContextKit \u2014 AI-ready metadata governance over OSI").version("0.2.0");
515
668
  program.addCommand(lintCommand);
516
- program.addCommand(initCommand);
669
+ program.addCommand(buildCommand);
670
+ program.addCommand(tierCommand);
517
671
  program.addCommand(explainCommand);
518
672
  program.addCommand(fixCommand);
519
673
  program.addCommand(devCommand);
674
+ program.addCommand(initCommand);
520
675
  program.addCommand(siteCommand);
521
676
  program.addCommand(serveCommand);
522
- await program.parseAsync(process.argv);
677
+ program.addCommand(validateOsiCommand);
678
+ program.parse();
523
679
  //# sourceMappingURL=index.js.map