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.
- package/LICENSE +1 -1
- package/README.ko.md +118 -0
- package/README.md +241 -97
- package/README.zh.md +118 -0
- package/assets/zclean-hero.png +0 -0
- package/bin/zclean.js +95 -59
- package/package.json +45 -8
- package/src/audit-actions.js +88 -0
- package/src/audit-candidates.js +86 -0
- package/src/audit-history.js +69 -0
- package/src/audit-printer.js +83 -0
- package/src/audit-report.js +135 -0
- package/src/audit.js +35 -0
- package/src/cache.js +256 -0
- package/src/commands/scheduler.js +58 -0
- package/src/commands/trust.js +157 -0
- package/src/config.js +125 -20
- package/src/detector/orphan.js +21 -131
- package/src/detector/patterns.js +97 -14
- package/src/detector/whitelist.js +26 -105
- package/src/doctor/checks.js +229 -0
- package/src/doctor/render.js +37 -0
- package/src/doctor.js +22 -0
- package/src/installer/bin-path.js +106 -0
- package/src/installer/hook.js +15 -4
- package/src/installer/launchd.js +57 -28
- package/src/installer/systemd.js +18 -29
- package/src/installer/taskscheduler.js +29 -36
- package/src/killer.js +68 -52
- package/src/process-tree.js +338 -0
- package/src/reporter.js +61 -2
- package/src/scanner.js +97 -241
- package/src/windows-processes.js +257 -0
package/bin/zclean.js
CHANGED
|
@@ -2,11 +2,25 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const os = require('os');
|
|
5
|
-
const { scan } = require('../src/scanner');
|
|
5
|
+
const { scan, hasScanErrors } = require('../src/scanner');
|
|
6
6
|
const { killZombies } = require('../src/killer');
|
|
7
|
-
const {
|
|
7
|
+
const {
|
|
8
|
+
loadConfig,
|
|
9
|
+
saveConfig,
|
|
10
|
+
readLogs,
|
|
11
|
+
pruneLogs,
|
|
12
|
+
DEFAULT_CONFIG,
|
|
13
|
+
appendLog,
|
|
14
|
+
getCumulativeStats,
|
|
15
|
+
getConfigFile,
|
|
16
|
+
} = require('../src/config');
|
|
8
17
|
const { reportDryRun, reportKill, reportStatus, reportLogs, reportConfig, c, bold } = require('../src/reporter');
|
|
9
18
|
const { installHook, removeHook } = require('../src/installer/hook');
|
|
19
|
+
const { runHistory, runProtect } = require('../src/commands/trust');
|
|
20
|
+
const { installScheduler, uninstallScheduler } = require('../src/commands/scheduler');
|
|
21
|
+
const { runDoctor } = require('../src/doctor');
|
|
22
|
+
const { runAudit } = require('../src/audit');
|
|
23
|
+
const { runCache } = require('../src/cache');
|
|
10
24
|
|
|
11
25
|
// Platform-specific installers (lazy loaded)
|
|
12
26
|
const platform = os.platform();
|
|
@@ -58,12 +72,30 @@ async function main() {
|
|
|
58
72
|
case 'logs':
|
|
59
73
|
return cmdLogs(config);
|
|
60
74
|
|
|
75
|
+
case 'history':
|
|
76
|
+
return runHistory(flags);
|
|
77
|
+
|
|
78
|
+
case 'protect':
|
|
79
|
+
return runProtect(config, flags, positional);
|
|
80
|
+
|
|
61
81
|
case 'uninstall':
|
|
62
82
|
return cmdUninstall();
|
|
63
83
|
|
|
64
84
|
case 'config':
|
|
65
85
|
return cmdConfig(config);
|
|
66
86
|
|
|
87
|
+
case 'doctor':
|
|
88
|
+
return cmdDoctor(config);
|
|
89
|
+
|
|
90
|
+
case 'report':
|
|
91
|
+
return cmdAudit(config, 'report');
|
|
92
|
+
|
|
93
|
+
case 'audit':
|
|
94
|
+
return cmdAudit(config, 'audit');
|
|
95
|
+
|
|
96
|
+
case 'cache':
|
|
97
|
+
return cmdCache();
|
|
98
|
+
|
|
67
99
|
case null:
|
|
68
100
|
// Default: scan (dry-run unless --yes)
|
|
69
101
|
return cmdScan(config);
|
|
@@ -82,13 +114,21 @@ async function main() {
|
|
|
82
114
|
* Dry-run unless --yes is passed.
|
|
83
115
|
*/
|
|
84
116
|
function cmdScan(config) {
|
|
85
|
-
const sessionPid =
|
|
117
|
+
const sessionPid = parseSessionPidFlag();
|
|
86
118
|
const force = flags.yes || flags.y;
|
|
87
119
|
|
|
88
120
|
console.log(bold('\n zclean') + c('gray', ' — scanning for zombie processes...\n'));
|
|
89
121
|
|
|
90
122
|
const zombies = scan(config, { sessionPid });
|
|
91
123
|
|
|
124
|
+
if (hasScanErrors(zombies)) {
|
|
125
|
+
reportDryRun(zombies);
|
|
126
|
+
appendLog({ action: 'scan-failed', errors: zombies.errors.length });
|
|
127
|
+
pruneLogs(config);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
92
132
|
if (force) {
|
|
93
133
|
// Kill mode
|
|
94
134
|
if (zombies.length === 0) {
|
|
@@ -98,9 +138,13 @@ function cmdScan(config) {
|
|
|
98
138
|
}
|
|
99
139
|
appendLog({ action: 'scan', found: zombies.length });
|
|
100
140
|
const results = killZombies(zombies, config);
|
|
141
|
+
results.cumulative = getCumulativeStats();
|
|
101
142
|
reportKill(results);
|
|
102
143
|
} else {
|
|
103
144
|
// Dry-run mode
|
|
145
|
+
if (config.dryRunDefault === false) {
|
|
146
|
+
console.log(c('yellow', ' dryRunDefault=false is not an auto-kill switch; pass --yes to clean.\n'));
|
|
147
|
+
}
|
|
104
148
|
reportDryRun(zombies);
|
|
105
149
|
if (zombies.length > 0) {
|
|
106
150
|
appendLog({ action: 'dry-run', found: zombies.length });
|
|
@@ -121,9 +165,9 @@ function cmdInit(config) {
|
|
|
121
165
|
const existingConfig = loadConfig();
|
|
122
166
|
if (JSON.stringify(existingConfig) === JSON.stringify(DEFAULT_CONFIG)) {
|
|
123
167
|
saveConfig(DEFAULT_CONFIG);
|
|
124
|
-
console.log(c('green', ' Config created:') + ` ${
|
|
168
|
+
console.log(c('green', ' Config created:') + ` ${getConfigFile()}`);
|
|
125
169
|
} else {
|
|
126
|
-
console.log(c('gray', ' Config exists:') + ` ${
|
|
170
|
+
console.log(c('gray', ' Config exists:') + ` ${getConfigFile()}`);
|
|
127
171
|
}
|
|
128
172
|
|
|
129
173
|
// 2. Install Claude Code hook
|
|
@@ -132,7 +176,7 @@ function cmdInit(config) {
|
|
|
132
176
|
console.log(`${hookIcon} ${hookResult.message}`);
|
|
133
177
|
|
|
134
178
|
// 3. Install platform-specific scheduler
|
|
135
|
-
installScheduler();
|
|
179
|
+
installScheduler(platform);
|
|
136
180
|
|
|
137
181
|
console.log();
|
|
138
182
|
}
|
|
@@ -144,6 +188,7 @@ function cmdStatus(config) {
|
|
|
144
188
|
const zombies = scan(config);
|
|
145
189
|
const logs = readLogs(100);
|
|
146
190
|
reportStatus(zombies, logs);
|
|
191
|
+
if (hasScanErrors(zombies)) process.exitCode = 1;
|
|
147
192
|
}
|
|
148
193
|
|
|
149
194
|
/**
|
|
@@ -165,7 +210,7 @@ function cmdUninstall() {
|
|
|
165
210
|
console.log(` Hook: ${hookResult.message}`);
|
|
166
211
|
|
|
167
212
|
// Remove scheduler
|
|
168
|
-
uninstallScheduler();
|
|
213
|
+
uninstallScheduler(platform);
|
|
169
214
|
|
|
170
215
|
console.log(c('gray', `\n Config and logs preserved at ~/.zclean/`));
|
|
171
216
|
console.log(c('gray', ` To fully remove: rm -rf ~/.zclean\n`));
|
|
@@ -175,69 +220,48 @@ function cmdUninstall() {
|
|
|
175
220
|
* config: Show current config.
|
|
176
221
|
*/
|
|
177
222
|
function cmdConfig(config) {
|
|
178
|
-
reportConfig(config,
|
|
223
|
+
reportConfig(config, getConfigFile());
|
|
179
224
|
}
|
|
180
225
|
|
|
181
|
-
|
|
226
|
+
/**
|
|
227
|
+
* doctor: Self-diagnosis — check if zclean is properly set up and running.
|
|
228
|
+
*/
|
|
229
|
+
function cmdDoctor(config) {
|
|
230
|
+
const report = runDoctor(config, { json: Boolean(flags.json) });
|
|
231
|
+
if (report.exitCode !== 0) process.exitCode = report.exitCode;
|
|
232
|
+
}
|
|
182
233
|
|
|
183
|
-
function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const result = installLaunchd();
|
|
188
|
-
const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
|
|
189
|
-
console.log(`${icon} ${result.message}`);
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
case 'linux': {
|
|
193
|
-
const { installSystemd } = require('../src/installer/systemd');
|
|
194
|
-
const result = installSystemd();
|
|
195
|
-
const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
|
|
196
|
-
console.log(`${icon} ${result.message}`);
|
|
197
|
-
break;
|
|
198
|
-
}
|
|
199
|
-
case 'win32': {
|
|
200
|
-
const { installTaskScheduler } = require('../src/installer/taskscheduler');
|
|
201
|
-
const result = installTaskScheduler();
|
|
202
|
-
const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
|
|
203
|
-
console.log(`${icon} ${result.message}`);
|
|
204
|
-
break;
|
|
205
|
-
}
|
|
206
|
-
default:
|
|
207
|
-
console.log(c('yellow', ` Scheduler: Unsupported platform (${platform}). Install a cron job manually.`));
|
|
208
|
-
}
|
|
234
|
+
function cmdAudit(config, commandName = 'audit') {
|
|
235
|
+
const sessionPid = parseSessionPidFlag();
|
|
236
|
+
const report = runAudit(config, { json: Boolean(flags.json), sessionPid, commandName });
|
|
237
|
+
if (!report.risk.enumerationComplete) process.exitCode = 1;
|
|
209
238
|
}
|
|
210
239
|
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
case 'win32': {
|
|
226
|
-
const { removeTaskScheduler } = require('../src/installer/taskscheduler');
|
|
227
|
-
const result = removeTaskScheduler();
|
|
228
|
-
console.log(` Scheduler: ${result.message}`);
|
|
229
|
-
break;
|
|
230
|
-
}
|
|
231
|
-
default:
|
|
232
|
-
console.log(c('yellow', ` Scheduler: Remove manually for ${platform}.`));
|
|
240
|
+
function cmdCache() {
|
|
241
|
+
runCache({
|
|
242
|
+
root: typeof flags.path === 'string' ? flags.path : process.cwd(),
|
|
243
|
+
yes: Boolean(flags.yes || flags.y),
|
|
244
|
+
json: Boolean(flags.json),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseSessionPidFlag() {
|
|
249
|
+
if (flags['session-pid'] === undefined) return null;
|
|
250
|
+
if (flags['session-pid'] === true || flags['session-pid'] === '') {
|
|
251
|
+
console.error(c('red', ' --session-pid must be a positive integer'));
|
|
252
|
+
process.exit(1);
|
|
233
253
|
}
|
|
254
|
+
const pid = Number(flags['session-pid']);
|
|
255
|
+
if (Number.isInteger(pid) && pid > 0) return pid;
|
|
256
|
+
console.error(c('red', ' --session-pid must be a positive integer'));
|
|
257
|
+
process.exit(1);
|
|
234
258
|
}
|
|
235
259
|
|
|
236
260
|
// ─── Help ───────────────────────────────────────────────────────────────────
|
|
237
261
|
|
|
238
262
|
function printHelp() {
|
|
239
263
|
console.log(`
|
|
240
|
-
${bold('zclean')} —
|
|
264
|
+
${bold('zclean')} — AI coding runtime hygiene for agent sessions
|
|
241
265
|
|
|
242
266
|
${bold('Usage:')}
|
|
243
267
|
zclean Scan for zombies (dry-run)
|
|
@@ -245,19 +269,31 @@ function printHelp() {
|
|
|
245
269
|
zclean init Install hooks + scheduler
|
|
246
270
|
zclean status Show current zombies and last cleanup
|
|
247
271
|
zclean logs Show recent cleanup history
|
|
272
|
+
zclean history [--json] Show cleanup history
|
|
273
|
+
zclean protect list [--json] Show protected whitelist entries
|
|
274
|
+
zclean protect add <entry> Add a whitelist entry
|
|
275
|
+
zclean protect remove <entry|--index=N>
|
|
276
|
+
Remove a whitelist entry
|
|
248
277
|
zclean uninstall Remove hooks + scheduler
|
|
249
278
|
zclean config Show current configuration
|
|
279
|
+
zclean doctor [--json] Check if zclean is properly set up
|
|
280
|
+
zclean report [--json] Show AI runtime hygiene report
|
|
281
|
+
zclean audit [--json] Alias for report
|
|
282
|
+
zclean cache [--json] Show safe workspace cache candidates
|
|
283
|
+
zclean cache --yes Remove supported workspace cache directories
|
|
250
284
|
|
|
251
285
|
${bold('Options:')}
|
|
252
286
|
--yes, -y Kill found zombies (default: dry-run)
|
|
253
287
|
--session-pid=PID Filter by parent session PID
|
|
288
|
+
--path=DIR Workspace path for zclean cache
|
|
289
|
+
--json Print machine-readable output for supported commands
|
|
254
290
|
--version, -v Show version
|
|
255
291
|
--help, -h Show this help
|
|
256
292
|
|
|
257
293
|
${bold('Config:')} ~/.zclean/config.json
|
|
258
294
|
${bold('Logs:')} ~/.zclean/history.jsonl
|
|
259
295
|
|
|
260
|
-
${bold('Docs:')} https://github.com/
|
|
296
|
+
${bold('Docs:')} https://github.com/TheStack-ai/zclean
|
|
261
297
|
`);
|
|
262
298
|
}
|
|
263
299
|
|
package/package.json
CHANGED
|
@@ -1,12 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-clean",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"bin": {
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "AI coding runtime hygiene reports, zombie cleanup, and safe workspace cache cleanup",
|
|
5
|
+
"bin": {
|
|
6
|
+
"zclean": "bin/zclean.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": "node --test test/patterns.test.js test/scanner.test.js test/process-tree.test.js test/killer.test.js test/config.test.js test/installer-fallback.test.js test/cli.test.js test/cli-history-protect.test.js test/cli-doctor.test.js test/audit.test.js test/cache.test.js",
|
|
10
|
+
"syntax": "node -e \"const fs=require('node:fs');const path=require('node:path');const {execFileSync}=require('node:child_process');const files=['bin/zclean.js'];function walk(dir){if(!fs.existsSync(dir))return;for(const entry of fs.readdirSync(dir,{withFileTypes:true})){const file=path.join(dir,entry.name);if(entry.isDirectory())walk(file);else if(entry.name.endsWith('.js'))files.push(file);}}for(const dir of ['src','test','scripts'])walk(dir);for(const file of files)execFileSync(process.execPath,['-c',file],{stdio:'inherit'});\"",
|
|
11
|
+
"smoke": "node scripts/smoke.js",
|
|
12
|
+
"pack:check": "npm pack --dry-run --json"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"cleanup",
|
|
18
|
+
"zombie",
|
|
19
|
+
"orphan",
|
|
20
|
+
"process",
|
|
21
|
+
"ai-tools",
|
|
22
|
+
"mcp",
|
|
23
|
+
"cache-cleaner",
|
|
24
|
+
"developer-tools",
|
|
25
|
+
"ai-coding"
|
|
26
|
+
],
|
|
27
|
+
"author": "TheStack-ai",
|
|
8
28
|
"license": "MIT",
|
|
9
|
-
"repository": {
|
|
10
|
-
|
|
11
|
-
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/TheStack-ai/zclean.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/TheStack-ai/zclean/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/TheStack-ai/zclean#readme",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"bin/",
|
|
42
|
+
"src/",
|
|
43
|
+
"assets/zclean-hero.png",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"README.md",
|
|
46
|
+
"README.ko.md",
|
|
47
|
+
"README.zh.md"
|
|
48
|
+
]
|
|
12
49
|
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function buildRecommendations({ zombieCount, errors, warnings, commandName }) {
|
|
4
|
+
if (errors.length > 0) {
|
|
5
|
+
return [
|
|
6
|
+
'Run `zclean doctor` before cleaning; process enumeration is incomplete.',
|
|
7
|
+
`Do not trust a zero-candidate ${commandName} until enumeration errors are resolved.`,
|
|
8
|
+
];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const recommendations = [
|
|
12
|
+
'Review candidates before cleanup. Manual cleanup still requires `zclean --yes`.',
|
|
13
|
+
];
|
|
14
|
+
if (zombieCount > 0) {
|
|
15
|
+
recommendations.push('Run `zclean --yes` only after confirming these are leftover AI coding runtimes.');
|
|
16
|
+
} else {
|
|
17
|
+
recommendations.push('No AI runtime leftovers are currently visible.');
|
|
18
|
+
}
|
|
19
|
+
if (warnings.length > 0) {
|
|
20
|
+
recommendations.push('Review scan warnings; some candidates may need manual attribution.');
|
|
21
|
+
}
|
|
22
|
+
recommendations.push(`Use \`zclean ${commandName} --json\` for dashboards, CI notes, or local automation.`);
|
|
23
|
+
return recommendations;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildNextActions({ zombieCount, errors, warnings, commandName }) {
|
|
27
|
+
if (errors.length > 0) {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
id: 'run-doctor',
|
|
31
|
+
priority: 'high',
|
|
32
|
+
command: 'zclean doctor',
|
|
33
|
+
description: 'Fix process enumeration before trusting the report.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'avoid-cleanup',
|
|
37
|
+
priority: 'high',
|
|
38
|
+
command: null,
|
|
39
|
+
description: `Do not run cleanup from this ${commandName} result while enumeration is incomplete.`,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const actions = [];
|
|
45
|
+
if (zombieCount > 0) {
|
|
46
|
+
actions.push({
|
|
47
|
+
id: 'review-top-candidates',
|
|
48
|
+
priority: 'high',
|
|
49
|
+
command: null,
|
|
50
|
+
description: 'Review topCandidates, largestCandidate, and oldestCandidate before cleanup.',
|
|
51
|
+
});
|
|
52
|
+
actions.push({
|
|
53
|
+
id: 'manual-cleanup-requires-yes',
|
|
54
|
+
priority: 'high',
|
|
55
|
+
command: 'zclean --yes',
|
|
56
|
+
description: 'Cleanup remains opt-in and requires an explicit --yes run after review.',
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
actions.push({
|
|
60
|
+
id: 'monitor-runtime-hygiene',
|
|
61
|
+
priority: 'normal',
|
|
62
|
+
command: `zclean ${commandName}`,
|
|
63
|
+
description: 'No AI runtime leftovers are visible; rerun the report when sessions change.',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (warnings.length > 0) {
|
|
68
|
+
actions.push({
|
|
69
|
+
id: 'review-warnings',
|
|
70
|
+
priority: 'normal',
|
|
71
|
+
command: null,
|
|
72
|
+
description: 'Review scan warnings before making cleanup decisions.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
actions.push({
|
|
77
|
+
id: 'export-json',
|
|
78
|
+
priority: 'low',
|
|
79
|
+
command: `zclean ${commandName} --json`,
|
|
80
|
+
description: 'Use the JSON report for local dashboards, CI notes, or automation.',
|
|
81
|
+
});
|
|
82
|
+
return actions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
buildNextActions,
|
|
87
|
+
buildRecommendations,
|
|
88
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function buildCandidateReview(item) {
|
|
4
|
+
const memory = item.mem || 0;
|
|
5
|
+
const age = item.age || 0;
|
|
6
|
+
const riskScore = Math.min(100, 30 + Math.floor(memory / (100 * 1024 * 1024)) * 5 + Math.floor(age / 3600000));
|
|
7
|
+
return {
|
|
8
|
+
pid: item.pid,
|
|
9
|
+
name: item.name || item.pattern || 'unknown',
|
|
10
|
+
pattern: item.pattern || item.name || 'unknown',
|
|
11
|
+
memoryBytes: memory,
|
|
12
|
+
ageMs: age,
|
|
13
|
+
reason: item.reason || '',
|
|
14
|
+
risk: {
|
|
15
|
+
score: riskScore,
|
|
16
|
+
level: riskScore >= 75 ? 'high' : riskScore >= 50 ? 'medium' : 'low',
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function groupCandidatesByPattern(candidates) {
|
|
22
|
+
const grouped = {};
|
|
23
|
+
for (const candidate of candidates) {
|
|
24
|
+
grouped[candidate.pattern] = (grouped[candidate.pattern] || 0) + 1;
|
|
25
|
+
}
|
|
26
|
+
return grouped;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function groupCandidateSourcesByPattern(candidates) {
|
|
30
|
+
const grouped = {};
|
|
31
|
+
for (const candidate of candidates) {
|
|
32
|
+
const pattern = candidate.pattern;
|
|
33
|
+
if (!grouped[pattern]) {
|
|
34
|
+
grouped[pattern] = {
|
|
35
|
+
count: 0,
|
|
36
|
+
memoryBytes: 0,
|
|
37
|
+
pids: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
grouped[pattern].count++;
|
|
41
|
+
grouped[pattern].memoryBytes += candidate.memoryBytes;
|
|
42
|
+
grouped[pattern].pids.push(candidate.pid);
|
|
43
|
+
}
|
|
44
|
+
return grouped;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sortCandidatesByRisk(candidates) {
|
|
48
|
+
return [...candidates].sort((left, right) => {
|
|
49
|
+
if (right.risk.score !== left.risk.score) return right.risk.score - left.risk.score;
|
|
50
|
+
if (right.memoryBytes !== left.memoryBytes) return right.memoryBytes - left.memoryBytes;
|
|
51
|
+
if (right.ageMs !== left.ageMs) return right.ageMs - left.ageMs;
|
|
52
|
+
return (left.pid || 0) - (right.pid || 0);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function pickCandidate(candidates, field) {
|
|
57
|
+
if (candidates.length === 0) return null;
|
|
58
|
+
return candidates.reduce((best, candidate) => {
|
|
59
|
+
if (candidate[field] > best[field]) return candidate;
|
|
60
|
+
if (candidate[field] === best[field] && (candidate.pid || 0) < (best.pid || 0)) return candidate;
|
|
61
|
+
return best;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function calculateScore(candidates, warnings, errors) {
|
|
66
|
+
if (errors.length > 0) return 0;
|
|
67
|
+
const candidatePenalty = candidates.reduce((sum, item) => sum + Math.min(20, Math.ceil(item.risk.score / 10)), 0);
|
|
68
|
+
const warningPenalty = warnings.length * 5;
|
|
69
|
+
return Math.max(0, 100 - candidatePenalty - warningPenalty);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function riskLevel(candidateCount, warningCount) {
|
|
73
|
+
if (candidateCount === 0 && warningCount === 0) return 'clean';
|
|
74
|
+
if (candidateCount <= 2 && warningCount === 0) return 'watch';
|
|
75
|
+
return 'attention';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
buildCandidateReview,
|
|
80
|
+
calculateScore,
|
|
81
|
+
groupCandidateSourcesByPattern,
|
|
82
|
+
groupCandidatesByPattern,
|
|
83
|
+
pickCandidate,
|
|
84
|
+
riskLevel,
|
|
85
|
+
sortCandidatesByRisk,
|
|
86
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function buildHistory(logs, stats) {
|
|
4
|
+
const lastCleanup = logs.filter((entry) => entry.action === 'cleanup-summary').pop() || null;
|
|
5
|
+
return {
|
|
6
|
+
totalKilled: stats.totalKilled || 0,
|
|
7
|
+
totalMemFreed: stats.totalMemFreed || 0,
|
|
8
|
+
weekKilled: stats.weekKilled || 0,
|
|
9
|
+
weekMemFreed: stats.weekMemFreed || 0,
|
|
10
|
+
lastRun: stats.lastRun || (lastCleanup ? lastCleanup.timestamp : null),
|
|
11
|
+
lastDryRun: findLastTimestamp(logs, 'dry-run'),
|
|
12
|
+
recentFailures: findRecentFailures(logs),
|
|
13
|
+
recent: summarizeLogs(logs),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function findLastTimestamp(logs, action) {
|
|
18
|
+
const entry = logs.filter((item) => item.action === action && item.timestamp).pop();
|
|
19
|
+
return entry ? entry.timestamp : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function findRecentFailures(logs) {
|
|
23
|
+
return logs
|
|
24
|
+
.filter((entry) => entry.action === 'scan-failed' || entry.action === 'kill-failed' || (entry.action === 'cleanup-summary' && entry.failed > 0))
|
|
25
|
+
.slice(-5)
|
|
26
|
+
.map((entry) => ({
|
|
27
|
+
timestamp: entry.timestamp || null,
|
|
28
|
+
action: entry.action,
|
|
29
|
+
pid: entry.pid || null,
|
|
30
|
+
message: entry.message || entry.error || null,
|
|
31
|
+
failed: entry.failed || 0,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function summarizeLogs(logs) {
|
|
36
|
+
const summary = {
|
|
37
|
+
dryRuns: 0,
|
|
38
|
+
scans: 0,
|
|
39
|
+
scanFailures: 0,
|
|
40
|
+
cleanupSummaries: 0,
|
|
41
|
+
failedKills: 0,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (const entry of logs) {
|
|
45
|
+
switch (entry.action) {
|
|
46
|
+
case 'dry-run':
|
|
47
|
+
summary.dryRuns++;
|
|
48
|
+
break;
|
|
49
|
+
case 'scan':
|
|
50
|
+
summary.scans++;
|
|
51
|
+
break;
|
|
52
|
+
case 'scan-failed':
|
|
53
|
+
summary.scanFailures++;
|
|
54
|
+
break;
|
|
55
|
+
case 'cleanup-summary':
|
|
56
|
+
summary.cleanupSummaries++;
|
|
57
|
+
break;
|
|
58
|
+
case 'kill-failed':
|
|
59
|
+
summary.failedKills++;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return summary;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
buildHistory,
|
|
69
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { c, bold, formatBytes, formatDuration } = require('./reporter');
|
|
4
|
+
|
|
5
|
+
function reportAudit(report, options = {}) {
|
|
6
|
+
const commandName = options.commandName || 'audit';
|
|
7
|
+
console.log(bold(`\n zclean ${commandName}`) + c('gray', ' - AI runtime hygiene review\n'));
|
|
8
|
+
console.log(` Score: ${formatRiskScore(report.risk)}`);
|
|
9
|
+
console.log(` Status: ${report.summary.status}`);
|
|
10
|
+
console.log(` Candidates: ${report.summary.zombieCount}`);
|
|
11
|
+
console.log(` Memory: ${formatBytes(report.summary.reclaimableBytes)} reclaimable`);
|
|
12
|
+
console.log(' Scope: AI coding runtime hygiene plus safe workspace caches; not app uninstall or whole-disk cleanup');
|
|
13
|
+
console.log();
|
|
14
|
+
|
|
15
|
+
console.log(c('cyan', ' Safety'));
|
|
16
|
+
console.log(` Manual scans are dry-run: ${report.proGradeReview.guardrails.dryRunDefault ? 'yes' : 'configured false, but --yes is still required'}`);
|
|
17
|
+
console.log(` Cleanup requires --yes: ${report.proGradeReview.guardrails.cleanupRequiresYes ? 'yes' : 'no'}`);
|
|
18
|
+
console.log(` Whitelist entries: ${report.proGradeReview.guardrails.whitelistCount}`);
|
|
19
|
+
console.log(` Custom AI dirs: ${report.proGradeReview.guardrails.customAiDirCount}`);
|
|
20
|
+
console.log(` Telemetry: none`);
|
|
21
|
+
console.log();
|
|
22
|
+
|
|
23
|
+
if (!report.risk.enumerationComplete) {
|
|
24
|
+
console.log(c('red', ' Enumeration incomplete'));
|
|
25
|
+
for (const error of report.diagnostics.errors) {
|
|
26
|
+
console.log(` ${formatDiagnostic(error)}`);
|
|
27
|
+
}
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (report.diagnostics.warnings.length > 0) {
|
|
32
|
+
console.log(c('yellow', ' Warnings'));
|
|
33
|
+
for (const warning of report.diagnostics.warnings) {
|
|
34
|
+
console.log(` ${formatDiagnostic(warning)}`);
|
|
35
|
+
}
|
|
36
|
+
console.log();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (report.proGradeReview.candidates.length > 0) {
|
|
40
|
+
console.log(c('cyan', ' Top candidates'));
|
|
41
|
+
for (const item of report.proGradeReview.topCandidates.slice(0, 5)) {
|
|
42
|
+
console.log(` PID ${String(item.pid).padStart(6)} ${item.pattern.padEnd(16)} ${formatBytes(item.memoryBytes).padStart(8)} ${formatDuration(item.ageMs).padStart(6)}`);
|
|
43
|
+
console.log(` ${item.reason}`);
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log(c('cyan', ' Recent history'));
|
|
49
|
+
console.log(` This week: ${report.history.weekKilled} cleaned, ${formatBytes(report.history.weekMemFreed)} freed`);
|
|
50
|
+
console.log(` All time: ${report.history.totalKilled} cleaned, ${formatBytes(report.history.totalMemFreed)} freed`);
|
|
51
|
+
console.log(` Last run: ${report.history.lastRun || 'never'}`);
|
|
52
|
+
console.log(` Last dry-run: ${report.history.lastDryRun || 'never'}`);
|
|
53
|
+
console.log(` Recent failures: ${report.history.recentFailures.length}`);
|
|
54
|
+
console.log();
|
|
55
|
+
|
|
56
|
+
console.log(c('cyan', ' Positioning'));
|
|
57
|
+
console.log(` ${report.differentiation}`);
|
|
58
|
+
console.log();
|
|
59
|
+
|
|
60
|
+
console.log(c('cyan', ' Recommendations'));
|
|
61
|
+
for (const action of report.nextActions) {
|
|
62
|
+
const command = action.command ? ` (${action.command})` : '';
|
|
63
|
+
console.log(` - [${action.priority}] ${action.description}${command}`);
|
|
64
|
+
}
|
|
65
|
+
console.log();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function formatRiskScore(risk) {
|
|
69
|
+
const text = `${risk.score}/100 (${risk.level})`;
|
|
70
|
+
if (risk.level === 'clean' || risk.level === 'watch') return c('green', text);
|
|
71
|
+
if (risk.level === 'unknown') return c('red', text);
|
|
72
|
+
return c('yellow', text);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function formatDiagnostic(item) {
|
|
76
|
+
if (typeof item === 'string') return item;
|
|
77
|
+
const code = item.code ? `${item.code}: ` : '';
|
|
78
|
+
return `${code}${item.message || 'scan diagnostic'}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
reportAudit,
|
|
83
|
+
};
|