fraim-hub 2.0.288 → 2.0.290
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/ai-hub/desktop-launcher.js +66 -0
- package/dist/src/ai-hub/desktop-main.js +48 -7
- package/dist/src/ai-hub/hub-app-materializer.js +285 -0
- package/dist/src/ai-hub/server.js +4 -8
- package/dist/src/cli/utils/managed-node-runtime.js +269 -0
- package/dist/src/config/ai-manager-hiring.js +6 -2
- package/dist/src/config/persona-capability-bundles.js +1 -1
- package/dist/src/config/persona-hiring.js +26 -26
- package/dist/src/first-run/session-service.js +12 -2
- package/package.json +5 -3
- package/public/ai-hub/script.js +46 -10
- package/public/first-run/script.js +10 -1
- package/public/portfolio/aida.html +13 -13
- package/public/portfolio/ashley.html +2 -2
- package/public/portfolio/auditya.html +2 -2
- package/public/portfolio/banke.html +2 -2
- package/public/portfolio/beza.html +2 -2
- package/public/portfolio/careena.html +2 -2
- package/public/portfolio/casey.html +2 -2
- package/public/portfolio/celia.html +2 -2
- package/public/portfolio/deidre.html +2 -2
- package/public/portfolio/hari.html +2 -2
- package/public/portfolio/index.html +5 -5
- package/public/portfolio/maestro.html +2 -2
- package/public/portfolio/mandy.html +2 -2
- package/public/portfolio/mona.html +2 -2
- package/public/portfolio/pam.html +2 -2
- package/public/portfolio/procella.html +2 -2
- package/public/portfolio/qasm.html +2 -2
- package/public/portfolio/ricardo.html +2 -2
- package/public/portfolio/sade.html +2 -2
- package/public/portfolio/sam.html +2 -2
- package/public/portfolio/sechar.html +2 -2
- package/public/portfolio/sreya.html +2 -2
- package/public/portfolio/swen.html +4 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const electron_1 = require("electron");
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const hub_app_materializer_1 = require("./hub-app-materializer");
|
|
9
|
+
// Issue #1415: this is the packaged Electron `main` entry (see packages/fraim-hub/package.json
|
|
10
|
+
// `"main"`). It is deliberately thin and changes rarely: its only job is to resolve which version
|
|
11
|
+
// of the actual Hub application (the code in desktop-main.ts) to run — the latest published to
|
|
12
|
+
// npm, materializing it once per machine if not already cached — and hand off to it. The native
|
|
13
|
+
// installer only ever needs to ship a new build of this file and the Electron shell itself;
|
|
14
|
+
// every ordinary `fraim-hub` release reaches an already-installed user the moment this
|
|
15
|
+
// resolution runs on their next launch, with no new installer download.
|
|
16
|
+
const BUNDLED_ENTRY_PATH = path_1.default.join(__dirname, 'desktop-main.js');
|
|
17
|
+
async function bootstrapFrom(entryPath) {
|
|
18
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
19
|
+
const entry = require(entryPath);
|
|
20
|
+
await entry.bootstrap();
|
|
21
|
+
}
|
|
22
|
+
async function launch() {
|
|
23
|
+
// Issue #1415: deterministic escape hatch for integration testing — skips the real npm
|
|
24
|
+
// resolution (and its dependence on whatever happens to be published at test time) so a test can
|
|
25
|
+
// prove the Electron main-process boot chain itself (resolveHubAppEntry -> require -> bootstrap
|
|
26
|
+
// -> healthy server) without a network round-trip. Not used in any real launch path; mirrors the
|
|
27
|
+
// existing `FRAIM_INSTALLER_LIFECYCLE_TEST` kill-switch pattern already used for testability.
|
|
28
|
+
const resolved = process.env.FRAIM_HUB_SKIP_MATERIALIZE === '1'
|
|
29
|
+
? { entryPath: BUNDLED_ENTRY_PATH, source: 'bundled', version: electron_1.app.getVersion() }
|
|
30
|
+
: await (0, hub_app_materializer_1.resolveHubAppEntry)({
|
|
31
|
+
bundledVersion: electron_1.app.getVersion(),
|
|
32
|
+
bundledEntryPath: BUNDLED_ENTRY_PATH,
|
|
33
|
+
onProgress: (message) => console.log(`[fraim-hub-launcher] ${message}`),
|
|
34
|
+
}).catch((error) => {
|
|
35
|
+
// resolveHubAppEntry is designed to never throw (every failure mode falls back internally),
|
|
36
|
+
// but a launch must survive even a bug in that resolution rather than fail here.
|
|
37
|
+
console.error('[fraim-hub-launcher] resolution failed unexpectedly; using the bundled build:', error);
|
|
38
|
+
return { entryPath: BUNDLED_ENTRY_PATH, source: 'bundled', version: electron_1.app.getVersion() };
|
|
39
|
+
});
|
|
40
|
+
console.log(`[fraim-hub-launcher] running FRAIM Hub ${resolved.version} (${resolved.source})`);
|
|
41
|
+
try {
|
|
42
|
+
await bootstrapFrom(resolved.entryPath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (resolved.entryPath === BUNDLED_ENTRY_PATH)
|
|
46
|
+
throw error;
|
|
47
|
+
// A materialized copy that fails to load (corrupt cache, an ABI mismatch some future
|
|
48
|
+
// dependency introduces) must not strand the user on a broken launch — fall back to the
|
|
49
|
+
// build that shipped with this installer, which is always known-good for this Electron binary.
|
|
50
|
+
console.error(`[fraim-hub-launcher] materialized FRAIM Hub ${resolved.version} failed to load; falling back to the bundled build:`, error);
|
|
51
|
+
await bootstrapFrom(BUNDLED_ENTRY_PATH);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (process.versions.electron && process.type !== 'renderer') {
|
|
55
|
+
launch().catch((error) => {
|
|
56
|
+
console.error(error instanceof Error ? error.message : error);
|
|
57
|
+
try {
|
|
58
|
+
electron_1.dialog.showErrorBox('FRAIM Hub failed to start', error instanceof Error ? error.message : String(error));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// dialog can be unavailable this early in rare startup failures; the console error above
|
|
62
|
+
// is the fallback record.
|
|
63
|
+
}
|
|
64
|
+
electron_1.app.exit(1);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.launchDesktopShell = launchDesktopShell;
|
|
7
|
+
exports.bootstrap = bootstrap;
|
|
7
8
|
const electron_1 = require("electron");
|
|
8
9
|
const path_1 = __importDefault(require("path"));
|
|
9
10
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -20,6 +21,7 @@ const server_2 = require("../first-run/server");
|
|
|
20
21
|
const session_service_1 = require("../first-run/session-service");
|
|
21
22
|
const fraim_mcp_latest_launcher_1 = require("../cli/mcp/fraim-mcp-latest-launcher");
|
|
22
23
|
const desktop_auto_updater_1 = require("./desktop-auto-updater");
|
|
24
|
+
const hub_app_materializer_1 = require("./hub-app-materializer");
|
|
23
25
|
// Keep installed, running, and user-pinned Windows shortcuts grouped under the
|
|
24
26
|
// stable identity declared in packages/fraim-hub/package.json.
|
|
25
27
|
electron_1.app.setAppUserModelId('ai.fraim.hub');
|
|
@@ -73,7 +75,7 @@ function configurePackagedFraimRuntime() {
|
|
|
73
75
|
if (!electron_1.app.isPackaged && process.env.FRAIM_DESKTOP_FIRST_RUN !== '1')
|
|
74
76
|
return;
|
|
75
77
|
process.env.FRAIM_PACKAGED_CLI_EXECUTABLE = process.execPath;
|
|
76
|
-
process.env.FRAIM_PACKAGED_CLI_SCRIPT =
|
|
78
|
+
process.env.FRAIM_PACKAGED_CLI_SCRIPT = (0, hub_app_materializer_1.resolvePackagedCliScriptPath)(__dirname);
|
|
77
79
|
(0, fraim_mcp_latest_launcher_1.ensureFraimMcpLatestLauncher)();
|
|
78
80
|
}
|
|
79
81
|
function shouldShowEmbeddedFirstRun() {
|
|
@@ -133,11 +135,39 @@ async function configureAutoUpdater() {
|
|
|
133
135
|
});
|
|
134
136
|
return result.action === 'installing';
|
|
135
137
|
}
|
|
136
|
-
|
|
138
|
+
// Issue #1415: a second-instance activation (the user re-clicking the icon while the Hub is
|
|
139
|
+
// already running) is the common case once login-item autostart + hide-to-tray keep the Hub
|
|
140
|
+
// resident for days, and it is the one launch-shaped event where the desktop-launcher.ts
|
|
141
|
+
// materializer never gets a chance to run — bootstrap() has already won the single-instance lock
|
|
142
|
+
// by the time it fires. Without this check, an already-running Hub could only ever pick up a new
|
|
143
|
+
// `fraim-hub` release through the slower electron-updater feed below, which is the exact lagging
|
|
144
|
+
// signal #1415 was filed about. `getFraimVersion()` (not `app.getVersion()`) is compared, because
|
|
145
|
+
// it resolves from wherever the currently-running code actually lives — the bundled app.asar or a
|
|
146
|
+
// previously materialized copy — while `app.getVersion()` always names the former.
|
|
147
|
+
async function checkForHubAppUpdateOnSecondInstance() {
|
|
148
|
+
const runningVersion = (0, version_utils_1.getFraimVersion)();
|
|
149
|
+
const resolved = await (0, hub_app_materializer_1.resolveHubAppEntry)({
|
|
150
|
+
bundledVersion: runningVersion,
|
|
151
|
+
bundledEntryPath: __filename,
|
|
152
|
+
});
|
|
153
|
+
if (resolved.version === runningVersion)
|
|
154
|
+
return false;
|
|
155
|
+
console.log(`[fraim] newer FRAIM Hub ${resolved.version} available (was running ${runningVersion}); restarting to apply it`);
|
|
156
|
+
electron_1.app.relaunch();
|
|
157
|
+
electron_1.app.quit();
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
// Issue #1415: shared by both re-launch-shaped events on an already-running Hub — Windows/Linux's
|
|
161
|
+
// `second-instance` (a new process lost the single-instance lock) and macOS's `activate` (the
|
|
162
|
+
// Dock icon click that never spawns a new process at all, so `second-instance` never fires there).
|
|
163
|
+
// Before this, neither path re-checked npm-latest, only the older, slower electron-updater feed.
|
|
164
|
+
function checkForRelaunchAttemptUpdate() {
|
|
137
165
|
if (!electron_1.app.isPackaged || process.env.FRAIM_INSTALLER_LIFECYCLE_TEST === '1')
|
|
138
166
|
return;
|
|
139
|
-
void
|
|
140
|
-
|
|
167
|
+
void checkForHubAppUpdateOnSecondInstance()
|
|
168
|
+
.then((restarting) => (restarting ? undefined : configureAutoUpdater().then(() => undefined)))
|
|
169
|
+
.catch((err) => {
|
|
170
|
+
console.warn('[fraim] relaunch-attempt update check failed:', err);
|
|
141
171
|
});
|
|
142
172
|
}
|
|
143
173
|
// ---------------------------------------------------------------------------
|
|
@@ -469,6 +499,9 @@ async function launchDesktopShell(options) {
|
|
|
469
499
|
// ---------------------------------------------------------------------------
|
|
470
500
|
// Bootstrap
|
|
471
501
|
// ---------------------------------------------------------------------------
|
|
502
|
+
// Issue #1415: exported so `desktop-launcher.ts` can `require()` this module (whether the copy
|
|
503
|
+
// bundled in this installer or a materialized npm-latest copy) and invoke bootstrap explicitly,
|
|
504
|
+
// instead of relying on the module's own top-level self-execution below.
|
|
472
505
|
async function bootstrap() {
|
|
473
506
|
const options = parseArgs(process.argv.slice(2));
|
|
474
507
|
applyUserDataOverride();
|
|
@@ -483,7 +516,7 @@ async function bootstrap() {
|
|
|
483
516
|
return;
|
|
484
517
|
}
|
|
485
518
|
electron_1.app.on('second-instance', () => {
|
|
486
|
-
void electron_1.app.whenReady().then(
|
|
519
|
+
void electron_1.app.whenReady().then(checkForRelaunchAttemptUpdate);
|
|
487
520
|
if (mainWindow) {
|
|
488
521
|
mainWindow.show();
|
|
489
522
|
mainWindow.focus();
|
|
@@ -508,7 +541,10 @@ async function bootstrap() {
|
|
|
508
541
|
return;
|
|
509
542
|
}
|
|
510
543
|
electron_1.app.on('activate', () => {
|
|
511
|
-
// macOS: clicking dock icon re-shows the window
|
|
544
|
+
// macOS: clicking dock icon re-shows the window. Unlike Windows/Linux, this never spawns a
|
|
545
|
+
// second process (so 'second-instance' never fires here) — it is macOS's equivalent
|
|
546
|
+
// relaunch-attempt event, so it gets the same npm-latest re-check (#1415).
|
|
547
|
+
checkForRelaunchAttemptUpdate();
|
|
512
548
|
if (mainWindow) {
|
|
513
549
|
mainWindow.show();
|
|
514
550
|
mainWindow.focus();
|
|
@@ -533,7 +569,12 @@ async function bootstrap() {
|
|
|
533
569
|
});
|
|
534
570
|
await launchDesktopShell(options);
|
|
535
571
|
}
|
|
536
|
-
|
|
572
|
+
// Issue #1415: self-execution is now gated on `require.main === module` in addition to the
|
|
573
|
+
// existing Electron-main-process check, so `desktop-launcher.ts` requiring this file (bundled or
|
|
574
|
+
// materialized) does not double-run bootstrap — the launcher calls the exported `bootstrap`
|
|
575
|
+
// explicitly instead. Direct invocation (`npm run hub:desktop`, Electron-launching test suites)
|
|
576
|
+
// is unaffected: this file is still `require.main` in that case.
|
|
577
|
+
if (require.main === module && process.versions.electron && process.type !== 'renderer') {
|
|
537
578
|
bootstrap().catch(async (error) => {
|
|
538
579
|
console.error(error instanceof Error ? error.message : error);
|
|
539
580
|
await stopServerOnce();
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.HUB_APP_ENTRY_RELATIVE = exports.HUB_APP_PACKAGE_NAME = void 0;
|
|
7
|
+
exports.resolvePackagedCliScriptPath = resolvePackagedCliScriptPath;
|
|
8
|
+
exports.planHubAppMaterialization = planHubAppMaterialization;
|
|
9
|
+
exports.resolveHubAppCacheDir = resolveHubAppCacheDir;
|
|
10
|
+
exports.isHubAppCacheComplete = isHubAppCacheComplete;
|
|
11
|
+
exports.listCachedHubAppVersions = listCachedHubAppVersions;
|
|
12
|
+
exports.ensureMaterializedHubApp = ensureMaterializedHubApp;
|
|
13
|
+
exports.resolveHubAppEntry = resolveHubAppEntry;
|
|
14
|
+
const fs_1 = __importDefault(require("fs"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
16
|
+
const child_process_1 = require("child_process");
|
|
17
|
+
const semver_1 = __importDefault(require("semver"));
|
|
18
|
+
const project_fraim_paths_1 = require("../core/utils/project-fraim-paths");
|
|
19
|
+
const hub_latest_version_1 = require("./hub-latest-version");
|
|
20
|
+
const managed_node_runtime_1 = require("../cli/utils/managed-node-runtime");
|
|
21
|
+
// Issue #1415: a packaged FRAIM Hub install can already be behind npm's published "latest" the
|
|
22
|
+
// moment it is downloaded, and its own electron-updater feed (#1387) only advances once a full
|
|
23
|
+
// signed desktop build has finished — which happens on a much slower cadence than npm publishes.
|
|
24
|
+
// This module reconciles the two: on every launch, resolve the version actually published to npm
|
|
25
|
+
// (`fraim-hub` and `fraim` are lockstep-versioned, so `hub-latest-version.ts`'s existing 5-minute-
|
|
26
|
+
// cached npm lookup is reused rather than duplicated), materialize that version's application code
|
|
27
|
+
// once per machine (mirroring `electron-dist.ts`'s "extract once, reuse forever" shared-dist
|
|
28
|
+
// pattern), and hand back the entry point to run. The Electron shell itself — installed once via
|
|
29
|
+
// the native installer — never needs a new install for an ordinary `fraim-hub` point release; only
|
|
30
|
+
// the application payload this module resolves does.
|
|
31
|
+
//
|
|
32
|
+
// The decision (`planHubAppMaterialization`) is a pure function so the whole staleness/cache/
|
|
33
|
+
// offline matrix is unit-testable without touching npm, disk, or Electron — the same separation
|
|
34
|
+
// `hub-launch-decision.ts` (#755) and `desktop-auto-updater.ts` (#1387) already established for
|
|
35
|
+
// this exact class of problem.
|
|
36
|
+
exports.HUB_APP_PACKAGE_NAME = 'fraim-hub';
|
|
37
|
+
/** Relative path, from a materialized (or bundled) package root, to the Electron main entry. */
|
|
38
|
+
exports.HUB_APP_ENTRY_RELATIVE = path_1.default.join('dist', 'src', 'ai-hub', 'desktop-main.js');
|
|
39
|
+
/**
|
|
40
|
+
* Where the packaged CLI/MCP shim (`ensureFraimMcpLatestLauncher()`) should find `fraim`'s CLI
|
|
41
|
+
* script, derived from `desktop-main.ts`'s own directory (`<packageRoot>/dist/src/ai-hub/`) rather
|
|
42
|
+
* than Electron's `app.getAppPath()`. `desktop-launcher.ts` may have loaded `desktop-main.js` from a
|
|
43
|
+
* materialized `fraim-hub@<latest>` copy under `~/.fraim/bin/hub-app/<version>/` rather than from
|
|
44
|
+
* the original app.asar, and `app.getAppPath()` always names the latter regardless of which copy is
|
|
45
|
+
* actually running. `moduleDir` tracks whichever copy is really executing. Kept in this
|
|
46
|
+
* Electron-free module (rather than inlined in `desktop-main.ts`) so it is unit-testable without an
|
|
47
|
+
* Electron `app` object.
|
|
48
|
+
*/
|
|
49
|
+
function resolvePackagedCliScriptPath(moduleDir) {
|
|
50
|
+
return path_1.default.join(moduleDir, '..', '..', '..', 'node_modules', 'fraim', 'bin', 'fraim.js');
|
|
51
|
+
}
|
|
52
|
+
function pickNewestComplete(cachedVersions) {
|
|
53
|
+
const versions = cachedVersions.filter((entry) => entry.complete).map((entry) => entry.version);
|
|
54
|
+
if (versions.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
return versions.reduce((newest, candidate) => (semver_1.default.gt(candidate, newest) ? candidate : newest));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Decide whether to run a cached materialized version, materialize a new one, or fall back to the
|
|
60
|
+
* version bundled with this installer. Pure: no fs, no network, no Electron.
|
|
61
|
+
*/
|
|
62
|
+
function planHubAppMaterialization(input) {
|
|
63
|
+
const { bundledVersion, latestPublishedVersion, cachedVersions } = input;
|
|
64
|
+
if (latestPublishedVersion === null) {
|
|
65
|
+
const newestCached = pickNewestComplete(cachedVersions);
|
|
66
|
+
if (newestCached) {
|
|
67
|
+
return {
|
|
68
|
+
action: 'use-cached',
|
|
69
|
+
version: newestCached,
|
|
70
|
+
reason: 'npm registry unreachable; reusing the newest complete cached version',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
action: 'use-bundled',
|
|
75
|
+
version: bundledVersion,
|
|
76
|
+
reason: 'npm registry unreachable and no cached version available; falling back to the version bundled with this installer',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (latestPublishedVersion === bundledVersion) {
|
|
80
|
+
return {
|
|
81
|
+
action: 'use-bundled',
|
|
82
|
+
version: bundledVersion,
|
|
83
|
+
reason: 'the installed shell already matches the latest published version',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const alreadyMaterialized = cachedVersions.some((entry) => entry.complete && entry.version === latestPublishedVersion);
|
|
87
|
+
if (alreadyMaterialized) {
|
|
88
|
+
return {
|
|
89
|
+
action: 'use-cached',
|
|
90
|
+
version: latestPublishedVersion,
|
|
91
|
+
reason: 'latest published version is already materialized on disk',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
action: 'materialize',
|
|
96
|
+
version: latestPublishedVersion,
|
|
97
|
+
reason: `latest published version ${latestPublishedVersion} is not yet materialized`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Cache directory layout: ~/.fraim/bin/hub-app/<version>/
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Same shape as an npm version, optionally with a prerelease/build suffix. Anchoring on this
|
|
104
|
+
// before ever joining a version into a path means a hostile string (`../../..`, an absolute path)
|
|
105
|
+
// can never escape the versioned cache directory.
|
|
106
|
+
const SAFE_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
107
|
+
function assertPathSafeVersion(version) {
|
|
108
|
+
if (!SAFE_VERSION.test(version)) {
|
|
109
|
+
throw new Error(`Refusing to use "${version}" as a fraim-hub version: expected an exact release such as 2.0.290.`);
|
|
110
|
+
}
|
|
111
|
+
return version;
|
|
112
|
+
}
|
|
113
|
+
function resolveHubAppCacheDir(version, fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)()) {
|
|
114
|
+
return path_1.default.join(fraimDir, 'bin', 'hub-app', assertPathSafeVersion(version));
|
|
115
|
+
}
|
|
116
|
+
/** Whether `dir` holds a complete materialized install for exactly `version`. */
|
|
117
|
+
function isHubAppCacheComplete(dir, version) {
|
|
118
|
+
try {
|
|
119
|
+
const recorded = fs_1.default.readFileSync(path_1.default.join(dir, 'version'), 'utf8').trim();
|
|
120
|
+
if (recorded !== version)
|
|
121
|
+
return false;
|
|
122
|
+
return fs_1.default.existsSync(path_1.default.join(dir, exports.HUB_APP_ENTRY_RELATIVE));
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Every version directory under the cache root that is a complete, trustworthy install. */
|
|
129
|
+
function listCachedHubAppVersions(fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)()) {
|
|
130
|
+
const root = path_1.default.join(fraimDir, 'bin', 'hub-app');
|
|
131
|
+
let entries;
|
|
132
|
+
try {
|
|
133
|
+
entries = fs_1.default.readdirSync(root, { withFileTypes: true });
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
const versions = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (!entry.isDirectory() || !semver_1.default.valid(entry.name))
|
|
141
|
+
continue;
|
|
142
|
+
if (isHubAppCacheComplete(path_1.default.join(root, entry.name), entry.name)) {
|
|
143
|
+
versions.push({ version: entry.name, complete: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return versions;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Default installer: `npm install fraim-hub@<version>` into a scratch prefix, then flatten the
|
|
150
|
+
* installed package up to `destDir` so callers can treat `destDir` itself as the package root
|
|
151
|
+
* (`destDir/dist/...`), with the package's own dependencies left as siblings under
|
|
152
|
+
* `destDir/node_modules/` where Node's module resolution finds them from any depth under `destDir`.
|
|
153
|
+
* A fresh, empty `--prefix` install of a single package has nothing to conflict with, so npm hoists
|
|
154
|
+
* every dependency to that top level; nothing here relies on nested-dependency resolution.
|
|
155
|
+
*
|
|
156
|
+
* The `npm` executable itself is resolved via `ensureManagedNpm()` (system PATH, an already-managed
|
|
157
|
+
* runtime, or a freshly downloaded one) rather than assumed present on PATH — a genuinely GUI-only
|
|
158
|
+
* install (native installer only, no CLI ever run) has neither system npm nor FRAIM's own managed
|
|
159
|
+
* one, since first-run's embedded-desktop path only needs Electron's own bundled Node for its own
|
|
160
|
+
* purposes and never downloads a standalone npm (see managed-node-runtime.ts's own header comment).
|
|
161
|
+
*/
|
|
162
|
+
// Node's spawnSync fails with EINVAL when a `.cmd` shim (npm's own launcher on Windows) is
|
|
163
|
+
// invoked directly without a shell — the same issue `fraim-mcp-latest-launcher.ts` already works
|
|
164
|
+
// around for `npm`/`npx`. Mirrors that exact fix: run through cmd.exe with args quoted, rather
|
|
165
|
+
// than inventing a different approach for this second call site.
|
|
166
|
+
function quoteCmdArg(arg) {
|
|
167
|
+
return /[\s"&|<>^]/.test(arg) ? `"${arg.replace(/"/g, '""')}"` : arg;
|
|
168
|
+
}
|
|
169
|
+
function runNpmInstall(npmPath, args) {
|
|
170
|
+
if (process.platform !== 'win32') {
|
|
171
|
+
return (0, child_process_1.spawnSync)(npmPath, args, { encoding: 'utf8' });
|
|
172
|
+
}
|
|
173
|
+
return (0, child_process_1.spawnSync)(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', [quoteCmdArg(npmPath), ...args.map(quoteCmdArg)].join(' ')], { encoding: 'utf8' });
|
|
174
|
+
}
|
|
175
|
+
const defaultInstallPackage = async (version, destDir) => {
|
|
176
|
+
fs_1.default.mkdirSync(destDir, { recursive: true });
|
|
177
|
+
const npm = await (0, managed_node_runtime_1.ensureManagedNpm)();
|
|
178
|
+
const result = runNpmInstall(npm.npmPath, ['install', `${exports.HUB_APP_PACKAGE_NAME}@${version}`, '--no-save', '--omit=dev', '--prefix', destDir]);
|
|
179
|
+
if (result.status !== 0) {
|
|
180
|
+
const detail = result.stderr || result.stdout || result.error?.message || 'unknown error';
|
|
181
|
+
throw new Error(`npm install ${exports.HUB_APP_PACKAGE_NAME}@${version} failed via ${npm.source} npm (exit ${result.status}): ${detail}`);
|
|
182
|
+
}
|
|
183
|
+
const installedRoot = path_1.default.join(destDir, 'node_modules', exports.HUB_APP_PACKAGE_NAME);
|
|
184
|
+
if (!fs_1.default.existsSync(installedRoot)) {
|
|
185
|
+
throw new Error(`npm install ${exports.HUB_APP_PACKAGE_NAME}@${version} did not produce ${installedRoot}`);
|
|
186
|
+
}
|
|
187
|
+
for (const entry of fs_1.default.readdirSync(installedRoot)) {
|
|
188
|
+
const from = path_1.default.join(installedRoot, entry);
|
|
189
|
+
const to = path_1.default.join(destDir, entry);
|
|
190
|
+
if (entry === 'node_modules' && fs_1.default.existsSync(to)) {
|
|
191
|
+
// Only reachable if npm nested a dependency inside fraim-hub's own node_modules despite the
|
|
192
|
+
// empty prefix (a version conflict with itself is not possible, but defend anyway): merge
|
|
193
|
+
// child-by-child rather than clobbering the siblings already hoisted to destDir/node_modules.
|
|
194
|
+
for (const nested of fs_1.default.readdirSync(from)) {
|
|
195
|
+
fs_1.default.renameSync(path_1.default.join(from, nested), path_1.default.join(to, nested));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
fs_1.default.renameSync(from, to);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
fs_1.default.rmSync(installedRoot, { recursive: true, force: true });
|
|
203
|
+
};
|
|
204
|
+
/**
|
|
205
|
+
* Return the entry path for `version`, materializing it first if it is not already a complete
|
|
206
|
+
* cache entry. Returns immediately (no npm invocation) when a complete entry already exists.
|
|
207
|
+
*/
|
|
208
|
+
async function ensureMaterializedHubApp(version, options = {}) {
|
|
209
|
+
const fraimDir = options.fraimDir ?? (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
210
|
+
const installPackage = options.installPackage ?? defaultInstallPackage;
|
|
211
|
+
const destDir = resolveHubAppCacheDir(version, fraimDir);
|
|
212
|
+
const entryPath = path_1.default.join(destDir, exports.HUB_APP_ENTRY_RELATIVE);
|
|
213
|
+
if (isHubAppCacheComplete(destDir, version)) {
|
|
214
|
+
return entryPath;
|
|
215
|
+
}
|
|
216
|
+
const stagingDir = `${destDir}.staging-${process.pid}`;
|
|
217
|
+
fs_1.default.mkdirSync(path_1.default.dirname(destDir), { recursive: true });
|
|
218
|
+
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
219
|
+
try {
|
|
220
|
+
await installPackage(version, stagingDir);
|
|
221
|
+
// Written only on success, after the installer's own work, so a throw never leaves a staging
|
|
222
|
+
// tree that could be mistaken for complete.
|
|
223
|
+
fs_1.default.writeFileSync(path_1.default.join(stagingDir, 'version'), version, 'utf8');
|
|
224
|
+
if (!isHubAppCacheComplete(stagingDir, version)) {
|
|
225
|
+
throw new Error(`materialized ${exports.HUB_APP_PACKAGE_NAME}@${version} is missing ${exports.HUB_APP_ENTRY_RELATIVE}; refusing to promote an incomplete install`);
|
|
226
|
+
}
|
|
227
|
+
// A concurrent launch may have finished materializing the same version already; the winner
|
|
228
|
+
// keeps its copy rather than being clobbered by this one (mirrors electron-dist.ts).
|
|
229
|
+
if (!isHubAppCacheComplete(destDir, version)) {
|
|
230
|
+
fs_1.default.rmSync(destDir, { recursive: true, force: true });
|
|
231
|
+
try {
|
|
232
|
+
fs_1.default.renameSync(stagingDir, destDir);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (!isHubAppCacheComplete(destDir, version))
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
finally {
|
|
241
|
+
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
return entryPath;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Resolve which `desktop-main.js` to run: the latest published version, materializing it first if
|
|
247
|
+
* needed, or the bundled build when npm is unreachable, already current, or materialization fails
|
|
248
|
+
* for any reason. A materialization failure must never block a launch — it always falls back to
|
|
249
|
+
* the bundled copy rather than propagating.
|
|
250
|
+
*/
|
|
251
|
+
async function resolveHubAppEntry(options) {
|
|
252
|
+
const fraimDir = options.fraimDir ?? (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
253
|
+
const getLatest = options.getLatestPublishedVersion ?? hub_latest_version_1.getLatestPublishedVersion;
|
|
254
|
+
const cachedVersions = listCachedHubAppVersions(fraimDir);
|
|
255
|
+
let latestPublishedVersion;
|
|
256
|
+
try {
|
|
257
|
+
latestPublishedVersion = await getLatest();
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
latestPublishedVersion = null;
|
|
261
|
+
}
|
|
262
|
+
const plan = planHubAppMaterialization({
|
|
263
|
+
bundledVersion: options.bundledVersion,
|
|
264
|
+
latestPublishedVersion,
|
|
265
|
+
cachedVersions,
|
|
266
|
+
});
|
|
267
|
+
if (plan.action === 'use-bundled') {
|
|
268
|
+
return { entryPath: options.bundledEntryPath, source: 'bundled', version: plan.version };
|
|
269
|
+
}
|
|
270
|
+
if (plan.action === 'use-cached') {
|
|
271
|
+
return {
|
|
272
|
+
entryPath: path_1.default.join(resolveHubAppCacheDir(plan.version, fraimDir), exports.HUB_APP_ENTRY_RELATIVE),
|
|
273
|
+
source: 'cached',
|
|
274
|
+
version: plan.version,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
options.onProgress?.(`Fetching FRAIM Hub ${plan.version}...`);
|
|
279
|
+
const entryPath = await ensureMaterializedHubApp(plan.version, { fraimDir, installPackage: options.installPackage });
|
|
280
|
+
return { entryPath, source: 'cached', version: plan.version };
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return { entryPath: options.bundledEntryPath, source: 'bundled', version: options.bundledVersion };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -6332,14 +6332,10 @@ class AiHubServer {
|
|
|
6332
6332
|
messages: persistedConversation ? persistedMessagesForRun(persistedConversation) : [],
|
|
6333
6333
|
events: resumeEvents,
|
|
6334
6334
|
eventLogRefs: persistedConversation?.eventLogRefs || [],
|
|
6335
|
-
//
|
|
6336
|
-
//
|
|
6337
|
-
//
|
|
6338
|
-
//
|
|
6339
|
-
// carry the persisted run projection forward regardless of the
|
|
6340
|
-
// conversation's prior status. If the resumed session genuinely has
|
|
6341
|
-
// nothing left to do, it simply reaches the same completed state
|
|
6342
|
-
// again on its own the next time seekMentoring reports its phase.
|
|
6335
|
+
// A manager can issue new instructions after a job was marked done.
|
|
6336
|
+
// That correction stays in the same conversation/job identity and keeps
|
|
6337
|
+
// its phase history so the tracker can show the job moving back to an
|
|
6338
|
+
// earlier phase instead of pretending this is unrelated fresh work.
|
|
6343
6339
|
currentPhase: persistedRun?.currentPhase || null,
|
|
6344
6340
|
phaseHistory: persistedRun?.phaseHistory || [],
|
|
6345
6341
|
phaseVisits: persistedRun?.phaseVisits || [],
|