@aigne/afs-cli 1.11.0-beta.7 → 1.11.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_virtual/rolldown_runtime.mjs +7 -0
- package/dist/config/afs-loader.cjs +466 -22
- package/dist/config/afs-loader.d.cts +6 -1
- package/dist/config/afs-loader.d.cts.map +1 -1
- package/dist/config/afs-loader.d.mts +6 -1
- package/dist/config/afs-loader.d.mts.map +1 -1
- package/dist/config/afs-loader.mjs +465 -22
- package/dist/config/afs-loader.mjs.map +1 -1
- package/dist/config/loader.cjs +28 -11
- package/dist/config/loader.mjs +28 -11
- package/dist/config/loader.mjs.map +1 -1
- package/dist/config/mount-commands.cjs +96 -46
- package/dist/config/mount-commands.d.cts +0 -6
- package/dist/config/mount-commands.d.cts.map +1 -1
- package/dist/config/mount-commands.d.mts +0 -6
- package/dist/config/mount-commands.d.mts.map +1 -1
- package/dist/config/mount-commands.mjs +93 -45
- package/dist/config/mount-commands.mjs.map +1 -1
- package/dist/config/schema.cjs +2 -2
- package/dist/config/schema.mjs +2 -2
- package/dist/config/schema.mjs.map +1 -1
- package/dist/core/commands/exec.cjs +6 -3
- package/dist/core/commands/exec.d.cts.map +1 -1
- package/dist/core/commands/exec.d.mts.map +1 -1
- package/dist/core/commands/exec.mjs +6 -3
- package/dist/core/commands/exec.mjs.map +1 -1
- package/dist/core/commands/explain.cjs +1 -1
- package/dist/core/commands/explain.mjs +1 -1
- package/dist/core/commands/mount.cjs +106 -23
- package/dist/core/commands/mount.d.cts +2 -0
- package/dist/core/commands/mount.d.cts.map +1 -1
- package/dist/core/commands/mount.d.mts +2 -0
- package/dist/core/commands/mount.d.mts.map +1 -1
- package/dist/core/commands/mount.mjs +106 -23
- package/dist/core/commands/mount.mjs.map +1 -1
- package/dist/core/commands/serve.cjs +38 -13
- package/dist/core/commands/serve.mjs +38 -13
- package/dist/core/commands/serve.mjs.map +1 -1
- package/dist/core/commands/types.cjs +6 -1
- package/dist/core/commands/types.d.cts.map +1 -1
- package/dist/core/commands/types.d.mts.map +1 -1
- package/dist/core/commands/types.mjs +6 -1
- package/dist/core/commands/types.mjs.map +1 -1
- package/dist/credential/auth-server.cjs +247 -0
- package/dist/credential/auth-server.mjs +247 -0
- package/dist/credential/auth-server.mjs.map +1 -0
- package/dist/credential/cli-auth-context.cjs +86 -0
- package/dist/credential/cli-auth-context.d.mts +1 -0
- package/dist/credential/cli-auth-context.mjs +86 -0
- package/dist/credential/cli-auth-context.mjs.map +1 -0
- package/dist/credential/index.cjs +5 -0
- package/dist/credential/index.d.mts +4 -0
- package/dist/credential/index.mjs +7 -0
- package/dist/credential/mcp-auth-context.cjs +186 -0
- package/dist/credential/mcp-auth-context.d.mts +1 -0
- package/dist/credential/mcp-auth-context.mjs +186 -0
- package/dist/credential/mcp-auth-context.mjs.map +1 -0
- package/dist/credential/resolver.cjs +125 -0
- package/dist/credential/resolver.d.mts +1 -0
- package/dist/credential/resolver.mjs +125 -0
- package/dist/credential/resolver.mjs.map +1 -0
- package/dist/credential/store.cjs +106 -0
- package/dist/credential/store.d.cts +30 -0
- package/dist/credential/store.d.cts.map +1 -0
- package/dist/credential/store.d.mts +30 -0
- package/dist/credential/store.d.mts.map +1 -0
- package/dist/credential/store.mjs +106 -0
- package/dist/credential/store.mjs.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +2 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/mcp/http-transport.cjs +22 -3
- package/dist/mcp/http-transport.mjs +22 -3
- package/dist/mcp/http-transport.mjs.map +1 -1
- package/dist/mcp/prompts.cjs +2 -2
- package/dist/mcp/prompts.mjs +2 -2
- package/dist/mcp/prompts.mjs.map +1 -1
- package/dist/mcp/server.cjs +16 -6
- package/dist/mcp/server.mjs +15 -6
- package/dist/mcp/server.mjs.map +1 -1
- package/dist/mcp/tools.cjs +2 -46
- package/dist/mcp/tools.mjs +2 -46
- package/dist/mcp/tools.mjs.map +1 -1
- package/dist/repl.cjs +9 -3
- package/dist/repl.d.cts.map +1 -1
- package/dist/repl.d.mts.map +1 -1
- package/dist/repl.mjs +9 -3
- package/dist/repl.mjs.map +1 -1
- package/package.json +22 -22
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"afs-loader.mjs","names":[],"sources":["../../src/config/afs-loader.ts"],"sourcesContent":["/**\n * AFS Loader - Lazy loading with parallel tolerant mount\n *\n * Provides loadAFS() for on-demand AFS creation with caching.\n * createAFS() uses Promise.allSettled for parallel provider creation + mount,\n * tolerating individual failures while reporting them to stderr.\n */\n\nimport { AFS } from \"@aigne/afs\";\nimport { createProviderRegistry } from \"@aigne/afs-provider-registry\";\nimport { ConfigLoader } from \"./loader.js\";\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface MountProgressEvent {\n total: number;\n completed: number;\n failed: number;\n}\n\nexport interface MountFailure {\n path: string;\n reason: string;\n}\n\nexport interface CreateAFSResult {\n afs: AFS;\n failures: MountFailure[];\n}\n\nexport interface CreateAFSOptions {\n onProgress?: (event: MountProgressEvent) => void;\n}\n\n// ─── Cache ────────────────────────────────────────────────────────────────\n\nlet cached: AFS | undefined;\n\n/**\n * Reset the cached AFS instance (for testing)\n */\nexport function resetAFSCache(): void {\n cached = undefined;\n}\n\n/**\n * Load AFS with caching - first call creates, subsequent calls return cached instance\n */\nexport async function loadAFS(cwd: string, options?: CreateAFSOptions): Promise<CreateAFSResult> {\n if (cached) return { afs: cached, failures: [] };\n const result = await createAFS(cwd, options);\n cached = result.afs;\n return result;\n}\n\n/**\n * Create AFS instance from config with parallel tolerant mount\n *\n * - All providers are created and mounted in parallel via Promise.allSettled\n * - Individual failures are logged to stderr and skipped (unless onProgress is provided)\n * - If ALL providers fail, throws an error\n * - If no mounts configured, returns empty AFS (no error)\n */\nexport async function createAFS(cwd: string, options?: CreateAFSOptions): Promise<CreateAFSResult> {\n const loader = new ConfigLoader();\n const config = await loader.load(cwd);\n const afs = new AFS();\n\n if (config.mounts.length === 0) {\n return { afs, failures: [] };\n }\n\n // Create registry and register workspace factory (injected here to break circular dep)\n const registry = createProviderRegistry();\n registry.register(\"workspace\", async (mount, parsed) => {\n const mod = await import(\"@aigne/afs-workspace\" as string);\n const AFSWorkspace = mod.AFSWorkspace ?? mod.default;\n if (!AFSWorkspace) {\n throw new Error(\n \"workspace:// scheme requires @aigne/afs-workspace package. Install it with: pnpm add @aigne/afs-workspace\",\n );\n }\n return new AFSWorkspace({\n workspacePath: parsed.path,\n registry,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"workspace\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n });\n\n // ── Inject loadProvider ──\n // Allows agents to mount new providers at runtime via /.actions/mount\n afs.loadProvider = async (uri: string, mountPath: string) => {\n const { parseURI } = await import(\"@aigne/afs-provider-registry\");\n // Validate URI format before passing to factory\n parseURI(uri);\n const provider = await registry.createProvider({ uri, path: mountPath });\n await afs.mount(provider, mountPath);\n };\n\n // ── Auto-mount Registry Provider (tolerant — silent on failure) ──\n if (config.registry?.enabled !== false) {\n try {\n const { AFSRegistry } = await import(\"@aigne/afs-registry\");\n const registryProvider = new AFSRegistry({\n providers: (config.registry?.providers ?? []) as any[],\n });\n await afs.mount(registryProvider, \"/registry/official\");\n } catch {\n // Silent degradation — registry is optional\n }\n }\n\n const total = config.mounts.length;\n let completed = 0;\n let failedCount = 0;\n\n options?.onProgress?.({ total, completed: 0, failed: 0 });\n\n const results = await Promise.allSettled(\n config.mounts.map(async (mount) => {\n const provider = await registry.createProvider(mount);\n await afs.mount(provider, mount.path, { namespace: mount.namespace ?? null });\n completed++;\n options?.onProgress?.({ total, completed, failed: failedCount });\n return mount.path;\n }),\n );\n\n const failures: MountFailure[] = [];\n const succeeded: string[] = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i]!;\n if (result.status === \"fulfilled\") {\n succeeded.push(result.value);\n } else {\n const reason = result.reason;\n const msg = reason instanceof Error ? reason.message : String(reason);\n failures.push({ path: config.mounts[i]!.path, reason: msg });\n failedCount++;\n completed++;\n options?.onProgress?.({ total, completed, failed: failedCount });\n }\n }\n\n // When onProgress is provided, the caller handles display; otherwise log to stderr\n if (!options?.onProgress && failures.length > 0) {\n console.warn(`[mount] ${succeeded.length} succeeded, ${failures.length} failed:`);\n for (const f of failures) {\n console.warn(` - ${f.path}: ${f.reason}`);\n }\n }\n\n if (succeeded.length === 0 && config.mounts.length > 0) {\n throw new Error(\"All providers failed to mount\");\n }\n\n return { afs, failures };\n}\n"],"mappings":";;;;;;;;;;;;AAoCA,IAAI;;;;AAYJ,eAAsB,QAAQ,KAAa,SAAsD;AAC/F,KAAI,OAAQ,QAAO;EAAE,KAAK;EAAQ,UAAU,EAAE;EAAE;CAChD,MAAM,SAAS,MAAM,UAAU,KAAK,QAAQ;AAC5C,UAAS,OAAO;AAChB,QAAO;;;;;;;;;;AAWT,eAAsB,UAAU,KAAa,SAAsD;CAEjG,MAAM,SAAS,MADA,IAAI,cAAc,CACL,KAAK,IAAI;CACrC,MAAM,MAAM,IAAI,KAAK;AAErB,KAAI,OAAO,OAAO,WAAW,EAC3B,QAAO;EAAE;EAAK,UAAU,EAAE;EAAE;CAI9B,MAAM,WAAW,wBAAwB;AACzC,UAAS,SAAS,aAAa,OAAO,OAAO,WAAW;EACtD,MAAM,MAAM,MAAM,OAAO;EACzB,MAAM,eAAe,IAAI,gBAAgB,IAAI;AAC7C,MAAI,CAAC,aACH,OAAM,IAAI,MACR,4GACD;AAEH,SAAO,IAAI,aAAa;GACtB,eAAe,OAAO;GACtB;GACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;GACjD,aAAa,MAAM;GACnB,YAAY,MAAM;GAClB,GAAG,MAAM;GACV,CAAC;GACF;AAIF,KAAI,eAAe,OAAO,KAAa,cAAsB;EAC3D,MAAM,EAAE,aAAa,MAAM,OAAO;AAElC,WAAS,IAAI;EACb,MAAM,WAAW,MAAM,SAAS,eAAe;GAAE;GAAK,MAAM;GAAW,CAAC;AACxE,QAAM,IAAI,MAAM,UAAU,UAAU;;AAItC,KAAI,OAAO,UAAU,YAAY,MAC/B,KAAI;EACF,MAAM,EAAE,gBAAgB,MAAM,OAAO;EACrC,MAAM,mBAAmB,IAAI,YAAY,EACvC,WAAY,OAAO,UAAU,aAAa,EAAE,EAC7C,CAAC;AACF,QAAM,IAAI,MAAM,kBAAkB,qBAAqB;SACjD;CAKV,MAAM,QAAQ,OAAO,OAAO;CAC5B,IAAI,YAAY;CAChB,IAAI,cAAc;AAElB,UAAS,aAAa;EAAE;EAAO,WAAW;EAAG,QAAQ;EAAG,CAAC;CAEzD,MAAM,UAAU,MAAM,QAAQ,WAC5B,OAAO,OAAO,IAAI,OAAO,UAAU;EACjC,MAAM,WAAW,MAAM,SAAS,eAAe,MAAM;AACrD,QAAM,IAAI,MAAM,UAAU,MAAM,MAAM,EAAE,WAAW,MAAM,aAAa,MAAM,CAAC;AAC7E;AACA,WAAS,aAAa;GAAE;GAAO;GAAW,QAAQ;GAAa,CAAC;AAChE,SAAO,MAAM;GACb,CACH;CAED,MAAM,WAA2B,EAAE;CACnC,MAAM,YAAsB,EAAE;AAE9B,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,SAAS,QAAQ;AACvB,MAAI,OAAO,WAAW,YACpB,WAAU,KAAK,OAAO,MAAM;OACvB;GACL,MAAM,SAAS,OAAO;GACtB,MAAM,MAAM,kBAAkB,QAAQ,OAAO,UAAU,OAAO,OAAO;AACrE,YAAS,KAAK;IAAE,MAAM,OAAO,OAAO,GAAI;IAAM,QAAQ;IAAK,CAAC;AAC5D;AACA;AACA,YAAS,aAAa;IAAE;IAAO;IAAW,QAAQ;IAAa,CAAC;;;AAKpE,KAAI,CAAC,SAAS,cAAc,SAAS,SAAS,GAAG;AAC/C,UAAQ,KAAK,WAAW,UAAU,OAAO,cAAc,SAAS,OAAO,UAAU;AACjF,OAAK,MAAM,KAAK,SACd,SAAQ,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS;;AAI9C,KAAI,UAAU,WAAW,KAAK,OAAO,OAAO,SAAS,EACnD,OAAM,IAAI,MAAM,gCAAgC;AAGlD,QAAO;EAAE;EAAK;EAAU"}
|
|
1
|
+
{"version":3,"file":"afs-loader.mjs","names":["options","opts","AFS"],"sources":["../../src/config/afs-loader.ts"],"sourcesContent":["/**\n * AFS Loader - Lazy loading with parallel tolerant mount\n *\n * Provides loadAFS() for on-demand AFS creation with caching.\n * createAFS() uses Promise.allSettled for parallel provider creation + mount,\n * tolerating individual failures while reporting them to stderr.\n *\n * Integrates 4-step credential resolution:\n * 1. Determine missing fields from provider schema\n * 2. Silent resolution (config > env > credential store)\n * 3. Interactive collection (provider auth() or default collect())\n * 4. Unified persistence (sensitive → credentials.toml, non-sensitive → config options)\n */\n\nimport type { AFSModule, AuthContext, MountConfig } from \"@aigne/afs\";\nimport { AFS, ProviderRegistry } from \"@aigne/afs\";\nimport { parseURI } from \"@aigne/afs/utils/uri\";\nimport type { CredentialStore } from \"../credential/store.js\";\nimport { ConfigLoader } from \"./loader.js\";\nimport {\n type ConfigMountEntry,\n type PersistScope,\n persistMount,\n unpersistMount,\n} from \"./mount-commands.js\";\n\n// ─── Workspace Factory Helper ─────────────────────────────────────────────\n\n/**\n * Register the workspace:// scheme on a ProviderRegistry.\n *\n * Extracted so that both createAFS() and verifyMount() can support\n * workspace URIs. createAFS() overrides this with a richer variant\n * that includes credential resolution.\n */\nfunction registerWorkspaceFactory(registry: import(\"@aigne/afs\").ProviderRegistry): void {\n registry.register(\"workspace\", async (mount, parsed) => {\n const mod = await import(\"@aigne/afs-workspace\" as string);\n const AFSWorkspace = mod.AFSWorkspace ?? mod.default;\n if (!AFSWorkspace) {\n throw new Error(\n \"workspace:// scheme requires @aigne/afs-workspace package. Install it with: pnpm add @aigne/afs-workspace\",\n );\n }\n return new AFSWorkspace({\n workspacePath: parsed.body,\n registry,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"workspace\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n });\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────\n\nexport interface MountProgressEvent {\n total: number;\n completed: number;\n failed: number;\n}\n\nexport interface MountFailure {\n path: string;\n reason: string;\n}\n\nexport interface CreateAFSResult {\n afs: AFS;\n failures: MountFailure[];\n}\n\nexport interface CreateAFSOptions {\n onProgress?: (event: MountProgressEvent) => void;\n /** Auth context for interactive credential collection (CLI or MCP) */\n authContext?: AuthContext;\n /** Credential store for reading/writing credentials */\n credentialStore?: CredentialStore;\n}\n\n// ─── Cache ────────────────────────────────────────────────────────────────\n\nlet cached: AFS | undefined;\n\n/**\n * Reset the cached AFS instance (for testing)\n */\nexport function resetAFSCache(): void {\n cached = undefined;\n}\n\n/**\n * Load AFS with caching - first call creates, subsequent calls return cached instance\n */\nexport async function loadAFS(cwd: string, options?: CreateAFSOptions): Promise<CreateAFSResult> {\n if (cached) return { afs: cached, failures: [] };\n const result = await createAFS(cwd, options);\n cached = result.afs;\n return result;\n}\n\n/**\n * Create AFS instance from config with parallel tolerant mount\n *\n * - All providers are created and mounted in parallel via Promise.allSettled\n * - Individual failures are logged to stderr and skipped (unless onProgress is provided)\n * - If ALL providers fail, throws an error\n * - If no mounts configured, returns empty AFS (no error)\n */\nexport async function createAFS(cwd: string, options?: CreateAFSOptions): Promise<CreateAFSResult> {\n const loader = new ConfigLoader();\n const { config, mountSources } = await loader.loadWithSources(cwd);\n const afs = new AFS();\n const authContext = options?.authContext;\n const credentialStore = options?.credentialStore;\n\n // Create registry — auto-loads built-in providers via manifest-driven resolution.\n // Only workspace needs explicit registration (to break circular deps + inject credential callback).\n const registry = new ProviderRegistry();\n registry.register(\"workspace\", async (mount, parsed) => {\n const mod = await import(\"@aigne/afs-workspace\" as string);\n const AFSWorkspace = mod.AFSWorkspace ?? mod.default;\n if (!AFSWorkspace) {\n throw new Error(\n \"workspace:// scheme requires @aigne/afs-workspace package. Install it with: pnpm add @aigne/afs-workspace\",\n );\n }\n const { resolve } = await import(\"node:path\");\n const _workspacePath = resolve(parsed.body);\n return new AFSWorkspace({\n workspacePath: parsed.body,\n registry,\n createProvider: async (subMount: MountConfig) => {\n const credResult = await resolveAndMergeCredentials(\n subMount,\n authContext,\n credentialStore,\n registry,\n );\n // Create provider with resolved credentials in mount.options\n const provider = await registry.createProvider(subMount);\n // Persist sensitive credentials to credential store\n if (credResult) {\n await persistCredentialResult(subMount, credResult);\n // Strip sensitive values from mount.options (keep only non-sensitive for config)\n const opts = subMount.options ?? {};\n for (const key of Object.keys(credResult.sensitive)) {\n delete opts[key];\n }\n subMount.options = Object.keys(opts).length > 0 ? opts : undefined;\n // Merge non-sensitive resolved values back\n if (Object.keys(credResult.nonSensitive).length > 0) {\n subMount.options = { ...(subMount.options ?? {}), ...credResult.nonSensitive };\n }\n }\n return provider;\n },\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"workspace\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n });\n\n // ── Inject loadProvider ──\n // Allows agents to mount new providers at runtime via /.actions/mount\n afs.loadProvider = async (uri: string, mountPath: string, options?: Record<string, unknown>) => {\n // Validate URI format before passing to factory\n parseURI(uri);\n\n // Extract env query params from MCP URIs for secure credential storage\n const { cleanUri: loadConfigUri, envRecord: loadEnvRecord } = extractEnvFromURI(uri);\n const hasLoadEnv = Object.keys(loadEnvRecord).length > 0;\n\n // Extract known mount-level fields; pass remaining as provider-specific options\n const { accessMode, auth, description, scope, ...providerOptions } = options ?? {};\n\n // Inject extracted env values into provider options\n if (hasLoadEnv) {\n const existingEnv = (providerOptions.env as Record<string, string>) ?? {};\n providerOptions.env = { ...existingEnv, ...loadEnvRecord };\n }\n\n const mount: MountConfig = {\n uri,\n path: mountPath,\n access_mode: (accessMode as \"readonly\" | \"readwrite\") ?? undefined,\n auth: (auth as string) ?? undefined,\n description: (description as string) ?? undefined,\n options: Object.keys(providerOptions).length > 0 ? providerOptions : undefined,\n };\n\n const persistScope = (scope as PersistScope) || \"cwd\";\n let credResult = await resolveAndMergeCredentials(\n mount,\n authContext,\n credentialStore,\n registry,\n );\n\n try {\n const provider = await registry.createProvider(mount);\n await afs.mount(provider, mountPath);\n } catch (mountError) {\n // Health check failed with silently resolved credentials →\n // retry once with forced interactive collection so user can fix values\n if (credResult === null && authContext) {\n // credResult === null means all fields resolved silently (no interactive collection)\n // Rebuild mount config from scratch for retry\n const retryProviderOptions = { ...providerOptions };\n const retryMount: MountConfig = {\n uri,\n path: mountPath,\n access_mode: (accessMode as \"readonly\" | \"readwrite\") ?? undefined,\n auth: (auth as string) ?? undefined,\n description: (description as string) ?? undefined,\n options: Object.keys(retryProviderOptions).length > 0 ? retryProviderOptions : undefined,\n };\n credResult = await resolveAndMergeCredentials(\n retryMount,\n authContext,\n credentialStore,\n registry,\n { forceCollect: true },\n );\n const retryProvider = await registry.createProvider(retryMount);\n await afs.mount(retryProvider, mountPath);\n // Update mount reference for persistence below\n Object.assign(mount, retryMount);\n } else {\n throw mountError;\n }\n }\n\n // Persist credentials if any were collected\n if (credResult) {\n await persistCredentialResult(mount, credResult);\n }\n\n // Persist extracted MCP env values to credentials.toml\n if (hasLoadEnv && credentialStore) {\n try {\n const envCreds: Record<string, string> = {};\n for (const [k, v] of Object.entries(loadEnvRecord)) {\n envCreds[`env:${k}`] = v;\n }\n // Merge with any existing sensitive values from credential resolution\n const existing = credResult ? { ...credResult.sensitive } : {};\n await credentialStore.set(loadConfigUri, { ...existing, ...envCreds });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[mount] env credential persistence failed: ${msg}`);\n }\n }\n\n // Always persist to config after successful mount\n try {\n // Use configUri (env stripped) for config.toml — env values are in credentials.toml\n const entry: ConfigMountEntry = {\n path: mountPath,\n uri: hasLoadEnv ? loadConfigUri : mount.uri,\n };\n if (description) entry.description = description as string;\n if (accessMode) entry.access_mode = accessMode as \"readonly\" | \"readwrite\";\n if (auth) entry.auth = auth as string;\n const mergedOptions = { ...providerOptions, ...credResult?.nonSensitive };\n // Strip env from config options — it's in credentials.toml\n if (hasLoadEnv) delete mergedOptions.env;\n if (Object.keys(mergedOptions).length > 0) entry.options = mergedOptions;\n await persistMount(cwd, entry, persistScope);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[mount] config persistence failed: ${msg}`);\n }\n };\n\n // ── Inject unloadProvider ──\n // Removes mount config when provider is unmounted via /.actions/unmount\n afs.unloadProvider = async (mountPath: string, options?: Record<string, unknown>) => {\n try {\n await unpersistMount(cwd, mountPath, (options?.scope as PersistScope) || undefined);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[unmount] config removal failed: ${msg}`);\n }\n };\n\n // ── Auto-mount Registry Providers (tolerant — silent on failure) ──\n if (config.registry?.enabled !== false) {\n try {\n const { AFSRegistry } = await import(\"@aigne/afs-registry\");\n\n // /registry/official — external providers from remote registry\n const officialRegistry = new AFSRegistry(\n config.registry?.providers?.length\n ? { providers: config.registry.providers as any[] }\n : {\n url: \"https://raw.githubusercontent.com/ArcBlock/afs-registry/refs/heads/main/providers.json\",\n },\n );\n await afs.mount(officialRegistry, \"/registry/official\");\n\n // /registry/internal — built-in providers bundled with CLI\n const internalRegistry = new AFSRegistry();\n await afs.mount(internalRegistry, \"/registry/internal\");\n } catch {\n // Silent degradation — registry is optional\n }\n }\n\n if (config.mounts.length === 0) {\n return { afs, failures: [] };\n }\n\n const total = config.mounts.length;\n let completed = 0;\n let failedCount = 0;\n\n options?.onProgress?.({ total, completed: 0, failed: 0 });\n\n // Load stored credentials keyed by URI\n if (credentialStore && mountSources.size > 0) {\n await mergeStoredCredentials(config.mounts, mountSources, credentialStore);\n }\n\n const results = await Promise.allSettled(\n config.mounts.map(async (mount) => {\n const provider = await registry.createProvider(mount);\n await afs.mount(provider, mount.path, { namespace: mount.namespace ?? null });\n completed++;\n options?.onProgress?.({ total, completed, failed: failedCount });\n return mount.path;\n }),\n );\n\n const failures: MountFailure[] = [];\n const succeeded: string[] = [];\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i]!;\n if (result.status === \"fulfilled\") {\n succeeded.push(result.value);\n } else {\n const reason = result.reason;\n const msg = reason instanceof Error ? reason.message : String(reason);\n failures.push({ path: config.mounts[i]!.path, reason: msg });\n failedCount++;\n completed++;\n options?.onProgress?.({ total, completed, failed: failedCount });\n }\n }\n\n // When onProgress is provided, the caller handles display; otherwise log to stderr\n if (!options?.onProgress && failures.length > 0) {\n console.warn(`[mount] ${succeeded.length} succeeded, ${failures.length} failed:`);\n for (const f of failures) {\n console.warn(` - ${f.path}: ${f.reason}`);\n }\n }\n\n if (succeeded.length === 0 && config.mounts.length > 0) {\n throw new Error(\"All providers failed to mount\");\n }\n\n return { afs, failures };\n}\n\n// ─── Credential Resolution Helpers ─────────────────────────────────────────\n\n/**\n * Extract env query params from MCP URIs for secure credential storage.\n *\n * MCP servers receive secrets via env vars (e.g., `mcp+stdio://npx?env=API_KEY=sk-xxx`).\n * This function extracts env params from the URI so they can be stored in credentials.toml\n * instead of being persisted in plaintext in config.toml.\n *\n * Only applies to MCP schemes (mcp://, mcp+stdio://, mcp+sse://).\n * Non-MCP URIs are returned unchanged.\n */\nexport function extractEnvFromURI(uri: string): {\n cleanUri: string;\n envRecord: Record<string, string>;\n} {\n const parsed = parseURI(uri);\n const envRecord: Record<string, string> = {};\n\n // Only extract env for MCP schemes\n if (!parsed.scheme.startsWith(\"mcp\")) {\n return { cleanUri: uri, envRecord };\n }\n\n const envValues = parsed.query.env;\n if (!envValues) return { cleanUri: uri, envRecord };\n\n // Parse env=KEY=VALUE format (split on first = only)\n const envList = Array.isArray(envValues) ? envValues : [envValues];\n for (const entry of envList) {\n const eqIdx = entry.indexOf(\"=\");\n if (eqIdx > 0) {\n envRecord[entry.slice(0, eqIdx)] = entry.slice(eqIdx + 1);\n }\n }\n\n // Rebuild URI without env params, preserving other query params\n const queryIndex = uri.indexOf(\"?\");\n if (queryIndex < 0) return { cleanUri: uri, envRecord };\n\n const rawQuery = uri.slice(queryIndex + 1);\n const params = new URLSearchParams(rawQuery);\n params.delete(\"env\");\n const newQuery = params.toString();\n const base = uri.slice(0, queryIndex);\n const cleanUri = newQuery ? `${base}?${newQuery}` : base;\n\n return { cleanUri, envRecord };\n}\n\n/**\n * Extract template variables from mount.uri using the manifest's uriTemplate\n * and merge them into mount.options so the credential resolver sees them as \"known\".\n *\n * Example: cloudflare://d9e5fca3... + template \"cloudflare://{accountId}\"\n * → mount.options.accountId = \"d9e5fca3...\"\n */\nfunction mergeTemplateVarsIntoMount(\n mount: MountConfig,\n manifest: import(\"@aigne/afs\").ProviderManifest | null | undefined,\n): void {\n if (!manifest?.uriTemplate) return;\n const { parseTemplate } =\n require(\"@aigne/afs/utils/uri-template\") as typeof import(\"@aigne/afs/utils/uri-template\");\n const parsed = parseURI(mount.uri);\n // Gracefully handle empty/partial URI bodies — template vars may not be\n // extractable yet (e.g. when the mount action was called without --uri and\n // without providing all required template parameters as args).\n let templateVars: Record<string, string | undefined>;\n try {\n templateVars = parseTemplate(manifest.uriTemplate, parsed.body);\n } catch {\n return; // Body doesn't match template yet — nothing to merge\n }\n for (const [key, value] of Object.entries(templateVars)) {\n if (value !== undefined) {\n if (!mount.options) mount.options = {};\n if (mount.options[key] === undefined) {\n mount.options[key] = value;\n }\n }\n }\n}\n\n/**\n * After credential resolution, rebuild mount.uri from the template if the\n * current URI body is empty/incomplete and resolved options can fill template vars.\n *\n * Example: mount.uri = \"cloudflare://\" (empty body), mount.options.accountId = \"abc\"\n * → mount.uri = \"cloudflare://abc\"\n */\nfunction rebuildURIFromTemplate(\n mount: MountConfig,\n manifest: import(\"@aigne/afs\").ProviderManifest | null | undefined,\n): void {\n if (!manifest?.uriTemplate) return;\n const { buildURI, getTemplateVariableNames } =\n require(\"@aigne/afs/utils/uri-template\") as typeof import(\"@aigne/afs/utils/uri-template\");\n const varNames = getTemplateVariableNames(manifest.uriTemplate);\n if (varNames.length === 0) return;\n\n // Check if any template vars are missing from the current URI body\n const parsed = parseURI(mount.uri);\n const { parseTemplate } =\n require(\"@aigne/afs/utils/uri-template\") as typeof import(\"@aigne/afs/utils/uri-template\");\n let existingVars: Record<string, string | undefined>;\n try {\n existingVars = parseTemplate(manifest.uriTemplate, parsed.body);\n } catch {\n existingVars = {}; // Body incomplete\n }\n\n // Merge: existing URI vars + mount.options (options fill in gaps)\n const allVars: Record<string, string | undefined> = { ...existingVars };\n for (const name of varNames) {\n if (!allVars[name] && mount.options?.[name] != null) {\n allVars[name] = String(mount.options[name]);\n }\n }\n\n // Rebuild if we can fill more vars than the current URI has\n try {\n const newURI = buildURI(manifest.uriTemplate, allVars);\n if (newURI !== mount.uri) {\n mount.uri = newURI;\n }\n } catch {\n // Still can't build a complete URI — that's OK, createProvider will handle it\n }\n}\n\n/**\n * Attempt credential resolution for a mount, merging resolved values into mount.options.\n *\n * Returns the credential result if any fields were collected interactively,\n * or null if no interactive collection was needed (or no schema/authContext).\n *\n * This function mutates mount.options and mount.auth/mount.token when credentials\n * are resolved, so the subsequent registry.createProvider(mount) receives complete values.\n */\nasync function resolveAndMergeCredentials(\n mount: MountConfig,\n authContext: AuthContext | undefined,\n credentialStore: CredentialStore | undefined,\n registry: ProviderRegistry,\n opts?: { forceCollect?: boolean },\n): Promise<import(\"../credential/resolver.js\").ResolveCredentialsResult | null> {\n // Get provider schema and auth method via registry\n const info = await registry.getProviderInfo(mount.uri);\n const schema = info?.schema ?? null;\n const providerAuth = info?.auth;\n\n if (!schema) return null;\n\n // Extract template variables from URI and merge into mount.options\n // so they appear as \"known\" values in the resolver (skipped in form)\n mergeTemplateVarsIntoMount(mount, info?.manifest);\n\n // Skip interactive credential resolution when schema has no sensitive or env-annotated\n // fields. Non-sensitive fields (like command for MCP, localPath for FS) are URI/config\n // parameters, not credentials — they should not trigger a browser form.\n const { getSensitiveFields } = await import(\"@aigne/afs/utils/schema\");\n const sensitiveFieldsInSchema = getSensitiveFields(schema);\n const schemaProps = (schema as any).properties ?? {};\n const hasEnvFields = Object.values(schemaProps).some((p: any) => Array.isArray(p?.env));\n if (sensitiveFieldsInSchema.length === 0 && !hasEnvFields && !providerAuth) return null;\n\n const { resolveCredentials } = await import(\"../credential/resolver.js\");\n\n const result = await resolveCredentials({\n mount,\n schema,\n authContext,\n credentialStore,\n providerAuth,\n forceCollect: opts?.forceCollect,\n });\n\n if (!result) {\n // User declined/cancelled credential collection — abort mount\n const fieldNames = Object.keys((schema as any).properties ?? {});\n const known = new Set<string>();\n if (mount.auth !== undefined) known.add(\"auth\");\n if (mount.token !== undefined) known.add(\"token\");\n if (mount.options) {\n for (const k of Object.keys(mount.options)) known.add(k);\n }\n const missing = fieldNames.filter((f) => !known.has(f));\n const fieldList = missing.length > 0 ? missing.join(\", \") : fieldNames.join(\", \");\n throw new Error(\n `Missing credentials: ${fieldList}. ` +\n `Retry with them as args, e.g. { \"uri\": \"${mount.uri}\", \"path\": \"${mount.path}\", ${missing.map((f) => `\"${f}\": \"...\"`).join(\", \")} }`,\n );\n }\n\n // Merge resolved values into mount config for provider creation\n if (Object.keys(result.values).length > 0) {\n // Map sensitive credential fields to mount.auth/token if applicable\n if (result.values.token !== undefined && mount.token === undefined) {\n mount.token = String(result.values.token);\n }\n if (result.values.auth !== undefined && mount.auth === undefined) {\n mount.auth = String(result.values.auth);\n }\n\n // Merge remaining resolved values into mount.options\n const opts = mount.options ?? {};\n for (const [key, value] of Object.entries(result.values)) {\n if (key !== \"token\" && key !== \"auth\" && opts[key] === undefined) {\n opts[key] = value;\n }\n }\n if (Object.keys(opts).length > 0) {\n mount.options = opts;\n }\n }\n\n // Rebuild URI if credential resolution filled in template variables\n // (e.g. accountId collected via form → cloudflare:// becomes cloudflare://abc)\n rebuildURIFromTemplate(mount, info?.manifest);\n\n return result.collected ? result : null;\n}\n\n/**\n * Persist credential resolution result (sensitive → credentials.toml, non-sensitive → config).\n */\nasync function persistCredentialResult(\n mount: MountConfig,\n result: import(\"../credential/resolver.js\").ResolveCredentialsResult,\n): Promise<void> {\n if (Object.keys(result.sensitive).length > 0) {\n try {\n const { createCredentialStore } = await import(\"../credential/store.js\");\n const store = createCredentialStore();\n await store.set(mount.uri, result.sensitive);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[mount] credential persistence failed: ${msg}`);\n }\n }\n}\n\n/**\n * Load stored credentials and merge into mount options during startup.\n *\n * Credentials are keyed by URI (the resource identity), not mount path.\n */\nasync function mergeStoredCredentials(\n mounts: MountConfig[],\n _mountSources: Map<string, string>,\n store: CredentialStore,\n): Promise<void> {\n for (const mount of mounts) {\n try {\n const stored = await store.get(mount.uri);\n if (stored) {\n const opts = mount.options ?? {};\n for (const [key, value] of Object.entries(stored)) {\n if (key.startsWith(\"env:\")) {\n // Reconstruct env Record from flattened env:KEY credential entries\n if (!opts.env) opts.env = {};\n (opts.env as Record<string, string>)[key.slice(4)] = value;\n } else if (opts[key] === undefined) {\n opts[key] = value;\n }\n }\n if (Object.keys(opts).length > 0) {\n mount.options = opts;\n }\n }\n } catch {\n // Credential lookup failure is non-fatal\n }\n }\n}\n\n// ─── Exported Credential Resolution for CLI mount add ───────────────────────\n\nexport interface ResolveCredentialsForMountOptions {\n cwd: string;\n uri: string;\n mountPath: string;\n authContext?: AuthContext;\n credentialStore?: CredentialStore;\n /** Extra key-value options provided by the user (e.g., --host, --password) */\n extraOptions?: Record<string, unknown>;\n /** Field names that should be treated as sensitive (for ad-hoc schema) */\n sensitiveArgs?: string[];\n /** Registry instance for resolving provider schema/auth. If not provided, one is created. */\n registry?: ProviderRegistry;\n /** Force interactive collection even when all fields are silently resolved.\n * Used for retry after health-check failure with stale env/store values. */\n forceCollect?: boolean;\n}\n\nexport interface ResolveCredentialsForMountResult {\n /** Whether interactive collection was performed */\n collected: boolean;\n /** Non-sensitive values to persist in config.toml options */\n nonSensitive: Record<string, unknown>;\n /** All resolved values (for mount verification before persisting) */\n allValues: Record<string, unknown>;\n /** Persist sensitive credentials to credentials.toml.\n * Call AFTER health check succeeds — never before. */\n persistCredentials: () => Promise<void>;\n /** Sensitive field names — caller should strip these from config options */\n sensitiveFields: string[];\n /** URI with env params stripped (for MCP URIs). Use for config.toml persistence.\n * Undefined when no env extraction was performed. */\n configUri?: string;\n}\n\n/**\n * Resolve and persist credentials for a mount configuration.\n *\n * Used by `mount add` CLI command to trigger credential collection\n * at add-time rather than deferring to AFS creation.\n *\n * - Gets provider schema via registry\n * - Runs 4-step credential resolution\n * - Persists sensitive values to credentials.toml\n * - Returns non-sensitive values for caller to update config\n *\n * Returns null if no schema found or no credentials needed.\n * Throws if user cancels collection when credentials are required.\n */\nexport async function resolveCredentialsForMount(\n options: ResolveCredentialsForMountOptions,\n): Promise<ResolveCredentialsForMountResult | null> {\n const { uri, mountPath, authContext, credentialStore, extraOptions, sensitiveArgs } = options;\n\n // Extract env query params from MCP URIs for secure credential storage\n const { cleanUri: configUri, envRecord } = extractEnvFromURI(uri);\n const hasExtractedEnv = Object.keys(envRecord).length > 0;\n\n // Get provider schema and auth via registry\n const registry = options.registry ?? new ProviderRegistry();\n const info = await registry.getProviderInfo(uri);\n let schema = info?.schema ?? null;\n const providerAuth = info?.auth;\n\n // Fallback: build ad-hoc schema from extraOptions + sensitiveArgs\n if (!schema && extraOptions && Object.keys(extraOptions).length > 0) {\n const { buildAdHocSchema } = await import(\"@aigne/afs/utils/schema\");\n schema = buildAdHocSchema(extraOptions, sensitiveArgs ?? []);\n }\n // Helper: return env-only result when no schema-based credential resolution is needed\n // but env values were extracted from MCP URI and need credential storage\n const envOnlyResult = (): ResolveCredentialsForMountResult | null => {\n if (!hasExtractedEnv) return null;\n return {\n collected: false,\n nonSensitive: {},\n allValues: {},\n persistCredentials: async () => {\n const toStore: Record<string, string> = {};\n for (const [key, val] of Object.entries(envRecord)) {\n toStore[`env:${key}`] = val;\n }\n if (credentialStore) {\n try {\n await credentialStore.set(configUri, toStore);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[mount add] credential persistence failed: ${msg}`);\n }\n }\n },\n sensitiveFields: [],\n configUri,\n };\n };\n\n if (!schema) return envOnlyResult();\n\n const properties = (schema as any).properties;\n if (!properties || Object.keys(properties).length === 0) return envOnlyResult();\n\n // Augment schema with sensitiveArgs — mark existing fields as sensitive\n // and add new fields not in provider schema (from extraOptions)\n if (sensitiveArgs && sensitiveArgs.length > 0) {\n const mergedProps = { ...properties };\n let changed = false;\n for (const field of sensitiveArgs) {\n if (mergedProps[field]) {\n // Existing field → mark as sensitive\n mergedProps[field] = { ...mergedProps[field], sensitive: true };\n changed = true;\n } else if (extraOptions?.[field] !== undefined) {\n // New field not in provider schema → add with inferred type\n const value = extraOptions[field];\n mergedProps[field] = {\n type:\n typeof value === \"number\"\n ? \"number\"\n : typeof value === \"boolean\"\n ? \"boolean\"\n : \"string\",\n sensitive: true,\n };\n changed = true;\n }\n }\n if (changed) {\n schema = { ...schema, properties: mergedProps } as typeof schema;\n }\n }\n\n // Skip credential resolution if no fields need credential handling.\n // A field needs credential handling if it has `sensitive: true` (needs secure storage)\n // or `env` metadata (can be resolved from environment variables).\n // Plain required fields (like localPath for fs://) are URI parameters, not credentials.\n const { getSensitiveFields } = await import(\"@aigne/afs/utils/schema\");\n const sensitiveFieldsInSchema = getSensitiveFields(schema);\n const schemaProps = (schema as any).properties;\n const hasEnvFields = Object.values(schemaProps).some((p: any) => Array.isArray(p?.env));\n if (sensitiveFieldsInSchema.length === 0 && !hasEnvFields && !extraOptions)\n return envOnlyResult();\n\n // Build mount config for resolver\n const mount: MountConfig = { uri, path: mountPath };\n\n // Merge extraOptions into mount.options so resolver sees them as known values\n if (extraOptions && Object.keys(extraOptions).length > 0) {\n mount.options = { ...(mount.options ?? {}), ...extraOptions };\n }\n\n // Extract template variables from URI and merge into mount.options\n mergeTemplateVarsIntoMount(mount, info?.manifest);\n\n // Resolve credentials — schema comes from manifest (user-facing fields only)\n const { resolveCredentials } = await import(\"../credential/resolver.js\");\n\n const result = await resolveCredentials({\n mount,\n schema,\n authContext,\n credentialStore,\n providerAuth,\n forceCollect: options.forceCollect,\n });\n\n if (!result) {\n // User declined — include field names for actionable error\n const fieldNames = Object.keys(properties);\n throw new Error(\n `Missing credentials: ${fieldNames.join(\", \")}. ` +\n `Retry with them as args, e.g. { \"uri\": \"${uri}\", \"path\": \"${mountPath}\", ${fieldNames.map((f) => `\"${f}\": \"...\"`).join(\", \")} }`,\n );\n }\n\n // Compute sensitive fields from (possibly augmented) schema\n const sensitiveFieldSet = new Set(sensitiveFieldsInSchema);\n\n // Build flattened sensitive values for credential store.\n // Handles env object → env:KEY flattening for flat credential storage.\n const flatSensitive: Record<string, string> = {};\n for (const field of sensitiveFieldsInSchema) {\n const val = result.values[field];\n if (val === undefined) continue;\n if (field === \"env\" && typeof val === \"object\" && val !== null) {\n for (const [envKey, envVal] of Object.entries(val as Record<string, string>)) {\n flatSensitive[`env:${envKey}`] = String(envVal);\n }\n } else {\n flatSensitive[field] = String(val);\n }\n }\n\n // Non-sensitive values for config.toml options\n const nonSensitive: Record<string, unknown> = result.collected\n ? result.nonSensitive\n : extraOptions\n ? Object.fromEntries(Object.entries(extraOptions).filter(([k]) => !sensitiveFieldSet.has(k)))\n : {};\n\n // Build deferred persistence callback — caller invokes AFTER health check succeeds\n // Merges schema-sensitive values with extracted env values from MCP URIs\n const persistCredentials = async () => {\n const toStore = { ...flatSensitive };\n // Merge env values extracted from MCP URI (env:KEY format)\n for (const [key, val] of Object.entries(envRecord)) {\n toStore[`env:${key}`] = val;\n }\n if (Object.keys(toStore).length > 0 && credentialStore) {\n try {\n // Use configUri (env stripped) as credential key so reload can match\n await credentialStore.set(configUri, toStore);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[mount add] credential persistence failed: ${msg}`);\n }\n }\n };\n\n return {\n collected: result.collected,\n nonSensitive,\n allValues: result.values,\n persistCredentials,\n sensitiveFields: sensitiveFieldsInSchema,\n configUri: hasExtractedEnv ? configUri : undefined,\n };\n}\n\n// ─── Mount Verification ───────────────────────────────────────────────────────\n\n/**\n * Verify that a mount configuration produces a working provider.\n *\n * Creates the provider and mounts it on a temporary AFS instance,\n * which triggers the built-in checkProviderOnMount (stat + data validation + list).\n * Throws if the mount check fails.\n *\n * @param uri - Provider URI\n * @param mountPath - Mount path\n * @param options - Merged options (non-sensitive + sensitive) for provider creation\n */\nexport async function verifyMount(\n uri: string,\n mountPath: string,\n options?: Record<string, unknown>,\n): Promise<void> {\n const mount: MountConfig = {\n uri,\n path: mountPath,\n options: options && Object.keys(options).length > 0 ? options : undefined,\n };\n\n const registry = new ProviderRegistry();\n registerWorkspaceFactory(registry);\n let provider: AFSModule;\n try {\n provider = await registry.createProvider(mount);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(`Mount verification failed (provider creation): ${msg}`);\n }\n\n // Mount on a temporary AFS to trigger the real checkProviderOnMount\n try {\n const { AFS } = await import(\"@aigne/afs\");\n const tempAFS = new AFS();\n await tempAFS.mount(provider, mountPath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n throw new Error(\n `Mount verification failed: could not reach provider at ${uri}. ` +\n `Error: ${msg}. Check your URI and credentials.`,\n );\n } finally {\n // Clean up provider resources (e.g., MCP process)\n if (typeof (provider as any).close === \"function\") {\n try {\n await (provider as any).close();\n } catch {\n // ignore cleanup errors\n }\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAmCA,SAAS,yBAAyB,UAAuD;AACvF,UAAS,SAAS,aAAa,OAAO,OAAO,WAAW;EACtD,MAAM,MAAM,MAAM,OAAO;EACzB,MAAM,eAAe,IAAI,gBAAgB,IAAI;AAC7C,MAAI,CAAC,aACH,OAAM,IAAI,MACR,4GACD;AAEH,SAAO,IAAI,aAAa;GACtB,eAAe,OAAO;GACtB;GACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;GACjD,aAAa,MAAM;GACnB,YAAY,MAAM;GAClB,GAAG,MAAM;GACV,CAAC;GACF;;AA+BJ,IAAI;;;;AAYJ,eAAsB,QAAQ,KAAa,SAAsD;AAC/F,KAAI,OAAQ,QAAO;EAAE,KAAK;EAAQ,UAAU,EAAE;EAAE;CAChD,MAAM,SAAS,MAAM,UAAU,KAAK,QAAQ;AAC5C,UAAS,OAAO;AAChB,QAAO;;;;;;;;;;AAWT,eAAsB,UAAU,KAAa,SAAsD;CAEjG,MAAM,EAAE,QAAQ,iBAAiB,MADlB,IAAI,cAAc,CACa,gBAAgB,IAAI;CAClE,MAAM,MAAM,IAAI,KAAK;CACrB,MAAM,cAAc,SAAS;CAC7B,MAAM,kBAAkB,SAAS;CAIjC,MAAM,WAAW,IAAI,kBAAkB;AACvC,UAAS,SAAS,aAAa,OAAO,OAAO,WAAW;EACtD,MAAM,MAAM,MAAM,OAAO;EACzB,MAAM,eAAe,IAAI,gBAAgB,IAAI;AAC7C,MAAI,CAAC,aACH,OAAM,IAAI,MACR,4GACD;EAEH,MAAM,EAAE,YAAY,MAAM,OAAO;AACV,UAAQ,OAAO,KAAK;AAC3C,SAAO,IAAI,aAAa;GACtB,eAAe,OAAO;GACtB;GACA,gBAAgB,OAAO,aAA0B;IAC/C,MAAM,aAAa,MAAM,2BACvB,UACA,aACA,iBACA,SACD;IAED,MAAM,WAAW,MAAM,SAAS,eAAe,SAAS;AAExD,QAAI,YAAY;AACd,WAAM,wBAAwB,UAAU,WAAW;KAEnD,MAAM,OAAO,SAAS,WAAW,EAAE;AACnC,UAAK,MAAM,OAAO,OAAO,KAAK,WAAW,UAAU,CACjD,QAAO,KAAK;AAEd,cAAS,UAAU,OAAO,KAAK,KAAK,CAAC,SAAS,IAAI,OAAO;AAEzD,SAAI,OAAO,KAAK,WAAW,aAAa,CAAC,SAAS,EAChD,UAAS,UAAU;MAAE,GAAI,SAAS,WAAW,EAAE;MAAG,GAAG,WAAW;MAAc;;AAGlF,WAAO;;GAET,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;GACjD,aAAa,MAAM;GACnB,YAAY,MAAM;GAClB,GAAG,MAAM;GACV,CAAC;GACF;AAIF,KAAI,eAAe,OAAO,KAAa,WAAmB,cAAsC;AAE9F,WAAS,IAAI;EAGb,MAAM,EAAE,UAAU,eAAe,WAAW,kBAAkB,kBAAkB,IAAI;EACpF,MAAM,aAAa,OAAO,KAAK,cAAc,CAAC,SAAS;EAGvD,MAAM,EAAE,YAAY,MAAM,aAAa,OAAO,GAAG,oBAAoBA,aAAW,EAAE;AAGlF,MAAI,WAEF,iBAAgB,MAAM;GAAE,GADH,gBAAgB,OAAkC,EAAE;GACjC,GAAG;GAAe;EAG5D,MAAM,QAAqB;GACzB;GACA,MAAM;GACN,aAAc,cAA2C;GACzD,MAAO,QAAmB;GAC1B,aAAc,eAA0B;GACxC,SAAS,OAAO,KAAK,gBAAgB,CAAC,SAAS,IAAI,kBAAkB;GACtE;EAED,MAAM,eAAgB,SAA0B;EAChD,IAAI,aAAa,MAAM,2BACrB,OACA,aACA,iBACA,SACD;AAED,MAAI;GACF,MAAM,WAAW,MAAM,SAAS,eAAe,MAAM;AACrD,SAAM,IAAI,MAAM,UAAU,UAAU;WAC7B,YAAY;AAGnB,OAAI,eAAe,QAAQ,aAAa;IAGtC,MAAM,uBAAuB,EAAE,GAAG,iBAAiB;IACnD,MAAM,aAA0B;KAC9B;KACA,MAAM;KACN,aAAc,cAA2C;KACzD,MAAO,QAAmB;KAC1B,aAAc,eAA0B;KACxC,SAAS,OAAO,KAAK,qBAAqB,CAAC,SAAS,IAAI,uBAAuB;KAChF;AACD,iBAAa,MAAM,2BACjB,YACA,aACA,iBACA,UACA,EAAE,cAAc,MAAM,CACvB;IACD,MAAM,gBAAgB,MAAM,SAAS,eAAe,WAAW;AAC/D,UAAM,IAAI,MAAM,eAAe,UAAU;AAEzC,WAAO,OAAO,OAAO,WAAW;SAEhC,OAAM;;AAKV,MAAI,WACF,OAAM,wBAAwB,OAAO,WAAW;AAIlD,MAAI,cAAc,gBAChB,KAAI;GACF,MAAM,WAAmC,EAAE;AAC3C,QAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,cAAc,CAChD,UAAS,OAAO,OAAO;GAGzB,MAAM,WAAW,aAAa,EAAE,GAAG,WAAW,WAAW,GAAG,EAAE;AAC9D,SAAM,gBAAgB,IAAI,eAAe;IAAE,GAAG;IAAU,GAAG;IAAU,CAAC;WAC/D,KAAK;GACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,WAAQ,KAAK,8CAA8C,MAAM;;AAKrE,MAAI;GAEF,MAAM,QAA0B;IAC9B,MAAM;IACN,KAAK,aAAa,gBAAgB,MAAM;IACzC;AACD,OAAI,YAAa,OAAM,cAAc;AACrC,OAAI,WAAY,OAAM,cAAc;AACpC,OAAI,KAAM,OAAM,OAAO;GACvB,MAAM,gBAAgB;IAAE,GAAG;IAAiB,GAAG,YAAY;IAAc;AAEzE,OAAI,WAAY,QAAO,cAAc;AACrC,OAAI,OAAO,KAAK,cAAc,CAAC,SAAS,EAAG,OAAM,UAAU;AAC3D,SAAM,aAAa,KAAK,OAAO,aAAa;WACrC,KAAK;GACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,WAAQ,KAAK,sCAAsC,MAAM;;;AAM7D,KAAI,iBAAiB,OAAO,WAAmB,cAAsC;AACnF,MAAI;AACF,SAAM,eAAe,KAAK,WAAYA,WAAS,SAA0B,OAAU;WAC5E,KAAK;GACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,WAAQ,KAAK,oCAAoC,MAAM;;;AAK3D,KAAI,OAAO,UAAU,YAAY,MAC/B,KAAI;EACF,MAAM,EAAE,gBAAgB,MAAM,OAAO;EAGrC,MAAM,mBAAmB,IAAI,YAC3B,OAAO,UAAU,WAAW,SACxB,EAAE,WAAW,OAAO,SAAS,WAAoB,GACjD,EACE,KAAK,0FACN,CACN;AACD,QAAM,IAAI,MAAM,kBAAkB,qBAAqB;EAGvD,MAAM,mBAAmB,IAAI,aAAa;AAC1C,QAAM,IAAI,MAAM,kBAAkB,qBAAqB;SACjD;AAKV,KAAI,OAAO,OAAO,WAAW,EAC3B,QAAO;EAAE;EAAK,UAAU,EAAE;EAAE;CAG9B,MAAM,QAAQ,OAAO,OAAO;CAC5B,IAAI,YAAY;CAChB,IAAI,cAAc;AAElB,UAAS,aAAa;EAAE;EAAO,WAAW;EAAG,QAAQ;EAAG,CAAC;AAGzD,KAAI,mBAAmB,aAAa,OAAO,EACzC,OAAM,uBAAuB,OAAO,QAAQ,cAAc,gBAAgB;CAG5E,MAAM,UAAU,MAAM,QAAQ,WAC5B,OAAO,OAAO,IAAI,OAAO,UAAU;EACjC,MAAM,WAAW,MAAM,SAAS,eAAe,MAAM;AACrD,QAAM,IAAI,MAAM,UAAU,MAAM,MAAM,EAAE,WAAW,MAAM,aAAa,MAAM,CAAC;AAC7E;AACA,WAAS,aAAa;GAAE;GAAO;GAAW,QAAQ;GAAa,CAAC;AAChE,SAAO,MAAM;GACb,CACH;CAED,MAAM,WAA2B,EAAE;CACnC,MAAM,YAAsB,EAAE;AAE9B,MAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;EACvC,MAAM,SAAS,QAAQ;AACvB,MAAI,OAAO,WAAW,YACpB,WAAU,KAAK,OAAO,MAAM;OACvB;GACL,MAAM,SAAS,OAAO;GACtB,MAAM,MAAM,kBAAkB,QAAQ,OAAO,UAAU,OAAO,OAAO;AACrE,YAAS,KAAK;IAAE,MAAM,OAAO,OAAO,GAAI;IAAM,QAAQ;IAAK,CAAC;AAC5D;AACA;AACA,YAAS,aAAa;IAAE;IAAO;IAAW,QAAQ;IAAa,CAAC;;;AAKpE,KAAI,CAAC,SAAS,cAAc,SAAS,SAAS,GAAG;AAC/C,UAAQ,KAAK,WAAW,UAAU,OAAO,cAAc,SAAS,OAAO,UAAU;AACjF,OAAK,MAAM,KAAK,SACd,SAAQ,KAAK,OAAO,EAAE,KAAK,IAAI,EAAE,SAAS;;AAI9C,KAAI,UAAU,WAAW,KAAK,OAAO,OAAO,SAAS,EACnD,OAAM,IAAI,MAAM,gCAAgC;AAGlD,QAAO;EAAE;EAAK;EAAU;;;;;;;;;;;;AAe1B,SAAgB,kBAAkB,KAGhC;CACA,MAAM,SAAS,SAAS,IAAI;CAC5B,MAAM,YAAoC,EAAE;AAG5C,KAAI,CAAC,OAAO,OAAO,WAAW,MAAM,CAClC,QAAO;EAAE,UAAU;EAAK;EAAW;CAGrC,MAAM,YAAY,OAAO,MAAM;AAC/B,KAAI,CAAC,UAAW,QAAO;EAAE,UAAU;EAAK;EAAW;CAGnD,MAAM,UAAU,MAAM,QAAQ,UAAU,GAAG,YAAY,CAAC,UAAU;AAClE,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,QAAQ,MAAM,QAAQ,IAAI;AAChC,MAAI,QAAQ,EACV,WAAU,MAAM,MAAM,GAAG,MAAM,IAAI,MAAM,MAAM,QAAQ,EAAE;;CAK7D,MAAM,aAAa,IAAI,QAAQ,IAAI;AACnC,KAAI,aAAa,EAAG,QAAO;EAAE,UAAU;EAAK;EAAW;CAEvD,MAAM,WAAW,IAAI,MAAM,aAAa,EAAE;CAC1C,MAAM,SAAS,IAAI,gBAAgB,SAAS;AAC5C,QAAO,OAAO,MAAM;CACpB,MAAM,WAAW,OAAO,UAAU;CAClC,MAAM,OAAO,IAAI,MAAM,GAAG,WAAW;AAGrC,QAAO;EAAE,UAFQ,WAAW,GAAG,KAAK,GAAG,aAAa;EAEjC;EAAW;;;;;;;;;AAUhC,SAAS,2BACP,OACA,UACM;AACN,KAAI,CAAC,UAAU,YAAa;CAC5B,MAAM,EAAE,4BACE,gCAAgC;CAC1C,MAAM,SAAS,SAAS,MAAM,IAAI;CAIlC,IAAI;AACJ,KAAI;AACF,iBAAe,cAAc,SAAS,aAAa,OAAO,KAAK;SACzD;AACN;;AAEF,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,aAAa,CACrD,KAAI,UAAU,QAAW;AACvB,MAAI,CAAC,MAAM,QAAS,OAAM,UAAU,EAAE;AACtC,MAAI,MAAM,QAAQ,SAAS,OACzB,OAAM,QAAQ,OAAO;;;;;;;;;;AAa7B,SAAS,uBACP,OACA,UACM;AACN,KAAI,CAAC,UAAU,YAAa;CAC5B,MAAM,EAAE,UAAU,uCACR,gCAAgC;CAC1C,MAAM,WAAW,yBAAyB,SAAS,YAAY;AAC/D,KAAI,SAAS,WAAW,EAAG;CAG3B,MAAM,SAAS,SAAS,MAAM,IAAI;CAClC,MAAM,EAAE,4BACE,gCAAgC;CAC1C,IAAI;AACJ,KAAI;AACF,iBAAe,cAAc,SAAS,aAAa,OAAO,KAAK;SACzD;AACN,iBAAe,EAAE;;CAInB,MAAM,UAA8C,EAAE,GAAG,cAAc;AACvE,MAAK,MAAM,QAAQ,SACjB,KAAI,CAAC,QAAQ,SAAS,MAAM,UAAU,SAAS,KAC7C,SAAQ,QAAQ,OAAO,MAAM,QAAQ,MAAM;AAK/C,KAAI;EACF,MAAM,SAAS,SAAS,SAAS,aAAa,QAAQ;AACtD,MAAI,WAAW,MAAM,IACnB,OAAM,MAAM;SAER;;;;;;;;;;;AAcV,eAAe,2BACb,OACA,aACA,iBACA,UACA,MAC8E;CAE9E,MAAM,OAAO,MAAM,SAAS,gBAAgB,MAAM,IAAI;CACtD,MAAM,SAAS,MAAM,UAAU;CAC/B,MAAM,eAAe,MAAM;AAE3B,KAAI,CAAC,OAAQ,QAAO;AAIpB,4BAA2B,OAAO,MAAM,SAAS;CAKjD,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAC5C,MAAM,0BAA0B,mBAAmB,OAAO;CAC1D,MAAM,cAAe,OAAe,cAAc,EAAE;CACpD,MAAM,eAAe,OAAO,OAAO,YAAY,CAAC,MAAM,MAAW,MAAM,QAAQ,GAAG,IAAI,CAAC;AACvF,KAAI,wBAAwB,WAAW,KAAK,CAAC,gBAAgB,CAAC,aAAc,QAAO;CAEnF,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAE5C,MAAM,SAAS,MAAM,mBAAmB;EACtC;EACA;EACA;EACA;EACA;EACA,cAAc,MAAM;EACrB,CAAC;AAEF,KAAI,CAAC,QAAQ;EAEX,MAAM,aAAa,OAAO,KAAM,OAAe,cAAc,EAAE,CAAC;EAChE,MAAM,wBAAQ,IAAI,KAAa;AAC/B,MAAI,MAAM,SAAS,OAAW,OAAM,IAAI,OAAO;AAC/C,MAAI,MAAM,UAAU,OAAW,OAAM,IAAI,QAAQ;AACjD,MAAI,MAAM,QACR,MAAK,MAAM,KAAK,OAAO,KAAK,MAAM,QAAQ,CAAE,OAAM,IAAI,EAAE;EAE1D,MAAM,UAAU,WAAW,QAAQ,MAAM,CAAC,MAAM,IAAI,EAAE,CAAC;EACvD,MAAM,YAAY,QAAQ,SAAS,IAAI,QAAQ,KAAK,KAAK,GAAG,WAAW,KAAK,KAAK;AACjF,QAAM,IAAI,MACR,wBAAwB,UAAU,4CACW,MAAM,IAAI,cAAc,MAAM,KAAK,KAAK,QAAQ,KAAK,MAAM,IAAI,EAAE,UAAU,CAAC,KAAK,KAAK,CAAC,IACrI;;AAIH,KAAI,OAAO,KAAK,OAAO,OAAO,CAAC,SAAS,GAAG;AAEzC,MAAI,OAAO,OAAO,UAAU,UAAa,MAAM,UAAU,OACvD,OAAM,QAAQ,OAAO,OAAO,OAAO,MAAM;AAE3C,MAAI,OAAO,OAAO,SAAS,UAAa,MAAM,SAAS,OACrD,OAAM,OAAO,OAAO,OAAO,OAAO,KAAK;EAIzC,MAAMC,SAAO,MAAM,WAAW,EAAE;AAChC,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,OAAO,CACtD,KAAI,QAAQ,WAAW,QAAQ,UAAUA,OAAK,SAAS,OACrD,QAAK,OAAO;AAGhB,MAAI,OAAO,KAAKA,OAAK,CAAC,SAAS,EAC7B,OAAM,UAAUA;;AAMpB,wBAAuB,OAAO,MAAM,SAAS;AAE7C,QAAO,OAAO,YAAY,SAAS;;;;;AAMrC,eAAe,wBACb,OACA,QACe;AACf,KAAI,OAAO,KAAK,OAAO,UAAU,CAAC,SAAS,EACzC,KAAI;EACF,MAAM,EAAE,0BAA0B,MAAM,OAAO;AAE/C,QADc,uBAAuB,CACzB,IAAI,MAAM,KAAK,OAAO,UAAU;UACrC,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,UAAQ,KAAK,0CAA0C,MAAM;;;;;;;;AAUnE,eAAe,uBACb,QACA,eACA,OACe;AACf,MAAK,MAAM,SAAS,OAClB,KAAI;EACF,MAAM,SAAS,MAAM,MAAM,IAAI,MAAM,IAAI;AACzC,MAAI,QAAQ;GACV,MAAM,OAAO,MAAM,WAAW,EAAE;AAChC,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,CAC/C,KAAI,IAAI,WAAW,OAAO,EAAE;AAE1B,QAAI,CAAC,KAAK,IAAK,MAAK,MAAM,EAAE;AAC5B,IAAC,KAAK,IAA+B,IAAI,MAAM,EAAE,IAAI;cAC5C,KAAK,SAAS,OACvB,MAAK,OAAO;AAGhB,OAAI,OAAO,KAAK,KAAK,CAAC,SAAS,EAC7B,OAAM,UAAU;;SAGd;;;;;;;;;;;;;;;;AAwDZ,eAAsB,2BACpB,SACkD;CAClD,MAAM,EAAE,KAAK,WAAW,aAAa,iBAAiB,cAAc,kBAAkB;CAGtF,MAAM,EAAE,UAAU,WAAW,cAAc,kBAAkB,IAAI;CACjE,MAAM,kBAAkB,OAAO,KAAK,UAAU,CAAC,SAAS;CAIxD,MAAM,OAAO,OADI,QAAQ,YAAY,IAAI,kBAAkB,EAC/B,gBAAgB,IAAI;CAChD,IAAI,SAAS,MAAM,UAAU;CAC7B,MAAM,eAAe,MAAM;AAG3B,KAAI,CAAC,UAAU,gBAAgB,OAAO,KAAK,aAAa,CAAC,SAAS,GAAG;EACnE,MAAM,EAAE,qBAAqB,MAAM,OAAO;AAC1C,WAAS,iBAAiB,cAAc,iBAAiB,EAAE,CAAC;;CAI9D,MAAM,sBAA+D;AACnE,MAAI,CAAC,gBAAiB,QAAO;AAC7B,SAAO;GACL,WAAW;GACX,cAAc,EAAE;GAChB,WAAW,EAAE;GACb,oBAAoB,YAAY;IAC9B,MAAM,UAAkC,EAAE;AAC1C,SAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,UAAU,CAChD,SAAQ,OAAO,SAAS;AAE1B,QAAI,gBACF,KAAI;AACF,WAAM,gBAAgB,IAAI,WAAW,QAAQ;aACtC,KAAK;KACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,aAAQ,KAAK,8CAA8C,MAAM;;;GAIvE,iBAAiB,EAAE;GACnB;GACD;;AAGH,KAAI,CAAC,OAAQ,QAAO,eAAe;CAEnC,MAAM,aAAc,OAAe;AACnC,KAAI,CAAC,cAAc,OAAO,KAAK,WAAW,CAAC,WAAW,EAAG,QAAO,eAAe;AAI/E,KAAI,iBAAiB,cAAc,SAAS,GAAG;EAC7C,MAAM,cAAc,EAAE,GAAG,YAAY;EACrC,IAAI,UAAU;AACd,OAAK,MAAM,SAAS,cAClB,KAAI,YAAY,QAAQ;AAEtB,eAAY,SAAS;IAAE,GAAG,YAAY;IAAQ,WAAW;IAAM;AAC/D,aAAU;aACD,eAAe,WAAW,QAAW;GAE9C,MAAM,QAAQ,aAAa;AAC3B,eAAY,SAAS;IACnB,MACE,OAAO,UAAU,WACb,WACA,OAAO,UAAU,YACf,YACA;IACR,WAAW;IACZ;AACD,aAAU;;AAGd,MAAI,QACF,UAAS;GAAE,GAAG;GAAQ,YAAY;GAAa;;CAQnD,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAC5C,MAAM,0BAA0B,mBAAmB,OAAO;CAC1D,MAAM,cAAe,OAAe;CACpC,MAAM,eAAe,OAAO,OAAO,YAAY,CAAC,MAAM,MAAW,MAAM,QAAQ,GAAG,IAAI,CAAC;AACvF,KAAI,wBAAwB,WAAW,KAAK,CAAC,gBAAgB,CAAC,aAC5D,QAAO,eAAe;CAGxB,MAAM,QAAqB;EAAE;EAAK,MAAM;EAAW;AAGnD,KAAI,gBAAgB,OAAO,KAAK,aAAa,CAAC,SAAS,EACrD,OAAM,UAAU;EAAE,GAAI,MAAM,WAAW,EAAE;EAAG,GAAG;EAAc;AAI/D,4BAA2B,OAAO,MAAM,SAAS;CAGjD,MAAM,EAAE,uBAAuB,MAAM,OAAO;CAE5C,MAAM,SAAS,MAAM,mBAAmB;EACtC;EACA;EACA;EACA;EACA;EACA,cAAc,QAAQ;EACvB,CAAC;AAEF,KAAI,CAAC,QAAQ;EAEX,MAAM,aAAa,OAAO,KAAK,WAAW;AAC1C,QAAM,IAAI,MACR,wBAAwB,WAAW,KAAK,KAAK,CAAC,4CACD,IAAI,cAAc,UAAU,KAAK,WAAW,KAAK,MAAM,IAAI,EAAE,UAAU,CAAC,KAAK,KAAK,CAAC,IACjI;;CAIH,MAAM,oBAAoB,IAAI,IAAI,wBAAwB;CAI1D,MAAM,gBAAwC,EAAE;AAChD,MAAK,MAAM,SAAS,yBAAyB;EAC3C,MAAM,MAAM,OAAO,OAAO;AAC1B,MAAI,QAAQ,OAAW;AACvB,MAAI,UAAU,SAAS,OAAO,QAAQ,YAAY,QAAQ,KACxD,MAAK,MAAM,CAAC,QAAQ,WAAW,OAAO,QAAQ,IAA8B,CAC1E,eAAc,OAAO,YAAY,OAAO,OAAO;MAGjD,eAAc,SAAS,OAAO,IAAI;;CAKtC,MAAM,eAAwC,OAAO,YACjD,OAAO,eACP,eACE,OAAO,YAAY,OAAO,QAAQ,aAAa,CAAC,QAAQ,CAAC,OAAO,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,GAC3F,EAAE;CAIR,MAAM,qBAAqB,YAAY;EACrC,MAAM,UAAU,EAAE,GAAG,eAAe;AAEpC,OAAK,MAAM,CAAC,KAAK,QAAQ,OAAO,QAAQ,UAAU,CAChD,SAAQ,OAAO,SAAS;AAE1B,MAAI,OAAO,KAAK,QAAQ,CAAC,SAAS,KAAK,gBACrC,KAAI;AAEF,SAAM,gBAAgB,IAAI,WAAW,QAAQ;WACtC,KAAK;GACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,WAAQ,KAAK,8CAA8C,MAAM;;;AAKvE,QAAO;EACL,WAAW,OAAO;EAClB;EACA,WAAW,OAAO;EAClB;EACA,iBAAiB;EACjB,WAAW,kBAAkB,YAAY;EAC1C;;;;;;;;;;;;;AAgBH,eAAsB,YACpB,KACA,WACA,SACe;CACf,MAAM,QAAqB;EACzB;EACA,MAAM;EACN,SAAS,WAAW,OAAO,KAAK,QAAQ,CAAC,SAAS,IAAI,UAAU;EACjE;CAED,MAAM,WAAW,IAAI,kBAAkB;AACvC,0BAAyB,SAAS;CAClC,IAAI;AACJ,KAAI;AACF,aAAW,MAAM,SAAS,eAAe,MAAM;UACxC,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,QAAM,IAAI,MAAM,kDAAkD,MAAM;;AAI1E,KAAI;EACF,MAAM,EAAE,eAAQ,MAAM,OAAO;AAE7B,QADgB,IAAIC,OAAK,CACX,MAAM,UAAU,UAAU;UACjC,KAAK;EACZ,MAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AAC5D,QAAM,IAAI,MACR,0DAA0D,IAAI,WAClD,IAAI,mCACjB;WACO;AAER,MAAI,OAAQ,SAAiB,UAAU,WACrC,KAAI;AACF,SAAO,SAAiB,OAAO;UACzB"}
|
package/dist/config/loader.cjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
2
|
const require_env = require('./env.cjs');
|
|
3
3
|
const require_schema = require('./schema.cjs');
|
|
4
|
-
let node_path = require("node:path");
|
|
5
4
|
let node_fs_promises = require("node:fs/promises");
|
|
6
5
|
let node_os = require("node:os");
|
|
6
|
+
let node_path = require("node:path");
|
|
7
7
|
let smol_toml = require("smol-toml");
|
|
8
8
|
|
|
9
9
|
//#region src/config/loader.ts
|
|
@@ -40,13 +40,25 @@ var ConfigLoader = class {
|
|
|
40
40
|
* @throws Error on invalid config, TOML parse error, or duplicate mount paths
|
|
41
41
|
*/
|
|
42
42
|
async load(cwd = process.cwd()) {
|
|
43
|
+
return (await this.loadWithSources(cwd)).config;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Load and merge configuration, also returning the config directory for each mount.
|
|
47
|
+
*
|
|
48
|
+
* mountSources maps "namespace:path" → config directory path (dirname of config file).
|
|
49
|
+
* Used by credential store to scope credentials per config location.
|
|
50
|
+
*/
|
|
51
|
+
async loadWithSources(cwd = process.cwd()) {
|
|
43
52
|
const configPaths = await this.getConfigPaths(cwd);
|
|
44
|
-
const
|
|
53
|
+
const entries = [];
|
|
45
54
|
for (const configPath of configPaths) {
|
|
46
55
|
const config = await this.loadSingleConfig(configPath);
|
|
47
|
-
|
|
56
|
+
entries.push({
|
|
57
|
+
config,
|
|
58
|
+
configDir: (0, node_path.dirname)(configPath)
|
|
59
|
+
});
|
|
48
60
|
}
|
|
49
|
-
return this.
|
|
61
|
+
return this.mergeConfigsWithSources(entries);
|
|
50
62
|
}
|
|
51
63
|
/**
|
|
52
64
|
* Get paths to all existing config files
|
|
@@ -128,7 +140,7 @@ var ConfigLoader = class {
|
|
|
128
140
|
}
|
|
129
141
|
const result = require_schema.ConfigSchema.safeParse(resolved);
|
|
130
142
|
if (!result.success) {
|
|
131
|
-
const errors = result.error.
|
|
143
|
+
const errors = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
132
144
|
throw new Error(`Invalid config at ${configPath}: ${errors}`);
|
|
133
145
|
}
|
|
134
146
|
return result.data;
|
|
@@ -141,14 +153,15 @@ var ConfigLoader = class {
|
|
|
141
153
|
return `${namespace ?? ""}:${path}`;
|
|
142
154
|
}
|
|
143
155
|
/**
|
|
144
|
-
* Merge
|
|
145
|
-
*
|
|
156
|
+
* Merge configs with source tracking.
|
|
157
|
+
* Returns merged config plus a map of mount key → config directory.
|
|
146
158
|
*/
|
|
147
|
-
|
|
159
|
+
mergeConfigsWithSources(entries) {
|
|
148
160
|
const mountIndexByKey = /* @__PURE__ */ new Map();
|
|
149
161
|
const allMounts = [];
|
|
162
|
+
const mountSources = /* @__PURE__ */ new Map();
|
|
150
163
|
let mergedServe;
|
|
151
|
-
for (const config of
|
|
164
|
+
for (const { config, configDir } of entries) {
|
|
152
165
|
for (const mount of config.mounts) {
|
|
153
166
|
const key = this.makeNamespacePathKey(mount.namespace, mount.path);
|
|
154
167
|
const existingIndex = mountIndexByKey.get(key);
|
|
@@ -157,6 +170,7 @@ var ConfigLoader = class {
|
|
|
157
170
|
mountIndexByKey.set(key, allMounts.length);
|
|
158
171
|
allMounts.push(mount);
|
|
159
172
|
}
|
|
173
|
+
mountSources.set(key, configDir);
|
|
160
174
|
}
|
|
161
175
|
if (config.serve) mergedServe = mergedServe ? {
|
|
162
176
|
...mergedServe,
|
|
@@ -164,8 +178,11 @@ var ConfigLoader = class {
|
|
|
164
178
|
} : config.serve;
|
|
165
179
|
}
|
|
166
180
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
181
|
+
config: {
|
|
182
|
+
mounts: allMounts,
|
|
183
|
+
serve: mergedServe
|
|
184
|
+
},
|
|
185
|
+
mountSources
|
|
169
186
|
};
|
|
170
187
|
}
|
|
171
188
|
/**
|
package/dist/config/loader.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { resolveEnvVarsInObject } from "./env.mjs";
|
|
2
2
|
import { ConfigSchema } from "./schema.mjs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
3
|
import { access, readFile } from "node:fs/promises";
|
|
5
4
|
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
6
|
import { parse } from "smol-toml";
|
|
7
7
|
|
|
8
8
|
//#region src/config/loader.ts
|
|
@@ -39,13 +39,25 @@ var ConfigLoader = class {
|
|
|
39
39
|
* @throws Error on invalid config, TOML parse error, or duplicate mount paths
|
|
40
40
|
*/
|
|
41
41
|
async load(cwd = process.cwd()) {
|
|
42
|
+
return (await this.loadWithSources(cwd)).config;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Load and merge configuration, also returning the config directory for each mount.
|
|
46
|
+
*
|
|
47
|
+
* mountSources maps "namespace:path" → config directory path (dirname of config file).
|
|
48
|
+
* Used by credential store to scope credentials per config location.
|
|
49
|
+
*/
|
|
50
|
+
async loadWithSources(cwd = process.cwd()) {
|
|
42
51
|
const configPaths = await this.getConfigPaths(cwd);
|
|
43
|
-
const
|
|
52
|
+
const entries = [];
|
|
44
53
|
for (const configPath of configPaths) {
|
|
45
54
|
const config = await this.loadSingleConfig(configPath);
|
|
46
|
-
|
|
55
|
+
entries.push({
|
|
56
|
+
config,
|
|
57
|
+
configDir: dirname(configPath)
|
|
58
|
+
});
|
|
47
59
|
}
|
|
48
|
-
return this.
|
|
60
|
+
return this.mergeConfigsWithSources(entries);
|
|
49
61
|
}
|
|
50
62
|
/**
|
|
51
63
|
* Get paths to all existing config files
|
|
@@ -127,7 +139,7 @@ var ConfigLoader = class {
|
|
|
127
139
|
}
|
|
128
140
|
const result = ConfigSchema.safeParse(resolved);
|
|
129
141
|
if (!result.success) {
|
|
130
|
-
const errors = result.error.
|
|
142
|
+
const errors = result.error.issues.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
131
143
|
throw new Error(`Invalid config at ${configPath}: ${errors}`);
|
|
132
144
|
}
|
|
133
145
|
return result.data;
|
|
@@ -140,14 +152,15 @@ var ConfigLoader = class {
|
|
|
140
152
|
return `${namespace ?? ""}:${path}`;
|
|
141
153
|
}
|
|
142
154
|
/**
|
|
143
|
-
* Merge
|
|
144
|
-
*
|
|
155
|
+
* Merge configs with source tracking.
|
|
156
|
+
* Returns merged config plus a map of mount key → config directory.
|
|
145
157
|
*/
|
|
146
|
-
|
|
158
|
+
mergeConfigsWithSources(entries) {
|
|
147
159
|
const mountIndexByKey = /* @__PURE__ */ new Map();
|
|
148
160
|
const allMounts = [];
|
|
161
|
+
const mountSources = /* @__PURE__ */ new Map();
|
|
149
162
|
let mergedServe;
|
|
150
|
-
for (const config of
|
|
163
|
+
for (const { config, configDir } of entries) {
|
|
151
164
|
for (const mount of config.mounts) {
|
|
152
165
|
const key = this.makeNamespacePathKey(mount.namespace, mount.path);
|
|
153
166
|
const existingIndex = mountIndexByKey.get(key);
|
|
@@ -156,6 +169,7 @@ var ConfigLoader = class {
|
|
|
156
169
|
mountIndexByKey.set(key, allMounts.length);
|
|
157
170
|
allMounts.push(mount);
|
|
158
171
|
}
|
|
172
|
+
mountSources.set(key, configDir);
|
|
159
173
|
}
|
|
160
174
|
if (config.serve) mergedServe = mergedServe ? {
|
|
161
175
|
...mergedServe,
|
|
@@ -163,8 +177,11 @@ var ConfigLoader = class {
|
|
|
163
177
|
} : config.serve;
|
|
164
178
|
}
|
|
165
179
|
return {
|
|
166
|
-
|
|
167
|
-
|
|
180
|
+
config: {
|
|
181
|
+
mounts: allMounts,
|
|
182
|
+
serve: mergedServe
|
|
183
|
+
},
|
|
184
|
+
mountSources
|
|
168
185
|
};
|
|
169
186
|
}
|
|
170
187
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.mjs","names":[],"sources":["../../src/config/loader.ts"],"sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { parse } from \"smol-toml\";\nimport { resolveEnvVarsInObject } from \"./env.js\";\nimport { type AFSConfig, ConfigSchema, type MountConfig, type ServeConfig } from \"./schema.js\";\n\nexport const CONFIG_DIR_NAME = \".afs-config\";\nexport const CONFIG_FILE_NAME = \"config.toml\";\n\nexport interface ConfigLoaderOptions {\n /** Custom path to user-level config directory (for testing) */\n userConfigDir?: string;\n}\n\n/**\n * Environment variable to override user config directory.\n * Useful for testing to isolate from real user config.\n */\nexport const AFS_USER_CONFIG_DIR_ENV = \"AFS_USER_CONFIG_DIR\";\n\n/**\n * Loads and merges AFS configuration from multiple layers\n *\n * Layer priority (lowest to highest):\n * 1. User-level: ~/.afs-config/config.toml\n * 2. All intermediate directories from project root to cwd\n *\n * Example: if cwd is /project/packages/cli, configs are merged from:\n * ~/.afs-config/config.toml (user)\n * /project/.afs-config/config.toml (project root, has .git)\n * /project/packages/.afs-config/config.toml (intermediate)\n * /project/packages/cli/.afs-config/config.toml (cwd)\n */\nexport class ConfigLoader {\n private userConfigDir: string;\n\n constructor(options: ConfigLoaderOptions = {}) {\n // Priority: options > environment variable > default (~/.afs-config)\n this.userConfigDir =\n options.userConfigDir ??\n process.env[AFS_USER_CONFIG_DIR_ENV] ??\n join(homedir(), CONFIG_DIR_NAME);\n }\n\n /**\n * Load and merge configuration from all layers\n *\n * @param cwd - Current working directory (defaults to process.cwd())\n * @returns Merged configuration\n * @throws Error on invalid config, TOML parse error, or duplicate mount paths\n */\n async load(cwd: string = process.cwd()): Promise<AFSConfig> {\n const configPaths = await this.getConfigPaths(cwd);\n const configs: AFSConfig[] = [];\n\n for (const configPath of configPaths) {\n const config = await this.loadSingleConfig(configPath);\n configs.push(config);\n }\n\n return this.mergeConfigs(configs);\n }\n\n /**\n * Get paths to all existing config files\n *\n * Collects configs from:\n * 1. User-level: ~/.afs-config/config.toml\n * 2. Project root (or topmost .afs-config dir) to cwd: all .afs-config/config.toml files\n */\n async getConfigPaths(cwd: string = process.cwd()): Promise<string[]> {\n const paths: string[] = [];\n\n // 1. User-level config\n const userConfigPath = join(this.userConfigDir, CONFIG_FILE_NAME);\n if (await this.fileExists(userConfigPath)) {\n paths.push(userConfigPath);\n }\n\n // 2. Find project root (look for .git going up)\n const projectRoot = await this.findProjectRoot(cwd);\n\n // 3. Determine start directory\n // If project root found, use it; otherwise find topmost .afs-config directory\n const startDir = projectRoot ?? (await this.findTopmostAfsDir(cwd)) ?? cwd;\n\n // 4. Collect all config files from start to cwd\n // Exclude user config directory to avoid loading it twice\n const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd, this.userConfigDir);\n paths.push(...intermediatePaths);\n\n return paths;\n }\n\n /**\n * Find the topmost directory containing .afs-config from startDir going up\n */\n private async findTopmostAfsDir(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n let topmostAfsDir: string | null = null;\n\n while (true) {\n if (await this.fileExists(join(currentDir, CONFIG_DIR_NAME))) {\n topmostAfsDir = currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n break;\n }\n currentDir = parentDir;\n }\n\n return topmostAfsDir;\n }\n\n /**\n * Collect all config files from startDir to endDir (inclusive)\n * Returns paths in order from startDir to endDir (parent to child)\n *\n * @param excludeConfigDir - Optional config directory to exclude (to avoid duplicates)\n */\n private async collectConfigsFromTo(\n startDir: string,\n endDir: string,\n excludeConfigDir?: string,\n ): Promise<string[]> {\n const paths: string[] = [];\n\n // Build list of directories from startDir to endDir\n const dirs: string[] = [];\n let current = endDir;\n\n while (true) {\n dirs.unshift(current); // prepend to maintain parent-to-child order\n\n if (current === startDir) {\n break;\n }\n\n const parent = dirname(current);\n if (parent === current) {\n // Reached filesystem root without finding startDir\n // This shouldn't happen if startDir is an ancestor of endDir\n break;\n }\n current = parent;\n }\n\n // Check each directory for config file\n for (const dir of dirs) {\n const configDir = join(dir, CONFIG_DIR_NAME);\n // Skip if this is the excluded config directory (e.g., user config already loaded)\n if (excludeConfigDir && configDir === excludeConfigDir) {\n continue;\n }\n const configPath = join(configDir, CONFIG_FILE_NAME);\n if (await this.fileExists(configPath)) {\n paths.push(configPath);\n }\n }\n\n return paths;\n }\n\n /**\n * Load a single config file\n */\n private async loadSingleConfig(configPath: string): Promise<AFSConfig> {\n const content = await readFile(configPath, \"utf-8\");\n\n let parsed: unknown;\n try {\n parsed = parse(content);\n } catch (error) {\n throw new Error(\n `Failed to parse TOML config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Resolve environment variables with friendly error messages\n let resolved: unknown;\n try {\n resolved = resolveEnvVarsInObject(parsed);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Extract variable name from error message like \"Environment variable GITHUB_TOKEN is not defined\"\n const match = message.match(/Environment variable (\\w+) is not defined/);\n if (match) {\n const varName = match[1];\n throw new Error(\n `Missing environment variable ${varName} in ${configPath}.\\n` +\n ` Set it in your shell: export ${varName}=your_value\\n` +\n ` Or add to .env file: ${varName}=your_value`,\n );\n }\n throw new Error(`Failed to resolve environment variables in ${configPath}: ${message}`);\n }\n\n // Validate against schema\n const result = ConfigSchema.safeParse(resolved);\n if (!result.success) {\n const errors = result.error.errors.map((e) => `${e.path.join(\".\")}: ${e.message}`).join(\"; \");\n throw new Error(`Invalid config at ${configPath}: ${errors}`);\n }\n\n return result.data;\n }\n\n /**\n * Create a composite key for namespace+path duplicate detection\n * Uses empty string for undefined namespace (default namespace)\n */\n private makeNamespacePathKey(namespace: string | undefined, path: string): string {\n return `${namespace ?? \"\"}:${path}`;\n }\n\n /**\n * Merge multiple configs with child configs overriding parent configs\n * For both mounts and serve, later (more specific) configs override earlier ones\n */\n private mergeConfigs(configs: AFSConfig[]): AFSConfig {\n // key = \"namespace:path\", value = index in allMounts array\n const mountIndexByKey = new Map<string, number>();\n const allMounts: MountConfig[] = [];\n let mergedServe: ServeConfig | undefined;\n\n for (const config of configs) {\n // Merge mounts - later configs override earlier ones with same namespace+path\n for (const mount of config.mounts) {\n const key = this.makeNamespacePathKey(mount.namespace, mount.path);\n const existingIndex = mountIndexByKey.get(key);\n if (existingIndex !== undefined) {\n // Override existing mount with the new one (child overrides parent)\n allMounts[existingIndex] = mount;\n } else {\n // Add new mount\n mountIndexByKey.set(key, allMounts.length);\n allMounts.push(mount);\n }\n }\n\n // Merge serve config (later configs override earlier ones)\n if (config.serve) {\n mergedServe = mergedServe ? { ...mergedServe, ...config.serve } : config.serve;\n }\n }\n\n return { mounts: allMounts, serve: mergedServe };\n }\n\n /**\n * Find project root by looking for .git\n * Note: Only .git is used as project root marker, not .afs-config,\n * because .afs-config can exist at multiple levels for hierarchical config\n */\n private async findProjectRoot(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n\n while (true) {\n // Check for .git directory\n if (await this.fileExists(join(currentDir, \".git\"))) {\n return currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n return null;\n }\n currentDir = parentDir;\n }\n }\n\n /**\n * Check if a file or directory exists\n */\n private async fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// Default singleton instance\nexport const configLoader = new ConfigLoader();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"loader.mjs","names":[],"sources":["../../src/config/loader.ts"],"sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { parse } from \"smol-toml\";\nimport { resolveEnvVarsInObject } from \"./env.js\";\nimport { type AFSConfig, ConfigSchema, type MountConfig, type ServeConfig } from \"./schema.js\";\n\nexport const CONFIG_DIR_NAME = \".afs-config\";\nexport const CONFIG_FILE_NAME = \"config.toml\";\n\nexport interface ConfigLoaderOptions {\n /** Custom path to user-level config directory (for testing) */\n userConfigDir?: string;\n}\n\nexport interface LoadWithSourcesResult {\n config: AFSConfig;\n /** Map from \"namespace:path\" → config directory that defines this mount */\n mountSources: Map<string, string>;\n}\n\n/**\n * Environment variable to override user config directory.\n * Useful for testing to isolate from real user config.\n */\nexport const AFS_USER_CONFIG_DIR_ENV = \"AFS_USER_CONFIG_DIR\";\n\n/**\n * Loads and merges AFS configuration from multiple layers\n *\n * Layer priority (lowest to highest):\n * 1. User-level: ~/.afs-config/config.toml\n * 2. All intermediate directories from project root to cwd\n *\n * Example: if cwd is /project/packages/cli, configs are merged from:\n * ~/.afs-config/config.toml (user)\n * /project/.afs-config/config.toml (project root, has .git)\n * /project/packages/.afs-config/config.toml (intermediate)\n * /project/packages/cli/.afs-config/config.toml (cwd)\n */\nexport class ConfigLoader {\n private userConfigDir: string;\n\n constructor(options: ConfigLoaderOptions = {}) {\n // Priority: options > environment variable > default (~/.afs-config)\n this.userConfigDir =\n options.userConfigDir ??\n process.env[AFS_USER_CONFIG_DIR_ENV] ??\n join(homedir(), CONFIG_DIR_NAME);\n }\n\n /**\n * Load and merge configuration from all layers\n *\n * @param cwd - Current working directory (defaults to process.cwd())\n * @returns Merged configuration\n * @throws Error on invalid config, TOML parse error, or duplicate mount paths\n */\n async load(cwd: string = process.cwd()): Promise<AFSConfig> {\n const result = await this.loadWithSources(cwd);\n return result.config;\n }\n\n /**\n * Load and merge configuration, also returning the config directory for each mount.\n *\n * mountSources maps \"namespace:path\" → config directory path (dirname of config file).\n * Used by credential store to scope credentials per config location.\n */\n async loadWithSources(cwd: string = process.cwd()): Promise<LoadWithSourcesResult> {\n const configPaths = await this.getConfigPaths(cwd);\n const entries: { config: AFSConfig; configDir: string }[] = [];\n\n for (const configPath of configPaths) {\n const config = await this.loadSingleConfig(configPath);\n entries.push({ config, configDir: dirname(configPath) });\n }\n\n return this.mergeConfigsWithSources(entries);\n }\n\n /**\n * Get paths to all existing config files\n *\n * Collects configs from:\n * 1. User-level: ~/.afs-config/config.toml\n * 2. Project root (or topmost .afs-config dir) to cwd: all .afs-config/config.toml files\n */\n async getConfigPaths(cwd: string = process.cwd()): Promise<string[]> {\n const paths: string[] = [];\n\n // 1. User-level config\n const userConfigPath = join(this.userConfigDir, CONFIG_FILE_NAME);\n if (await this.fileExists(userConfigPath)) {\n paths.push(userConfigPath);\n }\n\n // 2. Find project root (look for .git going up)\n const projectRoot = await this.findProjectRoot(cwd);\n\n // 3. Determine start directory\n // If project root found, use it; otherwise find topmost .afs-config directory\n const startDir = projectRoot ?? (await this.findTopmostAfsDir(cwd)) ?? cwd;\n\n // 4. Collect all config files from start to cwd\n // Exclude user config directory to avoid loading it twice\n const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd, this.userConfigDir);\n paths.push(...intermediatePaths);\n\n return paths;\n }\n\n /**\n * Find the topmost directory containing .afs-config from startDir going up\n */\n private async findTopmostAfsDir(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n let topmostAfsDir: string | null = null;\n\n while (true) {\n if (await this.fileExists(join(currentDir, CONFIG_DIR_NAME))) {\n topmostAfsDir = currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n break;\n }\n currentDir = parentDir;\n }\n\n return topmostAfsDir;\n }\n\n /**\n * Collect all config files from startDir to endDir (inclusive)\n * Returns paths in order from startDir to endDir (parent to child)\n *\n * @param excludeConfigDir - Optional config directory to exclude (to avoid duplicates)\n */\n private async collectConfigsFromTo(\n startDir: string,\n endDir: string,\n excludeConfigDir?: string,\n ): Promise<string[]> {\n const paths: string[] = [];\n\n // Build list of directories from startDir to endDir\n const dirs: string[] = [];\n let current = endDir;\n\n while (true) {\n dirs.unshift(current); // prepend to maintain parent-to-child order\n\n if (current === startDir) {\n break;\n }\n\n const parent = dirname(current);\n if (parent === current) {\n // Reached filesystem root without finding startDir\n // This shouldn't happen if startDir is an ancestor of endDir\n break;\n }\n current = parent;\n }\n\n // Check each directory for config file\n for (const dir of dirs) {\n const configDir = join(dir, CONFIG_DIR_NAME);\n // Skip if this is the excluded config directory (e.g., user config already loaded)\n if (excludeConfigDir && configDir === excludeConfigDir) {\n continue;\n }\n const configPath = join(configDir, CONFIG_FILE_NAME);\n if (await this.fileExists(configPath)) {\n paths.push(configPath);\n }\n }\n\n return paths;\n }\n\n /**\n * Load a single config file\n */\n private async loadSingleConfig(configPath: string): Promise<AFSConfig> {\n const content = await readFile(configPath, \"utf-8\");\n\n let parsed: unknown;\n try {\n parsed = parse(content);\n } catch (error) {\n throw new Error(\n `Failed to parse TOML config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Resolve environment variables with friendly error messages\n let resolved: unknown;\n try {\n resolved = resolveEnvVarsInObject(parsed);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n // Extract variable name from error message like \"Environment variable GITHUB_TOKEN is not defined\"\n const match = message.match(/Environment variable (\\w+) is not defined/);\n if (match) {\n const varName = match[1];\n throw new Error(\n `Missing environment variable ${varName} in ${configPath}.\\n` +\n ` Set it in your shell: export ${varName}=your_value\\n` +\n ` Or add to .env file: ${varName}=your_value`,\n );\n }\n throw new Error(`Failed to resolve environment variables in ${configPath}: ${message}`);\n }\n\n // Validate against schema\n const result = ConfigSchema.safeParse(resolved);\n if (!result.success) {\n const errors = result.error.issues.map((e) => `${e.path.join(\".\")}: ${e.message}`).join(\"; \");\n throw new Error(`Invalid config at ${configPath}: ${errors}`);\n }\n\n return result.data;\n }\n\n /**\n * Create a composite key for namespace+path duplicate detection\n * Uses empty string for undefined namespace (default namespace)\n */\n private makeNamespacePathKey(namespace: string | undefined, path: string): string {\n return `${namespace ?? \"\"}:${path}`;\n }\n\n /**\n * Merge configs with source tracking.\n * Returns merged config plus a map of mount key → config directory.\n */\n private mergeConfigsWithSources(\n entries: { config: AFSConfig; configDir: string }[],\n ): LoadWithSourcesResult {\n const mountIndexByKey = new Map<string, number>();\n const allMounts: MountConfig[] = [];\n const mountSources = new Map<string, string>();\n let mergedServe: ServeConfig | undefined;\n\n for (const { config, configDir } of entries) {\n for (const mount of config.mounts) {\n const key = this.makeNamespacePathKey(mount.namespace, mount.path);\n const existingIndex = mountIndexByKey.get(key);\n if (existingIndex !== undefined) {\n allMounts[existingIndex] = mount;\n } else {\n mountIndexByKey.set(key, allMounts.length);\n allMounts.push(mount);\n }\n // Track source (child overrides parent)\n mountSources.set(key, configDir);\n }\n\n if (config.serve) {\n mergedServe = mergedServe ? { ...mergedServe, ...config.serve } : config.serve;\n }\n }\n\n return {\n config: { mounts: allMounts, serve: mergedServe },\n mountSources,\n };\n }\n\n /**\n * Find project root by looking for .git\n * Note: Only .git is used as project root marker, not .afs-config,\n * because .afs-config can exist at multiple levels for hierarchical config\n */\n async findProjectRoot(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n\n while (true) {\n // Check for .git directory\n if (await this.fileExists(join(currentDir, \".git\"))) {\n return currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n return null;\n }\n currentDir = parentDir;\n }\n }\n\n /**\n * Check if a file or directory exists\n */\n private async fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// Default singleton instance\nexport const configLoader = new ConfigLoader();\n"],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
2
|
const require_schema = require('./schema.cjs');
|
|
3
3
|
const require_loader = require('./loader.cjs');
|
|
4
|
-
let node_path = require("node:path");
|
|
5
4
|
let node_fs_promises = require("node:fs/promises");
|
|
5
|
+
let node_os = require("node:os");
|
|
6
|
+
let node_path = require("node:path");
|
|
6
7
|
let smol_toml = require("smol-toml");
|
|
7
8
|
|
|
8
9
|
//#region src/config/mount-commands.ts
|
|
@@ -48,71 +49,118 @@ async function configMountListCommand(cwd) {
|
|
|
48
49
|
return { mounts: (await new require_loader.ConfigLoader().load(cwd)).mounts };
|
|
49
50
|
}
|
|
50
51
|
/**
|
|
51
|
-
*
|
|
52
|
+
* Remove a mount from config
|
|
52
53
|
*/
|
|
53
|
-
async function
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
async function mountRemoveCommand(cwd, path) {
|
|
55
|
+
const configPath = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME, require_loader.CONFIG_FILE_NAME);
|
|
56
|
+
try {
|
|
57
|
+
const config = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8"));
|
|
58
|
+
const mounts = config.mounts ?? [];
|
|
59
|
+
const index = mounts.findIndex((m) => m.path === path);
|
|
60
|
+
if (index === -1) return {
|
|
61
|
+
success: false,
|
|
62
|
+
message: `Mount path "${path}" not found`
|
|
63
|
+
};
|
|
64
|
+
mounts.splice(index, 1);
|
|
65
|
+
config.mounts = mounts;
|
|
66
|
+
await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(config), "utf-8");
|
|
67
|
+
return { success: true };
|
|
68
|
+
} catch {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
message: `Mount path "${path}" not found`
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Resolve a PersistScope to the config directory path.
|
|
77
|
+
*
|
|
78
|
+
* - "cwd" → <cwd>/.afs-config/
|
|
79
|
+
* - "project" → <projectRoot>/.afs-config/ (falls back to cwd if no .git found)
|
|
80
|
+
* - "user" → ~/.afs-config/
|
|
81
|
+
*/
|
|
82
|
+
async function resolveScopeDir(cwd, scope) {
|
|
83
|
+
if (scope === "user") return (0, node_path.join)((0, node_os.homedir)(), require_loader.CONFIG_DIR_NAME);
|
|
84
|
+
if (scope === "project") {
|
|
85
|
+
const projectRoot = await new require_loader.ConfigLoader().findProjectRoot(cwd);
|
|
86
|
+
if (projectRoot) return (0, node_path.join)(projectRoot, require_loader.CONFIG_DIR_NAME);
|
|
87
|
+
return (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME);
|
|
88
|
+
}
|
|
89
|
+
return (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Persist a mount entry to config.toml at the given scope.
|
|
93
|
+
*
|
|
94
|
+
* Uses upsert semantics: if a mount with the same path already exists,
|
|
95
|
+
* it is replaced. Otherwise the new entry is appended.
|
|
96
|
+
*/
|
|
97
|
+
async function persistMount(cwd, entry, scope = "cwd") {
|
|
98
|
+
const configDir = await resolveScopeDir(cwd, scope);
|
|
75
99
|
const configPath = (0, node_path.join)(configDir, require_loader.CONFIG_FILE_NAME);
|
|
76
100
|
const config = { mounts: [] };
|
|
77
101
|
try {
|
|
78
|
-
|
|
102
|
+
const parsed = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8"));
|
|
103
|
+
config.mounts = parsed.mounts ?? [];
|
|
104
|
+
for (const key of Object.keys(parsed)) if (key !== "mounts") config[key] = parsed[key];
|
|
79
105
|
} catch {}
|
|
80
|
-
const
|
|
81
|
-
if (config.mounts
|
|
82
|
-
|
|
83
|
-
message: `Mount path "${normalizedPath}" already exists`
|
|
84
|
-
};
|
|
85
|
-
config.mounts.push(newMount);
|
|
106
|
+
const existingIndex = config.mounts.findIndex((m) => m.path === entry.path);
|
|
107
|
+
if (existingIndex >= 0) config.mounts[existingIndex] = entry;
|
|
108
|
+
else config.mounts.push(entry);
|
|
86
109
|
try {
|
|
87
110
|
await (0, node_fs_promises.mkdir)(configDir, { recursive: true });
|
|
88
111
|
} catch {}
|
|
89
112
|
await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(config), "utf-8");
|
|
90
113
|
return {
|
|
91
114
|
success: true,
|
|
92
|
-
|
|
115
|
+
configPath
|
|
93
116
|
};
|
|
94
117
|
}
|
|
95
118
|
/**
|
|
96
|
-
* Remove a mount from config
|
|
119
|
+
* Remove a mount entry from config.toml.
|
|
120
|
+
*
|
|
121
|
+
* If `scope` is provided, only searches the specific config file for that scope.
|
|
122
|
+
* If `scope` is undefined, searches all config files (cwd, project, user) and
|
|
123
|
+
* removes from the first one that contains the mount path.
|
|
97
124
|
*/
|
|
98
|
-
async function
|
|
99
|
-
|
|
125
|
+
async function unpersistMount(cwd, mountPath, scope) {
|
|
126
|
+
if (scope) return removeFromConfigFile(cwd, mountPath, scope);
|
|
127
|
+
for (const s of [
|
|
128
|
+
"cwd",
|
|
129
|
+
"project",
|
|
130
|
+
"user"
|
|
131
|
+
]) {
|
|
132
|
+
const result = await removeFromConfigFile(cwd, mountPath, s);
|
|
133
|
+
if (result.success) return result;
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
success: false,
|
|
137
|
+
message: `Mount path "${mountPath}" not found in any config`
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Remove a mount from a specific config file.
|
|
142
|
+
*/
|
|
143
|
+
async function removeFromConfigFile(cwd, mountPath, scope) {
|
|
144
|
+
const configPath = (0, node_path.join)(await resolveScopeDir(cwd, scope), require_loader.CONFIG_FILE_NAME);
|
|
100
145
|
try {
|
|
101
|
-
const
|
|
102
|
-
const mounts =
|
|
103
|
-
const index = mounts.findIndex((m) => m.path ===
|
|
146
|
+
const parsed = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8"));
|
|
147
|
+
const mounts = parsed.mounts ?? [];
|
|
148
|
+
const index = mounts.findIndex((m) => m.path === mountPath);
|
|
104
149
|
if (index === -1) return {
|
|
105
150
|
success: false,
|
|
106
|
-
message: `Mount path "${
|
|
151
|
+
message: `Mount path "${mountPath}" not found in ${configPath}`
|
|
107
152
|
};
|
|
108
153
|
mounts.splice(index, 1);
|
|
109
|
-
|
|
110
|
-
await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(
|
|
111
|
-
return {
|
|
154
|
+
parsed.mounts = mounts;
|
|
155
|
+
await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(parsed), "utf-8");
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
configPath
|
|
159
|
+
};
|
|
112
160
|
} catch {
|
|
113
161
|
return {
|
|
114
162
|
success: false,
|
|
115
|
-
message: `
|
|
163
|
+
message: `Config file not found or unreadable: ${configPath}`
|
|
116
164
|
};
|
|
117
165
|
}
|
|
118
166
|
}
|
|
@@ -127,7 +175,7 @@ async function mountValidateCommand(cwd) {
|
|
|
127
175
|
for (const mount of mounts) {
|
|
128
176
|
const validation = require_schema.MountSchema.safeParse(mount);
|
|
129
177
|
if (!validation.success) {
|
|
130
|
-
for (const err of validation.error.
|
|
178
|
+
for (const err of validation.error.issues) errors.push(`Mount "${mount.path}": ${err.message}`);
|
|
131
179
|
continue;
|
|
132
180
|
}
|
|
133
181
|
if (mount.uri.startsWith("fs://")) {
|
|
@@ -153,6 +201,8 @@ async function mountValidateCommand(cwd) {
|
|
|
153
201
|
|
|
154
202
|
//#endregion
|
|
155
203
|
exports.configMountListCommand = configMountListCommand;
|
|
156
|
-
exports.mountAddCommand = mountAddCommand;
|
|
157
204
|
exports.mountRemoveCommand = mountRemoveCommand;
|
|
158
|
-
exports.mountValidateCommand = mountValidateCommand;
|
|
205
|
+
exports.mountValidateCommand = mountValidateCommand;
|
|
206
|
+
exports.persistMount = persistMount;
|
|
207
|
+
exports.resolveUriPath = resolveUriPath;
|
|
208
|
+
exports.unpersistMount = unpersistMount;
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
//#region src/config/mount-commands.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Mount Configuration Commands
|
|
4
|
-
*
|
|
5
|
-
* CLI-specific commands for managing mount configuration files.
|
|
6
|
-
* These operate on afs.toml config files, not the AFS instance directly.
|
|
7
|
-
*/
|
|
8
2
|
interface ConfigMountEntry {
|
|
9
3
|
path: string;
|
|
10
4
|
uri: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount-commands.d.cts","names":[],"sources":["../../src/config/mount-commands.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"mount-commands.d.cts","names":[],"sources":["../../src/config/mount-commands.ts"],"mappings":";UAwBiB,gBAAA;EACf,IAAA;EACA,GAAA;EACA,SAAA;EACA,WAAA;EACA,WAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,GAAU,MAAA;AAAA"}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
//#region src/config/mount-commands.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Mount Configuration Commands
|
|
4
|
-
*
|
|
5
|
-
* CLI-specific commands for managing mount configuration files.
|
|
6
|
-
* These operate on afs.toml config files, not the AFS instance directly.
|
|
7
|
-
*/
|
|
8
2
|
interface ConfigMountEntry {
|
|
9
3
|
path: string;
|
|
10
4
|
uri: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mount-commands.d.mts","names":[],"sources":["../../src/config/mount-commands.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"mount-commands.d.mts","names":[],"sources":["../../src/config/mount-commands.ts"],"mappings":";UAwBiB,gBAAA;EACf,IAAA;EACA,GAAA;EACA,SAAA;EACA,WAAA;EACA,WAAA;EACA,IAAA;EACA,KAAA;EACA,OAAA,GAAU,MAAA;AAAA"}
|