spawn-cli 0.1.6__tar.gz → 0.1.8__tar.gz
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-0.1.6/src/spawn_cli.egg-info → spawn_cli-0.1.8}/PKG-INFO +1 -1
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/pyproject.toml +1 -1
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/cli.py +12 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/high_level.py +31 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/low_level.py +108 -7
- {spawn_cli-0.1.6 → spawn_cli-0.1.8/src/spawn_cli.egg-info}/PKG-INFO +1 -1
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/tests/test_cli.py +22 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/LICENSE +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/README.md +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/setup.cfg +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/__init__.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/__init__.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/download.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/errors.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/scripts.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/errors.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/__init__.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/_helpers.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/_stub.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/_vacancy.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/claude_code.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/codex.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/cursor.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/gemini_cli.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/github_copilot.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/registry.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/windsurf.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/__init__.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/json_io.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/lock.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/paths.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/text_io.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/toml_io.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/yaml_io.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/__init__.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/config.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/mcp.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/metadata.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/navigation.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/skill.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/resources/__init__.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/resources/default_core_config.yaml +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/warnings_display.py +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/SOURCES.txt +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/entry_points.txt +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/requires.txt +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/top_level.txt +0 -0
- {spawn_cli-0.1.6 → spawn_cli-0.1.8}/tests/test_warnings_display.py +0 -0
|
@@ -40,6 +40,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
40
40
|
),
|
|
41
41
|
)
|
|
42
42
|
|
|
43
|
+
sub.add_parser(
|
|
44
|
+
"refresh",
|
|
45
|
+
help="Sync core config from defaults and rebuild IDE renders",
|
|
46
|
+
description=(
|
|
47
|
+
"Merge bundled defaults into spawn/.core/config.yaml, then refresh skills, MCP, "
|
|
48
|
+
"agent ignore, navigation, gitignore metadata, and IDE entry points."
|
|
49
|
+
),
|
|
50
|
+
)
|
|
51
|
+
|
|
43
52
|
rules_p = sub.add_parser(
|
|
44
53
|
"rules",
|
|
45
54
|
help="Rebuild navigation mandatory reads from spawn/rules/",
|
|
@@ -321,6 +330,9 @@ def _dispatch(args: argparse.Namespace, target_root: Path) -> int:
|
|
|
321
330
|
with spawn_lock(target_root):
|
|
322
331
|
if cmd == "rules":
|
|
323
332
|
return _dispatch_rules(args, target_root)
|
|
333
|
+
if cmd == "refresh":
|
|
334
|
+
hl.refresh_repository(target_root)
|
|
335
|
+
return 0
|
|
324
336
|
if cmd == "ide":
|
|
325
337
|
return _dispatch_ide(args, target_root)
|
|
326
338
|
if cmd == "extension":
|
|
@@ -222,6 +222,36 @@ def refresh_rules_navigation(target_root: Path) -> None:
|
|
|
222
222
|
ll.save_rules_navigation(target_root)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
+
def refresh_repository(target_root: Path) -> None:
|
|
226
|
+
"""Sync core config from package defaults and rebuild all IDE-facing metadata."""
|
|
227
|
+
_require_init(target_root)
|
|
228
|
+
ll.merge_core_config_from_defaults(target_root)
|
|
229
|
+
needs_skill = _any_extension_has_skill_files(target_root)
|
|
230
|
+
needs_mcp = _any_extension_has_mcp_servers(target_root)
|
|
231
|
+
for ide in ll.list_ides(target_root):
|
|
232
|
+
_warn_capability_gaps(
|
|
233
|
+
ide,
|
|
234
|
+
ide_get(ide).detect(target_root).capabilities,
|
|
235
|
+
needs_skill_render=needs_skill,
|
|
236
|
+
needs_mcp_merge=needs_mcp,
|
|
237
|
+
)
|
|
238
|
+
merged_any = False
|
|
239
|
+
for ide in ll.list_ides(target_root):
|
|
240
|
+
for ext in ll.list_extensions(target_root):
|
|
241
|
+
names = refresh_mcp(target_root, ide, ext, emit_mcp_merged_notice=False)
|
|
242
|
+
if names:
|
|
243
|
+
merged_any = True
|
|
244
|
+
_refresh_skills_all_extensions_for_ide(target_root, ide)
|
|
245
|
+
for ide in ll.list_ides(target_root):
|
|
246
|
+
refresh_agent_ignore(target_root, ide)
|
|
247
|
+
if merged_any:
|
|
248
|
+
print(MCP_MERGED_NOTICE)
|
|
249
|
+
refresh_gitignore(target_root)
|
|
250
|
+
refresh_navigation(target_root)
|
|
251
|
+
for ide in ll.list_ides(target_root):
|
|
252
|
+
refresh_entry_point(target_root, ide)
|
|
253
|
+
|
|
254
|
+
|
|
225
255
|
def _refresh_extension_core(target_root: Path, extension: str) -> None:
|
|
226
256
|
needs_skill = _any_extension_has_skill_files(target_root)
|
|
227
257
|
needs_mcp = bool(ll.list_mcp(target_root, extension).servers)
|
|
@@ -536,6 +566,7 @@ __all__ = [
|
|
|
536
566
|
"refresh_gitignore",
|
|
537
567
|
"refresh_mcp",
|
|
538
568
|
"refresh_navigation",
|
|
569
|
+
"refresh_repository",
|
|
539
570
|
"refresh_rules_navigation",
|
|
540
571
|
"refresh_skills",
|
|
541
572
|
"reinstall_extension",
|
|
@@ -18,7 +18,7 @@ from spawn_cli.io.json_io import load_json
|
|
|
18
18
|
from spawn_cli.io.paths import ensure_dir
|
|
19
19
|
from spawn_cli.io.text_io import read_lines, write_lines
|
|
20
20
|
from spawn_cli.io.yaml_io import configure_yaml_dump, load_yaml, save_yaml
|
|
21
|
-
from spawn_cli.models.config import ExtensionConfig, FileMode, IdeList, ReadFlag
|
|
21
|
+
from spawn_cli.models.config import CoreConfig, ExtensionConfig, FileMode, IdeList, ReadFlag
|
|
22
22
|
from spawn_cli.models.mcp import McpCapabilities, McpEnvVar, McpServer, McpTransport, NormalizedMcp
|
|
23
23
|
from spawn_cli.models.skill import SkillFileRef, SkillMetadata, SkillRawInfo
|
|
24
24
|
|
|
@@ -294,14 +294,59 @@ def _norm_read_path(path_str: str) -> str:
|
|
|
294
294
|
return Path(path_str).as_posix().replace("\\", "/")
|
|
295
295
|
|
|
296
296
|
|
|
297
|
+
def _navigation_rules_refs_from_section(section: Any) -> list[SkillFileRef]:
|
|
298
|
+
out: list[SkillFileRef] = []
|
|
299
|
+
if not isinstance(section, list):
|
|
300
|
+
return out
|
|
301
|
+
for grp in section:
|
|
302
|
+
if not isinstance(grp, dict):
|
|
303
|
+
continue
|
|
304
|
+
rules_raw = grp.get("rules")
|
|
305
|
+
if not isinstance(rules_raw, list):
|
|
306
|
+
continue
|
|
307
|
+
for entry in rules_raw:
|
|
308
|
+
if not isinstance(entry, dict):
|
|
309
|
+
continue
|
|
310
|
+
path_raw = entry.get("path")
|
|
311
|
+
if not isinstance(path_raw, str) or not path_raw.strip():
|
|
312
|
+
continue
|
|
313
|
+
desc_raw = entry.get("description")
|
|
314
|
+
desc = "" if desc_raw is None else str(desc_raw)
|
|
315
|
+
out.append(SkillFileRef(file=path_raw.strip(), description=desc))
|
|
316
|
+
return out
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _navigation_yaml_rules_refs(target_root: Path) -> tuple[list[SkillFileRef], list[SkillFileRef]]:
|
|
320
|
+
"""Return (read-required rules, read-contextual rules) from merged navigation.yaml."""
|
|
321
|
+
path = _spawn(target_root) / "navigation.yaml"
|
|
322
|
+
raw = load_yaml(path)
|
|
323
|
+
if not isinstance(raw, dict):
|
|
324
|
+
return [], []
|
|
325
|
+
rr = raw.get("read-required")
|
|
326
|
+
cq = raw.get("read-contextual")
|
|
327
|
+
return (
|
|
328
|
+
_navigation_rules_refs_from_section(rr if isinstance(rr, list) else []),
|
|
329
|
+
_navigation_rules_refs_from_section(cq if isinstance(cq, list) else []),
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
297
333
|
def _required_read_description_for_path(
|
|
298
|
-
p: str,
|
|
334
|
+
p: str,
|
|
335
|
+
merged_global: list[SkillFileRef],
|
|
336
|
+
local_req: list[SkillFileRef],
|
|
337
|
+
cfg_ext: ExtensionConfig,
|
|
338
|
+
nav_rules_required: list[SkillFileRef] | None = None,
|
|
299
339
|
) -> str:
|
|
340
|
+
nk = _norm_read_path(p)
|
|
341
|
+
if nav_rules_required:
|
|
342
|
+
for r in nav_rules_required:
|
|
343
|
+
if _norm_read_path(r.file) == nk and r.description:
|
|
344
|
+
return r.description
|
|
300
345
|
for r in merged_global:
|
|
301
|
-
if r.file ==
|
|
346
|
+
if _norm_read_path(r.file) == nk and r.description:
|
|
302
347
|
return r.description
|
|
303
348
|
for r in local_req:
|
|
304
|
-
if r.file ==
|
|
349
|
+
if _norm_read_path(r.file) == nk and r.description:
|
|
305
350
|
return r.description
|
|
306
351
|
return _describe_path(cfg_ext, p)
|
|
307
352
|
|
|
@@ -314,6 +359,7 @@ def generate_skills_metadata(target_root: Path, extension: str) -> list[SkillMet
|
|
|
314
359
|
merged_global_auto = _flatten_global_refs_ordered(target_root, get_auto_read_global_all(target_root))
|
|
315
360
|
local_required = get_required_read_ext_local(target_root, extension)
|
|
316
361
|
local_auto = get_auto_read_local(target_root, extension)
|
|
362
|
+
nav_rules_required, nav_rules_contextual = _navigation_yaml_rules_refs(target_root)
|
|
317
363
|
|
|
318
364
|
metas: list[SkillMetadata] = []
|
|
319
365
|
for skill_path in list_skills(target_root, extension):
|
|
@@ -331,10 +377,19 @@ def generate_skills_metadata(target_root: Path, extension: str) -> list[SkillMet
|
|
|
331
377
|
continue
|
|
332
378
|
seen_norm.add(nk)
|
|
333
379
|
required_paths.append(fp)
|
|
380
|
+
for ref in nav_rules_required:
|
|
381
|
+
nk = _norm_read_path(ref.file)
|
|
382
|
+
if nk in seen_norm:
|
|
383
|
+
continue
|
|
384
|
+
seen_norm.add(nk)
|
|
385
|
+
required_paths.append(ref.file)
|
|
386
|
+
|
|
334
387
|
required_out = [
|
|
335
388
|
SkillFileRef(
|
|
336
389
|
file=p,
|
|
337
|
-
description=_required_read_description_for_path(
|
|
390
|
+
description=_required_read_description_for_path(
|
|
391
|
+
p, merged_global_required, local_required, cfg_ext, nav_rules_required
|
|
392
|
+
),
|
|
338
393
|
)
|
|
339
394
|
for p in required_paths
|
|
340
395
|
]
|
|
@@ -355,6 +410,12 @@ def generate_skills_metadata(target_root: Path, extension: str) -> list[SkillMet
|
|
|
355
410
|
continue
|
|
356
411
|
seen_auto.add(key)
|
|
357
412
|
auto_out.append(ar)
|
|
413
|
+
for ar in nav_rules_contextual:
|
|
414
|
+
key = _norm_read_path(ar.file)
|
|
415
|
+
if key in required_key_set or key in seen_auto:
|
|
416
|
+
continue
|
|
417
|
+
seen_auto.add(key)
|
|
418
|
+
auto_out.append(ar)
|
|
358
419
|
|
|
359
420
|
metas.append(
|
|
360
421
|
SkillMetadata(
|
|
@@ -451,8 +512,6 @@ def get_navigation_metadata(target_root: Path, extension: str) -> dict:
|
|
|
451
512
|
|
|
452
513
|
def get_core_agent_ignore(target_root: Path) -> list[str]:
|
|
453
514
|
path = _spawn(target_root) / ".core" / "config.yaml"
|
|
454
|
-
from spawn_cli.models.config import CoreConfig
|
|
455
|
-
|
|
456
515
|
data = load_yaml(path)
|
|
457
516
|
if not data:
|
|
458
517
|
return []
|
|
@@ -460,6 +519,47 @@ def get_core_agent_ignore(target_root: Path) -> list[str]:
|
|
|
460
519
|
return list(core.agent_ignore)
|
|
461
520
|
|
|
462
521
|
|
|
522
|
+
def merge_core_config_from_defaults(target_root: Path) -> None:
|
|
523
|
+
"""Merge bundled default core config into ``spawn/.core/config.yaml``.
|
|
524
|
+
|
|
525
|
+
``version`` becomes the bundled default. ``agent-ignore`` is the bundled
|
|
526
|
+
list (in resource order) followed by any existing patterns not present in
|
|
527
|
+
the default set.
|
|
528
|
+
"""
|
|
529
|
+
path = _spawn(target_root) / ".core" / "config.yaml"
|
|
530
|
+
if not path.is_file():
|
|
531
|
+
raise SpawnError("spawn/.core/config.yaml is missing; run spawn init.")
|
|
532
|
+
bundled_txt = (
|
|
533
|
+
resources.files("spawn_cli.resources")
|
|
534
|
+
.joinpath("default_core_config.yaml")
|
|
535
|
+
.read_text(encoding="utf-8")
|
|
536
|
+
)
|
|
537
|
+
yparse = YAML(typ="safe")
|
|
538
|
+
raw_default = yparse.load(bundled_txt)
|
|
539
|
+
if not isinstance(raw_default, dict):
|
|
540
|
+
raise SpawnError("internal error: bundled default core config is not a mapping")
|
|
541
|
+
try:
|
|
542
|
+
default_core = CoreConfig.model_validate(raw_default)
|
|
543
|
+
except Exception as e:
|
|
544
|
+
raise SpawnError(f"internal error: bundled default core config invalid: {e}") from e
|
|
545
|
+
raw_existing = load_yaml(path)
|
|
546
|
+
if not raw_existing:
|
|
547
|
+
raise SpawnError(
|
|
548
|
+
"spawn/.core/config.yaml is empty or invalid; restore it or run spawn init again."
|
|
549
|
+
)
|
|
550
|
+
try:
|
|
551
|
+
current = CoreConfig.model_validate(raw_existing)
|
|
552
|
+
except Exception as e:
|
|
553
|
+
raise SpawnError(f"invalid spawn/.core/config.yaml: {e}") from e
|
|
554
|
+
default_set = set(default_core.agent_ignore)
|
|
555
|
+
merged_ignore = list(default_core.agent_ignore)
|
|
556
|
+
for g in current.agent_ignore:
|
|
557
|
+
if g not in default_set:
|
|
558
|
+
merged_ignore.append(g)
|
|
559
|
+
out = CoreConfig(version=default_core.version, agent_ignore=merged_ignore)
|
|
560
|
+
save_yaml(path, out.model_dump(by_alias=True))
|
|
561
|
+
|
|
562
|
+
|
|
463
563
|
def get_ext_agent_ignore(target_root: Path, extension: str) -> list[str]:
|
|
464
564
|
cfg = _load_ext_config(target_root, extension)
|
|
465
565
|
return list(cfg.agent_ignore)
|
|
@@ -890,6 +990,7 @@ __all__ = [
|
|
|
890
990
|
"list_ides",
|
|
891
991
|
"list_mcp",
|
|
892
992
|
"list_skills",
|
|
993
|
+
"merge_core_config_from_defaults",
|
|
893
994
|
"METADATA_TEMP_MAX_AGE_SECONDS",
|
|
894
995
|
"normalize_skill_name",
|
|
895
996
|
"prune_metadata_temp",
|
|
@@ -88,6 +88,28 @@ def test_spawn_rules_refresh(tmp_path, monkeypatch):
|
|
|
88
88
|
mock_refresh.assert_called_once_with(root)
|
|
89
89
|
|
|
90
90
|
|
|
91
|
+
def test_spawn_refresh(tmp_path, monkeypatch):
|
|
92
|
+
monkeypatch.chdir(tmp_path)
|
|
93
|
+
from spawn_cli.core import low_level as ll
|
|
94
|
+
|
|
95
|
+
ll.init(tmp_path)
|
|
96
|
+
root = tmp_path.resolve()
|
|
97
|
+
mock_repo = MagicMock()
|
|
98
|
+
|
|
99
|
+
with patch("spawn_cli.cli.spawn_lock", _noop_lock), patch(
|
|
100
|
+
"spawn_cli.cli.hl.refresh_repository", mock_repo
|
|
101
|
+
):
|
|
102
|
+
assert main(["refresh"]) == 0
|
|
103
|
+
|
|
104
|
+
mock_repo.assert_called_once_with(root)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_spawn_refresh_need_init(tmp_path, monkeypatch, capsys):
|
|
108
|
+
monkeypatch.chdir(tmp_path)
|
|
109
|
+
assert main(["refresh"]) == 1
|
|
110
|
+
assert "need init before" in capsys.readouterr().err
|
|
111
|
+
|
|
112
|
+
|
|
91
113
|
def test_spawn_ide_list_supported_ides(tmp_path, monkeypatch, capsys):
|
|
92
114
|
monkeypatch.chdir(tmp_path)
|
|
93
115
|
(tmp_path / "spawn").mkdir()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|