{"version":3,"file":"file-remote-target-registry.d.ts","sourceRoot":"","sources":["../../../src/core/remote-execution/file-remote-target-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EACX,sBAAsB,EACtB,0BAA0B,EAC1B,iBAAiB,EACjB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAA8B,KAAK,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AAQlG,MAAM,WAAW,+BAA+B;IAC/C,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,+BAA+B,IAAI,MAAM,CAIxD;AAED,qBAAa,wBAAyB,YAAW,iBAAiB;IACjE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAS;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IAErC,YAAY,OAAO,EAAE,+BAA+B,EAKnD;IAED,OAAO,CAAC,OAAO;YAID,WAAW;YAaX,QAAQ;IA0BhB,QAAQ,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,0BAA0B,CAAC,CAkBjF;IAEK,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAkB5D;IAEK,WAAW,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAcrC;IAEK,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAO/C;CACD;AAED,wBAAgB,8BAA8B,CAC7C,IAAI,GAAE,MAA0C,GAC9C,wBAAwB,CAE1B","sourcesContent":["/**\n * File Remote Target Registry (2.14.0).\n *\n * Durable JSON-file implementation of the RemoteTargetStore port. Mirrors the\n * other file stores: atomic write, schema validation on load, deterministic\n * corrupt surfacing. Targets are a catalog, not mission state.\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport * as fsp from \"node:fs/promises\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport lockfile from \"proper-lockfile\";\nimport type {\n\tRemoteTargetLoadResult,\n\tRemoteTargetRegisterResult,\n\tRemoteTargetStore,\n} from \"./remote-target-registry.js\";\nimport { parseRemoteExecutionTarget, type RemoteExecutionTarget } from \"./remote-target-types.js\";\n\nconst RECORD_SUFFIX = \".target.json\";\nconst ATOMIC_SUFFIX = \".tmp\";\n\nconst DEFAULT_LOCK_STALE_MS = 30_000;\nconst DEFAULT_LOCK_RETRIES = 8;\n\nexport interface FileRemoteTargetRegistryOptions {\n\troot: string;\n\tstoreId?: string;\n\tlockStaleMs?: number;\n\tlockRetries?: number;\n}\n\nexport function defaultRemoteTargetRegistryRoot(): string {\n\tconst env = process.env.JENSEN_REMOTE_TARGET_DIR;\n\tif (env?.trim()) return env.trim();\n\treturn path.join(os.homedir(), \".jensen\", \"agent\", \"remote-targets\");\n}\n\nexport class FileRemoteTargetRegistry implements RemoteTargetStore {\n\treadonly storeId: string;\n\tprivate readonly root: string;\n\tprivate readonly lockStaleMs: number;\n\tprivate readonly lockRetries: number;\n\n\tconstructor(options: FileRemoteTargetRegistryOptions) {\n\t\tthis.root = path.resolve(options.root);\n\t\tthis.storeId = options.storeId ?? \"file\";\n\t\tthis.lockStaleMs = options.lockStaleMs ?? DEFAULT_LOCK_STALE_MS;\n\t\tthis.lockRetries = options.lockRetries ?? DEFAULT_LOCK_RETRIES;\n\t}\n\n\tprivate resolve(targetId: string): string {\n\t\treturn path.join(this.root, `${targetId}${RECORD_SUFFIX}`);\n\t}\n\n\tprivate async writeAtomic(target: string, content: string): Promise<void> {\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst tmp = `${target}.${randomUUID()}${ATOMIC_SUFFIX}`;\n\t\tawait fsp.writeFile(tmp, content, \"utf8\");\n\t\tconst fh = await fsp.open(tmp, \"r\");\n\t\ttry {\n\t\t\tawait fh.sync();\n\t\t} finally {\n\t\t\tawait fh.close();\n\t\t}\n\t\tawait fsp.rename(tmp, target);\n\t}\n\n\tprivate async withLock<T>(targetId: string, fn: () => Promise<T>): Promise<T> {\n\t\tawait fsp.mkdir(this.root, { recursive: true });\n\t\tconst target = this.resolve(targetId);\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\ttry {\n\t\t\trelease = await lockfile.lock(target, {\n\t\t\t\trealpath: false,\n\t\t\t\tstale: this.lockStaleMs,\n\t\t\t\tretries: { retries: this.lockRetries, factor: 2, minTimeout: 20, maxTimeout: 250, randomize: true },\n\t\t\t});\n\t\t} catch {\n\t\t\tthrow new Error(`Timed out acquiring remote target registry lock for ${targetId}`);\n\t\t}\n\t\ttry {\n\t\t\treturn await fn();\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// best-effort release\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tasync register(target: RemoteExecutionTarget): Promise<RemoteTargetRegisterResult> {\n\t\tconst parsed = parseRemoteExecutionTarget(target);\n\t\tif (!parsed.ok) {\n\t\t\tthrow new Error(`Invalid remote target: ${parsed.diagnostic}`);\n\t\t}\n\t\treturn this.withLock(parsed.target.targetId, async () => {\n\t\t\tconst existing = await this.load(parsed.target.targetId);\n\t\t\tif (existing.status === \"ok\") {\n\t\t\t\tconst same = JSON.stringify(existing.target) === JSON.stringify(parsed.target);\n\t\t\t\tif (same) return { status: \"idempotent\" as const, target: existing.target };\n\t\t\t\treturn { status: \"conflict\" as const, error: \"targetId already registered with a different definition\" };\n\t\t\t}\n\t\t\tif (existing.status === \"corrupt\") {\n\t\t\t\treturn { status: \"conflict\" as const, error: `existing target record is corrupt: ${existing.diagnostic}` };\n\t\t\t}\n\t\t\tawait this.writeAtomic(this.resolve(parsed.target.targetId), JSON.stringify(parsed.target, null, 2));\n\t\t\treturn { status: \"created\" as const };\n\t\t});\n\t}\n\n\tasync load(targetId: string): Promise<RemoteTargetLoadResult> {\n\t\tlet raw: string | undefined;\n\t\ttry {\n\t\t\traw = await fsp.readFile(this.resolve(targetId), \"utf8\");\n\t\t} catch {\n\t\t\treturn { status: \"missing\" };\n\t\t}\n\t\tlet value: unknown;\n\t\ttry {\n\t\t\tvalue = JSON.parse(raw);\n\t\t} catch {\n\t\t\treturn { status: \"corrupt\", targetId, diagnostic: \"record is not valid JSON\" };\n\t\t}\n\t\tconst parsed = parseRemoteExecutionTarget(value);\n\t\tif (!parsed.ok) {\n\t\t\treturn { status: \"corrupt\", targetId, diagnostic: parsed.diagnostic };\n\t\t}\n\t\treturn { status: \"ok\", target: parsed.target };\n\t}\n\n\tasync listTargets(): Promise<string[]> {\n\t\tlet entries: string[];\n\t\ttry {\n\t\t\tentries = await fsp.readdir(this.root);\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t\tconst ids: string[] = [];\n\t\tfor (const entry of entries) {\n\t\t\tif (entry.endsWith(ATOMIC_SUFFIX)) continue;\n\t\t\tif (!entry.endsWith(RECORD_SUFFIX)) continue;\n\t\t\tids.push(entry.slice(0, -RECORD_SUFFIX.length));\n\t\t}\n\t\treturn ids.sort();\n\t}\n\n\tasync remove(targetId: string): Promise<boolean> {\n\t\ttry {\n\t\t\tawait fsp.unlink(this.resolve(targetId));\n\t\t\treturn true;\n\t\t} catch {\n\t\t\treturn false;\n\t\t}\n\t}\n}\n\nexport function createFileRemoteTargetRegistry(\n\troot: string = defaultRemoteTargetRegistryRoot(),\n): FileRemoteTargetRegistry {\n\treturn new FileRemoteTargetRegistry({ root });\n}\n"]}