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.
Files changed (49) hide show
  1. {spawn_cli-0.1.6/src/spawn_cli.egg-info → spawn_cli-0.1.8}/PKG-INFO +1 -1
  2. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/pyproject.toml +1 -1
  3. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/cli.py +12 -0
  4. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/high_level.py +31 -0
  5. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/low_level.py +108 -7
  6. {spawn_cli-0.1.6 → spawn_cli-0.1.8/src/spawn_cli.egg-info}/PKG-INFO +1 -1
  7. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/tests/test_cli.py +22 -0
  8. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/LICENSE +0 -0
  9. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/README.md +0 -0
  10. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/setup.cfg +0 -0
  11. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/__init__.py +0 -0
  12. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/__init__.py +0 -0
  13. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/download.py +0 -0
  14. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/errors.py +0 -0
  15. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/core/scripts.py +0 -0
  16. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/errors.py +0 -0
  17. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/__init__.py +0 -0
  18. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/_helpers.py +0 -0
  19. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/_stub.py +0 -0
  20. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/_vacancy.py +0 -0
  21. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/claude_code.py +0 -0
  22. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/codex.py +0 -0
  23. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/cursor.py +0 -0
  24. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/gemini_cli.py +0 -0
  25. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/github_copilot.py +0 -0
  26. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/registry.py +0 -0
  27. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/ide/windsurf.py +0 -0
  28. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/__init__.py +0 -0
  29. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/json_io.py +0 -0
  30. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/lock.py +0 -0
  31. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/paths.py +0 -0
  32. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/text_io.py +0 -0
  33. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/toml_io.py +0 -0
  34. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/io/yaml_io.py +0 -0
  35. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/__init__.py +0 -0
  36. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/config.py +0 -0
  37. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/mcp.py +0 -0
  38. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/metadata.py +0 -0
  39. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/navigation.py +0 -0
  40. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/models/skill.py +0 -0
  41. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/resources/__init__.py +0 -0
  42. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/resources/default_core_config.yaml +0 -0
  43. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli/warnings_display.py +0 -0
  44. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/SOURCES.txt +0 -0
  45. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
  46. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/entry_points.txt +0 -0
  47. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/requires.txt +0 -0
  48. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/src/spawn_cli.egg-info/top_level.txt +0 -0
  49. {spawn_cli-0.1.6 → spawn_cli-0.1.8}/tests/test_warnings_display.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spawn-cli"
7
- version = "0.1.6"
7
+ version = "0.1.8"
8
8
  description = "A minimal installable Python CLI package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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, merged_global: list[SkillFileRef], local_req: list[SkillFileRef], cfg_ext: ExtensionConfig
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 == p and r.description:
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 == p and r.description:
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(p, merged_global_required, local_required, cfg_ext),
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",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -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