@soederpop/luca 0.0.6 → 0.0.7

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 (211) hide show
  1. package/CLAUDE.md +10 -1
  2. package/bun.lock +1 -1
  3. package/commands/build-bootstrap.ts +78 -0
  4. package/commands/build-scaffolds.ts +24 -2
  5. package/commands/try-all-challenges.ts +543 -0
  6. package/commands/try-challenge.ts +100 -0
  7. package/docs/README.md +52 -80
  8. package/docs/TABLE-OF-CONTENTS.md +82 -51
  9. package/docs/apis/clients/elevenlabs.md +232 -8
  10. package/docs/apis/clients/graph.md +59 -8
  11. package/docs/apis/clients/openai.md +362 -2
  12. package/docs/apis/clients/rest.md +122 -2
  13. package/docs/apis/clients/websocket.md +71 -17
  14. package/docs/apis/features/agi/assistant.md +9 -3
  15. package/docs/apis/features/agi/assistants-manager.md +2 -2
  16. package/docs/apis/features/agi/claude-code.md +153 -14
  17. package/docs/apis/features/agi/conversation-history.md +15 -3
  18. package/docs/apis/features/agi/conversation.md +133 -20
  19. package/docs/apis/features/agi/openai-codex.md +90 -12
  20. package/docs/apis/features/agi/skills-library.md +23 -5
  21. package/docs/apis/features/node/container-link.md +59 -0
  22. package/docs/apis/features/node/content-db.md +1 -1
  23. package/docs/apis/features/node/disk-cache.md +1 -1
  24. package/docs/apis/features/node/dns.md +1 -0
  25. package/docs/apis/features/node/docker.md +2 -1
  26. package/docs/apis/features/node/esbuild.md +4 -3
  27. package/docs/apis/features/node/file-manager.md +13 -4
  28. package/docs/apis/features/node/fs.md +726 -171
  29. package/docs/apis/features/node/git.md +1 -0
  30. package/docs/apis/features/node/google-auth.md +23 -4
  31. package/docs/apis/features/node/google-calendar.md +14 -2
  32. package/docs/apis/features/node/google-docs.md +15 -2
  33. package/docs/apis/features/node/google-drive.md +21 -3
  34. package/docs/apis/features/node/google-sheets.md +14 -2
  35. package/docs/apis/features/node/grep.md +2 -0
  36. package/docs/apis/features/node/helpers.md +29 -0
  37. package/docs/apis/features/node/ink.md +2 -2
  38. package/docs/apis/features/node/networking.md +39 -4
  39. package/docs/apis/features/node/os.md +28 -0
  40. package/docs/apis/features/node/postgres.md +26 -4
  41. package/docs/apis/features/node/proc.md +37 -28
  42. package/docs/apis/features/node/process-manager.md +33 -5
  43. package/docs/apis/features/node/repl.md +1 -1
  44. package/docs/apis/features/node/runpod.md +1 -0
  45. package/docs/apis/features/node/secure-shell.md +7 -0
  46. package/docs/apis/features/node/semantic-search.md +12 -5
  47. package/docs/apis/features/node/sqlite.md +26 -4
  48. package/docs/apis/features/node/telegram.md +30 -5
  49. package/docs/apis/features/node/tts.md +17 -2
  50. package/docs/apis/features/node/ui.md +1 -1
  51. package/docs/apis/features/node/vault.md +4 -9
  52. package/docs/apis/features/node/vm.md +3 -12
  53. package/docs/apis/features/node/window-manager.md +128 -20
  54. package/docs/apis/features/web/asset-loader.md +13 -1
  55. package/docs/apis/features/web/container-link.md +59 -0
  56. package/docs/apis/features/web/esbuild.md +4 -3
  57. package/docs/apis/features/web/helpers.md +29 -0
  58. package/docs/apis/features/web/network.md +16 -2
  59. package/docs/apis/features/web/speech.md +16 -2
  60. package/docs/apis/features/web/vault.md +4 -9
  61. package/docs/apis/features/web/vm.md +3 -12
  62. package/docs/apis/features/web/voice.md +18 -1
  63. package/docs/apis/servers/express.md +18 -2
  64. package/docs/apis/servers/mcp.md +29 -4
  65. package/docs/apis/servers/websocket.md +34 -6
  66. package/docs/bootstrap/CLAUDE.md +100 -0
  67. package/docs/bootstrap/SKILL.md +222 -0
  68. package/docs/bootstrap/templates/about-command.ts +41 -0
  69. package/docs/bootstrap/templates/docs-models.ts +22 -0
  70. package/docs/bootstrap/templates/docs-readme.md +43 -0
  71. package/docs/bootstrap/templates/example-feature.ts +53 -0
  72. package/docs/bootstrap/templates/health-endpoint.ts +15 -0
  73. package/docs/bootstrap/templates/luca-cli.ts +25 -0
  74. package/docs/challenges/caching-proxy.md +16 -0
  75. package/docs/challenges/content-db-round-trip.md +14 -0
  76. package/docs/challenges/custom-command.md +9 -0
  77. package/docs/challenges/file-watcher-pipeline.md +11 -0
  78. package/docs/challenges/grep-audit-report.md +15 -0
  79. package/docs/challenges/multi-feature-dashboard.md +14 -0
  80. package/docs/challenges/process-orchestrator.md +17 -0
  81. package/docs/challenges/rest-api-server-with-client.md +12 -0
  82. package/docs/challenges/script-runner-with-vm.md +11 -0
  83. package/docs/challenges/simple-rest-api.md +15 -0
  84. package/docs/challenges/websocket-serve-and-client.md +11 -0
  85. package/docs/challenges/yaml-config-system.md +14 -0
  86. package/docs/command-system-overhaul.md +94 -0
  87. package/docs/examples/assistant/CORE.md +18 -0
  88. package/docs/examples/assistant/hooks.ts +3 -0
  89. package/docs/examples/assistant/tools.ts +10 -0
  90. package/docs/examples/window-manager-layouts.md +180 -0
  91. package/docs/in-memory-fs.md +4 -0
  92. package/docs/models.ts +13 -10
  93. package/docs/philosophy.md +4 -3
  94. package/docs/reports/console-hmr-design.md +170 -0
  95. package/docs/reports/helper-semantic-search.md +72 -0
  96. package/docs/scaffolds/client.md +29 -20
  97. package/docs/scaffolds/command.md +64 -50
  98. package/docs/scaffolds/endpoint.md +31 -36
  99. package/docs/scaffolds/feature.md +28 -18
  100. package/docs/scaffolds/selector.md +91 -0
  101. package/docs/scaffolds/server.md +18 -9
  102. package/docs/selectors.md +115 -0
  103. package/docs/sessions/custom-command/attempt-log-2.md +195 -0
  104. package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +728 -0
  105. package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +555 -0
  106. package/docs/sessions/grep-audit-report/attempt-log-1.md +289 -0
  107. package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +679 -0
  108. package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +1 -0
  109. package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +920 -0
  110. package/docs/sessions/simple-rest-api/attempt-log-1.md +593 -0
  111. package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +995 -0
  112. package/docs/tutorials/00-bootstrap.md +148 -0
  113. package/docs/tutorials/07-endpoints.md +7 -7
  114. package/docs/tutorials/08-commands.md +153 -72
  115. package/luca.cli.ts +3 -0
  116. package/package.json +6 -5
  117. package/public/index.html +1430 -0
  118. package/scripts/examples/using-ollama.ts +2 -1
  119. package/scripts/update-introspection-data.ts +2 -2
  120. package/src/agi/endpoints/experts.ts +1 -1
  121. package/src/agi/features/assistant.ts +7 -0
  122. package/src/agi/features/assistants-manager.ts +5 -5
  123. package/src/agi/features/claude-code.ts +263 -3
  124. package/src/agi/features/conversation-history.ts +7 -1
  125. package/src/agi/features/conversation.ts +26 -3
  126. package/src/agi/features/openai-codex.ts +26 -2
  127. package/src/agi/features/openapi.ts +6 -1
  128. package/src/agi/features/skills-library.ts +9 -1
  129. package/src/bootstrap/generated.ts +540 -0
  130. package/src/cli/cli.ts +64 -21
  131. package/src/client.ts +23 -357
  132. package/src/clients/civitai/index.ts +1 -1
  133. package/src/clients/client-template.ts +1 -1
  134. package/src/clients/comfyui/index.ts +13 -2
  135. package/src/clients/elevenlabs/index.ts +2 -1
  136. package/src/clients/graph.ts +87 -0
  137. package/src/clients/openai/index.ts +10 -1
  138. package/src/clients/rest.ts +207 -0
  139. package/src/clients/websocket.ts +176 -0
  140. package/src/command.ts +281 -34
  141. package/src/commands/bootstrap.ts +181 -0
  142. package/src/commands/chat.ts +5 -4
  143. package/src/commands/describe.ts +225 -2
  144. package/src/commands/help.ts +35 -9
  145. package/src/commands/index.ts +3 -0
  146. package/src/commands/introspect.ts +92 -2
  147. package/src/commands/prompt.ts +5 -6
  148. package/src/commands/run.ts +33 -10
  149. package/src/commands/save-api-docs.ts +49 -0
  150. package/src/commands/scaffold.ts +169 -23
  151. package/src/commands/select.ts +94 -0
  152. package/src/commands/serve.ts +10 -1
  153. package/src/container.ts +15 -0
  154. package/src/endpoint.ts +19 -0
  155. package/src/graft.ts +181 -0
  156. package/src/introspection/generated.agi.ts +12458 -8968
  157. package/src/introspection/generated.node.ts +10573 -7145
  158. package/src/introspection/generated.web.ts +1 -1
  159. package/src/introspection/index.ts +26 -0
  160. package/src/node/container.ts +6 -7
  161. package/src/node/features/content-db.ts +49 -2
  162. package/src/node/features/disk-cache.ts +16 -9
  163. package/src/node/features/dns.ts +16 -3
  164. package/src/node/features/docker.ts +16 -4
  165. package/src/node/features/esbuild.ts +20 -0
  166. package/src/node/features/file-manager.ts +184 -29
  167. package/src/node/features/fs.ts +704 -248
  168. package/src/node/features/git.ts +21 -8
  169. package/src/node/features/grep.ts +23 -3
  170. package/src/node/features/helpers.ts +372 -43
  171. package/src/node/features/networking.ts +39 -4
  172. package/src/node/features/opener.ts +28 -15
  173. package/src/node/features/os.ts +76 -0
  174. package/src/node/features/port-exposer.ts +11 -1
  175. package/src/node/features/postgres.ts +17 -1
  176. package/src/node/features/proc.ts +4 -1
  177. package/src/node/features/python.ts +63 -14
  178. package/src/node/features/repl.ts +11 -7
  179. package/src/node/features/runpod.ts +16 -3
  180. package/src/node/features/secure-shell.ts +27 -2
  181. package/src/node/features/semantic-search.ts +12 -1
  182. package/src/node/features/ui.ts +5 -69
  183. package/src/node/features/vm.ts +17 -0
  184. package/src/node/features/window-manager.ts +68 -20
  185. package/src/node.ts +5 -0
  186. package/src/scaffolds/generated.ts +492 -290
  187. package/src/scaffolds/template.ts +9 -0
  188. package/src/schemas/base.ts +46 -5
  189. package/src/selector.ts +282 -0
  190. package/src/server.ts +11 -0
  191. package/src/servers/express.ts +27 -12
  192. package/src/servers/socket.ts +45 -11
  193. package/src/web/clients/socket.ts +4 -1
  194. package/src/web/container.ts +2 -1
  195. package/src/web/features/network.ts +7 -1
  196. package/src/web/features/voice-recognition.ts +16 -1
  197. package/test/clients-servers.test.ts +2 -1
  198. package/test/command.test.ts +267 -0
  199. package/test-integration/assistants-manager.test.ts +10 -20
  200. package/tmp/.cache/luca-disk-cache/content-v2/sha512/1b/b5/c75b28794f00f94c4d609a98978e9420e9b7146d204a7fbf5b0b30477292581705d207c0100dabaac27eef540aaaece3374af75104a93219d4ec8bfb44e7 +1 -0
  201. package/tmp/.cache/luca-disk-cache/content-v2/sha512/da/df/1d90ce4e042abeb035a197832c6d6893420a747a056be773eb00e4f745a037d505c8db13dde7d36b36b6b893addbb7df0f5fe9f0c13e665f20056447318b +1 -0
  202. package/tmp/.cache/luca-disk-cache/content-v2/sha512/ed/04/e1d0c2a58c2db29b3921ca2affb3ea4febe831c53b38ebc21019fb799823aba6ed5b4611873d2cd25d422d49955b852a9c326da0d678899bc1c2c2960901 +1 -0
  203. package/tmp/.cache/luca-disk-cache/index-v5/00/13/572aa4c9a94f99eda999695d050cdd0ca7fe2d23a50af03234d4c8ce0791 +2 -0
  204. package/tmp/.cache/luca-disk-cache/index-v5/75/a9/cb61dc0f0589e8ec10a9aca27b834bc73884c479941042d22a2b22324cd3 +2 -0
  205. package/tmp/.cache/luca-disk-cache/index-v5/9f/0f/8b1f915ee64cfff7667dd96acd7a5ac0a96aa91a346e19cefd45909a9c9c +2 -0
  206. package/docs/apis/features/node/launcher-app-command-listener.md +0 -145
  207. package/docs/examples/launcher-app-command-listener.md +0 -120
  208. package/docs/tasks/web-container-helper-discovery.md +0 -71
  209. package/docs/todos.md +0 -1
  210. package/scripts/test-command-listener.ts +0 -123
  211. package/src/node/features/launcher-app-command-listener.ts +0 -389
@@ -7,9 +7,13 @@ import {
7
7
  readdirSync,
8
8
  statSync,
9
9
  readFileSync,
10
+ cpSync,
11
+ renameSync,
12
+
13
+ rmSync as nodeRmSync,
10
14
  } from "fs";
11
- import { join, resolve, dirname } from "path";
12
- import { readFile, stat, unlink, mkdir, writeFile, appendFile, readdir } from "fs/promises";
15
+ import { join, resolve, dirname, relative } from "path";
16
+ import { readFile, stat, unlink, mkdir, writeFile, appendFile, readdir, cp, rename, rm as nodeRm } from "fs/promises";
13
17
  import { native as rimraf } from 'rimraf'
14
18
 
15
19
  type WalkOptions = {
@@ -17,8 +21,25 @@ type WalkOptions = {
17
21
  files?: boolean;
18
22
  exclude?: string | string[];
19
23
  include?: string | string[];
24
+ /** When true, returned paths are relative to `baseDir` instead of absolute. */
25
+ relative?: boolean;
20
26
  };
21
27
 
28
+ /**
29
+ * Checks whether a path matches any of the given glob-like patterns.
30
+ * Supports simple wildcards: * matches anything except /, ** matches anything including /.
31
+ */
32
+ function matchesPattern(filePath: string, patterns: string[]): boolean {
33
+ return patterns.some(pattern => {
34
+ const regex = pattern
35
+ .replace(/\./g, '\\.')
36
+ .replace(/\*\*/g, '{{GLOBSTAR}}')
37
+ .replace(/\*/g, '[^/]*')
38
+ .replace(/\{\{GLOBSTAR\}\}/g, '.*')
39
+ return new RegExp(`(^|/)${regex}($|/)`).test(filePath)
40
+ })
41
+ }
42
+
22
43
  /**
23
44
  * The FS feature provides methods for interacting with the file system, relative to the
24
45
  * container's cwd.
@@ -30,7 +51,9 @@ type WalkOptions = {
30
51
  * const fs = container.feature('fs')
31
52
  * const content = fs.readFile('package.json')
32
53
  * const exists = fs.exists('tsconfig.json')
33
- * await fs.ensureFileAsync('output/result.json', '{}')
54
+ * await fs.writeFileAsync('output.txt', 'Hello World')
55
+ * fs.writeFile('sync-output.txt', 'Hello Sync')
56
+ * fs.copy('src', 'backup/src')
34
57
  * ```
35
58
  */
36
59
  export class FS extends Feature {
@@ -39,189 +62,144 @@ export class FS extends Feature {
39
62
  static override optionsSchema = FeatureOptionsSchema
40
63
  static { Feature.register(this, 'fs') }
41
64
 
65
+ // ---------------------------------------------------------------------------
66
+ // Read
67
+ // ---------------------------------------------------------------------------
68
+
42
69
  /**
43
- * Asynchronously reads a file and returns its contents as a Buffer.
70
+ * Synchronously reads a file and returns its contents as a string.
44
71
  *
45
72
  * @param {string} path - The file path relative to the container's working directory
46
- * @returns {Promise<Buffer>} A promise that resolves to the file contents as a Buffer
73
+ * @param {BufferEncoding | null} [encoding='utf-8'] - The encoding to use. Pass null to get a raw Buffer.
74
+ * @returns {string | Buffer} The file contents as a string (default) or Buffer if encoding is null
47
75
  * @throws {Error} Throws an error if the file doesn't exist or cannot be read
48
76
  *
49
77
  * @example
50
78
  * ```typescript
51
- * const fs = container.feature('fs')
52
- * const buffer = await fs.readFileAsync('data.txt')
53
- * console.log(buffer.toString())
79
+ * const content = fs.readFile('README.md')
80
+ * const buffer = fs.readFile('image.png', null)
54
81
  * ```
55
82
  */
56
- async readFileAsync(path: string) {
57
- return await readFile(this.container.paths.resolve(path))
83
+ readFile(path: string, encoding?: BufferEncoding | null): string | Buffer {
84
+ const filePath = this.container.paths.resolve(path);
85
+ if (encoding === null) {
86
+ return readFileSync(filePath)
87
+ }
88
+ return readFileSync(filePath, encoding ?? 'utf-8')
58
89
  }
59
90
 
60
91
  /**
61
- * Asynchronously reads the contents of a directory.
92
+ * Asynchronously reads a file and returns its contents as a string.
62
93
  *
63
- * @param {string} path - The directory path relative to the container's working directory
64
- * @returns {Promise<string[]>} A promise that resolves to an array of file and directory names
65
- * @throws {Error} Throws an error if the directory doesn't exist or cannot be read
94
+ * @param {string} path - The file path relative to the container's working directory
95
+ * @param {BufferEncoding | null} [encoding='utf-8'] - The encoding to use. Pass null to get a raw Buffer.
96
+ * @returns {Promise<string | Buffer>} A promise that resolves to the file contents as a string (default) or Buffer
97
+ * @throws {Error} Throws an error if the file doesn't exist or cannot be read
66
98
  *
67
99
  * @example
68
100
  * ```typescript
69
- * const fs = container.feature('fs')
70
- * const entries = await fs.readdir('src')
71
- * console.log(entries) // ['index.ts', 'utils.ts', 'components']
101
+ * const content = await fs.readFileAsync('data.txt')
102
+ * const buffer = await fs.readFileAsync('image.png', null)
72
103
  * ```
73
104
  */
74
- async readdir(path: string) {
75
- return await readdir(this.container.paths.resolve(path))
105
+ async readFileAsync(path: string, encoding?: BufferEncoding | null): Promise<string | Buffer> {
106
+ const filePath = this.container.paths.resolve(path);
107
+ if (encoding === null) {
108
+ return await readFile(filePath)
109
+ }
110
+ return await readFile(filePath, encoding ?? 'utf-8')
76
111
  }
77
112
 
78
113
  /**
79
- * Recursively walks a directory and returns an array of relative path names for each file and directory.
114
+ * Synchronously reads and parses a JSON file.
115
+ *
116
+ * @param {string} path - The path to the JSON file
117
+ * @returns {any} The parsed JSON content
118
+ * @throws {Error} Throws an error if the file doesn't exist, cannot be read, or contains invalid JSON
80
119
  *
81
- * @param {string} basePath - The base directory path to start walking from
82
- * @param {WalkOptions} options - Options to configure the walk behavior
83
- * @param {boolean} [options.directories=true] - Whether to include directories in results
84
- * @param {boolean} [options.files=true] - Whether to include files in results
85
- * @param {string | string[]} [options.exclude=[]] - Patterns to exclude from results
86
- * @param {string | string[]} [options.include=[]] - Patterns to include in results
87
- * @returns {{ directories: string[], files: string[] }} Object containing arrays of directory and file paths
88
- *
89
120
  * @example
90
121
  * ```typescript
91
- * const result = fs.walk('src', { files: true, directories: false })
92
- * console.log(result.files) // ['src/index.ts', 'src/utils.ts', 'src/components/Button.tsx']
122
+ * const config = fs.readJson('config.json')
123
+ * console.log(config.version)
93
124
  * ```
94
125
  */
95
- walk(basePath: string, options: WalkOptions = {}) {
96
- const {
97
- directories = true,
98
- files = true,
99
- exclude = [],
100
- include = [],
101
- } = options;
102
-
103
- const walk = (baseDir: string) => {
104
- const results = {
105
- directories: [] as string[],
106
- files: [] as string[],
107
- };
108
-
109
- const entries = readdirSync(baseDir, { withFileTypes: true });
110
-
111
- for (const entry of entries) {
112
- const name = entry.name;
113
- const path = join(baseDir, name);
114
- const isDir = entry.isDirectory();
115
-
116
- if (isDir && directories) {
117
- results.directories.push(path);
118
- }
119
-
120
- if (!isDir && files) {
121
- results.files.push(path);
122
- }
123
-
124
- if (isDir) {
125
- const subResults = walk(path);
126
- results.files.push(...subResults.files);
127
- results.directories.push(...subResults.directories);
128
- }
129
- }
130
-
131
- return results;
132
- };
133
-
134
- return walk(this.container.paths.resolve(basePath));
126
+ readJson(path: string) {
127
+ return JSON.parse(this.readFile(path) as string)
135
128
  }
136
129
 
137
130
  /**
138
- * Asynchronously and recursively walks a directory and returns an array of relative path names.
131
+ * Asynchronously reads and parses a JSON file.
132
+ *
133
+ * @param {string} path - The path to the JSON file
134
+ * @returns {Promise<any>} A promise that resolves to the parsed JSON content
135
+ * @throws {Error} Throws an error if the file doesn't exist, cannot be read, or contains invalid JSON
139
136
  *
140
- * @param {string} baseDir - The base directory path to start walking from
141
- * @param {WalkOptions} options - Options to configure the walk behavior
142
- * @param {boolean} [options.directories=true] - Whether to include directories in results
143
- * @param {boolean} [options.files=true] - Whether to include files in results
144
- * @param {string | string[]} [options.exclude=[]] - Patterns to exclude from results
145
- * @param {string | string[]} [options.include=[]] - Patterns to include in results
146
- * @returns {Promise<{ directories: string[], files: string[] }>} Promise resolving to object with directory and file paths
147
- * @throws {Error} Throws an error if the directory cannot be accessed
148
- *
149
137
  * @example
150
138
  * ```typescript
151
- * const result = await fs.walkAsync('src', { exclude: ['node_modules'] })
152
- * console.log(`Found ${result.files.length} files and ${result.directories.length} directories`)
139
+ * const config = await fs.readJsonAsync('config.json')
140
+ * console.log(config.version)
153
141
  * ```
154
142
  */
155
- async walkAsync(baseDir: string, options: WalkOptions = {}) {
156
- const {
157
- directories = true,
158
- files = true,
159
- exclude = [],
160
- include = [],
161
- } = options;
162
-
163
- const walk = async (baseDir: string) => {
164
- const results = {
165
- directories: [] as string[],
166
- files: [] as string[],
167
- };
168
-
169
- const entries = await readdir(baseDir, { withFileTypes: true });
170
-
171
- for (const entry of entries) {
172
- const name = entry.name;
173
- const path = join(baseDir, name);
174
- const isDir = entry.isDirectory();
175
-
176
- if (isDir && directories) {
177
- results.directories.push(path);
178
- }
179
-
180
- if (!isDir && files) {
181
- results.files.push(path);
182
- }
183
-
184
- if (isDir) {
185
- const subResults = await walk(path);
186
- results.files.push(...subResults.files);
187
- results.directories.push(...subResults.directories);
188
- }
189
- }
190
-
191
- return results;
192
- };
143
+ async readJsonAsync(path: string) {
144
+ const content = await this.readFileAsync(path)
145
+ return JSON.parse(content as string)
146
+ }
193
147
 
194
- return walk(this.container.paths.resolve(baseDir));
148
+ /**
149
+ * Synchronously reads the contents of a directory.
150
+ *
151
+ * @param {string} path - The directory path relative to the container's working directory
152
+ * @returns {string[]} An array of file and directory names
153
+ * @throws {Error} Throws an error if the directory doesn't exist or cannot be read
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const entries = fs.readdirSync('src')
158
+ * console.log(entries) // ['index.ts', 'utils.ts', 'components']
159
+ * ```
160
+ */
161
+ readdirSync(path: string) {
162
+ return readdirSync(this.container.paths.resolve(path))
195
163
  }
196
164
 
197
165
  /**
198
- * Asynchronously ensures a file exists with the specified content, creating directories as needed.
166
+ * Asynchronously reads the contents of a directory.
167
+ *
168
+ * @param {string} path - The directory path relative to the container's working directory
169
+ * @returns {Promise<string[]>} A promise that resolves to an array of file and directory names
170
+ * @throws {Error} Throws an error if the directory doesn't exist or cannot be read
199
171
  *
200
- * @param {string} path - The file path where the file should be created
201
- * @param {string} content - The content to write to the file
202
- * @param {boolean} [overwrite=false] - Whether to overwrite the file if it already exists
203
- * @returns {Promise<string>} A promise that resolves to the absolute file path
204
- * @throws {Error} Throws an error if the file cannot be created or written
205
- *
206
172
  * @example
207
173
  * ```typescript
208
- * await fs.ensureFileAsync('config/settings.json', '{}', true)
209
- * // Creates config directory and settings.json file with '{}' content
174
+ * const entries = await fs.readdir('src')
175
+ * console.log(entries) // ['index.ts', 'utils.ts', 'components']
210
176
  * ```
211
177
  */
212
- async ensureFileAsync(path: string, content: string, overwrite = false) {
213
- path = this.container.paths.resolve(path);
178
+ async readdir(path: string) {
179
+ return await readdir(this.container.paths.resolve(path))
180
+ }
214
181
 
215
- if (this.exists(path) && !overwrite) {
216
- return path;
217
- }
182
+ // ---------------------------------------------------------------------------
183
+ // Write
184
+ // ---------------------------------------------------------------------------
218
185
 
219
- const { dir } = this.container.paths.parse(path);
220
- await mkdir(dir, { recursive: true });
221
- await writeFile(path, content);
222
- return path;
186
+ /**
187
+ * Synchronously writes content to a file.
188
+ *
189
+ * @param {string} path - The file path where content should be written
190
+ * @param {Buffer | string} content - The content to write to the file
191
+ * @throws {Error} Throws an error if the file cannot be written
192
+ *
193
+ * @example
194
+ * ```typescript
195
+ * fs.writeFile('output.txt', 'Hello World')
196
+ * fs.writeFile('data.bin', Buffer.from([1, 2, 3, 4]))
197
+ * ```
198
+ */
199
+ writeFile(path: string, content: Buffer | string) {
200
+ writeFileSync(this.container.paths.resolve(path), content)
223
201
  }
224
-
202
+
225
203
  /**
226
204
  * Asynchronously writes content to a file.
227
205
  *
@@ -229,17 +207,52 @@ export class FS extends Feature {
229
207
  * @param {Buffer | string} content - The content to write to the file
230
208
  * @returns {Promise<void>} A promise that resolves when the file is written
231
209
  * @throws {Error} Throws an error if the file cannot be written
232
- *
210
+ *
233
211
  * @example
234
212
  * ```typescript
235
213
  * await fs.writeFileAsync('output.txt', 'Hello World')
236
214
  * await fs.writeFileAsync('data.bin', Buffer.from([1, 2, 3, 4]))
237
215
  * ```
238
216
  */
239
- async writeFileAsync(path:string, content: Buffer | string) {
217
+ async writeFileAsync(path: string, content: Buffer | string) {
240
218
  return writeFile(this.container.paths.resolve(path), content)
241
219
  }
242
220
 
221
+ /**
222
+ * Synchronously writes an object to a file as JSON.
223
+ *
224
+ * @param {string} path - The file path where the JSON should be written
225
+ * @param {any} data - The data to serialize as JSON
226
+ * @param {number} [indent=2] - The number of spaces to use for indentation
227
+ * @throws {Error} Throws an error if the file cannot be written
228
+ *
229
+ * @example
230
+ * ```typescript
231
+ * fs.writeJson('config.json', { version: '1.0.0', debug: false })
232
+ * ```
233
+ */
234
+ writeJson(path: string, data: any, indent: number = 2) {
235
+ this.writeFile(path, JSON.stringify(data, null, indent) + '\n')
236
+ }
237
+
238
+ /**
239
+ * Asynchronously writes an object to a file as JSON.
240
+ *
241
+ * @param {string} path - The file path where the JSON should be written
242
+ * @param {any} data - The data to serialize as JSON
243
+ * @param {number} [indent=2] - The number of spaces to use for indentation
244
+ * @returns {Promise<void>} A promise that resolves when the file is written
245
+ * @throws {Error} Throws an error if the file cannot be written
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * await fs.writeJsonAsync('config.json', { version: '1.0.0', debug: false })
250
+ * ```
251
+ */
252
+ async writeJsonAsync(path: string, data: any, indent: number = 2) {
253
+ return this.writeFileAsync(path, JSON.stringify(data, null, indent) + '\n')
254
+ }
255
+
243
256
  /**
244
257
  * Synchronously appends content to a file.
245
258
  *
@@ -271,27 +284,9 @@ export class FS extends Feature {
271
284
  return appendFile(this.container.paths.resolve(path), content)
272
285
  }
273
286
 
274
- /**
275
- * Synchronously ensures a directory exists, creating parent directories as needed.
276
- *
277
- * @param {string} path - The directory path to create
278
- * @returns {string} The resolved directory path
279
- * @throws {Error} Throws an error if the directory cannot be created
280
- *
281
- * @example
282
- * ```typescript
283
- * fs.ensureFolder('logs/debug')
284
- * // Creates logs and logs/debug directories if they don't exist
285
- * ```
286
- */
287
- ensureFolder(path: string) {
288
- mkdirSync(this.container.paths.resolve(path), { recursive: true });
289
- return this.container.paths.resolve(path);
290
- }
291
-
292
- mkdirp(folder: string) {
293
- return this.ensureFolder(folder)
294
- }
287
+ // ---------------------------------------------------------------------------
288
+ // Ensure (create if missing)
289
+ // ---------------------------------------------------------------------------
295
290
 
296
291
  /**
297
292
  * Synchronously ensures a file exists with the specified content, creating directories as needed.
@@ -301,11 +296,10 @@ export class FS extends Feature {
301
296
  * @param {boolean} [overwrite=false] - Whether to overwrite the file if it already exists
302
297
  * @returns {string} The resolved file path
303
298
  * @throws {Error} Throws an error if the file cannot be created or written
304
- *
299
+ *
305
300
  * @example
306
301
  * ```typescript
307
302
  * fs.ensureFile('logs/app.log', '', false)
308
- * // Creates logs directory and app.log file if they don't exist
309
303
  * ```
310
304
  */
311
305
  ensureFile(path: string, content: string, overwrite = false) {
@@ -322,81 +316,101 @@ export class FS extends Feature {
322
316
  }
323
317
 
324
318
  /**
325
- * Synchronously finds a file by walking up the directory tree from the current working directory.
319
+ * Asynchronously ensures a file exists with the specified content, creating directories as needed.
320
+ *
321
+ * @param {string} path - The file path where the file should be created
322
+ * @param {string} content - The content to write to the file
323
+ * @param {boolean} [overwrite=false] - Whether to overwrite the file if it already exists
324
+ * @returns {Promise<string>} A promise that resolves to the absolute file path
325
+ * @throws {Error} Throws an error if the file cannot be created or written
326
326
  *
327
- * @param {string} fileName - The name of the file to search for
328
- * @param {object} [options={}] - Options for the search
329
- * @param {string} [options.cwd] - The directory to start searching from (defaults to container.cwd)
330
- * @returns {string | null} The absolute path to the found file, or null if not found
331
- *
332
327
  * @example
333
328
  * ```typescript
334
- * const packageJson = fs.findUp('package.json')
335
- * if (packageJson) {
336
- * console.log(`Found package.json at: ${packageJson}`)
337
- * }
329
+ * await fs.ensureFileAsync('config/settings.json', '{}', true)
338
330
  * ```
339
331
  */
340
- findUp(fileName: string, options: { cwd?: string } = {}): string | null {
341
- const { cwd = this.container.cwd } = options;
342
- let startAt = cwd;
343
-
344
- // walk up the tree until we find the fileName exists
345
- if (this.exists(join(startAt, fileName))) {
346
- return resolve(startAt, fileName);
347
- }
332
+ async ensureFileAsync(path: string, content: string, overwrite = false) {
333
+ path = this.container.paths.resolve(path);
348
334
 
349
- // walk up the tree until we find the fileName exists
350
- while (startAt !== dirname(startAt)) {
351
- startAt = dirname(startAt);
352
- if (this.exists(join(startAt, fileName))) {
353
- return resolve(startAt, fileName);
354
- }
335
+ if (this.exists(path) && !overwrite) {
336
+ return path;
355
337
  }
356
338
 
357
- return null;
339
+ const { dir } = this.container.paths.parse(path);
340
+ await mkdir(dir, { recursive: true });
341
+ await writeFile(path, content);
342
+ return path;
358
343
  }
359
344
 
360
345
  /**
361
- * Asynchronously checks if a file or directory exists.
346
+ * Synchronously ensures a directory exists, creating parent directories as needed.
347
+ *
348
+ * @param {string} path - The directory path to create
349
+ * @returns {string} The resolved directory path
350
+ * @throws {Error} Throws an error if the directory cannot be created
362
351
  *
363
- * @param {string} path - The path to check for existence
364
- * @returns {Promise<boolean>} A promise that resolves to true if the path exists, false otherwise
365
- *
366
352
  * @example
367
353
  * ```typescript
368
- * if (await fs.existsAsync('config.json')) {
369
- * console.log('Config file exists!')
370
- * }
354
+ * fs.ensureFolder('logs/debug')
371
355
  * ```
372
356
  */
373
- async existsAsync(path: string) {
374
- const { container } = this;
375
- const filePath = container.paths.resolve(path);
376
- const exists = await stat(filePath)
377
- .then(() => true)
378
- .catch((e) => false);
379
-
380
- return exists
357
+ ensureFolder(path: string) {
358
+ mkdirSync(this.container.paths.resolve(path), { recursive: true });
359
+ return this.container.paths.resolve(path);
381
360
  }
382
361
 
383
362
  /**
384
- * Synchronously checks if a file or directory exists.
363
+ * Asynchronously ensures a directory exists, creating parent directories as needed.
385
364
  *
386
- * @param {string} path - The path to check for existence
387
- * @returns {boolean} True if the path exists, false otherwise
388
- *
389
- * @example
390
- * ```typescript
365
+ * @param {string} path - The directory path to create
366
+ * @returns {Promise<string>} A promise that resolves to the resolved directory path
367
+ * @throws {Error} Throws an error if the directory cannot be created
368
+ *
369
+ * @example
370
+ * ```typescript
371
+ * await fs.ensureFolderAsync('logs/debug')
372
+ * ```
373
+ */
374
+ async ensureFolderAsync(path: string) {
375
+ const resolved = this.container.paths.resolve(path);
376
+ await mkdir(resolved, { recursive: true });
377
+ return resolved;
378
+ }
379
+
380
+ /**
381
+ * Alias for ensureFolder. Synchronously creates a directory and all parent directories.
382
+ *
383
+ * @param {string} folder - The directory path to create
384
+ * @returns {string} The resolved directory path
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * fs.mkdirp('deep/nested/path')
389
+ * ```
390
+ */
391
+ mkdirp(folder: string) {
392
+ return this.ensureFolder(folder)
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Existence & stat
397
+ // ---------------------------------------------------------------------------
398
+
399
+ /**
400
+ * Synchronously checks if a file or directory exists.
401
+ *
402
+ * @param {string} path - The path to check for existence
403
+ * @returns {boolean} True if the path exists, false otherwise
404
+ *
405
+ * @example
406
+ * ```typescript
391
407
  * if (fs.exists('config.json')) {
392
408
  * console.log('Config file exists!')
393
409
  * }
394
410
  * ```
395
411
  */
396
412
  exists(path: string): boolean {
397
- const { container } = this;
398
- const filePath = container.paths.resolve(path);
399
-
413
+ const filePath = this.container.paths.resolve(path);
400
414
  try {
401
415
  statSync(filePath);
402
416
  return true;
@@ -405,62 +419,199 @@ export class FS extends Feature {
405
419
  }
406
420
  }
407
421
 
422
+ /**
423
+ * Alias for exists. Synchronously checks if a file or directory exists.
424
+ *
425
+ * @param {string} path - The path to check for existence
426
+ * @returns {boolean} True if the path exists, false otherwise
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * if (fs.existsSync('config.json')) {
431
+ * console.log('Config file exists!')
432
+ * }
433
+ * ```
434
+ */
408
435
  existsSync(path: string): boolean {
409
- return this.exists(path)
436
+ return this.exists(path)
410
437
  }
411
438
 
412
439
  /**
413
- * Asynchronously removes a file.
440
+ * Asynchronously checks if a file or directory exists.
441
+ *
442
+ * @param {string} path - The path to check for existence
443
+ * @returns {Promise<boolean>} A promise that resolves to true if the path exists, false otherwise
444
+ *
445
+ * @example
446
+ * ```typescript
447
+ * if (await fs.existsAsync('config.json')) {
448
+ * console.log('Config file exists!')
449
+ * }
450
+ * ```
451
+ */
452
+ async existsAsync(path: string) {
453
+ const filePath = this.container.paths.resolve(path);
454
+ return stat(filePath).then(() => true).catch(() => false)
455
+ }
456
+
457
+ /**
458
+ * Synchronously returns the stat object for a file or directory.
459
+ *
460
+ * @param {string} path - The path to stat
461
+ * @returns {import('fs').Stats} The Stats object with size, timestamps, and type checks
462
+ * @throws {Error} Throws an error if the path doesn't exist
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * const info = fs.stat('package.json')
467
+ * console.log(info.size, info.mtime)
468
+ * ```
469
+ */
470
+ stat(path: string) {
471
+ return statSync(this.container.paths.resolve(path))
472
+ }
473
+
474
+ /**
475
+ * Asynchronously returns the stat object for a file or directory.
476
+ *
477
+ * @param {string} path - The path to stat
478
+ * @returns {Promise<import('fs').Stats>} A promise that resolves to the Stats object
479
+ * @throws {Error} Throws an error if the path doesn't exist
480
+ *
481
+ * @example
482
+ * ```typescript
483
+ * const info = await fs.statAsync('package.json')
484
+ * console.log(info.size, info.mtime)
485
+ * ```
486
+ */
487
+ async statAsync(path: string) {
488
+ return stat(this.container.paths.resolve(path))
489
+ }
490
+
491
+ /**
492
+ * Synchronously checks if a path is a file.
493
+ *
494
+ * @param {string} path - The path to check
495
+ * @returns {boolean} True if the path is a file, false otherwise
496
+ *
497
+ * @example
498
+ * ```typescript
499
+ * if (fs.isFile('package.json')) {
500
+ * console.log('It is a file')
501
+ * }
502
+ * ```
503
+ */
504
+ isFile(path: string): boolean {
505
+ try {
506
+ return statSync(this.container.paths.resolve(path)).isFile()
507
+ } catch {
508
+ return false
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Asynchronously checks if a path is a file.
514
+ *
515
+ * @param {string} path - The path to check
516
+ * @returns {Promise<boolean>} A promise that resolves to true if the path is a file
517
+ *
518
+ * @example
519
+ * ```typescript
520
+ * if (await fs.isFileAsync('package.json')) {
521
+ * console.log('It is a file')
522
+ * }
523
+ * ```
524
+ */
525
+ async isFileAsync(path: string): Promise<boolean> {
526
+ return stat(this.container.paths.resolve(path)).then(s => s.isFile()).catch(() => false)
527
+ }
528
+
529
+ /**
530
+ * Synchronously checks if a path is a directory.
531
+ *
532
+ * @param {string} path - The path to check
533
+ * @returns {boolean} True if the path is a directory, false otherwise
534
+ *
535
+ * @example
536
+ * ```typescript
537
+ * if (fs.isDirectory('src')) {
538
+ * console.log('It is a directory')
539
+ * }
540
+ * ```
541
+ */
542
+ isDirectory(path: string): boolean {
543
+ try {
544
+ return statSync(this.container.paths.resolve(path)).isDirectory()
545
+ } catch {
546
+ return false
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Asynchronously checks if a path is a directory.
552
+ *
553
+ * @param {string} path - The path to check
554
+ * @returns {Promise<boolean>} A promise that resolves to true if the path is a directory
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * if (await fs.isDirectoryAsync('src')) {
559
+ * console.log('It is a directory')
560
+ * }
561
+ * ```
562
+ */
563
+ async isDirectoryAsync(path: string): Promise<boolean> {
564
+ return stat(this.container.paths.resolve(path)).then(s => s.isDirectory()).catch(() => false)
565
+ }
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // Delete
569
+ // ---------------------------------------------------------------------------
570
+
571
+ /**
572
+ * Synchronously removes a file.
414
573
  *
415
574
  * @param {string} path - The path of the file to remove
416
- * @returns {Promise<void>} A promise that resolves when the file is removed
417
575
  * @throws {Error} Throws an error if the file cannot be removed or doesn't exist
418
- *
576
+ *
419
577
  * @example
420
578
  * ```typescript
421
- * await fs.rm('temp/cache.tmp')
579
+ * fs.rmSync('temp/cache.tmp')
422
580
  * ```
423
581
  */
424
- async rm(path: string) {
425
- return await unlink(this.container.paths.resolve(path));
582
+ rmSync(path: string) {
583
+ nodeRmSync(this.container.paths.resolve(path), { force: true })
426
584
  }
427
585
 
428
586
  /**
429
- * Synchronously reads and parses a JSON file.
587
+ * Asynchronously removes a file.
588
+ *
589
+ * @param {string} path - The path of the file to remove
590
+ * @returns {Promise<void>} A promise that resolves when the file is removed
591
+ * @throws {Error} Throws an error if the file cannot be removed or doesn't exist
430
592
  *
431
- * @param {string} path - The path to the JSON file
432
- * @returns {any} The parsed JSON content
433
- * @throws {Error} Throws an error if the file doesn't exist, cannot be read, or contains invalid JSON
434
- *
435
593
  * @example
436
594
  * ```typescript
437
- * const config = fs.readJson('config.json')
438
- * console.log(config.version)
595
+ * await fs.rm('temp/cache.tmp')
439
596
  * ```
440
597
  */
441
- readJson(path: string) {
442
- const { container } = this;
443
- const filePath = container.paths.resolve(path);
444
- return JSON.parse(readFileSync(filePath).toString());
598
+ async rm(path: string) {
599
+ return await unlink(this.container.paths.resolve(path));
445
600
  }
446
601
 
447
602
  /**
448
- * Synchronously reads a file and returns its contents as a string.
603
+ * Synchronously removes a directory and all its contents.
604
+ *
605
+ * @param {string} dirPath - The path of the directory to remove
606
+ * @throws {Error} Throws an error if the directory cannot be removed
449
607
  *
450
- * @param {string} path - The path to the file
451
- * @returns {string} The file contents as a string
452
- * @throws {Error} Throws an error if the file doesn't exist or cannot be read
453
- *
454
608
  * @example
455
609
  * ```typescript
456
- * const content = fs.readFile('README.md')
457
- * console.log(content)
610
+ * fs.rmdirSync('temp/cache')
458
611
  * ```
459
612
  */
460
- readFile(path: string) {
461
- const { container } = this;
462
- const filePath = container.paths.resolve(path);
463
- return readFileSync(filePath).toString();
613
+ rmdirSync(dirPath: string) {
614
+ nodeRmSync(this.container.paths.resolve(dirPath), { recursive: true, force: true })
464
615
  }
465
616
 
466
617
  /**
@@ -469,17 +620,323 @@ export class FS extends Feature {
469
620
  * @param {string} dirPath - The path of the directory to remove
470
621
  * @returns {Promise<void>} A promise that resolves when the directory is removed
471
622
  * @throws {Error} Throws an error if the directory cannot be removed
472
- *
623
+ *
473
624
  * @example
474
625
  * ```typescript
475
626
  * await fs.rmdir('temp/cache')
476
- * // Removes the cache directory and all its contents
477
627
  * ```
478
628
  */
479
629
  async rmdir(dirPath: string) {
480
630
  await rimraf(this.container.paths.resolve(dirPath));
481
631
  }
482
632
 
633
+ // ---------------------------------------------------------------------------
634
+ // Copy & Move
635
+ // ---------------------------------------------------------------------------
636
+
637
+ /**
638
+ * Synchronously copies a file or directory. Auto-detects whether the source is a file or directory
639
+ * and handles each appropriately (recursive for directories).
640
+ *
641
+ * @param {string} src - The source path to copy from
642
+ * @param {string} dest - The destination path to copy to
643
+ * @param {object} [options={}] - Copy options
644
+ * @param {boolean} [options.overwrite=true] - Whether to overwrite existing files at the destination
645
+ * @throws {Error} Throws an error if the source doesn't exist or the copy fails
646
+ *
647
+ * @example
648
+ * ```typescript
649
+ * fs.copy('src/config.json', 'backup/config.json')
650
+ * fs.copy('src', 'backup/src')
651
+ * ```
652
+ */
653
+ copy(src: string, dest: string, options: { overwrite?: boolean } = {}) {
654
+ const { overwrite = true } = options
655
+ const resolvedSrc = this.container.paths.resolve(src)
656
+ const resolvedDest = this.container.paths.resolve(dest)
657
+ cpSync(resolvedSrc, resolvedDest, { recursive: true, force: overwrite })
658
+ }
659
+
660
+ /**
661
+ * Asynchronously copies a file or directory. Auto-detects whether the source is a file or directory
662
+ * and handles each appropriately (recursive for directories).
663
+ *
664
+ * @param {string} src - The source path to copy from
665
+ * @param {string} dest - The destination path to copy to
666
+ * @param {object} [options={}] - Copy options
667
+ * @param {boolean} [options.overwrite=true] - Whether to overwrite existing files at the destination
668
+ * @returns {Promise<void>} A promise that resolves when the copy is complete
669
+ * @throws {Error} Throws an error if the source doesn't exist or the copy fails
670
+ *
671
+ * @example
672
+ * ```typescript
673
+ * await fs.copyAsync('src/config.json', 'backup/config.json')
674
+ * await fs.copyAsync('src', 'backup/src')
675
+ * ```
676
+ */
677
+ async copyAsync(src: string, dest: string, options: { overwrite?: boolean } = {}) {
678
+ const { overwrite = true } = options
679
+ const resolvedSrc = this.container.paths.resolve(src)
680
+ const resolvedDest = this.container.paths.resolve(dest)
681
+ await cp(resolvedSrc, resolvedDest, { recursive: true, force: overwrite })
682
+ }
683
+
684
+ /**
685
+ * Synchronously moves (renames) a file or directory. Falls back to copy + delete for cross-device moves.
686
+ *
687
+ * @param {string} src - The source path to move from
688
+ * @param {string} dest - The destination path to move to
689
+ * @throws {Error} Throws an error if the source doesn't exist or the move fails
690
+ *
691
+ * @example
692
+ * ```typescript
693
+ * fs.move('temp/draft.txt', 'final/document.txt')
694
+ * fs.move('old-dir', 'new-dir')
695
+ * ```
696
+ */
697
+ move(src: string, dest: string) {
698
+ const resolvedSrc = this.container.paths.resolve(src)
699
+ const resolvedDest = this.container.paths.resolve(dest)
700
+ const destDir = dirname(resolvedDest)
701
+ mkdirSync(destDir, { recursive: true })
702
+ try {
703
+ renameSync(resolvedSrc, resolvedDest)
704
+ } catch (err: any) {
705
+ if (err.code === 'EXDEV') {
706
+ cpSync(resolvedSrc, resolvedDest, { recursive: true, force: true })
707
+ nodeRmSync(resolvedSrc, { recursive: true, force: true })
708
+ } else {
709
+ throw err
710
+ }
711
+ }
712
+ }
713
+
714
+ /**
715
+ * Asynchronously moves (renames) a file or directory. Falls back to copy + delete for cross-device moves.
716
+ *
717
+ * @param {string} src - The source path to move from
718
+ * @param {string} dest - The destination path to move to
719
+ * @returns {Promise<void>} A promise that resolves when the move is complete
720
+ * @throws {Error} Throws an error if the source doesn't exist or the move fails
721
+ *
722
+ * @example
723
+ * ```typescript
724
+ * await fs.moveAsync('temp/draft.txt', 'final/document.txt')
725
+ * await fs.moveAsync('old-dir', 'new-dir')
726
+ * ```
727
+ */
728
+ async moveAsync(src: string, dest: string) {
729
+ const resolvedSrc = this.container.paths.resolve(src)
730
+ const resolvedDest = this.container.paths.resolve(dest)
731
+ const destDir = dirname(resolvedDest)
732
+ await mkdir(destDir, { recursive: true })
733
+ try {
734
+ await rename(resolvedSrc, resolvedDest)
735
+ } catch (err: any) {
736
+ if (err.code === 'EXDEV') {
737
+ await cp(resolvedSrc, resolvedDest, { recursive: true, force: true })
738
+ await nodeRm(resolvedSrc, { recursive: true, force: true })
739
+ } else {
740
+ throw err
741
+ }
742
+ }
743
+ }
744
+
745
+ // ---------------------------------------------------------------------------
746
+ // Walk
747
+ // ---------------------------------------------------------------------------
748
+
749
+ /**
750
+ * Recursively walks a directory and returns arrays of file and directory paths.
751
+ * By default paths are absolute. Pass `relative: true` to get paths relative to `basePath`.
752
+ * Supports filtering with exclude and include glob patterns.
753
+ *
754
+ * @param {string} basePath - The base directory path to start walking from
755
+ * @param {WalkOptions} options - Options to configure the walk behavior
756
+ * @param {boolean} [options.directories=true] - Whether to include directories in results
757
+ * @param {boolean} [options.files=true] - Whether to include files in results
758
+ * @param {string | string[]} [options.exclude=[]] - Glob patterns to exclude (e.g. 'node_modules', '*.log')
759
+ * @param {string | string[]} [options.include=[]] - Glob patterns to include (only matching paths are returned)
760
+ * @param {boolean} [options.relative=false] - When true, returned paths are relative to basePath
761
+ * @returns {{ directories: string[], files: string[] }} Object containing arrays of directory and file paths
762
+ *
763
+ * @example
764
+ * ```typescript
765
+ * const result = fs.walk('src', { files: true, directories: false })
766
+ * const filtered = fs.walk('.', { exclude: ['node_modules', '.git'], include: ['*.ts'] })
767
+ * const relative = fs.walk('inbox', { relative: true }) // => { files: ['contact-1.json', ...] }
768
+ * ```
769
+ */
770
+ walk(basePath: string, options: WalkOptions = {}) {
771
+ const {
772
+ directories = true,
773
+ files = true,
774
+ exclude = [],
775
+ include = [],
776
+ relative: useRelative = false,
777
+ } = options;
778
+
779
+ const excludePatterns = Array.isArray(exclude) ? exclude : [exclude]
780
+ const includePatterns = Array.isArray(include) ? include : [include]
781
+ const resolvedBase = this.container.paths.resolve(basePath)
782
+
783
+ const walk = (baseDir: string) => {
784
+ const results = {
785
+ directories: [] as string[],
786
+ files: [] as string[],
787
+ };
788
+
789
+ const entries = readdirSync(baseDir, { withFileTypes: true });
790
+
791
+ for (const entry of entries) {
792
+ const name = entry.name;
793
+ const fullPath = join(baseDir, name);
794
+ const relativePath = relative(resolvedBase, fullPath)
795
+ const outputPath = useRelative ? relativePath : fullPath;
796
+ const isDir = entry.isDirectory();
797
+
798
+ if (excludePatterns.length && matchesPattern(relativePath, excludePatterns)) {
799
+ continue
800
+ }
801
+
802
+ const passes = !includePatterns.length || matchesPattern(relativePath, includePatterns)
803
+
804
+ if (isDir && directories && passes) {
805
+ results.directories.push(outputPath);
806
+ }
807
+
808
+ if (!isDir && files && passes) {
809
+ results.files.push(outputPath);
810
+ }
811
+
812
+ if (isDir) {
813
+ const subResults = walk(fullPath);
814
+ results.files.push(...subResults.files);
815
+ results.directories.push(...subResults.directories);
816
+ }
817
+ }
818
+
819
+ return results;
820
+ };
821
+
822
+ return walk(resolvedBase);
823
+ }
824
+
825
+ /**
826
+ * Asynchronously and recursively walks a directory and returns arrays of file and directory paths.
827
+ * By default paths are absolute. Pass `relative: true` to get paths relative to `baseDir`.
828
+ * Supports filtering with exclude and include glob patterns.
829
+ *
830
+ * @param {string} baseDir - The base directory path to start walking from
831
+ * @param {WalkOptions} options - Options to configure the walk behavior
832
+ * @param {boolean} [options.directories=true] - Whether to include directories in results
833
+ * @param {boolean} [options.files=true] - Whether to include files in results
834
+ * @param {string | string[]} [options.exclude=[]] - Glob patterns to exclude (e.g. 'node_modules', '.git')
835
+ * @param {string | string[]} [options.include=[]] - Glob patterns to include (only matching paths are returned)
836
+ * @param {boolean} [options.relative=false] - When true, returned paths are relative to baseDir
837
+ * @returns {Promise<{ directories: string[], files: string[] }>} Promise resolving to object with directory and file paths
838
+ * @throws {Error} Throws an error if the directory cannot be accessed
839
+ *
840
+ * @example
841
+ * ```typescript
842
+ * const result = await fs.walkAsync('src', { exclude: ['node_modules'] })
843
+ * const files = await fs.walkAsync('inbox', { relative: true })
844
+ * // files.files => ['contact-1.json', 'subfolder/file.txt', ...]
845
+ * ```
846
+ */
847
+ async walkAsync(baseDir: string, options: WalkOptions = {}) {
848
+ const {
849
+ directories = true,
850
+ files = true,
851
+ exclude = [],
852
+ include = [],
853
+ relative: useRelative = false,
854
+ } = options;
855
+
856
+ const excludePatterns = Array.isArray(exclude) ? exclude : [exclude]
857
+ const includePatterns = Array.isArray(include) ? include : [include]
858
+ const resolvedBase = this.container.paths.resolve(baseDir)
859
+
860
+ const walk = async (currentDir: string) => {
861
+ const results = {
862
+ directories: [] as string[],
863
+ files: [] as string[],
864
+ };
865
+
866
+ const entries = await readdir(currentDir, { withFileTypes: true });
867
+
868
+ for (const entry of entries) {
869
+ const name = entry.name;
870
+ const fullPath = join(currentDir, name);
871
+ const relativePath = relative(resolvedBase, fullPath)
872
+ const outputPath = useRelative ? relativePath : fullPath;
873
+ const isDir = entry.isDirectory();
874
+
875
+ if (excludePatterns.length && matchesPattern(relativePath, excludePatterns)) {
876
+ continue
877
+ }
878
+
879
+ const passes = !includePatterns.length || matchesPattern(relativePath, includePatterns)
880
+
881
+ if (isDir && directories && passes) {
882
+ results.directories.push(outputPath);
883
+ }
884
+
885
+ if (!isDir && files && passes) {
886
+ results.files.push(outputPath);
887
+ }
888
+
889
+ if (isDir) {
890
+ const subResults = await walk(fullPath);
891
+ results.files.push(...subResults.files);
892
+ results.directories.push(...subResults.directories);
893
+ }
894
+ }
895
+
896
+ return results;
897
+ };
898
+
899
+ return walk(resolvedBase);
900
+ }
901
+
902
+ // ---------------------------------------------------------------------------
903
+ // Find Up
904
+ // ---------------------------------------------------------------------------
905
+
906
+ /**
907
+ * Synchronously finds a file by walking up the directory tree from the current working directory.
908
+ *
909
+ * @param {string} fileName - The name of the file to search for
910
+ * @param {object} [options={}] - Options for the search
911
+ * @param {string} [options.cwd] - The directory to start searching from (defaults to container.cwd)
912
+ * @returns {string | null} The absolute path to the found file, or null if not found
913
+ *
914
+ * @example
915
+ * ```typescript
916
+ * const packageJson = fs.findUp('package.json')
917
+ * if (packageJson) {
918
+ * console.log(`Found package.json at: ${packageJson}`)
919
+ * }
920
+ * ```
921
+ */
922
+ findUp(fileName: string, options: { cwd?: string } = {}): string | null {
923
+ const { cwd = this.container.cwd } = options;
924
+ let startAt = cwd;
925
+
926
+ if (this.exists(join(startAt, fileName))) {
927
+ return resolve(startAt, fileName);
928
+ }
929
+
930
+ while (startAt !== dirname(startAt)) {
931
+ startAt = dirname(startAt);
932
+ if (this.exists(join(startAt, fileName))) {
933
+ return resolve(startAt, fileName);
934
+ }
935
+ }
936
+
937
+ return null;
938
+ }
939
+
483
940
  /**
484
941
  * Asynchronously finds a file by walking up the directory tree.
485
942
  *
@@ -488,8 +945,7 @@ export class FS extends Feature {
488
945
  * @param {string} [options.cwd] - The directory to start searching from (defaults to container.cwd)
489
946
  * @param {boolean} [options.multiple=false] - Whether to find multiple instances of the file
490
947
  * @returns {Promise<string | string[] | null>} The path(s) to the found file(s), or null if not found
491
- * @throws {Error} Throws an error if the search encounters filesystem issues
492
- *
948
+ *
493
949
  * @example
494
950
  * ```typescript
495
951
  * const packageJson = await fs.findUpAsync('package.json')
@@ -540,4 +996,4 @@ export class FS extends Feature {
540
996
  }
541
997
  }
542
998
 
543
- export default FS
999
+ export default FS