Files
zesdex/apps/packages/infrastructure/src/utils.ts
T
asepharyanaandClaude Opus 5 a2c7e5d44c feat(rewrite): tambah infrastructure — LLM client + full file persistence
Sub-fase 3a & 3e infrastructure TypeScript (@zesdex/infrastructure):
- llm/provider.ts: LlmClient (OpenAI/Anthropic-compatible fetch + SSE,
  retry 10x exponential backoff + jitter, abort on 401/402/403,
  stream quota-aware tool-call delta accumulation)
- resolveApiKey: settings.api_keys → app_config.env_var → inline default
- utils: writeJsonAtomic (tmp+rename+fsync), slugify, writeOsc52,
  truncateChars, buildWorkspaceTree, runCommand
- persistence/cms: JsonSettingsRepository, JsonAppConfigRepository
  (Claude credential auto-detection dari ~/.claude/settings.json + env),
  JsonConversationRepository, JsonlEditLogRepository (append NDJSON),
  MarkdownMemoryRepository (YAML-ish frontmatter), FileRewindBlobRepository
- persistence/iam: FileSystemSessionRepository, FileSystemSessionLockRepository,
  FileSystemOAuthRepository
- Workspace @zesdex/infrastructure tsc clean, semua test domain+application
  tetap hijau (38 pass)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 22:50:38 +07:00

120 lines
3.9 KiB
TypeScript

/**
* Utility helpers. Mirrors `apps/infrastructure/src/utils.rs`.
*/
import { randomUUID } from "node:crypto";
import * as fs from "node:fs";
import * as path from "node:path";
/** Atomically write JSON `data` to `path`, with optional mode and parent fsync. */
export function writeJsonAtomic(filePath: string, data: unknown, mode?: number): void {
const tmp = `${filePath}.tmp.${randomUUID()}`;
const parent = path.dirname(filePath);
try {
fs.mkdirSync(parent, { recursive: true });
const bytes = JSON.stringify(data, null, 2);
const fd = fs.openSync(tmp, "w");
try {
fs.writeFileSync(fd, bytes);
fs.fsyncSync(fd);
} finally {
fs.closeSync(fd);
}
if (mode !== undefined) fs.chmodSync(tmp, mode);
fs.renameSync(tmp, filePath);
try {
const dirFd = fs.openSync(parent, "r");
try {
fs.fsyncSync(dirFd);
} finally {
fs.closeSync(dirFd);
}
} catch {
/* best-effort */
}
} catch (e) {
try {
fs.unlinkSync(tmp);
} catch {
/* orphan cleanup */
}
throw e;
}
}
/** Convert a string into a filesystem-safe slug, or null if empty/too long. */
export function slugify(s: string): string | null {
let slug = s.toLowerCase().replace(/[^a-z0-9]/g, "-");
slug = slug.split("-").filter((seg) => seg !== "").join("-");
if (slug === "" || slug.length > 80) return null;
return slug;
}
/** Write `text` to the terminal clipboard via OSC-52 escape sequence. */
export function writeOsc52(text: string, out: { write: (s: string) => unknown } = process.stdout): void {
const encoded = Buffer.from(text, "utf8").toString("base64");
out.write(`\x1b]52;c;${encoded}\x1b\\`);
}
/** Truncate a string to at most `maxChars` characters (UTF-8 safe). */
export function truncateChars(s: string, maxChars: number): string {
if ([...s].length <= maxChars) return s;
return [...s].slice(0, maxChars).join("");
}
/** Build a string directory tree of `root`, respecting .gitignore semantics loosely. */
export function buildWorkspaceTree(root: string, maxFiles: number): string {
const lines: string[] = [];
let count = 0;
const walk = (dir: string, depth: number, ignore: Set<string>) => {
if (count >= maxFiles) {
lines.push("... (truncated)");
return;
}
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => (a.isDirectory() === b.isDirectory() ? a.name.localeCompare(b.name) : a.isDirectory() ? -1 : 1));
} catch {
return;
}
for (const entry of entries) {
if (count >= maxFiles) {
lines.push("... (truncated)");
return;
}
if (entry.name === ".git" || ignore.has(entry.name)) continue;
const indent = " ".repeat(Math.max(depth - 1, 0));
if (entry.isDirectory()) {
lines.push(`${indent}${entry.name}/`);
count++;
const subIgnore = new Set(ignore);
// Honor .gitignore briefly
try {
const gi = fs.readFileSync(path.join(dir, entry.name, ".gitignore"), "utf8");
for (const l of gi.split("\n")) {
const t = l.trim().replace(/\/$/, "");
if (t && !t.startsWith("#")) subIgnore.add(t);
}
} catch {
/* keep default ignore set */
}
walk(path.join(dir, entry.name), depth + 1, subIgnore);
} else {
lines.push(`${indent}${entry.name}`);
count++;
}
}
};
lines.push(".");
walk(root, 1, new Set(["node_modules", "target"]));
return lines.join("\n").trimEnd();
}
/** Run a shell command and return trimmed stdout (best-effort). */
export function runCommand(cmd: string, cwd?: string, args?: string[]): string {
try {
const res = Bun.spawnSync({ cmd: args ? [cmd, ...args] : [cmd], cwd, stdout: "pipe", stderr: "pipe" });
return res.stdout.toString().trim();
} catch {
return "";
}
}