@chrisromp/copilot-bridge 0.6.0-dev.2
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/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/copilot-bridge.js +61 -0
- package/config.sample.json +100 -0
- package/dist/channels/mattermost/adapter.d.ts +55 -0
- package/dist/channels/mattermost/adapter.d.ts.map +1 -0
- package/dist/channels/mattermost/adapter.js +524 -0
- package/dist/channels/mattermost/adapter.js.map +1 -0
- package/dist/channels/mattermost/streaming.d.ts +29 -0
- package/dist/channels/mattermost/streaming.d.ts.map +1 -0
- package/dist/channels/mattermost/streaming.js +151 -0
- package/dist/channels/mattermost/streaming.js.map +1 -0
- package/dist/config.d.ts +107 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +817 -0
- package/dist/config.js.map +1 -0
- package/dist/core/bridge.d.ts +73 -0
- package/dist/core/bridge.d.ts.map +1 -0
- package/dist/core/bridge.js +166 -0
- package/dist/core/bridge.js.map +1 -0
- package/dist/core/channel-idle.d.ts +40 -0
- package/dist/core/channel-idle.d.ts.map +1 -0
- package/dist/core/channel-idle.js +120 -0
- package/dist/core/channel-idle.js.map +1 -0
- package/dist/core/command-handler.d.ts +51 -0
- package/dist/core/command-handler.d.ts.map +1 -0
- package/dist/core/command-handler.js +393 -0
- package/dist/core/command-handler.js.map +1 -0
- package/dist/core/inter-agent.d.ts +52 -0
- package/dist/core/inter-agent.d.ts.map +1 -0
- package/dist/core/inter-agent.js +179 -0
- package/dist/core/inter-agent.js.map +1 -0
- package/dist/core/onboarding.d.ts +44 -0
- package/dist/core/onboarding.d.ts.map +1 -0
- package/dist/core/onboarding.js +205 -0
- package/dist/core/onboarding.js.map +1 -0
- package/dist/core/scheduler.d.ts +38 -0
- package/dist/core/scheduler.d.ts.map +1 -0
- package/dist/core/scheduler.js +253 -0
- package/dist/core/scheduler.js.map +1 -0
- package/dist/core/session-manager.d.ts +166 -0
- package/dist/core/session-manager.d.ts.map +1 -0
- package/dist/core/session-manager.js +1732 -0
- package/dist/core/session-manager.js.map +1 -0
- package/dist/core/stream-formatter.d.ts +14 -0
- package/dist/core/stream-formatter.d.ts.map +1 -0
- package/dist/core/stream-formatter.js +198 -0
- package/dist/core/stream-formatter.js.map +1 -0
- package/dist/core/thread-utils.d.ts +22 -0
- package/dist/core/thread-utils.d.ts.map +1 -0
- package/dist/core/thread-utils.js +44 -0
- package/dist/core/thread-utils.js.map +1 -0
- package/dist/core/workspace-manager.d.ts +38 -0
- package/dist/core/workspace-manager.d.ts.map +1 -0
- package/dist/core/workspace-manager.js +230 -0
- package/dist/core/workspace-manager.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1286 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +34 -0
- package/dist/logger.js.map +1 -0
- package/dist/state/store.d.ts +124 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +523 -0
- package/dist/state/store.js.map +1 -0
- package/dist/types.d.ts +185 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +61 -0
- package/scripts/check.ts +267 -0
- package/scripts/com.copilot-bridge.plist +41 -0
- package/scripts/copilot-bridge.service +30 -0
- package/scripts/init.ts +250 -0
- package/scripts/install-service.ts +123 -0
- package/scripts/lib/config-gen.ts +129 -0
- package/scripts/lib/mattermost.ts +109 -0
- package/scripts/lib/output.ts +69 -0
- package/scripts/lib/prerequisites.ts +86 -0
- package/scripts/lib/prompts.ts +65 -0
- package/scripts/lib/service.ts +191 -0
- package/scripts/uninstall-service.ts +90 -0
- package/templates/admin/AGENTS.md +325 -0
- package/templates/admin/MEMORY.md +4 -0
- package/templates/agents/AGENTS.md +97 -0
- package/templates/agents/MEMORY.md +4 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prerequisite checks shared between init and check commands.
|
|
3
|
+
* Validates Node.js version, GitHub Copilot CLI, and auth status.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { execSync } from 'node:child_process';
|
|
7
|
+
import type { CheckResult } from './output.js';
|
|
8
|
+
|
|
9
|
+
export function checkNodeVersion(): CheckResult {
|
|
10
|
+
const version = process.version; // e.g., "v22.0.0"
|
|
11
|
+
const major = parseInt(version.slice(1).split('.')[0], 10);
|
|
12
|
+
if (major >= 20) {
|
|
13
|
+
return { status: 'pass', label: `Node.js ${version}` };
|
|
14
|
+
}
|
|
15
|
+
return { status: 'fail', label: `Node.js ${version}`, detail: 'requires v20 or higher' };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function tryCommand(cmd: string): string | null {
|
|
19
|
+
try {
|
|
20
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function checkCopilotCLI(): CheckResult {
|
|
27
|
+
// Try gh copilot first (GitHub CLI extension), then standalone copilot
|
|
28
|
+
const ghVersion = tryCommand('gh copilot --version 2>&1');
|
|
29
|
+
if (ghVersion) {
|
|
30
|
+
const ver = ghVersion.split('\n')[0];
|
|
31
|
+
return { status: 'pass', label: 'GitHub Copilot CLI', detail: ver };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const standaloneVersion = tryCommand('copilot --version 2>&1');
|
|
35
|
+
if (standaloneVersion) {
|
|
36
|
+
const ver = standaloneVersion.split('\n')[0];
|
|
37
|
+
return { status: 'pass', label: 'Copilot CLI (standalone)', detail: ver };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
status: 'fail',
|
|
42
|
+
label: 'GitHub Copilot CLI',
|
|
43
|
+
detail: 'not found — install via: gh extension install github/gh-copilot',
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkGitHubAuth(): CheckResult {
|
|
48
|
+
// Check for Copilot-specific env token first (highest priority)
|
|
49
|
+
if (process.env.COPILOT_GITHUB_TOKEN) {
|
|
50
|
+
return { status: 'pass', label: 'GitHub authenticated', detail: 'via COPILOT_GITHUB_TOKEN' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for general GitHub tokens
|
|
54
|
+
if (process.env.GH_TOKEN) {
|
|
55
|
+
return { status: 'pass', label: 'GitHub authenticated', detail: 'via GH_TOKEN' };
|
|
56
|
+
}
|
|
57
|
+
if (process.env.GITHUB_TOKEN) {
|
|
58
|
+
return { status: 'pass', label: 'GitHub authenticated', detail: 'via GITHUB_TOKEN' };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check if gh CLI is authenticated
|
|
62
|
+
const authStatus = tryCommand('gh auth status 2>&1');
|
|
63
|
+
if (authStatus && authStatus.includes('Logged in')) {
|
|
64
|
+
return { status: 'pass', label: 'GitHub authenticated', detail: 'via gh CLI' };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Check if Copilot CLI has stored credentials
|
|
68
|
+
const copilotAuth = tryCommand('copilot auth status 2>&1');
|
|
69
|
+
if (copilotAuth && !copilotAuth.includes('not logged in')) {
|
|
70
|
+
return { status: 'pass', label: 'GitHub authenticated', detail: 'via Copilot CLI' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
status: 'warn',
|
|
75
|
+
label: 'GitHub authentication',
|
|
76
|
+
detail: 'no token found — set COPILOT_GITHUB_TOKEN, run gh auth login, or run copilot auth login',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function runAllPrereqs(): CheckResult[] {
|
|
81
|
+
return [
|
|
82
|
+
checkNodeVersion(),
|
|
83
|
+
checkCopilotCLI(),
|
|
84
|
+
checkGitHubAuth(),
|
|
85
|
+
];
|
|
86
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Readline-based interactive prompts. No external dependencies.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as readline from 'node:readline';
|
|
6
|
+
|
|
7
|
+
let rl: readline.Interface | null = null;
|
|
8
|
+
|
|
9
|
+
function getRL(): readline.Interface {
|
|
10
|
+
if (!rl) {
|
|
11
|
+
rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return rl;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function closePrompts(): void {
|
|
20
|
+
rl?.close();
|
|
21
|
+
rl = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function ask(question: string, defaultValue?: string): Promise<string> {
|
|
25
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
getRL().question(`${question}${suffix}: `, (answer) => {
|
|
28
|
+
resolve(answer.trim() || defaultValue || '');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function askRequired(question: string): Promise<string> {
|
|
34
|
+
let answer = '';
|
|
35
|
+
while (!answer) {
|
|
36
|
+
answer = await ask(question);
|
|
37
|
+
if (!answer) console.log(' This field is required.');
|
|
38
|
+
}
|
|
39
|
+
return answer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function confirm(question: string, defaultYes = true): Promise<boolean> {
|
|
43
|
+
const hint = defaultYes ? 'Y/n' : 'y/N';
|
|
44
|
+
const answer = await ask(`${question} (${hint})`);
|
|
45
|
+
if (!answer) return defaultYes;
|
|
46
|
+
return answer.toLowerCase().startsWith('y');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function choose(question: string, options: string[], defaultIndex = 0): Promise<number> {
|
|
50
|
+
console.log(`\n${question}`);
|
|
51
|
+
for (let i = 0; i < options.length; i++) {
|
|
52
|
+
const marker = i === defaultIndex ? '>' : ' ';
|
|
53
|
+
console.log(` ${marker} ${i + 1}. ${options[i]}`);
|
|
54
|
+
}
|
|
55
|
+
const answer = await ask('Choose', String(defaultIndex + 1));
|
|
56
|
+
const idx = parseInt(answer, 10) - 1;
|
|
57
|
+
if (idx >= 0 && idx < options.length) return idx;
|
|
58
|
+
return defaultIndex;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function askSecret(question: string): Promise<string> {
|
|
62
|
+
// For tokens — we don't mask input because readline doesn't support it
|
|
63
|
+
// without raw mode, but we label it clearly
|
|
64
|
+
return askRequired(question);
|
|
65
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-specific service file generation and installation.
|
|
3
|
+
* Supports macOS (launchd) and Linux (systemd system-level units).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'node:fs';
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import { execSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
export type Platform = 'macos' | 'linux' | 'unsupported';
|
|
12
|
+
|
|
13
|
+
export function detectPlatform(): Platform {
|
|
14
|
+
switch (process.platform) {
|
|
15
|
+
case 'darwin': return 'macos';
|
|
16
|
+
case 'linux': return 'linux';
|
|
17
|
+
default: return 'unsupported';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getNodePath(): string {
|
|
22
|
+
try {
|
|
23
|
+
return execSync('which node', { encoding: 'utf-8' }).trim();
|
|
24
|
+
} catch {
|
|
25
|
+
return '/usr/local/bin/node';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getSystemPath(): string {
|
|
30
|
+
// Include the directory containing the current node binary (e.g., nvm paths)
|
|
31
|
+
const nodeBinDir = path.dirname(getNodePath());
|
|
32
|
+
const platform = detectPlatform();
|
|
33
|
+
const basePath = platform === 'macos'
|
|
34
|
+
? '/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin'
|
|
35
|
+
: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
|
|
36
|
+
// Prepend node's bin dir if not already in the base path
|
|
37
|
+
if (basePath.split(':').includes(nodeBinDir)) return basePath;
|
|
38
|
+
return `${nodeBinDir}:${basePath}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// --- launchd (macOS) ---
|
|
42
|
+
|
|
43
|
+
export interface LaunchdConfig {
|
|
44
|
+
label: string;
|
|
45
|
+
bridgePath: string;
|
|
46
|
+
homePath: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function generateLaunchdPlist(config: LaunchdConfig): string {
|
|
50
|
+
const nodePath = getNodePath();
|
|
51
|
+
const tsxPath = path.join(config.bridgePath, 'node_modules', '.bin', 'tsx');
|
|
52
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
54
|
+
<plist version="1.0">
|
|
55
|
+
<dict>
|
|
56
|
+
<key>Label</key>
|
|
57
|
+
<string>${config.label}</string>
|
|
58
|
+
|
|
59
|
+
<key>ProgramArguments</key>
|
|
60
|
+
<array>
|
|
61
|
+
<string>${nodePath}</string>
|
|
62
|
+
<string>${tsxPath}</string>
|
|
63
|
+
<string>dist/index.js</string>
|
|
64
|
+
</array>
|
|
65
|
+
|
|
66
|
+
<key>WorkingDirectory</key>
|
|
67
|
+
<string>${config.bridgePath}</string>
|
|
68
|
+
|
|
69
|
+
<key>EnvironmentVariables</key>
|
|
70
|
+
<dict>
|
|
71
|
+
<key>PATH</key>
|
|
72
|
+
<string>${getSystemPath()}</string>
|
|
73
|
+
<key>HOME</key>
|
|
74
|
+
<string>${config.homePath}</string>
|
|
75
|
+
</dict>
|
|
76
|
+
|
|
77
|
+
<key>RunAtLoad</key>
|
|
78
|
+
<true/>
|
|
79
|
+
|
|
80
|
+
<key>KeepAlive</key>
|
|
81
|
+
<true/>
|
|
82
|
+
|
|
83
|
+
<key>ThrottleInterval</key>
|
|
84
|
+
<integer>10</integer>
|
|
85
|
+
|
|
86
|
+
<key>StandardOutPath</key>
|
|
87
|
+
<string>/tmp/copilot-bridge.log</string>
|
|
88
|
+
|
|
89
|
+
<key>StandardErrorPath</key>
|
|
90
|
+
<string>/tmp/copilot-bridge.log</string>
|
|
91
|
+
</dict>
|
|
92
|
+
</plist>`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getLaunchdInstallPath(): string {
|
|
96
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', 'com.copilot-bridge.plist');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function installLaunchd(plistContent: string): { installed: boolean; path: string; error?: string } {
|
|
100
|
+
const installPath = getLaunchdInstallPath();
|
|
101
|
+
try {
|
|
102
|
+
const dir = path.dirname(installPath);
|
|
103
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
// Unload existing service before overwriting (ignore errors if not loaded)
|
|
105
|
+
try { execSync(`launchctl bootout gui/$(id -u) "${installPath}" 2>/dev/null`, { encoding: 'utf-8' }); } catch { /* not loaded */ }
|
|
106
|
+
fs.writeFileSync(installPath, plistContent, 'utf-8');
|
|
107
|
+
execSync(`launchctl load "${installPath}"`, { encoding: 'utf-8' });
|
|
108
|
+
return { installed: true, path: installPath };
|
|
109
|
+
} catch (err) {
|
|
110
|
+
return { installed: false, path: installPath, error: String(err) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- systemd (Linux) ---
|
|
115
|
+
|
|
116
|
+
export interface SystemdConfig {
|
|
117
|
+
bridgePath: string;
|
|
118
|
+
homePath: string;
|
|
119
|
+
user: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function generateSystemdUnit(config: SystemdConfig): string {
|
|
123
|
+
const nodePath = getNodePath();
|
|
124
|
+
const tsxPath = path.join(config.bridgePath, 'node_modules', '.bin', 'tsx');
|
|
125
|
+
return `[Unit]
|
|
126
|
+
Description=Copilot Bridge
|
|
127
|
+
After=network.target
|
|
128
|
+
|
|
129
|
+
[Service]
|
|
130
|
+
Type=simple
|
|
131
|
+
User=${config.user}
|
|
132
|
+
ExecStart=${nodePath} ${tsxPath} ${config.bridgePath}/dist/index.js
|
|
133
|
+
WorkingDirectory=${config.bridgePath}
|
|
134
|
+
Environment=HOME=${config.homePath}
|
|
135
|
+
Environment=PATH=${getSystemPath()}
|
|
136
|
+
Restart=always
|
|
137
|
+
RestartSec=10
|
|
138
|
+
|
|
139
|
+
[Install]
|
|
140
|
+
WantedBy=multi-user.target`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getSystemdInstallPath(): string {
|
|
144
|
+
return '/etc/systemd/system/copilot-bridge.service';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Service status ---
|
|
148
|
+
|
|
149
|
+
export function getServiceStatus(): { running: boolean; pid?: number; detail: string } {
|
|
150
|
+
const platform = detectPlatform();
|
|
151
|
+
|
|
152
|
+
if (platform === 'macos') {
|
|
153
|
+
try {
|
|
154
|
+
const output = execSync('launchctl list com.copilot-bridge 2>/dev/null', { encoding: 'utf-8' });
|
|
155
|
+
const pidMatch = output.match(/"PID"\s*=\s*(\d+)/);
|
|
156
|
+
if (pidMatch) {
|
|
157
|
+
return { running: true, pid: parseInt(pidMatch[1], 10), detail: `launchd, PID ${pidMatch[1]}` };
|
|
158
|
+
}
|
|
159
|
+
// launchctl list succeeded but no PID — service loaded but not running
|
|
160
|
+
return { running: false, detail: 'launchd: loaded but not running' };
|
|
161
|
+
} catch {
|
|
162
|
+
return { running: false, detail: 'launchd: not loaded' };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (platform === 'linux') {
|
|
167
|
+
try {
|
|
168
|
+
// systemctl is-active exits non-zero for inactive/unknown services
|
|
169
|
+
const output = execSync('systemctl is-active copilot-bridge 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
170
|
+
if (output === 'active') {
|
|
171
|
+
try {
|
|
172
|
+
const pid = execSync('systemctl show copilot-bridge --property=MainPID --value 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
173
|
+
return { running: true, pid: parseInt(pid, 10), detail: `systemd, PID ${pid}` };
|
|
174
|
+
} catch {
|
|
175
|
+
return { running: true, detail: 'systemd: active' };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { running: false, detail: `systemd: ${output}` };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
// Exit code 3 = inactive/dead (unit exists but not running)
|
|
181
|
+
// Exit code 4 = no such unit file
|
|
182
|
+
const stdout = err instanceof Error && 'stdout' in err ? String((err as { stdout: unknown }).stdout).trim() : '';
|
|
183
|
+
if (stdout === 'inactive' || stdout === 'failed' || stdout === 'activating' || stdout === 'deactivating') {
|
|
184
|
+
return { running: false, detail: `systemd: ${stdout}` };
|
|
185
|
+
}
|
|
186
|
+
return { running: false, detail: 'systemd: not installed' };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { running: false, detail: 'unsupported platform' };
|
|
191
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* copilot-bridge uninstall-service — Remove the bridge system service.
|
|
4
|
+
*
|
|
5
|
+
* macOS: unloads and removes the launchd plist
|
|
6
|
+
* Linux: stops, disables, and removes the systemd unit (requires sudo)
|
|
7
|
+
*
|
|
8
|
+
* Usage: npm run uninstall-service
|
|
9
|
+
* npx tsx scripts/uninstall-service.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import { heading, success, fail, info, dim, blank } from './lib/output.js';
|
|
14
|
+
import { detectPlatform, getLaunchdInstallPath, getSystemdInstallPath } from './lib/service.js';
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
|
|
17
|
+
function main() {
|
|
18
|
+
const osPlatform = detectPlatform();
|
|
19
|
+
|
|
20
|
+
heading('🗑️ copilot-bridge service uninstaller');
|
|
21
|
+
blank();
|
|
22
|
+
|
|
23
|
+
if (osPlatform === 'macos') {
|
|
24
|
+
const plistPath = getLaunchdInstallPath();
|
|
25
|
+
|
|
26
|
+
if (!fs.existsSync(plistPath)) {
|
|
27
|
+
info('No launchd service found — nothing to uninstall.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
info(`Unloading and removing ${plistPath}`);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// bootout is the modern replacement for unload
|
|
35
|
+
execSync(`launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null || launchctl unload "${plistPath}" 2>/dev/null`, {
|
|
36
|
+
stdio: 'inherit',
|
|
37
|
+
});
|
|
38
|
+
} catch {
|
|
39
|
+
// Service may not be loaded — that's fine
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(plistPath);
|
|
44
|
+
blank();
|
|
45
|
+
success('Service uninstalled.');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
fail(`Failed to remove plist: ${err}`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
} else if (osPlatform === 'linux') {
|
|
52
|
+
const unitPath = getSystemdInstallPath();
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(unitPath)) {
|
|
55
|
+
info('No systemd service found — nothing to uninstall.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const isRoot = process.getuid?.() === 0;
|
|
60
|
+
if (!isRoot) {
|
|
61
|
+
info('This requires sudo to remove from /etc/systemd/system/.');
|
|
62
|
+
blank();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
execSync('sudo systemctl stop copilot-bridge 2>/dev/null || true', { stdio: 'inherit' });
|
|
67
|
+
execSync('sudo systemctl disable copilot-bridge 2>/dev/null || true', { stdio: 'inherit' });
|
|
68
|
+
execSync(`sudo rm "${unitPath}"`, { stdio: 'inherit' });
|
|
69
|
+
execSync('sudo systemctl daemon-reload', { stdio: 'inherit' });
|
|
70
|
+
blank();
|
|
71
|
+
success('Service uninstalled.');
|
|
72
|
+
} catch {
|
|
73
|
+
blank();
|
|
74
|
+
fail('Automatic uninstall failed (sudo may have been denied).');
|
|
75
|
+
blank();
|
|
76
|
+
info('To uninstall manually:');
|
|
77
|
+
dim(' sudo systemctl stop copilot-bridge');
|
|
78
|
+
dim(' sudo systemctl disable copilot-bridge');
|
|
79
|
+
dim(` sudo rm ${unitPath}`);
|
|
80
|
+
dim(' sudo systemctl daemon-reload');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
} else {
|
|
85
|
+
fail('Unsupported platform — no service to uninstall.');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
main();
|