@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.
Files changed (43) hide show
  1. package/package.json +7 -6
  2. package/src/cli.ts +0 -33
  3. package/src/context.ts +3 -29
  4. package/src/fake-gateways.ts +108 -380
  5. package/src/gateways/errors.ts +2 -2
  6. package/src/gateways/fs-utils.ts +2 -2
  7. package/src/gateways/project-fs.ts +29 -41
  8. package/src/gateways/project-gateway.ts +98 -31
  9. package/src/gateways.ts +34 -150
  10. package/src/index.ts +2 -28
  11. package/src/operations/check.ts +33 -20
  12. package/src/operations/doctor-skills.ts +71 -7
  13. package/src/operations/file-state.ts +4 -4
  14. package/src/operations/manifest-source-findings.ts +45 -0
  15. package/src/operations/manifest-sources.ts +119 -0
  16. package/src/operations/pi-settings.ts +2 -5
  17. package/src/operations/project-inspection.ts +32 -28
  18. package/src/operations/project-mutations.ts +5 -21
  19. package/src/operations/project-resolution.ts +2 -2
  20. package/src/operations/skill-find.ts +23 -3
  21. package/src/operations/skill-kind-apply-plan.ts +2 -2
  22. package/src/operations/skill-kind-frontmatter.ts +1 -1
  23. package/src/operations/skill-kind-inference.ts +30 -14
  24. package/src/operations/skill-kind.ts +34 -12
  25. package/src/sort.ts +3 -3
  26. package/src/gateways/command-constants.ts +0 -1
  27. package/src/gateways/github-gateway.ts +0 -111
  28. package/src/gateways/host-gateway.ts +0 -35
  29. package/src/gateways/mutation-policy.ts +0 -94
  30. package/src/gateways/npx-skills-gateway.ts +0 -53
  31. package/src/gateways/prompt-gateway.ts +0 -21
  32. package/src/gateways/skillx-workspace-gateway.ts +0 -220
  33. package/src/operations/frontmatter.ts +0 -151
  34. package/src/operations/init.ts +0 -548
  35. package/src/operations/lockfile.ts +0 -107
  36. package/src/operations/managed-markdown-block.ts +0 -47
  37. package/src/operations/project-agents.ts +0 -115
  38. package/src/operations/skill-mirror-conventions.ts +0 -126
  39. package/src/operations/skillx.ts +0 -305
  40. package/src/operations/toml-section.ts +0 -53
  41. package/src/operations/update-skills.ts +0 -325
  42. package/src/real-gateways.ts +0 -6
  43. package/src/skill-lookup.ts +0 -254
@@ -1,548 +0,0 @@
1
- import path from "node:path";
2
-
3
- import {
4
- confirmInteractiveOrUsageError,
5
- failure,
6
- ok,
7
- usageError,
8
- type ClinkrExit,
9
- } from "@nseng-ai/clinkr";
10
- import { resultErr, type Result } from "@nseng-ai/foundation/result";
11
- import { z } from "zod";
12
-
13
- import type { AregCliContext } from "../context.ts";
14
- import type { AregInitTextWritePlan, AregPathState, AregTextFileState } from "../gateways.ts";
15
- import { rejectTextState, validateOptionalDirectoryState } from "./file-state.ts";
16
- import {
17
- appendBlock,
18
- contentWithoutManagedBlock,
19
- managedBlockBounds,
20
- type ManagedMarkers,
21
- } from "./managed-markdown-block.ts";
22
- import { parseNsAregAgents, resolveProjectAgents } from "./project-agents.ts";
23
- import { inspectInitProject } from "./project-inspection.ts";
24
- import {
25
- applyProjectMutationPlan,
26
- PROJECT_MUTATION_OPERATION_STATUSES,
27
- PROJECT_MUTATION_OPERATION_TYPES,
28
- type ProjectMutationOperationStatusRecord,
29
- } from "./project-mutations.ts";
30
- import { renderAregSection, replaceOrAppendAregSection } from "./toml-section.ts";
31
-
32
- export {
33
- parseNsAregAgents,
34
- parseLegacyAregJsonAgents,
35
- resolveProjectAgents,
36
- } from "./project-agents.ts";
37
-
38
- const BOOTSTRAP_REPO = "dagster-io/asdl-tools";
39
- const BOOTSTRAP_SKILLS = ["skill-management", "skillx"] as const;
40
- const AGENTS_BLOCK_START = "<!-- areg:skills:start -->";
41
- const AGENTS_BLOCK_END = "<!-- areg:skills:end -->";
42
- const CLAUDE_BLOCK_START = "<!-- areg:claude-skills:start -->";
43
- const CLAUDE_BLOCK_END = "<!-- areg:claude-skills:end -->";
44
-
45
- export const AGENTS_BLOCK = [
46
- AGENTS_BLOCK_START,
47
- "## Skills",
48
- "",
49
- "This project uses agent skills installed on disk.",
50
- "",
51
- "- Discover installed skills from their `SKILL.md` frontmatter under `.agents/skills/`; do not keep a duplicate skill index in this file.",
52
- "- Local first-party skills live in `skills/<name>/`; `.agents/skills/<name>` should symlink to `../../skills/<name>`.",
53
- "- GitHub-sourced or vendored skills live as real directories under `.agents/skills/<name>/`; do not refactor or lint them as first-party project code unless explicitly asked.",
54
- "- `.claude/skills/<name>` entries symlink to `../../.agents/skills/<name>` for Claude Code.",
55
- "- For persistent skill add, update, remove, list, and publish workflows, use the installed `skill-management` skill, which documents the `npx skills` commands for this project.",
56
- AGENTS_BLOCK_END,
57
- ].join("\n");
58
-
59
- const CLAUDE_NOTE =
60
- "Claude Code discovers installed skills from `.claude/skills/`, which symlinks into `.agents/skills/`. Use Claude's skill invocation UI for installed skills when available. Use `skill-management` for persistent skill changes and `skillx` for transient GitHub skill execution.";
61
-
62
- export const SETTINGS_LOCAL_JSON = `{
63
- "permissions": {
64
- "allow": [
65
- "Bash(npx skills:*)"
66
- ]
67
- }
68
- }
69
- `;
70
-
71
- export const initRequestSchema = z.object({
72
- target: z.string().default(".").describe("Project directory to initialize."),
73
- agent: z.array(z.string()).default([]).describe("Agent to install skills for; repeatable."),
74
- yes: z
75
- .boolean()
76
- .default(false)
77
- .describe("Approve adding or updating areg-managed instruction blocks without prompting."),
78
- append: z
79
- .boolean()
80
- .default(true)
81
- .describe("Do not modify existing AGENTS.md or CLAUDE.md prose files."),
82
- });
83
-
84
- const skippedFileSchema = z.object({
85
- path: z.string(),
86
- reason: z.string(),
87
- });
88
-
89
- const initOperationStatusSchema = z.object({
90
- type: z.enum(PROJECT_MUTATION_OPERATION_TYPES),
91
- path: z.string(),
92
- description: z.string(),
93
- status: z.enum(PROJECT_MUTATION_OPERATION_STATUSES),
94
- error: z.unknown().optional(),
95
- });
96
-
97
- export const initResultSchema = z.object({
98
- projectDir: z.string(),
99
- agents: z.array(z.string()),
100
- bootstrapRepo: z.string(),
101
- bootstrapSkills: z.array(z.string()),
102
- writtenFiles: z.array(z.string()),
103
- skippedFiles: z.array(skippedFileSchema),
104
- mutationFailed: z.boolean().optional(),
105
- operations: z.array(initOperationStatusSchema).optional(),
106
- });
107
-
108
- export type InitRequest = z.infer<typeof initRequestSchema>;
109
- export type InitResult = z.infer<typeof initResultSchema>;
110
-
111
- interface SkippedFile {
112
- path: string;
113
- reason: string;
114
- }
115
-
116
- interface InitTextPlan {
117
- writes: readonly AregInitTextWritePlan[];
118
- skippedFiles: readonly SkippedFile[];
119
- }
120
-
121
- interface InitMutationFailureOptions {
122
- message: string;
123
- projectDir: string;
124
- agents: readonly string[];
125
- textPlan: InitTextPlan;
126
- operations: readonly ProjectMutationOperationStatusRecord[];
127
- }
128
-
129
- function initMutationFailure(options: InitMutationFailureOptions): ClinkrExit<InitResult> {
130
- return failure("areg-init-mutation-failed", options.message, {
131
- mutationFailed: true,
132
- projectDir: options.projectDir,
133
- agents: [...options.agents],
134
- bootstrapRepo: BOOTSTRAP_REPO,
135
- bootstrapSkills: [...BOOTSTRAP_SKILLS],
136
- writtenFiles: options.operations
137
- .filter((operation) => operation.type === "write" && operation.status === "applied")
138
- .map((operation) => operation.path),
139
- skippedFiles: options.textPlan.skippedFiles.map((skipped) => ({ ...skipped })),
140
- operations: options.operations.map((operation) => ({ ...operation })),
141
- });
142
- }
143
-
144
- function npxSkillsAddOperation(
145
- status: ProjectMutationOperationStatusRecord["status"],
146
- error?: ProjectMutationOperationStatusRecord["error"],
147
- ): ProjectMutationOperationStatusRecord {
148
- return {
149
- type: "external",
150
- path: "npx skills add",
151
- description: `Install bootstrap skills from ${BOOTSTRAP_REPO}`,
152
- status,
153
- ...(error === undefined ? {} : { error }),
154
- };
155
- }
156
-
157
- export async function runInit(
158
- ctx: AregCliContext,
159
- request: InitRequest,
160
- ): Promise<ClinkrExit<InitResult>> {
161
- const noAppend = !request.append;
162
- if (request.yes && noAppend) {
163
- return failure("invalid-request", "--yes and --no-append cannot be used together.");
164
- }
165
-
166
- const tool = await ctx.host.checkTool({ tool: "npx", cwd: ctx.cwd, env: ctx.env });
167
- if (tool.type === "missing") return failure("missing-tool", tool.message);
168
-
169
- const inspection = await inspectInitProject(ctx, request.target);
170
- if (inspection.projectPathState.type === "missing")
171
- return failure("invalid-project", `Target ${inspection.projectDir} does not exist.`);
172
- if (inspection.projectPathState.type !== "directory")
173
- return failure("invalid-project", `${inspection.projectDir} is not a directory.`);
174
-
175
- const repoRoot = await ctx.git.optionalRepoRoot({ cwd: inspection.projectDir });
176
- if (repoRoot.type === "error") return failure("git-error", repoRoot.error.message);
177
- if (repoRoot.type === "missing") {
178
- return failure(
179
- "invalid-project",
180
- `Target ${inspection.projectDir} must be a Git worktree root. Run git init first.`,
181
- );
182
- }
183
- if (repoRoot.value !== inspection.projectDir) {
184
- return failure(
185
- "invalid-project",
186
- `Target ${inspection.projectDir} is inside a Git worktree but is not the root. Run areg init ${repoRoot.value} instead.`,
187
- );
188
- }
189
-
190
- const agentsResult = resolveProjectAgents({
191
- explicitAgents: request.agent,
192
- nsToml: inspection.nsToml,
193
- aregJson: inspection.aregJson,
194
- });
195
- if (!agentsResult.ok) return failure("agent-resolution-failed", agentsResult.error.message);
196
- const agents = agentsResult.value;
197
-
198
- const planResult = await buildInitTextPlan(ctx, inspection, {
199
- agents,
200
- yes: request.yes,
201
- noAppend,
202
- });
203
- if (!planResult.ok) {
204
- if (planResult.error.code === "confirmation_required") {
205
- return usageError(planResult.error.message, {
206
- missingFlag: "--yes",
207
- howToSupply:
208
- "Pass --yes (or -y) to approve managed instruction block changes without prompting.",
209
- });
210
- }
211
- if (planResult.error.code === "aborted") return failure("aborted", "Aborted!");
212
- return failure("write-plan-failed", planResult.error.message);
213
- }
214
- const textPlan = planResult.value;
215
-
216
- const preflight = await applyProjectMutationPlan({
217
- ctx,
218
- projectDir: inspection.projectDir,
219
- policy: "init",
220
- writes: textPlan.writes,
221
- execute: false,
222
- });
223
- if (!preflight.ok) {
224
- return initMutationFailure({
225
- message: `Cannot initialize areg: ${preflight.error.message}`,
226
- projectDir: inspection.projectDir,
227
- agents,
228
- textPlan,
229
- operations: [npxSkillsAddOperation("not-attempted"), ...preflight.operationStatuses],
230
- });
231
- }
232
-
233
- const install = await ctx.npxSkills.addSkills({
234
- sourceRepo: BOOTSTRAP_REPO,
235
- skillNames: BOOTSTRAP_SKILLS,
236
- targetAgents: agents,
237
- cwd: inspection.projectDir,
238
- env: ctx.env,
239
- });
240
- if (install.type === "error") {
241
- return initMutationFailure({
242
- message: `npx skills add failed: ${install.error.message}`,
243
- projectDir: inspection.projectDir,
244
- agents,
245
- textPlan,
246
- operations: [npxSkillsAddOperation("failed", install.error), ...preflight.operationStatuses],
247
- });
248
- }
249
-
250
- const apply = await applyProjectMutationPlan({
251
- ctx,
252
- projectDir: inspection.projectDir,
253
- policy: "init",
254
- writes: textPlan.writes,
255
- });
256
- if (!apply.ok) {
257
- return initMutationFailure({
258
- message: `Cannot finish areg init: ${apply.error.message}`,
259
- projectDir: inspection.projectDir,
260
- agents,
261
- textPlan,
262
- operations: [npxSkillsAddOperation("applied"), ...apply.operationStatuses],
263
- });
264
- }
265
-
266
- return ok({
267
- projectDir: inspection.projectDir,
268
- agents: [...agents],
269
- bootstrapRepo: BOOTSTRAP_REPO,
270
- bootstrapSkills: [...BOOTSTRAP_SKILLS],
271
- writtenFiles: [...apply.appliedPaths.written],
272
- skippedFiles: textPlan.skippedFiles.map((skipped) => ({ ...skipped })),
273
- });
274
- }
275
-
276
- export function renderInit(result: InitResult): string {
277
- return [
278
- `Initialized areg in ${result.projectDir}`,
279
- "Bootstrap skills installed: skill-management, skillx",
280
- "Review and commit generated files when ready.",
281
- "Install more persistent skills with `npx skills add ...`.",
282
- ].join("\n");
283
- }
284
-
285
- export function claudeBlock(options: { includeAgentsRef: boolean }): string {
286
- const lines = [CLAUDE_BLOCK_START, "## Claude Code skills", ""];
287
- if (options.includeAgentsRef) lines.push("@AGENTS.md", "");
288
- lines.push(CLAUDE_NOTE, CLAUDE_BLOCK_END);
289
- return lines.join("\n");
290
- }
291
-
292
- async function buildInitTextPlan(
293
- ctx: AregCliContext,
294
- inspection: {
295
- projectDir: string;
296
- agentsMd: AregTextFileState;
297
- claudeMd: AregTextFileState;
298
- nsToml: AregTextFileState;
299
- claudeDir: AregPathState;
300
- claudeSettings: AregTextFileState;
301
- },
302
- options: { agents: readonly string[]; yes: boolean; noAppend: boolean },
303
- ): Promise<Result<InitTextPlan>> {
304
- const ns = planNsToml(inspection.nsToml, options.agents);
305
- if (!ns.ok) return ns;
306
-
307
- const agents = await planManagedBlock(ctx, {
308
- path: "AGENTS.md",
309
- state: inspection.agentsMd,
310
- newFileContent: `# Agents\n\n${AGENTS_BLOCK}\n`,
311
- block: AGENTS_BLOCK,
312
- markers: { start: AGENTS_BLOCK_START, end: AGENTS_BLOCK_END },
313
- yes: options.yes,
314
- noAppend: options.noAppend,
315
- appendPrompt: "AGENTS.md exists without an areg-managed Skills block. Add one?",
316
- updatePrompt: "AGENTS.md has an existing areg-managed Skills block. Replace it?",
317
- });
318
- if (!agents.ok) return agents;
319
-
320
- const claude = await planClaudeMd(ctx, inspection.projectDir, inspection.claudeMd, {
321
- yes: options.yes,
322
- noAppend: options.noAppend,
323
- });
324
- if (!claude.ok) return claude;
325
-
326
- const settings = planSettings(inspection.claudeDir, inspection.claudeSettings);
327
- if (!settings.ok) return settings;
328
-
329
- const textPlans = [agents.value, claude.value, settings.value];
330
- return {
331
- ok: true,
332
- value: {
333
- writes: [ns.value, ...textPlans.filter(isTextWritePlan)],
334
- skippedFiles: textPlans.filter(isSkippedFile).map((skipped) => ({ ...skipped })),
335
- },
336
- };
337
- }
338
-
339
- function planNsToml(
340
- state: AregTextFileState,
341
- agents: readonly string[],
342
- ): Result<AregInitTextWritePlan> {
343
- if (state.type === "missing")
344
- return { ok: true, value: writePlan("ns.toml", renderAregSection(agents), "ns.toml") };
345
- if (state.type !== "file")
346
- return rejectTextState({
347
- pathLabel: "ns.toml",
348
- state,
349
- description: "ns.toml",
350
- action: "manage it",
351
- });
352
- const parsed = parseNsAregAgents(state.text, "ns.toml");
353
- if (!parsed.ok) return parsed;
354
- return {
355
- ok: true,
356
- value: writePlan("ns.toml", replaceOrAppendAregSection(state.text, agents), "ns.toml"),
357
- };
358
- }
359
-
360
- async function planClaudeMd(
361
- ctx: AregCliContext,
362
- projectDir: string,
363
- state: AregTextFileState,
364
- options: { yes: boolean; noAppend: boolean },
365
- ): Promise<Result<AregInitTextWritePlan | SkippedFile>> {
366
- let includeAgentsRef = true;
367
- if (state.type === "file") {
368
- const outside = contentWithoutManagedBlock(
369
- state.text,
370
- { start: CLAUDE_BLOCK_START, end: CLAUDE_BLOCK_END },
371
- "CLAUDE.md",
372
- );
373
- if (!outside.ok) return outside;
374
- includeAgentsRef = !outside.value.includes("@AGENTS.md");
375
- }
376
- const block = claudeBlock({ includeAgentsRef });
377
- return await planManagedBlock(ctx, {
378
- path: "CLAUDE.md",
379
- state,
380
- newFileContent: `# ${path.basename(projectDir)}\n\n${block}\n`,
381
- block,
382
- markers: { start: CLAUDE_BLOCK_START, end: CLAUDE_BLOCK_END },
383
- yes: options.yes,
384
- noAppend: options.noAppend,
385
- appendPrompt: "CLAUDE.md exists without an areg-managed Claude skills block. Add one?",
386
- updatePrompt: "CLAUDE.md has an existing areg-managed Claude skills block. Replace it?",
387
- });
388
- }
389
-
390
- async function planManagedBlock(
391
- ctx: AregCliContext,
392
- input: {
393
- path: "AGENTS.md" | "CLAUDE.md";
394
- state: AregTextFileState;
395
- newFileContent: string;
396
- block: string;
397
- markers: ManagedMarkers;
398
- yes: boolean;
399
- noAppend: boolean;
400
- appendPrompt: string;
401
- updatePrompt: string;
402
- },
403
- ): Promise<Result<AregInitTextWritePlan | SkippedFile>> {
404
- if (input.state.type === "missing")
405
- return { ok: true, value: writePlan(input.path, input.newFileContent, input.path) };
406
- if (input.state.type !== "file")
407
- return rejectTextState({
408
- pathLabel: input.path,
409
- state: input.state,
410
- description: input.path,
411
- action: "manage it",
412
- });
413
- const bounds = managedBlockBounds(input.state.text, input.markers, input.path);
414
- if (!bounds.ok) return bounds;
415
- if (bounds.value === null) {
416
- if (input.noAppend)
417
- return {
418
- ok: true,
419
- value: {
420
- path: input.path,
421
- reason: "--no-append skips existing file without managed block",
422
- },
423
- };
424
- if (!input.yes) {
425
- const confirmation = await confirmManagedBlockChange(ctx, input.appendPrompt);
426
- if (confirmation.type === "error") return resultErr(confirmation.error);
427
- if (confirmation.type === "declined") {
428
- return {
429
- ok: true,
430
- value: { path: input.path, reason: "user declined adding managed block" },
431
- };
432
- }
433
- }
434
- return {
435
- ok: true,
436
- value: writePlan(input.path, appendBlock(input.state.text, input.block), input.path),
437
- };
438
- }
439
- const currentBlock = input.state.text.slice(bounds.value.start, bounds.value.end);
440
- if (currentBlock === input.block)
441
- return { ok: true, value: { path: input.path, reason: "managed block is already current" } };
442
- if (input.noAppend)
443
- return {
444
- ok: true,
445
- value: { path: input.path, reason: "--no-append skips existing managed block replacement" },
446
- };
447
- if (!input.yes) {
448
- const confirmation = await confirmManagedBlockChange(ctx, input.updatePrompt);
449
- if (confirmation.type === "error") return resultErr(confirmation.error);
450
- if (confirmation.type === "declined") {
451
- return {
452
- ok: true,
453
- value: { path: input.path, reason: "user declined replacing managed block" },
454
- };
455
- }
456
- }
457
- return {
458
- ok: true,
459
- value: writePlan(
460
- input.path,
461
- `${input.state.text.slice(0, bounds.value.start)}${input.block}${input.state.text.slice(bounds.value.end)}`,
462
- input.path,
463
- ),
464
- };
465
- }
466
-
467
- async function confirmManagedBlockChange(
468
- ctx: AregCliContext,
469
- message: string,
470
- ): Promise<
471
- | { type: "confirmed" }
472
- | { type: "declined" }
473
- | { type: "error"; error: { code: string; message: string } }
474
- > {
475
- const confirmed = await confirmInteractiveOrUsageError(ctx.interaction, {
476
- nonInteractive: {
477
- message: "Changing areg-managed instruction blocks requires --yes when non-interactive.",
478
- missingFlag: "--yes",
479
- howToSupply:
480
- "Pass --yes (or -y) to approve managed instruction block changes without prompting.",
481
- },
482
- confirmation: { message, defaultAnswer: "no" },
483
- });
484
- if (confirmed.type === "usageError") {
485
- return {
486
- type: "error",
487
- error: {
488
- code: "confirmation_required",
489
- message: confirmed.message,
490
- },
491
- };
492
- }
493
- if (confirmed.type === "aborted") {
494
- return { type: "error", error: { code: "aborted", message: "Aborted!" } };
495
- }
496
- return confirmed.type === "confirmed" ? { type: "confirmed" } : { type: "declined" };
497
- }
498
-
499
- function planSettings(
500
- claudeDirState: AregPathState,
501
- settingsState: AregTextFileState,
502
- ): Result<AregInitTextWritePlan | SkippedFile> {
503
- const claudeDir = validateOptionalDirectoryState({
504
- pathLabel: ".claude",
505
- state: claudeDirState,
506
- action: "manage it",
507
- symlinkSubject: ".claude at .claude",
508
- });
509
- if (!claudeDir.ok) return claudeDir;
510
- if (settingsState.type === "missing")
511
- return {
512
- ok: true,
513
- value: writePlan(
514
- ".claude/settings.local.json",
515
- SETTINGS_LOCAL_JSON,
516
- "settings.local.json",
517
- true,
518
- ),
519
- };
520
- if (settingsState.type !== "file")
521
- return rejectTextState({
522
- pathLabel: ".claude/settings.local.json",
523
- state: settingsState,
524
- description: "settings.local.json",
525
- action: "manage it",
526
- });
527
- return {
528
- ok: true,
529
- value: { path: ".claude/settings.local.json", reason: "existing settings file is preserved" },
530
- };
531
- }
532
-
533
- function isTextWritePlan(plan: AregInitTextWritePlan | SkippedFile): plan is AregInitTextWritePlan {
534
- return "relativePath" in plan;
535
- }
536
-
537
- function isSkippedFile(plan: AregInitTextWritePlan | SkippedFile): plan is SkippedFile {
538
- return !("relativePath" in plan);
539
- }
540
-
541
- function writePlan(
542
- relativePath: AregInitTextWritePlan["relativePath"],
543
- content: string,
544
- description: string,
545
- createParent = false,
546
- ): AregInitTextWritePlan {
547
- return { relativePath, content, description, createParent };
548
- }
@@ -1,107 +0,0 @@
1
- import { formatErrorMessage, formatZodIssue, optionalEntry } from "@nseng-ai/foundation/primitives";
2
- import { resultErr, type Result } from "@nseng-ai/foundation/result";
3
- import { z } from "zod";
4
-
5
- import type { AregTextFileState } from "../gateways.ts";
6
- import { sortStrings } from "../sort.ts";
7
-
8
- export const SOURCE_TYPES = ["local", "github", "git", "gitlab"] as const;
9
-
10
- export type SourceType = (typeof SOURCE_TYPES)[number];
11
-
12
- export interface LockfileSkillData {
13
- source: string;
14
- sourceType: SourceType;
15
- computedHash: string;
16
- skillPath?: string;
17
- }
18
-
19
- export interface LockfileSkill extends LockfileSkillData {
20
- name: string;
21
- }
22
-
23
- export interface SkillsLockfileData {
24
- version: 1;
25
- skills: Record<string, LockfileSkillData>;
26
- }
27
-
28
- export interface SkillsLockfile {
29
- version: 1;
30
- skills: readonly LockfileSkill[];
31
- }
32
-
33
- const lockfileSkillSchema: z.ZodType<LockfileSkillData> = z
34
- .object({
35
- source: z.string(),
36
- sourceType: z.enum(SOURCE_TYPES),
37
- computedHash: z.string(),
38
- skillPath: z.string().optional(),
39
- })
40
- .transform(
41
- (skill): LockfileSkillData => ({
42
- source: skill.source,
43
- sourceType: skill.sourceType,
44
- computedHash: skill.computedHash,
45
- ...optionalEntry("skillPath", skill.skillPath),
46
- }),
47
- );
48
-
49
- const skillsLockfileSchema: z.ZodType<SkillsLockfileData> = z.object({
50
- version: z.literal(1),
51
- skills: z.record(z.string(), lockfileSkillSchema),
52
- });
53
-
54
- export function parseLockfileData(data: unknown): Result<SkillsLockfile> {
55
- const result = skillsLockfileSchema.safeParse(data);
56
- if (!result.success)
57
- return invalidLockfile(
58
- formatZodIssue(result.error.issues[0], {
59
- rootPath: "$",
60
- pathPrefix: "$.",
61
- fallback: "invalid lockfile",
62
- }),
63
- );
64
- const lockfileData = result.data as SkillsLockfileData;
65
- const skills: LockfileSkill[] = [];
66
- for (const name of sortStrings(Object.keys(lockfileData.skills))) {
67
- const skill = lockfileData.skills[name];
68
- if (skill === undefined) continue;
69
- skills.push({
70
- name,
71
- source: skill.source,
72
- sourceType: skill.sourceType,
73
- computedHash: skill.computedHash,
74
- ...optionalEntry("skillPath", skill.skillPath),
75
- });
76
- }
77
- return { ok: true, value: { version: 1, skills } };
78
- }
79
-
80
- export function parseLockfileText(text: string): Result<SkillsLockfile> {
81
- let data: unknown;
82
- try {
83
- data = JSON.parse(text);
84
- } catch (error) {
85
- return resultErr({
86
- code: "lockfile_invalid_json",
87
- message: `Invalid JSON in skills-lock.json: ${formatErrorMessage(error)}`,
88
- });
89
- }
90
- return parseLockfileData(data);
91
- }
92
-
93
- export function parseInspectedLockfile(input: {
94
- projectDir: string;
95
- lockfile: AregTextFileState;
96
- }): Result<SkillsLockfile> {
97
- if (input.lockfile.type !== "file")
98
- return resultErr({
99
- code: "lockfile_missing",
100
- message: `skills-lock.json not found in ${input.projectDir}. Is this an areg project?`,
101
- });
102
- return parseLockfileText(input.lockfile.text);
103
- }
104
-
105
- function invalidLockfile(reason: string): Result<SkillsLockfile> {
106
- return resultErr({ code: "lockfile_invalid", message: `Invalid skills-lock.json: ${reason}.` });
107
- }
@@ -1,47 +0,0 @@
1
- import { managedRegionBounds } from "@nseng-ai/foundation/managed-region";
2
- import { resultErr, type Result } from "@nseng-ai/foundation/result";
3
-
4
- export interface ManagedMarkers {
5
- start: string;
6
- end: string;
7
- }
8
-
9
- export function managedBlockBounds(
10
- content: string,
11
- markers: ManagedMarkers,
12
- pathLabel: string,
13
- ): Result<{ start: number; end: number } | null> {
14
- const bounds = managedRegionBounds({
15
- text: content,
16
- startMarker: markers.start,
17
- endMarker: markers.end,
18
- });
19
- if (bounds.type === "missing") return { ok: true, value: null };
20
- if (bounds.type === "malformed")
21
- return resultErr({
22
- code: "managed_block_malformed",
23
- message: `${pathLabel} has a malformed areg-managed block. Fix the markers manually.`,
24
- });
25
- return { ok: true, value: { start: bounds.start, end: bounds.end } };
26
- }
27
-
28
- export function appendBlock(content: string, block: string): string {
29
- if (content.length === 0) return `${block}\n`;
30
- if (content.endsWith("\n\n")) return `${content}${block}\n`;
31
- if (content.endsWith("\n")) return `${content}\n${block}\n`;
32
- return `${content}\n\n${block}\n`;
33
- }
34
-
35
- export function contentWithoutManagedBlock(
36
- content: string,
37
- markers: ManagedMarkers,
38
- pathLabel: string,
39
- ): Result<string> {
40
- const bounds = managedBlockBounds(content, markers, pathLabel);
41
- if (!bounds.ok) return bounds;
42
- if (bounds.value === null) return { ok: true, value: content };
43
- return {
44
- ok: true,
45
- value: `${content.slice(0, bounds.value.start)}${content.slice(bounds.value.end)}`,
46
- };
47
- }