awesome-slash 2.5.0 → 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 (44) hide show
  1. package/.claude-plugin/marketplace.json +6 -6
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +35 -0
  4. package/README.md +23 -8
  5. package/lib/platform/state-dir.js +122 -0
  6. package/lib/sources/source-cache.js +26 -11
  7. package/lib/state/workflow-state.js +18 -13
  8. package/mcp-server/index.js +7 -11
  9. package/package.json +1 -1
  10. package/plugins/deslop-around/.claude-plugin/plugin.json +1 -1
  11. package/plugins/deslop-around/lib/patterns/slop-patterns.js +2 -3
  12. package/plugins/deslop-around/lib/platform/detect-platform.js +44 -287
  13. package/plugins/deslop-around/lib/platform/state-dir.js +122 -0
  14. package/plugins/deslop-around/lib/platform/verify-tools.js +11 -88
  15. package/plugins/deslop-around/lib/schemas/validator.js +44 -2
  16. package/plugins/deslop-around/lib/sources/source-cache.js +26 -11
  17. package/plugins/deslop-around/lib/state/workflow-state.js +18 -13
  18. package/plugins/next-task/.claude-plugin/plugin.json +1 -1
  19. package/plugins/next-task/lib/patterns/slop-patterns.js +2 -3
  20. package/plugins/next-task/lib/platform/detect-platform.js +44 -287
  21. package/plugins/next-task/lib/platform/state-dir.js +122 -0
  22. package/plugins/next-task/lib/platform/verify-tools.js +11 -88
  23. package/plugins/next-task/lib/schemas/validator.js +44 -2
  24. package/plugins/next-task/lib/sources/source-cache.js +26 -11
  25. package/plugins/next-task/lib/state/workflow-state.js +18 -13
  26. package/plugins/project-review/.claude-plugin/plugin.json +1 -1
  27. package/plugins/project-review/lib/patterns/slop-patterns.js +2 -3
  28. package/plugins/project-review/lib/platform/detect-platform.js +44 -287
  29. package/plugins/project-review/lib/platform/state-dir.js +122 -0
  30. package/plugins/project-review/lib/platform/verify-tools.js +11 -88
  31. package/plugins/project-review/lib/schemas/validator.js +44 -2
  32. package/plugins/project-review/lib/sources/source-cache.js +26 -11
  33. package/plugins/project-review/lib/state/workflow-state.js +18 -13
  34. package/plugins/reality-check/.claude-plugin/plugin.json +1 -1
  35. package/plugins/ship/.claude-plugin/plugin.json +1 -1
  36. package/plugins/ship/lib/patterns/slop-patterns.js +2 -3
  37. package/plugins/ship/lib/platform/detect-platform.js +44 -287
  38. package/plugins/ship/lib/platform/state-dir.js +122 -0
  39. package/plugins/ship/lib/platform/verify-tools.js +11 -88
  40. package/plugins/ship/lib/schemas/validator.js +44 -2
  41. package/plugins/ship/lib/sources/source-cache.js +26 -11
  42. package/plugins/ship/lib/state/workflow-state.js +18 -13
  43. package/scripts/install/codex.sh +216 -72
  44. package/scripts/install/opencode.sh +197 -21
@@ -12,14 +12,13 @@
12
12
 
13
13
  const fs = require('fs');
14
14
  const path = require('path');
15
- const { execSync, exec } = require('child_process');
15
+ const { exec } = require('child_process');
16
16
  const { promisify } = require('util');
17
17
 
18
18
  const execAsync = promisify(exec);
19
19
  const fsPromises = fs.promises;
20
20
 
21
21
  // Import shared utilities
22
- const { warnDeprecation, _resetDeprecationWarnings } = require('../utils/deprecation');
23
22
  const { CacheManager } = require('../utils/cache-manager');
24
23
  const {
25
24
  CI_CONFIGS,
@@ -48,7 +47,6 @@ function safeJSONParse(content, filename = 'unknown') {
48
47
  return null;
49
48
  }
50
49
  if (content.length > MAX_JSON_SIZE_BYTES) {
51
- // File too large - skip parsing to prevent DoS
52
50
  return null;
53
51
  }
54
52
  try {
@@ -85,48 +83,29 @@ function withTimeout(promise, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS, operation =
85
83
  * @param {number} timeoutMs - Timeout in milliseconds
86
84
  * @returns {Promise<{stdout: string, stderr: string}>}
87
85
  */
88
- async function execAsyncWithTimeout(cmd, options = {}, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS) {
86
+ async function execWithTimeout(cmd, options = {}, timeoutMs = DEFAULT_ASYNC_TIMEOUT_MS) {
89
87
  return withTimeout(execAsync(cmd, options), timeoutMs, `exec: ${cmd.substring(0, 50)}`);
90
88
  }
91
89
 
92
- // Maximum JSON file size and cached file size constants
90
+ // Maximum cached file size constant
93
91
  const MAX_CACHED_FILE_SIZE = 64 * 1024; // 64KB max per cached file
94
92
 
95
93
  // Cache instances using CacheManager abstraction
96
- const _detectionCache = new CacheManager({ maxSize: 1, ttl: 60000 }); // Single detection result, 1 min TTL
94
+ const _detectionCache = new CacheManager({ maxSize: 1, ttl: 60000 });
97
95
  const _fileCache = new CacheManager({ maxSize: 100, ttl: 60000, maxValueSize: MAX_CACHED_FILE_SIZE });
98
96
  const _existsCache = new CacheManager({ maxSize: 100, ttl: 60000 });
99
97
 
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
98
  /**
118
- * Generic file-based detector (asynchronous)
99
+ * Generic file-based detector
119
100
  * @param {Array} configs - Array of {file, platform} objects
120
101
  * @param {Function} existsChecker - Async function to check file existence
121
102
  * @returns {Promise<string|null>} Detected platform or null
122
103
  */
123
- async function detectFromFilesAsync(configs, existsChecker) {
124
- // Check all files in parallel for better performance
104
+ async function detectFromFiles(configs, existsChecker) {
125
105
  const checks = await Promise.all(
126
106
  configs.map(({ file }) => existsChecker(file))
127
107
  );
128
108
 
129
- // Return first match (maintains priority order)
130
109
  for (let i = 0; i < checks.length; i++) {
131
110
  if (checks[i]) {
132
111
  return configs[i].platform;
@@ -138,24 +117,9 @@ async function detectFromFilesAsync(configs, existsChecker) {
138
117
  /**
139
118
  * Check if a file exists (cached)
140
119
  * @param {string} filepath - Path to check
141
- * @returns {boolean}
142
- */
143
- function existsCached(filepath) {
144
- const cached = _existsCache.get(filepath);
145
- if (cached !== undefined) {
146
- return cached;
147
- }
148
- const exists = fs.existsSync(filepath);
149
- _existsCache.set(filepath, exists);
150
- return exists;
151
- }
152
-
153
- /**
154
- * Check if a file exists (cached, async)
155
- * @param {string} filepath - Path to check
156
120
  * @returns {Promise<boolean>}
157
121
  */
158
- async function existsCachedAsync(filepath) {
122
+ async function existsCached(filepath) {
159
123
  const cached = _existsCache.get(filepath);
160
124
  if (cached !== undefined) {
161
125
  return cached;
@@ -175,39 +139,9 @@ async function existsCachedAsync(filepath) {
175
139
  * Only caches files smaller than MAX_CACHED_FILE_SIZE to prevent memory bloat
176
140
  * Optimized: normalizes filepath to prevent cache pollution from variant paths
177
141
  * @param {string} filepath - Path to read
178
- * @returns {string|null}
179
- */
180
- function readFileCached(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;
188
- }
189
- try {
190
- const content = fs.readFileSync(normalizedPath, 'utf8');
191
- // CacheManager enforces maxValueSize, so small files are cached automatically
192
- _fileCache.set(normalizedPath, content);
193
- return content;
194
- } catch {
195
- // Cache null for missing files (small memory footprint)
196
- _fileCache.set(normalizedPath, null);
197
- return null;
198
- }
199
- }
200
-
201
- /**
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
205
- * @param {string} filepath - Path to read
206
142
  * @returns {Promise<string|null>}
207
143
  */
208
- async function readFileCachedAsync(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
144
+ async function readFileCached(filepath) {
211
145
  const normalizedPath = path.resolve(filepath);
212
146
 
213
147
  const cached = _fileCache.get(normalizedPath);
@@ -216,11 +150,9 @@ async function readFileCachedAsync(filepath) {
216
150
  }
217
151
  try {
218
152
  const content = await fsPromises.readFile(normalizedPath, 'utf8');
219
- // CacheManager enforces maxValueSize, so small files are cached automatically
220
153
  _fileCache.set(normalizedPath, content);
221
154
  return content;
222
155
  } catch {
223
- // Cache null for missing files (small memory footprint)
224
156
  _fileCache.set(normalizedPath, null);
225
157
  return null;
226
158
  }
@@ -228,69 +160,34 @@ async function readFileCachedAsync(filepath) {
228
160
 
229
161
  /**
230
162
  * Detects CI platform by scanning for configuration files
231
- * @deprecated Use detectCIAsync() instead. Will be removed in v3.0.0.
232
- * @returns {string|null} CI platform name or null if not detected
233
- */
234
- function detectCI() {
235
- warnDeprecation('detectCI', 'detectCIAsync');
236
- return detectFromFiles(CI_CONFIGS, existsCached);
237
- }
238
-
239
- /**
240
- * Detects CI platform by scanning for configuration files (async)
241
163
  * @returns {Promise<string|null>} CI platform name or null if not detected
242
164
  */
243
- async function detectCIAsync() {
244
- return detectFromFilesAsync(CI_CONFIGS, existsCachedAsync);
165
+ async function detectCI() {
166
+ return detectFromFiles(CI_CONFIGS, existsCached);
245
167
  }
246
168
 
247
169
  /**
248
170
  * Detects deployment platform by scanning for platform-specific files
249
- * @deprecated Use detectDeploymentAsync() instead. Will be removed in v3.0.0.
250
- * @returns {string|null} Deployment platform name or null if not detected
251
- */
252
- function detectDeployment() {
253
- warnDeprecation('detectDeployment', 'detectDeploymentAsync');
254
- return detectFromFiles(DEPLOYMENT_CONFIGS, existsCached);
255
- }
256
-
257
- /**
258
- * Detects deployment platform by scanning for platform-specific files (async)
259
171
  * @returns {Promise<string|null>} Deployment platform name or null if not detected
260
172
  */
261
- async function detectDeploymentAsync() {
262
- return detectFromFilesAsync(DEPLOYMENT_CONFIGS, existsCachedAsync);
173
+ async function detectDeployment() {
174
+ return detectFromFiles(DEPLOYMENT_CONFIGS, existsCached);
263
175
  }
264
176
 
265
177
  /**
266
178
  * Detects project type by scanning for language-specific files
267
- * @deprecated Use detectProjectTypeAsync() instead. Will be removed in v3.0.0.
268
- * @returns {string} Project type identifier
269
- */
270
- function detectProjectType() {
271
- warnDeprecation('detectProjectType', 'detectProjectTypeAsync');
272
- if (existsCached('package.json')) return 'nodejs';
273
- if (existsCached('requirements.txt') || existsCached('pyproject.toml') || existsCached('setup.py')) return 'python';
274
- if (existsCached('Cargo.toml')) return 'rust';
275
- if (existsCached('go.mod')) return 'go';
276
- if (existsCached('pom.xml') || existsCached('build.gradle')) return 'java';
277
- return 'unknown';
278
- }
279
-
280
- /**
281
- * Detects project type by scanning for language-specific files (async)
282
179
  * @returns {Promise<string>} Project type identifier
283
180
  */
284
- async function detectProjectTypeAsync() {
181
+ async function detectProjectType() {
285
182
  const checks = await Promise.all([
286
- existsCachedAsync('package.json'),
287
- existsCachedAsync('requirements.txt'),
288
- existsCachedAsync('pyproject.toml'),
289
- existsCachedAsync('setup.py'),
290
- existsCachedAsync('Cargo.toml'),
291
- existsCachedAsync('go.mod'),
292
- existsCachedAsync('pom.xml'),
293
- 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')
294
191
  ]);
295
192
 
296
193
  if (checks[0]) return 'nodejs';
@@ -303,86 +200,24 @@ async function detectProjectTypeAsync() {
303
200
 
304
201
  /**
305
202
  * Detects package manager by scanning for lockfiles
306
- * @deprecated Use detectPackageManagerAsync() instead. Will be removed in v3.0.0.
307
- * @returns {string|null} Package manager name or null if not detected
203
+ * @returns {Promise<string|null>} Package manager name or null if not detected
308
204
  */
309
- function detectPackageManager() {
310
- warnDeprecation('detectPackageManager', 'detectPackageManagerAsync');
205
+ async function detectPackageManager() {
311
206
  return detectFromFiles(
312
207
  PACKAGE_MANAGER_CONFIGS.map(({ file, manager }) => ({ file, platform: manager })),
313
208
  existsCached
314
209
  );
315
210
  }
316
211
 
317
- /**
318
- * Detects package manager by scanning for lockfiles (async)
319
- * @returns {Promise<string|null>} Package manager name or null if not detected
320
- */
321
- async function detectPackageManagerAsync() {
322
- return detectFromFilesAsync(
323
- PACKAGE_MANAGER_CONFIGS.map(({ file, manager }) => ({ file, platform: manager })),
324
- existsCachedAsync
325
- );
326
- }
327
-
328
212
  /**
329
213
  * Detects branch strategy (single-branch vs multi-branch with dev+prod)
330
- * @deprecated Use detectBranchStrategyAsync() instead. Will be removed in v3.0.0.
331
- * @returns {string} 'single-branch' or 'multi-branch'
332
- */
333
- function detectBranchStrategy() {
334
- warnDeprecation('detectBranchStrategy', 'detectBranchStrategyAsync');
335
- try {
336
- // Check both local and remote branches
337
- const localBranches = execSync('git branch', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
338
- let remoteBranches = '';
339
- try {
340
- remoteBranches = execSync('git branch -r', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] });
341
- } catch {}
342
-
343
- const allBranches = localBranches + remoteBranches;
344
-
345
- const hasStable = allBranches.includes('stable');
346
- const hasProduction = allBranches.includes('production') || allBranches.includes('prod');
347
-
348
- if (hasStable || hasProduction) {
349
- return 'multi-branch'; // dev + prod workflow
350
- }
351
-
352
- // Check deployment configs for multi-environment setup (uses cache)
353
- if (existsCached('railway.json')) {
354
- try {
355
- const content = readFileCached('railway.json');
356
- if (content) {
357
- const config = safeJSONParse(content, 'railway.json');
358
- // Validate JSON structure before accessing properties
359
- if (config &&
360
- typeof config === 'object' &&
361
- typeof config.environments === 'object' &&
362
- config.environments !== null &&
363
- Object.keys(config.environments).length > 1) {
364
- return 'multi-branch';
365
- }
366
- }
367
- } catch {}
368
- }
369
-
370
- return 'single-branch'; // main only
371
- } catch {
372
- return 'single-branch';
373
- }
374
- }
375
-
376
- /**
377
- * Detects branch strategy (single-branch vs multi-branch with dev+prod) (async)
378
214
  * @returns {Promise<string>} 'single-branch' or 'multi-branch'
379
215
  */
380
- async function detectBranchStrategyAsync() {
216
+ async function detectBranchStrategy() {
381
217
  try {
382
- // Run git commands in parallel with timeout protection
383
218
  const [localResult, remoteResult] = await Promise.all([
384
- execAsyncWithTimeout('git branch', { encoding: 'utf8' }).catch(() => ({ stdout: '' })),
385
- execAsyncWithTimeout('git branch -r', { encoding: 'utf8' }).catch(() => ({ stdout: '' }))
219
+ execWithTimeout('git branch', { encoding: 'utf8' }).catch(() => ({ stdout: '' })),
220
+ execWithTimeout('git branch -r', { encoding: 'utf8' }).catch(() => ({ stdout: '' }))
386
221
  ]);
387
222
 
388
223
  const allBranches = (localResult.stdout || '') + (remoteResult.stdout || '');
@@ -394,10 +229,9 @@ async function detectBranchStrategyAsync() {
394
229
  return 'multi-branch';
395
230
  }
396
231
 
397
- // Check deployment configs for multi-environment setup (uses cache)
398
- if (await existsCachedAsync('railway.json')) {
232
+ if (await existsCached('railway.json')) {
399
233
  try {
400
- const content = await readFileCachedAsync('railway.json');
234
+ const content = await readFileCached('railway.json');
401
235
  if (content) {
402
236
  const config = safeJSONParse(content, 'railway.json');
403
237
  if (config &&
@@ -419,45 +253,15 @@ async function detectBranchStrategyAsync() {
419
253
 
420
254
  /**
421
255
  * Detects the main branch name
422
- * @deprecated Use detectMainBranchAsync() instead. Will be removed in v3.0.0.
423
- * @returns {string} Main branch name ('main' or 'master')
424
- */
425
- function detectMainBranch() {
426
- warnDeprecation('detectMainBranch', 'detectMainBranchAsync');
427
- try {
428
- const defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
429
- encoding: 'utf8',
430
- stdio: ['pipe', 'pipe', 'ignore']
431
- })
432
- .trim()
433
- .replace('refs/remotes/origin/', '');
434
- return defaultBranch;
435
- } catch {
436
- // Fallback: check common names
437
- try {
438
- execSync('git rev-parse --verify main', {
439
- encoding: 'utf8',
440
- stdio: ['pipe', 'pipe', 'ignore']
441
- });
442
- return 'main';
443
- } catch {
444
- return 'master';
445
- }
446
- }
447
- }
448
-
449
- /**
450
- * Detects the main branch name (async)
451
256
  * @returns {Promise<string>} Main branch name ('main' or 'master')
452
257
  */
453
- async function detectMainBranchAsync() {
258
+ async function detectMainBranch() {
454
259
  try {
455
- const { stdout } = await execAsyncWithTimeout('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' });
260
+ const { stdout } = await execWithTimeout('git symbolic-ref refs/remotes/origin/HEAD', { encoding: 'utf8' });
456
261
  return stdout.trim().replace('refs/remotes/origin/', '');
457
262
  } catch {
458
- // Fallback: check common names
459
263
  try {
460
- await execAsyncWithTimeout('git rev-parse --verify main', { encoding: 'utf8' });
264
+ await execWithTimeout('git rev-parse --verify main', { encoding: 'utf8' });
461
265
  return 'main';
462
266
  } catch {
463
267
  return 'master';
@@ -466,47 +270,12 @@ async function detectMainBranchAsync() {
466
270
  }
467
271
 
468
272
  /**
469
- * Main detection function - aggregates all platform information (sync)
470
- * Uses caching to avoid repeated filesystem/git operations
471
- * @deprecated Use detectAsync() instead. Will be removed in v3.0.0.
472
- * @param {boolean} forceRefresh - Force cache refresh
473
- * @returns {Object} Platform configuration object
474
- */
475
- function detect(forceRefresh = false) {
476
- warnDeprecation('detect', 'detectAsync');
477
-
478
- // Return cached result if still valid
479
- if (!forceRefresh) {
480
- const cached = _detectionCache.get('detection');
481
- if (cached !== undefined) {
482
- return cached;
483
- }
484
- }
485
-
486
- const detection = {
487
- ci: detectCI(),
488
- deployment: detectDeployment(),
489
- projectType: detectProjectType(),
490
- packageManager: detectPackageManager(),
491
- branchStrategy: detectBranchStrategy(),
492
- mainBranch: detectMainBranch(),
493
- hasPlanFile: existsCached('PLAN.md'),
494
- hasTechDebtFile: existsCached('TECHNICAL_DEBT.md'),
495
- timestamp: new Date().toISOString()
496
- };
497
-
498
- _detectionCache.set('detection', detection);
499
- return detection;
500
- }
501
-
502
- /**
503
- * Main detection function - aggregates all platform information (async)
273
+ * Main detection function - aggregates all platform information
504
274
  * Uses Promise.all for parallel execution and caching
505
275
  * @param {boolean} forceRefresh - Force cache refresh
506
276
  * @returns {Promise<Object>} Platform configuration object
507
277
  */
508
- async function detectAsync(forceRefresh = false) {
509
- // Return cached result if still valid
278
+ async function detect(forceRefresh = false) {
510
279
  if (!forceRefresh) {
511
280
  const cached = _detectionCache.get('detection');
512
281
  if (cached !== undefined) {
@@ -514,7 +283,6 @@ async function detectAsync(forceRefresh = false) {
514
283
  }
515
284
  }
516
285
 
517
- // Run all detections in parallel
518
286
  const [
519
287
  ci,
520
288
  deployment,
@@ -525,14 +293,14 @@ async function detectAsync(forceRefresh = false) {
525
293
  hasPlanFile,
526
294
  hasTechDebtFile
527
295
  ] = await Promise.all([
528
- detectCIAsync(),
529
- detectDeploymentAsync(),
530
- detectProjectTypeAsync(),
531
- detectPackageManagerAsync(),
532
- detectBranchStrategyAsync(),
533
- detectMainBranchAsync(),
534
- existsCachedAsync('PLAN.md'),
535
- 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')
536
304
  ]);
537
305
 
538
306
  const detection = {
@@ -561,13 +329,11 @@ function invalidateCache() {
561
329
  _existsCache.clear();
562
330
  }
563
331
 
564
- // When run directly, output JSON (uses async for better performance)
332
+ // When run directly, output JSON
565
333
  if (require.main === module) {
566
334
  (async () => {
567
335
  try {
568
- const result = await detectAsync();
569
- // Optimize: only use pretty-printing when output is to terminal (TTY)
570
- // When piped to another program, use compact JSON for better performance
336
+ const result = await detect();
571
337
  const indent = process.stdout.isTTY ? 2 : 0;
572
338
  console.log(JSON.stringify(result, null, indent));
573
339
  } catch (error) {
@@ -584,20 +350,11 @@ if (require.main === module) {
584
350
  // Export for use as module
585
351
  module.exports = {
586
352
  detect,
587
- detectAsync,
588
353
  invalidateCache,
589
354
  detectCI,
590
- detectCIAsync,
591
355
  detectDeployment,
592
- detectDeploymentAsync,
593
356
  detectProjectType,
594
- detectProjectTypeAsync,
595
357
  detectPackageManager,
596
- detectPackageManagerAsync,
597
358
  detectBranchStrategy,
598
- detectBranchStrategyAsync,
599
- detectMainBranch,
600
- detectMainBranchAsync,
601
- // Testing utilities (prefixed with _ to indicate internal use)
602
- _resetDeprecationWarnings
359
+ detectMainBranch
603
360
  };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Platform-aware state directory detection
3
+ *
4
+ * Determines the appropriate state directory based on the AI coding assistant
5
+ * being used (Claude Code, OpenCode, or Codex CLI).
6
+ *
7
+ * @module lib/platform/state-dir
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ /**
14
+ * Cached state directory name (relative, without leading dot handling)
15
+ * @type {string|null}
16
+ */
17
+ let _cachedStateDir = null;
18
+
19
+ /**
20
+ * Detect which AI coding assistant is running and return appropriate state directory
21
+ *
22
+ * Detection order:
23
+ * 1. AI_STATE_DIR env var (user override)
24
+ * 2. OpenCode detection (OPENCODE_CONFIG env or .opencode/ exists)
25
+ * 3. Codex detection (CODEX_HOME env or .codex/ exists)
26
+ * 4. Default to .claude (Claude Code or unknown)
27
+ *
28
+ * @param {string} [basePath=process.cwd()] - Base path to check for project directories
29
+ * @returns {string} State directory name (e.g., '.claude', '.opencode', '.codex')
30
+ */
31
+ function getStateDir(basePath = process.cwd()) {
32
+ // Check user override first
33
+ if (process.env.AI_STATE_DIR) {
34
+ return process.env.AI_STATE_DIR;
35
+ }
36
+
37
+ // Return cached value if available
38
+ if (_cachedStateDir) {
39
+ return _cachedStateDir;
40
+ }
41
+
42
+ // OpenCode detection
43
+ if (process.env.OPENCODE_CONFIG || process.env.OPENCODE_CONFIG_DIR) {
44
+ _cachedStateDir = '.opencode';
45
+ return _cachedStateDir;
46
+ }
47
+
48
+ // Check for .opencode directory in project
49
+ try {
50
+ const opencodePath = path.join(basePath, '.opencode');
51
+ if (fs.existsSync(opencodePath) && fs.statSync(opencodePath).isDirectory()) {
52
+ _cachedStateDir = '.opencode';
53
+ return _cachedStateDir;
54
+ }
55
+ } catch {
56
+ // Ignore errors, continue detection
57
+ }
58
+
59
+ // Codex detection
60
+ if (process.env.CODEX_HOME) {
61
+ _cachedStateDir = '.codex';
62
+ return _cachedStateDir;
63
+ }
64
+
65
+ // Check for .codex directory in project
66
+ try {
67
+ const codexPath = path.join(basePath, '.codex');
68
+ if (fs.existsSync(codexPath) && fs.statSync(codexPath).isDirectory()) {
69
+ _cachedStateDir = '.codex';
70
+ return _cachedStateDir;
71
+ }
72
+ } catch {
73
+ // Ignore errors, continue detection
74
+ }
75
+
76
+ // Default to Claude Code
77
+ _cachedStateDir = '.claude';
78
+ return _cachedStateDir;
79
+ }
80
+
81
+ /**
82
+ * Get the full path to the state directory
83
+ * @param {string} [basePath=process.cwd()] - Base path
84
+ * @returns {string} Full path to state directory
85
+ */
86
+ function getStateDirPath(basePath = process.cwd()) {
87
+ return path.join(basePath, getStateDir(basePath));
88
+ }
89
+
90
+ /**
91
+ * Get the detected platform name
92
+ * @param {string} [basePath=process.cwd()] - Base path
93
+ * @returns {string} Platform name ('claude', 'opencode', 'codex', or 'custom')
94
+ */
95
+ function getPlatformName(basePath = process.cwd()) {
96
+ const stateDir = getStateDir(basePath);
97
+
98
+ if (process.env.AI_STATE_DIR) {
99
+ return 'custom';
100
+ }
101
+
102
+ switch (stateDir) {
103
+ case '.opencode': return 'opencode';
104
+ case '.codex': return 'codex';
105
+ case '.claude': return 'claude';
106
+ default: return 'unknown';
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Clear the cached state directory (useful for testing)
112
+ */
113
+ function clearCache() {
114
+ _cachedStateDir = null;
115
+ }
116
+
117
+ module.exports = {
118
+ getStateDir,
119
+ getStateDirPath,
120
+ getPlatformName,
121
+ clearCache
122
+ };