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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +16 -1
  2. package/dist/cli.cjs +53 -20
  3. package/dist/cli.mjs +54 -21
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/commands/exec.cjs +132 -14
  6. package/dist/commands/exec.mjs +129 -14
  7. package/dist/commands/exec.mjs.map +1 -1
  8. package/dist/commands/explain.cjs +1 -1
  9. package/dist/commands/explain.mjs +1 -1
  10. package/dist/commands/explain.mjs.map +1 -1
  11. package/dist/commands/index.mjs +1 -1
  12. package/dist/commands/ls.cjs +129 -30
  13. package/dist/commands/ls.mjs +129 -30
  14. package/dist/commands/ls.mjs.map +1 -1
  15. package/dist/commands/mount.cjs +2 -1
  16. package/dist/commands/mount.mjs +2 -1
  17. package/dist/commands/mount.mjs.map +1 -1
  18. package/dist/commands/read.cjs +213 -14
  19. package/dist/commands/read.mjs +213 -14
  20. package/dist/commands/read.mjs.map +1 -1
  21. package/dist/commands/serve.cjs +3 -1
  22. package/dist/commands/serve.mjs +3 -1
  23. package/dist/commands/serve.mjs.map +1 -1
  24. package/dist/commands/stat.cjs +116 -34
  25. package/dist/commands/stat.mjs +117 -34
  26. package/dist/commands/stat.mjs.map +1 -1
  27. package/dist/commands/write.cjs +37 -4
  28. package/dist/commands/write.mjs +38 -4
  29. package/dist/commands/write.mjs.map +1 -1
  30. package/dist/config/loader.cjs +33 -13
  31. package/dist/config/loader.mjs +33 -13
  32. package/dist/config/loader.mjs.map +1 -1
  33. package/dist/config/provider-factory.cjs +311 -3
  34. package/dist/config/provider-factory.mjs +311 -3
  35. package/dist/config/provider-factory.mjs.map +1 -1
  36. package/dist/config/schema.cjs +3 -1
  37. package/dist/config/schema.mjs +3 -1
  38. package/dist/config/schema.mjs.map +1 -1
  39. package/dist/config/uri-parser.cjs +195 -2
  40. package/dist/config/uri-parser.mjs +195 -2
  41. package/dist/config/uri-parser.mjs.map +1 -1
  42. package/dist/explorer/actions.cjs +53 -23
  43. package/dist/explorer/actions.mjs +54 -23
  44. package/dist/explorer/actions.mjs.map +1 -1
  45. package/dist/explorer/components/dialog.cjs +163 -10
  46. package/dist/explorer/components/dialog.mjs +163 -10
  47. package/dist/explorer/components/dialog.mjs.map +1 -1
  48. package/dist/explorer/components/file-list.mjs.map +1 -1
  49. package/dist/explorer/components/metadata-panel.cjs +39 -25
  50. package/dist/explorer/components/metadata-panel.mjs +39 -25
  51. package/dist/explorer/components/metadata-panel.mjs.map +1 -1
  52. package/dist/explorer/screen.cjs +23 -8
  53. package/dist/explorer/screen.mjs +24 -9
  54. package/dist/explorer/screen.mjs.map +1 -1
  55. package/dist/explorer/theme.cjs +3 -1
  56. package/dist/explorer/theme.mjs +3 -1
  57. package/dist/explorer/theme.mjs.map +1 -1
  58. package/dist/path-utils.cjs +2 -1
  59. package/dist/path-utils.mjs +1 -1
  60. package/dist/runtime.cjs +24 -0
  61. package/dist/runtime.mjs +24 -0
  62. package/dist/runtime.mjs.map +1 -1
  63. package/dist/ui/header.cjs +0 -9
  64. package/dist/ui/header.mjs +1 -9
  65. package/dist/ui/header.mjs.map +1 -1
  66. package/dist/ui/index.cjs +0 -2
  67. package/dist/ui/index.mjs +2 -3
  68. package/dist/ui/index.mjs.map +1 -1
  69. package/dist/utils/meta.cjs +51 -0
  70. package/dist/utils/meta.mjs +49 -0
  71. package/dist/utils/meta.mjs.map +1 -0
  72. package/package.json +19 -9
@@ -1,4 +1,5 @@
1
1
  import { colors } from "../ui/index.mjs";
2
+ import { isActionsPath } from "../utils/meta.mjs";
2
3
 
3
4
  //#region src/commands/ls.ts
4
5
  /**
@@ -11,15 +12,18 @@ async function lsCommand(runtime, path, options = {}) {
11
12
  maxChildren: options.maxChildren,
12
13
  pattern: options.pattern
13
14
  });
14
- const entries = result.data.map((entry) => ({
15
- path: entry.path,
16
- type: mapEntryType(entry.metadata?.type),
17
- size: entry.metadata?.size,
18
- modified: formatDate(entry.updatedAt),
19
- childrenCount: entry.metadata?.childrenCount,
20
- childrenTruncated: entry.metadata?.childrenTruncated
21
- }));
22
- const truncated = entries.some((e) => e.childrenTruncated) || options.limit !== void 0 && entries.length >= options.limit || result.message?.toLowerCase().includes("truncat");
15
+ const entries = result.data.map((entry) => {
16
+ const lsEntry = {
17
+ path: entry.path,
18
+ size: entry.metadata?.size,
19
+ modified: formatDate(entry.updatedAt),
20
+ childrenCount: entry.metadata?.childrenCount
21
+ };
22
+ const meta = extractMeta(entry.metadata);
23
+ if (meta && Object.keys(meta).length > 0) lsEntry.meta = meta;
24
+ return lsEntry;
25
+ });
26
+ const truncated = options.limit !== void 0 && entries.length >= options.limit || result.message?.toLowerCase().includes("truncat");
23
27
  return {
24
28
  entries,
25
29
  total: entries.length,
@@ -27,9 +31,20 @@ async function lsCommand(runtime, path, options = {}) {
27
31
  message: result.message
28
32
  };
29
33
  }
30
- function mapEntryType(type) {
31
- if (type === "file") return "file";
32
- return "directory";
34
+ /**
35
+ * Extract meta fields from entry metadata
36
+ */
37
+ function extractMeta(metadata) {
38
+ if (!metadata) return void 0;
39
+ const builtInFields = new Set([
40
+ "type",
41
+ "size",
42
+ "mimeType",
43
+ "childrenCount"
44
+ ]);
45
+ const meta = {};
46
+ for (const [key, value] of Object.entries(metadata)) if (!builtInFields.has(key) && value !== void 0) meta[key] = value;
47
+ return Object.keys(meta).length > 0 ? meta : void 0;
33
48
  }
34
49
  function formatDate(date) {
35
50
  if (!date) return void 0;
@@ -39,18 +54,20 @@ function formatDate(date) {
39
54
  /**
40
55
  * Format ls output for different views
41
56
  */
42
- function formatLsOutput(result, view) {
57
+ function formatLsOutput(result, view, options) {
58
+ const isActions = options?.path && isActionsPath(options.path);
43
59
  switch (view) {
44
- case "json": return formatJson(result);
45
- case "llm": return formatLlm(result);
46
- case "human": return formatHuman(result);
47
- default: return formatDefault(result);
60
+ case "json": return isActions ? formatActionsJson(result, options.path) : formatJson(result);
61
+ case "llm": return isActions ? formatActionsLlm(result, options.path) : formatLlm(result);
62
+ case "human": return isActions ? formatActionsHuman(result, options.path) : formatHuman(result);
63
+ default: return isActions ? formatActionsDefault(result) : formatDefault(result);
48
64
  }
49
65
  }
50
66
  /**
51
67
  * Default format: Machine truth, one path per line
52
68
  */
53
69
  function formatDefault(result) {
70
+ if (result.entries.length === 0 && result.message) return `# ${result.message}`;
54
71
  const lines = result.entries.map((entry) => entry.path);
55
72
  if (result.truncated) lines.push(`# Results truncated (${result.total} shown)`);
56
73
  return lines.join("\n");
@@ -67,16 +84,19 @@ function formatJson(result) {
67
84
  }, null, 2);
68
85
  }
69
86
  /**
70
- * LLM format: Token-efficient, semantic facts
87
+ * LLM format: Token-efficient, semantic facts (includes KIND if present)
71
88
  */
72
89
  function formatLlm(result) {
73
90
  const lines = [];
91
+ if (result.entries.length === 0 && result.message) {
92
+ lines.push(`ERROR ${result.message}`);
93
+ return lines.join("\n");
94
+ }
74
95
  for (const entry of result.entries) {
75
96
  const parts = [`ENTRY ${entry.path}`];
76
- parts.push(`TYPE=${entry.type}`);
97
+ if (entry.meta?.kind) parts.push(`KIND=${entry.meta.kind}`);
77
98
  if (entry.size !== void 0) parts.push(`SIZE=${entry.size}`);
78
99
  if (entry.childrenCount !== void 0) parts.push(`CHILDREN=${entry.childrenCount}`);
79
- if (entry.childrenTruncated) parts.push("TRUNCATED");
80
100
  lines.push(parts.join(" "));
81
101
  }
82
102
  lines.push(`TOTAL ${result.total}`);
@@ -87,6 +107,7 @@ function formatLlm(result) {
87
107
  * Human format: Tree structure
88
108
  */
89
109
  function formatHuman(result) {
110
+ if (result.entries.length === 0 && result.message) return colors.yellow(result.message);
90
111
  const root = {
91
112
  name: "",
92
113
  children: /* @__PURE__ */ new Map()
@@ -118,25 +139,103 @@ function renderTree(node, prefix, lines) {
118
139
  const child = children[i];
119
140
  const isLast = i === children.length - 1;
120
141
  const connector = colors.dim(isLast ? "└── " : "├── ");
121
- const icon = child.entry ? getTypeIcon(child.entry.type) : "📂";
122
- const name = child.entry?.type === "directory" ? colors.cyan(child.name) : child.name;
142
+ const icon = typeof child.entry?.childrenCount === "number" || child.children.size > 0 ? "📁" : "📄";
123
143
  const sizeStr = child.entry?.size !== void 0 ? colors.dim(` ${formatSize(child.entry.size)}`) : "";
124
- lines.push(`${colors.dim(prefix)}${connector}${icon} ${name}${sizeStr}`);
144
+ const kindStr = child.entry?.meta?.kind ? colors.dim(` (${child.entry.meta.kind})`) : "";
145
+ lines.push(`${colors.dim(prefix)}${connector}${icon} ${child.name}${kindStr}${sizeStr}`);
125
146
  renderTree(child, prefix + (isLast ? " " : "│ "), lines);
126
147
  }
127
148
  }
128
- function getTypeIcon(type) {
129
- switch (type) {
130
- case "directory": return "📂";
131
- case "file": return "📄";
132
- default: return "📄";
133
- }
134
- }
135
149
  function formatSize(bytes) {
136
150
  if (bytes < 1024) return `${bytes}B`;
137
151
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
138
152
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
139
153
  }
154
+ /**
155
+ * Extract action name from path (last segment after .actions/)
156
+ */
157
+ function getActionName(entry) {
158
+ const parts = entry.path.split("/");
159
+ return parts[parts.length - 1] || entry.path;
160
+ }
161
+ /**
162
+ * Get action description from entry metadata
163
+ */
164
+ function getActionDescription(entry) {
165
+ return entry.meta?.description;
166
+ }
167
+ /**
168
+ * Get the node path from an actions path (path before /.actions)
169
+ */
170
+ function getNodePathFromActions(actionsPath) {
171
+ const idx = actionsPath.lastIndexOf("/.actions");
172
+ return idx > 0 ? actionsPath.substring(0, idx) : "/";
173
+ }
174
+ /**
175
+ * Format actions list - Human view
176
+ * Shows: Available actions for /path:
177
+ * export Export table data to file
178
+ * truncate Delete all rows from table
179
+ */
180
+ function formatActionsHuman(result, path) {
181
+ const lines = [];
182
+ const nodePath = getNodePathFromActions(path);
183
+ if (result.entries.length === 0) {
184
+ lines.push(`No actions available for ${nodePath}`);
185
+ return lines.join("\n");
186
+ }
187
+ lines.push(`Available actions for ${nodePath}:`);
188
+ lines.push("");
189
+ const maxNameLen = Math.max(...result.entries.map((e) => getActionName(e).length));
190
+ for (const entry of result.entries) {
191
+ const name = getActionName(entry);
192
+ const desc = getActionDescription(entry) || "";
193
+ lines.push(` ${name.padEnd(maxNameLen + 2)}${desc}`);
194
+ }
195
+ return lines.join("\n");
196
+ }
197
+ /**
198
+ * Format actions list - LLM view
199
+ * Shows: ACTIONS /path/.actions
200
+ * ACTION export DESCRIPTION "Export table data to file"
201
+ */
202
+ function formatActionsLlm(result, path) {
203
+ const lines = [];
204
+ lines.push(`ACTIONS ${path}`);
205
+ if (result.entries.length === 0) {
206
+ lines.push("ACTIONS_COUNT 0");
207
+ return lines.join("\n");
208
+ }
209
+ for (const entry of result.entries) {
210
+ const name = getActionName(entry);
211
+ const desc = getActionDescription(entry);
212
+ const descPart = desc ? ` DESCRIPTION "${desc}"` : "";
213
+ lines.push(`ACTION ${name}${descPart}`);
214
+ }
215
+ return lines.join("\n");
216
+ }
217
+ /**
218
+ * Format actions list - JSON view
219
+ * Shows structured JSON with path and actions array
220
+ */
221
+ function formatActionsJson(result, path) {
222
+ const actions = result.entries.map((entry) => ({
223
+ name: getActionName(entry),
224
+ description: getActionDescription(entry)
225
+ }));
226
+ return JSON.stringify({
227
+ path,
228
+ data: actions
229
+ }, null, 2);
230
+ }
231
+ /**
232
+ * Format actions list - Default view
233
+ * Shows action names, one per line
234
+ */
235
+ function formatActionsDefault(result) {
236
+ if (result.entries.length === 0) return "";
237
+ return result.entries.map((entry) => getActionName(entry)).join("\n");
238
+ }
140
239
 
141
240
  //#endregion
142
241
  export { formatLsOutput, lsCommand };
@@ -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\";\nimport { colors } from \"../ui/index.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(colors.yellow(`(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 = colors.dim(isLast ? \"└── \" : \"├── \");\n const icon = child.entry ? getTypeIcon(child.entry.type) : \"\\u{1F4C2}\"; // 📂\n const name = child.entry?.type === \"directory\" ? colors.cyan(child.name) : child.name;\n const sizeStr =\n child.entry?.size !== undefined ? colors.dim(` ${formatSize(child.entry.size)}`) : \"\";\n\n lines.push(`${colors.dim(prefix)}${connector}${icon} ${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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"}
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\";\nimport { colors } from \"../ui/index.js\";\nimport { isActionsPath } from \"../utils/meta.js\";\n\nexport type ViewType = \"default\" | \"json\" | \"llm\" | \"human\";\n\nexport interface LsEntry {\n path: string;\n size?: number;\n modified?: string;\n hash?: string;\n childrenCount?: number;\n meta?: {\n kind?: string;\n icon?: string;\n label?: string;\n [key: string]: unknown;\n };\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 const lsEntry: LsEntry = {\n path: entry.path,\n size: entry.metadata?.size,\n modified: formatDate(entry.updatedAt),\n childrenCount: entry.metadata?.childrenCount,\n };\n\n // Add meta if present\n const meta = extractMeta(entry.metadata);\n if (meta && Object.keys(meta).length > 0) {\n lsEntry.meta = meta;\n }\n\n return lsEntry;\n });\n\n // Determine if results were truncated\n const truncated =\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\n/**\n * Extract meta fields from entry metadata\n */\nfunction extractMeta(\n metadata: Record<string, unknown> | null | undefined,\n): LsEntry[\"meta\"] | undefined {\n if (!metadata) return undefined;\n\n const builtInFields = new Set([\"type\", \"size\", \"mimeType\", \"childrenCount\"]);\n\n const meta: Record<string, unknown> = {};\n\n for (const [key, value] of Object.entries(metadata)) {\n if (!builtInFields.has(key) && value !== undefined) {\n meta[key] = value;\n }\n }\n\n return Object.keys(meta).length > 0 ? meta : undefined;\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\nexport interface FormatLsOptions {\n /** The path being listed (used for special formatting of .actions paths) */\n path?: string;\n}\n\n/**\n * Format ls output for different views\n */\nexport function formatLsOutput(\n result: LsResult,\n view: ViewType,\n options?: FormatLsOptions,\n): string {\n // Check if we're listing an actions path for special formatting\n const isActions = options?.path && isActionsPath(options.path);\n\n switch (view) {\n case \"json\":\n return isActions ? formatActionsJson(result, options.path!) : formatJson(result);\n case \"llm\":\n return isActions ? formatActionsLlm(result, options.path!) : formatLlm(result);\n case \"human\":\n return isActions ? formatActionsHuman(result, options.path!) : formatHuman(result);\n default:\n return isActions ? formatActionsDefault(result) : formatDefault(result);\n }\n}\n\n/**\n * Default format: Machine truth, one path per line\n */\nfunction formatDefault(result: LsResult): string {\n // If there are no entries and there's an error message, show it\n if (result.entries.length === 0 && result.message) {\n return `# ${result.message}`;\n }\n\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 (includes KIND if present)\n */\nfunction formatLlm(result: LsResult): string {\n const lines: string[] = [];\n\n // If there are no entries and there's an error message, show it\n if (result.entries.length === 0 && result.message) {\n lines.push(`ERROR ${result.message}`);\n return lines.join(\"\\n\");\n }\n\n for (const entry of result.entries) {\n const parts = [`ENTRY ${entry.path}`];\n\n if (entry.meta?.kind) {\n parts.push(`KIND=${entry.meta.kind}`);\n }\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 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 // If there are no entries and there's an error message, show it prominently\n if (result.entries.length === 0 && result.message) {\n return colors.yellow(result.message);\n }\n\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(colors.yellow(`(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 = colors.dim(isLast ? \"└── \" : \"├── \");\n // Use folder icon if:\n // 1. childrenCount is defined (directory), OR\n // 2. This tree node has children (implies it's a directory/mount point)\n const isDirectory = typeof child.entry?.childrenCount === \"number\" || child.children.size > 0;\n const icon = isDirectory ? \"\\u{1F4C1}\" : \"\\u{1F4C4}\"; // 📁 or 📄\n const sizeStr =\n child.entry?.size !== undefined ? colors.dim(` ${formatSize(child.entry.size)}`) : \"\";\n\n // Add kind in parentheses if present\n const kindStr = child.entry?.meta?.kind ? colors.dim(` (${child.entry.meta.kind})`) : \"\";\n\n lines.push(`${colors.dim(prefix)}${connector}${icon} ${child.name}${kindStr}${sizeStr}`);\n\n // Recurse into children\n const childPrefix = prefix + (isLast ? \" \" : \"│ \");\n renderTree(child, childPrefix, lines);\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\n/**\n * Extract action name from path (last segment after .actions/)\n */\nfunction getActionName(entry: LsEntry): string {\n const parts = entry.path.split(\"/\");\n return parts[parts.length - 1] || entry.path;\n}\n\n/**\n * Get action description from entry metadata\n */\nfunction getActionDescription(entry: LsEntry): string | undefined {\n return entry.meta?.description as string | undefined;\n}\n\n/**\n * Get the node path from an actions path (path before /.actions)\n */\nfunction getNodePathFromActions(actionsPath: string): string {\n const idx = actionsPath.lastIndexOf(\"/.actions\");\n return idx > 0 ? actionsPath.substring(0, idx) : \"/\";\n}\n\n/**\n * Format actions list - Human view\n * Shows: Available actions for /path:\n * export Export table data to file\n * truncate Delete all rows from table\n */\nfunction formatActionsHuman(result: LsResult, path: string): string {\n const lines: string[] = [];\n const nodePath = getNodePathFromActions(path);\n\n if (result.entries.length === 0) {\n lines.push(`No actions available for ${nodePath}`);\n return lines.join(\"\\n\");\n }\n\n lines.push(`Available actions for ${nodePath}:`);\n lines.push(\"\");\n\n // Find max action name length for padding\n const maxNameLen = Math.max(...result.entries.map((e) => getActionName(e).length));\n\n for (const entry of result.entries) {\n const name = getActionName(entry);\n const desc = getActionDescription(entry) || \"\";\n lines.push(` ${name.padEnd(maxNameLen + 2)}${desc}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Format actions list - LLM view\n * Shows: ACTIONS /path/.actions\n * ACTION export DESCRIPTION \"Export table data to file\"\n */\nfunction formatActionsLlm(result: LsResult, path: string): string {\n const lines: string[] = [];\n\n lines.push(`ACTIONS ${path}`);\n\n if (result.entries.length === 0) {\n lines.push(\"ACTIONS_COUNT 0\");\n return lines.join(\"\\n\");\n }\n\n for (const entry of result.entries) {\n const name = getActionName(entry);\n const desc = getActionDescription(entry);\n const descPart = desc ? ` DESCRIPTION \"${desc}\"` : \"\";\n lines.push(`ACTION ${name}${descPart}`);\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Format actions list - JSON view\n * Shows structured JSON with path and actions array\n */\nfunction formatActionsJson(result: LsResult, path: string): string {\n const actions = result.entries.map((entry) => ({\n name: getActionName(entry),\n description: getActionDescription(entry),\n }));\n\n return JSON.stringify(\n {\n path,\n data: actions,\n },\n null,\n 2,\n );\n}\n\n/**\n * Format actions list - Default view\n * Shows action names, one per line\n */\nfunction formatActionsDefault(result: LsResult): string {\n if (result.entries.length === 0) {\n return \"\";\n }\n return result.entries.map((entry) => getActionName(entry)).join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -66,7 +66,8 @@ async function mountAddCommand(cwd, path, uri, options = {}) {
66
66
  const newMount = {
67
67
  path: validation.data.path,
68
68
  uri: validation.data.uri,
69
- ...options.description && { description: options.description }
69
+ ...options.description && { description: options.description },
70
+ ...options.token && { token: options.token }
70
71
  };
71
72
  const configDir = (0, node_path.join)(cwd, require_loader.CONFIG_DIR_NAME);
72
73
  const configPath = (0, node_path.join)(configDir, require_loader.CONFIG_FILE_NAME);
@@ -65,7 +65,8 @@ async function mountAddCommand(cwd, path, uri, options = {}) {
65
65
  const newMount = {
66
66
  path: validation.data.path,
67
67
  uri: validation.data.uri,
68
- ...options.description && { description: options.description }
68
+ ...options.description && { description: options.description },
69
+ ...options.token && { token: options.token }
69
70
  };
70
71
  const configDir = join(cwd, CONFIG_DIR_NAME);
71
72
  const configPath = join(configDir, CONFIG_FILE_NAME);
@@ -1 +1 @@
1
- {"version":3,"file":"mount.mjs","names":[],"sources":["../../src/commands/mount.ts"],"sourcesContent":["import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { parse, stringify } from \"smol-toml\";\nimport { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader } from \"../config/loader.js\";\nimport { MountSchema } from \"../config/schema.js\";\nimport { colors } from \"../ui/index.js\";\nimport type { ViewType } from \"./ls.js\";\n\nexport interface MountEntry {\n path: string;\n uri: string;\n description?: string;\n access_mode?: \"readonly\" | \"readwrite\";\n auth?: string;\n options?: Record<string, unknown>;\n}\n\nexport interface MountListResult {\n mounts: MountEntry[];\n}\n\nexport interface MountCommandResult {\n success: boolean;\n message?: string;\n /** The normalized path (for display after successful add) */\n normalizedPath?: string;\n}\n\nexport interface MountValidateResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Check if a path looks like a remote Git URL\n * Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)\n */\nfunction isRemoteGitUrl(path: string): boolean {\n // SSH format: git@github.com:user/repo.git or user@host:path\n if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) {\n return true;\n }\n // Embedded protocol: https://github.com/user/repo.git\n if (/^(https?|ssh|git):\\/\\//.test(path)) {\n return true;\n }\n return false;\n}\n\n/**\n * Resolve relative paths in URI to absolute paths\n * Supports fs://, git://, sqlite://, json:// schemes\n * Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)\n */\nfunction resolveUriPath(uri: string, cwd: string): string {\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//);\n\n // No protocol prefix\n if (!schemeMatch) {\n // Check if it's a remote git URL (SSH format like git@github.com:user/repo.git)\n if (isRemoteGitUrl(uri)) {\n return `git://${uri}`;\n }\n // Treat as local filesystem path\n const absolutePath = isAbsolute(uri) ? uri : resolve(cwd, uri);\n return `fs://${absolutePath}`;\n }\n\n const scheme = schemeMatch[1];\n const pathPart = uri.slice(schemeMatch[0].length);\n\n // Only resolve for local file-based schemes\n if (![\"fs\", \"git\", \"sqlite\", \"json\"].includes(scheme!)) {\n return uri;\n }\n\n // For git:// scheme, check if the path is a remote URL (not a local path)\n if (scheme === \"git\" && isRemoteGitUrl(pathPart)) {\n return uri;\n }\n\n // If path is already absolute, return as-is\n if (isAbsolute(pathPart)) {\n return uri;\n }\n\n // Resolve relative path against cwd\n const absolutePath = resolve(cwd, pathPart);\n return `${scheme}://${absolutePath}`;\n}\n\n/**\n * List all mounts from config (merged from all config layers)\n */\nexport async function mountListCommand(cwd: string): Promise<MountListResult> {\n const loader = new ConfigLoader();\n const config = await loader.load(cwd);\n return {\n mounts: config.mounts as MountEntry[],\n };\n}\n\n/**\n * Add a mount to config\n */\nexport async function mountAddCommand(\n cwd: string,\n path: string,\n uri: string,\n options: { description?: string } = {},\n): Promise<MountCommandResult> {\n // Check for empty URI before resolving (resolveUriPath would transform empty string)\n if (!uri || uri.trim() === \"\") {\n return {\n success: false,\n message: \"URI is required\",\n };\n }\n\n // Resolve relative paths in URI to absolute paths\n const resolvedUri = resolveUriPath(uri, cwd);\n\n // Validate and normalize inputs using schema\n // The schema transforms path (normalizes it) and validates all fields\n const validation = MountSchema.safeParse({\n path,\n uri: resolvedUri,\n ...options,\n });\n if (!validation.success) {\n const errors = validation.error.errors.map((e) => e.message).join(\"; \");\n return {\n success: false,\n message: errors,\n };\n }\n\n // Build mount entry with validated/normalized path and resolved URI\n const newMount: MountEntry = {\n path: validation.data.path, // Use normalized path from schema\n uri: validation.data.uri,\n ...(options.description && { description: options.description }),\n };\n\n const configDir = join(cwd, CONFIG_DIR_NAME);\n const configPath = join(configDir, CONFIG_FILE_NAME);\n\n // Load existing config or create new\n const config: { mounts: MountEntry[] } = { mounts: [] };\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const parsed = parse(content) as { mounts?: MountEntry[] };\n config.mounts = parsed.mounts ?? [];\n } catch {\n // Config doesn't exist, will create\n }\n\n // Check for duplicate path (use normalized path for comparison)\n const normalizedPath = validation.data.path;\n if (config.mounts.some((m) => m.path === normalizedPath)) {\n return {\n success: false,\n message: `Mount path \"${normalizedPath}\" already exists`,\n };\n }\n\n // Add validated mount\n config.mounts.push(newMount);\n\n // Ensure config directory exists\n try {\n await mkdir(configDir, { recursive: true });\n } catch {\n // Directory might already exist\n }\n\n // Write config\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n normalizedPath,\n };\n}\n\n/**\n * Remove a mount from config\n */\nexport async function mountRemoveCommand(cwd: string, path: string): Promise<MountCommandResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n const index = mounts.findIndex((m) => m.path === path);\n if (index === -1) {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n\n mounts.splice(index, 1);\n config.mounts = mounts;\n\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n };\n } catch {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n}\n\n/**\n * Validate mount configuration\n */\nexport async function mountValidateCommand(cwd: string): Promise<MountValidateResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n const errors: string[] = [];\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n for (const mount of mounts) {\n // Validate mount using schema\n const validation = MountSchema.safeParse(mount);\n if (!validation.success) {\n for (const err of validation.error.errors) {\n errors.push(`Mount \"${mount.path}\": ${err.message}`);\n }\n continue;\n }\n\n // For fs:// URIs, check if target exists\n if (mount.uri.startsWith(\"fs://\")) {\n const targetPath = mount.uri.replace(\"fs://\", \"\");\n try {\n await access(targetPath);\n } catch {\n errors.push(`Mount target \"${targetPath}\" does not exist`);\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n } catch {\n return {\n valid: true,\n errors: [],\n };\n }\n}\n\n/**\n * Format mount list output for different views\n */\nexport function formatMountListOutput(mounts: MountEntry[], view: ViewType): string {\n switch (view) {\n case \"json\":\n return JSON.stringify({ mounts }, null, 2);\n case \"llm\":\n return formatLlm(mounts);\n case \"human\":\n return formatHuman(mounts);\n default:\n return formatDefault(mounts);\n }\n}\n\nfunction formatDefault(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"No mounts configured\";\n }\n\n return mounts\n .map((m) => {\n const desc = m.description ? ` (${m.description})` : \"\";\n return `${m.path} → ${m.uri}${desc}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatLlm(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"NO_MOUNTS\";\n }\n\n return mounts\n .map((m) => {\n const lines = [`MOUNT ${m.path}`, `URI=${m.uri}`];\n if (m.description) {\n lines.push(`DESC=${m.description}`);\n }\n return lines.join(\"\\n\");\n })\n .join(\"\\n\\n\");\n}\n\nfunction formatHuman(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return colors.dim(\"No mounts configured.\");\n }\n\n const lines = [colors.bold(\"Configured Mounts:\"), \"\"];\n for (const m of mounts) {\n lines.push(` ${colors.cyan(m.path)}`);\n lines.push(` ${colors.dim(\"URI:\")} ${m.uri}`);\n if (m.description) {\n lines.push(` ${colors.dim(\"Description:\")} ${m.description}`);\n }\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mount.mjs","names":[],"sources":["../../src/commands/mount.ts"],"sourcesContent":["import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { isAbsolute, join, resolve } from \"node:path\";\nimport { parse, stringify } from \"smol-toml\";\nimport { CONFIG_DIR_NAME, CONFIG_FILE_NAME, ConfigLoader } from \"../config/loader.js\";\nimport { MountSchema } from \"../config/schema.js\";\nimport { colors } from \"../ui/index.js\";\nimport type { ViewType } from \"./ls.js\";\n\nexport interface MountEntry {\n path: string;\n uri: string;\n description?: string;\n access_mode?: \"readonly\" | \"readwrite\";\n auth?: string;\n token?: string;\n options?: Record<string, unknown>;\n}\n\nexport interface MountListResult {\n mounts: MountEntry[];\n}\n\nexport interface MountCommandResult {\n success: boolean;\n message?: string;\n /** The normalized path (for display after successful add) */\n normalizedPath?: string;\n}\n\nexport interface MountValidateResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Check if a path looks like a remote Git URL\n * Matches SSH format (git@host:path) or embedded protocols (https://, http://, ssh://)\n */\nfunction isRemoteGitUrl(path: string): boolean {\n // SSH format: git@github.com:user/repo.git or user@host:path\n if (/^[a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+:/.test(path)) {\n return true;\n }\n // Embedded protocol: https://github.com/user/repo.git\n if (/^(https?|ssh|git):\\/\\//.test(path)) {\n return true;\n }\n return false;\n}\n\n/**\n * Resolve relative paths in URI to absolute paths\n * Supports fs://, git://, sqlite://, json:// schemes\n * Paths without protocol prefix are treated as fs:// paths (unless it's a remote git URL)\n */\nfunction resolveUriPath(uri: string, cwd: string): string {\n const schemeMatch = uri.match(/^([a-z]+):\\/\\//);\n\n // No protocol prefix\n if (!schemeMatch) {\n // Check if it's a remote git URL (SSH format like git@github.com:user/repo.git)\n if (isRemoteGitUrl(uri)) {\n return `git://${uri}`;\n }\n // Treat as local filesystem path\n const absolutePath = isAbsolute(uri) ? uri : resolve(cwd, uri);\n return `fs://${absolutePath}`;\n }\n\n const scheme = schemeMatch[1];\n const pathPart = uri.slice(schemeMatch[0].length);\n\n // Only resolve for local file-based schemes\n if (![\"fs\", \"git\", \"sqlite\", \"json\"].includes(scheme!)) {\n return uri;\n }\n\n // For git:// scheme, check if the path is a remote URL (not a local path)\n if (scheme === \"git\" && isRemoteGitUrl(pathPart)) {\n return uri;\n }\n\n // If path is already absolute, return as-is\n if (isAbsolute(pathPart)) {\n return uri;\n }\n\n // Resolve relative path against cwd\n const absolutePath = resolve(cwd, pathPart);\n return `${scheme}://${absolutePath}`;\n}\n\n/**\n * List all mounts from config (merged from all config layers)\n */\nexport async function mountListCommand(cwd: string): Promise<MountListResult> {\n const loader = new ConfigLoader();\n const config = await loader.load(cwd);\n return {\n mounts: config.mounts as MountEntry[],\n };\n}\n\n/**\n * Add a mount to config\n */\nexport async function mountAddCommand(\n cwd: string,\n path: string,\n uri: string,\n options: { description?: string; token?: string } = {},\n): Promise<MountCommandResult> {\n // Check for empty URI before resolving (resolveUriPath would transform empty string)\n if (!uri || uri.trim() === \"\") {\n return {\n success: false,\n message: \"URI is required\",\n };\n }\n\n // Resolve relative paths in URI to absolute paths\n const resolvedUri = resolveUriPath(uri, cwd);\n\n // Validate and normalize inputs using schema\n // The schema transforms path (normalizes it) and validates all fields\n const validation = MountSchema.safeParse({\n path,\n uri: resolvedUri,\n ...options,\n });\n if (!validation.success) {\n const errors = validation.error.errors.map((e) => e.message).join(\"; \");\n return {\n success: false,\n message: errors,\n };\n }\n\n // Build mount entry with validated/normalized path and resolved URI\n const newMount: MountEntry = {\n path: validation.data.path, // Use normalized path from schema\n uri: validation.data.uri,\n ...(options.description && { description: options.description }),\n ...(options.token && { token: options.token }),\n };\n\n const configDir = join(cwd, CONFIG_DIR_NAME);\n const configPath = join(configDir, CONFIG_FILE_NAME);\n\n // Load existing config or create new\n const config: { mounts: MountEntry[] } = { mounts: [] };\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const parsed = parse(content) as { mounts?: MountEntry[] };\n config.mounts = parsed.mounts ?? [];\n } catch {\n // Config doesn't exist, will create\n }\n\n // Check for duplicate path (use normalized path for comparison)\n const normalizedPath = validation.data.path;\n if (config.mounts.some((m) => m.path === normalizedPath)) {\n return {\n success: false,\n message: `Mount path \"${normalizedPath}\" already exists`,\n };\n }\n\n // Add validated mount\n config.mounts.push(newMount);\n\n // Ensure config directory exists\n try {\n await mkdir(configDir, { recursive: true });\n } catch {\n // Directory might already exist\n }\n\n // Write config\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n normalizedPath,\n };\n}\n\n/**\n * Remove a mount from config\n */\nexport async function mountRemoveCommand(cwd: string, path: string): Promise<MountCommandResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n const index = mounts.findIndex((m) => m.path === path);\n if (index === -1) {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n\n mounts.splice(index, 1);\n config.mounts = mounts;\n\n await writeFile(configPath, stringify(config), \"utf-8\");\n\n return {\n success: true,\n };\n } catch {\n return {\n success: false,\n message: `Mount path \"${path}\" not found`,\n };\n }\n}\n\n/**\n * Validate mount configuration\n */\nexport async function mountValidateCommand(cwd: string): Promise<MountValidateResult> {\n const configPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE_NAME);\n const errors: string[] = [];\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const config = parse(content) as { mounts?: MountEntry[] };\n const mounts = config.mounts ?? [];\n\n for (const mount of mounts) {\n // Validate mount using schema\n const validation = MountSchema.safeParse(mount);\n if (!validation.success) {\n for (const err of validation.error.errors) {\n errors.push(`Mount \"${mount.path}\": ${err.message}`);\n }\n continue;\n }\n\n // For fs:// URIs, check if target exists\n if (mount.uri.startsWith(\"fs://\")) {\n const targetPath = mount.uri.replace(\"fs://\", \"\");\n try {\n await access(targetPath);\n } catch {\n errors.push(`Mount target \"${targetPath}\" does not exist`);\n }\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n } catch {\n return {\n valid: true,\n errors: [],\n };\n }\n}\n\n/**\n * Format mount list output for different views\n */\nexport function formatMountListOutput(mounts: MountEntry[], view: ViewType): string {\n switch (view) {\n case \"json\":\n return JSON.stringify({ mounts }, null, 2);\n case \"llm\":\n return formatLlm(mounts);\n case \"human\":\n return formatHuman(mounts);\n default:\n return formatDefault(mounts);\n }\n}\n\nfunction formatDefault(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"No mounts configured\";\n }\n\n return mounts\n .map((m) => {\n const desc = m.description ? ` (${m.description})` : \"\";\n return `${m.path} → ${m.uri}${desc}`;\n })\n .join(\"\\n\");\n}\n\nfunction formatLlm(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return \"NO_MOUNTS\";\n }\n\n return mounts\n .map((m) => {\n const lines = [`MOUNT ${m.path}`, `URI=${m.uri}`];\n if (m.description) {\n lines.push(`DESC=${m.description}`);\n }\n return lines.join(\"\\n\");\n })\n .join(\"\\n\\n\");\n}\n\nfunction formatHuman(mounts: MountEntry[]): string {\n if (mounts.length === 0) {\n return colors.dim(\"No mounts configured.\");\n }\n\n const lines = [colors.bold(\"Configured Mounts:\"), \"\"];\n for (const m of mounts) {\n lines.push(` ${colors.cyan(m.path)}`);\n lines.push(` ${colors.dim(\"URI:\")} ${m.uri}`);\n if (m.description) {\n lines.push(` ${colors.dim(\"Description:\")} ${m.description}`);\n }\n lines.push(\"\");\n }\n\n return lines.join(\"\\n\").trimEnd();\n}\n"],"mappings":"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"}
@@ -1,22 +1,92 @@
1
+ const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
2
+ const require_meta = require('../utils/meta.cjs');
3
+ let _aigne_afs = require("@aigne/afs");
1
4
 
2
5
  //#region src/commands/read.ts
3
6
  /**
4
7
  * Read file content
5
8
  */
6
9
  async function readCommand(runtime, path) {
7
- const result = await runtime.read(path);
10
+ let result;
11
+ try {
12
+ result = await runtime.read(path);
13
+ } catch (error) {
14
+ if (error instanceof _aigne_afs.AFSNotFoundError) return {
15
+ path,
16
+ content: void 0
17
+ };
18
+ throw error;
19
+ }
8
20
  if (!result.data) return {
9
21
  path,
10
22
  content: void 0
11
23
  };
12
24
  const entry = result.data;
13
- return {
25
+ const isMeta = require_meta.isMetaPath(path);
26
+ const isAction = require_meta.isActionsPath(path);
27
+ const content = isMeta ? entry.content : typeof entry.content === "string" ? entry.content : entry.content !== void 0 ? JSON.stringify(entry.content) : void 0;
28
+ const readResult = {
14
29
  path: entry.path,
15
- content: typeof entry.content === "string" ? entry.content : JSON.stringify(entry.content),
16
- type: entry.metadata?.type === "directory" ? "directory" : "file",
30
+ content,
31
+ isMeta,
32
+ isAction,
17
33
  size: entry.metadata?.size,
18
- mimeType: entry.metadata?.mimeType
34
+ mimeType: entry.metadata?.mimeType,
35
+ childrenCount: entry.metadata?.childrenCount
19
36
  };
37
+ if (!isMeta && !isAction) try {
38
+ const statResult = await runtime.stat(path);
39
+ if (statResult.data?.actions && statResult.data.actions.length > 0) readResult.actions = statResult.data.actions;
40
+ } catch {}
41
+ const meta = extractMeta(entry.metadata);
42
+ if (meta && Object.keys(meta).length > 0) readResult.meta = meta;
43
+ return readResult;
44
+ }
45
+ /**
46
+ * Extract meta fields from entry metadata
47
+ * Excludes built-in fields that are exposed separately
48
+ */
49
+ function extractMeta(metadata) {
50
+ if (!metadata) return void 0;
51
+ const builtInFields = new Set([
52
+ "type",
53
+ "size",
54
+ "mimeType",
55
+ "childrenCount"
56
+ ]);
57
+ const meta = {};
58
+ for (const [key, value] of Object.entries(metadata)) if (!builtInFields.has(key) && value !== void 0) meta[key] = value;
59
+ return Object.keys(meta).length > 0 ? meta : void 0;
60
+ }
61
+ /**
62
+ * Extract user-defined metadata (excludes system fields)
63
+ */
64
+ function extractUserMetadata(meta) {
65
+ if (!meta) return {};
66
+ const systemFields = new Set([
67
+ "kind",
68
+ "kinds",
69
+ "childrenCount",
70
+ "inputSchema",
71
+ "required",
72
+ "description"
73
+ ]);
74
+ return Object.fromEntries(Object.entries(meta).filter(([k]) => !systemFields.has(k)));
75
+ }
76
+ /**
77
+ * Format params summary: format*,path,limit (* = required)
78
+ */
79
+ function formatParamsSummary(schema, required = []) {
80
+ if (!schema) return "";
81
+ const props = schema.properties || schema;
82
+ return Object.keys(props).map((name) => required.includes(name) ? `${name}*` : name).join(",");
83
+ }
84
+ /**
85
+ * Check if entry is executable (has afs:executable in kinds)
86
+ */
87
+ function isExecutable(meta) {
88
+ if (!meta?.kinds) return false;
89
+ return meta.kinds.includes("afs:executable");
20
90
  }
21
91
  /**
22
92
  * Format read output for different views
@@ -30,34 +100,163 @@ function formatReadOutput(read, view) {
30
100
  }
31
101
  }
32
102
  /**
33
- * Default format: Raw content
103
+ * Default format: Raw content or one-liner summary
104
+ * - If content exists: show raw content
105
+ * - If no content (directories): show one-liner summary /path kind children=N actions=a,b,c
106
+ * - For executables: /path afs:executable params=format*,path,limit
107
+ * - For meta paths: raw JSON
34
108
  */
35
109
  function formatDefault(read) {
36
- return read.content ?? "";
110
+ if (read.isMeta && typeof read.content === "object") return JSON.stringify(read.content, null, 2);
111
+ if (read.isAction && isExecutable(read.meta)) {
112
+ if (read.content !== void 0 && read.content !== null && read.content !== "") return typeof read.content === "string" ? read.content : JSON.stringify(read.content, null, 2);
113
+ const parts$1 = [read.path, "afs:executable"];
114
+ if (read.meta?.inputSchema) {
115
+ const params = formatParamsSummary(read.meta.inputSchema, read.meta.required);
116
+ if (params) parts$1.push(`params=${params}`);
117
+ }
118
+ return parts$1.join(" ");
119
+ }
120
+ if (read.content !== void 0 && read.content !== null && read.content !== "") return typeof read.content === "string" ? read.content : JSON.stringify(read.content, null, 2);
121
+ const parts = [read.path];
122
+ if (read.meta?.kind) parts.push(read.meta.kind);
123
+ if (read.childrenCount !== void 0) parts.push(`children=${read.childrenCount}`);
124
+ if (read.actions && read.actions.length > 0) parts.push(`actions=${read.actions.map((a) => a.name).join(",")}`);
125
+ return parts.join(" ");
37
126
  }
38
127
  /**
39
- * JSON format
128
+ * JSON format (includes meta field for regular files if present)
40
129
  */
41
130
  function formatJson(read) {
42
131
  return JSON.stringify(read, null, 2);
43
132
  }
44
133
  /**
45
- * LLM format: Content with header
134
+ * LLM format: Structured text for AI parsing
46
135
  */
47
136
  function formatLlm(read) {
48
137
  const lines = [];
49
- lines.push(`FILE ${read.path}`);
138
+ if (read.isMeta) {
139
+ lines.push(`META ${read.path}`);
140
+ const content = read.content;
141
+ if (content?.kind) lines.push(`KIND ${content.kind}`);
142
+ lines.push(`CONTENT`);
143
+ lines.push(JSON.stringify(content));
144
+ return lines.join("\n");
145
+ }
146
+ if (read.isAction && isExecutable(read.meta)) {
147
+ lines.push(`EXECUTABLE ${read.path}`);
148
+ if (read.meta?.description) lines.push(`DESCRIPTION "${read.meta.description}"`);
149
+ if (read.meta?.inputSchema) {
150
+ const props = read.meta.inputSchema.properties || {};
151
+ const required = read.meta.required || [];
152
+ for (const [name, propDef] of Object.entries(props)) {
153
+ const prop = propDef;
154
+ const isReq = required.includes(name);
155
+ let paramLine = `PARAM ${name} TYPE ${prop.type || "any"}`;
156
+ paramLine += isReq ? " REQUIRED" : " OPTIONAL";
157
+ if (prop.enum) paramLine += ` CHOICES ${prop.enum.join(",")}`;
158
+ if (prop.default !== void 0) paramLine += ` DEFAULT ${prop.default}`;
159
+ lines.push(paramLine);
160
+ }
161
+ }
162
+ return lines.join("\n");
163
+ }
164
+ lines.push(`NODE ${read.path}`);
165
+ if (read.meta?.kind) lines.push(`KIND ${read.meta.kind}`);
166
+ if (read.meta?.kinds && Array.isArray(read.meta.kinds) && read.meta.kinds.length > 0) lines.push(`KINDS ${read.meta.kinds.join(" ")}`);
167
+ if (read.childrenCount !== void 0) lines.push(`CHILDREN ${read.childrenCount}`);
50
168
  if (read.mimeType) lines.push(`MIME ${read.mimeType}`);
51
169
  if (read.size !== void 0) lines.push(`SIZE ${read.size}`);
52
- lines.push(`CONTENT`);
53
- lines.push(read.content ?? "");
170
+ const userMeta = extractUserMetadata(read.meta);
171
+ if (Object.keys(userMeta).length > 0) {
172
+ const metaStr = Object.entries(userMeta).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(" ");
173
+ lines.push(`META ${metaStr}`);
174
+ }
175
+ if (read.actions && read.actions.length > 0) lines.push(`ACTIONS ${read.actions.map((a) => a.name).join(" ")}`);
176
+ if (read.content !== void 0 && read.content !== null && read.content !== "") lines.push(`CONTENT ${JSON.stringify(read.content)}`);
54
177
  return lines.join("\n");
55
178
  }
56
179
  /**
57
- * Human format: With syntax highlighting placeholder
180
+ * Human format: Formatted display with metadata and actions
58
181
  */
59
182
  function formatHuman(read) {
60
- return `${`--- ${read.path} ---`}\n${read.content ?? "(empty)"}`;
183
+ const lines = [];
184
+ if (read.isMeta && typeof read.content === "object") {
185
+ lines.push(`Metadata for ${read.path.replace(/\/.meta$/, "")}`);
186
+ lines.push("");
187
+ lines.push(JSON.stringify(read.content, null, 2));
188
+ return lines.join("\n");
189
+ }
190
+ if (read.isAction && isExecutable(read.meta)) {
191
+ const actionName = read.path.split("/").pop() || "action";
192
+ lines.push(`Action: ${actionName}`);
193
+ lines.push(`Path: ${read.path}`);
194
+ if (read.meta?.description) {
195
+ lines.push("");
196
+ lines.push("Description:");
197
+ lines.push(` ${read.meta.description}`);
198
+ }
199
+ if (read.meta?.inputSchema) {
200
+ const props = read.meta.inputSchema.properties || {};
201
+ const required = read.meta.required || [];
202
+ if (Object.keys(props).length > 0) {
203
+ lines.push("");
204
+ lines.push("Parameters:");
205
+ for (const [name, propDef] of Object.entries(props)) {
206
+ const prop = propDef;
207
+ const isReq = required.includes(name);
208
+ const typeStr = `<${prop.type || "any"}>`;
209
+ const reqLabel = isReq ? "(required)" : "(optional)";
210
+ let line = ` --${name} ${typeStr}`.padEnd(24);
211
+ line += prop.description || "";
212
+ line += ` ${reqLabel}`;
213
+ lines.push(line);
214
+ if (prop.enum) lines.push(`${"".padEnd(24)}Choices: ${prop.enum.join(", ")}`);
215
+ if (prop.default !== void 0) lines.push(`${"".padEnd(24)}Default: ${prop.default}`);
216
+ }
217
+ }
218
+ }
219
+ lines.push("");
220
+ lines.push("Usage:");
221
+ lines.push(` afs exec ${read.path}`);
222
+ return lines.join("\n");
223
+ }
224
+ lines.push(read.path);
225
+ lines.push("");
226
+ if (read.meta?.kind) lines.push(`Kind: ${read.meta.kind}`);
227
+ if (read.meta?.kinds && Array.isArray(read.meta.kinds) && read.meta.kinds.length > 0) lines.push(`Kinds: ${read.meta.kinds.join(" → ")}`);
228
+ if (read.childrenCount !== void 0) lines.push(`Children: ${read.childrenCount}`);
229
+ const userMeta = extractUserMetadata(read.meta);
230
+ if (Object.keys(userMeta).length > 0) {
231
+ lines.push("");
232
+ lines.push("Metadata:");
233
+ for (const [key, value] of Object.entries(userMeta)) lines.push(` ${key.padEnd(12)} ${formatValue(value)}`);
234
+ }
235
+ if (read.actions && read.actions.length > 0) {
236
+ lines.push("");
237
+ lines.push(`Actions: ${read.actions.map((a) => a.name).join(", ")}`);
238
+ }
239
+ if (read.content !== void 0 && read.content !== null && read.content !== "") {
240
+ lines.push("");
241
+ lines.push("Content:");
242
+ lines.push(formatContent(read.content));
243
+ } else if (read.childrenCount !== void 0 && read.childrenCount > 0) {
244
+ lines.push("");
245
+ lines.push("Content:");
246
+ lines.push(" (directory - use 'afs ls' to list children)");
247
+ }
248
+ return lines.join("\n");
249
+ }
250
+ function formatValue(value) {
251
+ if (value === null || value === void 0) return "";
252
+ if (typeof value === "string") return value;
253
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
254
+ if (Array.isArray(value)) return value.join(", ");
255
+ return JSON.stringify(value);
256
+ }
257
+ function formatContent(content) {
258
+ if (typeof content === "string") return content.split("\n").map((line) => ` ${line}`).join("\n");
259
+ return ` ${JSON.stringify(content, null, 2).split("\n").join("\n ")}`;
61
260
  }
62
261
 
63
262
  //#endregion