docks-kit 0.17.1 → 0.17.3
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/AGENTS.md +41 -33
- package/cli/docs/omp-context.md +104 -0
- package/cli/docs/omp-models.md +106 -56
- package/cli/src/argv.ts +38 -469
- package/cli/src/argvSurface.ts +371 -0
- package/cli/src/argvValidate.ts +122 -0
- package/cli/src/commands/docs.ts +62 -42
- package/cli/src/commands/harnesses.ts +38 -45
- package/cli/src/commands/model.ts +48 -53
- package/cli/src/commands/models.ts +24 -26
- package/cli/src/commands/omp.ts +132 -112
- package/cli/src/commands/plugins.ts +20 -20
- package/cli/src/commands/skills.ts +22 -20
- package/cli/src/commands/status.ts +87 -80
- package/cli/src/commands/sync.ts +113 -87
- package/cli/src/commands/toolchain.ts +21 -21
- package/cli/src/commands/update.ts +139 -117
- package/cli/src/efforts.ts +35 -34
- package/cli/src/engine-native/DESIGN.md +30 -30
- package/cli/src/engine-native/bun.ts +77 -55
- package/cli/src/engine-native/claudeLsp.ts +154 -0
- package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
- package/cli/src/engine-native/claudePluginPasses.ts +348 -0
- package/cli/src/engine-native/claudeRemovals.ts +195 -0
- package/cli/src/engine-native/claudeRetired.ts +4 -4
- package/cli/src/engine-native/claudeRuntime.ts +94 -68
- package/cli/src/engine-native/claudeSettings.ts +174 -0
- package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
- package/cli/src/engine-native/claudeSync.ts +176 -456
- package/cli/src/engine-native/codexConfig.ts +158 -0
- package/cli/src/engine-native/codexHooks.ts +91 -0
- package/cli/src/engine-native/codexPlugins.ts +290 -0
- package/cli/src/engine-native/codexStatus.ts +24 -0
- package/cli/src/engine-native/codexSync.ts +110 -568
- package/cli/src/engine-native/codexToml.ts +140 -138
- package/cli/src/engine-native/deps.ts +144 -107
- package/cli/src/engine-native/engineCtx.ts +126 -0
- package/cli/src/engine-native/exec.ts +98 -87
- package/cli/src/engine-native/failures.ts +3 -3
- package/cli/src/engine-native/harnesses.ts +62 -72
- package/cli/src/engine-native/index.ts +34 -315
- package/cli/src/engine-native/jq.ts +25 -24
- package/cli/src/engine-native/logger.ts +98 -100
- package/cli/src/engine-native/models.ts +50 -45
- package/cli/src/engine-native/modes.ts +83 -80
- package/cli/src/engine-native/ompFileDeploy.ts +111 -0
- package/cli/src/engine-native/ompMarketplace.ts +136 -0
- package/cli/src/engine-native/ompOverlay.ts +67 -64
- package/cli/src/engine-native/ompPaths.ts +49 -38
- package/cli/src/engine-native/ompPlugins.ts +170 -0
- package/cli/src/engine-native/ompRemovals.ts +159 -0
- package/cli/src/engine-native/ompSync.ts +69 -409
- package/cli/src/engine-native/ompYaml.ts +43 -41
- package/cli/src/engine-native/os/darwin.ts +5 -5
- package/cli/src/engine-native/os/index.ts +15 -15
- package/cli/src/engine-native/os/linux.ts +5 -5
- package/cli/src/engine-native/os/posix.ts +28 -24
- package/cli/src/engine-native/os/targets.ts +17 -17
- package/cli/src/engine-native/os/types.ts +31 -31
- package/cli/src/engine-native/os/windows.ts +61 -61
- package/cli/src/engine-native/parseArgs.ts +122 -372
- package/cli/src/engine-native/parseHelp.ts +66 -0
- package/cli/src/engine-native/parseModifiers.ts +222 -0
- package/cli/src/engine-native/services.ts +34 -34
- package/cli/src/engine-native/settings.ts +15 -15
- package/cli/src/engine-native/sharedTypes.d.ts +46 -0
- package/cli/src/engine-native/skillsInstall.ts +105 -0
- package/cli/src/engine-native/skillsLinks.ts +203 -0
- package/cli/src/engine-native/skillsManifest.ts +16 -0
- package/cli/src/engine-native/skillsPrune.ts +108 -0
- package/cli/src/engine-native/skillsSync.ts +28 -362
- package/cli/src/engine-native/syncConcurrency.ts +55 -0
- package/cli/src/engine-native/syncDispatch.ts +141 -0
- package/cli/src/engine-native/toolchain.ts +65 -62
- package/cli/src/engine.ts +65 -57
- package/cli/src/generated/sotPayload.ts +7 -7
- package/cli/src/kitHome.ts +44 -40
- package/cli/src/main.ts +63 -56
- package/cli/src/manifests.ts +53 -54
- package/cli/src/md.d.ts +2 -2
- package/cli/src/payload.ts +9 -9
- package/cli/src/services.ts +21 -13
- package/cli/tsconfig.json +4 -1
- package/docks-kit +13 -2
- package/docks-kit.ps1 +21 -6
- package/package.json +10 -2
- package/cli/src/engine-native/claudePlugins.ts +0 -510
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
type SpawnOptions,
|
|
11
11
|
type SpawnSyncOptions,
|
|
12
12
|
type SpawnSyncOptionsWithStringEncoding,
|
|
13
|
-
type SpawnSyncReturns
|
|
14
|
-
} from "node:child_process"
|
|
15
|
-
import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs"
|
|
16
|
-
import { delimiter, extname, isAbsolute, join } from "node:path"
|
|
17
|
-
import { hostOs, type HostOs, type Invocation } from "./os"
|
|
13
|
+
type SpawnSyncReturns,
|
|
14
|
+
} from "node:child_process";
|
|
15
|
+
import { accessSync, constants, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
17
|
+
import { hostOs, type HostOs, type Invocation } from "./os";
|
|
18
18
|
|
|
19
19
|
/** A tool this host cannot resolve, shaped like the failed spawn it replaces. */
|
|
20
20
|
const notFound = (command: string): SpawnSyncReturns<string> => ({
|
|
@@ -24,8 +24,8 @@ const notFound = (command: string): SpawnSyncReturns<string> => ({
|
|
|
24
24
|
stderr: "",
|
|
25
25
|
status: null,
|
|
26
26
|
signal: null,
|
|
27
|
-
error: new Error(`command not found on PATH: ${command}`)
|
|
28
|
-
})
|
|
27
|
+
error: new Error(`command not found on PATH: ${command}`),
|
|
28
|
+
});
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Every synchronous child starts here, because two host facts must never be
|
|
@@ -37,150 +37,161 @@ export const spawnHost = (
|
|
|
37
37
|
command: string,
|
|
38
38
|
args: ReadonlyArray<string>,
|
|
39
39
|
overrides: SpawnSyncOptions = {},
|
|
40
|
-
host: HostOs = hostOs()
|
|
40
|
+
host: HostOs = hostOs(),
|
|
41
41
|
): SpawnSyncReturns<string> => {
|
|
42
|
-
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "")
|
|
43
|
-
const executablePath = resolvesSuffixes ? which(command, host.executableSuffixes) : command
|
|
44
|
-
if (executablePath === "") return notFound(command)
|
|
45
|
-
let invocation: Invocation
|
|
42
|
+
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "");
|
|
43
|
+
const executablePath = resolvesSuffixes ? which(command, host.executableSuffixes) : command;
|
|
44
|
+
if (executablePath === "") return notFound(command);
|
|
45
|
+
let invocation: Invocation;
|
|
46
46
|
try {
|
|
47
|
-
invocation = host.invoke(executablePath, args)
|
|
47
|
+
invocation = host.invoke(executablePath, args);
|
|
48
48
|
} catch (cause) {
|
|
49
49
|
// A value this host cannot put on a command line at all. Print the encoder's
|
|
50
50
|
// reason and exit, matching how a caller reports a failed child.
|
|
51
|
-
process.stderr.write(`${cause instanceof Error ? cause.message : String(cause)}\n`)
|
|
52
|
-
return process.exit(2)
|
|
51
|
+
process.stderr.write(`${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
52
|
+
return process.exit(2);
|
|
53
53
|
}
|
|
54
54
|
const options: SpawnSyncOptionsWithStringEncoding = {
|
|
55
55
|
stdio: ["ignore", "pipe", "pipe"],
|
|
56
56
|
...overrides,
|
|
57
57
|
encoding: "utf8",
|
|
58
|
-
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
59
|
-
}
|
|
60
|
-
return spawnSync(invocation.command, [...invocation.args], options)
|
|
61
|
-
}
|
|
58
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
59
|
+
};
|
|
60
|
+
return spawnSync(invocation.command, [...invocation.args], options);
|
|
61
|
+
};
|
|
62
62
|
|
|
63
63
|
/** Keep engine paths slash-separated so rendered output is host-stable. */
|
|
64
64
|
export function p(...parts: Array<string>): string {
|
|
65
|
-
return parts.join("/")
|
|
65
|
+
return parts.join("/");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export interface AsyncProcessResult {
|
|
69
|
-
readonly exitCode: number | null
|
|
70
|
-
readonly stdout: string
|
|
71
|
-
readonly stderr: string
|
|
72
|
-
readonly error?: Error
|
|
69
|
+
readonly exitCode: number | null;
|
|
70
|
+
readonly stdout: string;
|
|
71
|
+
readonly stderr: string;
|
|
72
|
+
readonly error?: Error;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
export interface AsyncProcessOptions {
|
|
76
|
-
readonly stdio?: SpawnOptions["stdio"]
|
|
76
|
+
readonly stdio?: SpawnOptions["stdio"];
|
|
77
77
|
/** Host whose executable resolution and argv shaping apply; tests inject it. */
|
|
78
|
-
readonly host?: HostOs
|
|
78
|
+
readonly host?: HostOs;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export function spawnProcess(
|
|
82
82
|
cmd: string,
|
|
83
83
|
args: ReadonlyArray<string>,
|
|
84
|
-
options: AsyncProcessOptions = {}
|
|
84
|
+
options: AsyncProcessOptions = {},
|
|
85
85
|
): Promise<AsyncProcessResult> {
|
|
86
|
-
const { promise, resolve } = Promise.withResolvers<AsyncProcessResult>()
|
|
87
|
-
const host = options.host ?? hostOs()
|
|
88
|
-
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "")
|
|
89
|
-
const executablePath = resolvesSuffixes ? which(cmd, host.executableSuffixes) : cmd
|
|
86
|
+
const { promise, resolve } = Promise.withResolvers<AsyncProcessResult>();
|
|
87
|
+
const host = options.host ?? hostOs();
|
|
88
|
+
const resolvesSuffixes = host.executableSuffixes.some((suffix) => suffix !== "");
|
|
89
|
+
const executablePath = resolvesSuffixes ? which(cmd, host.executableSuffixes) : cmd;
|
|
90
90
|
if (executablePath === "") {
|
|
91
91
|
// Never hand a pathless name to a host that resolves suffixes: CreateProcess
|
|
92
92
|
// searches the parent's current directory before the system one, so an
|
|
93
93
|
// untrusted checkout could answer for a missing tool.
|
|
94
|
-
resolve({
|
|
95
|
-
|
|
94
|
+
resolve({
|
|
95
|
+
exitCode: null,
|
|
96
|
+
stdout: "",
|
|
97
|
+
stderr: "",
|
|
98
|
+
error: new Error(`command not found on PATH: ${cmd}`),
|
|
99
|
+
});
|
|
100
|
+
return promise;
|
|
96
101
|
}
|
|
97
|
-
let child: ChildProcess
|
|
102
|
+
let child: ChildProcess;
|
|
98
103
|
try {
|
|
99
104
|
// Inside the try: a host whose invocation encoding rejects a value it
|
|
100
105
|
// cannot represent reports it like any other spawn failure.
|
|
101
|
-
const invocation = host.invoke(executablePath, args)
|
|
106
|
+
const invocation = host.invoke(executablePath, args);
|
|
102
107
|
child = spawn(invocation.command, [...invocation.args], {
|
|
103
108
|
stdio: options.stdio ?? ["ignore", "pipe", "ignore"],
|
|
104
|
-
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
105
|
-
})
|
|
109
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
|
|
110
|
+
});
|
|
106
111
|
} catch (cause) {
|
|
107
|
-
const error = cause instanceof Error ? cause : new Error(String(cause))
|
|
108
|
-
resolve({ exitCode: null, stdout: "", stderr: "", error })
|
|
109
|
-
return promise
|
|
112
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
113
|
+
resolve({ exitCode: null, stdout: "", stderr: "", error });
|
|
114
|
+
return promise;
|
|
110
115
|
}
|
|
111
|
-
let stdout = ""
|
|
112
|
-
let stderr = ""
|
|
113
|
-
let error: Error | undefined
|
|
116
|
+
let stdout = "";
|
|
117
|
+
let stderr = "";
|
|
118
|
+
let error: Error | undefined;
|
|
114
119
|
|
|
115
120
|
if (child.stdout !== null) {
|
|
116
|
-
child.stdout.setEncoding("utf8")
|
|
121
|
+
child.stdout.setEncoding("utf8");
|
|
117
122
|
child.stdout.on("data", (chunk: string) => {
|
|
118
|
-
stdout += chunk
|
|
119
|
-
})
|
|
123
|
+
stdout += chunk;
|
|
124
|
+
});
|
|
120
125
|
child.stdout.on("error", (cause) => {
|
|
121
|
-
error ??= cause
|
|
122
|
-
})
|
|
126
|
+
error ??= cause;
|
|
127
|
+
});
|
|
123
128
|
}
|
|
124
129
|
if (child.stderr !== null) {
|
|
125
|
-
child.stderr.setEncoding("utf8")
|
|
130
|
+
child.stderr.setEncoding("utf8");
|
|
126
131
|
child.stderr.on("data", (chunk: string) => {
|
|
127
|
-
stderr += chunk
|
|
128
|
-
})
|
|
132
|
+
stderr += chunk;
|
|
133
|
+
});
|
|
129
134
|
child.stderr.on("error", (cause) => {
|
|
130
|
-
error ??= cause
|
|
131
|
-
})
|
|
135
|
+
error ??= cause;
|
|
136
|
+
});
|
|
132
137
|
}
|
|
133
138
|
child.once("error", (cause) => {
|
|
134
|
-
error ??= cause
|
|
135
|
-
})
|
|
139
|
+
error ??= cause;
|
|
140
|
+
});
|
|
136
141
|
child.once("close", (exitCode) => {
|
|
137
|
-
resolve({ exitCode, stdout, stderr, ...(error !== undefined ? { error } : {}) })
|
|
138
|
-
})
|
|
139
|
-
return promise
|
|
142
|
+
resolve({ exitCode, stdout, stderr, ...(error !== undefined ? { error } : {}) });
|
|
143
|
+
});
|
|
144
|
+
return promise;
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
export async function capture(cmd: string, args: ReadonlyArray<string>): Promise<string> {
|
|
143
|
-
const res = await spawnProcess(cmd, args, { stdio: ["ignore", "pipe", "ignore"] })
|
|
144
|
-
if (res.error !== undefined || res.exitCode !== 0) return ""
|
|
145
|
-
return res.stdout.replace(/[\r\n]+$/, "")
|
|
148
|
+
const res = await spawnProcess(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
149
|
+
if (res.error !== undefined || res.exitCode !== 0) return "";
|
|
150
|
+
return res.stdout.replace(/[\r\n]+$/, "");
|
|
146
151
|
}
|
|
147
152
|
|
|
148
153
|
/** `command -v` — resolve an executable name on PATH. */
|
|
149
|
-
export function which(
|
|
154
|
+
export function which(
|
|
155
|
+
name: string,
|
|
156
|
+
suffixes: ReadonlyArray<string> = hostOs().executableSuffixes,
|
|
157
|
+
): string {
|
|
150
158
|
const runnableCandidate = (base: string): string => {
|
|
151
159
|
for (const suffix of suffixes) {
|
|
152
|
-
const candidate = `${base}${suffix}
|
|
153
|
-
if (isExecutable(candidate, suffixes)) return candidate
|
|
160
|
+
const candidate = `${base}${suffix}`;
|
|
161
|
+
if (isExecutable(candidate, suffixes)) return candidate;
|
|
154
162
|
}
|
|
155
|
-
return ""
|
|
156
|
-
}
|
|
163
|
+
return "";
|
|
164
|
+
};
|
|
157
165
|
|
|
158
|
-
if (isAbsolute(name) || name.includes("/")) return runnableCandidate(name)
|
|
166
|
+
if (isAbsolute(name) || name.includes("/")) return runnableCandidate(name);
|
|
159
167
|
for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
|
|
160
|
-
if (dir === "") continue
|
|
161
|
-
const candidate = runnableCandidate(join(dir, name))
|
|
162
|
-
if (candidate !== "") return candidate
|
|
168
|
+
if (dir === "") continue;
|
|
169
|
+
const candidate = runnableCandidate(join(dir, name));
|
|
170
|
+
if (candidate !== "") return candidate;
|
|
163
171
|
}
|
|
164
|
-
return ""
|
|
172
|
+
return "";
|
|
165
173
|
}
|
|
166
174
|
|
|
167
175
|
export function commandExists(name: string): boolean {
|
|
168
|
-
return which(name) !== ""
|
|
176
|
+
return which(name) !== "";
|
|
169
177
|
}
|
|
170
178
|
|
|
171
|
-
function isExecutable(
|
|
179
|
+
function isExecutable(
|
|
180
|
+
path: string,
|
|
181
|
+
suffixes: ReadonlyArray<string> = hostOs().executableSuffixes,
|
|
182
|
+
): boolean {
|
|
172
183
|
try {
|
|
173
|
-
if (!statSync(path).isFile()) return false
|
|
184
|
+
if (!statSync(path).isFile()) return false;
|
|
174
185
|
if (suffixes.some((suffix) => suffix !== "")) {
|
|
175
|
-
const lowerPath = path.toLowerCase()
|
|
186
|
+
const lowerPath = path.toLowerCase();
|
|
176
187
|
return suffixes.some((suffix) =>
|
|
177
|
-
suffix === "" ? extname(path) === "" : lowerPath.endsWith(suffix.toLowerCase())
|
|
178
|
-
)
|
|
188
|
+
suffix === "" ? extname(path) === "" : lowerPath.endsWith(suffix.toLowerCase()),
|
|
189
|
+
);
|
|
179
190
|
}
|
|
180
|
-
accessSync(path, constants.X_OK)
|
|
181
|
-
return true
|
|
191
|
+
accessSync(path, constants.X_OK);
|
|
192
|
+
return true;
|
|
182
193
|
} catch {
|
|
183
|
-
return false
|
|
194
|
+
return false;
|
|
184
195
|
}
|
|
185
196
|
}
|
|
186
197
|
|
|
@@ -189,14 +200,14 @@ function isExecutable(path: string, suffixes: ReadonlyArray<string> = hostOs().e
|
|
|
189
200
|
|
|
190
201
|
/** Write only when the content differs; returns whether a write happened. */
|
|
191
202
|
export function writeTextIfChanged(path: string, content: string): boolean {
|
|
192
|
-
if (existsSync(path) && readFileSync(path, "utf8") === content) return false
|
|
193
|
-
writeFileSync(path, content)
|
|
194
|
-
return true
|
|
203
|
+
if (existsSync(path) && readFileSync(path, "utf8") === content) return false;
|
|
204
|
+
writeFileSync(path, content);
|
|
205
|
+
return true;
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
export function writeBytesIfChanged(path: string, content: Uint8Array): boolean {
|
|
198
|
-
const bytes = Buffer.from(content)
|
|
199
|
-
if (existsSync(path) && readFileSync(path).equals(bytes)) return false
|
|
200
|
-
writeFileSync(path, bytes)
|
|
201
|
-
return true
|
|
209
|
+
const bytes = Buffer.from(content);
|
|
210
|
+
if (existsSync(path) && readFileSync(path).equals(bytes)) return false;
|
|
211
|
+
writeFileSync(path, bytes);
|
|
212
|
+
return true;
|
|
202
213
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* plugin command that exits non-zero is recorded here so engineSync can list
|
|
4
4
|
* it and exit non-zero instead of reporting a clean sync.
|
|
5
5
|
*/
|
|
6
|
-
import type { Ctx } from "./index"
|
|
6
|
+
import type { Ctx } from "./index";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Record a failed harness-CLI operation. The warning still prints immediately so
|
|
@@ -11,6 +11,6 @@ import type { Ctx } from "./index"
|
|
|
11
11
|
* list the failure in the summary and exit non-zero.
|
|
12
12
|
*/
|
|
13
13
|
export function recordFailure(ctx: Ctx, message: string): void {
|
|
14
|
-
ctx.failures.push(message)
|
|
15
|
-
ctx.services.logger.warn(message)
|
|
14
|
+
ctx.failures.push(message);
|
|
15
|
+
ctx.services.logger.warn(message);
|
|
16
16
|
}
|
|
@@ -4,153 +4,143 @@
|
|
|
4
4
|
* undefined so callers resolve it to LEGACY_SELECTION and existing machines
|
|
5
5
|
* keep today's behavior.
|
|
6
6
|
*/
|
|
7
|
-
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
8
|
-
import { homedir } from "node:os"
|
|
7
|
+
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
9
|
|
|
10
|
-
import { p } from "./exec"
|
|
10
|
+
import { p } from "./exec";
|
|
11
|
+
import type { OmpSessionModel } from "./sharedTypes";
|
|
11
12
|
|
|
12
|
-
export type Harness = "claude" | "codex" | "agents" | "omp"
|
|
13
|
+
export type Harness = "claude" | "codex" | "agents" | "omp";
|
|
13
14
|
|
|
14
|
-
export const HARNESSES: ReadonlyArray<Harness> = ["claude", "codex", "agents", "omp"]
|
|
15
|
-
export const LEGACY_SELECTION: ReadonlyArray<Harness> = ["claude", "codex", "agents"]
|
|
15
|
+
export const HARNESSES: ReadonlyArray<Harness> = ["claude", "codex", "agents", "omp"];
|
|
16
|
+
export const LEGACY_SELECTION: ReadonlyArray<Harness> = ["claude", "codex", "agents"];
|
|
16
17
|
|
|
17
|
-
export
|
|
18
|
-
readonly selector: string
|
|
19
|
-
// Session ceiling; absent when the model publishes no ladder, so no
|
|
20
|
-
// invented level ever reaches a selector that omp must resolve.
|
|
21
|
-
readonly thinking?: string
|
|
22
|
-
// Advisor level; absent when the model publishes no ladder.
|
|
23
|
-
readonly advisorThinking?: string
|
|
24
|
-
}
|
|
18
|
+
export type { OmpSessionModel };
|
|
25
19
|
|
|
26
20
|
export const DEFAULT_OMP_SESSION_MODEL: OmpSessionModel = {
|
|
27
21
|
selector: "opencode-zen/muse-spark-1.3-contributor-free",
|
|
28
22
|
thinking: "xhigh",
|
|
29
23
|
advisorThinking: "medium",
|
|
30
|
-
}
|
|
24
|
+
};
|
|
31
25
|
|
|
32
26
|
function isHarness(value: unknown): value is Harness {
|
|
33
|
-
return value === "claude" || value === "codex" || value === "agents" || value === "omp"
|
|
27
|
+
return value === "claude" || value === "codex" || value === "agents" || value === "omp";
|
|
34
28
|
}
|
|
35
29
|
|
|
36
30
|
function normalizeHarnesses(selection: ReadonlyArray<unknown>): Array<Harness> {
|
|
37
|
-
const selected = new Set<Harness>()
|
|
31
|
+
const selected = new Set<Harness>();
|
|
38
32
|
for (const value of selection) {
|
|
39
|
-
if (isHarness(value)) selected.add(value)
|
|
33
|
+
if (isHarness(value)) selected.add(value);
|
|
40
34
|
}
|
|
41
|
-
return HARNESSES.filter((harness) => selected.has(harness))
|
|
35
|
+
return HARNESSES.filter((harness) => selected.has(harness));
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
/** Resolve the engine home root from HOME with the platform home as fallback. */
|
|
45
39
|
export function engineHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
46
|
-
const home = env["HOME"]
|
|
47
|
-
return home !== undefined && home !== "" ? home : homedir()
|
|
40
|
+
const home = env["HOME"];
|
|
41
|
+
return home !== undefined && home !== "" ? home : homedir();
|
|
48
42
|
}
|
|
49
43
|
|
|
50
44
|
export function harnessStateFile(home: string): string {
|
|
51
|
-
return p(home, ".docks-kit", "state.json")
|
|
45
|
+
return p(home, ".docks-kit", "state.json");
|
|
52
46
|
}
|
|
53
47
|
|
|
54
48
|
// Read the whole state record so one key writer keeps sibling keys intact.
|
|
55
49
|
// A corrupt file degrades to undefined so callers fall back to defaults.
|
|
56
50
|
function readWholeState(home: string): Record<string, unknown> | undefined {
|
|
57
|
-
let parsed: unknown
|
|
51
|
+
let parsed: unknown;
|
|
58
52
|
try {
|
|
59
|
-
parsed = JSON.parse(readFileSync(harnessStateFile(home), "utf8")) as unknown
|
|
53
|
+
parsed = JSON.parse(readFileSync(harnessStateFile(home), "utf8")) as unknown;
|
|
60
54
|
} catch {
|
|
61
|
-
return undefined
|
|
55
|
+
return undefined;
|
|
62
56
|
}
|
|
63
57
|
|
|
64
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined
|
|
65
|
-
const state = parsed as Record<string, unknown
|
|
66
|
-
if (state["version"] !== 1) return undefined
|
|
67
|
-
return state
|
|
58
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
|
|
59
|
+
const state = parsed as Record<string, unknown>;
|
|
60
|
+
if (state["version"] !== 1) return undefined;
|
|
61
|
+
return state;
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
// Merge the patch over the stored record so independent keys never erase
|
|
71
65
|
// each other when only one writer runs.
|
|
72
66
|
function writeWholeState(home: string, patch: Record<string, unknown>): void {
|
|
73
|
-
const existing = readWholeState(home) ?? {}
|
|
74
|
-
const next = { ...existing, ...patch, version: 1 }
|
|
75
|
-
const directory = p(home, ".docks-kit")
|
|
76
|
-
const file = harnessStateFile(home)
|
|
77
|
-
const text = `${JSON.stringify(next, null, 2)}\n
|
|
67
|
+
const existing = readWholeState(home) ?? {};
|
|
68
|
+
const next = { ...existing, ...patch, version: 1 };
|
|
69
|
+
const directory = p(home, ".docks-kit");
|
|
70
|
+
const file = harnessStateFile(home);
|
|
71
|
+
const text = `${JSON.stringify(next, null, 2)}\n`;
|
|
78
72
|
// `mode` applies only when mkdir creates the path, so an existing permissive
|
|
79
73
|
// ~/.docks-kit would keep its mode.
|
|
80
|
-
mkdirSync(directory, { recursive: true, mode: 0o700 })
|
|
81
|
-
chmodSync(directory, 0o700)
|
|
82
|
-
writeFileSync(file, text, { mode: 0o600 })
|
|
83
|
-
chmodSync(file, 0o600)
|
|
74
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
75
|
+
chmodSync(directory, 0o700);
|
|
76
|
+
writeFileSync(file, text, { mode: 0o600 });
|
|
77
|
+
chmodSync(file, 0o600);
|
|
84
78
|
}
|
|
85
79
|
|
|
86
80
|
/** Read valid local state without allowing corruption to make sync unusable. */
|
|
87
81
|
export function readHarnessSelection(home: string): ReadonlyArray<Harness> | undefined {
|
|
88
|
-
const state = readWholeState(home)
|
|
89
|
-
if (state === undefined || !Array.isArray(state["harnesses"])) return undefined
|
|
82
|
+
const state = readWholeState(home);
|
|
83
|
+
if (state === undefined || !Array.isArray(state["harnesses"])) return undefined;
|
|
90
84
|
|
|
91
|
-
const selection = normalizeHarnesses(state["harnesses"])
|
|
92
|
-
return selection.length > 0 ? selection : undefined
|
|
85
|
+
const selection = normalizeHarnesses(state["harnesses"]);
|
|
86
|
+
return selection.length > 0 ? selection : undefined;
|
|
93
87
|
}
|
|
94
88
|
|
|
95
89
|
export function writeHarnessSelection(home: string, selection: ReadonlyArray<Harness>): void {
|
|
96
90
|
if (selection.length === 0) {
|
|
97
|
-
throw new Error("Cannot write an empty harness selection because sync would become a no-op")
|
|
91
|
+
throw new Error("Cannot write an empty harness selection because sync would become a no-op");
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
const harnesses = normalizeHarnesses(selection)
|
|
94
|
+
const harnesses = normalizeHarnesses(selection);
|
|
101
95
|
if (harnesses.length === 0) {
|
|
102
|
-
throw new Error("Harness selection must contain at least one known harness name")
|
|
96
|
+
throw new Error("Harness selection must contain at least one known harness name");
|
|
103
97
|
}
|
|
104
98
|
|
|
105
|
-
writeWholeState(home, { harnesses })
|
|
99
|
+
writeWholeState(home, { harnesses });
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
function isNonBlankString(value: unknown): value is string {
|
|
109
|
-
return typeof value === "string" && value.trim() !== ""
|
|
103
|
+
return typeof value === "string" && value.trim() !== "";
|
|
110
104
|
}
|
|
111
105
|
|
|
112
106
|
// Read the stored session model without throwing so a corrupt entry falls
|
|
113
107
|
// back to the default instead of breaking sync.
|
|
114
108
|
export function readOmpSessionModel(home: string): OmpSessionModel | undefined {
|
|
115
|
-
const state = readWholeState(home)
|
|
116
|
-
if (state === undefined) return undefined
|
|
117
|
-
const entry = state["ompSession"]
|
|
118
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return undefined
|
|
119
|
-
const record = entry as Record<string, unknown
|
|
109
|
+
const state = readWholeState(home);
|
|
110
|
+
if (state === undefined) return undefined;
|
|
111
|
+
const entry = state["ompSession"];
|
|
112
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return undefined;
|
|
113
|
+
const record = entry as Record<string, unknown>;
|
|
120
114
|
if (!isNonBlankString(record["selector"])) {
|
|
121
|
-
return undefined
|
|
115
|
+
return undefined;
|
|
122
116
|
}
|
|
123
117
|
// Each level stands alone, so a level-free model reads back with no
|
|
124
118
|
// levels while a half-corrupt entry keeps the valid level.
|
|
125
|
-
const
|
|
119
|
+
const thinking = record["thinking"];
|
|
120
|
+
const advisorThinking = record["advisorThinking"];
|
|
121
|
+
const model: OmpSessionModel = {
|
|
126
122
|
selector: record["selector"],
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (isNonBlankString(record["advisorThinking"])) {
|
|
132
|
-
model.advisorThinking = record["advisorThinking"]
|
|
133
|
-
}
|
|
134
|
-
return model
|
|
123
|
+
...(isNonBlankString(thinking) ? { thinking } : {}),
|
|
124
|
+
...(isNonBlankString(advisorThinking) ? { advisorThinking } : {}),
|
|
125
|
+
};
|
|
126
|
+
return model;
|
|
135
127
|
}
|
|
136
128
|
|
|
137
129
|
export function writeOmpSessionModel(home: string, model: OmpSessionModel): void {
|
|
138
130
|
// A blank selector would make omp resolve an arbitrary model, so reject it
|
|
139
131
|
// before anything reaches disk.
|
|
140
132
|
if (!isNonBlankString(model.selector)) {
|
|
141
|
-
throw new Error("Omp session model selector must be a non-empty string")
|
|
133
|
+
throw new Error("Omp session model selector must be a non-empty string");
|
|
142
134
|
}
|
|
143
135
|
// Persist only non-blank levels so a switch to a level-free model leaves
|
|
144
136
|
// no stale level behind in the stored record.
|
|
145
|
-
const
|
|
137
|
+
const thinking = model.thinking;
|
|
138
|
+
const advisorThinking = model.advisorThinking;
|
|
139
|
+
const entry: OmpSessionModel = {
|
|
146
140
|
selector: model.selector,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
if (isNonBlankString(model.advisorThinking)) {
|
|
152
|
-
entry.advisorThinking = model.advisorThinking
|
|
153
|
-
}
|
|
141
|
+
...(isNonBlankString(thinking) ? { thinking } : {}),
|
|
142
|
+
...(isNonBlankString(advisorThinking) ? { advisorThinking } : {}),
|
|
143
|
+
};
|
|
154
144
|
|
|
155
|
-
writeWholeState(home, { ompSession: entry })
|
|
145
|
+
writeWholeState(home, { ompSession: entry });
|
|
156
146
|
}
|