aislop 0.1.3 → 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/cli.js CHANGED
@@ -872,6 +872,7 @@ const JS_EXTENSIONS = new Set([
872
872
  ".cjs"
873
873
  ]);
874
874
  const PY_EXTENSIONS = new Set([".py"]);
875
+ const REMOVE_MARKER = "\0__AISLOP_REMOVE__";
875
876
  const extractJsImportedSymbols = (lines) => {
876
877
  const symbols = [];
877
878
  const importLines = /* @__PURE__ */ new Set();
@@ -980,32 +981,47 @@ const isSymbolUsed = (name, content, importLines, lines) => {
980
981
  }
981
982
  return false;
982
983
  };
984
+ const analyzeFile = (filePath) => {
985
+ if (isAutoGenerated(filePath)) return null;
986
+ let content;
987
+ try {
988
+ content = fs.readFileSync(filePath, "utf-8");
989
+ } catch {
990
+ return null;
991
+ }
992
+ const ext = path.extname(filePath);
993
+ const lines = content.split("\n");
994
+ let symbols;
995
+ let importLines;
996
+ if (JS_EXTENSIONS.has(ext)) {
997
+ const result = extractJsImportedSymbols(lines);
998
+ symbols = result.symbols;
999
+ importLines = result.importLines;
1000
+ } else if (PY_EXTENSIONS.has(ext)) {
1001
+ const result = extractPyImportedSymbols(lines);
1002
+ symbols = result.symbols;
1003
+ importLines = result.importLines;
1004
+ } else return null;
1005
+ return {
1006
+ lines,
1007
+ symbols,
1008
+ importLines,
1009
+ ext
1010
+ };
1011
+ };
1012
+ const getUnusedSymbols = (lines, symbols, importLines) => {
1013
+ const content = lines.join("\n");
1014
+ return symbols.filter((symbol) => !isSymbolUsed(symbol.name, content, importLines, lines));
1015
+ };
983
1016
  const detectUnusedImports = async (context) => {
984
1017
  const files = getSourceFiles(context);
985
1018
  const diagnostics = [];
986
1019
  for (const filePath of files) {
987
- if (isAutoGenerated(filePath)) continue;
988
- let content;
989
- try {
990
- content = fs.readFileSync(filePath, "utf-8");
991
- } catch {
992
- continue;
993
- }
994
- const ext = path.extname(filePath);
1020
+ const analysis = analyzeFile(filePath);
1021
+ if (!analysis) continue;
995
1022
  const relativePath = path.relative(context.rootDirectory, filePath);
996
- const lines = content.split("\n");
997
- let symbols;
998
- let importLinesSet;
999
- if (JS_EXTENSIONS.has(ext)) {
1000
- const result = extractJsImportedSymbols(lines);
1001
- symbols = result.symbols;
1002
- importLinesSet = result.importLines;
1003
- } else if (PY_EXTENSIONS.has(ext)) {
1004
- const result = extractPyImportedSymbols(lines);
1005
- symbols = result.symbols;
1006
- importLinesSet = result.importLines;
1007
- } else continue;
1008
- for (const symbol of symbols) if (!isSymbolUsed(symbol.name, content, importLinesSet, lines)) diagnostics.push({
1023
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
1024
+ for (const symbol of unused) diagnostics.push({
1009
1025
  filePath: relativePath,
1010
1026
  engine: "ai-slop",
1011
1027
  rule: "ai-slop/unused-import",
@@ -1643,13 +1659,41 @@ const isToolInstalled = async (tool) => {
1643
1659
  //#region src/engines/code-quality/knip.ts
1644
1660
  const KNIP_MESSAGE_MAP = {
1645
1661
  files: "Unused file",
1662
+ dependencies: "Unused dependency",
1663
+ devDependencies: "Unused devDependency",
1664
+ unlisted: "Unlisted dependency",
1665
+ unresolved: "Unresolved import",
1666
+ binaries: "Unlisted binary",
1646
1667
  exports: "Unused export",
1647
1668
  types: "Unused type",
1648
1669
  duplicates: "Duplicate export"
1649
1670
  };
1671
+ const DEPENDENCY_TYPES = [
1672
+ "dependencies",
1673
+ "devDependencies",
1674
+ "unlisted",
1675
+ "unresolved",
1676
+ "binaries"
1677
+ ];
1678
+ const isDependencyType = (type) => DEPENDENCY_TYPES.includes(type);
1679
+ const getIssueItems = (fileIssue, issueType) => {
1680
+ const items = fileIssue[issueType];
1681
+ return Array.isArray(items) ? items : [];
1682
+ };
1683
+ const DEPENDENCY_HELP = {
1684
+ dependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
1685
+ devDependencies: "This package is listed in package.json but not imported anywhere. Remove it with `npm uninstall` or `aislop fix`.",
1686
+ unlisted: "This package is imported in code but not declared in package.json. Run `npm install` to add it.",
1687
+ unresolved: "This import cannot be resolved. Check for typos or missing packages.",
1688
+ binaries: "This binary is used but its package is not in package.json."
1689
+ };
1650
1690
  const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
1651
1691
  const diagnostics = [];
1652
- const issues = issueType === "exports" ? fileIssue.exports ?? [] : issueType === "types" ? fileIssue.types ?? [] : fileIssue.duplicates ?? [];
1692
+ const issues = getIssueItems(fileIssue, issueType);
1693
+ const category = isDependencyType(issueType) ? "Dependencies" : "Dead Code";
1694
+ const severity = issueType === "unlisted" || issueType === "unresolved" ? "error" : "warning";
1695
+ const fixable = issueType === "dependencies" || issueType === "devDependencies";
1696
+ const help = DEPENDENCY_HELP[issueType] ?? "";
1653
1697
  for (const issue of issues) {
1654
1698
  const symbol = issue.name ?? issue.symbol ?? "unknown";
1655
1699
  const absolutePath = path.resolve(knipCwd, fileIssue.file);
@@ -1657,13 +1701,13 @@ const collectIssues = (fileIssue, issueType, rootDir, knipCwd) => {
1657
1701
  filePath: path.relative(rootDir, absolutePath),
1658
1702
  engine: "code-quality",
1659
1703
  rule: `knip/${issueType}`,
1660
- severity: "warning",
1704
+ severity,
1661
1705
  message: `${KNIP_MESSAGE_MAP[issueType]}: ${symbol}`,
1662
- help: "",
1706
+ help,
1663
1707
  line: issue.line ?? 0,
1664
1708
  column: issue.col ?? 0,
1665
- category: "Dead Code",
1666
- fixable: false
1709
+ category,
1710
+ fixable
1667
1711
  });
1668
1712
  }
1669
1713
  return diagnostics;
@@ -1697,6 +1741,38 @@ const findKnipBin = (rootDirectory, monorepoRoot) => {
1697
1741
  }
1698
1742
  return null;
1699
1743
  };
1744
+ const runKnipDependencyCheck = async (rootDirectory) => {
1745
+ return (await runKnip(rootDirectory)).filter((d) => d.rule === "knip/dependencies" || d.rule === "knip/devDependencies");
1746
+ };
1747
+ const fixUnusedDependencies = async (rootDirectory) => {
1748
+ const diagnostics = await runKnipDependencyCheck(rootDirectory);
1749
+ if (diagnostics.length === 0) return;
1750
+ const pkgPath = path.join(rootDirectory, "package.json");
1751
+ if (!fs.existsSync(pkgPath)) return;
1752
+ const raw = fs.readFileSync(pkgPath, "utf-8");
1753
+ const pkg = JSON.parse(raw);
1754
+ const unusedDeps = /* @__PURE__ */ new Set();
1755
+ const unusedDevDeps = /* @__PURE__ */ new Set();
1756
+ for (const d of diagnostics) {
1757
+ const pkgName = d.message.replace(/^Unused (dev)?[Dd]ependency: /, "");
1758
+ if (d.rule === "knip/dependencies") unusedDeps.add(pkgName);
1759
+ if (d.rule === "knip/devDependencies") unusedDevDeps.add(pkgName);
1760
+ }
1761
+ let changed = false;
1762
+ if (pkg.dependencies) {
1763
+ for (const name of unusedDeps) if (name in pkg.dependencies) {
1764
+ delete pkg.dependencies[name];
1765
+ changed = true;
1766
+ }
1767
+ }
1768
+ if (pkg.devDependencies) {
1769
+ for (const name of unusedDevDeps) if (name in pkg.devDependencies) {
1770
+ delete pkg.devDependencies[name];
1771
+ changed = true;
1772
+ }
1773
+ }
1774
+ if (changed) fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, " ")}\n`);
1775
+ };
1700
1776
  const runKnip = async (rootDirectory) => {
1701
1777
  const knipRuntime = findKnipBin(rootDirectory, findMonorepoRoot(rootDirectory));
1702
1778
  if (!knipRuntime) return [];
@@ -1730,11 +1806,13 @@ const runKnip = async (rootDirectory) => {
1730
1806
  fixable: false
1731
1807
  });
1732
1808
  const issues = parsed.issues ?? [];
1733
- for (const fileIssue of issues) for (const type of [
1809
+ const issueTypes = [
1810
+ ...DEPENDENCY_TYPES,
1734
1811
  "exports",
1735
1812
  "types",
1736
1813
  "duplicates"
1737
- ]) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
1814
+ ];
1815
+ for (const fileIssue of issues) for (const type of issueTypes) diagnostics.push(...collectIssues(fileIssue, type, rootDirectory, knipRuntime.cwd));
1738
1816
  return diagnostics;
1739
1817
  } catch {
1740
1818
  return [];
@@ -1860,14 +1938,11 @@ const parseBiomeJsonOutput = (output, rootDir) => {
1860
1938
  const fixBiomeFormat = async (context) => {
1861
1939
  const targets = getBiomeTargets(context);
1862
1940
  if (targets.length === 0) return;
1863
- const result = await runBiome([
1864
- "check",
1941
+ await runBiome([
1942
+ "format",
1865
1943
  "--write",
1866
- "--formatter-enabled=true",
1867
- "--linter-enabled=false",
1868
1944
  ...targets
1869
1945
  ], context.rootDirectory, 6e4);
1870
- if (result.exitCode !== 0) throw new Error(result.stderr || result.stdout || `Biome exited with code ${result.exitCode}`);
1871
1946
  };
1872
1947
 
1873
1948
  //#endregion
@@ -2430,6 +2505,7 @@ const fixOxlint = async (context) => {
2430
2505
  "-c",
2431
2506
  configPath,
2432
2507
  "--fix",
2508
+ "--fix-suggestions",
2433
2509
  "."
2434
2510
  ];
2435
2511
  const result = await runSubprocess(process.execPath, args, {
@@ -3118,14 +3194,13 @@ const logger = {
3118
3194
  * Application version — injected at build time by tsdown from package.json.
3119
3195
  * The fallback should always match the "version" field in package.json.
3120
3196
  */
3121
- const APP_VERSION = "0.1.3";
3197
+ const APP_VERSION = "0.2.1";
3122
3198
 
3123
3199
  //#endregion
3124
3200
  //#region src/output/layout.ts
3125
3201
  const formatElapsed$1 = (elapsedMs) => elapsedMs < 1e3 ? `${Math.round(elapsedMs)}ms` : `${(elapsedMs / 1e3).toFixed(1)}s`;
3126
3202
  const printCommandHeader = (commandName) => {
3127
- logger.log(highlighter.bold(`aislop ${commandName}`));
3128
- logger.log(highlighter.dim(`v${APP_VERSION}`));
3203
+ logger.log(`${highlighter.bold(`aislop ${commandName.toLowerCase()}`)} ${highlighter.dim(`v${APP_VERSION}`)}`);
3129
3204
  logger.break();
3130
3205
  };
3131
3206
  const formatProjectSummary = (project) => `Project ${highlighter.info(project.projectName)} (${highlighter.info(project.languages.join(", "))})`;
@@ -3136,81 +3211,6 @@ const printProjectMetadata = (project) => {
3136
3211
  logger.break();
3137
3212
  };
3138
3213
 
3139
- //#endregion
3140
- //#region src/output/pager.ts
3141
- const DEFAULT_COLUMNS = 80;
3142
- const DEFAULT_ROWS = 24;
3143
- const ANSI_PATTERN = new RegExp(String.raw`\u001B\[[0-?]*[ -/]*[@-~]`, "g");
3144
- const stripAnsi = (text) => text.replace(ANSI_PATTERN, "");
3145
- const resolvePagerCommand = () => {
3146
- const pager = process.env.PAGER?.trim();
3147
- if (pager) {
3148
- const [command, ...args] = pager.split(/\s+/);
3149
- if (command) return {
3150
- command,
3151
- args
3152
- };
3153
- }
3154
- return {
3155
- command: "less",
3156
- args: [
3157
- "-R",
3158
- "-F",
3159
- "-X"
3160
- ]
3161
- };
3162
- };
3163
- const writeToStdout = (text) => {
3164
- process.stdout.write(text);
3165
- };
3166
- const pipeToPager = async (command, args, text) => new Promise((resolve) => {
3167
- let settled = false;
3168
- const finish = (success) => {
3169
- if (settled) return;
3170
- settled = true;
3171
- resolve(success);
3172
- };
3173
- try {
3174
- const child = spawn(command, args, {
3175
- stdio: [
3176
- "pipe",
3177
- "inherit",
3178
- "inherit"
3179
- ],
3180
- windowsHide: true
3181
- });
3182
- child.once("error", () => finish(false));
3183
- child.once("close", (code) => finish(code === 0));
3184
- child.stdin?.on("error", () => void 0);
3185
- child.stdin?.end(text);
3186
- } catch {
3187
- finish(false);
3188
- }
3189
- });
3190
- const countRenderedLines = (text, columns = DEFAULT_COLUMNS) => {
3191
- const width = Math.max(1, columns);
3192
- return text.split("\n").reduce((count, line) => {
3193
- const visibleLine = stripAnsi(line).replaceAll(" ", " ");
3194
- return count + Math.max(1, Math.ceil(visibleLine.length / width));
3195
- }, 0);
3196
- };
3197
- const shouldPageOutput = (text, options = {}) => {
3198
- if (text.trim().length === 0) return false;
3199
- const stdinIsTTY = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
3200
- const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
3201
- if (!stdinIsTTY || !stdoutIsTTY) return false;
3202
- const rows = Math.max(1, options.rows ?? process.stdout.rows ?? DEFAULT_ROWS);
3203
- return countRenderedLines(text, Math.max(1, options.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS)) > rows - 1;
3204
- };
3205
- const printMaybePaged = async (text) => {
3206
- if (!shouldPageOutput(text)) {
3207
- writeToStdout(text);
3208
- return;
3209
- }
3210
- const pager = resolvePagerCommand();
3211
- if (!await pipeToPager(pager.command, pager.args, text)) writeToStdout(text);
3212
- };
3213
-
3214
3214
  //#endregion
3215
3215
  //#region src/output/scan-progress.ts
3216
3216
  const SPINNER_FRAMES = [
@@ -3692,9 +3692,13 @@ const getAnonymousId = () => {
3692
3692
  for (let i = 0; i < raw.length; i++) hash = hash * 33 ^ raw.charCodeAt(i);
3693
3693
  return `aislop_${(hash >>> 0).toString(36)}`;
3694
3694
  };
3695
+ /** Pending telemetry request — kept alive so Node doesn't exit before it completes. */
3696
+ let pendingRequest = null;
3695
3697
  /**
3696
3698
  * Fire-and-forget telemetry event to PostHog.
3697
- * Never throws, never blocks, never affects CLI output or exit code.
3699
+ * Never throws, never blocks CLI output.
3700
+ * The request is kept alive via `flushTelemetry()` so Node doesn't
3701
+ * exit before it completes.
3698
3702
  */
3699
3703
  const trackEvent = (event) => {
3700
3704
  const payload = {
@@ -3717,12 +3721,23 @@ const trackEvent = (event) => {
3717
3721
  },
3718
3722
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3719
3723
  };
3720
- fetch(`${POSTHOG_HOST}/capture/`, {
3724
+ pendingRequest = fetch(`${POSTHOG_HOST}/capture/`, {
3721
3725
  method: "POST",
3722
3726
  headers: { "Content-Type": "application/json" },
3723
3727
  body: JSON.stringify(payload),
3724
3728
  signal: AbortSignal.timeout(3e3)
3725
- }).catch(() => {});
3729
+ }).then(() => {}).catch(() => {});
3730
+ };
3731
+ /**
3732
+ * Wait for any pending telemetry request to complete.
3733
+ * Call this before `process.exit()` to ensure the event is delivered.
3734
+ * Times out after 3 seconds so it never hangs the CLI.
3735
+ */
3736
+ const flushTelemetry = async () => {
3737
+ if (pendingRequest) {
3738
+ await pendingRequest;
3739
+ pendingRequest = null;
3740
+ }
3726
3741
  };
3727
3742
 
3728
3743
  //#endregion
@@ -3805,12 +3820,13 @@ const scanCommand = async (directory, config, options) => {
3805
3820
  console.log(JSON.stringify(jsonOut, null, 2));
3806
3821
  return { exitCode };
3807
3822
  }
3808
- await printMaybePaged([
3823
+ const output = [
3809
3824
  "",
3810
3825
  allDiagnostics.length === 0 ? `${highlighter.success(" ✓ No issues found.")}\n` : renderDiagnostics(allDiagnostics, options.verbose),
3811
3826
  renderSummary(allDiagnostics, scoreResult, elapsedMs, projectInfo.sourceFileCount, config.scoring.thresholds),
3812
3827
  ""
3813
- ].join("\n"));
3828
+ ].join("\n");
3829
+ process.stdout.write(output);
3814
3830
  return { exitCode };
3815
3831
  };
3816
3832
 
@@ -3960,6 +3976,99 @@ const doctorCommand = async (directory) => {
3960
3976
  printDoctorConclusion(isAllGood());
3961
3977
  };
3962
3978
 
3979
+ //#endregion
3980
+ //#region src/engines/ai-slop/unused-imports-fix.ts
3981
+ const fixUnusedImports = async (context) => {
3982
+ const files = getSourceFiles(context);
3983
+ for (const filePath of files) {
3984
+ const analysis = analyzeFile(filePath);
3985
+ if (!analysis) continue;
3986
+ const unused = getUnusedSymbols(analysis.lines, analysis.symbols, analysis.importLines);
3987
+ if (unused.length === 0) continue;
3988
+ const unusedNames = new Set(unused.map((u) => u.name));
3989
+ const lines = [...analysis.lines];
3990
+ const symbolsByLine = /* @__PURE__ */ new Map();
3991
+ for (const sym of analysis.symbols) {
3992
+ const arr = symbolsByLine.get(sym.line) ?? [];
3993
+ arr.push(sym);
3994
+ symbolsByLine.set(sym.line, arr);
3995
+ }
3996
+ const linesToRemove = /* @__PURE__ */ new Set();
3997
+ for (const [lineNo, syms] of symbolsByLine) {
3998
+ const lineIdx = lineNo - 1;
3999
+ const allUnused = syms.every((s) => unusedNames.has(s.name));
4000
+ const importSpan = getImportSpan(lineIdx, analysis.importLines);
4001
+ if (allUnused) for (const idx of importSpan) linesToRemove.add(idx);
4002
+ else if (JS_EXTENSIONS.has(analysis.ext)) rewriteJsImportSpan(lines, importSpan, syms, unusedNames);
4003
+ else if (PY_EXTENSIONS.has(analysis.ext)) rewritePyImportLine(lines, lineIdx, syms, unusedNames);
4004
+ }
4005
+ if (linesToRemove.size === 0 && unused.length === 0) continue;
4006
+ const sortedRemove = [...linesToRemove].sort((a, b) => b - a);
4007
+ for (const idx of sortedRemove) lines.splice(idx, 1);
4008
+ const filtered = lines.filter((l) => l !== REMOVE_MARKER);
4009
+ while (filtered.length > 0 && filtered[0].trim() === "") filtered.shift();
4010
+ fs.writeFileSync(filePath, filtered.join("\n"));
4011
+ }
4012
+ };
4013
+ const getImportSpan = (startIdx, importLines) => {
4014
+ const span = [startIdx];
4015
+ let idx = startIdx + 1;
4016
+ while (importLines.has(idx)) {
4017
+ span.push(idx);
4018
+ idx++;
4019
+ }
4020
+ return span;
4021
+ };
4022
+ const rewriteJsImportSpan = (lines, span, syms, unusedNames) => {
4023
+ const fullImport = span.map((i) => lines[i]).join("\n");
4024
+ const namedMatch = fullImport.match(/\{([^}]+)\}/s);
4025
+ if (!namedMatch) return;
4026
+ const unusedNamed = syms.filter((s) => !s.isDefault && !s.isNamespace && unusedNames.has(s.name));
4027
+ if (unusedNamed.length === 0) return;
4028
+ const unusedNamedSet = new Set(unusedNamed.map((s) => s.name));
4029
+ const keptSpecifiers = namedMatch[1].split(",").map((s) => s.trim()).filter(Boolean).filter((spec) => {
4030
+ const parts = spec.split(/\s+as\s+/);
4031
+ const localName = parts.length > 1 ? parts[1].trim().replace(/^type\s+/, "") : parts[0].trim().replace(/^type\s+/, "");
4032
+ return !unusedNamedSet.has(localName);
4033
+ });
4034
+ if (keptSpecifiers.length === 0) {
4035
+ if (syms.find((s) => s.isDefault && !unusedNames.has(s.name))) {
4036
+ const rewritten = fullImport.replace(/,\s*\{[^}]*\}/s, "").replace(/\{[^}]*\}\s*,?\s*/s, "");
4037
+ lines[span[0]] = rewritten.replace(/\n/g, " ").replace(/\s+/g, " ");
4038
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4039
+ }
4040
+ return;
4041
+ }
4042
+ const fromMatch = fullImport.match(/\}\s*(from\s+.+)$/s);
4043
+ const fromClause = fromMatch ? fromMatch[1].trim() : "";
4044
+ const importPrefix = fullImport.match(/^(import\s+(?:\w+\s*,\s*)?)/);
4045
+ const prefix = importPrefix ? importPrefix[1] : "import ";
4046
+ const wasMultiLine = span.length > 1;
4047
+ let newImport;
4048
+ if (wasMultiLine && keptSpecifiers.length > 2) {
4049
+ const indentMatch = lines[span[1]]?.match(/^(\s+)/);
4050
+ const indent = indentMatch ? indentMatch[1] : " ";
4051
+ newImport = `${prefix}{\n${keptSpecifiers.map((s) => `${indent}${s},`).join("\n")}\n} ${fromClause}`;
4052
+ } else newImport = `${prefix}{ ${keptSpecifiers.join(", ")} } ${fromClause}`;
4053
+ lines[span[0]] = newImport;
4054
+ for (let i = 1; i < span.length; i++) lines[span[i]] = REMOVE_MARKER;
4055
+ };
4056
+ const rewritePyImportLine = (lines, lineIdx, syms, unusedNames) => {
4057
+ const fromMatch = lines[lineIdx].match(/^(\s*from\s+[\w.]+\s+import\s+)(.+)$/);
4058
+ if (!fromMatch) return;
4059
+ const prefix = fromMatch[1];
4060
+ const importPart = fromMatch[2].replace(/#.*$/, "").trim();
4061
+ const hasParen = importPart.startsWith("(");
4062
+ const keptSpecifiers = importPart.replace(/[()]/g, "").split(",").map((s) => s.trim()).filter((spec) => {
4063
+ const parts = spec.split(/\s+as\s+/);
4064
+ const localName = parts.length > 1 ? parts[1].trim() : parts[0].trim();
4065
+ return !unusedNames.has(localName);
4066
+ });
4067
+ if (keptSpecifiers.length === 0) return;
4068
+ const joined = keptSpecifiers.join(", ");
4069
+ lines[lineIdx] = hasParen ? `${prefix}(${joined})` : `${prefix}${joined}`;
4070
+ };
4071
+
3963
4072
  //#endregion
3964
4073
  //#region src/commands/fix.ts
3965
4074
  const uniqueFiles = (diagnostics) => [...new Set(diagnostics.map((d) => d.filePath))];
@@ -4014,7 +4123,7 @@ const runFixStep = async (name, detect, applyFix, options) => {
4014
4123
  }
4015
4124
  lines.push(...getFilePreviewLines("Affected", uniqueFiles(before), options.verbose));
4016
4125
  if (after.length > 0) lines.push(...getFilePreviewLines("Remaining", uniqueFiles(after), options.verbose));
4017
- await printMaybePaged(`${lines.join("\n")}\n\n`);
4126
+ process.stdout.write(`${lines.join("\n")}\n\n`);
4018
4127
  return result;
4019
4128
  };
4020
4129
  const createEngineContext = (rootDirectory, projectInfo, config) => ({
@@ -4057,6 +4166,15 @@ const fixCommand = async (directory, config, options = {
4057
4166
  printProjectMetadata(projectInfo);
4058
4167
  const context = createEngineContext(resolvedDir, projectInfo, config);
4059
4168
  const steps = [];
4169
+ if (config.engines["ai-slop"]) steps.push(await runFixStep("Unused imports", () => detectUnusedImports(context), () => fixUnusedImports(context), options));
4170
+ if (config.engines.lint) {
4171
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
4172
+ if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
4173
+ else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
4174
+ }
4175
+ if (config.engines["code-quality"]) {
4176
+ if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("Unused dependencies", () => runKnipDependencyCheck(resolvedDir), () => fixUnusedDependencies(resolvedDir), options));
4177
+ }
4060
4178
  if (config.engines.format) {
4061
4179
  if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS formatting", () => runBiomeFormat(context), () => fixBiomeFormat(context), options));
4062
4180
  if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python formatting", () => runRuffFormat(context), () => fixRuffFormat(resolvedDir), options));
@@ -4064,11 +4182,6 @@ const fixCommand = async (directory, config, options = {
4064
4182
  if (projectInfo.languages.includes("go") && projectInfo.installedTools.gofmt) steps.push(await runFixStep("Go formatting", () => runGofmt(context), () => fixGofmt(resolvedDir), options));
4065
4183
  else if (projectInfo.languages.includes("go")) logger.warn(" Go detected but gofmt is not installed; skipping Go formatting fixes.");
4066
4184
  }
4067
- if (config.engines.lint) {
4068
- if (projectInfo.languages.includes("typescript") || projectInfo.languages.includes("javascript")) steps.push(await runFixStep("JS/TS lint fixes", () => runOxlint(context), () => fixOxlint(context), options));
4069
- if (projectInfo.languages.includes("python") && projectInfo.installedTools.ruff) steps.push(await runFixStep("Python lint fixes", () => runRuffLint(context), () => fixRuffLint(resolvedDir), options));
4070
- else if (projectInfo.languages.includes("python")) logger.warn(" Python detected but ruff is not installed; skipping Python lint fixes.");
4071
- }
4072
4185
  if (steps.length === 0) logger.dim(" No applicable auto-fixers found for this project.");
4073
4186
  else {
4074
4187
  logger.break();
@@ -4148,6 +4261,11 @@ const BUILTIN_RULES = [
4148
4261
  engine: "code-quality",
4149
4262
  rules: [
4150
4263
  "knip/files",
4264
+ "knip/dependencies",
4265
+ "knip/devDependencies",
4266
+ "knip/unlisted",
4267
+ "knip/unresolved",
4268
+ "knip/binaries",
4151
4269
  "knip/exports",
4152
4270
  "knip/types",
4153
4271
  "complexity/file-too-large",
@@ -4204,7 +4322,7 @@ const rulesCommand = async (directory) => {
4204
4322
  lines.push("");
4205
4323
  }
4206
4324
  }
4207
- await printMaybePaged(`${lines.join("\n")}\n`);
4325
+ process.stdout.write(`${lines.join("\n")}\n`);
4208
4326
  };
4209
4327
 
4210
4328
  //#endregion
@@ -4433,7 +4551,10 @@ const program = new Command().name("aislop").description("The unified code quali
4433
4551
  verbose: Boolean(flags.verbose),
4434
4552
  json: Boolean(flags.json)
4435
4553
  });
4436
- if (exitCode !== 0) process.exit(exitCode);
4554
+ if (exitCode !== 0) {
4555
+ await flushTelemetry();
4556
+ process.exit(exitCode);
4557
+ }
4437
4558
  }).addHelpText("after", `
4438
4559
  ${highlighter.dim("Commands:")}
4439
4560
  aislop scan [dir] Full code quality scan
@@ -4460,7 +4581,10 @@ program.command("scan [directory]").description("Run full code quality scan").op
4460
4581
  verbose: Boolean(flags.verbose),
4461
4582
  json: Boolean(flags.json)
4462
4583
  });
4463
- if (exitCode !== 0) process.exit(exitCode);
4584
+ if (exitCode !== 0) {
4585
+ await flushTelemetry();
4586
+ process.exit(exitCode);
4587
+ }
4464
4588
  });
4465
4589
  program.command("fix [directory]").description("Auto-fix formatting and lint issues").option("-d, --verbose", "show detailed fix progress").action(async (directory = ".", _flags, command) => {
4466
4590
  const flags = command.optsWithGlobals();
@@ -4474,13 +4598,17 @@ program.command("doctor [directory]").description("Check installed tools and env
4474
4598
  });
4475
4599
  program.command("ci [directory]").description("CI-friendly JSON output with exit codes").action(async (directory = ".") => {
4476
4600
  const { exitCode } = await ciCommand(directory, loadConfig(directory));
4477
- if (exitCode !== 0) process.exit(exitCode);
4601
+ if (exitCode !== 0) {
4602
+ await flushTelemetry();
4603
+ process.exit(exitCode);
4604
+ }
4478
4605
  });
4479
4606
  program.command("rules [directory]").description("List all available rules").action(async (directory = ".") => {
4480
4607
  await rulesCommand(directory);
4481
4608
  });
4482
4609
  const main = async () => {
4483
4610
  await program.parseAsync();
4611
+ await flushTelemetry();
4484
4612
  };
4485
4613
  main();
4486
4614
 
@@ -3,7 +3,7 @@
3
3
  * Application version — injected at build time by tsdown from package.json.
4
4
  * The fallback should always match the "version" field in package.json.
5
5
  */
6
- const APP_VERSION = "0.1.3";
6
+ const APP_VERSION = "0.2.1";
7
7
 
8
8
  //#endregion
9
9
  //#region src/output/engine-info.ts