@paulduvall/claude-dev-toolkit 0.0.1-alpha.1

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.
@@ -0,0 +1,245 @@
1
+ const path = require('path');
2
+
3
+ /**
4
+ * Manages command selection and categorization
5
+ * Extracted from InteractiveSetupWizard for better separation of concerns
6
+ */
7
+ class CommandSelector {
8
+ constructor() {
9
+ this.commandCategories = {
10
+ 'planning': ['xplanning', 'xspec', 'xarchitecture'],
11
+ 'development': ['xgit', 'xtest', 'xquality', 'xrefactor', 'xtdd'],
12
+ 'security': ['xsecurity', 'xpolicy', 'xcompliance'],
13
+ 'deployment': ['xrelease', 'xpipeline', 'xinfra'],
14
+ 'documentation': ['xdocs']
15
+ };
16
+
17
+ this.presets = {
18
+ 'developer': {
19
+ installationType: 'standard',
20
+ commandSets: ['development', 'planning'],
21
+ securityHooks: true,
22
+ template: 'basic'
23
+ },
24
+ 'security-focused': {
25
+ installationType: 'full',
26
+ commandSets: ['security', 'development'],
27
+ securityHooks: true,
28
+ template: 'security-focused'
29
+ },
30
+ 'minimal': {
31
+ installationType: 'minimal',
32
+ commandSets: [],
33
+ securityHooks: false,
34
+ template: 'basic'
35
+ }
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Get available command categories
41
+ * @returns {Object} Command categories with commands
42
+ */
43
+ getCommandCategories() {
44
+ return this.commandCategories;
45
+ }
46
+
47
+ /**
48
+ * Get category names
49
+ * @returns {Array<string>} Category names
50
+ */
51
+ getCategoryNames() {
52
+ return Object.keys(this.commandCategories);
53
+ }
54
+
55
+ /**
56
+ * Get commands for a specific category
57
+ * @param {string} category - Category name
58
+ * @returns {Array<string>} Commands in category
59
+ */
60
+ getCommandsForCategory(category) {
61
+ return this.commandCategories[category] || [];
62
+ }
63
+
64
+ /**
65
+ * Select command sets based on categories
66
+ * @param {Array<string>} categories - Selected categories
67
+ * @returns {Object} Selection result with commands
68
+ */
69
+ selectCommandSets(categories) {
70
+ const validCategories = categories.filter(cat =>
71
+ this.commandCategories.hasOwnProperty(cat)
72
+ );
73
+
74
+ return {
75
+ selected: validCategories,
76
+ commands: validCategories.flatMap(cat => this.commandCategories[cat] || []),
77
+ invalid: categories.filter(cat => !this.commandCategories.hasOwnProperty(cat))
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Get all available commands
83
+ * @returns {Array<string>} All commands across categories
84
+ */
85
+ getAllCommands() {
86
+ return Object.values(this.commandCategories).flat();
87
+ }
88
+
89
+ /**
90
+ * Get command count for each category
91
+ * @returns {Object} Category names mapped to command counts
92
+ */
93
+ getCategoryCommandCounts() {
94
+ const counts = {};
95
+ Object.entries(this.commandCategories).forEach(([category, commands]) => {
96
+ counts[category] = commands.length;
97
+ });
98
+ return counts;
99
+ }
100
+
101
+ /**
102
+ * Find category for a specific command
103
+ * @param {string} command - Command name
104
+ * @returns {string|null} Category name or null if not found
105
+ */
106
+ findCategoryForCommand(command) {
107
+ for (const [category, commands] of Object.entries(this.commandCategories)) {
108
+ if (commands.includes(command)) {
109
+ return category;
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+
115
+ /**
116
+ * Get available presets
117
+ * @returns {Object} Available presets
118
+ */
119
+ getPresets() {
120
+ return this.presets;
121
+ }
122
+
123
+ /**
124
+ * Get preset names
125
+ * @returns {Array<string>} Preset names
126
+ */
127
+ getPresetNames() {
128
+ return Object.keys(this.presets);
129
+ }
130
+
131
+ /**
132
+ * Apply a preset configuration
133
+ * @param {string} presetName - Name of preset to apply
134
+ * @returns {Object|null} Preset configuration or null if not found
135
+ */
136
+ applyPreset(presetName) {
137
+ return this.presets[presetName] || null;
138
+ }
139
+
140
+ /**
141
+ * Validate command selection
142
+ * @param {Array<string>} commands - Commands to validate
143
+ * @returns {Object} Validation result
144
+ */
145
+ validateCommandSelection(commands) {
146
+ const allCommands = this.getAllCommands();
147
+ const valid = commands.filter(cmd => allCommands.includes(cmd));
148
+ const invalid = commands.filter(cmd => !allCommands.includes(cmd));
149
+
150
+ return {
151
+ valid: invalid.length === 0,
152
+ validCommands: valid,
153
+ invalidCommands: invalid,
154
+ errors: invalid.length > 0 ? [`Invalid commands: ${invalid.join(', ')}`] : []
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Get command statistics
160
+ * @returns {Object} Statistics about commands and categories
161
+ */
162
+ getCommandStats() {
163
+ const totalCommands = this.getAllCommands().length;
164
+ const categoryCount = Object.keys(this.commandCategories).length;
165
+ const presetCount = Object.keys(this.presets).length;
166
+
167
+ return {
168
+ totalCommands,
169
+ categoryCount,
170
+ presetCount,
171
+ averageCommandsPerCategory: Math.round(totalCommands / categoryCount),
172
+ categoriesWithCounts: this.getCategoryCommandCounts()
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Add new command category
178
+ * @param {string} category - Category name
179
+ * @param {Array<string>} commands - Commands in category
180
+ * @returns {boolean} Success status
181
+ */
182
+ addCommandCategory(category, commands) {
183
+ if (this.commandCategories.hasOwnProperty(category)) {
184
+ return false; // Category already exists
185
+ }
186
+
187
+ this.commandCategories[category] = [...commands];
188
+ return true;
189
+ }
190
+
191
+ /**
192
+ * Remove command category
193
+ * @param {string} category - Category name to remove
194
+ * @returns {boolean} Success status
195
+ */
196
+ removeCommandCategory(category) {
197
+ if (!this.commandCategories.hasOwnProperty(category)) {
198
+ return false; // Category doesn't exist
199
+ }
200
+
201
+ delete this.commandCategories[category];
202
+ return true;
203
+ }
204
+
205
+ /**
206
+ * Add command to existing category
207
+ * @param {string} category - Category name
208
+ * @param {string} command - Command to add
209
+ * @returns {boolean} Success status
210
+ */
211
+ addCommandToCategory(category, command) {
212
+ if (!this.commandCategories.hasOwnProperty(category)) {
213
+ return false; // Category doesn't exist
214
+ }
215
+
216
+ if (this.commandCategories[category].includes(command)) {
217
+ return false; // Command already exists in category
218
+ }
219
+
220
+ this.commandCategories[category].push(command);
221
+ return true;
222
+ }
223
+
224
+ /**
225
+ * Remove command from category
226
+ * @param {string} category - Category name
227
+ * @param {string} command - Command to remove
228
+ * @returns {boolean} Success status
229
+ */
230
+ removeCommandFromCategory(category, command) {
231
+ if (!this.commandCategories.hasOwnProperty(category)) {
232
+ return false; // Category doesn't exist
233
+ }
234
+
235
+ const index = this.commandCategories[category].indexOf(command);
236
+ if (index === -1) {
237
+ return false; // Command not in category
238
+ }
239
+
240
+ this.commandCategories[category].splice(index, 1);
241
+ return true;
242
+ }
243
+ }
244
+
245
+ module.exports = CommandSelector;
package/lib/config.js ADDED
@@ -0,0 +1,182 @@
1
+ // Configuration management for Claude Dev Toolkit
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const fs = require('fs');
5
+
6
+ /**
7
+ * Parse JSONC (JSON with Comments) format
8
+ * Handles comment key-value pairs and block comments
9
+ * @param {string} content - Raw JSONC content
10
+ * @returns {Object} - Parsed JSON object
11
+ * @throws {Error} - If JSON parsing fails
12
+ */
13
+ function parseJSONC(content) {
14
+ const lines = content.split('\n');
15
+ const cleanedLines = [];
16
+
17
+ for (const line of lines) {
18
+ // Skip lines that are comment key-value pairs like '"// comment": "value",'
19
+ if (line.trim().match(/^"\/\/[^"]*":\s*"[^"]*",?\s*$/)) {
20
+ continue;
21
+ }
22
+ // Skip pure comment lines
23
+ if (line.trim().startsWith('//')) {
24
+ continue;
25
+ }
26
+ cleanedLines.push(line);
27
+ }
28
+
29
+ const cleanedContent = cleanedLines.join('\n')
30
+ .replace(/\/\*[\s\S]*?\*\//g, '') // Remove /* */ comments
31
+ .replace(/,(\s*[}\]])/g, '$1'); // Remove trailing commas
32
+
33
+ return JSON.parse(cleanedContent);
34
+ }
35
+
36
+ /**
37
+ * Deep merge two objects, with second object taking precedence
38
+ * @param {Object} target - Target object to merge into
39
+ * @param {Object} source - Source object to merge from
40
+ * @returns {Object} - Merged object
41
+ */
42
+ function deepMerge(target, source) {
43
+ const result = { ...target };
44
+
45
+ for (const [key, value] of Object.entries(source)) {
46
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
47
+ result[key] = deepMerge(result[key] || {}, value);
48
+ } else {
49
+ result[key] = value;
50
+ }
51
+ }
52
+
53
+ return result;
54
+ }
55
+
56
+ /**
57
+ * Apply a configuration template to Claude Code settings
58
+ * Implements REQ-009: Configuration Template Application
59
+ *
60
+ * @param {string} templatePath - Path to the template file
61
+ * @param {string} settingsPath - Path to the settings file to create/update
62
+ * @returns {boolean} - True if successful, false otherwise
63
+ */
64
+ function applyConfigurationTemplate(templatePath, settingsPath) {
65
+ try {
66
+ // Validate inputs
67
+ if (!templatePath || !settingsPath) {
68
+ return false;
69
+ }
70
+
71
+ // Check if template exists
72
+ if (!fs.existsSync(templatePath)) {
73
+ return false;
74
+ }
75
+
76
+ // Read and parse template (handle JSONC with comments)
77
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
78
+ let templateData;
79
+
80
+ try {
81
+ templateData = parseJSONC(templateContent);
82
+ } catch (parseError) {
83
+ // Invalid JSON/JSONC format
84
+ return false;
85
+ }
86
+
87
+ // Validate template data
88
+ if (!templateData || typeof templateData !== 'object') {
89
+ return false;
90
+ }
91
+
92
+ // Read existing settings if they exist
93
+ let existingSettings = {};
94
+ if (fs.existsSync(settingsPath)) {
95
+ try {
96
+ const existingContent = fs.readFileSync(settingsPath, 'utf8');
97
+ existingSettings = JSON.parse(existingContent);
98
+ } catch (parseError) {
99
+ // If existing settings are invalid, start fresh but log the issue
100
+ existingSettings = {};
101
+ }
102
+ }
103
+
104
+ // Deep merge template with existing settings (template takes precedence)
105
+ const mergedSettings = deepMerge(existingSettings, templateData);
106
+
107
+ // Ensure target directory exists with correct permissions
108
+ const settingsDir = path.dirname(settingsPath);
109
+ fs.mkdirSync(settingsDir, { recursive: true, mode: 0o755 });
110
+
111
+ // Write merged settings with formatted output
112
+ const settingsJson = JSON.stringify(mergedSettings, null, 2);
113
+ fs.writeFileSync(settingsPath, settingsJson, { mode: 0o644 });
114
+
115
+ // Verify file was created successfully
116
+ return fs.existsSync(settingsPath);
117
+
118
+ } catch (error) {
119
+ // Log error in development but don't expose details
120
+ if (process.env.NODE_ENV === 'development') {
121
+ console.error('Configuration template application error:', error);
122
+ }
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get available configuration templates
129
+ * @param {string} templatesDir - Directory containing templates
130
+ * @returns {Array} - List of available templates with metadata
131
+ */
132
+ function getAvailableTemplates(templatesDir) {
133
+ try {
134
+ const templates = [];
135
+ const files = fs.readdirSync(templatesDir);
136
+
137
+ for (const file of files) {
138
+ if (file.endsWith('.json')) {
139
+ const templatePath = path.join(templatesDir, file);
140
+ try {
141
+ const content = fs.readFileSync(templatePath, 'utf8');
142
+ const data = parseJSONC(content);
143
+
144
+ templates.push({
145
+ id: path.basename(file, '.json'),
146
+ name: file,
147
+ path: templatePath,
148
+ description: data['// Description'] || `${file} template`,
149
+ features: Object.keys(data).filter(key => !key.startsWith('//')).length
150
+ });
151
+ } catch (error) {
152
+ // Skip invalid templates
153
+ continue;
154
+ }
155
+ }
156
+ }
157
+
158
+ return templates;
159
+ } catch (error) {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ module.exports = {
165
+ getConfigPath: () => {
166
+ return path.join(os.homedir(), '.claude', 'commands');
167
+ },
168
+
169
+ defaultConfig: {
170
+ commandsPath: './commands',
171
+ hooksEnabled: true,
172
+ colorOutput: true
173
+ },
174
+
175
+ // REQ-009 Implementation
176
+ applyConfigurationTemplate,
177
+ getAvailableTemplates,
178
+
179
+ // Utility functions (exposed for testing)
180
+ parseJSONC,
181
+ deepMerge
182
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Context Utilities
3
+ *
4
+ * Shared context creation patterns to eliminate duplication and provide
5
+ * consistent context objects across the application.
6
+ *
7
+ * Features:
8
+ * - Standardized context object creation
9
+ * - Context validation and enhancement
10
+ * - Operation context tracking
11
+ * - Context-aware processing patterns
12
+ */
13
+
14
+ class ContextUtils {
15
+ constructor() {
16
+ this.config = {
17
+ contextTypes: {
18
+ VALIDATION: 'validation',
19
+ OPERATION: 'operation',
20
+ ERROR_HANDLING: 'error_handling',
21
+ INSTALLATION: 'installation',
22
+ SYSTEM: 'system'
23
+ },
24
+ operationTypes: {
25
+ COMMAND_INSTALLATION: 'command_installation',
26
+ DEPENDENCY_CHECK: 'dependency_check',
27
+ PERMISSION_CHECK: 'permission_check',
28
+ SYSTEM_VALIDATION: 'system_validation',
29
+ CONFIGURATION: 'configuration'
30
+ }
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Create base context object with common properties
36
+ * @param {string} type - Context type
37
+ * @param {Object} data - Context data
38
+ * @param {Object} options - Additional options
39
+ * @returns {Object} Base context object
40
+ */
41
+ createBaseContext(type, data = {}, options = {}) {
42
+ return {
43
+ type,
44
+ timestamp: new Date().toISOString(),
45
+ id: this._generateContextId(),
46
+ data,
47
+ metadata: {
48
+ version: '1.0.0',
49
+ source: 'context-utils',
50
+ ...options.metadata
51
+ },
52
+ ...options
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Create operation context for tracking operations
58
+ * @param {string} operation - Operation name
59
+ * @param {Object} parameters - Operation parameters
60
+ * @param {Object} environment - Environment information
61
+ * @returns {Object} Operation context
62
+ */
63
+ createOperationContext(operation, parameters = {}, environment = {}) {
64
+ return this.createBaseContext(this.config.contextTypes.OPERATION, {
65
+ operation,
66
+ parameters,
67
+ environment: {
68
+ platform: process.platform,
69
+ nodeVersion: process.version,
70
+ workingDirectory: process.cwd(),
71
+ ...environment
72
+ },
73
+ status: 'initialized',
74
+ startTime: new Date().toISOString(),
75
+ progress: {
76
+ current: 0,
77
+ total: 1,
78
+ steps: []
79
+ }
80
+ });\n }\n \n /**\n * Create validation context for input validation\n * @param {*} input - Input to validate\n * @param {Object} rules - Validation rules\n * @param {Object} options - Validation options\n * @returns {Object} Validation context\n */\n createValidationContext(input, rules = {}, options = {}) {\n return this.createBaseContext(this.config.contextTypes.VALIDATION, {\n input,\n rules,\n options: {\n strict: false,\n allowEmpty: false,\n transformInput: false,\n ...options\n },\n results: {\n valid: null,\n errors: [],\n warnings: [],\n transformed: null\n }\n });\n }\n \n /**\n * Create error handling context\n * @param {Error} error - Error to handle\n * @param {Object} operation - Operation context where error occurred\n * @param {Object} recovery - Recovery options\n * @returns {Object} Error handling context\n */\n createErrorHandlingContext(error, operation = {}, recovery = {}) {\n return this.createBaseContext(this.config.contextTypes.ERROR_HANDLING, {\n error: {\n original: error,\n code: error?.code,\n message: error?.message,\n type: error?.constructor?.name,\n stack: error?.stack\n },\n operation,\n recovery: {\n attempted: false,\n strategies: [],\n successful: false,\n ...recovery\n },\n handling: {\n strategy: null,\n handled: false,\n escalated: false\n }\n });\n }\n \n /**\n * Create installation context for tracking installations\n * @param {string} target - Installation target (command, dependency, etc.)\n * @param {Object} configuration - Installation configuration\n * @param {Object} environment - Environment details\n * @returns {Object} Installation context\n */\n createInstallationContext(target, configuration = {}, environment = {}) {\n return this.createBaseContext(this.config.contextTypes.INSTALLATION, {\n target,\n configuration,\n environment: {\n platform: process.platform,\n arch: process.arch,\n userHome: require('os').homedir(),\n ...environment\n },\n installation: {\n phase: 'preparation',\n steps: [],\n backup: null,\n rollback: null\n },\n validation: {\n preInstall: null,\n postInstall: null\n }\n });\n }\n \n /**\n * Create system context for system operations\n * @param {Object} systemInfo - System information\n * @param {Object} requirements - System requirements\n * @returns {Object} System context\n */\n createSystemContext(systemInfo = {}, requirements = {}) {\n const os = require('os');\n \n return this.createBaseContext(this.config.contextTypes.SYSTEM, {\n system: {\n platform: process.platform,\n arch: process.arch,\n nodeVersion: process.version,\n totalMemory: os.totalmem(),\n freeMemory: os.freemem(),\n homeDir: os.homedir(),\n tempDir: os.tmpdir(),\n ...systemInfo\n },\n requirements,\n compatibility: {\n checked: false,\n compatible: null,\n issues: []\n }\n });\n }\n \n /**\n * Enhance existing context with additional data\n * @param {Object} context - Existing context to enhance\n * @param {Object} enhancement - Enhancement data\n * @returns {Object} Enhanced context\n */\n enhanceContext(context, enhancement) {\n if (!context || typeof context !== 'object') {\n throw new Error('Context must be a valid object');\n }\n \n return {\n ...context,\n ...enhancement,\n metadata: {\n ...context.metadata,\n ...enhancement.metadata,\n enhanced: true,\n enhancedAt: new Date().toISOString()\n }\n };\n }\n \n /**\n * Update operation context progress\n * @param {Object} context - Operation context to update\n * @param {number} current - Current step\n * @param {number} total - Total steps (optional)\n * @param {string} step - Current step description\n * @returns {Object} Updated context\n */\n updateOperationProgress(context, current, total = null, step = null) {\n if (!this.isOperationContext(context)) {\n throw new Error('Context must be an operation context');\n }\n \n const updatedContext = { ...context };\n updatedContext.data.progress.current = current;\n \n if (total !== null) {\n updatedContext.data.progress.total = total;\n }\n \n if (step !== null) {\n updatedContext.data.progress.steps.push({\n step,\n timestamp: new Date().toISOString(),\n index: current\n });\n }\n \n // Update status based on progress\n if (current >= updatedContext.data.progress.total) {\n updatedContext.data.status = 'completed';\n updatedContext.data.endTime = new Date().toISOString();\n } else {\n updatedContext.data.status = 'in_progress';\n }\n \n return updatedContext;\n }\n \n /**\n * Mark operation context as failed\n * @param {Object} context - Operation context to mark as failed\n * @param {Error} error - Error that caused failure\n * @returns {Object} Updated context\n */\n markOperationFailed(context, error) {\n if (!this.isOperationContext(context)) {\n throw new Error('Context must be an operation context');\n }\n \n return {\n ...context,\n data: {\n ...context.data,\n status: 'failed',\n endTime: new Date().toISOString(),\n error: {\n code: error?.code,\n message: error?.message,\n type: error?.constructor?.name\n }\n }\n };\n }\n \n /**\n * Check if context is of specific type\n * @param {Object} context - Context to check\n * @param {string} type - Expected type\n * @returns {boolean} True if context is of expected type\n */\n isContextType(context, type) {\n return context && context.type === type;\n }\n \n /**\n * Check if context is an operation context\n * @param {Object} context - Context to check\n * @returns {boolean} True if operation context\n */\n isOperationContext(context) {\n return this.isContextType(context, this.config.contextTypes.OPERATION);\n }\n \n /**\n * Check if context is a validation context\n * @param {Object} context - Context to check\n * @returns {boolean} True if validation context\n */\n isValidationContext(context) {\n return this.isContextType(context, this.config.contextTypes.VALIDATION);\n }\n \n /**\n * Extract summary information from context\n * @param {Object} context - Context to summarize\n * @returns {Object} Context summary\n */\n getContextSummary(context) {\n if (!context || typeof context !== 'object') {\n return { valid: false, error: 'Invalid context' };\n }\n \n const summary = {\n type: context.type,\n id: context.id,\n timestamp: context.timestamp,\n valid: true\n };\n \n // Add type-specific summary information\n if (this.isOperationContext(context)) {\n summary.operation = {\n name: context.data?.operation,\n status: context.data?.status,\n progress: `${context.data?.progress?.current || 0}/${context.data?.progress?.total || 1}`\n };\n } else if (this.isValidationContext(context)) {\n summary.validation = {\n valid: context.data?.results?.valid,\n errors: context.data?.results?.errors?.length || 0,\n warnings: context.data?.results?.warnings?.length || 0\n };\n }\n \n return summary;\n }\n \n /**\n * Generate unique context ID\n * @returns {string} Unique context ID\n * @private\n */\n _generateContextId() {\n return `ctx_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;\n }\n}\n\nmodule.exports = ContextUtils;