@clazic/urban 0.2.14 → 0.2.16

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.14",
3
+ "version": "0.2.16",
4
4
  "description": "도시계획연구 보고서 자동 수집·지식베이스 데몬",
5
5
  "type": "module",
6
6
  "engines": {
@@ -68,37 +68,47 @@ WantedBy=default.target
68
68
  }
69
69
 
70
70
  /**
71
- * 순수 함수: Windows PowerShell 커맨드 생성
71
+ * 순수 함수: Windows start-daemon.ps1 내용 생성
72
+ *
73
+ * ~/.urban/start-daemon.ps1 로 저장 후 Task Scheduler가 이 파일을 실행함.
74
+ * 직접 편집 가능 → 경로·환경변수 문제 발생 시 육안 확인 가능.
72
75
  */
73
- export function buildPowerShellCmd({ nodePath, daemonJs, urbanHome }) {
74
- const psEscape = (s) => s.replace(/'/g, "''");
75
-
76
- // VBScript 파일: URBAN_HOME에 저장
77
- // wscript.exe(GUI 앱)로 실행하면 콘솔 창이 열리지 않음
78
- const vbsPath = join(urbanHome, 'urban-launcher.vbs');
79
- const escapedVbsPath = psEscape(vbsPath);
80
- // VBScript 문자열 내 쌍따옴표 이스케이프 ("" = 리터럴 ")
81
- const vbsNodePath = nodePath.replace(/"/g, '""');
82
- const vbsDaemonJs = daemonJs.replace(/"/g, '""');
76
+ export function buildStartScript({ nodePath, daemonJs, urbanHome, logsDir }) {
77
+ const psQ = (s) => String(s).replace(/'/g, "''");
78
+ const outLog = join(logsDir, 'out.log');
79
+ const errLog = join(logsDir, 'err.log');
80
+ return `# Urban daemon start script (urban install 자동 생성)
81
+ $env:URBAN_HOME = '${psQ(urbanHome)}'
82
+ New-Item -ItemType Directory -Force -Path '${psQ(logsDir)}' | Out-Null
83
+ Set-Location -Path '${psQ(urbanHome)}'
84
+ & '${psQ(nodePath)}' '${psQ(daemonJs)}' 1>> '${psQ(outLog)}' 2>> '${psQ(errLog)}'
85
+ `;
86
+ }
83
87
 
88
+ /**
89
+ * 순수 함수: Windows Task Scheduler 등록 PowerShell 커맨드 생성
90
+ *
91
+ * VBScript / cmd.exe 방식 폐기 이유:
92
+ * - cmd.exe: Set-Content -Encoding ASCII가 한글 경로 깨뜨림 (조용히 실패)
93
+ * - VBScript oWsh.Run True(동기) + RestartCount 충돌 → 0xC000013A CTRL+BREAK 강제 종료
94
+ *
95
+ * 새 방식: powershell.exe -WindowStyle Hidden -File + S4U Principal
96
+ * - S4U(Service For User): 비대화형 세션 → 콘솔 창 절대 안 뜸
97
+ * - -WindowStyle Hidden: 이중 방어 (Interactive 폴백에서도 숨김)
98
+ * - 래퍼 스크립트를 파일로 저장 → 직접 편집·디버그 가능
99
+ */
100
+ export function buildPowerShellCmd({ urbanHome, startScriptPath }) {
101
+ const psQ = (s) => String(s).replace(/'/g, "''");
84
102
  return `
85
- $vbsPath = '${escapedVbsPath}'
86
- $vbsContent = @'
87
- Set oWsh = CreateObject("WScript.Shell")
88
- oWsh.Run Chr(34) & "${vbsNodePath}" & Chr(34) & " " & Chr(34) & "${vbsDaemonJs}" & Chr(34), 0, True
89
- '@
90
- Set-Content -Path $vbsPath -Value $vbsContent -Encoding ASCII -Force
91
- $action = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument ('"' + $vbsPath + '"')
92
- $trigger = New-ScheduledTaskTrigger -AtLogOn
93
- $settings = New-ScheduledTaskSettingsSet \`
94
- -ExecutionTimeLimit 0 \`
95
- -AllowStartIfOnBatteries \`
96
- -DontStopIfGoingOnBatteries \`
97
- -StartWhenAvailable \`
98
- -MultipleInstances IgnoreNew \`
99
- -RestartCount 5 \`
100
- -RestartInterval (New-TimeSpan -Minutes 1)
101
- Register-ScheduledTask -TaskName 'Urban' -Action $action -Trigger $trigger -Settings $settings -Force
103
+ $scriptPath = '${psQ(startScriptPath)}'
104
+ $urbanHome = '${psQ(urbanHome)}'
105
+ $argument = '-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "' + $scriptPath + '"'
106
+ $action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $argument -WorkingDirectory $urbanHome
107
+ $trigger = New-ScheduledTaskTrigger -AtLogOn -User $env:USERNAME
108
+ $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U -RunLevel Limited
109
+ $settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -RestartCount 5 -RestartInterval (New-TimeSpan -Minutes 1) -Hidden
110
+ Unregister-ScheduledTask -TaskName 'Urban' -Confirm:$false -ErrorAction SilentlyContinue | Out-Null
111
+ Register-ScheduledTask -TaskName 'Urban' -Action $action -Trigger $trigger -Principal $principal -Settings $settings | Out-Null
102
112
  `;
103
113
  }
104
114
 
@@ -139,16 +149,29 @@ export async function installDaemon() {
139
149
  return { ok: true, message: `macOS 데몬 등록 완료: ${plistPath}` };
140
150
  } else if (isWindows) {
141
151
  // Windows: Task Scheduler (PowerShell)
142
- const ps = buildPowerShellCmd({ nodePath, daemonJs, urbanHome: URBAN_HOME });
152
+ // 1. start-daemon.ps1 생성 사용자가 직접 열어 경로 확인 가능
153
+ const startScriptPath = join(URBAN_HOME, 'start-daemon.ps1');
154
+ const startScript = buildStartScript({ nodePath, daemonJs, urbanHome: URBAN_HOME, logsDir });
155
+ writeFileSync(startScriptPath, startScript, 'utf8');
156
+
157
+ // 2. Task Scheduler 등록
158
+ const ps = buildPowerShellCmd({ urbanHome: URBAN_HOME, startScriptPath });
143
159
  try {
144
160
  // -EncodedCommand: Base64(UTF-16LE) — 따옴표/특수문자 인젝션 방지
145
161
  const encoded = Buffer.from(ps, 'utf16le').toString('base64');
146
- execSync(`powershell -NoProfile -EncodedCommand ${encoded}`, { shell: false, stdio: 'ignore' });
162
+ execSync(`powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded}`, {
163
+ shell: false,
164
+ stdio: ['ignore', 'pipe', 'pipe'],
165
+ });
147
166
  } catch (err) {
148
- // PowerShell 실행 권한 문제 가능 메시지에 명시
149
- return { ok: false, message: `Windows 데몬 등록 실패 (관리자 권한 필요): ${err.message}` };
167
+ const stderr = err.stderr ? err.stderr.toString('utf8').trim() : '';
168
+ return { ok: false, message: `Windows 데몬 등록 실패: ${err.message}${stderr ? '\n ' + stderr : ''}` };
150
169
  }
151
- return { ok: true, message: `Windows 데몬 등록 완료 (작업 스케줄러: Urban)` };
170
+ // AtLogOn 트리거는 다음 로그온까지 대기 즉시 1회 시작
171
+ try {
172
+ execSync(`powershell -NoProfile -Command "Start-ScheduledTask -TaskName 'Urban'"`, { shell: true, stdio: 'ignore' });
173
+ } catch { /* 이미 실행 중이면 무시 */ }
174
+ return { ok: true, message: `Windows 데몬 등록 완료 (start-daemon.ps1: ${startScriptPath})` };
152
175
  } else if (isLinux) {
153
176
  // Linux: systemd user service
154
177
  const serviceDir = join(homedir(), '.config', 'systemd', 'user');
@@ -206,9 +229,8 @@ export async function uninstallDaemon() {
206
229
  } catch (err) {
207
230
  // 작업이 없거나 권한 부족 → 무시
208
231
  }
209
- // VBScript 런처 파일 정리
210
- const vbsPath = join(URBAN_HOME, 'urban-launcher.vbs');
211
- if (existsSync(vbsPath)) rmSync(vbsPath);
232
+ const startScriptPath = join(URBAN_HOME, 'start-daemon.ps1');
233
+ if (existsSync(startScriptPath)) rmSync(startScriptPath);
212
234
  return { ok: true, message: `Windows 데몬 제거 완료` };
213
235
  } else if (isLinux) {
214
236
  try {