ultra-builder-pro-cli 0.5.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/delivery-report.json +9 -0
- package/.ultra-template/docs/research/README.md +53 -0
- package/.ultra-template/specs/architecture.md +250 -0
- package/.ultra-template/specs/discovery.md +248 -0
- package/.ultra-template/specs/product.md +223 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +57 -0
- package/.ultra-template/tasks/tasks.json +33 -0
- package/.ultra-template/test-report.json +8 -0
- package/CHANGELOG.md +194 -0
- package/LICENSE +21 -0
- package/README.md +187 -0
- package/adapters/_shared/codex-assets.cjs +1128 -0
- package/adapters/_shared/codex-ultra-tools-entry.cjs +11 -0
- package/adapters/_shared/file-ops.cjs +115 -0
- package/adapters/_shared/frontmatter.cjs +52 -0
- package/adapters/_shared/handbook.cjs +144 -0
- package/adapters/_shared/md-to-toml.cjs +54 -0
- package/adapters/_shared/path-rewrite.cjs +79 -0
- package/adapters/_shared/rtk-detect.cjs +86 -0
- package/adapters/_shared/runtime-assets.cjs +70 -0
- package/adapters/_shared/settings-merge.cjs +76 -0
- package/adapters/_shared/tests/file-ops.test.cjs +54 -0
- package/adapters/_shared/tests/frontmatter.test.cjs +47 -0
- package/adapters/_shared/tests/handbook.test.cjs +86 -0
- package/adapters/_shared/tests/md-to-toml.test.cjs +48 -0
- package/adapters/_shared/tests/path-rewrite.test.cjs +60 -0
- package/adapters/_shared/tests/resolve-target.test.cjs +85 -0
- package/adapters/_shared/tests/rtk-detect.test.cjs +116 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +107 -0
- package/adapters/_shared/tests/settings-merge.test.cjs +85 -0
- package/adapters/_shared/tests/validate.test.cjs +30 -0
- package/adapters/_shared/validate.cjs +24 -0
- package/adapters/claude.js +182 -0
- package/adapters/codex.js +385 -0
- package/adapters/gemini.js +204 -0
- package/adapters/opencode.js +506 -0
- package/adapters/tests/claude.test.cjs +117 -0
- package/adapters/tests/codex-hook.test.cjs +90 -0
- package/adapters/tests/codex.test.cjs +356 -0
- package/adapters/tests/gemini.test.cjs +112 -0
- package/adapters/tests/opencode.test.cjs +246 -0
- package/agents/code-reviewer.md +102 -0
- package/agents/debugger.md +127 -0
- package/agents/review-code.md +125 -0
- package/agents/review-comments.md +98 -0
- package/agents/review-coordinator.md +146 -0
- package/agents/review-design.md +133 -0
- package/agents/review-errors.md +87 -0
- package/agents/review-tests.md +93 -0
- package/agents/tdd-runner.md +92 -0
- package/bin/handbook.js +54 -0
- package/bin/install.js +254 -0
- package/bin/orchestrator.js +214 -0
- package/commands/learn.md +41 -0
- package/commands/ultra-deliver.md +45 -0
- package/commands/ultra-dev.md +50 -0
- package/commands/ultra-init.md +51 -0
- package/commands/ultra-plan.md +49 -0
- package/commands/ultra-research.md +59 -0
- package/commands/ultra-status.md +46 -0
- package/commands/ultra-test.md +48 -0
- package/commands/ultra-think.md +45 -0
- package/docs/AGENT-CONTEXT.md +143 -0
- package/docs/ARCHITECTURE.md +197 -0
- package/docs/COMMIT-HASH-BACKFILL.md +101 -0
- package/docs/LEGACY-HERMES.md +1175 -0
- package/docs/PLAN.zh-CN.md +1683 -0
- package/docs/ROADMAP.md +102 -0
- package/docs/RUNTIME-COMPAT-MATRIX.md +98 -0
- package/docs/STATE-DB-ACCESS-POLICY.md +109 -0
- package/docs/USER-HANDBOOK-CONTRACT.md +61 -0
- package/hooks/README.md +24 -0
- package/hooks/active_task_context.py +44 -0
- package/hooks/adapters/codex.py +302 -0
- package/hooks/health_check.py +61 -0
- package/hooks/pre_stop_check.py +56 -0
- package/hooks/subagent_tracker.py +116 -0
- package/hooks/workflow_checkpoint.py +56 -0
- package/hooks/workflow_context.py +63 -0
- package/hooks/workflow_resume.py +50 -0
- package/mcp-server/lib/init-project.cjs +162 -0
- package/mcp-server/lib/llm-client.cjs +157 -0
- package/mcp-server/lib/llm-client.test.cjs +156 -0
- package/mcp-server/lib/plan-store.cjs +96 -0
- package/mcp-server/lib/plan-store.test.cjs +132 -0
- package/mcp-server/lib/prd-parser.cjs +110 -0
- package/mcp-server/lib/prd-parser.test.cjs +171 -0
- package/mcp-server/lib/pricing.cjs +53 -0
- package/mcp-server/lib/projector.cjs +158 -0
- package/mcp-server/lib/state-db.cjs +113 -0
- package/mcp-server/lib/state-db.test.cjs +116 -0
- package/mcp-server/lib/state-ops.cjs +725 -0
- package/mcp-server/lib/task-expander.cjs +123 -0
- package/mcp-server/lib/task-expander.test.cjs +253 -0
- package/mcp-server/lib/telemetry.cjs +72 -0
- package/mcp-server/lib/topo.cjs +131 -0
- package/mcp-server/lib/topo.test.cjs +158 -0
- package/mcp-server/server.cjs +482 -0
- package/mcp-server/tests/circuit-breaker.test.cjs +125 -0
- package/mcp-server/tests/commit-hash-flow.test.cjs +117 -0
- package/mcp-server/tests/concurrency-update.test.cjs +96 -0
- package/mcp-server/tests/concurrency.test.cjs +112 -0
- package/mcp-server/tests/db-ops.test.cjs +117 -0
- package/mcp-server/tests/init-project.test.cjs +141 -0
- package/mcp-server/tests/migrate.test.cjs +144 -0
- package/mcp-server/tests/projector.test.cjs +130 -0
- package/mcp-server/tests/server-contract.test.cjs +750 -0
- package/mcp-server/tests/staleness.test.cjs +190 -0
- package/mcp-server/tests/state-ops.test.cjs +286 -0
- package/mcp-server/tests/tagged-tasks.test.cjs +163 -0
- package/mcp-server/tests/telemetry.test.cjs +205 -0
- package/orchestrator/auto-merge.cjs +109 -0
- package/orchestrator/code-graph-watcher.cjs +121 -0
- package/orchestrator/daemon.cjs +112 -0
- package/orchestrator/dispatch-rules.cjs +93 -0
- package/orchestrator/parallel-orchestrator.cjs +195 -0
- package/orchestrator/planner/expand-prompt.cjs +62 -0
- package/orchestrator/planner/expand-prompt.test.cjs +52 -0
- package/orchestrator/planner/plan-builder.cjs +116 -0
- package/orchestrator/planner/plan-builder.test.cjs +165 -0
- package/orchestrator/planner/prd-prompt.cjs +44 -0
- package/orchestrator/planner/prd-prompt.test.cjs +30 -0
- package/orchestrator/recovery.cjs +70 -0
- package/orchestrator/session-runner.cjs +224 -0
- package/orchestrator/skill-miner.cjs +133 -0
- package/orchestrator/tests/auto-merge.test.cjs +216 -0
- package/orchestrator/tests/code-graph-watcher.test.cjs +183 -0
- package/orchestrator/tests/daemon.test.cjs +260 -0
- package/orchestrator/tests/dispatch-rules.test.cjs +122 -0
- package/orchestrator/tests/parallel-orchestrator.test.cjs +233 -0
- package/orchestrator/tests/phase-8b-gate.test.cjs +183 -0
- package/orchestrator/tests/recovery.test.cjs +190 -0
- package/orchestrator/tests/session-runner.test.cjs +314 -0
- package/orchestrator/tests/skill-miner.test.cjs +147 -0
- package/orchestrator/tests/worktree-manager.test.cjs +148 -0
- package/orchestrator/worktree-manager.cjs +131 -0
- package/output-styles/coding-vibes.md +67 -0
- package/output-styles/structural-thinking.md +61 -0
- package/package.json +109 -0
- package/settings.json +8 -0
- package/skills/cc-collab/SKILL.md +41 -0
- package/skills/code-review-expert/SKILL.md +118 -0
- package/skills/code-review-expert/references/code-quality-checklist.md +112 -0
- package/skills/code-review-expert/references/integration-checklist.md +46 -0
- package/skills/code-review-expert/references/removal-plan.md +52 -0
- package/skills/code-review-expert/references/security-checklist.md +100 -0
- package/skills/code-review-expert/references/solid-checklist.md +65 -0
- package/skills/codex-collab/SKILL.md +52 -0
- package/skills/codex-collab/evals/evals.json +40 -0
- package/skills/gemini-collab/SKILL.md +51 -0
- package/skills/gemini-collab/evals/evals.json +42 -0
- package/skills/integration-rules/SKILL.md +71 -0
- package/skills/learn/SKILL.md +117 -0
- package/skills/security-rules/SKILL.md +60 -0
- package/skills/testing-rules/SKILL.md +56 -0
- package/skills/ultra-deliver/SKILL.md +171 -0
- package/skills/ultra-dev/SKILL.md +324 -0
- package/skills/ultra-init/SKILL.md +148 -0
- package/skills/ultra-plan/SKILL.md +314 -0
- package/skills/ultra-research/SKILL.md +156 -0
- package/skills/ultra-research/steps/step-00-problem-validation.md +154 -0
- package/skills/ultra-research/steps/step-01-opportunity-discovery.md +172 -0
- package/skills/ultra-research/steps/step-02-market-assessment.md +195 -0
- package/skills/ultra-research/steps/step-03-competitive-landscape.md +197 -0
- package/skills/ultra-research/steps/step-04-product-strategy.md +196 -0
- package/skills/ultra-research/steps/step-05-assumptions-validation.md +234 -0
- package/skills/ultra-research/steps/step-10-user-personas.md +169 -0
- package/skills/ultra-research/steps/step-11-user-scenarios.md +181 -0
- package/skills/ultra-research/steps/step-20-user-stories.md +156 -0
- package/skills/ultra-research/steps/step-21-features-scope.md +157 -0
- package/skills/ultra-research/steps/step-22-success-metrics.md +195 -0
- package/skills/ultra-research/steps/step-30-architecture-context.md +154 -0
- package/skills/ultra-research/steps/step-31-solution-strategy.md +172 -0
- package/skills/ultra-research/steps/step-32-building-blocks.md +188 -0
- package/skills/ultra-research/steps/step-40-deployment.md +123 -0
- package/skills/ultra-research/steps/step-41-quality-risks.md +135 -0
- package/skills/ultra-research/steps/step-99-synthesis.md +188 -0
- package/skills/ultra-review/SKILL.md +437 -0
- package/skills/ultra-review/references/unified-schema.md +127 -0
- package/skills/ultra-review/scripts/review_verdict_update.py +99 -0
- package/skills/ultra-review/scripts/review_wait.py +120 -0
- package/skills/ultra-status/SKILL.md +162 -0
- package/skills/ultra-test/SKILL.md +195 -0
- package/skills/ultra-think/SKILL.md +109 -0
- package/skills/ultra-verify/SKILL.md +111 -0
- package/skills/ultra-verify/evals/evals.json +51 -0
- package/skills/ultra-verify/references/confidence-system.md +38 -0
- package/skills/ultra-verify/references/cross-verify-modes.md +73 -0
- package/skills/ultra-verify/scripts/verify_wait.py +131 -0
- package/spec/cli-protocol.md +118 -0
- package/spec/mcp-tools.yaml +658 -0
- package/spec/schemas/command-manifest.schema.json +47 -0
- package/spec/schemas/context-file.v4.5.schema.json +54 -0
- package/spec/schemas/mcp-tools.schema.json +136 -0
- package/spec/schemas/skill-manifest.schema.json +94 -0
- package/spec/schemas/state-db.sql +154 -0
- package/spec/schemas/tasks.v4.5.schema.json +66 -0
- package/ultra-tools/cli.cjs +121 -0
- package/ultra-tools/commands/db.cjs +175 -0
- package/ultra-tools/commands/legacy-memory.cjs +159 -0
- package/ultra-tools/commands/migrate.cjs +354 -0
- package/ultra-tools/commands/session.cjs +180 -0
- package/ultra-tools/commands/status.cjs +158 -0
- package/ultra-tools/commands/task.cjs +95 -0
- package/ultra-tools/legacy-memory.test.cjs +80 -0
- package/ultra-tools/status-command.test.cjs +132 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/** Build a native OpenCode Ultra Builder Pro plugin. */
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
copyTree,
|
|
11
|
+
ensureDir,
|
|
12
|
+
isManaged,
|
|
13
|
+
markManaged,
|
|
14
|
+
removeTree,
|
|
15
|
+
writeAtomic,
|
|
16
|
+
} = require('./_shared/file-ops.cjs');
|
|
17
|
+
const { buildMcpRuntime } = require('./_shared/codex-assets.cjs');
|
|
18
|
+
const {
|
|
19
|
+
CORE_PUBLIC_SKILLS,
|
|
20
|
+
skillsForRuntime,
|
|
21
|
+
} = require('./_shared/runtime-assets.cjs');
|
|
22
|
+
const {
|
|
23
|
+
parse: parseFm,
|
|
24
|
+
serialize: serializeFm,
|
|
25
|
+
lowercaseKeys,
|
|
26
|
+
} = require('./_shared/frontmatter.cjs');
|
|
27
|
+
const {
|
|
28
|
+
readJsonSafe,
|
|
29
|
+
} = require('./_shared/settings-merge.cjs');
|
|
30
|
+
|
|
31
|
+
const LEGACY_SENTINEL_KEY = '_ubp_manifest';
|
|
32
|
+
const MCP_SERVER_NAME = 'ultra-builder-pro';
|
|
33
|
+
const SOURCE_TAG = 'ubp';
|
|
34
|
+
const BUNDLE_DIR = '.ultra-builder-pro';
|
|
35
|
+
const MANAGED_TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
|
|
36
|
+
const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
|
|
37
|
+
const COMMAND_NAMES = CORE_PUBLIC_SKILLS.filter((name) => name !== 'ultra-review');
|
|
38
|
+
|
|
39
|
+
function resolveTarget(ctx = {}) {
|
|
40
|
+
if (ctx.configDir) return ctx.configDir;
|
|
41
|
+
if (ctx.scope === 'global') {
|
|
42
|
+
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
|
43
|
+
if (process.env.OPENCODE_CONFIG) return path.dirname(process.env.OPENCODE_CONFIG);
|
|
44
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
45
|
+
return path.join(xdg || path.join(ctx.homeDir || os.homedir(), '.config'), 'opencode');
|
|
46
|
+
}
|
|
47
|
+
return path.join(ctx.cwd || process.cwd(), '.opencode');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function resolveRepoRoot(ctx = {}) {
|
|
51
|
+
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function lowercaseFrontmatterTransform(buf, relPath) {
|
|
55
|
+
if (!relPath.endsWith('.md')) return buf;
|
|
56
|
+
const text = buf.toString('utf8');
|
|
57
|
+
const { fm, body } = parseFm(text);
|
|
58
|
+
if (!fm) return buf;
|
|
59
|
+
return Buffer.from(serializeFm(lowercaseKeys(fm), body), 'utf8');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function openCodeTextTransform(input, assetName = '') {
|
|
63
|
+
let text = String(input);
|
|
64
|
+
text = text.replaceAll('$CLAUDE_PLUGIN_ROOT/skills', '~/.config/opencode/skills');
|
|
65
|
+
text = text.replaceAll('~/.claude/skills', '~/.config/opencode/skills');
|
|
66
|
+
text = text.replaceAll('~/.claude/hooks', '~/.config/opencode/.ultra-builder-pro/hooks');
|
|
67
|
+
text = text.replaceAll('~/.claude', '~/.config/opencode');
|
|
68
|
+
text = text.replaceAll('CLAUDE.md', 'AGENTS.md');
|
|
69
|
+
text = text.replace(/@skills\/([a-z0-9-]+)\/SKILL\.md/g, '~/.config/opencode/skills/$1/SKILL.md');
|
|
70
|
+
|
|
71
|
+
text = text.replaceAll('TaskCreate/TaskUpdate (session-local)', 'the OpenCode plan (session-local)');
|
|
72
|
+
text = text.replaceAll('TaskCreate', 'an OpenCode plan item');
|
|
73
|
+
text = text.replaceAll('TaskUpdate', 'an OpenCode plan update');
|
|
74
|
+
text = text.replaceAll('TaskList', 'the current OpenCode plan');
|
|
75
|
+
text = text.replaceAll('`run_in_background: true`', 'concurrent OpenCode task execution');
|
|
76
|
+
text = text.replaceAll('run_in_background: true', 'concurrent OpenCode task execution');
|
|
77
|
+
text = text.replaceAll('Claude Task tool', 'OpenCode `task` tool');
|
|
78
|
+
text = text.replaceAll('Task tool', 'OpenCode `task` tool');
|
|
79
|
+
text = text.replaceAll('multiple Task calls', 'multiple OpenCode `task` calls');
|
|
80
|
+
text = text.replaceAll('Bash tool', 'OpenCode `bash` tool');
|
|
81
|
+
text = text.replaceAll('Read tool', 'OpenCode `read` tool');
|
|
82
|
+
text = text.replaceAll('Write tool', 'OpenCode `edit` tool');
|
|
83
|
+
|
|
84
|
+
text = text.replaceAll('MCP `review.run`', 'OpenCode native review subagents');
|
|
85
|
+
text = text.replaceAll('`review.run`', 'OpenCode native review subagents');
|
|
86
|
+
text = text.replaceAll('review.run', 'OpenCode native review subagents');
|
|
87
|
+
text = text.replaceAll('MCP `ask.question`', 'OpenCode `question` tool');
|
|
88
|
+
text = text.replaceAll('`ask.question`', 'OpenCode `question` tool');
|
|
89
|
+
text = text.replaceAll('ask.question', 'OpenCode question tool');
|
|
90
|
+
text = text.replaceAll('AskUserQuestion', 'OpenCode `question` tool');
|
|
91
|
+
text = text.replaceAll('ultra-tools ask --question … --options …', 'OpenCode `question` tool');
|
|
92
|
+
text = text.replaceAll('ultra-tools ask --question …', 'OpenCode `question` tool');
|
|
93
|
+
text = text.replaceAll('ultra-tools ask …', 'OpenCode `question` tool');
|
|
94
|
+
text = text.replaceAll('Claude runtime', 'OpenCode runtime');
|
|
95
|
+
text = text.replaceAll('Claude-only', 'OpenCode-only');
|
|
96
|
+
|
|
97
|
+
if (assetName === 'codex-collab' || assetName === 'gemini-collab') {
|
|
98
|
+
text = text.replaceAll('within Claude Code', 'from OpenCode');
|
|
99
|
+
text = text.replaceAll('Claude Code remains primary', 'OpenCode remains primary');
|
|
100
|
+
text = text.replaceAll('Claude orchestrates', 'OpenCode remains primary and orchestrates');
|
|
101
|
+
text = text.replaceAll('Claude synthesizes', 'OpenCode verifies and synthesizes');
|
|
102
|
+
text = text.replaceAll("Claude's", "OpenCode's");
|
|
103
|
+
text = text.replaceAll('proceed with Claude-only analysis', 'proceed with OpenCode-only analysis');
|
|
104
|
+
}
|
|
105
|
+
if (assetName === 'ultra-verify') {
|
|
106
|
+
text = text.replaceAll('claude-analysis.md', 'opencode-analysis.md');
|
|
107
|
+
text = text.replaceAll('Claude + Gemini + Codex', 'OpenCode + Gemini + Codex');
|
|
108
|
+
text = text.replaceAll('Claude Code', 'OpenCode');
|
|
109
|
+
text = text.replaceAll('Claude', 'OpenCode');
|
|
110
|
+
text = text.replace(
|
|
111
|
+
/Orchestrate OpenCode \+ Gemini \+ Codex for independent three-way analysis\./,
|
|
112
|
+
'OpenCode remains primary while Gemini and Codex provide independent read-only analyses.',
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (assetName === 'learn') {
|
|
116
|
+
text = text.replaceAll(
|
|
117
|
+
'~/.config/opencode/skills/learned/<name>_unverified.md',
|
|
118
|
+
'~/.config/opencode/skills/learned-<name>-unverified/SKILL.md',
|
|
119
|
+
);
|
|
120
|
+
text = text.replaceAll(
|
|
121
|
+
'~/.config/opencode/skills/learned/<pattern-slug>_unverified.md',
|
|
122
|
+
'~/.config/opencode/skills/learned-<pattern-slug>-unverified/SKILL.md',
|
|
123
|
+
);
|
|
124
|
+
text = text.replaceAll(
|
|
125
|
+
'~/.config/opencode/skills/learned/<slug>_unverified.md',
|
|
126
|
+
'~/.config/opencode/skills/learned-<slug>-unverified/SKILL.md',
|
|
127
|
+
);
|
|
128
|
+
text = text.replaceAll('append the `_unverified` suffix to the filename', 'append `-unverified` to the skill directory name');
|
|
129
|
+
text = text.replaceAll('Never overwrite an existing unverified file', 'Never overwrite an existing learned skill directory');
|
|
130
|
+
text = text.replaceAll('remove the `_unverified` suffix', 'rename the directory to remove `-unverified` and update the frontmatter name');
|
|
131
|
+
text = text.replaceAll('(`pattern-slug-2_unverified.md`)', '(`learned-pattern-slug-2-unverified/`)');
|
|
132
|
+
}
|
|
133
|
+
return text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function openCodeCommandTransform(buf, relPath, commandName) {
|
|
137
|
+
if (!relPath.endsWith('.md')) return buf;
|
|
138
|
+
const { fm, body } = parseFm(buf.toString('utf8'));
|
|
139
|
+
if (!fm) return Buffer.from(openCodeTextTransform(buf.toString('utf8'), commandName), 'utf8');
|
|
140
|
+
const nativeFm = {
|
|
141
|
+
description: openCodeTextTransform(fm.description || `${commandName} workflow`, commandName)
|
|
142
|
+
.replace(/\s+/g, ' ')
|
|
143
|
+
.trim(),
|
|
144
|
+
};
|
|
145
|
+
return Buffer.from(serializeFm(nativeFm, openCodeTextTransform(body, commandName)), 'utf8');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function openCodeSkillAssetTransform(buf, relPath, skillName) {
|
|
149
|
+
if (!relPath.endsWith('.md')) return buf;
|
|
150
|
+
const text = buf.toString('utf8');
|
|
151
|
+
const { fm, body } = parseFm(text);
|
|
152
|
+
if (!fm) return Buffer.from(openCodeTextTransform(text, skillName), 'utf8');
|
|
153
|
+
const nativeFm = {
|
|
154
|
+
name: String(fm.name || skillName),
|
|
155
|
+
description: openCodeTextTransform(fm.description || `${skillName} workflow`, skillName)
|
|
156
|
+
.replace(/\s+/g, ' ')
|
|
157
|
+
.trim(),
|
|
158
|
+
};
|
|
159
|
+
return Buffer.from(serializeFm(nativeFm, openCodeTextTransform(body, skillName)), 'utf8');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function withManagedMarker(buf) {
|
|
163
|
+
const text = buf.toString('utf8');
|
|
164
|
+
const { fm, body } = parseFm(text);
|
|
165
|
+
if (!fm) return Buffer.from(`${MANAGED_TEXT_MARKER}\n${text}`, 'utf8');
|
|
166
|
+
const cleanBody = body.replace(`${MANAGED_TEXT_MARKER}\n`, '');
|
|
167
|
+
return Buffer.from(serializeFm(fm, `${MANAGED_TEXT_MARKER}\n${cleanBody}`), 'utf8');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function sourceToolSet(value) {
|
|
171
|
+
if (typeof value === 'string') {
|
|
172
|
+
return new Set(value.split(',').map((name) => name.trim().toLowerCase()).filter(Boolean));
|
|
173
|
+
}
|
|
174
|
+
if (Array.isArray(value)) {
|
|
175
|
+
return new Set(value.map((name) => String(name).trim().toLowerCase()).filter(Boolean));
|
|
176
|
+
}
|
|
177
|
+
if (value && typeof value === 'object') {
|
|
178
|
+
return new Set(Object.entries(value)
|
|
179
|
+
.filter(([, enabled]) => enabled)
|
|
180
|
+
.map(([name]) => name.trim().toLowerCase()));
|
|
181
|
+
}
|
|
182
|
+
return new Set();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function openCodeAgentTransform(buf, relPath) {
|
|
186
|
+
if (!relPath.endsWith('.md')) return buf;
|
|
187
|
+
const text = buf.toString('utf8');
|
|
188
|
+
const { fm, body } = parseFm(text);
|
|
189
|
+
if (!fm) return buf;
|
|
190
|
+
|
|
191
|
+
const tools = sourceToolSet(fm.tools);
|
|
192
|
+
const allows = (name) => tools.has(name) ? 'allow' : 'deny';
|
|
193
|
+
const skills = Array.isArray(fm.skills)
|
|
194
|
+
? fm.skills.filter((name) => typeof name === 'string' && name.trim()).map((name) => name.trim())
|
|
195
|
+
: [];
|
|
196
|
+
const skillPermission = skills.length > 0
|
|
197
|
+
? Object.fromEntries([['*', 'deny'], ...skills.map((name) => [name, 'allow'])])
|
|
198
|
+
: 'deny';
|
|
199
|
+
const permission = {
|
|
200
|
+
read: allows('read'),
|
|
201
|
+
edit: tools.has('write') || tools.has('edit') ? 'allow' : 'deny',
|
|
202
|
+
glob: allows('glob'),
|
|
203
|
+
grep: allows('grep'),
|
|
204
|
+
list: allows('glob'),
|
|
205
|
+
bash: allows('bash'),
|
|
206
|
+
task: 'deny',
|
|
207
|
+
external_directory: 'deny',
|
|
208
|
+
todowrite: 'deny',
|
|
209
|
+
question: 'deny',
|
|
210
|
+
webfetch: 'deny',
|
|
211
|
+
websearch: 'deny',
|
|
212
|
+
codesearch: 'deny',
|
|
213
|
+
lsp: 'deny',
|
|
214
|
+
doom_loop: 'deny',
|
|
215
|
+
skill: skillPermission,
|
|
216
|
+
};
|
|
217
|
+
const nativeFm = {
|
|
218
|
+
description: fm.description,
|
|
219
|
+
mode: 'subagent',
|
|
220
|
+
...(Number.isInteger(fm.maxTurns) && fm.maxTurns > 0 ? { steps: fm.maxTurns } : {}),
|
|
221
|
+
permission,
|
|
222
|
+
};
|
|
223
|
+
return Buffer.from(
|
|
224
|
+
serializeFm(nativeFm, openCodeTextTransform(body, path.basename(relPath, '.md'))),
|
|
225
|
+
'utf8',
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isManagedTextFile(file) {
|
|
230
|
+
if (!fs.existsSync(file)) return false;
|
|
231
|
+
return fs.readFileSync(file, 'utf8').includes(MANAGED_TEXT_MARKER);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function assertManagedTextTarget(file, kind) {
|
|
235
|
+
if (fs.existsSync(file) && !isManagedTextFile(file)) {
|
|
236
|
+
throw new Error(`refusing to replace unmanaged OpenCode ${kind}: ${file}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function removeStaleManagedFiles(dir, expected) {
|
|
241
|
+
if (!fs.existsSync(dir)) return [];
|
|
242
|
+
const keep = new Set(expected);
|
|
243
|
+
const removed = [];
|
|
244
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
245
|
+
if (!entry.isFile() || keep.has(entry.name)) continue;
|
|
246
|
+
const file = path.join(dir, entry.name);
|
|
247
|
+
if (!isManagedTextFile(file)) continue;
|
|
248
|
+
fs.unlinkSync(file);
|
|
249
|
+
removed.push(entry.name);
|
|
250
|
+
}
|
|
251
|
+
return removed;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function removeEmptyDir(dir) {
|
|
255
|
+
if (!fs.existsSync(dir)) return;
|
|
256
|
+
if (fs.readdirSync(dir).length === 0) fs.rmdirSync(dir);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function copyCommands(repoRoot, target) {
|
|
260
|
+
const output = path.join(target, 'commands');
|
|
261
|
+
ensureDir(output);
|
|
262
|
+
removeStaleManagedFiles(output, COMMAND_NAMES.map((name) => `${name}.md`));
|
|
263
|
+
const copied = [];
|
|
264
|
+
for (const name of COMMAND_NAMES) {
|
|
265
|
+
const file = `${name}.md`;
|
|
266
|
+
const source = path.join(repoRoot, 'commands', file);
|
|
267
|
+
if (!fs.existsSync(source)) throw new Error(`missing allowlisted OpenCode command: ${file}`);
|
|
268
|
+
const transformed = withManagedMarker(openCodeCommandTransform(fs.readFileSync(source), file, name));
|
|
269
|
+
writeAtomic(path.join(output, file), transformed);
|
|
270
|
+
copied.push(file);
|
|
271
|
+
}
|
|
272
|
+
return copied;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function copySkills(repoRoot, target) {
|
|
276
|
+
const copied = [];
|
|
277
|
+
const names = skillsForRuntime('opencode');
|
|
278
|
+
const skillsRoot = path.join(target, 'skills');
|
|
279
|
+
ensureDir(skillsRoot);
|
|
280
|
+
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
281
|
+
if (!entry.isDirectory() || names.includes(entry.name)) continue;
|
|
282
|
+
const candidate = path.join(skillsRoot, entry.name);
|
|
283
|
+
if (isManaged(candidate)) removeTree(candidate);
|
|
284
|
+
}
|
|
285
|
+
for (const name of names) {
|
|
286
|
+
const source = path.join(repoRoot, 'skills', name);
|
|
287
|
+
if (!fs.existsSync(path.join(source, 'SKILL.md'))) {
|
|
288
|
+
throw new Error(`missing allowlisted OpenCode skill: ${name}`);
|
|
289
|
+
}
|
|
290
|
+
const destination = path.join(skillsRoot, name);
|
|
291
|
+
if (fs.existsSync(destination)) removeTree(destination);
|
|
292
|
+
const files = copyTree(source, destination, {
|
|
293
|
+
transform: (buf, relPath) => openCodeSkillAssetTransform(buf, relPath, name),
|
|
294
|
+
});
|
|
295
|
+
markManaged(destination, { adapter: 'opencode', asset: 'skill', name });
|
|
296
|
+
copied.push(...files.map((rel) => path.join(name, rel)));
|
|
297
|
+
}
|
|
298
|
+
return copied;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function copyAgents(repoRoot, target) {
|
|
302
|
+
const sourceRoot = path.join(repoRoot, 'agents');
|
|
303
|
+
const output = path.join(target, 'agents');
|
|
304
|
+
const names = fs.readdirSync(sourceRoot).filter((name) => name.endsWith('.md')).sort();
|
|
305
|
+
ensureDir(output);
|
|
306
|
+
removeStaleManagedFiles(output, names);
|
|
307
|
+
for (const name of names) {
|
|
308
|
+
const source = fs.readFileSync(path.join(sourceRoot, name));
|
|
309
|
+
writeAtomic(path.join(output, name), withManagedMarker(openCodeAgentTransform(source, name)));
|
|
310
|
+
}
|
|
311
|
+
return names;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function preflightAssets(repoRoot, target) {
|
|
315
|
+
for (const name of COMMAND_NAMES) {
|
|
316
|
+
assertManagedTextTarget(path.join(target, 'commands', `${name}.md`), 'command');
|
|
317
|
+
}
|
|
318
|
+
for (const name of skillsForRuntime('opencode')) {
|
|
319
|
+
const destination = path.join(target, 'skills', name);
|
|
320
|
+
if (fs.existsSync(destination) && !isManaged(destination)) {
|
|
321
|
+
throw new Error(`refusing to replace unmanaged OpenCode skill: ${destination}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
for (const name of fs.readdirSync(path.join(repoRoot, 'agents')).filter((entry) => entry.endsWith('.md'))) {
|
|
325
|
+
assertManagedTextTarget(path.join(target, 'agents', name), 'agent');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const pluginFile = path.join(target, 'plugins', 'ultra-builder-pro.js');
|
|
329
|
+
if (fs.existsSync(pluginFile) && !fs.readFileSync(pluginFile, 'utf8').startsWith(PLUGIN_MARKER)) {
|
|
330
|
+
throw new Error(`refusing to replace unmanaged OpenCode plugin: ${pluginFile}`);
|
|
331
|
+
}
|
|
332
|
+
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
333
|
+
if (fs.existsSync(bundleRoot) && !isManaged(bundleRoot)) {
|
|
334
|
+
throw new Error(`refusing to replace unmanaged OpenCode runtime bundle: ${bundleRoot}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function pluginSource() {
|
|
339
|
+
return `// Managed by Ultra Builder Pro. Rebuild through the adapter; do not edit in place.
|
|
340
|
+
import fs from "node:fs";
|
|
341
|
+
import path from "node:path";
|
|
342
|
+
|
|
343
|
+
const TERMINAL = new Set(["committed", "completed", "done", "cancelled"]);
|
|
344
|
+
|
|
345
|
+
function findActiveWorkflow(directory) {
|
|
346
|
+
let current = path.resolve(directory);
|
|
347
|
+
while (true) {
|
|
348
|
+
const file = path.join(current, ".ultra", "workflow-state.json");
|
|
349
|
+
if (fs.existsSync(file)) {
|
|
350
|
+
try {
|
|
351
|
+
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
352
|
+
return state && !TERMINAL.has(state.status) ? { root: current, state } : null;
|
|
353
|
+
} catch (error) {
|
|
354
|
+
process.stderr.write(\`[ultra-builder-pro] cannot read \${file}: \${error.message}\\n\`);
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const parent = path.dirname(current);
|
|
359
|
+
if (parent === current) return null;
|
|
360
|
+
current = parent;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function contextText(active) {
|
|
365
|
+
const state = active.state;
|
|
366
|
+
return [
|
|
367
|
+
"[Ultra active workflow]",
|
|
368
|
+
\`Project: \${active.root}\`,
|
|
369
|
+
\`Command: \${state.command ?? "unknown"}\`,
|
|
370
|
+
\`Task: \${state.task_id ?? state.task ?? "unknown"}\`,
|
|
371
|
+
\`Step: \${state.step ?? "unknown"}\`,
|
|
372
|
+
\`Status: \${state.status ?? "active"}\`,
|
|
373
|
+
"Authority: .ultra/state.db; recovery: .ultra/workflow-state.json.",
|
|
374
|
+
].join("\\n");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
|
|
378
|
+
const root = worktree || directory;
|
|
379
|
+
let active = findActiveWorkflow(root);
|
|
380
|
+
const refresh = () => { active = findActiveWorkflow(root); };
|
|
381
|
+
return {
|
|
382
|
+
event: async ({ event }) => {
|
|
383
|
+
if (["session.created", "session.compacted", "session.idle"].includes(event?.type)) refresh();
|
|
384
|
+
},
|
|
385
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
386
|
+
refresh();
|
|
387
|
+
if (active) output.system.push(contextText(active));
|
|
388
|
+
},
|
|
389
|
+
"experimental.session.compacting": async (_input, output) => {
|
|
390
|
+
refresh();
|
|
391
|
+
if (active) output.context.push(contextText(active));
|
|
392
|
+
},
|
|
393
|
+
"tool.execute.before": async () => { refresh(); },
|
|
394
|
+
"tool.execute.after": async () => { refresh(); },
|
|
395
|
+
};
|
|
396
|
+
};
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function install(ctx = {}) {
|
|
401
|
+
const target = resolveTarget(ctx);
|
|
402
|
+
const repoRoot = resolveRepoRoot(ctx);
|
|
403
|
+
ensureDir(target);
|
|
404
|
+
const report = { target, copied: {}, config: { updated: false } };
|
|
405
|
+
preflightAssets(repoRoot, target);
|
|
406
|
+
report.copied.commands = copyCommands(repoRoot, target);
|
|
407
|
+
report.copied.skills = copySkills(repoRoot, target);
|
|
408
|
+
report.copied.agents = copyAgents(repoRoot, target);
|
|
409
|
+
|
|
410
|
+
ensureDir(path.join(target, 'plugins'));
|
|
411
|
+
writeAtomic(path.join(target, 'plugins', 'ultra-builder-pro.js'), pluginSource());
|
|
412
|
+
report.copied.plugins = ['ultra-builder-pro.js'];
|
|
413
|
+
|
|
414
|
+
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
415
|
+
if (fs.existsSync(bundleRoot)) removeTree(bundleRoot);
|
|
416
|
+
ensureDir(bundleRoot);
|
|
417
|
+
const runtime = buildMcpRuntime(repoRoot, bundleRoot, { runtime: 'opencode' });
|
|
418
|
+
markManaged(bundleRoot, { adapter: 'opencode', asset: 'runtime-bundle' });
|
|
419
|
+
const configFile = path.join(target, 'opencode.json');
|
|
420
|
+
const existing = readJsonSafe(configFile, { rescue: true });
|
|
421
|
+
const mcp = { ...(existing.mcp || {}) };
|
|
422
|
+
mcp[MCP_SERVER_NAME] = {
|
|
423
|
+
type: 'local',
|
|
424
|
+
enabled: true,
|
|
425
|
+
command: [process.execPath, runtime.launcher],
|
|
426
|
+
};
|
|
427
|
+
const next = { ...existing, mcp };
|
|
428
|
+
delete next[LEGACY_SENTINEL_KEY];
|
|
429
|
+
writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
|
|
430
|
+
report.config.updated = true;
|
|
431
|
+
return report;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function uninstall(ctx = {}) {
|
|
435
|
+
const target = resolveTarget(ctx);
|
|
436
|
+
const report = { target, removed: {}, config: { updated: false } };
|
|
437
|
+
const pluginFile = path.join(target, 'plugins', 'ultra-builder-pro.js');
|
|
438
|
+
const bundleRoot = path.join(target, BUNDLE_DIR);
|
|
439
|
+
const pluginOwned = fs.existsSync(pluginFile)
|
|
440
|
+
&& fs.readFileSync(pluginFile, 'utf8').startsWith(PLUGIN_MARKER);
|
|
441
|
+
const bundleOwned = fs.existsSync(bundleRoot) && isManaged(bundleRoot);
|
|
442
|
+
const configFile = path.join(target, 'opencode.json');
|
|
443
|
+
if (fs.existsSync(configFile)) {
|
|
444
|
+
const existing = readJsonSafe(configFile);
|
|
445
|
+
if (pluginOwned || bundleOwned || existing[LEGACY_SENTINEL_KEY]) {
|
|
446
|
+
const mcp = { ...(existing.mcp || {}) };
|
|
447
|
+
delete mcp[MCP_SERVER_NAME];
|
|
448
|
+
const next = { ...existing, mcp };
|
|
449
|
+
delete next[LEGACY_SENTINEL_KEY];
|
|
450
|
+
if (Object.keys(next.mcp || {}).length === 0) delete next.mcp;
|
|
451
|
+
writeAtomic(configFile, JSON.stringify(next, null, 2) + '\n');
|
|
452
|
+
report.config.updated = true;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (pluginOwned) {
|
|
456
|
+
fs.unlinkSync(pluginFile);
|
|
457
|
+
report.removed.plugin = true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (const sub of ['commands', 'agents']) {
|
|
461
|
+
const dir = path.join(target, sub);
|
|
462
|
+
const removed = [];
|
|
463
|
+
if (fs.existsSync(dir)) {
|
|
464
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
465
|
+
if (!entry.isFile()) continue;
|
|
466
|
+
const file = path.join(dir, entry.name);
|
|
467
|
+
if (!isManagedTextFile(file)) continue;
|
|
468
|
+
fs.unlinkSync(file);
|
|
469
|
+
removed.push(entry.name);
|
|
470
|
+
}
|
|
471
|
+
removeEmptyDir(dir);
|
|
472
|
+
}
|
|
473
|
+
if (removed.length > 0) report.removed[sub] = removed;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const skillsRoot = path.join(target, 'skills');
|
|
477
|
+
const skills = [];
|
|
478
|
+
if (fs.existsSync(skillsRoot)) {
|
|
479
|
+
for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
|
|
480
|
+
if (!entry.isDirectory()) continue;
|
|
481
|
+
const dir = path.join(skillsRoot, entry.name);
|
|
482
|
+
if (!isManaged(dir)) continue;
|
|
483
|
+
removeTree(dir);
|
|
484
|
+
skills.push(entry.name);
|
|
485
|
+
}
|
|
486
|
+
removeEmptyDir(skillsRoot);
|
|
487
|
+
}
|
|
488
|
+
if (skills.length > 0) report.removed.skills = skills;
|
|
489
|
+
|
|
490
|
+
if (bundleOwned) {
|
|
491
|
+
removeTree(bundleRoot);
|
|
492
|
+
report.removed.runtime = true;
|
|
493
|
+
}
|
|
494
|
+
return report;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = {
|
|
498
|
+
name: 'opencode',
|
|
499
|
+
MCP_SERVER_NAME,
|
|
500
|
+
SOURCE_TAG,
|
|
501
|
+
BUNDLE_DIR,
|
|
502
|
+
pluginSource,
|
|
503
|
+
resolveTarget,
|
|
504
|
+
install,
|
|
505
|
+
uninstall,
|
|
506
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const os = require('node:os');
|
|
7
|
+
const path = require('node:path');
|
|
8
|
+
|
|
9
|
+
const claude = require('../claude.js');
|
|
10
|
+
const { skillsForRuntime, WORKFLOW_HOOK_FILES } = require('../_shared/runtime-assets.cjs');
|
|
11
|
+
|
|
12
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
13
|
+
|
|
14
|
+
function mkTarget() {
|
|
15
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-claude-'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function skillNames(root) {
|
|
19
|
+
return fs.readdirSync(path.join(root, 'skills'), { withFileTypes: true })
|
|
20
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(root, 'skills', entry.name, 'SKILL.md')))
|
|
21
|
+
.map((entry) => entry.name)
|
|
22
|
+
.sort();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('install builds the Claude-native plugin from the explicit Ultra allowlist', () => {
|
|
26
|
+
const parent = mkTarget();
|
|
27
|
+
const target = path.join(parent, 'skills', 'ultra-builder-pro');
|
|
28
|
+
try {
|
|
29
|
+
const report = claude.install({ configDir: parent, repoRoot: REPO_ROOT });
|
|
30
|
+
assert.deepEqual(skillNames(target), skillsForRuntime('claude').sort());
|
|
31
|
+
assert.deepEqual(report.copied.hooks.sort(), WORKFLOW_HOOK_FILES.slice().sort());
|
|
32
|
+
assert.ok(fs.existsSync(path.join(target, '.claude-plugin', 'plugin.json')));
|
|
33
|
+
assert.ok(fs.existsSync(path.join(target, 'hooks', 'hooks.json')));
|
|
34
|
+
assert.ok(fs.existsSync(path.join(target, '.mcp.json')));
|
|
35
|
+
assert.ok(fs.existsSync(path.join(target, 'agents', 'code-reviewer.md')));
|
|
36
|
+
|
|
37
|
+
const hooks = JSON.parse(fs.readFileSync(path.join(target, 'hooks', 'hooks.json'), 'utf8'));
|
|
38
|
+
const serialized = JSON.stringify(hooks);
|
|
39
|
+
for (const name of WORKFLOW_HOOK_FILES) assert.match(serialized, new RegExp(name.replace('.', '\\.')));
|
|
40
|
+
assert.doesNotMatch(serialized, /memory|recall|journal|observation_capture|user_prompt_capture|block_dangerous|post_edit_guard/);
|
|
41
|
+
assert.match(serialized, /\$\{CLAUDE_PLUGIN_ROOT\}/);
|
|
42
|
+
} finally {
|
|
43
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('Claude plugin collaboration and learn workflows are safe native plugin assets', () => {
|
|
48
|
+
const parent = mkTarget();
|
|
49
|
+
const target = path.join(parent, 'skills', 'ultra-builder-pro');
|
|
50
|
+
try {
|
|
51
|
+
claude.install({ configDir: parent, repoRoot: REPO_ROOT });
|
|
52
|
+
const read = (name) => fs.readFileSync(path.join(target, 'skills', name, 'SKILL.md'), 'utf8');
|
|
53
|
+
const learn = read('learn');
|
|
54
|
+
const codexCollab = read('codex-collab');
|
|
55
|
+
const geminiCollab = read('gemini-collab');
|
|
56
|
+
const verify = read('ultra-verify');
|
|
57
|
+
const review = read('ultra-review');
|
|
58
|
+
|
|
59
|
+
assert.match(learn, /~\/.claude\/skills\/learned-<pattern-slug>-unverified\/SKILL\.md/);
|
|
60
|
+
assert.doesNotMatch(learn, /_unverified\.md/);
|
|
61
|
+
assert.match(codexCollab, /-s read-only/);
|
|
62
|
+
assert.match(geminiCollab, /--approval-mode plan/);
|
|
63
|
+
assert.match(verify, /--approval-mode plan/);
|
|
64
|
+
assert.match(verify, /-s read-only/);
|
|
65
|
+
assert.match(verify, /\$CLAUDE_PLUGIN_ROOT\/skills\/ultra-verify\/scripts\/verify_wait\.py/);
|
|
66
|
+
assert.match(review, /\$CLAUDE_PLUGIN_ROOT\/skills\/ultra-review\/scripts\/review_wait\.py/);
|
|
67
|
+
|
|
68
|
+
for (const [name, contents] of Object.entries({ codexCollab, geminiCollab, verify })) {
|
|
69
|
+
assert.doesNotMatch(contents, /--yolo|--full-auto|\/codex:/, name);
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('global target is a Claude skills-directory plugin', () => {
|
|
77
|
+
const homeDir = mkTarget();
|
|
78
|
+
try {
|
|
79
|
+
const ctx = { scope: 'global', homeDir };
|
|
80
|
+
assert.equal(claude.resolveTarget(ctx), path.join(homeDir, '.claude'));
|
|
81
|
+
assert.equal(claude.resolvePluginRoot(ctx), path.join(homeDir, '.claude', 'skills', 'ultra-builder-pro'));
|
|
82
|
+
} finally {
|
|
83
|
+
fs.rmSync(homeDir, { recursive: true, force: true });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('reinstall is deterministic and uninstall removes only the managed plugin root', () => {
|
|
88
|
+
const parent = mkTarget();
|
|
89
|
+
const target = path.join(parent, 'skills', 'ultra-builder-pro');
|
|
90
|
+
const sibling = path.join(parent, 'user-plugin');
|
|
91
|
+
try {
|
|
92
|
+
fs.mkdirSync(sibling);
|
|
93
|
+
fs.writeFileSync(path.join(sibling, 'keep.txt'), 'keep');
|
|
94
|
+
claude.install({ configDir: parent, repoRoot: REPO_ROOT });
|
|
95
|
+
claude.install({ configDir: parent, repoRoot: REPO_ROOT });
|
|
96
|
+
claude.uninstall({ configDir: parent });
|
|
97
|
+
assert.ok(!fs.existsSync(target));
|
|
98
|
+
assert.equal(fs.readFileSync(path.join(sibling, 'keep.txt'), 'utf8'), 'keep');
|
|
99
|
+
} finally {
|
|
100
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('install refuses to replace an unmanaged directory', () => {
|
|
105
|
+
const parent = mkTarget();
|
|
106
|
+
const target = path.join(parent, 'skills', 'ultra-builder-pro');
|
|
107
|
+
try {
|
|
108
|
+
fs.mkdirSync(target, { recursive: true });
|
|
109
|
+
fs.writeFileSync(path.join(target, 'user.txt'), 'keep');
|
|
110
|
+
assert.throws(
|
|
111
|
+
() => claude.install({ configDir: parent, repoRoot: REPO_ROOT }),
|
|
112
|
+
/unmanaged Claude plugin/,
|
|
113
|
+
);
|
|
114
|
+
} finally {
|
|
115
|
+
fs.rmSync(parent, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
});
|