z-clean 0.3.1 → 0.3.3
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/README.ko.md +25 -3
- package/README.md +24 -7
- package/README.zh.md +25 -3
- package/bin/zclean.js +36 -40
- package/package.json +5 -3
- package/scripts/postinstall.js +7 -0
- package/src/cli-brand.js +129 -0
- package/src/commands/init.js +50 -0
- package/src/commands/scheduler.js +27 -23
- package/src/config.js +24 -14
- package/src/detector/custom-patterns.js +79 -0
- package/src/detector/patterns.js +30 -2
- package/src/installer/launchd.js +64 -15
- package/src/installer/systemd.js +39 -20
- package/src/installer/taskscheduler.js +17 -6
- package/src/killer.js +11 -2
- package/src/process-diagnostic.js +17 -0
- package/src/reporter.js +10 -8
- package/src/scanner.js +6 -2
- package/src/storage-security.js +65 -0
- package/src/terminal-text.js +11 -0
- package/src/windows-processes.js +37 -12
package/README.ko.md
CHANGED
|
@@ -55,17 +55,36 @@ AI 코딩 도구는 작업 중 많은 임시 런타임을 만듭니다.
|
|
|
55
55
|
|
|
56
56
|
## 설치
|
|
57
57
|
|
|
58
|
+
먼저 설치나 정리 없이 읽기 전용 audit을 실행하세요:
|
|
59
|
+
|
|
58
60
|
```bash
|
|
59
|
-
|
|
60
|
-
zclean init
|
|
61
|
+
npx --yes z-clean audit
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
정리 전에 후보를 검토하세요:
|
|
64
65
|
|
|
65
66
|
```bash
|
|
66
67
|
npx --yes z-clean
|
|
68
|
+
npx --yes z-clean report
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
계속 사용하려면 전역 설치하세요:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install --global z-clean --foreground-scripts
|
|
75
|
+
zclean report
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`--foreground-scripts`를 사용하면 zclean 설치 워드마크가 표시됩니다. npm 7 이상은 lifecycle 출력을 기본적으로 숨기므로 `npm install -g z-clean`도 정상 설치되지만 완료 화면은 보이지 않을 수 있습니다.
|
|
79
|
+
|
|
80
|
+
자동 실행은 선택 사항입니다:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
zclean init
|
|
67
84
|
```
|
|
68
85
|
|
|
86
|
+
`zclean init`은 Claude Code session hook과 OS scheduler를 설치합니다. dry-run 결과를 확인한 후에만 실행하세요. 상주 daemon을 설치하지는 않습니다.
|
|
87
|
+
|
|
69
88
|
## 자주 쓰는 명령
|
|
70
89
|
|
|
71
90
|
```bash
|
|
@@ -76,12 +95,15 @@ zclean report --json # 자동화용 JSON report
|
|
|
76
95
|
zclean cache # workspace cache dry-run scan
|
|
77
96
|
zclean cache --yes # 지원되는 cache directory 삭제
|
|
78
97
|
zclean cache --json # cache 후보 JSON 출력
|
|
98
|
+
zclean --pattern=my-agent-worker # 이번 scan에 literal orphan pattern 추가
|
|
79
99
|
zclean history --json # 정리 이력 JSON
|
|
80
100
|
zclean protect list # 보호 목록 확인
|
|
81
101
|
zclean protect add mcp-server-keep
|
|
82
102
|
zclean doctor --json # 설치/탐지/스케줄러 상태 점검
|
|
83
103
|
```
|
|
84
104
|
|
|
105
|
+
패턴을 계속 사용하려면 `~/.zclean/config.json`의 `customPatterns` 배열에 문자열을 추가하세요. 패턴은 정규식이 아닌 대소문자 무시 literal 문자열이며 3–80자의 출력 가능한 문자만 허용됩니다. `node`, `node /`, `ode` 같은 일반 runtime 이름과 부분 문자열은 거부되고, orphan이면서 `maxAge`(기본 24시간)를 넘은 process만 후보가 됩니다. 잘못되거나 0인 기간은 안전한 기본값 24시간으로 돌아갑니다. whitelist, PID 재검증, dry-run, `--yes` 안전장치는 그대로 유지됩니다.
|
|
106
|
+
|
|
85
107
|
다른 workspace cache를 확인하려면:
|
|
86
108
|
|
|
87
109
|
```bash
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ AI coding runtime hygiene
|
|
|
15
15
|
[](#)
|
|
16
16
|
[](#platform-status)
|
|
17
17
|
[](#)
|
|
18
|
-
[](#)
|
|
19
19
|
[](https://github.com/TheStack-ai/zclean)
|
|
20
20
|
[](https://github.com/rohitg00/awesome-claude-code-toolkit)
|
|
21
21
|
|
|
@@ -51,24 +51,28 @@ The goal is narrower: **AI coding cleanup and developer workspace hygiene**.
|
|
|
51
51
|
|
|
52
52
|
## Quick Start
|
|
53
53
|
|
|
54
|
+
Start with a read-only audit. This does not install hooks, schedule jobs, or clean anything:
|
|
55
|
+
|
|
54
56
|
```bash
|
|
55
|
-
|
|
56
|
-
zclean init
|
|
57
|
+
npx --yes z-clean audit
|
|
57
58
|
```
|
|
58
59
|
|
|
59
|
-
|
|
60
|
+
Review the candidates before cleaning:
|
|
60
61
|
|
|
61
62
|
```bash
|
|
62
63
|
npx --yes z-clean
|
|
64
|
+
npx --yes z-clean report
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
Install the CLI when you are ready to keep using it:
|
|
66
68
|
|
|
67
69
|
```bash
|
|
70
|
+
npm install --global z-clean --foreground-scripts
|
|
68
71
|
zclean report
|
|
69
|
-
zclean report --json
|
|
70
72
|
```
|
|
71
73
|
|
|
74
|
+
`--foreground-scripts` displays zclean's install wordmark. npm 7+ hides lifecycle output by default, so plain `npm install -g z-clean` still installs correctly but may not show the branded completion screen.
|
|
75
|
+
|
|
72
76
|
Clean only when you decide:
|
|
73
77
|
|
|
74
78
|
```bash
|
|
@@ -76,6 +80,14 @@ zclean --yes
|
|
|
76
80
|
zclean cache --yes
|
|
77
81
|
```
|
|
78
82
|
|
|
83
|
+
Optional automation:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
zclean init
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`zclean init` installs the Claude Code session hook and the native OS scheduler. Run it only after reviewing the dry-run output. It does not install a persistent daemon.
|
|
90
|
+
|
|
79
91
|
## Why developers use it
|
|
80
92
|
|
|
81
93
|
AI coding tools spawn many short-lived runtimes while they work:
|
|
@@ -101,6 +113,7 @@ When an agent crashes, a terminal closes, or a session ends abruptly, those chil
|
|
|
101
113
|
| `zclean cache` | Dry-run scan for supported workspace cache directories |
|
|
102
114
|
| `zclean cache --yes` | Remove supported workspace cache directories |
|
|
103
115
|
| `zclean cache --json` | Machine-readable cache hygiene report |
|
|
116
|
+
| `zclean --pattern=my-agent-worker` | Add one literal orphan-process pattern for this scan |
|
|
104
117
|
| `zclean history` | Recent cleanup history |
|
|
105
118
|
| `zclean history --json` | Sanitized cleanup history and cumulative stats |
|
|
106
119
|
| `zclean protect list` | Show protected process patterns |
|
|
@@ -249,7 +262,8 @@ zclean doctor --json
|
|
|
249
262
|
"sigterm_timeout": 10,
|
|
250
263
|
"dryRunDefault": true,
|
|
251
264
|
"logRetention": "30d",
|
|
252
|
-
"customAiDirs": []
|
|
265
|
+
"customAiDirs": [],
|
|
266
|
+
"customPatterns": []
|
|
253
267
|
}
|
|
254
268
|
```
|
|
255
269
|
|
|
@@ -263,6 +277,9 @@ zclean doctor --json
|
|
|
263
277
|
| `dryRunDefault` | `true` | Manual scans stay dry-run by default; cleanup still requires `--yes` |
|
|
264
278
|
| `logRetention` | `"30d"` | Cleanup history retention |
|
|
265
279
|
| `customAiDirs` | `[]` | Additional AI tool directories to detect, such as `[".mytool"]` |
|
|
280
|
+
| `customPatterns` | `[]` | Case-insensitive literal command fragments for additional orphan processes |
|
|
281
|
+
|
|
282
|
+
Custom patterns are deliberately restricted: values must contain 3–80 printable characters, generic runtime names and fragments such as `node`, `node /`, or `ode` are rejected, and values are treated as literal text rather than regular expressions. Candidates must be orphaned and older than `maxAge` (24 hours by default); invalid or zero durations fall back to that safe default. Whitelists, PID identity verification, batch limits, dry-run behavior, and the explicit `--yes` cleanup gate still apply.
|
|
266
283
|
|
|
267
284
|
## Supported tools
|
|
268
285
|
|
package/README.zh.md
CHANGED
|
@@ -55,17 +55,36 @@ AI 编程工具在工作时会启动很多临时运行时:
|
|
|
55
55
|
|
|
56
56
|
## 安装
|
|
57
57
|
|
|
58
|
+
先运行只读 audit;它不会安装 hook、创建定时任务或执行清理:
|
|
59
|
+
|
|
58
60
|
```bash
|
|
59
|
-
|
|
60
|
-
zclean init
|
|
61
|
+
npx --yes z-clean audit
|
|
61
62
|
```
|
|
62
63
|
|
|
63
|
-
|
|
64
|
+
清理前先查看候选项:
|
|
64
65
|
|
|
65
66
|
```bash
|
|
66
67
|
npx --yes z-clean
|
|
68
|
+
npx --yes z-clean report
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
准备长期使用时再全局安装:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npm install --global z-clean --foreground-scripts
|
|
75
|
+
zclean report
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`--foreground-scripts` 会显示 zclean 安装字标。npm 7 及以上默认隐藏 lifecycle 输出,因此普通的 `npm install -g z-clean` 仍会正常安装,但可能不显示品牌完成画面。
|
|
79
|
+
|
|
80
|
+
自动运行是可选项:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
zclean init
|
|
67
84
|
```
|
|
68
85
|
|
|
86
|
+
`zclean init` 会安装 Claude Code session hook 和原生 OS scheduler。请先检查 dry-run 结果;它不会安装常驻 daemon。
|
|
87
|
+
|
|
69
88
|
## 常用命令
|
|
70
89
|
|
|
71
90
|
```bash
|
|
@@ -76,12 +95,15 @@ zclean report --json # 自动化 JSON report
|
|
|
76
95
|
zclean cache # workspace cache dry-run scan
|
|
77
96
|
zclean cache --yes # 删除支持的 cache directory
|
|
78
97
|
zclean cache --json # 输出 cache 候选 JSON
|
|
98
|
+
zclean --pattern=my-agent-worker # 为本次扫描添加 literal orphan pattern
|
|
79
99
|
zclean history --json # 清理历史 JSON
|
|
80
100
|
zclean protect list # 查看保护列表
|
|
81
101
|
zclean protect add mcp-server-keep
|
|
82
102
|
zclean doctor --json # 检查安装、调度器和进程枚举状态
|
|
83
103
|
```
|
|
84
104
|
|
|
105
|
+
如需长期使用,请把字符串加入 `~/.zclean/config.json` 的 `customPatterns` 数组。pattern 是忽略大小写的 literal 文本,不是正则表达式;只允许 3–80 个可打印字符,并拒绝 `node`、`node /`、`ode` 等通用 runtime 名称或片段。只有 orphan 且超过 `maxAge`(默认 24 小时)的 process 才会成为候选项;无效或为零的时长会回退到安全的 24 小时默认值。whitelist、PID 再验证、dry-run 和显式 `--yes` 安全限制仍然有效。
|
|
106
|
+
|
|
85
107
|
扫描另一个 workspace:
|
|
86
108
|
|
|
87
109
|
```bash
|
package/bin/zclean.js
CHANGED
|
@@ -6,21 +6,21 @@ const { scan, hasScanErrors } = require('../src/scanner');
|
|
|
6
6
|
const { killZombies } = require('../src/killer');
|
|
7
7
|
const {
|
|
8
8
|
loadConfig,
|
|
9
|
-
saveConfig,
|
|
10
9
|
readLogs,
|
|
11
10
|
pruneLogs,
|
|
12
|
-
DEFAULT_CONFIG,
|
|
13
11
|
appendLog,
|
|
14
12
|
getCumulativeStats,
|
|
15
13
|
getConfigFile,
|
|
16
14
|
} = require('../src/config');
|
|
17
15
|
const { reportDryRun, reportKill, reportStatus, reportLogs, reportConfig, c, bold } = require('../src/reporter');
|
|
18
|
-
const {
|
|
16
|
+
const { removeHook } = require('../src/installer/hook');
|
|
19
17
|
const { runHistory, runProtect } = require('../src/commands/trust');
|
|
20
|
-
const {
|
|
18
|
+
const { uninstallScheduler } = require('../src/commands/scheduler');
|
|
21
19
|
const { runDoctor } = require('../src/doctor');
|
|
22
20
|
const { runAudit } = require('../src/audit');
|
|
23
21
|
const { runCache } = require('../src/cache');
|
|
22
|
+
const { getCustomPatternError, normalizeCustomPattern } = require('../src/detector/patterns');
|
|
23
|
+
const { runInit } = require('../src/commands/init');
|
|
24
24
|
|
|
25
25
|
// Platform-specific installers (lazy loaded)
|
|
26
26
|
const platform = os.platform();
|
|
@@ -33,8 +33,10 @@ const positional = [];
|
|
|
33
33
|
|
|
34
34
|
for (const arg of args) {
|
|
35
35
|
if (arg.startsWith('--')) {
|
|
36
|
-
const
|
|
37
|
-
|
|
36
|
+
const body = arg.substring(2);
|
|
37
|
+
const separator = body.indexOf('=');
|
|
38
|
+
const key = separator === -1 ? body : body.slice(0, separator);
|
|
39
|
+
flags[key] = separator === -1 ? true : body.slice(separator + 1);
|
|
38
40
|
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
39
41
|
flags[arg.substring(1)] = true;
|
|
40
42
|
} else {
|
|
@@ -64,7 +66,7 @@ async function main() {
|
|
|
64
66
|
|
|
65
67
|
switch (command) {
|
|
66
68
|
case 'init':
|
|
67
|
-
return
|
|
69
|
+
return runInit({ config, platform });
|
|
68
70
|
|
|
69
71
|
case 'status':
|
|
70
72
|
return cmdStatus(config);
|
|
@@ -114,17 +116,19 @@ async function main() {
|
|
|
114
116
|
* Dry-run unless --yes is passed.
|
|
115
117
|
*/
|
|
116
118
|
function cmdScan(config) {
|
|
119
|
+
const scanConfig = withPatternFlag(config);
|
|
120
|
+
if (!scanConfig) return;
|
|
117
121
|
const sessionPid = parseSessionPidFlag();
|
|
118
122
|
const force = flags.yes || flags.y;
|
|
119
123
|
|
|
120
124
|
console.log(bold('\n zclean') + c('gray', ' — scanning for zombie processes...\n'));
|
|
121
125
|
|
|
122
|
-
const zombies = scan(
|
|
126
|
+
const zombies = scan(scanConfig, { sessionPid });
|
|
123
127
|
|
|
124
128
|
if (hasScanErrors(zombies)) {
|
|
125
129
|
reportDryRun(zombies);
|
|
126
130
|
appendLog({ action: 'scan-failed', errors: zombies.errors.length });
|
|
127
|
-
pruneLogs(
|
|
131
|
+
pruneLogs(scanConfig);
|
|
128
132
|
process.exitCode = 1;
|
|
129
133
|
return;
|
|
130
134
|
}
|
|
@@ -137,12 +141,12 @@ function cmdScan(config) {
|
|
|
137
141
|
return;
|
|
138
142
|
}
|
|
139
143
|
appendLog({ action: 'scan', found: zombies.length });
|
|
140
|
-
const results = killZombies(zombies,
|
|
144
|
+
const results = killZombies(zombies, scanConfig);
|
|
141
145
|
results.cumulative = getCumulativeStats();
|
|
142
146
|
reportKill(results);
|
|
143
147
|
} else {
|
|
144
148
|
// Dry-run mode
|
|
145
|
-
if (
|
|
149
|
+
if (scanConfig.dryRunDefault === false) {
|
|
146
150
|
console.log(c('yellow', ' dryRunDefault=false is not an auto-kill switch; pass --yes to clean.\n'));
|
|
147
151
|
}
|
|
148
152
|
reportDryRun(zombies);
|
|
@@ -152,40 +156,16 @@ function cmdScan(config) {
|
|
|
152
156
|
}
|
|
153
157
|
|
|
154
158
|
// Prune old logs
|
|
155
|
-
pruneLogs(
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* init: Install hooks + scheduler.
|
|
160
|
-
*/
|
|
161
|
-
function cmdInit(config) {
|
|
162
|
-
console.log(bold('\n zclean init') + c('gray', ' — installing hooks and scheduler...\n'));
|
|
163
|
-
|
|
164
|
-
// 1. Save default config if none exists
|
|
165
|
-
const existingConfig = loadConfig();
|
|
166
|
-
if (JSON.stringify(existingConfig) === JSON.stringify(DEFAULT_CONFIG)) {
|
|
167
|
-
saveConfig(DEFAULT_CONFIG);
|
|
168
|
-
console.log(c('green', ' Config created:') + ` ${getConfigFile()}`);
|
|
169
|
-
} else {
|
|
170
|
-
console.log(c('gray', ' Config exists:') + ` ${getConfigFile()}`);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// 2. Install Claude Code hook
|
|
174
|
-
const hookResult = installHook();
|
|
175
|
-
const hookIcon = hookResult.installed ? c('green', ' Hook:') : c('yellow', ' Hook:');
|
|
176
|
-
console.log(`${hookIcon} ${hookResult.message}`);
|
|
177
|
-
|
|
178
|
-
// 3. Install platform-specific scheduler
|
|
179
|
-
installScheduler(platform);
|
|
180
|
-
|
|
181
|
-
console.log();
|
|
159
|
+
pruneLogs(scanConfig);
|
|
182
160
|
}
|
|
183
161
|
|
|
184
162
|
/**
|
|
185
163
|
* status: Show current zombies and last cleanup info.
|
|
186
164
|
*/
|
|
187
165
|
function cmdStatus(config) {
|
|
188
|
-
const
|
|
166
|
+
const scanConfig = withPatternFlag(config);
|
|
167
|
+
if (!scanConfig) return;
|
|
168
|
+
const zombies = scan(scanConfig);
|
|
189
169
|
const logs = readLogs(100);
|
|
190
170
|
reportStatus(zombies, logs);
|
|
191
171
|
if (hasScanErrors(zombies)) process.exitCode = 1;
|
|
@@ -232,8 +212,10 @@ function cmdDoctor(config) {
|
|
|
232
212
|
}
|
|
233
213
|
|
|
234
214
|
function cmdAudit(config, commandName = 'audit') {
|
|
215
|
+
const scanConfig = withPatternFlag(config);
|
|
216
|
+
if (!scanConfig) return;
|
|
235
217
|
const sessionPid = parseSessionPidFlag();
|
|
236
|
-
const report = runAudit(
|
|
218
|
+
const report = runAudit(scanConfig, { json: Boolean(flags.json), sessionPid, commandName });
|
|
237
219
|
if (!report.risk.enumerationComplete) process.exitCode = 1;
|
|
238
220
|
}
|
|
239
221
|
|
|
@@ -257,6 +239,19 @@ function parseSessionPidFlag() {
|
|
|
257
239
|
process.exit(1);
|
|
258
240
|
}
|
|
259
241
|
|
|
242
|
+
function withPatternFlag(config) {
|
|
243
|
+
if (flags.pattern === undefined) return config;
|
|
244
|
+
const error = getCustomPatternError(flags.pattern);
|
|
245
|
+
if (error) {
|
|
246
|
+
console.error(c('red', ` ${error}`));
|
|
247
|
+
process.exitCode = 1;
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
const literal = normalizeCustomPattern(flags.pattern);
|
|
251
|
+
const configured = Array.isArray(config.customPatterns) ? config.customPatterns : [];
|
|
252
|
+
return { ...config, customPatterns: [...configured, literal] };
|
|
253
|
+
}
|
|
254
|
+
|
|
260
255
|
// ─── Help ───────────────────────────────────────────────────────────────────
|
|
261
256
|
|
|
262
257
|
function printHelp() {
|
|
@@ -285,6 +280,7 @@ function printHelp() {
|
|
|
285
280
|
${bold('Options:')}
|
|
286
281
|
--yes, -y Kill found zombies (default: dry-run)
|
|
287
282
|
--session-pid=PID Filter by parent session PID
|
|
283
|
+
--pattern=TEXT Add a literal orphan-process pattern
|
|
288
284
|
--path=DIR Workspace path for zclean cache
|
|
289
285
|
--json Print machine-readable output for supported commands
|
|
290
286
|
--version, -v Show version
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "z-clean",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "AI coding runtime hygiene reports, zombie cleanup, and safe workspace cache cleanup",
|
|
5
5
|
"bin": {
|
|
6
6
|
"zclean": "bin/zclean.js"
|
|
7
7
|
},
|
|
8
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",
|
|
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/install-experience.test.js test/reporter.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
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
11
|
"smoke": "node scripts/smoke.js",
|
|
12
|
-
"pack:check": "npm pack --dry-run --json"
|
|
12
|
+
"pack:check": "npm pack --dry-run --json",
|
|
13
|
+
"postinstall": "node scripts/postinstall.js"
|
|
13
14
|
},
|
|
14
15
|
"keywords": [
|
|
15
16
|
"claude-code",
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
"files": [
|
|
41
42
|
"bin/",
|
|
42
43
|
"src/",
|
|
44
|
+
"scripts/postinstall.js",
|
|
43
45
|
"assets/zclean-hero.png",
|
|
44
46
|
"LICENSE",
|
|
45
47
|
"README.md",
|
package/src/cli-brand.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { c, bold } = require('./reporter');
|
|
4
|
+
const { sanitizeTerminalText } = require('./terminal-text');
|
|
5
|
+
|
|
6
|
+
const WORDMARK = [
|
|
7
|
+
' ███████ ██████ ██ ███████ █████ ███ ██',
|
|
8
|
+
' ██ ██ ██ ██ ██ ██ ████ ██',
|
|
9
|
+
' ███ ██ ██ █████ ███████ ██ ██ ██',
|
|
10
|
+
' ██ ██ ██ ██ ██ ██ ██ ██ ██',
|
|
11
|
+
' ███████ ██████ ███████ ███████ ██ ██ ██ ████',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const RULE = ' --------------------------------------------------------';
|
|
15
|
+
const FRAME_WIDTH = 55;
|
|
16
|
+
|
|
17
|
+
function renderPostinstall({ version }) {
|
|
18
|
+
return [
|
|
19
|
+
'',
|
|
20
|
+
c('gray', ' . + .'),
|
|
21
|
+
c('gray', ' + . :'),
|
|
22
|
+
...WORDMARK.map((line) => c('cyan', line)),
|
|
23
|
+
'',
|
|
24
|
+
` ${bold('Z / CLEAN')} ${c('gray', `v${version}`)}`,
|
|
25
|
+
c('gray', ' AI CODING RUNTIME HYGIENE'),
|
|
26
|
+
c('gray', RULE),
|
|
27
|
+
'',
|
|
28
|
+
` ${c('green', 'INSTALLED')} z-clean`,
|
|
29
|
+
` ${c('gray', 'COMMAND')} zclean`,
|
|
30
|
+
` ${c('gray', 'SAFETY')} dry-run by default`,
|
|
31
|
+
'',
|
|
32
|
+
c('gray', ' Continue setup'),
|
|
33
|
+
` ${c('cyan', '$ zclean init')}`,
|
|
34
|
+
'',
|
|
35
|
+
c('gray', ' Inspect first. Clean only when you decide.'),
|
|
36
|
+
'',
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderInit({ version, steps, warningCount }) {
|
|
41
|
+
const lines = [
|
|
42
|
+
'',
|
|
43
|
+
c('gray', ' . + .'),
|
|
44
|
+
c('gray', ' + . :'),
|
|
45
|
+
...WORDMARK.map((line) => c('cyan', line)),
|
|
46
|
+
'',
|
|
47
|
+
` ${bold('Z / CLEAN')} ${c('gray', `v${version}`)}`,
|
|
48
|
+
c('gray', ' RUNTIME HYGIENE SYSTEM'),
|
|
49
|
+
'',
|
|
50
|
+
c('gray', ` +${'-- SETUP '.padEnd(FRAME_WIDTH, '-')}+`),
|
|
51
|
+
c('gray', ` |${''.padEnd(FRAME_WIDTH)}|`),
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
for (const step of steps) {
|
|
55
|
+
lines.push(renderStep(step));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const ready = warningCount === 0;
|
|
59
|
+
lines.push(
|
|
60
|
+
c('gray', ` |${''.padEnd(FRAME_WIDTH)}|`),
|
|
61
|
+
c('gray', ` +${''.padEnd(FRAME_WIDTH, '-')}+`),
|
|
62
|
+
'',
|
|
63
|
+
ready
|
|
64
|
+
? c('green', ' SYSTEM READY')
|
|
65
|
+
: c('yellow', ` SETUP COMPLETED WITH ${warningCount} WARNING${warningCount === 1 ? '' : 'S'}`),
|
|
66
|
+
'',
|
|
67
|
+
` ${c('gray', 'Inspect runtime leftovers')} ${c('cyan', 'zclean audit')}`,
|
|
68
|
+
` ${c('gray', 'Verify this installation')} ${c('cyan', 'zclean doctor')}`,
|
|
69
|
+
'',
|
|
70
|
+
c('gray', ' Cleanup remains locked until you pass --yes.'),
|
|
71
|
+
''
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function renderStep(step) {
|
|
78
|
+
const state = String(step.state).padEnd(10);
|
|
79
|
+
const label = String(step.label).padEnd(14);
|
|
80
|
+
const tone = step.state === 'WARNING' ? 'yellow' : step.state === 'EXISTS' ? 'gray' : 'green';
|
|
81
|
+
const statusPrefix = ` ${step.index} ${label} `;
|
|
82
|
+
const statusSpacing = ''.padEnd(FRAME_WIDTH - statusPrefix.length - state.length);
|
|
83
|
+
const detail = padToWidth(` ${fitDetail(step.detail)}`, FRAME_WIDTH);
|
|
84
|
+
return [
|
|
85
|
+
` |${c('gray', statusPrefix)}${c(tone, state)}${statusSpacing}|`,
|
|
86
|
+
` |${c('gray', detail)}|`,
|
|
87
|
+
].join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fitDetail(value) {
|
|
91
|
+
const text = sanitizeTerminalText(value).replace(/\s+/g, ' ').trim();
|
|
92
|
+
if (displayWidth(text) <= 45) return text;
|
|
93
|
+
let output = '';
|
|
94
|
+
let width = 0;
|
|
95
|
+
for (const char of text) {
|
|
96
|
+
const next = charWidth(char);
|
|
97
|
+
if (width + next > 42) break;
|
|
98
|
+
output += char;
|
|
99
|
+
width += next;
|
|
100
|
+
}
|
|
101
|
+
return `${output}...`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function padToWidth(value, width) {
|
|
105
|
+
return `${value}${''.padEnd(Math.max(0, width - displayWidth(value)))}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function displayWidth(value) {
|
|
109
|
+
return [...value].reduce((width, char) => width + charWidth(char), 0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function charWidth(char) {
|
|
113
|
+
if (/\p{Mark}/u.test(char)) return 0;
|
|
114
|
+
const code = char.codePointAt(0);
|
|
115
|
+
const wide = code >= 0x1100 && (
|
|
116
|
+
code <= 0x115f || code === 0x2329 || code === 0x232a ||
|
|
117
|
+
(code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
|
|
118
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
119
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
120
|
+
(code >= 0xfe10 && code <= 0xfe6f) ||
|
|
121
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
122
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
123
|
+
(code >= 0x1f300 && code <= 0x1f9ff) ||
|
|
124
|
+
(code >= 0x20000 && code <= 0x3fffd)
|
|
125
|
+
);
|
|
126
|
+
return wide ? 2 : 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { renderPostinstall, renderInit };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const { saveConfig, getConfigFile } = require('../config');
|
|
6
|
+
const { installHook } = require('../installer/hook');
|
|
7
|
+
const { installScheduler, getSchedulerState } = require('./scheduler');
|
|
8
|
+
const { renderInit } = require('../cli-brand');
|
|
9
|
+
const { version } = require('../../package.json');
|
|
10
|
+
|
|
11
|
+
function runInit({ config, platform }) {
|
|
12
|
+
const configFile = getConfigFile();
|
|
13
|
+
const configExists = fs.existsSync(configFile);
|
|
14
|
+
if (!configExists) saveConfig(config);
|
|
15
|
+
|
|
16
|
+
const hookResult = installHook();
|
|
17
|
+
const schedulerResult = installScheduler(platform);
|
|
18
|
+
const schedulerState = getSchedulerState(schedulerResult);
|
|
19
|
+
const steps = [
|
|
20
|
+
{
|
|
21
|
+
index: '01',
|
|
22
|
+
label: 'CONFIG',
|
|
23
|
+
state: configExists ? 'EXISTS' : 'READY',
|
|
24
|
+
detail: configFile.replace(os.homedir(), '~'),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
index: '02',
|
|
28
|
+
label: 'CLAUDE HOOK',
|
|
29
|
+
state: hookResult.installed ? 'INSTALLED' : 'WARNING',
|
|
30
|
+
detail: hookResult.installed ? 'SessionEnd cleanup hook registered' : hookResult.message,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
index: '03',
|
|
34
|
+
label: 'SCHEDULER',
|
|
35
|
+
state: schedulerState,
|
|
36
|
+
detail: schedulerState === 'ACTIVE' ? schedulerLabel(platform) : schedulerResult.message,
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const warningCount = steps.filter((step) => step.state === 'WARNING').length;
|
|
40
|
+
console.log(renderInit({ version, steps, warningCount }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function schedulerLabel(platform) {
|
|
44
|
+
if (platform === 'darwin') return 'Hourly launchd hygiene check';
|
|
45
|
+
if (platform === 'linux') return 'Hourly systemd hygiene timer';
|
|
46
|
+
if (platform === 'win32') return 'Hourly Task Scheduler check';
|
|
47
|
+
return 'Runtime hygiene scheduler';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { runInit };
|
|
@@ -6,53 +6,57 @@ function installScheduler(platform) {
|
|
|
6
6
|
switch (platform) {
|
|
7
7
|
case 'darwin': {
|
|
8
8
|
const { installLaunchd } = require('../installer/launchd');
|
|
9
|
-
|
|
10
|
-
const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
|
|
11
|
-
console.log(`${icon} ${result.message}`);
|
|
12
|
-
break;
|
|
9
|
+
return installLaunchd();
|
|
13
10
|
}
|
|
14
11
|
case 'linux': {
|
|
15
12
|
const { installSystemd } = require('../installer/systemd');
|
|
16
|
-
|
|
17
|
-
const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
|
|
18
|
-
console.log(`${icon} ${result.message}`);
|
|
19
|
-
break;
|
|
13
|
+
return installSystemd();
|
|
20
14
|
}
|
|
21
15
|
case 'win32': {
|
|
22
16
|
const { installTaskScheduler } = require('../installer/taskscheduler');
|
|
23
|
-
|
|
24
|
-
const icon = result.installed ? c('green', ' Scheduler:') : c('yellow', ' Scheduler:');
|
|
25
|
-
console.log(`${icon} ${result.message}`);
|
|
26
|
-
break;
|
|
17
|
+
return installTaskScheduler();
|
|
27
18
|
}
|
|
28
19
|
default:
|
|
29
|
-
|
|
20
|
+
return {
|
|
21
|
+
installed: false,
|
|
22
|
+
message: `Unsupported platform (${platform}). Install a cron job manually.`,
|
|
23
|
+
};
|
|
30
24
|
}
|
|
31
25
|
}
|
|
32
26
|
|
|
33
|
-
function uninstallScheduler(platform) {
|
|
27
|
+
function uninstallScheduler(platform, options = {}) {
|
|
28
|
+
const log = options.log || console.log;
|
|
29
|
+
let result;
|
|
34
30
|
switch (platform) {
|
|
35
31
|
case 'darwin': {
|
|
36
32
|
const { removeLaunchd } = require('../installer/launchd');
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
result = (options.remove || removeLaunchd)();
|
|
34
|
+
log(` Scheduler: ${result.message}`);
|
|
39
35
|
break;
|
|
40
36
|
}
|
|
41
37
|
case 'linux': {
|
|
42
38
|
const { removeSystemd } = require('../installer/systemd');
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
result = (options.remove || removeSystemd)();
|
|
40
|
+
log(` Scheduler: ${result.message}`);
|
|
45
41
|
break;
|
|
46
42
|
}
|
|
47
43
|
case 'win32': {
|
|
48
44
|
const { removeTaskScheduler } = require('../installer/taskscheduler');
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
result = (options.remove || removeTaskScheduler)();
|
|
46
|
+
log(` Scheduler: ${result.message}`);
|
|
51
47
|
break;
|
|
52
48
|
}
|
|
53
|
-
default:
|
|
54
|
-
|
|
49
|
+
default: {
|
|
50
|
+
result = { removed: false, failed: false, message: `Remove manually for ${platform}.` };
|
|
51
|
+
log(c('yellow', ` Scheduler: ${result.message}`));
|
|
52
|
+
}
|
|
55
53
|
}
|
|
54
|
+
if (result.failed) process.exitCode = 1;
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getSchedulerState(result) {
|
|
59
|
+
return result.installed && result.active !== false ? 'ACTIVE' : 'WARNING';
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
module.exports = { installScheduler, uninstallScheduler };
|
|
62
|
+
module.exports = { installScheduler, uninstallScheduler, getSchedulerState };
|