ccraft 1.0.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 (40) hide show
  1. package/bin/claude-craft.js +85 -0
  2. package/package.json +39 -0
  3. package/src/commands/auth.js +43 -0
  4. package/src/commands/create.js +543 -0
  5. package/src/commands/install.js +480 -0
  6. package/src/commands/logout.js +24 -0
  7. package/src/commands/update.js +339 -0
  8. package/src/constants.js +299 -0
  9. package/src/generators/directories.js +30 -0
  10. package/src/generators/metadata.js +57 -0
  11. package/src/generators/security.js +39 -0
  12. package/src/prompts/gather.js +308 -0
  13. package/src/ui/brand.js +62 -0
  14. package/src/ui/cards.js +179 -0
  15. package/src/ui/format.js +55 -0
  16. package/src/ui/phase-header.js +20 -0
  17. package/src/ui/prompts.js +56 -0
  18. package/src/ui/tables.js +89 -0
  19. package/src/ui/tasks.js +258 -0
  20. package/src/ui/theme.js +83 -0
  21. package/src/utils/analysis-cache.js +519 -0
  22. package/src/utils/api-client.js +253 -0
  23. package/src/utils/api-file-writer.js +197 -0
  24. package/src/utils/bootstrap-runner.js +148 -0
  25. package/src/utils/claude-analyzer.js +255 -0
  26. package/src/utils/claude-optimizer.js +341 -0
  27. package/src/utils/claude-rewriter.js +553 -0
  28. package/src/utils/claude-scorer.js +101 -0
  29. package/src/utils/description-analyzer.js +116 -0
  30. package/src/utils/detect-project.js +1276 -0
  31. package/src/utils/existing-setup.js +341 -0
  32. package/src/utils/file-writer.js +64 -0
  33. package/src/utils/json-extract.js +56 -0
  34. package/src/utils/logger.js +27 -0
  35. package/src/utils/mcp-setup.js +461 -0
  36. package/src/utils/preflight.js +112 -0
  37. package/src/utils/prompt-api-key.js +59 -0
  38. package/src/utils/run-claude.js +152 -0
  39. package/src/utils/security.js +82 -0
  40. package/src/utils/toolkit-rule-generator.js +364 -0
@@ -0,0 +1,57 @@
1
+ import { join } from 'path';
2
+ import { ensureDir } from 'fs-extra/esm';
3
+ import { safeWriteFile } from '../utils/file-writer.js';
4
+ import { VERSION } from '../constants.js';
5
+
6
+ /**
7
+ * Generate .claude/.claude-craft.json — single metadata + project index file.
8
+ * Combines installation metadata with the project snapshot (previously PROJECT_INDEX.json).
9
+ */
10
+ export async function generate(config, targetDir, opts = {}) {
11
+ const claudeDir = join(targetDir, '.claude');
12
+ await ensureDir(claudeDir);
13
+
14
+ const metadata = {
15
+ version: VERSION,
16
+ generatedAt: new Date().toISOString(),
17
+ preset: config._presetAlias || null,
18
+ role: config.role || null,
19
+ intents: config.intents || [],
20
+ components: config.components || [],
21
+ agents: (config._selectedAgents || []).map((a) => a.id),
22
+ skills: (config._selectedSkills || []).map((s) => s.id),
23
+ rules: (config._selectedRules || []).map((r) => r.id),
24
+ mcps: (config._selectedMcps || []).map((m) => m.id),
25
+ workflows: (config._selectedWorkflows || []).map((w) => w.id),
26
+ project: {
27
+ name: config.name || null,
28
+ description: config.description || null,
29
+ type: config.projectType || null,
30
+ complexity: config.complexity ?? 0.5,
31
+ },
32
+ stack: {
33
+ languages: config.languages || [],
34
+ frameworks: config.frameworks || [],
35
+ codeStyle: config.codeStyle || [],
36
+ testFramework: config.testFramework || null,
37
+ packageManager: config.packageManager || null,
38
+ cicd: config.cicd || [],
39
+ },
40
+ architecture: config.architecture || null,
41
+ entryPoints: config.entryPoints || [],
42
+ coreModules: config.coreModules || [],
43
+ subprojects: config.subprojects || [],
44
+ buildCommands: config.buildCommands || {},
45
+ metrics: config.metrics || null,
46
+ stacks: (config._resolvedStacks || []).map(s => ({
47
+ id: s.id, name: s.name, impactScore: s.impactScore,
48
+ })),
49
+ };
50
+
51
+ const result = await safeWriteFile(
52
+ join(claudeDir, '.claude-craft.json'),
53
+ JSON.stringify(metadata, null, 2) + '\n',
54
+ { force: opts.force }
55
+ );
56
+ return [result];
57
+ }
@@ -0,0 +1,39 @@
1
+ import { join } from 'path';
2
+ import { pathExists, outputFile } from 'fs-extra/esm';
3
+ import { readFileSync } from 'fs';
4
+ import { generateSecurityGitignore } from '../utils/security.js';
5
+
6
+ export async function generate(config, targetDir, opts = {}) {
7
+ const results = [];
8
+
9
+ if (!config.addSecurityGitignore) return results;
10
+
11
+ const gitignorePath = join(targetDir, '.gitignore');
12
+ const securityBlock = generateSecurityGitignore(config._detected || {});
13
+ const marker = '# ── Security: sensitive files (added by claude-craft) ──';
14
+
15
+ let content = '';
16
+ let exists = false;
17
+
18
+ if (await pathExists(gitignorePath)) {
19
+ exists = true;
20
+ content = readFileSync(gitignorePath, 'utf8');
21
+
22
+ // Don't add if already present
23
+ if (content.includes(marker)) {
24
+ results.push({ path: gitignorePath, status: 'skipped' });
25
+ return results;
26
+ }
27
+
28
+ // Append to existing
29
+ if (!content.endsWith('\n')) content += '\n';
30
+ content += '\n' + securityBlock;
31
+ } else {
32
+ content = securityBlock;
33
+ }
34
+
35
+ await outputFile(gitignorePath, content, 'utf8');
36
+ results.push({ path: gitignorePath, status: exists ? 'updated' : 'created' });
37
+
38
+ return results;
39
+ }
@@ -0,0 +1,308 @@
1
+ import { resolve } from 'path';
2
+ import { existsSync, statSync } from 'fs';
3
+ import chalk from 'chalk';
4
+ import {
5
+ ROLES,
6
+ INTENTS,
7
+ COMPONENTS,
8
+ PROJECT_TYPES,
9
+ SOURCE_CONTROLS,
10
+ DOCUMENT_TOOLS,
11
+ } from '../constants.js';
12
+ import { validateApiKeyFormat } from '../utils/mcp-setup.js';
13
+ import { themedSelect, themedCheckbox, themedConfirm, themedPassword, themedInput } from '../ui/prompts.js';
14
+ import { renderSummaryCard, renderWarningCard } from '../ui/cards.js';
15
+ import { colors } from '../ui/theme.js';
16
+
17
+ // ── Project path ────────────────────────────────────────────────────────
18
+
19
+ export async function gatherProjectPath() {
20
+ const projectPath = await themedInput({
21
+ message: 'Project path:',
22
+ hint: 'Enter the path to the project you want to configure.',
23
+ default: process.cwd(),
24
+ validate: (v) => {
25
+ const p = resolve(v.trim());
26
+ if (!existsSync(p)) return 'Path does not exist.';
27
+ if (!statSync(p).isDirectory()) return 'Path is not a directory.';
28
+ return true;
29
+ },
30
+ });
31
+ return resolve(projectPath.trim());
32
+ }
33
+
34
+ // ── Create profile (new project from scratch) ─────────────────────────
35
+
36
+ export async function gatherCreateProfile() {
37
+ console.log(colors.muted('\n Let\'s set up your new project.\n'));
38
+
39
+ // Project name
40
+ const name = await themedInput({
41
+ message: 'Project name:',
42
+ hint: 'Leave empty to use the current directory. Letters, numbers, dots, hyphens, underscores only.',
43
+ validate: (v) => {
44
+ const t = v.trim();
45
+ if (!t) return true; // empty = use current directory
46
+ if (!/^[a-zA-Z0-9._-]+$/.test(t)) return 'Only letters, numbers, dots, hyphens, and underscores allowed.';
47
+ return true;
48
+ },
49
+ });
50
+
51
+ // Description (free text)
52
+ const description = await themedInput({
53
+ message: 'Describe what you want to build:',
54
+ hint: 'Be specific — this drives tech stack selection and project scaffolding.',
55
+ validate: (v) => (v.trim().length > 0 ? true : 'Please describe your project.'),
56
+ });
57
+
58
+ // Dev-only role (filtered from ROLES → developer children)
59
+ const devParent = ROLES.find((r) => r.value === 'developer');
60
+ const roleChoices = devParent.children.map((c) => ({
61
+ name: c.name,
62
+ value: c.value,
63
+ description: c.description,
64
+ }));
65
+
66
+ const role = await themedSelect({
67
+ message: 'What type of developer are you?',
68
+ hint: 'This tailors which agents and rules are installed.',
69
+ choices: roleChoices,
70
+ });
71
+
72
+ // Project type
73
+ const projectType = await themedSelect({
74
+ message: 'What kind of project is this?',
75
+ hint: 'Affects architecture-level agents and rules.',
76
+ choices: PROJECT_TYPES.map((pt) => ({
77
+ name: pt.name,
78
+ value: pt.value,
79
+ })),
80
+ });
81
+
82
+ return { name: name.trim(), description: description.trim(), role, projectType };
83
+ }
84
+
85
+ // ── Phase 1: User profile ──────────────────────────────────────────────
86
+
87
+ export async function gatherUserProfile() {
88
+ console.log(colors.muted('\n Let\'s personalize your Claude Code environment.\n'));
89
+
90
+ // Role category
91
+
92
+ const categoryChoices = ROLES.map((r) => ({
93
+ name: r.name,
94
+ value: r.value,
95
+ description: r.description,
96
+ }));
97
+ const category = await themedSelect({
98
+ message: 'What best describes your role?',
99
+ hint: 'A few agents and rules will be tailored to match your specialty.',
100
+ choices: categoryChoices,
101
+ });
102
+
103
+ // Sub-role if category has children
104
+ let role;
105
+ const parent = ROLES.find((r) => r.value === category);
106
+ if (parent && parent.children) {
107
+
108
+ role = await themedSelect({
109
+ message: `Which ${parent.name.toLowerCase()} specialty fits best?`,
110
+ hint: 'This fine-tunes which agents and rules are installed.',
111
+ choices: parent.children.map((c) => ({
112
+ name: c.name,
113
+ value: c.value,
114
+ description: c.description,
115
+ })),
116
+ });
117
+ } else {
118
+ role = category;
119
+ }
120
+
121
+ // Intents
122
+
123
+ const intents = await themedCheckbox({
124
+ message: 'What will you use Claude Code for?',
125
+ hint: 'Select everything that applies. This determines which skills and workflows we install.',
126
+ choices: INTENTS.map((i) => ({
127
+ ...i,
128
+ checked: true,
129
+ })),
130
+ validate: (selected) => selected.length > 0 || 'Select at least one intent',
131
+ });
132
+
133
+ // Source control
134
+
135
+ const sourceControl = await themedSelect({
136
+ message: 'Where do you host your code?',
137
+ hint: 'We\'ll configure source-control integrations accordingly.',
138
+ choices: SOURCE_CONTROLS.map((sc) => ({
139
+ name: sc.name,
140
+ value: sc.value,
141
+ description: sc.description,
142
+ })),
143
+ });
144
+
145
+ // Document tools
146
+
147
+ const documentTools = await themedCheckbox({
148
+ message: 'Which project management tools do you use?',
149
+ hint: 'We can install MCP servers for these to give Claude context from your docs and tickets.',
150
+ choices: DOCUMENT_TOOLS.map((dt) => ({ ...dt, checked: false })),
151
+ required: true,
152
+ });
153
+
154
+ // If "None" was selected, clear all selections
155
+ if (documentTools.includes('none')) {
156
+ documentTools.length = 0;
157
+ }
158
+
159
+ // If "Other" was selected, prompt for the tool name
160
+ const otherIndex = documentTools.indexOf('other');
161
+ if (otherIndex !== -1) {
162
+ const otherTool = await themedInput({
163
+ message: 'Enter the name of your tool:',
164
+ validate: (v) => (v.trim() ? true : 'Please enter a tool name.'),
165
+ });
166
+ documentTools[otherIndex] = otherTool.trim();
167
+ }
168
+
169
+ return { role, intents, sourceControl, documentTools };
170
+ }
171
+
172
+ // ── MCP selection + credential setup ──────────────────────────────────
173
+
174
+ export async function gatherMcpConfig(scoredMcps) {
175
+ console.log(chalk.bold('\n MCP Servers\n'));
176
+
177
+ // Split into guaranteed and optional
178
+ const GUARANTEED_TIERS = new Set(['core', 'role', 'stack', 'auto']);
179
+ const guaranteedMcps = scoredMcps.filter((m) => GUARANTEED_TIERS.has(m.tier));
180
+ const optionalMcps = scoredMcps.filter((m) => !GUARANTEED_TIERS.has(m.tier));
181
+
182
+ // Display guaranteed MCPs
183
+ if (guaranteedMcps.length > 0) {
184
+ console.log(chalk.dim(' Auto-installed (core/role/stack):'));
185
+ for (const mcp of guaranteedMcps) {
186
+ const keyTag = mcp.requiresKey ? chalk.yellow(' [key required]') : '';
187
+ const tokenTag = mcp.tokenSaving ? colors.success(' [saves tokens]') : '';
188
+ console.log(colors.success(` ✔ ${mcp.id}`) + chalk.dim(` — ${mcp.description}${keyTag}${tokenTag}`));
189
+ }
190
+ console.log();
191
+ }
192
+
193
+ // Show optional MCPs as checkbox
194
+ let selectedOptionalMcps = [];
195
+ if (optionalMcps.length > 0) {
196
+ const choices = optionalMcps.map((mcp) => {
197
+ const tags = [];
198
+ if (mcp.requiresKey) tags.push(chalk.yellow('[key required]'));
199
+ if (mcp.tokenSaving) tags.push(colors.success('[saves tokens]'));
200
+ if (mcp.score != null) tags.push(chalk.dim(`(${Math.round(mcp.score * 100)}%)`));
201
+ const suffix = tags.length ? ' ' + tags.join(' ') : '';
202
+ return {
203
+ name: `${mcp.id}${suffix}`,
204
+ value: mcp.id,
205
+ description: mcp.description,
206
+ checked: mcp.recommended || false,
207
+ };
208
+ });
209
+
210
+
211
+ const selectedOptionalIds = await themedCheckbox({
212
+ message: 'Select additional MCP servers',
213
+ hint: 'Sorted by relevance to your project. Recommended ones are pre-selected.',
214
+ choices,
215
+ });
216
+
217
+ selectedOptionalMcps = optionalMcps.filter((m) => selectedOptionalIds.includes(m.id));
218
+ }
219
+
220
+ const selectedMcps = [...guaranteedMcps, ...selectedOptionalMcps];
221
+
222
+ // Collect API keys
223
+ const mcpKeys = {};
224
+ const needKeys = selectedMcps.filter((m) => m.requiresKey);
225
+
226
+ if (needKeys.length > 0) {
227
+ console.log(chalk.dim('\n Configure API keys\n'));
228
+
229
+ for (const mcp of needKeys) {
230
+ const keyDefs = mcp.keyNames || [{ name: mcp.keyName, description: mcp.keyDescription }];
231
+ const collected = {};
232
+
233
+ for (const keyDef of keyDefs) {
234
+ const key = await themedPassword({
235
+ message: `${mcp.id} — ${keyDef.description}:`,
236
+ hint: `Press Enter to skip. You can set ${keyDef.name} as an env variable later.`,
237
+ mask: '*',
238
+ });
239
+ if (key && key.trim()) {
240
+ const { warning } = validateApiKeyFormat(mcp.id, keyDef.name, key.trim());
241
+ if (warning) {
242
+ console.log(chalk.yellow(` ⚠ ${warning}`));
243
+ }
244
+ collected[keyDef.name] = key.trim();
245
+ } else {
246
+ console.log(chalk.dim(` Skipped — set ${keyDef.name} env var later`));
247
+ }
248
+ }
249
+
250
+ if (Object.keys(collected).length > 0) {
251
+ mcpKeys[mcp.id] = collected;
252
+ }
253
+ }
254
+ }
255
+
256
+ return { selectedMcps, mcpKeys };
257
+ }
258
+
259
+ // ── Security configuration ────────────────────────────────────────────
260
+
261
+ export function gatherSecurityConfig(detected) {
262
+ if (detected.sensitiveFiles.found.length > 0) {
263
+ const items = [...detected.sensitiveFiles.found];
264
+ if (!detected.sensitiveFiles.gitignoreCovers) {
265
+ items.push('These may not be covered by .gitignore!');
266
+ }
267
+ renderWarningCard('Sensitive files detected:', items);
268
+ console.log();
269
+ }
270
+
271
+ // Always add security patterns — no prompt needed
272
+ return { addSecurityGitignore: true };
273
+ }
274
+
275
+ // ── Confirm installation ──────────────────────────────────────────────
276
+
277
+ export async function confirmInstallation(summary) {
278
+ renderSummaryCard(summary);
279
+ console.log();
280
+
281
+
282
+ const proceed = await themedConfirm({
283
+ message: 'Ready to install?',
284
+ hint: 'This will create files in .claude/ and update .gitignore and settings.json.',
285
+ default: true,
286
+ });
287
+
288
+ return proceed;
289
+ }
290
+
291
+ // ── Defaults (non-interactive mode) ───────────────────────────────────
292
+
293
+ export function getDefaults(detected) {
294
+ return {
295
+ role: 'web',
296
+ intents: ['implementing', 'debugging', 'refactoring', 'testing', 'reviewing'],
297
+ sourceControl: 'github',
298
+ documentTools: [],
299
+ name: detected.name || 'my-project',
300
+ description: detected.description || '',
301
+ projectType: detected.projectType || 'monolith',
302
+ languages: detected.languages.length ? detected.languages : ['JavaScript'],
303
+ frameworks: detected.frameworks,
304
+ components: COMPONENTS.map((c) => c.value),
305
+ addSecurityGitignore: true,
306
+ mcpKeys: {},
307
+ };
308
+ }
@@ -0,0 +1,62 @@
1
+ import figlet from 'figlet';
2
+ import gradient from 'gradient-string';
3
+ import boxen from 'boxen';
4
+ import chalk from 'chalk';
5
+ import { isNarrow, isTTY, colors } from './theme.js';
6
+
7
+ const LOGO_TEXT = 'Claude Craft';
8
+
9
+ const OVERVIEW_LINES = [
10
+ ' What we\'ll do:',
11
+ '',
12
+ ' 1. Learn about you Your role and preferences',
13
+ ' 2. Analyze project Deep-scan your codebase',
14
+ ' 3. Configure Pick servers and settings',
15
+ ' 4. Install Write optimized config',
16
+ ' 5. Finalize Polish and verify',
17
+ ];
18
+
19
+ /**
20
+ * Render the ASCII logo with gradient coloring.
21
+ * Falls back to plain text on narrow terminals or non-TTY.
22
+ */
23
+ export function renderLogo() {
24
+ if (isNarrow() || !isTTY()) {
25
+ return chalk.bold.cyan(' Claude Craft');
26
+ }
27
+
28
+ try {
29
+ const raw = figlet.textSync(LOGO_TEXT, { font: 'Small' });
30
+ const colored = gradient(['#6EC1E4', '#8B5CF6']).multiline(raw);
31
+ return colored
32
+ .split('\n')
33
+ .map((line) => ' ' + line)
34
+ .join('\n');
35
+ } catch {
36
+ return chalk.bold.cyan(' Claude Craft');
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Render the full welcome banner: logo + version + overview card.
42
+ */
43
+ export function renderBanner(version) {
44
+ console.log();
45
+ console.log(renderLogo());
46
+ console.log(colors.muted(` v${version} Intelligent Claude Code Configurator`));
47
+ console.log();
48
+
49
+ if (!isNarrow()) {
50
+ const overviewContent = OVERVIEW_LINES.join('\n');
51
+ const box = boxen(overviewContent, {
52
+ padding: { top: 0, bottom: 0, left: 0, right: 1 },
53
+ margin: { top: 0, bottom: 0, left: 2, right: 0 },
54
+ borderStyle: 'round',
55
+ borderColor: 'cyan',
56
+ dimBorder: true,
57
+ });
58
+ console.log(box);
59
+ }
60
+
61
+ console.log();
62
+ }
@@ -0,0 +1,179 @@
1
+ import boxen from 'boxen';
2
+ import chalk from 'chalk';
3
+ import { boxStyles, isNarrow, isTTY, colors } from './theme.js';
4
+
5
+ /**
6
+ * Render project analysis as a styled card.
7
+ */
8
+ export function renderProjectCard(projectInfo) {
9
+ const lines = [];
10
+
11
+ lines.push(chalk.bold(' Project Analysis'));
12
+ lines.push('');
13
+
14
+ const row = (label, value) => {
15
+ if (!value) return;
16
+ const padded = label.padEnd(16);
17
+ lines.push(` ${chalk.dim(padded)}${colors.success(value)}`);
18
+ };
19
+
20
+ row('Name', projectInfo.name + (projectInfo.description ? chalk.dim(` — ${projectInfo.description}`) : ''));
21
+ row('Type', projectInfo.projectType);
22
+
23
+ if (projectInfo.languageDistribution) {
24
+ const distStr = Object.entries(projectInfo.languageDistribution)
25
+ .sort(([, a], [, b]) => b - a)
26
+ .map(([lang, pct]) => `${lang} (${pct}%)`)
27
+ .join(', ');
28
+ row('Languages', distStr);
29
+ } else if (projectInfo.languages?.length) {
30
+ row('Languages', projectInfo.languages.join(', '));
31
+ }
32
+
33
+ if (projectInfo.frameworks?.length) row('Frameworks', projectInfo.frameworks.join(', '));
34
+ if (projectInfo.codeStyle?.length) row('Code style', projectInfo.codeStyle.join(', '));
35
+ if (projectInfo.cicd?.length) row('CI/CD', projectInfo.cicd.join(', '));
36
+ if (projectInfo.architecture) row('Architecture', projectInfo.architecture);
37
+
38
+ if (typeof projectInfo.complexity === 'number') {
39
+ const cl = projectInfo.complexity;
40
+ const label = cl >= 0.7 ? 'complex' : cl >= 0.4 ? 'moderate' : 'simple';
41
+ row('Complexity', `${cl.toFixed(2)} (${label})`);
42
+ }
43
+
44
+ if (projectInfo.metrics) {
45
+ const m = projectInfo.metrics;
46
+ const parts = [];
47
+ if (m.totalFiles) parts.push(`${m.totalFiles} files`);
48
+ if (m.dependencyCount) parts.push(`${m.dependencyCount} deps`);
49
+ if (m.testFileCount) parts.push(`${m.testFileCount} tests`);
50
+ if (m.estimatedTestCoverage && m.estimatedTestCoverage !== 'unknown') parts.push(`~${m.estimatedTestCoverage} coverage`);
51
+ if (parts.length) row('Metrics', parts.join(', '));
52
+ }
53
+
54
+ if (projectInfo.entryPoints?.length > 0) {
55
+ row('Entry points', projectInfo.entryPoints.map((e) => e.path).join(', '));
56
+ }
57
+
58
+ if (projectInfo.subprojects?.length > 0) {
59
+ lines.push(chalk.dim(' Subprojects:'));
60
+ for (const sub of projectInfo.subprojects) {
61
+ const fws = sub.frameworks?.length ? ` (${sub.frameworks.join(', ')})` : '';
62
+ lines.push(chalk.dim(` ${chalk.green('•')} ${sub.path}: ${sub.languages.join(', ')}${fws}`));
63
+ }
64
+ }
65
+
66
+ if (projectInfo.buildCommands) {
67
+ const defined = Object.entries(projectInfo.buildCommands).filter(([, v]) => v);
68
+ if (defined.length > 0) {
69
+ row('Commands', defined.map(([k, v]) => `${k}: ${v}`).join(', '));
70
+ }
71
+ }
72
+
73
+ const content = lines.join('\n');
74
+
75
+ if (isNarrow() || !isTTY()) {
76
+ console.log(content);
77
+ return;
78
+ }
79
+
80
+ console.log(boxen(content, {
81
+ ...boxStyles.info,
82
+ title: 'Analysis',
83
+ titleAlignment: 'left',
84
+ }));
85
+ }
86
+
87
+ /**
88
+ * Render the installation summary card before confirmation.
89
+ */
90
+ export function renderSummaryCard(summary) {
91
+ const lines = [];
92
+ lines.push(chalk.bold(' Installation Summary'));
93
+ lines.push('');
94
+
95
+ const countItems = (bucket) => {
96
+ if (!bucket) return 0;
97
+ return Object.values(bucket).reduce((sum, arr) => sum + (Array.isArray(arr) ? arr.length : 0), 0);
98
+ };
99
+
100
+ const guaranteedCount = countItems(summary.guaranteed);
101
+ const candidateCount = countItems(summary.candidates);
102
+ const mcpCount = summary.mcps ? summary.mcps.length : 0;
103
+
104
+ lines.push(` ${chalk.dim('Guaranteed components'.padEnd(24))}${colors.success(String(guaranteedCount))}`);
105
+ if (candidateCount > 0) {
106
+ lines.push(` ${chalk.dim('Candidates for scoring'.padEnd(24))}${colors.primary(String(candidateCount))}`);
107
+ }
108
+ lines.push(` ${chalk.dim('MCP servers'.padEnd(24))}${colors.success(String(mcpCount))}`);
109
+
110
+ const content = lines.join('\n');
111
+
112
+ if (isNarrow() || !isTTY()) {
113
+ console.log(content);
114
+ return;
115
+ }
116
+
117
+ console.log(boxen(content, boxStyles.info));
118
+ }
119
+
120
+ /**
121
+ * Render the final success card.
122
+ */
123
+ export function renderSuccessCard({ totalItems, mcpCount, mcpsNeedingKeys }) {
124
+ const lines = [];
125
+ lines.push(chalk.bold.green(' Installation complete!'));
126
+ lines.push('');
127
+ lines.push(` ${chalk.dim('Settings installed'.padEnd(22))}${colors.success(String(totalItems))} ${chalk.dim('(guaranteed + selected)')}`);
128
+ lines.push(` ${chalk.dim('MCP servers'.padEnd(22))}${colors.success(String(mcpCount))}`);
129
+
130
+ if (mcpsNeedingKeys.length > 0) {
131
+ lines.push('');
132
+ lines.push(chalk.yellow(' MCP servers needing API keys:'));
133
+ for (const r of mcpsNeedingKeys) {
134
+ lines.push(chalk.yellow(` ${chalk.dim('•')} ${r.id}: set ${r.apiKey.keyName}`));
135
+ }
136
+ }
137
+
138
+ lines.push('');
139
+ lines.push(chalk.dim(' Next steps:'));
140
+ lines.push(chalk.dim(` 1. Read ${chalk.underline('USER_GUIDE.md')} for a full feature overview`));
141
+
142
+ let step = 2;
143
+ if (mcpsNeedingKeys.length > 0) {
144
+ lines.push(chalk.dim(` ${step}. Set missing API keys for MCP servers (see above)`));
145
+ step++;
146
+ }
147
+ lines.push(chalk.dim(` ${step}. Start Claude Code and try out some commands!`));
148
+ step++;
149
+ lines.push(chalk.dim(` ${step}. Customize .claude/ to your needs`));
150
+
151
+ const content = lines.join('\n');
152
+
153
+ if (isNarrow() || !isTTY()) {
154
+ console.log('\n' + content);
155
+ return;
156
+ }
157
+
158
+ console.log(boxen(content, boxStyles.success));
159
+ }
160
+
161
+ /**
162
+ * Render a warning card (e.g., sensitive files detected).
163
+ */
164
+ export function renderWarningCard(title, items) {
165
+ const lines = [];
166
+ lines.push(chalk.yellow(` ${title}`));
167
+ for (const item of items) {
168
+ lines.push(chalk.yellow(` ${chalk.dim('•')} ${item}`));
169
+ }
170
+
171
+ const content = lines.join('\n');
172
+
173
+ if (isNarrow() || !isTTY()) {
174
+ console.log(content);
175
+ return;
176
+ }
177
+
178
+ console.log(boxen(content, boxStyles.warning));
179
+ }
@@ -0,0 +1,55 @@
1
+ import chalk from 'chalk';
2
+ import { getTerminalWidth } from './theme.js';
3
+
4
+ /**
5
+ * Indent every line of text by N spaces.
6
+ */
7
+ export function indent(text, spaces = 2) {
8
+ const pad = ' '.repeat(spaces);
9
+ return text
10
+ .split('\n')
11
+ .map((line) => pad + line)
12
+ .join('\n');
13
+ }
14
+
15
+ /**
16
+ * Horizontal divider line.
17
+ */
18
+ export function divider(width) {
19
+ const w = width || Math.min(getTerminalWidth() - 4, 56);
20
+ return chalk.dim(' ' + '─'.repeat(w));
21
+ }
22
+
23
+ /**
24
+ * Pluralize a word based on count.
25
+ */
26
+ export function pluralize(count, singular, plural) {
27
+ return count === 1 ? `${count} ${singular}` : `${count} ${plural || singular + 's'}`;
28
+ }
29
+
30
+ /**
31
+ * Truncate text with ellipsis if too long.
32
+ */
33
+ export function truncate(text, maxLen = 60) {
34
+ if (text.length <= maxLen) return text;
35
+ return text.slice(0, maxLen - 1) + '…';
36
+ }
37
+
38
+ /**
39
+ * Format milliseconds as human-readable duration.
40
+ */
41
+ export function formatDuration(ms) {
42
+ if (ms < 1000) return `${ms}ms`;
43
+ return `${(ms / 1000).toFixed(1)}s`;
44
+ }
45
+
46
+ /**
47
+ * Dot-padded label + value (e.g., "Node.js v20.11.0 ............ ok").
48
+ */
49
+ export function dotPad(label, value, totalWidth) {
50
+ const w = totalWidth || Math.min(getTerminalWidth() - 8, 48);
51
+ const stripped = label.replace(/\x1b\[[0-9;]*m/g, '');
52
+ const valStripped = value.replace(/\x1b\[[0-9;]*m/g, '');
53
+ const dotsNeeded = Math.max(2, w - stripped.length - valStripped.length);
54
+ return label + chalk.dim(' ' + '.'.repeat(dotsNeeded) + ' ') + value;
55
+ }