spawn-cli 0.1.1__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.
- spawn_cli/__init__.py +3 -0
- spawn_cli/cli.py +243 -0
- spawn_cli/core/__init__.py +5 -0
- spawn_cli/core/download.py +532 -0
- spawn_cli/core/errors.py +5 -0
- spawn_cli/core/high_level.py +462 -0
- spawn_cli/core/low_level.py +914 -0
- spawn_cli/core/scripts.py +246 -0
- spawn_cli/errors.py +9 -0
- spawn_cli/ide/__init__.py +63 -0
- spawn_cli/ide/_helpers.py +168 -0
- spawn_cli/ide/_stub.py +63 -0
- spawn_cli/ide/_vacancy.py +221 -0
- spawn_cli/ide/claude_code.py +145 -0
- spawn_cli/ide/codex.py +120 -0
- spawn_cli/ide/cursor.py +122 -0
- spawn_cli/ide/gemini_cli.py +130 -0
- spawn_cli/ide/github_copilot.py +167 -0
- spawn_cli/ide/registry.py +142 -0
- spawn_cli/ide/windsurf.py +91 -0
- spawn_cli/io/__init__.py +21 -0
- spawn_cli/io/json_io.py +21 -0
- spawn_cli/io/lock.py +24 -0
- spawn_cli/io/paths.py +23 -0
- spawn_cli/io/text_io.py +16 -0
- spawn_cli/io/toml_io.py +26 -0
- spawn_cli/io/yaml_io.py +32 -0
- spawn_cli/models/__init__.py +63 -0
- spawn_cli/models/config.py +111 -0
- spawn_cli/models/mcp.py +49 -0
- spawn_cli/models/metadata.py +28 -0
- spawn_cli/models/navigation.py +30 -0
- spawn_cli/models/skill.py +29 -0
- spawn_cli/resources/__init__.py +0 -0
- spawn_cli/resources/default_core_config.yaml +5 -0
- spawn_cli/warnings_display.py +35 -0
- spawn_cli-0.1.1.dist-info/METADATA +11 -0
- spawn_cli-0.1.1.dist-info/RECORD +41 -0
- spawn_cli-0.1.1.dist-info/WHEEL +5 -0
- spawn_cli-0.1.1.dist-info/entry_points.txt +2 -0
- spawn_cli-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
import warnings
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from spawn_cli.core import download as dl
|
|
9
|
+
from spawn_cli.core import low_level as ll
|
|
10
|
+
from spawn_cli.core import scripts
|
|
11
|
+
from spawn_cli.core.errors import SpawnError, SpawnWarning
|
|
12
|
+
from spawn_cli.ide.registry import get as ide_get
|
|
13
|
+
from spawn_cli.io.json_io import load_json
|
|
14
|
+
from spawn_cli.io.paths import ensure_dir, safe_path
|
|
15
|
+
from spawn_cli.io.yaml_io import load_yaml, save_yaml
|
|
16
|
+
from spawn_cli.models.config import ExtensionConfig, ReadFlag
|
|
17
|
+
|
|
18
|
+
SPAWN_ENTRY_POINT_PROMPT = """\
|
|
19
|
+
Before working, read `spawn/navigation.yaml`.
|
|
20
|
+
Read every file listed under `read-required`.
|
|
21
|
+
Inspect `read-contextual` descriptions and read only files relevant to the current task.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _require_init(target_root: Path) -> None:
|
|
26
|
+
if not (target_root / "spawn").is_dir():
|
|
27
|
+
raise SpawnError("need init before running this command")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _extend_dir(target_root: Path, extension: str) -> Path:
|
|
31
|
+
return target_root / "spawn" / ".extend" / extension
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _agent_ignore_merge_excluding(target_root: Path, skip_ext: str) -> list[str]:
|
|
35
|
+
merged: list[str] = []
|
|
36
|
+
seen: set[str] = set()
|
|
37
|
+
for g in ll.get_core_agent_ignore(target_root):
|
|
38
|
+
if g not in seen:
|
|
39
|
+
seen.add(g)
|
|
40
|
+
merged.append(g)
|
|
41
|
+
for ext in ll.list_extensions(target_root):
|
|
42
|
+
if ext == skip_ext:
|
|
43
|
+
continue
|
|
44
|
+
for g in ll.get_ext_agent_ignore(target_root, ext):
|
|
45
|
+
if g not in seen:
|
|
46
|
+
seen.add(g)
|
|
47
|
+
merged.append(g)
|
|
48
|
+
return merged
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def refresh_gitignore(target_root: Path) -> None:
|
|
52
|
+
_require_init(target_root)
|
|
53
|
+
new_items: list[str] = []
|
|
54
|
+
seen: set[str] = set()
|
|
55
|
+
for ext in ll.list_extensions(target_root):
|
|
56
|
+
for g in ll.get_ext_git_ignore(target_root, ext):
|
|
57
|
+
if g not in seen:
|
|
58
|
+
seen.add(g)
|
|
59
|
+
new_items.append(g)
|
|
60
|
+
existing = ll.get_git_ignore_list(target_root)
|
|
61
|
+
ll.save_git_ignore_list(target_root, new_items)
|
|
62
|
+
new_set, ex_set = set(new_items), set(existing)
|
|
63
|
+
ll.push_to_global_gitignore(target_root, sorted(new_set - ex_set))
|
|
64
|
+
ll.remove_from_global_gitignore(target_root, sorted(ex_set - new_set))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def refresh_agent_ignore(target_root: Path, ide: str) -> None:
|
|
68
|
+
_require_init(target_root)
|
|
69
|
+
old = ll.get_agent_ignore_list(target_root, ide)
|
|
70
|
+
new = ll.get_all_agent_ignore(target_root)
|
|
71
|
+
adapter = ide_get(ide)
|
|
72
|
+
adapter.remove_agent_ignore(target_root, sorted(set(old) - set(new)))
|
|
73
|
+
adapter.add_agent_ignore(target_root, sorted(set(new) - set(old)))
|
|
74
|
+
ll.save_agent_ignore_list(target_root, ide, new)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def refresh_skills(target_root: Path, ide: str, extension: str) -> None:
|
|
78
|
+
_require_init(target_root)
|
|
79
|
+
adapter = ide_get(ide)
|
|
80
|
+
prior = ll.get_rendered_skills(target_root, ide, extension)
|
|
81
|
+
ll.validate_rendered_identity(target_root)
|
|
82
|
+
adapter.remove_skills(target_root, prior)
|
|
83
|
+
metas = ll.generate_skills_metadata(target_root, extension)
|
|
84
|
+
rendered = adapter.add_skills(target_root, metas)
|
|
85
|
+
ll.save_skills_rendered(target_root, ide, extension, rendered)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def remove_skills(target_root: Path, ide: str, extension: str) -> None:
|
|
89
|
+
_require_init(target_root)
|
|
90
|
+
prior = ll.get_rendered_skills(target_root, ide, extension)
|
|
91
|
+
ide_get(ide).remove_skills(target_root, prior)
|
|
92
|
+
ll.save_skills_rendered(target_root, ide, extension, [])
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def refresh_mcp(target_root: Path, ide: str, extension: str) -> None:
|
|
96
|
+
_require_init(target_root)
|
|
97
|
+
prior = ll.get_rendered_mcp(target_root, ide, extension)
|
|
98
|
+
ll.validate_rendered_identity(target_root)
|
|
99
|
+
nm = ll.list_mcp(target_root, extension)
|
|
100
|
+
adapter = ide_get(ide)
|
|
101
|
+
adapter.remove_mcp(target_root, prior)
|
|
102
|
+
new_names = adapter.add_mcp(target_root, nm)
|
|
103
|
+
ll.save_mcp_rendered(target_root, ide, extension, new_names)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def remove_mcp(target_root: Path, ide: str, extension: str) -> None:
|
|
107
|
+
_require_init(target_root)
|
|
108
|
+
names = ll.get_rendered_mcp(target_root, ide, extension)
|
|
109
|
+
ide_get(ide).remove_mcp(target_root, names)
|
|
110
|
+
ll.save_mcp_rendered(target_root, ide, extension, [])
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def refresh_entry_point(target_root: Path, ide: str) -> None:
|
|
114
|
+
_require_init(target_root)
|
|
115
|
+
ide_get(ide).rewrite_entry_point(target_root, SPAWN_ENTRY_POINT_PROMPT)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def refresh_extension_for_ide(target_root: Path, ide: str, extension: str) -> None:
|
|
119
|
+
refresh_mcp(target_root, ide, extension)
|
|
120
|
+
refresh_skills(target_root, ide, extension)
|
|
121
|
+
refresh_agent_ignore(target_root, ide)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def remove_extension_for_ide(target_root: Path, ide: str, extension: str) -> None:
|
|
125
|
+
remove_mcp(target_root, ide, extension)
|
|
126
|
+
remove_skills(target_root, ide, extension)
|
|
127
|
+
old = ll.get_agent_ignore_list(target_root, ide)
|
|
128
|
+
new = _agent_ignore_merge_excluding(target_root, extension)
|
|
129
|
+
adapter = ide_get(ide)
|
|
130
|
+
adapter.remove_agent_ignore(target_root, sorted(set(old) - set(new)))
|
|
131
|
+
adapter.add_agent_ignore(target_root, sorted(set(new) - set(old)))
|
|
132
|
+
ll.save_agent_ignore_list(target_root, ide, new)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def refresh_navigation(target_root: Path) -> None:
|
|
136
|
+
_require_init(target_root)
|
|
137
|
+
for ext in ll.list_extensions(target_root):
|
|
138
|
+
ll.save_extension_navigation(
|
|
139
|
+
target_root,
|
|
140
|
+
ext,
|
|
141
|
+
ll.get_required_read_global(target_root, ext),
|
|
142
|
+
ll.get_auto_read_global(target_root, ext),
|
|
143
|
+
)
|
|
144
|
+
ll.save_rules_navigation(target_root)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def refresh_rules_navigation(target_root: Path) -> None:
|
|
148
|
+
_require_init(target_root)
|
|
149
|
+
ll.save_rules_navigation(target_root)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _refresh_extension_core(target_root: Path, extension: str) -> None:
|
|
153
|
+
for ide in ll.list_ides(target_root):
|
|
154
|
+
refresh_mcp(target_root, ide, extension)
|
|
155
|
+
refresh_skills(target_root, ide, extension)
|
|
156
|
+
for ide in ll.list_ides(target_root):
|
|
157
|
+
refresh_agent_ignore(target_root, ide)
|
|
158
|
+
refresh_gitignore(target_root)
|
|
159
|
+
refresh_navigation(target_root)
|
|
160
|
+
for ide in ll.list_ides(target_root):
|
|
161
|
+
refresh_entry_point(target_root, ide)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def refresh_extension(target_root: Path, extension: str) -> None:
|
|
165
|
+
_require_init(target_root)
|
|
166
|
+
scripts.run_before_install_scripts(target_root, extension)
|
|
167
|
+
_refresh_extension_core(target_root, extension)
|
|
168
|
+
scripts.run_after_install_scripts(target_root, extension)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def remove_extension(target_root: Path, extension: str) -> None:
|
|
172
|
+
_require_init(target_root)
|
|
173
|
+
if extension not in ll.list_extensions(target_root):
|
|
174
|
+
return
|
|
175
|
+
cfg = ll._load_ext_config(target_root, extension)
|
|
176
|
+
scripts.run_before_uninstall_scripts(target_root, extension)
|
|
177
|
+
for ide in ll.list_ides(target_root):
|
|
178
|
+
remove_extension_for_ide(target_root, ide, extension)
|
|
179
|
+
static_files, static_folders = ll.get_removable(target_root, extension)
|
|
180
|
+
for rel in static_files:
|
|
181
|
+
p = safe_path(target_root, rel.replace("\\", "/"))
|
|
182
|
+
if p.is_file():
|
|
183
|
+
p.unlink(missing_ok=True)
|
|
184
|
+
for rel in static_folders:
|
|
185
|
+
p = safe_path(target_root, rel.replace("\\", "/"))
|
|
186
|
+
if p.is_dir():
|
|
187
|
+
shutil.rmtree(p, ignore_errors=True)
|
|
188
|
+
ext_dir = _extend_dir(target_root, extension)
|
|
189
|
+
after_spec = scripts.snapshot_after_uninstall_script(ext_dir, cfg)
|
|
190
|
+
if ext_dir.is_dir():
|
|
191
|
+
shutil.rmtree(ext_dir, ignore_errors=True)
|
|
192
|
+
refresh_gitignore(target_root)
|
|
193
|
+
refresh_navigation(target_root)
|
|
194
|
+
for ide in ll.list_ides(target_root):
|
|
195
|
+
refresh_agent_ignore(target_root, ide)
|
|
196
|
+
refresh_entry_point(target_root, ide)
|
|
197
|
+
scripts.run_after_uninstall_from_snapshot(target_root, extension, after_spec)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def update_extension(target_root: Path, extension: str, *, force: bool = False) -> None:
|
|
201
|
+
_require_init(target_root)
|
|
202
|
+
stored = dl._load_stored_source(target_root, extension)
|
|
203
|
+
if not stored:
|
|
204
|
+
raise SpawnError(f"no source.yaml for extension {extension!r}")
|
|
205
|
+
staged = dl._stage_extension(target_root, stored.source.path, stored.source.branch, require_init=True)
|
|
206
|
+
try:
|
|
207
|
+
cand = staged.config
|
|
208
|
+
if dl._source_info_key(staged.source_info) != dl._source_info_key(stored.source):
|
|
209
|
+
raise SpawnError(
|
|
210
|
+
"source identity does not match installed record; remove the extension then install again"
|
|
211
|
+
)
|
|
212
|
+
if not force and dl.compare_version_strings(cand.version, stored.installed.version) <= 0:
|
|
213
|
+
raise SpawnError(
|
|
214
|
+
f"extension version must be newer than installed {stored.installed.version!r} "
|
|
215
|
+
f"(got {cand.version!r})"
|
|
216
|
+
)
|
|
217
|
+
dl._validate_render_identity_for_new_extension(target_root, extension, staged.extsrc_dir)
|
|
218
|
+
scripts.run_before_install_scripts(target_root, extension, ext_layout=staged.extsrc_dir)
|
|
219
|
+
dest = _extend_dir(target_root, extension)
|
|
220
|
+
ensure_dir(dest.parent)
|
|
221
|
+
dl._copy_extsrc_tree(staged.extsrc_dir, dest)
|
|
222
|
+
dl._write_source_yaml(target_root, extension, staged.source_info, cand.version)
|
|
223
|
+
dl._materialize_files(target_root, extension, cand)
|
|
224
|
+
finally:
|
|
225
|
+
staged.cleanup()
|
|
226
|
+
_refresh_extension_core(target_root, extension)
|
|
227
|
+
scripts.run_after_install_scripts(target_root, extension)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def reinstall_extension(target_root: Path, extension: str) -> None:
|
|
231
|
+
_require_init(target_root)
|
|
232
|
+
if extension not in ll.list_extensions(target_root):
|
|
233
|
+
raise SpawnError(f"extension {extension!r} is not installed")
|
|
234
|
+
stored = dl._load_stored_source(target_root, extension)
|
|
235
|
+
if not stored:
|
|
236
|
+
raise SpawnError(f"no source.yaml for extension {extension!r}")
|
|
237
|
+
remove_extension(target_root, extension)
|
|
238
|
+
install_extension(target_root, stored.source.path, stored.source.branch)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def extension_healthcheck(target_root: Path, extension: str) -> bool:
|
|
242
|
+
_require_init(target_root)
|
|
243
|
+
if extension not in ll.list_extensions(target_root):
|
|
244
|
+
return False
|
|
245
|
+
ext_root = _extend_dir(target_root, extension)
|
|
246
|
+
try:
|
|
247
|
+
extension_check(ext_root, strict=True)
|
|
248
|
+
except SpawnError:
|
|
249
|
+
return False
|
|
250
|
+
return scripts.run_healthcheck_scripts(target_root, extension)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def extension_init(path: Path, name: str) -> None:
|
|
254
|
+
extsrc = path / "extsrc"
|
|
255
|
+
cfg_path = extsrc / "config.yaml"
|
|
256
|
+
if cfg_path.is_file():
|
|
257
|
+
warnings.warn(
|
|
258
|
+
"extsrc/config.yaml already exists; left unchanged during extension init",
|
|
259
|
+
SpawnWarning,
|
|
260
|
+
)
|
|
261
|
+
return
|
|
262
|
+
ensure_dir(extsrc / "skills")
|
|
263
|
+
ensure_dir(extsrc / "files")
|
|
264
|
+
ensure_dir(extsrc / "setup")
|
|
265
|
+
template = {
|
|
266
|
+
"name": name,
|
|
267
|
+
"schema": 1,
|
|
268
|
+
"version": "0.1.0",
|
|
269
|
+
"files": {},
|
|
270
|
+
"folders": {},
|
|
271
|
+
"agent-ignore": [],
|
|
272
|
+
"git-ignore": [],
|
|
273
|
+
"skills": {},
|
|
274
|
+
"setup": {},
|
|
275
|
+
}
|
|
276
|
+
save_yaml(cfg_path, template)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def extension_check(path: Path, strict: bool = False) -> list[str]:
|
|
280
|
+
warnings_out: list[str] = []
|
|
281
|
+
if (path / "extsrc" / "config.yaml").is_file():
|
|
282
|
+
extsrc = path / "extsrc"
|
|
283
|
+
elif (path / "config.yaml").is_file():
|
|
284
|
+
extsrc = path
|
|
285
|
+
else:
|
|
286
|
+
raise SpawnError("extsrc/config.yaml is missing")
|
|
287
|
+
cfg_path = extsrc / "config.yaml"
|
|
288
|
+
raw = load_yaml(cfg_path)
|
|
289
|
+
try:
|
|
290
|
+
cfg = ExtensionConfig.model_validate(raw)
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise SpawnError(f"invalid extension config: {e}") from e
|
|
293
|
+
skills_dir = extsrc / "skills"
|
|
294
|
+
setup_dir = extsrc / "setup"
|
|
295
|
+
files_dir = extsrc / "files"
|
|
296
|
+
for key, sk in cfg.skills.items():
|
|
297
|
+
skill_file = skills_dir / key
|
|
298
|
+
if not skill_file.is_file():
|
|
299
|
+
msg = f"skill file missing: skills/{key}"
|
|
300
|
+
if strict:
|
|
301
|
+
raise SpawnError(msg)
|
|
302
|
+
warnings_out.append(msg)
|
|
303
|
+
for _ent_key, ent in cfg.files.items():
|
|
304
|
+
gr, lr = ent.globalRead, ent.localRead
|
|
305
|
+
if gr != ReadFlag.no or lr != ReadFlag.no:
|
|
306
|
+
if not ent.description or not ent.description.strip():
|
|
307
|
+
msg = f"file {_ent_key!r} has read visibility but no description"
|
|
308
|
+
if strict:
|
|
309
|
+
raise SpawnError(msg)
|
|
310
|
+
warnings_out.append(msg)
|
|
311
|
+
mcp_path = extsrc / "mcp.json"
|
|
312
|
+
if mcp_path.is_file():
|
|
313
|
+
try:
|
|
314
|
+
load_json(mcp_path)
|
|
315
|
+
except Exception as e:
|
|
316
|
+
msg = f"mcp.json not parseable: {e}"
|
|
317
|
+
if strict:
|
|
318
|
+
raise SpawnError(msg) from e
|
|
319
|
+
warnings_out.append(msg)
|
|
320
|
+
if cfg.setup:
|
|
321
|
+
for _phase, rel in (
|
|
322
|
+
("before-install", cfg.setup.before_install),
|
|
323
|
+
("after-install", cfg.setup.after_install),
|
|
324
|
+
("before-uninstall", cfg.setup.before_uninstall),
|
|
325
|
+
("after-uninstall", cfg.setup.after_uninstall),
|
|
326
|
+
("healthcheck", cfg.setup.healthcheck),
|
|
327
|
+
):
|
|
328
|
+
if rel and not (setup_dir / rel).is_file():
|
|
329
|
+
msg = f"setup script missing: setup/{rel}"
|
|
330
|
+
if strict:
|
|
331
|
+
raise SpawnError(msg)
|
|
332
|
+
warnings_out.append(msg)
|
|
333
|
+
declared = set(cfg.files.keys())
|
|
334
|
+
if files_dir.is_dir():
|
|
335
|
+
for f in files_dir.rglob("*"):
|
|
336
|
+
if f.is_file():
|
|
337
|
+
rel = f.relative_to(files_dir).as_posix()
|
|
338
|
+
if rel not in declared:
|
|
339
|
+
msg = f"undeclared file under extsrc/files: {rel}"
|
|
340
|
+
if strict:
|
|
341
|
+
raise SpawnError(msg)
|
|
342
|
+
warnings_out.append(msg)
|
|
343
|
+
return warnings_out
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def extension_from_rules(source: str, output_path: Path, name: str, branch: str | None = None) -> None:
|
|
347
|
+
tmp = Path(tempfile.mkdtemp(prefix="spawn-from-rules-"))
|
|
348
|
+
try:
|
|
349
|
+
src_path = Path(source).expanduser()
|
|
350
|
+
if src_path.is_dir() and (src_path.resolve() / "spawn" / "rules").is_dir():
|
|
351
|
+
root = src_path.resolve()
|
|
352
|
+
else:
|
|
353
|
+
root = dl.stage_repository_root(str(source), branch, tmp)
|
|
354
|
+
rules = root / "spawn" / "rules"
|
|
355
|
+
extsrc = output_path / "extsrc"
|
|
356
|
+
ensure_dir(extsrc / "files" / "spawn" / "rules")
|
|
357
|
+
if rules.is_dir():
|
|
358
|
+
for f in rules.rglob("*"):
|
|
359
|
+
if f.is_file():
|
|
360
|
+
rel = f.relative_to(rules)
|
|
361
|
+
dest = extsrc / "files" / "spawn" / "rules" / rel
|
|
362
|
+
ensure_dir(dest.parent)
|
|
363
|
+
shutil.copy2(f, dest)
|
|
364
|
+
files_meta: dict = {}
|
|
365
|
+
if rules.is_dir():
|
|
366
|
+
for f in sorted(rules.rglob("*")):
|
|
367
|
+
if f.is_file():
|
|
368
|
+
rel_key = ("spawn/rules/" + f.relative_to(rules).as_posix()).replace("\\", "/")
|
|
369
|
+
files_meta[rel_key] = {
|
|
370
|
+
"description": "Rule imported by extension_from_rules.",
|
|
371
|
+
"mode": "static",
|
|
372
|
+
"globalRead": "required",
|
|
373
|
+
"localRead": "no",
|
|
374
|
+
}
|
|
375
|
+
save_yaml(
|
|
376
|
+
extsrc / "config.yaml",
|
|
377
|
+
{
|
|
378
|
+
"name": name,
|
|
379
|
+
"schema": 1,
|
|
380
|
+
"version": "0.1.0",
|
|
381
|
+
"files": files_meta,
|
|
382
|
+
"folders": {},
|
|
383
|
+
"agent-ignore": [],
|
|
384
|
+
"git-ignore": [],
|
|
385
|
+
"skills": {},
|
|
386
|
+
"setup": {},
|
|
387
|
+
},
|
|
388
|
+
)
|
|
389
|
+
ensure_dir(extsrc / "skills")
|
|
390
|
+
ensure_dir(extsrc / "setup")
|
|
391
|
+
finally:
|
|
392
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def add_ide(target_root: Path, ide: str) -> None:
|
|
396
|
+
_require_init(target_root)
|
|
397
|
+
ll.add_ide_to_list(target_root, ide)
|
|
398
|
+
adapter = ide_get(ide)
|
|
399
|
+
dr = adapter.detect(target_root)
|
|
400
|
+
caps = dr.capabilities
|
|
401
|
+
if caps.skills == "unsupported":
|
|
402
|
+
warnings.warn(
|
|
403
|
+
f"IDE {ide!r} does not support skills; skills were skipped",
|
|
404
|
+
SpawnWarning,
|
|
405
|
+
)
|
|
406
|
+
if caps.mcp in ("unsupported", "external"):
|
|
407
|
+
warnings.warn(
|
|
408
|
+
f"IDE {ide!r} has limited MCP support ({caps.mcp})",
|
|
409
|
+
SpawnWarning,
|
|
410
|
+
)
|
|
411
|
+
refresh_entry_point(target_root, ide)
|
|
412
|
+
for ext in ll.list_extensions(target_root):
|
|
413
|
+
refresh_mcp(target_root, ide, ext)
|
|
414
|
+
refresh_skills(target_root, ide, ext)
|
|
415
|
+
refresh_agent_ignore(target_root, ide)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def remove_ide(target_root: Path, ide: str) -> None:
|
|
419
|
+
_require_init(target_root)
|
|
420
|
+
for ext in ll.list_extensions(target_root):
|
|
421
|
+
remove_mcp(target_root, ide, ext)
|
|
422
|
+
remove_skills(target_root, ide, ext)
|
|
423
|
+
old = ll.get_agent_ignore_list(target_root, ide)
|
|
424
|
+
ide_get(ide).remove_agent_ignore(target_root, old)
|
|
425
|
+
ide_get(ide).finalize_repo_after_ide_removed(target_root)
|
|
426
|
+
ll.remove_ide_from_list(target_root, ide)
|
|
427
|
+
ll.remove_ide_metadata_dir(target_root, ide)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def install_extension(target_root: Path, path: str, branch: str | None = None) -> None:
|
|
431
|
+
_require_init(target_root)
|
|
432
|
+
name = dl.download_extension(target_root, path, branch)
|
|
433
|
+
_refresh_extension_core(target_root, name)
|
|
434
|
+
scripts.run_after_install_scripts(target_root, name)
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
__all__ = [
|
|
438
|
+
"SPAWN_ENTRY_POINT_PROMPT",
|
|
439
|
+
"_refresh_extension_core",
|
|
440
|
+
"add_ide",
|
|
441
|
+
"extension_check",
|
|
442
|
+
"extension_from_rules",
|
|
443
|
+
"extension_healthcheck",
|
|
444
|
+
"extension_init",
|
|
445
|
+
"install_extension",
|
|
446
|
+
"refresh_agent_ignore",
|
|
447
|
+
"refresh_entry_point",
|
|
448
|
+
"refresh_extension",
|
|
449
|
+
"refresh_extension_for_ide",
|
|
450
|
+
"refresh_gitignore",
|
|
451
|
+
"refresh_mcp",
|
|
452
|
+
"refresh_navigation",
|
|
453
|
+
"refresh_rules_navigation",
|
|
454
|
+
"refresh_skills",
|
|
455
|
+
"reinstall_extension",
|
|
456
|
+
"remove_extension",
|
|
457
|
+
"remove_extension_for_ide",
|
|
458
|
+
"remove_ide",
|
|
459
|
+
"remove_mcp",
|
|
460
|
+
"remove_skills",
|
|
461
|
+
"update_extension",
|
|
462
|
+
]
|