spawn-cli 0.1.13__tar.gz → 0.1.14__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.13/src/spawn_cli.egg-info → spawn_cli-0.1.14}/PKG-INFO +1 -1
  2. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/pyproject.toml +1 -1
  3. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/cli.py +1 -1
  4. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/download.py +1 -10
  5. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/high_level.py +54 -8
  6. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/low_level.py +69 -3
  7. {spawn_cli-0.1.13 → spawn_cli-0.1.14/src/spawn_cli.egg-info}/PKG-INFO +1 -1
  8. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/LICENSE +0 -0
  9. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/README.md +0 -0
  10. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/setup.cfg +0 -0
  11. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/__init__.py +0 -0
  12. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/__init__.py +0 -0
  13. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/errors.py +0 -0
  14. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/scripts.py +0 -0
  15. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/errors.py +0 -0
  16. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/__init__.py +0 -0
  17. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/_helpers.py +0 -0
  18. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/_stub.py +0 -0
  19. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/_vacancy.py +0 -0
  20. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/claude_code.py +0 -0
  21. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/codex.py +0 -0
  22. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/cursor.py +0 -0
  23. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/gemini_cli.py +0 -0
  24. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/github_copilot.py +0 -0
  25. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/registry.py +0 -0
  26. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/windsurf.py +0 -0
  27. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/__init__.py +0 -0
  28. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/json_io.py +0 -0
  29. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/lock.py +0 -0
  30. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/paths.py +0 -0
  31. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/text_io.py +0 -0
  32. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/toml_io.py +0 -0
  33. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/yaml_io.py +0 -0
  34. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/__init__.py +0 -0
  35. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/config.py +0 -0
  36. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/mcp.py +0 -0
  37. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/metadata.py +0 -0
  38. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/navigation.py +0 -0
  39. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/skill.py +0 -0
  40. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/resources/__init__.py +0 -0
  41. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/resources/default_core_config.yaml +0 -0
  42. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/warnings_display.py +0 -0
  43. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/SOURCES.txt +0 -0
  44. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
  45. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/entry_points.txt +0 -0
  46. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/requires.txt +0 -0
  47. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/top_level.txt +0 -0
  48. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/tests/test_cli.py +0 -0
  49. {spawn_cli-0.1.13 → spawn_cli-0.1.14}/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.13
3
+ Version: 0.1.14
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.13"
7
+ version = "0.1.14"
8
8
  description = "A minimal installable Python CLI package."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -210,7 +210,7 @@ def build_parser() -> argparse.ArgumentParser:
210
210
  action="store_true",
211
211
  help=(
212
212
  "Fail on issues that default to warnings "
213
- "(missing skill files, bad mcp.json, empty file descriptions)."
213
+ "(missing skill files, invalid extsrc/mcp/*.json, empty file descriptions)."
214
214
  ),
215
215
  )
216
216
 
@@ -21,7 +21,6 @@ from ruamel.yaml import YAML
21
21
  from spawn_cli.core import low_level as ll
22
22
  from spawn_cli.core.errors import SpawnError, SpawnWarning
23
23
  from spawn_cli.core import scripts
24
- from spawn_cli.io.json_io import load_json
25
24
  from spawn_cli.io.paths import ensure_dir, safe_path
26
25
  from spawn_cli.io.yaml_io import load_yaml, save_yaml
27
26
  from spawn_cli.models.config import ExtensionConfig, ExtensionsMeta, FileMode, SourceYaml
@@ -128,15 +127,7 @@ def _candidate_norm_skill_names(extsrc_dir: Path) -> list[str]:
128
127
 
129
128
 
130
129
  def _candidate_mcp_server_names(extsrc_dir: Path) -> list[str]:
131
- path = extsrc_dir / "mcp.json"
132
- if not path.is_file():
133
- return []
134
- data = load_json(path)
135
- names: list[str] = []
136
- for s in data.get("servers") or []:
137
- if isinstance(s, dict) and s.get("name"):
138
- names.append(str(s["name"]))
139
- return names
130
+ return ll.extsrc_mcp_server_names_for_staging(extsrc_dir)
140
131
 
141
132
 
142
133
  def _validate_render_identity_for_new_extension(target_root: Path, new_name: str, extsrc_dir: Path) -> None:
@@ -10,7 +10,7 @@ from spawn_cli.core import low_level as ll
10
10
  from spawn_cli.core import scripts
11
11
  from spawn_cli.core.errors import SpawnError, SpawnWarning
12
12
  from spawn_cli.ide.registry import IdeCapabilities, get as ide_get
13
- from spawn_cli.io.json_io import load_json
13
+ from spawn_cli.io.json_io import save_json
14
14
  from spawn_cli.io.paths import ensure_dir, safe_path
15
15
  from spawn_cli.io.yaml_io import load_yaml, save_yaml
16
16
  from spawn_cli.models.config import ExtensionConfig, ReadFlag
@@ -417,6 +417,16 @@ def extension_healthcheck(target_root: Path, extension: str) -> bool:
417
417
  return scripts.run_healthcheck_scripts(target_root, extension)
418
418
 
419
419
 
420
+ def _ensure_empty_mcp_platform_files(extsrc: Path) -> None:
421
+ mdir = extsrc / "mcp"
422
+ ensure_dir(mdir)
423
+ empty: dict = {"servers": []}
424
+ for stem in ("windows", "linux", "macos"):
425
+ p = mdir / f"{stem}.json"
426
+ if not p.is_file():
427
+ save_json(p, empty)
428
+
429
+
420
430
  def extension_init(path: Path, name: str) -> None:
421
431
  extsrc = path / "extsrc"
422
432
  cfg_path = extsrc / "config.yaml"
@@ -429,6 +439,7 @@ def extension_init(path: Path, name: str) -> None:
429
439
  ensure_dir(extsrc / "skills")
430
440
  ensure_dir(extsrc / "files")
431
441
  ensure_dir(extsrc / "setup")
442
+ _ensure_empty_mcp_platform_files(extsrc)
432
443
  template = {
433
444
  "name": name,
434
445
  "schema": 1,
@@ -475,15 +486,49 @@ def extension_check(path: Path, strict: bool = False) -> list[str]:
475
486
  if strict:
476
487
  raise SpawnError(msg)
477
488
  warnings_out.append(msg)
478
- mcp_path = extsrc / "mcp.json"
479
- if mcp_path.is_file():
480
- try:
481
- load_json(mcp_path)
482
- except Exception as e:
483
- msg = f"mcp.json not parseable: {e}"
489
+ root_mcp = extsrc / "mcp.json"
490
+ if root_mcp.is_file():
491
+ msg = (
492
+ "obsolete extsrc/mcp.json is not used; remove it and use "
493
+ "extsrc/mcp/windows.json, linux.json, and macos.json only"
494
+ )
495
+ if strict:
496
+ raise SpawnError(msg)
497
+ warnings_out.append(msg)
498
+ mcp_dir = extsrc / "mcp"
499
+ if not mcp_dir.is_dir():
500
+ msg = "missing extsrc/mcp/ directory (expected windows.json, linux.json, macos.json)"
501
+ if strict:
502
+ raise SpawnError(msg)
503
+ warnings_out.append(msg)
504
+ else:
505
+ paths = ll.extension_mcp_platform_json_paths(extsrc)
506
+ if not all(p.is_file() for p in paths):
507
+ missing = [p.name for p in paths if not p.is_file()]
508
+ msg = f"incomplete extsrc/mcp layout (missing {', '.join(missing)})"
484
509
  if strict:
485
- raise SpawnError(msg) from e
510
+ raise SpawnError(msg)
486
511
  warnings_out.append(msg)
512
+ else:
513
+ name_sets: list[frozenset[str]] = []
514
+ for p in paths:
515
+ try:
516
+ nm = ll.normalized_mcp_from_mcp_json_path(p, cfg.name or "extension")
517
+ except Exception as e:
518
+ if strict:
519
+ raise SpawnError(f"MCP file invalid ({p}): {e}") from e
520
+ warnings_out.append(f"MCP file invalid ({p}): {e}")
521
+ name_sets = []
522
+ break
523
+ name_sets.append(frozenset(s.name for s in nm.servers))
524
+ if name_sets and len(set(name_sets)) != 1:
525
+ msg = (
526
+ "MCP server names must match across mcp/windows.json, "
527
+ "linux.json, and macos.json"
528
+ )
529
+ if strict:
530
+ raise SpawnError(msg)
531
+ warnings_out.append(msg)
487
532
  if cfg.setup:
488
533
  for _phase, rel in (
489
534
  ("before-install", cfg.setup.before_install),
@@ -555,6 +600,7 @@ def extension_from_rules(source: str, output_path: Path, name: str, branch: str
555
600
  )
556
601
  ensure_dir(extsrc / "skills")
557
602
  ensure_dir(extsrc / "setup")
603
+ _ensure_empty_mcp_platform_files(extsrc)
558
604
  finally:
559
605
  shutil.rmtree(tmp, ignore_errors=True)
560
606
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  import shutil
5
+ import sys
5
6
  import time
6
7
  import uuid
7
8
  import warnings
@@ -22,6 +23,8 @@ from spawn_cli.models.config import CoreConfig, ExtensionConfig, FileMode, IdeLi
22
23
  from spawn_cli.models.mcp import McpCapabilities, McpEnvVar, McpServer, McpTransport, NormalizedMcp
23
24
  from spawn_cli.models.skill import SkillFileRef, SkillMetadata, SkillRawInfo
24
25
 
26
+ MCP_PLATFORM_STEMS: tuple[str, ...] = ("windows", "linux", "macos")
27
+
25
28
  CANONICAL_IDE_KEYS: tuple[str, ...] = (
26
29
  "cursor",
27
30
  "codex",
@@ -658,9 +661,25 @@ def _mcp_capabilities(raw: dict[str, Any]) -> McpCapabilities:
658
661
  return McpCapabilities.model_validate(c) if isinstance(c, dict) else McpCapabilities()
659
662
 
660
663
 
661
- def list_mcp(target_root: Path, extension: str) -> NormalizedMcp:
662
- path = _extend_root(target_root) / extension / "mcp.json"
663
- data = load_json(path)
664
+ def mcp_host_platform_stem() -> str:
665
+ plat = sys.platform
666
+ if plat == "win32":
667
+ return "windows"
668
+ if plat == "darwin":
669
+ return "macos"
670
+ if plat.startswith("linux"):
671
+ return "linux"
672
+ raise SpawnError(
673
+ f"unsupported platform for MCP selection: {plat!r} (expected Windows, Linux, or macOS)"
674
+ )
675
+
676
+
677
+ def extension_mcp_platform_json_paths(extension_root: Path) -> tuple[Path, Path, Path]:
678
+ d = extension_root / "mcp"
679
+ return tuple(d / f"{s}.json" for s in MCP_PLATFORM_STEMS)
680
+
681
+
682
+ def _normalized_mcp_from_loaded(data: dict, path: Path, extension: str) -> NormalizedMcp:
664
683
  servers_out: list[McpServer] = []
665
684
  for s in data.get("servers") or []:
666
685
  if not isinstance(s, dict):
@@ -690,6 +709,49 @@ def list_mcp(target_root: Path, extension: str) -> NormalizedMcp:
690
709
  return NormalizedMcp(servers=servers_out)
691
710
 
692
711
 
712
+ def normalized_mcp_from_mcp_json_path(path: Path, extension: str) -> NormalizedMcp:
713
+ data = load_json(path)
714
+ return _normalized_mcp_from_loaded(data, path, extension)
715
+
716
+
717
+ def list_mcp(target_root: Path, extension: str) -> NormalizedMcp:
718
+ ext_root = _extend_root(target_root) / extension
719
+ mcp_dir = ext_root / "mcp"
720
+ if not mcp_dir.is_dir():
721
+ return NormalizedMcp(servers=[])
722
+ stem = mcp_host_platform_stem()
723
+ path = mcp_dir / f"{stem}.json"
724
+ if not path.is_file():
725
+ raise SpawnError(f"missing MCP platform file for this OS: {path}")
726
+ return normalized_mcp_from_mcp_json_path(path, extension)
727
+
728
+
729
+ def extsrc_mcp_server_names_for_staging(extsrc_dir: Path) -> list[str]:
730
+ """Return sorted MCP server names from extsrc ``mcp/*.json`` or [] if ``mcp/`` is absent.
731
+
732
+ When ``mcp/`` exists, all three platform files must be present and declare the same
733
+ server ``name`` set.
734
+ """
735
+ mcp_dir = extsrc_dir / "mcp"
736
+ if not mcp_dir.is_dir():
737
+ return []
738
+ paths = list(extension_mcp_platform_json_paths(extsrc_dir))
739
+ if not all(p.is_file() for p in paths):
740
+ missing = [p.name for p in paths if not p.is_file()]
741
+ raise SpawnError(
742
+ f"extsrc/mcp layout incomplete (missing {', '.join(missing)}) under {mcp_dir}"
743
+ )
744
+ name_sets: list[frozenset[str]] = []
745
+ for p in paths:
746
+ nm = normalized_mcp_from_mcp_json_path(p, "_staging")
747
+ name_sets.append(frozenset(s.name for s in nm.servers))
748
+ if len(set(name_sets)) != 1:
749
+ raise SpawnError(
750
+ "MCP server names must be identical across mcp/windows.json, linux.json, macos.json"
751
+ )
752
+ return sorted(name_sets[0])
753
+
754
+
693
755
  def get_navigation_metadata(target_root: Path, extension: str) -> dict:
694
756
  return {
695
757
  "required": [x.model_dump(by_alias=True) for x in get_required_read_global(target_root, extension)],
@@ -1205,14 +1267,18 @@ __all__ = [
1205
1267
  "get_required_read_global_all",
1206
1268
  "get_skill_raw_info",
1207
1269
  "init",
1270
+ "extsrc_mcp_server_names_for_staging",
1271
+ "extension_mcp_platform_json_paths",
1208
1272
  "list_extensions",
1209
1273
  "list_ides",
1210
1274
  "list_mcp",
1211
1275
  "list_skills",
1276
+ "mcp_host_platform_stem",
1212
1277
  "sync_core_config_from_defaults",
1213
1278
  "merge_core_and_extension_agent_ignore",
1214
1279
  "METADATA_TEMP_MAX_AGE_SECONDS",
1215
1280
  "normalize_skill_name",
1281
+ "normalized_mcp_from_mcp_json_path",
1216
1282
  "prune_metadata_temp",
1217
1283
  "push_to_global_gitignore",
1218
1284
  "remove_from_global_gitignore",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.13
3
+ Version: 0.1.14
4
4
  Summary: A minimal installable Python CLI package.
5
5
  License: MIT
6
6
  Classifier: License :: OSI Approved :: MIT License
File without changes
File without changes
File without changes
File without changes