@haposoft/cafekit 0.16.0-rc.6 → 0.16.0-rc.8
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.
- package/README.md +21 -58
- package/bin/__tests__/change-firewall.semantic-firewall.test.js +4 -5
- package/bin/__tests__/codex-hooks.test.js +382 -0
- package/bin/__tests__/codex-native.test.js +579 -80
- package/bin/__tests__/develop-contract.test.js +155 -76
- package/bin/__tests__/installer-safety.test.js +19 -154
- package/bin/__tests__/package-inventory.test.js +494 -26
- package/bin/__tests__/release-preflight.semantic-firewall.test.js +2 -2
- package/bin/__tests__/validator-grounding.test.js +11 -11
- package/bin/install.js +2 -7
- package/bin/lib/context.js +0 -45
- package/bin/lib/copy-utils.js +1 -1
- package/bin/lib/i18n.js +0 -3
- package/bin/lib/manifest.js +1 -1
- package/bin/phases/claude-runtime.js +1 -3
- package/bin/phases/codex-runtime.js +4 -2
- package/bin/phases/copy-payload.js +3 -15
- package/bin/phases/post-install.js +5 -17
- package/bin/phases/root-config.js +2 -3
- package/bin/phases/select-platform.js +2 -5
- package/bin/phases/summary.js +0 -1
- package/package.json +2 -5
- package/src/claude/CLAUDE.md +19 -6
- package/src/claude/agents/project-manager.md +41 -9
- package/src/claude/agents/spec-maker.md +82 -155
- package/src/claude/gitignore +1 -1
- package/src/claude/hooks/__tests__/spec-gate.test.js +238 -0
- package/src/claude/hooks/__tests__/state.test.js +249 -3
- package/src/claude/hooks/lib/context.cjs +1 -1
- package/src/claude/hooks/lib/detect.cjs +1 -1
- package/src/claude/hooks/spec-gate.cjs +44 -9
- package/src/claude/hooks/spec-state.cjs +51 -13
- package/src/claude/hooks/task-scaffold-guard.cjs +1 -1
- package/src/claude/migration-manifest.json +17 -3
- package/src/claude/rules/manage-docs.md +43 -24
- package/src/claude/rules/state-sync.md +57 -62
- package/src/claude/rules/workflow.md +30 -13
- package/src/claude/scripts/change-firewall.cjs +2 -2
- package/src/claude/scripts/generate-skill-catalog.cjs +0 -1
- package/src/claude/scripts/spec-receipt.cjs +196 -1
- package/src/claude/scripts/spec-resolver.cjs +428 -0
- package/src/claude/skills/develop/SKILL.md +89 -125
- package/src/claude/skills/develop/references/parallel-waves.md +75 -162
- package/src/claude/skills/develop/references/quality-gate.md +61 -121
- package/src/claude/skills/develop/references/subagent-patterns.md +31 -38
- package/src/claude/skills/question/SKILL.md +1 -1
- package/src/claude/skills/specs/SKILL.md +152 -204
- package/src/claude/skills/specs/references/review.md +124 -243
- package/src/claude/skills/specs/references/templates.md +202 -0
- package/src/claude/skills/sync/SKILL.md +66 -90
- package/src/claude/skills/sync/references/sync-protocols.md +62 -83
- package/src/codex/AGENTS.md +20 -7
- package/src/codex/hooks/lib/spec-receipt.cjs +18 -0
- package/src/codex/hooks/lib/spec-utils.cjs +25 -0
- package/src/codex/hooks/spec-gate.cjs +46 -16
- package/src/codex/hooks/spec-state.cjs +54 -16
- package/src/codex/rules/state-sync.md +57 -61
- package/src/common/AGENTS.md +8 -1
- package/bin/__tests__/opencode-spec-gate.test.js +0 -799
- package/bin/lib/opencode-install.js +0 -899
- package/bin/phases/opencode-runtime.js +0 -38
- package/src/claude/skills/specs/references/archive-workflow.md +0 -74
- package/src/claude/skills/specs/references/ask-user-question-gates.md +0 -70
- package/src/claude/skills/specs/references/codebase-analysis.md +0 -102
- package/src/claude/skills/specs/references/cross-spec-dependency.md +0 -76
- package/src/claude/skills/specs/references/research-strategy.md +0 -107
- package/src/claude/skills/specs/references/scope-inquiry.md +0 -107
- package/src/claude/skills/specs/references/translation-mirror.md +0 -83
- package/src/claude/skills/specs/rules/design-discovery-full.md +0 -45
- package/src/claude/skills/specs/rules/design-discovery-light.md +0 -28
- package/src/claude/skills/specs/rules/design-principles.md +0 -138
- package/src/claude/skills/specs/rules/design-review.md +0 -55
- package/src/claude/skills/specs/rules/ears-format.md +0 -42
- package/src/claude/skills/specs/rules/phase-decision-matrix.md +0 -56
- package/src/claude/skills/specs/rules/task-scoring-rubric.md +0 -52
- package/src/claude/skills/specs/rules/tasks-generation.md +0 -189
- package/src/opencode/AGENTS.md +0 -41
- package/src/opencode/plugins/docs-sync.ts +0 -175
- package/src/opencode/plugins/inspect-block.ts +0 -164
- package/src/opencode/plugins/privacy-block.ts +0 -182
- package/src/opencode/plugins/rules.ts +0 -155
- package/src/opencode/plugins/session.ts +0 -179
- package/src/opencode/plugins/spec-gate.ts +0 -457
- package/src/opencode/plugins/spec-state.ts +0 -311
- package/src/opencode/plugins/state.ts +0 -253
- package/src/opencode/plugins/task-scaffold-guard.ts +0 -161
- package/src/opencode/plugins/usage.ts +0 -178
|
@@ -1,899 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const { copyRecursive, isTextAsset, isGeneratedArtifact, normalizeSourcePaths } = require('./copy-utils');
|
|
7
|
-
const { preserveAddressingSection } = require('./instruction-blocks');
|
|
8
|
-
|
|
9
|
-
const SPEC_ROOT = path.resolve(__dirname, '..', '..');
|
|
10
|
-
const OPENCODE_FOLDER = '.opencode';
|
|
11
|
-
const OPENCODE_PLUGIN_PACKAGE = '@opencode-ai/plugin';
|
|
12
|
-
const OPENCODE_PLUGIN_VERSION = '^1.15.11';
|
|
13
|
-
|
|
14
|
-
const OPENCODE_TOOL_MAP = {
|
|
15
|
-
Read: 'read',
|
|
16
|
-
Glob: 'glob',
|
|
17
|
-
Grep: 'grep',
|
|
18
|
-
Edit: 'edit',
|
|
19
|
-
Write: 'edit',
|
|
20
|
-
MultiEdit: 'edit',
|
|
21
|
-
Bash: 'bash',
|
|
22
|
-
WebFetch: 'webfetch',
|
|
23
|
-
WebSearch: 'websearch',
|
|
24
|
-
NotebookEdit: 'edit'
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const OPENCODE_COMMAND_TEMPLATES = [
|
|
28
|
-
{
|
|
29
|
-
fileName: 'brainstorm.md',
|
|
30
|
-
skillName: 'brainstorm',
|
|
31
|
-
agent: 'brainstormer',
|
|
32
|
-
subtask: true,
|
|
33
|
-
description: 'Explore and narrow an idea before writing a CafeKit spec.'
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
fileName: 'question.md',
|
|
37
|
-
skillName: 'question',
|
|
38
|
-
agent: 'inspector',
|
|
39
|
-
subtask: true,
|
|
40
|
-
description: 'Answer questions using repo evidence first, then external sources when needed.'
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
fileName: 'specs.md',
|
|
44
|
-
skillName: 'specs',
|
|
45
|
-
agent: 'spec-maker',
|
|
46
|
-
subtask: true,
|
|
47
|
-
description: 'Create, update, validate, or approve a CafeKit specification.'
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
fileName: 'develop.md',
|
|
51
|
-
skillName: 'develop',
|
|
52
|
-
agent: 'implementer',
|
|
53
|
-
subtask: true,
|
|
54
|
-
description: 'Implement approved CafeKit spec tasks with scope fidelity.'
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
fileName: 'test.md',
|
|
58
|
-
skillName: 'test',
|
|
59
|
-
agent: 'test-runner',
|
|
60
|
-
subtask: true,
|
|
61
|
-
description: 'Run CafeKit verification, QA, and evidence collection.'
|
|
62
|
-
},
|
|
63
|
-
{
|
|
64
|
-
fileName: 'code-review.md',
|
|
65
|
-
skillName: 'code-review',
|
|
66
|
-
agent: 'code-auditor',
|
|
67
|
-
subtask: true,
|
|
68
|
-
description: 'Review implementation against code quality and spec compliance.'
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
fileName: 'debug.md',
|
|
72
|
-
skillName: 'debug',
|
|
73
|
-
agent: 'debugger',
|
|
74
|
-
subtask: true,
|
|
75
|
-
description: 'Diagnose a bug or failure with evidence before fixing.'
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
fileName: 'hotfix.md',
|
|
79
|
-
skillName: 'hotfix',
|
|
80
|
-
agent: 'implementer',
|
|
81
|
-
subtask: true,
|
|
82
|
-
description: 'Apply a scout-first, narrow production hotfix.'
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
fileName: 'docs.md',
|
|
86
|
-
skillName: 'docs',
|
|
87
|
-
agent: 'docs-keeper',
|
|
88
|
-
subtask: true,
|
|
89
|
-
description: 'Create, update, or reconstruct source-backed project documentation.'
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
fileName: 'inspect.md',
|
|
93
|
-
skillName: 'inspect',
|
|
94
|
-
agent: 'inspector',
|
|
95
|
-
subtask: true,
|
|
96
|
-
description: 'Inspect a codebase, artifact, or external context before planning.'
|
|
97
|
-
},
|
|
98
|
-
// Domain skills — wrappers so users can invoke them directly via slash
|
|
99
|
-
// commands. Each delegates to the matching skill under .opencode/skills/.
|
|
100
|
-
{
|
|
101
|
-
fileName: 'agent-browser.md',
|
|
102
|
-
skillName: 'agent-browser',
|
|
103
|
-
agent: 'researcher',
|
|
104
|
-
subtask: true,
|
|
105
|
-
description: 'Drive a browser session for research, snapshots, or scripted flows.'
|
|
106
|
-
},
|
|
107
|
-
{
|
|
108
|
-
fileName: 'ai-multimodal.md',
|
|
109
|
-
skillName: 'ai-multimodal',
|
|
110
|
-
agent: 'researcher',
|
|
111
|
-
subtask: true,
|
|
112
|
-
description: 'Analyze images, audio, video, or mixed-media artifacts.'
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
fileName: 'backend-development.md',
|
|
116
|
-
skillName: 'backend-development',
|
|
117
|
-
agent: 'implementer',
|
|
118
|
-
subtask: true,
|
|
119
|
-
description: 'Implement backend services, APIs, data layers, and integrations.'
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
fileName: 'chrome-devtools.md',
|
|
123
|
-
skillName: 'chrome-devtools',
|
|
124
|
-
agent: 'debugger',
|
|
125
|
-
subtask: true,
|
|
126
|
-
description: 'Collect Chrome DevTools or Puppeteer evidence for web debugging.'
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
fileName: 'devops.md',
|
|
130
|
-
skillName: 'devops',
|
|
131
|
-
agent: 'deployer',
|
|
132
|
-
subtask: true,
|
|
133
|
-
description: 'Plan or execute deployment, CI/CD, container, and infrastructure work.'
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
fileName: 'docx.md',
|
|
137
|
-
skillName: 'docx',
|
|
138
|
-
agent: 'researcher',
|
|
139
|
-
subtask: true,
|
|
140
|
-
description: 'Create, edit, or analyze Word (.docx) documents.'
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
fileName: 'frontend-design.md',
|
|
144
|
-
skillName: 'frontend-design',
|
|
145
|
-
agent: 'ui-ux-designer',
|
|
146
|
-
subtask: true,
|
|
147
|
-
description: 'Define UI style, layout, design system, and UX direction.'
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
fileName: 'frontend-development.md',
|
|
151
|
-
skillName: 'frontend-development',
|
|
152
|
-
agent: 'implementer',
|
|
153
|
-
subtask: true,
|
|
154
|
-
description: 'Build React/TypeScript UI components and frontend features.'
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
fileName: 'git.md',
|
|
158
|
-
skillName: 'git',
|
|
159
|
-
agent: 'git-ops',
|
|
160
|
-
subtask: true,
|
|
161
|
-
description: 'Run git workflows: branch, commit, tag, push, or release prep.'
|
|
162
|
-
},
|
|
163
|
-
{
|
|
164
|
-
fileName: 'mobile-development.md',
|
|
165
|
-
skillName: 'mobile-development',
|
|
166
|
-
agent: 'implementer',
|
|
167
|
-
subtask: true,
|
|
168
|
-
description: 'Implement iOS, Android, React Native, or Flutter mobile features.'
|
|
169
|
-
},
|
|
170
|
-
{
|
|
171
|
-
fileName: 'pdf.md',
|
|
172
|
-
skillName: 'pdf',
|
|
173
|
-
agent: 'researcher',
|
|
174
|
-
subtask: true,
|
|
175
|
-
description: 'Read, edit, extract from, or generate PDF documents.'
|
|
176
|
-
},
|
|
177
|
-
{
|
|
178
|
-
fileName: 'pptx.md',
|
|
179
|
-
skillName: 'pptx',
|
|
180
|
-
agent: 'researcher',
|
|
181
|
-
subtask: true,
|
|
182
|
-
description: 'Create or edit PowerPoint (.pptx) slide decks.'
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
fileName: 'react-best-practices.md',
|
|
186
|
-
skillName: 'react-best-practices',
|
|
187
|
-
agent: 'code-auditor',
|
|
188
|
-
subtask: true,
|
|
189
|
-
description: 'Apply React/Next.js performance and rerender best practices.'
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
fileName: 'research.md',
|
|
193
|
-
skillName: 'research',
|
|
194
|
-
agent: 'researcher',
|
|
195
|
-
subtask: true,
|
|
196
|
-
description: 'Investigate external tools, docs, or best practices.'
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
fileName: 'sync.md',
|
|
200
|
-
skillName: 'sync',
|
|
201
|
-
agent: 'docs-keeper',
|
|
202
|
-
subtask: true,
|
|
203
|
-
description: 'Reconcile spec, docs, and code state after task completion.'
|
|
204
|
-
},
|
|
205
|
-
{
|
|
206
|
-
fileName: 'ui-ux-pro-max.md',
|
|
207
|
-
skillName: 'ui-ux-pro-max',
|
|
208
|
-
agent: 'ui-ux-designer',
|
|
209
|
-
subtask: true,
|
|
210
|
-
description: 'Apply advanced UI/UX intelligence and visual polish guidance.'
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
fileName: 'web-testing.md',
|
|
214
|
-
skillName: 'web-testing',
|
|
215
|
-
agent: 'test-runner',
|
|
216
|
-
subtask: true,
|
|
217
|
-
description: 'Plan or run E2E, integration, accessibility, or load testing.'
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
fileName: 'xlsx.md',
|
|
221
|
-
skillName: 'xlsx',
|
|
222
|
-
agent: 'researcher',
|
|
223
|
-
subtask: true,
|
|
224
|
-
description: 'Read, edit, or generate Excel (.xlsx) and CSV spreadsheets.'
|
|
225
|
-
}
|
|
226
|
-
];
|
|
227
|
-
|
|
228
|
-
function splitFrontmatter(content) {
|
|
229
|
-
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
230
|
-
|
|
231
|
-
if (!match) {
|
|
232
|
-
return {
|
|
233
|
-
frontmatter: {},
|
|
234
|
-
body: content
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const frontmatter = {};
|
|
239
|
-
match[1].split(/\r?\n/).forEach(line => {
|
|
240
|
-
const separatorIndex = line.indexOf(':');
|
|
241
|
-
if (separatorIndex === -1) return;
|
|
242
|
-
|
|
243
|
-
const key = line.slice(0, separatorIndex).trim();
|
|
244
|
-
let value = line.slice(separatorIndex + 1).trim();
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
248
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
249
|
-
) {
|
|
250
|
-
value = value.slice(1, -1);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (key) {
|
|
254
|
-
frontmatter[key] = value;
|
|
255
|
-
}
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
return {
|
|
259
|
-
frontmatter,
|
|
260
|
-
body: content.slice(match[0].length)
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function formatOpenCodeValue(value) {
|
|
265
|
-
return JSON.stringify(String(value || ''));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Rewrite Claude-runtime path references to OpenCode equivalents.
|
|
269
|
-
// Applied to every text asset copied into .opencode/ so the installed
|
|
270
|
-
// content points at the self-contained OpenCode runtime layout.
|
|
271
|
-
function normalizeOpenCodeBody(content) {
|
|
272
|
-
return normalizeSourcePaths(content, {
|
|
273
|
-
runtimeRoot: '.opencode',
|
|
274
|
-
skillsRoot: '.opencode/skills'
|
|
275
|
-
})
|
|
276
|
-
.replace(/(?<!~\/)\.claude\/(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode/$1')
|
|
277
|
-
.replace(/(?<!~\\)\.claude\\(agents|commands|skills|rules|scripts|references|hooks)\b/g, '.opencode\\$1')
|
|
278
|
-
.replace(/\.claude\/runtime\.json/g, '.opencode/runtime.json')
|
|
279
|
-
.replace(/\.claude\/ROUTING\.md/g, '.opencode/ROUTING.md')
|
|
280
|
-
.replace(/\.claude\/\.env/g, '.opencode/.env')
|
|
281
|
-
.replace(/(['"])\.claude\1/g, '$1.opencode$1')
|
|
282
|
-
.replace(/`\.claude`/g, '`.opencode`')
|
|
283
|
-
.replace(/\bCLAUDE\.md\b/g, 'AGENTS.md')
|
|
284
|
-
// Runtime artifact paths inside .opencode/.gitignore that originate from
|
|
285
|
-
// the Claude template (hooks/.logs/ → plugins/.logs/).
|
|
286
|
-
.replace(/\bhooks\/\.logs\b/g, 'plugins/.logs')
|
|
287
|
-
// OpenCode skill name regex strips the Claude-only `hapo:` prefix.
|
|
288
|
-
.replace(/^(\s*name:\s*)hapo:([a-z0-9-]+)\s*$/gm, '$1$2');
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function getOpenCodeCopyOptions(baseOptions = {}) {
|
|
292
|
-
return { ...baseOptions, transform: normalizeOpenCodeBody };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function getOpenCodeToolEntries(toolsValue) {
|
|
296
|
-
const toolNames = String(toolsValue || '')
|
|
297
|
-
.split(',')
|
|
298
|
-
.map(tool => tool.trim())
|
|
299
|
-
.filter(Boolean);
|
|
300
|
-
|
|
301
|
-
const mappedTools = new Set();
|
|
302
|
-
|
|
303
|
-
toolNames.forEach(toolName => {
|
|
304
|
-
const mapped = OPENCODE_TOOL_MAP[toolName];
|
|
305
|
-
if (mapped) {
|
|
306
|
-
mappedTools.add(mapped);
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
return Array.from(mappedTools).sort();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function convertOpenCodeAgentContent(content, fileName) {
|
|
314
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
315
|
-
const agentName = frontmatter.name || path.basename(fileName, '.md');
|
|
316
|
-
const description = frontmatter.description || `CafeKit agent: ${agentName}`;
|
|
317
|
-
const mode = agentName === 'brainstormer' ? 'primary' : 'subagent';
|
|
318
|
-
const permissionEntries = new Set(getOpenCodeToolEntries(frontmatter.tools));
|
|
319
|
-
permissionEntries.add('skill');
|
|
320
|
-
permissionEntries.add('task');
|
|
321
|
-
const lines = [
|
|
322
|
-
'---',
|
|
323
|
-
`description: ${formatOpenCodeValue(description)}`,
|
|
324
|
-
`mode: ${mode}`
|
|
325
|
-
];
|
|
326
|
-
|
|
327
|
-
if (permissionEntries.size > 0) {
|
|
328
|
-
lines.push('permission:');
|
|
329
|
-
Array.from(permissionEntries).sort().forEach(permissionName => {
|
|
330
|
-
lines.push(` ${permissionName}: allow`);
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
lines.push('---', '');
|
|
335
|
-
|
|
336
|
-
return `${lines.join('\n')}${normalizeOpenCodeBody(body).trimStart()}`;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function convertOpenCodeCommandContent(content, fileName) {
|
|
340
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
341
|
-
const commandName = path.basename(fileName, '.md');
|
|
342
|
-
const description = frontmatter.description || `CafeKit command: ${commandName}`;
|
|
343
|
-
const lines = [
|
|
344
|
-
'---',
|
|
345
|
-
`description: ${formatOpenCodeValue(description)}`
|
|
346
|
-
];
|
|
347
|
-
|
|
348
|
-
if (frontmatter.agent) {
|
|
349
|
-
lines.push(`agent: ${formatOpenCodeValue(frontmatter.agent)}`);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
lines.push('---', '');
|
|
353
|
-
|
|
354
|
-
return `${lines.join('\n')}${normalizeOpenCodeBody(body).trimStart()}`;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function createOpenCodeSkillCommandContent(command) {
|
|
358
|
-
const lines = [
|
|
359
|
-
'---',
|
|
360
|
-
`description: ${formatOpenCodeValue(command.description)}`
|
|
361
|
-
];
|
|
362
|
-
|
|
363
|
-
if (command.agent) {
|
|
364
|
-
lines.push(`agent: ${formatOpenCodeValue(command.agent)}`);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
if (typeof command.subtask === 'boolean') {
|
|
368
|
-
lines.push(`subtask: ${command.subtask ? 'true' : 'false'}`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
if (command.model) {
|
|
372
|
-
lines.push(`model: ${formatOpenCodeValue(command.model)}`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
lines.push(
|
|
376
|
-
'---',
|
|
377
|
-
'',
|
|
378
|
-
`Use the CafeKit skill at \`.opencode/skills/${command.skillName}/SKILL.md\`.`,
|
|
379
|
-
'',
|
|
380
|
-
'Read that skill first, then execute its workflow with these arguments:',
|
|
381
|
-
'',
|
|
382
|
-
'$ARGUMENTS',
|
|
383
|
-
'',
|
|
384
|
-
'If the skill references supporting files, resolve them relative to the skill directory.',
|
|
385
|
-
'Keep the response aligned with the skill instructions and the current project context.',
|
|
386
|
-
''
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
return lines.join('\n');
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function createOpenCodeAgentsContent(sourceContent) {
|
|
393
|
-
const adaptedContent = normalizeOpenCodeBody(sourceContent)
|
|
394
|
-
.replace(/^# CLAUDE\.md/m, '# AGENTS.md')
|
|
395
|
-
.replace(
|
|
396
|
-
'Primary operating instructions for Claude Code or any AI agent using this CafeKit runtime.',
|
|
397
|
-
'Primary operating instructions for OpenCode or any AI agent using this CafeKit runtime.'
|
|
398
|
-
)
|
|
399
|
-
.replace(/Claude Code/g, 'OpenCode');
|
|
400
|
-
|
|
401
|
-
const openCodeRuntimeBlock = [
|
|
402
|
-
'## OpenCode Runtime Mapping',
|
|
403
|
-
'',
|
|
404
|
-
'- OpenCode project instructions live in `AGENTS.md`.',
|
|
405
|
-
'- CafeKit commands live in `.opencode/commands/` and can be run as OpenCode slash commands.',
|
|
406
|
-
'- CafeKit subagents live in `.opencode/agents/` using OpenCode frontmatter.',
|
|
407
|
-
'- CafeKit skills live in `.opencode/skills/` and are read natively by OpenCode.',
|
|
408
|
-
'- CafeKit support files live in `.opencode/rules/`, `.opencode/scripts/`, `.opencode/references/`, and `.opencode/runtime.json`.',
|
|
409
|
-
'- OpenCode config is merged into `opencode.json`; keep project-specific model/provider choices there.',
|
|
410
|
-
''
|
|
411
|
-
].join('\n');
|
|
412
|
-
|
|
413
|
-
return adaptedContent.replace('## Core Objective', `${openCodeRuntimeBlock}\n## Core Objective`);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function managedOpenCodeRange(content) {
|
|
417
|
-
const existing = String(content || '');
|
|
418
|
-
const startMarker = '<!-- CAFEKIT OPENCODE START -->';
|
|
419
|
-
const endMarker = '<!-- CAFEKIT OPENCODE END -->';
|
|
420
|
-
const start = existing.indexOf(startMarker);
|
|
421
|
-
const end = existing.indexOf(endMarker);
|
|
422
|
-
if (start < 0 && end < 0) return null;
|
|
423
|
-
const duplicateStart = existing.indexOf(startMarker, start + startMarker.length);
|
|
424
|
-
const duplicateEnd = existing.indexOf(endMarker, end + endMarker.length);
|
|
425
|
-
if (start < 0 || end <= start || duplicateStart >= 0 || duplicateEnd >= 0) return false;
|
|
426
|
-
return {
|
|
427
|
-
start,
|
|
428
|
-
end: end + endMarker.length,
|
|
429
|
-
bodyStart: start + startMarker.length,
|
|
430
|
-
bodyEnd: end
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
436
|
-
const start = '<!-- CAFEKIT OPENCODE START -->';
|
|
437
|
-
const end = '<!-- CAFEKIT OPENCODE END -->';
|
|
438
|
-
let body = String(blockContent).trim();
|
|
439
|
-
// Reinstall replaces the managed block in place. If the new template dropped
|
|
440
|
-
// its Addressing section, carry the exact section over from the existing
|
|
441
|
-
// managed block so the saved address survives for setupAddressing. Only reads
|
|
442
|
-
// the managed body — never user-owned sections outside the block.
|
|
443
|
-
const range = managedOpenCodeRange(existingContent);
|
|
444
|
-
if (range) {
|
|
445
|
-
const existingBody = existingContent.slice(range.bodyStart, range.bodyEnd);
|
|
446
|
-
body = preserveAddressingSection(body, existingBody);
|
|
447
|
-
}
|
|
448
|
-
const managedBlock = `${start}\n${body}\n${end}`;
|
|
449
|
-
if (range === false) return existingContent;
|
|
450
|
-
if (range) {
|
|
451
|
-
return `${existingContent.slice(0, range.start)}${managedBlock}${existingContent.slice(range.end)}`;
|
|
452
|
-
}
|
|
453
|
-
if (!existingContent) return `${managedBlock}\n`;
|
|
454
|
-
// Pre-managed-block releases wrote the shipped template as the whole file.
|
|
455
|
-
// Wrap only an exact shipped prefix and preserve any user/Codex suffix.
|
|
456
|
-
if (existingContent.startsWith(blockContent)) {
|
|
457
|
-
return `${managedBlock}${existingContent.slice(blockContent.length)}`;
|
|
458
|
-
}
|
|
459
|
-
const trimmedLegacy = blockContent.trimEnd();
|
|
460
|
-
if (
|
|
461
|
-
existingContent.startsWith(trimmedLegacy)
|
|
462
|
-
&& ['', '\n', '\r'].includes(existingContent.charAt(trimmedLegacy.length))
|
|
463
|
-
) {
|
|
464
|
-
return `${managedBlock}${existingContent.slice(trimmedLegacy.length)}`;
|
|
465
|
-
}
|
|
466
|
-
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
467
|
-
return `${existingContent}${separator}${managedBlock}\n`;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
/** Transform only the CafeKit-owned OpenCode body, preserving surrounding bytes. */
|
|
471
|
-
function transformManagedOpenCodeContent(content, transform) {
|
|
472
|
-
const existing = String(content || '');
|
|
473
|
-
const range = managedOpenCodeRange(existing);
|
|
474
|
-
if (!range || typeof transform !== 'function') return existing;
|
|
475
|
-
const body = existing.slice(range.bodyStart, range.bodyEnd);
|
|
476
|
-
return `${existing.slice(0, range.bodyStart)}${transform(body)}${existing.slice(range.bodyEnd)}`;
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
function getOpenCodeAgentsTemplateContent() {
|
|
480
|
-
const openCodeTemplate = path.join(SPEC_ROOT, 'src/opencode/AGENTS.md');
|
|
481
|
-
if (!fs.existsSync(openCodeTemplate)) return null;
|
|
482
|
-
return normalizeOpenCodeBody(fs.readFileSync(openCodeTemplate, 'utf8'));
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
// Copy OpenCode project instructions to AGENTS.md at the project root.
|
|
486
|
-
function copyOpenCodeAgentsMdFile(platformKey, results, options = {}) {
|
|
487
|
-
if (platformKey !== 'opencode') return;
|
|
488
|
-
|
|
489
|
-
const dest = 'AGENTS.md';
|
|
490
|
-
const content = getOpenCodeAgentsTemplateContent();
|
|
491
|
-
|
|
492
|
-
if (content) {
|
|
493
|
-
const destinationExists = fs.existsSync(dest);
|
|
494
|
-
const existingContent = destinationExists ? fs.readFileSync(dest, 'utf8') : '';
|
|
495
|
-
if (managedOpenCodeRange(existingContent) === false) {
|
|
496
|
-
console.log(` ⚠ AGENTS.md: malformed CafeKit OPENCODE marker topology; preserved ${dest} without writing`);
|
|
497
|
-
results.errors++;
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
const nextContent = upsertCafeKitAgentsBlock(existingContent, content);
|
|
501
|
-
|
|
502
|
-
if (nextContent === existingContent) {
|
|
503
|
-
console.log(` → Skipped: AGENTS.md (already up to date)`);
|
|
504
|
-
results.skipped++;
|
|
505
|
-
} else if (destinationExists) {
|
|
506
|
-
fs.writeFileSync(dest, nextContent, 'utf8');
|
|
507
|
-
console.log(` ↻ AGENTS.md merged`);
|
|
508
|
-
results.updated++;
|
|
509
|
-
} else {
|
|
510
|
-
fs.writeFileSync(dest, nextContent, 'utf8');
|
|
511
|
-
console.log(` ✓ AGENTS.md installed`);
|
|
512
|
-
results.copied++;
|
|
513
|
-
}
|
|
514
|
-
} else {
|
|
515
|
-
console.log(` ⚠ AGENTS.md template source not found`);
|
|
516
|
-
results.missingDependencies++;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
function copyOpenCodeSharedRuntimeFiles(platformKey, results, options = {}) {
|
|
521
|
-
if (platformKey !== 'opencode') return;
|
|
522
|
-
|
|
523
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
524
|
-
const srcBase = path.join(SPEC_ROOT, 'src/claude');
|
|
525
|
-
const runtimeFiles = ['gitignore', 'runtime.json'];
|
|
526
|
-
|
|
527
|
-
runtimeFiles.forEach(relPath => {
|
|
528
|
-
const srcPath = path.join(srcBase, relPath);
|
|
529
|
-
const targetRelPath = relPath === 'gitignore' ? '.gitignore' : relPath;
|
|
530
|
-
const targetPath = path.join(OPENCODE_FOLDER, targetRelPath);
|
|
531
|
-
|
|
532
|
-
if (!fs.existsSync(srcPath)) {
|
|
533
|
-
console.log(` ⚠ Runtime file not found: ${relPath}`);
|
|
534
|
-
results.missingDependencies++;
|
|
535
|
-
return;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const targetExists = fs.existsSync(targetPath);
|
|
539
|
-
const shouldCopy = shouldOverwriteManagedFiles || !targetExists;
|
|
540
|
-
|
|
541
|
-
if (shouldCopy) {
|
|
542
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
543
|
-
const sourceContent = fs.readFileSync(srcPath, 'utf8');
|
|
544
|
-
fs.writeFileSync(targetPath, normalizeOpenCodeBody(sourceContent), 'utf8');
|
|
545
|
-
|
|
546
|
-
if (targetExists) {
|
|
547
|
-
console.log(` ↻ Shared runtime updated: ${targetRelPath}`);
|
|
548
|
-
results.updated++;
|
|
549
|
-
} else {
|
|
550
|
-
console.log(` ✓ Shared runtime installed: ${targetRelPath}`);
|
|
551
|
-
results.copied++;
|
|
552
|
-
}
|
|
553
|
-
} else {
|
|
554
|
-
results.skipped++;
|
|
555
|
-
}
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function copyOpenCodePlugins(platformKey, results, options = {}) {
|
|
560
|
-
if (platformKey !== 'opencode') return;
|
|
561
|
-
|
|
562
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
563
|
-
const sourceDir = path.join(SPEC_ROOT, 'src/opencode/plugins');
|
|
564
|
-
const targetDir = path.join(OPENCODE_FOLDER, 'plugins');
|
|
565
|
-
|
|
566
|
-
if (!fs.existsSync(sourceDir)) {
|
|
567
|
-
console.log(' ⚠ OpenCode plugin source not found, skipping');
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
572
|
-
|
|
573
|
-
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
|
|
574
|
-
entries.forEach(entry => {
|
|
575
|
-
// Skip generated artifacts before any directory/file handling, matching
|
|
576
|
-
// the filter copyRecursive applies to directory children.
|
|
577
|
-
if (isGeneratedArtifact(entry.name)) return;
|
|
578
|
-
|
|
579
|
-
const sourcePath = path.join(sourceDir, entry.name);
|
|
580
|
-
const destPath = path.join(targetDir, entry.name);
|
|
581
|
-
|
|
582
|
-
if (entry.isDirectory()) {
|
|
583
|
-
copyRecursive(sourcePath, destPath, getOpenCodeCopyOptions(options));
|
|
584
|
-
console.log(` ✓ Plugin lib installed: ${entry.name}/`);
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
if (!entry.isFile()) return;
|
|
589
|
-
|
|
590
|
-
const destExists = fs.existsSync(destPath);
|
|
591
|
-
if (destExists && !shouldOverwriteManagedFiles) {
|
|
592
|
-
console.log(` → Plugin exists: ${entry.name}`);
|
|
593
|
-
results.skipped++;
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (isTextAsset(sourcePath)) {
|
|
598
|
-
const sourceContent = fs.readFileSync(sourcePath, 'utf8');
|
|
599
|
-
fs.writeFileSync(destPath, normalizeOpenCodeBody(sourceContent), 'utf8');
|
|
600
|
-
} else {
|
|
601
|
-
fs.copyFileSync(sourcePath, destPath);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
if (destExists) {
|
|
605
|
-
console.log(` ↻ Plugin updated: ${entry.name}`);
|
|
606
|
-
results.updated++;
|
|
607
|
-
} else {
|
|
608
|
-
console.log(` ✓ Plugin installed: ${entry.name}`);
|
|
609
|
-
results.copied++;
|
|
610
|
-
}
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
ensureOpenCodePluginPackageJson(results, shouldOverwriteManagedFiles);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function ensureOpenCodePluginPackageJson(results, shouldOverwriteManagedFiles) {
|
|
617
|
-
const pkgPath = path.join(OPENCODE_FOLDER, 'package.json');
|
|
618
|
-
const pkgExists = fs.existsSync(pkgPath);
|
|
619
|
-
|
|
620
|
-
let pkg = {};
|
|
621
|
-
if (pkgExists) {
|
|
622
|
-
try {
|
|
623
|
-
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
624
|
-
} catch {
|
|
625
|
-
pkg = {};
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
const currentDep = pkg.dependencies?.[OPENCODE_PLUGIN_PACKAGE];
|
|
630
|
-
const needsUpdate = !pkgExists
|
|
631
|
-
|| !currentDep
|
|
632
|
-
|| (shouldOverwriteManagedFiles && currentDep !== OPENCODE_PLUGIN_VERSION);
|
|
633
|
-
|
|
634
|
-
if (!needsUpdate) {
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
const nextPkg = {
|
|
639
|
-
name: pkg.name || 'cafekit-opencode-plugins',
|
|
640
|
-
private: pkg.private !== undefined ? pkg.private : true,
|
|
641
|
-
type: pkg.type || 'module',
|
|
642
|
-
...pkg,
|
|
643
|
-
dependencies: {
|
|
644
|
-
...(pkg.dependencies || {}),
|
|
645
|
-
[OPENCODE_PLUGIN_PACKAGE]: OPENCODE_PLUGIN_VERSION,
|
|
646
|
-
},
|
|
647
|
-
};
|
|
648
|
-
|
|
649
|
-
fs.writeFileSync(pkgPath, JSON.stringify(nextPkg, null, 2) + '\n', 'utf8');
|
|
650
|
-
|
|
651
|
-
if (pkgExists) {
|
|
652
|
-
console.log(` ↻ Plugin package.json updated (${OPENCODE_PLUGIN_PACKAGE} ${OPENCODE_PLUGIN_VERSION})`);
|
|
653
|
-
results.updated++;
|
|
654
|
-
} else {
|
|
655
|
-
console.log(` ✓ Plugin package.json installed (${OPENCODE_PLUGIN_PACKAGE} ${OPENCODE_PLUGIN_VERSION})`);
|
|
656
|
-
console.log(' OpenCode will run "bun install" automatically on startup.');
|
|
657
|
-
results.copied++;
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function copyOpenCodeCommandTemplates(platformKey, results, options = {}) {
|
|
662
|
-
if (platformKey !== 'opencode') return;
|
|
663
|
-
|
|
664
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
665
|
-
const commandsDir = path.join(OPENCODE_FOLDER, 'commands');
|
|
666
|
-
|
|
667
|
-
OPENCODE_COMMAND_TEMPLATES.forEach(command => {
|
|
668
|
-
const targetPath = path.join(commandsDir, command.fileName);
|
|
669
|
-
const targetExists = fs.existsSync(targetPath);
|
|
670
|
-
|
|
671
|
-
if (targetExists && !shouldOverwriteManagedFiles) {
|
|
672
|
-
console.log(` → OpenCode command exists: ${command.fileName}`);
|
|
673
|
-
results.skipped++;
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
678
|
-
fs.writeFileSync(targetPath, createOpenCodeSkillCommandContent(command), 'utf8');
|
|
679
|
-
|
|
680
|
-
if (targetExists) {
|
|
681
|
-
console.log(` ↻ OpenCode command updated: ${command.fileName}`);
|
|
682
|
-
results.updated++;
|
|
683
|
-
} else {
|
|
684
|
-
console.log(` ✓ OpenCode command installed: ${command.fileName}`);
|
|
685
|
-
results.copied++;
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function readJsonWithComments(filePath) {
|
|
691
|
-
if (!fs.existsSync(filePath)) {
|
|
692
|
-
return {};
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
const rawContent = fs.readFileSync(filePath, 'utf8');
|
|
696
|
-
const withoutBlockComments = rawContent.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
697
|
-
const withoutLineComments = withoutBlockComments.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
698
|
-
|
|
699
|
-
// Empty opencode.json is a valid project marker; treat as empty object.
|
|
700
|
-
if (withoutLineComments.trim() === '') {
|
|
701
|
-
return {};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
return JSON.parse(withoutLineComments);
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
function getOpenCodeConfigPath() {
|
|
708
|
-
const jsonPath = 'opencode.json';
|
|
709
|
-
const jsoncPath = 'opencode.jsonc';
|
|
710
|
-
|
|
711
|
-
if (fs.existsSync(jsonPath)) {
|
|
712
|
-
return jsonPath;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
if (fs.existsSync(jsoncPath)) {
|
|
716
|
-
return jsoncPath;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return jsonPath;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function mergeOpenCodeConfig(platformKey, results) {
|
|
723
|
-
if (platformKey !== 'opencode') return;
|
|
724
|
-
|
|
725
|
-
const targetPath = getOpenCodeConfigPath();
|
|
726
|
-
const targetExists = fs.existsSync(targetPath);
|
|
727
|
-
let config = {};
|
|
728
|
-
|
|
729
|
-
try {
|
|
730
|
-
config = readJsonWithComments(targetPath);
|
|
731
|
-
} catch (error) {
|
|
732
|
-
console.log(` ⚠ OpenCode config not merged: ${error.message}`);
|
|
733
|
-
results.errors++;
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
const before = JSON.stringify(config);
|
|
738
|
-
|
|
739
|
-
if (!config.$schema) {
|
|
740
|
-
config.$schema = 'https://opencode.ai/config.json';
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
if (!Array.isArray(config.instructions)) {
|
|
744
|
-
config.instructions = [];
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
if (!config.instructions.includes('AGENTS.md')) {
|
|
748
|
-
config.instructions.push('AGENTS.md');
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
if (typeof config.permission === 'string') {
|
|
752
|
-
if (config.permission === 'allow') {
|
|
753
|
-
const afterStringPermission = JSON.stringify(config);
|
|
754
|
-
if (before === afterStringPermission && targetExists) {
|
|
755
|
-
console.log(` → OpenCode config already up to date: ${targetPath}`);
|
|
756
|
-
results.skipped++;
|
|
757
|
-
return;
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
761
|
-
console.log(` ↻ OpenCode config merged: ${targetPath}`);
|
|
762
|
-
results.updated++;
|
|
763
|
-
return;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
console.log(` ⚠ OpenCode config permission is "${config.permission}"; preserving user setting`);
|
|
767
|
-
} else if (!config.permission || typeof config.permission !== 'object' || Array.isArray(config.permission)) {
|
|
768
|
-
config.permission = {};
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (typeof config.permission === 'object' && !Array.isArray(config.permission)) {
|
|
772
|
-
if (!config.permission.skill || typeof config.permission.skill !== 'object' || Array.isArray(config.permission.skill)) {
|
|
773
|
-
config.permission.skill = {};
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
if (!config.permission.skill['*']) {
|
|
777
|
-
config.permission.skill['*'] = 'allow';
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
if (!config.permission.task || typeof config.permission.task !== 'object' || Array.isArray(config.permission.task)) {
|
|
781
|
-
config.permission.task = {};
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
if (!config.permission.task['*']) {
|
|
785
|
-
config.permission.task['*'] = 'allow';
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const after = JSON.stringify(config);
|
|
790
|
-
if (before === after && targetExists) {
|
|
791
|
-
console.log(` → OpenCode config already up to date: ${targetPath}`);
|
|
792
|
-
results.skipped++;
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
797
|
-
|
|
798
|
-
if (targetExists) {
|
|
799
|
-
console.log(` ↻ OpenCode config merged: ${targetPath}`);
|
|
800
|
-
results.updated++;
|
|
801
|
-
} else {
|
|
802
|
-
console.log(` ✓ OpenCode config created: ${targetPath}`);
|
|
803
|
-
results.copied++;
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function isValidOpenCodeModel(model) {
|
|
808
|
-
return typeof model === 'string' && /^[^/\s]+\/[^/\s]+$/.test(model.trim());
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function writeOpenCodeModel(model, results, sourceLabel = 'user input') {
|
|
812
|
-
const targetPath = getOpenCodeConfigPath();
|
|
813
|
-
let config = {};
|
|
814
|
-
|
|
815
|
-
try {
|
|
816
|
-
config = readJsonWithComments(targetPath);
|
|
817
|
-
} catch (error) {
|
|
818
|
-
console.log(` ⚠ OpenCode model not configured: ${error.message}`);
|
|
819
|
-
results.errors++;
|
|
820
|
-
return false;
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
if (typeof config.model === 'string' && config.model.trim()) {
|
|
824
|
-
console.log(` → OpenCode model already configured: ${config.model}`);
|
|
825
|
-
results.skipped++;
|
|
826
|
-
return false;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
config.model = model.trim();
|
|
830
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
831
|
-
console.log(` ✓ OpenCode model configured from ${sourceLabel}: ${config.model}`);
|
|
832
|
-
results.updated++;
|
|
833
|
-
return true;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
async function promptOpenCodeModel() {
|
|
837
|
-
const rl = readline.createInterface({
|
|
838
|
-
input: process.stdin,
|
|
839
|
-
output: process.stdout
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
console.log('\n📝 OpenCode Model Configuration');
|
|
843
|
-
console.log(' Optional but recommended for subagents.');
|
|
844
|
-
console.log(' Format: provider/model-id (example: anthropic/claude-sonnet-4-20250514)');
|
|
845
|
-
console.log();
|
|
846
|
-
|
|
847
|
-
return new Promise((resolve) => {
|
|
848
|
-
rl.question('Enter OpenCode model (or press Enter to skip): ', (answer) => {
|
|
849
|
-
rl.close();
|
|
850
|
-
resolve(answer.trim());
|
|
851
|
-
});
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
async function setupOpenCodeModel(platforms, results) {
|
|
856
|
-
if (!platforms.includes('opencode')) return;
|
|
857
|
-
|
|
858
|
-
const envModel = process.env.OPENCODE_MODEL || process.env.OPENCODE_DEFAULT_MODEL;
|
|
859
|
-
if (envModel) {
|
|
860
|
-
if (isValidOpenCodeModel(envModel)) {
|
|
861
|
-
writeOpenCodeModel(envModel, results, 'environment');
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
console.log(` ⚠ Ignoring invalid OpenCode model from environment: ${envModel}`);
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
const model = await promptOpenCodeModel();
|
|
869
|
-
|
|
870
|
-
if (!model) {
|
|
871
|
-
console.log(' ℹ Skipped OpenCode model configuration');
|
|
872
|
-
console.log(' Set later in opencode.json as: "model": "provider/model-id"');
|
|
873
|
-
return;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (!isValidOpenCodeModel(model)) {
|
|
877
|
-
console.log(' ⚠ Invalid OpenCode model format. Expected provider/model-id. Skipped.');
|
|
878
|
-
return;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
writeOpenCodeModel(model, results);
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
module.exports = {
|
|
885
|
-
OPENCODE_PLUGIN_PACKAGE,
|
|
886
|
-
OPENCODE_PLUGIN_VERSION,
|
|
887
|
-
normalizeOpenCodeBody,
|
|
888
|
-
getOpenCodeCopyOptions,
|
|
889
|
-
convertOpenCodeAgentContent,
|
|
890
|
-
convertOpenCodeCommandContent,
|
|
891
|
-
copyOpenCodeAgentsMdFile,
|
|
892
|
-
managedOpenCodeRange,
|
|
893
|
-
transformManagedOpenCodeContent,
|
|
894
|
-
copyOpenCodeSharedRuntimeFiles,
|
|
895
|
-
copyOpenCodePlugins,
|
|
896
|
-
copyOpenCodeCommandTemplates,
|
|
897
|
-
mergeOpenCodeConfig,
|
|
898
|
-
setupOpenCodeModel
|
|
899
|
-
};
|