@cydm/happy-elves 0.1.0-beta.68 → 0.1.0-beta.69

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.
Files changed (55) hide show
  1. package/README.md +7 -1
  2. package/apps/cli/dist/commands/account.js +75 -36
  3. package/apps/cli/dist/commands/config.js +10 -2
  4. package/apps/cli/dist/commands/daemon.js +76 -48
  5. package/apps/cli/dist/commands/lib/args.js +1 -0
  6. package/apps/cli/dist/commands/lib/bootstrap-config.d.ts +26 -13
  7. package/apps/cli/dist/commands/lib/bootstrap-config.js +105 -79
  8. package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +6 -8
  9. package/apps/cli/dist/commands/lib/bootstrap-daemon.js +155 -23
  10. package/apps/cli/dist/commands/lib/bootstrap.js +230 -52
  11. package/apps/cli/dist/commands/lib/config.d.ts +29 -2
  12. package/apps/cli/dist/commands/lib/config.js +738 -16
  13. package/apps/cli/dist/commands/lib/doctor.d.ts +14 -0
  14. package/apps/cli/dist/commands/lib/doctor.js +301 -33
  15. package/apps/cli/dist/commands/lib/exit.js +27 -6
  16. package/apps/cli/dist/commands/lib/local-daemon.d.ts +41 -2
  17. package/apps/cli/dist/commands/lib/local-daemon.js +148 -22
  18. package/apps/cli/dist/commands/lib/paths.d.ts +1 -0
  19. package/apps/cli/dist/commands/lib/paths.js +1 -0
  20. package/apps/cli/dist/commands/lib/relay-http.d.ts +4 -5
  21. package/apps/cli/dist/commands/lib/relay-http.js +10 -29
  22. package/apps/cli/dist/commands/lib/usage.js +7 -2
  23. package/apps/cli/dist/commands/remote.js +73 -13
  24. package/apps/daemon/dist/account-config-lock.d.ts +14 -0
  25. package/apps/daemon/dist/account-config-lock.js +360 -0
  26. package/apps/daemon/dist/config.d.ts +1 -1
  27. package/apps/daemon/dist/config.js +60 -4
  28. package/apps/daemon/dist/gateway/runtime.js +39 -18
  29. package/apps/daemon/dist/pair.js +152 -21
  30. package/apps/daemon/dist/paths.d.ts +1 -0
  31. package/apps/daemon/dist/paths.js +1 -0
  32. package/apps/daemon/dist/process.d.ts +3 -0
  33. package/apps/daemon/dist/process.js +32 -2
  34. package/apps/daemon/dist/start.js +55 -6
  35. package/apps/daemon/package.json +1 -1
  36. package/apps/relay/dist/connections.d.ts +9 -1
  37. package/apps/relay/dist/controller-handlers.js +16 -13
  38. package/apps/relay/dist/machine-command-result-handlers.js +1 -0
  39. package/apps/relay/dist/machine-handler-context.d.ts +1 -0
  40. package/apps/relay/dist/relay-context.d.ts +1 -0
  41. package/apps/relay/dist/relay-context.js +44 -0
  42. package/apps/relay/dist/session-projection-reducer.js +14 -0
  43. package/apps/relay/dist/websocket.js +37 -23
  44. package/build-identity.json +2 -2
  45. package/npm-shrinkwrap.json +2 -2
  46. package/package.json +1 -1
  47. package/packages/client/dist/account.d.ts +3 -1
  48. package/packages/client/dist/account.js +4 -2
  49. package/packages/client/dist/client.d.ts +3 -1
  50. package/packages/client/dist/client.js +2 -2
  51. package/packages/client/dist/transport.js +55 -6
  52. package/packages/client/dist/types.d.ts +2 -0
  53. package/packages/shared/dist/node/daemon-process.d.ts +32 -0
  54. package/packages/shared/dist/node/daemon-process.js +85 -0
  55. package/packages/shared/dist/protocol.d.ts +2 -0
package/README.md CHANGED
@@ -5,7 +5,13 @@ Public beta CLI for Happy Elves.
5
5
  ## Hosted relay
6
6
 
7
7
  ```bash
8
- happy-elves account create --relay https://relay.happyelves.ai --device-name "CLI" --json
8
+ happy-elves start
9
+ ```
10
+
11
+ Existing workspace recovery uses a one-time invite from Settings → Browsers → Add:
12
+
13
+ ```bash
14
+ happy-elves remote join-controller --url "<invite-url>" --recover-controller --json
9
15
  ```
10
16
 
11
17
  ## Self-host relay
@@ -2,9 +2,10 @@ import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { createInterface } from "node:readline/promises";
5
- import { CliError, configDir, configPath, generateAccountSecret, ok, parsePairingStartResponse, randomId, readRelayJson, requireRelayUrl, stopLocalDaemon, throwRelayHttpError, writeConfig } from "./lib/index.js";
5
+ import { CliError, configDir, configPath, generateAccountSecret, ok, parsePairingStartResponse, randomId, readRelayJson, requireRelayUrl, stopLocalDaemon, throwRelayHttpError, withAccountConfigTransaction } from "./lib/index.js";
6
6
  const daemonConfigPath = path.join(configDir, "daemon.json");
7
7
  const accountConfigFiles = ["config.json", "daemon.json"];
8
+ const accountCreateRelayTimeoutMs = 15_000;
8
9
  function accountConfigTarget(file) {
9
10
  return file === "config.json" ? configPath : daemonConfigPath;
10
11
  }
@@ -83,8 +84,21 @@ async function copyConfigIfPresent(source, target) {
83
84
  }
84
85
  async function backupAccountConfig(prefix = "account-reset") {
85
86
  const stamp = new Date().toISOString().replace(/[:.]/gu, "-");
86
- const backupPath = path.join(configDir, "backups", `${prefix}-${stamp}`);
87
- await fs.mkdir(backupPath, { recursive: true, mode: 0o700 });
87
+ const backupsRoot = path.join(configDir, "backups");
88
+ try {
89
+ await fs.mkdir(backupsRoot, { mode: 0o700 });
90
+ }
91
+ catch (error) {
92
+ if (error.code !== "EEXIST")
93
+ throw error;
94
+ }
95
+ const backupsStat = await fs.lstat(backupsRoot);
96
+ if (!backupsStat.isDirectory() || backupsStat.isSymbolicLink()) {
97
+ throw new CliError("Refusing to write an account backup through an unsafe backups path.", "CONFIG_BACKUP_UNSAFE");
98
+ }
99
+ await fs.chmod(backupsRoot, 0o700);
100
+ const backupPath = path.join(backupsRoot, `${prefix}-${stamp}-${randomId("backup")}`);
101
+ await fs.mkdir(backupPath, { mode: 0o700 });
88
102
  await fs.chmod(backupPath, 0o700);
89
103
  const files = [];
90
104
  if (await copyConfigIfPresent(configPath, path.join(backupPath, "config.json")))
@@ -231,43 +245,65 @@ async function applyAccountRestore(staged) {
231
245
  }
232
246
  export async function handleAccount({ domain, action, flags }) {
233
247
  if (domain === "account" && action === "create") {
234
- const relayUrl = requireRelayUrl(flags);
235
- const deviceId = typeof flags["device-id"] === "string" ? flags["device-id"] : randomId("dev");
236
- const deviceName = typeof flags["device-name"] === "string" ? flags["device-name"] : os.hostname();
237
- const accountSecret = generateAccountSecret();
238
- const response = await fetch(`${relayUrl}/api/pairing/start`, {
239
- method: "POST",
240
- headers: { "content-type": "application/json" },
241
- body: JSON.stringify({ deviceId, deviceName }),
242
- });
243
- if (!response.ok) {
244
- await throwRelayHttpError(response, "Account creation failed");
245
- }
246
- const paired = parsePairingStartResponse(await readRelayJson(response, "Account creation response is not valid JSON"));
247
- const config = {
248
- relayUrl,
249
- controllerToken: paired.controllerToken,
250
- accountSecret,
251
- };
252
- await writeConfig(config);
253
- ok("account.create", {
254
- accountId: paired.accountId,
255
- configPath,
256
- relayUrl,
257
- deviceId,
258
- controllerToken: paired.controllerToken,
259
- accountSecret,
260
- pairingCode: paired.pairingCode,
261
- expiresAt: paired.expiresAt,
248
+ const created = await withAccountConfigTransaction(async (transaction) => {
249
+ // This check intentionally precedes every relay request. Holding the
250
+ // account-wide lease through pairing prevents concurrent creators from
251
+ // producing an orphan remote account and racing the local credential.
252
+ if (await transaction.controllerExists() || await transaction.daemonExists()) {
253
+ throw new CliError("Happy Elves account configuration already exists. Use an invite to recover the existing account; account create never replaces credentials.", "CONFIG_EXISTS");
254
+ }
255
+ const relayUrl = requireRelayUrl(flags);
256
+ const deviceId = typeof flags["device-id"] === "string" ? flags["device-id"] : randomId("dev");
257
+ const deviceName = typeof flags["device-name"] === "string" ? flags["device-name"] : os.hostname();
258
+ const accountSecret = generateAccountSecret();
259
+ let response;
260
+ try {
261
+ response = await fetch(`${relayUrl}/api/pairing/start`, {
262
+ method: "POST",
263
+ headers: { "content-type": "application/json" },
264
+ body: JSON.stringify({ deviceId, deviceName }),
265
+ signal: AbortSignal.timeout(accountCreateRelayTimeoutMs),
266
+ });
267
+ }
268
+ catch (error) {
269
+ if (error.name === "TimeoutError") {
270
+ throw new CliError("Timed out creating the account at the relay. No local configuration was written.", "RELAY_TIMEOUT");
271
+ }
272
+ throw error;
273
+ }
274
+ if (!response.ok) {
275
+ await throwRelayHttpError(response, "Account creation failed");
276
+ }
277
+ const paired = parsePairingStartResponse(await readRelayJson(response, "Account creation response is not valid JSON"));
278
+ const config = {
279
+ relayUrl,
280
+ controllerToken: paired.controllerToken,
281
+ accountSecret,
282
+ };
283
+ await transaction.writeController(config, "create");
284
+ return {
285
+ accountId: paired.accountId,
286
+ configPath,
287
+ relayUrl,
288
+ deviceId,
289
+ controllerToken: paired.controllerToken,
290
+ accountSecret,
291
+ pairingCode: paired.pairingCode,
292
+ expiresAt: paired.expiresAt,
293
+ };
262
294
  });
295
+ ok("account.create", created);
263
296
  return true;
264
297
  }
265
298
  if (domain === "account" && action === "reset") {
266
299
  await confirmReset(flags);
267
300
  const stop = await requireDaemonStopped();
268
- const backup = await backupAccountConfig();
269
- await fs.rm(configPath, { force: true });
270
- await fs.rm(daemonConfigPath, { force: true });
301
+ const backup = await withAccountConfigTransaction(async () => {
302
+ const result = await backupAccountConfig();
303
+ await fs.rm(configPath, { force: true });
304
+ await fs.rm(daemonConfigPath, { force: true });
305
+ return result;
306
+ });
271
307
  ok("account.reset", {
272
308
  removed: [configPath, daemonConfigPath],
273
309
  backup,
@@ -301,8 +337,11 @@ export async function handleAccount({ domain, action, flags }) {
301
337
  const existing = await currentAccountConfigFiles();
302
338
  await confirmRestore(flags, files.length === 0 || existing.length > 0);
303
339
  const stop = await requireDaemonStopped();
304
- const preRestoreBackup = await backupAccountConfig("account-pre-restore");
305
- const restored = await applyAccountRestore({ path: staged.path, files });
340
+ const { preRestoreBackup, restored } = await withAccountConfigTransaction(async () => {
341
+ const preRestoreBackup = await backupAccountConfig("account-pre-restore");
342
+ const restored = await applyAccountRestore({ path: staged.path, files });
343
+ return { preRestoreBackup, restored };
344
+ });
306
345
  ok("account.restore", {
307
346
  backupPath,
308
347
  ...restored,
@@ -1,4 +1,4 @@
1
- import { configPath, ok, readConfig, redactedControllerConfig, redactedRelayUrl, requireRelayUrl, requireString, wantsJson, writeConfig } from "./lib/index.js";
1
+ import { CliError, configPath, ok, readConfig, redactedControllerConfig, redactedRelayUrl, requireRelayUrl, requireString, wantsJson, withAccountConfigTransaction } from "./lib/index.js";
2
2
  export async function handleConfig({ domain, action, flags }) {
3
3
  if (domain === "config" && action === "init") {
4
4
  const config = {
@@ -6,7 +6,15 @@ export async function handleConfig({ domain, action, flags }) {
6
6
  controllerToken: requireString(flags, "token"),
7
7
  accountSecret: requireString(flags, "secret"),
8
8
  };
9
- await writeConfig(config);
9
+ await withAccountConfigTransaction(async (transaction) => {
10
+ if (await transaction.controllerExists()) {
11
+ throw new CliError("Controller configuration already exists. config init never replaces credentials.", "CONFIG_EXISTS");
12
+ }
13
+ if (await transaction.daemonExists()) {
14
+ throw new CliError("A daemon configuration already exists. Recover a controller with a same-account invite instead of creating an unverifiable split configuration.", "CONFIG_SPLIT_BRAIN");
15
+ }
16
+ await transaction.writeController(config, "create");
17
+ });
10
18
  ok("config.init", { path: configPath, relayUrl: redactedRelayUrl(config.relayUrl) });
11
19
  return true;
12
20
  }
@@ -1,6 +1,7 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
1
2
  import os from "node:os";
2
3
  import path from "node:path";
3
- import { assertLocalDaemonVerified, CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, wantsJson, writeDaemonConfig } from "./lib/index.js";
4
+ import { assertLocalDaemonVerified, cliErrorFromFetch, CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, diagnoseInstallation, emitDoctorResult, idleSessionStatuses, localDaemonStatus, normalizeRelayUrl, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, wantsJson, withAccountConfigTransaction } from "./lib/index.js";
4
5
  function positiveLimit(flags, fallback) {
5
6
  const value = flags.limit;
6
7
  if (value === undefined)
@@ -96,58 +97,57 @@ export async function handleDaemon({ domain, action, flags }) {
96
97
  const accountSecret = requireString(flags, "secret");
97
98
  const machineName = typeof flags.name === "string" && flags.name.trim() ? flags.name.trim() : os.hostname();
98
99
  const machineId = typeof flags["machine-id"] === "string" ? flags["machine-id"] : randomId("mach");
99
- const response = await fetch(`${relayUrl}/api/pairing/claim`, {
100
- method: "POST",
101
- headers: { "content-type": "application/json" },
102
- body: JSON.stringify({ code, machineId }),
100
+ let daemonConfig;
101
+ await withAccountConfigTransaction(async (transaction) => {
102
+ if (await transaction.daemonExists()) {
103
+ throw new CliError(`Daemon configuration already exists at ${transaction.daemonPath}. Pairing never replaces an existing machine identity; use an explicit account recovery or migration flow.`, "CONFIG_EXISTS");
104
+ }
105
+ const existingController = await transaction.readController();
106
+ if (existingController && (canonicalPairRelay(existingController.relayUrl) !== canonicalPairRelay(relayUrl) ||
107
+ !pairSecretsEqual(existingController.accountSecret, accountSecret))) {
108
+ throw new CliError("Daemon pairing does not match the existing controller relay/account. No relay request was sent and no configuration was changed.", "CONFIG_SPLIT_BRAIN");
109
+ }
110
+ let response;
111
+ try {
112
+ response = await fetch(`${relayUrl}/api/pairing/claim`, {
113
+ method: "POST",
114
+ headers: { "content-type": "application/json" },
115
+ body: JSON.stringify({ code, machineId }),
116
+ signal: AbortSignal.timeout(10_000),
117
+ });
118
+ }
119
+ catch (error) {
120
+ if (error.name === "TimeoutError" || error.name === "AbortError") {
121
+ throw new CliError("Pairing claim timed out; daemon configuration was not changed.", "PAIRING_TIMEOUT");
122
+ }
123
+ throw cliErrorFromFetch(error, "RELAY_UNREACHABLE", `Could not reach relay while pairing ${relayUrl}`);
124
+ }
125
+ if (!response.ok) {
126
+ await throwRelayHttpError(response, "Pairing failed");
127
+ }
128
+ const claimed = parsePairingClaimResponse(await readRelayJson(response, "Pairing response is not valid JSON"));
129
+ daemonConfig = {
130
+ relayUrl,
131
+ accountId: claimed.accountId,
132
+ machineId: claimed.machineId,
133
+ machineName,
134
+ machineToken: claimed.machineToken,
135
+ accountSecret,
136
+ };
137
+ await transaction.writeDaemon(daemonConfig, "create");
103
138
  });
104
- if (!response.ok) {
105
- await throwRelayHttpError(response, "Pairing failed");
106
- }
107
- const claimed = parsePairingClaimResponse(await readRelayJson(response, "Pairing response is not valid JSON"));
108
- const daemonConfig = {
109
- relayUrl,
110
- accountId: claimed.accountId,
111
- machineId: claimed.machineId,
112
- machineName,
113
- machineToken: claimed.machineToken,
114
- accountSecret,
115
- };
116
- const daemonConfigPath = await writeDaemonConfig(daemonConfig);
139
+ if (!daemonConfig)
140
+ throw new CliError("Pairing did not produce daemon configuration", "PAIRING_FAILED");
117
141
  ok("daemon.pair", {
118
- path: daemonConfigPath,
142
+ path: path.join(configDir, "daemon.json"),
119
143
  config: redactedDaemonConfig(daemonConfig),
120
144
  });
121
145
  return true;
122
146
  }
123
147
  if (domain === "daemon" && action === "doctor") {
124
- const checks = [];
125
- let config;
126
- try {
127
- config = await readConfig(flags);
128
- checks.push({ name: "config", ok: true, message: configPath });
129
- }
130
- catch (error) {
131
- checks.push({ name: "config", ok: false, message: error instanceof Error ? error.message : String(error) });
132
- }
133
- if (config) {
134
- try {
135
- const response = await fetch(`${config.relayUrl}/health`);
136
- checks.push({ name: "relay", ok: response.ok, message: response.ok ? "reachable" : await response.text() });
137
- }
138
- catch (error) {
139
- checks.push({ name: "relay", ok: false, message: error instanceof Error ? error.message : String(error) });
140
- }
141
- }
142
- const data = { ok: checks.every((check) => check.ok), checks };
143
- if (!wantsJson(flags)) {
144
- console.log(`Daemon doctor: ${data.ok ? "ok" : "issues found"}`);
145
- for (const check of checks) {
146
- console.log(`${check.ok ? "ok" : "fail"} ${check.name}: ${check.message}`);
147
- }
148
- return true;
149
- }
150
- ok("daemon.doctor", data);
148
+ const requestedRelay = typeof flags.relay === "string" ? flags.relay : undefined;
149
+ const data = await diagnoseInstallation("daemon", requestedRelay);
150
+ emitDoctorResult("daemon.doctor", "Daemon doctor", data, flags);
151
151
  return true;
152
152
  }
153
153
  if (domain === "daemon" && action === "config") {
@@ -203,13 +203,21 @@ export async function handleDaemon({ domain, action, flags }) {
203
203
  const status = await localDaemonStatus();
204
204
  assertLocalDaemonVerified(status);
205
205
  if (status.running) {
206
- ok("daemon.start", { ...status, started: false });
206
+ const data = { ...status, started: false };
207
+ if (wantsJson(flags))
208
+ ok("daemon.start", data);
209
+ else
210
+ writeHumanDaemonStartOutput(data);
207
211
  return true;
208
212
  }
209
213
  await readDaemonConfig();
210
214
  const startArgs = typeof flags.relay === "string" ? ["--relay", flags.relay] : [];
211
215
  const pid = spawnDaemonStart(startArgs);
212
- ok("daemon.start", { pid, pidPath: daemonPidPath, started: true });
216
+ const data = { pid, pidPath: daemonPidPath, started: true };
217
+ if (wantsJson(flags))
218
+ ok("daemon.start", data);
219
+ else
220
+ writeHumanDaemonStartOutput(data);
213
221
  return true;
214
222
  }
215
223
  if (domain === "daemon" && action === "stop") {
@@ -338,3 +346,23 @@ export async function handleDaemon({ domain, action, flags }) {
338
346
  }
339
347
  return false;
340
348
  }
349
+ function canonicalPairRelay(value) {
350
+ const url = new URL(value);
351
+ url.hash = "";
352
+ return normalizeRelayUrl(url.toString());
353
+ }
354
+ function pairSecretsEqual(left, right) {
355
+ const leftDigest = createHash("sha256").update(left).digest();
356
+ const rightDigest = createHash("sha256").update(right).digest();
357
+ return timingSafeEqual(leftDigest, rightDigest);
358
+ }
359
+ function writeHumanDaemonStartOutput(data) {
360
+ if (data.started) {
361
+ console.log(`Daemon start requested${data.pid ? ` (pid ${data.pid})` : ""}.`);
362
+ }
363
+ else {
364
+ console.log(`Daemon is already running${data.pid ? ` (pid ${data.pid})` : ""}.`);
365
+ }
366
+ console.log("This command manages only the paired background daemon; it does not open a browser page.");
367
+ console.log("For setup or browser onboarding, run: happy-elves start");
368
+ }
@@ -127,6 +127,7 @@ const booleanFlags = new Set([
127
127
  "repair-head",
128
128
  "reaction-disabled",
129
129
  "reaction-enabled",
130
+ "recover-controller",
130
131
  "require-mention",
131
132
  "respond-all",
132
133
  "summary",
@@ -1,28 +1,41 @@
1
1
  import type { CliConfig, DaemonConfig } from "./types.js";
2
- export type ConfigBackup = {
3
- path: string;
4
- backupPath: string;
5
- reason: string;
2
+ import { type AccountConfigTransaction } from "./config.js";
3
+ export type StartConfigPreflight = {
4
+ kind: "fresh" | "controller-only" | "ready";
5
+ relayUrl: string;
6
+ explicitRelay: boolean;
7
+ controller?: CliConfig;
8
+ daemon?: DaemonConfig;
6
9
  };
10
+ /**
11
+ * Read and classify both local identities before `start` performs any network
12
+ * request or mutates either config. A controller and daemon are one workspace
13
+ * only when both their relay and account secret match; `start` is deliberately
14
+ * not an account-migration command.
15
+ */
16
+ export declare function readStartConfigPreflight(flags: Record<string, string | boolean>, transaction?: AccountConfigTransaction): Promise<StartConfigPreflight>;
17
+ export declare function classifyStartConfig(params: {
18
+ controller?: CliConfig;
19
+ daemon?: DaemonConfig;
20
+ explicitRelayUrl?: string;
21
+ }): StartConfigPreflight;
7
22
  export declare function ensureControllerConfig(params: {
8
23
  relayUrl: string;
9
24
  explicitRelay: boolean;
10
25
  deviceName: string;
26
+ existing?: CliConfig;
27
+ transaction: AccountConfigTransaction;
28
+ deferWrite?: boolean;
29
+ signal?: AbortSignal;
11
30
  }): Promise<{
12
31
  config: CliConfig;
13
32
  created: boolean;
14
33
  accountId?: string;
15
- backup?: ConfigBackup;
16
- reconfigured: boolean;
34
+ reconfigured: false;
17
35
  }>;
18
- export declare function ensureDaemonPaired(config: CliConfig, machineName: string): Promise<{
36
+ export declare function ensureDaemonPaired(config: CliConfig, machineName: string, existing: DaemonConfig | undefined, transaction: AccountConfigTransaction, deferWrite?: boolean, signal?: AbortSignal, expectedAccountId?: string): Promise<{
19
37
  config: DaemonConfig;
20
38
  created: boolean;
21
- backup?: ConfigBackup;
22
- reconfigured: boolean;
39
+ reconfigured: false;
23
40
  }>;
24
41
  export declare function controllerBaseUrl(flags: Record<string, string | boolean>): string;
25
- export declare function startRelayUrl(flags: Record<string, string | boolean>, existing?: CliConfig): {
26
- relayUrl: string;
27
- explicit: boolean;
28
- };