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.
- package/dist/src/cli/commands/first-run.js +1 -1
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +16 -0
- package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +31 -130
- package/dist/src/cli/mcp/mcp-server-registry.js +1 -6
- package/dist/src/cli/utils/managed-agent-paths.js +14 -3
- package/dist/src/cli/utils/managed-node-runtime.js +22 -19
- package/dist/src/first-run/session-service.js +17 -11
- package/dist/src/services/installer-service.js +5 -47
- package/package.json +1 -1
|
@@ -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 (.
|
|
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
|
|
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
|
-
|
|
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
|
|
256
|
-
exec
|
|
190
|
+
if command -v ${executable} >/dev/null 2>&1; then
|
|
191
|
+
exec ${executable} ${args}
|
|
257
192
|
fi
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
310
|
-
|
|
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 :
|
|
332
|
-
writeFileIfChanged(fraimNpxShimPath, process.platform === 'win32' ? windowsFraimNpxShimSource :
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
213
|
-
//
|
|
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
|
-
|
|
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 (.
|
|
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
|
-
|
|
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 =
|
|
681
|
+
row.verb = 'Node and FRAIM launchers ready';
|
|
675
682
|
this.persist();
|
|
676
|
-
return this.respond(
|
|
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
|
-
(
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|