specpro-cli 0.1.0__py3-none-any.whl

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 (76) hide show
  1. specpro_cli/__init__.py +16 -0
  2. specpro_cli/assets/commands/specpro.analyze.md +1102 -0
  3. specpro_cli/assets/commands/specpro.checklist.md +335 -0
  4. specpro_cli/assets/commands/specpro.clarify.md +581 -0
  5. specpro_cli/assets/commands/specpro.constitution.md +488 -0
  6. specpro_cli/assets/commands/specpro.feature.md +115 -0
  7. specpro_cli/assets/commands/specpro.implement.md +1881 -0
  8. specpro_cli/assets/commands/specpro.manual-test.md +206 -0
  9. specpro_cli/assets/commands/specpro.plan.md +3284 -0
  10. specpro_cli/assets/commands/specpro.qc.md +1489 -0
  11. specpro_cli/assets/commands/specpro.scenarios.md +154 -0
  12. specpro_cli/assets/commands/specpro.specify.md +1449 -0
  13. specpro_cli/assets/commands/specpro.status.md +863 -0
  14. specpro_cli/assets/commands/specpro.tasks.md +1207 -0
  15. specpro_cli/assets/commands/specpro.test-implement.md +462 -0
  16. specpro_cli/assets/commands/specpro.test-plan.md +383 -0
  17. specpro_cli/assets/commands/specpro.user-manual.md +178 -0
  18. specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
  19. specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
  20. specpro_cli/assets/scripts/bash/common.sh +88 -0
  21. specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
  22. specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
  23. specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
  24. specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
  25. specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
  26. specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
  27. specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
  28. specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
  29. specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
  30. specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
  31. specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
  32. specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
  33. specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
  34. specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
  35. specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
  36. specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
  37. specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
  38. specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
  39. specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
  40. specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
  41. specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
  42. specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
  43. specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
  44. specpro_cli/assets/templates/agent-context-template.md +49 -0
  45. specpro_cli/assets/templates/assumptions-template.md +248 -0
  46. specpro_cli/assets/templates/checklist-template.md +40 -0
  47. specpro_cli/assets/templates/clarifications-template.md +155 -0
  48. specpro_cli/assets/templates/constitution-template.md +50 -0
  49. specpro_cli/assets/templates/feature-spec-template.md +66 -0
  50. specpro_cli/assets/templates/plan-overview-template.md +150 -0
  51. specpro_cli/assets/templates/plan-template.md +387 -0
  52. specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
  53. specpro_cli/assets/templates/requirements-template.md +356 -0
  54. specpro_cli/assets/templates/spec-template.md +267 -0
  55. specpro_cli/assets/templates/tasks-template.md +252 -0
  56. specpro_cli/assets/templates/test-tasks-template.md +174 -0
  57. specpro_cli/cli/__init__.py +5 -0
  58. specpro_cli/cli/cmd_init.py +416 -0
  59. specpro_cli/cli/cmd_remove.py +122 -0
  60. specpro_cli/cli/entry.py +181 -0
  61. specpro_cli/integrations/__init__.py +36 -0
  62. specpro_cli/integrations/base.py +601 -0
  63. specpro_cli/integrations/claude/__init__.py +101 -0
  64. specpro_cli/integrations/copilot/__init__.py +153 -0
  65. specpro_cli/integrations/cursor_agent/__init__.py +51 -0
  66. specpro_cli/integrations/gemini/__init__.py +44 -0
  67. specpro_cli/integrations/opencode/__init__.py +48 -0
  68. specpro_cli/integrations/qodercli/__init__.py +54 -0
  69. specpro_cli/integrations/registry.py +88 -0
  70. specpro_cli/packaged/__init__.py +5 -0
  71. specpro_cli/packaged/sync.py +106 -0
  72. specpro_cli-0.1.0.dist-info/METADATA +117 -0
  73. specpro_cli-0.1.0.dist-info/RECORD +76 -0
  74. specpro_cli-0.1.0.dist-info/WHEEL +4 -0
  75. specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
  76. specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,122 @@
1
+ """`specpro remove` — uninstall the specpro files of ONE agent from a project.
2
+
3
+ The safety of this command lives entirely in one rule, fixed by the contract
4
+ (`install-cli.md` → `## Uninstall Contract`) and not re-argued here:
5
+
6
+ **Remove only paths derivable from the registry** — for each command template,
7
+ exactly the path `setup()` would write (`planned_paths()` is that derivation).
8
+ Three corollaries are the acceptance scenario's three clauses, and they hold by
9
+ construction, not by checking:
10
+ 1. a user's own files are not in the set, so they cannot be deleted;
11
+ 2. another agent's installed files belong to a DIFFERENT integration's set;
12
+ 3. a specpro file the user RENAMED is no longer derivable, so it is not removed —
13
+ reported as zero removals, not as a failure.
14
+
15
+ `--agent` is REQUIRED with no detection: detection reads the existence of the
16
+ layout root, and after an uninstall that directory may still be there (created by
17
+ the user or by other tooling) — using it as the removal target would report a
18
+ successful uninstall of something that was never installed.
19
+
20
+ The shared helpers (`_check_runtime`, `_abort`) are imported from `cmd_init`
21
+ rather than copied: they are the same validation block's step 1 and the same
22
+ failure shape, and a copy would drift the first time one of them is tuned.
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ from pathlib import Path
28
+
29
+ import typer
30
+
31
+ from specpro_cli.cli.cmd_init import _abort, _check_runtime
32
+ from specpro_cli.integrations import registry
33
+
34
+
35
+ def cmd_remove(
36
+ path: Path = typer.Argument(
37
+ Path("."),
38
+ help="Target project root.",
39
+ show_default=False,
40
+ ),
41
+ agent: str = typer.Option(
42
+ ..., "--agent", help="Agent key to uninstall (required — no detection)."
43
+ ),
44
+ source: Path | None = typer.Option(
45
+ None, "--source", help="Read assets from this path instead of the built-in snapshot."
46
+ ),
47
+ ) -> None:
48
+ """Remove the specpro files of one agent; never touch anything else."""
49
+ project_root = path.expanduser().resolve()
50
+
51
+ # --- pre-removal validation (contract: mirrors init's block) ------------------------
52
+ _check_runtime() # 1 FR-002 — removal runs through the same Python runtime
53
+
54
+ integration = registry.get(agent) # 2 FR-005 — unknown key names the valid set
55
+ if integration is None:
56
+ keys = registry.keys()
57
+ _abort(
58
+ f"unknown agent {agent!r}.",
59
+ f"Valid keys: {', '.join(keys) if keys else '(none registered)'}",
60
+ )
61
+
62
+ # 3 a missing layout root is SUCCESS with a statement, not an error — there is
63
+ # nothing installed for this agent, and "uninstalled zero files" is the truth.
64
+ dest_root = integration.commands_dest(project_root).parent # the layout root
65
+ if not dest_root.is_dir():
66
+ typer.echo(f"specpro remove → {project_root}")
67
+ typer.echo(f"agent: {agent} ({(integration.config or {}).get('name') or agent})")
68
+ typer.echo(f"nothing installed for {agent!r} under {dest_root}")
69
+ return
70
+
71
+ # --- removal: the registry-derived set, nothing else --------------------------------
72
+ from specpro_cli.cli.cmd_init import assets_commands_dir
73
+
74
+ # Same asset-sourcing rule as install (Uninstall Contract's `--source` row): the
75
+ # derivable path set needs the template NAME set, and a source checkout has no
76
+ # built-in snapshot to take it from.
77
+ commands_dir = assets_commands_dir(source)
78
+ if not commands_dir.is_dir():
79
+ _abort(
80
+ f"command templates not found at {commands_dir}",
81
+ "The built-in snapshot ships with the wheel; without it the derivable "
82
+ "path set cannot be computed, and guessing paths is exactly what this "
83
+ "command must never do.",
84
+ "In a source checkout, pass the repository (or its commands/ dir): "
85
+ "--source <path>",
86
+ )
87
+
88
+ dest_roots = set(integration.deploy_roots(project_root))
89
+
90
+ removed: list[Path] = []
91
+ for target in integration.planned_paths(project_root, commands_dir):
92
+ if not target.is_file():
93
+ continue # never installed (or already removed) — not in the report
94
+ target.unlink()
95
+ removed.append(target)
96
+ # Skills layout: the file sits in its own `specpro-<stem>/` directory. When
97
+ # this command's removal empties it, take the empty directory too — a
98
+ # NON-recursive rmdir, so a directory the user has put anything else into is
99
+ # left (with their files) rather than destroyed. Deploy roots themselves are
100
+ # never removed (the user's own files may live beside ours there).
101
+ parent = target.parent
102
+ if parent not in dest_roots:
103
+ try:
104
+ parent.rmdir()
105
+ except OSError:
106
+ pass # not empty — the user's files stay, silently
107
+
108
+ # --- report (contract Output Contract: same shape as install's) ---------------------
109
+ display = (integration.config or {}).get("name") or integration.key
110
+ typer.echo(f"specpro remove → {project_root}")
111
+ typer.echo(f"agent: {integration.key} ({display})")
112
+ typer.echo(f"removed: {len(removed)}")
113
+ for p in removed:
114
+ typer.echo(f" - {p}")
115
+ if not removed:
116
+ # The contract names this explicitly: zero removals against an EXISTING root
117
+ # is a real outcome (everything renamed or already gone), and it must be
118
+ # said — not folded into a silent success.
119
+ typer.echo(
120
+ f"no recognizable specpro files for {integration.key!r} "
121
+ f"(renamed files are never removed) — nothing was deleted."
122
+ )
@@ -0,0 +1,181 @@
1
+ """The `specpro` entry point — the Typer app and the registry-derived lookups.
2
+
3
+ `specs/contracts/install-cli.md` -> "入口层的职责边界" fixes what may live here:
4
+ parse arguments, validate preconditions, orchestrate, report. Two prohibitions:
5
+
6
+ - MUST NOT hardcode any agent name, path, or format — **everything is queried from the
7
+ registry**. That is what makes "add an agent = add one subclass" true (FR-003); a
8
+ single hardcoded name here would break it, and this file is where it would happen.
9
+ - MUST NOT contain format-conversion logic — that belongs to the format base classes.
10
+
11
+ `cmd_init` (the main command) is a User Story 1 deliverable and is not registered here
12
+ yet; this skeleton carries `check` and `version` only, which is what T016 specifies.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from pathlib import Path
18
+
19
+ import typer
20
+
21
+ from specpro_cli.cli import cmd_init as _cmd_init
22
+ from specpro_cli.cli import cmd_remove as _cmd_remove
23
+ from specpro_cli.integrations import registry
24
+
25
+ # FR-067 — brand header, printed on the bare invocation and before EVERY subcommand's
26
+ # output. One constant each, defined once; the callback below is the only place the
27
+ # header is emitted from.
28
+ BANNER = r"""
29
+ ███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
30
+ ██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔══╝╚██╗ ██╔╝
31
+ ███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
32
+ ╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
33
+ ███████║██║ ███████╗╚██████╗██║██║ ██║
34
+ ╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
35
+ """
36
+ SLOGAN = "Spec Pro - Better Spec-Driven Development Toolkit for AI Coding Agents"
37
+
38
+ app = typer.Typer(
39
+ name="specpro",
40
+ help="Install specpro's distributed assets into a project, for a supported AI coding agent.",
41
+ # The header callback must also run on a BARE invocation; without this Click
42
+ # rejects the missing subcommand before the callback fires ("Missing command").
43
+ invoke_without_command=True,
44
+ )
45
+
46
+
47
+ @app.callback()
48
+ def _header(ctx: typer.Context) -> None:
49
+ """Print the brand header, then dispatch.
50
+
51
+ A bare invocation (no subcommand) gets header + help — the two together are what
52
+ FR-067 means by "裸调用与每一个子命令的输出". `no_args_is_help` is deliberately NOT
53
+ used: its help path bypasses the callback, which would make the bare invocation the
54
+ ONE output without the header.
55
+ """
56
+ typer.echo(BANNER)
57
+ typer.echo(f" {SLOGAN}")
58
+ typer.echo("")
59
+ if ctx.invoked_subcommand is None:
60
+ typer.echo(ctx.get_help())
61
+ raise typer.Exit()
62
+
63
+ # ---------------------------------------------------------------------------------------
64
+ # Registry-derived lookups — the ONLY way this layer learns about agents
65
+ # ---------------------------------------------------------------------------------------
66
+
67
+
68
+ def agent_keys() -> list[str]:
69
+ """Every selectable `--agent` value, from the registry."""
70
+ return registry.keys()
71
+
72
+
73
+ def require_agent_key(key: str) -> None:
74
+ """Reject an unknown `--agent` value, listing the valid ones.
75
+
76
+ The list is not decoration: FR-005 requires the command to exit with the valid keys
77
+ named, because an agent key is not something a user can guess — it is the agent's
78
+ binary name, and a typo is indistinguishable from an unsupported agent without it.
79
+ """
80
+ known = agent_keys()
81
+ if key not in known:
82
+ raise typer.BadParameter(
83
+ f"unknown agent {key!r}. Valid keys: {', '.join(known) if known else '(none registered)'}"
84
+ )
85
+
86
+
87
+ def detect_candidates(project_root: Path) -> list[str]:
88
+ """Agent keys whose deploy root already exists under `project_root`.
89
+
90
+ Derived from each integration's own `commands_dest()`, so the detection rule lives
91
+ with the integration that declares the layout — never here. FR-004 requires the
92
+ caller to ask the user when this returns more than one.
93
+ """
94
+ found: list[str] = []
95
+ for key, integration in sorted(registry.registered().items()):
96
+ if integration.commands_dest(project_root).parent.exists():
97
+ found.append(key)
98
+ return found
99
+
100
+
101
+ def _package_version() -> str:
102
+ from importlib.metadata import PackageNotFoundError, version
103
+
104
+ try:
105
+ return version("specpro-cli")
106
+ except PackageNotFoundError: # running from a source tree with no install
107
+ return "0.0.0+source"
108
+
109
+
110
+ # ---------------------------------------------------------------------------------------
111
+ # Commands
112
+ # ---------------------------------------------------------------------------------------
113
+
114
+
115
+ # The main command. Registered here rather than in `cmd_init` itself so that this file
116
+ # stays the single place where the CLI's surface is declared — the same reason `check`
117
+ # and `version` live here.
118
+ app.command(name="init")(_cmd_init.cmd_init)
119
+ app.command(name="remove")(_cmd_remove.cmd_remove)
120
+
121
+
122
+ @app.command()
123
+ def version() -> None:
124
+ """Print the installed specpro CLI version and system information (FR-069)."""
125
+ import platform
126
+ import sys
127
+
128
+ typer.echo(f"specpro-cli {_package_version()}")
129
+ typer.echo(f"python {sys.version.split()[0]} ({sys.executable})")
130
+ typer.echo(f"os {platform.system()} {platform.release()} ({platform.machine()})")
131
+
132
+
133
+ @app.command()
134
+ def check() -> None:
135
+ """Self-check: runtime preconditions and which agents are already installed here.
136
+
137
+ Read-only by construction — it must remain safe to run in any directory, including
138
+ one that is not a specpro project.
139
+ """
140
+ import shutil
141
+ import subprocess
142
+ import sys
143
+
144
+ typer.echo(f"specpro-cli {_package_version()}")
145
+ typer.echo(f"python {sys.version.split()[0]} ({sys.executable})")
146
+
147
+ # FR-068 — probe the tools the workflow itself depends on, one row per tool,
148
+ # present or missing NAMED (a bare count leaves the user unable to act).
149
+ git = shutil.which("git")
150
+ if git:
151
+ ver = subprocess.run(["git", "--version"], capture_output=True, text=True)
152
+ typer.echo(f"git: present — {ver.stdout.strip() or '(version not reported)'}")
153
+ else:
154
+ typer.echo("git: MISSING — install it (https://git-scm.com) — specpro commits per repository")
155
+
156
+ keys = agent_keys()
157
+ if keys:
158
+ typer.echo(f"registered agents ({len(keys)}): {', '.join(keys)}")
159
+ else:
160
+ # Not an error: the registry fills as agent modules are imported, and the
161
+ # skeleton ships before they exist. Saying so beats printing an empty list.
162
+ typer.echo("registered agents (0): none — no integration modules are imported yet")
163
+
164
+ # Each integration declares its own CLI (`cli_command`); the entry layer only
165
+ # probes what the registry supplies — never a hardcoded name.
166
+ for key in keys:
167
+ integration = registry.registered()[key]
168
+ cli = getattr(integration, "cli_command", None)
169
+ if cli is None:
170
+ typer.echo(f"{key}: IDE-only — no standalone CLI to probe")
171
+ elif shutil.which(cli):
172
+ typer.echo(f"{key}: present — {cli} on PATH")
173
+ else:
174
+ typer.echo(f"{key}: CLI MISSING — `{cli}` not on PATH (the IDE may still be usable)")
175
+
176
+ candidates = detect_candidates(Path.cwd())
177
+ typer.echo(f"detected in {Path.cwd()}: {', '.join(candidates) if candidates else '(none)'}")
178
+
179
+
180
+ if __name__ == "__main__": # pragma: no cover
181
+ app()
@@ -0,0 +1,36 @@
1
+ """Integration layer — `IntegrationBase` + the five format base classes + one module per agent.
2
+
3
+ Each agent module declares its own `key` / `config` / `registrar_config` and registers
4
+ itself on import. Modules MUST NOT import one another (FR-003); the registration block
5
+ below is the only place that knows they exist **as a set**.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ # ---------------------------------------------------------------------------------------
11
+ # Registration order — explicit, alphabetical, and the only list of its kind (FR-003)
12
+ # ---------------------------------------------------------------------------------------
13
+ #
14
+ # ⚠️ **Why explicit imports rather than directory discovery.** A `pkgutil.iter_modules`
15
+ # walk would make "which agents are supported" a property of the filesystem, and the
16
+ # failure mode of a filesystem property is **a missing file**: a module that fails to
17
+ # import, or is skipped by a bad glob, simply does not appear — and nothing reports it,
18
+ # because at every call site an absent agent and an unsupported agent have the same
19
+ # shape (`registry.get(key)` returns `None` for both). The explicit list converts that
20
+ # into an `ImportError` at package-import time.
21
+ #
22
+ # ⚠️ **Why alphabetical.** The CLI reads the registry through `registry.keys()`, which
23
+ # sorts, so this order does not decide what the user sees. It decides **registration**
24
+ # order — and an unsorted list has no rule for where a new line goes, which is exactly
25
+ # how the duplicate-key check stops being obviously complete. Alphabetical is the one
26
+ # rule a reader can verify at a glance.
27
+ #
28
+ # ⚠️ **Complete as of T025**: all six first-batch agents have a module, each named on
29
+ # its import line with the task that added it. The explicit-imports rule above stands —
30
+ # a seventh agent lands here as a new line (alphabetical), never as directory discovery.
31
+ from specpro_cli.integrations import claude # noqa: F401 (registers `claude`)
32
+ from specpro_cli.integrations import copilot # noqa: F401 (registers `copilot`) # T021
33
+ from specpro_cli.integrations import cursor_agent # noqa: F401 (registers `cursor-agent`) # T022
34
+ from specpro_cli.integrations import gemini # noqa: F401 (registers `gemini`) # T023
35
+ from specpro_cli.integrations import opencode # noqa: F401 (registers `opencode`) # T024
36
+ from specpro_cli.integrations import qodercli # noqa: F401 (registers `qodercli`) # T025