fraim 2.0.326 → 2.0.327

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.
@@ -34,7 +34,7 @@ function openBrowser(url) {
34
34
  }
35
35
  const runFirstRun = async (options) => {
36
36
  // Issue #646: the key is optional. When launched by the no-terminal macOS
37
- // installer (.pkg), no key is passed — the wizard prompts the user to paste it.
37
+ // installer (.dmg), no key is passed — the wizard prompts the user to paste it.
38
38
  const key = options.key || process.env.FRAIM_API_KEY || process.env.FRAIM_SETUP_KEY || process.env.FRAIM_INSTALL_KEY || '';
39
39
  const sessionService = new session_service_1.FirstRunSessionService({
40
40
  key,
@@ -150,6 +150,22 @@ function diagnoseFraimMcpLaunchPlan(fraimServer, platform = process.platform, np
150
150
  }
151
151
  };
152
152
  }
153
+ const shimSource = fs_1.default.readFileSync(command, 'utf8');
154
+ if (/ELECTRON_RUN_AS_NODE|app\.asar/i.test(shimSource)) {
155
+ return {
156
+ status: 'error',
157
+ message: 'FRAIM MCP config uses an obsolete Hub/Electron-routed stable shim',
158
+ suggestion: 'Run: fraim doctor --fix-mcp to replace it with the stable node/npx launcher',
159
+ details: {
160
+ launchPlanSource: 'legacy-hub-routed-shim',
161
+ phase: 'preflight',
162
+ command,
163
+ args,
164
+ expectedCommand: fraimMcpShimPath,
165
+ companionNpxShim: fraimNpxShimPath
166
+ }
167
+ };
168
+ }
153
169
  return {
154
170
  status: 'passed',
155
171
  message: 'FRAIM MCP config uses the stable FRAIM shim',
@@ -8,15 +8,12 @@ exports.getFraimMcpShimPath = getFraimMcpShimPath;
8
8
  exports.getFraimNpxShimPath = getFraimNpxShimPath;
9
9
  exports.getFraimCliShimPath = getFraimCliShimPath;
10
10
  exports.getPackagedFraimRuntimePath = getPackagedFraimRuntimePath;
11
- exports.getPackagedFraimRuntime = getPackagedFraimRuntime;
12
- exports.hasPackagedFraimRuntime = hasPackagedFraimRuntime;
13
11
  exports.ensureFraimMcpLatestLauncher = ensureFraimMcpLatestLauncher;
14
12
  const fs_1 = __importDefault(require("fs"));
15
13
  const os_1 = __importDefault(require("os"));
16
14
  const path_1 = __importDefault(require("path"));
17
15
  const windows_shim_path_search_1 = require("../../core/utils/windows-shim-path-search");
18
16
  const LAUNCHER_VERSION = 1;
19
- const PACKAGED_RUNTIME_VERSION = 1;
20
17
  const launcherSource = `#!/usr/bin/env node
21
18
  const fs = require('fs');
22
19
  const os = require('os');
@@ -32,7 +29,9 @@ const CACHE_ROOT = path.join(USER_DIR, 'npm-cache', 'mcp');
32
29
  const commandName = (name) => process.platform === 'win32' ? name + '.cmd' : name;
33
30
  const npmCommand = commandName('npm');
34
31
  const npxCommand = commandName('npx');
35
- const cliArgs = process.argv.slice(2).length > 0 ? process.argv.slice(2) : ['mcp'];
32
+ const rawArgs = process.argv.slice(2);
33
+ const cliMode = rawArgs[0] === '--fraim-cli';
34
+ const cliArgs = cliMode ? rawArgs.slice(1) : (rawArgs.length > 0 ? rawArgs : ['mcp']);
36
35
 
37
36
  function quoteCmdArg(arg) {
38
37
  const value = String(arg);
@@ -133,75 +132,6 @@ function getFraimCliShimPath() {
133
132
  function getPackagedFraimRuntimePath() {
134
133
  return path_1.default.join(process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim'), 'bin', 'packaged-fraim-runtime.json');
135
134
  }
136
- function runtimeFromEnv(env = process.env) {
137
- return env.FRAIM_PACKAGED_CLI_EXECUTABLE && env.FRAIM_PACKAGED_CLI_SCRIPT
138
- ? { executable: env.FRAIM_PACKAGED_CLI_EXECUTABLE, script: env.FRAIM_PACKAGED_CLI_SCRIPT }
139
- : null;
140
- }
141
- // Regular Node.js (and ELECTRON_RUN_AS_NODE) cannot see inside Electron's
142
- // .asar archives. For paths like …/app.asar/node_modules/…, verify the
143
- // archive file itself rather than the internal entry.
144
- function existsOrAsarExists(filePath) {
145
- const normalized = filePath.replace(/\\/g, '/');
146
- const asarSep = normalized.indexOf('.asar/');
147
- if (asarSep === -1)
148
- return fs_1.default.existsSync(filePath);
149
- return fs_1.default.existsSync(filePath.slice(0, asarSep + '.asar'.length));
150
- }
151
- function getPackagedFraimRuntime(env = process.env) {
152
- const envRuntime = runtimeFromEnv(env);
153
- if (envRuntime)
154
- return envRuntime;
155
- try {
156
- const raw = JSON.parse(fs_1.default.readFileSync(getPackagedFraimRuntimePath(), 'utf8'));
157
- if (!raw.executable || !raw.script)
158
- return null;
159
- if (!fs_1.default.existsSync(raw.executable) || !existsOrAsarExists(raw.script))
160
- return null;
161
- return { executable: raw.executable, script: raw.script };
162
- }
163
- catch {
164
- return null;
165
- }
166
- }
167
- function hasPackagedFraimRuntime(env = process.env) {
168
- return getPackagedFraimRuntime(env) !== null;
169
- }
170
- function posixSingleQuote(value) {
171
- return `'${value.replace(/'/g, `'\\''`)}'`;
172
- }
173
- function windowsBatchPath(value) {
174
- return value.replace(/%/g, '%%').replace(/"/g, '""');
175
- }
176
- function writePackagedRuntimeMetadata(runtime) {
177
- writeFileIfChanged(getPackagedFraimRuntimePath(), JSON.stringify({
178
- version: PACKAGED_RUNTIME_VERSION,
179
- executable: runtime.executable,
180
- script: runtime.script,
181
- updatedAt: new Date().toISOString(),
182
- }, null, 2) + '\n');
183
- }
184
- function packagedFraimShimSource(command, runtime) {
185
- const { executable, script } = runtime;
186
- const commandArg = command === 'mcp' ? ' mcp' : '';
187
- if (process.platform === 'win32') {
188
- return [
189
- '@echo off',
190
- 'setlocal',
191
- 'set "ELECTRON_RUN_AS_NODE=1"',
192
- `"${windowsBatchPath(executable)}" "${windowsBatchPath(script)}"${commandArg} %*`,
193
- 'exit /b %ERRORLEVEL%',
194
- '',
195
- ].join('\r\n');
196
- }
197
- return [
198
- '#!/usr/bin/env sh',
199
- 'set -eu',
200
- 'export ELECTRON_RUN_AS_NODE=1',
201
- `exec ${posixSingleQuote(executable)} ${posixSingleQuote(script)}${commandArg} "$@"`,
202
- '',
203
- ].join('\n');
204
- }
205
135
  const windowsFraimMcpShimSource = `@echo off
206
136
  setlocal
207
137
  for %%I in ("%~dp0..") do set "FRAIM_USER_DIR_VALUE=%%~fI"
@@ -225,6 +155,9 @@ for /f "delims=" %%D in ('dir /b /ad /o-n "%FRAIM_USER_DIR_VALUE%\\node\\node-v*
225
155
  echo [fraim-mcp] Could not find node on PATH or in "%FRAIM_USER_DIR_VALUE%\\node" 1>&2
226
156
  exit /b 1
227
157
  `;
158
+ const windowsFraimCliShimSource = windowsFraimMcpShimSource
159
+ .replace(/fraim-mcp-latest\.js" %\*/g, 'fraim-mcp-latest.js" --fraim-cli %*')
160
+ .replace(/\[fraim-mcp\]/g, '[fraim]');
228
161
  const windowsFraimNpxShimSource = `@echo off
229
162
  setlocal
230
163
  for %%I in ("%~dp0..") do set "FRAIM_USER_DIR_VALUE=%%~fI"
@@ -246,47 +179,31 @@ for /f "delims=" %%D in ('dir /b /ad /o-n "%FRAIM_USER_DIR_VALUE%\\node\\node-v*
246
179
  echo [fraim-npx] Could not find npx on PATH or in "%FRAIM_USER_DIR_VALUE%\\node" 1>&2
247
180
  exit /b 1
248
181
  `;
249
- const posixFraimMcpShimSource = `#!/usr/bin/env sh
182
+ function posixFraimShimSource(executable) {
183
+ const args = executable === 'node' ? '"$FRAIM_USER_DIR_VALUE/bin/fraim-mcp-latest.js" "$@"' : '"$@"';
184
+ const label = executable === 'node' ? 'fraim-mcp' : 'fraim-npx';
185
+ return `#!/usr/bin/env sh
250
186
  set -eu
251
187
  SHIM_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
252
188
  FRAIM_USER_DIR_VALUE=$(dirname "$SHIM_DIR")
253
- LAUNCHER="$FRAIM_USER_DIR_VALUE/bin/fraim-mcp-latest.js"
254
189
 
255
- if command -v node >/dev/null 2>&1; then
256
- exec node "$LAUNCHER" "$@"
190
+ if command -v ${executable} >/dev/null 2>&1; then
191
+ exec ${executable} ${args}
257
192
  fi
258
- if [ -x "$FRAIM_USER_DIR_VALUE/node/bin/node" ]; then
259
- PATH="$FRAIM_USER_DIR_VALUE/node/bin:$PATH"
260
- export PATH
261
- exec "$FRAIM_USER_DIR_VALUE/node/bin/node" "$LAUNCHER" "$@"
262
- fi
263
- if [ -x "$FRAIM_USER_DIR_VALUE/node/node" ]; then
264
- PATH="$FRAIM_USER_DIR_VALUE/node:$PATH"
265
- export PATH
266
- exec "$FRAIM_USER_DIR_VALUE/node/node" "$LAUNCHER" "$@"
267
- fi
268
-
269
- echo "[fraim-mcp] Could not find node on PATH or in $FRAIM_USER_DIR_VALUE/node" >&2
270
- exit 1
271
- `;
272
- const posixFraimNpxShimSource = `#!/usr/bin/env sh
273
- set -eu
274
- SHIM_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
275
- FRAIM_USER_DIR_VALUE=$(dirname "$SHIM_DIR")
276
-
277
- if command -v npx >/dev/null 2>&1; then
278
- exec npx "$@"
279
- fi
280
- if [ -x "$FRAIM_USER_DIR_VALUE/node/bin/npx" ]; then
281
- exec "$FRAIM_USER_DIR_VALUE/node/bin/npx" "$@"
282
- fi
283
- if [ -x "$FRAIM_USER_DIR_VALUE/node/npx" ]; then
284
- exec "$FRAIM_USER_DIR_VALUE/node/npx" "$@"
285
- fi
286
-
287
- echo "[fraim-npx] Could not find npx on PATH or in $FRAIM_USER_DIR_VALUE/node" >&2
193
+ # Match the generating host's distribution layout; staging and other architectures
194
+ # cannot match this suffix. Legacy installs remain valid fallback locations.
195
+ for NODE_BIN in "$FRAIM_USER_DIR_VALUE/node"/node-v*-${process.platform}-${process.arch}/bin "$FRAIM_USER_DIR_VALUE/node/bin" "$FRAIM_USER_DIR_VALUE/node"; do
196
+ if [ -x "$NODE_BIN/node" ] && [ -x "$NODE_BIN/${executable}" ]; then
197
+ PATH="$NODE_BIN:$PATH"
198
+ export PATH
199
+ exec "$NODE_BIN/${executable}" ${args}
200
+ fi
201
+ done
202
+
203
+ echo "[${label}] Could not find ${executable} on PATH or in $FRAIM_USER_DIR_VALUE/node" >&2
288
204
  exit 1
289
205
  `;
206
+ }
290
207
  const writeFileIfChanged = (filePath, content, mode) => {
291
208
  if (!fs_1.default.existsSync(filePath) || fs_1.default.readFileSync(filePath, 'utf8') !== content) {
292
209
  fs_1.default.writeFileSync(filePath, content, 'utf8');
@@ -306,30 +223,14 @@ function ensureFraimMcpLatestLauncher() {
306
223
  fs_1.default.mkdirSync(launcherDir, { recursive: true });
307
224
  const fraimMcpShimPath = getFraimMcpShimPath();
308
225
  const fraimNpxShimPath = getFraimNpxShimPath();
309
- const packagedRuntime = getPackagedFraimRuntime();
310
- if (packagedRuntime) {
311
- const fraimCliShimPath = getFraimCliShimPath();
312
- if (runtimeFromEnv()) {
313
- writePackagedRuntimeMetadata(packagedRuntime);
314
- }
315
- writeFileIfChanged(fraimMcpShimPath, packagedFraimShimSource('mcp', packagedRuntime), process.platform === 'win32' ? undefined : 0o755);
316
- writeFileIfChanged(fraimCliShimPath, packagedFraimShimSource('cli', packagedRuntime), process.platform === 'win32' ? undefined : 0o755);
317
- // Issue #1474: keep the npx companion shim in parity with the non-packaged branch below.
318
- // The packaged fraim-mcp shim itself never invokes npx, but git/playwright base MCP servers
319
- // (added later via the connect-mcp skill once a system npm is available) and doctor's
320
- // launch-plan diagnostic both expect this file to exist alongside the stable fraim shim.
321
- // Without it, doctor's "stable shim missing on disk" check could never be satisfied on a
322
- // packaged install, and --fix-mcp/add-ide (which call this same function) could never fix it.
323
- writeFileIfChanged(fraimNpxShimPath, process.platform === 'win32' ? windowsFraimNpxShimSource : posixFraimNpxShimSource, process.platform === 'win32' ? undefined : 0o755);
324
- return {
325
- command: fraimMcpShimPath,
326
- args: [],
327
- path: packagedRuntime.script,
328
- };
329
- }
226
+ const fraimCliShimPath = getFraimCliShimPath();
227
+ fs_1.default.rmSync(getPackagedFraimRuntimePath(), { force: true });
330
228
  writeFileIfChanged(launcherPath, launcherSource, process.platform === 'win32' ? undefined : 0o755);
331
- writeFileIfChanged(fraimMcpShimPath, process.platform === 'win32' ? windowsFraimMcpShimSource : posixFraimMcpShimSource, process.platform === 'win32' ? undefined : 0o755);
332
- writeFileIfChanged(fraimNpxShimPath, process.platform === 'win32' ? windowsFraimNpxShimSource : posixFraimNpxShimSource, process.platform === 'win32' ? undefined : 0o755);
229
+ writeFileIfChanged(fraimMcpShimPath, process.platform === 'win32' ? windowsFraimMcpShimSource : posixFraimShimSource('node'), process.platform === 'win32' ? undefined : 0o755);
230
+ writeFileIfChanged(fraimNpxShimPath, process.platform === 'win32' ? windowsFraimNpxShimSource : posixFraimShimSource('npx'), process.platform === 'win32' ? undefined : 0o755);
231
+ writeFileIfChanged(fraimCliShimPath, process.platform === 'win32' ? windowsFraimCliShimSource : posixFraimShimSource('node')
232
+ .replace(/fraim-mcp-latest\.js" "\$@"/g, 'fraim-mcp-latest.js" --fraim-cli "$@"')
233
+ .replace(/\[fraim-mcp\]/g, '[fraim]'), process.platform === 'win32' ? undefined : 0o755);
333
234
  return {
334
235
  command: fraimMcpShimPath,
335
236
  args: [],
@@ -56,12 +56,7 @@ exports.BASE_MCP_SERVERS = [
56
56
  }
57
57
  ];
58
58
  function activeBaseMCPServers() {
59
- // The packaged desktop owns FRAIM's runtime but intentionally does not bundle
60
- // npm. Keep third-party npx MCP servers out of fresh desktop-generated config;
61
- // users can add them later through connect-mcp when a system npm is available.
62
- return (0, fraim_mcp_latest_launcher_1.hasPackagedFraimRuntime)()
63
- ? exports.BASE_MCP_SERVERS.filter((server) => server.id === 'fraim')
64
- : exports.BASE_MCP_SERVERS;
59
+ return exports.BASE_MCP_SERVERS;
65
60
  }
66
61
  // ============================================================================
67
62
  // PROVIDER MCP SERVER BUILDER (uses provider registry)
@@ -36,12 +36,12 @@ Object.defineProperty(exports, "resolveMachinePathCached", { enumerable: true, g
36
36
  function getManagedNodeRoot() {
37
37
  return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
38
38
  }
39
- function getPortableNodeBinPath() {
39
+ function getPortableNodeBinPath(platform = process.platform, arch = process.arch) {
40
40
  const nodeRoot = getManagedNodeRoot();
41
- if (process.platform === 'win32') {
41
+ if (platform === 'win32') {
42
42
  if (fs_1.default.existsSync(nodeRoot)) {
43
43
  const extractedDir = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
44
- .filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v'))
44
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith('node-v') && !entry.name.includes('.staging-'))
45
45
  .sort((a, b) => b.name.localeCompare(a.name))[0];
46
46
  if (extractedDir) {
47
47
  return path_1.default.join(nodeRoot, extractedDir.name);
@@ -49,6 +49,17 @@ function getPortableNodeBinPath() {
49
49
  }
50
50
  return nodeRoot;
51
51
  }
52
+ if (fs_1.default.existsSync(nodeRoot)) {
53
+ const versioned = fs_1.default.readdirSync(nodeRoot, { withFileTypes: true })
54
+ .filter(entry => entry.isDirectory() && /^node-v\d+\.\d+\.\d+-/.test(entry.name)
55
+ && entry.name.endsWith(`-${platform}-${arch}`))
56
+ .sort((a, b) => b.name.localeCompare(a.name, undefined, { numeric: true }));
57
+ for (const entry of versioned) {
58
+ const bin = path_1.default.join(nodeRoot, entry.name, 'bin');
59
+ if (fs_1.default.existsSync(path_1.default.join(bin, 'node')) && fs_1.default.existsSync(path_1.default.join(bin, 'npm')))
60
+ return bin;
61
+ }
62
+ }
52
63
  return path_1.default.join(nodeRoot, 'bin');
53
64
  }
54
65
  function getManagedAgentBinDirs() {
@@ -17,16 +17,11 @@ const path_1 = __importDefault(require("path"));
17
17
  const https_1 = __importDefault(require("https"));
18
18
  const crypto_1 = __importDefault(require("crypto"));
19
19
  const managed_agent_paths_1 = require("./managed-agent-paths");
20
- // Issue #1415 (extended): a genuinely GUI-only FRAIM Hub install — the native installer
21
- // downloaded from the website, never a CLI — has no working `npm`/`npx` anywhere. First-run's
22
- // `embeddedDesktop` branch (session-service.ts) deliberately skips installing one (it only needs
23
- // Electron's own bundled Node for FRAIM's own CLI/MCP shim, via ELECTRON_RUN_AS_NODE), and the
24
- // separate CLI installer's portable-Node bootstrap (scripts/installer/fraim-install-win.template.cmd)
25
- // never runs for a desktop-only user. That left two features silently unable to do their real job
26
- // on such a machine: `hub-app-materializer.ts`'s npm install (this issue), and agent-CLI
27
- // installation (`managed-agent-install.ts`), which also shells out to a bare `npm`.
20
+ // Desktop-only users may have no working system npm/npx. Installer setup, Hub
21
+ // materialization, first-run setup, and agent installation share this managed
22
+ // runtime resolver so each can prepare or reuse standalone Node and npm.
28
23
  //
29
- // This module is the one place that guarantees a real, usable `npm`/`node` exists, for both
24
+ // This module is the one place that guarantees a real, usable `npm`/`node` exists for its
30
25
  // consumers: prefer whatever is already on system PATH (common for technical users), then an
31
26
  // already-downloaded managed runtime (shared with the CLI installer's `~/.fraim/node/` convention
32
27
  // via `managed-agent-paths.ts` — reused, not duplicated, so a user who has ever run the `fraim` CLI
@@ -65,11 +60,14 @@ function findExecutableOnPath(name, pathValue = process.env.PATH) {
65
60
  function hasWorkingNodeAndNpm(dir, platform = process.platform) {
66
61
  return fs_1.default.existsSync(path_1.default.join(dir, npmExecutableName(platform))) && fs_1.default.existsSync(path_1.default.join(dir, nodeExecutableName(platform)));
67
62
  }
68
- /** Whether `dir` (a specific versioned bin dir) holds a complete extraction for exactly `version`. */
63
+ function nodeBinDir(root, platform) {
64
+ return platform === 'win32' ? root : path_1.default.join(root, 'bin');
65
+ }
66
+ /** Whether `dir` (a versioned distribution root) holds a complete extraction for `version`. */
69
67
  function isManagedNodeVersionComplete(dir, version, platform = process.platform) {
70
68
  try {
71
69
  const recorded = fs_1.default.readFileSync(path_1.default.join(dir, 'fraim-managed-node-version'), 'utf8').trim();
72
- return recorded === version && hasWorkingNodeAndNpm(dir, platform);
70
+ return recorded === version && hasWorkingNodeAndNpm(nodeBinDir(dir, platform), platform);
73
71
  }
74
72
  catch {
75
73
  return false;
@@ -99,6 +97,9 @@ function assertPathSafeNodeVersion(version) {
99
97
  }
100
98
  /** The exact folder name Node's own official archive extracts as — also this module's versioned subfolder name under the shared `~/.fraim/node/` root. */
101
99
  function nodeVersionedDirName(request) {
100
+ if (!/^[a-z0-9_]+$/.test(request.arch)) {
101
+ throw new Error('Refusing to use an invalid managed Node.js architecture.');
102
+ }
102
103
  return `node-v${assertPathSafeNodeVersion(request.version)}-${nodePlatformName(request.platform)}-${request.arch}`;
103
104
  }
104
105
  function nodeArchiveFileName(request) {
@@ -203,15 +204,16 @@ const defaultExtractNodeArchive = async (archivePath, destDir, request) => {
203
204
  async function downloadAndPromoteVersionedNodeDir(args) {
204
205
  const { nodeRoot, request, downloadArchive, extractArchive } = args;
205
206
  const destDir = path_1.default.join(nodeRoot, nodeVersionedDirName(request));
206
- const stagingDir = `${destDir}.staging-${process.pid}`;
207
207
  fs_1.default.mkdirSync(nodeRoot, { recursive: true });
208
- fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
209
208
  const archivePath = await downloadArchive(request);
209
+ let stagingDir;
210
210
  try {
211
+ // tar requires an existing cwd. Each invocation owns a separate directory,
212
+ // including simultaneous requests within the same process.
213
+ stagingDir = fs_1.default.mkdtempSync(`${destDir}.staging-${process.pid}-`);
211
214
  await extractArchive(archivePath, stagingDir, request);
212
- // The archive's own top-level folder (matching nodeVersionedDirName) may land either as the
213
- // extraction root itself (tar) or as one nested level inside it (some zip extractors preserve
214
- // the archive's internal folder) — handle both without assuming either.
215
+ // Official tar and ZIP archives preserve their top-level distribution folder.
216
+ // Also accept a flattened root from a supplied extractor.
215
217
  const nestedCandidate = path_1.default.join(stagingDir, nodeVersionedDirName(request));
216
218
  const extractedRoot = fs_1.default.existsSync(nestedCandidate) ? nestedCandidate : stagingDir;
217
219
  fs_1.default.writeFileSync(path_1.default.join(extractedRoot, 'fraim-managed-node-version'), request.version, 'utf8');
@@ -233,9 +235,10 @@ async function downloadAndPromoteVersionedNodeDir(args) {
233
235
  }
234
236
  finally {
235
237
  fs_1.default.rmSync(archivePath, { force: true });
236
- fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
238
+ if (stagingDir)
239
+ fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
237
240
  }
238
- return destDir;
241
+ return nodeBinDir(destDir, request.platform);
239
242
  }
240
243
  /**
241
244
  * Resolve a working `npm` executable, preferring (in order): system PATH, an already-downloaded
@@ -252,7 +255,7 @@ async function ensureManagedNpm(options = {}) {
252
255
  if (systemNpm) {
253
256
  return { npmPath: systemNpm, binDir: path_1.default.dirname(systemNpm), source: 'system' };
254
257
  }
255
- const existingBinDir = (0, managed_agent_paths_1.getPortableNodeBinPath)();
258
+ const existingBinDir = (0, managed_agent_paths_1.getPortableNodeBinPath)(platform, arch);
256
259
  if (hasWorkingNodeAndNpm(existingBinDir, platform)) {
257
260
  return { npmPath: path_1.default.join(existingBinDir, npmExecutableName(platform)), binDir: existingBinDir, source: 'managed-existing' };
258
261
  }
@@ -323,7 +323,7 @@ function buildFirstRunHubLaunchArgs() {
323
323
  class FirstRunSessionService {
324
324
  constructor(options) {
325
325
  // Issue #646: the key may be absent when first-run is launched by the macOS
326
- // installer (.pkg) instead of `fraim first-run --key=…`. In that case the
326
+ // installer (.dmg) instead of `fraim first-run --key=…`. In that case the
327
327
  // wizard prompts the user to paste their key (see setKey / needsKey).
328
328
  this.key = options.key || '';
329
329
  this.headless = options.headless === true;
@@ -393,13 +393,15 @@ class FirstRunSessionService {
393
393
  // Real detection — populate row statuses from `command -v` style probes.
394
394
  const nodeRow = this.getRow('node');
395
395
  const nodeVer = this.embeddedDesktop ? process.version : commandVersion('node');
396
- if (nodeVer) {
396
+ const embeddedLaunchersReady = !this.embeddedDesktop
397
+ || (fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) && fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)()));
398
+ if (nodeVer && embeddedLaunchersReady) {
397
399
  nodeRow.status = 'ok';
398
400
  nodeRow.verb = this.embeddedDesktop ? `${nodeVer} bundled with FRAIM` : `${nodeVer} detected`;
399
401
  }
400
402
  else {
401
403
  nodeRow.status = 'pending';
402
- nodeRow.verb = "we'll install";
404
+ nodeRow.verb = this.embeddedDesktop ? 'installer repair required' : "we'll install";
403
405
  }
404
406
  const gitRow = this.getRow('git');
405
407
  const gitVer = commandVersion('git');
@@ -669,11 +671,16 @@ class FirstRunSessionService {
669
671
  async runNodeRow() {
670
672
  const row = this.getRow('node');
671
673
  if (this.embeddedDesktop) {
674
+ if (!this.fakeMode && (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) || !fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)()))) {
675
+ this.setRowError(row, 'Checking the native installer setup', 'Stable FRAIM CLI/MCP launchers are missing. Re-run the native installer in repair mode.', [{ id: 'retry', label: 'Check again', variant: 'primary' }]);
676
+ this.persist();
677
+ return this.respond('FRAIM installer repair is required.', false);
678
+ }
672
679
  persistShellPath(this.embeddedDesktop);
673
680
  row.status = 'ok';
674
- row.verb = `${process.version} bundled with FRAIM`;
681
+ row.verb = 'Node and FRAIM launchers ready';
675
682
  this.persist();
676
- return this.respond(`FRAIM includes its own runtime (${process.version}).`, true);
683
+ return this.respond('FRAIM command-line tools are ready.', true);
677
684
  }
678
685
  // Node is bootstrapped by the install template before the wizard starts.
679
686
  // If node is missing here something is severely wrong; fall through to error.
@@ -737,10 +744,10 @@ class FirstRunSessionService {
737
744
  return this.respond('FRAIM configured.', true);
738
745
  }
739
746
  try {
740
- if (this.embeddedDesktop) {
741
- (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
747
+ if (this.embeddedDesktop && (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) || !fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)()))) {
748
+ throw new Error('Stable FRAIM launchers are missing. Re-run the native installer in repair mode.');
742
749
  }
743
- else if (!forceConfigure && !commandVersion('fraim')) {
750
+ else if (!this.embeddedDesktop && !forceConfigure && !commandVersion('fraim')) {
744
751
  const prefix = path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'node');
745
752
  fs_1.default.mkdirSync(prefix, { recursive: true });
746
753
  row.streamOutput = 'Installing FRAIM on this machine...';
@@ -1051,12 +1058,11 @@ class FirstRunSessionService {
1051
1058
  };
1052
1059
  }
1053
1060
  if (this.embeddedDesktop) {
1054
- (0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
1055
- if (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)())) {
1061
+ if (!fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimCliShimPath)()) || !fs_1.default.existsSync((0, fraim_mcp_latest_launcher_1.getFraimMcpShimPath)())) {
1056
1062
  return {
1057
1063
  ok: false,
1058
1064
  needsAgentSetup: true,
1059
- message: 'FRAIM CLI was not installed into the packaged desktop profile. Re-run FRAIM setup before opening Hub.',
1065
+ message: 'FRAIM CLI/MCP launchers are missing. Re-run the native installer in repair mode before opening Hub.',
1060
1066
  };
1061
1067
  }
1062
1068
  appendInstallLog('hub-opened embedded-desktop');
@@ -1,26 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * InstallerService - Generates installer scripts and dashboard HTML for the Get Started flow.
4
- * Scripts are loaded from templates in scripts/installer/ and populated with token/baseUrl.
3
+ * InstallerService - Generates dashboard HTML for the Get Started flow.
5
4
  */
6
5
  Object.defineProperty(exports, "__esModule", { value: true });
7
6
  exports.InstallerService = void 0;
8
- const fs_1 = require("fs");
9
- const path_1 = require("path");
10
- function getScriptsDir() {
11
- // When running from dist: dist/services/ -> dist/../scripts = scripts
12
- // When running from src with tsx: src/services/ -> src/../scripts = scripts
13
- const candidates = [
14
- (0, path_1.join)(process.cwd(), 'scripts', 'installer'),
15
- (0, path_1.join)(__dirname, '..', '..', 'scripts', 'installer'),
16
- (0, path_1.join)(__dirname, '..', '..', '..', 'scripts', 'installer')
17
- ];
18
- for (const dir of candidates) {
19
- if ((0, fs_1.existsSync)(dir))
20
- return dir;
21
- }
22
- return null;
23
- }
24
7
  /**
25
8
  * Default marketing site base URL. All asset references and nav links in the
26
9
  * layout shell resolve against this host. Overridable via FRAIM_MARKETING_URL.
@@ -55,15 +38,6 @@ function buildLayoutShell(marketingBaseUrl) {
55
38
  <script src="${m}/js/script.js"></script></body></html>`;
56
39
  }
57
40
  class InstallerService {
58
- constructor() {
59
- this.scriptsDir = getScriptsDir();
60
- }
61
- requireScriptsDir() {
62
- if (!this.scriptsDir) {
63
- throw new Error('Installer scripts directory not found. Installer download is unavailable.');
64
- }
65
- return this.scriptsDir;
66
- }
67
41
  /**
68
42
  * Build the HTML layout shell (nav, styles, theme) wrapping body content.
69
43
  *
@@ -105,27 +79,11 @@ class InstallerService {
105
79
  <h1 style="color:var(--error-color);margin-bottom:12px">${title}</h1><p style="color:var(--text-secondary)">${message}</p>
106
80
  <p style="margin-top:16px"><a href="${linkHref}" class="btn-primary">${linkText}</a></p></div></div>`;
107
81
  }
108
- /**
109
- * Generate the Mac installer script (.command) with the API key embedded.
110
- */
111
- generateMacScript(key, _baseUrl) {
112
- const fraimCmd = `npx -y fraim@latest first-run --key=${key}`;
113
- const templatePath = (0, path_1.join)(this.requireScriptsDir(), 'fraim-install-mac.template.sh');
114
- const template = (0, fs_1.readFileSync)(templatePath, 'utf-8');
115
- return template
116
- .replace('{{FRAIM_CMD}}', fraimCmd)
117
- .split('{{KEY}}').join(key);
82
+ generateMacScript(_key, _baseUrl) {
83
+ throw new Error('Shell installer fallbacks are retired; download FRAIM-Setup.dmg.');
118
84
  }
119
- /**
120
- * Generate the Windows installer script (.cmd) with the API key embedded.
121
- */
122
- generateWindowsScript(key, _baseUrl) {
123
- const fraimCmd = `npx -y fraim@latest first-run --key=${key}`;
124
- const templatePath = (0, path_1.join)(this.requireScriptsDir(), 'fraim-install-win.template.cmd');
125
- const template = (0, fs_1.readFileSync)(templatePath, 'utf-8');
126
- return template
127
- .replace('{{FRAIM_CMD}}', fraimCmd)
128
- .split('{{KEY}}').join(key);
85
+ generateWindowsScript(_key, _baseUrl) {
86
+ throw new Error('Command installer fallbacks are retired; download FRAIM-Setup.exe.');
129
87
  }
130
88
  }
131
89
  exports.InstallerService = InstallerService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.326",
3
+ "version": "2.0.327",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {