nexus-agent-installer 0.1.1 → 0.1.3
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/install.js +237 -11
- 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
|
-
|
|
14
|
-
process.exit(1);
|
|
50
|
+
throw new Error(msg);
|
|
15
51
|
}
|
|
16
52
|
|
|
17
53
|
function whlSuffix() {
|
|
@@ -30,12 +66,170 @@ 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",
|
|
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-progress");
|
|
135
|
+
return [
|
|
136
|
+
...args,
|
|
137
|
+
"--python",
|
|
138
|
+
"3.13",
|
|
139
|
+
"--no-build",
|
|
140
|
+
"--find-links",
|
|
141
|
+
depDir,
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildUvInstallEnv(isWindows = IS_WIN, env = process.env) {
|
|
146
|
+
if (!isWindows) return env;
|
|
147
|
+
return {
|
|
148
|
+
...env,
|
|
149
|
+
UV_CONCURRENT_INSTALLS: env.UV_CONCURRENT_INSTALLS || "1",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toolEnvironmentPaths() {
|
|
154
|
+
const toolDirResult = capture("uv", ["tool", "dir"]);
|
|
155
|
+
if (toolDirResult.status !== 0) return null;
|
|
156
|
+
const toolRoot = String(toolDirResult.stdout || "").trim();
|
|
157
|
+
if (!toolRoot) return null;
|
|
158
|
+
const binResult = capture("uv", ["tool", "dir", "--bin"]);
|
|
159
|
+
const binRoot = binResult.status === 0 ? String(binResult.stdout || "").trim() : "";
|
|
160
|
+
const environment = path.join(toolRoot, "nexus-agent-platform");
|
|
161
|
+
return {
|
|
162
|
+
environment,
|
|
163
|
+
python: IS_WIN
|
|
164
|
+
? path.join(environment, "Scripts", "python.exe")
|
|
165
|
+
: path.join(environment, "bin", "python"),
|
|
166
|
+
launcher: binRoot
|
|
167
|
+
? path.join(binRoot, IS_WIN ? "nexus-agent.exe" : "nexus-agent")
|
|
168
|
+
: "",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function stopNexusToolProcesses() {
|
|
173
|
+
if (!IS_WIN) return "";
|
|
174
|
+
const paths = toolEnvironmentPaths();
|
|
175
|
+
if (!paths) return "";
|
|
176
|
+
|
|
177
|
+
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-agent-stop-"));
|
|
178
|
+
try {
|
|
179
|
+
const scriptPath = path.join(scriptDir, "stop-nexus-processes.ps1");
|
|
180
|
+
fs.writeFileSync(scriptPath, STOP_TOOL_PROCESSES_PS, "ascii");
|
|
181
|
+
const result = capture(
|
|
182
|
+
"powershell",
|
|
183
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
|
|
184
|
+
{
|
|
185
|
+
env: {
|
|
186
|
+
...process.env,
|
|
187
|
+
NEXUS_TOOL_ENVIRONMENT: paths.environment,
|
|
188
|
+
NEXUS_TOOL_LAUNCHER: paths.launcher,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
const stopped = String(result.stdout || "").trim();
|
|
193
|
+
if (stopped) {
|
|
194
|
+
console.log(`실행 중인 Nexus Agent 프로세스를 종료했습니다: ${stopped}`);
|
|
195
|
+
}
|
|
196
|
+
return stopped;
|
|
197
|
+
} finally {
|
|
198
|
+
fs.rmSync(scriptDir, { recursive: true, force: true });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function verifyRuntime() {
|
|
203
|
+
const paths = toolEnvironmentPaths();
|
|
204
|
+
if (!paths) fail("uv tool 경로를 확인하지 못했습니다.");
|
|
205
|
+
if (!fs.existsSync(paths.python)) {
|
|
206
|
+
fail(`설치된 Python을 찾을 수 없습니다: ${paths.python}`);
|
|
207
|
+
}
|
|
208
|
+
const result = run(paths.python, ["-I", "-m", INTEGRITY_MODULE]);
|
|
209
|
+
if (result.status !== 0) fail("Nexus Agent 설치 무결성 검증에 실패했습니다.");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function removeDamagedTool() {
|
|
213
|
+
console.warn("설치 검증 실패 — 손상된 환경을 제거합니다.");
|
|
214
|
+
const removed = run("uv", ["tool", "uninstall", "nexus-agent-platform"]);
|
|
215
|
+
if (removed.status !== 0) {
|
|
216
|
+
console.warn("손상된 도구 환경을 자동으로 제거하지 못했습니다.");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function reportSecuritySoftwareDiagnostics() {
|
|
221
|
+
if (!IS_WIN) return;
|
|
222
|
+
const uvPath = capture("where.exe", ["uv"]);
|
|
223
|
+
const toolDir = capture("uv", ["tool", "dir"]);
|
|
224
|
+
const cacheDir = capture("uv", ["cache", "dir"]);
|
|
225
|
+
console.warn("설치 프로세스가 예고 없이 종료됐다면 보안 프로그램/EDR 검역 로그를 확인하세요:");
|
|
226
|
+
if (uvPath.status === 0) console.warn(` uv: ${uvPath.stdout.trim()}`);
|
|
227
|
+
if (toolDir.status === 0) console.warn(` tool: ${toolDir.stdout.trim()}`);
|
|
228
|
+
if (cacheDir.status === 0) console.warn(` cache: ${cacheDir.stdout.trim()}`);
|
|
35
229
|
}
|
|
36
230
|
|
|
37
231
|
function hasUv() {
|
|
38
|
-
return spawnSync("uv", ["--version"], { stdio: "ignore"
|
|
232
|
+
return spawnSync("uv", ["--version"], { stdio: "ignore" }).status === 0;
|
|
39
233
|
}
|
|
40
234
|
|
|
41
235
|
function installUv() {
|
|
@@ -52,6 +246,9 @@ async function main() {
|
|
|
52
246
|
const suffix = whlSuffix();
|
|
53
247
|
|
|
54
248
|
if (!hasUv()) installUv();
|
|
249
|
+
// ensureSafeWindowsUv가 uv tool uninstall을 부를 수 있으므로 그 전에 잠금을 푼다.
|
|
250
|
+
stopNexusToolProcesses();
|
|
251
|
+
ensureSafeWindowsUv();
|
|
55
252
|
|
|
56
253
|
console.log("최신 릴리스 확인 중...");
|
|
57
254
|
const headers = { "User-Agent": "nexus-agent-installer", Accept: "application/vnd.github+json" };
|
|
@@ -77,6 +274,7 @@ async function main() {
|
|
|
77
274
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nexus-agent-install-"));
|
|
78
275
|
const whlPath = path.join(tmpDir, asset.name);
|
|
79
276
|
const depDir = path.join(tmpDir, "deps");
|
|
277
|
+
let installationCompleted = false;
|
|
80
278
|
try {
|
|
81
279
|
fs.writeFileSync(whlPath, Buffer.from(await dl.arrayBuffer()));
|
|
82
280
|
fs.mkdirSync(depDir);
|
|
@@ -90,19 +288,32 @@ async function main() {
|
|
|
90
288
|
if (untar.status !== 0) fail("의존성 번들 압축 해제에 실패했습니다.");
|
|
91
289
|
}
|
|
92
290
|
|
|
93
|
-
|
|
291
|
+
// 다운로드하는 동안 사용자가 다시 실행했을 수 있으므로 설치 직전에 한 번 더 정리한다.
|
|
292
|
+
stopNexusToolProcesses();
|
|
293
|
+
|
|
294
|
+
const uvArgs = buildUvInstallArgs(depDir);
|
|
295
|
+
const uvOptions = { env: buildUvInstallEnv() };
|
|
94
296
|
const constraints = path.join(depDir, "constraints.txt");
|
|
95
297
|
let result;
|
|
96
298
|
if (fs.existsSync(constraints)) {
|
|
97
|
-
result = run("uv", [...uvArgs, "--constraints", constraints, whlPath]);
|
|
98
|
-
if (result.status !== 0) {
|
|
299
|
+
result = run("uv", [...uvArgs, "--constraints", constraints, whlPath], uvOptions);
|
|
300
|
+
if (result.status !== 0 && !IS_WIN) {
|
|
99
301
|
console.log("고정 버전 설치에 실패해 버전 고정 없이 다시 시도합니다...");
|
|
100
|
-
result = run("uv", [...uvArgs, whlPath]);
|
|
302
|
+
result = run("uv", [...uvArgs, whlPath], uvOptions);
|
|
101
303
|
}
|
|
102
304
|
} else {
|
|
103
|
-
result = run("uv", [...uvArgs, whlPath]);
|
|
305
|
+
result = run("uv", [...uvArgs, whlPath], uvOptions);
|
|
104
306
|
}
|
|
105
|
-
if (result.status !== 0)
|
|
307
|
+
if (result.status !== 0) {
|
|
308
|
+
const detail = result.signal ? `signal ${result.signal}` : `exit code ${result.status ?? "unknown"}`;
|
|
309
|
+
fail(`uv tool install이 실패했습니다 (${detail}).`);
|
|
310
|
+
}
|
|
311
|
+
installationCompleted = true;
|
|
312
|
+
verifyRuntime();
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (installationCompleted) removeDamagedTool();
|
|
315
|
+
reportSecuritySoftwareDiagnostics();
|
|
316
|
+
throw error;
|
|
106
317
|
} finally {
|
|
107
318
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
108
319
|
}
|
|
@@ -115,4 +326,19 @@ async function main() {
|
|
|
115
326
|
console.log(" nexus-agent update # 이후 최신 버전 업데이트");
|
|
116
327
|
}
|
|
117
328
|
|
|
118
|
-
|
|
329
|
+
if (process.env.NEXUS_INSTALLER_LIBRARY_MODE === "1") {
|
|
330
|
+
module.exports = {
|
|
331
|
+
INTEGRITY_MODULE,
|
|
332
|
+
STOP_TOOL_PROCESSES_PS,
|
|
333
|
+
buildUvInstallArgs,
|
|
334
|
+
buildUvInstallEnv,
|
|
335
|
+
parseUvVersion,
|
|
336
|
+
stopNexusToolProcesses,
|
|
337
|
+
windowsRepairPlan,
|
|
338
|
+
};
|
|
339
|
+
} else {
|
|
340
|
+
main().catch((error) => {
|
|
341
|
+
console.error(`오류: ${error.message}`);
|
|
342
|
+
process.exitCode = 1;
|
|
343
|
+
});
|
|
344
|
+
}
|
package/package.json
CHANGED