dryai 2.0.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 +120 -85
- package/dest/commands/skills/add.d.ts +2 -1
- package/dest/commands/skills/add.js +25 -7
- package/dest/commands/skills/index.js +48 -5
- package/dest/commands/skills/rehash-all.d.ts +6 -0
- package/dest/commands/skills/rehash-all.js +42 -0
- package/dest/commands/skills/rehash.d.ts +8 -0
- package/dest/commands/skills/rehash.js +28 -0
- package/dest/commands/skills/update-all.d.ts +3 -1
- package/dest/commands/skills/update-all.js +33 -13
- package/dest/commands/skills/update.d.ts +1 -0
- package/dest/commands/skills/update.js +23 -12
- package/dest/lib/skills.d.ts +53 -1
- package/dest/lib/skills.js +121 -2
- package/package.json +7 -2
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
|
-
|
|
5
|
+
Global CLI options:
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
11
|
+
These are root-level options for the CLI. They modify command behavior for any given command.
|
|
10
12
|
|
|
11
|
-
##
|
|
13
|
+
## Config Layout
|
|
12
14
|
|
|
13
|
-
Input config files live under the selected
|
|
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
|
-
|
|
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,56 +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 update <name> Update a managed skill from its tracked source
|
|
123
|
-
dryai skills update-all Update all managed skills from their tracked sources
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
`<repo>` may be a full git remote URL or a GitHub `owner/repo` shorthand such as `anthropics/skills`.
|
|
127
|
-
|
|
128
|
-
## Managed Skills
|
|
129
|
-
|
|
130
|
-
Imported skills are copied into `config/skills/<name>/` and tracked in `skills.lock.json`.
|
|
131
|
-
|
|
132
|
-
When you run `skills` commands, local skill directories and `skills.lock.json` are read from and written to the selected config root.
|
|
133
|
-
|
|
134
|
-
`skills add` requires at least one `--skill <name>` value. Each requested skill is always resolved from `<repo root>/skills/<name>`.
|
|
135
|
-
|
|
136
|
-
Use `--as <name>` to choose a different local managed skill name when importing exactly one skill.
|
|
137
|
-
|
|
138
|
-
Examples:
|
|
139
|
-
|
|
140
|
-
```sh
|
|
141
|
-
dryai skills add anthropics/skills --skill skill-creator
|
|
142
|
-
dryai skills add vercel-labs/agent-skills --skill pr-review commit
|
|
143
|
-
dryai skills add https://github.com/vercel-labs/agent-skills.git --skill pr-review commit
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
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.
|
|
147
|
-
|
|
148
|
-
The lockfile records:
|
|
149
|
-
|
|
150
|
-
- the local skill name
|
|
151
|
-
- the source repository
|
|
152
|
-
- the source path within that repository
|
|
153
|
-
- the requested git ref, when one was provided
|
|
154
|
-
- the resolved commit that was imported
|
|
155
|
-
|
|
156
|
-
`skills update` and `skills update-all` re-fetch the tracked repository snapshot and replace the local copied skill directory.
|
|
157
|
-
|
|
158
|
-
`skills remove` deletes the local copied skill directory and removes its lockfile entry.
|
|
159
|
-
|
|
160
|
-
`skills list` reports local skill directories, annotating managed entries from the lockfile and flagging managed entries whose local directory is missing.
|
|
161
|
-
|
|
162
215
|
## Development
|
|
163
216
|
|
|
164
|
-
For development, use `pnpm dev` to rebuild into `dest/` on change and `pnpm dev:dryai
|
|
217
|
+
For development, use `pnpm dev` to rebuild into `dest/` on change and `pnpm dev:dryai <...>` to run the built CLI.
|
|
165
218
|
|
|
166
219
|
Run `pnpm run setup:editor` after installing dependencies if you want the Effect language service workspace patch applied locally.
|
|
167
220
|
|
|
@@ -172,39 +225,21 @@ pnpm run dev
|
|
|
172
225
|
pnpm run test
|
|
173
226
|
pnpm run test:watch
|
|
174
227
|
|
|
175
|
-
pnpm dev:dryai
|
|
176
|
-
pnpm dev:dryai --test install
|
|
177
|
-
pnpm dev:dryai --output-root ./tmp/install-root install
|
|
178
|
-
pnpm dev:dryai --config-root ./config install
|
|
228
|
+
pnpm dev:dryai <...>
|
|
179
229
|
```
|
|
180
230
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
- On pull request open or update
|
|
184
|
-
- Run CI validation with build, test, and `npm pack --dry-run`.
|
|
185
|
-
- On changes landing on `main`
|
|
186
|
-
- Run the same CI validation with build, test, and `npm pack --dry-run`.
|
|
231
|
+
---
|
|
187
232
|
|
|
188
|
-
|
|
189
|
-
- Verify the tag matches the checked-in `package.json` version.
|
|
190
|
-
- Verify the tagged commit is on `main`.
|
|
191
|
-
- Build and test the CLI.
|
|
192
|
-
- Create a tarball with `npm pack`.
|
|
193
|
-
- Create or update the matching GitHub Release.
|
|
194
|
-
- Upload the tarball as a release asset.
|
|
195
|
-
- After the release workflow succeeds, the publish workflow will:
|
|
196
|
-
- Download the tarball artifact produced by the release workflow.
|
|
197
|
-
- Publish that exact tarball to npm using npm trusted publishing.
|
|
233
|
+
## VS Code Setup
|
|
198
234
|
|
|
199
|
-
|
|
235
|
+
VS Code Copilot does not automatically discover prompt files from `~/.copilot/prompts`.
|
|
200
236
|
|
|
201
|
-
|
|
202
|
-
git tag v0.1.0
|
|
203
|
-
git push origin v0.1.0
|
|
204
|
-
```
|
|
237
|
+
Add this to your VS Code user settings so prompt files installed by `dryai` are picked up:
|
|
205
238
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
239
|
+
```json
|
|
240
|
+
{
|
|
241
|
+
"chat.promptFilesLocations": {
|
|
242
|
+
"~/.copilot/prompts": true
|
|
243
|
+
}
|
|
244
|
+
}
|
|
210
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
|
|
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, createImportedSkillRecord, ensureSkillsLockfile, ensureSkillsRoot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, normalizeRemoteRepo, replaceManagedSkillDirectory, resolveManagedSkillImportPath,
|
|
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
|
-
*
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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,17 +76,19 @@ 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
|
|
79
|
+
const sourceDir = await resolveSkillSourceDirByPath({
|
|
64
80
|
checkoutDir: checkout.checkoutDir,
|
|
65
81
|
repo,
|
|
66
|
-
|
|
82
|
+
skillPath: importedSkillPath,
|
|
67
83
|
});
|
|
68
84
|
await replaceManagedSkillDirectory({
|
|
69
85
|
targetDir,
|
|
70
86
|
sourceDir,
|
|
71
87
|
});
|
|
88
|
+
const installedFiles = await computeDirectoryHashes(targetDir);
|
|
72
89
|
const importedSkill = createImportedSkillRecord({
|
|
73
90
|
commit: checkout.commit,
|
|
91
|
+
files: installedFiles,
|
|
74
92
|
importedAt: timestampNow(),
|
|
75
93
|
name: skillName,
|
|
76
94
|
path: importedSkillPath,
|
|
@@ -5,6 +5,8 @@ import { nonEmptyOptionStringSchema, parseOptionsObject, parseOptionValue, } fro
|
|
|
5
5
|
import {} from '../../lib/context.js';
|
|
6
6
|
import { runSkillsAddCommand } from './add.js';
|
|
7
7
|
import { runSkillsListCommand } from './list.js';
|
|
8
|
+
import { runSkillsRehashAllCommand } from './rehash-all.js';
|
|
9
|
+
import { runSkillsRehashCommand } from './rehash.js';
|
|
8
10
|
import { runSkillsRemoveCommand } from './remove.js';
|
|
9
11
|
import { runSkillsUpdateAllCommand } from './update-all.js';
|
|
10
12
|
import { runSkillsUpdateCommand } from './update.js';
|
|
@@ -12,8 +14,12 @@ const skillsImportOptionsSchema = z.object({
|
|
|
12
14
|
skill: z.array(z.string()).optional(),
|
|
13
15
|
as: nonEmptyOptionStringSchema.optional(),
|
|
14
16
|
pin: z.boolean().optional().default(false),
|
|
17
|
+
path: nonEmptyOptionStringSchema.optional(),
|
|
15
18
|
ref: nonEmptyOptionStringSchema.optional(),
|
|
16
19
|
});
|
|
20
|
+
const skillsUpdateOptionsSchema = z.object({
|
|
21
|
+
force: z.boolean().optional().default(false),
|
|
22
|
+
});
|
|
17
23
|
/**
|
|
18
24
|
* Registers the managed skills command tree on the parent CLI program.
|
|
19
25
|
*/
|
|
@@ -29,7 +35,10 @@ export function addSkillsCommand(input) {
|
|
|
29
35
|
Examples:
|
|
30
36
|
${commandName} list
|
|
31
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
|
|
32
40
|
${commandName} add vercel-labs/agent-skills --skill pr-review commit
|
|
41
|
+
${commandName} rehash skill-creator
|
|
33
42
|
${commandName} update skill-creator
|
|
34
43
|
`)
|
|
35
44
|
.action(() => {
|
|
@@ -44,7 +53,11 @@ export function addSkillsCommand(input) {
|
|
|
44
53
|
skills
|
|
45
54
|
.command('add <repo>')
|
|
46
55
|
.description('Add managed skills from a remote repository')
|
|
47
|
-
.option('--skill <names...>', 'Import one or more skills
|
|
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
|
+
}))
|
|
48
61
|
.option('--as <name>', 'Store the imported skill under a different local managed name', parseOptionValue({
|
|
49
62
|
schema: nonEmptyOptionStringSchema,
|
|
50
63
|
optionLabel: '--as',
|
|
@@ -62,6 +75,7 @@ export function addSkillsCommand(input) {
|
|
|
62
75
|
});
|
|
63
76
|
await runSkillsAddCommand(resolveContext(), {
|
|
64
77
|
repo,
|
|
78
|
+
repoPath: parsedOptions.path,
|
|
65
79
|
skillNames: parsedOptions.skill ?? [],
|
|
66
80
|
asName: parsedOptions.as,
|
|
67
81
|
pin: parsedOptions.pin,
|
|
@@ -74,17 +88,46 @@ export function addSkillsCommand(input) {
|
|
|
74
88
|
.action(async (skillName) => {
|
|
75
89
|
await runSkillsRemoveCommand(resolveContext(), { skillName });
|
|
76
90
|
});
|
|
91
|
+
skills
|
|
92
|
+
.command('rehash <name>')
|
|
93
|
+
.description('Refresh stored file hashes for one managed skill')
|
|
94
|
+
.action(async (skillName) => {
|
|
95
|
+
await runSkillsRehashCommand(resolveContext(), { skillName });
|
|
96
|
+
});
|
|
97
|
+
skills
|
|
98
|
+
.command('rehash-all')
|
|
99
|
+
.description('Refresh stored file hashes for all managed skills')
|
|
100
|
+
.action(async () => {
|
|
101
|
+
await runSkillsRehashAllCommand(resolveContext());
|
|
102
|
+
});
|
|
77
103
|
skills
|
|
78
104
|
.command('update <name>')
|
|
79
105
|
.description('Update a managed skill from its tracked source')
|
|
80
|
-
.
|
|
81
|
-
|
|
106
|
+
.option('--force', 'Overwrite local skill edits with the fetched remote copy')
|
|
107
|
+
.action(async (skillName, options) => {
|
|
108
|
+
const parsedOptions = parseOptionsObject({
|
|
109
|
+
schema: skillsUpdateOptionsSchema,
|
|
110
|
+
options,
|
|
111
|
+
optionsLabel: 'skills update options',
|
|
112
|
+
});
|
|
113
|
+
await runSkillsUpdateCommand(resolveContext(), {
|
|
114
|
+
force: parsedOptions.force,
|
|
115
|
+
skillName,
|
|
116
|
+
});
|
|
82
117
|
});
|
|
83
118
|
skills
|
|
84
119
|
.command('update-all')
|
|
85
120
|
.description('Update all managed skills from their tracked sources')
|
|
86
|
-
.
|
|
87
|
-
|
|
121
|
+
.option('--force', 'Overwrite local skill edits with the fetched remote copy')
|
|
122
|
+
.action(async (options) => {
|
|
123
|
+
const parsedOptions = parseOptionsObject({
|
|
124
|
+
schema: skillsUpdateOptionsSchema,
|
|
125
|
+
options,
|
|
126
|
+
optionsLabel: 'skills update-all options',
|
|
127
|
+
});
|
|
128
|
+
await runSkillsUpdateAllCommand(resolveContext(), {
|
|
129
|
+
force: parsedOptions.force,
|
|
130
|
+
});
|
|
88
131
|
});
|
|
89
132
|
return skills;
|
|
90
133
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentsContext } from '../../lib/context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Refreshes the stored file hashes for every managed skill using current local directory contents.
|
|
4
|
+
*/
|
|
5
|
+
export declare function runSkillsRehashAllCommand(context: AgentsContext): Promise<void>;
|
|
6
|
+
//# sourceMappingURL=rehash-all.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { computeDirectoryHashes, createUpdatedSkillRecord, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
3
|
+
/**
|
|
4
|
+
* Refreshes the stored file hashes for every managed skill using current local directory contents.
|
|
5
|
+
*/
|
|
6
|
+
export async function runSkillsRehashAllCommand(context) {
|
|
7
|
+
let lockfile = await loadSkillsLockfile(context);
|
|
8
|
+
if (lockfile.skills.length === 0) {
|
|
9
|
+
console.log('No managed skills to rehash.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const rehashedLines = [];
|
|
13
|
+
const skippedLines = [];
|
|
14
|
+
for (const managedSkill of lockfile.skills) {
|
|
15
|
+
const targetDir = getManagedSkillDirectory(context, {
|
|
16
|
+
skillName: managedSkill.name,
|
|
17
|
+
});
|
|
18
|
+
if (!(await fs.pathExists(targetDir))) {
|
|
19
|
+
skippedLines.push(`- ${formatManagedSkillSummary(managedSkill)} missing-local-directory`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const installedFiles = await computeDirectoryHashes(targetDir);
|
|
23
|
+
const updatedSkill = createUpdatedSkillRecord({
|
|
24
|
+
commit: managedSkill.commit,
|
|
25
|
+
existingSkill: managedSkill,
|
|
26
|
+
files: installedFiles,
|
|
27
|
+
updatedAt: timestampNow(),
|
|
28
|
+
});
|
|
29
|
+
lockfile = upsertManagedSkill(lockfile, { updatedSkill });
|
|
30
|
+
rehashedLines.push(`- ${formatManagedSkillSummary(updatedSkill)}`);
|
|
31
|
+
}
|
|
32
|
+
await saveSkillsLockfile(context, { lockfile });
|
|
33
|
+
if (rehashedLines.length > 0) {
|
|
34
|
+
console.log(`Rehashed ${rehashedLines.length} managed skills:\n${rehashedLines.join('\n')}`);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log('No managed skills were rehashed.');
|
|
38
|
+
}
|
|
39
|
+
if (skippedLines.length > 0) {
|
|
40
|
+
console.warn(`Skipped ${skippedLines.length} managed skills because the local directory is missing:\n${skippedLines.join('\n')}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AgentsContext } from '../../lib/context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Refreshes the stored file hashes for one managed skill using the current local directory contents.
|
|
4
|
+
*/
|
|
5
|
+
export declare function runSkillsRehashCommand(context: AgentsContext, input: {
|
|
6
|
+
skillName: string;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=rehash.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import { computeDirectoryHashes, createUpdatedSkillRecord, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
3
|
+
/**
|
|
4
|
+
* Refreshes the stored file hashes for one managed skill using the current local directory contents.
|
|
5
|
+
*/
|
|
6
|
+
export async function runSkillsRehashCommand(context, input) {
|
|
7
|
+
const { skillName } = input;
|
|
8
|
+
const lockfile = await loadSkillsLockfile(context);
|
|
9
|
+
const managedSkill = findManagedSkill(lockfile, { name: skillName });
|
|
10
|
+
if (!managedSkill) {
|
|
11
|
+
throw new Error(`Managed skill not found: ${skillName}`);
|
|
12
|
+
}
|
|
13
|
+
const targetDir = getManagedSkillDirectory(context, { skillName });
|
|
14
|
+
if (!(await fs.pathExists(targetDir))) {
|
|
15
|
+
throw new Error(`Managed skill directory not found: ${targetDir}`);
|
|
16
|
+
}
|
|
17
|
+
const installedFiles = await computeDirectoryHashes(targetDir);
|
|
18
|
+
const updatedSkill = createUpdatedSkillRecord({
|
|
19
|
+
commit: managedSkill.commit,
|
|
20
|
+
existingSkill: managedSkill,
|
|
21
|
+
files: installedFiles,
|
|
22
|
+
updatedAt: timestampNow(),
|
|
23
|
+
});
|
|
24
|
+
await saveSkillsLockfile(context, {
|
|
25
|
+
lockfile: upsertManagedSkill(lockfile, { updatedSkill }),
|
|
26
|
+
});
|
|
27
|
+
console.log(`Rehashed ${formatManagedSkillSummary(updatedSkill)}`);
|
|
28
|
+
}
|
|
@@ -2,5 +2,7 @@ import type { AgentsContext } from '../../lib/context.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* Updates every managed skill from its tracked remote source and saves the refreshed lockfile.
|
|
4
4
|
*/
|
|
5
|
-
export declare function runSkillsUpdateAllCommand(context: AgentsContext
|
|
5
|
+
export declare function runSkillsUpdateAllCommand(context: AgentsContext, input: {
|
|
6
|
+
force: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
6
8
|
//# sourceMappingURL=update-all.d.ts.map
|
|
@@ -1,15 +1,27 @@
|
|
|
1
|
-
import { createUpdatedSkillRecord, fetchRemoteSkillSnapshot, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, replaceManagedSkillDirectory, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
1
|
+
import { computeDirectoryHashes, createUpdatedSkillRecord, detectLocalSkillEdits, fetchRemoteSkillSnapshot, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, replaceManagedSkillDirectory, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
2
2
|
/**
|
|
3
3
|
* Updates every managed skill from its tracked remote source and saves the refreshed lockfile.
|
|
4
4
|
*/
|
|
5
|
-
export async function runSkillsUpdateAllCommand(context) {
|
|
5
|
+
export async function runSkillsUpdateAllCommand(context, input) {
|
|
6
6
|
let lockfile = await loadSkillsLockfile(context);
|
|
7
7
|
if (lockfile.skills.length === 0) {
|
|
8
8
|
console.log('No managed skills to update.');
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
11
|
const updatedLines = [];
|
|
12
|
+
const skippedLines = [];
|
|
12
13
|
for (const managedSkill of lockfile.skills) {
|
|
14
|
+
const targetDir = getManagedSkillDirectory(context, {
|
|
15
|
+
skillName: managedSkill.name,
|
|
16
|
+
});
|
|
17
|
+
const localEditState = await detectLocalSkillEdits({
|
|
18
|
+
skillDir: targetDir,
|
|
19
|
+
storedFiles: managedSkill.files,
|
|
20
|
+
});
|
|
21
|
+
if (localEditState.modified && !input.force) {
|
|
22
|
+
skippedLines.push(`- ${managedSkill.name} local edits detected in ${localEditState.changedFiles.join(', ')}`);
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
13
25
|
const snapshot = await fetchRemoteSkillSnapshot({
|
|
14
26
|
ref: managedSkill.ref,
|
|
15
27
|
repo: managedSkill.repo,
|
|
@@ -17,23 +29,31 @@ export async function runSkillsUpdateAllCommand(context) {
|
|
|
17
29
|
});
|
|
18
30
|
try {
|
|
19
31
|
await replaceManagedSkillDirectory({
|
|
20
|
-
targetDir
|
|
21
|
-
skillName: managedSkill.name,
|
|
22
|
-
}),
|
|
32
|
+
targetDir,
|
|
23
33
|
sourceDir: snapshot.sourceDir,
|
|
24
34
|
});
|
|
35
|
+
const installedFiles = await computeDirectoryHashes(targetDir);
|
|
36
|
+
const updatedSkill = createUpdatedSkillRecord({
|
|
37
|
+
commit: snapshot.commit,
|
|
38
|
+
existingSkill: managedSkill,
|
|
39
|
+
files: installedFiles,
|
|
40
|
+
updatedAt: timestampNow(),
|
|
41
|
+
});
|
|
42
|
+
lockfile = upsertManagedSkill(lockfile, { updatedSkill });
|
|
43
|
+
updatedLines.push(`- ${formatManagedSkillSummary(updatedSkill)}`);
|
|
25
44
|
}
|
|
26
45
|
finally {
|
|
27
46
|
await snapshot.cleanup();
|
|
28
47
|
}
|
|
29
|
-
const updatedSkill = createUpdatedSkillRecord({
|
|
30
|
-
commit: snapshot.commit,
|
|
31
|
-
existingSkill: managedSkill,
|
|
32
|
-
updatedAt: timestampNow(),
|
|
33
|
-
});
|
|
34
|
-
lockfile = upsertManagedSkill(lockfile, { updatedSkill });
|
|
35
|
-
updatedLines.push(`- ${formatManagedSkillSummary(updatedSkill)}`);
|
|
36
48
|
}
|
|
37
49
|
await saveSkillsLockfile(context, { lockfile });
|
|
38
|
-
|
|
50
|
+
if (updatedLines.length > 0) {
|
|
51
|
+
console.log(`Updated ${updatedLines.length} managed skills:\n${updatedLines.join('\n')}`);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.log('No managed skills were updated.');
|
|
55
|
+
}
|
|
56
|
+
if (skippedLines.length > 0) {
|
|
57
|
+
console.warn(`Skipped ${skippedLines.length} managed skills due to local edits. Re-run with --force to overwrite local changes:\n${skippedLines.join('\n')}`);
|
|
58
|
+
}
|
|
39
59
|
}
|
|
@@ -3,6 +3,7 @@ import type { AgentsContext } from '../../lib/context.js';
|
|
|
3
3
|
* Updates one managed skill from its tracked remote source and refreshes the lockfile.
|
|
4
4
|
*/
|
|
5
5
|
export declare function runSkillsUpdateCommand(context: AgentsContext, input: {
|
|
6
|
+
force: boolean;
|
|
6
7
|
skillName: string;
|
|
7
8
|
}): Promise<void>;
|
|
8
9
|
//# sourceMappingURL=update.d.ts.map
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
import { createUpdatedSkillRecord, fetchRemoteSkillSnapshot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, replaceManagedSkillDirectory, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
1
|
+
import { computeDirectoryHashes, createUpdatedSkillRecord, detectLocalSkillEdits, fetchRemoteSkillSnapshot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, replaceManagedSkillDirectory, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
2
2
|
/**
|
|
3
3
|
* Updates one managed skill from its tracked remote source and refreshes the lockfile.
|
|
4
4
|
*/
|
|
5
5
|
export async function runSkillsUpdateCommand(context, input) {
|
|
6
|
-
const { skillName } = input;
|
|
6
|
+
const { force, skillName } = input;
|
|
7
7
|
const lockfile = await loadSkillsLockfile(context);
|
|
8
8
|
const managedSkill = findManagedSkill(lockfile, { name: skillName });
|
|
9
9
|
if (!managedSkill) {
|
|
10
10
|
throw new Error(`Managed skill not found: ${skillName}`);
|
|
11
11
|
}
|
|
12
|
+
const targetDir = getManagedSkillDirectory(context, { skillName });
|
|
13
|
+
const localEditState = await detectLocalSkillEdits({
|
|
14
|
+
skillDir: targetDir,
|
|
15
|
+
storedFiles: managedSkill.files,
|
|
16
|
+
});
|
|
17
|
+
if (localEditState.modified && !force) {
|
|
18
|
+
console.warn(`Skipped ${skillName} because local edits were detected in: ${localEditState.changedFiles.join(', ')}. Re-run with --force to overwrite local changes.`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
12
21
|
const snapshot = await fetchRemoteSkillSnapshot({
|
|
13
22
|
ref: managedSkill.ref,
|
|
14
23
|
repo: managedSkill.repo,
|
|
@@ -16,20 +25,22 @@ export async function runSkillsUpdateCommand(context, input) {
|
|
|
16
25
|
});
|
|
17
26
|
try {
|
|
18
27
|
await replaceManagedSkillDirectory({
|
|
19
|
-
targetDir
|
|
28
|
+
targetDir,
|
|
20
29
|
sourceDir: snapshot.sourceDir,
|
|
21
30
|
});
|
|
31
|
+
const installedFiles = await computeDirectoryHashes(targetDir);
|
|
32
|
+
const updatedSkill = createUpdatedSkillRecord({
|
|
33
|
+
commit: snapshot.commit,
|
|
34
|
+
existingSkill: managedSkill,
|
|
35
|
+
files: installedFiles,
|
|
36
|
+
updatedAt: timestampNow(),
|
|
37
|
+
});
|
|
38
|
+
await saveSkillsLockfile(context, {
|
|
39
|
+
lockfile: upsertManagedSkill(lockfile, { updatedSkill }),
|
|
40
|
+
});
|
|
41
|
+
console.log(`Updated ${formatManagedSkillSummary(updatedSkill)}`);
|
|
22
42
|
}
|
|
23
43
|
finally {
|
|
24
44
|
await snapshot.cleanup();
|
|
25
45
|
}
|
|
26
|
-
const updatedSkill = createUpdatedSkillRecord({
|
|
27
|
-
commit: snapshot.commit,
|
|
28
|
-
existingSkill: managedSkill,
|
|
29
|
-
updatedAt: timestampNow(),
|
|
30
|
-
});
|
|
31
|
-
await saveSkillsLockfile(context, {
|
|
32
|
-
lockfile: upsertManagedSkill(lockfile, { updatedSkill }),
|
|
33
|
-
});
|
|
34
|
-
console.log(`Updated ${formatManagedSkillSummary(updatedSkill)}`);
|
|
35
46
|
}
|
package/dest/lib/skills.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { AgentsContext } from './context.js';
|
|
3
|
+
declare const managedSkillFilesSchema: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
3
4
|
declare const skillLockEntrySchema: z.ZodObject<{
|
|
4
5
|
name: z.ZodString;
|
|
5
6
|
repo: z.ZodString;
|
|
6
7
|
path: z.ZodString;
|
|
7
8
|
ref: z.ZodOptional<z.ZodString>;
|
|
8
9
|
commit: z.ZodString;
|
|
10
|
+
files: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
9
11
|
importedAt: z.ZodString;
|
|
10
12
|
updatedAt: z.ZodString;
|
|
11
13
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -16,6 +18,7 @@ declare const skillLockEntrySchema: z.ZodObject<{
|
|
|
16
18
|
importedAt: string;
|
|
17
19
|
updatedAt: string;
|
|
18
20
|
ref?: string | undefined;
|
|
21
|
+
files?: Record<string, string> | undefined;
|
|
19
22
|
}, {
|
|
20
23
|
name: string;
|
|
21
24
|
path: string;
|
|
@@ -24,6 +27,7 @@ declare const skillLockEntrySchema: z.ZodObject<{
|
|
|
24
27
|
importedAt: string;
|
|
25
28
|
updatedAt: string;
|
|
26
29
|
ref?: string | undefined;
|
|
30
|
+
files?: Record<string, string> | undefined;
|
|
27
31
|
}>;
|
|
28
32
|
declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
29
33
|
version: z.ZodLiteral<1>;
|
|
@@ -33,6 +37,7 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
33
37
|
path: z.ZodString;
|
|
34
38
|
ref: z.ZodOptional<z.ZodString>;
|
|
35
39
|
commit: z.ZodString;
|
|
40
|
+
files: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
36
41
|
importedAt: z.ZodString;
|
|
37
42
|
updatedAt: z.ZodString;
|
|
38
43
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -43,6 +48,7 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
43
48
|
importedAt: string;
|
|
44
49
|
updatedAt: string;
|
|
45
50
|
ref?: string | undefined;
|
|
51
|
+
files?: Record<string, string> | undefined;
|
|
46
52
|
}, {
|
|
47
53
|
name: string;
|
|
48
54
|
path: string;
|
|
@@ -51,6 +57,7 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
51
57
|
importedAt: string;
|
|
52
58
|
updatedAt: string;
|
|
53
59
|
ref?: string | undefined;
|
|
60
|
+
files?: Record<string, string> | undefined;
|
|
54
61
|
}>, "many">;
|
|
55
62
|
}, "strip", z.ZodTypeAny, {
|
|
56
63
|
skills: {
|
|
@@ -61,6 +68,7 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
61
68
|
importedAt: string;
|
|
62
69
|
updatedAt: string;
|
|
63
70
|
ref?: string | undefined;
|
|
71
|
+
files?: Record<string, string> | undefined;
|
|
64
72
|
}[];
|
|
65
73
|
version: 1;
|
|
66
74
|
}, {
|
|
@@ -72,6 +80,7 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
72
80
|
importedAt: string;
|
|
73
81
|
updatedAt: string;
|
|
74
82
|
ref?: string | undefined;
|
|
83
|
+
files?: Record<string, string> | undefined;
|
|
75
84
|
}[];
|
|
76
85
|
version: 1;
|
|
77
86
|
}>, {
|
|
@@ -83,6 +92,7 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
83
92
|
importedAt: string;
|
|
84
93
|
updatedAt: string;
|
|
85
94
|
ref?: string | undefined;
|
|
95
|
+
files?: Record<string, string> | undefined;
|
|
86
96
|
}[];
|
|
87
97
|
version: 1;
|
|
88
98
|
}, {
|
|
@@ -94,11 +104,13 @@ declare const skillsLockfileSchema: z.ZodEffects<z.ZodObject<{
|
|
|
94
104
|
importedAt: string;
|
|
95
105
|
updatedAt: string;
|
|
96
106
|
ref?: string | undefined;
|
|
107
|
+
files?: Record<string, string> | undefined;
|
|
97
108
|
}[];
|
|
98
109
|
version: 1;
|
|
99
110
|
}>;
|
|
100
111
|
export type ManagedSkill = z.infer<typeof skillLockEntrySchema>;
|
|
101
112
|
export type SkillsLockfile = z.infer<typeof skillsLockfileSchema>;
|
|
113
|
+
export type ManagedSkillFiles = z.infer<typeof managedSkillFilesSchema>;
|
|
102
114
|
export type RemoteSkillSnapshot = {
|
|
103
115
|
cleanup: () => Promise<void>;
|
|
104
116
|
commit: string;
|
|
@@ -141,20 +153,52 @@ export declare function normalizeRemoteRepo(repo: string): string;
|
|
|
141
153
|
export declare function resolveManagedSkillImportPath({ skillName, }: {
|
|
142
154
|
skillName: string;
|
|
143
155
|
}): string;
|
|
144
|
-
|
|
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;
|
|
167
|
+
/**
|
|
168
|
+
* Creates the lockfile record for a newly imported managed skill.
|
|
169
|
+
*/
|
|
145
170
|
export declare function createImportedSkillRecord(input: {
|
|
146
171
|
commit: string;
|
|
172
|
+
files: ManagedSkillFiles;
|
|
147
173
|
importedAt: string;
|
|
148
174
|
name: string;
|
|
149
175
|
path: string;
|
|
150
176
|
ref: string | undefined;
|
|
151
177
|
repo: string;
|
|
152
178
|
}): ManagedSkill;
|
|
179
|
+
/**
|
|
180
|
+
* Creates the next lockfile record for a managed skill after its local contents have been refreshed.
|
|
181
|
+
*/
|
|
153
182
|
export declare function createUpdatedSkillRecord(input: {
|
|
154
183
|
commit: string;
|
|
155
184
|
existingSkill: ManagedSkill;
|
|
185
|
+
files: ManagedSkillFiles;
|
|
156
186
|
updatedAt: string;
|
|
157
187
|
}): ManagedSkill;
|
|
188
|
+
/**
|
|
189
|
+
* Computes stable SHA-256 hashes for every file within a managed skill directory.
|
|
190
|
+
*/
|
|
191
|
+
export declare function computeDirectoryHashes(directoryPath: string): Promise<ManagedSkillFiles>;
|
|
192
|
+
/**
|
|
193
|
+
* Detects whether a managed skill directory has local content changes relative to the lockfile snapshot.
|
|
194
|
+
*/
|
|
195
|
+
export declare function detectLocalSkillEdits(input: {
|
|
196
|
+
skillDir: string;
|
|
197
|
+
storedFiles: ManagedSkillFiles | undefined;
|
|
198
|
+
}): Promise<{
|
|
199
|
+
changedFiles: string[];
|
|
200
|
+
modified: boolean;
|
|
201
|
+
}>;
|
|
158
202
|
/**
|
|
159
203
|
* Clones a remote repository into a temporary checkout and resolves the fetched commit.
|
|
160
204
|
*/
|
|
@@ -170,6 +214,14 @@ export declare function resolveSkillSourceDir(input: {
|
|
|
170
214
|
repo: string;
|
|
171
215
|
skillName: string;
|
|
172
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>;
|
|
173
225
|
/**
|
|
174
226
|
* Fetches a validated remote skill directory snapshot for a specific repository path.
|
|
175
227
|
*/
|
package/dest/lib/skills.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
2
3
|
import os from 'node:os';
|
|
3
4
|
import path from 'node:path';
|
|
4
5
|
import { simpleGit } from 'simple-git';
|
|
5
6
|
import { z } from 'zod';
|
|
7
|
+
const managedSkillFilesSchema = z.record(z.string(), z.string());
|
|
6
8
|
const skillLockEntrySchema = z.object({
|
|
7
9
|
name: z.string().min(1),
|
|
8
10
|
repo: z.string().min(1),
|
|
9
11
|
path: z.string().min(1),
|
|
10
12
|
ref: z.string().min(1).optional(),
|
|
11
13
|
commit: z.string().min(1),
|
|
14
|
+
files: managedSkillFilesSchema.optional(),
|
|
12
15
|
importedAt: z.string().datetime({ offset: true }),
|
|
13
16
|
updatedAt: z.string().datetime({ offset: true }),
|
|
14
17
|
});
|
|
@@ -136,13 +139,38 @@ export function resolveManagedSkillImportPath({ skillName, }) {
|
|
|
136
139
|
}
|
|
137
140
|
return `skills/${trimmedSkillName}`;
|
|
138
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Normalizes an explicitly provided repository-relative skill path.
|
|
144
|
+
*/
|
|
139
145
|
export function normalizeImportedSkillPath(skillPath) {
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
if (skillPath === undefined) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return path.normalize(skillPath);
|
|
142
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));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Creates the lockfile record for a newly imported managed skill.
|
|
169
|
+
*/
|
|
143
170
|
export function createImportedSkillRecord(input) {
|
|
144
171
|
return {
|
|
145
172
|
commit: input.commit,
|
|
173
|
+
files: input.files,
|
|
146
174
|
importedAt: input.importedAt,
|
|
147
175
|
name: input.name,
|
|
148
176
|
path: input.path,
|
|
@@ -151,13 +179,56 @@ export function createImportedSkillRecord(input) {
|
|
|
151
179
|
updatedAt: input.importedAt,
|
|
152
180
|
};
|
|
153
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Creates the next lockfile record for a managed skill after its local contents have been refreshed.
|
|
184
|
+
*/
|
|
154
185
|
export function createUpdatedSkillRecord(input) {
|
|
155
186
|
return {
|
|
156
187
|
...input.existingSkill,
|
|
157
188
|
commit: input.commit,
|
|
189
|
+
files: input.files,
|
|
158
190
|
updatedAt: input.updatedAt,
|
|
159
191
|
};
|
|
160
192
|
}
|
|
193
|
+
/**
|
|
194
|
+
* Computes stable SHA-256 hashes for every file within a managed skill directory.
|
|
195
|
+
*/
|
|
196
|
+
export async function computeDirectoryHashes(directoryPath) {
|
|
197
|
+
const relativeFilePaths = await listRelativeFilePaths(directoryPath);
|
|
198
|
+
const hashEntries = await Promise.all(relativeFilePaths.map(async (relativeFilePath) => {
|
|
199
|
+
const fileBuffer = await fs.readFile(path.join(directoryPath, relativeFilePath));
|
|
200
|
+
return [
|
|
201
|
+
toPortableRelativePath(relativeFilePath),
|
|
202
|
+
createHash('sha256').update(fileBuffer).digest('hex'),
|
|
203
|
+
];
|
|
204
|
+
}));
|
|
205
|
+
return Object.fromEntries(hashEntries);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Detects whether a managed skill directory has local content changes relative to the lockfile snapshot.
|
|
209
|
+
*/
|
|
210
|
+
export async function detectLocalSkillEdits(input) {
|
|
211
|
+
if (!input.storedFiles) {
|
|
212
|
+
return {
|
|
213
|
+
changedFiles: [],
|
|
214
|
+
modified: false,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
if (!(await fs.pathExists(input.skillDir))) {
|
|
218
|
+
return {
|
|
219
|
+
changedFiles: [],
|
|
220
|
+
modified: false,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const currentFiles = await computeDirectoryHashes(input.skillDir);
|
|
224
|
+
const changedFiles = [...new Set([...Object.keys(input.storedFiles), ...Object.keys(currentFiles)])]
|
|
225
|
+
.filter((relativeFilePath) => input.storedFiles?.[relativeFilePath] !== currentFiles[relativeFilePath])
|
|
226
|
+
.sort((left, right) => left.localeCompare(right));
|
|
227
|
+
return {
|
|
228
|
+
changedFiles,
|
|
229
|
+
modified: changedFiles.length > 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
161
232
|
/**
|
|
162
233
|
* Clones a remote repository into a temporary checkout and resolves the fetched commit.
|
|
163
234
|
*/
|
|
@@ -209,6 +280,25 @@ export async function resolveSkillSourceDir(input) {
|
|
|
209
280
|
});
|
|
210
281
|
return sourceDir;
|
|
211
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
|
+
}
|
|
212
302
|
/**
|
|
213
303
|
* Fetches a validated remote skill directory snapshot for a specific repository path.
|
|
214
304
|
*/
|
|
@@ -268,6 +358,35 @@ export function shortCommit(commit) {
|
|
|
268
358
|
export function timestampNow() {
|
|
269
359
|
return new Date().toISOString();
|
|
270
360
|
}
|
|
361
|
+
/**
|
|
362
|
+
* Recursively lists all file paths inside a directory relative to that directory root.
|
|
363
|
+
*/
|
|
364
|
+
async function listRelativeFilePaths(directoryPath) {
|
|
365
|
+
const directoryEntries = await fs.readdir(directoryPath, {
|
|
366
|
+
withFileTypes: true,
|
|
367
|
+
});
|
|
368
|
+
const relativeFilePaths = [];
|
|
369
|
+
for (const directoryEntry of directoryEntries) {
|
|
370
|
+
const entryPath = path.join(directoryPath, directoryEntry.name);
|
|
371
|
+
if (directoryEntry.isDirectory()) {
|
|
372
|
+
const nestedFilePaths = await listRelativeFilePaths(entryPath);
|
|
373
|
+
for (const nestedFilePath of nestedFilePaths) {
|
|
374
|
+
relativeFilePaths.push(path.join(directoryEntry.name, nestedFilePath));
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (directoryEntry.isFile()) {
|
|
379
|
+
relativeFilePaths.push(directoryEntry.name);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return relativeFilePaths.sort((left, right) => left.localeCompare(right));
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Normalizes a relative path to use forward slashes for lockfile portability.
|
|
386
|
+
*/
|
|
387
|
+
function toPortableRelativePath(relativePath) {
|
|
388
|
+
return relativePath.split(path.sep).join('/');
|
|
389
|
+
}
|
|
271
390
|
function sortSkillsLockfile(lockfile) {
|
|
272
391
|
return {
|
|
273
392
|
version: lockfile.version,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dryai",
|
|
3
|
-
"version": "2.
|
|
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": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"dev:dryai": "node ./dest/main.js",
|
|
20
20
|
"tsc": "tsc -b",
|
|
21
21
|
"test": "vitest run",
|
|
22
|
-
"test:watch": "vitest"
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"prepare": "husky"
|
|
23
24
|
},
|
|
24
25
|
"bin": {
|
|
25
26
|
"dryai": "dest/main.js"
|
|
@@ -37,10 +38,14 @@
|
|
|
37
38
|
"zod": "^3.24.1"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
41
|
+
"@commitlint/cli": "^20.5.0",
|
|
42
|
+
"@commitlint/config-conventional": "^20.5.0",
|
|
43
|
+
"@commitlint/types": "^20.5.0",
|
|
40
44
|
"@effect/language-service": "^0.85.0",
|
|
41
45
|
"@effect/vitest": "^0.29.0",
|
|
42
46
|
"@types/fs-extra": "^11.0.4",
|
|
43
47
|
"@types/node": "^24.0.0",
|
|
48
|
+
"husky": "^9.1.7",
|
|
44
49
|
"typescript": "^5.8.3",
|
|
45
50
|
"vitest": "^4.1.4"
|
|
46
51
|
},
|