@rbbtsn0w/adg 0.1.0-alpha.1 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/adg.js +703 -0
- package/dist/src/adapters/anthropic.js +54 -0
- package/dist/src/adapters/index.js +10 -0
- package/dist/src/adapters/openai.js +30 -0
- package/dist/src/adapters/reverse.js +53 -0
- package/dist/src/agents/claude.js +118 -0
- package/dist/src/agents/codex.js +61 -0
- package/{src/agents/index.ts → dist/src/agents/index.js} +6 -8
- package/dist/src/agents/registry.js +24 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/commands/adapt.js +26 -0
- package/dist/src/commands/import.js +51 -0
- package/dist/src/commands/init.js +104 -0
- package/dist/src/commands/install.js +257 -0
- package/dist/src/commands/link.js +34 -0
- package/dist/src/commands/list.js +19 -0
- package/dist/src/commands/marketplace.js +124 -0
- package/dist/src/commands/migrate.js +60 -0
- package/dist/src/commands/multiselect-skills.js +103 -0
- package/dist/src/commands/remove.js +102 -0
- package/dist/src/commands/select-agents.js +40 -0
- package/dist/src/commands/select-components.js +61 -0
- package/dist/src/commands/select-plugins.js +25 -0
- package/dist/src/commands/select-scope.js +20 -0
- package/dist/src/commands/update.js +50 -0
- package/dist/src/commands/validate.js +50 -0
- package/dist/src/components.js +90 -0
- package/dist/src/deps.js +46 -0
- package/dist/src/fsutil.js +32 -0
- package/dist/src/hash.js +51 -0
- package/dist/src/lock.js +51 -0
- package/dist/src/manifest.js +110 -0
- package/dist/src/marketplace.js +39 -0
- package/{src/package.ts → dist/src/package.js} +37 -42
- package/{src/paths.ts → dist/src/paths.js} +54 -60
- package/dist/src/semver.js +55 -0
- package/dist/src/skills.js +79 -0
- package/dist/src/sources.js +122 -0
- package/dist/src/types.js +19 -0
- package/dist/vendor/skills/package.json +143 -0
- package/dist/vendor/skills/src/add.js +1663 -0
- package/dist/vendor/skills/src/agents.js +729 -0
- package/dist/vendor/skills/src/blob.js +436 -0
- package/dist/vendor/skills/src/cli.js +340 -0
- package/dist/vendor/skills/src/constants.js +3 -0
- package/dist/vendor/skills/src/detect-agent.js +56 -0
- package/dist/vendor/skills/src/find.js +294 -0
- package/dist/vendor/skills/src/frontmatter.js +13 -0
- package/dist/vendor/skills/src/git-tree.js +32 -0
- package/dist/vendor/skills/src/git.js +235 -0
- package/dist/vendor/skills/src/install.js +75 -0
- package/dist/vendor/skills/src/installer.js +924 -0
- package/dist/vendor/skills/src/list.js +201 -0
- package/dist/vendor/skills/src/local-lock.js +109 -0
- package/dist/vendor/skills/src/plugin-manifest.js +152 -0
- package/dist/vendor/skills/src/prompts/search-multiselect.js +312 -0
- package/dist/vendor/skills/src/providers/index.js +4 -0
- package/dist/vendor/skills/src/providers/registry.js +42 -0
- package/dist/vendor/skills/src/providers/types.js +1 -0
- package/dist/vendor/skills/src/providers/wellknown.js +625 -0
- package/dist/vendor/skills/src/remove.js +263 -0
- package/dist/vendor/skills/src/sanitize.js +57 -0
- package/dist/vendor/skills/src/self-cli.js +15 -0
- package/dist/vendor/skills/src/skill-lock.js +237 -0
- package/dist/vendor/skills/src/skills.js +264 -0
- package/dist/vendor/skills/src/source-parser.js +367 -0
- package/dist/vendor/skills/src/sync.js +404 -0
- package/dist/vendor/skills/src/telemetry.js +101 -0
- package/dist/vendor/skills/src/test-utils.js +59 -0
- package/dist/vendor/skills/src/types.js +1 -0
- package/dist/vendor/skills/src/update-source.js +76 -0
- package/dist/vendor/skills/src/update.js +590 -0
- package/dist/vendor/skills/src/use.js +505 -0
- package/package.json +15 -7
- package/bin/adg.ts +0 -758
- package/src/adapters/anthropic.ts +0 -54
- package/src/adapters/index.ts +0 -24
- package/src/adapters/openai.ts +0 -37
- package/src/adapters/reverse.ts +0 -60
- package/src/agents/claude.ts +0 -124
- package/src/agents/codex.ts +0 -67
- package/src/agents/registry.ts +0 -30
- package/src/agents/types.ts +0 -47
- package/src/commands/adapt.ts +0 -36
- package/src/commands/import.ts +0 -69
- package/src/commands/init.ts +0 -146
- package/src/commands/install.ts +0 -411
- package/src/commands/link.ts +0 -61
- package/src/commands/list.ts +0 -28
- package/src/commands/marketplace.ts +0 -198
- package/src/commands/migrate.ts +0 -84
- package/src/commands/multiselect-skills.ts +0 -137
- package/src/commands/remove.ts +0 -136
- package/src/commands/select-agents.ts +0 -45
- package/src/commands/select-components.ts +0 -66
- package/src/commands/select-plugins.ts +0 -28
- package/src/commands/select-scope.ts +0 -21
- package/src/commands/update.ts +0 -85
- package/src/commands/validate.ts +0 -57
- package/src/components.ts +0 -90
- package/src/deps.ts +0 -64
- package/src/fsutil.ts +0 -38
- package/src/hash.ts +0 -61
- package/src/lock.ts +0 -57
- package/src/manifest.ts +0 -113
- package/src/marketplace.ts +0 -41
- package/src/semver.ts +0 -67
- package/src/skills.ts +0 -88
- package/src/sources.ts +0 -159
- package/src/types.ts +0 -140
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { parse as parseYaml } from 'yaml';
|
|
2
|
+
/**
|
|
3
|
+
* Minimal frontmatter parser. Only supports YAML (the `---` delimiter).
|
|
4
|
+
* Does NOT support `---js` / `---javascript` to avoid eval()-based RCE
|
|
5
|
+
* that exists in gray-matter's built-in JS engine.
|
|
6
|
+
*/
|
|
7
|
+
export function parseFrontmatter(raw) {
|
|
8
|
+
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
9
|
+
if (!match)
|
|
10
|
+
return { data: {}, content: raw };
|
|
11
|
+
const data = parseYaml(match[1]) ?? {};
|
|
12
|
+
return { data, content: match[2] ?? '' };
|
|
13
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* ADG-added file (see vendor/skills/PROVENANCE.md → Local patches).
|
|
6
|
+
*
|
|
7
|
+
* Return the git *tree object SHA* for a folder inside a freshly cloned repo —
|
|
8
|
+
* the SAME 40-hex value GitHub's Trees API returns and that
|
|
9
|
+
* `getSkillFolderHashFromTree` (blob.ts) compares against at update time.
|
|
10
|
+
*
|
|
11
|
+
* Used by `add.ts` so a github source that fell back to a `git clone` at install
|
|
12
|
+
* still records a tree SHA, not a sha256 content hash. Otherwise the install and
|
|
13
|
+
* update hashing schemes diverge and every update perpetually re-flags the skill
|
|
14
|
+
* (the bug that made a collection repo "fully update" on every run).
|
|
15
|
+
*
|
|
16
|
+
* `folder === ''` means the repo root. Returns null if git can't resolve it.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately uses `child_process` rather than simple-git: this keeps the file
|
|
19
|
+
* free of simple-git's typings, which don't satisfy ADG's strict tsconfig when a
|
|
20
|
+
* test pulls the module into the typecheck graph.
|
|
21
|
+
*/
|
|
22
|
+
export async function gitTreeShaForFolder(repoDir, folder) {
|
|
23
|
+
const spec = folder ? `HEAD:${folder}` : 'HEAD^{tree}';
|
|
24
|
+
try {
|
|
25
|
+
const { stdout } = await execFileAsync('git', ['-C', repoDir, 'rev-parse', spec]);
|
|
26
|
+
const sha = stdout.trim();
|
|
27
|
+
return /^[0-9a-f]{40}$/.test(sha) ? sha : null;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// ADG patch: named import. The default import is not callable under the root
|
|
2
|
+
// tsconfig (NodeNext + verbatimModuleSyntax, no esModuleInterop); simple-git
|
|
3
|
+
// exposes `simpleGit` as a named export. See vendor/skills/PROVENANCE.md.
|
|
4
|
+
import { simpleGit } from 'simple-git';
|
|
5
|
+
import { join, normalize, resolve, sep } from 'path';
|
|
6
|
+
import { mkdtemp, mkdir, rm } from 'fs/promises';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
const DEFAULT_CLONE_TIMEOUT_MS = 300_000; // 5 minutes
|
|
11
|
+
const CLONE_TIMEOUT_MS = (() => {
|
|
12
|
+
const raw = process.env.SKILLS_CLONE_TIMEOUT_MS;
|
|
13
|
+
if (!raw)
|
|
14
|
+
return DEFAULT_CLONE_TIMEOUT_MS;
|
|
15
|
+
const parsed = Number.parseInt(raw, 10);
|
|
16
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CLONE_TIMEOUT_MS;
|
|
17
|
+
})();
|
|
18
|
+
const execFileAsync = promisify(execFile);
|
|
19
|
+
export class GitCloneError extends Error {
|
|
20
|
+
url;
|
|
21
|
+
isTimeout;
|
|
22
|
+
isAuthError;
|
|
23
|
+
constructor(message, url, isTimeout = false, isAuthError = false) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = 'GitCloneError';
|
|
26
|
+
this.url = url;
|
|
27
|
+
this.isTimeout = isTimeout;
|
|
28
|
+
this.isAuthError = isAuthError;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function parseGitHubRepoUrl(url) {
|
|
32
|
+
const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i);
|
|
33
|
+
if (sshMatch) {
|
|
34
|
+
const owner = sshMatch[1];
|
|
35
|
+
const repo = sshMatch[2];
|
|
36
|
+
return {
|
|
37
|
+
owner,
|
|
38
|
+
repo,
|
|
39
|
+
slug: `${owner}/${repo}`,
|
|
40
|
+
sshUrl: `git@github.com:${owner}/${repo}.git`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
if (parsed.hostname !== 'github.com')
|
|
46
|
+
return null;
|
|
47
|
+
const match = parsed.pathname.match(/^\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
|
48
|
+
if (!match)
|
|
49
|
+
return null;
|
|
50
|
+
const owner = match[1];
|
|
51
|
+
const repo = match[2];
|
|
52
|
+
return {
|
|
53
|
+
owner,
|
|
54
|
+
repo,
|
|
55
|
+
slug: `${owner}/${repo}`,
|
|
56
|
+
sshUrl: `git@github.com:${owner}/${repo}.git`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function isGitHubHttpsCloneUrl(url) {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(url);
|
|
66
|
+
return parsed.protocol === 'https:' && parsed.hostname === 'github.com';
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function isGitHubSsoAuthError(message) {
|
|
73
|
+
const lower = message.toLowerCase();
|
|
74
|
+
return (lower.includes('saml sso') ||
|
|
75
|
+
lower.includes('enforced sso') ||
|
|
76
|
+
lower.includes('enabled or enforced saml') ||
|
|
77
|
+
lower.includes('re-authorize the oauth application'));
|
|
78
|
+
}
|
|
79
|
+
function isAuthFailure(message) {
|
|
80
|
+
return (message.includes('Authentication failed') ||
|
|
81
|
+
message.includes('could not read Username') ||
|
|
82
|
+
message.includes('Permission denied') ||
|
|
83
|
+
message.includes('Repository not found') ||
|
|
84
|
+
message.includes('requested URL returned error: 403') ||
|
|
85
|
+
isGitHubSsoAuthError(message));
|
|
86
|
+
}
|
|
87
|
+
function createGitClient(extraEnv) {
|
|
88
|
+
return simpleGit({
|
|
89
|
+
timeout: { block: CLONE_TIMEOUT_MS },
|
|
90
|
+
// When git-lfs is NOT installed, GIT_LFS_SKIP_SMUDGE has no effect —
|
|
91
|
+
// git sees `filter=lfs` in .gitattributes, tries to run
|
|
92
|
+
// `git-lfs filter-process`, and aborts the checkout with:
|
|
93
|
+
// git-lfs filter-process: git-lfs: command not found
|
|
94
|
+
// fatal: the remote end hung up unexpectedly
|
|
95
|
+
// warning: Clone succeeded, but checkout failed.
|
|
96
|
+
// Overriding filter.lfs.* at the command level disables the filter
|
|
97
|
+
// entirely for this clone, so checkout succeeds regardless of whether
|
|
98
|
+
// git-lfs is installed. LFS-tracked files are left as ~130-byte
|
|
99
|
+
// pointer files, which the skills installer doesn't read anyway
|
|
100
|
+
// (skills are plain text — HTML/MD/JSON — never LFS-tracked).
|
|
101
|
+
//
|
|
102
|
+
// Reported downstream: heygen-com/hyperframes#407.
|
|
103
|
+
config: [
|
|
104
|
+
'filter.lfs.required=false',
|
|
105
|
+
'filter.lfs.smudge=',
|
|
106
|
+
'filter.lfs.clean=',
|
|
107
|
+
'filter.lfs.process=',
|
|
108
|
+
],
|
|
109
|
+
// ADG patch: simple-git >=3.36 blocks `filter.*.smudge/clean` configs (an RCE
|
|
110
|
+
// vector via a malicious filter command) unless explicitly allowed, failing
|
|
111
|
+
// every clone with "Configuring filter.smudge is not permitted without
|
|
112
|
+
// enabling allowUnsafeFilter". Here the filters are set to EMPTY — we are
|
|
113
|
+
// *disabling* the LFS filter, not running one — so opting in is safe and is
|
|
114
|
+
// exactly the intent of the config above. See vendor/skills/PROVENANCE.md.
|
|
115
|
+
unsafe: {
|
|
116
|
+
allowUnsafeFilter: true,
|
|
117
|
+
},
|
|
118
|
+
// ADG patch: env must be applied via `.env()`, not as a constructor option.
|
|
119
|
+
// simple-git's factory only reads baseDir/maxConcurrentProcesses/trimmed from
|
|
120
|
+
// the options object and silently drops `env`, so the GIT_TERMINAL_PROMPT /
|
|
121
|
+
// GIT_LFS_SKIP_SMUDGE / GIT_SSH_COMMAND overrides below never reached the
|
|
122
|
+
// spawned git before this. See vendor/skills/PROVENANCE.md.
|
|
123
|
+
}).env({
|
|
124
|
+
...process.env,
|
|
125
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
126
|
+
// When git-lfs IS installed, tell it not to download LFS content during
|
|
127
|
+
// checkout. See #952 for context and empirical impact.
|
|
128
|
+
GIT_LFS_SKIP_SMUDGE: '1',
|
|
129
|
+
...extraEnv,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
async function resetTempDir(dir) {
|
|
133
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
134
|
+
await mkdir(dir, { recursive: true });
|
|
135
|
+
}
|
|
136
|
+
async function tryGhClone(repo, tempDir, ref) {
|
|
137
|
+
let cloneTarget = repo.slug;
|
|
138
|
+
try {
|
|
139
|
+
const { stdout, stderr } = await execFileAsync('gh', ['auth', 'status', '-h', 'github.com'], {
|
|
140
|
+
timeout: 5000,
|
|
141
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
142
|
+
});
|
|
143
|
+
const statusOutput = `${stdout}${stderr}`;
|
|
144
|
+
if (/Git operations protocol:\s+ssh/i.test(statusOutput)) {
|
|
145
|
+
cloneTarget = repo.sshUrl;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const gitFlags = ref ? ['--depth=1', '--branch', ref] : ['--depth=1'];
|
|
152
|
+
await execFileAsync('gh', ['repo', 'clone', cloneTarget, tempDir, '--', ...gitFlags], {
|
|
153
|
+
timeout: CLONE_TIMEOUT_MS,
|
|
154
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
155
|
+
});
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
function buildGitHubAuthError(url, repo, message) {
|
|
159
|
+
if (repo && isGitHubSsoAuthError(message)) {
|
|
160
|
+
return (`GitHub blocked HTTPS access to ${url} because the organization enforces SAML SSO.\n` +
|
|
161
|
+
` skills tried your existing git credentials and available fallbacks, but none succeeded.\n` +
|
|
162
|
+
` - Re-authorize your GitHub credentials/app for that org's SSO policy\n` +
|
|
163
|
+
` - Or rerun with SSH: npx skills add ${repo.sshUrl}\n` +
|
|
164
|
+
` - Verify access with: gh auth status -h github.com or ssh -T git@github.com`);
|
|
165
|
+
}
|
|
166
|
+
if (repo) {
|
|
167
|
+
return (`Authentication failed for ${url}.\n` +
|
|
168
|
+
` - For private repos, ensure you have access\n` +
|
|
169
|
+
` - Retry with SSH: npx skills add ${repo.sshUrl}\n` +
|
|
170
|
+
` - Check access with: gh auth status -h github.com or ssh -T git@github.com`);
|
|
171
|
+
}
|
|
172
|
+
return (`Authentication failed for ${url}.\n` +
|
|
173
|
+
` - For private repos, ensure you have access\n` +
|
|
174
|
+
` - For SSH: Check your keys with 'ssh -T git@github.com'\n` +
|
|
175
|
+
` - For HTTPS: Run 'gh auth login' or configure git credentials`);
|
|
176
|
+
}
|
|
177
|
+
export async function cloneRepo(url, ref) {
|
|
178
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'skills-'));
|
|
179
|
+
const cloneOptions = ref ? ['--depth', '1', '--branch', ref] : ['--depth', '1'];
|
|
180
|
+
const repo = parseGitHubRepoUrl(url);
|
|
181
|
+
try {
|
|
182
|
+
await createGitClient().clone(url, tempDir, cloneOptions);
|
|
183
|
+
return tempDir;
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
187
|
+
const isTimeout = errorMessage.includes('block timeout') || errorMessage.includes('timed out');
|
|
188
|
+
const isAuthError = isAuthFailure(errorMessage);
|
|
189
|
+
if (isTimeout) {
|
|
190
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
191
|
+
const seconds = Math.round(CLONE_TIMEOUT_MS / 1000);
|
|
192
|
+
throw new GitCloneError(`Clone timed out after ${seconds}s. Common causes:\n` +
|
|
193
|
+
` - Large repository: raise the timeout with SKILLS_CLONE_TIMEOUT_MS=600000 (10m)\n` +
|
|
194
|
+
` - Slow network: retry, or clone manually and pass the local path to 'skills add'\n` +
|
|
195
|
+
` - Private repo without credentials: ensure auth is configured\n` +
|
|
196
|
+
` - For SSH: ssh-add -l (to check loaded keys)\n` +
|
|
197
|
+
` - For HTTPS: gh auth status (if using GitHub CLI)`, url, true, false);
|
|
198
|
+
}
|
|
199
|
+
if (isAuthError && repo && isGitHubHttpsCloneUrl(url)) {
|
|
200
|
+
try {
|
|
201
|
+
await resetTempDir(tempDir);
|
|
202
|
+
if (await tryGhClone(repo, tempDir, ref)) {
|
|
203
|
+
return tempDir;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
// Fall through to SSH retry.
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
await resetTempDir(tempDir);
|
|
211
|
+
await createGitClient({
|
|
212
|
+
GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? 'ssh -o BatchMode=yes',
|
|
213
|
+
}).clone(repo.sshUrl, tempDir, cloneOptions);
|
|
214
|
+
return tempDir;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Fall through to the targeted auth error below.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
await rm(tempDir, { recursive: true, force: true }).catch(() => { });
|
|
221
|
+
if (isAuthError) {
|
|
222
|
+
throw new GitCloneError(buildGitHubAuthError(url, repo, errorMessage), url, false, true);
|
|
223
|
+
}
|
|
224
|
+
throw new GitCloneError(`Failed to clone ${url}: ${errorMessage}`, url, false, false);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export async function cleanupTempDir(dir) {
|
|
228
|
+
// Validate that the directory path is within tmpdir to prevent deletion of arbitrary paths
|
|
229
|
+
const normalizedDir = normalize(resolve(dir));
|
|
230
|
+
const normalizedTmpDir = normalize(resolve(tmpdir()));
|
|
231
|
+
if (!normalizedDir.startsWith(normalizedTmpDir + sep) && normalizedDir !== normalizedTmpDir) {
|
|
232
|
+
throw new Error('Attempted to clean up directory outside of temp directory');
|
|
233
|
+
}
|
|
234
|
+
await rm(dir, { recursive: true, force: true });
|
|
235
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { readLocalLock } from "./local-lock.js";
|
|
4
|
+
import { runAdd } from "./add.js";
|
|
5
|
+
import { runSync, parseSyncOptions } from "./sync.js";
|
|
6
|
+
import { getUniversalAgents } from "./agents.js";
|
|
7
|
+
/**
|
|
8
|
+
* Install all skills from the local skills-lock.json.
|
|
9
|
+
* Groups skills by source and calls `runAdd` for each group.
|
|
10
|
+
*
|
|
11
|
+
* Only installs to .agents/skills/ (universal agents) -- the canonical
|
|
12
|
+
* project-level location. Does not install to agent-specific directories.
|
|
13
|
+
*
|
|
14
|
+
* node_modules skills are handled via experimental_sync.
|
|
15
|
+
*/
|
|
16
|
+
export async function runInstallFromLock(args) {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
const lock = await readLocalLock(cwd);
|
|
19
|
+
const skillEntries = Object.entries(lock.skills);
|
|
20
|
+
if (skillEntries.length === 0) {
|
|
21
|
+
p.log.warn('No project skills found in skills-lock.json');
|
|
22
|
+
p.log.info(`Add project-level skills with ${pc.cyan('npx skills add <package>')} (without ${pc.cyan('-g')})`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
// Only install to .agents/skills/ (universal agents)
|
|
26
|
+
const universalAgentNames = getUniversalAgents();
|
|
27
|
+
// Separate node_modules skills from remote skills
|
|
28
|
+
const nodeModuleSkills = [];
|
|
29
|
+
const bySource = new Map();
|
|
30
|
+
for (const [skillName, entry] of skillEntries) {
|
|
31
|
+
if (entry.sourceType === 'node_modules') {
|
|
32
|
+
nodeModuleSkills.push(skillName);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const installSource = entry.ref ? `${entry.source}#${entry.ref}` : entry.source;
|
|
36
|
+
const existing = bySource.get(installSource);
|
|
37
|
+
if (existing) {
|
|
38
|
+
existing.skills.push(skillName);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
bySource.set(installSource, {
|
|
42
|
+
sourceType: entry.sourceType,
|
|
43
|
+
skills: [skillName],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const remoteCount = skillEntries.length - nodeModuleSkills.length;
|
|
48
|
+
if (remoteCount > 0) {
|
|
49
|
+
p.log.info(`Restoring ${pc.cyan(String(remoteCount))} skill${remoteCount !== 1 ? 's' : ''} from skills-lock.json into ${pc.dim('.agents/skills/')}`);
|
|
50
|
+
}
|
|
51
|
+
// Install remote skills grouped by source
|
|
52
|
+
for (const [source, { skills }] of bySource) {
|
|
53
|
+
try {
|
|
54
|
+
await runAdd([source], {
|
|
55
|
+
skill: skills,
|
|
56
|
+
agent: universalAgentNames,
|
|
57
|
+
yes: true,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
p.log.error(`Failed to install from ${pc.cyan(source)}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Handle node_modules skills via sync
|
|
65
|
+
if (nodeModuleSkills.length > 0) {
|
|
66
|
+
p.log.info(`${pc.cyan(String(nodeModuleSkills.length))} skill${nodeModuleSkills.length !== 1 ? 's' : ''} from node_modules`);
|
|
67
|
+
try {
|
|
68
|
+
const { options: syncOptions } = parseSyncOptions(args);
|
|
69
|
+
await runSync(args, { ...syncOptions, yes: true, agent: universalAgentNames });
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
p.log.error(`Failed to sync node_modules skills: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|