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.
Files changed (83) hide show
  1. package/README.ko.md +7 -0
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +23 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +135 -38
  9. package/package.json +13 -4
  10. package/preload.js +6 -0
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +190 -55
  14. package/renderer/app-drawer-content.js +48 -44
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +72 -54
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -13
  19. package/renderer/app-events-navigation.js +77 -30
  20. package/renderer/app-events-sessions.js +156 -9
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +20 -38
  23. package/renderer/app-graph-orchestration.js +15 -13
  24. package/renderer/app-graph-view.js +228 -113
  25. package/renderer/app-management.js +211 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +103 -33
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +52 -44
  32. package/renderer/app.js +153 -75
  33. package/renderer/i18n-messages.js +833 -16
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +145 -59
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +6 -0
  38. package/renderer/styles-cards.css +26 -0
  39. package/renderer/styles-components.css +86 -9
  40. package/renderer/styles-management.css +355 -0
  41. package/renderer/styles-overlays.css +8 -2
  42. package/renderer/styles-product.css +24 -16
  43. package/renderer/styles-quality.css +312 -0
  44. package/renderer/styles-readability.css +862 -0
  45. package/renderer/styles-responsive-product.css +84 -12
  46. package/renderer/styles-responsive-runtime.css +22 -14
  47. package/renderer/styles-responsive-shell.css +44 -48
  48. package/renderer/styles-responsive-workflows.css +37 -3
  49. package/renderer/styles-run-composer.css +5 -0
  50. package/renderer/styles-runtime-overview.css +285 -0
  51. package/renderer/styles-settings.css +160 -0
  52. package/renderer/styles-terminal.css +339 -2
  53. package/renderer/styles-workflow-map.css +362 -15
  54. package/renderer/styles-workflows.css +69 -2
  55. package/renderer/styles.css +27 -12
  56. package/renderer/terminal-agent.js +17 -13
  57. package/renderer/terminal-events.js +233 -40
  58. package/renderer/terminal-workbench.js +165 -77
  59. package/renderer/terminal.js +341 -34
  60. package/src/agentMonitor/claudeParser.js +96 -7
  61. package/src/agentMonitor/codexParser.js +59 -4
  62. package/src/agentMonitor/executionActivity.js +282 -0
  63. package/src/agentMonitor/genericParser.js +56 -13
  64. package/src/agentMonitor/hierarchy.js +17 -0
  65. package/src/agentMonitor/responseIntent.js +51 -0
  66. package/src/agentMonitor.js +21 -3
  67. package/src/agentRunner.js +66 -1
  68. package/src/attentionNotifier.js +14 -10
  69. package/src/automationMonitor.js +258 -0
  70. package/src/contracts.js +10 -1
  71. package/src/ipc/registerAgentIpc.js +9 -3
  72. package/src/ipc/registerAppIpc.js +4 -1
  73. package/src/ipc/registerTerminalIpc.js +12 -6
  74. package/src/macUpdateHelper.js +210 -0
  75. package/src/monitorWorker.js +65 -6
  76. package/src/platformPath.js +80 -0
  77. package/src/processMonitor.js +80 -22
  78. package/src/providerVisibilityStore.js +45 -0
  79. package/src/sessionIntelligence.js +247 -0
  80. package/src/terminalHost.js +381 -0
  81. package/src/terminalHostDaemon.js +60 -0
  82. package/src/terminalManager.js +158 -3
  83. package/src/updateInstaller.js +175 -0
@@ -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
- this.sessions.delete(id);
265
- this.emit('state', { change: 'removed', session: publicSession(session, false), sessions: this.list() });
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
+ };