@aigne/afs-cli 1.11.0-beta.1 → 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 +63 -9
- package/dist/_virtual/rolldown_runtime.cjs +29 -0
- package/dist/cli.cjs +251 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.mjs +241 -19
- package/dist/cli.mjs.map +1 -0
- package/dist/commands/exec.cjs +46 -0
- package/dist/commands/exec.mjs +45 -0
- package/dist/commands/exec.mjs.map +1 -0
- package/dist/commands/explain.cjs +244 -0
- package/dist/commands/explain.mjs +242 -0
- package/dist/commands/explain.mjs.map +1 -0
- package/dist/commands/index.cjs +8 -0
- package/dist/commands/index.mjs +10 -0
- package/dist/commands/ls.cjs +141 -0
- package/dist/commands/ls.mjs +140 -0
- package/dist/commands/ls.mjs.map +1 -0
- package/dist/commands/mount.cjs +170 -0
- package/dist/commands/mount.mjs +166 -0
- package/dist/commands/mount.mjs.map +1 -0
- package/dist/commands/read.cjs +65 -0
- package/dist/commands/read.mjs +64 -0
- package/dist/commands/read.mjs.map +1 -0
- package/dist/commands/serve.cjs +141 -0
- package/dist/commands/serve.mjs +140 -0
- package/dist/commands/serve.mjs.map +1 -0
- package/dist/commands/stat.cjs +113 -0
- package/dist/commands/stat.mjs +112 -0
- package/dist/commands/stat.mjs.map +1 -0
- package/dist/commands/write.cjs +52 -0
- package/dist/commands/write.mjs +51 -0
- package/dist/commands/write.mjs.map +1 -0
- package/dist/config/env.cjs +46 -0
- package/dist/config/env.mjs +46 -0
- package/dist/config/env.mjs.map +1 -0
- package/dist/config/loader.cjs +171 -0
- package/dist/config/loader.mjs +169 -0
- package/dist/config/loader.mjs.map +1 -0
- package/dist/config/provider-factory.cjs +92 -0
- package/dist/config/provider-factory.mjs +93 -0
- package/dist/config/provider-factory.mjs.map +1 -0
- package/dist/config/schema.cjs +36 -0
- package/dist/config/schema.mjs +36 -0
- package/dist/config/schema.mjs.map +1 -0
- package/dist/config/uri-parser.cjs +92 -0
- package/dist/config/uri-parser.mjs +92 -0
- package/dist/config/uri-parser.mjs.map +1 -0
- package/dist/errors.cjs +29 -0
- package/dist/errors.mjs +28 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +1 -3
- package/dist/index.mjs +1 -1
- package/dist/runtime.cjs +82 -0
- package/dist/runtime.mjs +82 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/version.cjs +9 -0
- package/dist/version.d.cts +5 -0
- package/dist/version.d.cts.map +1 -0
- package/dist/version.d.mts +5 -0
- package/dist/version.d.mts.map +1 -0
- package/dist/version.mjs +9 -0
- package/dist/version.mjs.map +1 -0
- package/package.json +52 -11
- package/.turbo/turbo-build.log +0 -18
- package/.turbo/turbo-check-types.log +0 -4
- package/dist/version--p6A8sKX.mjs +0 -5
- package/src/cli.test.ts +0 -8
- package/src/cli.ts +0 -29
- package/src/index.ts +0 -7
- package/src/version.ts +0 -1
- package/tsconfig.json +0 -16
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const require_exec = require('./exec.cjs');
|
|
2
|
+
const require_mount = require('./mount.cjs');
|
|
3
|
+
const require_explain = require('./explain.cjs');
|
|
4
|
+
const require_ls = require('./ls.cjs');
|
|
5
|
+
const require_read = require('./read.cjs');
|
|
6
|
+
const require_serve = require('./serve.cjs');
|
|
7
|
+
const require_stat = require('./stat.cjs');
|
|
8
|
+
const require_write = require('./write.cjs');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { execCommand, formatExecOutput } from "./exec.mjs";
|
|
2
|
+
import { formatMountListOutput, mountAddCommand, mountListCommand, mountRemoveCommand, mountValidateCommand } from "./mount.mjs";
|
|
3
|
+
import { explainCommand, explainPathCommand, formatExplainOutput, formatPathExplainOutput } from "./explain.mjs";
|
|
4
|
+
import { formatLsOutput, lsCommand } from "./ls.mjs";
|
|
5
|
+
import { formatReadOutput, readCommand } from "./read.mjs";
|
|
6
|
+
import { formatServeOutput, serveCommand } from "./serve.mjs";
|
|
7
|
+
import { formatStatOutput, statCommand } from "./stat.mjs";
|
|
8
|
+
import { formatWriteOutput, writeCommand } from "./write.mjs";
|
|
9
|
+
|
|
10
|
+
export { };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/commands/ls.ts
|
|
3
|
+
/**
|
|
4
|
+
* List directory contents
|
|
5
|
+
*/
|
|
6
|
+
async function lsCommand(runtime, path, options = {}) {
|
|
7
|
+
const result = await runtime.list(path, {
|
|
8
|
+
maxDepth: options.maxDepth ?? 1,
|
|
9
|
+
limit: options.limit,
|
|
10
|
+
maxChildren: options.maxChildren,
|
|
11
|
+
pattern: options.pattern
|
|
12
|
+
});
|
|
13
|
+
const entries = result.data.map((entry) => ({
|
|
14
|
+
path: entry.path,
|
|
15
|
+
type: mapEntryType(entry.metadata?.type),
|
|
16
|
+
size: entry.metadata?.size,
|
|
17
|
+
modified: formatDate(entry.updatedAt),
|
|
18
|
+
childrenCount: entry.metadata?.childrenCount,
|
|
19
|
+
childrenTruncated: entry.metadata?.childrenTruncated
|
|
20
|
+
}));
|
|
21
|
+
const truncated = entries.some((e) => e.childrenTruncated) || options.limit !== void 0 && entries.length >= options.limit || result.message?.toLowerCase().includes("truncat");
|
|
22
|
+
return {
|
|
23
|
+
entries,
|
|
24
|
+
total: entries.length,
|
|
25
|
+
truncated,
|
|
26
|
+
message: result.message
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function mapEntryType(type) {
|
|
30
|
+
if (type === "file") return "file";
|
|
31
|
+
return "directory";
|
|
32
|
+
}
|
|
33
|
+
function formatDate(date) {
|
|
34
|
+
if (!date) return void 0;
|
|
35
|
+
if (typeof date === "string") return date;
|
|
36
|
+
return date.toISOString();
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Format ls output for different views
|
|
40
|
+
*/
|
|
41
|
+
function formatLsOutput(result, view) {
|
|
42
|
+
switch (view) {
|
|
43
|
+
case "json": return formatJson(result);
|
|
44
|
+
case "llm": return formatLlm(result);
|
|
45
|
+
case "human": return formatHuman(result);
|
|
46
|
+
default: return formatDefault(result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Default format: Machine truth, one path per line
|
|
51
|
+
*/
|
|
52
|
+
function formatDefault(result) {
|
|
53
|
+
const lines = result.entries.map((entry) => entry.path);
|
|
54
|
+
if (result.truncated) lines.push(`# Results truncated (${result.total} shown)`);
|
|
55
|
+
return lines.join("\n");
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* JSON format: Structured output
|
|
59
|
+
*/
|
|
60
|
+
function formatJson(result) {
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
entries: result.entries,
|
|
63
|
+
total: result.total,
|
|
64
|
+
truncated: result.truncated,
|
|
65
|
+
message: result.message
|
|
66
|
+
}, null, 2);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* LLM format: Token-efficient, semantic facts
|
|
70
|
+
*/
|
|
71
|
+
function formatLlm(result) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
for (const entry of result.entries) {
|
|
74
|
+
const parts = [`ENTRY ${entry.path}`];
|
|
75
|
+
parts.push(`TYPE=${entry.type}`);
|
|
76
|
+
if (entry.size !== void 0) parts.push(`SIZE=${entry.size}`);
|
|
77
|
+
if (entry.childrenCount !== void 0) parts.push(`CHILDREN=${entry.childrenCount}`);
|
|
78
|
+
if (entry.childrenTruncated) parts.push("TRUNCATED");
|
|
79
|
+
lines.push(parts.join(" "));
|
|
80
|
+
}
|
|
81
|
+
lines.push(`TOTAL ${result.total}`);
|
|
82
|
+
if (result.truncated) lines.push("TRUNCATED true");
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Human format: Tree structure
|
|
87
|
+
*/
|
|
88
|
+
function formatHuman(result) {
|
|
89
|
+
const root = {
|
|
90
|
+
name: "",
|
|
91
|
+
children: /* @__PURE__ */ new Map()
|
|
92
|
+
};
|
|
93
|
+
for (const entry of result.entries) {
|
|
94
|
+
const parts = entry.path.split("/").filter(Boolean);
|
|
95
|
+
let current = root;
|
|
96
|
+
for (let i = 0; i < parts.length; i++) {
|
|
97
|
+
const part = parts[i];
|
|
98
|
+
if (!current.children.has(part)) current.children.set(part, {
|
|
99
|
+
name: part,
|
|
100
|
+
children: /* @__PURE__ */ new Map()
|
|
101
|
+
});
|
|
102
|
+
current = current.children.get(part);
|
|
103
|
+
if (i === parts.length - 1) current.entry = entry;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const lines = [];
|
|
107
|
+
renderTree(root, "", lines);
|
|
108
|
+
if (result.truncated) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push(`(Results truncated - ${result.total} entries shown)`);
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
function renderTree(node, prefix, lines) {
|
|
115
|
+
const children = Array.from(node.children.values());
|
|
116
|
+
for (let i = 0; i < children.length; i++) {
|
|
117
|
+
const child = children[i];
|
|
118
|
+
const isLast = i === children.length - 1;
|
|
119
|
+
const connector = isLast ? "└── " : "├── ";
|
|
120
|
+
const icon = child.entry ? getTypeIcon(child.entry.type) : "📂";
|
|
121
|
+
const sizeStr = child.entry?.size !== void 0 ? ` ${formatSize(child.entry.size)}` : "";
|
|
122
|
+
lines.push(`${prefix}${connector}${icon} ${child.name}${sizeStr}`);
|
|
123
|
+
renderTree(child, prefix + (isLast ? " " : "│ "), lines);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function getTypeIcon(type) {
|
|
127
|
+
switch (type) {
|
|
128
|
+
case "directory": return "📂";
|
|
129
|
+
case "file": return "📄";
|
|
130
|
+
default: return "📄";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function formatSize(bytes) {
|
|
134
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
135
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
136
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
exports.formatLsOutput = formatLsOutput;
|
|
141
|
+
exports.lsCommand = lsCommand;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
//#region src/commands/ls.ts
|
|
2
|
+
/**
|
|
3
|
+
* List directory contents
|
|
4
|
+
*/
|
|
5
|
+
async function lsCommand(runtime, path, options = {}) {
|
|
6
|
+
const result = await runtime.list(path, {
|
|
7
|
+
maxDepth: options.maxDepth ?? 1,
|
|
8
|
+
limit: options.limit,
|
|
9
|
+
maxChildren: options.maxChildren,
|
|
10
|
+
pattern: options.pattern
|
|
11
|
+
});
|
|
12
|
+
const entries = result.data.map((entry) => ({
|
|
13
|
+
path: entry.path,
|
|
14
|
+
type: mapEntryType(entry.metadata?.type),
|
|
15
|
+
size: entry.metadata?.size,
|
|
16
|
+
modified: formatDate(entry.updatedAt),
|
|
17
|
+
childrenCount: entry.metadata?.childrenCount,
|
|
18
|
+
childrenTruncated: entry.metadata?.childrenTruncated
|
|
19
|
+
}));
|
|
20
|
+
const truncated = entries.some((e) => e.childrenTruncated) || options.limit !== void 0 && entries.length >= options.limit || result.message?.toLowerCase().includes("truncat");
|
|
21
|
+
return {
|
|
22
|
+
entries,
|
|
23
|
+
total: entries.length,
|
|
24
|
+
truncated,
|
|
25
|
+
message: result.message
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function mapEntryType(type) {
|
|
29
|
+
if (type === "file") return "file";
|
|
30
|
+
return "directory";
|
|
31
|
+
}
|
|
32
|
+
function formatDate(date) {
|
|
33
|
+
if (!date) return void 0;
|
|
34
|
+
if (typeof date === "string") return date;
|
|
35
|
+
return date.toISOString();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Format ls output for different views
|
|
39
|
+
*/
|
|
40
|
+
function formatLsOutput(result, view) {
|
|
41
|
+
switch (view) {
|
|
42
|
+
case "json": return formatJson(result);
|
|
43
|
+
case "llm": return formatLlm(result);
|
|
44
|
+
case "human": return formatHuman(result);
|
|
45
|
+
default: return formatDefault(result);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Default format: Machine truth, one path per line
|
|
50
|
+
*/
|
|
51
|
+
function formatDefault(result) {
|
|
52
|
+
const lines = result.entries.map((entry) => entry.path);
|
|
53
|
+
if (result.truncated) lines.push(`# Results truncated (${result.total} shown)`);
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* JSON format: Structured output
|
|
58
|
+
*/
|
|
59
|
+
function formatJson(result) {
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
entries: result.entries,
|
|
62
|
+
total: result.total,
|
|
63
|
+
truncated: result.truncated,
|
|
64
|
+
message: result.message
|
|
65
|
+
}, null, 2);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* LLM format: Token-efficient, semantic facts
|
|
69
|
+
*/
|
|
70
|
+
function formatLlm(result) {
|
|
71
|
+
const lines = [];
|
|
72
|
+
for (const entry of result.entries) {
|
|
73
|
+
const parts = [`ENTRY ${entry.path}`];
|
|
74
|
+
parts.push(`TYPE=${entry.type}`);
|
|
75
|
+
if (entry.size !== void 0) parts.push(`SIZE=${entry.size}`);
|
|
76
|
+
if (entry.childrenCount !== void 0) parts.push(`CHILDREN=${entry.childrenCount}`);
|
|
77
|
+
if (entry.childrenTruncated) parts.push("TRUNCATED");
|
|
78
|
+
lines.push(parts.join(" "));
|
|
79
|
+
}
|
|
80
|
+
lines.push(`TOTAL ${result.total}`);
|
|
81
|
+
if (result.truncated) lines.push("TRUNCATED true");
|
|
82
|
+
return lines.join("\n");
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Human format: Tree structure
|
|
86
|
+
*/
|
|
87
|
+
function formatHuman(result) {
|
|
88
|
+
const root = {
|
|
89
|
+
name: "",
|
|
90
|
+
children: /* @__PURE__ */ new Map()
|
|
91
|
+
};
|
|
92
|
+
for (const entry of result.entries) {
|
|
93
|
+
const parts = entry.path.split("/").filter(Boolean);
|
|
94
|
+
let current = root;
|
|
95
|
+
for (let i = 0; i < parts.length; i++) {
|
|
96
|
+
const part = parts[i];
|
|
97
|
+
if (!current.children.has(part)) current.children.set(part, {
|
|
98
|
+
name: part,
|
|
99
|
+
children: /* @__PURE__ */ new Map()
|
|
100
|
+
});
|
|
101
|
+
current = current.children.get(part);
|
|
102
|
+
if (i === parts.length - 1) current.entry = entry;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const lines = [];
|
|
106
|
+
renderTree(root, "", lines);
|
|
107
|
+
if (result.truncated) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(`(Results truncated - ${result.total} entries shown)`);
|
|
110
|
+
}
|
|
111
|
+
return lines.join("\n");
|
|
112
|
+
}
|
|
113
|
+
function renderTree(node, prefix, lines) {
|
|
114
|
+
const children = Array.from(node.children.values());
|
|
115
|
+
for (let i = 0; i < children.length; i++) {
|
|
116
|
+
const child = children[i];
|
|
117
|
+
const isLast = i === children.length - 1;
|
|
118
|
+
const connector = isLast ? "└── " : "├── ";
|
|
119
|
+
const icon = child.entry ? getTypeIcon(child.entry.type) : "📂";
|
|
120
|
+
const sizeStr = child.entry?.size !== void 0 ? ` ${formatSize(child.entry.size)}` : "";
|
|
121
|
+
lines.push(`${prefix}${connector}${icon} ${child.name}${sizeStr}`);
|
|
122
|
+
renderTree(child, prefix + (isLast ? " " : "│ "), lines);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function getTypeIcon(type) {
|
|
126
|
+
switch (type) {
|
|
127
|
+
case "directory": return "📂";
|
|
128
|
+
case "file": return "📄";
|
|
129
|
+
default: return "📄";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function formatSize(bytes) {
|
|
133
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
134
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
135
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
//#endregion
|
|
139
|
+
export { formatLsOutput, lsCommand };
|
|
140
|
+
//# sourceMappingURL=ls.mjs.map
|
|
@@ -0,0 +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: 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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"}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
const require_loader = require('../config/loader.cjs');
|
|
3
|
+
let node_fs_promises = require("node:fs/promises");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let smol_toml = require("smol-toml");
|
|
6
|
+
|
|
7
|
+
//#region src/commands/mount.ts
|
|
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
|
+
/**
|
|
18
|
+
* Resolve relative paths in URI to absolute paths
|
|
19
|
+
* Supports fs://, git://, sqlite://, json:// schemes
|
|
20
|
+
* Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)
|
|
21
|
+
*/
|
|
22
|
+
function resolveUriPath(uri, cwd) {
|
|
23
|
+
const schemeMatch = uri.match(/^([a-z]+):\/\//);
|
|
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
|
+
}
|
|
28
|
+
const scheme = schemeMatch[1];
|
|
29
|
+
const pathPart = uri.slice(schemeMatch[0].length);
|
|
30
|
+
if (![
|
|
31
|
+
"fs",
|
|
32
|
+
"git",
|
|
33
|
+
"sqlite",
|
|
34
|
+
"json"
|
|
35
|
+
].includes(scheme)) return uri;
|
|
36
|
+
if (scheme === "git" && isRemoteGitUrl(pathPart)) return uri;
|
|
37
|
+
if ((0, node_path.isAbsolute)(pathPart)) return uri;
|
|
38
|
+
return `${scheme}://${(0, node_path.resolve)(cwd, pathPart)}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* List all mounts from config (merged from all config layers)
|
|
42
|
+
*/
|
|
43
|
+
async function mountListCommand(cwd) {
|
|
44
|
+
return { mounts: (await new require_loader.ConfigLoader().load(cwd)).mounts };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Add a mount to config
|
|
48
|
+
*/
|
|
49
|
+
async function mountAddCommand(cwd, path, uri, options = {}) {
|
|
50
|
+
const configDir = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME);
|
|
51
|
+
const configPath = (0, node_path.join)(configDir, require_loader.CONFIG_FILE_NAME);
|
|
52
|
+
const config = { mounts: [] };
|
|
53
|
+
try {
|
|
54
|
+
config.mounts = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8")).mounts ?? [];
|
|
55
|
+
} catch {}
|
|
56
|
+
if (config.mounts.some((m) => m.path === path)) return {
|
|
57
|
+
success: false,
|
|
58
|
+
message: `Mount path "${path}" already exists`
|
|
59
|
+
};
|
|
60
|
+
const newMount = {
|
|
61
|
+
path,
|
|
62
|
+
uri: resolveUriPath(uri, cwd),
|
|
63
|
+
...options.description && { description: options.description }
|
|
64
|
+
};
|
|
65
|
+
config.mounts.push(newMount);
|
|
66
|
+
try {
|
|
67
|
+
await (0, node_fs_promises.mkdir)(configDir, { recursive: true });
|
|
68
|
+
} catch {}
|
|
69
|
+
await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(config), "utf-8");
|
|
70
|
+
return { success: true };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Remove a mount from config
|
|
74
|
+
*/
|
|
75
|
+
async function mountRemoveCommand(cwd, path) {
|
|
76
|
+
const configPath = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME, require_loader.CONFIG_FILE_NAME);
|
|
77
|
+
try {
|
|
78
|
+
const config = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8"));
|
|
79
|
+
const mounts = config.mounts ?? [];
|
|
80
|
+
const index = mounts.findIndex((m) => m.path === path);
|
|
81
|
+
if (index === -1) return {
|
|
82
|
+
success: false,
|
|
83
|
+
message: `Mount path "${path}" not found`
|
|
84
|
+
};
|
|
85
|
+
mounts.splice(index, 1);
|
|
86
|
+
config.mounts = mounts;
|
|
87
|
+
await (0, node_fs_promises.writeFile)(configPath, (0, smol_toml.stringify)(config), "utf-8");
|
|
88
|
+
return { success: true };
|
|
89
|
+
} catch {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
message: `Mount path "${path}" not found`
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Validate mount configuration
|
|
98
|
+
*/
|
|
99
|
+
async function mountValidateCommand(cwd) {
|
|
100
|
+
const configPath = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME, require_loader.CONFIG_FILE_NAME);
|
|
101
|
+
const errors = [];
|
|
102
|
+
try {
|
|
103
|
+
const mounts = (0, smol_toml.parse)(await (0, node_fs_promises.readFile)(configPath, "utf-8")).mounts ?? [];
|
|
104
|
+
for (const mount of mounts) {
|
|
105
|
+
if (!mount.path.startsWith("/")) errors.push(`Mount path "${mount.path}" must start with /`);
|
|
106
|
+
if (!mount.uri || mount.uri.trim() === "") errors.push(`Mount at "${mount.path}" has empty URI`);
|
|
107
|
+
if (mount.uri.startsWith("fs://")) {
|
|
108
|
+
const targetPath = mount.uri.replace("fs://", "");
|
|
109
|
+
try {
|
|
110
|
+
await (0, node_fs_promises.access)(targetPath);
|
|
111
|
+
} catch {
|
|
112
|
+
errors.push(`Mount target "${targetPath}" does not exist`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
valid: errors.length === 0,
|
|
118
|
+
errors
|
|
119
|
+
};
|
|
120
|
+
} catch {
|
|
121
|
+
return {
|
|
122
|
+
valid: true,
|
|
123
|
+
errors: []
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Format mount list output for different views
|
|
129
|
+
*/
|
|
130
|
+
function formatMountListOutput(mounts, view) {
|
|
131
|
+
switch (view) {
|
|
132
|
+
case "json": return JSON.stringify({ mounts }, null, 2);
|
|
133
|
+
case "llm": return formatLlm(mounts);
|
|
134
|
+
case "human": return formatHuman(mounts);
|
|
135
|
+
default: return formatDefault(mounts);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function formatDefault(mounts) {
|
|
139
|
+
if (mounts.length === 0) return "No mounts configured";
|
|
140
|
+
return mounts.map((m) => {
|
|
141
|
+
const desc = m.description ? ` (${m.description})` : "";
|
|
142
|
+
return `${m.path} → ${m.uri}${desc}`;
|
|
143
|
+
}).join("\n");
|
|
144
|
+
}
|
|
145
|
+
function formatLlm(mounts) {
|
|
146
|
+
if (mounts.length === 0) return "NO_MOUNTS";
|
|
147
|
+
return mounts.map((m) => {
|
|
148
|
+
const lines = [`MOUNT ${m.path}`, `URI=${m.uri}`];
|
|
149
|
+
if (m.description) lines.push(`DESC=${m.description}`);
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
}).join("\n\n");
|
|
152
|
+
}
|
|
153
|
+
function formatHuman(mounts) {
|
|
154
|
+
if (mounts.length === 0) return "No mounts configured.";
|
|
155
|
+
const lines = ["Configured Mounts:", ""];
|
|
156
|
+
for (const m of mounts) {
|
|
157
|
+
lines.push(` ${m.path}`);
|
|
158
|
+
lines.push(` URI: ${m.uri}`);
|
|
159
|
+
if (m.description) lines.push(` Description: ${m.description}`);
|
|
160
|
+
lines.push("");
|
|
161
|
+
}
|
|
162
|
+
return lines.join("\n").trimEnd();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
//#endregion
|
|
166
|
+
exports.formatMountListOutput = formatMountListOutput;
|
|
167
|
+
exports.mountAddCommand = mountAddCommand;
|
|
168
|
+
exports.mountListCommand = mountListCommand;
|
|
169
|
+
exports.mountRemoveCommand = mountRemoveCommand;
|
|
170
|
+
exports.mountValidateCommand = mountValidateCommand;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader } from "../config/loader.mjs";
|
|
2
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import { parse, stringify } from "smol-toml";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/mount.ts
|
|
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
|
+
/**
|
|
17
|
+
* Resolve relative paths in URI to absolute paths
|
|
18
|
+
* Supports fs://, git://, sqlite://, json:// schemes
|
|
19
|
+
* Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)
|
|
20
|
+
*/
|
|
21
|
+
function resolveUriPath(uri, cwd) {
|
|
22
|
+
const schemeMatch = uri.match(/^([a-z]+):\/\//);
|
|
23
|
+
if (!schemeMatch) {
|
|
24
|
+
if (isRemoteGitUrl(uri)) return `git://${uri}`;
|
|
25
|
+
return `fs://${isAbsolute(uri) ? uri : resolve(cwd, uri)}`;
|
|
26
|
+
}
|
|
27
|
+
const scheme = schemeMatch[1];
|
|
28
|
+
const pathPart = uri.slice(schemeMatch[0].length);
|
|
29
|
+
if (![
|
|
30
|
+
"fs",
|
|
31
|
+
"git",
|
|
32
|
+
"sqlite",
|
|
33
|
+
"json"
|
|
34
|
+
].includes(scheme)) return uri;
|
|
35
|
+
if (scheme === "git" && isRemoteGitUrl(pathPart)) return uri;
|
|
36
|
+
if (isAbsolute(pathPart)) return uri;
|
|
37
|
+
return `${scheme}://${resolve(cwd, pathPart)}`;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* List all mounts from config (merged from all config layers)
|
|
41
|
+
*/
|
|
42
|
+
async function mountListCommand(cwd) {
|
|
43
|
+
return { mounts: (await new ConfigLoader().load(cwd)).mounts };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Add a mount to config
|
|
47
|
+
*/
|
|
48
|
+
async function mountAddCommand(cwd, path, uri, options = {}) {
|
|
49
|
+
const configDir = join(cwd, CONFIG_DIR_NAME);
|
|
50
|
+
const configPath = join(configDir, CONFIG_FILE_NAME);
|
|
51
|
+
const config = { mounts: [] };
|
|
52
|
+
try {
|
|
53
|
+
config.mounts = parse(await readFile(configPath, "utf-8")).mounts ?? [];
|
|
54
|
+
} catch {}
|
|
55
|
+
if (config.mounts.some((m) => m.path === path)) return {
|
|
56
|
+
success: false,
|
|
57
|
+
message: `Mount path "${path}" already exists`
|
|
58
|
+
};
|
|
59
|
+
const newMount = {
|
|
60
|
+
path,
|
|
61
|
+
uri: resolveUriPath(uri, cwd),
|
|
62
|
+
...options.description && { description: options.description }
|
|
63
|
+
};
|
|
64
|
+
config.mounts.push(newMount);
|
|
65
|
+
try {
|
|
66
|
+
await mkdir(configDir, { recursive: true });
|
|
67
|
+
} catch {}
|
|
68
|
+
await writeFile(configPath, stringify(config), "utf-8");
|
|
69
|
+
return { success: true };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Remove a mount from config
|
|
73
|
+
*/
|
|
74
|
+
async function mountRemoveCommand(cwd, path) {
|
|
75
|
+
const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
|
|
76
|
+
try {
|
|
77
|
+
const config = parse(await readFile(configPath, "utf-8"));
|
|
78
|
+
const mounts = config.mounts ?? [];
|
|
79
|
+
const index = mounts.findIndex((m) => m.path === path);
|
|
80
|
+
if (index === -1) return {
|
|
81
|
+
success: false,
|
|
82
|
+
message: `Mount path "${path}" not found`
|
|
83
|
+
};
|
|
84
|
+
mounts.splice(index, 1);
|
|
85
|
+
config.mounts = mounts;
|
|
86
|
+
await writeFile(configPath, stringify(config), "utf-8");
|
|
87
|
+
return { success: true };
|
|
88
|
+
} catch {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
message: `Mount path "${path}" not found`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate mount configuration
|
|
97
|
+
*/
|
|
98
|
+
async function mountValidateCommand(cwd) {
|
|
99
|
+
const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);
|
|
100
|
+
const errors = [];
|
|
101
|
+
try {
|
|
102
|
+
const mounts = parse(await readFile(configPath, "utf-8")).mounts ?? [];
|
|
103
|
+
for (const mount of mounts) {
|
|
104
|
+
if (!mount.path.startsWith("/")) errors.push(`Mount path "${mount.path}" must start with /`);
|
|
105
|
+
if (!mount.uri || mount.uri.trim() === "") errors.push(`Mount at "${mount.path}" has empty URI`);
|
|
106
|
+
if (mount.uri.startsWith("fs://")) {
|
|
107
|
+
const targetPath = mount.uri.replace("fs://", "");
|
|
108
|
+
try {
|
|
109
|
+
await access(targetPath);
|
|
110
|
+
} catch {
|
|
111
|
+
errors.push(`Mount target "${targetPath}" does not exist`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
valid: errors.length === 0,
|
|
117
|
+
errors
|
|
118
|
+
};
|
|
119
|
+
} catch {
|
|
120
|
+
return {
|
|
121
|
+
valid: true,
|
|
122
|
+
errors: []
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Format mount list output for different views
|
|
128
|
+
*/
|
|
129
|
+
function formatMountListOutput(mounts, view) {
|
|
130
|
+
switch (view) {
|
|
131
|
+
case "json": return JSON.stringify({ mounts }, null, 2);
|
|
132
|
+
case "llm": return formatLlm(mounts);
|
|
133
|
+
case "human": return formatHuman(mounts);
|
|
134
|
+
default: return formatDefault(mounts);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function formatDefault(mounts) {
|
|
138
|
+
if (mounts.length === 0) return "No mounts configured";
|
|
139
|
+
return mounts.map((m) => {
|
|
140
|
+
const desc = m.description ? ` (${m.description})` : "";
|
|
141
|
+
return `${m.path} → ${m.uri}${desc}`;
|
|
142
|
+
}).join("\n");
|
|
143
|
+
}
|
|
144
|
+
function formatLlm(mounts) {
|
|
145
|
+
if (mounts.length === 0) return "NO_MOUNTS";
|
|
146
|
+
return mounts.map((m) => {
|
|
147
|
+
const lines = [`MOUNT ${m.path}`, `URI=${m.uri}`];
|
|
148
|
+
if (m.description) lines.push(`DESC=${m.description}`);
|
|
149
|
+
return lines.join("\n");
|
|
150
|
+
}).join("\n\n");
|
|
151
|
+
}
|
|
152
|
+
function formatHuman(mounts) {
|
|
153
|
+
if (mounts.length === 0) return "No mounts configured.";
|
|
154
|
+
const lines = ["Configured Mounts:", ""];
|
|
155
|
+
for (const m of mounts) {
|
|
156
|
+
lines.push(` ${m.path}`);
|
|
157
|
+
lines.push(` URI: ${m.uri}`);
|
|
158
|
+
if (m.description) lines.push(` Description: ${m.description}`);
|
|
159
|
+
lines.push("");
|
|
160
|
+
}
|
|
161
|
+
return lines.join("\n").trimEnd();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
export { formatMountListOutput, mountAddCommand, mountListCommand, mountRemoveCommand, mountValidateCommand };
|
|
166
|
+
//# sourceMappingURL=mount.mjs.map
|
|
@@ -0,0 +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 * 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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"}
|