From 179f787c92728dad06c207e4a7791873d52a9e8f Mon Sep 17 00:00:00 2001 From: israel Date: Fri, 22 May 2026 13:03:01 +0100 Subject: [PATCH 1/2] chore(platform): seed personal dev config dir from examples on first run The dev orchestrator was defaulting `TALE_CONFIG_DIR` to the in-repo `examples/` directory. Every provider edit, model save, secret rotation, or `fetchConfiguredProviderModels` call writes back through that path, which means routine UI clicks show up in `git status` and risk riding along on unrelated PRs (see #1729 where two `examples/providers/*.json` edits had to be reverted from a provider-drawer feature PR). Switch the default to a personal, gitignored location: `~/.tale/dev-config/`. On first run the directory is seeded from `examples/` (one-time, atomic, `.DS_Store` filtered). On subsequent runs the seed is skipped, so local edits persist across sessions and `git pull` updates to `examples/` no longer clobber per-developer state. `TALE_CONFIG_DIR` in `.env.local` still wins, so anyone pointing at a customer config or a shared checkout keeps that behaviour. Also logs the active `TALE_CONFIG_DIR` on startup so the override is discoverable without grepping the script. --- services/platform/scripts/dev.ts | 46 ++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/services/platform/scripts/dev.ts b/services/platform/scripts/dev.ts index b11d510ec..de30402ee 100644 --- a/services/platform/scripts/dev.ts +++ b/services/platform/scripts/dev.ts @@ -23,8 +23,9 @@ */ import { type ChildProcess, spawn } from 'node:child_process'; -import { existsSync, readFileSync } from 'node:fs'; +import { cpSync, existsSync, mkdirSync, readFileSync } from 'node:fs'; import { createConnection } from 'node:net'; +import { homedir } from 'node:os'; import { join } from 'node:path'; import process from 'node:process'; @@ -55,6 +56,41 @@ function parseDotEnv(filePath: string): Record { return result; } +// Personal, gitignored config dir for the local dev instance. The app +// mutates this on every provider/agent/workflow edit, so we MUST keep it +// outside the tracked tree — otherwise routine UI clicks bleed into +// `git status` and ride along on unrelated PRs (see #1729 fallout). +// +// Seeded once from the in-repo `examples/` starter on first run; never +// overwritten after that, so the user's edits persist across sessions +// and `git pull` updates to `examples/` don't clobber local state. +function seedDevConfigDir(): string { + const target = join(homedir(), '.tale', 'dev-config'); + if (existsSync(target)) return target; + + const source = join(repoRoot, 'examples'); + if (!existsSync(source)) { + // No examples to seed from; just create an empty dir so the convex + // file_utils don't blow up on the first read. + mkdirSync(target, { recursive: true }); + console.log( + `[dev] 📁 Created empty dev config dir (no examples/ to seed from): ${target}`, + ); + return target; + } + + mkdirSync(join(homedir(), '.tale'), { recursive: true }); + cpSync(source, target, { + recursive: true, + // Skip macOS metadata — would pollute the seeded tree. + filter: (src) => !src.endsWith('/.DS_Store'), + }); + console.log( + `[dev] 📁 Seeded dev config from ${source} → ${target} (one-time; your edits stay local)`, + ); + return target; +} + function envNormalizeCommon() { process.env.NODE_ENV = 'development'; if (!process.env.PORT) process.env.PORT = '3000'; @@ -71,9 +107,15 @@ function envNormalizeCommon() { // Root config directory only — Convex derives sub-dirs (agents/workflows/ // integrations/providers) from TALE_CONFIG_DIR via `convex/*/file_utils.ts`. + // + // Default to a personal gitignored dir seeded from `examples/` so the + // tracked tree is never the runtime write target. Override by setting + // TALE_CONFIG_DIR in `.env.local` if you want a different location + // (e.g. a checked-out customer config). if (!process.env.TALE_CONFIG_DIR) { - process.env.TALE_CONFIG_DIR = join(repoRoot, 'examples'); + process.env.TALE_CONFIG_DIR = seedDevConfigDir(); } + console.log(`[dev] 📂 TALE_CONFIG_DIR=${process.env.TALE_CONFIG_DIR}`); } function ensureInstanceSecret() { From c609fb71d196cec3ae20356f6c4037943012e817 Mon Sep 17 00:00:00 2001 From: israel Date: Fri, 22 May 2026 13:14:59 +0100 Subject: [PATCH 2/2] fix(platform): make dev config seed atomic and cross-platform CodeRabbit feedback on #1731: - Copy into a sibling temp dir, then renameSync into place. Prevents a crashed/interrupted seed from leaving a half-populated dev-config/ that the existsSync short-circuit would then treat as already seeded on the next run. Cleanup on error so failed runs don't leave orphan temp dirs. - Use path.basename for the .DS_Store filter instead of endsWith on a POSIX slash, so the filter still works on Windows (which uses backslashes). --- services/platform/scripts/dev.ts | 46 +++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/services/platform/scripts/dev.ts b/services/platform/scripts/dev.ts index de30402ee..d70f76c52 100644 --- a/services/platform/scripts/dev.ts +++ b/services/platform/scripts/dev.ts @@ -23,10 +23,17 @@ */ import { type ChildProcess, spawn } from 'node:child_process'; -import { cpSync, existsSync, mkdirSync, readFileSync } from 'node:fs'; +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, +} from 'node:fs'; import { createConnection } from 'node:net'; import { homedir } from 'node:os'; -import { join } from 'node:path'; +import { basename, join } from 'node:path'; import process from 'node:process'; import kill from 'tree-kill'; @@ -65,7 +72,8 @@ function parseDotEnv(filePath: string): Record { // overwritten after that, so the user's edits persist across sessions // and `git pull` updates to `examples/` don't clobber local state. function seedDevConfigDir(): string { - const target = join(homedir(), '.tale', 'dev-config'); + const taleDir = join(homedir(), '.tale'); + const target = join(taleDir, 'dev-config'); if (existsSync(target)) return target; const source = join(repoRoot, 'examples'); @@ -79,12 +87,32 @@ function seedDevConfigDir(): string { return target; } - mkdirSync(join(homedir(), '.tale'), { recursive: true }); - cpSync(source, target, { - recursive: true, - // Skip macOS metadata — would pollute the seeded tree. - filter: (src) => !src.endsWith('/.DS_Store'), - }); + // Copy into a sibling temp dir first, then atomically rename to the + // final target. Prevents a crashed/interrupted seed from leaving a + // half-populated `dev-config/` that the `existsSync` short-circuit + // would then treat as "already seeded" on the next run. + mkdirSync(taleDir, { recursive: true }); + const tempTarget = join( + taleDir, + `dev-config.tmp.${process.pid}.${Date.now()}`, + ); + try { + cpSync(source, tempTarget, { + recursive: true, + // Skip macOS metadata; use basename so it works on Windows paths + // (which use backslashes) as well as POSIX. + filter: (src) => basename(src) !== '.DS_Store', + }); + renameSync(tempTarget, target); + } catch (err) { + // Best-effort cleanup; if rmSync also throws, surface the original. + try { + rmSync(tempTarget, { recursive: true, force: true }); + } catch { + // ignore + } + throw err; + } console.log( `[dev] 📁 Seeded dev config from ${source} → ${target} (one-time; your edits stay local)`, );