orca-agent-cleanup 1.5.0 → 1.5.1

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
@@ -6,9 +6,9 @@
6
6
 
7
7
  남은 Orca 연결을 검사하고, 확인된 잔재를 복구 가능하게 격리합니다.
8
8
 
9
- [English](README.md) · **한국어**
9
+ [English](https://github.com/wilgon456/orca-agent-cleanup/blob/main/README.md) · **한국어**
10
10
 
11
- ![project: utility](https://img.shields.io/badge/project-utility-22d3ee?style=flat-square) ![stack: Node.js 18+](https://img.shields.io/badge/stack-Node.js%2018%2B-22d3ee?style=flat-square) [![License](https://img.shields.io/badge/license-MIT-22d3ee?style=flat-square)](LICENSE)
11
+ ![project: utility](https://img.shields.io/badge/project-utility-22d3ee?style=flat-square) ![stack: Node.js 18+](https://img.shields.io/badge/stack-Node.js%2018%2B-22d3ee?style=flat-square) [![License](https://img.shields.io/badge/license-MIT-22d3ee?style=flat-square)](https://github.com/wilgon456/orca-agent-cleanup/blob/main/LICENSE)
12
12
 
13
13
  [빠른 시작](#-빠른-시작) · [정리 범위](#-무엇을-정리하나요) · [복구](#-복구-방법)
14
14
 
@@ -427,7 +427,7 @@ Windows 전용 기존 도구의 회귀 테스트:
427
427
  .\tests\Invoke-SmokeTest.ps1
428
428
  ```
429
429
 
430
- 보안 취약점 신고 방법은 [SECURITY.md](SECURITY.md)를 참고하세요.
430
+ 보안 취약점 신고 방법은 [SECURITY.md](https://github.com/wilgon456/orca-agent-cleanup/blob/main/SECURITY.md)를 참고하세요.
431
431
 
432
432
  ## 🙋 자주 묻는 질문
433
433
 
@@ -458,7 +458,7 @@ Orca가 `~/.orca`에 저장한 암호화 음성 토큰과 Bitbucket·Linear·Jir
458
458
 
459
459
  ## 📄 라이선스
460
460
 
461
- [MIT License](LICENSE)로 배포합니다.
461
+ [MIT License](https://github.com/wilgon456/orca-agent-cleanup/blob/main/LICENSE)로 배포합니다.
462
462
 
463
463
  ---
464
464
 
package/README.md CHANGED
@@ -6,9 +6,9 @@
6
6
 
7
7
  Inspect leftover Orca integrations, quarantine verified residue and restore it when needed.
8
8
 
9
- **English** · [한국어](README.ko.md)
9
+ **English** · [한국어](https://github.com/wilgon456/orca-agent-cleanup/blob/main/README.ko.md)
10
10
 
11
- ![project: utility](https://img.shields.io/badge/project-utility-22d3ee?style=flat-square) ![stack: Node.js 18+](https://img.shields.io/badge/stack-Node.js%2018%2B-22d3ee?style=flat-square) [![License](https://img.shields.io/badge/license-MIT-22d3ee?style=flat-square)](LICENSE)
11
+ ![project: utility](https://img.shields.io/badge/project-utility-22d3ee?style=flat-square) ![stack: Node.js 18+](https://img.shields.io/badge/stack-Node.js%2018%2B-22d3ee?style=flat-square) [![License](https://img.shields.io/badge/license-MIT-22d3ee?style=flat-square)](https://github.com/wilgon456/orca-agent-cleanup/blob/main/LICENSE)
12
12
 
13
13
  [Quick start](#quick-start) · [Coverage](#cleanup-coverage) · [Restore](#restore)
14
14
 
@@ -180,8 +180,8 @@ The legacy Windows interface remains available:
180
180
  .\tests\Invoke-SmokeTest.ps1
181
181
  ```
182
182
 
183
- See [SECURITY.md](SECURITY.md) for vulnerability reporting. For bug reports, include the OS, whether Orca was uninstalled, a redacted scan result and the expected versus observed behavior.
183
+ See [SECURITY.md](https://github.com/wilgon456/orca-agent-cleanup/blob/main/SECURITY.md) for vulnerability reporting. For bug reports, include the OS, whether Orca was uninstalled, a redacted scan result and the expected versus observed behavior.
184
184
 
185
185
  ## License
186
186
 
187
- [MIT](LICENSE).
187
+ [MIT](https://github.com/wilgon456/orca-agent-cleanup/blob/main/LICENSE).
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "orca-agent-cleanup",
3
- "version": "1.5.0",
4
- "description": "Find and safely quarantine leftover Orca (stablyai/orca) skills, agent hooks, CLI launchers, credentials and app data after uninstalling Orca, with verified restore.",
5
- "keywords": [
6
- "orca",
7
- "stablyai",
8
- "uninstall",
9
- "cleanup",
10
- "claude-code",
11
- "codex",
12
- "agent-hooks",
13
- "skills"
14
- ],
15
- "license": "MIT",
16
- "author": "wilgon456",
17
- "homepage": "https://github.com/wilgon456/orca-agent-cleanup#readme",
18
- "repository": {
19
- "type": "git",
20
- "url": "git+https://github.com/wilgon456/orca-agent-cleanup.git"
21
- },
22
- "bugs": {
23
- "url": "https://github.com/wilgon456/orca-agent-cleanup/issues"
24
- },
25
- "type": "module",
26
- "bin": {
27
- "orca-agent-cleanup": "scripts/orca-agent-cleanup.mjs"
28
- },
29
- "files": [
30
- "scripts/orca-agent-cleanup.mjs",
31
- "scripts/orca-cleanup-core.mjs",
32
- "README.md",
33
- "README.ko.md",
34
- "LICENSE"
35
- ],
36
- "engines": {
37
- "node": ">=18"
38
- },
39
- "scripts": {
40
- "test": "node --test tests/orca-agent-cleanup.test.mjs"
41
- }
42
- }
1
+ {
2
+ "name": "orca-agent-cleanup",
3
+ "version": "1.5.1",
4
+ "description": "Find and safely quarantine leftover Orca (stablyai/orca) skills, agent hooks, CLI launchers, credentials and app data after uninstalling Orca, with verified restore.",
5
+ "keywords": [
6
+ "orca",
7
+ "stablyai",
8
+ "uninstall",
9
+ "cleanup",
10
+ "claude-code",
11
+ "codex",
12
+ "agent-hooks",
13
+ "skills"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "wilgon456",
17
+ "homepage": "https://github.com/wilgon456/orca-agent-cleanup#readme",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/wilgon456/orca-agent-cleanup.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/wilgon456/orca-agent-cleanup/issues"
24
+ },
25
+ "type": "module",
26
+ "bin": {
27
+ "orca-agent-cleanup": "scripts/orca-agent-cleanup.mjs"
28
+ },
29
+ "files": [
30
+ "scripts/orca-agent-cleanup.mjs",
31
+ "scripts/orca-cleanup-core.mjs",
32
+ "README.md",
33
+ "README.ko.md",
34
+ "LICENSE"
35
+ ],
36
+ "engines": {
37
+ "node": ">=18"
38
+ },
39
+ "scripts": {
40
+ "test": "node --test tests/orca-agent-cleanup.test.mjs"
41
+ }
42
+ }
@@ -1,221 +1,221 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'node:fs';
4
- import process from 'node:process';
5
- import {
6
- buildContext,
7
- cleanOrcaResidue,
8
- OFFICIAL_ORCA_SKILLS,
9
- restoreOrcaBackup,
10
- scanOrcaResidue,
11
- verifyOrcaBackup,
12
- } from './orca-cleanup-core.mjs';
13
-
14
- const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
15
-
16
- const HELP = `
17
- Orca Agent Cleanup v${VERSION} : Windows·macOS·Linux 공통 정리 도구
18
-
19
- 사용법:
20
- npx orca-agent-cleanup scan [옵션]
21
- npx orca-agent-cleanup clean [옵션]
22
- npx orca-agent-cleanup restore --manifest <manifest.json> [옵션]
23
- npx orca-agent-cleanup verify --manifest <manifest.json> [옵션]
24
-
25
- 저장소에서 직접 실행할 때는 npx orca-agent-cleanup 대신 node scripts/orca-agent-cleanup.mjs를 씁니다.
26
-
27
- 명령:
28
- scan 잔재를 검사합니다(기본 명령, 변경 없음).
29
- clean 확인된 잔재를 백업 폴더로 격리합니다.
30
- restore manifest에 기록된 파일과 설정을 복원합니다.
31
- verify manifest와 백업 내용의 무결성을 검사합니다.
32
-
33
- 옵션:
34
- --dry-run clean의 예정 작업만 표시합니다.
35
- --stop-processes 앱 없이 남아 도는 Orca 백그라운드 프로세스를 끕니다.
36
- --force Orca 앱이 설치·실행 중이어도 정리합니다(권장하지 않음).
37
- --include-app-data Orca 앱 데이터·캐시를 포함합니다.
38
- --include-voice-data 별도 음성 모델 캐시를 포함합니다.
39
- --include-user-state ~/.orca 전체(자격증명·세션 포함)를 포함합니다.
40
- --include-wsl 실행 중인 WSL 배포판의 Orca 잔재를 검사·정리합니다.
41
- --wsl-home <경로> WSL 홈 경로를 직접 추가합니다(반복 가능).
42
- --include-remote SSH 릴레이·원격 세션 잔재를 포함합니다.
43
- --remote-home <경로> 접근 가능한 원격 홈 경로를 추가합니다(반복 가능).
44
- --include-project-state --project의 .orca·worktree 휴지통을 포함합니다.
45
- --project <경로> 프로젝트 스킬 위치를 추가합니다(반복 가능).
46
- --voice-model-path <경로> 사용자 지정 음성 모델 경로를 포함합니다(반복 가능).
47
- --backup-root <경로> 격리·백업 폴더를 지정합니다.
48
- --manifest <경로> restore·verify에 사용할 manifest.json을 지정합니다.
49
- --json 결과를 JSON으로 출력합니다.
50
- -v, --version 버전을 표시합니다.
51
- -h, --help 도움말을 표시합니다.
52
-
53
- 예시:
54
- npx orca-agent-cleanup scan
55
- npx orca-agent-cleanup clean --dry-run --include-app-data --include-voice-data --stop-processes
56
- npx orca-agent-cleanup clean --include-app-data --include-voice-data --stop-processes
57
- npx orca-agent-cleanup restore --dry-run --manifest /path/to/manifest.json
58
- `;
59
-
60
- function parseArgs(argv) {
61
- const result = {
62
- command: 'scan', projects: [], customVoicePaths: [], wslHomes: [], remoteHomes: [], json: false,
63
- dryRun: false, force: false, stopProcesses: false, includeAppData: false, includeVoiceData: false,
64
- includeUserState: false, includeWsl: false, includeRemote: false, includeProjectState: false,
65
- };
66
- const args = [...argv];
67
- if (args[0] && !args[0].startsWith('-')) result.command = args.shift();
68
- while (args.length) {
69
- const arg = args.shift();
70
- if (arg === '--dry-run') result.dryRun = true;
71
- else if (arg === '--force') result.force = true;
72
- else if (arg === '--stop-processes') result.stopProcesses = true;
73
- else if (arg === '--include-app-data') result.includeAppData = true;
74
- else if (arg === '--include-voice-data') result.includeVoiceData = true;
75
- else if (arg === '--include-user-state') result.includeUserState = true;
76
- else if (arg === '--include-wsl') result.includeWsl = true;
77
- else if (arg === '--include-remote') result.includeRemote = true;
78
- else if (arg === '--include-project-state') result.includeProjectState = true;
79
- else if (arg === '--json') result.json = true;
80
- else if (arg === '-v' || arg === '--version') result.version = true;
81
- else if (arg === '-h' || arg === '--help') result.help = true;
82
- else if (arg === '--project') result.projects.push(requireValue(arg, args));
83
- else if (arg === '--wsl-home') result.wslHomes.push(requireValue(arg, args));
84
- else if (arg === '--remote-home') result.remoteHomes.push(requireValue(arg, args));
85
- else if (arg === '--voice-model-path') result.customVoicePaths.push(requireValue(arg, args));
86
- else if (arg === '--backup-root') result.backupRoot = requireValue(arg, args);
87
- else if (arg === '--manifest') result.manifestPath = requireValue(arg, args);
88
- else throw new Error(`알 수 없는 옵션입니다: ${arg}`);
89
- }
90
- if (!['scan', 'clean', 'restore', 'verify'].includes(result.command)) throw new Error(`알 수 없는 명령입니다: ${result.command}`);
91
- if (['restore', 'verify'].includes(result.command) && !result.manifestPath && !result.help && !result.version) {
92
- throw new Error(`${result.command}에는 --manifest <경로>가 필요합니다.`);
93
- }
94
- return result;
95
- }
96
-
97
- function requireValue(option, args) {
98
- const value = args.shift();
99
- if (!value || value.startsWith('--')) throw new Error(`${option} 뒤에 경로가 필요합니다.`);
100
- return value;
101
- }
102
-
103
- function icon(kind) {
104
- return ({ skill: '🧩', 'skill-lock': '🔒', hook: '🪝', 'shared-state': '🔗',
105
- 'skill-transaction': '🧹', extension: '🧱', 'app-data': '🗂️', 'voice-data': '🎙️',
106
- 'user-state': '🔐', 'wsl-state': '🐧', 'remote-state': '🌐', cli: '⌨️',
107
- 'project-state': '📁', 'app-installed': '⛔', 'app-running': '⛔', process: '⚙️',
108
- 'path-entry': '🛣️', unverified: '🛡️' })[kind] || '•';
109
- }
110
-
111
- function printScan(findings) {
112
- console.log('\nOrca Agent Cleanup 검사 결과\n');
113
- if (!findings.length) {
114
- console.log('✅ 확인된 Orca 잔재가 없습니다.');
115
- return;
116
- }
117
- for (const item of findings) {
118
- const option = item.requires ? ` [정리 시 --${item.requires.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)} 필요]` : '';
119
- console.log(`${icon(item.kind)} ${item.path}`);
120
- console.log(` ${item.reason}${option}`);
121
- }
122
- console.log(`\n총 ${findings.length}개가 확인되었습니다. 검사만 수행했으며 변경 사항은 없습니다.`);
123
- if (findings.some((item) => ['app-installed', 'app-running'].includes(item.kind))) {
124
- console.log('\n⛔ Orca 앱이 아직 설치돼 있거나 실행 중입니다. 앱이 켜질 때 훅을 다시 설치하므로 먼저 앱을 제거하세요. 그 전까지 clean은 멈춥니다.');
125
- }
126
- }
127
-
128
- function printClean(result) {
129
- console.log(`\nOrca Agent Cleanup ${result.dryRun ? '미리보기' : '정리 결과'}\n`);
130
- if (result.blocked && !result.dryRun) {
131
- console.log(`⛔ ${result.errors[0].message}`);
132
- return;
133
- }
134
- if (result.blocked) {
135
- console.log('⛔ Orca 앱이 아직 설치돼 있거나 실행 중입니다. 실제 clean은 앱을 제거하기 전까지 멈춥니다.\n');
136
- }
137
- if (!result.actions.length) {
138
- console.log('✅ 정리할 항목이 없습니다.');
139
- return;
140
- }
141
- const labels = {
142
- quarantined: '✅ 격리', edited: '✅ 수정', 'would-quarantine': '🔎 격리 예정',
143
- 'would-edit': '🔎 수정 예정', skipped: '⏭️ 옵션 제외', covered: '↳ 상위 상태에 포함',
144
- unchanged: '➖ 변경 없음', error: '❌ 오류',
145
- 'pending-quarantine': '⚠️ 격리 상태 확인 필요', 'pending-edit': '⚠️ 수정 상태 확인 필요',
146
- stopped: '✅ 프로세스 끔', 'would-stop': '🔎 프로세스 끌 예정',
147
- };
148
- for (const item of result.actions) console.log(`${labels[item.action] || item.action} ${item.path}`);
149
- if (!result.dryRun && result.actions.some((item) => ['quarantined', 'edited'].includes(item.action))) {
150
- console.log(`\n복구용 백업: ${result.backupRoot}`);
151
- }
152
- if (result.errors.length) console.log(`\n⚠️ ${result.errors.length}개 항목은 자동 처리하지 못했습니다. 위 오류를 확인하세요.`);
153
- }
154
-
155
- function printRestore(result) {
156
- console.log(`\nOrca Agent Cleanup ${result.dryRun ? '복원 미리보기' : '복원 결과'}\n`);
157
- if (result.cleanupStatus === 'in-progress') {
158
- console.log('⚠️ 중단된 정리 journal을 복구합니다. 해시 기록 전 중단된 항목은 백업 존재 여부로 판정합니다.\n');
159
- }
160
- if (!result.actions.length) {
161
- console.log('✅ manifest에 복원할 완료 작업이 없습니다.');
162
- return;
163
- }
164
- const labels = {
165
- restored: '✅ 복원', 'would-restore': '🔎 복원 예정',
166
- 'already-restored': '➖ 이미 복원됨', error: '❌ 오류',
167
- };
168
- for (const item of result.actions) {
169
- console.log(`${labels[item.action] || item.action} ${item.path}`);
170
- if (item.conflictBackup) console.log(` 현재 항목 보존: ${item.conflictBackup}`);
171
- }
172
- if (result.reportPath) console.log(`\n복원 보고서: ${result.reportPath}`);
173
- if (result.errors.length) console.log(`\n⚠️ ${result.errors.length}개 항목은 복원하지 못했습니다.`);
174
- }
175
-
176
- function printVerify(result) {
177
- console.log('\nOrca Agent Cleanup 백업 검증 결과\n');
178
- const labels = {
179
- verified: '✅ 검증됨', 'not-recorded': 'ℹ️ v1 해시 없음',
180
- 'not-applied': '➖ 미적용', 'recoverable-unverified': '⚠️ 중단 작업', error: '❌ 오류',
181
- };
182
- for (const item of result.actions) console.log(`${labels[item.status] || item.status} ${item.backup || item.path}`);
183
- console.log(result.valid ? '\n✅ manifest와 백업을 사용할 수 있습니다.' : `\n❌ ${result.errors.length}개 오류가 있습니다.`);
184
- }
185
-
186
- try {
187
- const options = parseArgs(process.argv.slice(2));
188
- if (options.version) {
189
- console.log(VERSION);
190
- process.exit(0);
191
- } else if (options.help) {
192
- console.log(HELP.trim());
193
- process.exit(0);
194
- }
195
- if (options.command === 'verify') {
196
- const result = verifyOrcaBackup(options.manifestPath, options);
197
- if (options.json) console.log(JSON.stringify(result, null, 2));
198
- else printVerify(result);
199
- if (!result.valid) process.exitCode = 2;
200
- } else if (options.command === 'restore') {
201
- const result = restoreOrcaBackup(options.manifestPath, options);
202
- if (options.json) console.log(JSON.stringify(result, null, 2));
203
- else printRestore(result);
204
- if (result.errors.length) process.exitCode = 2;
205
- } else if (options.command === 'scan') {
206
- const context = buildContext(options);
207
- const findings = scanOrcaResidue(context);
208
- if (options.json) console.log(JSON.stringify({ platform: context.platform, officialSkills: OFFICIAL_ORCA_SKILLS, findings }, null, 2));
209
- else printScan(findings);
210
- } else {
211
- const context = buildContext(options);
212
- const result = cleanOrcaResidue(context, options);
213
- if (options.json) console.log(JSON.stringify(result, null, 2));
214
- else printClean(result);
215
- if (result.errors.length) process.exitCode = 2;
216
- }
217
- } catch (error) {
218
- console.error(`오류: ${error.message}`);
219
- console.error('도움말: npx orca-agent-cleanup --help');
220
- process.exitCode = 1;
221
- }
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs';
4
+ import process from 'node:process';
5
+ import {
6
+ buildContext,
7
+ cleanOrcaResidue,
8
+ OFFICIAL_ORCA_SKILLS,
9
+ restoreOrcaBackup,
10
+ scanOrcaResidue,
11
+ verifyOrcaBackup,
12
+ } from './orca-cleanup-core.mjs';
13
+
14
+ const VERSION = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
15
+
16
+ const HELP = `
17
+ Orca Agent Cleanup v${VERSION} : Windows·macOS·Linux 공통 정리 도구
18
+
19
+ 사용법:
20
+ npx orca-agent-cleanup scan [옵션]
21
+ npx orca-agent-cleanup clean [옵션]
22
+ npx orca-agent-cleanup restore --manifest <manifest.json> [옵션]
23
+ npx orca-agent-cleanup verify --manifest <manifest.json> [옵션]
24
+
25
+ 저장소에서 직접 실행할 때는 npx orca-agent-cleanup 대신 node scripts/orca-agent-cleanup.mjs를 씁니다.
26
+
27
+ 명령:
28
+ scan 잔재를 검사합니다(기본 명령, 변경 없음).
29
+ clean 확인된 잔재를 백업 폴더로 격리합니다.
30
+ restore manifest에 기록된 파일과 설정을 복원합니다.
31
+ verify manifest와 백업 내용의 무결성을 검사합니다.
32
+
33
+ 옵션:
34
+ --dry-run clean의 예정 작업만 표시합니다.
35
+ --stop-processes 앱 없이 남아 도는 Orca 백그라운드 프로세스를 끕니다.
36
+ --force Orca 앱이 설치·실행 중이어도 정리합니다(권장하지 않음).
37
+ --include-app-data Orca 앱 데이터·캐시를 포함합니다.
38
+ --include-voice-data 별도 음성 모델 캐시를 포함합니다.
39
+ --include-user-state ~/.orca 전체(자격증명·세션 포함)를 포함합니다.
40
+ --include-wsl 실행 중인 WSL 배포판의 Orca 잔재를 검사·정리합니다.
41
+ --wsl-home <경로> WSL 홈 경로를 직접 추가합니다(반복 가능).
42
+ --include-remote SSH 릴레이·원격 세션 잔재를 포함합니다.
43
+ --remote-home <경로> 접근 가능한 원격 홈 경로를 추가합니다(반복 가능).
44
+ --include-project-state --project의 .orca·worktree 휴지통을 포함합니다.
45
+ --project <경로> 프로젝트 스킬 위치를 추가합니다(반복 가능).
46
+ --voice-model-path <경로> 사용자 지정 음성 모델 경로를 포함합니다(반복 가능).
47
+ --backup-root <경로> 격리·백업 폴더를 지정합니다.
48
+ --manifest <경로> restore·verify에 사용할 manifest.json을 지정합니다.
49
+ --json 결과를 JSON으로 출력합니다.
50
+ -v, --version 버전을 표시합니다.
51
+ -h, --help 도움말을 표시합니다.
52
+
53
+ 예시:
54
+ npx orca-agent-cleanup scan
55
+ npx orca-agent-cleanup clean --dry-run --include-app-data --include-voice-data --stop-processes
56
+ npx orca-agent-cleanup clean --include-app-data --include-voice-data --stop-processes
57
+ npx orca-agent-cleanup restore --dry-run --manifest /path/to/manifest.json
58
+ `;
59
+
60
+ function parseArgs(argv) {
61
+ const result = {
62
+ command: 'scan', projects: [], customVoicePaths: [], wslHomes: [], remoteHomes: [], json: false,
63
+ dryRun: false, force: false, stopProcesses: false, includeAppData: false, includeVoiceData: false,
64
+ includeUserState: false, includeWsl: false, includeRemote: false, includeProjectState: false,
65
+ };
66
+ const args = [...argv];
67
+ if (args[0] && !args[0].startsWith('-')) result.command = args.shift();
68
+ while (args.length) {
69
+ const arg = args.shift();
70
+ if (arg === '--dry-run') result.dryRun = true;
71
+ else if (arg === '--force') result.force = true;
72
+ else if (arg === '--stop-processes') result.stopProcesses = true;
73
+ else if (arg === '--include-app-data') result.includeAppData = true;
74
+ else if (arg === '--include-voice-data') result.includeVoiceData = true;
75
+ else if (arg === '--include-user-state') result.includeUserState = true;
76
+ else if (arg === '--include-wsl') result.includeWsl = true;
77
+ else if (arg === '--include-remote') result.includeRemote = true;
78
+ else if (arg === '--include-project-state') result.includeProjectState = true;
79
+ else if (arg === '--json') result.json = true;
80
+ else if (arg === '-v' || arg === '--version') result.version = true;
81
+ else if (arg === '-h' || arg === '--help') result.help = true;
82
+ else if (arg === '--project') result.projects.push(requireValue(arg, args));
83
+ else if (arg === '--wsl-home') result.wslHomes.push(requireValue(arg, args));
84
+ else if (arg === '--remote-home') result.remoteHomes.push(requireValue(arg, args));
85
+ else if (arg === '--voice-model-path') result.customVoicePaths.push(requireValue(arg, args));
86
+ else if (arg === '--backup-root') result.backupRoot = requireValue(arg, args);
87
+ else if (arg === '--manifest') result.manifestPath = requireValue(arg, args);
88
+ else throw new Error(`알 수 없는 옵션입니다: ${arg}`);
89
+ }
90
+ if (!['scan', 'clean', 'restore', 'verify'].includes(result.command)) throw new Error(`알 수 없는 명령입니다: ${result.command}`);
91
+ if (['restore', 'verify'].includes(result.command) && !result.manifestPath && !result.help && !result.version) {
92
+ throw new Error(`${result.command}에는 --manifest <경로>가 필요합니다.`);
93
+ }
94
+ return result;
95
+ }
96
+
97
+ function requireValue(option, args) {
98
+ const value = args.shift();
99
+ if (!value || value.startsWith('--')) throw new Error(`${option} 뒤에 경로가 필요합니다.`);
100
+ return value;
101
+ }
102
+
103
+ function icon(kind) {
104
+ return ({ skill: '🧩', 'skill-lock': '🔒', hook: '🪝', 'shared-state': '🔗',
105
+ 'skill-transaction': '🧹', extension: '🧱', 'app-data': '🗂️', 'voice-data': '🎙️',
106
+ 'user-state': '🔐', 'wsl-state': '🐧', 'remote-state': '🌐', cli: '⌨️',
107
+ 'project-state': '📁', 'app-installed': '⛔', 'app-running': '⛔', process: '⚙️',
108
+ 'path-entry': '🛣️', unverified: '🛡️' })[kind] || '•';
109
+ }
110
+
111
+ function printScan(findings) {
112
+ console.log('\nOrca Agent Cleanup 검사 결과\n');
113
+ if (!findings.length) {
114
+ console.log('✅ 확인된 Orca 잔재가 없습니다.');
115
+ return;
116
+ }
117
+ for (const item of findings) {
118
+ const option = item.requires ? ` [정리 시 --${item.requires.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`)} 필요]` : '';
119
+ console.log(`${icon(item.kind)} ${item.path}`);
120
+ console.log(` ${item.reason}${option}`);
121
+ }
122
+ console.log(`\n총 ${findings.length}개가 확인되었습니다. 검사만 수행했으며 변경 사항은 없습니다.`);
123
+ if (findings.some((item) => ['app-installed', 'app-running'].includes(item.kind))) {
124
+ console.log('\n⛔ Orca 앱이 아직 설치돼 있거나 실행 중입니다. 앱이 켜질 때 훅을 다시 설치하므로 먼저 앱을 제거하세요. 그 전까지 clean은 멈춥니다.');
125
+ }
126
+ }
127
+
128
+ function printClean(result) {
129
+ console.log(`\nOrca Agent Cleanup ${result.dryRun ? '미리보기' : '정리 결과'}\n`);
130
+ if (result.blocked && !result.dryRun) {
131
+ console.log(`⛔ ${result.errors[0].message}`);
132
+ return;
133
+ }
134
+ if (result.blocked) {
135
+ console.log('⛔ Orca 앱이 아직 설치돼 있거나 실행 중입니다. 실제 clean은 앱을 제거하기 전까지 멈춥니다.\n');
136
+ }
137
+ if (!result.actions.length) {
138
+ console.log('✅ 정리할 항목이 없습니다.');
139
+ return;
140
+ }
141
+ const labels = {
142
+ quarantined: '✅ 격리', edited: '✅ 수정', 'would-quarantine': '🔎 격리 예정',
143
+ 'would-edit': '🔎 수정 예정', skipped: '⏭️ 옵션 제외', covered: '↳ 상위 상태에 포함',
144
+ unchanged: '➖ 변경 없음', error: '❌ 오류',
145
+ 'pending-quarantine': '⚠️ 격리 상태 확인 필요', 'pending-edit': '⚠️ 수정 상태 확인 필요',
146
+ stopped: '✅ 프로세스 끔', 'would-stop': '🔎 프로세스 끌 예정',
147
+ };
148
+ for (const item of result.actions) console.log(`${labels[item.action] || item.action} ${item.path}`);
149
+ if (!result.dryRun && result.actions.some((item) => ['quarantined', 'edited'].includes(item.action))) {
150
+ console.log(`\n복구용 백업: ${result.backupRoot}`);
151
+ }
152
+ if (result.errors.length) console.log(`\n⚠️ ${result.errors.length}개 항목은 자동 처리하지 못했습니다. 위 오류를 확인하세요.`);
153
+ }
154
+
155
+ function printRestore(result) {
156
+ console.log(`\nOrca Agent Cleanup ${result.dryRun ? '복원 미리보기' : '복원 결과'}\n`);
157
+ if (result.cleanupStatus === 'in-progress') {
158
+ console.log('⚠️ 중단된 정리 journal을 복구합니다. 해시 기록 전 중단된 항목은 백업 존재 여부로 판정합니다.\n');
159
+ }
160
+ if (!result.actions.length) {
161
+ console.log('✅ manifest에 복원할 완료 작업이 없습니다.');
162
+ return;
163
+ }
164
+ const labels = {
165
+ restored: '✅ 복원', 'would-restore': '🔎 복원 예정',
166
+ 'already-restored': '➖ 이미 복원됨', error: '❌ 오류',
167
+ };
168
+ for (const item of result.actions) {
169
+ console.log(`${labels[item.action] || item.action} ${item.path}`);
170
+ if (item.conflictBackup) console.log(` 현재 항목 보존: ${item.conflictBackup}`);
171
+ }
172
+ if (result.reportPath) console.log(`\n복원 보고서: ${result.reportPath}`);
173
+ if (result.errors.length) console.log(`\n⚠️ ${result.errors.length}개 항목은 복원하지 못했습니다.`);
174
+ }
175
+
176
+ function printVerify(result) {
177
+ console.log('\nOrca Agent Cleanup 백업 검증 결과\n');
178
+ const labels = {
179
+ verified: '✅ 검증됨', 'not-recorded': 'ℹ️ v1 해시 없음',
180
+ 'not-applied': '➖ 미적용', 'recoverable-unverified': '⚠️ 중단 작업', error: '❌ 오류',
181
+ };
182
+ for (const item of result.actions) console.log(`${labels[item.status] || item.status} ${item.backup || item.path}`);
183
+ console.log(result.valid ? '\n✅ manifest와 백업을 사용할 수 있습니다.' : `\n❌ ${result.errors.length}개 오류가 있습니다.`);
184
+ }
185
+
186
+ try {
187
+ const options = parseArgs(process.argv.slice(2));
188
+ if (options.version) {
189
+ console.log(VERSION);
190
+ process.exit(0);
191
+ } else if (options.help) {
192
+ console.log(HELP.trim());
193
+ process.exit(0);
194
+ }
195
+ if (options.command === 'verify') {
196
+ const result = verifyOrcaBackup(options.manifestPath, options);
197
+ if (options.json) console.log(JSON.stringify(result, null, 2));
198
+ else printVerify(result);
199
+ if (!result.valid) process.exitCode = 2;
200
+ } else if (options.command === 'restore') {
201
+ const result = restoreOrcaBackup(options.manifestPath, options);
202
+ if (options.json) console.log(JSON.stringify(result, null, 2));
203
+ else printRestore(result);
204
+ if (result.errors.length) process.exitCode = 2;
205
+ } else if (options.command === 'scan') {
206
+ const context = buildContext(options);
207
+ const findings = scanOrcaResidue(context);
208
+ if (options.json) console.log(JSON.stringify({ platform: context.platform, officialSkills: OFFICIAL_ORCA_SKILLS, findings }, null, 2));
209
+ else printScan(findings);
210
+ } else {
211
+ const context = buildContext(options);
212
+ const result = cleanOrcaResidue(context, options);
213
+ if (options.json) console.log(JSON.stringify(result, null, 2));
214
+ else printClean(result);
215
+ if (result.errors.length) process.exitCode = 2;
216
+ }
217
+ } catch (error) {
218
+ console.error(`오류: ${error.message}`);
219
+ console.error('도움말: npx orca-agent-cleanup --help');
220
+ process.exitCode = 1;
221
+ }