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); });
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);
});