Compare commits

...
Author SHA1 Message Date
nexxeln c85334ce09 fix(cli): preserve symlinked config files 2026-09-19 13:59:41 +00:00
3 changed files with 32 additions and 23 deletions
+5 -14
View File
@@ -7,6 +7,7 @@ import { produce, type Draft } from "immer"
import { applyEdits, modify, parse, type ParseError } from "jsonc-parser"
import path from "path"
import { ConfigMigration } from "./migrate"
import { ConfigPersistence } from "./persist"
import { Info, SchemaURL } from "./schema"
export * from "./schema"
@@ -39,13 +40,6 @@ export const layer = Layer.effect(
return parseRecord(text)
})
const write = Effect.fnUntraced(function* (text: string) {
const temp = file + ".tmp"
yield* fs.makeDirectory(path.dirname(file), { recursive: true })
yield* fs.writeFileString(temp, text, { mode: 0o600 })
yield* fs.rename(temp, file)
})
const migrate = ConfigMigration.run({ file, config: global.config, state: global.state }).pipe(
Effect.provideService(FileSystem.FileSystem, fs),
)
@@ -103,7 +97,9 @@ export const layer = Layer.effect(
const errors: ParseError[] = []
const config = Option.getOrUndefined(decode(parse(updated, errors, { allowTrailingComma: true })))
if (errors.length || config === undefined) return yield* Effect.fail(new Error("Invalid CLI config update"))
yield* write(updated.endsWith("\n") ? updated : updated + "\n")
yield* ConfigPersistence.write(file, updated.endsWith("\n") ? updated : updated + "\n").pipe(
Effect.provideService(FileSystem.FileSystem, fs),
)
return merge(config, content)
}),
).pipe(Effect.mapError((cause) => new Error("Failed to update CLI config", { cause }))),
@@ -117,12 +113,7 @@ type Edit = { readonly path: (string | number)[]; readonly value: any }
function merge(...values: readonly (Info | undefined)[]) {
return Option.getOrElse(
decode(
values.reduce<Record<string, unknown>>(
(result, value) => mergeRecords(result, value ?? {}),
{},
),
),
decode(values.reduce<Record<string, unknown>>((result, value) => mergeRecords(result, value ?? {}), {})),
() => empty,
)
}
+9 -9
View File
@@ -4,9 +4,9 @@ import { TuiConfigV1 } from "@opencode/tui/config/v1"
import { TuiKeybind } from "@opencode/tui/config/v1/keybind"
import { Definitions } from "@opencode/tui/config/keybind"
import { Effect, FileSystem, Option, Schema } from "effect"
import { randomUUID } from "crypto"
import { applyEdits, createScanner, modify, parse, parseTree, type Node, type ParseError } from "jsonc-parser"
import path from "path"
import { ConfigPersistence } from "./persist"
import { Info, SchemaURL } from "./schema"
const decodeV1 = Schema.decodeUnknownOption(TuiConfigV1.Info)
@@ -21,20 +21,20 @@ export const run = Effect.fn("cli.config.migrate")(function* (input: {
}) {
const fs = yield* FileSystem.FileSystem
const persist = Effect.fnUntraced(function* (text: string, info: Info) {
const temp = `${input.file}.${process.pid}.${randomUUID()}.tmp`
const cause = yield* Effect.gen(function* () {
yield* fs.makeDirectory(path.dirname(input.file), { recursive: true })
yield* fs.writeFileString(temp, text, { mode: 0o600 })
yield* fs.rename(temp, input.file)
}).pipe(
const cause = yield* ConfigPersistence.write(input.file, text).pipe(
Effect.provideService(FileSystem.FileSystem, fs),
Effect.as(undefined),
Effect.catchCause((cause) => Effect.succeed(cause)),
Effect.ensuring(fs.remove(temp).pipe(Effect.ignore)),
)
return cause === undefined ? { info } : { info, cause }
})
if (yield* fs.exists(input.file).pipe(Effect.orElseSucceed(() => false))) {
if (
yield* fs.realPath(input.file).pipe(
Effect.as(true),
Effect.orElseSucceed(() => false),
)
) {
const text = yield* fs.readFileString(input.file)
const errors: ParseError[] = []
const value: any = parse(text, errors, { allowTrailingComma: true })
+18
View File
@@ -0,0 +1,18 @@
export * as ConfigPersistence from "./persist"
import { randomUUID } from "crypto"
import { Effect, FileSystem, Option } from "effect"
import path from "path"
export const write = Effect.fn("cli.config.persist")(function* (file: string, text: string) {
const fs = yield* FileSystem.FileSystem
const link = yield* fs.readLink(file).pipe(Effect.option)
const target = Option.isSome(link) ? yield* fs.realPath(file) : file
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`
yield* Effect.gen(function* () {
yield* fs.makeDirectory(path.dirname(target), { recursive: true })
yield* fs.writeFileString(temp, text, { mode: 0o600 })
yield* fs.rename(temp, target)
}).pipe(Effect.ensuring(fs.remove(temp).pipe(Effect.ignore)))
})