@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.
Files changed (110) hide show
  1. package/dist/bin/adg.js +703 -0
  2. package/dist/src/adapters/anthropic.js +54 -0
  3. package/dist/src/adapters/index.js +10 -0
  4. package/dist/src/adapters/openai.js +30 -0
  5. package/dist/src/adapters/reverse.js +53 -0
  6. package/dist/src/agents/claude.js +118 -0
  7. package/dist/src/agents/codex.js +61 -0
  8. package/{src/agents/index.ts → dist/src/agents/index.js} +6 -8
  9. package/dist/src/agents/registry.js +24 -0
  10. package/dist/src/agents/types.js +1 -0
  11. package/dist/src/commands/adapt.js +26 -0
  12. package/dist/src/commands/import.js +51 -0
  13. package/dist/src/commands/init.js +104 -0
  14. package/dist/src/commands/install.js +257 -0
  15. package/dist/src/commands/link.js +34 -0
  16. package/dist/src/commands/list.js +19 -0
  17. package/dist/src/commands/marketplace.js +124 -0
  18. package/dist/src/commands/migrate.js +60 -0
  19. package/dist/src/commands/multiselect-skills.js +103 -0
  20. package/dist/src/commands/remove.js +102 -0
  21. package/dist/src/commands/select-agents.js +40 -0
  22. package/dist/src/commands/select-components.js +61 -0
  23. package/dist/src/commands/select-plugins.js +25 -0
  24. package/dist/src/commands/select-scope.js +20 -0
  25. package/dist/src/commands/update.js +50 -0
  26. package/dist/src/commands/validate.js +50 -0
  27. package/dist/src/components.js +90 -0
  28. package/dist/src/deps.js +46 -0
  29. package/dist/src/fsutil.js +32 -0
  30. package/dist/src/hash.js +51 -0
  31. package/dist/src/lock.js +51 -0
  32. package/dist/src/manifest.js +110 -0
  33. package/dist/src/marketplace.js +39 -0
  34. package/{src/package.ts → dist/src/package.js} +37 -42
  35. package/{src/paths.ts → dist/src/paths.js} +54 -60
  36. package/dist/src/semver.js +55 -0
  37. package/dist/src/skills.js +79 -0
  38. package/dist/src/sources.js +122 -0
  39. package/dist/src/types.js +19 -0
  40. package/dist/vendor/skills/package.json +143 -0
  41. package/dist/vendor/skills/src/add.js +1663 -0
  42. package/dist/vendor/skills/src/agents.js +729 -0
  43. package/dist/vendor/skills/src/blob.js +436 -0
  44. package/dist/vendor/skills/src/cli.js +340 -0
  45. package/dist/vendor/skills/src/constants.js +3 -0
  46. package/dist/vendor/skills/src/detect-agent.js +56 -0
  47. package/dist/vendor/skills/src/find.js +294 -0
  48. package/dist/vendor/skills/src/frontmatter.js +13 -0
  49. package/dist/vendor/skills/src/git-tree.js +32 -0
  50. package/dist/vendor/skills/src/git.js +235 -0
  51. package/dist/vendor/skills/src/install.js +75 -0
  52. package/dist/vendor/skills/src/installer.js +924 -0
  53. package/dist/vendor/skills/src/list.js +201 -0
  54. package/dist/vendor/skills/src/local-lock.js +109 -0
  55. package/dist/vendor/skills/src/plugin-manifest.js +152 -0
  56. package/dist/vendor/skills/src/prompts/search-multiselect.js +312 -0
  57. package/dist/vendor/skills/src/providers/index.js +4 -0
  58. package/dist/vendor/skills/src/providers/registry.js +42 -0
  59. package/dist/vendor/skills/src/providers/types.js +1 -0
  60. package/dist/vendor/skills/src/providers/wellknown.js +625 -0
  61. package/dist/vendor/skills/src/remove.js +263 -0
  62. package/dist/vendor/skills/src/sanitize.js +57 -0
  63. package/dist/vendor/skills/src/self-cli.js +15 -0
  64. package/dist/vendor/skills/src/skill-lock.js +237 -0
  65. package/dist/vendor/skills/src/skills.js +264 -0
  66. package/dist/vendor/skills/src/source-parser.js +367 -0
  67. package/dist/vendor/skills/src/sync.js +404 -0
  68. package/dist/vendor/skills/src/telemetry.js +101 -0
  69. package/dist/vendor/skills/src/test-utils.js +59 -0
  70. package/dist/vendor/skills/src/types.js +1 -0
  71. package/dist/vendor/skills/src/update-source.js +76 -0
  72. package/dist/vendor/skills/src/update.js +590 -0
  73. package/dist/vendor/skills/src/use.js +505 -0
  74. package/package.json +15 -7
  75. package/bin/adg.ts +0 -758
  76. package/src/adapters/anthropic.ts +0 -54
  77. package/src/adapters/index.ts +0 -24
  78. package/src/adapters/openai.ts +0 -37
  79. package/src/adapters/reverse.ts +0 -60
  80. package/src/agents/claude.ts +0 -124
  81. package/src/agents/codex.ts +0 -67
  82. package/src/agents/registry.ts +0 -30
  83. package/src/agents/types.ts +0 -47
  84. package/src/commands/adapt.ts +0 -36
  85. package/src/commands/import.ts +0 -69
  86. package/src/commands/init.ts +0 -146
  87. package/src/commands/install.ts +0 -411
  88. package/src/commands/link.ts +0 -61
  89. package/src/commands/list.ts +0 -28
  90. package/src/commands/marketplace.ts +0 -198
  91. package/src/commands/migrate.ts +0 -84
  92. package/src/commands/multiselect-skills.ts +0 -137
  93. package/src/commands/remove.ts +0 -136
  94. package/src/commands/select-agents.ts +0 -45
  95. package/src/commands/select-components.ts +0 -66
  96. package/src/commands/select-plugins.ts +0 -28
  97. package/src/commands/select-scope.ts +0 -21
  98. package/src/commands/update.ts +0 -85
  99. package/src/commands/validate.ts +0 -57
  100. package/src/components.ts +0 -90
  101. package/src/deps.ts +0 -64
  102. package/src/fsutil.ts +0 -38
  103. package/src/hash.ts +0 -61
  104. package/src/lock.ts +0 -57
  105. package/src/manifest.ts +0 -113
  106. package/src/marketplace.ts +0 -41
  107. package/src/semver.ts +0 -67
  108. package/src/skills.ts +0 -88
  109. package/src/sources.ts +0 -159
  110. package/src/types.ts +0 -140
@@ -0,0 +1,263 @@
1
+ import * as p from '@clack/prompts';
2
+ import pc from 'picocolors';
3
+ import { readdir, rm, lstat } from 'fs/promises';
4
+ import { join } from 'path';
5
+ import { agents, detectInstalledAgents } from "./agents.js";
6
+ import { track } from "./telemetry.js";
7
+ import { detectAgent } from "./detect-agent.js";
8
+ import { removeSkillFromLock, getSkillFromLock } from "./skill-lock.js";
9
+ import { getInstallPath, getCanonicalPath, getCanonicalSkillsDir, sanitizeName, } from "./installer.js";
10
+ export async function removeCommand(skillNames, options) {
11
+ // Auto-enable non-interactive mode when running inside an AI agent
12
+ const agentResult = await detectAgent();
13
+ if (agentResult.isAgent) {
14
+ options.yes = true;
15
+ p.log.info(pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
16
+ ' ' +
17
+ 'Agent detected — removing non-interactively');
18
+ }
19
+ const isGlobal = options.global ?? false;
20
+ const cwd = process.cwd();
21
+ const spinner = p.spinner();
22
+ spinner.start('Scanning for installed skills...');
23
+ const skillNamesSet = new Set();
24
+ const scanDir = async (dir) => {
25
+ try {
26
+ const entries = await readdir(dir, { withFileTypes: true });
27
+ for (const entry of entries) {
28
+ if (entry.isDirectory()) {
29
+ skillNamesSet.add(entry.name);
30
+ }
31
+ }
32
+ }
33
+ catch (err) {
34
+ if (err instanceof Error && err.code !== 'ENOENT') {
35
+ p.log.warn(`Could not scan directory ${dir}: ${err.message}`);
36
+ }
37
+ }
38
+ };
39
+ if (isGlobal) {
40
+ await scanDir(getCanonicalSkillsDir(true, cwd));
41
+ for (const agent of Object.values(agents)) {
42
+ if (agent.globalSkillsDir !== undefined) {
43
+ await scanDir(agent.globalSkillsDir);
44
+ }
45
+ }
46
+ }
47
+ else {
48
+ await scanDir(getCanonicalSkillsDir(false, cwd));
49
+ for (const agent of Object.values(agents)) {
50
+ await scanDir(join(cwd, agent.skillsDir));
51
+ }
52
+ }
53
+ const installedSkills = Array.from(skillNamesSet).sort();
54
+ spinner.stop(`Found ${installedSkills.length} unique installed skill(s)`);
55
+ if (installedSkills.length === 0) {
56
+ p.outro(pc.yellow('No skills found to remove.'));
57
+ return;
58
+ }
59
+ // Validate agent options BEFORE prompting for skill selection
60
+ if (options.agent && options.agent.length > 0) {
61
+ const validAgents = Object.keys(agents);
62
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
63
+ if (invalidAgents.length > 0) {
64
+ p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
65
+ p.log.info(`Valid agents: ${validAgents.join(', ')}`);
66
+ process.exit(1);
67
+ }
68
+ }
69
+ let selectedSkills = [];
70
+ if (options.all) {
71
+ selectedSkills = installedSkills;
72
+ }
73
+ else if (skillNames.length > 0) {
74
+ selectedSkills = installedSkills.filter((s) => skillNames.some((name) => name.toLowerCase() === s.toLowerCase()));
75
+ if (selectedSkills.length === 0) {
76
+ p.log.error(`No matching skills found for: ${skillNames.join(', ')}`);
77
+ return;
78
+ }
79
+ }
80
+ else {
81
+ const choices = installedSkills.map((s) => ({
82
+ value: s,
83
+ label: s,
84
+ }));
85
+ const selected = await p.multiselect({
86
+ message: `Select skills to remove ${pc.dim('(space to toggle)')}`,
87
+ options: choices,
88
+ required: true,
89
+ });
90
+ if (p.isCancel(selected)) {
91
+ p.cancel('Removal cancelled');
92
+ process.exit(0);
93
+ }
94
+ selectedSkills = selected;
95
+ }
96
+ let targetAgents;
97
+ if (options.agent && options.agent.length > 0) {
98
+ targetAgents = options.agent;
99
+ }
100
+ else {
101
+ // When removing, we should target all known agents to ensure
102
+ // ghost symlinks are cleaned up, even if the agent is not detected.
103
+ targetAgents = Object.keys(agents);
104
+ spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
105
+ }
106
+ if (!options.yes) {
107
+ console.log();
108
+ p.log.info('Skills to remove:');
109
+ for (const skill of selectedSkills) {
110
+ p.log.message(` ${pc.red('•')} ${skill}`);
111
+ }
112
+ console.log();
113
+ const confirmed = await p.confirm({
114
+ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?`,
115
+ });
116
+ if (p.isCancel(confirmed) || !confirmed) {
117
+ p.cancel('Removal cancelled');
118
+ process.exit(0);
119
+ }
120
+ }
121
+ spinner.start('Removing skills...');
122
+ const results = [];
123
+ for (const skillName of selectedSkills) {
124
+ try {
125
+ const canonicalPath = getCanonicalPath(skillName, { global: isGlobal, cwd });
126
+ for (const agentKey of targetAgents) {
127
+ const agent = agents[agentKey];
128
+ const skillPath = getInstallPath(skillName, agentKey, { global: isGlobal, cwd });
129
+ // Determine potential paths to cleanup. For universal agents, getInstallPath
130
+ // now returns the canonical path, so we also need to check their 'native'
131
+ // directory to clean up any legacy symlinks.
132
+ const pathsToCleanup = new Set([skillPath]);
133
+ const sanitizedName = sanitizeName(skillName);
134
+ if (isGlobal && agent.globalSkillsDir) {
135
+ pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName));
136
+ }
137
+ else {
138
+ pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName));
139
+ }
140
+ for (const pathToCleanup of pathsToCleanup) {
141
+ // Skip if this is the canonical path - we'll handle that after checking all agents
142
+ if (pathToCleanup === canonicalPath) {
143
+ continue;
144
+ }
145
+ try {
146
+ const stats = await lstat(pathToCleanup).catch(() => null);
147
+ if (stats) {
148
+ await rm(pathToCleanup, { recursive: true, force: true });
149
+ }
150
+ }
151
+ catch (err) {
152
+ p.log.warn(`Could not remove skill from ${agent.displayName}: ${err instanceof Error ? err.message : String(err)}`);
153
+ }
154
+ }
155
+ }
156
+ // Only remove the canonical path if no other installed agents are using it.
157
+ // This prevents breaking other agents when uninstalling from a specific agent (#287).
158
+ const installedAgents = await detectInstalledAgents();
159
+ const remainingAgents = installedAgents.filter((a) => !targetAgents.includes(a));
160
+ let isStillUsed = false;
161
+ for (const agentKey of remainingAgents) {
162
+ const path = getInstallPath(skillName, agentKey, { global: isGlobal, cwd });
163
+ const exists = await lstat(path).catch(() => null);
164
+ if (exists) {
165
+ isStillUsed = true;
166
+ break;
167
+ }
168
+ }
169
+ if (!isStillUsed) {
170
+ await rm(canonicalPath, { recursive: true, force: true });
171
+ }
172
+ const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
173
+ const effectiveSource = lockEntry?.source || 'local';
174
+ const effectiveSourceType = lockEntry?.sourceType || 'local';
175
+ if (isGlobal) {
176
+ await removeSkillFromLock(skillName);
177
+ }
178
+ results.push({
179
+ skill: skillName,
180
+ success: true,
181
+ source: effectiveSource,
182
+ sourceType: effectiveSourceType,
183
+ });
184
+ }
185
+ catch (err) {
186
+ results.push({
187
+ skill: skillName,
188
+ success: false,
189
+ error: err instanceof Error ? err.message : String(err),
190
+ });
191
+ }
192
+ }
193
+ spinner.stop('Removal process complete');
194
+ const successful = results.filter((r) => r.success);
195
+ const failed = results.filter((r) => !r.success);
196
+ // Track removal (grouped by source)
197
+ if (successful.length > 0) {
198
+ const bySource = new Map();
199
+ for (const r of successful) {
200
+ const source = r.source || 'local';
201
+ const existing = bySource.get(source) || { skills: [] };
202
+ existing.skills.push(r.skill);
203
+ existing.sourceType = r.sourceType;
204
+ bySource.set(source, existing);
205
+ }
206
+ for (const [source, data] of bySource) {
207
+ track({
208
+ event: 'remove',
209
+ source,
210
+ skills: data.skills.join(','),
211
+ agents: targetAgents.join(','),
212
+ ...(isGlobal && { global: '1' }),
213
+ sourceType: data.sourceType,
214
+ });
215
+ }
216
+ }
217
+ if (successful.length > 0) {
218
+ p.log.success(pc.green(`Successfully removed ${successful.length} skill(s)`));
219
+ }
220
+ if (failed.length > 0) {
221
+ p.log.error(pc.red(`Failed to remove ${failed.length} skill(s)`));
222
+ for (const r of failed) {
223
+ p.log.message(` ${pc.red('✗')} ${r.skill}: ${r.error}`);
224
+ }
225
+ }
226
+ console.log();
227
+ p.outro(pc.green('Done!'));
228
+ }
229
+ /**
230
+ * Parse command line options for the remove command.
231
+ * Separates skill names from options flags.
232
+ */
233
+ export function parseRemoveOptions(args) {
234
+ const options = {};
235
+ const skills = [];
236
+ for (let i = 0; i < args.length; i++) {
237
+ const arg = args[i];
238
+ if (arg === '-g' || arg === '--global') {
239
+ options.global = true;
240
+ }
241
+ else if (arg === '-y' || arg === '--yes') {
242
+ options.yes = true;
243
+ }
244
+ else if (arg === '--all') {
245
+ options.all = true;
246
+ }
247
+ else if (arg === '-a' || arg === '--agent') {
248
+ options.agent = options.agent || [];
249
+ i++;
250
+ let nextArg = args[i];
251
+ while (i < args.length && nextArg && !nextArg.startsWith('-')) {
252
+ options.agent.push(nextArg);
253
+ i++;
254
+ nextArg = args[i];
255
+ }
256
+ i--; // Back up one since the loop will increment
257
+ }
258
+ else if (arg && !arg.startsWith('-')) {
259
+ skills.push(arg);
260
+ }
261
+ }
262
+ return { skills, options };
263
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Sanitize untrusted strings before terminal output.
3
+ *
4
+ * Strips ALL terminal escape sequences from a string, including:
5
+ * - CSI sequences (ESC [ ... final_byte) — cursor movement, screen clear, SGR colors
6
+ * - OSC sequences (ESC ] ... BEL/ST) — window title, hyperlinks
7
+ * - Simple escapes (ESC followed by one char) — e.g. ESC 7 (save cursor)
8
+ * - C1 control codes (0x80–0x9F)
9
+ * - Raw control characters (BEL, BS, etc.) — except \t and \n which are safe
10
+ *
11
+ * This defends against CWE-150 (terminal escape injection) where
12
+ * untrusted data (e.g., skill name/description from SKILL.md frontmatter
13
+ * or remote APIs) could clear the screen, move the cursor, change the
14
+ * window title, or render attacker-controlled text that looks like
15
+ * legitimate CLI output.
16
+ */
17
+ // CSI sequences: ESC[ followed by parameter bytes (0x30-0x3F), intermediate bytes (0x20-0x2F), and a final byte (0x40-0x7E)
18
+ const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
19
+ // OSC sequences: ESC] ... terminated by BEL (\x07) or ST (ESC\)
20
+ const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
21
+ // DCS, PM, APC sequences: ESC P|^|_ ... terminated by ST (ESC\)
22
+ const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
23
+ // Simple two-byte escape sequences: ESC followed by a single char in 0x20-0x7E range
24
+ // Includes ESC 7 (DECSC), ESC 8 (DECRC), ESC c (RIS), ESC M (RI), etc.
25
+ const SIMPLE_ESC_RE = /\x1b[\x20-\x7e]/g;
26
+ // C1 control codes (0x80-0x9F) — used as 8-bit equivalents of ESC sequences
27
+ const C1_RE = /[\x80-\x9f]/g;
28
+ // Raw control characters except tab (\x09) and newline (\x0a)
29
+ // Includes BEL (\x07), BS (\x08), CR (\x0d), and others
30
+ const CONTROL_RE = /[\x00-\x06\x07\x08\x0b\x0c\x0d-\x1a\x1c-\x1f\x7f]/g;
31
+ /**
32
+ * Strip all terminal escape sequences and dangerous control characters
33
+ * from a string.
34
+ *
35
+ * Safe for use on untrusted input before printing to the terminal.
36
+ */
37
+ export function stripTerminalEscapes(str) {
38
+ return str
39
+ .replace(OSC_RE, '') // OSC first (longest match)
40
+ .replace(DCS_PM_APC_RE, '') // DCS/PM/APC
41
+ .replace(CSI_RE, '') // CSI sequences
42
+ .replace(SIMPLE_ESC_RE, '') // Simple ESC+char
43
+ .replace(C1_RE, '') // C1 control codes
44
+ .replace(CONTROL_RE, ''); // Raw control chars (keep \t \n)
45
+ }
46
+ /**
47
+ * Sanitize a skill metadata string (name, description, etc.) for safe terminal display.
48
+ *
49
+ * In addition to stripping escape sequences, this also trims whitespace and
50
+ * collapses internal newlines into spaces (skill names/descriptions should
51
+ * be single-line when displayed).
52
+ */
53
+ export function sanitizeMetadata(str) {
54
+ return stripTerminalEscapes(str)
55
+ .replace(/[\r\n]+/g, ' ')
56
+ .trim();
57
+ }
@@ -0,0 +1,15 @@
1
+ // ADG patch (new file): standalone helper for re-invoking the vendored CLI on
2
+ // `cli.ts`. Kept dependency-free (no heavy transitive imports) so a test can
3
+ // import it without pulling update.ts's graph (which currently includes the
4
+ // detect-agent.ts ↔ @vercel/detect-agent API mismatch). Mirrors the rationale
5
+ // for git-tree.ts. See vendor/skills/PROVENANCE.md.
6
+ /**
7
+ * Args for re-invoking Node on the vendored `cli.ts`. `process.execArgv` is
8
+ * forwarded so the child inherits the parent's Node flags (e.g.
9
+ * --experimental-strip-types, required to run TypeScript directly on Node
10
+ * 22.6–23.5). `execArgv` is a parameter so the forwarding can be tested with a
11
+ * non-empty flag set.
12
+ */
13
+ export function selfCliArgv(cliEntry, args, execArgv = process.execArgv) {
14
+ return [...execArgv, cliEntry, ...args];
15
+ }
@@ -0,0 +1,237 @@
1
+ import { readFile, writeFile, mkdir } from 'fs/promises';
2
+ import { join, dirname } from 'path';
3
+ import { homedir } from 'os';
4
+ import { createHash } from 'crypto';
5
+ import { execSync } from 'child_process';
6
+ import pc from 'picocolors';
7
+ const AGENTS_DIR = '.agents';
8
+ const LOCK_FILE = '.skill-lock.json';
9
+ const CURRENT_VERSION = 3; // Bumped from 2 to 3 for folder hash support (GitHub tree SHA)
10
+ /**
11
+ * Get the path to the global skill lock file.
12
+ *
13
+ * ADG patch: anchor the lock under `.agents/` in BOTH modes so the skills and
14
+ * plugins domains always share one universal `~/.agents` (or
15
+ * `$XDG_STATE_HOME/.agents`) home — matching ADG's `globalPluginsDir()`.
16
+ * Upstream put the XDG variant at `$XDG_STATE_HOME/skills/`, which broke that
17
+ * shared root when XDG_STATE_HOME was set. See vendor/skills/PROVENANCE.md.
18
+ *
19
+ * Resolves to `$XDG_STATE_HOME/.agents/.skill-lock.json` if set,
20
+ * otherwise `~/.agents/.skill-lock.json`.
21
+ */
22
+ export function getSkillLockPath() {
23
+ const root = process.env.XDG_STATE_HOME ?? homedir();
24
+ return join(root, AGENTS_DIR, LOCK_FILE);
25
+ }
26
+ /**
27
+ * Read the skill lock file.
28
+ * Returns an empty lock file structure if the file doesn't exist.
29
+ * Wipes the lock file if it's an old format (version < CURRENT_VERSION).
30
+ */
31
+ export async function readSkillLock() {
32
+ const lockPath = getSkillLockPath();
33
+ try {
34
+ const content = await readFile(lockPath, 'utf-8');
35
+ const parsed = JSON.parse(content);
36
+ // Validate version - wipe if old format
37
+ if (typeof parsed.version !== 'number' || !parsed.skills) {
38
+ return createEmptyLockFile();
39
+ }
40
+ // If old version, wipe and start fresh (backwards incompatible change)
41
+ // v3 adds skillFolderHash - we want fresh installs to populate it
42
+ if (parsed.version < CURRENT_VERSION) {
43
+ return createEmptyLockFile();
44
+ }
45
+ return parsed;
46
+ }
47
+ catch (error) {
48
+ // File doesn't exist or is invalid - return empty
49
+ return createEmptyLockFile();
50
+ }
51
+ }
52
+ /**
53
+ * Write the skill lock file.
54
+ * Creates the directory if it doesn't exist.
55
+ */
56
+ export async function writeSkillLock(lock) {
57
+ const lockPath = getSkillLockPath();
58
+ // Ensure directory exists
59
+ await mkdir(dirname(lockPath), { recursive: true });
60
+ // Write with pretty formatting for human readability
61
+ const content = JSON.stringify(lock, null, 2);
62
+ await writeFile(lockPath, content, 'utf-8');
63
+ }
64
+ /**
65
+ * Compute SHA-256 hash of content.
66
+ */
67
+ export function computeContentHash(content) {
68
+ return createHash('sha256').update(content, 'utf-8').digest('hex');
69
+ }
70
+ let _ghWarningShown = false;
71
+ /** For tests only. Resets the one-shot warning flag. */
72
+ export function resetGhAuthWarning() {
73
+ _ghWarningShown = false;
74
+ }
75
+ /**
76
+ * Get GitHub token from user's environment.
77
+ * Tries in order:
78
+ * 1. GITHUB_TOKEN environment variable (silent)
79
+ * 2. GH_TOKEN environment variable (silent)
80
+ * 3. gh CLI auth token, if gh is installed. Prints a one-time warning to
81
+ * stderr before invoking `gh auth token`, because that subprocess call
82
+ * is flagged by some corporate endpoint security tooling (Defender, etc.)
83
+ * as credential extraction. Callers should invoke this function lazily
84
+ * (e.g. only after an unauthenticated request hits a rate limit) so the
85
+ * fallback rarely runs in practice.
86
+ *
87
+ * @returns The token string or null if not available
88
+ */
89
+ export function getGitHubToken() {
90
+ // Check environment variables first (silent: user has explicitly opted in)
91
+ if (process.env.GITHUB_TOKEN) {
92
+ return process.env.GITHUB_TOKEN;
93
+ }
94
+ if (process.env.GH_TOKEN) {
95
+ return process.env.GH_TOKEN;
96
+ }
97
+ // Last resort: spawn gh CLI. Warn the user once per process before doing so.
98
+ // ADG patch: message broadened — this resolver is now also used to reach
99
+ // private repos, not just to recover from a rate limit.
100
+ if (!_ghWarningShown) {
101
+ process.stderr.write(`${pc.yellow('│')} ${pc.yellow('GitHub authentication needed')} — using your ${pc.cyan('gh')} login to continue.\n` +
102
+ `${pc.yellow('│')} ${pc.dim(`Tip: set ${pc.cyan('GITHUB_TOKEN')} to avoid this prompt, or use ${pc.cyan('--full-depth')} to clone instead.\n`)}`);
103
+ _ghWarningShown = true;
104
+ }
105
+ try {
106
+ const token = execSync('gh auth token', {
107
+ encoding: 'utf-8',
108
+ stdio: ['pipe', 'pipe', 'pipe'],
109
+ }).trim();
110
+ if (token) {
111
+ return token;
112
+ }
113
+ }
114
+ catch {
115
+ // gh not installed or not authenticated
116
+ }
117
+ return null;
118
+ }
119
+ /**
120
+ * Fetch the tree SHA (folder hash) for a skill folder using GitHub's Trees API.
121
+ * This makes ONE API call to get the entire repo tree, then extracts the SHA
122
+ * for the specific skill folder.
123
+ *
124
+ * @param ownerRepo - GitHub owner/repo (e.g., "vercel-labs/agent-skills")
125
+ * @param skillPath - Path to skill folder or SKILL.md (e.g., "skills/react-best-practices/SKILL.md")
126
+ * @param getToken - Optional lazy token resolver. Invoked only if the
127
+ * unauthenticated request hits a rate limit.
128
+ * @param ref - Optional branch/tag ref. Defaults to trying main then master.
129
+ * @returns The tree SHA for the skill folder, or null if not found
130
+ */
131
+ export async function fetchSkillFolderHash(ownerRepo, skillPath, getToken, ref) {
132
+ const { fetchRepoTree, getSkillFolderHashFromTree } = await import("./blob.js");
133
+ const tree = await fetchRepoTree(ownerRepo, ref, getToken ?? undefined);
134
+ if (!tree)
135
+ return null;
136
+ return getSkillFolderHashFromTree(tree, skillPath);
137
+ }
138
+ /**
139
+ * Add or update a skill entry in the lock file.
140
+ */
141
+ export async function addSkillToLock(skillName, entry) {
142
+ const lock = await readSkillLock();
143
+ const now = new Date().toISOString();
144
+ const existingEntry = lock.skills[skillName];
145
+ lock.skills[skillName] = {
146
+ ...entry,
147
+ installedAt: existingEntry?.installedAt ?? now,
148
+ updatedAt: now,
149
+ };
150
+ await writeSkillLock(lock);
151
+ }
152
+ /**
153
+ * Remove a skill from the lock file.
154
+ */
155
+ export async function removeSkillFromLock(skillName) {
156
+ const lock = await readSkillLock();
157
+ if (!(skillName in lock.skills)) {
158
+ return false;
159
+ }
160
+ delete lock.skills[skillName];
161
+ await writeSkillLock(lock);
162
+ return true;
163
+ }
164
+ /**
165
+ * Get a skill entry from the lock file.
166
+ */
167
+ export async function getSkillFromLock(skillName) {
168
+ const lock = await readSkillLock();
169
+ return lock.skills[skillName] ?? null;
170
+ }
171
+ /**
172
+ * Get all skills from the lock file.
173
+ */
174
+ export async function getAllLockedSkills() {
175
+ const lock = await readSkillLock();
176
+ return lock.skills;
177
+ }
178
+ /**
179
+ * Get skills grouped by source for batch update operations.
180
+ */
181
+ export async function getSkillsBySource() {
182
+ const lock = await readSkillLock();
183
+ const bySource = new Map();
184
+ for (const [skillName, entry] of Object.entries(lock.skills)) {
185
+ const existing = bySource.get(entry.source);
186
+ if (existing) {
187
+ existing.skills.push(skillName);
188
+ }
189
+ else {
190
+ bySource.set(entry.source, { skills: [skillName], entry });
191
+ }
192
+ }
193
+ return bySource;
194
+ }
195
+ /**
196
+ * Create an empty lock file structure.
197
+ */
198
+ function createEmptyLockFile() {
199
+ return {
200
+ version: CURRENT_VERSION,
201
+ skills: {},
202
+ dismissed: {},
203
+ };
204
+ }
205
+ /**
206
+ * Check if a prompt has been dismissed.
207
+ */
208
+ export async function isPromptDismissed(promptKey) {
209
+ const lock = await readSkillLock();
210
+ return lock.dismissed?.[promptKey] === true;
211
+ }
212
+ /**
213
+ * Mark a prompt as dismissed.
214
+ */
215
+ export async function dismissPrompt(promptKey) {
216
+ const lock = await readSkillLock();
217
+ if (!lock.dismissed) {
218
+ lock.dismissed = {};
219
+ }
220
+ lock.dismissed[promptKey] = true;
221
+ await writeSkillLock(lock);
222
+ }
223
+ /**
224
+ * Get the last selected agents.
225
+ */
226
+ export async function getLastSelectedAgents() {
227
+ const lock = await readSkillLock();
228
+ return lock.lastSelectedAgents;
229
+ }
230
+ /**
231
+ * Save the selected agents to the lock file.
232
+ */
233
+ export async function saveSelectedAgents(agents) {
234
+ const lock = await readSkillLock();
235
+ lock.lastSelectedAgents = agents;
236
+ await writeSkillLock(lock);
237
+ }