awesome-slash 2.4.4 → 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.
- package/.claude-plugin/marketplace.json +6 -6
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +88 -1
- package/README.md +173 -161
- package/SECURITY.md +25 -81
- package/adapters/codex/install.sh +58 -16
- package/adapters/opencode/install.sh +92 -23
- package/lib/index.js +47 -4
- package/lib/patterns/review-patterns.js +58 -11
- package/lib/patterns/slop-patterns.js +154 -147
- package/lib/platform/detect-platform.js +99 -350
- package/lib/platform/detection-configs.js +93 -0
- package/lib/platform/verify-tools.js +10 -78
- package/lib/schemas/README.md +195 -0
- package/lib/schemas/validator.js +247 -0
- package/lib/sources/custom-handler.js +199 -0
- package/lib/sources/policy-questions.js +239 -0
- package/lib/sources/source-cache.js +149 -0
- package/lib/state/workflow-state.js +363 -665
- package/lib/types/README.md +292 -0
- package/lib/types/agent-frontmatter.d.ts +134 -0
- package/lib/types/command-frontmatter.d.ts +107 -0
- package/lib/types/hook-frontmatter.d.ts +115 -0
- package/lib/types/index.d.ts +84 -0
- package/lib/types/plugin-manifest.d.ts +102 -0
- package/lib/types/skill-frontmatter.d.ts +89 -0
- package/lib/utils/cache-manager.js +154 -0
- package/lib/utils/context-optimizer.js +5 -36
- package/lib/utils/deprecation.js +37 -0
- package/lib/utils/shell-escape.js +88 -0
- package/mcp-server/index.js +513 -18
- package/package.json +6 -2
- package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop-around/lib/index.js +170 -0
- package/plugins/deslop-around/lib/patterns/review-patterns.js +58 -11
- package/plugins/deslop-around/lib/patterns/slop-patterns.js +170 -129
- package/plugins/deslop-around/lib/platform/detect-platform.js +212 -123
- package/plugins/deslop-around/lib/platform/detection-configs.js +93 -0
- package/plugins/deslop-around/lib/platform/verify-tools.js +10 -1
- package/plugins/deslop-around/lib/schemas/README.md +195 -0
- package/plugins/deslop-around/lib/schemas/validator.js +205 -0
- package/plugins/deslop-around/lib/sources/custom-handler.js +199 -0
- package/plugins/deslop-around/lib/sources/policy-questions.js +239 -0
- package/plugins/deslop-around/lib/sources/source-cache.js +149 -0
- package/plugins/deslop-around/lib/state/workflow-state.js +382 -484
- package/plugins/deslop-around/lib/types/README.md +292 -0
- package/plugins/deslop-around/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/deslop-around/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/deslop-around/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/deslop-around/lib/types/index.d.ts +84 -0
- package/plugins/deslop-around/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/deslop-around/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/deslop-around/lib/utils/cache-manager.js +154 -0
- package/plugins/deslop-around/lib/utils/context-optimizer.js +115 -37
- package/plugins/deslop-around/lib/utils/deprecation.js +37 -0
- package/plugins/deslop-around/lib/utils/shell-escape.js +88 -0
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/delivery-validator.md +2 -2
- package/plugins/next-task/agents/implementation-agent.md +3 -4
- package/plugins/next-task/agents/planning-agent.md +77 -19
- package/plugins/next-task/agents/review-orchestrator.md +21 -122
- package/plugins/next-task/agents/task-discoverer.md +164 -23
- package/plugins/next-task/commands/next-task.md +180 -14
- package/plugins/next-task/lib/index.js +170 -0
- package/plugins/next-task/lib/patterns/review-patterns.js +58 -11
- package/plugins/next-task/lib/patterns/slop-patterns.js +170 -129
- package/plugins/next-task/lib/platform/detect-platform.js +212 -123
- package/plugins/next-task/lib/platform/detection-configs.js +93 -0
- package/plugins/next-task/lib/platform/verify-tools.js +10 -1
- package/plugins/next-task/lib/schemas/README.md +195 -0
- package/plugins/next-task/lib/schemas/validator.js +205 -0
- package/plugins/next-task/lib/sources/custom-handler.js +199 -0
- package/plugins/next-task/lib/sources/policy-questions.js +239 -0
- package/plugins/next-task/lib/sources/source-cache.js +149 -0
- package/plugins/next-task/lib/state/workflow-state.js +382 -484
- package/plugins/next-task/lib/types/README.md +292 -0
- package/plugins/next-task/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/next-task/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/next-task/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/next-task/lib/types/index.d.ts +84 -0
- package/plugins/next-task/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/next-task/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/next-task/lib/utils/cache-manager.js +154 -0
- package/plugins/next-task/lib/utils/context-optimizer.js +115 -37
- package/plugins/next-task/lib/utils/deprecation.js +37 -0
- package/plugins/next-task/lib/utils/shell-escape.js +88 -0
- package/plugins/project-review/.claude-plugin/plugin.json +1 -1
- package/plugins/project-review/lib/index.js +170 -0
- package/plugins/project-review/lib/patterns/review-patterns.js +58 -11
- package/plugins/project-review/lib/patterns/slop-patterns.js +170 -129
- package/plugins/project-review/lib/platform/detect-platform.js +212 -123
- package/plugins/project-review/lib/platform/detection-configs.js +93 -0
- package/plugins/project-review/lib/platform/verify-tools.js +10 -1
- package/plugins/project-review/lib/schemas/README.md +195 -0
- package/plugins/project-review/lib/schemas/validator.js +205 -0
- package/plugins/project-review/lib/sources/custom-handler.js +199 -0
- package/plugins/project-review/lib/sources/policy-questions.js +239 -0
- package/plugins/project-review/lib/sources/source-cache.js +149 -0
- package/plugins/project-review/lib/state/workflow-state.js +382 -484
- package/plugins/project-review/lib/types/README.md +292 -0
- package/plugins/project-review/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/project-review/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/project-review/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/project-review/lib/types/index.d.ts +84 -0
- package/plugins/project-review/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/project-review/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/project-review/lib/utils/cache-manager.js +154 -0
- package/plugins/project-review/lib/utils/context-optimizer.js +115 -37
- package/plugins/project-review/lib/utils/deprecation.js +37 -0
- package/plugins/project-review/lib/utils/shell-escape.js +88 -0
- package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
- package/plugins/reality-check/agents/code-explorer.md +1 -1
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/lib/index.js +170 -0
- package/plugins/ship/lib/patterns/review-patterns.js +58 -11
- package/plugins/ship/lib/patterns/slop-patterns.js +170 -129
- package/plugins/ship/lib/platform/detect-platform.js +212 -123
- package/plugins/ship/lib/platform/detection-configs.js +93 -0
- package/plugins/ship/lib/platform/verify-tools.js +10 -1
- package/plugins/ship/lib/schemas/README.md +195 -0
- package/plugins/ship/lib/schemas/validator.js +205 -0
- package/plugins/ship/lib/sources/custom-handler.js +199 -0
- package/plugins/ship/lib/sources/policy-questions.js +239 -0
- package/plugins/ship/lib/sources/source-cache.js +149 -0
- package/plugins/ship/lib/state/workflow-state.js +382 -484
- package/plugins/ship/lib/types/README.md +292 -0
- package/plugins/ship/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/ship/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/ship/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/ship/lib/types/index.d.ts +84 -0
- package/plugins/ship/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/ship/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/ship/lib/utils/cache-manager.js +154 -0
- package/plugins/ship/lib/utils/context-optimizer.js +115 -37
- package/plugins/ship/lib/utils/deprecation.js +37 -0
- package/plugins/ship/lib/utils/shell-escape.js +88 -0
- package/lib/state/workflow-state.schema.json +0 -282
- package/plugins/deslop-around/lib/state/workflow-state.schema.json +0 -282
- package/plugins/next-task/agents/policy-selector.md +0 -248
- package/plugins/next-task/lib/state/tasks-registry.schema.json +0 -85
- package/plugins/next-task/lib/state/workflow-state.schema.json +0 -282
- package/plugins/next-task/lib/state/worktree-status.schema.json +0 -219
- package/plugins/project-review/lib/state/workflow-state.schema.json +0 -282
- 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
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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(
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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(
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
existsCachedAsync
|
|
234
|
-
|
|
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 =
|
|
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
|
-
|
|
308
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
476
|
+
warnDeprecation('detect', 'detectAsync');
|
|
397
477
|
|
|
398
478
|
// Return cached result if still valid
|
|
399
|
-
if (!forceRefresh
|
|
400
|
-
|
|
479
|
+
if (!forceRefresh) {
|
|
480
|
+
const cached = _detectionCache.get('detection');
|
|
481
|
+
if (cached !== undefined) {
|
|
482
|
+
return cached;
|
|
483
|
+
}
|
|
401
484
|
}
|
|
402
485
|
|
|
403
|
-
|
|
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(
|
|
495
|
+
timestamp: new Date().toISOString()
|
|
413
496
|
};
|
|
414
|
-
_cacheExpiry = now + CACHE_TTL_MS;
|
|
415
497
|
|
|
416
|
-
|
|
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
|
|
430
|
-
|
|
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
|
-
|
|
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(
|
|
547
|
+
timestamp: new Date().toISOString()
|
|
464
548
|
};
|
|
465
|
-
_cacheExpiry = now + CACHE_TTL_MS;
|
|
466
549
|
|
|
467
|
-
|
|
550
|
+
_detectionCache.set('detection', detection);
|
|
551
|
+
return detection;
|
|
468
552
|
}
|
|
469
553
|
|
|
470
554
|
/**
|
|
471
|
-
* Invalidate
|
|
555
|
+
* Invalidate all detection caches
|
|
472
556
|
* Call this after making changes that affect platform detection
|
|
473
557
|
*/
|
|
474
558
|
function invalidateCache() {
|
|
475
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
};
|