fscr 6.2.6 → 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +48 -30
  2. package/dist/index.js +502 -186
  3. package/dist/lib/auth/auth-conf.js +49 -45
  4. package/dist/lib/cache/README.md +341 -0
  5. package/dist/lib/cache/cli.js +152 -0
  6. package/dist/lib/cache/file-watcher.js +193 -0
  7. package/dist/lib/cache/index.js +422 -0
  8. package/dist/lib/cache/monitor.js +224 -0
  9. package/dist/lib/commands/doctor.js +225 -0
  10. package/dist/lib/completions/completion.js +342 -0
  11. package/dist/lib/completions/generator.js +152 -0
  12. package/dist/lib/completions/scripts/bash.sh +108 -0
  13. package/dist/lib/completions/scripts/fish.sh +105 -0
  14. package/dist/lib/completions/scripts/powershell.ps1 +168 -0
  15. package/dist/lib/completions/scripts/zsh.sh +124 -0
  16. package/dist/lib/diagnostics/cache.js +121 -0
  17. package/dist/lib/diagnostics/fileSystem.js +236 -0
  18. package/dist/lib/diagnostics/gitCheck.js +41 -0
  19. package/dist/lib/diagnostics/nodeVersion.js +68 -0
  20. package/dist/lib/diagnostics/packageManager.js +64 -0
  21. package/dist/lib/diagnostics/performance.js +141 -0
  22. package/dist/lib/encryption/decryptConfig.js +3 -2
  23. package/dist/lib/encryption/encryption.js +153 -113
  24. package/dist/lib/generators/generateFScripts.js +16 -13
  25. package/dist/lib/generators/generateToc.js +23 -14
  26. package/dist/lib/generators/index.js +1 -1
  27. package/dist/lib/git/pub.js +27 -17
  28. package/dist/lib/git/taskRunner.js +79 -69
  29. package/dist/lib/git/validateNotDev.js +65 -54
  30. package/dist/lib/optionList.js +69 -57
  31. package/dist/lib/parsers/parseScriptsMd.cached.js +208 -0
  32. package/dist/lib/parsers/parseScriptsMd.js +88 -79
  33. package/dist/lib/parsers/parseScriptsPackage.js +4 -3
  34. package/dist/lib/performance/cache.js +199 -0
  35. package/dist/lib/performance/lazy-loader.js +189 -0
  36. package/dist/lib/performance/monitor.js +303 -0
  37. package/dist/lib/plugins/deployment/index.js +113 -0
  38. package/dist/lib/plugins/hooks.js +17 -0
  39. package/dist/lib/plugins/loader.js +91 -0
  40. package/dist/lib/plugins/task-notifier/index.js +72 -0
  41. package/dist/lib/release/bump.js +50 -45
  42. package/dist/lib/release/commitWithMessage.js +80 -52
  43. package/dist/lib/release/publish.js +19 -14
  44. package/dist/lib/release/pushToGit.js +40 -31
  45. package/dist/lib/release/releasenotes.js +116 -97
  46. package/dist/lib/release/seeChangedFiles.js +68 -60
  47. package/dist/lib/release/sort.js +200 -116
  48. package/dist/lib/release/tree.js +161 -147
  49. package/dist/lib/release/validateNotDev.js +52 -44
  50. package/dist/lib/running/index.js +1 -1
  51. package/dist/lib/running/runCLICommand.js +41 -31
  52. package/dist/lib/running/runParallel.js +61 -59
  53. package/dist/lib/running/runSequence.js +55 -53
  54. package/dist/lib/startScripts.js +129 -114
  55. package/dist/lib/taskList.js +97 -90
  56. package/dist/lib/test-files/.fscripts.md +113 -0
  57. package/dist/lib/test-files/.fscripts.test.md +103 -0
  58. package/dist/lib/test-files/.fscriptsb.md +107 -0
  59. package/dist/lib/test-files/.mdtest.md +40 -0
  60. package/dist/lib/test-files/consoleSample.js +13 -9
  61. package/dist/lib/test-files/inputSample.js +17 -14
  62. package/dist/lib/test-files/testConsole.js +1 -1
  63. package/dist/lib/test-files/testInput.js +1 -1
  64. package/dist/lib/upgradePackages.js +56 -46
  65. package/dist/lib/utils/clear.js +16 -13
  66. package/dist/lib/utils/console.js +27 -21
  67. package/dist/lib/utils/encryption.js +55 -13
  68. package/dist/lib/utils/hash.js +128 -0
  69. package/dist/lib/utils/helpers.js +153 -142
  70. package/dist/lib/utils/index.js +1 -1
  71. package/dist/lib/utils/prompt.js +24 -29
  72. package/package.json +11 -29
@@ -0,0 +1,193 @@
1
+ /**
2
+ * File watcher for automatic cache invalidation
3
+ * @module cache/file-watcher
4
+ */
5
+
6
+ import { watch } from 'fs';
7
+ import { resolve } from 'path';
8
+ import EventEmitter from 'events';
9
+
10
+ /**
11
+ * FileWatcher - Monitors files and triggers cache invalidation
12
+ * @class
13
+ * @extends EventEmitter
14
+ */
15
+ class FileWatcher extends EventEmitter {
16
+ /**
17
+ * Create a file watcher
18
+ * @param {CacheManager} cacheManager - Cache manager instance
19
+ * @param {Object} options - Configuration options
20
+ * @param {number} [options.debounceMs=100] - Debounce time in milliseconds
21
+ */
22
+ constructor(cacheManager, options = {}) {
23
+ super();
24
+
25
+ this.cacheManager = cacheManager;
26
+ this.debounceMs = options.debounceMs || 100;
27
+ this.watchers = new Map();
28
+ this.debounceTimers = new Map();
29
+
30
+ // Statistics
31
+ this.stats = {
32
+ filesWatched: 0,
33
+ changeEvents: 0,
34
+ invalidations: 0
35
+ };
36
+ }
37
+
38
+ /**
39
+ * Watch a file for changes
40
+ * @param {string} filePath - Path to file
41
+ * @returns {void}
42
+ */
43
+ watch(filePath) {
44
+ const absolutePath = resolve(filePath);
45
+
46
+ // Don't watch if already watching
47
+ if (this.watchers.has(absolutePath)) {
48
+ return;
49
+ }
50
+
51
+ try {
52
+ const watcher = watch(absolutePath, (eventType) => {
53
+ this._handleChange(absolutePath, eventType);
54
+ });
55
+
56
+ // unref() so watching a file never keeps a one-shot CLI process alive.
57
+ if (typeof watcher.unref === 'function') {
58
+ watcher.unref();
59
+ }
60
+
61
+ watcher.on('error', (error) => {
62
+ console.warn(`File watcher error for ${absolutePath}:`, error.message);
63
+ this.unwatch(absolutePath);
64
+ });
65
+
66
+ this.watchers.set(absolutePath, watcher);
67
+ this.stats.filesWatched++;
68
+
69
+ this.emit('watch', { filePath: absolutePath });
70
+ } catch (error) {
71
+ console.warn(`Failed to watch file ${absolutePath}:`, error.message);
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Stop watching a file
77
+ * @param {string} filePath - Path to file
78
+ * @returns {boolean} True if file was being watched
79
+ */
80
+ unwatch(filePath) {
81
+ const absolutePath = resolve(filePath);
82
+ const watcher = this.watchers.get(absolutePath);
83
+
84
+ if (watcher) {
85
+ watcher.close();
86
+ this.watchers.delete(absolutePath);
87
+ this.stats.filesWatched--;
88
+ this.emit('unwatch', { filePath: absolutePath });
89
+ return true;
90
+ }
91
+
92
+ return false;
93
+ }
94
+
95
+ /**
96
+ * Handle file change event
97
+ * @private
98
+ * @param {string} filePath - Path to changed file
99
+ * @param {string} eventType - Type of change event
100
+ */
101
+ _handleChange(filePath, eventType) {
102
+ // Clear existing debounce timer
103
+ if (this.debounceTimers.has(filePath)) {
104
+ clearTimeout(this.debounceTimers.get(filePath));
105
+ }
106
+
107
+ // Debounce the invalidation
108
+ const timer = setTimeout(() => {
109
+ this._invalidateFile(filePath, eventType);
110
+ this.debounceTimers.delete(filePath);
111
+ }, this.debounceMs);
112
+ if (typeof timer.unref === 'function') {
113
+ timer.unref();
114
+ }
115
+
116
+ this.debounceTimers.set(filePath, timer);
117
+ }
118
+
119
+ /**
120
+ * Invalidate cache entries for a file
121
+ * @private
122
+ * @param {string} filePath - Path to file
123
+ * @param {string} eventType - Type of change event
124
+ */
125
+ _invalidateFile(filePath, eventType) {
126
+ this.stats.changeEvents++;
127
+
128
+ const invalidated = this.cacheManager.invalidateFile(filePath);
129
+
130
+ if (invalidated > 0) {
131
+ this.stats.invalidations += invalidated;
132
+ this.emit('invalidate', {
133
+ filePath,
134
+ eventType,
135
+ count: invalidated
136
+ });
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Stop watching all files
142
+ * @returns {void}
143
+ */
144
+ unwatchAll() {
145
+ for (const [filePath, watcher] of this.watchers.entries()) {
146
+ watcher.close();
147
+ }
148
+ this.watchers.clear();
149
+ this.stats.filesWatched = 0;
150
+
151
+ // Clear all debounce timers
152
+ for (const timer of this.debounceTimers.values()) {
153
+ clearTimeout(timer);
154
+ }
155
+ this.debounceTimers.clear();
156
+
157
+ this.emit('unwatchAll');
158
+ }
159
+
160
+ /**
161
+ * Get watcher statistics
162
+ * @returns {Object} Statistics object
163
+ */
164
+ getStats() {
165
+ return {
166
+ filesWatched: this.stats.filesWatched,
167
+ changeEvents: this.stats.changeEvents,
168
+ invalidations: this.stats.invalidations,
169
+ watchedFiles: Array.from(this.watchers.keys())
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Check if a file is being watched
175
+ * @param {string} filePath - Path to file
176
+ * @returns {boolean} True if being watched
177
+ */
178
+ isWatching(filePath) {
179
+ const absolutePath = resolve(filePath);
180
+ return this.watchers.has(absolutePath);
181
+ }
182
+
183
+ /**
184
+ * Destroy the file watcher and cleanup resources
185
+ * @returns {void}
186
+ */
187
+ destroy() {
188
+ this.unwatchAll();
189
+ this.removeAllListeners();
190
+ }
191
+ }
192
+
193
+ export default FileWatcher;
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Cache Manager with TTL support
3
+ * Provides in-memory caching with automatic invalidation
4
+ * @module cache
5
+ */
6
+
7
+ import { createHash } from 'crypto';
8
+ import { statSync } from 'fs';
9
+ import EventEmitter from 'events';
10
+
11
+ /**
12
+ * Cache entry structure
13
+ * @typedef {Object} CacheEntry
14
+ * @property {*} data - Cached data
15
+ * @property {number} timestamp - Creation timestamp
16
+ * @property {number} ttl - Time to live in milliseconds
17
+ * @property {string} key - Cache key
18
+ * @property {string} [filePath] - Associated file path for file-based caching
19
+ * @property {number} [mtime] - File modification time
20
+ */
21
+
22
+ /**
23
+ * Cache statistics
24
+ * @typedef {Object} CacheStats
25
+ * @property {number} hits - Cache hit count
26
+ * @property {number} misses - Cache miss count
27
+ * @property {number} invalidations - Cache invalidation count
28
+ * @property {number} size - Current cache size
29
+ * @property {number} hitRate - Cache hit rate percentage
30
+ */
31
+
32
+ /**
33
+ * CacheManager - In-memory cache with TTL and file watching
34
+ * @class
35
+ * @extends EventEmitter
36
+ */
37
+ class CacheManager extends EventEmitter {
38
+ /**
39
+ * Create a cache manager
40
+ * @param {Object} options - Configuration options
41
+ * @param {number} [options.defaultTTL=300000] - Default TTL in milliseconds (5 minutes)
42
+ * @param {number} [options.maxSize=100] - Maximum number of cache entries
43
+ * @param {boolean} [options.enableStats=true] - Enable statistics tracking
44
+ */
45
+ constructor(options = {}) {
46
+ super();
47
+
48
+ this.cache = new Map();
49
+ this.defaultTTL = options.defaultTTL || 300000; // 5 minutes
50
+ this.maxSize = options.maxSize || 100;
51
+ this.enableStats = options.enableStats !== false;
52
+
53
+ // Statistics
54
+ this.stats = {
55
+ hits: 0,
56
+ misses: 0,
57
+ invalidations: 0,
58
+ evictions: 0,
59
+ startTime: Date.now()
60
+ };
61
+
62
+ // Cleanup interval - run every minute.
63
+ // unref() so this background timer never keeps a one-shot CLI process alive.
64
+ this.cleanupInterval = setInterval(() => this._cleanup(), 60000);
65
+ if (this.cleanupInterval && typeof this.cleanupInterval.unref === "function") {
66
+ this.cleanupInterval.unref();
67
+ }
68
+
69
+ // Ensure cleanup on exit
70
+ process.on('exit', () => this.destroy());
71
+ }
72
+
73
+ /**
74
+ * Generate a cache key from file path and modification time
75
+ * @param {string} filePath - File path
76
+ * @returns {string} Cache key
77
+ */
78
+ generateFileKey(filePath) {
79
+ try {
80
+ const stats = statSync(filePath);
81
+ const mtime = stats.mtimeMs;
82
+ return this._hash(`${filePath}:${mtime}`);
83
+ } catch (error) {
84
+ throw new Error(`Failed to generate cache key for ${filePath}: ${error.message}`);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Generate a hash-based cache key
90
+ * @private
91
+ * @param {string} input - Input string
92
+ * @returns {string} Hash
93
+ */
94
+ _hash(input) {
95
+ return createHash('sha256').update(input).digest('hex').substring(0, 16);
96
+ }
97
+
98
+ /**
99
+ * Get value from cache
100
+ * @template T
101
+ * @param {string} key - Cache key
102
+ * @returns {T|null} Cached value or null
103
+ */
104
+ get(key) {
105
+ const entry = this.cache.get(key);
106
+
107
+ if (!entry) {
108
+ this._recordMiss();
109
+ return null;
110
+ }
111
+
112
+ // Check if entry is expired
113
+ if (this._isExpired(entry)) {
114
+ this.invalidate(key);
115
+ this._recordMiss();
116
+ return null;
117
+ }
118
+
119
+ // Check if file-based cache is still valid
120
+ if (entry.filePath) {
121
+ try {
122
+ const stats = statSync(entry.filePath);
123
+ if (stats.mtimeMs !== entry.mtime) {
124
+ this.invalidate(key);
125
+ this._recordMiss();
126
+ return null;
127
+ }
128
+ } catch (error) {
129
+ // File might have been deleted
130
+ this.invalidate(key);
131
+ this._recordMiss();
132
+ return null;
133
+ }
134
+ }
135
+
136
+ this._recordHit();
137
+ this.emit('hit', { key, data: entry.data });
138
+ return entry.data;
139
+ }
140
+
141
+ /**
142
+ * Set value in cache
143
+ * @template T
144
+ * @param {string} key - Cache key
145
+ * @param {T} data - Data to cache
146
+ * @param {Object} [options] - Cache options
147
+ * @param {number} [options.ttl] - Custom TTL in milliseconds
148
+ * @param {string} [options.filePath] - Associated file path
149
+ * @returns {void}
150
+ */
151
+ set(key, data, options = {}) {
152
+ // Evict oldest entry if cache is full
153
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
154
+ this._evictOldest();
155
+ }
156
+
157
+ const ttl = options.ttl !== undefined ? options.ttl : this.defaultTTL;
158
+ const entry = {
159
+ data,
160
+ timestamp: Date.now(),
161
+ ttl,
162
+ key
163
+ };
164
+
165
+ // Add file tracking if provided
166
+ if (options.filePath) {
167
+ try {
168
+ const stats = statSync(options.filePath);
169
+ entry.filePath = options.filePath;
170
+ entry.mtime = stats.mtimeMs;
171
+ } catch (error) {
172
+ console.warn(`Warning: Could not stat file ${options.filePath}`);
173
+ }
174
+ }
175
+
176
+ this.cache.set(key, entry);
177
+ this.emit('set', { key, data, ttl });
178
+ }
179
+
180
+ /**
181
+ * Invalidate a cache entry
182
+ * @param {string} key - Cache key
183
+ * @returns {boolean} True if entry was deleted
184
+ */
185
+ invalidate(key) {
186
+ const deleted = this.cache.delete(key);
187
+ if (deleted) {
188
+ this.stats.invalidations++;
189
+ this.emit('invalidate', { key });
190
+ }
191
+ return deleted;
192
+ }
193
+
194
+ /**
195
+ * Invalidate all entries for a specific file path
196
+ * @param {string} filePath - File path
197
+ * @returns {number} Number of invalidated entries
198
+ */
199
+ invalidateFile(filePath) {
200
+ let count = 0;
201
+ for (const [key, entry] of this.cache.entries()) {
202
+ if (entry.filePath === filePath) {
203
+ this.invalidate(key);
204
+ count++;
205
+ }
206
+ }
207
+ return count;
208
+ }
209
+
210
+ /**
211
+ * Clear all cache entries
212
+ * @returns {void}
213
+ */
214
+ clear() {
215
+ const size = this.cache.size;
216
+ this.cache.clear();
217
+ this.stats.invalidations += size;
218
+ this.emit('clear');
219
+ }
220
+
221
+ /**
222
+ * Check if a cache entry is valid
223
+ * @param {string} key - Cache key
224
+ * @returns {boolean} True if valid
225
+ */
226
+ isValid(key) {
227
+ const entry = this.cache.get(key);
228
+ if (!entry) return false;
229
+
230
+ if (this._isExpired(entry)) return false;
231
+
232
+ // Check file modification time if applicable
233
+ if (entry.filePath) {
234
+ try {
235
+ const stats = statSync(entry.filePath);
236
+ return stats.mtimeMs === entry.mtime;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+
242
+ return true;
243
+ }
244
+
245
+ /**
246
+ * Check if an entry is expired
247
+ * @private
248
+ * @param {CacheEntry} entry - Cache entry
249
+ * @returns {boolean} True if expired
250
+ */
251
+ _isExpired(entry) {
252
+ if (entry.ttl === 0) return false; // TTL of 0 means never expire
253
+ return Date.now() - entry.timestamp > entry.ttl;
254
+ }
255
+
256
+ /**
257
+ * Get cache statistics
258
+ * @returns {CacheStats} Statistics object
259
+ */
260
+ getStats() {
261
+ const total = this.stats.hits + this.stats.misses;
262
+ const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
263
+ const uptime = Date.now() - this.stats.startTime;
264
+
265
+ return {
266
+ hits: this.stats.hits,
267
+ misses: this.stats.misses,
268
+ invalidations: this.stats.invalidations,
269
+ evictions: this.stats.evictions,
270
+ size: this.cache.size,
271
+ maxSize: this.maxSize,
272
+ hitRate: hitRate.toFixed(2),
273
+ uptime,
274
+ totalRequests: total
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Reset statistics
280
+ * @returns {void}
281
+ */
282
+ resetStats() {
283
+ this.stats = {
284
+ hits: 0,
285
+ misses: 0,
286
+ invalidations: 0,
287
+ evictions: 0,
288
+ startTime: Date.now()
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Cleanup expired entries
294
+ * @private
295
+ * @returns {number} Number of cleaned entries
296
+ */
297
+ _cleanup() {
298
+ let cleaned = 0;
299
+ for (const [key, entry] of this.cache.entries()) {
300
+ if (this._isExpired(entry)) {
301
+ this.cache.delete(key);
302
+ cleaned++;
303
+ }
304
+ }
305
+
306
+ if (cleaned > 0) {
307
+ this.emit('cleanup', { count: cleaned });
308
+ }
309
+
310
+ return cleaned;
311
+ }
312
+
313
+ /**
314
+ * Evict the oldest cache entry
315
+ * @private
316
+ * @returns {void}
317
+ */
318
+ _evictOldest() {
319
+ let oldestKey = null;
320
+ let oldestTime = Infinity;
321
+
322
+ for (const [key, entry] of this.cache.entries()) {
323
+ if (entry.timestamp < oldestTime) {
324
+ oldestTime = entry.timestamp;
325
+ oldestKey = key;
326
+ }
327
+ }
328
+
329
+ if (oldestKey) {
330
+ this.cache.delete(oldestKey);
331
+ this.stats.evictions++;
332
+ this.emit('evict', { key: oldestKey });
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Record a cache hit
338
+ * @private
339
+ */
340
+ _recordHit() {
341
+ if (this.enableStats) {
342
+ this.stats.hits++;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Record a cache miss
348
+ * @private
349
+ */
350
+ _recordMiss() {
351
+ if (this.enableStats) {
352
+ this.stats.misses++;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Destroy the cache manager and cleanup resources
358
+ * @returns {void}
359
+ */
360
+ destroy() {
361
+ if (this.cleanupInterval) {
362
+ clearInterval(this.cleanupInterval);
363
+ this.cleanupInterval = null;
364
+ }
365
+ this.clear();
366
+ this.removeAllListeners();
367
+ }
368
+
369
+ /**
370
+ * Get all cache keys
371
+ * @returns {string[]} Array of cache keys
372
+ */
373
+ keys() {
374
+ return Array.from(this.cache.keys());
375
+ }
376
+
377
+ /**
378
+ * Get cache size
379
+ * @returns {number} Number of entries
380
+ */
381
+ size() {
382
+ return this.cache.size;
383
+ }
384
+
385
+ /**
386
+ * Check if cache has a key
387
+ * @param {string} key - Cache key
388
+ * @returns {boolean} True if key exists
389
+ */
390
+ has(key) {
391
+ return this.cache.has(key) && this.isValid(key);
392
+ }
393
+ }
394
+
395
+ // Singleton instance
396
+ let globalCache = null;
397
+
398
+ /**
399
+ * Get the global cache instance
400
+ * @param {Object} [options] - Configuration options
401
+ * @returns {CacheManager} Cache manager instance
402
+ */
403
+ export function getCache(options) {
404
+ if (!globalCache) {
405
+ globalCache = new CacheManager(options);
406
+ }
407
+ return globalCache;
408
+ }
409
+
410
+ /**
411
+ * Reset the global cache instance
412
+ * @returns {void}
413
+ */
414
+ export function resetCache() {
415
+ if (globalCache) {
416
+ globalCache.destroy();
417
+ globalCache = null;
418
+ }
419
+ }
420
+
421
+ export { CacheManager };
422
+ export default CacheManager;