import { homedir } from "node:os"; import { FileSystem, Option, Path, Schema } from "effect"; import type { PlatformError } from "effect/PlatformError"; import * as Effect from "effect/Effect"; import { normalizeExecutorServerConnection, type ExecutorServerConnection, type ExecutorServerConnectionInput, } from "@executor-js/sdk/shared"; export interface CliServerConnectionProfile { readonly name: string; readonly connection: ExecutorServerConnection; } export interface CliServerConnectionStore { readonly version: 1; readonly defaultProfile: string | null; readonly profiles: readonly CliServerConnectionProfile[]; } export const emptyCliServerConnectionStore: CliServerConnectionStore = { version: 1, defaultProfile: null, profiles: [], }; export const validateCliServerConnectionProfileName = (name: string): string => { const trimmed = name.trim(); if (!/^[A-Za-z0-9_.-]+$/.test(trimmed)) { throw new Error( "Server profile names may contain only letters, numbers, dots, underscores, and dashes.", ); } return trimmed; }; const resolveDataDir = (path: Path.Path): string => process.env.EXECUTOR_DATA_DIR ?? path.join(homedir(), ".executor"); const serverConnectionStorePath = (path: Path.Path): string => path.join(resolveDataDir(path), "server-connections.json"); const PersistedAuth = Schema.Union([ Schema.Struct({ kind: Schema.Literal("basic"), username: Schema.optional(Schema.String), password: Schema.String, }), Schema.Struct({ kind: Schema.Literal("bearer"), token: Schema.String, }), ]); const PersistedConnection = Schema.Struct({ kind: Schema.optional(Schema.Literals(["http", "desktop-sidecar"])), key: Schema.optional(Schema.String), origin: Schema.optional(Schema.String), apiBaseUrl: Schema.optional(Schema.String), displayName: Schema.optional(Schema.String), auth: Schema.optional(PersistedAuth), }); const PersistedProfile = Schema.Struct({ name: Schema.String, connection: PersistedConnection, }); const PersistedStore = Schema.Struct({ version: Schema.Literal(1), defaultProfile: Schema.optional(Schema.NullOr(Schema.String)), profiles: Schema.Array(PersistedProfile), }); const decodeStoreJson = Schema.decodeUnknownOption(Schema.fromJsonString(PersistedStore)); const decodeConnection = ( input: ExecutorServerConnectionInput, ): ExecutorServerConnection | null => { if (!input.origin && !input.apiBaseUrl) return null; return normalizeExecutorServerConnection(input); }; export const parseCliServerConnectionStore = (raw: string): CliServerConnectionStore => { const decoded = decodeStoreJson(raw); if (Option.isNone(decoded)) return emptyCliServerConnectionStore; const record = decoded.value; const profiles = record.profiles.flatMap((value): readonly CliServerConnectionProfile[] => { const connection = decodeConnection(value.connection); if (!connection) return []; try { return [{ name: validateCliServerConnectionProfileName(value.name), connection }]; } catch { return []; } }); const defaultProfile = record.defaultProfile && profiles.some((profile) => profile.name === record.defaultProfile) ? record.defaultProfile : null; return { version: 1, defaultProfile, profiles, }; }; const serializeCliServerConnectionStore = (store: CliServerConnectionStore): string => `${JSON.stringify(store, null, 2)}\n`; export const readCliServerConnectionStore = (): Effect.Effect< CliServerConnectionStore, never, FileSystem.FileSystem | Path.Path > => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const raw = yield* fs .readFileString(serverConnectionStorePath(path)) .pipe(Effect.catchCause(() => Effect.succeed(null))); if (raw === null) return emptyCliServerConnectionStore; return parseCliServerConnectionStore(raw); }); export const writeCliServerConnectionStore = ( store: CliServerConnectionStore, ): Effect.Effect => Effect.gen(function* () { const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const dataDir = resolveDataDir(path); yield* fs.makeDirectory(dataDir, { recursive: true }); yield* fs.writeFileString( serverConnectionStorePath(path), serializeCliServerConnectionStore(store), ); }); export const upsertCliServerConnectionProfile = (input: { readonly name: string; readonly connection: ExecutorServerConnectionInput; readonly makeDefault: boolean; }): Effect.Effect => Effect.gen(function* () { const name = validateCliServerConnectionProfileName(input.name); const store = yield* readCliServerConnectionStore(); const connection = normalizeExecutorServerConnection({ ...input.connection, key: input.connection.key ?? `profile:${name}`, displayName: input.connection.displayName ?? name, }); const nextProfiles = [ ...store.profiles.filter((profile) => profile.name !== name), { name, connection }, ].sort((a, b) => a.name.localeCompare(b.name)); const nextStore: CliServerConnectionStore = { version: 1, defaultProfile: input.makeDefault || store.defaultProfile === null ? name : store.defaultProfile, profiles: nextProfiles, }; yield* writeCliServerConnectionStore(nextStore); return nextStore; }); export const setDefaultCliServerConnectionProfile = ( name: string, ): Effect.Effect< CliServerConnectionStore, Error | PlatformError, FileSystem.FileSystem | Path.Path > => Effect.gen(function* () { const profileName = validateCliServerConnectionProfileName(name); const store = yield* readCliServerConnectionStore(); if (!store.profiles.some((profile) => profile.name === profileName)) { return yield* Effect.fail(new Error(`No server profile named "${profileName}".`)); } const nextStore: CliServerConnectionStore = { ...store, defaultProfile: profileName }; yield* writeCliServerConnectionStore(nextStore); return nextStore; }); export const removeCliServerConnectionProfile = ( name: string, ): Effect.Effect => Effect.gen(function* () { const profileName = validateCliServerConnectionProfileName(name); const store = yield* readCliServerConnectionStore(); const nextProfiles = store.profiles.filter((profile) => profile.name !== profileName); const nextStore: CliServerConnectionStore = { version: 1, defaultProfile: store.defaultProfile === profileName ? null : store.defaultProfile, profiles: nextProfiles, }; yield* writeCliServerConnectionStore(nextStore); return nextStore; }); export const findCliServerConnectionProfile = ( store: CliServerConnectionStore, name: string, ): CliServerConnectionProfile | null => { const profileName = validateCliServerConnectionProfileName(name); return store.profiles.find((profile) => profile.name === profileName) ?? null; }; export const defaultCliServerConnectionProfile = ( store: CliServerConnectionStore, ): CliServerConnectionProfile | null => store.defaultProfile ? findCliServerConnectionProfile(store, store.defaultProfile) : null;