viruagent-cli 0.3.4 → 0.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ const readNaverCredentials = () => {
2
+ const username = process.env.NAVER_USERNAME || process.env.NAVER_USER || process.env.NAVER_ID;
3
+ const password = process.env.NAVER_PASSWORD || process.env.NAVER_PW;
4
+ return {
5
+ username: typeof username === 'string' && username.trim() ? username.trim() : null,
6
+ password: typeof password === 'string' && password.trim() ? password.trim() : null,
7
+ };
8
+ };
9
+
10
+ const parseNaverSessionError = (error) => {
11
+ const message = String(error?.message || '').toLowerCase();
12
+ return [
13
+ '세션 파일이 없습니다',
14
+ '세션에 유효한 쿠키',
15
+ '세션이 만료',
16
+ '로그인이 필요합니다',
17
+ 'blogid를 찾을 수 없습니다',
18
+ '블로그 정보 조회 실패',
19
+ '다시 로그인',
20
+ '401',
21
+ '403',
22
+ ].some((token) => message.includes(token.toLowerCase()));
23
+ };
24
+
25
+ const buildLoginErrorMessage = (error) => String(error?.message || '세션 검증에 실패했습니다.');
26
+
27
+ const normalizeNaverTagList = (value = '') => {
28
+ const source = Array.isArray(value)
29
+ ? value
30
+ : String(value || '').replace(/\r?\n/g, ',').split(',');
31
+ return source
32
+ .map((tag) => String(tag || '').trim())
33
+ .filter(Boolean)
34
+ .map((tag) => tag.replace(/["']/g, '').trim())
35
+ .filter(Boolean)
36
+ .slice(0, 10)
37
+ .join(',');
38
+ };
39
+
40
+ const mapNaverVisibility = (visibility) => {
41
+ const normalized = String(visibility || 'public').toLowerCase();
42
+ if (normalized === 'private') return 0;
43
+ if (normalized === 'protected' || normalized === 'mutual') return 1;
44
+ return 2; // public (openType: 2)
45
+ };
46
+
47
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
48
+
49
+ module.exports = {
50
+ readNaverCredentials,
51
+ parseNaverSessionError,
52
+ buildLoginErrorMessage,
53
+ normalizeNaverTagList,
54
+ mapNaverVisibility,
55
+ sleep,
56
+ };
@@ -192,6 +192,19 @@ const findWindowsChromePath = () => {
192
192
  return candidates.find(p => fs.existsSync(p)) || null;
193
193
  };
194
194
 
195
+ const findMacChromePath = () => {
196
+ const candidates = [
197
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
198
+ path.join(os.homedir(), 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
199
+ ];
200
+ return candidates.find(p => fs.existsSync(p)) || null;
201
+ };
202
+
203
+ const findChromePath = () => {
204
+ if (process.platform === 'win32') return findWindowsChromePath();
205
+ return findMacChromePath();
206
+ };
207
+
195
208
  const generateSelfSignedCert = (domain) => {
196
209
  const tempDir = path.join(os.tmpdir(), `viruagent-cert-${Date.now()}`);
197
210
  fs.mkdirSync(tempDir, { recursive: true });
@@ -245,19 +258,28 @@ const findChromeDebugPort = async () => {
245
258
  const ws = await tryConnectCDP(CDP_DEBUG_PORT);
246
259
  if (ws) return { port: CDP_DEBUG_PORT, wsUrl: ws };
247
260
 
248
- // 2. DevToolsActivePort 파일 확인
249
- const dtpPath = path.join(
250
- process.env.LOCALAPPDATA || '',
251
- 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'
252
- );
253
- try {
254
- const content = fs.readFileSync(dtpPath, 'utf-8').trim();
255
- const port = parseInt(content.split('\n')[0], 10);
256
- if (port > 0) {
257
- const ws2 = await tryConnectCDP(port);
258
- if (ws2) return { port, wsUrl: ws2 };
259
- }
260
- } catch {}
261
+ // 2. DevToolsActivePort 파일 확인 (Windows/macOS 모두)
262
+ const dtpPaths = [];
263
+ if (process.platform === 'win32') {
264
+ dtpPaths.push(path.join(
265
+ process.env.LOCALAPPDATA || '',
266
+ 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'
267
+ ));
268
+ } else {
269
+ dtpPaths.push(path.join(
270
+ os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'
271
+ ));
272
+ }
273
+ for (const dtpPath of dtpPaths) {
274
+ try {
275
+ const content = fs.readFileSync(dtpPath, 'utf-8').trim();
276
+ const port = parseInt(content.split('\n')[0], 10);
277
+ if (port > 0) {
278
+ const ws2 = await tryConnectCDP(port);
279
+ if (ws2) return { port, wsUrl: ws2 };
280
+ }
281
+ } catch {}
282
+ }
261
283
 
262
284
  return null;
263
285
  };
@@ -440,10 +462,32 @@ const getOrCreateJunctionPath = (chromeRoot) => {
440
462
  return junctionPath;
441
463
  };
442
464
 
465
+ const gracefulKillChrome = async () => {
466
+ try {
467
+ if (process.platform === 'win32') {
468
+ execSync('cmd /c "taskkill /IM chrome.exe"', { stdio: 'ignore', timeout: 10000 });
469
+ } else {
470
+ // macOS: SIGTERM으로 graceful 종료
471
+ execSync('pkill -TERM "Google Chrome" 2>/dev/null || true', { stdio: 'ignore', timeout: 10000 });
472
+ }
473
+ } catch {}
474
+ await new Promise(r => setTimeout(r, 2000));
475
+ if (isChromeRunning()) {
476
+ try {
477
+ if (process.platform === 'win32') {
478
+ execSync('cmd /c "taskkill /F /IM chrome.exe"', { stdio: 'ignore', timeout: 5000 });
479
+ } else {
480
+ execSync('pkill -9 "Google Chrome" 2>/dev/null || true', { stdio: 'ignore', timeout: 5000 });
481
+ }
482
+ } catch {}
483
+ await new Promise(r => setTimeout(r, 1000));
484
+ }
485
+ };
486
+
443
487
  const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName) => {
444
488
  // Chrome 실행 중: CDP(Chrome DevTools Protocol)로 쿠키 추출
445
489
  // 1단계: 이미 디버그 포트가 열려있으면 바로 연결 (크롬 종료 없음)
446
- // 2단계: 없으면 한 번만 재시작 + 바로가기 수정 (이후 재시작 불필요)
490
+ // 2단계: 없으면 한 번만 재시작 (Windows: 바로가기 수정, macOS: 직접 재시작)
447
491
  const { spawn } = require('child_process');
448
492
 
449
493
  // 1. 이미 디버그 포트가 열려있는지 확인
@@ -453,42 +497,39 @@ const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName)
453
497
  return await extractCookiesFromCDP(existing.port, targetSessionPath);
454
498
  }
455
499
 
456
- // 2. 디버그 포트 없음 → Chrome 바로가기에 디버그 포트 추가 (이후 재시작 불필요)
457
- console.log('[chrome-cdp] Chrome 디버그 포트 미감지 — 바로가기에 --remote-debugging-port 추가 중...');
458
- const shortcutModified = enableChromeDebugPort();
459
- if (shortcutModified) {
460
- console.log('[chrome-cdp] Chrome 바로가기 수정 완료 — 다음부터는 크롬 종료 없이 쿠키 추출 가능');
500
+ // 2. 디버그 포트 없음 → Windows만 바로가기 수정 (macOS는 바로가기 개념 없음)
501
+ if (process.platform === 'win32') {
502
+ console.log('[chrome-cdp] Chrome 디버그 포트 미감지 — 바로가기에 --remote-debugging-port 추가 중...');
503
+ const shortcutModified = enableChromeDebugPort();
504
+ if (shortcutModified) {
505
+ console.log('[chrome-cdp] Chrome 바로가기 수정 완료 — 다음부터는 크롬 종료 없이 쿠키 추출 가능');
506
+ }
461
507
  }
462
508
 
463
- // 3. Chrome graceful하게 종료하고 디버그 포트로 재시작 (최초 1회만)
464
- const chromePath = findWindowsChromePath();
509
+ // 3. Chrome 경로 확인
510
+ const chromePath = findChromePath();
465
511
  if (!chromePath) throw new Error('Chrome 실행 파일을 찾을 수 없습니다.');
466
512
 
513
+ // 4. Chrome을 graceful하게 종료하고 디버그 포트로 재시작 (최초 1회만)
467
514
  console.log('[chrome-cdp] Chrome을 디버그 포트와 함께 재시작합니다 (탭 자동 복원)...');
468
- try {
469
- if (process.platform === 'win32') {
470
- execSync('cmd /c "taskkill /IM chrome.exe"', { stdio: 'ignore', timeout: 10000 });
471
- }
472
- } catch {}
473
- await new Promise(r => setTimeout(r, 2000));
474
- if (isChromeRunning()) {
475
- try { execSync('cmd /c "taskkill /F /IM chrome.exe"', { stdio: 'ignore', timeout: 5000 }); } catch {}
476
- await new Promise(r => setTimeout(r, 1000));
477
- }
478
-
479
- // 4. Junction 경로로 디버그 포트 + 세션 복원 재시작
480
- // Chrome 145+는 기본 user-data-dir에서 디버그 포트를 거부하므로 junction으로 우회
481
- const junctionRoot = getOrCreateJunctionPath(chromeRoot);
515
+ await gracefulKillChrome();
516
+
517
+ // 5. 디버그 포트 + 세션 복원 재시작
518
+ // Windows Chrome 145+: Junction으로 user-data-dir 우회
519
+ // macOS: user-data-dir 직접 지정
520
+ const userDataDir = process.platform === 'win32'
521
+ ? getOrCreateJunctionPath(chromeRoot)
522
+ : chromeRoot;
482
523
  const chromeProc = spawn(chromePath, [
483
524
  `--remote-debugging-port=${CDP_DEBUG_PORT}`,
484
525
  '--remote-allow-origins=*',
485
526
  '--restore-last-session',
486
- `--user-data-dir=${junctionRoot}`,
527
+ `--user-data-dir=${userDataDir}`,
487
528
  `--profile-directory=${profileName}`,
488
529
  ], { detached: true, stdio: 'ignore' });
489
530
  chromeProc.unref();
490
531
 
491
- // 5. CDP 연결 대기
532
+ // 6. CDP 연결 대기
492
533
  let connected = null;
493
534
  const maxWait = 15000;
494
535
  const start = Date.now();
@@ -499,7 +540,7 @@ const extractCookiesViaCDP = async (targetSessionPath, chromeRoot, profileName)
499
540
  }
500
541
  if (!connected) throw new Error('Chrome 디버그 포트 연결 시간 초과');
501
542
 
502
- // 6. 쿠키 추출 (Chrome은 계속 실행 상태 유지 — 종료하지 않음)
543
+ // 7. 쿠키 추출 (Chrome은 계속 실행 상태 유지 — 종료하지 않음)
503
544
  return await extractCookiesFromCDP(connected.port, targetSessionPath);
504
545
  };
505
546
 
@@ -676,12 +717,16 @@ const importSessionFromChrome = async (targetSessionPath, profileName = 'Default
676
717
  // 3) 카카오 세션 쿠키가 있으면 Playwright에 주입 후 자동 로그인
677
718
  const hasKakaoSession = kakaoCookies.some(c => c.domain.includes('kakao.com') && (c.name === '_kawlt' || c.name === '_kawltea' || c.name === '_karmt'));
678
719
  if (!hasKakaoSession) {
679
- // Windows v20 App Bound Encryption: DPAPI만으로 복호화 불가
680
- // Playwright persistent context (pipe 모드) Chrome 기본 프로필에서 직접 추출
720
+ // 쿠키 복호화로 세션을 얻을 없는 경우 → CDP로 Chrome에서 직접 추출
721
+ if (isChromeRunning()) {
722
+ return await extractCookiesViaCDP(targetSessionPath, chromeRoot, profileName);
723
+ }
724
+ // Windows: Chrome이 꺼져있으면 DirectLaunch 방식으로 추출
681
725
  if (process.platform === 'win32') {
682
726
  return await importSessionViaChromeDirectLaunch(targetSessionPath, chromeRoot, profileName);
683
727
  }
684
- throw new Error('Chrome 카카오 로그인 세션이 없습니다. Chrome에서 먼저 카카오 계정에 로그인해 주세요.');
728
+ // macOS: Chrome 꺼져있으면 CDP로 재시작하여 추출
729
+ return await extractCookiesViaCDP(targetSessionPath, chromeRoot, profileName);
685
730
  }
686
731
 
687
732
  const browser = await chromium.launch({ headless: true });