@aigne/afs-cli 1.11.0-beta.11 → 1.11.0-beta.13
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/cli.cjs +3 -2
- package/dist/cli.mjs +3 -2
- package/dist/cli.mjs.map +1 -1
- package/dist/config/afs-loader.cjs +36 -315
- package/dist/config/afs-loader.d.cts.map +1 -1
- package/dist/config/afs-loader.d.mts +2 -1
- package/dist/config/afs-loader.d.mts.map +1 -1
- package/dist/config/afs-loader.mjs +28 -307
- package/dist/config/afs-loader.mjs.map +1 -1
- package/dist/config/credential-helpers.cjs +303 -0
- package/dist/config/credential-helpers.d.mts +2 -0
- package/dist/config/credential-helpers.mjs +300 -0
- package/dist/config/credential-helpers.mjs.map +1 -0
- package/dist/config/loader.cjs +3 -1
- package/dist/config/loader.mjs +3 -2
- package/dist/config/loader.mjs.map +1 -1
- package/dist/config/program-install.cjs +450 -0
- package/dist/config/program-install.d.mts +1 -0
- package/dist/config/program-install.mjs +444 -0
- package/dist/config/program-install.mjs.map +1 -0
- package/dist/core/commands/connect.cjs +53 -0
- package/dist/core/commands/connect.d.mts +2 -0
- package/dist/core/commands/connect.mjs +55 -0
- package/dist/core/commands/connect.mjs.map +1 -0
- package/dist/core/commands/daemon.cjs +211 -0
- package/dist/core/commands/daemon.d.mts +2 -0
- package/dist/core/commands/daemon.mjs +212 -0
- package/dist/core/commands/daemon.mjs.map +1 -0
- package/dist/core/commands/explain.cjs +3 -1
- package/dist/core/commands/explain.mjs +3 -1
- package/dist/core/commands/explain.mjs.map +1 -1
- package/dist/core/commands/explore.cjs +47 -12
- package/dist/core/commands/explore.mjs +47 -12
- package/dist/core/commands/explore.mjs.map +1 -1
- package/dist/core/commands/gen-agent-md.cjs +126 -0
- package/dist/core/commands/gen-agent-md.d.mts +2 -0
- package/dist/core/commands/gen-agent-md.mjs +125 -0
- package/dist/core/commands/gen-agent-md.mjs.map +1 -0
- package/dist/core/commands/index.cjs +13 -1
- package/dist/core/commands/index.d.cts.map +1 -1
- package/dist/core/commands/index.d.mts +6 -0
- package/dist/core/commands/index.d.mts.map +1 -1
- package/dist/core/commands/index.mjs +13 -1
- package/dist/core/commands/index.mjs.map +1 -1
- package/dist/core/commands/install.cjs +139 -0
- package/dist/core/commands/install.d.mts +2 -0
- package/dist/core/commands/install.mjs +140 -0
- package/dist/core/commands/install.mjs.map +1 -0
- package/dist/core/commands/ls.cjs +14 -2
- package/dist/core/commands/ls.d.cts +2 -0
- package/dist/core/commands/ls.d.cts.map +1 -1
- package/dist/core/commands/ls.d.mts +2 -0
- package/dist/core/commands/ls.d.mts.map +1 -1
- package/dist/core/commands/ls.mjs +14 -2
- package/dist/core/commands/ls.mjs.map +1 -1
- package/dist/core/commands/mcp-bridge.cjs +201 -0
- package/dist/core/commands/mcp-bridge.d.mts +2 -0
- package/dist/core/commands/mcp-bridge.mjs +201 -0
- package/dist/core/commands/mcp-bridge.mjs.map +1 -0
- package/dist/core/commands/read.cjs +20 -7
- package/dist/core/commands/read.d.cts +2 -0
- package/dist/core/commands/read.d.cts.map +1 -1
- package/dist/core/commands/read.d.mts +2 -0
- package/dist/core/commands/read.d.mts.map +1 -1
- package/dist/core/commands/read.mjs +20 -7
- package/dist/core/commands/read.mjs.map +1 -1
- package/dist/core/commands/search.cjs +5 -1
- package/dist/core/commands/search.mjs +5 -1
- package/dist/core/commands/search.mjs.map +1 -1
- package/dist/core/commands/stat.mjs.map +1 -1
- package/dist/core/commands/types.d.cts +2 -0
- package/dist/core/commands/types.d.cts.map +1 -1
- package/dist/core/commands/types.d.mts +2 -0
- package/dist/core/commands/types.d.mts.map +1 -1
- package/dist/core/commands/types.mjs.map +1 -1
- package/dist/core/commands/vault.cjs +289 -0
- package/dist/core/commands/vault.d.mts +2 -0
- package/dist/core/commands/vault.mjs +289 -0
- package/dist/core/commands/vault.mjs.map +1 -0
- package/dist/core/commands/write.cjs +19 -6
- package/dist/core/commands/write.d.cts +2 -1
- package/dist/core/commands/write.d.cts.map +1 -1
- package/dist/core/commands/write.d.mts +2 -1
- package/dist/core/commands/write.d.mts.map +1 -1
- package/dist/core/commands/write.mjs +19 -6
- package/dist/core/commands/write.mjs.map +1 -1
- package/dist/core/executor/index.cjs +95 -19
- package/dist/core/executor/index.d.cts +4 -0
- package/dist/core/executor/index.d.cts.map +1 -1
- package/dist/core/executor/index.d.mts +4 -0
- package/dist/core/executor/index.d.mts.map +1 -1
- package/dist/core/executor/index.mjs +95 -19
- package/dist/core/executor/index.mjs.map +1 -1
- package/dist/core/formatters/index.d.mts +1 -0
- package/dist/core/formatters/install.cjs +40 -0
- package/dist/core/formatters/install.d.mts +1 -0
- package/dist/core/formatters/install.mjs +36 -0
- package/dist/core/formatters/install.mjs.map +1 -0
- package/dist/core/formatters/vault.cjs +36 -0
- package/dist/core/formatters/vault.mjs +32 -0
- package/dist/core/formatters/vault.mjs.map +1 -0
- package/dist/credential/auth-server.cjs +22 -4
- package/dist/credential/auth-server.mjs +22 -4
- package/dist/credential/auth-server.mjs.map +1 -1
- package/dist/credential/index.d.mts +2 -1
- package/dist/credential/mcp-auth-context.cjs +21 -5
- package/dist/credential/mcp-auth-context.mjs +21 -5
- package/dist/credential/mcp-auth-context.mjs.map +1 -1
- package/dist/credential/resolver.cjs +11 -3
- package/dist/credential/resolver.mjs +11 -3
- package/dist/credential/resolver.mjs.map +1 -1
- package/dist/credential/vault-store.d.mts +1 -0
- package/dist/daemon/config-manager.cjs +279 -0
- package/dist/daemon/config-manager.mjs +279 -0
- package/dist/daemon/config-manager.mjs.map +1 -0
- package/dist/daemon/manager.cjs +164 -0
- package/dist/daemon/manager.mjs +157 -0
- package/dist/daemon/manager.mjs.map +1 -0
- package/dist/daemon/server.cjs +220 -0
- package/dist/daemon/server.mjs +220 -0
- package/dist/daemon/server.mjs.map +1 -0
- package/dist/mcp/http-transport.cjs +14 -1
- package/dist/mcp/http-transport.mjs +14 -1
- package/dist/mcp/http-transport.mjs.map +1 -1
- package/dist/mcp/server.cjs +4 -2
- package/dist/mcp/server.mjs +4 -2
- package/dist/mcp/server.mjs.map +1 -1
- package/dist/mcp/tools.cjs +62 -12
- package/dist/mcp/tools.mjs +62 -12
- package/dist/mcp/tools.mjs.map +1 -1
- package/dist/program/daemon-integration.cjs +46 -0
- package/dist/program/daemon-integration.mjs +45 -0
- package/dist/program/daemon-integration.mjs.map +1 -0
- package/dist/program/program-manager.cjs +166 -0
- package/dist/program/program-manager.mjs +166 -0
- package/dist/program/program-manager.mjs.map +1 -0
- package/dist/program/trigger-scanner.cjs +148 -0
- package/dist/program/trigger-scanner.mjs +148 -0
- package/dist/program/trigger-scanner.mjs.map +1 -0
- package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +11 -0
- package/dist/providers/vault/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs.map +1 -0
- package/dist/providers/vault/dist/encrypted-file.cjs +158 -0
- package/dist/providers/vault/dist/encrypted-file.mjs +153 -0
- package/dist/providers/vault/dist/encrypted-file.mjs.map +1 -0
- package/dist/providers/vault/dist/index.cjs +405 -0
- package/dist/providers/vault/dist/index.mjs +400 -0
- package/dist/providers/vault/dist/index.mjs.map +1 -0
- package/dist/providers/vault/dist/key-resolver.cjs +181 -0
- package/dist/providers/vault/dist/key-resolver.mjs +180 -0
- package/dist/providers/vault/dist/key-resolver.mjs.map +1 -0
- package/dist/repl.cjs +109 -14
- package/dist/repl.d.cts.map +1 -1
- package/dist/repl.d.mts.map +1 -1
- package/dist/repl.mjs +109 -14
- package/dist/repl.mjs.map +1 -1
- package/package.json +27 -20
package/dist/config/loader.mjs
CHANGED
|
@@ -181,7 +181,8 @@ var ConfigLoader = class {
|
|
|
181
181
|
mounts: allMounts,
|
|
182
182
|
serve: mergedServe
|
|
183
183
|
},
|
|
184
|
-
mountSources
|
|
184
|
+
mountSources,
|
|
185
|
+
configDirs: entries.map((e) => e.configDir)
|
|
185
186
|
};
|
|
186
187
|
}
|
|
187
188
|
/**
|
|
@@ -213,5 +214,5 @@ var ConfigLoader = class {
|
|
|
213
214
|
const configLoader = new ConfigLoader();
|
|
214
215
|
|
|
215
216
|
//#endregion
|
|
216
|
-
export { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader };
|
|
217
|
+
export { AFS_USER_CONFIG_DIR_ENV, CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader };
|
|
217
218
|
//# sourceMappingURL=loader.mjs.map
|
|
@@ -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\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
|
+
{"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 /** Config directories in order from outermost to innermost */\n configDirs: 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 configDirs: entries.map((e) => e.configDir),\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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"}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_loader = require('./loader.cjs');
|
|
3
|
+
const require_mount_commands = require('./mount-commands.cjs');
|
|
4
|
+
let node_child_process = require("node:child_process");
|
|
5
|
+
let node_fs_promises = require("node:fs/promises");
|
|
6
|
+
let node_os = require("node:os");
|
|
7
|
+
let node_path = require("node:path");
|
|
8
|
+
let _aigne_afs = require("@aigne/afs");
|
|
9
|
+
|
|
10
|
+
//#region src/config/program-install.ts
|
|
11
|
+
/**
|
|
12
|
+
* Program Installation
|
|
13
|
+
*
|
|
14
|
+
* Business logic for installing, listing, and removing AFS programs.
|
|
15
|
+
* Programs are installed to ~/.afs-config/programs/<id>/ and mounted
|
|
16
|
+
* at /programs/<id> in the CWD config (same as `mount add`).
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Get the user config directory.
|
|
20
|
+
*/
|
|
21
|
+
function getUserConfigDir(override) {
|
|
22
|
+
return override ?? process.env[require_loader.AFS_USER_CONFIG_DIR_ENV] ?? (0, node_path.join)((0, node_os.homedir)(), require_loader.CONFIG_DIR_NAME);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Detect the source type from a source string.
|
|
26
|
+
*/
|
|
27
|
+
function detectSourceType(source) {
|
|
28
|
+
if (source.endsWith(".zip")) return "zip";
|
|
29
|
+
if (source.startsWith("https://github.com/") || source.startsWith("http://github.com/") || source.startsWith("github.com/")) return "github";
|
|
30
|
+
return "local";
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse a GitHub URL into owner, repo, ref, and optional subdirectory.
|
|
34
|
+
*/
|
|
35
|
+
function parseGitHubURL(url) {
|
|
36
|
+
let normalized = url;
|
|
37
|
+
if (normalized.startsWith("github.com/")) normalized = `https://${normalized}`;
|
|
38
|
+
const match = normalized.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)\/(.+))?$/);
|
|
39
|
+
if (!match) return { cloneUrl: normalized.replace(/\/+$/, "") };
|
|
40
|
+
const [, owner, repo, _ref, subdir] = match;
|
|
41
|
+
return {
|
|
42
|
+
cloneUrl: `https://github.com/${owner}/${repo}.git`,
|
|
43
|
+
subdir: subdir || void 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Resolve a source to a local directory path containing program.yaml.
|
|
48
|
+
* For github/zip sources, downloads/extracts to a temp directory.
|
|
49
|
+
*
|
|
50
|
+
* Returns [resolvedPath, tempDir?] — tempDir is set if caller should clean up.
|
|
51
|
+
*/
|
|
52
|
+
async function resolveSource(source, type, cwd) {
|
|
53
|
+
switch (type) {
|
|
54
|
+
case "local": {
|
|
55
|
+
const dirPath = (0, node_path.isAbsolute)(source) ? source : (0, node_path.resolve)(cwd, source);
|
|
56
|
+
try {
|
|
57
|
+
await (0, node_fs_promises.readdir)(dirPath);
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`Source directory does not exist: ${dirPath}`);
|
|
60
|
+
}
|
|
61
|
+
return [dirPath, void 0];
|
|
62
|
+
}
|
|
63
|
+
case "github": {
|
|
64
|
+
const { cloneUrl, subdir } = parseGitHubURL(source);
|
|
65
|
+
const tempDir = (0, node_path.join)((0, node_os.tmpdir)(), `afs-install-${Date.now()}`);
|
|
66
|
+
await (0, node_fs_promises.mkdir)(tempDir, { recursive: true });
|
|
67
|
+
try {
|
|
68
|
+
(0, node_child_process.execSync)(`git clone --depth 1 ${cloneUrl} ${(0, node_path.join)(tempDir, "repo")}`, {
|
|
69
|
+
stdio: "pipe",
|
|
70
|
+
timeout: 6e4
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
await (0, node_fs_promises.rm)(tempDir, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
force: true
|
|
76
|
+
});
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
throw new Error(`Failed to clone GitHub repository: ${msg}`);
|
|
79
|
+
}
|
|
80
|
+
return [subdir ? (0, node_path.join)(tempDir, "repo", subdir) : (0, node_path.join)(tempDir, "repo"), tempDir];
|
|
81
|
+
}
|
|
82
|
+
case "zip": {
|
|
83
|
+
const zipPath = (0, node_path.isAbsolute)(source) ? source : (0, node_path.resolve)(cwd, source);
|
|
84
|
+
const tempDir = (0, node_path.join)((0, node_os.tmpdir)(), `afs-install-${Date.now()}`);
|
|
85
|
+
const extractDir = (0, node_path.join)(tempDir, "extracted");
|
|
86
|
+
await (0, node_fs_promises.mkdir)(extractDir, { recursive: true });
|
|
87
|
+
try {
|
|
88
|
+
const lines = (0, node_child_process.execSync)(`unzip -l "${zipPath}"`, {
|
|
89
|
+
encoding: "utf-8",
|
|
90
|
+
stdio: [
|
|
91
|
+
"pipe",
|
|
92
|
+
"pipe",
|
|
93
|
+
"pipe"
|
|
94
|
+
]
|
|
95
|
+
}).split("\n");
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.trim();
|
|
98
|
+
if (!trimmed || trimmed.startsWith("Archive:") || trimmed.startsWith("Length") || trimmed.startsWith("---")) continue;
|
|
99
|
+
const parts = trimmed.split(/\s+/);
|
|
100
|
+
if (parts.length >= 4) {
|
|
101
|
+
const name = parts.slice(3).join(" ");
|
|
102
|
+
if (name.includes("..") || name.startsWith("/")) {
|
|
103
|
+
await (0, node_fs_promises.rm)(tempDir, {
|
|
104
|
+
recursive: true,
|
|
105
|
+
force: true
|
|
106
|
+
});
|
|
107
|
+
throw new Error(`Zip file contains unsafe path: ${name}. Refusing to extract.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err instanceof Error && err.message.includes("unsafe path")) throw err;
|
|
113
|
+
await (0, node_fs_promises.rm)(tempDir, {
|
|
114
|
+
recursive: true,
|
|
115
|
+
force: true
|
|
116
|
+
});
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
throw new Error(`Failed to read zip file: ${msg}`);
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
(0, node_child_process.execSync)(`unzip -o "${zipPath}" -d "${extractDir}"`, {
|
|
122
|
+
stdio: "pipe",
|
|
123
|
+
timeout: 3e4
|
|
124
|
+
});
|
|
125
|
+
} catch (err) {
|
|
126
|
+
await (0, node_fs_promises.rm)(tempDir, {
|
|
127
|
+
recursive: true,
|
|
128
|
+
force: true
|
|
129
|
+
});
|
|
130
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
131
|
+
throw new Error(`Failed to extract zip file: ${msg}`);
|
|
132
|
+
}
|
|
133
|
+
const entries = await (0, node_fs_promises.readdir)(extractDir);
|
|
134
|
+
if (entries.includes("program.yaml")) return [extractDir, tempDir];
|
|
135
|
+
if (entries.length === 1) {
|
|
136
|
+
const subPath = (0, node_path.join)(extractDir, entries[0]);
|
|
137
|
+
try {
|
|
138
|
+
if ((await (0, node_fs_promises.readdir)(subPath)).includes("program.yaml")) return [subPath, tempDir];
|
|
139
|
+
} catch {}
|
|
140
|
+
}
|
|
141
|
+
await (0, node_fs_promises.rm)(tempDir, {
|
|
142
|
+
recursive: true,
|
|
143
|
+
force: true
|
|
144
|
+
});
|
|
145
|
+
throw new Error("No program.yaml found in zip archive");
|
|
146
|
+
}
|
|
147
|
+
default: throw new Error(`Unsupported source type: ${type}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Validate a directory contains a valid program.yaml.
|
|
152
|
+
* Returns the parsed manifest.
|
|
153
|
+
*/
|
|
154
|
+
async function validateProgramDir(dirPath) {
|
|
155
|
+
const yamlPath = (0, node_path.join)(dirPath, "program.yaml");
|
|
156
|
+
let content;
|
|
157
|
+
try {
|
|
158
|
+
content = await (0, node_fs_promises.readFile)(yamlPath, "utf-8");
|
|
159
|
+
} catch {
|
|
160
|
+
throw new Error(`No program.yaml found in ${dirPath}`);
|
|
161
|
+
}
|
|
162
|
+
return (0, _aigne_afs.parseProgramManifest)(content);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Install a program from a source (local dir, GitHub URL, or zip file).
|
|
166
|
+
*/
|
|
167
|
+
async function installProgram(source, options) {
|
|
168
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
169
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
170
|
+
const [resolvedPath, tempDir] = await resolveSource(source, detectSourceType(source), cwd);
|
|
171
|
+
try {
|
|
172
|
+
const manifest = await validateProgramDir(resolvedPath);
|
|
173
|
+
const programsDir = (0, node_path.join)(userConfigDir, "programs");
|
|
174
|
+
const installPath = (0, node_path.join)(programsDir, manifest.id);
|
|
175
|
+
const mountPath = `/programs/${manifest.id}`;
|
|
176
|
+
await (0, node_fs_promises.mkdir)(programsDir, { recursive: true });
|
|
177
|
+
const tempInstallPath = `${installPath}.installing`;
|
|
178
|
+
await (0, node_fs_promises.rm)(tempInstallPath, {
|
|
179
|
+
recursive: true,
|
|
180
|
+
force: true
|
|
181
|
+
});
|
|
182
|
+
await (0, node_fs_promises.cp)(resolvedPath, tempInstallPath, { recursive: true });
|
|
183
|
+
await (0, node_fs_promises.rm)(installPath, {
|
|
184
|
+
recursive: true,
|
|
185
|
+
force: true
|
|
186
|
+
});
|
|
187
|
+
await (0, node_fs_promises.rename)(tempInstallPath, installPath);
|
|
188
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(userConfigDir, "data", manifest.id), { recursive: true });
|
|
189
|
+
await require_mount_commands.persistMount(cwd, {
|
|
190
|
+
path: mountPath,
|
|
191
|
+
uri: `fs://${installPath}`
|
|
192
|
+
});
|
|
193
|
+
const { notifyDaemonReload } = await Promise.resolve().then(() => require("../program/daemon-integration.cjs"));
|
|
194
|
+
const { getDaemonStatus } = await Promise.resolve().then(() => require("../daemon/manager.cjs"));
|
|
195
|
+
await notifyDaemonReload({ getDaemonStatus });
|
|
196
|
+
return {
|
|
197
|
+
success: true,
|
|
198
|
+
programId: manifest.id,
|
|
199
|
+
programName: manifest.name,
|
|
200
|
+
installPath,
|
|
201
|
+
mountPath
|
|
202
|
+
};
|
|
203
|
+
} finally {
|
|
204
|
+
if (tempDir) await (0, node_fs_promises.rm)(tempDir, {
|
|
205
|
+
recursive: true,
|
|
206
|
+
force: true
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* List installed programs by scanning ~/.afs-config/programs/.
|
|
212
|
+
*/
|
|
213
|
+
async function listInstalledPrograms(options) {
|
|
214
|
+
const programsDir = (0, node_path.join)(getUserConfigDir(options?.userConfigDir), "programs");
|
|
215
|
+
let entries;
|
|
216
|
+
try {
|
|
217
|
+
entries = await (0, node_fs_promises.readdir)(programsDir);
|
|
218
|
+
} catch {
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
const programs = [];
|
|
222
|
+
for (const entry of entries) {
|
|
223
|
+
const dirPath = (0, node_path.join)(programsDir, entry);
|
|
224
|
+
try {
|
|
225
|
+
const manifest = await validateProgramDir(dirPath);
|
|
226
|
+
programs.push({
|
|
227
|
+
id: manifest.id,
|
|
228
|
+
name: manifest.name,
|
|
229
|
+
entrypoint: manifest.entrypoint,
|
|
230
|
+
installPath: dirPath,
|
|
231
|
+
mountPath: `/programs/${manifest.id}`
|
|
232
|
+
});
|
|
233
|
+
} catch {}
|
|
234
|
+
}
|
|
235
|
+
return programs;
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Remove an installed program.
|
|
239
|
+
*/
|
|
240
|
+
async function removeProgram(programId, options) {
|
|
241
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
242
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
243
|
+
const installPath = (0, node_path.join)(userConfigDir, "programs", programId);
|
|
244
|
+
try {
|
|
245
|
+
await (0, node_fs_promises.readdir)(installPath);
|
|
246
|
+
} catch {
|
|
247
|
+
throw new Error(`Program "${programId}" is not installed`);
|
|
248
|
+
}
|
|
249
|
+
await (0, node_fs_promises.rm)(installPath, {
|
|
250
|
+
recursive: true,
|
|
251
|
+
force: true
|
|
252
|
+
});
|
|
253
|
+
await require_mount_commands.unpersistMount(cwd, `/programs/${programId}`);
|
|
254
|
+
let purgedData = false;
|
|
255
|
+
if (options?.purge) {
|
|
256
|
+
await (0, node_fs_promises.rm)((0, node_path.join)(userConfigDir, "data", programId), {
|
|
257
|
+
recursive: true,
|
|
258
|
+
force: true
|
|
259
|
+
});
|
|
260
|
+
purgedData = true;
|
|
261
|
+
}
|
|
262
|
+
const { notifyDaemonReload } = await Promise.resolve().then(() => require("../program/daemon-integration.cjs"));
|
|
263
|
+
const { getDaemonStatus } = await Promise.resolve().then(() => require("../daemon/manager.cjs"));
|
|
264
|
+
await notifyDaemonReload({ getDaemonStatus });
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
programId,
|
|
268
|
+
purgedData
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Read per-program mount overrides from mounts.toml.
|
|
273
|
+
*
|
|
274
|
+
* Location: ~/.afs-config/data/{programId}/mounts.toml
|
|
275
|
+
*
|
|
276
|
+
* Returns [] if the file is missing or invalid.
|
|
277
|
+
*/
|
|
278
|
+
async function readProgramMountOverrides(programId, options) {
|
|
279
|
+
const mountsPath = (0, node_path.join)(getUserConfigDir(options?.userConfigDir), "data", programId, "mounts.toml");
|
|
280
|
+
let content;
|
|
281
|
+
try {
|
|
282
|
+
content = await (0, node_fs_promises.readFile)(mountsPath, "utf-8");
|
|
283
|
+
} catch {
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const { parse } = await import("smol-toml");
|
|
288
|
+
const mounts = parse(content).mounts;
|
|
289
|
+
if (!Array.isArray(mounts)) return [];
|
|
290
|
+
const result = [];
|
|
291
|
+
for (const entry of mounts) {
|
|
292
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
293
|
+
const { path, uri, options: options$1 } = entry;
|
|
294
|
+
if (typeof path !== "string" || typeof uri !== "string") continue;
|
|
295
|
+
const override = {
|
|
296
|
+
target: path,
|
|
297
|
+
uri
|
|
298
|
+
};
|
|
299
|
+
if (options$1 && typeof options$1 === "object" && !Array.isArray(options$1)) {
|
|
300
|
+
const opts = options$1;
|
|
301
|
+
if (Object.keys(opts).length > 0) override.options = opts;
|
|
302
|
+
}
|
|
303
|
+
result.push(override);
|
|
304
|
+
}
|
|
305
|
+
return result;
|
|
306
|
+
} catch {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Interactive configure flow for a program's mount dependencies.
|
|
312
|
+
*
|
|
313
|
+
* Uses the standard credential resolution flow (browser form / OAuth) for each
|
|
314
|
+
* mount. Mounts with already-stored credentials are skipped silently.
|
|
315
|
+
* After resolution, URI templates are rebuilt with collected values and
|
|
316
|
+
* credentials are persisted under the final URI.
|
|
317
|
+
*/
|
|
318
|
+
async function configureProgramMounts(programId, options) {
|
|
319
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
320
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
321
|
+
const manifest = await validateProgramDir((0, node_path.join)(userConfigDir, "programs", programId));
|
|
322
|
+
const existingOverrides = await readProgramMountOverrides(programId, { userConfigDir });
|
|
323
|
+
const existingByTarget = new Map(existingOverrides.map((o) => [o.target, o]));
|
|
324
|
+
const { ProviderRegistry } = await import("@aigne/afs");
|
|
325
|
+
const { resolveCredentialsForMount } = await Promise.resolve().then(() => require("./credential-helpers.cjs"));
|
|
326
|
+
const { createCLIAuthContext } = await Promise.resolve().then(() => require("../credential/cli-auth-context.cjs"));
|
|
327
|
+
const { createCredentialStore } = await Promise.resolve().then(() => require("../credential/store.cjs"));
|
|
328
|
+
const registry = new ProviderRegistry();
|
|
329
|
+
const authContext = createCLIAuthContext();
|
|
330
|
+
const credentialStore = createCredentialStore();
|
|
331
|
+
const configuredMounts = [];
|
|
332
|
+
const { getTemplateVariableNames } = await import("@aigne/afs/utils/uri-template");
|
|
333
|
+
for (const mountDecl of manifest.mounts) {
|
|
334
|
+
const existing = existingByTarget.get(mountDecl.target);
|
|
335
|
+
const startUri = existing?.uri || mountDecl.uri;
|
|
336
|
+
try {
|
|
337
|
+
const forceThis = options?.force === true || options?.force === "" || options?.force === mountDecl.target;
|
|
338
|
+
const result = await resolveCredentialsForMount({
|
|
339
|
+
cwd,
|
|
340
|
+
uri: startUri,
|
|
341
|
+
mountPath: mountDecl.target,
|
|
342
|
+
authContext,
|
|
343
|
+
credentialStore,
|
|
344
|
+
registry,
|
|
345
|
+
forceCollect: forceThis || void 0,
|
|
346
|
+
extraOptions: existing?.options
|
|
347
|
+
});
|
|
348
|
+
if (result) {
|
|
349
|
+
await result.persistCredentials();
|
|
350
|
+
const finalUri = result.resolvedUri || result.configUri || startUri;
|
|
351
|
+
const info = await registry.getProviderInfo(startUri);
|
|
352
|
+
const templateVars = new Set(info?.manifest?.uriTemplate ? getTemplateVariableNames(info.manifest.uriTemplate) : []);
|
|
353
|
+
const filteredOptions = {};
|
|
354
|
+
if (existing?.options) {
|
|
355
|
+
for (const [k, v] of Object.entries(existing.options)) if (!templateVars.has(k)) filteredOptions[k] = v;
|
|
356
|
+
}
|
|
357
|
+
if (result.collected) {
|
|
358
|
+
for (const [k, v] of Object.entries(result.nonSensitive)) if (!templateVars.has(k)) filteredOptions[k] = v;
|
|
359
|
+
}
|
|
360
|
+
const options$1 = Object.keys(filteredOptions).length > 0 ? filteredOptions : void 0;
|
|
361
|
+
configuredMounts.push({
|
|
362
|
+
target: mountDecl.target,
|
|
363
|
+
uri: finalUri,
|
|
364
|
+
options: options$1
|
|
365
|
+
});
|
|
366
|
+
} else configuredMounts.push({
|
|
367
|
+
target: mountDecl.target,
|
|
368
|
+
uri: startUri,
|
|
369
|
+
options: existing?.options
|
|
370
|
+
});
|
|
371
|
+
} catch (err) {
|
|
372
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
373
|
+
console.warn(` Skipping ${mountDecl.target}: ${msg}`);
|
|
374
|
+
configuredMounts.push({
|
|
375
|
+
target: mountDecl.target,
|
|
376
|
+
uri: startUri,
|
|
377
|
+
options: existing?.options
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const changedMounts = configuredMounts.filter((m) => {
|
|
382
|
+
const decl = manifest.mounts.find((d) => d.target === m.target);
|
|
383
|
+
return decl && (m.uri !== decl.uri || m.options && Object.keys(m.options).length > 0);
|
|
384
|
+
});
|
|
385
|
+
const dataDir = (0, node_path.join)(userConfigDir, "data", programId);
|
|
386
|
+
const mountsTomlPath = (0, node_path.join)(dataDir, "mounts.toml");
|
|
387
|
+
if (changedMounts.length > 0) {
|
|
388
|
+
await (0, node_fs_promises.mkdir)(dataDir, { recursive: true });
|
|
389
|
+
const { stringify } = await import("smol-toml");
|
|
390
|
+
await (0, node_fs_promises.writeFile)(mountsTomlPath, stringify({ mounts: changedMounts.map((m) => ({
|
|
391
|
+
path: m.target,
|
|
392
|
+
uri: m.uri,
|
|
393
|
+
...m.options && Object.keys(m.options).length > 0 ? { options: m.options } : {}
|
|
394
|
+
})) }), "utf-8");
|
|
395
|
+
} else try {
|
|
396
|
+
await (0, node_fs_promises.rm)(mountsTomlPath);
|
|
397
|
+
} catch {}
|
|
398
|
+
try {
|
|
399
|
+
const { notifyDaemonReload } = await Promise.resolve().then(() => require("../program/daemon-integration.cjs"));
|
|
400
|
+
const { getDaemonStatus } = await Promise.resolve().then(() => require("../daemon/manager.cjs"));
|
|
401
|
+
await notifyDaemonReload({ getDaemonStatus });
|
|
402
|
+
} catch {}
|
|
403
|
+
return {
|
|
404
|
+
success: true,
|
|
405
|
+
programId,
|
|
406
|
+
configuredMounts: changedMounts
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* List configurable mounts and their current status for a program.
|
|
411
|
+
*/
|
|
412
|
+
async function listProgramMountStatus(programId, options) {
|
|
413
|
+
const userConfigDir = getUserConfigDir(options?.userConfigDir);
|
|
414
|
+
const manifest = await validateProgramDir((0, node_path.join)(userConfigDir, "programs", programId));
|
|
415
|
+
const overrides = await readProgramMountOverrides(programId, { userConfigDir });
|
|
416
|
+
const overrideByTarget = new Map(overrides.map((o) => [o.target, o]));
|
|
417
|
+
const { createCredentialStore } = await Promise.resolve().then(() => require("../credential/store.cjs"));
|
|
418
|
+
const credentialStore = createCredentialStore();
|
|
419
|
+
const mounts = [];
|
|
420
|
+
for (const mountDecl of manifest.mounts) {
|
|
421
|
+
const override = overrideByTarget.get(mountDecl.target);
|
|
422
|
+
const effectiveUri = override?.uri || mountDecl.uri;
|
|
423
|
+
let hasCredentials = false;
|
|
424
|
+
try {
|
|
425
|
+
const stored = await credentialStore.get(effectiveUri);
|
|
426
|
+
hasCredentials = !!stored && Object.keys(stored).length > 0;
|
|
427
|
+
} catch {}
|
|
428
|
+
mounts.push({
|
|
429
|
+
path: mountDecl.target,
|
|
430
|
+
uri: mountDecl.uri,
|
|
431
|
+
configuredUri: override?.uri !== mountDecl.uri ? override?.uri : void 0,
|
|
432
|
+
required: mountDecl.required,
|
|
433
|
+
hasCredentials,
|
|
434
|
+
hasOptions: !!(override?.options && Object.keys(override.options).length > 0)
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
programId,
|
|
439
|
+
mounts
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
//#endregion
|
|
444
|
+
exports.configureProgramMounts = configureProgramMounts;
|
|
445
|
+
exports.getUserConfigDir = getUserConfigDir;
|
|
446
|
+
exports.installProgram = installProgram;
|
|
447
|
+
exports.listInstalledPrograms = listInstalledPrograms;
|
|
448
|
+
exports.listProgramMountStatus = listProgramMountStatus;
|
|
449
|
+
exports.readProgramMountOverrides = readProgramMountOverrides;
|
|
450
|
+
exports.removeProgram = removeProgram;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@aigne/afs";
|