@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,220 +0,0 @@
1
- import { lstat, mkdtemp, readdir, realpath, rm } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- import { formatErrorMessage } from "@nseng-ai/foundation/primitives";
6
- import { resultErr, resultOk } from "@nseng-ai/foundation/result";
7
-
8
- import type {
9
- AregErrorInfo,
10
- AregNpxSkillsGateway,
11
- AregOperationResult,
12
- AregSkillxInstallRequest,
13
- AregSkillxInstallResult,
14
- AregSkillxInstalledSkill,
15
- AregSkillxWorkspaceGateway,
16
- } from "../gateways.ts";
17
- import { sortStrings } from "../sort.ts";
18
- import { errorInfo } from "./errors.ts";
19
- import { inspectPath, isNodeErrorCode, isPathAtOrBelow } from "./fs-utils.ts";
20
-
21
- export class RealAregSkillxWorkspaceGateway implements AregSkillxWorkspaceGateway {
22
- private readonly npxSkills: AregNpxSkillsGateway;
23
-
24
- constructor(options: { npxSkills: AregNpxSkillsGateway }) {
25
- this.npxSkills = options.npxSkills;
26
- }
27
-
28
- async installIntoWorkspace(request: AregSkillxInstallRequest): Promise<AregSkillxInstallResult> {
29
- const workspaceRoot = await mkdtemp(path.join(os.tmpdir(), "skillx."));
30
- const install = await this.npxSkills.addSkills({
31
- sourceRepo: request.sourceRepo,
32
- skillNames: request.skillName === undefined ? [] : [request.skillName],
33
- targetAgents: ["codex"],
34
- cwd: workspaceRoot,
35
- env: request.env,
36
- });
37
- if (install.type === "error") {
38
- await removeWorkspaceQuietly(workspaceRoot);
39
- return {
40
- type: "error",
41
- error: errorInfo(
42
- "skillx-install-failed",
43
- `npx skills add failed: ${install.error.message}`,
44
- install.error.displayCommand,
45
- ),
46
- };
47
- }
48
- const inspected = await inspectInstalledSkills(workspaceRoot, request.skillName);
49
- if (inspected.type === "error") {
50
- await removeWorkspaceQuietly(workspaceRoot);
51
- return { type: "error", error: inspected.error };
52
- }
53
- return { type: "ok", workspace: { workspaceRoot, installedSkills: inspected.installedSkills } };
54
- }
55
-
56
- async cleanupWorkspace(request: { workspaceRoot: string }): Promise<AregOperationResult> {
57
- return await cleanupSkillxWorkspace(request.workspaceRoot);
58
- }
59
- }
60
-
61
- async function inspectInstalledSkills(
62
- workspaceRoot: string,
63
- requestedSkillName: string | undefined,
64
- ): Promise<
65
- | { type: "ok"; installedSkills: AregSkillxInstalledSkill[] }
66
- | { type: "error"; error: AregErrorInfo }
67
- > {
68
- const skillsRoot = path.join(workspaceRoot, ".agents", "skills");
69
- const skillsRootState = await inspectPath(skillsRoot);
70
- if (skillsRootState.type !== "directory")
71
- return { type: "error", error: errorInfo("skillx-no-skills", "No skills were installed") };
72
- if (requestedSkillName !== undefined) {
73
- const inspected = await inspectOneSkill(skillsRoot, requestedSkillName);
74
- if (inspected.type === "error") return inspected;
75
- return { type: "ok", installedSkills: [inspected.skill] };
76
- }
77
- const entries = await readdir(skillsRoot, { withFileTypes: true });
78
- const skillNames = sortStrings(
79
- entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name),
80
- );
81
- if (skillNames.length === 0)
82
- return { type: "error", error: errorInfo("skillx-no-skills", "No skills were installed") };
83
- const installedSkills: AregSkillxInstalledSkill[] = [];
84
- for (const skillName of skillNames) {
85
- const inspected = await inspectOneSkill(skillsRoot, skillName);
86
- if (inspected.type === "error") return inspected;
87
- installedSkills.push(inspected.skill);
88
- }
89
- return { type: "ok", installedSkills };
90
- }
91
-
92
- async function inspectOneSkill(
93
- skillsRoot: string,
94
- skillName: string,
95
- ): Promise<
96
- { type: "ok"; skill: AregSkillxInstalledSkill } | { type: "error"; error: AregErrorInfo }
97
- > {
98
- const directory = path.join(skillsRoot, skillName);
99
- const directoryKind = await inspectPath(directory);
100
- if (directoryKind.type !== "directory")
101
- return {
102
- type: "error",
103
- error: errorInfo(
104
- "skillx-skill-missing",
105
- `Skill '${skillName}' was not found in installed skills`,
106
- ),
107
- };
108
- const skillFile = path.join(directory, "SKILL.md");
109
- const fileKind = await inspectPath(skillFile);
110
- if (fileKind.type !== "file")
111
- return {
112
- type: "error",
113
- error: errorInfo(
114
- "skillx-skill-malformed",
115
- `Installed skill '${skillName}' is missing SKILL.md`,
116
- ),
117
- };
118
- return {
119
- type: "ok",
120
- skill: {
121
- name: skillName,
122
- directory,
123
- skillFile,
124
- relativeFiles: await listRelativeFiles(directory),
125
- },
126
- };
127
- }
128
-
129
- async function listRelativeFiles(root: string): Promise<string[]> {
130
- const files: string[] = [];
131
- async function visit(directory: string, prefix: string): Promise<void> {
132
- const entries = await readdir(directory, { withFileTypes: true });
133
- entries.sort((left, right) => left.name.localeCompare(right.name));
134
- for (const entry of entries) {
135
- const relativePath = prefix.length === 0 ? entry.name : `${prefix}/${entry.name}`;
136
- const fullPath = path.join(directory, entry.name);
137
- if (entry.isDirectory()) {
138
- await visit(fullPath, relativePath);
139
- continue;
140
- }
141
- if (entry.isFile()) files.push(relativePath);
142
- }
143
- }
144
- await visit(root, "");
145
- return sortStrings(files);
146
- }
147
-
148
- async function cleanupSkillxWorkspace(workspaceRoot: string): Promise<AregOperationResult> {
149
- if (!path.basename(workspaceRoot).startsWith("skillx.")) {
150
- return resultErr(
151
- errorInfo(
152
- "skillx-cleanup-refused",
153
- `Refusing to remove non-skillx workspace: ${workspaceRoot}`,
154
- ),
155
- );
156
- }
157
- let info;
158
- try {
159
- info = await lstat(workspaceRoot);
160
- } catch (error) {
161
- if (isNodeErrorCode(error, "ENOENT"))
162
- return resultErr(
163
- errorInfo("skillx-cleanup-missing", `Workspace does not exist: ${workspaceRoot}`),
164
- );
165
- return resultErr(
166
- errorInfo(
167
- "skillx-cleanup-stat-failed",
168
- `Could not inspect workspace: ${formatErrorMessage(error)}`,
169
- ),
170
- );
171
- }
172
- if (info.isSymbolicLink())
173
- return resultErr(
174
- errorInfo("skillx-cleanup-symlink", `Refusing to remove symlink workspace: ${workspaceRoot}`),
175
- );
176
- if (!info.isDirectory())
177
- return resultErr(
178
- errorInfo("skillx-cleanup-not-directory", `Workspace is not a directory: ${workspaceRoot}`),
179
- );
180
- let resolvedWorkspace: string;
181
- let resolvedTemp: string;
182
- try {
183
- resolvedWorkspace = await realpath(workspaceRoot);
184
- resolvedTemp = await realpath(os.tmpdir());
185
- } catch (error) {
186
- return resultErr(
187
- errorInfo(
188
- "skillx-cleanup-realpath-failed",
189
- `Could not resolve workspace path: ${formatErrorMessage(error)}`,
190
- ),
191
- );
192
- }
193
- if (!isPathAtOrBelow(resolvedWorkspace, resolvedTemp)) {
194
- return resultErr(
195
- errorInfo(
196
- "skillx-cleanup-outside-temp",
197
- `Refusing to remove workspace outside temp directory: ${workspaceRoot}`,
198
- ),
199
- );
200
- }
201
- try {
202
- await rm(resolvedWorkspace, { recursive: true });
203
- return resultOk(undefined);
204
- } catch (error) {
205
- return resultErr(
206
- errorInfo(
207
- "skillx-cleanup-remove-failed",
208
- `Could not remove workspace: ${formatErrorMessage(error)}`,
209
- ),
210
- );
211
- }
212
- }
213
-
214
- async function removeWorkspaceQuietly(workspaceRoot: string): Promise<void> {
215
- try {
216
- await rm(workspaceRoot, { recursive: true, force: true });
217
- } catch {
218
- // Best-effort cleanup of a directory this gateway just created; the command result carries the original failure.
219
- }
220
- }
@@ -1,151 +0,0 @@
1
- import {
2
- splitMarkdownFrontmatter,
3
- stripLineEnding,
4
- } from "@nseng-ai/foundation/markdown-frontmatter";
5
- import { err, type Result } from "@nseng-ai/foundation/result";
6
-
7
- const FRONTMATTER_KEY_RE = /^(?<key>[A-Za-z0-9_-]+):(?<value>.*)$/u;
8
-
9
- export interface SkillFrontmatterData {
10
- fields: Readonly<Record<string, string>>;
11
- keys: ReadonlySet<string>;
12
- }
13
-
14
- export type SkillFrontmatterParseResult = Result<SkillFrontmatterData>;
15
-
16
- export type SkillFrontmatterTopLevelLineParseResult =
17
- | { type: "key"; key: string; value: string }
18
- | { type: "not_top_level" }
19
- | { type: "invalid"; line: string };
20
-
21
- export function parseSkillFrontmatterBlock(text: string): SkillFrontmatterParseResult {
22
- const split = splitMarkdownFrontmatter(text);
23
- if (split.type === "not_found")
24
- return err({
25
- code: "frontmatter_missing_opening_delimiter",
26
- message: "missing opening frontmatter delimiter '---'",
27
- });
28
- if (split.type === "missing_closing_fence")
29
- return err({
30
- code: "frontmatter_missing_closing_delimiter",
31
- message: "missing closing frontmatter delimiter '---'",
32
- });
33
-
34
- const lines = split.block.frontmatterLinesWithEndings.map(stripLineEnding);
35
- const fields: Record<string, string> = {};
36
- const keys = new Set<string>();
37
- let currentKey: string | undefined;
38
- let currentValues: string[] = [];
39
- function flushCurrent(): void {
40
- if (currentKey === undefined) return;
41
- let rawValue = currentValues
42
- .filter((value) => value.length > 0)
43
- .join(" ")
44
- .trim();
45
- if (
46
- rawValue.length >= 2 &&
47
- rawValue[0] === rawValue.at(-1) &&
48
- (rawValue[0] === '"' || rawValue[0] === "'")
49
- )
50
- rawValue = rawValue.slice(1, -1);
51
- fields[currentKey] = rawValue;
52
- }
53
- for (const line of lines) {
54
- const stripped = line.trim();
55
- if (stripped.length === 0) continue;
56
- if (stripped.startsWith("#")) continue;
57
- const parsedLine = parseSkillFrontmatterTopLevelLine(line);
58
- if (parsedLine.type === "key") {
59
- flushCurrent();
60
- if (keys.has(parsedLine.key))
61
- return err({
62
- code: "frontmatter_duplicate_key",
63
- message: `duplicate frontmatter key: ${JSON.stringify(parsedLine.key)}`,
64
- });
65
- currentKey = parsedLine.key;
66
- keys.add(currentKey);
67
- currentValues = [];
68
- const inlineValue = parsedLine.value.trim();
69
- if (inlineValue.length > 0) currentValues.push(inlineValue);
70
- continue;
71
- }
72
- if (parsedLine.type === "invalid")
73
- return err({
74
- code: "frontmatter_invalid_line",
75
- message: `invalid frontmatter line: ${JSON.stringify(parsedLine.line)}`,
76
- });
77
- if (currentKey === undefined)
78
- return err({
79
- code: "frontmatter_invalid_line",
80
- message: `invalid frontmatter line: ${JSON.stringify(line)}`,
81
- });
82
- currentValues.push(line.trim());
83
- }
84
- flushCurrent();
85
- return { ok: true, value: { fields, keys } };
86
- }
87
-
88
- export function parseSkillFrontmatterTopLevelLine(
89
- line: string,
90
- ): SkillFrontmatterTopLevelLineParseResult {
91
- const content = stripLineEnding(line);
92
- if (content.trim().length === 0) return { type: "not_top_level" };
93
- if (content.trim().startsWith("#")) return { type: "not_top_level" };
94
- if (content.startsWith(" ") || content.startsWith("\t")) return { type: "not_top_level" };
95
- const match = FRONTMATTER_KEY_RE.exec(content);
96
- if (match?.groups === undefined) return { type: "invalid", line: content };
97
- return { type: "key", key: match.groups.key ?? "", value: match.groups.value ?? "" };
98
- }
99
-
100
- export function isSkillFrontmatterTopLevelKey(line: string, key: string): boolean {
101
- const parsed = parseSkillFrontmatterTopLevelLine(line);
102
- return parsed.type === "key" && parsed.key === key;
103
- }
104
-
105
- export function transformSkillFrontmatter(
106
- text: string,
107
- pathLabel: string,
108
- desired: Readonly<Record<string, string | undefined>>,
109
- ): Result<string> {
110
- const parsed = parseSkillFrontmatterBlock(text);
111
- if (!parsed.ok)
112
- return {
113
- ok: false,
114
- error: { ...parsed.error, message: `${pathLabel} ${parsed.error.message}` },
115
- };
116
-
117
- const split = splitMarkdownFrontmatter(text);
118
- if (split.type !== "found")
119
- return err({
120
- code: "frontmatter_invalid_bounds",
121
- message: `${pathLabel} invalid frontmatter bounds`,
122
- });
123
-
124
- const lines = [...split.block.linesWithEndings];
125
- const nameIndex = lines.findIndex(
126
- (line, index) =>
127
- index > 0 && index < split.block.closingIndex && isSkillFrontmatterTopLevelKey(line, "name"),
128
- );
129
- if (nameIndex === -1)
130
- return err({
131
- code: "frontmatter_missing_name",
132
- message: `${pathLabel} missing name field in frontmatter`,
133
- });
134
-
135
- const managedKeys = Object.keys(desired);
136
- const kept = lines.filter(
137
- (line, index) =>
138
- index <= 0 ||
139
- index >= split.block.closingIndex ||
140
- !managedKeys.some((key) => isSkillFrontmatterTopLevelKey(line, key)),
141
- );
142
- const keptNameIndex = kept.findIndex(
143
- (line, index) => index > 0 && isSkillFrontmatterTopLevelKey(line, "name"),
144
- );
145
- const additions = managedKeys.flatMap((key) => {
146
- const value = desired[key];
147
- return value === undefined ? [] : [`${key}: ${value}${split.block.lineEnding}`];
148
- });
149
- kept.splice(keptNameIndex + 1, 0, ...additions);
150
- return { ok: true, value: kept.join("") };
151
- }