awesome-slash 2.4.3 → 2.5.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 (146) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +99 -1
  4. package/README.md +173 -161
  5. package/SECURITY.md +25 -81
  6. package/adapters/codex/install.sh +58 -16
  7. package/adapters/opencode/install.sh +92 -23
  8. package/lib/index.js +47 -4
  9. package/lib/patterns/review-patterns.js +58 -11
  10. package/lib/patterns/slop-patterns.js +154 -147
  11. package/lib/platform/detect-platform.js +99 -350
  12. package/lib/platform/detection-configs.js +93 -0
  13. package/lib/platform/verify-tools.js +10 -78
  14. package/lib/schemas/README.md +195 -0
  15. package/lib/schemas/validator.js +247 -0
  16. package/lib/sources/custom-handler.js +199 -0
  17. package/lib/sources/policy-questions.js +239 -0
  18. package/lib/sources/source-cache.js +149 -0
  19. package/lib/state/workflow-state.js +363 -665
  20. package/lib/types/README.md +292 -0
  21. package/lib/types/agent-frontmatter.d.ts +134 -0
  22. package/lib/types/command-frontmatter.d.ts +107 -0
  23. package/lib/types/hook-frontmatter.d.ts +115 -0
  24. package/lib/types/index.d.ts +84 -0
  25. package/lib/types/plugin-manifest.d.ts +102 -0
  26. package/lib/types/skill-frontmatter.d.ts +89 -0
  27. package/lib/utils/cache-manager.js +154 -0
  28. package/lib/utils/context-optimizer.js +5 -36
  29. package/lib/utils/deprecation.js +37 -0
  30. package/lib/utils/shell-escape.js +88 -0
  31. package/mcp-server/index.js +513 -18
  32. package/package.json +6 -2
  33. package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
  34. package/plugins/deslop-around/lib/index.js +170 -0
  35. package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
  36. package/plugins/deslop-around/lib/patterns/slop-patterns.js +170 -129
  37. package/plugins/deslop-around/lib/platform/detect-platform.js +212 -123
  38. package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
  39. package/plugins/deslop-around/lib/platform/verify-tools.js +10 -1
  40. package/plugins/deslop-around/lib/schemas/README.md +195 -0
  41. package/plugins/deslop-around/lib/schemas/validator.js +205 -0
  42. package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
  43. package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
  44. package/plugins/deslop-around/lib/sources/source-cache.js +149 -0
  45. package/plugins/deslop-around/lib/state/workflow-state.js +382 -484
  46. package/plugins/deslop-around/lib/types/README.md +292 -0
  47. package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
  48. package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
  49. package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
  50. package/plugins/deslop-around/lib/types/index.d.ts +84 -0
  51. package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
  52. package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
  53. package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
  54. package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
  55. package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
  56. package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
  57. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  58. package/plugins/next-task/agents/ci-monitor.md +19 -0
  59. package/plugins/next-task/agents/delivery-validator.md +2 -2
  60. package/plugins/next-task/agents/implementation-agent.md +3 -4
  61. package/plugins/next-task/agents/planning-agent.md +77 -19
  62. package/plugins/next-task/agents/review-orchestrator.md +21 -122
  63. package/plugins/next-task/agents/task-discoverer.md +164 -23
  64. package/plugins/next-task/commands/next-task.md +180 -14
  65. package/plugins/next-task/lib/index.js +170 -0
  66. package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
  67. package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
  68. package/plugins/next-task/lib/platform/detect-platform.js +212 -123
  69. package/plugins/next-task/lib/platform/detection-configs.js +93 -0
  70. package/plugins/next-task/lib/platform/verify-tools.js +10 -1
  71. package/plugins/next-task/lib/schemas/README.md +195 -0
  72. package/plugins/next-task/lib/schemas/validator.js +205 -0
  73. package/plugins/next-task/lib/sources/custom-handler.js +199 -0
  74. package/plugins/next-task/lib/sources/policy-questions.js +239 -0
  75. package/plugins/next-task/lib/sources/source-cache.js +149 -0
  76. package/plugins/next-task/lib/state/workflow-state.js +382 -484
  77. package/plugins/next-task/lib/types/README.md +292 -0
  78. package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
  79. package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
  80. package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
  81. package/plugins/next-task/lib/types/index.d.ts +84 -0
  82. package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
  83. package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
  84. package/plugins/next-task/lib/utils/cache-manager.js +154 -0
  85. package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
  86. package/plugins/next-task/lib/utils/deprecation.js +37 -0
  87. package/plugins/next-task/lib/utils/shell-escape.js +88 -0
  88. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  89. package/plugins/project-review/lib/index.js +170 -0
  90. package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
  91. package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
  92. package/plugins/project-review/lib/platform/detect-platform.js +212 -123
  93. package/plugins/project-review/lib/platform/detection-configs.js +93 -0
  94. package/plugins/project-review/lib/platform/verify-tools.js +10 -1
  95. package/plugins/project-review/lib/schemas/README.md +195 -0
  96. package/plugins/project-review/lib/schemas/validator.js +205 -0
  97. package/plugins/project-review/lib/sources/custom-handler.js +199 -0
  98. package/plugins/project-review/lib/sources/policy-questions.js +239 -0
  99. package/plugins/project-review/lib/sources/source-cache.js +149 -0
  100. package/plugins/project-review/lib/state/workflow-state.js +382 -484
  101. package/plugins/project-review/lib/types/README.md +292 -0
  102. package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
  103. package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
  104. package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
  105. package/plugins/project-review/lib/types/index.d.ts +84 -0
  106. package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
  107. package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
  108. package/plugins/project-review/lib/utils/cache-manager.js +154 -0
  109. package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
  110. package/plugins/project-review/lib/utils/deprecation.js +37 -0
  111. package/plugins/project-review/lib/utils/shell-escape.js +88 -0
  112. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  113. package/plugins/reality-check/agents/code-explorer.md +1 -1
  114. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  115. package/plugins/ship/commands/ship-ci-review-loop.md +19 -0
  116. package/plugins/ship/lib/index.js +170 -0
  117. package/plugins/ship/lib/patterns/review-patterns.js +58 -11
  118. package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
  119. package/plugins/ship/lib/platform/detect-platform.js +212 -123
  120. package/plugins/ship/lib/platform/detection-configs.js +93 -0
  121. package/plugins/ship/lib/platform/verify-tools.js +10 -1
  122. package/plugins/ship/lib/schemas/README.md +195 -0
  123. package/plugins/ship/lib/schemas/validator.js +205 -0
  124. package/plugins/ship/lib/sources/custom-handler.js +199 -0
  125. package/plugins/ship/lib/sources/policy-questions.js +239 -0
  126. package/plugins/ship/lib/sources/source-cache.js +149 -0
  127. package/plugins/ship/lib/state/workflow-state.js +382 -484
  128. package/plugins/ship/lib/types/README.md +292 -0
  129. package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
  130. package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
  131. package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
  132. package/plugins/ship/lib/types/index.d.ts +84 -0
  133. package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
  134. package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
  135. package/plugins/ship/lib/utils/cache-manager.js +154 -0
  136. package/plugins/ship/lib/utils/context-optimizer.js +115 -37
  137. package/plugins/ship/lib/utils/deprecation.js +37 -0
  138. package/plugins/ship/lib/utils/shell-escape.js +88 -0
  139. package/lib/state/workflow-state.schema.json +0 -282
  140. package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
  141. package/plugins/next-task/agents/policy-selector.md +0 -248
  142. package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
  143. package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
  144. package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
  145. package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
  146. package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
@@ -11,20 +11,129 @@
11
11
  */
12
12
 
13
13
  const fs = require('fs');
14
+ const path = require('path');
14
15
  const { execSync, exec } = require('child_process');
15
16
  const { promisify } = require('util');
16
17
 
17
18
  const execAsync = promisify(exec);
18
19
  const fsPromises = fs.promises;
19
20
 
20
- // Detection cache for performance (platform rarely changes during session)
21
- let _cachedDetection = null;
22
- let _cacheExpiry = 0;
23
- const CACHE_TTL_MS = 60000; // 1 minute cache
21
+ // Import shared utilities
22
+ const { warnDeprecation, _resetDeprecationWarnings } = require('../utils/deprecation');
23
+ const { CacheManager } = require('../utils/cache-manager');
24
+ const {
25
+ CI_CONFIGS,
26
+ DEPLOYMENT_CONFIGS,
27
+ PACKAGE_MANAGER_CONFIGS
28
+ } = require('./detection-configs');
24
29
 
25
- // File read cache to avoid reading the same file multiple times (#17)
26
- const _fileCache = new Map();
27
- const _existsCache = new Map();
30
+ /**
31
+ * Default timeout for async operations (5 seconds)
32
+ */
33
+ const DEFAULT_ASYNC_TIMEOUT_MS = 5000;
34
+
35
+ /**
36
+ * Maximum JSON file size to parse (1MB) - prevents DoS via large files
37
+ */
38
+ const MAX_JSON_SIZE_BYTES = 1024 * 1024;
39
+
40
+ /**
41
+ * Safely parse JSON content with size limit
42
+ * @param {string} content - JSON string to parse
43
+ * @param {string} filename - Filename for error messages
44
+ * @returns {Object|null} Parsed object or null if invalid/too large
45
+ */
46
+ function safeJSONParse(content, filename = 'unknown') {
47
+ if (!content || typeof content !== 'string') {
48
+ return null;
49
+ }
50
+ if (content.length > MAX_JSON_SIZE_BYTES) {
51
+ // File too large - skip parsing to prevent DoS
52
+ return null;
53
+ }
54
+ try {
55
+ return JSON.parse(content);
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Wrap a promise with a timeout
63
+ * @param {Promise} promise - Promise to wrap
64
+ * @param {number} timeoutMs - Timeout in milliseconds
65
+ * @param {string} operation - Operation name for error message
66
+ * @returns {Promise} Promise that rejects on timeout
67
+ */
68
+ function withTimeout(promise, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, operation = 'operation') {
69
+ let timeoutId;
70
+ const timeoutPromise = new Promise((_, reject) => {
71
+ timeoutId = setTimeout(() => {
72
+ reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
73
+ }, timeoutMs);
74
+ });
75
+
76
+ return Promise.race([promise, timeoutPromise]).finally(() => {
77
+ clearTimeout(timeoutId);
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Execute a command with timeout protection
83
+ * @param {string} cmd - Command to execute
84
+ * @param {Object} options - exec options
85
+ * @param {number} timeoutMs - Timeout in milliseconds
86
+ * @returns {Promise<{stdout: string, stderr: string}>}
87
+ */
88
+ async function execAsyncWithTimeout(cmd, options = {}, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS) {
89
+ return withTimeout(execAsync(cmd, options), timeoutMs, `exec: ${cmd.substring(0, 50)}`);
90
+ }
91
+
92
+ // Maximum JSON file size and cached file size constants
93
+ const MAX_CACHED_FILE_SIZE = 64 * 1024; // 64KB max per cached file
94
+
95
+ // Cache instances using CacheManager abstraction
96
+ const _detectionCache = new CacheManager({ maxSize: 1, ttl: 60000 }); // Single detection result, 1 min TTL
97
+ const _fileCache = new CacheManager({ maxSize: 100, ttl: 60000, maxValueSize: MAX_CACHED_FILE_SIZE });
98
+ const _existsCache = new CacheManager({ maxSize: 100, ttl: 60000 });
99
+
100
+ // Note: enforceMaxCacheSize() removed - now handled by CacheManager internally
101
+
102
+ /**
103
+ * Generic file-based detector (synchronous)
104
+ * @param {Array} configs - Array of {file, platform} objects
105
+ * @param {Function} existsChecker - Function to check file existence
106
+ * @returns {string|null} Detected platform or null
107
+ */
108
+ function detectFromFiles(configs, existsChecker) {
109
+ for (const { file, platform } of configs) {
110
+ if (existsChecker(file)) {
111
+ return platform;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Generic file-based detector (asynchronous)
119
+ * @param {Array} configs - Array of {file, platform} objects
120
+ * @param {Function} existsChecker - Async function to check file existence
121
+ * @returns {Promise<string|null>} Detected platform or null
122
+ */
123
+ async function detectFromFilesAsync(configs, existsChecker) {
124
+ // Check all files in parallel for better performance
125
+ const checks = await Promise.all(
126
+ configs.map(({ file }) => existsChecker(file))
127
+ );
128
+
129
+ // Return first match (maintains priority order)
130
+ for (let i = 0; i < checks.length; i++) {
131
+ if (checks[i]) {
132
+ return configs[i].platform;
133
+ }
134
+ }
135
+ return null;
136
+ }
28
137
 
29
138
  /**
30
139
  * Check if a file exists (cached)
@@ -32,8 +141,9 @@ const _existsCache = new Map();
32
141
  * @returns {boolean}
33
142
  */
34
143
  function existsCached(filepath) {
35
- if (_existsCache.has(filepath)) {
36
- return _existsCache.get(filepath);
144
+ const cached = _existsCache.get(filepath);
145
+ if (cached !== undefined) {
146
+ return cached;
37
147
  }
38
148
  const exists = fs.existsSync(filepath);
39
149
  _existsCache.set(filepath, exists);
@@ -46,8 +156,9 @@ function existsCached(filepath) {
46
156
  * @returns {Promise<boolean>}
47
157
  */
48
158
  async function existsCachedAsync(filepath) {
49
- if (_existsCache.has(filepath)) {
50
- return _existsCache.get(filepath);
159
+ const cached = _existsCache.get(filepath);
160
+ if (cached !== undefined) {
161
+ return cached;
51
162
  }
52
163
  try {
53
164
  await fsPromises.access(filepath);
@@ -61,53 +172,68 @@ async function existsCachedAsync(filepath) {
61
172
 
62
173
  /**
63
174
  * Read file contents (cached)
175
+ * Only caches files smaller than MAX_CACHED_FILE_SIZE to prevent memory bloat
176
+ * Optimized: normalizes filepath to prevent cache pollution from variant paths
64
177
  * @param {string} filepath - Path to read
65
178
  * @returns {string|null}
66
179
  */
67
180
  function readFileCached(filepath) {
68
- if (_fileCache.has(filepath)) {
69
- return _fileCache.get(filepath);
181
+ // Normalize filepath to prevent cache pollution (./foo vs foo vs /abs/foo)
182
+ // This ensures that different representations of the same path use the same cache entry
183
+ const normalizedPath = path.resolve(filepath);
184
+
185
+ const cached = _fileCache.get(normalizedPath);
186
+ if (cached !== undefined) {
187
+ return cached;
70
188
  }
71
189
  try {
72
- const content = fs.readFileSync(filepath, 'utf8');
73
- _fileCache.set(filepath, content);
190
+ const content = fs.readFileSync(normalizedPath, 'utf8');
191
+ // CacheManager enforces maxValueSize, so small files are cached automatically
192
+ _fileCache.set(normalizedPath, content);
74
193
  return content;
75
194
  } catch {
76
- _fileCache.set(filepath, null);
195
+ // Cache null for missing files (small memory footprint)
196
+ _fileCache.set(normalizedPath, null);
77
197
  return null;
78
198
  }
79
199
  }
80
200
 
81
201
  /**
82
202
  * Read file contents (cached, async)
203
+ * Only caches files smaller than MAX_CACHED_FILE_SIZE to prevent memory bloat
204
+ * Optimized: normalizes filepath to prevent cache pollution from variant paths
83
205
  * @param {string} filepath - Path to read
84
206
  * @returns {Promise<string|null>}
85
207
  */
86
208
  async function readFileCachedAsync(filepath) {
87
- if (_fileCache.has(filepath)) {
88
- return _fileCache.get(filepath);
209
+ // Normalize filepath to prevent cache pollution (./foo vs foo vs /abs/foo)
210
+ // This ensures that different representations of the same path use the same cache entry
211
+ const normalizedPath = path.resolve(filepath);
212
+
213
+ const cached = _fileCache.get(normalizedPath);
214
+ if (cached !== undefined) {
215
+ return cached;
89
216
  }
90
217
  try {
91
- const content = await fsPromises.readFile(filepath, 'utf8');
92
- _fileCache.set(filepath, content);
218
+ const content = await fsPromises.readFile(normalizedPath, 'utf8');
219
+ // CacheManager enforces maxValueSize, so small files are cached automatically
220
+ _fileCache.set(normalizedPath, content);
93
221
  return content;
94
222
  } catch {
95
- _fileCache.set(filepath, null);
223
+ // Cache null for missing files (small memory footprint)
224
+ _fileCache.set(normalizedPath, null);
96
225
  return null;
97
226
  }
98
227
  }
99
228
 
100
229
  /**
101
230
  * Detects CI platform by scanning for configuration files
231
+ * @deprecated Use detectCIAsync() instead. Will be removed in v3.0.0.
102
232
  * @returns {string|null} CI platform name or null if not detected
103
233
  */
104
234
  function detectCI() {
105
- if (existsCached('.github/workflows')) return 'github-actions';
106
- if (existsCached('.gitlab-ci.yml')) return 'gitlab-ci';
107
- if (existsCached('.circleci/config.yml')) return 'circleci';
108
- if (existsCached('Jenkinsfile')) return 'jenkins';
109
- if (existsCached('.travis.yml')) return 'travis';
110
- return null;
235
+ warnDeprecation('detectCI', 'detectCIAsync');
236
+ return detectFromFiles(CI_CONFIGS, existsCached);
111
237
  }
112
238
 
113
239
  /**
@@ -115,34 +241,17 @@ function detectCI() {
115
241
  * @returns {Promise<string|null>} CI platform name or null if not detected
116
242
  */
117
243
  async function detectCIAsync() {
118
- const checks = await Promise.all([
119
- existsCachedAsync('.github/workflows'),
120
- existsCachedAsync('.gitlab-ci.yml'),
121
- existsCachedAsync('.circleci/config.yml'),
122
- existsCachedAsync('Jenkinsfile'),
123
- existsCachedAsync('.travis.yml')
124
- ]);
125
-
126
- if (checks[0]) return 'github-actions';
127
- if (checks[1]) return 'gitlab-ci';
128
- if (checks[2]) return 'circleci';
129
- if (checks[3]) return 'jenkins';
130
- if (checks[4]) return 'travis';
131
- return null;
244
+ return detectFromFilesAsync(CI_CONFIGS, existsCachedAsync);
132
245
  }
133
246
 
134
247
  /**
135
248
  * Detects deployment platform by scanning for platform-specific files
249
+ * @deprecated Use detectDeploymentAsync() instead. Will be removed in v3.0.0.
136
250
  * @returns {string|null} Deployment platform name or null if not detected
137
251
  */
138
252
  function detectDeployment() {
139
- if (existsCached('railway.json') || existsCached('railway.toml')) return 'railway';
140
- if (existsCached('vercel.json')) return 'vercel';
141
- if (existsCached('netlify.toml') || existsCached('.netlify')) return 'netlify';
142
- if (existsCached('fly.toml')) return 'fly';
143
- if (existsCached('.platform.sh')) return 'platform-sh';
144
- if (existsCached('render.yaml')) return 'render';
145
- return null;
253
+ warnDeprecation('detectDeployment', 'detectDeploymentAsync');
254
+ return detectFromFiles(DEPLOYMENT_CONFIGS, existsCached);
146
255
  }
147
256
 
148
257
  /**
@@ -150,31 +259,16 @@ function detectDeployment() {
150
259
  * @returns {Promise<string|null>} Deployment platform name or null if not detected
151
260
  */
152
261
  async function detectDeploymentAsync() {
153
- const checks = await Promise.all([
154
- existsCachedAsync('railway.json'),
155
- existsCachedAsync('railway.toml'),
156
- existsCachedAsync('vercel.json'),
157
- existsCachedAsync('netlify.toml'),
158
- existsCachedAsync('.netlify'),
159
- existsCachedAsync('fly.toml'),
160
- existsCachedAsync('.platform.sh'),
161
- existsCachedAsync('render.yaml')
162
- ]);
163
-
164
- if (checks[0] || checks[1]) return 'railway';
165
- if (checks[2]) return 'vercel';
166
- if (checks[3] || checks[4]) return 'netlify';
167
- if (checks[5]) return 'fly';
168
- if (checks[6]) return 'platform-sh';
169
- if (checks[7]) return 'render';
170
- return null;
262
+ return detectFromFilesAsync(DEPLOYMENT_CONFIGS, existsCachedAsync);
171
263
  }
172
264
 
173
265
  /**
174
266
  * Detects project type by scanning for language-specific files
267
+ * @deprecated Use detectProjectTypeAsync() instead. Will be removed in v3.0.0.
175
268
  * @returns {string} Project type identifier
176
269
  */
177
270
  function detectProjectType() {
271
+ warnDeprecation('detectProjectType', 'detectProjectTypeAsync');
178
272
  if (existsCached('package.json')) return 'nodejs';
179
273
  if (existsCached('requirements.txt') || existsCached('pyproject.toml') || existsCached('setup.py')) return 'python';
180
274
  if (existsCached('Cargo.toml')) return 'rust';
@@ -209,18 +303,15 @@ async function detectProjectTypeAsync() {
209
303
 
210
304
  /**
211
305
  * Detects package manager by scanning for lockfiles
306
+ * @deprecated Use detectPackageManagerAsync() instead. Will be removed in v3.0.0.
212
307
  * @returns {string|null} Package manager name or null if not detected
213
308
  */
214
309
  function detectPackageManager() {
215
- if (existsCached('pnpm-lock.yaml')) return 'pnpm';
216
- if (existsCached('yarn.lock')) return 'yarn';
217
- if (existsCached('bun.lockb')) return 'bun';
218
- if (existsCached('package-lock.json')) return 'npm';
219
- if (existsCached('poetry.lock')) return 'poetry';
220
- if (existsCached('Pipfile.lock')) return 'pipenv';
221
- if (existsCached('Cargo.lock')) return 'cargo';
222
- if (existsCached('go.sum')) return 'go';
223
- return null;
310
+ warnDeprecation('detectPackageManager', 'detectPackageManagerAsync');
311
+ return detectFromFiles(
312
+ PACKAGE_MANAGER_CONFIGS.map(({ file, manager }) => ({ file, platform: manager })),
313
+ existsCached
314
+ );
224
315
  }
225
316
 
226
317
  /**
@@ -228,33 +319,19 @@ function detectPackageManager() {
228
319
  * @returns {Promise<string|null>} Package manager name or null if not detected
229
320
  */
230
321
  async function detectPackageManagerAsync() {
231
- const checks = await Promise.all([
232
- existsCachedAsync('pnpm-lock.yaml'),
233
- existsCachedAsync('yarn.lock'),
234
- existsCachedAsync('bun.lockb'),
235
- existsCachedAsync('package-lock.json'),
236
- existsCachedAsync('poetry.lock'),
237
- existsCachedAsync('Pipfile.lock'),
238
- existsCachedAsync('Cargo.lock'),
239
- existsCachedAsync('go.sum')
240
- ]);
241
-
242
- if (checks[0]) return 'pnpm';
243
- if (checks[1]) return 'yarn';
244
- if (checks[2]) return 'bun';
245
- if (checks[3]) return 'npm';
246
- if (checks[4]) return 'poetry';
247
- if (checks[5]) return 'pipenv';
248
- if (checks[6]) return 'cargo';
249
- if (checks[7]) return 'go';
250
- return null;
322
+ return detectFromFilesAsync(
323
+ PACKAGE_MANAGER_CONFIGS.map(({ file, manager }) => ({ file, platform: manager })),
324
+ existsCachedAsync
325
+ );
251
326
  }
252
327
 
253
328
  /**
254
329
  * Detects branch strategy (single-branch vs multi-branch with dev+prod)
330
+ * @deprecated Use detectBranchStrategyAsync() instead. Will be removed in v3.0.0.
255
331
  * @returns {string} 'single-branch' or 'multi-branch'
256
332
  */
257
333
  function detectBranchStrategy() {
334
+ warnDeprecation('detectBranchStrategy', 'detectBranchStrategyAsync');
258
335
  try {
259
336
  // Check both local and remote branches
260
337
  const localBranches = execSync('git branch', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
@@ -277,7 +354,7 @@ function detectBranchStrategy() {
277
354
  try {
278
355
  const content = readFileCached('railway.json');
279
356
  if (content) {
280
- const config = JSON.parse(content);
357
+ const config = safeJSONParse(content, 'railway.json');
281
358
  // Validate JSON structure before accessing properties
282
359
  if (config &&
283
360
  typeof config === 'object' &&
@@ -302,10 +379,10 @@ function detectBranchStrategy() {
302
379
  */
303
380
  async function detectBranchStrategyAsync() {
304
381
  try {
305
- // Run git commands in parallel
382
+ // Run git commands in parallel with timeout protection
306
383
  const [localResult, remoteResult] = await Promise.all([
307
- execAsync('git branch', { encoding: 'utf8' }).catch(() => ({ stdout: '' })),
308
- execAsync('git branch -r', { encoding: 'utf8' }).catch(() => ({ stdout: '' }))
384
+ execAsyncWithTimeout('git branch', { encoding: 'utf8' }).catch(() => ({ stdout: '' })),
385
+ execAsyncWithTimeout('git branch -r', { encoding: 'utf8' }).catch(() => ({ stdout: '' }))
309
386
  ]);
310
387
 
311
388
  const allBranches = (localResult.stdout || '') + (remoteResult.stdout || '');
@@ -322,7 +399,7 @@ async function detectBranchStrategyAsync() {
322
399
  try {
323
400
  const content = await readFileCachedAsync('railway.json');
324
401
  if (content) {
325
- const config = JSON.parse(content);
402
+ const config = safeJSONParse(content, 'railway.json');
326
403
  if (config &&
327
404
  typeof config === 'object' &&
328
405
  typeof config.environments === 'object' &&
@@ -342,9 +419,11 @@ async function detectBranchStrategyAsync() {
342
419
 
343
420
  /**
344
421
  * Detects the main branch name
422
+ * @deprecated Use detectMainBranchAsync() instead. Will be removed in v3.0.0.
345
423
  * @returns {string} Main branch name ('main' or 'master')
346
424
  */
347
425
  function detectMainBranch() {
426
+ warnDeprecation('detectMainBranch', 'detectMainBranchAsync');
348
427
  try {
349
428
  const defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
350
429
  encoding: 'utf8',
@@ -373,12 +452,12 @@ function detectMainBranch() {
373
452
  */
374
453
  async function detectMainBranchAsync() {
375
454
  try {
376
- const { stdout } = await execAsync('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' });
455
+ const { stdout } = await execAsyncWithTimeout('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' });
377
456
  return stdout.trim().replace('refs/remotes/origin/', '');
378
457
  } catch {
379
458
  // Fallback: check common names
380
459
  try {
381
- await execAsync('git rev-parse --verify main', { encoding: 'utf8' });
460
+ await execAsyncWithTimeout('git rev-parse --verify main', { encoding: 'utf8' });
382
461
  return 'main';
383
462
  } catch {
384
463
  return 'master';
@@ -389,18 +468,22 @@ async function detectMainBranchAsync() {
389
468
  /**
390
469
  * Main detection function - aggregates all platform information (sync)
391
470
  * Uses caching to avoid repeated filesystem/git operations
471
+ * @deprecated Use detectAsync() instead. Will be removed in v3.0.0.
392
472
  * @param {boolean} forceRefresh - Force cache refresh
393
473
  * @returns {Object} Platform configuration object
394
474
  */
395
475
  function detect(forceRefresh = false) {
396
- const now = Date.now();
476
+ warnDeprecation('detect', 'detectAsync');
397
477
 
398
478
  // Return cached result if still valid
399
- if (!forceRefresh && _cachedDetection && now < _cacheExpiry) {
400
- return _cachedDetection;
479
+ if (!forceRefresh) {
480
+ const cached = _detectionCache.get('detection');
481
+ if (cached !== undefined) {
482
+ return cached;
483
+ }
401
484
  }
402
485
 
403
- _cachedDetection = {
486
+ const detection = {
404
487
  ci: detectCI(),
405
488
  deployment: detectDeployment(),
406
489
  projectType: detectProjectType(),
@@ -409,11 +492,11 @@ function detect(forceRefresh = false) {
409
492
  mainBranch: detectMainBranch(),
410
493
  hasPlanFile: existsCached('PLAN.md'),
411
494
  hasTechDebtFile: existsCached('TECHNICAL_DEBT.md'),
412
- timestamp: new Date(now).toISOString()
495
+ timestamp: new Date().toISOString()
413
496
  };
414
- _cacheExpiry = now + CACHE_TTL_MS;
415
497
 
416
- return _cachedDetection;
498
+ _detectionCache.set('detection', detection);
499
+ return detection;
417
500
  }
418
501
 
419
502
  /**
@@ -423,11 +506,12 @@ function detect(forceRefresh = false) {
423
506
  * @returns {Promise<Object>} Platform configuration object
424
507
  */
425
508
  async function detectAsync(forceRefresh = false) {
426
- const now = Date.now();
427
-
428
509
  // Return cached result if still valid
429
- if (!forceRefresh && _cachedDetection && now < _cacheExpiry) {
430
- return _cachedDetection;
510
+ if (!forceRefresh) {
511
+ const cached = _detectionCache.get('detection');
512
+ if (cached !== undefined) {
513
+ return cached;
514
+ }
431
515
  }
432
516
 
433
517
  // Run all detections in parallel
@@ -451,7 +535,7 @@ async function detectAsync(forceRefresh = false) {
451
535
  existsCachedAsync('TECHNICAL_DEBT.md')
452
536
  ]);
453
537
 
454
- _cachedDetection = {
538
+ const detection = {
455
539
  ci,
456
540
  deployment,
457
541
  projectType,
@@ -460,20 +544,19 @@ async function detectAsync(forceRefresh = false) {
460
544
  mainBranch,
461
545
  hasPlanFile,
462
546
  hasTechDebtFile,
463
- timestamp: new Date(now).toISOString()
547
+ timestamp: new Date().toISOString()
464
548
  };
465
- _cacheExpiry = now + CACHE_TTL_MS;
466
549
 
467
- return _cachedDetection;
550
+ _detectionCache.set('detection', detection);
551
+ return detection;
468
552
  }
469
553
 
470
554
  /**
471
- * Invalidate the detection cache
555
+ * Invalidate all detection caches
472
556
  * Call this after making changes that affect platform detection
473
557
  */
474
558
  function invalidateCache() {
475
- _cachedDetection = null;
476
- _cacheExpiry = 0;
559
+ _detectionCache.clear();
477
560
  _fileCache.clear();
478
561
  _existsCache.clear();
479
562
  }
@@ -483,12 +566,16 @@ if (require.main === module) {
483
566
  (async () => {
484
567
  try {
485
568
  const result = await detectAsync();
486
- console.log(JSON.stringify(result, null, 2));
569
+ // Optimize: only use pretty-printing when output is to terminal (TTY)
570
+ // When piped to another program, use compact JSON for better performance
571
+ const indent = process.stdout.isTTY ? 2 : 0;
572
+ console.log(JSON.stringify(result, null, indent));
487
573
  } catch (error) {
574
+ const indent = process.stderr.isTTY ? 2 : 0;
488
575
  console.error(JSON.stringify({
489
576
  error: error.message,
490
577
  timestamp: new Date().toISOString()
491
- }, null, 2));
578
+ }, null, indent));
492
579
  process.exit(1);
493
580
  }
494
581
  })();
@@ -510,5 +597,7 @@ module.exports = {
510
597
  detectBranchStrategy,
511
598
  detectBranchStrategyAsync,
512
599
  detectMainBranch,
513
- detectMainBranchAsync
600
+ detectMainBranchAsync,
601
+ // Testing utilities (prefixed with _ to indicate internal use)
602
+ _resetDeprecationWarnings
514
603
  };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Detection Configurations
3
+ * Centralized config for platform detection logic
4
+ *
5
+ * @module lib/platform/detection-configs
6
+ * @author Avi Fenesh
7
+ * @license MIT
8
+ */
9
+
10
+ /**
11
+ * CI platform detection configuration
12
+ * Order matters - first match wins
13
+ */
14
+ const CI_CONFIGS = [
15
+ { file: '.github/workflows', platform: 'github-actions' },
16
+ { file: '.gitlab-ci.yml', platform: 'gitlab-ci' },
17
+ { file: '.circleci/config.yml', platform: 'circleci' },
18
+ { file: 'Jenkinsfile', platform: 'jenkins' },
19
+ { file: '.travis.yml', platform: 'travis' }
20
+ ];
21
+
22
+ /**
23
+ * Deployment platform detection configuration
24
+ * Order matters - first match wins
25
+ */
26
+ const DEPLOYMENT_CONFIGS = [
27
+ { file: 'railway.json', platform: 'railway' },
28
+ { file: 'railway.toml', platform: 'railway' }, // Legacy Railway marker
29
+ { file: 'vercel.json', platform: 'vercel' },
30
+ { file: 'netlify.toml', platform: 'netlify' },
31
+ { file: '.netlify', platform: 'netlify' }, // Legacy Netlify marker
32
+ { file: 'fly.toml', platform: 'fly' },
33
+ { file: '.platform.app.yaml', platform: 'platformsh' },
34
+ { file: 'render.yaml', platform: 'render' }
35
+ ];
36
+
37
+ /**
38
+ * Project type detection configuration
39
+ * Checks package.json for framework indicators
40
+ */
41
+ const PROJECT_TYPE_CONFIGS = {
42
+ dependencies: [
43
+ { name: 'next', type: 'nextjs' },
44
+ { name: 'react', type: 'react' },
45
+ { name: 'vue', type: 'vue' },
46
+ { name: '@angular/core', type: 'angular' },
47
+ { name: 'svelte', type: 'svelte' },
48
+ { name: 'express', type: 'express' },
49
+ { name: '@nestjs/core', type: 'nestjs' },
50
+ { name: 'gatsby', type: 'gatsby' },
51
+ { name: '@remix-run/react', type: 'remix' },
52
+ { name: 'astro', type: 'astro' }
53
+ ]
54
+ };
55
+
56
+ /**
57
+ * Package manager detection configuration
58
+ * Lock files indicate package manager used
59
+ * Order matters - first match wins (prioritize pnpm > yarn > bun > npm for Node.js)
60
+ */
61
+ const PACKAGE_MANAGER_CONFIGS = [
62
+ { file: 'pnpm-lock.yaml', manager: 'pnpm' },
63
+ { file: 'yarn.lock', manager: 'yarn' },
64
+ { file: 'bun.lockb', manager: 'bun' },
65
+ { file: 'package-lock.json', manager: 'npm' },
66
+ { file: 'poetry.lock', manager: 'poetry' },
67
+ { file: 'Pipfile.lock', manager: 'pipenv' },
68
+ { file: 'Cargo.lock', manager: 'cargo' },
69
+ { file: 'go.sum', manager: 'go' }
70
+ ];
71
+
72
+ /**
73
+ * Branch strategy patterns
74
+ */
75
+ const BRANCH_STRATEGIES = {
76
+ gitflow: ['develop', 'main', 'master'],
77
+ githubflow: ['main'],
78
+ trunkbased: ['main', 'trunk']
79
+ };
80
+
81
+ /**
82
+ * Main branch candidates (in priority order)
83
+ */
84
+ const MAIN_BRANCH_CANDIDATES = ['main', 'master', 'trunk', 'develop'];
85
+
86
+ module.exports = {
87
+ CI_CONFIGS,
88
+ DEPLOYMENT_CONFIGS,
89
+ PROJECT_TYPE_CONFIGS,
90
+ PACKAGE_MANAGER_CONFIGS,
91
+ BRANCH_STRATEGIES,
92
+ MAIN_BRANCH_CANDIDATES
93
+ };