claude-cli-advanced-starter-pack 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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/OVERVIEW.md +597 -0
  3. package/README.md +439 -0
  4. package/bin/gtask.js +282 -0
  5. package/bin/postinstall.js +53 -0
  6. package/package.json +69 -0
  7. package/src/agents/phase-dev-templates.js +1011 -0
  8. package/src/agents/templates.js +668 -0
  9. package/src/analysis/checklist-parser.js +414 -0
  10. package/src/analysis/codebase.js +481 -0
  11. package/src/cli/menu.js +958 -0
  12. package/src/commands/claude-audit.js +1482 -0
  13. package/src/commands/claude-settings.js +2243 -0
  14. package/src/commands/create-agent.js +681 -0
  15. package/src/commands/create-command.js +337 -0
  16. package/src/commands/create-hook.js +262 -0
  17. package/src/commands/create-phase-dev/codebase-analyzer.js +813 -0
  18. package/src/commands/create-phase-dev/documentation-generator.js +352 -0
  19. package/src/commands/create-phase-dev/post-completion.js +404 -0
  20. package/src/commands/create-phase-dev/scale-calculator.js +344 -0
  21. package/src/commands/create-phase-dev/wizard.js +492 -0
  22. package/src/commands/create-phase-dev.js +481 -0
  23. package/src/commands/create-skill.js +313 -0
  24. package/src/commands/create.js +446 -0
  25. package/src/commands/decompose.js +392 -0
  26. package/src/commands/detect-tech-stack.js +768 -0
  27. package/src/commands/explore-mcp/claude-md-updater.js +252 -0
  28. package/src/commands/explore-mcp/mcp-installer.js +346 -0
  29. package/src/commands/explore-mcp/mcp-registry.js +438 -0
  30. package/src/commands/explore-mcp.js +638 -0
  31. package/src/commands/gtask-init.js +641 -0
  32. package/src/commands/help.js +128 -0
  33. package/src/commands/init.js +1890 -0
  34. package/src/commands/install.js +250 -0
  35. package/src/commands/list.js +116 -0
  36. package/src/commands/roadmap.js +750 -0
  37. package/src/commands/setup-wizard.js +482 -0
  38. package/src/commands/setup.js +351 -0
  39. package/src/commands/sync.js +534 -0
  40. package/src/commands/test-run.js +456 -0
  41. package/src/commands/test-setup.js +456 -0
  42. package/src/commands/validate.js +67 -0
  43. package/src/config/tech-stack.defaults.json +182 -0
  44. package/src/config/tech-stack.schema.json +502 -0
  45. package/src/github/client.js +359 -0
  46. package/src/index.js +84 -0
  47. package/src/templates/claude-command.js +244 -0
  48. package/src/templates/issue-body.js +284 -0
  49. package/src/testing/config.js +411 -0
  50. package/src/utils/template-engine.js +398 -0
  51. package/src/utils/validate-templates.js +223 -0
  52. package/src/utils.js +396 -0
  53. package/templates/commands/ccasp-setup.template.md +113 -0
  54. package/templates/commands/context-audit.template.md +97 -0
  55. package/templates/commands/create-task-list.template.md +382 -0
  56. package/templates/commands/deploy-full.template.md +261 -0
  57. package/templates/commands/github-task-start.template.md +99 -0
  58. package/templates/commands/github-update.template.md +69 -0
  59. package/templates/commands/happy-start.template.md +117 -0
  60. package/templates/commands/phase-track.template.md +142 -0
  61. package/templates/commands/tunnel-start.template.md +127 -0
  62. package/templates/commands/tunnel-stop.template.md +106 -0
  63. package/templates/hooks/context-guardian.template.js +173 -0
  64. package/templates/hooks/deployment-orchestrator.template.js +219 -0
  65. package/templates/hooks/github-progress-hook.template.js +197 -0
  66. package/templates/hooks/happy-checkpoint-manager.template.js +222 -0
  67. package/templates/hooks/phase-dev-enforcer.template.js +183 -0
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Template Engine
3
+ *
4
+ * Handles placeholder replacement in .claude files using tech-stack.json values.
5
+ * Supports:
6
+ * - Nested property access: {{frontend.port}}, {{deployment.backend.platform}}
7
+ * - Conditional blocks: {{#if condition}}...{{/if}}
8
+ * - Equality checks: {{#if (eq path "value")}}...{{/if}}
9
+ * - Else blocks: {{#if condition}}...{{else}}...{{/if}}
10
+ * - Each loops: {{#each array}}{{this}}{{/each}}
11
+ * - Path variables: ${CWD}, ${HOME}
12
+ */
13
+
14
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'fs';
15
+ import { join, extname } from 'path';
16
+ import { homedir } from 'os';
17
+
18
+ /**
19
+ * Get nested property from object using dot notation
20
+ * @param {object} obj - The object to search
21
+ * @param {string} path - Dot-notated path (e.g., "frontend.port")
22
+ * @returns {*} The value or undefined
23
+ */
24
+ function getNestedValue(obj, path) {
25
+ return path.split('.').reduce((current, key) => {
26
+ return current && current[key] !== undefined ? current[key] : undefined;
27
+ }, obj);
28
+ }
29
+
30
+ /**
31
+ * Evaluate a condition expression
32
+ * @param {string} condition - The condition to evaluate
33
+ * @param {object} values - Tech stack values
34
+ * @returns {boolean} Whether the condition is truthy
35
+ */
36
+ function evaluateCondition(condition, values) {
37
+ const trimmed = condition.trim();
38
+
39
+ // Handle (eq path "value") syntax
40
+ const eqMatch = trimmed.match(/^\(eq\s+([^\s]+)\s+["']([^"']+)["']\)$/);
41
+ if (eqMatch) {
42
+ const [, path, expected] = eqMatch;
43
+ const actual = getNestedValue(values, path);
44
+ return actual === expected;
45
+ }
46
+
47
+ // Handle (neq path "value") syntax
48
+ const neqMatch = trimmed.match(/^\(neq\s+([^\s]+)\s+["']([^"']+)["']\)$/);
49
+ if (neqMatch) {
50
+ const [, path, expected] = neqMatch;
51
+ const actual = getNestedValue(values, path);
52
+ return actual !== expected;
53
+ }
54
+
55
+ // Handle (not path) syntax
56
+ const notMatch = trimmed.match(/^\(not\s+([^\s]+)\)$/);
57
+ if (notMatch) {
58
+ const [, path] = notMatch;
59
+ const value = getNestedValue(values, path);
60
+ return !value;
61
+ }
62
+
63
+ // Handle (and condition1 condition2) syntax
64
+ const andMatch = trimmed.match(/^\(and\s+(.+)\s+(.+)\)$/);
65
+ if (andMatch) {
66
+ const [, cond1, cond2] = andMatch;
67
+ return evaluateCondition(cond1, values) && evaluateCondition(cond2, values);
68
+ }
69
+
70
+ // Handle (or condition1 condition2) syntax
71
+ const orMatch = trimmed.match(/^\(or\s+(.+)\s+(.+)\)$/);
72
+ if (orMatch) {
73
+ const [, cond1, cond2] = orMatch;
74
+ return evaluateCondition(cond1, values) || evaluateCondition(cond2, values);
75
+ }
76
+
77
+ // Simple path - check if value is truthy
78
+ const value = getNestedValue(values, trimmed);
79
+ return !!value && value !== 'none' && value !== '';
80
+ }
81
+
82
+ /**
83
+ * Process {{#each array}}...{{/each}} blocks
84
+ * @param {string} content - Content with each blocks
85
+ * @param {object} values - Tech stack values
86
+ * @returns {string} Processed content
87
+ */
88
+ function processEachBlocks(content, values) {
89
+ // Match {{#each path}}content{{/each}}
90
+ const eachRegex = /\{\{#each\s+([^\}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g;
91
+
92
+ return content.replace(eachRegex, (match, path, innerContent) => {
93
+ const array = getNestedValue(values, path.trim());
94
+
95
+ if (!Array.isArray(array) || array.length === 0) {
96
+ return '';
97
+ }
98
+
99
+ return array
100
+ .map((item, index) => {
101
+ let result = innerContent;
102
+ // Replace {{this}} with current item
103
+ result = result.replace(/\{\{this\}\}/g, String(item));
104
+ // Replace {{@index}} with current index
105
+ result = result.replace(/\{\{@index\}\}/g, String(index));
106
+ // Replace {{@first}} with boolean
107
+ result = result.replace(/\{\{@first\}\}/g, String(index === 0));
108
+ // Replace {{@last}} with boolean
109
+ result = result.replace(/\{\{@last\}\}/g, String(index === array.length - 1));
110
+ return result;
111
+ })
112
+ .join('');
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Process {{#if condition}}...{{else}}...{{/if}} blocks
118
+ * @param {string} content - Content with conditional blocks
119
+ * @param {object} values - Tech stack values
120
+ * @returns {string} Processed content
121
+ */
122
+ function processConditionalBlocks(content, values) {
123
+ // Process from innermost to outermost to handle nested conditionals
124
+ let result = content;
125
+ let previousResult;
126
+
127
+ // Keep processing until no more changes (handles nested blocks)
128
+ do {
129
+ previousResult = result;
130
+
131
+ // Match {{#if condition}}...{{else}}...{{/if}} or {{#if condition}}...{{/if}}
132
+ // Non-greedy match for innermost blocks first
133
+ const ifElseRegex = /\{\{#if\s+([^\}]+)\}\}([\s\S]*?)\{\{else\}\}([\s\S]*?)\{\{\/if\}\}/g;
134
+ const ifOnlyRegex = /\{\{#if\s+([^\}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g;
135
+
136
+ // First handle if-else blocks
137
+ result = result.replace(ifElseRegex, (match, condition, ifContent, elseContent) => {
138
+ const isTruthy = evaluateCondition(condition, values);
139
+ return isTruthy ? ifContent : elseContent;
140
+ });
141
+
142
+ // Then handle if-only blocks (no else)
143
+ result = result.replace(ifOnlyRegex, (match, condition, ifContent) => {
144
+ const isTruthy = evaluateCondition(condition, values);
145
+ return isTruthy ? ifContent : '';
146
+ });
147
+ } while (result !== previousResult);
148
+
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Process path variables like ${CWD} and ${HOME}
154
+ * @param {string} content - Content with path variables
155
+ * @returns {string} Content with paths resolved
156
+ */
157
+ function processPathVariables(content) {
158
+ return content
159
+ .replace(/\$\{CWD\}/g, process.cwd())
160
+ .replace(/\$\{HOME\}/g, homedir())
161
+ .replace(/\$\{CLAUDE_DIR\}/g, join(process.cwd(), '.claude'));
162
+ }
163
+
164
+ /**
165
+ * Replace all placeholders in a string
166
+ * @param {string} content - Content with {{placeholder}} patterns
167
+ * @param {object} values - Tech stack values object
168
+ * @param {object} options - Options for replacement
169
+ * @returns {string} Content with placeholders replaced
170
+ */
171
+ export function replacePlaceholders(content, values, options = {}) {
172
+ const { preserveUnknown = false, warnOnMissing = true, processConditionals = true } = options;
173
+ const warnings = [];
174
+
175
+ let result = content;
176
+
177
+ // Step 1: Process path variables (${CWD}, ${HOME}, etc.)
178
+ result = processPathVariables(result);
179
+
180
+ // Step 2: Process {{#each}} blocks
181
+ result = processEachBlocks(result, values);
182
+
183
+ // Step 3: Process {{#if}}...{{/if}} conditional blocks
184
+ if (processConditionals) {
185
+ result = processConditionalBlocks(result, values);
186
+ }
187
+
188
+ // Step 4: Replace simple {{path.to.value}} placeholders
189
+ result = result.replace(/\{\{([^#\/][^}]*)\}\}/g, (match, path) => {
190
+ const trimmedPath = path.trim();
191
+
192
+ // Skip special syntax that wasn't processed
193
+ if (trimmedPath.startsWith('#') || trimmedPath.startsWith('/') || trimmedPath.startsWith('@')) {
194
+ return match;
195
+ }
196
+
197
+ const value = getNestedValue(values, trimmedPath);
198
+
199
+ if (value === undefined || value === null || value === '') {
200
+ if (warnOnMissing && !trimmedPath.includes('PLACEHOLDER')) {
201
+ warnings.push(`Missing value for: ${trimmedPath}`);
202
+ }
203
+ return preserveUnknown ? match : `{{${trimmedPath}}}`; // Keep placeholder
204
+ }
205
+
206
+ // Handle arrays
207
+ if (Array.isArray(value)) {
208
+ return value.join(', ');
209
+ }
210
+
211
+ // Handle objects - return JSON
212
+ if (typeof value === 'object') {
213
+ return JSON.stringify(value, null, 2);
214
+ }
215
+
216
+ return String(value);
217
+ });
218
+
219
+ return { content: result, warnings };
220
+ }
221
+
222
+ /**
223
+ * Process a single file
224
+ * @param {string} filePath - Path to the file
225
+ * @param {object} values - Tech stack values
226
+ * @param {object} options - Processing options
227
+ * @returns {object} Result with stats
228
+ */
229
+ export function processFile(filePath, values, options = {}) {
230
+ const { dryRun = false, verbose = false } = options;
231
+
232
+ const originalContent = readFileSync(filePath, 'utf8');
233
+ const { content: newContent, warnings } = replacePlaceholders(originalContent, values, options);
234
+
235
+ const placeholderCount = (originalContent.match(/\{\{[^}]+\}\}/g) || []).length;
236
+ const replacedCount = placeholderCount - (newContent.match(/\{\{[^}]+\}\}/g) || []).length;
237
+
238
+ const result = {
239
+ file: filePath,
240
+ placeholders: placeholderCount,
241
+ replaced: replacedCount,
242
+ remaining: placeholderCount - replacedCount,
243
+ warnings,
244
+ changed: originalContent !== newContent,
245
+ };
246
+
247
+ if (!dryRun && result.changed) {
248
+ writeFileSync(filePath, newContent, 'utf8');
249
+ }
250
+
251
+ return result;
252
+ }
253
+
254
+ /**
255
+ * Process all template files in a directory
256
+ * @param {string} dirPath - Directory path
257
+ * @param {object} values - Tech stack values
258
+ * @param {object} options - Processing options
259
+ * @returns {object[]} Array of results
260
+ */
261
+ export function processDirectory(dirPath, values, options = {}) {
262
+ const {
263
+ extensions = ['.md', '.json', '.js', '.ts', '.yml', '.yaml'],
264
+ recursive = true,
265
+ exclude = ['node_modules', '.git', 'dist', 'build'],
266
+ } = options;
267
+
268
+ const results = [];
269
+
270
+ function walkDir(dir) {
271
+ const entries = readdirSync(dir);
272
+
273
+ for (const entry of entries) {
274
+ const fullPath = join(dir, entry);
275
+
276
+ // Skip excluded directories
277
+ if (exclude.includes(entry)) continue;
278
+
279
+ const stat = statSync(fullPath);
280
+
281
+ if (stat.isDirectory() && recursive) {
282
+ walkDir(fullPath);
283
+ } else if (stat.isFile()) {
284
+ const ext = extname(entry);
285
+ if (extensions.includes(ext)) {
286
+ results.push(processFile(fullPath, values, options));
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ walkDir(dirPath);
293
+ return results;
294
+ }
295
+
296
+ /**
297
+ * Generate a tech-stack.json from detected values
298
+ * @param {object} detected - Detected tech stack
299
+ * @param {object} userOverrides - User-provided overrides
300
+ * @returns {object} Merged tech stack
301
+ */
302
+ export function generateTechStack(detected, userOverrides = {}) {
303
+ // Deep merge function
304
+ function deepMerge(target, source) {
305
+ const result = { ...target };
306
+ for (const key of Object.keys(source)) {
307
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
308
+ result[key] = deepMerge(result[key] || {}, source[key]);
309
+ } else if (source[key] !== undefined && source[key] !== null) {
310
+ result[key] = source[key];
311
+ }
312
+ }
313
+ return result;
314
+ }
315
+
316
+ return deepMerge(detected, userOverrides);
317
+ }
318
+
319
+ /**
320
+ * Flatten nested object to dot-notation keys
321
+ * @param {object} obj - Object to flatten
322
+ * @param {string} prefix - Current key prefix
323
+ * @returns {object} Flattened object
324
+ */
325
+ export function flattenObject(obj, prefix = '') {
326
+ const result = {};
327
+
328
+ for (const [key, value] of Object.entries(obj)) {
329
+ const newKey = prefix ? `${prefix}.${key}` : key;
330
+
331
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
332
+ Object.assign(result, flattenObject(value, newKey));
333
+ } else {
334
+ result[newKey] = value;
335
+ }
336
+ }
337
+
338
+ return result;
339
+ }
340
+
341
+ /**
342
+ * Get all unique placeholders from content
343
+ * @param {string} content - Content to scan
344
+ * @returns {string[]} Array of placeholder paths
345
+ */
346
+ export function extractPlaceholders(content) {
347
+ const matches = content.match(/\{\{([^}]+)\}\}/g) || [];
348
+ const placeholders = matches.map((m) => m.replace(/\{\{|\}\}/g, '').trim());
349
+ return [...new Set(placeholders)];
350
+ }
351
+
352
+ /**
353
+ * Validate a tech-stack.json against required placeholders
354
+ * @param {object} techStack - Tech stack values
355
+ * @param {string[]} requiredPlaceholders - Required placeholder paths
356
+ * @returns {object} Validation result
357
+ */
358
+ export function validateTechStack(techStack, requiredPlaceholders) {
359
+ const missing = [];
360
+ const present = [];
361
+
362
+ for (const placeholder of requiredPlaceholders) {
363
+ const value = getNestedValue(techStack, placeholder);
364
+ if (value === undefined || value === null || value === '') {
365
+ missing.push(placeholder);
366
+ } else {
367
+ present.push(placeholder);
368
+ }
369
+ }
370
+
371
+ return {
372
+ valid: missing.length === 0,
373
+ missing,
374
+ present,
375
+ coverage: present.length / requiredPlaceholders.length,
376
+ };
377
+ }
378
+
379
+ export {
380
+ evaluateCondition,
381
+ processConditionalBlocks,
382
+ processEachBlocks,
383
+ processPathVariables,
384
+ };
385
+
386
+ export default {
387
+ replacePlaceholders,
388
+ processFile,
389
+ processDirectory,
390
+ generateTechStack,
391
+ flattenObject,
392
+ extractPlaceholders,
393
+ validateTechStack,
394
+ evaluateCondition,
395
+ processConditionalBlocks,
396
+ processEachBlocks,
397
+ processPathVariables,
398
+ };
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Template Validation Scanner
3
+ *
4
+ * Scans template files for hardcoded values that should be parameterized.
5
+ * Ensures 100% platform agnosticism by detecting project-specific values.
6
+ */
7
+
8
+ import { readFileSync, readdirSync, statSync } from 'fs';
9
+ import { join, extname } from 'path';
10
+ import chalk from 'chalk';
11
+
12
+ /**
13
+ * Forbidden patterns - hardcoded values that should never appear in templates
14
+ * These are examples of project-specific values that must use placeholders
15
+ */
16
+ const FORBIDDEN_PATTERNS = [
17
+ // Railway IDs (example patterns)
18
+ { pattern: /[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}/g, name: 'UUID (potential service/project ID)' },
19
+
20
+ // Hardcoded ngrok usage without conditional
21
+ { pattern: /ngrok\s+http\s+\d+(?!\s*\}\})/g, name: 'Hardcoded ngrok command' },
22
+
23
+ // Specific project names that look hardcoded
24
+ { pattern: /--project-name=[a-z][a-z0-9-]+(?![\s]*\}\})/g, name: 'Hardcoded project name' },
25
+
26
+ // Hardcoded GitHub usernames/orgs (not in placeholders)
27
+ { pattern: /--owner\s+[a-zA-Z][a-zA-Z0-9-]+(?!\s*\}\})/g, name: 'Hardcoded GitHub owner' },
28
+
29
+ // Hardcoded API keys (patterns)
30
+ { pattern: /dev-key-[a-z0-9]+/gi, name: 'Hardcoded API key' },
31
+ { pattern: /api[_-]?key["\s:=]+[a-zA-Z0-9_-]{20,}/gi, name: 'Potential API key' },
32
+
33
+ // Hardcoded ports that should be configurable
34
+ { pattern: /localhost:\d{4}(?!\s*\}\})/g, name: 'Hardcoded localhost port' },
35
+
36
+ // Hardcoded URLs that look project-specific
37
+ { pattern: /https?:\/\/[a-z0-9-]+\.(railway|vercel|netlify)\.app/gi, name: 'Hardcoded deployment URL' },
38
+
39
+ // Specific SSH hosts
40
+ { pattern: /ssh\s+[a-z]+@\d+\.\d+\.\d+\.\d+/g, name: 'Hardcoded SSH host' },
41
+
42
+ // Hardcoded email addresses
43
+ { pattern: /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}(?!\s*\}\})/gi, name: 'Hardcoded email address' },
44
+ ];
45
+
46
+ /**
47
+ * Allowed patterns - these are OK even if they match forbidden patterns
48
+ */
49
+ const ALLOWED_PATTERNS = [
50
+ // Template placeholders
51
+ /\{\{[^}]+\}\}/g,
52
+
53
+ // Documentation examples marked as such
54
+ /<example>[\s\S]*?<\/example>/g,
55
+
56
+ // Code comments explaining placeholders
57
+ /\/\/.*placeholder/gi,
58
+ /\/\*[\s\S]*?placeholder[\s\S]*?\*\//gi,
59
+
60
+ // Generic localhost for documentation
61
+ /localhost:5173|localhost:8000|localhost:3000/g,
62
+
63
+ // Schema URLs
64
+ /https:\/\/json-schema\.org/g,
65
+ /https:\/\/github\.com\/[^/]+\/[^/]+\/tech-stack\.schema/g,
66
+ ];
67
+
68
+ /**
69
+ * Check if a match is within an allowed context
70
+ */
71
+ function isInAllowedContext(content, matchIndex, matchLength) {
72
+ // Get surrounding context
73
+ const start = Math.max(0, matchIndex - 50);
74
+ const end = Math.min(content.length, matchIndex + matchLength + 50);
75
+ const context = content.substring(start, end);
76
+
77
+ // Check if it's inside a placeholder
78
+ const beforeMatch = content.substring(Math.max(0, matchIndex - 10), matchIndex);
79
+ const afterMatch = content.substring(matchIndex + matchLength, Math.min(content.length, matchIndex + matchLength + 10));
80
+
81
+ if (beforeMatch.includes('{{') && afterMatch.includes('}}')) {
82
+ return true;
83
+ }
84
+
85
+ // Check if it's in a conditional block with placeholders
86
+ if (context.includes('{{#if') || context.includes('{{deployment.') || context.includes('{{versionControl.')) {
87
+ return true;
88
+ }
89
+
90
+ // Check if it's marked as an example
91
+ if (context.toLowerCase().includes('example') || context.includes('<placeholder>')) {
92
+ return true;
93
+ }
94
+
95
+ return false;
96
+ }
97
+
98
+ /**
99
+ * Scan a single file for forbidden patterns
100
+ */
101
+ export function scanFile(filePath) {
102
+ const content = readFileSync(filePath, 'utf8');
103
+ const violations = [];
104
+
105
+ for (const { pattern, name } of FORBIDDEN_PATTERNS) {
106
+ // Reset regex
107
+ pattern.lastIndex = 0;
108
+
109
+ let match;
110
+ while ((match = pattern.exec(content)) !== null) {
111
+ // Check if this match is in an allowed context
112
+ if (!isInAllowedContext(content, match.index, match[0].length)) {
113
+ // Get line number
114
+ const lineNumber = content.substring(0, match.index).split('\n').length;
115
+
116
+ violations.push({
117
+ file: filePath,
118
+ line: lineNumber,
119
+ match: match[0],
120
+ pattern: name,
121
+ context: content.substring(
122
+ Math.max(0, match.index - 20),
123
+ Math.min(content.length, match.index + match[0].length + 20)
124
+ ).replace(/\n/g, '\\n'),
125
+ });
126
+ }
127
+ }
128
+ }
129
+
130
+ return violations;
131
+ }
132
+
133
+ /**
134
+ * Scan all template files in a directory
135
+ */
136
+ export function scanDirectory(dirPath, options = {}) {
137
+ const {
138
+ extensions = ['.md', '.js', '.ts', '.json', '.yml', '.yaml'],
139
+ exclude = ['node_modules', '.git', 'dist', 'build'],
140
+ recursive = true,
141
+ } = options;
142
+
143
+ const violations = [];
144
+
145
+ function walkDir(dir) {
146
+ const entries = readdirSync(dir);
147
+
148
+ for (const entry of entries) {
149
+ if (exclude.includes(entry)) continue;
150
+
151
+ const fullPath = join(dir, entry);
152
+ const stat = statSync(fullPath);
153
+
154
+ if (stat.isDirectory() && recursive) {
155
+ walkDir(fullPath);
156
+ } else if (stat.isFile()) {
157
+ const ext = extname(entry);
158
+ if (extensions.includes(ext)) {
159
+ violations.push(...scanFile(fullPath));
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ walkDir(dirPath);
166
+ return violations;
167
+ }
168
+
169
+ /**
170
+ * Format violations for display
171
+ */
172
+ export function formatViolations(violations) {
173
+ if (violations.length === 0) {
174
+ return chalk.green('✓ No hardcoded values detected');
175
+ }
176
+
177
+ let output = chalk.red(`\n✗ Found ${violations.length} potential hardcoded value(s):\n\n`);
178
+
179
+ // Group by file
180
+ const byFile = {};
181
+ for (const v of violations) {
182
+ if (!byFile[v.file]) byFile[v.file] = [];
183
+ byFile[v.file].push(v);
184
+ }
185
+
186
+ for (const [file, fileViolations] of Object.entries(byFile)) {
187
+ output += chalk.yellow(` ${file}\n`);
188
+
189
+ for (const v of fileViolations) {
190
+ output += chalk.dim(` Line ${v.line}: `) + chalk.red(v.pattern) + '\n';
191
+ output += chalk.dim(` Match: "${v.match}"\n`);
192
+ output += chalk.dim(` Context: ...${v.context}...\n`);
193
+ output += '\n';
194
+ }
195
+ }
196
+
197
+ output += chalk.dim('\nSuggestion: Replace hardcoded values with {{placeholder}} syntax.\n');
198
+ output += chalk.dim('Example: --project-name={{deployment.frontend.projectName}}\n');
199
+
200
+ return output;
201
+ }
202
+
203
+ /**
204
+ * Run validation as CLI tool
205
+ */
206
+ export async function runValidation(targetPath) {
207
+ console.log(chalk.cyan('Template Validation Scanner'));
208
+ console.log(chalk.dim('Checking for hardcoded values...\n'));
209
+
210
+ const violations = scanDirectory(targetPath);
211
+
212
+ console.log(formatViolations(violations));
213
+
214
+ return violations.length === 0;
215
+ }
216
+
217
+ export default {
218
+ scanFile,
219
+ scanDirectory,
220
+ formatViolations,
221
+ runValidation,
222
+ FORBIDDEN_PATTERNS,
223
+ };