|
1 | | -import { resolve } from "node:path"; |
| 1 | +import { copyFileSync, existsSync, mkdirSync, renameSync } from "node:fs"; |
| 2 | +import { homedir } from "node:os"; |
| 3 | +import { dirname, join, resolve } from "node:path"; |
2 | 4 | import { LocalFileStateBackend, type StateScope } from "@openagentpack/sdk"; |
3 | 5 | import { RUNTIME_PROJECT_NAME } from "@/lib/build-runtime-config"; |
4 | 6 |
|
5 | | -// Historical on-disk state anchor. Previously derived via deriveStatePath() from the |
6 | | -// demo file examples/bailian/bailian-cli/agents.yaml → agents.state.json. We pin the default |
7 | | -// to that exact location so previously provisioned remote_ids (agents/vault/environment) |
8 | | -// keep resolving and are not re-created. Override with AGENTS_STATE_PATH. |
9 | | -const DEFAULT_STATE_PATH = "examples/bailian/bailian-cli/agents.state.json"; |
| 7 | +/** |
| 8 | + * Default playground state lives in the user home directory, alongside the |
| 9 | + * provider config (~/.agents/config.json). This avoids colliding with CLI |
| 10 | + * example configs that happen to live in the repo's examples/ directory. |
| 11 | + * |
| 12 | + * Override with AGENTS_STATE_PATH for custom deployment layouts. |
| 13 | + */ |
| 14 | +const DEFAULT_STATE_PATH = join(homedir(), ".agents", "playground.state.json"); |
| 15 | + |
| 16 | +/** |
| 17 | + * Legacy state path. Before this migration the server stored its state inside |
| 18 | + * the repo's example directory. We migrate it once so previously-provisioned |
| 19 | + * remote_ids are preserved. |
| 20 | + */ |
| 21 | +const LEGACY_STATE_PATH = "examples/bailian/bailian-cli/agents.state.json"; |
| 22 | + |
| 23 | +let migrationChecked = false; |
| 24 | + |
| 25 | +/** |
| 26 | + * One-time migration: if the legacy state file exists and the new default does |
| 27 | + * not, copy it over and log a warning. Idempotent — subsequent calls after a |
| 28 | + * successful migration (or when no migration is needed) are no-ops. A failed |
| 29 | + * copy allows retry on the next call so transient I/O errors don't permanently |
| 30 | + * disable migration for the process lifetime. |
| 31 | + */ |
| 32 | +function ensureMigrated(newPath: string, cwd: string): void { |
| 33 | + if (migrationChecked) return; |
| 34 | + |
| 35 | + if (existsSync(newPath)) { |
| 36 | + migrationChecked = true; |
| 37 | + return; |
| 38 | + } |
| 39 | + const legacyPath = resolve(cwd, LEGACY_STATE_PATH); |
| 40 | + if (!existsSync(legacyPath)) { |
| 41 | + migrationChecked = true; |
| 42 | + return; |
| 43 | + } |
| 44 | + |
| 45 | + try { |
| 46 | + mkdirSync(dirname(newPath), { recursive: true }); |
| 47 | + copyFileSync(legacyPath, newPath); |
| 48 | + // Rename the legacy file so a version rollback won't silently read stale state. |
| 49 | + try { |
| 50 | + renameSync(legacyPath, `${legacyPath}.migrated`); |
| 51 | + } catch { |
| 52 | + // Non-fatal: the copy succeeded, state is in the new location. |
| 53 | + } |
| 54 | + migrationChecked = true; |
| 55 | + console.warn( |
| 56 | + `[state] Migrated playground state from legacy path:\n` + |
| 57 | + ` ${legacyPath}\n` + |
| 58 | + ` → ${newPath}\n` + |
| 59 | + ` The legacy file has been renamed to ${legacyPath}.migrated.`, |
| 60 | + ); |
| 61 | + } catch (error) { |
| 62 | + // Don't set migrationChecked — allow retry on next call. |
| 63 | + console.warn(`[state] Failed to migrate legacy state file: ${error instanceof Error ? error.message : error}`); |
| 64 | + } |
| 65 | +} |
10 | 66 |
|
11 | 67 | export function resolveStatePath(env: NodeJS.ProcessEnv = process.env, cwd: string = process.cwd()): string { |
12 | 68 | const configured = env.AGENTS_STATE_PATH?.trim(); |
13 | | - return configured ? resolve(cwd, configured) : resolve(cwd, DEFAULT_STATE_PATH); |
| 69 | + if (configured) return resolve(cwd, configured); |
| 70 | + |
| 71 | + ensureMigrated(DEFAULT_STATE_PATH, cwd); |
| 72 | + return DEFAULT_STATE_PATH; |
14 | 73 | } |
15 | 74 |
|
16 | 75 | export function deriveWebUiStateScope(): StateScope { |
|
0 commit comments