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.
Files changed (87) hide show
  1. package/AGENTS.md +41 -33
  2. package/cli/docs/omp-context.md +104 -0
  3. package/cli/docs/omp-models.md +106 -56
  4. package/cli/src/argv.ts +38 -469
  5. package/cli/src/argvSurface.ts +371 -0
  6. package/cli/src/argvValidate.ts +122 -0
  7. package/cli/src/commands/docs.ts +62 -42
  8. package/cli/src/commands/harnesses.ts +38 -45
  9. package/cli/src/commands/model.ts +48 -53
  10. package/cli/src/commands/models.ts +24 -26
  11. package/cli/src/commands/omp.ts +132 -112
  12. package/cli/src/commands/plugins.ts +20 -20
  13. package/cli/src/commands/skills.ts +22 -20
  14. package/cli/src/commands/status.ts +87 -80
  15. package/cli/src/commands/sync.ts +113 -87
  16. package/cli/src/commands/toolchain.ts +21 -21
  17. package/cli/src/commands/update.ts +139 -117
  18. package/cli/src/efforts.ts +35 -34
  19. package/cli/src/engine-native/DESIGN.md +30 -30
  20. package/cli/src/engine-native/bun.ts +77 -55
  21. package/cli/src/engine-native/claudeLsp.ts +154 -0
  22. package/cli/src/engine-native/claudeOptionalPlugins.ts +113 -0
  23. package/cli/src/engine-native/claudePluginPasses.ts +348 -0
  24. package/cli/src/engine-native/claudeRemovals.ts +195 -0
  25. package/cli/src/engine-native/claudeRetired.ts +4 -4
  26. package/cli/src/engine-native/claudeRuntime.ts +94 -68
  27. package/cli/src/engine-native/claudeSettings.ts +174 -0
  28. package/cli/src/engine-native/claudeSettingsModifiers.ts +50 -54
  29. package/cli/src/engine-native/claudeSync.ts +176 -456
  30. package/cli/src/engine-native/codexConfig.ts +158 -0
  31. package/cli/src/engine-native/codexHooks.ts +91 -0
  32. package/cli/src/engine-native/codexPlugins.ts +290 -0
  33. package/cli/src/engine-native/codexStatus.ts +24 -0
  34. package/cli/src/engine-native/codexSync.ts +110 -568
  35. package/cli/src/engine-native/codexToml.ts +140 -138
  36. package/cli/src/engine-native/deps.ts +144 -107
  37. package/cli/src/engine-native/engineCtx.ts +126 -0
  38. package/cli/src/engine-native/exec.ts +98 -87
  39. package/cli/src/engine-native/failures.ts +3 -3
  40. package/cli/src/engine-native/harnesses.ts +62 -72
  41. package/cli/src/engine-native/index.ts +34 -315
  42. package/cli/src/engine-native/jq.ts +25 -24
  43. package/cli/src/engine-native/logger.ts +98 -100
  44. package/cli/src/engine-native/models.ts +50 -45
  45. package/cli/src/engine-native/modes.ts +83 -80
  46. package/cli/src/engine-native/ompFileDeploy.ts +111 -0
  47. package/cli/src/engine-native/ompMarketplace.ts +136 -0
  48. package/cli/src/engine-native/ompOverlay.ts +67 -64
  49. package/cli/src/engine-native/ompPaths.ts +49 -38
  50. package/cli/src/engine-native/ompPlugins.ts +170 -0
  51. package/cli/src/engine-native/ompRemovals.ts +159 -0
  52. package/cli/src/engine-native/ompSync.ts +69 -409
  53. package/cli/src/engine-native/ompYaml.ts +43 -41
  54. package/cli/src/engine-native/os/darwin.ts +5 -5
  55. package/cli/src/engine-native/os/index.ts +15 -15
  56. package/cli/src/engine-native/os/linux.ts +5 -5
  57. package/cli/src/engine-native/os/posix.ts +28 -24
  58. package/cli/src/engine-native/os/targets.ts +17 -17
  59. package/cli/src/engine-native/os/types.ts +31 -31
  60. package/cli/src/engine-native/os/windows.ts +61 -61
  61. package/cli/src/engine-native/parseArgs.ts +122 -372
  62. package/cli/src/engine-native/parseHelp.ts +66 -0
  63. package/cli/src/engine-native/parseModifiers.ts +222 -0
  64. package/cli/src/engine-native/services.ts +34 -34
  65. package/cli/src/engine-native/settings.ts +15 -15
  66. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  67. package/cli/src/engine-native/skillsInstall.ts +105 -0
  68. package/cli/src/engine-native/skillsLinks.ts +203 -0
  69. package/cli/src/engine-native/skillsManifest.ts +16 -0
  70. package/cli/src/engine-native/skillsPrune.ts +108 -0
  71. package/cli/src/engine-native/skillsSync.ts +28 -362
  72. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  73. package/cli/src/engine-native/syncDispatch.ts +141 -0
  74. package/cli/src/engine-native/toolchain.ts +65 -62
  75. package/cli/src/engine.ts +65 -57
  76. package/cli/src/generated/sotPayload.ts +7 -7
  77. package/cli/src/kitHome.ts +44 -40
  78. package/cli/src/main.ts +63 -56
  79. package/cli/src/manifests.ts +53 -54
  80. package/cli/src/md.d.ts +2 -2
  81. package/cli/src/payload.ts +9 -9
  82. package/cli/src/services.ts +21 -13
  83. package/cli/tsconfig.json +4 -1
  84. package/docks-kit +13 -2
  85. package/docks-kit.ps1 +21 -6
  86. package/package.json +10 -2
  87. 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({ exitCode: null, stdout: "", stderr: "", error: new Error(`command not found on PATH: ${cmd}`) })
95
- return promise
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(name: string, suffixes: ReadonlyArray<string> = hostOs().executableSuffixes): string {
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(path: string, suffixes: ReadonlyArray<string> = hostOs().executableSuffixes): boolean {
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 interface OmpSessionModel {
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 model: { selector: string; thinking?: string; advisorThinking?: string } = {
119
+ const thinking = record["thinking"];
120
+ const advisorThinking = record["advisorThinking"];
121
+ const model: OmpSessionModel = {
126
122
  selector: record["selector"],
127
- }
128
- if (isNonBlankString(record["thinking"])) {
129
- model.thinking = record["thinking"]
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 entry: { selector: string; thinking?: string; advisorThinking?: string } = {
137
+ const thinking = model.thinking;
138
+ const advisorThinking = model.advisorThinking;
139
+ const entry: OmpSessionModel = {
146
140
  selector: model.selector,
147
- }
148
- if (isNonBlankString(model.thinking)) {
149
- entry.thinking = model.thinking
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
  }