@kennethsolomon/shipkit 1.0.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 +321 -0
- package/bin/shipkit.js +146 -0
- package/commands/sk/brainstorm.md +63 -0
- package/commands/sk/branch.md +35 -0
- package/commands/sk/config.md +96 -0
- package/commands/sk/execute-plan.md +85 -0
- package/commands/sk/features.md +238 -0
- package/commands/sk/finish-feature.md +154 -0
- package/commands/sk/help.md +103 -0
- package/commands/sk/hotfix.md +61 -0
- package/commands/sk/plan.md +30 -0
- package/commands/sk/release.md +72 -0
- package/commands/sk/security-check.md +188 -0
- package/commands/sk/set-profile.md +71 -0
- package/commands/sk/status.md +25 -0
- package/commands/sk/update-task.md +35 -0
- package/commands/sk/write-plan.md +72 -0
- package/package.json +23 -0
- package/skills/sk:accessibility/LICENSE.txt +177 -0
- package/skills/sk:accessibility/SKILL.md +150 -0
- package/skills/sk:api-design/LICENSE.txt +177 -0
- package/skills/sk:api-design/SKILL.md +158 -0
- package/skills/sk:brainstorming/SKILL.md +124 -0
- package/skills/sk:debug/SKILL.md +252 -0
- package/skills/sk:debug/debug_conductor.py +177 -0
- package/skills/sk:debug/lib/__init__.py +1 -0
- package/skills/sk:debug/lib/bug_gatherer.py +55 -0
- package/skills/sk:debug/lib/context_reader.py +139 -0
- package/skills/sk:debug/lib/findings_writer.py +76 -0
- package/skills/sk:debug/lib/lessons_writer.py +165 -0
- package/skills/sk:debug/lib/step_runner.py +326 -0
- package/skills/sk:features/SKILL.md +238 -0
- package/skills/sk:frontend-design/LICENSE.txt +177 -0
- package/skills/sk:frontend-design/SKILL.md +191 -0
- package/skills/sk:laravel-init/SKILL.md +37 -0
- package/skills/sk:laravel-new/SKILL.md +68 -0
- package/skills/sk:lint/SKILL.md +113 -0
- package/skills/sk:perf/LICENSE.txt +177 -0
- package/skills/sk:perf/SKILL.md +188 -0
- package/skills/sk:release/SKILL.md +113 -0
- package/skills/sk:release/references/android-checklist.md +269 -0
- package/skills/sk:release/references/ios-checklist.md +339 -0
- package/skills/sk:release/release.sh +378 -0
- package/skills/sk:review/SKILL.md +346 -0
- package/skills/sk:review/references/security-checklist.md +223 -0
- package/skills/sk:schema-migrate/SKILL.md +125 -0
- package/skills/sk:schema-migrate/orms/drizzle.md +546 -0
- package/skills/sk:schema-migrate/orms/laravel.md +367 -0
- package/skills/sk:schema-migrate/orms/prisma.md +357 -0
- package/skills/sk:schema-migrate/orms/rails.md +351 -0
- package/skills/sk:schema-migrate/orms/sqlalchemy.md +385 -0
- package/skills/sk:schema-migrate/references/detection.md +110 -0
- package/skills/sk:setup-claude/SKILL.md +365 -0
- package/skills/sk:setup-claude/references/detection.md +6 -0
- package/skills/sk:setup-claude/references/templates.md +11 -0
- package/skills/sk:setup-claude/scripts/apply_setup_claude.py +443 -0
- package/skills/sk:setup-claude/scripts/detect_arch_changes.py +437 -0
- package/skills/sk:setup-claude/templates/.claude/docs/arch-changelog-guide.md.template +6 -0
- package/skills/sk:setup-claude/templates/.claude/docs/changelog-guide.md.template +12 -0
- package/skills/sk:setup-claude/templates/CHANGELOG.md.template +21 -0
- package/skills/sk:setup-claude/templates/CLAUDE.md.template +299 -0
- package/skills/sk:setup-claude/templates/arch-changelog-guide.md.template +3 -0
- package/skills/sk:setup-claude/templates/changelog-guide.md.template +3 -0
- package/skills/sk:setup-claude/templates/commands/brainstorm.md.template +74 -0
- package/skills/sk:setup-claude/templates/commands/execute-plan.md.template +57 -0
- package/skills/sk:setup-claude/templates/commands/features.md.template +238 -0
- package/skills/sk:setup-claude/templates/commands/finish-feature.md.template +155 -0
- package/skills/sk:setup-claude/templates/commands/plan.md.template +30 -0
- package/skills/sk:setup-claude/templates/commands/re-setup.md.template +38 -0
- package/skills/sk:setup-claude/templates/commands/release.md.template +74 -0
- package/skills/sk:setup-claude/templates/commands/security-check.md.template +172 -0
- package/skills/sk:setup-claude/templates/commands/status.md.template +17 -0
- package/skills/sk:setup-claude/templates/commands/write-plan.md.template +34 -0
- package/skills/sk:setup-claude/templates/finish-feature.md.template +3 -0
- package/skills/sk:setup-claude/templates/plan.md.template +3 -0
- package/skills/sk:setup-claude/templates/status.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks/findings.md.template +19 -0
- package/skills/sk:setup-claude/templates/tasks/lessons.md.template +26 -0
- package/skills/sk:setup-claude/templates/tasks/progress.md.template +20 -0
- package/skills/sk:setup-claude/templates/tasks/security-findings.md.template +5 -0
- package/skills/sk:setup-claude/templates/tasks/todo.md.template +26 -0
- package/skills/sk:setup-claude/templates/tasks/workflow-status.md.template +31 -0
- package/skills/sk:setup-claude/templates/tasks-findings.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks-lessons.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks-progress.md.template +3 -0
- package/skills/sk:setup-claude/templates/tasks-todo.md.template +3 -0
- package/skills/sk:setup-claude/tests/test_apply_setup_claude.py +193 -0
- package/skills/sk:setup-optimizer/SKILL.md +184 -0
- package/skills/sk:setup-optimizer/lib/__init__.py +24 -0
- package/skills/sk:setup-optimizer/lib/detect.py +205 -0
- package/skills/sk:setup-optimizer/lib/discover.py +221 -0
- package/skills/sk:setup-optimizer/lib/enrich.py +163 -0
- package/skills/sk:setup-optimizer/lib/merge.py +277 -0
- package/skills/sk:setup-optimizer/lib/sidecar.py +129 -0
- package/skills/sk:setup-optimizer/optimize_claude.py +174 -0
- package/skills/sk:setup-optimizer/templates/CLAUDE.md.template +105 -0
- package/skills/sk:skill-creator/LICENSE.txt +202 -0
- package/skills/sk:skill-creator/SKILL.md +479 -0
- package/skills/sk:skill-creator/agents/analyzer.md +274 -0
- package/skills/sk:skill-creator/agents/comparator.md +202 -0
- package/skills/sk:skill-creator/agents/grader.md +223 -0
- package/skills/sk:skill-creator/assets/eval_review.html +146 -0
- package/skills/sk:skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/sk:skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/sk:skill-creator/references/schemas.md +430 -0
- package/skills/sk:skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/sk:skill-creator/scripts/generate_report.py +326 -0
- package/skills/sk:skill-creator/scripts/improve_description.py +248 -0
- package/skills/sk:skill-creator/scripts/package_skill.py +136 -0
- package/skills/sk:skill-creator/scripts/quick_validate.py +103 -0
- package/skills/sk:skill-creator/scripts/run_eval.py +310 -0
- package/skills/sk:skill-creator/scripts/run_loop.py +332 -0
- package/skills/sk:skill-creator/scripts/utils.py +47 -0
- package/skills/sk:smart-commit/SKILL.md +175 -0
- package/skills/sk:test/SKILL.md +171 -0
- package/skills/sk:write-tests/SKILL.md +195 -0
- package/skills/sk:write-tests/references/patterns.md +209 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Intelligent project structure, documentation, and workflow discovery."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Dict, List, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Directories to exclude from discovery
|
|
8
|
+
EXCLUDE_DIRS = {
|
|
9
|
+
'node_modules', '.next', 'dist', 'build', 'out',
|
|
10
|
+
'.venv', 'venv', 'env', '__pycache__',
|
|
11
|
+
'.git', '.github/actions', 'vendor',
|
|
12
|
+
'.pytest_cache', '.mypy_cache', '.tox',
|
|
13
|
+
'coverage', '.coverage', 'htmlcov',
|
|
14
|
+
'egg-info', '.eggs', '*.egg-info',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# Documentation patterns to look for
|
|
18
|
+
DOC_PATTERNS = {
|
|
19
|
+
'README.md': 'Project overview',
|
|
20
|
+
'CONTRIBUTING.md': 'How to contribute',
|
|
21
|
+
'CODE_OF_CONDUCT.md': 'Code of conduct',
|
|
22
|
+
'CHANGELOG.md': 'Version history',
|
|
23
|
+
'LICENSE': 'License information',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def discover_directories(root_path: Path) -> Dict[str, str]:
|
|
28
|
+
"""Auto-discover key project directories and their purposes.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
root_path: Project root directory
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dict mapping directory names to descriptions
|
|
35
|
+
"""
|
|
36
|
+
found_dirs = {}
|
|
37
|
+
|
|
38
|
+
# Define directory patterns and their descriptions
|
|
39
|
+
dir_patterns = {
|
|
40
|
+
'src': 'Application source code',
|
|
41
|
+
'lib': 'Library utilities and helpers',
|
|
42
|
+
'app': 'Application code',
|
|
43
|
+
'source': 'Source code',
|
|
44
|
+
'tests': 'Test suite',
|
|
45
|
+
'test': 'Test suite',
|
|
46
|
+
'__tests__': 'Test suite',
|
|
47
|
+
'spec': 'Test specifications',
|
|
48
|
+
'docs': 'Documentation',
|
|
49
|
+
'public': 'Static assets and public files',
|
|
50
|
+
'static': 'Static assets',
|
|
51
|
+
'assets': 'Project assets',
|
|
52
|
+
'scripts': 'Utility and build scripts',
|
|
53
|
+
'config': 'Configuration files',
|
|
54
|
+
'migrations': 'Database migrations',
|
|
55
|
+
'prisma': 'Prisma database schema',
|
|
56
|
+
'alembic': 'SQLAlchemy migrations',
|
|
57
|
+
'.github': 'GitHub configuration and workflows',
|
|
58
|
+
'infra': 'Infrastructure as code',
|
|
59
|
+
'terraform': 'Terraform configuration',
|
|
60
|
+
'docker': 'Docker configuration',
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for dir_name, description in dir_patterns.items():
|
|
64
|
+
dir_path = root_path / dir_name
|
|
65
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
66
|
+
# Skip if in exclude list
|
|
67
|
+
if dir_name not in EXCLUDE_DIRS:
|
|
68
|
+
found_dirs[dir_name] = description
|
|
69
|
+
|
|
70
|
+
return found_dirs
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def discover_documentation(root_path: Path) -> Dict[str, str]:
|
|
74
|
+
"""Find documentation files and link them.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
root_path: Project root directory
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dict mapping doc file paths to descriptions
|
|
81
|
+
"""
|
|
82
|
+
found_docs = {}
|
|
83
|
+
|
|
84
|
+
# Check root level documentation
|
|
85
|
+
for pattern, description in DOC_PATTERNS.items():
|
|
86
|
+
doc_path = root_path / pattern
|
|
87
|
+
if doc_path.exists() and doc_path.is_file():
|
|
88
|
+
found_docs[pattern] = description
|
|
89
|
+
|
|
90
|
+
# Check docs/ directory for additional documentation
|
|
91
|
+
docs_dir = root_path / 'docs'
|
|
92
|
+
if docs_dir.exists() and docs_dir.is_dir():
|
|
93
|
+
# Look for common subdirectories and files
|
|
94
|
+
doc_files = {
|
|
95
|
+
'API.md': 'API documentation',
|
|
96
|
+
'api.md': 'API documentation',
|
|
97
|
+
'ARCHITECTURE.md': 'Architecture overview',
|
|
98
|
+
'architecture.md': 'Architecture overview',
|
|
99
|
+
'DEPLOYMENT.md': 'Deployment guide',
|
|
100
|
+
'deployment.md': 'Deployment guide',
|
|
101
|
+
'SETUP.md': 'Setup instructions',
|
|
102
|
+
'setup.md': 'Setup instructions',
|
|
103
|
+
'TROUBLESHOOTING.md': 'Troubleshooting guide',
|
|
104
|
+
'troubleshooting.md': 'Troubleshooting guide',
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for filename, description in doc_files.items():
|
|
108
|
+
doc_file = docs_dir / filename
|
|
109
|
+
if doc_file.exists() and doc_file.is_file():
|
|
110
|
+
found_docs[f'docs/{filename}'] = description
|
|
111
|
+
|
|
112
|
+
# Check for subdirectories
|
|
113
|
+
try:
|
|
114
|
+
for item in docs_dir.iterdir():
|
|
115
|
+
if item.is_dir() and item.name not in EXCLUDE_DIRS:
|
|
116
|
+
found_docs[f'docs/{item.name}/'] = f'{item.name.capitalize()} documentation'
|
|
117
|
+
except PermissionError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# Check .github for documentation
|
|
121
|
+
github_dir = root_path / '.github'
|
|
122
|
+
if github_dir.exists() and github_dir.is_dir():
|
|
123
|
+
contributing = github_dir / 'CONTRIBUTING.md'
|
|
124
|
+
if contributing.exists():
|
|
125
|
+
found_docs['.github/CONTRIBUTING.md'] = 'GitHub contribution guidelines'
|
|
126
|
+
|
|
127
|
+
return found_docs
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def discover_workflows(root_path: Path) -> Dict[str, List[str]]:
|
|
131
|
+
"""Discover build workflows, scripts, and common commands.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
root_path: Project root directory
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict mapping workflow type to list of commands/targets
|
|
138
|
+
"""
|
|
139
|
+
workflows = {}
|
|
140
|
+
|
|
141
|
+
# Check Makefile for targets
|
|
142
|
+
makefile_targets = _extract_makefile_targets(root_path)
|
|
143
|
+
if makefile_targets:
|
|
144
|
+
workflows['make'] = makefile_targets
|
|
145
|
+
|
|
146
|
+
# Check package.json for npm/yarn scripts
|
|
147
|
+
npm_scripts = _extract_npm_scripts(root_path)
|
|
148
|
+
if npm_scripts:
|
|
149
|
+
workflows['npm'] = npm_scripts
|
|
150
|
+
|
|
151
|
+
# Check for GitHub Actions workflows
|
|
152
|
+
github_workflows = _find_github_workflows(root_path)
|
|
153
|
+
if github_workflows:
|
|
154
|
+
workflows['workflows'] = github_workflows
|
|
155
|
+
|
|
156
|
+
return workflows
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _extract_makefile_targets(root_path: Path) -> List[str]:
|
|
160
|
+
"""Extract targets from Makefile."""
|
|
161
|
+
makefile = root_path / 'Makefile'
|
|
162
|
+
if not makefile.exists():
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
targets = []
|
|
166
|
+
try:
|
|
167
|
+
content = makefile.read_text()
|
|
168
|
+
for line in content.split('\n'):
|
|
169
|
+
if line.startswith('.PHONY'):
|
|
170
|
+
# Extract targets from .PHONY line
|
|
171
|
+
targets_str = line.split(':')[1].strip()
|
|
172
|
+
targets.extend(targets_str.split())
|
|
173
|
+
elif ':' in line and not line.startswith('\t') and not line.startswith(' '):
|
|
174
|
+
target = line.split(':')[0].strip()
|
|
175
|
+
if target and not target.startswith('.'):
|
|
176
|
+
targets.append(target)
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
|
|
180
|
+
return list(set(targets))[:10] # Limit to 10 targets
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _extract_npm_scripts(root_path: Path) -> List[str]:
|
|
184
|
+
"""Extract scripts from package.json."""
|
|
185
|
+
import json
|
|
186
|
+
|
|
187
|
+
package_json = root_path / 'package.json'
|
|
188
|
+
if not package_json.exists():
|
|
189
|
+
return []
|
|
190
|
+
|
|
191
|
+
scripts = []
|
|
192
|
+
try:
|
|
193
|
+
content = json.loads(package_json.read_text())
|
|
194
|
+
if 'scripts' in content:
|
|
195
|
+
# Include common scripts, exclude default ones
|
|
196
|
+
exclude = {'test', 'start', 'dev', 'build'}
|
|
197
|
+
for script_name in content['scripts'].keys():
|
|
198
|
+
if script_name not in exclude:
|
|
199
|
+
scripts.append(script_name)
|
|
200
|
+
except Exception:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
return scripts[:8] # Limit to 8 scripts
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _find_github_workflows(root_path: Path) -> List[str]:
|
|
207
|
+
"""Find GitHub Actions workflows."""
|
|
208
|
+
workflows_dir = root_path / '.github' / 'workflows'
|
|
209
|
+
if not workflows_dir.exists():
|
|
210
|
+
return []
|
|
211
|
+
|
|
212
|
+
workflows = []
|
|
213
|
+
try:
|
|
214
|
+
for workflow_file in workflows_dir.glob('*.yml'):
|
|
215
|
+
workflows.append(workflow_file.stem)
|
|
216
|
+
for workflow_file in workflows_dir.glob('*.yaml'):
|
|
217
|
+
workflows.append(workflow_file.stem)
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
return workflows[:5] # Limit to 5 workflows
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Enrich CLAUDE.md with comprehensive project context sections."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def generate_directories_section(discovered: Dict[str, str]) -> str:
|
|
7
|
+
"""Generate Key Directories section from discovered structure.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
discovered: Dict of directory_name -> description
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
Formatted section content
|
|
14
|
+
"""
|
|
15
|
+
if not discovered:
|
|
16
|
+
return ''
|
|
17
|
+
|
|
18
|
+
lines = []
|
|
19
|
+
for dir_name in sorted(discovered.keys()):
|
|
20
|
+
description = discovered[dir_name]
|
|
21
|
+
lines.append(f'- **{dir_name}/** - {description}')
|
|
22
|
+
|
|
23
|
+
return '\n'.join(lines)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def generate_documentation_section(docs: Dict[str, str]) -> str:
|
|
27
|
+
"""Generate Documentation & Resources section from discovered files.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
docs: Dict of file_path -> description
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Formatted section content
|
|
34
|
+
"""
|
|
35
|
+
if not docs:
|
|
36
|
+
return ''
|
|
37
|
+
|
|
38
|
+
lines = []
|
|
39
|
+
|
|
40
|
+
# Prioritize certain docs
|
|
41
|
+
priority_order = [
|
|
42
|
+
'README.md', 'CONTRIBUTING.md', 'docs/API.md', 'docs/api.md',
|
|
43
|
+
'docs/ARCHITECTURE.md', 'docs/architecture.md',
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
added = set()
|
|
47
|
+
|
|
48
|
+
# Add priority docs first
|
|
49
|
+
for pattern in priority_order:
|
|
50
|
+
if pattern in docs:
|
|
51
|
+
lines.append(f'- **{pattern}** - {docs[pattern]}')
|
|
52
|
+
added.add(pattern)
|
|
53
|
+
|
|
54
|
+
# Add remaining docs
|
|
55
|
+
for doc_path in sorted(docs.keys()):
|
|
56
|
+
if doc_path not in added:
|
|
57
|
+
description = docs[doc_path]
|
|
58
|
+
lines.append(f'- **{doc_path}** - {description}')
|
|
59
|
+
|
|
60
|
+
return '\n'.join(lines)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def generate_workflows_section(workflows: Dict[str, List[str]]) -> str:
|
|
64
|
+
"""Generate Common Workflows section from discovered commands.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
workflows: Dict of workflow_type -> list of commands
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Formatted section content
|
|
71
|
+
"""
|
|
72
|
+
if not workflows:
|
|
73
|
+
return ''
|
|
74
|
+
|
|
75
|
+
lines = []
|
|
76
|
+
|
|
77
|
+
# npm/yarn scripts
|
|
78
|
+
if 'npm' in workflows:
|
|
79
|
+
lines.append('**Common npm scripts:**')
|
|
80
|
+
for script in workflows['npm'][:5]:
|
|
81
|
+
lines.append(f'- `npm run {script}`')
|
|
82
|
+
lines.append('')
|
|
83
|
+
|
|
84
|
+
# Makefile targets
|
|
85
|
+
if 'make' in workflows:
|
|
86
|
+
lines.append('**Makefile targets:**')
|
|
87
|
+
for target in workflows['make'][:5]:
|
|
88
|
+
lines.append(f'- `make {target}`')
|
|
89
|
+
lines.append('')
|
|
90
|
+
|
|
91
|
+
# GitHub workflows
|
|
92
|
+
if 'workflows' in workflows:
|
|
93
|
+
lines.append('**GitHub Actions workflows:**')
|
|
94
|
+
for workflow in workflows['workflows'][:3]:
|
|
95
|
+
lines.append(f'- `.github/workflows/{workflow}.yml`')
|
|
96
|
+
lines.append('')
|
|
97
|
+
|
|
98
|
+
return '\n'.join(lines).strip()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def generate_directories_section_compact(discovered: Dict[str, str]) -> str:
|
|
102
|
+
"""Generate a compact version of Key Directories section.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
discovered: Dict of directory_name -> description
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Compact formatted section
|
|
109
|
+
"""
|
|
110
|
+
if not discovered:
|
|
111
|
+
return ''
|
|
112
|
+
|
|
113
|
+
# For compact mode, use inline format
|
|
114
|
+
lines = []
|
|
115
|
+
items = []
|
|
116
|
+
|
|
117
|
+
for dir_name in sorted(discovered.keys()):
|
|
118
|
+
items.append(f'`{dir_name}/`')
|
|
119
|
+
|
|
120
|
+
if items:
|
|
121
|
+
lines.append('Project structure:')
|
|
122
|
+
lines.append(', '.join(items))
|
|
123
|
+
|
|
124
|
+
return '\n'.join(lines)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def merge_into_development_section(
|
|
128
|
+
base_content: str,
|
|
129
|
+
discovered_workflows: Dict[str, List[str]],
|
|
130
|
+
) -> str:
|
|
131
|
+
"""Merge discovered workflows into Development section.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
base_content: Base development section content
|
|
135
|
+
discovered_workflows: Discovered workflows
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Enhanced development section
|
|
139
|
+
"""
|
|
140
|
+
lines = [base_content.rstrip()]
|
|
141
|
+
|
|
142
|
+
# Add discovered workflows if any
|
|
143
|
+
if discovered_workflows:
|
|
144
|
+
lines.append('')
|
|
145
|
+
lines.append('### Discovered Workflows')
|
|
146
|
+
lines.append(generate_workflows_section(discovered_workflows))
|
|
147
|
+
|
|
148
|
+
return '\n'.join(lines)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def estimate_content_lines(section_dict: Dict[str, str]) -> int:
|
|
152
|
+
"""Estimate total lines for section content.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
section_dict: Dict of sections
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Approximate total line count
|
|
159
|
+
"""
|
|
160
|
+
total = 0
|
|
161
|
+
for content in section_dict.values():
|
|
162
|
+
total += len(content.split('\n'))
|
|
163
|
+
return total
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Smart merging of CLAUDE.md sections with user customization preservation."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Dict, Tuple
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_claude_md(content: str) -> Dict[str, str]:
|
|
8
|
+
"""Parse CLAUDE.md into sections.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
content: CLAUDE.md content
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Dict mapping section names to their content
|
|
15
|
+
"""
|
|
16
|
+
sections = {}
|
|
17
|
+
current_section = None
|
|
18
|
+
current_content = []
|
|
19
|
+
|
|
20
|
+
lines = content.split('\n')
|
|
21
|
+
in_code_block = False
|
|
22
|
+
|
|
23
|
+
for line in lines:
|
|
24
|
+
# Track code blocks to avoid treating ``` as section markers
|
|
25
|
+
if line.strip().startswith('```'):
|
|
26
|
+
in_code_block = not in_code_block
|
|
27
|
+
|
|
28
|
+
# Check for section headers (## level)
|
|
29
|
+
if line.startswith('## ') and not in_code_block:
|
|
30
|
+
# Save previous section
|
|
31
|
+
if current_section:
|
|
32
|
+
sections[current_section] = '\n'.join(current_content).strip()
|
|
33
|
+
|
|
34
|
+
# Start new section
|
|
35
|
+
current_section = line[3:].strip()
|
|
36
|
+
current_content = []
|
|
37
|
+
elif current_section:
|
|
38
|
+
current_content.append(line)
|
|
39
|
+
|
|
40
|
+
# Save last section
|
|
41
|
+
if current_section:
|
|
42
|
+
sections[current_section] = '\n'.join(current_content).strip()
|
|
43
|
+
|
|
44
|
+
return sections
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def detect_user_edits(current: str, generated: str) -> bool:
|
|
48
|
+
"""Detect if a section has been user-customized.
|
|
49
|
+
|
|
50
|
+
Uses dual detection: content comparison + marker detection.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
current: Current section content from file
|
|
54
|
+
generated: What we would generate from template
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if section appears to be user-edited
|
|
58
|
+
"""
|
|
59
|
+
current = current.strip()
|
|
60
|
+
generated = generated.strip()
|
|
61
|
+
|
|
62
|
+
# Check for user edit marker
|
|
63
|
+
if '<!-- EDITED -->' in current:
|
|
64
|
+
return True
|
|
65
|
+
|
|
66
|
+
# Check if content differs significantly from generated version
|
|
67
|
+
if current == generated:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
# If content is substantially different, mark as user-edited
|
|
71
|
+
current_len = len(current)
|
|
72
|
+
generated_len = len(generated)
|
|
73
|
+
|
|
74
|
+
# Allow small variations (formatting), but flag major differences
|
|
75
|
+
if current_len == 0 and generated_len == 0:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
if current_len == 0 or generated_len == 0:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
# Check if content matches at least 50% (allowing for additions)
|
|
82
|
+
matching_ratio = len(set(current) & set(generated)) / max(current_len, generated_len)
|
|
83
|
+
if matching_ratio < 0.5:
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def should_lock_section(section_name: str, current_content: str) -> bool:
|
|
90
|
+
"""Determine if a section should be locked from regeneration.
|
|
91
|
+
|
|
92
|
+
Sections are locked if:
|
|
93
|
+
1. They have <!-- LOCK --> marker
|
|
94
|
+
2. They're in the smart default lock list AND have user content
|
|
95
|
+
3. They're marked as edited
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
section_name: Name of the section
|
|
99
|
+
current_content: Current content of the section
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
True if section should be locked
|
|
103
|
+
"""
|
|
104
|
+
# Explicit lock marker
|
|
105
|
+
if '<!-- LOCK -->' in current_content:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
# Smart defaults - auto-lock these if they have user content
|
|
109
|
+
auto_lock_sections = {
|
|
110
|
+
'Important Context': True,
|
|
111
|
+
'Known Issues': True,
|
|
112
|
+
'Notes': True,
|
|
113
|
+
'Custom Configuration': True,
|
|
114
|
+
'Recommended Workflow': True,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if section_name in auto_lock_sections and current_content.strip():
|
|
118
|
+
# Check if it looks like user content (not template boilerplate)
|
|
119
|
+
if not current_content.startswith('[') and len(current_content) > 20:
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def merge_sections(
|
|
126
|
+
existing: Dict[str, str],
|
|
127
|
+
generated: Dict[str, str],
|
|
128
|
+
) -> Tuple[Dict[str, str], Dict[str, str]]:
|
|
129
|
+
"""Merge existing and generated sections smartly.
|
|
130
|
+
|
|
131
|
+
Strategy:
|
|
132
|
+
- Keep generated sections as base
|
|
133
|
+
- Override with existing sections if user-edited
|
|
134
|
+
- Lock sections if needed
|
|
135
|
+
- Return (merged_sections, preservation_report)
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
existing: Current sections from file
|
|
139
|
+
generated: Newly generated sections from template
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Tuple of (merged sections dict, report dict of what was preserved)
|
|
143
|
+
"""
|
|
144
|
+
merged = {}
|
|
145
|
+
preserved = {}
|
|
146
|
+
|
|
147
|
+
# Process all sections (both existing and generated)
|
|
148
|
+
all_sections = set(list(existing.keys()) + list(generated.keys()))
|
|
149
|
+
|
|
150
|
+
for section_name in all_sections:
|
|
151
|
+
existing_content = existing.get(section_name, '').strip()
|
|
152
|
+
generated_content = generated.get(section_name, '').strip()
|
|
153
|
+
|
|
154
|
+
# Determine if user customized this section
|
|
155
|
+
is_user_edited = (
|
|
156
|
+
existing_content and
|
|
157
|
+
detect_user_edits(existing_content, generated_content)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Determine if section should be locked
|
|
161
|
+
is_locked = should_lock_section(section_name, existing_content)
|
|
162
|
+
|
|
163
|
+
if is_locked or is_user_edited:
|
|
164
|
+
# Preserve user content
|
|
165
|
+
merged[section_name] = existing_content
|
|
166
|
+
preserved[section_name] = 'user-edited' if is_user_edited else 'locked'
|
|
167
|
+
elif existing_content and generated_content:
|
|
168
|
+
# Both exist - prefer generated but keep structure
|
|
169
|
+
merged[section_name] = generated_content
|
|
170
|
+
elif existing_content:
|
|
171
|
+
# Only exists in current
|
|
172
|
+
merged[section_name] = existing_content
|
|
173
|
+
preserved[section_name] = 'preserved'
|
|
174
|
+
else:
|
|
175
|
+
# Only in generated or both same
|
|
176
|
+
merged[section_name] = generated_content
|
|
177
|
+
|
|
178
|
+
return merged, preserved
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def reconstruct_claude_md(
|
|
182
|
+
sections: Dict[str, str],
|
|
183
|
+
include_marker: bool = True,
|
|
184
|
+
) -> str:
|
|
185
|
+
"""Reconstruct CLAUDE.md from sections.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
sections: Dict of section_name -> content
|
|
189
|
+
include_marker: Whether to add generation marker
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
Reconstructed CLAUDE.md content
|
|
193
|
+
"""
|
|
194
|
+
# Define standard section order
|
|
195
|
+
section_order = [
|
|
196
|
+
'Project Header', # Placeholder for title/description
|
|
197
|
+
'Stack',
|
|
198
|
+
'Quick Start',
|
|
199
|
+
'Key Directories',
|
|
200
|
+
'Documentation & Resources',
|
|
201
|
+
'Development',
|
|
202
|
+
'Common Workflows',
|
|
203
|
+
'Build & Deploy',
|
|
204
|
+
'Recommended Workflow',
|
|
205
|
+
'Important Context',
|
|
206
|
+
'Environment Variables',
|
|
207
|
+
'Common Tasks',
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
lines = []
|
|
211
|
+
|
|
212
|
+
# Add sections in order (putting unordered sections at end)
|
|
213
|
+
added_sections = set()
|
|
214
|
+
for section_name in section_order:
|
|
215
|
+
if section_name in sections and section_name not in added_sections:
|
|
216
|
+
content = sections[section_name].strip()
|
|
217
|
+
if content:
|
|
218
|
+
lines.append(f'## {section_name}')
|
|
219
|
+
lines.append('')
|
|
220
|
+
lines.append(content)
|
|
221
|
+
lines.append('')
|
|
222
|
+
added_sections.add(section_name)
|
|
223
|
+
|
|
224
|
+
# Add any remaining sections
|
|
225
|
+
for section_name in sorted(sections.keys()):
|
|
226
|
+
if section_name not in added_sections:
|
|
227
|
+
content = sections[section_name].strip()
|
|
228
|
+
if content:
|
|
229
|
+
lines.append(f'## {section_name}')
|
|
230
|
+
lines.append('')
|
|
231
|
+
lines.append(content)
|
|
232
|
+
lines.append('')
|
|
233
|
+
|
|
234
|
+
# Add marker
|
|
235
|
+
if include_marker:
|
|
236
|
+
lines.append('<!-- Generated by /setup-claude-tools -->')
|
|
237
|
+
|
|
238
|
+
result = '\n'.join(lines).strip()
|
|
239
|
+
return result + '\n'
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def extract_preservation_report(preserved: Dict[str, str]) -> str:
|
|
243
|
+
"""Format a human-readable report of preserved content.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
preserved: Dict of section_name -> preservation_reason
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Formatted report string
|
|
250
|
+
"""
|
|
251
|
+
if not preserved:
|
|
252
|
+
return ''
|
|
253
|
+
|
|
254
|
+
report_lines = ['📝 Preservation Report:', '']
|
|
255
|
+
|
|
256
|
+
user_edited = [s for s, r in preserved.items() if r == 'user-edited']
|
|
257
|
+
locked = [s for s, r in preserved.items() if r == 'locked']
|
|
258
|
+
kept = [s for s, r in preserved.items() if r == 'preserved']
|
|
259
|
+
|
|
260
|
+
if user_edited:
|
|
261
|
+
report_lines.append('✅ Preserved (user-edited):')
|
|
262
|
+
for section in user_edited:
|
|
263
|
+
report_lines.append(f' - {section}')
|
|
264
|
+
report_lines.append('')
|
|
265
|
+
|
|
266
|
+
if locked:
|
|
267
|
+
report_lines.append('🔒 Preserved (locked):')
|
|
268
|
+
for section in locked:
|
|
269
|
+
report_lines.append(f' - {section}')
|
|
270
|
+
report_lines.append('')
|
|
271
|
+
|
|
272
|
+
if kept:
|
|
273
|
+
report_lines.append('📌 Preserved (as-is):')
|
|
274
|
+
for section in kept:
|
|
275
|
+
report_lines.append(f' - {section}')
|
|
276
|
+
|
|
277
|
+
return '\n'.join(report_lines)
|