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.
- package/apps/agent-engine/.env.example +6 -8
- package/apps/agent-engine/dist/config.js +18 -17
- package/apps/control-plane/.env.example +4 -4
- package/apps/control-plane/dist/config.js +19 -17
- package/apps/control-plane/dist/copilot-hub.js +16 -4
- package/apps/control-plane/dist/hub-token-config.js +37 -0
- package/apps/control-plane/dist/test/hub-token-config.test.js +46 -0
- package/package.json +2 -1
- package/packages/core/dist/env-config.d.ts +11 -0
- package/packages/core/dist/env-config.js +70 -0
- package/packages/core/dist/env-config.js.map +1 -0
- package/packages/core/dist/index.d.ts +2 -1
- package/packages/core/dist/index.js +2 -1
- package/packages/core/dist/index.js.map +1 -1
- package/packages/core/dist/workspace-paths.d.ts +12 -1
- package/packages/core/dist/workspace-paths.js +30 -6
- package/packages/core/dist/workspace-paths.js.map +1 -1
- package/packages/core/package.json +4 -0
- package/scripts/dist/cli.mjs +52 -1
- package/scripts/dist/configure.mjs +26 -68
- package/scripts/dist/daemon.mjs +17 -0
- package/scripts/dist/env-file-utils.mjs +83 -0
- package/scripts/dist/install-layout.mjs +130 -1
- package/scripts/dist/supervisor.mjs +5 -0
- package/scripts/src/cli.mts +65 -1
- package/scripts/src/configure.mts +32 -88
- package/scripts/src/daemon.mts +28 -0
- package/scripts/src/env-file-utils.mts +98 -0
- package/scripts/src/install-layout.mts +172 -2
- package/scripts/src/supervisor.mts +5 -0
- package/scripts/test/install-layout.test.mjs +54 -2
|
@@ -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 = "
|
|
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 =
|
|
33
|
-
const controlPlaneLines =
|
|
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
|
-
|
|
52
|
-
|
|
53
|
+
writeEnvLines(engineEnvPath, engineLines);
|
|
54
|
+
writeEnvLines(controlPlaneEnvPath, controlPlaneLines);
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
async function configureRequiredTokens({ rl, controlPlaneLines }) {
|
|
56
|
-
const
|
|
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 =
|
|
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}: `);
|
package/scripts/src/daemon.mts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
49
|
-
|
|
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
|
});
|