@softspark/ai-toolkit 1.3.14 → 1.4.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/CHANGELOG.md +41 -0
- package/README.md +62 -16
- package/app/agents/backend-specialist.md +8 -0
- package/app/agents/code-reviewer.md +9 -0
- package/app/agents/database-architect.md +8 -0
- package/app/agents/debugger.md +8 -0
- package/app/agents/devops-implementer.md +8 -0
- package/app/agents/documenter.md +8 -0
- package/app/agents/frontend-specialist.md +8 -0
- package/app/agents/performance-optimizer.md +8 -0
- package/app/agents/security-auditor.md +9 -0
- package/app/agents/test-engineer.md +9 -0
- package/app/skills/analyze/SKILL.md +15 -0
- package/app/skills/api-patterns/SKILL.md +10 -0
- package/app/skills/ci-cd-patterns/SKILL.md +10 -0
- package/app/skills/clean-code/SKILL.md +10 -0
- package/app/skills/database-patterns/SKILL.md +10 -0
- package/app/skills/debug/SKILL.md +16 -0
- package/app/skills/docs/SKILL.md +16 -0
- package/app/skills/git-mastery/SKILL.md +10 -0
- package/app/skills/onboard/SKILL.md +15 -0
- package/app/skills/performance-profiling/SKILL.md +10 -0
- package/app/skills/plan/SKILL.md +16 -0
- package/app/skills/refactor/SKILL.md +16 -0
- package/app/skills/review/SKILL.md +58 -3
- package/app/skills/security-patterns/SKILL.md +10 -0
- package/app/skills/tdd/SKILL.md +6 -0
- package/app/skills/testing-patterns/SKILL.md +10 -0
- package/bin/ai-toolkit.js +33 -5
- package/kb/procedures/release-verification-sop.md +283 -0
- package/kb/reference/architecture-overview.md +36 -7
- package/kb/reference/competitive-features-implementation.md +51 -52
- package/kb/reference/language-rules.md +18 -4
- package/kb/reference/skills-catalog.md +57 -1
- package/llms-full.txt +451 -64
- package/llms.txt +1 -0
- package/manifest.json +1 -1
- package/package.json +4 -2
- package/scripts/dir_rules_shared.py +441 -0
- package/scripts/generate_antigravity.py +36 -0
- package/scripts/generate_augment_rules.py +107 -0
- package/scripts/generate_cline_rules.py +31 -0
- package/scripts/generate_conventions.py +37 -0
- package/scripts/generate_cursor_mdc.py +127 -0
- package/scripts/generate_roo_rules.py +30 -0
- package/scripts/generate_windsurf_rules.py +31 -0
- package/scripts/install.py +26 -1
- package/scripts/install_steps/ai_tools.py +149 -31
- package/scripts/install_steps/detect_language.py +68 -5
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate .cursor/rules/*.mdc files for Cursor IDE.
|
|
3
|
+
|
|
4
|
+
Cursor reads rules from .cursor/rules/*.mdc (since Cursor 0.45).
|
|
5
|
+
Each .mdc file has YAML frontmatter controlling when the rule applies:
|
|
6
|
+
- alwaysApply: true — always in context
|
|
7
|
+
- globs: ["**/*.ts"] — auto-attached for matching files
|
|
8
|
+
- description: "..." — AI decides whether to include (Agent Requested)
|
|
9
|
+
|
|
10
|
+
The legacy .cursorrules format is still generated separately by
|
|
11
|
+
generate_cursor_rules.py for backwards compatibility.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python3 scripts/generate_cursor_mdc.py [target-dir]
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
|
+
from dir_rules_shared import (
|
|
23
|
+
PREFIX,
|
|
24
|
+
cleanup_stale,
|
|
25
|
+
rule_agents_and_skills,
|
|
26
|
+
rule_code_style,
|
|
27
|
+
rule_quality_standards,
|
|
28
|
+
rule_security,
|
|
29
|
+
rule_testing,
|
|
30
|
+
rule_workflow,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# .mdc wrapper: prepends YAML frontmatter to markdown content
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
def _mdc(content: str, *, description: str = "",
|
|
39
|
+
globs: list[str] | None = None,
|
|
40
|
+
always_apply: bool = False) -> str:
|
|
41
|
+
"""Wrap markdown content with Cursor .mdc YAML frontmatter."""
|
|
42
|
+
lines = ["---"]
|
|
43
|
+
if description:
|
|
44
|
+
lines.append(f"description: {description}")
|
|
45
|
+
if globs:
|
|
46
|
+
globs_str = ", ".join(f'"{g}"' for g in globs)
|
|
47
|
+
lines.append(f"globs: [{globs_str}]")
|
|
48
|
+
lines.append(f"alwaysApply: {'true' if always_apply else 'false'}")
|
|
49
|
+
lines.append("---")
|
|
50
|
+
lines.append("")
|
|
51
|
+
lines.append(content.rstrip("\n"))
|
|
52
|
+
lines.append("")
|
|
53
|
+
return "\n".join(lines)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Rule files registry
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def _make_rules() -> dict[str, callable]:
|
|
61
|
+
"""Build the .mdc rule file registry."""
|
|
62
|
+
return {
|
|
63
|
+
# Always active
|
|
64
|
+
f"{PREFIX}agents-and-skills.mdc": lambda: _mdc(
|
|
65
|
+
rule_agents_and_skills(),
|
|
66
|
+
description="AI toolkit agents, skills, and guidelines catalog",
|
|
67
|
+
always_apply=True,
|
|
68
|
+
),
|
|
69
|
+
f"{PREFIX}security.mdc": lambda: _mdc(
|
|
70
|
+
rule_security(),
|
|
71
|
+
description="Security rules — OWASP, secrets, input validation",
|
|
72
|
+
always_apply=True,
|
|
73
|
+
),
|
|
74
|
+
f"{PREFIX}quality-standards.mdc": lambda: _mdc(
|
|
75
|
+
rule_quality_standards(),
|
|
76
|
+
description="Quality standards — tests, safety, operational integrity",
|
|
77
|
+
always_apply=True,
|
|
78
|
+
),
|
|
79
|
+
# Auto-attached by file type
|
|
80
|
+
f"{PREFIX}code-style.mdc": lambda: _mdc(
|
|
81
|
+
rule_code_style(),
|
|
82
|
+
description="Code style conventions for all languages",
|
|
83
|
+
),
|
|
84
|
+
f"{PREFIX}testing.mdc": lambda: _mdc(
|
|
85
|
+
rule_testing(),
|
|
86
|
+
description="Testing standards and patterns",
|
|
87
|
+
globs=["**/*.test.*", "**/*.spec.*", "**/test_*", "**/tests/**"],
|
|
88
|
+
),
|
|
89
|
+
f"{PREFIX}workflow.mdc": lambda: _mdc(
|
|
90
|
+
rule_workflow(),
|
|
91
|
+
description="Development workflow — planning, commits, quality gates",
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
RULES = _make_rules()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Main
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def generate(target_dir: Path) -> None:
|
|
104
|
+
"""Write .cursor/rules/*.mdc files to target_dir."""
|
|
105
|
+
rules_dir = target_dir / ".cursor" / "rules"
|
|
106
|
+
rules_dir.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
|
|
108
|
+
# Clean stale ai-toolkit-*.mdc files
|
|
109
|
+
current = set(RULES.keys())
|
|
110
|
+
if rules_dir.is_dir():
|
|
111
|
+
for f in rules_dir.iterdir():
|
|
112
|
+
if f.name.startswith(PREFIX) and f.suffix == ".mdc" and f.name not in current:
|
|
113
|
+
f.unlink()
|
|
114
|
+
print(f" Removed stale: .cursor/rules/{f.name}")
|
|
115
|
+
|
|
116
|
+
for filename, content_fn in RULES.items():
|
|
117
|
+
(rules_dir / filename).write_text(content_fn(), encoding="utf-8")
|
|
118
|
+
print(f" Generated: .cursor/rules/{filename}")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def main() -> None:
|
|
122
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
123
|
+
generate(target)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
main()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate .roo/rules/*.md shared rules for Roo Code.
|
|
3
|
+
|
|
4
|
+
Roo Code reads shared rules from .roo/rules/*.md (applied to all modes).
|
|
5
|
+
The .roomodes JSON is still generated separately by generate_roo_modes.py.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python3 scripts/generate_roo_rules.py [target-dir]
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
16
|
+
from dir_rules_shared import STANDARD_RULES, write_rules
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def generate(target_dir: Path) -> None:
|
|
20
|
+
"""Write .roo/rules/*.md files to target_dir."""
|
|
21
|
+
write_rules(target_dir, STANDARD_RULES, ".roo/rules")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
26
|
+
generate(target)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if __name__ == "__main__":
|
|
30
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate .windsurf/rules/*.md files for Windsurf IDE.
|
|
3
|
+
|
|
4
|
+
Windsurf reads directory-based rules from .windsurf/rules/*.md (since mid-2025).
|
|
5
|
+
The legacy .windsurfrules single-file format is still generated separately
|
|
6
|
+
by generate_windsurf.py for backwards compatibility.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 scripts/generate_windsurf_rules.py [target-dir]
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
|
+
from dir_rules_shared import STANDARD_RULES, write_rules
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def generate(target_dir: Path) -> None:
|
|
21
|
+
"""Write .windsurf/rules/*.md files to target_dir."""
|
|
22
|
+
write_rules(target_dir, STANDARD_RULES, ".windsurf/rules")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
target = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.cwd()
|
|
27
|
+
generate(target)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
package/scripts/install.py
CHANGED
|
@@ -197,6 +197,8 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
197
197
|
"modules": "",
|
|
198
198
|
"auto_detect": False,
|
|
199
199
|
"status": False,
|
|
200
|
+
"lang": "",
|
|
201
|
+
"editors": "",
|
|
200
202
|
}
|
|
201
203
|
i = 0
|
|
202
204
|
while i < len(argv):
|
|
@@ -236,6 +238,16 @@ def parse_args(argv: list[str]) -> dict:
|
|
|
236
238
|
elif arg == "--modules":
|
|
237
239
|
i += 1
|
|
238
240
|
cfg["modules"] = argv[i] if i < len(argv) else ""
|
|
241
|
+
elif arg.startswith("--lang="):
|
|
242
|
+
cfg["lang"] = arg.split("=", 1)[1]
|
|
243
|
+
elif arg == "--lang":
|
|
244
|
+
i += 1
|
|
245
|
+
cfg["lang"] = argv[i] if i < len(argv) else ""
|
|
246
|
+
elif arg.startswith("--editors="):
|
|
247
|
+
cfg["editors"] = arg.split("=", 1)[1]
|
|
248
|
+
elif arg == "--editors":
|
|
249
|
+
i += 1
|
|
250
|
+
cfg["editors"] = argv[i] if i < len(argv) else ""
|
|
239
251
|
elif arg.startswith("-"):
|
|
240
252
|
print(f"Unknown option: {arg}")
|
|
241
253
|
sys.exit(1)
|
|
@@ -405,6 +417,17 @@ def main() -> None:
|
|
|
405
417
|
persona: str = cfg["persona"]
|
|
406
418
|
modules_arg: str = cfg["modules"]
|
|
407
419
|
auto_detect: bool = cfg["auto_detect"]
|
|
420
|
+
lang_arg: str = cfg["lang"]
|
|
421
|
+
|
|
422
|
+
# --lang <list> → merge into --modules as rules-<lang> entries
|
|
423
|
+
_LANG_ALIASES = {"go": "golang", "c++": "cpp", "c#": "csharp", "cs": "csharp"}
|
|
424
|
+
if lang_arg:
|
|
425
|
+
langs = [_LANG_ALIASES.get(l.strip(), l.strip()) for l in lang_arg.split(",") if l.strip()]
|
|
426
|
+
lang_modules = ",".join(f"rules-{l}" for l in langs)
|
|
427
|
+
modules_arg = f"{modules_arg},{lang_modules}" if modules_arg else lang_modules
|
|
428
|
+
auto_detect = False # explicit --lang overrides auto-detect
|
|
429
|
+
if not local:
|
|
430
|
+
local = True # language rules are project-local
|
|
408
431
|
|
|
409
432
|
rules_dir = Path.home() / ".ai-toolkit" / "rules"
|
|
410
433
|
hooks_scripts_dir = Path.home() / ".ai-toolkit" / "hooks"
|
|
@@ -447,7 +470,9 @@ def main() -> None:
|
|
|
447
470
|
if local:
|
|
448
471
|
# Pass language modules for --auto-detect / --modules rules-*
|
|
449
472
|
lang_modules = [m for m in (resolved_modules or []) if m.startswith("rules-")]
|
|
450
|
-
|
|
473
|
+
editors_arg: str = cfg["editors"]
|
|
474
|
+
install_local_project(rules_dir, dry_run, reset, lang_modules or None,
|
|
475
|
+
editors=editors_arg)
|
|
451
476
|
|
|
452
477
|
install_persona(target_dir, persona, dry_run)
|
|
453
478
|
install_strict_git_hooks(profile, local, dry_run)
|
|
@@ -120,18 +120,81 @@ def run_script(script_name: str, *args: str, capture: bool = False) -> str:
|
|
|
120
120
|
return result.stdout if capture else ""
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
# All known editor identifiers for --editors flag
|
|
124
|
+
ALL_EDITORS = [
|
|
125
|
+
"copilot", "cursor", "windsurf", "cline", "roo",
|
|
126
|
+
"aider", "augment", "antigravity",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
# Map of project files/dirs → editor names for auto-detection
|
|
130
|
+
_EDITOR_MARKERS: dict[str, str] = {
|
|
131
|
+
".github/copilot-instructions.md": "copilot",
|
|
132
|
+
".cursorrules": "cursor",
|
|
133
|
+
".cursor/rules": "cursor",
|
|
134
|
+
".windsurfrules": "windsurf",
|
|
135
|
+
".windsurf/rules": "windsurf",
|
|
136
|
+
".clinerules": "cline",
|
|
137
|
+
".cline/rules": "cline",
|
|
138
|
+
".roomodes": "roo",
|
|
139
|
+
".roo/rules": "roo",
|
|
140
|
+
".aider.conf.yml": "aider",
|
|
141
|
+
"CONVENTIONS.md": "aider",
|
|
142
|
+
".augment/rules": "augment",
|
|
143
|
+
".agent/rules": "antigravity",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _detect_editors(cwd: Path) -> list[str]:
|
|
148
|
+
"""Detect which editors have configs in the project directory."""
|
|
149
|
+
found: set[str] = set()
|
|
150
|
+
for marker, editor in _EDITOR_MARKERS.items():
|
|
151
|
+
p = cwd / marker
|
|
152
|
+
if p.exists():
|
|
153
|
+
found.add(editor)
|
|
154
|
+
return sorted(found)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _resolve_editors(editors_arg: str, cwd: Path) -> list[str]:
|
|
158
|
+
"""Resolve --editors argument to a list of editor names.
|
|
159
|
+
|
|
160
|
+
- "" → auto-detect from existing project files (empty if none found)
|
|
161
|
+
- "all" → all editors
|
|
162
|
+
- "cursor,aider" → explicit list
|
|
163
|
+
"""
|
|
164
|
+
if editors_arg == "all":
|
|
165
|
+
return list(ALL_EDITORS)
|
|
166
|
+
if editors_arg:
|
|
167
|
+
return [e.strip() for e in editors_arg.split(",") if e.strip()]
|
|
168
|
+
# Auto-detect: return editors that already have configs in the project
|
|
169
|
+
return _detect_editors(cwd)
|
|
170
|
+
|
|
171
|
+
|
|
123
172
|
def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
124
|
-
language_modules: list[str] | None = None
|
|
125
|
-
|
|
173
|
+
language_modules: list[str] | None = None,
|
|
174
|
+
editors: str = "") -> None:
|
|
175
|
+
"""Install project-local configs.
|
|
176
|
+
|
|
177
|
+
Claude Code configs (CLAUDE.md, settings, constitution) are always installed.
|
|
178
|
+
Editor configs are installed based on ``--editors`` flag:
|
|
179
|
+
- ``--editors all``: install all editors
|
|
180
|
+
- ``--editors cursor,aider``: install only these
|
|
181
|
+
- (empty): auto-detect from existing project files, install only those
|
|
182
|
+
"""
|
|
126
183
|
cwd = Path.cwd()
|
|
184
|
+
resolved_editors = _resolve_editors(editors, cwd)
|
|
185
|
+
|
|
127
186
|
print()
|
|
128
187
|
print(f"## Project-local ({cwd})")
|
|
129
188
|
if reset:
|
|
130
189
|
print(" Mode: RESET (all local configs will be wiped and recreated)")
|
|
190
|
+
if resolved_editors:
|
|
191
|
+
print(f" Editors: {', '.join(resolved_editors)}")
|
|
192
|
+
else:
|
|
193
|
+
print(" Editors: none (use --editors <list> or --editors all to enable)")
|
|
131
194
|
print()
|
|
132
195
|
|
|
133
196
|
if dry_run:
|
|
134
|
-
_install_local_dry_run(reset)
|
|
197
|
+
_install_local_dry_run(reset, resolved_editors)
|
|
135
198
|
if language_modules:
|
|
136
199
|
print(f" Would inject language rules: {', '.join(language_modules)}")
|
|
137
200
|
return
|
|
@@ -162,7 +225,8 @@ def install_local_project(rules_dir: Path, dry_run: bool, reset: bool,
|
|
|
162
225
|
# Inject language-specific rules into project CLAUDE.md
|
|
163
226
|
_inject_language_rules(cwd, language_modules)
|
|
164
227
|
|
|
165
|
-
|
|
228
|
+
# Install editor configs only for resolved editors
|
|
229
|
+
_create_local_ai_tool_configs(cwd, rules_dir, resolved_editors)
|
|
166
230
|
|
|
167
231
|
|
|
168
232
|
def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> None:
|
|
@@ -226,21 +290,36 @@ def _inject_language_rules(cwd: Path, language_modules: list[str] | None) -> Non
|
|
|
226
290
|
tmp_path.unlink(missing_ok=True)
|
|
227
291
|
|
|
228
292
|
|
|
229
|
-
def _install_local_dry_run(reset: bool) -> None:
|
|
293
|
+
def _install_local_dry_run(reset: bool, editors: list[str] | None = None) -> None:
|
|
294
|
+
eds = set(editors or [])
|
|
230
295
|
if reset:
|
|
231
296
|
print(" Would remove: CLAUDE.md, .claude/settings.local.json")
|
|
232
|
-
print(" Would remove: .claude/constitution.md
|
|
297
|
+
print(" Would remove: .claude/constitution.md and all editor configs")
|
|
233
298
|
print(" Would recreate all from templates (clean slate)")
|
|
234
|
-
print(" Would install git hooks (if .git/hooks exists)")
|
|
235
299
|
else:
|
|
236
300
|
print(" Would create: CLAUDE.md (if missing)")
|
|
237
301
|
print(" Would create: .claude/settings.local.json (if missing)")
|
|
238
302
|
print(" Would inject: .claude/constitution.md")
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
303
|
+
|
|
304
|
+
# Editor-specific dry-run messages
|
|
305
|
+
_EDITOR_DRY_RUN = {
|
|
306
|
+
"copilot": " Would inject: .github/copilot-instructions.md",
|
|
307
|
+
"cursor": " Would generate: .cursorrules + .cursor/rules/*.mdc",
|
|
308
|
+
"windsurf": " Would generate: .windsurfrules + .windsurf/rules/*.md",
|
|
309
|
+
"cline": " Would generate: .clinerules + .cline/rules/*.md",
|
|
310
|
+
"roo": " Would generate: .roomodes + .roo/rules/*.md",
|
|
311
|
+
"aider": " Would generate: .aider.conf.yml + CONVENTIONS.md",
|
|
312
|
+
"augment": " Would generate: .augment/rules/ai-toolkit-*.md",
|
|
313
|
+
"antigravity": " Would generate: .agent/rules/ + .agent/workflows/",
|
|
314
|
+
}
|
|
315
|
+
for ed, msg in _EDITOR_DRY_RUN.items():
|
|
316
|
+
if ed in eds:
|
|
317
|
+
print(msg)
|
|
318
|
+
|
|
319
|
+
if not eds:
|
|
320
|
+
print(" No editors selected (use --editors <list> or --editors all)")
|
|
321
|
+
|
|
322
|
+
print(" Would install: .git/hooks/pre-commit")
|
|
244
323
|
|
|
245
324
|
|
|
246
325
|
def _reset_local_configs(cwd: Path) -> None:
|
|
@@ -314,24 +393,63 @@ def _create_local_settings(cwd: Path, reset: bool) -> None:
|
|
|
314
393
|
print(" Kept: .claude/settings.local.json (already exists)")
|
|
315
394
|
|
|
316
395
|
|
|
317
|
-
def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
396
|
+
def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
397
|
+
editors: list[str]) -> None:
|
|
398
|
+
eds = set(editors)
|
|
399
|
+
|
|
400
|
+
if "copilot" in eds:
|
|
401
|
+
inject_with_rules(
|
|
402
|
+
"generate-copilot.sh",
|
|
403
|
+
cwd / ".github" / "copilot-instructions.md",
|
|
404
|
+
rules_dir,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
if "cursor" in eds:
|
|
408
|
+
inject_with_rules(
|
|
409
|
+
"generate-cursor-rules.sh",
|
|
410
|
+
cwd / ".cursorrules",
|
|
411
|
+
rules_dir,
|
|
412
|
+
)
|
|
413
|
+
from generate_cursor_mdc import generate as gen_cursor_mdc
|
|
414
|
+
gen_cursor_mdc(cwd)
|
|
415
|
+
|
|
416
|
+
if "windsurf" in eds:
|
|
417
|
+
inject_with_rules(
|
|
418
|
+
"generate-windsurf.sh",
|
|
419
|
+
cwd / ".windsurfrules",
|
|
420
|
+
rules_dir,
|
|
421
|
+
)
|
|
422
|
+
from generate_windsurf_rules import generate as gen_windsurf_rules
|
|
423
|
+
gen_windsurf_rules(cwd)
|
|
424
|
+
|
|
425
|
+
if "cline" in eds:
|
|
426
|
+
inject_with_rules(
|
|
427
|
+
"generate-cline.sh",
|
|
428
|
+
cwd / ".clinerules",
|
|
429
|
+
rules_dir,
|
|
430
|
+
)
|
|
431
|
+
from generate_cline_rules import generate as gen_cline_rules
|
|
432
|
+
gen_cline_rules(cwd)
|
|
433
|
+
|
|
434
|
+
if "roo" in eds:
|
|
435
|
+
roo_output = run_script("generate-roo-modes.sh", capture=True)
|
|
436
|
+
(cwd / ".roomodes").write_text(roo_output, encoding="utf-8")
|
|
437
|
+
print(" Created: .roomodes")
|
|
438
|
+
from generate_roo_rules import generate as gen_roo_rules
|
|
439
|
+
gen_roo_rules(cwd)
|
|
440
|
+
|
|
441
|
+
if "aider" in eds:
|
|
442
|
+
aider_output = run_script("generate-aider-conf.sh", capture=True)
|
|
443
|
+
(cwd / ".aider.conf.yml").write_text(aider_output, encoding="utf-8")
|
|
444
|
+
print(" Created: .aider.conf.yml")
|
|
445
|
+
inject_with_rules("generate_conventions.py", cwd / "CONVENTIONS.md", rules_dir)
|
|
446
|
+
|
|
447
|
+
if "augment" in eds:
|
|
448
|
+
from generate_augment_rules import generate as gen_augment_rules
|
|
449
|
+
gen_augment_rules(cwd)
|
|
450
|
+
|
|
451
|
+
if "antigravity" in eds:
|
|
452
|
+
from generate_antigravity import generate as gen_antigravity
|
|
453
|
+
gen_antigravity(cwd)
|
|
336
454
|
|
|
337
455
|
run_script("install-git-hooks.sh", str(cwd))
|
|
@@ -1,10 +1,40 @@
|
|
|
1
|
-
"""Detect project language from file markers
|
|
1
|
+
"""Detect project language from file markers and source file extensions."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
|
|
8
|
+
# Map file extensions to manifest module names.
|
|
9
|
+
# Only extensions that unambiguously identify a language are listed.
|
|
10
|
+
_EXT_TO_MODULE: dict[str, str] = {
|
|
11
|
+
".py": "rules-python",
|
|
12
|
+
".ts": "rules-typescript",
|
|
13
|
+
".tsx": "rules-typescript",
|
|
14
|
+
".go": "rules-golang",
|
|
15
|
+
".rs": "rules-rust",
|
|
16
|
+
".java": "rules-java",
|
|
17
|
+
".kt": "rules-kotlin",
|
|
18
|
+
".kts": "rules-kotlin",
|
|
19
|
+
".swift": "rules-swift",
|
|
20
|
+
".dart": "rules-dart",
|
|
21
|
+
".cs": "rules-csharp",
|
|
22
|
+
".php": "rules-php",
|
|
23
|
+
".cpp": "rules-cpp",
|
|
24
|
+
".cc": "rules-cpp",
|
|
25
|
+
".cxx": "rules-cpp",
|
|
26
|
+
".hpp": "rules-cpp",
|
|
27
|
+
".rb": "rules-ruby",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Directories that should never be scanned (dependency dirs, build output, etc.)
|
|
31
|
+
_SKIP_DIRS = frozenset({
|
|
32
|
+
"node_modules", ".git", "__pycache__", "venv", ".venv", "env",
|
|
33
|
+
"dist", "build", ".tox", ".mypy_cache", ".pytest_cache",
|
|
34
|
+
"vendor", "target", ".next", ".nuxt", "coverage",
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
|
|
8
38
|
def _load_manifest_modules(toolkit_dir: Path) -> dict:
|
|
9
39
|
"""Load the modules section from manifest.json."""
|
|
10
40
|
manifest_path = toolkit_dir / "manifest.json"
|
|
@@ -24,19 +54,49 @@ def _glob_matches(project_dir: Path, pattern: str) -> bool:
|
|
|
24
54
|
return any(project_dir.glob(pattern))
|
|
25
55
|
|
|
26
56
|
|
|
57
|
+
def _detect_by_extensions(project_dir: Path) -> set[str]:
|
|
58
|
+
"""Scan source files (top-level + 1 level deep) and return detected modules.
|
|
59
|
+
|
|
60
|
+
Skips dependency/build directories for speed. Stops scanning for a
|
|
61
|
+
given extension once one match is found.
|
|
62
|
+
"""
|
|
63
|
+
found: set[str] = set()
|
|
64
|
+
seen_exts: set[str] = set()
|
|
65
|
+
|
|
66
|
+
for child in project_dir.iterdir():
|
|
67
|
+
if child.is_file():
|
|
68
|
+
ext = child.suffix
|
|
69
|
+
if ext in _EXT_TO_MODULE and ext not in seen_exts:
|
|
70
|
+
seen_exts.add(ext)
|
|
71
|
+
found.add(_EXT_TO_MODULE[ext])
|
|
72
|
+
elif child.is_dir() and child.name not in _SKIP_DIRS:
|
|
73
|
+
for grandchild in child.iterdir():
|
|
74
|
+
if grandchild.is_file():
|
|
75
|
+
ext = grandchild.suffix
|
|
76
|
+
if ext in _EXT_TO_MODULE and ext not in seen_exts:
|
|
77
|
+
seen_exts.add(ext)
|
|
78
|
+
found.add(_EXT_TO_MODULE[ext])
|
|
79
|
+
|
|
80
|
+
return found
|
|
81
|
+
|
|
82
|
+
|
|
27
83
|
def detect_languages(project_dir: Path, toolkit_dir: Path) -> list[str]:
|
|
28
84
|
"""Return list of detected language module names.
|
|
29
85
|
|
|
30
|
-
|
|
31
|
-
``auto_detect``
|
|
86
|
+
Two-phase detection:
|
|
87
|
+
1. Marker files (``auto_detect`` in manifest.json) — config-level signals.
|
|
88
|
+
2. Source file extensions (top-level + 1 deep) — actual code presence.
|
|
89
|
+
|
|
90
|
+
Both phases contribute; duplicates are merged.
|
|
32
91
|
|
|
33
92
|
Returns:
|
|
34
93
|
Sorted list of matching module names, e.g.
|
|
35
94
|
``["rules-python", "rules-typescript"]``.
|
|
36
95
|
"""
|
|
37
96
|
modules = _load_manifest_modules(toolkit_dir)
|
|
38
|
-
detected:
|
|
97
|
+
detected: set[str] = set()
|
|
39
98
|
|
|
99
|
+
# Phase 1: marker files from manifest
|
|
40
100
|
for module_name, module_cfg in modules.items():
|
|
41
101
|
markers = module_cfg.get("auto_detect")
|
|
42
102
|
if not markers:
|
|
@@ -44,7 +104,10 @@ def detect_languages(project_dir: Path, toolkit_dir: Path) -> list[str]:
|
|
|
44
104
|
|
|
45
105
|
for pattern in markers:
|
|
46
106
|
if _glob_matches(project_dir, pattern):
|
|
47
|
-
detected.
|
|
107
|
+
detected.add(module_name)
|
|
48
108
|
break # one match is enough for this module
|
|
49
109
|
|
|
110
|
+
# Phase 2: source file extensions
|
|
111
|
+
detected |= _detect_by_extensions(project_dir)
|
|
112
|
+
|
|
50
113
|
return sorted(detected)
|