@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.
- package/README.md +7 -1
- package/apps/cli/dist/commands/account.js +75 -36
- package/apps/cli/dist/commands/config.js +10 -2
- package/apps/cli/dist/commands/daemon.js +76 -48
- package/apps/cli/dist/commands/lib/args.js +1 -0
- package/apps/cli/dist/commands/lib/bootstrap-config.d.ts +26 -13
- package/apps/cli/dist/commands/lib/bootstrap-config.js +105 -79
- package/apps/cli/dist/commands/lib/bootstrap-daemon.d.ts +6 -8
- package/apps/cli/dist/commands/lib/bootstrap-daemon.js +155 -23
- package/apps/cli/dist/commands/lib/bootstrap.js +230 -52
- package/apps/cli/dist/commands/lib/config.d.ts +29 -2
- package/apps/cli/dist/commands/lib/config.js +738 -16
- package/apps/cli/dist/commands/lib/doctor.d.ts +14 -0
- package/apps/cli/dist/commands/lib/doctor.js +301 -33
- package/apps/cli/dist/commands/lib/exit.js +27 -6
- package/apps/cli/dist/commands/lib/local-daemon.d.ts +41 -2
- package/apps/cli/dist/commands/lib/local-daemon.js +148 -22
- package/apps/cli/dist/commands/lib/paths.d.ts +1 -0
- package/apps/cli/dist/commands/lib/paths.js +1 -0
- package/apps/cli/dist/commands/lib/relay-http.d.ts +4 -5
- package/apps/cli/dist/commands/lib/relay-http.js +10 -29
- package/apps/cli/dist/commands/lib/usage.js +7 -2
- package/apps/cli/dist/commands/remote.js +73 -13
- package/apps/daemon/dist/account-config-lock.d.ts +14 -0
- package/apps/daemon/dist/account-config-lock.js +360 -0
- package/apps/daemon/dist/config.d.ts +1 -1
- package/apps/daemon/dist/config.js +60 -4
- package/apps/daemon/dist/gateway/runtime.js +39 -18
- package/apps/daemon/dist/pair.js +152 -21
- package/apps/daemon/dist/paths.d.ts +1 -0
- package/apps/daemon/dist/paths.js +1 -0
- package/apps/daemon/dist/process.d.ts +3 -0
- package/apps/daemon/dist/process.js +32 -2
- package/apps/daemon/dist/start.js +55 -6
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/connections.d.ts +9 -1
- package/apps/relay/dist/controller-handlers.js +16 -13
- package/apps/relay/dist/machine-command-result-handlers.js +1 -0
- package/apps/relay/dist/machine-handler-context.d.ts +1 -0
- package/apps/relay/dist/relay-context.d.ts +1 -0
- package/apps/relay/dist/relay-context.js +44 -0
- package/apps/relay/dist/session-projection-reducer.js +14 -0
- package/apps/relay/dist/websocket.js +37 -23
- package/build-identity.json +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/packages/client/dist/account.d.ts +3 -1
- package/packages/client/dist/account.js +4 -2
- package/packages/client/dist/client.d.ts +3 -1
- package/packages/client/dist/client.js +2 -2
- package/packages/client/dist/transport.js +55 -6
- package/packages/client/dist/types.d.ts +2 -0
- package/packages/shared/dist/node/daemon-process.d.ts +32 -0
- package/packages/shared/dist/node/daemon-process.js +85 -0
- 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
|
|
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,
|
|
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
|
|
87
|
-
|
|
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
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
305
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 (!
|
|
105
|
-
|
|
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:
|
|
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
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -1,28 +1,41 @@
|
|
|
1
1
|
import type { CliConfig, DaemonConfig } from "./types.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
};
|