@nseng-ai/areg 0.1.1 → 0.1.2
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 +7 -6
- package/src/cli.ts +0 -33
- package/src/context.ts +3 -29
- package/src/fake-gateways.ts +108 -380
- package/src/gateways/errors.ts +2 -2
- package/src/gateways/fs-utils.ts +2 -2
- package/src/gateways/project-fs.ts +29 -41
- package/src/gateways/project-gateway.ts +98 -31
- package/src/gateways.ts +34 -150
- package/src/index.ts +2 -28
- package/src/operations/check.ts +33 -20
- package/src/operations/doctor-skills.ts +71 -7
- package/src/operations/file-state.ts +4 -4
- package/src/operations/manifest-source-findings.ts +45 -0
- package/src/operations/manifest-sources.ts +119 -0
- package/src/operations/pi-settings.ts +2 -5
- package/src/operations/project-inspection.ts +32 -28
- package/src/operations/project-mutations.ts +5 -21
- package/src/operations/project-resolution.ts +2 -2
- package/src/operations/skill-find.ts +23 -3
- package/src/operations/skill-kind-apply-plan.ts +2 -2
- package/src/operations/skill-kind-frontmatter.ts +1 -1
- package/src/operations/skill-kind-inference.ts +30 -14
- package/src/operations/skill-kind.ts +34 -12
- package/src/sort.ts +3 -3
- package/src/gateways/command-constants.ts +0 -1
- package/src/gateways/github-gateway.ts +0 -111
- package/src/gateways/host-gateway.ts +0 -35
- package/src/gateways/mutation-policy.ts +0 -94
- package/src/gateways/npx-skills-gateway.ts +0 -53
- package/src/gateways/prompt-gateway.ts +0 -21
- package/src/gateways/skillx-workspace-gateway.ts +0 -220
- package/src/operations/frontmatter.ts +0 -151
- package/src/operations/init.ts +0 -548
- package/src/operations/lockfile.ts +0 -107
- package/src/operations/managed-markdown-block.ts +0 -47
- package/src/operations/project-agents.ts +0 -115
- package/src/operations/skill-mirror-conventions.ts +0 -126
- package/src/operations/skillx.ts +0 -305
- package/src/operations/toml-section.ts +0 -53
- package/src/operations/update-skills.ts +0 -325
- package/src/real-gateways.ts +0 -6
- package/src/skill-lookup.ts +0 -254
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
import { failure, ok, type ClinkrExit } from "@nseng-ai/clinkr";
|
|
2
|
-
import type { Result } from "@nseng-ai/foundation/result";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
import type { AregCliContext } from "../context.ts";
|
|
6
|
-
import type { AregErrorInfo, AregGithubSkillFileResult } from "../gateways.ts";
|
|
7
|
-
import { sortStrings } from "../sort.ts";
|
|
8
|
-
import { parseInspectedLockfile, type LockfileSkill } from "./lockfile.ts";
|
|
9
|
-
import { resolveProjectAgents } from "./project-agents.ts";
|
|
10
|
-
|
|
11
|
-
const updateStatusSchema = z.enum(["planned", "updated", "failed"]);
|
|
12
|
-
|
|
13
|
-
const selectedUpdateSchema = z.object({
|
|
14
|
-
skill: z.string(),
|
|
15
|
-
source: z.string(),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
const attemptedUpdateSchema = selectedUpdateSchema.extend({
|
|
19
|
-
status: updateStatusSchema,
|
|
20
|
-
error: z.string().optional(),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
export const updateSkillsRequestSchema = z.object({
|
|
24
|
-
path: z.string().default(".").describe("Project directory containing skills-lock.json."),
|
|
25
|
-
skill: z.array(z.string()).default([]).describe("Skill name to update; repeatable."),
|
|
26
|
-
source: z
|
|
27
|
-
.array(z.string())
|
|
28
|
-
.default([])
|
|
29
|
-
.describe("Only update skills whose lockfile source matches; repeatable."),
|
|
30
|
-
agent: z.array(z.string()).default([]).describe("Agent directory to populate; repeatable."),
|
|
31
|
-
dryRun: z
|
|
32
|
-
.boolean()
|
|
33
|
-
.default(false)
|
|
34
|
-
.describe("Print planned updates without source preflight or npx calls."),
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
export const updateSkillsResultSchema = z.object({
|
|
38
|
-
ok: z.boolean(),
|
|
39
|
-
projectDir: z.string(),
|
|
40
|
-
agents: z.array(z.string()),
|
|
41
|
-
dryRun: z.boolean(),
|
|
42
|
-
selectedUpdates: z.array(selectedUpdateSchema),
|
|
43
|
-
attemptedUpdates: z.array(attemptedUpdateSchema),
|
|
44
|
-
failureCount: z.number().int().nonnegative(),
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
export type UpdateSkillsRequest = z.infer<typeof updateSkillsRequestSchema>;
|
|
48
|
-
export type UpdateSkillsResult = z.infer<typeof updateSkillsResultSchema>;
|
|
49
|
-
interface SelectedUpdate extends z.infer<typeof selectedUpdateSchema> {
|
|
50
|
-
skillPath?: string;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
type AttemptedUpdate = z.infer<typeof attemptedUpdateSchema>;
|
|
54
|
-
|
|
55
|
-
type PreflightOutcome = { ok: true } | { ok: false; code: string; message: string };
|
|
56
|
-
type GithubPreflightFailure =
|
|
57
|
-
| { type: "missing" | "auth-error"; message: string }
|
|
58
|
-
| { type: "error"; error: AregErrorInfo };
|
|
59
|
-
|
|
60
|
-
export async function runUpdateSkills(
|
|
61
|
-
ctx: AregCliContext,
|
|
62
|
-
request: UpdateSkillsRequest,
|
|
63
|
-
): Promise<ClinkrExit<UpdateSkillsResult>> {
|
|
64
|
-
const inspection = await ctx.project.inspectProjectBase({
|
|
65
|
-
cwd: ctx.cwd,
|
|
66
|
-
projectPath: request.path,
|
|
67
|
-
env: ctx.env,
|
|
68
|
-
});
|
|
69
|
-
if (inspection.projectPathState.type !== "directory") {
|
|
70
|
-
return failure("invalid-project", `${inspection.projectDir} is not a directory`);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const lockfileResult = parseInspectedLockfile(inspection);
|
|
74
|
-
if (!lockfileResult.ok) return failure("lockfile-invalid", lockfileResult.error.message);
|
|
75
|
-
|
|
76
|
-
const selection = selectGithubUpdates(lockfileResult.value.skills, request);
|
|
77
|
-
if (!selection.ok) return failure("invalid-selection", selection.error.message);
|
|
78
|
-
const selectedUpdates = selection.value;
|
|
79
|
-
|
|
80
|
-
if (selectedUpdates.length === 0)
|
|
81
|
-
return ok(emptyReport(inspection.projectDir, request.dryRun, true));
|
|
82
|
-
|
|
83
|
-
const agentsResult = resolveProjectAgents({
|
|
84
|
-
explicitAgents: request.agent,
|
|
85
|
-
nsToml: inspection.nsToml,
|
|
86
|
-
aregJson: inspection.aregJson,
|
|
87
|
-
});
|
|
88
|
-
if (!agentsResult.ok) return failure("agent-resolution-failed", agentsResult.error.message);
|
|
89
|
-
const agents = agentsResult.value;
|
|
90
|
-
|
|
91
|
-
if (!request.dryRun) {
|
|
92
|
-
const gh = await ctx.host.checkTool({ tool: "gh", cwd: inspection.projectDir, env: ctx.env });
|
|
93
|
-
if (gh.type === "missing") return failure("missing-tool", gh.message);
|
|
94
|
-
const preflight = await preflightSelectedUpdates(ctx, selectedUpdates);
|
|
95
|
-
if (!preflight.ok) return failure("skill-source-preflight-failed", preflight.error.message);
|
|
96
|
-
const npx = await ctx.host.checkTool({ tool: "npx", cwd: inspection.projectDir, env: ctx.env });
|
|
97
|
-
if (npx.type === "missing") return failure("missing-tool", npx.message);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const attemptedUpdates: AttemptedUpdate[] = [];
|
|
101
|
-
for (const update of selectedUpdates) {
|
|
102
|
-
if (request.dryRun) {
|
|
103
|
-
attemptedUpdates.push({ ...publicSelectedUpdate(update), status: "planned" });
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
const result = await ctx.npxSkills.addSkills({
|
|
107
|
-
sourceRepo: update.source,
|
|
108
|
-
skillNames: [update.skill],
|
|
109
|
-
targetAgents: agents,
|
|
110
|
-
cwd: inspection.projectDir,
|
|
111
|
-
env: ctx.env,
|
|
112
|
-
});
|
|
113
|
-
if (result.type === "ok") {
|
|
114
|
-
attemptedUpdates.push({ ...publicSelectedUpdate(update), status: "updated" });
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
attemptedUpdates.push({
|
|
118
|
-
...publicSelectedUpdate(update),
|
|
119
|
-
status: "failed",
|
|
120
|
-
error: result.error.message,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const finalReport = report({
|
|
125
|
-
projectDir: inspection.projectDir,
|
|
126
|
-
agents,
|
|
127
|
-
dryRun: request.dryRun,
|
|
128
|
-
selectedUpdates,
|
|
129
|
-
attemptedUpdates,
|
|
130
|
-
});
|
|
131
|
-
if (finalReport.failureCount > 0)
|
|
132
|
-
return failure("skill-update-failed", formatFailureMessage(finalReport));
|
|
133
|
-
return ok(finalReport);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function renderUpdateSkills(result: UpdateSkillsResult): string {
|
|
137
|
-
if (result.selectedUpdates.length === 0)
|
|
138
|
-
return "No github-sourced skills match. Nothing to update.";
|
|
139
|
-
const suffix = result.dryRun ? " [dry-run]" : "";
|
|
140
|
-
const lines = [
|
|
141
|
-
`Updating ${result.selectedUpdates.length} skill(s) with agents ${result.agents.join(", ")}${suffix}:`,
|
|
142
|
-
];
|
|
143
|
-
for (const update of result.attemptedUpdates) {
|
|
144
|
-
lines.push(` ${update.skill} <- ${update.source}`);
|
|
145
|
-
if (update.status === "failed") lines.push(` FAILED: ${update.error ?? "unknown error"}`);
|
|
146
|
-
}
|
|
147
|
-
if (result.dryRun)
|
|
148
|
-
lines.push("", `Planned: ${result.attemptedUpdates.length} skill(s). No changes made.`);
|
|
149
|
-
else lines.push("", `Updated ${result.attemptedUpdates.length} skill(s).`);
|
|
150
|
-
return lines.join("\n");
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function selectGithubUpdates(
|
|
154
|
-
skills: readonly LockfileSkill[],
|
|
155
|
-
request: UpdateSkillsRequest,
|
|
156
|
-
): Result<readonly SelectedUpdate[]> {
|
|
157
|
-
const githubEntries = new Map<string, { source: string; skillPath?: string }>();
|
|
158
|
-
for (const skill of skills) {
|
|
159
|
-
if (skill.sourceType === "github") {
|
|
160
|
-
githubEntries.set(skill.name, {
|
|
161
|
-
source: skill.source,
|
|
162
|
-
...(skill.skillPath === undefined ? {} : { skillPath: skill.skillPath }),
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const requestedSkills = new Set(request.skill);
|
|
168
|
-
if (requestedSkills.size > 0) {
|
|
169
|
-
const unknown = sortStrings([...requestedSkills].filter((skill) => !githubEntries.has(skill)));
|
|
170
|
-
if (unknown.length > 0)
|
|
171
|
-
return {
|
|
172
|
-
ok: false,
|
|
173
|
-
error: {
|
|
174
|
-
code: "skill_not_found",
|
|
175
|
-
message: `Skill(s) not found in lockfile (or not github-sourced): ${unknown.join(", ")}`,
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const requestedSources = new Set(request.source);
|
|
181
|
-
const updates: SelectedUpdate[] = [];
|
|
182
|
-
for (const skill of sortStrings([...githubEntries.keys()])) {
|
|
183
|
-
const entry = githubEntries.get(skill);
|
|
184
|
-
if (entry === undefined) continue;
|
|
185
|
-
if (requestedSkills.size > 0 && !requestedSkills.has(skill)) continue;
|
|
186
|
-
if (requestedSources.size > 0 && !requestedSources.has(entry.source)) continue;
|
|
187
|
-
updates.push({
|
|
188
|
-
skill,
|
|
189
|
-
source: entry.source,
|
|
190
|
-
...(entry.skillPath === undefined ? {} : { skillPath: entry.skillPath }),
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
return { ok: true, value: updates };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
async function preflightSelectedUpdates(
|
|
197
|
-
ctx: AregCliContext,
|
|
198
|
-
selectedUpdates: readonly SelectedUpdate[],
|
|
199
|
-
): Promise<Result<undefined>> {
|
|
200
|
-
const failures: string[] = [];
|
|
201
|
-
for (const update of selectedUpdates) {
|
|
202
|
-
const result = await preflightSelectedUpdate(ctx, update);
|
|
203
|
-
if (!result.ok) failures.push(result.error.message);
|
|
204
|
-
}
|
|
205
|
-
if (failures.length === 0) return { ok: true, value: undefined };
|
|
206
|
-
return {
|
|
207
|
-
ok: false,
|
|
208
|
-
error: {
|
|
209
|
-
code: "skill_source_preflight_failed",
|
|
210
|
-
message: `Source preflight failed before updating skills:\n${failures
|
|
211
|
-
.map((message) => ` - ${message}`)
|
|
212
|
-
.join("\n")}`,
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async function preflightSelectedUpdate(
|
|
218
|
-
ctx: AregCliContext,
|
|
219
|
-
update: SelectedUpdate,
|
|
220
|
-
): Promise<Result<undefined>> {
|
|
221
|
-
if (update.skillPath !== undefined) {
|
|
222
|
-
const result = await ctx.github.checkSkillFile({
|
|
223
|
-
repo: update.source,
|
|
224
|
-
path: update.skillPath,
|
|
225
|
-
env: ctx.env,
|
|
226
|
-
});
|
|
227
|
-
return preflightOutcomeResult(update, normalizeSkillFilePreflightResult(result));
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
const result = await ctx.github.listSkillDirectoryNames({ repo: update.source, env: ctx.env });
|
|
231
|
-
if (result.type === "ok") {
|
|
232
|
-
if (result.skillNames.includes(update.skill)) return { ok: true, value: undefined };
|
|
233
|
-
return preflightOutcomeResult(update, {
|
|
234
|
-
ok: false,
|
|
235
|
-
code: "skill_source_preflight_failed",
|
|
236
|
-
message: "not found in skills directory before update",
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
return preflightOutcomeResult(update, normalizeGithubPreflightFailure(result));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function normalizeSkillFilePreflightResult(result: AregGithubSkillFileResult): PreflightOutcome {
|
|
243
|
-
if (result.type === "found") return { ok: true };
|
|
244
|
-
return normalizeGithubPreflightFailure(result);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function normalizeGithubPreflightFailure(result: GithubPreflightFailure): PreflightOutcome {
|
|
248
|
-
switch (result.type) {
|
|
249
|
-
case "missing":
|
|
250
|
-
case "auth-error":
|
|
251
|
-
return {
|
|
252
|
-
ok: false,
|
|
253
|
-
code: "skill_source_preflight_failed",
|
|
254
|
-
message: result.message,
|
|
255
|
-
};
|
|
256
|
-
case "error":
|
|
257
|
-
return {
|
|
258
|
-
ok: false,
|
|
259
|
-
code: result.error.code,
|
|
260
|
-
message: result.error.message,
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
function preflightOutcomeResult(
|
|
266
|
-
update: SelectedUpdate,
|
|
267
|
-
outcome: PreflightOutcome,
|
|
268
|
-
): Result<undefined> {
|
|
269
|
-
if (outcome.ok) return { ok: true, value: undefined };
|
|
270
|
-
return {
|
|
271
|
-
ok: false,
|
|
272
|
-
error: {
|
|
273
|
-
code: outcome.code,
|
|
274
|
-
message: `${update.skill} from ${update.source}: ${outcome.message}`,
|
|
275
|
-
},
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function report(input: {
|
|
280
|
-
ok?: boolean;
|
|
281
|
-
projectDir: string;
|
|
282
|
-
agents: readonly string[];
|
|
283
|
-
dryRun: boolean;
|
|
284
|
-
selectedUpdates: readonly SelectedUpdate[];
|
|
285
|
-
attemptedUpdates: readonly AttemptedUpdate[];
|
|
286
|
-
}): UpdateSkillsResult {
|
|
287
|
-
const failures = input.attemptedUpdates.filter((update) => update.status === "failed");
|
|
288
|
-
return {
|
|
289
|
-
ok: input.ok ?? failures.length === 0,
|
|
290
|
-
projectDir: input.projectDir,
|
|
291
|
-
agents: [...input.agents],
|
|
292
|
-
dryRun: input.dryRun,
|
|
293
|
-
selectedUpdates: input.selectedUpdates.map(publicSelectedUpdate),
|
|
294
|
-
attemptedUpdates: input.attemptedUpdates.map((update) => ({ ...update })),
|
|
295
|
-
failureCount: failures.length,
|
|
296
|
-
};
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function publicSelectedUpdate(update: SelectedUpdate): z.infer<typeof selectedUpdateSchema> {
|
|
300
|
-
return { skill: update.skill, source: update.source };
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function emptyReport(projectDir: string, dryRun: boolean, isOk: boolean): UpdateSkillsResult {
|
|
304
|
-
return report({
|
|
305
|
-
ok: isOk,
|
|
306
|
-
projectDir,
|
|
307
|
-
agents: [],
|
|
308
|
-
dryRun,
|
|
309
|
-
selectedUpdates: [],
|
|
310
|
-
attemptedUpdates: [],
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function formatFailureMessage(result: UpdateSkillsResult): string {
|
|
315
|
-
const failed = result.attemptedUpdates.filter((update) => update.status === "failed");
|
|
316
|
-
const lines = [
|
|
317
|
-
`${failed.length} skill(s) failed to update: ${failed.map((update) => update.skill).join(", ")}`,
|
|
318
|
-
];
|
|
319
|
-
for (const update of failed)
|
|
320
|
-
lines.push(
|
|
321
|
-
` ${update.skill} <- ${update.source}`,
|
|
322
|
-
` FAILED: ${update.error ?? "unknown error"}`,
|
|
323
|
-
);
|
|
324
|
-
return lines.join("\n");
|
|
325
|
-
}
|
package/src/real-gateways.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { RealAregProjectGateway } from "./gateways/project-gateway.ts";
|
|
2
|
-
export { RealAregGithubGateway } from "./gateways/github-gateway.ts";
|
|
3
|
-
export { RealAregHostGateway } from "./gateways/host-gateway.ts";
|
|
4
|
-
export { buildNpxSkillsAddArgs, RealAregNpxSkillsGateway } from "./gateways/npx-skills-gateway.ts";
|
|
5
|
-
export { RealAregPromptGateway } from "./gateways/prompt-gateway.ts";
|
|
6
|
-
export { RealAregSkillxWorkspaceGateway } from "./gateways/skillx-workspace-gateway.ts";
|
package/src/skill-lookup.ts
DELETED
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { lstat, realpath } from "node:fs/promises";
|
|
2
|
-
import { dirname, join, parse, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
import { isPathInside } from "@nseng-ai/foundation/primitives";
|
|
5
|
-
|
|
6
|
-
export const SKILL_LOOKUP_ROOT_DESCRIPTORS = [
|
|
7
|
-
{ root: "skills", sourceType: "repo" },
|
|
8
|
-
{ root: ".agents/skills", sourceType: "vendored" },
|
|
9
|
-
{ root: ".claude/skills", sourceType: "claude" },
|
|
10
|
-
] as const satisfies ReadonlyArray<{ root: string; sourceType: string }>;
|
|
11
|
-
|
|
12
|
-
export type SkillLookupRootDescriptor = (typeof SKILL_LOOKUP_ROOT_DESCRIPTORS)[number];
|
|
13
|
-
export type SkillLookupRoot = SkillLookupRootDescriptor["root"];
|
|
14
|
-
export type SkillLookupSourceType = SkillLookupRootDescriptor["sourceType"];
|
|
15
|
-
|
|
16
|
-
export const SKILL_LOOKUP_ROOTS = SKILL_LOOKUP_ROOT_DESCRIPTORS.map(
|
|
17
|
-
(descriptor): SkillLookupRoot => descriptor.root,
|
|
18
|
-
) as [SkillLookupRoot, ...SkillLookupRoot[]];
|
|
19
|
-
export const SKILL_LOOKUP_SOURCE_TYPES = SKILL_LOOKUP_ROOT_DESCRIPTORS.map(
|
|
20
|
-
(descriptor): SkillLookupSourceType => descriptor.sourceType,
|
|
21
|
-
) as [SkillLookupSourceType, ...SkillLookupSourceType[]];
|
|
22
|
-
|
|
23
|
-
const SKILL_LOOKUP_DESCRIPTOR_BY_ROOT = new Map<SkillLookupRoot, SkillLookupRootDescriptor>(
|
|
24
|
-
SKILL_LOOKUP_ROOT_DESCRIPTORS.map((descriptor) => [descriptor.root, descriptor] as const),
|
|
25
|
-
);
|
|
26
|
-
const SKILL_LOOKUP_DESCRIPTOR_BY_SOURCE_TYPE = new Map<
|
|
27
|
-
SkillLookupSourceType,
|
|
28
|
-
SkillLookupRootDescriptor
|
|
29
|
-
>(SKILL_LOOKUP_ROOT_DESCRIPTORS.map((descriptor) => [descriptor.sourceType, descriptor] as const));
|
|
30
|
-
const SKILL_LOOKUP_ROOT_RANKS = new Map<SkillLookupRoot, number>(
|
|
31
|
-
SKILL_LOOKUP_ROOT_DESCRIPTORS.map((descriptor, index) => [descriptor.root, index] as const),
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
export interface SkillLookupPathStat {
|
|
35
|
-
isFile(): boolean;
|
|
36
|
-
isDirectory(): boolean;
|
|
37
|
-
isSymbolicLink(): boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface SkillLookupIo {
|
|
41
|
-
statPath?: (path: string) => Promise<SkillLookupPathStat>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SkillLookupSearchedRoot {
|
|
45
|
-
root: SkillLookupRoot;
|
|
46
|
-
sourceType: SkillLookupSourceType;
|
|
47
|
-
searchedRelativePath: string;
|
|
48
|
-
searchedPath: string;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface FoundSkillLookup {
|
|
52
|
-
type: "found";
|
|
53
|
-
root: SkillLookupRoot;
|
|
54
|
-
sourceType: SkillLookupSourceType;
|
|
55
|
-
baseRelativePath: string;
|
|
56
|
-
skillFileRelativePath: string;
|
|
57
|
-
basePath: string;
|
|
58
|
-
skillFilePath: string;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface MissingSkillLookup {
|
|
62
|
-
type: "missing";
|
|
63
|
-
searchedRoots: readonly SkillLookupSearchedRoot[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface FailedSkillLookup {
|
|
67
|
-
type: "error";
|
|
68
|
-
message: string;
|
|
69
|
-
path: string;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type SkillLookupResult = FoundSkillLookup | MissingSkillLookup | FailedSkillLookup;
|
|
73
|
-
|
|
74
|
-
export interface ResolveExactSkillLookupOptions extends SkillLookupIo {
|
|
75
|
-
projectDir: string;
|
|
76
|
-
skillName: string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface ResolveSkillLookupProjectRootOptions extends Pick<SkillLookupIo, "statPath"> {
|
|
80
|
-
cwd: string;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function skillLookupDescriptorForRoot(root: SkillLookupRoot): SkillLookupRootDescriptor {
|
|
84
|
-
const descriptor = SKILL_LOOKUP_DESCRIPTOR_BY_ROOT.get(root);
|
|
85
|
-
if (descriptor === undefined) throw new Error(`Unknown skill lookup root: ${root}`);
|
|
86
|
-
return descriptor;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function skillLookupDescriptorForSourceType(
|
|
90
|
-
sourceType: SkillLookupSourceType,
|
|
91
|
-
): SkillLookupRootDescriptor {
|
|
92
|
-
const descriptor = SKILL_LOOKUP_DESCRIPTOR_BY_SOURCE_TYPE.get(sourceType);
|
|
93
|
-
if (descriptor === undefined) throw new Error(`Unknown skill lookup source type: ${sourceType}`);
|
|
94
|
-
return descriptor;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function skillLookupRootRank(root: SkillLookupRoot): number {
|
|
98
|
-
return SKILL_LOOKUP_ROOT_RANKS.get(root) ?? SKILL_LOOKUP_ROOT_DESCRIPTORS.length;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function skillLookupBaseRelativePath(root: SkillLookupRoot, skillName: string): string {
|
|
102
|
-
return `${root}/${skillName}`;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function skillLookupFileRelativePath(root: SkillLookupRoot, skillName: string): string {
|
|
106
|
-
return `${skillLookupBaseRelativePath(root, skillName)}/SKILL.md`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function skillLookupIoOptions(options: SkillLookupIo): SkillLookupIo {
|
|
110
|
-
if (options.statPath === undefined) return {};
|
|
111
|
-
return { statPath: options.statPath };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function buildSkillLookupSearchedRoots(
|
|
115
|
-
projectDir: string,
|
|
116
|
-
skillName: string,
|
|
117
|
-
): SkillLookupSearchedRoot[] {
|
|
118
|
-
return SKILL_LOOKUP_ROOT_DESCRIPTORS.map((descriptor) => {
|
|
119
|
-
const searchedRelativePath = skillLookupFileRelativePath(descriptor.root, skillName);
|
|
120
|
-
return {
|
|
121
|
-
root: descriptor.root,
|
|
122
|
-
sourceType: descriptor.sourceType,
|
|
123
|
-
searchedRelativePath,
|
|
124
|
-
searchedPath: join(projectDir, descriptor.root, skillName, "SKILL.md"),
|
|
125
|
-
};
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function defaultStatPath(path: string): Promise<SkillLookupPathStat> {
|
|
130
|
-
return lstat(path);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function defaultRealpathPath(path: string): Promise<string> {
|
|
134
|
-
return realpath(path);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function containmentErrorOrUndefined(options: {
|
|
138
|
-
base: string;
|
|
139
|
-
target: string;
|
|
140
|
-
skillFilePath: string;
|
|
141
|
-
projectDir: string;
|
|
142
|
-
}): FailedSkillLookup | undefined {
|
|
143
|
-
if (isPathInside(options.base, options.target)) return undefined;
|
|
144
|
-
return {
|
|
145
|
-
type: "error",
|
|
146
|
-
message: `Backing skill path ${options.skillFilePath} resolves outside repository root ${options.projectDir}.`,
|
|
147
|
-
path: options.skillFilePath,
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function resolveSkillLookupProjectRoot(
|
|
152
|
-
options: ResolveSkillLookupProjectRootOptions,
|
|
153
|
-
): Promise<string> {
|
|
154
|
-
const statPath = options.statPath ?? defaultStatPath;
|
|
155
|
-
let current = resolve(options.cwd);
|
|
156
|
-
const root = parse(current).root;
|
|
157
|
-
|
|
158
|
-
while (true) {
|
|
159
|
-
if (await hasGitMarker(current, statPath)) return current;
|
|
160
|
-
if (current === root) {
|
|
161
|
-
throw new Error(`Could not find a Git repository root from ${options.cwd}.`);
|
|
162
|
-
}
|
|
163
|
-
current = dirname(current);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export async function resolveExactSkillLookup(
|
|
168
|
-
options: ResolveExactSkillLookupOptions,
|
|
169
|
-
): Promise<SkillLookupResult> {
|
|
170
|
-
const statPath = options.statPath ?? defaultStatPath;
|
|
171
|
-
const projectDir = resolve(options.projectDir);
|
|
172
|
-
const realProjectDir = await defaultRealpathPath(projectDir);
|
|
173
|
-
const searchedRoots = buildSkillLookupSearchedRoots(projectDir, options.skillName);
|
|
174
|
-
|
|
175
|
-
for (const descriptor of SKILL_LOOKUP_ROOT_DESCRIPTORS) {
|
|
176
|
-
const baseRelativePath = skillLookupBaseRelativePath(descriptor.root, options.skillName);
|
|
177
|
-
const skillFileRelativePath = skillLookupFileRelativePath(descriptor.root, options.skillName);
|
|
178
|
-
const basePath = join(projectDir, descriptor.root, options.skillName);
|
|
179
|
-
const skillFilePath = join(basePath, "SKILL.md");
|
|
180
|
-
const normalizedSkillFilePath = resolve(skillFilePath);
|
|
181
|
-
const normalizedContainmentError = containmentErrorOrUndefined({
|
|
182
|
-
base: projectDir,
|
|
183
|
-
target: normalizedSkillFilePath,
|
|
184
|
-
skillFilePath,
|
|
185
|
-
projectDir,
|
|
186
|
-
});
|
|
187
|
-
if (normalizedContainmentError !== undefined) return normalizedContainmentError;
|
|
188
|
-
|
|
189
|
-
const skillStat = await statPathOrUndefined(statPath, skillFilePath);
|
|
190
|
-
if (skillStat === undefined || (!skillStat.isFile() && !skillStat.isSymbolicLink())) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
if (skillStat.isSymbolicLink()) {
|
|
194
|
-
return {
|
|
195
|
-
type: "error",
|
|
196
|
-
message: `Refusing to read symlinked backing skill at ${skillFilePath}.`,
|
|
197
|
-
path: skillFilePath,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let realSkillFilePath: string;
|
|
202
|
-
try {
|
|
203
|
-
realSkillFilePath = await defaultRealpathPath(skillFilePath);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
return {
|
|
206
|
-
type: "error",
|
|
207
|
-
message: `Could not resolve backing skill path ${skillFilePath}: ${formatUnknownError(error)}`,
|
|
208
|
-
path: skillFilePath,
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
const realContainmentError = containmentErrorOrUndefined({
|
|
212
|
-
base: realProjectDir,
|
|
213
|
-
target: realSkillFilePath,
|
|
214
|
-
skillFilePath,
|
|
215
|
-
projectDir,
|
|
216
|
-
});
|
|
217
|
-
if (realContainmentError !== undefined) return realContainmentError;
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
type: "found",
|
|
221
|
-
root: descriptor.root,
|
|
222
|
-
sourceType: descriptor.sourceType,
|
|
223
|
-
baseRelativePath,
|
|
224
|
-
skillFileRelativePath,
|
|
225
|
-
basePath,
|
|
226
|
-
skillFilePath,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
return { type: "missing", searchedRoots };
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async function hasGitMarker(
|
|
234
|
-
directory: string,
|
|
235
|
-
statPath: (path: string) => Promise<SkillLookupPathStat>,
|
|
236
|
-
): Promise<boolean> {
|
|
237
|
-
const marker = await statPathOrUndefined(statPath, join(directory, ".git"));
|
|
238
|
-
return marker !== undefined && (marker.isDirectory() || marker.isFile());
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function statPathOrUndefined(
|
|
242
|
-
statPath: (path: string) => Promise<SkillLookupPathStat>,
|
|
243
|
-
path: string,
|
|
244
|
-
): Promise<SkillLookupPathStat | undefined> {
|
|
245
|
-
try {
|
|
246
|
-
return await statPath(path);
|
|
247
|
-
} catch {
|
|
248
|
-
return undefined;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function formatUnknownError(error: unknown): string {
|
|
253
|
-
return error instanceof Error ? error.message : String(error);
|
|
254
|
-
}
|