179 lines · 5.4 KB · TypeScript
RawDownload
import { Effect } from "effect";import { FileSystem } from "effect";import type { PlatformError } from "effect/PlatformError";import * as jsonc from "jsonc-parser";import type { SourceConfig, ExecutorFileConfig } from "./schema"; export class ConfigWriteError {  readonly _tag = "ConfigWriteError";  constructor(    readonly path: string,    readonly cause: unknown,  ) {}} const FORMATTING: jsonc.FormattingOptions = {  tabSize: 2,  insertSpaces: true,  eol: "\n",}; const DEFAULT_CONFIG = `{  "sources": []}`; /** Read the raw JSONC text from a config file, or create a default one. */const readOrCreate = (  fs: FileSystem.FileSystem,  path: string,): Effect.Effect<string, PlatformError> =>  Effect.gen(function* () {    const exists = yield* fs.exists(path);    if (exists) return yield* fs.readFileString(path);    yield* fs.writeFileString(path, DEFAULT_CONFIG);    return DEFAULT_CONFIG;  }); /** * Add a source entry to the config file. Creates the file if it doesn't exist. * Preserves existing comments and formatting. */export const addSourceToConfig = (  path: string,  source: SourceConfig,): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>  Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    let text = yield* readOrCreate(fs, path);     // Ensure "sources" array exists    let tree = jsonc.parseTree(text);    let sourcesNode = tree ? jsonc.findNodeAtLocation(tree, ["sources"]) : undefined;     if (!sourcesNode) {      const edits = jsonc.modify(text, ["sources"], [source], {        formattingOptions: FORMATTING,      });      text = jsonc.applyEdits(text, edits);    } else {      // Remove existing entry with same namespace (if any) to avoid duplicates      const ns = "namespace" in source ? source.namespace : undefined;      if (ns && sourcesNode.children) {        for (let i = sourcesNode.children.length - 1; i >= 0; i--) {          const child = sourcesNode.children[i]!;          const nsNode = jsonc.findNodeAtLocation(child, ["namespace"]);          if (nsNode && jsonc.getNodeValue(nsNode) === ns) {            const edits = jsonc.modify(text, ["sources", i], undefined, {              formattingOptions: FORMATTING,            });            text = jsonc.applyEdits(text, edits);          }        }        // Re-parse after removals        tree = jsonc.parseTree(text);        sourcesNode = tree ? jsonc.findNodeAtLocation(tree, ["sources"]) : undefined;      }       const count = sourcesNode?.children?.length ?? 0;      const edits = jsonc.modify(text, ["sources", count], source, {        formattingOptions: FORMATTING,      });      text = jsonc.applyEdits(text, edits);    }     yield* fs.writeFileString(path, text);  }); /** * Remove a source from the config file by namespace. */export const removeSourceFromConfig = (  path: string,  namespace: string,): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>  Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;     const exists = yield* fs.exists(path);    if (!exists) return;     let text = yield* fs.readFileString(path);    const tree = jsonc.parseTree(text);    if (!tree) return;     const sourcesNode = jsonc.findNodeAtLocation(tree, ["sources"]);    if (!sourcesNode?.children) return;     // Walk backwards so indices stay valid after each removal    for (let i = sourcesNode.children.length - 1; i >= 0; i--) {      const child = sourcesNode.children[i]!;      const nsNode = jsonc.findNodeAtLocation(child, ["namespace"]);      if (nsNode && jsonc.getNodeValue(nsNode) === namespace) {        const edits = jsonc.modify(text, ["sources", i], undefined, {          formattingOptions: FORMATTING,        });        text = jsonc.applyEdits(text, edits);      }    }     yield* fs.writeFileString(path, text);  }); /** * Write a full config object to a file. */export const writeConfig = (  path: string,  config: ExecutorFileConfig,): Effect.Effect<void, ConfigWriteError | PlatformError, FileSystem.FileSystem> =>  Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    const text = yield* Effect.try({      try: () => JSON.stringify(config, null, 2) + "\n",      catch: (cause) => new ConfigWriteError(path, cause),    });    yield* fs.writeFileString(path, text);  }); /** * Add secret metadata to the config file. */export const addSecretToConfig = (  path: string,  secretId: string,  metadata: { name: string; provider?: string; purpose?: string },): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>  Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    let text = yield* readOrCreate(fs, path);     const edits = jsonc.modify(text, ["secrets", secretId], metadata, {      formattingOptions: FORMATTING,    });    text = jsonc.applyEdits(text, edits);     yield* fs.writeFileString(path, text);  }); /** * Remove secret metadata from the config file. */export const removeSecretFromConfig = (  path: string,  secretId: string,): Effect.Effect<void, PlatformError, FileSystem.FileSystem> =>  Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;     const exists = yield* fs.exists(path);    if (!exists) return;     let text = yield* fs.readFileString(path);    const edits = jsonc.modify(text, ["secrets", secretId], undefined, {      formattingOptions: FORMATTING,    });    text = jsonc.applyEdits(text, edits);     yield* fs.writeFileString(path, text);  });