fraim 2.0.325 → 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/core/fraim-config-schema.generated.js +5 -1
- package/dist/src/first-run/session-service.js +17 -11
- package/dist/src/local-mcp-server/artifact-retention-cleanup.js +106 -6
- 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
|
}
|
|
@@ -627,6 +627,9 @@ exports.FRAIM_CONFIG_SCHEMA = {
|
|
|
627
627
|
},
|
|
628
628
|
"learning_usage": {
|
|
629
629
|
"kind": "number"
|
|
630
|
+
},
|
|
631
|
+
"hub_event_logs": {
|
|
632
|
+
"kind": "number"
|
|
630
633
|
}
|
|
631
634
|
}
|
|
632
635
|
}
|
|
@@ -768,5 +771,6 @@ exports.SUPPORTED_FRAIM_CONFIG_PATHS = [
|
|
|
768
771
|
"artifact_retention_period.evidence",
|
|
769
772
|
"artifact_retention_period.feedback",
|
|
770
773
|
"artifact_retention_period.cleanup_manifests",
|
|
771
|
-
"artifact_retention_period.learning_usage"
|
|
774
|
+
"artifact_retention_period.learning_usage",
|
|
775
|
+
"artifact_retention_period.hub_event_logs"
|
|
772
776
|
];
|
|
@@ -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');
|
|
@@ -8,6 +8,7 @@ exports.resolveArtifactRetentionConfig = resolveArtifactRetentionConfig;
|
|
|
8
8
|
exports.runArtifactCleanup = runArtifactCleanup;
|
|
9
9
|
exports.planArtifactCleanup = planArtifactCleanup;
|
|
10
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
11
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
11
12
|
const node_child_process_1 = require("node:child_process");
|
|
12
13
|
const node_path_1 = __importDefault(require("node:path"));
|
|
13
14
|
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
@@ -25,6 +26,7 @@ exports.ARTIFACT_RETENTION_CATEGORIES = [
|
|
|
25
26
|
// deleted. Aggregate counters are never pruned; deleting them would misreport
|
|
26
27
|
// history rather than minimise it.
|
|
27
28
|
'learning_usage',
|
|
29
|
+
'hub_event_logs',
|
|
28
30
|
];
|
|
29
31
|
function readWorkspaceConfig(workspaceRoot) {
|
|
30
32
|
try {
|
|
@@ -69,7 +71,7 @@ function resolveArtifactRetentionConfig(workspaceRoot) {
|
|
|
69
71
|
for (const category of exports.ARTIFACT_RETENTION_CATEGORIES) {
|
|
70
72
|
values[category] = category in rawRetention
|
|
71
73
|
? parseRetentionValue(category, rawRetention[category], defaultValue, invalidValues)
|
|
72
|
-
: defaultValue;
|
|
74
|
+
: category === 'hub_event_logs' && !('default' in rawRetention) ? 7 : defaultValue;
|
|
73
75
|
}
|
|
74
76
|
return { values, defaultValue, invalidValues };
|
|
75
77
|
}
|
|
@@ -172,7 +174,8 @@ function evidenceEligibilityDate(workspaceRoot, absolutePath, gitDates) {
|
|
|
172
174
|
const relativePath = normalizeRelative(workspaceRoot, absolutePath);
|
|
173
175
|
return gitDates.get(relativePath) ?? fileMtime(absolutePath);
|
|
174
176
|
}
|
|
175
|
-
function collectCandidates(workspaceRoot,
|
|
177
|
+
function collectCandidates(workspaceRoot, options) {
|
|
178
|
+
const { activeUserEmail } = options;
|
|
176
179
|
const candidates = [];
|
|
177
180
|
const add = (category, absolutePath, eligible, reason, eligibilityDate) => {
|
|
178
181
|
candidates.push({
|
|
@@ -224,8 +227,82 @@ function collectCandidates(workspaceRoot, activeUserEmail) {
|
|
|
224
227
|
}
|
|
225
228
|
add('evidence', filePath, true, 'Evidence file.', eligibilityDate);
|
|
226
229
|
}
|
|
230
|
+
// A workspace policy owns only its project bucket. Manager/company archives
|
|
231
|
+
// and other projects must not inherit whichever project is being cleaned.
|
|
232
|
+
const logRoot = node_path_1.default.resolve(options.hubEventLogRoot ?? node_path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), 'ai-hub-event-logs'));
|
|
233
|
+
// Project archive keys use the resolved path; keep core cleanup independent
|
|
234
|
+
// of the separately shipped Hub package that also uses this convention.
|
|
235
|
+
const bucket = node_crypto_1.default.createHash('sha256').update(node_path_1.default.resolve(workspaceRoot)).digest('hex').slice(0, 24);
|
|
236
|
+
const bucketDir = node_path_1.default.join(logRoot, bucket);
|
|
237
|
+
try {
|
|
238
|
+
if (!node_fs_1.default.lstatSync(bucketDir).isDirectory())
|
|
239
|
+
return candidates;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return candidates;
|
|
243
|
+
}
|
|
244
|
+
for (const filePath of listFiles(bucketDir)) {
|
|
245
|
+
if (!filePath.endsWith('.jsonl'))
|
|
246
|
+
continue;
|
|
247
|
+
const state = readHubArchiveState(logRoot, filePath);
|
|
248
|
+
if (!state)
|
|
249
|
+
continue;
|
|
250
|
+
add('hub_event_logs', filePath, state.eligible, state.reason, state.eligibilityDate);
|
|
251
|
+
Object.assign(candidates[candidates.length - 1], state);
|
|
252
|
+
}
|
|
227
253
|
return candidates;
|
|
228
254
|
}
|
|
255
|
+
function readHubArchiveState(root, filePath) {
|
|
256
|
+
if (!isConfinedRegularFile(root, filePath))
|
|
257
|
+
return null;
|
|
258
|
+
try {
|
|
259
|
+
const metaPath = `${filePath}.meta.json`;
|
|
260
|
+
const hasMeta = isConfinedRegularFile(root, metaPath);
|
|
261
|
+
let meta = {};
|
|
262
|
+
if (hasMeta) {
|
|
263
|
+
try {
|
|
264
|
+
meta = JSON.parse(node_fs_1.default.readFileSync(metaPath, 'utf8')) ?? {};
|
|
265
|
+
}
|
|
266
|
+
catch { /* Crashed or legacy archive: use mtime. */ }
|
|
267
|
+
}
|
|
268
|
+
const endedAt = typeof meta.endedAt === 'string' ? parseDate(meta.endedAt) : undefined;
|
|
269
|
+
const active = !endedAt && isLiveProcess(meta.ownerPid);
|
|
270
|
+
const stat = node_fs_1.default.statSync(filePath);
|
|
271
|
+
return {
|
|
272
|
+
eligible: !active,
|
|
273
|
+
reason: active ? 'Archive writer is still running.' : endedAt ? 'Run has ended.' : 'Archive has no terminal stamp; using last write.',
|
|
274
|
+
eligibilityDate: endedAt ?? stat.mtime,
|
|
275
|
+
sidecarPath: hasMeta ? metaPath : undefined,
|
|
276
|
+
hubArchive: { root, realPath: node_fs_1.default.realpathSync(filePath), mtimeMs: stat.mtimeMs, size: stat.size },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function isConfinedRegularFile(root, filePath) {
|
|
284
|
+
try {
|
|
285
|
+
if (!node_fs_1.default.lstatSync(filePath).isFile())
|
|
286
|
+
return false;
|
|
287
|
+
const relative = node_path_1.default.relative(node_fs_1.default.realpathSync(root), node_fs_1.default.realpathSync(filePath));
|
|
288
|
+
return relative !== '' && !relative.startsWith(`..${node_path_1.default.sep}`) && relative !== '..' && !node_path_1.default.isAbsolute(relative);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function isLiveProcess(pid) {
|
|
295
|
+
if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0)
|
|
296
|
+
return false;
|
|
297
|
+
try {
|
|
298
|
+
process.kill(pid, 0);
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
// Access denied still means a process exists. PID reuse errs toward retention.
|
|
303
|
+
return error.code !== 'ESRCH';
|
|
304
|
+
}
|
|
305
|
+
}
|
|
229
306
|
function daysBetween(from, to) {
|
|
230
307
|
return Math.floor((to.getTime() - from.getTime()) / 86_400_000);
|
|
231
308
|
}
|
|
@@ -254,7 +331,7 @@ function incrementCount(report, action) {
|
|
|
254
331
|
function runArtifactCleanup(workspaceRoot, options) {
|
|
255
332
|
const now = options.now ?? new Date();
|
|
256
333
|
const retention = resolveArtifactRetentionConfig(workspaceRoot);
|
|
257
|
-
const candidates = collectCandidates(workspaceRoot, options
|
|
334
|
+
const candidates = collectCandidates(workspaceRoot, options);
|
|
258
335
|
const report = {
|
|
259
336
|
generatedAt: now.toISOString(),
|
|
260
337
|
apply: options.apply,
|
|
@@ -265,7 +342,7 @@ function runArtifactCleanup(workspaceRoot, options) {
|
|
|
265
342
|
};
|
|
266
343
|
for (const candidate of candidates) {
|
|
267
344
|
const retentionValue = retention.values[candidate.category];
|
|
268
|
-
|
|
345
|
+
let decision = shouldDelete(candidate, retentionValue, now);
|
|
269
346
|
let action;
|
|
270
347
|
let error;
|
|
271
348
|
if (!candidate.eligible) {
|
|
@@ -279,8 +356,31 @@ function runArtifactCleanup(workspaceRoot, options) {
|
|
|
279
356
|
}
|
|
280
357
|
else {
|
|
281
358
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
359
|
+
if (candidate.category === 'hub_event_logs' && candidate.hubArchive) {
|
|
360
|
+
const prior = candidate.hubArchive;
|
|
361
|
+
const current = readHubArchiveState(prior.root, candidate.absolutePath);
|
|
362
|
+
// A terminal run can resume while other candidates are being collected.
|
|
363
|
+
// Revalidate immediately before unlink; a changed archive waits for the
|
|
364
|
+
// next cleanup, even when its new retention setting would be immediate.
|
|
365
|
+
if (!current || !current.eligible || current.hubArchive?.realPath !== prior.realPath
|
|
366
|
+
|| current.hubArchive?.mtimeMs !== prior.mtimeMs || current.hubArchive?.size !== prior.size
|
|
367
|
+
|| current.eligibilityDate?.getTime() !== candidate.eligibilityDate?.getTime()
|
|
368
|
+
|| current.sidecarPath !== candidate.sidecarPath) {
|
|
369
|
+
decision = { delete: false, reason: 'Archive changed or resumed after enumeration; retained for a later cleanup.' };
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
decision = shouldDelete({ ...candidate, ...current }, retentionValue, now);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!decision.delete) {
|
|
376
|
+
action = 'retained';
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
node_fs_1.default.unlinkSync(candidate.absolutePath);
|
|
380
|
+
if (candidate.sidecarPath)
|
|
381
|
+
node_fs_1.default.unlinkSync(candidate.sidecarPath);
|
|
382
|
+
action = 'deleted';
|
|
383
|
+
}
|
|
284
384
|
}
|
|
285
385
|
catch (err) {
|
|
286
386
|
action = 'failed';
|
|
@@ -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;
|