@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.
Files changed (56) hide show
  1. package/README.md +161 -68
  2. package/dev-kit.example.jsonc +8 -3
  3. package/package.json +19 -15
  4. package/schema/dev-kit.schema.json +52 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +74 -24
  8. package/skills/effect-atom-data-fetching/SKILL.md +40 -0
  9. package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
  10. package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
  11. package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
  12. package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
  13. package/skills/effect-atom-data-fetching/references/testing.md +63 -0
  14. package/skills/effect-ts/agents/openai.yaml +0 -1
  15. package/skills/effect-ts/references/audit-services.md +11 -11
  16. package/skills/effect-ts/references/guide-effect.md +56 -69
  17. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  18. package/skills/effect-ts/references/guide-layers.md +187 -215
  19. package/skills/effect-ts/references/guide-observability.md +91 -116
  20. package/skills/effect-ts/references/guide-retries.md +32 -44
  21. package/skills/effect-ts/references/guide-schedule.md +26 -40
  22. package/skills/effect-ts/references/guide-schema.md +50 -57
  23. package/skills/effect-ts/references/guide-sql.md +47 -50
  24. package/skills/effect-ts/references/guide-testing.md +96 -98
  25. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  26. package/skills/effect-ts/references/version-and-source.md +0 -1
  27. package/src/bin/dev-kit.ts +61 -28
  28. package/src/catalog-manager.ts +86 -34
  29. package/src/catalog.ts +72 -34
  30. package/src/cli-ui.ts +20 -16
  31. package/src/effect-source.ts +49 -19
  32. package/src/effect-tsgo.ts +66 -35
  33. package/src/gitignore.ts +19 -6
  34. package/src/index.ts +12 -0
  35. package/src/manifest.ts +51 -3
  36. package/src/node-symbolic-link.ts +3 -0
  37. package/src/oxlint-plugin-effect.js +3 -0
  38. package/src/oxlint-plugin-style.d.ts +8 -0
  39. package/src/oxlint-plugin-style.js +8 -0
  40. package/src/oxlint.js +14 -0
  41. package/src/oxlint.ts +14 -0
  42. package/src/package-skill-source.ts +190 -75
  43. package/src/path-digest.ts +37 -10
  44. package/src/project-package.ts +59 -0
  45. package/src/project-process-lock.ts +19 -12
  46. package/src/project-state.ts +29 -2
  47. package/src/skill-manager.ts +134 -55
  48. package/src/skill-selector.ts +8 -2
  49. package/src/source-manifest.ts +2 -6
  50. package/src/sync.ts +491 -121
  51. package/src/vendor.ts +112 -42
  52. package/src/vite-plus-hooks.ts +174 -0
  53. package/src/vite-plus-quality.ts +49 -0
  54. package/templates/AGENTS.md +9 -0
  55. package/templates/vite-plus/github-actions-check.yml +44 -0
  56. 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(Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN)))),
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,
@@ -0,0 +1,8 @@
1
+ declare const styleOxlintPlugin: {
2
+ readonly meta: { readonly name: "dev-kit-style" };
3
+ readonly rules: {
4
+ readonly "padding-line-between-statements": unknown;
5
+ };
6
+ };
7
+
8
+ export default styleOxlintPlugin;
@@ -0,0 +1,8 @@
1
+ import stylisticPlugin from "@stylistic/eslint-plugin";
2
+
3
+ export default {
4
+ meta: { name: "dev-kit-style" },
5
+ rules: {
6
+ "padding-line-between-statements": stylisticPlugin.rules["padding-line-between-statements"],
7
+ },
8
+ };
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 ProjectPackageSchema = Schema.fromJsonString(Schema.Struct({
28
- dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
29
- devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
30
- optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
31
- peerDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
32
- }));
33
-
34
- const PackageMetadataSchema = Schema.fromJsonString(Schema.Struct({
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
- if (typeof intent === "object" && intent !== null &&
47
- "version" in intent && intent.version === 1 &&
48
- "repo" in intent && nonEmptyString(intent.repo) &&
49
- "docs" in intent && nonEmptyString(intent.docs)) {
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
- return nonEmptyString(repository) ||
54
- (typeof repository === "object" && repository !== null &&
55
- "url" in repository && nonEmptyString(repository.url));
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 && value.trim() === value && ![...value].some((character) => {
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({ message: `package skill contains a symlink: ${current}` });
124
+ return yield* new PackageSkillSourceError({
125
+ message: `package skill contains a symlink: ${current}`,
126
+ });
106
127
  }
107
- const info = yield* fs.stat(current).pipe(
108
- Effect.mapError(() => new PackageSkillSourceError({ message: `could not inspect package skill: ${current}` })),
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.readDirectory(current).pipe(
112
- Effect.mapError(() => new PackageSkillSourceError({ message: `could not read package skill: ${current}` })),
113
- )) pending.push(path.join(current, entry));
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(() => new PackageSkillSourceError({ message: `package skill package is not installed: ${packageName}` })),
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(() => new PackageSkillSourceError({ message: `could not inspect package skill package: ${packageName}` })),
174
+ Effect.mapError(
175
+ () =>
176
+ new PackageSkillSourceError({
177
+ message: `could not inspect package skill package: ${packageName}`,
178
+ }),
179
+ ),
153
180
  );
154
- if (packageInfo.type !== "Directory") return yield* new PackageSkillSourceError({ message: `package skill package is not a directory: ${packageName}` });
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(() => new PackageSkillSourceError({ message: `invalid package.json for package skill package: ${packageName}` })),
188
+ Effect.mapError(
189
+ () =>
190
+ new PackageSkillSourceError({
191
+ message: `invalid package.json for package skill package: ${packageName}`,
192
+ }),
193
+ ),
158
194
  );
159
- if (metadata.name !== packageName) return yield* new PackageSkillSourceError({ message: `package.json name does not match package skill package: ${packageName}` });
160
- if (!isSafePackageVersion(metadata.version)) return yield* new PackageSkillSourceError({ message: `package.json has an invalid version for package skill package: ${packageName}` });
161
- if (!hasIntentDiscoveryMetadata(metadata)) return yield* new PackageSkillSourceError({ message: `package does not declare Intent-compatible discovery metadata: ${packageName}` });
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
- if ((yield* observeSymbolicLink(skillsLink)).kind === "symlink") return yield* new PackageSkillSourceError({ message: `package skills path is a symlink: ${packageName}/${skillsPath}` });
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(() => new PackageSkillSourceError({ message: `package skill package has no skills directory: ${packageName}` })),
216
+ Effect.mapError(
217
+ () =>
218
+ new PackageSkillSourceError({
219
+ message: `package skill package has no skills directory: ${packageName}`,
220
+ }),
221
+ ),
167
222
  );
168
- if (!isContained(path, packageRoot, skillsRoot)) return yield* new PackageSkillSourceError({ message: `package skills path resolves outside package root: ${packageName}/${skillsPath}` });
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
- if (skillsInfo.type !== "Directory") return yield* new PackageSkillSourceError({ message: `package skills path is not a directory: ${packageName}/${skillsPath}` });
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(() => new PackageSkillSourceError({
173
- message: `package skill package has no readable skills directory: ${packageName}`,
174
- })),
175
- )).filter(isSkillName).sort();
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({ message: `package skill contains a symlink: ${selector}` });
268
+ return yield* new PackageSkillSourceError({
269
+ message: `package skill contains a symlink: ${selector}`,
270
+ });
198
271
  }
199
- const skillRoot = yield* fs.realPath(linkPath).pipe(
200
- Effect.mapError(() => new PackageSkillSourceError({ message: `package skill does not exist: ${selector}` })),
201
- );
202
- if (!isContained(path, installed.skillsRoot, skillRoot) ||
203
- (yield* fs.stat(skillRoot)).type !== "Directory") {
204
- return yield* new PackageSkillSourceError({ message: `package skill is not a contained directory: ${selector}` });
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(() => new PackageSkillSourceError({ message: `package skill is missing SKILL.md: ${selector}` })),
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* (projectDir: string) {
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({ package: packageName, message: `invalid direct dependency package name: ${packageName}` });
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
- return { candidates: candidates.sort((left, right) => left.selector.localeCompare(right.selector)), diagnostics };
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* (projectDir: string, selector: string) {
366
+ export const resolvePackageSkillSelector = Effect.fn("resolvePackageSkillSelector")(function* (
367
+ projectDir: string,
368
+ selector: string,
369
+ ) {
264
370
  const parsed = parseSkillSelector(selector);
265
- if (parsed?.type !== "package") return yield* new PackageSkillSourceError({ message: `invalid package skill selector: ${selector}` });
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
- if (!directDependencies.includes(parsed.package)) return yield* new PackageSkillSourceError({ message: `package skill package is not a direct dependency: ${parsed.package}` });
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,