dryai 2.0.0 → 2.1.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 +23 -2
- package/dest/commands/skills/add.js +3 -1
- package/dest/commands/skills/index.js +39 -4
- 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 +34 -0
- package/dest/lib/skills.js +79 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -119,8 +119,10 @@ dryai install
|
|
|
119
119
|
dryai skills list List local skills
|
|
120
120
|
dryai skills add [options] <repo> Add managed skills from a remote repository
|
|
121
121
|
dryai skills remove <name> Remove a managed skill
|
|
122
|
-
dryai skills
|
|
123
|
-
dryai skills
|
|
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
|
|
124
126
|
```
|
|
125
127
|
|
|
126
128
|
`<repo>` may be a full git remote URL or a GitHub `owner/repo` shorthand such as `anthropics/skills`.
|
|
@@ -152,13 +154,32 @@ The lockfile records:
|
|
|
152
154
|
- the source path within that repository
|
|
153
155
|
- the requested git ref, when one was provided
|
|
154
156
|
- the resolved commit that was imported
|
|
157
|
+
- the last installed content hash for each file in the managed skill directory
|
|
155
158
|
|
|
156
159
|
`skills update` and `skills update-all` re-fetch the tracked repository snapshot and replace the local copied skill directory.
|
|
157
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
|
+
|
|
158
177
|
`skills remove` deletes the local copied skill directory and removes its lockfile entry.
|
|
159
178
|
|
|
160
179
|
`skills list` reports local skill directories, annotating managed entries from the lockfile and flagging managed entries whose local directory is missing.
|
|
161
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
|
+
|
|
162
183
|
## Development
|
|
163
184
|
|
|
164
185
|
For development, use `pnpm dev` to rebuild into `dest/` on change and `pnpm dev:dryai --test install` to run the built CLI.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
-
import { cloneRemoteRepo, createImportedSkillRecord, ensureSkillsLockfile, ensureSkillsRoot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, normalizeRemoteRepo, replaceManagedSkillDirectory, resolveManagedSkillImportPath, resolveSkillSourceDir, saveSkillsLockfile, timestampNow, upsertManagedSkill, } from '../../lib/skills.js';
|
|
2
|
+
import { cloneRemoteRepo, computeDirectoryHashes, createImportedSkillRecord, ensureSkillsLockfile, ensureSkillsRoot, findManagedSkill, formatManagedSkillSummary, getManagedSkillDirectory, loadSkillsLockfile, normalizeRemoteRepo, replaceManagedSkillDirectory, resolveManagedSkillImportPath, resolveSkillSourceDir, 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
|
*/
|
|
@@ -69,8 +69,10 @@ export async function runSkillsAddCommand(context, input) {
|
|
|
69
69
|
targetDir,
|
|
70
70
|
sourceDir,
|
|
71
71
|
});
|
|
72
|
+
const installedFiles = await computeDirectoryHashes(targetDir);
|
|
72
73
|
const importedSkill = createImportedSkillRecord({
|
|
73
74
|
commit: checkout.commit,
|
|
75
|
+
files: installedFiles,
|
|
74
76
|
importedAt: timestampNow(),
|
|
75
77
|
name: skillName,
|
|
76
78
|
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';
|
|
@@ -14,6 +16,9 @@ const skillsImportOptionsSchema = z.object({
|
|
|
14
16
|
pin: z.boolean().optional().default(false),
|
|
15
17
|
ref: nonEmptyOptionStringSchema.optional(),
|
|
16
18
|
});
|
|
19
|
+
const skillsUpdateOptionsSchema = z.object({
|
|
20
|
+
force: z.boolean().optional().default(false),
|
|
21
|
+
});
|
|
17
22
|
/**
|
|
18
23
|
* Registers the managed skills command tree on the parent CLI program.
|
|
19
24
|
*/
|
|
@@ -30,6 +35,7 @@ export function addSkillsCommand(input) {
|
|
|
30
35
|
${commandName} list
|
|
31
36
|
${commandName} add anthropics/skills --skill skill-creator
|
|
32
37
|
${commandName} add vercel-labs/agent-skills --skill pr-review commit
|
|
38
|
+
${commandName} rehash skill-creator
|
|
33
39
|
${commandName} update skill-creator
|
|
34
40
|
`)
|
|
35
41
|
.action(() => {
|
|
@@ -74,17 +80,46 @@ export function addSkillsCommand(input) {
|
|
|
74
80
|
.action(async (skillName) => {
|
|
75
81
|
await runSkillsRemoveCommand(resolveContext(), { skillName });
|
|
76
82
|
});
|
|
83
|
+
skills
|
|
84
|
+
.command('rehash <name>')
|
|
85
|
+
.description('Refresh stored file hashes for one managed skill')
|
|
86
|
+
.action(async (skillName) => {
|
|
87
|
+
await runSkillsRehashCommand(resolveContext(), { skillName });
|
|
88
|
+
});
|
|
89
|
+
skills
|
|
90
|
+
.command('rehash-all')
|
|
91
|
+
.description('Refresh stored file hashes for all managed skills')
|
|
92
|
+
.action(async () => {
|
|
93
|
+
await runSkillsRehashAllCommand(resolveContext());
|
|
94
|
+
});
|
|
77
95
|
skills
|
|
78
96
|
.command('update <name>')
|
|
79
97
|
.description('Update a managed skill from its tracked source')
|
|
80
|
-
.
|
|
81
|
-
|
|
98
|
+
.option('--force', 'Overwrite local skill edits with the fetched remote copy')
|
|
99
|
+
.action(async (skillName, options) => {
|
|
100
|
+
const parsedOptions = parseOptionsObject({
|
|
101
|
+
schema: skillsUpdateOptionsSchema,
|
|
102
|
+
options,
|
|
103
|
+
optionsLabel: 'skills update options',
|
|
104
|
+
});
|
|
105
|
+
await runSkillsUpdateCommand(resolveContext(), {
|
|
106
|
+
force: parsedOptions.force,
|
|
107
|
+
skillName,
|
|
108
|
+
});
|
|
82
109
|
});
|
|
83
110
|
skills
|
|
84
111
|
.command('update-all')
|
|
85
112
|
.description('Update all managed skills from their tracked sources')
|
|
86
|
-
.
|
|
87
|
-
|
|
113
|
+
.option('--force', 'Overwrite local skill edits with the fetched remote copy')
|
|
114
|
+
.action(async (options) => {
|
|
115
|
+
const parsedOptions = parseOptionsObject({
|
|
116
|
+
schema: skillsUpdateOptionsSchema,
|
|
117
|
+
options,
|
|
118
|
+
optionsLabel: 'skills update-all options',
|
|
119
|
+
});
|
|
120
|
+
await runSkillsUpdateAllCommand(resolveContext(), {
|
|
121
|
+
force: parsedOptions.force,
|
|
122
|
+
});
|
|
88
123
|
});
|
|
89
124
|
return skills;
|
|
90
125
|
}
|
|
@@ -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;
|
|
@@ -142,19 +154,41 @@ export declare function resolveManagedSkillImportPath({ skillName, }: {
|
|
|
142
154
|
skillName: string;
|
|
143
155
|
}): string;
|
|
144
156
|
export declare function normalizeImportedSkillPath(skillPath: string | undefined): string;
|
|
157
|
+
/**
|
|
158
|
+
* Creates the lockfile record for a newly imported managed skill.
|
|
159
|
+
*/
|
|
145
160
|
export declare function createImportedSkillRecord(input: {
|
|
146
161
|
commit: string;
|
|
162
|
+
files: ManagedSkillFiles;
|
|
147
163
|
importedAt: string;
|
|
148
164
|
name: string;
|
|
149
165
|
path: string;
|
|
150
166
|
ref: string | undefined;
|
|
151
167
|
repo: string;
|
|
152
168
|
}): ManagedSkill;
|
|
169
|
+
/**
|
|
170
|
+
* Creates the next lockfile record for a managed skill after its local contents have been refreshed.
|
|
171
|
+
*/
|
|
153
172
|
export declare function createUpdatedSkillRecord(input: {
|
|
154
173
|
commit: string;
|
|
155
174
|
existingSkill: ManagedSkill;
|
|
175
|
+
files: ManagedSkillFiles;
|
|
156
176
|
updatedAt: string;
|
|
157
177
|
}): ManagedSkill;
|
|
178
|
+
/**
|
|
179
|
+
* Computes stable SHA-256 hashes for every file within a managed skill directory.
|
|
180
|
+
*/
|
|
181
|
+
export declare function computeDirectoryHashes(directoryPath: string): Promise<ManagedSkillFiles>;
|
|
182
|
+
/**
|
|
183
|
+
* Detects whether a managed skill directory has local content changes relative to the lockfile snapshot.
|
|
184
|
+
*/
|
|
185
|
+
export declare function detectLocalSkillEdits(input: {
|
|
186
|
+
skillDir: string;
|
|
187
|
+
storedFiles: ManagedSkillFiles | undefined;
|
|
188
|
+
}): Promise<{
|
|
189
|
+
changedFiles: string[];
|
|
190
|
+
modified: boolean;
|
|
191
|
+
}>;
|
|
158
192
|
/**
|
|
159
193
|
* Clones a remote repository into a temporary checkout and resolves the fetched commit.
|
|
160
194
|
*/
|
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
|
});
|
|
@@ -140,9 +143,13 @@ export function normalizeImportedSkillPath(skillPath) {
|
|
|
140
143
|
const normalizedPath = skillPath && skillPath.length > 0 ? skillPath : '.';
|
|
141
144
|
return path.normalize(normalizedPath);
|
|
142
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Creates the lockfile record for a newly imported managed skill.
|
|
148
|
+
*/
|
|
143
149
|
export function createImportedSkillRecord(input) {
|
|
144
150
|
return {
|
|
145
151
|
commit: input.commit,
|
|
152
|
+
files: input.files,
|
|
146
153
|
importedAt: input.importedAt,
|
|
147
154
|
name: input.name,
|
|
148
155
|
path: input.path,
|
|
@@ -151,13 +158,56 @@ export function createImportedSkillRecord(input) {
|
|
|
151
158
|
updatedAt: input.importedAt,
|
|
152
159
|
};
|
|
153
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Creates the next lockfile record for a managed skill after its local contents have been refreshed.
|
|
163
|
+
*/
|
|
154
164
|
export function createUpdatedSkillRecord(input) {
|
|
155
165
|
return {
|
|
156
166
|
...input.existingSkill,
|
|
157
167
|
commit: input.commit,
|
|
168
|
+
files: input.files,
|
|
158
169
|
updatedAt: input.updatedAt,
|
|
159
170
|
};
|
|
160
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Computes stable SHA-256 hashes for every file within a managed skill directory.
|
|
174
|
+
*/
|
|
175
|
+
export async function computeDirectoryHashes(directoryPath) {
|
|
176
|
+
const relativeFilePaths = await listRelativeFilePaths(directoryPath);
|
|
177
|
+
const hashEntries = await Promise.all(relativeFilePaths.map(async (relativeFilePath) => {
|
|
178
|
+
const fileBuffer = await fs.readFile(path.join(directoryPath, relativeFilePath));
|
|
179
|
+
return [
|
|
180
|
+
toPortableRelativePath(relativeFilePath),
|
|
181
|
+
createHash('sha256').update(fileBuffer).digest('hex'),
|
|
182
|
+
];
|
|
183
|
+
}));
|
|
184
|
+
return Object.fromEntries(hashEntries);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Detects whether a managed skill directory has local content changes relative to the lockfile snapshot.
|
|
188
|
+
*/
|
|
189
|
+
export async function detectLocalSkillEdits(input) {
|
|
190
|
+
if (!input.storedFiles) {
|
|
191
|
+
return {
|
|
192
|
+
changedFiles: [],
|
|
193
|
+
modified: false,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (!(await fs.pathExists(input.skillDir))) {
|
|
197
|
+
return {
|
|
198
|
+
changedFiles: [],
|
|
199
|
+
modified: false,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const currentFiles = await computeDirectoryHashes(input.skillDir);
|
|
203
|
+
const changedFiles = [...new Set([...Object.keys(input.storedFiles), ...Object.keys(currentFiles)])]
|
|
204
|
+
.filter((relativeFilePath) => input.storedFiles?.[relativeFilePath] !== currentFiles[relativeFilePath])
|
|
205
|
+
.sort((left, right) => left.localeCompare(right));
|
|
206
|
+
return {
|
|
207
|
+
changedFiles,
|
|
208
|
+
modified: changedFiles.length > 0,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
161
211
|
/**
|
|
162
212
|
* Clones a remote repository into a temporary checkout and resolves the fetched commit.
|
|
163
213
|
*/
|
|
@@ -268,6 +318,35 @@ export function shortCommit(commit) {
|
|
|
268
318
|
export function timestampNow() {
|
|
269
319
|
return new Date().toISOString();
|
|
270
320
|
}
|
|
321
|
+
/**
|
|
322
|
+
* Recursively lists all file paths inside a directory relative to that directory root.
|
|
323
|
+
*/
|
|
324
|
+
async function listRelativeFilePaths(directoryPath) {
|
|
325
|
+
const directoryEntries = await fs.readdir(directoryPath, {
|
|
326
|
+
withFileTypes: true,
|
|
327
|
+
});
|
|
328
|
+
const relativeFilePaths = [];
|
|
329
|
+
for (const directoryEntry of directoryEntries) {
|
|
330
|
+
const entryPath = path.join(directoryPath, directoryEntry.name);
|
|
331
|
+
if (directoryEntry.isDirectory()) {
|
|
332
|
+
const nestedFilePaths = await listRelativeFilePaths(entryPath);
|
|
333
|
+
for (const nestedFilePath of nestedFilePaths) {
|
|
334
|
+
relativeFilePaths.push(path.join(directoryEntry.name, nestedFilePath));
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (directoryEntry.isFile()) {
|
|
339
|
+
relativeFilePaths.push(directoryEntry.name);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return relativeFilePaths.sort((left, right) => left.localeCompare(right));
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Normalizes a relative path to use forward slashes for lockfile portability.
|
|
346
|
+
*/
|
|
347
|
+
function toPortableRelativePath(relativePath) {
|
|
348
|
+
return relativePath.split(path.sep).join('/');
|
|
349
|
+
}
|
|
271
350
|
function sortSkillsLockfile(lockfile) {
|
|
272
351
|
return {
|
|
273
352
|
version: lockfile.version,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dryai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.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
|
},
|