@aigne/afs-cli 1.11.0-beta.4 → 1.11.0-beta.5

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/README.md CHANGED
@@ -59,10 +59,11 @@ access_mode = "readonly"
59
59
  path = "/db"
60
60
  uri = "sqlite:///path/to/app.db"
61
61
 
62
- # 远程 AFS 服务器
62
+ # 远程 AFS 服务器 (带认证)
63
63
  [[mounts]]
64
64
  path = "/remote"
65
65
  uri = "https://afs.example.com/afs"
66
+ token = "${AFS_TOKEN}" # 使用环境变量
66
67
 
67
68
  # HTTP 服务器配置
68
69
  [serve]
@@ -72,8 +73,22 @@ path = "/afs"
72
73
  readonly = false
73
74
  cors = false
74
75
  max_body_size = 10485760 # 10MB
76
+ token = "${AFS_SERVER_TOKEN}" # 服务器认证 token
75
77
  ```
76
78
 
79
+ ### 挂载配置字段
80
+
81
+ | 字段 | 必填 | 说明 |
82
+ |------|------|------|
83
+ | `path` | 是 | 挂载路径 (如 `/src`) |
84
+ | `uri` | 是 | Provider URI (如 `fs:///path`) |
85
+ | `description` | 否 | 人类/LLM 可读描述 |
86
+ | `access_mode` | 否 | 访问模式: `readonly` 或 `readwrite` |
87
+ | `token` | 否 | HTTP Provider 认证 token (支持 `${ENV_VAR}`) |
88
+ | `auth` | 否 | 认证字符串 (支持 `${ENV_VAR}`) |
89
+ | `namespace` | 否 | 命名空间 (用于隔离不同环境) |
90
+ | `options` | 否 | Provider 特定选项 |
91
+
77
92
  ## 命令
78
93
 
79
94
  ### 文件操作
package/dist/cli.cjs CHANGED
@@ -205,10 +205,16 @@ async function main() {
205
205
  }).option("description", {
206
206
  type: "string",
207
207
  description: "Human-readable description for this mount"
208
+ }).option("token", {
209
+ type: "string",
210
+ description: "Bearer token for HTTP provider authorization"
208
211
  }), async (argv) => {
209
212
  const view = getViewType(argv);
210
213
  await maybeShowHeader(view);
211
- const result = await require_mount.mountAddCommand(process.cwd(), argv.path, argv.uri, { description: argv.description });
214
+ const result = await require_mount.mountAddCommand(process.cwd(), argv.path, argv.uri, {
215
+ description: argv.description,
216
+ token: argv.token
217
+ });
212
218
  if (view === "json") console.log(JSON.stringify(result, null, 2));
213
219
  else if (result.success) {
214
220
  const msg = view === "human" ? `${require_index.colors.success("Added mount")} ${require_index.colors.cyan(result.normalizedPath)}` : `Added mount ${result.normalizedPath}`;
@@ -289,6 +295,9 @@ async function main() {
289
295
  }).option("max-body", {
290
296
  type: "number",
291
297
  description: "Maximum request body size in bytes (default: 10MB)"
298
+ }).option("token", {
299
+ type: "string",
300
+ description: "Bearer token for authorization"
292
301
  }), async (argv) => {
293
302
  await maybeShowHeader(getViewType(argv));
294
303
  const result = await require_serve.serveCommand({
@@ -297,7 +306,8 @@ async function main() {
297
306
  path: argv.path,
298
307
  readonly: argv.readonly,
299
308
  cors: argv.cors,
300
- maxBodySize: argv["max-body"]
309
+ maxBodySize: argv["max-body"],
310
+ token: argv.token
301
311
  });
302
312
  console.log(require_serve.formatServeOutput(result));
303
313
  await new Promise(() => {});
package/dist/cli.mjs CHANGED
@@ -203,10 +203,16 @@ async function main() {
203
203
  }).option("description", {
204
204
  type: "string",
205
205
  description: "Human-readable description for this mount"
206
+ }).option("token", {
207
+ type: "string",
208
+ description: "Bearer token for HTTP provider authorization"
206
209
  }), async (argv) => {
207
210
  const view = getViewType(argv);
208
211
  await maybeShowHeader(view);
209
- const result = await mountAddCommand(process.cwd(), argv.path, argv.uri, { description: argv.description });
212
+ const result = await mountAddCommand(process.cwd(), argv.path, argv.uri, {
213
+ description: argv.description,
214
+ token: argv.token
215
+ });
210
216
  if (view === "json") console.log(JSON.stringify(result, null, 2));
211
217
  else if (result.success) {
212
218
  const msg = view === "human" ? `${colors.success("Added mount")} ${colors.cyan(result.normalizedPath)}` : `Added mount ${result.normalizedPath}`;
@@ -287,6 +293,9 @@ async function main() {
287
293
  }).option("max-body", {
288
294
  type: "number",
289
295
  description: "Maximum request body size in bytes (default: 10MB)"
296
+ }).option("token", {
297
+ type: "string",
298
+ description: "Bearer token for authorization"
290
299
  }), async (argv) => {
291
300
  await maybeShowHeader(getViewType(argv));
292
301
  const result = await serveCommand({
@@ -295,7 +304,8 @@ async function main() {
295
304
  path: argv.path,
296
305
  readonly: argv.readonly,
297
306
  cors: argv.cors,
298
- maxBodySize: argv["max-body"]
307
+ maxBodySize: argv["max-body"],
308
+ token: argv.token
299
309
  });
300
310
  console.log(formatServeOutput(result));
301
311
  await new Promise(() => {});
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":["yargs"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * AFS CLI - Command Line Interface\n *\n * Commands:\n * - afs mount list|ls List mount configurations\n * - afs mount add <path> <uri> Add a mount\n * - afs mount remove|rm <path> Remove a mount\n * - afs mount validate Validate mount configuration\n * - afs list|ls [path] List directory\n * - afs stat <path> Get file/directory info\n * - afs read <path> Read file content\n * - afs write <path> Write file content (--content or stdin)\n * - afs exec <path> <action> Execute operation\n * - afs serve Start HTTP server to expose AFS\n * - afs explore [path] Interactive TUI file explorer\n *\n * Output modes (default: human):\n * - --view=human: Human friendly format with colors (default)\n * - --view=llm: LLM optimized output (token-efficient)\n * - --view=default: Machine truth (script/pipe friendly)\n * - --json: Structured JSON\n */\n\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport {\n execCommand,\n explainCommand,\n explainPathCommand,\n formatExecOutput,\n formatExplainOutput,\n formatLsOutput,\n formatMountListOutput,\n formatPathExplainOutput,\n formatReadOutput,\n formatServeOutput,\n formatStatOutput,\n formatWriteOutput,\n lsCommand,\n mountAddCommand,\n mountListCommand,\n mountRemoveCommand,\n mountValidateCommand,\n readCommand,\n serveCommand,\n statCommand,\n type ViewType,\n writeCommand,\n} from \"./commands/index.js\";\nimport { ConfigLoader } from \"./config/loader.js\";\nimport { CLIError, ExitCode } from \"./errors.js\";\nimport { createExplorerScreen } from \"./explorer/screen.js\";\nimport { createRuntime } from \"./runtime.js\";\nimport { colors, printHeader, shouldShowHeader } from \"./ui/index.js\";\nimport { VERSION } from \"./version.js\";\n\n// Global view type derived from args\n// Default is \"human\" for interactive use; LLMs can use --view=llm or --view=default\nfunction getViewType(argv: { json?: boolean; view?: string }): ViewType {\n if (argv.json) return \"json\";\n if (argv.view) return argv.view as ViewType;\n return \"human\";\n}\n\n// Track if header has been shown (show only once per invocation)\nlet headerShown = false;\n\n/**\n * Show header if in human view mode and not already shown\n */\nasync function maybeShowHeader(view: ViewType): Promise<void> {\n if (view !== \"human\" || headerShown || !shouldShowHeader()) {\n return;\n }\n\n headerShown = true;\n\n // Load mount count for header\n try {\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n printHeader({\n version: VERSION,\n mountCount: config.mounts.length,\n });\n } catch {\n // If config loading fails, just show header without mount count\n printHeader({\n version: VERSION,\n mountCount: 0,\n });\n }\n}\n\n// Show header for help/version in interactive mode\nasync function showHeaderIfNeeded(): Promise<void> {\n const args = process.argv.slice(2);\n const isHelp =\n args.length === 0 ||\n args.includes(\"--help\") ||\n args.includes(\"-h\") ||\n (args.length === 1 && [\"help\", \"mount\", \"explain\"].includes(args[0]!));\n\n if (isHelp && shouldShowHeader()) {\n try {\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n printHeader({\n version: VERSION,\n mountCount: config.mounts.length,\n });\n } catch {\n printHeader({\n version: VERSION,\n mountCount: 0,\n });\n }\n }\n}\n\n// Run the CLI\nasync function main() {\n await showHeaderIfNeeded();\n\n const cli = yargs(hideBin(process.argv))\n .scriptName(\"afs\")\n .version(VERSION)\n .alias(\"version\", \"V\")\n .help(\"help\")\n .alias(\"help\", \"h\")\n .usage(\"$0 <command> [options]\")\n .option(\"json\", {\n type: \"boolean\",\n description: \"Output in JSON format\",\n global: true,\n })\n .option(\"view\", {\n type: \"string\",\n choices: [\"default\", \"llm\", \"human\"],\n default: \"human\",\n description: \"Output format (llm for AI agents, default for scripts)\",\n global: true,\n })\n .command(\n [\"list [path]\", \"ls [path]\"],\n \"List directory contents\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n default: \"/\",\n description: \"Path to list\",\n })\n .option(\"depth\", {\n type: \"number\",\n default: 1,\n description: \"Maximum depth to list\",\n })\n .option(\"limit\", {\n alias: \"n\",\n type: \"number\",\n description: \"Maximum number of entries to return\",\n })\n .option(\"max-children\", {\n type: \"number\",\n description: \"Maximum children per directory\",\n })\n .option(\"pattern\", {\n alias: \"p\",\n type: \"string\",\n description: \"Glob pattern to filter entries (e.g., *.ts, **/*.js)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const runtime = await createRuntime();\n const result = await lsCommand(runtime, argv.path!, {\n maxDepth: argv.depth,\n limit: argv.limit,\n maxChildren: argv[\"max-children\"],\n pattern: argv.pattern,\n });\n console.log(formatLsOutput(result, view));\n },\n )\n .command(\n \"stat <path>\",\n \"Get file or directory info\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to stat\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const runtime = await createRuntime();\n const result = await statCommand(runtime, argv.path!);\n console.log(formatStatOutput(result, view));\n },\n )\n .command(\n \"read <path>\",\n \"Read file content\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to read\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const runtime = await createRuntime();\n const result = await readCommand(runtime, argv.path!);\n console.log(formatReadOutput(result, view));\n },\n )\n .command(\n \"write <path>\",\n \"Write content to file (from --content or stdin)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to write\",\n })\n .option(\"content\", {\n type: \"string\",\n description: \"Content to write (if not provided, reads from stdin)\",\n })\n .option(\"append\", {\n type: \"boolean\",\n default: false,\n description: \"Append to file instead of overwrite\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n let content: string;\n\n if (argv.content !== undefined) {\n // Use --content parameter\n content = argv.content;\n } else {\n // Read content from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n content = Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n const runtime = await createRuntime();\n const result = await writeCommand(runtime, argv.path!, content, {\n append: argv.append,\n });\n console.log(formatWriteOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"exec <path> [action]\",\n \"Execute operation on path\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to execute on\",\n })\n .positional(\"action\", {\n type: \"string\",\n default: \"default\",\n description: \"Action to execute\",\n })\n .option(\"params\", {\n type: \"string\",\n description: \"JSON parameters for the action\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const params = argv.params ? JSON.parse(argv.params) : {};\n\n const runtime = await createRuntime();\n const result = await execCommand(runtime, argv.path!, argv.action!, params);\n console.log(formatExecOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"mount\",\n \"Manage mount configurations\",\n (yargs) =>\n yargs\n .command(\n [\"list\", \"ls\"],\n \"List all mounts\",\n () => {},\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const result = await mountListCommand(process.cwd());\n console.log(formatMountListOutput(result.mounts, view));\n },\n )\n .command(\n \"add <path> <uri>\",\n \"Add a new mount (path=virtual path, uri=data source)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path in AFS namespace (e.g., /src, /data)\",\n })\n .positional(\"uri\", {\n type: \"string\",\n demandOption: true,\n description: \"Data source URI: fs:///local/path, git://repo, sqlite:///db.sqlite\",\n })\n .option(\"description\", {\n type: \"string\",\n description: \"Human-readable description for this mount\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await mountAddCommand(process.cwd(), argv.path!, argv.uri!, {\n description: argv.description,\n });\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n const msg =\n view === \"human\"\n ? `${colors.success(\"Added mount\")} ${colors.cyan(result.normalizedPath!)}`\n : `Added mount ${result.normalizedPath}`;\n console.log(msg);\n } else {\n const msg = view === \"human\" ? colors.error(result.message!) : result.message;\n console.error(msg);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n [\"remove <path>\", \"rm <path>\"],\n \"Remove a mount\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path to remove (e.g., /src)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await mountRemoveCommand(process.cwd(), argv.path!);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n const msg =\n view === \"human\"\n ? `${colors.success(\"Removed mount\")} ${colors.cyan(argv.path!)}`\n : `Removed mount ${argv.path}`;\n console.log(msg);\n } else {\n const msg = view === \"human\" ? colors.error(result.message!) : result.message;\n console.error(msg);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n \"validate\",\n \"Validate mount configuration\",\n () => {},\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await mountValidateCommand(process.cwd());\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.valid) {\n const msg =\n view === \"human\"\n ? colors.success(\"Configuration is valid\")\n : \"Configuration is valid\";\n console.log(msg);\n } else {\n const titleMsg =\n view === \"human\"\n ? colors.error(\"Configuration has errors:\")\n : \"Configuration has errors:\";\n console.error(titleMsg);\n for (const error of result.errors) {\n const errMsg =\n view === \"human\"\n ? ` ${colors.dim(\"-\")} ${colors.error(error)}`\n : ` - ${error}`;\n console.error(errMsg);\n }\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .demandCommand(1, \"Please specify a mount subcommand\"),\n () => {},\n )\n .command(\n \"explain [topic]\",\n \"Explain AFS concepts or AFS object\",\n (yargs) =>\n yargs.positional(\"topic\", {\n type: \"string\",\n description: \"Topic (mount, path, uri) or AFS path (e.g., /modules/src)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const topic = argv.topic;\n\n // If topic starts with /, treat as object path\n if (topic?.startsWith(\"/\")) {\n const runtime = await createRuntime();\n const result = await explainPathCommand(runtime, topic);\n console.log(formatPathExplainOutput(result, view));\n } else {\n const result = await explainCommand(process.cwd(), topic);\n console.log(formatExplainOutput(result, view));\n }\n },\n )\n .command(\n \"serve\",\n \"Start HTTP server to expose AFS providers\",\n (yargs) =>\n yargs\n .option(\"host\", {\n type: \"string\",\n default: \"localhost\",\n description: \"Host address to listen on\",\n })\n .option(\"port\", {\n type: \"number\",\n default: 3000,\n description: \"Port to listen on\",\n })\n .option(\"path\", {\n type: \"string\",\n default: \"/afs\",\n description: \"Base path for the server\",\n })\n .option(\"readonly\", {\n type: \"boolean\",\n default: false,\n description: \"Run in readonly mode (disable write operations)\",\n })\n .option(\"cors\", {\n type: \"boolean\",\n default: false,\n description: \"Enable CORS support\",\n })\n .option(\"max-body\", {\n type: \"number\",\n description: \"Maximum request body size in bytes (default: 10MB)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await serveCommand({\n host: argv.host,\n port: argv.port,\n path: argv.path,\n readonly: argv.readonly,\n cors: argv.cors,\n maxBodySize: argv[\"max-body\"],\n });\n\n console.log(formatServeOutput(result));\n\n // Keep the process running\n await new Promise(() => {});\n },\n )\n .command(\n \"explore [path]\",\n \"Interactive TUI file explorer (PC Tools style)\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n default: \"/\",\n description: \"Starting path to explore\",\n }),\n async (argv) => {\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n const runtime = await createRuntime(process.cwd(), { configLoader });\n await createExplorerScreen({\n runtime,\n startPath: argv.path,\n version: VERSION,\n mountCount: config.mounts.length,\n });\n },\n )\n .demandCommand(1, \"Please specify a command\")\n .strict();\n\n try {\n await cli.parse();\n } catch (error) {\n if (error instanceof CLIError) {\n console.error(error.message);\n process.exit(error.exitCode);\n }\n throw error;\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":["yargs"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * AFS CLI - Command Line Interface\n *\n * Commands:\n * - afs mount list|ls List mount configurations\n * - afs mount add <path> <uri> Add a mount\n * - afs mount remove|rm <path> Remove a mount\n * - afs mount validate Validate mount configuration\n * - afs list|ls [path] List directory\n * - afs stat <path> Get file/directory info\n * - afs read <path> Read file content\n * - afs write <path> Write file content (--content or stdin)\n * - afs exec <path> <action> Execute operation\n * - afs serve Start HTTP server to expose AFS\n * - afs explore [path] Interactive TUI file explorer\n *\n * Output modes (default: human):\n * - --view=human: Human friendly format with colors (default)\n * - --view=llm: LLM optimized output (token-efficient)\n * - --view=default: Machine truth (script/pipe friendly)\n * - --json: Structured JSON\n */\n\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport {\n execCommand,\n explainCommand,\n explainPathCommand,\n formatExecOutput,\n formatExplainOutput,\n formatLsOutput,\n formatMountListOutput,\n formatPathExplainOutput,\n formatReadOutput,\n formatServeOutput,\n formatStatOutput,\n formatWriteOutput,\n lsCommand,\n mountAddCommand,\n mountListCommand,\n mountRemoveCommand,\n mountValidateCommand,\n readCommand,\n serveCommand,\n statCommand,\n type ViewType,\n writeCommand,\n} from \"./commands/index.js\";\nimport { ConfigLoader } from \"./config/loader.js\";\nimport { CLIError, ExitCode } from \"./errors.js\";\nimport { createExplorerScreen } from \"./explorer/screen.js\";\nimport { createRuntime } from \"./runtime.js\";\nimport { colors, printHeader, shouldShowHeader } from \"./ui/index.js\";\nimport { VERSION } from \"./version.js\";\n\n// Global view type derived from args\n// Default is \"human\" for interactive use; LLMs can use --view=llm or --view=default\nfunction getViewType(argv: { json?: boolean; view?: string }): ViewType {\n if (argv.json) return \"json\";\n if (argv.view) return argv.view as ViewType;\n return \"human\";\n}\n\n// Track if header has been shown (show only once per invocation)\nlet headerShown = false;\n\n/**\n * Show header if in human view mode and not already shown\n */\nasync function maybeShowHeader(view: ViewType): Promise<void> {\n if (view !== \"human\" || headerShown || !shouldShowHeader()) {\n return;\n }\n\n headerShown = true;\n\n // Load mount count for header\n try {\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n printHeader({\n version: VERSION,\n mountCount: config.mounts.length,\n });\n } catch {\n // If config loading fails, just show header without mount count\n printHeader({\n version: VERSION,\n mountCount: 0,\n });\n }\n}\n\n// Show header for help/version in interactive mode\nasync function showHeaderIfNeeded(): Promise<void> {\n const args = process.argv.slice(2);\n const isHelp =\n args.length === 0 ||\n args.includes(\"--help\") ||\n args.includes(\"-h\") ||\n (args.length === 1 && [\"help\", \"mount\", \"explain\"].includes(args[0]!));\n\n if (isHelp && shouldShowHeader()) {\n try {\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n printHeader({\n version: VERSION,\n mountCount: config.mounts.length,\n });\n } catch {\n printHeader({\n version: VERSION,\n mountCount: 0,\n });\n }\n }\n}\n\n// Run the CLI\nasync function main() {\n await showHeaderIfNeeded();\n\n const cli = yargs(hideBin(process.argv))\n .scriptName(\"afs\")\n .version(VERSION)\n .alias(\"version\", \"V\")\n .help(\"help\")\n .alias(\"help\", \"h\")\n .usage(\"$0 <command> [options]\")\n .option(\"json\", {\n type: \"boolean\",\n description: \"Output in JSON format\",\n global: true,\n })\n .option(\"view\", {\n type: \"string\",\n choices: [\"default\", \"llm\", \"human\"],\n default: \"human\",\n description: \"Output format (llm for AI agents, default for scripts)\",\n global: true,\n })\n .command(\n [\"list [path]\", \"ls [path]\"],\n \"List directory contents\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n default: \"/\",\n description: \"Path to list\",\n })\n .option(\"depth\", {\n type: \"number\",\n default: 1,\n description: \"Maximum depth to list\",\n })\n .option(\"limit\", {\n alias: \"n\",\n type: \"number\",\n description: \"Maximum number of entries to return\",\n })\n .option(\"max-children\", {\n type: \"number\",\n description: \"Maximum children per directory\",\n })\n .option(\"pattern\", {\n alias: \"p\",\n type: \"string\",\n description: \"Glob pattern to filter entries (e.g., *.ts, **/*.js)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const runtime = await createRuntime();\n const result = await lsCommand(runtime, argv.path!, {\n maxDepth: argv.depth,\n limit: argv.limit,\n maxChildren: argv[\"max-children\"],\n pattern: argv.pattern,\n });\n console.log(formatLsOutput(result, view));\n },\n )\n .command(\n \"stat <path>\",\n \"Get file or directory info\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to stat\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const runtime = await createRuntime();\n const result = await statCommand(runtime, argv.path!);\n console.log(formatStatOutput(result, view));\n },\n )\n .command(\n \"read <path>\",\n \"Read file content\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to read\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const runtime = await createRuntime();\n const result = await readCommand(runtime, argv.path!);\n console.log(formatReadOutput(result, view));\n },\n )\n .command(\n \"write <path>\",\n \"Write content to file (from --content or stdin)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to write\",\n })\n .option(\"content\", {\n type: \"string\",\n description: \"Content to write (if not provided, reads from stdin)\",\n })\n .option(\"append\", {\n type: \"boolean\",\n default: false,\n description: \"Append to file instead of overwrite\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n let content: string;\n\n if (argv.content !== undefined) {\n // Use --content parameter\n content = argv.content;\n } else {\n // Read content from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n content = Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n const runtime = await createRuntime();\n const result = await writeCommand(runtime, argv.path!, content, {\n append: argv.append,\n });\n console.log(formatWriteOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"exec <path> [action]\",\n \"Execute operation on path\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to execute on\",\n })\n .positional(\"action\", {\n type: \"string\",\n default: \"default\",\n description: \"Action to execute\",\n })\n .option(\"params\", {\n type: \"string\",\n description: \"JSON parameters for the action\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const params = argv.params ? JSON.parse(argv.params) : {};\n\n const runtime = await createRuntime();\n const result = await execCommand(runtime, argv.path!, argv.action!, params);\n console.log(formatExecOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"mount\",\n \"Manage mount configurations\",\n (yargs) =>\n yargs\n .command(\n [\"list\", \"ls\"],\n \"List all mounts\",\n () => {},\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const result = await mountListCommand(process.cwd());\n console.log(formatMountListOutput(result.mounts, view));\n },\n )\n .command(\n \"add <path> <uri>\",\n \"Add a new mount (path=virtual path, uri=data source)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path in AFS namespace (e.g., /src, /data)\",\n })\n .positional(\"uri\", {\n type: \"string\",\n demandOption: true,\n description: \"Data source URI: fs:///local/path, git://repo, sqlite:///db.sqlite\",\n })\n .option(\"description\", {\n type: \"string\",\n description: \"Human-readable description for this mount\",\n })\n .option(\"token\", {\n type: \"string\",\n description: \"Bearer token for HTTP provider authorization\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await mountAddCommand(process.cwd(), argv.path!, argv.uri!, {\n description: argv.description,\n token: argv.token,\n });\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n const msg =\n view === \"human\"\n ? `${colors.success(\"Added mount\")} ${colors.cyan(result.normalizedPath!)}`\n : `Added mount ${result.normalizedPath}`;\n console.log(msg);\n } else {\n const msg = view === \"human\" ? colors.error(result.message!) : result.message;\n console.error(msg);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n [\"remove <path>\", \"rm <path>\"],\n \"Remove a mount\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path to remove (e.g., /src)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await mountRemoveCommand(process.cwd(), argv.path!);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n const msg =\n view === \"human\"\n ? `${colors.success(\"Removed mount\")} ${colors.cyan(argv.path!)}`\n : `Removed mount ${argv.path}`;\n console.log(msg);\n } else {\n const msg = view === \"human\" ? colors.error(result.message!) : result.message;\n console.error(msg);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n \"validate\",\n \"Validate mount configuration\",\n () => {},\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await mountValidateCommand(process.cwd());\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.valid) {\n const msg =\n view === \"human\"\n ? colors.success(\"Configuration is valid\")\n : \"Configuration is valid\";\n console.log(msg);\n } else {\n const titleMsg =\n view === \"human\"\n ? colors.error(\"Configuration has errors:\")\n : \"Configuration has errors:\";\n console.error(titleMsg);\n for (const error of result.errors) {\n const errMsg =\n view === \"human\"\n ? ` ${colors.dim(\"-\")} ${colors.error(error)}`\n : ` - ${error}`;\n console.error(errMsg);\n }\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .demandCommand(1, \"Please specify a mount subcommand\"),\n () => {},\n )\n .command(\n \"explain [topic]\",\n \"Explain AFS concepts or AFS object\",\n (yargs) =>\n yargs.positional(\"topic\", {\n type: \"string\",\n description: \"Topic (mount, path, uri) or AFS path (e.g., /modules/src)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n const topic = argv.topic;\n\n // If topic starts with /, treat as object path\n if (topic?.startsWith(\"/\")) {\n const runtime = await createRuntime();\n const result = await explainPathCommand(runtime, topic);\n console.log(formatPathExplainOutput(result, view));\n } else {\n const result = await explainCommand(process.cwd(), topic);\n console.log(formatExplainOutput(result, view));\n }\n },\n )\n .command(\n \"serve\",\n \"Start HTTP server to expose AFS providers\",\n (yargs) =>\n yargs\n .option(\"host\", {\n type: \"string\",\n default: \"localhost\",\n description: \"Host address to listen on\",\n })\n .option(\"port\", {\n type: \"number\",\n default: 3000,\n description: \"Port to listen on\",\n })\n .option(\"path\", {\n type: \"string\",\n default: \"/afs\",\n description: \"Base path for the server\",\n })\n .option(\"readonly\", {\n type: \"boolean\",\n default: false,\n description: \"Run in readonly mode (disable write operations)\",\n })\n .option(\"cors\", {\n type: \"boolean\",\n default: false,\n description: \"Enable CORS support\",\n })\n .option(\"max-body\", {\n type: \"number\",\n description: \"Maximum request body size in bytes (default: 10MB)\",\n })\n .option(\"token\", {\n type: \"string\",\n description: \"Bearer token for authorization\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n await maybeShowHeader(view);\n\n const result = await serveCommand({\n host: argv.host,\n port: argv.port,\n path: argv.path,\n readonly: argv.readonly,\n cors: argv.cors,\n maxBodySize: argv[\"max-body\"],\n token: argv.token,\n });\n\n console.log(formatServeOutput(result));\n\n // Keep the process running\n await new Promise(() => {});\n },\n )\n .command(\n \"explore [path]\",\n \"Interactive TUI file explorer (PC Tools style)\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n default: \"/\",\n description: \"Starting path to explore\",\n }),\n async (argv) => {\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n const runtime = await createRuntime(process.cwd(), { configLoader });\n await createExplorerScreen({\n runtime,\n startPath: argv.path,\n version: VERSION,\n mountCount: config.mounts.length,\n });\n },\n )\n .demandCommand(1, \"Please specify a command\")\n .strict();\n\n try {\n await cli.parse();\n } catch (error) {\n if (error instanceof CLIError) {\n console.error(error.message);\n process.exit(error.exitCode);\n }\n throw error;\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n});\n"],"mappings":"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"}
@@ -66,7 +66,8 @@ async function mountAddCommand(cwd, path, uri, options = {}) {
66
66
  const newMount = {
67
67
  path: validation.data.path,
68
68
  uri: validation.data.uri,
69
- ...options.description && { description: options.description }
69
+ ...options.description && { description: options.description },
70
+ ...options.token && { token: options.token }
70
71
  };
71
72
  const configDir = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME);
72
73
  const configPath = (0, node_path.join)(configDir, require_loader.CONFIG_FILE_NAME);
@@ -65,7 +65,8 @@ async function mountAddCommand(cwd, path, uri, options = {}) {
65
65
  const newMount = {
66
66
  path: validation.data.path,
67
67
  uri: validation.data.uri,
68
- ...options.description && { description: options.description }
68
+ ...options.description && { description: options.description },
69
+ ...options.token && { token: options.token }
69
70
  };
70
71
  const configDir = join(cwd, CONFIG_DIR_NAME);
71
72
  const configPath = join(configDir, CONFIG_FILE_NAME);
@@ -1 +1 @@
1
- {"version":3,"file":"mount.mjs","names":[],"sources":["../../src/commands/mount.ts"],"sourcesContent":["import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { parse, stringify } from \"smol-toml\";\nimport { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader } from \"../config/loader.js\";\nimport { MountSchema } from \"../config/schema.js\";\nimport { colors } from \"../ui/index.js\";\nimport type { ViewType } from \"./ls.js\";\n\nexport interface MountEntry {\n path: string;\n uri: string;\n description?: string;\n access_mode?: \"readonly\" | \"readwrite\";\n auth?: string;\n options?: Record<string, unknown>;\n}\n\nexport interface MountListResult {\n mounts: MountEntry[];\n}\n\nexport interface MountCommandResult {\n success: boolean;\n message?: string;\n /** The normalized path (for display after successful add) */\n normalizedPath?: string;\n}\n\nexport interface MountValidateResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Check if a path looks like a remote Git URL\n * Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)\n */\nfunction isRemoteGitUrl(path: string): boolean {\n // SSH format: git@github.com:user/repo.git or user@host:path\n if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) {\n return true;\n }\n // Embedded protocol: https://github.com/user/repo.git\n if (/^(https?|ssh|git):\\/\\//.test(path)) {\n return true;\n }\n return false;\n}\n\n/**\n * Resolve relative paths in URI to absolute paths\n * Supports fs://, git://, sqlite://, json:// schemes\n * Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)\n */\nfunction resolveUriPath(uri: string, cwd: string): string {\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//);\n\n // No protocol prefix\n if (!schemeMatch) {\n // Check if it's a remote git URL (SSH format like git@github.com:user/repo.git)\n if (isRemoteGitUrl(uri)) {\n return `git://${uri}`;\n }\n // Treat as local filesystem path\n const absolutePath = isAbsolute(uri) ? uri : resolve(cwd, uri);\n return `fs://${absolutePath}`;\n }\n\n const scheme = schemeMatch[1];\n const pathPart = uri.slice(schemeMatch[0].length);\n\n // Only resolve for local file-based schemes\n if (![\"fs\", \"git\", \"sqlite\", \"json\"].includes(scheme!)) {\n return uri;\n }\n\n // For git:// scheme, check if the path is a remote URL (not a local path)\n if (scheme === \"git\" && isRemoteGitUrl(pathPart)) {\n return uri;\n }\n\n // If path is already absolute, return as-is\n if (isAbsolute(pathPart)) {\n return uri;\n }\n\n // Resolve relative path against cwd\n const absolutePath = resolve(cwd, pathPart);\n return `${scheme}://${absolutePath}`;\n}\n\n/**\n * List all mounts from config (merged from all config layers)\n */\nexport async function mountListCommand(cwd: string): Promise<MountListResult> {\n const loader = new ConfigLoader();\n const config = await loader.load(cwd);\n return {\n mounts: config.mounts as MountEntry[],\n };\n}\n\n/**\n * Add a mount to config\n */\nexport async function mountAddCommand(\n cwd: string,\n path: string,\n uri: string,\n options: { description?: string } = {},\n): Promise<MountCommandResult> {\n // Check for empty URI before resolving (resolveUriPath would transform empty string)\n if (!uri || uri.trim() === \"\") {\n return {\n success: false,\n message: \"URI is required\",\n };\n }\n\n // Resolve relative paths in URI to absolute paths\n const resolvedUri = resolveUriPath(uri, cwd);\n\n // Validate and normalize inputs using schema\n // The schema transforms path (normalizes it) and validates all fields\n const validation = MountSchema.safeParse({\n path,\n uri: resolvedUri,\n ...options,\n });\n if (!validation.success) {\n const errors = validation.error.errors.map((e) => e.message).join(\"; \");\n return {\n success: false,\n message: errors,\n };\n }\n\n // Build mount entry with validated/normalized path and resolved URI\n const newMount: MountEntry = {\n path: validation.data.path, // Use normalized path from schema\n uri: validation.data.uri,\n ...(options.description && { description: options.description }),\n };\n\n const configDir = join(cwd, CONFIG_DIR_NAME);\n const configPath = join(configDir, CONFIG_FILE_NAME);\n\n // Load existing config or create new\n const config: { mounts: MountEntry[] } = { mounts: [] };\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const parsed = parse(content) as { mounts?: MountEntry[] };\n config.mounts = parsed.mounts ?? [];\n } catch {\n // Config doesn't exist, will create\n }\n\n // Check for duplicate path (use normalized path for comparison)\n const normalizedPath = validation.data.path;\n if (config.mounts.some((m) => m.path === normalizedPath)) {\n return {\n success: false,\n message: `Mount path \"${normalizedPath}\" already exists`,\n };\n }\n\n // Add validated mount\n config.mounts.push(newMount);\n\n // Ensure config directory exists\n try {\n await mkdir(configDir, { recursive: true });\n } catch {\n // Directory might already exist\n }\n\n // Write config\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n normalizedPath,\n };\n}\n\n/**\n * Remove a mount from config\n */\nexport async function mountRemoveCommand(cwd: string, path: string): Promise<MountCommandResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n const index = mounts.findIndex((m) => m.path === path);\n if (index === -1) {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n\n mounts.splice(index, 1);\n config.mounts = mounts;\n\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n };\n } catch {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n}\n\n/**\n * Validate mount configuration\n */\nexport async function mountValidateCommand(cwd: string): Promise<MountValidateResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n const errors: string[] = [];\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n for (const mount of mounts) {\n // Validate mount using schema\n const validation = MountSchema.safeParse(mount);\n if (!validation.success) {\n for (const err of validation.error.errors) {\n errors.push(`Mount \"${mount.path}\": ${err.message}`);\n }\n continue;\n }\n\n // For fs:// URIs, check if target exists\n if (mount.uri.startsWith(\"fs://\")) {\n const targetPath = mount.uri.replace(\"fs://\", \"\");\n try {\n await access(targetPath);\n } catch {\n errors.push(`Mount target \"${targetPath}\" does not exist`);\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n } catch {\n return {\n valid: true,\n errors: [],\n };\n }\n}\n\n/**\n * Format mount list output for different views\n */\nexport function formatMountListOutput(mounts: MountEntry[], view: ViewType): string {\n switch (view) {\n case \"json\":\n return JSON.stringify({ mounts }, null, 2);\n case \"llm\":\n return formatLlm(mounts);\n case \"human\":\n return formatHuman(mounts);\n default:\n return formatDefault(mounts);\n }\n}\n\nfunction formatDefault(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"No mounts configured\";\n }\n\n return mounts\n .map((m) => {\n const desc = m.description ? ` (${m.description})` : \"\";\n return `${m.path} → ${m.uri}${desc}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatLlm(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"NO_MOUNTS\";\n }\n\n return mounts\n .map((m) => {\n const lines = [`MOUNT ${m.path}`, `URI=${m.uri}`];\n if (m.description) {\n lines.push(`DESC=${m.description}`);\n }\n return lines.join(\"\\n\");\n })\n .join(\"\\n\\n\");\n}\n\nfunction formatHuman(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return colors.dim(\"No mounts configured.\");\n }\n\n const lines = [colors.bold(\"Configured Mounts:\"), \"\"];\n for (const m of mounts) {\n lines.push(` ${colors.cyan(m.path)}`);\n lines.push(` ${colors.dim(\"URI:\")} ${m.uri}`);\n if (m.description) {\n lines.push(` ${colors.dim(\"Description:\")} ${m.description}`);\n }\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mount.mjs","names":[],"sources":["../../src/commands/mount.ts"],"sourcesContent":["import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { parse, stringify } from \"smol-toml\";\nimport { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader } from \"../config/loader.js\";\nimport { MountSchema } from \"../config/schema.js\";\nimport { colors } from \"../ui/index.js\";\nimport type { ViewType } from \"./ls.js\";\n\nexport interface MountEntry {\n path: string;\n uri: string;\n description?: string;\n access_mode?: \"readonly\" | \"readwrite\";\n auth?: string;\n token?: string;\n options?: Record<string, unknown>;\n}\n\nexport interface MountListResult {\n mounts: MountEntry[];\n}\n\nexport interface MountCommandResult {\n success: boolean;\n message?: string;\n /** The normalized path (for display after successful add) */\n normalizedPath?: string;\n}\n\nexport interface MountValidateResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Check if a path looks like a remote Git URL\n * Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)\n */\nfunction isRemoteGitUrl(path: string): boolean {\n // SSH format: git@github.com:user/repo.git or user@host:path\n if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) {\n return true;\n }\n // Embedded protocol: https://github.com/user/repo.git\n if (/^(https?|ssh|git):\\/\\//.test(path)) {\n return true;\n }\n return false;\n}\n\n/**\n * Resolve relative paths in URI to absolute paths\n * Supports fs://, git://, sqlite://, json:// schemes\n * Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)\n */\nfunction resolveUriPath(uri: string, cwd: string): string {\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//);\n\n // No protocol prefix\n if (!schemeMatch) {\n // Check if it's a remote git URL (SSH format like git@github.com:user/repo.git)\n if (isRemoteGitUrl(uri)) {\n return `git://${uri}`;\n }\n // Treat as local filesystem path\n const absolutePath = isAbsolute(uri) ? uri : resolve(cwd, uri);\n return `fs://${absolutePath}`;\n }\n\n const scheme = schemeMatch[1];\n const pathPart = uri.slice(schemeMatch[0].length);\n\n // Only resolve for local file-based schemes\n if (![\"fs\", \"git\", \"sqlite\", \"json\"].includes(scheme!)) {\n return uri;\n }\n\n // For git:// scheme, check if the path is a remote URL (not a local path)\n if (scheme === \"git\" && isRemoteGitUrl(pathPart)) {\n return uri;\n }\n\n // If path is already absolute, return as-is\n if (isAbsolute(pathPart)) {\n return uri;\n }\n\n // Resolve relative path against cwd\n const absolutePath = resolve(cwd, pathPart);\n return `${scheme}://${absolutePath}`;\n}\n\n/**\n * List all mounts from config (merged from all config layers)\n */\nexport async function mountListCommand(cwd: string): Promise<MountListResult> {\n const loader = new ConfigLoader();\n const config = await loader.load(cwd);\n return {\n mounts: config.mounts as MountEntry[],\n };\n}\n\n/**\n * Add a mount to config\n */\nexport async function mountAddCommand(\n cwd: string,\n path: string,\n uri: string,\n options: { description?: string; token?: string } = {},\n): Promise<MountCommandResult> {\n // Check for empty URI before resolving (resolveUriPath would transform empty string)\n if (!uri || uri.trim() === \"\") {\n return {\n success: false,\n message: \"URI is required\",\n };\n }\n\n // Resolve relative paths in URI to absolute paths\n const resolvedUri = resolveUriPath(uri, cwd);\n\n // Validate and normalize inputs using schema\n // The schema transforms path (normalizes it) and validates all fields\n const validation = MountSchema.safeParse({\n path,\n uri: resolvedUri,\n ...options,\n });\n if (!validation.success) {\n const errors = validation.error.errors.map((e) => e.message).join(\"; \");\n return {\n success: false,\n message: errors,\n };\n }\n\n // Build mount entry with validated/normalized path and resolved URI\n const newMount: MountEntry = {\n path: validation.data.path, // Use normalized path from schema\n uri: validation.data.uri,\n ...(options.description && { description: options.description }),\n ...(options.token && { token: options.token }),\n };\n\n const configDir = join(cwd, CONFIG_DIR_NAME);\n const configPath = join(configDir, CONFIG_FILE_NAME);\n\n // Load existing config or create new\n const config: { mounts: MountEntry[] } = { mounts: [] };\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const parsed = parse(content) as { mounts?: MountEntry[] };\n config.mounts = parsed.mounts ?? [];\n } catch {\n // Config doesn't exist, will create\n }\n\n // Check for duplicate path (use normalized path for comparison)\n const normalizedPath = validation.data.path;\n if (config.mounts.some((m) => m.path === normalizedPath)) {\n return {\n success: false,\n message: `Mount path \"${normalizedPath}\" already exists`,\n };\n }\n\n // Add validated mount\n config.mounts.push(newMount);\n\n // Ensure config directory exists\n try {\n await mkdir(configDir, { recursive: true });\n } catch {\n // Directory might already exist\n }\n\n // Write config\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n normalizedPath,\n };\n}\n\n/**\n * Remove a mount from config\n */\nexport async function mountRemoveCommand(cwd: string, path: string): Promise<MountCommandResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n const index = mounts.findIndex((m) => m.path === path);\n if (index === -1) {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n\n mounts.splice(index, 1);\n config.mounts = mounts;\n\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n };\n } catch {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n}\n\n/**\n * Validate mount configuration\n */\nexport async function mountValidateCommand(cwd: string): Promise<MountValidateResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n const errors: string[] = [];\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n for (const mount of mounts) {\n // Validate mount using schema\n const validation = MountSchema.safeParse(mount);\n if (!validation.success) {\n for (const err of validation.error.errors) {\n errors.push(`Mount \"${mount.path}\": ${err.message}`);\n }\n continue;\n }\n\n // For fs:// URIs, check if target exists\n if (mount.uri.startsWith(\"fs://\")) {\n const targetPath = mount.uri.replace(\"fs://\", \"\");\n try {\n await access(targetPath);\n } catch {\n errors.push(`Mount target \"${targetPath}\" does not exist`);\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n } catch {\n return {\n valid: true,\n errors: [],\n };\n }\n}\n\n/**\n * Format mount list output for different views\n */\nexport function formatMountListOutput(mounts: MountEntry[], view: ViewType): string {\n switch (view) {\n case \"json\":\n return JSON.stringify({ mounts }, null, 2);\n case \"llm\":\n return formatLlm(mounts);\n case \"human\":\n return formatHuman(mounts);\n default:\n return formatDefault(mounts);\n }\n}\n\nfunction formatDefault(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"No mounts configured\";\n }\n\n return mounts\n .map((m) => {\n const desc = m.description ? ` (${m.description})` : \"\";\n return `${m.path} → ${m.uri}${desc}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatLlm(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"NO_MOUNTS\";\n }\n\n return mounts\n .map((m) => {\n const lines = [`MOUNT ${m.path}`, `URI=${m.uri}`];\n if (m.description) {\n lines.push(`DESC=${m.description}`);\n }\n return lines.join(\"\\n\");\n })\n .join(\"\\n\\n\");\n}\n\nfunction formatHuman(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return colors.dim(\"No mounts configured.\");\n }\n\n const lines = [colors.bold(\"Configured Mounts:\"), \"\"];\n for (const m of mounts) {\n lines.push(` ${colors.cyan(m.path)}`);\n lines.push(` ${colors.dim(\"URI:\")} ${m.uri}`);\n if (m.description) {\n lines.push(` ${colors.dim(\"Description:\")} ${m.description}`);\n }\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n"],"mappings":"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"}
@@ -44,6 +44,7 @@ async function serveCommand(options = {}) {
44
44
  const readonly = options.readonly ?? serveConfig.readonly ?? false;
45
45
  const cors = options.cors ?? serveConfig.cors ?? false;
46
46
  const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;
47
+ const token = options.token ?? serveConfig.token;
47
48
  const runtime = await require_runtime.createRuntime();
48
49
  const mounts = config.mounts.map((m) => ({
49
50
  path: m.path,
@@ -51,7 +52,8 @@ async function serveCommand(options = {}) {
51
52
  }));
52
53
  const handler = (0, _aigne_afs_http.createAFSHttpHandler)({
53
54
  module: createRuntimeModule(runtime, readonly),
54
- maxBodySize
55
+ maxBodySize,
56
+ token
55
57
  });
56
58
  const server = (0, node_http.createServer)(async (req, res) => {
57
59
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -43,6 +43,7 @@ async function serveCommand(options = {}) {
43
43
  const readonly = options.readonly ?? serveConfig.readonly ?? false;
44
44
  const cors = options.cors ?? serveConfig.cors ?? false;
45
45
  const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;
46
+ const token = options.token ?? serveConfig.token;
46
47
  const runtime = await createRuntime();
47
48
  const mounts = config.mounts.map((m) => ({
48
49
  path: m.path,
@@ -50,7 +51,8 @@ async function serveCommand(options = {}) {
50
51
  }));
51
52
  const handler = createAFSHttpHandler({
52
53
  module: createRuntimeModule(runtime, readonly),
53
- maxBodySize
54
+ maxBodySize,
55
+ token
54
56
  });
55
57
  const server = createServer(async (req, res) => {
56
58
  const url = new URL(req.url || "/", `http://${req.headers.host}`);
@@ -1 +1 @@
1
- {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/commands/serve.ts"],"sourcesContent":["/**\n * AFS Serve Command\n *\n * Starts an HTTP server to expose AFS providers over HTTP transport\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { createServer } from \"node:http\";\nimport type { AFSModule } from \"@aigne/afs\";\nimport { createAFSHttpHandler } from \"@aigne/afs-http\";\nimport { ConfigLoader } from \"../config/loader.js\";\nimport type { MountConfig, ServeConfig } from \"../config/schema.js\";\nimport { type AFSRuntime, createRuntime } from \"../runtime.js\";\nimport { colors } from \"../ui/index.js\";\n\nexport interface ServeOptions {\n host?: string;\n port?: number;\n path?: string;\n readonly?: boolean;\n cors?: boolean;\n maxBodySize?: number;\n}\n\nexport interface ServeResult {\n success: boolean;\n host: string;\n port: number;\n path: string;\n url: string;\n mounts: Array<{ path: string; provider: string }>;\n}\n\n/**\n * Create an AFSModule wrapper around AFSRuntime\n */\nfunction createRuntimeModule(runtime: AFSRuntime, readonly: boolean): AFSModule {\n return {\n name: \"afs-server\",\n accessMode: readonly ? \"readonly\" : \"readwrite\",\n async list(path, options) {\n return runtime.list(path, options);\n },\n async read(path, options) {\n return runtime.read(path, options);\n },\n async write(path, content, options) {\n if (readonly) {\n throw new Error(\"Server is in readonly mode\");\n }\n return runtime.write(path, content, options);\n },\n async delete(path, options) {\n if (readonly) {\n throw new Error(\"Server is in readonly mode\");\n }\n return runtime.delete(path, options);\n },\n async search(path, query, options) {\n return runtime.search(path, query, options);\n },\n };\n}\n\n/**\n * Start HTTP server to expose AFS providers\n */\nexport async function serveCommand(options: ServeOptions = {}): Promise<ServeResult> {\n // Load config to get mount information and serve defaults\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n\n // Get serve config from config file (may be partial or undefined)\n const serveConfig: Partial<ServeConfig> = config.serve ?? {};\n\n // Merge: command line > config file > defaults\n const host = options.host ?? serveConfig.host ?? \"localhost\";\n const port = options.port ?? serveConfig.port ?? 3000;\n const basePath = options.path ?? serveConfig.path ?? \"/afs\";\n const readonly = options.readonly ?? serveConfig.readonly ?? false;\n const cors = options.cors ?? serveConfig.cors ?? false;\n const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;\n\n // Create runtime and load all mounts\n const runtime = await createRuntime();\n const mounts: Array<{ path: string; provider: string }> = config.mounts.map((m: MountConfig) => ({\n path: m.path,\n provider: m.uri,\n }));\n\n // Create AFSModule wrapper\n const module = createRuntimeModule(runtime, readonly);\n\n // Create HTTP handler\n const handler = createAFSHttpHandler({\n module,\n maxBodySize,\n });\n\n // Create HTTP server with path routing\n const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || \"/\", `http://${req.headers.host}`);\n\n // Log request (default view)\n const timestamp = new Date().toISOString();\n console.error(`${timestamp} ${req.method} ${url.pathname}`);\n\n // CORS support\n if (cors) {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n }\n\n // Check if request is for our base path\n if (!url.pathname.startsWith(basePath)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ code: 1, error: \"Not found\" }));\n return;\n }\n\n // Convert Node.js request to Web Standard Request\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n headers.append(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n }\n\n // Collect request body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk);\n }\n const body = Buffer.concat(chunks);\n\n const request = new Request(`http://${req.headers.host}${req.url}`, {\n method: req.method,\n headers,\n body: body.length > 0 ? body : undefined,\n });\n\n try {\n // Call handler\n const response = await handler(request);\n\n // Copy response to Node.js response\n res.writeHead(response.status, {\n \"Content-Type\": response.headers.get(\"Content-Type\") || \"application/json\",\n });\n\n const responseBody = await response.text();\n res.end(responseBody);\n\n // Log response\n console.error(`${timestamp} ${req.method} ${url.pathname} status=${response.status}`);\n } catch (error) {\n console.error(`Error handling request:`, error);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ code: 5, error: \"Internal server error\" }));\n }\n });\n\n // Start server\n await new Promise<void>((resolve, reject) => {\n server.listen(port, host, () => {\n resolve();\n });\n server.on(\"error\", reject);\n });\n\n // Handle graceful shutdown\n const shutdown = () => {\n console.error(\"\\nShutting down server...\");\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n const url = `http://${host}:${port}${basePath}`;\n\n return {\n success: true,\n host,\n port,\n path: basePath,\n url,\n mounts,\n };\n}\n\n/**\n * Format serve result for output\n */\nexport function formatServeOutput(result: ServeResult): string {\n const lines: string[] = [];\n\n lines.push(colors.green(\"AFS HTTP Server starting...\"));\n lines.push(colors.bold(\"Mounted providers:\"));\n\n for (const mount of result.mounts) {\n lines.push(` ${colors.cyan(mount.path.padEnd(20))} ${colors.dim(mount.provider)}`);\n }\n\n lines.push(\"\");\n lines.push(`${colors.dim(\"Listening on:\")} ${colors.brightCyan(result.url)}`);\n lines.push(colors.dim(\"Press Ctrl+C to stop\"));\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"serve.mjs","names":[],"sources":["../../src/commands/serve.ts"],"sourcesContent":["/**\n * AFS Serve Command\n *\n * Starts an HTTP server to expose AFS providers over HTTP transport\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { createServer } from \"node:http\";\nimport type { AFSModule } from \"@aigne/afs\";\nimport { createAFSHttpHandler } from \"@aigne/afs-http\";\nimport { ConfigLoader } from \"../config/loader.js\";\nimport type { MountConfig, ServeConfig } from \"../config/schema.js\";\nimport { type AFSRuntime, createRuntime } from \"../runtime.js\";\nimport { colors } from \"../ui/index.js\";\n\nexport interface ServeOptions {\n host?: string;\n port?: number;\n path?: string;\n readonly?: boolean;\n cors?: boolean;\n maxBodySize?: number;\n token?: string;\n}\n\nexport interface ServeResult {\n success: boolean;\n host: string;\n port: number;\n path: string;\n url: string;\n mounts: Array<{ path: string; provider: string }>;\n}\n\n/**\n * Create an AFSModule wrapper around AFSRuntime\n */\nfunction createRuntimeModule(runtime: AFSRuntime, readonly: boolean): AFSModule {\n return {\n name: \"afs-server\",\n accessMode: readonly ? \"readonly\" : \"readwrite\",\n async list(path, options) {\n return runtime.list(path, options);\n },\n async read(path, options) {\n return runtime.read(path, options);\n },\n async write(path, content, options) {\n if (readonly) {\n throw new Error(\"Server is in readonly mode\");\n }\n return runtime.write(path, content, options);\n },\n async delete(path, options) {\n if (readonly) {\n throw new Error(\"Server is in readonly mode\");\n }\n return runtime.delete(path, options);\n },\n async search(path, query, options) {\n return runtime.search(path, query, options);\n },\n };\n}\n\n/**\n * Start HTTP server to expose AFS providers\n */\nexport async function serveCommand(options: ServeOptions = {}): Promise<ServeResult> {\n // Load config to get mount information and serve defaults\n const configLoader = new ConfigLoader();\n const config = await configLoader.load(process.cwd());\n\n // Get serve config from config file (may be partial or undefined)\n const serveConfig: Partial<ServeConfig> = config.serve ?? {};\n\n // Merge: command line > config file > defaults\n const host = options.host ?? serveConfig.host ?? \"localhost\";\n const port = options.port ?? serveConfig.port ?? 3000;\n const basePath = options.path ?? serveConfig.path ?? \"/afs\";\n const readonly = options.readonly ?? serveConfig.readonly ?? false;\n const cors = options.cors ?? serveConfig.cors ?? false;\n const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;\n const token = options.token ?? serveConfig.token;\n\n // Create runtime and load all mounts\n const runtime = await createRuntime();\n const mounts: Array<{ path: string; provider: string }> = config.mounts.map((m: MountConfig) => ({\n path: m.path,\n provider: m.uri,\n }));\n\n // Create AFSModule wrapper\n const module = createRuntimeModule(runtime, readonly);\n\n // Create HTTP handler\n const handler = createAFSHttpHandler({\n module,\n maxBodySize,\n token,\n });\n\n // Create HTTP server with path routing\n const server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const url = new URL(req.url || \"/\", `http://${req.headers.host}`);\n\n // Log request (default view)\n const timestamp = new Date().toISOString();\n console.error(`${timestamp} ${req.method} ${url.pathname}`);\n\n // CORS support\n if (cors) {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\");\n\n if (req.method === \"OPTIONS\") {\n res.writeHead(204);\n res.end();\n return;\n }\n }\n\n // Check if request is for our base path\n if (!url.pathname.startsWith(basePath)) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ code: 1, error: \"Not found\" }));\n return;\n }\n\n // Convert Node.js request to Web Standard Request\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) {\n headers.append(key, Array.isArray(value) ? value.join(\", \") : value);\n }\n }\n\n // Collect request body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk);\n }\n const body = Buffer.concat(chunks);\n\n const request = new Request(`http://${req.headers.host}${req.url}`, {\n method: req.method,\n headers,\n body: body.length > 0 ? body : undefined,\n });\n\n try {\n // Call handler\n const response = await handler(request);\n\n // Copy response to Node.js response\n res.writeHead(response.status, {\n \"Content-Type\": response.headers.get(\"Content-Type\") || \"application/json\",\n });\n\n const responseBody = await response.text();\n res.end(responseBody);\n\n // Log response\n console.error(`${timestamp} ${req.method} ${url.pathname} status=${response.status}`);\n } catch (error) {\n console.error(`Error handling request:`, error);\n res.writeHead(500, { \"Content-Type\": \"application/json\" });\n res.end(JSON.stringify({ code: 5, error: \"Internal server error\" }));\n }\n });\n\n // Start server\n await new Promise<void>((resolve, reject) => {\n server.listen(port, host, () => {\n resolve();\n });\n server.on(\"error\", reject);\n });\n\n // Handle graceful shutdown\n const shutdown = () => {\n console.error(\"\\nShutting down server...\");\n server.close(() => {\n process.exit(0);\n });\n };\n\n process.on(\"SIGINT\", shutdown);\n process.on(\"SIGTERM\", shutdown);\n\n const url = `http://${host}:${port}${basePath}`;\n\n return {\n success: true,\n host,\n port,\n path: basePath,\n url,\n mounts,\n };\n}\n\n/**\n * Format serve result for output\n */\nexport function formatServeOutput(result: ServeResult): string {\n const lines: string[] = [];\n\n lines.push(colors.green(\"AFS HTTP Server starting...\"));\n lines.push(colors.bold(\"Mounted providers:\"));\n\n for (const mount of result.mounts) {\n lines.push(` ${colors.cyan(mount.path.padEnd(20))} ${colors.dim(mount.provider)}`);\n }\n\n lines.push(\"\");\n lines.push(`${colors.dim(\"Listening on:\")} ${colors.brightCyan(result.url)}`);\n lines.push(colors.dim(\"Press Ctrl+C to stop\"));\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -10,6 +10,11 @@ let node_os = require("node:os");
10
10
  const CONFIG_DIR_NAME = ".afs-config";
11
11
  const CONFIG_FILE_NAME = "config.toml";
12
12
  /**
13
+ * Environment variable to override user config directory.
14
+ * Useful for testing to isolate from real user config.
15
+ */
16
+ const AFS_USER_CONFIG_DIR_ENV = "AFS_USER_CONFIG_DIR";
17
+ /**
13
18
  * Loads and merges AFS configuration from multiple layers
14
19
  *
15
20
  * Layer priority (lowest to highest):
@@ -25,7 +30,7 @@ const CONFIG_FILE_NAME = "config.toml";
25
30
  var ConfigLoader = class {
26
31
  userConfigDir;
27
32
  constructor(options = {}) {
28
- this.userConfigDir = options.userConfigDir ?? (0, node_path.join)((0, node_os.homedir)(), CONFIG_DIR_NAME);
33
+ this.userConfigDir = options.userConfigDir ?? process.env[AFS_USER_CONFIG_DIR_ENV] ?? (0, node_path.join)((0, node_os.homedir)(), CONFIG_DIR_NAME);
29
34
  }
30
35
  /**
31
36
  * Load and merge configuration from all layers
@@ -55,7 +60,7 @@ var ConfigLoader = class {
55
60
  const userConfigPath = (0, node_path.join)(this.userConfigDir, CONFIG_FILE_NAME);
56
61
  if (await this.fileExists(userConfigPath)) paths.push(userConfigPath);
57
62
  const startDir = await this.findProjectRoot(cwd) ?? await this.findTopmostAfsDir(cwd) ?? cwd;
58
- const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd);
63
+ const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd, this.userConfigDir);
59
64
  paths.push(...intermediatePaths);
60
65
  return paths;
61
66
  }
@@ -76,8 +81,10 @@ var ConfigLoader = class {
76
81
  /**
77
82
  * Collect all config files from startDir to endDir (inclusive)
78
83
  * Returns paths in order from startDir to endDir (parent to child)
84
+ *
85
+ * @param excludeConfigDir - Optional config directory to exclude (to avoid duplicates)
79
86
  */
80
- async collectConfigsFromTo(startDir, endDir) {
87
+ async collectConfigsFromTo(startDir, endDir, excludeConfigDir) {
81
88
  const paths = [];
82
89
  const dirs = [];
83
90
  let current = endDir;
@@ -89,7 +96,9 @@ var ConfigLoader = class {
89
96
  current = parent;
90
97
  }
91
98
  for (const dir of dirs) {
92
- const configPath = (0, node_path.join)(dir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
99
+ const configDir = (0, node_path.join)(dir, CONFIG_DIR_NAME);
100
+ if (excludeConfigDir && configDir === excludeConfigDir) continue;
101
+ const configPath = (0, node_path.join)(configDir, CONFIG_FILE_NAME);
93
102
  if (await this.fileExists(configPath)) paths.push(configPath);
94
103
  }
95
104
  return paths;
@@ -121,22 +130,22 @@ var ConfigLoader = class {
121
130
  return `${namespace ?? ""}:${path}`;
122
131
  }
123
132
  /**
124
- * Merge multiple configs, checking for duplicate mount paths within same namespace
125
- * For serve config, later (more specific) configs override earlier ones
133
+ * Merge multiple configs with child configs overriding parent configs
134
+ * For both mounts and serve, later (more specific) configs override earlier ones
126
135
  */
127
136
  mergeConfigs(configs) {
137
+ const mountIndexByKey = /* @__PURE__ */ new Map();
128
138
  const allMounts = [];
129
- const seenPaths = /* @__PURE__ */ new Map();
130
139
  let mergedServe;
131
140
  for (const config of configs) {
132
141
  for (const mount of config.mounts) {
133
142
  const key = this.makeNamespacePathKey(mount.namespace, mount.path);
134
- if (seenPaths.has(key)) {
135
- const nsLabel = mount.namespace ? `namespace '${mount.namespace}'` : "default namespace";
136
- throw new Error(`Duplicate mount path "${mount.path}" in ${nsLabel} found in configuration. Mount paths must be unique within each namespace.`);
143
+ const existingIndex = mountIndexByKey.get(key);
144
+ if (existingIndex !== void 0) allMounts[existingIndex] = mount;
145
+ else {
146
+ mountIndexByKey.set(key, allMounts.length);
147
+ allMounts.push(mount);
137
148
  }
138
- seenPaths.set(key, mount.uri);
139
- allMounts.push(mount);
140
149
  }
141
150
  if (config.serve) mergedServe = mergedServe ? {
142
151
  ...mergedServe,
@@ -9,6 +9,11 @@ import { homedir } from "node:os";
9
9
  const CONFIG_DIR_NAME = ".afs-config";
10
10
  const CONFIG_FILE_NAME = "config.toml";
11
11
  /**
12
+ * Environment variable to override user config directory.
13
+ * Useful for testing to isolate from real user config.
14
+ */
15
+ const AFS_USER_CONFIG_DIR_ENV = "AFS_USER_CONFIG_DIR";
16
+ /**
12
17
  * Loads and merges AFS configuration from multiple layers
13
18
  *
14
19
  * Layer priority (lowest to highest):
@@ -24,7 +29,7 @@ const CONFIG_FILE_NAME = "config.toml";
24
29
  var ConfigLoader = class {
25
30
  userConfigDir;
26
31
  constructor(options = {}) {
27
- this.userConfigDir = options.userConfigDir ?? join(homedir(), CONFIG_DIR_NAME);
32
+ this.userConfigDir = options.userConfigDir ?? process.env[AFS_USER_CONFIG_DIR_ENV] ?? join(homedir(), CONFIG_DIR_NAME);
28
33
  }
29
34
  /**
30
35
  * Load and merge configuration from all layers
@@ -54,7 +59,7 @@ var ConfigLoader = class {
54
59
  const userConfigPath = join(this.userConfigDir, CONFIG_FILE_NAME);
55
60
  if (await this.fileExists(userConfigPath)) paths.push(userConfigPath);
56
61
  const startDir = await this.findProjectRoot(cwd) ?? await this.findTopmostAfsDir(cwd) ?? cwd;
57
- const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd);
62
+ const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd, this.userConfigDir);
58
63
  paths.push(...intermediatePaths);
59
64
  return paths;
60
65
  }
@@ -75,8 +80,10 @@ var ConfigLoader = class {
75
80
  /**
76
81
  * Collect all config files from startDir to endDir (inclusive)
77
82
  * Returns paths in order from startDir to endDir (parent to child)
83
+ *
84
+ * @param excludeConfigDir - Optional config directory to exclude (to avoid duplicates)
78
85
  */
79
- async collectConfigsFromTo(startDir, endDir) {
86
+ async collectConfigsFromTo(startDir, endDir, excludeConfigDir) {
80
87
  const paths = [];
81
88
  const dirs = [];
82
89
  let current = endDir;
@@ -88,7 +95,9 @@ var ConfigLoader = class {
88
95
  current = parent;
89
96
  }
90
97
  for (const dir of dirs) {
91
- const configPath = join(dir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
98
+ const configDir = join(dir, CONFIG_DIR_NAME);
99
+ if (excludeConfigDir && configDir === excludeConfigDir) continue;
100
+ const configPath = join(configDir, CONFIG_FILE_NAME);
92
101
  if (await this.fileExists(configPath)) paths.push(configPath);
93
102
  }
94
103
  return paths;
@@ -120,22 +129,22 @@ var ConfigLoader = class {
120
129
  return `${namespace ?? ""}:${path}`;
121
130
  }
122
131
  /**
123
- * Merge multiple configs, checking for duplicate mount paths within same namespace
124
- * For serve config, later (more specific) configs override earlier ones
132
+ * Merge multiple configs with child configs overriding parent configs
133
+ * For both mounts and serve, later (more specific) configs override earlier ones
125
134
  */
126
135
  mergeConfigs(configs) {
136
+ const mountIndexByKey = /* @__PURE__ */ new Map();
127
137
  const allMounts = [];
128
- const seenPaths = /* @__PURE__ */ new Map();
129
138
  let mergedServe;
130
139
  for (const config of configs) {
131
140
  for (const mount of config.mounts) {
132
141
  const key = this.makeNamespacePathKey(mount.namespace, mount.path);
133
- if (seenPaths.has(key)) {
134
- const nsLabel = mount.namespace ? `namespace '${mount.namespace}'` : "default namespace";
135
- throw new Error(`Duplicate mount path "${mount.path}" in ${nsLabel} found in configuration. Mount paths must be unique within each namespace.`);
142
+ const existingIndex = mountIndexByKey.get(key);
143
+ if (existingIndex !== void 0) allMounts[existingIndex] = mount;
144
+ else {
145
+ mountIndexByKey.set(key, allMounts.length);
146
+ allMounts.push(mount);
136
147
  }
137
- seenPaths.set(key, mount.uri);
138
- allMounts.push(mount);
139
148
  }
140
149
  if (config.serve) mergedServe = mergedServe ? {
141
150
  ...mergedServe,
@@ -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 * 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 this.userConfigDir = options.userConfigDir ?? 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 const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd);\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 private async collectConfigsFromTo(startDir: string, endDir: string): 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 configPath = join(dir, CONFIG_DIR_NAME, 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\n const resolved = resolveEnvVarsInObject(parsed);\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, checking for duplicate mount paths within same namespace\n * For serve config, later (more specific) configs override earlier ones\n */\n private mergeConfigs(configs: AFSConfig[]): AFSConfig {\n const allMounts: MountConfig[] = [];\n // key = \"namespace:path\", value = uri for error message\n const seenPaths = new Map<string, string>();\n let mergedServe: ServeConfig | undefined;\n\n for (const config of configs) {\n // Merge mounts\n for (const mount of config.mounts) {\n const key = this.makeNamespacePathKey(mount.namespace, mount.path);\n if (seenPaths.has(key)) {\n const nsLabel = mount.namespace ? `namespace '${mount.namespace}'` : \"default namespace\";\n throw new Error(\n `Duplicate mount path \"${mount.path}\" in ${nsLabel} found in configuration. ` +\n `Mount paths must be unique within each namespace.`,\n );\n }\n seenPaths.set(key, mount.uri);\n allMounts.push(mount);\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\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\n const resolved = resolveEnvVarsInObject(parsed);\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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"}
@@ -84,6 +84,7 @@ async function createHttpProvider(mount) {
84
84
  name: mount.path.slice(1).replace(/\//g, "-") || "http",
85
85
  description: mount.description,
86
86
  accessMode: mount.access_mode,
87
+ token: mount.token,
87
88
  ...mount.options
88
89
  });
89
90
  }
@@ -84,6 +84,7 @@ async function createHttpProvider(mount) {
84
84
  name: mount.path.slice(1).replace(/\//g, "-") || "http",
85
85
  description: mount.description,
86
86
  accessMode: mount.access_mode,
87
+ token: mount.token,
87
88
  ...mount.options
88
89
  });
89
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"provider-factory.mjs","names":[],"sources":["../../src/config/provider-factory.ts"],"sourcesContent":["import type { AFSModule } from \"@aigne/afs\";\nimport type { MountConfig } from \"./schema.js\";\nimport { parseURI } from \"./uri-parser.js\";\n\n/**\n * Create an AFS provider from a mount configuration\n *\n * @param mount - Mount configuration with URI and options\n * @returns Provider instance\n * @throws Error if scheme is unknown or not implemented\n */\nexport async function createProvider(mount: MountConfig): Promise<AFSModule> {\n const parsed = parseURI(mount.uri);\n\n switch (parsed.scheme) {\n case \"fs\":\n return createAFSFSProvider(mount, parsed.path);\n\n case \"git\":\n return createGitProvider(mount, parsed.path, parsed.params, parsed.host);\n\n case \"sqlite\":\n return createSQLiteProvider(mount, parsed.path);\n\n case \"json\":\n return createJSONProvider(mount, parsed.path);\n\n case \"http\":\n case \"https\":\n return createHttpProvider(mount);\n\n default:\n throw new Error(`Unknown URI scheme: ${parsed.scheme}`);\n }\n}\n\nasync function createAFSFSProvider(mount: MountConfig, localPath: string): Promise<AFSModule> {\n const { AFSFS } = await import(\"@aigne/afs-fs\");\n\n return new AFSFS({\n localPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"fs\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createGitProvider(\n mount: MountConfig,\n repoPath: string,\n params: Record<string, string>,\n host?: string,\n): Promise<AFSModule> {\n const { AFSGit } = await import(\"@aigne/afs-git\");\n\n // For remote repos (SSH-style or https), use remoteUrl\n // For local repos, use repoPath directly\n if (host) {\n const remoteUrl = `git@${host}:${repoPath}`;\n return new AFSGit({\n remoteUrl,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n }\n\n // Check if repoPath is actually a remote URL (https:// or http://)\n if (repoPath.startsWith(\"https://\") || repoPath.startsWith(\"http://\")) {\n return new AFSGit({\n remoteUrl: repoPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n }\n\n return new AFSGit({\n repoPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n}\n\nasync function createSQLiteProvider(mount: MountConfig, dbPath: string): Promise<AFSModule> {\n const { SQLiteAFS } = await import(\"@aigne/afs-sqlite\");\n\n // SQLiteAFS auto-initializes via onMount when mounted to AFS\n return new SQLiteAFS({\n url: `file:${dbPath}`,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"sqlite\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createJSONProvider(mount: MountConfig, jsonPath: string): Promise<AFSModule> {\n const { AFSJSON } = await import(\"@aigne/afs-json\");\n\n return new AFSJSON({\n jsonPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"json\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createHttpProvider(mount: MountConfig): Promise<AFSModule> {\n const { AFSHttpClient } = await import(\"@aigne/afs-http\");\n\n return new AFSHttpClient({\n url: mount.uri,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"http\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n"],"mappings":";;;;;;;;;;AAWA,eAAsB,eAAe,OAAwC;CAC3E,MAAM,SAAS,SAAS,MAAM,IAAI;AAElC,SAAQ,OAAO,QAAf;EACE,KAAK,KACH,QAAO,oBAAoB,OAAO,OAAO,KAAK;EAEhD,KAAK,MACH,QAAO,kBAAkB,OAAO,OAAO,MAAM,OAAO,QAAQ,OAAO,KAAK;EAE1E,KAAK,SACH,QAAO,qBAAqB,OAAO,OAAO,KAAK;EAEjD,KAAK,OACH,QAAO,mBAAmB,OAAO,OAAO,KAAK;EAE/C,KAAK;EACL,KAAK,QACH,QAAO,mBAAmB,MAAM;EAElC,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,SAAS;;;AAI7D,eAAe,oBAAoB,OAAoB,WAAuC;CAC5F,MAAM,EAAE,UAAU,MAAM,OAAO;AAE/B,QAAO,IAAI,MAAM;EACf;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,kBACb,OACA,UACA,QACA,MACoB;CACpB,MAAM,EAAE,WAAW,MAAM,OAAO;AAIhC,KAAI,KAEF,QAAO,IAAI,OAAO;EAChB,WAFgB,OAAO,KAAK,GAAG;EAG/B,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;AAIJ,KAAI,SAAS,WAAW,WAAW,IAAI,SAAS,WAAW,UAAU,CACnE,QAAO,IAAI,OAAO;EAChB,WAAW;EACX,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;AAGJ,QAAO,IAAI,OAAO;EAChB;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,qBAAqB,OAAoB,QAAoC;CAC1F,MAAM,EAAE,cAAc,MAAM,OAAO;AAGnC,QAAO,IAAI,UAAU;EACnB,KAAK,QAAQ;EACb,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,mBAAmB,OAAoB,UAAsC;CAC1F,MAAM,EAAE,YAAY,MAAM,OAAO;AAEjC,QAAO,IAAI,QAAQ;EACjB;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,mBAAmB,OAAwC;CACxE,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cAAc;EACvB,KAAK,MAAM;EACX,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC"}
1
+ {"version":3,"file":"provider-factory.mjs","names":[],"sources":["../../src/config/provider-factory.ts"],"sourcesContent":["import type { AFSModule } from \"@aigne/afs\";\nimport type { MountConfig } from \"./schema.js\";\nimport { parseURI } from \"./uri-parser.js\";\n\n/**\n * Create an AFS provider from a mount configuration\n *\n * @param mount - Mount configuration with URI and options\n * @returns Provider instance\n * @throws Error if scheme is unknown or not implemented\n */\nexport async function createProvider(mount: MountConfig): Promise<AFSModule> {\n const parsed = parseURI(mount.uri);\n\n switch (parsed.scheme) {\n case \"fs\":\n return createAFSFSProvider(mount, parsed.path);\n\n case \"git\":\n return createGitProvider(mount, parsed.path, parsed.params, parsed.host);\n\n case \"sqlite\":\n return createSQLiteProvider(mount, parsed.path);\n\n case \"json\":\n return createJSONProvider(mount, parsed.path);\n\n case \"http\":\n case \"https\":\n return createHttpProvider(mount);\n\n default:\n throw new Error(`Unknown URI scheme: ${parsed.scheme}`);\n }\n}\n\nasync function createAFSFSProvider(mount: MountConfig, localPath: string): Promise<AFSModule> {\n const { AFSFS } = await import(\"@aigne/afs-fs\");\n\n return new AFSFS({\n localPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"fs\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createGitProvider(\n mount: MountConfig,\n repoPath: string,\n params: Record<string, string>,\n host?: string,\n): Promise<AFSModule> {\n const { AFSGit } = await import(\"@aigne/afs-git\");\n\n // For remote repos (SSH-style or https), use remoteUrl\n // For local repos, use repoPath directly\n if (host) {\n const remoteUrl = `git@${host}:${repoPath}`;\n return new AFSGit({\n remoteUrl,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n }\n\n // Check if repoPath is actually a remote URL (https:// or http://)\n if (repoPath.startsWith(\"https://\") || repoPath.startsWith(\"http://\")) {\n return new AFSGit({\n remoteUrl: repoPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n }\n\n return new AFSGit({\n repoPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n}\n\nasync function createSQLiteProvider(mount: MountConfig, dbPath: string): Promise<AFSModule> {\n const { SQLiteAFS } = await import(\"@aigne/afs-sqlite\");\n\n // SQLiteAFS auto-initializes via onMount when mounted to AFS\n return new SQLiteAFS({\n url: `file:${dbPath}`,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"sqlite\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createJSONProvider(mount: MountConfig, jsonPath: string): Promise<AFSModule> {\n const { AFSJSON } = await import(\"@aigne/afs-json\");\n\n return new AFSJSON({\n jsonPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"json\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createHttpProvider(mount: MountConfig): Promise<AFSModule> {\n const { AFSHttpClient } = await import(\"@aigne/afs-http\");\n\n return new AFSHttpClient({\n url: mount.uri,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"http\",\n description: mount.description,\n accessMode: mount.access_mode,\n token: mount.token,\n ...mount.options,\n });\n}\n"],"mappings":";;;;;;;;;;AAWA,eAAsB,eAAe,OAAwC;CAC3E,MAAM,SAAS,SAAS,MAAM,IAAI;AAElC,SAAQ,OAAO,QAAf;EACE,KAAK,KACH,QAAO,oBAAoB,OAAO,OAAO,KAAK;EAEhD,KAAK,MACH,QAAO,kBAAkB,OAAO,OAAO,MAAM,OAAO,QAAQ,OAAO,KAAK;EAE1E,KAAK,SACH,QAAO,qBAAqB,OAAO,OAAO,KAAK;EAEjD,KAAK,OACH,QAAO,mBAAmB,OAAO,OAAO,KAAK;EAE/C,KAAK;EACL,KAAK,QACH,QAAO,mBAAmB,MAAM;EAElC,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,SAAS;;;AAI7D,eAAe,oBAAoB,OAAoB,WAAuC;CAC5F,MAAM,EAAE,UAAU,MAAM,OAAO;AAE/B,QAAO,IAAI,MAAM;EACf;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,kBACb,OACA,UACA,QACA,MACoB;CACpB,MAAM,EAAE,WAAW,MAAM,OAAO;AAIhC,KAAI,KAEF,QAAO,IAAI,OAAO;EAChB,WAFgB,OAAO,KAAK,GAAG;EAG/B,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;AAIJ,KAAI,SAAS,WAAW,WAAW,IAAI,SAAS,WAAW,UAAU,CACnE,QAAO,IAAI,OAAO;EAChB,WAAW;EACX,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;AAGJ,QAAO,IAAI,OAAO;EAChB;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,qBAAqB,OAAoB,QAAoC;CAC1F,MAAM,EAAE,cAAc,MAAM,OAAO;AAGnC,QAAO,IAAI,UAAU;EACnB,KAAK,QAAQ;EACb,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,mBAAmB,OAAoB,UAAsC;CAC1F,MAAM,EAAE,YAAY,MAAM,OAAO;AAEjC,QAAO,IAAI,QAAQ;EACjB;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,mBAAmB,OAAwC;CACxE,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cAAc;EACvB,KAAK,MAAM;EACX,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,OAAO,MAAM;EACb,GAAG,MAAM;EACV,CAAC"}
@@ -63,6 +63,7 @@ const MountSchema = zod.z.object({
63
63
  description: zod.z.string().optional(),
64
64
  access_mode: zod.z.enum(["readonly", "readwrite"]).optional(),
65
65
  auth: zod.z.string().optional(),
66
+ token: zod.z.string().optional(),
66
67
  options: zod.z.record(zod.z.unknown()).optional()
67
68
  });
68
69
  /**
@@ -74,7 +75,8 @@ const ServeSchema = zod.z.object({
74
75
  path: zod.z.string().default("/afs"),
75
76
  readonly: zod.z.boolean().default(false),
76
77
  cors: zod.z.boolean().default(false),
77
- max_body_size: zod.z.number().int().positive().default(10 * 1024 * 1024)
78
+ max_body_size: zod.z.number().int().positive().default(10 * 1024 * 1024),
79
+ token: zod.z.string().optional()
78
80
  });
79
81
  /**
80
82
  * Root configuration schema for afs.toml
@@ -62,6 +62,7 @@ const MountSchema = z.object({
62
62
  description: z.string().optional(),
63
63
  access_mode: z.enum(["readonly", "readwrite"]).optional(),
64
64
  auth: z.string().optional(),
65
+ token: z.string().optional(),
65
66
  options: z.record(z.unknown()).optional()
66
67
  });
67
68
  /**
@@ -73,7 +74,8 @@ const ServeSchema = z.object({
73
74
  path: z.string().default("/afs"),
74
75
  readonly: z.boolean().default(false),
75
76
  cors: z.boolean().default(false),
76
- max_body_size: z.number().int().positive().default(10 * 1024 * 1024)
77
+ max_body_size: z.number().int().positive().default(10 * 1024 * 1024),
78
+ token: z.string().optional()
77
79
  });
78
80
  /**
79
81
  * Root configuration schema for afs.toml
@@ -1 +1 @@
1
- {"version":3,"file":"schema.mjs","names":[],"sources":["../../src/config/schema.ts"],"sourcesContent":["import { AFSPathError, validatePath } from \"@aigne/afs\";\nimport { z } from \"zod\";\n\n/**\n * Characters forbidden in namespace names (security-sensitive)\n */\nconst NAMESPACE_FORBIDDEN_CHARS = [\n \"/\", // Path separator\n \"\\\\\", // Windows path separator\n \":\", // Namespace separator\n \";\", // Shell metachar\n \"|\", // Shell pipe\n \"&\", // Shell background\n \"`\", // Shell command substitution\n \"$\", // Shell variable\n \"(\", // Shell subshell\n \")\", // Shell subshell\n \">\", // Shell redirect\n \"<\", // Shell redirect\n \"\\n\", // Newline\n \"\\r\", // Carriage return\n \"\\t\", // Tab\n \"\\x00\", // NUL\n];\n\n/**\n * Validate namespace name\n */\nfunction validateNamespace(namespace: string): string {\n if (!namespace || namespace.trim() === \"\") {\n throw new Error(\"Namespace cannot be empty or whitespace-only\");\n }\n\n for (const char of NAMESPACE_FORBIDDEN_CHARS) {\n if (namespace.includes(char)) {\n throw new Error(`Namespace contains forbidden character: '${char}'`);\n }\n }\n\n return namespace;\n}\n\n/**\n * Mount configuration schema\n */\nexport const MountSchema = z.object({\n /** Mount path (must follow Unix path semantics) */\n path: z.string().transform((val, ctx) => {\n try {\n return validatePath(val);\n } catch (e) {\n const message = e instanceof AFSPathError ? e.message : \"Invalid path\";\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message,\n });\n return z.NEVER;\n }\n }),\n\n /** Provider URI (e.g., fs:///path, git:///repo?branch=main) */\n uri: z.string().min(1, \"URI is required\"),\n\n /** Namespace for this mount (optional, defaults to default namespace) */\n namespace: z\n .string()\n .transform((val, ctx) => {\n try {\n return validateNamespace(val);\n } catch (e) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: e instanceof Error ? e.message : \"Invalid namespace\",\n });\n return z.NEVER;\n }\n })\n .optional(),\n\n /** Human/LLM readable description */\n description: z.string().optional(),\n\n /** Access mode: readonly or readwrite */\n access_mode: z.enum([\"readonly\", \"readwrite\"]).optional(),\n\n /** Authentication string (supports ${ENV_VAR} references) */\n auth: z.string().optional(),\n\n /** Provider-specific options (passed through to provider) */\n options: z.record(z.unknown()).optional(),\n});\n\n/**\n * Serve configuration schema\n */\nexport const ServeSchema = z.object({\n /** Host address to listen on */\n host: z.string().default(\"localhost\"),\n\n /** Port to listen on */\n port: z.number().int().positive().default(3000),\n\n /** Base path for the server */\n path: z.string().default(\"/afs\"),\n\n /** Run in readonly mode (disable write operations) */\n readonly: z.boolean().default(false),\n\n /** Enable CORS support */\n cors: z.boolean().default(false),\n\n /** Maximum request body size in bytes */\n max_body_size: z\n .number()\n .int()\n .positive()\n .default(10 * 1024 * 1024), // 10MB\n});\n\n/**\n * Root configuration schema for afs.toml\n */\nexport const ConfigSchema = z.object({\n /** List of mount configurations */\n mounts: z.array(MountSchema).default([]),\n\n /** HTTP server configuration */\n serve: ServeSchema.optional(),\n});\n\n/** Type for a single mount configuration */\nexport type MountConfig = z.infer<typeof MountSchema>;\n\n/** Type for serve configuration */\nexport type ServeConfig = z.infer<typeof ServeSchema>;\n\n/** Type for the root AFS configuration */\nexport type AFSConfig = z.infer<typeof ConfigSchema>;\n"],"mappings":";;;;;;;AAMA,MAAM,4BAA4B;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;AAKD,SAAS,kBAAkB,WAA2B;AACpD,KAAI,CAAC,aAAa,UAAU,MAAM,KAAK,GACrC,OAAM,IAAI,MAAM,+CAA+C;AAGjE,MAAK,MAAM,QAAQ,0BACjB,KAAI,UAAU,SAAS,KAAK,CAC1B,OAAM,IAAI,MAAM,4CAA4C,KAAK,GAAG;AAIxE,QAAO;;;;;AAMT,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,WAAW,KAAK,QAAQ;AACvC,MAAI;AACF,UAAO,aAAa,IAAI;WACjB,GAAG;GACV,MAAM,UAAU,aAAa,eAAe,EAAE,UAAU;AACxD,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB;IACD,CAAC;AACF,UAAO,EAAE;;GAEX;CAGF,KAAK,EAAE,QAAQ,CAAC,IAAI,GAAG,kBAAkB;CAGzC,WAAW,EACR,QAAQ,CACR,WAAW,KAAK,QAAQ;AACvB,MAAI;AACF,UAAO,kBAAkB,IAAI;WACtB,GAAG;AACV,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,SAAS,aAAa,QAAQ,EAAE,UAAU;IAC3C,CAAC;AACF,UAAO,EAAE;;GAEX,CACD,UAAU;CAGb,aAAa,EAAE,QAAQ,CAAC,UAAU;CAGlC,aAAa,EAAE,KAAK,CAAC,YAAY,YAAY,CAAC,CAAC,UAAU;CAGzD,MAAM,EAAE,QAAQ,CAAC,UAAU;CAG3B,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,UAAU;CAC1C,CAAC;;;;AAKF,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,QAAQ,YAAY;CAGrC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,IAAK;CAG/C,MAAM,EAAE,QAAQ,CAAC,QAAQ,OAAO;CAGhC,UAAU,EAAE,SAAS,CAAC,QAAQ,MAAM;CAGpC,MAAM,EAAE,SAAS,CAAC,QAAQ,MAAM;CAGhC,eAAe,EACZ,QAAQ,CACR,KAAK,CACL,UAAU,CACV,QAAQ,KAAK,OAAO,KAAK;CAC7B,CAAC;;;;AAKF,MAAa,eAAe,EAAE,OAAO;CAEnC,QAAQ,EAAE,MAAM,YAAY,CAAC,QAAQ,EAAE,CAAC;CAGxC,OAAO,YAAY,UAAU;CAC9B,CAAC"}
1
+ {"version":3,"file":"schema.mjs","names":[],"sources":["../../src/config/schema.ts"],"sourcesContent":["import { AFSPathError, validatePath } from \"@aigne/afs\";\nimport { z } from \"zod\";\n\n/**\n * Characters forbidden in namespace names (security-sensitive)\n */\nconst NAMESPACE_FORBIDDEN_CHARS = [\n \"/\", // Path separator\n \"\\\\\", // Windows path separator\n \":\", // Namespace separator\n \";\", // Shell metachar\n \"|\", // Shell pipe\n \"&\", // Shell background\n \"`\", // Shell command substitution\n \"$\", // Shell variable\n \"(\", // Shell subshell\n \")\", // Shell subshell\n \">\", // Shell redirect\n \"<\", // Shell redirect\n \"\\n\", // Newline\n \"\\r\", // Carriage return\n \"\\t\", // Tab\n \"\\x00\", // NUL\n];\n\n/**\n * Validate namespace name\n */\nfunction validateNamespace(namespace: string): string {\n if (!namespace || namespace.trim() === \"\") {\n throw new Error(\"Namespace cannot be empty or whitespace-only\");\n }\n\n for (const char of NAMESPACE_FORBIDDEN_CHARS) {\n if (namespace.includes(char)) {\n throw new Error(`Namespace contains forbidden character: '${char}'`);\n }\n }\n\n return namespace;\n}\n\n/**\n * Mount configuration schema\n */\nexport const MountSchema = z.object({\n /** Mount path (must follow Unix path semantics) */\n path: z.string().transform((val, ctx) => {\n try {\n return validatePath(val);\n } catch (e) {\n const message = e instanceof AFSPathError ? e.message : \"Invalid path\";\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message,\n });\n return z.NEVER;\n }\n }),\n\n /** Provider URI (e.g., fs:///path, git:///repo?branch=main) */\n uri: z.string().min(1, \"URI is required\"),\n\n /** Namespace for this mount (optional, defaults to default namespace) */\n namespace: z\n .string()\n .transform((val, ctx) => {\n try {\n return validateNamespace(val);\n } catch (e) {\n ctx.addIssue({\n code: z.ZodIssueCode.custom,\n message: e instanceof Error ? e.message : \"Invalid namespace\",\n });\n return z.NEVER;\n }\n })\n .optional(),\n\n /** Human/LLM readable description */\n description: z.string().optional(),\n\n /** Access mode: readonly or readwrite */\n access_mode: z.enum([\"readonly\", \"readwrite\"]).optional(),\n\n /** Authentication string (supports ${ENV_VAR} references) */\n auth: z.string().optional(),\n\n /** Authorization token for HTTP providers (supports ${ENV_VAR} references) */\n token: z.string().optional(),\n\n /** Provider-specific options (passed through to provider) */\n options: z.record(z.unknown()).optional(),\n});\n\n/**\n * Serve configuration schema\n */\nexport const ServeSchema = z.object({\n /** Host address to listen on */\n host: z.string().default(\"localhost\"),\n\n /** Port to listen on */\n port: z.number().int().positive().default(3000),\n\n /** Base path for the server */\n path: z.string().default(\"/afs\"),\n\n /** Run in readonly mode (disable write operations) */\n readonly: z.boolean().default(false),\n\n /** Enable CORS support */\n cors: z.boolean().default(false),\n\n /** Maximum request body size in bytes */\n max_body_size: z\n .number()\n .int()\n .positive()\n .default(10 * 1024 * 1024), // 10MB\n\n /** Bearer token for authorization (supports ${ENV_VAR} references) */\n token: z.string().optional(),\n});\n\n/**\n * Root configuration schema for afs.toml\n */\nexport const ConfigSchema = z.object({\n /** List of mount configurations */\n mounts: z.array(MountSchema).default([]),\n\n /** HTTP server configuration */\n serve: ServeSchema.optional(),\n});\n\n/** Type for a single mount configuration */\nexport type MountConfig = z.infer<typeof MountSchema>;\n\n/** Type for serve configuration */\nexport type ServeConfig = z.infer<typeof ServeSchema>;\n\n/** Type for the root AFS configuration */\nexport type AFSConfig = z.infer<typeof ConfigSchema>;\n"],"mappings":";;;;;;;AAMA,MAAM,4BAA4B;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;AAKD,SAAS,kBAAkB,WAA2B;AACpD,KAAI,CAAC,aAAa,UAAU,MAAM,KAAK,GACrC,OAAM,IAAI,MAAM,+CAA+C;AAGjE,MAAK,MAAM,QAAQ,0BACjB,KAAI,UAAU,SAAS,KAAK,CAC1B,OAAM,IAAI,MAAM,4CAA4C,KAAK,GAAG;AAIxE,QAAO;;;;;AAMT,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,WAAW,KAAK,QAAQ;AACvC,MAAI;AACF,UAAO,aAAa,IAAI;WACjB,GAAG;GACV,MAAM,UAAU,aAAa,eAAe,EAAE,UAAU;AACxD,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB;IACD,CAAC;AACF,UAAO,EAAE;;GAEX;CAGF,KAAK,EAAE,QAAQ,CAAC,IAAI,GAAG,kBAAkB;CAGzC,WAAW,EACR,QAAQ,CACR,WAAW,KAAK,QAAQ;AACvB,MAAI;AACF,UAAO,kBAAkB,IAAI;WACtB,GAAG;AACV,OAAI,SAAS;IACX,MAAM,EAAE,aAAa;IACrB,SAAS,aAAa,QAAQ,EAAE,UAAU;IAC3C,CAAC;AACF,UAAO,EAAE;;GAEX,CACD,UAAU;CAGb,aAAa,EAAE,QAAQ,CAAC,UAAU;CAGlC,aAAa,EAAE,KAAK,CAAC,YAAY,YAAY,CAAC,CAAC,UAAU;CAGzD,MAAM,EAAE,QAAQ,CAAC,UAAU;CAG3B,OAAO,EAAE,QAAQ,CAAC,UAAU;CAG5B,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,UAAU;CAC1C,CAAC;;;;AAKF,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,QAAQ,YAAY;CAGrC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,IAAK;CAG/C,MAAM,EAAE,QAAQ,CAAC,QAAQ,OAAO;CAGhC,UAAU,EAAE,SAAS,CAAC,QAAQ,MAAM;CAGpC,MAAM,EAAE,SAAS,CAAC,QAAQ,MAAM;CAGhC,eAAe,EACZ,QAAQ,CACR,KAAK,CACL,UAAU,CACV,QAAQ,KAAK,OAAO,KAAK;CAG5B,OAAO,EAAE,QAAQ,CAAC,UAAU;CAC7B,CAAC;;;;AAKF,MAAa,eAAe,EAAE,OAAO;CAEnC,QAAQ,EAAE,MAAM,YAAY,CAAC,QAAQ,EAAE,CAAC;CAGxC,OAAO,YAAY,UAAU;CAC9B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aigne/afs-cli",
3
- "version": "1.11.0-beta.4",
3
+ "version": "1.11.0-beta.5",
4
4
  "description": "AFS Command Line Interface",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -44,12 +44,12 @@
44
44
  "smol-toml": "^1.3.1",
45
45
  "yargs": "^17.7.2",
46
46
  "zod": "^3.24.1",
47
- "@aigne/afs": "1.11.0-beta.4",
48
- "@aigne/afs-git": "1.11.0-beta.4",
49
- "@aigne/afs-json": "1.11.0-beta.4",
50
- "@aigne/afs-fs": "1.11.0-beta.4",
51
- "@aigne/afs-sqlite": "1.11.0-beta.4",
52
- "@aigne/afs-http": "1.11.0-beta.4"
47
+ "@aigne/afs": "1.11.0-beta.5",
48
+ "@aigne/afs-git": "1.11.0-beta.5",
49
+ "@aigne/afs-json": "1.11.0-beta.5",
50
+ "@aigne/afs-fs": "1.11.0-beta.5",
51
+ "@aigne/afs-sqlite": "1.11.0-beta.5",
52
+ "@aigne/afs-http": "1.11.0-beta.5"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/blessed": "^0.1.25",