@kb-labs/impact-core 0.5.0 → 2.7.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.
@@ -0,0 +1,810 @@
1
+ import { defineCommand } from '@kb-labs/sdk';
2
+ import { existsSync, readFileSync, readdirSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+ import { execSync } from 'child_process';
5
+ import { DEFAULT_IMPACT_CONFIG } from '@kb-labs/impact-contracts';
6
+
7
+ // src/cli/commands/check.ts
8
+ function findWorkspaceRoot(cwd) {
9
+ let dir = resolve(process.cwd());
10
+ for (let i = 0; i < 10; i++) {
11
+ if (existsSync(join(dir, ".gitmodules")) || existsSync(join(dir, ".kb", "kb.config.json"))) {
12
+ return dir;
13
+ }
14
+ const parent = resolve(dir, "..");
15
+ if (parent === dir) {
16
+ break;
17
+ }
18
+ dir = parent;
19
+ }
20
+ throw new Error("Could not find workspace root (no .gitmodules or .kb/kb.config.json found)");
21
+ }
22
+ function listSubRepos(workspaceRoot) {
23
+ const gitmodulesPath = join(workspaceRoot, ".gitmodules");
24
+ if (existsSync(gitmodulesPath)) {
25
+ return parseSubReposFromGitmodules(gitmodulesPath, workspaceRoot);
26
+ }
27
+ return scanFlatLayout(workspaceRoot);
28
+ }
29
+ function parseSubReposFromGitmodules(gitmodulesPath, workspaceRoot) {
30
+ const repos = [];
31
+ try {
32
+ const content = readFileSync(gitmodulesPath, "utf-8");
33
+ const pathMatches = content.matchAll(/^\s*path\s*=\s*(.+)$/gm);
34
+ for (const match of pathMatches) {
35
+ const relPath = (match[1] ?? "").trim();
36
+ if (!relPath) {
37
+ continue;
38
+ }
39
+ const fullPath = join(workspaceRoot, relPath);
40
+ if (!existsSync(join(fullPath, ".git")) && !existsSync(join(fullPath, "package.json"))) {
41
+ continue;
42
+ }
43
+ const parts = relPath.split("/");
44
+ const name = parts.at(-1) ?? relPath;
45
+ const category = parts.length > 1 ? parts.slice(0, -1).join("/") : "";
46
+ repos.push({ path: relPath, category, name });
47
+ }
48
+ } catch {
49
+ }
50
+ return repos;
51
+ }
52
+ function scanFlatLayout(workspaceRoot) {
53
+ const repos = [];
54
+ try {
55
+ for (const entry of readdirSync(workspaceRoot, { withFileTypes: true })) {
56
+ if (!entry.isDirectory() || entry.name.startsWith(".") || entry.name === "node_modules") {
57
+ continue;
58
+ }
59
+ const fullPath = join(workspaceRoot, entry.name);
60
+ if (existsSync(join(fullPath, ".git")) || existsSync(join(fullPath, "package.json"))) {
61
+ repos.push({ path: entry.name, category: "", name: entry.name });
62
+ }
63
+ }
64
+ } catch {
65
+ }
66
+ return repos;
67
+ }
68
+ function git(cwd, args) {
69
+ try {
70
+ return execSync(`git ${args}`, { cwd, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
71
+ } catch {
72
+ return "";
73
+ }
74
+ }
75
+ function getSubmodulePointer(workspaceRoot, repoPath) {
76
+ const out = git(workspaceRoot, `ls-tree HEAD ${repoPath}`);
77
+ const parts = out.split(/\s+/);
78
+ return parts[2] || "";
79
+ }
80
+ function getActualHead(fullPath) {
81
+ return git(fullPath, "rev-parse HEAD");
82
+ }
83
+ function findPackagesInRepo(workspaceRoot, repo) {
84
+ const fullRepoPath = join(workspaceRoot, repo.path);
85
+ const packagesDir = join(fullRepoPath, "packages");
86
+ const results = [];
87
+ if (!existsSync(packagesDir)) {
88
+ const pkgJsonPath = join(fullRepoPath, "package.json");
89
+ if (!existsSync(pkgJsonPath)) {
90
+ return [];
91
+ }
92
+ try {
93
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
94
+ if (pkg.name?.startsWith("@kb-labs/")) {
95
+ results.push({ name: pkg.name, repo: repo.path, changedFiles: 1 });
96
+ }
97
+ } catch {
98
+ }
99
+ return results;
100
+ }
101
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
102
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
103
+ continue;
104
+ }
105
+ const pkgJsonPath = join(packagesDir, entry.name, "package.json");
106
+ if (!existsSync(pkgJsonPath)) {
107
+ continue;
108
+ }
109
+ try {
110
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
111
+ if (!pkg.name?.startsWith("@kb-labs/")) {
112
+ continue;
113
+ }
114
+ const pointerSha = getSubmodulePointer(workspaceRoot, repo.path);
115
+ let changedFiles = 0;
116
+ if (pointerSha) {
117
+ const diff = git(fullRepoPath, `diff ${pointerSha}..HEAD --name-only -- packages/${entry.name}/src/`);
118
+ changedFiles = diff ? diff.split("\n").filter(Boolean).length : 0;
119
+ } else {
120
+ const status = git(fullRepoPath, `status --porcelain -- packages/${entry.name}/src/`);
121
+ changedFiles = status ? status.split("\n").filter(Boolean).length : 0;
122
+ }
123
+ if (changedFiles > 0) {
124
+ results.push({ name: pkg.name, repo: repo.path, changedFiles });
125
+ }
126
+ } catch {
127
+ }
128
+ }
129
+ return results;
130
+ }
131
+ function detectChangedPackages(workspaceRoot) {
132
+ const repos = listSubRepos(workspaceRoot);
133
+ const changed = [];
134
+ for (const repo of repos) {
135
+ const fullPath = join(workspaceRoot, repo.path);
136
+ if (!existsSync(join(fullPath, ".git"))) {
137
+ continue;
138
+ }
139
+ const pointerSha = getSubmodulePointer(workspaceRoot, repo.path);
140
+ const actualSha = getActualHead(fullPath);
141
+ if (pointerSha && pointerSha === actualSha) {
142
+ continue;
143
+ }
144
+ if (pointerSha === actualSha) {
145
+ const dirty = git(fullPath, "status --porcelain");
146
+ if (!dirty) {
147
+ continue;
148
+ }
149
+ }
150
+ const pkgs = findPackagesInRepo(workspaceRoot, repo);
151
+ changed.push(...pkgs);
152
+ }
153
+ return changed;
154
+ }
155
+ function discoverPackages(workspaceRoot) {
156
+ const repos = listSubRepos(workspaceRoot);
157
+ const packages = [];
158
+ for (const repo of repos) {
159
+ const fullRepoPath = join(workspaceRoot, repo.path);
160
+ const packagesDir = join(fullRepoPath, "packages");
161
+ const dirs = existsSync(packagesDir) ? readdirSync(packagesDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => join(packagesDir, d.name)) : [fullRepoPath];
162
+ for (const dir of dirs) {
163
+ const pkgPath = join(dir, "package.json");
164
+ if (!existsSync(pkgPath)) {
165
+ continue;
166
+ }
167
+ try {
168
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
169
+ if (!pkg.name?.startsWith("@kb-labs/")) {
170
+ continue;
171
+ }
172
+ const allDeps = { ...pkg.dependencies };
173
+ const kbDeps = Object.keys(allDeps).filter((d) => d.startsWith("@kb-labs/"));
174
+ packages.push({ name: pkg.name, repo: repo.path, deps: kbDeps });
175
+ } catch {
176
+ }
177
+ }
178
+ }
179
+ return packages;
180
+ }
181
+ function buildReverseDependencyGraph(workspaceRoot) {
182
+ const packages = discoverPackages(workspaceRoot);
183
+ const graph = /* @__PURE__ */ new Map();
184
+ for (const pkg of packages) {
185
+ graph.set(pkg.name, {
186
+ name: pkg.name,
187
+ repo: pkg.repo,
188
+ dependsOn: pkg.deps,
189
+ dependedBy: []
190
+ });
191
+ }
192
+ for (const pkg of packages) {
193
+ for (const dep of pkg.deps) {
194
+ const node = graph.get(dep);
195
+ if (node) {
196
+ node.dependedBy.push(pkg.name);
197
+ }
198
+ }
199
+ }
200
+ return graph;
201
+ }
202
+
203
+ // src/core/package-analyzer.ts
204
+ function analyzePackageImpact(changed, graph) {
205
+ const direct = [];
206
+ const dependent = [];
207
+ const transitive = [];
208
+ const changedNames = new Set(changed.map((c) => c.name));
209
+ const seen = /* @__PURE__ */ new Set();
210
+ for (const pkg of changed) {
211
+ direct.push({
212
+ name: pkg.name,
213
+ repo: pkg.repo,
214
+ level: "direct",
215
+ changedFiles: pkg.changedFiles
216
+ });
217
+ seen.add(pkg.name);
218
+ }
219
+ const queue = [];
220
+ for (const name of changedNames) {
221
+ const node = graph.get(name);
222
+ if (!node) {
223
+ continue;
224
+ }
225
+ for (const dep of node.dependedBy) {
226
+ if (!seen.has(dep)) {
227
+ queue.push({ name: dep, depth: 1 });
228
+ }
229
+ }
230
+ }
231
+ while (queue.length > 0) {
232
+ const { name, depth } = queue.shift();
233
+ if (seen.has(name)) {
234
+ continue;
235
+ }
236
+ seen.add(name);
237
+ const node = graph.get(name);
238
+ if (!node) {
239
+ continue;
240
+ }
241
+ const reasonPkg = node.dependsOn.find((d) => changedNames.has(d) || seen.has(d));
242
+ const impact = {
243
+ name: node.name,
244
+ repo: node.repo,
245
+ level: depth === 1 ? "dependent" : "transitive",
246
+ reason: reasonPkg ? `depends on ${reasonPkg}` : void 0
247
+ };
248
+ if (depth === 1) {
249
+ dependent.push(impact);
250
+ } else {
251
+ transitive.push(impact);
252
+ }
253
+ for (const dep of node.dependedBy) {
254
+ if (!seen.has(dep)) {
255
+ queue.push({ name: dep, depth: depth + 1 });
256
+ }
257
+ }
258
+ }
259
+ return { direct, dependent, transitive };
260
+ }
261
+ function generateRecommendations(packages, docs) {
262
+ const recs = [];
263
+ if (packages.dependent.length > 0) {
264
+ const names = packages.dependent.map((p) => p.name).join(", ");
265
+ recs.push(`Rebuild ${names}`);
266
+ }
267
+ const testRepos = /* @__PURE__ */ new Set([
268
+ ...packages.dependent.map((p) => p.repo),
269
+ ...packages.transitive.map((p) => p.repo)
270
+ ]);
271
+ if (testRepos.size > 0) {
272
+ recs.push(`Run tests in ${[...testRepos].join(", ")}`);
273
+ }
274
+ for (const doc of docs.stale) {
275
+ if (doc.file) {
276
+ recs.push(`Regenerate ${doc.file}`);
277
+ }
278
+ }
279
+ for (const doc of docs.review) {
280
+ if (doc.file) {
281
+ recs.push(`Review ${doc.file}`);
282
+ }
283
+ }
284
+ for (const doc of docs.reindex) {
285
+ if (doc.command) {
286
+ recs.push(`Run: ${doc.command}`);
287
+ }
288
+ }
289
+ return recs;
290
+ }
291
+
292
+ // src/core/doc-analyzer.ts
293
+ function matchesRule(packageName, pattern) {
294
+ if (pattern === packageName) {
295
+ return true;
296
+ }
297
+ if (pattern === "__new_package__") {
298
+ return false;
299
+ }
300
+ if (pattern.startsWith("*")) {
301
+ return packageName.endsWith(pattern.slice(1));
302
+ }
303
+ if (pattern.endsWith("*")) {
304
+ return packageName.startsWith(pattern.slice(0, -1));
305
+ }
306
+ return false;
307
+ }
308
+ function analyzeDocImpact(impactedPackages, config) {
309
+ const stale = [];
310
+ const review = [];
311
+ const reindex = [];
312
+ const seenDocs = /* @__PURE__ */ new Set();
313
+ for (const pkg of impactedPackages) {
314
+ for (const rule of config.docRules) {
315
+ if (!matchesRule(pkg.name, rule.match)) {
316
+ continue;
317
+ }
318
+ if (rule.action === "reindex") {
319
+ const key = `reindex:${rule.command ?? ""}`;
320
+ if (!seenDocs.has(key)) {
321
+ seenDocs.add(key);
322
+ reindex.push({
323
+ reason: `${pkg.name} changed`,
324
+ action: "reindex",
325
+ command: rule.command
326
+ });
327
+ }
328
+ continue;
329
+ }
330
+ for (const doc of rule.docs ?? []) {
331
+ const key = `${rule.action}:${doc}`;
332
+ if (seenDocs.has(key)) {
333
+ continue;
334
+ }
335
+ seenDocs.add(key);
336
+ const impact = {
337
+ file: doc,
338
+ reason: `${pkg.name} changed`,
339
+ action: rule.action,
340
+ command: rule.command
341
+ };
342
+ if (rule.action === "regenerate") {
343
+ stale.push(impact);
344
+ } else {
345
+ review.push(impact);
346
+ }
347
+ }
348
+ }
349
+ }
350
+ return { stale, review, reindex };
351
+ }
352
+ var TEST_DIRS = ["tests", "test", "__tests__"];
353
+ var TEST_PATTERNS = [".test.ts", ".spec.ts", ".test.tsx", ".spec.tsx"];
354
+ function findPackageDir(workspaceRoot, repo, packageName) {
355
+ const repoPath = join(workspaceRoot, repo);
356
+ const packagesDir = join(repoPath, "packages");
357
+ if (!existsSync(packagesDir)) {
358
+ return repoPath;
359
+ }
360
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) {
361
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
362
+ continue;
363
+ }
364
+ const pkgJsonPath = join(packagesDir, entry.name, "package.json");
365
+ if (!existsSync(pkgJsonPath)) {
366
+ continue;
367
+ }
368
+ try {
369
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8"));
370
+ if (pkg.name === packageName) {
371
+ return join(packagesDir, entry.name);
372
+ }
373
+ } catch {
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+ function countTestFiles(dir) {
379
+ let count = 0;
380
+ function walk(d) {
381
+ if (!existsSync(d)) {
382
+ return;
383
+ }
384
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
385
+ if (entry.name === "node_modules" || entry.name === "dist") {
386
+ continue;
387
+ }
388
+ const full = join(d, entry.name);
389
+ if (entry.isDirectory()) {
390
+ walk(full);
391
+ } else if (TEST_PATTERNS.some((p) => entry.name.endsWith(p))) {
392
+ count++;
393
+ }
394
+ }
395
+ }
396
+ walk(dir);
397
+ return count;
398
+ }
399
+ function detectTests(packageDir) {
400
+ for (const testDir of TEST_DIRS) {
401
+ const fullPath = join(packageDir, testDir);
402
+ if (existsSync(fullPath)) {
403
+ const count = countTestFiles(fullPath);
404
+ if (count > 0) {
405
+ return { hasTests: true, testCount: count };
406
+ }
407
+ }
408
+ }
409
+ const srcDir = join(packageDir, "src");
410
+ if (existsSync(srcDir)) {
411
+ const count = countTestFiles(srcDir);
412
+ if (count > 0) {
413
+ return { hasTests: true, testCount: count };
414
+ }
415
+ }
416
+ return { hasTests: false, testCount: 0 };
417
+ }
418
+ function analyzeTestImpact(allImpacted, workspaceRoot) {
419
+ const mustRun = [];
420
+ const noTests = [];
421
+ for (const pkg of allImpacted) {
422
+ const pkgDir = findPackageDir(workspaceRoot, pkg.repo, pkg.name);
423
+ const { hasTests, testCount } = pkgDir ? detectTests(pkgDir) : { hasTests: false, testCount: 0 };
424
+ const impact = {
425
+ name: pkg.name,
426
+ repo: pkg.repo,
427
+ level: pkg.level,
428
+ reason: pkg.reason,
429
+ hasTests,
430
+ testCount: hasTests ? testCount : void 0,
431
+ command: hasTests ? `pnpm --filter ${pkg.name} run test` : void 0
432
+ };
433
+ if (hasTests) {
434
+ mustRun.push(impact);
435
+ } else {
436
+ noTests.push(impact);
437
+ }
438
+ }
439
+ return { mustRun, noTests };
440
+ }
441
+
442
+ // src/core/build-analyzer.ts
443
+ function analyzeBuildImpact(allImpacted, graph) {
444
+ const impactedNames = new Set(allImpacted.map((p) => p.name));
445
+ const impactedMap = new Map(allImpacted.map((p) => [p.name, p]));
446
+ const inDegree = /* @__PURE__ */ new Map();
447
+ const deps = /* @__PURE__ */ new Map();
448
+ for (const name of impactedNames) {
449
+ const node = graph.get(name);
450
+ const filteredDeps = (node?.dependsOn ?? []).filter((d) => impactedNames.has(d));
451
+ deps.set(name, filteredDeps);
452
+ inDegree.set(name, filteredDeps.length);
453
+ }
454
+ const queue = [];
455
+ for (const [name, degree] of inDegree) {
456
+ if (degree === 0) {
457
+ queue.push(name);
458
+ }
459
+ }
460
+ const sorted = [];
461
+ while (queue.length > 0) {
462
+ const current = queue.shift();
463
+ sorted.push(current);
464
+ const node = graph.get(current);
465
+ if (node) {
466
+ for (const dependent of node.dependedBy) {
467
+ if (!impactedNames.has(dependent)) {
468
+ continue;
469
+ }
470
+ const deg = (inDegree.get(dependent) ?? 1) - 1;
471
+ inDegree.set(dependent, deg);
472
+ if (deg === 0) {
473
+ queue.push(dependent);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ for (const name of impactedNames) {
479
+ if (!sorted.includes(name)) {
480
+ sorted.push(name);
481
+ }
482
+ }
483
+ const steps = sorted.map((name, idx) => {
484
+ const pkg = impactedMap.get(name);
485
+ return {
486
+ name: pkg.name,
487
+ repo: pkg.repo,
488
+ level: pkg.level,
489
+ order: idx + 1,
490
+ reason: pkg.reason
491
+ };
492
+ });
493
+ const filterArgs = sorted.map((name) => `--filter ${name}`).join(" ");
494
+ const command = sorted.length > 0 ? `pnpm ${filterArgs} run build` : "";
495
+ return {
496
+ steps,
497
+ command,
498
+ totalPackages: steps.length
499
+ };
500
+ }
501
+ async function loadImpactRules(useConfigFn) {
502
+ if (useConfigFn) {
503
+ try {
504
+ const config = await useConfigFn();
505
+ if (config?.docRules?.length) {
506
+ return { docRules: config.docRules };
507
+ }
508
+ } catch {
509
+ }
510
+ }
511
+ return DEFAULT_IMPACT_CONFIG;
512
+ }
513
+
514
+ // src/core/formatter.ts
515
+ var BOLD = "\x1B[1m";
516
+ var DIM = "\x1B[2m";
517
+ var RESET = "\x1B[0m";
518
+ var RED = "\x1B[31m";
519
+ var YELLOW = "\x1B[33m";
520
+ var GREEN = "\x1B[32m";
521
+ var CYAN = "\x1B[36m";
522
+ function formatHumanReadable(result) {
523
+ const lines = [];
524
+ lines.push(`${BOLD}\u{1F4CA} Impact Analysis${RESET}`);
525
+ lines.push("");
526
+ const { direct, dependent, transitive } = result.packages;
527
+ const totalPkgs = direct.length + dependent.length + transitive.length;
528
+ if (totalPkgs > 0) {
529
+ lines.push(`${BOLD}\u{1F4E6} Package Impact${RESET}`);
530
+ if (direct.length > 0) {
531
+ lines.push(` ${GREEN}Direct (${direct.length}):${RESET}`);
532
+ for (const p of direct) {
533
+ lines.push(` ${p.name} ${DIM}(${p.repo})${RESET} \u2014 ${p.changedFiles} file${p.changedFiles === 1 ? "" : "s"} changed`);
534
+ }
535
+ }
536
+ if (dependent.length > 0) {
537
+ lines.push(` ${YELLOW}Dependent (${dependent.length}):${RESET}`);
538
+ for (const p of dependent) {
539
+ lines.push(` ${p.name} ${DIM}\u2190 ${p.reason}${RESET}`);
540
+ }
541
+ }
542
+ if (transitive.length > 0) {
543
+ lines.push(` ${DIM}Transitive (${transitive.length}):${RESET}`);
544
+ for (const p of transitive) {
545
+ lines.push(` ${p.name} ${DIM}\u2190 ${p.reason}${RESET}`);
546
+ }
547
+ }
548
+ lines.push("");
549
+ }
550
+ const { steps: buildSteps, command: buildCommand, totalPackages: buildTotal } = result.build;
551
+ if (buildTotal > 0) {
552
+ lines.push(`${BOLD}\u{1F528} Build Impact${RESET}`);
553
+ lines.push(` ${YELLOW}Rebuild (${buildTotal} packages in order):${RESET}`);
554
+ for (const b of buildSteps) {
555
+ const reason = b.level === "direct" ? "changed" : b.reason ?? "";
556
+ lines.push(` ${DIM}${b.order}.${RESET} ${b.name} ${DIM}\u2190 ${reason}${RESET}`);
557
+ }
558
+ if (buildCommand) {
559
+ lines.push("");
560
+ lines.push(` ${CYAN}\u2192 ${buildCommand}${RESET}`);
561
+ }
562
+ lines.push("");
563
+ }
564
+ const { stale, review, reindex } = result.docs;
565
+ const totalDocs = stale.length + review.length + reindex.length;
566
+ if (totalDocs > 0) {
567
+ lines.push(`${BOLD}\u{1F4C4} Doc Impact${RESET}`);
568
+ if (stale.length > 0) {
569
+ lines.push(` ${YELLOW}Stale (${stale.length}):${RESET}`);
570
+ for (const d of stale) {
571
+ lines.push(` ${d.file} \u2014 ${d.reason}`);
572
+ if (d.command) {
573
+ lines.push(` ${CYAN}\u2192 Run: ${d.command}${RESET}`);
574
+ }
575
+ }
576
+ }
577
+ if (review.length > 0) {
578
+ lines.push(` ${GREEN}Review (${review.length}):${RESET}`);
579
+ for (const d of review) {
580
+ lines.push(` ${d.file} \u2014 ${d.reason}`);
581
+ }
582
+ }
583
+ if (reindex.length > 0) {
584
+ lines.push(` ${CYAN}Reindex (${reindex.length}):${RESET}`);
585
+ for (const d of reindex) {
586
+ lines.push(` ${d.reason}`);
587
+ if (d.command) {
588
+ lines.push(` ${CYAN}\u2192 Run: ${d.command}${RESET}`);
589
+ }
590
+ }
591
+ }
592
+ lines.push("");
593
+ }
594
+ const { mustRun, noTests } = result.tests;
595
+ const totalTests = mustRun.length + noTests.length;
596
+ if (totalTests > 0) {
597
+ lines.push(`${BOLD}\u{1F9EA} Test Impact${RESET}`);
598
+ if (mustRun.length > 0) {
599
+ lines.push(` ${GREEN}Must run (${mustRun.length}):${RESET}`);
600
+ for (const t of mustRun) {
601
+ const count = t.testCount ? ` \u2014 ${t.testCount} test file${t.testCount === 1 ? "" : "s"}` : "";
602
+ const reason = t.level === "direct" ? "changed" : t.reason ?? "";
603
+ lines.push(` ${t.name}${count} ${DIM}\u2190 ${reason}${RESET}`);
604
+ if (t.command) {
605
+ lines.push(` ${CYAN}\u2192 ${t.command}${RESET}`);
606
+ }
607
+ }
608
+ }
609
+ if (noTests.length > 0) {
610
+ lines.push(` ${RED}\u26A0\uFE0F No tests (${noTests.length}):${RESET}`);
611
+ for (const t of noTests) {
612
+ const reason = t.level === "direct" ? "changed, NO TESTS" : `${t.reason}, NO TESTS`;
613
+ lines.push(` ${RED}${t.name}${RESET} ${DIM}\u2190 ${reason}${RESET}`);
614
+ }
615
+ }
616
+ lines.push("");
617
+ }
618
+ if (result.recommendations.length > 0) {
619
+ lines.push(`${BOLD}\u26A0\uFE0F Recommendations:${RESET}`);
620
+ for (const rec of result.recommendations) {
621
+ lines.push(` \u2022 ${rec}`);
622
+ }
623
+ lines.push("");
624
+ }
625
+ if (totalPkgs === 0 && totalDocs === 0 && totalTests === 0) {
626
+ lines.push(`${GREEN}\u2705 No impact detected.${RESET}`);
627
+ }
628
+ return lines.join("\n");
629
+ }
630
+
631
+ // src/cli/commands/check.ts
632
+ var EMPTY_RESULT = {
633
+ packages: { direct: [], dependent: [], transitive: [] },
634
+ docs: { stale: [], review: [], reindex: [] },
635
+ tests: { mustRun: [], noTests: [] },
636
+ build: { steps: [], command: "", totalPackages: 0 },
637
+ recommendations: []
638
+ };
639
+ var check_default = defineCommand({
640
+ id: "impact:check",
641
+ description: "Full impact analysis (packages + docs)",
642
+ handler: {
643
+ async execute(ctx, input) {
644
+ const flags = input.flags ?? input;
645
+ let root;
646
+ try {
647
+ root = findWorkspaceRoot();
648
+ } catch {
649
+ if (flags.json) {
650
+ ctx.ui?.json?.(EMPTY_RESULT);
651
+ } else {
652
+ ctx.ui?.warn?.("Could not find workspace root");
653
+ }
654
+ return { exitCode: 1 };
655
+ }
656
+ const changed = detectChangedPackages(root);
657
+ if (changed.length === 0) {
658
+ if (flags.json) {
659
+ ctx.ui?.json?.(EMPTY_RESULT);
660
+ } else {
661
+ ctx.ui?.success?.("No changes detected");
662
+ }
663
+ return { exitCode: 0, result: EMPTY_RESULT };
664
+ }
665
+ const graph = buildReverseDependencyGraph(root);
666
+ const packages = analyzePackageImpact(changed, graph);
667
+ const useConfigFn = async () => {
668
+ try {
669
+ const { useConfig } = await import('@kb-labs/sdk');
670
+ return await useConfig();
671
+ } catch {
672
+ return void 0;
673
+ }
674
+ };
675
+ const rules = await loadImpactRules(useConfigFn);
676
+ const allImpacted = [...packages.direct, ...packages.dependent, ...packages.transitive];
677
+ const docs = analyzeDocImpact(allImpacted, rules);
678
+ const tests = analyzeTestImpact(allImpacted, root);
679
+ const build = analyzeBuildImpact(allImpacted, graph);
680
+ const recommendations = generateRecommendations(packages, docs);
681
+ const result = { packages, docs, tests, build, recommendations };
682
+ if (flags.json) {
683
+ ctx.ui?.json?.(result);
684
+ } else {
685
+ ctx.ui?.write?.(formatHumanReadable(result));
686
+ }
687
+ return { exitCode: 0, result };
688
+ }
689
+ }
690
+ });
691
+ var packages_default = defineCommand({
692
+ id: "impact:packages",
693
+ description: "Package dependency impact analysis",
694
+ handler: {
695
+ async execute(ctx, input) {
696
+ const flags = input.flags ?? input;
697
+ let root;
698
+ try {
699
+ root = findWorkspaceRoot();
700
+ } catch {
701
+ if (flags.json) {
702
+ ctx.ui?.json?.({ packages: { direct: [], dependent: [], transitive: [] } });
703
+ } else {
704
+ ctx.ui?.warn?.("Could not find workspace root");
705
+ }
706
+ return { exitCode: 1 };
707
+ }
708
+ const changed = detectChangedPackages(root);
709
+ if (changed.length === 0) {
710
+ const empty = { packages: { direct: [], dependent: [], transitive: [] }, tests: { mustRun: [], noTests: [] }, recommendations: [] };
711
+ if (flags.json) {
712
+ ctx.ui?.json?.(empty);
713
+ } else {
714
+ ctx.ui?.success?.("No changes detected");
715
+ }
716
+ return { exitCode: 0, result: empty };
717
+ }
718
+ const graph = buildReverseDependencyGraph(root);
719
+ const packages = analyzePackageImpact(changed, graph);
720
+ const allImpacted = [...packages.direct, ...packages.dependent, ...packages.transitive];
721
+ const tests = analyzeTestImpact(allImpacted, root);
722
+ const build = analyzeBuildImpact(allImpacted, graph);
723
+ const recommendations = generateRecommendations(packages, { stale: [], review: [], reindex: [] });
724
+ const result = { packages, tests, build, recommendations };
725
+ if (flags.json) {
726
+ ctx.ui?.json?.(result);
727
+ } else {
728
+ ctx.ui?.write?.(formatHumanReadable({
729
+ packages,
730
+ docs: { stale: [], review: [], reindex: [] },
731
+ tests,
732
+ build,
733
+ recommendations
734
+ }));
735
+ }
736
+ return { exitCode: 0, result };
737
+ }
738
+ }
739
+ });
740
+ var docs_default = defineCommand({
741
+ id: "impact:docs",
742
+ description: "Documentation impact analysis",
743
+ handler: {
744
+ async execute(ctx, input) {
745
+ const flags = input.flags ?? input;
746
+ let root;
747
+ try {
748
+ root = findWorkspaceRoot();
749
+ } catch {
750
+ if (flags.json) {
751
+ ctx.ui?.json?.({ docs: { stale: [], review: [], reindex: [] } });
752
+ } else {
753
+ ctx.ui?.warn?.("Could not find workspace root");
754
+ }
755
+ return { exitCode: 1 };
756
+ }
757
+ const changed = detectChangedPackages(root);
758
+ if (changed.length === 0) {
759
+ const empty = { docs: { stale: [], review: [], reindex: [] }, recommendations: [] };
760
+ if (flags.json) {
761
+ ctx.ui?.json?.(empty);
762
+ } else {
763
+ ctx.ui?.success?.("No changes detected");
764
+ }
765
+ return { exitCode: 0, result: empty };
766
+ }
767
+ const graph = buildReverseDependencyGraph(root);
768
+ const packages = analyzePackageImpact(changed, graph);
769
+ const useConfigFn = async () => {
770
+ try {
771
+ const { useConfig } = await import('@kb-labs/sdk');
772
+ return await useConfig();
773
+ } catch {
774
+ return void 0;
775
+ }
776
+ };
777
+ const rules = await loadImpactRules(useConfigFn);
778
+ const allImpacted = [...packages.direct, ...packages.dependent];
779
+ const docs = analyzeDocImpact(allImpacted, rules);
780
+ const recommendations = [];
781
+ for (const d of docs.stale) {
782
+ if (d.command) {
783
+ recommendations.push(`Run: ${d.command}`);
784
+ }
785
+ }
786
+ for (const d of docs.reindex) {
787
+ if (d.command) {
788
+ recommendations.push(`Run: ${d.command}`);
789
+ }
790
+ }
791
+ const result = { docs, recommendations };
792
+ if (flags.json) {
793
+ ctx.ui?.json?.(result);
794
+ } else {
795
+ ctx.ui?.write?.(formatHumanReadable({
796
+ packages: { direct: [], dependent: [], transitive: [] },
797
+ docs,
798
+ tests: { mustRun: [], noTests: [] },
799
+ build: { steps: [], command: "", totalPackages: 0 },
800
+ recommendations
801
+ }));
802
+ }
803
+ return { exitCode: 0, result };
804
+ }
805
+ }
806
+ });
807
+
808
+ export { check_default as checkCommand, docs_default as docsCommand, packages_default as packagesCommand };
809
+ //# sourceMappingURL=index.js.map
810
+ //# sourceMappingURL=index.js.map