agileflow 4.0.0-alpha.29 → 4.0.0-alpha.30

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agileflow",
3
- "version": "4.0.0-alpha.29",
3
+ "version": "4.0.0-alpha.30",
4
4
  "description": "AI-driven agile development toolkit for Claude Code — skills-first architecture with opt-in plugins (v4)",
5
5
  "keywords": [
6
6
  "agile",
@@ -2,71 +2,79 @@
2
2
  * `agileflow plugins <action>` — inspect the plugin registry.
3
3
  *
4
4
  * Actions:
5
- * list Print all discoverable plugins with enabled status, skill count,
6
- * and description. Reads agileflow.config.json for the enable state;
7
- * falls back to the plugin's `enabledByDefault` flag when no config
8
- * is present.
5
+ * list Print all discoverable plugins with enabled status, skill
6
+ * count, and description. Supports --enabled / --disabled
7
+ * filters.
8
+ * search <query> Find plugins whose id, name, description, or provided
9
+ * skill ids contain the query (case-insensitive substring).
10
+ * show <id> Print full detail for a single plugin: version, depends,
11
+ * and the complete provides breakdown.
9
12
  *
10
- * Usage:
11
- * agileflow plugins list
12
- * agileflow plugins list --json
13
+ * All actions support --json for machine-readable output. The enable state
14
+ * is resolved from agileflow.config.json when present, otherwise from each
15
+ * plugin's `enabledByDefault` / `cannotDisable` flags.
13
16
  */
14
- const path = require("path");
15
17
  const { loadConfig } = require("../../runtime/config/loader.js");
16
18
  const { discoverPlugins } = require("../../runtime/plugins/registry.js");
17
19
  const { InvalidArgumentError, fail } = require("../../lib/errors.js");
18
20
 
19
21
  /**
20
- * @param {string} action - 'list'
21
- * @param {{ json?: boolean }} options
22
+ * Resolve enabled state for a plugin given the user's config and the plugin's
23
+ * own defaults. Mirrors the rules used by the original `list` action.
22
24
  */
23
- async function plugins(action, options = {}) {
24
- if (action !== "list") {
25
- fail(
26
- new InvalidArgumentError(`unknown action "${action}"`, {
27
- suggestion: "agileflow plugins list",
28
- }),
29
- { command: "plugins" },
30
- );
25
+ function resolveEnabled(plugin, configPlugins) {
26
+ const cfgEntry = configPlugins[plugin.id];
27
+ if (cfgEntry !== undefined) {
28
+ return Boolean(cfgEntry && cfgEntry.enabled);
31
29
  }
30
+ return Boolean(plugin.enabledByDefault) || Boolean(plugin.cannotDisable);
31
+ }
32
32
 
33
+ function buildRow(plugin, configPlugins) {
34
+ const enabled = resolveEnabled(plugin, configPlugins);
35
+ const skillCount =
36
+ plugin.provides && plugin.provides.skills
37
+ ? plugin.provides.skills.length
38
+ : 0;
39
+ const description = (plugin.description || "")
40
+ .replace(/\n/g, " ")
41
+ .trim()
42
+ .slice(0, 72);
43
+ return {
44
+ id: plugin.id,
45
+ enabled,
46
+ cannotDisable: Boolean(plugin.cannotDisable),
47
+ skills: skillCount,
48
+ description,
49
+ };
50
+ }
51
+
52
+ async function loadContext() {
33
53
  const cwd = process.cwd();
34
54
  const { config, source } = await loadConfig(cwd);
35
55
  const configPlugins = source === "file" ? config.plugins || {} : {};
36
-
37
56
  const discovered = discoverPlugins();
57
+ return { source, configPlugins, discovered };
58
+ }
38
59
 
39
- const rows = discovered.map((p) => {
40
- const cfgEntry = configPlugins[p.id];
41
- let enabled;
42
- if (cfgEntry !== undefined) {
43
- enabled = Boolean(cfgEntry && cfgEntry.enabled);
44
- } else {
45
- enabled = Boolean(p.enabledByDefault) || Boolean(p.cannotDisable);
46
- }
47
-
48
- const skillCount =
49
- p.provides && p.provides.skills ? p.provides.skills.length : 0;
50
- const description = (p.description || "")
51
- .replace(/\n/g, " ")
52
- .trim()
53
- .slice(0, 72);
54
-
55
- return {
56
- id: p.id,
57
- enabled,
58
- cannotDisable: Boolean(p.cannotDisable),
59
- skills: skillCount,
60
- description,
61
- };
62
- });
63
-
60
+ function printList(
61
+ rows,
62
+ source,
63
+ options,
64
+ { heading = "Available plugins:" } = {},
65
+ ) {
64
66
  if (options.json) {
65
67
  // eslint-disable-next-line no-console
66
68
  console.log(JSON.stringify({ source, plugins: rows }, null, 2));
67
69
  return;
68
70
  }
69
71
 
72
+ if (rows.length === 0) {
73
+ // eslint-disable-next-line no-console
74
+ console.log("\nNo plugins match the current filter.");
75
+ return;
76
+ }
77
+
70
78
  const idW = Math.max(6, ...rows.map((r) => r.id.length));
71
79
  const statusW = 8; // "disabled" length
72
80
  const skillsW = 6; // "skills" length
@@ -74,8 +82,10 @@ async function plugins(action, options = {}) {
74
82
  const header = `${"Plugin".padEnd(idW)} ${"Status".padEnd(statusW)} ${"Skills".padEnd(skillsW)} Description`;
75
83
  const sep = `${"-".repeat(idW)} ${"-".repeat(statusW)} ${"-".repeat(skillsW)} ${"-".repeat(40)}`;
76
84
 
77
- // eslint-disable-next-line no-console
78
- console.log("\nAvailable plugins:\n");
85
+ if (heading) {
86
+ // eslint-disable-next-line no-console
87
+ console.log(`\n${heading}\n`);
88
+ }
79
89
  // eslint-disable-next-line no-console
80
90
  console.log(header);
81
91
  // eslint-disable-next-line no-console
@@ -96,7 +106,7 @@ async function plugins(action, options = {}) {
96
106
 
97
107
  const enabledCount = rows.filter((r) => r.enabled || r.cannotDisable).length;
98
108
  // eslint-disable-next-line no-console
99
- console.log(`\n${rows.length} plugin(s) available, ${enabledCount} enabled.`);
109
+ console.log(`\n${rows.length} plugin(s) shown, ${enabledCount} enabled.`);
100
110
  if (source === "defaults") {
101
111
  // eslint-disable-next-line no-console
102
112
  console.log(
@@ -110,4 +120,210 @@ async function plugins(action, options = {}) {
110
120
  }
111
121
  }
112
122
 
123
+ async function listAction(options) {
124
+ const { source, configPlugins, discovered } = await loadContext();
125
+ let rows = discovered.map((p) => buildRow(p, configPlugins));
126
+
127
+ if (options.enabled && options.disabled) {
128
+ fail(
129
+ new InvalidArgumentError(
130
+ "--enabled and --disabled are mutually exclusive",
131
+ { suggestion: "use one filter at a time" },
132
+ ),
133
+ { command: "plugins" },
134
+ );
135
+ }
136
+ if (options.enabled) {
137
+ rows = rows.filter((r) => r.enabled || r.cannotDisable);
138
+ } else if (options.disabled) {
139
+ rows = rows.filter((r) => !r.enabled && !r.cannotDisable);
140
+ }
141
+
142
+ printList(rows, source, options);
143
+ }
144
+
145
+ function matchesQuery(plugin, query) {
146
+ const q = query.toLowerCase();
147
+ if (plugin.id.toLowerCase().includes(q)) return true;
148
+ if ((plugin.name || "").toLowerCase().includes(q)) return true;
149
+ if ((plugin.description || "").toLowerCase().includes(q)) return true;
150
+ const skills = (plugin.provides && plugin.provides.skills) || [];
151
+ return skills.some((s) => (s.id || "").toLowerCase().includes(q));
152
+ }
153
+
154
+ async function searchAction(query, options) {
155
+ if (!query || !query.trim()) {
156
+ fail(
157
+ new InvalidArgumentError("search requires a query string", {
158
+ suggestion: "agileflow plugins search <query>",
159
+ }),
160
+ { command: "plugins" },
161
+ );
162
+ }
163
+
164
+ const { source, configPlugins, discovered } = await loadContext();
165
+ const matches = discovered.filter((p) => matchesQuery(p, query));
166
+ const rows = matches.map((p) => buildRow(p, configPlugins));
167
+
168
+ if (options.json) {
169
+ // eslint-disable-next-line no-console
170
+ console.log(JSON.stringify({ source, query, plugins: rows }, null, 2));
171
+ return;
172
+ }
173
+
174
+ if (rows.length === 0) {
175
+ // eslint-disable-next-line no-console
176
+ console.log(`\nNo plugins match "${query}".`);
177
+ return;
178
+ }
179
+
180
+ printList(rows, source, options, {
181
+ heading: `Plugins matching "${query}":`,
182
+ });
183
+ }
184
+
185
+ function formatProvides(provides) {
186
+ const sections = [
187
+ ["Skills", provides.skills],
188
+ ["Agents", provides.agents],
189
+ ["Hooks", provides.hooks],
190
+ ["Templates", provides.templates],
191
+ ];
192
+ const lines = [];
193
+ for (const [label, items] of sections) {
194
+ const list = Array.isArray(items) ? items : [];
195
+ lines.push(` ${label} (${list.length}):`);
196
+ if (list.length === 0) {
197
+ lines.push(" (none)");
198
+ continue;
199
+ }
200
+ for (const item of list) {
201
+ const id = item.id || item.name || "(unnamed)";
202
+ const dir = item.dir ? ` ${item.dir}` : "";
203
+ lines.push(` - ${id}${dir}`);
204
+ }
205
+ }
206
+ return lines.join("\n");
207
+ }
208
+
209
+ async function showAction(id, options) {
210
+ if (!id || !id.trim()) {
211
+ fail(
212
+ new InvalidArgumentError("show requires a plugin id", {
213
+ suggestion: "agileflow plugins show <id>",
214
+ }),
215
+ { command: "plugins" },
216
+ );
217
+ }
218
+
219
+ const { source, configPlugins, discovered } = await loadContext();
220
+ const plugin = discovered.find((p) => p.id === id);
221
+ if (!plugin) {
222
+ fail(
223
+ new InvalidArgumentError(`plugin "${id}" not found`, {
224
+ suggestion: "agileflow plugins list",
225
+ }),
226
+ { command: "plugins" },
227
+ );
228
+ }
229
+
230
+ const enabled = resolveEnabled(plugin, configPlugins);
231
+ const provides = plugin.provides || {
232
+ skills: [],
233
+ agents: [],
234
+ hooks: [],
235
+ templates: [],
236
+ };
237
+
238
+ if (options.json) {
239
+ // eslint-disable-next-line no-console
240
+ console.log(
241
+ JSON.stringify(
242
+ {
243
+ source,
244
+ plugin: {
245
+ id: plugin.id,
246
+ name: plugin.name,
247
+ description: plugin.description,
248
+ version: plugin.version,
249
+ enabled,
250
+ cannotDisable: Boolean(plugin.cannotDisable),
251
+ enabledByDefault: Boolean(plugin.enabledByDefault),
252
+ depends: plugin.depends || [],
253
+ provides,
254
+ },
255
+ },
256
+ null,
257
+ 2,
258
+ ),
259
+ );
260
+ return;
261
+ }
262
+
263
+ const status = plugin.cannotDisable
264
+ ? "always (cannot disable)"
265
+ : enabled
266
+ ? "enabled"
267
+ : "disabled";
268
+ const depends =
269
+ plugin.depends && plugin.depends.length
270
+ ? plugin.depends.join(", ")
271
+ : "(none)";
272
+
273
+ // eslint-disable-next-line no-console
274
+ console.log(`\n${plugin.name} (${plugin.id})`);
275
+ // eslint-disable-next-line no-console
276
+ console.log(` Version: ${plugin.version}`);
277
+ // eslint-disable-next-line no-console
278
+ console.log(` Status: ${status}`);
279
+ // eslint-disable-next-line no-console
280
+ console.log(` Depends: ${depends}`);
281
+ // eslint-disable-next-line no-console
282
+ console.log(` Description: ${plugin.description}`);
283
+ // eslint-disable-next-line no-console
284
+ console.log("\n Provides:");
285
+ // eslint-disable-next-line no-console
286
+ console.log(formatProvides(provides));
287
+ // eslint-disable-next-line no-console
288
+ console.log("");
289
+ }
290
+
291
+ /**
292
+ * @param {string} action - 'list' | 'search' | 'show'
293
+ * @param {string} [arg] - query for search, id for show
294
+ * @param {{ json?: boolean, enabled?: boolean, disabled?: boolean }} options
295
+ */
296
+ async function plugins(action, arg, options = {}) {
297
+ // Commander invokes action handlers with (...args, command). When no
298
+ // <arg> is declared on a subcommand, the second positional may be the
299
+ // options object instead of a string. Normalize defensively so direct
300
+ // calls and commander dispatch both work.
301
+ if (
302
+ arg &&
303
+ typeof arg === "object" &&
304
+ options &&
305
+ Object.keys(options).length === 0
306
+ ) {
307
+ options = arg;
308
+ arg = undefined;
309
+ }
310
+ options = options || {};
311
+
312
+ switch (action) {
313
+ case "list":
314
+ return listAction(options);
315
+ case "search":
316
+ return searchAction(arg, options);
317
+ case "show":
318
+ return showAction(arg, options);
319
+ default:
320
+ fail(
321
+ new InvalidArgumentError(`unknown action "${action}"`, {
322
+ suggestion: "agileflow plugins list | search <query> | show <id>",
323
+ }),
324
+ { command: "plugins" },
325
+ );
326
+ }
327
+ }
328
+
113
329
  module.exports = plugins;
package/src/cli/index.js CHANGED
@@ -98,9 +98,13 @@ function buildProgram() {
98
98
  .action(skills);
99
99
 
100
100
  program
101
- .command("plugins <action>")
102
- .description("inspect the plugin registry (action: list)")
101
+ .command("plugins <action> [arg]")
102
+ .description(
103
+ "inspect the plugin registry (actions: list, search <query>, show <id>)",
104
+ )
103
105
  .option("--json", "output as JSON")
106
+ .option("--enabled", "list: show only enabled plugins")
107
+ .option("--disabled", "list: show only disabled plugins")
104
108
  .action(plugins);
105
109
 
106
110
  program