docks-kit 0.17.0 → 0.17.2

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 (84) hide show
  1. package/AGENTS.md +37 -33
  2. package/README.md +1 -0
  3. package/cli/docs/omp-models.md +114 -58
  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 +57 -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 +178 -88
  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 +93 -53
  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/ompSync.ts +64 -408
  52. package/cli/src/engine-native/ompYaml.ts +43 -41
  53. package/cli/src/engine-native/os/darwin.ts +5 -5
  54. package/cli/src/engine-native/os/index.ts +15 -15
  55. package/cli/src/engine-native/os/linux.ts +5 -5
  56. package/cli/src/engine-native/os/posix.ts +28 -24
  57. package/cli/src/engine-native/os/targets.ts +17 -17
  58. package/cli/src/engine-native/os/types.ts +31 -31
  59. package/cli/src/engine-native/os/windows.ts +61 -61
  60. package/cli/src/engine-native/parseArgs.ts +122 -372
  61. package/cli/src/engine-native/parseHelp.ts +66 -0
  62. package/cli/src/engine-native/parseModifiers.ts +222 -0
  63. package/cli/src/engine-native/services.ts +34 -34
  64. package/cli/src/engine-native/settings.ts +15 -15
  65. package/cli/src/engine-native/sharedTypes.d.ts +46 -0
  66. package/cli/src/engine-native/skillsInstall.ts +105 -0
  67. package/cli/src/engine-native/skillsLinks.ts +203 -0
  68. package/cli/src/engine-native/skillsManifest.ts +16 -0
  69. package/cli/src/engine-native/skillsPrune.ts +108 -0
  70. package/cli/src/engine-native/skillsSync.ts +28 -362
  71. package/cli/src/engine-native/syncConcurrency.ts +55 -0
  72. package/cli/src/engine-native/syncDispatch.ts +141 -0
  73. package/cli/src/engine-native/toolchain.ts +65 -62
  74. package/cli/src/engine.ts +65 -57
  75. package/cli/src/generated/sotPayload.ts +7 -7
  76. package/cli/src/kitHome.ts +44 -40
  77. package/cli/src/main.ts +63 -56
  78. package/cli/src/manifests.ts +53 -54
  79. package/cli/src/md.d.ts +2 -2
  80. package/cli/src/payload.ts +9 -9
  81. package/cli/src/services.ts +21 -13
  82. package/cli/tsconfig.json +4 -1
  83. package/package.json +9 -2
  84. package/cli/src/engine-native/claudePlugins.ts +0 -510
@@ -4,341 +4,60 @@
4
4
  * Golden suites drive it through main.ts's harness-private `native-raw`
5
5
  * channel, which bypasses @effect/cli so tests see the internal argv
6
6
  * vocabulary directly.
7
+ *
8
+ * Split: run context lives in `engineCtx.ts`, scheduling in
9
+ * `syncConcurrency.ts`, and pipeline dispatch in `syncDispatch.ts`. This
10
+ * module keeps the entry routing and re-exports the public surface so
11
+ * existing import paths keep working.
7
12
  */
8
- import { p } from "./exec"
9
- import { engineHome } from "./harnesses"
10
-
11
- import { kitHome } from "../kitHome"
12
- import { payloadText } from "../payload"
13
- import { makeEngineServices, type EngineServices, type Logger } from "./services"
14
- import type { TerminalLease } from "./logger"
15
- import type { BunRuntimeState } from "./bun"
16
- import { claudeNextSteps, claudeSummary, claudeSync, type ClaudeRuntimeState } from "./claudeSync"
17
- import { codexNextSteps, codexSummary, codexSync } from "./codexSync"
18
- import { ompNextSteps, ompSummary, ompSync, type OmpState } from "./ompSync"
19
- import { normalizeManifest, skillsNextSteps, skillsSummary, skillsSync, type SkillsState } from "./skillsSync"
20
- import { modeModel, modeToolchain } from "./modes"
21
- import { ExitError, parseArgs, parseClaudePlugin, parseCompactWindow, validateModifierFlags } from "./parseArgs"
22
-
23
- export type ModifierFlag =
24
- | "--claude-model"
25
- | "--claude-effort"
26
- | "--claude-advisor"
27
- | "--claude-compact-window"
28
- | "--claude-permissive"
29
- | "--claude-plugin"
30
- | "--codex-model"
31
- | "--codex-effort"
32
-
33
- export type SyncConcurrency = 1 | 2 | 3
34
- export type SyncTask<T> = () => Promise<T>
35
-
36
- /**
37
- * Run input-ordered tasks with bounded overlap. Once one task rejects, queued
38
- * tasks stay queued while already-started tasks drain; the earliest rejection
39
- * in input order is then propagated.
40
- */
41
- export async function runBounded<T>(
42
- tasks: ReadonlyArray<SyncTask<T>>,
43
- concurrency: SyncConcurrency
44
- ): Promise<Array<T>> {
45
- const results = new Array<T>(tasks.length)
46
- const failures = new Map<number, unknown>()
47
- let nextIndex = 0
48
- let stopped = false
49
-
50
- const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
51
- for (;;) {
52
- if (stopped || nextIndex >= tasks.length) return
53
- const index = nextIndex
54
- nextIndex += 1
55
- try {
56
- results[index] = await tasks[index]!()
57
- } catch (error) {
58
- failures.set(index, error)
59
- stopped = true
60
- }
61
- }
62
- })
63
- await Promise.all(workers)
64
-
65
- for (let index = 0; index < tasks.length; index += 1) {
66
- if (failures.has(index)) throw failures.get(index)
67
- }
68
- return results
69
- }
70
-
71
- export function syncConcurrencyForManifest(
72
- configured: SyncConcurrency,
73
- manifest: string,
74
- claudeSelected: boolean,
75
- skillsSelected: boolean
76
- ): SyncConcurrency {
77
- if (!claudeSelected || !skillsSelected || normalizeManifest(manifest).length === 0) return configured
78
- return 1
79
- }
80
-
81
- export interface Ctx {
82
- readonly repoDir: string
83
- readonly home: string
84
- readonly agentsDir: string
85
- readonly interactive: boolean
86
- dryRun: boolean
87
- verbose: boolean
88
- skipBubblewrap: boolean
89
- skipPluginRefresh?: boolean
90
- reconcile: boolean
91
- prune: boolean
92
- claudeCompactWindow: string
93
- claudePermissive: boolean
94
- claudePlugins: Array<string>
95
- claudeModel: string
96
- claudeEffort: string
97
- claudeAdvisor: string
98
- codexModel: string
99
- codexEffort: string
100
- /** Distinguishes an explicitly empty modifier from an option that was not supplied. */
101
- modifierFlags?: Set<ModifierFlag>
102
- /** Injected capability seam (logger/deps/platform) — see services.ts. */
103
- readonly services: EngineServices
104
- syncConcurrency: SyncConcurrency
105
- terminalLease?: TerminalLease
106
- bunRuntime?: Promise<BunRuntimeState>
107
- targetFilterSet: boolean
108
- syncClaude: boolean
109
- syncCodex: boolean
110
- syncAgents: boolean
111
- syncOmp: boolean
112
- /** Per-run next-step triggers (Output Policy): advice prints only when its trigger changed or --verbose. */
113
- readonly nextStepTriggers: {
114
- claudePlugins: boolean
115
- claudeRestart: boolean
116
- codexRestart: boolean
117
- skillsRestart: boolean
118
- ompRestart: boolean
119
- }
120
- /** Harness-CLI operations that failed this run; a non-empty list fails the sync. */
121
- readonly failures: Array<string>
122
- }
123
-
124
- /** Globals default from env using the historical ${VAR:-default} contract. */
125
- function makeCtx(services: EngineServices): Ctx {
126
- const env = process.env
127
- const home = engineHome(env)
128
- const compactWindowSource = env["CLAUDE_COMPACT_WINDOW"] ?? ""
129
- const claudeCompactWindow = compactWindowSource === "" ? "" : parseCompactWindow(compactWindowSource)
130
- if (claudeCompactWindow === undefined) {
131
- services.logger.err("CLAUDE_COMPACT_WINDOW expects a token count (e.g. 680000 or 680k)")
132
- throw new ExitError(2)
133
- }
134
- const claudePlugins = (env["CLAUDE_PLUGINS"] ?? "")
135
- .split(" ")
136
- .filter((plugin) => plugin !== "")
137
- .map((plugin) => parseClaudePlugin(plugin, services.logger.err))
138
- return {
139
- repoDir: kitHome(),
140
- home,
141
- agentsDir: env["AGENTS_DIR"] !== undefined && env["AGENTS_DIR"] !== "" ? env["AGENTS_DIR"] : p(home, ".agents"),
142
- interactive:
143
- env["DOCKS_KIT_INTERACTIVE"] === "1" ||
144
- (env["DOCKS_KIT_INTERACTIVE"] !== "0" &&
145
- process.stdout.isTTY === true &&
146
- process.stdin.isTTY === true),
147
- dryRun: env["DRY_RUN"] === "1",
148
- verbose: env["DOCKS_KIT_VERBOSE"] === "1",
149
- skipBubblewrap: env["SKIP_BUBBLEWRAP"] === "1",
150
- skipPluginRefresh: false,
151
- reconcile: env["RECONCILE"] === "1",
152
- prune: env["PRUNE"] === "1",
153
- claudeCompactWindow,
154
- claudePermissive: env["CLAUDE_PERMISSIVE"] === "1",
155
- claudePlugins,
156
- claudeModel: env["CLAUDE_MODEL"] ?? "",
157
- claudeEffort: "",
158
- claudeAdvisor: "",
159
- codexModel: env["CODEX_MODEL"] ?? "",
160
- codexEffort: "",
161
- modifierFlags: new Set(),
162
- syncConcurrency: 3,
163
- services,
164
- targetFilterSet: false,
165
- syncClaude: false,
166
- syncCodex: false,
167
- syncAgents: false,
168
- syncOmp: false,
169
- nextStepTriggers: {
170
- claudePlugins: false,
171
- claudeRestart: false,
172
- codexRestart: false,
173
- skillsRestart: false,
174
- ompRestart: false
175
- },
176
- failures: []
177
- }
178
- }
179
-
180
- async function engineSync(ctx: Ctx, args: ReadonlyArray<string>): Promise<number> {
181
- const { acquireTerminal, echo, err } = ctx.services.logger
182
- parseArgs(ctx, args)
183
- validateModifierFlags(ctx)
184
-
185
- const configuredConcurrency = process.env["DOCKS_KIT_SYNC_CONCURRENCY"]
186
- if (configuredConcurrency === undefined || configuredConcurrency === "") {
187
- ctx.syncConcurrency = 3
188
- } else if (
189
- configuredConcurrency === "1" ||
190
- configuredConcurrency === "2" ||
191
- configuredConcurrency === "3"
192
- ) {
193
- ctx.syncConcurrency = Number(configuredConcurrency) as SyncConcurrency
194
- } else {
195
- err("DOCKS_KIT_SYNC_CONCURRENCY must be 1, 2, or 3")
196
- throw new ExitError(2)
197
- }
198
-
199
- type PipelineResult =
200
- | { readonly kind: "claude"; readonly runtime: ClaudeRuntimeState }
201
- | { readonly kind: "codex" }
202
- | { readonly kind: "skills"; readonly state: SkillsState }
203
- | { readonly kind: "omp"; readonly state: OmpState }
204
- interface SelectedPipeline {
205
- readonly name: string
206
- readonly run: SyncTask<PipelineResult>
207
- }
208
-
209
- const selected: Array<SelectedPipeline> = []
210
- if (ctx.syncClaude) {
211
- selected.push({
212
- name: "Claude",
213
- run: async () => ({ kind: "claude", runtime: await claudeSync(ctx) })
214
- })
215
- }
216
- if (ctx.syncCodex) {
217
- selected.push({
218
- name: "Codex",
219
- run: async () => {
220
- await codexSync(ctx)
221
- return { kind: "codex" }
222
- }
223
- })
224
- }
225
- if (ctx.syncAgents) {
226
- selected.push({
227
- name: "skills",
228
- run: async () => ({ kind: "skills", state: await skillsSync(ctx) })
229
- })
230
- }
231
- if (ctx.syncOmp) {
232
- selected.push({
233
- name: "omp",
234
- run: async () => ({ kind: "omp", state: await ompSync(ctx) })
235
- })
236
- }
237
-
238
- // A populated skills manifest deploys with `-a claude-code codex`, and symlink healing also writes into Claude's tree.
239
- ctx.syncConcurrency = syncConcurrencyForManifest(
240
- ctx.syncConcurrency,
241
- payloadText("SoT/.agents/skills.txt"),
242
- ctx.syncClaude,
243
- ctx.syncAgents
244
- )
245
-
246
- const remaining = new Set(selected.map(({ name }) => name))
247
- const lease = acquireTerminal(`Syncing ${[...remaining].join(", ")}...`)
248
- ctx.terminalLease = lease
249
- let results: Array<PipelineResult>
250
- try {
251
- const tasks = selected.map(
252
- ({ name, run }): SyncTask<PipelineResult> =>
253
- async () => {
254
- try {
255
- return await run()
256
- } finally {
257
- remaining.delete(name)
258
- if (remaining.size > 0) lease.update(`Syncing ${[...remaining].join(", ")}...`)
259
- }
260
- }
261
- )
262
- results = await runBounded(tasks, ctx.syncConcurrency)
263
- } finally {
264
- lease.release()
265
- ctx.terminalLease = undefined
266
- }
267
-
268
- const claudeRan = ctx.syncClaude
269
- const codexRan = ctx.syncCodex
270
- let claudeRuntime: ClaudeRuntimeState | undefined
271
- let skillsState: SkillsState | undefined
272
- let ompState: OmpState | undefined
273
- for (const result of results) {
274
- if (result.kind === "claude") claudeRuntime = result.runtime
275
- else if (result.kind === "skills") skillsState = result.state
276
- else if (result.kind === "omp") ompState = result.state
277
- }
278
-
279
- echo("")
280
- echo("--- Sync complete ---")
281
- echo(`Repo: ${ctx.repoDir}`)
282
- if (claudeRuntime !== undefined) claudeSummary(ctx, claudeRuntime)
283
- if (codexRan) codexSummary(ctx)
284
- if (skillsState !== undefined) skillsSummary(ctx, skillsState)
285
- if (ompState !== undefined) ompSummary(ctx, ompState)
286
-
287
- const advice = [
288
- ...(claudeRan ? claudeNextSteps(ctx) : []),
289
- ...(codexRan ? codexNextSteps(ctx) : []),
290
- ...(skillsState !== undefined ? skillsNextSteps(ctx) : []),
291
- ...(ompState !== undefined ? ompNextSteps(ctx) : [])
292
- ]
293
- if (advice.length > 0) {
294
- echo("")
295
- for (const line of advice) echo(line)
296
- }
297
- if (ctx.failures.length > 0) {
298
- echo("")
299
- echo("--- Failures ---")
300
- for (const failure of ctx.failures) echo(`- ${failure}`)
301
- return 1
302
- }
303
- return 0
304
- }
305
-
306
-
307
- export async function runEngineNative(argv: ReadonlyArray<string>, services?: EngineServices): Promise<number> {
308
- let ctx!: Ctx
309
- const baseServices = services ?? makeEngineServices()
310
- const baseLogger = baseServices.logger
13
+ import { makeCtx, type Ctx } from "./engineCtx";
14
+ import { modeModel, modeToolchain } from "./modes";
15
+ import { ExitError } from "./parseArgs";
16
+ import { makeEngineServices, type EngineServices, type Logger } from "./services";
17
+ import { engineSync } from "./syncDispatch";
18
+
19
+ export type { Ctx, ModifierFlag } from "./engineCtx";
20
+ export { runBounded, syncConcurrencyForManifest } from "./syncConcurrency";
21
+ export type { SyncConcurrency, SyncTask } from "./syncConcurrency";
22
+
23
+ export async function runEngineNative(
24
+ argv: ReadonlyArray<string>,
25
+ services?: EngineServices,
26
+ ): Promise<number> {
27
+ let ctx!: Ctx;
28
+ const baseServices = services ?? makeEngineServices();
29
+ const baseLogger = baseServices.logger;
311
30
  const logger: Logger = {
312
31
  change: (msg) => baseLogger.change(msg),
313
32
  progress: (msg) => baseLogger.progress(msg),
314
33
  clearProgress: () => baseLogger.clearProgress(),
315
34
  verbose: (msg) => {
316
- if (ctx.verbose) baseLogger.verbose(msg)
35
+ if (ctx.verbose) baseLogger.verbose(msg);
317
36
  },
318
37
  warn: (msg) => baseLogger.warn(msg),
319
38
  err: (msg) => baseLogger.err(msg),
320
39
  echo: (line) => baseLogger.echo(line),
321
- acquireTerminal: (message) => baseLogger.acquireTerminal(message)
322
- }
40
+ acquireTerminal: (message) => baseLogger.acquireTerminal(message),
41
+ };
323
42
  const runServices: EngineServices = {
324
43
  logger,
325
44
  deps: baseServices.deps,
326
- platform: baseServices.platform
327
- }
45
+ platform: baseServices.platform,
46
+ };
328
47
  try {
329
- ctx = makeCtx(runServices)
48
+ ctx = makeCtx(runServices);
330
49
  switch (argv[0]) {
331
50
  case "model":
332
- return modeModel(ctx, argv.slice(1))
51
+ return modeModel(ctx, argv.slice(1));
333
52
  case "toolchain":
334
- return await modeToolchain(ctx, argv.slice(1))
53
+ return await modeToolchain(ctx, argv.slice(1));
335
54
  case "sync":
336
- return await engineSync(ctx, argv.slice(1))
55
+ return await engineSync(ctx, argv.slice(1));
337
56
  default:
338
- return await engineSync(ctx, argv)
57
+ return await engineSync(ctx, argv);
339
58
  }
340
59
  } catch (e) {
341
- if (e instanceof ExitError) return e.code
342
- throw e
60
+ if (e instanceof ExitError) return e.code;
61
+ throw e;
343
62
  }
344
63
  }
@@ -11,60 +11,61 @@
11
11
  * - output formatting: 2-space indent, UTF-8 raw, trailing newline.
12
12
  */
13
13
 
14
- import { existsSync, readFileSync } from "node:fs"
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import type { JsonObject } from "./sharedTypes";
15
16
 
16
- export type Json = null | boolean | number | string | Array<Json> | { [key: string]: Json }
17
+ export type Json = null | boolean | number | string | Array<Json> | { [key: string]: Json };
17
18
 
18
- export function isObject(v: Json): v is { [key: string]: Json } {
19
- return typeof v === "object" && v !== null && !Array.isArray(v)
19
+ export function isObject(v: Json): v is JsonObject {
20
+ return typeof v === "object" && v !== null && !Array.isArray(v);
20
21
  }
21
22
 
22
23
  /** jq `$left * $right`. */
23
24
  export function deepMerge(left: Json, right: Json): Json {
24
- if (!isObject(left) || !isObject(right)) return right
25
- const out: { [key: string]: Json } = { ...left }
25
+ if (!isObject(left) || !isObject(right)) return right;
26
+ const out: JsonObject = { ...left };
26
27
  for (const [k, v] of Object.entries(right)) {
27
- const lv = out[k]
28
- out[k] = lv !== undefined && isObject(lv) && isObject(v) ? deepMerge(lv, v) : v
28
+ const lv = out[k];
29
+ out[k] = lv !== undefined && isObject(lv) && isObject(v) ? deepMerge(lv, v) : v;
29
30
  }
30
- return out
31
+ return out;
31
32
  }
32
33
 
33
34
  /** jq `unique` over an array of strings: codepoint sort + dedup. */
34
35
  export function uniqueStrings(arr: Array<string>): Array<string> {
35
- return [...new Set(arr)].sort(compareCodepoints)
36
+ return [...new Set(arr)].sort(compareCodepoints);
36
37
  }
37
38
 
38
39
  export function compareCodepoints(a: string, b: string): number {
39
- const ia = a[Symbol.iterator]()
40
- const ib = b[Symbol.iterator]()
40
+ const ia = a[Symbol.iterator]();
41
+ const ib = b[Symbol.iterator]();
41
42
  for (;;) {
42
- const ra = ia.next()
43
- const rb = ib.next()
44
- if (ra.done && rb.done) return 0
45
- if (ra.done) return -1
46
- if (rb.done) return 1
47
- const ca = ra.value.codePointAt(0)!
48
- const cb = rb.value.codePointAt(0)!
49
- if (ca !== cb) return ca - cb
43
+ const ra = ia.next();
44
+ const rb = ib.next();
45
+ if (ra.done && rb.done) return 0;
46
+ if (ra.done) return -1;
47
+ if (rb.done) return 1;
48
+ const ca = ra.value.codePointAt(0)!;
49
+ const cb = rb.value.codePointAt(0)!;
50
+ if (ca !== cb) return ca - cb;
50
51
  }
51
52
  }
52
53
 
53
54
  /** jq's default pretty-printer for the documents this kit writes. */
54
55
  export function jqStringify(v: Json): string {
55
- return `${JSON.stringify(v, null, 2)}\n`
56
+ return `${JSON.stringify(v, null, 2)}\n`;
56
57
  }
57
58
 
58
59
  /** jq `empty` validation: parse or null. */
59
60
  export function parseJson(text: string): Json | undefined {
60
61
  try {
61
- return JSON.parse(text) as Json
62
+ return JSON.parse(text) as Json;
62
63
  } catch {
63
- return undefined
64
+ return undefined;
64
65
  }
65
66
  }
66
67
 
67
68
  /** Parse a JSON file with `parseJson` semantics; a missing file is `undefined`. */
68
69
  export function readJsonFile(file: string): Json | undefined {
69
- return existsSync(file) ? parseJson(readFileSync(file, "utf8")) : undefined
70
+ return existsSync(file) ? parseJson(readFileSync(file, "utf8")) : undefined;
70
71
  }