@danieljvdm/dev-kit 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -14,18 +14,29 @@ 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, scan the repository's agent instructions,
32
+ package and lock files, framework or tool configuration, and CI workflows for
33
+ the capabilities it actually uses. Search and inspect candidates using those
34
+ signals, then add the matching skills individually:
35
+
36
+ ```bash
37
+ bun x dev-kit search cloudflare
38
+ bun x dev-kit info workers-best-practices
39
+ bun x dev-kit add workers-best-practices wrangler
29
40
  ```
30
41
 
31
42
  `add` updates `dev-kit.jsonc` and applies the selection immediately. The
@@ -49,8 +60,8 @@ or use `--no-apply` to edit the manifest without syncing yet.
49
60
  For review-first workflows, edit the manifest or pass `--no-apply`, then:
50
61
 
51
62
  ```bash
52
- bunx dev-kit plan
53
- bunx dev-kit apply
63
+ bun x dev-kit plan
64
+ bun x dev-kit apply
54
65
  ```
55
66
 
56
67
  Commit the generated `dev-kit.lock.json`, then use locked mode in your package
@@ -135,7 +146,13 @@ tool versions. A project-local process lock also prevents concurrent applies.
135
146
  ```jsonc
136
147
  {
137
148
  "$schema": "./node_modules/@danieljvdm/dev-kit/schema/dev-kit.schema.json",
138
- "include": ["dev-kit", "effect", "emilkowalski-skills"],
149
+ "include": [
150
+ "dev-kit",
151
+ "effect",
152
+ "workers-best-practices",
153
+ "wrangler",
154
+ "serve-sim"
155
+ ],
139
156
  "exclude": ["animation-vocabulary"],
140
157
  "targets": {
141
158
  "agents": { "enabled": true, "mode": "copy" },
@@ -147,8 +164,11 @@ tool versions. A project-local process lock also prevents concurrent applies.
147
164
 
148
165
  - `dev-kit` installs guidance for operating the toolkit itself.
149
166
  - `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.
167
+ - Prefer individual external skills such as `workers-best-practices` and
168
+ `wrangler`, selected after scanning the project for relevant technologies.
169
+ - `serve-sim` selects the approved Evan Bacon simulator skill directly.
170
+ - An approved source ID is broad shorthand that selects every skill from that
171
+ source. Use it only when the scan confirms that every member applies.
152
172
 
153
173
  Dev Kit reserves `.repos/<source-id>` for project-local source checkouts. Run
154
174
  `dev-kit gitignore` to add `.repos/` and `.dev-kit/` to the project ignore file.
@@ -196,7 +216,7 @@ Pin the compatible packages in the consuming project:
196
216
  ```jsonc
197
217
  {
198
218
  "devDependencies": {
199
- "@danieljvdm/dev-kit": "github:danieljvdm/agent-skills",
219
+ "@danieljvdm/dev-kit": "^0.2.0",
200
220
  "@effect/tsgo": "0.24.3",
201
221
  "typescript": "7.0.2"
202
222
  }
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Declarative project development toolkit with portable agent skills.",
@@ -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",
@@ -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",
@@ -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,11 +21,15 @@ 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.
24
+ 1. Establish the Git root. Read project agent instructions, `dev-kit.jsonc`,
25
+ `dev-kit.lock.json` when present, package and lock files, framework or tool
26
+ configuration, CI workflows, and the configured target paths. Derive the
27
+ capabilities the project actually uses from those signals.
28
+ 2. Use `dev-kit search <terms>` and `dev-kit info <skill>` to inspect matching
29
+ catalog entries. Prefer individual external skills; select an external
30
+ source family only when every approved skill from it applies. Update
31
+ `dev-kit.jsonc`, preserve JSONC comments, and validate against the package
32
+ schema. Finish with each desired resource represented once.
29
33
  3. Run `dev-kit plan`. Use `--manifest`, `--project-dir`, or `--lockfile` when
30
34
  the project overrides their defaults. Planning is read-only; inspect every
31
35
  create, update, remove, adoption, and conflict before proceeding. Finish
@@ -39,8 +43,10 @@ stores explicit skill names and exact commit/content digests.
39
43
  ## Manifest
40
44
 
41
45
  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.
46
+ `exclude`. Built-in families such as `effect` are intentional bundles. An
47
+ external source ID is also a family, but expands to every approved skill from
48
+ that source, so prefer individually relevant external skills. Include this
49
+ skill as `dev-kit` when project agents should carry the toolkit procedure.
44
50
 
45
51
  ```jsonc
46
52
  {
@@ -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);