claude-code-templates 1.14.0 → 1.14.2
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/bin/create-claude-config.js +3 -2
- package/package.json +1 -1
- package/src/file-operations.js +217 -81
- package/src/index.js +104 -68
- package/src/tracking-service.js +188 -0
|
@@ -43,8 +43,9 @@ program
|
|
|
43
43
|
.name('create-claude-config')
|
|
44
44
|
.description('Setup Claude Code configurations for different programming languages')
|
|
45
45
|
.version(require('../package.json').version)
|
|
46
|
-
.option('-l, --language <language>', 'specify programming language')
|
|
47
|
-
.option('-f, --framework <framework>', 'specify framework')
|
|
46
|
+
.option('-l, --language <language>', 'specify programming language (deprecated, use --template)')
|
|
47
|
+
.option('-f, --framework <framework>', 'specify framework (deprecated, use --template)')
|
|
48
|
+
.option('-t, --template <template>', 'specify template (e.g., common, javascript-typescript, python, ruby)')
|
|
48
49
|
.option('-d, --directory <directory>', 'target directory (default: current directory)')
|
|
49
50
|
.option('-y, --yes', 'skip prompts and use defaults')
|
|
50
51
|
.option('--dry-run', 'show what would be copied without actually copying')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-templates",
|
|
3
|
-
"version": "1.14.
|
|
3
|
+
"version": "1.14.2",
|
|
4
4
|
"description": "CLI tool to setup Claude Code configurations with framework-specific commands, automation hooks and MCP Servers for your projects",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
package/src/file-operations.js
CHANGED
|
@@ -4,6 +4,152 @@ const chalk = require('chalk');
|
|
|
4
4
|
const inquirer = require('inquirer');
|
|
5
5
|
const { getHooksForLanguage, filterHooksBySelection, getMCPsForLanguage, filterMCPsBySelection } = require('./hook-scanner');
|
|
6
6
|
|
|
7
|
+
// GitHub configuration for downloading templates
|
|
8
|
+
const GITHUB_CONFIG = {
|
|
9
|
+
owner: 'davila7',
|
|
10
|
+
repo: 'claude-code-templates',
|
|
11
|
+
branch: 'main',
|
|
12
|
+
templatesPath: 'cli-tool/templates'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// Cache for downloaded files to avoid repeated downloads
|
|
16
|
+
const downloadCache = new Map();
|
|
17
|
+
|
|
18
|
+
async function downloadFileFromGitHub(filePath) {
|
|
19
|
+
// Check cache first
|
|
20
|
+
if (downloadCache.has(filePath)) {
|
|
21
|
+
return downloadCache.get(filePath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const githubUrl = `https://raw.githubusercontent.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}/${filePath}`;
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(githubUrl);
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const content = await response.text();
|
|
33
|
+
downloadCache.set(filePath, content);
|
|
34
|
+
return content;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(chalk.red(`❌ Error downloading ${filePath} from GitHub:`), error.message);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function downloadDirectoryFromGitHub(dirPath) {
|
|
42
|
+
// For directories, we need to get the list of files first
|
|
43
|
+
// GitHub API endpoint to get directory contents
|
|
44
|
+
const apiUrl = `https://api.github.com/repos/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/contents/${GITHUB_CONFIG.templatesPath}/${dirPath}?ref=${GITHUB_CONFIG.branch}`;
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const response = await fetch(apiUrl);
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`Failed to get directory listing for ${dirPath}: ${response.status} ${response.statusText}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const items = await response.json();
|
|
53
|
+
const files = {};
|
|
54
|
+
|
|
55
|
+
for (const item of items) {
|
|
56
|
+
if (item.type === 'file') {
|
|
57
|
+
const relativePath = path.relative(GITHUB_CONFIG.templatesPath, item.path);
|
|
58
|
+
const content = await downloadFileFromGitHub(relativePath);
|
|
59
|
+
files[item.name] = content;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return files;
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(chalk.red(`❌ Error downloading directory ${dirPath} from GitHub:`), error.message);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Helper functions for processing downloaded content
|
|
71
|
+
async function processSettingsFileFromContent(settingsContent, destPath, templateConfig) {
|
|
72
|
+
const settings = JSON.parse(settingsContent);
|
|
73
|
+
|
|
74
|
+
// Filter hooks based on selection
|
|
75
|
+
if (templateConfig.selectedHooks && settings.hooks) {
|
|
76
|
+
settings.hooks = filterHooksBySelection(settings.hooks, templateConfig.selectedHooks);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const destDir = path.dirname(destPath);
|
|
80
|
+
await fs.ensureDir(destDir);
|
|
81
|
+
await fs.writeJson(destPath, settings, { spaces: 2 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function mergeSettingsFileFromContent(settingsContent, destPath, templateConfig) {
|
|
85
|
+
const newSettings = JSON.parse(settingsContent);
|
|
86
|
+
let existingSettings = {};
|
|
87
|
+
|
|
88
|
+
if (await fs.pathExists(destPath)) {
|
|
89
|
+
existingSettings = await fs.readJson(destPath);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Filter hooks based on selection
|
|
93
|
+
if (templateConfig.selectedHooks && newSettings.hooks) {
|
|
94
|
+
newSettings.hooks = filterHooksBySelection(newSettings.hooks, templateConfig.selectedHooks);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Merge settings
|
|
98
|
+
const mergedSettings = {
|
|
99
|
+
...existingSettings,
|
|
100
|
+
...newSettings,
|
|
101
|
+
hooks: {
|
|
102
|
+
...existingSettings.hooks,
|
|
103
|
+
...newSettings.hooks
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const destDir = path.dirname(destPath);
|
|
108
|
+
await fs.ensureDir(destDir);
|
|
109
|
+
await fs.writeJson(destPath, mergedSettings, { spaces: 2 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function processMCPFileFromContent(mcpContent, destPath, templateConfig) {
|
|
113
|
+
const mcpConfig = JSON.parse(mcpContent);
|
|
114
|
+
|
|
115
|
+
// Filter MCPs based on selection
|
|
116
|
+
if (templateConfig.selectedMCPs && mcpConfig.mcpServers) {
|
|
117
|
+
mcpConfig.mcpServers = filterMCPsBySelection(mcpConfig.mcpServers, templateConfig.selectedMCPs);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const destDir = path.dirname(destPath);
|
|
121
|
+
await fs.ensureDir(destDir);
|
|
122
|
+
await fs.writeJson(destPath, mcpConfig, { spaces: 2 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function mergeMCPFileFromContent(mcpContent, destPath, templateConfig) {
|
|
126
|
+
const newMcpConfig = JSON.parse(mcpContent);
|
|
127
|
+
let existingMcpConfig = {};
|
|
128
|
+
|
|
129
|
+
if (await fs.pathExists(destPath)) {
|
|
130
|
+
existingMcpConfig = await fs.readJson(destPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Filter MCPs based on selection
|
|
134
|
+
if (templateConfig.selectedMCPs && newMcpConfig.mcpServers) {
|
|
135
|
+
newMcpConfig.mcpServers = filterMCPsBySelection(newMcpConfig.mcpServers, templateConfig.selectedMCPs);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Merge MCP configurations
|
|
139
|
+
const mergedMcpConfig = {
|
|
140
|
+
...existingMcpConfig,
|
|
141
|
+
...newMcpConfig,
|
|
142
|
+
mcpServers: {
|
|
143
|
+
...existingMcpConfig.mcpServers,
|
|
144
|
+
...newMcpConfig.mcpServers
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const destDir = path.dirname(destPath);
|
|
149
|
+
await fs.ensureDir(destDir);
|
|
150
|
+
await fs.writeJson(destPath, mergedMcpConfig, { spaces: 2 });
|
|
151
|
+
}
|
|
152
|
+
|
|
7
153
|
async function checkExistingFiles(targetDir, templateConfig) {
|
|
8
154
|
const existingFiles = [];
|
|
9
155
|
|
|
@@ -86,7 +232,7 @@ async function createBackups(existingFiles, targetDir) {
|
|
|
86
232
|
}
|
|
87
233
|
|
|
88
234
|
async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
89
|
-
|
|
235
|
+
console.log(chalk.gray(`📥 Downloading templates from GitHub (${GITHUB_CONFIG.branch} branch)...`));
|
|
90
236
|
|
|
91
237
|
// Check for existing files and get user preference
|
|
92
238
|
const existingFiles = await checkExistingFiles(targetDir, templateConfig);
|
|
@@ -114,7 +260,6 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
|
114
260
|
|
|
115
261
|
// Copy base files and framework-specific files
|
|
116
262
|
for (const file of templateConfig.files) {
|
|
117
|
-
const sourcePath = path.join(templateDir, file.source);
|
|
118
263
|
const destPath = path.join(targetDir, file.destination);
|
|
119
264
|
|
|
120
265
|
try {
|
|
@@ -123,94 +268,115 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
|
123
268
|
// This is a framework-specific commands directory - merge with existing commands
|
|
124
269
|
await fs.ensureDir(destPath);
|
|
125
270
|
|
|
126
|
-
//
|
|
127
|
-
const frameworkFiles = await
|
|
128
|
-
for (const
|
|
129
|
-
const
|
|
130
|
-
const destFile = path.join(destPath, frameworkFile);
|
|
271
|
+
// Download framework-specific commands from GitHub
|
|
272
|
+
const frameworkFiles = await downloadDirectoryFromGitHub(file.source);
|
|
273
|
+
for (const [frameworkFileName, content] of Object.entries(frameworkFiles)) {
|
|
274
|
+
const destFile = path.join(destPath, frameworkFileName);
|
|
131
275
|
|
|
132
276
|
// In merge mode, skip if file already exists
|
|
133
277
|
if (userAction === 'merge' && await fs.pathExists(destFile)) {
|
|
134
|
-
console.log(chalk.blue(`⏭️ Skipped ${
|
|
278
|
+
console.log(chalk.blue(`⏭️ Skipped ${frameworkFileName} (already exists)`));
|
|
135
279
|
continue;
|
|
136
280
|
}
|
|
137
281
|
|
|
138
|
-
await fs.
|
|
282
|
+
await fs.writeFile(destFile, content, 'utf8');
|
|
139
283
|
}
|
|
140
284
|
|
|
141
|
-
console.log(chalk.green(`✓
|
|
285
|
+
console.log(chalk.green(`✓ Downloaded framework commands ${file.source} → ${file.destination}`));
|
|
142
286
|
} else if (file.source.includes('.claude') && !file.source.includes('examples/')) {
|
|
143
|
-
// This is base .claude directory -
|
|
144
|
-
await fs.
|
|
145
|
-
overwrite: shouldOverwrite,
|
|
146
|
-
filter: (src) => {
|
|
147
|
-
// Skip the commands directory itself - we'll handle it separately
|
|
148
|
-
return !src.endsWith('.claude/commands');
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Now handle base commands specifically
|
|
153
|
-
const baseCommandsPath = path.join(sourcePath, 'commands');
|
|
154
|
-
const destCommandsPath = path.join(destPath, 'commands');
|
|
287
|
+
// This is base .claude directory - download it but handle commands specially
|
|
288
|
+
await fs.ensureDir(destPath);
|
|
155
289
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
// Copy base commands, but exclude framework-specific ones that were moved
|
|
160
|
-
const baseCommands = await fs.readdir(baseCommandsPath);
|
|
161
|
-
const excludeCommands = ['react-component.md', 'route.md', 'api-endpoint.md']; // Commands moved to framework dirs
|
|
290
|
+
// Download base .claude directory structure from GitHub
|
|
291
|
+
try {
|
|
292
|
+
const baseClaudeFiles = await downloadDirectoryFromGitHub(file.source);
|
|
162
293
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const destFile = path.join(
|
|
294
|
+
// Write non-command files first
|
|
295
|
+
for (const [fileName, content] of Object.entries(baseClaudeFiles)) {
|
|
296
|
+
if (fileName !== 'commands') { // Skip commands directory, handle separately
|
|
297
|
+
const destFile = path.join(destPath, fileName);
|
|
167
298
|
|
|
168
299
|
// In merge mode, skip if file already exists
|
|
169
300
|
if (userAction === 'merge' && await fs.pathExists(destFile)) {
|
|
170
|
-
console.log(chalk.blue(`⏭️ Skipped ${
|
|
301
|
+
console.log(chalk.blue(`⏭️ Skipped ${fileName} (already exists)`));
|
|
171
302
|
continue;
|
|
172
303
|
}
|
|
173
304
|
|
|
174
|
-
await fs.
|
|
305
|
+
await fs.writeFile(destFile, content, 'utf8');
|
|
175
306
|
}
|
|
176
307
|
}
|
|
308
|
+
|
|
309
|
+
// Now handle base commands specifically
|
|
310
|
+
const destCommandsPath = path.join(destPath, 'commands');
|
|
311
|
+
await fs.ensureDir(destCommandsPath);
|
|
312
|
+
|
|
313
|
+
// Download base commands from GitHub
|
|
314
|
+
const baseCommandsDir = `${file.source}/commands`;
|
|
315
|
+
try {
|
|
316
|
+
const baseCommands = await downloadDirectoryFromGitHub(baseCommandsDir);
|
|
317
|
+
const excludeCommands = ['react-component.md', 'route.md', 'api-endpoint.md']; // Commands moved to framework dirs
|
|
318
|
+
|
|
319
|
+
for (const [baseCommandName, commandContent] of Object.entries(baseCommands)) {
|
|
320
|
+
if (!excludeCommands.includes(baseCommandName)) {
|
|
321
|
+
const destFile = path.join(destCommandsPath, baseCommandName);
|
|
322
|
+
|
|
323
|
+
// In merge mode, skip if file already exists
|
|
324
|
+
if (userAction === 'merge' && await fs.pathExists(destFile)) {
|
|
325
|
+
console.log(chalk.blue(`⏭️ Skipped ${baseCommandName} (already exists)`));
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
await fs.writeFile(destFile, commandContent, 'utf8');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
// Commands directory might not exist for some templates, that's ok
|
|
334
|
+
console.log(chalk.yellow(`⚠️ No commands directory found for ${baseCommandsDir}`));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
} catch (error) {
|
|
338
|
+
console.error(chalk.red(`❌ Error downloading .claude directory: ${error.message}`));
|
|
339
|
+
throw error;
|
|
177
340
|
}
|
|
178
341
|
|
|
179
|
-
console.log(chalk.green(`✓
|
|
342
|
+
console.log(chalk.green(`✓ Downloaded base configuration and commands ${file.source} → ${file.destination}`));
|
|
180
343
|
} else if (file.source.includes('settings.json') && templateConfig.selectedHooks) {
|
|
344
|
+
// Download and process settings.json with hooks
|
|
345
|
+
const settingsContent = await downloadFileFromGitHub(file.source);
|
|
346
|
+
|
|
181
347
|
// In merge mode, merge settings instead of overwriting
|
|
182
348
|
if (userAction === 'merge') {
|
|
183
|
-
await
|
|
349
|
+
await mergeSettingsFileFromContent(settingsContent, destPath, templateConfig);
|
|
184
350
|
console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected hooks)`));
|
|
185
351
|
} else {
|
|
186
|
-
await
|
|
187
|
-
console.log(chalk.green(`✓
|
|
352
|
+
await processSettingsFileFromContent(settingsContent, destPath, templateConfig);
|
|
353
|
+
console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination} (with selected hooks)`));
|
|
188
354
|
}
|
|
189
355
|
} else if (file.source.includes('.mcp.json') && templateConfig.selectedMCPs) {
|
|
356
|
+
// Download and process MCP config with selected MCPs
|
|
357
|
+
const mcpContent = await downloadFileFromGitHub(file.source);
|
|
358
|
+
|
|
190
359
|
// In merge mode, merge MCP config instead of overwriting
|
|
191
360
|
if (userAction === 'merge') {
|
|
192
|
-
await
|
|
361
|
+
await mergeMCPFileFromContent(mcpContent, destPath, templateConfig);
|
|
193
362
|
console.log(chalk.green(`✓ Merged ${file.source} → ${file.destination} (with selected MCPs)`));
|
|
194
363
|
} else {
|
|
195
|
-
await
|
|
196
|
-
console.log(chalk.green(`✓
|
|
364
|
+
await processMCPFileFromContent(mcpContent, destPath, templateConfig);
|
|
365
|
+
console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination} (with selected MCPs)`));
|
|
197
366
|
}
|
|
198
367
|
} else {
|
|
199
|
-
//
|
|
368
|
+
// Download regular files (CLAUDE.md, etc.)
|
|
200
369
|
// In merge mode, skip if file already exists
|
|
201
370
|
if (userAction === 'merge' && await fs.pathExists(destPath)) {
|
|
202
371
|
console.log(chalk.blue(`⏭️ Skipped ${file.destination} (already exists)`));
|
|
203
372
|
continue;
|
|
204
373
|
}
|
|
205
374
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
console.log(chalk.green(`✓ Copied ${file.source} → ${file.destination}`));
|
|
375
|
+
const fileContent = await downloadFileFromGitHub(file.source);
|
|
376
|
+
const destDir = path.dirname(destPath);
|
|
377
|
+
await fs.ensureDir(destDir);
|
|
378
|
+
await fs.writeFile(destPath, fileContent, 'utf8');
|
|
379
|
+
console.log(chalk.green(`✓ Downloaded ${file.source} → ${file.destination}`));
|
|
214
380
|
}
|
|
215
381
|
} catch (error) {
|
|
216
382
|
console.error(chalk.red(`✗ Failed to copy ${file.source}:`), error.message);
|
|
@@ -218,38 +384,8 @@ async function copyTemplateFiles(templateConfig, targetDir, options = {}) {
|
|
|
218
384
|
}
|
|
219
385
|
}
|
|
220
386
|
|
|
387
|
+
console.log(chalk.cyan(`📦 All templates downloaded from: https://github.com/${GITHUB_CONFIG.owner}/${GITHUB_CONFIG.repo}/tree/${GITHUB_CONFIG.branch}/${GITHUB_CONFIG.templatesPath}`));
|
|
221
388
|
return true; // Indicate successful completion
|
|
222
|
-
|
|
223
|
-
// Copy selected commands individually
|
|
224
|
-
if (templateConfig.selectedCommands && templateConfig.selectedCommands.length > 0) {
|
|
225
|
-
const commandsDir = path.join(targetDir, '.claude', 'commands');
|
|
226
|
-
await fs.ensureDir(commandsDir);
|
|
227
|
-
|
|
228
|
-
for (const command of templateConfig.selectedCommands) {
|
|
229
|
-
try {
|
|
230
|
-
const commandFileName = `${command.name}.md`;
|
|
231
|
-
const destPath = path.join(commandsDir, commandFileName);
|
|
232
|
-
|
|
233
|
-
await fs.copy(command.filePath, destPath);
|
|
234
|
-
console.log(chalk.green(`✓ Added command: ${command.displayName}`));
|
|
235
|
-
} catch (error) {
|
|
236
|
-
console.error(chalk.red(`✗ Failed to copy command ${command.name}:`), error.message);
|
|
237
|
-
// Don't throw - continue with other commands
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
console.log(chalk.cyan(`📋 Installed ${templateConfig.selectedCommands.length} commands`));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Report hook selection
|
|
245
|
-
if (templateConfig.selectedHooks && templateConfig.selectedHooks.length > 0) {
|
|
246
|
-
console.log(chalk.magenta(`🔧 Installed ${templateConfig.selectedHooks.length} automation hooks`));
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Report MCP selection
|
|
250
|
-
if (templateConfig.selectedMCPs && templateConfig.selectedMCPs.length > 0) {
|
|
251
|
-
console.log(chalk.blue(`🔧 Installed ${templateConfig.selectedMCPs.length} MCP`));
|
|
252
|
-
}
|
|
253
389
|
}
|
|
254
390
|
|
|
255
391
|
async function runPostInstallationValidation(targetDir, templateConfig) {
|
package/src/index.js
CHANGED
|
@@ -14,6 +14,7 @@ const { runHookStats } = require('./hook-stats');
|
|
|
14
14
|
const { runMCPStats } = require('./mcp-stats');
|
|
15
15
|
const { runAnalytics } = require('./analytics');
|
|
16
16
|
const { runHealthCheck } = require('./health-check');
|
|
17
|
+
const { trackingService } = require('./tracking-service');
|
|
17
18
|
|
|
18
19
|
async function showMainMenu() {
|
|
19
20
|
console.log('');
|
|
@@ -49,12 +50,14 @@ async function showMainMenu() {
|
|
|
49
50
|
|
|
50
51
|
if (initialChoice.action === 'analytics') {
|
|
51
52
|
console.log(chalk.blue('📊 Launching Claude Code Analytics Dashboard...'));
|
|
53
|
+
trackingService.trackAnalyticsDashboard({ page: 'dashboard', source: 'interactive_menu' });
|
|
52
54
|
await runAnalytics({});
|
|
53
55
|
return;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
if (initialChoice.action === 'chats') {
|
|
57
59
|
console.log(chalk.blue('💬 Launching Claude Code Chats Dashboard...'));
|
|
60
|
+
trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'interactive_menu' });
|
|
58
61
|
await runAnalytics({ openTo: 'agents' });
|
|
59
62
|
return;
|
|
60
63
|
}
|
|
@@ -62,6 +65,13 @@ async function showMainMenu() {
|
|
|
62
65
|
if (initialChoice.action === 'health') {
|
|
63
66
|
console.log(chalk.blue('🔍 Running Health Check...'));
|
|
64
67
|
const healthResult = await runHealthCheck();
|
|
68
|
+
|
|
69
|
+
// Track health check usage
|
|
70
|
+
trackingService.trackHealthCheck({
|
|
71
|
+
setup_recommended: healthResult.runSetup,
|
|
72
|
+
issues_found: healthResult.issues || 0
|
|
73
|
+
});
|
|
74
|
+
|
|
65
75
|
if (healthResult.runSetup) {
|
|
66
76
|
console.log(chalk.blue('⚙️ Starting Project Setup...'));
|
|
67
77
|
// Continue with setup flow
|
|
@@ -116,12 +126,14 @@ async function createClaudeConfig(options = {}) {
|
|
|
116
126
|
|
|
117
127
|
// Handle analytics dashboard
|
|
118
128
|
if (options.analytics) {
|
|
129
|
+
trackingService.trackAnalyticsDashboard({ page: 'dashboard', source: 'command_line' });
|
|
119
130
|
await runAnalytics(options);
|
|
120
131
|
return;
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
// Handle chats/agents dashboard
|
|
124
135
|
if (options.chats || options.agents) {
|
|
136
|
+
trackingService.trackAnalyticsDashboard({ page: 'agents', source: 'command_line' });
|
|
125
137
|
await runAnalytics({ ...options, openTo: 'agents' });
|
|
126
138
|
return;
|
|
127
139
|
}
|
|
@@ -130,6 +142,14 @@ async function createClaudeConfig(options = {}) {
|
|
|
130
142
|
let shouldRunSetup = false;
|
|
131
143
|
if (options.healthCheck || options.health || options.check || options.verify) {
|
|
132
144
|
const healthResult = await runHealthCheck();
|
|
145
|
+
|
|
146
|
+
// Track health check usage
|
|
147
|
+
trackingService.trackHealthCheck({
|
|
148
|
+
setup_recommended: healthResult.runSetup,
|
|
149
|
+
issues_found: healthResult.issues || 0,
|
|
150
|
+
source: 'command_line'
|
|
151
|
+
});
|
|
152
|
+
|
|
133
153
|
if (healthResult.runSetup) {
|
|
134
154
|
console.log(chalk.blue('⚙️ Starting Project Setup...'));
|
|
135
155
|
shouldRunSetup = true;
|
|
@@ -155,8 +175,8 @@ async function createClaudeConfig(options = {}) {
|
|
|
155
175
|
|
|
156
176
|
let config;
|
|
157
177
|
if (options.yes) {
|
|
158
|
-
// Use defaults
|
|
159
|
-
const selectedLanguage = options.language || projectInfo.detectedLanguage || 'common';
|
|
178
|
+
// Use defaults - prioritize --template over --language for backward compatibility
|
|
179
|
+
const selectedLanguage = options.template || options.language || projectInfo.detectedLanguage || 'common';
|
|
160
180
|
|
|
161
181
|
// Check if selected language is coming soon
|
|
162
182
|
if (selectedLanguage && TEMPLATES_CONFIG[selectedLanguage] && TEMPLATES_CONFIG[selectedLanguage].comingSoon) {
|
|
@@ -244,8 +264,8 @@ async function createClaudeConfig(options = {}) {
|
|
|
244
264
|
console.log(chalk.white(' 2. Customize the configuration for your project'));
|
|
245
265
|
console.log(chalk.white(' 3. Start using Claude Code with: claude'));
|
|
246
266
|
console.log('');
|
|
247
|
-
console.log(chalk.blue('🌐 View all available templates at: https://
|
|
248
|
-
console.log(chalk.blue('📖 Read the complete documentation at: https://
|
|
267
|
+
console.log(chalk.blue('🌐 View all available templates at: https://aitmpl.com/'));
|
|
268
|
+
console.log(chalk.blue('📖 Read the complete documentation at: https://aitmpl.com/docu/'));
|
|
249
269
|
|
|
250
270
|
if (config.language !== 'common') {
|
|
251
271
|
console.log(chalk.yellow(`💡 Language-specific features for ${config.language} have been configured`));
|
|
@@ -262,6 +282,17 @@ async function createClaudeConfig(options = {}) {
|
|
|
262
282
|
if (config.mcps && config.mcps.length > 0) {
|
|
263
283
|
console.log(chalk.blue(`🔧 ${config.mcps.length} MCP servers have been configured`));
|
|
264
284
|
}
|
|
285
|
+
|
|
286
|
+
// Track successful template installation
|
|
287
|
+
if (!options.agent && !options.command && !options.mcp) {
|
|
288
|
+
trackingService.trackTemplateInstallation(config.language, config.framework, {
|
|
289
|
+
installation_method: options.setupFromMenu ? 'interactive_menu' : 'command_line',
|
|
290
|
+
dry_run: options.dryRun || false,
|
|
291
|
+
hooks_count: config.hooks ? config.hooks.length : 0,
|
|
292
|
+
mcps_count: config.mcps ? config.mcps.length : 0,
|
|
293
|
+
project_detected: !!options.detectedProject
|
|
294
|
+
});
|
|
295
|
+
}
|
|
265
296
|
|
|
266
297
|
// Run post-installation validation
|
|
267
298
|
if (!options.dryRun) {
|
|
@@ -274,46 +305,40 @@ async function installIndividualAgent(agentName, targetDir, options) {
|
|
|
274
305
|
console.log(chalk.blue(`🤖 Installing agent: ${agentName}`));
|
|
275
306
|
|
|
276
307
|
try {
|
|
277
|
-
//
|
|
278
|
-
const
|
|
279
|
-
|
|
308
|
+
// Download agent directly from GitHub
|
|
309
|
+
const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/agents/${agentName}.md`;
|
|
310
|
+
console.log(chalk.gray(`📥 Downloading from GitHub (main branch)...`));
|
|
280
311
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const agents = await fs.readdir(componentsPath);
|
|
288
|
-
agents.filter(f => f.endsWith('.md')).forEach(agent => {
|
|
289
|
-
console.log(chalk.cyan(` - ${agent.replace('.md', '')}`));
|
|
290
|
-
});
|
|
312
|
+
const response = await fetch(githubUrl);
|
|
313
|
+
if (!response.ok) {
|
|
314
|
+
if (response.status === 404) {
|
|
315
|
+
console.log(chalk.red(`❌ Agent "${agentName}" not found`));
|
|
316
|
+
console.log(chalk.yellow('Available agents: api-security-audit, database-optimization, react-performance-optimization'));
|
|
317
|
+
return;
|
|
291
318
|
}
|
|
292
|
-
|
|
319
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
293
320
|
}
|
|
294
321
|
|
|
295
|
-
|
|
296
|
-
// the appropriate language/framework and install the complete template
|
|
297
|
-
const agentContent = await fs.readFile(agentFile, 'utf8');
|
|
298
|
-
const language = extractLanguageFromAgent(agentContent, agentName);
|
|
299
|
-
const framework = extractFrameworkFromAgent(agentContent, agentName);
|
|
322
|
+
const agentContent = await response.text();
|
|
300
323
|
|
|
301
|
-
|
|
302
|
-
|
|
324
|
+
// Create .claude/agents directory if it doesn't exist
|
|
325
|
+
const agentsDir = path.join(targetDir, '.claude', 'agents');
|
|
326
|
+
await fs.ensureDir(agentsDir);
|
|
303
327
|
|
|
304
|
-
//
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
language,
|
|
308
|
-
framework,
|
|
309
|
-
yes: true,
|
|
310
|
-
targetDirectory: targetDir,
|
|
311
|
-
agent: null // Remove agent to avoid recursion
|
|
312
|
-
};
|
|
313
|
-
delete setupOptions.agent;
|
|
314
|
-
await createClaudeConfig(setupOptions);
|
|
328
|
+
// Write the agent file
|
|
329
|
+
const targetFile = path.join(agentsDir, `${agentName}.md`);
|
|
330
|
+
await fs.writeFile(targetFile, agentContent, 'utf8');
|
|
315
331
|
|
|
316
332
|
console.log(chalk.green(`✅ Agent "${agentName}" installed successfully!`));
|
|
333
|
+
console.log(chalk.cyan(`📁 Installed to: ${path.relative(targetDir, targetFile)}`));
|
|
334
|
+
console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
|
|
335
|
+
|
|
336
|
+
// Track successful agent installation
|
|
337
|
+
trackingService.trackDownload('agent', agentName, {
|
|
338
|
+
installation_type: 'individual_component',
|
|
339
|
+
target_directory: path.relative(process.cwd(), targetDir),
|
|
340
|
+
source: 'github_main'
|
|
341
|
+
});
|
|
317
342
|
|
|
318
343
|
} catch (error) {
|
|
319
344
|
console.log(chalk.red(`❌ Error installing agent: ${error.message}`));
|
|
@@ -324,34 +349,40 @@ async function installIndividualCommand(commandName, targetDir, options) {
|
|
|
324
349
|
console.log(chalk.blue(`⚡ Installing command: ${commandName}`));
|
|
325
350
|
|
|
326
351
|
try {
|
|
327
|
-
//
|
|
328
|
-
const
|
|
329
|
-
|
|
352
|
+
// Download command directly from GitHub
|
|
353
|
+
const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/commands/${commandName}.md`;
|
|
354
|
+
console.log(chalk.gray(`📥 Downloading from GitHub (main branch)...`));
|
|
330
355
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const commands = await fs.readdir(componentsPath);
|
|
338
|
-
commands.filter(f => f.endsWith('.md')).forEach(command => {
|
|
339
|
-
console.log(chalk.cyan(` - ${command.replace('.md', '')}`));
|
|
340
|
-
});
|
|
356
|
+
const response = await fetch(githubUrl);
|
|
357
|
+
if (!response.ok) {
|
|
358
|
+
if (response.status === 404) {
|
|
359
|
+
console.log(chalk.red(`❌ Command "${commandName}" not found`));
|
|
360
|
+
console.log(chalk.yellow('Available commands: check-file, generate-tests'));
|
|
361
|
+
return;
|
|
341
362
|
}
|
|
342
|
-
|
|
363
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
343
364
|
}
|
|
344
365
|
|
|
366
|
+
const commandContent = await response.text();
|
|
367
|
+
|
|
345
368
|
// Create .claude/commands directory if it doesn't exist
|
|
346
369
|
const commandsDir = path.join(targetDir, '.claude', 'commands');
|
|
347
370
|
await fs.ensureDir(commandsDir);
|
|
348
371
|
|
|
349
|
-
//
|
|
372
|
+
// Write the command file
|
|
350
373
|
const targetFile = path.join(commandsDir, `${commandName}.md`);
|
|
351
|
-
await fs.
|
|
374
|
+
await fs.writeFile(targetFile, commandContent, 'utf8');
|
|
352
375
|
|
|
353
376
|
console.log(chalk.green(`✅ Command "${commandName}" installed successfully!`));
|
|
354
377
|
console.log(chalk.cyan(`📁 Installed to: ${path.relative(targetDir, targetFile)}`));
|
|
378
|
+
console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
|
|
379
|
+
|
|
380
|
+
// Track successful command installation
|
|
381
|
+
trackingService.trackDownload('command', commandName, {
|
|
382
|
+
installation_type: 'individual_command',
|
|
383
|
+
target_directory: path.relative(process.cwd(), targetDir),
|
|
384
|
+
source: 'github_main'
|
|
385
|
+
});
|
|
355
386
|
|
|
356
387
|
} catch (error) {
|
|
357
388
|
console.log(chalk.red(`❌ Error installing command: ${error.message}`));
|
|
@@ -362,26 +393,22 @@ async function installIndividualMCP(mcpName, targetDir, options) {
|
|
|
362
393
|
console.log(chalk.blue(`🔌 Installing MCP: ${mcpName}`));
|
|
363
394
|
|
|
364
395
|
try {
|
|
365
|
-
//
|
|
366
|
-
const
|
|
367
|
-
|
|
396
|
+
// Download MCP directly from GitHub
|
|
397
|
+
const githubUrl = `https://raw.githubusercontent.com/davila7/claude-code-templates/main/cli-tool/components/mcps/${mcpName}.json`;
|
|
398
|
+
console.log(chalk.gray(`📥 Downloading from GitHub (main branch)...`));
|
|
368
399
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
const mcps = await fs.readdir(componentsPath);
|
|
376
|
-
mcps.filter(f => f.endsWith('.json')).forEach(mcp => {
|
|
377
|
-
console.log(chalk.cyan(` - ${mcp.replace('.json', '')}`));
|
|
378
|
-
});
|
|
400
|
+
const response = await fetch(githubUrl);
|
|
401
|
+
if (!response.ok) {
|
|
402
|
+
if (response.status === 404) {
|
|
403
|
+
console.log(chalk.red(`❌ MCP "${mcpName}" not found`));
|
|
404
|
+
console.log(chalk.yellow('Available MCPs: web-fetch, filesystem-access, github-integration, memory-integration, mysql-integration, postgresql-integration, deepgraph-react, deepgraph-nextjs, deepgraph-typescript, deepgraph-vue'));
|
|
405
|
+
return;
|
|
379
406
|
}
|
|
380
|
-
|
|
407
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
381
408
|
}
|
|
382
409
|
|
|
383
|
-
|
|
384
|
-
const mcpConfig =
|
|
410
|
+
const mcpConfigText = await response.text();
|
|
411
|
+
const mcpConfig = JSON.parse(mcpConfigText);
|
|
385
412
|
|
|
386
413
|
// Check if .mcp.json exists in target directory
|
|
387
414
|
const targetMcpFile = path.join(targetDir, '.mcp.json');
|
|
@@ -403,6 +430,15 @@ async function installIndividualMCP(mcpName, targetDir, options) {
|
|
|
403
430
|
|
|
404
431
|
console.log(chalk.green(`✅ MCP "${mcpName}" installed successfully!`));
|
|
405
432
|
console.log(chalk.cyan(`📁 Configuration merged into: ${path.relative(targetDir, targetMcpFile)}`));
|
|
433
|
+
console.log(chalk.cyan(`📦 Downloaded from: ${githubUrl}`));
|
|
434
|
+
|
|
435
|
+
// Track successful MCP installation
|
|
436
|
+
trackingService.trackDownload('mcp', mcpName, {
|
|
437
|
+
installation_type: 'individual_mcp',
|
|
438
|
+
merged_with_existing: existingConfig !== null,
|
|
439
|
+
servers_count: Object.keys(mergedConfig.mcpServers || {}).length,
|
|
440
|
+
source: 'github_main'
|
|
441
|
+
});
|
|
406
442
|
|
|
407
443
|
} catch (error) {
|
|
408
444
|
console.log(chalk.red(`❌ Error installing MCP: ${error.message}`));
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TrackingService - Download analytics using GitHub Issues as backend
|
|
3
|
+
* Records component installations for analytics without impacting user experience
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class TrackingService {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.repoOwner = 'davila7';
|
|
9
|
+
this.repoName = 'claude-code-templates';
|
|
10
|
+
this.trackingEnabled = this.shouldEnableTracking();
|
|
11
|
+
this.timeout = 5000; // 5s timeout for tracking requests
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if tracking should be enabled (respects user privacy)
|
|
16
|
+
*/
|
|
17
|
+
shouldEnableTracking() {
|
|
18
|
+
// Allow users to opt-out
|
|
19
|
+
if (process.env.CCT_NO_TRACKING === 'true' ||
|
|
20
|
+
process.env.CCT_NO_ANALYTICS === 'true' ||
|
|
21
|
+
process.env.CI === 'true') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Enable by default (anonymous usage data only)
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Track a component download/installation
|
|
31
|
+
* @param {string} componentType - 'agent', 'command', or 'mcp'
|
|
32
|
+
* @param {string} componentName - Name of the component
|
|
33
|
+
* @param {object} metadata - Additional context (optional)
|
|
34
|
+
*/
|
|
35
|
+
async trackDownload(componentType, componentName, metadata = {}) {
|
|
36
|
+
if (!this.trackingEnabled) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Create tracking payload
|
|
42
|
+
const trackingData = this.createTrackingPayload(componentType, componentName, metadata);
|
|
43
|
+
|
|
44
|
+
// Fire-and-forget tracking (don't block user experience)
|
|
45
|
+
this.sendTrackingData(trackingData)
|
|
46
|
+
.catch(error => {
|
|
47
|
+
// Silent failure - tracking should never impact functionality
|
|
48
|
+
console.debug('📊 Tracking info (non-critical):', error.message);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Silently handle any tracking errors
|
|
53
|
+
console.debug('📊 Analytics error (non-critical):', error.message);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Create standardized tracking payload
|
|
59
|
+
*/
|
|
60
|
+
createTrackingPayload(componentType, componentName, metadata) {
|
|
61
|
+
const timestamp = new Date().toISOString();
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
event: 'component_download',
|
|
65
|
+
component_type: componentType,
|
|
66
|
+
component_name: componentName,
|
|
67
|
+
timestamp: timestamp,
|
|
68
|
+
session_id: this.generateSessionId(),
|
|
69
|
+
environment: {
|
|
70
|
+
node_version: process.version,
|
|
71
|
+
platform: process.platform,
|
|
72
|
+
arch: process.arch,
|
|
73
|
+
cli_version: this.getCliVersion()
|
|
74
|
+
},
|
|
75
|
+
metadata: metadata
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Send tracking data to GitHub Issues (async, non-blocking)
|
|
81
|
+
*/
|
|
82
|
+
async sendTrackingData(trackingData) {
|
|
83
|
+
const title = `📊 ${trackingData.component_type}:${trackingData.component_name} - ${trackingData.timestamp.split('T')[0]}`;
|
|
84
|
+
|
|
85
|
+
const body = `\`\`\`json
|
|
86
|
+
${JSON.stringify(trackingData, null, 2)}
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
<!-- ANALYTICS_DATA -->
|
|
90
|
+
Component: **${trackingData.component_name}** (${trackingData.component_type})
|
|
91
|
+
Platform: ${trackingData.environment.platform} ${trackingData.environment.arch}
|
|
92
|
+
Node: ${trackingData.environment.node_version}
|
|
93
|
+
CLI: ${trackingData.environment.cli_version}
|
|
94
|
+
Session: \`${trackingData.session_id}\`
|
|
95
|
+
`;
|
|
96
|
+
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(`https://api.github.com/repos/${this.repoOwner}/${this.repoName}/issues`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': 'application/json',
|
|
105
|
+
'User-Agent': 'claude-code-templates-cli'
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
title: title,
|
|
109
|
+
body: body,
|
|
110
|
+
labels: ['📊 analytics', 'download-tracking', `type:${trackingData.component_type}`]
|
|
111
|
+
}),
|
|
112
|
+
signal: controller.signal
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
clearTimeout(timeoutId);
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`GitHub API responded with ${response.status}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.debug('📊 Download tracked successfully');
|
|
122
|
+
|
|
123
|
+
} catch (error) {
|
|
124
|
+
clearTimeout(timeoutId);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a session ID for grouping related downloads
|
|
131
|
+
*/
|
|
132
|
+
generateSessionId() {
|
|
133
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get CLI version from package.json
|
|
138
|
+
*/
|
|
139
|
+
getCliVersion() {
|
|
140
|
+
try {
|
|
141
|
+
const path = require('path');
|
|
142
|
+
const fs = require('fs');
|
|
143
|
+
const packagePath = path.join(__dirname, '..', 'package.json');
|
|
144
|
+
const packageData = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
145
|
+
return packageData.version || 'unknown';
|
|
146
|
+
} catch (error) {
|
|
147
|
+
return 'unknown';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Track template installation (full project setup)
|
|
153
|
+
*/
|
|
154
|
+
async trackTemplateInstallation(language, framework, metadata = {}) {
|
|
155
|
+
return this.trackDownload('template', `${language}/${framework}`, {
|
|
156
|
+
...metadata,
|
|
157
|
+
installation_type: 'full_template'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Track health check usage
|
|
163
|
+
*/
|
|
164
|
+
async trackHealthCheck(results = {}) {
|
|
165
|
+
return this.trackDownload('health-check', 'system-validation', {
|
|
166
|
+
installation_type: 'health_check',
|
|
167
|
+
results_summary: results
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Track analytics dashboard usage
|
|
173
|
+
*/
|
|
174
|
+
async trackAnalyticsDashboard(metadata = {}) {
|
|
175
|
+
return this.trackDownload('analytics', 'dashboard-launch', {
|
|
176
|
+
installation_type: 'analytics_dashboard',
|
|
177
|
+
...metadata
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Export singleton instance
|
|
183
|
+
const trackingService = new TrackingService();
|
|
184
|
+
|
|
185
|
+
module.exports = {
|
|
186
|
+
TrackingService,
|
|
187
|
+
trackingService
|
|
188
|
+
};
|