habi-agent 0.1.3 → 0.1.5
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/dist/cli.d.ts +12 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +279 -184
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +8 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +127 -0
- package/dist/daemon.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +56 -1
- package/dist/server.js.map +1 -1
- package/dist/service.d.ts +17 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +267 -0
- package/dist/service.js.map +1 -0
- package/package.json +7 -6
- package/src/cli.ts +300 -195
- package/src/daemon.ts +110 -0
- package/src/server.ts +56 -1
- package/src/service.ts +223 -0
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* HABI Daemon — long-running background process
|
|
4
|
+
* Reads config from ~/.habi/config.json
|
|
5
|
+
* Managed by OS service layer (Windows Service / macOS LaunchAgent / Linux systemd)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { HabiLocalServer } from './server';
|
|
9
|
+
import { readHardwareIdentity } from './hardware';
|
|
10
|
+
import { registerDevice, sendHeartbeat } from './cloud';
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
|
|
15
|
+
const CONFIG_PATH = path.join(os.homedir(), '.habi', 'config.json');
|
|
16
|
+
const LOG_PATH = path.join(os.homedir(), '.habi', 'habi.log');
|
|
17
|
+
|
|
18
|
+
// ── Logging ────────────────────────────────────────────────────────────────
|
|
19
|
+
function log(msg: string) {
|
|
20
|
+
const line = `[${new Date().toISOString()}] ${msg}`;
|
|
21
|
+
console.log(line);
|
|
22
|
+
try {
|
|
23
|
+
fs.appendFileSync(LOG_PATH, line + '\n');
|
|
24
|
+
} catch { /* ignore */ }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Load config ────────────────────────────────────────────────────────────
|
|
28
|
+
interface HabiConfig {
|
|
29
|
+
key: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadConfig(): HabiConfig {
|
|
34
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
35
|
+
throw new Error(`HABI config not found at ${CONFIG_PATH}. Run: npx habi-agent@latest start --key <key> --name <name>`);
|
|
36
|
+
}
|
|
37
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
41
|
+
async function main() {
|
|
42
|
+
log('HABI daemon starting...');
|
|
43
|
+
|
|
44
|
+
let config: HabiConfig;
|
|
45
|
+
try {
|
|
46
|
+
config = loadConfig();
|
|
47
|
+
} catch (err) {
|
|
48
|
+
log(`Config error: ${err}`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Inject into env so cloud.ts picks them up
|
|
53
|
+
process.env.HABI_KEY = config.key;
|
|
54
|
+
process.env.HABI_DEVICE_NAME = config.name;
|
|
55
|
+
|
|
56
|
+
const server = new HabiLocalServer();
|
|
57
|
+
|
|
58
|
+
const shutdown = () => {
|
|
59
|
+
log('HABI daemon shutting down...');
|
|
60
|
+
server.stop();
|
|
61
|
+
process.exit(0);
|
|
62
|
+
};
|
|
63
|
+
process.on('SIGINT', shutdown);
|
|
64
|
+
process.on('SIGTERM', shutdown);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await server.start();
|
|
68
|
+
log(`Agent running on port 7432`);
|
|
69
|
+
|
|
70
|
+
const hw = readHardwareIdentity();
|
|
71
|
+
log(`Hardware: ${hw.fingerprint} (${hw.type})`);
|
|
72
|
+
|
|
73
|
+
log(`Registering "${config.name}" with cloud...`);
|
|
74
|
+
const cloudState = await registerDevice({
|
|
75
|
+
friendlyName: config.name,
|
|
76
|
+
hardwareFingerprint: hw.fingerprint,
|
|
77
|
+
fingerprintType: hw.type,
|
|
78
|
+
agentVersion: '0.1.5',
|
|
79
|
+
osPlatform: os.platform(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (cloudState) {
|
|
83
|
+
log(`✓ Registered — device ID: ${cloudState.deviceId.slice(0, 8)}...`);
|
|
84
|
+
|
|
85
|
+
let currentToken = cloudState.sessionToken;
|
|
86
|
+
setInterval(async () => {
|
|
87
|
+
const hb = await sendHeartbeat({
|
|
88
|
+
deviceId: cloudState.deviceId,
|
|
89
|
+
sessionToken: currentToken,
|
|
90
|
+
hardwareFingerprint: hw.fingerprint,
|
|
91
|
+
});
|
|
92
|
+
if (hb) {
|
|
93
|
+
currentToken = hb.sessionToken;
|
|
94
|
+
log('Heartbeat sent');
|
|
95
|
+
}
|
|
96
|
+
}, 30000);
|
|
97
|
+
} else {
|
|
98
|
+
log('Warning: cloud registration failed — running in local mode');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
log('HABI daemon running. Logs: ' + LOG_PATH);
|
|
102
|
+
await new Promise(() => {}); // keep alive
|
|
103
|
+
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log(`Fatal error: ${err}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|
package/src/server.ts
CHANGED
|
@@ -158,7 +158,61 @@ export class HabiLocalServer {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
private async handleLogActivity(req: import('http').IncomingMessage, res: import('http').ServerResponse): Promise<void> {
|
|
162
|
+
try {
|
|
163
|
+
const data = await readBody(req) as Record<string, string>;
|
|
164
|
+
const hw = readHardwareIdentity();
|
|
165
|
+
const habiKey = process.env.HABI_KEY || '';
|
|
166
|
+
|
|
167
|
+
// Console log always
|
|
168
|
+
const icons: Record<string, string> = {
|
|
169
|
+
'claude.ai': '🟠', 'chatgpt.com': '🟢', 'cursor.sh': '🔵',
|
|
170
|
+
'gemini.google.com': '🔷', 'github.com': '⚫', 'perplexity.ai': '🔵',
|
|
171
|
+
'copilot.microsoft.com': '🔷', 'v0.dev': '⬛', 'character.ai': '🟣',
|
|
172
|
+
};
|
|
173
|
+
const icon = icons[data.hostname ?? ''] ?? '🤖';
|
|
174
|
+
const toolName = data.toolName ?? 'Unknown';
|
|
175
|
+
const engine = data.engine ?? 'Unknown';
|
|
176
|
+
console.log(`[HABI Activity] ${icon} ${toolName} — ${engine}`);
|
|
177
|
+
|
|
178
|
+
// Cloud log via the post() function from cloud module
|
|
179
|
+
if (habiKey) {
|
|
180
|
+
const logPayload: Record<string, unknown> = {
|
|
181
|
+
eventType: 'ACTION_ALLOWED',
|
|
182
|
+
operation: `${toolName} — ${engine}`,
|
|
183
|
+
hardwareId: hw.fingerprint,
|
|
184
|
+
toolName,
|
|
185
|
+
engine,
|
|
186
|
+
visitUrl: data.url ?? '',
|
|
187
|
+
sessionId: data.sessionId ?? '',
|
|
188
|
+
ts: data.ts ?? new Date().toISOString(),
|
|
189
|
+
};
|
|
190
|
+
// Use dynamic import to avoid circular dependency
|
|
191
|
+
Promise.resolve().then(async () => {
|
|
192
|
+
const https = await import('https');
|
|
193
|
+
const body = JSON.stringify({ ...logPayload, habiKey });
|
|
194
|
+
const opts: import('https').RequestOptions = {
|
|
195
|
+
hostname: 'dokcixjitwihtbdlkbdb.supabase.co',
|
|
196
|
+
port: 443,
|
|
197
|
+
path: '/functions/v1/log-ai-activity',
|
|
198
|
+
method: 'POST',
|
|
199
|
+
headers: {
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
'Content-Length': String(Buffer.byteLength(body)),
|
|
202
|
+
'x-habi-key': habiKey,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
const r = https.default.request(opts, (res2) => { res2.resume(); });
|
|
206
|
+
r.on('error', () => {});
|
|
207
|
+
r.write(body); r.end();
|
|
208
|
+
}).catch(() => {});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
json(res, 200, { ok: true, logged: true, tool: toolName, hw: hw.fingerprint });
|
|
212
|
+
} catch (err) {
|
|
213
|
+
json(res, 400, { ok: false, error: String(err) });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
162
216
|
private getHardware(): HardwareIdentity {
|
|
163
217
|
if (!this.hardware) {
|
|
164
218
|
this.hardware = readHardwareIdentity();
|
|
@@ -221,6 +275,7 @@ export class HabiLocalServer {
|
|
|
221
275
|
if (url === '/verify' && method === 'POST') return this.handleVerify(res);
|
|
222
276
|
if (url === '/assert-context' && method === 'POST') return this.handleAssertContext(req, res);
|
|
223
277
|
if (url === '/audit' && method === 'POST') return this.handleAudit(req, res);
|
|
278
|
+
if (url === '/log-activity' && method === 'POST') return this.handleLogActivity(req, res);
|
|
224
279
|
|
|
225
280
|
json(res, 404, { error: `Unknown route: ${method} ${url}` });
|
|
226
281
|
});
|
package/src/service.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HABI Service Manager
|
|
3
|
+
* Installs/uninstalls the daemon as a persistent background service
|
|
4
|
+
* per operating system — no admin required on any platform.
|
|
5
|
+
*
|
|
6
|
+
* Windows : Task Scheduler (schtasks) — runs at logon, no UAC prompt
|
|
7
|
+
* macOS : launchd LaunchAgent — ~/Library/LaunchAgents/
|
|
8
|
+
* Linux : systemd user service — ~/.config/systemd/user/
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import { execSync, spawnSync } from 'child_process';
|
|
15
|
+
|
|
16
|
+
const SERVICE_NAME = 'com.onspot.habi';
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.habi');
|
|
18
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
19
|
+
const DAEMON_PATH = path.join(__dirname, 'daemon.js'); // resolved at runtime
|
|
20
|
+
const NODE_BIN = process.execPath; // exact node binary used right now
|
|
21
|
+
const PID_PATH = path.join(CONFIG_DIR, 'habi.pid');
|
|
22
|
+
|
|
23
|
+
// ── Config helpers ─────────────────────────────────────────────────────────
|
|
24
|
+
export function saveConfig(key: string, name: string): void {
|
|
25
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify({ key, name, nodeBin: NODE_BIN, daemonPath: DAEMON_PATH }, null, 2));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function configExists(): boolean {
|
|
30
|
+
return fs.existsSync(CONFIG_PATH);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ── Install ────────────────────────────────────────────────────────────────
|
|
34
|
+
export async function installService(): Promise<void> {
|
|
35
|
+
const platform = os.platform();
|
|
36
|
+
if (platform === 'win32') await installWindows();
|
|
37
|
+
else if (platform === 'darwin') await installMacos();
|
|
38
|
+
else await installLinux();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Uninstall ──────────────────────────────────────────────────────────────
|
|
42
|
+
export async function uninstallService(): Promise<void> {
|
|
43
|
+
const platform = os.platform();
|
|
44
|
+
if (platform === 'win32') uninstallWindows();
|
|
45
|
+
else if (platform === 'darwin') uninstallMacos();
|
|
46
|
+
else uninstallLinux();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Stop ───────────────────────────────────────────────────────────────────
|
|
50
|
+
export function stopService(): void {
|
|
51
|
+
const platform = os.platform();
|
|
52
|
+
try {
|
|
53
|
+
if (platform === 'win32') {
|
|
54
|
+
try { execSync(`schtasks /end /tn "${SERVICE_NAME}" > nul 2>&1`); } catch {}
|
|
55
|
+
} else if (platform === 'darwin') {
|
|
56
|
+
spawnSync('sh', ['-c', `launchctl unload ~/Library/LaunchAgents/${SERVICE_NAME}.plist 2>/dev/null || true`]);
|
|
57
|
+
} else {
|
|
58
|
+
spawnSync('sh', ['-c', 'systemctl --user stop habi 2>/dev/null || true']);
|
|
59
|
+
}
|
|
60
|
+
} catch { /* already stopped */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Restart ────────────────────────────────────────────────────────────────
|
|
64
|
+
export function restartService(): void {
|
|
65
|
+
stopService();
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
const platform = os.platform();
|
|
68
|
+
try {
|
|
69
|
+
if (platform === 'win32') {
|
|
70
|
+
try { execSync(`schtasks /run /tn "${SERVICE_NAME}" > nul 2>&1`); } catch {}
|
|
71
|
+
} else if (platform === 'darwin') {
|
|
72
|
+
spawnSync('sh', ['-c', `launchctl load ~/Library/LaunchAgents/${SERVICE_NAME}.plist 2>/dev/null || true`]);
|
|
73
|
+
} else {
|
|
74
|
+
spawnSync('sh', ['-c', 'systemctl --user start habi 2>/dev/null || true']);
|
|
75
|
+
}
|
|
76
|
+
} catch { /* ignore */ }
|
|
77
|
+
}, 1500);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Service running check ──────────────────────────────────────────────────
|
|
81
|
+
export function isServiceInstalled(): boolean {
|
|
82
|
+
const platform = os.platform();
|
|
83
|
+
try {
|
|
84
|
+
if (platform === 'win32') {
|
|
85
|
+
const r = spawnSync('schtasks', ['/query', '/tn', SERVICE_NAME], { encoding: 'utf8', stdio: ['ignore','pipe','pipe'] });
|
|
86
|
+
return r.status === 0;
|
|
87
|
+
} else if (platform === 'darwin') {
|
|
88
|
+
return fs.existsSync(`${os.homedir()}/Library/LaunchAgents/${SERVICE_NAME}.plist`);
|
|
89
|
+
} else {
|
|
90
|
+
return fs.existsSync(`${os.homedir()}/.config/systemd/user/habi.service`);
|
|
91
|
+
}
|
|
92
|
+
} catch { return false; }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Windows: Task Scheduler ─────────────────────────────────────────────────
|
|
96
|
+
async function installWindows(): Promise<void> {
|
|
97
|
+
// Wrap in a VBScript launcher so no console window flashes
|
|
98
|
+
const launcherPath = path.join(CONFIG_DIR, 'launch.vbs');
|
|
99
|
+
const vbsContent = `Set WshShell = CreateObject("WScript.Shell")\r\nWshShell.Run """${NODE_BIN}"" ""${DAEMON_PATH}""", 0, False\r\n`;
|
|
100
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
101
|
+
fs.writeFileSync(launcherPath, vbsContent);
|
|
102
|
+
|
|
103
|
+
// Delete existing task silently, then create fresh
|
|
104
|
+
spawnSync('schtasks', ['/delete', '/tn', SERVICE_NAME, '/f'], { stdio: ['ignore','ignore','ignore'] });
|
|
105
|
+
|
|
106
|
+
const result = spawnSync('schtasks', [
|
|
107
|
+
'/create',
|
|
108
|
+
'/tn', SERVICE_NAME,
|
|
109
|
+
'/tr', `wscript "${launcherPath}"`,
|
|
110
|
+
'/sc', 'onlogon',
|
|
111
|
+
'/rl', 'limited', // no UAC elevation needed
|
|
112
|
+
'/f', // force overwrite
|
|
113
|
+
'/delay', '0000:30', // 30s delay after logon (let desktop load first)
|
|
114
|
+
], { encoding: 'utf8' });
|
|
115
|
+
|
|
116
|
+
if (result.status !== 0) {
|
|
117
|
+
throw new Error(`Task Scheduler error: ${result.stderr || result.stdout}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Run it now without waiting for next logon
|
|
121
|
+
spawnSync('wscript', [launcherPath]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function uninstallWindows(): void {
|
|
125
|
+
spawnSync('schtasks', ['/delete', '/tn', SERVICE_NAME, '/f'], { stdio: ['ignore','ignore','ignore'] });
|
|
126
|
+
const launcherPath = path.join(CONFIG_DIR, 'launch.vbs');
|
|
127
|
+
if (fs.existsSync(launcherPath)) fs.unlinkSync(launcherPath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── macOS: launchd LaunchAgent ──────────────────────────────────────────────
|
|
131
|
+
async function installMacos(): Promise<void> {
|
|
132
|
+
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
133
|
+
const plistPath = path.join(plistDir, `${SERVICE_NAME}.plist`);
|
|
134
|
+
const logPath = path.join(CONFIG_DIR, 'habi.log');
|
|
135
|
+
|
|
136
|
+
if (!fs.existsSync(plistDir)) fs.mkdirSync(plistDir, { recursive: true });
|
|
137
|
+
|
|
138
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
139
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
140
|
+
<plist version="1.0">
|
|
141
|
+
<dict>
|
|
142
|
+
<key>Label</key> <string>${SERVICE_NAME}</string>
|
|
143
|
+
<key>ProgramArguments</key>
|
|
144
|
+
<array>
|
|
145
|
+
<string>${NODE_BIN}</string>
|
|
146
|
+
<string>${DAEMON_PATH}</string>
|
|
147
|
+
</array>
|
|
148
|
+
<key>RunAtLoad</key> <true/>
|
|
149
|
+
<key>KeepAlive</key> <true/>
|
|
150
|
+
<key>StandardOutPath</key> <string>${logPath}</string>
|
|
151
|
+
<key>StandardErrorPath</key> <string>${logPath}</string>
|
|
152
|
+
<key>WorkingDirectory</key> <string>${CONFIG_DIR}</string>
|
|
153
|
+
</dict>
|
|
154
|
+
</plist>`;
|
|
155
|
+
|
|
156
|
+
fs.writeFileSync(plistPath, plist);
|
|
157
|
+
|
|
158
|
+
// Unload if already loaded, then load fresh
|
|
159
|
+
spawnSync('launchctl', ['unload', plistPath], { stdio: ['ignore','ignore','ignore'] as any });
|
|
160
|
+
const result = spawnSync('launchctl', ['load', '-w', plistPath], { encoding: 'utf8' });
|
|
161
|
+
if (result.status !== 0) {
|
|
162
|
+
throw new Error(`launchctl error: ${result.stderr}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function uninstallMacos(): void {
|
|
167
|
+
const plistPath = path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_NAME}.plist`);
|
|
168
|
+
if (fs.existsSync(plistPath)) {
|
|
169
|
+
spawnSync('launchctl', ['unload', plistPath], { stdio: ['ignore','ignore','ignore'] as any });
|
|
170
|
+
fs.unlinkSync(plistPath);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Linux: systemd user service ─────────────────────────────────────────────
|
|
175
|
+
async function installLinux(): Promise<void> {
|
|
176
|
+
const unitDir = path.join(os.homedir(), '.config', 'systemd', 'user');
|
|
177
|
+
const unitPath = path.join(unitDir, 'habi.service');
|
|
178
|
+
const logPath = path.join(CONFIG_DIR, 'habi.log');
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(unitDir)) fs.mkdirSync(unitDir, { recursive: true });
|
|
181
|
+
|
|
182
|
+
const unit = `[Unit]
|
|
183
|
+
Description=HABI Hardware Identity Agent
|
|
184
|
+
After=network.target
|
|
185
|
+
|
|
186
|
+
[Service]
|
|
187
|
+
ExecStart=${NODE_BIN} ${DAEMON_PATH}
|
|
188
|
+
Restart=always
|
|
189
|
+
RestartSec=10
|
|
190
|
+
WorkingDirectory=${CONFIG_DIR}
|
|
191
|
+
StandardOutput=append:${logPath}
|
|
192
|
+
StandardError=append:${logPath}
|
|
193
|
+
Environment=HOME=${os.homedir()}
|
|
194
|
+
|
|
195
|
+
[Install]
|
|
196
|
+
WantedBy=default.target
|
|
197
|
+
`;
|
|
198
|
+
|
|
199
|
+
fs.writeFileSync(unitPath, unit);
|
|
200
|
+
|
|
201
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: ['ignore','ignore','ignore'] as any });
|
|
202
|
+
spawnSync('systemctl', ['--user', 'enable', 'habi'], { stdio: ['ignore','ignore','ignore'] as any });
|
|
203
|
+
const result = spawnSync('systemctl', ['--user', 'start', 'habi'], { encoding: 'utf8' });
|
|
204
|
+
|
|
205
|
+
if (result.status !== 0) {
|
|
206
|
+
// systemd might not be running (WSL, older systems) — fall back to backgrounding
|
|
207
|
+
console.log(' (systemd not available — starting daemon directly)');
|
|
208
|
+
const { spawn } = require('child_process');
|
|
209
|
+
const child = spawn(NODE_BIN, [DAEMON_PATH], {
|
|
210
|
+
detached: true,
|
|
211
|
+
stdio: ['ignore', fs.openSync(logPath, 'a'), fs.openSync(logPath, 'a')],
|
|
212
|
+
});
|
|
213
|
+
child.unref();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function uninstallLinux(): void {
|
|
218
|
+
spawnSync('systemctl', ['--user', 'stop', 'habi'], { stdio: ['ignore','ignore','ignore'] as any });
|
|
219
|
+
spawnSync('systemctl', ['--user', 'disable', 'habi'], { stdio: ['ignore','ignore','ignore'] as any });
|
|
220
|
+
const unitPath = path.join(os.homedir(), '.config', 'systemd', 'user', 'habi.service');
|
|
221
|
+
if (fs.existsSync(unitPath)) fs.unlinkSync(unitPath);
|
|
222
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: ['ignore','ignore','ignore'] as any });
|
|
223
|
+
}
|