@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.
- package/README.md +240 -12
- package/dist/cli.cjs +144 -26
- package/dist/cli.mjs +144 -26
- package/dist/cli.mjs.map +1 -1
- package/dist/commands/index.cjs +1 -0
- package/dist/commands/index.mjs +1 -0
- package/dist/commands/ls.cjs +12 -5
- package/dist/commands/ls.mjs +13 -5
- package/dist/commands/ls.mjs.map +1 -1
- package/dist/commands/mount.cjs +53 -17
- package/dist/commands/mount.mjs +53 -17
- package/dist/commands/mount.mjs.map +1 -1
- package/dist/commands/serve.cjs +142 -0
- package/dist/commands/serve.mjs +141 -0
- package/dist/commands/serve.mjs.map +1 -0
- package/dist/config/loader.cjs +28 -6
- package/dist/config/loader.mjs +28 -6
- package/dist/config/loader.mjs.map +1 -1
- package/dist/config/provider-factory.cjs +19 -1
- package/dist/config/provider-factory.mjs +19 -1
- package/dist/config/provider-factory.mjs.map +1 -1
- package/dist/config/schema.cjs +70 -3
- package/dist/config/schema.mjs +69 -3
- package/dist/config/schema.mjs.map +1 -1
- package/dist/config/uri-parser.cjs +17 -0
- package/dist/config/uri-parser.mjs +17 -0
- package/dist/config/uri-parser.mjs.map +1 -1
- package/dist/explorer/actions.cjs +246 -0
- package/dist/explorer/actions.mjs +240 -0
- package/dist/explorer/actions.mjs.map +1 -0
- package/dist/explorer/components/dialog.cjs +231 -0
- package/dist/explorer/components/dialog.mjs +232 -0
- package/dist/explorer/components/dialog.mjs.map +1 -0
- package/dist/explorer/components/file-list.cjs +107 -0
- package/dist/explorer/components/file-list.mjs +107 -0
- package/dist/explorer/components/file-list.mjs.map +1 -0
- package/dist/explorer/components/function-bar.cjs +55 -0
- package/dist/explorer/components/function-bar.mjs +55 -0
- package/dist/explorer/components/function-bar.mjs.map +1 -0
- package/dist/explorer/components/index.cjs +5 -0
- package/dist/explorer/components/index.mjs +7 -0
- package/dist/explorer/components/metadata-panel.cjs +122 -0
- package/dist/explorer/components/metadata-panel.mjs +122 -0
- package/dist/explorer/components/metadata-panel.mjs.map +1 -0
- package/dist/explorer/components/status-bar.cjs +53 -0
- package/dist/explorer/components/status-bar.mjs +54 -0
- package/dist/explorer/components/status-bar.mjs.map +1 -0
- package/dist/explorer/keybindings.cjs +214 -0
- package/dist/explorer/keybindings.mjs +213 -0
- package/dist/explorer/keybindings.mjs.map +1 -0
- package/dist/explorer/screen.cjs +200 -0
- package/dist/explorer/screen.mjs +199 -0
- package/dist/explorer/screen.mjs.map +1 -0
- package/dist/explorer/state.cjs +53 -0
- package/dist/explorer/state.mjs +53 -0
- package/dist/explorer/state.mjs.map +1 -0
- package/dist/explorer/theme.cjs +158 -0
- package/dist/explorer/theme.mjs +155 -0
- package/dist/explorer/theme.mjs.map +1 -0
- package/dist/path-utils.cjs +104 -0
- package/dist/path-utils.mjs +104 -0
- package/dist/path-utils.mjs.map +1 -0
- package/dist/runtime.cjs +47 -33
- package/dist/runtime.mjs +47 -33
- package/dist/runtime.mjs.map +1 -1
- package/dist/ui/header.cjs +60 -0
- package/dist/ui/header.mjs +59 -0
- package/dist/ui/header.mjs.map +1 -0
- package/dist/ui/index.cjs +17 -0
- package/dist/ui/index.mjs +15 -0
- package/dist/ui/index.mjs.map +1 -0
- package/dist/ui/terminal.cjs +97 -0
- package/dist/ui/terminal.mjs +95 -0
- package/dist/ui/terminal.mjs.map +1 -0
- package/package.json +9 -6
package/dist/commands/mount.cjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
77
|
+
const normalizedPath = validation.data.path;
|
|
78
|
+
if (config.mounts.some((m) => m.path === normalizedPath)) return {
|
|
44
79
|
success: false,
|
|
45
|
-
message: `Mount path "${
|
|
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 {
|
|
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
|
-
|
|
93
|
-
if (!
|
|
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();
|
package/dist/commands/mount.mjs
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
76
|
+
const normalizedPath = validation.data.path;
|
|
77
|
+
if (config.mounts.some((m) => m.path === normalizedPath)) return {
|
|
43
78
|
success: false,
|
|
44
|
-
message: `Mount path "${
|
|
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 {
|
|
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
|
-
|
|
92
|
-
if (!
|
|
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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"}
|
package/dist/config/loader.cjs
CHANGED
|
@@ -114,17 +114,39 @@ var ConfigLoader = class {
|
|
|
114
114
|
return result.data;
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
|
-
*
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 {
|
|
146
|
+
return {
|
|
147
|
+
mounts: allMounts,
|
|
148
|
+
serve: mergedServe
|
|
149
|
+
};
|
|
128
150
|
}
|
|
129
151
|
/**
|
|
130
152
|
* Find project root by looking for .git
|