fraim-hub 2.0.329 → 2.0.332

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 (31) hide show
  1. package/dist/src/ai-hub/background-command.js +87 -0
  2. package/dist/src/ai-hub/cert-store.js +30 -5
  3. package/dist/src/ai-hub/cli.js +2 -1
  4. package/dist/src/ai-hub/configured-agent-command.js +51 -0
  5. package/dist/src/ai-hub/configured-agents.js +11 -2
  6. package/dist/src/ai-hub/desktop-admission.js +53 -0
  7. package/dist/src/ai-hub/desktop-auto-updater.js +15 -9
  8. package/dist/src/ai-hub/desktop-installer-setup.js +19 -9
  9. package/dist/src/ai-hub/desktop-launcher.js +22 -3
  10. package/dist/src/ai-hub/desktop-main.js +149 -42
  11. package/dist/src/ai-hub/hosts.js +102 -7
  12. package/dist/src/ai-hub/hub-app-materializer.js +6 -5
  13. package/dist/src/ai-hub/hub-instance-reconciliation.js +65 -42
  14. package/dist/src/ai-hub/hub-latest-version.js +24 -7
  15. package/dist/src/ai-hub/hub-restart-plan.js +62 -0
  16. package/dist/src/ai-hub/office-sideload.js +47 -9
  17. package/dist/src/ai-hub/process-liveness.js +5 -0
  18. package/dist/src/ai-hub/server.js +9 -6
  19. package/dist/src/cli/doctor/checks/agent-cli-health-checks.js +15 -2
  20. package/dist/src/cli/doctor/redact-paths.js +78 -0
  21. package/dist/src/cli/setup/ide-detector.js +5 -5
  22. package/dist/src/cli/utils/machine-path.js +73 -5
  23. package/dist/src/cli/utils/managed-agent-paths.js +1 -0
  24. package/dist/src/core/utils/anthropic-auth-route.js +243 -0
  25. package/dist/src/core/utils/claude-subscription-login.js +92 -0
  26. package/dist/src/core/utils/env-var-names.js +64 -0
  27. package/dist/src/local-mcp-server/learning-context-builder.js +86 -14
  28. package/package.json +8 -3
  29. package/public/ai-hub/index.html +3 -3
  30. package/public/ai-hub/script.js +31 -3
  31. package/public/ai-hub/styles.css +31 -6
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.killBackgroundProcessTree = killBackgroundProcessTree;
7
+ exports.runBackgroundCommand = runBackgroundCommand;
8
+ const child_process_1 = require("child_process");
9
+ const tree_kill_1 = __importDefault(require("tree-kill"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const hub_main_diagnostics_1 = require("./hub-main-diagnostics");
12
+ function killBackgroundProcessTree(pid, signal = 'SIGTERM', callback = () => undefined) {
13
+ if (!Number.isSafeInteger(pid) || pid <= 0 || pid === process.pid) {
14
+ callback(new Error('Refusing invalid or current process termination'));
15
+ return;
16
+ }
17
+ if (process.platform === 'win32') {
18
+ (0, child_process_1.execFile)('taskkill', ['/PID', String(pid), '/T', '/F'], { windowsHide: true, timeout: 5000 }, callback);
19
+ }
20
+ else
21
+ (0, tree_kill_1.default)(pid, signal, callback);
22
+ }
23
+ /** Captured, bounded infrastructure work. Completion follows child close, including timeout cleanup. */
24
+ function runBackgroundCommand(command, args, options = {}) {
25
+ const started = performance.now();
26
+ // Log only an allowlisted executable category, never paths, arguments or output.
27
+ const executable = node_path_1.default.basename(command).toLowerCase().replace(/\.(exe|cmd|bat)$/, '');
28
+ const category = ['node', 'electron', 'npm', 'cmd', 'powershell', 'pwsh', 'reg', 'certutil', 'security'].includes(executable)
29
+ ? executable : 'other';
30
+ return new Promise((resolve) => {
31
+ const child = (0, child_process_1.spawn)(command, args, {
32
+ env: options.env ?? process.env, windowsHide: true,
33
+ windowsVerbatimArguments: options.windowsVerbatimArguments,
34
+ stdio: ['ignore', 'pipe', 'pipe'],
35
+ });
36
+ let stdout = '';
37
+ let stderr = '';
38
+ let error;
39
+ let outcome;
40
+ let termination;
41
+ const terminate = () => {
42
+ if (termination || !child.pid || child.exitCode !== null)
43
+ return;
44
+ const pid = child.pid;
45
+ termination = new Promise((done) => {
46
+ const ended = (failure) => {
47
+ if (failure && child.exitCode === null) {
48
+ error = failure;
49
+ child.kill('SIGKILL');
50
+ }
51
+ done();
52
+ };
53
+ killBackgroundProcessTree(pid, 'SIGKILL', ended);
54
+ });
55
+ };
56
+ const timer = setTimeout(() => {
57
+ outcome = 'timeout';
58
+ error = new Error(`Background command exceeded ${options.timeoutMs ?? 15000}ms deadline`);
59
+ terminate();
60
+ }, options.timeoutMs ?? 15000);
61
+ const capture = (chunk, stream) => {
62
+ if (stream === 'stdout')
63
+ stdout += chunk.toString();
64
+ else
65
+ stderr += chunk.toString();
66
+ if (stdout.length + stderr.length > 1024 * 1024) {
67
+ outcome = 'output-limit';
68
+ stdout = stdout.slice(0, 512 * 1024);
69
+ stderr = stderr.slice(0, 512 * 1024);
70
+ error = new Error('Background command exceeded output limit');
71
+ terminate();
72
+ }
73
+ };
74
+ child.stdout.on('data', chunk => capture(chunk, 'stdout'));
75
+ child.stderr.on('data', chunk => capture(chunk, 'stderr'));
76
+ child.on('error', failure => { error = failure; outcome = 'spawn-error'; });
77
+ child.on('close', async (status, signal) => {
78
+ clearTimeout(timer);
79
+ await termination;
80
+ (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.background_command', {
81
+ category, outcome: outcome ?? (status === 0 ? 'success' : signal ? 'terminated' : 'nonzero-exit'),
82
+ exitCode: status, signal, durationMs: performance.now() - started,
83
+ });
84
+ resolve({ status: error ? null : status, stdout, stderr, error });
85
+ });
86
+ });
87
+ }
@@ -4,6 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports._internal = void 0;
7
+ exports.isCertTrustedAsync = isCertTrustedAsync;
8
+ exports.trustCertAsync = trustCertAsync;
7
9
  exports.certPaths = certPaths;
8
10
  exports.loadOrCreateCert = loadOrCreateCert;
9
11
  exports.trustCert = trustCert;
@@ -15,6 +17,29 @@ const path_1 = __importDefault(require("path"));
15
17
  const os_1 = __importDefault(require("os"));
16
18
  const crypto_1 = __importDefault(require("crypto"));
17
19
  const child_process_1 = require("child_process");
20
+ const background_command_1 = require("./background-command");
21
+ async function isCertTrustedAsync(certPath) {
22
+ if (!fs_1.default.existsSync(certPath))
23
+ return false;
24
+ const fingerprint = computeCertFingerprint(fs_1.default.readFileSync(certPath, 'utf8'));
25
+ const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
26
+ if (process.platform !== 'win32' && process.platform !== 'darwin')
27
+ return false;
28
+ const result = process.platform === 'win32'
29
+ ? await exports._internal.runBackgroundCommand('certutil', ['-store', '-user', 'Root'])
30
+ : await exports._internal.runBackgroundCommand('security', ['find-certificate', '-a', '-c', 'localhost', '-Z', keychain]);
31
+ return result.status === 0 && result.stdout.toLowerCase().replace(/:/g, '').includes(fingerprint);
32
+ }
33
+ async function trustCertAsync(certPath) {
34
+ if (process.platform !== 'win32' && process.platform !== 'darwin')
35
+ return;
36
+ const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
37
+ const result = process.platform === 'win32'
38
+ ? await exports._internal.runBackgroundCommand('certutil', ['-addstore', '-user', 'Root', certPath], { timeoutMs: 120000 })
39
+ : await exports._internal.runBackgroundCommand('security', ['add-trusted-cert', '-r', 'trustRoot', '-k', keychain, certPath], { timeoutMs: 120000 });
40
+ if (result.status !== 0)
41
+ throw result.error ?? new Error(result.stderr || 'Certificate trust failed');
42
+ }
18
43
  function certDir() {
19
44
  const base = process.env.APPDATA ||
20
45
  (process.platform === 'darwin'
@@ -58,7 +83,7 @@ async function loadOrCreateCert() {
58
83
  * Swapping the export on Node's built-in `child_process` module itself is not reliable for
59
84
  * this (confirmed empirically while fixing this issue) - this repo-owned seam is.
60
85
  */
61
- exports._internal = { spawnSync: child_process_1.spawnSync };
86
+ exports._internal = { spawnSync: child_process_1.spawnSync, runBackgroundCommand: background_command_1.runBackgroundCommand };
62
87
  /**
63
88
  * Installs the cert as a trusted root CA in the user's OS certificate store.
64
89
  * ONLY call this for explicit Word-Online support — it prompts a Windows security
@@ -69,7 +94,7 @@ exports._internal = { spawnSync: child_process_1.spawnSync };
69
94
  */
70
95
  function trustCert(certPath) {
71
96
  if (process.platform === 'win32') {
72
- const result = exports._internal.spawnSync('certutil', ['-addstore', '-user', 'Root', certPath], { encoding: 'utf8' });
97
+ const result = exports._internal.spawnSync('certutil', ['-addstore', '-user', 'Root', certPath], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
73
98
  if (result.status !== 0) {
74
99
  console.warn('[fraim] could not trust localhost certificate:', result.stderr || result.stdout);
75
100
  }
@@ -83,7 +108,7 @@ function trustCert(certPath) {
83
108
  /** Removes a specific cert from the trust store, by fingerprint. Test-only helper. */
84
109
  function untrustCert(fingerprint) {
85
110
  if (process.platform === 'win32') {
86
- exports._internal.spawnSync('certutil', ['-delstore', '-user', 'Root', fingerprint], { encoding: 'utf8' });
111
+ exports._internal.spawnSync('certutil', ['-delstore', '-user', 'Root', fingerprint], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
87
112
  return;
88
113
  }
89
114
  if (process.platform === 'darwin') {
@@ -116,14 +141,14 @@ function isCertTrusted(certPath) {
116
141
  return false;
117
142
  const fingerprint = computeCertFingerprint(fs_1.default.readFileSync(certPath, 'utf8'));
118
143
  if (process.platform === 'win32') {
119
- const result = (0, child_process_1.spawnSync)('certutil', ['-store', '-user', 'Root'], { encoding: 'utf8' });
144
+ const result = (0, child_process_1.spawnSync)('certutil', ['-store', '-user', 'Root'], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
120
145
  if (result.status !== 0)
121
146
  return false;
122
147
  return result.stdout.toLowerCase().includes(fingerprint);
123
148
  }
124
149
  if (process.platform === 'darwin') {
125
150
  const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
126
- const result = (0, child_process_1.spawnSync)('security', ['find-certificate', '-a', '-c', 'localhost', '-Z', keychain], { encoding: 'utf8' });
151
+ const result = (0, child_process_1.spawnSync)('security', ['find-certificate', '-a', '-c', 'localhost', '-Z', keychain], { encoding: 'utf8', windowsHide: true, timeout: 15000 });
127
152
  if (result.status !== 0)
128
153
  return false;
129
154
  return result.stdout.toLowerCase().replace(/:/g, '').includes(fingerprint);
@@ -90,6 +90,7 @@ async function openDesktopWindow(projectPath, preferredPort, runtimeId) {
90
90
  args.push('--hub-runtime-id', runtimeId);
91
91
  }
92
92
  const child = (0, child_process_1.spawn)(electron.binaryPath, args, {
93
+ windowsHide: true,
93
94
  detached: true,
94
95
  stdio: 'ignore',
95
96
  });
@@ -97,7 +98,7 @@ async function openDesktopWindow(projectPath, preferredPort, runtimeId) {
97
98
  }
98
99
  function openBrowser(url) {
99
100
  if (process.platform === 'win32') {
100
- const child = (0, child_process_1.spawn)('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore' });
101
+ const child = (0, child_process_1.spawn)('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true });
101
102
  child.unref();
102
103
  return;
103
104
  }
@@ -5,10 +5,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.parseConfiguredCommand = parseConfiguredCommand;
7
7
  exports.describeConfiguredCommandInvocationProblem = describeConfiguredCommandInvocationProblem;
8
+ exports.__setCommandResolutionProbeForTests = __setCommandResolutionProbeForTests;
9
+ exports.probeCommandResolvesNonInteractively = probeCommandResolvesNonInteractively;
8
10
  exports.commandContainsLiteralSecret = commandContainsLiteralSecret;
9
11
  exports.decorateHostPlanWithConfiguredAgent = decorateHostPlanWithConfiguredAgent;
10
12
  const fs_1 = __importDefault(require("fs"));
11
13
  const path_1 = __importDefault(require("path"));
14
+ const child_process_1 = require("child_process");
12
15
  const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
13
16
  const BASH_ENV_PREFIX = /^([A-Za-z_][A-Za-z0-9_]*)=(?:\$\([^)]*\)|"[^"]*"|'[^']*'|\S*)\s+/;
14
17
  // The value may be a parenthesized PowerShell expression followed by a
@@ -51,6 +54,54 @@ function describeConfiguredCommandInvocationProblem(parsed, baseHostId, agentLab
51
54
  }
52
55
  return null;
53
56
  }
57
+ let commandResolutionProbeOverride = null;
58
+ function __setCommandResolutionProbeForTests(stub) {
59
+ commandResolutionProbeOverride = stub;
60
+ }
61
+ const PROBE_CACHE_TTL_MS = 60_000;
62
+ const NON_INTERACTIVE_PROBE_TIMEOUT_MS = 3_000;
63
+ const probeCache = new Map();
64
+ // Probes whether commandName is resolvable in the non-interactive shell context
65
+ // that Hub actually uses to launch configured agents (bash --noprofile --norc,
66
+ // or powershell -NoProfile). Commands defined only as profile functions or
67
+ // aliases are unavailable in that context and would cause silent launch failure.
68
+ // Results are cached for PROBE_CACHE_TTL_MS to avoid repeated spawnSync calls
69
+ // on frequent availability checks.
70
+ function probeCommandResolvesNonInteractively(runner, commandName) {
71
+ if (commandResolutionProbeOverride !== null)
72
+ return commandResolutionProbeOverride(runner, commandName);
73
+ const key = `${runner}\x00${commandName}`;
74
+ const cached = probeCache.get(key);
75
+ if (cached && Date.now() - cached.at < PROBE_CACHE_TTL_MS)
76
+ return cached.result;
77
+ let result;
78
+ if (runner === 'powershell') {
79
+ if (process.platform !== 'win32') {
80
+ result = null;
81
+ }
82
+ else {
83
+ const r = (0, child_process_1.spawnSync)('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `Get-Command -Name ${JSON.stringify(commandName)} -ErrorAction Stop`], { encoding: 'utf8', timeout: NON_INTERACTIVE_PROBE_TIMEOUT_MS });
84
+ result = r.error || r.status !== 0
85
+ ? `Command '${commandName}' not found in non-interactive PowerShell (powershell -NoProfile). ` +
86
+ `This command may be a PowerShell profile function or alias that is unavailable without a profile. ` +
87
+ `Create a .cmd wrapper on PATH that calls the real executable directly.`
88
+ : null;
89
+ }
90
+ }
91
+ else {
92
+ const r = (0, child_process_1.spawnSync)('bash', ['--noprofile', '--norc', '-c', `command -v ${commandName}`], {
93
+ encoding: 'utf8',
94
+ timeout: NON_INTERACTIVE_PROBE_TIMEOUT_MS,
95
+ });
96
+ result = r.error || r.status !== 0
97
+ ? `Command '${commandName}' not found in non-interactive bash (bash --noprofile --norc). ` +
98
+ `This command may be defined only in a shell profile. ` +
99
+ `Add the command to a PATH location accessible without sourcing any profile.`
100
+ : null;
101
+ }
102
+ probeCache.set(key, { result, at: Date.now() });
103
+ return result;
104
+ }
54
105
  function tokenizeShellLike(text) {
55
106
  const tokens = [];
56
107
  const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.describeConfiguredCommandInvocationProblem = exports.decorateHostPlanWithConfiguredAgent = exports.commandContainsLiteralSecret = exports.parseConfiguredCommand = exports.AiHubConfiguredAgentStore = void 0;
6
+ exports.__setCommandResolutionProbeForTests = exports.probeCommandResolvesNonInteractively = exports.describeConfiguredCommandInvocationProblem = exports.decorateHostPlanWithConfiguredAgent = exports.commandContainsLiteralSecret = exports.parseConfiguredCommand = exports.AiHubConfiguredAgentStore = void 0;
7
7
  exports.synthesizeDefaultConfiguredAgents = synthesizeDefaultConfiguredAgents;
8
8
  exports.checkConfiguredAgentAvailability = checkConfiguredAgentAvailability;
9
9
  exports.checkConfiguredAgentReadiness = checkConfiguredAgentReadiness;
@@ -182,8 +182,14 @@ function checkConfiguredAgentAvailability(agent, hosts, env = process.env) {
182
182
  try {
183
183
  const parsed = (0, configured_agent_command_1.parseConfiguredCommand)(agent.command.runner, agent.command.command);
184
184
  const problem = (0, configured_agent_command_1.describeConfiguredCommandInvocationProblem)(parsed, agent.baseHostId, agent.label);
185
- if (problem)
185
+ if (problem) {
186
186
  reasons.push(problem);
187
+ }
188
+ else if (parsed.invocationHead) {
189
+ const probeError = (0, configured_agent_command_1.probeCommandResolvesNonInteractively)(agent.command.runner, parsed.invocationHead);
190
+ if (probeError)
191
+ reasons.push(probeError);
192
+ }
187
193
  }
188
194
  catch (error) {
189
195
  reasons.push(error instanceof Error ? error.message : 'Configured agent command could not be parsed.');
@@ -304,6 +310,7 @@ function runSetupScript(setupScript) {
304
310
  ...args,
305
311
  ]];
306
312
  const result = (0, child_process_1.spawnSync)(invocation[0], invocation[1], {
313
+ windowsHide: true,
307
314
  encoding: 'utf8',
308
315
  timeout: setupScript.timeoutMs || 30_000,
309
316
  env: captureEnv,
@@ -348,3 +355,5 @@ Object.defineProperty(exports, "parseConfiguredCommand", { enumerable: true, get
348
355
  Object.defineProperty(exports, "commandContainsLiteralSecret", { enumerable: true, get: function () { return configured_agent_command_2.commandContainsLiteralSecret; } });
349
356
  Object.defineProperty(exports, "decorateHostPlanWithConfiguredAgent", { enumerable: true, get: function () { return configured_agent_command_2.decorateHostPlanWithConfiguredAgent; } });
350
357
  Object.defineProperty(exports, "describeConfiguredCommandInvocationProblem", { enumerable: true, get: function () { return configured_agent_command_2.describeConfiguredCommandInvocationProblem; } });
358
+ Object.defineProperty(exports, "probeCommandResolvesNonInteractively", { enumerable: true, get: function () { return configured_agent_command_2.probeCommandResolvesNonInteractively; } });
359
+ Object.defineProperty(exports, "__setCommandResolutionProbeForTests", { enumerable: true, get: function () { return configured_agent_command_2.__setCommandResolutionProbeForTests; } });
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.admitDesktopInstance = admitDesktopInstance;
7
+ exports.attachDesktopFocusHandler = attachDesktopFocusHandler;
8
+ const electron_1 = require("electron");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ // A materialized payload has its own module cache. The process owns admission,
11
+ // so both copies share it across the launcher-to-application handoff.
12
+ const admissionKey = Symbol.for('fraim.desktop.admission');
13
+ const processState = globalThis;
14
+ function admitDesktopInstance() {
15
+ if (processState[admissionKey])
16
+ return processState[admissionKey].admitted;
17
+ const runtimeArg = process.argv.indexOf('--hub-runtime-id');
18
+ const runtimeId = (runtimeArg >= 0 ? process.argv[runtimeArg + 1] : undefined)
19
+ || process.env.FRAIM_HUB_RUNTIME_ID || 'hub';
20
+ electron_1.app.setName(process.env.FRAIM_HUB_DISPLAY_NAME || (runtimeId === 'hub2' ? 'FRAIM Hub 2' : 'FRAIM Hub'));
21
+ electron_1.app.setAppUserModelId('ai.fraim.hub');
22
+ const userDataDir = process.env.FRAIM_AI_HUB_USER_DATA_DIR;
23
+ if (userDataDir) {
24
+ fs_1.default.mkdirSync(userDataDir, { recursive: true });
25
+ electron_1.app.setPath('userData', userDataDir);
26
+ }
27
+ const state = {
28
+ admitted: process.env.FRAIM_AI_HUB_FAKE_HOST === '1' || electron_1.app.requestSingleInstanceLock(),
29
+ pendingFocus: false,
30
+ };
31
+ processState[admissionKey] = state;
32
+ if (!state.admitted) {
33
+ electron_1.app.quit();
34
+ return false;
35
+ }
36
+ electron_1.app.on('second-instance', () => {
37
+ if (state.focus)
38
+ state.focus();
39
+ else
40
+ state.pendingFocus = true;
41
+ });
42
+ return true;
43
+ }
44
+ function attachDesktopFocusHandler(focus) {
45
+ const state = processState[admissionKey];
46
+ if (!state?.admitted)
47
+ return;
48
+ state.focus = focus;
49
+ if (state.pendingFocus) {
50
+ state.pendingFocus = false;
51
+ focus();
52
+ }
53
+ }
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.runMandatoryDesktopUpdateCheck = runMandatoryDesktopUpdateCheck;
4
+ function beforeDeadline(operation, timeoutMs, onTimeout) {
5
+ return new Promise((resolve, reject) => {
6
+ const timer = setTimeout(() => {
7
+ reject(new Error(`Desktop update exceeded ${timeoutMs}ms deadline`));
8
+ onTimeout?.();
9
+ }, timeoutMs);
10
+ operation.then(resolve, reject).finally(() => clearTimeout(timer));
11
+ });
12
+ }
4
13
  let updateCheckInFlight = null;
5
14
  function runMandatoryDesktopUpdateCheck(options) {
6
15
  if (!options.isPackaged)
@@ -17,7 +26,7 @@ async function runMandatoryDesktopUpdateCheckOnce(options) {
17
26
  const { updater, prompt, currentVersion, logger = console } = options;
18
27
  updater.autoDownload = false;
19
28
  updater.autoInstallOnAppQuit = false;
20
- const checkResult = await checkForDesktopUpdate(updater, prompt, currentVersion, logger);
29
+ const checkResult = await checkForDesktopUpdate(updater, prompt, currentVersion, logger, options.checkTimeoutMs ?? 4000);
21
30
  if (checkResult.action === 'check-failed')
22
31
  return checkResult;
23
32
  if (!checkResult.update?.isUpdateAvailable) {
@@ -25,14 +34,11 @@ async function runMandatoryDesktopUpdateCheckOnce(options) {
25
34
  return { action: 'current', availableVersion: checkResult.update?.updateInfo?.version };
26
35
  }
27
36
  const availableVersion = checkResult.update.updateInfo?.version;
37
+ const cancellationToken = checkResult.update.cancellationToken
38
+ ?? new (require('builder-util-runtime').CancellationToken)();
28
39
  try {
29
40
  logger.info(`[fraim] desktop update ${availableVersion ?? 'unknown'} available; downloading`);
30
- if (checkResult.update.downloadPromise) {
31
- await checkResult.update.downloadPromise;
32
- }
33
- else {
34
- await updater.downloadUpdate();
35
- }
41
+ await beforeDeadline(checkResult.update.downloadPromise ?? updater.downloadUpdate(cancellationToken), options.downloadTimeoutMs ?? 120000, () => cancellationToken.cancel());
36
42
  }
37
43
  catch (error) {
38
44
  const message = errorMessage(error);
@@ -58,10 +64,10 @@ async function runMandatoryDesktopUpdateCheckOnce(options) {
58
64
  return { action: 'install-failed', availableVersion, error: message };
59
65
  }
60
66
  }
61
- async function checkForDesktopUpdate(updater, prompt, currentVersion, logger) {
67
+ async function checkForDesktopUpdate(updater, prompt, currentVersion, logger, timeoutMs) {
62
68
  try {
63
69
  logger.info(`[fraim] checking for desktop update from ${currentVersion}`);
64
- return { action: 'checked', update: await updater.checkForUpdates() };
70
+ return { action: 'checked', update: await beforeDeadline(updater.checkForUpdates(), timeoutMs) };
65
71
  }
66
72
  catch (error) {
67
73
  const message = errorMessage(error);
@@ -3,18 +3,28 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isDesktopRuntimeComplete = isDesktopRuntimeComplete;
6
7
  exports.readInstallerSetupApiKey = readInstallerSetupApiKey;
7
8
  exports.repairInstallerIdeMcpEntries = repairInstallerIdeMcpEntries;
9
+ exports.runDesktopRuntimeSetup = runDesktopRuntimeSetup;
8
10
  exports.runDesktopInstallerSetup = runDesktopInstallerSetup;
9
11
  const fs_1 = __importDefault(require("fs"));
10
12
  const path_1 = __importDefault(require("path"));
11
- const child_process_1 = require("child_process");
12
13
  const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
13
14
  const ide_detector_1 = require("../cli/setup/ide-detector");
14
15
  const auto_mcp_setup_1 = require("../cli/setup/auto-mcp-setup");
15
16
  const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
16
17
  const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
17
- const win32_cmd_spawn_1 = require("../cli/utils/win32-cmd-spawn");
18
+ const managed_agent_paths_1 = require("../cli/utils/managed-agent-paths");
19
+ const hub_app_materializer_1 = require("./hub-app-materializer");
20
+ function isDesktopRuntimeComplete() {
21
+ if (![(0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)(), (0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)(), (0, fraim_mcp_latest_launcher_1.getFraimNpxShimPath)(), (0, fraim_mcp_latest_launcher_1.getFraimMcpLatestLauncherPath)()]
22
+ .every(file => fs_1.default.existsSync(file)))
23
+ return false;
24
+ const systemNpm = (0, managed_node_runtime_1.findExecutableOnPath)((0, managed_node_runtime_1.npmExecutableName)());
25
+ return (systemNpm !== null && (0, managed_node_runtime_1.hasWorkingNodeAndNpm)(path_1.default.dirname(systemNpm)))
26
+ || (0, managed_node_runtime_1.hasWorkingNodeAndNpm)((0, managed_agent_paths_1.getPortableNodeBinPath)());
27
+ }
18
28
  function readInstallerSetupApiKey(configPath = path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'config.json')) {
19
29
  if (!fs_1.default.existsSync(configPath))
20
30
  return null;
@@ -58,22 +68,22 @@ async function repairInstallerIdeMcpEntries(options = {}) {
58
68
  process.env.FRAIM_NON_INTERACTIVE = previousNonInteractive;
59
69
  }
60
70
  }
61
- function warmInstallerCliCache() {
71
+ async function warmInstallerCliCache() {
62
72
  const cliShim = (0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)();
63
- const options = { env: process.env, encoding: 'utf8', stdio: 'inherit' };
64
- const result = process.platform === 'win32'
65
- ? (0, win32_cmd_spawn_1.spawnViaCmdExe)(cliShim, ['--version'], options)
66
- : (0, child_process_1.spawnSync)(cliShim, ['--version'], options);
73
+ const result = await (0, hub_app_materializer_1.runNpmInstall)(cliShim, ['--version']);
67
74
  if (result.status !== 0) {
68
75
  throw new Error(`FRAIM CLI bootstrap exited with code ${result.status ?? 'unknown'}.`);
69
76
  }
70
77
  }
71
- async function runDesktopInstallerSetup(logger = console) {
78
+ async function runDesktopRuntimeSetup(logger = console) {
72
79
  await (0, managed_node_runtime_1.ensureManagedNpm)({
73
80
  onProgress: (message) => logger.log(`[fraim-installer-setup] ${message}`),
74
81
  });
75
82
  (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
76
- warmInstallerCliCache();
83
+ await warmInstallerCliCache();
84
+ }
85
+ async function runDesktopInstallerSetup(logger = console) {
86
+ await runDesktopRuntimeSetup(logger);
77
87
  await repairInstallerIdeMcpEntries({ logger });
78
88
  logger.log('[fraim-installer-setup] stable CLI and MCP launchers are ready');
79
89
  }
@@ -6,6 +6,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const electron_1 = require("electron");
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const hub_app_materializer_1 = require("./hub-app-materializer");
9
+ const desktop_admission_1 = require("./desktop-admission");
10
+ const hub_main_diagnostics_1 = require("./hub-main-diagnostics");
9
11
  // Issue #1415: this is the packaged Electron `main` entry (see packages/fraim-hub/package.json
10
12
  // `"main"`). It is deliberately thin and changes rarely: its only job is to resolve which version
11
13
  // of the actual Hub application (the code in desktop-main.ts) to run — the latest published to
@@ -20,10 +22,16 @@ async function bootstrapFrom(entryPath) {
20
22
  await entry.bootstrap();
21
23
  }
22
24
  async function launch() {
23
- if (process.argv.includes('--fraim-installer-setup')) {
25
+ if (process.argv.includes('--fraim-installer-setup') || process.argv.includes('--fraim-runtime-repair')) {
24
26
  await bootstrapFrom(BUNDLED_ENTRY_PATH);
25
27
  return;
26
28
  }
29
+ const admissionStarted = performance.now();
30
+ if (!(0, desktop_admission_1.admitDesktopInstance)())
31
+ return;
32
+ (0, hub_main_diagnostics_1.installHubMainConsoleTee)();
33
+ (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.startup_stage', { stage: 'admission', durationMs: performance.now() - admissionStarted });
34
+ const resolutionStarted = performance.now();
27
35
  // Issue #1415: deterministic escape hatch for integration testing — skips the real npm
28
36
  // resolution (and its dependence on whatever happens to be published at test time) so a test can
29
37
  // prove the Electron main-process boot chain itself (resolveHubAppEntry -> require -> bootstrap
@@ -42,8 +50,16 @@ async function launch() {
42
50
  return { entryPath: BUNDLED_ENTRY_PATH, source: 'bundled', version: electron_1.app.getVersion() };
43
51
  });
44
52
  console.log(`[fraim-hub-launcher] running FRAIM Hub ${resolved.version} (${resolved.source})`);
53
+ (0, hub_main_diagnostics_1.recordHubMainDiagnostic)('hub.startup_stage', {
54
+ stage: 'payload-resolution', durationMs: performance.now() - resolutionStarted,
55
+ selectedVersion: resolved.version, source: resolved.source,
56
+ outcome: resolved.materializationError ? 'fallback' : 'success',
57
+ });
58
+ let entry;
45
59
  try {
46
- await bootstrapFrom(resolved.entryPath);
60
+ entry = require(resolved.entryPath);
61
+ if (typeof entry.bootstrap !== 'function')
62
+ throw new Error('Hub payload has no bootstrap entry');
47
63
  }
48
64
  catch (error) {
49
65
  if (resolved.entryPath === BUNDLED_ENTRY_PATH)
@@ -52,8 +68,11 @@ async function launch() {
52
68
  // dependency introduces) must not strand the user on a broken launch — fall back to the
53
69
  // build that shipped with this installer, which is always known-good for this Electron binary.
54
70
  console.error(`[fraim-hub-launcher] materialized FRAIM Hub ${resolved.version} failed to load; falling back to the bundled build:`, error);
55
- await bootstrapFrom(BUNDLED_ENTRY_PATH);
71
+ entry = require(BUNDLED_ENTRY_PATH);
56
72
  }
73
+ // Once bootstrap starts it can own servers/windows. Do not start a second
74
+ // application over those resources if initialization subsequently fails.
75
+ await entry.bootstrap();
57
76
  }
58
77
  if (process.versions.electron && process.type !== 'renderer') {
59
78
  launch().catch((error) => {