@a5c-ai/babysitter-github 0.1.1-staging.0a3fc67d

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.
@@ -0,0 +1,450 @@
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-github';
9
+ const PLUGIN_CATEGORY = 'Coding';
10
+ const LEGACY_HOOK_SCRIPT_NAMES = [
11
+ 'session-start.sh',
12
+ 'stop-hook.sh',
13
+ 'user-prompt-submit.sh',
14
+ ];
15
+ const HOOK_SCRIPT_NAMES = [
16
+ 'session-start.sh',
17
+ 'session-start.ps1',
18
+ 'session-end.sh',
19
+ 'session-end.ps1',
20
+ 'user-prompt-submitted.sh',
21
+ 'user-prompt-submitted.ps1',
22
+ ];
23
+ const DEFAULT_MARKETPLACE = {
24
+ name: 'local-plugins',
25
+ interface: {
26
+ displayName: 'Local Plugins',
27
+ },
28
+ plugins: [],
29
+ };
30
+ const PLUGIN_BUNDLE_ENTRIES = [
31
+ 'plugin.json',
32
+ 'hooks.json',
33
+ 'hooks',
34
+ 'skills',
35
+ 'versions.json',
36
+ 'AGENTS.md',
37
+ ];
38
+
39
+ function getCopilotHome() {
40
+ if (process.env.COPILOT_HOME) return path.resolve(process.env.COPILOT_HOME);
41
+ return path.join(os.homedir(), '.copilot');
42
+ }
43
+
44
+ function getUserHome() {
45
+ if (process.env.USERPROFILE) return path.resolve(process.env.USERPROFILE);
46
+ if (process.env.HOME) return path.resolve(process.env.HOME);
47
+ return os.homedir();
48
+ }
49
+
50
+ function getGlobalStateDir() {
51
+ if (process.env.BABYSITTER_GLOBAL_STATE_DIR) {
52
+ return path.resolve(process.env.BABYSITTER_GLOBAL_STATE_DIR);
53
+ }
54
+ return path.join(getUserHome(), '.a5c');
55
+ }
56
+
57
+ function getHomePluginRoot() {
58
+ if (process.env.BABYSITTER_GITHUB_PLUGIN_DIR) {
59
+ return path.resolve(process.env.BABYSITTER_GITHUB_PLUGIN_DIR, PLUGIN_NAME);
60
+ }
61
+ return path.join(getCopilotHome(), 'plugins', PLUGIN_NAME);
62
+ }
63
+
64
+ function getHomeMarketplacePath() {
65
+ if (process.env.BABYSITTER_GITHUB_MARKETPLACE_PATH) {
66
+ return path.resolve(process.env.BABYSITTER_GITHUB_MARKETPLACE_PATH);
67
+ }
68
+ return path.join(getUserHome(), '.agents', 'plugins', 'marketplace.json');
69
+ }
70
+
71
+ function writeFileIfChanged(filePath, contents) {
72
+ if (fs.existsSync(filePath)) {
73
+ const current = fs.readFileSync(filePath, 'utf8');
74
+ if (current === contents) {
75
+ return false;
76
+ }
77
+ }
78
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
79
+ fs.writeFileSync(filePath, contents, 'utf8');
80
+ return true;
81
+ }
82
+
83
+ function copyRecursive(src, dest) {
84
+ const stat = fs.statSync(src);
85
+ if (stat.isDirectory()) {
86
+ fs.mkdirSync(dest, { recursive: true });
87
+ for (const entry of fs.readdirSync(src)) {
88
+ if (['node_modules', '.git', 'test', '.a5c'].includes(entry)) continue;
89
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (path.basename(src) === 'SKILL.md') {
95
+ const file = fs.readFileSync(src);
96
+ const hasBom = file.length >= 3 && file[0] === 0xef && file[1] === 0xbb && file[2] === 0xbf;
97
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
98
+ fs.writeFileSync(dest, hasBom ? file.subarray(3) : file);
99
+ return;
100
+ }
101
+
102
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
103
+ fs.copyFileSync(src, dest);
104
+ }
105
+
106
+ function copyPluginBundle(packageRoot, pluginRoot) {
107
+ if (path.resolve(packageRoot) === path.resolve(pluginRoot)) {
108
+ return;
109
+ }
110
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
111
+ fs.mkdirSync(pluginRoot, { recursive: true });
112
+ for (const entry of PLUGIN_BUNDLE_ENTRIES) {
113
+ const src = path.join(packageRoot, entry);
114
+ if (fs.existsSync(src)) {
115
+ copyRecursive(src, path.join(pluginRoot, entry));
116
+ }
117
+ }
118
+ }
119
+
120
+ function ensureExecutable(filePath) {
121
+ try {
122
+ fs.chmodSync(filePath, 0o755);
123
+ } catch {
124
+ // Best-effort only. Windows and some filesystems may ignore mode changes.
125
+ }
126
+ }
127
+
128
+ function normalizeMarketplaceSourcePath(marketplacePath, pluginSourcePath) {
129
+ let next = pluginSourcePath;
130
+ if (path.isAbsolute(next)) {
131
+ next = path.relative(path.dirname(marketplacePath), next);
132
+ }
133
+ next = String(next || '').replace(/\\/g, '/');
134
+ if (!next.startsWith('./') && !next.startsWith('../')) {
135
+ next = `./${next}`;
136
+ }
137
+ return next;
138
+ }
139
+
140
+ function readJson(filePath) {
141
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
142
+ }
143
+
144
+ function writeJson(filePath, value) {
145
+ writeFileIfChanged(filePath, `${JSON.stringify(value, null, 2)}\n`);
146
+ }
147
+
148
+ function ensureMarketplaceEntry(marketplacePath, pluginSourcePath) {
149
+ const marketplace = fs.existsSync(marketplacePath)
150
+ ? readJson(marketplacePath)
151
+ : { ...DEFAULT_MARKETPLACE, plugins: [] };
152
+ marketplace.name = marketplace.name || DEFAULT_MARKETPLACE.name;
153
+ marketplace.interface = marketplace.interface || {};
154
+ marketplace.interface.displayName =
155
+ marketplace.interface.displayName || DEFAULT_MARKETPLACE.interface.displayName;
156
+ const nextEntry = {
157
+ name: PLUGIN_NAME,
158
+ source: {
159
+ source: 'local',
160
+ path: normalizeMarketplaceSourcePath(marketplacePath, pluginSourcePath),
161
+ },
162
+ policy: {
163
+ installation: 'AVAILABLE',
164
+ authentication: 'ON_INSTALL',
165
+ },
166
+ category: PLUGIN_CATEGORY,
167
+ };
168
+ const existingIndex = Array.isArray(marketplace.plugins)
169
+ ? marketplace.plugins.findIndex((entry) => entry && entry.name === PLUGIN_NAME)
170
+ : -1;
171
+ if (!Array.isArray(marketplace.plugins)) {
172
+ marketplace.plugins = [nextEntry];
173
+ } else if (existingIndex >= 0) {
174
+ marketplace.plugins[existingIndex] = nextEntry;
175
+ } else {
176
+ marketplace.plugins.push(nextEntry);
177
+ }
178
+ writeJson(marketplacePath, marketplace);
179
+ return nextEntry;
180
+ }
181
+
182
+ function removeMarketplaceEntry(marketplacePath) {
183
+ if (!fs.existsSync(marketplacePath)) {
184
+ return;
185
+ }
186
+ const marketplace = readJson(marketplacePath);
187
+ if (!Array.isArray(marketplace.plugins)) {
188
+ return;
189
+ }
190
+ marketplace.plugins = marketplace.plugins.filter((entry) => entry && entry.name !== PLUGIN_NAME);
191
+ writeJson(marketplacePath, marketplace);
192
+ }
193
+
194
+ /**
195
+ * Registers the plugin in ~/.copilot/config.json.
196
+ */
197
+ function registerCopilotPlugin(pluginRoot) {
198
+ const copilotHome = getCopilotHome();
199
+ const configPath = path.join(copilotHome, 'config.json');
200
+
201
+ fs.mkdirSync(copilotHome, { recursive: true });
202
+
203
+ let config = {};
204
+ if (fs.existsSync(configPath)) {
205
+ try {
206
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
207
+ } catch {
208
+ config = {};
209
+ }
210
+ }
211
+
212
+ if (!config.plugins) {
213
+ config.plugins = [];
214
+ }
215
+
216
+ const existing = config.plugins.findIndex(
217
+ (p) => (typeof p === 'string' ? p : p.path) === pluginRoot
218
+ );
219
+
220
+ if (existing === -1) {
221
+ config.plugins.push({
222
+ path: pluginRoot,
223
+ enabled: true,
224
+ });
225
+ } else {
226
+ config.plugins[existing] = {
227
+ path: pluginRoot,
228
+ enabled: true,
229
+ };
230
+ }
231
+
232
+ writeFileIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`);
233
+ }
234
+
235
+ /**
236
+ * Removes the plugin entry from ~/.copilot/config.json.
237
+ */
238
+ function deregisterCopilotPlugin(pluginRoot) {
239
+ const configPath = path.join(getCopilotHome(), 'config.json');
240
+ if (!fs.existsSync(configPath)) return;
241
+
242
+ try {
243
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
244
+ if (Array.isArray(config.plugins)) {
245
+ config.plugins = config.plugins.filter(
246
+ (p) => (typeof p === 'string' ? p : p.path) !== pluginRoot
247
+ );
248
+ writeFileIfChanged(configPath, `${JSON.stringify(config, null, 2)}\n`);
249
+ console.log(`[${PLUGIN_NAME}] Removed plugin entry from config.json`);
250
+ }
251
+ } catch (err) {
252
+ console.warn(`[${PLUGIN_NAME}] Warning: Could not update config.json: ${err.message}`);
253
+ }
254
+ }
255
+
256
+ function installManagedSkills(packageRoot, copilotHome) {
257
+ const sourceRoot = path.join(packageRoot, 'skills');
258
+ if (!fs.existsSync(sourceRoot)) return;
259
+ const targetRoot = path.join(copilotHome, 'skills');
260
+ fs.mkdirSync(targetRoot, { recursive: true });
261
+
262
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
263
+ if (!entry.isDirectory()) continue;
264
+ copyRecursive(
265
+ path.join(sourceRoot, entry.name),
266
+ path.join(targetRoot, entry.name),
267
+ );
268
+ }
269
+ }
270
+
271
+ function mergeManagedHooksConfig(packageRoot, copilotHome) {
272
+ const hooksJsonPath = path.join(packageRoot, 'hooks.json');
273
+ if (!fs.existsSync(hooksJsonPath)) return;
274
+ const managedConfig = readJson(hooksJsonPath);
275
+ const managedHooks = managedConfig.hooks || {};
276
+ const hooksConfigPath = path.join(copilotHome, 'hooks.json');
277
+ const existing = fs.existsSync(hooksConfigPath)
278
+ ? readJson(hooksConfigPath)
279
+ : { version: 1, hooks: {} };
280
+ // Ensure version field is present per Copilot CLI spec
281
+ existing.version = existing.version || 1;
282
+ if (!existing.hooks || typeof existing.hooks !== 'object') {
283
+ existing.hooks = {};
284
+ }
285
+
286
+ // Remove legacy PascalCase event entries that are no longer valid
287
+ for (const legacyEvent of ['SessionStart', 'UserPromptSubmit', 'Stop']) {
288
+ delete existing.hooks[legacyEvent];
289
+ }
290
+
291
+ const allScriptNames = [...LEGACY_HOOK_SCRIPT_NAMES, ...HOOK_SCRIPT_NAMES];
292
+ for (const [eventName, entries] of Object.entries(managedHooks)) {
293
+ const existingEntries = Array.isArray(existing.hooks[eventName]) ? existing.hooks[eventName] : [];
294
+ const filteredEntries = existingEntries
295
+ .filter((entry) => {
296
+ const bash = String(entry.bash || entry.command || '');
297
+ const ps = String(entry.powershell || '');
298
+ return !allScriptNames.some((name) => bash.includes(name) || ps.includes(name));
299
+ });
300
+ existing.hooks[eventName] = [...filteredEntries, ...entries];
301
+ }
302
+
303
+ writeJson(hooksConfigPath, existing);
304
+ }
305
+
306
+ function installManagedHooks(packageRoot, copilotHome) {
307
+ const sourceRoot = path.join(packageRoot, 'hooks');
308
+ if (!fs.existsSync(sourceRoot)) return;
309
+ const targetRoot = path.join(copilotHome, 'hooks');
310
+ fs.mkdirSync(targetRoot, { recursive: true });
311
+
312
+ for (const scriptName of HOOK_SCRIPT_NAMES) {
313
+ const sourcePath = path.join(sourceRoot, scriptName);
314
+ if (!fs.existsSync(sourcePath)) continue;
315
+ const targetPath = path.join(targetRoot, scriptName);
316
+ copyRecursive(sourcePath, targetPath);
317
+ ensureExecutable(targetPath);
318
+ }
319
+
320
+ mergeManagedHooksConfig(packageRoot, copilotHome);
321
+ }
322
+
323
+ function removeLegacyHooks(copilotHome) {
324
+ for (const hookName of LEGACY_HOOK_SCRIPT_NAMES) {
325
+ fs.rmSync(path.join(copilotHome, 'hooks', hookName), { force: true });
326
+ }
327
+
328
+ const hooksConfigPath = path.join(copilotHome, 'hooks.json');
329
+ if (!fs.existsSync(hooksConfigPath)) {
330
+ return;
331
+ }
332
+ let hooksConfig;
333
+ try {
334
+ hooksConfig = readJson(hooksConfigPath);
335
+ } catch {
336
+ return;
337
+ }
338
+ if (!hooksConfig.hooks || typeof hooksConfig.hooks !== 'object') {
339
+ return;
340
+ }
341
+ for (const eventName of ['SessionStart', 'UserPromptSubmit', 'Stop', 'sessionStart', 'sessionEnd', 'userPromptSubmitted']) {
342
+ const eventHooks = Array.isArray(hooksConfig.hooks[eventName]) ? hooksConfig.hooks[eventName] : [];
343
+ const filteredMatchers = eventHooks
344
+ .map((matcher) => {
345
+ const hooks = Array.isArray(matcher.hooks) ? matcher.hooks : [];
346
+ const keptHooks = hooks.filter((hook) => {
347
+ const command = String(hook.command || '');
348
+ return !LEGACY_HOOK_SCRIPT_NAMES.some((name) => command.includes(name));
349
+ });
350
+ return keptHooks.length > 0 ? { ...matcher, hooks: keptHooks } : null;
351
+ })
352
+ .filter(Boolean);
353
+ if (filteredMatchers.length > 0) {
354
+ hooksConfig.hooks[eventName] = filteredMatchers;
355
+ } else {
356
+ delete hooksConfig.hooks[eventName];
357
+ }
358
+ }
359
+ if (Object.keys(hooksConfig.hooks).length === 0) {
360
+ fs.rmSync(hooksConfigPath, { force: true });
361
+ } else {
362
+ writeJson(hooksConfigPath, hooksConfig);
363
+ }
364
+ }
365
+
366
+ function installCopilotSurface(packageRoot, copilotHome) {
367
+ removeLegacyHooks(copilotHome);
368
+ installManagedSkills(packageRoot, copilotHome);
369
+ installManagedHooks(packageRoot, copilotHome);
370
+ }
371
+
372
+ function resolveBabysitterCommand(packageRoot) {
373
+ if (process.env.BABYSITTER_SDK_CLI) {
374
+ return {
375
+ command: process.execPath,
376
+ argsPrefix: [path.resolve(process.env.BABYSITTER_SDK_CLI)],
377
+ };
378
+ }
379
+ try {
380
+ return {
381
+ command: process.execPath,
382
+ argsPrefix: [
383
+ require.resolve('@a5c-ai/babysitter-sdk/dist/cli/main.js', {
384
+ paths: [packageRoot],
385
+ }),
386
+ ],
387
+ };
388
+ } catch {
389
+ return {
390
+ command: 'babysitter',
391
+ argsPrefix: [],
392
+ };
393
+ }
394
+ }
395
+
396
+ function runBabysitterCli(packageRoot, cliArgs, options = {}) {
397
+ const resolved = resolveBabysitterCommand(packageRoot);
398
+ const result = spawnSync(resolved.command, [...resolved.argsPrefix, ...cliArgs], {
399
+ cwd: options.cwd || process.cwd(),
400
+ stdio: ['ignore', 'pipe', 'pipe'],
401
+ encoding: 'utf8',
402
+ env: {
403
+ ...process.env,
404
+ ...(options.env || {}),
405
+ },
406
+ });
407
+ if (result.status !== 0) {
408
+ const stderr = (result.stderr || '').trim();
409
+ const stdout = (result.stdout || '').trim();
410
+ throw new Error(
411
+ `babysitter ${cliArgs.join(' ')} failed` +
412
+ (stderr ? `: ${stderr}` : stdout ? `: ${stdout}` : ''),
413
+ );
414
+ }
415
+ return result.stdout;
416
+ }
417
+
418
+ function ensureGlobalProcessLibrary(packageRoot) {
419
+ return JSON.parse(
420
+ runBabysitterCli(
421
+ packageRoot,
422
+ ['process-library:active', '--state-dir', getGlobalStateDir(), '--json'],
423
+ { cwd: packageRoot },
424
+ ),
425
+ );
426
+ }
427
+
428
+ function warnWindowsHooks() {
429
+ if (process.platform !== 'win32') {
430
+ return;
431
+ }
432
+ console.warn(`[${PLUGIN_NAME}] Note: On Windows, Copilot CLI will use .ps1 PowerShell hooks.`);
433
+ console.warn(`[${PLUGIN_NAME}] Both bash (.sh) and PowerShell (.ps1) hook scripts are included.`);
434
+ }
435
+
436
+ module.exports = {
437
+ copyPluginBundle,
438
+ deregisterCopilotPlugin,
439
+ ensureGlobalProcessLibrary,
440
+ ensureMarketplaceEntry,
441
+ getCopilotHome,
442
+ getHomeMarketplacePath,
443
+ getHomePluginRoot,
444
+ installCopilotSurface,
445
+ registerCopilotPlugin,
446
+ removeLegacyHooks,
447
+ removeMarketplaceEntry,
448
+ warnWindowsHooks,
449
+ writeJson,
450
+ };
package/bin/install.js ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const { spawnSync } = require('child_process');
6
+ const {
7
+ copyPluginBundle,
8
+ ensureGlobalProcessLibrary,
9
+ ensureMarketplaceEntry,
10
+ getCopilotHome,
11
+ getHomeMarketplacePath,
12
+ getHomePluginRoot,
13
+ installCopilotSurface,
14
+ warnWindowsHooks,
15
+ } = require('./install-shared');
16
+
17
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
18
+
19
+ /**
20
+ * Attempt to register the plugin via `copilot plugin install ./path`.
21
+ * Falls back to manual config.json registration if the CLI is not available.
22
+ */
23
+ function registerViaCopilotCli(pluginRoot) {
24
+ const result = spawnSync('copilot', ['plugin', 'install', pluginRoot], {
25
+ stdio: ['ignore', 'pipe', 'pipe'],
26
+ encoding: 'utf8',
27
+ timeout: 30000,
28
+ });
29
+
30
+ if (result.status === 0) {
31
+ console.log(`[babysitter-github] Registered plugin via 'copilot plugin install'`);
32
+ return true;
33
+ }
34
+
35
+ // CLI not available or failed -- fall back to manual registration
36
+ const stderr = (result.stderr || '').trim();
37
+ if (result.error || stderr.includes('not found') || stderr.includes('not recognized')) {
38
+ console.log(`[babysitter-github] Copilot CLI not found, using manual registration`);
39
+ } else {
40
+ console.warn(`[babysitter-github] 'copilot plugin install' failed: ${stderr || 'unknown error'}, using manual registration`);
41
+ }
42
+ return false;
43
+ }
44
+
45
+ function main() {
46
+ const copilotHome = getCopilotHome();
47
+ const pluginRoot = getHomePluginRoot();
48
+ const marketplacePath = getHomeMarketplacePath();
49
+
50
+ console.log(`[babysitter-github] Installing plugin to ${pluginRoot}`);
51
+
52
+ try {
53
+ copyPluginBundle(PACKAGE_ROOT, pluginRoot);
54
+ ensureMarketplaceEntry(marketplacePath, pluginRoot);
55
+
56
+ // Try native copilot CLI registration first; fall back to manual config.json
57
+ if (!registerViaCopilotCli(pluginRoot)) {
58
+ const { registerCopilotPlugin } = require('./install-shared');
59
+ registerCopilotPlugin(pluginRoot);
60
+ }
61
+
62
+ installCopilotSurface(PACKAGE_ROOT, copilotHome);
63
+
64
+ const active = ensureGlobalProcessLibrary(PACKAGE_ROOT);
65
+ console.log(`[babysitter-github] marketplace: ${marketplacePath}`);
66
+ console.log(`[babysitter-github] copilot config: ${path.join(copilotHome, 'config.json')}`);
67
+ console.log(`[babysitter-github] process library: ${active.binding?.dir}`);
68
+ if (active.defaultSpec?.cloneDir) {
69
+ console.log(`[babysitter-github] process library clone: ${active.defaultSpec.cloneDir}`);
70
+ }
71
+ console.log(`[babysitter-github] process library state: ${active.stateFile}`);
72
+ warnWindowsHooks();
73
+ console.log('[babysitter-github] Installation complete!');
74
+ console.log('[babysitter-github] Restart GitHub Copilot CLI to pick up the installed plugin and config changes.');
75
+ } catch (err) {
76
+ console.error(`[babysitter-github] Failed to install plugin: ${err.message}`);
77
+ process.exitCode = 1;
78
+ }
79
+ }
80
+
81
+ main();
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const { spawnSync } = require('child_process');
6
+ const {
7
+ deregisterCopilotPlugin,
8
+ getCopilotHome,
9
+ getHomeMarketplacePath,
10
+ getHomePluginRoot,
11
+ removeLegacyHooks,
12
+ removeMarketplaceEntry,
13
+ } = require('./install-shared');
14
+
15
+ const PLUGIN_NAME = 'babysitter-github';
16
+
17
+ /**
18
+ * Attempt to unregister the plugin via `copilot plugin uninstall`.
19
+ * Falls back to manual config.json cleanup if the CLI is not available.
20
+ */
21
+ function unregisterViaCopilotCli() {
22
+ const result = spawnSync('copilot', ['plugin', 'uninstall', PLUGIN_NAME], {
23
+ stdio: ['ignore', 'pipe', 'pipe'],
24
+ encoding: 'utf8',
25
+ timeout: 30000,
26
+ });
27
+
28
+ if (result.status === 0) {
29
+ console.log(`[${PLUGIN_NAME}] Unregistered plugin via 'copilot plugin uninstall'`);
30
+ return true;
31
+ }
32
+
33
+ // CLI not available or failed
34
+ const stderr = (result.stderr || '').trim();
35
+ if (result.error || stderr.includes('not found') || stderr.includes('not recognized')) {
36
+ console.log(`[${PLUGIN_NAME}] Copilot CLI not found, using manual cleanup`);
37
+ } else {
38
+ console.warn(`[${PLUGIN_NAME}] 'copilot plugin uninstall' failed: ${stderr || 'unknown error'}, using manual cleanup`);
39
+ }
40
+ return false;
41
+ }
42
+
43
+ function main() {
44
+ const copilotHome = getCopilotHome();
45
+ const pluginRoot = getHomePluginRoot();
46
+ const marketplacePath = getHomeMarketplacePath();
47
+ let removedPlugin = false;
48
+
49
+ if (fs.existsSync(pluginRoot)) {
50
+ try {
51
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
52
+ console.log(`[${PLUGIN_NAME}] Removed ${pluginRoot}`);
53
+ removedPlugin = true;
54
+ } catch (err) {
55
+ console.warn(`[${PLUGIN_NAME}] Warning: Could not remove plugin directory ${pluginRoot}: ${err.message}`);
56
+ }
57
+ }
58
+
59
+ removeMarketplaceEntry(marketplacePath);
60
+
61
+ // Try native copilot CLI unregistration first; fall back to manual config.json
62
+ if (!unregisterViaCopilotCli()) {
63
+ deregisterCopilotPlugin(pluginRoot);
64
+ }
65
+
66
+ removeLegacyHooks(copilotHome);
67
+
68
+ if (!removedPlugin) {
69
+ console.log(`[${PLUGIN_NAME}] Plugin directory not found, config and hooks cleaned if present.`);
70
+ return;
71
+ }
72
+
73
+ console.log(`[${PLUGIN_NAME}] Restart GitHub Copilot CLI to complete uninstallation.`);
74
+ }
75
+
76
+ main();
@@ -0,0 +1,68 @@
1
+ # Babysitter Session End Hook for GitHub Copilot CLI (PowerShell)
2
+ # Cleanup and logging on session exit.
3
+ #
4
+ # NOTE: Unlike Claude Code's Stop hook, sessionEnd output is IGNORED by
5
+ # Copilot CLI. This hook cannot block session exit or drive an orchestration
6
+ # loop. It is purely for cleanup and logging.
7
+
8
+ $ErrorActionPreference = "Continue"
9
+
10
+ $PluginRoot = if ($env:COPILOT_PLUGIN_DIR) { $env:COPILOT_PLUGIN_DIR } else { Split-Path -Parent $PSScriptRoot }
11
+
12
+ # Resolve babysitter CLI
13
+ $hasBabysitter = [bool](Get-Command babysitter -ErrorAction SilentlyContinue)
14
+ $useFallback = $false
15
+
16
+ if (-not $hasBabysitter) {
17
+ $localBin = Join-Path $env:USERPROFILE ".local\bin\babysitter.cmd"
18
+ if (Test-Path $localBin) {
19
+ $env:PATH = "$(Split-Path $localBin);$env:PATH"
20
+ $hasBabysitter = $true
21
+ } else {
22
+ $versionsFile = Join-Path $PluginRoot "versions.json"
23
+ try {
24
+ $SdkVersion = (Get-Content $versionsFile -Raw | ConvertFrom-Json).sdkVersion
25
+ if (-not $SdkVersion) { $SdkVersion = "latest" }
26
+ } catch {
27
+ $SdkVersion = "latest"
28
+ }
29
+ $useFallback = $true
30
+ }
31
+ }
32
+
33
+ $LogDir = if ($env:BABYSITTER_LOG_DIR) { $env:BABYSITTER_LOG_DIR } else { Join-Path $PluginRoot ".a5c\logs" }
34
+ $LogFile = Join-Path $LogDir "babysitter-session-end-hook.log"
35
+ New-Item -ItemType Directory -Path $LogDir -Force -ErrorAction SilentlyContinue | Out-Null
36
+
37
+ function Write-Blog {
38
+ param([string]$Message)
39
+ $ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
40
+ Add-Content -Path $LogFile -Value "[INFO] $ts $Message" -ErrorAction SilentlyContinue
41
+ }
42
+
43
+ Write-Blog "Hook script invoked"
44
+ Write-Blog "PLUGIN_ROOT=$PluginRoot"
45
+
46
+ # Capture stdin
47
+ $InputFile = [System.IO.Path]::GetTempFileName()
48
+ $input | Out-File -FilePath $InputFile -Encoding utf8
49
+
50
+ Write-Blog "Hook input received"
51
+
52
+ $stderrLog = Join-Path $LogDir "babysitter-session-end-hook-stderr.log"
53
+
54
+ try {
55
+ if ($useFallback) {
56
+ Get-Content $InputFile | & npx -y "@a5c-ai/babysitter-sdk@$SdkVersion" hook:run --hook-type session-end --harness github-copilot --plugin-root $PluginRoot --json 2>$stderrLog | Out-Null
57
+ } elseif ($hasBabysitter) {
58
+ Get-Content $InputFile | & babysitter hook:run --hook-type session-end --harness github-copilot --plugin-root $PluginRoot --json 2>$stderrLog | Out-Null
59
+ }
60
+ } catch {
61
+ Write-Blog "Hook error: $_"
62
+ }
63
+
64
+ Write-Blog "Session end hook complete"
65
+
66
+ Remove-Item $InputFile -Force -ErrorAction SilentlyContinue
67
+
68
+ exit 0