@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
@@ -1,198 +0,0 @@
1
- import type { AdapterTarget } from "../adapters/index.ts";
2
- import type { GitRunner } from "../sources.ts";
3
- import type { PluginSource } from "../types.ts";
4
- import { lockPath } from "../paths.ts";
5
- import { readLock } from "../lock.ts";
6
- import { addPlugins, type InstallResult } from "./install.ts";
7
- import { removePlugin } from "./remove.ts";
8
- import type { AgentScope } from "../agents/index.ts";
9
-
10
- /**
11
- * The marketplace layer is a *view* over installed plugins grouped by their
12
- * source, not a separate registry. Every plugin records where it came from in
13
- * `lock.origin`; a "marketplace" is just the set of plugins sharing a source.
14
- */
15
-
16
- /** Active-scope context, used to make "source not found" errors actionable. */
17
- export interface ScopeInfo {
18
- /** Human label for the active scope: "project", "global", or an explicit dir. */
19
- label: string;
20
- /** The global plugins dir, so we can suggest `-g` when a source is only there. */
21
- globalDir?: string;
22
- }
23
-
24
- export interface MarketplaceScope {
25
- pluginsDir: string;
26
- now?: string;
27
- /** Where the active pluginsDir came from, for scope-aware error messages. */
28
- scope?: ScopeInfo;
29
- }
30
-
31
- /**
32
- * A stable key identifying the source a plugin came from. GitHub/git plugins
33
- * group by repo/url (the marketplace root, ignoring per-plugin sub-paths);
34
- * local installs share a single "(local)" bucket since their original source
35
- * directory isn't recoverable for re-sync.
36
- */
37
- export function sourceKey(origin: PluginSource): string {
38
- switch (origin.type) {
39
- case "github":
40
- return origin.repo;
41
- case "git":
42
- return origin.url;
43
- case "local":
44
- return "(local)";
45
- }
46
- }
47
-
48
- /** True for sources that can be re-fetched (have a recoverable remote spec). */
49
- function isRemoteKey(key: string): boolean {
50
- return key !== "(local)";
51
- }
52
-
53
- export interface MarketplaceGroup {
54
- /** Source key (owner/repo, git url, or "(local)"). */
55
- source: string;
56
- /** ref shared by the group's plugins, if any. */
57
- ref?: string;
58
- /** Installed plugin names from this source. */
59
- installed: string[];
60
- /** Whether this source can be re-synced (remote). */
61
- remote: boolean;
62
- }
63
-
64
- /** Group installed plugins by source, read straight from the lock. */
65
- export function marketplaceList(opts: { pluginsDir: string }): MarketplaceGroup[] {
66
- const lock = readLock(lockPath(opts.pluginsDir));
67
- const groups = new Map<string, { ref?: string; installed: string[] }>();
68
-
69
- for (const [name, entry] of Object.entries(lock.plugins)) {
70
- const key = sourceKey(entry.origin);
71
- const ref = entry.origin.type === "github" || entry.origin.type === "git" ? entry.origin.ref : undefined;
72
- const g = groups.get(key) ?? { ref, installed: [] };
73
- g.installed.push(name);
74
- if (g.ref === undefined && ref !== undefined) g.ref = ref;
75
- groups.set(key, g);
76
- }
77
-
78
- return [...groups.entries()]
79
- .map(([source, g]) => ({
80
- source,
81
- ...(g.ref ? { ref: g.ref } : {}),
82
- installed: g.installed.sort(),
83
- remote: isRemoteKey(source),
84
- }))
85
- .sort((a, b) => a.source.localeCompare(b.source));
86
- }
87
-
88
- /**
89
- * Find one source group by key, or throw with the available keys. The error is
90
- * scope-aware: it names the scope/path it searched, and — when the source isn't
91
- * here but exists globally — suggests re-running with `-g`.
92
- */
93
- function requireGroup(pluginsDir: string, source: string, scope?: ScopeInfo): MarketplaceGroup {
94
- const groups = marketplaceList({ pluginsDir });
95
- const hit = groups.find((g) => g.source === source);
96
- if (hit) return hit;
97
-
98
- const keys = groups.map((g) => g.source).join(", ") || "(none)";
99
- const where = scope?.label ? `${scope.label}: ${pluginsDir}` : pluginsDir;
100
- let msg = `no installed source "${source}" in ${where}. Known sources: ${keys}`;
101
-
102
- if (scope?.globalDir && scope.globalDir !== pluginsDir) {
103
- const inGlobal = marketplaceList({ pluginsDir: scope.globalDir }).some((g) => g.source === source);
104
- if (inGlobal) msg += ` — found in global; did you mean \`-g\`?`;
105
- }
106
- throw new Error(msg);
107
- }
108
-
109
- export interface MarketplaceUpgradeResult {
110
- source: string;
111
- /** Plugins re-installed/updated from the source. */
112
- updated: InstallResult[];
113
- converted: string[];
114
- /** Discovered in the source but not installed (offered for `--all`). */
115
- available: string[];
116
- }
117
-
118
- /**
119
- * Re-fetch a source and update the plugins installed from it. By default only
120
- * already-installed plugins are refreshed; `all` also installs everything new
121
- * the source now offers. With no `source`, every remote source is upgraded.
122
- */
123
- export async function marketplaceUpgrade(
124
- opts: MarketplaceScope & {
125
- source?: string;
126
- all?: boolean;
127
- targets?: AdapterTarget[];
128
- gitRunner?: GitRunner;
129
- /** Re-activate the agents after upgrade (set by the CLI; off by default). */
130
- activate?: boolean;
131
- /** Install scope for re-activation after upgrade. */
132
- agentScope?: AgentScope;
133
- },
134
- ): Promise<MarketplaceUpgradeResult[]> {
135
- const groups = opts.source
136
- ? [requireGroup(opts.pluginsDir, opts.source, opts.scope)]
137
- : marketplaceList({ pluginsDir: opts.pluginsDir }).filter((g) => g.remote);
138
-
139
- if (opts.source && !groups[0]!.remote) {
140
- throw new Error(`source "${opts.source}" is local and cannot be re-synced; re-run \`adg plugins add\`.`);
141
- }
142
- if (groups.length === 0) {
143
- throw new Error(`no remote sources installed in ${opts.pluginsDir}`);
144
- }
145
-
146
- const now = opts.now ?? new Date().toISOString();
147
- const results: MarketplaceUpgradeResult[] = [];
148
-
149
- for (const group of groups) {
150
- const { installed, converted, available } = await addPlugins({
151
- spec: group.source,
152
- pluginsDir: opts.pluginsDir,
153
- ref: group.ref,
154
- // Default: refresh what's installed. --all: install everything too.
155
- ...(opts.all ? { all: true } : { plugins: group.installed }),
156
- targets: opts.targets,
157
- marketplaceName: group.source,
158
- gitRunner: opts.gitRunner,
159
- // Re-activate so the agents pick up the upgraded content, not just the store.
160
- activate: opts.activate,
161
- scope: opts.agentScope,
162
- now,
163
- });
164
- const installedSet = new Set(group.installed);
165
- results.push({
166
- source: group.source,
167
- updated: installed,
168
- converted,
169
- available: available.filter((n) => !installedSet.has(n)),
170
- });
171
- }
172
-
173
- return results;
174
- }
175
-
176
- export interface MarketplaceRemoveResult {
177
- source: string;
178
- removed: string[];
179
- }
180
-
181
- /** Uninstall every plugin that came from a given source (and from the agents). */
182
- export function marketplaceRemove(
183
- opts: MarketplaceScope & { source: string; force?: boolean; deactivate?: boolean; agentScope?: AgentScope },
184
- ): MarketplaceRemoveResult {
185
- const group = requireGroup(opts.pluginsDir, opts.source, opts.scope);
186
- const removed: string[] = [];
187
- for (const name of group.installed) {
188
- removePlugin({
189
- pluginsDir: opts.pluginsDir,
190
- name,
191
- force: opts.force,
192
- deactivate: opts.deactivate,
193
- scope: opts.agentScope,
194
- });
195
- removed.push(name);
196
- }
197
- return { source: opts.source, removed };
198
- }
@@ -1,84 +0,0 @@
1
- import { existsSync, mkdirSync, renameSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { lockPath, marketplacePath, marketplaceSourcePath, pluginDir } from "../paths.ts";
4
- import { readLock } from "../lock.ts";
5
- import { readMarketplace, upsertMarketplacePlugin, writeMarketplace } from "../marketplace.ts";
6
-
7
- export interface MigrateMove {
8
- name: string;
9
- from: string;
10
- to: string;
11
- }
12
-
13
- export interface MigrateResult {
14
- /** Plugin directories relocated into their per-marketplace bucket. */
15
- moved: MigrateMove[];
16
- /** Already in the right place (or local/flat) — left untouched. */
17
- unchanged: string[];
18
- /** In the lock but no directory found at either the old or new path. */
19
- missing: string[];
20
- }
21
-
22
- /**
23
- * Migrate a flat plugins directory to the per-marketplace nested layout.
24
- *
25
- * For every locked plugin, move `<pluginsDir>/<name>` to the origin-derived
26
- * `<pluginsDir>/<segment>/<name>` (remote sources only; local installs stay
27
- * flat) and rewrite its marketplace.json `source.path` to match. Idempotent:
28
- * plugins already at their target path are reported as unchanged.
29
- */
30
- export function migrateLayout(pluginsDir: string): MigrateResult {
31
- const lock = readLock(lockPath(pluginsDir));
32
- const moved: MigrateMove[] = [];
33
- const unchanged: string[] = [];
34
- const missing: string[] = [];
35
-
36
- const marketFile = marketplacePath(pluginsDir);
37
- const market = readMarketplace(marketFile, "");
38
- let marketDirty = false;
39
-
40
- for (const [name, entry] of Object.entries(lock.plugins)) {
41
- const flat = join(pluginsDir, name);
42
- const target = pluginDir(pluginsDir, name, entry.origin);
43
-
44
- if (target === flat) {
45
- unchanged.push(name);
46
- continue;
47
- }
48
-
49
- if (existsSync(target)) {
50
- // Already migrated; nothing to move, but make sure the export agrees.
51
- if (rewriteMarketplacePath(market, name, pluginsDir, target)) marketDirty = true;
52
- unchanged.push(name);
53
- continue;
54
- }
55
-
56
- if (!existsSync(flat)) {
57
- missing.push(name);
58
- continue;
59
- }
60
-
61
- mkdirSync(dirname(target), { recursive: true });
62
- renameSync(flat, target);
63
- if (rewriteMarketplacePath(market, name, pluginsDir, target)) marketDirty = true;
64
- moved.push({ name, from: flat, to: target });
65
- }
66
-
67
- if (marketDirty) writeMarketplace(marketFile, market);
68
- return { moved, unchanged, missing };
69
- }
70
-
71
- /** Point a marketplace entry's `source.path` at the plugin's on-disk dir. */
72
- function rewriteMarketplacePath(
73
- market: ReturnType<typeof readMarketplace>,
74
- name: string,
75
- pluginsDir: string,
76
- dir: string,
77
- ): boolean {
78
- const existing = market.plugins.find((p) => p.name === name);
79
- if (!existing) return false;
80
- const path = marketplaceSourcePath(pluginsDir, dir);
81
- if (existing.source.path === path) return false;
82
- upsertMarketplacePlugin(market, { ...existing, source: { ...existing.source, path } });
83
- return true;
84
- }
@@ -1,137 +0,0 @@
1
- import { MultiSelectPrompt } from "@clack/core";
2
- import pc from "picocolors";
3
-
4
- /**
5
- * A skills multiselect with an on-demand description toggle. `@clack/prompts`'
6
- * `multiselect` can't intercept extra keys, so this builds directly on
7
- * `@clack/core`'s `MultiSelectPrompt` and replicates clack's chrome, adding a
8
- * `d` key that lazily loads and inlines each skill's description as a hint.
9
- *
10
- * Descriptions are read only while the toggle is on (and cached by the loader),
11
- * so a list of many skills costs nothing until the user actually asks for them.
12
- */
13
-
14
- // Match @clack/prompts' unicode-aware glyphs so the picker is visually seamless.
15
- const unicode =
16
- process.platform !== "win32" ||
17
- Boolean(process.env.WT_SESSION) ||
18
- process.env.TERM_PROGRAM === "vscode" ||
19
- process.env.TERM === "xterm-256color";
20
- const g = (a: string, b: string) => (unicode ? a : b);
21
- const S_BAR = g("│", "|");
22
- const S_BAR_END = g("└", "—");
23
- const S_STEP_ACTIVE = g("◆", "*");
24
- const S_STEP_SUBMIT = g("◇", "o");
25
- const S_STEP_CANCEL = g("■", "x");
26
- const S_CHECK_ACTIVE = g("◻", "[•]");
27
- const S_CHECK_SELECTED = g("◼", "[+]");
28
- const S_CHECK_INACTIVE = g("◻", "[ ]");
29
-
30
- export interface SkillOption {
31
- value: string;
32
- label: string;
33
- }
34
-
35
- export type RowKind = "active" | "selected" | "active-selected" | "inactive";
36
-
37
- /** Format one option row, appending the description as a dim hint when present. */
38
- export function formatSkillRow(label: string, hint: string | undefined, kind: RowKind): string {
39
- const tail = hint ? ` ${pc.dim(hint)}` : "";
40
- switch (kind) {
41
- case "active":
42
- return `${pc.cyan(S_CHECK_ACTIVE)} ${label}${tail}`;
43
- case "active-selected":
44
- return `${pc.green(S_CHECK_SELECTED)} ${label}${tail}`;
45
- case "selected":
46
- return `${pc.green(S_CHECK_SELECTED)} ${pc.dim(label)}${tail}`;
47
- default:
48
- return `${pc.dim(S_CHECK_INACTIVE)} ${pc.dim(label)}`;
49
- }
50
- }
51
-
52
- export interface BuildRowsState {
53
- cursor: number;
54
- selected: string[];
55
- /** When false, `loadDescription` is never called — this is the lazy gate. */
56
- showDesc: boolean;
57
- loadDescription: (value: string) => string | undefined;
58
- }
59
-
60
- /**
61
- * Build the rendered option rows. Descriptions are resolved only when
62
- * `showDesc` is true, so the picker performs zero SKILL.md reads until the user
63
- * presses `d`. Exported as the pure core of the picker for direct testing.
64
- */
65
- export function buildSkillRows(options: SkillOption[], state: BuildRowsState): string[] {
66
- return options.map((o, i) => {
67
- const sel = state.selected.includes(o.value);
68
- const active = i === state.cursor;
69
- const hint = state.showDesc ? state.loadDescription(o.value) : undefined;
70
- const kind: RowKind = active && sel ? "active-selected" : sel ? "selected" : active ? "active" : "inactive";
71
- return formatSkillRow(o.label, hint, kind);
72
- });
73
- }
74
-
75
- export interface MultiselectSkillsOptions {
76
- message: string;
77
- options: SkillOption[];
78
- initialValues: string[];
79
- /** Lazy, cached description lookup; invoked only while the toggle is on. */
80
- loadDescription: (value: string) => string | undefined;
81
- /** Injection seams for tests; default to process.stdin/stdout. */
82
- input?: NodeJS.ReadStream;
83
- output?: NodeJS.WriteStream;
84
- }
85
-
86
- /** Resolves to the chosen values, or a cancel symbol (use `isCancel` to detect). */
87
- export function multiselectSkills(opts: MultiselectSkillsOptions): Promise<string[] | symbol> {
88
- let showDesc = false;
89
-
90
- const symbol = (state: string): string => {
91
- if (state === "submit") return pc.green(S_STEP_SUBMIT);
92
- if (state === "cancel") return pc.red(S_STEP_CANCEL);
93
- return pc.cyan(S_STEP_ACTIVE);
94
- };
95
- const footer = () =>
96
- pc.dim(`space ${pc.gray("toggle")} · a ${pc.gray("all")} · d ${pc.gray(showDesc ? "hide" : "show")} descriptions`);
97
-
98
- const prompt = new MultiSelectPrompt({
99
- input: opts.input,
100
- output: opts.output,
101
- options: opts.options,
102
- initialValues: opts.initialValues,
103
- required: true,
104
- validate(this: { required?: boolean }, value: unknown) {
105
- if (this.required && Array.isArray(value) && value.length === 0) {
106
- return "Please select at least one option.";
107
- }
108
- return undefined;
109
- },
110
- render(this: { state: string; value: string[]; cursor: number; options: SkillOption[] }) {
111
- const head = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
112
- if (this.state === "submit" || this.state === "cancel") {
113
- const chosen = this.options
114
- .filter((o) => this.value.includes(o.value))
115
- .map((o) => pc.dim(o.label))
116
- .join(pc.dim(", "));
117
- return `${head}${pc.gray(S_BAR)} ${chosen || pc.dim("none")}`;
118
- }
119
- const rows = buildSkillRows(this.options, {
120
- cursor: this.cursor,
121
- selected: this.value,
122
- showDesc,
123
- loadDescription: opts.loadDescription,
124
- }).join(`\n${pc.cyan(S_BAR)} `);
125
- return `${head}${pc.cyan(S_BAR)} ${rows}\n${pc.cyan(S_BAR)} ${footer()}\n${pc.cyan(S_BAR_END)}\n`;
126
- },
127
- });
128
-
129
- // `a` (toggle-all) is bound by MultiSelectPrompt itself; `d` is free for us.
130
- // Touching showDesc here re-renders on the same keypress (onKeypress renders
131
- // after emitting "key"), so the first `d` press shows descriptions at once.
132
- (prompt as unknown as { on(e: string, cb: (c: string) => void): void }).on("key", (c) => {
133
- if (c === "d") showDesc = !showDesc;
134
- });
135
-
136
- return (prompt as unknown as { prompt(): Promise<string[] | symbol> }).prompt();
137
- }
@@ -1,136 +0,0 @@
1
- import { existsSync, lstatSync, readdirSync, readlinkSync, rmdirSync, rmSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
- import { claudeSkillsDir, lockPath, marketplacePath, pluginDir } from "../paths.ts";
4
- import { readLock, removeEntry, writeLock } from "../lock.ts";
5
- import { readMarketplace, removeMarketplacePlugin, writeMarketplace } from "../marketplace.ts";
6
- import { basename } from "node:path";
7
- import { resolveAgents, type Agent, type AgentScope, type AgentSyncResult } from "../agents/index.ts";
8
-
9
- export interface RemoveOptions {
10
- /** Plugins directory the plugin lives in. */
11
- pluginsDir: string;
12
- name: string;
13
- /** Remove even if other installed plugins depend on it. */
14
- force?: boolean;
15
- cwd?: string;
16
- /** Also uninstall the plugin from the agents via their CLIs. */
17
- deactivate?: boolean;
18
- /** Install scope to uninstall from; "user" (global) or "project". */
19
- scope?: AgentScope;
20
- /** Injection seam for tests; defaults to every registered agent. */
21
- agents?: Agent[];
22
- }
23
-
24
- export interface RemoveResult {
25
- name: string;
26
- /** Plugin directory that was deleted, if it existed. */
27
- removedDir?: string;
28
- /** Claude skills symlinks that were cleaned up. */
29
- unlinked: string[];
30
- removedFromLock: boolean;
31
- removedFromMarketplace: boolean;
32
- /** Per-agent deactivation outcome (when `deactivate` was set). */
33
- agents?: AgentSyncResult[];
34
- }
35
-
36
- /**
37
- * Remove an installed plugin: delete its directory, drop it from
38
- * `.plugin-lock.json` and `marketplace.json`, and clean up any Claude
39
- * skills-dir symlinks that pointed at it. Only paths under `pluginsDir` (plus
40
- * symlinks that resolve back into it) are touched.
41
- *
42
- * Refuses when another installed plugin depends on it, unless `force` is set.
43
- */
44
- export function removePlugin(opts: RemoveOptions): RemoveResult {
45
- const { name } = opts;
46
- const cwd = opts.cwd ?? process.cwd();
47
-
48
- const lockFile = lockPath(opts.pluginsDir);
49
- const lock = readLock(lockFile);
50
-
51
- const inLock = name in lock.plugins;
52
- // Locate the plugin via its recorded origin (nested for remote sources). An
53
- // orphan that is on disk but absent from the lock falls back to the flat path.
54
- const dir = inLock
55
- ? pluginDir(opts.pluginsDir, name, lock.plugins[name]!.origin)
56
- : join(opts.pluginsDir, name);
57
- const onDisk = existsSync(dir);
58
- if (!inLock && !onDisk) {
59
- throw new Error(`plugin "${name}" is not installed in ${opts.pluginsDir}`);
60
- }
61
-
62
- if (!opts.force) {
63
- const dependents = Object.entries(lock.plugins)
64
- .filter(([n, e]) => n !== name && e.dependencies && name in e.dependencies)
65
- .map(([n]) => n);
66
- if (dependents.length > 0) {
67
- throw new Error(
68
- `cannot remove "${name}": required by ${dependents.join(", ")}. ` +
69
- `Remove the dependents first or pass --force.`,
70
- );
71
- }
72
- }
73
-
74
- // Clean up Claude symlinks (created by `adg plugins link --target claude`) in
75
- // both global and project scopes — but only ones that resolve back to this
76
- // plugin's directory, so unrelated entries are never disturbed.
77
- const target = resolve(dir);
78
- const unlinked: string[] = [];
79
- for (const dir of [claudeSkillsDir(true, cwd), claudeSkillsDir(false, cwd)]) {
80
- const linkPath = join(dir, name);
81
- if (isSymlinkTo(linkPath, target)) {
82
- rmSync(linkPath);
83
- unlinked.push(linkPath);
84
- }
85
- }
86
-
87
- let removedDir: string | undefined;
88
- if (onDisk) {
89
- rmSync(dir, { recursive: true, force: true });
90
- removedDir = dir;
91
- // Drop the per-marketplace bucket if it became empty (nested remote layout).
92
- pruneEmptyParent(dirname(dir), opts.pluginsDir);
93
- }
94
-
95
- const removedFromLock = removeEntry(lock, name);
96
- if (removedFromLock) writeLock(lockFile, lock);
97
-
98
- const marketFile = marketplacePath(opts.pluginsDir);
99
- const market = readMarketplace(marketFile, basename(opts.pluginsDir));
100
- const removedFromMarketplace = removeMarketplacePlugin(market, name);
101
- if (removedFromMarketplace) writeMarketplace(marketFile, market);
102
-
103
- // Uninstall from the agents so we don't leave them enabled, pointing at a
104
- // now-deleted directory. Best-effort across all; missing CLIs are skipped.
105
- let agents: AgentSyncResult[] | undefined;
106
- if (opts.deactivate) {
107
- const ctx = { pluginsDir: opts.pluginsDir, plugins: [name], scope: opts.scope ?? "project" };
108
- agents = (opts.agents ?? resolveAgents()).map((a) => a.deactivate(ctx));
109
- }
110
-
111
- return { name, removedDir, unlinked, removedFromLock, removedFromMarketplace, agents };
112
- }
113
-
114
- /**
115
- * Remove `parent` if it is an empty per-marketplace bucket below `pluginsDir`.
116
- * Never touches `pluginsDir` itself (that would be the flat local layout).
117
- */
118
- function pruneEmptyParent(parent: string, pluginsDir: string): void {
119
- if (resolve(parent) === resolve(pluginsDir)) return;
120
- try {
121
- if (readdirSync(parent).length === 0) rmdirSync(parent);
122
- } catch {
123
- // bucket already gone or not readable — nothing to prune
124
- }
125
- }
126
-
127
- export function isSymlinkTo(linkPath: string, target: string): boolean {
128
- try {
129
- if (!lstatSync(linkPath).isSymbolicLink()) return false;
130
- // readlinkSync may return a path relative to the link's own directory; resolve
131
- // it there (resolve ignores the base when the target is already absolute).
132
- return resolve(dirname(linkPath), readlinkSync(linkPath)) === resolve(target);
133
- } catch {
134
- return false;
135
- }
136
- }
@@ -1,45 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import pc from "picocolors";
3
- import type { AdapterTarget } from "../adapters/index.ts";
4
- import { allAgents, detectedAgents } from "../agents/index.ts";
5
-
6
- /**
7
- * Interactively choose which AI agents to adapt an installed plugin for.
8
- *
9
- * Mirrors the skills install flow: detect the agents present on the machine,
10
- * pre-select them, and let the user toggle. When nothing is detected we fall
11
- * back to pre-selecting every target so a fresh setup still adapts for all.
12
- *
13
- * Callers should only reach this in an interactive TTY without an explicit
14
- * `--target`; non-interactive paths stay flag-driven.
15
- */
16
- export async function selectTargetsInteractive(): Promise<AdapterTarget[]> {
17
- const agents = allAgents();
18
- const detectedSet = new Set(detectedAgents().map((a) => a.id));
19
- const initialValues = (detectedSet.size > 0 ? [...detectedSet] : agents.map((a) => a.id)) as AdapterTarget[];
20
-
21
- if (detectedSet.size > 0) {
22
- const names = agents.filter((a) => detectedSet.has(a.id)).map((a) => pc.cyan(a.displayName)).join(", ");
23
- p.log.info(`Detected: ${names}`);
24
- } else {
25
- p.log.info(pc.dim("No agents detected locally — pre-selecting all."));
26
- }
27
-
28
- const selected = await p.multiselect({
29
- message: `Which agents do you want to adapt for? ${pc.dim("(space to toggle)")}`,
30
- options: agents.map((a) => ({
31
- value: a.id as AdapterTarget,
32
- label: a.displayName,
33
- ...(detectedSet.has(a.id) ? { hint: "detected" } : {}),
34
- })),
35
- initialValues,
36
- required: true,
37
- });
38
-
39
- if (p.isCancel(selected)) {
40
- p.cancel("Installation cancelled");
41
- process.exit(0);
42
- }
43
-
44
- return selected as AdapterTarget[];
45
- }
@@ -1,66 +0,0 @@
1
- import * as p from "@clack/prompts";
2
- import pc from "picocolors";
3
- import type { SelectComponentsRequest } from "./install.ts";
4
- import type { ComponentType, PluginSelection } from "../types.ts";
5
- import { multiselectSkills } from "./multiselect-skills.ts";
6
-
7
- function cancelled<T>(value: T | symbol): value is symbol {
8
- if (p.isCancel(value)) {
9
- p.cancel("Installation cancelled");
10
- process.exit(0);
11
- }
12
- return false;
13
- }
14
-
15
- /**
16
- * The "install everything?" gate. Returns true to install each selected plugin
17
- * in full, false to drop into per-plugin component selection. Used by `add` in
18
- * an interactive terminal when no --only/--skill flags were given.
19
- */
20
- export async function confirmFullInstall(plugins: string[]): Promise<boolean> {
21
- const list = plugins.length === 1 ? plugins[0] : `${plugins.length} plugins`;
22
- const choice = await p.select({
23
- message: `Install everything in ${pc.cyan(list)}?`,
24
- options: [
25
- { value: true, label: "Yes, install all", hint: "every skill, command, agent, mcp…" },
26
- { value: false, label: "No, let me choose", hint: "pick components per plugin" },
27
- ],
28
- initialValue: true,
29
- });
30
- if (cancelled(choice)) return true;
31
- return choice as boolean;
32
- }
33
-
34
- /**
35
- * Per-plugin component picker (①). Choose which categories to expose; if skills
36
- * is kept and the plugin has several, drill in to pick individual skills. Files
37
- * are still installed in full — this only narrows what the runtime sees.
38
- */
39
- export async function selectComponentsInteractive(req: SelectComponentsRequest): Promise<PluginSelection> {
40
- const components = await p.multiselect({
41
- message: `${pc.bold(req.name)} — what to install? ${pc.dim("(space to toggle)")}`,
42
- options: req.present.map((c) => ({ value: c, label: `${c} (${req.contents[c].length})` })),
43
- initialValues: [...req.present],
44
- required: true,
45
- });
46
- if (cancelled(components)) return { components: [...req.present] };
47
- const chosen = components as ComponentType[];
48
-
49
- const selection: PluginSelection = { components: chosen };
50
-
51
- // Drill into individual skills only when skills is kept and there's a choice.
52
- if (chosen.includes("skills") && req.contents.skills.length > 1) {
53
- const message = `${pc.bold(req.name)} — which skills? ${pc.dim("(space to toggle)")}`;
54
- const options = req.contents.skills.map((s) => ({ value: s, label: s }));
55
- const initialValues = [...req.contents.skills];
56
- const skills = req.skillDescription
57
- ? await multiselectSkills({ message, options, initialValues, loadDescription: req.skillDescription })
58
- : await p.multiselect({ message, options, initialValues, required: true });
59
- if (!cancelled(skills)) {
60
- const picked = skills as string[];
61
- if (picked.length !== req.contents.skills.length) selection.skills = picked;
62
- }
63
- }
64
-
65
- return selection;
66
- }