spawn-cli 0.1.1__tar.gz → 0.1.3__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.1 → spawn_cli-0.1.3}/PKG-INFO +1 -1
  2. spawn_cli-0.1.3/README.md +114 -0
  3. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/pyproject.toml +1 -1
  4. spawn_cli-0.1.3/src/spawn_cli/cli.py +454 -0
  5. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/core/low_level.py +0 -5
  6. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/_stub.py +1 -12
  7. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/registry.py +0 -1
  8. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli.egg-info/PKG-INFO +1 -1
  9. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli.egg-info/SOURCES.txt +1 -0
  10. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/tests/test_cli.py +9 -0
  11. spawn_cli-0.1.1/src/spawn_cli/cli.py +0 -243
  12. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/setup.cfg +0 -0
  13. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/__init__.py +0 -0
  14. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/core/__init__.py +0 -0
  15. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/core/download.py +0 -0
  16. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/core/errors.py +0 -0
  17. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/core/high_level.py +0 -0
  18. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/core/scripts.py +0 -0
  19. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/errors.py +0 -0
  20. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/__init__.py +0 -0
  21. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/_helpers.py +0 -0
  22. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/_vacancy.py +0 -0
  23. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/claude_code.py +0 -0
  24. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/codex.py +0 -0
  25. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/cursor.py +0 -0
  26. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/gemini_cli.py +0 -0
  27. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/github_copilot.py +0 -0
  28. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/ide/windsurf.py +0 -0
  29. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/__init__.py +0 -0
  30. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/json_io.py +0 -0
  31. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/lock.py +0 -0
  32. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/paths.py +0 -0
  33. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/text_io.py +0 -0
  34. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/toml_io.py +0 -0
  35. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/io/yaml_io.py +0 -0
  36. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/models/__init__.py +0 -0
  37. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/models/config.py +0 -0
  38. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/models/mcp.py +0 -0
  39. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/models/metadata.py +0 -0
  40. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/models/navigation.py +0 -0
  41. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/models/skill.py +0 -0
  42. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/resources/__init__.py +0 -0
  43. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/resources/default_core_config.yaml +0 -0
  44. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli/warnings_display.py +0 -0
  45. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli.egg-info/dependency_links.txt +0 -0
  46. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli.egg-info/entry_points.txt +0 -0
  47. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli.egg-info/requires.txt +0 -0
  48. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/src/spawn_cli.egg-info/top_level.txt +0 -0
  49. {spawn_cli-0.1.1 → spawn_cli-0.1.3}/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.1
3
+ Version: 0.1.3
4
4
  Summary: A minimal installable Python CLI package.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pydantic>=2.0
@@ -0,0 +1,114 @@
1
+ # spawn-cli
2
+
3
+ Installable CLI **`spawn`** for initializing and managing Spawn metadata (extensions, IDE adapters, navigation) in a repository.
4
+
5
+ ## Quick start (uv)
6
+
7
+ Install `spawn` on your `PATH` (persistent **uv** tool environment):
8
+
9
+ ```bash
10
+ uv tool install spawn-cli && spawn --help
11
+ ```
12
+
13
+ Run once without installing a tool (ephemeral **uvx**):
14
+
15
+ ```bash
16
+ uvx --from spawn-cli spawn --help
17
+ ```
18
+
19
+ ## Upgrade (uv)
20
+
21
+ Upgrade the persistent tool:
22
+
23
+ ```bash
24
+ uv tool upgrade spawn-cli && spawn --help
25
+ ```
26
+
27
+ Latest resolver result for a one-shot run:
28
+
29
+ ```bash
30
+ uvx --refresh --from spawn-cli spawn --help
31
+ ```
32
+
33
+ ---
34
+
35
+ - **Python:** 3.10+ (see `pyproject.toml`).
36
+ - **Usage:** run from the **root of the repository** you configure; most commands need `spawn init` first.
37
+
38
+ ## Install
39
+
40
+ The PyPI package name is **`spawn-cli`**; the console script is **`spawn`**.
41
+
42
+ ### From PyPI
43
+
44
+ ```bash
45
+ pip install spawn-cli
46
+ spawn --help
47
+ ```
48
+
49
+ Isolated install:
50
+
51
+ ```bash
52
+ pipx install spawn-cli
53
+ spawn --help
54
+ ```
55
+
56
+ With **uv** (tool on `PATH`):
57
+
58
+ ```bash
59
+ uv tool install spawn-cli
60
+ spawn --help
61
+ ```
62
+
63
+ One-shot without installing into an environment:
64
+
65
+ ```bash
66
+ uvx --from spawn-cli spawn --help
67
+ ```
68
+
69
+ ### From a local checkout
70
+
71
+ ```bash
72
+ cd /path/to/spawn-cli
73
+ uv sync
74
+ uv run spawn --help
75
+ ```
76
+
77
+ Or editable install with **pip**:
78
+
79
+ ```bash
80
+ cd /path/to/spawn-cli
81
+ pip install -e .
82
+ spawn --help
83
+ ```
84
+
85
+ ## Upgrade
86
+
87
+ Use the same installer you used for the initial install. **If you use uv only, see [Upgrade (uv)](#upgrade-uv) at the top.**
88
+
89
+ **pip** (current environment):
90
+
91
+ ```bash
92
+ pip install --upgrade spawn-cli
93
+ spawn --help
94
+ ```
95
+
96
+ **pipx**:
97
+
98
+ ```bash
99
+ pipx upgrade spawn-cli
100
+ spawn --help
101
+ ```
102
+
103
+ **Local checkout:** pull changes, then reinstall so the editable install sees updates:
104
+
105
+ ```bash
106
+ cd /path/to/spawn-cli
107
+ git pull
108
+ uv sync
109
+ # or: pip install -e .
110
+ ```
111
+
112
+ ## More
113
+
114
+ Design and command details: `spec/design/user-guide.md` and `spec/design/utility.md`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spawn-cli"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "A minimal installable Python CLI package."
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -0,0 +1,454 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from spawn_cli import __version__
8
+ from spawn_cli.core import download as dl
9
+ from spawn_cli.core import high_level as hl
10
+ from spawn_cli.core import low_level as ll
11
+ from spawn_cli.core.errors import SpawnError
12
+ from spawn_cli.ide.registry import DetectResult, detect_supported_ides
13
+ from spawn_cli.io.lock import spawn_lock
14
+ from spawn_cli.warnings_display import install_spawn_warning_format
15
+
16
+
17
+ def _require_init(target_root: Path) -> None:
18
+ if not (target_root / "spawn").is_dir():
19
+ raise SpawnError("need init before: run spawn init in this repository.")
20
+
21
+
22
+ def build_parser() -> argparse.ArgumentParser:
23
+ parser = argparse.ArgumentParser(
24
+ prog="spawn",
25
+ description="Spawn CLI: manage Spawn extensions and IDE adapters in this repository.",
26
+ )
27
+ parser.add_argument(
28
+ "--version",
29
+ action="version",
30
+ version=f"spawn {__version__}",
31
+ )
32
+ sub = parser.add_subparsers(dest="command", required=True)
33
+
34
+ sub.add_parser(
35
+ "init",
36
+ help="Initialize spawn/ in the current repository",
37
+ description=(
38
+ "Create the spawn/ tree in the repository root "
39
+ "(config, metadata layout, navigation stub)."
40
+ ),
41
+ )
42
+
43
+ rules_p = sub.add_parser(
44
+ "rules",
45
+ help="Rebuild navigation mandatory reads from spawn/rules/",
46
+ description="Refresh navigation.yaml from files under spawn/rules/.",
47
+ )
48
+ rules_sub = rules_p.add_subparsers(dest="rules_command", required=True)
49
+ rules_sub.add_parser(
50
+ "refresh",
51
+ help="Sync spawn/rules/ into navigation.yaml",
52
+ description=(
53
+ "Scan spawn/rules/ and rewrite read-required paths in spawn/navigation.yaml."
54
+ ),
55
+ )
56
+
57
+ ide_p = sub.add_parser(
58
+ "ide",
59
+ help="Add, remove, or list IDE adapters registered for this repo",
60
+ description=(
61
+ "Control which IDE keys Spawn configures (rendered metadata under spawn/.metadata/)."
62
+ ),
63
+ )
64
+ ide_sub = ide_p.add_subparsers(dest="ide_command", required=True)
65
+
66
+ _ide_add = ide_sub.add_parser(
67
+ "add",
68
+ help="Install metadata rendering for IDE key(s)",
69
+ description="Install metadata rendering for one or more IDE keys (cursor, codex, ...).",
70
+ )
71
+ _ide_add.add_argument(
72
+ "ides",
73
+ nargs="+",
74
+ metavar="IDE",
75
+ help="IDE identifiers to add (canonical keys from Spawn).",
76
+ )
77
+
78
+ _ide_remove = ide_sub.add_parser(
79
+ "remove",
80
+ help="Unregister IDE key(s) and clean vacancy where supported",
81
+ description="Unregister IDE keys and drop rendered outputs when the adapter defines cleanup.",
82
+ )
83
+ _ide_remove.add_argument(
84
+ "ides",
85
+ nargs="+",
86
+ metavar="IDE",
87
+ help="IDE identifiers to remove.",
88
+ )
89
+
90
+ ide_sub.add_parser(
91
+ "list",
92
+ help="Print registered IDE identifiers for this repository",
93
+ description="Print one IDE identifier per line from the Spawn registry.",
94
+ )
95
+ ide_sub.add_parser(
96
+ "list-supported-ides",
97
+ help="Print YAML map of IDE keys to detected capabilities",
98
+ description=(
99
+ "Probe installed IDE adapters on this machine; emit YAML suited for tooling."
100
+ ),
101
+ )
102
+
103
+ ext_p = sub.add_parser(
104
+ "extension",
105
+ help="Install, scaffold, upgrade, validate, or list Spawn extensions",
106
+ description=(
107
+ "Manage Spawn extensions under spawn/.extend/ (sources, reinstall, scaffold, checks)."
108
+ ),
109
+ )
110
+ ext_sub = ext_p.add_subparsers(dest="ext_command", required=True)
111
+
112
+ _ext_add = ext_sub.add_parser(
113
+ "add",
114
+ help="Fetch and register an extension (git archive, ZIP, local path)",
115
+ description="Download or copy an extension into spawn/.extend/ and refresh renders.",
116
+ )
117
+ _ext_add.add_argument(
118
+ "path",
119
+ metavar="SOURCE",
120
+ help="Extension source URL, git ref, ZIP path, or local directory/archive.",
121
+ )
122
+ _ext_add.add_argument(
123
+ "--branch",
124
+ default=None,
125
+ metavar="REV",
126
+ help="Git revision to use when the source is a Git URL (ignored for non-git sources).",
127
+ )
128
+
129
+ _ext_update = ext_sub.add_parser(
130
+ "update",
131
+ help="Pull or resync extension content from recorded source.yaml",
132
+ description="Upgrade an installed extension to its configured upstream revision.",
133
+ )
134
+ _ext_update.add_argument(
135
+ "extension_name",
136
+ metavar="NAME",
137
+ help="Installed extension name (directory spawn/.extend/<name>/).",
138
+ )
139
+
140
+ _ext_remove = ext_sub.add_parser(
141
+ "remove",
142
+ help="Unregister and delete an extension from this repo",
143
+ description="Remove spawn/.extend/<name>/ entry and unload extension-side scripts.",
144
+ )
145
+ _ext_remove.add_argument(
146
+ "extension_name",
147
+ metavar="NAME",
148
+ help="Installed extension name to remove.",
149
+ )
150
+
151
+ _ext_reinstall = ext_sub.add_parser(
152
+ "reinstall",
153
+ help="Re-download extension from spawn/.extend/<name>/source.yaml",
154
+ description="Staging reinstall from cached source.yaml (recovery after corrupted tree).",
155
+ )
156
+ _ext_reinstall.add_argument(
157
+ "extension_name",
158
+ metavar="NAME",
159
+ help="Installed extension name to reinstall.",
160
+ )
161
+
162
+ ext_sub.add_parser(
163
+ "list",
164
+ help="Print installed extension names",
165
+ description="Emit one extension name per line.",
166
+ )
167
+
168
+ _ext_init = ext_sub.add_parser(
169
+ "init",
170
+ help="Create a Spawn extension scaffold from a filesystem path",
171
+ description="Lay out skeleton files for a new extension under the chosen directory.",
172
+ )
173
+ _ext_init.add_argument(
174
+ "path",
175
+ nargs="?",
176
+ default=".",
177
+ metavar="PATH",
178
+ help="Directory receiving the scaffold (defaults to '.').",
179
+ )
180
+ _ext_init.add_argument(
181
+ "--name",
182
+ required=True,
183
+ metavar="ID",
184
+ help="Canonical extension id used in spawn/.extend/<id>/ when installed.",
185
+ )
186
+
187
+ _ext_check = ext_sub.add_parser(
188
+ "check",
189
+ help="Lint extension workspace against authoring rules",
190
+ description="Warn on missing declares, dangling paths, skill layout issues.",
191
+ )
192
+ _ext_check.add_argument(
193
+ "path",
194
+ nargs="?",
195
+ default=".",
196
+ metavar="PATH",
197
+ help="Extension project root directory to validate.",
198
+ )
199
+ _ext_check.add_argument(
200
+ "--strict",
201
+ action="store_true",
202
+ help=(
203
+ "Fail on issues that default to warnings "
204
+ "(missing skill files, bad mcp.json, empty file descriptions)."
205
+ ),
206
+ )
207
+
208
+ _ext_from_rules = ext_sub.add_parser(
209
+ "from-rules",
210
+ help="Compose an extension scaffold from YAML rules-derived sources",
211
+ description="Hydrate packaged rules into a scaffold under --output.",
212
+ )
213
+ _ext_from_rules.add_argument(
214
+ "source",
215
+ metavar="SOURCE",
216
+ help=(
217
+ "Repository root containing spawn/rules (local dir), "
218
+ "or remote URL/Git ref staged before import."
219
+ ),
220
+ )
221
+ _ext_from_rules.add_argument(
222
+ "--name",
223
+ required=True,
224
+ metavar="ID",
225
+ help="Canonical extension id for the generated scaffold.",
226
+ )
227
+ _ext_from_rules.add_argument(
228
+ "--branch",
229
+ default=None,
230
+ metavar="REV",
231
+ help="Revision for Git-backed sources referenced by SOURCE.",
232
+ )
233
+ _ext_from_rules.add_argument(
234
+ "--output",
235
+ default=".",
236
+ metavar="DIR",
237
+ help="Existing directory receiving new extension files.",
238
+ )
239
+
240
+ _ext_health = ext_sub.add_parser(
241
+ "healthcheck",
242
+ help="Verify installed extension manifests and renders",
243
+ description="Smoke-test extension installs (non-zero exit on failure).",
244
+ )
245
+ _ext_health.add_argument(
246
+ "extension_name",
247
+ metavar="NAME",
248
+ help="Installed extension name to diagnose.",
249
+ )
250
+
251
+ build_p = sub.add_parser(
252
+ "build",
253
+ help="Install extensions listed in an aggregate build YAML manifest",
254
+ description="Batch installs driven by YAML enumerating URLs or paths.",
255
+ )
256
+ build_sub = build_p.add_subparsers(dest="build_command", required=True)
257
+
258
+ _build_install = build_sub.add_parser(
259
+ "install",
260
+ help="Resolve manifest entries and spawn extension add each",
261
+ description="Iterate manifest entries calling install-extension logic serially.",
262
+ )
263
+ _build_install.add_argument(
264
+ "path",
265
+ metavar="MANIFEST",
266
+ help="YAML file listing extensions to install.",
267
+ )
268
+ _build_install.add_argument(
269
+ "--branch",
270
+ default=None,
271
+ metavar="REV",
272
+ help="Fallback Git revision for entries omitting explicit branch.",
273
+ )
274
+
275
+ _build_list = build_sub.add_parser(
276
+ "list",
277
+ help="Dry-run: print resolved manifest entries without installing",
278
+ description="Normalize manifest and emit YAML of resolved extension specs.",
279
+ )
280
+ _build_list.add_argument(
281
+ "path",
282
+ metavar="MANIFEST",
283
+ help="YAML aggregate manifest describing extension sources.",
284
+ )
285
+ _build_list.add_argument(
286
+ "--branch",
287
+ default=None,
288
+ metavar="REV",
289
+ help="Fallback Git revision applied when entries lack branch.",
290
+ )
291
+
292
+ return parser
293
+
294
+
295
+ def main(argv: list[str] | None = None) -> int:
296
+ install_spawn_warning_format()
297
+ parser = build_parser()
298
+ args = parser.parse_args(argv)
299
+ target_root = Path.cwd().resolve()
300
+
301
+ try:
302
+ return _dispatch(args, target_root)
303
+ except SpawnError as e:
304
+ print(f"Error: {e}", file=sys.stderr)
305
+ return 1
306
+ except Exception as e:
307
+ print(f"Unexpected error: {e}", file=sys.stderr)
308
+ return 2
309
+
310
+
311
+ def _dispatch(args: argparse.Namespace, target_root: Path) -> int:
312
+ cmd = args.command
313
+
314
+ if cmd == "init":
315
+ with spawn_lock(target_root):
316
+ ll.init(target_root)
317
+ return 0
318
+
319
+ _require_init(target_root)
320
+
321
+ with spawn_lock(target_root):
322
+ if cmd == "rules":
323
+ return _dispatch_rules(args, target_root)
324
+ if cmd == "ide":
325
+ return _dispatch_ide(args, target_root)
326
+ if cmd == "extension":
327
+ return _dispatch_extension(args, target_root)
328
+ if cmd == "build":
329
+ return _dispatch_build(args, target_root)
330
+
331
+ return 1
332
+
333
+
334
+ def _dispatch_rules(args: argparse.Namespace, target_root: Path) -> int:
335
+ sub = args.rules_command
336
+ if sub == "refresh":
337
+ hl.refresh_rules_navigation(target_root)
338
+ return 0
339
+ return 1
340
+
341
+
342
+ def _dispatch_ide(args: argparse.Namespace, target_root: Path) -> int:
343
+ sub = args.ide_command
344
+
345
+ if sub == "list-supported-ides":
346
+ results = detect_supported_ides(target_root)
347
+ _print_yaml(results)
348
+ return 0
349
+
350
+ if sub == "list":
351
+ ides = ll.list_ides(target_root)
352
+ for ide in ides:
353
+ print(ide)
354
+ return 0
355
+
356
+ if sub == "add":
357
+ for ide in args.ides:
358
+ hl.add_ide(target_root, ide)
359
+ return 0
360
+
361
+ if sub == "remove":
362
+ for ide in args.ides:
363
+ hl.remove_ide(target_root, ide)
364
+ return 0
365
+
366
+ return 1
367
+
368
+
369
+ def _dispatch_extension(args: argparse.Namespace, target_root: Path) -> int:
370
+ sub = args.ext_command
371
+
372
+ if sub == "list":
373
+ for ext in ll.list_extensions(target_root):
374
+ print(ext)
375
+ return 0
376
+
377
+ if sub == "add":
378
+ dl.install_extension(target_root, args.path, args.branch)
379
+ return 0
380
+
381
+ if sub == "update":
382
+ hl.update_extension(target_root, args.extension_name)
383
+ return 0
384
+
385
+ if sub == "remove":
386
+ hl.remove_extension(target_root, args.extension_name)
387
+ return 0
388
+
389
+ if sub == "reinstall":
390
+ hl.reinstall_extension(target_root, args.extension_name)
391
+ return 0
392
+
393
+ if sub == "init":
394
+ hl.extension_init(Path(args.path).resolve(), args.name)
395
+ return 0
396
+
397
+ if sub == "check":
398
+ warn_list = hl.extension_check(Path(args.path).resolve(), strict=args.strict)
399
+ for w in warn_list:
400
+ print(f"Warning: {w}")
401
+ return 0
402
+
403
+ if sub == "from-rules":
404
+ hl.extension_from_rules(
405
+ args.source,
406
+ Path(args.output).resolve(),
407
+ args.name,
408
+ args.branch,
409
+ )
410
+ return 0
411
+
412
+ if sub == "healthcheck":
413
+ ok = hl.extension_healthcheck(target_root, args.extension_name)
414
+ return 0 if ok else 1
415
+
416
+ return 1
417
+
418
+
419
+ def _dispatch_build(args: argparse.Namespace, target_root: Path) -> int:
420
+ sub = args.build_command
421
+
422
+ if sub == "list":
423
+ exts = dl.list_build_extensions(args.path, args.branch)
424
+ _print_yaml(exts)
425
+ return 0
426
+
427
+ if sub == "install":
428
+ dl.install_build(target_root, args.path, args.branch)
429
+ return 0
430
+
431
+ return 1
432
+
433
+
434
+ def _print_yaml(data: object) -> None:
435
+ """Serialize data to YAML. DetectResult dataclass values use utility.md output shape."""
436
+ from ruamel.yaml import YAML
437
+
438
+ from spawn_cli.io.yaml_io import configure_yaml_dump
439
+
440
+ def _to_serializable(obj: object) -> object:
441
+ if isinstance(obj, DetectResult):
442
+ return {
443
+ "used-in-repo": obj.used_in_repo,
444
+ "capabilities": obj.capabilities.to_dict(),
445
+ }
446
+ if isinstance(obj, dict):
447
+ return {k: _to_serializable(v) for k, v in obj.items()}
448
+ if isinstance(obj, list):
449
+ return [_to_serializable(i) for i in obj]
450
+ return obj
451
+
452
+ yaml = YAML()
453
+ configure_yaml_dump(yaml)
454
+ yaml.dump(_to_serializable(data), sys.stdout)
@@ -25,15 +25,10 @@ from spawn_cli.models.skill import SkillFileRef, SkillMetadata, SkillRawInfo
25
25
  CANONICAL_IDE_KEYS: tuple[str, ...] = (
26
26
  "cursor",
27
27
  "codex",
28
- "qoder",
29
28
  "claude-code",
30
- "qwen-code",
31
29
  "windsurf",
32
30
  "github-copilot",
33
- "aider",
34
- "zed",
35
31
  "gemini-cli",
36
- "devin",
37
32
  )
38
33
 
39
34
  SPAWN_GITIGNORE_START = "# spawn:start"
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import warnings
4
4
  from pathlib import Path
5
5
 
6
- from .registry import DetectResult, IdeAdapter, IdeCapabilities, register
6
+ from .registry import DetectResult, IdeAdapter, IdeCapabilities
7
7
  from spawn_cli.models.mcp import NormalizedMcp
8
8
  from spawn_cli.models.skill import SkillMetadata
9
9
 
@@ -49,15 +49,4 @@ class StubAdapter(IdeAdapter):
49
49
  return ""
50
50
 
51
51
 
52
- _STUBS: list[tuple[str, IdeCapabilities]] = [
53
- ("qoder", IdeCapabilities("native", "project", "project", "agents-md")),
54
- ("qwen-code", IdeCapabilities("native", "project", "project", "qwen-md")),
55
- ("aider", IdeCapabilities("entry-only", "unsupported", "project", "conventions-md")),
56
- ("zed", IdeCapabilities("unsupported", "project", "unsupported", "agents-md")),
57
- ("devin", IdeCapabilities("native", "project", "unsupported", "agents-md")),
58
- ]
59
-
60
- for _key, _caps in _STUBS:
61
- register(StubAdapter(_key, _caps))
62
-
63
52
  __all__ = ["StubAdapter"]
@@ -86,7 +86,6 @@ class IdeAdapter(ABC):
86
86
 
87
87
  ALIASES: dict[str, str] = {
88
88
  "claude": "claude-code",
89
- "qwen": "qwen-code",
90
89
  "gemini": "gemini-cli",
91
90
  "copilot": "github-copilot",
92
91
  "github": "github-copilot",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spawn-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: A minimal installable Python CLI package.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: pydantic>=2.0
@@ -1,3 +1,4 @@
1
+ README.md
1
2
  pyproject.toml
2
3
  src/spawn_cli/__init__.py
3
4
  src/spawn_cli/cli.py
@@ -23,6 +23,15 @@ def test_spawn_help(capsys):
23
23
  assert "spawn" in capsys.readouterr().out
24
24
 
25
25
 
26
+ def test_spawn_extension_add_help_shows_argument_descriptions(capsys):
27
+ with pytest.raises(SystemExit) as exc:
28
+ main(["extension", "add", "--help"])
29
+ assert exc.value.code == 0
30
+ out = capsys.readouterr().out
31
+ assert "--branch" in out
32
+ assert "Git" in out or "git" in out
33
+
34
+
26
35
  def test_spawn_init(tmp_path, monkeypatch):
27
36
  monkeypatch.chdir(tmp_path)
28
37
  root = tmp_path.resolve()
@@ -1,243 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import argparse
4
- import sys
5
- from pathlib import Path
6
-
7
- from spawn_cli import __version__
8
- from spawn_cli.core import download as dl
9
- from spawn_cli.core import high_level as hl
10
- from spawn_cli.core import low_level as ll
11
- from spawn_cli.core.errors import SpawnError
12
- from spawn_cli.ide.registry import DetectResult, detect_supported_ides
13
- from spawn_cli.io.lock import spawn_lock
14
- from spawn_cli.warnings_display import install_spawn_warning_format
15
-
16
-
17
- def _require_init(target_root: Path) -> None:
18
- if not (target_root / "spawn").is_dir():
19
- raise SpawnError("need init before: run spawn init in this repository.")
20
-
21
-
22
- def build_parser() -> argparse.ArgumentParser:
23
- parser = argparse.ArgumentParser(prog="spawn", description="Spawn CLI")
24
- parser.add_argument(
25
- "--version",
26
- action="version",
27
- version=f"spawn {__version__}",
28
- )
29
- sub = parser.add_subparsers(dest="command", required=True)
30
-
31
- sub.add_parser("init", help="Initialize spawn/ in the current repository")
32
-
33
- rules_p = sub.add_parser("rules")
34
- rules_sub = rules_p.add_subparsers(dest="rules_command", required=True)
35
- rules_sub.add_parser("refresh", help="Sync spawn/rules/ into navigation.yaml")
36
-
37
- ide_p = sub.add_parser("ide")
38
- ide_sub = ide_p.add_subparsers(dest="ide_command", required=True)
39
- _ide_add = ide_sub.add_parser("add")
40
- _ide_add.add_argument("ides", nargs="+")
41
- _ide_remove = ide_sub.add_parser("remove")
42
- _ide_remove.add_argument("ides", nargs="+")
43
- ide_sub.add_parser("list")
44
- ide_sub.add_parser("list-supported-ides")
45
-
46
- ext_p = sub.add_parser("extension")
47
- ext_sub = ext_p.add_subparsers(dest="ext_command", required=True)
48
- _ext_add = ext_sub.add_parser("add")
49
- _ext_add.add_argument("path")
50
- _ext_add.add_argument("--branch", default=None)
51
- _ext_update = ext_sub.add_parser("update")
52
- _ext_update.add_argument("extension_name")
53
- _ext_remove = ext_sub.add_parser("remove")
54
- _ext_remove.add_argument("extension_name")
55
- _ext_reinstall = ext_sub.add_parser("reinstall")
56
- _ext_reinstall.add_argument("extension_name")
57
- ext_sub.add_parser("list")
58
- _ext_init = ext_sub.add_parser("init")
59
- _ext_init.add_argument("path", nargs="?", default=".")
60
- _ext_init.add_argument("--name", required=True)
61
- _ext_check = ext_sub.add_parser("check")
62
- _ext_check.add_argument("path", nargs="?", default=".")
63
- _ext_check.add_argument("--strict", action="store_true")
64
- _ext_from_rules = ext_sub.add_parser("from-rules")
65
- _ext_from_rules.add_argument("source")
66
- _ext_from_rules.add_argument("--name", required=True)
67
- _ext_from_rules.add_argument("--branch", default=None)
68
- _ext_from_rules.add_argument("--output", default=".")
69
- _ext_health = ext_sub.add_parser("healthcheck")
70
- _ext_health.add_argument("extension_name")
71
-
72
- build_p = sub.add_parser("build")
73
- build_sub = build_p.add_subparsers(dest="build_command", required=True)
74
- _build_install = build_sub.add_parser("install")
75
- _build_install.add_argument("path")
76
- _build_install.add_argument("--branch", default=None)
77
- _build_list = build_sub.add_parser("list")
78
- _build_list.add_argument("path")
79
- _build_list.add_argument("--branch", default=None)
80
-
81
- return parser
82
-
83
-
84
- def main(argv: list[str] | None = None) -> int:
85
- install_spawn_warning_format()
86
- parser = build_parser()
87
- args = parser.parse_args(argv)
88
- target_root = Path.cwd().resolve()
89
-
90
- try:
91
- return _dispatch(args, target_root)
92
- except SpawnError as e:
93
- print(f"Error: {e}", file=sys.stderr)
94
- return 1
95
- except Exception as e:
96
- print(f"Unexpected error: {e}", file=sys.stderr)
97
- return 2
98
-
99
-
100
- def _dispatch(args: argparse.Namespace, target_root: Path) -> int:
101
- cmd = args.command
102
-
103
- if cmd == "init":
104
- with spawn_lock(target_root):
105
- ll.init(target_root)
106
- return 0
107
-
108
- _require_init(target_root)
109
-
110
- with spawn_lock(target_root):
111
- if cmd == "rules":
112
- return _dispatch_rules(args, target_root)
113
- if cmd == "ide":
114
- return _dispatch_ide(args, target_root)
115
- if cmd == "extension":
116
- return _dispatch_extension(args, target_root)
117
- if cmd == "build":
118
- return _dispatch_build(args, target_root)
119
-
120
- return 1
121
-
122
-
123
- def _dispatch_rules(args: argparse.Namespace, target_root: Path) -> int:
124
- sub = args.rules_command
125
- if sub == "refresh":
126
- hl.refresh_rules_navigation(target_root)
127
- return 0
128
- return 1
129
-
130
-
131
- def _dispatch_ide(args: argparse.Namespace, target_root: Path) -> int:
132
- sub = args.ide_command
133
-
134
- if sub == "list-supported-ides":
135
- results = detect_supported_ides(target_root)
136
- _print_yaml(results)
137
- return 0
138
-
139
- if sub == "list":
140
- ides = ll.list_ides(target_root)
141
- for ide in ides:
142
- print(ide)
143
- return 0
144
-
145
- if sub == "add":
146
- for ide in args.ides:
147
- hl.add_ide(target_root, ide)
148
- return 0
149
-
150
- if sub == "remove":
151
- for ide in args.ides:
152
- hl.remove_ide(target_root, ide)
153
- return 0
154
-
155
- return 1
156
-
157
-
158
- def _dispatch_extension(args: argparse.Namespace, target_root: Path) -> int:
159
- sub = args.ext_command
160
-
161
- if sub == "list":
162
- for ext in ll.list_extensions(target_root):
163
- print(ext)
164
- return 0
165
-
166
- if sub == "add":
167
- dl.install_extension(target_root, args.path, args.branch)
168
- return 0
169
-
170
- if sub == "update":
171
- hl.update_extension(target_root, args.extension_name)
172
- return 0
173
-
174
- if sub == "remove":
175
- hl.remove_extension(target_root, args.extension_name)
176
- return 0
177
-
178
- if sub == "reinstall":
179
- hl.reinstall_extension(target_root, args.extension_name)
180
- return 0
181
-
182
- if sub == "init":
183
- hl.extension_init(Path(args.path).resolve(), args.name)
184
- return 0
185
-
186
- if sub == "check":
187
- warn_list = hl.extension_check(Path(args.path).resolve(), strict=args.strict)
188
- for w in warn_list:
189
- print(f"Warning: {w}")
190
- return 0
191
-
192
- if sub == "from-rules":
193
- hl.extension_from_rules(
194
- args.source,
195
- Path(args.output).resolve(),
196
- args.name,
197
- args.branch,
198
- )
199
- return 0
200
-
201
- if sub == "healthcheck":
202
- ok = hl.extension_healthcheck(target_root, args.extension_name)
203
- return 0 if ok else 1
204
-
205
- return 1
206
-
207
-
208
- def _dispatch_build(args: argparse.Namespace, target_root: Path) -> int:
209
- sub = args.build_command
210
-
211
- if sub == "list":
212
- exts = dl.list_build_extensions(args.path, args.branch)
213
- _print_yaml(exts)
214
- return 0
215
-
216
- if sub == "install":
217
- dl.install_build(target_root, args.path, args.branch)
218
- return 0
219
-
220
- return 1
221
-
222
-
223
- def _print_yaml(data: object) -> None:
224
- """Serialize data to YAML. DetectResult dataclass values use utility.md output shape."""
225
- from ruamel.yaml import YAML
226
-
227
- from spawn_cli.io.yaml_io import configure_yaml_dump
228
-
229
- def _to_serializable(obj: object) -> object:
230
- if isinstance(obj, DetectResult):
231
- return {
232
- "used-in-repo": obj.used_in_repo,
233
- "capabilities": obj.capabilities.to_dict(),
234
- }
235
- if isinstance(obj, dict):
236
- return {k: _to_serializable(v) for k, v in obj.items()}
237
- if isinstance(obj, list):
238
- return [_to_serializable(i) for i in obj]
239
- return obj
240
-
241
- yaml = YAML()
242
- configure_yaml_dump(yaml)
243
- yaml.dump(_to_serializable(data), sys.stdout)
File without changes