awesome-slash 2.4.4 → 2.5.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.
Files changed (151) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +123 -1
  4. package/README.md +186 -159
  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/state-dir.js +122 -0
  14. package/lib/platform/verify-tools.js +10 -78
  15. package/lib/schemas/README.md +195 -0
  16. package/lib/schemas/validator.js +247 -0
  17. package/lib/sources/custom-handler.js +199 -0
  18. package/lib/sources/policy-questions.js +239 -0
  19. package/lib/sources/source-cache.js +164 -0
  20. package/lib/state/workflow-state.js +368 -665
  21. package/lib/types/README.md +292 -0
  22. package/lib/types/agent-frontmatter.d.ts +134 -0
  23. package/lib/types/command-frontmatter.d.ts +107 -0
  24. package/lib/types/hook-frontmatter.d.ts +115 -0
  25. package/lib/types/index.d.ts +84 -0
  26. package/lib/types/plugin-manifest.d.ts +102 -0
  27. package/lib/types/skill-frontmatter.d.ts +89 -0
  28. package/lib/utils/cache-manager.js +154 -0
  29. package/lib/utils/context-optimizer.js +5 -36
  30. package/lib/utils/deprecation.js +37 -0
  31. package/lib/utils/shell-escape.js +88 -0
  32. package/mcp-server/index.js +513 -22
  33. package/package.json +6 -2
  34. package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
  35. package/plugins/deslop-around/lib/index.js +170 -0
  36. package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
  37. package/plugins/deslop-around/lib/patterns/slop-patterns.js +169 -129
  38. package/plugins/deslop-around/lib/platform/detect-platform.js +162 -316
  39. package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
  40. package/plugins/deslop-around/lib/platform/state-dir.js +122 -0
  41. package/plugins/deslop-around/lib/platform/verify-tools.js +10 -78
  42. package/plugins/deslop-around/lib/schemas/README.md +195 -0
  43. package/plugins/deslop-around/lib/schemas/validator.js +247 -0
  44. package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
  45. package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
  46. package/plugins/deslop-around/lib/sources/source-cache.js +164 -0
  47. package/plugins/deslop-around/lib/state/workflow-state.js +387 -484
  48. package/plugins/deslop-around/lib/types/README.md +292 -0
  49. package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
  50. package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
  51. package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
  52. package/plugins/deslop-around/lib/types/index.d.ts +84 -0
  53. package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
  54. package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
  55. package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
  56. package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
  57. package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
  58. package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
  59. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  60. package/plugins/next-task/agents/delivery-validator.md +2 -2
  61. package/plugins/next-task/agents/implementation-agent.md +3 -4
  62. package/plugins/next-task/agents/planning-agent.md +77 -19
  63. package/plugins/next-task/agents/review-orchestrator.md +21 -122
  64. package/plugins/next-task/agents/task-discoverer.md +164 -23
  65. package/plugins/next-task/commands/next-task.md +180 -14
  66. package/plugins/next-task/lib/index.js +170 -0
  67. package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
  68. package/plugins/next-task/lib/patterns/slop-patterns.js +169 -129
  69. package/plugins/next-task/lib/platform/detect-platform.js +162 -316
  70. package/plugins/next-task/lib/platform/detection-configs.js +93 -0
  71. package/plugins/next-task/lib/platform/state-dir.js +122 -0
  72. package/plugins/next-task/lib/platform/verify-tools.js +10 -78
  73. package/plugins/next-task/lib/schemas/README.md +195 -0
  74. package/plugins/next-task/lib/schemas/validator.js +247 -0
  75. package/plugins/next-task/lib/sources/custom-handler.js +199 -0
  76. package/plugins/next-task/lib/sources/policy-questions.js +239 -0
  77. package/plugins/next-task/lib/sources/source-cache.js +164 -0
  78. package/plugins/next-task/lib/state/workflow-state.js +387 -484
  79. package/plugins/next-task/lib/types/README.md +292 -0
  80. package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
  81. package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
  82. package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
  83. package/plugins/next-task/lib/types/index.d.ts +84 -0
  84. package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
  85. package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
  86. package/plugins/next-task/lib/utils/cache-manager.js +154 -0
  87. package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
  88. package/plugins/next-task/lib/utils/deprecation.js +37 -0
  89. package/plugins/next-task/lib/utils/shell-escape.js +88 -0
  90. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  91. package/plugins/project-review/lib/index.js +170 -0
  92. package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
  93. package/plugins/project-review/lib/patterns/slop-patterns.js +169 -129
  94. package/plugins/project-review/lib/platform/detect-platform.js +162 -316
  95. package/plugins/project-review/lib/platform/detection-configs.js +93 -0
  96. package/plugins/project-review/lib/platform/state-dir.js +122 -0
  97. package/plugins/project-review/lib/platform/verify-tools.js +10 -78
  98. package/plugins/project-review/lib/schemas/README.md +195 -0
  99. package/plugins/project-review/lib/schemas/validator.js +247 -0
  100. package/plugins/project-review/lib/sources/custom-handler.js +199 -0
  101. package/plugins/project-review/lib/sources/policy-questions.js +239 -0
  102. package/plugins/project-review/lib/sources/source-cache.js +164 -0
  103. package/plugins/project-review/lib/state/workflow-state.js +387 -484
  104. package/plugins/project-review/lib/types/README.md +292 -0
  105. package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
  106. package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
  107. package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
  108. package/plugins/project-review/lib/types/index.d.ts +84 -0
  109. package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
  110. package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
  111. package/plugins/project-review/lib/utils/cache-manager.js +154 -0
  112. package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
  113. package/plugins/project-review/lib/utils/deprecation.js +37 -0
  114. package/plugins/project-review/lib/utils/shell-escape.js +88 -0
  115. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  116. package/plugins/reality-check/agents/code-explorer.md +1 -1
  117. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  118. package/plugins/ship/lib/index.js +170 -0
  119. package/plugins/ship/lib/patterns/review-patterns.js +58 -11
  120. package/plugins/ship/lib/patterns/slop-patterns.js +169 -129
  121. package/plugins/ship/lib/platform/detect-platform.js +162 -316
  122. package/plugins/ship/lib/platform/detection-configs.js +93 -0
  123. package/plugins/ship/lib/platform/state-dir.js +122 -0
  124. package/plugins/ship/lib/platform/verify-tools.js +10 -78
  125. package/plugins/ship/lib/schemas/README.md +195 -0
  126. package/plugins/ship/lib/schemas/validator.js +247 -0
  127. package/plugins/ship/lib/sources/custom-handler.js +199 -0
  128. package/plugins/ship/lib/sources/policy-questions.js +239 -0
  129. package/plugins/ship/lib/sources/source-cache.js +164 -0
  130. package/plugins/ship/lib/state/workflow-state.js +387 -484
  131. package/plugins/ship/lib/types/README.md +292 -0
  132. package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
  133. package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
  134. package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
  135. package/plugins/ship/lib/types/index.d.ts +84 -0
  136. package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
  137. package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
  138. package/plugins/ship/lib/utils/cache-manager.js +154 -0
  139. package/plugins/ship/lib/utils/context-optimizer.js +115 -37
  140. package/plugins/ship/lib/utils/deprecation.js +37 -0
  141. package/plugins/ship/lib/utils/shell-escape.js +88 -0
  142. package/scripts/install/codex.sh +216 -72
  143. package/scripts/install/opencode.sh +197 -21
  144. package/lib/state/workflow-state.schema.json +0 -282
  145. package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
  146. package/plugins/next-task/agents/policy-selector.md +0 -248
  147. package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
  148. package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
  149. package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
  150. package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
  151. package/plugins/ship/lib/state/workflow-state.schema.json +0 -282
@@ -11,43 +11,118 @@
11
11
  */
12
12
 
13
13
  const fs = require('fs');
14
- const { execSync, exec } = require('child_process');
14
+ const path = require('path');
15
+ const { 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 { CacheManager } = require('../utils/cache-manager');
23
+ const {
24
+ CI_CONFIGS,
25
+ DEPLOYMENT_CONFIGS,
26
+ PACKAGE_MANAGER_CONFIGS
27
+ } = require('./detection-configs');
24
28
 
25
- // File read cache to avoid reading the same file multiple times (#17)
26
- const _fileCache = new Map();
27
- const _existsCache = new Map();
29
+ /**
30
+ * Default timeout for async operations (5 seconds)
31
+ */
32
+ const DEFAULT_ASYNC_TIMEOUT_MS = 5000;
28
33
 
29
34
  /**
30
- * Check if a file exists (cached)
31
- * @param {string} filepath - Path to check
32
- * @returns {boolean}
35
+ * Maximum JSON file size to parse (1MB) - prevents DoS via large files
33
36
  */
34
- function existsCached(filepath) {
35
- if (_existsCache.has(filepath)) {
36
- return _existsCache.get(filepath);
37
+ const MAX_JSON_SIZE_BYTES = 1024 * 1024;
38
+
39
+ /**
40
+ * Safely parse JSON content with size limit
41
+ * @param {string} content - JSON string to parse
42
+ * @param {string} filename - Filename for error messages
43
+ * @returns {Object|null} Parsed object or null if invalid/too large
44
+ */
45
+ function safeJSONParse(content, filename = 'unknown') {
46
+ if (!content || typeof content !== 'string') {
47
+ return null;
48
+ }
49
+ if (content.length > MAX_JSON_SIZE_BYTES) {
50
+ return null;
51
+ }
52
+ try {
53
+ return JSON.parse(content);
54
+ } catch {
55
+ return null;
37
56
  }
38
- const exists = fs.existsSync(filepath);
39
- _existsCache.set(filepath, exists);
40
- return exists;
41
57
  }
42
58
 
43
59
  /**
44
- * Check if a file exists (cached, async)
60
+ * Wrap a promise with a timeout
61
+ * @param {Promise} promise - Promise to wrap
62
+ * @param {number} timeoutMs - Timeout in milliseconds
63
+ * @param {string} operation - Operation name for error message
64
+ * @returns {Promise} Promise that rejects on timeout
65
+ */
66
+ function withTimeout(promise, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, operation = 'operation') {
67
+ let timeoutId;
68
+ const timeoutPromise = new Promise((_, reject) => {
69
+ timeoutId = setTimeout(() => {
70
+ reject(new Error(`${operation} timed out after ${timeoutMs}ms`));
71
+ }, timeoutMs);
72
+ });
73
+
74
+ return Promise.race([promise, timeoutPromise]).finally(() => {
75
+ clearTimeout(timeoutId);
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Execute a command with timeout protection
81
+ * @param {string} cmd - Command to execute
82
+ * @param {Object} options - exec options
83
+ * @param {number} timeoutMs - Timeout in milliseconds
84
+ * @returns {Promise<{stdout: string, stderr: string}>}
85
+ */
86
+ async function execWithTimeout(cmd, options = {}, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS) {
87
+ return withTimeout(execAsync(cmd, options), timeoutMs, `exec: ${cmd.substring(0, 50)}`);
88
+ }
89
+
90
+ // Maximum cached file size constant
91
+ const MAX_CACHED_FILE_SIZE = 64 * 1024; // 64KB max per cached file
92
+
93
+ // Cache instances using CacheManager abstraction
94
+ const _detectionCache = new CacheManager({ maxSize: 1, ttl: 60000 });
95
+ const _fileCache = new CacheManager({ maxSize: 100, ttl: 60000, maxValueSize: MAX_CACHED_FILE_SIZE });
96
+ const _existsCache = new CacheManager({ maxSize: 100, ttl: 60000 });
97
+
98
+ /**
99
+ * Generic file-based detector
100
+ * @param {Array} configs - Array of {file, platform} objects
101
+ * @param {Function} existsChecker - Async function to check file existence
102
+ * @returns {Promise<string|null>} Detected platform or null
103
+ */
104
+ async function detectFromFiles(configs, existsChecker) {
105
+ const checks = await Promise.all(
106
+ configs.map(({ file }) => existsChecker(file))
107
+ );
108
+
109
+ for (let i = 0; i < checks.length; i++) {
110
+ if (checks[i]) {
111
+ return configs[i].platform;
112
+ }
113
+ }
114
+ return null;
115
+ }
116
+
117
+ /**
118
+ * Check if a file exists (cached)
45
119
  * @param {string} filepath - Path to check
46
120
  * @returns {Promise<boolean>}
47
121
  */
48
- async function existsCachedAsync(filepath) {
49
- if (_existsCache.has(filepath)) {
50
- return _existsCache.get(filepath);
122
+ async function existsCached(filepath) {
123
+ const cached = _existsCache.get(filepath);
124
+ if (cached !== undefined) {
125
+ return cached;
51
126
  }
52
127
  try {
53
128
  await fsPromises.access(filepath);
@@ -61,142 +136,58 @@ async function existsCachedAsync(filepath) {
61
136
 
62
137
  /**
63
138
  * Read file contents (cached)
64
- * @param {string} filepath - Path to read
65
- * @returns {string|null}
66
- */
67
- function readFileCached(filepath) {
68
- if (_fileCache.has(filepath)) {
69
- return _fileCache.get(filepath);
70
- }
71
- try {
72
- const content = fs.readFileSync(filepath, 'utf8');
73
- _fileCache.set(filepath, content);
74
- return content;
75
- } catch {
76
- _fileCache.set(filepath, null);
77
- return null;
78
- }
79
- }
80
-
81
- /**
82
- * Read file contents (cached, async)
139
+ * Only caches files smaller than MAX_CACHED_FILE_SIZE to prevent memory bloat
140
+ * Optimized: normalizes filepath to prevent cache pollution from variant paths
83
141
  * @param {string} filepath - Path to read
84
142
  * @returns {Promise<string|null>}
85
143
  */
86
- async function readFileCachedAsync(filepath) {
87
- if (_fileCache.has(filepath)) {
88
- return _fileCache.get(filepath);
144
+ async function readFileCached(filepath) {
145
+ const normalizedPath = path.resolve(filepath);
146
+
147
+ const cached = _fileCache.get(normalizedPath);
148
+ if (cached !== undefined) {
149
+ return cached;
89
150
  }
90
151
  try {
91
- const content = await fsPromises.readFile(filepath, 'utf8');
92
- _fileCache.set(filepath, content);
152
+ const content = await fsPromises.readFile(normalizedPath, 'utf8');
153
+ _fileCache.set(normalizedPath, content);
93
154
  return content;
94
155
  } catch {
95
- _fileCache.set(filepath, null);
156
+ _fileCache.set(normalizedPath, null);
96
157
  return null;
97
158
  }
98
159
  }
99
160
 
100
161
  /**
101
162
  * Detects CI platform by scanning for configuration files
102
- * @returns {string|null} CI platform name or null if not detected
103
- */
104
- 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;
111
- }
112
-
113
- /**
114
- * Detects CI platform by scanning for configuration files (async)
115
163
  * @returns {Promise<string|null>} CI platform name or null if not detected
116
164
  */
117
- 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;
165
+ async function detectCI() {
166
+ return detectFromFiles(CI_CONFIGS, existsCached);
132
167
  }
133
168
 
134
169
  /**
135
170
  * Detects deployment platform by scanning for platform-specific files
136
- * @returns {string|null} Deployment platform name or null if not detected
137
- */
138
- 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;
146
- }
147
-
148
- /**
149
- * Detects deployment platform by scanning for platform-specific files (async)
150
171
  * @returns {Promise<string|null>} Deployment platform name or null if not detected
151
172
  */
152
- 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;
173
+ async function detectDeployment() {
174
+ return detectFromFiles(DEPLOYMENT_CONFIGS, existsCached);
171
175
  }
172
176
 
173
177
  /**
174
178
  * Detects project type by scanning for language-specific files
175
- * @returns {string} Project type identifier
176
- */
177
- function detectProjectType() {
178
- if (existsCached('package.json')) return 'nodejs';
179
- if (existsCached('requirements.txt') || existsCached('pyproject.toml') || existsCached('setup.py')) return 'python';
180
- if (existsCached('Cargo.toml')) return 'rust';
181
- if (existsCached('go.mod')) return 'go';
182
- if (existsCached('pom.xml') || existsCached('build.gradle')) return 'java';
183
- return 'unknown';
184
- }
185
-
186
- /**
187
- * Detects project type by scanning for language-specific files (async)
188
179
  * @returns {Promise<string>} Project type identifier
189
180
  */
190
- async function detectProjectTypeAsync() {
181
+ async function detectProjectType() {
191
182
  const checks = await Promise.all([
192
- existsCachedAsync('package.json'),
193
- existsCachedAsync('requirements.txt'),
194
- existsCachedAsync('pyproject.toml'),
195
- existsCachedAsync('setup.py'),
196
- existsCachedAsync('Cargo.toml'),
197
- existsCachedAsync('go.mod'),
198
- existsCachedAsync('pom.xml'),
199
- existsCachedAsync('build.gradle')
183
+ existsCached('package.json'),
184
+ existsCached('requirements.txt'),
185
+ existsCached('pyproject.toml'),
186
+ existsCached('setup.py'),
187
+ existsCached('Cargo.toml'),
188
+ existsCached('go.mod'),
189
+ existsCached('pom.xml'),
190
+ existsCached('build.gradle')
200
191
  ]);
201
192
 
202
193
  if (checks[0]) return 'nodejs';
@@ -209,103 +200,24 @@ async function detectProjectTypeAsync() {
209
200
 
210
201
  /**
211
202
  * Detects package manager by scanning for lockfiles
212
- * @returns {string|null} Package manager name or null if not detected
213
- */
214
- 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;
224
- }
225
-
226
- /**
227
- * Detects package manager by scanning for lockfiles (async)
228
203
  * @returns {Promise<string|null>} Package manager name or null if not detected
229
204
  */
230
- 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;
205
+ async function detectPackageManager() {
206
+ return detectFromFiles(
207
+ PACKAGE_MANAGER_CONFIGS.map(({ file, manager }) => ({ file, platform: manager })),
208
+ existsCached
209
+ );
251
210
  }
252
211
 
253
212
  /**
254
213
  * Detects branch strategy (single-branch vs multi-branch with dev+prod)
255
- * @returns {string} 'single-branch' or 'multi-branch'
256
- */
257
- function detectBranchStrategy() {
258
- try {
259
- // Check both local and remote branches
260
- const localBranches = execSync('git branch', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
261
- let remoteBranches = '';
262
- try {
263
- remoteBranches = execSync('git branch -r', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
264
- } catch {}
265
-
266
- const allBranches = localBranches + remoteBranches;
267
-
268
- const hasStable = allBranches.includes('stable');
269
- const hasProduction = allBranches.includes('production') || allBranches.includes('prod');
270
-
271
- if (hasStable || hasProduction) {
272
- return 'multi-branch'; // dev + prod workflow
273
- }
274
-
275
- // Check deployment configs for multi-environment setup (uses cache)
276
- if (existsCached('railway.json')) {
277
- try {
278
- const content = readFileCached('railway.json');
279
- if (content) {
280
- const config = JSON.parse(content);
281
- // Validate JSON structure before accessing properties
282
- if (config &&
283
- typeof config === 'object' &&
284
- typeof config.environments === 'object' &&
285
- config.environments !== null &&
286
- Object.keys(config.environments).length > 1) {
287
- return 'multi-branch';
288
- }
289
- }
290
- } catch {}
291
- }
292
-
293
- return 'single-branch'; // main only
294
- } catch {
295
- return 'single-branch';
296
- }
297
- }
298
-
299
- /**
300
- * Detects branch strategy (single-branch vs multi-branch with dev+prod) (async)
301
214
  * @returns {Promise<string>} 'single-branch' or 'multi-branch'
302
215
  */
303
- async function detectBranchStrategyAsync() {
216
+ async function detectBranchStrategy() {
304
217
  try {
305
- // Run git commands in parallel
306
218
  const [localResult, remoteResult] = await Promise.all([
307
- execAsync('git branch', { encoding: 'utf8' }).catch(() => ({ stdout: '' })),
308
- execAsync('git branch -r', { encoding: 'utf8' }).catch(() => ({ stdout: '' }))
219
+ execWithTimeout('git branch', { encoding: 'utf8' }).catch(() => ({ stdout: '' })),
220
+ execWithTimeout('git branch -r', { encoding: 'utf8' }).catch(() => ({ stdout: '' }))
309
221
  ]);
310
222
 
311
223
  const allBranches = (localResult.stdout || '') + (remoteResult.stdout || '');
@@ -317,12 +229,11 @@ async function detectBranchStrategyAsync() {
317
229
  return 'multi-branch';
318
230
  }
319
231
 
320
- // Check deployment configs for multi-environment setup (uses cache)
321
- if (await existsCachedAsync('railway.json')) {
232
+ if (await existsCached('railway.json')) {
322
233
  try {
323
- const content = await readFileCachedAsync('railway.json');
234
+ const content = await readFileCached('railway.json');
324
235
  if (content) {
325
- const config = JSON.parse(content);
236
+ const config = safeJSONParse(content, 'railway.json');
326
237
  if (config &&
327
238
  typeof config === 'object' &&
328
239
  typeof config.environments === 'object' &&
@@ -342,43 +253,15 @@ async function detectBranchStrategyAsync() {
342
253
 
343
254
  /**
344
255
  * Detects the main branch name
345
- * @returns {string} Main branch name ('main' or 'master')
346
- */
347
- function detectMainBranch() {
348
- try {
349
- const defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
350
- encoding: 'utf8',
351
- stdio: ['pipe', 'pipe', 'ignore']
352
- })
353
- .trim()
354
- .replace('refs/remotes/origin/', '');
355
- return defaultBranch;
356
- } catch {
357
- // Fallback: check common names
358
- try {
359
- execSync('git rev-parse --verify main', {
360
- encoding: 'utf8',
361
- stdio: ['pipe', 'pipe', 'ignore']
362
- });
363
- return 'main';
364
- } catch {
365
- return 'master';
366
- }
367
- }
368
- }
369
-
370
- /**
371
- * Detects the main branch name (async)
372
256
  * @returns {Promise<string>} Main branch name ('main' or 'master')
373
257
  */
374
- async function detectMainBranchAsync() {
258
+ async function detectMainBranch() {
375
259
  try {
376
- const { stdout } = await execAsync('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' });
260
+ const { stdout } = await execWithTimeout('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' });
377
261
  return stdout.trim().replace('refs/remotes/origin/', '');
378
262
  } catch {
379
- // Fallback: check common names
380
263
  try {
381
- await execAsync('git rev-parse --verify main', { encoding: 'utf8' });
264
+ await execWithTimeout('git rev-parse --verify main', { encoding: 'utf8' });
382
265
  return 'main';
383
266
  } catch {
384
267
  return 'master';
@@ -387,50 +270,19 @@ async function detectMainBranchAsync() {
387
270
  }
388
271
 
389
272
  /**
390
- * Main detection function - aggregates all platform information (sync)
391
- * Uses caching to avoid repeated filesystem/git operations
392
- * @param {boolean} forceRefresh - Force cache refresh
393
- * @returns {Object} Platform configuration object
394
- */
395
- function detect(forceRefresh = false) {
396
- const now = Date.now();
397
-
398
- // Return cached result if still valid
399
- if (!forceRefresh && _cachedDetection && now < _cacheExpiry) {
400
- return _cachedDetection;
401
- }
402
-
403
- _cachedDetection = {
404
- ci: detectCI(),
405
- deployment: detectDeployment(),
406
- projectType: detectProjectType(),
407
- packageManager: detectPackageManager(),
408
- branchStrategy: detectBranchStrategy(),
409
- mainBranch: detectMainBranch(),
410
- hasPlanFile: existsCached('PLAN.md'),
411
- hasTechDebtFile: existsCached('TECHNICAL_DEBT.md'),
412
- timestamp: new Date(now).toISOString()
413
- };
414
- _cacheExpiry = now + CACHE_TTL_MS;
415
-
416
- return _cachedDetection;
417
- }
418
-
419
- /**
420
- * Main detection function - aggregates all platform information (async)
273
+ * Main detection function - aggregates all platform information
421
274
  * Uses Promise.all for parallel execution and caching
422
275
  * @param {boolean} forceRefresh - Force cache refresh
423
276
  * @returns {Promise<Object>} Platform configuration object
424
277
  */
425
- async function detectAsync(forceRefresh = false) {
426
- const now = Date.now();
427
-
428
- // Return cached result if still valid
429
- if (!forceRefresh && _cachedDetection && now < _cacheExpiry) {
430
- return _cachedDetection;
278
+ async function detect(forceRefresh = false) {
279
+ if (!forceRefresh) {
280
+ const cached = _detectionCache.get('detection');
281
+ if (cached !== undefined) {
282
+ return cached;
283
+ }
431
284
  }
432
285
 
433
- // Run all detections in parallel
434
286
  const [
435
287
  ci,
436
288
  deployment,
@@ -441,17 +293,17 @@ async function detectAsync(forceRefresh = false) {
441
293
  hasPlanFile,
442
294
  hasTechDebtFile
443
295
  ] = await Promise.all([
444
- detectCIAsync(),
445
- detectDeploymentAsync(),
446
- detectProjectTypeAsync(),
447
- detectPackageManagerAsync(),
448
- detectBranchStrategyAsync(),
449
- detectMainBranchAsync(),
450
- existsCachedAsync('PLAN.md'),
451
- existsCachedAsync('TECHNICAL_DEBT.md')
296
+ detectCI(),
297
+ detectDeployment(),
298
+ detectProjectType(),
299
+ detectPackageManager(),
300
+ detectBranchStrategy(),
301
+ detectMainBranch(),
302
+ existsCached('PLAN.md'),
303
+ existsCached('TECHNICAL_DEBT.md')
452
304
  ]);
453
305
 
454
- _cachedDetection = {
306
+ const detection = {
455
307
  ci,
456
308
  deployment,
457
309
  projectType,
@@ -460,35 +312,36 @@ async function detectAsync(forceRefresh = false) {
460
312
  mainBranch,
461
313
  hasPlanFile,
462
314
  hasTechDebtFile,
463
- timestamp: new Date(now).toISOString()
315
+ timestamp: new Date().toISOString()
464
316
  };
465
- _cacheExpiry = now + CACHE_TTL_MS;
466
317
 
467
- return _cachedDetection;
318
+ _detectionCache.set('detection', detection);
319
+ return detection;
468
320
  }
469
321
 
470
322
  /**
471
- * Invalidate the detection cache
323
+ * Invalidate all detection caches
472
324
  * Call this after making changes that affect platform detection
473
325
  */
474
326
  function invalidateCache() {
475
- _cachedDetection = null;
476
- _cacheExpiry = 0;
327
+ _detectionCache.clear();
477
328
  _fileCache.clear();
478
329
  _existsCache.clear();
479
330
  }
480
331
 
481
- // When run directly, output JSON (uses async for better performance)
332
+ // When run directly, output JSON
482
333
  if (require.main === module) {
483
334
  (async () => {
484
335
  try {
485
- const result = await detectAsync();
486
- console.log(JSON.stringify(result, null, 2));
336
+ const result = await detect();
337
+ const indent = process.stdout.isTTY ? 2 : 0;
338
+ console.log(JSON.stringify(result, null, indent));
487
339
  } catch (error) {
340
+ const indent = process.stderr.isTTY ? 2 : 0;
488
341
  console.error(JSON.stringify({
489
342
  error: error.message,
490
343
  timestamp: new Date().toISOString()
491
- }, null, 2));
344
+ }, null, indent));
492
345
  process.exit(1);
493
346
  }
494
347
  })();
@@ -497,18 +350,11 @@ if (require.main === module) {
497
350
  // Export for use as module
498
351
  module.exports = {
499
352
  detect,
500
- detectAsync,
501
353
  invalidateCache,
502
354
  detectCI,
503
- detectCIAsync,
504
355
  detectDeployment,
505
- detectDeploymentAsync,
506
356
  detectProjectType,
507
- detectProjectTypeAsync,
508
357
  detectPackageManager,
509
- detectPackageManagerAsync,
510
358
  detectBranchStrategy,
511
- detectBranchStrategyAsync,
512
- detectMainBranch,
513
- detectMainBranchAsync
359
+ detectMainBranch
514
360
  };