@clazic/urban 0.2.19 → 0.2.21

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clazic/urban",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
4
4
  "description": "도시계획연구 보고서 자동 수집·지식베이스 데몬",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,16 +1,13 @@
1
1
  #!/usr/bin/env node
2
- // postinstall: graphify 설치 → urban 자동 설정 → 데몬 등록 + 시작
2
+ // postinstall: urban 자동 설정 → poppler 설치 → 데몬 등록 + 시작
3
3
  // 모든 단계는 실패해도 설치 자체를 중단하지 않음 (exit 0 보장)
4
4
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
5
5
  import { homedir } from 'os';
6
- import { join, dirname } from 'path';
7
- import { fileURLToPath } from 'url';
8
-
9
- const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ import { join } from 'path';
10
7
 
11
8
  try {
12
- // ── 1. graphify 설치 (선택 기능, 실패 무시) ─────────────────────────────────
13
- await import('./setup-graphify.js').catch(() => {});
9
+ // ── 1. poppler 설치 (Windows, 선택 기능, 실패 무시) ───────────────────────────
10
+ await import('./setup-poppler.js').catch(() => {});
14
11
 
15
12
  // ── 2. urban 자동 설정 (기본값으로 .env 생성) ─────────────────────────────────
16
13
  const URBAN_HOME = process.env.URBAN_HOME ?? join(homedir(), '.urban');
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // setup-poppler.js: Windows에서 poppler (pdftoppm) 자동 설치
3
+ // 1) winget 시도 → 2) GitHub 릴리즈 직접 다운로드 폴백
4
+ // 실패해도 설치 자체를 중단하지 않음 (exit 0 보장)
5
+
6
+ import { execSync } from 'child_process';
7
+ import { existsSync, mkdirSync, rmSync, writeFileSync, readdirSync } from 'fs';
8
+ import { createWriteStream } from 'fs';
9
+ import { homedir, tmpdir } from 'os';
10
+ import { join } from 'path';
11
+ import https from 'https';
12
+
13
+ if (process.platform !== 'win32') process.exit(0);
14
+
15
+ const URBAN_HOME = process.env.URBAN_HOME ?? join(homedir(), '.urban');
16
+ const POPPLER_DIR = join(URBAN_HOME, 'bin', 'poppler');
17
+ const BIN_PATH_FILE = join(URBAN_HOME, 'bin', 'poppler-bin-path.txt');
18
+
19
+ /** 재귀적으로 pdftoppm.exe가 있는 폴더 탐색 */
20
+ function findBinDir(dir) {
21
+ if (!existsSync(dir)) return null;
22
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
+ const full = join(dir, entry.name);
24
+ if (entry.isDirectory()) {
25
+ const found = findBinDir(full);
26
+ if (found) return found;
27
+ } else if (entry.name.toLowerCase() === 'pdftoppm.exe') {
28
+ return dir;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /** 시스템 PATH 또는 로컬 설치 여부 확인 */
35
+ function hasPdftoppm() {
36
+ try { execSync('where pdftoppm', { stdio: 'ignore', shell: true }); return true; } catch {}
37
+ return findBinDir(POPPLER_DIR) !== null;
38
+ }
39
+
40
+ /** GitHub API에서 최신 릴리즈 zip URL 조회 */
41
+ function getLatestZipUrl() {
42
+ return new Promise((resolve, reject) => {
43
+ https.get({
44
+ hostname: 'api.github.com',
45
+ path: '/repos/oschwartz10612/poppler-windows/releases/latest',
46
+ headers: { 'User-Agent': 'urban-postinstall' },
47
+ }, (res) => {
48
+ let data = '';
49
+ res.on('data', d => data += d);
50
+ res.on('end', () => {
51
+ try {
52
+ const json = JSON.parse(data);
53
+ const asset = json.assets?.find(a => a.name.endsWith('.zip'));
54
+ resolve(asset?.browser_download_url ?? null);
55
+ } catch (e) { reject(e); }
56
+ });
57
+ }).on('error', reject);
58
+ });
59
+ }
60
+
61
+ /** HTTPS 다운로드 (리다이렉트 최대 5회) */
62
+ function download(url, dest, hops = 5) {
63
+ return new Promise((resolve, reject) => {
64
+ if (hops <= 0) { reject(new Error('Too many redirects')); return; }
65
+ https.get(url, { headers: { 'User-Agent': 'urban-postinstall' } }, (res) => {
66
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
67
+ download(res.headers.location, dest, hops - 1).then(resolve, reject);
68
+ return;
69
+ }
70
+ if (res.statusCode !== 200) { reject(new Error(`HTTP ${res.statusCode}`)); return; }
71
+ const file = createWriteStream(dest);
72
+ res.pipe(file);
73
+ file.on('finish', () => file.close(resolve));
74
+ file.on('error', reject);
75
+ }).on('error', reject);
76
+ });
77
+ }
78
+
79
+ /** unzipper로 압축 해제 */
80
+ async function extractZip(zipPath, destDir) {
81
+ const { default: unzipper } = await import('unzipper');
82
+ const { createReadStream } = await import('fs');
83
+ mkdirSync(destDir, { recursive: true });
84
+ await createReadStream(zipPath)
85
+ .pipe(unzipper.Extract({ path: destDir }))
86
+ .promise();
87
+ }
88
+
89
+ async function main() {
90
+ if (hasPdftoppm()) {
91
+ console.log('[urban] pdftoppm 이미 설치됨 — 건너뜀');
92
+ // 로컬 설치 경로가 있으면 경로 파일 업데이트
93
+ const binDir = findBinDir(POPPLER_DIR);
94
+ if (binDir) {
95
+ mkdirSync(join(URBAN_HOME, 'bin'), { recursive: true });
96
+ writeFileSync(BIN_PATH_FILE, binDir, 'utf8');
97
+ }
98
+ return;
99
+ }
100
+
101
+ console.log('[urban] poppler (pdftoppm) 설치 중...');
102
+
103
+ // ── 1. winget 시도 ─────────────────────────────────────────────────
104
+ try {
105
+ execSync(
106
+ 'winget install --exact --silent --accept-package-agreements --accept-source-agreements "oschwartz10612.poppler"',
107
+ { stdio: 'pipe', shell: true, timeout: 120_000 }
108
+ );
109
+ console.log('[urban] poppler 설치 완료 (winget)');
110
+ return;
111
+ } catch {
112
+ console.log('[urban] winget 실패 → GitHub 릴리즈에서 직접 다운로드...');
113
+ }
114
+
115
+ // ── 2. GitHub 릴리즈 직접 다운로드 ────────────────────────────────
116
+ const url = await getLatestZipUrl();
117
+ if (!url) throw new Error('GitHub 릴리즈 URL을 가져오지 못했습니다');
118
+
119
+ console.log(`[urban] 다운로드 중: ${url}`);
120
+ const zipPath = join(tmpdir(), 'poppler-windows.zip');
121
+ await download(url, zipPath);
122
+
123
+ console.log('[urban] 압축 해제 중...');
124
+ await extractZip(zipPath, POPPLER_DIR);
125
+ try { rmSync(zipPath); } catch {}
126
+
127
+ // pdftoppm.exe 위치 탐색
128
+ const binDir = findBinDir(POPPLER_DIR);
129
+ if (!binDir) throw new Error('압축 해제 후 pdftoppm.exe를 찾을 수 없습니다');
130
+
131
+ // 데몬 시작 스크립트가 PATH에 주입할 수 있도록 경로 저장
132
+ mkdirSync(join(URBAN_HOME, 'bin'), { recursive: true });
133
+ writeFileSync(BIN_PATH_FILE, binDir, 'utf8');
134
+
135
+ console.log(`[urban] poppler 설치 완료 → ${binDir}`);
136
+ }
137
+
138
+ main()
139
+ .catch(err => {
140
+ console.warn('[urban] poppler 자동 설치 실패 (계속 진행):', err.message);
141
+ console.warn('[urban] 수동 설치: winget install oschwartz10612.poppler');
142
+ })
143
+ .finally(() => process.exit(0));
package/src/daemon.js CHANGED
@@ -107,8 +107,23 @@ log.info('Urban 데몬 초기화 시작', { urbanHome: URBAN_HOME });
107
107
  try { execSync(`${which} ${cmd}`, { stdio: 'ignore' }); return true; } catch { return false; }
108
108
  };
109
109
  if (!hasBin('pdftoppm')) {
110
- log.warn('[preflight] pdftoppm 미설치 — kordoc PDF 렌더링 불가');
111
- if (process.platform === 'darwin') {
110
+ if (process.platform === 'win32') {
111
+ // postinstall에서 다운로드한 로컬 poppler 경로 확인
112
+ const popplerBinFile = join(URBAN_HOME, 'bin', 'poppler-bin-path.txt');
113
+ let injected = false;
114
+ try {
115
+ const { readFileSync } = await import('node:fs');
116
+ const binDir = readFileSync(popplerBinFile, 'utf8').trim();
117
+ if (binDir && existsSync(binDir)) {
118
+ process.env.PATH = binDir + ';' + process.env.PATH;
119
+ log.info('[preflight] poppler 로컬 경로 PATH 추가', { binDir });
120
+ injected = true;
121
+ }
122
+ } catch {}
123
+ if (!injected) {
124
+ log.warn('[preflight] pdftoppm 미설치 — 수동 설치: winget install oschwartz10612.poppler');
125
+ }
126
+ } else if (process.platform === 'darwin') {
112
127
  try {
113
128
  log.info('[preflight] brew install poppler 실행 중...');
114
129
  execSync('brew install poppler', { stdio: 'pipe', timeout: 120_000 });
@@ -77,8 +77,15 @@ export function buildStartScript({ nodePath, daemonJs, urbanHome, logsDir }) {
77
77
  const psQ = (s) => String(s).replace(/'/g, "''");
78
78
  const outLog = join(logsDir, 'out.log');
79
79
  const errLog = join(logsDir, 'err.log');
80
+ const popplerBinFile = join(urbanHome, 'bin', 'poppler-bin-path.txt');
80
81
  return `# Urban daemon start script (urban install 자동 생성)
81
82
  $env:URBAN_HOME = '${psQ(urbanHome)}'
83
+ # poppler (pdftoppm) 경로 자동 주입
84
+ $popplerBinFile = '${psQ(popplerBinFile)}'
85
+ if (Test-Path $popplerBinFile) {
86
+ $pb = (Get-Content $popplerBinFile -Raw).Trim()
87
+ if ($pb -and (Test-Path $pb)) { $env:PATH = $pb + ';' + $env:PATH }
88
+ }
82
89
  New-Item -ItemType Directory -Force -Path '${psQ(logsDir)}' | Out-Null
83
90
  Set-Location -Path '${psQ(urbanHome)}'
84
91
  & '${psQ(nodePath)}' '${psQ(daemonJs)}' 1>> '${psQ(outLog)}' 2>> '${psQ(errLog)}'
package/src/web/server.js CHANGED
@@ -532,7 +532,7 @@ async function handleApiRoute(req, res, pathname, url, { db, queues, bus, graph,
532
532
  // GET /api/settings/kordoc — kordoc OCR 설정 조회
533
533
  if (pathname === '/api/settings/kordoc' && method === 'GET') {
534
534
  try {
535
- const mode = getSecureConfig('kordoc.mode') ?? 'cli';
535
+ const mode = getSecureConfig('kordoc.mode') ?? 'api';
536
536
  const cli = {
537
537
  ocrMode: getSecureConfig('kordoc.cli.ocrMode') ?? '',
538
538
  geminiModel: getSecureConfig('kordoc.cli.geminiModel') ?? '',
@@ -103,7 +103,7 @@ function renderProvidersList(providers) {
103
103
  async function loadKordocSettings() {
104
104
  try {
105
105
  const data = await getJson("/api/settings/kordoc");
106
- const mode = data.mode ?? "cli";
106
+ const mode = data.mode ?? "api";
107
107
  document.querySelectorAll('input[name="kordoc-mode"]').forEach((r) => { r.checked = r.value === mode; });
108
108
  toggleKordocPanels(mode);
109
109
  const cli = data.cli ?? {};
@@ -1,107 +0,0 @@
1
- #!/usr/bin/env node
2
- // postinstall: uv 설치 확인 → graphifyy 가상환경 구성 + 터미널용 symlink 생성
3
- import { execSync, spawnSync } from 'child_process';
4
- import { existsSync, mkdirSync, symlinkSync, unlinkSync, writeFileSync } from 'fs';
5
- import { homedir } from 'os';
6
- import { join } from 'path';
7
-
8
- const isWin = process.platform === 'win32';
9
- const PYTHON_VERSION = '3.12';
10
- const uvEnvDir = join(homedir(), '.urban', '.venv');
11
- const pythonBin = join(uvEnvDir, isWin ? 'Scripts/python.exe' : 'bin/python');
12
- const graphifyBin = join(uvEnvDir, isWin ? 'Scripts/graphify.exe' : 'bin/graphify');
13
- const localBin = join(homedir(), '.local', 'bin');
14
- const symlinkTarget = join(localBin, isWin ? 'graphify.cmd' : 'graphify');
15
-
16
- function findUv() {
17
- try { execSync(isWin ? 'where uv' : 'which uv', { stdio: 'ignore' }); return 'uv'; } catch {}
18
- const candidates = isWin
19
- ? [join(homedir(), '.local', 'bin', 'uv.exe'), join(homedir(), '.cargo', 'bin', 'uv.exe')]
20
- : [join(homedir(), '.local', 'bin', 'uv'), join(homedir(), '.cargo', 'bin', 'uv')];
21
- return candidates.find(p => existsSync(p)) ?? null;
22
- }
23
-
24
- function installUv() {
25
- console.log('[urban] uv 설치 중...');
26
- if (isWin) {
27
- execSync('powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"', { stdio: 'inherit' });
28
- } else {
29
- execSync('curl -LsSf https://astral.sh/uv/install.sh | sh', { stdio: 'inherit' });
30
- }
31
- return findUv();
32
- }
33
-
34
- // uv 자체 관리 Python 경로 반환 (없으면 자동 설치)
35
- function ensureUvPython(uv) {
36
- console.log(`[urban] uv Python ${PYTHON_VERSION} 준비 중...`);
37
- spawnSync(uv, ['python', 'install', PYTHON_VERSION], { stdio: 'inherit', shell: isWin });
38
- const result = spawnSync(uv, ['python', 'find', PYTHON_VERSION], { stdio: 'pipe', shell: isWin, encoding: 'utf8' });
39
- const path = result.stdout?.trim();
40
- if (!path) throw new Error(`uv python find ${PYTHON_VERSION} 실패`);
41
- console.log(`[urban] uv Python 경로: ${path}`);
42
- return path;
43
- }
44
-
45
- function run(cmd, args, opts = {}) {
46
- const r = spawnSync(cmd, args, { stdio: 'inherit', shell: isWin, ...opts });
47
- if (r.status !== 0) throw new Error(`${cmd} ${args.join(' ')} 실패 (exit ${r.status})`);
48
- }
49
-
50
- // 터미널 어디서나 실행 가능하도록 symlink(Unix) 또는 .cmd 래퍼(Windows) 생성
51
- function createSymlink() {
52
- mkdirSync(localBin, { recursive: true });
53
-
54
- if (isWin) {
55
- const wrapper = `@echo off\n"${graphifyBin}" %*\n`;
56
- writeFileSync(symlinkTarget, wrapper);
57
- console.log('[urban] graphify 래퍼 생성 →', symlinkTarget);
58
- } else {
59
- try { unlinkSync(symlinkTarget); } catch {}
60
- symlinkSync(graphifyBin, symlinkTarget);
61
- console.log('[urban] graphify symlink 생성 →', symlinkTarget);
62
- }
63
-
64
- // ~/.local/bin 이 PATH에 없으면 안내
65
- const pathDirs = (process.env.PATH ?? '').split(isWin ? ';' : ':');
66
- if (!pathDirs.includes(localBin)) {
67
- console.warn(`[urban] ⚠️ ${localBin} 이 PATH에 없습니다.`);
68
- if (isWin) {
69
- console.warn(` setx PATH "%PATH%;${localBin}"`);
70
- } else {
71
- const rc = process.env.SHELL?.includes('zsh') ? '~/.zshrc' : '~/.bashrc';
72
- console.warn(` ${rc} 에 추가: export PATH="$HOME/.local/bin:$PATH"`);
73
- }
74
- }
75
- }
76
-
77
- if (process.env.URBAN_SKIP_GRAPHIFY === '1') {
78
- console.log('[urban] URBAN_SKIP_GRAPHIFY=1 — graphify 설치 건너뜀');
79
- process.exit(0);
80
- }
81
-
82
- try {
83
- let uv = findUv();
84
- if (!uv) uv = installUv();
85
- if (!uv) throw new Error('uv 설치 실패 — https://docs.astral.sh/uv/getting-started/installation 참고');
86
-
87
- if (existsSync(pythonBin)) {
88
- console.log('[urban] graphify 환경이 이미 존재합니다:', uvEnvDir);
89
- createSymlink();
90
- process.exit(0);
91
- }
92
-
93
- // uv 자체 관리 Python 사용 (Homebrew·시스템 Python 무시)
94
- const uvPython = ensureUvPython(uv);
95
-
96
- console.log('[urban] graphify 가상환경 생성 중...');
97
- run(uv, ['venv', uvEnvDir, '--python', uvPython]);
98
-
99
- console.log('[urban] graphifyy 설치 중...');
100
- run(uv, ['pip', 'install', '--python', pythonBin, 'graphifyy']);
101
-
102
- console.log('[urban] graphify 설치 완료 →', uvEnvDir);
103
- createSymlink();
104
- } catch (err) {
105
- console.warn('[urban] graphify 설치 실패 (선택 기능이므로 계속):', err.message);
106
- process.exit(0);
107
- }