@ez-corp/ez-context 0.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/dist/cli.js ADDED
@@ -0,0 +1,597 @@
1
+ #!/usr/bin/env node
2
+ import { a as writeWithMarkers, i as MARKER_START, n as emit, o as extractConventions, r as MARKER_END, s as createBridge, t as FORMAT_EMITTER_MAP } from "./emitters-D6bP4xWs.js";
3
+ import { copyFile, readFile, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { existsSync } from "node:fs";
6
+ import { Command } from "commander";
7
+ import ora from "ora";
8
+ import chalk from "chalk";
9
+
10
+ //#region src/commands/generate.ts
11
+ const DRY_RUN_PREVIEW_LINES = 20;
12
+ const VALID_FORMATS = [
13
+ "claude",
14
+ "agents",
15
+ "cursor",
16
+ "copilot",
17
+ "skills",
18
+ "rulesync",
19
+ "ruler"
20
+ ];
21
+ function parseFormats(raw) {
22
+ const formats = [...new Set(raw.split(",").map((s) => s.trim()).filter(Boolean))];
23
+ const invalid = formats.filter((f) => !VALID_FORMATS.includes(f));
24
+ if (invalid.length > 0) throw new Error(`Invalid format(s): ${invalid.join(", ")}. Valid: ${VALID_FORMATS.join(", ")}`);
25
+ return formats;
26
+ }
27
+ function truncatePreview(content) {
28
+ const lines = content.split("\n");
29
+ if (lines.length <= DRY_RUN_PREVIEW_LINES) return content;
30
+ return `${lines.slice(0, DRY_RUN_PREVIEW_LINES).join("\n")}\n... (${lines.length} lines total)`;
31
+ }
32
+ async function generateAction(pathArg, options) {
33
+ const projectPath = path.resolve(pathArg);
34
+ const spinner = ora("Analyzing project conventions...").start();
35
+ try {
36
+ const registry = await extractConventions(projectPath);
37
+ const conventionCount = registry.conventions.length;
38
+ spinner.succeed(`Found ${conventionCount} convention${conventionCount === 1 ? "" : "s"}`);
39
+ const confidenceThreshold = parseFloat(options.threshold ?? "0.7");
40
+ const outputDir = path.resolve(options.output ?? ".");
41
+ const formats = parseFormats(options.format ?? "claude,agents");
42
+ const emitOptions = {
43
+ outputDir,
44
+ confidenceThreshold,
45
+ dryRun: options.dryRun ?? false,
46
+ formats
47
+ };
48
+ const genSpinner = ora(formats.length === 2 && formats.includes("claude") && formats.includes("agents") ? "Generating context files..." : `Generating ${formats.length} context file${formats.length === 1 ? "" : "s"}...`).start();
49
+ const result = await emit(registry, emitOptions);
50
+ if (options.dryRun) {
51
+ genSpinner.succeed("Dry run complete");
52
+ console.log();
53
+ console.log(chalk.bold.yellow("╔══════════════════════════════════════╗"));
54
+ console.log(chalk.bold.yellow("║ DRY RUN -- no files will be written ║"));
55
+ console.log(chalk.bold.yellow("╚══════════════════════════════════════╝"));
56
+ console.log();
57
+ for (const [format, content] of Object.entries(result.rendered)) {
58
+ console.log(chalk.cyan(`--- ${format.toUpperCase()} ---`));
59
+ console.log(truncatePreview(content));
60
+ console.log();
61
+ }
62
+ } else {
63
+ genSpinner.succeed(`Generated ${result.filesWritten.length} file${result.filesWritten.length === 1 ? "" : "s"}`);
64
+ console.log();
65
+ console.log(chalk.bold.green("Generated files:"));
66
+ for (const filePath of result.filesWritten) {
67
+ const relPath = path.relative(outputDir, filePath);
68
+ console.log(` ${chalk.cyan(relPath)}`);
69
+ }
70
+ }
71
+ } catch (err) {
72
+ spinner.fail("Analysis failed");
73
+ const message = err instanceof Error ? err.message : String(err);
74
+ console.error(chalk.red(message));
75
+ process.exit(1);
76
+ }
77
+ }
78
+
79
+ //#endregion
80
+ //#region src/commands/inspect.ts
81
+ function confidenceDot(confidence) {
82
+ if (confidence >= .8) return chalk.green("●");
83
+ if (confidence >= .6) return chalk.yellow("●");
84
+ return chalk.red("●");
85
+ }
86
+ async function inspectAction(pathArg, options) {
87
+ const projectPath = path.resolve(pathArg);
88
+ const spinner = ora("Analyzing project conventions...").start();
89
+ try {
90
+ const registry = await extractConventions(projectPath);
91
+ const totalCount = registry.conventions.length;
92
+ spinner.succeed(`Extracted ${totalCount} convention${totalCount === 1 ? "" : "s"}`);
93
+ const threshold = parseFloat(options.threshold ?? "0.7");
94
+ const filtered = registry.conventions.filter((c) => c.confidence >= threshold);
95
+ if (filtered.length === 0) {
96
+ console.log(chalk.yellow(`\nNo conventions found above ${threshold} confidence threshold. Try lowering --threshold.`));
97
+ return;
98
+ }
99
+ const byCategory = /* @__PURE__ */ new Map();
100
+ for (const convention of filtered) {
101
+ const group = byCategory.get(convention.category) ?? [];
102
+ group.push(convention);
103
+ byCategory.set(convention.category, group);
104
+ }
105
+ console.log();
106
+ for (const [category, conventions] of byCategory) {
107
+ console.log(chalk.bold(category.toUpperCase()));
108
+ for (const convention of conventions) {
109
+ const pct = Math.round(convention.confidence * 100);
110
+ console.log(` ${confidenceDot(convention.confidence)} ${convention.pattern} ${chalk.gray(`(${pct}%)`)}`);
111
+ }
112
+ console.log();
113
+ }
114
+ const categoryCount = byCategory.size;
115
+ console.log(chalk.gray(`Found ${filtered.length} convention${filtered.length === 1 ? "" : "s"} across ${categoryCount} categor${categoryCount === 1 ? "y" : "ies"} (threshold: ${threshold})`));
116
+ } catch (err) {
117
+ spinner.fail("Analysis failed");
118
+ const message = err instanceof Error ? err.message : String(err);
119
+ console.error(chalk.red(message));
120
+ process.exit(1);
121
+ }
122
+ }
123
+
124
+ //#endregion
125
+ //#region src/core/drift/claim-extractor.ts
126
+ /**
127
+ * Matches boilerplate key-value lines that are structural metadata, not
128
+ * behavioral claims. Applied AFTER bold/code stripping.
129
+ *
130
+ * Examples skipped:
131
+ * "Language: TypeScript"
132
+ * "Package Manager: bun"
133
+ */
134
+ const BOILERPLATE_VALUE = /^(Language|Framework|Build|Package Manager|Test Runner|Pattern|Layers):\s/i;
135
+ /**
136
+ * Extract all testable claims from a markdown string.
137
+ *
138
+ * @param content Raw markdown content of the context file
139
+ * @param sourceFile Path to the source file (stored on each claim)
140
+ * @returns Array of extracted claims, filtered and deduplicated
141
+ */
142
+ function extractClaims(content, sourceFile) {
143
+ const claims = [];
144
+ const lines = content.split("\n");
145
+ let currentSection = "";
146
+ for (let i = 0; i < lines.length; i++) {
147
+ const line = lines[i].trim();
148
+ const lineNum = i + 1;
149
+ if (!line) continue;
150
+ if (line.startsWith("<!--")) continue;
151
+ if (line.includes("ez-context:")) continue;
152
+ const heading = line.match(/^#{1,3}\s+(.+)/);
153
+ if (heading) {
154
+ currentSection = heading[1].trim();
155
+ continue;
156
+ }
157
+ const bullet = line.match(/^[-*+]\s+(.+)/);
158
+ const numbered = !bullet ? line.match(/^\d+\.\s+(.+)/) : null;
159
+ const rawText = bullet ? bullet[1] : numbered ? numbered[1] : null;
160
+ if (!rawText) continue;
161
+ const text = rawText.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").trim();
162
+ if (text.length < 10 || text.length > 300) continue;
163
+ if (BOILERPLATE_VALUE.test(text)) continue;
164
+ claims.push({
165
+ text,
166
+ sourceFile,
167
+ sourceLine: lineNum,
168
+ sourceSection: currentSection
169
+ });
170
+ }
171
+ return claims;
172
+ }
173
+
174
+ //#endregion
175
+ //#region src/core/drift/claim-scorer.ts
176
+ const GREEN_THRESHOLD = .65;
177
+ const YELLOW_THRESHOLD = .4;
178
+ const BATCH_SIZE = 10;
179
+ function chunk(arr, size) {
180
+ const chunks = [];
181
+ for (let i = 0; i < arr.length; i += size) chunks.push(arr.slice(i, i + size));
182
+ return chunks;
183
+ }
184
+ function classifyScore(score) {
185
+ if (score >= GREEN_THRESHOLD) return "GREEN";
186
+ if (score >= YELLOW_THRESHOLD) return "YELLOW";
187
+ return "RED";
188
+ }
189
+ async function scoreSingleClaim(claim, bridge) {
190
+ const evidence = await bridge.search(claim.text, { k: 5 });
191
+ const topScore = evidence.length > 0 ? evidence[0].score : 0;
192
+ return {
193
+ claim,
194
+ status: classifyScore(topScore),
195
+ score: topScore,
196
+ evidence
197
+ };
198
+ }
199
+ /**
200
+ * Score all claims by searching the code index in batches of BATCH_SIZE.
201
+ *
202
+ * @param claims Claims to score
203
+ * @param bridge EzSearchBridge instance bound to the project
204
+ * @param onProgress Optional callback fired after each batch: (done, total)
205
+ * @returns ScoredClaim[] in the same order as input claims
206
+ */
207
+ async function scoreClaims(claims, bridge, onProgress) {
208
+ const total = claims.length;
209
+ const batches = chunk(claims, BATCH_SIZE);
210
+ const results = [];
211
+ let completed = 0;
212
+ for (const batch of batches) {
213
+ const batchResults = await Promise.all(batch.map((claim) => scoreSingleClaim(claim, bridge)));
214
+ results.push(...batchResults);
215
+ completed += batch.length;
216
+ onProgress?.(completed, total);
217
+ }
218
+ return results;
219
+ }
220
+
221
+ //#endregion
222
+ //#region src/core/drift/report.ts
223
+ /**
224
+ * Compute the aggregate health score for a set of scored claims.
225
+ * Returns 100 for empty input (no claims = no drift).
226
+ */
227
+ function computeHealthScore(scoredClaims) {
228
+ if (scoredClaims.length === 0) return 100;
229
+ const mean = scoredClaims.reduce((sum, sc) => sum + sc.score, 0) / scoredClaims.length;
230
+ return Math.round(mean * 100);
231
+ }
232
+ /**
233
+ * Build a DriftReport from a source file path and its scored claims.
234
+ */
235
+ function buildDriftReport(sourceFile, scoredClaims) {
236
+ return {
237
+ sourceFile,
238
+ healthScore: computeHealthScore(scoredClaims),
239
+ scoredClaims
240
+ };
241
+ }
242
+ const STATUS_LABEL = {
243
+ GREEN: "Confirmed",
244
+ YELLOW: "Possibly Stale",
245
+ RED: "Contradicted"
246
+ };
247
+ /**
248
+ * Render a drift report as a readable markdown string.
249
+ *
250
+ * Layout:
251
+ * # Drift Report
252
+ * Health score, source file, claim count
253
+ *
254
+ * ## Confirmed (GREEN)
255
+ * - [GREEN] claim text (score: X.XX)
256
+ *
257
+ * ## Possibly Stale (YELLOW)
258
+ * - [YELLOW] claim text (score: X.XX)
259
+ * - file: chunk_preview
260
+ *
261
+ * ## Contradicted (RED)
262
+ * - [RED] claim text (score: X.XX)
263
+ * - file: chunk_preview
264
+ *
265
+ * Summary: X confirmed, Y possibly stale, Z contradicted
266
+ */
267
+ function renderDriftReport(report) {
268
+ const { sourceFile, healthScore, scoredClaims } = report;
269
+ const lines = [];
270
+ const green = scoredClaims.filter((sc) => sc.status === "GREEN");
271
+ const yellow = scoredClaims.filter((sc) => sc.status === "YELLOW");
272
+ const red = scoredClaims.filter((sc) => sc.status === "RED");
273
+ lines.push("# Drift Report");
274
+ lines.push("");
275
+ lines.push(`**Health Score:** ${healthScore}/100`);
276
+ lines.push(`**File:** ${sourceFile}`);
277
+ lines.push(`**Claims:** ${scoredClaims.length}`);
278
+ lines.push("");
279
+ const renderGroup = (group, status) => {
280
+ if (group.length === 0) return;
281
+ const label = STATUS_LABEL[status] ?? status;
282
+ lines.push(`## ${label} (${status})`);
283
+ lines.push("");
284
+ for (const sc of group) {
285
+ lines.push(`- [${sc.status}] ${sc.claim.text} (score: ${sc.score.toFixed(2)})`);
286
+ if (sc.status !== "GREEN") {
287
+ const topEvidence = sc.evidence.slice(0, 2);
288
+ for (const ev of topEvidence) {
289
+ const preview = ev.chunk.replace(/\s+/g, " ").trim().slice(0, 80);
290
+ lines.push(` - ${ev.file}: ${preview}`);
291
+ }
292
+ }
293
+ }
294
+ lines.push("");
295
+ };
296
+ renderGroup(green, "GREEN");
297
+ renderGroup(yellow, "YELLOW");
298
+ renderGroup(red, "RED");
299
+ lines.push(`Summary: ${green.length} confirmed, ${yellow.length} possibly stale, ${red.length} contradicted`);
300
+ return lines.join("\n");
301
+ }
302
+
303
+ //#endregion
304
+ //#region src/commands/drift.ts
305
+ const CANDIDATE_FILES = [
306
+ "CLAUDE.md",
307
+ "AGENTS.md",
308
+ ".cursorrules",
309
+ "CONTEXT.md"
310
+ ];
311
+ function healthColor(score) {
312
+ if (score >= 70) return chalk.green(String(score));
313
+ if (score >= 40) return chalk.yellow(String(score));
314
+ return chalk.red(String(score));
315
+ }
316
+ async function driftAction(pathArg, options) {
317
+ const projectPath = path.resolve(pathArg);
318
+ const spinner = ora("Loading context files...").start();
319
+ try {
320
+ const bridge = await createBridge(projectPath);
321
+ if (!await bridge.hasIndex(projectPath)) {
322
+ spinner.fail("No search index found");
323
+ console.error(chalk.red("Run 'ez-context generate' or 'ez-search index .' first to create an index."));
324
+ process.exit(1);
325
+ }
326
+ let filePaths;
327
+ if (options.file) filePaths = [path.resolve(projectPath, options.file)];
328
+ else filePaths = CANDIDATE_FILES.map((name) => path.join(projectPath, name)).filter((p) => existsSync(p));
329
+ if (filePaths.length === 0) {
330
+ spinner.fail("No context files found");
331
+ console.error(chalk.red("No CLAUDE.md, AGENTS.md, .cursorrules, or CONTEXT.md found. Use --file to specify one."));
332
+ process.exit(1);
333
+ }
334
+ const claimsByFile = /* @__PURE__ */ new Map();
335
+ for (const filePath of filePaths) {
336
+ const claims = extractClaims(await readFile(filePath, "utf-8"), filePath);
337
+ claimsByFile.set(filePath, claims);
338
+ }
339
+ const allClaims = [...claimsByFile.values()].flat();
340
+ spinner.text = `Analyzing ${allClaims.length} claims...`;
341
+ const scoredAll = await scoreClaims(allClaims, bridge, (done, total) => {
342
+ spinner.text = `Checking claim ${done}/${total}...`;
343
+ });
344
+ const reports = filePaths.map((filePath) => {
345
+ const fileClaims = claimsByFile.get(filePath) ?? [];
346
+ return buildDriftReport(filePath, scoredAll.filter((sc) => fileClaims.some((c) => c === sc.claim)));
347
+ });
348
+ const overallScore = computeHealthScore(scoredAll);
349
+ spinner.succeed(`Drift analysis complete — health score: ${healthColor(overallScore)}/100`);
350
+ console.log();
351
+ for (const report of reports) {
352
+ console.log(renderDriftReport(report));
353
+ console.log();
354
+ }
355
+ } catch (err) {
356
+ spinner.fail("Drift analysis failed");
357
+ const message = err instanceof Error ? err.message : String(err);
358
+ console.error(chalk.red(message));
359
+ process.exit(1);
360
+ }
361
+ }
362
+
363
+ //#endregion
364
+ //#region src/core/updater.ts
365
+ /**
366
+ * Updater — targeted regeneration engine for `ez-context update`.
367
+ *
368
+ * Orchestrates:
369
+ * 1. Marker validation (pre-flight check, markers strategy only)
370
+ * 2. Drift detection (skip GREEN files, markers strategy only)
371
+ * 3. File backup (before any write)
372
+ * 4. Re-rendering (via FORMAT_EMITTER_MAP)
373
+ * 5. Write-back (writeWithMarkers for markers strategy, writeFile for direct)
374
+ */
375
+ /**
376
+ * Pre-flight marker check for updateFile.
377
+ *
378
+ * Unlike writeWithMarkers (which silently appends on unpaired markers),
379
+ * validateMarkers rejects unpaired markers so updateFile can abort safely.
380
+ *
381
+ * Returns:
382
+ * - { valid: true, mode: "append" } — no markers, safe to append
383
+ * - { valid: true, mode: "splice", startIdx, endIdx } — well-formed pair
384
+ * - { valid: false, mode: "invalid", reason } — unpaired or inverted markers
385
+ */
386
+ function validateMarkers(content) {
387
+ const startIdx = content.indexOf(MARKER_START);
388
+ const endIdx = content.indexOf(MARKER_END);
389
+ const hasStart = startIdx !== -1;
390
+ const hasEnd = endIdx !== -1;
391
+ if (!hasStart && !hasEnd) return {
392
+ valid: true,
393
+ mode: "append"
394
+ };
395
+ if (hasStart && hasEnd) {
396
+ if (endIdx < startIdx) return {
397
+ valid: false,
398
+ mode: "invalid",
399
+ reason: "End marker appears before start marker (corrupted file)"
400
+ };
401
+ return {
402
+ valid: true,
403
+ mode: "splice",
404
+ startIdx,
405
+ endIdx
406
+ };
407
+ }
408
+ if (hasStart && !hasEnd) return {
409
+ valid: false,
410
+ mode: "invalid",
411
+ reason: "Unpaired ez-context marker: end marker missing"
412
+ };
413
+ return {
414
+ valid: false,
415
+ mode: "invalid",
416
+ reason: "Unpaired ez-context marker: start marker missing"
417
+ };
418
+ }
419
+ /**
420
+ * Copy filePath to filePath.bak and return the backup path.
421
+ * Returns null if the file does not exist.
422
+ * Overwrites any existing .bak silently (represents state before this run).
423
+ */
424
+ async function backupFile(filePath) {
425
+ if (!existsSync(filePath)) return null;
426
+ const backupPath = filePath + ".bak";
427
+ await copyFile(filePath, backupPath);
428
+ return backupPath;
429
+ }
430
+ /**
431
+ * Look up the FORMAT_EMITTER_MAP entry whose filename suffix matches filePath.
432
+ * Returns undefined if the file doesn't correspond to a known format.
433
+ */
434
+ function findFormatEntry(filePath) {
435
+ const normalized = path.normalize(filePath);
436
+ for (const entry of Object.values(FORMAT_EMITTER_MAP)) if (normalized.endsWith(path.normalize(entry.filename))) return entry;
437
+ }
438
+ /**
439
+ * Orchestrate drift detection and targeted re-rendering for a single file.
440
+ *
441
+ * The write strategy is determined by FORMAT_EMITTER_MAP:
442
+ * - "markers" strategy: drift detection + writeWithMarkers (default)
443
+ * - "direct" strategy: always regenerate + writeFile (full overwrite)
444
+ *
445
+ * Flow for markers strategy:
446
+ * 1. File existence check — skip if missing
447
+ * 2. Marker validation — abort on invalid markers
448
+ * 3. Drift check (splice mode only) — skip if all claims GREEN
449
+ * 4. Backup creation
450
+ * 5. Re-render + writeWithMarkers
451
+ *
452
+ * Flow for direct strategy:
453
+ * 1. File existence check — skip if missing
454
+ * 2. Backup creation
455
+ * 3. Re-render + writeFile (full overwrite)
456
+ *
457
+ * @param filePath Absolute path to the context file
458
+ * @param registry Pre-computed convention registry (NOT extracted per-file)
459
+ * @param bridge EzSearchBridge instance for drift scoring
460
+ * @param confidenceThreshold Confidence floor passed to the renderer (default 0.7)
461
+ */
462
+ async function updateFile(filePath, registry, bridge, confidenceThreshold = .7) {
463
+ if (!existsSync(filePath)) return {
464
+ filePath,
465
+ action: "skipped",
466
+ reason: "File does not exist"
467
+ };
468
+ const formatEntry = findFormatEntry(filePath);
469
+ const strategy = formatEntry?.strategy ?? "markers";
470
+ const render = formatEntry?.render ?? FORMAT_EMITTER_MAP.claude.render;
471
+ if (strategy === "direct") {
472
+ const backupPath = await backupFile(filePath) ?? void 0;
473
+ await writeFile(filePath, render(registry, confidenceThreshold), "utf-8");
474
+ return {
475
+ filePath,
476
+ action: "updated",
477
+ reason: "Re-rendered (direct strategy)",
478
+ backupPath
479
+ };
480
+ }
481
+ const content = await readFile(filePath, "utf-8");
482
+ const validation = validateMarkers(content);
483
+ if (!validation.valid) return {
484
+ filePath,
485
+ action: "aborted",
486
+ reason: validation.reason
487
+ };
488
+ if (validation.mode === "splice") {
489
+ const claims = extractClaims(content, filePath);
490
+ if (claims.length === 0) return {
491
+ filePath,
492
+ action: "skipped",
493
+ reason: "No drift detected"
494
+ };
495
+ if (!(await scoreClaims(claims, bridge)).some((s) => s.status !== "GREEN")) return {
496
+ filePath,
497
+ action: "skipped",
498
+ reason: "No drift detected"
499
+ };
500
+ }
501
+ const backupPath = await backupFile(filePath) ?? void 0;
502
+ await writeWithMarkers(filePath, render(registry, confidenceThreshold));
503
+ return {
504
+ filePath,
505
+ action: "updated",
506
+ reason: "Re-rendered drifted sections",
507
+ backupPath
508
+ };
509
+ }
510
+
511
+ //#endregion
512
+ //#region src/commands/update.ts
513
+ async function updateAction(pathArg, options) {
514
+ const projectPath = path.resolve(pathArg);
515
+ const spinner = ora("Checking for drift...").start();
516
+ try {
517
+ const bridge = await createBridge(projectPath);
518
+ if (!await bridge.hasIndex(projectPath)) {
519
+ spinner.fail("No search index found");
520
+ console.error(chalk.red("Run 'ez-context generate' or 'ez-search index .' first to create an index."));
521
+ process.exit(1);
522
+ }
523
+ let filePaths;
524
+ if (options.file) filePaths = [path.resolve(projectPath, options.file)];
525
+ else filePaths = Object.values(FORMAT_EMITTER_MAP).map((entry) => path.join(projectPath, entry.filename)).filter((p) => existsSync(p));
526
+ if (filePaths.length === 0) {
527
+ spinner.fail("No context files found");
528
+ console.error(chalk.red("No generated context files found. Run 'ez-context generate' first, or use --file to specify one."));
529
+ process.exit(1);
530
+ }
531
+ if (options.dryRun) {
532
+ spinner.succeed("Dry run complete");
533
+ console.log();
534
+ console.log(chalk.bold.yellow("╔══════════════════════════════════════╗"));
535
+ console.log(chalk.bold.yellow("║ DRY RUN -- no files will be written ║"));
536
+ console.log(chalk.bold.yellow("╚══════════════════════════════════════╝"));
537
+ console.log();
538
+ for (const filePath of filePaths) {
539
+ const basename = path.basename(filePath);
540
+ const { readFile } = await import("node:fs/promises");
541
+ const claims = extractClaims(await readFile(filePath, "utf-8"), filePath);
542
+ if (claims.length === 0) {
543
+ console.log(` ${chalk.gray("-")} ${basename} ${chalk.gray("(no claims to check)")}`);
544
+ continue;
545
+ }
546
+ if ((await scoreClaims(claims, bridge)).some((s) => s.status !== "GREEN")) console.log(` ${chalk.yellow("~")} Would update ${chalk.cyan(basename)}`);
547
+ else console.log(` ${chalk.gray("-")} Up to date: ${chalk.gray(basename)}`);
548
+ }
549
+ return;
550
+ }
551
+ spinner.text = "Extracting conventions...";
552
+ const registry = await extractConventions(projectPath);
553
+ const results = [];
554
+ for (const filePath of filePaths) {
555
+ spinner.text = `Updating ${path.basename(filePath)}...`;
556
+ const result = await updateFile(filePath, registry, bridge);
557
+ results.push(result);
558
+ }
559
+ const updated = results.filter((r) => r.action === "updated");
560
+ const aborted = results.filter((r) => r.action === "aborted");
561
+ if (updated.length === 0 && aborted.length === 0) spinner.succeed("All context files are up to date");
562
+ else if (updated.length > 0) spinner.succeed(`Updated ${updated.length} file${updated.length === 1 ? "" : "s"}`);
563
+ else spinner.fail("Update incomplete — some files could not be updated");
564
+ console.log();
565
+ for (const result of results) {
566
+ const basename = path.basename(result.filePath);
567
+ if (result.action === "updated") {
568
+ const backup = result.backupPath ? ` (backup: ${path.basename(result.backupPath)})` : "";
569
+ console.log(` ${chalk.green("✓")} ${chalk.cyan(basename)}${chalk.gray(backup)}`);
570
+ } else if (result.action === "skipped") console.log(` ${chalk.gray("-")} ${chalk.gray(basename)} ${chalk.gray(`(${result.reason})`)}`);
571
+ else console.log(` ${chalk.yellow("⚠")} ${chalk.yellow(basename)} ${chalk.yellow(`(${result.reason})`)}`);
572
+ }
573
+ if (aborted.length > 0) {
574
+ console.log();
575
+ console.log(chalk.yellow(`Warning: ${aborted.length} file${aborted.length === 1 ? "" : "s"} could not be updated due to marker issues.`));
576
+ }
577
+ } catch (err) {
578
+ spinner.fail("Update failed");
579
+ const message = err instanceof Error ? err.message : String(err);
580
+ console.error(chalk.red(message));
581
+ process.exit(1);
582
+ }
583
+ }
584
+
585
+ //#endregion
586
+ //#region src/cli.ts
587
+ const program = new Command();
588
+ program.name("ez-context").description("Generate AI context files from any project").version("0.1.0");
589
+ program.command("generate").description("Extract conventions and generate context files").argument("[path]", "project root to analyze", ".").option("--dry-run", "preview without writing files").option("-y, --yes", "non-interactive mode").option("--output <dir>", "output directory", ".").option("--threshold <number>", "confidence threshold 0-1", "0.7").option("--format <formats>", "output formats: claude,agents,cursor,copilot,skills,rulesync,ruler (comma-separated)", "claude,agents").action(generateAction);
590
+ program.command("inspect").description("Display detected conventions").argument("[path]", "project root to analyze", ".").option("--threshold <number>", "confidence threshold 0-1", "0.7").action(inspectAction);
591
+ program.command("drift").description("Check context files against code for semantic drift").argument("[path]", "project root to analyze", ".").option("--file <contextFile>", "specific context file to check").action(driftAction);
592
+ program.command("update").description("Update drifted sections in context files, preserving manual edits").argument("[path]", "project root to analyze", ".").option("--file <contextFile>", "specific context file to update").option("--dry-run", "preview changes without writing files").option("-y, --yes", "non-interactive mode").action(updateAction);
593
+ await program.parseAsync();
594
+
595
+ //#endregion
596
+ export { };
597
+ //# sourceMappingURL=cli.js.map