@a5c-ai/babysitter-opencode 5.0.1-staging.e4f17eff → 5.0.1-staging.ef4e872c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/cli.cjs +1 -191
- package/bin/cli.js +90 -49
- package/bin/install-shared.cjs +1 -478
- package/bin/install-shared.js +615 -0
- package/bin/install.cjs +1 -143
- package/bin/install.js +18 -98
- package/bin/uninstall.cjs +1 -87
- package/bin/uninstall.js +12 -34
- package/commands/doctor.md +5 -5
- package/commands/help.md +245 -244
- package/commands/observe.md +12 -12
- package/hooks/babysitter-proxied-session-created.js +18 -212
- package/hooks/babysitter-proxied-session-created.sh +11 -0
- package/hooks/babysitter-proxied-session-idle.js +18 -167
- package/hooks/babysitter-proxied-session-idle.sh +3 -0
- package/hooks/babysitter-proxied-shell-env.js +21 -145
- package/hooks/babysitter-proxied-shell-env.sh +3 -0
- package/hooks/babysitter-proxied-tool-execute-after.js +20 -160
- package/hooks/babysitter-proxied-tool-execute-after.sh +3 -0
- package/hooks/babysitter-proxied-tool-execute-before.js +20 -162
- package/hooks/babysitter-proxied-tool-execute-before.sh +3 -0
- package/hooks/hooks.json +18 -18
- package/package.json +22 -18
- package/plugin.json +6 -4
- package/scripts/team-install.js +23 -0
- package/skills/contrib/SKILL.md +25 -25
- package/skills/doctor/SKILL.md +5 -5
- package/skills/help/SKILL.md +3 -2
- package/skills/observe/SKILL.md +1 -1
- package/skills/plugins/SKILL.md +243 -243
- package/skills/project-install/SKILL.md +3 -3
- package/skills/resume/SKILL.md +1 -1
- package/skills/retrospect/SKILL.md +48 -48
- package/skills/user-install/SKILL.md +3 -3
- package/versions.json +2 -2
- package/hooks/hooks.json.legacy +0 -46
- package/hooks/proxied-hooks.json +0 -47
- package/hooks/session-created.js +0 -182
- package/hooks/session-idle.js +0 -124
- package/hooks/shell-env.js +0 -88
- package/hooks/tool-execute-after.js +0 -107
- package/hooks/tool-execute-before.js +0 -109
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const PLUGIN_NAME = "babysitter";
|
|
9
|
+
const PLUGIN_CATEGORY = 'Coding';
|
|
10
|
+
|
|
11
|
+
function getUserHome() {
|
|
12
|
+
return os.homedir();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getHarnessHome() {
|
|
16
|
+
return path.join(os.homedir(), ".opencode");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getHomePluginRoot(scope) {
|
|
20
|
+
if (scope === 'workspace') return path.join(process.cwd(), '.a5c', 'plugins', PLUGIN_NAME);
|
|
21
|
+
return path.join(path.join(getHarnessHome(), 'plugins'), PLUGIN_NAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getHomeMarketplacePath() {
|
|
25
|
+
return path.join(getHarnessHome(), 'plugins', 'marketplace.json');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeFileIfChanged(filePath, contents) {
|
|
29
|
+
try {
|
|
30
|
+
const existing = fs.readFileSync(filePath, 'utf8');
|
|
31
|
+
if (existing === contents) return false;
|
|
32
|
+
} catch {}
|
|
33
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
34
|
+
fs.writeFileSync(filePath, contents);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function copyRecursive(src, dest) {
|
|
39
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
40
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
41
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
42
|
+
const s = path.join(src, entry.name);
|
|
43
|
+
const d = path.join(dest, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
copyRecursive(s, d);
|
|
46
|
+
} else {
|
|
47
|
+
fs.copyFileSync(s, d);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function copyPluginBundle(packageRoot, pluginRoot) {
|
|
53
|
+
const bundleEntries = fs.readdirSync(packageRoot).filter(
|
|
54
|
+
e => !['node_modules', '.git', 'test', 'dist'].includes(e)
|
|
55
|
+
);
|
|
56
|
+
fs.mkdirSync(pluginRoot, { recursive: true });
|
|
57
|
+
for (const entry of bundleEntries) {
|
|
58
|
+
const src = path.join(packageRoot, entry);
|
|
59
|
+
const dest = path.join(pluginRoot, entry);
|
|
60
|
+
const stat = fs.statSync(src);
|
|
61
|
+
if (stat.isDirectory()) {
|
|
62
|
+
copyRecursive(src, dest);
|
|
63
|
+
} else {
|
|
64
|
+
fs.copyFileSync(src, dest);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readJson(filePath) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function writeJson(filePath, value) {
|
|
78
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureExecutable(filePath) {
|
|
83
|
+
try {
|
|
84
|
+
fs.chmodSync(filePath, 0o755);
|
|
85
|
+
} catch {}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeMarketplaceSourcePath(source, marketplacePath) {
|
|
89
|
+
if (typeof source === 'string') {
|
|
90
|
+
return path.relative(path.dirname(marketplacePath), source).replace(/\\/g, '/');
|
|
91
|
+
}
|
|
92
|
+
return source;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function ensureMarketplaceEntry(marketplacePath, pluginRoot) {
|
|
96
|
+
let marketplace = readJson(marketplacePath) || {
|
|
97
|
+
name: "a5c.ai",
|
|
98
|
+
plugins: [],
|
|
99
|
+
};
|
|
100
|
+
if (!Array.isArray(marketplace.plugins)) marketplace.plugins = [];
|
|
101
|
+
const idx = marketplace.plugins.findIndex(p => p.name === PLUGIN_NAME);
|
|
102
|
+
const relSource = './' + normalizeMarketplaceSourcePath(pluginRoot, marketplacePath);
|
|
103
|
+
const entry = {
|
|
104
|
+
name: PLUGIN_NAME,
|
|
105
|
+
source: relSource,
|
|
106
|
+
description: "Orchestrate complex, multi-step workflows with event-sourced state management, hook-based extensibility, and human-in-the-loop approval",
|
|
107
|
+
version: "5.0.0",
|
|
108
|
+
author: { name: "a5c.ai" },
|
|
109
|
+
};
|
|
110
|
+
if (idx >= 0) marketplace.plugins[idx] = entry;
|
|
111
|
+
else marketplace.plugins.push(entry);
|
|
112
|
+
writeJson(marketplacePath, marketplace);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function removeMarketplaceEntry(marketplacePath) {
|
|
116
|
+
const marketplace = readJson(marketplacePath);
|
|
117
|
+
if (!marketplace || !Array.isArray(marketplace.plugins)) return;
|
|
118
|
+
marketplace.plugins = marketplace.plugins.filter(p => p.name !== PLUGIN_NAME);
|
|
119
|
+
writeJson(marketplacePath, marketplace);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function warnWindowsHooks() {
|
|
123
|
+
if (process.platform === 'win32') {
|
|
124
|
+
console.warn('[' + PLUGIN_NAME + '] Windows detected — shell hooks (.sh) require Git Bash or WSL.');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function runPostInstall(pluginRoot) {
|
|
129
|
+
const postInstall = path.join(pluginRoot, 'scripts', 'post-install.js');
|
|
130
|
+
if (fs.existsSync(postInstall)) {
|
|
131
|
+
spawnSync(process.execPath, [postInstall], {
|
|
132
|
+
cwd: pluginRoot, stdio: 'inherit',
|
|
133
|
+
env: { ...process.env, PLUGIN_ROOT: pluginRoot },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getGlobalStateDir() {
|
|
139
|
+
return process.env.BABYSITTER_GLOBAL_STATE_DIR || path.join(getUserHome(), '.a5c');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resolveCliCommand(packageRoot) {
|
|
143
|
+
try {
|
|
144
|
+
const result = spawnSync('babysitter', ['--version'], { stdio: 'pipe', timeout: 10000 });
|
|
145
|
+
if (result.status === 0) return 'babysitter';
|
|
146
|
+
} catch {}
|
|
147
|
+
const versionsPath = path.join(packageRoot, 'versions.json');
|
|
148
|
+
const versions = readJson(versionsPath) || {};
|
|
149
|
+
const ver = versions.sdkVersion || 'latest';
|
|
150
|
+
return `npx -y @a5c-ai/babysitter-sdk@${ver}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function runCli(packageRoot, cliArgs, options = {}) {
|
|
154
|
+
const cmd = resolveCliCommand(packageRoot);
|
|
155
|
+
const parts = cmd.split(' ');
|
|
156
|
+
const result = spawnSync(parts[0], [...parts.slice(1), ...cliArgs], {
|
|
157
|
+
stdio: options.stdio || 'inherit',
|
|
158
|
+
timeout: options.timeout || 120000,
|
|
159
|
+
cwd: options.cwd || process.cwd(),
|
|
160
|
+
env: { ...process.env, ...options.env },
|
|
161
|
+
});
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ensureGlobalProcessLibrary(packageRoot) {
|
|
166
|
+
const stateDir = getGlobalStateDir();
|
|
167
|
+
const activeFile = path.join(stateDir, 'active', 'process-library.json');
|
|
168
|
+
let active = readJson(activeFile);
|
|
169
|
+
if (active && active.binding && active.binding.dir) {
|
|
170
|
+
return active;
|
|
171
|
+
}
|
|
172
|
+
const defaultSpec = readJson(path.join(stateDir, 'process-library-defaults.json'));
|
|
173
|
+
const cloneDir = defaultSpec && defaultSpec.cloneDir
|
|
174
|
+
? defaultSpec.cloneDir
|
|
175
|
+
: path.join(stateDir, 'process-library', PLUGIN_NAME + '-repo');
|
|
176
|
+
runCli(packageRoot, [
|
|
177
|
+
'process-library:clone',
|
|
178
|
+
'--dir', cloneDir,
|
|
179
|
+
'--state-dir', stateDir,
|
|
180
|
+
'--json',
|
|
181
|
+
], { stdio: 'pipe' });
|
|
182
|
+
runCli(packageRoot, [
|
|
183
|
+
'process-library:use',
|
|
184
|
+
'--dir', cloneDir,
|
|
185
|
+
'--state-dir', stateDir,
|
|
186
|
+
'--json',
|
|
187
|
+
], { stdio: 'pipe' });
|
|
188
|
+
active = readJson(activeFile);
|
|
189
|
+
return {
|
|
190
|
+
binding: active && active.binding ? active.binding : { dir: cloneDir },
|
|
191
|
+
defaultSpec: defaultSpec || { cloneDir },
|
|
192
|
+
stateFile: activeFile,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// Opencode harness-specific surface
|
|
199
|
+
//
|
|
200
|
+
// This file is appended by the unified plugin compiler after the generic
|
|
201
|
+
// install-shared base and the SDK surface. It may reference any identifier
|
|
202
|
+
// already declared in those layers (PLUGIN_NAME, getUserHome, readJson,
|
|
203
|
+
// writeJson, writeFileIfChanged, getGlobalStateDir, resolveCliCommand,
|
|
204
|
+
// runCli, ensureGlobalProcessLibrary, etc.) and may re-declare functions
|
|
205
|
+
// to override the base implementation.
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
const PLUGIN_BUNDLE_ENTRIES = [
|
|
209
|
+
'plugin.json',
|
|
210
|
+
'versions.json',
|
|
211
|
+
'hooks',
|
|
212
|
+
'skills',
|
|
213
|
+
'commands',
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
const HOOK_SCRIPT_NAMES = [
|
|
217
|
+
'session-created.js',
|
|
218
|
+
'session-idle.js',
|
|
219
|
+
'shell-env.js',
|
|
220
|
+
'tool-execute-before.js',
|
|
221
|
+
'tool-execute-after.js',
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const DEFAULT_MARKETPLACE = {
|
|
225
|
+
name: 'local-plugins',
|
|
226
|
+
interface: {
|
|
227
|
+
displayName: 'Local Plugins',
|
|
228
|
+
},
|
|
229
|
+
plugins: [],
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Path helpers (override base)
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Resolve the OpenCode config root.
|
|
238
|
+
* OpenCode uses `.opencode/` in the workspace directory by default.
|
|
239
|
+
* Respect OPENCODE_HOME env var if set.
|
|
240
|
+
*/
|
|
241
|
+
function getOpenCodeHome(workspace) {
|
|
242
|
+
if (process.env.OPENCODE_HOME) return path.resolve(process.env.OPENCODE_HOME);
|
|
243
|
+
return path.join(workspace || process.cwd(), '.opencode');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function getHomePluginRoot(workspace) {
|
|
247
|
+
if (process.env.BABYSITTER_OPENCODE_PLUGIN_DIR) {
|
|
248
|
+
return path.resolve(process.env.BABYSITTER_OPENCODE_PLUGIN_DIR, PLUGIN_NAME);
|
|
249
|
+
}
|
|
250
|
+
return path.join(getOpenCodeHome(workspace), 'plugins', PLUGIN_NAME);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getHomeMarketplacePath(workspace) {
|
|
254
|
+
if (process.env.BABYSITTER_OPENCODE_MARKETPLACE_PATH) {
|
|
255
|
+
return path.resolve(process.env.BABYSITTER_OPENCODE_MARKETPLACE_PATH);
|
|
256
|
+
}
|
|
257
|
+
return path.join(getUserHome(), '.agents', 'plugins', 'marketplace.json');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// File utilities (override base — adds BOM stripping for SKILL.md)
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
function copyRecursive(src, dest) {
|
|
265
|
+
const stat = fs.statSync(src);
|
|
266
|
+
if (stat.isDirectory()) {
|
|
267
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
268
|
+
for (const entry of fs.readdirSync(src)) {
|
|
269
|
+
if (['node_modules', '.git', 'test', '.a5c'].includes(entry)) continue;
|
|
270
|
+
copyRecursive(path.join(src, entry), path.join(dest, entry));
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Strip BOM from SKILL.md files
|
|
276
|
+
if (path.basename(src) === 'SKILL.md') {
|
|
277
|
+
const file = fs.readFileSync(src);
|
|
278
|
+
const hasBom = file.length >= 3 && file[0] === 0xef && file[1] === 0xbb && file[2] === 0xbf;
|
|
279
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
280
|
+
fs.writeFileSync(dest, hasBom ? file.subarray(3) : file);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
285
|
+
fs.copyFileSync(src, dest);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------------------------------------------------------------------------
|
|
289
|
+
// Plugin bundle (override base — uses PLUGIN_BUNDLE_ENTRIES allowlist)
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
|
|
292
|
+
function copyPluginBundle(packageRoot, pluginRoot) {
|
|
293
|
+
if (path.resolve(packageRoot) === path.resolve(pluginRoot)) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
fs.rmSync(pluginRoot, { recursive: true, force: true });
|
|
297
|
+
fs.mkdirSync(pluginRoot, { recursive: true });
|
|
298
|
+
for (const entry of PLUGIN_BUNDLE_ENTRIES) {
|
|
299
|
+
const src = path.join(packageRoot, entry);
|
|
300
|
+
if (fs.existsSync(src)) {
|
|
301
|
+
copyRecursive(src, path.join(pluginRoot, entry));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// OpenCode index.js entry point generation
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
function writeIndexJs(pluginRoot) {
|
|
311
|
+
const indexContent = `#!/usr/bin/env node
|
|
312
|
+
/**
|
|
313
|
+
* Babysitter plugin entry point for OpenCode.
|
|
314
|
+
*
|
|
315
|
+
* OpenCode discovers plugins by looking for JS/TS modules in
|
|
316
|
+
* .opencode/plugins/. This file registers the babysitter hooks
|
|
317
|
+
* with the OpenCode plugin system.
|
|
318
|
+
*/
|
|
319
|
+
|
|
320
|
+
"use strict";
|
|
321
|
+
|
|
322
|
+
const path = require("path");
|
|
323
|
+
|
|
324
|
+
const PLUGIN_DIR = __dirname;
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
name: "babysitter",
|
|
328
|
+
version: require(path.join(PLUGIN_DIR, "plugin.json")).version,
|
|
329
|
+
|
|
330
|
+
hooks: {
|
|
331
|
+
"session.created": require(path.join(PLUGIN_DIR, "hooks", "session-created.js")),
|
|
332
|
+
"session.idle": require(path.join(PLUGIN_DIR, "hooks", "session-idle.js")),
|
|
333
|
+
"shell.env": require(path.join(PLUGIN_DIR, "hooks", "shell-env.js")),
|
|
334
|
+
"tool.execute.before": require(path.join(PLUGIN_DIR, "hooks", "tool-execute-before.js")),
|
|
335
|
+
"tool.execute.after": require(path.join(PLUGIN_DIR, "hooks", "tool-execute-after.js")),
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
`;
|
|
339
|
+
fs.writeFileSync(path.join(pluginRoot, 'index.js'), indexContent, 'utf8');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// OpenCode hooks.json config registration
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
function mergeHooksConfig(packageRoot, openCodeHome) {
|
|
347
|
+
const hooksJsonPath = path.join(packageRoot, 'hooks', 'hooks.json');
|
|
348
|
+
if (!fs.existsSync(hooksJsonPath)) return;
|
|
349
|
+
const managedConfig = readJson(hooksJsonPath);
|
|
350
|
+
const managedHooks = managedConfig.hooks || {};
|
|
351
|
+
const hooksConfigPath = path.join(openCodeHome, 'hooks.json');
|
|
352
|
+
const existing = fs.existsSync(hooksConfigPath)
|
|
353
|
+
? readJson(hooksConfigPath)
|
|
354
|
+
: { version: 1, hooks: {} };
|
|
355
|
+
existing.version = existing.version || 1;
|
|
356
|
+
if (!existing.hooks || typeof existing.hooks !== 'object') {
|
|
357
|
+
existing.hooks = {};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const [eventName, entries] of Object.entries(managedHooks)) {
|
|
361
|
+
const existingEntries = Array.isArray(existing.hooks[eventName]) ? existing.hooks[eventName] : [];
|
|
362
|
+
const filteredEntries = existingEntries.filter((entry) => {
|
|
363
|
+
const script = String(entry.script || entry.command || entry.bash || '');
|
|
364
|
+
return !HOOK_SCRIPT_NAMES.some((name) => script.includes(name));
|
|
365
|
+
});
|
|
366
|
+
const installedEntries = entries.map((entry) => {
|
|
367
|
+
const relativeScript = String(entry.script || '').trim();
|
|
368
|
+
if (relativeScript) {
|
|
369
|
+
const normalizedScript = relativeScript.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
370
|
+
return {
|
|
371
|
+
...entry,
|
|
372
|
+
script: `npx -y -p @a5c-ai/hooks-mux-cli -c "a5c-hooks-mux invoke --adapter opencode --handler 'node ./plugins/${PLUGIN_NAME}/${normalizedScript}' --json"`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (entry.command) {
|
|
376
|
+
return {
|
|
377
|
+
...entry,
|
|
378
|
+
script: entry.command,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
return entry;
|
|
382
|
+
});
|
|
383
|
+
existing.hooks[eventName] = [...filteredEntries, ...installedEntries];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
writeJson(hooksConfigPath, existing);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function removeManagedHooks(openCodeHome) {
|
|
390
|
+
const hooksConfigPath = path.join(openCodeHome, 'hooks.json');
|
|
391
|
+
if (!fs.existsSync(hooksConfigPath)) return;
|
|
392
|
+
|
|
393
|
+
let hooksConfig;
|
|
394
|
+
try {
|
|
395
|
+
hooksConfig = readJson(hooksConfigPath);
|
|
396
|
+
} catch {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (!hooksConfig.hooks || typeof hooksConfig.hooks !== 'object') return;
|
|
400
|
+
|
|
401
|
+
for (const eventName of Object.keys(hooksConfig.hooks)) {
|
|
402
|
+
const eventHooks = Array.isArray(hooksConfig.hooks[eventName]) ? hooksConfig.hooks[eventName] : [];
|
|
403
|
+
const filtered = eventHooks.filter((entry) => {
|
|
404
|
+
const script = String(entry.script || entry.command || entry.bash || '');
|
|
405
|
+
return !HOOK_SCRIPT_NAMES.some((name) => script.includes(name));
|
|
406
|
+
});
|
|
407
|
+
if (filtered.length > 0) {
|
|
408
|
+
hooksConfig.hooks[eventName] = filtered;
|
|
409
|
+
} else {
|
|
410
|
+
delete hooksConfig.hooks[eventName];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (Object.keys(hooksConfig.hooks).length === 0) {
|
|
414
|
+
fs.rmSync(hooksConfigPath, { force: true });
|
|
415
|
+
} else {
|
|
416
|
+
writeJson(hooksConfigPath, hooksConfig);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---------------------------------------------------------------------------
|
|
421
|
+
// Marketplace (override base — opencode format with normalizeMarketplaceName)
|
|
422
|
+
// ---------------------------------------------------------------------------
|
|
423
|
+
|
|
424
|
+
function normalizeMarketplaceName(name) {
|
|
425
|
+
const raw = String(name || '').trim();
|
|
426
|
+
const sanitized = raw
|
|
427
|
+
.replace(/[^A-Za-z0-9_-]+/g, '-')
|
|
428
|
+
.replace(/^-+|-+$/g, '');
|
|
429
|
+
return sanitized || DEFAULT_MARKETPLACE.name;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function normalizeMarketplaceSourcePath(marketplacePath, pluginSourcePath) {
|
|
433
|
+
let next = pluginSourcePath;
|
|
434
|
+
if (path.isAbsolute(next)) {
|
|
435
|
+
next = path.relative(path.dirname(marketplacePath), next);
|
|
436
|
+
}
|
|
437
|
+
next = String(next || '').replace(/\\/g, '/');
|
|
438
|
+
if (!next.startsWith('./') && !next.startsWith('../')) {
|
|
439
|
+
next = `./${next}`;
|
|
440
|
+
}
|
|
441
|
+
return next;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function ensureMarketplaceEntry(marketplacePath, pluginSourcePath) {
|
|
445
|
+
const marketplace = fs.existsSync(marketplacePath)
|
|
446
|
+
? readJson(marketplacePath)
|
|
447
|
+
: { ...DEFAULT_MARKETPLACE, plugins: [] };
|
|
448
|
+
marketplace.name = normalizeMarketplaceName(marketplace.name);
|
|
449
|
+
marketplace.interface = marketplace.interface || {};
|
|
450
|
+
marketplace.interface.displayName =
|
|
451
|
+
marketplace.interface.displayName || DEFAULT_MARKETPLACE.interface.displayName;
|
|
452
|
+
const nextEntry = {
|
|
453
|
+
name: PLUGIN_NAME,
|
|
454
|
+
source: {
|
|
455
|
+
source: 'local',
|
|
456
|
+
path: normalizeMarketplaceSourcePath(marketplacePath, pluginSourcePath),
|
|
457
|
+
},
|
|
458
|
+
policy: {
|
|
459
|
+
installation: 'AVAILABLE',
|
|
460
|
+
authentication: 'ON_INSTALL',
|
|
461
|
+
},
|
|
462
|
+
category: 'Coding',
|
|
463
|
+
};
|
|
464
|
+
const existingIndex = Array.isArray(marketplace.plugins)
|
|
465
|
+
? marketplace.plugins.findIndex((entry) => entry && entry.name === PLUGIN_NAME)
|
|
466
|
+
: -1;
|
|
467
|
+
if (!Array.isArray(marketplace.plugins)) {
|
|
468
|
+
marketplace.plugins = [nextEntry];
|
|
469
|
+
} else if (existingIndex >= 0) {
|
|
470
|
+
marketplace.plugins[existingIndex] = nextEntry;
|
|
471
|
+
} else {
|
|
472
|
+
marketplace.plugins.push(nextEntry);
|
|
473
|
+
}
|
|
474
|
+
writeJson(marketplacePath, marketplace);
|
|
475
|
+
return nextEntry;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function removeMarketplaceEntry(marketplacePath) {
|
|
479
|
+
if (!fs.existsSync(marketplacePath)) return;
|
|
480
|
+
const marketplace = readJson(marketplacePath);
|
|
481
|
+
if (!Array.isArray(marketplace.plugins)) return;
|
|
482
|
+
marketplace.plugins = marketplace.plugins.filter((entry) => entry && entry.name !== PLUGIN_NAME);
|
|
483
|
+
writeJson(marketplacePath, marketplace);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Accomplish AI detection and paths
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Returns the Accomplish user data directory for the current platform.
|
|
492
|
+
* If OPENCODE_CONFIG_DIR is set, returns its parent (the Accomplish data dir).
|
|
493
|
+
* Otherwise falls back to platform-specific defaults.
|
|
494
|
+
*/
|
|
495
|
+
function getAccomplishDataDir() {
|
|
496
|
+
if (process.env.OPENCODE_CONFIG_DIR) {
|
|
497
|
+
return path.resolve(process.env.OPENCODE_CONFIG_DIR, '..');
|
|
498
|
+
}
|
|
499
|
+
const home = getUserHome();
|
|
500
|
+
switch (process.platform) {
|
|
501
|
+
case 'darwin':
|
|
502
|
+
return path.join(home, 'Library', 'Application Support', 'Accomplish');
|
|
503
|
+
case 'win32': {
|
|
504
|
+
const appData = process.env.APPDATA || process.env.LOCALAPPDATA;
|
|
505
|
+
return appData
|
|
506
|
+
? path.join(appData, 'Accomplish')
|
|
507
|
+
: path.join(home, 'AppData', 'Roaming', 'Accomplish');
|
|
508
|
+
}
|
|
509
|
+
default:
|
|
510
|
+
return path.join(home, '.config', 'Accomplish');
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Returns true if Accomplish AI appears to be installed or is running.
|
|
516
|
+
* Checks for:
|
|
517
|
+
* - ACCOMPLISH_TASK_ID env var (running inside Accomplish)
|
|
518
|
+
* - Accomplish data dir with an opencode/ subdirectory on disk
|
|
519
|
+
*/
|
|
520
|
+
function isAccomplishInstalled() {
|
|
521
|
+
if (process.env.ACCOMPLISH_TASK_ID) return true;
|
|
522
|
+
try {
|
|
523
|
+
const dataDir = getAccomplishDataDir();
|
|
524
|
+
const openCodeDir = path.join(dataDir, 'opencode');
|
|
525
|
+
return fs.existsSync(openCodeDir);
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Returns the OpenCode home directory inside the Accomplish data dir.
|
|
533
|
+
*/
|
|
534
|
+
function getAccomplishOpenCodeHome() {
|
|
535
|
+
return path.join(getAccomplishDataDir(), 'opencode');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Install the babysitter plugin into Accomplish's OpenCode directory.
|
|
540
|
+
* Mirrors the standard OpenCode install: copies bundle, writes index.js,
|
|
541
|
+
* installs skills and hooks.
|
|
542
|
+
*/
|
|
543
|
+
function installAccomplishSurface(packageRoot, accomplishOpenCodeHome) {
|
|
544
|
+
const pluginRoot = path.join(accomplishOpenCodeHome, 'plugins', PLUGIN_NAME);
|
|
545
|
+
|
|
546
|
+
// Copy plugin bundle
|
|
547
|
+
copyPluginBundle(packageRoot, pluginRoot);
|
|
548
|
+
|
|
549
|
+
// Create index.js entry point
|
|
550
|
+
writeIndexJs(pluginRoot);
|
|
551
|
+
|
|
552
|
+
// Install skills and hooks config
|
|
553
|
+
installOpenCodeSurface(packageRoot, accomplishOpenCodeHome);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// OpenCode surface installation
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
function installOpenCodeSurface(packageRoot, openCodeHome) {
|
|
561
|
+
// Copy skills into .opencode/skills/
|
|
562
|
+
const sourceSkills = path.join(packageRoot, 'skills');
|
|
563
|
+
if (fs.existsSync(sourceSkills)) {
|
|
564
|
+
const targetSkills = path.join(openCodeHome, 'skills');
|
|
565
|
+
fs.mkdirSync(targetSkills, { recursive: true });
|
|
566
|
+
for (const entry of fs.readdirSync(sourceSkills, { withFileTypes: true })) {
|
|
567
|
+
if (!entry.isDirectory()) continue;
|
|
568
|
+
copyRecursive(
|
|
569
|
+
path.join(sourceSkills, entry.name),
|
|
570
|
+
path.join(targetSkills, entry.name),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Merge hooks config
|
|
576
|
+
mergeHooksConfig(packageRoot, openCodeHome);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
module.exports = {
|
|
581
|
+
PLUGIN_NAME,
|
|
582
|
+
PLUGIN_CATEGORY,
|
|
583
|
+
getUserHome,
|
|
584
|
+
getHarnessHome,
|
|
585
|
+
writeFileIfChanged,
|
|
586
|
+
readJson,
|
|
587
|
+
writeJson,
|
|
588
|
+
ensureExecutable,
|
|
589
|
+
warnWindowsHooks,
|
|
590
|
+
runPostInstall,
|
|
591
|
+
getGlobalStateDir,
|
|
592
|
+
resolveCliCommand,
|
|
593
|
+
runCli,
|
|
594
|
+
ensureGlobalProcessLibrary,
|
|
595
|
+
PLUGIN_BUNDLE_ENTRIES,
|
|
596
|
+
copyRecursive,
|
|
597
|
+
copyPluginBundle,
|
|
598
|
+
DEFAULT_MARKETPLACE,
|
|
599
|
+
normalizeMarketplaceSourcePath,
|
|
600
|
+
normalizeMarketplaceName,
|
|
601
|
+
ensureMarketplaceEntry,
|
|
602
|
+
removeMarketplaceEntry,
|
|
603
|
+
HOOK_SCRIPT_NAMES,
|
|
604
|
+
getOpenCodeHome,
|
|
605
|
+
getHomePluginRoot,
|
|
606
|
+
getHomeMarketplacePath,
|
|
607
|
+
writeIndexJs,
|
|
608
|
+
mergeHooksConfig,
|
|
609
|
+
removeManagedHooks,
|
|
610
|
+
getAccomplishDataDir,
|
|
611
|
+
isAccomplishInstalled,
|
|
612
|
+
getAccomplishOpenCodeHome,
|
|
613
|
+
installAccomplishSurface,
|
|
614
|
+
installOpenCodeSurface,
|
|
615
|
+
};
|