Skip to content

Commit 3cbb4f7

Browse files
committed
feat: add Tier 1 file content operations (read, write, append, concatenate, move)
Add essential file content operations to complete the file operations toolkit: Operations added: - read-file: Read entire file contents as string - write-file: Write string contents to file (overwrites existing) - append-to-file: Append content to existing file (creates if doesn't exist) - concatenate-files: Merge multiple source files into single destination - move-path: Move/rename files or directories (cross-filesystem compatible) Implementation details: - All operations include security validation via ValidatePath - Parent directory auto-creation for write operations - MovePath falls back to copy+remove for cross-filesystem moves - Full JSON batch operation support with backward compatibility - Comprehensive test coverage for all new operations Files modified: - wit/file-operations.wit: Added 5 new WIT interface functions - tinygo/operations.go: Implemented all 5 operations (157 lines) - tinygo/json_bridge.go: Added JSON support with validation (129 lines) - tinygo/operations_test.go: Added 8 comprehensive tests (236 lines) All tests passing (32/32), component builds successfully.
1 parent 6f2c635 commit 3cbb4f7

File tree

4 files changed

+535
-2
lines changed

4 files changed

+535
-2
lines changed

tinygo/json_bridge.go

Lines changed: 127 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ type Operation struct {
2727
Args []string `json:"args,omitempty"`
2828
WorkDir string `json:"work_dir,omitempty"`
2929
OutputFile string `json:"output_file,omitempty"`
30+
Content string `json:"content,omitempty"` // For write_file, append_to_file
31+
Sources []string `json:"sources,omitempty"` // For concatenate_files
3032
}
3133

3234
// WorkspaceInfo represents the result of workspace operations
@@ -108,15 +110,17 @@ func GetJsonSchema() string {
108110
"properties": {
109111
"type": {
110112
"type": "string",
111-
"enum": ["copy_file", "mkdir", "copy_directory_contents", "run_command"]
113+
"enum": ["copy_file", "mkdir", "copy_directory_contents", "run_command", "read_file", "write_file", "append_to_file", "concatenate_files", "move_path"]
112114
},
113115
"src_path": {"type": "string"},
114116
"dest_path": {"type": "string"},
115117
"path": {"type": "string"},
116118
"command": {"type": "string"},
117119
"args": {"type": "array", "items": {"type": "string"}},
118120
"work_dir": {"type": "string"},
119-
"output_file": {"type": "string"}
121+
"output_file": {"type": "string"},
122+
"content": {"type": "string"},
123+
"sources": {"type": "array", "items": {"type": "string"}}
120124
}
121125
}
122126
}
@@ -180,6 +184,52 @@ func validateOperation(op Operation, index int) error {
180184
if op.Command == "" {
181185
return fmt.Errorf("operation %d: run_command requires command", index)
182186
}
187+
case "read_file":
188+
if op.Path == "" {
189+
return fmt.Errorf("operation %d: read_file requires path", index)
190+
}
191+
if !filepath.IsAbs(op.Path) {
192+
return fmt.Errorf("operation %d: path must be absolute: %s", index, op.Path)
193+
}
194+
case "write_file":
195+
if op.Path == "" {
196+
return fmt.Errorf("operation %d: write_file requires path", index)
197+
}
198+
if filepath.IsAbs(op.Path) {
199+
return fmt.Errorf("operation %d: path must be relative: %s", index, op.Path)
200+
}
201+
case "append_to_file":
202+
if op.Path == "" {
203+
return fmt.Errorf("operation %d: append_to_file requires path", index)
204+
}
205+
if filepath.IsAbs(op.Path) {
206+
return fmt.Errorf("operation %d: path must be relative: %s", index, op.Path)
207+
}
208+
case "concatenate_files":
209+
if len(op.Sources) == 0 {
210+
return fmt.Errorf("operation %d: concatenate_files requires sources", index)
211+
}
212+
if op.DestPath == "" {
213+
return fmt.Errorf("operation %d: concatenate_files requires dest_path", index)
214+
}
215+
for i, source := range op.Sources {
216+
if !filepath.IsAbs(source) {
217+
return fmt.Errorf("operation %d: source %d must be absolute: %s", index, i, source)
218+
}
219+
}
220+
if filepath.IsAbs(op.DestPath) {
221+
return fmt.Errorf("operation %d: dest_path must be relative: %s", index, op.DestPath)
222+
}
223+
case "move_path":
224+
if op.SrcPath == "" || op.DestPath == "" {
225+
return fmt.Errorf("operation %d: move_path requires src_path and dest_path", index)
226+
}
227+
if !filepath.IsAbs(op.SrcPath) {
228+
return fmt.Errorf("operation %d: src_path must be absolute: %s", index, op.SrcPath)
229+
}
230+
if filepath.IsAbs(op.DestPath) {
231+
return fmt.Errorf("operation %d: dest_path must be relative: %s", index, op.DestPath)
232+
}
183233
default:
184234
return fmt.Errorf("operation %d: unknown operation type: %s", index, op.Type)
185235
}
@@ -198,6 +248,16 @@ func executeJsonOperation(op Operation, workspaceDir string) ([]string, error) {
198248
return executeJsonCopyDirectoryContents(op, workspaceDir)
199249
case "run_command":
200250
return executeJsonRunCommand(op, workspaceDir)
251+
case "read_file":
252+
return executeJsonReadFile(op, workspaceDir)
253+
case "write_file":
254+
return executeJsonWriteFile(op, workspaceDir)
255+
case "append_to_file":
256+
return executeJsonAppendToFile(op, workspaceDir)
257+
case "concatenate_files":
258+
return executeJsonConcatenateFiles(op, workspaceDir)
259+
case "move_path":
260+
return executeJsonMovePath(op, workspaceDir)
201261
default:
202262
return nil, fmt.Errorf("unsupported operation type: %s", op.Type)
203263
}
@@ -295,3 +355,68 @@ func executeJsonRunCommand(op Operation, workspaceDir string) ([]string, error)
295355

296356
return []string{}, nil
297357
}
358+
359+
// executeJsonReadFile executes read_file operation
360+
func executeJsonReadFile(op Operation, workspaceDir string) ([]string, error) {
361+
// Read file uses absolute path (from validation)
362+
content, err := ReadFile(op.Path)
363+
if err != nil {
364+
return nil, err
365+
}
366+
367+
// If output_file is specified, write content there
368+
if op.OutputFile != "" {
369+
outputPath := filepath.Join(workspaceDir, op.OutputFile)
370+
if err := WriteFile(outputPath, content); err != nil {
371+
return nil, fmt.Errorf("failed to write output: %w", err)
372+
}
373+
return []string{outputPath}, nil
374+
}
375+
376+
// Otherwise just return the source path as processed
377+
return []string{op.Path}, nil
378+
}
379+
380+
// executeJsonWriteFile executes write_file operation
381+
func executeJsonWriteFile(op Operation, workspaceDir string) ([]string, error) {
382+
path := filepath.Join(workspaceDir, op.Path)
383+
384+
if err := WriteFile(path, op.Content); err != nil {
385+
return nil, err
386+
}
387+
388+
return []string{path}, nil
389+
}
390+
391+
// executeJsonAppendToFile executes append_to_file operation
392+
func executeJsonAppendToFile(op Operation, workspaceDir string) ([]string, error) {
393+
path := filepath.Join(workspaceDir, op.Path)
394+
395+
if err := AppendToFile(path, op.Content); err != nil {
396+
return nil, err
397+
}
398+
399+
return []string{path}, nil
400+
}
401+
402+
// executeJsonConcatenateFiles executes concatenate_files operation
403+
func executeJsonConcatenateFiles(op Operation, workspaceDir string) ([]string, error) {
404+
dest := filepath.Join(workspaceDir, op.DestPath)
405+
406+
if err := ConcatenateFiles(op.Sources, dest); err != nil {
407+
return nil, err
408+
}
409+
410+
return []string{dest}, nil
411+
}
412+
413+
// executeJsonMovePath executes move_path operation
414+
func executeJsonMovePath(op Operation, workspaceDir string) ([]string, error) {
415+
dest := filepath.Join(workspaceDir, op.DestPath)
416+
417+
if err := MovePath(op.SrcPath, dest); err != nil {
418+
return nil, err
419+
}
420+
421+
return []string{dest}, nil
422+
}

tinygo/operations.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,163 @@ func ListDirectory(dir string, pattern *string) ([]string, error) {
200200
return result, nil
201201
}
202202

203+
// ReadFile reads the entire contents of a file as a string
204+
// Implements the read-file WIT interface function
205+
func ReadFile(path string) (string, error) {
206+
// Security validation
207+
if err := ValidatePath(path, []string{}); err != nil {
208+
return "", fmt.Errorf("security validation failed: %w", err)
209+
}
210+
211+
content, err := os.ReadFile(path)
212+
if err != nil {
213+
return "", fmt.Errorf("failed to read file %s: %w", path, err)
214+
}
215+
216+
return string(content), nil
217+
}
218+
219+
// WriteFile writes string contents to a file, overwriting if it exists
220+
// Implements the write-file WIT interface function
221+
func WriteFile(path, content string) error {
222+
// Security validation
223+
if err := ValidatePath(path, []string{}); err != nil {
224+
return fmt.Errorf("security validation failed: %w", err)
225+
}
226+
227+
// Ensure parent directory exists
228+
dir := filepath.Dir(path)
229+
if err := os.MkdirAll(dir, 0755); err != nil {
230+
return fmt.Errorf("failed to create parent directory %s: %w", dir, err)
231+
}
232+
233+
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
234+
return fmt.Errorf("failed to write file %s: %w", path, err)
235+
}
236+
237+
return nil
238+
}
239+
240+
// AppendToFile appends string content to an existing file (creates if doesn't exist)
241+
// Implements the append-to-file WIT interface function
242+
func AppendToFile(path, content string) error {
243+
// Security validation
244+
if err := ValidatePath(path, []string{}); err != nil {
245+
return fmt.Errorf("security validation failed: %w", err)
246+
}
247+
248+
// Ensure parent directory exists
249+
dir := filepath.Dir(path)
250+
if err := os.MkdirAll(dir, 0755); err != nil {
251+
return fmt.Errorf("failed to create parent directory %s: %w", dir, err)
252+
}
253+
254+
// Open file in append mode (create if doesn't exist)
255+
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
256+
if err != nil {
257+
return fmt.Errorf("failed to open file %s for appending: %w", path, err)
258+
}
259+
defer file.Close()
260+
261+
if _, err := file.WriteString(content); err != nil {
262+
return fmt.Errorf("failed to append to file %s: %w", path, err)
263+
}
264+
265+
return nil
266+
}
267+
268+
// ConcatenateFiles concatenates multiple source files into a single destination file
269+
// Implements the concatenate-files WIT interface function
270+
func ConcatenateFiles(sources []string, dest string) error {
271+
// Security validation for destination
272+
if err := ValidatePath(dest, []string{}); err != nil {
273+
return fmt.Errorf("security validation failed for destination: %w", err)
274+
}
275+
276+
if len(sources) == 0 {
277+
return fmt.Errorf("no source files provided for concatenation")
278+
}
279+
280+
// Ensure destination directory exists
281+
destDir := filepath.Dir(dest)
282+
if err := os.MkdirAll(destDir, 0755); err != nil {
283+
return fmt.Errorf("failed to create destination directory %s: %w", destDir, err)
284+
}
285+
286+
// Create destination file
287+
destFile, err := os.Create(dest)
288+
if err != nil {
289+
return fmt.Errorf("failed to create destination file %s: %w", dest, err)
290+
}
291+
defer destFile.Close()
292+
293+
// Read and concatenate each source file
294+
for i, source := range sources {
295+
// Security validation for each source
296+
if err := ValidatePath(source, []string{}); err != nil {
297+
return fmt.Errorf("security validation failed for source %d (%s): %w", i, source, err)
298+
}
299+
300+
content, err := os.ReadFile(source)
301+
if err != nil {
302+
return fmt.Errorf("failed to read source file %s: %w", source, err)
303+
}
304+
305+
if _, err := destFile.Write(content); err != nil {
306+
return fmt.Errorf("failed to write content from %s to destination: %w", source, err)
307+
}
308+
}
309+
310+
return nil
311+
}
312+
313+
// MovePath moves or renames a file or directory from source to destination
314+
// Implements the move-path WIT interface function
315+
func MovePath(src, dest string) error {
316+
// Security validation
317+
if err := ValidatePath(src, []string{}); err != nil {
318+
return fmt.Errorf("security validation failed for source: %w", err)
319+
}
320+
if err := ValidatePath(dest, []string{}); err != nil {
321+
return fmt.Errorf("security validation failed for destination: %w", err)
322+
}
323+
324+
// Ensure destination parent directory exists
325+
destDir := filepath.Dir(dest)
326+
if err := os.MkdirAll(destDir, 0755); err != nil {
327+
return fmt.Errorf("failed to create destination directory %s: %w", destDir, err)
328+
}
329+
330+
// Attempt rename (works if on same filesystem)
331+
err := os.Rename(src, dest)
332+
if err != nil {
333+
// If rename fails (e.g., cross-filesystem), fall back to copy + remove
334+
srcInfo, statErr := os.Stat(src)
335+
if statErr != nil {
336+
return fmt.Errorf("failed to stat source %s: %w", src, statErr)
337+
}
338+
339+
if srcInfo.IsDir() {
340+
// Copy directory recursively
341+
if err := CopyDirectory(src, dest); err != nil {
342+
return fmt.Errorf("failed to copy directory during move: %w", err)
343+
}
344+
} else {
345+
// Copy file
346+
if err := CopyFile(src, dest); err != nil {
347+
return fmt.Errorf("failed to copy file during move: %w", err)
348+
}
349+
}
350+
351+
// Remove source after successful copy
352+
if err := RemovePath(src); err != nil {
353+
return fmt.Errorf("failed to remove source after move: %w", err)
354+
}
355+
}
356+
357+
return nil
358+
}
359+
203360
// Helper functions
204361

205362
// copyDirectoryContents recursively copies directory contents

0 commit comments

Comments
 (0)