copilot-hub 0.1.28 → 0.1.30

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.
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import process, { stdin as input, stdout as output } from "node:process";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createInterface } from "node:readline/promises";
7
+ import { parseEnvMap, readEnvLines, setEnvValue, writeEnvLines } from "./env-file-utils.mjs";
7
8
  import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
@@ -18,7 +19,8 @@ const engineExamplePath = path.join(repoRoot, "apps", "agent-engine", ".env.exam
18
19
  const controlPlaneEnvPath = layout.controlPlaneEnvPath;
19
20
  const controlPlaneExamplePath = path.join(repoRoot, "apps", "control-plane", ".env.example");
20
21
  const TELEGRAM_TOKEN_PATTERN = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
21
- const DEFAULT_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN";
22
+ const DEFAULT_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN_FILE";
23
+ const LEGACY_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN";
22
24
 
23
25
  const args = new Set(process.argv.slice(2));
24
26
  const requiredOnly = args.has("--required-only");
@@ -29,8 +31,8 @@ async function main() {
29
31
  ensureEnvFile(engineEnvPath, engineExamplePath);
30
32
  ensureEnvFile(controlPlaneEnvPath, controlPlaneExamplePath);
31
33
 
32
- const engineLines = readLines(engineEnvPath);
33
- const controlPlaneLines = readLines(controlPlaneEnvPath);
34
+ const engineLines = readEnvLines(engineEnvPath);
35
+ const controlPlaneLines = readEnvLines(controlPlaneEnvPath);
34
36
 
35
37
  const rl = createInterface({ input, output });
36
38
 
@@ -48,18 +50,12 @@ async function main() {
48
50
  rl.close();
49
51
  }
50
52
 
51
- writeLines(engineEnvPath, engineLines);
52
- writeLines(controlPlaneEnvPath, controlPlaneLines);
53
+ writeEnvLines(engineEnvPath, engineLines);
54
+ writeEnvLines(controlPlaneEnvPath, controlPlaneLines);
53
55
  }
54
56
 
55
57
  async function configureRequiredTokens({ rl, controlPlaneLines }) {
56
- const controlPlaneMap = parseEnvMap(controlPlaneLines);
57
-
58
- const controlPlaneTokenEnvName = nonEmpty(
59
- controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV,
60
- DEFAULT_CONTROL_PLANE_TOKEN_ENV,
61
- );
62
- setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvName);
58
+ const controlPlaneTokenEnvName = migrateControlPlaneTokenEnv(controlPlaneLines);
63
59
 
64
60
  const postControlPlaneMap = parseEnvMap(controlPlaneLines);
65
61
  const currentToken = String(postControlPlaneMap[controlPlaneTokenEnvName] ?? "").trim();
@@ -82,15 +78,9 @@ async function configureRequiredTokens({ rl, controlPlaneLines }) {
82
78
  }
83
79
 
84
80
  async function configureAll({ rl, controlPlaneLines }) {
85
- const controlPlaneMap = parseEnvMap(controlPlaneLines);
86
-
87
81
  console.log("\nCopilot Hub control-plane configuration\n");
88
82
 
89
- const controlPlaneTokenEnvDefault = nonEmpty(
90
- controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV,
91
- DEFAULT_CONTROL_PLANE_TOKEN_ENV,
92
- );
93
- setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvDefault);
83
+ const controlPlaneTokenEnvDefault = migrateControlPlaneTokenEnv(controlPlaneLines);
94
84
  const currentControlPlaneToken = String(
95
85
  parseEnvMap(controlPlaneLines)[controlPlaneTokenEnvDefault] ?? "",
96
86
  ).trim();
@@ -106,6 +96,29 @@ async function configureAll({ rl, controlPlaneLines }) {
106
96
  }
107
97
  }
108
98
 
99
+ function migrateControlPlaneTokenEnv(lines) {
100
+ const controlPlaneMap = parseEnvMap(lines);
101
+ const configuredTokenEnvName = nonEmpty(
102
+ controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV,
103
+ DEFAULT_CONTROL_PLANE_TOKEN_ENV,
104
+ );
105
+ const shouldMigrateLegacyName = configuredTokenEnvName === LEGACY_CONTROL_PLANE_TOKEN_ENV;
106
+ const nextTokenEnvName = shouldMigrateLegacyName
107
+ ? DEFAULT_CONTROL_PLANE_TOKEN_ENV
108
+ : configuredTokenEnvName;
109
+ setEnvValue(lines, "HUB_TELEGRAM_TOKEN_ENV", nextTokenEnvName);
110
+
111
+ if (shouldMigrateLegacyName) {
112
+ const legacyToken = String(controlPlaneMap[LEGACY_CONTROL_PLANE_TOKEN_ENV] ?? "").trim();
113
+ const dedicatedToken = String(controlPlaneMap[DEFAULT_CONTROL_PLANE_TOKEN_ENV] ?? "").trim();
114
+ if (legacyToken && !dedicatedToken) {
115
+ setEnvValue(lines, DEFAULT_CONTROL_PLANE_TOKEN_ENV, legacyToken);
116
+ }
117
+ }
118
+
119
+ return nextTokenEnvName;
120
+ }
121
+
109
122
  function ensureEnvFile(envPath, examplePath) {
110
123
  fs.mkdirSync(path.dirname(envPath), { recursive: true });
111
124
  if (fs.existsSync(envPath)) {
@@ -120,80 +133,11 @@ function ensureEnvFile(envPath, examplePath) {
120
133
  fs.writeFileSync(envPath, "", "utf8");
121
134
  }
122
135
 
123
- function readLines(filePath) {
124
- const content = fs.readFileSync(filePath, "utf8");
125
- return content.split(/\r?\n/);
126
- }
127
-
128
- function writeLines(filePath, lines) {
129
- const normalized = [...lines];
130
- if (normalized.length === 0 || normalized[normalized.length - 1] !== "") {
131
- normalized.push("");
132
- }
133
- fs.writeFileSync(filePath, normalized.join("\n"), "utf8");
134
- }
135
-
136
- function parseEnvMap(lines) {
137
- const map: Record<string, string> = {};
138
- for (const line of lines) {
139
- const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
140
- if (!match) {
141
- continue;
142
- }
143
-
144
- const key = match[1];
145
- const value = unquote(match[2] ?? "");
146
- map[key] = value;
147
- }
148
- return map;
149
- }
150
-
151
- function setEnvValue(lines, key, value) {
152
- const safeValue = sanitizeValue(value);
153
- const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
154
- for (let index = 0; index < lines.length; index += 1) {
155
- if (!pattern.test(lines[index])) {
156
- continue;
157
- }
158
-
159
- lines[index] = `${key}=${safeValue}`;
160
- return;
161
- }
162
-
163
- if (lines.length > 0 && lines[lines.length - 1] !== "") {
164
- lines.push("");
165
- }
166
- lines.push(`${key}=${safeValue}`);
167
- }
168
-
169
- function sanitizeValue(value) {
170
- return String(value ?? "")
171
- .replace(/[\r\n]/g, "")
172
- .trim();
173
- }
174
-
175
- function unquote(value) {
176
- const raw = String(value ?? "").trim();
177
- if (!raw) {
178
- return "";
179
- }
180
-
181
- if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
182
- return raw.slice(1, -1);
183
- }
184
-
185
- return raw;
186
- }
187
-
188
136
  function nonEmpty(value, fallback) {
189
137
  const normalized = String(value ?? "").trim();
190
138
  return normalized || fallback;
191
139
  }
192
140
 
193
- function escapeRegex(value) {
194
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
195
- }
196
-
197
141
  async function askRequired(rl, label) {
198
142
  while (true) {
199
143
  const value = await rl.question(`${label}: `);
@@ -600,6 +600,34 @@ function detectFatalStartupError(ensureResult) {
600
600
  };
601
601
  }
602
602
 
603
+ const invalidHubTokenLine = findLineContaining(
604
+ evidenceChunks,
605
+ (line) => line.includes("hub telegram token in") && line.includes("is invalid"),
606
+ );
607
+ if (invalidHubTokenLine) {
608
+ return {
609
+ reason: invalidHubTokenLine,
610
+ action:
611
+ "Run 'copilot-hub configure' to save a valid hub token in the control-plane config, then retry service.",
612
+ detectedAt: new Date().toISOString(),
613
+ };
614
+ }
615
+
616
+ const workspaceRootLine = findLineContaining(
617
+ evidenceChunks,
618
+ (line) =>
619
+ line.includes("default_workspace_root must be outside kernel directory") ||
620
+ line.includes("hub_workspace_root must be outside kernel directory"),
621
+ );
622
+ if (workspaceRootLine) {
623
+ return {
624
+ reason: workspaceRootLine,
625
+ action:
626
+ "Set DEFAULT_WORKSPACE_ROOT to a folder outside the copilot-hub installation, then retry service.",
627
+ detectedAt: new Date().toISOString(),
628
+ };
629
+ }
630
+
603
631
  return null;
604
632
  }
605
633
 
@@ -0,0 +1,98 @@
1
+ import fs from "node:fs";
2
+
3
+ export function ensureEnvTextFile(filePath: string): void {
4
+ if (fs.existsSync(filePath)) {
5
+ return;
6
+ }
7
+ fs.mkdirSync(requireParentDir(filePath), { recursive: true });
8
+ fs.writeFileSync(filePath, "", "utf8");
9
+ }
10
+
11
+ export function readEnvLines(filePath: string): string[] {
12
+ const content = fs.readFileSync(filePath, "utf8");
13
+ return content.split(/\r?\n/);
14
+ }
15
+
16
+ export function writeEnvLines(filePath: string, lines: string[]): void {
17
+ const normalized = [...lines];
18
+ if (normalized.length === 0 || normalized[normalized.length - 1] !== "") {
19
+ normalized.push("");
20
+ }
21
+ fs.writeFileSync(filePath, normalized.join("\n"), "utf8");
22
+ }
23
+
24
+ export function parseEnvMap(lines: string[]): Record<string, string> {
25
+ const map: Record<string, string> = {};
26
+ for (const line of lines) {
27
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
28
+ if (!match) {
29
+ continue;
30
+ }
31
+
32
+ const key = match[1];
33
+ const value = unquote(match[2] ?? "");
34
+ map[key] = value;
35
+ }
36
+ return map;
37
+ }
38
+
39
+ export function setEnvValue(lines: string[], key: string, value: unknown): void {
40
+ const safeValue = sanitizeEnvValue(value);
41
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
42
+ for (let index = 0; index < lines.length; index += 1) {
43
+ if (!pattern.test(lines[index])) {
44
+ continue;
45
+ }
46
+
47
+ lines[index] = `${key}=${safeValue}`;
48
+ return;
49
+ }
50
+
51
+ if (lines.length > 0 && lines[lines.length - 1] !== "") {
52
+ lines.push("");
53
+ }
54
+ lines.push(`${key}=${safeValue}`);
55
+ }
56
+
57
+ export function removeEnvKeys(lines: string[], keys: readonly string[]): boolean {
58
+ const patterns = keys.map((key) => new RegExp(`^\\s*${escapeRegex(key)}\\s*=`));
59
+ const originalLength = lines.length;
60
+ const kept = lines.filter((line) => !patterns.some((pattern) => pattern.test(line)));
61
+ if (kept.length === originalLength) {
62
+ return false;
63
+ }
64
+ lines.splice(0, lines.length, ...kept);
65
+ return true;
66
+ }
67
+
68
+ export function sanitizeEnvValue(value: unknown): string {
69
+ return String(value ?? "")
70
+ .replace(/[\r\n]/g, "")
71
+ .trim();
72
+ }
73
+
74
+ function unquote(value: string): string {
75
+ const raw = String(value ?? "").trim();
76
+ if (!raw) {
77
+ return "";
78
+ }
79
+
80
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
81
+ return raw.slice(1, -1);
82
+ }
83
+
84
+ return raw;
85
+ }
86
+
87
+ function escapeRegex(value: string): string {
88
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
89
+ }
90
+
91
+ function requireParentDir(filePath: string): string {
92
+ const parts = String(filePath ?? "").split(/[\\/]/);
93
+ if (parts.length <= 1) {
94
+ return ".";
95
+ }
96
+ parts.pop();
97
+ return parts.join("/") || ".";
98
+ }
@@ -2,6 +2,8 @@
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import process from "node:process";
6
+ import { parseEnvMap, readEnvLines, removeEnvKeys, writeEnvLines } from "./env-file-utils.mjs";
5
7
 
6
8
  export type CopilotHubLayout = {
7
9
  homeDir: string;
@@ -60,10 +62,44 @@ export function initializeCopilotHubLayout({
60
62
  }: {
61
63
  repoRoot: string;
62
64
  layout: CopilotHubLayout;
63
- }): { migratedPaths: string[] } {
65
+ }): { migratedPaths: string[]; normalizedEnvPaths: string[] } {
64
66
  ensureCopilotHubLayout(layout);
65
67
  const migratedPaths = migrateLegacyLayout({ repoRoot, layout });
66
- return { migratedPaths };
68
+ const normalizedEnvPaths = normalizePersistentEnvFiles(layout);
69
+ return { migratedPaths, normalizedEnvPaths };
70
+ }
71
+
72
+ export function resetCopilotHubConfig({ layout }: { layout: CopilotHubLayout }): {
73
+ removedPaths: string[];
74
+ } {
75
+ const removedPaths: string[] = [];
76
+
77
+ for (const target of [layout.configDir, layout.dataDir, layout.logsDir]) {
78
+ if (!fs.existsSync(target)) {
79
+ continue;
80
+ }
81
+ fs.rmSync(target, { recursive: true, force: true });
82
+ removedPaths.push(target);
83
+ }
84
+
85
+ const runtimeTargets = [
86
+ path.join(layout.runtimeDir, "pids"),
87
+ path.join(layout.runtimeDir, "services"),
88
+ path.join(layout.runtimeDir, "last-startup-error.json"),
89
+ layout.servicePromptStatePath,
90
+ ];
91
+ for (const target of runtimeTargets) {
92
+ if (!fs.existsSync(target)) {
93
+ continue;
94
+ }
95
+ fs.rmSync(target, { recursive: true, force: true });
96
+ removedPaths.push(target);
97
+ }
98
+
99
+ ensureCopilotHubLayout(layout);
100
+ return {
101
+ removedPaths: removedPaths.sort(),
102
+ };
67
103
  }
68
104
 
69
105
  export function ensureCopilotHubLayout(layout: CopilotHubLayout): void {
@@ -137,6 +173,132 @@ function migrateLegacyLayout({
137
173
  return migratedPaths;
138
174
  }
139
175
 
176
+ function normalizePersistentEnvFiles(layout: CopilotHubLayout): string[] {
177
+ const normalizedPaths: string[] = [];
178
+
179
+ if (
180
+ normalizePersistentEnvFile(layout.agentEngineEnvPath, [
181
+ {
182
+ key: "BOT_DATA_DIR",
183
+ legacyValues: ["./data"],
184
+ wrongResolvedPath: path.join(layout.configDir, "data"),
185
+ },
186
+ {
187
+ key: "BOT_REGISTRY_FILE",
188
+ legacyValues: ["./data/bot-registry.json"],
189
+ wrongResolvedPath: path.join(layout.configDir, "data", "bot-registry.json"),
190
+ },
191
+ {
192
+ key: "SECRET_STORE_FILE",
193
+ legacyValues: ["./data/secrets.json"],
194
+ wrongResolvedPath: path.join(layout.configDir, "data", "secrets.json"),
195
+ },
196
+ {
197
+ key: "INSTANCE_LOCK_FILE",
198
+ legacyValues: ["./data/runtime.lock"],
199
+ wrongResolvedPath: path.join(layout.configDir, "data", "runtime.lock"),
200
+ },
201
+ ])
202
+ ) {
203
+ normalizedPaths.push(layout.agentEngineEnvPath);
204
+ }
205
+
206
+ if (
207
+ normalizePersistentEnvFile(layout.controlPlaneEnvPath, [
208
+ {
209
+ key: "BOT_DATA_DIR",
210
+ legacyValues: ["./data"],
211
+ wrongResolvedPath: path.join(layout.configDir, "data"),
212
+ },
213
+ {
214
+ key: "BOT_REGISTRY_FILE",
215
+ legacyValues: ["./data/bot-registry.json"],
216
+ wrongResolvedPath: path.join(layout.configDir, "data", "bot-registry.json"),
217
+ },
218
+ {
219
+ key: "SECRET_STORE_FILE",
220
+ legacyValues: ["./data/secrets.json"],
221
+ wrongResolvedPath: path.join(layout.configDir, "data", "secrets.json"),
222
+ },
223
+ {
224
+ key: "INSTANCE_LOCK_FILE",
225
+ legacyValues: ["./data/runtime.lock"],
226
+ wrongResolvedPath: path.join(layout.configDir, "data", "runtime.lock"),
227
+ },
228
+ {
229
+ key: "HUB_DATA_DIR",
230
+ legacyValues: ["./data/copilot_hub"],
231
+ wrongResolvedPath: path.join(layout.configDir, "data", "copilot_hub"),
232
+ },
233
+ ])
234
+ ) {
235
+ normalizedPaths.push(layout.controlPlaneEnvPath);
236
+ }
237
+
238
+ return normalizedPaths.sort();
239
+ }
240
+
241
+ function normalizePersistentEnvFile(
242
+ filePath: string,
243
+ rules: Array<{ key: string; legacyValues: string[]; wrongResolvedPath: string }>,
244
+ ): boolean {
245
+ if (!fs.existsSync(filePath)) {
246
+ return false;
247
+ }
248
+
249
+ const lines = readEnvLines(filePath);
250
+ const envMap = parseEnvMap(lines);
251
+ const keysToRemove = rules
252
+ .filter((rule) =>
253
+ shouldRemoveLegacyManagedPath(envMap[rule.key], {
254
+ legacyValues: rule.legacyValues,
255
+ wrongResolvedPath: rule.wrongResolvedPath,
256
+ configBaseDir: path.dirname(filePath),
257
+ }),
258
+ )
259
+ .map((rule) => rule.key);
260
+
261
+ if (keysToRemove.length === 0) {
262
+ return false;
263
+ }
264
+
265
+ removeEnvKeys(lines, keysToRemove);
266
+ writeEnvLines(filePath, lines);
267
+ return true;
268
+ }
269
+
270
+ function shouldRemoveLegacyManagedPath(
271
+ rawValue: string | undefined,
272
+ {
273
+ legacyValues,
274
+ wrongResolvedPath,
275
+ configBaseDir,
276
+ }: {
277
+ legacyValues: string[];
278
+ wrongResolvedPath: string;
279
+ configBaseDir: string;
280
+ },
281
+ ): boolean {
282
+ const value = String(rawValue ?? "").trim();
283
+ if (!value) {
284
+ return false;
285
+ }
286
+
287
+ const normalizedValue = normalizeForCompare(value);
288
+ if (legacyValues.some((entry) => normalizeForCompare(entry) === normalizedValue)) {
289
+ return true;
290
+ }
291
+
292
+ if (path.isAbsolute(value)) {
293
+ return normalizeForCompare(value) === normalizeForCompare(wrongResolvedPath);
294
+ }
295
+
296
+ return (
297
+ normalizeForCompare(path.resolve(configBaseDir, value)) ===
298
+ normalizeForCompare(wrongResolvedPath)
299
+ );
300
+ }
301
+
140
302
  function resolveLegacyPaths(repoRoot: string): {
141
303
  agentEngineEnvPath: string;
142
304
  controlPlaneEnvPath: string;
@@ -195,6 +357,14 @@ function normalizePath(value: unknown, pathApi: typeof path.posix | typeof path.
195
357
  return normalized ? pathApi.resolve(normalized) : "";
196
358
  }
197
359
 
360
+ function normalizeForCompare(value: unknown): string {
361
+ const normalized = String(value ?? "").trim();
362
+ if (!normalized) {
363
+ return "";
364
+ }
365
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
366
+ }
367
+
198
368
  function getPathApi(platform: NodeJS.Platform): typeof path.posix | typeof path.win32 {
199
369
  return platform === "win32" ? path.win32 : path.posix;
200
370
  }
@@ -354,6 +354,11 @@ function buildServiceEnvironment(service) {
354
354
  BOT_REGISTRY_FILE: service.botRegistryFilePath,
355
355
  SECRET_STORE_FILE: service.secretStoreFilePath,
356
356
  INSTANCE_LOCK_FILE: service.instanceLockFilePath,
357
+ ...(service.id === "control-plane"
358
+ ? {
359
+ HUB_DATA_DIR: path.join(service.dataDir, "copilot_hub"),
360
+ }
361
+ : {}),
357
362
  };
358
363
  }
359
364
 
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import test from "node:test";
6
6
  import {
7
7
  initializeCopilotHubLayout,
8
+ resetCopilotHubConfig,
8
9
  resolveCopilotHubHomeDir,
9
10
  resolveCopilotHubLayout,
10
11
  } from "../dist/install-layout.mjs";
@@ -45,8 +46,16 @@ test("initializeCopilotHubLayout migrates legacy env and data files once", () =>
45
46
  fs.mkdirSync(path.dirname(legacyControlEnvPath), { recursive: true });
46
47
  fs.mkdirSync(path.dirname(legacyEngineDataFile), { recursive: true });
47
48
  fs.mkdirSync(path.dirname(legacyPromptStatePath), { recursive: true });
48
- fs.writeFileSync(legacyEngineEnvPath, "TELEGRAM_TOKEN_AGENT_1=123:abc\n", "utf8");
49
- fs.writeFileSync(legacyControlEnvPath, "HUB_TELEGRAM_TOKEN=456:def\n", "utf8");
49
+ fs.writeFileSync(
50
+ legacyEngineEnvPath,
51
+ ["TELEGRAM_TOKEN_AGENT_1=123:abc", "BOT_REGISTRY_FILE=./data/bot-registry.json", ""].join("\n"),
52
+ "utf8",
53
+ );
54
+ fs.writeFileSync(
55
+ legacyControlEnvPath,
56
+ ["HUB_TELEGRAM_TOKEN=456:def", "HUB_DATA_DIR=./data/copilot_hub", ""].join("\n"),
57
+ "utf8",
58
+ );
50
59
  fs.writeFileSync(legacyEngineDataFile, '{"ok":true}\n', "utf8");
51
60
  fs.writeFileSync(legacyEngineLockPath, "stale-lock\n", "utf8");
52
61
  fs.writeFileSync(legacyPromptStatePath, '{"decision":"accepted"}\n', "utf8");
@@ -79,4 +88,47 @@ test("initializeCopilotHubLayout migrates legacy env and data files once", () =>
79
88
 
80
89
  const secondPass = initializeCopilotHubLayout({ repoRoot, layout });
81
90
  assert.deepEqual(secondPass.migratedPaths, []);
91
+ assert.deepEqual(secondPass.normalizedEnvPaths, []);
92
+ });
93
+
94
+ test("resetCopilotHubConfig removes persisted state but keeps the layout shell", () => {
95
+ const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "copilot-hub-reset-"));
96
+ const layout = resolveCopilotHubLayout({
97
+ repoRoot,
98
+ env: {
99
+ COPILOT_HUB_HOME_DIR: path.join(repoRoot, "user-home"),
100
+ },
101
+ homeDirectory: repoRoot,
102
+ });
103
+
104
+ initializeCopilotHubLayout({ repoRoot, layout });
105
+ fs.writeFileSync(layout.agentEngineEnvPath, "TELEGRAM_TOKEN_AGENT_1=123:abc\n", "utf8");
106
+ fs.mkdirSync(layout.agentEngineDataDir, { recursive: true });
107
+ fs.writeFileSync(
108
+ path.join(layout.agentEngineDataDir, "bot-registry.json"),
109
+ '{"version":3}\n',
110
+ "utf8",
111
+ );
112
+ fs.mkdirSync(path.join(layout.runtimeDir, "pids"), { recursive: true });
113
+ fs.writeFileSync(path.join(layout.runtimeDir, "pids", "daemon.json"), '{"pid":1}\n', "utf8");
114
+ fs.writeFileSync(layout.servicePromptStatePath, '{"decision":"accepted"}\n', "utf8");
115
+ fs.writeFileSync(
116
+ path.join(layout.runtimeDir, "windows-daemon-launcher.vbs"),
117
+ "' launcher\n",
118
+ "utf8",
119
+ );
120
+
121
+ const reset = resetCopilotHubConfig({ layout });
122
+
123
+ assert.ok(reset.removedPaths.includes(layout.configDir));
124
+ assert.ok(reset.removedPaths.includes(layout.dataDir));
125
+ assert.ok(reset.removedPaths.includes(layout.logsDir));
126
+ assert.ok(fs.existsSync(layout.configDir));
127
+ assert.ok(fs.existsSync(layout.dataDir));
128
+ assert.ok(fs.existsSync(layout.logsDir));
129
+ assert.equal(fs.existsSync(layout.agentEngineEnvPath), false);
130
+ assert.equal(fs.existsSync(path.join(layout.agentEngineDataDir, "bot-registry.json")), false);
131
+ assert.equal(fs.existsSync(path.join(layout.runtimeDir, "pids")), false);
132
+ assert.equal(fs.existsSync(layout.servicePromptStatePath), false);
133
+ assert.equal(fs.existsSync(path.join(layout.runtimeDir, "windows-daemon-launcher.vbs")), true);
82
134
  });