agent-notes 2.0.4__py3-none-any.whl
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.
- agent_notes/VERSION +1 -0
- agent_notes/__init__.py +1 -0
- agent_notes/__main__.py +4 -0
- agent_notes/cli.py +348 -0
- agent_notes/commands/__init__.py +27 -0
- agent_notes/commands/_install_helpers.py +262 -0
- agent_notes/commands/build.py +170 -0
- agent_notes/commands/doctor.py +112 -0
- agent_notes/commands/info.py +95 -0
- agent_notes/commands/install.py +99 -0
- agent_notes/commands/list.py +169 -0
- agent_notes/commands/memory.py +430 -0
- agent_notes/commands/regenerate.py +152 -0
- agent_notes/commands/set_role.py +143 -0
- agent_notes/commands/uninstall.py +26 -0
- agent_notes/commands/update.py +169 -0
- agent_notes/commands/validate.py +199 -0
- agent_notes/commands/wizard.py +720 -0
- agent_notes/config.py +154 -0
- agent_notes/data/agents/agents.yaml +352 -0
- agent_notes/data/agents/analyst.md +45 -0
- agent_notes/data/agents/api-reviewer.md +47 -0
- agent_notes/data/agents/architect.md +46 -0
- agent_notes/data/agents/coder.md +28 -0
- agent_notes/data/agents/database-specialist.md +45 -0
- agent_notes/data/agents/debugger.md +47 -0
- agent_notes/data/agents/devil.md +47 -0
- agent_notes/data/agents/devops.md +38 -0
- agent_notes/data/agents/explorer.md +23 -0
- agent_notes/data/agents/integrations.md +44 -0
- agent_notes/data/agents/lead.md +216 -0
- agent_notes/data/agents/performance-profiler.md +44 -0
- agent_notes/data/agents/refactorer.md +48 -0
- agent_notes/data/agents/reviewer.md +44 -0
- agent_notes/data/agents/security-auditor.md +44 -0
- agent_notes/data/agents/system-auditor.md +38 -0
- agent_notes/data/agents/tech-writer.md +32 -0
- agent_notes/data/agents/test-runner.md +36 -0
- agent_notes/data/agents/test-writer.md +39 -0
- agent_notes/data/cli/claude.yaml +25 -0
- agent_notes/data/cli/copilot.yaml +18 -0
- agent_notes/data/cli/opencode.yaml +22 -0
- agent_notes/data/commands/brainstorm.md +8 -0
- agent_notes/data/commands/debug.md +9 -0
- agent_notes/data/commands/review.md +10 -0
- agent_notes/data/global-claude.md +290 -0
- agent_notes/data/global-copilot.md +27 -0
- agent_notes/data/global-opencode.md +40 -0
- agent_notes/data/hooks/session-context.md.tpl +19 -0
- agent_notes/data/models/claude-haiku-4-5.yaml +15 -0
- agent_notes/data/models/claude-opus-4-1.yaml +16 -0
- agent_notes/data/models/claude-opus-4-5.yaml +16 -0
- agent_notes/data/models/claude-opus-4-6.yaml +16 -0
- agent_notes/data/models/claude-opus-4-7.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4-5.yaml +16 -0
- agent_notes/data/models/claude-sonnet-4-6.yaml +15 -0
- agent_notes/data/models/claude-sonnet-4.yaml +16 -0
- agent_notes/data/pricing.yaml +33 -0
- agent_notes/data/roles/orchestrator.yaml +5 -0
- agent_notes/data/roles/reasoner.yaml +5 -0
- agent_notes/data/roles/scout.yaml +5 -0
- agent_notes/data/roles/worker.yaml +5 -0
- agent_notes/data/rules/code-quality.md +9 -0
- agent_notes/data/rules/safety.md +10 -0
- agent_notes/data/scripts/cost-report +211 -0
- agent_notes/data/skills/brainstorming/SKILL.md +57 -0
- agent_notes/data/skills/code-review/SKILL.md +64 -0
- agent_notes/data/skills/debugging-protocol/SKILL.md +51 -0
- agent_notes/data/skills/docker-compose/SKILL.md +318 -0
- agent_notes/data/skills/docker-compose-advanced/SKILL.md +575 -0
- agent_notes/data/skills/docker-dockerfile/SKILL.md +385 -0
- agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +293 -0
- agent_notes/data/skills/git/SKILL.md +87 -0
- agent_notes/data/skills/rails-active-storage/SKILL.md +321 -0
- agent_notes/data/skills/rails-broadcasting/SKILL.md +374 -0
- agent_notes/data/skills/rails-concerns/SKILL.md +806 -0
- agent_notes/data/skills/rails-controllers/SKILL.md +510 -0
- agent_notes/data/skills/rails-controllers-advanced/SKILL.md +441 -0
- agent_notes/data/skills/rails-helpers/SKILL.md +677 -0
- agent_notes/data/skills/rails-initializers/SKILL.md +79 -0
- agent_notes/data/skills/rails-javascript/SKILL.md +567 -0
- agent_notes/data/skills/rails-jobs/SKILL.md +700 -0
- agent_notes/data/skills/rails-kamal/SKILL.md +483 -0
- agent_notes/data/skills/rails-lib/SKILL.md +101 -0
- agent_notes/data/skills/rails-mailers/SKILL.md +321 -0
- agent_notes/data/skills/rails-migrations/SKILL.md +268 -0
- agent_notes/data/skills/rails-models/SKILL.md +459 -0
- agent_notes/data/skills/rails-models-advanced/SKILL.md +398 -0
- agent_notes/data/skills/rails-routes/SKILL.md +804 -0
- agent_notes/data/skills/rails-style/SKILL.md +538 -0
- agent_notes/data/skills/rails-testing-controllers/SKILL.md +343 -0
- agent_notes/data/skills/rails-testing-models/SKILL.md +296 -0
- agent_notes/data/skills/rails-testing-system/SKILL.md +375 -0
- agent_notes/data/skills/rails-validations/SKILL.md +108 -0
- agent_notes/data/skills/rails-view-components/SKILL.md +511 -0
- agent_notes/data/skills/rails-view-components-advanced/SKILL.md +376 -0
- agent_notes/data/skills/rails-views/SKILL.md +413 -0
- agent_notes/data/skills/rails-views-advanced/SKILL.md +450 -0
- agent_notes/data/skills/refactoring-protocol/SKILL.md +64 -0
- agent_notes/data/skills/tdd/SKILL.md +57 -0
- agent_notes/data/templates/__init__.py +1 -0
- agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__init__.py +1 -0
- agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/cursor.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
- agent_notes/data/templates/frontmatter/claude.py +44 -0
- agent_notes/data/templates/frontmatter/opencode.py +104 -0
- agent_notes/doctor_checks.py +189 -0
- agent_notes/domain/__init__.py +17 -0
- agent_notes/domain/agent.py +34 -0
- agent_notes/domain/cli_backend.py +40 -0
- agent_notes/domain/diagnostics.py +29 -0
- agent_notes/domain/diff.py +44 -0
- agent_notes/domain/model.py +27 -0
- agent_notes/domain/role.py +13 -0
- agent_notes/domain/rule.py +13 -0
- agent_notes/domain/skill.py +15 -0
- agent_notes/domain/state.py +46 -0
- agent_notes/install_state.py +11 -0
- agent_notes/registries/__init__.py +16 -0
- agent_notes/registries/_base.py +46 -0
- agent_notes/registries/agent_registry.py +107 -0
- agent_notes/registries/cli_registry.py +89 -0
- agent_notes/registries/model_registry.py +85 -0
- agent_notes/registries/role_registry.py +64 -0
- agent_notes/registries/rule_registry.py +80 -0
- agent_notes/registries/skill_registry.py +141 -0
- agent_notes/services/__init__.py +8 -0
- agent_notes/services/diagnostics/__init__.py +47 -0
- agent_notes/services/diagnostics/_checks.py +272 -0
- agent_notes/services/diagnostics/_display.py +346 -0
- agent_notes/services/diagnostics/_fix.py +169 -0
- agent_notes/services/diff.py +349 -0
- agent_notes/services/fs.py +195 -0
- agent_notes/services/install_state_builder.py +210 -0
- agent_notes/services/installer.py +293 -0
- agent_notes/services/memory_backend.py +155 -0
- agent_notes/services/rendering.py +329 -0
- agent_notes/services/session_context.py +23 -0
- agent_notes/services/settings_writer.py +79 -0
- agent_notes/services/state_store.py +249 -0
- agent_notes/services/ui.py +419 -0
- agent_notes/services/user_config.py +62 -0
- agent_notes/services/validation.py +67 -0
- agent_notes/state.py +21 -0
- agent_notes-2.0.4.dist-info/METADATA +14 -0
- agent_notes-2.0.4.dist-info/RECORD +162 -0
- agent_notes-2.0.4.dist-info/WHEEL +5 -0
- agent_notes-2.0.4.dist-info/entry_points.txt +2 -0
- agent_notes-2.0.4.dist-info/licenses/LICENSE +21 -0
- agent_notes-2.0.4.dist-info/top_level.txt +2 -0
- tests/conftest.py +20 -0
- tests/functional/__init__.py +0 -0
- tests/functional/test_build_commands.py +88 -0
- tests/functional/test_registries.py +128 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_build_output.py +129 -0
- tests/plugins/__init__.py +0 -0
- tests/plugins/test_agents.py +93 -0
- tests/plugins/test_skills.py +77 -0
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
"""Interactive install wizard for agent-notes."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict, Set, Optional
|
|
6
|
+
|
|
7
|
+
from .build import build
|
|
8
|
+
from ._install_helpers import (
|
|
9
|
+
count_agents, count_global, count_skills
|
|
10
|
+
)
|
|
11
|
+
from ..services.fs import place_file, place_dir_contents
|
|
12
|
+
from ..services.ui import (
|
|
13
|
+
_can_interactive, _safe_input, _checkbox_select, _radio_select,
|
|
14
|
+
_checkbox_select_fallback, _radio_select_fallback
|
|
15
|
+
)
|
|
16
|
+
from ..config import (
|
|
17
|
+
Color, AGENTS_HOME, PKG_DIR, get_version,
|
|
18
|
+
DIST_SKILLS_DIR, DIST_RULES_DIR, DIST_CLAUDE_DIR, DIST_OPENCODE_DIR,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_ROLE_ANSI = {
|
|
22
|
+
'purple': "\033[0;35m",
|
|
23
|
+
'red': "\033[0;31m",
|
|
24
|
+
'cyan': "\033[0;36m",
|
|
25
|
+
'blue': "\033[0;34m",
|
|
26
|
+
'green': "\033[0;32m",
|
|
27
|
+
'yellow': "\033[0;33m",
|
|
28
|
+
'orange': "\033[0;33m",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_skill_groups() -> Dict[str, List[str]]:
|
|
33
|
+
"""Get skill names grouped by technology."""
|
|
34
|
+
# For testing, allow bypassing the registry
|
|
35
|
+
import os
|
|
36
|
+
if os.environ.get('_WIZARD_TEST_MODE'):
|
|
37
|
+
if not DIST_SKILLS_DIR.exists():
|
|
38
|
+
return {}
|
|
39
|
+
all_skills = [d.name for d in DIST_SKILLS_DIR.iterdir() if d.is_dir()]
|
|
40
|
+
else:
|
|
41
|
+
try:
|
|
42
|
+
from ..registries import default_skill_registry
|
|
43
|
+
registry = default_skill_registry()
|
|
44
|
+
|
|
45
|
+
# If the registry has per-skill grouping (skill.group field), use it.
|
|
46
|
+
# If every skill falls into "uncategorized" (the default), fall through
|
|
47
|
+
# to the hardcoded prefix-based grouping below so the wizard still
|
|
48
|
+
# presents meaningful groups.
|
|
49
|
+
if hasattr(registry, 'by_group'):
|
|
50
|
+
groups = registry.by_group()
|
|
51
|
+
real_groups = {
|
|
52
|
+
gn: [s.name for s in skills]
|
|
53
|
+
for gn, skills in groups.items()
|
|
54
|
+
if gn != "uncategorized" and skills
|
|
55
|
+
}
|
|
56
|
+
if real_groups:
|
|
57
|
+
return real_groups
|
|
58
|
+
# else fall through to hardcoded grouping with registry's skill list
|
|
59
|
+
all_skills = [s.name for s in registry.all()]
|
|
60
|
+
else:
|
|
61
|
+
# Fallback to old hardcoded grouping
|
|
62
|
+
all_skills = [skill.name for skill in registry.all()]
|
|
63
|
+
except Exception:
|
|
64
|
+
# Fallback to old behavior if registry fails
|
|
65
|
+
if not DIST_SKILLS_DIR.exists():
|
|
66
|
+
return {}
|
|
67
|
+
all_skills = [d.name for d in DIST_SKILLS_DIR.iterdir() if d.is_dir()]
|
|
68
|
+
|
|
69
|
+
# Hardcoded grouping for backward compatibility
|
|
70
|
+
groups = {
|
|
71
|
+
"Rails": [s for s in all_skills if s.startswith("rails-") and s != "rails-kamal"],
|
|
72
|
+
"Docker": [s for s in all_skills if s.startswith("docker-")],
|
|
73
|
+
"Kamal": [s for s in all_skills if s == "rails-kamal"],
|
|
74
|
+
"Git": [s for s in all_skills if s == "git"]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {k: v for k, v in groups.items() if v}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _count_rules() -> int:
|
|
81
|
+
"""Count rule files."""
|
|
82
|
+
# For testing, allow bypassing the registry
|
|
83
|
+
import os
|
|
84
|
+
if os.environ.get('_WIZARD_TEST_MODE'):
|
|
85
|
+
if not DIST_RULES_DIR.exists():
|
|
86
|
+
return 0
|
|
87
|
+
return len(list(DIST_RULES_DIR.glob("*.md")))
|
|
88
|
+
else:
|
|
89
|
+
try:
|
|
90
|
+
from ..registries import default_rule_registry
|
|
91
|
+
registry = default_rule_registry()
|
|
92
|
+
return len(registry.all())
|
|
93
|
+
except Exception:
|
|
94
|
+
# Fallback to old behavior if registry fails
|
|
95
|
+
if not DIST_RULES_DIR.exists():
|
|
96
|
+
return 0
|
|
97
|
+
return len(list(DIST_RULES_DIR.glob("*.md")))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _select_cli(step: int = 0, total: int = 0, version: str = '') -> Set[str]:
|
|
101
|
+
"""Step 1: CLI selection."""
|
|
102
|
+
from ..registries.cli_registry import load_registry
|
|
103
|
+
registry = load_registry()
|
|
104
|
+
# Show only CLIs that have a global_template (i.e. are meant to be user-selectable)
|
|
105
|
+
# AND support at least one meaningful component (not just copilot which is config-only).
|
|
106
|
+
options = []
|
|
107
|
+
for backend in sorted(registry.all(), key=lambda b: b.name):
|
|
108
|
+
options.append((backend.label, backend.name))
|
|
109
|
+
|
|
110
|
+
# Default to Claude Code only
|
|
111
|
+
safe_defaults = {"claude"}
|
|
112
|
+
|
|
113
|
+
if _can_interactive():
|
|
114
|
+
result = _checkbox_select("Which CLI do you use?", options, defaults=safe_defaults,
|
|
115
|
+
step=step, total=total, version=version)
|
|
116
|
+
else:
|
|
117
|
+
result = _checkbox_select_fallback("Which CLI do you use?", options, defaults=safe_defaults,
|
|
118
|
+
step=step, total=total, version=version)
|
|
119
|
+
|
|
120
|
+
labels = [label for label, val in options if val in result]
|
|
121
|
+
print(f" {Color.GREEN}✓{Color.NC} CLI: {', '.join(labels) if labels else 'None'}")
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _select_models_per_role(clis: Set[str], step: int = 0, total: int = 0, version: str = '') -> Dict[str, Dict[str, str]]:
|
|
126
|
+
"""For each CLI that supports agents, ask user to pick a model per role.
|
|
127
|
+
|
|
128
|
+
Returns: {cli_name: {role_name: model_id}}. Config-only CLIs are skipped (no entry).
|
|
129
|
+
"""
|
|
130
|
+
from ..registries.cli_registry import load_registry
|
|
131
|
+
from ..registries.model_registry import load_model_registry
|
|
132
|
+
from ..registries.role_registry import load_role_registry
|
|
133
|
+
|
|
134
|
+
registry = load_registry()
|
|
135
|
+
models = load_model_registry().all()
|
|
136
|
+
roles = load_role_registry().all()
|
|
137
|
+
|
|
138
|
+
# Sort roles by name for deterministic UI (registry already returns sorted)
|
|
139
|
+
roles_sorted = sorted(roles, key=lambda r: r.name)
|
|
140
|
+
|
|
141
|
+
result = {}
|
|
142
|
+
for backend_name in sorted(clis):
|
|
143
|
+
backend = registry.get(backend_name)
|
|
144
|
+
if backend is None or not backend.supports("agents"):
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
# Compatible models = those with at least one alias in backend.accepted_providers
|
|
148
|
+
compatible = [m for m in models if backend.first_alias_for(m.aliases) is not None]
|
|
149
|
+
if not compatible:
|
|
150
|
+
# Could be: empty accepted_providers, or no models declare an alias for
|
|
151
|
+
# any accepted provider. Either way, we can't drive this CLI — skip it
|
|
152
|
+
# with a clear warning rather than hard-crashing the wizard.
|
|
153
|
+
print(
|
|
154
|
+
f" {Color.YELLOW}Warning:{Color.NC} no compatible models found for "
|
|
155
|
+
f"{backend.label} (accepted providers: "
|
|
156
|
+
f"{list(backend.accepted_providers) or 'none'}). Skipping model selection; "
|
|
157
|
+
f"this CLI will rely on legacy tier resolution."
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
cli_role_models = {}
|
|
162
|
+
for role in roles_sorted:
|
|
163
|
+
# Default: newest model (by registry order; iterate reversed so e.g.
|
|
164
|
+
# claude-opus-4-7 wins over claude-opus-4-6) whose class == role.typical_class.
|
|
165
|
+
# Fallback to first compatible if nothing matches.
|
|
166
|
+
default_model = next(
|
|
167
|
+
(m for m in reversed(compatible) if m.model_class == role.typical_class),
|
|
168
|
+
compatible[0],
|
|
169
|
+
)
|
|
170
|
+
default_idx = compatible.index(default_model)
|
|
171
|
+
|
|
172
|
+
# Build options: "Claude Opus 4.7 (via anthropic)" style
|
|
173
|
+
options = []
|
|
174
|
+
for m in compatible:
|
|
175
|
+
prov_alias = backend.first_alias_for(m.aliases)
|
|
176
|
+
provider = prov_alias[0] if prov_alias else "?"
|
|
177
|
+
options.append((f"{m.label} (via {provider})", m.id))
|
|
178
|
+
|
|
179
|
+
role_color = (_ROLE_ANSI.get(role.color, '') if sys.stdout.isatty() else '') if role.color else ''
|
|
180
|
+
role_label_colored = f"{role_color}{role.label}{Color.NC}" if role_color else role.label
|
|
181
|
+
title = (
|
|
182
|
+
f"{Color.DIM}CLI{Color.NC} {Color.YELLOW}{backend.label}{Color.NC}\n"
|
|
183
|
+
f" {Color.DIM}Role{Color.NC} {role_label_colored}\n"
|
|
184
|
+
f" {Color.DIM}Description{Color.NC} {role.description}"
|
|
185
|
+
)
|
|
186
|
+
if _can_interactive():
|
|
187
|
+
picked = _radio_select(title, options, default=default_idx,
|
|
188
|
+
step=step, total=total, version=version)
|
|
189
|
+
else:
|
|
190
|
+
picked = _radio_select_fallback(title, options, default=default_idx,
|
|
191
|
+
step=step, total=total, version=version)
|
|
192
|
+
cli_role_models[role.name] = picked
|
|
193
|
+
|
|
194
|
+
picked_label = next(label for label, mid in options if mid == picked)
|
|
195
|
+
print(f" {Color.GREEN}✓{Color.NC} {role_label_colored}: {picked_label}")
|
|
196
|
+
|
|
197
|
+
result[backend_name] = cli_role_models
|
|
198
|
+
return result
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _select_scope(clis: Set[str] = None, step: int = 0, total: int = 0, version: str = '') -> str:
|
|
202
|
+
"""Step 3: Install scope."""
|
|
203
|
+
from ..registries.cli_registry import load_registry
|
|
204
|
+
|
|
205
|
+
registry = load_registry()
|
|
206
|
+
selected_backends = [b for b in registry.all() if (not clis or b.name in clis)]
|
|
207
|
+
|
|
208
|
+
def _path_lines(backends, path_fn) -> str:
|
|
209
|
+
parts = [f"\n {Color.DIM}{b.label} → {path_fn(b)}{Color.NC}" for b in backends]
|
|
210
|
+
return "".join(parts)
|
|
211
|
+
|
|
212
|
+
global_label = "Global" + _path_lines(selected_backends, lambda b: str(b.global_home))
|
|
213
|
+
local_label = "Local" + _path_lines(selected_backends, lambda b: str(Path.cwd() / b.local_dir))
|
|
214
|
+
|
|
215
|
+
options = [
|
|
216
|
+
(global_label, "global"),
|
|
217
|
+
(local_label, "local"),
|
|
218
|
+
]
|
|
219
|
+
if _can_interactive():
|
|
220
|
+
result = _radio_select("Where to install?", options, default=0,
|
|
221
|
+
step=step, total=total, version=version)
|
|
222
|
+
else:
|
|
223
|
+
result = _radio_select_fallback("Where to install?", options, default=0,
|
|
224
|
+
step=step, total=total, version=version)
|
|
225
|
+
|
|
226
|
+
label = "Global" if result == "global" else "Local"
|
|
227
|
+
print(f" {Color.GREEN}✓{Color.NC} Scope: {label}")
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _select_mode(step: int = 0, total: int = 0, version: str = '') -> bool:
|
|
232
|
+
"""Step 4: Install mode."""
|
|
233
|
+
options = [
|
|
234
|
+
("Symlink (auto-updates when source changes)", "symlink"),
|
|
235
|
+
("Copy (standalone, allows local customization)", "copy"),
|
|
236
|
+
]
|
|
237
|
+
if _can_interactive():
|
|
238
|
+
result = _radio_select("How to install?", options, default=0,
|
|
239
|
+
step=step, total=total, version=version)
|
|
240
|
+
else:
|
|
241
|
+
result = _radio_select_fallback("How to install?", options, default=0,
|
|
242
|
+
step=step, total=total, version=version)
|
|
243
|
+
|
|
244
|
+
label = "Symlink" if result == "symlink" else "Copy"
|
|
245
|
+
print(f" {Color.GREEN}✓{Color.NC} Mode: {label}")
|
|
246
|
+
return result == "copy"
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _select_skills(step: int = 0, total: int = 0, version: str = '') -> List[str]:
|
|
250
|
+
"""Step 5: Skill selection."""
|
|
251
|
+
skill_groups = _get_skill_groups()
|
|
252
|
+
|
|
253
|
+
if not skill_groups:
|
|
254
|
+
return []
|
|
255
|
+
|
|
256
|
+
# Process skills are always included — separate them from tech skill groups.
|
|
257
|
+
process_skills = skill_groups.get("process", [])
|
|
258
|
+
tech_groups = {k: v for k, v in skill_groups.items() if k != "process"}
|
|
259
|
+
|
|
260
|
+
descriptions = {
|
|
261
|
+
"rails": "models, controllers, views, routes, testing",
|
|
262
|
+
"docker": "Dockerfile, Compose patterns",
|
|
263
|
+
"kamal": "deployment with Kamal",
|
|
264
|
+
"git": "commit workflow, conventional commits",
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
selected_skills = list(process_skills)
|
|
268
|
+
|
|
269
|
+
if tech_groups:
|
|
270
|
+
options = []
|
|
271
|
+
for group_name, skills in tech_groups.items():
|
|
272
|
+
desc = descriptions.get(group_name, group_name.lower())
|
|
273
|
+
count = len(skills)
|
|
274
|
+
label = f"{group_name.capitalize()} — {desc} ({count} {'skill' if count == 1 else 'skills'})"
|
|
275
|
+
options.append((label, group_name))
|
|
276
|
+
|
|
277
|
+
all_group_names = set(tech_groups.keys())
|
|
278
|
+
|
|
279
|
+
title = "Which domain skills to include?\n (process skills are always included)"
|
|
280
|
+
if _can_interactive():
|
|
281
|
+
selected_groups = _checkbox_select(title, options, defaults=all_group_names,
|
|
282
|
+
step=step, total=total, version=version)
|
|
283
|
+
else:
|
|
284
|
+
selected_groups = _checkbox_select_fallback(title, options, defaults=all_group_names,
|
|
285
|
+
step=step, total=total, version=version)
|
|
286
|
+
|
|
287
|
+
skill_summary_parts = [f"process ({len(process_skills)})"] if process_skills else []
|
|
288
|
+
for group_name, skills in tech_groups.items():
|
|
289
|
+
if group_name in selected_groups:
|
|
290
|
+
selected_skills.extend(skills)
|
|
291
|
+
skill_summary_parts.append(f"{group_name.capitalize()} ({len(skills)})")
|
|
292
|
+
else:
|
|
293
|
+
skill_summary_parts = [f"process ({len(process_skills)})"] if process_skills else []
|
|
294
|
+
|
|
295
|
+
summary = ", ".join(skill_summary_parts) if skill_summary_parts else "None"
|
|
296
|
+
print(f" {Color.GREEN}✓{Color.NC} Skills: {summary}")
|
|
297
|
+
return selected_skills
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _render_install_summary(clis: Set[str], scope: str, copy_mode: bool, selected_skills: List[str], role_models: Dict[str, Dict[str, str]], skill_groups: Dict, registry, memory_backend: str = '', memory_path: str = '') -> None:
|
|
301
|
+
"""Print the confirmation summary in per-CLI format with role colors."""
|
|
302
|
+
from ..services.installer import config_filename_for as _cfg_filename
|
|
303
|
+
from ..registries.model_registry import load_model_registry
|
|
304
|
+
from ..registries.role_registry import load_role_registry
|
|
305
|
+
|
|
306
|
+
# Evaluate color codes at render time so they're never stale from import-time disable
|
|
307
|
+
_tty = sys.stdout.isatty()
|
|
308
|
+
_DIM = "\033[2m" if _tty else ""
|
|
309
|
+
_NC = "\033[0m" if _tty else ""
|
|
310
|
+
_CYAN = "\033[0;36m" if _tty else ""
|
|
311
|
+
|
|
312
|
+
selected_backends = [b for b in registry.all() if b.name in clis]
|
|
313
|
+
models_registry = load_model_registry()
|
|
314
|
+
role_registry = load_role_registry()
|
|
315
|
+
role_map = {r.name: r for r in role_registry.all()}
|
|
316
|
+
|
|
317
|
+
# ── Shared section ────────────────────────────────────────────────────────
|
|
318
|
+
print("")
|
|
319
|
+
scope_label = "Global" if scope == "global" else "Local"
|
|
320
|
+
print(f" {_DIM}Scope{_NC} {scope_label}")
|
|
321
|
+
print(f" {_DIM}Mode{_NC} {'Copy' if copy_mode else 'Symlink'}")
|
|
322
|
+
|
|
323
|
+
if selected_skills:
|
|
324
|
+
all_grouped = {s for gs in skill_groups.values() for s in gs}
|
|
325
|
+
parts = []
|
|
326
|
+
for gname, gskills in skill_groups.items():
|
|
327
|
+
cnt = sum(1 for s in selected_skills if s in gskills)
|
|
328
|
+
if cnt:
|
|
329
|
+
parts.append(f"{gname.capitalize()} ({cnt})")
|
|
330
|
+
ungrouped = sum(1 for s in selected_skills if s not in all_grouped)
|
|
331
|
+
if ungrouped:
|
|
332
|
+
parts.append(f"Other ({ungrouped})")
|
|
333
|
+
print(f" {_DIM}Skills{_NC} {', '.join(parts) if parts else 'none'}")
|
|
334
|
+
|
|
335
|
+
if memory_backend and memory_backend != "none":
|
|
336
|
+
mem_label = (f"Obsidian → {memory_path}" if memory_path else "Obsidian") if memory_backend == "obsidian" else "Local markdown"
|
|
337
|
+
print(f" {_DIM}Memory{_NC} {mem_label}")
|
|
338
|
+
|
|
339
|
+
# ── Per-CLI sections ──────────────────────────────────────────────────────
|
|
340
|
+
rules_count = _count_rules()
|
|
341
|
+
|
|
342
|
+
for backend in selected_backends:
|
|
343
|
+
print(f"\n {_CYAN}{backend.label}{_NC}")
|
|
344
|
+
|
|
345
|
+
# Agent roles
|
|
346
|
+
if backend.name in role_models and role_models[backend.name]:
|
|
347
|
+
print(f" {_DIM}Agent roles:{_NC}")
|
|
348
|
+
for role_name, model_id in sorted(role_models[backend.name].items()):
|
|
349
|
+
role = role_map.get(role_name)
|
|
350
|
+
role_label = role.label if role else role_name
|
|
351
|
+
role_ansi = (_ROLE_ANSI.get(role.color, "") if role and role.color else "") if _tty else ""
|
|
352
|
+
colored_role = f"{role_ansi}{role_label}{_NC}" if role_ansi else role_label
|
|
353
|
+
# Pad using visible label length (not raw string length which includes ANSI codes)
|
|
354
|
+
padding = " " * max(0, 28 - len(role_label))
|
|
355
|
+
try:
|
|
356
|
+
model = models_registry.get(model_id)
|
|
357
|
+
prov_alias = backend.first_alias_for(model.aliases)
|
|
358
|
+
alias = prov_alias[1] if prov_alias else model_id
|
|
359
|
+
except KeyError:
|
|
360
|
+
alias = model_id
|
|
361
|
+
print(f" {colored_role}{padding} {_DIM}{alias}{_NC}")
|
|
362
|
+
|
|
363
|
+
# Agents count
|
|
364
|
+
if backend.supports("agents"):
|
|
365
|
+
n_agents = count_agents(backend)
|
|
366
|
+
print(f" {_DIM}Agents:{_NC} {n_agents}")
|
|
367
|
+
|
|
368
|
+
# Config + Rules
|
|
369
|
+
cfg = _cfg_filename(backend)
|
|
370
|
+
if cfg:
|
|
371
|
+
cfg_desc = cfg
|
|
372
|
+
if rules_count:
|
|
373
|
+
cfg_desc += f" + {rules_count} rules"
|
|
374
|
+
print(f" {_DIM}Config:{_NC} {cfg_desc}")
|
|
375
|
+
|
|
376
|
+
print("")
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _detect_project_name() -> str:
|
|
380
|
+
"""Return git repo name, or cwd name as fallback."""
|
|
381
|
+
import subprocess
|
|
382
|
+
try:
|
|
383
|
+
result = subprocess.run(
|
|
384
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
385
|
+
capture_output=True, text=True, timeout=3,
|
|
386
|
+
)
|
|
387
|
+
if result.returncode == 0:
|
|
388
|
+
return Path(result.stdout.strip()).name
|
|
389
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
390
|
+
pass
|
|
391
|
+
return Path.cwd().name
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _detect_obsidian_vaults() -> List[Path]:
|
|
395
|
+
"""Scan common locations for Obsidian vaults (dirs containing .obsidian/)."""
|
|
396
|
+
candidates = []
|
|
397
|
+
search_roots = [Path.home() / "Documents", Path.home() / "Desktop", Path.home()]
|
|
398
|
+
for root in search_roots:
|
|
399
|
+
if not root.exists():
|
|
400
|
+
continue
|
|
401
|
+
try:
|
|
402
|
+
for d in root.iterdir():
|
|
403
|
+
try:
|
|
404
|
+
if d.is_dir() and (d / ".obsidian").exists():
|
|
405
|
+
candidates.append(d)
|
|
406
|
+
except (PermissionError, OSError):
|
|
407
|
+
continue
|
|
408
|
+
except (PermissionError, OSError):
|
|
409
|
+
continue
|
|
410
|
+
return candidates[:5]
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _select_memory(step: int, total: int, version: str = '') -> tuple:
|
|
414
|
+
"""Step N: choose memory backend. Returns (backend, path)."""
|
|
415
|
+
from ..config import Color
|
|
416
|
+
|
|
417
|
+
options = [
|
|
418
|
+
("Local markdown files (~/.claude/agent-memory/)", "local"),
|
|
419
|
+
("Obsidian vault", "obsidian"),
|
|
420
|
+
("None (disable memory)", "none"),
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
if _can_interactive():
|
|
424
|
+
backend = _radio_select("How should agents store memory?", options, default=0,
|
|
425
|
+
step=step, total=total, version=version)
|
|
426
|
+
else:
|
|
427
|
+
backend = _radio_select_fallback("How should agents store memory?", options, default=0,
|
|
428
|
+
step=step, total=total, version=version)
|
|
429
|
+
|
|
430
|
+
path = ""
|
|
431
|
+
|
|
432
|
+
if backend == "obsidian":
|
|
433
|
+
project_name = _detect_project_name()
|
|
434
|
+
candidates = _detect_obsidian_vaults()
|
|
435
|
+
if candidates:
|
|
436
|
+
_hint_suffix = f"agent-notes/{project_name}" if project_name != "agent-notes" else "agent-notes"
|
|
437
|
+
print(f" {Color.DIM}Detected vaults (notes go into {_hint_suffix}/ inside):{Color.NC}")
|
|
438
|
+
for c in candidates[:3]:
|
|
439
|
+
print(f" {c}/{_hint_suffix}")
|
|
440
|
+
_mem_base = candidates[0] if candidates else Path.home() / "Documents" / "Obsidian Vault"
|
|
441
|
+
_mem_full = _mem_base / "agent-notes" / project_name
|
|
442
|
+
# Avoid agent-notes/agent-notes when project name matches parent folder
|
|
443
|
+
if _mem_full.parent.name == _mem_full.name:
|
|
444
|
+
_mem_full = _mem_full.parent
|
|
445
|
+
default_path = str(_mem_full)
|
|
446
|
+
raw = _safe_input(f" Memory folder path [{default_path}]: ", default_path)
|
|
447
|
+
path = raw.strip() or default_path
|
|
448
|
+
|
|
449
|
+
label = {"local": "Local markdown", "obsidian": f"Obsidian ({path})", "none": "Disabled"}[backend]
|
|
450
|
+
print(f" {Color.GREEN}✓{Color.NC} Memory: {label}")
|
|
451
|
+
return backend, path
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _confirm_install(clis: Set[str], scope: str, copy_mode: bool, selected_skills: List[str], role_models: Dict[str, Dict[str, str]], version: str = '', memory_backend: str = 'local', memory_path: str = '') -> bool:
|
|
455
|
+
"""Step 7: Confirmation."""
|
|
456
|
+
from ..services.ui import _clear_screen, _render_step_header
|
|
457
|
+
from ..registries.cli_registry import load_registry
|
|
458
|
+
_clear_screen()
|
|
459
|
+
_render_step_header(7, 7, version)
|
|
460
|
+
skill_groups = _get_skill_groups()
|
|
461
|
+
registry = load_registry()
|
|
462
|
+
|
|
463
|
+
_render_install_summary(clis, scope, copy_mode, selected_skills, role_models, skill_groups, registry,
|
|
464
|
+
memory_backend=memory_backend, memory_path=memory_path)
|
|
465
|
+
|
|
466
|
+
choice = _safe_input("Proceed? [Y/n]: ", "Y").lower()
|
|
467
|
+
return choice != "n"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def install_skills_filtered(skill_names: List[str], targets: List[Path], copy_mode: bool = False) -> None:
|
|
471
|
+
"""Install only specified skills to target directories."""
|
|
472
|
+
if not skill_names or not DIST_SKILLS_DIR.exists():
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
for target_dir in targets:
|
|
476
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
477
|
+
|
|
478
|
+
for skill_name in sorted(skill_names):
|
|
479
|
+
skill_dir = DIST_SKILLS_DIR / skill_name
|
|
480
|
+
if skill_dir.is_dir():
|
|
481
|
+
place_file(skill_dir, target_dir / skill_name, copy_mode)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def install_agents_filtered(clis: Set[str], scope: str, copy_mode: bool = False) -> None:
|
|
485
|
+
"""Install agents for selected CLIs (filtered by the wizard)."""
|
|
486
|
+
from ..services import installer
|
|
487
|
+
from ..registries.cli_registry import load_registry
|
|
488
|
+
|
|
489
|
+
registry = load_registry()
|
|
490
|
+
for backend in registry.all():
|
|
491
|
+
if backend.name not in clis:
|
|
492
|
+
continue
|
|
493
|
+
src = installer.dist_source_for(backend, "agents")
|
|
494
|
+
if src is None:
|
|
495
|
+
continue
|
|
496
|
+
dst = installer.target_dir_for(backend, "agents", scope)
|
|
497
|
+
if dst is None:
|
|
498
|
+
continue
|
|
499
|
+
|
|
500
|
+
# Only install if there are files to install
|
|
501
|
+
files = list(src.glob("*.md"))
|
|
502
|
+
if not files:
|
|
503
|
+
continue
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
place_dir_contents(src, dst, "*.md", copy_mode)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def install_config_filtered(clis: Set[str], scope: str, copy_mode: bool = False) -> None:
|
|
510
|
+
"""Install config + rules for selected CLIs."""
|
|
511
|
+
from ..services import installer
|
|
512
|
+
from ..registries.cli_registry import load_registry
|
|
513
|
+
|
|
514
|
+
registry = load_registry()
|
|
515
|
+
|
|
516
|
+
for backend in registry.all():
|
|
517
|
+
if backend.name not in clis:
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
# Install config file (CLAUDE.md / AGENTS.md / copilot-instructions.md)
|
|
521
|
+
config_src = installer.dist_source_for(backend, "config")
|
|
522
|
+
config_dst = installer.target_dir_for(backend, "config", scope)
|
|
523
|
+
if config_src is not None and config_dst is not None:
|
|
524
|
+
filename = installer.config_filename_for(backend)
|
|
525
|
+
if filename:
|
|
526
|
+
src_file = config_src / filename
|
|
527
|
+
if src_file.exists():
|
|
528
|
+
place_file(src_file, config_dst / filename, copy_mode)
|
|
529
|
+
|
|
530
|
+
# Install rules (only backends that support it — currently just claude)
|
|
531
|
+
rules_src = installer.dist_source_for(backend, "rules")
|
|
532
|
+
rules_dst = installer.target_dir_for(backend, "rules", scope)
|
|
533
|
+
if rules_src is not None and rules_dst is not None:
|
|
534
|
+
files = list(rules_src.glob("*.md"))
|
|
535
|
+
if files:
|
|
536
|
+
place_dir_contents(rules_src, rules_dst, "*.md", copy_mode)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def interactive_install() -> None:
|
|
541
|
+
"""Run the interactive install wizard."""
|
|
542
|
+
try:
|
|
543
|
+
_interactive_install()
|
|
544
|
+
except KeyboardInterrupt:
|
|
545
|
+
from ..config import Color
|
|
546
|
+
print(f"\n\n {Color.YELLOW}Cancelled.{Color.NC}")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _interactive_install() -> None:
|
|
550
|
+
"""Inner implementation — called by interactive_install() with KeyboardInterrupt guard."""
|
|
551
|
+
from ..services.ui import _clear_screen
|
|
552
|
+
version = get_version()
|
|
553
|
+
from ..registries.cli_registry import load_registry
|
|
554
|
+
registry = load_registry()
|
|
555
|
+
|
|
556
|
+
# Get total agent count across all backends that support agents
|
|
557
|
+
total_agents = 0
|
|
558
|
+
for backend in registry.all():
|
|
559
|
+
if backend.supports("agents"):
|
|
560
|
+
total_agents += count_agents(backend)
|
|
561
|
+
|
|
562
|
+
n_skills = count_skills()
|
|
563
|
+
n_rules = _count_rules()
|
|
564
|
+
|
|
565
|
+
TOTAL_STEPS = 7
|
|
566
|
+
|
|
567
|
+
_clear_screen()
|
|
568
|
+
print(f"\n {Color.BOLD}AgentNotes{Color.NC} {Color.CYAN}v{version}{Color.NC}")
|
|
569
|
+
print(f" {Color.DIM}AI agent configuration manager for Claude Code and OpenCode.{Color.NC}\n")
|
|
570
|
+
print(f" Includes {total_agents} agents, {n_skills} skills, and {n_rules} rules.\n")
|
|
571
|
+
|
|
572
|
+
# Step 1: CLI selection
|
|
573
|
+
clis = _select_cli(step=1, total=TOTAL_STEPS, version=version)
|
|
574
|
+
|
|
575
|
+
if not clis:
|
|
576
|
+
print("No CLI selected. Installation cancelled.")
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
# Step 2: Model selection per role (for CLIs that support agents)
|
|
580
|
+
role_models = _select_models_per_role(clis, step=2, total=TOTAL_STEPS, version=version)
|
|
581
|
+
|
|
582
|
+
# Step 3: Install scope
|
|
583
|
+
scope = _select_scope(clis=clis, step=3, total=TOTAL_STEPS, version=version)
|
|
584
|
+
|
|
585
|
+
# Step 4: Install mode (always shown)
|
|
586
|
+
copy_mode = _select_mode(step=4, total=TOTAL_STEPS, version=version)
|
|
587
|
+
|
|
588
|
+
# Step 5: Skill selection
|
|
589
|
+
selected_skills = _select_skills(step=5, total=TOTAL_STEPS, version=version)
|
|
590
|
+
|
|
591
|
+
# Step 6: Memory backend
|
|
592
|
+
memory_backend, memory_path = _select_memory(step=6, total=TOTAL_STEPS, version=version)
|
|
593
|
+
|
|
594
|
+
# Step 7: Confirmation
|
|
595
|
+
if not _confirm_install(clis, scope, copy_mode, selected_skills, role_models, version=version,
|
|
596
|
+
memory_backend=memory_backend, memory_path=memory_path):
|
|
597
|
+
print("Installation cancelled.")
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
# Build first
|
|
601
|
+
print("\nBuilding from source...")
|
|
602
|
+
try:
|
|
603
|
+
build()
|
|
604
|
+
except Exception as e:
|
|
605
|
+
print(f"{Color.RED}Build failed: {e}{Color.NC}")
|
|
606
|
+
return
|
|
607
|
+
|
|
608
|
+
# Execute installation
|
|
609
|
+
print(f"\nInstalling ({scope}, {'copy' if copy_mode else 'symlink'}) ...\n")
|
|
610
|
+
|
|
611
|
+
from ..services import fs as _fs
|
|
612
|
+
_fs.silent_file_ops = True
|
|
613
|
+
|
|
614
|
+
from ..registries.cli_registry import load_registry as _load_registry
|
|
615
|
+
from ..services import installer as _installer
|
|
616
|
+
_registry = _load_registry()
|
|
617
|
+
|
|
618
|
+
# Scripts (global only)
|
|
619
|
+
if scope == "global":
|
|
620
|
+
from ..services.installer import install_scripts_global
|
|
621
|
+
install_scripts_global()
|
|
622
|
+
|
|
623
|
+
# Skills
|
|
624
|
+
if selected_skills:
|
|
625
|
+
targets = []
|
|
626
|
+
for _b in _registry.all():
|
|
627
|
+
if _b.name in clis and _b.supports("skills"):
|
|
628
|
+
_t = _installer.target_dir_for(_b, "skills", scope)
|
|
629
|
+
if _t is not None:
|
|
630
|
+
targets.append(_t)
|
|
631
|
+
if scope == "global":
|
|
632
|
+
targets.append(AGENTS_HOME / "skills")
|
|
633
|
+
install_skills_filtered(selected_skills, targets, copy_mode)
|
|
634
|
+
_skill_groups = _get_skill_groups()
|
|
635
|
+
_group_parts = []
|
|
636
|
+
for _gn, _gs in _skill_groups.items():
|
|
637
|
+
_cnt = sum(1 for s in selected_skills if s in _gs)
|
|
638
|
+
if _cnt:
|
|
639
|
+
_group_parts.append(f"{_gn} ({_cnt})")
|
|
640
|
+
_all_grouped = {s for gs in _skill_groups.values() for s in gs}
|
|
641
|
+
_ungrouped = sum(1 for s in selected_skills if s not in _all_grouped)
|
|
642
|
+
if _ungrouped:
|
|
643
|
+
_group_parts.append(f"Other ({_ungrouped})")
|
|
644
|
+
print(f" {Color.GREEN}✓{Color.NC} Skills {', '.join(_group_parts) if _group_parts else str(len(selected_skills)) + ' skills'}")
|
|
645
|
+
|
|
646
|
+
# Agents
|
|
647
|
+
install_agents_filtered(clis, scope, copy_mode)
|
|
648
|
+
_agent_parts = []
|
|
649
|
+
for _b in _registry.all():
|
|
650
|
+
if _b.name in clis and _b.supports("agents"):
|
|
651
|
+
_cnt = count_agents(_b)
|
|
652
|
+
if _cnt:
|
|
653
|
+
_agent_parts.append(f"{_b.label} ({_cnt})")
|
|
654
|
+
if _agent_parts:
|
|
655
|
+
print(f" {Color.GREEN}✓{Color.NC} Agents {', '.join(_agent_parts)}")
|
|
656
|
+
|
|
657
|
+
# Config + Rules
|
|
658
|
+
install_config_filtered(clis, scope, copy_mode)
|
|
659
|
+
_rules_n = _count_rules()
|
|
660
|
+
_cfg_files = [_installer.config_filename_for(_b) for _b in _registry.all() if _b.name in clis and _installer.config_filename_for(_b)]
|
|
661
|
+
_cfg_desc = ", ".join(_cfg_files) if _cfg_files else "config"
|
|
662
|
+
_cfg_desc += f" + {_rules_n} rules" if _rules_n else ""
|
|
663
|
+
print(f" {Color.GREEN}✓{Color.NC} Config {_cfg_desc}")
|
|
664
|
+
|
|
665
|
+
# Commands
|
|
666
|
+
from ..services.installer import install_component_for_backend as _install_component
|
|
667
|
+
for _backend in _registry.all():
|
|
668
|
+
if _backend.name in clis:
|
|
669
|
+
_install_component(_backend, "commands", scope, copy_mode)
|
|
670
|
+
_cmd_names = [f.stem for f in (PKG_DIR / "dist" / "commands").glob("*.md")] if (PKG_DIR / "dist" / "commands").exists() else []
|
|
671
|
+
if _cmd_names:
|
|
672
|
+
print(f" {Color.GREEN}✓{Color.NC} Commands {', '.join(sorted(_cmd_names))}")
|
|
673
|
+
|
|
674
|
+
# SessionStart hook (Claude Code only)
|
|
675
|
+
from ..services.installer import _install_session_hook
|
|
676
|
+
try:
|
|
677
|
+
_claude = _registry.get("claude")
|
|
678
|
+
if _claude.name in clis:
|
|
679
|
+
_install_session_hook(_claude, scope)
|
|
680
|
+
except (KeyError, Exception):
|
|
681
|
+
pass
|
|
682
|
+
|
|
683
|
+
_fs.silent_file_ops = False
|
|
684
|
+
|
|
685
|
+
# Write state.json
|
|
686
|
+
from ..services.install_state_builder import build_install_state
|
|
687
|
+
from ..services.state_store import record_install_state
|
|
688
|
+
from ..domain.state import MemoryConfig
|
|
689
|
+
project_path = Path.cwd() if scope == "local" else None
|
|
690
|
+
try:
|
|
691
|
+
st = build_install_state(
|
|
692
|
+
mode="copy" if copy_mode else "symlink",
|
|
693
|
+
scope=scope,
|
|
694
|
+
repo_root=PKG_DIR.parent,
|
|
695
|
+
project_path=project_path,
|
|
696
|
+
role_models=role_models,
|
|
697
|
+
selected_clis=set(clis),
|
|
698
|
+
)
|
|
699
|
+
st.memory = MemoryConfig(backend=memory_backend, path=memory_path)
|
|
700
|
+
record_install_state(st)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
print(f"{Color.YELLOW}Warning: failed to write state.json: {e}{Color.NC}")
|
|
703
|
+
|
|
704
|
+
# Initialize memory vault / directory on disk
|
|
705
|
+
if memory_backend != "none":
|
|
706
|
+
from ..config import memory_dir_for_backend
|
|
707
|
+
from ..services.memory_backend import obsidian_init, local_init
|
|
708
|
+
_mem_path = memory_dir_for_backend(memory_backend, memory_path)
|
|
709
|
+
try:
|
|
710
|
+
if memory_backend == "obsidian":
|
|
711
|
+
obsidian_init(_mem_path)
|
|
712
|
+
memory_label = f"Obsidian → {_mem_path}"
|
|
713
|
+
else:
|
|
714
|
+
local_init(_mem_path)
|
|
715
|
+
memory_label = f"Local markdown → {_mem_path}"
|
|
716
|
+
except Exception as e:
|
|
717
|
+
memory_label = f"(init failed: {e})"
|
|
718
|
+
print(f" {Color.GREEN}✓{Color.NC} Memory {memory_label}")
|
|
719
|
+
|
|
720
|
+
print(f"\n{Color.GREEN}Done.{Color.NC} Restart Claude Code / OpenCode to pick up changes.")
|