z-clean 0.1.1 → 0.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.
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ const { hasScanErrors } = require('./scanner');
4
+ const {
5
+ buildCandidateReview,
6
+ calculateScore,
7
+ groupCandidateSourcesByPattern,
8
+ groupCandidatesByPattern,
9
+ pickCandidate,
10
+ riskLevel,
11
+ sortCandidatesByRisk,
12
+ } = require('./audit-candidates');
13
+ const { buildHistory } = require('./audit-history');
14
+ const { buildNextActions, buildRecommendations } = require('./audit-actions');
15
+
16
+ function buildAuditReport(...args) {
17
+ const input = normalizeBuildInput(args);
18
+ const config = input.config || {};
19
+ const zombies = Array.isArray(input.zombies) ? input.zombies : [];
20
+ const logs = Array.isArray(input.logs) ? input.logs : [];
21
+ const stats = input.stats || {};
22
+ const generatedAt = normalizeTime(input.now);
23
+ const commandName = input.commandName || 'audit';
24
+ const warnings = Array.isArray(input.zombies?.warnings) ? input.zombies.warnings : [];
25
+ const errors = Array.isArray(input.zombies?.errors) ? input.zombies.errors : [];
26
+ const enumerationComplete = !hasScanErrors(input.zombies || []);
27
+ const reclaimableBytes = zombies.reduce((sum, item) => sum + (item.mem || 0), 0);
28
+ const candidates = zombies.map((item) => buildCandidateReview(item));
29
+ const topCandidates = sortCandidatesByRisk(candidates);
30
+ const history = buildHistory(logs, stats);
31
+ const status = errors.length > 0 ? 'blocked' : zombies.length > 0 ? 'attention' : 'clean';
32
+ const recommendationInput = { zombieCount: zombies.length, errors, warnings, commandName };
33
+
34
+ return {
35
+ schemaVersion: 1,
36
+ generatedAt,
37
+ kind: 'ai-coding-runtime-hygiene',
38
+ notGeneralCleaner: true,
39
+ profile: buildProfile(),
40
+ differentiation: 'zclean audits AI coding runtime leftovers and safe workspace cache candidates; it does not uninstall apps, draw disk maps, or sweep the whole system.',
41
+ summary: {
42
+ status,
43
+ zombieCount: zombies.length,
44
+ reclaimableBytes,
45
+ warningCount: warnings.length,
46
+ errorCount: errors.length,
47
+ },
48
+ risk: {
49
+ score: calculateScore(candidates, warnings, errors),
50
+ level: status === 'blocked' ? 'unknown' : riskLevel(candidates.length, warnings.length),
51
+ enumerationComplete,
52
+ },
53
+ candidates: {
54
+ count: zombies.length,
55
+ memoryReclaimable: reclaimableBytes,
56
+ byPattern: groupCandidatesByPattern(candidates),
57
+ largestCandidate: pickCandidate(candidates, 'memoryBytes'),
58
+ oldestCandidate: pickCandidate(candidates, 'ageMs'),
59
+ },
60
+ candidateSources: {
61
+ byPattern: groupCandidateSourcesByPattern(candidates),
62
+ },
63
+ safety: buildSafety(config),
64
+ proGradeReview: {
65
+ safeToClean: enumerationComplete && zombies.length > 0,
66
+ candidates,
67
+ topCandidates,
68
+ guardrails: buildGuardrails(config),
69
+ },
70
+ diagnostics: {
71
+ warnings,
72
+ errors,
73
+ },
74
+ history,
75
+ nextActions: buildNextActions(recommendationInput),
76
+ recommendations: buildRecommendations(recommendationInput),
77
+ };
78
+ }
79
+
80
+ function normalizeBuildInput(args) {
81
+ if (args.length === 1 && args[0] && typeof args[0] === 'object' && Object.prototype.hasOwnProperty.call(args[0], 'zombies')) {
82
+ return args[0];
83
+ }
84
+
85
+ const [config = {}, zombies = [], logs = [], stats = {}, options = {}] = args;
86
+ return {
87
+ config,
88
+ zombies,
89
+ logs,
90
+ stats,
91
+ now: options.now,
92
+ commandName: options.commandName,
93
+ };
94
+ }
95
+
96
+ function normalizeTime(value) {
97
+ if (value instanceof Date) return value.toISOString();
98
+ if (typeof value === 'string') return value;
99
+ return new Date().toISOString();
100
+ }
101
+
102
+ function buildProfile() {
103
+ return {
104
+ tool: 'zclean',
105
+ focus: 'ai-coding-runtime-hygiene',
106
+ localOnly: true,
107
+ telemetry: false,
108
+ positioning: 'zclean is not a general Mac cleaner; it reviews AI coding runtime leftovers, safe workspace caches, and only cleans when explicitly asked.',
109
+ };
110
+ }
111
+
112
+ function buildSafety(config) {
113
+ return {
114
+ cleanupRequiresYes: true,
115
+ whitelistCount: Array.isArray(config.whitelist) ? config.whitelist.length : 0,
116
+ customAiDirsCount: Array.isArray(config.customAiDirs) ? config.customAiDirs.length : 0,
117
+ dryRunDefault: config.dryRunDefault !== false,
118
+ };
119
+ }
120
+
121
+ function buildGuardrails(config) {
122
+ return {
123
+ dryRunDefault: config.dryRunDefault !== false,
124
+ cleanupRequiresYes: true,
125
+ maxKillBatch: config.maxKillBatch || null,
126
+ whitelistCount: Array.isArray(config.whitelist) ? config.whitelist.length : 0,
127
+ customAiDirCount: Array.isArray(config.customAiDirs) ? config.customAiDirs.length : 0,
128
+ localOnly: true,
129
+ telemetry: false,
130
+ };
131
+ }
132
+
133
+ module.exports = {
134
+ buildAuditReport,
135
+ };
package/src/audit.js ADDED
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ const { scan } = require('./scanner');
4
+ const { readLogs, getCumulativeStats } = require('./config');
5
+ const { buildAuditReport } = require('./audit-report');
6
+ const { reportAudit } = require('./audit-printer');
7
+
8
+ function runAudit(config, options = {}) {
9
+ const scanOptions = options.sessionPid ? { sessionPid: options.sessionPid } : {};
10
+ const zombies = options.scanResult || scan(config, scanOptions);
11
+ const logs = options.logs || readLogs(100);
12
+ const stats = options.stats || getCumulativeStats() || {};
13
+ const report = buildAuditReport({
14
+ config,
15
+ zombies,
16
+ logs,
17
+ stats,
18
+ now: options.now,
19
+ commandName: options.commandName,
20
+ });
21
+
22
+ if (options.json) {
23
+ console.log(JSON.stringify(report, null, 2));
24
+ } else {
25
+ reportAudit(report, { commandName: options.commandName });
26
+ }
27
+
28
+ return report;
29
+ }
30
+
31
+ module.exports = {
32
+ runAudit,
33
+ buildAuditReport,
34
+ reportAudit,
35
+ };
package/src/cache.js ADDED
@@ -0,0 +1,256 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { appendLog } = require('./config');
6
+ const { c, bold, formatBytes } = require('./reporter');
7
+
8
+ const EXACT_CACHE_PATHS = new Set([
9
+ '.next/cache',
10
+ '.nuxt',
11
+ '.turbo',
12
+ '.vite',
13
+ '.parcel-cache',
14
+ '.pytest_cache',
15
+ '.ruff_cache',
16
+ '.mypy_cache',
17
+ 'node_modules/.cache',
18
+ ]);
19
+
20
+ const CACHE_DIR_NAMES = new Set(['__pycache__']);
21
+ const CACHE_DIR_NAMES_ANYWHERE = new Set([
22
+ '.nuxt',
23
+ '.turbo',
24
+ '.vite',
25
+ '.parcel-cache',
26
+ '.pytest_cache',
27
+ '.ruff_cache',
28
+ '.mypy_cache',
29
+ ]);
30
+ const SKIP_DIR_NAMES = new Set(['.git', '.hg', '.svn', '.omo', '.zclean']);
31
+ const MAX_SCAN_DEPTH = 5;
32
+
33
+ function runCache(options = {}) {
34
+ const root = path.resolve(options.root || process.cwd());
35
+ const yes = Boolean(options.yes);
36
+ const report = buildCacheReport({ root, yes });
37
+
38
+ if (yes && report._privateCandidates.length > 0) {
39
+ const result = cleanCacheTargets(report._privateCandidates);
40
+ report.summary.deleted = result.deleted.length;
41
+ report.summary.failed = result.failed.length;
42
+ report.deleted = result.deleted.map(toPublicCandidate);
43
+ report.failed = result.failed.map((item) => ({
44
+ ...toPublicCandidate(item),
45
+ error: item.error,
46
+ }));
47
+ appendLog({
48
+ action: 'cache-cleanup-summary',
49
+ deleted: report.summary.deleted,
50
+ failed: report.summary.failed,
51
+ totalBytes: report.summary.totalBytes,
52
+ });
53
+ } else if (!yes && report.candidates.length > 0) {
54
+ appendLog({ action: 'cache-dry-run', found: report.candidates.length, totalBytes: report.summary.totalBytes });
55
+ }
56
+
57
+ if (options.json) {
58
+ writeJson(report);
59
+ } else {
60
+ reportCacheText(report);
61
+ }
62
+
63
+ return report;
64
+ }
65
+
66
+ function buildCacheReport(options = {}) {
67
+ const root = path.resolve(options.root || process.cwd());
68
+ const candidates = scanCacheTargets(root);
69
+ const totalBytes = candidates.reduce((sum, item) => sum + item.bytes, 0);
70
+
71
+ return {
72
+ schemaVersion: 1,
73
+ generatedAt: new Date().toISOString(),
74
+ kind: 'workspace-cache-hygiene',
75
+ dryRun: !options.yes,
76
+ workspace: path.basename(root) || '.',
77
+ scope: 'safe workspace cache paths only; no app uninstall or whole-disk sweep',
78
+ candidates: candidates.map(toPublicCandidate),
79
+ summary: {
80
+ count: candidates.length,
81
+ totalBytes,
82
+ deleted: 0,
83
+ failed: 0,
84
+ },
85
+ _privateCandidates: candidates,
86
+ };
87
+ }
88
+
89
+ function scanCacheTargets(root) {
90
+ if (!isDirectory(root)) return [];
91
+ const found = [];
92
+ walk(root, root, 0, found);
93
+ return found.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
94
+ }
95
+
96
+ function walk(root, current, depth, found) {
97
+ if (depth > MAX_SCAN_DEPTH) return;
98
+ let entries;
99
+ try {
100
+ entries = fs.readdirSync(current, { withFileTypes: true });
101
+ } catch {
102
+ return;
103
+ }
104
+
105
+ for (const entry of entries) {
106
+ if (!entry.isDirectory()) continue;
107
+ if (entry.isSymbolicLink && entry.isSymbolicLink()) continue;
108
+
109
+ const absolutePath = path.join(current, entry.name);
110
+ const relativePath = toPosix(path.relative(root, absolutePath));
111
+ const isNodeModules = entry.name === 'node_modules';
112
+
113
+ if (isCachePath(relativePath, entry.name)) {
114
+ found.push({
115
+ id: cacheId(relativePath, entry.name),
116
+ absolutePath,
117
+ relativePath,
118
+ type: 'directory',
119
+ bytes: directorySize(absolutePath),
120
+ });
121
+ continue;
122
+ }
123
+
124
+ if (SKIP_DIR_NAMES.has(entry.name)) continue;
125
+ if (isNodeModules) {
126
+ const nodeCache = path.join(absolutePath, '.cache');
127
+ if (isDirectory(nodeCache)) {
128
+ found.push({
129
+ id: 'node_modules-cache',
130
+ absolutePath: nodeCache,
131
+ relativePath: toPosix(path.relative(root, nodeCache)),
132
+ type: 'directory',
133
+ bytes: directorySize(nodeCache),
134
+ });
135
+ }
136
+ continue;
137
+ }
138
+
139
+ walk(root, absolutePath, depth + 1, found);
140
+ }
141
+ }
142
+
143
+ function cleanCacheTargets(candidates) {
144
+ const deleted = [];
145
+ const failed = [];
146
+ for (const candidate of [...candidates].sort((a, b) => b.relativePath.length - a.relativePath.length)) {
147
+ try {
148
+ fs.rmSync(candidate.absolutePath, { recursive: true, force: true });
149
+ deleted.push(candidate);
150
+ } catch (err) {
151
+ failed.push({ ...candidate, error: err.message });
152
+ }
153
+ }
154
+ return { deleted, failed };
155
+ }
156
+
157
+ function isCachePath(relativePath, name) {
158
+ return EXACT_CACHE_PATHS.has(relativePath)
159
+ || relativePath.endsWith('/.next/cache')
160
+ || relativePath.endsWith('/node_modules/.cache')
161
+ || CACHE_DIR_NAMES.has(name)
162
+ || CACHE_DIR_NAMES_ANYWHERE.has(name);
163
+ }
164
+
165
+ function cacheId(relativePath, name) {
166
+ if (relativePath === 'node_modules/.cache') return 'node_modules-cache';
167
+ if (name === '__pycache__') return 'python-bytecode-cache';
168
+ return relativePath.replace(/[/.]+/g, '-').replace(/^-|-$/g, '') || 'cache';
169
+ }
170
+
171
+ function directorySize(dir) {
172
+ let total = 0;
173
+ let entries;
174
+ try {
175
+ entries = fs.readdirSync(dir, { withFileTypes: true });
176
+ } catch {
177
+ return 0;
178
+ }
179
+ for (const entry of entries) {
180
+ const file = path.join(dir, entry.name);
181
+ try {
182
+ const stat = fs.lstatSync(file);
183
+ if (stat.isSymbolicLink()) continue;
184
+ if (stat.isDirectory()) total += directorySize(file);
185
+ else if (stat.isFile()) total += stat.size;
186
+ } catch {}
187
+ }
188
+ return total;
189
+ }
190
+
191
+ function isDirectory(value) {
192
+ try {
193
+ return fs.statSync(value).isDirectory();
194
+ } catch {
195
+ return false;
196
+ }
197
+ }
198
+
199
+ function toPublicCandidate(candidate) {
200
+ return {
201
+ id: candidate.id,
202
+ relativePath: candidate.relativePath,
203
+ type: candidate.type,
204
+ bytes: candidate.bytes,
205
+ };
206
+ }
207
+
208
+ function writeJson(report) {
209
+ const publicReport = { ...report };
210
+ delete publicReport._privateCandidates;
211
+ console.log(JSON.stringify(publicReport, null, 2));
212
+ }
213
+
214
+ function reportCacheText(report) {
215
+ console.log(bold('\n zclean cache') + c('gray', ' - workspace cache hygiene\n'));
216
+ console.log(` Workspace: ${report.workspace}`);
217
+ console.log(` Mode: ${report.dryRun ? 'dry-run' : 'clean'}`);
218
+ console.log(` Candidates: ${report.summary.count}`);
219
+ console.log(` Reclaimable: ${formatBytes(report.summary.totalBytes)}`);
220
+ console.log();
221
+
222
+ if (report.candidates.length === 0) {
223
+ console.log(c('green', ' No supported workspace cache paths found.\n'));
224
+ return;
225
+ }
226
+
227
+ for (const item of report.candidates.slice(0, 20)) {
228
+ console.log(` ${c('cyan', item.relativePath.padEnd(28))} ${formatBytes(item.bytes).padStart(10)}`);
229
+ }
230
+ if (report.candidates.length > 20) {
231
+ console.log(c('gray', ` ... ${report.candidates.length - 20} more`));
232
+ }
233
+ console.log();
234
+
235
+ if (report.dryRun) {
236
+ console.log(c('gray', ' Run zclean cache --yes to remove these cache directories.\n'));
237
+ return;
238
+ }
239
+
240
+ if (report.summary.failed > 0) {
241
+ console.log(c('yellow', ` Removed ${report.summary.deleted}; failed ${report.summary.failed}.\n`));
242
+ } else {
243
+ console.log(c('green', ` Removed ${report.summary.deleted} cache director${report.summary.deleted === 1 ? 'y' : 'ies'}.\n`));
244
+ }
245
+ }
246
+
247
+ function toPosix(value) {
248
+ return value.split(path.sep).join('/');
249
+ }
250
+
251
+ module.exports = {
252
+ buildCacheReport,
253
+ cleanCacheTargets,
254
+ runCache,
255
+ scanCacheTargets,
256
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { c } = require('../reporter');
4
+
5
+ function installScheduler(platform) {
6
+ switch (platform) {
7
+ case 'darwin': {
8
+ const { installLaunchd } = require('../installer/launchd');
9
+ const result = installLaunchd();
10
+ const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
11
+ console.log(`${icon} ${result.message}`);
12
+ break;
13
+ }
14
+ case 'linux': {
15
+ const { installSystemd } = require('../installer/systemd');
16
+ const result = installSystemd();
17
+ const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
18
+ console.log(`${icon} ${result.message}`);
19
+ break;
20
+ }
21
+ case 'win32': {
22
+ const { installTaskScheduler } = require('../installer/taskscheduler');
23
+ const result = installTaskScheduler();
24
+ const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
25
+ console.log(`${icon} ${result.message}`);
26
+ break;
27
+ }
28
+ default:
29
+ console.log(c('yellow', ` Scheduler: Unsupported platform (${platform}). Install a cron job manually.`));
30
+ }
31
+ }
32
+
33
+ function uninstallScheduler(platform) {
34
+ switch (platform) {
35
+ case 'darwin': {
36
+ const { removeLaunchd } = require('../installer/launchd');
37
+ const result = removeLaunchd();
38
+ console.log(` Scheduler: ${result.message}`);
39
+ break;
40
+ }
41
+ case 'linux': {
42
+ const { removeSystemd } = require('../installer/systemd');
43
+ const result = removeSystemd();
44
+ console.log(` Scheduler: ${result.message}`);
45
+ break;
46
+ }
47
+ case 'win32': {
48
+ const { removeTaskScheduler } = require('../installer/taskscheduler');
49
+ const result = removeTaskScheduler();
50
+ console.log(` Scheduler: ${result.message}`);
51
+ break;
52
+ }
53
+ default:
54
+ console.log(c('yellow', ` Scheduler: Remove manually for ${platform}.`));
55
+ }
56
+ }
57
+
58
+ module.exports = { installScheduler, uninstallScheduler };
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const {
5
+ saveConfig,
6
+ readLogs,
7
+ getCumulativeStats,
8
+ getConfigFile,
9
+ getLogFile,
10
+ summarizeHistory,
11
+ } = require('../config');
12
+ const { reportLogs, c, bold } = require('../reporter');
13
+
14
+ const SAFE_HISTORY_ENTRY_KEYS = new Set([
15
+ 'action',
16
+ 'errors',
17
+ 'failed',
18
+ 'found',
19
+ 'killed',
20
+ 'skipped',
21
+ 'timestamp',
22
+ 'totalMemFreed',
23
+ ]);
24
+
25
+ function runHistory(flags) {
26
+ const logs = readLogs(50);
27
+ if (flags.json) {
28
+ console.log(JSON.stringify({
29
+ schemaVersion: 1,
30
+ generatedAt: new Date().toISOString(),
31
+ entries: logs.map(sanitizeHistoryEntry),
32
+ summary: summarizeHistory(logs, getCumulativeStats()),
33
+ }, null, 2));
34
+ return;
35
+ }
36
+ reportLogs(logs);
37
+ }
38
+
39
+ function runProtect(config, flags, positional) {
40
+ const subcommand = positional[1] || 'list';
41
+
42
+ switch (subcommand) {
43
+ case 'list':
44
+ return protectList(config, flags);
45
+ case 'add':
46
+ return protectAdd(config, flags, positional);
47
+ case 'remove':
48
+ return protectRemove(config, flags, positional);
49
+ default:
50
+ console.error(c('red', ` Unknown protect command: ${subcommand}`));
51
+ console.error(c('gray', ' Usage: zclean protect list|add|remove'));
52
+ process.exit(1);
53
+ }
54
+ }
55
+
56
+ function protectList(config, flags) {
57
+ const whitelist = getWhitelist(config);
58
+ if (flags.json) {
59
+ console.log(JSON.stringify({
60
+ schemaVersion: 1,
61
+ generatedAt: new Date().toISOString(),
62
+ whitelist,
63
+ }, null, 2));
64
+ return;
65
+ }
66
+
67
+ console.log(bold('\n Protected entries\n'));
68
+ if (whitelist.length === 0) {
69
+ console.log(c('gray', ' No protected entries configured.\n'));
70
+ return;
71
+ }
72
+ whitelist.forEach((entry, index) => {
73
+ console.log(` ${String(index + 1).padStart(2)}. ${entry}`);
74
+ });
75
+ console.log();
76
+ }
77
+
78
+ function protectAdd(config, flags, positional) {
79
+ const entry = normalizeProtectEntry(positional.slice(2).join(' '));
80
+ if (!entry) failProtect('Protection entry is required and cannot be empty.');
81
+
82
+ const writableConfig = readWritableConfig(config);
83
+ const whitelist = getWhitelist(writableConfig);
84
+ if (whitelist.includes(entry)) {
85
+ failProtect(`Entry is already protected: ${entry}`);
86
+ }
87
+
88
+ saveConfig({ ...writableConfig, whitelist: [...whitelist, entry] });
89
+ console.log(c('green', ` Protected: ${entry}`));
90
+ if (flags.reason) {
91
+ console.log(c('gray', ' Note: --reason is accepted for workflow notes but is not stored in config yet.'));
92
+ }
93
+ }
94
+
95
+ function protectRemove(config, flags, positional) {
96
+ const writableConfig = readWritableConfig(config);
97
+ const whitelist = getWhitelist(writableConfig);
98
+ const indexValue = flags.index;
99
+ let index = -1;
100
+ let entry = null;
101
+
102
+ if (indexValue !== undefined) {
103
+ index = parseProtectIndex(indexValue, whitelist.length);
104
+ entry = whitelist[index];
105
+ } else {
106
+ entry = normalizeProtectEntry(positional.slice(2).join(' '));
107
+ if (!entry) failProtect('Protection entry or --index=N is required.');
108
+ index = whitelist.indexOf(entry);
109
+ if (index === -1) failProtect(`Entry is not protected: ${entry}`);
110
+ }
111
+
112
+ saveConfig({ ...writableConfig, whitelist: whitelist.filter((_, current) => current !== index) });
113
+ console.log(c('green', ` Removed protected entry: ${entry}`));
114
+ }
115
+
116
+ function sanitizeHistoryEntry(entry) {
117
+ const safe = {};
118
+ for (const key of SAFE_HISTORY_ENTRY_KEYS) {
119
+ if (Object.prototype.hasOwnProperty.call(entry, key)) safe[key] = entry[key];
120
+ }
121
+ return safe;
122
+ }
123
+
124
+ function readWritableConfig(fallback) {
125
+ const configFile = getConfigFile();
126
+ if (!fs.existsSync(configFile)) return fallback;
127
+ try {
128
+ const parsed = JSON.parse(fs.readFileSync(configFile, 'utf-8'));
129
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
130
+ } catch {}
131
+ return fallback;
132
+ }
133
+
134
+ function getWhitelist(config) {
135
+ return Array.isArray(config.whitelist)
136
+ ? config.whitelist.filter((entry) => typeof entry === 'string')
137
+ : [];
138
+ }
139
+
140
+ function normalizeProtectEntry(value) {
141
+ return String(value || '').trim();
142
+ }
143
+
144
+ function parseProtectIndex(value, length) {
145
+ if (value === true || value === '') failProtect('--index must be a positive integer.');
146
+ const index = Number(value);
147
+ if (!Number.isInteger(index) || index < 1) failProtect('--index must be a positive integer.');
148
+ if (index > length) failProtect(`Protection index out of range: ${index}`);
149
+ return index - 1;
150
+ }
151
+
152
+ function failProtect(message) {
153
+ console.error(c('red', ` ${message}`));
154
+ process.exit(1);
155
+ }
156
+
157
+ module.exports = { runHistory, runProtect };