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.
- {spawn_cli-0.1.13/src/spawn_cli.egg-info → spawn_cli-0.1.14}/PKG-INFO +1 -1
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/pyproject.toml +1 -1
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/cli.py +1 -1
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/download.py +1 -10
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/high_level.py +54 -8
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/low_level.py +69 -3
- {spawn_cli-0.1.13 → spawn_cli-0.1.14/src/spawn_cli.egg-info}/PKG-INFO +1 -1
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/LICENSE +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/README.md +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/setup.cfg +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/__init__.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/__init__.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/errors.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/core/scripts.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/errors.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/__init__.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/_helpers.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/_stub.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/_vacancy.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/claude_code.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/codex.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/cursor.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/gemini_cli.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/github_copilot.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/registry.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/ide/windsurf.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/__init__.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/json_io.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/lock.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/paths.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/text_io.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/toml_io.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/io/yaml_io.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/__init__.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/config.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/mcp.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/metadata.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/navigation.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/models/skill.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/resources/__init__.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/resources/default_core_config.yaml +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli/warnings_display.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/SOURCES.txt +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/entry_points.txt +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/requires.txt +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/src/spawn_cli.egg-info/top_level.txt +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/tests/test_cli.py +0 -0
- {spawn_cli-0.1.13 → spawn_cli-0.1.14}/tests/test_warnings_display.py +0 -0
|
@@ -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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
479
|
-
if
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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)
|
|
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
|
|
662
|
-
|
|
663
|
-
|
|
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",
|
|
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
|