@buoy-design/cli 0.1.30 → 0.2.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 (135) hide show
  1. package/assets/commands/contribution-loop.md +195 -0
  2. package/dist/bin.js +0 -0
  3. package/dist/cloud/config.d.ts +8 -0
  4. package/dist/cloud/config.d.ts.map +1 -1
  5. package/dist/cloud/config.js +10 -0
  6. package/dist/cloud/config.js.map +1 -1
  7. package/dist/cloud/feature-gate.d.ts +30 -0
  8. package/dist/cloud/feature-gate.d.ts.map +1 -0
  9. package/dist/cloud/feature-gate.js +138 -0
  10. package/dist/cloud/feature-gate.js.map +1 -0
  11. package/dist/cloud/index.d.ts +1 -0
  12. package/dist/cloud/index.d.ts.map +1 -1
  13. package/dist/cloud/index.js +1 -0
  14. package/dist/cloud/index.js.map +1 -1
  15. package/dist/commands/anchor.js +1 -1
  16. package/dist/commands/anchor.js.map +1 -1
  17. package/dist/commands/begin.d.ts.map +1 -1
  18. package/dist/commands/begin.js +118 -5
  19. package/dist/commands/begin.js.map +1 -1
  20. package/dist/commands/billing.d.ts +1 -1
  21. package/dist/commands/billing.js +22 -18
  22. package/dist/commands/billing.js.map +1 -1
  23. package/dist/commands/bot.d.ts +9 -0
  24. package/dist/commands/bot.d.ts.map +1 -0
  25. package/dist/commands/bot.js +258 -0
  26. package/dist/commands/bot.js.map +1 -0
  27. package/dist/commands/check.d.ts.map +1 -1
  28. package/dist/commands/check.js +16 -2
  29. package/dist/commands/check.js.map +1 -1
  30. package/dist/commands/cloud.d.ts +15 -0
  31. package/dist/commands/cloud.d.ts.map +1 -0
  32. package/dist/commands/cloud.js +36 -0
  33. package/dist/commands/cloud.js.map +1 -0
  34. package/dist/commands/commands.d.ts +8 -0
  35. package/dist/commands/commands.d.ts.map +1 -0
  36. package/dist/commands/commands.js +148 -0
  37. package/dist/commands/commands.js.map +1 -0
  38. package/dist/commands/components-query.d.ts +13 -0
  39. package/dist/commands/components-query.d.ts.map +1 -0
  40. package/dist/commands/components-query.js +263 -0
  41. package/dist/commands/components-query.js.map +1 -0
  42. package/dist/commands/components.d.ts +8 -0
  43. package/dist/commands/components.d.ts.map +1 -0
  44. package/dist/commands/components.js +14 -0
  45. package/dist/commands/components.js.map +1 -0
  46. package/dist/commands/dock.d.ts +13 -0
  47. package/dist/commands/dock.d.ts.map +1 -1
  48. package/dist/commands/dock.js +594 -832
  49. package/dist/commands/dock.js.map +1 -1
  50. package/dist/commands/drift.d.ts.map +1 -1
  51. package/dist/commands/drift.js +4 -13
  52. package/dist/commands/drift.js.map +1 -1
  53. package/dist/commands/fix.js +1 -1
  54. package/dist/commands/fix.js.map +1 -1
  55. package/dist/commands/github.d.ts.map +1 -1
  56. package/dist/commands/github.js +8 -6
  57. package/dist/commands/github.js.map +1 -1
  58. package/dist/commands/history.js +2 -2
  59. package/dist/commands/history.js.map +1 -1
  60. package/dist/commands/import.js +1 -1
  61. package/dist/commands/import.js.map +1 -1
  62. package/dist/commands/index.d.ts +2 -22
  63. package/dist/commands/index.d.ts.map +1 -1
  64. package/dist/commands/index.js +3 -23
  65. package/dist/commands/index.js.map +1 -1
  66. package/dist/commands/learn.d.ts +9 -0
  67. package/dist/commands/learn.d.ts.map +1 -0
  68. package/dist/commands/learn.js +244 -0
  69. package/dist/commands/learn.js.map +1 -0
  70. package/dist/commands/link.js +2 -2
  71. package/dist/commands/link.js.map +1 -1
  72. package/dist/commands/login.d.ts.map +1 -1
  73. package/dist/commands/login.js +12 -3
  74. package/dist/commands/login.js.map +1 -1
  75. package/dist/commands/onboard.d.ts.map +1 -1
  76. package/dist/commands/onboard.js +64 -1
  77. package/dist/commands/onboard.js.map +1 -1
  78. package/dist/commands/plans.d.ts +9 -0
  79. package/dist/commands/plans.d.ts.map +1 -0
  80. package/dist/commands/plans.js +140 -0
  81. package/dist/commands/plans.js.map +1 -0
  82. package/dist/commands/ship.d.ts +14 -0
  83. package/dist/commands/ship.d.ts.map +1 -0
  84. package/dist/commands/ship.js +342 -0
  85. package/dist/commands/ship.js.map +1 -0
  86. package/dist/commands/show.d.ts +3 -0
  87. package/dist/commands/show.d.ts.map +1 -0
  88. package/dist/commands/show.js +340 -0
  89. package/dist/commands/show.js.map +1 -0
  90. package/dist/commands/status.d.ts.map +1 -1
  91. package/dist/commands/status.js +13 -0
  92. package/dist/commands/status.js.map +1 -1
  93. package/dist/commands/sync.js +1 -1
  94. package/dist/commands/sync.js.map +1 -1
  95. package/dist/commands/tokens-lookup.d.ts +14 -0
  96. package/dist/commands/tokens-lookup.d.ts.map +1 -0
  97. package/dist/commands/tokens-lookup.js +300 -0
  98. package/dist/commands/tokens-lookup.js.map +1 -0
  99. package/dist/commands/tokens.d.ts.map +1 -1
  100. package/dist/commands/tokens.js +6 -1
  101. package/dist/commands/tokens.js.map +1 -1
  102. package/dist/commands/whoami.d.ts.map +1 -1
  103. package/dist/commands/whoami.js +60 -3
  104. package/dist/commands/whoami.js.map +1 -1
  105. package/dist/config/schema.d.ts +132 -132
  106. package/dist/fix/applier.js +1 -1
  107. package/dist/fix/applier.js.map +1 -1
  108. package/dist/hooks/index.d.ts +2 -1
  109. package/dist/hooks/index.d.ts.map +1 -1
  110. package/dist/hooks/index.js +31 -3
  111. package/dist/hooks/index.js.map +1 -1
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.js +19 -43
  114. package/dist/index.js.map +1 -1
  115. package/dist/insights/project-insights.d.ts.map +1 -1
  116. package/dist/insights/project-insights.js +0 -8
  117. package/dist/insights/project-insights.js.map +1 -1
  118. package/dist/output/fix-formatters.d.ts.map +1 -1
  119. package/dist/output/fix-formatters.js +10 -2
  120. package/dist/output/fix-formatters.js.map +1 -1
  121. package/dist/services/context-generator.js +2 -2
  122. package/dist/services/context-generator.js.map +1 -1
  123. package/dist/services/skill-export.d.ts.map +1 -1
  124. package/dist/services/skill-export.js +39 -4
  125. package/dist/services/skill-export.js.map +1 -1
  126. package/dist/wizard/ai-guardrails-generator.js +1 -1
  127. package/package.json +16 -15
  128. package/LICENSE +0 -21
  129. package/dist/commands/explain.d.ts +0 -3
  130. package/dist/commands/explain.js +0 -224
  131. package/dist/commands/explain.js.map +0 -1
  132. package/dist/explain/agents.d.ts +0 -31
  133. package/dist/explain/agents.d.ts.map +0 -1
  134. package/dist/explain/agents.js +0 -507
  135. package/dist/explain/agents.js.map +0 -1
@@ -1,885 +1,647 @@
1
+ /**
2
+ * buoy dock - Dock Buoy into your project
3
+ *
4
+ * Smart walkthrough that sets up config, agents, and hooks.
5
+ *
6
+ * Usage:
7
+ * buoy dock # Smart walkthrough: config → agents → hooks
8
+ * buoy dock config # Just create buoy.config.mjs
9
+ * buoy dock agents # Set up AI agents (skills + context)
10
+ * buoy dock skills # Just create skill files
11
+ * buoy dock context # Just generate CLAUDE.md section
12
+ * buoy dock hooks # Just set up git hooks
13
+ */
1
14
  import { Command } from "commander";
2
- import { writeFileSync, existsSync, readFileSync } from "fs";
3
- import { resolve, extname } from "path";
15
+ import { writeFileSync, existsSync, readFileSync, mkdirSync, appendFileSync, copyFileSync, readdirSync } from "fs";
16
+ import { resolve, dirname, join } from "path";
17
+ import { homedir } from "os";
18
+ import { fileURLToPath } from "url";
4
19
  import chalk from "chalk";
5
- import ora from "ora";
6
20
  import { createInterface } from "readline";
7
- import { success, error, info, warning } from "../output/reporters.js";
8
- import { ProjectDetector, detectMonorepoConfig, expandPatternsForMonorepo, } from "../detect/index.js";
9
- import { detectFrameworks, getPluginInstallCommand, PLUGIN_INFO, BUILTIN_SCANNERS, } from "../detect/frameworks.js";
10
- import { setupHooks, generateStandaloneHook, detectHookSystem, } from "../hooks/index.js";
11
- import { parseTokenFile, detectFormat } from "@buoy-design/core";
12
- function generateConfig(project) {
13
- const lines = [];
14
- // Detect monorepo configuration for pattern expansion
15
- const monorepoConfig = detectMonorepoConfig(project.root);
16
- lines.push(`/** @type {import('@buoy-design/cli').BuoyConfig} */`);
17
- lines.push(`export default {`);
18
- lines.push(` project: {`);
19
- lines.push(` name: '${project.name}',`);
20
- lines.push(` },`);
21
- lines.push(` sources: {`);
22
- // Determine the correct source key based on framework
23
- const getSourceKey = (frameworkName) => {
24
- // React-based frameworks
25
- if ([
26
- "react",
27
- "nextjs",
28
- "remix",
29
- "gatsby",
30
- "react-native",
31
- "expo",
32
- "preact",
33
- "solid",
34
- ].includes(frameworkName)) {
35
- return "react";
36
- }
37
- // Vue-based frameworks
38
- if (["vue", "nuxt"].includes(frameworkName)) {
39
- return "vue";
40
- }
41
- // Svelte-based frameworks
42
- if (["svelte", "sveltekit"].includes(frameworkName)) {
43
- return "svelte";
44
- }
45
- // Angular
46
- if (frameworkName === "angular") {
47
- return "angular";
48
- }
49
- // Web Components
50
- if (["lit", "stencil"].includes(frameworkName)) {
51
- return "webcomponent";
52
- }
53
- // Astro is special - can use multiple frameworks
54
- if (frameworkName === "astro") {
55
- return "react"; // Default to React for Astro
56
- }
57
- return null;
21
+ import ora from "ora";
22
+ import { success, error, info, warning, setJsonMode, spinner } from "../output/reporters.js";
23
+ import { loadConfig, getConfigPath } from "../config/loader.js";
24
+ import { buildAutoConfig } from "../config/auto-detect.js";
25
+ import { ScanOrchestrator } from "../scan/orchestrator.js";
26
+ import { SkillExportService } from "../services/skill-export.js";
27
+ import { generateContext } from "../services/context-generator.js";
28
+ import { setupHooks, generateStandaloneHook, detectHookSystem, setupClaudeHooks } from "../hooks/index.js";
29
+ import { ProjectDetector, detectMonorepoConfig, expandPatternsForMonorepo } from "../detect/index.js";
30
+ import { detectFrameworks, getPluginInstallCommand } from "../detect/frameworks.js";
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+ export function createDockCommand() {
34
+ const cmd = new Command("dock")
35
+ .description("Dock Buoy into your project")
36
+ .option("-y, --yes", "Auto-accept all defaults")
37
+ .option("--json", "Output results as JSON")
38
+ .action(async (options) => {
39
+ await runSmartDock(options);
40
+ });
41
+ // dock config - Just create config
42
+ cmd
43
+ .command("config")
44
+ .description("Create buoy.config.mjs")
45
+ .option("-f, --force", "Overwrite existing configuration")
46
+ .option("-y, --yes", "Auto-install recommended plugins without prompting")
47
+ .option("--skip-detect", "Skip auto-detection and create minimal config")
48
+ .action(async (options) => {
49
+ await runConfigDock(options);
50
+ });
51
+ // dock agents - Full AI agent onboarding
52
+ cmd
53
+ .command("agents")
54
+ .description("Set up AI agents with your design system")
55
+ .option("--dry-run", "Show what would be created without writing files")
56
+ .option("--json", "Output result as JSON")
57
+ .action(async (options) => {
58
+ await runAgentsDock(options);
59
+ });
60
+ // dock skills - Create skill files
61
+ cmd
62
+ .command("skills")
63
+ .description("Create skill files for AI agents")
64
+ .option("-o, --output <path>", "Output directory", ".claude/skills/design-system")
65
+ .option("--global", "Export to global skills directory (~/.claude/skills/)")
66
+ .option("--dry-run", "Show what would be created without writing files")
67
+ .option("--json", "Output result as JSON")
68
+ .action(async (options) => {
69
+ await runSkillsDock(options);
70
+ });
71
+ // dock context - Generate CLAUDE.md section
72
+ cmd
73
+ .command("context")
74
+ .description("Generate design system context for CLAUDE.md")
75
+ .option("-o, --output <path>", "Output file path (default: CLAUDE.md)")
76
+ .option("--stdout", "Output to stdout instead of file")
77
+ .option("-d, --detail <level>", "Detail level: minimal, standard, comprehensive", "standard")
78
+ .option("--json", "Output as JSON with stats")
79
+ .action(async (options) => {
80
+ await runContextDock(options);
81
+ });
82
+ // dock hooks - Set up git hooks
83
+ cmd
84
+ .command("hooks")
85
+ .description("Set up git hooks for drift checking")
86
+ .option("--claude", "Also set up Claude Code hooks")
87
+ .action(async (options) => {
88
+ await runHooksDock(options);
89
+ });
90
+ return cmd;
91
+ }
92
+ /**
93
+ * Smart dock walkthrough - goes through config → agents → hooks
94
+ */
95
+ async function runSmartDock(options) {
96
+ const cwd = process.cwd();
97
+ if (options.json) {
98
+ setJsonMode(true);
99
+ }
100
+ console.log("");
101
+ console.log(chalk.cyan.bold(" Docking Buoy..."));
102
+ console.log(chalk.dim(" ─".repeat(25)));
103
+ console.log("");
104
+ const results = {
105
+ configCreated: false,
106
+ agentsSetup: false,
107
+ hooksSetup: false,
58
108
  };
59
- // File extensions by framework
60
- const getExtensions = (sourceKey, typescript) => {
61
- switch (sourceKey) {
62
- case "vue":
63
- return ["vue"];
64
- case "svelte":
65
- return ["svelte"];
66
- case "angular":
67
- return ["component.ts"];
68
- case "webcomponent":
69
- return ["ts"];
70
- default: // react
71
- return typescript ? ["tsx", "jsx"] : ["jsx", "tsx"];
109
+ // Step 1: Check/create config
110
+ const configPath = getConfigPath();
111
+ if (!configPath) {
112
+ console.log(chalk.yellow(" ⚠ No buoy.config.mjs found"));
113
+ console.log("");
114
+ const shouldCreateConfig = options.yes || await promptConfirm(" Create config now?", true);
115
+ if (shouldCreateConfig) {
116
+ console.log("");
117
+ await runConfigDock({ yes: options.yes });
118
+ results.configCreated = true;
72
119
  }
73
- };
74
- // JS Framework config (React, Vue, Svelte, Angular, Web Components)
75
- // Handle multiple frameworks - generate config for each UI framework
76
- const addedSourceKeys = new Set();
77
- for (const framework of project.frameworks) {
78
- const sourceKey = getSourceKey(framework.name);
79
- if (sourceKey && !addedSourceKeys.has(sourceKey)) {
80
- addedSourceKeys.add(sourceKey);
81
- const extensions = getExtensions(sourceKey, framework.typescript);
82
- const jsComponents = project.components.filter((c) => c.type === "jsx" ||
83
- c.type === "vue" ||
84
- c.type === "svelte" ||
85
- !c.type);
86
- let includePatterns;
87
- if (jsComponents.length > 0) {
88
- includePatterns = jsComponents.flatMap((c) => extensions.map((ext) => `${c.path}/**/*.${ext}`));
89
- }
90
- else {
91
- // Use default patterns, but expand for monorepo if detected
92
- const defaultPatterns = extensions.map((ext) => `src/**/*.${ext}`);
93
- if (monorepoConfig.type) {
94
- const expanded = expandPatternsForMonorepo(defaultPatterns, monorepoConfig);
95
- includePatterns = expanded.allPatterns;
96
- }
97
- else {
98
- includePatterns = defaultPatterns;
99
- }
100
- }
101
- lines.push(` ${sourceKey}: {`);
102
- lines.push(` enabled: true,`);
103
- lines.push(` include: [${includePatterns.map((p) => `'${p}'`).join(", ")}],`);
104
- lines.push(` exclude: ['**/*.test.*', '**/*.spec.*', '**/*.stories.*'],`);
105
- if (project.designSystem) {
106
- lines.push(` designSystemPackage: '${project.designSystem.package}',`);
107
- }
108
- if (sourceKey === "webcomponent") {
109
- const wcFramework = framework.name === "lit" ? "lit" : "stencil";
110
- lines.push(` framework: '${wcFramework}',`);
111
- }
112
- lines.push(` },`);
120
+ else {
121
+ console.log("");
122
+ info("Run `buoy dock config` later to create config.");
123
+ console.log("");
124
+ return;
113
125
  }
114
126
  }
115
- // Server-side / template-based framework config
116
- const serverFrameworks = [
117
- "php",
118
- "laravel",
119
- "symfony",
120
- "rails",
121
- "django",
122
- "flask",
123
- "fastapi",
124
- "express",
125
- "nestjs",
126
- "spring",
127
- "aspnet",
128
- "go",
129
- "hugo",
130
- "jekyll",
131
- "eleventy",
132
- ];
133
- // Map framework to template type
134
- const getTemplateType = (frameworkName, componentType) => {
135
- // Return component type directly if it's a known template type
136
- const knownTypes = [
137
- // Server-side templates
138
- "blade", "erb", "twig", "njk", "razor", "hbs", "mustache",
139
- "ejs", "pug", "liquid", "slim", "haml", "jinja", "django",
140
- "thymeleaf", "freemarker", "go-template", "edge", "eta", "heex",
141
- "velocity", "xslt",
142
- // JS frameworks
143
- "astro", "solid", "qwik", "marko", "lit", "fast", "angular",
144
- "stencil", "alpine", "htmx",
145
- // Static site generators
146
- "hugo", "jekyll", "eleventy", "shopify",
147
- // Documentation
148
- "markdown", "mdx", "asciidoc",
149
- // Graphics
150
- "svg",
151
- // Data templates
152
- "yaml-template", "json-template"
153
- ];
154
- if (componentType && knownTypes.includes(componentType)) {
155
- return componentType;
156
- }
157
- // Framework-based defaults
158
- if (frameworkName === "laravel")
159
- return "blade";
160
- if (frameworkName === "rails")
161
- return "erb";
162
- if (frameworkName === "symfony")
163
- return "twig";
164
- if (frameworkName === "eleventy")
165
- return "eleventy";
166
- if (frameworkName === "aspnet")
167
- return "razor";
168
- if (frameworkName === "express")
169
- return "ejs";
170
- if (frameworkName === "flask")
171
- return "jinja";
172
- if (frameworkName === "django")
173
- return "django";
174
- if (frameworkName === "spring")
175
- return "thymeleaf";
176
- if (frameworkName === "go")
177
- return "go-template";
178
- if (frameworkName === "astro")
179
- return "astro";
180
- if (frameworkName === "hugo")
181
- return "hugo";
182
- if (frameworkName === "jekyll")
183
- return "jekyll";
184
- return "html";
185
- };
186
- // Check if any framework is a server-side framework
187
- const serverFramework = project.frameworks.find((f) => serverFrameworks.includes(f.name));
188
- if (serverFramework) {
189
- const templateTypes = [
190
- // Server-side templates
191
- "php", "blade", "erb", "twig", "html", "njk", "razor", "hbs",
192
- "mustache", "ejs", "pug", "liquid", "slim", "haml", "jinja",
193
- "django", "thymeleaf", "freemarker", "go-template", "edge",
194
- "eta", "heex", "velocity", "xslt",
195
- // JS frameworks
196
- "astro", "solid", "qwik", "marko", "lit", "fast", "angular",
197
- "stencil", "alpine", "htmx",
198
- // Static site generators
199
- "hugo", "jekyll", "eleventy", "shopify",
200
- // Documentation
201
- "markdown", "mdx", "asciidoc",
202
- // Graphics
203
- "svg",
204
- // Data templates
205
- "yaml-template", "json-template"
206
- ];
207
- const templateComponents = project.components.filter((c) => c.type && templateTypes.includes(c.type));
208
- if (templateComponents.length > 0) {
209
- // Use the first component's type to determine template type
210
- const templateType = getTemplateType(serverFramework.name, templateComponents[0]?.type);
211
- lines.push(` templates: {`);
212
- lines.push(` enabled: true,`);
213
- lines.push(` type: '${templateType}',`);
214
- lines.push(` include: [`);
215
- for (const comp of templateComponents) {
216
- lines.push(` '${comp.pattern}',`);
217
- }
218
- lines.push(` ],`);
219
- lines.push(` },`);
127
+ else {
128
+ results.configCreated = true;
129
+ console.log(` ${chalk.green("")} Config exists`);
130
+ }
131
+ // Step 2: Check/setup AI agents
132
+ const skillsDir = resolve(cwd, ".claude/skills/design-system");
133
+ const claudeMdPath = join(cwd, "CLAUDE.md");
134
+ const hasSkills = existsSync(skillsDir);
135
+ const hasClaudeMd = existsSync(claudeMdPath) && readFileSync(claudeMdPath, "utf-8").includes("Design System");
136
+ if (!hasSkills || !hasClaudeMd) {
137
+ console.log("");
138
+ console.log(chalk.yellow(" ⚠ AI agents not configured"));
139
+ if (!hasSkills)
140
+ console.log(chalk.dim(" • Missing skill files"));
141
+ if (!hasClaudeMd)
142
+ console.log(chalk.dim(" • Missing CLAUDE.md section"));
143
+ console.log("");
144
+ const shouldSetupAgents = options.yes || await promptConfirm(" Set up AI agents?", true);
145
+ if (shouldSetupAgents) {
146
+ console.log("");
147
+ await runAgentsDock({ json: options.json });
148
+ results.agentsSetup = true;
220
149
  }
221
150
  }
222
- // Storybook config
223
- if (project.storybook) {
224
- lines.push(` storybook: {`);
225
- lines.push(` enabled: true,`);
226
- lines.push(` },`);
227
- }
228
- // Token files config
229
- const tokenFiles = project.tokens.filter((t) => t.type !== "tailwind");
230
- const hasTailwind = project.tokens.some((t) => t.type === "tailwind");
231
- if (tokenFiles.length > 0 || hasTailwind) {
232
- lines.push(` tokens: {`);
233
- lines.push(` enabled: true,`);
234
- if (tokenFiles.length > 0) {
235
- lines.push(` files: [`);
236
- for (const token of tokenFiles) {
237
- lines.push(` '${token.path}',`);
238
- }
239
- lines.push(` ],`);
151
+ else {
152
+ console.log(` ${chalk.green("✓")} AI agents configured`);
153
+ results.agentsSetup = true;
154
+ }
155
+ // Step 3: Check/setup hooks
156
+ const hasHooks = existsSync(join(cwd, ".husky/pre-commit")) ||
157
+ existsSync(join(cwd, "lefthook.yml")) ||
158
+ existsSync(join(cwd, ".git/hooks/pre-commit"));
159
+ if (!hasHooks) {
160
+ console.log("");
161
+ console.log(chalk.yellow(" ⚠ No pre-commit hooks"));
162
+ console.log("");
163
+ const shouldSetupHooks = options.yes || await promptConfirm(" Set up hooks?", true);
164
+ if (shouldSetupHooks) {
165
+ await runHooksDock({});
166
+ results.hooksSetup = true;
240
167
  }
241
- lines.push(` },`);
242
168
  }
243
- // Figma placeholder (always disabled by default)
244
- lines.push(` figma: {`);
245
- lines.push(` enabled: false,`);
246
- lines.push(` // accessToken: process.env.FIGMA_ACCESS_TOKEN,`);
247
- lines.push(` // fileKeys: [],`);
248
- lines.push(` },`);
249
- lines.push(` },`);
250
- lines.push(` output: {`);
251
- lines.push(` format: 'table',`);
252
- lines.push(` colors: true,`);
253
- lines.push(` },`);
254
- lines.push(`};`);
255
- lines.push(``);
256
- return lines.join("\n");
257
- }
258
- function printDetectionResults(project) {
259
- console.log("");
260
- console.log(chalk.bold(" Detected:"));
261
- const frameworkNames = {
262
- // JS frameworks
263
- react: "React",
264
- vue: "Vue",
265
- svelte: "Svelte",
266
- angular: "Angular",
267
- solid: "Solid",
268
- preact: "Preact",
269
- // Meta-frameworks
270
- nextjs: "Next.js",
271
- nuxt: "Nuxt",
272
- astro: "Astro",
273
- remix: "Remix",
274
- sveltekit: "SvelteKit",
275
- gatsby: "Gatsby",
276
- // Mobile
277
- "react-native": "React Native",
278
- flutter: "Flutter",
279
- expo: "Expo",
280
- // Web Components
281
- lit: "Lit",
282
- stencil: "Stencil",
283
- // Server-side
284
- php: "PHP",
285
- laravel: "Laravel",
286
- symfony: "Symfony",
287
- rails: "Ruby on Rails",
288
- django: "Django",
289
- flask: "Flask",
290
- fastapi: "FastAPI",
291
- express: "Express",
292
- nestjs: "NestJS",
293
- spring: "Spring Boot",
294
- aspnet: "ASP.NET",
295
- go: "Go",
296
- // Static site generators
297
- hugo: "Hugo",
298
- jekyll: "Jekyll",
299
- eleventy: "Eleventy",
300
- };
301
- // Frameworks - show all detected
302
- if (project.frameworks.length > 0) {
303
- // Show warning if multiple UI frameworks detected (framework sprawl)
304
- const uiFrameworks = [
305
- "react",
306
- "vue",
307
- "svelte",
308
- "angular",
309
- "solid",
310
- "preact",
311
- "lit",
312
- "stencil",
313
- "nextjs",
314
- "nuxt",
315
- "astro",
316
- "remix",
317
- "sveltekit",
318
- "gatsby",
319
- "react-native",
320
- "expo",
321
- "flutter",
322
- ];
323
- const uiCount = project.frameworks.filter((f) => uiFrameworks.includes(f.name)).length;
324
- if (uiCount > 1) {
325
- console.log(chalk.yellow(" ⚠ ") +
326
- chalk.yellow.bold("Multiple UI frameworks detected (framework sprawl)"));
327
- }
328
- for (const framework of project.frameworks) {
329
- const ts = framework.typescript ? " + TypeScript" : "";
330
- const frameworkName = frameworkNames[framework.name] || capitalize(framework.name);
331
- const meta = framework.meta ? chalk.dim(` (${framework.meta})`) : "";
332
- const version = framework.version !== "unknown" ? ` ${framework.version}` : "";
333
- console.log(chalk.green(" ✓ ") +
334
- chalk.bold(frameworkName) +
335
- ts +
336
- meta +
337
- chalk.dim(version));
338
- }
339
- }
340
- // Components
341
- if (project.components.length > 0) {
342
- for (const comp of project.components) {
343
- const typeLabels = {
344
- // JS frameworks
345
- jsx: "component files",
346
- tsx: "TypeScript components",
347
- vue: "Vue components",
348
- svelte: "Svelte components",
349
- astro: "Astro components",
350
- solid: "Solid components",
351
- qwik: "Qwik components",
352
- marko: "Marko components",
353
- lit: "Lit elements",
354
- fast: "FAST elements",
355
- // Server-side templates
356
- php: "PHP templates",
357
- blade: "Blade templates",
358
- erb: "ERB templates",
359
- twig: "Twig templates",
360
- html: "HTML templates",
361
- njk: "Nunjucks templates",
362
- razor: "Razor views",
363
- hbs: "Handlebars templates",
364
- mustache: "Mustache templates",
365
- ejs: "EJS templates",
366
- pug: "Pug templates",
367
- liquid: "Liquid templates",
368
- slim: "Slim templates",
369
- haml: "Haml templates",
370
- jinja: "Jinja templates",
371
- django: "Django templates",
372
- thymeleaf: "Thymeleaf templates",
373
- freemarker: "Freemarker templates",
374
- velocity: "Velocity templates",
375
- "go-template": "Go templates",
376
- edge: "Edge.js templates",
377
- eta: "Eta templates",
378
- heex: "HEEx templates",
379
- xslt: "XSLT stylesheets",
380
- // Static site generators
381
- hugo: "Hugo layouts",
382
- jekyll: "Jekyll layouts",
383
- eleventy: "Eleventy templates",
384
- shopify: "Shopify templates",
385
- // Documentation
386
- markdown: "Markdown files",
387
- mdx: "MDX files",
388
- asciidoc: "AsciiDoc files",
389
- // Data templates
390
- "yaml-template": "YAML templates",
391
- "json-template": "JSON templates",
392
- // Additional JS frameworks
393
- angular: "Angular components",
394
- stencil: "Stencil components",
395
- alpine: "Alpine.js templates",
396
- htmx: "HTMX templates",
397
- // Graphics
398
- svg: "SVG components",
399
- };
400
- const typeLabel = comp.type
401
- ? typeLabels[comp.type] || "template files"
402
- : "component files";
403
- console.log(chalk.green(" ✓ ") +
404
- `${comp.fileCount} ${typeLabel} in ` +
405
- chalk.cyan(comp.path));
406
- }
407
- }
408
- // Tokens
409
- if (project.tokens.length > 0) {
410
- for (const token of project.tokens) {
411
- console.log(chalk.green(" ✓ ") + `${token.name}: ` + chalk.cyan(token.path));
412
- }
413
- }
414
- // Storybook
415
- if (project.storybook) {
416
- const version = project.storybook.version
417
- ? ` (${project.storybook.version})`
418
- : "";
419
- console.log(chalk.green(" ✓ ") + `Storybook` + chalk.dim(version));
420
- }
421
- // Design system
422
- if (project.designSystem) {
423
- console.log(chalk.green(" ✓ ") +
424
- `Design system: ` +
425
- chalk.cyan(project.designSystem.package));
426
- }
427
- // Monorepo
428
- if (project.monorepo) {
429
- // Show basic monorepo info
430
- console.log(chalk.green(" ✓ ") +
431
- capitalize(project.monorepo.type) +
432
- ` monorepo (${project.monorepo.packages.length} packages)`);
433
- // Show monorepo component paths if detected
434
- const monorepoComponents = project.components.filter((c) => c.path.startsWith("packages/") ||
435
- c.path.startsWith("apps/") ||
436
- c.path.startsWith("libs/") ||
437
- c.path.startsWith("modules/"));
438
- if (monorepoComponents.length > 0) {
439
- console.log(chalk.dim(" ") +
440
- chalk.dim(`Scanning: ${monorepoComponents.map((c) => c.path).slice(0, 3).join(", ")}${monorepoComponents.length > 3 ? ` +${monorepoComponents.length - 3} more` : ""}`));
441
- }
442
- }
443
- // Design system documentation tools
444
- if (project.designSystemDocs) {
445
- const typeNames = {
446
- 'zeroheight': 'Zeroheight',
447
- 'supernova': 'Supernova',
448
- 'specify': 'Specify',
449
- 'knapsack': 'Knapsack',
450
- 'framer': 'Framer',
451
- 'tokenforge': 'TokenForge',
452
- 'tokens-studio': 'Tokens Studio (Figma Tokens)',
453
- };
454
- const name = typeNames[project.designSystemDocs.type] || project.designSystemDocs.type;
455
- const path = project.designSystemDocs.exportPath || project.designSystemDocs.configPath || '';
456
- console.log(chalk.green(" ✓ ") +
457
- chalk.bold(name) +
458
- (path ? chalk.dim(` (${path})`) : ''));
459
- }
460
- // Nothing found
461
- if (project.frameworks.length === 0 &&
462
- project.components.length === 0 &&
463
- project.tokens.length === 0 &&
464
- !project.storybook &&
465
- !project.designSystemDocs) {
466
- console.log(chalk.yellow(" ⚠ ") + "No sources auto-detected");
467
- console.log(chalk.dim(" You can manually configure sources in buoy.config.mjs"));
169
+ else {
170
+ console.log(` ${chalk.green("✓")} Hooks configured`);
171
+ results.hooksSetup = true;
468
172
  }
173
+ // Summary
469
174
  console.log("");
470
- }
471
- function capitalize(s) {
472
- return s.charAt(0).toUpperCase() + s.slice(1);
473
- }
474
- /**
475
- * Import tokens from a detected source and generate output file
476
- */
477
- async function importTokensFromSource(sourcePath, outputPath) {
478
- try {
479
- const content = readFileSync(sourcePath, 'utf-8');
480
- const ext = extname(sourcePath).toLowerCase();
481
- let tokens = [];
482
- if (ext === '.json') {
483
- const json = JSON.parse(content);
484
- const format = detectFormat(json);
485
- info(`Detected format: ${formatTokenFormat(format)}`);
486
- tokens = parseTokenFile(content);
487
- }
488
- else if (ext === '.css') {
489
- tokens = parseCssVariablesForInit(content, sourcePath);
175
+ console.log(chalk.dim(" ─".repeat(25)));
176
+ console.log("");
177
+ if (options.json) {
178
+ console.log(JSON.stringify(results, null, 2));
179
+ }
180
+ else {
181
+ const allDone = results.configCreated && results.agentsSetup && results.hooksSetup;
182
+ if (allDone) {
183
+ console.log(chalk.green.bold(" ✓ Buoy is docked!"));
184
+ console.log("");
185
+ console.log(chalk.dim(" Try:"));
186
+ console.log(` ${chalk.cyan("buoy show all")} # See your design system`);
187
+ console.log(` ${chalk.cyan("buoy show drift")} # Check for drift`);
490
188
  }
491
189
  else {
492
- return { success: false, tokenCount: 0 };
493
- }
494
- if (tokens.length === 0) {
495
- return { success: false, tokenCount: 0 };
190
+ console.log(chalk.yellow(" Partially docked"));
191
+ console.log("");
192
+ if (!results.configCreated)
193
+ console.log(` Run ${chalk.cyan("buoy dock config")}`);
194
+ if (!results.agentsSetup)
195
+ console.log(` Run ${chalk.cyan("buoy dock agents")}`);
196
+ if (!results.hooksSetup)
197
+ console.log(` Run ${chalk.cyan("buoy dock hooks")}`);
496
198
  }
497
- // Generate CSS output
498
- const outputContent = generateCssFromTokens(tokens);
499
- writeFileSync(outputPath, outputContent);
500
- return { success: true, tokenCount: tokens.length };
501
- }
502
- catch {
503
- return { success: false, tokenCount: 0 };
504
199
  }
200
+ console.log("");
505
201
  }
506
202
  /**
507
- * Parse CSS variables for token import
203
+ * Config dock - creates buoy.config.mjs
508
204
  */
509
- function parseCssVariablesForInit(content, filePath) {
510
- const tokens = [];
511
- const varRegex = /--([\\w-]+)\\s*:\\s*([^;]+);/g;
512
- let match;
513
- while ((match = varRegex.exec(content)) !== null) {
514
- const name = match[1];
515
- const value = match[2].trim();
516
- tokens.push({
517
- id: `css:${filePath}:${name}`,
518
- name: `--${name}`,
519
- category: inferTokenCategory(name, value),
520
- value: parseTokenValue(value),
521
- source: { type: 'css', path: filePath },
522
- aliases: [],
523
- usedBy: [],
524
- metadata: {},
525
- scannedAt: new Date(),
526
- });
205
+ async function runConfigDock(options) {
206
+ const cwd = process.cwd();
207
+ const configFilePath = resolve(cwd, "buoy.config.mjs");
208
+ if (existsSync(configFilePath) && !options.force) {
209
+ warning(`Config already exists at ${configFilePath}`);
210
+ info("Use --force to overwrite");
211
+ return;
527
212
  }
528
- return tokens;
529
- }
530
- /**
531
- * Infer token category from name or value
532
- */
533
- function inferTokenCategory(name, value) {
534
- const nameLower = name.toLowerCase();
535
- if (nameLower.includes('color') || value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl')) {
536
- return 'color';
213
+ let project;
214
+ if (options.skipDetect) {
215
+ const detector = new ProjectDetector(cwd);
216
+ project = {
217
+ name: (await detector.detect()).name,
218
+ root: cwd,
219
+ frameworks: [],
220
+ primaryFramework: null,
221
+ components: [],
222
+ tokens: [],
223
+ storybook: null,
224
+ designSystem: null,
225
+ monorepo: null,
226
+ designSystemDocs: null,
227
+ };
537
228
  }
538
- if (nameLower.includes('spacing') || nameLower.includes('space') || nameLower.includes('gap')) {
539
- return 'spacing';
229
+ else {
230
+ const spin = ora("Scanning project...").start();
231
+ try {
232
+ const detector = new ProjectDetector(cwd);
233
+ project = await detector.detect();
234
+ spin.stop();
235
+ printDetectionResults(project);
236
+ }
237
+ catch (err) {
238
+ spin.fail("Detection failed");
239
+ error(err instanceof Error ? err.message : String(err));
240
+ process.exit(1);
241
+ }
540
242
  }
541
- if (nameLower.includes('font') || nameLower.includes('text') || nameLower.includes('size')) {
542
- return 'typography';
243
+ // Show frameworks and offer plugin install
244
+ const detectedFrameworks = await detectFrameworks(cwd);
245
+ if (detectedFrameworks.length > 0) {
246
+ await showFrameworksAndPlugins(detectedFrameworks, options.yes);
543
247
  }
544
- if (nameLower.includes('radius') || nameLower.includes('rounded')) {
545
- return 'border';
248
+ // Generate and write config
249
+ const content = generateConfig(project);
250
+ try {
251
+ writeFileSync(configFilePath, content, "utf-8");
252
+ success("Created buoy.config.mjs");
546
253
  }
547
- if (nameLower.includes('shadow')) {
548
- return 'shadow';
254
+ catch (err) {
255
+ error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
256
+ process.exit(1);
549
257
  }
550
- return 'other';
551
258
  }
552
259
  /**
553
- * Parse a CSS value into TokenValue
260
+ * Agents dock - sets up AI agents (skills + context + commands)
554
261
  */
555
- function parseTokenValue(value) {
556
- if (value.startsWith('#') || value.startsWith('rgb') || value.startsWith('hsl')) {
557
- return { type: 'color', hex: value.startsWith('#') ? value.toLowerCase() : value };
262
+ async function runAgentsDock(options) {
263
+ const cwd = process.cwd();
264
+ if (options.json)
265
+ setJsonMode(true);
266
+ const spin = spinner("Setting up AI agents...");
267
+ try {
268
+ const { config, projectName } = await loadOrBuildConfig(cwd);
269
+ spin.text = "Scanning...";
270
+ const orchestrator = new ScanOrchestrator(config, cwd);
271
+ const scanResult = await orchestrator.scan({ onProgress: (msg) => { spin.text = msg; } });
272
+ spin.text = "Analyzing...";
273
+ const { SemanticDiffEngine } = await import("@buoy-design/core/analysis");
274
+ const engine = new SemanticDiffEngine();
275
+ const diffResult = engine.analyzeComponents(scanResult.components, {
276
+ checkDeprecated: true,
277
+ checkNaming: true,
278
+ checkDocumentation: true,
279
+ });
280
+ spin.stop();
281
+ const results = {
282
+ skillCreated: false,
283
+ contextUpdated: false,
284
+ commandsInstalled: [],
285
+ stats: { tokens: 0, components: 0 },
286
+ };
287
+ // Create skill files
288
+ const skillPath = resolve(cwd, ".claude/skills/design-system");
289
+ if (options.dryRun) {
290
+ console.log(chalk.dim(" Would create: " + skillPath));
291
+ }
292
+ else {
293
+ const exportService = new SkillExportService(projectName);
294
+ const skillResult = await exportService.export({ tokens: scanResult.tokens, components: scanResult.components, drifts: diffResult.drifts, projectName }, { sections: ["tokens", "components", "patterns", "anti-patterns"], outputPath: skillPath });
295
+ for (const file of skillResult.files) {
296
+ const dir = dirname(file.path);
297
+ if (!existsSync(dir))
298
+ mkdirSync(dir, { recursive: true });
299
+ writeFileSync(file.path, file.content);
300
+ }
301
+ results.skillCreated = true;
302
+ results.stats.tokens = skillResult.stats.tokens.total;
303
+ results.stats.components = skillResult.stats.components;
304
+ }
305
+ // Update CLAUDE.md
306
+ const claudeMdPath = join(cwd, "CLAUDE.md");
307
+ if (options.dryRun) {
308
+ console.log(chalk.dim(" Would update: CLAUDE.md"));
309
+ }
310
+ else {
311
+ const contextResult = generateContext({ tokens: scanResult.tokens, components: scanResult.components, drifts: diffResult.drifts, projectName }, { detailLevel: "standard" });
312
+ let existingContent = "";
313
+ let hasSection = false;
314
+ if (existsSync(claudeMdPath)) {
315
+ existingContent = readFileSync(claudeMdPath, "utf-8");
316
+ hasSection = /^##?\s*[Dd]esign\s*[Ss]ystem/m.test(existingContent.replace(/```[\s\S]*?```/g, ""));
317
+ }
318
+ if (!hasSection) {
319
+ const section = `\n## Design System\n\n${contextResult.content}\n`;
320
+ if (existingContent) {
321
+ appendFileSync(claudeMdPath, section);
322
+ }
323
+ else {
324
+ writeFileSync(claudeMdPath, `# CLAUDE.md\n${section}`);
325
+ }
326
+ results.contextUpdated = true;
327
+ }
328
+ }
329
+ // Install commands
330
+ if (!options.dryRun) {
331
+ const { installed } = installClaudeCommands(false);
332
+ results.commandsInstalled = installed;
333
+ }
334
+ if (options.json) {
335
+ console.log(JSON.stringify(results, null, 2));
336
+ }
337
+ else if (!options.dryRun) {
338
+ console.log("");
339
+ if (results.skillCreated)
340
+ console.log(` ${chalk.green("✓")} Created skill files`);
341
+ if (results.contextUpdated)
342
+ console.log(` ${chalk.green("✓")} Updated CLAUDE.md`);
343
+ if (results.commandsInstalled.length > 0) {
344
+ console.log(` ${chalk.green("✓")} Installed: ${results.commandsInstalled.map(c => `/${c}`).join(", ")}`);
345
+ }
346
+ console.log("");
347
+ }
558
348
  }
559
- const dimMatch = value.match(/^([\d.]+)(px|rem|em|%)$/);
560
- if (dimMatch) {
561
- return { type: 'spacing', value: parseFloat(dimMatch[1]), unit: dimMatch[2] };
349
+ catch (err) {
350
+ spin.stop();
351
+ error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
352
+ process.exit(1);
562
353
  }
563
- return { type: 'raw', value };
564
354
  }
565
355
  /**
566
- * Generate CSS file from tokens
356
+ * Skills dock - creates skill files only
567
357
  */
568
- function generateCssFromTokens(tokens) {
569
- const lines = [
570
- '/**',
571
- ' * Design Tokens',
572
- ' * Imported by Buoy',
573
- ' */',
574
- '',
575
- ':root {',
576
- ];
577
- // Group by category
578
- const groups = {};
579
- for (const token of tokens) {
580
- const cat = token.category;
581
- if (!groups[cat])
582
- groups[cat] = [];
583
- groups[cat].push(token);
584
- }
585
- for (const [category, catTokens] of Object.entries(groups)) {
586
- lines.push(` /* ${capitalize(category)} */`);
587
- for (const token of catTokens) {
588
- const name = token.name.startsWith('--') ? token.name : `--${token.name}`;
589
- const value = getTokenDisplayValue(token);
590
- lines.push(` ${name}: ${value};`);
591
- }
592
- lines.push('');
593
- }
594
- lines.push('}');
595
- return lines.join('\\n');
596
- }
597
- /**
598
- * Get display value for a token
599
- */
600
- function getTokenDisplayValue(token) {
601
- const v = token.value;
602
- if (v.type === 'color')
603
- return v.hex;
604
- if (v.type === 'spacing')
605
- return `${v.value}${v.unit}`;
606
- if (v.type === 'raw')
607
- return v.value;
608
- return JSON.stringify(v);
358
+ async function runSkillsDock(options) {
359
+ const cwd = process.cwd();
360
+ if (options.json)
361
+ setJsonMode(true);
362
+ const spin = spinner("Creating skills...");
363
+ try {
364
+ const { config, projectName } = await loadOrBuildConfig(cwd);
365
+ spin.text = "Scanning...";
366
+ const orchestrator = new ScanOrchestrator(config, cwd);
367
+ const scanResult = await orchestrator.scan({ onProgress: (msg) => { spin.text = msg; } });
368
+ spin.text = "Analyzing...";
369
+ const { SemanticDiffEngine } = await import("@buoy-design/core/analysis");
370
+ const engine = new SemanticDiffEngine();
371
+ const diffResult = engine.analyzeComponents(scanResult.components, {});
372
+ spin.stop();
373
+ let outputPath = options.output || ".claude/skills/design-system";
374
+ if (options.global)
375
+ outputPath = join(homedir(), ".claude", "skills", projectName);
376
+ outputPath = resolve(cwd, outputPath);
377
+ if (options.dryRun) {
378
+ console.log(chalk.dim(`Would create: ${outputPath}`));
379
+ return;
380
+ }
381
+ const exportService = new SkillExportService(projectName);
382
+ const result = await exportService.export({ tokens: scanResult.tokens, components: scanResult.components, drifts: diffResult.drifts, projectName }, { sections: ["tokens", "components", "patterns", "anti-patterns"], outputPath });
383
+ for (const file of result.files) {
384
+ const dir = dirname(file.path);
385
+ if (!existsSync(dir))
386
+ mkdirSync(dir, { recursive: true });
387
+ writeFileSync(file.path, file.content);
388
+ }
389
+ if (options.json) {
390
+ console.log(JSON.stringify({ path: outputPath, stats: result.stats }, null, 2));
391
+ }
392
+ else {
393
+ success(`Created skills at ${outputPath}`);
394
+ }
395
+ }
396
+ catch (err) {
397
+ spin.stop();
398
+ error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
399
+ process.exit(1);
400
+ }
609
401
  }
610
402
  /**
611
- * Format token format name for display
403
+ * Context dock - generates CLAUDE.md section
612
404
  */
613
- function formatTokenFormat(format) {
614
- const names = {
615
- dtcg: 'W3C Design Tokens (DTCG)',
616
- 'tokens-studio': 'Tokens Studio (Figma Tokens)',
617
- 'style-dictionary': 'Style Dictionary',
618
- };
619
- return names[format] || format;
620
- }
621
- async function promptConfirm(message, defaultValue = true) {
622
- const rl = createInterface({
623
- input: process.stdin,
624
- output: process.stdout,
625
- });
626
- const suffix = defaultValue ? "[Y/n]" : "[y/N]";
627
- return new Promise((resolve) => {
628
- rl.question(`${message} ${suffix} `, (answer) => {
629
- rl.close();
630
- const trimmed = answer.trim().toLowerCase();
631
- if (trimmed === "") {
632
- resolve(defaultValue);
405
+ async function runContextDock(options) {
406
+ const cwd = process.cwd();
407
+ if (options.json)
408
+ setJsonMode(true);
409
+ const spin = spinner("Generating context...");
410
+ try {
411
+ const { config, projectName } = await loadOrBuildConfig(cwd);
412
+ spin.text = "Scanning...";
413
+ const orchestrator = new ScanOrchestrator(config, cwd);
414
+ const scanResult = await orchestrator.scan();
415
+ spin.text = "Analyzing...";
416
+ const { SemanticDiffEngine } = await import("@buoy-design/core/analysis");
417
+ const engine = new SemanticDiffEngine();
418
+ const diffResult = engine.analyzeComponents(scanResult.components, { availableTokens: scanResult.tokens });
419
+ spin.stop();
420
+ const contextResult = generateContext({ tokens: scanResult.tokens, components: scanResult.components, drifts: diffResult.drifts, projectName }, { detailLevel: options.detail || "standard" });
421
+ if (options.json) {
422
+ console.log(JSON.stringify({ content: contextResult.content, stats: contextResult.stats }, null, 2));
423
+ return;
424
+ }
425
+ if (options.stdout) {
426
+ console.log(contextResult.content);
427
+ return;
428
+ }
429
+ const claudeMdPath = options.output || join(cwd, "CLAUDE.md");
430
+ const header = "## Design System\n\n";
431
+ if (existsSync(claudeMdPath)) {
432
+ const existing = readFileSync(claudeMdPath, "utf-8");
433
+ if (!existing.includes("## Design System")) {
434
+ appendFileSync(claudeMdPath, `\n${header}${contextResult.content}\n`);
435
+ success("Appended to CLAUDE.md");
633
436
  }
634
437
  else {
635
- resolve(trimmed === "y" || trimmed === "yes");
438
+ warning("CLAUDE.md already has Design System section");
636
439
  }
637
- });
638
- });
440
+ }
441
+ else {
442
+ writeFileSync(claudeMdPath, `# CLAUDE.md\n\n${header}${contextResult.content}\n`);
443
+ success("Created CLAUDE.md");
444
+ }
445
+ }
446
+ catch (err) {
447
+ spin.stop();
448
+ error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
449
+ process.exit(1);
450
+ }
639
451
  }
640
- export function createDockCommand() {
641
- const cmd = new Command("dock")
642
- .description("Dock Buoy into your project")
643
- .option("-f, --force", "Overwrite existing configuration")
644
- .option("-n, --name <name>", "Project name")
645
- .option("--skip-detect", "Skip auto-detection and create minimal config")
646
- .option("-y, --yes", "Auto-install recommended plugins without prompting")
647
- .option("--no-install", "Skip plugin installation prompts")
648
- .option("--hooks", "Setup pre-commit hook for drift checking")
649
- .action(async (options) => {
650
- const cwd = process.cwd();
651
- const configPath = resolve(cwd, "buoy.config.mjs");
652
- // Check if config already exists
653
- if (existsSync(configPath) && !options.force) {
654
- warning(`Configuration already exists at ${configPath}`);
655
- info("Use --force to overwrite");
656
- return;
452
+ /**
453
+ * Hooks dock - sets up git hooks
454
+ */
455
+ async function runHooksDock(options) {
456
+ const cwd = process.cwd();
457
+ console.log("");
458
+ const hookSystem = detectHookSystem(cwd);
459
+ if (hookSystem) {
460
+ info(`Detected: ${hookSystem}`);
461
+ const hookResult = setupHooks(cwd);
462
+ if (hookResult.success) {
463
+ success(hookResult.message);
657
464
  }
658
- let project;
659
- if (options.skipDetect) {
660
- // Minimal detection - just get the project name
661
- const detector = new ProjectDetector(cwd);
662
- project = {
663
- name: options.name || (await detector.detect()).name,
664
- root: cwd,
665
- frameworks: [],
666
- primaryFramework: null,
667
- components: [],
668
- tokens: [],
669
- storybook: null,
670
- designSystem: null,
671
- monorepo: null,
672
- designSystemDocs: null,
673
- };
465
+ else {
466
+ warning(hookResult.message);
467
+ }
468
+ }
469
+ else {
470
+ const standaloneResult = generateStandaloneHook(cwd);
471
+ if (standaloneResult.success) {
472
+ success(standaloneResult.message);
473
+ info("Copy to .git/hooks/pre-commit to activate");
674
474
  }
675
475
  else {
676
- // Run auto-detection
677
- const spinner = ora("Scanning project...").start();
678
- try {
679
- const detector = new ProjectDetector(cwd);
680
- project = await detector.detect();
681
- if (options.name) {
682
- project.name = options.name;
683
- }
684
- spinner.stop();
685
- printDetectionResults(project);
686
- }
687
- catch (err) {
688
- spinner.fail("Detection failed");
689
- const message = err instanceof Error ? err.message : String(err);
690
- error(message);
691
- process.exit(1);
692
- }
476
+ warning(standaloneResult.message);
693
477
  }
694
- // Track imported token file path for config generation
695
- let importedTokenFile = null;
696
- // Check if design system docs were detected and offer to import tokens
697
- if (project.designSystemDocs && !options.skipDetect) {
698
- const docType = project.designSystemDocs.type;
699
- const sourcePath = project.designSystemDocs.exportPath || project.designSystemDocs.configPath;
700
- const typeNames = {
701
- 'zeroheight': 'Zeroheight',
702
- 'supernova': 'Supernova',
703
- 'specify': 'Specify',
704
- 'knapsack': 'Knapsack',
705
- 'framer': 'Framer',
706
- 'tokenforge': 'TokenForge',
707
- 'tokens-studio': 'Tokens Studio',
708
- };
709
- const docName = typeNames[docType] || docType;
710
- if (sourcePath && existsSync(resolve(cwd, sourcePath))) {
711
- console.log(chalk.bold(" Token Import"));
712
- console.log("");
713
- console.log(` Found ${chalk.cyan(docName)} tokens at ${chalk.dim(sourcePath)}`);
714
- console.log("");
715
- let shouldImport = false;
716
- if (options.yes) {
717
- shouldImport = true;
718
- }
719
- else if (process.stdin.isTTY) {
720
- shouldImport = await promptConfirm(` Import tokens from ${docName}?`, true);
721
- }
722
- if (shouldImport) {
723
- const outputFile = 'design-tokens.css';
724
- const importSpinner = ora(`Importing tokens from ${docName}...`).start();
725
- const result = await importTokensFromSource(resolve(cwd, sourcePath), resolve(cwd, outputFile));
726
- if (result.success) {
727
- importSpinner.succeed(`Imported ${result.tokenCount} tokens to ${outputFile}`);
728
- importedTokenFile = outputFile;
729
- // Add to project tokens for config generation
730
- project.tokens.push({
731
- name: 'Design Tokens',
732
- path: outputFile,
733
- type: 'css',
734
- });
735
- }
736
- else {
737
- importSpinner.warn('Could not parse tokens from source');
738
- info(`You can manually import later with: ${chalk.cyan(`buoy import ${sourcePath}`)}`);
739
- }
740
- console.log("");
741
- }
742
- }
478
+ }
479
+ if (options.claude) {
480
+ console.log("");
481
+ const { config, projectName } = await loadOrBuildConfig(cwd);
482
+ const orchestrator = new ScanOrchestrator(config, cwd);
483
+ const scanResult = await orchestrator.scan();
484
+ const { SemanticDiffEngine } = await import("@buoy-design/core/analysis");
485
+ const engine = new SemanticDiffEngine();
486
+ const diffResult = engine.analyzeComponents(scanResult.components, {});
487
+ const exportService = new SkillExportService(projectName);
488
+ const condensedContext = exportService.generateCondensedContext({
489
+ tokens: scanResult.tokens,
490
+ components: scanResult.components,
491
+ drifts: diffResult.drifts,
492
+ projectName,
493
+ });
494
+ const claudeDir = resolve(cwd, ".claude");
495
+ if (!existsSync(claudeDir))
496
+ mkdirSync(claudeDir, { recursive: true });
497
+ writeFileSync(resolve(claudeDir, "buoy-context.md"), condensedContext);
498
+ const claudeResult = setupClaudeHooks(cwd);
499
+ if (claudeResult.success) {
500
+ success("Created Claude hooks");
743
501
  }
744
- // Show detected frameworks and scanners
745
- const detectedFrameworks = await detectFrameworks(cwd);
746
- if (detectedFrameworks.length > 0) {
747
- // Separate built-in scanners from optional plugins
748
- const builtIn = detectedFrameworks.filter((fw) => fw.scanner);
749
- const optionalPlugins = detectedFrameworks.filter((fw) => fw.plugin && !fw.scanner);
750
- // Show built-in scanners (no install needed)
751
- if (builtIn.length > 0) {
752
- console.log(chalk.bold(" Built-in Scanners") +
753
- chalk.dim(" (no install needed)"));
754
- console.log("");
755
- for (const fw of builtIn) {
756
- const scannerInfo = BUILTIN_SCANNERS[fw.scanner];
757
- const scannerLabel = scannerInfo?.description || capitalize(fw.name);
758
- console.log(` ${chalk.green("✓")} ${chalk.cyan.bold(scannerLabel)}`);
759
- console.log(` ${chalk.dim(fw.evidence)}`);
760
- console.log("");
761
- }
762
- }
763
- // Show optional plugins (need install)
764
- if (optionalPlugins.length > 0) {
765
- console.log(chalk.bold(" Optional Plugins"));
766
- console.log("");
767
- for (const fw of optionalPlugins) {
768
- const pluginInfo = PLUGIN_INFO[fw.plugin];
769
- const pluginName = pluginInfo?.name || `@buoy-design/plugin-${fw.plugin}`;
770
- console.log(` ${chalk.dim("┌")} ${chalk.cyan.bold(pluginName)}`);
771
- console.log(` ${chalk.dim("│")}`);
772
- // What was detected
773
- const detectsLabel = pluginInfo?.detects || capitalize(fw.name);
774
- console.log(` ${chalk.dim("│")} ${chalk.white("Detected:")} ${detectsLabel} ${chalk.dim(`(${fw.evidence.toLowerCase()})`)}`);
775
- console.log(` ${chalk.dim("│")}`);
776
- // What the plugin does
777
- if (pluginInfo?.description) {
778
- console.log(` ${chalk.dim("│")} ${chalk.dim(pluginInfo.description)}`);
779
- }
780
- console.log(` ${chalk.dim("└─")} ${chalk.dim(getPluginInstallCommand([fw.plugin]))}`);
781
- console.log("");
782
- }
783
- const missingPlugins = optionalPlugins
784
- .map((fw) => fw.plugin)
785
- .filter((plugin, index, self) => self.indexOf(plugin) === index);
786
- if (missingPlugins.length > 0) {
787
- console.log(chalk.dim(" " + "─".repeat(65)));
788
- console.log("");
789
- console.log(chalk.bold(" Install all optional plugins:"));
790
- console.log(` ${chalk.cyan(getPluginInstallCommand(missingPlugins))}`);
791
- console.log("");
792
- // Determine if we should install plugins
793
- let shouldInstall = false;
794
- if (options.yes) {
795
- shouldInstall = true;
796
- }
797
- else if (options.install !== false) {
798
- // Only prompt if --no-install was not passed and stdin is a TTY
799
- if (process.stdin.isTTY) {
800
- shouldInstall = await promptConfirm("Install optional plugins now?", true);
801
- }
802
- }
803
- if (shouldInstall) {
804
- const { execSync } = await import("node:child_process");
805
- console.log("");
806
- console.log("Installing plugins...");
807
- try {
808
- execSync(getPluginInstallCommand(missingPlugins), {
809
- stdio: "inherit",
810
- });
811
- success("Plugins installed successfully");
812
- }
813
- catch {
814
- warning("Plugin installation failed. You can install manually with the command above.");
815
- }
816
- }
817
- }
818
- }
819
- console.log("");
502
+ else {
503
+ warning(`Claude hooks failed: ${claudeResult.message}`);
820
504
  }
821
- // Generate and write config
822
- const content = generateConfig(project);
823
- try {
824
- writeFileSync(configPath, content, "utf-8");
825
- success(`Created buoy.config.mjs`);
826
- // Setup hooks if --hooks flag is provided
827
- if (options.hooks) {
828
- console.log("");
829
- const hookSystem = detectHookSystem(cwd);
830
- if (hookSystem) {
831
- info(`Detected hook system: ${hookSystem}`);
832
- const hookResult = setupHooks(cwd);
833
- if (hookResult.success) {
834
- success(hookResult.message);
835
- }
836
- else {
837
- warning(hookResult.message);
838
- }
839
- }
840
- else {
841
- // No hook system detected, create standalone hook
842
- const standaloneResult = generateStandaloneHook(cwd);
843
- if (standaloneResult.success) {
844
- success(standaloneResult.message);
845
- info("To use this hook, copy it to .git/hooks/pre-commit or configure your hook system");
846
- }
847
- else {
848
- warning(standaloneResult.message);
849
- }
850
- }
851
- }
852
- console.log("");
853
- info("Next steps:");
854
- let stepNum = 1;
855
- if (importedTokenFile) {
856
- info(` ${stepNum}. Review imported tokens in ${chalk.cyan(importedTokenFile)}`);
857
- stepNum++;
858
- }
859
- info(` ${stepNum}. Run ${chalk.cyan("buoy sweep")} to scan your codebase`);
860
- stepNum++;
861
- info(` ${stepNum}. Run ${chalk.cyan("buoy drift check")} to detect drift`);
862
- stepNum++;
863
- if (!options.hooks) {
864
- info(` ${stepNum}. Run ` +
865
- chalk.cyan("buoy dock --hooks") +
866
- " to setup pre-commit hooks");
867
- stepNum++;
505
+ }
506
+ console.log("");
507
+ }
508
+ // ============ Helpers ============
509
+ async function loadOrBuildConfig(cwd) {
510
+ const existingConfigPath = getConfigPath();
511
+ if (existingConfigPath) {
512
+ const result = await loadConfig();
513
+ return { config: result.config, projectName: result.config.project?.name || "design-system" };
514
+ }
515
+ const autoResult = await buildAutoConfig(cwd);
516
+ return { config: autoResult.config, projectName: autoResult.config.project?.name || "design-system" };
517
+ }
518
+ function installClaudeCommands(dryRun = false) {
519
+ const commandsDir = join(homedir(), ".claude", "commands");
520
+ const assetsDir = resolve(__dirname, "..", "..", "assets", "commands");
521
+ const installed = [];
522
+ const alreadyExisted = [];
523
+ if (!existsSync(assetsDir))
524
+ return { installed, alreadyExisted };
525
+ if (!dryRun && !existsSync(commandsDir))
526
+ mkdirSync(commandsDir, { recursive: true });
527
+ for (const file of readdirSync(assetsDir).filter(f => f.endsWith(".md"))) {
528
+ const destPath = join(commandsDir, file);
529
+ if (existsSync(destPath)) {
530
+ alreadyExisted.push(file.replace(".md", ""));
531
+ }
532
+ else {
533
+ if (!dryRun)
534
+ copyFileSync(join(assetsDir, file), destPath);
535
+ installed.push(file.replace(".md", ""));
536
+ }
537
+ }
538
+ return { installed, alreadyExisted };
539
+ }
540
+ function generateConfig(project) {
541
+ const lines = [];
542
+ const monorepoConfig = detectMonorepoConfig(project.root);
543
+ lines.push(`/** @type {import('@buoy-design/cli').BuoyConfig} */`);
544
+ lines.push(`export default {`);
545
+ lines.push(` project: { name: '${project.name}' },`);
546
+ lines.push(` sources: {`);
547
+ for (const framework of project.frameworks) {
548
+ const sourceKey = getSourceKey(framework.name);
549
+ if (sourceKey) {
550
+ const extensions = getExtensions(sourceKey, framework.typescript);
551
+ const defaultPatterns = extensions.map(ext => `src/**/*.${ext}`);
552
+ const patterns = monorepoConfig.type
553
+ ? expandPatternsForMonorepo(defaultPatterns, monorepoConfig).allPatterns
554
+ : defaultPatterns;
555
+ lines.push(` ${sourceKey}: {`);
556
+ lines.push(` enabled: true,`);
557
+ lines.push(` include: [${patterns.map(p => `'${p}'`).join(", ")}],`);
558
+ lines.push(` exclude: ['**/*.test.*', '**/*.spec.*', '**/*.stories.*'],`);
559
+ lines.push(` },`);
560
+ }
561
+ }
562
+ if (project.tokens.length > 0) {
563
+ lines.push(` tokens: { enabled: true, files: [${project.tokens.map(t => `'${t.path}'`).join(", ")}] },`);
564
+ }
565
+ lines.push(` figma: { enabled: false },`);
566
+ lines.push(` },`);
567
+ lines.push(` output: { format: 'table', colors: true },`);
568
+ lines.push(`};`);
569
+ return lines.join("\n");
570
+ }
571
+ function getSourceKey(name) {
572
+ if (["react", "nextjs", "remix", "gatsby", "react-native", "expo", "preact", "solid"].includes(name))
573
+ return "react";
574
+ if (["vue", "nuxt"].includes(name))
575
+ return "vue";
576
+ if (["svelte", "sveltekit"].includes(name))
577
+ return "svelte";
578
+ if (name === "angular")
579
+ return "angular";
580
+ if (["lit", "stencil"].includes(name))
581
+ return "webcomponent";
582
+ return null;
583
+ }
584
+ function getExtensions(sourceKey, typescript) {
585
+ switch (sourceKey) {
586
+ case "vue": return ["vue"];
587
+ case "svelte": return ["svelte"];
588
+ case "angular": return ["component.ts"];
589
+ case "webcomponent": return ["ts"];
590
+ default: return typescript ? ["tsx", "jsx"] : ["jsx", "tsx"];
591
+ }
592
+ }
593
+ function printDetectionResults(project) {
594
+ console.log("");
595
+ console.log(chalk.bold(" Detected:"));
596
+ for (const fw of project.frameworks) {
597
+ console.log(chalk.green(" ✓ ") + chalk.bold(fw.name) + (fw.typescript ? " + TS" : ""));
598
+ }
599
+ for (const comp of project.components) {
600
+ console.log(chalk.green(" ✓ ") + `${comp.fileCount} files in ${chalk.cyan(comp.path)}`);
601
+ }
602
+ for (const token of project.tokens) {
603
+ console.log(chalk.green(" ✓ ") + `${token.name}: ${chalk.cyan(token.path)}`);
604
+ }
605
+ if (project.storybook)
606
+ console.log(chalk.green(" ✓ ") + "Storybook");
607
+ console.log("");
608
+ }
609
+ async function showFrameworksAndPlugins(frameworks, autoInstall) {
610
+ const builtIn = frameworks.filter(fw => fw.scanner);
611
+ const plugins = frameworks.filter(fw => fw.plugin && !fw.scanner);
612
+ if (builtIn.length > 0) {
613
+ console.log(chalk.bold(" Built-in Scanners"));
614
+ for (const fw of builtIn)
615
+ console.log(` ${chalk.green("✓")} ${chalk.cyan(fw.name)}`);
616
+ console.log("");
617
+ }
618
+ if (plugins.length > 0) {
619
+ const missing = plugins.map(fw => fw.plugin).filter((p, i, arr) => arr.indexOf(p) === i);
620
+ console.log(chalk.bold(" Optional Plugins"));
621
+ console.log(` ${chalk.dim(getPluginInstallCommand(missing))}`);
622
+ console.log("");
623
+ if (autoInstall) {
624
+ const { execSync } = await import("node:child_process");
625
+ try {
626
+ execSync(getPluginInstallCommand(missing), { stdio: "inherit" });
627
+ success("Plugins installed");
868
628
  }
869
- info(` ${stepNum}. Run ` +
870
- chalk.cyan("buoy onboard") +
871
- " to integrate with AI assistants");
872
- if (!project.storybook) {
873
- console.log("");
874
- info(chalk.dim("Optional: Connect Figma by adding your API key to the config"));
629
+ catch {
630
+ warning("Plugin install failed");
875
631
  }
876
632
  }
877
- catch (err) {
878
- const message = err instanceof Error ? err.message : String(err);
879
- error(`Failed to create configuration: ${message}`);
880
- process.exit(1);
881
- }
633
+ }
634
+ }
635
+ async function promptConfirm(message, defaultValue = true) {
636
+ if (!process.stdin.isTTY)
637
+ return defaultValue;
638
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
639
+ return new Promise(resolve => {
640
+ rl.question(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"} `, answer => {
641
+ rl.close();
642
+ const t = answer.trim().toLowerCase();
643
+ resolve(t === "" ? defaultValue : t === "y" || t === "yes");
644
+ });
882
645
  });
883
- return cmd;
884
646
  }
885
647
  //# sourceMappingURL=dock.js.map