dryai 2.1.0 → 2.2.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 CHANGED
@@ -2,15 +2,17 @@
2
2
 
3
3
  Installs command, rule, and skill sources from `~/.config/dryai` by default into Copilot and Cursor targets.
4
4
 
5
- Pass `--config-root <path>` to read configs from a different root such as `./config`.
5
+ Global CLI options:
6
6
 
7
- Pass `--output-root <path>` to write generated output somewhere other than your home directory.
7
+ - `--config-root <path>` reads configs from a different root such as `./config`.
8
+ - `--output-root <path>` writes generated output somewhere other than your home directory.
9
+ - `--test` is a shortcut for `--output-root ./output-test`, and if both are provided, `--output-root` wins.
8
10
 
9
- `--test` is a shortcut for `--output-root ./output-test`, and if both are provided, `--output-root` wins.
11
+ These are root-level options for the CLI. They modify command behavior for any given command.
10
12
 
11
- ## Input
13
+ ## Config Layout
12
14
 
13
- Input config files live under the selected input root:
15
+ Input config files live under the selected config root:
14
16
 
15
17
  - `commands`
16
18
  - `rules`
@@ -24,9 +26,7 @@ Live output is written to:
24
26
  - `~/.cursor/rules`
25
27
  - `~/.cursor/skills`
26
28
 
27
- ## Example Configs
28
-
29
- One input root can contain all three source types:
29
+ One config root can contain all three source types:
30
30
 
31
31
  ```text
32
32
  ~/.config/dryai/
@@ -39,6 +39,106 @@ One input root can contain all three source types:
39
39
  └── SKILL.md
40
40
  ```
41
41
 
42
+ See VS Code editor setup note below.
43
+
44
+ ## Commands
45
+
46
+ ### `install`
47
+
48
+ - Purpose: Install commands, rules, and skills from the selected config root into the Copilot and Cursor target directories.
49
+ - Input roots: Reads from `commands`, `rules`, and `skills` under the selected config root.
50
+ - Output roots: Writes to `~/.copilot/prompts`, `~/.copilot/instructions`, `~/.copilot/skills`, `~/.cursor/rules`, and `~/.cursor/skills` by default.
51
+
52
+ ### `skills add`
53
+
54
+ - Purpose: Import one or more managed skills from a remote repository.
55
+ - Repository argument: `<repo>` may be a full git remote URL or a GitHub `owner/repo` shorthand such as `anthropics/skills`.
56
+ - Storage: Imported skills are copied into `config/skills/<name>/` and tracked in `skills.lock.json`.
57
+ - Config root: Local skill directories and `skills.lock.json` are read from and written to the selected config root.
58
+ - Required: `--skill <name>` is required at least once.
59
+ - Default resolution: Each requested skill resolves from `<repo root>/skills/<name>`.
60
+ - `--path <repoPath>`: Resolves each requested skill from a different base directory.
61
+ - `--path .`: Resolves each requested skill from the repository root itself.
62
+ - `--as <name>`: Stores the imported skill under a different local managed name when importing exactly one skill.
63
+ - `--ref <gitRef>`: Fetches a specific branch, tag, or commit instead of the remote default branch.
64
+ - `--pin`: Stores the resolved commit instead of tracking a moving ref.
65
+ - Examples:
66
+
67
+ ```sh
68
+ # Resolves from <repo root>/skills/skill-creator
69
+ dryai skills add anthropics/skills --skill skill-creator
70
+
71
+ # Resolves from <repo root>/review-helper
72
+ dryai skills add anthropics/skills --path . --skill review-helper
73
+
74
+ # Resolves from <repo root>/tools/review-helper
75
+ dryai skills add anthropics/skills --path tools --skill review-helper
76
+
77
+ # Resolves from <repo root>/skills/pr-review and <repo root>/skills/commit
78
+ dryai skills add vercel-labs/agent-skills --skill pr-review commit
79
+
80
+ # Resolves from <repo root>/skills/pr-review and <repo root>/skills/commit
81
+ dryai skills add https://github.com/vercel-labs/agent-skills.git --skill pr-review commit
82
+ ```
83
+
84
+ By default, imports track the requested ref. With no `--ref`, that means the remote default branch `HEAD` is tracked.
85
+
86
+ ### `skills update`
87
+
88
+ - Purpose: Re-fetch one managed skill from its tracked source and replace the local copied directory.
89
+ - `--force`: Overwrites local edits instead of skipping the update.
90
+
91
+ ### `skills update-all`
92
+
93
+ - Purpose: Re-fetch all managed skills from their tracked sources and replace the local copied directories.
94
+ - `--force`: Overwrites local edits instead of skipping the update.
95
+
96
+ ### `skills rehash`
97
+
98
+ - Purpose: Refresh the stored file hashes for one managed skill using its current local contents.
99
+
100
+ ### `skills rehash-all`
101
+
102
+ - Purpose: Refresh the stored file hashes for every managed skill using their current local contents.
103
+ - Behavior: Skips managed entries whose local directory is missing.
104
+
105
+ ### `skills remove`
106
+
107
+ - Purpose: Delete a managed skill's local copied directory and remove its lockfile entry.
108
+
109
+ ### `skills list`
110
+
111
+ - Purpose: Report local skill directories, annotate managed entries from the lockfile, and flag managed entries whose local directory is missing.
112
+
113
+ ### `skills` lockfile
114
+
115
+ The lockfile records:
116
+
117
+ - the local skill name
118
+ - the source repository
119
+ - the source path within that repository
120
+ - the requested git ref, when one was provided
121
+ - the resolved commit that was imported
122
+ - the last installed content hash for each file in the managed skill directory
123
+
124
+ Before replacing a managed skill, `dryai` compares the current local files against the hashes stored in `skills.lock.json`. If any file was added, removed, or edited locally, the update is skipped and a warning is printed so you do not lose your customizations by accident.
125
+
126
+ For skills imported before hash tracking existed, use these commands to store hashes from the current local directory without fetching from the remote source:
127
+
128
+ ```sh
129
+ dryai skills rehash review-helper
130
+ dryai skills rehash-all
131
+ ```
132
+
133
+ Use these commands to intentionally overwrite local edits:
134
+
135
+ ```sh
136
+ dryai skills update review-helper --force
137
+ dryai skills update-all --force
138
+ ```
139
+
140
+ ## Example Configs
141
+
42
142
  ### Example Rule
43
143
 
44
144
  Rules are markdown files under `rules/`. `dryai` recognizes these rule frontmatter fields:
@@ -112,77 +212,9 @@ Focus on findings first.
112
212
  - Keep the overview brief unless the user asks for a deeper walkthrough.
113
213
  ```
114
214
 
115
- ## Commands
116
-
117
- ```sh
118
- dryai install
119
- dryai skills list List local skills
120
- dryai skills add [options] <repo> Add managed skills from a remote repository
121
- dryai skills remove <name> Remove a managed skill
122
- dryai skills rehash <name> Refresh stored file hashes for one managed skill
123
- dryai skills rehash-all Refresh stored file hashes for all managed skills
124
- dryai skills update [options] <name> Update a managed skill from its tracked source
125
- dryai skills update-all [options] Update all managed skills from their tracked sources
126
- ```
127
-
128
- `<repo>` may be a full git remote URL or a GitHub `owner/repo` shorthand such as `anthropics/skills`.
129
-
130
- ## Managed Skills
131
-
132
- Imported skills are copied into `config/skills/<name>/` and tracked in `skills.lock.json`.
133
-
134
- When you run `skills` commands, local skill directories and `skills.lock.json` are read from and written to the selected config root.
135
-
136
- `skills add` requires at least one `--skill <name>` value. Each requested skill is always resolved from `<repo root>/skills/<name>`.
137
-
138
- Use `--as <name>` to choose a different local managed skill name when importing exactly one skill.
139
-
140
- Examples:
141
-
142
- ```sh
143
- dryai skills add anthropics/skills --skill skill-creator
144
- dryai skills add vercel-labs/agent-skills --skill pr-review commit
145
- dryai skills add https://github.com/vercel-labs/agent-skills.git --skill pr-review commit
146
- ```
147
-
148
- By default, imports track the requested ref. With no `--ref`, that means the remote default branch `HEAD` is tracked. Use `--pin` to store the currently resolved commit instead, so later `skills update` operations stay pinned to that commit.
149
-
150
- The lockfile records:
151
-
152
- - the local skill name
153
- - the source repository
154
- - the source path within that repository
155
- - the requested git ref, when one was provided
156
- - the resolved commit that was imported
157
- - the last installed content hash for each file in the managed skill directory
158
-
159
- `skills update` and `skills update-all` re-fetch the tracked repository snapshot and replace the local copied skill directory.
160
-
161
- Before replacing a managed skill, `dryai` compares the current local files against the hashes stored in `skills.lock.json`. If any file was added, removed, or edited locally, the update is skipped and a warning is printed so you do not lose your customizations by accident.
162
-
163
- For skills imported before hash tracking existed, use `rehash` to store hashes from the current local directory without fetching from the remote source:
164
-
165
- ```sh
166
- dryai skills rehash review-helper
167
- dryai skills rehash-all
168
- ```
169
-
170
- Use `--force` to intentionally overwrite local edits:
171
-
172
- ```sh
173
- dryai skills update review-helper --force
174
- dryai skills update-all --force
175
- ```
176
-
177
- `skills remove` deletes the local copied skill directory and removes its lockfile entry.
178
-
179
- `skills list` reports local skill directories, annotating managed entries from the lockfile and flagging managed entries whose local directory is missing.
180
-
181
- `skills rehash` updates the stored file hashes for one managed skill using its current local contents. `skills rehash-all` does the same for every managed skill and skips any managed entry whose local directory is missing.
182
-
183
215
  ## Development
184
216
 
185
- For development, use `pnpm dev` to rebuild into `dest/` on change and `pnpm dev:dryai --test install` to run the built CLI.
217
+ For development, use `pnpm dev` to rebuild into `dest/` on change and `pnpm dev:dryai <...>` to run the built CLI.
186
218
 
187
219
  Run `pnpm run setup:editor` after installing dependencies if you want the Effect language service workspace patch applied locally.
188
220
 
@@ -193,39 +225,21 @@ pnpm run dev
193
225
  pnpm run test
194
226
  pnpm run test:watch
195
227
 
196
- pnpm dev:dryai install
197
- pnpm dev:dryai --test install
198
- pnpm dev:dryai --output-root ./tmp/install-root install
199
- pnpm dev:dryai --config-root ./config install
228
+ pnpm dev:dryai <...>
200
229
  ```
201
230
 
202
- ## CI and Release
203
-
204
- - On pull request open or update
205
- - Run CI validation with build, test, and `npm pack --dry-run`.
206
- - On changes landing on `main`
207
- - Run the same CI validation with build, test, and `npm pack --dry-run`.
208
-
209
- - On `v*` tag pushed to `main`, the release workflow will:
210
- - Verify the tag matches the checked-in `package.json` version.
211
- - Verify the tagged commit is on `main`.
212
- - Build and test the CLI.
213
- - Create a tarball with `npm pack`.
214
- - Create or update the matching GitHub Release.
215
- - Upload the tarball as a release asset.
216
- - After the release workflow succeeds, the publish workflow will:
217
- - Download the tarball artifact produced by the release workflow.
218
- - Publish that exact tarball to npm using npm trusted publishing.
231
+ ---
219
232
 
220
- Example release flow:
233
+ ## VS Code Setup
221
234
 
222
- ```sh
223
- git tag v0.1.0
224
- git push origin v0.1.0
225
- ```
235
+ VS Code Copilot does not automatically discover prompt files from `~/.copilot/prompts`.
226
236
 
227
- Install from the release tarball with:
237
+ Add this to your VS Code user settings so prompt files installed by `dryai` are picked up:
228
238
 
229
- ```sh
230
- npm install -g https://github.com/willmruzek/share-ai-config/releases/download/v0.1.0/share-ai-config-0.1.0.tgz
239
+ ```json
240
+ {
241
+ "chat.promptFilesLocations": {
242
+ "~/.copilot/prompts": true
243
+ }
244
+ }
231
245
  ```
@@ -1,9 +1,10 @@
1
1
  import type { AgentsContext } from '../../lib/context.js';
2
2
  /**
3
- * Imports one or more managed skills from a remote repository `skills/` directory into the local skills directory.
3
+ * Imports one or more managed skills from a remote repository into the local skills directory.
4
4
  */
5
5
  export declare function runSkillsAddCommand(context: AgentsContext, input: {
6
6
  repo: string;
7
+ repoPath: string | undefined;
7
8
  skillNames: string[];
8
9
  asName: string | undefined;
9
10
  pin: boolean;
@@ -1,5 +1,5 @@
1
1
  import fs from 'fs-extra';
2
- import { cloneRemoteRepo, computeDirectoryHashes, createImportedSkillRecord, ensureSkillsLockfile, ensureSkillsRoot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, normalizeRemoteRepo, replaceManagedSkillDirectory, resolveManagedSkillImportPath, resolveSkillSourceDir, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
2
+ import { cloneRemoteRepo, computeDirectoryHashes, createImportedSkillRecord, deriveSkillName, ensureSkillsLockfile, ensureSkillsRoot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, normalizeImportedSkillPath, normalizeRemoteRepo, replaceManagedSkillDirectory, resolveManagedSkillImportPath, resolveManagedSkillImportPathFromBase, resolveSkillSourceDirByPath, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
3
3
  /**
4
4
  * Normalizes and de-duplicates requested skill names while preserving their input order.
5
5
  */
@@ -20,10 +20,21 @@ function normalizeRequestedSkillNames(skillNames) {
20
20
  return uniqueSkillNames;
21
21
  }
22
22
  /**
23
- * Imports one or more managed skills from a remote repository `skills/` directory into the local skills directory.
23
+ * Resolves the remote repository path for one managed skill import request.
24
+ */
25
+ function resolveRequestedImportPath(input) {
26
+ const importPath = resolveManagedSkillImportPathFromBase({
27
+ basePath: input.basePath,
28
+ skillName: input.requestedSkillName,
29
+ });
30
+ return normalizeImportedSkillPath(importPath) ?? importPath;
31
+ }
32
+ /**
33
+ * Imports one or more managed skills from a remote repository into the local skills directory.
24
34
  */
25
35
  export async function runSkillsAddCommand(context, input) {
26
36
  const repo = normalizeRemoteRepo(input.repo);
37
+ const normalizedBasePath = normalizeImportedSkillPath(input.repoPath);
27
38
  if (input.skillNames.length === 0) {
28
39
  throw new Error('At least one skill name must be provided with --skill');
29
40
  }
@@ -45,9 +56,14 @@ export async function runSkillsAddCommand(context, input) {
45
56
  const importedSkillSummaries = [];
46
57
  try {
47
58
  for (const requestedSkillName of requestedSkillNames) {
48
- const skillName = input.asName ?? requestedSkillName;
49
- const importedSkillPath = resolveManagedSkillImportPath({
50
- skillName: requestedSkillName,
59
+ const importedSkillPath = resolveRequestedImportPath({
60
+ basePath: normalizedBasePath,
61
+ requestedSkillName,
62
+ });
63
+ const skillName = deriveSkillName({
64
+ repo,
65
+ skillPath: importedSkillPath,
66
+ explicitName: input.asName,
51
67
  });
52
68
  const existingManagedSkill = findManagedSkill(lockfile, {
53
69
  name: skillName,
@@ -60,10 +76,10 @@ export async function runSkillsAddCommand(context, input) {
60
76
  if (await fs.pathExists(targetDir)) {
61
77
  throw new Error(`A local skill directory already exists: ${targetDir}`);
62
78
  }
63
- const sourceDir = await resolveSkillSourceDir({
79
+ const sourceDir = await resolveSkillSourceDirByPath({
64
80
  checkoutDir: checkout.checkoutDir,
65
81
  repo,
66
- skillName: requestedSkillName,
82
+ skillPath: importedSkillPath,
67
83
  });
68
84
  await replaceManagedSkillDirectory({
69
85
  targetDir,
@@ -14,6 +14,7 @@ const skillsImportOptionsSchema = z.object({
14
14
  skill: z.array(z.string()).optional(),
15
15
  as: nonEmptyOptionStringSchema.optional(),
16
16
  pin: z.boolean().optional().default(false),
17
+ path: nonEmptyOptionStringSchema.optional(),
17
18
  ref: nonEmptyOptionStringSchema.optional(),
18
19
  });
19
20
  const skillsUpdateOptionsSchema = z.object({
@@ -34,6 +35,8 @@ export function addSkillsCommand(input) {
34
35
  Examples:
35
36
  ${commandName} list
36
37
  ${commandName} add anthropics/skills --skill skill-creator
38
+ ${commandName} add anthropics/skills --path . --skill review-helper
39
+ ${commandName} add anthropics/skills --path tools --skill review-helper
37
40
  ${commandName} add vercel-labs/agent-skills --skill pr-review commit
38
41
  ${commandName} rehash skill-creator
39
42
  ${commandName} update skill-creator
@@ -50,7 +53,11 @@ export function addSkillsCommand(input) {
50
53
  skills
51
54
  .command('add <repo>')
52
55
  .description('Add managed skills from a remote repository')
53
- .option('--skill <names...>', 'Import one or more skills from the repository root skills/ directory')
56
+ .option('--skill <names...>', 'Import one or more skills by directory name')
57
+ .option('--path <repoPath>', 'Resolve each --skill from a different repository subdirectory; use . for the repository root instead of the default skills/ directory', parseOptionValue({
58
+ schema: nonEmptyOptionStringSchema,
59
+ optionLabel: '--path',
60
+ }))
54
61
  .option('--as <name>', 'Store the imported skill under a different local managed name', parseOptionValue({
55
62
  schema: nonEmptyOptionStringSchema,
56
63
  optionLabel: '--as',
@@ -68,6 +75,7 @@ export function addSkillsCommand(input) {
68
75
  });
69
76
  await runSkillsAddCommand(resolveContext(), {
70
77
  repo,
78
+ repoPath: parsedOptions.path,
71
79
  skillNames: parsedOptions.skill ?? [],
72
80
  asName: parsedOptions.as,
73
81
  pin: parsedOptions.pin,
@@ -153,7 +153,17 @@ export declare function normalizeRemoteRepo(repo: string): string;
153
153
  export declare function resolveManagedSkillImportPath({ skillName, }: {
154
154
  skillName: string;
155
155
  }): string;
156
- export declare function normalizeImportedSkillPath(skillPath: string | undefined): string;
156
+ /**
157
+ * Normalizes an explicitly provided repository-relative skill path.
158
+ */
159
+ export declare function normalizeImportedSkillPath(skillPath: string | undefined): string | undefined;
160
+ /**
161
+ * Joins an optional base repository path with a requested managed skill name.
162
+ */
163
+ export declare function resolveManagedSkillImportPathFromBase(input: {
164
+ basePath: string | undefined;
165
+ skillName: string;
166
+ }): string;
157
167
  /**
158
168
  * Creates the lockfile record for a newly imported managed skill.
159
169
  */
@@ -204,6 +214,14 @@ export declare function resolveSkillSourceDir(input: {
204
214
  repo: string;
205
215
  skillName: string;
206
216
  }): Promise<string>;
217
+ /**
218
+ * Resolves and validates an arbitrary managed skill directory path from a temporary repository checkout.
219
+ */
220
+ export declare function resolveSkillSourceDirByPath(input: {
221
+ checkoutDir: string;
222
+ repo: string;
223
+ skillPath: string;
224
+ }): Promise<string>;
207
225
  /**
208
226
  * Fetches a validated remote skill directory snapshot for a specific repository path.
209
227
  */
@@ -139,9 +139,30 @@ export function resolveManagedSkillImportPath({ skillName, }) {
139
139
  }
140
140
  return `skills/${trimmedSkillName}`;
141
141
  }
142
+ /**
143
+ * Normalizes an explicitly provided repository-relative skill path.
144
+ */
142
145
  export function normalizeImportedSkillPath(skillPath) {
143
- const normalizedPath = skillPath && skillPath.length > 0 ? skillPath : '.';
144
- return path.normalize(normalizedPath);
146
+ if (skillPath === undefined) {
147
+ return undefined;
148
+ }
149
+ return path.normalize(skillPath);
150
+ }
151
+ /**
152
+ * Joins an optional base repository path with a requested managed skill name.
153
+ */
154
+ export function resolveManagedSkillImportPathFromBase(input) {
155
+ const normalizedBasePath = normalizeImportedSkillPath(input.basePath);
156
+ const defaultSkillPath = resolveManagedSkillImportPath({
157
+ skillName: input.skillName,
158
+ });
159
+ if (normalizedBasePath === '.') {
160
+ return path.normalize(input.skillName);
161
+ }
162
+ if (normalizedBasePath === undefined) {
163
+ return defaultSkillPath;
164
+ }
165
+ return path.normalize(path.join(normalizedBasePath, input.skillName));
145
166
  }
146
167
  /**
147
168
  * Creates the lockfile record for a newly imported managed skill.
@@ -259,6 +280,25 @@ export async function resolveSkillSourceDir(input) {
259
280
  });
260
281
  return sourceDir;
261
282
  }
283
+ /**
284
+ * Resolves and validates an arbitrary managed skill directory path from a temporary repository checkout.
285
+ */
286
+ export async function resolveSkillSourceDirByPath(input) {
287
+ const normalizedSkillPath = normalizeImportedSkillPath(input.skillPath);
288
+ if (normalizedSkillPath === undefined) {
289
+ throw new Error('Skill path may not be empty');
290
+ }
291
+ const sourceDir = resolveRemoteSkillDirectory({
292
+ checkoutDir: input.checkoutDir,
293
+ skillPath: normalizedSkillPath,
294
+ });
295
+ await validateRemoteSkillDirectory({
296
+ sourceDir,
297
+ skillPath: normalizedSkillPath,
298
+ repo: normalizeRemoteRepo(input.repo),
299
+ });
300
+ return sourceDir;
301
+ }
262
302
  /**
263
303
  * Fetches a validated remote skill directory snapshot for a specific repository path.
264
304
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dryai",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "CLI for installing shared AI commands, rules, and skills into Copilot and Cursor.",
5
5
  "type": "module",
6
6
  "repository": {