@cabane/companion 0.6.237 → 0.6.238

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.
@@ -1,591 +0,0 @@
1
- // src/cabane-command.ts
2
- import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
3
- import { homedir as homedir4 } from "os";
4
- import { basename as basename2, delimiter, dirname as dirname2, join as join3, sep as sep2 } from "path";
5
-
6
- // src/install-kind.ts
7
- import { realpathSync } from "fs";
8
- import { homedir } from "os";
9
- import { dirname, join, sep } from "path";
10
- import { fileURLToPath } from "url";
11
- function installLineWords(install = companionInstall()) {
12
- const line = install === "script" ? "installer line" : "npm line";
13
- return `the ${line} from Your settings \u2192 Devices \u2192 Add a device in your Cabane`;
14
- }
15
- function companionInstall(execPath = process.execPath, home = homedir()) {
16
- const real = (path) => {
17
- try {
18
- return realpathSync(path);
19
- } catch {
20
- return path;
21
- }
22
- };
23
- const scriptNode = join(real(home), ".cabane", "node") + sep;
24
- return real(execPath).startsWith(scriptNode) ? "script" : "npm";
25
- }
26
-
27
- // src/cli-entry.ts
28
- import { existsSync } from "fs";
29
- import { fileURLToPath as fileURLToPath2 } from "url";
30
-
31
- // src/install-record.ts
32
- import { existsSync as existsSync2, readFileSync } from "fs";
33
- import { homedir as homedir2 } from "os";
34
- import { basename, join as join2 } from "path";
35
-
36
- // src/term.ts
37
- import { homedir as homedir3 } from "os";
38
- import pc from "picocolors";
39
-
40
- // src/cabane-command.ts
41
- var VERSIONS_SEGMENT = `${sep2}.cabane${sep2}versions${sep2}`;
42
- function cabane(rest) {
43
- return `cabane ${rest}`;
44
- }
45
-
46
- // src/config.ts
47
- import {
48
- chmodSync,
49
- mkdirSync,
50
- readFileSync as readFileSync2,
51
- renameSync,
52
- rmSync,
53
- writeFileSync,
54
- existsSync as existsSync3
55
- } from "fs";
56
- import { homedir as homedir5, userInfo } from "os";
57
- import { dirname as dirname3, isAbsolute, join as join4 } from "path";
58
- import { z as z2 } from "zod";
59
-
60
- // src/errors.ts
61
- var CompanionError = class extends Error {
62
- constructor(message) {
63
- super(message);
64
- this.name = "CompanionError";
65
- }
66
- };
67
- var StandaloneError = class extends CompanionError {
68
- constructor(message) {
69
- super(message);
70
- this.name = "StandaloneError";
71
- }
72
- };
73
- var ApiError = class extends CompanionError {
74
- constructor(status, message, body) {
75
- super(message);
76
- this.status = status;
77
- this.body = body;
78
- this.name = "ApiError";
79
- }
80
- status;
81
- body;
82
- };
83
- var ConfigError = class extends CompanionError {
84
- constructor(message) {
85
- super(message);
86
- this.name = "ConfigError";
87
- }
88
- };
89
-
90
- // src/pairing.ts
91
- function isAllowedBaseUrl(raw) {
92
- let url;
93
- try {
94
- url = new URL(raw);
95
- } catch {
96
- return false;
97
- }
98
- if (url.protocol === "https:") return true;
99
- if (url.protocol === "http:") {
100
- const host = url.hostname.toLowerCase();
101
- return host === "localhost" || host.endsWith(".localhost") || host === "127.0.0.1" || host === "::1" || host === "[::1]";
102
- }
103
- return false;
104
- }
105
-
106
- // src/prepare-hook.ts
107
- import { spawn } from "child_process";
108
- import { z } from "zod";
109
- var prepareHookSchema = z.object({
110
- command: z.string().min(1),
111
- args: z.array(z.string()).optional(),
112
- // Extra env handed to the hook process itself (merged over process.env).
113
- env: z.record(z.string(), z.string()).optional(),
114
- // Wall-clock cap for the hook. Provisioning is slow (minutes), so the
115
- // default is generous; a hook that hangs past this is killed and the turn
116
- // fails with a clear timeout message rather than pinning the companion.
117
- timeoutMs: z.number().int().positive().optional()
118
- }).strict();
119
- var DEFAULT_TIMEOUT_MS = 10 * 6e4;
120
- var prepareResultSchema = z.object({
121
- cwd: z.string().min(1),
122
- env: z.record(z.string(), z.string()).optional()
123
- });
124
-
125
- // src/config.ts
126
- var HARNESS_RUNTIMES = ["claude-code", "codex", "opencode"];
127
- var realHomeCache;
128
- function realAccountHome() {
129
- if (realHomeCache === void 0) {
130
- try {
131
- realHomeCache = userInfo().homedir;
132
- } catch {
133
- realHomeCache = null;
134
- }
135
- }
136
- return realHomeCache;
137
- }
138
- function cabaneDir() {
139
- const dir = join4(process.env.CABANE_COMPANION_HOME || homedir5(), ".cabane");
140
- if (process.env.VITEST) {
141
- const real = realAccountHome();
142
- if (real && dir === join4(real, ".cabane")) {
143
- throw new Error(
144
- `cabaneDir() resolved to the real ${dir} during a test run. Companion tests must swap process.env.HOME to a tmp dir before touching the config dir; this guard prevents wiping the operator's real companion config (see apps/companion/test/setup-home.ts).`
145
- );
146
- }
147
- }
148
- return dir;
149
- }
150
- function configPath() {
151
- return join4(cabaneDir(), "config.json");
152
- }
153
- var localAgentConfigSchema = z2.object({
154
- cwd: z2.string().optional(),
155
- prepareHook: prepareHookSchema.optional(),
156
- // The Claude Code auto-memory escape hatch. Auto-memory is forced OFF by
157
- // default (memory belongs in the Cabane workspace, and a shared companion
158
- // would pool one cwd-keyed memory dir across users). On a companion you run
159
- // yourself, `claudeCode: { autoMemory: true }` hands auto-memory back to your
160
- // own `~/.claude/settings.json` — Cabane then stops
161
- // injecting the off switch and your normal Claude Code memory workflow applies
162
- // (the user and project settings load on every Claude Code turn).
163
- claudeCode: z2.object({ autoMemory: z2.boolean().optional() }).strict().optional()
164
- }).strict();
165
- var agentsConfigSchema = z2.record(z2.string(), localAgentConfigSchema);
166
- function isRootPath(value) {
167
- return isAbsolute(value) || value === "~" || value.startsWith("~/");
168
- }
169
- var companionConfigSchema = z2.object({
170
- // https-enforced (loopback exempt), so a hand-edited config can't put the
171
- // device token and prompt on a plaintext channel.
172
- baseUrl: z2.string().url().refine(isAllowedBaseUrl, {
173
- message: "baseUrl must be https (loopback http is allowed for local dev only)"
174
- }),
175
- // The `cabdev_` device token plaintext — the companion's one durable credential,
176
- // presented as `Authorization: Bearer <token>` on the device-facing pull +
177
- // heartbeat endpoints. Optional so `cabane-companion logout` can strip it (a
178
- // "paired but logged out" state the supervisor refuses to run) while keeping
179
- // the rest of the config; `pair` always writes one.
180
- deviceToken: z2.string().optional(),
181
- // Identity hints, learned from the device flow and refreshed on the first
182
- // assignments pull. Cosmetic — used for `status`/dashboard display only.
183
- deviceId: z2.string().optional(),
184
- deviceLabel: z2.string().optional(),
185
- // Optional per-agent machine-local overrides (cwd / prepareHook), keyed by
186
- // agentId / username / `slug/username`. Hand-added by the operator; the companion
187
- // never writes this (it only persists credentials + the device, elsewhere).
188
- agents: agentsConfigSchema.optional(),
189
- // The DEVICE-LEVEL prepare hook, for every agent assigned here unless its own
190
- // `agents` row names one. Per-agent hooks alone make the config a second
191
- // silent roster: an agent nobody listed gets no hook and no error, and starts
192
- // in the operator's `$HOME`.
193
- prepareHook: prepareHookSchema.optional(),
194
- // The CLI ignores both dashboard settings. Desktop uses `dashboardPort`;
195
- // `autoOpen` remains parseable for older config files but no longer controls
196
- // startup. `logLevel` is pino's level, live-editable.
197
- dashboardPort: z2.number().int().min(1).max(65535).optional(),
198
- autoOpen: z2.boolean().optional(),
199
- logLevel: z2.enum(["warn", "info", "debug"]).optional(),
200
- logFormat: z2.enum(["human", "json"]).optional(),
201
- // Where this device's `opencode serve` answers, when it isn't the default
202
- // address (`DEFAULT_OPENCODE_SERVER_URL`). Detection probes this one; a serve
203
- // that answers is connected. Auth is opencode's own; Cabane never sees
204
- // provider keys.
205
- opencode: z2.object({
206
- serverUrl: z2.string().url()
207
- }).strict().optional(),
208
- // The one knob on agents: an allowlist. Agents are detected, never offered
209
- // (`harness-detect.ts`): whatever is signed in here is connected. Someone with
210
- // Codex on the device who doesn't want Cabane to see it lists the ones it may:
211
- // `"harnesses": ["claude-code"]`. Absent means all three.
212
- harnesses: z2.array(z2.enum(HARNESS_RUNTIMES)).optional(),
213
- // Whether this companion keeps itself current: installs the version Cabane names, between
214
- // turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
215
- // which `start` offers; off either way until someone says yes.
216
- update: z2.object({
217
- auto: z2.boolean()
218
- }).strict().optional(),
219
- // The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
220
- // `cabane service install` still installs it.
221
- serviceDeclined: z2.boolean().optional(),
222
- // Where agents stand on this device. Every (agent, conversation) pair gets a directory under it,
223
- // and an agent with no folder of its own and no prepare hook works there
224
- // (`turn-execution.ts`, `prepareEnvironment`). Default `~/.cabane/work`: hidden, so pairing a
225
- // laptop creates no new visible folder; set it to make the work visible.
226
- root: z2.string().min(1).refine(isRootPath, { message: "must be an absolute path, or start with ~/" }).optional(),
227
- // An argv prefix the operator puts in front of every turn's runner: the companion starts
228
- // `<prefix…> <its own run-turn command…>` instead of the runner bare. What the prefix does
229
- // is the operator's (a container or VM per turn, say); the companion knows nothing of it
230
- // beyond handing the runner over, with the turn's inputs in its environment and the
231
- // credential and secrets on its stdin (`runner/launch.ts`, `wrapped`). The README lists
232
- // what a wrapper can rely on.
233
- runner: z2.object({
234
- command: z2.array(z2.string().min(1)).min(1),
235
- // The external wrapper supplies only broker placeholders and removes
236
- // credential mounts. Do not hand provider logins to that runner on stdin.
237
- brokeredCredentials: z2.boolean().optional()
238
- }).strict().optional()
239
- });
240
- var RETIRED_ROW_KEYS = ["sandbox"];
241
- var RETIRED_HOOK_KEYS = ["validateCached", "failureReporter"];
242
- function isObject(value) {
243
- return !!value && typeof value === "object" && !Array.isArray(value);
244
- }
245
- function splitRetired(raw) {
246
- const keys = [];
247
- const without = (obj, names, path) => {
248
- const out = {};
249
- for (const [k, v] of Object.entries(obj)) {
250
- if (names.includes(k)) keys.push(`${path}${k}`);
251
- else out[k] = v;
252
- }
253
- return out;
254
- };
255
- const hook = (row, path) => isObject(row.prepareHook) ? { ...row, prepareHook: without(row.prepareHook, RETIRED_HOOK_KEYS, `${path}prepareHook.`) } : row;
256
- const kept = hook(without(raw, ["runners"], ""), "");
257
- if (isObject(kept.agents)) {
258
- const agents = {};
259
- for (const [key, row] of Object.entries(kept.agents)) {
260
- const path = `agents.${key}.`;
261
- agents[key] = isObject(row) ? hook(without(row, RETIRED_ROW_KEYS, path), path) : row;
262
- }
263
- kept.agents = agents;
264
- }
265
- return { keys, kept };
266
- }
267
- function withoutRetiredKeys(raw) {
268
- return isObject(raw) ? splitRetired(raw).kept : raw;
269
- }
270
- var legacyOfferSchema = z2.object({
271
- claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
272
- codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
273
- harnessesChosen: z2.boolean().optional(),
274
- opencodeDeclined: z2.boolean().optional()
275
- });
276
- function migrateHarnessAllowlist(raw) {
277
- const legacy = legacyOfferSchema.safeParse(raw);
278
- if (!legacy.success || !legacy.data.harnessesChosen) return null;
279
- const declined = /* @__PURE__ */ new Set();
280
- if (legacy.data.claudeCode?.enabled === false) declined.add("claude-code");
281
- if (legacy.data.codex?.enabled === false) declined.add("codex");
282
- if (legacy.data.opencodeDeclined === true) declined.add("opencode");
283
- if (declined.size === 0) return null;
284
- return HARNESS_RUNTIMES.filter((runtime) => !declined.has(runtime));
285
- }
286
- function carriesLegacyOffer(raw) {
287
- if (!raw || typeof raw !== "object") return false;
288
- const obj = raw;
289
- return "harnessesChosen" in obj || "opencodeDeclined" in obj || typeof obj.claudeCode === "object" && obj.claudeCode !== null && "enabled" in obj.claudeCode || obj.codex !== void 0;
290
- }
291
- function loadConfig() {
292
- const path = configPath();
293
- if (!existsSync3(path)) return null;
294
- let raw;
295
- try {
296
- raw = readFileSync2(path, "utf8");
297
- } catch (err) {
298
- throw new ConfigError(
299
- `couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
300
- );
301
- }
302
- if (raw.trim().length === 0) return null;
303
- let parsed;
304
- try {
305
- parsed = withoutRetiredKeys(JSON.parse(raw));
306
- } catch (err) {
307
- throw new ConfigError(
308
- `${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
309
- );
310
- }
311
- const result = companionConfigSchema.safeParse(parsed);
312
- if (!result.success) {
313
- const agentIssue = result.error.issues.find((i) => i.path[0] === "agents");
314
- if (agentIssue) {
315
- throw new ConfigError(
316
- `${path}: invalid "agents" config at \`${agentIssue.path.join(".")}\` \u2014 ${agentIssue.message}. Fix the agents block in the config and retry.`
317
- );
318
- }
319
- const rootIssue = result.error.issues.find((i) => i.path[0] === "root");
320
- if (rootIssue) {
321
- throw new ConfigError(
322
- `${path}: invalid "root" \u2014 ${rootIssue.message}. Fix it in the config and retry.`
323
- );
324
- }
325
- throw new ConfigError(
326
- `${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("setup")}\` to pair again, or \`${cabane("logout --purge")}\` to reset.`
327
- );
328
- }
329
- if (carriesLegacyOffer(parsed)) {
330
- const allowlist = migrateHarnessAllowlist(parsed);
331
- const migrated = {
332
- ...result.data,
333
- ...allowlist && !result.data.harnesses ? { harnesses: allowlist } : {}
334
- };
335
- saveConfig(migrated);
336
- return migrated;
337
- }
338
- return result.data;
339
- }
340
- function loadConfigTolerant() {
341
- const path = configPath();
342
- const empty = {};
343
- const fresh = { local: empty, note: null, hadPriorConfig: false };
344
- if (!existsSync3(path)) return fresh;
345
- let raw;
346
- try {
347
- raw = readFileSync2(path, "utf8");
348
- } catch {
349
- return fresh;
350
- }
351
- if (raw.trim().length === 0) return fresh;
352
- let parsed;
353
- try {
354
- parsed = withoutRetiredKeys(JSON.parse(raw));
355
- } catch {
356
- return {
357
- local: empty,
358
- note: `${path} was unreadable (invalid JSON) and has been reset.`,
359
- hadPriorConfig: true
360
- };
361
- }
362
- const strict = companionConfigSchema.safeParse(parsed);
363
- if (strict.success) {
364
- const {
365
- baseUrl: _baseUrl,
366
- deviceToken: _deviceToken,
367
- deviceId: _deviceId,
368
- deviceLabel: _deviceLabel,
369
- ...local2
370
- } = strict.data;
371
- return { local: local2, note: null, hadPriorConfig: true };
372
- }
373
- const obj = parsed && typeof parsed === "object" ? parsed : {};
374
- const local = {};
375
- const agents = agentsConfigSchema.safeParse(obj.agents);
376
- if (agents.success) local.agents = agents.data;
377
- const prepareHook = prepareHookSchema.safeParse(obj.prepareHook);
378
- if (prepareHook.success) local.prepareHook = prepareHook.data;
379
- const root = companionConfigSchema.shape.root.safeParse(obj.root);
380
- if (root.success && root.data !== void 0) local.root = root.data;
381
- if (typeof obj.dashboardPort === "number") local.dashboardPort = obj.dashboardPort;
382
- if (typeof obj.autoOpen === "boolean") local.autoOpen = obj.autoOpen;
383
- if (obj.logFormat === "human" || obj.logFormat === "json") local.logFormat = obj.logFormat;
384
- if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
385
- local.logLevel = obj.logLevel;
386
- }
387
- const harnesses = companionConfigSchema.shape.harnesses.safeParse(obj.harnesses);
388
- if (harnesses.success && harnesses.data !== void 0) local.harnesses = harnesses.data;
389
- const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
390
- if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
391
- const update = companionConfigSchema.shape.update.safeParse(obj.update);
392
- if (update.success && update.data !== void 0) local.update = update.data;
393
- if (typeof obj.serviceDeclined === "boolean") local.serviceDeclined = obj.serviceDeclined;
394
- const runner = companionConfigSchema.shape.runner.safeParse(obj.runner);
395
- if (runner.success && runner.data !== void 0) local.runner = runner.data;
396
- return {
397
- local,
398
- note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
399
- hadPriorConfig: true
400
- };
401
- }
402
- function saveConfig(cfg) {
403
- const path = configPath();
404
- mkdirSync(dirname3(path), { recursive: true });
405
- try {
406
- chmodSync(cabaneDir(), 448);
407
- } catch {
408
- }
409
- const tmp = `${path}.${process.pid}.tmp`;
410
- try {
411
- writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n", { mode: 384 });
412
- try {
413
- chmodSync(tmp, 384);
414
- } catch {
415
- }
416
- renameSync(tmp, path);
417
- } catch (err) {
418
- try {
419
- rmSync(tmp, { force: true });
420
- } catch {
421
- }
422
- throw err;
423
- }
424
- }
425
-
426
- // src/enrollment.ts
427
- var EnrollmentExpiredError = class extends CompanionError {
428
- constructor() {
429
- super("this pairing code expired before it was confirmed.");
430
- this.name = "EnrollmentExpiredError";
431
- }
432
- };
433
- var EnrollmentCancelledError = class extends CompanionError {
434
- constructor() {
435
- super("pairing was cancelled.");
436
- this.name = "EnrollmentCancelledError";
437
- }
438
- };
439
- function trimBase(baseUrl) {
440
- return baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
441
- }
442
- async function postJson(baseUrl, path, body, signal) {
443
- let res;
444
- try {
445
- res = await fetch(`${trimBase(baseUrl)}${path}`, {
446
- method: "POST",
447
- headers: { "content-type": "application/json", accept: "application/json" },
448
- body: JSON.stringify(body),
449
- ...signal ? { signal } : {}
450
- });
451
- } catch {
452
- if (signal?.aborted) throw new EnrollmentCancelledError();
453
- throw new CompanionError(
454
- `Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
455
- );
456
- }
457
- let raw;
458
- try {
459
- raw = await res.text();
460
- } catch {
461
- if (signal?.aborted) throw new EnrollmentCancelledError();
462
- throw new CompanionError(
463
- `Couldn't reach Cabane at ${baseUrl}. Check the server URL and your connection.`
464
- );
465
- }
466
- let parsed = void 0;
467
- if (raw.length > 0) {
468
- try {
469
- parsed = JSON.parse(raw);
470
- } catch {
471
- parsed = raw;
472
- }
473
- }
474
- if (res.status >= 400) {
475
- if (res.status === 429)
476
- throw new CompanionError(
477
- "Too many pairing requests from this network. Wait a few minutes and run cabane setup again."
478
- );
479
- if (res.status === 404 && path.endsWith("/code"))
480
- throw new StandaloneError(
481
- `No pairing endpoint at ${baseUrl}: it isn't a Cabane's address, or that Cabane is too old for this companion.
482
- Run ${installLineWords()}.`
483
- );
484
- const msg = parsed && typeof parsed === "object" && "error" in parsed ? String(parsed.error) : `${res.status}`;
485
- throw new ApiError(res.status, `${res.status} ${msg}`, parsed);
486
- }
487
- return parsed;
488
- }
489
- function requestEnrollmentCode(baseUrl, opts = {}) {
490
- return postJson(
491
- baseUrl,
492
- "/api/device-enrollment/code",
493
- { ...opts.label ? { label: opts.label } : {}, ...hostFacts() },
494
- opts.signal
495
- );
496
- }
497
- function hostFacts() {
498
- const nodeMajor = Number.parseInt(process.versions.node.split(".")[0] ?? "", 10);
499
- return {
500
- platform: process.platform,
501
- arch: process.arch,
502
- ...Number.isInteger(nodeMajor) ? { nodeMajor } : {}
503
- };
504
- }
505
- function pollOnce(baseUrl, deviceCode, signal) {
506
- return postJson(baseUrl, "/api/device-enrollment/poll", { deviceCode }, signal);
507
- }
508
- var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
509
- async function awaitEnrollment(baseUrl, code, opts = {}) {
510
- const now = opts.now ?? (() => Date.now());
511
- const wait = opts.sleepMs ?? sleep;
512
- const deadline = now() + code.expiresIn * 1e3;
513
- let intervalMs = Math.max(1, code.interval) * 1e3;
514
- while (now() < deadline) {
515
- await wait(intervalMs);
516
- if (opts.signal?.aborted) throw new EnrollmentCancelledError();
517
- const res = await pollOnce(baseUrl, code.deviceCode, opts.signal);
518
- if (opts.signal?.aborted) throw new EnrollmentCancelledError();
519
- if (res.status === "complete") {
520
- if (!res.deviceToken || !res.baseUrl)
521
- throw new CompanionError("Cabane reported the pairing complete but returned no token.");
522
- return {
523
- baseUrl: res.baseUrl,
524
- deviceToken: res.deviceToken,
525
- ...res.deviceId ? { deviceId: res.deviceId } : {},
526
- ...res.deviceLabel ? { deviceLabel: res.deviceLabel } : {}
527
- };
528
- }
529
- if (res.status === "expired") throw new EnrollmentExpiredError();
530
- if (res.status === "slow_down") intervalMs += 1e3;
531
- }
532
- throw new EnrollmentExpiredError();
533
- }
534
-
535
- // src/pairing-config.ts
536
- function resolvePairBaseUrl(server) {
537
- const raw = (server ?? process.env.CABANE_BASE_URL ?? "").trim();
538
- if (!raw) throw new NoPairAddressError();
539
- if (!isAllowedBaseUrl(raw)) {
540
- throw new CompanionError(
541
- `invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("setup --server https://your-cabane")}\`.`
542
- );
543
- }
544
- return raw;
545
- }
546
- var NoPairAddressError = class extends StandaloneError {
547
- constructor(install = companionInstall()) {
548
- const words = installLineWords(install);
549
- super(
550
- `Setup doesn't know which Cabane to pair this device with. Run the
551
- ${words.slice("the ".length)}.`
552
- );
553
- this.name = "NoPairAddressError";
554
- }
555
- };
556
- function rememberPairAddress(baseUrl) {
557
- const { local } = loadConfigTolerant();
558
- saveConfig({ ...local, baseUrl });
559
- }
560
- function writePairedConfig(paired) {
561
- const { local, note } = loadConfigTolerant();
562
- const config = {
563
- ...local,
564
- baseUrl: paired.baseUrl,
565
- deviceToken: paired.deviceToken,
566
- ...paired.deviceId ? { deviceId: paired.deviceId } : {},
567
- ...paired.deviceLabel ? { deviceLabel: paired.deviceLabel } : {}
568
- };
569
- saveConfig(config);
570
- return { config, note };
571
- }
572
- function isDevicePaired() {
573
- try {
574
- return !!loadConfig()?.deviceToken;
575
- } catch {
576
- return false;
577
- }
578
- }
579
- export {
580
- ApiError,
581
- CompanionError,
582
- EnrollmentCancelledError,
583
- EnrollmentExpiredError,
584
- NoPairAddressError,
585
- awaitEnrollment,
586
- isDevicePaired,
587
- rememberPairAddress,
588
- requestEnrollmentCode,
589
- resolvePairBaseUrl,
590
- writePairedConfig
591
- };