skillo 0.2.5 → 0.2.8
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 +198 -198
- package/dist/api-client-BF6GDR7Q.js +12 -0
- package/dist/{chunk-WJKZWKER.js → chunk-63FVALWX.js} +1 -1
- package/dist/chunk-63FVALWX.js.map +1 -0
- package/dist/chunk-6GOJPFZ7.js +113 -0
- package/dist/chunk-6GOJPFZ7.js.map +1 -0
- package/dist/chunk-6UGTWBUW.js +89 -0
- package/dist/chunk-6UGTWBUW.js.map +1 -0
- package/dist/{chunk-2CVEPT6U.js → chunk-73NUWYUO.js} +2 -2
- package/dist/chunk-73NUWYUO.js.map +1 -0
- package/dist/chunk-QIV4VIXA.js +221 -0
- package/dist/chunk-QIV4VIXA.js.map +1 -0
- package/dist/{chunk-CPL3P2OF.js → chunk-QUXHHRRK.js} +2 -2
- package/dist/chunk-QUXHHRRK.js.map +1 -0
- package/dist/{chunk-ODOZM4QV.js → chunk-RQ2SC5HW.js} +72 -10
- package/dist/chunk-RQ2SC5HW.js.map +1 -0
- package/dist/chunk-U53QWUOR.js +727 -0
- package/dist/chunk-U53QWUOR.js.map +1 -0
- package/dist/chunk-VAQ73XPE.js +68 -0
- package/dist/chunk-VAQ73XPE.js.map +1 -0
- package/dist/chunk-XLJGCOVT.js +975 -0
- package/dist/chunk-XLJGCOVT.js.map +1 -0
- package/dist/{claude-watcher-N6GN6WHJ.js → claude-watcher-WKGBJYKN.js} +65 -3
- package/dist/claude-watcher-WKGBJYKN.js.map +1 -0
- package/dist/cli.js +499 -1807
- package/dist/cli.js.map +1 -1
- package/dist/{config-P5EM5L7N.js → config-ZOKAP2LJ.js} +3 -3
- package/dist/daemon-6DTCMOJB.js +28 -0
- package/dist/daemon-runner.js +352 -65
- package/dist/daemon-runner.js.map +1 -1
- package/dist/database-KQY5OSCS.js +9 -0
- package/dist/git-OGUSYBJS.js +16 -0
- package/dist/git-OGUSYBJS.js.map +1 -0
- package/dist/git-OUAHIOY2.js +110 -0
- package/dist/git-OUAHIOY2.js.map +1 -0
- package/dist/index.js.map +1 -1
- package/dist/{paths-INOKEM66.js → paths-MPOZBOKE.js} +2 -2
- package/dist/paths-MPOZBOKE.js.map +1 -0
- package/dist/project-OFU2W6MH.js +19 -0
- package/dist/project-OFU2W6MH.js.map +1 -0
- package/dist/shell-NZABRJLA.js +16 -0
- package/dist/shell-NZABRJLA.js.map +1 -0
- package/dist/skill-installer-F67OAOQN.js +121 -0
- package/dist/skill-installer-F67OAOQN.js.map +1 -0
- package/dist/{skill-usage-detector-EO26MRYV.js → skill-usage-detector-MSW5VWQZ.js} +2 -2
- package/dist/skill-usage-detector-MSW5VWQZ.js.map +1 -0
- package/dist/{tray-YOL4R2RH.js → tray-WKFGUUTO.js} +117 -179
- package/dist/tray-WKFGUUTO.js.map +1 -0
- package/package.json +62 -63
- package/scripts/postinstall.mjs +415 -364
- package/scripts/tray-helper-darwin +0 -0
- package/scripts/tray-helper-darwin.swift +180 -180
- package/scripts/tray-helper-linux.py +322 -0
- package/scripts/tray-helper-windows.cs +322 -0
- package/dist/api-client-KUQW7FSC.js +0 -12
- package/dist/chunk-2CVEPT6U.js.map +0 -1
- package/dist/chunk-CPL3P2OF.js.map +0 -1
- package/dist/chunk-ODOZM4QV.js.map +0 -1
- package/dist/chunk-WJKZWKER.js.map +0 -1
- package/dist/claude-watcher-N6GN6WHJ.js.map +0 -1
- package/dist/database-F3BFFZKG.js +0 -9
- package/dist/skill-usage-detector-EO26MRYV.js.map +0 -1
- package/dist/tray-YOL4R2RH.js.map +0 -1
- /package/dist/{api-client-KUQW7FSC.js.map → api-client-BF6GDR7Q.js.map} +0 -0
- /package/dist/{config-P5EM5L7N.js.map → config-ZOKAP2LJ.js.map} +0 -0
- /package/dist/{database-F3BFFZKG.js.map → daemon-6DTCMOJB.js.map} +0 -0
- /package/dist/{paths-INOKEM66.js.map → database-KQY5OSCS.js.map} +0 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
logger_default
|
|
4
|
+
} from "./chunk-VAQ73XPE.js";
|
|
5
|
+
import {
|
|
6
|
+
StatusWriter
|
|
7
|
+
} from "./chunk-6UGTWBUW.js";
|
|
8
|
+
import {
|
|
9
|
+
loadConfig
|
|
10
|
+
} from "./chunk-QUXHHRRK.js";
|
|
11
|
+
import {
|
|
12
|
+
ensureDirectory,
|
|
13
|
+
getActiveSessionsDir,
|
|
14
|
+
getConfigFile,
|
|
15
|
+
getDataDir,
|
|
16
|
+
getLogFile,
|
|
17
|
+
getPidFile,
|
|
18
|
+
getTrackedProjectsCacheFile
|
|
19
|
+
} from "./chunk-63FVALWX.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/daemon.ts
|
|
22
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
23
|
+
import { fork, spawn } from "child_process";
|
|
24
|
+
import { join as join2 } from "path";
|
|
25
|
+
import { Command } from "commander";
|
|
26
|
+
|
|
27
|
+
// src/utils/os-service.ts
|
|
28
|
+
import { existsSync, writeFileSync, unlinkSync, mkdirSync, readdirSync } from "fs";
|
|
29
|
+
import { join } from "path";
|
|
30
|
+
import { homedir, platform } from "os";
|
|
31
|
+
import { execSync } from "child_process";
|
|
32
|
+
var DAEMON_LABEL = "one.skillo.daemon";
|
|
33
|
+
var TRAY_LABEL = "one.skillo.tray";
|
|
34
|
+
var WIN_REG_KEY = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
35
|
+
var WIN_DAEMON_VALUE = "SkilloDaemon";
|
|
36
|
+
var WIN_TRAY_VALUE = "SkilloTray";
|
|
37
|
+
var isWin = platform() === "win32";
|
|
38
|
+
var pathSep = isWin ? ";" : ":";
|
|
39
|
+
function getLaunchAgentsDir() {
|
|
40
|
+
return join(homedir(), "Library", "LaunchAgents");
|
|
41
|
+
}
|
|
42
|
+
function getSystemdUserDir() {
|
|
43
|
+
return join(homedir(), ".config", "systemd", "user");
|
|
44
|
+
}
|
|
45
|
+
function getDaemonPlistPath() {
|
|
46
|
+
return join(getLaunchAgentsDir(), `${DAEMON_LABEL}.plist`);
|
|
47
|
+
}
|
|
48
|
+
function getTrayPlistPath() {
|
|
49
|
+
return join(getLaunchAgentsDir(), `${TRAY_LABEL}.plist`);
|
|
50
|
+
}
|
|
51
|
+
function getDaemonServicePath() {
|
|
52
|
+
return join(getSystemdUserDir(), "skillo-daemon.service");
|
|
53
|
+
}
|
|
54
|
+
function getTrayServicePath() {
|
|
55
|
+
return join(getSystemdUserDir(), "skillo-tray.service");
|
|
56
|
+
}
|
|
57
|
+
function getSkilloDataDir() {
|
|
58
|
+
return join(homedir(), ".skillo");
|
|
59
|
+
}
|
|
60
|
+
function getWinDaemonVbsPath() {
|
|
61
|
+
return join(getSkilloDataDir(), "skillo-daemon.vbs");
|
|
62
|
+
}
|
|
63
|
+
function findSkilloBin() {
|
|
64
|
+
try {
|
|
65
|
+
const whichCmd = isWin ? "where skillo" : "which skillo";
|
|
66
|
+
const result = execSync(whichCmd, { encoding: "utf-8" }).trim();
|
|
67
|
+
const firstLine = result.split(/\r?\n/)[0].trim();
|
|
68
|
+
if (firstLine) return firstLine;
|
|
69
|
+
} catch {
|
|
70
|
+
}
|
|
71
|
+
if (isWin) {
|
|
72
|
+
const appData = process.env.APPDATA || join(homedir(), "AppData", "Roaming");
|
|
73
|
+
const candidates = [
|
|
74
|
+
join(appData, "npm", "skillo.cmd"),
|
|
75
|
+
join(homedir(), "AppData", "Local", "fnm_multishells")
|
|
76
|
+
];
|
|
77
|
+
for (const c of candidates) {
|
|
78
|
+
if (existsSync(c)) return c;
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
const candidates = [
|
|
82
|
+
"/usr/local/bin/skillo",
|
|
83
|
+
"/usr/bin/skillo"
|
|
84
|
+
];
|
|
85
|
+
for (const c of candidates) {
|
|
86
|
+
if (existsSync(c)) return c;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return "skillo";
|
|
90
|
+
}
|
|
91
|
+
function buildPath() {
|
|
92
|
+
const paths = /* @__PURE__ */ new Set();
|
|
93
|
+
const home = homedir();
|
|
94
|
+
(process.env.PATH || "").split(pathSep).forEach((p) => paths.add(p));
|
|
95
|
+
if (isWin) {
|
|
96
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
97
|
+
paths.add(join(appData, "npm"));
|
|
98
|
+
paths.add(join(home, "AppData", "Local", "Programs", "nodejs"));
|
|
99
|
+
const nvmHome = process.env.NVM_HOME;
|
|
100
|
+
if (nvmHome) paths.add(nvmHome);
|
|
101
|
+
const nvmSymlink = process.env.NVM_SYMLINK;
|
|
102
|
+
if (nvmSymlink) paths.add(nvmSymlink);
|
|
103
|
+
const fnmDir = join(home, ".fnm");
|
|
104
|
+
if (existsSync(fnmDir)) paths.add(fnmDir);
|
|
105
|
+
} else {
|
|
106
|
+
const nvmDir = process.env.NVM_DIR || join(home, ".nvm");
|
|
107
|
+
if (existsSync(nvmDir)) {
|
|
108
|
+
try {
|
|
109
|
+
const defaultAlias = join(nvmDir, "alias", "default");
|
|
110
|
+
if (existsSync(defaultAlias)) {
|
|
111
|
+
const versionsDir = join(nvmDir, "versions", "node");
|
|
112
|
+
if (existsSync(versionsDir)) {
|
|
113
|
+
const versions = readdirSync(versionsDir);
|
|
114
|
+
for (const v of versions) {
|
|
115
|
+
paths.add(join(versionsDir, v, "bin"));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
paths.add(join(home, ".fnm", "current", "bin"));
|
|
123
|
+
paths.add("/opt/homebrew/bin");
|
|
124
|
+
paths.add("/usr/local/bin");
|
|
125
|
+
paths.add("/usr/bin");
|
|
126
|
+
paths.add("/bin");
|
|
127
|
+
paths.add(join(home, ".npm-global", "bin"));
|
|
128
|
+
paths.add(join(home, ".local", "bin"));
|
|
129
|
+
}
|
|
130
|
+
return [...paths].filter(Boolean).join(pathSep);
|
|
131
|
+
}
|
|
132
|
+
function generateDaemonPlist(skilloBin, envPath) {
|
|
133
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
134
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
135
|
+
<plist version="1.0">
|
|
136
|
+
<dict>
|
|
137
|
+
<key>Label</key>
|
|
138
|
+
<string>${DAEMON_LABEL}</string>
|
|
139
|
+
<key>ProgramArguments</key>
|
|
140
|
+
<array>
|
|
141
|
+
<string>${skilloBin}</string>
|
|
142
|
+
<string>start</string>
|
|
143
|
+
<string>--foreground</string>
|
|
144
|
+
</array>
|
|
145
|
+
<key>RunAtLoad</key>
|
|
146
|
+
<true/>
|
|
147
|
+
<key>KeepAlive</key>
|
|
148
|
+
<dict>
|
|
149
|
+
<key>SuccessfulExit</key>
|
|
150
|
+
<false/>
|
|
151
|
+
</dict>
|
|
152
|
+
<key>ThrottleInterval</key>
|
|
153
|
+
<integer>10</integer>
|
|
154
|
+
<key>EnvironmentVariables</key>
|
|
155
|
+
<dict>
|
|
156
|
+
<key>PATH</key>
|
|
157
|
+
<string>${envPath}</string>
|
|
158
|
+
<key>HOME</key>
|
|
159
|
+
<string>${homedir()}</string>
|
|
160
|
+
</dict>
|
|
161
|
+
<key>StandardOutPath</key>
|
|
162
|
+
<string>${join(homedir(), ".skillo", "launchd-stdout.log")}</string>
|
|
163
|
+
<key>StandardErrorPath</key>
|
|
164
|
+
<string>${join(homedir(), ".skillo", "launchd-stderr.log")}</string>
|
|
165
|
+
</dict>
|
|
166
|
+
</plist>`;
|
|
167
|
+
}
|
|
168
|
+
function generateTrayPlist(skilloBin, envPath) {
|
|
169
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
170
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
171
|
+
<plist version="1.0">
|
|
172
|
+
<dict>
|
|
173
|
+
<key>Label</key>
|
|
174
|
+
<string>${TRAY_LABEL}</string>
|
|
175
|
+
<key>ProgramArguments</key>
|
|
176
|
+
<array>
|
|
177
|
+
<string>${skilloBin}</string>
|
|
178
|
+
<string>tray</string>
|
|
179
|
+
</array>
|
|
180
|
+
<key>RunAtLoad</key>
|
|
181
|
+
<true/>
|
|
182
|
+
<key>LimitLoadToSessionType</key>
|
|
183
|
+
<string>Aqua</string>
|
|
184
|
+
<key>KeepAlive</key>
|
|
185
|
+
<dict>
|
|
186
|
+
<key>SuccessfulExit</key>
|
|
187
|
+
<false/>
|
|
188
|
+
</dict>
|
|
189
|
+
<key>ThrottleInterval</key>
|
|
190
|
+
<integer>10</integer>
|
|
191
|
+
<key>EnvironmentVariables</key>
|
|
192
|
+
<dict>
|
|
193
|
+
<key>PATH</key>
|
|
194
|
+
<string>${envPath}</string>
|
|
195
|
+
<key>HOME</key>
|
|
196
|
+
<string>${homedir()}</string>
|
|
197
|
+
</dict>
|
|
198
|
+
<key>StandardOutPath</key>
|
|
199
|
+
<string>${join(homedir(), ".skillo", "launchd-tray-stdout.log")}</string>
|
|
200
|
+
<key>StandardErrorPath</key>
|
|
201
|
+
<string>${join(homedir(), ".skillo", "launchd-tray-stderr.log")}</string>
|
|
202
|
+
</dict>
|
|
203
|
+
</plist>`;
|
|
204
|
+
}
|
|
205
|
+
function generateDaemonUnit(skilloBin, envPath) {
|
|
206
|
+
return `[Unit]
|
|
207
|
+
Description=Skillo Daemon
|
|
208
|
+
After=network.target
|
|
209
|
+
|
|
210
|
+
[Service]
|
|
211
|
+
Type=simple
|
|
212
|
+
ExecStart=${skilloBin} start --foreground
|
|
213
|
+
Restart=on-failure
|
|
214
|
+
RestartSec=10
|
|
215
|
+
Environment=PATH=${envPath}
|
|
216
|
+
Environment=HOME=${homedir()}
|
|
217
|
+
|
|
218
|
+
[Install]
|
|
219
|
+
WantedBy=default.target
|
|
220
|
+
`;
|
|
221
|
+
}
|
|
222
|
+
function generateTrayUnit(skilloBin, envPath) {
|
|
223
|
+
return `[Unit]
|
|
224
|
+
Description=Skillo Tray Icon
|
|
225
|
+
After=graphical-session.target
|
|
226
|
+
PartOf=graphical-session.target
|
|
227
|
+
|
|
228
|
+
[Service]
|
|
229
|
+
Type=simple
|
|
230
|
+
ExecStart=${skilloBin} tray
|
|
231
|
+
Restart=on-failure
|
|
232
|
+
RestartSec=10
|
|
233
|
+
Environment=PATH=${envPath}
|
|
234
|
+
Environment=HOME=${homedir()}
|
|
235
|
+
Environment=DISPLAY=:0
|
|
236
|
+
|
|
237
|
+
[Install]
|
|
238
|
+
WantedBy=graphical-session.target
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
241
|
+
function generateDaemonVbs(skilloBin) {
|
|
242
|
+
const escaped = skilloBin.replace(/\\/g, "\\\\");
|
|
243
|
+
return `' Skillo Daemon \u2014 hidden launcher\r
|
|
244
|
+
Set WshShell = CreateObject("WScript.Shell")\r
|
|
245
|
+
WshShell.Run """${escaped}"" start --foreground", 0, False\r
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
function winRegAdd(valueName, data) {
|
|
249
|
+
execSync(
|
|
250
|
+
`reg add "${WIN_REG_KEY}" /v "${valueName}" /t REG_SZ /d "${data}" /f`,
|
|
251
|
+
{ stdio: "ignore" }
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
function winRegDelete(valueName) {
|
|
255
|
+
try {
|
|
256
|
+
execSync(
|
|
257
|
+
`reg delete "${WIN_REG_KEY}" /v "${valueName}" /f`,
|
|
258
|
+
{ stdio: "ignore" }
|
|
259
|
+
);
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function winRegExists(valueName) {
|
|
264
|
+
try {
|
|
265
|
+
execSync(`reg query "${WIN_REG_KEY}" /v "${valueName}"`, { stdio: "ignore" });
|
|
266
|
+
return true;
|
|
267
|
+
} catch {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function installService(options) {
|
|
272
|
+
const os = platform();
|
|
273
|
+
const skilloBin = findSkilloBin();
|
|
274
|
+
const envPath = buildPath();
|
|
275
|
+
const includeTray = options?.includeTray ?? true;
|
|
276
|
+
try {
|
|
277
|
+
if (os === "darwin") {
|
|
278
|
+
const agentsDir = getLaunchAgentsDir();
|
|
279
|
+
if (!existsSync(agentsDir)) mkdirSync(agentsDir, { recursive: true });
|
|
280
|
+
const daemonPlist = getDaemonPlistPath();
|
|
281
|
+
writeFileSync(daemonPlist, generateDaemonPlist(skilloBin, envPath), "utf-8");
|
|
282
|
+
try {
|
|
283
|
+
execSync(`launchctl unload "${daemonPlist}" 2>/dev/null`);
|
|
284
|
+
} catch {
|
|
285
|
+
}
|
|
286
|
+
execSync(`launchctl load "${daemonPlist}"`);
|
|
287
|
+
if (includeTray) {
|
|
288
|
+
const trayPlist = getTrayPlistPath();
|
|
289
|
+
writeFileSync(trayPlist, generateTrayPlist(skilloBin, envPath), "utf-8");
|
|
290
|
+
try {
|
|
291
|
+
execSync(`launchctl unload "${trayPlist}" 2>/dev/null`);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
execSync(`launchctl load "${trayPlist}"`);
|
|
295
|
+
}
|
|
296
|
+
return { success: true };
|
|
297
|
+
} else if (os === "linux") {
|
|
298
|
+
const systemdDir = getSystemdUserDir();
|
|
299
|
+
if (!existsSync(systemdDir)) mkdirSync(systemdDir, { recursive: true });
|
|
300
|
+
writeFileSync(getDaemonServicePath(), generateDaemonUnit(skilloBin, envPath), "utf-8");
|
|
301
|
+
execSync("systemctl --user daemon-reload");
|
|
302
|
+
execSync("systemctl --user enable skillo-daemon.service");
|
|
303
|
+
execSync("systemctl --user start skillo-daemon.service");
|
|
304
|
+
if (includeTray) {
|
|
305
|
+
writeFileSync(getTrayServicePath(), generateTrayUnit(skilloBin, envPath), "utf-8");
|
|
306
|
+
execSync("systemctl --user daemon-reload");
|
|
307
|
+
execSync("systemctl --user enable skillo-tray.service");
|
|
308
|
+
try {
|
|
309
|
+
execSync("systemctl --user start skillo-tray.service");
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return { success: true };
|
|
314
|
+
} else if (os === "win32") {
|
|
315
|
+
const dataDir = getSkilloDataDir();
|
|
316
|
+
if (!existsSync(dataDir)) mkdirSync(dataDir, { recursive: true });
|
|
317
|
+
const vbsPath = getWinDaemonVbsPath();
|
|
318
|
+
writeFileSync(vbsPath, generateDaemonVbs(skilloBin), "utf-8");
|
|
319
|
+
winRegAdd(WIN_DAEMON_VALUE, `wscript.exe "${vbsPath}"`);
|
|
320
|
+
if (includeTray) {
|
|
321
|
+
winRegAdd(WIN_TRAY_VALUE, `"${skilloBin}" tray`);
|
|
322
|
+
}
|
|
323
|
+
return { success: true };
|
|
324
|
+
} else {
|
|
325
|
+
return { success: false, error: `Unsupported platform: ${os}. Auto-start is available on macOS, Linux, and Windows.` };
|
|
326
|
+
}
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function uninstallService() {
|
|
332
|
+
const os = platform();
|
|
333
|
+
try {
|
|
334
|
+
if (os === "darwin") {
|
|
335
|
+
const daemonPlist = getDaemonPlistPath();
|
|
336
|
+
if (existsSync(daemonPlist)) {
|
|
337
|
+
try {
|
|
338
|
+
execSync(`launchctl unload "${daemonPlist}" 2>/dev/null`);
|
|
339
|
+
} catch {
|
|
340
|
+
}
|
|
341
|
+
unlinkSync(daemonPlist);
|
|
342
|
+
}
|
|
343
|
+
const trayPlist = getTrayPlistPath();
|
|
344
|
+
if (existsSync(trayPlist)) {
|
|
345
|
+
try {
|
|
346
|
+
execSync(`launchctl unload "${trayPlist}" 2>/dev/null`);
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
unlinkSync(trayPlist);
|
|
350
|
+
}
|
|
351
|
+
return { success: true };
|
|
352
|
+
} else if (os === "linux") {
|
|
353
|
+
const daemonService = getDaemonServicePath();
|
|
354
|
+
if (existsSync(daemonService)) {
|
|
355
|
+
try {
|
|
356
|
+
execSync("systemctl --user stop skillo-daemon.service 2>/dev/null");
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
execSync("systemctl --user disable skillo-daemon.service 2>/dev/null");
|
|
361
|
+
} catch {
|
|
362
|
+
}
|
|
363
|
+
unlinkSync(daemonService);
|
|
364
|
+
}
|
|
365
|
+
const trayService = getTrayServicePath();
|
|
366
|
+
if (existsSync(trayService)) {
|
|
367
|
+
try {
|
|
368
|
+
execSync("systemctl --user stop skillo-tray.service 2>/dev/null");
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
execSync("systemctl --user disable skillo-tray.service 2>/dev/null");
|
|
373
|
+
} catch {
|
|
374
|
+
}
|
|
375
|
+
unlinkSync(trayService);
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
execSync("systemctl --user daemon-reload");
|
|
379
|
+
} catch {
|
|
380
|
+
}
|
|
381
|
+
return { success: true };
|
|
382
|
+
} else if (os === "win32") {
|
|
383
|
+
winRegDelete(WIN_DAEMON_VALUE);
|
|
384
|
+
winRegDelete(WIN_TRAY_VALUE);
|
|
385
|
+
const vbsPath = getWinDaemonVbsPath();
|
|
386
|
+
if (existsSync(vbsPath)) {
|
|
387
|
+
try {
|
|
388
|
+
unlinkSync(vbsPath);
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return { success: true };
|
|
393
|
+
} else {
|
|
394
|
+
return { success: false, error: `Unsupported platform: ${os}` };
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function getServiceStatus() {
|
|
401
|
+
const os = platform();
|
|
402
|
+
if (os === "darwin") {
|
|
403
|
+
const daemonInstalled = existsSync(getDaemonPlistPath());
|
|
404
|
+
const trayInstalled = existsSync(getTrayPlistPath());
|
|
405
|
+
let daemonLoaded = false;
|
|
406
|
+
let trayLoaded = false;
|
|
407
|
+
try {
|
|
408
|
+
const output = execSync("launchctl list", { encoding: "utf-8" });
|
|
409
|
+
daemonLoaded = output.includes(DAEMON_LABEL);
|
|
410
|
+
trayLoaded = output.includes(TRAY_LABEL);
|
|
411
|
+
} catch {
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
platform: "macos",
|
|
415
|
+
daemon: { installed: daemonInstalled, loaded: daemonLoaded },
|
|
416
|
+
tray: { installed: trayInstalled, loaded: trayLoaded }
|
|
417
|
+
};
|
|
418
|
+
} else if (os === "linux") {
|
|
419
|
+
const daemonInstalled = existsSync(getDaemonServicePath());
|
|
420
|
+
const trayInstalled = existsSync(getTrayServicePath());
|
|
421
|
+
let daemonLoaded = false;
|
|
422
|
+
let trayLoaded = false;
|
|
423
|
+
try {
|
|
424
|
+
execSync("systemctl --user is-active skillo-daemon.service", { encoding: "utf-8" });
|
|
425
|
+
daemonLoaded = true;
|
|
426
|
+
} catch {
|
|
427
|
+
}
|
|
428
|
+
try {
|
|
429
|
+
execSync("systemctl --user is-active skillo-tray.service", { encoding: "utf-8" });
|
|
430
|
+
trayLoaded = true;
|
|
431
|
+
} catch {
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
platform: "linux",
|
|
435
|
+
daemon: { installed: daemonInstalled, loaded: daemonLoaded },
|
|
436
|
+
tray: { installed: trayInstalled, loaded: trayLoaded }
|
|
437
|
+
};
|
|
438
|
+
} else if (os === "win32") {
|
|
439
|
+
const daemonInstalled = winRegExists(WIN_DAEMON_VALUE);
|
|
440
|
+
const trayInstalled = winRegExists(WIN_TRAY_VALUE);
|
|
441
|
+
return {
|
|
442
|
+
platform: "windows",
|
|
443
|
+
daemon: { installed: daemonInstalled, loaded: daemonInstalled },
|
|
444
|
+
tray: { installed: trayInstalled, loaded: trayInstalled }
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
platform: "unsupported",
|
|
449
|
+
daemon: { installed: false, loaded: false },
|
|
450
|
+
tray: { installed: false, loaded: false }
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/utils/daemon-tasks.ts
|
|
455
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readdirSync as readdirSync2 } from "fs";
|
|
456
|
+
async function updateTrackedProjectsCache(client, log) {
|
|
457
|
+
try {
|
|
458
|
+
const result = await client.listProjects(true);
|
|
459
|
+
if (result.success && result.data?.projects) {
|
|
460
|
+
const projects = result.data.projects.filter((p) => p.trackingEnabled).map((p) => ({ path: p.path, name: p.name, gitRemoteNormalized: p.gitRemoteNormalized }));
|
|
461
|
+
const cacheData = { updatedAt: Date.now(), projects };
|
|
462
|
+
writeFileSync2(getTrackedProjectsCacheFile(), JSON.stringify(cacheData, null, 2));
|
|
463
|
+
log("DEBUG", `Updated tracked projects cache: ${projects.length} project(s)`);
|
|
464
|
+
return projects;
|
|
465
|
+
}
|
|
466
|
+
} catch (error) {
|
|
467
|
+
log("ERROR", `Failed to update tracked projects cache: ${error}`);
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
async function cleanupStaleSessions(client, log) {
|
|
472
|
+
const sessionsDir = getActiveSessionsDir();
|
|
473
|
+
if (!existsSync2(sessionsDir)) return;
|
|
474
|
+
try {
|
|
475
|
+
const files = readdirSync2(sessionsDir);
|
|
476
|
+
for (const file of files) {
|
|
477
|
+
if (!file.endsWith(".json")) continue;
|
|
478
|
+
const pid = parseInt(file.replace(".json", ""), 10);
|
|
479
|
+
if (isNaN(pid)) continue;
|
|
480
|
+
let isRunning = false;
|
|
481
|
+
try {
|
|
482
|
+
process.kill(pid, 0);
|
|
483
|
+
isRunning = true;
|
|
484
|
+
} catch {
|
|
485
|
+
}
|
|
486
|
+
if (!isRunning) {
|
|
487
|
+
try {
|
|
488
|
+
const sessionData = JSON.parse(readFileSync2(`${sessionsDir}/${file}`, "utf-8"));
|
|
489
|
+
if (sessionData.sessionId) {
|
|
490
|
+
await client.endSession(sessionData.sessionId);
|
|
491
|
+
log("INFO", `Ended stale session ${sessionData.sessionId.slice(0, 8)} (PID ${pid} dead)`);
|
|
492
|
+
}
|
|
493
|
+
unlinkSync2(`${sessionsDir}/${file}`);
|
|
494
|
+
} catch {
|
|
495
|
+
try {
|
|
496
|
+
unlinkSync2(`${sessionsDir}/${file}`);
|
|
497
|
+
} catch {
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
log("ERROR", `Stale session cleanup error: ${error}`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/commands/daemon.ts
|
|
508
|
+
function isDaemonRunning() {
|
|
509
|
+
const pidFile = getPidFile();
|
|
510
|
+
if (!existsSync3(pidFile)) {
|
|
511
|
+
return { running: false, pid: null };
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const pidStr = readFileSync3(pidFile, "utf-8").trim();
|
|
515
|
+
const pid = parseInt(pidStr, 10);
|
|
516
|
+
if (isNaN(pid)) {
|
|
517
|
+
return { running: false, pid: null };
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
process.kill(pid, 0);
|
|
521
|
+
return { running: true, pid };
|
|
522
|
+
} catch {
|
|
523
|
+
try {
|
|
524
|
+
unlinkSync3(pidFile);
|
|
525
|
+
} catch {
|
|
526
|
+
}
|
|
527
|
+
return { running: false, pid: null };
|
|
528
|
+
}
|
|
529
|
+
} catch {
|
|
530
|
+
return { running: false, pid: null };
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
function startDaemonProcess() {
|
|
534
|
+
const daemonScript = new URL("./daemon-runner.js", import.meta.url).pathname;
|
|
535
|
+
if (process.platform === "win32") {
|
|
536
|
+
const scriptPath = daemonScript.replace(/^\/([A-Za-z]:)/, "$1");
|
|
537
|
+
const child2 = spawn(process.execPath, [scriptPath], {
|
|
538
|
+
detached: true,
|
|
539
|
+
stdio: "ignore",
|
|
540
|
+
env: { ...process.env, SKILLO_DAEMON: "1" }
|
|
541
|
+
});
|
|
542
|
+
child2.unref();
|
|
543
|
+
if (child2.pid) {
|
|
544
|
+
writeFileSync3(getPidFile(), String(child2.pid));
|
|
545
|
+
return child2.pid;
|
|
546
|
+
}
|
|
547
|
+
return null;
|
|
548
|
+
}
|
|
549
|
+
const child = fork(daemonScript, [], {
|
|
550
|
+
detached: true,
|
|
551
|
+
stdio: "ignore",
|
|
552
|
+
env: {
|
|
553
|
+
...process.env,
|
|
554
|
+
SKILLO_DAEMON: "1"
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
child.unref();
|
|
558
|
+
if (child.pid) {
|
|
559
|
+
writeFileSync3(getPidFile(), String(child.pid));
|
|
560
|
+
return child.pid;
|
|
561
|
+
}
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
function isTrayRunning() {
|
|
565
|
+
const trayPidFile = join2(getDataDir(), "tray.pid");
|
|
566
|
+
if (!existsSync3(trayPidFile)) return false;
|
|
567
|
+
try {
|
|
568
|
+
const pid = parseInt(readFileSync3(trayPidFile, "utf-8").trim(), 10);
|
|
569
|
+
if (isNaN(pid)) return false;
|
|
570
|
+
process.kill(pid, 0);
|
|
571
|
+
return true;
|
|
572
|
+
} catch {
|
|
573
|
+
try {
|
|
574
|
+
unlinkSync3(trayPidFile);
|
|
575
|
+
} catch {
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
function startTrayProcess() {
|
|
581
|
+
if (isTrayRunning()) return;
|
|
582
|
+
const cliEntry = process.argv[1];
|
|
583
|
+
const child = spawn(process.execPath, [cliEntry, "tray"], {
|
|
584
|
+
detached: true,
|
|
585
|
+
stdio: "ignore",
|
|
586
|
+
env: { ...process.env }
|
|
587
|
+
});
|
|
588
|
+
child.unref();
|
|
589
|
+
}
|
|
590
|
+
async function ensureInitialized() {
|
|
591
|
+
if (existsSync3(getConfigFile())) return;
|
|
592
|
+
const { getDefaultConfig, saveConfig } = await import("./config-ZOKAP2LJ.js");
|
|
593
|
+
const { SkilloDatabase } = await import("./database-KQY5OSCS.js");
|
|
594
|
+
const { getConfigDir, getSkillsDir } = await import("./paths-MPOZBOKE.js");
|
|
595
|
+
ensureDirectory(getDataDir());
|
|
596
|
+
ensureDirectory(getConfigDir());
|
|
597
|
+
ensureDirectory(getSkillsDir());
|
|
598
|
+
saveConfig(getDefaultConfig());
|
|
599
|
+
const db = new SkilloDatabase();
|
|
600
|
+
await db.initialize();
|
|
601
|
+
db.close();
|
|
602
|
+
logger_default.dim("Skillo initialized.");
|
|
603
|
+
}
|
|
604
|
+
async function ensureLoggedIn() {
|
|
605
|
+
const { getApiClient } = await import("./api-client-BF6GDR7Q.js");
|
|
606
|
+
const client = getApiClient();
|
|
607
|
+
if (client.hasApiKey()) {
|
|
608
|
+
const result = await client.authenticate();
|
|
609
|
+
if (result.success) return true;
|
|
610
|
+
const err = (result.error || "").toLowerCase();
|
|
611
|
+
const isAuthFailure = err.includes("unauthorized") || err.includes("invalid") || err.includes("expired") || err.includes("revoked");
|
|
612
|
+
if (isAuthFailure) {
|
|
613
|
+
client.clearApiKey();
|
|
614
|
+
logger_default.warn("Session expired. Logging in again...");
|
|
615
|
+
} else {
|
|
616
|
+
logger_default.warn("Could not verify login (server unreachable). Continuing...");
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
logger_default.blank();
|
|
621
|
+
logger_default.info("Login required. Opening browser...");
|
|
622
|
+
logger_default.blank();
|
|
623
|
+
const { hostname } = await import("os");
|
|
624
|
+
const deviceName = `CLI on ${hostname()}`;
|
|
625
|
+
const deviceAuthResult = await client.startDeviceAuth(deviceName);
|
|
626
|
+
if (!deviceAuthResult.success || !deviceAuthResult.data) {
|
|
627
|
+
logger_default.error("Failed to start authentication: " + (deviceAuthResult.error || "Unknown error"));
|
|
628
|
+
logger_default.dim("You can also login manually: skillo login <api-key>");
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
const { code, verification_url, interval } = deviceAuthResult.data;
|
|
632
|
+
logger_default.highlight(` ${verification_url}`);
|
|
633
|
+
logger_default.blank();
|
|
634
|
+
try {
|
|
635
|
+
const { exec: execCb } = await import("child_process");
|
|
636
|
+
const { promisify } = await import("util");
|
|
637
|
+
const execAsync = promisify(execCb);
|
|
638
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? 'start ""' : "xdg-open";
|
|
639
|
+
await execAsync(`${openCmd} "${verification_url}"`);
|
|
640
|
+
logger_default.dim("Browser opened. Waiting for authorization...");
|
|
641
|
+
} catch {
|
|
642
|
+
logger_default.dim("Open the URL above in your browser to authorize.");
|
|
643
|
+
}
|
|
644
|
+
logger_default.dim("(Press Ctrl+C to cancel)");
|
|
645
|
+
const maxAttempts = 120;
|
|
646
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
647
|
+
await new Promise((r) => setTimeout(r, interval * 1e3));
|
|
648
|
+
const statusResult = await client.checkTokenStatus(code);
|
|
649
|
+
if (!statusResult.success) continue;
|
|
650
|
+
const status = statusResult.data?.status;
|
|
651
|
+
if (status === "ready") {
|
|
652
|
+
const tokenResult = await client.exchangeToken(code, deviceName);
|
|
653
|
+
if (tokenResult.success && tokenResult.data) {
|
|
654
|
+
client.saveApiKey(tokenResult.data.api_key);
|
|
655
|
+
const authResult = await client.authenticate();
|
|
656
|
+
logger_default.blank();
|
|
657
|
+
logger_default.success("Login successful!");
|
|
658
|
+
if (authResult.success && authResult.data) {
|
|
659
|
+
logger_default.info(`Welcome, ${authResult.data.user.name}!`);
|
|
660
|
+
}
|
|
661
|
+
logger_default.blank();
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
logger_default.error("Failed to complete login: " + (tokenResult.error || "Unknown error"));
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
if (status === "expired" || status === "used" || status === "not_found") {
|
|
668
|
+
logger_default.error(`Authorization ${status}. Please try again.`);
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
logger_default.error("Authorization timed out.");
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
var startCommand = new Command("start").description("Start the Skillo daemon").option("-f, --foreground", "Run in foreground (don't daemonize)").action(async (options) => {
|
|
676
|
+
await ensureInitialized();
|
|
677
|
+
if (!options.foreground) {
|
|
678
|
+
const loggedIn = await ensureLoggedIn();
|
|
679
|
+
if (!loggedIn) {
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const { running, pid } = isDaemonRunning();
|
|
684
|
+
if (running) {
|
|
685
|
+
logger_default.success(`Daemon is already running (PID: ${pid})`);
|
|
686
|
+
const serviceStatus = getServiceStatus();
|
|
687
|
+
if (!serviceStatus.daemon.installed) {
|
|
688
|
+
const serviceResult = await installService({ includeTray: true });
|
|
689
|
+
if (serviceResult.success) {
|
|
690
|
+
logger_default.dim("Auto-start service installed.");
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (!isTrayRunning()) {
|
|
694
|
+
startTrayProcess();
|
|
695
|
+
}
|
|
696
|
+
process.exit(0);
|
|
697
|
+
}
|
|
698
|
+
if (options.foreground) {
|
|
699
|
+
logger_default.info("Starting Skillo daemon in foreground...");
|
|
700
|
+
logger_default.dim("Press Ctrl+C to stop");
|
|
701
|
+
logger_default.blank();
|
|
702
|
+
await runDaemonForeground();
|
|
703
|
+
} else {
|
|
704
|
+
logger_default.info("Starting Skillo daemon...");
|
|
705
|
+
const daemonPid = startDaemonProcess();
|
|
706
|
+
if (daemonPid) {
|
|
707
|
+
logger_default.success(`Daemon started (PID: ${daemonPid})`);
|
|
708
|
+
const serviceResult = await installService({ includeTray: true });
|
|
709
|
+
if (serviceResult.success) {
|
|
710
|
+
logger_default.dim("Auto-start & tray service installed. Daemon will survive reboots.");
|
|
711
|
+
}
|
|
712
|
+
startTrayProcess();
|
|
713
|
+
logger_default.blank();
|
|
714
|
+
logger_default.dim("Use 'skillo status' to check daemon status");
|
|
715
|
+
logger_default.dim("Use 'skillo stop' to stop the daemon");
|
|
716
|
+
} else {
|
|
717
|
+
logger_default.error("Failed to start daemon");
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
process.exit(0);
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
var stopCommand = new Command("stop").description("Stop the Skillo daemon").action(async () => {
|
|
724
|
+
const { running, pid } = isDaemonRunning();
|
|
725
|
+
if (!running) {
|
|
726
|
+
logger_default.dim("Daemon is not running.");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
logger_default.info(`Stopping daemon (PID: ${pid})...`);
|
|
730
|
+
const trayPidFile = join2(getDataDir(), "tray.pid");
|
|
731
|
+
if (existsSync3(trayPidFile)) {
|
|
732
|
+
try {
|
|
733
|
+
const trayPid = parseInt(readFileSync3(trayPidFile, "utf-8").trim(), 10);
|
|
734
|
+
process.kill(trayPid, "SIGTERM");
|
|
735
|
+
logger_default.dim("Tray icon stopped.");
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
try {
|
|
739
|
+
unlinkSync3(trayPidFile);
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
try {
|
|
744
|
+
process.kill(pid, "SIGTERM");
|
|
745
|
+
logger_default.success("Daemon stopped.");
|
|
746
|
+
logger_default.dim("Auto-start services are still installed. Daemon will restart on login.");
|
|
747
|
+
logger_default.dim("To remove auto-start: skillo service uninstall");
|
|
748
|
+
} catch (error) {
|
|
749
|
+
if (error.code === "ESRCH") {
|
|
750
|
+
logger_default.dim("Daemon already stopped");
|
|
751
|
+
} else if (error.code === "EPERM") {
|
|
752
|
+
logger_default.error("Permission denied. Try with sudo.");
|
|
753
|
+
process.exit(1);
|
|
754
|
+
} else {
|
|
755
|
+
throw error;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const pidFile = getPidFile();
|
|
759
|
+
if (existsSync3(pidFile)) {
|
|
760
|
+
try {
|
|
761
|
+
unlinkSync3(pidFile);
|
|
762
|
+
} catch {
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
var logsCommand = new Command("logs").description("Show daemon logs").option("-n, --lines <number>", "Number of lines to show", "50").option("-f, --follow", "Follow log output").action(async (options) => {
|
|
767
|
+
const logFile = getLogFile();
|
|
768
|
+
if (!existsSync3(logFile)) {
|
|
769
|
+
logger_default.dim("No logs found.");
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
const numLines = parseInt(options.lines, 10);
|
|
773
|
+
if (options.follow) {
|
|
774
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
775
|
+
const tail = spawn2("tail", ["-f", "-n", String(numLines), logFile], {
|
|
776
|
+
stdio: "inherit"
|
|
777
|
+
});
|
|
778
|
+
tail.on("error", () => {
|
|
779
|
+
logger_default.warn("Follow mode not supported on this platform.");
|
|
780
|
+
showLogs(logFile, numLines);
|
|
781
|
+
});
|
|
782
|
+
} else {
|
|
783
|
+
showLogs(logFile, numLines);
|
|
784
|
+
}
|
|
785
|
+
});
|
|
786
|
+
function showLogs(logFile, numLines) {
|
|
787
|
+
const content = readFileSync3(logFile, "utf-8");
|
|
788
|
+
const lines = content.split("\n").filter(Boolean);
|
|
789
|
+
const lastLines = lines.slice(-numLines);
|
|
790
|
+
lastLines.forEach((line) => console.log(line));
|
|
791
|
+
}
|
|
792
|
+
var serviceCommand = new Command("service").description("Manage auto-start service").addCommand(
|
|
793
|
+
new Command("install").description("Install auto-start service (LaunchAgent on macOS, systemd on Linux)").option("--no-tray", "Skip installing tray icon service").action(async (options) => {
|
|
794
|
+
logger_default.info("Installing auto-start service...");
|
|
795
|
+
const result = await installService({ includeTray: options.tray !== false });
|
|
796
|
+
if (result.success) {
|
|
797
|
+
logger_default.success("Auto-start service installed.");
|
|
798
|
+
logger_default.dim("Daemon will start automatically on login and restart on crash.");
|
|
799
|
+
if (options.tray !== false) {
|
|
800
|
+
logger_default.dim("Tray icon will appear in GUI sessions.");
|
|
801
|
+
}
|
|
802
|
+
} else {
|
|
803
|
+
logger_default.error(`Failed to install service: ${result.error}`);
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
).addCommand(
|
|
807
|
+
new Command("uninstall").description("Remove auto-start service").action(async () => {
|
|
808
|
+
logger_default.info("Removing auto-start service...");
|
|
809
|
+
const result = await uninstallService();
|
|
810
|
+
if (result.success) {
|
|
811
|
+
logger_default.success("Auto-start service removed.");
|
|
812
|
+
} else {
|
|
813
|
+
logger_default.error(`Failed to remove service: ${result.error}`);
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
).addCommand(
|
|
817
|
+
new Command("status").description("Show auto-start service status").action(async () => {
|
|
818
|
+
const status = getServiceStatus();
|
|
819
|
+
logger_default.blank();
|
|
820
|
+
logger_default.info(`Platform: ${status.platform}`);
|
|
821
|
+
logger_default.blank();
|
|
822
|
+
if (status.daemon.installed) {
|
|
823
|
+
logger_default.running(`Daemon service: installed${status.daemon.loaded ? " & loaded" : " (not loaded)"}`);
|
|
824
|
+
} else {
|
|
825
|
+
logger_default.stopped("Daemon service: not installed");
|
|
826
|
+
}
|
|
827
|
+
if (status.tray.installed) {
|
|
828
|
+
logger_default.running(`Tray service: installed${status.tray.loaded ? " & loaded" : " (not loaded)"}`);
|
|
829
|
+
} else {
|
|
830
|
+
logger_default.stopped("Tray service: not installed");
|
|
831
|
+
}
|
|
832
|
+
logger_default.blank();
|
|
833
|
+
if (!status.daemon.installed) {
|
|
834
|
+
logger_default.dim("Run 'skillo service install' to enable auto-start.");
|
|
835
|
+
}
|
|
836
|
+
logger_default.blank();
|
|
837
|
+
})
|
|
838
|
+
);
|
|
839
|
+
async function runDaemonForeground() {
|
|
840
|
+
const pidFile = getPidFile();
|
|
841
|
+
writeFileSync3(pidFile, String(process.pid));
|
|
842
|
+
logger_default.dim(`Daemon PID: ${process.pid}`);
|
|
843
|
+
logger_default.dim(`Log file: ${getLogFile()}`);
|
|
844
|
+
logger_default.blank();
|
|
845
|
+
const statusWriter = new StatusWriter();
|
|
846
|
+
const cleanup = () => {
|
|
847
|
+
logger_default.blank();
|
|
848
|
+
logger_default.info("Shutting down daemon...");
|
|
849
|
+
statusWriter.stop();
|
|
850
|
+
if (existsSync3(pidFile)) {
|
|
851
|
+
try {
|
|
852
|
+
unlinkSync3(pidFile);
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
process.exit(0);
|
|
857
|
+
};
|
|
858
|
+
process.on("SIGINT", cleanup);
|
|
859
|
+
process.on("SIGTERM", cleanup);
|
|
860
|
+
const config = loadConfig();
|
|
861
|
+
const { getApiClient } = await import("./api-client-BF6GDR7Q.js");
|
|
862
|
+
const client = getApiClient();
|
|
863
|
+
if (!client.hasApiKey()) {
|
|
864
|
+
logger_default.error("Not logged in. Run 'skillo login' first.");
|
|
865
|
+
process.exit(1);
|
|
866
|
+
}
|
|
867
|
+
try {
|
|
868
|
+
const authResult = await client.authenticate();
|
|
869
|
+
const user = authResult.data?.user || authResult.user;
|
|
870
|
+
if (authResult.success && user) {
|
|
871
|
+
statusWriter.update({ user: user.name });
|
|
872
|
+
logger_default.dim(`Authenticated as: ${user.name}`);
|
|
873
|
+
}
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
ensureDirectory(getDataDir());
|
|
877
|
+
ensureDirectory(getActiveSessionsDir());
|
|
878
|
+
const log = (level, msg) => {
|
|
879
|
+
if (level === "ERROR") logger_default.error(msg);
|
|
880
|
+
else if (level === "WARN") logger_default.warn(msg);
|
|
881
|
+
else logger_default.dim(msg);
|
|
882
|
+
};
|
|
883
|
+
const projects = await updateTrackedProjectsCache(client, log);
|
|
884
|
+
if (projects) statusWriter.update({ trackedProjects: projects });
|
|
885
|
+
await cleanupStaleSessions(client, log);
|
|
886
|
+
const { ClaudeWatcher } = await import("./claude-watcher-WKGBJYKN.js");
|
|
887
|
+
const watchInterval = (config.daemon?.conversationCheckInterval || 5) * 1e3;
|
|
888
|
+
const watcher = new ClaudeWatcher(client, {
|
|
889
|
+
intervalMs: watchInterval,
|
|
890
|
+
callbacks: {
|
|
891
|
+
onSync: (count) => {
|
|
892
|
+
logger_default.success(`Synced ${count} Claude prompt(s)`);
|
|
893
|
+
statusWriter.update({ claudeWatcher: { promptsSynced: count, lastSync: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
|
|
894
|
+
},
|
|
895
|
+
onError: (err) => {
|
|
896
|
+
logger_default.error(`Watcher error: ${err.message}`);
|
|
897
|
+
statusWriter.update({ claudeWatcher: { lastError: err.message } });
|
|
898
|
+
},
|
|
899
|
+
log: (level, msg) => {
|
|
900
|
+
if (level === "ERROR") logger_default.error(msg);
|
|
901
|
+
else if (level === "WARN") logger_default.warn(msg);
|
|
902
|
+
else logger_default.dim(msg);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
});
|
|
906
|
+
await watcher.start();
|
|
907
|
+
const { SkillUsageDetector } = await import("./skill-usage-detector-MSW5VWQZ.js");
|
|
908
|
+
const skillDetector = new SkillUsageDetector(client, {
|
|
909
|
+
intervalMs: 3e4,
|
|
910
|
+
callbacks: {
|
|
911
|
+
onDetection: (count) => {
|
|
912
|
+
logger_default.success(`Detected ${count} skill usage(s)`);
|
|
913
|
+
statusWriter.update({ skillDetector: { usagesDetected: count, lastDetection: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
|
|
914
|
+
},
|
|
915
|
+
onError: (err) => {
|
|
916
|
+
logger_default.error(`Skill detection error: ${err.message}`);
|
|
917
|
+
statusWriter.update({ skillDetector: { lastError: err.message } });
|
|
918
|
+
},
|
|
919
|
+
log: (level, msg) => {
|
|
920
|
+
if (level === "ERROR") logger_default.error(msg);
|
|
921
|
+
else if (level === "WARN") logger_default.warn(msg);
|
|
922
|
+
else logger_default.dim(msg);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
await skillDetector.start();
|
|
927
|
+
const { SkillInstaller } = await import("./skill-installer-F67OAOQN.js");
|
|
928
|
+
const skillInstaller = new SkillInstaller(client, {
|
|
929
|
+
intervalMs: 3e4,
|
|
930
|
+
callbacks: {
|
|
931
|
+
onInstall: (slug) => {
|
|
932
|
+
logger_default.success(`Installed skill: ${slug}`);
|
|
933
|
+
statusWriter.update({ skillInstaller: { lastInstall: slug, lastAction: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
|
|
934
|
+
},
|
|
935
|
+
onUninstall: (slug) => {
|
|
936
|
+
logger_default.success(`Uninstalled skill: ${slug}`);
|
|
937
|
+
statusWriter.update({ skillInstaller: { lastUninstall: slug, lastAction: (/* @__PURE__ */ new Date()).toISOString(), lastError: null } });
|
|
938
|
+
},
|
|
939
|
+
onError: (err) => {
|
|
940
|
+
logger_default.error(`Skill installer error: ${err.message}`);
|
|
941
|
+
statusWriter.update({ skillInstaller: { lastError: err.message } });
|
|
942
|
+
},
|
|
943
|
+
log: (level, msg) => {
|
|
944
|
+
if (level === "ERROR") logger_default.error(msg);
|
|
945
|
+
else if (level === "WARN") logger_default.warn(msg);
|
|
946
|
+
else logger_default.dim(msg);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
await skillInstaller.start();
|
|
951
|
+
statusWriter.start();
|
|
952
|
+
setInterval(async () => {
|
|
953
|
+
const updated = await updateTrackedProjectsCache(client, log);
|
|
954
|
+
if (updated) statusWriter.update({ trackedProjects: updated });
|
|
955
|
+
}, 6e4);
|
|
956
|
+
setInterval(() => cleanupStaleSessions(client, log), 3e5);
|
|
957
|
+
logger_default.success("Daemon is running. Watching Claude conversations and skill usage...");
|
|
958
|
+
await new Promise(() => {
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
export {
|
|
963
|
+
installService,
|
|
964
|
+
getServiceStatus,
|
|
965
|
+
isDaemonRunning,
|
|
966
|
+
startDaemonProcess,
|
|
967
|
+
isTrayRunning,
|
|
968
|
+
startTrayProcess,
|
|
969
|
+
ensureLoggedIn,
|
|
970
|
+
startCommand,
|
|
971
|
+
stopCommand,
|
|
972
|
+
logsCommand,
|
|
973
|
+
serviceCommand
|
|
974
|
+
};
|
|
975
|
+
//# sourceMappingURL=chunk-XLJGCOVT.js.map
|