pactkit-opencode 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.
@@ -0,0 +1,8 @@
1
+ """PactKit adapter for OpenCode IDE.
2
+
3
+ Auto-registers OpenCodeDeployer via entry_points when imported.
4
+ """
5
+
6
+ from pactkit_opencode.deployer import OpenCodeDeployer
7
+
8
+ __all__ = ["OpenCodeDeployer"]
@@ -0,0 +1,306 @@
1
+ """OpenCode deployer — generates OpenCode-native configuration.
2
+
3
+ Extracted from pactkit core (STORY-slim-058).
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from pactkit import __version__, prompts
10
+ from pactkit.config import (
11
+ VALID_AGENTS,
12
+ VALID_COMMANDS,
13
+ VALID_RULES,
14
+ VALID_SKILLS,
15
+ auto_merge_config_file,
16
+ load_config,
17
+ )
18
+ from pactkit.generators.deploy_base import DeployerBase, register_deployer
19
+ from pactkit.generators.deployer import (
20
+ _cleanup_legacy,
21
+ _deploy_agents,
22
+ _deploy_ci,
23
+ _deploy_commands,
24
+ _deploy_rules,
25
+ _deploy_skills,
26
+ )
27
+ from pactkit.profiles import get_profile
28
+ from pactkit.utils import atomic_write
29
+
30
+
31
+ class OpenCodeDeployer(DeployerBase):
32
+ """OpenCode deployment — generate OpenCode-native configuration (STORY-069).
33
+
34
+ OpenCode (anomalyco/opencode) is an open-source AI coding assistant that supports
35
+ multiple LLM providers. This deployment mode generates OpenCode-native files:
36
+ - AGENTS.md (slim header — rules loaded via instructions)
37
+ - rules/ directory with modular rule files (STORY-071 R6)
38
+ - opencode.json (global config with instructions: ["rules/*.md"])
39
+ - agents/, commands/, skills/ directories
40
+
41
+ STORY-slim-008: Aligned with _deploy_classic() feature set:
42
+ - Reads pactkit.yaml for selective deployment (R1)
43
+ - Calls auto_merge_config_file() for automatic config updates (R2)
44
+ - Calls _cleanup_legacy() to remove stale skill files (R3)
45
+ - Generates project-level AGENTS.md when not in preview mode (R4)
46
+ - Prints MCP server recommendations (R5)
47
+ """
48
+
49
+ profile = get_profile("opencode")
50
+
51
+ def deploy(self, config=None, target=None):
52
+ opencode_root = Path(target) if target else Path.home() / ".config" / "opencode"
53
+
54
+ print("šŸš€ PactKit OpenCode Deployment")
55
+
56
+ # Prepare directories
57
+ agents_dir = opencode_root / "agents"
58
+ commands_dir = opencode_root / "commands"
59
+ skills_dir = opencode_root / "skills"
60
+ rules_dir = opencode_root / "rules"
61
+
62
+ for d in [opencode_root, agents_dir, commands_dir, skills_dir, rules_dir]:
63
+ d.mkdir(parents=True, exist_ok=True)
64
+
65
+ # STORY-slim-008 R1+R2: Load config from project-level pactkit.yaml
66
+ from pactkit.config import find_pactkit_yaml
67
+
68
+ project_yaml = find_pactkit_yaml()
69
+
70
+ if project_yaml is not None:
71
+ auto_added = auto_merge_config_file(project_yaml)
72
+ for item in auto_added:
73
+ print(f" -> Auto-added: {item}")
74
+ cfg = load_config(project_yaml)
75
+ else:
76
+ cfg = {}
77
+
78
+ enabled_agents = cfg.get("agents", sorted(VALID_AGENTS))
79
+ enabled_commands = cfg.get("commands", sorted(VALID_COMMANDS))
80
+ enabled_skills = cfg.get("skills", sorted(VALID_SKILLS))
81
+ enabled_rules = cfg.get("rules", sorted(VALID_RULES))
82
+
83
+ # Deploy components with OpenCode profile
84
+ n_skills = _deploy_skills(skills_dir, enabled_skills, profile=self.profile)
85
+ _cleanup_legacy(skills_dir)
86
+
87
+ n_rules = _deploy_rules(opencode_root, enabled_rules)
88
+ self._deploy_agents_md_inline(opencode_root)
89
+
90
+ providers = self._load_opencode_providers(opencode_root)
91
+ if not providers:
92
+ providers = self._load_opencode_providers(
93
+ Path(self.profile.global_config_dir).expanduser()
94
+ )
95
+ command_models = cfg.get("command_models", {})
96
+ self._update_global_opencode_json(
97
+ opencode_root, command_models=command_models, providers=providers
98
+ )
99
+
100
+ n_agents = _deploy_agents(agents_dir, enabled_agents, profile=self.profile)
101
+ n_commands = _deploy_commands(
102
+ commands_dir, enabled_commands, profile=self.profile, config=cfg
103
+ )
104
+
105
+ ci_config = cfg.get("ci", {})
106
+ ci_provider = ci_config.get("provider", "none") if isinstance(ci_config, dict) else "none"
107
+ project_root = Path.cwd()
108
+ _deploy_ci(ci_provider, project_root, cfg)
109
+
110
+ print(
111
+ f"\nāœ… OpenCode: {n_agents} Agents, {n_commands} Commands, "
112
+ f"{n_skills} Skills, {n_rules} Rules → {opencode_root}"
113
+ )
114
+
115
+ if target is None:
116
+ self._generate_project_agents_md()
117
+
118
+ self._print_mcp_recommendations_opencode()
119
+
120
+ # --- OpenCode-specific helpers ---
121
+
122
+ @staticmethod
123
+ def _generate_project_agents_md():
124
+ """Generate project-level AGENTS.md if it does not exist (STORY-slim-008 R4)."""
125
+ project_root = Path.cwd()
126
+ if project_root.resolve() == Path.home().resolve():
127
+ return
128
+ agents_md_path = project_root / "AGENTS.md"
129
+ if agents_md_path.exists():
130
+ return
131
+ project_name = project_root.name
132
+ content = f"# {project_name}\n\n@./docs/product/context.md\noutput MUST use Chinese\n"
133
+ atomic_write(agents_md_path, content)
134
+
135
+ @staticmethod
136
+ def _deploy_agents_md_inline(opencode_root):
137
+ """Generate AGENTS.md with on-demand @reference index (STORY-slim-009 R3)."""
138
+ ref_lines = []
139
+ ondemand_descriptions = {
140
+ "08-architecture-principles.md": "Architecture decisions, SOLID/DRY patterns",
141
+ "04-routing-table.md": "Agent/command routing table",
142
+ "06-mcp-integration.md": "MCP server integration guide",
143
+ "05-workflow-conventions.md": "PDCA workflow conventions",
144
+ "07-shared-protocols.md": "PDCA shared protocols (test mapping, visualize, context.md format)",
145
+ "03-file-atlas.md": "File atlas (project file locations)",
146
+ }
147
+ for filename in sorted(prompts.RULES_ONDEMAND_FILES.values()):
148
+ desc = ondemand_descriptions.get(filename, filename)
149
+ ref_lines.append(f"- {desc}: @rules/{filename}")
150
+
151
+ core_names = ", ".join(
152
+ f.replace(".md", "") for f in sorted(prompts.RULES_CORE_FILES.values())
153
+ )
154
+
155
+ lines = [
156
+ f"# PactKit Global Constitution (v{__version__} Modular)",
157
+ "",
158
+ f"Core rules ({core_names}) are always loaded via `instructions`.",
159
+ "",
160
+ "## On-Demand Rules",
161
+ "",
162
+ "CRITICAL: When you encounter a file reference below (e.g., @rules/xxx.md), "
163
+ "use your Read tool to load it on a need-to-know basis. "
164
+ "Do NOT preemptively load all references — use lazy loading based on actual need.",
165
+ "",
166
+ *ref_lines,
167
+ "",
168
+ "## Quick Reference",
169
+ "",
170
+ "- **Specs** (`docs/specs/`) are the source of truth",
171
+ "- **Sprint Board**: `docs/product/sprint_board.md`",
172
+ "- **Architecture**: `docs/architecture/graphs/`",
173
+ "- **Commands**: Type `/` followed by command name (e.g., `/project-plan`)",
174
+ "",
175
+ "> **TIP**: Run `/project-init` to set up project governance and enable cross-session context.",
176
+ "",
177
+ ]
178
+ atomic_write(opencode_root / "AGENTS.md", "\n".join(lines))
179
+
180
+ @staticmethod
181
+ def _deploy_opencode_json(opencode_root):
182
+ """Generate opencode.json project configuration (STORY-071 R1/R2)."""
183
+ config = {
184
+ "$schema": "https://opencode.ai/config.json",
185
+ "instructions": ["AGENTS.md", "docs/product/context.md"],
186
+ "agent": {
187
+ "build": {"model": "inherit"},
188
+ "plan": {"model": "inherit"},
189
+ },
190
+ "permission": {
191
+ "edit": "allow",
192
+ "bash": {
193
+ "*": "allow",
194
+ "rm -rf /*": "deny",
195
+ "rm -rf /Users/*": "deny",
196
+ "rm -rf /System/*": "deny",
197
+ "sudo rm *": "deny",
198
+ "sudo mkfs *": "deny",
199
+ "curl * | sh": "deny",
200
+ "wget * | sh": "deny",
201
+ },
202
+ "read": {
203
+ "*": "allow",
204
+ "*.env": "deny",
205
+ "*.env.*": "deny",
206
+ "*.env.example": "allow",
207
+ },
208
+ },
209
+ "mcp": {
210
+ "context7": {
211
+ "type": "remote",
212
+ "url": "https://mcp.context7.com/mcp",
213
+ },
214
+ },
215
+ }
216
+ content = json.dumps(config, indent=2, ensure_ascii=False) + "\n"
217
+ atomic_write(opencode_root / "opencode.json", content)
218
+
219
+ @staticmethod
220
+ def _resolve_opencode_model_id(short_name, providers):
221
+ """Resolve a short model name to a full provider/model-id (STORY-073)."""
222
+ if not short_name or not providers:
223
+ return None
224
+ keyword = short_name.lower()
225
+ for provider_name, provider_data in providers.items():
226
+ models = provider_data.get("models", {})
227
+ for model_id in models:
228
+ if keyword in model_id.lower():
229
+ return f"{provider_name}/{model_id}"
230
+ return None
231
+
232
+ @staticmethod
233
+ def _load_opencode_providers(opencode_root):
234
+ """Load provider config from opencode.json for model resolution."""
235
+ json_path = opencode_root / "opencode.json"
236
+ if json_path.is_file():
237
+ try:
238
+ data = json.loads(json_path.read_text(encoding="utf-8"))
239
+ return data.get("provider", {})
240
+ except (json.JSONDecodeError, OSError):
241
+ pass
242
+ return {}
243
+
244
+ @staticmethod
245
+ def _update_global_opencode_json(opencode_root, command_models=None, providers=None):
246
+ """Update global opencode.json with instructions and command model routing."""
247
+ json_path = opencode_root / "opencode.json"
248
+ config = {}
249
+
250
+ if json_path.is_file():
251
+ try:
252
+ config = json.loads(json_path.read_text(encoding="utf-8"))
253
+ except (json.JSONDecodeError, OSError):
254
+ config = {}
255
+
256
+ config.setdefault("$schema", "https://opencode.ai/config.json")
257
+
258
+ credential_path = f"rules/{prompts.CREDENTIAL_SAFETY_FILE}"
259
+ existing = [
260
+ i
261
+ for i in config.get("instructions", [])
262
+ if i != "rules/*.md" and not (i.startswith("rules/") and i != credential_path)
263
+ ]
264
+ if credential_path not in existing:
265
+ existing.append(credential_path)
266
+ config["instructions"] = existing
267
+
268
+ if command_models and providers:
269
+ cmd_config = config.get("command", {})
270
+ for cmd_name, model_short in command_models.items():
271
+ model_id = OpenCodeDeployer._resolve_opencode_model_id(model_short, providers)
272
+ if model_id:
273
+ if cmd_name not in cmd_config:
274
+ cmd_config[cmd_name] = {}
275
+ cmd_config[cmd_name]["model"] = model_id
276
+ if cmd_config:
277
+ config["command"] = cmd_config
278
+
279
+ content = json.dumps(config, indent=2, ensure_ascii=False) + "\n"
280
+ atomic_write(json_path, content)
281
+
282
+ @staticmethod
283
+ def _print_mcp_recommendations_opencode():
284
+ """Print MCP server recommendations for OpenCode deployment (STORY-071 R4)."""
285
+ print("\nšŸ“¦ MCP Server Configuration (add to opencode.json):")
286
+ print()
287
+ print(" Remote MCP (no install needed):")
288
+ print(' "context7": { "type": "remote", "url": "https://mcp.context7.com/mcp" }')
289
+ print()
290
+ print(" Local MCP (requires npx):")
291
+ print(
292
+ ' "memory": { "type": "local", "command": '
293
+ '["npx", "-y", "@modelcontextprotocol/server-memory"] }'
294
+ )
295
+ print(
296
+ ' "playwright": { "type": "local", "command": '
297
+ '["npx", "-y", "@playwright/mcp"] }'
298
+ )
299
+ print(
300
+ ' "puppeteer": { "type": "local", "command": '
301
+ '["npx", "-y", "@anthropic/mcp-puppeteer"] }'
302
+ )
303
+
304
+
305
+ # Auto-register when this module is imported
306
+ register_deployer("opencode", OpenCodeDeployer, force=True)
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: pactkit-opencode
3
+ Version: 0.1.0
4
+ Summary: PactKit adapter for OpenCode IDE
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: pactkit>=2.5.0
@@ -0,0 +1,7 @@
1
+ pactkit_opencode/__init__.py,sha256=ZK-SR04GQwpZpH9SdTlNJWQXDJZgmUKLH70N2znpbNU,194
2
+ pactkit_opencode/deployer.py,sha256=WN5zW28UjZE9RVtkMx9bJN9zGzSrjzgX-eZx7pY4rFA,11992
3
+ pactkit_opencode-0.1.0.dist-info/METADATA,sha256=GuRjXAhJ9cBpkbTRso7XITXq248mmecX9TWRDXcoIjA,180
4
+ pactkit_opencode-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ pactkit_opencode-0.1.0.dist-info/entry_points.txt,sha256=By7lE89sPCfucVIOpDRWK2biKi_ZarkCltUidcCAo6Y,65
6
+ pactkit_opencode-0.1.0.dist-info/top_level.txt,sha256=kmFfcuDMNxmyylzdSsieob_DGbBJ43YN1_DOZMIrudA,17
7
+ pactkit_opencode-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [pactkit.deployers]
2
+ opencode = pactkit_opencode:OpenCodeDeployer
@@ -0,0 +1 @@
1
+ pactkit_opencode