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 CHANGED
@@ -55,17 +55,36 @@ AI 코딩 도구는 작업 중 많은 임시 런타임을 만듭니다.
55
55
 
56
56
  ## 설치
57
57
 
58
+ 먼저 설치나 정리 없이 읽기 전용 audit을 실행하세요:
59
+
58
60
  ```bash
59
- npm install -g z-clean
60
- zclean init
61
+ npx --yes z-clean audit
61
62
  ```
62
63
 
63
- 설치 없이 1회 실행:
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
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-339933?style=flat-square&logo=node.js&logoColor=white)](#)
16
16
  [![Platform](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey?style=flat-square)](#platform-status)
17
17
  [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-blue?style=flat-square)](#)
18
- [![Tests](https://img.shields.io/badge/tests-101%20passing-brightgreen?style=flat-square)](#)
18
+ [![Tests](https://img.shields.io/badge/tests-142%20passing-brightgreen?style=flat-square)](#)
19
19
  [![GitHub stars](https://img.shields.io/github/stars/TheStack-ai/zclean?style=flat-square)](https://github.com/TheStack-ai/zclean)
20
20
  [![Mentioned in Awesome Claude Code Toolkit](https://awesome.re/mentioned-badge.svg)](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
- npm install -g z-clean
56
- zclean init
57
+ npx --yes z-clean audit
57
58
  ```
58
59
 
59
- One-off dry run without installation:
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
- Review before cleaning:
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
- npm install -g z-clean
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 { installHook, removeHook } = require('../src/installer/hook');
16
+ const { removeHook } = require('../src/installer/hook');
19
17
  const { runHistory, runProtect } = require('../src/commands/trust');
20
- const { installScheduler, uninstallScheduler } = require('../src/commands/scheduler');
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 [key, value] = arg.substring(2).split('=');
37
- flags[key] = value !== undefined ? value : true;
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 cmdInit(config);
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(config, { sessionPid });
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(config);
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, config);
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 (config.dryRunDefault === false) {
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(config);
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 zombies = scan(config);
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(config, { json: Boolean(flags.json), sessionPid, commandName });
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.1",
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",
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { version } = require('../package.json');
5
+ const { renderPostinstall } = require('../src/cli-brand');
6
+
7
+ process.stdout.write(renderPostinstall({ version }));
@@ -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
- const result = installLaunchd();
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
- const result = installSystemd();
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
- const result = installTaskScheduler();
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
- console.log(c('yellow', ` Scheduler: Unsupported platform (${platform}). Install a cron job manually.`));
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
- const result = removeLaunchd();
38
- console.log(` Scheduler: ${result.message}`);
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
- const result = removeSystemd();
44
- console.log(` Scheduler: ${result.message}`);
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
- const result = removeTaskScheduler();
50
- console.log(` Scheduler: ${result.message}`);
45
+ result = (options.remove || removeTaskScheduler)();
46
+ log(` Scheduler: ${result.message}`);
51
47
  break;
52
48
  }
53
- default:
54
- console.log(c('yellow', ` Scheduler: Remove manually for ${platform}.`));
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 };