@rbbtsn0w/adg 0.1.0-alpha.1

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 (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/bin/adg.ts +758 -0
  4. package/docs/agents-spec.md +132 -0
  5. package/docs/authoring.md +352 -0
  6. package/package.json +50 -0
  7. package/schemas/adg-plugin.schema.json +77 -0
  8. package/schemas/marketplace.schema.json +86 -0
  9. package/schemas/plugin-lock.schema.json +90 -0
  10. package/src/adapters/anthropic.ts +54 -0
  11. package/src/adapters/index.ts +24 -0
  12. package/src/adapters/openai.ts +37 -0
  13. package/src/adapters/reverse.ts +60 -0
  14. package/src/agents/claude.ts +124 -0
  15. package/src/agents/codex.ts +67 -0
  16. package/src/agents/index.ts +12 -0
  17. package/src/agents/registry.ts +30 -0
  18. package/src/agents/types.ts +47 -0
  19. package/src/commands/adapt.ts +36 -0
  20. package/src/commands/import.ts +69 -0
  21. package/src/commands/init.ts +146 -0
  22. package/src/commands/install.ts +411 -0
  23. package/src/commands/link.ts +61 -0
  24. package/src/commands/list.ts +28 -0
  25. package/src/commands/marketplace.ts +198 -0
  26. package/src/commands/migrate.ts +84 -0
  27. package/src/commands/multiselect-skills.ts +137 -0
  28. package/src/commands/remove.ts +136 -0
  29. package/src/commands/select-agents.ts +45 -0
  30. package/src/commands/select-components.ts +66 -0
  31. package/src/commands/select-plugins.ts +28 -0
  32. package/src/commands/select-scope.ts +21 -0
  33. package/src/commands/update.ts +85 -0
  34. package/src/commands/validate.ts +57 -0
  35. package/src/components.ts +90 -0
  36. package/src/deps.ts +64 -0
  37. package/src/fsutil.ts +38 -0
  38. package/src/hash.ts +61 -0
  39. package/src/lock.ts +57 -0
  40. package/src/manifest.ts +113 -0
  41. package/src/marketplace.ts +41 -0
  42. package/src/package.ts +74 -0
  43. package/src/paths.ts +129 -0
  44. package/src/semver.ts +67 -0
  45. package/src/skills.ts +88 -0
  46. package/src/sources.ts +159 -0
  47. package/src/types.ts +140 -0
  48. package/vendor/skills/LICENSE +29 -0
  49. package/vendor/skills/PROVENANCE.md +60 -0
  50. package/vendor/skills/ThirdPartyNoticeText.txt +117 -0
  51. package/vendor/skills/package.json +143 -0
  52. package/vendor/skills/src/add.ts +1999 -0
  53. package/vendor/skills/src/agents.ts +755 -0
  54. package/vendor/skills/src/blob.ts +567 -0
  55. package/vendor/skills/src/cli.ts +387 -0
  56. package/vendor/skills/src/constants.ts +3 -0
  57. package/vendor/skills/src/detect-agent.ts +62 -0
  58. package/vendor/skills/src/find.ts +357 -0
  59. package/vendor/skills/src/frontmatter.ts +16 -0
  60. package/vendor/skills/src/git-tree.ts +36 -0
  61. package/vendor/skills/src/git.ts +277 -0
  62. package/vendor/skills/src/install.ts +91 -0
  63. package/vendor/skills/src/installer.ts +1097 -0
  64. package/vendor/skills/src/list.ts +231 -0
  65. package/vendor/skills/src/local-lock.ts +182 -0
  66. package/vendor/skills/src/plugin-manifest.ts +183 -0
  67. package/vendor/skills/src/prompts/search-multiselect.ts +387 -0
  68. package/vendor/skills/src/providers/index.ts +14 -0
  69. package/vendor/skills/src/providers/registry.ts +51 -0
  70. package/vendor/skills/src/providers/types.ts +97 -0
  71. package/vendor/skills/src/providers/wellknown.ts +804 -0
  72. package/vendor/skills/src/remove.ts +323 -0
  73. package/vendor/skills/src/sanitize.ts +65 -0
  74. package/vendor/skills/src/self-cli.ts +20 -0
  75. package/vendor/skills/src/skill-lock.ts +329 -0
  76. package/vendor/skills/src/skills.ts +316 -0
  77. package/vendor/skills/src/source-parser.ts +438 -0
  78. package/vendor/skills/src/sync.ts +478 -0
  79. package/vendor/skills/src/telemetry.ts +186 -0
  80. package/vendor/skills/src/test-utils.ts +73 -0
  81. package/vendor/skills/src/types.ts +128 -0
  82. package/vendor/skills/src/update-source.ts +90 -0
  83. package/vendor/skills/src/update.ts +749 -0
  84. package/vendor/skills/src/use.ts +675 -0
@@ -0,0 +1,323 @@
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.ts';
6
+ import { track } from './telemetry.ts';
7
+ import { detectAgent } from './detect-agent.ts';
8
+ import { removeSkillFromLock, getSkillFromLock } from './skill-lock.ts';
9
+ import type { AgentType } from './types.ts';
10
+ import {
11
+ getInstallPath,
12
+ getCanonicalPath,
13
+ getCanonicalSkillsDir,
14
+ sanitizeName,
15
+ } from './installer.ts';
16
+
17
+ export interface RemoveOptions {
18
+ global?: boolean;
19
+ agent?: string[];
20
+ yes?: boolean;
21
+ all?: boolean;
22
+ }
23
+
24
+ export async function removeCommand(skillNames: string[], options: RemoveOptions) {
25
+ // Auto-enable non-interactive mode when running inside an AI agent
26
+ const agentResult = await detectAgent();
27
+ if (agentResult.isAgent) {
28
+ options.yes = true;
29
+ p.log.info(
30
+ pc.bgCyan(pc.black(pc.bold(` ${agentResult.agent.name} `))) +
31
+ ' ' +
32
+ 'Agent detected — removing non-interactively'
33
+ );
34
+ }
35
+
36
+ const isGlobal = options.global ?? false;
37
+ const cwd = process.cwd();
38
+
39
+ const spinner = p.spinner();
40
+
41
+ spinner.start('Scanning for installed skills...');
42
+ const skillNamesSet = new Set<string>();
43
+
44
+ const scanDir = async (dir: string) => {
45
+ try {
46
+ const entries = await readdir(dir, { withFileTypes: true });
47
+ for (const entry of entries) {
48
+ if (entry.isDirectory()) {
49
+ skillNamesSet.add(entry.name);
50
+ }
51
+ }
52
+ } catch (err) {
53
+ if (err instanceof Error && (err as { code?: string }).code !== 'ENOENT') {
54
+ p.log.warn(`Could not scan directory ${dir}: ${err.message}`);
55
+ }
56
+ }
57
+ };
58
+
59
+ if (isGlobal) {
60
+ await scanDir(getCanonicalSkillsDir(true, cwd));
61
+ for (const agent of Object.values(agents)) {
62
+ if (agent.globalSkillsDir !== undefined) {
63
+ await scanDir(agent.globalSkillsDir);
64
+ }
65
+ }
66
+ } else {
67
+ await scanDir(getCanonicalSkillsDir(false, cwd));
68
+ for (const agent of Object.values(agents)) {
69
+ await scanDir(join(cwd, agent.skillsDir));
70
+ }
71
+ }
72
+
73
+ const installedSkills = Array.from(skillNamesSet).sort();
74
+ spinner.stop(`Found ${installedSkills.length} unique installed skill(s)`);
75
+
76
+ if (installedSkills.length === 0) {
77
+ p.outro(pc.yellow('No skills found to remove.'));
78
+ return;
79
+ }
80
+
81
+ // Validate agent options BEFORE prompting for skill selection
82
+ if (options.agent && options.agent.length > 0) {
83
+ const validAgents = Object.keys(agents);
84
+ const invalidAgents = options.agent.filter((a) => !validAgents.includes(a));
85
+
86
+ if (invalidAgents.length > 0) {
87
+ p.log.error(`Invalid agents: ${invalidAgents.join(', ')}`);
88
+ p.log.info(`Valid agents: ${validAgents.join(', ')}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ let selectedSkills: string[] = [];
94
+
95
+ if (options.all) {
96
+ selectedSkills = installedSkills;
97
+ } else if (skillNames.length > 0) {
98
+ selectedSkills = installedSkills.filter((s) =>
99
+ skillNames.some((name) => name.toLowerCase() === s.toLowerCase())
100
+ );
101
+
102
+ if (selectedSkills.length === 0) {
103
+ p.log.error(`No matching skills found for: ${skillNames.join(', ')}`);
104
+ return;
105
+ }
106
+ } else {
107
+ const choices = installedSkills.map((s) => ({
108
+ value: s,
109
+ label: s,
110
+ }));
111
+
112
+ const selected = await p.multiselect({
113
+ message: `Select skills to remove ${pc.dim('(space to toggle)')}`,
114
+ options: choices,
115
+ required: true,
116
+ });
117
+
118
+ if (p.isCancel(selected)) {
119
+ p.cancel('Removal cancelled');
120
+ process.exit(0);
121
+ }
122
+
123
+ selectedSkills = selected as string[];
124
+ }
125
+
126
+ let targetAgents: AgentType[];
127
+ if (options.agent && options.agent.length > 0) {
128
+ targetAgents = options.agent as AgentType[];
129
+ } else {
130
+ // When removing, we should target all known agents to ensure
131
+ // ghost symlinks are cleaned up, even if the agent is not detected.
132
+ targetAgents = Object.keys(agents) as AgentType[];
133
+ spinner.stop(`Targeting ${targetAgents.length} potential agent(s)`);
134
+ }
135
+
136
+ if (!options.yes) {
137
+ console.log();
138
+ p.log.info('Skills to remove:');
139
+ for (const skill of selectedSkills) {
140
+ p.log.message(` ${pc.red('•')} ${skill}`);
141
+ }
142
+ console.log();
143
+
144
+ const confirmed = await p.confirm({
145
+ message: `Are you sure you want to uninstall ${selectedSkills.length} skill(s)?`,
146
+ });
147
+
148
+ if (p.isCancel(confirmed) || !confirmed) {
149
+ p.cancel('Removal cancelled');
150
+ process.exit(0);
151
+ }
152
+ }
153
+
154
+ spinner.start('Removing skills...');
155
+
156
+ const results: {
157
+ skill: string;
158
+ success: boolean;
159
+ source?: string;
160
+ sourceType?: string;
161
+ error?: string;
162
+ }[] = [];
163
+
164
+ for (const skillName of selectedSkills) {
165
+ try {
166
+ const canonicalPath = getCanonicalPath(skillName, { global: isGlobal, cwd });
167
+
168
+ for (const agentKey of targetAgents) {
169
+ const agent = agents[agentKey];
170
+ const skillPath = getInstallPath(skillName, agentKey, { global: isGlobal, cwd });
171
+
172
+ // Determine potential paths to cleanup. For universal agents, getInstallPath
173
+ // now returns the canonical path, so we also need to check their 'native'
174
+ // directory to clean up any legacy symlinks.
175
+ const pathsToCleanup = new Set([skillPath]);
176
+ const sanitizedName = sanitizeName(skillName);
177
+ if (isGlobal && agent.globalSkillsDir) {
178
+ pathsToCleanup.add(join(agent.globalSkillsDir, sanitizedName));
179
+ } else {
180
+ pathsToCleanup.add(join(cwd, agent.skillsDir, sanitizedName));
181
+ }
182
+
183
+ for (const pathToCleanup of pathsToCleanup) {
184
+ // Skip if this is the canonical path - we'll handle that after checking all agents
185
+ if (pathToCleanup === canonicalPath) {
186
+ continue;
187
+ }
188
+
189
+ try {
190
+ const stats = await lstat(pathToCleanup).catch(() => null);
191
+ if (stats) {
192
+ await rm(pathToCleanup, { recursive: true, force: true });
193
+ }
194
+ } catch (err) {
195
+ p.log.warn(
196
+ `Could not remove skill from ${agent.displayName}: ${
197
+ err instanceof Error ? err.message : String(err)
198
+ }`
199
+ );
200
+ }
201
+ }
202
+ }
203
+
204
+ // Only remove the canonical path if no other installed agents are using it.
205
+ // This prevents breaking other agents when uninstalling from a specific agent (#287).
206
+ const installedAgents = await detectInstalledAgents();
207
+ const remainingAgents = installedAgents.filter((a) => !targetAgents.includes(a));
208
+
209
+ let isStillUsed = false;
210
+ for (const agentKey of remainingAgents) {
211
+ const path = getInstallPath(skillName, agentKey, { global: isGlobal, cwd });
212
+ const exists = await lstat(path).catch(() => null);
213
+ if (exists) {
214
+ isStillUsed = true;
215
+ break;
216
+ }
217
+ }
218
+
219
+ if (!isStillUsed) {
220
+ await rm(canonicalPath, { recursive: true, force: true });
221
+ }
222
+
223
+ const lockEntry = isGlobal ? await getSkillFromLock(skillName) : null;
224
+ const effectiveSource = lockEntry?.source || 'local';
225
+ const effectiveSourceType = lockEntry?.sourceType || 'local';
226
+
227
+ if (isGlobal) {
228
+ await removeSkillFromLock(skillName);
229
+ }
230
+
231
+ results.push({
232
+ skill: skillName,
233
+ success: true,
234
+ source: effectiveSource,
235
+ sourceType: effectiveSourceType,
236
+ });
237
+ } catch (err) {
238
+ results.push({
239
+ skill: skillName,
240
+ success: false,
241
+ error: err instanceof Error ? err.message : String(err),
242
+ });
243
+ }
244
+ }
245
+
246
+ spinner.stop('Removal process complete');
247
+
248
+ const successful = results.filter((r) => r.success);
249
+ const failed = results.filter((r) => !r.success);
250
+
251
+ // Track removal (grouped by source)
252
+ if (successful.length > 0) {
253
+ const bySource = new Map<string, { skills: string[]; sourceType?: string }>();
254
+
255
+ for (const r of successful) {
256
+ const source = r.source || 'local';
257
+ const existing = bySource.get(source) || { skills: [] };
258
+ existing.skills.push(r.skill);
259
+ existing.sourceType = r.sourceType;
260
+ bySource.set(source, existing);
261
+ }
262
+
263
+ for (const [source, data] of bySource) {
264
+ track({
265
+ event: 'remove',
266
+ source,
267
+ skills: data.skills.join(','),
268
+ agents: targetAgents.join(','),
269
+ ...(isGlobal && { global: '1' }),
270
+ sourceType: data.sourceType,
271
+ });
272
+ }
273
+ }
274
+
275
+ if (successful.length > 0) {
276
+ p.log.success(pc.green(`Successfully removed ${successful.length} skill(s)`));
277
+ }
278
+
279
+ if (failed.length > 0) {
280
+ p.log.error(pc.red(`Failed to remove ${failed.length} skill(s)`));
281
+ for (const r of failed) {
282
+ p.log.message(` ${pc.red('✗')} ${r.skill}: ${r.error}`);
283
+ }
284
+ }
285
+
286
+ console.log();
287
+ p.outro(pc.green('Done!'));
288
+ }
289
+
290
+ /**
291
+ * Parse command line options for the remove command.
292
+ * Separates skill names from options flags.
293
+ */
294
+ export function parseRemoveOptions(args: string[]): { skills: string[]; options: RemoveOptions } {
295
+ const options: RemoveOptions = {};
296
+ const skills: string[] = [];
297
+
298
+ for (let i = 0; i < args.length; i++) {
299
+ const arg = args[i];
300
+
301
+ if (arg === '-g' || arg === '--global') {
302
+ options.global = true;
303
+ } else if (arg === '-y' || arg === '--yes') {
304
+ options.yes = true;
305
+ } else if (arg === '--all') {
306
+ options.all = true;
307
+ } else if (arg === '-a' || arg === '--agent') {
308
+ options.agent = options.agent || [];
309
+ i++;
310
+ let nextArg = args[i];
311
+ while (i < args.length && nextArg && !nextArg.startsWith('-')) {
312
+ options.agent.push(nextArg);
313
+ i++;
314
+ nextArg = args[i];
315
+ }
316
+ i--; // Back up one since the loop will increment
317
+ } else if (arg && !arg.startsWith('-')) {
318
+ skills.push(arg);
319
+ }
320
+ }
321
+
322
+ return { skills, options };
323
+ }
@@ -0,0 +1,65 @@
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
+
18
+ // CSI sequences: ESC[ followed by parameter bytes (0x30-0x3F), intermediate bytes (0x20-0x2F), and a final byte (0x40-0x7E)
19
+ const CSI_RE = /\x1b\[[\x30-\x3f]*[\x20-\x2f]*[\x40-\x7e]/g;
20
+
21
+ // OSC sequences: ESC] ... terminated by BEL (\x07) or ST (ESC\)
22
+ const OSC_RE = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
23
+
24
+ // DCS, PM, APC sequences: ESC P|^|_ ... terminated by ST (ESC\)
25
+ const DCS_PM_APC_RE = /\x1b[P^_][\s\S]*?(?:\x1b\\)/g;
26
+
27
+ // Simple two-byte escape sequences: ESC followed by a single char in 0x20-0x7E range
28
+ // Includes ESC 7 (DECSC), ESC 8 (DECRC), ESC c (RIS), ESC M (RI), etc.
29
+ const SIMPLE_ESC_RE = /\x1b[\x20-\x7e]/g;
30
+
31
+ // C1 control codes (0x80-0x9F) — used as 8-bit equivalents of ESC sequences
32
+ const C1_RE = /[\x80-\x9f]/g;
33
+
34
+ // Raw control characters except tab (\x09) and newline (\x0a)
35
+ // Includes BEL (\x07), BS (\x08), CR (\x0d), and others
36
+ const CONTROL_RE = /[\x00-\x06\x07\x08\x0b\x0c\x0d-\x1a\x1c-\x1f\x7f]/g;
37
+
38
+ /**
39
+ * Strip all terminal escape sequences and dangerous control characters
40
+ * from a string.
41
+ *
42
+ * Safe for use on untrusted input before printing to the terminal.
43
+ */
44
+ export function stripTerminalEscapes(str: string): string {
45
+ return str
46
+ .replace(OSC_RE, '') // OSC first (longest match)
47
+ .replace(DCS_PM_APC_RE, '') // DCS/PM/APC
48
+ .replace(CSI_RE, '') // CSI sequences
49
+ .replace(SIMPLE_ESC_RE, '') // Simple ESC+char
50
+ .replace(C1_RE, '') // C1 control codes
51
+ .replace(CONTROL_RE, ''); // Raw control chars (keep \t \n)
52
+ }
53
+
54
+ /**
55
+ * Sanitize a skill metadata string (name, description, etc.) for safe terminal display.
56
+ *
57
+ * In addition to stripping escape sequences, this also trims whitespace and
58
+ * collapses internal newlines into spaces (skill names/descriptions should
59
+ * be single-line when displayed).
60
+ */
61
+ export function sanitizeMetadata(str: string): string {
62
+ return stripTerminalEscapes(str)
63
+ .replace(/[\r\n]+/g, ' ')
64
+ .trim();
65
+ }
@@ -0,0 +1,20 @@
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
+ /**
8
+ * Args for re-invoking Node on the vendored `cli.ts`. `process.execArgv` is
9
+ * forwarded so the child inherits the parent's Node flags (e.g.
10
+ * --experimental-strip-types, required to run TypeScript directly on Node
11
+ * 22.6–23.5). `execArgv` is a parameter so the forwarding can be tested with a
12
+ * non-empty flag set.
13
+ */
14
+ export function selfCliArgv(
15
+ cliEntry: string,
16
+ args: string[],
17
+ execArgv: string[] = process.execArgv
18
+ ): string[] {
19
+ return [...execArgv, cliEntry, ...args];
20
+ }