@nseng-ai/areg 0.1.1
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/package.json +29 -0
- package/src/cli.ts +109 -0
- package/src/context.ts +59 -0
- package/src/fake-gateways.ts +1009 -0
- package/src/gateways/command-constants.ts +1 -0
- package/src/gateways/errors.ts +5 -0
- package/src/gateways/fs-utils.ts +26 -0
- package/src/gateways/github-gateway.ts +111 -0
- package/src/gateways/host-gateway.ts +35 -0
- package/src/gateways/mutation-policy.ts +94 -0
- package/src/gateways/npx-skills-gateway.ts +53 -0
- package/src/gateways/project-fs.ts +320 -0
- package/src/gateways/project-gateway.ts +801 -0
- package/src/gateways/prompt-gateway.ts +21 -0
- package/src/gateways/skill-kind-classification.ts +39 -0
- package/src/gateways/skillx-workspace-gateway.ts +220 -0
- package/src/gateways.ts +361 -0
- package/src/index.ts +48 -0
- package/src/operations/check.ts +559 -0
- package/src/operations/doctor-skills-report.ts +109 -0
- package/src/operations/doctor-skills-severity.ts +9 -0
- package/src/operations/doctor-skills.ts +471 -0
- package/src/operations/file-state.ts +77 -0
- package/src/operations/frontmatter.ts +151 -0
- package/src/operations/init.ts +548 -0
- package/src/operations/lockfile.ts +107 -0
- package/src/operations/managed-markdown-block.ts +47 -0
- package/src/operations/pi-replacement.ts +33 -0
- package/src/operations/pi-settings.ts +60 -0
- package/src/operations/project-agents.ts +115 -0
- package/src/operations/project-inspection.ts +165 -0
- package/src/operations/project-mutations.ts +383 -0
- package/src/operations/project-resolution.ts +53 -0
- package/src/operations/skill-find.ts +295 -0
- package/src/operations/skill-kind-apply-plan.ts +531 -0
- package/src/operations/skill-kind-frontmatter.ts +55 -0
- package/src/operations/skill-kind-inference.ts +391 -0
- package/src/operations/skill-kind.ts +525 -0
- package/src/operations/skill-mirror-conventions.ts +126 -0
- package/src/operations/skillx.ts +305 -0
- package/src/operations/toml-section.ts +53 -0
- package/src/operations/update-skills.ts +325 -0
- package/src/real-gateways.ts +6 -0
- package/src/skill-lookup.ts +254 -0
- package/src/sort.ts +7 -0
|
@@ -0,0 +1,801 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import { lstat, mkdir, readdir, realpath, rm, rmdir, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { RealGitGateway } from "@nseng-ai/capability-kit/git";
|
|
6
|
+
import type { GitGateway } from "@nseng-ai/capability-kit/git";
|
|
7
|
+
import { visibleCommandBackedReplacementSurfaces } from "@nseng-ai/command-backed-skill-registry";
|
|
8
|
+
import { NodeCommandExecApi } from "@nseng-ai/foundation/exec";
|
|
9
|
+
import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
|
|
10
|
+
import {
|
|
11
|
+
SKILL_LOOKUP_ROOT_DESCRIPTORS,
|
|
12
|
+
skillLookupBaseRelativePath,
|
|
13
|
+
skillLookupDescriptorForSourceType,
|
|
14
|
+
skillLookupFileRelativePath,
|
|
15
|
+
type SkillLookupSourceType,
|
|
16
|
+
} from "../skill-lookup.ts";
|
|
17
|
+
|
|
18
|
+
import { AREG_SKILL_KIND_ROOT_DESCRIPTORS, skillKindDescriptorForSourceType } from "../gateways.ts";
|
|
19
|
+
import type {
|
|
20
|
+
AregCheckPairingDirectory,
|
|
21
|
+
AregCheckSkillInspection,
|
|
22
|
+
AregErrorInfo,
|
|
23
|
+
AregPiSkillInventoryInspection,
|
|
24
|
+
AregProjectFileDeleteRequest,
|
|
25
|
+
AregProjectGateway,
|
|
26
|
+
AregProjectMutationResult,
|
|
27
|
+
AregProjectRemoveEmptyDirRequest,
|
|
28
|
+
AregProjectRemoveEmptyDirResult,
|
|
29
|
+
AregProjectSymlinkDeleteRequest,
|
|
30
|
+
AregProjectTextWriteRequest,
|
|
31
|
+
AregSkillFindRootsInspection,
|
|
32
|
+
AregSkillFindSkillInspection,
|
|
33
|
+
AregSkillInspectionRequest,
|
|
34
|
+
AregSkillKindResolveRequest,
|
|
35
|
+
AregSkillKindResolveResult,
|
|
36
|
+
AregSkillKindSkillInspection,
|
|
37
|
+
AregTextFileState,
|
|
38
|
+
} from "../gateways.ts";
|
|
39
|
+
import { sortStrings } from "../sort.ts";
|
|
40
|
+
import { errorInfo } from "./errors.ts";
|
|
41
|
+
import { getAregProjectMutationPolicyDescriptor } from "./mutation-policy.ts";
|
|
42
|
+
import {
|
|
43
|
+
inspectPath,
|
|
44
|
+
inspectTextFile,
|
|
45
|
+
isNodeErrorCode,
|
|
46
|
+
resolveAllowedWriteTarget,
|
|
47
|
+
resolveExistingDirectory,
|
|
48
|
+
toProjectPath,
|
|
49
|
+
validateSkillKindDeleteSymlinkTarget,
|
|
50
|
+
validateSkillKindDeleteTarget,
|
|
51
|
+
validateSkillKindRemoveDirTarget,
|
|
52
|
+
validateWriteTarget,
|
|
53
|
+
} from "./project-fs.ts";
|
|
54
|
+
import { classifyResolvedSkillKindInspection } from "./skill-kind-classification.ts";
|
|
55
|
+
|
|
56
|
+
const PI_GENERIC_REPLACEMENT_ADAPTER_RELATIVE_PATH = ".pi/extensions/backing-skill-commands.ts";
|
|
57
|
+
const PI_GENERIC_REPLACEMENT_PACKAGE_MODULE_RELATIVE_PATH =
|
|
58
|
+
"ts/packages/internal/pi-tools/src/backing-skill-commands/extension.ts";
|
|
59
|
+
// AREG reads the composed command-backed skill catalog without importing
|
|
60
|
+
// the project-local Pi extension entrypoint.
|
|
61
|
+
const AREG_VISIBLE_REPLACEMENT_SURFACES = visibleCommandBackedReplacementSurfaces();
|
|
62
|
+
|
|
63
|
+
export class RealAregProjectGateway implements AregProjectGateway {
|
|
64
|
+
private readonly git: GitGateway;
|
|
65
|
+
|
|
66
|
+
constructor(options: { git?: GitGateway } = {}) {
|
|
67
|
+
this.git = options.git ?? new RealGitGateway(new NodeCommandExecApi());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async inspectProjectBase(request: { cwd: string; projectPath: string; env: NodeJS.ProcessEnv }) {
|
|
71
|
+
const projectDir = path.resolve(request.cwd, request.projectPath);
|
|
72
|
+
return {
|
|
73
|
+
projectDir,
|
|
74
|
+
projectPathState: await inspectPath(projectDir),
|
|
75
|
+
lockfile: await inspectTextFile(path.join(projectDir, "skills-lock.json")),
|
|
76
|
+
nsToml: await inspectTextFile(path.join(projectDir, "ns.toml")),
|
|
77
|
+
aregJson: await inspectTextFile(path.join(projectDir, "areg.json")),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async inspectInstructionFiles(request: { projectDir: string; env: NodeJS.ProcessEnv }) {
|
|
82
|
+
return {
|
|
83
|
+
agentsMd: await inspectTextFile(path.join(request.projectDir, "AGENTS.md")),
|
|
84
|
+
claudeMd: await inspectTextFile(path.join(request.projectDir, "CLAUDE.md")),
|
|
85
|
+
claudeDir: await inspectPath(path.join(request.projectDir, ".claude")),
|
|
86
|
+
claudeSettings: await inspectTextFile(
|
|
87
|
+
path.join(request.projectDir, ".claude", "settings.local.json"),
|
|
88
|
+
),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async inspectPiArtifacts(request: { projectDir: string; env: NodeJS.ProcessEnv }) {
|
|
93
|
+
return {
|
|
94
|
+
piDir: await inspectPath(path.join(request.projectDir, ".pi")),
|
|
95
|
+
piSettings: await inspectTextFile(path.join(request.projectDir, ".pi", "settings.json")),
|
|
96
|
+
replacement: await inspectReplacementSurfaces(request.projectDir),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async inspectPiSkillInventory(request: {
|
|
101
|
+
projectDir: string;
|
|
102
|
+
env: NodeJS.ProcessEnv;
|
|
103
|
+
}): Promise<AregPiSkillInventoryInspection> {
|
|
104
|
+
return {
|
|
105
|
+
skillNames: await listPiRepoFallbackSkillNames(request.projectDir),
|
|
106
|
+
isApproximation: true,
|
|
107
|
+
source: "repo-fallback-resolvable-skill-roots",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async inspectSkillNameInventory(request: { projectDir: string; env: NodeJS.ProcessEnv }) {
|
|
112
|
+
return {
|
|
113
|
+
skillsDirectoryNames: await listChildNamesForSourceType(request.projectDir, "repo"),
|
|
114
|
+
agentsSkillNames: await listChildNamesForSourceType(request.projectDir, "vendored"),
|
|
115
|
+
claudeSkillNames: await listChildNamesForSourceType(request.projectDir, "claude"),
|
|
116
|
+
skillKindNames: await listSkillKindNames(request.projectDir),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async inspectSkillFindRoots(request: {
|
|
121
|
+
projectDir: string;
|
|
122
|
+
env: NodeJS.ProcessEnv;
|
|
123
|
+
}): Promise<AregSkillFindRootsInspection> {
|
|
124
|
+
return { skills: await inspectSkillFindRoots(request.projectDir) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async inspectCheckSkill(request: AregSkillInspectionRequest): Promise<AregCheckSkillInspection> {
|
|
128
|
+
return inspectCheckSkill(request.projectDir, request.skillName);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async inspectSkillKindSkill(
|
|
132
|
+
request: AregSkillInspectionRequest,
|
|
133
|
+
): Promise<AregSkillKindSkillInspection> {
|
|
134
|
+
return inspectSkillKindSkill(request.projectDir, request.skillName);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async inspectPairingDirectories(request: {
|
|
138
|
+
projectDir: string;
|
|
139
|
+
env: NodeJS.ProcessEnv;
|
|
140
|
+
}): Promise<readonly AregCheckPairingDirectory[]> {
|
|
141
|
+
return await inspectPairingDirectories(request.projectDir);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async readLocallyExcludedSkillNames(request: {
|
|
145
|
+
projectDir: string;
|
|
146
|
+
env: NodeJS.ProcessEnv;
|
|
147
|
+
}): Promise<readonly string[]> {
|
|
148
|
+
return await readLocallyExcludedSkillNames({ projectDir: request.projectDir, git: this.git });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async resolveSkillKindSpec(
|
|
152
|
+
request: AregSkillKindResolveRequest,
|
|
153
|
+
): Promise<AregSkillKindResolveResult> {
|
|
154
|
+
const resolved = await resolveSkillKindSpec(request);
|
|
155
|
+
if (resolved.type === "error") return resolved;
|
|
156
|
+
const inspected = await inspectSkillKindSkill(request.projectDir, resolved.skillName);
|
|
157
|
+
return classifyResolvedSkillKindInspection({
|
|
158
|
+
spec: request.spec,
|
|
159
|
+
skillName: resolved.skillName,
|
|
160
|
+
inspection: inspected,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async preflightWriteTextFile(
|
|
165
|
+
request: AregProjectTextWriteRequest,
|
|
166
|
+
): Promise<AregProjectMutationResult> {
|
|
167
|
+
const target = await resolveWriteTextFileTarget(request);
|
|
168
|
+
return target.type === "error" ? { ok: false, error: target.error } : { ok: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async preflightDeleteFile(
|
|
172
|
+
request: AregProjectFileDeleteRequest,
|
|
173
|
+
): Promise<AregProjectMutationResult> {
|
|
174
|
+
const target = await resolveDeleteFileTarget(request);
|
|
175
|
+
return target.type === "error" ? { ok: false, error: target.error } : { ok: true };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async preflightDeleteSymlink(
|
|
179
|
+
request: AregProjectSymlinkDeleteRequest,
|
|
180
|
+
): Promise<AregProjectMutationResult> {
|
|
181
|
+
const target = await resolveDeleteSymlinkTarget(request);
|
|
182
|
+
return target.type === "error" ? { ok: false, error: target.error } : { ok: true };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async preflightRemoveEmptyDir(
|
|
186
|
+
request: AregProjectRemoveEmptyDirRequest,
|
|
187
|
+
): Promise<AregProjectMutationResult> {
|
|
188
|
+
const target = await resolveRemoveEmptyDirTarget(request);
|
|
189
|
+
return target.type === "error" ? { ok: false, error: target.error } : { ok: true };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async writeTextFile(request: AregProjectTextWriteRequest): Promise<AregProjectMutationResult> {
|
|
193
|
+
const target = await resolveWriteTextFileTarget(request);
|
|
194
|
+
if (target.type === "error") return { ok: false, error: target.error };
|
|
195
|
+
const policyDescriptor = getAregProjectMutationPolicyDescriptor(request.policy);
|
|
196
|
+
if (request.createParent) {
|
|
197
|
+
try {
|
|
198
|
+
await mkdir(path.dirname(target.value), { recursive: true });
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
error: errorInfo(
|
|
203
|
+
policyDescriptor.parentCreateFailedCode,
|
|
204
|
+
`Failed to create ${path.dirname(target.value)}: ${formatErrorMessage(error)}`,
|
|
205
|
+
),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const revalidation = await validateWriteTarget({
|
|
209
|
+
policy: request.policy,
|
|
210
|
+
target: target.value,
|
|
211
|
+
projectRoot: target.projectRoot,
|
|
212
|
+
shouldCreateParent: request.createParent,
|
|
213
|
+
description: request.description,
|
|
214
|
+
});
|
|
215
|
+
if (!revalidation.ok) return revalidation;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
await writeFile(target.value, request.content, "utf8");
|
|
219
|
+
return { ok: true };
|
|
220
|
+
} catch (error) {
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
error: errorInfo(
|
|
224
|
+
policyDescriptor.writeFailedCode,
|
|
225
|
+
`Failed to write ${request.description} at ${target.value}: ${formatErrorMessage(error)}`,
|
|
226
|
+
),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async deleteFile(request: AregProjectFileDeleteRequest): Promise<AregProjectMutationResult> {
|
|
232
|
+
const target = await resolveDeleteFileTarget(request);
|
|
233
|
+
if (target.type === "error") return { ok: false, error: target.error };
|
|
234
|
+
try {
|
|
235
|
+
await rm(target.value);
|
|
236
|
+
return { ok: true };
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return {
|
|
239
|
+
ok: false,
|
|
240
|
+
error: errorInfo(
|
|
241
|
+
"skill-kind-delete-failed",
|
|
242
|
+
`Failed to delete ${request.description} at ${target.value}: ${formatErrorMessage(error)}`,
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async deleteSymlink(
|
|
249
|
+
request: AregProjectSymlinkDeleteRequest,
|
|
250
|
+
): Promise<AregProjectMutationResult> {
|
|
251
|
+
const target = await resolveDeleteSymlinkTarget(request);
|
|
252
|
+
if (target.type === "error") return { ok: false, error: target.error };
|
|
253
|
+
try {
|
|
254
|
+
await unlink(target.value);
|
|
255
|
+
return { ok: true };
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
error: errorInfo(
|
|
260
|
+
"skill-kind-delete-symlink-failed",
|
|
261
|
+
`Failed to delete ${request.description} at ${target.value}: ${formatErrorMessage(error)}`,
|
|
262
|
+
),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async removeEmptyDir(
|
|
268
|
+
request: AregProjectRemoveEmptyDirRequest,
|
|
269
|
+
): Promise<AregProjectRemoveEmptyDirResult> {
|
|
270
|
+
const target = await resolveRemoveEmptyDirTarget(request);
|
|
271
|
+
if (target.type === "error") return { ok: false, error: target.error };
|
|
272
|
+
if (!target.exists) return { ok: true, removed: false };
|
|
273
|
+
try {
|
|
274
|
+
await rmdir(target.value);
|
|
275
|
+
return { ok: true, removed: true };
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (isNodeErrorCode(error, "ENOTEMPTY")) return { ok: true, removed: false };
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
error: errorInfo(
|
|
281
|
+
"skill-kind-remove-dir-failed",
|
|
282
|
+
`Failed to remove ${request.description} at ${target.value}: ${formatErrorMessage(error)}`,
|
|
283
|
+
),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function resolveWriteTextFileTarget(
|
|
290
|
+
request: AregProjectTextWriteRequest,
|
|
291
|
+
): Promise<
|
|
292
|
+
{ type: "ok"; value: string; projectRoot: string } | { type: "error"; error: AregErrorInfo }
|
|
293
|
+
> {
|
|
294
|
+
const target = await resolveProjectMutationTarget(request);
|
|
295
|
+
if (target.type === "error") return target;
|
|
296
|
+
const validation = await validateWriteTarget({
|
|
297
|
+
policy: request.policy,
|
|
298
|
+
target: target.value,
|
|
299
|
+
projectRoot: target.projectRoot,
|
|
300
|
+
shouldCreateParent: request.createParent,
|
|
301
|
+
description: request.description,
|
|
302
|
+
});
|
|
303
|
+
if (!validation.ok) return { type: "error", error: validation.error };
|
|
304
|
+
return target;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function resolveDeleteFileTarget(
|
|
308
|
+
request: AregProjectFileDeleteRequest,
|
|
309
|
+
): Promise<{ type: "ok"; value: string } | { type: "error"; error: AregErrorInfo }> {
|
|
310
|
+
const target = await resolveProjectMutationTarget(request);
|
|
311
|
+
if (target.type === "error") return target;
|
|
312
|
+
const validation = await validateSkillKindDeleteTarget(
|
|
313
|
+
target.value,
|
|
314
|
+
target.projectRoot,
|
|
315
|
+
request.description,
|
|
316
|
+
);
|
|
317
|
+
if (!validation.ok) return { type: "error", error: validation.error };
|
|
318
|
+
return { type: "ok", value: target.value };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function resolveDeleteSymlinkTarget(
|
|
322
|
+
request: AregProjectSymlinkDeleteRequest,
|
|
323
|
+
): Promise<{ type: "ok"; value: string } | { type: "error"; error: AregErrorInfo }> {
|
|
324
|
+
const projectRoot = await resolveExistingDirectory(request.projectDir, "project root");
|
|
325
|
+
if (projectRoot.type === "error") return { type: "error", error: projectRoot.error };
|
|
326
|
+
if (path.isAbsolute(request.relativePath) || request.relativePath.split("/").includes("..")) {
|
|
327
|
+
return {
|
|
328
|
+
type: "error",
|
|
329
|
+
error: errorInfo(
|
|
330
|
+
"skill-kind-delete-symlink-refused",
|
|
331
|
+
`Refusing to delete ${request.description}: unsafe target ${request.relativePath}.`,
|
|
332
|
+
),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const target = toProjectPath(projectRoot.value, request.relativePath);
|
|
336
|
+
const validation = await validateSkillKindDeleteSymlinkTarget(
|
|
337
|
+
target,
|
|
338
|
+
projectRoot.value,
|
|
339
|
+
request.relativePath,
|
|
340
|
+
request.description,
|
|
341
|
+
);
|
|
342
|
+
if (!validation.ok) return { type: "error", error: validation.error };
|
|
343
|
+
return { type: "ok", value: target };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function resolveRemoveEmptyDirTarget(
|
|
347
|
+
request: AregProjectRemoveEmptyDirRequest,
|
|
348
|
+
): Promise<
|
|
349
|
+
{ type: "ok"; value: string; exists: boolean } | { type: "error"; error: AregErrorInfo }
|
|
350
|
+
> {
|
|
351
|
+
const target = await resolveProjectMutationTarget(request);
|
|
352
|
+
if (target.type === "error") return target;
|
|
353
|
+
const validation = await validateSkillKindRemoveDirTarget(
|
|
354
|
+
target.value,
|
|
355
|
+
target.projectRoot,
|
|
356
|
+
request.description,
|
|
357
|
+
);
|
|
358
|
+
if (!validation.ok) return { type: "error", error: validation.error };
|
|
359
|
+
return { type: "ok", value: target.value, exists: validation.exists };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async function resolveProjectMutationTarget(request: {
|
|
363
|
+
projectDir: string;
|
|
364
|
+
policy: AregProjectTextWriteRequest["policy"];
|
|
365
|
+
relativePath: string;
|
|
366
|
+
description: string;
|
|
367
|
+
}): Promise<
|
|
368
|
+
{ type: "ok"; value: string; projectRoot: string } | { type: "error"; error: AregErrorInfo }
|
|
369
|
+
> {
|
|
370
|
+
const projectRoot = await resolveExistingDirectory(request.projectDir, "project root");
|
|
371
|
+
if (projectRoot.type === "error") return { type: "error", error: projectRoot.error };
|
|
372
|
+
const target = resolveAllowedWriteTarget({
|
|
373
|
+
policy: request.policy,
|
|
374
|
+
projectRoot: projectRoot.value,
|
|
375
|
+
relativePath: request.relativePath,
|
|
376
|
+
description: request.description,
|
|
377
|
+
});
|
|
378
|
+
if (target.type === "error") return { type: "error", error: target.error };
|
|
379
|
+
return { type: "ok", value: target.value, projectRoot: projectRoot.value };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function inspectCheckSkill(
|
|
383
|
+
projectDir: string,
|
|
384
|
+
name: string,
|
|
385
|
+
): Promise<AregCheckSkillInspection> {
|
|
386
|
+
const repoDescriptor = skillLookupDescriptorForSourceType("repo");
|
|
387
|
+
const vendoredDescriptor = skillLookupDescriptorForSourceType("vendored");
|
|
388
|
+
const claudeDescriptor = skillLookupDescriptorForSourceType("claude");
|
|
389
|
+
const repoBase = toProjectPath(
|
|
390
|
+
projectDir,
|
|
391
|
+
skillLookupBaseRelativePath(repoDescriptor.root, name),
|
|
392
|
+
);
|
|
393
|
+
const vendoredBase = toProjectPath(
|
|
394
|
+
projectDir,
|
|
395
|
+
skillLookupBaseRelativePath(vendoredDescriptor.root, name),
|
|
396
|
+
);
|
|
397
|
+
const localSkillMd = await inspectTextFile(
|
|
398
|
+
toProjectPath(projectDir, skillLookupFileRelativePath(repoDescriptor.root, name)),
|
|
399
|
+
);
|
|
400
|
+
const remoteSkillMd = await inspectTextFile(
|
|
401
|
+
toProjectPath(projectDir, skillLookupFileRelativePath(vendoredDescriptor.root, name)),
|
|
402
|
+
);
|
|
403
|
+
const policyRoot = localSkillMd.type === "file" ? repoDescriptor.root : vendoredDescriptor.root;
|
|
404
|
+
return {
|
|
405
|
+
name,
|
|
406
|
+
skillsPath: await inspectPath(repoBase),
|
|
407
|
+
agentsPath: await inspectPath(vendoredBase),
|
|
408
|
+
claudePath: await inspectPath(
|
|
409
|
+
toProjectPath(projectDir, skillLookupBaseRelativePath(claudeDescriptor.root, name)),
|
|
410
|
+
),
|
|
411
|
+
localSkillMd,
|
|
412
|
+
remoteSkillMd,
|
|
413
|
+
openaiPolicy: await inspectTextFile(
|
|
414
|
+
toProjectPath(
|
|
415
|
+
projectDir,
|
|
416
|
+
`${skillLookupBaseRelativePath(policyRoot, name)}/agents/openai.yaml`,
|
|
417
|
+
),
|
|
418
|
+
),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function listPiRepoFallbackSkillNames(projectDir: string): Promise<string[]> {
|
|
423
|
+
return collectSortedUniqueNames(
|
|
424
|
+
await Promise.all(
|
|
425
|
+
AREG_SKILL_KIND_ROOT_DESCRIPTORS.map((descriptor) =>
|
|
426
|
+
listSkillsWithSkillMd(toProjectPath(projectDir, descriptor.root), {
|
|
427
|
+
includeSymlinks: descriptor.sourceType !== "vendored",
|
|
428
|
+
}),
|
|
429
|
+
),
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function listSkillKindNames(projectDir: string): Promise<string[]> {
|
|
435
|
+
return collectSortedUniqueNames(
|
|
436
|
+
await Promise.all(
|
|
437
|
+
AREG_SKILL_KIND_ROOT_DESCRIPTORS.map((descriptor) =>
|
|
438
|
+
descriptor.sourceType === "repo"
|
|
439
|
+
? listFirstPartySkillKindNames(projectDir)
|
|
440
|
+
: listVendoredSkillKindNames(projectDir),
|
|
441
|
+
),
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function collectSortedUniqueNames(namesByRoot: readonly (readonly string[])[]): string[] {
|
|
447
|
+
return sortStrings([...new Set(namesByRoot.flat())]);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function listFirstPartySkillKindNames(projectDir: string): Promise<string[]> {
|
|
451
|
+
const descriptor = skillKindDescriptorForSourceType("repo");
|
|
452
|
+
const skillsRoot = toProjectPath(projectDir, descriptor.root);
|
|
453
|
+
return (
|
|
454
|
+
await scanSkillRootEntries(skillsRoot, {
|
|
455
|
+
includeSymlinks: true,
|
|
456
|
+
keepEntry: (entry) =>
|
|
457
|
+
entry.dirent.isDirectory() ||
|
|
458
|
+
entry.dirent.isSymbolicLink() ||
|
|
459
|
+
entry.skillMd.type !== "missing",
|
|
460
|
+
})
|
|
461
|
+
).map((entry) => entry.name);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function listSkillsWithSkillMd(
|
|
465
|
+
root: string,
|
|
466
|
+
options: ScanSkillRootOptions,
|
|
467
|
+
): Promise<string[]> {
|
|
468
|
+
return (
|
|
469
|
+
await scanSkillRootEntries(root, {
|
|
470
|
+
...options,
|
|
471
|
+
keepEntry: (entry) => entry.skillMd.type === "file",
|
|
472
|
+
})
|
|
473
|
+
).map((entry) => entry.name);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function listVendoredSkillKindNames(projectDir: string): Promise<string[]> {
|
|
477
|
+
const descriptor = skillKindDescriptorForSourceType("vendored");
|
|
478
|
+
const agentsRoot = toProjectPath(projectDir, descriptor.root);
|
|
479
|
+
return (
|
|
480
|
+
await scanSkillRootEntries(agentsRoot, {
|
|
481
|
+
includeSymlinks: false,
|
|
482
|
+
keepEntry: (entry) => entry.dirent.isDirectory() || entry.skillMd.type !== "missing",
|
|
483
|
+
})
|
|
484
|
+
).map((entry) => entry.name);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function inspectSkillFindRoots(projectDir: string): Promise<AregSkillFindSkillInspection[]> {
|
|
488
|
+
const skills: AregSkillFindSkillInspection[] = [];
|
|
489
|
+
for (const root of SKILL_LOOKUP_ROOT_DESCRIPTORS) {
|
|
490
|
+
const rootPath = toProjectPath(projectDir, root.root);
|
|
491
|
+
const entries = await scanSkillRootEntries(rootPath, {
|
|
492
|
+
includeSymlinks: root.sourceType !== "vendored",
|
|
493
|
+
keepEntry: (entry) =>
|
|
494
|
+
(entry.dirent.isDirectory() || entry.dirent.isSymbolicLink()) &&
|
|
495
|
+
entry.skillMd.type === "file",
|
|
496
|
+
});
|
|
497
|
+
for (const entry of entries) {
|
|
498
|
+
const baseRelativePath = skillLookupBaseRelativePath(root.root, entry.name);
|
|
499
|
+
const basePath = path.join(rootPath, entry.name);
|
|
500
|
+
skills.push({
|
|
501
|
+
name: entry.name,
|
|
502
|
+
root: root.root,
|
|
503
|
+
sourceType: root.sourceType,
|
|
504
|
+
baseRelativePath,
|
|
505
|
+
skillDir: await inspectPath(basePath),
|
|
506
|
+
skillMd: entry.skillMd,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return skills;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
interface ScanSkillRootOptions {
|
|
514
|
+
includeSymlinks: boolean;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
interface ScannedSkillRootEntry {
|
|
518
|
+
name: string;
|
|
519
|
+
dirent: Dirent;
|
|
520
|
+
skillMd: AregTextFileState;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
async function scanSkillRootEntries(
|
|
524
|
+
root: string,
|
|
525
|
+
options: ScanSkillRootOptions & {
|
|
526
|
+
keepEntry: (entry: ScannedSkillRootEntry) => boolean;
|
|
527
|
+
},
|
|
528
|
+
): Promise<ScannedSkillRootEntry[]> {
|
|
529
|
+
try {
|
|
530
|
+
const rootInfo = await lstat(root);
|
|
531
|
+
if (!rootInfo.isDirectory()) return [];
|
|
532
|
+
const dirents = await readdir(root, { withFileTypes: true });
|
|
533
|
+
const entries: ScannedSkillRootEntry[] = [];
|
|
534
|
+
for (const dirent of dirents) {
|
|
535
|
+
if (dirent.name === ".DS_Store") continue;
|
|
536
|
+
if (!options.includeSymlinks && dirent.isSymbolicLink()) continue;
|
|
537
|
+
const entry = {
|
|
538
|
+
name: dirent.name,
|
|
539
|
+
dirent,
|
|
540
|
+
skillMd: await inspectTextFile(path.join(root, dirent.name, "SKILL.md")),
|
|
541
|
+
};
|
|
542
|
+
if (options.keepEntry(entry)) entries.push(entry);
|
|
543
|
+
}
|
|
544
|
+
return entries;
|
|
545
|
+
} catch {
|
|
546
|
+
// Skill root inventory is best-effort: absent or unreadable roots behave like empty
|
|
547
|
+
// roots so diagnostics can continue from the remaining registry sources.
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
async function inspectSkillKindSkill(
|
|
553
|
+
projectDir: string,
|
|
554
|
+
name: string,
|
|
555
|
+
): Promise<AregSkillKindSkillInspection> {
|
|
556
|
+
const repoDescriptor = skillKindDescriptorForSourceType("repo");
|
|
557
|
+
const repoBase = toProjectPath(
|
|
558
|
+
projectDir,
|
|
559
|
+
skillLookupBaseRelativePath(repoDescriptor.root, name),
|
|
560
|
+
);
|
|
561
|
+
const repoDir = await inspectPath(repoBase);
|
|
562
|
+
const repoSkillMd = await inspectTextFile(
|
|
563
|
+
toProjectPath(projectDir, skillLookupFileRelativePath(repoDescriptor.root, name)),
|
|
564
|
+
);
|
|
565
|
+
const agentsPath = await inspectPath(toProjectPath(projectDir, `.agents/skills/${name}`));
|
|
566
|
+
const claudePath = await inspectPath(toProjectPath(projectDir, `.claude/skills/${name}`));
|
|
567
|
+
if (repoDir.type !== "missing" || repoSkillMd.type !== "missing") {
|
|
568
|
+
return {
|
|
569
|
+
name,
|
|
570
|
+
sourceType: repoDescriptor.sourceType,
|
|
571
|
+
baseRelativePath: skillLookupBaseRelativePath(repoDescriptor.root, name),
|
|
572
|
+
skillDir: repoDir,
|
|
573
|
+
skillMd: repoSkillMd,
|
|
574
|
+
openaiPolicy: await inspectTextFile(path.join(repoBase, "agents", "openai.yaml")),
|
|
575
|
+
agentsPath,
|
|
576
|
+
claudePath,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const vendoredDescriptor = skillKindDescriptorForSourceType("vendored");
|
|
580
|
+
const vendoredBase = toProjectPath(
|
|
581
|
+
projectDir,
|
|
582
|
+
skillLookupBaseRelativePath(vendoredDescriptor.root, name),
|
|
583
|
+
);
|
|
584
|
+
return {
|
|
585
|
+
name,
|
|
586
|
+
sourceType: vendoredDescriptor.sourceType,
|
|
587
|
+
baseRelativePath: skillLookupBaseRelativePath(vendoredDescriptor.root, name),
|
|
588
|
+
skillDir: await inspectPath(vendoredBase),
|
|
589
|
+
skillMd: await inspectTextFile(
|
|
590
|
+
toProjectPath(projectDir, skillLookupFileRelativePath(vendoredDescriptor.root, name)),
|
|
591
|
+
),
|
|
592
|
+
openaiPolicy: await inspectTextFile(path.join(vendoredBase, "agents", "openai.yaml")),
|
|
593
|
+
agentsPath,
|
|
594
|
+
claudePath,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function resolveSkillKindSpec(
|
|
599
|
+
request: AregSkillKindResolveRequest,
|
|
600
|
+
): Promise<AregSkillKindResolveResult> {
|
|
601
|
+
if (!isPathLikeSkillSpec(request.spec)) return { type: "ok", skillName: request.spec };
|
|
602
|
+
const candidate = path.resolve(request.cwd, request.spec);
|
|
603
|
+
const projectDir = await realpath(request.projectDir);
|
|
604
|
+
const canonical = await canonicalSkillKindPath(projectDir, candidate);
|
|
605
|
+
if (canonical.type === "ok") return canonical;
|
|
606
|
+
if (canonical.error.code === "skill-kind-outside-managed-skills") {
|
|
607
|
+
return {
|
|
608
|
+
type: "error",
|
|
609
|
+
error: errorInfo(
|
|
610
|
+
"skill-kind-non-managed-skill",
|
|
611
|
+
`Skill spec does not resolve to a managed skill: ${request.spec}`,
|
|
612
|
+
),
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return canonical;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async function canonicalSkillKindPath(
|
|
619
|
+
projectDir: string,
|
|
620
|
+
candidate: string,
|
|
621
|
+
): Promise<AregSkillKindResolveResult> {
|
|
622
|
+
const direct = classifyCanonicalSkillPath(projectDir, candidate);
|
|
623
|
+
if (direct.type === "ok") return direct;
|
|
624
|
+
if (direct.error.code === "skill-kind-nested-spec") return direct;
|
|
625
|
+
try {
|
|
626
|
+
const resolved = await realpath(candidate);
|
|
627
|
+
return classifyCanonicalSkillPath(projectDir, resolved);
|
|
628
|
+
} catch (error) {
|
|
629
|
+
if (isNodeErrorCode(error, "ENOENT"))
|
|
630
|
+
return {
|
|
631
|
+
type: "error",
|
|
632
|
+
error: errorInfo("skill-kind-missing-spec", `Skill path does not exist: ${candidate}`),
|
|
633
|
+
};
|
|
634
|
+
return {
|
|
635
|
+
type: "error",
|
|
636
|
+
error: errorInfo(
|
|
637
|
+
"skill-kind-resolve-failed",
|
|
638
|
+
`Could not resolve skill path ${candidate}: ${formatErrorMessage(error)}`,
|
|
639
|
+
),
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function classifyCanonicalSkillPath(
|
|
645
|
+
projectDir: string,
|
|
646
|
+
candidate: string,
|
|
647
|
+
): AregSkillKindResolveResult {
|
|
648
|
+
const roots = AREG_SKILL_KIND_ROOT_DESCRIPTORS.map((descriptor) =>
|
|
649
|
+
toProjectPath(projectDir, descriptor.root),
|
|
650
|
+
);
|
|
651
|
+
for (const root of roots) {
|
|
652
|
+
const classified = classifySkillPathUnderRoot(root, candidate);
|
|
653
|
+
if (classified.type === "ok" || classified.error.code === "skill-kind-nested-spec")
|
|
654
|
+
return classified;
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
type: "error",
|
|
658
|
+
error: errorInfo(
|
|
659
|
+
"skill-kind-outside-managed-skills",
|
|
660
|
+
`Skill path is outside ${AREG_SKILL_KIND_ROOT_DESCRIPTORS.map((descriptor) => descriptor.root).join(" and ")}: ${candidate}`,
|
|
661
|
+
),
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function classifySkillPathUnderRoot(root: string, candidate: string): AregSkillKindResolveResult {
|
|
666
|
+
const relative = path.relative(root, candidate);
|
|
667
|
+
if (relative.length === 0 || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
668
|
+
return {
|
|
669
|
+
type: "error",
|
|
670
|
+
error: errorInfo(
|
|
671
|
+
"skill-kind-outside-managed-skills",
|
|
672
|
+
`Skill path is outside ${root}: ${candidate}`,
|
|
673
|
+
),
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const parts = relative.split(path.sep).filter((part) => part.length > 0);
|
|
677
|
+
const skillName = parts[0];
|
|
678
|
+
if (skillName === undefined)
|
|
679
|
+
return {
|
|
680
|
+
type: "error",
|
|
681
|
+
error: errorInfo("skill-kind-invalid-spec", `Invalid skill path: ${candidate}`),
|
|
682
|
+
};
|
|
683
|
+
if (parts.length === 1) return { type: "ok", skillName };
|
|
684
|
+
if (parts.length === 2 && parts[1] === "SKILL.md") return { type: "ok", skillName };
|
|
685
|
+
return {
|
|
686
|
+
type: "error",
|
|
687
|
+
error: errorInfo(
|
|
688
|
+
"skill-kind-nested-spec",
|
|
689
|
+
`Skill path must be skills/<name>, .agents/skills/<name>, or a direct SKILL.md: ${candidate}`,
|
|
690
|
+
),
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function isPathLikeSkillSpec(spec: string): boolean {
|
|
695
|
+
return (
|
|
696
|
+
path.isAbsolute(spec) || spec.includes("/") || spec.includes("\\") || spec.endsWith("SKILL.md")
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
async function inspectReplacementSurfaces(
|
|
701
|
+
projectDir: string,
|
|
702
|
+
): Promise<{ verifiedSurfaces: readonly string[] }> {
|
|
703
|
+
const hasAdapter =
|
|
704
|
+
(await inspectTextFile(path.join(projectDir, PI_GENERIC_REPLACEMENT_ADAPTER_RELATIVE_PATH)))
|
|
705
|
+
.type === "file";
|
|
706
|
+
const hasPackageModule =
|
|
707
|
+
(
|
|
708
|
+
await inspectTextFile(
|
|
709
|
+
path.join(projectDir, PI_GENERIC_REPLACEMENT_PACKAGE_MODULE_RELATIVE_PATH),
|
|
710
|
+
)
|
|
711
|
+
).type === "file";
|
|
712
|
+
return {
|
|
713
|
+
verifiedSurfaces: hasAdapter && hasPackageModule ? [...AREG_VISIBLE_REPLACEMENT_SURFACES] : [],
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
async function listChildNamesForSourceType(
|
|
718
|
+
projectDir: string,
|
|
719
|
+
sourceType: SkillLookupSourceType,
|
|
720
|
+
): Promise<string[]> {
|
|
721
|
+
const descriptor = skillLookupDescriptorForSourceType(sourceType);
|
|
722
|
+
return await listChildNames(toProjectPath(projectDir, descriptor.root));
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
async function listChildNames(directory: string): Promise<string[]> {
|
|
726
|
+
try {
|
|
727
|
+
const info = await lstat(directory);
|
|
728
|
+
if (!info.isDirectory()) return [];
|
|
729
|
+
const entries = await readdir(directory);
|
|
730
|
+
return sortStrings(entries.filter((entry) => entry !== ".DS_Store"));
|
|
731
|
+
} catch (error) {
|
|
732
|
+
if (isNodeErrorCode(error, "ENOENT")) return [];
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
async function readLocallyExcludedSkillNames(options: {
|
|
738
|
+
projectDir: string;
|
|
739
|
+
git: GitGateway;
|
|
740
|
+
}): Promise<string[]> {
|
|
741
|
+
const gitPath = await options.git.gitPath({
|
|
742
|
+
cwd: options.projectDir,
|
|
743
|
+
relativePath: "info/exclude",
|
|
744
|
+
});
|
|
745
|
+
if (!gitPath.ok) return [];
|
|
746
|
+
const exclude = await inspectTextFile(gitPath.value);
|
|
747
|
+
if (exclude.type !== "file") return [];
|
|
748
|
+
const prefixes = [".agents/skills/", ".claude/skills/"];
|
|
749
|
+
const names = new Set<string>();
|
|
750
|
+
for (const rawLine of exclude.text.split(/\r?\n/u)) {
|
|
751
|
+
const line = rawLine.trim();
|
|
752
|
+
if (line.length === 0 || line.startsWith("#")) continue;
|
|
753
|
+
for (const prefix of prefixes) {
|
|
754
|
+
if (line.startsWith(prefix)) names.add(line.slice(prefix.length));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return sortStrings([...names]);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function inspectPairingDirectories(projectDir: string): Promise<AregCheckPairingDirectory[]> {
|
|
761
|
+
const results: AregCheckPairingDirectory[] = [];
|
|
762
|
+
async function visit(directory: string, relativeDir: string): Promise<void> {
|
|
763
|
+
let entries;
|
|
764
|
+
try {
|
|
765
|
+
entries = await readdir(directory, { withFileTypes: true });
|
|
766
|
+
} catch {
|
|
767
|
+
// Pairing directory inspection is best-effort; unreadable or disappearing directories
|
|
768
|
+
// should not fail the whole project inspection.
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
const names = new Set(entries.map((entry) => entry.name));
|
|
772
|
+
const hasAgents =
|
|
773
|
+
names.has("AGENTS.md") &&
|
|
774
|
+
(await inspectTextFile(path.join(directory, "AGENTS.md"))).type === "file";
|
|
775
|
+
const claude = names.has("CLAUDE.md")
|
|
776
|
+
? await inspectTextFile(path.join(directory, "CLAUDE.md"))
|
|
777
|
+
: { type: "missing" as const };
|
|
778
|
+
const hasClaude = claude.type === "file";
|
|
779
|
+
if (hasAgents || hasClaude) {
|
|
780
|
+
results.push({
|
|
781
|
+
relativeDir,
|
|
782
|
+
hasAgents,
|
|
783
|
+
hasClaude,
|
|
784
|
+
...(claude.type === "file" ? { claudeText: claude.text } : {}),
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
const subdirs = sortStrings(
|
|
788
|
+
entries
|
|
789
|
+
.filter((entry) => entry.isDirectory())
|
|
790
|
+
.map((entry) => entry.name)
|
|
791
|
+
.filter((name) => ![".venv", ".git", "node_modules"].includes(name)),
|
|
792
|
+
);
|
|
793
|
+
for (const name of subdirs) {
|
|
794
|
+
const childRelative = relativeDir.length === 0 ? name : `${relativeDir}/${name}`;
|
|
795
|
+
if (childRelative === ".agents/skills" || childRelative === ".claude/skills") continue;
|
|
796
|
+
await visit(path.join(directory, name), childRelative);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
await visit(projectDir, "");
|
|
800
|
+
return results;
|
|
801
|
+
}
|