nexus-agent-installer 0.1.1 → 0.1.2

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.
Files changed (2) hide show
  1. package/install.js +214 -6
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -8,10 +8,46 @@ const path = require("node:path");
8
8
 
9
9
  const REPO = "EJCHO-salary/nexus-agent-releases";
10
10
  const IS_WIN = process.platform === "win32";
11
+ const MIN_SAFE_UV_VERSION = [0, 10, 10];
12
+ const INTEGRITY_MODULE = "nexus_agent.install_integrity";
13
+
14
+ // 실행 중인 프로세스가 tool 환경 파일을 잠그면 uv가 기존 패키지를 지우다 실패해
15
+ // 디렉토리가 반쯤 남는다(dotenv 네임스페이스만 남고 load_dotenv 소실 등).
16
+ // 대상은 PID가 아니라 실행 파일 경로로 식별한다 — 런처는 tool 환경 밖에 있어
17
+ // 하위 경로 매칭에 걸리지 않으므로 별도로 비교한다.
18
+ // PowerShell 5.1은 BOM 없는 스크립트를 ANSI로 읽으므로 본문은 순수 ASCII로 두고,
19
+ // 사용자에게 보여줄 한국어 메시지는 종료된 PID를 받아 Node가 출력한다.
20
+ const STOP_TOOL_PROCESSES_PS = [
21
+ '$ErrorActionPreference = "Stop"',
22
+ "$toolEnvironment = $env:NEXUS_TOOL_ENVIRONMENT",
23
+ "$launcher = $env:NEXUS_TOOL_LAUNCHER",
24
+ "if (-not $toolEnvironment) { exit 0 }",
25
+ "$separator = [System.IO.Path]::DirectorySeparatorChar",
26
+ "$toolPrefix = [System.IO.Path]::GetFullPath($toolEnvironment).TrimEnd($separator) + $separator",
27
+ '$launcherFull = ""',
28
+ "if ($launcher) { $launcherFull = [System.IO.Path]::GetFullPath($launcher) }",
29
+ "$processes = @(",
30
+ " Get-CimInstance -ClassName Win32_Process -ErrorAction SilentlyContinue |",
31
+ " Where-Object {",
32
+ " if (-not $_.ExecutablePath) { return $false }",
33
+ " if ($_.ProcessId -eq $PID) { return $false }",
34
+ " try {",
35
+ " $executable = [System.IO.Path]::GetFullPath([string]$_.ExecutablePath)",
36
+ " if ($executable.StartsWith($toolPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { return $true }",
37
+ " return $launcherFull -and $executable.Equals($launcherFull, [System.StringComparison]::OrdinalIgnoreCase)",
38
+ " }",
39
+ " catch { return $false }",
40
+ " }",
41
+ ")",
42
+ "if ($processes.Count -eq 0) { exit 0 }",
43
+ "$processIds = @($processes | ForEach-Object { [int]$_.ProcessId })",
44
+ "foreach ($processId in $processIds) { Stop-Process -Id $processId -Force -ErrorAction SilentlyContinue }",
45
+ "foreach ($processId in $processIds) { Wait-Process -Id $processId -ErrorAction SilentlyContinue }",
46
+ '$processIds -join ","',
47
+ ].join("\n");
11
48
 
12
49
  function fail(msg) {
13
- console.error(`오류: ${msg}`);
14
- process.exit(1);
50
+ throw new Error(msg);
15
51
  }
16
52
 
17
53
  function whlSuffix() {
@@ -30,12 +66,158 @@ function whlSuffix() {
30
66
  fail(`지원하지 않는 플랫폼입니다: ${platform}`);
31
67
  }
32
68
 
69
+ // shell을 쓰지 않는다 — Windows에서 shell을 켜면 인자가 그대로 이어붙어
70
+ // `C:\Users\John Doe\...` 같은 공백 포함 경로가 쪼개진다. uv/tar/powershell은
71
+ // 모두 실제 exe라 PATH 검색만으로 실행된다.
33
72
  function run(cmd, args, opts = {}) {
34
- return spawnSync(cmd, args, { stdio: "inherit", shell: IS_WIN, ...opts });
73
+ return spawnSync(cmd, args, { stdio: "inherit", ...opts });
74
+ }
75
+
76
+ function capture(cmd, args, opts = {}) {
77
+ return spawnSync(cmd, args, {
78
+ encoding: "utf8",
79
+ windowsHide: true,
80
+ ...opts,
81
+ });
82
+ }
83
+
84
+ function parseUvVersion(text) {
85
+ const match = String(text || "").match(/\b(\d+)\.(\d+)\.(\d+)\b/);
86
+ return match ? match.slice(1).map(Number) : null;
87
+ }
88
+
89
+ function compareVersions(left, right) {
90
+ for (let index = 0; index < 3; index += 1) {
91
+ if (left[index] !== right[index]) return left[index] - right[index];
92
+ }
93
+ return 0;
94
+ }
95
+
96
+ function windowsRepairPlan(versionOutput) {
97
+ const version = parseUvVersion(versionOutput);
98
+ return version && compareVersions(version, MIN_SAFE_UV_VERSION) >= 0
99
+ ? []
100
+ : ["self-update", "uninstall-if-still-unsafe"];
101
+ }
102
+
103
+ function currentUvVersionOutput() {
104
+ const result = capture("uv", ["--version"]);
105
+ return result.status === 0 ? result.stdout || result.stderr : "";
106
+ }
107
+
108
+ function ensureSafeWindowsUv() {
109
+ if (!IS_WIN || windowsRepairPlan(currentUvVersionOutput()).length === 0) return;
110
+
111
+ console.log("uv 0.10.10 이상으로 업데이트를 시도합니다...");
112
+ const updated = run("uv", ["self", "update"]);
113
+ if (updated.status !== 0) {
114
+ console.warn("uv 자동 업데이트를 사용할 수 없어 안전한 재설치로 전환합니다.");
115
+ }
116
+
117
+ if (windowsRepairPlan(currentUvVersionOutput()).length !== 0) {
118
+ console.log("구버전 uv 도구 환경을 제거한 뒤 다시 설치합니다...");
119
+ const removed = run("uv", ["tool", "uninstall", "nexus-agent-platform"]);
120
+ if (removed.status !== 0) {
121
+ console.warn("기존 도구 환경 제거가 완료되지 않았습니다. 강제 재설치를 계속합니다.");
122
+ }
123
+ }
124
+ }
125
+
126
+ function buildUvInstallArgs(depDir, isWindows = IS_WIN) {
127
+ const args = [
128
+ "tool",
129
+ "install",
130
+ "--force",
131
+ "--reinstall",
132
+ "--refresh",
133
+ ];
134
+ if (isWindows) args.push("--no-cache", "--link-mode", "copy");
135
+ return [
136
+ ...args,
137
+ "--python",
138
+ "3.13",
139
+ "--no-build",
140
+ "--find-links",
141
+ depDir,
142
+ ];
143
+ }
144
+
145
+ function toolEnvironmentPaths() {
146
+ const toolDirResult = capture("uv", ["tool", "dir"]);
147
+ if (toolDirResult.status !== 0) return null;
148
+ const toolRoot = String(toolDirResult.stdout || "").trim();
149
+ if (!toolRoot) return null;
150
+ const binResult = capture("uv", ["tool", "dir", "--bin"]);
151
+ const binRoot = binResult.status === 0 ? String(binResult.stdout || "").trim() : "";
152
+ const environment = path.join(toolRoot, "nexus-agent-platform");
153
+ return {
154
+ environment,
155
+ python: IS_WIN
156
+ ? path.join(environment, "Scripts", "python.exe")
157
+ : path.join(environment, "bin", "python"),
158
+ launcher: binRoot
159
+ ? path.join(binRoot, IS_WIN ? "nexus-agent.exe" : "nexus-agent")
160
+ : "",
161
+ };
162
+ }
163
+
164
+ function stopNexusToolProcesses() {
165
+ if (!IS_WIN) return "";
166
+ const paths = toolEnvironmentPaths();
167
+ if (!paths) return "";
168
+
169
+ const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-agent-stop-"));
170
+ try {
171
+ const scriptPath = path.join(scriptDir, "stop-nexus-processes.ps1");
172
+ fs.writeFileSync(scriptPath, STOP_TOOL_PROCESSES_PS, "ascii");
173
+ const result = capture(
174
+ "powershell",
175
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
176
+ {
177
+ env: {
178
+ ...process.env,
179
+ NEXUS_TOOL_ENVIRONMENT: paths.environment,
180
+ NEXUS_TOOL_LAUNCHER: paths.launcher,
181
+ },
182
+ },
183
+ );
184
+ const stopped = String(result.stdout || "").trim();
185
+ if (stopped) {
186
+ console.log(`실행 중인 Nexus Agent 프로세스를 종료했습니다: ${stopped}`);
187
+ }
188
+ return stopped;
189
+ } finally {
190
+ fs.rmSync(scriptDir, { recursive: true, force: true });
191
+ }
192
+ }
193
+
194
+ function verifyRuntime() {
195
+ const paths = toolEnvironmentPaths();
196
+ if (!paths) fail("uv tool 경로를 확인하지 못했습니다.");
197
+ if (!fs.existsSync(paths.python)) {
198
+ fail(`설치된 Python을 찾을 수 없습니다: ${paths.python}`);
199
+ }
200
+ const result = run(paths.python, ["-I", "-m", INTEGRITY_MODULE]);
201
+ if (result.status !== 0) fail("Nexus Agent 설치 무결성 검증에 실패했습니다.");
202
+ }
203
+
204
+ function removeDamagedTool() {
205
+ console.warn("설치 검증 실패 — 손상된 환경을 제거합니다.");
206
+ const toolDir = capture("uv", ["tool", "dir"]);
207
+ const cacheDir = capture("uv", ["cache", "dir"]);
208
+ const removed = run("uv", ["tool", "uninstall", "nexus-agent-platform"]);
209
+ if (removed.status !== 0) {
210
+ console.warn("손상된 도구 환경을 자동으로 제거하지 못했습니다.");
211
+ }
212
+ if (IS_WIN) {
213
+ console.warn("백신/EDR 검역 로그에서 아래 경로를 확인하세요:");
214
+ if (toolDir.status === 0) console.warn(` ${toolDir.stdout.trim()}`);
215
+ if (cacheDir.status === 0) console.warn(` ${cacheDir.stdout.trim()}`);
216
+ }
35
217
  }
36
218
 
37
219
  function hasUv() {
38
- return spawnSync("uv", ["--version"], { stdio: "ignore", shell: IS_WIN }).status === 0;
220
+ return spawnSync("uv", ["--version"], { stdio: "ignore" }).status === 0;
39
221
  }
40
222
 
41
223
  function installUv() {
@@ -52,6 +234,9 @@ async function main() {
52
234
  const suffix = whlSuffix();
53
235
 
54
236
  if (!hasUv()) installUv();
237
+ // ensureSafeWindowsUv가 uv tool uninstall을 부를 수 있으므로 그 전에 잠금을 푼다.
238
+ stopNexusToolProcesses();
239
+ ensureSafeWindowsUv();
55
240
 
56
241
  console.log("최신 릴리스 확인 중...");
57
242
  const headers = { "User-Agent": "nexus-agent-installer", Accept: "application/vnd.github+json" };
@@ -77,6 +262,7 @@ async function main() {
77
262
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-agent-install-"));
78
263
  const whlPath = path.join(tmpDir, asset.name);
79
264
  const depDir = path.join(tmpDir, "deps");
265
+ let installationCompleted = false;
80
266
  try {
81
267
  fs.writeFileSync(whlPath, Buffer.from(await dl.arrayBuffer()));
82
268
  fs.mkdirSync(depDir);
@@ -90,7 +276,10 @@ async function main() {
90
276
  if (untar.status !== 0) fail("의존성 번들 압축 해제에 실패했습니다.");
91
277
  }
92
278
 
93
- const uvArgs = ["tool", "install", "--force", "--python", "3.13", "--no-build", "--find-links", depDir];
279
+ // 다운로드하는 동안 사용자가 다시 실행했을 있으므로 설치 직전에 한 번 더 정리한다.
280
+ stopNexusToolProcesses();
281
+
282
+ const uvArgs = buildUvInstallArgs(depDir);
94
283
  const constraints = path.join(depDir, "constraints.txt");
95
284
  let result;
96
285
  if (fs.existsSync(constraints)) {
@@ -103,6 +292,11 @@ async function main() {
103
292
  result = run("uv", [...uvArgs, whlPath]);
104
293
  }
105
294
  if (result.status !== 0) fail("uv tool install이 실패했습니다.");
295
+ installationCompleted = true;
296
+ verifyRuntime();
297
+ } catch (error) {
298
+ if (installationCompleted) removeDamagedTool();
299
+ throw error;
106
300
  } finally {
107
301
  fs.rmSync(tmpDir, { recursive: true, force: true });
108
302
  }
@@ -115,4 +309,18 @@ async function main() {
115
309
  console.log(" nexus-agent update # 이후 최신 버전 업데이트");
116
310
  }
117
311
 
118
- main().catch((e) => fail(e.message));
312
+ if (process.env.NEXUS_INSTALLER_LIBRARY_MODE === "1") {
313
+ module.exports = {
314
+ INTEGRITY_MODULE,
315
+ STOP_TOOL_PROCESSES_PS,
316
+ buildUvInstallArgs,
317
+ parseUvVersion,
318
+ stopNexusToolProcesses,
319
+ windowsRepairPlan,
320
+ };
321
+ } else {
322
+ main().catch((error) => {
323
+ console.error(`오류: ${error.message}`);
324
+ process.exitCode = 1;
325
+ });
326
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-agent-installer",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Installer for Nexus Agent — downloads the platform wheel from GitHub Releases and installs it via uv",
5
5
  "scripts": {
6
6
  "postinstall": "node install.js"