loadtoagent 1.1.0 → 1.3.1
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/README.ko.md +7 -0
- package/README.md +7 -0
- package/README.zh-CN.md +7 -0
- package/docs/PROVIDER-CONTRACTS.md +23 -1
- package/docs/UI-AUDIT-100.md +118 -0
- package/docs/UI-AUDIT-101-200.md +120 -0
- package/docs/UI-AUDIT-201-300.md +120 -0
- package/main.js +135 -38
- package/package.json +13 -4
- package/preload.js +6 -0
- package/renderer/app-agent-actions.js +125 -53
- package/renderer/app-bootstrap.js +54 -19
- package/renderer/app-dashboard.js +190 -55
- package/renderer/app-drawer-content.js +48 -44
- package/renderer/app-drawer-data.js +26 -13
- package/renderer/app-drawer.js +72 -54
- package/renderer/app-events-dialogs.js +146 -37
- package/renderer/app-events-filters.js +172 -13
- package/renderer/app-events-navigation.js +77 -30
- package/renderer/app-events-sessions.js +156 -9
- package/renderer/app-events.js +2 -1
- package/renderer/app-graph-model.js +20 -38
- package/renderer/app-graph-orchestration.js +15 -13
- package/renderer/app-graph-view.js +228 -113
- package/renderer/app-management.js +211 -0
- package/renderer/app-provider-visibility.js +124 -0
- package/renderer/app-quality.js +568 -0
- package/renderer/app-run-modal.js +103 -33
- package/renderer/app-runtime-overview.js +312 -0
- package/renderer/app-session-render.js +94 -37
- package/renderer/app-tmux-render.js +52 -44
- package/renderer/app.js +153 -75
- package/renderer/i18n-messages.js +833 -16
- package/renderer/i18n.js +104 -0
- package/renderer/index.html +145 -59
- package/renderer/shared.js +17 -0
- package/renderer/styles-agent-map.css +6 -0
- package/renderer/styles-cards.css +26 -0
- package/renderer/styles-components.css +86 -9
- package/renderer/styles-management.css +355 -0
- package/renderer/styles-overlays.css +8 -2
- package/renderer/styles-product.css +24 -16
- package/renderer/styles-quality.css +312 -0
- package/renderer/styles-readability.css +862 -0
- package/renderer/styles-responsive-product.css +84 -12
- package/renderer/styles-responsive-runtime.css +22 -14
- package/renderer/styles-responsive-shell.css +44 -48
- package/renderer/styles-responsive-workflows.css +37 -3
- package/renderer/styles-run-composer.css +5 -0
- package/renderer/styles-runtime-overview.css +285 -0
- package/renderer/styles-settings.css +160 -0
- package/renderer/styles-terminal.css +339 -2
- package/renderer/styles-workflow-map.css +362 -15
- package/renderer/styles-workflows.css +69 -2
- package/renderer/styles.css +27 -12
- package/renderer/terminal-agent.js +17 -13
- package/renderer/terminal-events.js +233 -40
- package/renderer/terminal-workbench.js +165 -77
- package/renderer/terminal.js +341 -34
- package/src/agentMonitor/claudeParser.js +96 -7
- package/src/agentMonitor/codexParser.js +59 -4
- package/src/agentMonitor/executionActivity.js +282 -0
- package/src/agentMonitor/genericParser.js +56 -13
- package/src/agentMonitor/hierarchy.js +17 -0
- package/src/agentMonitor/responseIntent.js +51 -0
- package/src/agentMonitor.js +21 -3
- package/src/agentRunner.js +66 -1
- package/src/attentionNotifier.js +14 -10
- package/src/automationMonitor.js +258 -0
- package/src/contracts.js +10 -1
- package/src/ipc/registerAgentIpc.js +9 -3
- package/src/ipc/registerAppIpc.js +4 -1
- package/src/ipc/registerTerminalIpc.js +12 -6
- package/src/macUpdateHelper.js +210 -0
- package/src/monitorWorker.js +65 -6
- package/src/platformPath.js +80 -0
- package/src/processMonitor.js +80 -22
- package/src/providerVisibilityStore.js +45 -0
- package/src/sessionIntelligence.js +247 -0
- package/src/terminalHost.js +381 -0
- package/src/terminalHostDaemon.js +60 -0
- package/src/terminalManager.js +158 -3
- package/src/updateInstaller.js +175 -0
package/src/terminalManager.js
CHANGED
|
@@ -11,6 +11,9 @@ const { runBestEffort } = require('./diagnostics');
|
|
|
11
11
|
const MAX_SESSIONS = 24;
|
|
12
12
|
const MAX_INPUT_CHARS = 128 * 1024;
|
|
13
13
|
const MAX_REPLAY_CHARS = 2 * 1024 * 1024;
|
|
14
|
+
const MAX_STORE_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
const STORE_VERSION = 1;
|
|
16
|
+
const PERSIST_DELAY_MS = 150;
|
|
14
17
|
const TERMINAL_TYPES = new Set(['powershell', 'cmd', 'shell', 'wsl', 'tmux', 'agent']);
|
|
15
18
|
const AGENT_PROVIDERS = Object.freeze({
|
|
16
19
|
claude: { command: 'claude', label: 'Claude' },
|
|
@@ -218,6 +221,46 @@ function publicSession(session, includeReplay = false) {
|
|
|
218
221
|
return value;
|
|
219
222
|
}
|
|
220
223
|
|
|
224
|
+
function validTimestamp(value, fallback) {
|
|
225
|
+
const text = cleanText(value, 50);
|
|
226
|
+
return text && Number.isFinite(Date.parse(text)) ? new Date(text).toISOString() : fallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function restoredOptions(value = {}, platform = process.platform) {
|
|
230
|
+
const fallbackType = platform === 'win32' ? 'powershell' : 'shell';
|
|
231
|
+
const type = TERMINAL_TYPES.has(value.type) ? value.type : fallbackType;
|
|
232
|
+
const provider = cleanText(value.provider, 30).toLowerCase();
|
|
233
|
+
if (type === 'agent' && !AGENT_PROVIDERS[provider]) return null;
|
|
234
|
+
return {
|
|
235
|
+
type,
|
|
236
|
+
cwd: cleanText(value.cwd, 2_000) || os.homedir(),
|
|
237
|
+
distro: cleanText(value.distro, 100),
|
|
238
|
+
tmuxSession: cleanText(value.tmuxSession, 100),
|
|
239
|
+
tmuxPane: cleanText(value.tmuxPane, 100),
|
|
240
|
+
provider,
|
|
241
|
+
args: Array.isArray(value.args) ? value.args.slice(0, 80).map(item => cleanText(item, 2_000)) : [],
|
|
242
|
+
bridgeId: cleanText(value.bridgeId, 100),
|
|
243
|
+
title: cleanText(value.title, 100),
|
|
244
|
+
cols: numericDimension(value.cols, 120, 20, 500),
|
|
245
|
+
rows: numericDimension(value.rows, 32, 5, 200),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function persistedSession(session) {
|
|
250
|
+
return {
|
|
251
|
+
id: session.id,
|
|
252
|
+
options: { ...session.options, cols: session.cols, rows: session.rows },
|
|
253
|
+
title: session.title,
|
|
254
|
+
shell: session.shell,
|
|
255
|
+
status: session.status,
|
|
256
|
+
createdAt: session.createdAt,
|
|
257
|
+
updatedAt: session.updatedAt,
|
|
258
|
+
exitCode: session.exitCode,
|
|
259
|
+
signal: session.signal,
|
|
260
|
+
replay: session.replay,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
221
264
|
class TerminalManager extends EventEmitter {
|
|
222
265
|
constructor(options = {}) {
|
|
223
266
|
super();
|
|
@@ -225,7 +268,89 @@ class TerminalManager extends EventEmitter {
|
|
|
225
268
|
this.killTree = options.killTree || killPtyTree;
|
|
226
269
|
this.platform = options.platform || process.platform;
|
|
227
270
|
this.agentProviders = options.agentProviders || AGENT_PROVIDERS;
|
|
271
|
+
this.fileSystem = options.fileSystem || fs;
|
|
272
|
+
this.storeFile = typeof options.storeFile === 'string' && options.storeFile.trim()
|
|
273
|
+
? path.resolve(options.storeFile)
|
|
274
|
+
: '';
|
|
275
|
+
this.onPersistenceError = typeof options.onPersistenceError === 'function'
|
|
276
|
+
? options.onPersistenceError
|
|
277
|
+
: () => {};
|
|
278
|
+
this.persistTimer = null;
|
|
228
279
|
this.sessions = new Map();
|
|
280
|
+
this.loadPersistedSessions();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
persistenceError(operation, error) {
|
|
284
|
+
runBestEffort(`terminal-persistence:${operation}`, () => this.onPersistenceError(operation, error));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
loadPersistedSessions() {
|
|
288
|
+
if (!this.storeFile) return;
|
|
289
|
+
try {
|
|
290
|
+
const stat = this.fileSystem.statSync(this.storeFile);
|
|
291
|
+
if (!stat.isFile() || stat.size > MAX_STORE_BYTES) throw new Error('터미널 기록 파일의 크기가 허용 범위를 초과했습니다.');
|
|
292
|
+
const parsed = JSON.parse(this.fileSystem.readFileSync(this.storeFile, 'utf8'));
|
|
293
|
+
if (parsed?.version !== STORE_VERSION || !Array.isArray(parsed.sessions)) throw new Error('지원하지 않는 터미널 기록 형식입니다.');
|
|
294
|
+
for (const value of parsed.sessions.slice(0, MAX_SESSIONS)) {
|
|
295
|
+
const id = cleanText(value?.id, 200);
|
|
296
|
+
const options = restoredOptions(value?.options, this.platform);
|
|
297
|
+
if (!id || !options || this.sessions.has(id)) continue;
|
|
298
|
+
const now = new Date().toISOString();
|
|
299
|
+
const createdAt = validTimestamp(value.createdAt, now);
|
|
300
|
+
const updatedAt = validTimestamp(value.updatedAt, createdAt);
|
|
301
|
+
const status = value.status === 'failed' ? 'failed' : 'exited';
|
|
302
|
+
this.sessions.set(id, {
|
|
303
|
+
id,
|
|
304
|
+
options,
|
|
305
|
+
spec: null,
|
|
306
|
+
title: cleanText(value.title, 100) || options.title || options.tmuxSession || options.provider || options.type,
|
|
307
|
+
shell: cleanText(value.shell, 2_000),
|
|
308
|
+
pid: null,
|
|
309
|
+
status,
|
|
310
|
+
createdAt,
|
|
311
|
+
updatedAt,
|
|
312
|
+
exitCode: Number.isFinite(value.exitCode) ? value.exitCode : null,
|
|
313
|
+
signal: Number.isFinite(value.signal) ? value.signal : null,
|
|
314
|
+
cols: options.cols,
|
|
315
|
+
rows: options.rows,
|
|
316
|
+
replay: String(value.replay || '').slice(-MAX_REPLAY_CHARS),
|
|
317
|
+
process: null,
|
|
318
|
+
generation: 0,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error?.code !== 'ENOENT') this.persistenceError('load', error);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
schedulePersist() {
|
|
327
|
+
if (!this.storeFile || this.persistTimer) return;
|
|
328
|
+
this.persistTimer = setTimeout(() => {
|
|
329
|
+
this.persistTimer = null;
|
|
330
|
+
this.persistNow();
|
|
331
|
+
}, PERSIST_DELAY_MS);
|
|
332
|
+
if (typeof this.persistTimer.unref === 'function') this.persistTimer.unref();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
persistNow() {
|
|
336
|
+
if (!this.storeFile) return;
|
|
337
|
+
if (this.persistTimer) {
|
|
338
|
+
clearTimeout(this.persistTimer);
|
|
339
|
+
this.persistTimer = null;
|
|
340
|
+
}
|
|
341
|
+
const temporary = `${this.storeFile}.${process.pid}.tmp`;
|
|
342
|
+
try {
|
|
343
|
+
this.fileSystem.mkdirSync(path.dirname(this.storeFile), { recursive: true });
|
|
344
|
+
const payload = {
|
|
345
|
+
version: STORE_VERSION,
|
|
346
|
+
sessions: [...this.sessions.values()].map(persistedSession),
|
|
347
|
+
};
|
|
348
|
+
this.fileSystem.writeFileSync(temporary, JSON.stringify(payload), 'utf8');
|
|
349
|
+
this.fileSystem.renameSync(temporary, this.storeFile);
|
|
350
|
+
} catch (error) {
|
|
351
|
+
runBestEffort('terminal-persistence-temp-cleanup', () => this.fileSystem.unlinkSync(temporary));
|
|
352
|
+
this.persistenceError('save', error);
|
|
353
|
+
}
|
|
229
354
|
}
|
|
230
355
|
|
|
231
356
|
pty() {
|
|
@@ -261,14 +386,22 @@ class TerminalManager extends EventEmitter {
|
|
|
261
386
|
try {
|
|
262
387
|
this.spawn(session);
|
|
263
388
|
} catch (error) {
|
|
264
|
-
|
|
265
|
-
|
|
389
|
+
// Keep failed launches visible until the user explicitly closes them.
|
|
390
|
+
// The failed session contains the startup error in replay and can be
|
|
391
|
+
// inspected, restarted, or removed from the session terminal.
|
|
392
|
+
this.persistNow();
|
|
266
393
|
throw error;
|
|
267
394
|
}
|
|
395
|
+
this.persistNow();
|
|
268
396
|
return publicSession(session, true);
|
|
269
397
|
}
|
|
270
398
|
|
|
271
399
|
spawn(session) {
|
|
400
|
+
if (!session.spec) {
|
|
401
|
+
session.options = normalizeLaunchOptions(session.options, this.platform);
|
|
402
|
+
session.spec = launchSpec(session.options, this.platform, this.agentProviders);
|
|
403
|
+
session.shell = session.spec.file;
|
|
404
|
+
}
|
|
272
405
|
const generation = ++session.generation;
|
|
273
406
|
session.status = 'starting';
|
|
274
407
|
session.exitCode = null;
|
|
@@ -296,6 +429,7 @@ class TerminalManager extends EventEmitter {
|
|
|
296
429
|
session.replay = `${session.replay}${text}`.slice(-MAX_REPLAY_CHARS);
|
|
297
430
|
session.updatedAt = new Date().toISOString();
|
|
298
431
|
this.emit('data', { id: session.id, data: text });
|
|
432
|
+
this.schedulePersist();
|
|
299
433
|
});
|
|
300
434
|
processHandle.onExit(event => {
|
|
301
435
|
processHandle.__loadtoagentExited = true;
|
|
@@ -322,6 +456,7 @@ class TerminalManager extends EventEmitter {
|
|
|
322
456
|
|
|
323
457
|
emitState(change, session) {
|
|
324
458
|
this.emit('state', { change, session: session ? publicSession(session, false) : null, sessions: this.list() });
|
|
459
|
+
this.schedulePersist();
|
|
325
460
|
}
|
|
326
461
|
|
|
327
462
|
list() {
|
|
@@ -360,6 +495,7 @@ class TerminalManager extends EventEmitter {
|
|
|
360
495
|
session.cols = numericDimension(cols, session.cols, 20, 500);
|
|
361
496
|
session.rows = numericDimension(rows, session.rows, 5, 200);
|
|
362
497
|
if (session.process && session.status === 'running') session.process.resize(session.cols, session.rows);
|
|
498
|
+
this.schedulePersist();
|
|
363
499
|
return { ok: true, cols: session.cols, rows: session.rows };
|
|
364
500
|
}
|
|
365
501
|
|
|
@@ -387,6 +523,7 @@ class TerminalManager extends EventEmitter {
|
|
|
387
523
|
session.status = 'exited';
|
|
388
524
|
session.updatedAt = new Date().toISOString();
|
|
389
525
|
this.emitState('updated', session);
|
|
526
|
+
this.persistNow();
|
|
390
527
|
return { ok: true };
|
|
391
528
|
}
|
|
392
529
|
|
|
@@ -415,13 +552,31 @@ class TerminalManager extends EventEmitter {
|
|
|
415
552
|
session.updatedAt = new Date().toISOString();
|
|
416
553
|
this.sessions.delete(session.id);
|
|
417
554
|
this.emit('state', { change: 'removed', session: publicSession(session, false), sessions: this.list() });
|
|
555
|
+
this.persistNow();
|
|
418
556
|
return { ok: true };
|
|
419
557
|
}
|
|
420
558
|
|
|
421
|
-
dispose() {
|
|
559
|
+
dispose({ preserveSessions = false } = {}) {
|
|
560
|
+
if (preserveSessions) {
|
|
561
|
+
const now = new Date().toISOString();
|
|
562
|
+
for (const session of this.sessions.values()) {
|
|
563
|
+
if (session.process) {
|
|
564
|
+
const handle = session.process;
|
|
565
|
+
session.process = null;
|
|
566
|
+
session.generation += 1;
|
|
567
|
+
this.killTree(handle, session.pid);
|
|
568
|
+
}
|
|
569
|
+
if (session.status === 'running' || session.status === 'starting') session.status = 'exited';
|
|
570
|
+
session.pid = null;
|
|
571
|
+
session.updatedAt = now;
|
|
572
|
+
}
|
|
573
|
+
this.persistNow();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
422
576
|
for (const id of [...this.sessions.keys()]) {
|
|
423
577
|
runBestEffort(`terminal-dispose:${id}`, () => this.close(id));
|
|
424
578
|
}
|
|
579
|
+
this.persistNow();
|
|
425
580
|
}
|
|
426
581
|
}
|
|
427
582
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn: spawnProcess } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const MAC_UPDATE_HELPER_SOURCE = path.join(__dirname, 'macUpdateHelper.js');
|
|
8
|
+
|
|
9
|
+
const WINDOWS_UPDATE_HELPER = `param(
|
|
10
|
+
[Parameter(Mandatory = $true)][string]$InstallerPath,
|
|
11
|
+
[Parameter(Mandatory = $true)][int]$ParentPid,
|
|
12
|
+
[Parameter(Mandatory = $true)][string]$AppPath,
|
|
13
|
+
[Parameter(Mandatory = $true)][string]$LogPath
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
$ErrorActionPreference = 'Stop'
|
|
17
|
+
$exitCode = -1
|
|
18
|
+
try {
|
|
19
|
+
Wait-Process -Id $ParentPid -ErrorAction SilentlyContinue
|
|
20
|
+
$installer = Start-Process -FilePath $InstallerPath -ArgumentList '/S' -PassThru -Wait -WindowStyle Hidden
|
|
21
|
+
$exitCode = $installer.ExitCode
|
|
22
|
+
} catch {
|
|
23
|
+
$_ | Out-String | Set-Content -LiteralPath $LogPath -Encoding UTF8
|
|
24
|
+
} finally {
|
|
25
|
+
try { "exitCode=$exitCode" | Add-Content -LiteralPath $LogPath -Encoding UTF8 } catch {}
|
|
26
|
+
if ($exitCode -ne 0) {
|
|
27
|
+
try { "updateFailed=true" | Add-Content -LiteralPath $LogPath -Encoding UTF8 } catch {}
|
|
28
|
+
}
|
|
29
|
+
if (Test-Path -LiteralPath $AppPath) {
|
|
30
|
+
Remove-Item Env:ELECTRON_RUN_AS_NODE -ErrorAction SilentlyContinue
|
|
31
|
+
try { Start-Process -FilePath $AppPath } catch {}
|
|
32
|
+
}
|
|
33
|
+
Remove-Item -LiteralPath $PSCommandPath -Force -ErrorAction SilentlyContinue
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function isWithinDirectory(file, directory) {
|
|
38
|
+
if (!file || !directory) return false;
|
|
39
|
+
const relative = path.relative(path.resolve(directory), path.resolve(file));
|
|
40
|
+
return relative !== '' && !relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function macAppBundlePath(executablePath) {
|
|
44
|
+
const normalized = path.posix.normalize(String(executablePath || '').replace(/\\/g, '/'));
|
|
45
|
+
const match = normalized.match(/^((?:\/|[A-Za-z]:\/).+?\.app)\/Contents\/MacOS\/[^/]+$/i);
|
|
46
|
+
return match ? match[1] : '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function automaticInstallPlatform({ platform, installType, installerPath, downloadsDir, appPath }) {
|
|
50
|
+
if (installType !== 'desktop' || !isWithinDirectory(installerPath, downloadsDir)) return '';
|
|
51
|
+
const fileName = path.basename(installerPath);
|
|
52
|
+
if (platform === 'win32' && /^LoadToAgent-Setup-[0-9A-Za-z.-]+\.exe$/i.test(fileName)) return 'win32';
|
|
53
|
+
if (platform === 'darwin' && /^LoadToAgent-[0-9A-Za-z.-]+-(?:arm64|x64)\.dmg$/i.test(fileName)) {
|
|
54
|
+
const appBundle = macAppBundlePath(appPath);
|
|
55
|
+
if (appBundle && appBundle !== '/Volumes' && !appBundle.startsWith('/Volumes/')) return 'darwin';
|
|
56
|
+
}
|
|
57
|
+
return '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function canInstallSilently(options) {
|
|
61
|
+
return Boolean(automaticInstallPlatform(options || {}));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function windowsPowerShell(environment = process.env) {
|
|
65
|
+
const systemRoot = String(environment.SystemRoot || environment.WINDIR || 'C:\\Windows');
|
|
66
|
+
return path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function waitForProcessSpawn(child, timeoutMs = 5000) {
|
|
70
|
+
if (!child || typeof child.once !== 'function' || typeof child.unref !== 'function') {
|
|
71
|
+
return Promise.reject(new Error('업데이트 설치 프로세스를 시작하지 못했습니다.'));
|
|
72
|
+
}
|
|
73
|
+
return new Promise((resolve, reject) => {
|
|
74
|
+
let settled = false;
|
|
75
|
+
const finish = error => {
|
|
76
|
+
if (settled) return;
|
|
77
|
+
settled = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
if (error) reject(error);
|
|
80
|
+
else resolve();
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => finish(new Error('업데이트 설치 프로세스 시작 확인 시간이 초과되었습니다.')), timeoutMs);
|
|
83
|
+
child.once('spawn', () => finish());
|
|
84
|
+
child.once('error', error => finish(error));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function launchDownloadedUpdate(options = {}) {
|
|
89
|
+
const installerPath = String(options.installerPath || '');
|
|
90
|
+
const downloadsDir = String(options.downloadsDir || '');
|
|
91
|
+
if (!installerPath || !fs.existsSync(installerPath)) throw new Error('내려받은 설치 파일을 찾지 못했습니다. 다시 다운로드해 주세요.');
|
|
92
|
+
|
|
93
|
+
const platform = String(options.platform || process.platform);
|
|
94
|
+
const automaticPlatform = automaticInstallPlatform({
|
|
95
|
+
platform,
|
|
96
|
+
installType: String(options.installType || ''),
|
|
97
|
+
installerPath,
|
|
98
|
+
downloadsDir,
|
|
99
|
+
appPath: String(options.appPath || ''),
|
|
100
|
+
});
|
|
101
|
+
if (!automaticPlatform) {
|
|
102
|
+
if (!options.shell || typeof options.shell.openPath !== 'function') throw new Error('설치 파일을 열 수 없습니다.');
|
|
103
|
+
const openError = await options.shell.openPath(installerPath);
|
|
104
|
+
if (openError) throw new Error(openError);
|
|
105
|
+
return { mode: 'manual' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const appPath = String(options.appPath || '');
|
|
109
|
+
const parentPid = Number(options.parentPid);
|
|
110
|
+
if (!appPath || !fs.existsSync(appPath) || !Number.isSafeInteger(parentPid) || parentPid <= 0) {
|
|
111
|
+
throw new Error('업데이트 후 앱을 다시 시작할 정보를 준비하지 못했습니다.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const spawn = options.spawn || spawnProcess;
|
|
115
|
+
if (automaticPlatform === 'darwin') {
|
|
116
|
+
const targetApp = macAppBundlePath(appPath);
|
|
117
|
+
if (!targetApp || !fs.existsSync(targetApp)) throw new Error('현재 설치된 macOS 앱을 찾지 못했습니다.');
|
|
118
|
+
const helperPath = path.join(downloadsDir, 'install-update-macos.js');
|
|
119
|
+
const logPath = path.join(downloadsDir, 'install-update.log');
|
|
120
|
+
const helperSource = await fs.promises.readFile(MAC_UPDATE_HELPER_SOURCE, 'utf8');
|
|
121
|
+
await fs.promises.writeFile(helperPath, helperSource, { encoding: 'utf8', mode: 0o700 });
|
|
122
|
+
const environment = { ...process.env, ...(options.environment || {}), ELECTRON_RUN_AS_NODE: '1' };
|
|
123
|
+
const child = spawn(appPath, [
|
|
124
|
+
helperPath,
|
|
125
|
+
'--dmg', installerPath,
|
|
126
|
+
'--target', targetApp,
|
|
127
|
+
'--parent-pid', String(parentPid),
|
|
128
|
+
'--log', logPath,
|
|
129
|
+
], {
|
|
130
|
+
detached: true,
|
|
131
|
+
stdio: 'ignore',
|
|
132
|
+
env: environment,
|
|
133
|
+
});
|
|
134
|
+
await waitForProcessSpawn(child, Number(options.spawnTimeoutMs) || 5000);
|
|
135
|
+
if (!Number.isSafeInteger(child.pid) || child.pid <= 0) throw new Error('업데이트 설치 프로세스를 시작하지 못했습니다.');
|
|
136
|
+
child.unref();
|
|
137
|
+
return { mode: 'automatic', helperPath, logPath, targetApp };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const helperPath = path.join(downloadsDir, 'install-update.ps1');
|
|
141
|
+
const logPath = path.join(downloadsDir, 'install-update.log');
|
|
142
|
+
await fs.promises.writeFile(helperPath, WINDOWS_UPDATE_HELPER, { encoding: 'utf8', mode: 0o600 });
|
|
143
|
+
const child = spawn(windowsPowerShell(options.environment), [
|
|
144
|
+
'-NoLogo',
|
|
145
|
+
'-NoProfile',
|
|
146
|
+
'-NonInteractive',
|
|
147
|
+
'-WindowStyle', 'Hidden',
|
|
148
|
+
'-ExecutionPolicy', 'Bypass',
|
|
149
|
+
'-File', helperPath,
|
|
150
|
+
'-InstallerPath', installerPath,
|
|
151
|
+
'-ParentPid', String(parentPid),
|
|
152
|
+
'-AppPath', appPath,
|
|
153
|
+
'-LogPath', logPath,
|
|
154
|
+
], {
|
|
155
|
+
detached: true,
|
|
156
|
+
windowsHide: true,
|
|
157
|
+
stdio: 'ignore',
|
|
158
|
+
});
|
|
159
|
+
await waitForProcessSpawn(child, Number(options.spawnTimeoutMs) || 5000);
|
|
160
|
+
if (!Number.isSafeInteger(child.pid) || child.pid <= 0) throw new Error('업데이트 설치 프로세스를 시작하지 못했습니다.');
|
|
161
|
+
child.unref();
|
|
162
|
+
return { mode: 'automatic', helperPath, logPath };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = {
|
|
166
|
+
MAC_UPDATE_HELPER_SOURCE,
|
|
167
|
+
WINDOWS_UPDATE_HELPER,
|
|
168
|
+
automaticInstallPlatform,
|
|
169
|
+
canInstallSilently,
|
|
170
|
+
isWithinDirectory,
|
|
171
|
+
launchDownloadedUpdate,
|
|
172
|
+
macAppBundlePath,
|
|
173
|
+
waitForProcessSpawn,
|
|
174
|
+
windowsPowerShell,
|
|
175
|
+
};
|