@link-assistant/agent 0.0.8 → 0.0.11
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/EXAMPLES.md +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +78 -0
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +554 -332
- package/src/json-standard/index.ts +173 -0
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/config/markdown.ts
CHANGED
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { NamedError } from
|
|
2
|
-
import matter from
|
|
3
|
-
import { z } from
|
|
1
|
+
import { NamedError } from '../util/error';
|
|
2
|
+
import matter from 'gray-matter';
|
|
3
|
+
import { z } from 'zod';
|
|
4
4
|
|
|
5
5
|
export namespace ConfigMarkdown {
|
|
6
|
-
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g
|
|
7
|
-
export const SHELL_REGEX = /!`([^`]+)`/g
|
|
6
|
+
export const FILE_REGEX = /(?<![\w`])@(\.?[^\s`,.]*(?:\.[^\s`,.]+)*)/g;
|
|
7
|
+
export const SHELL_REGEX = /!`([^`]+)`/g;
|
|
8
8
|
|
|
9
9
|
export function files(template: string) {
|
|
10
|
-
return Array.from(template.matchAll(FILE_REGEX))
|
|
10
|
+
return Array.from(template.matchAll(FILE_REGEX));
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function shell(template: string) {
|
|
14
|
-
return Array.from(template.matchAll(SHELL_REGEX))
|
|
14
|
+
return Array.from(template.matchAll(SHELL_REGEX));
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function parse(filePath: string) {
|
|
18
|
-
const template = await Bun.file(filePath).text()
|
|
18
|
+
const template = await Bun.file(filePath).text();
|
|
19
19
|
|
|
20
20
|
try {
|
|
21
|
-
const md = matter(template)
|
|
22
|
-
return md
|
|
21
|
+
const md = matter(template);
|
|
22
|
+
return md;
|
|
23
23
|
} catch (err) {
|
|
24
24
|
throw new FrontmatterError(
|
|
25
25
|
{
|
|
26
26
|
path: filePath,
|
|
27
27
|
message: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
28
28
|
},
|
|
29
|
-
{ cause: err }
|
|
30
|
-
)
|
|
29
|
+
{ cause: err }
|
|
30
|
+
);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export const FrontmatterError = NamedError.create(
|
|
35
|
-
|
|
35
|
+
'ConfigFrontmatterError',
|
|
36
36
|
z.object({
|
|
37
37
|
path: z.string(),
|
|
38
38
|
message: z.string(),
|
|
39
|
-
})
|
|
40
|
-
)
|
|
39
|
+
})
|
|
40
|
+
);
|
|
41
41
|
}
|
package/src/file/ripgrep.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
// Ripgrep utility functions
|
|
2
|
-
import path from
|
|
3
|
-
import { Global } from
|
|
4
|
-
import fs from
|
|
5
|
-
import z from
|
|
6
|
-
import { NamedError } from
|
|
7
|
-
import { lazy } from
|
|
8
|
-
import { $ } from
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Global } from '../global';
|
|
4
|
+
import fs from 'fs/promises';
|
|
5
|
+
import z from 'zod';
|
|
6
|
+
import { NamedError } from '../util/error';
|
|
7
|
+
import { lazy } from '../util/lazy';
|
|
8
|
+
import { $ } from 'bun';
|
|
9
9
|
|
|
10
|
-
import { ZipReader, BlobReader, BlobWriter } from
|
|
11
|
-
import { Log } from
|
|
10
|
+
import { ZipReader, BlobReader, BlobWriter } from '@zip.js/zip.js';
|
|
11
|
+
import { Log } from '../util/log';
|
|
12
12
|
|
|
13
13
|
export namespace Ripgrep {
|
|
14
|
-
const log = Log.create({ service:
|
|
14
|
+
const log = Log.create({ service: 'ripgrep' });
|
|
15
15
|
const Stats = z.object({
|
|
16
16
|
elapsed: z.object({
|
|
17
17
|
secs: z.number(),
|
|
@@ -24,19 +24,19 @@ export namespace Ripgrep {
|
|
|
24
24
|
bytes_printed: z.number(),
|
|
25
25
|
matched_lines: z.number(),
|
|
26
26
|
matches: z.number(),
|
|
27
|
-
})
|
|
27
|
+
});
|
|
28
28
|
|
|
29
29
|
const Begin = z.object({
|
|
30
|
-
type: z.literal(
|
|
30
|
+
type: z.literal('begin'),
|
|
31
31
|
data: z.object({
|
|
32
32
|
path: z.object({
|
|
33
33
|
text: z.string(),
|
|
34
34
|
}),
|
|
35
35
|
}),
|
|
36
|
-
})
|
|
36
|
+
});
|
|
37
37
|
|
|
38
38
|
export const Match = z.object({
|
|
39
|
-
type: z.literal(
|
|
39
|
+
type: z.literal('match'),
|
|
40
40
|
data: z.object({
|
|
41
41
|
path: z.object({
|
|
42
42
|
text: z.string(),
|
|
@@ -53,13 +53,13 @@ export namespace Ripgrep {
|
|
|
53
53
|
}),
|
|
54
54
|
start: z.number(),
|
|
55
55
|
end: z.number(),
|
|
56
|
-
})
|
|
56
|
+
})
|
|
57
57
|
),
|
|
58
58
|
}),
|
|
59
|
-
})
|
|
59
|
+
});
|
|
60
60
|
|
|
61
61
|
const End = z.object({
|
|
62
|
-
type: z.literal(
|
|
62
|
+
type: z.literal('end'),
|
|
63
63
|
data: z.object({
|
|
64
64
|
path: z.object({
|
|
65
65
|
text: z.string(),
|
|
@@ -67,10 +67,10 @@ export namespace Ripgrep {
|
|
|
67
67
|
binary_offset: z.number().nullable(),
|
|
68
68
|
stats: Stats,
|
|
69
69
|
}),
|
|
70
|
-
})
|
|
70
|
+
});
|
|
71
71
|
|
|
72
72
|
const Summary = z.object({
|
|
73
|
-
type: z.literal(
|
|
73
|
+
type: z.literal('summary'),
|
|
74
74
|
data: z.object({
|
|
75
75
|
elapsed_total: z.object({
|
|
76
76
|
human: z.string(),
|
|
@@ -79,313 +79,345 @@ export namespace Ripgrep {
|
|
|
79
79
|
}),
|
|
80
80
|
stats: Stats,
|
|
81
81
|
}),
|
|
82
|
-
})
|
|
82
|
+
});
|
|
83
83
|
|
|
84
|
-
const Result = z.union([Begin, Match, End, Summary])
|
|
84
|
+
const Result = z.union([Begin, Match, End, Summary]);
|
|
85
85
|
|
|
86
|
-
export type Result = z.infer<typeof Result
|
|
87
|
-
export type Match = z.infer<typeof Match
|
|
88
|
-
export type Begin = z.infer<typeof Begin
|
|
89
|
-
export type End = z.infer<typeof End
|
|
90
|
-
export type Summary = z.infer<typeof Summary
|
|
86
|
+
export type Result = z.infer<typeof Result>;
|
|
87
|
+
export type Match = z.infer<typeof Match>;
|
|
88
|
+
export type Begin = z.infer<typeof Begin>;
|
|
89
|
+
export type End = z.infer<typeof End>;
|
|
90
|
+
export type Summary = z.infer<typeof Summary>;
|
|
91
91
|
const PLATFORM = {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
platform:
|
|
95
|
-
extension:
|
|
92
|
+
'arm64-darwin': { platform: 'aarch64-apple-darwin', extension: 'tar.gz' },
|
|
93
|
+
'arm64-linux': {
|
|
94
|
+
platform: 'aarch64-unknown-linux-gnu',
|
|
95
|
+
extension: 'tar.gz',
|
|
96
96
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
} as const
|
|
97
|
+
'x64-darwin': { platform: 'x86_64-apple-darwin', extension: 'tar.gz' },
|
|
98
|
+
'x64-linux': { platform: 'x86_64-unknown-linux-musl', extension: 'tar.gz' },
|
|
99
|
+
'x64-win32': { platform: 'x86_64-pc-windows-msvc', extension: 'zip' },
|
|
100
|
+
} as const;
|
|
101
101
|
|
|
102
102
|
export const ExtractionFailedError = NamedError.create(
|
|
103
|
-
|
|
103
|
+
'RipgrepExtractionFailedError',
|
|
104
104
|
z.object({
|
|
105
105
|
filepath: z.string(),
|
|
106
106
|
stderr: z.string(),
|
|
107
|
-
})
|
|
108
|
-
)
|
|
107
|
+
})
|
|
108
|
+
);
|
|
109
109
|
|
|
110
110
|
export const UnsupportedPlatformError = NamedError.create(
|
|
111
|
-
|
|
111
|
+
'RipgrepUnsupportedPlatformError',
|
|
112
112
|
z.object({
|
|
113
113
|
platform: z.string(),
|
|
114
|
-
})
|
|
115
|
-
)
|
|
114
|
+
})
|
|
115
|
+
);
|
|
116
116
|
|
|
117
117
|
export const DownloadFailedError = NamedError.create(
|
|
118
|
-
|
|
118
|
+
'RipgrepDownloadFailedError',
|
|
119
119
|
z.object({
|
|
120
120
|
url: z.string(),
|
|
121
121
|
status: z.number(),
|
|
122
|
-
})
|
|
123
|
-
)
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
124
|
|
|
125
125
|
const state = lazy(async () => {
|
|
126
|
-
let filepath = Bun.which(
|
|
127
|
-
if (filepath) return { filepath }
|
|
128
|
-
filepath = path.join(
|
|
129
|
-
|
|
130
|
-
|
|
126
|
+
let filepath = Bun.which('rg');
|
|
127
|
+
if (filepath) return { filepath };
|
|
128
|
+
filepath = path.join(
|
|
129
|
+
Global.Path.bin,
|
|
130
|
+
'rg' + (process.platform === 'win32' ? '.exe' : '')
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const file = Bun.file(filepath);
|
|
131
134
|
if (!(await file.exists())) {
|
|
132
|
-
const platformKey =
|
|
133
|
-
|
|
134
|
-
|
|
135
|
+
const platformKey =
|
|
136
|
+
`${process.arch}-${process.platform}` as keyof typeof PLATFORM;
|
|
137
|
+
const config = PLATFORM[platformKey];
|
|
138
|
+
if (!config)
|
|
139
|
+
throw new UnsupportedPlatformError({ platform: platformKey });
|
|
135
140
|
|
|
136
|
-
const version =
|
|
137
|
-
const filename = `ripgrep-${version}-${config.platform}.${config.extension}
|
|
138
|
-
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}
|
|
141
|
+
const version = '14.1.1';
|
|
142
|
+
const filename = `ripgrep-${version}-${config.platform}.${config.extension}`;
|
|
143
|
+
const url = `https://github.com/BurntSushi/ripgrep/releases/download/${version}/${filename}`;
|
|
139
144
|
|
|
140
|
-
const response = await fetch(url)
|
|
141
|
-
if (!response.ok)
|
|
145
|
+
const response = await fetch(url);
|
|
146
|
+
if (!response.ok)
|
|
147
|
+
throw new DownloadFailedError({ url, status: response.status });
|
|
142
148
|
|
|
143
|
-
const buffer = await response.arrayBuffer()
|
|
144
|
-
const archivePath = path.join(Global.Path.bin, filename)
|
|
145
|
-
await Bun.write(archivePath, buffer)
|
|
146
|
-
if (config.extension ===
|
|
147
|
-
const args = [
|
|
149
|
+
const buffer = await response.arrayBuffer();
|
|
150
|
+
const archivePath = path.join(Global.Path.bin, filename);
|
|
151
|
+
await Bun.write(archivePath, buffer);
|
|
152
|
+
if (config.extension === 'tar.gz') {
|
|
153
|
+
const args = ['tar', '-xzf', archivePath, '--strip-components=1'];
|
|
148
154
|
|
|
149
|
-
if (platformKey.endsWith(
|
|
150
|
-
if (platformKey.endsWith(
|
|
155
|
+
if (platformKey.endsWith('-darwin')) args.push('--include=*/rg');
|
|
156
|
+
if (platformKey.endsWith('-linux')) args.push('--wildcards', '*/rg');
|
|
151
157
|
|
|
152
158
|
const proc = Bun.spawn(args, {
|
|
153
159
|
cwd: Global.Path.bin,
|
|
154
|
-
stderr:
|
|
155
|
-
stdout:
|
|
156
|
-
})
|
|
157
|
-
await proc.exited
|
|
160
|
+
stderr: 'pipe',
|
|
161
|
+
stdout: 'pipe',
|
|
162
|
+
});
|
|
163
|
+
await proc.exited;
|
|
158
164
|
if (proc.exitCode !== 0)
|
|
159
165
|
throw new ExtractionFailedError({
|
|
160
166
|
filepath,
|
|
161
167
|
stderr: await Bun.readableStreamToText(proc.stderr),
|
|
162
|
-
})
|
|
168
|
+
});
|
|
163
169
|
}
|
|
164
|
-
if (config.extension ===
|
|
165
|
-
if (config.extension ===
|
|
166
|
-
const zipFileReader = new ZipReader(
|
|
167
|
-
|
|
168
|
-
|
|
170
|
+
if (config.extension === 'zip') {
|
|
171
|
+
if (config.extension === 'zip') {
|
|
172
|
+
const zipFileReader = new ZipReader(
|
|
173
|
+
new BlobReader(
|
|
174
|
+
new Blob([await Bun.file(archivePath).arrayBuffer()])
|
|
175
|
+
)
|
|
176
|
+
);
|
|
177
|
+
const entries = await zipFileReader.getEntries();
|
|
178
|
+
let rgEntry: any;
|
|
169
179
|
for (const entry of entries) {
|
|
170
|
-
if (entry.filename.endsWith(
|
|
171
|
-
rgEntry = entry
|
|
172
|
-
break
|
|
180
|
+
if (entry.filename.endsWith('rg.exe')) {
|
|
181
|
+
rgEntry = entry;
|
|
182
|
+
break;
|
|
173
183
|
}
|
|
174
184
|
}
|
|
175
185
|
|
|
176
186
|
if (!rgEntry) {
|
|
177
187
|
throw new ExtractionFailedError({
|
|
178
188
|
filepath: archivePath,
|
|
179
|
-
stderr:
|
|
180
|
-
})
|
|
189
|
+
stderr: 'rg.exe not found in zip archive',
|
|
190
|
+
});
|
|
181
191
|
}
|
|
182
192
|
|
|
183
|
-
const rgBlob = await rgEntry.getData(new BlobWriter())
|
|
193
|
+
const rgBlob = await rgEntry.getData(new BlobWriter());
|
|
184
194
|
if (!rgBlob) {
|
|
185
195
|
throw new ExtractionFailedError({
|
|
186
196
|
filepath: archivePath,
|
|
187
|
-
stderr:
|
|
188
|
-
})
|
|
197
|
+
stderr: 'Failed to extract rg.exe from zip archive',
|
|
198
|
+
});
|
|
189
199
|
}
|
|
190
|
-
await Bun.write(filepath, await rgBlob.arrayBuffer())
|
|
191
|
-
await zipFileReader.close()
|
|
200
|
+
await Bun.write(filepath, await rgBlob.arrayBuffer());
|
|
201
|
+
await zipFileReader.close();
|
|
192
202
|
}
|
|
193
203
|
}
|
|
194
|
-
await fs.unlink(archivePath)
|
|
195
|
-
if (!platformKey.endsWith(
|
|
204
|
+
await fs.unlink(archivePath);
|
|
205
|
+
if (!platformKey.endsWith('-win32')) await fs.chmod(filepath, 0o755);
|
|
196
206
|
}
|
|
197
207
|
|
|
198
208
|
return {
|
|
199
209
|
filepath,
|
|
200
|
-
}
|
|
201
|
-
})
|
|
210
|
+
};
|
|
211
|
+
});
|
|
202
212
|
|
|
203
213
|
export async function filepath() {
|
|
204
|
-
const { filepath } = await state()
|
|
205
|
-
return filepath
|
|
214
|
+
const { filepath } = await state();
|
|
215
|
+
return filepath;
|
|
206
216
|
}
|
|
207
217
|
|
|
208
218
|
export async function* files(input: { cwd: string; glob?: string[] }) {
|
|
209
|
-
const args = [
|
|
219
|
+
const args = [
|
|
220
|
+
await filepath(),
|
|
221
|
+
'--files',
|
|
222
|
+
'--follow',
|
|
223
|
+
'--hidden',
|
|
224
|
+
'--glob=!.git/*',
|
|
225
|
+
];
|
|
210
226
|
if (input.glob) {
|
|
211
227
|
for (const g of input.glob) {
|
|
212
|
-
args.push(`--glob=${g}`)
|
|
228
|
+
args.push(`--glob=${g}`);
|
|
213
229
|
}
|
|
214
230
|
}
|
|
215
231
|
|
|
216
232
|
// Bun.spawn should throw this, but it incorrectly reports that the executable does not exist.
|
|
217
233
|
// See https://github.com/oven-sh/bun/issues/24012
|
|
218
234
|
if (!(await fs.stat(input.cwd).catch(() => undefined))?.isDirectory()) {
|
|
219
|
-
throw Object.assign(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
235
|
+
throw Object.assign(
|
|
236
|
+
new Error(`No such file or directory: '${input.cwd}'`),
|
|
237
|
+
{
|
|
238
|
+
code: 'ENOENT',
|
|
239
|
+
errno: -2,
|
|
240
|
+
path: input.cwd,
|
|
241
|
+
}
|
|
242
|
+
);
|
|
224
243
|
}
|
|
225
244
|
|
|
226
245
|
const proc = Bun.spawn(args, {
|
|
227
246
|
cwd: input.cwd,
|
|
228
|
-
stdout:
|
|
229
|
-
stderr:
|
|
247
|
+
stdout: 'pipe',
|
|
248
|
+
stderr: 'ignore',
|
|
230
249
|
maxBuffer: 1024 * 1024 * 20,
|
|
231
|
-
})
|
|
250
|
+
});
|
|
232
251
|
|
|
233
|
-
const reader = proc.stdout.getReader()
|
|
234
|
-
const decoder = new TextDecoder()
|
|
235
|
-
let buffer =
|
|
252
|
+
const reader = proc.stdout.getReader();
|
|
253
|
+
const decoder = new TextDecoder();
|
|
254
|
+
let buffer = '';
|
|
236
255
|
|
|
237
256
|
try {
|
|
238
257
|
while (true) {
|
|
239
|
-
const { done, value } = await reader.read()
|
|
240
|
-
if (done) break
|
|
258
|
+
const { done, value } = await reader.read();
|
|
259
|
+
if (done) break;
|
|
241
260
|
|
|
242
|
-
buffer += decoder.decode(value, { stream: true })
|
|
243
|
-
const lines = buffer.split(
|
|
244
|
-
buffer = lines.pop() ||
|
|
261
|
+
buffer += decoder.decode(value, { stream: true });
|
|
262
|
+
const lines = buffer.split('\n');
|
|
263
|
+
buffer = lines.pop() || '';
|
|
245
264
|
|
|
246
265
|
for (const line of lines) {
|
|
247
|
-
if (line) yield line
|
|
266
|
+
if (line) yield line;
|
|
248
267
|
}
|
|
249
268
|
}
|
|
250
269
|
|
|
251
|
-
if (buffer) yield buffer
|
|
270
|
+
if (buffer) yield buffer;
|
|
252
271
|
} finally {
|
|
253
|
-
reader.releaseLock()
|
|
254
|
-
await proc.exited
|
|
272
|
+
reader.releaseLock();
|
|
273
|
+
await proc.exited;
|
|
255
274
|
}
|
|
256
275
|
}
|
|
257
276
|
|
|
258
277
|
export async function tree(input: { cwd: string; limit?: number }) {
|
|
259
|
-
log.info(
|
|
260
|
-
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }))
|
|
278
|
+
log.info('tree', input);
|
|
279
|
+
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }));
|
|
261
280
|
interface Node {
|
|
262
|
-
path: string[]
|
|
263
|
-
children: Node[]
|
|
281
|
+
path: string[];
|
|
282
|
+
children: Node[];
|
|
264
283
|
}
|
|
265
284
|
|
|
266
285
|
function getPath(node: Node, parts: string[], create: boolean) {
|
|
267
|
-
if (parts.length === 0) return node
|
|
268
|
-
let current = node
|
|
286
|
+
if (parts.length === 0) return node;
|
|
287
|
+
let current = node;
|
|
269
288
|
for (const part of parts) {
|
|
270
|
-
let existing = current.children.find((x) => x.path.at(-1) === part)
|
|
289
|
+
let existing = current.children.find((x) => x.path.at(-1) === part);
|
|
271
290
|
if (!existing) {
|
|
272
|
-
if (!create) return
|
|
291
|
+
if (!create) return;
|
|
273
292
|
existing = {
|
|
274
293
|
path: current.path.concat(part),
|
|
275
294
|
children: [],
|
|
276
|
-
}
|
|
277
|
-
current.children.push(existing)
|
|
295
|
+
};
|
|
296
|
+
current.children.push(existing);
|
|
278
297
|
}
|
|
279
|
-
current = existing
|
|
298
|
+
current = existing;
|
|
280
299
|
}
|
|
281
|
-
return current
|
|
300
|
+
return current;
|
|
282
301
|
}
|
|
283
302
|
|
|
284
303
|
const root: Node = {
|
|
285
304
|
path: [],
|
|
286
305
|
children: [],
|
|
287
|
-
}
|
|
306
|
+
};
|
|
288
307
|
for (const file of files) {
|
|
289
|
-
if (file.includes(
|
|
290
|
-
const parts = file.split(path.sep)
|
|
291
|
-
getPath(root, parts, true)
|
|
308
|
+
if (file.includes('.opencode')) continue;
|
|
309
|
+
const parts = file.split(path.sep);
|
|
310
|
+
getPath(root, parts, true);
|
|
292
311
|
}
|
|
293
312
|
|
|
294
313
|
function sort(node: Node) {
|
|
295
314
|
node.children.sort((a, b) => {
|
|
296
|
-
if (!a.children.length && b.children.length) return 1
|
|
297
|
-
if (!b.children.length && a.children.length) return -1
|
|
298
|
-
return a.path.at(-1)!.localeCompare(b.path.at(-1)!)
|
|
299
|
-
})
|
|
315
|
+
if (!a.children.length && b.children.length) return 1;
|
|
316
|
+
if (!b.children.length && a.children.length) return -1;
|
|
317
|
+
return a.path.at(-1)!.localeCompare(b.path.at(-1)!);
|
|
318
|
+
});
|
|
300
319
|
for (const child of node.children) {
|
|
301
|
-
sort(child)
|
|
320
|
+
sort(child);
|
|
302
321
|
}
|
|
303
322
|
}
|
|
304
|
-
sort(root)
|
|
323
|
+
sort(root);
|
|
305
324
|
|
|
306
|
-
let current = [root]
|
|
325
|
+
let current = [root];
|
|
307
326
|
const result: Node = {
|
|
308
327
|
path: [],
|
|
309
328
|
children: [],
|
|
310
|
-
}
|
|
329
|
+
};
|
|
311
330
|
|
|
312
|
-
let processed = 0
|
|
313
|
-
const limit = input.limit ?? 50
|
|
331
|
+
let processed = 0;
|
|
332
|
+
const limit = input.limit ?? 50;
|
|
314
333
|
while (current.length > 0) {
|
|
315
|
-
const next = []
|
|
334
|
+
const next = [];
|
|
316
335
|
for (const node of current) {
|
|
317
|
-
if (node.children.length) next.push(...node.children)
|
|
336
|
+
if (node.children.length) next.push(...node.children);
|
|
318
337
|
}
|
|
319
|
-
const max = Math.max(...current.map((x) => x.children.length))
|
|
338
|
+
const max = Math.max(...current.map((x) => x.children.length));
|
|
320
339
|
for (let i = 0; i < max && processed < limit; i++) {
|
|
321
340
|
for (const node of current) {
|
|
322
|
-
const child = node.children[i]
|
|
323
|
-
if (!child) continue
|
|
324
|
-
getPath(result, child.path, true)
|
|
325
|
-
processed
|
|
326
|
-
if (processed >= limit) break
|
|
341
|
+
const child = node.children[i];
|
|
342
|
+
if (!child) continue;
|
|
343
|
+
getPath(result, child.path, true);
|
|
344
|
+
processed++;
|
|
345
|
+
if (processed >= limit) break;
|
|
327
346
|
}
|
|
328
347
|
}
|
|
329
348
|
if (processed >= limit) {
|
|
330
349
|
for (const node of [...current, ...next]) {
|
|
331
|
-
const compare = getPath(result, node.path, false)
|
|
332
|
-
if (!compare) continue
|
|
350
|
+
const compare = getPath(result, node.path, false);
|
|
351
|
+
if (!compare) continue;
|
|
333
352
|
if (compare?.children.length !== node.children.length) {
|
|
334
|
-
const diff = node.children.length - compare.children.length
|
|
353
|
+
const diff = node.children.length - compare.children.length;
|
|
335
354
|
compare.children.push({
|
|
336
355
|
path: compare.path.concat(`[${diff} truncated]`),
|
|
337
356
|
children: [],
|
|
338
|
-
})
|
|
357
|
+
});
|
|
339
358
|
}
|
|
340
359
|
}
|
|
341
|
-
break
|
|
360
|
+
break;
|
|
342
361
|
}
|
|
343
|
-
current = next
|
|
362
|
+
current = next;
|
|
344
363
|
}
|
|
345
364
|
|
|
346
|
-
const lines: string[] = []
|
|
365
|
+
const lines: string[] = [];
|
|
347
366
|
|
|
348
367
|
function render(node: Node, depth: number) {
|
|
349
|
-
const indent =
|
|
350
|
-
lines.push(indent + node.path.at(-1) + (node.children.length ?
|
|
368
|
+
const indent = '\t'.repeat(depth);
|
|
369
|
+
lines.push(indent + node.path.at(-1) + (node.children.length ? '/' : ''));
|
|
351
370
|
for (const child of node.children) {
|
|
352
|
-
render(child, depth + 1)
|
|
371
|
+
render(child, depth + 1);
|
|
353
372
|
}
|
|
354
373
|
}
|
|
355
|
-
result.children.map((x) => render(x, 0))
|
|
374
|
+
result.children.map((x) => render(x, 0));
|
|
356
375
|
|
|
357
|
-
return lines.join(
|
|
376
|
+
return lines.join('\n');
|
|
358
377
|
}
|
|
359
378
|
|
|
360
|
-
export async function search(input: {
|
|
361
|
-
|
|
379
|
+
export async function search(input: {
|
|
380
|
+
cwd: string;
|
|
381
|
+
pattern: string;
|
|
382
|
+
glob?: string[];
|
|
383
|
+
limit?: number;
|
|
384
|
+
}) {
|
|
385
|
+
const args = [
|
|
386
|
+
`${await filepath()}`,
|
|
387
|
+
'--json',
|
|
388
|
+
'--hidden',
|
|
389
|
+
"--glob='!.git/*'",
|
|
390
|
+
];
|
|
362
391
|
|
|
363
392
|
if (input.glob) {
|
|
364
393
|
for (const g of input.glob) {
|
|
365
|
-
args.push(`--glob=${g}`)
|
|
394
|
+
args.push(`--glob=${g}`);
|
|
366
395
|
}
|
|
367
396
|
}
|
|
368
397
|
|
|
369
398
|
if (input.limit) {
|
|
370
|
-
args.push(`--max-count=${input.limit}`)
|
|
399
|
+
args.push(`--max-count=${input.limit}`);
|
|
371
400
|
}
|
|
372
401
|
|
|
373
|
-
args.push(
|
|
374
|
-
args.push(input.pattern)
|
|
402
|
+
args.push('--');
|
|
403
|
+
args.push(input.pattern);
|
|
375
404
|
|
|
376
|
-
const command = args.join(
|
|
377
|
-
const result = await $`${{ raw: command }}
|
|
405
|
+
const command = args.join(' ');
|
|
406
|
+
const result = await $`${{ raw: command }}`
|
|
407
|
+
.cwd(input.cwd)
|
|
408
|
+
.quiet()
|
|
409
|
+
.nothrow();
|
|
378
410
|
if (result.exitCode !== 0) {
|
|
379
|
-
return []
|
|
411
|
+
return [];
|
|
380
412
|
}
|
|
381
413
|
|
|
382
|
-
const lines = result.text().trim().split(
|
|
414
|
+
const lines = result.text().trim().split('\n').filter(Boolean);
|
|
383
415
|
// Parse JSON lines from ripgrep output
|
|
384
416
|
|
|
385
417
|
return lines
|
|
386
418
|
.map((line) => JSON.parse(line))
|
|
387
419
|
.map((parsed) => Result.parse(parsed))
|
|
388
|
-
.filter((r) => r.type ===
|
|
389
|
-
.map((r) => r.data)
|
|
420
|
+
.filter((r) => r.type === 'match')
|
|
421
|
+
.map((r) => r.data);
|
|
390
422
|
}
|
|
391
423
|
}
|