@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.
- package/LICENSE +21 -0
- package/README.md +622 -0
- package/bin/cli.mjs +1973 -0
- package/content/cli/agents/fluent-cli/agent.json +149 -0
- package/content/cli/agents/fluent-cli.md +132 -0
- package/content/cli/skills/fluent-bootstrap/SKILL.md +181 -0
- package/content/cli/skills/fluent-cli-index/SKILL.md +63 -0
- package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +77 -0
- package/content/cli/skills/fluent-cli-reference/SKILL.md +1031 -0
- package/content/cli/skills/fluent-cli-retailer/SKILL.md +85 -0
- package/content/cli/skills/fluent-cli-settings/SKILL.md +106 -0
- package/content/cli/skills/fluent-connect/SKILL.md +886 -0
- package/content/cli/skills/fluent-module-deploy/SKILL.md +349 -0
- package/content/cli/skills/fluent-profile/SKILL.md +180 -0
- package/content/cli/skills/fluent-workflow/SKILL.md +310 -0
- package/content/dev/agents/fluent-dev/agent.json +88 -0
- package/content/dev/agents/fluent-dev.md +525 -0
- package/content/dev/reference-modules/catalog.json +4754 -0
- package/content/dev/skills/fluent-build/SKILL.md +192 -0
- package/content/dev/skills/fluent-connection-analysis/SKILL.md +386 -0
- package/content/dev/skills/fluent-custom-code/SKILL.md +895 -0
- package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +714 -0
- package/content/dev/skills/fluent-e2e-test/SKILL.md +394 -0
- package/content/dev/skills/fluent-event-api/SKILL.md +945 -0
- package/content/dev/skills/fluent-feature-explain/SKILL.md +603 -0
- package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +695 -0
- package/content/dev/skills/fluent-feature-plan/SKILL.md +227 -0
- package/content/dev/skills/fluent-job-batch/SKILL.md +138 -0
- package/content/dev/skills/fluent-mermaid-validate/SKILL.md +86 -0
- package/content/dev/skills/fluent-module-scaffold/SKILL.md +1928 -0
- package/content/dev/skills/fluent-module-validate/SKILL.md +775 -0
- package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1108 -0
- package/content/dev/skills/fluent-retailer-config/SKILL.md +1111 -0
- package/content/dev/skills/fluent-rule-scaffold/SKILL.md +385 -0
- package/content/dev/skills/fluent-scope-decompose/SKILL.md +1021 -0
- package/content/dev/skills/fluent-session-audit-export/SKILL.md +632 -0
- package/content/dev/skills/fluent-session-summary/SKILL.md +195 -0
- package/content/dev/skills/fluent-settings/SKILL.md +1058 -0
- package/content/dev/skills/fluent-source-onboard/SKILL.md +632 -0
- package/content/dev/skills/fluent-system-monitoring/SKILL.md +767 -0
- package/content/dev/skills/fluent-test-data/SKILL.md +513 -0
- package/content/dev/skills/fluent-trace/SKILL.md +1143 -0
- package/content/dev/skills/fluent-transition-api/SKILL.md +346 -0
- package/content/dev/skills/fluent-version-manage/SKILL.md +744 -0
- package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +959 -0
- package/content/dev/skills/fluent-workflow-builder/SKILL.md +319 -0
- package/content/dev/skills/fluent-workflow-deploy/SKILL.md +267 -0
- package/content/mcp-extn/agents/fluent-mcp.md +69 -0
- package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +461 -0
- package/content/mcp-official/agents/fluent-mcp-core.md +91 -0
- package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -0
- package/content/rfl/agents/fluent-rfl.md +56 -0
- package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -0
- package/docs/CAPABILITY_MAP.md +77 -0
- package/docs/CLI_COVERAGE.md +47 -0
- package/docs/DEV_WORKFLOW.md +802 -0
- package/docs/FLOW_RUN.md +142 -0
- package/docs/USE_CASES.md +404 -0
- package/metadata.json +156 -0
- 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();
|