program-context-protocol 0.12.4__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.
- pcp/__init__.py +3 -0
- pcp/assertions.py +152 -0
- pcp/attest.py +111 -0
- pcp/build_loop_bypass.py +76 -0
- pcp/build_report.py +54 -0
- pcp/capture.py +339 -0
- pcp/cli.py +104 -0
- pcp/commands/__init__.py +0 -0
- pcp/commands/amend.py +283 -0
- pcp/commands/architect_review.py +291 -0
- pcp/commands/architecture_justification.py +164 -0
- pcp/commands/audit.py +371 -0
- pcp/commands/build.py +4523 -0
- pcp/commands/build_plan.py +153 -0
- pcp/commands/build_status.py +83 -0
- pcp/commands/capture.py +72 -0
- pcp/commands/check.py +584 -0
- pcp/commands/context.py +151 -0
- pcp/commands/control_audit_cmd.py +54 -0
- pcp/commands/correct_objective.py +160 -0
- pcp/commands/dashboard.py +732 -0
- pcp/commands/deploy.py +199 -0
- pcp/commands/deploy_check.py +134 -0
- pcp/commands/design_audit.py +323 -0
- pcp/commands/diff.py +153 -0
- pcp/commands/diff_reduce.py +355 -0
- pcp/commands/docs.py +538 -0
- pcp/commands/doctor.py +820 -0
- pcp/commands/escalations_cmd.py +64 -0
- pcp/commands/gate.py +209 -0
- pcp/commands/import_project.py +404 -0
- pcp/commands/init.py +1634 -0
- pcp/commands/install_hook.py +283 -0
- pcp/commands/install_skill.py +48 -0
- pcp/commands/kickoff.py +772 -0
- pcp/commands/narrative_lint.py +54 -0
- pcp/commands/objective_conflicts_cmd.py +68 -0
- pcp/commands/pm.py +504 -0
- pcp/commands/pressure_test_cmd.py +72 -0
- pcp/commands/provenance.py +313 -0
- pcp/commands/prune.py +179 -0
- pcp/commands/report.py +49 -0
- pcp/commands/run_log_cmd.py +122 -0
- pcp/commands/scan.py +346 -0
- pcp/commands/self_update.py +125 -0
- pcp/commands/status.py +180 -0
- pcp/commands/takeover.py +55 -0
- pcp/commands/telemetry_cmd.py +167 -0
- pcp/commands/validate_module.py +153 -0
- pcp/commands/validate_strategy.py +413 -0
- pcp/commands/verify.py +166 -0
- pcp/commands/verify_syntax_fix.py +74 -0
- pcp/commands/watch.py +372 -0
- pcp/config_audit.py +141 -0
- pcp/context_map.py +124 -0
- pcp/control_audit.py +159 -0
- pcp/coupling.py +178 -0
- pcp/coverage_audit.py +77 -0
- pcp/decision_log.py +134 -0
- pcp/discovery/__init__.py +0 -0
- pcp/discovery/clusters.py +124 -0
- pcp/discovery/graph.py +110 -0
- pcp/discovery/scanner.py +109 -0
- pcp/escalations.py +193 -0
- pcp/evidence.py +30 -0
- pcp/evidence_chain.py +56 -0
- pcp/impact.py +164 -0
- pcp/install_approvals.py +44 -0
- pcp/integrity_audit.py +176 -0
- pcp/librarian.py +89 -0
- pcp/llm/__init__.py +0 -0
- pcp/llm/client.py +183 -0
- pcp/llm/coding_agent_contract.py +104 -0
- pcp/llm/harness/__init__.py +12 -0
- pcp/llm/harness/agy.py +121 -0
- pcp/llm/harness/agy_coding_loop.py +180 -0
- pcp/llm/harness/claude.py +241 -0
- pcp/llm/ledger.py +47 -0
- pcp/narrative_lint.py +229 -0
- pcp/nav_graph.py +226 -0
- pcp/objective_conflicts.py +129 -0
- pcp/operational.py +70 -0
- pcp/orphaned_work.py +262 -0
- pcp/pcp_dir.py +35 -0
- pcp/pcp_status.py +313 -0
- pcp/policy.py +81 -0
- pcp/pressure_test.py +196 -0
- pcp/qa.py +445 -0
- pcp/run_log.py +225 -0
- pcp/schema/__init__.py +0 -0
- pcp/schema/ci_rules.schema.json +106 -0
- pcp/schema/controls.schema.json +39 -0
- pcp/schema/module_acceptance.schema.json +144 -0
- pcp/schema/module_spec.schema.json +78 -0
- pcp/schema/sdlc_phase.schema.json +52 -0
- pcp/schema/validator.py +77 -0
- pcp/skill_data/pcp/SKILL.md +1897 -0
- pcp/spec_write.py +269 -0
- pcp/spend.py +77 -0
- pcp/symbols.py +86 -0
- pcp/telemetry.py +308 -0
- pcp/uat.py +271 -0
- pcp/version_drift.py +222 -0
- program_context_protocol-0.12.4.dist-info/METADATA +123 -0
- program_context_protocol-0.12.4.dist-info/RECORD +109 -0
- program_context_protocol-0.12.4.dist-info/WHEEL +4 -0
- program_context_protocol-0.12.4.dist-info/entry_points.txt +2 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-APACHE +202 -0
- program_context_protocol-0.12.4.dist-info/licenses/LICENSE-MIT +21 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""pcp install-hook — install pcp check as a git commit-msg hook (Layer 1 gate)."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from pcp.pcp_dir import find_pcp_dir, NoPCPDir
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
COMMIT_MSG_HOOK = """\
|
|
14
|
+
#!/bin/sh
|
|
15
|
+
# PCP Layer 1 gate + commit hygiene
|
|
16
|
+
# Installed by: pcp install-hook
|
|
17
|
+
#
|
|
18
|
+
# Strips any Co-Authored-By trailer referencing Claude/Anthropic before the
|
|
19
|
+
# commit message is finalized -- defense in depth for "never attribute a
|
|
20
|
+
# commit to Claude": even if some other tool, session, or human adds one by
|
|
21
|
+
# habit, it never reaches the actual commit object. Uses perl, not sed -i,
|
|
22
|
+
# because sed's in-place-edit flag syntax differs between BSD (macOS) and
|
|
23
|
+
# GNU (Linux) sed -- perl -ni is identical on both.
|
|
24
|
+
perl -ni -e 'print unless /^Co-Authored-By:.*(claude|anthropic)/i' "$1"
|
|
25
|
+
#
|
|
26
|
+
# Agent-session attribution trailer (2026-07-17, incremental identity
|
|
27
|
+
# hardening): when the commit happens inside a PCP agent session, stamp the
|
|
28
|
+
# session id into the commit message as a queryable trailer. Still rooted in
|
|
29
|
+
# a self-declared env var — honestly NOT cryptographic identity (the IETF
|
|
30
|
+
# dynamic-attestation draft is the eventual target) — but it moves the
|
|
31
|
+
# marker from "ambient env var at gate time" to "recorded on the commit
|
|
32
|
+
# object itself", queryable via plain `git log --grep`.
|
|
33
|
+
if [ -n "$PCP_AGENT_SESSION" ]; then
|
|
34
|
+
if ! grep -q '^PCP-Agent-Session:' "$1"; then
|
|
35
|
+
printf '\nPCP-Agent-Session: %s\n' "${PCP_AGENT_SESSION_ID:-unidentified}" >> "$1"
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
#
|
|
39
|
+
# Runs as a commit-msg hook, not pre-commit: git does not write the final
|
|
40
|
+
# commit message to disk until after pre-commit runs (confirmed empirically —
|
|
41
|
+
# COMMIT_EDITMSG holds the PREVIOUS commit's message at pre-commit time when
|
|
42
|
+
# committing via `-m`), so a pre-commit hook can never see a `[pcp-bypass:
|
|
43
|
+
# reason]` marker in the message being created. commit-msg fires after the
|
|
44
|
+
# message is finalized but still before the commit object is created, so it
|
|
45
|
+
# blocks just as effectively and the bypass marker actually works.
|
|
46
|
+
pcp check --commit-msg-file "$1"
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
POST_COMMIT_HOOK = """\
|
|
50
|
+
#!/bin/sh
|
|
51
|
+
# PCP post-commit state refresh
|
|
52
|
+
# Installed by: pcp install-hook / pcp init
|
|
53
|
+
#
|
|
54
|
+
# Regenerates current_state.md + diff.md after every commit -- not just
|
|
55
|
+
# when a human remembers to run `pcp scan` manually, and not just for
|
|
56
|
+
# commits made inside `pcp build`'s own loop (which already calls scan
|
|
57
|
+
# directly). Closes the gap where a commit made outside that loop (a human
|
|
58
|
+
# editing code directly, or `pcp pm`) left drift state stale until the next
|
|
59
|
+
# unrelated `pcp scan` invocation.
|
|
60
|
+
#
|
|
61
|
+
# Best-effort and silent: a post-commit hook runs after the commit object
|
|
62
|
+
# already exists, so it can never block or undo the commit -- failures here
|
|
63
|
+
# are logged, never fatal. No-ops cleanly if `pcp` isn't on PATH yet or the
|
|
64
|
+
# project has no modules scaffolded (pcp scan already handles both cases).
|
|
65
|
+
command -v pcp >/dev/null 2>&1 && pcp scan --quiet >/dev/null 2>&1
|
|
66
|
+
exit 0
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
PRE_COMMIT_FRAMEWORK_CONFIG = """\
|
|
70
|
+
repos:
|
|
71
|
+
- repo: local
|
|
72
|
+
hooks:
|
|
73
|
+
- id: pcp-check
|
|
74
|
+
name: PCP Layer 1 gate
|
|
75
|
+
entry: pcp check --commit-msg-file
|
|
76
|
+
language: system
|
|
77
|
+
stages: [commit-msg]
|
|
78
|
+
pass_filenames: true
|
|
79
|
+
always_run: true
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _find_git_dir(project_root: Path) -> Path | None:
|
|
84
|
+
import subprocess
|
|
85
|
+
try:
|
|
86
|
+
r = subprocess.run(["git", "rev-parse", "--git-dir"],
|
|
87
|
+
capture_output=True, text=True, cwd=project_root)
|
|
88
|
+
git_dir = Path(r.stdout.strip()) if r.returncode == 0 else None
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
git_dir = None
|
|
91
|
+
if git_dir and not git_dir.is_absolute():
|
|
92
|
+
git_dir = project_root / git_dir
|
|
93
|
+
return git_dir
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def install_git_hook(project_root: Path, force: bool = False) -> tuple[bool, str]:
|
|
97
|
+
"""Just the commit-msg hook file. Pulled out of the CLI command so `pcp
|
|
98
|
+
init` can call this directly and get a project under real Layer-1
|
|
99
|
+
enforcement the moment it's scaffolded.
|
|
100
|
+
|
|
101
|
+
Returns (installed: bool, message: str) -- never raises, so callers
|
|
102
|
+
that want this to be a best-effort side effect (like init.py) can just
|
|
103
|
+
print the message and move on rather than handling exceptions."""
|
|
104
|
+
git_dir = _find_git_dir(project_root)
|
|
105
|
+
if not git_dir:
|
|
106
|
+
return False, "not a git repository yet -- skipped"
|
|
107
|
+
|
|
108
|
+
hooks_dir = git_dir / "hooks"
|
|
109
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
110
|
+
hook_path = hooks_dir / "commit-msg"
|
|
111
|
+
|
|
112
|
+
if hook_path.exists() and not force:
|
|
113
|
+
if "pcp check --commit-msg-file" in hook_path.read_text():
|
|
114
|
+
commit_msg_result = True, f"already installed at {hook_path}"
|
|
115
|
+
else:
|
|
116
|
+
return False, f"a different commit-msg hook already exists at {hook_path} -- run `pcp install-hook --force` to overwrite, or `--pre-commit-framework` to append"
|
|
117
|
+
else:
|
|
118
|
+
hook_path.write_text(COMMIT_MSG_HOOK)
|
|
119
|
+
hook_path.chmod(0o755)
|
|
120
|
+
commit_msg_result = True, f"installed {hook_path}"
|
|
121
|
+
|
|
122
|
+
post_commit_path = hooks_dir / "post-commit"
|
|
123
|
+
if post_commit_path.exists() and not force:
|
|
124
|
+
if "pcp scan" not in post_commit_path.read_text():
|
|
125
|
+
# Don't clobber a human's own post-commit hook -- just skip ours.
|
|
126
|
+
return commit_msg_result
|
|
127
|
+
else:
|
|
128
|
+
post_commit_path.write_text(POST_COMMIT_HOOK)
|
|
129
|
+
post_commit_path.chmod(0o755)
|
|
130
|
+
|
|
131
|
+
return commit_msg_result
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@click.command()
|
|
135
|
+
@click.option("--path", "project_path", type=click.Path(), default=None)
|
|
136
|
+
@click.option("--pre-commit-framework", is_flag=True,
|
|
137
|
+
help="Add to .pre-commit-config.yaml instead of .git/hooks/.")
|
|
138
|
+
@click.option("--force", is_flag=True, help="Overwrite existing hook.")
|
|
139
|
+
def install_hook(project_path: str | None, pre_commit_framework: bool, force: bool):
|
|
140
|
+
"""Install pcp check as a git commit-msg hook (needed so `[pcp-bypass: reason]` is visible)."""
|
|
141
|
+
try:
|
|
142
|
+
pcp_dir = find_pcp_dir(Path(project_path) if project_path else None)
|
|
143
|
+
except NoPCPDir as e:
|
|
144
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
145
|
+
sys.exit(2)
|
|
146
|
+
|
|
147
|
+
project_root = pcp_dir.parent
|
|
148
|
+
|
|
149
|
+
if pre_commit_framework:
|
|
150
|
+
config_path = project_root / ".pre-commit-config.yaml"
|
|
151
|
+
if config_path.exists():
|
|
152
|
+
existing = config_path.read_text()
|
|
153
|
+
if "pcp-check" in existing:
|
|
154
|
+
console.print("[dim]pcp-check already in .pre-commit-config.yaml[/dim]")
|
|
155
|
+
sys.exit(0)
|
|
156
|
+
with open(config_path, "a") as f:
|
|
157
|
+
f.write("\n" + PRE_COMMIT_FRAMEWORK_CONFIG)
|
|
158
|
+
console.print("[green]appended[/green] pcp-check to .pre-commit-config.yaml")
|
|
159
|
+
else:
|
|
160
|
+
config_path.write_text(PRE_COMMIT_FRAMEWORK_CONFIG)
|
|
161
|
+
console.print("[green]created[/green] .pre-commit-config.yaml with pcp-check")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
git_dir = _find_git_dir(project_root)
|
|
165
|
+
if not git_dir:
|
|
166
|
+
console.print("[red]Error:[/red] not a git repository.")
|
|
167
|
+
sys.exit(2)
|
|
168
|
+
|
|
169
|
+
hooks_dir = git_dir / "hooks"
|
|
170
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
171
|
+
hook_path = hooks_dir / "commit-msg"
|
|
172
|
+
|
|
173
|
+
# Bug fixed 2026-07-21: this used to sys.exit(1) here whenever commit-msg
|
|
174
|
+
# already existed, which meant the post-commit check below could NEVER
|
|
175
|
+
# run in that case -- a project with a working commit-msg hook but a
|
|
176
|
+
# missing post-commit hook (e.g. this repo itself, predating the
|
|
177
|
+
# post-commit feature) could never get it installed via this command,
|
|
178
|
+
# only via `pcp init`'s own install_git_hook() call, which didn't have
|
|
179
|
+
# this bug. commit-msg's own outcome is now independent of post-commit's.
|
|
180
|
+
commit_msg_installed = False
|
|
181
|
+
if hook_path.exists() and not force:
|
|
182
|
+
console.print(f"[yellow]Hook already exists:[/yellow] {hook_path}")
|
|
183
|
+
console.print("Use --force to overwrite (this replaces the whole file — merge manually "
|
|
184
|
+
"if you have other commit-msg hooks), or --pre-commit-framework to append.")
|
|
185
|
+
else:
|
|
186
|
+
hook_path.write_text(COMMIT_MSG_HOOK)
|
|
187
|
+
hook_path.chmod(0o755)
|
|
188
|
+
console.print(f"[green]installed[/green] {hook_path}")
|
|
189
|
+
console.print("[dim]pcp check will run before every commit finalizes.[/dim]")
|
|
190
|
+
commit_msg_installed = True
|
|
191
|
+
|
|
192
|
+
post_commit_path = hooks_dir / "post-commit"
|
|
193
|
+
if post_commit_path.exists() and not force and "pcp scan" not in post_commit_path.read_text():
|
|
194
|
+
console.print(f"[yellow]A different post-commit hook already exists at {post_commit_path} — skipped (use --force to overwrite).[/yellow]")
|
|
195
|
+
else:
|
|
196
|
+
post_commit_path.write_text(POST_COMMIT_HOOK)
|
|
197
|
+
post_commit_path.chmod(0o755)
|
|
198
|
+
console.print(f"[green]installed[/green] {post_commit_path}")
|
|
199
|
+
console.print("[dim]current_state.md + diff.md refresh after every commit.[/dim]")
|
|
200
|
+
|
|
201
|
+
_remove_legacy_cron_jobs()
|
|
202
|
+
|
|
203
|
+
# commit-msg is the primary hook this command exists for -- preserve the
|
|
204
|
+
# original exit-1-on-refusal contract for it specifically, even though
|
|
205
|
+
# post-commit (checked above, independently) may have installed fine.
|
|
206
|
+
if not commit_msg_installed:
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# Removed 2026-07-27, pre-launch review. This command used to also install two
|
|
211
|
+
# global crontab jobs via `_install_cron_scripts()`, unconditionally and with no
|
|
212
|
+
# prompt:
|
|
213
|
+
#
|
|
214
|
+
# 1. `upgrade_skill.sh` — daily `curl` of SKILL.md from a hardcoded personal
|
|
215
|
+
# GitHub raw URL, overwriting ~/.claude/skills/pcp/SKILL.md with no
|
|
216
|
+
# signature or hash check. That file is an AGENT INSTRUCTION file: whatever
|
|
217
|
+
# the fetched content says, the next `/pcp` session executes. A remote,
|
|
218
|
+
# unverified, self-updating instruction channel is the single worst thing
|
|
219
|
+
# to ship in a tool whose entire purpose is governing what agents do. It
|
|
220
|
+
# was dormant only because the origin repo is private (`curl -sf` on a
|
|
221
|
+
# private URL returns empty, so the upgrader no-opped) -- making the repo
|
|
222
|
+
# PUBLIC is what would have armed it.
|
|
223
|
+
# 2. `aggregate_interventions.sh` — scanned a hardcoded `~/Claude-code` across
|
|
224
|
+
# every project on the machine and posted a summary to a hardcoded Slack
|
|
225
|
+
# channel. Personal-workflow plumbing with no place in a public tool, and
|
|
226
|
+
# it interpolated each discovered path straight into a `python3 -c` string,
|
|
227
|
+
# so a directory name containing a quote executed arbitrary code daily.
|
|
228
|
+
#
|
|
229
|
+
# Neither was ever a documented feature of `pcp install-hook`, whose stated job
|
|
230
|
+
# is installing git hooks. Deleted outright rather than fixed: skill updates
|
|
231
|
+
# belong in the package (`pcp install-skill`, versioned via PyPI), not in an
|
|
232
|
+
# out-of-band self-updater.
|
|
233
|
+
_LEGACY_CRON_MARKER = "/.pcp/cron/"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _remove_legacy_cron_jobs() -> None:
|
|
237
|
+
"""Uninstall the cron jobs older PCP versions installed silently.
|
|
238
|
+
|
|
239
|
+
Deleting the installer does nothing for machines that already ran it --
|
|
240
|
+
those crontab entries keep firing forever. `pcp install-hook` is the
|
|
241
|
+
command that put them there, so it is the right place to take them back
|
|
242
|
+
out. Removes only lines this tool wrote (matched on the `~/.pcp/cron/`
|
|
243
|
+
path it always used) and reports what went; never touches any other
|
|
244
|
+
crontab line, and never fails the hook install if crontab is unavailable."""
|
|
245
|
+
import subprocess
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
existing = subprocess.run(["crontab", "-l"], capture_output=True, text=True)
|
|
249
|
+
except FileNotFoundError:
|
|
250
|
+
return
|
|
251
|
+
if existing.returncode != 0:
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
lines = existing.stdout.splitlines()
|
|
255
|
+
keep = [ln for ln in lines if _LEGACY_CRON_MARKER not in ln]
|
|
256
|
+
if len(keep) == len(lines):
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
removed = [ln for ln in lines if _LEGACY_CRON_MARKER in ln]
|
|
260
|
+
body = "\n".join(keep).strip("\n")
|
|
261
|
+
proc = subprocess.run(
|
|
262
|
+
["crontab", "-"], input=(body + "\n") if body else "", text=True,
|
|
263
|
+
capture_output=True,
|
|
264
|
+
)
|
|
265
|
+
if proc.returncode != 0:
|
|
266
|
+
console.print(
|
|
267
|
+
"[yellow]Found legacy PCP cron jobs but could not remove them "
|
|
268
|
+
"automatically. Remove these lines with `crontab -e`:[/yellow]"
|
|
269
|
+
)
|
|
270
|
+
for ln in removed:
|
|
271
|
+
console.print(f" [dim]{ln}[/dim]")
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
console.print(f"[green]removed[/green] {len(removed)} legacy PCP cron job(s):")
|
|
275
|
+
for ln in removed:
|
|
276
|
+
console.print(f" [dim]{ln}[/dim]")
|
|
277
|
+
console.print(
|
|
278
|
+
"[dim]These were installed by an older `pcp install-hook` and included a "
|
|
279
|
+
"daily unverified remote overwrite of your PCP skill file. The scripts "
|
|
280
|
+
"themselves are still on disk at ~/.pcp/cron/ — delete them yourself.[/dim]"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""pcp install-skill — install the full `/pcp` orchestrator skill.
|
|
2
|
+
|
|
3
|
+
Distinct from this repo's own SKILL.md (the self-install bootstrap served to
|
|
4
|
+
a fresh session that just needs to `pip install` and run plain CLI commands).
|
|
5
|
+
This installs the much larger orchestrator skill — vision workshop, parallel
|
|
6
|
+
build via the Workflow tool, escalation handling, multi-project status — as
|
|
7
|
+
a real Claude Code skill at ~/.claude/skills/pcp/SKILL.md, so `/pcp` becomes
|
|
8
|
+
available as a slash command.
|
|
9
|
+
|
|
10
|
+
Bundled as package data (src/pcp/skill_data/pcp/SKILL.md) rather than
|
|
11
|
+
downloaded, so it installs offline together with the wheel.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import shutil
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
BUNDLED_SKILL_PATH = Path(__file__).parent.parent / "skill_data" / "pcp" / "SKILL.md"
|
|
24
|
+
DEFAULT_INSTALL_PATH = Path.home() / ".claude" / "skills" / "pcp" / "SKILL.md"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@click.command(name="install-skill")
|
|
28
|
+
@click.option("--force", is_flag=True, help="Overwrite an existing installed skill.")
|
|
29
|
+
@click.option("--path", "install_path", type=click.Path(), default=None,
|
|
30
|
+
help="Override the install destination (default: ~/.claude/skills/pcp/SKILL.md).")
|
|
31
|
+
def install_skill(force: bool, install_path: str | None):
|
|
32
|
+
"""Install the /pcp orchestrator skill to ~/.claude/skills/pcp/SKILL.md."""
|
|
33
|
+
if not BUNDLED_SKILL_PATH.exists():
|
|
34
|
+
console.print(f"[red]Error:[/red] bundled skill not found at {BUNDLED_SKILL_PATH} "
|
|
35
|
+
"— this install may be missing package data.")
|
|
36
|
+
sys.exit(2)
|
|
37
|
+
|
|
38
|
+
dest = Path(install_path) if install_path else DEFAULT_INSTALL_PATH
|
|
39
|
+
|
|
40
|
+
if dest.exists() and not force:
|
|
41
|
+
console.print(f"[yellow]Already installed:[/yellow] {dest}")
|
|
42
|
+
console.print("Use --force to overwrite with the bundled version.")
|
|
43
|
+
sys.exit(1)
|
|
44
|
+
|
|
45
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
shutil.copy(BUNDLED_SKILL_PATH, dest)
|
|
47
|
+
console.print(f"[green]installed[/green] {dest}")
|
|
48
|
+
console.print("[dim]/pcp is now available as a Claude Code skill.[/dim]")
|