@cabane/companion 0.6.159 → 0.6.161
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 +2 -2
- package/dist/cli.js +7410 -7614
- package/dist/launch.js +47 -68
- package/dist/pairing-config.js +74 -160
- package/dist/postinstall.js +75 -28
- package/dist/runtime.js +2011 -2207
- package/dist/static/app.js +5 -53
- package/package.json +2 -2
package/dist/launch.js
CHANGED
|
@@ -1,50 +1,47 @@
|
|
|
1
1
|
// src/launch.ts
|
|
2
|
-
import { readFileSync as
|
|
3
|
-
import { join as
|
|
2
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
3
|
+
import { join as join6 } from "path";
|
|
4
4
|
import { pathToFileURL } from "url";
|
|
5
5
|
|
|
6
6
|
// src/cabane-command.ts
|
|
7
7
|
import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
|
|
8
|
-
import { homedir as
|
|
9
|
-
import { delimiter, join as
|
|
8
|
+
import { homedir as homedir4 } from "os";
|
|
9
|
+
import { basename as basename2, delimiter, dirname as dirname2, join as join3, sep as sep2 } from "path";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
12
|
-
import { realpathSync
|
|
13
|
-
import { createRequire } from "module";
|
|
11
|
+
// src/install-kind.ts
|
|
12
|
+
import { realpathSync } from "fs";
|
|
14
13
|
import { homedir } from "os";
|
|
15
14
|
import { dirname, join, sep } from "path";
|
|
16
15
|
import { fileURLToPath } from "url";
|
|
17
|
-
var CODEX_CLI = "@openai/codex";
|
|
18
|
-
var CODEX_PACKAGE_BY_TRIPLE = {
|
|
19
|
-
"x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
|
|
20
|
-
"aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
|
|
21
|
-
"x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
|
|
22
|
-
"aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
|
|
23
|
-
"x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
|
|
24
|
-
"aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
|
|
25
|
-
};
|
|
26
16
|
|
|
27
17
|
// src/cli-entry.ts
|
|
28
18
|
import { existsSync } from "fs";
|
|
29
19
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
30
20
|
|
|
31
|
-
// src/
|
|
32
|
-
import {
|
|
21
|
+
// src/install-record.ts
|
|
22
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
33
23
|
import { homedir as homedir2 } from "os";
|
|
24
|
+
import { basename, join as join2 } from "path";
|
|
25
|
+
|
|
26
|
+
// src/term.ts
|
|
27
|
+
import { homedir as homedir3 } from "os";
|
|
34
28
|
import pc from "picocolors";
|
|
35
29
|
|
|
30
|
+
// src/cabane-command.ts
|
|
31
|
+
var VERSIONS_SEGMENT = `${sep2}.cabane${sep2}versions${sep2}`;
|
|
32
|
+
|
|
36
33
|
// src/config.ts
|
|
37
34
|
import {
|
|
38
35
|
chmodSync,
|
|
39
36
|
mkdirSync,
|
|
40
|
-
readFileSync,
|
|
37
|
+
readFileSync as readFileSync2,
|
|
41
38
|
renameSync,
|
|
42
39
|
rmSync,
|
|
43
40
|
writeFileSync,
|
|
44
|
-
existsSync as
|
|
41
|
+
existsSync as existsSync3
|
|
45
42
|
} from "fs";
|
|
46
|
-
import { homedir as
|
|
47
|
-
import { dirname as
|
|
43
|
+
import { homedir as homedir5, userInfo } from "os";
|
|
44
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
48
45
|
import { z as z2 } from "zod";
|
|
49
46
|
|
|
50
47
|
// src/pairing.ts
|
|
@@ -101,6 +98,7 @@ var prepareResultSchema = z.object({
|
|
|
101
98
|
});
|
|
102
99
|
|
|
103
100
|
// src/config.ts
|
|
101
|
+
var HARNESS_RUNTIMES = ["claude-code", "codex", "opencode"];
|
|
104
102
|
var realHomeCache;
|
|
105
103
|
function realAccountHome() {
|
|
106
104
|
if (realHomeCache === void 0) {
|
|
@@ -113,10 +111,10 @@ function realAccountHome() {
|
|
|
113
111
|
return realHomeCache;
|
|
114
112
|
}
|
|
115
113
|
function cabaneDir() {
|
|
116
|
-
const dir =
|
|
114
|
+
const dir = join4(process.env.CABANE_COMPANION_HOME || homedir5(), ".cabane");
|
|
117
115
|
if (process.env.VITEST) {
|
|
118
116
|
const real = realAccountHome();
|
|
119
|
-
if (real && dir ===
|
|
117
|
+
if (real && dir === join4(real, ".cabane")) {
|
|
120
118
|
throw new Error(
|
|
121
119
|
`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).`
|
|
122
120
|
);
|
|
@@ -185,31 +183,18 @@ var companionConfigSchema = z2.object({
|
|
|
185
183
|
autoOpen: z2.boolean().optional(),
|
|
186
184
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
187
185
|
logFormat: z2.enum(["human", "json"]).optional(),
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
186
|
+
// Where this device's `opencode serve` answers, when it isn't the default
|
|
187
|
+
// address (`DEFAULT_OPENCODE_SERVER_URL`). Detection probes this one; a serve
|
|
188
|
+
// that answers is connected. Auth is opencode's own; Cabane never sees
|
|
189
|
+
// provider keys.
|
|
191
190
|
opencode: z2.object({
|
|
192
191
|
serverUrl: z2.string().url()
|
|
193
192
|
}).strict().optional(),
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
// the
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
// Absent means one of two things, told apart by `harnessesChosen`: a legacy
|
|
200
|
-
// config whose user was never asked (`migrateConnectedHarnesses`), or a
|
|
201
|
-
// device that has never been offered Claude Code. `enabled: false` is a
|
|
202
|
-
// decline, and `start` doesn't offer it again.
|
|
203
|
-
//
|
|
204
|
-
// Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
|
|
205
|
-
// switch.
|
|
206
|
-
claudeCode: z2.object({
|
|
207
|
-
enabled: z2.boolean().optional()
|
|
208
|
-
}).strict().optional(),
|
|
209
|
-
// Written by every pairing that asks which agents to connect. With it, an
|
|
210
|
-
// absent `claudeCode` block means "never offered"; without it, the block's
|
|
211
|
-
// absence marks a config from before connectors were chosen.
|
|
212
|
-
harnessesChosen: z2.boolean().optional(),
|
|
193
|
+
// The one knob on agents: an allowlist. Agents are detected, never offered
|
|
194
|
+
// (`harness-detect.ts`): whatever is signed in here is connected. Someone with
|
|
195
|
+
// Codex on the device who doesn't want Cabane to see it lists the ones it may:
|
|
196
|
+
// `"harnesses": ["claude-code"]`. Absent means all three.
|
|
197
|
+
harnesses: z2.array(z2.enum(HARNESS_RUNTIMES)).optional(),
|
|
213
198
|
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
214
199
|
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
215
200
|
// which `start` offers; off either way until someone says yes.
|
|
@@ -219,18 +204,6 @@ var companionConfigSchema = z2.object({
|
|
|
219
204
|
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
220
205
|
// `cabane service install` still installs it.
|
|
221
206
|
serviceDeclined: z2.boolean().optional(),
|
|
222
|
-
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
223
|
-
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
224
|
-
// serve would be offered again on every `start`.
|
|
225
|
-
opencodeDeclined: z2.boolean().optional(),
|
|
226
|
-
// Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
|
|
227
|
-
// an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
|
|
228
|
-
// never sees the key). The block's presence enables it; `enabled: false` turns
|
|
229
|
-
// it off. Half the answer: advertising also requires the SDK's vendored
|
|
230
|
-
// `codex` binary to resolve.
|
|
231
|
-
codex: z2.object({
|
|
232
|
-
enabled: z2.boolean().optional()
|
|
233
|
-
}).strict().optional(),
|
|
234
207
|
// Run each turn in a child runner process. Absent or `enabled: false` is the
|
|
235
208
|
// in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
|
|
236
209
|
// host supports one; `'none'` (the default) is a plain child, supported in its
|
|
@@ -274,30 +247,36 @@ var companionConfigSchema = z2.object({
|
|
|
274
247
|
policy: z2.unknown().optional()
|
|
275
248
|
}).strict().optional()
|
|
276
249
|
});
|
|
250
|
+
var legacyOfferSchema = z2.object({
|
|
251
|
+
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
252
|
+
codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
253
|
+
harnessesChosen: z2.boolean().optional(),
|
|
254
|
+
opencodeDeclined: z2.boolean().optional()
|
|
255
|
+
});
|
|
277
256
|
|
|
278
257
|
// src/install-layout.ts
|
|
279
258
|
import {
|
|
280
|
-
existsSync as
|
|
259
|
+
existsSync as existsSync4,
|
|
281
260
|
lstatSync,
|
|
282
261
|
mkdirSync as mkdirSync2,
|
|
283
|
-
readFileSync as
|
|
262
|
+
readFileSync as readFileSync3,
|
|
284
263
|
readdirSync,
|
|
285
264
|
realpathSync as realpathSync3,
|
|
286
265
|
renameSync as renameSync2,
|
|
287
266
|
rmSync as rmSync2,
|
|
288
267
|
symlinkSync
|
|
289
268
|
} from "fs";
|
|
290
|
-
import { dirname as
|
|
269
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
291
270
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
292
271
|
function currentLink() {
|
|
293
|
-
return
|
|
272
|
+
return join5(cabaneDir(), "current");
|
|
294
273
|
}
|
|
295
274
|
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
296
|
-
return
|
|
275
|
+
return dirname4(dirname4(fileURLToPath3(moduleUrl)));
|
|
297
276
|
}
|
|
298
277
|
function packageVersion(root) {
|
|
299
278
|
try {
|
|
300
|
-
const pkg = JSON.parse(
|
|
279
|
+
const pkg = JSON.parse(readFileSync3(join5(root, "package.json"), "utf8"));
|
|
301
280
|
return typeof pkg.version === "string" ? pkg.version : null;
|
|
302
281
|
} catch {
|
|
303
282
|
return null;
|
|
@@ -306,14 +285,14 @@ function packageVersion(root) {
|
|
|
306
285
|
function currentRoot() {
|
|
307
286
|
try {
|
|
308
287
|
const root = realpathSync3(currentLink());
|
|
309
|
-
return
|
|
288
|
+
return existsSync4(join5(root, "dist", "cli.js")) ? root : null;
|
|
310
289
|
} catch {
|
|
311
290
|
return null;
|
|
312
291
|
}
|
|
313
292
|
}
|
|
314
293
|
function pointCurrentAt(root) {
|
|
315
294
|
const link = currentLink();
|
|
316
|
-
mkdirSync2(
|
|
295
|
+
mkdirSync2(dirname4(link), { recursive: true });
|
|
317
296
|
const fresh = `${link}.${process.pid}.${Date.now()}`;
|
|
318
297
|
symlinkSync(root, fresh);
|
|
319
298
|
try {
|
|
@@ -369,7 +348,7 @@ function parseEnvFile(text) {
|
|
|
369
348
|
function loadServiceEnv() {
|
|
370
349
|
let text;
|
|
371
350
|
try {
|
|
372
|
-
text =
|
|
351
|
+
text = readFileSync4(join6(cabaneDir(), "service.env"), "utf8");
|
|
373
352
|
} catch {
|
|
374
353
|
return;
|
|
375
354
|
}
|
|
@@ -402,7 +381,7 @@ async function launch(argv = process.argv.slice(2)) {
|
|
|
402
381
|
await import("./cli.js");
|
|
403
382
|
return;
|
|
404
383
|
}
|
|
405
|
-
await import(pathToFileURL(
|
|
384
|
+
await import(pathToFileURL(join6(run.root, "dist", "cli.js")).href);
|
|
406
385
|
}
|
|
407
386
|
export {
|
|
408
387
|
chooseInstall,
|
package/dist/pairing-config.js
CHANGED
|
@@ -1,120 +1,45 @@
|
|
|
1
1
|
// src/cabane-command.ts
|
|
2
2
|
import { accessSync, constants, realpathSync as realpathSync2 } from "fs";
|
|
3
|
-
import { homedir as
|
|
4
|
-
import { delimiter, join as
|
|
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
5
|
|
|
6
|
-
// src/
|
|
7
|
-
import { realpathSync
|
|
8
|
-
import { createRequire } from "module";
|
|
6
|
+
// src/install-kind.ts
|
|
7
|
+
import { realpathSync } from "fs";
|
|
9
8
|
import { homedir } from "os";
|
|
10
9
|
import { dirname, join, sep } from "path";
|
|
11
10
|
import { fileURLToPath } from "url";
|
|
12
|
-
var CODEX_CLI = "@openai/codex";
|
|
13
|
-
var CODEX_PACKAGE_BY_TRIPLE = {
|
|
14
|
-
"x86_64-unknown-linux-musl": `${CODEX_CLI}-linux-x64`,
|
|
15
|
-
"aarch64-unknown-linux-musl": `${CODEX_CLI}-linux-arm64`,
|
|
16
|
-
"x86_64-apple-darwin": `${CODEX_CLI}-darwin-x64`,
|
|
17
|
-
"aarch64-apple-darwin": `${CODEX_CLI}-darwin-arm64`,
|
|
18
|
-
"x86_64-pc-windows-msvc": `${CODEX_CLI}-win32-x64`,
|
|
19
|
-
"aarch64-pc-windows-msvc": `${CODEX_CLI}-win32-arm64`
|
|
20
|
-
};
|
|
21
|
-
function companionInstall(execPath = process.execPath, home = homedir()) {
|
|
22
|
-
const real = (path) => {
|
|
23
|
-
try {
|
|
24
|
-
return realpathSync(path);
|
|
25
|
-
} catch {
|
|
26
|
-
return path;
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
const scriptNode = join(real(home), ".cabane", "node") + sep;
|
|
30
|
-
return real(execPath).startsWith(scriptNode) ? "script" : "npm";
|
|
31
|
-
}
|
|
32
11
|
|
|
33
12
|
// src/cli-entry.ts
|
|
34
13
|
import { existsSync } from "fs";
|
|
35
14
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
36
|
-
var RELATIVE_CANDIDATES = ["./cli.js", "../dist/cli.js", "../cli.js"];
|
|
37
|
-
function companionCliEntry(deps = {}) {
|
|
38
|
-
const exists = deps.exists ?? existsSync;
|
|
39
|
-
const candidates = deps.candidates ?? RELATIVE_CANDIDATES.map((rel) => fileURLToPath2(new URL(rel, import.meta.url)));
|
|
40
|
-
for (const candidate of candidates) {
|
|
41
|
-
if (exists(candidate)) return candidate;
|
|
42
|
-
}
|
|
43
|
-
const argv1 = "argv1" in deps ? deps.argv1 : process.argv[1];
|
|
44
|
-
if (argv1 && exists(argv1)) return argv1;
|
|
45
|
-
return candidates[0] ?? "";
|
|
46
|
-
}
|
|
47
15
|
|
|
48
|
-
// src/
|
|
49
|
-
import {
|
|
16
|
+
// src/install-record.ts
|
|
17
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
50
18
|
import { homedir as homedir2 } from "os";
|
|
19
|
+
import { basename, join as join2 } from "path";
|
|
20
|
+
|
|
21
|
+
// src/term.ts
|
|
22
|
+
import { homedir as homedir3 } from "os";
|
|
51
23
|
import pc from "picocolors";
|
|
52
|
-
function tildePath(path, home = homedir2()) {
|
|
53
|
-
if (home && path.startsWith(home + "/")) return `~${path.slice(home.length)}`;
|
|
54
|
-
return path;
|
|
55
|
-
}
|
|
56
24
|
|
|
57
25
|
// src/cabane-command.ts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (install === "script") return join2(home, ".cabane", "bin", "cabane");
|
|
62
|
-
const entry = deps.cliEntry ?? companionCliEntry();
|
|
63
|
-
const marker = `${sep2}lib${sep2}node_modules${sep2}@cabane${sep2}companion${sep2}`;
|
|
64
|
-
const at = entry.indexOf(marker);
|
|
65
|
-
if (at < 0) return null;
|
|
66
|
-
return join2(entry.slice(0, at), "bin", "cabane");
|
|
67
|
-
}
|
|
68
|
-
function cabaneCommand(deps = {}) {
|
|
69
|
-
const own = installedCommandPath(deps);
|
|
70
|
-
if (!own) return "cabane";
|
|
71
|
-
const realpath = deps.realpath ?? safeRealpath;
|
|
72
|
-
const isExecutable = deps.isExecutable ?? executable;
|
|
73
|
-
const ownReal = realpath(own);
|
|
74
|
-
if (!ownReal) return "cabane";
|
|
75
|
-
const onPath = firstOnPath("cabane", "path" in deps ? deps.path : process.env.PATH, isExecutable);
|
|
76
|
-
if (onPath && realpath(onPath) === ownReal) return "cabane";
|
|
77
|
-
return tildePath(own, deps.home);
|
|
78
|
-
}
|
|
79
|
-
function cabane(rest, deps) {
|
|
80
|
-
return `${cabaneCommand(deps)} ${rest}`;
|
|
81
|
-
}
|
|
82
|
-
function firstOnPath(name, path, isExecutable = executable) {
|
|
83
|
-
for (const dir of (path ?? "").split(delimiter)) {
|
|
84
|
-
if (!dir) continue;
|
|
85
|
-
const candidate = join2(dir, name);
|
|
86
|
-
if (isExecutable(candidate)) return candidate;
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
function executable(path) {
|
|
91
|
-
try {
|
|
92
|
-
accessSync(path, constants.X_OK);
|
|
93
|
-
return true;
|
|
94
|
-
} catch {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
function safeRealpath(path) {
|
|
99
|
-
try {
|
|
100
|
-
return realpathSync2(path);
|
|
101
|
-
} catch {
|
|
102
|
-
return null;
|
|
103
|
-
}
|
|
26
|
+
var VERSIONS_SEGMENT = `${sep2}.cabane${sep2}versions${sep2}`;
|
|
27
|
+
function cabane(rest) {
|
|
28
|
+
return `cabane ${rest}`;
|
|
104
29
|
}
|
|
105
30
|
|
|
106
31
|
// src/config.ts
|
|
107
32
|
import {
|
|
108
33
|
chmodSync,
|
|
109
34
|
mkdirSync,
|
|
110
|
-
readFileSync,
|
|
35
|
+
readFileSync as readFileSync2,
|
|
111
36
|
renameSync,
|
|
112
37
|
rmSync,
|
|
113
38
|
writeFileSync,
|
|
114
|
-
existsSync as
|
|
39
|
+
existsSync as existsSync3
|
|
115
40
|
} from "fs";
|
|
116
|
-
import { homedir as
|
|
117
|
-
import { dirname as
|
|
41
|
+
import { homedir as homedir5, userInfo } from "os";
|
|
42
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
118
43
|
import { z as z2 } from "zod";
|
|
119
44
|
|
|
120
45
|
// src/errors.ts
|
|
@@ -195,6 +120,7 @@ var prepareResultSchema = z.object({
|
|
|
195
120
|
});
|
|
196
121
|
|
|
197
122
|
// src/config.ts
|
|
123
|
+
var HARNESS_RUNTIMES = ["claude-code", "codex", "opencode"];
|
|
198
124
|
var realHomeCache;
|
|
199
125
|
function realAccountHome() {
|
|
200
126
|
if (realHomeCache === void 0) {
|
|
@@ -207,10 +133,10 @@ function realAccountHome() {
|
|
|
207
133
|
return realHomeCache;
|
|
208
134
|
}
|
|
209
135
|
function cabaneDir() {
|
|
210
|
-
const dir =
|
|
136
|
+
const dir = join4(process.env.CABANE_COMPANION_HOME || homedir5(), ".cabane");
|
|
211
137
|
if (process.env.VITEST) {
|
|
212
138
|
const real = realAccountHome();
|
|
213
|
-
if (real && dir ===
|
|
139
|
+
if (real && dir === join4(real, ".cabane")) {
|
|
214
140
|
throw new Error(
|
|
215
141
|
`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).`
|
|
216
142
|
);
|
|
@@ -219,7 +145,7 @@ function cabaneDir() {
|
|
|
219
145
|
return dir;
|
|
220
146
|
}
|
|
221
147
|
function configPath() {
|
|
222
|
-
return
|
|
148
|
+
return join4(cabaneDir(), "config.json");
|
|
223
149
|
}
|
|
224
150
|
var localAgentConfigSchema = z2.object({
|
|
225
151
|
cwd: z2.string().optional(),
|
|
@@ -282,31 +208,18 @@ var companionConfigSchema = z2.object({
|
|
|
282
208
|
autoOpen: z2.boolean().optional(),
|
|
283
209
|
logLevel: z2.enum(["warn", "info", "debug"]).optional(),
|
|
284
210
|
logFormat: z2.enum(["human", "json"]).optional(),
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
211
|
+
// Where this device's `opencode serve` answers, when it isn't the default
|
|
212
|
+
// address (`DEFAULT_OPENCODE_SERVER_URL`). Detection probes this one; a serve
|
|
213
|
+
// that answers is connected. Auth is opencode's own; Cabane never sees
|
|
214
|
+
// provider keys.
|
|
288
215
|
opencode: z2.object({
|
|
289
216
|
serverUrl: z2.string().url()
|
|
290
217
|
}).strict().optional(),
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
// the
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
// Absent means one of two things, told apart by `harnessesChosen`: a legacy
|
|
297
|
-
// config whose user was never asked (`migrateConnectedHarnesses`), or a
|
|
298
|
-
// device that has never been offered Claude Code. `enabled: false` is a
|
|
299
|
-
// decline, and `start` doesn't offer it again.
|
|
300
|
-
//
|
|
301
|
-
// Not the per-agent `agents.<key>.claudeCode.autoMemory`, one agent's memory
|
|
302
|
-
// switch.
|
|
303
|
-
claudeCode: z2.object({
|
|
304
|
-
enabled: z2.boolean().optional()
|
|
305
|
-
}).strict().optional(),
|
|
306
|
-
// Written by every pairing that asks which agents to connect. With it, an
|
|
307
|
-
// absent `claudeCode` block means "never offered"; without it, the block's
|
|
308
|
-
// absence marks a config from before connectors were chosen.
|
|
309
|
-
harnessesChosen: z2.boolean().optional(),
|
|
218
|
+
// The one knob on agents: an allowlist. Agents are detected, never offered
|
|
219
|
+
// (`harness-detect.ts`): whatever is signed in here is connected. Someone with
|
|
220
|
+
// Codex on the device who doesn't want Cabane to see it lists the ones it may:
|
|
221
|
+
// `"harnesses": ["claude-code"]`. Absent means all three.
|
|
222
|
+
harnesses: z2.array(z2.enum(HARNESS_RUNTIMES)).optional(),
|
|
310
223
|
// Whether this companion keeps itself current: installs the version Cabane names, between
|
|
311
224
|
// turns, and rolls back one that doesn't come up (`updater.ts`). Absent means never asked,
|
|
312
225
|
// which `start` offers; off either way until someone says yes.
|
|
@@ -316,18 +229,6 @@ var companionConfigSchema = z2.object({
|
|
|
316
229
|
// The answer to `start`'s "start at boot?" when it was no, so it isn't asked again.
|
|
317
230
|
// `cabane service install` still installs it.
|
|
318
231
|
serviceDeclined: z2.boolean().optional(),
|
|
319
|
-
// OpenCode's decline. Its block is an address, so a decline can't live there
|
|
320
|
-
// the way `claudeCode`/`codex` `enabled: false` does; without it a declined
|
|
321
|
-
// serve would be offered again on every `start`.
|
|
322
|
-
opencodeDeclined: z2.boolean().optional(),
|
|
323
|
-
// Codex is a local CLI the SDK spawns per turn, so this is an opt-in flag, not
|
|
324
|
-
// an address. The operator logs in (`codex login` or `CODEX_API_KEY`; Cabane
|
|
325
|
-
// never sees the key). The block's presence enables it; `enabled: false` turns
|
|
326
|
-
// it off. Half the answer: advertising also requires the SDK's vendored
|
|
327
|
-
// `codex` binary to resolve.
|
|
328
|
-
codex: z2.object({
|
|
329
|
-
enabled: z2.boolean().optional()
|
|
330
|
-
}).strict().optional(),
|
|
331
232
|
// Run each turn in a child runner process. Absent or `enabled: false` is the
|
|
332
233
|
// in-process path. `sandbox: 'os'` wraps the runner in an OS sandbox where the
|
|
333
234
|
// host supports one; `'none'` (the default) is a plain child, supported in its
|
|
@@ -371,12 +272,33 @@ var companionConfigSchema = z2.object({
|
|
|
371
272
|
policy: z2.unknown().optional()
|
|
372
273
|
}).strict().optional()
|
|
373
274
|
});
|
|
275
|
+
var legacyOfferSchema = z2.object({
|
|
276
|
+
claudeCode: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
277
|
+
codex: z2.object({ enabled: z2.boolean().optional() }).passthrough().optional(),
|
|
278
|
+
harnessesChosen: z2.boolean().optional(),
|
|
279
|
+
opencodeDeclined: z2.boolean().optional()
|
|
280
|
+
});
|
|
281
|
+
function migrateHarnessAllowlist(raw) {
|
|
282
|
+
const legacy = legacyOfferSchema.safeParse(raw);
|
|
283
|
+
if (!legacy.success || !legacy.data.harnessesChosen) return null;
|
|
284
|
+
const declined = /* @__PURE__ */ new Set();
|
|
285
|
+
if (legacy.data.claudeCode?.enabled === false) declined.add("claude-code");
|
|
286
|
+
if (legacy.data.codex?.enabled === false) declined.add("codex");
|
|
287
|
+
if (legacy.data.opencodeDeclined === true) declined.add("opencode");
|
|
288
|
+
if (declined.size === 0) return null;
|
|
289
|
+
return HARNESS_RUNTIMES.filter((runtime) => !declined.has(runtime));
|
|
290
|
+
}
|
|
291
|
+
function carriesLegacyOffer(raw) {
|
|
292
|
+
if (!raw || typeof raw !== "object") return false;
|
|
293
|
+
const obj = raw;
|
|
294
|
+
return "harnessesChosen" in obj || "opencodeDeclined" in obj || typeof obj.claudeCode === "object" && obj.claudeCode !== null && "enabled" in obj.claudeCode || obj.codex !== void 0;
|
|
295
|
+
}
|
|
374
296
|
function loadConfig() {
|
|
375
297
|
const path = configPath();
|
|
376
|
-
if (!
|
|
298
|
+
if (!existsSync3(path)) return null;
|
|
377
299
|
let raw;
|
|
378
300
|
try {
|
|
379
|
-
raw =
|
|
301
|
+
raw = readFileSync2(path, "utf8");
|
|
380
302
|
} catch (err) {
|
|
381
303
|
throw new ConfigError(
|
|
382
304
|
`couldn't read ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -400,19 +322,28 @@ function loadConfig() {
|
|
|
400
322
|
);
|
|
401
323
|
}
|
|
402
324
|
throw new ConfigError(
|
|
403
|
-
`${path} is from an incompatible or older version of the companion, or was hand-edited. Run \`${cabane("
|
|
325
|
+
`${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.`
|
|
404
326
|
);
|
|
405
327
|
}
|
|
328
|
+
if (carriesLegacyOffer(parsed)) {
|
|
329
|
+
const allowlist = migrateHarnessAllowlist(parsed);
|
|
330
|
+
const migrated = {
|
|
331
|
+
...result.data,
|
|
332
|
+
...allowlist && !result.data.harnesses ? { harnesses: allowlist } : {}
|
|
333
|
+
};
|
|
334
|
+
saveConfig(migrated);
|
|
335
|
+
return migrated;
|
|
336
|
+
}
|
|
406
337
|
return result.data;
|
|
407
338
|
}
|
|
408
339
|
function loadConfigTolerant() {
|
|
409
340
|
const path = configPath();
|
|
410
341
|
const empty = {};
|
|
411
342
|
const fresh = { local: empty, note: null, hadPriorConfig: false };
|
|
412
|
-
if (!
|
|
343
|
+
if (!existsSync3(path)) return fresh;
|
|
413
344
|
let raw;
|
|
414
345
|
try {
|
|
415
|
-
raw =
|
|
346
|
+
raw = readFileSync2(path, "utf8");
|
|
416
347
|
} catch {
|
|
417
348
|
return fresh;
|
|
418
349
|
}
|
|
@@ -436,11 +367,8 @@ function loadConfigTolerant() {
|
|
|
436
367
|
autoOpen,
|
|
437
368
|
logLevel,
|
|
438
369
|
logFormat,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
opencode: opencode2,
|
|
442
|
-
opencodeDeclined,
|
|
443
|
-
harnessesChosen
|
|
370
|
+
harnesses: harnesses2,
|
|
371
|
+
opencode: opencode2
|
|
444
372
|
} = strict.data;
|
|
445
373
|
return {
|
|
446
374
|
local: {
|
|
@@ -450,11 +378,8 @@ function loadConfigTolerant() {
|
|
|
450
378
|
...autoOpen !== void 0 ? { autoOpen } : {},
|
|
451
379
|
...logLevel !== void 0 ? { logLevel } : {},
|
|
452
380
|
...logFormat !== void 0 ? { logFormat } : {},
|
|
453
|
-
...
|
|
454
|
-
...
|
|
455
|
-
...opencode2 !== void 0 ? { opencode: opencode2 } : {},
|
|
456
|
-
...opencodeDeclined !== void 0 ? { opencodeDeclined } : {},
|
|
457
|
-
...harnessesChosen !== void 0 ? { harnessesChosen } : {}
|
|
381
|
+
...harnesses2 !== void 0 ? { harnesses: harnesses2 } : {},
|
|
382
|
+
...opencode2 !== void 0 ? { opencode: opencode2 } : {}
|
|
458
383
|
},
|
|
459
384
|
note: null,
|
|
460
385
|
hadPriorConfig: true
|
|
@@ -472,14 +397,10 @@ function loadConfigTolerant() {
|
|
|
472
397
|
if (obj.logLevel === "warn" || obj.logLevel === "info" || obj.logLevel === "debug") {
|
|
473
398
|
local.logLevel = obj.logLevel;
|
|
474
399
|
}
|
|
475
|
-
const
|
|
476
|
-
if (
|
|
477
|
-
const codex = companionConfigSchema.shape.codex.safeParse(obj.codex);
|
|
478
|
-
if (codex.success && codex.data !== void 0) local.codex = codex.data;
|
|
400
|
+
const harnesses = companionConfigSchema.shape.harnesses.safeParse(obj.harnesses);
|
|
401
|
+
if (harnesses.success && harnesses.data !== void 0) local.harnesses = harnesses.data;
|
|
479
402
|
const opencode = companionConfigSchema.shape.opencode.safeParse(obj.opencode);
|
|
480
403
|
if (opencode.success && opencode.data !== void 0) local.opencode = opencode.data;
|
|
481
|
-
if (obj.opencodeDeclined === true) local.opencodeDeclined = true;
|
|
482
|
-
if (obj.harnessesChosen === true) local.harnessesChosen = true;
|
|
483
404
|
return {
|
|
484
405
|
local,
|
|
485
406
|
note: `the existing ${path} was from an older or incompatible companion; re-pairing rewrote it.`,
|
|
@@ -488,7 +409,7 @@ function loadConfigTolerant() {
|
|
|
488
409
|
}
|
|
489
410
|
function saveConfig(cfg) {
|
|
490
411
|
const path = configPath();
|
|
491
|
-
mkdirSync(
|
|
412
|
+
mkdirSync(dirname3(path), { recursive: true });
|
|
492
413
|
try {
|
|
493
414
|
chmodSync(cabaneDir(), 448);
|
|
494
415
|
} catch {
|
|
@@ -626,21 +547,14 @@ function resolvePairBaseUrl(server) {
|
|
|
626
547
|
const raw = (server ?? process.env.CABANE_BASE_URL ?? DEFAULT_BASE_URL).trim();
|
|
627
548
|
if (!isAllowedBaseUrl(raw)) {
|
|
628
549
|
throw new CompanionError(
|
|
629
|
-
`invalid server URL "${raw}": must be an https URL (loopback http is allowed for local dev). Pass it as \`${cabane("
|
|
550
|
+
`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")}\`.`
|
|
630
551
|
);
|
|
631
552
|
}
|
|
632
553
|
return raw;
|
|
633
554
|
}
|
|
634
555
|
function writePairedConfig(paired) {
|
|
635
|
-
const { local, note
|
|
556
|
+
const { local, note } = loadConfigTolerant();
|
|
636
557
|
const config = {
|
|
637
|
-
// A device pairs connected to NOTHING, and its agents are chosen from here
|
|
638
|
-
// on: `harnessesChosen` tells the harness migration that an absent
|
|
639
|
-
// `claudeCode` block means "never offered", not a legacy Claude device.
|
|
640
|
-
// Only when there was no config before: stamped on a RE-pair, an upgrading
|
|
641
|
-
// user's working device would be disconnected. `local` spreads after,
|
|
642
|
-
// keeping a prior answer.
|
|
643
|
-
...hadPriorConfig ? {} : { harnessesChosen: true },
|
|
644
558
|
...local,
|
|
645
559
|
baseUrl: paired.baseUrl,
|
|
646
560
|
deviceToken: paired.deviceToken,
|