copilot-hub 0.1.21 → 0.1.24
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 +21 -13
- package/apps/agent-engine/dist/config.js +35 -18
- package/apps/control-plane/dist/channels/telegram-channel.js +2 -1
- package/apps/control-plane/dist/config.js +35 -18
- package/apps/control-plane/dist/copilot-hub.js +2 -2
- package/package.json +1 -2
- package/packages/core/dist/config-paths.d.ts +11 -0
- package/packages/core/dist/config-paths.js +42 -0
- package/packages/core/dist/config-paths.js.map +1 -0
- package/packages/core/dist/index.d.ts +1 -0
- package/packages/core/dist/index.js +1 -0
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/telegram-channel.js +2 -1
- package/packages/core/dist/telegram-channel.js.map +1 -1
- package/packages/core/package.json +4 -0
- package/scripts/dist/cli.mjs +8 -89
- package/scripts/dist/configure.mjs +8 -9
- package/scripts/dist/daemon.mjs +41 -10
- package/scripts/dist/install-layout.mjs +140 -0
- package/scripts/dist/service.mjs +78 -11
- package/scripts/dist/supervisor.mjs +35 -8
- package/scripts/dist/windows-hidden-launcher.mjs +51 -0
- package/scripts/src/cli.mts +10 -115
- package/scripts/src/configure.mts +9 -10
- package/scripts/src/daemon.mts +48 -10
- package/scripts/src/install-layout.mts +207 -0
- package/scripts/src/service.mts +91 -11
- package/scripts/src/supervisor.mts +36 -8
- package/scripts/src/windows-hidden-launcher.mts +81 -0
- package/scripts/test/install-layout.test.mjs +82 -0
- package/scripts/test/windows-hidden-launcher.test.mjs +66 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
export function resolveCopilotHubLayout({ repoRoot, env = process.env, platform = process.platform, homeDirectory = os.homedir(), }) {
|
|
6
|
+
const pathApi = getPathApi(platform);
|
|
7
|
+
const homeDir = resolveCopilotHubHomeDir({
|
|
8
|
+
env,
|
|
9
|
+
platform,
|
|
10
|
+
homeDirectory,
|
|
11
|
+
});
|
|
12
|
+
const configDir = pathApi.join(homeDir, "config");
|
|
13
|
+
const dataDir = pathApi.join(homeDir, "data");
|
|
14
|
+
const logsDir = pathApi.join(homeDir, "logs");
|
|
15
|
+
const runtimeDir = pathApi.join(homeDir, "runtime");
|
|
16
|
+
void repoRoot;
|
|
17
|
+
return {
|
|
18
|
+
homeDir,
|
|
19
|
+
configDir,
|
|
20
|
+
dataDir,
|
|
21
|
+
logsDir,
|
|
22
|
+
runtimeDir,
|
|
23
|
+
agentEngineEnvPath: pathApi.join(configDir, "agent-engine.env"),
|
|
24
|
+
controlPlaneEnvPath: pathApi.join(configDir, "control-plane.env"),
|
|
25
|
+
agentEngineDataDir: pathApi.join(dataDir, "agent-engine"),
|
|
26
|
+
controlPlaneDataDir: pathApi.join(dataDir, "control-plane"),
|
|
27
|
+
servicePromptStatePath: pathApi.join(runtimeDir, "service-onboarding.json"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function initializeCopilotHubLayout({ repoRoot, layout, }) {
|
|
31
|
+
ensureCopilotHubLayout(layout);
|
|
32
|
+
const migratedPaths = migrateLegacyLayout({ repoRoot, layout });
|
|
33
|
+
return { migratedPaths };
|
|
34
|
+
}
|
|
35
|
+
export function ensureCopilotHubLayout(layout) {
|
|
36
|
+
fs.mkdirSync(layout.homeDir, { recursive: true });
|
|
37
|
+
fs.mkdirSync(layout.configDir, { recursive: true });
|
|
38
|
+
fs.mkdirSync(layout.dataDir, { recursive: true });
|
|
39
|
+
fs.mkdirSync(layout.logsDir, { recursive: true });
|
|
40
|
+
fs.mkdirSync(layout.runtimeDir, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
export function resolveCopilotHubHomeDir({ env = process.env, platform = process.platform, homeDirectory = os.homedir(), } = {}) {
|
|
43
|
+
const pathApi = getPathApi(platform);
|
|
44
|
+
const explicit = normalizePath(env.COPILOT_HUB_HOME_DIR ?? env.COPILOT_HUB_HOME ?? "", pathApi);
|
|
45
|
+
if (explicit) {
|
|
46
|
+
return explicit;
|
|
47
|
+
}
|
|
48
|
+
if (platform === "win32") {
|
|
49
|
+
const appData = normalizePath(env.APPDATA ?? "", pathApi);
|
|
50
|
+
if (appData) {
|
|
51
|
+
return pathApi.join(appData, "copilot-hub");
|
|
52
|
+
}
|
|
53
|
+
return pathApi.join(homeDirectory, "AppData", "Roaming", "copilot-hub");
|
|
54
|
+
}
|
|
55
|
+
if (platform === "darwin") {
|
|
56
|
+
return pathApi.join(homeDirectory, "Library", "Application Support", "copilot-hub");
|
|
57
|
+
}
|
|
58
|
+
const xdgConfigHome = normalizePath(env.XDG_CONFIG_HOME ?? "", pathApi);
|
|
59
|
+
if (xdgConfigHome) {
|
|
60
|
+
return pathApi.join(xdgConfigHome, "copilot-hub");
|
|
61
|
+
}
|
|
62
|
+
return pathApi.join(homeDirectory, ".config", "copilot-hub");
|
|
63
|
+
}
|
|
64
|
+
function migrateLegacyLayout({ repoRoot, layout, }) {
|
|
65
|
+
const migratedPaths = [];
|
|
66
|
+
const legacy = resolveLegacyPaths(repoRoot);
|
|
67
|
+
if (copyFileIfMissing(legacy.agentEngineEnvPath, layout.agentEngineEnvPath)) {
|
|
68
|
+
migratedPaths.push(layout.agentEngineEnvPath);
|
|
69
|
+
}
|
|
70
|
+
if (copyFileIfMissing(legacy.controlPlaneEnvPath, layout.controlPlaneEnvPath)) {
|
|
71
|
+
migratedPaths.push(layout.controlPlaneEnvPath);
|
|
72
|
+
}
|
|
73
|
+
if (copyDirectoryIfMissing(legacy.agentEngineDataDir, layout.agentEngineDataDir)) {
|
|
74
|
+
migratedPaths.push(layout.agentEngineDataDir);
|
|
75
|
+
}
|
|
76
|
+
if (copyDirectoryIfMissing(legacy.controlPlaneDataDir, layout.controlPlaneDataDir)) {
|
|
77
|
+
migratedPaths.push(layout.controlPlaneDataDir);
|
|
78
|
+
}
|
|
79
|
+
if (copyFileIfMissing(legacy.servicePromptStatePath, layout.servicePromptStatePath)) {
|
|
80
|
+
migratedPaths.push(layout.servicePromptStatePath);
|
|
81
|
+
}
|
|
82
|
+
return migratedPaths;
|
|
83
|
+
}
|
|
84
|
+
function resolveLegacyPaths(repoRoot) {
|
|
85
|
+
return {
|
|
86
|
+
agentEngineEnvPath: path.join(repoRoot, "apps", "agent-engine", ".env"),
|
|
87
|
+
controlPlaneEnvPath: path.join(repoRoot, "apps", "control-plane", ".env"),
|
|
88
|
+
agentEngineDataDir: path.join(repoRoot, "apps", "agent-engine", "data"),
|
|
89
|
+
controlPlaneDataDir: path.join(repoRoot, "apps", "control-plane", "data"),
|
|
90
|
+
servicePromptStatePath: path.join(repoRoot, ".copilot-hub", "service-onboarding.json"),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function copyFileIfMissing(sourcePath, targetPath) {
|
|
94
|
+
if (!fs.existsSync(sourcePath) || fs.existsSync(targetPath)) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
98
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
function copyDirectoryIfMissing(sourceDir, targetDir) {
|
|
102
|
+
if (!fs.existsSync(sourceDir) || !fs.statSync(sourceDir).isDirectory()) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
if (directoryHasEntries(targetDir)) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
fs.mkdirSync(path.dirname(targetDir), { recursive: true });
|
|
109
|
+
fs.cpSync(sourceDir, targetDir, {
|
|
110
|
+
recursive: true,
|
|
111
|
+
errorOnExist: false,
|
|
112
|
+
force: false,
|
|
113
|
+
});
|
|
114
|
+
removeVolatileRuntimeFiles(targetDir);
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
function directoryHasEntries(directoryPath) {
|
|
118
|
+
if (!fs.existsSync(directoryPath)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
return fs.readdirSync(directoryPath).length > 0;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function normalizePath(value, pathApi) {
|
|
129
|
+
const normalized = String(value ?? "").trim();
|
|
130
|
+
return normalized ? pathApi.resolve(normalized) : "";
|
|
131
|
+
}
|
|
132
|
+
function getPathApi(platform) {
|
|
133
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
134
|
+
}
|
|
135
|
+
function removeVolatileRuntimeFiles(targetDir) {
|
|
136
|
+
const runtimeLockPath = path.join(targetDir, "runtime.lock");
|
|
137
|
+
if (fs.existsSync(runtimeLockPath)) {
|
|
138
|
+
fs.rmSync(runtimeLockPath, { force: true });
|
|
139
|
+
}
|
|
140
|
+
}
|
package/scripts/dist/service.mjs
CHANGED
|
@@ -6,11 +6,16 @@ import path from "node:path";
|
|
|
6
6
|
import process from "node:process";
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
|
|
10
|
+
import { buildWindowsHiddenLauncherCommand, ensureWindowsHiddenLauncher, getWindowsHiddenLauncherScriptPath, resolveWindowsScriptHost, } from "./windows-hidden-launcher.mjs";
|
|
9
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
12
|
const __dirname = path.dirname(__filename);
|
|
11
13
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
14
|
+
const layout = resolveCopilotHubLayout({ repoRoot });
|
|
15
|
+
initializeCopilotHubLayout({ repoRoot, layout });
|
|
12
16
|
const nodeBin = process.execPath;
|
|
13
17
|
const daemonScriptPath = path.join(repoRoot, "scripts", "dist", "daemon.mjs");
|
|
18
|
+
const windowsLauncherScriptPath = getWindowsHiddenLauncherScriptPath(layout.runtimeDir);
|
|
14
19
|
const WINDOWS_TASK_NAME = "CopilotHub";
|
|
15
20
|
const WINDOWS_RUN_KEY_PATH = "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run";
|
|
16
21
|
const WINDOWS_RUN_VALUE_NAME = "CopilotHub";
|
|
@@ -114,7 +119,13 @@ async function showStatus() {
|
|
|
114
119
|
}
|
|
115
120
|
async function startService() {
|
|
116
121
|
if (process.platform === "win32") {
|
|
117
|
-
startWindowsAutoStart();
|
|
122
|
+
const mode = startWindowsAutoStart();
|
|
123
|
+
if (mode === "run-key") {
|
|
124
|
+
console.log("Service started in background (Windows startup registry entry).");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.log("Service started in background (Windows Task Scheduler).");
|
|
128
|
+
}
|
|
118
129
|
return;
|
|
119
130
|
}
|
|
120
131
|
if (process.platform === "linux") {
|
|
@@ -157,7 +168,7 @@ function installWindowsAutoStart() {
|
|
|
157
168
|
throw new Error(taskCreate.combinedOutput || "Failed to create Windows auto-start task.");
|
|
158
169
|
}
|
|
159
170
|
installWindowsRunKey(command);
|
|
160
|
-
|
|
171
|
+
runWindowsHiddenLauncher();
|
|
161
172
|
return "run-key";
|
|
162
173
|
}
|
|
163
174
|
function uninstallWindowsAutoStart() {
|
|
@@ -182,6 +193,9 @@ function uninstallWindowsAutoStart() {
|
|
|
182
193
|
else if (!isRegistryValueNotFoundMessage(runKeyDelete.combinedOutput)) {
|
|
183
194
|
throw new Error(runKeyDelete.combinedOutput || "Failed to remove Windows startup registry entry.");
|
|
184
195
|
}
|
|
196
|
+
if (fs.existsSync(windowsLauncherScriptPath)) {
|
|
197
|
+
fs.rmSync(windowsLauncherScriptPath, { force: true });
|
|
198
|
+
}
|
|
185
199
|
return removed;
|
|
186
200
|
}
|
|
187
201
|
function showWindowsAutoStartStatus() {
|
|
@@ -216,12 +230,20 @@ function runWindowsTask() {
|
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
function startWindowsAutoStart() {
|
|
233
|
+
const command = buildWindowsLaunchCommand();
|
|
219
234
|
const runKey = queryWindowsRunKey();
|
|
220
235
|
if (runKey.installed) {
|
|
221
|
-
|
|
222
|
-
|
|
236
|
+
installWindowsRunKey(command);
|
|
237
|
+
runWindowsHiddenLauncher();
|
|
238
|
+
return "run-key";
|
|
239
|
+
}
|
|
240
|
+
const task = queryWindowsTask();
|
|
241
|
+
if (!task.installed) {
|
|
242
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
223
243
|
}
|
|
244
|
+
ensureTaskSchedulerAutoStart(command);
|
|
224
245
|
runWindowsTask();
|
|
246
|
+
return "task";
|
|
225
247
|
}
|
|
226
248
|
function queryWindowsRunKey() {
|
|
227
249
|
const result = runChecked("reg", ["query", WINDOWS_RUN_KEY_PATH, "/v", WINDOWS_RUN_VALUE_NAME], {
|
|
@@ -235,6 +257,18 @@ function queryWindowsRunKey() {
|
|
|
235
257
|
}
|
|
236
258
|
throw new Error(result.combinedOutput || "Failed to query Windows startup registry entry.");
|
|
237
259
|
}
|
|
260
|
+
function queryWindowsTask() {
|
|
261
|
+
const result = runChecked("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME], {
|
|
262
|
+
allowFailure: true,
|
|
263
|
+
});
|
|
264
|
+
if (result.ok) {
|
|
265
|
+
return { installed: true };
|
|
266
|
+
}
|
|
267
|
+
if (isNotFoundMessage(result.combinedOutput)) {
|
|
268
|
+
return { installed: false };
|
|
269
|
+
}
|
|
270
|
+
throw new Error(result.combinedOutput || "Failed to query Windows auto-start task.");
|
|
271
|
+
}
|
|
238
272
|
function installWindowsRunKey(command) {
|
|
239
273
|
runChecked("reg", [
|
|
240
274
|
"add",
|
|
@@ -248,6 +282,16 @@ function installWindowsRunKey(command) {
|
|
|
248
282
|
"/f",
|
|
249
283
|
], { stdio: "pipe" });
|
|
250
284
|
}
|
|
285
|
+
function ensureTaskSchedulerAutoStart(command) {
|
|
286
|
+
const result = runChecked("schtasks", ["/Create", "/TN", WINDOWS_TASK_NAME, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", command], { allowFailure: true });
|
|
287
|
+
if (result.ok) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (isNotFoundMessage(result.combinedOutput)) {
|
|
291
|
+
throw new Error("Service is not installed. Run 'copilot-hub service install' first.");
|
|
292
|
+
}
|
|
293
|
+
throw new Error(result.combinedOutput || "Failed to update Windows auto-start task.");
|
|
294
|
+
}
|
|
251
295
|
function installLinuxService() {
|
|
252
296
|
ensureSystemctl();
|
|
253
297
|
const unitPath = getLinuxUnitPath();
|
|
@@ -282,7 +326,7 @@ function installMacosService() {
|
|
|
282
326
|
ensureCommandAvailable("launchctl", ["help"], "launchctl is not available.");
|
|
283
327
|
const plistPath = getMacosPlistPath();
|
|
284
328
|
fs.mkdirSync(path.dirname(plistPath), { recursive: true });
|
|
285
|
-
fs.mkdirSync(
|
|
329
|
+
fs.mkdirSync(layout.logsDir, { recursive: true });
|
|
286
330
|
fs.writeFileSync(plistPath, buildMacosPlist(), "utf8");
|
|
287
331
|
stopMacosService({ allowFailure: true });
|
|
288
332
|
const target = getMacosLaunchTarget();
|
|
@@ -351,7 +395,7 @@ function buildLinuxUnitContent() {
|
|
|
351
395
|
"",
|
|
352
396
|
"[Service]",
|
|
353
397
|
"Type=simple",
|
|
354
|
-
`WorkingDirectory=${
|
|
398
|
+
`WorkingDirectory=${layout.runtimeDir}`,
|
|
355
399
|
`ExecStart="${nodeBin}" "${daemonScriptPath}" run`,
|
|
356
400
|
`ExecStop="${nodeBin}" "${daemonScriptPath}" stop`,
|
|
357
401
|
"Restart=always",
|
|
@@ -364,13 +408,13 @@ function buildLinuxUnitContent() {
|
|
|
364
408
|
].join("\n");
|
|
365
409
|
}
|
|
366
410
|
function buildMacosPlist() {
|
|
367
|
-
const stdoutPath = path.join(
|
|
368
|
-
const stderrPath = path.join(
|
|
411
|
+
const stdoutPath = path.join(layout.logsDir, "service-launchd.log");
|
|
412
|
+
const stderrPath = path.join(layout.logsDir, "service-launchd.error.log");
|
|
369
413
|
const values = {
|
|
370
414
|
label: escapeXml(MACOS_LABEL),
|
|
371
415
|
node: escapeXml(nodeBin),
|
|
372
416
|
script: escapeXml(daemonScriptPath),
|
|
373
|
-
cwd: escapeXml(
|
|
417
|
+
cwd: escapeXml(layout.runtimeDir),
|
|
374
418
|
stdoutPath: escapeXml(stdoutPath),
|
|
375
419
|
stderrPath: escapeXml(stderrPath),
|
|
376
420
|
};
|
|
@@ -410,6 +454,19 @@ function ensureDaemonScript() {
|
|
|
410
454
|
].join("\n"));
|
|
411
455
|
}
|
|
412
456
|
}
|
|
457
|
+
function runWindowsHiddenLauncher() {
|
|
458
|
+
const launcherScriptPath = ensureWindowsLauncherScript();
|
|
459
|
+
const scriptHost = resolveWindowsScriptHost(process.env);
|
|
460
|
+
if (!fs.existsSync(scriptHost)) {
|
|
461
|
+
throw new Error("Windows Script Host is not available.");
|
|
462
|
+
}
|
|
463
|
+
const result = runChecked(scriptHost, ["//B", "//Nologo", launcherScriptPath], {
|
|
464
|
+
allowFailure: true,
|
|
465
|
+
});
|
|
466
|
+
if (!result.ok) {
|
|
467
|
+
throw new Error(result.combinedOutput || "Failed to launch hidden Windows service starter.");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
413
470
|
function ensureSystemctl() {
|
|
414
471
|
ensureCommandAvailable("systemctl", ["--version"], "systemd is not available. This command requires Linux with systemd user services.");
|
|
415
472
|
}
|
|
@@ -431,7 +488,7 @@ function runDaemon(actionValue, { allowFailure = false } = {}) {
|
|
|
431
488
|
}
|
|
432
489
|
function runChecked(command, args, { stdio = "pipe", allowFailure = false } = {}) {
|
|
433
490
|
const result = spawnSync(command, args, {
|
|
434
|
-
cwd:
|
|
491
|
+
cwd: layout.runtimeDir,
|
|
435
492
|
shell: false,
|
|
436
493
|
stdio,
|
|
437
494
|
windowsHide: true,
|
|
@@ -514,7 +571,17 @@ function getErrorMessage(error) {
|
|
|
514
571
|
return String(error ?? "Unknown error.");
|
|
515
572
|
}
|
|
516
573
|
function buildWindowsLaunchCommand() {
|
|
517
|
-
|
|
574
|
+
const launcherScriptPath = ensureWindowsLauncherScript();
|
|
575
|
+
return buildWindowsHiddenLauncherCommand(launcherScriptPath, process.env);
|
|
576
|
+
}
|
|
577
|
+
function ensureWindowsLauncherScript() {
|
|
578
|
+
ensureDaemonScript();
|
|
579
|
+
return ensureWindowsHiddenLauncher({
|
|
580
|
+
scriptPath: windowsLauncherScriptPath,
|
|
581
|
+
nodeBin,
|
|
582
|
+
daemonScriptPath,
|
|
583
|
+
runtimeDir: layout.runtimeDir,
|
|
584
|
+
});
|
|
518
585
|
}
|
|
519
586
|
function escapeXml(value) {
|
|
520
587
|
return String(value ?? "")
|
|
@@ -4,28 +4,39 @@ import path from "node:path";
|
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
9
10
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
10
|
-
const
|
|
11
|
+
const layout = resolveCopilotHubLayout({ repoRoot });
|
|
12
|
+
initializeCopilotHubLayout({ repoRoot, layout });
|
|
13
|
+
const runtimeDir = layout.runtimeDir;
|
|
11
14
|
const pidsDir = path.join(runtimeDir, "pids");
|
|
12
|
-
const logsDir =
|
|
15
|
+
const logsDir = layout.logsDir;
|
|
16
|
+
const servicesRuntimeDir = path.join(runtimeDir, "services");
|
|
13
17
|
const SERVICES = [
|
|
14
18
|
{
|
|
15
19
|
id: "agent-engine",
|
|
16
|
-
workingDir: path.join(
|
|
17
|
-
entryScript: "dist
|
|
20
|
+
workingDir: path.join(servicesRuntimeDir, "agent-engine"),
|
|
21
|
+
entryScript: path.join(repoRoot, "apps", "agent-engine", "dist", "index.js"),
|
|
18
22
|
logFile: path.join(logsDir, "agent-engine.log"),
|
|
23
|
+
envFilePath: layout.agentEngineEnvPath,
|
|
24
|
+
dataDir: layout.agentEngineDataDir,
|
|
19
25
|
},
|
|
20
26
|
{
|
|
21
27
|
id: "control-plane",
|
|
22
|
-
workingDir: path.join(
|
|
23
|
-
entryScript: "dist
|
|
28
|
+
workingDir: path.join(servicesRuntimeDir, "control-plane"),
|
|
29
|
+
entryScript: path.join(repoRoot, "apps", "control-plane", "dist", "copilot-hub.js"),
|
|
24
30
|
logFile: path.join(logsDir, "control-plane.log"),
|
|
31
|
+
envFilePath: layout.controlPlaneEnvPath,
|
|
32
|
+
dataDir: layout.controlPlaneDataDir,
|
|
25
33
|
},
|
|
26
34
|
].map((service) => ({
|
|
27
35
|
...service,
|
|
28
36
|
pidFile: path.join(pidsDir, `${service.id}.json`),
|
|
37
|
+
botRegistryFilePath: path.join(service.dataDir, "bot-registry.json"),
|
|
38
|
+
secretStoreFilePath: path.join(service.dataDir, "secrets.json"),
|
|
39
|
+
instanceLockFilePath: path.join(service.dataDir, "runtime.lock"),
|
|
29
40
|
}));
|
|
30
41
|
const action = String(process.argv[2] ?? "up")
|
|
31
42
|
.trim()
|
|
@@ -66,7 +77,7 @@ async function startServices() {
|
|
|
66
77
|
for (let index = started.length - 1; index >= 0; index -= 1) {
|
|
67
78
|
await stopService(started[index]);
|
|
68
79
|
}
|
|
69
|
-
console.error("One or more services failed to start. Run '
|
|
80
|
+
console.error("One or more services failed to start. Run 'copilot-hub logs' for details.");
|
|
70
81
|
process.exit(1);
|
|
71
82
|
}
|
|
72
83
|
started.push(service);
|
|
@@ -129,13 +140,14 @@ async function startService(service, options = {}) {
|
|
|
129
140
|
const logFd = fs.openSync(service.logFile, "a");
|
|
130
141
|
let child;
|
|
131
142
|
try {
|
|
143
|
+
const childEnv = buildServiceEnvironment(service);
|
|
132
144
|
child = spawn(process.execPath, [service.entryScript], {
|
|
133
145
|
cwd: service.workingDir,
|
|
134
146
|
detached: true,
|
|
135
147
|
stdio: ["ignore", logFd, logFd],
|
|
136
148
|
windowsHide: true,
|
|
137
149
|
shell: false,
|
|
138
|
-
env:
|
|
150
|
+
env: childEnv,
|
|
139
151
|
});
|
|
140
152
|
}
|
|
141
153
|
finally {
|
|
@@ -281,6 +293,21 @@ function ensureRuntimeDirs() {
|
|
|
281
293
|
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
282
294
|
fs.mkdirSync(pidsDir, { recursive: true });
|
|
283
295
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
296
|
+
fs.mkdirSync(servicesRuntimeDir, { recursive: true });
|
|
297
|
+
for (const service of SERVICES) {
|
|
298
|
+
fs.mkdirSync(service.workingDir, { recursive: true });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function buildServiceEnvironment(service) {
|
|
302
|
+
return {
|
|
303
|
+
...process.env,
|
|
304
|
+
COPILOT_HUB_HOME_DIR: process.env.COPILOT_HUB_HOME_DIR || layout.homeDir,
|
|
305
|
+
COPILOT_HUB_ENV_PATH: service.envFilePath,
|
|
306
|
+
BOT_DATA_DIR: service.dataDir,
|
|
307
|
+
BOT_REGISTRY_FILE: service.botRegistryFilePath,
|
|
308
|
+
SECRET_STORE_FILE: service.secretStoreFilePath,
|
|
309
|
+
INSTANCE_LOCK_FILE: service.instanceLockFilePath,
|
|
310
|
+
};
|
|
284
311
|
}
|
|
285
312
|
function printTail(filePath, lines) {
|
|
286
313
|
if (!fs.existsSync(filePath)) {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export function resolveWindowsScriptHost(env = process.env) {
|
|
4
|
+
const systemRoot = String(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows").trim();
|
|
5
|
+
const baseDir = systemRoot || "C:\\Windows";
|
|
6
|
+
return path.win32.join(baseDir, "System32", "wscript.exe");
|
|
7
|
+
}
|
|
8
|
+
export function getWindowsHiddenLauncherScriptPath(runtimeDir) {
|
|
9
|
+
return path.win32.join(runtimeDir, "windows-daemon-launcher.vbs");
|
|
10
|
+
}
|
|
11
|
+
export function ensureWindowsHiddenLauncher({ scriptPath, nodeBin, daemonScriptPath, runtimeDir, }) {
|
|
12
|
+
const content = buildWindowsHiddenLauncherContent({
|
|
13
|
+
nodeBin,
|
|
14
|
+
daemonScriptPath,
|
|
15
|
+
runtimeDir,
|
|
16
|
+
});
|
|
17
|
+
let current = "";
|
|
18
|
+
try {
|
|
19
|
+
current = fs.readFileSync(scriptPath, "utf8");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
current = "";
|
|
23
|
+
}
|
|
24
|
+
if (current !== content) {
|
|
25
|
+
fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
|
|
26
|
+
fs.writeFileSync(scriptPath, content, "utf8");
|
|
27
|
+
}
|
|
28
|
+
return scriptPath;
|
|
29
|
+
}
|
|
30
|
+
export function buildWindowsHiddenLauncherCommand(scriptPath, env = process.env) {
|
|
31
|
+
const scriptHost = resolveWindowsScriptHost(env);
|
|
32
|
+
return `"${scriptHost}" //B //Nologo "${scriptPath}"`;
|
|
33
|
+
}
|
|
34
|
+
export function buildWindowsHiddenLauncherContent({ nodeBin, daemonScriptPath, runtimeDir, }) {
|
|
35
|
+
const command = buildWindowsCommandLine([nodeBin, daemonScriptPath, "start"]);
|
|
36
|
+
return [
|
|
37
|
+
"Option Explicit",
|
|
38
|
+
"Dim shell",
|
|
39
|
+
'Set shell = CreateObject("WScript.Shell")',
|
|
40
|
+
`shell.CurrentDirectory = "${escapeVbsString(runtimeDir)}"`,
|
|
41
|
+
`shell.Run "${escapeVbsString(command)}", 0, False`,
|
|
42
|
+
"Set shell = Nothing",
|
|
43
|
+
"",
|
|
44
|
+
].join("\r\n");
|
|
45
|
+
}
|
|
46
|
+
function buildWindowsCommandLine(args) {
|
|
47
|
+
return args.map((value) => `"${String(value ?? "")}"`).join(" ");
|
|
48
|
+
}
|
|
49
|
+
function escapeVbsString(value) {
|
|
50
|
+
return String(value ?? "").replace(/"/g, '""');
|
|
51
|
+
}
|
package/scripts/src/cli.mts
CHANGED
|
@@ -6,6 +6,7 @@ import { spawnSync } from "node:child_process";
|
|
|
6
6
|
import { createInterface } from "node:readline/promises";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
import { codexInstallPackageSpec } from "./codex-version.mjs";
|
|
9
|
+
import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
|
|
9
10
|
import {
|
|
10
11
|
buildCodexCompatibilityError,
|
|
11
12
|
buildCodexCompatibilityNotice,
|
|
@@ -17,12 +18,13 @@ import {
|
|
|
17
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
18
19
|
const __dirname = path.dirname(__filename);
|
|
19
20
|
const repoRoot = path.resolve(__dirname, "..", "..");
|
|
21
|
+
const layout = resolveCopilotHubLayout({ repoRoot });
|
|
20
22
|
const packageJsonPath = path.join(repoRoot, "package.json");
|
|
21
|
-
const runtimeDir =
|
|
22
|
-
const servicePromptStatePath =
|
|
23
|
+
const runtimeDir = layout.runtimeDir;
|
|
24
|
+
const servicePromptStatePath = layout.servicePromptStatePath;
|
|
23
25
|
const nodeBin = process.execPath;
|
|
24
|
-
const agentEngineEnvPath =
|
|
25
|
-
const controlPlaneEnvPath =
|
|
26
|
+
const agentEngineEnvPath = layout.agentEngineEnvPath;
|
|
27
|
+
const controlPlaneEnvPath = layout.controlPlaneEnvPath;
|
|
26
28
|
const codexInstallCommand = `npm install -g ${codexInstallPackageSpec}`;
|
|
27
29
|
const packageVersion = readPackageVersion();
|
|
28
30
|
|
|
@@ -50,6 +52,8 @@ async function main() {
|
|
|
50
52
|
return;
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
initializeCopilotHubLayout({ repoRoot, layout });
|
|
56
|
+
|
|
53
57
|
switch (action) {
|
|
54
58
|
case "start": {
|
|
55
59
|
runNode(["scripts/dist/configure.mjs", "--required-only"]);
|
|
@@ -114,18 +118,6 @@ async function main() {
|
|
|
114
118
|
runNode(["scripts/dist/service.mjs", ...rawArgs.slice(1)]);
|
|
115
119
|
return;
|
|
116
120
|
}
|
|
117
|
-
case "_update_resume": {
|
|
118
|
-
await resumeAfterUpdate({
|
|
119
|
-
serviceInstalled: rawArgs.includes("--service-installed"),
|
|
120
|
-
runningBeforeUpdate: rawArgs.includes("--resume-running"),
|
|
121
|
-
});
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
case "update":
|
|
125
|
-
case "upgrade": {
|
|
126
|
-
await runSelfUpdate();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
121
|
default: {
|
|
130
122
|
printUsage();
|
|
131
123
|
process.exit(1);
|
|
@@ -279,86 +271,6 @@ async function maybeOfferServiceInstall() {
|
|
|
279
271
|
writeServicePromptState("accepted");
|
|
280
272
|
}
|
|
281
273
|
|
|
282
|
-
async function runSelfUpdate() {
|
|
283
|
-
const serviceInstalled = isServiceAlreadyInstalled();
|
|
284
|
-
const runningBeforeUpdate = serviceInstalled ? isDaemonRunning() : hasRunningSupervisorWorkers();
|
|
285
|
-
|
|
286
|
-
if (serviceInstalled) {
|
|
287
|
-
const stopService = runNodeCapture(["scripts/dist/service.mjs", "stop"], "inherit");
|
|
288
|
-
if (!stopService.ok) {
|
|
289
|
-
console.log("Service stop reported an error. Continuing update attempt.");
|
|
290
|
-
}
|
|
291
|
-
} else {
|
|
292
|
-
const stopLocal = runNodeCapture(["scripts/dist/supervisor.mjs", "down"], "inherit");
|
|
293
|
-
if (!stopLocal.ok) {
|
|
294
|
-
console.log("Local stop reported an error. Continuing update attempt.");
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const install = runNpm(["install", "-g", "copilot-hub@latest"], "inherit");
|
|
299
|
-
if (!install.ok) {
|
|
300
|
-
const detail =
|
|
301
|
-
firstLine(install.errorMessage) || firstLine(install.stderr) || firstLine(install.stdout);
|
|
302
|
-
const normalizedDetail = detail.toLowerCase();
|
|
303
|
-
if (normalizedDetail.includes("ebusy") || normalizedDetail.includes("resource busy")) {
|
|
304
|
-
throw new Error(
|
|
305
|
-
[
|
|
306
|
-
"Update failed because files are locked by another process (EBUSY).",
|
|
307
|
-
"Close other terminals using copilot-hub, then retry 'copilot-hub update'.",
|
|
308
|
-
].join("\n"),
|
|
309
|
-
);
|
|
310
|
-
}
|
|
311
|
-
throw new Error(`Update failed: ${detail || "Unknown npm error."}`);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
console.log("copilot-hub updated to latest.");
|
|
315
|
-
const resume = runNodeCapture(
|
|
316
|
-
[
|
|
317
|
-
"scripts/dist/cli.mjs",
|
|
318
|
-
"_update_resume",
|
|
319
|
-
...(serviceInstalled ? ["--service-installed"] : ["--local-mode"]),
|
|
320
|
-
...(runningBeforeUpdate ? ["--resume-running"] : ["--stopped"]),
|
|
321
|
-
],
|
|
322
|
-
"inherit",
|
|
323
|
-
);
|
|
324
|
-
if (!resume.ok) {
|
|
325
|
-
console.log(
|
|
326
|
-
"Update completed, but post-update Codex validation or restart failed. Run 'copilot-hub start' manually.",
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
async function resumeAfterUpdate({
|
|
332
|
-
serviceInstalled,
|
|
333
|
-
runningBeforeUpdate,
|
|
334
|
-
}: {
|
|
335
|
-
serviceInstalled: boolean;
|
|
336
|
-
runningBeforeUpdate: boolean;
|
|
337
|
-
}) {
|
|
338
|
-
await ensureCompatibleCodexBinary({
|
|
339
|
-
autoInstall: true,
|
|
340
|
-
purpose: "update",
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
if (!runningBeforeUpdate) {
|
|
344
|
-
console.log("Services remain stopped. Run 'copilot-hub start' when ready.");
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (serviceInstalled) {
|
|
349
|
-
const startService = runNodeCapture(["scripts/dist/service.mjs", "start"], "inherit");
|
|
350
|
-
if (!startService.ok) {
|
|
351
|
-
console.log("Update completed, but service start failed. Run 'copilot-hub start' manually.");
|
|
352
|
-
}
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const startLocal = runNodeCapture(["scripts/dist/supervisor.mjs", "up"], "inherit");
|
|
357
|
-
if (!startLocal.ok) {
|
|
358
|
-
console.log("Update completed, but local start failed. Run 'copilot-hub start' manually.");
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
|
|
362
274
|
function isServiceSupportedOnCurrentPlatform() {
|
|
363
275
|
return (
|
|
364
276
|
process.platform === "win32" || process.platform === "linux" || process.platform === "darwin"
|
|
@@ -377,18 +289,6 @@ function isServiceAlreadyInstalled() {
|
|
|
377
289
|
return status.ok;
|
|
378
290
|
}
|
|
379
291
|
|
|
380
|
-
function isDaemonRunning() {
|
|
381
|
-
const status = runNodeCapture(["scripts/dist/daemon.mjs", "status"], "pipe");
|
|
382
|
-
const output = String(status.combinedOutput ?? "").toLowerCase();
|
|
383
|
-
return output.includes("=== daemon ===") && output.includes("running: yes");
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function hasRunningSupervisorWorkers() {
|
|
387
|
-
const status = runNodeCapture(["scripts/dist/supervisor.mjs", "status"], "pipe");
|
|
388
|
-
const output = String(status.combinedOutput ?? "").toLowerCase();
|
|
389
|
-
return output.includes("running: yes");
|
|
390
|
-
}
|
|
391
|
-
|
|
392
292
|
function readServicePromptState() {
|
|
393
293
|
if (!fs.existsSync(servicePromptStatePath)) {
|
|
394
294
|
return null;
|
|
@@ -429,7 +329,7 @@ async function ensureCompatibleCodexBinary({
|
|
|
429
329
|
purpose,
|
|
430
330
|
}: {
|
|
431
331
|
autoInstall: boolean;
|
|
432
|
-
purpose: "start" | "restart" | "service"
|
|
332
|
+
purpose: "start" | "restart" | "service";
|
|
433
333
|
}): Promise<string> {
|
|
434
334
|
const resolved = resolveCodexBinForStart({
|
|
435
335
|
repoRoot,
|
|
@@ -501,11 +401,7 @@ async function ensureCompatibleCodexBinary({
|
|
|
501
401
|
}
|
|
502
402
|
|
|
503
403
|
if (!shouldInstall) {
|
|
504
|
-
throw new Error(
|
|
505
|
-
purpose === "update"
|
|
506
|
-
? "Compatible Codex CLI is required before restarting services."
|
|
507
|
-
: "Compatible Codex CLI is required before starting services.",
|
|
508
|
-
);
|
|
404
|
+
throw new Error("Compatible Codex CLI is required before starting services.");
|
|
509
405
|
}
|
|
510
406
|
|
|
511
407
|
const install = runNpm(["install", "-g", codexInstallPackageSpec], "inherit");
|
|
@@ -697,7 +593,6 @@ function printUsage() {
|
|
|
697
593
|
console.log(
|
|
698
594
|
[
|
|
699
595
|
"Usage: node scripts/dist/cli.mjs <start|stop|restart|status|logs|configure|service|version|help>",
|
|
700
|
-
" node scripts/dist/cli.mjs <update|upgrade>",
|
|
701
596
|
"Service management:",
|
|
702
597
|
" node scripts/dist/cli.mjs service <install|uninstall|status|start|stop|help>",
|
|
703
598
|
].join("\n"),
|