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