#!/usr/bin/env node 'use strict'; // ============================================================================ // 설정 파일 명령줄 도구 (v1.5.7, 요구 1·3·4) // // 설치·연동 스크립트(deploy/*.sh)와 운영자가 설정 파일(data/config.json)을 읽고 쓰는 데 쓴다. 웹 설정 페이지와 같은 규칙(lib/config.js). // // node scripts/config.js list 모든 항목의 동작 값과 출처 // node scripts/config.js get 값 하나 (스크립트에서 $(…) 로 쓰기 좋게 값만 출력) // node scripts/config.js set [ …] 값 저장 (비밀값 항목이면 보관소에 암호화 저장) // node scripts/config.js unset 항목을 파일에서 지움 (환경 변수·기본값으로 돌아감) // node scripts/config.js import-env [--unit <유닛 파일>] 환경 변수(또는 systemd 유닛의 Environment= 줄)에서 아직 없는 항목을 옮겨 적음 // node scripts/config.js path 설정 파일 경로 // node scripts/config.js setup-token 초기 설정 페이지 접속 토큰(없으면 만듦) — 설치 스크립트가 접속 주소 안내에 쓴다 // node scripts/config.js setup-url 초기 설정 페이지 접속 주소(토큰 포함) 한 줄씩 — v1.5.11 기본 http://<서버 IP>:8795/?token=… // node scripts/config.js check 필수 값 점검 (비어 있으면 종료 코드 2 와 안내) // // 환경 변수 DATA_ROOT / WEBMAKER_CONFIG 는 서비스와 같게 준다 (기본: 앱 폴더의 data/config.json). // ============================================================================ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const config = require('../lib/config'); const argv = process.argv.slice(2); const cmd = argv[0]; const DATA_ROOT = process.env.DATA_ROOT || path.join(__dirname, '..', 'data'); /** systemd 유닛 파일의 Environment= 줄을 객체로 */ function envFromUnit(file) { const out = {}; const text = fs.readFileSync(file, 'utf8'); for (const line of text.split('\n')) { const m = line.match(/^\s*Environment=\s*"?([A-Za-z_][A-Za-z0-9_]*)=([^"]*)"?\s*$/); if (m) out[m[1]] = m[2]; } return out; } function setupToken() { const f = path.join(DATA_ROOT, 'setup.token'); try { const t = fs.readFileSync(f, 'utf8').trim(); if (t.length >= 24) return t; } catch (_) {} fs.mkdirSync(DATA_ROOT, { recursive: true }); const t = crypto.randomBytes(24).toString('base64url'); fs.writeFileSync(f, t + '\n', { mode: 0o600 }); return t; } try { if (cmd === 'list') { const s = config.summary(); const d = config.load(); console.log(`# ${config.FILE} ${d.exists ? `(저장 ${d.updatedAt || '-'} · ${d.updatedBy || '-'})` : '(파일 없음 — 환경 변수·기본값으로 동작)'}`); for (const [k, v] of Object.entries(s)) { const e = config.KEYS[k].type === 'secret' ? { source: 'vault' } : config.effective(k); console.log(`${k.padEnd(30)} = ${JSON.stringify(v)} [${e.source}]`); } } else if (cmd === 'get') { const k = argv[1]; if (!k) throw new Error('key 를 주세요'); const v = config.get(k); process.stdout.write((v === undefined || v === null ? '' : String(v)) + '\n'); } else if (cmd === 'set') { const patch = {}; for (let i = 1; i + 1 < argv.length; i += 2) patch[argv[i]] = argv[i + 1]; if (!Object.keys(patch).length) throw new Error('set …'); const r = config.set(patch, 'cli'); console.log(`저장: ${r.changed.join(', ') || '(변경 없음)'}${r.restart ? ' — 서비스를 재시작해야 적용되는 항목이 있습니다' : ''}`); } else if (cmd === 'unset') { if (!argv[1]) throw new Error('unset '); config.unset(argv[1], 'cli'); console.log(`지움: ${argv[1]}`); } else if (cmd === 'import-env') { const ui = argv.indexOf('--unit'); const env = ui >= 0 ? { ...process.env, ...envFromUnit(argv[ui + 1]) } : process.env; const moved = config.migrateFromEnv(ui >= 0 ? `migrate:unit:${path.basename(argv[ui + 1])}` : 'migrate:cli', env); console.log(moved.length ? `옮김: ${moved.join(', ')}` : '옮길 항목 없음 (이미 설정 파일에 있거나 환경 변수가 비어 있음)'); console.log(`설정 파일: ${config.FILE}`); } else if (cmd === 'path') console.log(config.FILE); else if (cmd === 'setup-token') console.log(setupToken()); else if (cmd === 'setup-url') { // v1.6.31 (요구 6): 쓸 수 있는 관리자가 있으면 토큰 로그인이 거절되므로 토큰 없는 주소와 "관리자 계정으로 로그인" 안내만 보인다 (토큰은 관리자가 없을 때만 유효) let usable = false; try { usable = !!require('../lib/auth').adminState().usable; } catch (_) {} if (usable) { for (const u of config.setupUrls('')) console.log(u.replace(/\?token=.*$/, '') + ' (관리자 계정으로 로그인 — 관리자가 있으면 접속 토큰은 쓰이지 않습니다)'); } else { const t = setupToken(); for (const u of config.setupUrls(t)) console.log(u); } } // v1.5.11: 바인드 주소·포트(기본 8795)에 맞춘 주소 (lib/config.setupUrls) else if (cmd === 'check') { const missing = []; for (const def of Object.values(config.KEYS)) if (def.required && !config.get(def.key)) missing.push(`${def.key} (${def.label})`); const warn = []; if (!config.get('source.remote')) warn.push('source.remote — 사이트 소스를 원격(Gitea)에 보관하지 않습니다 (deploy/install-gitea.sh)'); if ((config.get('mail.provider') || 'none') === 'none') warn.push('mail.provider=none — 받는 메일·웹메일 서버(Arimail)가 연결되지 않았습니다 (deploy/install-arimail.sh)'); if (missing.length) { console.log('필수 값 없음: ' + missing.join(', ')); } for (const w of warn) console.log('참고: ' + w); if (!missing.length) console.log('필수 값 모두 있음'); process.exit(missing.length ? 2 : 0); } else { console.log(fs.readFileSync(__filename, 'utf8').split('\n').filter((l) => l.startsWith('// node')).map((l) => l.slice(3)).join('\n')); process.exit(1); } } catch (e) { console.error(e.message); process.exit(1); }