@danieljvdm/dev-kit 0.5.0 → 0.7.0
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/README.md +161 -68
- package/dev-kit.example.jsonc +8 -3
- package/package.json +19 -15
- package/schema/dev-kit.schema.json +52 -0
- package/skill-sources.jsonc +8 -12
- package/skill-sources.lock.json +3 -9
- package/skills/dev-kit/SKILL.md +74 -24
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/skills/effect-ts/agents/openai.yaml +0 -1
- package/skills/effect-ts/references/audit-services.md +11 -11
- package/skills/effect-ts/references/guide-effect.md +56 -69
- package/skills/effect-ts/references/guide-error-handling.md +64 -73
- package/skills/effect-ts/references/guide-layers.md +187 -215
- package/skills/effect-ts/references/guide-observability.md +91 -116
- package/skills/effect-ts/references/guide-retries.md +32 -44
- package/skills/effect-ts/references/guide-schedule.md +26 -40
- package/skills/effect-ts/references/guide-schema.md +50 -57
- package/skills/effect-ts/references/guide-sql.md +47 -50
- package/skills/effect-ts/references/guide-testing.md +96 -98
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
- package/skills/effect-ts/references/version-and-source.md +0 -1
- package/src/bin/dev-kit.ts +61 -28
- package/src/catalog-manager.ts +86 -34
- package/src/catalog.ts +72 -34
- package/src/cli-ui.ts +20 -16
- package/src/effect-source.ts +49 -19
- package/src/effect-tsgo.ts +66 -35
- package/src/gitignore.ts +19 -6
- package/src/index.ts +12 -0
- package/src/manifest.ts +51 -3
- package/src/node-symbolic-link.ts +3 -0
- package/src/oxlint-plugin-effect.js +3 -0
- package/src/oxlint-plugin-style.d.ts +8 -0
- package/src/oxlint-plugin-style.js +8 -0
- package/src/oxlint.js +14 -0
- package/src/oxlint.ts +14 -0
- package/src/package-skill-source.ts +190 -75
- package/src/path-digest.ts +37 -10
- package/src/project-package.ts +59 -0
- package/src/project-process-lock.ts +19 -12
- package/src/project-state.ts +29 -2
- package/src/skill-manager.ts +134 -55
- package/src/skill-selector.ts +8 -2
- package/src/source-manifest.ts +2 -6
- package/src/sync.ts +491 -121
- package/src/vendor.ts +112 -42
- package/src/vite-plus-hooks.ts +174 -0
- package/src/vite-plus-quality.ts +49 -0
- package/templates/AGENTS.md +9 -0
- package/templates/vite-plus/github-actions-check.yml +44 -0
- package/templates/vite-plus/vite.config.ts +22 -0
package/src/manifest.ts
CHANGED
|
@@ -37,21 +37,47 @@ export const EffectSourceSetupSchema = Schema.Struct({
|
|
|
37
37
|
|
|
38
38
|
export type EffectSourceSetup = typeof EffectSourceSetupSchema.Type;
|
|
39
39
|
|
|
40
|
+
export const AgentInstructionsSetupSchema = Schema.Struct({
|
|
41
|
+
enabled: Schema.optional(Schema.Boolean),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type AgentInstructionsSetup = typeof AgentInstructionsSetupSchema.Type;
|
|
45
|
+
|
|
40
46
|
export const ClaudeInstructionsSetupSchema = Schema.Struct({
|
|
41
47
|
enabled: Schema.optional(Schema.Boolean),
|
|
42
48
|
});
|
|
43
49
|
|
|
44
50
|
export type ClaudeInstructionsSetup = typeof ClaudeInstructionsSetupSchema.Type;
|
|
45
51
|
|
|
52
|
+
export const VitePlusHooksSetupSchema = Schema.Struct({
|
|
53
|
+
enabled: Schema.optional(Schema.Boolean),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const VitePlusQualitySetupSchema = Schema.Struct({
|
|
57
|
+
enabled: Schema.optional(Schema.Boolean),
|
|
58
|
+
});
|
|
59
|
+
export type VitePlusQualitySetup = typeof VitePlusQualitySetupSchema.Type;
|
|
60
|
+
|
|
61
|
+
export const VitePlusSetupSchema = Schema.Struct({
|
|
62
|
+
hooks: Schema.optional(VitePlusHooksSetupSchema),
|
|
63
|
+
quality: Schema.optional(VitePlusQualitySetupSchema),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export type VitePlusSetup = typeof VitePlusSetupSchema.Type;
|
|
67
|
+
|
|
46
68
|
export const DevKitManifestSchema = Schema.Struct({
|
|
47
69
|
$schema: Schema.optional(Schema.String),
|
|
48
70
|
include: Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN))),
|
|
49
|
-
exclude: Schema.optional(
|
|
71
|
+
exclude: Schema.optional(
|
|
72
|
+
Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN))),
|
|
73
|
+
),
|
|
50
74
|
setup: Schema.optional(
|
|
51
75
|
Schema.Struct({
|
|
76
|
+
agentInstructions: Schema.optional(AgentInstructionsSetupSchema),
|
|
52
77
|
claudeInstructions: Schema.optional(ClaudeInstructionsSetupSchema),
|
|
53
78
|
effectSource: Schema.optional(EffectSourceSetupSchema),
|
|
54
79
|
effectTsgo: Schema.optional(EffectTsgoSetupSchema),
|
|
80
|
+
vitePlus: Schema.optional(VitePlusSetupSchema),
|
|
55
81
|
}),
|
|
56
82
|
),
|
|
57
83
|
targets: Schema.optional(
|
|
@@ -75,6 +101,9 @@ export type NormalizedManifest = {
|
|
|
75
101
|
readonly include: ReadonlyArray<string>;
|
|
76
102
|
readonly exclude: ReadonlyArray<string>;
|
|
77
103
|
readonly setup: {
|
|
104
|
+
readonly agentInstructions: {
|
|
105
|
+
readonly enabled: boolean;
|
|
106
|
+
};
|
|
78
107
|
readonly claudeInstructions: {
|
|
79
108
|
readonly enabled: boolean;
|
|
80
109
|
};
|
|
@@ -89,6 +118,14 @@ export type NormalizedManifest = {
|
|
|
89
118
|
readonly force: boolean;
|
|
90
119
|
readonly typescriptPackage: string;
|
|
91
120
|
};
|
|
121
|
+
readonly vitePlus: {
|
|
122
|
+
readonly hooks: {
|
|
123
|
+
readonly enabled: boolean;
|
|
124
|
+
};
|
|
125
|
+
readonly quality: {
|
|
126
|
+
readonly enabled: boolean;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
92
129
|
};
|
|
93
130
|
readonly targets: Readonly<Record<HarnessTarget, NormalizedTargetConfig>>;
|
|
94
131
|
};
|
|
@@ -112,6 +149,7 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
|
|
|
112
149
|
|
|
113
150
|
for (const key of ["agents", "claude", "opencode"] as const) {
|
|
114
151
|
const override = manifest.targets?.[key];
|
|
152
|
+
|
|
115
153
|
if (override) {
|
|
116
154
|
targets[key] = {
|
|
117
155
|
enabled: override.enabled ?? DEFAULT_TARGETS[key].enabled,
|
|
@@ -125,6 +163,9 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
|
|
|
125
163
|
exclude: manifest.exclude ?? [],
|
|
126
164
|
include: manifest.include,
|
|
127
165
|
setup: {
|
|
166
|
+
agentInstructions: {
|
|
167
|
+
enabled: manifest.setup?.agentInstructions?.enabled ?? false,
|
|
168
|
+
},
|
|
128
169
|
claudeInstructions: {
|
|
129
170
|
enabled: manifest.setup?.claudeInstructions?.enabled ?? false,
|
|
130
171
|
},
|
|
@@ -133,14 +174,21 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
|
|
|
133
174
|
packageName: manifest.setup?.effectSource?.packageName ?? "effect",
|
|
134
175
|
path: manifest.setup?.effectSource?.path ?? ".repos/effect",
|
|
135
176
|
repository:
|
|
136
|
-
manifest.setup?.effectSource?.repository ??
|
|
137
|
-
"https://github.com/Effect-TS/effect.git",
|
|
177
|
+
manifest.setup?.effectSource?.repository ?? "https://github.com/Effect-TS/effect.git",
|
|
138
178
|
},
|
|
139
179
|
effectTsgo: {
|
|
140
180
|
enabled: manifest.setup?.effectTsgo?.enabled ?? false,
|
|
141
181
|
force: manifest.setup?.effectTsgo?.force ?? false,
|
|
142
182
|
typescriptPackage: manifest.setup?.effectTsgo?.typescriptPackage ?? "typescript",
|
|
143
183
|
},
|
|
184
|
+
vitePlus: {
|
|
185
|
+
hooks: {
|
|
186
|
+
enabled: manifest.setup?.vitePlus?.hooks?.enabled ?? false,
|
|
187
|
+
},
|
|
188
|
+
quality: {
|
|
189
|
+
enabled: manifest.setup?.vitePlus?.quality?.enabled ?? false,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
144
192
|
},
|
|
145
193
|
targets,
|
|
146
194
|
};
|
|
@@ -9,6 +9,7 @@ export type SymbolicLinkObservation =
|
|
|
9
9
|
const isNotSymbolicLink = (error: PlatformError.PlatformError): boolean => {
|
|
10
10
|
if (error.reason._tag !== "Unknown") return false;
|
|
11
11
|
const cause = error.reason.cause;
|
|
12
|
+
|
|
12
13
|
return cause instanceof Error && "code" in cause && cause.code === "EINVAL";
|
|
13
14
|
};
|
|
14
15
|
|
|
@@ -16,6 +17,7 @@ export const observeSymbolicLink = Effect.fn("observeSymbolicLink")(function* (
|
|
|
16
17
|
absolutePath: string,
|
|
17
18
|
) {
|
|
18
19
|
const fs = yield* FileSystem.FileSystem;
|
|
20
|
+
|
|
19
21
|
return yield* fs.readLink(absolutePath).pipe(
|
|
20
22
|
Effect.map((target): SymbolicLinkObservation => ({ kind: "symlink", target })),
|
|
21
23
|
Effect.catch((error) => {
|
|
@@ -25,6 +27,7 @@ export const observeSymbolicLink = Effect.fn("observeSymbolicLink")(function* (
|
|
|
25
27
|
if (isNotSymbolicLink(error)) {
|
|
26
28
|
return Effect.succeed<SymbolicLinkObservation>({ kind: "not-symlink" });
|
|
27
29
|
}
|
|
30
|
+
|
|
28
31
|
return Effect.fail(error);
|
|
29
32
|
}),
|
|
30
33
|
);
|
|
@@ -33,6 +33,7 @@ const noPromiseAtomMode = {
|
|
|
33
33
|
const isMode =
|
|
34
34
|
(key.type === "Identifier" && key.name === "mode") ||
|
|
35
35
|
(key.type === "Literal" && key.value === "mode");
|
|
36
|
+
|
|
36
37
|
if (isMode && value.type === "Literal" && value.value === "promise") {
|
|
37
38
|
context.report({ node, messageId: "noPromiseAtomMode" });
|
|
38
39
|
}
|
|
@@ -117,8 +118,10 @@ const noAsyncWorkflow = {
|
|
|
117
118
|
parent?.type === "Property" &&
|
|
118
119
|
((parent.key.type === "Identifier" && parent.key.name === "try") ||
|
|
119
120
|
(parent.key.type === "Literal" && parent.key.value === "try"));
|
|
121
|
+
|
|
120
122
|
if (!isCapturedTryThunk) context.report({ node, messageId: "noAsyncWorkflow" });
|
|
121
123
|
};
|
|
124
|
+
|
|
122
125
|
return {
|
|
123
126
|
ArrowFunctionExpression: check,
|
|
124
127
|
FunctionDeclaration: check,
|
package/src/oxlint.js
CHANGED
|
@@ -13,6 +13,10 @@ export const recommendedOxlintConfig = {
|
|
|
13
13
|
name: "effect",
|
|
14
14
|
specifier: "@danieljvdm/dev-kit/oxlint-plugin-effect",
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
name: "stylistic",
|
|
18
|
+
specifier: "@danieljvdm/dev-kit/oxlint-plugin-style",
|
|
19
|
+
},
|
|
16
20
|
],
|
|
17
21
|
plugins: ["import", "react", "vitest"],
|
|
18
22
|
rules: {
|
|
@@ -24,6 +28,16 @@ export const recommendedOxlintConfig = {
|
|
|
24
28
|
"import/no-self-import": "error",
|
|
25
29
|
"react/exhaustive-deps": "error",
|
|
26
30
|
"react/rules-of-hooks": "error",
|
|
31
|
+
"stylistic/padding-line-between-statements": [
|
|
32
|
+
"error",
|
|
33
|
+
{ blankLine: "always", prev: ["const", "let", "var"], next: "*" },
|
|
34
|
+
{
|
|
35
|
+
blankLine: "any",
|
|
36
|
+
prev: ["const", "let", "var"],
|
|
37
|
+
next: ["const", "let", "var"],
|
|
38
|
+
},
|
|
39
|
+
{ blankLine: "always", prev: "*", next: "return" },
|
|
40
|
+
],
|
|
27
41
|
"typescript/consistent-type-imports": "error",
|
|
28
42
|
"typescript/no-floating-promises": "off",
|
|
29
43
|
"typescript/no-explicit-any": "error",
|
package/src/oxlint.ts
CHANGED
|
@@ -16,6 +16,10 @@ export const recommendedOxlintConfig = {
|
|
|
16
16
|
name: "effect",
|
|
17
17
|
specifier: "@danieljvdm/dev-kit/oxlint-plugin-effect",
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
name: "stylistic",
|
|
21
|
+
specifier: "@danieljvdm/dev-kit/oxlint-plugin-style",
|
|
22
|
+
},
|
|
19
23
|
],
|
|
20
24
|
plugins: ["import", "react", "vitest"],
|
|
21
25
|
rules: {
|
|
@@ -27,6 +31,16 @@ export const recommendedOxlintConfig = {
|
|
|
27
31
|
"import/no-self-import": "error",
|
|
28
32
|
"react/exhaustive-deps": "error",
|
|
29
33
|
"react/rules-of-hooks": "error",
|
|
34
|
+
"stylistic/padding-line-between-statements": [
|
|
35
|
+
"error",
|
|
36
|
+
{ blankLine: "always", prev: ["const", "let", "var"], next: "*" },
|
|
37
|
+
{
|
|
38
|
+
blankLine: "any",
|
|
39
|
+
prev: ["const", "let", "var"],
|
|
40
|
+
next: ["const", "let", "var"],
|
|
41
|
+
},
|
|
42
|
+
{ blankLine: "always", prev: "*", next: "return" },
|
|
43
|
+
],
|
|
30
44
|
"typescript/consistent-type-imports": "error",
|
|
31
45
|
"typescript/no-floating-promises": "off",
|
|
32
46
|
"typescript/no-explicit-any": "error",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect, FileSystem, Path, Result, Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
import { observeSymbolicLink } from "./node-symbolic-link.ts";
|
|
4
|
+
import { readDirectDependencyNames } from "./project-package.ts";
|
|
4
5
|
import { isSkillName, parseSkillSelector } from "./skill-selector.ts";
|
|
5
6
|
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
6
7
|
|
|
@@ -24,72 +25,88 @@ export type DiscoveredPackageSkill = {
|
|
|
24
25
|
readonly linkPath: string;
|
|
25
26
|
};
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
name: Schema.String,
|
|
36
|
-
version: Schema.String,
|
|
37
|
-
intent: Schema.optional(Schema.Unknown),
|
|
38
|
-
repository: Schema.optional(Schema.Unknown),
|
|
39
|
-
}));
|
|
28
|
+
const PackageMetadataSchema = Schema.fromJsonString(
|
|
29
|
+
Schema.Struct({
|
|
30
|
+
name: Schema.String,
|
|
31
|
+
version: Schema.String,
|
|
32
|
+
intent: Schema.optional(Schema.Unknown),
|
|
33
|
+
repository: Schema.optional(Schema.Unknown),
|
|
34
|
+
}),
|
|
35
|
+
);
|
|
40
36
|
|
|
41
37
|
const nonEmptyString = (value: unknown): value is string =>
|
|
42
38
|
typeof value === "string" && value.trim().length > 0;
|
|
43
39
|
|
|
44
40
|
const hasIntentDiscoveryMetadata = (metadata: typeof PackageMetadataSchema.Type): boolean => {
|
|
45
41
|
const intent = metadata.intent;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
typeof intent === "object" &&
|
|
45
|
+
intent !== null &&
|
|
46
|
+
"version" in intent &&
|
|
47
|
+
intent.version === 1 &&
|
|
48
|
+
"repo" in intent &&
|
|
49
|
+
nonEmptyString(intent.repo) &&
|
|
50
|
+
"docs" in intent &&
|
|
51
|
+
nonEmptyString(intent.docs)
|
|
52
|
+
) {
|
|
50
53
|
return true;
|
|
51
54
|
}
|
|
52
55
|
const repository = metadata.repository;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
nonEmptyString(repository) ||
|
|
59
|
+
(typeof repository === "object" &&
|
|
60
|
+
repository !== null &&
|
|
61
|
+
"url" in repository &&
|
|
62
|
+
nonEmptyString(repository.url))
|
|
63
|
+
);
|
|
56
64
|
};
|
|
57
65
|
|
|
58
66
|
const isSafePackageVersion = (value: string): boolean =>
|
|
59
|
-
value.length > 0 &&
|
|
67
|
+
value.length > 0 &&
|
|
68
|
+
value.trim() === value &&
|
|
69
|
+
![...value].some((character) => {
|
|
60
70
|
const code = character.charCodeAt(0);
|
|
71
|
+
|
|
61
72
|
return code <= 32 || (code >= 127 && code <= 159);
|
|
62
73
|
});
|
|
63
74
|
|
|
64
75
|
const isContained = (path: Path.Path, root: string, candidate: string): boolean => {
|
|
65
76
|
const relative = path.relative(root, candidate);
|
|
77
|
+
|
|
66
78
|
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
67
79
|
};
|
|
68
80
|
|
|
69
81
|
const frontmatterScalar = (document: string, key: string): string | undefined => {
|
|
70
82
|
const body = document.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
83
|
+
|
|
71
84
|
if (body === undefined) return undefined;
|
|
72
85
|
const lines = body.split(/\r?\n/);
|
|
73
86
|
const index = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
87
|
+
|
|
74
88
|
if (index < 0) return undefined;
|
|
75
89
|
const raw = lines[index]?.slice(key.length + 1).trim() ?? "";
|
|
76
90
|
const block = raw.match(/^([|>])(?:[1-9][+-]?|[+-][1-9]?)?$/)?.[1];
|
|
91
|
+
|
|
77
92
|
if (block !== undefined) {
|
|
78
93
|
const values: Array<string> = [];
|
|
94
|
+
|
|
79
95
|
for (const line of lines.slice(index + 1)) {
|
|
80
96
|
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
81
97
|
values.push(line.trim());
|
|
82
98
|
}
|
|
83
99
|
const value = block === "|" ? values.join("\n").trim() : values.join(" ").trim();
|
|
100
|
+
|
|
84
101
|
return value.length > 0 ? value : undefined;
|
|
85
102
|
}
|
|
86
103
|
const quoted = raw.match(/^(['"])([\s\S]*?)\1(?:\s+#.*)?$/)?.[2];
|
|
87
104
|
const value = (quoted ?? raw.replace(/\s+#.*$/, "")).trim();
|
|
105
|
+
|
|
88
106
|
return value.length > 0 ? value : undefined;
|
|
89
107
|
};
|
|
90
108
|
|
|
91
|
-
const skillName = (document: string): string | undefined =>
|
|
92
|
-
frontmatterScalar(document, "name");
|
|
109
|
+
const skillName = (document: string): string | undefined => frontmatterScalar(document, "name");
|
|
93
110
|
|
|
94
111
|
const skillDescription = (document: string): string | undefined =>
|
|
95
112
|
frontmatterScalar(document, "description");
|
|
@@ -98,38 +115,38 @@ const rejectNestedSymlinks = Effect.fn("rejectPackageSkillSymlinks")(function* (
|
|
|
98
115
|
const fs = yield* FileSystem.FileSystem;
|
|
99
116
|
const path = yield* Path.Path;
|
|
100
117
|
const pending = [skillRoot];
|
|
118
|
+
|
|
101
119
|
while (pending.length > 0) {
|
|
102
120
|
const current = pending.pop();
|
|
121
|
+
|
|
103
122
|
if (current === undefined) continue;
|
|
104
123
|
if ((yield* observeSymbolicLink(current)).kind === "symlink") {
|
|
105
|
-
return yield* new PackageSkillSourceError({
|
|
124
|
+
return yield* new PackageSkillSourceError({
|
|
125
|
+
message: `package skill contains a symlink: ${current}`,
|
|
126
|
+
});
|
|
106
127
|
}
|
|
107
|
-
const info = yield* fs
|
|
108
|
-
|
|
109
|
-
|
|
128
|
+
const info = yield* fs
|
|
129
|
+
.stat(current)
|
|
130
|
+
.pipe(
|
|
131
|
+
Effect.mapError(
|
|
132
|
+
() =>
|
|
133
|
+
new PackageSkillSourceError({ message: `could not inspect package skill: ${current}` }),
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
|
|
110
137
|
if (info.type !== "Directory") continue;
|
|
111
|
-
for (const entry of yield* fs
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
for (const entry of yield* fs
|
|
139
|
+
.readDirectory(current)
|
|
140
|
+
.pipe(
|
|
141
|
+
Effect.mapError(
|
|
142
|
+
() =>
|
|
143
|
+
new PackageSkillSourceError({ message: `could not read package skill: ${current}` }),
|
|
144
|
+
),
|
|
145
|
+
))
|
|
146
|
+
pending.push(path.join(current, entry));
|
|
114
147
|
}
|
|
115
148
|
});
|
|
116
149
|
|
|
117
|
-
const readDirectDependencyNames = Effect.fn("readDirectPackageSkillDependencyNames")(function* (projectDir: string) {
|
|
118
|
-
const fs = yield* FileSystem.FileSystem;
|
|
119
|
-
const path = yield* Path.Path;
|
|
120
|
-
const manifestPath = path.join(projectDir, "package.json");
|
|
121
|
-
const manifest = yield* fs.readFileString(manifestPath).pipe(
|
|
122
|
-
Effect.flatMap(Schema.decodeUnknownEffect(ProjectPackageSchema)),
|
|
123
|
-
Effect.mapError(() => new PackageSkillSourceError({ message: `invalid project package.json: ${manifestPath}` })),
|
|
124
|
-
);
|
|
125
|
-
return [...new Set([
|
|
126
|
-
...Object.keys(manifest.dependencies ?? {}),
|
|
127
|
-
...Object.keys(manifest.devDependencies ?? {}),
|
|
128
|
-
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
129
|
-
...Object.keys(manifest.peerDependencies ?? {}),
|
|
130
|
-
])].sort();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
150
|
type InstalledPackageSkills = {
|
|
134
151
|
readonly package: string;
|
|
135
152
|
readonly version: string;
|
|
@@ -146,33 +163,85 @@ const loadInstalledPackageSkills = Effect.fn("loadInstalledPackageSkills")(funct
|
|
|
146
163
|
const path = yield* Path.Path;
|
|
147
164
|
const packageLink = path.join(projectDir, "node_modules", ...packageName.split("/"));
|
|
148
165
|
const packageRoot = yield* fs.realPath(packageLink).pipe(
|
|
149
|
-
Effect.mapError(
|
|
166
|
+
Effect.mapError(
|
|
167
|
+
() =>
|
|
168
|
+
new PackageSkillSourceError({
|
|
169
|
+
message: `package skill package is not installed: ${packageName}`,
|
|
170
|
+
}),
|
|
171
|
+
),
|
|
150
172
|
);
|
|
151
173
|
const packageInfo = yield* fs.stat(packageRoot).pipe(
|
|
152
|
-
Effect.mapError(
|
|
174
|
+
Effect.mapError(
|
|
175
|
+
() =>
|
|
176
|
+
new PackageSkillSourceError({
|
|
177
|
+
message: `could not inspect package skill package: ${packageName}`,
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
153
180
|
);
|
|
154
|
-
|
|
181
|
+
|
|
182
|
+
if (packageInfo.type !== "Directory")
|
|
183
|
+
return yield* new PackageSkillSourceError({
|
|
184
|
+
message: `package skill package is not a directory: ${packageName}`,
|
|
185
|
+
});
|
|
155
186
|
const metadata = yield* fs.readFileString(path.join(packageRoot, "package.json")).pipe(
|
|
156
187
|
Effect.flatMap(Schema.decodeUnknownEffect(PackageMetadataSchema)),
|
|
157
|
-
Effect.mapError(
|
|
188
|
+
Effect.mapError(
|
|
189
|
+
() =>
|
|
190
|
+
new PackageSkillSourceError({
|
|
191
|
+
message: `invalid package.json for package skill package: ${packageName}`,
|
|
192
|
+
}),
|
|
193
|
+
),
|
|
158
194
|
);
|
|
159
|
-
|
|
160
|
-
if (
|
|
161
|
-
|
|
195
|
+
|
|
196
|
+
if (metadata.name !== packageName)
|
|
197
|
+
return yield* new PackageSkillSourceError({
|
|
198
|
+
message: `package.json name does not match package skill package: ${packageName}`,
|
|
199
|
+
});
|
|
200
|
+
if (!isSafePackageVersion(metadata.version))
|
|
201
|
+
return yield* new PackageSkillSourceError({
|
|
202
|
+
message: `package.json has an invalid version for package skill package: ${packageName}`,
|
|
203
|
+
});
|
|
204
|
+
if (!hasIntentDiscoveryMetadata(metadata))
|
|
205
|
+
return yield* new PackageSkillSourceError({
|
|
206
|
+
message: `package does not declare Intent-compatible discovery metadata: ${packageName}`,
|
|
207
|
+
});
|
|
162
208
|
const skillsPath = "skills";
|
|
163
209
|
const skillsLink = path.join(packageLink, skillsPath);
|
|
164
|
-
|
|
210
|
+
|
|
211
|
+
if ((yield* observeSymbolicLink(skillsLink)).kind === "symlink")
|
|
212
|
+
return yield* new PackageSkillSourceError({
|
|
213
|
+
message: `package skills path is a symlink: ${packageName}/${skillsPath}`,
|
|
214
|
+
});
|
|
165
215
|
const skillsRoot = yield* fs.realPath(skillsLink).pipe(
|
|
166
|
-
Effect.mapError(
|
|
216
|
+
Effect.mapError(
|
|
217
|
+
() =>
|
|
218
|
+
new PackageSkillSourceError({
|
|
219
|
+
message: `package skill package has no skills directory: ${packageName}`,
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
167
222
|
);
|
|
168
|
-
|
|
223
|
+
|
|
224
|
+
if (!isContained(path, packageRoot, skillsRoot))
|
|
225
|
+
return yield* new PackageSkillSourceError({
|
|
226
|
+
message: `package skills path resolves outside package root: ${packageName}/${skillsPath}`,
|
|
227
|
+
});
|
|
169
228
|
const skillsInfo = yield* fs.stat(skillsRoot);
|
|
170
|
-
|
|
229
|
+
|
|
230
|
+
if (skillsInfo.type !== "Directory")
|
|
231
|
+
return yield* new PackageSkillSourceError({
|
|
232
|
+
message: `package skills path is not a directory: ${packageName}/${skillsPath}`,
|
|
233
|
+
});
|
|
171
234
|
const names = (yield* fs.readDirectory(skillsRoot).pipe(
|
|
172
|
-
Effect.mapError(
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
235
|
+
Effect.mapError(
|
|
236
|
+
() =>
|
|
237
|
+
new PackageSkillSourceError({
|
|
238
|
+
message: `package skill package has no readable skills directory: ${packageName}`,
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
))
|
|
242
|
+
.filter(isSkillName)
|
|
243
|
+
.sort();
|
|
244
|
+
|
|
176
245
|
return {
|
|
177
246
|
package: packageName,
|
|
178
247
|
version: metadata.version,
|
|
@@ -188,36 +257,57 @@ const inspectPackageSkill = Effect.fn("inspectInstalledPackageSkill")(function*
|
|
|
188
257
|
) {
|
|
189
258
|
const fs = yield* FileSystem.FileSystem;
|
|
190
259
|
const path = yield* Path.Path;
|
|
260
|
+
|
|
191
261
|
if (!isSkillName(name)) {
|
|
192
262
|
return yield* new PackageSkillSourceError({ message: `invalid package skill name: ${name}` });
|
|
193
263
|
}
|
|
194
264
|
const selector = `${installed.package}#${name}`;
|
|
195
265
|
const linkPath = path.join(installed.packageLink, "skills", name);
|
|
266
|
+
|
|
196
267
|
if ((yield* observeSymbolicLink(linkPath)).kind === "symlink") {
|
|
197
|
-
return yield* new PackageSkillSourceError({
|
|
268
|
+
return yield* new PackageSkillSourceError({
|
|
269
|
+
message: `package skill contains a symlink: ${selector}`,
|
|
270
|
+
});
|
|
198
271
|
}
|
|
199
|
-
const skillRoot = yield* fs
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
272
|
+
const skillRoot = yield* fs
|
|
273
|
+
.realPath(linkPath)
|
|
274
|
+
.pipe(
|
|
275
|
+
Effect.mapError(
|
|
276
|
+
() => new PackageSkillSourceError({ message: `package skill does not exist: ${selector}` }),
|
|
277
|
+
),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
if (
|
|
281
|
+
!isContained(path, installed.skillsRoot, skillRoot) ||
|
|
282
|
+
(yield* fs.stat(skillRoot)).type !== "Directory"
|
|
283
|
+
) {
|
|
284
|
+
return yield* new PackageSkillSourceError({
|
|
285
|
+
message: `package skill is not a contained directory: ${selector}`,
|
|
286
|
+
});
|
|
205
287
|
}
|
|
206
288
|
yield* rejectNestedSymlinks(skillRoot);
|
|
207
289
|
const document = yield* fs.readFileString(path.join(skillRoot, "SKILL.md")).pipe(
|
|
208
|
-
Effect.mapError(
|
|
290
|
+
Effect.mapError(
|
|
291
|
+
() =>
|
|
292
|
+
new PackageSkillSourceError({
|
|
293
|
+
message: `package skill is missing SKILL.md: ${selector}`,
|
|
294
|
+
}),
|
|
295
|
+
),
|
|
209
296
|
);
|
|
297
|
+
|
|
210
298
|
if (skillName(document) !== name) {
|
|
211
299
|
return yield* new PackageSkillSourceError({
|
|
212
300
|
message: `package skill SKILL.md name must match directory: ${selector}`,
|
|
213
301
|
});
|
|
214
302
|
}
|
|
215
303
|
const description = skillDescription(document);
|
|
304
|
+
|
|
216
305
|
if (description === undefined) {
|
|
217
306
|
return yield* new PackageSkillSourceError({
|
|
218
307
|
message: `package skill SKILL.md must declare a description: ${selector}`,
|
|
219
308
|
});
|
|
220
309
|
}
|
|
310
|
+
|
|
221
311
|
return {
|
|
222
312
|
selector,
|
|
223
313
|
name,
|
|
@@ -230,41 +320,66 @@ const inspectPackageSkill = Effect.fn("inspectInstalledPackageSkill")(function*
|
|
|
230
320
|
});
|
|
231
321
|
|
|
232
322
|
/** Read direct project dependencies only; malformed packages are returned as diagnostics, never executed. */
|
|
233
|
-
export const discoverPackageSkills = Effect.fn("discoverInstalledPackageSkills")(function* (
|
|
323
|
+
export const discoverPackageSkills = Effect.fn("discoverInstalledPackageSkills")(function* (
|
|
324
|
+
projectDir: string,
|
|
325
|
+
) {
|
|
234
326
|
const fs = yield* FileSystem.FileSystem;
|
|
235
327
|
const path = yield* Path.Path;
|
|
236
328
|
const candidates: Array<DiscoveredPackageSkill> = [];
|
|
237
329
|
const diagnostics: Array<PackageSkillDiagnostic> = [];
|
|
330
|
+
|
|
238
331
|
if (!(yield* fs.exists(path.join(projectDir, "package.json")))) {
|
|
239
332
|
return { candidates, diagnostics };
|
|
240
333
|
}
|
|
241
334
|
for (const packageName of yield* readDirectDependencyNames(projectDir)) {
|
|
242
335
|
if (!isTypeScriptPackageName(packageName)) {
|
|
243
|
-
diagnostics.push({
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
package: packageName,
|
|
338
|
+
message: `invalid direct dependency package name: ${packageName}`,
|
|
339
|
+
});
|
|
244
340
|
continue;
|
|
245
341
|
}
|
|
246
342
|
const skillsLink = path.join(projectDir, "node_modules", ...packageName.split("/"), "skills");
|
|
343
|
+
|
|
247
344
|
if (!(yield* fs.exists(skillsLink))) continue;
|
|
248
345
|
const installed = yield* Effect.result(loadInstalledPackageSkills(projectDir, packageName));
|
|
346
|
+
|
|
249
347
|
if (Result.isFailure(installed)) {
|
|
250
348
|
diagnostics.push({ package: packageName, message: installed.failure.message });
|
|
251
349
|
continue;
|
|
252
350
|
}
|
|
253
351
|
for (const name of installed.success.names) {
|
|
254
352
|
const inspected = yield* Effect.result(inspectPackageSkill(installed.success, name));
|
|
353
|
+
|
|
255
354
|
if (Result.isSuccess(inspected)) candidates.push(inspected.success);
|
|
256
355
|
else diagnostics.push({ package: packageName, message: inspected.failure.message });
|
|
257
356
|
}
|
|
258
357
|
}
|
|
259
|
-
|
|
358
|
+
|
|
359
|
+
return {
|
|
360
|
+
candidates: candidates.sort((left, right) => left.selector.localeCompare(right.selector)),
|
|
361
|
+
diagnostics,
|
|
362
|
+
};
|
|
260
363
|
});
|
|
261
364
|
|
|
262
365
|
/** Resolve one explicitly selected package skill. Unlike browsing, every malformed or missing part is an error. */
|
|
263
|
-
export const resolvePackageSkillSelector = Effect.fn("resolvePackageSkillSelector")(function* (
|
|
366
|
+
export const resolvePackageSkillSelector = Effect.fn("resolvePackageSkillSelector")(function* (
|
|
367
|
+
projectDir: string,
|
|
368
|
+
selector: string,
|
|
369
|
+
) {
|
|
264
370
|
const parsed = parseSkillSelector(selector);
|
|
265
|
-
|
|
371
|
+
|
|
372
|
+
if (parsed?.type !== "package")
|
|
373
|
+
return yield* new PackageSkillSourceError({
|
|
374
|
+
message: `invalid package skill selector: ${selector}`,
|
|
375
|
+
});
|
|
266
376
|
const directDependencies = yield* readDirectDependencyNames(projectDir);
|
|
267
|
-
|
|
377
|
+
|
|
378
|
+
if (!directDependencies.includes(parsed.package))
|
|
379
|
+
return yield* new PackageSkillSourceError({
|
|
380
|
+
message: `package skill package is not a direct dependency: ${parsed.package}`,
|
|
381
|
+
});
|
|
382
|
+
|
|
268
383
|
return yield* inspectPackageSkill(
|
|
269
384
|
yield* loadInstalledPackageSkills(projectDir, parsed.package),
|
|
270
385
|
parsed.skill,
|