pactkit-opencode 0.1.0__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.
- pactkit_opencode-0.1.0/PKG-INFO +7 -0
- pactkit_opencode-0.1.0/pyproject.toml +23 -0
- pactkit_opencode-0.1.0/setup.cfg +4 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode/__init__.py +8 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode/deployer.py +306 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode.egg-info/PKG-INFO +7 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode.egg-info/SOURCES.txt +15 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode.egg-info/dependency_links.txt +1 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode.egg-info/entry_points.txt +2 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode.egg-info/requires.txt +1 -0
- pactkit_opencode-0.1.0/src/pactkit_opencode.egg-info/top_level.txt +1 -0
- pactkit_opencode-0.1.0/tests/test_bug035_opencode_dual_layer.py +105 -0
- pactkit_opencode-0.1.0/tests/test_deploy_opencode_parity.py +313 -0
- pactkit_opencode-0.1.0/tests/test_story069_opencode_format.py +241 -0
- pactkit_opencode-0.1.0/tests/test_story070_opencode_compliance.py +255 -0
- pactkit_opencode-0.1.0/tests/test_story071_opencode_config_parity.py +221 -0
- pactkit_opencode-0.1.0/tests/test_story073_command_model_routing.py +238 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pactkit-opencode"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "PactKit adapter for OpenCode IDE"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pactkit>=2.5.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.entry-points."pactkit.deployers"]
|
|
16
|
+
opencode = "pactkit_opencode:OpenCodeDeployer"
|
|
17
|
+
|
|
18
|
+
[tool.setuptools.packages.find]
|
|
19
|
+
where = ["src"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
23
|
+
pythonpath = ["src", "../pactkit/src"]
|
|
@@ -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,15 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
src/pactkit_opencode/__init__.py
|
|
3
|
+
src/pactkit_opencode/deployer.py
|
|
4
|
+
src/pactkit_opencode.egg-info/PKG-INFO
|
|
5
|
+
src/pactkit_opencode.egg-info/SOURCES.txt
|
|
6
|
+
src/pactkit_opencode.egg-info/dependency_links.txt
|
|
7
|
+
src/pactkit_opencode.egg-info/entry_points.txt
|
|
8
|
+
src/pactkit_opencode.egg-info/requires.txt
|
|
9
|
+
src/pactkit_opencode.egg-info/top_level.txt
|
|
10
|
+
tests/test_bug035_opencode_dual_layer.py
|
|
11
|
+
tests/test_deploy_opencode_parity.py
|
|
12
|
+
tests/test_story069_opencode_format.py
|
|
13
|
+
tests/test_story070_opencode_compliance.py
|
|
14
|
+
tests/test_story071_opencode_config_parity.py
|
|
15
|
+
tests/test_story073_command_model_routing.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pactkit>=2.5.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pactkit_opencode
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
BUG-035: OpenCode Format Should Follow Dual-Layer Architecture
|
|
3
|
+
Tests for dual-layer deployment: global (pactkit init) vs project (/project-init).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from pactkit.generators.deployer import deploy
|
|
8
|
+
from pactkit_opencode.deployer import OpenCodeDeployer
|
|
9
|
+
|
|
10
|
+
# ===========================================================================
|
|
11
|
+
# R1: Global deployment does NOT generate opencode.json
|
|
12
|
+
# ===========================================================================
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestR1NoGlobalOpencodeJson:
|
|
16
|
+
"""R1: pactkit init --format opencode creates opencode.json with instructions (STORY-071 R7).
|
|
17
|
+
|
|
18
|
+
Note: Previously (BUG-035), opencode.json was NOT created by global deployment.
|
|
19
|
+
STORY-071 R7 changed this: global deployment now creates opencode.json with
|
|
20
|
+
instructions: ["rules/*.md"] for modular rule loading.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def test_global_deploy_has_opencode_json(self, tmp_path):
|
|
24
|
+
"""opencode.json IS created in global deployment (STORY-071 R7)."""
|
|
25
|
+
out = tmp_path / "opencode-global"
|
|
26
|
+
deploy(format="opencode", target=str(out))
|
|
27
|
+
assert (out / "opencode.json").is_file()
|
|
28
|
+
|
|
29
|
+
def test_global_deploy_has_agents_md(self, tmp_path):
|
|
30
|
+
"""AGENTS.md IS created in global deployment."""
|
|
31
|
+
out = tmp_path / "opencode-global"
|
|
32
|
+
deploy(format="opencode", target=str(out))
|
|
33
|
+
assert (out / "AGENTS.md").is_file()
|
|
34
|
+
|
|
35
|
+
def test_global_deploy_has_agents_dir(self, tmp_path):
|
|
36
|
+
"""agents/ IS created in global deployment."""
|
|
37
|
+
out = tmp_path / "opencode-global"
|
|
38
|
+
deploy(format="opencode", target=str(out))
|
|
39
|
+
assert (out / "agents").is_dir()
|
|
40
|
+
|
|
41
|
+
def test_global_deploy_has_commands_dir(self, tmp_path):
|
|
42
|
+
"""commands/ IS created in global deployment."""
|
|
43
|
+
out = tmp_path / "opencode-global"
|
|
44
|
+
deploy(format="opencode", target=str(out))
|
|
45
|
+
assert (out / "commands").is_dir()
|
|
46
|
+
|
|
47
|
+
def test_global_deploy_has_skills_dir(self, tmp_path):
|
|
48
|
+
"""skills/ IS created in global deployment."""
|
|
49
|
+
out = tmp_path / "opencode-global"
|
|
50
|
+
deploy(format="opencode", target=str(out))
|
|
51
|
+
assert (out / "skills").is_dir()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ===========================================================================
|
|
55
|
+
# R3/R4: /project-init playbook contains OpenCode detection
|
|
56
|
+
# ===========================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class TestR3ProjectInitOpencodeDetection:
|
|
60
|
+
"""R3: /project-init playbook must detect OpenCode environment."""
|
|
61
|
+
|
|
62
|
+
def test_project_init_contains_opencode_detection(self):
|
|
63
|
+
"""project-init.md contains OpenCode environment detection instructions."""
|
|
64
|
+
from pactkit.prompts.commands import COMMANDS_CONTENT
|
|
65
|
+
|
|
66
|
+
content = COMMANDS_CONTENT["project-init.md"]
|
|
67
|
+
assert "opencode" in content.lower() or "OpenCode" in content
|
|
68
|
+
|
|
69
|
+
def test_project_init_contains_opencode_json_generation(self):
|
|
70
|
+
"""project-init.md contains opencode.json generation instructions."""
|
|
71
|
+
from pactkit.prompts.commands import COMMANDS_CONTENT
|
|
72
|
+
|
|
73
|
+
content = COMMANDS_CONTENT["project-init.md"]
|
|
74
|
+
assert "opencode.json" in content
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ===========================================================================
|
|
78
|
+
# R5: opencode.json structure (helper function still exists for project-init)
|
|
79
|
+
# ===========================================================================
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestR5OpencodeJsonStructure:
|
|
83
|
+
"""R5: _deploy_opencode_json helper produces correct structure."""
|
|
84
|
+
|
|
85
|
+
def test_opencode_json_helper_exists(self):
|
|
86
|
+
"""_deploy_opencode_json function still exists for project-init use."""
|
|
87
|
+
assert callable(OpenCodeDeployer._deploy_opencode_json)
|
|
88
|
+
|
|
89
|
+
def test_opencode_json_has_schema(self, tmp_path):
|
|
90
|
+
"""opencode.json contains $schema field."""
|
|
91
|
+
import json
|
|
92
|
+
|
|
93
|
+
OpenCodeDeployer._deploy_opencode_json(tmp_path)
|
|
94
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
95
|
+
assert "$schema" in data
|
|
96
|
+
assert "opencode.ai" in data["$schema"]
|
|
97
|
+
|
|
98
|
+
def test_opencode_json_has_instructions(self, tmp_path):
|
|
99
|
+
"""opencode.json contains instructions field."""
|
|
100
|
+
import json
|
|
101
|
+
|
|
102
|
+
OpenCodeDeployer._deploy_opencode_json(tmp_path)
|
|
103
|
+
data = json.loads((tmp_path / "opencode.json").read_text())
|
|
104
|
+
assert "instructions" in data
|
|
105
|
+
assert isinstance(data["instructions"], list)
|