@soederpop/luca 0.0.28 → 0.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/try-all-challenges.ts +1 -1
- package/docs/TABLE-OF-CONTENTS.md +0 -3
- package/docs/examples/structured-output-with-assistants.md +144 -0
- package/docs/tutorials/20-browser-esm.md +234 -0
- package/package.json +1 -1
- package/src/agi/container.server.ts +4 -0
- package/src/agi/features/assistant.ts +132 -2
- package/src/agi/features/browser-use.ts +623 -0
- package/src/agi/features/conversation.ts +135 -45
- package/src/agi/lib/interceptor-chain.ts +79 -0
- package/src/bootstrap/generated.ts +381 -308
- package/src/cli/build-info.ts +2 -2
- package/src/clients/rest.ts +7 -7
- package/src/commands/chat.ts +22 -0
- package/src/commands/describe.ts +67 -2
- package/src/commands/prompt.ts +23 -3
- package/src/container.ts +411 -113
- package/src/helper.ts +189 -5
- package/src/introspection/generated.agi.ts +17664 -11568
- package/src/introspection/generated.node.ts +4891 -1860
- package/src/introspection/generated.web.ts +379 -291
- package/src/introspection/index.ts +7 -0
- package/src/introspection/scan.ts +224 -7
- package/src/node/container.ts +31 -10
- package/src/node/features/content-db.ts +7 -7
- package/src/node/features/disk-cache.ts +11 -11
- package/src/node/features/esbuild.ts +3 -3
- package/src/node/features/file-manager.ts +37 -16
- package/src/node/features/fs.ts +64 -25
- package/src/node/features/git.ts +10 -10
- package/src/node/features/helpers.ts +25 -18
- package/src/node/features/ink.ts +13 -13
- package/src/node/features/ipc-socket.ts +8 -8
- package/src/node/features/networking.ts +3 -3
- package/src/node/features/os.ts +7 -7
- package/src/node/features/package-finder.ts +15 -15
- package/src/node/features/proc.ts +1 -1
- package/src/node/features/ui.ts +13 -13
- package/src/node/features/vm.ts +4 -4
- package/src/scaffolds/generated.ts +1 -1
- package/src/servers/express.ts +6 -6
- package/src/servers/mcp.ts +4 -4
- package/src/servers/socket.ts +6 -6
- package/test/interceptor-chain.test.ts +61 -0
- package/docs/apis/features/node/window-manager.md +0 -445
- package/docs/examples/window-manager-layouts.md +0 -180
- package/docs/examples/window-manager.md +0 -125
- package/docs/window-manager-fix.md +0 -249
- package/scripts/test-window-manager-lifecycle.ts +0 -86
- package/scripts/test-window-manager.ts +0 -43
- package/src/node/features/window-manager.ts +0 -1603
|
@@ -3,7 +3,7 @@ import { FeatureEventsSchema, FeatureStateSchema, FeatureOptionsSchema } from '.
|
|
|
3
3
|
import { State } from "../../state.js";
|
|
4
4
|
import { Feature } from "../feature.js";
|
|
5
5
|
import { parse, relative, join as pathJoin } from "path";
|
|
6
|
-
import { statSync, readFileSync, existsSync } from "fs";
|
|
6
|
+
import { statSync, readFileSync, existsSync, readdirSync, lstatSync } from "fs";
|
|
7
7
|
import micromatch from "micromatch";
|
|
8
8
|
import { castArray } from "lodash-es";
|
|
9
9
|
import chokidar from "chokidar";
|
|
@@ -78,12 +78,12 @@ export class FileManager<
|
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
/** Returns an array of all relative file paths indexed by the file manager. */
|
|
81
|
-
get fileIds() {
|
|
81
|
+
get fileIds(): string[] {
|
|
82
82
|
return Array.from(this.files.keys());
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
/** Returns an array of all file metadata objects indexed by the file manager. */
|
|
86
|
-
get fileObjects() {
|
|
86
|
+
get fileObjects(): File[] {
|
|
87
87
|
return Array.from(this.files.values());
|
|
88
88
|
}
|
|
89
89
|
|
|
@@ -92,7 +92,7 @@ export class FileManager<
|
|
|
92
92
|
* @param {string | string[]} patterns - The patterns to match against the file IDs
|
|
93
93
|
* @returns {string[]} The file IDs that match the patterns
|
|
94
94
|
*/
|
|
95
|
-
match(patterns: string | string[]) {
|
|
95
|
+
match(patterns: string | string[]): string[] {
|
|
96
96
|
return micromatch(this.files.keys(), patterns);
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -102,7 +102,7 @@ export class FileManager<
|
|
|
102
102
|
* @param {string | string[]} patterns - The patterns to match against the file IDs
|
|
103
103
|
* @returns {File[]} The file objects that match the patterns
|
|
104
104
|
*/
|
|
105
|
-
matchFiles(patterns: string | string[]) {
|
|
105
|
+
matchFiles(patterns: string | string[]): (File | undefined)[] {
|
|
106
106
|
const fileIds = this.match(Array.isArray(patterns) ? patterns : [patterns]);
|
|
107
107
|
return fileIds.map((fileId) => this.files.get(fileId));
|
|
108
108
|
}
|
|
@@ -110,7 +110,7 @@ export class FileManager<
|
|
|
110
110
|
/**
|
|
111
111
|
* Returns the directory IDs for all of the files in the project.
|
|
112
112
|
*/
|
|
113
|
-
get directoryIds() {
|
|
113
|
+
get directoryIds(): string[] {
|
|
114
114
|
return Array.from(
|
|
115
115
|
new Set(
|
|
116
116
|
this.files
|
|
@@ -122,7 +122,7 @@ export class FileManager<
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/** Returns an array of unique file extensions found across all indexed files. */
|
|
125
|
-
get uniqueExtensions() {
|
|
125
|
+
get uniqueExtensions(): string[] {
|
|
126
126
|
return Array.from(
|
|
127
127
|
new Set(
|
|
128
128
|
this.files.values().map((file) => file.extension)
|
|
@@ -131,17 +131,17 @@ export class FileManager<
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
/** Whether the file manager has completed its initial scan. */
|
|
134
|
-
get isStarted() {
|
|
134
|
+
get isStarted(): boolean {
|
|
135
135
|
return !!this.state.get("started");
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/** Whether the file manager is currently performing its initial scan. */
|
|
139
|
-
get isStarting() {
|
|
139
|
+
get isStarting(): boolean {
|
|
140
140
|
return !!this.state.get("starting");
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
/** Whether the file watcher is actively monitoring for changes. */
|
|
144
|
-
get isWatching() {
|
|
144
|
+
get isWatching(): boolean {
|
|
145
145
|
return !!this.state.get("watching");
|
|
146
146
|
}
|
|
147
147
|
|
|
@@ -156,7 +156,7 @@ export class FileManager<
|
|
|
156
156
|
* @param {string | string[]} [options.exclude] - The patterns to exclude from the scan
|
|
157
157
|
* @returns {Promise<FileManager>} The file manager instance
|
|
158
158
|
*/
|
|
159
|
-
async start(options: { exclude?: string | string[] } = {}) {
|
|
159
|
+
async start(options: { exclude?: string | string[] } = {}): Promise<this> {
|
|
160
160
|
if (this.isStarted) {
|
|
161
161
|
return this;
|
|
162
162
|
}
|
|
@@ -302,7 +302,7 @@ export class FileManager<
|
|
|
302
302
|
* @param {string | string[]} [options.exclude] - The patterns to exclude from the scan
|
|
303
303
|
* @returns {Promise<FileManager>} The file manager instance
|
|
304
304
|
*/
|
|
305
|
-
async scanFiles(options: { exclude?: string | string[] } = {}) {
|
|
305
|
+
async scanFiles(options: { exclude?: string | string[] } = {}): Promise<this> {
|
|
306
306
|
const { cwd, git, fs } = this.container;
|
|
307
307
|
|
|
308
308
|
const fileIds: string[] = [];
|
|
@@ -339,7 +339,28 @@ export class FileManager<
|
|
|
339
339
|
}
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
|
+
|
|
343
|
+
// git ls-files doesn't traverse symlinked directories — walk them via fs
|
|
344
|
+
// to pick up their contents. fs.walk now follows symlinks natively.
|
|
345
|
+
try {
|
|
346
|
+
const topEntries = readdirSync(cwd, { withFileTypes: true });
|
|
347
|
+
for (const entry of topEntries) {
|
|
348
|
+
if (entry.isSymbolicLink()) {
|
|
349
|
+
const fullPath = pathJoin(cwd, entry.name);
|
|
350
|
+
try {
|
|
351
|
+
const target = statSync(fullPath);
|
|
352
|
+
if (target.isDirectory()) {
|
|
353
|
+
const walked = await fs.walkAsync(fullPath, { exclude });
|
|
354
|
+
for (const absFile of walked.files) {
|
|
355
|
+
fileIds.push(relative(cwd, absFile));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
} catch {}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch {}
|
|
342
362
|
} else {
|
|
363
|
+
// fs.walkAsync follows symlinks, so non-git repos get symlink support for free
|
|
343
364
|
await fs.walkAsync(cwd).then(({ files } : { files: string[] }) => fileIds.push(...files));
|
|
344
365
|
}
|
|
345
366
|
|
|
@@ -395,7 +416,7 @@ export class FileManager<
|
|
|
395
416
|
* @param {string | string[]} [options.exclude] - The patterns to exclude from the watch
|
|
396
417
|
* @returns {Promise<void>}
|
|
397
418
|
*/
|
|
398
|
-
async watch(options: { paths?: string | string[]; exclude?: string | string[] } = {}) {
|
|
419
|
+
async watch(options: { paths?: string | string[]; exclude?: string | string[] } = {}): Promise<void> {
|
|
399
420
|
const pathsToWatch = castArray(options.paths || this.directoryIds.map(id => this.container.paths.resolve(id)))
|
|
400
421
|
.map(p => this.container.paths.resolve(p));
|
|
401
422
|
|
|
@@ -466,7 +487,7 @@ export class FileManager<
|
|
|
466
487
|
this.watcher = watcher;
|
|
467
488
|
}
|
|
468
489
|
|
|
469
|
-
async stopWatching() {
|
|
490
|
+
async stopWatching(): Promise<void> {
|
|
470
491
|
if (!this.isWatching) {
|
|
471
492
|
return;
|
|
472
493
|
}
|
|
@@ -479,7 +500,7 @@ export class FileManager<
|
|
|
479
500
|
}
|
|
480
501
|
}
|
|
481
502
|
|
|
482
|
-
async updateFile(path: string) {
|
|
503
|
+
async updateFile(path: string): Promise<void> {
|
|
483
504
|
const absolutePath = this.container.paths.resolve(path);
|
|
484
505
|
const { name, ext, dir } = parse(absolutePath);
|
|
485
506
|
|
|
@@ -505,7 +526,7 @@ export class FileManager<
|
|
|
505
526
|
}
|
|
506
527
|
}
|
|
507
528
|
|
|
508
|
-
async removeFile(path: string) {
|
|
529
|
+
async removeFile(path: string): Promise<void> {
|
|
509
530
|
this.files.delete(path);
|
|
510
531
|
}
|
|
511
532
|
}
|
package/src/node/features/fs.ts
CHANGED
|
@@ -9,11 +9,14 @@ import {
|
|
|
9
9
|
readFileSync,
|
|
10
10
|
cpSync,
|
|
11
11
|
renameSync,
|
|
12
|
+
lstatSync,
|
|
13
|
+
realpathSync,
|
|
12
14
|
|
|
13
15
|
rmSync as nodeRmSync,
|
|
16
|
+
type Stats,
|
|
14
17
|
} from "fs";
|
|
15
18
|
import { join, resolve, dirname, relative } from "path";
|
|
16
|
-
import { readFile, stat, unlink, mkdir, writeFile, appendFile, readdir, cp, rename, rm as nodeRm } from "fs/promises";
|
|
19
|
+
import { readFile, stat, lstat, realpath, unlink, mkdir, writeFile, appendFile, readdir, cp, rename, rm as nodeRm } from "fs/promises";
|
|
17
20
|
import { native as rimraf } from 'rimraf'
|
|
18
21
|
|
|
19
22
|
type WalkOptions = {
|
|
@@ -40,6 +43,16 @@ function matchesPattern(filePath: string, patterns: string[]): boolean {
|
|
|
40
43
|
})
|
|
41
44
|
}
|
|
42
45
|
|
|
46
|
+
/** Sync: check if a symlink target is a directory */
|
|
47
|
+
function lstatFollowIsDir(fullPath: string): boolean {
|
|
48
|
+
try { return statSync(fullPath).isDirectory() } catch { return false }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Async: check if a symlink target is a directory */
|
|
52
|
+
async function lstatFollowIsDirAsync(fullPath: string): Promise<boolean> {
|
|
53
|
+
try { return (await stat(fullPath)).isDirectory() } catch { return false }
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
/**
|
|
44
57
|
* The FS feature provides methods for interacting with the file system, relative to the
|
|
45
58
|
* container's cwd.
|
|
@@ -133,7 +146,7 @@ export class FS extends Feature {
|
|
|
133
146
|
* console.log(config.version)
|
|
134
147
|
* ```
|
|
135
148
|
*/
|
|
136
|
-
readJson(path: string) {
|
|
149
|
+
readJson(path: string): any {
|
|
137
150
|
return JSON.parse(this.readFile(path) as string)
|
|
138
151
|
}
|
|
139
152
|
|
|
@@ -141,7 +154,7 @@ export class FS extends Feature {
|
|
|
141
154
|
* Read and parse a JSON file synchronously
|
|
142
155
|
* @alias readJson
|
|
143
156
|
*/
|
|
144
|
-
readJsonSync(path: string) {
|
|
157
|
+
readJsonSync(path: string): any {
|
|
145
158
|
return this.readJson(path)
|
|
146
159
|
}
|
|
147
160
|
|
|
@@ -158,7 +171,7 @@ export class FS extends Feature {
|
|
|
158
171
|
* console.log(config.version)
|
|
159
172
|
* ```
|
|
160
173
|
*/
|
|
161
|
-
async readJsonAsync(path: string) {
|
|
174
|
+
async readJsonAsync(path: string): Promise<any> {
|
|
162
175
|
const content = await this.readFileAsync(path)
|
|
163
176
|
return JSON.parse(content as string)
|
|
164
177
|
}
|
|
@@ -176,7 +189,7 @@ export class FS extends Feature {
|
|
|
176
189
|
* console.log(entries) // ['index.ts', 'utils.ts', 'components']
|
|
177
190
|
* ```
|
|
178
191
|
*/
|
|
179
|
-
readdirSync(path: string) {
|
|
192
|
+
readdirSync(path: string): string[] {
|
|
180
193
|
return readdirSync(this.container.paths.resolve(path))
|
|
181
194
|
}
|
|
182
195
|
|
|
@@ -193,7 +206,7 @@ export class FS extends Feature {
|
|
|
193
206
|
* console.log(entries) // ['index.ts', 'utils.ts', 'components']
|
|
194
207
|
* ```
|
|
195
208
|
*/
|
|
196
|
-
async readdir(path: string) {
|
|
209
|
+
async readdir(path: string): Promise<string[]> {
|
|
197
210
|
return await readdir(this.container.paths.resolve(path))
|
|
198
211
|
}
|
|
199
212
|
|
|
@@ -214,7 +227,7 @@ export class FS extends Feature {
|
|
|
214
227
|
* fs.writeFile('data.bin', Buffer.from([1, 2, 3, 4]))
|
|
215
228
|
* ```
|
|
216
229
|
*/
|
|
217
|
-
writeFile(path: string, content: Buffer | string) {
|
|
230
|
+
writeFile(path: string, content: Buffer | string): void {
|
|
218
231
|
writeFileSync(this.container.paths.resolve(path), content)
|
|
219
232
|
}
|
|
220
233
|
|
|
@@ -249,7 +262,7 @@ export class FS extends Feature {
|
|
|
249
262
|
* fs.writeJson('config.json', { version: '1.0.0', debug: false })
|
|
250
263
|
* ```
|
|
251
264
|
*/
|
|
252
|
-
writeJson(path: string, data: any, indent: number = 2) {
|
|
265
|
+
writeJson(path: string, data: any, indent: number = 2): void {
|
|
253
266
|
this.writeFile(path, JSON.stringify(data, null, indent) + '\n')
|
|
254
267
|
}
|
|
255
268
|
|
|
@@ -282,7 +295,7 @@ export class FS extends Feature {
|
|
|
282
295
|
* fs.appendFile('log.txt', 'New line\n')
|
|
283
296
|
* ```
|
|
284
297
|
*/
|
|
285
|
-
appendFile(path: string, content: Buffer | string) {
|
|
298
|
+
appendFile(path: string, content: Buffer | string): void {
|
|
286
299
|
appendFileSync(this.container.paths.resolve(path), content)
|
|
287
300
|
}
|
|
288
301
|
|
|
@@ -320,7 +333,7 @@ export class FS extends Feature {
|
|
|
320
333
|
* fs.ensureFile('logs/app.log', '', false)
|
|
321
334
|
* ```
|
|
322
335
|
*/
|
|
323
|
-
ensureFile(path: string, content: string, overwrite = false) {
|
|
336
|
+
ensureFile(path: string, content: string, overwrite = false): string {
|
|
324
337
|
path = this.container.paths.resolve(path);
|
|
325
338
|
|
|
326
339
|
if (this.exists(path) && !overwrite) {
|
|
@@ -347,7 +360,7 @@ export class FS extends Feature {
|
|
|
347
360
|
* await fs.ensureFileAsync('config/settings.json', '{}', true)
|
|
348
361
|
* ```
|
|
349
362
|
*/
|
|
350
|
-
async ensureFileAsync(path: string, content: string, overwrite = false) {
|
|
363
|
+
async ensureFileAsync(path: string, content: string, overwrite = false): Promise<string> {
|
|
351
364
|
path = this.container.paths.resolve(path);
|
|
352
365
|
|
|
353
366
|
if (this.exists(path) && !overwrite) {
|
|
@@ -372,7 +385,7 @@ export class FS extends Feature {
|
|
|
372
385
|
* fs.ensureFolder('logs/debug')
|
|
373
386
|
* ```
|
|
374
387
|
*/
|
|
375
|
-
ensureFolder(path: string) {
|
|
388
|
+
ensureFolder(path: string): string {
|
|
376
389
|
mkdirSync(this.container.paths.resolve(path), { recursive: true });
|
|
377
390
|
return this.container.paths.resolve(path);
|
|
378
391
|
}
|
|
@@ -389,7 +402,7 @@ export class FS extends Feature {
|
|
|
389
402
|
* await fs.ensureFolderAsync('logs/debug')
|
|
390
403
|
* ```
|
|
391
404
|
*/
|
|
392
|
-
async ensureFolderAsync(path: string) {
|
|
405
|
+
async ensureFolderAsync(path: string): Promise<string> {
|
|
393
406
|
const resolved = this.container.paths.resolve(path);
|
|
394
407
|
await mkdir(resolved, { recursive: true });
|
|
395
408
|
return resolved;
|
|
@@ -406,7 +419,7 @@ export class FS extends Feature {
|
|
|
406
419
|
* fs.mkdirp('deep/nested/path')
|
|
407
420
|
* ```
|
|
408
421
|
*/
|
|
409
|
-
mkdirp(folder: string) {
|
|
422
|
+
mkdirp(folder: string): string {
|
|
410
423
|
return this.ensureFolder(folder)
|
|
411
424
|
}
|
|
412
425
|
|
|
@@ -467,11 +480,33 @@ export class FS extends Feature {
|
|
|
467
480
|
* }
|
|
468
481
|
* ```
|
|
469
482
|
*/
|
|
470
|
-
async existsAsync(path: string) {
|
|
483
|
+
async existsAsync(path: string): Promise<boolean> {
|
|
471
484
|
const filePath = this.container.paths.resolve(path);
|
|
472
485
|
return stat(filePath).then(() => true).catch(() => false)
|
|
473
486
|
}
|
|
474
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Checks if a path is a symbolic link.
|
|
490
|
+
*
|
|
491
|
+
* @param {string} path - The path to check
|
|
492
|
+
* @returns {boolean} True if the path is a symlink
|
|
493
|
+
*/
|
|
494
|
+
isSymlink(path: string): boolean {
|
|
495
|
+
const filePath = this.container.paths.resolve(path);
|
|
496
|
+
try { return lstatSync(filePath).isSymbolicLink() } catch { return false }
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Resolves a symlink to its real path. Returns the resolved path as-is if not a symlink.
|
|
501
|
+
*
|
|
502
|
+
* @param {string} path - The path to resolve
|
|
503
|
+
* @returns {string} The real path after resolving all symlinks
|
|
504
|
+
*/
|
|
505
|
+
realpath(path: string): string {
|
|
506
|
+
const filePath = this.container.paths.resolve(path);
|
|
507
|
+
return realpathSync(filePath)
|
|
508
|
+
}
|
|
509
|
+
|
|
475
510
|
/**
|
|
476
511
|
* Synchronously returns the stat object for a file or directory.
|
|
477
512
|
*
|
|
@@ -485,7 +520,7 @@ export class FS extends Feature {
|
|
|
485
520
|
* console.log(info.size, info.mtime)
|
|
486
521
|
* ```
|
|
487
522
|
*/
|
|
488
|
-
stat(path: string) {
|
|
523
|
+
stat(path: string): Stats {
|
|
489
524
|
return statSync(this.container.paths.resolve(path))
|
|
490
525
|
}
|
|
491
526
|
|
|
@@ -502,7 +537,7 @@ export class FS extends Feature {
|
|
|
502
537
|
* console.log(info.size, info.mtime)
|
|
503
538
|
* ```
|
|
504
539
|
*/
|
|
505
|
-
async statAsync(path: string) {
|
|
540
|
+
async statAsync(path: string): Promise<Stats> {
|
|
506
541
|
return stat(this.container.paths.resolve(path))
|
|
507
542
|
}
|
|
508
543
|
|
|
@@ -597,7 +632,7 @@ export class FS extends Feature {
|
|
|
597
632
|
* fs.rmSync('temp/cache.tmp')
|
|
598
633
|
* ```
|
|
599
634
|
*/
|
|
600
|
-
rmSync(path: string) {
|
|
635
|
+
rmSync(path: string): void {
|
|
601
636
|
nodeRmSync(this.container.paths.resolve(path), { force: true })
|
|
602
637
|
}
|
|
603
638
|
|
|
@@ -628,7 +663,7 @@ export class FS extends Feature {
|
|
|
628
663
|
* fs.rmdirSync('temp/cache')
|
|
629
664
|
* ```
|
|
630
665
|
*/
|
|
631
|
-
rmdirSync(dirPath: string) {
|
|
666
|
+
rmdirSync(dirPath: string): void {
|
|
632
667
|
nodeRmSync(this.container.paths.resolve(dirPath), { recursive: true, force: true })
|
|
633
668
|
}
|
|
634
669
|
|
|
@@ -668,7 +703,7 @@ export class FS extends Feature {
|
|
|
668
703
|
* fs.copy('src', 'backup/src')
|
|
669
704
|
* ```
|
|
670
705
|
*/
|
|
671
|
-
copy(src: string, dest: string, options: { overwrite?: boolean } = {}) {
|
|
706
|
+
copy(src: string, dest: string, options: { overwrite?: boolean } = {}): void {
|
|
672
707
|
const { overwrite = true } = options
|
|
673
708
|
const resolvedSrc = this.container.paths.resolve(src)
|
|
674
709
|
const resolvedDest = this.container.paths.resolve(dest)
|
|
@@ -712,7 +747,7 @@ export class FS extends Feature {
|
|
|
712
747
|
* fs.move('old-dir', 'new-dir')
|
|
713
748
|
* ```
|
|
714
749
|
*/
|
|
715
|
-
move(src: string, dest: string) {
|
|
750
|
+
move(src: string, dest: string): void {
|
|
716
751
|
const resolvedSrc = this.container.paths.resolve(src)
|
|
717
752
|
const resolvedDest = this.container.paths.resolve(dest)
|
|
718
753
|
const destDir = dirname(resolvedDest)
|
|
@@ -785,7 +820,7 @@ export class FS extends Feature {
|
|
|
785
820
|
* const relative = fs.walk('inbox', { relative: true }) // => { files: ['contact-1.json', ...] }
|
|
786
821
|
* ```
|
|
787
822
|
*/
|
|
788
|
-
walk(basePath: string, options: WalkOptions = {}) {
|
|
823
|
+
walk(basePath: string, options: WalkOptions = {}): { directories: string[], files: string[] } {
|
|
789
824
|
const {
|
|
790
825
|
directories = true,
|
|
791
826
|
files = true,
|
|
@@ -811,7 +846,9 @@ export class FS extends Feature {
|
|
|
811
846
|
const fullPath = join(baseDir, name);
|
|
812
847
|
const relativePath = relative(resolvedBase, fullPath)
|
|
813
848
|
const outputPath = useRelative ? relativePath : fullPath;
|
|
814
|
-
|
|
849
|
+
// Follow symlinks: isDirectory() returns false for symlinks,
|
|
850
|
+
// so check isSymbolicLink() and resolve to the real target
|
|
851
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && lstatFollowIsDir(fullPath));
|
|
815
852
|
|
|
816
853
|
if (excludePatterns.length && matchesPattern(relativePath, excludePatterns)) {
|
|
817
854
|
continue
|
|
@@ -862,7 +899,7 @@ export class FS extends Feature {
|
|
|
862
899
|
* // files.files => ['contact-1.json', 'subfolder/file.txt', ...]
|
|
863
900
|
* ```
|
|
864
901
|
*/
|
|
865
|
-
async walkAsync(baseDir: string, options: WalkOptions = {}) {
|
|
902
|
+
async walkAsync(baseDir: string, options: WalkOptions = {}): Promise<{ directories: string[], files: string[] }> {
|
|
866
903
|
const {
|
|
867
904
|
directories = true,
|
|
868
905
|
files = true,
|
|
@@ -888,7 +925,9 @@ export class FS extends Feature {
|
|
|
888
925
|
const fullPath = join(currentDir, name);
|
|
889
926
|
const relativePath = relative(resolvedBase, fullPath)
|
|
890
927
|
const outputPath = useRelative ? relativePath : fullPath;
|
|
891
|
-
|
|
928
|
+
// Follow symlinks: isDirectory() returns false for symlinks,
|
|
929
|
+
// so check isSymbolicLink() and resolve to the real target
|
|
930
|
+
const isDir = entry.isDirectory() || (entry.isSymbolicLink() && await lstatFollowIsDirAsync(fullPath));
|
|
892
931
|
|
|
893
932
|
if (excludePatterns.length && matchesPattern(relativePath, excludePatterns)) {
|
|
894
933
|
continue
|
package/src/node/features/git.ts
CHANGED
|
@@ -98,7 +98,7 @@ export class Git extends Feature {
|
|
|
98
98
|
* })
|
|
99
99
|
* ```
|
|
100
100
|
*/
|
|
101
|
-
async lsFiles(options: LsFilesOptions = {}) {
|
|
101
|
+
async lsFiles(options: LsFilesOptions = {}): Promise<string[]> {
|
|
102
102
|
const {
|
|
103
103
|
cached = false,
|
|
104
104
|
deleted = false,
|
|
@@ -153,7 +153,7 @@ export class Git extends Feature {
|
|
|
153
153
|
* }
|
|
154
154
|
* ```
|
|
155
155
|
*/
|
|
156
|
-
get branch() {
|
|
156
|
+
get branch(): string | null {
|
|
157
157
|
if(!this.isRepo) { return null }
|
|
158
158
|
return this.container.feature('proc').exec(`${this.gitPath} branch`).split("\n").filter(line => line.startsWith('*')).map(line => line.replace('*', '').trim()).pop()
|
|
159
159
|
}
|
|
@@ -171,7 +171,7 @@ export class Git extends Feature {
|
|
|
171
171
|
* }
|
|
172
172
|
* ```
|
|
173
173
|
*/
|
|
174
|
-
get sha() {
|
|
174
|
+
get sha(): string | null {
|
|
175
175
|
if(!this.isRepo) { return null }
|
|
176
176
|
return this.container.feature('proc').exec(`${this.gitPath} rev-parse HEAD`, { cwd: this.repoRoot })
|
|
177
177
|
}
|
|
@@ -190,7 +190,7 @@ export class Git extends Feature {
|
|
|
190
190
|
* }
|
|
191
191
|
* ```
|
|
192
192
|
*/
|
|
193
|
-
get isRepo() {
|
|
193
|
+
get isRepo(): boolean {
|
|
194
194
|
return !!this.repoRoot
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -208,7 +208,7 @@ export class Git extends Feature {
|
|
|
208
208
|
* }
|
|
209
209
|
* ```
|
|
210
210
|
*/
|
|
211
|
-
get isRepoRoot() {
|
|
211
|
+
get isRepoRoot(): boolean {
|
|
212
212
|
return this.repoRoot == this.container.cwd
|
|
213
213
|
}
|
|
214
214
|
|
|
@@ -228,7 +228,7 @@ export class Git extends Feature {
|
|
|
228
228
|
* }
|
|
229
229
|
* ```
|
|
230
230
|
*/
|
|
231
|
-
get repoRoot() {
|
|
231
|
+
get repoRoot(): string | null {
|
|
232
232
|
if (this.state.has('repoRoot')) {
|
|
233
233
|
return this.state.get('repoRoot')
|
|
234
234
|
}
|
|
@@ -260,7 +260,7 @@ export class Git extends Feature {
|
|
|
260
260
|
* }
|
|
261
261
|
* ```
|
|
262
262
|
*/
|
|
263
|
-
async getLatestChanges(numberOfChanges: number = 10) {
|
|
263
|
+
async getLatestChanges(numberOfChanges: number = 10): Promise<Array<{ title: string, message: string, author: string }>> {
|
|
264
264
|
if (!this.isRepo) return []
|
|
265
265
|
|
|
266
266
|
const separator = '---COMMIT---'
|
|
@@ -299,7 +299,7 @@ export class Git extends Feature {
|
|
|
299
299
|
* }
|
|
300
300
|
* ```
|
|
301
301
|
*/
|
|
302
|
-
fileLog(...files: string[]) {
|
|
302
|
+
fileLog(...files: string[]): Array<{ sha: string, message: string }> {
|
|
303
303
|
if (!this.isRepo || !files.length) return []
|
|
304
304
|
|
|
305
305
|
const proc = this.container.feature('proc')
|
|
@@ -349,7 +349,7 @@ export class Git extends Feature {
|
|
|
349
349
|
* const d = git.diff('src/index.ts', 'feature-branch', 'main')
|
|
350
350
|
* ```
|
|
351
351
|
*/
|
|
352
|
-
diff(file: string, compareTo: string, compareFrom?: string) {
|
|
352
|
+
diff(file: string, compareTo: string, compareFrom?: string): string {
|
|
353
353
|
if (!this.isRepo) return ''
|
|
354
354
|
|
|
355
355
|
const proc = this.container.feature('proc')
|
|
@@ -531,7 +531,7 @@ export class Git extends Feature {
|
|
|
531
531
|
* const history = git.getChangeHistoryForFiles('src/node/features/*.ts')
|
|
532
532
|
* ```
|
|
533
533
|
*/
|
|
534
|
-
getChangeHistoryForFiles(...paths: string[]) {
|
|
534
|
+
getChangeHistoryForFiles(...paths: string[]): Array<{ sha: string, message: string, longMessage: string, filesMatched: string[] }> {
|
|
535
535
|
if (!this.isRepo || !paths.length) return []
|
|
536
536
|
|
|
537
537
|
const proc = this.container.feature('proc')
|
|
@@ -438,24 +438,22 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
|
|
|
438
438
|
}
|
|
439
439
|
} catch {}
|
|
440
440
|
|
|
441
|
-
// Try fileManager first (faster in git repos
|
|
441
|
+
// Try fileManager first (faster in git repos, and now symlink-aware),
|
|
442
|
+
// fall back to fs.walk which also follows symlinks natively.
|
|
442
443
|
let files: string[] = []
|
|
443
444
|
try {
|
|
444
445
|
const fm = await this.ensureFileManager()
|
|
445
|
-
// fileManager may store absolute or relative keys — use absolute patterns
|
|
446
446
|
const absPatterns = [`${dir}/*.ts`, `${dir}/**/*.ts`]
|
|
447
447
|
const relPatterns = [`${type}/*.ts`, `${type}/**/*.ts`]
|
|
448
448
|
const matched = fm.match([...absPatterns, ...relPatterns])
|
|
449
449
|
files = matched.map((f: string) => f.startsWith('/') ? f : resolve(this.rootDir, f))
|
|
450
450
|
} catch {}
|
|
451
451
|
|
|
452
|
-
// Fall back to
|
|
452
|
+
// Fall back to fs.walk if fileManager found nothing
|
|
453
453
|
if (files.length === 0) {
|
|
454
|
-
const {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
files.push(resolve(dir, file))
|
|
458
|
-
}
|
|
454
|
+
const { fs } = this.container
|
|
455
|
+
const walked = fs.walk(dir, { include: ['**/*.ts'] })
|
|
456
|
+
files = walked.files
|
|
459
457
|
}
|
|
460
458
|
|
|
461
459
|
for (const absPath of files) {
|
|
@@ -550,10 +548,15 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
|
|
|
550
548
|
*/
|
|
551
549
|
private async discoverCommandsViaVM(dir: string): Promise<void> {
|
|
552
550
|
this.seedVirtualModules()
|
|
553
|
-
const {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
551
|
+
const { fs } = this.container
|
|
552
|
+
// Commands are top-level only (not recursive) — walk and filter to *.ts in the immediate dir
|
|
553
|
+
const walked = fs.walk(dir, { include: ['*.ts'] })
|
|
554
|
+
const tsFiles = walked.files
|
|
555
|
+
.map(f => parse(f))
|
|
556
|
+
.filter(p => p.dir === dir) // top-level only
|
|
557
|
+
.map(p => p.base)
|
|
558
|
+
|
|
559
|
+
for (const file of tsFiles) {
|
|
557
560
|
if (file === 'index.ts') continue
|
|
558
561
|
|
|
559
562
|
const absPath = resolve(dir, file)
|
|
@@ -619,10 +622,14 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
|
|
|
619
622
|
*/
|
|
620
623
|
private async discoverSelectorsViaVM(dir: string): Promise<void> {
|
|
621
624
|
this.seedVirtualModules()
|
|
622
|
-
const {
|
|
623
|
-
const
|
|
625
|
+
const { fs } = this.container
|
|
626
|
+
const walked = fs.walk(dir, { include: ['*.ts'] })
|
|
627
|
+
const tsFiles = walked.files
|
|
628
|
+
.map(f => parse(f))
|
|
629
|
+
.filter(p => p.dir === dir)
|
|
630
|
+
.map(p => p.base)
|
|
624
631
|
|
|
625
|
-
for
|
|
632
|
+
for (const file of tsFiles) {
|
|
626
633
|
if (file === 'index.ts') continue
|
|
627
634
|
|
|
628
635
|
const absPath = resolve(dir, file)
|
|
@@ -661,10 +668,10 @@ export class Helpers extends Feature<HelpersState, HelpersOptions> {
|
|
|
661
668
|
* Actual mounting to an express server is handled separately by ExpressServer.useEndpoints().
|
|
662
669
|
*/
|
|
663
670
|
private async discoverEndpoints(dir: string): Promise<void> {
|
|
664
|
-
const {
|
|
665
|
-
const
|
|
671
|
+
const { fs } = this.container
|
|
672
|
+
const walked = fs.walk(dir, { include: ['**/*.ts'] })
|
|
666
673
|
|
|
667
|
-
for
|
|
674
|
+
for (const file of walked.files) {
|
|
668
675
|
try {
|
|
669
676
|
const mod = await this.loadModuleExports(file)
|
|
670
677
|
const endpointModule = mod.default || mod
|