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

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.
Files changed (75) hide show
  1. package/README.md +240 -12
  2. package/dist/cli.cjs +144 -26
  3. package/dist/cli.mjs +144 -26
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/commands/index.cjs +1 -0
  6. package/dist/commands/index.mjs +1 -0
  7. package/dist/commands/ls.cjs +12 -5
  8. package/dist/commands/ls.mjs +13 -5
  9. package/dist/commands/ls.mjs.map +1 -1
  10. package/dist/commands/mount.cjs +53 -17
  11. package/dist/commands/mount.mjs +53 -17
  12. package/dist/commands/mount.mjs.map +1 -1
  13. package/dist/commands/serve.cjs +142 -0
  14. package/dist/commands/serve.mjs +141 -0
  15. package/dist/commands/serve.mjs.map +1 -0
  16. package/dist/config/loader.cjs +28 -6
  17. package/dist/config/loader.mjs +28 -6
  18. package/dist/config/loader.mjs.map +1 -1
  19. package/dist/config/provider-factory.cjs +19 -1
  20. package/dist/config/provider-factory.mjs +19 -1
  21. package/dist/config/provider-factory.mjs.map +1 -1
  22. package/dist/config/schema.cjs +70 -3
  23. package/dist/config/schema.mjs +69 -3
  24. package/dist/config/schema.mjs.map +1 -1
  25. package/dist/config/uri-parser.cjs +17 -0
  26. package/dist/config/uri-parser.mjs +17 -0
  27. package/dist/config/uri-parser.mjs.map +1 -1
  28. package/dist/explorer/actions.cjs +246 -0
  29. package/dist/explorer/actions.mjs +240 -0
  30. package/dist/explorer/actions.mjs.map +1 -0
  31. package/dist/explorer/components/dialog.cjs +231 -0
  32. package/dist/explorer/components/dialog.mjs +232 -0
  33. package/dist/explorer/components/dialog.mjs.map +1 -0
  34. package/dist/explorer/components/file-list.cjs +107 -0
  35. package/dist/explorer/components/file-list.mjs +107 -0
  36. package/dist/explorer/components/file-list.mjs.map +1 -0
  37. package/dist/explorer/components/function-bar.cjs +55 -0
  38. package/dist/explorer/components/function-bar.mjs +55 -0
  39. package/dist/explorer/components/function-bar.mjs.map +1 -0
  40. package/dist/explorer/components/index.cjs +5 -0
  41. package/dist/explorer/components/index.mjs +7 -0
  42. package/dist/explorer/components/metadata-panel.cjs +122 -0
  43. package/dist/explorer/components/metadata-panel.mjs +122 -0
  44. package/dist/explorer/components/metadata-panel.mjs.map +1 -0
  45. package/dist/explorer/components/status-bar.cjs +53 -0
  46. package/dist/explorer/components/status-bar.mjs +54 -0
  47. package/dist/explorer/components/status-bar.mjs.map +1 -0
  48. package/dist/explorer/keybindings.cjs +214 -0
  49. package/dist/explorer/keybindings.mjs +213 -0
  50. package/dist/explorer/keybindings.mjs.map +1 -0
  51. package/dist/explorer/screen.cjs +200 -0
  52. package/dist/explorer/screen.mjs +199 -0
  53. package/dist/explorer/screen.mjs.map +1 -0
  54. package/dist/explorer/state.cjs +53 -0
  55. package/dist/explorer/state.mjs +53 -0
  56. package/dist/explorer/state.mjs.map +1 -0
  57. package/dist/explorer/theme.cjs +158 -0
  58. package/dist/explorer/theme.mjs +155 -0
  59. package/dist/explorer/theme.mjs.map +1 -0
  60. package/dist/path-utils.cjs +104 -0
  61. package/dist/path-utils.mjs +104 -0
  62. package/dist/path-utils.mjs.map +1 -0
  63. package/dist/runtime.cjs +47 -33
  64. package/dist/runtime.mjs +47 -33
  65. package/dist/runtime.mjs.map +1 -1
  66. package/dist/ui/header.cjs +60 -0
  67. package/dist/ui/header.mjs +59 -0
  68. package/dist/ui/header.mjs.map +1 -0
  69. package/dist/ui/index.cjs +17 -0
  70. package/dist/ui/index.mjs +15 -0
  71. package/dist/ui/index.mjs.map +1 -0
  72. package/dist/ui/terminal.cjs +97 -0
  73. package/dist/ui/terminal.mjs +95 -0
  74. package/dist/ui/terminal.mjs.map +1 -0
  75. package/package.json +9 -6
@@ -1,18 +1,32 @@
1
1
  const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ const require_schema = require('../config/schema.cjs');
2
3
  const require_loader = require('../config/loader.cjs');
4
+ const require_index = require('../ui/index.cjs');
3
5
  let node_fs_promises = require("node:fs/promises");
4
6
  let node_path = require("node:path");
5
7
  let smol_toml = require("smol-toml");
6
8
 
7
9
  //#region src/commands/mount.ts
8
10
  /**
11
+ * Check if a path looks like a remote Git URL
12
+ * Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)
13
+ */
14
+ function isRemoteGitUrl(path) {
15
+ if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) return true;
16
+ if (/^(https?|ssh|git):\/\//.test(path)) return true;
17
+ return false;
18
+ }
19
+ /**
9
20
  * Resolve relative paths in URI to absolute paths
10
21
  * Supports fs://, git://, sqlite://, json:// schemes
11
- * Paths without protocol prefix are treated as fs:// paths
22
+ * Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)
12
23
  */
13
24
  function resolveUriPath(uri, cwd) {
14
25
  const schemeMatch = uri.match(/^([a-z]+):\/\//);
15
- if (!schemeMatch) return `fs://${(0, node_path.isAbsolute)(uri) ? uri : (0, node_path.resolve)(cwd, uri)}`;
26
+ if (!schemeMatch) {
27
+ if (isRemoteGitUrl(uri)) return `git://${uri}`;
28
+ return `fs://${(0, node_path.isAbsolute)(uri) ? uri : (0, node_path.resolve)(cwd, uri)}`;
29
+ }
16
30
  const scheme = schemeMatch[1];
17
31
  const pathPart = uri.slice(schemeMatch[0].length);
18
32
  if (![
@@ -21,6 +35,7 @@ function resolveUriPath(uri, cwd) {
21
35
  "sqlite",
22
36
  "json"
23
37
  ].includes(scheme)) return uri;
38
+ if (scheme === "git" && isRemoteGitUrl(pathPart)) return uri;
24
39
  if ((0, node_path.isAbsolute)(pathPart)) return uri;
25
40
  return `${scheme}://${(0, node_path.resolve)(cwd, pathPart)}`;
26
41
  }
@@ -34,27 +49,45 @@ async function mountListCommand(cwd) {
34
49
  * Add a mount to config
35
50
  */
36
51
  async function mountAddCommand(cwd, path, uri, options = {}) {
52
+ if (!uri || uri.trim() === "") return {
53
+ success: false,
54
+ message: "URI is required"
55
+ };
56
+ const resolvedUri = resolveUriPath(uri, cwd);
57
+ const validation = require_schema.MountSchema.safeParse({
58
+ path,
59
+ uri: resolvedUri,
60
+ ...options
61
+ });
62
+ if (!validation.success) return {
63
+ success: false,
64
+ message: validation.error.errors.map((e) => e.message).join("; ")
65
+ };
66
+ const newMount = {
67
+ path: validation.data.path,
68
+ uri: validation.data.uri,
69
+ ...options.description && { description: options.description }
70
+ };
37
71
  const configDir = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME);
38
72
  const configPath = (0, node_path.join)(configDir, require_loader.CONFIG_FILE_NAME);
39
73
  const config = { mounts: [] };
40
74
  try {
41
75
  config.mounts = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8")).mounts ?? [];
42
76
  } catch {}
43
- if (config.mounts.some((m) => m.path === path)) return {
77
+ const normalizedPath = validation.data.path;
78
+ if (config.mounts.some((m) => m.path === normalizedPath)) return {
44
79
  success: false,
45
- message: `Mount path "${path}" already exists`
46
- };
47
- const newMount = {
48
- path,
49
- uri: resolveUriPath(uri, cwd),
50
- ...options.description && { description: options.description }
80
+ message: `Mount path "${normalizedPath}" already exists`
51
81
  };
52
82
  config.mounts.push(newMount);
53
83
  try {
54
84
  await (0, node_fs_promises.mkdir)(configDir, { recursive: true });
55
85
  } catch {}
56
86
  await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(config), "utf-8");
57
- return { success: true };
87
+ return {
88
+ success: true,
89
+ normalizedPath
90
+ };
58
91
  }
59
92
  /**
60
93
  * Remove a mount from config
@@ -89,8 +122,11 @@ async function mountValidateCommand(cwd) {
89
122
  try {
90
123
  const mounts = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8")).mounts ?? [];
91
124
  for (const mount of mounts) {
92
- if (!mount.path.startsWith("/")) errors.push(`Mount path "${mount.path}" must start with /`);
93
- if (!mount.uri || mount.uri.trim() === "") errors.push(`Mount at "${mount.path}" has empty URI`);
125
+ const validation = require_schema.MountSchema.safeParse(mount);
126
+ if (!validation.success) {
127
+ for (const err of validation.error.errors) errors.push(`Mount "${mount.path}": ${err.message}`);
128
+ continue;
129
+ }
94
130
  if (mount.uri.startsWith("fs://")) {
95
131
  const targetPath = mount.uri.replace("fs://", "");
96
132
  try {
@@ -138,12 +174,12 @@ function formatLlm(mounts) {
138
174
  }).join("\n\n");
139
175
  }
140
176
  function formatHuman(mounts) {
141
- if (mounts.length === 0) return "No mounts configured.";
142
- const lines = ["Configured Mounts:", ""];
177
+ if (mounts.length === 0) return require_index.colors.dim("No mounts configured.");
178
+ const lines = [require_index.colors.bold("Configured Mounts:"), ""];
143
179
  for (const m of mounts) {
144
- lines.push(` ${m.path}`);
145
- lines.push(` URI: ${m.uri}`);
146
- if (m.description) lines.push(` Description: ${m.description}`);
180
+ lines.push(` ${require_index.colors.cyan(m.path)}`);
181
+ lines.push(` ${require_index.colors.dim("URI:")} ${m.uri}`);
182
+ if (m.description) lines.push(` ${require_index.colors.dim("Description:")} ${m.description}`);
147
183
  lines.push("");
148
184
  }
149
185
  return lines.join("\n").trimEnd();
@@ -1,17 +1,31 @@
1
+ import { MountSchema } from "../config/schema.mjs";
1
2
  import { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader } from "../config/loader.mjs";
3
+ import { colors } from "../ui/index.mjs";
2
4
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
5
  import { isAbsolute, join, resolve } from "node:path";
4
6
  import { parse, stringify } from "smol-toml";
5
7
 
6
8
  //#region src/commands/mount.ts
7
9
  /**
10
+ * Check if a path looks like a remote Git URL
11
+ * Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)
12
+ */
13
+ function isRemoteGitUrl(path) {
14
+ if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) return true;
15
+ if (/^(https?|ssh|git):\/\//.test(path)) return true;
16
+ return false;
17
+ }
18
+ /**
8
19
  * Resolve relative paths in URI to absolute paths
9
20
  * Supports fs://, git://, sqlite://, json:// schemes
10
- * Paths without protocol prefix are treated as fs:// paths
21
+ * Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)
11
22
  */
12
23
  function resolveUriPath(uri, cwd) {
13
24
  const schemeMatch = uri.match(/^([a-z]+):\/\//);
14
- if (!schemeMatch) return `fs://${isAbsolute(uri) ? uri : resolve(cwd, uri)}`;
25
+ if (!schemeMatch) {
26
+ if (isRemoteGitUrl(uri)) return `git://${uri}`;
27
+ return `fs://${isAbsolute(uri) ? uri : resolve(cwd, uri)}`;
28
+ }
15
29
  const scheme = schemeMatch[1];
16
30
  const pathPart = uri.slice(schemeMatch[0].length);
17
31
  if (![
@@ -20,6 +34,7 @@ function resolveUriPath(uri, cwd) {
20
34
  "sqlite",
21
35
  "json"
22
36
  ].includes(scheme)) return uri;
37
+ if (scheme === "git" && isRemoteGitUrl(pathPart)) return uri;
23
38
  if (isAbsolute(pathPart)) return uri;
24
39
  return `${scheme}://${resolve(cwd, pathPart)}`;
25
40
  }
@@ -33,27 +48,45 @@ async function mountListCommand(cwd) {
33
48
  * Add a mount to config
34
49
  */
35
50
  async function mountAddCommand(cwd, path, uri, options = {}) {
51
+ if (!uri || uri.trim() === "") return {
52
+ success: false,
53
+ message: "URI is required"
54
+ };
55
+ const resolvedUri = resolveUriPath(uri, cwd);
56
+ const validation = MountSchema.safeParse({
57
+ path,
58
+ uri: resolvedUri,
59
+ ...options
60
+ });
61
+ if (!validation.success) return {
62
+ success: false,
63
+ message: validation.error.errors.map((e) => e.message).join("; ")
64
+ };
65
+ const newMount = {
66
+ path: validation.data.path,
67
+ uri: validation.data.uri,
68
+ ...options.description && { description: options.description }
69
+ };
36
70
  const configDir = join(cwd, CONFIG_DIR_NAME);
37
71
  const configPath = join(configDir, CONFIG_FILE_NAME);
38
72
  const config = { mounts: [] };
39
73
  try {
40
74
  config.mounts = parse(await readFile(configPath, "utf-8")).mounts ?? [];
41
75
  } catch {}
42
- if (config.mounts.some((m) => m.path === path)) return {
76
+ const normalizedPath = validation.data.path;
77
+ if (config.mounts.some((m) => m.path === normalizedPath)) return {
43
78
  success: false,
44
- message: `Mount path "${path}" already exists`
45
- };
46
- const newMount = {
47
- path,
48
- uri: resolveUriPath(uri, cwd),
49
- ...options.description && { description: options.description }
79
+ message: `Mount path "${normalizedPath}" already exists`
50
80
  };
51
81
  config.mounts.push(newMount);
52
82
  try {
53
83
  await mkdir(configDir, { recursive: true });
54
84
  } catch {}
55
85
  await writeFile(configPath, stringify(config), "utf-8");
56
- return { success: true };
86
+ return {
87
+ success: true,
88
+ normalizedPath
89
+ };
57
90
  }
58
91
  /**
59
92
  * Remove a mount from config
@@ -88,8 +121,11 @@ async function mountValidateCommand(cwd) {
88
121
  try {
89
122
  const mounts = parse(await readFile(configPath, "utf-8")).mounts ?? [];
90
123
  for (const mount of mounts) {
91
- if (!mount.path.startsWith("/")) errors.push(`Mount path "${mount.path}" must start with /`);
92
- if (!mount.uri || mount.uri.trim() === "") errors.push(`Mount at "${mount.path}" has empty URI`);
124
+ const validation = MountSchema.safeParse(mount);
125
+ if (!validation.success) {
126
+ for (const err of validation.error.errors) errors.push(`Mount "${mount.path}": ${err.message}`);
127
+ continue;
128
+ }
93
129
  if (mount.uri.startsWith("fs://")) {
94
130
  const targetPath = mount.uri.replace("fs://", "");
95
131
  try {
@@ -137,12 +173,12 @@ function formatLlm(mounts) {
137
173
  }).join("\n\n");
138
174
  }
139
175
  function formatHuman(mounts) {
140
- if (mounts.length === 0) return "No mounts configured.";
141
- const lines = ["Configured Mounts:", ""];
176
+ if (mounts.length === 0) return colors.dim("No mounts configured.");
177
+ const lines = [colors.bold("Configured Mounts:"), ""];
142
178
  for (const m of mounts) {
143
- lines.push(` ${m.path}`);
144
- lines.push(` URI: ${m.uri}`);
145
- if (m.description) lines.push(` Description: ${m.description}`);
179
+ lines.push(` ${colors.cyan(m.path)}`);
180
+ lines.push(` ${colors.dim("URI:")} ${m.uri}`);
181
+ if (m.description) lines.push(` ${colors.dim("Description:")} ${m.description}`);
146
182
  lines.push("");
147
183
  }
148
184
  return lines.join("\n").trimEnd();
@@ -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 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}\n\nexport interface MountValidateResult {\n valid: boolean;\n errors: string[];\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\n */\nfunction resolveUriPath(uri: string, cwd: string): string {\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//);\n\n // No protocol prefix - treat as local filesystem path\n if (!schemeMatch) {\n // If it's a relative path, resolve it; otherwise use as-is\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 // 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 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\n if (config.mounts.some((m) => m.path === path)) {\n return {\n success: false,\n message: `Mount path \"${path}\" already exists`,\n };\n }\n\n // Resolve relative paths in URI to absolute paths\n const resolvedUri = resolveUriPath(uri, cwd);\n\n // Add new mount\n const newMount: MountEntry = {\n path,\n uri: resolvedUri,\n ...(options.description && { description: options.description }),\n };\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 };\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 path starts with /\n if (!mount.path.startsWith(\"/\")) {\n errors.push(`Mount path \"${mount.path}\" must start with /`);\n }\n\n // Validate URI is not empty\n if (!mount.uri || mount.uri.trim() === \"\") {\n errors.push(`Mount at \"${mount.path}\" has empty URI`);\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 \"No mounts configured.\";\n }\n\n const lines = [\"Configured Mounts:\", \"\"];\n for (const m of mounts) {\n lines.push(` ${m.path}`);\n lines.push(` URI: ${m.uri}`);\n if (m.description) {\n lines.push(` 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 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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"}
@@ -0,0 +1,142 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ const require_loader = require('../config/loader.cjs');
3
+ const require_index = require('../ui/index.cjs');
4
+ const require_runtime = require('../runtime.cjs');
5
+ let node_http = require("node:http");
6
+ let _aigne_afs_http = require("@aigne/afs-http");
7
+
8
+ //#region src/commands/serve.ts
9
+ /**
10
+ * Create an AFSModule wrapper around AFSRuntime
11
+ */
12
+ function createRuntimeModule(runtime, readonly) {
13
+ return {
14
+ name: "afs-server",
15
+ accessMode: readonly ? "readonly" : "readwrite",
16
+ async list(path, options) {
17
+ return runtime.list(path, options);
18
+ },
19
+ async read(path, options) {
20
+ return runtime.read(path, options);
21
+ },
22
+ async write(path, content, options) {
23
+ if (readonly) throw new Error("Server is in readonly mode");
24
+ return runtime.write(path, content, options);
25
+ },
26
+ async delete(path, options) {
27
+ if (readonly) throw new Error("Server is in readonly mode");
28
+ return runtime.delete(path, options);
29
+ },
30
+ async search(path, query, options) {
31
+ return runtime.search(path, query, options);
32
+ }
33
+ };
34
+ }
35
+ /**
36
+ * Start HTTP server to expose AFS providers
37
+ */
38
+ async function serveCommand(options = {}) {
39
+ const config = await new require_loader.ConfigLoader().load(process.cwd());
40
+ const serveConfig = config.serve ?? {};
41
+ const host = options.host ?? serveConfig.host ?? "localhost";
42
+ const port = options.port ?? serveConfig.port ?? 3e3;
43
+ const basePath = options.path ?? serveConfig.path ?? "/afs";
44
+ const readonly = options.readonly ?? serveConfig.readonly ?? false;
45
+ const cors = options.cors ?? serveConfig.cors ?? false;
46
+ const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;
47
+ const runtime = await require_runtime.createRuntime();
48
+ const mounts = config.mounts.map((m) => ({
49
+ path: m.path,
50
+ provider: m.uri
51
+ }));
52
+ const handler = (0, _aigne_afs_http.createAFSHttpHandler)({
53
+ module: createRuntimeModule(runtime, readonly),
54
+ maxBodySize
55
+ });
56
+ const server = (0, node_http.createServer)(async (req, res) => {
57
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
58
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
59
+ console.error(`${timestamp} ${req.method} ${url.pathname}`);
60
+ if (cors) {
61
+ res.setHeader("Access-Control-Allow-Origin", "*");
62
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
63
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
64
+ if (req.method === "OPTIONS") {
65
+ res.writeHead(204);
66
+ res.end();
67
+ return;
68
+ }
69
+ }
70
+ if (!url.pathname.startsWith(basePath)) {
71
+ res.writeHead(404, { "Content-Type": "application/json" });
72
+ res.end(JSON.stringify({
73
+ code: 1,
74
+ error: "Not found"
75
+ }));
76
+ return;
77
+ }
78
+ const headers = new Headers();
79
+ for (const [key, value] of Object.entries(req.headers)) if (value) headers.append(key, Array.isArray(value) ? value.join(", ") : value);
80
+ const chunks = [];
81
+ for await (const chunk of req) chunks.push(chunk);
82
+ const body = Buffer.concat(chunks);
83
+ const request = new Request(`http://${req.headers.host}${req.url}`, {
84
+ method: req.method,
85
+ headers,
86
+ body: body.length > 0 ? body : void 0
87
+ });
88
+ try {
89
+ const response = await handler(request);
90
+ res.writeHead(response.status, { "Content-Type": response.headers.get("Content-Type") || "application/json" });
91
+ const responseBody = await response.text();
92
+ res.end(responseBody);
93
+ console.error(`${timestamp} ${req.method} ${url.pathname} status=${response.status}`);
94
+ } catch (error) {
95
+ console.error(`Error handling request:`, error);
96
+ res.writeHead(500, { "Content-Type": "application/json" });
97
+ res.end(JSON.stringify({
98
+ code: 5,
99
+ error: "Internal server error"
100
+ }));
101
+ }
102
+ });
103
+ await new Promise((resolve, reject) => {
104
+ server.listen(port, host, () => {
105
+ resolve();
106
+ });
107
+ server.on("error", reject);
108
+ });
109
+ const shutdown = () => {
110
+ console.error("\nShutting down server...");
111
+ server.close(() => {
112
+ process.exit(0);
113
+ });
114
+ };
115
+ process.on("SIGINT", shutdown);
116
+ process.on("SIGTERM", shutdown);
117
+ return {
118
+ success: true,
119
+ host,
120
+ port,
121
+ path: basePath,
122
+ url: `http://${host}:${port}${basePath}`,
123
+ mounts
124
+ };
125
+ }
126
+ /**
127
+ * Format serve result for output
128
+ */
129
+ function formatServeOutput(result) {
130
+ const lines = [];
131
+ lines.push(require_index.colors.green("AFS HTTP Server starting..."));
132
+ lines.push(require_index.colors.bold("Mounted providers:"));
133
+ for (const mount of result.mounts) lines.push(` ${require_index.colors.cyan(mount.path.padEnd(20))} ${require_index.colors.dim(mount.provider)}`);
134
+ lines.push("");
135
+ lines.push(`${require_index.colors.dim("Listening on:")} ${require_index.colors.brightCyan(result.url)}`);
136
+ lines.push(require_index.colors.dim("Press Ctrl+C to stop"));
137
+ return lines.join("\n");
138
+ }
139
+
140
+ //#endregion
141
+ exports.formatServeOutput = formatServeOutput;
142
+ exports.serveCommand = serveCommand;
@@ -0,0 +1,141 @@
1
+ import { ConfigLoader } from "../config/loader.mjs";
2
+ import { colors } from "../ui/index.mjs";
3
+ import { createRuntime } from "../runtime.mjs";
4
+ import { createServer } from "node:http";
5
+ import { createAFSHttpHandler } from "@aigne/afs-http";
6
+
7
+ //#region src/commands/serve.ts
8
+ /**
9
+ * Create an AFSModule wrapper around AFSRuntime
10
+ */
11
+ function createRuntimeModule(runtime, readonly) {
12
+ return {
13
+ name: "afs-server",
14
+ accessMode: readonly ? "readonly" : "readwrite",
15
+ async list(path, options) {
16
+ return runtime.list(path, options);
17
+ },
18
+ async read(path, options) {
19
+ return runtime.read(path, options);
20
+ },
21
+ async write(path, content, options) {
22
+ if (readonly) throw new Error("Server is in readonly mode");
23
+ return runtime.write(path, content, options);
24
+ },
25
+ async delete(path, options) {
26
+ if (readonly) throw new Error("Server is in readonly mode");
27
+ return runtime.delete(path, options);
28
+ },
29
+ async search(path, query, options) {
30
+ return runtime.search(path, query, options);
31
+ }
32
+ };
33
+ }
34
+ /**
35
+ * Start HTTP server to expose AFS providers
36
+ */
37
+ async function serveCommand(options = {}) {
38
+ const config = await new ConfigLoader().load(process.cwd());
39
+ const serveConfig = config.serve ?? {};
40
+ const host = options.host ?? serveConfig.host ?? "localhost";
41
+ const port = options.port ?? serveConfig.port ?? 3e3;
42
+ const basePath = options.path ?? serveConfig.path ?? "/afs";
43
+ const readonly = options.readonly ?? serveConfig.readonly ?? false;
44
+ const cors = options.cors ?? serveConfig.cors ?? false;
45
+ const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;
46
+ const runtime = await createRuntime();
47
+ const mounts = config.mounts.map((m) => ({
48
+ path: m.path,
49
+ provider: m.uri
50
+ }));
51
+ const handler = createAFSHttpHandler({
52
+ module: createRuntimeModule(runtime, readonly),
53
+ maxBodySize
54
+ });
55
+ const server = createServer(async (req, res) => {
56
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
57
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
58
+ console.error(`${timestamp} ${req.method} ${url.pathname}`);
59
+ if (cors) {
60
+ res.setHeader("Access-Control-Allow-Origin", "*");
61
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
62
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
63
+ if (req.method === "OPTIONS") {
64
+ res.writeHead(204);
65
+ res.end();
66
+ return;
67
+ }
68
+ }
69
+ if (!url.pathname.startsWith(basePath)) {
70
+ res.writeHead(404, { "Content-Type": "application/json" });
71
+ res.end(JSON.stringify({
72
+ code: 1,
73
+ error: "Not found"
74
+ }));
75
+ return;
76
+ }
77
+ const headers = new Headers();
78
+ for (const [key, value] of Object.entries(req.headers)) if (value) headers.append(key, Array.isArray(value) ? value.join(", ") : value);
79
+ const chunks = [];
80
+ for await (const chunk of req) chunks.push(chunk);
81
+ const body = Buffer.concat(chunks);
82
+ const request = new Request(`http://${req.headers.host}${req.url}`, {
83
+ method: req.method,
84
+ headers,
85
+ body: body.length > 0 ? body : void 0
86
+ });
87
+ try {
88
+ const response = await handler(request);
89
+ res.writeHead(response.status, { "Content-Type": response.headers.get("Content-Type") || "application/json" });
90
+ const responseBody = await response.text();
91
+ res.end(responseBody);
92
+ console.error(`${timestamp} ${req.method} ${url.pathname} status=${response.status}`);
93
+ } catch (error) {
94
+ console.error(`Error handling request:`, error);
95
+ res.writeHead(500, { "Content-Type": "application/json" });
96
+ res.end(JSON.stringify({
97
+ code: 5,
98
+ error: "Internal server error"
99
+ }));
100
+ }
101
+ });
102
+ await new Promise((resolve, reject) => {
103
+ server.listen(port, host, () => {
104
+ resolve();
105
+ });
106
+ server.on("error", reject);
107
+ });
108
+ const shutdown = () => {
109
+ console.error("\nShutting down server...");
110
+ server.close(() => {
111
+ process.exit(0);
112
+ });
113
+ };
114
+ process.on("SIGINT", shutdown);
115
+ process.on("SIGTERM", shutdown);
116
+ return {
117
+ success: true,
118
+ host,
119
+ port,
120
+ path: basePath,
121
+ url: `http://${host}:${port}${basePath}`,
122
+ mounts
123
+ };
124
+ }
125
+ /**
126
+ * Format serve result for output
127
+ */
128
+ function formatServeOutput(result) {
129
+ const lines = [];
130
+ lines.push(colors.green("AFS HTTP Server starting..."));
131
+ lines.push(colors.bold("Mounted providers:"));
132
+ for (const mount of result.mounts) lines.push(` ${colors.cyan(mount.path.padEnd(20))} ${colors.dim(mount.provider)}`);
133
+ lines.push("");
134
+ lines.push(`${colors.dim("Listening on:")} ${colors.brightCyan(result.url)}`);
135
+ lines.push(colors.dim("Press Ctrl+C to stop"));
136
+ return lines.join("\n");
137
+ }
138
+
139
+ //#endregion
140
+ export { formatServeOutput, serveCommand };
141
+ //# sourceMappingURL=serve.mjs.map
@@ -0,0 +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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"}
@@ -114,17 +114,39 @@ var ConfigLoader = class {
114
114
  return result.data;
115
115
  }
116
116
  /**
117
- * Merge multiple configs, checking for duplicate mount paths
117
+ * Create a composite key for namespace+path duplicate detection
118
+ * Uses empty string for undefined namespace (default namespace)
119
+ */
120
+ makeNamespacePathKey(namespace, path) {
121
+ return `${namespace ?? ""}:${path}`;
122
+ }
123
+ /**
124
+ * Merge multiple configs, checking for duplicate mount paths within same namespace
125
+ * For serve config, later (more specific) configs override earlier ones
118
126
  */
119
127
  mergeConfigs(configs) {
120
128
  const allMounts = [];
121
129
  const seenPaths = /* @__PURE__ */ new Map();
122
- for (const config of configs) for (const mount of config.mounts) {
123
- if (seenPaths.has(mount.path)) throw new Error(`Duplicate mount path "${mount.path}" found in configuration. Mount paths must be unique across all config files.`);
124
- seenPaths.set(mount.path, mount.uri);
125
- allMounts.push(mount);
130
+ let mergedServe;
131
+ for (const config of configs) {
132
+ for (const mount of config.mounts) {
133
+ 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.`);
137
+ }
138
+ seenPaths.set(key, mount.uri);
139
+ allMounts.push(mount);
140
+ }
141
+ if (config.serve) mergedServe = mergedServe ? {
142
+ ...mergedServe,
143
+ ...config.serve
144
+ } : config.serve;
126
145
  }
127
- return { mounts: allMounts };
146
+ return {
147
+ mounts: allMounts,
148
+ serve: mergedServe
149
+ };
128
150
  }
129
151
  /**
130
152
  * Find project root by looking for .git