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.
@@ -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
+ }
@@ -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
- runDaemon("start");
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
- runDaemon("start");
222
- return;
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(path.join(repoRoot, "logs"), { recursive: true });
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=${repoRoot}`,
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(repoRoot, "logs", "service-launchd.log");
368
- const stderrPath = path.join(repoRoot, "logs", "service-launchd.error.log");
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(repoRoot),
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: repoRoot,
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
- return `"${nodeBin}" "${daemonScriptPath}" run`;
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 runtimeDir = path.join(repoRoot, ".copilot-hub");
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 = path.join(repoRoot, "logs");
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(repoRoot, "apps", "agent-engine"),
17
- entryScript: "dist/index.js",
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(repoRoot, "apps", "control-plane"),
23
- entryScript: "dist/copilot-hub.js",
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 'npm run logs' for details.");
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: process.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
+ }
@@ -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 = path.join(repoRoot, ".copilot-hub");
22
- const servicePromptStatePath = path.join(runtimeDir, "service-onboarding.json");
23
+ const runtimeDir = layout.runtimeDir;
24
+ const servicePromptStatePath = layout.servicePromptStatePath;
23
25
  const nodeBin = process.execPath;
24
- const agentEngineEnvPath = path.join(repoRoot, "apps", "agent-engine", ".env");
25
- const controlPlaneEnvPath = path.join(repoRoot, "apps", "control-plane", ".env");
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" | "update";
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"),