ultra-builder-pro-cli 0.25.1 → 0.26.0
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/.ultra-template/contexts/TEMPLATE.md +87 -0
- package/.ultra-template/decisions/.gitkeep +1 -0
- package/.ultra-template/evidence/.gitkeep +1 -0
- package/.ultra-template/north-star.md +46 -0
- package/.ultra-template/research/.gitkeep +1 -0
- package/.ultra-template/specs/discovery.md +2 -2
- package/.ultra-template/tasks.json +3 -0
- package/.ultra-template/test-report.json +27 -0
- package/CHANGELOG.md +40 -0
- package/README.md +217 -579
- package/adapters/_shared/delegate-profiles/kimi-read-only.md +15 -0
- package/adapters/_shared/delegate-profiles/kimi-write.md +17 -0
- package/adapters/_shared/file-ops.cjs +76 -2
- package/adapters/_shared/host-profile.cjs +97 -0
- package/adapters/_shared/plugin-core.cjs +204 -0
- package/adapters/_shared/runtime-assets.cjs +31 -96
- package/adapters/claude.js +92 -291
- package/adapters/codex.js +309 -842
- package/adapters/grok.js +143 -1469
- package/adapters/kimi.js +124 -1053
- package/adapters/opencode.js +230 -742
- package/bin/delegate-worker.cjs +323 -0
- package/bin/delegate.cjs +456 -0
- package/bin/install.js +38 -11
- package/docs/ARCHITECTURE.md +122 -216
- package/docs/ARTIFACT-AUTHORITY.md +90 -268
- package/docs/DECISIONS.md +75 -87
- package/docs/PHILOSOPHY.md +177 -0
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +62 -39
- package/docs/RUNTIME-COMPAT-MATRIX.md +99 -156
- package/docs/SKILL-AUTHORING.md +94 -73
- package/docs/WORKFLOW-LIFECYCLE.md +93 -189
- package/hooks/README.md +17 -37
- package/hooks/_common.py +113 -0
- package/hooks/adapters/codex.py +9 -16
- package/hooks/adapters/grok.py +5 -5
- package/hooks/adapters/kimi.py +8 -14
- package/hooks/block_dangerous_commands.py +95 -0
- package/hooks/compact_context.py +65 -0
- package/hooks/mid_workflow_recall.py +25 -0
- package/hooks/post_edit_guard.py +105 -0
- package/hooks/session_context.py +30 -0
- package/package.json +17 -52
- package/skills/ultra-change/SKILL.md +97 -50
- package/skills/ultra-change/references/change-contract.md +40 -1
- package/skills/ultra-delegate/SKILL.md +82 -0
- package/skills/ultra-delegate/references/delegation-contract.md +51 -0
- package/skills/ultra-delegate/scripts/delegate_wait.py +86 -0
- package/skills/ultra-deliver/SKILL.md +61 -42
- package/skills/ultra-deliver/references/baseline-reconciliation.md +39 -20
- package/skills/ultra-dev/SKILL.md +96 -62
- package/skills/ultra-dev/references/debugging.md +22 -0
- package/skills/ultra-domain-modeling/SKILL.md +79 -0
- package/skills/ultra-grilling/SKILL.md +79 -0
- package/skills/ultra-init/SKILL.md +94 -37
- package/skills/ultra-init/references/brownfield-adoption.md +33 -83
- package/skills/ultra-init/scripts/init_project.cjs +84 -0
- package/skills/ultra-plan/SKILL.md +88 -68
- package/skills/ultra-research/SKILL.md +62 -48
- package/skills/ultra-research/references/00-problem-validation.md +5 -5
- package/skills/ultra-research/references/01-opportunity-discovery.md +3 -3
- package/skills/ultra-research/references/02-market-assessment.md +5 -4
- package/skills/ultra-research/references/03-competitive-landscape.md +3 -3
- package/skills/ultra-research/references/04-product-strategy.md +3 -3
- package/skills/ultra-research/references/05-assumptions-validation.md +5 -5
- package/skills/ultra-research/references/10-user-personas.md +3 -2
- package/skills/ultra-research/references/11-user-scenarios.md +3 -3
- package/skills/ultra-research/references/20-user-stories.md +4 -3
- package/skills/ultra-research/references/21-features-scope.md +3 -2
- package/skills/ultra-research/references/22-success-metrics.md +3 -3
- package/skills/ultra-research/references/30-architecture-context.md +3 -3
- package/skills/ultra-research/references/31-solution-strategy.md +3 -3
- package/skills/ultra-research/references/32-building-blocks.md +3 -3
- package/skills/ultra-research/references/40-deployment.md +3 -3
- package/skills/ultra-research/references/41-quality-risks.md +3 -3
- package/skills/ultra-research/references/99-synthesis.md +7 -7
- package/skills/ultra-review/SKILL.md +59 -32
- package/skills/ultra-review/references/code.md +17 -0
- package/skills/ultra-review/references/comments.md +12 -0
- package/skills/ultra-review/references/design.md +12 -0
- package/skills/ultra-review/references/errors.md +13 -0
- package/skills/ultra-review/references/spec.md +13 -0
- package/skills/ultra-review/references/tests.md +12 -0
- package/skills/ultra-review/references/worker-packet.md +40 -0
- package/skills/ultra-review/scripts/review_wait.py +26 -15
- package/skills/ultra-status/SKILL.md +61 -36
- package/skills/ultra-tdd/SKILL.md +81 -0
- package/skills/ultra-tdd/references/templates/README.md +20 -0
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +79 -0
- package/skills/ultra-tdd/references/templates/persistence-real.ts +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +78 -0
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +73 -0
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +66 -0
- package/skills/ultra-tdd/references/test-execution.md +18 -0
- package/skills/ultra-test/SKILL.md +70 -30
- package/skills/ultra-test/references/export-syntax.md +18 -0
- package/skills/ultra-test/scripts/worktree_digest.cjs +54 -0
- package/skills/ultra-think/SKILL.md +49 -27
- package/skills/ultra-think/references/autonomy-boundary.md +48 -0
- package/skills/ultra-think/references/interaction-boundary.md +18 -33
- package/.ultra-template/docs/research/README.md +0 -55
- package/.ultra-template/reports/templates/delivery-report.json +0 -17
- package/.ultra-template/reports/templates/test-report.json +0 -18
- package/.ultra-template/tasks/tasks.json +0 -15
- package/.ultra-template/templates/task-context.md +0 -19
- package/adapters/_shared/codex-assets.cjs +0 -725
- package/adapters/_shared/codex-ultra-tools-entry.cjs +0 -11
- package/adapters/_shared/interaction-contract.cjs +0 -129
- package/adapters/_shared/path-rewrite.cjs +0 -79
- package/adapters/_shared/runtime-native.cjs +0 -132
- package/adapters/_shared/settings-merge.cjs +0 -76
- package/agents/code-reviewer.md +0 -42
- package/agents/debugger.md +0 -40
- package/agents/review-code.md +0 -49
- package/agents/review-comments.md +0 -38
- package/agents/review-coordinator.md +0 -56
- package/agents/review-design.md +0 -40
- package/agents/review-errors.md +0 -43
- package/agents/review-spec.md +0 -39
- package/agents/review-tests.md +0 -40
- package/agents/tdd-runner.md +0 -38
- package/bin/orchestrator.js +0 -383
- package/commands/ultra-change.md +0 -10
- package/commands/ultra-deliver.md +0 -10
- package/commands/ultra-dev.md +0 -10
- package/commands/ultra-doctor.md +0 -10
- package/commands/ultra-init.md +0 -10
- package/commands/ultra-plan.md +0 -10
- package/commands/ultra-research.md +0 -10
- package/commands/ultra-review.md +0 -10
- package/commands/ultra-status.md +0 -10
- package/commands/ultra-test.md +0 -10
- package/commands/ultra-think.md +0 -10
- package/docs/AGENT-CONTEXT.md +0 -191
- package/docs/COMMIT-HASH-BACKFILL.md +0 -51
- package/docs/LEGACY-CLI-CROSSWALK.md +0 -175
- package/docs/ROADMAP.md +0 -101
- package/docs/STATE-DB-ACCESS-POLICY.md +0 -161
- package/hooks/active_task_context.py +0 -152
- package/hooks/context_envelope.py +0 -100
- package/hooks/health_check.py +0 -196
- package/hooks/pre_stop_check.py +0 -66
- package/hooks/runtime_paths.py +0 -396
- package/hooks/subagent_tracker.py +0 -88
- package/hooks/workflow_checkpoint.py +0 -76
- package/hooks/workflow_context.py +0 -48
- package/hooks/workflow_resume.py +0 -84
- package/mcp-server/hook-context.cjs +0 -38
- package/mcp-server/hook-event.cjs +0 -34
- package/mcp-server/lib/archive-journal.cjs +0 -1119
- package/mcp-server/lib/archive-mutation-worker.py +0 -365
- package/mcp-server/lib/artifact-registry.cjs +0 -1232
- package/mcp-server/lib/baseline-workflow.cjs +0 -1193
- package/mcp-server/lib/canonical-json.cjs +0 -19
- package/mcp-server/lib/change-authority.cjs +0 -26
- package/mcp-server/lib/change-packet.cjs +0 -790
- package/mcp-server/lib/change-workflow.cjs +0 -65
- package/mcp-server/lib/context-envelope.cjs +0 -581
- package/mcp-server/lib/context-paths.cjs +0 -142
- package/mcp-server/lib/decision-records.cjs +0 -423
- package/mcp-server/lib/delivery-transaction.cjs +0 -471
- package/mcp-server/lib/doctor-backup-worker.cjs +0 -328
- package/mcp-server/lib/doctor.cjs +0 -627
- package/mcp-server/lib/git-bootstrap.cjs +0 -651
- package/mcp-server/lib/init-project.cjs +0 -1021
- package/mcp-server/lib/kernel-change-workflow.cjs +0 -2029
- package/mcp-server/lib/legacy-change-workflow.cjs +0 -3032
- package/mcp-server/lib/managed-file-write.cjs +0 -172
- package/mcp-server/lib/plan-checkpoint.cjs +0 -177
- package/mcp-server/lib/plan-publication-worker.cjs +0 -830
- package/mcp-server/lib/plan-store.cjs +0 -794
- package/mcp-server/lib/prd-parser.cjs +0 -83
- package/mcp-server/lib/pricing.cjs +0 -44
- package/mcp-server/lib/project-context.cjs +0 -178
- package/mcp-server/lib/projector.cjs +0 -386
- package/mcp-server/lib/provider-refs.cjs +0 -42
- package/mcp-server/lib/runtime-paths.cjs +0 -1714
- package/mcp-server/lib/runtime-state.cjs +0 -222
- package/mcp-server/lib/safe-project-file.cjs +0 -385
- package/mcp-server/lib/stage-checkpoints.cjs +0 -463
- package/mcp-server/lib/state-authority.cjs +0 -95
- package/mcp-server/lib/state-db.cjs +0 -1580
- package/mcp-server/lib/state-ops.cjs +0 -1428
- package/mcp-server/lib/task-contract.cjs +0 -404
- package/mcp-server/lib/task-expander.cjs +0 -105
- package/mcp-server/lib/task-ledger.cjs +0 -1964
- package/mcp-server/lib/telemetry.cjs +0 -85
- package/mcp-server/lib/topo.cjs +0 -131
- package/mcp-server/lib/ultra-facade.cjs +0 -2684
- package/mcp-server/lib/worker-packet.cjs +0 -358
- package/mcp-server/server.cjs +0 -437
- package/orchestrator/auto-merge.cjs +0 -128
- package/orchestrator/daemon.cjs +0 -264
- package/orchestrator/dispatch-rules.cjs +0 -78
- package/orchestrator/parallel-orchestrator.cjs +0 -903
- package/orchestrator/planner/plan-builder.cjs +0 -125
- package/orchestrator/recovery.cjs +0 -188
- package/orchestrator/session-close-journal-worker.cjs +0 -5
- package/orchestrator/session-close-journal.cjs +0 -1014
- package/orchestrator/session-runner.cjs +0 -1153
- package/orchestrator/worktree-manager.cjs +0 -282
- package/settings.json +0 -7
- package/skills/cc-collab/SKILL.md +0 -42
- package/skills/code-review-expert/SKILL.md +0 -43
- package/skills/code-review-expert/references/correctness-reliability.md +0 -30
- package/skills/code-review-expert/references/design-boundaries.md +0 -26
- package/skills/code-review-expert/references/removal-plan.md +0 -24
- package/skills/codex-collab/SKILL.md +0 -49
- package/skills/codex-collab/evals/evals.json +0 -35
- package/skills/integration-rules/SKILL.md +0 -32
- package/skills/security-rules/SKILL.md +0 -38
- package/skills/testing-rules/SKILL.md +0 -32
- package/skills/ultra-doctor/SKILL.md +0 -41
- package/skills/ultra-plan/references/semantic-preflight.md +0 -20
- package/skills/ultra-verify/SKILL.md +0 -46
- package/skills/ultra-verify/evals/evals.json +0 -46
- package/skills/ultra-verify/references/cross-verify-modes.md +0 -28
- package/skills/ultra-verify/references/evidence-status.md +0 -21
- package/skills/ultra-verify/scripts/verify_wait.py +0 -94
- package/spec/cli-protocol.md +0 -102
- package/spec/mcp-tools.yaml +0 -312
- package/spec/schemas/baseline-reconciliation.v1.schema.json +0 -73
- package/spec/schemas/change-delta.v1.schema.json +0 -153
- package/spec/schemas/command-manifest.schema.json +0 -29
- package/spec/schemas/context-file.v4.5.schema.json +0 -54
- package/spec/schemas/delivery-report.v1.schema.json +0 -66
- package/spec/schemas/documentation-reconciliation.v1.schema.json +0 -106
- package/spec/schemas/mcp-tools.schema.json +0 -136
- package/spec/schemas/skill-manifest.schema.json +0 -24
- package/spec/schemas/state-db.sql +0 -641
- package/spec/schemas/task-ledger.v2.schema.json +0 -296
- package/spec/schemas/tasks.v4.5.schema.json +0 -96
- package/spec/schemas/test-report.v1.schema.json +0 -225
- package/ultra-tools/cli.cjs +0 -129
- package/ultra-tools/commands/db.cjs +0 -226
- package/ultra-tools/commands/legacy-memory.cjs +0 -159
- package/ultra-tools/commands/migrate.cjs +0 -1386
- package/ultra-tools/commands/session.cjs +0 -213
- package/ultra-tools/commands/status.cjs +0 -559
- package/ultra-tools/commands/system.cjs +0 -498
- package/ultra-tools/commands/task.cjs +0 -105
package/adapters/opencode.js
CHANGED
|
@@ -1,63 +1,40 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
/** Build a native OpenCode Ultra Builder Pro plugin. */
|
|
4
|
-
|
|
5
3
|
const fs = require('node:fs');
|
|
6
|
-
const crypto = require('node:crypto');
|
|
7
4
|
const os = require('node:os');
|
|
8
5
|
const path = require('node:path');
|
|
9
|
-
const
|
|
10
|
-
const { isDeepStrictEqual } = require('node:util');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
11
7
|
|
|
12
8
|
const {
|
|
13
|
-
|
|
9
|
+
copyHooks,
|
|
10
|
+
copySkills,
|
|
11
|
+
inspectPlugin,
|
|
12
|
+
markManaged,
|
|
13
|
+
removeManaged,
|
|
14
|
+
writePluginProvenance,
|
|
15
|
+
} = require('./_shared/plugin-core.cjs');
|
|
16
|
+
const {
|
|
17
|
+
captureAbsent,
|
|
14
18
|
ensureDir,
|
|
15
19
|
isManaged,
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
managedMetadata,
|
|
21
|
+
pruneCreatedEmpty,
|
|
22
|
+
removeTree: removePath,
|
|
18
23
|
writeAtomic,
|
|
19
24
|
} = require('./_shared/file-ops.cjs');
|
|
20
|
-
const {
|
|
21
|
-
applyNativeDoctor,
|
|
22
|
-
buildMcpRuntime,
|
|
23
|
-
mcpCommand,
|
|
24
|
-
} = require('./_shared/codex-assets.cjs');
|
|
25
|
-
const { adaptInteractionGuidance } = require('./_shared/interaction-contract.cjs');
|
|
26
|
-
const provenance = require('./_shared/provenance.cjs');
|
|
27
|
-
const {
|
|
28
|
-
CORE_PUBLIC_SKILLS,
|
|
29
|
-
skillsForRuntime,
|
|
30
|
-
} = require('./_shared/runtime-assets.cjs');
|
|
31
|
-
const {
|
|
32
|
-
parse: parseFm,
|
|
33
|
-
serialize: serializeFm,
|
|
34
|
-
lowercaseKeys,
|
|
35
|
-
} = require('./_shared/frontmatter.cjs');
|
|
36
|
-
const {
|
|
37
|
-
readJsonSafe,
|
|
38
|
-
} = require('./_shared/settings-merge.cjs');
|
|
25
|
+
const { skillsForRuntime } = require('./_shared/runtime-assets.cjs');
|
|
39
26
|
|
|
40
|
-
const
|
|
41
|
-
const MCP_SERVER_NAME = 'ultra-builder-pro';
|
|
42
|
-
const SOURCE_TAG = 'ubp';
|
|
27
|
+
const PLUGIN_NAME = 'ultra-builder-pro';
|
|
43
28
|
const BUNDLE_DIR = '.ultra-builder-pro';
|
|
44
|
-
const MANAGED_TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
|
|
45
29
|
const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
|
|
46
|
-
const
|
|
47
|
-
const COMMAND_NAMES = CORE_PUBLIC_SKILLS;
|
|
48
|
-
|
|
49
|
-
function discoverableSkillNames() {
|
|
50
|
-
return skillsForRuntime('opencode')
|
|
51
|
-
.filter((name) => !CORE_PUBLIC_SKILLS.includes(name));
|
|
52
|
-
}
|
|
30
|
+
const TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
|
|
53
31
|
|
|
54
32
|
function resolveTarget(ctx = {}) {
|
|
55
|
-
if (ctx.configDir) return ctx.configDir;
|
|
33
|
+
if (ctx.configDir) return path.resolve(ctx.configDir);
|
|
56
34
|
if (ctx.scope === 'global') {
|
|
57
35
|
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
|
58
36
|
if (process.env.OPENCODE_CONFIG) return path.dirname(process.env.OPENCODE_CONFIG);
|
|
59
|
-
|
|
60
|
-
return path.join(xdg || path.join(ctx.homeDir || os.homedir(), '.config'), 'opencode');
|
|
37
|
+
return path.join(process.env.XDG_CONFIG_HOME || path.join(ctx.homeDir || os.homedir(), '.config'), 'opencode');
|
|
61
38
|
}
|
|
62
39
|
return path.join(ctx.cwd || process.cwd(), '.opencode');
|
|
63
40
|
}
|
|
@@ -66,464 +43,122 @@ function resolveRepoRoot(ctx = {}) {
|
|
|
66
43
|
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
67
44
|
}
|
|
68
45
|
|
|
69
|
-
function
|
|
70
|
-
if (typeof ctx.runHostCli === 'boolean') return ctx.runHostCli;
|
|
71
|
-
return ctx.scope === 'global' && !ctx.configDir;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function runOpenCodeCli(args, ctx = {}) {
|
|
75
|
-
const target = resolveTarget(ctx);
|
|
76
|
-
const result = spawnSync(ctx.opencodeBin || 'opencode', args, {
|
|
77
|
-
cwd: ctx.cwd || process.cwd(),
|
|
78
|
-
encoding: 'utf8',
|
|
79
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
80
|
-
killSignal: 'SIGKILL',
|
|
81
|
-
env: {
|
|
82
|
-
...process.env,
|
|
83
|
-
OPENCODE_CONFIG_DIR: target,
|
|
84
|
-
},
|
|
85
|
-
timeout: ctx.hostCliTimeoutMs || 30000,
|
|
86
|
-
});
|
|
87
|
-
if (result.error) {
|
|
88
|
-
if (result.error.code === 'ETIMEDOUT') {
|
|
89
|
-
const error = new Error(`opencode ${args.join(' ')} timed out`);
|
|
90
|
-
error.code = 'HOST_CLI_TIMEOUT';
|
|
91
|
-
throw error;
|
|
92
|
-
}
|
|
93
|
-
throw result.error;
|
|
94
|
-
}
|
|
95
|
-
if (result.status !== 0) {
|
|
96
|
-
const detail = (result.stderr || result.stdout || '').trim();
|
|
97
|
-
throw new Error(`opencode ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
|
98
|
-
}
|
|
99
|
-
return result.stdout;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function openCodeMcpRow(output, serverName) {
|
|
103
|
-
for (const rawLine of output.split(/\r?\n/)) {
|
|
104
|
-
const tokens = rawLine.trim().split(/\s+/);
|
|
105
|
-
if (tokens.some((token) => token === serverName)) return rawLine;
|
|
106
|
-
}
|
|
107
|
-
return '';
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function inspectOpenCodeHost(ctx = {}) {
|
|
111
|
-
const target = resolveTarget(ctx);
|
|
112
|
-
const expectedLauncher = path.join(target, BUNDLE_DIR, 'runtime', 'launch.cjs');
|
|
113
|
-
const expected = managedMcpEntry(target);
|
|
114
|
-
const config = JSON.parse(runOpenCodeCli(['debug', 'config'], ctx));
|
|
115
|
-
const entry = config?.mcp?.[MCP_SERVER_NAME];
|
|
116
|
-
const configOk = isDeepStrictEqual(entry, expected);
|
|
117
|
-
const mcpOutput = runOpenCodeCli(['mcp', 'list'], ctx)
|
|
118
|
-
.replace(/\u001b\[[0-9;]*m/g, '');
|
|
119
|
-
const mcpLine = openCodeMcpRow(mcpOutput, MCP_SERVER_NAME);
|
|
120
|
-
const mcpOk = /\bconnected\b/i.test(mcpLine);
|
|
121
|
-
return { config, configOk, mcpOutput, mcpOk };
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function managedMcpEntry(target) {
|
|
125
|
-
const command = mcpCommand(path.join(target, BUNDLE_DIR, 'runtime', 'launch.cjs'));
|
|
46
|
+
function buildHooksManifest() {
|
|
126
47
|
return {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function hasManagedMcpIdentity(entry, target) {
|
|
134
|
-
return Boolean(
|
|
135
|
-
entry
|
|
136
|
-
&& typeof entry === 'object'
|
|
137
|
-
&& !Array.isArray(entry)
|
|
138
|
-
&& isDeepStrictEqual(entry.command, managedMcpEntry(target).command),
|
|
139
|
-
);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function lowercaseFrontmatterTransform(buf, relPath) {
|
|
143
|
-
if (!relPath.endsWith('.md')) return buf;
|
|
144
|
-
const text = buf.toString('utf8');
|
|
145
|
-
const { fm, body } = parseFm(text);
|
|
146
|
-
if (!fm) return buf;
|
|
147
|
-
return Buffer.from(serializeFm(lowercaseKeys(fm), body), 'utf8');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function openCodeTextTransform(input, assetName = '') {
|
|
151
|
-
let text = adaptInteractionGuidance(input, 'opencode');
|
|
152
|
-
text = text.replaceAll('$CLAUDE_PLUGIN_ROOT/skills', '~/.config/opencode/skills');
|
|
153
|
-
text = text.replaceAll('~/.claude/skills', '~/.config/opencode/skills');
|
|
154
|
-
text = text.replaceAll('~/.claude/hooks', '~/.config/opencode/.ultra-builder-pro/hooks');
|
|
155
|
-
text = text.replaceAll('~/.claude', '~/.config/opencode');
|
|
156
|
-
text = text.replaceAll('CLAUDE.md', 'AGENTS.md');
|
|
157
|
-
text = text.replace(/@skills\/([a-z0-9-]+)\/SKILL\.md/g, '~/.config/opencode/skills/$1/SKILL.md');
|
|
158
|
-
|
|
159
|
-
if (assetName === 'codex-collab') {
|
|
160
|
-
text = text.replaceAll('the current host', 'OpenCode');
|
|
161
|
-
text = text.replaceAll('current host', 'OpenCode');
|
|
162
|
-
}
|
|
163
|
-
if (assetName === 'ultra-verify') {
|
|
164
|
-
text = text.replaceAll('the current host', 'OpenCode');
|
|
165
|
-
text = text.replaceAll('current host', 'OpenCode');
|
|
166
|
-
text = text.replaceAll('claude-analysis.md', 'opencode-analysis.md');
|
|
167
|
-
text = text.replaceAll('Claude Code', 'OpenCode');
|
|
168
|
-
text = text.replaceAll('Claude', 'OpenCode');
|
|
169
|
-
}
|
|
170
|
-
if (assetName === 'cc-collab') {
|
|
171
|
-
text = text.replaceAll('the current host', 'OpenCode');
|
|
172
|
-
text = text.replaceAll('current host', 'OpenCode');
|
|
173
|
-
}
|
|
174
|
-
if (assetName === 'ultra-review') {
|
|
175
|
-
text = text.replace(
|
|
176
|
-
/(?:the current host's native|the host-native) bounded-worker\s+mechanism/g,
|
|
177
|
-
'the OpenCode `task` tool using the installed bounded review agents',
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
return text;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function openCodeCommandTransform(buf, relPath, commandName, workflowFile) {
|
|
184
|
-
if (!relPath.endsWith('.md')) return buf;
|
|
185
|
-
const { fm } = parseFm(buf.toString('utf8'));
|
|
186
|
-
if (!fm) throw new Error(`missing frontmatter in OpenCode command: ${commandName}`);
|
|
187
|
-
const nativeFm = {
|
|
188
|
-
description: openCodeTextTransform(fm.description || `${commandName} workflow`, commandName)
|
|
189
|
-
.replace(/\s+/g, ' ')
|
|
190
|
-
.trim(),
|
|
191
|
-
};
|
|
192
|
-
const body = [
|
|
193
|
-
`# Run ${commandName}`,
|
|
194
|
-
'',
|
|
195
|
-
`Read and follow \`${workflowFile}\` as the only workflow definition.`,
|
|
196
|
-
'This explicit command is the activation boundary; do not substitute a model-discovered',
|
|
197
|
-
'public skill or start another public Ultra workflow automatically.',
|
|
198
|
-
'',
|
|
199
|
-
'Arguments: $ARGUMENTS',
|
|
200
|
-
'',
|
|
201
|
-
].join('\n');
|
|
202
|
-
return Buffer.from(serializeFm(nativeFm, body), 'utf8');
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function openCodeSkillAssetTransform(buf, relPath, skillName) {
|
|
206
|
-
if (!relPath.endsWith('.md')) return buf;
|
|
207
|
-
const text = buf.toString('utf8');
|
|
208
|
-
const { fm, body } = parseFm(text);
|
|
209
|
-
if (!fm) return Buffer.from(openCodeTextTransform(text, skillName), 'utf8');
|
|
210
|
-
const nativeFm = {
|
|
211
|
-
name: String(fm.name || skillName),
|
|
212
|
-
description: openCodeTextTransform(fm.description || `${skillName} workflow`, skillName)
|
|
213
|
-
.replace(/\s+/g, ' ')
|
|
214
|
-
.trim(),
|
|
215
|
-
};
|
|
216
|
-
return Buffer.from(serializeFm(nativeFm, openCodeTextTransform(body, skillName)), 'utf8');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function withManagedMarker(buf) {
|
|
220
|
-
const text = buf.toString('utf8');
|
|
221
|
-
const { fm, body } = parseFm(text);
|
|
222
|
-
if (!fm) return Buffer.from(`${MANAGED_TEXT_MARKER}\n${text}`, 'utf8');
|
|
223
|
-
const cleanBody = body.replace(`${MANAGED_TEXT_MARKER}\n`, '');
|
|
224
|
-
return Buffer.from(serializeFm(fm, `${MANAGED_TEXT_MARKER}\n${cleanBody}`), 'utf8');
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function sourceToolSet(value) {
|
|
228
|
-
if (typeof value === 'string') {
|
|
229
|
-
return new Set(value.split(',').map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
230
|
-
}
|
|
231
|
-
if (Array.isArray(value)) {
|
|
232
|
-
return new Set(value.map((name) => String(name).trim().toLowerCase()).filter(Boolean));
|
|
233
|
-
}
|
|
234
|
-
if (value && typeof value === 'object') {
|
|
235
|
-
return new Set(Object.entries(value)
|
|
236
|
-
.filter(([, enabled]) => enabled)
|
|
237
|
-
.map(([name]) => name.trim().toLowerCase()));
|
|
238
|
-
}
|
|
239
|
-
return new Set();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function openCodeAgentTransform(buf, relPath) {
|
|
243
|
-
if (!relPath.endsWith('.md')) return buf;
|
|
244
|
-
const text = buf.toString('utf8');
|
|
245
|
-
const { fm, body } = parseFm(text);
|
|
246
|
-
if (!fm) return buf;
|
|
247
|
-
|
|
248
|
-
const tools = sourceToolSet(fm.tools);
|
|
249
|
-
const allows = (name) => tools.has(name) ? 'allow' : 'deny';
|
|
250
|
-
const skills = Array.isArray(fm.skills)
|
|
251
|
-
? fm.skills.filter((name) => typeof name === 'string' && name.trim()).map((name) => name.trim())
|
|
252
|
-
: [];
|
|
253
|
-
const skillPermission = skills.length > 0
|
|
254
|
-
? Object.fromEntries([['*', 'deny'], ...skills.map((name) => [name, 'allow'])])
|
|
255
|
-
: 'deny';
|
|
256
|
-
const permission = {
|
|
257
|
-
read: allows('read'),
|
|
258
|
-
edit: tools.has('write') || tools.has('edit') ? 'allow' : 'deny',
|
|
259
|
-
glob: allows('glob'),
|
|
260
|
-
grep: allows('grep'),
|
|
261
|
-
list: allows('glob'),
|
|
262
|
-
bash: allows('bash'),
|
|
263
|
-
task: 'deny',
|
|
264
|
-
external_directory: 'deny',
|
|
265
|
-
todowrite: 'deny',
|
|
266
|
-
question: 'deny',
|
|
267
|
-
webfetch: 'deny',
|
|
268
|
-
websearch: 'deny',
|
|
269
|
-
codesearch: 'deny',
|
|
270
|
-
lsp: 'deny',
|
|
271
|
-
doom_loop: 'deny',
|
|
272
|
-
skill: skillPermission,
|
|
48
|
+
SessionStart: ['session_context.py', 'compact_context.py'],
|
|
49
|
+
PreToolUse: ['mid_workflow_recall.py', 'block_dangerous_commands.py'],
|
|
50
|
+
PostToolUse: ['post_edit_guard.py'],
|
|
51
|
+
PreCompact: ['compact_context.py'],
|
|
273
52
|
};
|
|
274
|
-
const nativeFm = {
|
|
275
|
-
description: fm.description,
|
|
276
|
-
mode: 'subagent',
|
|
277
|
-
...(Number.isInteger(fm.maxTurns) && fm.maxTurns > 0 ? { steps: fm.maxTurns } : {}),
|
|
278
|
-
permission,
|
|
279
|
-
};
|
|
280
|
-
return Buffer.from(
|
|
281
|
-
serializeFm(nativeFm, openCodeTextTransform(body, path.basename(relPath, '.md'))),
|
|
282
|
-
'utf8',
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function isManagedTextFile(file) {
|
|
287
|
-
if (!fs.existsSync(file)) return false;
|
|
288
|
-
return fs.readFileSync(file, 'utf8').includes(MANAGED_TEXT_MARKER);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function assertManagedTextTarget(file, kind) {
|
|
292
|
-
if (fs.existsSync(file) && !isManagedTextFile(file)) {
|
|
293
|
-
throw new Error(`refusing to replace unmanaged OpenCode ${kind}: ${file}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function removeStaleManagedFiles(dir, expected) {
|
|
298
|
-
if (!fs.existsSync(dir)) return [];
|
|
299
|
-
const keep = new Set(expected);
|
|
300
|
-
const removed = [];
|
|
301
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
302
|
-
if (!entry.isFile() || keep.has(entry.name)) continue;
|
|
303
|
-
const file = path.join(dir, entry.name);
|
|
304
|
-
if (!isManagedTextFile(file)) continue;
|
|
305
|
-
fs.unlinkSync(file);
|
|
306
|
-
removed.push(entry.name);
|
|
307
|
-
}
|
|
308
|
-
return removed;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function removeEmptyDir(dir) {
|
|
312
|
-
if (!fs.existsSync(dir)) return;
|
|
313
|
-
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function copyCommands(repoRoot, target, publishedTarget = target) {
|
|
317
|
-
const output = path.join(target, 'commands');
|
|
318
|
-
ensureDir(output);
|
|
319
|
-
removeStaleManagedFiles(output, COMMAND_NAMES.map((name) => `${name}.md`));
|
|
320
|
-
const copied = [];
|
|
321
|
-
for (const name of COMMAND_NAMES) {
|
|
322
|
-
const file = `${name}.md`;
|
|
323
|
-
const source = path.join(repoRoot, 'commands', file);
|
|
324
|
-
if (!fs.existsSync(source)) throw new Error(`missing allowlisted OpenCode command: ${file}`);
|
|
325
|
-
const workflowFile = path.join(publishedTarget, BUNDLE_DIR, 'workflows', name, 'SKILL.md');
|
|
326
|
-
const transformed = withManagedMarker(
|
|
327
|
-
openCodeCommandTransform(fs.readFileSync(source), file, name, workflowFile),
|
|
328
|
-
);
|
|
329
|
-
writeAtomic(path.join(output, file), transformed);
|
|
330
|
-
copied.push(file);
|
|
331
|
-
}
|
|
332
|
-
return copied;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function copySkills(repoRoot, target) {
|
|
336
|
-
const copied = [];
|
|
337
|
-
const names = discoverableSkillNames();
|
|
338
|
-
const skillsRoot = path.join(target, 'skills');
|
|
339
|
-
ensureDir(skillsRoot);
|
|
340
|
-
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
341
|
-
if (!entry.isDirectory() || names.includes(entry.name)) continue;
|
|
342
|
-
const candidate = path.join(skillsRoot, entry.name);
|
|
343
|
-
if (isManaged(candidate)) removeTree(candidate);
|
|
344
|
-
}
|
|
345
|
-
for (const name of names) {
|
|
346
|
-
const source = path.join(repoRoot, 'skills', name);
|
|
347
|
-
if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
|
|
348
|
-
throw new Error(`missing allowlisted OpenCode skill: ${name}`);
|
|
349
|
-
}
|
|
350
|
-
const destination = path.join(skillsRoot, name);
|
|
351
|
-
if (fs.existsSync(destination)) removeTree(destination);
|
|
352
|
-
const files = copyTree(source, destination, {
|
|
353
|
-
transform: (buf, relPath) => openCodeSkillAssetTransform(buf, relPath, name),
|
|
354
|
-
});
|
|
355
|
-
markManaged(destination, { adapter: 'opencode', asset: 'skill', name });
|
|
356
|
-
copied.push(...files.map((rel) => path.join(name, rel)));
|
|
357
|
-
}
|
|
358
|
-
return copied;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function copyPrivateWorkflows(repoRoot, bundleRoot) {
|
|
362
|
-
const copied = [];
|
|
363
|
-
const workflowsRoot = path.join(bundleRoot, 'workflows');
|
|
364
|
-
for (const name of CORE_PUBLIC_SKILLS) {
|
|
365
|
-
const source = path.join(repoRoot, 'skills', name);
|
|
366
|
-
if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
|
|
367
|
-
throw new Error(`missing allowlisted OpenCode workflow: ${name}`);
|
|
368
|
-
}
|
|
369
|
-
const files = copyTree(source, path.join(workflowsRoot, name), {
|
|
370
|
-
transform: (buf, relPath) => openCodeSkillAssetTransform(buf, relPath, name),
|
|
371
|
-
});
|
|
372
|
-
copied.push(...files.map((rel) => path.join('workflows', name, rel)));
|
|
373
|
-
}
|
|
374
|
-
return copied;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function copyAgents(repoRoot, target) {
|
|
378
|
-
const sourceRoot = path.join(repoRoot, 'agents');
|
|
379
|
-
const output = path.join(target, 'agents');
|
|
380
|
-
const names = fs.readdirSync(sourceRoot).filter((name) => name.endsWith('.md')).sort();
|
|
381
|
-
ensureDir(output);
|
|
382
|
-
removeStaleManagedFiles(output, names);
|
|
383
|
-
for (const name of names) {
|
|
384
|
-
const source = fs.readFileSync(path.join(sourceRoot, name));
|
|
385
|
-
writeAtomic(path.join(output, name), withManagedMarker(openCodeAgentTransform(source, name)));
|
|
386
|
-
}
|
|
387
|
-
return names;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function preflightAssets(repoRoot, target) {
|
|
391
|
-
for (const name of COMMAND_NAMES) {
|
|
392
|
-
assertManagedTextTarget(path.join(target, 'commands', `${name}.md`), 'command');
|
|
393
|
-
}
|
|
394
|
-
for (const name of discoverableSkillNames()) {
|
|
395
|
-
const destination = path.join(target, 'skills', name);
|
|
396
|
-
if (fs.existsSync(destination) && !isManaged(destination)) {
|
|
397
|
-
throw new Error(`refusing to replace unmanaged OpenCode skill: ${destination}`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
for (const name of fs.readdirSync(path.join(repoRoot, 'agents')).filter((entry) => entry.endsWith('.md'))) {
|
|
401
|
-
assertManagedTextTarget(path.join(target, 'agents', name), 'agent');
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
const pluginFile = path.join(target, 'plugins', 'ultra-builder-pro.js');
|
|
405
|
-
if (fs.existsSync(pluginFile) && !fs.readFileSync(pluginFile, 'utf8').startsWith(PLUGIN_MARKER)) {
|
|
406
|
-
throw new Error(`refusing to replace unmanaged OpenCode plugin: ${pluginFile}`);
|
|
407
|
-
}
|
|
408
|
-
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
409
|
-
if (fs.existsSync(bundleRoot) && !isManaged(bundleRoot)) {
|
|
410
|
-
throw new Error(`refusing to replace unmanaged OpenCode runtime bundle: ${bundleRoot}`);
|
|
411
|
-
}
|
|
412
53
|
}
|
|
413
54
|
|
|
414
55
|
function pluginSource() {
|
|
415
|
-
return
|
|
56
|
+
return `${PLUGIN_MARKER}
|
|
416
57
|
import path from "node:path";
|
|
417
58
|
import { execFileSync } from "node:child_process";
|
|
418
59
|
import { fileURLToPath } from "node:url";
|
|
419
60
|
|
|
420
|
-
const TEAM_TASK_LEDGER = ".ultra/tasks/tasks.json";
|
|
421
|
-
const LIVE_TASK_PROJECTION = ".ultra/.runtime/projections/tasks.json";
|
|
422
|
-
const NODE_BINARY = ${JSON.stringify(process.execPath)};
|
|
423
61
|
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
424
|
-
const
|
|
425
|
-
PLUGIN_DIR, "..", ${JSON.stringify(BUNDLE_DIR)}, "runtime", "hook-context.cjs",
|
|
426
|
-
);
|
|
62
|
+
const HOOK_ROOT = path.resolve(PLUGIN_DIR, "..", ${JSON.stringify(BUNDLE_DIR)}, "hooks");
|
|
427
63
|
|
|
428
|
-
function
|
|
64
|
+
function runHook(name, root, payload) {
|
|
429
65
|
try {
|
|
430
|
-
const
|
|
431
|
-
cwd:
|
|
66
|
+
const stdout = execFileSync("python3", [path.join(HOOK_ROOT, name)], {
|
|
67
|
+
cwd: root,
|
|
68
|
+
input: JSON.stringify({ cwd: root, ...payload }),
|
|
432
69
|
encoding: "utf8",
|
|
433
|
-
stdio: ["
|
|
434
|
-
timeout:
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
const root = value?.root;
|
|
438
|
-
if (typeof root !== "string" || !root) return null;
|
|
439
|
-
if (!value?.context || typeof value.text !== "string" || !value.text) {
|
|
440
|
-
return { root, context: null, text: null };
|
|
441
|
-
}
|
|
442
|
-
return { root, context: value.context, text: value.text };
|
|
70
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
71
|
+
timeout: 10000,
|
|
72
|
+
}).trim();
|
|
73
|
+
return stdout ? JSON.parse(stdout) : {};
|
|
443
74
|
} catch (error) {
|
|
444
|
-
|
|
445
|
-
if (/RUNTIME_(?:STATE_CONFLICT|PATH_UNSAFE|ORPHAN_SIDECAR|ROOT_INVALID|AUTHORITY_MISMATCH)|both legacy .*runtime.*state\\.db/i.test(detail)) {
|
|
446
|
-
throw new Error(detail.trim());
|
|
447
|
-
}
|
|
448
|
-
return null;
|
|
75
|
+
return { systemMessage: "Ultra hook failed open: " + String(error?.message || error) };
|
|
449
76
|
}
|
|
450
77
|
}
|
|
451
78
|
|
|
452
|
-
function
|
|
453
|
-
|
|
454
|
-
const paths = [args.file_path, args.filePath, args.filepath, args.path]
|
|
455
|
-
.filter((value) => typeof value === "string" && value.length > 0);
|
|
456
|
-
const patch = args.patch ?? args.command;
|
|
457
|
-
if (String(tool).toLowerCase() === "apply_patch" && typeof patch === "string") {
|
|
458
|
-
const pattern = /^\\*\\*\\* (?:Add|Update|Delete) File: (.+?)\\s*$/gm;
|
|
459
|
-
for (const match of patch.matchAll(pattern)) paths.push(match[1].trim());
|
|
460
|
-
}
|
|
461
|
-
return [...new Set(paths)];
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function managedTaskRootForTarget(baseRoot, candidate) {
|
|
465
|
-
const target = path.isAbsolute(candidate)
|
|
466
|
-
? path.resolve(candidate)
|
|
467
|
-
: path.resolve(baseRoot, candidate);
|
|
468
|
-
if (target === path.resolve(baseRoot, TEAM_TASK_LEDGER)
|
|
469
|
-
|| target === path.resolve(baseRoot, LIVE_TASK_PROJECTION)) return baseRoot;
|
|
470
|
-
if (path.basename(target) !== "tasks.json") return null;
|
|
471
|
-
const parent = path.dirname(target);
|
|
472
|
-
if (path.basename(parent) === "tasks"
|
|
473
|
-
&& path.basename(path.dirname(parent)) === ".ultra") {
|
|
474
|
-
return path.dirname(path.dirname(parent));
|
|
475
|
-
}
|
|
476
|
-
if (path.basename(parent) === "projections"
|
|
477
|
-
&& path.basename(path.dirname(parent)) === ".runtime"
|
|
478
|
-
&& path.basename(path.dirname(path.dirname(parent))) === ".ultra") {
|
|
479
|
-
return path.dirname(path.dirname(path.dirname(parent)));
|
|
480
|
-
}
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function protectsManagedTaskFile(baseRoot, candidate) {
|
|
485
|
-
const targetRoot = managedTaskRootForTarget(baseRoot, candidate);
|
|
486
|
-
if (!targetRoot) return false;
|
|
487
|
-
return Boolean(readUltraContext(targetRoot));
|
|
79
|
+
function contextOf(output) {
|
|
80
|
+
return output?.hookSpecificOutput?.additionalContext || output?.systemMessage || "";
|
|
488
81
|
}
|
|
489
82
|
|
|
490
83
|
export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
|
|
491
84
|
const root = worktree || directory;
|
|
492
|
-
let active = readUltraContext(root);
|
|
493
|
-
const refresh = () => { active = readUltraContext(root); };
|
|
494
85
|
return {
|
|
495
|
-
event: async ({ event }) => {
|
|
496
|
-
if (["session.created", "session.compacted", "session.idle"].includes(event?.type)) refresh();
|
|
497
|
-
},
|
|
498
86
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
499
|
-
|
|
500
|
-
if (
|
|
87
|
+
const text = contextOf(runHook("session_context.py", root, { hook_event_name: "SessionStart" }));
|
|
88
|
+
if (text) output.system.push(text);
|
|
501
89
|
},
|
|
502
90
|
"experimental.session.compacting": async (_input, output) => {
|
|
503
|
-
|
|
504
|
-
|
|
91
|
+
runHook("compact_context.py", root, { hook_event_name: "PreCompact" });
|
|
92
|
+
const text = contextOf(runHook("compact_context.py", root, {
|
|
93
|
+
hook_event_name: "SessionStart", source: "compact",
|
|
94
|
+
}));
|
|
95
|
+
if (text) output.context.push(text);
|
|
505
96
|
},
|
|
506
97
|
"tool.execute.before": async (input, output) => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
"
|
|
517
|
-
|
|
518
|
-
|
|
98
|
+
const tool = String(input?.tool || "").toLowerCase();
|
|
99
|
+
if (tool === "bash") {
|
|
100
|
+
const verdict = runHook("block_dangerous_commands.py", root, {
|
|
101
|
+
hook_event_name: "PreToolUse", tool_name: "Bash", tool_input: output?.args || {},
|
|
102
|
+
});
|
|
103
|
+
if (verdict?.decision === "block") throw new Error(verdict.reason);
|
|
104
|
+
}
|
|
105
|
+
if (["write", "edit", "grep", "apply_patch"].includes(tool)) {
|
|
106
|
+
runHook("mid_workflow_recall.py", root, {
|
|
107
|
+
hook_event_name: "PreToolUse", tool_name: input?.tool, tool_input: output?.args || {},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"tool.execute.after": async (input) => {
|
|
112
|
+
const tool = String(input?.tool || "").toLowerCase();
|
|
113
|
+
if (["write", "edit", "apply_patch"].includes(tool)) {
|
|
114
|
+
runHook("post_edit_guard.py", root, {
|
|
115
|
+
hook_event_name: "PostToolUse", tool_name: input?.tool, tool_input: input?.args || {},
|
|
116
|
+
});
|
|
519
117
|
}
|
|
520
118
|
},
|
|
521
|
-
"tool.execute.after": async () => { refresh(); },
|
|
522
119
|
};
|
|
523
120
|
};
|
|
524
121
|
`;
|
|
525
122
|
}
|
|
526
123
|
|
|
124
|
+
function planRetiredCleanup(target) {
|
|
125
|
+
const files = [];
|
|
126
|
+
for (const dirName of ['commands', 'agents']) {
|
|
127
|
+
const dir = path.join(target, dirName);
|
|
128
|
+
if (!fs.existsSync(dir)) continue;
|
|
129
|
+
for (const file of fs.readdirSync(dir)) {
|
|
130
|
+
const candidate = path.join(dir, file);
|
|
131
|
+
if (!fs.statSync(candidate).isFile()) continue;
|
|
132
|
+
const text = fs.readFileSync(candidate, 'utf8');
|
|
133
|
+
if (text.includes(TEXT_MARKER)) files.push(candidate);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const configFile = path.join(target, 'opencode.json');
|
|
137
|
+
let config = null;
|
|
138
|
+
if (fs.existsSync(configFile)) {
|
|
139
|
+
const value = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
140
|
+
if (value?.mcp?.[PLUGIN_NAME]) {
|
|
141
|
+
delete value.mcp[PLUGIN_NAME];
|
|
142
|
+
if (Object.keys(value.mcp).length === 0) delete value.mcp;
|
|
143
|
+
config = `${JSON.stringify(value, null, 2)}\n`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return { config, configFile, files };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function cleanupRetired(target, plan = planRetiredCleanup(target)) {
|
|
150
|
+
for (const file of plan.files) {
|
|
151
|
+
if (fs.existsSync(file) && fs.readFileSync(file, 'utf8').includes(TEXT_MARKER)) {
|
|
152
|
+
fs.unlinkSync(file);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const dirName of ['commands', 'agents']) {
|
|
156
|
+
const dir = path.join(target, dirName);
|
|
157
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
158
|
+
}
|
|
159
|
+
if (plan.config !== null) writeAtomic(plan.configFile, plan.config);
|
|
160
|
+
}
|
|
161
|
+
|
|
527
162
|
function createInstallTransaction() {
|
|
528
163
|
const changes = [];
|
|
529
164
|
const backups = [];
|
|
@@ -561,10 +196,11 @@ function createInstallTransaction() {
|
|
|
561
196
|
},
|
|
562
197
|
removeFile(target) {
|
|
563
198
|
if (!fs.existsSync(target)) return;
|
|
199
|
+
const link = fs.lstatSync(target).isSymbolicLink() ? fs.readlinkSync(target) : null;
|
|
564
200
|
const stat = fs.statSync(target);
|
|
565
|
-
const previous = { contents: fs.readFileSync(target), mode: stat.mode & 0o7777 };
|
|
201
|
+
const previous = { contents: fs.readFileSync(target), link, mode: stat.mode & 0o7777 };
|
|
566
202
|
fs.unlinkSync(target);
|
|
567
|
-
changes.push({ type: 'file', target, previous });
|
|
203
|
+
changes.push({ type: 'file', target, previous, removed: true });
|
|
568
204
|
},
|
|
569
205
|
removeTree(target) {
|
|
570
206
|
if (!fs.existsSync(target)) return;
|
|
@@ -575,7 +211,7 @@ function createInstallTransaction() {
|
|
|
575
211
|
},
|
|
576
212
|
commit() {
|
|
577
213
|
for (const backup of backups) {
|
|
578
|
-
if (fs.existsSync(backup))
|
|
214
|
+
if (fs.existsSync(backup)) removePath(backup);
|
|
579
215
|
}
|
|
580
216
|
changes.length = 0;
|
|
581
217
|
backups.length = 0;
|
|
@@ -586,150 +222,69 @@ function createInstallTransaction() {
|
|
|
586
222
|
try {
|
|
587
223
|
if (change.type === 'file') {
|
|
588
224
|
if (change.previous) {
|
|
589
|
-
|
|
590
|
-
|
|
225
|
+
if (change.removed && change.previous.link !== null) {
|
|
226
|
+
ensureDir(path.dirname(change.target));
|
|
227
|
+
fs.symlinkSync(change.previous.link, change.target);
|
|
228
|
+
} else {
|
|
229
|
+
writeAtomic(change.target, change.previous.contents);
|
|
230
|
+
fs.chmodSync(resolveTargetFile(change.target), change.previous.mode);
|
|
231
|
+
}
|
|
591
232
|
} else if (fs.existsSync(change.target)) {
|
|
592
233
|
fs.unlinkSync(change.target);
|
|
593
234
|
}
|
|
594
235
|
} else {
|
|
595
|
-
if (!change.removed && fs.existsSync(change.target))
|
|
596
|
-
if (change.backup && fs.existsSync(change.backup))
|
|
597
|
-
fs.renameSync(change.backup, change.target);
|
|
598
|
-
}
|
|
236
|
+
if (!change.removed && fs.existsSync(change.target)) removePath(change.target);
|
|
237
|
+
if (change.backup && fs.existsSync(change.backup)) fs.renameSync(change.backup, change.target);
|
|
599
238
|
}
|
|
600
239
|
} catch (error) {
|
|
601
240
|
errors.push(error);
|
|
602
241
|
}
|
|
603
242
|
}
|
|
604
|
-
if (errors.length
|
|
605
|
-
throw new AggregateError(errors, 'OpenCode install rollback failed');
|
|
606
|
-
}
|
|
243
|
+
if (errors.length) throw new AggregateError(errors, 'OpenCode install rollback failed');
|
|
607
244
|
},
|
|
608
245
|
};
|
|
609
246
|
}
|
|
610
247
|
|
|
611
|
-
function
|
|
612
|
-
|
|
613
|
-
report.copied.commands = copyCommands(repoRoot, staging, target);
|
|
614
|
-
report.copied.skills = copySkills(repoRoot, staging);
|
|
615
|
-
report.copied.agents = copyAgents(repoRoot, staging);
|
|
616
|
-
ensureDir(path.join(staging, 'plugins'));
|
|
617
|
-
writeAtomic(path.join(staging, 'plugins', 'ultra-builder-pro.js'), pluginSource());
|
|
618
|
-
report.copied.plugins = ['ultra-builder-pro.js'];
|
|
619
|
-
|
|
620
|
-
const bundleRoot = path.join(staging, BUNDLE_DIR);
|
|
621
|
-
ensureDir(bundleRoot);
|
|
622
|
-
report.copied.workflows = copyPrivateWorkflows(repoRoot, bundleRoot);
|
|
623
|
-
buildMcpRuntime(repoRoot, bundleRoot, { runtime: 'opencode' });
|
|
624
|
-
markManaged(bundleRoot, { adapter: 'opencode', asset: 'runtime-bundle' });
|
|
625
|
-
const configFile = path.join(target, 'opencode.json');
|
|
626
|
-
const existing = readJsonSafe(configFile, { rescue: true });
|
|
627
|
-
const mcp = { ...(existing.mcp || {}) };
|
|
628
|
-
mcp[MCP_SERVER_NAME] = managedMcpEntry(target);
|
|
629
|
-
const next = { ...existing, mcp };
|
|
630
|
-
delete next[LEGACY_SENTINEL_KEY];
|
|
631
|
-
writeAtomic(path.join(staging, 'opencode.json'), JSON.stringify(next, null, 2) + '\n');
|
|
632
|
-
report.config.updated = true;
|
|
633
|
-
const source = provenance.packageSource(repoRoot);
|
|
634
|
-
const bundleAssets = provenance.assetRefsForTree('config', bundleRoot, {
|
|
635
|
-
exclude: [PROVENANCE_FILE],
|
|
636
|
-
}).map((asset) => ({ ...asset, path: path.join(BUNDLE_DIR, asset.path) }));
|
|
637
|
-
const skillMarkers = discoverableSkillNames().map((name) => ({
|
|
638
|
-
root: 'config', path: path.join('skills', name, '.ubp-managed'),
|
|
639
|
-
}));
|
|
640
|
-
const provenanceFile = path.join(bundleRoot, PROVENANCE_FILE);
|
|
641
|
-
report.provenance = provenance.writeProvenance({
|
|
642
|
-
file: provenanceFile,
|
|
643
|
-
adapter: 'opencode',
|
|
644
|
-
...source,
|
|
645
|
-
roots: { config: target },
|
|
646
|
-
assetRoots: { config: staging },
|
|
647
|
-
assets: [
|
|
648
|
-
...report.copied.commands.map((file) => ({ root: 'config', path: path.join('commands', file) })),
|
|
649
|
-
...report.copied.skills.map((file) => ({ root: 'config', path: path.join('skills', file) })),
|
|
650
|
-
...skillMarkers,
|
|
651
|
-
...report.copied.agents.map((file) => ({ root: 'config', path: path.join('agents', file) })),
|
|
652
|
-
{ root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
|
|
653
|
-
...bundleAssets,
|
|
654
|
-
],
|
|
655
|
-
contracts: {
|
|
656
|
-
host_plugin: { root: 'config', path: path.join('plugins', 'ultra-builder-pro.js') },
|
|
657
|
-
mcp_config: { root: 'config', path: 'opencode.json' },
|
|
658
|
-
mcp_launcher: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'launch.cjs') },
|
|
659
|
-
native_runtime: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'native-runtime.json') },
|
|
660
|
-
context_envelope_helper: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'hook-context.cjs') },
|
|
661
|
-
hook_event_helper: { root: 'config', path: path.join(BUNDLE_DIR, 'runtime', 'hook-event.cjs') },
|
|
662
|
-
},
|
|
663
|
-
});
|
|
664
|
-
report.provenance.file = path.join(target, BUNDLE_DIR, PROVENANCE_FILE);
|
|
665
|
-
return report;
|
|
248
|
+
function resolveTargetFile(file) {
|
|
249
|
+
return fs.lstatSync(file).isSymbolicLink() ? fs.realpathSync(file) : file;
|
|
666
250
|
}
|
|
667
251
|
|
|
668
|
-
function
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
252
|
+
function publishStaging({ staging, target, bundleRoot, skillRoot, pluginFile, skills, retiredCleanup }) {
|
|
253
|
+
const expected = new Set(skills);
|
|
254
|
+
const staleSkills = [];
|
|
255
|
+
if (fs.existsSync(skillRoot)) {
|
|
256
|
+
for (const entry of fs.readdirSync(skillRoot, { withFileTypes: true })) {
|
|
257
|
+
const candidate = path.join(skillRoot, entry.name);
|
|
258
|
+
if (entry.isDirectory() && !expected.has(entry.name) && isManaged(candidate)) staleSkills.push(candidate);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (fs.existsSync(bundleRoot) && !isManaged(bundleRoot)) {
|
|
262
|
+
throw new Error(`refusing to replace unmanaged OpenCode bundle: ${bundleRoot}`);
|
|
263
|
+
}
|
|
264
|
+
for (const name of skills) {
|
|
265
|
+
const candidate = path.join(skillRoot, name);
|
|
266
|
+
if (fs.existsSync(candidate) && !isManaged(candidate)) {
|
|
267
|
+
throw new Error(`refusing to replace unmanaged OpenCode skill: ${candidate}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
674
270
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
.map((entry) => entry.name);
|
|
680
|
-
}
|
|
271
|
+
const stagedConfig = path.join(staging, 'opencode.json');
|
|
272
|
+
const stagedPlugin = path.join(staging, `${PLUGIN_NAME}.js`);
|
|
273
|
+
writeAtomic(stagedPlugin, pluginSource());
|
|
274
|
+
if (retiredCleanup.config !== null) writeAtomic(stagedConfig, retiredCleanup.config);
|
|
681
275
|
|
|
682
|
-
function publishStaging(staging, target, report) {
|
|
683
276
|
const transaction = createInstallTransaction();
|
|
684
|
-
const commandNames = report.copied.commands;
|
|
685
|
-
const agentNames = report.copied.agents;
|
|
686
|
-
const skillNames = discoverableSkillNames();
|
|
687
277
|
try {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
path.join(target, 'commands', file),
|
|
692
|
-
);
|
|
693
|
-
}
|
|
694
|
-
for (const stale of managedTextFiles(path.join(target, 'commands'))) {
|
|
695
|
-
if (!commandNames.includes(stale)) {
|
|
696
|
-
transaction.removeFile(path.join(target, 'commands', stale));
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
for (const name of skillNames) {
|
|
700
|
-
transaction.replaceTree(
|
|
701
|
-
path.join(staging, 'skills', name),
|
|
702
|
-
path.join(target, 'skills', name),
|
|
703
|
-
);
|
|
704
|
-
}
|
|
705
|
-
for (const stale of managedSkillDirs(path.join(target, 'skills'))) {
|
|
706
|
-
if (!skillNames.includes(stale)) {
|
|
707
|
-
transaction.removeTree(path.join(target, 'skills', stale));
|
|
708
|
-
}
|
|
278
|
+
transaction.replaceTree(path.join(staging, 'bundle'), bundleRoot);
|
|
279
|
+
for (const name of skills) {
|
|
280
|
+
transaction.replaceTree(path.join(staging, 'skills', name), path.join(skillRoot, name));
|
|
709
281
|
}
|
|
710
|
-
for (const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
);
|
|
282
|
+
for (const candidate of staleSkills) transaction.removeTree(candidate);
|
|
283
|
+
transaction.replaceFile(stagedPlugin, pluginFile);
|
|
284
|
+
for (const file of retiredCleanup.files) transaction.removeFile(file);
|
|
285
|
+
if (retiredCleanup.config !== null) {
|
|
286
|
+
transaction.replaceFile(stagedConfig, retiredCleanup.configFile);
|
|
715
287
|
}
|
|
716
|
-
for (const stale of managedTextFiles(path.join(target, 'agents'))) {
|
|
717
|
-
if (!agentNames.includes(stale)) {
|
|
718
|
-
transaction.removeFile(path.join(target, 'agents', stale));
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
transaction.replaceFile(
|
|
722
|
-
path.join(staging, 'plugins', 'ultra-builder-pro.js'),
|
|
723
|
-
path.join(target, 'plugins', 'ultra-builder-pro.js'),
|
|
724
|
-
);
|
|
725
|
-
transaction.replaceTree(
|
|
726
|
-
path.join(staging, BUNDLE_DIR),
|
|
727
|
-
path.join(target, BUNDLE_DIR),
|
|
728
|
-
);
|
|
729
|
-
transaction.replaceFile(
|
|
730
|
-
path.join(staging, 'opencode.json'),
|
|
731
|
-
path.join(target, 'opencode.json'),
|
|
732
|
-
);
|
|
733
288
|
transaction.commit();
|
|
734
289
|
} catch (error) {
|
|
735
290
|
try {
|
|
@@ -739,182 +294,115 @@ function publishStaging(staging, target, report) {
|
|
|
739
294
|
}
|
|
740
295
|
throw error;
|
|
741
296
|
}
|
|
297
|
+
|
|
298
|
+
for (const dirName of ['commands', 'agents']) {
|
|
299
|
+
const directory = path.join(target, dirName);
|
|
300
|
+
if (fs.existsSync(directory) && fs.readdirSync(directory).length === 0) fs.rmdirSync(directory);
|
|
301
|
+
}
|
|
742
302
|
}
|
|
743
303
|
|
|
744
304
|
function install(ctx = {}) {
|
|
745
305
|
const target = resolveTarget(ctx);
|
|
746
306
|
const repoRoot = resolveRepoRoot(ctx);
|
|
307
|
+
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
308
|
+
const skillRoot = path.join(target, 'skills');
|
|
309
|
+
const pluginFile = path.join(target, 'plugins', `${PLUGIN_NAME}.js`);
|
|
310
|
+
const cleanupAbsent = managedMetadata(bundleRoot)?.cleanup_absent
|
|
311
|
+
|| captureAbsent(target, ['.', 'skills', 'plugins']);
|
|
747
312
|
ensureDir(target);
|
|
748
|
-
|
|
749
|
-
|
|
313
|
+
|
|
314
|
+
if (fs.existsSync(pluginFile) && !fs.readFileSync(pluginFile, 'utf8').startsWith(PLUGIN_MARKER)) {
|
|
315
|
+
throw new Error(`refusing to replace unmanaged OpenCode plugin: ${pluginFile}`);
|
|
316
|
+
}
|
|
317
|
+
const retiredCleanup = planRetiredCleanup(target);
|
|
318
|
+
const staging = fs.mkdtempSync(path.join(target, `.${PLUGIN_NAME}-staging-`));
|
|
319
|
+
const stagedBundle = path.join(staging, 'bundle');
|
|
320
|
+
const stagedSkills = path.join(staging, 'skills');
|
|
321
|
+
ensureDir(stagedBundle);
|
|
322
|
+
ensureDir(stagedSkills);
|
|
750
323
|
try {
|
|
751
|
-
const
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
324
|
+
const skills = copySkills({ runtime: 'opencode', repoRoot, skillRoot: stagedSkills });
|
|
325
|
+
for (const name of skills) markManaged(path.join(stagedSkills, name), { adapter: 'opencode', asset: 'skill', name });
|
|
326
|
+
copyHooks({
|
|
327
|
+
runtime: 'opencode', repoRoot, hookRoot: path.join(stagedBundle, 'hooks'),
|
|
328
|
+
hooksManifest: buildHooksManifest(),
|
|
329
|
+
});
|
|
330
|
+
markManaged(stagedBundle, { adapter: 'opencode', plugin: PLUGIN_NAME });
|
|
331
|
+
writePluginProvenance({
|
|
332
|
+
adapter: 'opencode', repoRoot, stagingRoot: stagedBundle, publishedRoot: bundleRoot,
|
|
333
|
+
contracts: { hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' } },
|
|
334
|
+
});
|
|
335
|
+
markManaged(stagedBundle, { cleanup_absent: cleanupAbsent });
|
|
336
|
+
|
|
337
|
+
publishStaging({
|
|
338
|
+
staging, target, bundleRoot, skillRoot, pluginFile, skills, retiredCleanup,
|
|
339
|
+
});
|
|
340
|
+
removePath(staging);
|
|
341
|
+
return {
|
|
342
|
+
target: bundleRoot,
|
|
343
|
+
pluginRoot: bundleRoot,
|
|
344
|
+
skillRoot,
|
|
345
|
+
hookRoot: path.join(bundleRoot, 'hooks'),
|
|
346
|
+
pluginFile,
|
|
347
|
+
copied: { skills },
|
|
348
|
+
};
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (fs.existsSync(staging)) removePath(staging);
|
|
351
|
+
pruneCreatedEmpty(target, cleanupAbsent);
|
|
352
|
+
throw error;
|
|
756
353
|
}
|
|
757
354
|
}
|
|
758
355
|
|
|
759
356
|
function doctor(ctx = {}) {
|
|
760
357
|
const target = resolveTarget(ctx);
|
|
761
|
-
const
|
|
762
|
-
const
|
|
763
|
-
const report =
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
358
|
+
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
359
|
+
const pluginFile = path.join(target, 'plugins', `${PLUGIN_NAME}.js`);
|
|
360
|
+
const report = inspectPlugin({
|
|
361
|
+
adapter: 'opencode', repoRoot: resolveRepoRoot(ctx), pluginRoot: bundleRoot,
|
|
362
|
+
skillRoot: path.join(target, 'skills'), hookRoot: path.join(bundleRoot, 'hooks'),
|
|
363
|
+
manifestFile: pluginFile,
|
|
767
364
|
});
|
|
768
|
-
|
|
769
|
-
const expectedLauncher = path.join(target, BUNDLE_DIR, 'runtime', 'launch.cjs');
|
|
770
|
-
let registrationOk = false;
|
|
365
|
+
let configClean = true;
|
|
771
366
|
try {
|
|
772
|
-
const
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
&& entry.enabled === true
|
|
777
|
-
&& Array.isArray(entry.command)
|
|
778
|
-
&& JSON.stringify(entry.command) === JSON.stringify([
|
|
779
|
-
command.command,
|
|
780
|
-
...command.args,
|
|
781
|
-
]);
|
|
782
|
-
} catch (error) {
|
|
783
|
-
report.issues.push({ code: 'MCP_REGISTRATION_INVALID', message: error.message });
|
|
784
|
-
}
|
|
785
|
-
if (!registrationOk && !report.issues.some((entry) => entry.code === 'MCP_REGISTRATION_INVALID')) {
|
|
786
|
-
report.issues.push({
|
|
787
|
-
code: 'MCP_REGISTRATION_INVALID',
|
|
788
|
-
path: configFile,
|
|
789
|
-
expected_launcher: expectedLauncher,
|
|
790
|
-
});
|
|
791
|
-
}
|
|
792
|
-
report.checks.registration = { status: registrationOk ? 'pass' : 'fail' };
|
|
793
|
-
if (shouldRunHostCli(ctx)) {
|
|
794
|
-
try {
|
|
795
|
-
const host = inspectOpenCodeHost(ctx);
|
|
796
|
-
report.checks.host_config = { status: host.configOk ? 'pass' : 'fail' };
|
|
797
|
-
report.checks.host_mcp = { status: host.mcpOk ? 'pass' : 'fail' };
|
|
798
|
-
if (!host.configOk) {
|
|
799
|
-
report.issues.push({
|
|
800
|
-
code: 'HOST_CONFIG_NOT_CONSUMED',
|
|
801
|
-
path: configFile,
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
if (!host.mcpOk) {
|
|
805
|
-
report.issues.push({
|
|
806
|
-
code: 'HOST_MCP_NOT_CONNECTED',
|
|
807
|
-
server: MCP_SERVER_NAME,
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
} catch (error) {
|
|
811
|
-
report.checks.host_config = { status: 'fail' };
|
|
812
|
-
report.checks.host_mcp = { status: 'fail' };
|
|
813
|
-
report.issues.push({
|
|
814
|
-
code: error.code === 'HOST_CLI_TIMEOUT'
|
|
815
|
-
? 'HOST_CLI_TIMEOUT'
|
|
816
|
-
: 'HOST_CLI_INSPECTION_FAILED',
|
|
817
|
-
message: error.message,
|
|
818
|
-
});
|
|
367
|
+
const configFile = path.join(target, 'opencode.json');
|
|
368
|
+
if (fs.existsSync(configFile)) {
|
|
369
|
+
const value = JSON.parse(fs.readFileSync(configFile, 'utf8'));
|
|
370
|
+
configClean = !value?.mcp?.[PLUGIN_NAME];
|
|
819
371
|
}
|
|
820
|
-
}
|
|
821
|
-
|
|
372
|
+
} catch { configClean = false; }
|
|
373
|
+
report.checks.no_mcp_registration = { status: configClean ? 'pass' : 'fail' };
|
|
374
|
+
if (!configClean) report.issues.push({ code: 'RETIRED_MCP_REGISTRATION_PRESENT' });
|
|
375
|
+
report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
|
|
376
|
+
return report;
|
|
822
377
|
}
|
|
823
378
|
|
|
824
379
|
function uninstall(ctx = {}) {
|
|
825
380
|
const target = resolveTarget(ctx);
|
|
826
|
-
const
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
381
|
+
const pluginRoot = path.join(target, BUNDLE_DIR);
|
|
382
|
+
const cleanupAbsent = managedMetadata(pluginRoot)?.cleanup_absent || [];
|
|
383
|
+
const skillRoot = path.join(target, 'skills');
|
|
384
|
+
const removedSkills = [];
|
|
385
|
+
for (const name of skillsForRuntime('opencode')) {
|
|
386
|
+
if (removeManaged(path.join(skillRoot, name), `OpenCode skill ${name}`)) removedSkills.push(name);
|
|
387
|
+
}
|
|
388
|
+
const pluginFile = path.join(target, 'plugins', `${PLUGIN_NAME}.js`);
|
|
389
|
+
if (fs.existsSync(pluginFile) && fs.readFileSync(pluginFile, 'utf8').startsWith(PLUGIN_MARKER)) fs.unlinkSync(pluginFile);
|
|
390
|
+
cleanupRetired(target);
|
|
391
|
+
const removedBundle = removeManaged(pluginRoot, 'OpenCode bundle');
|
|
392
|
+
const cleaned = pruneCreatedEmpty(target, cleanupAbsent);
|
|
393
|
+
return {
|
|
394
|
+
target: pluginRoot, pluginRoot,
|
|
395
|
+
removed: { bundle: removedBundle, skills: removedSkills },
|
|
396
|
+
cleaned,
|
|
831
397
|
};
|
|
832
|
-
const pluginFile = path.join(target, 'plugins', 'ultra-builder-pro.js');
|
|
833
|
-
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
834
|
-
const pluginOwned = fs.existsSync(pluginFile)
|
|
835
|
-
&& fs.readFileSync(pluginFile, 'utf8').startsWith(PLUGIN_MARKER);
|
|
836
|
-
const bundleOwned = fs.existsSync(bundleRoot) && isManaged(bundleRoot);
|
|
837
|
-
const configFile = path.join(target, 'opencode.json');
|
|
838
|
-
if (fs.existsSync(configFile)) {
|
|
839
|
-
const existing = readJsonSafe(configFile);
|
|
840
|
-
const currentMcp = existing.mcp?.[MCP_SERVER_NAME];
|
|
841
|
-
const expectedMcp = managedMcpEntry(target);
|
|
842
|
-
const mcpOwned = hasManagedMcpIdentity(currentMcp, target);
|
|
843
|
-
if (currentMcp !== undefined && !mcpOwned) {
|
|
844
|
-
report.config.skipped = true;
|
|
845
|
-
report.config.reason = 'ownership-conflict';
|
|
846
|
-
report.issues.push({
|
|
847
|
-
code: 'MCP_OWNERSHIP_CONFLICT',
|
|
848
|
-
path: configFile,
|
|
849
|
-
server: MCP_SERVER_NAME,
|
|
850
|
-
expected: expectedMcp,
|
|
851
|
-
actual: currentMcp,
|
|
852
|
-
});
|
|
853
|
-
return report;
|
|
854
|
-
}
|
|
855
|
-
if (mcpOwned || existing[LEGACY_SENTINEL_KEY]) {
|
|
856
|
-
const mcp = { ...(existing.mcp || {}) };
|
|
857
|
-
if (mcpOwned) delete mcp[MCP_SERVER_NAME];
|
|
858
|
-
const next = { ...existing, mcp };
|
|
859
|
-
delete next[LEGACY_SENTINEL_KEY];
|
|
860
|
-
if (Object.keys(next.mcp || {}).length === 0) delete next.mcp;
|
|
861
|
-
if (!isDeepStrictEqual(next, existing)) {
|
|
862
|
-
writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
|
|
863
|
-
report.config.updated = true;
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
if (pluginOwned) {
|
|
868
|
-
fs.unlinkSync(pluginFile);
|
|
869
|
-
report.removed.plugin = true;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
for (const sub of ['commands', 'agents']) {
|
|
873
|
-
const dir = path.join(target, sub);
|
|
874
|
-
const removed = [];
|
|
875
|
-
if (fs.existsSync(dir)) {
|
|
876
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
877
|
-
if (!entry.isFile()) continue;
|
|
878
|
-
const file = path.join(dir, entry.name);
|
|
879
|
-
if (!isManagedTextFile(file)) continue;
|
|
880
|
-
fs.unlinkSync(file);
|
|
881
|
-
removed.push(entry.name);
|
|
882
|
-
}
|
|
883
|
-
removeEmptyDir(dir);
|
|
884
|
-
}
|
|
885
|
-
if (removed.length > 0) report.removed[sub] = removed;
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
const skillsRoot = path.join(target, 'skills');
|
|
889
|
-
const skills = [];
|
|
890
|
-
if (fs.existsSync(skillsRoot)) {
|
|
891
|
-
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
892
|
-
if (!entry.isDirectory()) continue;
|
|
893
|
-
const dir = path.join(skillsRoot, entry.name);
|
|
894
|
-
if (!isManaged(dir)) continue;
|
|
895
|
-
removeTree(dir);
|
|
896
|
-
skills.push(entry.name);
|
|
897
|
-
}
|
|
898
|
-
removeEmptyDir(skillsRoot);
|
|
899
|
-
}
|
|
900
|
-
if (skills.length > 0) report.removed.skills = skills;
|
|
901
|
-
|
|
902
|
-
if (bundleOwned) {
|
|
903
|
-
removeTree(bundleRoot);
|
|
904
|
-
report.removed.runtime = true;
|
|
905
|
-
}
|
|
906
|
-
return report;
|
|
907
398
|
}
|
|
908
399
|
|
|
909
400
|
module.exports = {
|
|
910
401
|
name: 'opencode',
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
BUNDLE_DIR,
|
|
914
|
-
pluginSource,
|
|
915
|
-
inspectOpenCodeHost,
|
|
402
|
+
PLUGIN_NAME,
|
|
403
|
+
buildHooksManifest,
|
|
916
404
|
resolveTarget,
|
|
917
405
|
install,
|
|
918
|
-
uninstall,
|
|
919
406
|
doctor,
|
|
407
|
+
uninstall,
|
|
920
408
|
};
|