@wbern/obscene 1.4.0 → 1.5.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.
Files changed (2) hide show
  1. package/dist/cli.js +138 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
+ import { existsSync, writeFileSync } from "fs";
4
5
  import { Command } from "commander";
5
6
 
6
7
  // src/analyze.ts
@@ -385,6 +386,106 @@ function getNestingDepths(filePaths) {
385
386
  }
386
387
  return depths;
387
388
  }
389
+ var INIT_DIR_RULES = [
390
+ {
391
+ dir: ".github",
392
+ pattern: ".github/**",
393
+ comment: "GitHub Actions and workflows"
394
+ },
395
+ {
396
+ dir: ".circleci",
397
+ pattern: ".circleci/**",
398
+ comment: "CircleCI configuration"
399
+ },
400
+ { dir: ".husky", pattern: ".husky/**", comment: "Git hooks" },
401
+ { dir: ".vscode", pattern: ".vscode/**", comment: "VS Code settings" },
402
+ { dir: ".idea", pattern: ".idea/**", comment: "JetBrains settings" },
403
+ {
404
+ dir: "scripts",
405
+ pattern: "scripts/**",
406
+ comment: "Build and utility scripts"
407
+ },
408
+ { dir: "docs", pattern: "docs/**", comment: "Documentation" },
409
+ { dir: "docker", pattern: "docker/**", comment: "Docker configuration" },
410
+ {
411
+ dir: "fixtures",
412
+ pattern: "fixtures/**",
413
+ comment: "Test fixtures"
414
+ },
415
+ {
416
+ dir: "vendor",
417
+ pattern: "vendor/**",
418
+ comment: "Vendored dependencies"
419
+ }
420
+ ];
421
+ var INIT_FILE_RULES = [
422
+ {
423
+ test: /\.generated\./,
424
+ pattern: "*.generated.*",
425
+ comment: "Generated code"
426
+ },
427
+ { test: /\.gen\.[^.]+$/, pattern: "*.gen.*", comment: "Generated code" },
428
+ {
429
+ test: /\.config\.\w/,
430
+ pattern: "*.config.*",
431
+ comment: "Configuration files"
432
+ },
433
+ {
434
+ test: /(?:^|\/)\.gitlab-ci/,
435
+ pattern: ".gitlab-ci*",
436
+ comment: "GitLab CI configuration"
437
+ }
438
+ ];
439
+ function detectIgnorePatterns() {
440
+ let raw;
441
+ try {
442
+ raw = execSync("git ls-files", {
443
+ maxBuffer: 50 * 1024 * 1024,
444
+ stdio: ["pipe", "pipe", "pipe"]
445
+ });
446
+ } catch {
447
+ throw new Error("Not a git repository or git is not installed.");
448
+ }
449
+ const trackedFiles = raw.toString().split("\n").map((l) => normalizePath(l.trim())).filter(Boolean);
450
+ const patterns = [];
451
+ const topDirs = /* @__PURE__ */ new Set();
452
+ for (const f of trackedFiles) {
453
+ const slash = f.indexOf("/");
454
+ if (slash > 0) topDirs.add(f.slice(0, slash));
455
+ }
456
+ for (const rule of INIT_DIR_RULES) {
457
+ if (topDirs.has(rule.dir)) {
458
+ patterns.push({ pattern: rule.pattern, comment: rule.comment });
459
+ }
460
+ }
461
+ for (const rule of INIT_FILE_RULES) {
462
+ if (trackedFiles.some((f) => rule.test.test(f))) {
463
+ patterns.push({ pattern: rule.pattern, comment: rule.comment });
464
+ }
465
+ }
466
+ return patterns;
467
+ }
468
+ function formatIgnoreFile(patterns) {
469
+ const lines = [
470
+ "# Generated by obscene init",
471
+ "# Edit this file to customize which files are excluded from analysis.",
472
+ "# Patterns use glob syntax (same as .gitignore).",
473
+ "# See: https://github.com/wbern/obscene#ignore-files",
474
+ ""
475
+ ];
476
+ if (patterns.length === 0) {
477
+ lines.push("# No project-specific patterns detected.");
478
+ lines.push("# Add glob patterns here, one per line.");
479
+ lines.push("");
480
+ } else {
481
+ for (const p of patterns) {
482
+ lines.push(`# ${p.comment}`);
483
+ lines.push(p.pattern);
484
+ lines.push("");
485
+ }
486
+ }
487
+ return lines.join("\n");
488
+ }
388
489
  var RRF_K = 10;
389
490
  function computeComposite(rankings, churn, top) {
390
491
  const totalDimensions = Object.keys(rankings).length;
@@ -756,7 +857,7 @@ function formatCompositeTable(output) {
756
857
 
757
858
  // src/cli.ts
758
859
  var program = new Command();
759
- program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.4.0");
860
+ program.name("obscene").description("Identify hotspot files \u2014 complex code that changes frequently").version("1.5.0");
760
861
  var REPORT_GUIDE = {
761
862
  complexity: "Cyclomatic complexity (branch/loop count). NOT a quality judgment \u2014 a 500-line parser will naturally score high. Compare density, not raw values.",
762
863
  complexityDensity: "Complexity per line of code. Normalizes for file size. >0.25 suggests dense logic worth reviewing; <0.10 is typical for straightforward code.",
@@ -812,6 +913,13 @@ addSharedOptions(
812
913
  exitWithError(err);
813
914
  }
814
915
  });
916
+ program.command("init").description("generate a starter .obsignore based on project structure").action(() => {
917
+ try {
918
+ runInit();
919
+ } catch (err) {
920
+ exitWithError(err);
921
+ }
922
+ });
815
923
  function resolveExcludes(cliExcludes) {
816
924
  return [...readIgnoreFile(), ...cliExcludes ?? []];
817
925
  }
@@ -929,6 +1037,35 @@ function runCoupling(opts) {
929
1037
  `);
930
1038
  }
931
1039
  }
1040
+ function runInit() {
1041
+ if (existsSync(".obsignore")) {
1042
+ throw new Error(
1043
+ ".obsignore already exists. Remove it first to regenerate."
1044
+ );
1045
+ }
1046
+ if (existsSync(".obsceneignore")) {
1047
+ throw new Error(
1048
+ ".obsceneignore already exists. Remove it first to regenerate."
1049
+ );
1050
+ }
1051
+ const patterns = detectIgnorePatterns();
1052
+ const content = formatIgnoreFile(patterns);
1053
+ writeFileSync(".obsignore", content);
1054
+ if (patterns.length === 0) {
1055
+ process.stderr.write(
1056
+ "Created .obsignore (no project-specific patterns detected)\n"
1057
+ );
1058
+ } else {
1059
+ process.stderr.write(
1060
+ `Created .obsignore with ${patterns.length} patterns:
1061
+ `
1062
+ );
1063
+ for (const p of patterns) {
1064
+ process.stderr.write(` ${p.pattern.padEnd(20)} ${p.comment}
1065
+ `);
1066
+ }
1067
+ }
1068
+ }
932
1069
  function exitWithError(err) {
933
1070
  const message = err instanceof Error ? err.message : String(err);
934
1071
  process.stderr.write(`Error: ${message}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wbern/obscene",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Identify hotspot files — complex code that changes frequently. Churn × complexity analysis for any git repo.",
5
5
  "type": "module",
6
6
  "bin": {