@ccmsg/cli 0.12.0 → 0.14.0

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.
@@ -1,4 +1,5 @@
1
1
  import { realpathSync } from "node:fs";
2
+ import { realpath } from "node:fs/promises";
2
3
  import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
3
4
  import type { FileKind, Role, Sid } from "@ccmsg/protocol";
4
5
  import { OpError } from "../dispatch/index.ts";
@@ -69,11 +70,11 @@ export class Containment {
69
70
  constructor(private readonly source: RootsSource) {}
70
71
 
71
72
  /** A path named by kind, as an op's arguments give it. */
72
- locate(args: PathArgs, viewer: Viewer = {}): Located {
73
+ async locate(args: PathArgs, viewer: Viewer = {}): Promise<Located> {
73
74
  const roots = this.rootsFor(args.sid, viewer);
74
- const named = this.absolute(args, roots);
75
- const real = canonical(named);
76
- return { ...this.admit(args.kind, real, roots), named };
75
+ const named = await this.absolute(args, roots);
76
+ const real = await canonical(named);
77
+ return { ...(await this.admit(args.kind, real, roots)), named };
77
78
  }
78
79
 
79
80
  /** An absolute path with no kind: which surface admits it, if any.
@@ -81,7 +82,7 @@ export class Containment {
81
82
  * The surfaces are tried in the order the contract states, and the answer is
82
83
  * one value for every refusal — outside the allowlists, or simply not there —
83
84
  * so a caller cannot learn from it whether a path it may not read exists. */
84
- identify(sid: Sid, path: string, viewer: Viewer = {}): Located | undefined {
85
+ async identify(sid: Sid, path: string, viewer: Viewer = {}): Promise<Located | undefined> {
85
86
  let roots: SessionRoots;
86
87
  try {
87
88
  roots = this.rootsFor(sid, viewer);
@@ -90,10 +91,10 @@ export class Containment {
90
91
  }
91
92
  if (!isAbsolute(path)) return undefined;
92
93
  const named = resolve(path);
93
- const real = canonical(named);
94
+ const real = await canonical(named);
94
95
  for (const kind of KINDS) {
95
96
  try {
96
- return { ...this.admit(kind, real, roots), named };
97
+ return { ...(await this.admit(kind, real, roots)), named };
97
98
  } catch {
98
99
  // The next surface may admit it; running out of surfaces is the miss.
99
100
  }
@@ -106,15 +107,15 @@ export class Containment {
106
107
  * fixed to (DR-0019). A name that leaves the inbox is refused as unwritable
107
108
  * rather than as forbidden — the path is reachable, and only writing there
108
109
  * is not. */
109
- inbox(sid: Sid, path: string, viewer: Viewer = {}): Located {
110
+ async inbox(sid: Sid, path: string, viewer: Viewer = {}): Promise<Located> {
110
111
  const roots = this.rootsFor(sid, viewer);
111
112
  const cwd = roots.cwd;
112
113
  if (cwd === undefined || !isAbsolute(cwd)) {
113
114
  throw new OpError("path_forbidden", `${sid} states no working directory to write into`);
114
115
  }
115
- const base = canonical(cwd);
116
+ const base = await canonical(cwd);
116
117
  const named = resolve(base, path);
117
- const real = canonical(named);
118
+ const real = await canonical(named);
118
119
  const inbox = join(base, INBOX);
119
120
  if (!within(real, inbox)) {
120
121
  throw new OpError("path_not_writable", `only ${INBOX}/ takes a written file`);
@@ -123,7 +124,7 @@ export class Containment {
123
124
  }
124
125
 
125
126
  /** The directory a listing or a walk starts from. */
126
- root(args: DirArgs, viewer: Viewer = {}): Located {
127
+ root(args: DirArgs, viewer: Viewer = {}): Promise<Located> {
127
128
  return this.locate({ sid: args.sid, kind: args.kind, path: args.path ?? "" }, viewer);
128
129
  }
129
130
 
@@ -142,13 +143,13 @@ export class Containment {
142
143
  }
143
144
 
144
145
  /** Turn an op's `path` into an absolute one, in the shape its kind states. */
145
- private absolute(args: PathArgs, roots: SessionRoots): string {
146
+ private async absolute(args: PathArgs, roots: SessionRoots): Promise<string> {
146
147
  if (args.kind === "contained") {
147
148
  const root = roots.root;
148
149
  if (root === undefined || !isAbsolute(root)) {
149
150
  throw new OpError("path_forbidden", "this session states no root to be contained by");
150
151
  }
151
- return resolve(canonical(root), `.${sep}${args.path}`);
152
+ return resolve(await canonical(root), `.${sep}${args.path}`);
152
153
  }
153
154
  if (!isAbsolute(args.path)) {
154
155
  throw new OpError("path_forbidden", `a ${args.kind} path is absolute`);
@@ -159,17 +160,21 @@ export class Containment {
159
160
  /** Whether a resolved path is inside the surface it claims. The check runs on
160
161
  * what the filesystem resolved, so a symlink pointing out of a root is
161
162
  * refused however it was spelled (DR-0008 §3). */
162
- private admit(kind: FileKind, real: string, roots: SessionRoots): Omit<Located, "named"> {
163
+ private async admit(
164
+ kind: FileKind,
165
+ real: string,
166
+ roots: SessionRoots,
167
+ ): Promise<Omit<Located, "named">> {
163
168
  if (kind === "contained") {
164
- const root = roots.root === undefined ? undefined : canonical(roots.root);
169
+ const root = roots.root === undefined ? undefined : await canonical(roots.root);
165
170
  if (root === undefined || !within(real, root)) {
166
171
  throw new OpError("path_forbidden", "the path is outside the session's root");
167
172
  }
168
173
  return { kind, real, path: relativeTo(root, real) };
169
174
  }
170
175
  if (kind === "workspace") {
171
- const folder = roots.workspace_folders.find((each) => within(real, canonical(each)));
172
- if (folder === undefined) {
176
+ const folders = await Promise.all(roots.workspace_folders.map(canonical));
177
+ if (!folders.some((folder) => within(real, folder))) {
173
178
  throw new OpError("path_forbidden", "the path is in no workspace folder of this session");
174
179
  }
175
180
  return { kind, real, path: real };
@@ -179,7 +184,9 @@ export class Containment {
179
184
  // path spelled through a symlink and the same file spelled directly are
180
185
  // one entry, and a path since replaced by a symlink resolves elsewhere and
181
186
  // is no longer in the list.
182
- const named = roots.external_files.some((each) => canonical(each) === real);
187
+ const named = (await Promise.all(roots.external_files.map(canonical))).some(
188
+ (each) => each === real,
189
+ );
183
190
  if (!named) {
184
191
  throw new OpError("path_forbidden", "the path is not one this session's transcript named");
185
192
  }
@@ -234,7 +241,23 @@ const KINDS = ["contained", "workspace", "external"] as const;
234
241
  * last segment back, so a file about to be created is decided by where it would
235
242
  * land rather than being refused for not being there. A parent that does not
236
243
  * resolve either leaves the path as written, which no surface admits. */
237
- export function canonical(path: string): string {
244
+ export async function canonical(path: string): Promise<string> {
245
+ const absolute = resolve(path);
246
+ try {
247
+ return await realpath(absolute);
248
+ } catch {
249
+ const parent = dirname(absolute);
250
+ if (parent === absolute) return absolute;
251
+ try {
252
+ return join(await realpath(parent), basename(absolute));
253
+ } catch {
254
+ return absolute;
255
+ }
256
+ }
257
+ }
258
+
259
+ /** The synchronous form required while `session.status` states its value synchronously (DESIGN §6); an asynchronous topic value uses `canonical` as the single path. */
260
+ export function canonicalSync(path: string): string {
238
261
  const absolute = resolve(path);
239
262
  try {
240
263
  return realpathSync(absolute);
@@ -1,16 +1,14 @@
1
1
  import {
2
- closeSync,
3
- lstatSync,
4
- mkdirSync,
5
- openSync,
6
- readdirSync,
7
- readFileSync,
8
- readSync,
9
- renameSync,
10
- statSync,
11
- unlinkSync,
12
- writeFileSync,
13
- } from "node:fs";
2
+ lstat,
3
+ mkdir,
4
+ open,
5
+ readdir,
6
+ readFile,
7
+ rename,
8
+ stat,
9
+ unlink,
10
+ writeFile,
11
+ } from "node:fs/promises";
14
12
  import type { Dirent } from "node:fs";
15
13
  import { basename, dirname, join, relative } from "node:path";
16
14
  import type {
@@ -62,27 +60,34 @@ const FIND_VISITS = 20_000;
62
60
  * caller — the role dispatch states for a `scope: "role"` op, and the session
63
61
  * the connection speaks for — is handed over unread. */
64
62
  export function fileHandlers(paths: Containment) {
65
- const viewer = (input: HandlerInput): Viewer => ({ role: input.role, sid: input.identity?.sid });
63
+ const viewer = (input: HandlerInput): Viewer => ({
64
+ role: input.role,
65
+ sid: input.identity?.sid,
66
+ });
66
67
 
67
68
  return {
68
- "dir.list": (input: HandlerInput): DirListResult => {
69
+ "dir.list": async (input: HandlerInput): Promise<DirListResult> => {
69
70
  const args = input.args as unknown as DirListArgs;
70
- const at = paths.root(args, viewer(input));
71
- const stat = existing(at);
71
+ const at = await paths.root(args, viewer(input));
72
+ const stat = await existing(at);
72
73
  if (!stat.isDirectory()) throw new OpError("not_found", `${args.path ?? ""} is not a folder`);
73
- return { sid: args.sid, path: at.path, entries: entriesOf(at.real) };
74
+ return {
75
+ sid: args.sid,
76
+ path: at.path,
77
+ entries: await entriesOf(at.real),
78
+ };
74
79
  },
75
80
 
76
- "file.read": (input: HandlerInput): FileReadResult => {
81
+ "file.read": async (input: HandlerInput): Promise<FileReadResult> => {
77
82
  const args = input.args as unknown as FileReadArgs;
78
- const at = paths.locate(args, viewer(input));
79
- const stat = existing(at);
83
+ const at = await paths.locate(args, viewer(input));
84
+ const stat = await existing(at);
80
85
  if (!stat.isFile()) throw new OpError("not_found", `${args.path} is not a file`);
81
86
  // As much as the answer may carry and no more: a file larger than the
82
87
  // limit is answered from its head, so reading it whole would cost the
83
88
  // instance the whole of a file whose size is what the limit exists to
84
89
  // refuse.
85
- const head = bytesOf(at.real, READ_LIMIT);
90
+ const head = await bytesOf(at.real, READ_LIMIT);
86
91
  const binary = isBinary(head);
87
92
  return {
88
93
  sid: args.sid,
@@ -95,115 +100,126 @@ export function fileHandlers(paths: Containment) {
95
100
  };
96
101
  },
97
102
 
98
- "file.write": (input: HandlerInput): FileWriteResult => {
103
+ "file.write": async (input: HandlerInput): Promise<FileWriteResult> => {
99
104
  const args = input.args as unknown as FileWriteArgs;
100
- const at = paths.inbox(args.sid, args.path, viewer(input));
105
+ const at = await paths.inbox(args.sid, args.path, viewer(input));
101
106
  // The inbox takes new notes, so an existing name is refused rather than
102
107
  // replaced; the folder itself is made, since a repository that has never
103
108
  // had one is exactly where the first note goes (DR-0019 §2.1).
104
- mkdirSync(dirname(at.real), { recursive: true });
105
- create(at.real, args.content);
109
+ await mkdir(dirname(at.real), { recursive: true });
110
+ await create(at.real, args.content);
106
111
  return { sid: args.sid, path: at.path };
107
112
  },
108
113
 
109
- "file.create": (input: HandlerInput): FileCreateResult => {
114
+ "file.create": async (input: HandlerInput): Promise<FileCreateResult> => {
110
115
  const args = input.args as unknown as FileCreateArgs;
111
- const at = paths.locate(args, viewer(input));
116
+ const at = await paths.locate(args, viewer(input));
112
117
  const parent = dirname(at.real);
113
- if (!isDirectory(parent)) {
118
+ if (!(await isDirectory(parent))) {
114
119
  throw new OpError("not_found", `${args.path} has no folder to be created in`);
115
120
  }
116
- create(at.real, args.content);
121
+ await create(at.real, args.content);
117
122
  return { sid: args.sid, path: at.path };
118
123
  },
119
124
 
120
- "file.edit": (input: HandlerInput): FileEditResult => {
125
+ "file.edit": async (input: HandlerInput): Promise<FileEditResult> => {
121
126
  const args = input.args as unknown as FileEditArgs;
122
- const at = paths.locate(args, viewer(input));
123
- const stat = existing(at);
124
- if (!stat.isFile()) throw new OpError("not_found", `${args.path} is not a file`);
125
- if (isBinary(bytesOf(at.real, SNIFF))) {
127
+ const at = await paths.locate(args, viewer(input));
128
+ const before = await existing(at);
129
+ if (!before.isFile()) throw new OpError("not_found", `${args.path} is not a file`);
130
+ if (isBinary(await bytesOf(at.real, SNIFF))) {
126
131
  throw new OpError("not_a_text_file", `${args.path} holds binary content`);
127
132
  }
128
- if (mtimeOf(stat) !== args.expected_mtime_at || stat.size !== args.expected_size) {
133
+ if (mtimeOf(before) !== args.expected_mtime_at || before.size !== args.expected_size) {
129
134
  throw new OpError("file_conflict", `${args.path} changed since it was read`);
130
135
  }
131
- replace(at.real, args.content);
132
- const after = statSync(at.real);
133
- return { sid: args.sid, path: at.path, size: after.size, mtime_at: mtimeOf(after) };
136
+ await replace(at.real, args.content);
137
+ const after = await stat(at.real);
138
+ return {
139
+ sid: args.sid,
140
+ path: at.path,
141
+ size: after.size,
142
+ mtime_at: mtimeOf(after),
143
+ };
134
144
  },
135
145
 
136
- "file.delete": (input: HandlerInput): FileDeleteResult => {
146
+ "file.delete": async (input: HandlerInput): Promise<FileDeleteResult> => {
137
147
  const args = input.args as unknown as FileDeleteArgs;
138
- const at = paths.locate(args, viewer(input));
148
+ const at = await paths.locate(args, viewer(input));
139
149
  // What is unlinked is what is named, so this reads the name itself rather
140
150
  // than what it resolves to: a symlink is refused as the wrong kind of
141
151
  // thing instead of taking its target's answer. The resolved path is the
142
152
  // one containment admitted and would answer for the target, which is the
143
153
  // file a link inside the root could otherwise be pointed at.
144
- const stat = lstatOf(at.named);
154
+ const stat = await lstatOf(at.named);
145
155
  if (stat === undefined) throw new OpError("not_found", `${args.path} is not there`);
146
156
  if (!stat.isFile()) {
147
157
  throw new OpError("path_forbidden", `${args.path} is not a plain file`);
148
158
  }
149
- unlinkSync(at.named);
159
+ await unlink(at.named);
150
160
  return { sid: args.sid, path: at.path };
151
161
  },
152
162
 
153
- "file.find": (input: HandlerInput): FileFindResult => {
163
+ "file.find": async (input: HandlerInput): Promise<FileFindResult> => {
154
164
  const args = input.args as unknown as FileFindArgs;
155
- const at = paths.root(
156
- { sid: args.sid, kind: args.kind, ...(args.root === undefined ? {} : { path: args.root }) },
165
+ const at = await paths.root(
166
+ {
167
+ sid: args.sid,
168
+ kind: args.kind,
169
+ ...(args.root === undefined ? {} : { path: args.root }),
170
+ },
157
171
  viewer(input),
158
172
  );
159
173
  const terms = parseQuery(args.query);
160
174
  // A query with nothing to include matches nothing rather than the whole
161
175
  // tree, so a cleared search box costs no walk at all.
162
176
  if (terms.include.length === 0) return { sid: args.sid, hits: [], truncated: false };
163
- const walk = find(at, terms, args.respect_gitignore ?? true);
177
+ const walk = await find(at, terms, args.respect_gitignore ?? true);
164
178
  return { sid: args.sid, hits: walk.hits, truncated: walk.truncated };
165
179
  },
166
180
 
167
- "file.stat": (input: HandlerInput): FileStatResult => {
181
+ "file.stat": async (input: HandlerInput): Promise<FileStatResult> => {
168
182
  const args = input.args as unknown as FileStatArgs;
169
- const results = args.paths.map((path): FileStatEntry | null => {
170
- const at = paths.identify(args.sid, path, viewer(input));
171
- if (at === undefined || !isFile(at.real)) return null;
172
- return { kind: at.kind, path: at.path };
173
- });
183
+ const results = await Promise.all(
184
+ args.paths.map(async (path): Promise<FileStatEntry | null> => {
185
+ const at = await paths.identify(args.sid, path, viewer(input));
186
+ if (at === undefined || !(await isFile(at.real))) return null;
187
+ return { kind: at.kind, path: at.path };
188
+ }),
189
+ );
174
190
  return { results };
175
191
  },
176
192
  };
177
193
  }
178
194
 
179
195
  /** The file a located path names, or the contract's word for "not there". */
180
- function existing(at: Located) {
196
+ async function existing(at: Located) {
181
197
  try {
182
- return statSync(at.real);
198
+ return await stat(at.real);
183
199
  } catch {
184
200
  throw new OpError("not_found", `${at.path} is not there`);
185
201
  }
186
202
  }
187
203
 
188
- function lstatOf(path: string) {
204
+ async function lstatOf(path: string) {
189
205
  try {
190
- return lstatSync(path);
206
+ return await lstat(path);
191
207
  } catch {
192
208
  return undefined;
193
209
  }
194
210
  }
195
211
 
196
- function isFile(path: string): boolean {
212
+ async function isFile(path: string): Promise<boolean> {
197
213
  try {
198
- return statSync(path).isFile();
214
+ return (await stat(path)).isFile();
199
215
  } catch {
200
216
  return false;
201
217
  }
202
218
  }
203
219
 
204
- function isDirectory(path: string): boolean {
220
+ async function isDirectory(path: string): Promise<boolean> {
205
221
  try {
206
- return statSync(path).isDirectory();
222
+ return (await stat(path)).isDirectory();
207
223
  } catch {
208
224
  return false;
209
225
  }
@@ -216,14 +232,14 @@ function mtimeOf(stat: { mtimeMs: number }): Timestamp {
216
232
  }
217
233
 
218
234
  /** A file's leading bytes, at most `limit` of them. */
219
- function bytesOf(path: string, limit: number): Buffer {
220
- const fd = openSync(path, "r");
235
+ async function bytesOf(path: string, limit: number): Promise<Buffer> {
236
+ const handle = await open(path, "r");
221
237
  try {
222
238
  const buffer = Buffer.alloc(limit);
223
- const read = readSync(fd, buffer, 0, limit, 0);
224
- return buffer.subarray(0, read);
239
+ const { bytesRead } = await handle.read(buffer, 0, limit, 0);
240
+ return buffer.subarray(0, bytesRead);
225
241
  } finally {
226
- closeSync(fd);
242
+ await handle.close();
227
243
  }
228
244
  }
229
245
 
@@ -233,9 +249,9 @@ function isBinary(bytes: Buffer): boolean {
233
249
 
234
250
  /** Write a file that must not be there yet. The exclusive open is what decides
235
251
  * it: a check followed by a write would answer about the moment before. */
236
- function create(path: string, content: string): void {
252
+ async function create(path: string, content: string): Promise<void> {
237
253
  try {
238
- writeFileSync(path, content, { flag: "wx" });
254
+ await writeFile(path, content, { flag: "wx" });
239
255
  } catch (cause) {
240
256
  if ((cause as NodeJS.ErrnoException).code === "EEXIST") {
241
257
  throw new OpError("file_exists", `${basename(path)} is already there`);
@@ -247,39 +263,42 @@ function create(path: string, content: string): void {
247
263
  /** Replace a file's content whole. The write lands beside it and is renamed
248
264
  * over it, so a reader sees either the old file or the new one and never a
249
265
  * half-written one. */
250
- function replace(path: string, content: string): void {
266
+ async function replace(path: string, content: string): Promise<void> {
251
267
  const temporary = `${path}.ccmsg-${process.pid}-${Date.now()}`;
252
- writeFileSync(temporary, content);
268
+ await writeFile(temporary, content);
253
269
  try {
254
- renameSync(temporary, path);
270
+ await rename(temporary, path);
255
271
  } catch (cause) {
256
- unlinkSync(temporary);
272
+ await unlink(temporary);
257
273
  throw cause;
258
274
  }
259
275
  }
260
276
 
261
- function entriesOf(dir: string): DirEntry[] {
262
- return readdirSync(dir, { withFileTypes: true })
263
- .map((entry): DirEntry => {
264
- const type = entry.isSymbolicLink()
265
- ? "symlink"
266
- : entry.isDirectory()
267
- ? "dir"
268
- : entry.isFile()
269
- ? "file"
270
- : "other";
271
- // A symlink is reported as itself, so what is stated about it is the link
272
- // and never what it points at — including one pointing out of the root,
273
- // which is listed here and refuses to resolve everywhere else.
274
- const stat = type === "symlink" ? undefined : lstatOf(join(dir, entry.name));
275
- return {
276
- name: entry.name,
277
- type,
278
- ...(stat?.isFile() === true ? { size: stat.size } : {}),
279
- ...(stat === undefined ? {} : { mtime_at: mtimeOf(stat) }),
280
- };
281
- })
282
- .sort((a, b) => a.name.localeCompare(b.name));
277
+ async function entriesOf(dir: string): Promise<DirEntry[]> {
278
+ const entries = await readdir(dir, { withFileTypes: true });
279
+ return (
280
+ await Promise.all(
281
+ entries.map(async (entry): Promise<DirEntry> => {
282
+ const type = entry.isSymbolicLink()
283
+ ? "symlink"
284
+ : entry.isDirectory()
285
+ ? "dir"
286
+ : entry.isFile()
287
+ ? "file"
288
+ : "other";
289
+ // A symlink is reported as itself, so what is stated about it is the link
290
+ // and never what it points at including one pointing out of the root,
291
+ // which is listed here and refuses to resolve everywhere else.
292
+ const info = type === "symlink" ? undefined : await lstatOf(join(dir, entry.name));
293
+ return {
294
+ name: entry.name,
295
+ type,
296
+ ...(info?.isFile() === true ? { size: info.size } : {}),
297
+ ...(info === undefined ? {} : { mtime_at: mtimeOf(info) }),
298
+ };
299
+ }),
300
+ )
301
+ ).sort((a, b) => a.name.localeCompare(b.name));
283
302
  }
284
303
 
285
304
  interface Terms {
@@ -312,17 +331,17 @@ function matches(path: string, terms: Terms): boolean {
312
331
  * Both caps are reported the same way: the hits are the ones found, and
313
332
  * `truncated` says they are not the whole match set. Saying so is better than
314
333
  * implying these are all. */
315
- function find(at: Located, terms: Terms, respectGitignore: boolean) {
334
+ async function find(at: Located, terms: Terms, respectGitignore: boolean) {
316
335
  const hits: FileFindHit[] = [];
317
336
  let visits = 0;
318
337
  let truncated = false;
319
338
 
320
- const walk = (dir: string, ignored: Ignores): void => {
339
+ const walk = async (dir: string, ignored: Ignores): Promise<void> => {
321
340
  if (truncated) return;
322
- const here = respectGitignore ? ignored.descend(dir) : ignored;
341
+ const here = respectGitignore ? await ignored.descend(dir) : ignored;
323
342
  let entries: Dirent[];
324
343
  try {
325
- entries = readdirSync(dir, { withFileTypes: true });
344
+ entries = await readdir(dir, { withFileTypes: true });
326
345
  } catch {
327
346
  // A folder that cannot be read contributes nothing, and a walk that
328
347
  // stopped at one would answer less than it can.
@@ -349,13 +368,13 @@ function find(at: Located, terms: Terms, respectGitignore: boolean) {
349
368
  // Only real directories are descended: a symlink is answered as itself,
350
369
  // and following one would walk out of the root the walk is bounded by.
351
370
  if (isDir && !entry.isSymbolicLink()) {
352
- walk(full, here);
371
+ await walk(full, here);
353
372
  if (truncated) return;
354
373
  }
355
374
  }
356
375
  };
357
376
 
358
- walk(at.real, EMPTY_IGNORES);
377
+ await walk(at.real, EMPTY_IGNORES);
359
378
  return { hits, truncated };
360
379
  }
361
380
 
@@ -372,7 +391,7 @@ function find(at: Located, terms: Terms, respectGitignore: boolean) {
372
391
  * hiding one. */
373
392
  interface Ignores {
374
393
  hides(name: string, isDir: boolean): boolean;
375
- descend(dir: string): Ignores;
394
+ descend(dir: string): Promise<Ignores>;
376
395
  }
377
396
 
378
397
  const ALWAYS_HIDDEN = new Set([".git"]);
@@ -385,17 +404,17 @@ function makeIgnores(patterns: readonly RegExp[]): Ignores {
385
404
  if (ALWAYS_HIDDEN.has(name)) return true;
386
405
  return patterns.some((pattern) => pattern.test(name));
387
406
  },
388
- descend(dir) {
389
- const own = readIgnoreFile(join(dir, ".gitignore"));
407
+ async descend(dir) {
408
+ const own = await readIgnoreFile(join(dir, ".gitignore"));
390
409
  return own.length === 0 ? makeIgnores(patterns) : makeIgnores([...patterns, ...own]);
391
410
  },
392
411
  };
393
412
  }
394
413
 
395
- function readIgnoreFile(file: string): RegExp[] {
414
+ async function readIgnoreFile(file: string): Promise<RegExp[]> {
396
415
  let text: string;
397
416
  try {
398
- text = readFileSync(file, "utf8");
417
+ text = await readFile(file, "utf8");
399
418
  } catch {
400
419
  return [];
401
420
  }
Binary file