@aigne/afs-cli 1.11.0-beta.2 → 1.11.0-beta.3
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 +50 -0
- package/dist/cli.cjs +37 -1
- package/dist/cli.mjs +37 -1
- 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 +6 -1
- package/dist/commands/ls.mjs +6 -1
- package/dist/commands/ls.mjs.map +1 -1
- package/dist/commands/mount.cjs +15 -2
- package/dist/commands/mount.mjs +15 -2
- package/dist/commands/mount.mjs.map +1 -1
- package/dist/commands/serve.cjs +141 -0
- package/dist/commands/serve.mjs +140 -0
- package/dist/commands/serve.mjs.map +1 -0
- package/dist/config/loader.cjs +16 -5
- package/dist/config/loader.mjs +16 -5
- 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 +15 -1
- package/dist/config/schema.mjs +15 -1
- 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/package.json +7 -6
package/README.md
CHANGED
|
@@ -16,6 +16,56 @@ AFS 命令行工具
|
|
|
16
16
|
| `afs mount remove <path>` (别名: `rm`) | 移除挂载 |
|
|
17
17
|
| `afs mount validate` | 验证挂载配置 |
|
|
18
18
|
| `afs explain [topic]` | 解释概念 (mount, uri, /path) |
|
|
19
|
+
| `afs serve` | 启动 HTTP 服务器暴露 AFS 提供者 |
|
|
20
|
+
|
|
21
|
+
## HTTP 服务器和远程挂载
|
|
22
|
+
|
|
23
|
+
### 启动 AFS HTTP 服务器
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 启动服务器,暴露所有配置的挂载
|
|
27
|
+
afs serve
|
|
28
|
+
|
|
29
|
+
# 自定义主机和端口
|
|
30
|
+
afs serve --host 0.0.0.0 --port 8080
|
|
31
|
+
|
|
32
|
+
# 只读模式(禁用写入操作)
|
|
33
|
+
afs serve --readonly
|
|
34
|
+
|
|
35
|
+
# 启用 CORS 支持
|
|
36
|
+
afs serve --cors
|
|
37
|
+
|
|
38
|
+
# 自定义基础路径
|
|
39
|
+
afs serve --path /api/afs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 挂载远程 AFS 服务器
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 添加远程 HTTP 挂载
|
|
46
|
+
afs mount add /remote http://localhost:3000/afs
|
|
47
|
+
|
|
48
|
+
# 添加 HTTPS 挂载
|
|
49
|
+
afs mount add /api https://api.example.com/afs
|
|
50
|
+
|
|
51
|
+
# 带描述的挂载
|
|
52
|
+
afs mount add /remote http://localhost:3000/afs --description "Remote AFS server"
|
|
53
|
+
|
|
54
|
+
# 访问远程挂载
|
|
55
|
+
afs list /remote
|
|
56
|
+
afs read /remote/file.txt
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 支持的 URI 方案
|
|
60
|
+
|
|
61
|
+
| 方案 | 示例 | 说明 |
|
|
62
|
+
|------|------|------|
|
|
63
|
+
| `fs://` | `fs:///path/to/dir` | 本地文件系统 |
|
|
64
|
+
| `git://` | `git:///path/to/repo` | Git 仓库 |
|
|
65
|
+
| `sqlite://` | `sqlite:///path/to/db.sqlite` | SQLite 数据库 |
|
|
66
|
+
| `json://` | `json:///path/to/config.json` | JSON/YAML 文件 |
|
|
67
|
+
| `http://` | `http://localhost:3000/afs` | HTTP 远程 AFS |
|
|
68
|
+
| `https://` | `https://api.example.com/afs` | HTTPS 远程 AFS |
|
|
19
69
|
|
|
20
70
|
## 输出模式
|
|
21
71
|
|
package/dist/cli.cjs
CHANGED
|
@@ -6,11 +6,12 @@ const require_mount = require('./commands/mount.cjs');
|
|
|
6
6
|
const require_explain = require('./commands/explain.cjs');
|
|
7
7
|
const require_ls = require('./commands/ls.cjs');
|
|
8
8
|
const require_read = require('./commands/read.cjs');
|
|
9
|
+
const require_runtime = require('./runtime.cjs');
|
|
10
|
+
const require_serve = require('./commands/serve.cjs');
|
|
9
11
|
const require_stat = require('./commands/stat.cjs');
|
|
10
12
|
const require_write = require('./commands/write.cjs');
|
|
11
13
|
require('./commands/index.cjs');
|
|
12
14
|
const require_errors = require('./errors.cjs');
|
|
13
|
-
const require_runtime = require('./runtime.cjs');
|
|
14
15
|
let yargs = require("yargs");
|
|
15
16
|
yargs = require_rolldown_runtime.__toESM(yargs);
|
|
16
17
|
let yargs_helpers = require("yargs/helpers");
|
|
@@ -29,6 +30,7 @@ let yargs_helpers = require("yargs/helpers");
|
|
|
29
30
|
* - afs read <path> Read file content
|
|
30
31
|
* - afs write <path> Write file content (--content or stdin)
|
|
31
32
|
* - afs exec <path> <action> Execute operation
|
|
33
|
+
* - afs serve Start HTTP server to expose AFS
|
|
32
34
|
*
|
|
33
35
|
* Output modes:
|
|
34
36
|
* - Default: Machine Truth (LLM/script friendly)
|
|
@@ -196,6 +198,40 @@ async function main() {
|
|
|
196
198
|
const result = await require_explain.explainCommand(process.cwd(), topic);
|
|
197
199
|
console.log(require_explain.formatExplainOutput(result, view));
|
|
198
200
|
}
|
|
201
|
+
}).command("serve", "Start HTTP server to expose AFS providers", (yargs$2) => yargs$2.option("host", {
|
|
202
|
+
type: "string",
|
|
203
|
+
default: "localhost",
|
|
204
|
+
description: "Host address to listen on"
|
|
205
|
+
}).option("port", {
|
|
206
|
+
type: "number",
|
|
207
|
+
default: 3e3,
|
|
208
|
+
description: "Port to listen on"
|
|
209
|
+
}).option("path", {
|
|
210
|
+
type: "string",
|
|
211
|
+
default: "/afs",
|
|
212
|
+
description: "Base path for the server"
|
|
213
|
+
}).option("readonly", {
|
|
214
|
+
type: "boolean",
|
|
215
|
+
default: false,
|
|
216
|
+
description: "Run in readonly mode (disable write operations)"
|
|
217
|
+
}).option("cors", {
|
|
218
|
+
type: "boolean",
|
|
219
|
+
default: false,
|
|
220
|
+
description: "Enable CORS support"
|
|
221
|
+
}).option("max-body", {
|
|
222
|
+
type: "number",
|
|
223
|
+
description: "Maximum request body size in bytes (default: 10MB)"
|
|
224
|
+
}), async (argv) => {
|
|
225
|
+
const result = await require_serve.serveCommand({
|
|
226
|
+
host: argv.host,
|
|
227
|
+
port: argv.port,
|
|
228
|
+
path: argv.path,
|
|
229
|
+
readonly: argv.readonly,
|
|
230
|
+
cors: argv.cors,
|
|
231
|
+
maxBodySize: argv["max-body"]
|
|
232
|
+
});
|
|
233
|
+
console.log(require_serve.formatServeOutput(result));
|
|
234
|
+
await new Promise(() => {});
|
|
199
235
|
}).demandCommand(1, "Please specify a command").strict();
|
|
200
236
|
try {
|
|
201
237
|
await cli.parse();
|
package/dist/cli.mjs
CHANGED
|
@@ -5,11 +5,12 @@ import { formatMountListOutput, mountAddCommand, mountListCommand, mountRemoveCo
|
|
|
5
5
|
import { explainCommand, explainPathCommand, formatExplainOutput, formatPathExplainOutput } from "./commands/explain.mjs";
|
|
6
6
|
import { formatLsOutput, lsCommand } from "./commands/ls.mjs";
|
|
7
7
|
import { formatReadOutput, readCommand } from "./commands/read.mjs";
|
|
8
|
+
import { createRuntime } from "./runtime.mjs";
|
|
9
|
+
import { formatServeOutput, serveCommand } from "./commands/serve.mjs";
|
|
8
10
|
import { formatStatOutput, statCommand } from "./commands/stat.mjs";
|
|
9
11
|
import { formatWriteOutput, writeCommand } from "./commands/write.mjs";
|
|
10
12
|
import "./commands/index.mjs";
|
|
11
13
|
import { CLIError, ExitCode } from "./errors.mjs";
|
|
12
|
-
import { createRuntime } from "./runtime.mjs";
|
|
13
14
|
import yargs from "yargs";
|
|
14
15
|
import { hideBin } from "yargs/helpers";
|
|
15
16
|
|
|
@@ -27,6 +28,7 @@ import { hideBin } from "yargs/helpers";
|
|
|
27
28
|
* - afs read <path> Read file content
|
|
28
29
|
* - afs write <path> Write file content (--content or stdin)
|
|
29
30
|
* - afs exec <path> <action> Execute operation
|
|
31
|
+
* - afs serve Start HTTP server to expose AFS
|
|
30
32
|
*
|
|
31
33
|
* Output modes:
|
|
32
34
|
* - Default: Machine Truth (LLM/script friendly)
|
|
@@ -194,6 +196,40 @@ async function main() {
|
|
|
194
196
|
const result = await explainCommand(process.cwd(), topic);
|
|
195
197
|
console.log(formatExplainOutput(result, view));
|
|
196
198
|
}
|
|
199
|
+
}).command("serve", "Start HTTP server to expose AFS providers", (yargs$1) => yargs$1.option("host", {
|
|
200
|
+
type: "string",
|
|
201
|
+
default: "localhost",
|
|
202
|
+
description: "Host address to listen on"
|
|
203
|
+
}).option("port", {
|
|
204
|
+
type: "number",
|
|
205
|
+
default: 3e3,
|
|
206
|
+
description: "Port to listen on"
|
|
207
|
+
}).option("path", {
|
|
208
|
+
type: "string",
|
|
209
|
+
default: "/afs",
|
|
210
|
+
description: "Base path for the server"
|
|
211
|
+
}).option("readonly", {
|
|
212
|
+
type: "boolean",
|
|
213
|
+
default: false,
|
|
214
|
+
description: "Run in readonly mode (disable write operations)"
|
|
215
|
+
}).option("cors", {
|
|
216
|
+
type: "boolean",
|
|
217
|
+
default: false,
|
|
218
|
+
description: "Enable CORS support"
|
|
219
|
+
}).option("max-body", {
|
|
220
|
+
type: "number",
|
|
221
|
+
description: "Maximum request body size in bytes (default: 10MB)"
|
|
222
|
+
}), async (argv) => {
|
|
223
|
+
const result = await serveCommand({
|
|
224
|
+
host: argv.host,
|
|
225
|
+
port: argv.port,
|
|
226
|
+
path: argv.path,
|
|
227
|
+
readonly: argv.readonly,
|
|
228
|
+
cors: argv.cors,
|
|
229
|
+
maxBodySize: argv["max-body"]
|
|
230
|
+
});
|
|
231
|
+
console.log(formatServeOutput(result));
|
|
232
|
+
await new Promise(() => {});
|
|
197
233
|
}).demandCommand(1, "Please specify a command").strict();
|
|
198
234
|
try {
|
|
199
235
|
await cli.parse();
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["yargs"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * AFS CLI - Command Line Interface\n *\n * Commands:\n * - afs mount list|ls List mount configurations\n * - afs mount add <path> <uri> Add a mount\n * - afs mount remove|rm <path> Remove a mount\n * - afs mount validate Validate mount configuration\n * - afs list|ls [path] List directory\n * - afs stat <path> Get file/directory info\n * - afs read <path> Read file content\n * - afs write <path> Write file content (--content or stdin)\n * - afs exec <path> <action> Execute operation\n *\n * Output modes:\n * - Default: Machine Truth (LLM/script friendly)\n * - --json: Structured JSON\n * - --view=llm: LLM optimized output\n * - --view=human: Human friendly format\n */\n\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport {\n execCommand,\n explainCommand,\n explainPathCommand,\n formatExecOutput,\n formatExplainOutput,\n formatLsOutput,\n formatMountListOutput,\n formatPathExplainOutput,\n formatReadOutput,\n formatStatOutput,\n formatWriteOutput,\n lsCommand,\n mountAddCommand,\n mountListCommand,\n mountRemoveCommand,\n mountValidateCommand,\n readCommand,\n statCommand,\n type ViewType,\n writeCommand,\n} from \"./commands/index.js\";\nimport { CLIError, ExitCode } from \"./errors.js\";\nimport { createRuntime } from \"./runtime.js\";\nimport { VERSION } from \"./version.js\";\n\n// Global view type derived from args\nfunction getViewType(argv: { json?: boolean; view?: string }): ViewType {\n if (argv.json) return \"json\";\n if (argv.view) return argv.view as ViewType;\n return \"default\";\n}\n\n// Run the CLI\nasync function main() {\n const cli = yargs(hideBin(process.argv))\n .scriptName(\"afs\")\n .version(VERSION)\n .alias(\"version\", \"V\")\n .help(\"help\")\n .alias(\"help\", \"h\")\n .usage(\"$0 <command> [options]\")\n .option(\"json\", {\n type: \"boolean\",\n description: \"Output in JSON format\",\n global: true,\n })\n .option(\"view\", {\n type: \"string\",\n choices: [\"default\", \"llm\", \"human\"],\n description: \"Output view format\",\n global: true,\n })\n .command(\n [\"list [path]\", \"ls [path]\"],\n \"List directory contents\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n default: \"/\",\n description: \"Path to list\",\n })\n .option(\"depth\", {\n type: \"number\",\n default: 1,\n description: \"Maximum depth to list\",\n })\n .option(\"limit\", {\n alias: \"n\",\n type: \"number\",\n description: \"Maximum number of entries to return\",\n })\n .option(\"max-children\", {\n type: \"number\",\n description: \"Maximum children per directory\",\n })\n .option(\"pattern\", {\n alias: \"p\",\n type: \"string\",\n description: \"Glob pattern to filter entries (e.g., *.ts, **/*.js)\",\n }),\n async (argv) => {\n const runtime = await createRuntime();\n const result = await lsCommand(runtime, argv.path!, {\n maxDepth: argv.depth,\n limit: argv.limit,\n maxChildren: argv[\"max-children\"],\n pattern: argv.pattern,\n });\n const view = getViewType(argv);\n console.log(formatLsOutput(result, view));\n },\n )\n .command(\n \"stat <path>\",\n \"Get file or directory info\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to stat\",\n }),\n async (argv) => {\n const runtime = await createRuntime();\n const result = await statCommand(runtime, argv.path!);\n const view = getViewType(argv);\n console.log(formatStatOutput(result, view));\n },\n )\n .command(\n \"read <path>\",\n \"Read file content\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to read\",\n }),\n async (argv) => {\n const runtime = await createRuntime();\n const result = await readCommand(runtime, argv.path!);\n const view = getViewType(argv);\n console.log(formatReadOutput(result, view));\n },\n )\n .command(\n \"write <path>\",\n \"Write content to file (from --content or stdin)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to write\",\n })\n .option(\"content\", {\n type: \"string\",\n description: \"Content to write (if not provided, reads from stdin)\",\n })\n .option(\"append\", {\n type: \"boolean\",\n default: false,\n description: \"Append to file instead of overwrite\",\n }),\n async (argv) => {\n let content: string;\n\n if (argv.content !== undefined) {\n // Use --content parameter\n content = argv.content;\n } else {\n // Read content from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n content = Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n const runtime = await createRuntime();\n const result = await writeCommand(runtime, argv.path!, content, {\n append: argv.append,\n });\n const view = getViewType(argv);\n console.log(formatWriteOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"exec <path> [action]\",\n \"Execute operation on path\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to execute on\",\n })\n .positional(\"action\", {\n type: \"string\",\n default: \"default\",\n description: \"Action to execute\",\n })\n .option(\"params\", {\n type: \"string\",\n description: \"JSON parameters for the action\",\n }),\n async (argv) => {\n const params = argv.params ? JSON.parse(argv.params) : {};\n\n const runtime = await createRuntime();\n const result = await execCommand(runtime, argv.path!, argv.action!, params);\n const view = getViewType(argv);\n console.log(formatExecOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"mount\",\n \"Manage mount configurations\",\n (yargs) =>\n yargs\n .command(\n [\"list\", \"ls\"],\n \"List all mounts\",\n () => {},\n async (argv) => {\n const result = await mountListCommand(process.cwd());\n const view = getViewType(argv);\n console.log(formatMountListOutput(result.mounts, view));\n },\n )\n .command(\n \"add <path> <uri>\",\n \"Add a new mount (path=virtual path, uri=data source)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path in AFS namespace (e.g., /src, /data)\",\n })\n .positional(\"uri\", {\n type: \"string\",\n demandOption: true,\n description: \"Data source URI: fs:///local/path, git://repo, sqlite:///db.sqlite\",\n })\n .option(\"description\", {\n type: \"string\",\n description: \"Human-readable description for this mount\",\n }),\n async (argv) => {\n const result = await mountAddCommand(process.cwd(), argv.path!, argv.uri!, {\n description: argv.description,\n });\n const view = getViewType(argv);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n console.log(`Added mount ${argv.path}`);\n } else {\n console.error(result.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n [\"remove <path>\", \"rm <path>\"],\n \"Remove a mount\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path to remove (e.g., /src)\",\n }),\n async (argv) => {\n const result = await mountRemoveCommand(process.cwd(), argv.path!);\n const view = getViewType(argv);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n console.log(`Removed mount ${argv.path}`);\n } else {\n console.error(result.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n \"validate\",\n \"Validate mount configuration\",\n () => {},\n async (argv) => {\n const result = await mountValidateCommand(process.cwd());\n const view = getViewType(argv);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.valid) {\n console.log(\"Configuration is valid\");\n } else {\n console.error(\"Configuration has errors:\");\n for (const error of result.errors) {\n console.error(` - ${error}`);\n }\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .demandCommand(1, \"Please specify a mount subcommand\"),\n () => {},\n )\n .command(\n \"explain [topic]\",\n \"Explain AFS concepts or AFS object\",\n (yargs) =>\n yargs.positional(\"topic\", {\n type: \"string\",\n description: \"Topic (mount, path, uri) or AFS path (e.g., /modules/src)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n const topic = argv.topic;\n\n // If topic starts with /, treat as object path\n if (topic?.startsWith(\"/\")) {\n const runtime = await createRuntime();\n const result = await explainPathCommand(runtime, topic);\n console.log(formatPathExplainOutput(result, view));\n } else {\n const result = await explainCommand(process.cwd(), topic);\n console.log(formatExplainOutput(result, view));\n }\n },\n )\n .demandCommand(1, \"Please specify a command\")\n .strict();\n\n try {\n await cli.parse();\n } catch (error) {\n if (error instanceof CLIError) {\n console.error(error.message);\n process.exit(error.exitCode);\n }\n throw error;\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["yargs"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * AFS CLI - Command Line Interface\n *\n * Commands:\n * - afs mount list|ls List mount configurations\n * - afs mount add <path> <uri> Add a mount\n * - afs mount remove|rm <path> Remove a mount\n * - afs mount validate Validate mount configuration\n * - afs list|ls [path] List directory\n * - afs stat <path> Get file/directory info\n * - afs read <path> Read file content\n * - afs write <path> Write file content (--content or stdin)\n * - afs exec <path> <action> Execute operation\n * - afs serve Start HTTP server to expose AFS\n *\n * Output modes:\n * - Default: Machine Truth (LLM/script friendly)\n * - --json: Structured JSON\n * - --view=llm: LLM optimized output\n * - --view=human: Human friendly format\n */\n\nimport yargs from \"yargs\";\nimport { hideBin } from \"yargs/helpers\";\nimport {\n execCommand,\n explainCommand,\n explainPathCommand,\n formatExecOutput,\n formatExplainOutput,\n formatLsOutput,\n formatMountListOutput,\n formatPathExplainOutput,\n formatReadOutput,\n formatServeOutput,\n formatStatOutput,\n formatWriteOutput,\n lsCommand,\n mountAddCommand,\n mountListCommand,\n mountRemoveCommand,\n mountValidateCommand,\n readCommand,\n serveCommand,\n statCommand,\n type ViewType,\n writeCommand,\n} from \"./commands/index.js\";\nimport { CLIError, ExitCode } from \"./errors.js\";\nimport { createRuntime } from \"./runtime.js\";\nimport { VERSION } from \"./version.js\";\n\n// Global view type derived from args\nfunction getViewType(argv: { json?: boolean; view?: string }): ViewType {\n if (argv.json) return \"json\";\n if (argv.view) return argv.view as ViewType;\n return \"default\";\n}\n\n// Run the CLI\nasync function main() {\n const cli = yargs(hideBin(process.argv))\n .scriptName(\"afs\")\n .version(VERSION)\n .alias(\"version\", \"V\")\n .help(\"help\")\n .alias(\"help\", \"h\")\n .usage(\"$0 <command> [options]\")\n .option(\"json\", {\n type: \"boolean\",\n description: \"Output in JSON format\",\n global: true,\n })\n .option(\"view\", {\n type: \"string\",\n choices: [\"default\", \"llm\", \"human\"],\n description: \"Output view format\",\n global: true,\n })\n .command(\n [\"list [path]\", \"ls [path]\"],\n \"List directory contents\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n default: \"/\",\n description: \"Path to list\",\n })\n .option(\"depth\", {\n type: \"number\",\n default: 1,\n description: \"Maximum depth to list\",\n })\n .option(\"limit\", {\n alias: \"n\",\n type: \"number\",\n description: \"Maximum number of entries to return\",\n })\n .option(\"max-children\", {\n type: \"number\",\n description: \"Maximum children per directory\",\n })\n .option(\"pattern\", {\n alias: \"p\",\n type: \"string\",\n description: \"Glob pattern to filter entries (e.g., *.ts, **/*.js)\",\n }),\n async (argv) => {\n const runtime = await createRuntime();\n const result = await lsCommand(runtime, argv.path!, {\n maxDepth: argv.depth,\n limit: argv.limit,\n maxChildren: argv[\"max-children\"],\n pattern: argv.pattern,\n });\n const view = getViewType(argv);\n console.log(formatLsOutput(result, view));\n },\n )\n .command(\n \"stat <path>\",\n \"Get file or directory info\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to stat\",\n }),\n async (argv) => {\n const runtime = await createRuntime();\n const result = await statCommand(runtime, argv.path!);\n const view = getViewType(argv);\n console.log(formatStatOutput(result, view));\n },\n )\n .command(\n \"read <path>\",\n \"Read file content\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to read\",\n }),\n async (argv) => {\n const runtime = await createRuntime();\n const result = await readCommand(runtime, argv.path!);\n const view = getViewType(argv);\n console.log(formatReadOutput(result, view));\n },\n )\n .command(\n \"write <path>\",\n \"Write content to file (from --content or stdin)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to write\",\n })\n .option(\"content\", {\n type: \"string\",\n description: \"Content to write (if not provided, reads from stdin)\",\n })\n .option(\"append\", {\n type: \"boolean\",\n default: false,\n description: \"Append to file instead of overwrite\",\n }),\n async (argv) => {\n let content: string;\n\n if (argv.content !== undefined) {\n // Use --content parameter\n content = argv.content;\n } else {\n // Read content from stdin\n const chunks: Buffer[] = [];\n for await (const chunk of process.stdin) {\n chunks.push(chunk);\n }\n content = Buffer.concat(chunks).toString(\"utf-8\");\n }\n\n const runtime = await createRuntime();\n const result = await writeCommand(runtime, argv.path!, content, {\n append: argv.append,\n });\n const view = getViewType(argv);\n console.log(formatWriteOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"exec <path> [action]\",\n \"Execute operation on path\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Path to execute on\",\n })\n .positional(\"action\", {\n type: \"string\",\n default: \"default\",\n description: \"Action to execute\",\n })\n .option(\"params\", {\n type: \"string\",\n description: \"JSON parameters for the action\",\n }),\n async (argv) => {\n const params = argv.params ? JSON.parse(argv.params) : {};\n\n const runtime = await createRuntime();\n const result = await execCommand(runtime, argv.path!, argv.action!, params);\n const view = getViewType(argv);\n console.log(formatExecOutput(result, view));\n\n if (!result.success) {\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n },\n )\n .command(\n \"mount\",\n \"Manage mount configurations\",\n (yargs) =>\n yargs\n .command(\n [\"list\", \"ls\"],\n \"List all mounts\",\n () => {},\n async (argv) => {\n const result = await mountListCommand(process.cwd());\n const view = getViewType(argv);\n console.log(formatMountListOutput(result.mounts, view));\n },\n )\n .command(\n \"add <path> <uri>\",\n \"Add a new mount (path=virtual path, uri=data source)\",\n (yargs) =>\n yargs\n .positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path in AFS namespace (e.g., /src, /data)\",\n })\n .positional(\"uri\", {\n type: \"string\",\n demandOption: true,\n description: \"Data source URI: fs:///local/path, git://repo, sqlite:///db.sqlite\",\n })\n .option(\"description\", {\n type: \"string\",\n description: \"Human-readable description for this mount\",\n }),\n async (argv) => {\n const result = await mountAddCommand(process.cwd(), argv.path!, argv.uri!, {\n description: argv.description,\n });\n const view = getViewType(argv);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n console.log(`Added mount ${argv.path}`);\n } else {\n console.error(result.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n [\"remove <path>\", \"rm <path>\"],\n \"Remove a mount\",\n (yargs) =>\n yargs.positional(\"path\", {\n type: \"string\",\n demandOption: true,\n description: \"Virtual path to remove (e.g., /src)\",\n }),\n async (argv) => {\n const result = await mountRemoveCommand(process.cwd(), argv.path!);\n const view = getViewType(argv);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.success) {\n console.log(`Removed mount ${argv.path}`);\n } else {\n console.error(result.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .command(\n \"validate\",\n \"Validate mount configuration\",\n () => {},\n async (argv) => {\n const result = await mountValidateCommand(process.cwd());\n const view = getViewType(argv);\n\n if (view === \"json\") {\n console.log(JSON.stringify(result, null, 2));\n } else {\n if (result.valid) {\n console.log(\"Configuration is valid\");\n } else {\n console.error(\"Configuration has errors:\");\n for (const error of result.errors) {\n console.error(` - ${error}`);\n }\n process.exit(ExitCode.RUNTIME_ERROR);\n }\n }\n },\n )\n .demandCommand(1, \"Please specify a mount subcommand\"),\n () => {},\n )\n .command(\n \"explain [topic]\",\n \"Explain AFS concepts or AFS object\",\n (yargs) =>\n yargs.positional(\"topic\", {\n type: \"string\",\n description: \"Topic (mount, path, uri) or AFS path (e.g., /modules/src)\",\n }),\n async (argv) => {\n const view = getViewType(argv);\n const topic = argv.topic;\n\n // If topic starts with /, treat as object path\n if (topic?.startsWith(\"/\")) {\n const runtime = await createRuntime();\n const result = await explainPathCommand(runtime, topic);\n console.log(formatPathExplainOutput(result, view));\n } else {\n const result = await explainCommand(process.cwd(), topic);\n console.log(formatExplainOutput(result, view));\n }\n },\n )\n .command(\n \"serve\",\n \"Start HTTP server to expose AFS providers\",\n (yargs) =>\n yargs\n .option(\"host\", {\n type: \"string\",\n default: \"localhost\",\n description: \"Host address to listen on\",\n })\n .option(\"port\", {\n type: \"number\",\n default: 3000,\n description: \"Port to listen on\",\n })\n .option(\"path\", {\n type: \"string\",\n default: \"/afs\",\n description: \"Base path for the server\",\n })\n .option(\"readonly\", {\n type: \"boolean\",\n default: false,\n description: \"Run in readonly mode (disable write operations)\",\n })\n .option(\"cors\", {\n type: \"boolean\",\n default: false,\n description: \"Enable CORS support\",\n })\n .option(\"max-body\", {\n type: \"number\",\n description: \"Maximum request body size in bytes (default: 10MB)\",\n }),\n async (argv) => {\n const result = await serveCommand({\n host: argv.host,\n port: argv.port,\n path: argv.path,\n readonly: argv.readonly,\n cors: argv.cors,\n maxBodySize: argv[\"max-body\"],\n });\n\n console.log(formatServeOutput(result));\n\n // Keep the process running\n await new Promise(() => {});\n },\n )\n .demandCommand(1, \"Please specify a command\")\n .strict();\n\n try {\n await cli.parse();\n } catch (error) {\n if (error instanceof CLIError) {\n console.error(error.message);\n process.exit(error.exitCode);\n }\n throw error;\n }\n}\n\nmain().catch((error) => {\n console.error(\"Fatal error:\", error.message);\n process.exit(ExitCode.RUNTIME_ERROR);\n});\n"],"mappings":"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"}
|
package/dist/commands/index.cjs
CHANGED
|
@@ -3,5 +3,6 @@ const require_mount = require('./mount.cjs');
|
|
|
3
3
|
const require_explain = require('./explain.cjs');
|
|
4
4
|
const require_ls = require('./ls.cjs');
|
|
5
5
|
const require_read = require('./read.cjs');
|
|
6
|
+
const require_serve = require('./serve.cjs');
|
|
6
7
|
const require_stat = require('./stat.cjs');
|
|
7
8
|
const require_write = require('./write.cjs');
|
package/dist/commands/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { formatMountListOutput, mountAddCommand, mountListCommand, mountRemoveCo
|
|
|
3
3
|
import { explainCommand, explainPathCommand, formatExplainOutput, formatPathExplainOutput } from "./explain.mjs";
|
|
4
4
|
import { formatLsOutput, lsCommand } from "./ls.mjs";
|
|
5
5
|
import { formatReadOutput, readCommand } from "./read.mjs";
|
|
6
|
+
import { formatServeOutput, serveCommand } from "./serve.mjs";
|
|
6
7
|
import { formatStatOutput, statCommand } from "./stat.mjs";
|
|
7
8
|
import { formatWriteOutput, writeCommand } from "./write.mjs";
|
|
8
9
|
|
package/dist/commands/ls.cjs
CHANGED
|
@@ -14,7 +14,7 @@ async function lsCommand(runtime, path, options = {}) {
|
|
|
14
14
|
path: entry.path,
|
|
15
15
|
type: mapEntryType(entry.metadata?.type),
|
|
16
16
|
size: entry.metadata?.size,
|
|
17
|
-
modified: entry.updatedAt
|
|
17
|
+
modified: formatDate(entry.updatedAt),
|
|
18
18
|
childrenCount: entry.metadata?.childrenCount,
|
|
19
19
|
childrenTruncated: entry.metadata?.childrenTruncated
|
|
20
20
|
}));
|
|
@@ -30,6 +30,11 @@ function mapEntryType(type) {
|
|
|
30
30
|
if (type === "file") return "file";
|
|
31
31
|
return "directory";
|
|
32
32
|
}
|
|
33
|
+
function formatDate(date) {
|
|
34
|
+
if (!date) return void 0;
|
|
35
|
+
if (typeof date === "string") return date;
|
|
36
|
+
return date.toISOString();
|
|
37
|
+
}
|
|
33
38
|
/**
|
|
34
39
|
* Format ls output for different views
|
|
35
40
|
*/
|
package/dist/commands/ls.mjs
CHANGED
|
@@ -13,7 +13,7 @@ async function lsCommand(runtime, path, options = {}) {
|
|
|
13
13
|
path: entry.path,
|
|
14
14
|
type: mapEntryType(entry.metadata?.type),
|
|
15
15
|
size: entry.metadata?.size,
|
|
16
|
-
modified: entry.updatedAt
|
|
16
|
+
modified: formatDate(entry.updatedAt),
|
|
17
17
|
childrenCount: entry.metadata?.childrenCount,
|
|
18
18
|
childrenTruncated: entry.metadata?.childrenTruncated
|
|
19
19
|
}));
|
|
@@ -29,6 +29,11 @@ function mapEntryType(type) {
|
|
|
29
29
|
if (type === "file") return "file";
|
|
30
30
|
return "directory";
|
|
31
31
|
}
|
|
32
|
+
function formatDate(date) {
|
|
33
|
+
if (!date) return void 0;
|
|
34
|
+
if (typeof date === "string") return date;
|
|
35
|
+
return date.toISOString();
|
|
36
|
+
}
|
|
32
37
|
/**
|
|
33
38
|
* Format ls output for different views
|
|
34
39
|
*/
|
package/dist/commands/ls.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ls.mjs","names":[],"sources":["../../src/commands/ls.ts"],"sourcesContent":["import type { AFSEntry } from \"@aigne/afs\";\nimport type { AFSRuntime } from \"../runtime.js\";\n\nexport type ViewType = \"default\" | \"json\" | \"llm\" | \"human\";\n\nexport interface LsEntry {\n path: string;\n type: \"file\" | \"directory\";\n size?: number;\n modified?: string;\n hash?: string;\n childrenCount?: number;\n childrenTruncated?: boolean;\n}\n\nexport interface LsOptions {\n maxDepth?: number;\n limit?: number;\n maxChildren?: number;\n pattern?: string;\n}\n\nexport interface LsResult {\n entries: LsEntry[];\n total: number;\n truncated?: boolean;\n message?: string;\n}\n\n/**\n * List directory contents\n */\nexport async function lsCommand(\n runtime: AFSRuntime,\n path: string,\n options: LsOptions = {},\n): Promise<LsResult> {\n const result = await runtime.list(path, {\n maxDepth: options.maxDepth ?? 1,\n limit: options.limit,\n maxChildren: options.maxChildren,\n pattern: options.pattern,\n });\n\n const entries: LsEntry[] = result.data.map((entry: AFSEntry) => ({\n path: entry.path,\n type: mapEntryType(entry.metadata?.type),\n size: entry.metadata?.size,\n modified: entry.updatedAt
|
|
1
|
+
{"version":3,"file":"ls.mjs","names":[],"sources":["../../src/commands/ls.ts"],"sourcesContent":["import type { AFSEntry } from \"@aigne/afs\";\nimport type { AFSRuntime } from \"../runtime.js\";\n\nexport type ViewType = \"default\" | \"json\" | \"llm\" | \"human\";\n\nexport interface LsEntry {\n path: string;\n type: \"file\" | \"directory\";\n size?: number;\n modified?: string;\n hash?: string;\n childrenCount?: number;\n childrenTruncated?: boolean;\n}\n\nexport interface LsOptions {\n maxDepth?: number;\n limit?: number;\n maxChildren?: number;\n pattern?: string;\n}\n\nexport interface LsResult {\n entries: LsEntry[];\n total: number;\n truncated?: boolean;\n message?: string;\n}\n\n/**\n * List directory contents\n */\nexport async function lsCommand(\n runtime: AFSRuntime,\n path: string,\n options: LsOptions = {},\n): Promise<LsResult> {\n const result = await runtime.list(path, {\n maxDepth: options.maxDepth ?? 1,\n limit: options.limit,\n maxChildren: options.maxChildren,\n pattern: options.pattern,\n });\n\n const entries: LsEntry[] = result.data.map((entry: AFSEntry) => ({\n path: entry.path,\n type: mapEntryType(entry.metadata?.type),\n size: entry.metadata?.size,\n modified: formatDate(entry.updatedAt),\n childrenCount: entry.metadata?.childrenCount,\n childrenTruncated: entry.metadata?.childrenTruncated,\n }));\n\n // Check if any entry has truncated children\n const hasTruncatedChildren = entries.some((e) => e.childrenTruncated);\n\n // Determine if results were truncated\n const truncated =\n hasTruncatedChildren ||\n (options.limit !== undefined && entries.length >= options.limit) ||\n result.message?.toLowerCase().includes(\"truncat\");\n\n return {\n entries,\n total: entries.length,\n truncated,\n message: result.message,\n };\n}\n\nfunction mapEntryType(type?: string): LsEntry[\"type\"] {\n // Only \"file\" is a true file, all other types are treated as directories\n if (type === \"file\") {\n return \"file\";\n }\n return \"directory\";\n}\n\nfunction formatDate(date: Date | string | undefined): string | undefined {\n if (!date) return undefined;\n // If it's already a string (from HTTP JSON), return as-is\n if (typeof date === \"string\") return date;\n // If it's a Date object, convert to ISO string\n return date.toISOString();\n}\n\n/**\n * Format ls output for different views\n */\nexport function formatLsOutput(result: LsResult, view: ViewType): string {\n switch (view) {\n case \"json\":\n return formatJson(result);\n case \"llm\":\n return formatLlm(result);\n case \"human\":\n return formatHuman(result);\n default:\n return formatDefault(result);\n }\n}\n\n/**\n * Default format: Machine truth, one path per line\n */\nfunction formatDefault(result: LsResult): string {\n const lines = result.entries.map((entry) => entry.path);\n if (result.truncated) {\n lines.push(`# Results truncated (${result.total} shown)`);\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * JSON format: Structured output\n */\nfunction formatJson(result: LsResult): string {\n return JSON.stringify(\n {\n entries: result.entries,\n total: result.total,\n truncated: result.truncated,\n message: result.message,\n },\n null,\n 2,\n );\n}\n\n/**\n * LLM format: Token-efficient, semantic facts\n */\nfunction formatLlm(result: LsResult): string {\n const lines: string[] = [];\n\n for (const entry of result.entries) {\n const parts = [`ENTRY ${entry.path}`];\n parts.push(`TYPE=${entry.type}`);\n\n if (entry.size !== undefined) {\n parts.push(`SIZE=${entry.size}`);\n }\n\n if (entry.childrenCount !== undefined) {\n parts.push(`CHILDREN=${entry.childrenCount}`);\n }\n\n if (entry.childrenTruncated) {\n parts.push(\"TRUNCATED\");\n }\n\n lines.push(parts.join(\" \"));\n }\n\n lines.push(`TOTAL ${result.total}`);\n if (result.truncated) {\n lines.push(\"TRUNCATED true\");\n }\n return lines.join(\"\\n\");\n}\n\ninterface TreeNode {\n name: string;\n entry?: LsEntry;\n children: Map<string, TreeNode>;\n}\n\n/**\n * Human format: Tree structure\n */\nfunction formatHuman(result: LsResult): string {\n // Build tree structure from flat paths\n const root: TreeNode = { name: \"\", children: new Map() };\n\n for (const entry of result.entries) {\n const parts = entry.path.split(\"/\").filter(Boolean);\n let current = root;\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i]!;\n if (!current.children.has(part)) {\n current.children.set(part, { name: part, children: new Map() });\n }\n current = current.children.get(part)!;\n\n // Attach entry to the leaf node\n if (i === parts.length - 1) {\n current.entry = entry;\n }\n }\n }\n\n // Render tree\n const lines: string[] = [];\n renderTree(root, \"\", lines);\n\n if (result.truncated) {\n lines.push(\"\");\n lines.push(`(Results truncated - ${result.total} entries shown)`);\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction renderTree(node: TreeNode, prefix: string, lines: string[]): void {\n const children = Array.from(node.children.values());\n\n for (let i = 0; i < children.length; i++) {\n const child = children[i]!;\n const isLast = i === children.length - 1;\n const connector = isLast ? \"└── \" : \"├── \";\n const icon = child.entry ? getTypeIcon(child.entry.type) : \"\\u{1F4C2}\"; // 📂\n const sizeStr = child.entry?.size !== undefined ? ` ${formatSize(child.entry.size)}` : \"\";\n\n lines.push(`${prefix}${connector}${icon} ${child.name}${sizeStr}`);\n\n // Recurse into children\n const childPrefix = prefix + (isLast ? \" \" : \"│ \");\n renderTree(child, childPrefix, lines);\n }\n}\n\nfunction getTypeIcon(type: LsEntry[\"type\"]): string {\n switch (type) {\n case \"directory\":\n return \"\\u{1F4C2}\"; // 📂\n case \"file\":\n return \"\\u{1F4C4}\"; // 📄\n default:\n return \"\\u{1F4C4}\"; // 📄\n }\n}\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes}B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n}\n"],"mappings":"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"}
|
package/dist/commands/mount.cjs
CHANGED
|
@@ -6,13 +6,25 @@ let smol_toml = require("smol-toml");
|
|
|
6
6
|
|
|
7
7
|
//#region src/commands/mount.ts
|
|
8
8
|
/**
|
|
9
|
+
* Check if a path looks like a remote Git URL
|
|
10
|
+
* Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)
|
|
11
|
+
*/
|
|
12
|
+
function isRemoteGitUrl(path) {
|
|
13
|
+
if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) return true;
|
|
14
|
+
if (/^(https?|ssh|git):\/\//.test(path)) return true;
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
9
18
|
* Resolve relative paths in URI to absolute paths
|
|
10
19
|
* Supports fs://, git://, sqlite://, json:// schemes
|
|
11
|
-
* Paths without protocol prefix are treated as fs:// paths
|
|
20
|
+
* Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)
|
|
12
21
|
*/
|
|
13
22
|
function resolveUriPath(uri, cwd) {
|
|
14
23
|
const schemeMatch = uri.match(/^([a-z]+):\/\//);
|
|
15
|
-
if (!schemeMatch)
|
|
24
|
+
if (!schemeMatch) {
|
|
25
|
+
if (isRemoteGitUrl(uri)) return `git://${uri}`;
|
|
26
|
+
return `fs://${(0, node_path.isAbsolute)(uri) ? uri : (0, node_path.resolve)(cwd, uri)}`;
|
|
27
|
+
}
|
|
16
28
|
const scheme = schemeMatch[1];
|
|
17
29
|
const pathPart = uri.slice(schemeMatch[0].length);
|
|
18
30
|
if (![
|
|
@@ -21,6 +33,7 @@ function resolveUriPath(uri, cwd) {
|
|
|
21
33
|
"sqlite",
|
|
22
34
|
"json"
|
|
23
35
|
].includes(scheme)) return uri;
|
|
36
|
+
if (scheme === "git" && isRemoteGitUrl(pathPart)) return uri;
|
|
24
37
|
if ((0, node_path.isAbsolute)(pathPart)) return uri;
|
|
25
38
|
return `${scheme}://${(0, node_path.resolve)(cwd, pathPart)}`;
|
|
26
39
|
}
|
package/dist/commands/mount.mjs
CHANGED
|
@@ -5,13 +5,25 @@ import { parse, stringify } from "smol-toml";
|
|
|
5
5
|
|
|
6
6
|
//#region src/commands/mount.ts
|
|
7
7
|
/**
|
|
8
|
+
* Check if a path looks like a remote Git URL
|
|
9
|
+
* Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)
|
|
10
|
+
*/
|
|
11
|
+
function isRemoteGitUrl(path) {
|
|
12
|
+
if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) return true;
|
|
13
|
+
if (/^(https?|ssh|git):\/\//.test(path)) return true;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
8
17
|
* Resolve relative paths in URI to absolute paths
|
|
9
18
|
* Supports fs://, git://, sqlite://, json:// schemes
|
|
10
|
-
* Paths without protocol prefix are treated as fs:// paths
|
|
19
|
+
* Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)
|
|
11
20
|
*/
|
|
12
21
|
function resolveUriPath(uri, cwd) {
|
|
13
22
|
const schemeMatch = uri.match(/^([a-z]+):\/\//);
|
|
14
|
-
if (!schemeMatch)
|
|
23
|
+
if (!schemeMatch) {
|
|
24
|
+
if (isRemoteGitUrl(uri)) return `git://${uri}`;
|
|
25
|
+
return `fs://${isAbsolute(uri) ? uri : resolve(cwd, uri)}`;
|
|
26
|
+
}
|
|
15
27
|
const scheme = schemeMatch[1];
|
|
16
28
|
const pathPart = uri.slice(schemeMatch[0].length);
|
|
17
29
|
if (![
|
|
@@ -20,6 +32,7 @@ function resolveUriPath(uri, cwd) {
|
|
|
20
32
|
"sqlite",
|
|
21
33
|
"json"
|
|
22
34
|
].includes(scheme)) return uri;
|
|
35
|
+
if (scheme === "git" && isRemoteGitUrl(pathPart)) return uri;
|
|
23
36
|
if (isAbsolute(pathPart)) return uri;
|
|
24
37
|
return `${scheme}://${resolve(cwd, pathPart)}`;
|
|
25
38
|
}
|
|
@@ -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 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 * 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 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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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_loader = require('../config/loader.cjs');
|
|
3
|
+
const require_runtime = require('../runtime.cjs');
|
|
4
|
+
let node_http = require("node:http");
|
|
5
|
+
let _aigne_afs_http = require("@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 require_loader.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 require_runtime.createRuntime();
|
|
47
|
+
const mounts = config.mounts.map((m) => ({
|
|
48
|
+
path: m.path,
|
|
49
|
+
provider: m.uri
|
|
50
|
+
}));
|
|
51
|
+
const handler = (0, _aigne_afs_http.createAFSHttpHandler)({
|
|
52
|
+
module: createRuntimeModule(runtime, readonly),
|
|
53
|
+
maxBodySize
|
|
54
|
+
});
|
|
55
|
+
const server = (0, node_http.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("AFS HTTP Server starting...");
|
|
131
|
+
lines.push("Mounted providers:");
|
|
132
|
+
for (const mount of result.mounts) lines.push(` ${mount.path.padEnd(20)} ${mount.provider}`);
|
|
133
|
+
lines.push("");
|
|
134
|
+
lines.push(`Listening on: ${result.url}`);
|
|
135
|
+
lines.push("Press Ctrl+C to stop");
|
|
136
|
+
return lines.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
exports.formatServeOutput = formatServeOutput;
|
|
141
|
+
exports.serveCommand = serveCommand;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ConfigLoader } from "../config/loader.mjs";
|
|
2
|
+
import { createRuntime } from "../runtime.mjs";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { createAFSHttpHandler } from "@aigne/afs-http";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/serve.ts
|
|
7
|
+
/**
|
|
8
|
+
* Create an AFSModule wrapper around AFSRuntime
|
|
9
|
+
*/
|
|
10
|
+
function createRuntimeModule(runtime, readonly) {
|
|
11
|
+
return {
|
|
12
|
+
name: "afs-server",
|
|
13
|
+
accessMode: readonly ? "readonly" : "readwrite",
|
|
14
|
+
async list(path, options) {
|
|
15
|
+
return runtime.list(path, options);
|
|
16
|
+
},
|
|
17
|
+
async read(path, options) {
|
|
18
|
+
return runtime.read(path, options);
|
|
19
|
+
},
|
|
20
|
+
async write(path, content, options) {
|
|
21
|
+
if (readonly) throw new Error("Server is in readonly mode");
|
|
22
|
+
return runtime.write(path, content, options);
|
|
23
|
+
},
|
|
24
|
+
async delete(path, options) {
|
|
25
|
+
if (readonly) throw new Error("Server is in readonly mode");
|
|
26
|
+
return runtime.delete(path, options);
|
|
27
|
+
},
|
|
28
|
+
async search(path, query, options) {
|
|
29
|
+
return runtime.search(path, query, options);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Start HTTP server to expose AFS providers
|
|
35
|
+
*/
|
|
36
|
+
async function serveCommand(options = {}) {
|
|
37
|
+
const config = await new ConfigLoader().load(process.cwd());
|
|
38
|
+
const serveConfig = config.serve ?? {};
|
|
39
|
+
const host = options.host ?? serveConfig.host ?? "localhost";
|
|
40
|
+
const port = options.port ?? serveConfig.port ?? 3e3;
|
|
41
|
+
const basePath = options.path ?? serveConfig.path ?? "/afs";
|
|
42
|
+
const readonly = options.readonly ?? serveConfig.readonly ?? false;
|
|
43
|
+
const cors = options.cors ?? serveConfig.cors ?? false;
|
|
44
|
+
const maxBodySize = options.maxBodySize ?? serveConfig.max_body_size ?? 10 * 1024 * 1024;
|
|
45
|
+
const runtime = await createRuntime();
|
|
46
|
+
const mounts = config.mounts.map((m) => ({
|
|
47
|
+
path: m.path,
|
|
48
|
+
provider: m.uri
|
|
49
|
+
}));
|
|
50
|
+
const handler = createAFSHttpHandler({
|
|
51
|
+
module: createRuntimeModule(runtime, readonly),
|
|
52
|
+
maxBodySize
|
|
53
|
+
});
|
|
54
|
+
const server = createServer(async (req, res) => {
|
|
55
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
56
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
57
|
+
console.error(`${timestamp} ${req.method} ${url.pathname}`);
|
|
58
|
+
if (cors) {
|
|
59
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
60
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
61
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
62
|
+
if (req.method === "OPTIONS") {
|
|
63
|
+
res.writeHead(204);
|
|
64
|
+
res.end();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!url.pathname.startsWith(basePath)) {
|
|
69
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
70
|
+
res.end(JSON.stringify({
|
|
71
|
+
code: 1,
|
|
72
|
+
error: "Not found"
|
|
73
|
+
}));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const headers = new Headers();
|
|
77
|
+
for (const [key, value] of Object.entries(req.headers)) if (value) headers.append(key, Array.isArray(value) ? value.join(", ") : value);
|
|
78
|
+
const chunks = [];
|
|
79
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
80
|
+
const body = Buffer.concat(chunks);
|
|
81
|
+
const request = new Request(`http://${req.headers.host}${req.url}`, {
|
|
82
|
+
method: req.method,
|
|
83
|
+
headers,
|
|
84
|
+
body: body.length > 0 ? body : void 0
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
const response = await handler(request);
|
|
88
|
+
res.writeHead(response.status, { "Content-Type": response.headers.get("Content-Type") || "application/json" });
|
|
89
|
+
const responseBody = await response.text();
|
|
90
|
+
res.end(responseBody);
|
|
91
|
+
console.error(`${timestamp} ${req.method} ${url.pathname} status=${response.status}`);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(`Error handling request:`, error);
|
|
94
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
95
|
+
res.end(JSON.stringify({
|
|
96
|
+
code: 5,
|
|
97
|
+
error: "Internal server error"
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
await new Promise((resolve, reject) => {
|
|
102
|
+
server.listen(port, host, () => {
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
server.on("error", reject);
|
|
106
|
+
});
|
|
107
|
+
const shutdown = () => {
|
|
108
|
+
console.error("\nShutting down server...");
|
|
109
|
+
server.close(() => {
|
|
110
|
+
process.exit(0);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
process.on("SIGINT", shutdown);
|
|
114
|
+
process.on("SIGTERM", shutdown);
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
host,
|
|
118
|
+
port,
|
|
119
|
+
path: basePath,
|
|
120
|
+
url: `http://${host}:${port}${basePath}`,
|
|
121
|
+
mounts
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Format serve result for output
|
|
126
|
+
*/
|
|
127
|
+
function formatServeOutput(result) {
|
|
128
|
+
const lines = [];
|
|
129
|
+
lines.push("AFS HTTP Server starting...");
|
|
130
|
+
lines.push("Mounted providers:");
|
|
131
|
+
for (const mount of result.mounts) lines.push(` ${mount.path.padEnd(20)} ${mount.provider}`);
|
|
132
|
+
lines.push("");
|
|
133
|
+
lines.push(`Listening on: ${result.url}`);
|
|
134
|
+
lines.push("Press Ctrl+C to stop");
|
|
135
|
+
return lines.join("\n");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
export { formatServeOutput, serveCommand };
|
|
140
|
+
//# 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\";\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(\"AFS HTTP Server starting...\");\n lines.push(\"Mounted providers:\");\n\n for (const mount of result.mounts) {\n lines.push(` ${mount.path.padEnd(20)} ${mount.provider}`);\n }\n\n lines.push(\"\");\n lines.push(`Listening on: ${result.url}`);\n lines.push(\"Press Ctrl+C to stop\");\n\n return lines.join(\"\\n\");\n}\n"],"mappings":"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"}
|
package/dist/config/loader.cjs
CHANGED
|
@@ -115,16 +115,27 @@ var ConfigLoader = class {
|
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* Merge multiple configs, checking for duplicate mount paths
|
|
118
|
+
* For serve config, later (more specific) configs override earlier ones
|
|
118
119
|
*/
|
|
119
120
|
mergeConfigs(configs) {
|
|
120
121
|
const allMounts = [];
|
|
121
122
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
let mergedServe;
|
|
124
|
+
for (const config of configs) {
|
|
125
|
+
for (const mount of config.mounts) {
|
|
126
|
+
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.`);
|
|
127
|
+
seenPaths.set(mount.path, mount.uri);
|
|
128
|
+
allMounts.push(mount);
|
|
129
|
+
}
|
|
130
|
+
if (config.serve) mergedServe = mergedServe ? {
|
|
131
|
+
...mergedServe,
|
|
132
|
+
...config.serve
|
|
133
|
+
} : config.serve;
|
|
126
134
|
}
|
|
127
|
-
return {
|
|
135
|
+
return {
|
|
136
|
+
mounts: allMounts,
|
|
137
|
+
serve: mergedServe
|
|
138
|
+
};
|
|
128
139
|
}
|
|
129
140
|
/**
|
|
130
141
|
* Find project root by looking for .git
|
package/dist/config/loader.mjs
CHANGED
|
@@ -114,16 +114,27 @@ var ConfigLoader = class {
|
|
|
114
114
|
}
|
|
115
115
|
/**
|
|
116
116
|
* Merge multiple configs, checking for duplicate mount paths
|
|
117
|
+
* For serve config, later (more specific) configs override earlier ones
|
|
117
118
|
*/
|
|
118
119
|
mergeConfigs(configs) {
|
|
119
120
|
const allMounts = [];
|
|
120
121
|
const seenPaths = /* @__PURE__ */ new Map();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
122
|
+
let mergedServe;
|
|
123
|
+
for (const config of configs) {
|
|
124
|
+
for (const mount of config.mounts) {
|
|
125
|
+
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.`);
|
|
126
|
+
seenPaths.set(mount.path, mount.uri);
|
|
127
|
+
allMounts.push(mount);
|
|
128
|
+
}
|
|
129
|
+
if (config.serve) mergedServe = mergedServe ? {
|
|
130
|
+
...mergedServe,
|
|
131
|
+
...config.serve
|
|
132
|
+
} : config.serve;
|
|
125
133
|
}
|
|
126
|
-
return {
|
|
134
|
+
return {
|
|
135
|
+
mounts: allMounts,
|
|
136
|
+
serve: mergedServe
|
|
137
|
+
};
|
|
127
138
|
}
|
|
128
139
|
/**
|
|
129
140
|
* Find project root by looking for .git
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.mjs","names":[],"sources":["../../src/config/loader.ts"],"sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { parse } from \"smol-toml\";\nimport { resolveEnvVarsInObject } from \"./env.js\";\nimport { type AFSConfig, ConfigSchema, type MountConfig } from \"./schema.js\";\n\nexport const CONFIG_DIR_NAME = \".afs-config\";\nexport const CONFIG_FILE_NAME = \"config.toml\";\n\nexport interface ConfigLoaderOptions {\n /** Custom path to user-level config directory (for testing) */\n userConfigDir?: string;\n}\n\n/**\n * Loads and merges AFS configuration from multiple layers\n *\n * Layer priority (lowest to highest):\n * 1. User-level: ~/.afs-config/config.toml\n * 2. All intermediate directories from project root to cwd\n *\n * Example: if cwd is /project/packages/cli, configs are merged from:\n * ~/.afs-config/config.toml (user)\n * /project/.afs-config/config.toml (project root, has .git)\n * /project/packages/.afs-config/config.toml (intermediate)\n * /project/packages/cli/.afs-config/config.toml (cwd)\n */\nexport class ConfigLoader {\n private userConfigDir: string;\n\n constructor(options: ConfigLoaderOptions = {}) {\n this.userConfigDir = options.userConfigDir ?? join(homedir(), CONFIG_DIR_NAME);\n }\n\n /**\n * Load and merge configuration from all layers\n *\n * @param cwd - Current working directory (defaults to process.cwd())\n * @returns Merged configuration\n * @throws Error on invalid config, TOML parse error, or duplicate mount paths\n */\n async load(cwd: string = process.cwd()): Promise<AFSConfig> {\n const configPaths = await this.getConfigPaths(cwd);\n const configs: AFSConfig[] = [];\n\n for (const configPath of configPaths) {\n const config = await this.loadSingleConfig(configPath);\n configs.push(config);\n }\n\n return this.mergeConfigs(configs);\n }\n\n /**\n * Get paths to all existing config files\n *\n * Collects configs from:\n * 1. User-level: ~/.afs-config/config.toml\n * 2. Project root (or topmost .afs-config dir) to cwd: all .afs-config/config.toml files\n */\n async getConfigPaths(cwd: string = process.cwd()): Promise<string[]> {\n const paths: string[] = [];\n\n // 1. User-level config\n const userConfigPath = join(this.userConfigDir, CONFIG_FILE_NAME);\n if (await this.fileExists(userConfigPath)) {\n paths.push(userConfigPath);\n }\n\n // 2. Find project root (look for .git going up)\n const projectRoot = await this.findProjectRoot(cwd);\n\n // 3. Determine start directory\n // If project root found, use it; otherwise find topmost .afs-config directory\n const startDir = projectRoot ?? (await this.findTopmostAfsDir(cwd)) ?? cwd;\n\n // 4. Collect all config files from start to cwd\n const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd);\n paths.push(...intermediatePaths);\n\n return paths;\n }\n\n /**\n * Find the topmost directory containing .afs-config from startDir going up\n */\n private async findTopmostAfsDir(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n let topmostAfsDir: string | null = null;\n\n while (true) {\n if (await this.fileExists(join(currentDir, CONFIG_DIR_NAME))) {\n topmostAfsDir = currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n break;\n }\n currentDir = parentDir;\n }\n\n return topmostAfsDir;\n }\n\n /**\n * Collect all config files from startDir to endDir (inclusive)\n * Returns paths in order from startDir to endDir (parent to child)\n */\n private async collectConfigsFromTo(startDir: string, endDir: string): Promise<string[]> {\n const paths: string[] = [];\n\n // Build list of directories from startDir to endDir\n const dirs: string[] = [];\n let current = endDir;\n\n while (true) {\n dirs.unshift(current); // prepend to maintain parent-to-child order\n\n if (current === startDir) {\n break;\n }\n\n const parent = dirname(current);\n if (parent === current) {\n // Reached filesystem root without finding startDir\n // This shouldn't happen if startDir is an ancestor of endDir\n break;\n }\n current = parent;\n }\n\n // Check each directory for config file\n for (const dir of dirs) {\n const configPath = join(dir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n if (await this.fileExists(configPath)) {\n paths.push(configPath);\n }\n }\n\n return paths;\n }\n\n /**\n * Load a single config file\n */\n private async loadSingleConfig(configPath: string): Promise<AFSConfig> {\n const content = await readFile(configPath, \"utf-8\");\n\n let parsed: unknown;\n try {\n parsed = parse(content);\n } catch (error) {\n throw new Error(\n `Failed to parse TOML config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Resolve environment variables\n const resolved = resolveEnvVarsInObject(parsed);\n\n // Validate against schema\n const result = ConfigSchema.safeParse(resolved);\n if (!result.success) {\n const errors = result.error.errors.map((e) => `${e.path.join(\".\")}: ${e.message}`).join(\"; \");\n throw new Error(`Invalid config at ${configPath}: ${errors}`);\n }\n\n return result.data;\n }\n\n /**\n * Merge multiple configs, checking for duplicate mount paths\n */\n private mergeConfigs(configs: AFSConfig[]): AFSConfig {\n const allMounts: MountConfig[] = [];\n const seenPaths = new Map<string, string>(); // path -> source file info\n\n for (const config of configs) {\n for (const mount of config.mounts) {\n if (seenPaths.has(mount.path)) {\n throw new Error(\n `Duplicate mount path \"${mount.path}\" found in configuration. ` +\n `Mount paths must be unique across all config files.`,\n );\n }\n seenPaths.set(mount.path, mount.uri);\n allMounts.push(mount);\n }\n }\n\n return { mounts: allMounts };\n }\n\n /**\n * Find project root by looking for .git\n * Note: Only .git is used as project root marker, not .afs-config,\n * because .afs-config can exist at multiple levels for hierarchical config\n */\n private async findProjectRoot(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n\n while (true) {\n // Check for .git directory\n if (await this.fileExists(join(currentDir, \".git\"))) {\n return currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n return null;\n }\n currentDir = parentDir;\n }\n }\n\n /**\n * Check if a file or directory exists\n */\n private async fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// Default singleton instance\nexport const configLoader = new ConfigLoader();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"loader.mjs","names":[],"sources":["../../src/config/loader.ts"],"sourcesContent":["import { access, readFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { parse } from \"smol-toml\";\nimport { resolveEnvVarsInObject } from \"./env.js\";\nimport { type AFSConfig, ConfigSchema, type MountConfig, type ServeConfig } from \"./schema.js\";\n\nexport const CONFIG_DIR_NAME = \".afs-config\";\nexport const CONFIG_FILE_NAME = \"config.toml\";\n\nexport interface ConfigLoaderOptions {\n /** Custom path to user-level config directory (for testing) */\n userConfigDir?: string;\n}\n\n/**\n * Loads and merges AFS configuration from multiple layers\n *\n * Layer priority (lowest to highest):\n * 1. User-level: ~/.afs-config/config.toml\n * 2. All intermediate directories from project root to cwd\n *\n * Example: if cwd is /project/packages/cli, configs are merged from:\n * ~/.afs-config/config.toml (user)\n * /project/.afs-config/config.toml (project root, has .git)\n * /project/packages/.afs-config/config.toml (intermediate)\n * /project/packages/cli/.afs-config/config.toml (cwd)\n */\nexport class ConfigLoader {\n private userConfigDir: string;\n\n constructor(options: ConfigLoaderOptions = {}) {\n this.userConfigDir = options.userConfigDir ?? join(homedir(), CONFIG_DIR_NAME);\n }\n\n /**\n * Load and merge configuration from all layers\n *\n * @param cwd - Current working directory (defaults to process.cwd())\n * @returns Merged configuration\n * @throws Error on invalid config, TOML parse error, or duplicate mount paths\n */\n async load(cwd: string = process.cwd()): Promise<AFSConfig> {\n const configPaths = await this.getConfigPaths(cwd);\n const configs: AFSConfig[] = [];\n\n for (const configPath of configPaths) {\n const config = await this.loadSingleConfig(configPath);\n configs.push(config);\n }\n\n return this.mergeConfigs(configs);\n }\n\n /**\n * Get paths to all existing config files\n *\n * Collects configs from:\n * 1. User-level: ~/.afs-config/config.toml\n * 2. Project root (or topmost .afs-config dir) to cwd: all .afs-config/config.toml files\n */\n async getConfigPaths(cwd: string = process.cwd()): Promise<string[]> {\n const paths: string[] = [];\n\n // 1. User-level config\n const userConfigPath = join(this.userConfigDir, CONFIG_FILE_NAME);\n if (await this.fileExists(userConfigPath)) {\n paths.push(userConfigPath);\n }\n\n // 2. Find project root (look for .git going up)\n const projectRoot = await this.findProjectRoot(cwd);\n\n // 3. Determine start directory\n // If project root found, use it; otherwise find topmost .afs-config directory\n const startDir = projectRoot ?? (await this.findTopmostAfsDir(cwd)) ?? cwd;\n\n // 4. Collect all config files from start to cwd\n const intermediatePaths = await this.collectConfigsFromTo(startDir, cwd);\n paths.push(...intermediatePaths);\n\n return paths;\n }\n\n /**\n * Find the topmost directory containing .afs-config from startDir going up\n */\n private async findTopmostAfsDir(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n let topmostAfsDir: string | null = null;\n\n while (true) {\n if (await this.fileExists(join(currentDir, CONFIG_DIR_NAME))) {\n topmostAfsDir = currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n break;\n }\n currentDir = parentDir;\n }\n\n return topmostAfsDir;\n }\n\n /**\n * Collect all config files from startDir to endDir (inclusive)\n * Returns paths in order from startDir to endDir (parent to child)\n */\n private async collectConfigsFromTo(startDir: string, endDir: string): Promise<string[]> {\n const paths: string[] = [];\n\n // Build list of directories from startDir to endDir\n const dirs: string[] = [];\n let current = endDir;\n\n while (true) {\n dirs.unshift(current); // prepend to maintain parent-to-child order\n\n if (current === startDir) {\n break;\n }\n\n const parent = dirname(current);\n if (parent === current) {\n // Reached filesystem root without finding startDir\n // This shouldn't happen if startDir is an ancestor of endDir\n break;\n }\n current = parent;\n }\n\n // Check each directory for config file\n for (const dir of dirs) {\n const configPath = join(dir, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n if (await this.fileExists(configPath)) {\n paths.push(configPath);\n }\n }\n\n return paths;\n }\n\n /**\n * Load a single config file\n */\n private async loadSingleConfig(configPath: string): Promise<AFSConfig> {\n const content = await readFile(configPath, \"utf-8\");\n\n let parsed: unknown;\n try {\n parsed = parse(content);\n } catch (error) {\n throw new Error(\n `Failed to parse TOML config at ${configPath}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Resolve environment variables\n const resolved = resolveEnvVarsInObject(parsed);\n\n // Validate against schema\n const result = ConfigSchema.safeParse(resolved);\n if (!result.success) {\n const errors = result.error.errors.map((e) => `${e.path.join(\".\")}: ${e.message}`).join(\"; \");\n throw new Error(`Invalid config at ${configPath}: ${errors}`);\n }\n\n return result.data;\n }\n\n /**\n * Merge multiple configs, checking for duplicate mount paths\n * For serve config, later (more specific) configs override earlier ones\n */\n private mergeConfigs(configs: AFSConfig[]): AFSConfig {\n const allMounts: MountConfig[] = [];\n const seenPaths = new Map<string, string>(); // path -> source file info\n let mergedServe: ServeConfig | undefined;\n\n for (const config of configs) {\n // Merge mounts\n for (const mount of config.mounts) {\n if (seenPaths.has(mount.path)) {\n throw new Error(\n `Duplicate mount path \"${mount.path}\" found in configuration. ` +\n `Mount paths must be unique across all config files.`,\n );\n }\n seenPaths.set(mount.path, mount.uri);\n allMounts.push(mount);\n }\n\n // Merge serve config (later configs override earlier ones)\n if (config.serve) {\n mergedServe = mergedServe ? { ...mergedServe, ...config.serve } : config.serve;\n }\n }\n\n return { mounts: allMounts, serve: mergedServe };\n }\n\n /**\n * Find project root by looking for .git\n * Note: Only .git is used as project root marker, not .afs-config,\n * because .afs-config can exist at multiple levels for hierarchical config\n */\n private async findProjectRoot(startDir: string): Promise<string | null> {\n let currentDir = startDir;\n\n while (true) {\n // Check for .git directory\n if (await this.fileExists(join(currentDir, \".git\"))) {\n return currentDir;\n }\n\n const parentDir = dirname(currentDir);\n if (parentDir === currentDir) {\n // Reached filesystem root\n return null;\n }\n currentDir = parentDir;\n }\n }\n\n /**\n * Check if a file or directory exists\n */\n private async fileExists(path: string): Promise<boolean> {\n try {\n await access(path);\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// Default singleton instance\nexport const configLoader = new ConfigLoader();\n"],"mappings":"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"}
|
|
@@ -16,7 +16,7 @@ async function createProvider(mount) {
|
|
|
16
16
|
case "sqlite": return createSQLiteProvider(mount, parsed.path);
|
|
17
17
|
case "json": return createJSONProvider(mount, parsed.path);
|
|
18
18
|
case "http":
|
|
19
|
-
case "https":
|
|
19
|
+
case "https": return createHttpProvider(mount);
|
|
20
20
|
default: throw new Error(`Unknown URI scheme: ${parsed.scheme}`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -40,6 +40,14 @@ async function createGitProvider(mount, repoPath, params, host) {
|
|
|
40
40
|
branches: params.branch ? [params.branch] : void 0,
|
|
41
41
|
...mount.options
|
|
42
42
|
});
|
|
43
|
+
if (repoPath.startsWith("https://") || repoPath.startsWith("http://")) return new AFSGit({
|
|
44
|
+
remoteUrl: repoPath,
|
|
45
|
+
name: mount.path.slice(1).replace(/\//g, "-") || "git",
|
|
46
|
+
description: mount.description,
|
|
47
|
+
accessMode: mount.access_mode ?? "readonly",
|
|
48
|
+
branches: params.branch ? [params.branch] : void 0,
|
|
49
|
+
...mount.options
|
|
50
|
+
});
|
|
43
51
|
return new AFSGit({
|
|
44
52
|
repoPath,
|
|
45
53
|
name: mount.path.slice(1).replace(/\//g, "-") || "git",
|
|
@@ -69,6 +77,16 @@ async function createJSONProvider(mount, jsonPath) {
|
|
|
69
77
|
...mount.options
|
|
70
78
|
});
|
|
71
79
|
}
|
|
80
|
+
async function createHttpProvider(mount) {
|
|
81
|
+
const { AFSHttpClient } = await import("@aigne/afs-http");
|
|
82
|
+
return new AFSHttpClient({
|
|
83
|
+
url: mount.uri,
|
|
84
|
+
name: mount.path.slice(1).replace(/\//g, "-") || "http",
|
|
85
|
+
description: mount.description,
|
|
86
|
+
accessMode: mount.access_mode,
|
|
87
|
+
...mount.options
|
|
88
|
+
});
|
|
89
|
+
}
|
|
72
90
|
|
|
73
91
|
//#endregion
|
|
74
92
|
exports.createProvider = createProvider;
|
|
@@ -16,7 +16,7 @@ async function createProvider(mount) {
|
|
|
16
16
|
case "sqlite": return createSQLiteProvider(mount, parsed.path);
|
|
17
17
|
case "json": return createJSONProvider(mount, parsed.path);
|
|
18
18
|
case "http":
|
|
19
|
-
case "https":
|
|
19
|
+
case "https": return createHttpProvider(mount);
|
|
20
20
|
default: throw new Error(`Unknown URI scheme: ${parsed.scheme}`);
|
|
21
21
|
}
|
|
22
22
|
}
|
|
@@ -40,6 +40,14 @@ async function createGitProvider(mount, repoPath, params, host) {
|
|
|
40
40
|
branches: params.branch ? [params.branch] : void 0,
|
|
41
41
|
...mount.options
|
|
42
42
|
});
|
|
43
|
+
if (repoPath.startsWith("https://") || repoPath.startsWith("http://")) return new AFSGit({
|
|
44
|
+
remoteUrl: repoPath,
|
|
45
|
+
name: mount.path.slice(1).replace(/\//g, "-") || "git",
|
|
46
|
+
description: mount.description,
|
|
47
|
+
accessMode: mount.access_mode ?? "readonly",
|
|
48
|
+
branches: params.branch ? [params.branch] : void 0,
|
|
49
|
+
...mount.options
|
|
50
|
+
});
|
|
43
51
|
return new AFSGit({
|
|
44
52
|
repoPath,
|
|
45
53
|
name: mount.path.slice(1).replace(/\//g, "-") || "git",
|
|
@@ -69,6 +77,16 @@ async function createJSONProvider(mount, jsonPath) {
|
|
|
69
77
|
...mount.options
|
|
70
78
|
});
|
|
71
79
|
}
|
|
80
|
+
async function createHttpProvider(mount) {
|
|
81
|
+
const { AFSHttpClient } = await import("@aigne/afs-http");
|
|
82
|
+
return new AFSHttpClient({
|
|
83
|
+
url: mount.uri,
|
|
84
|
+
name: mount.path.slice(1).replace(/\//g, "-") || "http",
|
|
85
|
+
description: mount.description,
|
|
86
|
+
accessMode: mount.access_mode,
|
|
87
|
+
...mount.options
|
|
88
|
+
});
|
|
89
|
+
}
|
|
72
90
|
|
|
73
91
|
//#endregion
|
|
74
92
|
export { createProvider };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provider-factory.mjs","names":[],"sources":["../../src/config/provider-factory.ts"],"sourcesContent":["import type { AFSModule } from \"@aigne/afs\";\nimport type { MountConfig } from \"./schema.js\";\nimport { parseURI } from \"./uri-parser.js\";\n\n/**\n * Create an AFS provider from a mount configuration\n *\n * @param mount - Mount configuration with URI and options\n * @returns Provider instance\n * @throws Error if scheme is unknown or not implemented\n */\nexport async function createProvider(mount: MountConfig): Promise<AFSModule> {\n const parsed = parseURI(mount.uri);\n\n switch (parsed.scheme) {\n case \"fs\":\n return createAFSFSProvider(mount, parsed.path);\n\n case \"git\":\n return createGitProvider(mount, parsed.path, parsed.params, parsed.host);\n\n case \"sqlite\":\n return createSQLiteProvider(mount, parsed.path);\n\n case \"json\":\n return createJSONProvider(mount, parsed.path);\n\n case \"http\":\n case \"https\":\n
|
|
1
|
+
{"version":3,"file":"provider-factory.mjs","names":[],"sources":["../../src/config/provider-factory.ts"],"sourcesContent":["import type { AFSModule } from \"@aigne/afs\";\nimport type { MountConfig } from \"./schema.js\";\nimport { parseURI } from \"./uri-parser.js\";\n\n/**\n * Create an AFS provider from a mount configuration\n *\n * @param mount - Mount configuration with URI and options\n * @returns Provider instance\n * @throws Error if scheme is unknown or not implemented\n */\nexport async function createProvider(mount: MountConfig): Promise<AFSModule> {\n const parsed = parseURI(mount.uri);\n\n switch (parsed.scheme) {\n case \"fs\":\n return createAFSFSProvider(mount, parsed.path);\n\n case \"git\":\n return createGitProvider(mount, parsed.path, parsed.params, parsed.host);\n\n case \"sqlite\":\n return createSQLiteProvider(mount, parsed.path);\n\n case \"json\":\n return createJSONProvider(mount, parsed.path);\n\n case \"http\":\n case \"https\":\n return createHttpProvider(mount);\n\n default:\n throw new Error(`Unknown URI scheme: ${parsed.scheme}`);\n }\n}\n\nasync function createAFSFSProvider(mount: MountConfig, localPath: string): Promise<AFSModule> {\n const { AFSFS } = await import(\"@aigne/afs-fs\");\n\n return new AFSFS({\n localPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"fs\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createGitProvider(\n mount: MountConfig,\n repoPath: string,\n params: Record<string, string>,\n host?: string,\n): Promise<AFSModule> {\n const { AFSGit } = await import(\"@aigne/afs-git\");\n\n // For remote repos (SSH-style or https), use remoteUrl\n // For local repos, use repoPath directly\n if (host) {\n const remoteUrl = `git@${host}:${repoPath}`;\n return new AFSGit({\n remoteUrl,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n }\n\n // Check if repoPath is actually a remote URL (https:// or http://)\n if (repoPath.startsWith(\"https://\") || repoPath.startsWith(\"http://\")) {\n return new AFSGit({\n remoteUrl: repoPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n }\n\n return new AFSGit({\n repoPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"git\",\n description: mount.description,\n accessMode: mount.access_mode ?? \"readonly\",\n branches: params.branch ? [params.branch] : undefined,\n ...mount.options,\n });\n}\n\nasync function createSQLiteProvider(mount: MountConfig, dbPath: string): Promise<AFSModule> {\n const { SQLiteAFS } = await import(\"@aigne/afs-sqlite\");\n\n // SQLiteAFS auto-initializes via onMount when mounted to AFS\n return new SQLiteAFS({\n url: `file:${dbPath}`,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"sqlite\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createJSONProvider(mount: MountConfig, jsonPath: string): Promise<AFSModule> {\n const { AFSJSON } = await import(\"@aigne/afs-json\");\n\n return new AFSJSON({\n jsonPath,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"json\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n\nasync function createHttpProvider(mount: MountConfig): Promise<AFSModule> {\n const { AFSHttpClient } = await import(\"@aigne/afs-http\");\n\n return new AFSHttpClient({\n url: mount.uri,\n name: mount.path.slice(1).replace(/\\//g, \"-\") || \"http\",\n description: mount.description,\n accessMode: mount.access_mode,\n ...mount.options,\n });\n}\n"],"mappings":";;;;;;;;;;AAWA,eAAsB,eAAe,OAAwC;CAC3E,MAAM,SAAS,SAAS,MAAM,IAAI;AAElC,SAAQ,OAAO,QAAf;EACE,KAAK,KACH,QAAO,oBAAoB,OAAO,OAAO,KAAK;EAEhD,KAAK,MACH,QAAO,kBAAkB,OAAO,OAAO,MAAM,OAAO,QAAQ,OAAO,KAAK;EAE1E,KAAK,SACH,QAAO,qBAAqB,OAAO,OAAO,KAAK;EAEjD,KAAK,OACH,QAAO,mBAAmB,OAAO,OAAO,KAAK;EAE/C,KAAK;EACL,KAAK,QACH,QAAO,mBAAmB,MAAM;EAElC,QACE,OAAM,IAAI,MAAM,uBAAuB,OAAO,SAAS;;;AAI7D,eAAe,oBAAoB,OAAoB,WAAuC;CAC5F,MAAM,EAAE,UAAU,MAAM,OAAO;AAE/B,QAAO,IAAI,MAAM;EACf;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,kBACb,OACA,UACA,QACA,MACoB;CACpB,MAAM,EAAE,WAAW,MAAM,OAAO;AAIhC,KAAI,KAEF,QAAO,IAAI,OAAO;EAChB,WAFgB,OAAO,KAAK,GAAG;EAG/B,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;AAIJ,KAAI,SAAS,WAAW,WAAW,IAAI,SAAS,WAAW,UAAU,CACnE,QAAO,IAAI,OAAO;EAChB,WAAW;EACX,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;AAGJ,QAAO,IAAI,OAAO;EAChB;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM,eAAe;EACjC,UAAU,OAAO,SAAS,CAAC,OAAO,OAAO,GAAG;EAC5C,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,qBAAqB,OAAoB,QAAoC;CAC1F,MAAM,EAAE,cAAc,MAAM,OAAO;AAGnC,QAAO,IAAI,UAAU;EACnB,KAAK,QAAQ;EACb,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,mBAAmB,OAAoB,UAAsC;CAC1F,MAAM,EAAE,YAAY,MAAM,OAAO;AAEjC,QAAO,IAAI,QAAQ;EACjB;EACA,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC;;AAGJ,eAAe,mBAAmB,OAAwC;CACxE,MAAM,EAAE,kBAAkB,MAAM,OAAO;AAEvC,QAAO,IAAI,cAAc;EACvB,KAAK,MAAM;EACX,MAAM,MAAM,KAAK,MAAM,EAAE,CAAC,QAAQ,OAAO,IAAI,IAAI;EACjD,aAAa,MAAM;EACnB,YAAY,MAAM;EAClB,GAAG,MAAM;EACV,CAAC"}
|
package/dist/config/schema.cjs
CHANGED
|
@@ -14,9 +14,23 @@ const MountSchema = zod.z.object({
|
|
|
14
14
|
options: zod.z.record(zod.z.unknown()).optional()
|
|
15
15
|
});
|
|
16
16
|
/**
|
|
17
|
+
* Serve configuration schema
|
|
18
|
+
*/
|
|
19
|
+
const ServeSchema = zod.z.object({
|
|
20
|
+
host: zod.z.string().default("localhost"),
|
|
21
|
+
port: zod.z.number().int().positive().default(3e3),
|
|
22
|
+
path: zod.z.string().default("/afs"),
|
|
23
|
+
readonly: zod.z.boolean().default(false),
|
|
24
|
+
cors: zod.z.boolean().default(false),
|
|
25
|
+
max_body_size: zod.z.number().int().positive().default(10 * 1024 * 1024)
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
17
28
|
* Root configuration schema for afs.toml
|
|
18
29
|
*/
|
|
19
|
-
const ConfigSchema = zod.z.object({
|
|
30
|
+
const ConfigSchema = zod.z.object({
|
|
31
|
+
mounts: zod.z.array(MountSchema).default([]),
|
|
32
|
+
serve: ServeSchema.optional()
|
|
33
|
+
});
|
|
20
34
|
|
|
21
35
|
//#endregion
|
|
22
36
|
exports.ConfigSchema = ConfigSchema;
|
package/dist/config/schema.mjs
CHANGED
|
@@ -13,9 +13,23 @@ const MountSchema = z.object({
|
|
|
13
13
|
options: z.record(z.unknown()).optional()
|
|
14
14
|
});
|
|
15
15
|
/**
|
|
16
|
+
* Serve configuration schema
|
|
17
|
+
*/
|
|
18
|
+
const ServeSchema = z.object({
|
|
19
|
+
host: z.string().default("localhost"),
|
|
20
|
+
port: z.number().int().positive().default(3e3),
|
|
21
|
+
path: z.string().default("/afs"),
|
|
22
|
+
readonly: z.boolean().default(false),
|
|
23
|
+
cors: z.boolean().default(false),
|
|
24
|
+
max_body_size: z.number().int().positive().default(10 * 1024 * 1024)
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
16
27
|
* Root configuration schema for afs.toml
|
|
17
28
|
*/
|
|
18
|
-
const ConfigSchema = z.object({
|
|
29
|
+
const ConfigSchema = z.object({
|
|
30
|
+
mounts: z.array(MountSchema).default([]),
|
|
31
|
+
serve: ServeSchema.optional()
|
|
32
|
+
});
|
|
19
33
|
|
|
20
34
|
//#endregion
|
|
21
35
|
export { ConfigSchema };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.mjs","names":[],"sources":["../../src/config/schema.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * Mount configuration schema\n */\nexport const MountSchema = z.object({\n /** Mount path (must be absolute, starting with /) */\n path: z.string().startsWith(\"/\", \"Mount path must be absolute (start with /)\"),\n\n /** Provider URI (e.g., fs:///path, git:///repo?branch=main) */\n uri: z.string().min(1, \"URI is required\"),\n\n /** Human/LLM readable description */\n description: z.string().optional(),\n\n /** Access mode: readonly or readwrite */\n access_mode: z.enum([\"readonly\", \"readwrite\"]).optional(),\n\n /** Authentication string (supports ${ENV_VAR} references) */\n auth: z.string().optional(),\n\n /** Provider-specific options (passed through to provider) */\n options: z.record(z.unknown()).optional(),\n});\n\n/**\n * Root configuration schema for afs.toml\n */\nexport const ConfigSchema = z.object({\n /** List of mount configurations */\n mounts: z.array(MountSchema).default([]),\n});\n\n/** Type for a single mount configuration */\nexport type MountConfig = z.infer<typeof MountSchema>;\n\n/** Type for the root AFS configuration */\nexport type AFSConfig = z.infer<typeof ConfigSchema>;\n"],"mappings":";;;;;;AAKA,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,WAAW,KAAK,6CAA6C;CAG9E,KAAK,EAAE,QAAQ,CAAC,IAAI,GAAG,kBAAkB;CAGzC,aAAa,EAAE,QAAQ,CAAC,UAAU;CAGlC,aAAa,EAAE,KAAK,CAAC,YAAY,YAAY,CAAC,CAAC,UAAU;CAGzD,MAAM,EAAE,QAAQ,CAAC,UAAU;CAG3B,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,UAAU;CAC1C,CAAC;;;;AAKF,MAAa,
|
|
1
|
+
{"version":3,"file":"schema.mjs","names":[],"sources":["../../src/config/schema.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/**\n * Mount configuration schema\n */\nexport const MountSchema = z.object({\n /** Mount path (must be absolute, starting with /) */\n path: z.string().startsWith(\"/\", \"Mount path must be absolute (start with /)\"),\n\n /** Provider URI (e.g., fs:///path, git:///repo?branch=main) */\n uri: z.string().min(1, \"URI is required\"),\n\n /** Human/LLM readable description */\n description: z.string().optional(),\n\n /** Access mode: readonly or readwrite */\n access_mode: z.enum([\"readonly\", \"readwrite\"]).optional(),\n\n /** Authentication string (supports ${ENV_VAR} references) */\n auth: z.string().optional(),\n\n /** Provider-specific options (passed through to provider) */\n options: z.record(z.unknown()).optional(),\n});\n\n/**\n * Serve configuration schema\n */\nexport const ServeSchema = z.object({\n /** Host address to listen on */\n host: z.string().default(\"localhost\"),\n\n /** Port to listen on */\n port: z.number().int().positive().default(3000),\n\n /** Base path for the server */\n path: z.string().default(\"/afs\"),\n\n /** Run in readonly mode (disable write operations) */\n readonly: z.boolean().default(false),\n\n /** Enable CORS support */\n cors: z.boolean().default(false),\n\n /** Maximum request body size in bytes */\n max_body_size: z\n .number()\n .int()\n .positive()\n .default(10 * 1024 * 1024), // 10MB\n});\n\n/**\n * Root configuration schema for afs.toml\n */\nexport const ConfigSchema = z.object({\n /** List of mount configurations */\n mounts: z.array(MountSchema).default([]),\n\n /** HTTP server configuration */\n serve: ServeSchema.optional(),\n});\n\n/** Type for a single mount configuration */\nexport type MountConfig = z.infer<typeof MountSchema>;\n\n/** Type for serve configuration */\nexport type ServeConfig = z.infer<typeof ServeSchema>;\n\n/** Type for the root AFS configuration */\nexport type AFSConfig = z.infer<typeof ConfigSchema>;\n"],"mappings":";;;;;;AAKA,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,WAAW,KAAK,6CAA6C;CAG9E,KAAK,EAAE,QAAQ,CAAC,IAAI,GAAG,kBAAkB;CAGzC,aAAa,EAAE,QAAQ,CAAC,UAAU;CAGlC,aAAa,EAAE,KAAK,CAAC,YAAY,YAAY,CAAC,CAAC,UAAU;CAGzD,MAAM,EAAE,QAAQ,CAAC,UAAU;CAG3B,SAAS,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC,UAAU;CAC1C,CAAC;;;;AAKF,MAAa,cAAc,EAAE,OAAO;CAElC,MAAM,EAAE,QAAQ,CAAC,QAAQ,YAAY;CAGrC,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,QAAQ,IAAK;CAG/C,MAAM,EAAE,QAAQ,CAAC,QAAQ,OAAO;CAGhC,UAAU,EAAE,SAAS,CAAC,QAAQ,MAAM;CAGpC,MAAM,EAAE,SAAS,CAAC,QAAQ,MAAM;CAGhC,eAAe,EACZ,QAAQ,CACR,KAAK,CACL,UAAU,CACV,QAAQ,KAAK,OAAO,KAAK;CAC7B,CAAC;;;;AAKF,MAAa,eAAe,EAAE,OAAO;CAEnC,QAAQ,EAAE,MAAM,YAAY,CAAC,QAAQ,EAAE,CAAC;CAGxC,OAAO,YAAY,UAAU;CAC9B,CAAC"}
|
|
@@ -52,6 +52,23 @@ function parseHttpURI(uri, scheme) {
|
|
|
52
52
|
}
|
|
53
53
|
function parseLocalURI(uri, scheme) {
|
|
54
54
|
const withoutScheme = uri.slice(scheme.length + 3);
|
|
55
|
+
if (scheme === "git") {
|
|
56
|
+
const sshMatch = withoutScheme.match(/^([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+):(.+)$/);
|
|
57
|
+
if (sshMatch?.[1] && sshMatch[2]) {
|
|
58
|
+
const atIndex = sshMatch[1].indexOf("@");
|
|
59
|
+
return {
|
|
60
|
+
scheme: "git",
|
|
61
|
+
host: sshMatch[1].slice(atIndex + 1),
|
|
62
|
+
path: sshMatch[2],
|
|
63
|
+
params: {}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (withoutScheme.startsWith("https://") || withoutScheme.startsWith("http://")) return {
|
|
67
|
+
scheme: "git",
|
|
68
|
+
path: withoutScheme,
|
|
69
|
+
params: {}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
55
72
|
const queryIndex = withoutScheme.indexOf("?");
|
|
56
73
|
let path;
|
|
57
74
|
let queryString;
|
|
@@ -51,6 +51,23 @@ function parseHttpURI(uri, scheme) {
|
|
|
51
51
|
}
|
|
52
52
|
function parseLocalURI(uri, scheme) {
|
|
53
53
|
const withoutScheme = uri.slice(scheme.length + 3);
|
|
54
|
+
if (scheme === "git") {
|
|
55
|
+
const sshMatch = withoutScheme.match(/^([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+):(.+)$/);
|
|
56
|
+
if (sshMatch?.[1] && sshMatch[2]) {
|
|
57
|
+
const atIndex = sshMatch[1].indexOf("@");
|
|
58
|
+
return {
|
|
59
|
+
scheme: "git",
|
|
60
|
+
host: sshMatch[1].slice(atIndex + 1),
|
|
61
|
+
path: sshMatch[2],
|
|
62
|
+
params: {}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (withoutScheme.startsWith("https://") || withoutScheme.startsWith("http://")) return {
|
|
66
|
+
scheme: "git",
|
|
67
|
+
path: withoutScheme,
|
|
68
|
+
params: {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
54
71
|
const queryIndex = withoutScheme.indexOf("?");
|
|
55
72
|
let path;
|
|
56
73
|
let queryString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"uri-parser.mjs","names":[],"sources":["../../src/config/uri-parser.ts"],"sourcesContent":["/**\n * Supported URI schemes\n */\nexport type URIScheme = \"fs\" | \"git\" | \"sqlite\" | \"json\" | \"http\" | \"https\";\n\n/**\n * Parsed URI result\n */\nexport interface ParsedURI {\n /** URI scheme */\n scheme: URIScheme;\n /** Path component (decoded) */\n path: string;\n /** Query parameters */\n params: Record<string, string>;\n /** Host (for http/https only) */\n host?: string;\n /** Port (for http/https only) */\n port?: number;\n}\n\nconst SUPPORTED_SCHEMES = new Set<string>([\"fs\", \"git\", \"sqlite\", \"json\", \"http\", \"https\"]);\n\n/**\n * Parse an AFS URI into components\n *\n * Supported formats:\n * - fs:///path/to/dir\n * - git:///path/to/repo?branch=main\n * - git@github.com:user/repo.git (SSH-style)\n * - sqlite:///path/to/db.sqlite\n * - json:///path/to/config.json\n * - http://host:port/path\n * - https://host:port/path\n */\nexport function parseURI(uri: string): ParsedURI {\n if (!uri || uri.trim() === \"\") {\n throw new Error(\"URI cannot be empty\");\n }\n\n // Handle SSH-style git URLs: git@host:path\n const sshGitMatch = uri.match(/^git@([^:]+):(.+)$/);\n if (sshGitMatch?.[1] && sshGitMatch[2]) {\n return {\n scheme: \"git\",\n host: sshGitMatch[1],\n path: sshGitMatch[2],\n params: {},\n };\n }\n\n // Extract scheme\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//i);\n if (!schemeMatch?.[1]) {\n throw new Error(`Invalid URI format: ${uri}`);\n }\n\n const scheme = schemeMatch[1].toLowerCase();\n if (!SUPPORTED_SCHEMES.has(scheme)) {\n throw new Error(`Unknown URI scheme: ${scheme}`);\n }\n\n // For http/https, use URL API for proper parsing\n if (scheme === \"http\" || scheme === \"https\") {\n return parseHttpURI(uri, scheme as \"http\" | \"https\");\n }\n\n // For local schemes (fs, git, sqlite, json)\n return parseLocalURI(uri, scheme as \"fs\" | \"git\" | \"sqlite\" | \"json\");\n}\n\nfunction parseHttpURI(uri: string, scheme: \"http\" | \"https\"): ParsedURI {\n const url = new URL(uri);\n\n const params: Record<string, string> = {};\n url.searchParams.forEach((value, key) => {\n params[key] = value;\n });\n\n return {\n scheme,\n host: url.hostname,\n port: url.port ? Number.parseInt(url.port, 10) : undefined,\n path: url.pathname || \"/\",\n params,\n };\n}\n\nfunction parseLocalURI(uri: string, scheme: \"fs\" | \"git\" | \"sqlite\" | \"json\"): ParsedURI {\n // Remove scheme prefix (e.g., \"fs://\")\n const withoutScheme = uri.slice(scheme.length + 3);\n\n // Split path and query\n const queryIndex = withoutScheme.indexOf(\"?\");\n let path: string;\n let queryString: string | undefined;\n\n if (queryIndex >= 0) {\n path = withoutScheme.slice(0, queryIndex);\n queryString = withoutScheme.slice(queryIndex + 1);\n } else {\n path = withoutScheme;\n }\n\n // Decode URI components\n path = decodeURIComponent(path);\n\n // Parse query params\n const params: Record<string, string> = {};\n if (queryString) {\n const searchParams = new URLSearchParams(queryString);\n searchParams.forEach((value, key) => {\n params[key] = value;\n });\n }\n\n return {\n scheme,\n path,\n params,\n };\n}\n"],"mappings":";AAqBA,MAAM,oBAAoB,IAAI,IAAY;CAAC;CAAM;CAAO;CAAU;CAAQ;CAAQ;CAAQ,CAAC;;;;;;;;;;;;;AAc3F,SAAgB,SAAS,KAAwB;AAC/C,KAAI,CAAC,OAAO,IAAI,MAAM,KAAK,GACzB,OAAM,IAAI,MAAM,sBAAsB;CAIxC,MAAM,cAAc,IAAI,MAAM,qBAAqB;AACnD,KAAI,cAAc,MAAM,YAAY,GAClC,QAAO;EACL,QAAQ;EACR,MAAM,YAAY;EAClB,MAAM,YAAY;EAClB,QAAQ,EAAE;EACX;CAIH,MAAM,cAAc,IAAI,MAAM,kBAAkB;AAChD,KAAI,CAAC,cAAc,GACjB,OAAM,IAAI,MAAM,uBAAuB,MAAM;CAG/C,MAAM,SAAS,YAAY,GAAG,aAAa;AAC3C,KAAI,CAAC,kBAAkB,IAAI,OAAO,CAChC,OAAM,IAAI,MAAM,uBAAuB,SAAS;AAIlD,KAAI,WAAW,UAAU,WAAW,QAClC,QAAO,aAAa,KAAK,OAA2B;AAItD,QAAO,cAAc,KAAK,OAA2C;;AAGvE,SAAS,aAAa,KAAa,QAAqC;CACtE,MAAM,MAAM,IAAI,IAAI,IAAI;CAExB,MAAM,SAAiC,EAAE;AACzC,KAAI,aAAa,SAAS,OAAO,QAAQ;AACvC,SAAO,OAAO;GACd;AAEF,QAAO;EACL;EACA,MAAM,IAAI;EACV,MAAM,IAAI,OAAO,OAAO,SAAS,IAAI,MAAM,GAAG,GAAG;EACjD,MAAM,IAAI,YAAY;EACtB;EACD;;AAGH,SAAS,cAAc,KAAa,QAAqD;CAEvF,MAAM,gBAAgB,IAAI,MAAM,OAAO,SAAS,EAAE;
|
|
1
|
+
{"version":3,"file":"uri-parser.mjs","names":[],"sources":["../../src/config/uri-parser.ts"],"sourcesContent":["/**\n * Supported URI schemes\n */\nexport type URIScheme = \"fs\" | \"git\" | \"sqlite\" | \"json\" | \"http\" | \"https\";\n\n/**\n * Parsed URI result\n */\nexport interface ParsedURI {\n /** URI scheme */\n scheme: URIScheme;\n /** Path component (decoded) */\n path: string;\n /** Query parameters */\n params: Record<string, string>;\n /** Host (for http/https only) */\n host?: string;\n /** Port (for http/https only) */\n port?: number;\n}\n\nconst SUPPORTED_SCHEMES = new Set<string>([\"fs\", \"git\", \"sqlite\", \"json\", \"http\", \"https\"]);\n\n/**\n * Parse an AFS URI into components\n *\n * Supported formats:\n * - fs:///path/to/dir\n * - git:///path/to/repo?branch=main\n * - git@github.com:user/repo.git (SSH-style)\n * - sqlite:///path/to/db.sqlite\n * - json:///path/to/config.json\n * - http://host:port/path\n * - https://host:port/path\n */\nexport function parseURI(uri: string): ParsedURI {\n if (!uri || uri.trim() === \"\") {\n throw new Error(\"URI cannot be empty\");\n }\n\n // Handle SSH-style git URLs: git@host:path\n const sshGitMatch = uri.match(/^git@([^:]+):(.+)$/);\n if (sshGitMatch?.[1] && sshGitMatch[2]) {\n return {\n scheme: \"git\",\n host: sshGitMatch[1],\n path: sshGitMatch[2],\n params: {},\n };\n }\n\n // Extract scheme\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//i);\n if (!schemeMatch?.[1]) {\n throw new Error(`Invalid URI format: ${uri}`);\n }\n\n const scheme = schemeMatch[1].toLowerCase();\n if (!SUPPORTED_SCHEMES.has(scheme)) {\n throw new Error(`Unknown URI scheme: ${scheme}`);\n }\n\n // For http/https, use URL API for proper parsing\n if (scheme === \"http\" || scheme === \"https\") {\n return parseHttpURI(uri, scheme as \"http\" | \"https\");\n }\n\n // For local schemes (fs, git, sqlite, json)\n return parseLocalURI(uri, scheme as \"fs\" | \"git\" | \"sqlite\" | \"json\");\n}\n\nfunction parseHttpURI(uri: string, scheme: \"http\" | \"https\"): ParsedURI {\n const url = new URL(uri);\n\n const params: Record<string, string> = {};\n url.searchParams.forEach((value, key) => {\n params[key] = value;\n });\n\n return {\n scheme,\n host: url.hostname,\n port: url.port ? Number.parseInt(url.port, 10) : undefined,\n path: url.pathname || \"/\",\n params,\n };\n}\n\nfunction parseLocalURI(uri: string, scheme: \"fs\" | \"git\" | \"sqlite\" | \"json\"): ParsedURI {\n // Remove scheme prefix (e.g., \"fs://\")\n const withoutScheme = uri.slice(scheme.length + 3);\n\n // For git:// scheme, check if the path is actually an SSH-style URL (git@host:path)\n if (scheme === \"git\") {\n const sshMatch = withoutScheme.match(/^([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+):(.+)$/);\n if (sshMatch?.[1] && sshMatch[2]) {\n // Extract host from user@host format\n const atIndex = sshMatch[1].indexOf(\"@\");\n const host = sshMatch[1].slice(atIndex + 1);\n return {\n scheme: \"git\",\n host,\n path: sshMatch[2],\n params: {},\n };\n }\n\n // Also handle embedded https:// URLs (git://https://github.com/user/repo.git)\n if (withoutScheme.startsWith(\"https://\") || withoutScheme.startsWith(\"http://\")) {\n return {\n scheme: \"git\",\n path: withoutScheme, // Store full URL as path, provider will handle it\n params: {},\n };\n }\n }\n\n // Split path and query\n const queryIndex = withoutScheme.indexOf(\"?\");\n let path: string;\n let queryString: string | undefined;\n\n if (queryIndex >= 0) {\n path = withoutScheme.slice(0, queryIndex);\n queryString = withoutScheme.slice(queryIndex + 1);\n } else {\n path = withoutScheme;\n }\n\n // Decode URI components\n path = decodeURIComponent(path);\n\n // Parse query params\n const params: Record<string, string> = {};\n if (queryString) {\n const searchParams = new URLSearchParams(queryString);\n searchParams.forEach((value, key) => {\n params[key] = value;\n });\n }\n\n return {\n scheme,\n path,\n params,\n };\n}\n"],"mappings":";AAqBA,MAAM,oBAAoB,IAAI,IAAY;CAAC;CAAM;CAAO;CAAU;CAAQ;CAAQ;CAAQ,CAAC;;;;;;;;;;;;;AAc3F,SAAgB,SAAS,KAAwB;AAC/C,KAAI,CAAC,OAAO,IAAI,MAAM,KAAK,GACzB,OAAM,IAAI,MAAM,sBAAsB;CAIxC,MAAM,cAAc,IAAI,MAAM,qBAAqB;AACnD,KAAI,cAAc,MAAM,YAAY,GAClC,QAAO;EACL,QAAQ;EACR,MAAM,YAAY;EAClB,MAAM,YAAY;EAClB,QAAQ,EAAE;EACX;CAIH,MAAM,cAAc,IAAI,MAAM,kBAAkB;AAChD,KAAI,CAAC,cAAc,GACjB,OAAM,IAAI,MAAM,uBAAuB,MAAM;CAG/C,MAAM,SAAS,YAAY,GAAG,aAAa;AAC3C,KAAI,CAAC,kBAAkB,IAAI,OAAO,CAChC,OAAM,IAAI,MAAM,uBAAuB,SAAS;AAIlD,KAAI,WAAW,UAAU,WAAW,QAClC,QAAO,aAAa,KAAK,OAA2B;AAItD,QAAO,cAAc,KAAK,OAA2C;;AAGvE,SAAS,aAAa,KAAa,QAAqC;CACtE,MAAM,MAAM,IAAI,IAAI,IAAI;CAExB,MAAM,SAAiC,EAAE;AACzC,KAAI,aAAa,SAAS,OAAO,QAAQ;AACvC,SAAO,OAAO;GACd;AAEF,QAAO;EACL;EACA,MAAM,IAAI;EACV,MAAM,IAAI,OAAO,OAAO,SAAS,IAAI,MAAM,GAAG,GAAG;EACjD,MAAM,IAAI,YAAY;EACtB;EACD;;AAGH,SAAS,cAAc,KAAa,QAAqD;CAEvF,MAAM,gBAAgB,IAAI,MAAM,OAAO,SAAS,EAAE;AAGlD,KAAI,WAAW,OAAO;EACpB,MAAM,WAAW,cAAc,MAAM,2CAA2C;AAChF,MAAI,WAAW,MAAM,SAAS,IAAI;GAEhC,MAAM,UAAU,SAAS,GAAG,QAAQ,IAAI;AAExC,UAAO;IACL,QAAQ;IACR,MAHW,SAAS,GAAG,MAAM,UAAU,EAAE;IAIzC,MAAM,SAAS;IACf,QAAQ,EAAE;IACX;;AAIH,MAAI,cAAc,WAAW,WAAW,IAAI,cAAc,WAAW,UAAU,CAC7E,QAAO;GACL,QAAQ;GACR,MAAM;GACN,QAAQ,EAAE;GACX;;CAKL,MAAM,aAAa,cAAc,QAAQ,IAAI;CAC7C,IAAI;CACJ,IAAI;AAEJ,KAAI,cAAc,GAAG;AACnB,SAAO,cAAc,MAAM,GAAG,WAAW;AACzC,gBAAc,cAAc,MAAM,aAAa,EAAE;OAEjD,QAAO;AAIT,QAAO,mBAAmB,KAAK;CAG/B,MAAM,SAAiC,EAAE;AACzC,KAAI,YAEF,CADqB,IAAI,gBAAgB,YAAY,CACxC,SAAS,OAAO,QAAQ;AACnC,SAAO,OAAO;GACd;AAGJ,QAAO;EACL;EACA;EACA;EACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aigne/afs-cli",
|
|
3
|
-
"version": "1.11.0-beta.
|
|
3
|
+
"version": "1.11.0-beta.3",
|
|
4
4
|
"description": "AFS Command Line Interface",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
@@ -43,11 +43,12 @@
|
|
|
43
43
|
"smol-toml": "^1.3.1",
|
|
44
44
|
"yargs": "^17.7.2",
|
|
45
45
|
"zod": "^3.24.1",
|
|
46
|
-
"@aigne/afs
|
|
47
|
-
"@aigne/afs-json": "1.11.0-beta.
|
|
48
|
-
"@aigne/afs-
|
|
49
|
-
"@aigne/afs-
|
|
50
|
-
"@aigne/afs": "1.11.0-beta.
|
|
46
|
+
"@aigne/afs": "1.11.0-beta.3",
|
|
47
|
+
"@aigne/afs-json": "1.11.0-beta.3",
|
|
48
|
+
"@aigne/afs-git": "1.11.0-beta.3",
|
|
49
|
+
"@aigne/afs-fs": "1.11.0-beta.3",
|
|
50
|
+
"@aigne/afs-sqlite": "1.11.0-beta.3",
|
|
51
|
+
"@aigne/afs-http": "1.11.0-beta.3"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
54
|
"@types/bun": "^1.3.6",
|