@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,531 @@
|
|
|
1
|
+
import { err, type Result } from "@nseng-ai/foundation/result";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
AregSkillKindDeletePlan,
|
|
5
|
+
AregSkillKindDeleteSymlinkPlan,
|
|
6
|
+
AregSkillKindRemoveEmptyDirPlan,
|
|
7
|
+
AregSkillKindSkillInspection,
|
|
8
|
+
AregSkillKindTextWritePlan,
|
|
9
|
+
} from "../gateways.ts";
|
|
10
|
+
import { replacementAdvice, verifyPiReplacement } from "./pi-replacement.ts";
|
|
11
|
+
import {
|
|
12
|
+
agentsSkillMirrorRelativePath,
|
|
13
|
+
claudeSkillMirrorRelativePath,
|
|
14
|
+
expectedAgentsSkillSymlinkTarget,
|
|
15
|
+
expectedClaudeSkillSymlinkTarget,
|
|
16
|
+
} from "./skill-mirror-conventions.ts";
|
|
17
|
+
import { parsePiSettings, type PiSettingsData } from "./pi-settings.ts";
|
|
18
|
+
import {
|
|
19
|
+
PROJECT_FILE_MUTATION_OPERATION_TYPES,
|
|
20
|
+
PROJECT_MUTATION_OPERATION_TYPES,
|
|
21
|
+
type ProjectMutationOperationStatusRecord,
|
|
22
|
+
} from "./project-mutations.ts";
|
|
23
|
+
import { planFrontmatterOperation } from "./skill-kind-frontmatter.ts";
|
|
24
|
+
import {
|
|
25
|
+
isLegacyBareOpenaiPolicyContent,
|
|
26
|
+
isManagedOpenaiPolicyContent,
|
|
27
|
+
KIND_PROPERTIES,
|
|
28
|
+
MANAGED_OPENAI_POLICY,
|
|
29
|
+
type SkillInvocationKind,
|
|
30
|
+
type SkillKindProjectInspection,
|
|
31
|
+
validateInspectableSkill,
|
|
32
|
+
} from "./skill-kind-inference.ts";
|
|
33
|
+
|
|
34
|
+
export const APPLY_OPERATION_TYPES = [...PROJECT_FILE_MUTATION_OPERATION_TYPES, "skip"] as const;
|
|
35
|
+
export const APPLY_STATUS_OPERATION_TYPES = [...PROJECT_MUTATION_OPERATION_TYPES, "skip"] as const;
|
|
36
|
+
|
|
37
|
+
export type ApplyOperationType = (typeof APPLY_OPERATION_TYPES)[number];
|
|
38
|
+
|
|
39
|
+
export interface PlannedApplyOperationBase {
|
|
40
|
+
type: ApplyOperationType;
|
|
41
|
+
relativePath: string;
|
|
42
|
+
description: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PlannedWriteOperation extends PlannedApplyOperationBase {
|
|
46
|
+
type: "write";
|
|
47
|
+
content: string;
|
|
48
|
+
shouldCreateParent: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PlannedSkipOperation extends PlannedApplyOperationBase {
|
|
52
|
+
type: "skip";
|
|
53
|
+
reason: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface PlannedDeleteOperation extends PlannedApplyOperationBase {
|
|
57
|
+
type: "delete";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PlannedRemoveEmptyDirOperation extends PlannedApplyOperationBase {
|
|
61
|
+
type: "remove-empty-dir";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface PlannedDeleteSymlinkOperation extends PlannedApplyOperationBase {
|
|
65
|
+
type: "delete-symlink";
|
|
66
|
+
expectedTarget: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type PlannedApplyOperation =
|
|
70
|
+
| PlannedWriteOperation
|
|
71
|
+
| PlannedSkipOperation
|
|
72
|
+
| PlannedDeleteOperation
|
|
73
|
+
| PlannedDeleteSymlinkOperation
|
|
74
|
+
| PlannedRemoveEmptyDirOperation;
|
|
75
|
+
|
|
76
|
+
export interface SkillKindApplyPlan {
|
|
77
|
+
skill: string;
|
|
78
|
+
kind: SkillInvocationKind;
|
|
79
|
+
operations: readonly PlannedApplyOperation[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SkillKindApplyOperationResult {
|
|
83
|
+
type: ApplyOperationType;
|
|
84
|
+
path: string;
|
|
85
|
+
reason?: string;
|
|
86
|
+
isApplied: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SkillKindApplySkipStatusResult {
|
|
90
|
+
type: "skip";
|
|
91
|
+
path: string;
|
|
92
|
+
description: string;
|
|
93
|
+
status: "skipped";
|
|
94
|
+
error?: unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type SkillKindApplyOperationStatusResult =
|
|
98
|
+
| ProjectMutationOperationStatusRecord
|
|
99
|
+
| SkillKindApplySkipStatusResult;
|
|
100
|
+
|
|
101
|
+
export function buildSkillKindApplyPlan(
|
|
102
|
+
inspection: SkillKindProjectInspection,
|
|
103
|
+
skillName: string,
|
|
104
|
+
kind: SkillInvocationKind,
|
|
105
|
+
): Result<SkillKindApplyPlan> {
|
|
106
|
+
const skill = inspection.skills.find((candidate) => candidate.name === skillName);
|
|
107
|
+
if (skill === undefined)
|
|
108
|
+
return err({ code: "skill_not_found", message: `Managed skill not found: ${skillName}` });
|
|
109
|
+
const readiness = validateInspectableSkill(skill);
|
|
110
|
+
if (!readiness.ok) return readiness;
|
|
111
|
+
if (skill.skillMd.type !== "file")
|
|
112
|
+
return err({
|
|
113
|
+
code: "skill_not_found",
|
|
114
|
+
message: `${skill.baseRelativePath}/SKILL.md does not exist`,
|
|
115
|
+
});
|
|
116
|
+
if (kind === "command-backed") {
|
|
117
|
+
const replacement = verifyPiReplacement(skill.name, inspection.replacement);
|
|
118
|
+
if (!replacement.verified)
|
|
119
|
+
return err({
|
|
120
|
+
code: "skill_not_found",
|
|
121
|
+
message: replacementAdvice(skill.name, replacement.surface),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
if (kind === "unlisted") {
|
|
125
|
+
if (skill.sourceType !== "repo")
|
|
126
|
+
return err({
|
|
127
|
+
code: "unlisted_requires_first_party",
|
|
128
|
+
message: `Skill '${skill.name}' is not first-party (${skill.baseRelativePath}); unlisted only applies to skills/<name>/ sources.`,
|
|
129
|
+
});
|
|
130
|
+
const replacement = verifyPiReplacement(skill.name, inspection.replacement);
|
|
131
|
+
if (replacement.surface !== undefined)
|
|
132
|
+
return err({
|
|
133
|
+
code: "unlisted_registry_surface_present",
|
|
134
|
+
message: `Skill '${skill.name}' still has a COMMAND_BACKED_SKILL_REGISTRY entry (/${replacement.surface}); remove the registry entry first, then apply unlisted.`,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const piSettings = parsePiSettings(inspection.piDir, inspection.piSettings);
|
|
138
|
+
if (!piSettings.ok) return piSettings;
|
|
139
|
+
const frontmatter = planFrontmatterOperation(skill.baseRelativePath, skill.skillMd.text, kind);
|
|
140
|
+
if (!frontmatter.ok) return frontmatter;
|
|
141
|
+
const sidecar = planSidecarOperations(skill, kind);
|
|
142
|
+
if (!sidecar.ok) return sidecar;
|
|
143
|
+
const pi = planPiSettingsOperation(skill.name, kind, piSettings.value);
|
|
144
|
+
if (!pi.ok) return pi;
|
|
145
|
+
const mirrors = planMirrorOperations(skill, kind);
|
|
146
|
+
if (!mirrors.ok) return mirrors;
|
|
147
|
+
return {
|
|
148
|
+
ok: true,
|
|
149
|
+
value: {
|
|
150
|
+
skill: skill.name,
|
|
151
|
+
kind,
|
|
152
|
+
operations: [frontmatter.value, ...sidecar.value, pi.value, ...mirrors.value],
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function inspectionAfterPlannedApply(
|
|
158
|
+
inspection: SkillKindProjectInspection,
|
|
159
|
+
plan: SkillKindApplyPlan,
|
|
160
|
+
): SkillKindProjectInspection {
|
|
161
|
+
const piSettingsWrite = plan.operations.find(
|
|
162
|
+
(operation): operation is PlannedWriteOperation =>
|
|
163
|
+
operation.type === "write" && operation.relativePath === ".pi/settings.json",
|
|
164
|
+
);
|
|
165
|
+
return {
|
|
166
|
+
...inspection,
|
|
167
|
+
piSettings:
|
|
168
|
+
piSettingsWrite === undefined
|
|
169
|
+
? inspection.piSettings
|
|
170
|
+
: { type: "file", text: piSettingsWrite.content },
|
|
171
|
+
skills: inspection.skills.map((skill) =>
|
|
172
|
+
skill.name === plan.skill ? skillAfterPlannedApply(skill, plan) : skill,
|
|
173
|
+
),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function skillAfterPlannedApply(
|
|
178
|
+
skill: AregSkillKindSkillInspection,
|
|
179
|
+
plan: SkillKindApplyPlan,
|
|
180
|
+
): AregSkillKindSkillInspection {
|
|
181
|
+
let skillMd = skill.skillMd;
|
|
182
|
+
let openaiPolicy = skill.openaiPolicy;
|
|
183
|
+
let agentsPath = skill.agentsPath;
|
|
184
|
+
let claudePath = skill.claudePath;
|
|
185
|
+
for (const operation of plan.operations) {
|
|
186
|
+
if (
|
|
187
|
+
operation.type === "write" &&
|
|
188
|
+
operation.relativePath === `${skill.baseRelativePath}/SKILL.md`
|
|
189
|
+
)
|
|
190
|
+
skillMd = { type: "file", text: operation.content };
|
|
191
|
+
if (
|
|
192
|
+
operation.type === "write" &&
|
|
193
|
+
operation.relativePath === `${skill.baseRelativePath}/agents/openai.yaml`
|
|
194
|
+
)
|
|
195
|
+
openaiPolicy = { type: "file", text: operation.content };
|
|
196
|
+
if (
|
|
197
|
+
operation.type === "delete" &&
|
|
198
|
+
operation.relativePath === `${skill.baseRelativePath}/agents/openai.yaml`
|
|
199
|
+
)
|
|
200
|
+
openaiPolicy = { type: "missing" };
|
|
201
|
+
if (
|
|
202
|
+
operation.type === "delete-symlink" &&
|
|
203
|
+
operation.relativePath === agentsSkillMirrorRelativePath(skill.name)
|
|
204
|
+
)
|
|
205
|
+
agentsPath = { type: "missing" };
|
|
206
|
+
if (
|
|
207
|
+
operation.type === "delete-symlink" &&
|
|
208
|
+
operation.relativePath === claudeSkillMirrorRelativePath(skill.name)
|
|
209
|
+
)
|
|
210
|
+
claudePath = { type: "missing" };
|
|
211
|
+
}
|
|
212
|
+
return { ...skill, skillMd, openaiPolicy, agentsPath, claudePath };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Plans skill mirror symlink removals. Only the unlisted kind manages mirrors,
|
|
217
|
+
* and only in the delete direction: areg never creates mirror symlinks, so
|
|
218
|
+
* reverting unlisted back to a listed kind requires re-running the
|
|
219
|
+
* skill-management install flow (areg check stays red until the mirrors are
|
|
220
|
+
* reinstalled).
|
|
221
|
+
*/
|
|
222
|
+
export function planMirrorOperations(
|
|
223
|
+
skill: AregSkillKindSkillInspection,
|
|
224
|
+
kind: SkillInvocationKind,
|
|
225
|
+
): Result<readonly PlannedApplyOperation[]> {
|
|
226
|
+
if (kind !== "unlisted") return { ok: true, value: [] };
|
|
227
|
+
const mirrors = [
|
|
228
|
+
{
|
|
229
|
+
relativePath: claudeSkillMirrorRelativePath(skill.name),
|
|
230
|
+
description: "Claude skill mirror symlink",
|
|
231
|
+
expectedTarget: expectedClaudeSkillSymlinkTarget(skill.name),
|
|
232
|
+
state: skill.claudePath,
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
relativePath: agentsSkillMirrorRelativePath(skill.name),
|
|
236
|
+
description: "agents skill mirror symlink",
|
|
237
|
+
expectedTarget: expectedAgentsSkillSymlinkTarget(skill.name),
|
|
238
|
+
state: skill.agentsPath,
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
const operations: PlannedApplyOperation[] = [];
|
|
242
|
+
for (const mirror of mirrors) {
|
|
243
|
+
if (mirror.state.type === "missing") {
|
|
244
|
+
operations.push({
|
|
245
|
+
type: "skip",
|
|
246
|
+
relativePath: mirror.relativePath,
|
|
247
|
+
description: mirror.description,
|
|
248
|
+
reason: `${mirror.relativePath} absent`,
|
|
249
|
+
});
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (mirror.state.type !== "symlink")
|
|
253
|
+
return err({
|
|
254
|
+
code: "mirror_not_symlink",
|
|
255
|
+
message: `${mirror.relativePath} exists but is not a symlink; resolve it manually before applying unlisted.`,
|
|
256
|
+
});
|
|
257
|
+
if (mirror.state.target !== mirror.expectedTarget)
|
|
258
|
+
return err({
|
|
259
|
+
code: "mirror_wrong_target",
|
|
260
|
+
message: `${mirror.relativePath} points to ${mirror.state.target}, expected ${mirror.expectedTarget}; resolve it manually before applying unlisted.`,
|
|
261
|
+
});
|
|
262
|
+
operations.push({
|
|
263
|
+
type: "delete-symlink",
|
|
264
|
+
relativePath: mirror.relativePath,
|
|
265
|
+
description: mirror.description,
|
|
266
|
+
expectedTarget: mirror.expectedTarget,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
return { ok: true, value: operations };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function planSidecarOperations(
|
|
273
|
+
skill: AregSkillKindSkillInspection,
|
|
274
|
+
kind: SkillInvocationKind,
|
|
275
|
+
): Result<readonly PlannedApplyOperation[]> {
|
|
276
|
+
const relativePath = `${skill.baseRelativePath}/agents/openai.yaml`;
|
|
277
|
+
const agentsDir = `${skill.baseRelativePath}/agents`;
|
|
278
|
+
const shouldExist = KIND_PROPERTIES[kind].hasCodexSidecar;
|
|
279
|
+
if (shouldExist) {
|
|
280
|
+
if (skill.openaiPolicy.type === "symlink")
|
|
281
|
+
return err({
|
|
282
|
+
code: "path_symlink",
|
|
283
|
+
message: `${relativePath} is a symlink; refusing to manage it.`,
|
|
284
|
+
});
|
|
285
|
+
if (skill.openaiPolicy.type === "file") {
|
|
286
|
+
if (isManagedOpenaiPolicyContent(skill.openaiPolicy.text))
|
|
287
|
+
return {
|
|
288
|
+
ok: true,
|
|
289
|
+
value: [
|
|
290
|
+
{
|
|
291
|
+
type: "skip",
|
|
292
|
+
relativePath,
|
|
293
|
+
description: "Codex openai.yaml",
|
|
294
|
+
reason: "Codex openai.yaml already current",
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
};
|
|
298
|
+
if (isLegacyBareOpenaiPolicyContent(skill.openaiPolicy.text))
|
|
299
|
+
return {
|
|
300
|
+
ok: true,
|
|
301
|
+
value: [
|
|
302
|
+
{
|
|
303
|
+
type: "write",
|
|
304
|
+
relativePath,
|
|
305
|
+
description: "Codex openai.yaml",
|
|
306
|
+
content: MANAGED_OPENAI_POLICY,
|
|
307
|
+
shouldCreateParent: true,
|
|
308
|
+
},
|
|
309
|
+
],
|
|
310
|
+
};
|
|
311
|
+
return err({
|
|
312
|
+
code: "non_managed_openai_policy",
|
|
313
|
+
message: `${relativePath} exists with non-managed content; resolve it manually before applying ${kind}.`,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (skill.openaiPolicy.type !== "missing")
|
|
317
|
+
return err({ code: "path_not_file", message: `${relativePath} exists but is not a file.` });
|
|
318
|
+
return {
|
|
319
|
+
ok: true,
|
|
320
|
+
value: [
|
|
321
|
+
{
|
|
322
|
+
type: "write",
|
|
323
|
+
relativePath,
|
|
324
|
+
description: "Codex openai.yaml",
|
|
325
|
+
content: MANAGED_OPENAI_POLICY,
|
|
326
|
+
shouldCreateParent: true,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
if (skill.openaiPolicy.type === "missing")
|
|
332
|
+
return {
|
|
333
|
+
ok: true,
|
|
334
|
+
value: [
|
|
335
|
+
{
|
|
336
|
+
type: "skip",
|
|
337
|
+
relativePath,
|
|
338
|
+
description: "Codex openai.yaml",
|
|
339
|
+
reason: "Codex openai.yaml absent",
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
if (skill.openaiPolicy.type === "symlink")
|
|
344
|
+
return err({
|
|
345
|
+
code: "path_symlink",
|
|
346
|
+
message: `${relativePath} is a symlink; refusing to delete it.`,
|
|
347
|
+
});
|
|
348
|
+
if (skill.openaiPolicy.type !== "file")
|
|
349
|
+
return err({ code: "path_not_file", message: `${relativePath} exists but is not a file.` });
|
|
350
|
+
if (!isManagedOpenaiPolicyContent(skill.openaiPolicy.text))
|
|
351
|
+
return err({
|
|
352
|
+
code: "non_managed_openai_policy",
|
|
353
|
+
message: `${relativePath} exists with non-managed content; resolve it manually before applying ${kind}.`,
|
|
354
|
+
});
|
|
355
|
+
return {
|
|
356
|
+
ok: true,
|
|
357
|
+
value: [
|
|
358
|
+
{ type: "delete", relativePath, description: "Codex openai.yaml" },
|
|
359
|
+
{
|
|
360
|
+
type: "remove-empty-dir",
|
|
361
|
+
relativePath: agentsDir,
|
|
362
|
+
description: "empty skill agents directory",
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function planPiSettingsOperation(
|
|
369
|
+
skillName: string,
|
|
370
|
+
kind: SkillInvocationKind,
|
|
371
|
+
settings: PiSettingsData,
|
|
372
|
+
): Result<PlannedApplyOperation> {
|
|
373
|
+
const relativePath = ".pi/settings.json";
|
|
374
|
+
const entry = `-skills/${skillName}`;
|
|
375
|
+
const shouldExclude = KIND_PROPERTIES[kind].isPiExcluded;
|
|
376
|
+
const currentExclusions = settings.exclusions;
|
|
377
|
+
const hasEntry = currentExclusions.includes(entry);
|
|
378
|
+
if (shouldExclude && hasEntry)
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
value: {
|
|
382
|
+
type: "skip",
|
|
383
|
+
relativePath,
|
|
384
|
+
description: "Pi settings",
|
|
385
|
+
reason: `${entry} already present`,
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
if (!shouldExclude && !hasEntry)
|
|
389
|
+
return {
|
|
390
|
+
ok: true,
|
|
391
|
+
value: { type: "skip", relativePath, description: "Pi settings", reason: `${entry} absent` },
|
|
392
|
+
};
|
|
393
|
+
const nextData: Record<string, unknown> = settings.data === undefined ? {} : { ...settings.data };
|
|
394
|
+
const nextSkills = shouldExclude
|
|
395
|
+
? [...currentExclusions, entry]
|
|
396
|
+
: currentExclusions.filter((candidate) => candidate !== entry);
|
|
397
|
+
nextData.skills = nextSkills;
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
value: {
|
|
401
|
+
type: "write",
|
|
402
|
+
relativePath,
|
|
403
|
+
description: "Pi settings",
|
|
404
|
+
content: `${JSON.stringify(nextData, null, 2)}\n`,
|
|
405
|
+
shouldCreateParent: settings.text === undefined,
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function hasDeletionPrompt(plan: SkillKindApplyPlan): boolean {
|
|
411
|
+
return plan.operations.some(isDeletionOperation);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function deletionPrompt(plan: SkillKindApplyPlan): string {
|
|
415
|
+
const paths = plan.operations
|
|
416
|
+
.filter(isDeletionOperation)
|
|
417
|
+
.map((operation) => `- ${operation.relativePath}`)
|
|
418
|
+
.join("\n");
|
|
419
|
+
return `Apply ${plan.kind} to ${plan.skill} will delete managed artifacts:\n${paths}\nContinue?`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function isDeletionOperation(operation: PlannedApplyOperation): boolean {
|
|
423
|
+
return (
|
|
424
|
+
operation.type === "delete" ||
|
|
425
|
+
operation.type === "delete-symlink" ||
|
|
426
|
+
operation.type === "remove-empty-dir"
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function plannedWrites(plan: SkillKindApplyPlan): readonly AregSkillKindTextWritePlan[] {
|
|
431
|
+
return plan.operations.flatMap((operation) =>
|
|
432
|
+
operation.type === "write"
|
|
433
|
+
? [
|
|
434
|
+
{
|
|
435
|
+
relativePath: operation.relativePath,
|
|
436
|
+
content: operation.content,
|
|
437
|
+
description: operation.description,
|
|
438
|
+
createParent: operation.shouldCreateParent,
|
|
439
|
+
},
|
|
440
|
+
]
|
|
441
|
+
: [],
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function plannedDeletes(plan: SkillKindApplyPlan): readonly AregSkillKindDeletePlan[] {
|
|
446
|
+
return plan.operations.flatMap((operation) =>
|
|
447
|
+
operation.type === "delete"
|
|
448
|
+
? [{ relativePath: operation.relativePath, description: operation.description }]
|
|
449
|
+
: [],
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function plannedDeleteSymlinks(
|
|
454
|
+
plan: SkillKindApplyPlan,
|
|
455
|
+
): readonly AregSkillKindDeleteSymlinkPlan[] {
|
|
456
|
+
return plan.operations.flatMap((operation) =>
|
|
457
|
+
operation.type === "delete-symlink"
|
|
458
|
+
? [{ relativePath: operation.relativePath, description: operation.description }]
|
|
459
|
+
: [],
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function plannedRemoveEmptyDirs(
|
|
464
|
+
plan: SkillKindApplyPlan,
|
|
465
|
+
): readonly AregSkillKindRemoveEmptyDirPlan[] {
|
|
466
|
+
return plan.operations.flatMap((operation) =>
|
|
467
|
+
operation.type === "remove-empty-dir"
|
|
468
|
+
? [{ relativePath: operation.relativePath, description: operation.description }]
|
|
469
|
+
: [],
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function toApplyResult(
|
|
474
|
+
operation: PlannedApplyOperation,
|
|
475
|
+
hasAppliedOperation: boolean,
|
|
476
|
+
hasRemovedEmptyDir: boolean,
|
|
477
|
+
): SkillKindApplyOperationResult {
|
|
478
|
+
return {
|
|
479
|
+
type: operation.type,
|
|
480
|
+
path: operation.relativePath,
|
|
481
|
+
...(operation.type === "skip" ? { reason: operation.reason } : {}),
|
|
482
|
+
isApplied:
|
|
483
|
+
operation.type === "skip"
|
|
484
|
+
? false
|
|
485
|
+
: operation.type === "remove-empty-dir"
|
|
486
|
+
? hasRemovedEmptyDir
|
|
487
|
+
: hasAppliedOperation,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export function operationStatusesForPlans(
|
|
492
|
+
plans: readonly SkillKindApplyPlan[],
|
|
493
|
+
operationStatuses: readonly ProjectMutationOperationStatusRecord[],
|
|
494
|
+
): readonly { skill: string; operations: readonly SkillKindApplyOperationStatusResult[] }[] {
|
|
495
|
+
const consumedStatusIndexes = new Set<number>();
|
|
496
|
+
return plans.map((plan) => ({
|
|
497
|
+
skill: plan.skill,
|
|
498
|
+
operations: plan.operations.map((operation) => {
|
|
499
|
+
if (operation.type === "skip")
|
|
500
|
+
return {
|
|
501
|
+
type: operation.type,
|
|
502
|
+
path: operation.relativePath,
|
|
503
|
+
description: operation.description,
|
|
504
|
+
status: "skipped" as const,
|
|
505
|
+
};
|
|
506
|
+
const statusIndex = operationStatuses.findIndex(
|
|
507
|
+
(status, index) =>
|
|
508
|
+
!consumedStatusIndexes.has(index) &&
|
|
509
|
+
status.type === operation.type &&
|
|
510
|
+
status.path === operation.relativePath &&
|
|
511
|
+
status.description === operation.description,
|
|
512
|
+
);
|
|
513
|
+
if (statusIndex === -1)
|
|
514
|
+
return {
|
|
515
|
+
type: operation.type,
|
|
516
|
+
path: operation.relativePath,
|
|
517
|
+
description: operation.description,
|
|
518
|
+
status: "not-attempted" as const,
|
|
519
|
+
};
|
|
520
|
+
consumedStatusIndexes.add(statusIndex);
|
|
521
|
+
return (
|
|
522
|
+
operationStatuses[statusIndex] ?? {
|
|
523
|
+
type: operation.type,
|
|
524
|
+
path: operation.relativePath,
|
|
525
|
+
description: operation.description,
|
|
526
|
+
status: "not-attempted" as const,
|
|
527
|
+
}
|
|
528
|
+
);
|
|
529
|
+
}),
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Result } from "@nseng-ai/foundation/result";
|
|
2
|
+
|
|
3
|
+
import { transformSkillFrontmatter } from "./frontmatter.ts";
|
|
4
|
+
import {
|
|
5
|
+
DISABLE_MODEL_INVOCATION_KEY,
|
|
6
|
+
KIND_PROPERTIES,
|
|
7
|
+
USER_INVOCABLE_KEY,
|
|
8
|
+
type SkillInvocationKind,
|
|
9
|
+
} from "./skill-kind-inference.ts";
|
|
10
|
+
import type { PlannedApplyOperation } from "./skill-kind-apply-plan.ts";
|
|
11
|
+
|
|
12
|
+
export function planFrontmatterOperation(
|
|
13
|
+
baseRelativePath: string,
|
|
14
|
+
text: string,
|
|
15
|
+
kind: SkillInvocationKind,
|
|
16
|
+
): Result<PlannedApplyOperation> {
|
|
17
|
+
const relativePath = `${baseRelativePath}/SKILL.md`;
|
|
18
|
+
// Inference parses frontmatter into normalized key/value facts, while apply planning
|
|
19
|
+
// rewrites source text and must preserve delimiter bounds, line endings, unrelated
|
|
20
|
+
// keys, and body text. Keep the parse and rewrite paths separate even though both
|
|
21
|
+
// validate frontmatter before changing managed keys.
|
|
22
|
+
const transformed = transformSkillFrontmatter(text, relativePath, desiredFrontmatter(kind));
|
|
23
|
+
if (!transformed.ok) return transformed;
|
|
24
|
+
if (transformed.value === text)
|
|
25
|
+
return {
|
|
26
|
+
ok: true,
|
|
27
|
+
value: {
|
|
28
|
+
type: "skip",
|
|
29
|
+
relativePath,
|
|
30
|
+
description: "SKILL.md",
|
|
31
|
+
reason: "SKILL.md frontmatter already current",
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
ok: true,
|
|
36
|
+
value: {
|
|
37
|
+
type: "write",
|
|
38
|
+
relativePath,
|
|
39
|
+
description: "SKILL.md",
|
|
40
|
+
content: transformed.value,
|
|
41
|
+
shouldCreateParent: false,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function desiredFrontmatter(
|
|
47
|
+
kind: SkillInvocationKind,
|
|
48
|
+
): Readonly<Record<string, string | undefined>> {
|
|
49
|
+
return {
|
|
50
|
+
[DISABLE_MODEL_INVOCATION_KEY]: KIND_PROPERTIES[kind].shouldDisableModelInvocation
|
|
51
|
+
? "true"
|
|
52
|
+
: undefined,
|
|
53
|
+
[USER_INVOCABLE_KEY]: kind === "ambient-only" ? "false" : undefined,
|
|
54
|
+
};
|
|
55
|
+
}
|