ultra-builder-pro-cli 5.0.0 → 6.0.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/CHANGELOG.md +113 -3
- package/README.md +67 -478
- package/README.zh-CN.md +49 -422
- package/adapters/_shared/frontmatter.cjs +1 -18
- package/adapters/_shared/plugin-core.cjs +17 -83
- package/adapters/_shared/runtime-assets.cjs +6 -120
- package/adapters/claude.js +8 -24
- package/adapters/codex.js +16 -38
- package/bin/install.js +35 -57
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +13 -101
- package/docs/examples/drive/claude-reviewer.sh +53 -0
- package/docs/examples/drive/claude-writer.sh +43 -0
- package/engine/FORMATS.md +1051 -0
- package/engine/checks.cjs +139 -0
- package/engine/cli.cjs +148 -0
- package/engine/completion.cjs +48 -0
- package/engine/context.cjs +94 -0
- package/engine/drive.cjs +220 -0
- package/engine/expect-worker.cjs +5 -0
- package/engine/expect.cjs +33 -0
- package/engine/formats.cjs +189 -0
- package/engine/git-state.cjs +112 -0
- package/engine/ledger.cjs +205 -0
- package/engine/loading.cjs +28 -0
- package/engine/merge.cjs +273 -0
- package/engine/milestone.cjs +89 -0
- package/engine/plan.cjs +232 -0
- package/engine/project.cjs +5 -0
- package/engine/signals.cjs +3 -0
- package/engine/states.cjs +175 -0
- package/hooks/session-start.cjs +54 -0
- package/package.json +17 -52
- package/skills/ultra/SKILL.md +395 -0
- package/.ultra-template/changes/abandoned/.gitkeep +0 -0
- package/.ultra-template/changes/active/.gitkeep +0 -1
- package/.ultra-template/changes/archive/.gitkeep +0 -1
- package/.ultra-template/contexts/TEMPLATE.md +0 -135
- package/.ultra-template/decisions/.gitkeep +0 -1
- package/.ultra-template/evidence/.gitkeep +0 -1
- package/.ultra-template/north-star.md +0 -52
- package/.ultra-template/project-brief.md +0 -28
- package/.ultra-template/research/.gitkeep +0 -1
- package/.ultra-template/specs/architecture.md +0 -81
- package/.ultra-template/specs/discovery.md +0 -74
- package/.ultra-template/specs/product.md +0 -80
- package/.ultra-template/specs/research-distillate.md +0 -57
- package/.ultra-template/tasks.json +0 -4
- package/.ultra-template/test-report.json +0 -44
- package/adapters/_shared/host-profile.cjs +0 -5
- package/adapters/grok.js +0 -242
- package/adapters/kimi.js +0 -232
- package/adapters/opencode.js +0 -408
- package/adapters/zcode.js +0 -375
- package/bin/delegate-worker.cjs +0 -5
- package/bin/delegate.cjs +0 -5
- package/docs/ARCHITECTURE.md +0 -274
- package/docs/ARTIFACT-AUTHORITY.md +0 -305
- package/docs/DECISIONS.md +0 -229
- package/docs/PHILOSOPHY.md +0 -261
- package/docs/RUNTIME-COMPAT-MATRIX.md +0 -315
- package/docs/SKILL-AUTHORING.md +0 -113
- package/docs/WORKFLOW-LIFECYCLE.md +0 -350
- package/docs/evals/adversarial-review-2026-08-14.md +0 -84
- package/docs/evals/ultra-5-0-journey-results.md +0 -62
- package/docs/evals/ultra-5-0-live-acp-conformance.md +0 -93
- package/docs/evals/ultra-5-0-route-journeys.md +0 -145
- package/docs/evals/zcode-automation-2026-08-14.md +0 -103
- package/hooks/README.md +0 -52
- package/hooks/_common.py +0 -1779
- package/hooks/adapters/codex.py +0 -295
- package/hooks/adapters/grok.py +0 -134
- package/hooks/adapters/kimi.py +0 -156
- package/hooks/adapters/zcode.py +0 -227
- package/hooks/block_dangerous_commands.py +0 -201
- package/hooks/compact_context.py +0 -224
- package/hooks/mid_workflow_recall.py +0 -47
- package/hooks/post_edit_guard.py +0 -215
- package/hooks/session_context.py +0 -741
- package/skills/ultra-change/SKILL.md +0 -165
- package/skills/ultra-change/references/change-contract.md +0 -199
- package/skills/ultra-change/references/execution-grant.md +0 -109
- package/skills/ultra-change/references/primary-transfer.md +0 -273
- package/skills/ultra-change/scripts/validate_primary_transfer.cjs +0 -1928
- package/skills/ultra-delegate/SKILL.md +0 -195
- package/skills/ultra-delegate/patches/zcode-acp-0.1.0-zcode-0.16.5.patch +0 -184
- package/skills/ultra-delegate/references/delegation-contract.md +0 -138
- package/skills/ultra-delegate/scripts/acp-conformance.cjs +0 -287
- package/skills/ultra-delegate/scripts/acp-driver.cjs +0 -762
- package/skills/ultra-delegate/scripts/agent-execution-driver.cjs +0 -320
- package/skills/ultra-delegate/scripts/cli-driver.cjs +0 -61
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-read-only.md +0 -15
- package/skills/ultra-delegate/scripts/delegate-profiles/kimi-write.md +0 -17
- package/skills/ultra-delegate/scripts/delegate-worker.cjs +0 -328
- package/skills/ultra-delegate/scripts/delegate.cjs +0 -690
- package/skills/ultra-delegate/scripts/delegate_wait.py +0 -86
- package/skills/ultra-delegate/scripts/host-profile.cjs +0 -267
- package/skills/ultra-deliver/SKILL.md +0 -194
- package/skills/ultra-deliver/references/baseline-reconciliation.md +0 -41
- package/skills/ultra-dev/SKILL.md +0 -265
- package/skills/ultra-dev/references/debugging.md +0 -22
- package/skills/ultra-domain-modeling/SKILL.md +0 -79
- package/skills/ultra-grilling/SKILL.md +0 -121
- package/skills/ultra-grilling/references/reframing.md +0 -76
- package/skills/ultra-init/SKILL.md +0 -143
- package/skills/ultra-init/references/brownfield-adoption.md +0 -32
- package/skills/ultra-init/scripts/init_project.cjs +0 -1031
- package/skills/ultra-plan/SKILL.md +0 -160
- package/skills/ultra-plan/references/task-evidence-v2.md +0 -300
- package/skills/ultra-plan/scripts/validate_task_evidence.cjs +0 -859
- package/skills/ultra-research/SKILL.md +0 -167
- package/skills/ultra-research/references/00-problem-validation.md +0 -36
- package/skills/ultra-research/references/01-opportunity-discovery.md +0 -33
- package/skills/ultra-research/references/02-market-assessment.md +0 -31
- package/skills/ultra-research/references/03-alternatives.md +0 -31
- package/skills/ultra-research/references/04-product-strategy.md +0 -41
- package/skills/ultra-research/references/05-assumptions-validation.md +0 -31
- package/skills/ultra-research/references/10-user-personas.md +0 -30
- package/skills/ultra-research/references/11-user-scenarios.md +0 -28
- package/skills/ultra-research/references/20-user-stories.md +0 -31
- package/skills/ultra-research/references/21-features-scope.md +0 -38
- package/skills/ultra-research/references/22-success-metrics.md +0 -33
- package/skills/ultra-research/references/30-architecture-context.md +0 -30
- package/skills/ultra-research/references/31-solution-strategy.md +0 -28
- package/skills/ultra-research/references/32-building-blocks.md +0 -28
- package/skills/ultra-research/references/40-deployment.md +0 -29
- package/skills/ultra-research/references/41-quality-risks.md +0 -28
- package/skills/ultra-research/references/99-synthesis.md +0 -59
- package/skills/ultra-research/references/north-star-v2.md +0 -168
- package/skills/ultra-research/references/wayfinding.md +0 -50
- package/skills/ultra-research/scripts/validate_north_star.cjs +0 -1174
- package/skills/ultra-review/SKILL.md +0 -275
- package/skills/ultra-review/references/adversarial-evaluation.md +0 -31
- package/skills/ultra-review/references/code.md +0 -17
- package/skills/ultra-review/references/comments.md +0 -12
- package/skills/ultra-review/references/design.md +0 -12
- package/skills/ultra-review/references/errors.md +0 -13
- package/skills/ultra-review/references/review-modes.md +0 -29
- package/skills/ultra-review/references/spec.md +0 -18
- package/skills/ultra-review/references/tests.md +0 -12
- package/skills/ultra-review/references/unified-schema.md +0 -284
- package/skills/ultra-review/references/worker-packet.md +0 -183
- package/skills/ultra-review/scripts/review_wait.py +0 -2943
- package/skills/ultra-status/SKILL.md +0 -209
- package/skills/ultra-status/references/route-contract.md +0 -100
- package/skills/ultra-tdd/SKILL.md +0 -91
- package/skills/ultra-tdd/references/templates/README.md +0 -20
- package/skills/ultra-tdd/references/templates/feature-flag-default-audit.sh +0 -79
- package/skills/ultra-tdd/references/templates/persistence-real.ts +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.py +0 -78
- package/skills/ultra-tdd/references/templates/testcontainer-postgres.ts +0 -73
- package/skills/ultra-tdd/references/templates/vertical-slice.ts +0 -66
- package/skills/ultra-tdd/references/test-execution.md +0 -23
- package/skills/ultra-test/SKILL.md +0 -244
- package/skills/ultra-test/references/export-syntax.md +0 -18
- package/skills/ultra-test/scripts/validate_review_transport.cjs +0 -549
- package/skills/ultra-test/scripts/worktree_digest.cjs +0 -576
- package/skills/ultra-think/SKILL.md +0 -56
- package/skills/ultra-think/references/autonomy-boundary.md +0 -48
- package/skills/ultra-think/references/interaction-boundary.md +0 -42
package/adapters/kimi.js
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
|
|
7
|
-
const {
|
|
8
|
-
copyHooks,
|
|
9
|
-
copySkills,
|
|
10
|
-
inspectPlugin,
|
|
11
|
-
markManaged,
|
|
12
|
-
publishManagedTrees,
|
|
13
|
-
removeManaged,
|
|
14
|
-
writePluginProvenance,
|
|
15
|
-
} = require('./_shared/plugin-core.cjs');
|
|
16
|
-
const {
|
|
17
|
-
captureAbsent,
|
|
18
|
-
ensureDir,
|
|
19
|
-
managedMetadata,
|
|
20
|
-
pruneCreatedEmpty,
|
|
21
|
-
removeTree,
|
|
22
|
-
writeAtomic,
|
|
23
|
-
} = require('./_shared/file-ops.cjs');
|
|
24
|
-
|
|
25
|
-
const PLUGIN_ID = 'ultra-builder-pro';
|
|
26
|
-
|
|
27
|
-
function resolveTarget(ctx = {}) {
|
|
28
|
-
if (ctx.configDir) return path.resolve(ctx.configDir);
|
|
29
|
-
if (ctx.scope === 'global') return process.env.KIMI_CODE_HOME || path.join(ctx.homeDir || os.homedir(), '.kimi-code');
|
|
30
|
-
return path.join(ctx.cwd || process.cwd(), '.kimi-code');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolvePluginRoot(ctx = {}) {
|
|
34
|
-
return path.join(resolveTarget(ctx), 'plugins', 'managed', PLUGIN_ID);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resolveRegistryFile(ctx = {}) {
|
|
38
|
-
return path.join(resolveTarget(ctx), 'plugins', 'installed.json');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function resolveRepoRoot(ctx = {}) {
|
|
42
|
-
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function assertPluginScope(ctx = {}) {
|
|
46
|
-
if (ctx.scope === 'local' && !ctx.configDir) {
|
|
47
|
-
throw new Error('Kimi plugins are user-scoped; use global scope or an isolated configDir');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function hookCommand(name) {
|
|
52
|
-
return `python3 ./hooks/adapters/kimi.py ${JSON.stringify(name)}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function buildHooksManifest() {
|
|
56
|
-
return [
|
|
57
|
-
{ event: 'SessionStart', command: hookCommand('session_context.py'), timeout: 10 },
|
|
58
|
-
{ event: 'SessionStart', matcher: 'compact', command: hookCommand('compact_context.py'), timeout: 10 },
|
|
59
|
-
{ event: 'PreToolUse', matcher: 'Edit|Write|Grep', command: hookCommand('mid_workflow_recall.py'), timeout: 10 },
|
|
60
|
-
{ event: 'PreToolUse', matcher: 'Bash', command: hookCommand('block_dangerous_commands.py'), timeout: 10 },
|
|
61
|
-
{ event: 'PostToolUse', matcher: 'Edit|Write', command: hookCommand('post_edit_guard.py'), timeout: 10 },
|
|
62
|
-
{ event: 'PreCompact', command: hookCommand('compact_context.py'), timeout: 10 },
|
|
63
|
-
{ event: 'PostCompact', command: hookCommand('compact_context.py'), timeout: 10 },
|
|
64
|
-
];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function loadRegistry(file) {
|
|
68
|
-
if (!fs.existsSync(file)) return { version: 1, plugins: [] };
|
|
69
|
-
const value = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
70
|
-
if (!value || value.version !== 1 || !Array.isArray(value.plugins)) {
|
|
71
|
-
throw new Error(`invalid Kimi registry: ${file}`);
|
|
72
|
-
}
|
|
73
|
-
return value;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function nextRegistry(value, target, repoRoot) {
|
|
77
|
-
const existing = value.plugins.find((entry) => entry?.id === PLUGIN_ID);
|
|
78
|
-
if (existing && path.resolve(existing.root || '') !== path.resolve(target)) {
|
|
79
|
-
throw new Error(`refusing to replace conflicting Kimi registration: ${existing.root}`);
|
|
80
|
-
}
|
|
81
|
-
const now = new Date().toISOString();
|
|
82
|
-
const record = {
|
|
83
|
-
id: PLUGIN_ID,
|
|
84
|
-
root: target,
|
|
85
|
-
source: 'local-path',
|
|
86
|
-
enabled: existing?.enabled ?? true,
|
|
87
|
-
installedAt: existing?.installedAt || now,
|
|
88
|
-
updatedAt: now,
|
|
89
|
-
originalSource: repoRoot,
|
|
90
|
-
};
|
|
91
|
-
return {
|
|
92
|
-
version: 1,
|
|
93
|
-
plugins: [...value.plugins.filter((entry) => entry?.id !== PLUGIN_ID), record],
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function buildStaging(repoRoot, staging, publishedRoot) {
|
|
98
|
-
const skills = copySkills({ runtime: 'kimi', repoRoot, skillRoot: path.join(staging, 'skills') });
|
|
99
|
-
const hooks = buildHooksManifest();
|
|
100
|
-
copyHooks({ runtime: 'kimi', repoRoot, hookRoot: path.join(staging, 'hooks'), hooksManifest: hooks });
|
|
101
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
|
|
102
|
-
writeAtomic(path.join(staging, 'kimi.plugin.json'), `${JSON.stringify({
|
|
103
|
-
name: PLUGIN_ID,
|
|
104
|
-
version: pkg.version,
|
|
105
|
-
description: 'File-first Ultra Builder Pro workflows for Kimi Code.',
|
|
106
|
-
keywords: ['kimi-code', 'skills', 'hooks', 'ultra-builder-pro'],
|
|
107
|
-
author: { name: pkg.author },
|
|
108
|
-
homepage: pkg.homepage,
|
|
109
|
-
license: pkg.license,
|
|
110
|
-
skills: ['./skills'],
|
|
111
|
-
hooks,
|
|
112
|
-
interface: {
|
|
113
|
-
displayName: 'Ultra Builder Pro',
|
|
114
|
-
shortDescription: 'File-first project workflows and evidence.',
|
|
115
|
-
developerName: pkg.author,
|
|
116
|
-
websiteURL: pkg.homepage,
|
|
117
|
-
},
|
|
118
|
-
}, null, 2)}\n`);
|
|
119
|
-
markManaged(staging, { adapter: 'kimi', plugin: PLUGIN_ID, version: pkg.version });
|
|
120
|
-
writePluginProvenance({
|
|
121
|
-
adapter: 'kimi', repoRoot, stagingRoot: staging, publishedRoot,
|
|
122
|
-
contracts: {
|
|
123
|
-
plugin_manifest: { root: 'plugin', path: 'kimi.plugin.json' },
|
|
124
|
-
hooks_manifest: { root: 'plugin', path: 'hooks/hooks.json' },
|
|
125
|
-
hook_adapter: { root: 'plugin', path: 'hooks/adapters/kimi.py' },
|
|
126
|
-
},
|
|
127
|
-
});
|
|
128
|
-
return { skills };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function install(ctx = {}) {
|
|
132
|
-
assertPluginScope(ctx);
|
|
133
|
-
const target = resolvePluginRoot(ctx);
|
|
134
|
-
const configRoot = resolveTarget(ctx);
|
|
135
|
-
const registryFile = resolveRegistryFile(ctx);
|
|
136
|
-
const repoRoot = resolveRepoRoot(ctx);
|
|
137
|
-
const previousMetadata = managedMetadata(target) || {};
|
|
138
|
-
const cleanupAbsent = previousMetadata.cleanup_absent
|
|
139
|
-
|| captureAbsent(configRoot, ['.', 'plugins', path.join('plugins', 'managed')]);
|
|
140
|
-
const registryExisted = fs.existsSync(registryFile);
|
|
141
|
-
const registryCreated = previousMetadata.registry_created === true
|
|
142
|
-
|| (previousMetadata.registry_created === undefined && !registryExisted);
|
|
143
|
-
const registryBefore = registryExisted ? fs.readFileSync(registryFile) : null;
|
|
144
|
-
const registry = nextRegistry(loadRegistry(registryFile), target, repoRoot);
|
|
145
|
-
ensureDir(path.dirname(target));
|
|
146
|
-
const staging = fs.mkdtempSync(path.join(path.dirname(target), `.${PLUGIN_ID}-staging-`));
|
|
147
|
-
try {
|
|
148
|
-
const copied = buildStaging(repoRoot, staging, target);
|
|
149
|
-
markManaged(staging, {
|
|
150
|
-
cleanup_absent: cleanupAbsent,
|
|
151
|
-
registry_created: registryCreated,
|
|
152
|
-
});
|
|
153
|
-
writeAtomic(registryFile, `${JSON.stringify(registry, null, 2)}\n`);
|
|
154
|
-
try {
|
|
155
|
-
publishManagedTrees([{ source: staging, target, label: 'Kimi plugin' }]);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
if (registryExisted) writeAtomic(registryFile, registryBefore);
|
|
158
|
-
else if (fs.existsSync(registryFile)) fs.unlinkSync(registryFile);
|
|
159
|
-
throw error;
|
|
160
|
-
}
|
|
161
|
-
return {
|
|
162
|
-
target,
|
|
163
|
-
pluginRoot: target,
|
|
164
|
-
skillRoot: path.join(target, 'skills'),
|
|
165
|
-
hookRoot: path.join(target, 'hooks'),
|
|
166
|
-
registry: registryFile,
|
|
167
|
-
copied,
|
|
168
|
-
};
|
|
169
|
-
} catch (error) {
|
|
170
|
-
if (fs.existsSync(staging)) removeTree(staging);
|
|
171
|
-
pruneCreatedEmpty(configRoot, cleanupAbsent);
|
|
172
|
-
throw error;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function doctor(ctx = {}) {
|
|
177
|
-
assertPluginScope(ctx);
|
|
178
|
-
const target = resolvePluginRoot(ctx);
|
|
179
|
-
const report = inspectPlugin({
|
|
180
|
-
adapter: 'kimi', repoRoot: resolveRepoRoot(ctx), pluginRoot: target,
|
|
181
|
-
skillRoot: path.join(target, 'skills'), hookRoot: path.join(target, 'hooks'),
|
|
182
|
-
manifestFile: path.join(target, 'kimi.plugin.json'),
|
|
183
|
-
});
|
|
184
|
-
let registered = false;
|
|
185
|
-
try {
|
|
186
|
-
const record = loadRegistry(resolveRegistryFile(ctx)).plugins.find((entry) => entry?.id === PLUGIN_ID);
|
|
187
|
-
registered = !!record && record.enabled === true && path.resolve(record.root) === path.resolve(target);
|
|
188
|
-
} catch {}
|
|
189
|
-
report.checks.registration = { status: registered ? 'pass' : 'fail' };
|
|
190
|
-
if (!registered) report.issues.push({ code: 'PLUGIN_REGISTRATION_INVALID' });
|
|
191
|
-
report.status = report.issues.length === 0 ? 'healthy' : 'degraded';
|
|
192
|
-
return report;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function uninstall(ctx = {}) {
|
|
196
|
-
assertPluginScope(ctx);
|
|
197
|
-
const target = resolvePluginRoot(ctx);
|
|
198
|
-
const configRoot = resolveTarget(ctx);
|
|
199
|
-
const metadata = managedMetadata(target) || {};
|
|
200
|
-
const cleanupAbsent = metadata.cleanup_absent || [];
|
|
201
|
-
const registryFile = resolveRegistryFile(ctx);
|
|
202
|
-
const registry = loadRegistry(registryFile);
|
|
203
|
-
const record = registry.plugins.find((entry) => entry?.id === PLUGIN_ID);
|
|
204
|
-
if (record && path.resolve(record.root || '') !== path.resolve(target)) {
|
|
205
|
-
throw new Error(`refusing to remove conflicting Kimi registration: ${record.root}`);
|
|
206
|
-
}
|
|
207
|
-
if (record) {
|
|
208
|
-
registry.plugins = registry.plugins.filter((entry) => entry?.id !== PLUGIN_ID);
|
|
209
|
-
if (registry.plugins.length === 0 && metadata.registry_created === true) fs.unlinkSync(registryFile);
|
|
210
|
-
else writeAtomic(registryFile, `${JSON.stringify(registry, null, 2)}\n`);
|
|
211
|
-
}
|
|
212
|
-
const removedPlugin = removeManaged(target, 'Kimi plugin');
|
|
213
|
-
const cleaned = pruneCreatedEmpty(configRoot, cleanupAbsent);
|
|
214
|
-
return {
|
|
215
|
-
target,
|
|
216
|
-
pluginRoot: target,
|
|
217
|
-
removed: { plugin: removedPlugin, registration: !!record },
|
|
218
|
-
cleaned,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
module.exports = {
|
|
223
|
-
name: 'kimi',
|
|
224
|
-
PLUGIN_ID,
|
|
225
|
-
buildHooksManifest,
|
|
226
|
-
resolveTarget,
|
|
227
|
-
resolvePluginRoot,
|
|
228
|
-
resolveRegistryFile,
|
|
229
|
-
install,
|
|
230
|
-
doctor,
|
|
231
|
-
uninstall,
|
|
232
|
-
};
|
package/adapters/opencode.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('node:fs');
|
|
4
|
-
const os = require('node:os');
|
|
5
|
-
const path = require('node:path');
|
|
6
|
-
const crypto = require('node:crypto');
|
|
7
|
-
|
|
8
|
-
const {
|
|
9
|
-
copyHooks,
|
|
10
|
-
copySkills,
|
|
11
|
-
inspectPlugin,
|
|
12
|
-
markManaged,
|
|
13
|
-
removeManaged,
|
|
14
|
-
writePluginProvenance,
|
|
15
|
-
} = require('./_shared/plugin-core.cjs');
|
|
16
|
-
const {
|
|
17
|
-
captureAbsent,
|
|
18
|
-
ensureDir,
|
|
19
|
-
isManaged,
|
|
20
|
-
managedMetadata,
|
|
21
|
-
pruneCreatedEmpty,
|
|
22
|
-
removeTree: removePath,
|
|
23
|
-
writeAtomic,
|
|
24
|
-
} = require('./_shared/file-ops.cjs');
|
|
25
|
-
const { skillsForRuntime } = require('./_shared/runtime-assets.cjs');
|
|
26
|
-
|
|
27
|
-
const PLUGIN_NAME = 'ultra-builder-pro';
|
|
28
|
-
const BUNDLE_DIR = '.ultra-builder-pro';
|
|
29
|
-
const PLUGIN_MARKER = '// Managed by Ultra Builder Pro.';
|
|
30
|
-
const TEXT_MARKER = '<!-- ultra-builder-pro:managed -->';
|
|
31
|
-
|
|
32
|
-
function resolveTarget(ctx = {}) {
|
|
33
|
-
if (ctx.configDir) return path.resolve(ctx.configDir);
|
|
34
|
-
if (ctx.scope === 'global') {
|
|
35
|
-
if (process.env.OPENCODE_CONFIG_DIR) return process.env.OPENCODE_CONFIG_DIR;
|
|
36
|
-
if (process.env.OPENCODE_CONFIG) return path.dirname(process.env.OPENCODE_CONFIG);
|
|
37
|
-
return path.join(process.env.XDG_CONFIG_HOME || path.join(ctx.homeDir || os.homedir(), '.config'), 'opencode');
|
|
38
|
-
}
|
|
39
|
-
return path.join(ctx.cwd || process.cwd(), '.opencode');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function resolveRepoRoot(ctx = {}) {
|
|
43
|
-
return ctx.repoRoot || path.resolve(__dirname, '..');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function buildHooksManifest() {
|
|
47
|
-
return {
|
|
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'],
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function pluginSource() {
|
|
56
|
-
return `${PLUGIN_MARKER}
|
|
57
|
-
import path from "node:path";
|
|
58
|
-
import { execFileSync } from "node:child_process";
|
|
59
|
-
import { fileURLToPath } from "node:url";
|
|
60
|
-
|
|
61
|
-
const PLUGIN_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
62
|
-
const HOOK_ROOT = path.resolve(PLUGIN_DIR, "..", ${JSON.stringify(BUNDLE_DIR)}, "hooks");
|
|
63
|
-
|
|
64
|
-
function runHook(name, root, payload) {
|
|
65
|
-
try {
|
|
66
|
-
const stdout = execFileSync("python3", [path.join(HOOK_ROOT, name)], {
|
|
67
|
-
cwd: root,
|
|
68
|
-
input: JSON.stringify({ cwd: root, ...payload }),
|
|
69
|
-
encoding: "utf8",
|
|
70
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
71
|
-
timeout: 10000,
|
|
72
|
-
}).trim();
|
|
73
|
-
return stdout ? JSON.parse(stdout) : {};
|
|
74
|
-
} catch (error) {
|
|
75
|
-
return { systemMessage: "Ultra hook failed open: " + String(error?.message || error) };
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function contextOf(output) {
|
|
80
|
-
return output?.hookSpecificOutput?.additionalContext || output?.systemMessage || "";
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export const UltraBuilderProPlugin = async ({ directory, worktree }) => {
|
|
84
|
-
const root = worktree || directory;
|
|
85
|
-
return {
|
|
86
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
87
|
-
const text = contextOf(runHook("session_context.py", root, { hook_event_name: "SessionStart" }));
|
|
88
|
-
if (text) output.system.push(text);
|
|
89
|
-
},
|
|
90
|
-
"experimental.session.compacting": async (_input, output) => {
|
|
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);
|
|
96
|
-
},
|
|
97
|
-
"tool.execute.before": async (input, output) => {
|
|
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
|
-
});
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
`;
|
|
122
|
-
}
|
|
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
|
-
|
|
162
|
-
function createInstallTransaction() {
|
|
163
|
-
const changes = [];
|
|
164
|
-
const backups = [];
|
|
165
|
-
const backupPath = (target) => path.join(
|
|
166
|
-
path.dirname(target),
|
|
167
|
-
`.${path.basename(target)}.ubp-backup-${crypto.randomUUID()}`,
|
|
168
|
-
);
|
|
169
|
-
return {
|
|
170
|
-
replaceFile(source, target) {
|
|
171
|
-
let previous = null;
|
|
172
|
-
try {
|
|
173
|
-
const stat = fs.statSync(target);
|
|
174
|
-
previous = { contents: fs.readFileSync(target), mode: stat.mode & 0o7777 };
|
|
175
|
-
} catch (error) {
|
|
176
|
-
if (error.code !== 'ENOENT') throw error;
|
|
177
|
-
}
|
|
178
|
-
writeAtomic(target, fs.readFileSync(source));
|
|
179
|
-
changes.push({ type: 'file', target, previous });
|
|
180
|
-
},
|
|
181
|
-
replaceTree(source, target) {
|
|
182
|
-
ensureDir(path.dirname(target));
|
|
183
|
-
let backup = null;
|
|
184
|
-
if (fs.existsSync(target)) {
|
|
185
|
-
backup = backupPath(target);
|
|
186
|
-
fs.renameSync(target, backup);
|
|
187
|
-
backups.push(backup);
|
|
188
|
-
}
|
|
189
|
-
try {
|
|
190
|
-
fs.renameSync(source, target);
|
|
191
|
-
} catch (error) {
|
|
192
|
-
if (backup && fs.existsSync(backup)) fs.renameSync(backup, target);
|
|
193
|
-
throw error;
|
|
194
|
-
}
|
|
195
|
-
changes.push({ type: 'tree', target, backup });
|
|
196
|
-
},
|
|
197
|
-
removeFile(target) {
|
|
198
|
-
if (!fs.existsSync(target)) return;
|
|
199
|
-
const link = fs.lstatSync(target).isSymbolicLink() ? fs.readlinkSync(target) : null;
|
|
200
|
-
const stat = fs.statSync(target);
|
|
201
|
-
const previous = { contents: fs.readFileSync(target), link, mode: stat.mode & 0o7777 };
|
|
202
|
-
fs.unlinkSync(target);
|
|
203
|
-
changes.push({ type: 'file', target, previous, removed: true });
|
|
204
|
-
},
|
|
205
|
-
removeTree(target) {
|
|
206
|
-
if (!fs.existsSync(target)) return;
|
|
207
|
-
const backup = backupPath(target);
|
|
208
|
-
fs.renameSync(target, backup);
|
|
209
|
-
backups.push(backup);
|
|
210
|
-
changes.push({ type: 'tree', target, backup, removed: true });
|
|
211
|
-
},
|
|
212
|
-
commit() {
|
|
213
|
-
for (const backup of backups) {
|
|
214
|
-
if (fs.existsSync(backup)) removePath(backup);
|
|
215
|
-
}
|
|
216
|
-
changes.length = 0;
|
|
217
|
-
backups.length = 0;
|
|
218
|
-
},
|
|
219
|
-
rollback() {
|
|
220
|
-
const errors = [];
|
|
221
|
-
for (const change of changes.reverse()) {
|
|
222
|
-
try {
|
|
223
|
-
if (change.type === 'file') {
|
|
224
|
-
if (change.previous) {
|
|
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
|
-
}
|
|
232
|
-
} else if (fs.existsSync(change.target)) {
|
|
233
|
-
fs.unlinkSync(change.target);
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
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);
|
|
238
|
-
}
|
|
239
|
-
} catch (error) {
|
|
240
|
-
errors.push(error);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (errors.length) throw new AggregateError(errors, 'OpenCode install rollback failed');
|
|
244
|
-
},
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function resolveTargetFile(file) {
|
|
249
|
-
return fs.lstatSync(file).isSymbolicLink() ? fs.realpathSync(file) : file;
|
|
250
|
-
}
|
|
251
|
-
|
|
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
|
-
}
|
|
270
|
-
|
|
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);
|
|
275
|
-
|
|
276
|
-
const transaction = createInstallTransaction();
|
|
277
|
-
try {
|
|
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));
|
|
281
|
-
}
|
|
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);
|
|
287
|
-
}
|
|
288
|
-
transaction.commit();
|
|
289
|
-
} catch (error) {
|
|
290
|
-
try {
|
|
291
|
-
transaction.rollback();
|
|
292
|
-
} catch (rollbackError) {
|
|
293
|
-
throw new AggregateError([error, rollbackError], 'OpenCode install and rollback both failed');
|
|
294
|
-
}
|
|
295
|
-
throw error;
|
|
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
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function install(ctx = {}) {
|
|
305
|
-
const target = resolveTarget(ctx);
|
|
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']);
|
|
312
|
-
ensureDir(target);
|
|
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);
|
|
323
|
-
try {
|
|
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;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function doctor(ctx = {}) {
|
|
357
|
-
const target = resolveTarget(ctx);
|
|
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,
|
|
364
|
-
});
|
|
365
|
-
let configClean = true;
|
|
366
|
-
try {
|
|
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];
|
|
371
|
-
}
|
|
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;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function uninstall(ctx = {}) {
|
|
380
|
-
const target = resolveTarget(ctx);
|
|
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,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
module.exports = {
|
|
401
|
-
name: 'opencode',
|
|
402
|
-
PLUGIN_NAME,
|
|
403
|
-
buildHooksManifest,
|
|
404
|
-
resolveTarget,
|
|
405
|
-
install,
|
|
406
|
-
doctor,
|
|
407
|
-
uninstall,
|
|
408
|
-
};
|