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
  const __filename = fileURLToPath(import.meta.url);
9
10
  const __dirname = path.dirname(__filename);
@@ -15,15 +16,16 @@ const engineExamplePath = path.join(repoRoot, "apps", "agent-engine", ".env.exam
15
16
  const controlPlaneEnvPath = layout.controlPlaneEnvPath;
16
17
  const controlPlaneExamplePath = path.join(repoRoot, "apps", "control-plane", ".env.example");
17
18
  const TELEGRAM_TOKEN_PATTERN = /^\d{5,}:[A-Za-z0-9_-]{20,}$/;
18
- const DEFAULT_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN";
19
+ const DEFAULT_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN_FILE";
20
+ const LEGACY_CONTROL_PLANE_TOKEN_ENV = "HUB_TELEGRAM_TOKEN";
19
21
  const args = new Set(process.argv.slice(2));
20
22
  const requiredOnly = args.has("--required-only");
21
23
  await main();
22
24
  async function main() {
23
25
  ensureEnvFile(engineEnvPath, engineExamplePath);
24
26
  ensureEnvFile(controlPlaneEnvPath, controlPlaneExamplePath);
25
- const engineLines = readLines(engineEnvPath);
26
- const controlPlaneLines = readLines(controlPlaneEnvPath);
27
+ const engineLines = readEnvLines(engineEnvPath);
28
+ const controlPlaneLines = readEnvLines(controlPlaneEnvPath);
27
29
  const rl = createInterface({ input, output });
28
30
  try {
29
31
  if (requiredOnly) {
@@ -40,13 +42,11 @@ async function main() {
40
42
  finally {
41
43
  rl.close();
42
44
  }
43
- writeLines(engineEnvPath, engineLines);
44
- writeLines(controlPlaneEnvPath, controlPlaneLines);
45
+ writeEnvLines(engineEnvPath, engineLines);
46
+ writeEnvLines(controlPlaneEnvPath, controlPlaneLines);
45
47
  }
46
48
  async function configureRequiredTokens({ rl, controlPlaneLines }) {
47
- const controlPlaneMap = parseEnvMap(controlPlaneLines);
48
- const controlPlaneTokenEnvName = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, DEFAULT_CONTROL_PLANE_TOKEN_ENV);
49
- setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvName);
49
+ const controlPlaneTokenEnvName = migrateControlPlaneTokenEnv(controlPlaneLines);
50
50
  const postControlPlaneMap = parseEnvMap(controlPlaneLines);
51
51
  const currentToken = String(postControlPlaneMap[controlPlaneTokenEnvName] ?? "").trim();
52
52
  if (isUsableTelegramToken(currentToken)) {
@@ -62,10 +62,8 @@ async function configureRequiredTokens({ rl, controlPlaneLines }) {
62
62
  console.log("Required token saved.");
63
63
  }
64
64
  async function configureAll({ rl, controlPlaneLines }) {
65
- const controlPlaneMap = parseEnvMap(controlPlaneLines);
66
65
  console.log("\nCopilot Hub control-plane configuration\n");
67
- const controlPlaneTokenEnvDefault = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, DEFAULT_CONTROL_PLANE_TOKEN_ENV);
68
- setEnvValue(controlPlaneLines, "HUB_TELEGRAM_TOKEN_ENV", controlPlaneTokenEnvDefault);
66
+ const controlPlaneTokenEnvDefault = migrateControlPlaneTokenEnv(controlPlaneLines);
69
67
  const currentControlPlaneToken = String(parseEnvMap(controlPlaneLines)[controlPlaneTokenEnvDefault] ?? "").trim();
70
68
  const newControlPlaneToken = currentControlPlaneToken
71
69
  ? await askTelegramToken(rl, "Control-plane Telegram token (press Enter to keep current)", true)
@@ -77,6 +75,23 @@ async function configureAll({ rl, controlPlaneLines }) {
77
75
  console.log("- Control-plane token left unchanged.");
78
76
  }
79
77
  }
78
+ function migrateControlPlaneTokenEnv(lines) {
79
+ const controlPlaneMap = parseEnvMap(lines);
80
+ const configuredTokenEnvName = nonEmpty(controlPlaneMap.HUB_TELEGRAM_TOKEN_ENV, DEFAULT_CONTROL_PLANE_TOKEN_ENV);
81
+ const shouldMigrateLegacyName = configuredTokenEnvName === LEGACY_CONTROL_PLANE_TOKEN_ENV;
82
+ const nextTokenEnvName = shouldMigrateLegacyName
83
+ ? DEFAULT_CONTROL_PLANE_TOKEN_ENV
84
+ : configuredTokenEnvName;
85
+ setEnvValue(lines, "HUB_TELEGRAM_TOKEN_ENV", nextTokenEnvName);
86
+ if (shouldMigrateLegacyName) {
87
+ const legacyToken = String(controlPlaneMap[LEGACY_CONTROL_PLANE_TOKEN_ENV] ?? "").trim();
88
+ const dedicatedToken = String(controlPlaneMap[DEFAULT_CONTROL_PLANE_TOKEN_ENV] ?? "").trim();
89
+ if (legacyToken && !dedicatedToken) {
90
+ setEnvValue(lines, DEFAULT_CONTROL_PLANE_TOKEN_ENV, legacyToken);
91
+ }
92
+ }
93
+ return nextTokenEnvName;
94
+ }
80
95
  function ensureEnvFile(envPath, examplePath) {
81
96
  fs.mkdirSync(path.dirname(envPath), { recursive: true });
82
97
  if (fs.existsSync(envPath)) {
@@ -88,67 +103,10 @@ function ensureEnvFile(envPath, examplePath) {
88
103
  }
89
104
  fs.writeFileSync(envPath, "", "utf8");
90
105
  }
91
- function readLines(filePath) {
92
- const content = fs.readFileSync(filePath, "utf8");
93
- return content.split(/\r?\n/);
94
- }
95
- function writeLines(filePath, lines) {
96
- const normalized = [...lines];
97
- if (normalized.length === 0 || normalized[normalized.length - 1] !== "") {
98
- normalized.push("");
99
- }
100
- fs.writeFileSync(filePath, normalized.join("\n"), "utf8");
101
- }
102
- function parseEnvMap(lines) {
103
- const map = {};
104
- for (const line of lines) {
105
- const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
106
- if (!match) {
107
- continue;
108
- }
109
- const key = match[1];
110
- const value = unquote(match[2] ?? "");
111
- map[key] = value;
112
- }
113
- return map;
114
- }
115
- function setEnvValue(lines, key, value) {
116
- const safeValue = sanitizeValue(value);
117
- const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
118
- for (let index = 0; index < lines.length; index += 1) {
119
- if (!pattern.test(lines[index])) {
120
- continue;
121
- }
122
- lines[index] = `${key}=${safeValue}`;
123
- return;
124
- }
125
- if (lines.length > 0 && lines[lines.length - 1] !== "") {
126
- lines.push("");
127
- }
128
- lines.push(`${key}=${safeValue}`);
129
- }
130
- function sanitizeValue(value) {
131
- return String(value ?? "")
132
- .replace(/[\r\n]/g, "")
133
- .trim();
134
- }
135
- function unquote(value) {
136
- const raw = String(value ?? "").trim();
137
- if (!raw) {
138
- return "";
139
- }
140
- if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
141
- return raw.slice(1, -1);
142
- }
143
- return raw;
144
- }
145
106
  function nonEmpty(value, fallback) {
146
107
  const normalized = String(value ?? "").trim();
147
108
  return normalized || fallback;
148
109
  }
149
- function escapeRegex(value) {
150
- return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
151
- }
152
110
  async function askRequired(rl, label) {
153
111
  while (true) {
154
112
  const value = await rl.question(`${label}: `);
@@ -508,6 +508,23 @@ function detectFatalStartupError(ensureResult) {
508
508
  detectedAt: new Date().toISOString(),
509
509
  };
510
510
  }
511
+ const invalidHubTokenLine = findLineContaining(evidenceChunks, (line) => line.includes("hub telegram token in") && line.includes("is invalid"));
512
+ if (invalidHubTokenLine) {
513
+ return {
514
+ reason: invalidHubTokenLine,
515
+ action: "Run 'copilot-hub configure' to save a valid hub token in the control-plane config, then retry service.",
516
+ detectedAt: new Date().toISOString(),
517
+ };
518
+ }
519
+ const workspaceRootLine = findLineContaining(evidenceChunks, (line) => line.includes("default_workspace_root must be outside kernel directory") ||
520
+ line.includes("hub_workspace_root must be outside kernel directory"));
521
+ if (workspaceRootLine) {
522
+ return {
523
+ reason: workspaceRootLine,
524
+ action: "Set DEFAULT_WORKSPACE_ROOT to a folder outside the copilot-hub installation, then retry service.",
525
+ detectedAt: new Date().toISOString(),
526
+ };
527
+ }
511
528
  return null;
512
529
  }
513
530
  function readLogTail(filePath, maxLines = 120) {
@@ -0,0 +1,83 @@
1
+ import fs from "node:fs";
2
+ export function ensureEnvTextFile(filePath) {
3
+ if (fs.existsSync(filePath)) {
4
+ return;
5
+ }
6
+ fs.mkdirSync(requireParentDir(filePath), { recursive: true });
7
+ fs.writeFileSync(filePath, "", "utf8");
8
+ }
9
+ export function readEnvLines(filePath) {
10
+ const content = fs.readFileSync(filePath, "utf8");
11
+ return content.split(/\r?\n/);
12
+ }
13
+ export function writeEnvLines(filePath, lines) {
14
+ const normalized = [...lines];
15
+ if (normalized.length === 0 || normalized[normalized.length - 1] !== "") {
16
+ normalized.push("");
17
+ }
18
+ fs.writeFileSync(filePath, normalized.join("\n"), "utf8");
19
+ }
20
+ export function parseEnvMap(lines) {
21
+ const map = {};
22
+ for (const line of lines) {
23
+ const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
24
+ if (!match) {
25
+ continue;
26
+ }
27
+ const key = match[1];
28
+ const value = unquote(match[2] ?? "");
29
+ map[key] = value;
30
+ }
31
+ return map;
32
+ }
33
+ export function setEnvValue(lines, key, value) {
34
+ const safeValue = sanitizeEnvValue(value);
35
+ const pattern = new RegExp(`^\\s*${escapeRegex(key)}\\s*=`);
36
+ for (let index = 0; index < lines.length; index += 1) {
37
+ if (!pattern.test(lines[index])) {
38
+ continue;
39
+ }
40
+ lines[index] = `${key}=${safeValue}`;
41
+ return;
42
+ }
43
+ if (lines.length > 0 && lines[lines.length - 1] !== "") {
44
+ lines.push("");
45
+ }
46
+ lines.push(`${key}=${safeValue}`);
47
+ }
48
+ export function removeEnvKeys(lines, keys) {
49
+ const patterns = keys.map((key) => new RegExp(`^\\s*${escapeRegex(key)}\\s*=`));
50
+ const originalLength = lines.length;
51
+ const kept = lines.filter((line) => !patterns.some((pattern) => pattern.test(line)));
52
+ if (kept.length === originalLength) {
53
+ return false;
54
+ }
55
+ lines.splice(0, lines.length, ...kept);
56
+ return true;
57
+ }
58
+ export function sanitizeEnvValue(value) {
59
+ return String(value ?? "")
60
+ .replace(/[\r\n]/g, "")
61
+ .trim();
62
+ }
63
+ function unquote(value) {
64
+ const raw = String(value ?? "").trim();
65
+ if (!raw) {
66
+ return "";
67
+ }
68
+ if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
69
+ return raw.slice(1, -1);
70
+ }
71
+ return raw;
72
+ }
73
+ function escapeRegex(value) {
74
+ return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
75
+ }
76
+ function requireParentDir(filePath) {
77
+ const parts = String(filePath ?? "").split(/[\\/]/);
78
+ if (parts.length <= 1) {
79
+ return ".";
80
+ }
81
+ parts.pop();
82
+ return parts.join("/") || ".";
83
+ }
@@ -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
  export function resolveCopilotHubLayout({ repoRoot, env = process.env, platform = process.platform, homeDirectory = os.homedir(), }) {
6
8
  const pathApi = getPathApi(platform);
7
9
  const homeDir = resolveCopilotHubHomeDir({
@@ -30,7 +32,35 @@ export function resolveCopilotHubLayout({ repoRoot, env = process.env, platform
30
32
  export function initializeCopilotHubLayout({ repoRoot, layout, }) {
31
33
  ensureCopilotHubLayout(layout);
32
34
  const migratedPaths = migrateLegacyLayout({ repoRoot, layout });
33
- return { migratedPaths };
35
+ const normalizedEnvPaths = normalizePersistentEnvFiles(layout);
36
+ return { migratedPaths, normalizedEnvPaths };
37
+ }
38
+ export function resetCopilotHubConfig({ layout }) {
39
+ const removedPaths = [];
40
+ for (const target of [layout.configDir, layout.dataDir, layout.logsDir]) {
41
+ if (!fs.existsSync(target)) {
42
+ continue;
43
+ }
44
+ fs.rmSync(target, { recursive: true, force: true });
45
+ removedPaths.push(target);
46
+ }
47
+ const runtimeTargets = [
48
+ path.join(layout.runtimeDir, "pids"),
49
+ path.join(layout.runtimeDir, "services"),
50
+ path.join(layout.runtimeDir, "last-startup-error.json"),
51
+ layout.servicePromptStatePath,
52
+ ];
53
+ for (const target of runtimeTargets) {
54
+ if (!fs.existsSync(target)) {
55
+ continue;
56
+ }
57
+ fs.rmSync(target, { recursive: true, force: true });
58
+ removedPaths.push(target);
59
+ }
60
+ ensureCopilotHubLayout(layout);
61
+ return {
62
+ removedPaths: removedPaths.sort(),
63
+ };
34
64
  }
35
65
  export function ensureCopilotHubLayout(layout) {
36
66
  fs.mkdirSync(layout.homeDir, { recursive: true });
@@ -81,6 +111,98 @@ function migrateLegacyLayout({ repoRoot, layout, }) {
81
111
  }
82
112
  return migratedPaths;
83
113
  }
114
+ function normalizePersistentEnvFiles(layout) {
115
+ const normalizedPaths = [];
116
+ if (normalizePersistentEnvFile(layout.agentEngineEnvPath, [
117
+ {
118
+ key: "BOT_DATA_DIR",
119
+ legacyValues: ["./data"],
120
+ wrongResolvedPath: path.join(layout.configDir, "data"),
121
+ },
122
+ {
123
+ key: "BOT_REGISTRY_FILE",
124
+ legacyValues: ["./data/bot-registry.json"],
125
+ wrongResolvedPath: path.join(layout.configDir, "data", "bot-registry.json"),
126
+ },
127
+ {
128
+ key: "SECRET_STORE_FILE",
129
+ legacyValues: ["./data/secrets.json"],
130
+ wrongResolvedPath: path.join(layout.configDir, "data", "secrets.json"),
131
+ },
132
+ {
133
+ key: "INSTANCE_LOCK_FILE",
134
+ legacyValues: ["./data/runtime.lock"],
135
+ wrongResolvedPath: path.join(layout.configDir, "data", "runtime.lock"),
136
+ },
137
+ ])) {
138
+ normalizedPaths.push(layout.agentEngineEnvPath);
139
+ }
140
+ if (normalizePersistentEnvFile(layout.controlPlaneEnvPath, [
141
+ {
142
+ key: "BOT_DATA_DIR",
143
+ legacyValues: ["./data"],
144
+ wrongResolvedPath: path.join(layout.configDir, "data"),
145
+ },
146
+ {
147
+ key: "BOT_REGISTRY_FILE",
148
+ legacyValues: ["./data/bot-registry.json"],
149
+ wrongResolvedPath: path.join(layout.configDir, "data", "bot-registry.json"),
150
+ },
151
+ {
152
+ key: "SECRET_STORE_FILE",
153
+ legacyValues: ["./data/secrets.json"],
154
+ wrongResolvedPath: path.join(layout.configDir, "data", "secrets.json"),
155
+ },
156
+ {
157
+ key: "INSTANCE_LOCK_FILE",
158
+ legacyValues: ["./data/runtime.lock"],
159
+ wrongResolvedPath: path.join(layout.configDir, "data", "runtime.lock"),
160
+ },
161
+ {
162
+ key: "HUB_DATA_DIR",
163
+ legacyValues: ["./data/copilot_hub"],
164
+ wrongResolvedPath: path.join(layout.configDir, "data", "copilot_hub"),
165
+ },
166
+ ])) {
167
+ normalizedPaths.push(layout.controlPlaneEnvPath);
168
+ }
169
+ return normalizedPaths.sort();
170
+ }
171
+ function normalizePersistentEnvFile(filePath, rules) {
172
+ if (!fs.existsSync(filePath)) {
173
+ return false;
174
+ }
175
+ const lines = readEnvLines(filePath);
176
+ const envMap = parseEnvMap(lines);
177
+ const keysToRemove = rules
178
+ .filter((rule) => shouldRemoveLegacyManagedPath(envMap[rule.key], {
179
+ legacyValues: rule.legacyValues,
180
+ wrongResolvedPath: rule.wrongResolvedPath,
181
+ configBaseDir: path.dirname(filePath),
182
+ }))
183
+ .map((rule) => rule.key);
184
+ if (keysToRemove.length === 0) {
185
+ return false;
186
+ }
187
+ removeEnvKeys(lines, keysToRemove);
188
+ writeEnvLines(filePath, lines);
189
+ return true;
190
+ }
191
+ function shouldRemoveLegacyManagedPath(rawValue, { legacyValues, wrongResolvedPath, configBaseDir, }) {
192
+ const value = String(rawValue ?? "").trim();
193
+ if (!value) {
194
+ return false;
195
+ }
196
+ const normalizedValue = normalizeForCompare(value);
197
+ if (legacyValues.some((entry) => normalizeForCompare(entry) === normalizedValue)) {
198
+ return true;
199
+ }
200
+ if (path.isAbsolute(value)) {
201
+ return normalizeForCompare(value) === normalizeForCompare(wrongResolvedPath);
202
+ }
203
+ return (normalizeForCompare(path.resolve(configBaseDir, value)) ===
204
+ normalizeForCompare(wrongResolvedPath));
205
+ }
84
206
  function resolveLegacyPaths(repoRoot) {
85
207
  return {
86
208
  agentEngineEnvPath: path.join(repoRoot, "apps", "agent-engine", ".env"),
@@ -129,6 +251,13 @@ function normalizePath(value, pathApi) {
129
251
  const normalized = String(value ?? "").trim();
130
252
  return normalized ? pathApi.resolve(normalized) : "";
131
253
  }
254
+ function normalizeForCompare(value) {
255
+ const normalized = String(value ?? "").trim();
256
+ if (!normalized) {
257
+ return "";
258
+ }
259
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
260
+ }
132
261
  function getPathApi(platform) {
133
262
  return platform === "win32" ? path.win32 : path.posix;
134
263
  }
@@ -307,6 +307,11 @@ function buildServiceEnvironment(service) {
307
307
  BOT_REGISTRY_FILE: service.botRegistryFilePath,
308
308
  SECRET_STORE_FILE: service.secretStoreFilePath,
309
309
  INSTANCE_LOCK_FILE: service.instanceLockFilePath,
310
+ ...(service.id === "control-plane"
311
+ ? {
312
+ HUB_DATA_DIR: path.join(service.dataDir, "copilot_hub"),
313
+ }
314
+ : {}),
310
315
  };
311
316
  }
312
317
  function printTail(filePath, lines) {
@@ -7,7 +7,11 @@ import { createInterface } from "node:readline/promises";
7
7
  import { fileURLToPath } from "node:url";
8
8
  import { spawnCodexSync } from "./codex-spawn.mjs";
9
9
  import { codexInstallPackageSpec } from "./codex-version.mjs";
10
- import { initializeCopilotHubLayout, resolveCopilotHubLayout } from "./install-layout.mjs";
10
+ import {
11
+ initializeCopilotHubLayout,
12
+ resetCopilotHubConfig,
13
+ resolveCopilotHubLayout,
14
+ } from "./install-layout.mjs";
11
15
  import {
12
16
  buildCodexCompatibilityError,
13
17
  buildCodexCompatibilityNotice,
@@ -35,6 +39,7 @@ const rawArgs = process.argv
35
39
  .filter(Boolean);
36
40
  const wantsVersion = rawArgs.includes("--version") || rawArgs.includes("-v");
37
41
  const wantsHelp = rawArgs.includes("--help") || rawArgs.includes("-h");
42
+ const wantsYes = rawArgs.includes("--yes") || rawArgs.includes("-y");
38
43
 
39
44
  const action = String(rawArgs[0] ?? "start")
40
45
  .trim()
@@ -106,6 +111,13 @@ async function main() {
106
111
  runNode(["scripts/dist/configure.mjs"]);
107
112
  return;
108
113
  }
114
+ case "reset-config":
115
+ case "reset_config": {
116
+ await resetConfig({
117
+ force: wantsYes,
118
+ });
119
+ return;
120
+ }
109
121
  case "service": {
110
122
  const serviceAction = String(rawArgs[1] ?? "")
111
123
  .trim()
@@ -126,6 +138,56 @@ async function main() {
126
138
  }
127
139
  }
128
140
 
141
+ async function resetConfig({ force }: { force: boolean }): Promise<void> {
142
+ if (!force) {
143
+ if (!process.stdin.isTTY) {
144
+ throw new Error(
145
+ "reset-config requires confirmation. Re-run with '--yes' in non-interactive mode.",
146
+ );
147
+ }
148
+
149
+ const rl = createInterface({ input, output });
150
+ try {
151
+ const confirmed = await askYesNo(
152
+ rl,
153
+ [
154
+ "Reset Copilot Hub config and runtime state?",
155
+ "This removes persisted config, bot registry, secrets, logs, and runtime state.",
156
+ "Agent workspaces are kept.",
157
+ ].join(" "),
158
+ false,
159
+ );
160
+ if (!confirmed) {
161
+ console.log("Reset canceled.");
162
+ return;
163
+ }
164
+ } finally {
165
+ rl.close();
166
+ }
167
+ }
168
+
169
+ if (isServiceAlreadyInstalled()) {
170
+ runNodeCapture(["scripts/dist/service.mjs", "stop"], "inherit");
171
+ } else {
172
+ runNodeCapture(["scripts/dist/supervisor.mjs", "down"], "inherit");
173
+ }
174
+
175
+ const reset = resetCopilotHubConfig({ layout });
176
+ initializeCopilotHubLayout({ repoRoot, layout });
177
+
178
+ console.log("Copilot Hub config reset completed.");
179
+ if (reset.removedPaths.length > 0) {
180
+ console.log("Removed:");
181
+ for (const removedPath of reset.removedPaths) {
182
+ console.log(`- ${removedPath}`);
183
+ }
184
+ }
185
+ console.log("Kept:");
186
+ console.log("- package installation");
187
+ console.log("- external workspaces (for example Desktop/copilot_workspaces)");
188
+ console.log("Next step: run 'copilot-hub configure' then 'copilot-hub start'.");
189
+ }
190
+
129
191
  function runNode(scriptArgs) {
130
192
  const result = runNodeCapture(scriptArgs, "inherit");
131
193
  const code = Number.isInteger(result.status) ? result.status : 1;
@@ -577,6 +639,8 @@ function printUsage() {
577
639
  console.log(
578
640
  [
579
641
  "Usage: node scripts/dist/cli.mjs <start|stop|restart|status|logs|configure|service|version|help>",
642
+ "Reset persistent state:",
643
+ " node scripts/dist/cli.mjs reset-config [--yes]",
580
644
  "Service management:",
581
645
  " node scripts/dist/cli.mjs service <install|uninstall|status|start|stop|help>",
582
646
  ].join("\n"),