@pennyfarthing/core 10.0.3 → 10.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/README.md +9 -7
- package/package.json +7 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts +5 -1
- package/packages/core/dist/cli/commands/cyclist.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/cyclist.js +4 -4
- package/packages/core/dist/cli/commands/cyclist.js.map +1 -1
- package/packages/core/dist/cli/commands/cyclist.test.js +2 -2
- package/packages/core/dist/cli/commands/cyclist.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor-legacy.test.js +17 -16
- package/packages/core/dist/cli/commands/doctor-legacy.test.js.map +1 -1
- package/packages/core/dist/cli/commands/doctor.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/doctor.js +251 -4
- package/packages/core/dist/cli/commands/doctor.js.map +1 -1
- package/packages/core/dist/cli/commands/init.d.ts +7 -0
- package/packages/core/dist/cli/commands/init.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/init.js +43 -7
- package/packages/core/dist/cli/commands/init.js.map +1 -1
- package/packages/core/dist/cli/commands/update.d.ts.map +1 -1
- package/packages/core/dist/cli/commands/update.js +26 -0
- package/packages/core/dist/cli/commands/update.js.map +1 -1
- package/packages/core/dist/cli/index.js +1 -1
- package/packages/core/dist/cli/index.js.map +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js +1 -1
- package/packages/core/dist/cli/ocean-profiles.test.js.map +1 -1
- package/packages/core/dist/cli/utils/files.d.ts +10 -0
- package/packages/core/dist/cli/utils/files.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/files.js +35 -0
- package/packages/core/dist/cli/utils/files.js.map +1 -1
- package/packages/core/dist/cli/utils/python.d.ts +22 -0
- package/packages/core/dist/cli/utils/python.d.ts.map +1 -0
- package/packages/core/dist/cli/utils/python.js +102 -0
- package/packages/core/dist/cli/utils/python.js.map +1 -0
- package/packages/core/dist/cli/utils/settings.d.ts.map +1 -1
- package/packages/core/dist/cli/utils/settings.js +10 -0
- package/packages/core/dist/cli/utils/settings.js.map +1 -1
- package/packages/core/dist/scripts/generate-report.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-report.js +11 -7
- package/packages/core/dist/scripts/generate-report.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider-report.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-spider-report.js +12 -8
- package/packages/core/dist/scripts/generate-spider-report.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider.d.ts.map +1 -1
- package/packages/core/dist/scripts/generate-spider.js +6 -4
- package/packages/core/dist/scripts/generate-spider.js.map +1 -1
- package/packages/core/dist/scripts/generate-spider.test.js +2 -2
- package/packages/core/dist/scripts/generate-spider.test.js.map +1 -1
- package/pennyfarthing-dist/agents/README.md +1 -3
- package/pennyfarthing-dist/agents/architect.md +0 -6
- package/pennyfarthing-dist/agents/devops.md +0 -6
- package/pennyfarthing-dist/agents/orchestrator.md +0 -6
- package/pennyfarthing-dist/agents/pm.md +1 -7
- package/pennyfarthing-dist/agents/sm-finish.md +1 -1
- package/pennyfarthing-dist/agents/sm-setup.md +2 -2
- package/pennyfarthing-dist/agents/sm.md +4 -11
- package/pennyfarthing-dist/commands/architect.md +11 -3
- package/pennyfarthing-dist/commands/close-epic.md +24 -131
- package/pennyfarthing-dist/commands/create-theme.md +14 -24
- package/pennyfarthing-dist/commands/dev.md +11 -3
- package/pennyfarthing-dist/commands/devops.md +11 -3
- package/pennyfarthing-dist/commands/health-check.md +1 -3
- package/pennyfarthing-dist/commands/help.md +8 -12
- package/pennyfarthing-dist/commands/list-themes.md +14 -16
- package/pennyfarthing-dist/commands/orchestrator.md +11 -3
- package/pennyfarthing-dist/commands/parallel-work.md +1 -3
- package/pennyfarthing-dist/commands/pm.md +11 -3
- package/pennyfarthing-dist/commands/prime.md +6 -6
- package/pennyfarthing-dist/commands/repo-status.md +2 -2
- package/pennyfarthing-dist/commands/reviewer.md +11 -3
- package/pennyfarthing-dist/commands/run-ci.md +1 -1
- package/pennyfarthing-dist/commands/set-theme.md +14 -51
- package/pennyfarthing-dist/commands/setup.md +1 -1
- package/pennyfarthing-dist/commands/show-theme.md +14 -16
- package/pennyfarthing-dist/commands/sm.md +11 -3
- package/pennyfarthing-dist/commands/sprint.md +8 -8
- package/pennyfarthing-dist/commands/tea.md +11 -3
- package/pennyfarthing-dist/commands/tech-writer.md +11 -3
- package/pennyfarthing-dist/commands/theme-maker.md +14 -671
- package/pennyfarthing-dist/commands/theme.md +95 -0
- package/pennyfarthing-dist/commands/ux-designer.md +11 -3
- package/pennyfarthing-dist/commands/work.md +3 -5
- package/pennyfarthing-dist/guides/agent-coordination.md +11 -13
- package/pennyfarthing-dist/guides/agent-template-tactical.md +2 -3
- package/pennyfarthing-dist/guides/command-tag-taxonomy.md +212 -0
- package/pennyfarthing-dist/guides/hooks.md +5 -5
- package/pennyfarthing-dist/guides/patterns/fan-out-fan-in-pattern.md +3 -3
- package/pennyfarthing-dist/guides/patterns/helper-delegation-pattern.md +9 -59
- package/pennyfarthing-dist/guides/patterns/tdd-flow-pattern.md +4 -5
- package/pennyfarthing-dist/guides/prime.md +2 -2
- package/pennyfarthing-dist/guides/skill-schema.md +25 -26
- package/pennyfarthing-dist/guides/xml-tags.md +2 -2
- package/pennyfarthing-dist/scripts/README.md +2 -2
- package/pennyfarthing-dist/scripts/core/agent-session.sh +6 -2
- package/pennyfarthing-dist/scripts/core/prime.sh +8 -10
- package/pennyfarthing-dist/scripts/git/git-status-all.sh +1 -1
- package/pennyfarthing-dist/scripts/git/install-git-hooks.sh +8 -6
- package/pennyfarthing-dist/scripts/git/worktree-manager.sh +3 -3
- package/pennyfarthing-dist/scripts/hooks/post-merge.sh +14 -12
- package/pennyfarthing-dist/scripts/hooks/pre-commit.sh +4 -3
- package/pennyfarthing-dist/scripts/hooks/pre-push.sh +11 -5
- package/pennyfarthing-dist/scripts/hooks/sprint-yaml-validation.sh +1 -1
- package/pennyfarthing-dist/scripts/misc/README.md +1 -1
- package/pennyfarthing-dist/scripts/misc/repo-utils.sh +3 -3
- package/pennyfarthing-dist/scripts/misc/validate-subagent-frontmatter.sh +1 -2
- package/pennyfarthing-dist/scripts/sprint/README.md +32 -17
- package/pennyfarthing-dist/scripts/story/README.md +1 -1
- package/pennyfarthing-dist/scripts/test/test-setup.sh +1 -1
- package/pennyfarthing-dist/scripts/tests/handoff-phase-update.test.sh +5 -5
- package/pennyfarthing-dist/scripts/tests/test-drift-detection.sh +3 -79
- package/pennyfarthing-dist/scripts/theme/README.md +1 -1
- package/pennyfarthing-dist/scripts/validation/validate-agent-schema.sh +0 -1
- package/pennyfarthing-dist/scripts/workflow/finish-story.sh +62 -17
- package/pennyfarthing-dist/skills/dev-patterns/SKILL.md +2 -2
- package/pennyfarthing-dist/skills/skill-registry.yaml +41 -28
- package/pennyfarthing-dist/skills/sprint/skill.md +386 -68
- package/pennyfarthing-dist/skills/story/skill.md +14 -206
- package/pennyfarthing-dist/skills/theme/skill.md +290 -75
- package/pennyfarthing-dist/skills/theme-creation/SKILL.md +23 -166
- package/pennyfarthing-dist/skills/workflow/skill.md +4 -4
- package/pennyfarthing-dist/templates/agent-scopes.yaml.template +0 -11
- package/pennyfarthing-dist/templates/auto-load-sm.sh.template +14 -0
- package/pennyfarthing-dist/templates/settings.local.json.template +9 -0
- package/pennyfarthing-dist/workflows/2party-tdd.yaml +399 -0
- package/pennyfarthing-dist/workflows/epics-and-stories/steps/step-05-import-to-future.md +42 -25
- package/pennyfarthing-dist/workflows/git-cleanup.yaml +1 -1
- package/pennyfarthing-dist/workflows/project-setup/steps/step-10-complete.md +1 -1
- package/pennyfarthing_scripts/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/hooks.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/schema_validation_hook.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/cli.py +15 -0
- package/pennyfarthing_scripts/codemarkers/__init__.py +19 -0
- package/pennyfarthing_scripts/codemarkers/__main__.py +6 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/codemarkers/analyze.py +326 -0
- package/pennyfarthing_scripts/codemarkers/cli.py +129 -0
- package/pennyfarthing_scripts/codemarkers/formatters.py +89 -0
- package/pennyfarthing_scripts/codemarkers/models.py +45 -0
- package/pennyfarthing_scripts/common/__pycache__/config.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/__pycache__/themes.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/common/config.py +2 -1
- package/pennyfarthing_scripts/complexity/__init__.py +15 -0
- package/pennyfarthing_scripts/complexity/__main__.py +6 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/complexity/analyze.py +207 -0
- package/pennyfarthing_scripts/complexity/cli.py +78 -0
- package/pennyfarthing_scripts/complexity/formatters.py +64 -0
- package/pennyfarthing_scripts/complexity/models.py +32 -0
- package/pennyfarthing_scripts/deadcode/__init__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__main__.py +6 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/deadcode/analyze.py +323 -0
- package/pennyfarthing_scripts/deadcode/cli.py +163 -0
- package/pennyfarthing_scripts/deadcode/formatters.py +106 -0
- package/pennyfarthing_scripts/deadcode/models.py +54 -0
- package/pennyfarthing_scripts/dependencies/__init__.py +20 -0
- package/pennyfarthing_scripts/dependencies/__main__.py +5 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/dependencies/analyze.py +155 -0
- package/pennyfarthing_scripts/dependencies/cli.py +72 -0
- package/pennyfarthing_scripts/dependencies/formatters.py +63 -0
- package/pennyfarthing_scripts/dependencies/models.py +39 -0
- package/pennyfarthing_scripts/healthscore/__init__.py +21 -0
- package/pennyfarthing_scripts/healthscore/__main__.py +6 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/healthscore/analyze.py +161 -0
- package/pennyfarthing_scripts/healthscore/cli.py +76 -0
- package/pennyfarthing_scripts/healthscore/formatters.py +46 -0
- package/pennyfarthing_scripts/healthscore/models.py +44 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/__main__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/analyze.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/formatters.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/hotspots/analyze.py +28 -1
- package/pennyfarthing_scripts/hotspots/cli.py +11 -9
- package/pennyfarthing_scripts/jira/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/bidirectional.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/client.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/create.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/operations.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/__pycache__/reconcile.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/jira/bidirectional.py +42 -15
- package/pennyfarthing_scripts/jira/cli.py +78 -1
- package/pennyfarthing_scripts/jira/client.py +28 -0
- package/pennyfarthing_scripts/prime/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/models.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/persona.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/tiers.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/__pycache__/workflow.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/prime/workflow.py +5 -3
- package/pennyfarthing_scripts/sprint/__pycache__/archive.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/archive_epic.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/epic_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/loader.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_add.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_finish.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/story_update.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validate_cmd.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/validator.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/work.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/__pycache__/yaml_io.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/sprint/archive.py +63 -6
- package/pennyfarthing_scripts/sprint/archive_epic.py +198 -85
- package/pennyfarthing_scripts/sprint/cli.py +1565 -65
- package/pennyfarthing_scripts/sprint/epic_add.py +173 -0
- package/pennyfarthing_scripts/sprint/loader.py +46 -2
- package/pennyfarthing_scripts/sprint/story_add.py +202 -27
- package/pennyfarthing_scripts/sprint/story_finish.py +211 -0
- package/pennyfarthing_scripts/sprint/validate_cmd.py +44 -5
- package/pennyfarthing_scripts/sprint/validator.py +13 -3
- package/pennyfarthing_scripts/sprint/work.py +43 -3
- package/pennyfarthing_scripts/sprint/yaml_io.py +124 -15
- package/pennyfarthing_scripts/tests/__pycache__/test_codemarkers.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_healthscore.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_package.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_sprint_validator.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_add.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_story_update.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_validate_cmd.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/__pycache__/test_yaml_io.cpython-314-pytest-9.0.2.pyc +0 -0
- package/pennyfarthing_scripts/tests/test_codemarkers.py +682 -0
- package/pennyfarthing_scripts/tests/test_healthscore.py +524 -0
- package/pennyfarthing_scripts/tests/test_sprint_package.py +166 -0
- package/pennyfarthing_scripts/tests/test_yaml_io.py +117 -0
- package/pennyfarthing_scripts/theme/__init__.py +5 -0
- package/pennyfarthing_scripts/theme/__main__.py +6 -0
- package/pennyfarthing_scripts/theme/__pycache__/__init__.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/__pycache__/cli.cpython-314.pyc +0 -0
- package/pennyfarthing_scripts/theme/cli.py +286 -0
- package/scripts/README.md +53 -0
- package/scripts/postinstall.cjs +34 -0
- package/pennyfarthing-dist/agents/workflow-status-check.md +0 -96
- package/pennyfarthing-dist/scripts/sprint/archive-story.sh +0 -133
- package/pennyfarthing-dist/scripts/sprint/available-stories.sh +0 -91
- package/pennyfarthing-dist/scripts/sprint/check-story.sh +0 -158
- package/pennyfarthing-dist/scripts/sprint/get-epic-field.sh +0 -52
- package/pennyfarthing-dist/scripts/sprint/get-story-field.sh +0 -63
- package/pennyfarthing-dist/scripts/sprint/list-future.sh +0 -145
- package/pennyfarthing-dist/scripts/sprint/new-sprint.sh +0 -110
- package/pennyfarthing-dist/scripts/sprint/promote-epic.sh +0 -148
- package/pennyfarthing-dist/scripts/sprint/sprint-common.sh +0 -415
- package/pennyfarthing-dist/scripts/sprint/sprint-info.sh +0 -33
- package/pennyfarthing-dist/scripts/sprint/sprint-metrics.sh +0 -230
- package/pennyfarthing-dist/scripts/sprint/sprint-status.sh +0 -134
- package/pennyfarthing-dist/scripts/sprint/validate-sprint-yaml.sh +0 -139
- package/pennyfarthing-dist/skills/sprint/scripts/archive-story.sh +0 -101
- package/pennyfarthing-dist/skills/sprint/scripts/available-stories.sh +0 -97
- package/pennyfarthing-dist/skills/sprint/scripts/check-story.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/create-jira-epic.sh +0 -23
- package/pennyfarthing-dist/skills/sprint/scripts/new-sprint.sh +0 -116
- package/pennyfarthing-dist/skills/sprint/scripts/promote-epic.sh +0 -164
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-info.sh +0 -39
- package/pennyfarthing-dist/skills/sprint/scripts/sprint-status.sh +0 -147
- package/pennyfarthing-dist/skills/sprint/scripts/sync-epic-jira.sh +0 -23
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core dead code detection engine.
|
|
3
|
+
|
|
4
|
+
Layer 1: Compares git ls-files against git log --since to find files with no recent commits.
|
|
5
|
+
Layer 2: Runs ts-prune to find unused TypeScript exports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import fnmatch
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
|
|
17
|
+
from pennyfarthing_scripts.deadcode.models import (
|
|
18
|
+
DeadCodeResult,
|
|
19
|
+
StaleFile,
|
|
20
|
+
UnusedExport,
|
|
21
|
+
UnusedExportResult,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Default file patterns to exclude from analysis
|
|
25
|
+
DEFAULT_EXCLUDES = [
|
|
26
|
+
"node_modules/*",
|
|
27
|
+
"dist/*",
|
|
28
|
+
"build/*",
|
|
29
|
+
"*.lock",
|
|
30
|
+
"*.min.js",
|
|
31
|
+
"*.min.css",
|
|
32
|
+
"package-lock.json",
|
|
33
|
+
"pnpm-lock.yaml",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
# Source file extensions to include
|
|
37
|
+
SOURCE_EXTENSIONS = {
|
|
38
|
+
".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs",
|
|
39
|
+
".java", ".kt", ".swift", ".rb", ".sh", ".bash",
|
|
40
|
+
".css", ".scss", ".less", ".html", ".md", ".yaml", ".yml",
|
|
41
|
+
".json", ".toml",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
async def _run_git_command(args: list[str], cwd: Path) -> tuple[str, str, int]:
|
|
46
|
+
"""Run a git command asynchronously.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
args: Git command arguments (without 'git')
|
|
50
|
+
cwd: Working directory
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
(stdout, stderr, return_code)
|
|
54
|
+
"""
|
|
55
|
+
proc = await asyncio.create_subprocess_exec(
|
|
56
|
+
"git",
|
|
57
|
+
*args,
|
|
58
|
+
cwd=cwd,
|
|
59
|
+
stdout=asyncio.subprocess.PIPE,
|
|
60
|
+
stderr=asyncio.subprocess.PIPE,
|
|
61
|
+
)
|
|
62
|
+
stdout, stderr = await proc.communicate()
|
|
63
|
+
return (
|
|
64
|
+
stdout.decode("utf-8", errors="replace").strip(),
|
|
65
|
+
stderr.decode("utf-8", errors="replace").strip(),
|
|
66
|
+
proc.returncode or 0,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _should_exclude(path: str, patterns: list[str]) -> bool:
|
|
71
|
+
"""Check if a file path matches any exclusion pattern."""
|
|
72
|
+
for pattern in patterns:
|
|
73
|
+
if fnmatch.fnmatch(path, pattern):
|
|
74
|
+
return True
|
|
75
|
+
if fnmatch.fnmatch(path.split("/")[-1], pattern):
|
|
76
|
+
return True
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _is_source_file(path: str) -> bool:
|
|
81
|
+
"""Check if a file has a recognized source extension."""
|
|
82
|
+
suffix = Path(path).suffix.lower()
|
|
83
|
+
return suffix in SOURCE_EXTENSIONS
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def find_stale_files(
|
|
87
|
+
repo_path: Path,
|
|
88
|
+
days: int = 180,
|
|
89
|
+
excludes: list[str] | None = None,
|
|
90
|
+
branch: str = "--all",
|
|
91
|
+
) -> DeadCodeResult:
|
|
92
|
+
"""Find files with no commits in the given time window.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
repo_path: Path to the git repository
|
|
96
|
+
days: Time window in days
|
|
97
|
+
excludes: Additional file patterns to exclude
|
|
98
|
+
branch: Branch spec (default --all)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
DeadCodeResult with stale files
|
|
102
|
+
"""
|
|
103
|
+
all_excludes = DEFAULT_EXCLUDES + (excludes or [])
|
|
104
|
+
resolved = Path(repo_path).resolve()
|
|
105
|
+
|
|
106
|
+
# Get all tracked files
|
|
107
|
+
ls_stdout, ls_stderr, ls_rc = await _run_git_command(["ls-files"], resolved)
|
|
108
|
+
|
|
109
|
+
if ls_rc != 0:
|
|
110
|
+
return DeadCodeResult(
|
|
111
|
+
success=False,
|
|
112
|
+
repo_name=resolved.name,
|
|
113
|
+
repo_path=str(resolved),
|
|
114
|
+
time_window_days=days,
|
|
115
|
+
error=f"git ls-files failed: {ls_stderr}",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
all_files = set()
|
|
119
|
+
for line in ls_stdout.split("\n"):
|
|
120
|
+
line = line.strip()
|
|
121
|
+
if line:
|
|
122
|
+
all_files.add(line)
|
|
123
|
+
|
|
124
|
+
if not all_files:
|
|
125
|
+
return DeadCodeResult(
|
|
126
|
+
success=True,
|
|
127
|
+
repo_name=resolved.name,
|
|
128
|
+
repo_path=str(resolved),
|
|
129
|
+
time_window_days=days,
|
|
130
|
+
total_files=0,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Get recently touched files
|
|
134
|
+
log_stdout, log_stderr, log_rc = await _run_git_command(
|
|
135
|
+
["log", f"--since={days} days ago", branch, "--name-only", "--pretty=format:"],
|
|
136
|
+
resolved,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
recent_files = set()
|
|
140
|
+
if log_rc == 0 and log_stdout:
|
|
141
|
+
for line in log_stdout.split("\n"):
|
|
142
|
+
line = line.strip()
|
|
143
|
+
if line:
|
|
144
|
+
recent_files.add(line)
|
|
145
|
+
|
|
146
|
+
# Set difference: stale = all - recent
|
|
147
|
+
candidate_stale = all_files - recent_files
|
|
148
|
+
|
|
149
|
+
# Filter: exclude patterns and non-source files
|
|
150
|
+
filtered = []
|
|
151
|
+
for fpath in candidate_stale:
|
|
152
|
+
if _should_exclude(fpath, all_excludes):
|
|
153
|
+
continue
|
|
154
|
+
if not _is_source_file(fpath):
|
|
155
|
+
continue
|
|
156
|
+
filtered.append(fpath)
|
|
157
|
+
|
|
158
|
+
# Enrich each stale file
|
|
159
|
+
now = datetime.now(timezone.utc)
|
|
160
|
+
stale_files = []
|
|
161
|
+
for fpath in sorted(filtered):
|
|
162
|
+
# Get last commit date
|
|
163
|
+
date_stdout, _, _ = await _run_git_command(
|
|
164
|
+
["log", "-1", "--format=%aI", "--", fpath],
|
|
165
|
+
resolved,
|
|
166
|
+
)
|
|
167
|
+
last_commit_date = date_stdout.strip()
|
|
168
|
+
|
|
169
|
+
# Calculate days since last commit
|
|
170
|
+
days_since = 0
|
|
171
|
+
if last_commit_date:
|
|
172
|
+
try:
|
|
173
|
+
last_dt = datetime.fromisoformat(last_commit_date)
|
|
174
|
+
days_since = int((now - last_dt).total_seconds() / 86400)
|
|
175
|
+
except (ValueError, TypeError):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
# Get file size
|
|
179
|
+
size_bytes = 0
|
|
180
|
+
try:
|
|
181
|
+
full_path = resolved / fpath
|
|
182
|
+
size_bytes = full_path.stat().st_size
|
|
183
|
+
except (OSError, FileNotFoundError):
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
stale_files.append(
|
|
187
|
+
StaleFile(
|
|
188
|
+
path=fpath,
|
|
189
|
+
last_commit_date=last_commit_date,
|
|
190
|
+
days_since_last_commit=days_since,
|
|
191
|
+
size_bytes=size_bytes,
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return DeadCodeResult(
|
|
196
|
+
success=True,
|
|
197
|
+
repo_name=resolved.name,
|
|
198
|
+
repo_path=str(resolved),
|
|
199
|
+
time_window_days=days,
|
|
200
|
+
stale_files=stale_files,
|
|
201
|
+
total_files=len(all_files),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
async def analyze_repo(
|
|
206
|
+
name: str,
|
|
207
|
+
path: Path,
|
|
208
|
+
days: int = 180,
|
|
209
|
+
excludes: list[str] | None = None,
|
|
210
|
+
branch: str = "--all",
|
|
211
|
+
) -> DeadCodeResult:
|
|
212
|
+
"""Analyze a single repository for stale files.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
name: Display name for the repository
|
|
216
|
+
path: Path to the git repository
|
|
217
|
+
days: Time window in days
|
|
218
|
+
excludes: Additional file patterns to exclude
|
|
219
|
+
branch: Branch spec (default --all)
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
DeadCodeResult with stale files
|
|
223
|
+
"""
|
|
224
|
+
resolved = Path(path).resolve()
|
|
225
|
+
|
|
226
|
+
if not resolved.exists():
|
|
227
|
+
return DeadCodeResult(
|
|
228
|
+
success=False,
|
|
229
|
+
repo_name=name,
|
|
230
|
+
repo_path=str(resolved),
|
|
231
|
+
time_window_days=days,
|
|
232
|
+
error=f"Path not found: {resolved}",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
result = await find_stale_files(resolved, days, excludes, branch)
|
|
236
|
+
# Override repo_name with the provided name
|
|
237
|
+
result.repo_name = name
|
|
238
|
+
return result
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ts-prune output line pattern: "path/to/file.ts:line - symbolName"
|
|
242
|
+
_TS_PRUNE_LINE_RE = re.compile(r"^(.+):(\d+) - (.+)$")
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _parse_ts_prune_output(output: str) -> list[UnusedExport]:
|
|
246
|
+
"""Parse ts-prune stdout into UnusedExport instances.
|
|
247
|
+
|
|
248
|
+
ts-prune format: ``path/to/file.ts:10 - symbolName``
|
|
249
|
+
Lines ending with ``(used in module)`` are skipped (not truly unused).
|
|
250
|
+
"""
|
|
251
|
+
exports: list[UnusedExport] = []
|
|
252
|
+
for line in output.strip().split("\n"):
|
|
253
|
+
line = line.strip()
|
|
254
|
+
if not line:
|
|
255
|
+
continue
|
|
256
|
+
# Skip "used in module" markers — these are re-exported and consumed
|
|
257
|
+
if line.endswith("(used in module)"):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
m = _TS_PRUNE_LINE_RE.match(line)
|
|
261
|
+
if not m:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
file_path, line_no, symbol = m.group(1), int(m.group(2)), m.group(3).strip()
|
|
265
|
+
export_type = "default" if symbol == "default" else "named"
|
|
266
|
+
exports.append(
|
|
267
|
+
UnusedExport(
|
|
268
|
+
symbol=symbol,
|
|
269
|
+
file=file_path,
|
|
270
|
+
line=line_no,
|
|
271
|
+
export_type=export_type,
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
return exports
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
async def find_unused_exports(repo_path: Path) -> UnusedExportResult:
|
|
278
|
+
"""Find unused TypeScript exports via ts-prune.
|
|
279
|
+
|
|
280
|
+
Runs ``npx ts-prune`` in the repo and parses the output.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
repo_path: Path to a TypeScript project with tsconfig.json
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
UnusedExportResult with unused exports
|
|
287
|
+
"""
|
|
288
|
+
resolved = Path(repo_path).resolve()
|
|
289
|
+
|
|
290
|
+
if not resolved.exists():
|
|
291
|
+
return UnusedExportResult(
|
|
292
|
+
success=False,
|
|
293
|
+
repo_name=resolved.name,
|
|
294
|
+
repo_path=str(resolved),
|
|
295
|
+
error=f"Path not found: {resolved}",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
proc = await asyncio.create_subprocess_exec(
|
|
299
|
+
"npx", "ts-prune",
|
|
300
|
+
cwd=resolved,
|
|
301
|
+
stdout=asyncio.subprocess.PIPE,
|
|
302
|
+
stderr=asyncio.subprocess.PIPE,
|
|
303
|
+
)
|
|
304
|
+
stdout_bytes, stderr_bytes = await proc.communicate()
|
|
305
|
+
stdout = stdout_bytes.decode("utf-8", errors="replace").strip()
|
|
306
|
+
stderr = stderr_bytes.decode("utf-8", errors="replace").strip()
|
|
307
|
+
|
|
308
|
+
if proc.returncode and proc.returncode != 0:
|
|
309
|
+
return UnusedExportResult(
|
|
310
|
+
success=False,
|
|
311
|
+
repo_name=resolved.name,
|
|
312
|
+
repo_path=str(resolved),
|
|
313
|
+
error=f"ts-prune failed (exit {proc.returncode}): {stderr}",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
exports = _parse_ts_prune_output(stdout)
|
|
317
|
+
return UnusedExportResult(
|
|
318
|
+
success=True,
|
|
319
|
+
repo_name=resolved.name,
|
|
320
|
+
repo_path=str(resolved),
|
|
321
|
+
unused_exports=exports,
|
|
322
|
+
total_exports_scanned=0, # ts-prune doesn't report total count
|
|
323
|
+
)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI commands for dead code detection.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
pf deadcode stale [OPTIONS]
|
|
6
|
+
pf deadcode exports [OPTIONS]
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import click
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group()
|
|
18
|
+
def deadcode():
|
|
19
|
+
"""Dead code detection tools.
|
|
20
|
+
|
|
21
|
+
\b
|
|
22
|
+
Commands:
|
|
23
|
+
stale - Find files with no recent commits
|
|
24
|
+
exports - Find unused TypeScript exports via ts-prune
|
|
25
|
+
"""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _common_options(fn):
|
|
30
|
+
"""Shared options for deadcode commands."""
|
|
31
|
+
fn = click.option("--repo", help="Analyze a single named repo from repos.yaml")(fn)
|
|
32
|
+
fn = click.option("--path", "repo_path", type=click.Path(exists=True), help="Analyze a standalone repo path")(fn)
|
|
33
|
+
fn = click.option("--days", default=180, show_default=True, help="Time window in days")(fn)
|
|
34
|
+
fn = click.option("--top", default=20, show_default=True, help="Number of top results to show")(fn)
|
|
35
|
+
fn = click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]), default="table", show_default=True)(fn)
|
|
36
|
+
fn = click.option("--output", "output_file", type=click.Path(), help="Write output to file")(fn)
|
|
37
|
+
fn = click.option("--exclude", multiple=True, help="Additional exclude patterns (repeatable)")(fn)
|
|
38
|
+
fn = click.option("--branch", default="--all", show_default=True, help="Branch spec for git log")(fn)
|
|
39
|
+
return fn
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_analysis(repo: str | None, repo_path: str | None, days: int, exclude: tuple, branch: str):
|
|
43
|
+
"""Run analysis and return result."""
|
|
44
|
+
from pennyfarthing_scripts.deadcode.analyze import analyze_repo, find_stale_files
|
|
45
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
46
|
+
|
|
47
|
+
excludes = list(exclude) if exclude else None
|
|
48
|
+
|
|
49
|
+
if repo_path:
|
|
50
|
+
p = Path(repo_path).resolve()
|
|
51
|
+
return asyncio.run(analyze_repo(p.name, p, days, excludes, branch))
|
|
52
|
+
elif repo:
|
|
53
|
+
project_root = get_project_root()
|
|
54
|
+
from pennyfarthing_scripts.common.config import load_yaml_config
|
|
55
|
+
repos_yaml = load_yaml_config(project_root / ".pennyfarthing" / "repos.yaml")
|
|
56
|
+
if repos_yaml and repo in repos_yaml:
|
|
57
|
+
cfg = repos_yaml[repo]
|
|
58
|
+
rpath = cfg.get("path", repo) if isinstance(cfg, dict) else str(cfg)
|
|
59
|
+
return asyncio.run(analyze_repo(repo, project_root / rpath, days, excludes, branch))
|
|
60
|
+
else:
|
|
61
|
+
candidate = project_root / repo
|
|
62
|
+
if candidate.exists():
|
|
63
|
+
return asyncio.run(analyze_repo(repo, candidate, days, excludes, branch))
|
|
64
|
+
raise click.ClickException(f"Repo not found: {repo}")
|
|
65
|
+
else:
|
|
66
|
+
project_root = get_project_root()
|
|
67
|
+
return asyncio.run(analyze_repo(project_root.name, project_root, days, excludes, branch))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _output_result(result, fmt: str, output_file: str | None, top: int):
|
|
71
|
+
"""Format and output the analysis result."""
|
|
72
|
+
from pennyfarthing_scripts.deadcode.formatters import (
|
|
73
|
+
export_csv,
|
|
74
|
+
export_json,
|
|
75
|
+
format_table,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if fmt == "json":
|
|
79
|
+
text = export_json(result)
|
|
80
|
+
elif fmt == "csv":
|
|
81
|
+
text = export_csv(result.stale_files[:top])
|
|
82
|
+
else:
|
|
83
|
+
text = format_table(result.stale_files, top)
|
|
84
|
+
|
|
85
|
+
if output_file:
|
|
86
|
+
Path(output_file).write_text(text)
|
|
87
|
+
click.echo(f"Output written to {output_file}", err=True)
|
|
88
|
+
else:
|
|
89
|
+
click.echo(text)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@deadcode.command()
|
|
93
|
+
@_common_options
|
|
94
|
+
def stale(repo, repo_path, days, top, fmt, output_file, exclude, branch):
|
|
95
|
+
"""Find files with no recent git commits."""
|
|
96
|
+
result = _run_analysis(repo, repo_path, days, exclude, branch)
|
|
97
|
+
_output_result(result, fmt, output_file, top)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _exports_options(fn):
|
|
101
|
+
"""Shared options for exports subcommand."""
|
|
102
|
+
fn = click.option("--repo", help="Analyze a single named repo from repos.yaml")(fn)
|
|
103
|
+
fn = click.option("--path", "repo_path", type=click.Path(exists=True), help="Analyze a standalone repo path")(fn)
|
|
104
|
+
fn = click.option("--top", default=20, show_default=True, help="Number of top results to show")(fn)
|
|
105
|
+
fn = click.option("--format", "fmt", type=click.Choice(["table", "json", "csv"]), default="table", show_default=True)(fn)
|
|
106
|
+
fn = click.option("--output", "output_file", type=click.Path(), help="Write output to file")(fn)
|
|
107
|
+
return fn
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _run_exports_analysis(repo: str | None, repo_path: str | None):
|
|
111
|
+
"""Run unused export analysis and return result."""
|
|
112
|
+
from pennyfarthing_scripts.deadcode.analyze import find_unused_exports
|
|
113
|
+
from pennyfarthing_scripts.common.config import get_project_root
|
|
114
|
+
|
|
115
|
+
if repo_path:
|
|
116
|
+
p = Path(repo_path).resolve()
|
|
117
|
+
return asyncio.run(find_unused_exports(p))
|
|
118
|
+
elif repo:
|
|
119
|
+
project_root = get_project_root()
|
|
120
|
+
from pennyfarthing_scripts.common.config import load_yaml_config
|
|
121
|
+
repos_yaml = load_yaml_config(project_root / ".pennyfarthing" / "repos.yaml")
|
|
122
|
+
if repos_yaml and repo in repos_yaml:
|
|
123
|
+
cfg = repos_yaml[repo]
|
|
124
|
+
rpath = cfg.get("path", repo) if isinstance(cfg, dict) else str(cfg)
|
|
125
|
+
return asyncio.run(find_unused_exports(project_root / rpath))
|
|
126
|
+
else:
|
|
127
|
+
candidate = project_root / repo
|
|
128
|
+
if candidate.exists():
|
|
129
|
+
return asyncio.run(find_unused_exports(candidate))
|
|
130
|
+
raise click.ClickException(f"Repo not found: {repo}")
|
|
131
|
+
else:
|
|
132
|
+
project_root = get_project_root()
|
|
133
|
+
return asyncio.run(find_unused_exports(project_root))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _output_exports_result(result, fmt: str, output_file: str | None, top: int):
|
|
137
|
+
"""Format and output the exports analysis result."""
|
|
138
|
+
from pennyfarthing_scripts.deadcode.formatters import (
|
|
139
|
+
export_exports_csv,
|
|
140
|
+
export_exports_json,
|
|
141
|
+
format_exports_table,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if fmt == "json":
|
|
145
|
+
text = export_exports_json(result)
|
|
146
|
+
elif fmt == "csv":
|
|
147
|
+
text = export_exports_csv(result.unused_exports[:top])
|
|
148
|
+
else:
|
|
149
|
+
text = format_exports_table(result.unused_exports, top)
|
|
150
|
+
|
|
151
|
+
if output_file:
|
|
152
|
+
Path(output_file).write_text(text)
|
|
153
|
+
click.echo(f"Output written to {output_file}", err=True)
|
|
154
|
+
else:
|
|
155
|
+
click.echo(text)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@deadcode.command()
|
|
159
|
+
@_exports_options
|
|
160
|
+
def exports(repo, repo_path, top, fmt, output_file):
|
|
161
|
+
"""Find unused TypeScript exports via ts-prune."""
|
|
162
|
+
result = _run_exports_analysis(repo, repo_path)
|
|
163
|
+
_output_exports_result(result, fmt, output_file, top)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatters for dead code analysis results.
|
|
3
|
+
|
|
4
|
+
Supports table, JSON, and CSV output formats.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import asdict
|
|
13
|
+
|
|
14
|
+
from pennyfarthing_scripts.deadcode.models import (
|
|
15
|
+
DeadCodeResult,
|
|
16
|
+
StaleFile,
|
|
17
|
+
UnusedExport,
|
|
18
|
+
UnusedExportResult,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def format_table(stale_files: list[StaleFile], top_n: int = 20) -> str:
|
|
23
|
+
"""Format stale files as a human-readable table."""
|
|
24
|
+
if not stale_files:
|
|
25
|
+
return "No stale files found."
|
|
26
|
+
|
|
27
|
+
files = stale_files[:top_n]
|
|
28
|
+
|
|
29
|
+
# Column widths
|
|
30
|
+
path_width = max(len("Path"), max(len(f.path) for f in files))
|
|
31
|
+
header = f"{'Path':<{path_width}} {'Days':>6} {'Size':>10} {'Last Commit'}"
|
|
32
|
+
separator = "-" * len(header)
|
|
33
|
+
|
|
34
|
+
lines = [header, separator]
|
|
35
|
+
for f in files:
|
|
36
|
+
size_str = _format_size(f.size_bytes)
|
|
37
|
+
date_str = f.last_commit_date[:10] if f.last_commit_date else "unknown"
|
|
38
|
+
lines.append(
|
|
39
|
+
f"{f.path:<{path_width}} {f.days_since_last_commit:>6} {size_str:>10} {date_str}"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return "\n".join(lines)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_size(size_bytes: int) -> str:
|
|
46
|
+
"""Format bytes as human-readable size."""
|
|
47
|
+
if size_bytes < 1024:
|
|
48
|
+
return f"{size_bytes} B"
|
|
49
|
+
elif size_bytes < 1024 * 1024:
|
|
50
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
51
|
+
else:
|
|
52
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def export_json(result: DeadCodeResult) -> str:
|
|
56
|
+
"""Export result as JSON."""
|
|
57
|
+
return json.dumps(asdict(result), indent=2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def export_csv(stale_files: list[StaleFile]) -> str:
|
|
61
|
+
"""Export stale files as CSV."""
|
|
62
|
+
output = io.StringIO()
|
|
63
|
+
writer = csv.writer(output)
|
|
64
|
+
writer.writerow(["path", "last_commit_date", "days_since_last_commit", "size_bytes"])
|
|
65
|
+
for f in stale_files:
|
|
66
|
+
writer.writerow([f.path, f.last_commit_date, f.days_since_last_commit, f.size_bytes])
|
|
67
|
+
return output.getvalue()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---- Unused export formatters ----
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_exports_table(unused_exports: list[UnusedExport], top_n: int = 20) -> str:
|
|
74
|
+
"""Format unused exports as a human-readable table."""
|
|
75
|
+
if not unused_exports:
|
|
76
|
+
return "No unused exports found."
|
|
77
|
+
|
|
78
|
+
exports = unused_exports[:top_n]
|
|
79
|
+
|
|
80
|
+
file_width = max(len("File"), max(len(ue.file) for ue in exports))
|
|
81
|
+
symbol_width = max(len("Symbol"), max(len(ue.symbol) for ue in exports))
|
|
82
|
+
header = f"{'File':<{file_width}} {'Line':>5} {'Symbol':<{symbol_width}} {'Type'}"
|
|
83
|
+
separator = "-" * len(header)
|
|
84
|
+
|
|
85
|
+
lines = [header, separator]
|
|
86
|
+
for ue in exports:
|
|
87
|
+
lines.append(
|
|
88
|
+
f"{ue.file:<{file_width}} {ue.line:>5} {ue.symbol:<{symbol_width}} {ue.export_type}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return "\n".join(lines)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def export_exports_json(result: UnusedExportResult) -> str:
|
|
95
|
+
"""Export unused export result as JSON."""
|
|
96
|
+
return json.dumps(asdict(result), indent=2)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def export_exports_csv(unused_exports: list[UnusedExport]) -> str:
|
|
100
|
+
"""Export unused exports as CSV."""
|
|
101
|
+
output = io.StringIO()
|
|
102
|
+
writer = csv.writer(output)
|
|
103
|
+
writer.writerow(["symbol", "file", "line", "export_type"])
|
|
104
|
+
for ue in unused_exports:
|
|
105
|
+
writer.writerow([ue.symbol, ue.file, ue.line, ue.export_type])
|
|
106
|
+
return output.getvalue()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for dead code analysis results.
|
|
3
|
+
|
|
4
|
+
Follows ADR-0008 result pattern — structured dataclasses with success/error fields.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class StaleFile:
|
|
14
|
+
"""A file detected as stale (no commits within time window)."""
|
|
15
|
+
|
|
16
|
+
path: str
|
|
17
|
+
last_commit_date: str = "" # ISO 8601
|
|
18
|
+
days_since_last_commit: int = 0
|
|
19
|
+
size_bytes: int = 0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DeadCodeResult:
|
|
24
|
+
"""Analysis result for stale file detection in a single repository."""
|
|
25
|
+
|
|
26
|
+
success: bool
|
|
27
|
+
repo_name: str
|
|
28
|
+
repo_path: str
|
|
29
|
+
time_window_days: int
|
|
30
|
+
stale_files: list[StaleFile] = field(default_factory=list)
|
|
31
|
+
total_files: int = 0
|
|
32
|
+
error: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class UnusedExport:
|
|
37
|
+
"""An exported symbol with no importers, detected by ts-prune."""
|
|
38
|
+
|
|
39
|
+
symbol: str
|
|
40
|
+
file: str
|
|
41
|
+
line: int
|
|
42
|
+
export_type: str = "named" # named, default, re-export
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class UnusedExportResult:
|
|
47
|
+
"""Analysis result for unused export detection in a single repository."""
|
|
48
|
+
|
|
49
|
+
success: bool
|
|
50
|
+
repo_name: str
|
|
51
|
+
repo_path: str
|
|
52
|
+
unused_exports: list[UnusedExport] = field(default_factory=list)
|
|
53
|
+
total_exports_scanned: int = 0
|
|
54
|
+
error: str | None = None
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dependency staleness and security analysis for Node.js projects.
|
|
3
|
+
|
|
4
|
+
Wraps npm outdated and npm audit to detect outdated packages
|
|
5
|
+
and security vulnerabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pennyfarthing_scripts.dependencies.models import (
|
|
9
|
+
OutdatedPackage,
|
|
10
|
+
SecurityAdvisory,
|
|
11
|
+
DependenciesResult,
|
|
12
|
+
)
|
|
13
|
+
from pennyfarthing_scripts.dependencies.analyze import analyze_dependencies
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"OutdatedPackage",
|
|
17
|
+
"SecurityAdvisory",
|
|
18
|
+
"DependenciesResult",
|
|
19
|
+
"analyze_dependencies",
|
|
20
|
+
]
|