@danieljvdm/dev-kit 0.2.0 → 0.2.3

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 CHANGED
@@ -14,18 +14,37 @@ collection of unrelated postinstall scripts. It can:
14
14
 
15
15
  ## Quick start
16
16
 
17
- Install Dev Kit from GitHub:
17
+ Install the published Dev Kit package:
18
18
 
19
19
  ```bash
20
- bun add -d github:danieljvdm/agent-skills
20
+ bun add -d @danieljvdm/dev-kit
21
21
  ```
22
22
 
23
23
  Initialize the project, browse the approved catalog, and add skills:
24
24
 
25
25
  ```bash
26
- bunx dev-kit init
27
- bunx dev-kit list --all
28
- bunx dev-kit add dev-kit effect
26
+ bun x dev-kit init
27
+ bun x dev-kit list --all
28
+ bun x dev-kit add dev-kit effect
29
+ ```
30
+
31
+ Before adding external skills, have the agent inspect repository instructions,
32
+ workspace dependencies, framework and tool configuration, representative
33
+ source boundaries, and CI workflows. It should compare that concrete capability
34
+ inventory with catalog descriptions and select the narrowest useful set. Broad
35
+ umbrella skills and source families belong only when their full breadth is
36
+ intentional; explicit creative or advisory requests remain valid even without a
37
+ mechanical dependency signal. Treat lazy reference folders inside one skill as
38
+ progressive-disclosure content, not as separately triggered skills; a repository
39
+ using several covered products may reasonably select that umbrella while still
40
+ excluding unrelated top-level skills.
41
+
42
+ Search and inspect candidates, then add the matching skills individually:
43
+
44
+ ```bash
45
+ bun x dev-kit search cloudflare
46
+ bun x dev-kit info workers-best-practices
47
+ bun x dev-kit add workers-best-practices wrangler
29
48
  ```
30
49
 
31
50
  `add` updates `dev-kit.jsonc` and applies the selection immediately. The
@@ -49,8 +68,8 @@ or use `--no-apply` to edit the manifest without syncing yet.
49
68
  For review-first workflows, edit the manifest or pass `--no-apply`, then:
50
69
 
51
70
  ```bash
52
- bunx dev-kit plan
53
- bunx dev-kit apply
71
+ bun x dev-kit plan
72
+ bun x dev-kit apply
54
73
  ```
55
74
 
56
75
  Commit the generated `dev-kit.lock.json`, then use locked mode in your package
@@ -135,7 +154,13 @@ tool versions. A project-local process lock also prevents concurrent applies.
135
154
  ```jsonc
136
155
  {
137
156
  "$schema": "./node_modules/@danieljvdm/dev-kit/schema/dev-kit.schema.json",
138
- "include": ["dev-kit", "effect", "emilkowalski-skills"],
157
+ "include": [
158
+ "dev-kit",
159
+ "effect",
160
+ "workers-best-practices",
161
+ "wrangler",
162
+ "serve-sim"
163
+ ],
139
164
  "exclude": ["animation-vocabulary"],
140
165
  "targets": {
141
166
  "agents": { "enabled": true, "mode": "copy" },
@@ -147,8 +172,11 @@ tool versions. A project-local process lock also prevents concurrent applies.
147
172
 
148
173
  - `dev-kit` installs guidance for operating the toolkit itself.
149
174
  - `effect` expands to the consolidated `effect-ts` skill.
150
- - An approved source ID selects every skill from that catalog source.
151
- - An individual catalog skill can be selected directly.
175
+ - Prefer individual external skills such as `workers-best-practices` and
176
+ `wrangler`, selected after scanning the project for relevant technologies.
177
+ - `serve-sim` selects the approved Evan Bacon simulator skill directly.
178
+ - An approved source ID is broad shorthand that selects every skill from that
179
+ source. Use it only when the scan confirms that every member applies.
152
180
 
153
181
  Dev Kit reserves `.repos/<source-id>` for project-local source checkouts. Run
154
182
  `dev-kit gitignore` to add `.repos/` and `.dev-kit/` to the project ignore file.
@@ -196,7 +224,7 @@ Pin the compatible packages in the consuming project:
196
224
  ```jsonc
197
225
  {
198
226
  "devDependencies": {
199
- "@danieljvdm/dev-kit": "github:danieljvdm/agent-skills",
227
+ "@danieljvdm/dev-kit": "^0.2.0",
200
228
  "@effect/tsgo": "0.24.3",
201
229
  "typescript": "7.0.2"
202
230
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./schema/dev-kit.schema.json",
3
- "include": ["dev-kit", "effect", "emilkowalski-skills"],
3
+ "include": ["dev-kit", "effect"],
4
4
  "setup": {
5
5
  "effectSource": { "enabled": true },
6
6
  "effectTsgo": { "enabled": true }
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Declarative project development toolkit with portable agent skills.",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/danieljvdm/agent-skills.git"
9
+ "url": "git+https://github.com/danieljvdm/dev-kit.git"
10
10
  },
11
11
  "license": "MIT",
12
12
  "bin": {
@@ -34,7 +34,7 @@
34
34
  "prepare": "./bin/dev-kit.mjs apply --locked",
35
35
  "dev-kit": "./bin/dev-kit.mjs",
36
36
  "changeset": "changeset",
37
- "version-packages": "changeset version",
37
+ "version-packages": "changeset version && ./bin/dev-kit.mjs apply",
38
38
  "release": "changeset publish",
39
39
  "check": "tsc --noEmit",
40
40
  "plan": "./bin/dev-kit.mjs plan",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/danieljvdm/agent-skills/main/schema/dev-kit.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/danieljvdm/dev-kit/main/schema/dev-kit.schema.json",
4
4
  "title": "Dev Kit Manifest",
5
5
  "description": "Project-local desired state for portable skills and explicit dev-kit setup tasks.",
6
6
  "type": "object",
@@ -10,7 +10,7 @@
10
10
  "type": "string"
11
11
  },
12
12
  "include": {
13
- "description": "Skill names or family names to sync. External source ids are available as family names.",
13
+ "description": "Skill names or family names to sync. Prefer individually relevant external skills; an external source id selects every approved skill from that source.",
14
14
  "type": "array",
15
15
  "items": {
16
16
  "type": "string",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "https://raw.githubusercontent.com/danieljvdm/agent-skills/main/schema/skill-sources.schema.json",
3
+ "$id": "https://raw.githubusercontent.com/danieljvdm/dev-kit/main/schema/skill-sources.schema.json",
4
4
  "title": "External Agent Skill Sources",
5
5
  "description": "Repository-level sources approved for the skill catalog.",
6
6
  "type": "object",
@@ -23,7 +23,7 @@
23
23
  "additionalProperties": false,
24
24
  "properties": {
25
25
  "id": {
26
- "description": "Unique source id. It also becomes a family name in project manifests.",
26
+ "description": "Unique source id. It also becomes broad family shorthand that selects every approved source skill in project manifests.",
27
27
  "type": "string",
28
28
  "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
29
29
  },
@@ -21,26 +21,46 @@ stores explicit skill names and exact commit/content digests.
21
21
 
22
22
  ## Apply loop
23
23
 
24
- 1. Establish the Git root. Read `dev-kit.jsonc`, `dev-kit.lock.json` when
25
- present, `package.json`, and the configured target paths. Finish with the
26
- intended skill selection, targets, and setup tasks identified.
27
- 2. Update `dev-kit.jsonc`. Preserve JSONC comments and validate against the
28
- package schema. Finish when every desired resource is represented once.
29
- 3. Run `dev-kit plan`. Use `--manifest`, `--project-dir`, or `--lockfile` when
24
+ 1. Establish the Git root. Read project agent instructions, the current
25
+ manifest and lock, package and workspace manifests, framework and tool
26
+ configuration, representative source boundaries, and CI workflows. Build a
27
+ concrete inventory of the platforms, frameworks, tools, and workflows the
28
+ repository actually uses; do not infer capabilities from a product or
29
+ company name alone.
30
+ 2. Run `dev-kit list --all`, then use `dev-kit search <terms>` and `dev-kit info
31
+ <skill>` for each capability in the inventory. Compare every candidate's
32
+ trigger description with concrete repository evidence. Keep explicitly
33
+ requested creative or advisory skills even when they have no mechanical
34
+ dependency signal.
35
+ 3. Choose the narrowest useful set. Prefer focused external skills over a
36
+ generic umbrella when they cover the repository's work. Select an umbrella
37
+ or external source family only when its full breadth is intentionally useful;
38
+ never select one merely because one member or product matches. Explain any
39
+ uncertain inclusion before applying it. Distinguish separately triggered
40
+ skills from lazy `references/` bundled inside one skill: unused reference
41
+ folders cost repository space but are not loaded into agent context unless
42
+ the skill routes to them. A multi-product repository can therefore justify
43
+ an umbrella while still excluding unrelated top-level skills.
44
+ 4. Update `dev-kit.jsonc`. Preserve JSONC comments and validate against the
45
+ package schema. Finish with each desired resource represented once and every
46
+ external selection supported by repository evidence or an explicit request.
47
+ 5. Run `dev-kit plan`. Use `--manifest`, `--project-dir`, or `--lockfile` when
30
48
  the project overrides their defaults. Planning is read-only; inspect every
31
49
  create, update, remove, adoption, and conflict before proceeding. Finish
32
50
  when the plan contains only intended actions and understood conflicts.
33
- 4. Resolve conflicts, then run `dev-kit apply`. Commit the manifest and
51
+ 6. Resolve conflicts, then run `dev-kit apply`. Commit the manifest and
34
52
  regenerated `dev-kit.lock.json`; keep `.dev-kit/` local. Finish when a second
35
53
  plan reports only unchanged resources and setup tasks.
36
- 5. Use `dev-kit apply --locked` in CI and the package lifecycle. Finish when a
54
+ 7. Use `dev-kit apply --locked` in CI and the package lifecycle. Finish when a
37
55
  clean install converges from the committed manifest and lock.
38
56
 
39
57
  ## Manifest
40
58
 
41
59
  Use skill names or family names in `include`; subtract selections with
42
- `exclude`. Include this skill as `dev-kit` when project agents should carry the
43
- toolkit procedure.
60
+ `exclude`. Built-in families such as `effect` are intentional bundles. An
61
+ external source ID is also a family, but expands to every approved skill from
62
+ that source, so prefer individually relevant external skills. Include this
63
+ skill as `dev-kit` when project agents should carry the toolkit procedure.
44
64
 
45
65
  ```jsonc
46
66
  {
@@ -9,7 +9,7 @@ export class ProjectAlreadyLockedError extends Schema.TaggedErrorClass<ProjectAl
9
9
  { path: Schema.String },
10
10
  ) {
11
11
  override get message() {
12
- return `another dev-kit apply may be active (${this.path}); verify the owner before removing a stale lock`;
12
+ return `another dev-kit operation may be active (${this.path}); verify the owner before removing a stale lock`;
13
13
  }
14
14
  }
15
15
 
@@ -5,6 +5,7 @@ import { Prompt } from "effect/unstable/cli";
5
5
  import { loadSkillCatalog } from "./catalog.ts";
6
6
  import { isInteractiveTerminal, printDetail, printLine, printStatus } from "./cli-ui.ts";
7
7
  import { DevKitManifestSchema } from "./manifest.ts";
8
+ import { observeSymbolicLink } from "./node-symbolic-link.ts";
8
9
  import { runProjectSkillPlan } from "./sync.ts";
9
10
  import { patchProjectGitignore } from "./gitignore.ts";
10
11
 
@@ -27,22 +28,83 @@ const packageRoot = Effect.fn("skillManagerPackageRoot")(function* () {
27
28
  const resolvePaths = Effect.fn("resolveSkillManagerPaths")(function* (
28
29
  options: ManagerOptions,
29
30
  ) {
31
+ const fs = yield* FileSystem.FileSystem;
30
32
  const path = yield* Path.Path;
31
33
  const projectDir = path.resolve(options.projectDir ?? ".");
34
+ const candidate = options.manifestPath ?? "dev-kit.jsonc";
35
+ if (candidate.length === 0 || path.isAbsolute(candidate)) {
36
+ return yield* new SkillManagerError({
37
+ message: "--manifest must be a non-empty project-relative path",
38
+ });
39
+ }
40
+ const manifestPath = path.resolve(projectDir, candidate);
41
+ const relative = path.relative(projectDir, manifestPath);
42
+ if (
43
+ relative === ".." ||
44
+ relative.startsWith(`..${path.sep}`) ||
45
+ path.isAbsolute(relative)
46
+ ) {
47
+ return yield* new SkillManagerError({
48
+ message: "--manifest must resolve inside the project",
49
+ });
50
+ }
51
+ let ancestor = projectDir;
52
+ for (const segment of relative.split(path.sep).slice(0, -1)) {
53
+ ancestor = path.join(ancestor, segment);
54
+ if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
55
+ return yield* new SkillManagerError({
56
+ message: `manifest ancestor is a symlink: ${path.relative(projectDir, ancestor)}`,
57
+ });
58
+ }
59
+ }
60
+ const destination = yield* observeSymbolicLink(manifestPath);
61
+ if (destination.kind === "symlink") {
62
+ return yield* new SkillManagerError({ message: `manifest is a symlink: ${relative}` });
63
+ }
64
+ if (destination.kind === "not-symlink" && (yield* fs.stat(manifestPath)).type !== "File") {
65
+ return yield* new SkillManagerError({ message: `manifest is not a regular file: ${relative}` });
66
+ }
32
67
  return {
33
68
  projectDir,
34
- manifestPath: path.resolve(projectDir, options.manifestPath ?? "dev-kit.jsonc"),
69
+ manifestPath,
35
70
  };
36
71
  });
37
72
 
38
- const defaultManifest = `{
39
- "$schema": "./node_modules/@danieljvdm/dev-kit/schema/dev-kit.schema.json",
40
- "include": [],
41
- "targets": {
42
- "agents": { "enabled": true, "mode": "copy" }
43
- }
44
- }
45
- `;
73
+ const renderDefaultManifest = (projectDir: string, manifestPath: string, path: Path.Path) => {
74
+ const rawSchemaPath = path.relative(
75
+ path.dirname(manifestPath),
76
+ path.join(projectDir, "node_modules", "@danieljvdm", "dev-kit", "schema", "dev-kit.schema.json"),
77
+ );
78
+ const portableSchemaPath = path.sep === "/"
79
+ ? rawSchemaPath
80
+ : rawSchemaPath.split(path.sep).join("/");
81
+ const schemaPath = portableSchemaPath.startsWith(".")
82
+ ? portableSchemaPath
83
+ : `./${portableSchemaPath}`;
84
+ return `${JSON.stringify({
85
+ $schema: schemaPath,
86
+ include: [],
87
+ targets: { agents: { enabled: true, mode: "copy" } },
88
+ }, null, 2)}\n`;
89
+ };
90
+
91
+ const createDefaultManifest = Effect.fn("createDefaultSkillManifest")(function* (
92
+ paths: { readonly projectDir: string; readonly manifestPath: string },
93
+ ) {
94
+ const fs = yield* FileSystem.FileSystem;
95
+ const path = yield* Path.Path;
96
+ yield* fs.makeDirectory(path.dirname(paths.manifestPath), { recursive: true });
97
+ const staged = yield* fs.makeTempFileScoped({
98
+ directory: path.dirname(paths.manifestPath),
99
+ prefix: ".dev-kit-init-",
100
+ });
101
+ yield* fs.writeFileString(
102
+ staged,
103
+ renderDefaultManifest(paths.projectDir, paths.manifestPath, path),
104
+ );
105
+ yield* fs.rename(staged, paths.manifestPath);
106
+ yield* patchProjectGitignore({ projectDir: paths.projectDir });
107
+ });
46
108
 
47
109
  const readManifest = Effect.fn("readManagedSkillManifest")(function* (
48
110
  options: ManagerOptions,
@@ -56,8 +118,7 @@ const readManifest = Effect.fn("readManagedSkillManifest")(function* (
56
118
  message: "dev-kit.jsonc not found. Run `dev-kit init` first.",
57
119
  });
58
120
  }
59
- yield* fs.writeFileString(paths.manifestPath, defaultManifest);
60
- yield* patchProjectGitignore({ projectDir: paths.projectDir });
121
+ yield* createDefaultManifest(paths);
61
122
  }
62
123
  const raw = yield* fs.readFileString(paths.manifestPath);
63
124
  const errors: Array<ParseError> = [];
@@ -147,8 +208,7 @@ export const initProject = Effect.fn("initDevKitProject")(function* (options: Ma
147
208
  yield* printStatus("info", "Already initialized", paths.manifestPath);
148
209
  return;
149
210
  }
150
- yield* fs.writeFileString(paths.manifestPath, defaultManifest);
151
- yield* patchProjectGitignore({ projectDir: paths.projectDir });
211
+ yield* createDefaultManifest(paths);
152
212
  yield* printStatus("success", "Created dev-kit.jsonc");
153
213
  yield* printDetail("Add a skill with: dev-kit add <name>");
154
214
  });
@@ -166,6 +226,16 @@ export const addSkills = Effect.fn("addManagedSkills")(function* (
166
226
  message: `unknown skill${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Try \`dev-kit search ${unknown[0]}\`.`,
167
227
  });
168
228
  }
229
+ for (const source of catalog.lock?.sources ?? []) {
230
+ if (!names.includes(source.id)) continue;
231
+ yield* printStatus(
232
+ "info",
233
+ `Source family ${source.id} selects all ${source.skills.length} approved skills`,
234
+ );
235
+ yield* printDetail(
236
+ `Prefer individual skill names unless every skill applies. Inspect with: dev-kit search ${source.id}`,
237
+ );
238
+ }
169
239
  const include = [...new Set([...current.manifest.include, ...names])];
170
240
  const exclude = (current.manifest.exclude ?? []).filter((name) => !names.includes(name));
171
241
  yield* writeArray(current.manifestPath, current.raw, "include", include);
@@ -234,7 +304,8 @@ export const listSkills = Effect.fn("listManagedSkills")(function* (
234
304
  for (const skill of visible) {
235
305
  const marker = selected.has(skill.name) ? "✓" : " ";
236
306
  const origin = skill.bundled ? "built in" : skill.source;
237
- yield* printLine(`${marker} ${skill.name} ${summary(skill.description, origin)}`);
307
+ const provenance = skill.bundled ? "" : ` [${skill.source}]`;
308
+ yield* printLine(`${marker} ${skill.name}${provenance} ${summary(skill.description, origin)}`);
238
309
  }
239
310
  yield* printLine();
240
311
  yield* printLine(`${selected.size} selected · ${catalog.skills.length} approved`);
package/src/sync.ts CHANGED
@@ -786,7 +786,13 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
786
786
  yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
787
787
  yield* fs.writeFileString(stagedState, canonicalState(plan.nextState));
788
788
 
789
- type Replacement = { readonly destination: string; readonly backup: string; readonly staged?: string };
789
+ type Replacement = {
790
+ readonly destination: string;
791
+ readonly backup: string;
792
+ readonly expected?: ObservedPath;
793
+ readonly path: string;
794
+ readonly staged?: string;
795
+ };
790
796
  const replacements: Array<Replacement> = [];
791
797
  let replacementIndex = 0;
792
798
  for (const action of mutating) {
@@ -801,12 +807,24 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
801
807
  replacements.push({
802
808
  destination: action.action === "remove" ? action.destination : action.desired.destination,
803
809
  backup: path.join(backupDir, String(replacementIndex++)),
810
+ expected: action.observed,
811
+ path: action.action === "remove" ? action.previous.path : action.desired.path,
804
812
  ...(staged === undefined ? {} : { staged }),
805
813
  });
806
814
  }
807
815
  replacements.push(
808
- { destination: plan.lockfilePath, backup: path.join(backupDir, "lock"), staged: stagedLock },
809
- { destination: plan.statePath, backup: path.join(backupDir, "state"), staged: stagedState },
816
+ {
817
+ destination: plan.lockfilePath,
818
+ backup: path.join(backupDir, "lock"),
819
+ path: plan.lockfilePath,
820
+ staged: stagedLock,
821
+ },
822
+ {
823
+ destination: plan.statePath,
824
+ backup: path.join(backupDir, "state"),
825
+ path: plan.statePath,
826
+ staged: stagedState,
827
+ },
810
828
  );
811
829
 
812
830
  const installed: Array<string> = [];
@@ -824,6 +842,12 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
824
842
  const apply = Effect.gen(function* () {
825
843
  for (const replacement of replacements) {
826
844
  const observed = yield* observePath(replacement.destination);
845
+ if (
846
+ replacement.expected !== undefined &&
847
+ !observationsEqual(observed, replacement.expected)
848
+ ) {
849
+ return yield* new ApplyRaceError({ path: replacement.path });
850
+ }
827
851
  if (observed.kind !== "missing") {
828
852
  yield* fs.makeDirectory(path.dirname(replacement.backup), { recursive: true });
829
853
  yield* fs.rename(replacement.destination, replacement.backup);
package/src/vendor.ts CHANGED
@@ -4,6 +4,7 @@ import { ChildProcess } from "effect/unstable/process";
4
4
 
5
5
  import { printStatus, withSpinner } from "./cli-ui.ts";
6
6
  import { observePath, type Digest } from "./path-digest.ts";
7
+ import { acquireProjectProcessLock } from "./project-process-lock.ts";
7
8
 
8
9
  import {
9
10
  SkillSourcesLockSchema,
@@ -754,6 +755,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
754
755
  : Effect.fail(error),
755
756
  ),
756
757
  );
758
+ yield* acquireProjectProcessLock(repoDir);
757
759
  const sourcesPath = path.resolve(repoDir, options.sourcesPath ?? DEFAULT_SOURCES_PATH);
758
760
  const lockfilePath = path.resolve(repoDir, options.lockfilePath ?? DEFAULT_LOCKFILE_PATH);
759
761
  const manifest = yield* readJsonc(sourcesPath, SkillSourcesManifestSchema);