@fluentcommerce/ai-skills 0.1.0

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +622 -0
  3. package/bin/cli.mjs +1973 -0
  4. package/content/cli/agents/fluent-cli/agent.json +149 -0
  5. package/content/cli/agents/fluent-cli.md +132 -0
  6. package/content/cli/skills/fluent-bootstrap/SKILL.md +181 -0
  7. package/content/cli/skills/fluent-cli-index/SKILL.md +63 -0
  8. package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +77 -0
  9. package/content/cli/skills/fluent-cli-reference/SKILL.md +1031 -0
  10. package/content/cli/skills/fluent-cli-retailer/SKILL.md +85 -0
  11. package/content/cli/skills/fluent-cli-settings/SKILL.md +106 -0
  12. package/content/cli/skills/fluent-connect/SKILL.md +886 -0
  13. package/content/cli/skills/fluent-module-deploy/SKILL.md +349 -0
  14. package/content/cli/skills/fluent-profile/SKILL.md +180 -0
  15. package/content/cli/skills/fluent-workflow/SKILL.md +310 -0
  16. package/content/dev/agents/fluent-dev/agent.json +88 -0
  17. package/content/dev/agents/fluent-dev.md +525 -0
  18. package/content/dev/reference-modules/catalog.json +4754 -0
  19. package/content/dev/skills/fluent-build/SKILL.md +192 -0
  20. package/content/dev/skills/fluent-connection-analysis/SKILL.md +386 -0
  21. package/content/dev/skills/fluent-custom-code/SKILL.md +895 -0
  22. package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +714 -0
  23. package/content/dev/skills/fluent-e2e-test/SKILL.md +394 -0
  24. package/content/dev/skills/fluent-event-api/SKILL.md +945 -0
  25. package/content/dev/skills/fluent-feature-explain/SKILL.md +603 -0
  26. package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +695 -0
  27. package/content/dev/skills/fluent-feature-plan/SKILL.md +227 -0
  28. package/content/dev/skills/fluent-job-batch/SKILL.md +138 -0
  29. package/content/dev/skills/fluent-mermaid-validate/SKILL.md +86 -0
  30. package/content/dev/skills/fluent-module-scaffold/SKILL.md +1928 -0
  31. package/content/dev/skills/fluent-module-validate/SKILL.md +775 -0
  32. package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1108 -0
  33. package/content/dev/skills/fluent-retailer-config/SKILL.md +1111 -0
  34. package/content/dev/skills/fluent-rule-scaffold/SKILL.md +385 -0
  35. package/content/dev/skills/fluent-scope-decompose/SKILL.md +1021 -0
  36. package/content/dev/skills/fluent-session-audit-export/SKILL.md +632 -0
  37. package/content/dev/skills/fluent-session-summary/SKILL.md +195 -0
  38. package/content/dev/skills/fluent-settings/SKILL.md +1058 -0
  39. package/content/dev/skills/fluent-source-onboard/SKILL.md +632 -0
  40. package/content/dev/skills/fluent-system-monitoring/SKILL.md +767 -0
  41. package/content/dev/skills/fluent-test-data/SKILL.md +513 -0
  42. package/content/dev/skills/fluent-trace/SKILL.md +1143 -0
  43. package/content/dev/skills/fluent-transition-api/SKILL.md +346 -0
  44. package/content/dev/skills/fluent-version-manage/SKILL.md +744 -0
  45. package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +959 -0
  46. package/content/dev/skills/fluent-workflow-builder/SKILL.md +319 -0
  47. package/content/dev/skills/fluent-workflow-deploy/SKILL.md +267 -0
  48. package/content/mcp-extn/agents/fluent-mcp.md +69 -0
  49. package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +461 -0
  50. package/content/mcp-official/agents/fluent-mcp-core.md +91 -0
  51. package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -0
  52. package/content/rfl/agents/fluent-rfl.md +56 -0
  53. package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -0
  54. package/docs/CAPABILITY_MAP.md +77 -0
  55. package/docs/CLI_COVERAGE.md +47 -0
  56. package/docs/DEV_WORKFLOW.md +802 -0
  57. package/docs/FLOW_RUN.md +142 -0
  58. package/docs/USE_CASES.md +404 -0
  59. package/metadata.json +156 -0
  60. package/package.json +51 -0
package/bin/cli.mjs ADDED
@@ -0,0 +1,1973 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ cpSync,
7
+ rmSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { spawnSync } from "node:child_process";
13
+ import { join, dirname, basename, isAbsolute } from "node:path";
14
+ import { homedir } from "node:os";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ const PACKAGE_ROOT = join(__dirname, "..");
20
+ const CONTENT_ROOT = join(PACKAGE_ROOT, "content");
21
+
22
+ const CLAUDE_DIR = process.env.FLUENT_AI_SKILLS_HOME || join(homedir(), ".claude");
23
+ const AGENTS_DIR = join(CLAUDE_DIR, "agents");
24
+ const SKILLS_DIR = join(CLAUDE_DIR, "skills");
25
+
26
+ const pkg = readJson(join(PACKAGE_ROOT, "package.json")) || { version: "0.0.0" };
27
+ const metadata = readJson(join(PACKAGE_ROOT, "metadata.json")) || {};
28
+
29
+ const GROUP_ALIASES = {
30
+ mcp: "mcp-extn",
31
+ extn: "mcp-extn",
32
+ extension: "mcp-extn",
33
+ "mcp-core": "mcp-official",
34
+ official: "mcp-official",
35
+ };
36
+
37
+ const SUPPORTED_TARGETS = ["claude", "cursor", "copilot", "vscode", "windsurf", "codex", "gemini"];
38
+ const TARGET_ALIASES = {
39
+ vscode: "copilot",
40
+ };
41
+
42
+ const GENERATED_MARKER = "<!-- Generated by fluent-ai-skills. -->";
43
+ const MANAGED_BLOCK_START = "<!-- fluent-ai-skills:start -->";
44
+ const MANAGED_BLOCK_END = "<!-- fluent-ai-skills:end -->";
45
+ const DEFAULT_MCP_EXTN_REPO = "https://bitbucket.org/fluentcommerce/fluent-mcp-extn.git";
46
+ const DEFAULT_MCP_EXTN_DIR = "fluent-mcp-extn";
47
+ const DEFAULT_FLOW_REPORT_DIR = ".fluent-ai-skills";
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // Utilities
51
+ // ---------------------------------------------------------------------------
52
+
53
+ function readJson(path) {
54
+ try {
55
+ return JSON.parse(readFileSync(path, "utf-8"));
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function log(message = "") {
62
+ console.log(message);
63
+ }
64
+
65
+ function logOk(message) {
66
+ console.log(` + ${message}`);
67
+ }
68
+
69
+ function logWarn(message) {
70
+ console.log(` ! ${message}`);
71
+ }
72
+
73
+ function logErr(message) {
74
+ console.error(` x ${message}`);
75
+ }
76
+
77
+ function runCommand(command, args, cwd) {
78
+ const useWindowsShell = process.platform === "win32";
79
+ const result = useWindowsShell
80
+ ? spawnSync(formatCommand(command, args), {
81
+ cwd,
82
+ stdio: "inherit",
83
+ shell: true,
84
+ env: process.env,
85
+ })
86
+ : spawnSync(command, args, {
87
+ cwd,
88
+ stdio: "inherit",
89
+ shell: false,
90
+ env: process.env,
91
+ });
92
+
93
+ if (result.error) {
94
+ throw new Error(`Failed to run '${command}': ${result.error.message}`);
95
+ }
96
+ if (result.status !== 0) {
97
+ throw new Error(`Command failed (${result.status}): ${command} ${args.join(" ")}`);
98
+ }
99
+ }
100
+
101
+ function quoteArg(arg) {
102
+ const value = String(arg ?? "");
103
+ if (value.length === 0) {
104
+ return '""';
105
+ }
106
+ return /\s/.test(value) ? JSON.stringify(value) : value;
107
+ }
108
+
109
+ function formatCommand(command, args = []) {
110
+ return [command, ...args.map((arg) => quoteArg(arg))].join(" ");
111
+ }
112
+
113
+ function truncateText(text, maxLength = 12000) {
114
+ if (!text) return "";
115
+ if (text.length <= maxLength) return text;
116
+ return `${text.slice(0, maxLength)}\n...[truncated ${text.length - maxLength} chars]`;
117
+ }
118
+
119
+ function redactSecrets(text) {
120
+ if (!text) return "";
121
+ const rules = [
122
+ /(client[_-]?secret\s*[:=]\s*)([^\s"']+)/gi,
123
+ /(password\s*[:=]\s*)([^\s"']+)/gi,
124
+ /(access[_-]?token\s*[:=]\s*)([^\s"']+)/gi,
125
+ /(authorization\s*:\s*bearer\s+)([^\s]+)/gi,
126
+ ];
127
+
128
+ let redacted = text;
129
+ for (const pattern of rules) {
130
+ redacted = redacted.replace(pattern, "$1***");
131
+ }
132
+ return redacted;
133
+ }
134
+
135
+ function sanitizeOutput(text) {
136
+ return truncateText(redactSecrets((text || "").trim()));
137
+ }
138
+
139
+ function runCommandCapture(command, args, cwd) {
140
+ const startedAt = Date.now();
141
+ const useWindowsShell = process.platform === "win32";
142
+ const result = useWindowsShell
143
+ ? spawnSync(formatCommand(command, args), {
144
+ cwd,
145
+ stdio: "pipe",
146
+ shell: true,
147
+ env: process.env,
148
+ encoding: "utf-8",
149
+ })
150
+ : spawnSync(command, args, {
151
+ cwd,
152
+ stdio: "pipe",
153
+ shell: false,
154
+ env: process.env,
155
+ encoding: "utf-8",
156
+ });
157
+
158
+ const durationMs = Date.now() - startedAt;
159
+ const errorText = result.error ? result.error.message : "";
160
+ const stderr = [result.stderr || "", errorText].filter(Boolean).join("\n");
161
+
162
+ return {
163
+ command,
164
+ args,
165
+ commandText: formatCommand(command, args),
166
+ cwd,
167
+ durationMs,
168
+ exitCode: Number.isInteger(result.status) ? result.status : -1,
169
+ stdout: sanitizeOutput(result.stdout || ""),
170
+ stderr: sanitizeOutput(stderr),
171
+ };
172
+ }
173
+
174
+ function ensureDir(path) {
175
+ if (!existsSync(path)) {
176
+ mkdirSync(path, { recursive: true });
177
+ }
178
+ }
179
+
180
+ function normalizeNewlines(text) {
181
+ return text.replace(/\r\n/g, "\n");
182
+ }
183
+
184
+ function escapeRegExp(text) {
185
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
186
+ }
187
+
188
+ function managedBlockRegex() {
189
+ return new RegExp(
190
+ `${escapeRegExp(MANAGED_BLOCK_START)}[\\s\\S]*?${escapeRegExp(MANAGED_BLOCK_END)}\\n?`,
191
+ "g"
192
+ );
193
+ }
194
+
195
+ function stripManagedBlocks(text) {
196
+ const normalized = normalizeNewlines(text);
197
+ return normalized.replace(managedBlockRegex(), "").replace(/\n{3,}/g, "\n\n").trimEnd();
198
+ }
199
+
200
+ function hasManagedBlock(text) {
201
+ const normalized = normalizeNewlines(text);
202
+ return normalized.includes(MANAGED_BLOCK_START) && normalized.includes(MANAGED_BLOCK_END);
203
+ }
204
+
205
+ function buildManagedBlock(content) {
206
+ const body = content.trimEnd();
207
+ return [
208
+ MANAGED_BLOCK_START,
209
+ GENERATED_MARKER,
210
+ "",
211
+ body,
212
+ MANAGED_BLOCK_END,
213
+ ].join("\n");
214
+ }
215
+
216
+ function writeManagedBlockFile(path, content) {
217
+ const existing = existsSync(path) ? readFileSync(path, "utf-8") : "";
218
+ const base = stripManagedBlocks(existing);
219
+ const managed = buildManagedBlock(content);
220
+
221
+ const parts = [];
222
+ if (base.length > 0) {
223
+ parts.push(base);
224
+ }
225
+ parts.push(managed);
226
+
227
+ writeFileSync(path, `${parts.join("\n\n").trimEnd()}\n`);
228
+ }
229
+
230
+ function removeManagedBlockFile(path) {
231
+ if (!existsSync(path)) {
232
+ return "missing";
233
+ }
234
+
235
+ const existing = readFileSync(path, "utf-8");
236
+ if (!hasManagedBlock(existing)) {
237
+ return "not-managed";
238
+ }
239
+
240
+ const stripped = stripManagedBlocks(existing);
241
+ if (stripped.length === 0) {
242
+ rmSync(path, { force: true });
243
+ return "removed-file";
244
+ }
245
+
246
+ writeFileSync(path, `${stripped}\n`);
247
+ return "removed-block";
248
+ }
249
+
250
+ function listEntries(path) {
251
+ if (!existsSync(path)) {
252
+ return [];
253
+ }
254
+ return readdirSync(path, { withFileTypes: true })
255
+ .filter((entry) => !entry.name.startsWith("."))
256
+ .map((entry) => ({
257
+ name: entry.name,
258
+ type: entry.isDirectory() ? "dir" : "file",
259
+ }));
260
+ }
261
+
262
+ function parseFrontmatter(text) {
263
+ const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
264
+ if (!match) return { meta: {}, body: text };
265
+ const meta = {};
266
+ for (const line of match[1].split(/\r?\n/)) {
267
+ const idx = line.indexOf(":");
268
+ if (idx > 0) {
269
+ meta[line.slice(0, idx).trim()] = line.slice(idx + 1).trim();
270
+ }
271
+ }
272
+ return { meta, body: match[2] };
273
+ }
274
+
275
+ /** Ensure a name has the fluent- prefix exactly once. */
276
+ function fluentName(name) {
277
+ return name.startsWith("fluent-") ? name : `fluent-${name}`;
278
+ }
279
+
280
+ /** Convert a logical name into a cross-platform safe filename stem. */
281
+ function safeFileStem(name) {
282
+ return fluentName(name)
283
+ .replace(/[<>:"/\\|?*\x00-\x1F]/g, "-")
284
+ .replace(/\s+/g, "-")
285
+ .replace(/-+/g, "-")
286
+ .replace(/^-+|-+$/g, "");
287
+ }
288
+
289
+ /**
290
+ * Walk a group's agents/ and skills/ directories and collect all .md files
291
+ * with their parsed frontmatter. Skips non-markdown files (.json, .py, etc).
292
+ */
293
+ function collectMarkdownFiles(group) {
294
+ const files = [];
295
+
296
+ function walkDir(dir, prefix) {
297
+ if (!existsSync(dir)) return;
298
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
299
+ const full = join(dir, entry.name);
300
+ if (entry.isFile() && entry.name.endsWith(".md")) {
301
+ // In skill directories, only recognize SKILL.md as the skill entry point.
302
+ // Other .md files (e.g., PLAN_TEMPLATE.md) are supporting content, not skills.
303
+ if (prefix === "skill" && entry.name !== "SKILL.md") continue;
304
+ const raw = readFileSync(full, "utf-8");
305
+ const { meta, body } = parseFrontmatter(raw);
306
+ // For SKILL.md files without frontmatter name, use parent directory as a stable fallback.
307
+ const fallbackName = entry.name === "SKILL.md"
308
+ ? basename(dirname(full))
309
+ : entry.name.replace(/\.md$/, "");
310
+ const name = meta.name || fallbackName;
311
+ files.push({ name, meta, body, path: full, prefix });
312
+ } else if (entry.isDirectory()) {
313
+ walkDir(full, prefix);
314
+ }
315
+ }
316
+ }
317
+
318
+ walkDir(group.agentsRoot, "agent");
319
+ walkDir(group.skillsRoot, "skill");
320
+ return files;
321
+ }
322
+
323
+ // ---------------------------------------------------------------------------
324
+ // Group discovery and resolution (shared across all targets)
325
+ // ---------------------------------------------------------------------------
326
+
327
+ function discoverGroups() {
328
+ if (!existsSync(CONTENT_ROOT)) {
329
+ return [];
330
+ }
331
+
332
+ const metadataGroups = new Map();
333
+ if (Array.isArray(metadata.groups)) {
334
+ for (const group of metadata.groups) {
335
+ if (group && group.name) {
336
+ metadataGroups.set(group.name, group);
337
+ }
338
+ }
339
+ } else if (metadata.groups && typeof metadata.groups === "object") {
340
+ for (const [name, group] of Object.entries(metadata.groups)) {
341
+ metadataGroups.set(name, { name, ...group });
342
+ }
343
+ }
344
+
345
+ const names = readdirSync(CONTENT_ROOT, { withFileTypes: true })
346
+ .filter((entry) => entry.isDirectory())
347
+ .map((entry) => entry.name)
348
+ .sort();
349
+
350
+ return names.map((name) => {
351
+ const groupRoot = join(CONTENT_ROOT, name);
352
+ const agentsRoot = join(groupRoot, "agents");
353
+ const skillsRoot = join(groupRoot, "skills");
354
+ const meta = metadataGroups.get(name) || {};
355
+
356
+ return {
357
+ name,
358
+ description: meta.description || `Fluent skills group '${name}'`,
359
+ root: groupRoot,
360
+ agentsRoot,
361
+ skillsRoot,
362
+ agents: listEntries(agentsRoot),
363
+ skills: listEntries(skillsRoot),
364
+ };
365
+ });
366
+ }
367
+
368
+ function resolveGroups(requested, groups) {
369
+ const allNames = groups.map((group) => group.name);
370
+ const normalizedRequested = requested.map((name) => GROUP_ALIASES[name] || name);
371
+
372
+ if (normalizedRequested.length === 0 || normalizedRequested.includes("all")) {
373
+ return groups;
374
+ }
375
+
376
+ const requestedUnique = [...new Set(normalizedRequested)];
377
+ const unknown = requestedUnique.filter((name) => !allNames.includes(name));
378
+ if (unknown.length > 0) {
379
+ throw new Error(
380
+ `Unknown group(s): ${unknown.join(", ")}. Available groups: ${allNames.join(", ")}`
381
+ );
382
+ }
383
+
384
+ return groups.filter((group) => requestedUnique.includes(group.name));
385
+ }
386
+
387
+ // ---------------------------------------------------------------------------
388
+ // Target: Claude Code (~/.claude/agents + ~/.claude/skills)
389
+ // ---------------------------------------------------------------------------
390
+
391
+ function copyEntry(src, dest, type) {
392
+ if (type === "dir") {
393
+ cpSync(src, dest, { recursive: true, force: true });
394
+ } else {
395
+ cpSync(src, dest, { force: true });
396
+ }
397
+ }
398
+
399
+ function removeEntry(path, type) {
400
+ if (type === "dir") {
401
+ rmSync(path, { recursive: true, force: true });
402
+ } else {
403
+ rmSync(path, { force: true });
404
+ }
405
+ }
406
+
407
+ function installClaude(groups) {
408
+ log("");
409
+ log("Installing for Claude Code...");
410
+ log("============================");
411
+ log("");
412
+
413
+ ensureDir(AGENTS_DIR);
414
+ ensureDir(SKILLS_DIR);
415
+
416
+ let copiedAgents = 0;
417
+ let copiedSkills = 0;
418
+
419
+ for (const group of groups) {
420
+ log(`Group: ${group.name}`);
421
+
422
+ for (const entry of group.agents) {
423
+ const src = join(group.agentsRoot, entry.name);
424
+ const dest = join(AGENTS_DIR, entry.name);
425
+ copyEntry(src, dest, entry.type);
426
+ copiedAgents += 1;
427
+ logOk(`agent: ${entry.name}`);
428
+ }
429
+
430
+ for (const entry of group.skills) {
431
+ const src = join(group.skillsRoot, entry.name);
432
+ const dest = join(SKILLS_DIR, entry.name);
433
+ copyEntry(src, dest, entry.type);
434
+ copiedSkills += 1;
435
+ logOk(`skill: ${entry.name}`);
436
+ }
437
+ }
438
+
439
+ log("");
440
+ log(`Installed to: ${CLAUDE_DIR}`);
441
+ log(`Components: ${copiedAgents} agent(s), ${copiedSkills} skill(s)`);
442
+ return copiedAgents + copiedSkills;
443
+ }
444
+
445
+ function uninstallClaude(groups) {
446
+ let removed = 0;
447
+
448
+ for (const group of groups) {
449
+ log(`Group: ${group.name}`);
450
+
451
+ for (const entry of group.agents) {
452
+ const dest = join(AGENTS_DIR, entry.name);
453
+ if (existsSync(dest)) {
454
+ removeEntry(dest, entry.type);
455
+ removed++;
456
+ logOk(`removed agent: ${entry.name}`);
457
+ } else {
458
+ logWarn(`agent not found: ${entry.name}`);
459
+ }
460
+ }
461
+
462
+ for (const entry of group.skills) {
463
+ const dest = join(SKILLS_DIR, entry.name);
464
+ if (existsSync(dest)) {
465
+ removeEntry(dest, entry.type);
466
+ removed++;
467
+ logOk(`removed skill: ${entry.name}`);
468
+ } else {
469
+ logWarn(`skill not found: ${entry.name}`);
470
+ }
471
+ }
472
+ }
473
+
474
+ return removed;
475
+ }
476
+
477
+ function statusClaude(groups) {
478
+ let fullyInstalled = 0;
479
+ let partial = 0;
480
+ let missing = 0;
481
+
482
+ for (const group of groups) {
483
+ const checks = [
484
+ ...group.agents.map((e) => ({
485
+ kind: "agent",
486
+ name: e.name,
487
+ installed: existsSync(join(AGENTS_DIR, e.name)),
488
+ })),
489
+ ...group.skills.map((e) => ({
490
+ kind: "skill",
491
+ name: e.name,
492
+ installed: existsSync(join(SKILLS_DIR, e.name)),
493
+ })),
494
+ ];
495
+ const installedCount = checks.filter((c) => c.installed).length;
496
+ const total = checks.length;
497
+
498
+ let state = "missing";
499
+ if (installedCount === total && total > 0) {
500
+ state = "installed";
501
+ fullyInstalled++;
502
+ } else if (installedCount > 0) {
503
+ state = "partial";
504
+ partial++;
505
+ } else {
506
+ missing++;
507
+ }
508
+
509
+ log(`Group: ${group.name} (${state}) ${installedCount}/${total}`);
510
+ for (const c of checks) {
511
+ if (c.installed) logOk(`${c.kind}: ${c.name}`);
512
+ else logWarn(`${c.kind}: ${c.name}`);
513
+ }
514
+ log("");
515
+ }
516
+
517
+ log(`Summary: ${fullyInstalled} installed, ${partial} partial, ${missing} missing`);
518
+ return { fullyInstalled, partial, missing };
519
+ }
520
+
521
+ // ---------------------------------------------------------------------------
522
+ // Target: Cursor (.cursor/rules/*.mdc)
523
+ // ---------------------------------------------------------------------------
524
+
525
+ function cursorRulesDir() {
526
+ return join(process.cwd(), ".cursor", "rules");
527
+ }
528
+
529
+ function installCursor(groups) {
530
+ const rulesDir = cursorRulesDir();
531
+ ensureDir(rulesDir);
532
+ let count = 0;
533
+
534
+ for (const group of groups) {
535
+ log(`Group: ${group.name}`);
536
+ const files = collectMarkdownFiles(group);
537
+ for (const f of files) {
538
+ const fileName = `${safeFileStem(f.name)}.mdc`;
539
+ const mdc = [
540
+ "---",
541
+ `description: ${f.meta.description || f.name}`,
542
+ "alwaysApply: true",
543
+ "---",
544
+ "",
545
+ GENERATED_MARKER,
546
+ "",
547
+ f.body,
548
+ ].join("\n");
549
+ const dest = join(rulesDir, fileName);
550
+ writeFileSync(dest, mdc);
551
+ count++;
552
+ logOk(`${f.prefix}: ${fileName}`);
553
+ }
554
+ }
555
+
556
+ log("");
557
+ log(`Installed to: ${rulesDir}`);
558
+ log(`Rules created: ${count}`);
559
+ return count;
560
+ }
561
+
562
+ function uninstallCursor(groups) {
563
+ const rulesDir = cursorRulesDir();
564
+ let removed = 0;
565
+
566
+ for (const group of groups) {
567
+ log(`Group: ${group.name}`);
568
+ const files = collectMarkdownFiles(group);
569
+ for (const f of files) {
570
+ const fileName = `${safeFileStem(f.name)}.mdc`;
571
+ const dest = join(rulesDir, fileName);
572
+ if (existsSync(dest)) {
573
+ rmSync(dest);
574
+ removed++;
575
+ logOk(`removed: ${fileName}`);
576
+ } else {
577
+ logWarn(`not found: ${fileName}`);
578
+ }
579
+ }
580
+ }
581
+
582
+ return removed;
583
+ }
584
+
585
+ function statusCursor(groups) {
586
+ const rulesDir = cursorRulesDir();
587
+ let installed = 0;
588
+ let missing = 0;
589
+
590
+ for (const group of groups) {
591
+ const files = collectMarkdownFiles(group);
592
+ const checks = files.map((f) => ({
593
+ name: `${safeFileStem(f.name)}.mdc`,
594
+ exists: existsSync(join(rulesDir, `${safeFileStem(f.name)}.mdc`)),
595
+ }));
596
+ const found = checks.filter((c) => c.exists).length;
597
+ const total = checks.length;
598
+
599
+ log(`Group: ${group.name} (${found === total && total > 0 ? "installed" : found > 0 ? "partial" : "missing"}) ${found}/${total}`);
600
+ for (const c of checks) {
601
+ if (c.exists) { logOk(c.name); installed++; }
602
+ else { logWarn(c.name); missing++; }
603
+ }
604
+ log("");
605
+ }
606
+
607
+ log(`Summary: ${installed} installed, ${missing} missing`);
608
+ return { installed, missing };
609
+ }
610
+
611
+ // ---------------------------------------------------------------------------
612
+ // Target: GitHub Copilot (.github/copilot-instructions.md)
613
+ // ---------------------------------------------------------------------------
614
+
615
+ function copilotFile() {
616
+ return join(process.cwd(), ".github", "copilot-instructions.md");
617
+ }
618
+
619
+ function buildGroupedMarkdown(groups) {
620
+ const lines = [];
621
+ let count = 0;
622
+
623
+ for (const group of groups) {
624
+ log(`Group: ${group.name}`);
625
+ lines.push(`## Group: ${group.name}`, "");
626
+ const files = collectMarkdownFiles(group);
627
+ for (const f of files) {
628
+ lines.push(`### ${f.meta.name || f.name}`, "", f.body.trim(), "");
629
+ count++;
630
+ logOk(`${f.prefix}: ${f.meta.name || f.name}`);
631
+ }
632
+ }
633
+
634
+ return { content: lines.join("\n").trim(), count };
635
+ }
636
+
637
+ function installCopilot(groups) {
638
+ const dest = copilotFile();
639
+ ensureDir(dirname(dest));
640
+
641
+ const { content, count } = buildGroupedMarkdown(groups);
642
+ writeManagedBlockFile(dest, content);
643
+
644
+ log("");
645
+ log(`Installed to: ${dest}`);
646
+ log(`Sections written: ${count}`);
647
+ return count;
648
+ }
649
+
650
+ function uninstallCopilot() {
651
+ const dest = copilotFile();
652
+ const result = removeManagedBlockFile(dest);
653
+
654
+ if (result === "missing") {
655
+ logWarn(".github/copilot-instructions.md not found");
656
+ return 0;
657
+ }
658
+ if (result === "not-managed") {
659
+ logWarn(".github/copilot-instructions.md exists but has no managed block");
660
+ return 0;
661
+ }
662
+ if (result === "removed-file") {
663
+ logOk("removed file: .github/copilot-instructions.md");
664
+ return 1;
665
+ }
666
+
667
+ logOk("removed managed block from: .github/copilot-instructions.md");
668
+ return 1;
669
+ }
670
+
671
+ function statusCopilot() {
672
+ const dest = copilotFile();
673
+ if (!existsSync(dest)) {
674
+ log(".github/copilot-instructions.md (missing)");
675
+ return { installed: 0, missing: 1 };
676
+ }
677
+
678
+ const content = readFileSync(dest, "utf-8");
679
+ const installed = hasManagedBlock(content);
680
+ if (installed) {
681
+ log(".github/copilot-instructions.md (installed)");
682
+ return { installed: 1, missing: 0 };
683
+ }
684
+
685
+ log(".github/copilot-instructions.md (exists, not managed)");
686
+ return { installed: 0, missing: 1 };
687
+ }
688
+
689
+ // ---------------------------------------------------------------------------
690
+ // Target: Windsurf (single .windsurfrules file)
691
+ // ---------------------------------------------------------------------------
692
+
693
+ function windsurfFile() {
694
+ return join(process.cwd(), ".windsurfrules");
695
+ }
696
+
697
+ function installWindsurf(groups) {
698
+ const sections = [];
699
+ let count = 0;
700
+
701
+ for (const group of groups) {
702
+ log(`Group: ${group.name}`);
703
+ const files = collectMarkdownFiles(group);
704
+ for (const f of files) {
705
+ sections.push(`## ${f.meta.name || f.name}`, "", f.body, "");
706
+ count++;
707
+ logOk(`${f.prefix}: ${f.name}`);
708
+ }
709
+ }
710
+
711
+ const dest = windsurfFile();
712
+ writeManagedBlockFile(dest, sections.join("\n").trim());
713
+ log("");
714
+ log(`Installed to: ${dest}`);
715
+ log(`Sections: ${count}`);
716
+ return count;
717
+ }
718
+
719
+ function uninstallWindsurf() {
720
+ const dest = windsurfFile();
721
+ const result = removeManagedBlockFile(dest);
722
+
723
+ if (result === "missing") {
724
+ logWarn(".windsurfrules not found");
725
+ return 0;
726
+ }
727
+ if (result === "not-managed") {
728
+ logWarn(".windsurfrules exists but has no managed block");
729
+ return 0;
730
+ }
731
+ if (result === "removed-file") {
732
+ logOk("removed file: .windsurfrules");
733
+ return 1;
734
+ }
735
+
736
+ logOk("removed managed block from: .windsurfrules");
737
+ return 1;
738
+ }
739
+
740
+ function statusWindsurf() {
741
+ const dest = windsurfFile();
742
+ if (!existsSync(dest)) {
743
+ log(".windsurfrules (missing)");
744
+ return { installed: 0, missing: 1 };
745
+ }
746
+
747
+ const content = readFileSync(dest, "utf-8");
748
+ const installed = hasManagedBlock(content);
749
+ if (installed) {
750
+ log(".windsurfrules (installed)");
751
+ return { installed: 1, missing: 0 };
752
+ }
753
+
754
+ log(".windsurfrules (exists, not managed)");
755
+ return { installed: 0, missing: 1 };
756
+ }
757
+
758
+ // ---------------------------------------------------------------------------
759
+ // Target: Codex (single AGENTS.md file)
760
+ // ---------------------------------------------------------------------------
761
+
762
+ function codexFile() {
763
+ return join(process.cwd(), "AGENTS.md");
764
+ }
765
+
766
+ function installCodex(groups) {
767
+ const sections = ["# Fluent Commerce AI Skills", ""];
768
+ let count = 0;
769
+
770
+ for (const group of groups) {
771
+ log(`Group: ${group.name}`);
772
+ const files = collectMarkdownFiles(group);
773
+ for (const f of files) {
774
+ sections.push(`## ${f.meta.name || f.name}`, "", f.body, "");
775
+ count++;
776
+ logOk(`${f.prefix}: ${f.name}`);
777
+ }
778
+ }
779
+
780
+ const dest = codexFile();
781
+ writeManagedBlockFile(dest, sections.join("\n").trim());
782
+ log("");
783
+ log(`Installed to: ${dest}`);
784
+ log(`Sections: ${count}`);
785
+ return count;
786
+ }
787
+
788
+ function uninstallCodex() {
789
+ const dest = codexFile();
790
+ const result = removeManagedBlockFile(dest);
791
+
792
+ if (result === "missing") {
793
+ logWarn("AGENTS.md not found");
794
+ return 0;
795
+ }
796
+ if (result === "not-managed") {
797
+ logWarn("AGENTS.md exists but has no managed block");
798
+ return 0;
799
+ }
800
+ if (result === "removed-file") {
801
+ logOk("removed file: AGENTS.md");
802
+ return 1;
803
+ }
804
+
805
+ logOk("removed managed block from: AGENTS.md");
806
+ return 1;
807
+ }
808
+
809
+ function statusCodex() {
810
+ const dest = codexFile();
811
+ if (!existsSync(dest)) {
812
+ log("AGENTS.md (missing)");
813
+ return { installed: 0, missing: 1 };
814
+ }
815
+
816
+ const content = readFileSync(dest, "utf-8");
817
+ const installed = hasManagedBlock(content);
818
+ if (installed) {
819
+ log("AGENTS.md (installed)");
820
+ return { installed: 1, missing: 0 };
821
+ }
822
+
823
+ log("AGENTS.md (exists, not managed)");
824
+ return { installed: 0, missing: 1 };
825
+ }
826
+
827
+ // ---------------------------------------------------------------------------
828
+ // Target: Gemini (single GEMINI.md file)
829
+ // ---------------------------------------------------------------------------
830
+
831
+ function geminiFile() {
832
+ return join(process.cwd(), "GEMINI.md");
833
+ }
834
+
835
+ function installGemini(groups) {
836
+ const sections = ["# Fluent Commerce AI Skills", ""];
837
+ let count = 0;
838
+
839
+ for (const group of groups) {
840
+ log(`Group: ${group.name}`);
841
+ const files = collectMarkdownFiles(group);
842
+ for (const f of files) {
843
+ sections.push(`## ${f.meta.name || f.name}`, "", f.body, "");
844
+ count++;
845
+ logOk(`${f.prefix}: ${f.name}`);
846
+ }
847
+ }
848
+
849
+ const dest = geminiFile();
850
+ writeManagedBlockFile(dest, sections.join("\n").trim());
851
+ log("");
852
+ log(`Installed to: ${dest}`);
853
+ log(`Sections: ${count}`);
854
+ return count;
855
+ }
856
+
857
+ function uninstallGemini() {
858
+ const dest = geminiFile();
859
+ const result = removeManagedBlockFile(dest);
860
+
861
+ if (result === "missing") {
862
+ logWarn("GEMINI.md not found");
863
+ return 0;
864
+ }
865
+ if (result === "not-managed") {
866
+ logWarn("GEMINI.md exists but has no managed block");
867
+ return 0;
868
+ }
869
+ if (result === "removed-file") {
870
+ logOk("removed file: GEMINI.md");
871
+ return 1;
872
+ }
873
+
874
+ logOk("removed managed block from: GEMINI.md");
875
+ return 1;
876
+ }
877
+
878
+ function statusGemini() {
879
+ const dest = geminiFile();
880
+ if (!existsSync(dest)) {
881
+ log("GEMINI.md (missing)");
882
+ return { installed: 0, missing: 1 };
883
+ }
884
+
885
+ const content = readFileSync(dest, "utf-8");
886
+ const installed = hasManagedBlock(content);
887
+ if (installed) {
888
+ log("GEMINI.md (installed)");
889
+ return { installed: 1, missing: 0 };
890
+ }
891
+
892
+ log("GEMINI.md (exists, not managed)");
893
+ return { installed: 0, missing: 1 };
894
+ }
895
+
896
+ // ---------------------------------------------------------------------------
897
+ // Target dispatch
898
+ // ---------------------------------------------------------------------------
899
+
900
+ function targetInstall(target, groups) {
901
+ switch (target) {
902
+ case "claude":
903
+ return installClaude(groups);
904
+ case "cursor":
905
+ return installCursor(groups);
906
+ case "copilot":
907
+ return installCopilot(groups);
908
+ case "windsurf":
909
+ return installWindsurf(groups);
910
+ case "codex":
911
+ return installCodex(groups);
912
+ case "gemini":
913
+ return installGemini(groups);
914
+ default:
915
+ throw new Error(`Unknown target: ${target}`);
916
+ }
917
+ }
918
+
919
+ function targetUninstall(target, groups) {
920
+ switch (target) {
921
+ case "claude":
922
+ return uninstallClaude(groups);
923
+ case "cursor":
924
+ return uninstallCursor(groups);
925
+ case "copilot":
926
+ return uninstallCopilot();
927
+ case "windsurf":
928
+ return uninstallWindsurf();
929
+ case "codex":
930
+ return uninstallCodex();
931
+ case "gemini":
932
+ return uninstallGemini();
933
+ default:
934
+ throw new Error(`Unknown target: ${target}`);
935
+ }
936
+ }
937
+
938
+ function targetStatus(target, groups) {
939
+ switch (target) {
940
+ case "claude":
941
+ return statusClaude(groups);
942
+ case "cursor":
943
+ return statusCursor(groups);
944
+ case "copilot":
945
+ return statusCopilot();
946
+ case "windsurf":
947
+ return statusWindsurf();
948
+ case "codex":
949
+ return statusCodex();
950
+ case "gemini":
951
+ return statusGemini();
952
+ default:
953
+ throw new Error(`Unknown target: ${target}`);
954
+ }
955
+ }
956
+
957
+ // ---------------------------------------------------------------------------
958
+ // MCP bootstrap (.mcp.json + optional extn source download/build)
959
+ // ---------------------------------------------------------------------------
960
+
961
+ function toPosixPath(path) {
962
+ return path.replace(/\\/g, "/");
963
+ }
964
+
965
+ function readJsonObjectOrDefault(path, fallback) {
966
+ if (!existsSync(path)) {
967
+ return fallback;
968
+ }
969
+
970
+ try {
971
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
972
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
973
+ throw new Error("JSON root must be an object");
974
+ }
975
+ return parsed;
976
+ } catch (error) {
977
+ throw new Error(`Failed to parse ${path}: ${error.message}`);
978
+ }
979
+ }
980
+
981
+ function mergeServerConfig(existing, desired) {
982
+ const safeExisting = existing && typeof existing === "object" && !Array.isArray(existing)
983
+ ? existing
984
+ : {};
985
+ const existingEnv = safeExisting.env && typeof safeExisting.env === "object" && !Array.isArray(safeExisting.env)
986
+ ? safeExisting.env
987
+ : {};
988
+ const desiredEnv = desired.env && typeof desired.env === "object" ? desired.env : {};
989
+
990
+ return {
991
+ ...safeExisting,
992
+ ...desired,
993
+ type: desired.type,
994
+ command: desired.command,
995
+ args: desired.args,
996
+ env: { ...desiredEnv, ...existingEnv },
997
+ };
998
+ }
999
+
1000
+ function parseMcpSetupArgs(args) {
1001
+ const options = {
1002
+ installExtnSource: false,
1003
+ skipBuild: false,
1004
+ extnDir: DEFAULT_MCP_EXTN_DIR,
1005
+ extnRepo: DEFAULT_MCP_EXTN_REPO,
1006
+ profile: "",
1007
+ profileRetailer: "",
1008
+ officialServerName: "fluent-mcp",
1009
+ extnServerName: "fluent-mcp-extn",
1010
+ };
1011
+
1012
+ for (let i = 0; i < args.length; i++) {
1013
+ const arg = args[i];
1014
+
1015
+ switch (arg) {
1016
+ case "--install-extn-source":
1017
+ case "--with-extn-source":
1018
+ case "--download-extn-source":
1019
+ options.installExtnSource = true;
1020
+ break;
1021
+ case "--skip-build":
1022
+ options.skipBuild = true;
1023
+ break;
1024
+ case "--extn-dir":
1025
+ options.extnDir = args[++i];
1026
+ if (!options.extnDir) {
1027
+ throw new Error("Missing value for --extn-dir");
1028
+ }
1029
+ break;
1030
+ case "--extn-repo":
1031
+ options.extnRepo = args[++i];
1032
+ if (!options.extnRepo) {
1033
+ throw new Error("Missing value for --extn-repo");
1034
+ }
1035
+ break;
1036
+ case "--profile":
1037
+ options.profile = args[++i];
1038
+ if (!options.profile) {
1039
+ throw new Error("Missing value for --profile");
1040
+ }
1041
+ break;
1042
+ case "--profile-retailer":
1043
+ options.profileRetailer = args[++i];
1044
+ if (!options.profileRetailer) {
1045
+ throw new Error("Missing value for --profile-retailer");
1046
+ }
1047
+ break;
1048
+ case "--official-server-name":
1049
+ options.officialServerName = args[++i];
1050
+ if (!options.officialServerName) {
1051
+ throw new Error("Missing value for --official-server-name");
1052
+ }
1053
+ break;
1054
+ case "--extn-server-name":
1055
+ options.extnServerName = args[++i];
1056
+ if (!options.extnServerName) {
1057
+ throw new Error("Missing value for --extn-server-name");
1058
+ }
1059
+ break;
1060
+ default:
1061
+ throw new Error(`Unknown option for mcp-setup: ${arg}`);
1062
+ }
1063
+ }
1064
+
1065
+ if (options.profileRetailer && !options.profile) {
1066
+ throw new Error("--profile-retailer requires --profile");
1067
+ }
1068
+
1069
+ return options;
1070
+ }
1071
+
1072
+ function installMcpExtnSource(options) {
1073
+ const cwd = process.cwd();
1074
+ const extnDirAbsolute = join(cwd, options.extnDir);
1075
+ const packageJson = join(extnDirAbsolute, "package.json");
1076
+
1077
+ if (!existsSync(extnDirAbsolute)) {
1078
+ log(`Cloning Fluent MCP extension source into ${options.extnDir} ...`);
1079
+ runCommand("git", ["clone", "--depth", "1", options.extnRepo, options.extnDir], cwd);
1080
+ logOk(`cloned: ${options.extnRepo}`);
1081
+ } else {
1082
+ logWarn(`extension directory already exists: ${options.extnDir}`);
1083
+ }
1084
+
1085
+ if (!existsSync(packageJson)) {
1086
+ throw new Error(
1087
+ `Expected ${options.extnDir}/package.json after clone. Check --extn-dir/--extn-repo.`
1088
+ );
1089
+ }
1090
+
1091
+ if (options.skipBuild) {
1092
+ logWarn("Skipping npm install/build due to --skip-build");
1093
+ return;
1094
+ }
1095
+
1096
+ log(`Installing extension dependencies in ${options.extnDir} ...`);
1097
+ runCommand("npm", ["install"], extnDirAbsolute);
1098
+ log(`Building extension runtime in ${options.extnDir} ...`);
1099
+ runCommand("npm", ["run", "build"], extnDirAbsolute);
1100
+ logOk(`built: ${options.extnDir}/dist/index.js`);
1101
+ }
1102
+
1103
+ function setupMcp(options) {
1104
+ if (options.installExtnSource) {
1105
+ installMcpExtnSource(options);
1106
+ }
1107
+
1108
+ const cwd = process.cwd();
1109
+ const mcpConfigPath = join(cwd, ".mcp.json");
1110
+ const mcpConfig = readJsonObjectOrDefault(mcpConfigPath, {});
1111
+
1112
+ if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== "object" || Array.isArray(mcpConfig.mcpServers)) {
1113
+ mcpConfig.mcpServers = {};
1114
+ }
1115
+
1116
+ const officialDesired = {
1117
+ type: "stdio",
1118
+ command: "fluent",
1119
+ args: ["mcp", "server", "--stdio"],
1120
+ env: options.profile ? { FLUENT_PROFILE: options.profile } : {},
1121
+ };
1122
+
1123
+ // When --install-extn-source is used, wire to local dist/index.js.
1124
+ // Otherwise, default to npx @fluentcommerce/fluent-mcp-extn (npm package).
1125
+ let extnDesired;
1126
+ if (options.installExtnSource) {
1127
+ const extnRuntimePath = toPosixPath(join(options.extnDir, "dist", "index.js"));
1128
+ extnDesired = {
1129
+ type: "stdio",
1130
+ command: "node",
1131
+ args: [extnRuntimePath],
1132
+ };
1133
+ } else {
1134
+ extnDesired = {
1135
+ type: "stdio",
1136
+ command: "npx",
1137
+ args: ["@fluentcommerce/fluent-mcp-extn"],
1138
+ };
1139
+ }
1140
+ if (options.profile) {
1141
+ extnDesired.env = {
1142
+ FLUENT_PROFILE: options.profile,
1143
+ ...(options.profileRetailer
1144
+ ? { FLUENT_PROFILE_RETAILER: options.profileRetailer }
1145
+ : {}),
1146
+ };
1147
+ } else {
1148
+ extnDesired.env = {
1149
+ FLUENT_BASE_URL: "https://YOUR_ACCOUNT.sandbox.api.fluentretail.com",
1150
+ FLUENT_RETAILER_ID: "YOUR_RETAILER",
1151
+ FLUENT_CLIENT_ID: "your-client-id",
1152
+ FLUENT_CLIENT_SECRET: "your-client-secret",
1153
+ FLUENT_USERNAME: "your-username",
1154
+ FLUENT_PASSWORD: "your-password",
1155
+ };
1156
+ }
1157
+
1158
+ const officialExisting = mcpConfig.mcpServers[options.officialServerName];
1159
+ const extnExisting = mcpConfig.mcpServers[options.extnServerName];
1160
+
1161
+ const officialMerged = mergeServerConfig(officialExisting, officialDesired);
1162
+ if (options.profile) {
1163
+ officialMerged.env = { ...(officialMerged.env || {}), FLUENT_PROFILE: options.profile };
1164
+ }
1165
+ const extnMerged = mergeServerConfig(extnExisting, extnDesired);
1166
+ if (options.profile) {
1167
+ extnMerged.env = {
1168
+ ...(extnMerged.env || {}),
1169
+ FLUENT_PROFILE: options.profile,
1170
+ ...(options.profileRetailer
1171
+ ? { FLUENT_PROFILE_RETAILER: options.profileRetailer }
1172
+ : {}),
1173
+ };
1174
+ }
1175
+
1176
+ mcpConfig.mcpServers[options.officialServerName] = officialMerged;
1177
+ mcpConfig.mcpServers[options.extnServerName] = extnMerged;
1178
+
1179
+ writeFileSync(mcpConfigPath, `${JSON.stringify(mcpConfig, null, 2)}\n`);
1180
+
1181
+ log("");
1182
+ log("MCP bootstrap complete.");
1183
+ log(`Updated: ${mcpConfigPath}`);
1184
+ logOk(`official server: ${options.officialServerName}`);
1185
+ logOk(`extension server: ${options.extnServerName}`);
1186
+ log("");
1187
+ log("Next steps:");
1188
+ log(" 1) Fill credentials in .mcp.json (or keep your existing values).");
1189
+ if (options.installExtnSource) {
1190
+ if (options.skipBuild) {
1191
+ log(` 2) Build extension runtime: cd ${options.extnDir} && npm run build`);
1192
+ }
1193
+ } else {
1194
+ log(" 2) Ensure @fluentcommerce/fluent-mcp-extn is installed:");
1195
+ log(" npm install @fluentcommerce/fluent-mcp-extn");
1196
+ log(" (or use --install-extn-source for local source clone instead)");
1197
+ }
1198
+ log(" 3) Restart your IDE/agent session.");
1199
+ }
1200
+
1201
+ // ---------------------------------------------------------------------------
1202
+ // Flow runner (diagnostics + optional guarded deploy)
1203
+ // ---------------------------------------------------------------------------
1204
+
1205
+ function showFlowRunHelp() {
1206
+ log(`
1207
+ flow-run: diagnostics and optional deploy orchestration
1208
+
1209
+ USAGE
1210
+ npx @fluentcommerce/ai-skills flow-run [options]
1211
+
1212
+ OPTIONS
1213
+ --profile <name> Fluent profile for list/install checks
1214
+ --retailer <ref> Retailer ref for workflow checks
1215
+ --module <name-or-path> Module package name or local zip/path
1216
+ --config <file> Module config file to validate (and use on deploy)
1217
+ --deploy Run fluent module install (write operation)
1218
+ --yes Required with --deploy (explicit confirmation)
1219
+ --exclude-workflows Pass --exclude workflows during deploy
1220
+ --force Pass --force during deploy
1221
+ --report <path> Report output path (default: ./${DEFAULT_FLOW_REPORT_DIR}/flow-run-report-<timestamp>.json)
1222
+ --official-server-name <n> MCP key for official server (default: fluent-mcp)
1223
+ --extn-server-name <n> MCP key for extn server (default: fluent-mcp-extn)
1224
+ --skip-mcp-check Skip .mcp.json validation
1225
+ --help, -h Show flow-run help
1226
+
1227
+ EXAMPLES
1228
+ npx @fluentcommerce/ai-skills flow-run --profile HMDEV --retailer HM_TEST --module dist/module.zip --config config/module.config.json
1229
+ npx @fluentcommerce/ai-skills flow-run --profile HMDEV --retailer HM_TEST --module dist/module.zip --config config/module.config.json --deploy --yes
1230
+ npx @fluentcommerce/ai-skills flow-run --profile HMDEV --retailer HM_TEST --module @fluentcommerce/fc-module-core --deploy --yes --exclude-workflows
1231
+
1232
+ NOTES
1233
+ - By default, flow-run executes read-only diagnostics.
1234
+ - Deploy is opt-in and requires --yes.
1235
+ - A JSON report is always written with step-by-step results.
1236
+ `);
1237
+ }
1238
+
1239
+ function parseFlowRunArgs(args) {
1240
+ const options = {
1241
+ profile: "",
1242
+ retailer: "",
1243
+ module: "",
1244
+ config: "",
1245
+ deploy: false,
1246
+ yes: false,
1247
+ excludeWorkflows: false,
1248
+ force: false,
1249
+ report: "",
1250
+ officialServerName: "fluent-mcp",
1251
+ extnServerName: "fluent-mcp-extn",
1252
+ skipMcpCheck: false,
1253
+ help: false,
1254
+ };
1255
+
1256
+ for (let i = 0; i < args.length; i++) {
1257
+ const arg = args[i];
1258
+ switch (arg) {
1259
+ case "--profile":
1260
+ options.profile = args[++i];
1261
+ if (!options.profile) throw new Error("Missing value for --profile");
1262
+ break;
1263
+ case "--retailer":
1264
+ options.retailer = args[++i];
1265
+ if (!options.retailer) throw new Error("Missing value for --retailer");
1266
+ break;
1267
+ case "--module":
1268
+ options.module = args[++i];
1269
+ if (!options.module) throw new Error("Missing value for --module");
1270
+ break;
1271
+ case "--config":
1272
+ options.config = args[++i];
1273
+ if (!options.config) throw new Error("Missing value for --config");
1274
+ break;
1275
+ case "--deploy":
1276
+ options.deploy = true;
1277
+ break;
1278
+ case "--yes":
1279
+ case "--confirm":
1280
+ options.yes = true;
1281
+ break;
1282
+ case "--exclude-workflows":
1283
+ options.excludeWorkflows = true;
1284
+ break;
1285
+ case "--force":
1286
+ options.force = true;
1287
+ break;
1288
+ case "--report":
1289
+ case "--report-file":
1290
+ options.report = args[++i];
1291
+ if (!options.report) throw new Error("Missing value for --report");
1292
+ break;
1293
+ case "--official-server-name":
1294
+ options.officialServerName = args[++i];
1295
+ if (!options.officialServerName) throw new Error("Missing value for --official-server-name");
1296
+ break;
1297
+ case "--extn-server-name":
1298
+ options.extnServerName = args[++i];
1299
+ if (!options.extnServerName) throw new Error("Missing value for --extn-server-name");
1300
+ break;
1301
+ case "--skip-mcp-check":
1302
+ options.skipMcpCheck = true;
1303
+ break;
1304
+ case "--help":
1305
+ case "-h":
1306
+ options.help = true;
1307
+ break;
1308
+ default:
1309
+ throw new Error(`Unknown option for flow-run: ${arg}`);
1310
+ }
1311
+ }
1312
+
1313
+ if (options.deploy && !options.yes) {
1314
+ throw new Error("--deploy is a write operation. Re-run with --yes to confirm.");
1315
+ }
1316
+ if (options.deploy && !options.module) {
1317
+ throw new Error("--deploy requires --module <name-or-path>.");
1318
+ }
1319
+ if (options.deploy && (!options.profile || !options.retailer)) {
1320
+ throw new Error("--deploy requires --profile and --retailer.");
1321
+ }
1322
+
1323
+ return options;
1324
+ }
1325
+
1326
+ function defaultFlowReportPath() {
1327
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
1328
+ return join(process.cwd(), DEFAULT_FLOW_REPORT_DIR, `flow-run-report-${stamp}.json`);
1329
+ }
1330
+
1331
+ function resolveLocalPath(path) {
1332
+ return isAbsolute(path) ? path : join(process.cwd(), path);
1333
+ }
1334
+
1335
+ function hasPlaceholders(text) {
1336
+ const matches = text.match(/\[\[[^\]]+\]\]/g) || [];
1337
+ return [...new Set(matches)];
1338
+ }
1339
+
1340
+ function buildFlowCommandCheck(id, title, phase, command, args, blocking = true) {
1341
+ const result = runCommandCapture(command, args, process.cwd());
1342
+ return {
1343
+ id,
1344
+ title,
1345
+ phase,
1346
+ status: result.exitCode === 0 ? "passed" : "failed",
1347
+ blocking,
1348
+ command: result.commandText,
1349
+ exitCode: result.exitCode,
1350
+ durationMs: result.durationMs,
1351
+ stdout: result.stdout,
1352
+ stderr: result.stderr,
1353
+ message: result.exitCode === 0 ? "Command succeeded." : `Command failed with exit code ${result.exitCode}.`,
1354
+ };
1355
+ }
1356
+
1357
+ function summarizeFlowChecks(checks) {
1358
+ const summary = {
1359
+ passed: 0,
1360
+ failed: 0,
1361
+ warning: 0,
1362
+ skipped: 0,
1363
+ };
1364
+
1365
+ for (const check of checks) {
1366
+ if (Object.prototype.hasOwnProperty.call(summary, check.status)) {
1367
+ summary[check.status] += 1;
1368
+ }
1369
+ }
1370
+
1371
+ const overall =
1372
+ summary.failed > 0 ? "failed" : summary.warning > 0 ? "warning" : "passed";
1373
+
1374
+ return { ...summary, total: checks.length, overall };
1375
+ }
1376
+
1377
+ function hasBlockingFailures(checks) {
1378
+ return checks.some((check) => check.status === "failed" && check.blocking !== false);
1379
+ }
1380
+
1381
+ function addFlowCheck(report, check) {
1382
+ report.checks.push(check);
1383
+ const label = `${check.id}: ${check.title}`;
1384
+
1385
+ if (check.status === "passed") {
1386
+ logOk(label);
1387
+ } else if (check.status === "warning") {
1388
+ logWarn(`${label} (${check.message})`);
1389
+ } else if (check.status === "skipped") {
1390
+ logWarn(`${label} (skipped: ${check.message})`);
1391
+ } else {
1392
+ logErr(`${label} (${check.message})`);
1393
+ }
1394
+ }
1395
+
1396
+ function runFlow(options) {
1397
+ if (options.help) {
1398
+ showFlowRunHelp();
1399
+ return;
1400
+ }
1401
+
1402
+ const reportPath = resolveLocalPath(options.report || defaultFlowReportPath());
1403
+ const configPath = options.config ? resolveLocalPath(options.config) : "";
1404
+ const report = {
1405
+ schemaVersion: 1,
1406
+ generatedAt: new Date().toISOString(),
1407
+ package: {
1408
+ name: pkg.name || "@fluentcommerce/ai-skills",
1409
+ version: pkg.version || "0.0.0",
1410
+ },
1411
+ command: "flow-run",
1412
+ cwd: process.cwd(),
1413
+ options: {
1414
+ ...options,
1415
+ report: reportPath,
1416
+ config: configPath,
1417
+ },
1418
+ checks: [],
1419
+ summary: {},
1420
+ recommendations: [],
1421
+ };
1422
+
1423
+ log("");
1424
+ log("Flow run: diagnostics + optional deploy");
1425
+ log("=======================================");
1426
+ log(`Working directory: ${report.cwd}`);
1427
+ log(`Report file: ${reportPath}`);
1428
+ log("");
1429
+
1430
+ if (options.skipMcpCheck) {
1431
+ addFlowCheck(report, {
1432
+ id: "mcp-config",
1433
+ title: "MCP config validation",
1434
+ phase: "local",
1435
+ status: "skipped",
1436
+ blocking: false,
1437
+ message: "Skipped by --skip-mcp-check.",
1438
+ });
1439
+ } else {
1440
+ const mcpPath = join(process.cwd(), ".mcp.json");
1441
+ if (!existsSync(mcpPath)) {
1442
+ addFlowCheck(report, {
1443
+ id: "mcp-config",
1444
+ title: "MCP config validation",
1445
+ phase: "local",
1446
+ status: "warning",
1447
+ blocking: false,
1448
+ message: ".mcp.json was not found in the current directory.",
1449
+ details: { path: mcpPath },
1450
+ });
1451
+ } else {
1452
+ try {
1453
+ const parsed = readJsonObjectOrDefault(mcpPath, {});
1454
+ const servers =
1455
+ parsed.mcpServers && typeof parsed.mcpServers === "object" && !Array.isArray(parsed.mcpServers)
1456
+ ? parsed.mcpServers
1457
+ : {};
1458
+ const missingServers = [];
1459
+ if (!servers[options.officialServerName]) missingServers.push(options.officialServerName);
1460
+ if (!servers[options.extnServerName]) missingServers.push(options.extnServerName);
1461
+
1462
+ const unresolvedEnvKeys = [];
1463
+ for (const serverName of [options.officialServerName, options.extnServerName]) {
1464
+ const env = servers?.[serverName]?.env;
1465
+ if (!env || typeof env !== "object" || Array.isArray(env)) continue;
1466
+
1467
+ for (const [key, value] of Object.entries(env)) {
1468
+ if (typeof value !== "string") continue;
1469
+ const unresolved =
1470
+ /\[\[[^\]]+\]\]/.test(value) ||
1471
+ /YOUR_/i.test(value) ||
1472
+ /your-[a-z0-9-]+/i.test(value);
1473
+ if (unresolved) {
1474
+ unresolvedEnvKeys.push(`${serverName}.${key}`);
1475
+ }
1476
+ }
1477
+ }
1478
+
1479
+ if (missingServers.length > 0) {
1480
+ addFlowCheck(report, {
1481
+ id: "mcp-config",
1482
+ title: "MCP config validation",
1483
+ phase: "local",
1484
+ status: "warning",
1485
+ blocking: false,
1486
+ message: `Missing MCP server entries: ${missingServers.join(", ")}.`,
1487
+ details: { path: mcpPath, missingServers, unresolvedEnvKeys },
1488
+ });
1489
+ } else if (unresolvedEnvKeys.length > 0) {
1490
+ addFlowCheck(report, {
1491
+ id: "mcp-config",
1492
+ title: "MCP config validation",
1493
+ phase: "local",
1494
+ status: "warning",
1495
+ blocking: false,
1496
+ message: "MCP server env contains unresolved placeholder values.",
1497
+ details: { path: mcpPath, unresolvedEnvKeys },
1498
+ });
1499
+ } else {
1500
+ addFlowCheck(report, {
1501
+ id: "mcp-config",
1502
+ title: "MCP config validation",
1503
+ phase: "local",
1504
+ status: "passed",
1505
+ blocking: false,
1506
+ message: "Official and extension MCP server entries look valid.",
1507
+ details: {
1508
+ path: mcpPath,
1509
+ officialServer: options.officialServerName,
1510
+ extensionServer: options.extnServerName,
1511
+ },
1512
+ });
1513
+ }
1514
+ } catch (error) {
1515
+ addFlowCheck(report, {
1516
+ id: "mcp-config",
1517
+ title: "MCP config validation",
1518
+ phase: "local",
1519
+ status: "failed",
1520
+ blocking: false,
1521
+ message: error.message,
1522
+ });
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ if (!configPath) {
1528
+ addFlowCheck(report, {
1529
+ id: "config-placeholders",
1530
+ title: "Module config placeholder scan",
1531
+ phase: "local",
1532
+ status: "skipped",
1533
+ blocking: true,
1534
+ message: "No --config provided.",
1535
+ });
1536
+ } else if (!existsSync(configPath)) {
1537
+ addFlowCheck(report, {
1538
+ id: "config-placeholders",
1539
+ title: "Module config placeholder scan",
1540
+ phase: "local",
1541
+ status: "failed",
1542
+ blocking: true,
1543
+ message: `Config file not found: ${configPath}`,
1544
+ details: { configPath },
1545
+ });
1546
+ } else {
1547
+ try {
1548
+ const configText = readFileSync(configPath, "utf-8");
1549
+ const unresolved = hasPlaceholders(configText);
1550
+ if (unresolved.length > 0) {
1551
+ addFlowCheck(report, {
1552
+ id: "config-placeholders",
1553
+ title: "Module config placeholder scan",
1554
+ phase: "local",
1555
+ status: "failed",
1556
+ blocking: true,
1557
+ message: `Found unresolved placeholders in config: ${unresolved.join(", ")}`,
1558
+ details: { configPath, placeholders: unresolved },
1559
+ });
1560
+ } else {
1561
+ addFlowCheck(report, {
1562
+ id: "config-placeholders",
1563
+ title: "Module config placeholder scan",
1564
+ phase: "local",
1565
+ status: "passed",
1566
+ blocking: true,
1567
+ message: "No unresolved [[...]] placeholders found.",
1568
+ details: { configPath },
1569
+ });
1570
+ }
1571
+ } catch (error) {
1572
+ addFlowCheck(report, {
1573
+ id: "config-placeholders",
1574
+ title: "Module config placeholder scan",
1575
+ phase: "local",
1576
+ status: "failed",
1577
+ blocking: true,
1578
+ message: error.message,
1579
+ details: { configPath },
1580
+ });
1581
+ }
1582
+ }
1583
+
1584
+ const diagnostics = [
1585
+ {
1586
+ id: "fluent-version",
1587
+ title: "Fluent CLI version check",
1588
+ phase: "cli-preflight",
1589
+ command: "fluent",
1590
+ args: ["--version"],
1591
+ blocking: true,
1592
+ },
1593
+ {
1594
+ id: "profile-active",
1595
+ title: "Active profile check",
1596
+ phase: "cli-preflight",
1597
+ command: "fluent",
1598
+ args: ["profile", "active"],
1599
+ blocking: true,
1600
+ },
1601
+ ];
1602
+
1603
+ if (options.module) {
1604
+ diagnostics.push({
1605
+ id: "module-describe",
1606
+ title: "Module describe",
1607
+ phase: "cli-read",
1608
+ command: "fluent",
1609
+ args: ["module", "describe", options.module],
1610
+ blocking: true,
1611
+ });
1612
+ }
1613
+ if (options.profile) {
1614
+ diagnostics.push({
1615
+ id: "module-list",
1616
+ title: "Installed modules check",
1617
+ phase: "cli-read",
1618
+ command: "fluent",
1619
+ args: ["module", "list", "-p", options.profile],
1620
+ blocking: false,
1621
+ });
1622
+ }
1623
+ if (options.profile && options.retailer) {
1624
+ diagnostics.push({
1625
+ id: "workflow-list",
1626
+ title: "Workflow list check",
1627
+ phase: "cli-read",
1628
+ command: "fluent",
1629
+ args: ["workflow", "list", "-p", options.profile, "-r", options.retailer],
1630
+ blocking: false,
1631
+ });
1632
+ }
1633
+
1634
+ for (const step of diagnostics) {
1635
+ addFlowCheck(
1636
+ report,
1637
+ buildFlowCommandCheck(step.id, step.title, step.phase, step.command, step.args, step.blocking)
1638
+ );
1639
+ }
1640
+
1641
+ const profileCheck = report.checks.find((check) => check.id === "profile-active");
1642
+ if (options.profile && profileCheck && profileCheck.status === "passed") {
1643
+ const activeMatches = profileCheck.stdout.toLowerCase().includes(options.profile.toLowerCase());
1644
+ if (!activeMatches) {
1645
+ addFlowCheck(report, {
1646
+ id: "profile-match",
1647
+ title: "Requested profile matches active session",
1648
+ phase: "cli-preflight",
1649
+ status: "warning",
1650
+ blocking: false,
1651
+ message: `Active profile output does not clearly mention '${options.profile}'.`,
1652
+ });
1653
+ } else {
1654
+ addFlowCheck(report, {
1655
+ id: "profile-match",
1656
+ title: "Requested profile matches active session",
1657
+ phase: "cli-preflight",
1658
+ status: "passed",
1659
+ blocking: false,
1660
+ message: `Active profile appears to match '${options.profile}'.`,
1661
+ });
1662
+ }
1663
+ }
1664
+
1665
+ if (options.deploy) {
1666
+ if (hasBlockingFailures(report.checks)) {
1667
+ addFlowCheck(report, {
1668
+ id: "module-install",
1669
+ title: "Module deploy",
1670
+ phase: "cli-write",
1671
+ status: "skipped",
1672
+ blocking: true,
1673
+ message: "Skipped because one or more blocking checks failed.",
1674
+ });
1675
+ } else {
1676
+ const installArgs = ["module", "install", options.module, "--profile", options.profile, "--retailer", options.retailer];
1677
+ if (configPath) installArgs.push("--config", configPath);
1678
+ if (options.excludeWorkflows) installArgs.push("--exclude", "workflows");
1679
+ if (options.force) installArgs.push("--force");
1680
+
1681
+ const deployCheck = buildFlowCommandCheck(
1682
+ "module-install",
1683
+ "Module deploy",
1684
+ "cli-write",
1685
+ "fluent",
1686
+ installArgs,
1687
+ true
1688
+ );
1689
+ addFlowCheck(report, deployCheck);
1690
+
1691
+ if (deployCheck.status === "passed") {
1692
+ addFlowCheck(
1693
+ report,
1694
+ buildFlowCommandCheck(
1695
+ "module-list-post",
1696
+ "Post-deploy module verification",
1697
+ "cli-verify",
1698
+ "fluent",
1699
+ ["module", "list", "-p", options.profile],
1700
+ false
1701
+ )
1702
+ );
1703
+
1704
+ addFlowCheck(
1705
+ report,
1706
+ buildFlowCommandCheck(
1707
+ "workflow-list-post",
1708
+ "Post-deploy workflow verification",
1709
+ "cli-verify",
1710
+ "fluent",
1711
+ ["workflow", "list", "-p", options.profile, "-r", options.retailer],
1712
+ false
1713
+ )
1714
+ );
1715
+ } else {
1716
+ addFlowCheck(report, {
1717
+ id: "post-deploy-verify",
1718
+ title: "Post-deploy verification",
1719
+ phase: "cli-verify",
1720
+ status: "skipped",
1721
+ blocking: false,
1722
+ message: "Skipped because deploy step failed.",
1723
+ });
1724
+ }
1725
+ }
1726
+ } else {
1727
+ addFlowCheck(report, {
1728
+ id: "module-install",
1729
+ title: "Module deploy",
1730
+ phase: "cli-write",
1731
+ status: "skipped",
1732
+ blocking: true,
1733
+ message: "Deploy not requested. Re-run with --deploy --yes to install.",
1734
+ });
1735
+ }
1736
+
1737
+ report.summary = summarizeFlowChecks(report.checks);
1738
+
1739
+ if (report.checks.some((check) => check.id === "config-placeholders" && check.status === "failed")) {
1740
+ report.recommendations.push("Resolve all [[...]] placeholders in the module config before deploy.");
1741
+ }
1742
+ if (report.checks.some((check) => check.id === "mcp-config" && check.status === "warning")) {
1743
+ report.recommendations.push("Fix .mcp.json server/env placeholders before relying on MCP-driven validation.");
1744
+ }
1745
+ if (!options.deploy) {
1746
+ report.recommendations.push("When diagnostics are clean, run again with --deploy --yes to install the module.");
1747
+ }
1748
+ if (options.deploy && report.summary.overall === "passed") {
1749
+ report.recommendations.push("Deploy completed successfully. Run environment-specific smoke tests next.");
1750
+ }
1751
+
1752
+ ensureDir(dirname(reportPath));
1753
+ writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`);
1754
+
1755
+ log("");
1756
+ log("Flow run summary:");
1757
+ log(` passed: ${report.summary.passed}`);
1758
+ log(` warning: ${report.summary.warning}`);
1759
+ log(` failed: ${report.summary.failed}`);
1760
+ log(` skipped: ${report.summary.skipped}`);
1761
+ log(` overall: ${report.summary.overall}`);
1762
+ log("");
1763
+ log(`Report written: ${reportPath}`);
1764
+
1765
+ if (report.summary.overall === "failed") {
1766
+ throw new Error("flow-run completed with failures. See report for details.");
1767
+ }
1768
+ }
1769
+
1770
+ // ---------------------------------------------------------------------------
1771
+ // CLI
1772
+ // ---------------------------------------------------------------------------
1773
+
1774
+ function listGroups(groups) {
1775
+ log("");
1776
+ log("Fluent AI Skills - Available Groups");
1777
+ log("====================================");
1778
+ log("");
1779
+
1780
+ for (const group of groups) {
1781
+ log(`- ${group.name}`);
1782
+ log(` ${group.description}`);
1783
+ log(` Agents: ${group.agents.length} | Skills: ${group.skills.length}`);
1784
+ }
1785
+
1786
+ log("");
1787
+ }
1788
+
1789
+ function showHelp(groups) {
1790
+ const groupNames = groups.map((group) => group.name).join(", ");
1791
+ log(`
1792
+ fluent-ai-skills v${pkg.version}
1793
+
1794
+ Install Fluent Commerce AI skills for any coding assistant.
1795
+
1796
+ USAGE
1797
+ npx @fluentcommerce/ai-skills <command> [--target <target>] [groups...]
1798
+
1799
+ COMMANDS
1800
+ install [groups...] Install all groups (default) or selected groups
1801
+ uninstall [groups...] Uninstall all groups (default) or selected groups
1802
+ status [groups...] Show install status for all or selected groups
1803
+ mcp-setup [options] Bootstrap .mcp.json for official + extn servers
1804
+ flow-run [options] Run diagnostics and optional guarded module deploy
1805
+ list List available groups
1806
+ --version, -v Show package version
1807
+ --help, -h Show this help
1808
+
1809
+ TARGETS (--target <name>)
1810
+ claude Claude Code: ~/.claude/agents + ~/.claude/skills (default, primary)
1811
+ cursor Cursor: .cursor/rules/*.mdc (beta)
1812
+ copilot GitHub Copilot: .github/copilot-instructions.md (beta)
1813
+ vscode VS Code (Copilot format): alias of 'copilot' (beta)
1814
+ windsurf Windsurf: .windsurfrules (beta)
1815
+ codex OpenAI Codex: AGENTS.md (beta)
1816
+ gemini Gemini CLI: GEMINI.md (beta)
1817
+
1818
+ GROUPS
1819
+ ${groupNames || "(none discovered)"}
1820
+
1821
+ EXAMPLES
1822
+ npx @fluentcommerce/ai-skills install
1823
+ npx @fluentcommerce/ai-skills install --target cursor
1824
+ npx @fluentcommerce/ai-skills install --target copilot cli mcp-extn
1825
+ npx @fluentcommerce/ai-skills mcp-setup --profile HMDEV
1826
+ npx @fluentcommerce/ai-skills mcp-setup --profile HMDEV --profile-retailer HM_TEST
1827
+ npx @fluentcommerce/ai-skills mcp-setup --install-extn-source
1828
+ npx @fluentcommerce/ai-skills flow-run --profile HMDEV --retailer HM_TEST --module dist/module.zip --config config/module.config.json
1829
+ npx @fluentcommerce/ai-skills flow-run --profile HMDEV --retailer HM_TEST --module dist/module.zip --config config/module.config.json --deploy --yes
1830
+ npx @fluentcommerce/ai-skills uninstall --target windsurf
1831
+ npx @fluentcommerce/ai-skills status --target cursor
1832
+
1833
+ NOTES
1834
+ - Claude is the primary target.
1835
+ - Other targets are beta adapters and install to cwd.
1836
+ - Claude target installs globally to ~/.claude/. All others install to cwd.
1837
+ - Non-Claude targets use managed blocks and preserve existing file content.
1838
+ - mcp-setup writes/merges .mcp.json in the current project directory.
1839
+ - flow-run writes a JSON execution report in ./.fluent-ai-skills by default.
1840
+ - Set FLUENT_AI_SKILLS_HOME to override the Claude install location.
1841
+ - Aliases: mcp -> mcp-extn, mcp-core -> mcp-official.
1842
+ `);
1843
+ }
1844
+
1845
+ function resolveTarget(target) {
1846
+ return TARGET_ALIASES[target] || target;
1847
+ }
1848
+
1849
+ function parseArgs(argv) {
1850
+ let target = "claude";
1851
+ const rest = [];
1852
+
1853
+ for (let i = 0; i < argv.length; i++) {
1854
+ if (argv[i] === "--target" || argv[i] === "-t") {
1855
+ const requestedTarget = (argv[i + 1] || "").toLowerCase();
1856
+ if (!SUPPORTED_TARGETS.includes(requestedTarget)) {
1857
+ throw new Error(
1858
+ `Unknown target: ${requestedTarget}. Supported: ${SUPPORTED_TARGETS.join(", ")}`
1859
+ );
1860
+ }
1861
+ target = resolveTarget(requestedTarget);
1862
+ i++; // skip the value
1863
+ } else {
1864
+ rest.push(argv[i]);
1865
+ }
1866
+ }
1867
+
1868
+ return {
1869
+ target,
1870
+ command: rest[0],
1871
+ commandArgs: rest.slice(1),
1872
+ groupArgs: rest.slice(1).map((v) => v.toLowerCase()),
1873
+ };
1874
+ }
1875
+
1876
+ function main() {
1877
+ const groups = discoverGroups();
1878
+ const args = process.argv.slice(2);
1879
+
1880
+ if (args.length === 0) {
1881
+ showHelp(groups);
1882
+ process.exit(0);
1883
+ }
1884
+
1885
+ let parsed;
1886
+ try {
1887
+ parsed = parseArgs(args);
1888
+ } catch (error) {
1889
+ logErr(error.message);
1890
+ process.exit(1);
1891
+ }
1892
+
1893
+ const { target, command, commandArgs, groupArgs } = parsed;
1894
+
1895
+ if (!command) {
1896
+ showHelp(groups);
1897
+ process.exit(0);
1898
+ }
1899
+
1900
+ try {
1901
+ switch (command) {
1902
+ case "install": {
1903
+ const selected = resolveGroups(groupArgs, groups);
1904
+ log("");
1905
+ log(`Target: ${target}`);
1906
+ targetInstall(target, selected);
1907
+ log("");
1908
+ log(`Done. Groups: ${selected.map((g) => g.name).join(", ")}`);
1909
+ log("");
1910
+ break;
1911
+ }
1912
+ case "uninstall":
1913
+ case "remove": {
1914
+ const selected = resolveGroups(groupArgs, groups);
1915
+ log("");
1916
+ log(`Target: ${target}`);
1917
+ log("Uninstalling...");
1918
+ log("");
1919
+ targetUninstall(target, selected);
1920
+ log("");
1921
+ log(`Done. Groups: ${selected.map((g) => g.name).join(", ")}`);
1922
+ log("");
1923
+ break;
1924
+ }
1925
+ case "status":
1926
+ case "check": {
1927
+ const selected = resolveGroups(groupArgs, groups);
1928
+ log("");
1929
+ log(`Target: ${target}`);
1930
+ log("");
1931
+ targetStatus(target, selected);
1932
+ log("");
1933
+ break;
1934
+ }
1935
+ case "mcp-setup":
1936
+ case "setup-mcp":
1937
+ case "init-mcp": {
1938
+ const options = parseMcpSetupArgs(commandArgs);
1939
+ setupMcp(options);
1940
+ break;
1941
+ }
1942
+ case "flow-run":
1943
+ case "run-flow": {
1944
+ const options = parseFlowRunArgs(commandArgs);
1945
+ runFlow(options);
1946
+ break;
1947
+ }
1948
+ case "list":
1949
+ listGroups(groups);
1950
+ break;
1951
+ case "--version":
1952
+ case "-v":
1953
+ log(`fluent-ai-skills v${pkg.version}`);
1954
+ break;
1955
+ case "--help":
1956
+ case "-h":
1957
+ case "help":
1958
+ showHelp(groups);
1959
+ break;
1960
+ default:
1961
+ logErr(`Unknown command: ${command}`);
1962
+ log("");
1963
+ showHelp(groups);
1964
+ process.exit(1);
1965
+ }
1966
+ } catch (error) {
1967
+ logErr(error.message);
1968
+ log("");
1969
+ process.exit(1);
1970
+ }
1971
+ }
1972
+
1973
+ main();