fraim 2.0.175 → 2.0.177
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 +3 -5
- package/dist/src/cli/setup/ide-detector.js +40 -7
- package/dist/src/first-run/install-state.js +1 -0
- package/dist/src/first-run/server.js +13 -0
- package/dist/src/first-run/session-service.js +20 -1
- package/package.json +3 -1
- package/public/first-run/script.js +64 -0
- package/public/first-run/styles.css +14 -0
|
@@ -65,11 +65,9 @@ function openBrowser(url) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
const runFirstRun = async (options) => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
68
|
+
// Issue #646: the key is optional. When launched by the no-terminal macOS
|
|
69
|
+
// installer (.pkg), no key is passed — the wizard prompts the user to paste it.
|
|
70
|
+
const key = options.key || process.env.FRAIM_API_KEY || process.env.FRAIM_SETUP_KEY || process.env.FRAIM_INSTALL_KEY || '';
|
|
73
71
|
const sessionService = new session_service_1.FirstRunSessionService({
|
|
74
72
|
key,
|
|
75
73
|
headless: options.headless,
|
|
@@ -23,6 +23,39 @@ exports.expandPath = expandPath;
|
|
|
23
23
|
const checkMultiplePaths = (paths) => {
|
|
24
24
|
return paths.some(p => fs_1.default.existsSync(expandPath(p)));
|
|
25
25
|
};
|
|
26
|
+
// Issue #646 (Bug 2): a config directory left behind after uninstall (e.g. ~/.cursor,
|
|
27
|
+
// ~/Library/Application Support/Cursor) made config-surface detection report a GUI app
|
|
28
|
+
// as "Installed" when it was not. On macOS an installed GUI app keeps its .app bundle,
|
|
29
|
+
// which is the reliable signal. So on macOS we require app-bundle evidence for GUI apps;
|
|
30
|
+
// on Windows/Linux we keep the existing config-surface check unchanged (no reported
|
|
31
|
+
// false-positive there, and those install paths are not validated on this platform).
|
|
32
|
+
const macAppBundlePaths = (appName) => [
|
|
33
|
+
`/Applications/${appName}.app`,
|
|
34
|
+
`~/Applications/${appName}.app`,
|
|
35
|
+
];
|
|
36
|
+
// Issue #646 follow-up: a user may run a GUI app from a non-standard location
|
|
37
|
+
// (e.g. straight from a mounted DMG: /Volumes/.../Cursor.app) that the
|
|
38
|
+
// /Applications checks miss. A running process of the app is definitive proof it
|
|
39
|
+
// is installed and in use, and a stale config dir alone has no such process — so
|
|
40
|
+
// this catches real installs without re-introducing the stale-dir false positive.
|
|
41
|
+
const isMacAppRunning = (appName) => {
|
|
42
|
+
if (process.platform !== 'darwin')
|
|
43
|
+
return false;
|
|
44
|
+
// Test seam: `pgrep` is system-global (not HOME-scoped), so tests that simulate
|
|
45
|
+
// "app not installed" via a temp HOME set this to keep results deterministic.
|
|
46
|
+
if (process.env.FRAIM_DETECT_DISABLE_PROCESS_CHECK === '1')
|
|
47
|
+
return false;
|
|
48
|
+
const result = (0, child_process_1.spawnSync)('pgrep', ['-f', `${appName}.app/Contents`], { encoding: 'utf8', timeout: 1500 });
|
|
49
|
+
return result.status === 0 && Boolean((result.stdout || '').trim());
|
|
50
|
+
};
|
|
51
|
+
const guiAppDetect = (configSurfaceCheck, macAppName) => {
|
|
52
|
+
return () => {
|
|
53
|
+
if (process.platform === 'darwin') {
|
|
54
|
+
return checkMultiplePaths(macAppBundlePaths(macAppName)) || isMacAppRunning(macAppName);
|
|
55
|
+
}
|
|
56
|
+
return configSurfaceCheck();
|
|
57
|
+
};
|
|
58
|
+
};
|
|
26
59
|
const availableByVersionProbe = (command) => {
|
|
27
60
|
const result = process.platform === 'win32'
|
|
28
61
|
? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500 })
|
|
@@ -112,7 +145,7 @@ exports.IDE_CONFIGS = [
|
|
|
112
145
|
configFormat: 'json',
|
|
113
146
|
configType: 'claude',
|
|
114
147
|
invocationProfile: 'launch-phrase',
|
|
115
|
-
detectMethod: detectClaude,
|
|
148
|
+
detectMethod: guiAppDetect(detectClaude, 'Claude'),
|
|
116
149
|
aliases: ['claude', 'claude-desktop', 'claude desktop', 'claude-cowork', 'cowork', 'claude cowork'],
|
|
117
150
|
alternativePaths: [
|
|
118
151
|
'~/Library/Application Support/Claude/claude_desktop_config.json'
|
|
@@ -126,9 +159,9 @@ exports.IDE_CONFIGS = [
|
|
|
126
159
|
configFormat: 'json',
|
|
127
160
|
configType: 'standard',
|
|
128
161
|
invocationProfile: 'cursor-mention',
|
|
129
|
-
detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
|
|
162
|
+
detectMethod: guiAppDetect(() => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')), 'Antigravity'),
|
|
130
163
|
description: 'Google Gemini Antigravity IDE',
|
|
131
|
-
downloadUrl: 'https://
|
|
164
|
+
downloadUrl: 'https://antigravity.google/',
|
|
132
165
|
},
|
|
133
166
|
{
|
|
134
167
|
name: 'Gemini CLI',
|
|
@@ -152,7 +185,7 @@ exports.IDE_CONFIGS = [
|
|
|
152
185
|
configFormat: 'json',
|
|
153
186
|
configType: 'kiro',
|
|
154
187
|
invocationProfile: 'kiro-hashtag',
|
|
155
|
-
detectMethod: () => fs_1.default.existsSync(expandPath('~/.kiro')),
|
|
188
|
+
detectMethod: guiAppDetect(() => fs_1.default.existsSync(expandPath('~/.kiro')), 'Kiro'),
|
|
156
189
|
description: 'Kiro AI-powered IDE',
|
|
157
190
|
downloadUrl: 'https://kiro.dev/',
|
|
158
191
|
},
|
|
@@ -163,7 +196,7 @@ exports.IDE_CONFIGS = [
|
|
|
163
196
|
configType: 'kiro',
|
|
164
197
|
adapterConfigType: 'cursor',
|
|
165
198
|
invocationProfile: 'cursor-mention',
|
|
166
|
-
detectMethod: detectCursor,
|
|
199
|
+
detectMethod: guiAppDetect(detectCursor, 'Cursor'),
|
|
167
200
|
alternativePaths: [
|
|
168
201
|
'~/Library/Application Support/Cursor/mcp.json',
|
|
169
202
|
'~/AppData/Roaming/Cursor/mcp.json',
|
|
@@ -182,7 +215,7 @@ exports.IDE_CONFIGS = [
|
|
|
182
215
|
configFormat: 'json',
|
|
183
216
|
configType: 'vscode',
|
|
184
217
|
invocationProfile: 'vscode-prompt',
|
|
185
|
-
detectMethod: detectVSCode,
|
|
218
|
+
detectMethod: guiAppDetect(detectVSCode, 'Visual Studio Code'),
|
|
186
219
|
alternativePaths: [
|
|
187
220
|
'~/Library/Application Support/Code/User/mcp.json',
|
|
188
221
|
'~/AppData/Roaming/Code/User/mcp.json',
|
|
@@ -219,7 +252,7 @@ exports.IDE_CONFIGS = [
|
|
|
219
252
|
configFormat: 'json',
|
|
220
253
|
configType: 'windsurf',
|
|
221
254
|
invocationProfile: 'windsurf-command',
|
|
222
|
-
detectMethod: detectWindsurf,
|
|
255
|
+
detectMethod: guiAppDetect(detectWindsurf, 'Windsurf'),
|
|
223
256
|
alternativePaths: [
|
|
224
257
|
'~/Library/Application Support/Windsurf/mcp_config.json',
|
|
225
258
|
'~/AppData/Roaming/Windsurf/mcp_config.json',
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.maskInstallKey = maskInstallKey;
|
|
6
7
|
exports.createInitialFirstRunState = createInitialFirstRunState;
|
|
7
8
|
exports.loadFirstRunState = loadFirstRunState;
|
|
8
9
|
exports.saveFirstRunState = saveFirstRunState;
|
|
@@ -183,6 +183,19 @@ class FirstRunServer {
|
|
|
183
183
|
return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not run row.' });
|
|
184
184
|
}
|
|
185
185
|
});
|
|
186
|
+
// Issue #646: accept a user-pasted key when first-run was launched without one
|
|
187
|
+
// (the no-terminal macOS installer path).
|
|
188
|
+
this.app.post('/api/first-run/set-key', (req, res) => {
|
|
189
|
+
const { key } = req.body || {};
|
|
190
|
+
if (!key || typeof key !== 'string') {
|
|
191
|
+
return res.status(400).json({ ok: false, error: 'key is required.' });
|
|
192
|
+
}
|
|
193
|
+
const result = this.sessionService.setKey(key);
|
|
194
|
+
if (!result.ok) {
|
|
195
|
+
return res.status(400).json({ ok: false, error: result.message });
|
|
196
|
+
}
|
|
197
|
+
return res.json({ ok: true, session: this.sessionService.getSession() });
|
|
198
|
+
});
|
|
186
199
|
this.app.post('/api/first-run/agent/change', (req, res) => {
|
|
187
200
|
try {
|
|
188
201
|
return res.json(this.sessionService.changeAgent(req.body || {}));
|
|
@@ -200,7 +200,10 @@ function surfaceForAgent(option) {
|
|
|
200
200
|
}
|
|
201
201
|
class FirstRunSessionService {
|
|
202
202
|
constructor(options) {
|
|
203
|
-
|
|
203
|
+
// Issue #646: the key may be absent when first-run is launched by the macOS
|
|
204
|
+
// installer (.pkg) instead of `fraim first-run --key=…`. In that case the
|
|
205
|
+
// wizard prompts the user to paste their key (see setKey / needsKey).
|
|
206
|
+
this.key = options.key || '';
|
|
204
207
|
this.headless = options.headless === true;
|
|
205
208
|
this.fakeMode = getFakeStateMode();
|
|
206
209
|
this.fakeStderr =
|
|
@@ -449,8 +452,24 @@ class FirstRunSessionService {
|
|
|
449
452
|
agentOptions: types_1.FIRST_RUN_AGENT_OPTIONS,
|
|
450
453
|
currentAgentId: this.state.agentId,
|
|
451
454
|
supportedAgents,
|
|
455
|
+
needsKey: !this.key,
|
|
452
456
|
};
|
|
453
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Issue #646: accept a user-pasted FRAIM key when first-run was launched
|
|
460
|
+
* without one (the no-terminal macOS installer path). Returns ok=false with a
|
|
461
|
+
* message on an invalid key so the wizard can show inline guidance.
|
|
462
|
+
*/
|
|
463
|
+
setKey(rawKey) {
|
|
464
|
+
const key = (rawKey || '').trim();
|
|
465
|
+
if (!/^fraim_[A-Za-z0-9]+$/.test(key)) {
|
|
466
|
+
return { ok: false, message: 'That doesn\'t look like a FRAIM key. It should start with "fraim_" — copy it from your account page.' };
|
|
467
|
+
}
|
|
468
|
+
this.key = key;
|
|
469
|
+
this.state.installKeyRef = (0, install_state_1.maskInstallKey)(key);
|
|
470
|
+
this.persist();
|
|
471
|
+
return { ok: true };
|
|
472
|
+
}
|
|
454
473
|
respond(message, ok) {
|
|
455
474
|
return {
|
|
456
475
|
ok,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.177",
|
|
4
4
|
"description": "FRAIM CLI - Framework for Rigor-based AI Management (alias for fraim-framework)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"hub:desktop": "npm run build && electron dist/src/ai-hub/desktop-main.js",
|
|
29
29
|
"hub:dev": "tsx scripts/start-hub-dev.ts",
|
|
30
30
|
"firstrun:dev": "tsx scripts/start-firstrun-dev.ts",
|
|
31
|
+
"sign:mac": "bash scripts/sign-macos-installer.sh",
|
|
32
|
+
"build:mac-installer": "bash scripts/build-macos-installer.sh",
|
|
31
33
|
"start:fraim": "tsx src/fraim-mcp-server.ts",
|
|
32
34
|
"dev:fraim": "tsx --watch src/fraim-mcp-server.ts",
|
|
33
35
|
"serve:website": "node fraim-pro/serve.js",
|
|
@@ -580,8 +580,72 @@
|
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
582
|
|
|
583
|
+
// Issue #646: when first-run was launched without a key (the no-terminal macOS
|
|
584
|
+
// installer path), gate the whole wizard behind a paste-your-key step.
|
|
585
|
+
function renderKeyEntry() {
|
|
586
|
+
CHECKLIST_EL.className = 'setup-shell';
|
|
587
|
+
CHECKLIST_EL.innerHTML = '';
|
|
588
|
+
PRIMARY_BUTTON.style.display = 'none';
|
|
589
|
+
setHeader('Set up FRAIM', 'Paste the FRAIM key from your account page to get started.');
|
|
590
|
+
|
|
591
|
+
const card = document.createElement('div');
|
|
592
|
+
card.className = 'setup-pane';
|
|
593
|
+
card.setAttribute('data-testid', 'key-entry');
|
|
594
|
+
|
|
595
|
+
const label = document.createElement('label');
|
|
596
|
+
label.className = 'pane-copy';
|
|
597
|
+
label.setAttribute('for', 'fraim-key-input');
|
|
598
|
+
label.textContent = 'Your FRAIM key';
|
|
599
|
+
card.appendChild(label);
|
|
600
|
+
|
|
601
|
+
const input = document.createElement('input');
|
|
602
|
+
input.type = 'text';
|
|
603
|
+
input.id = 'fraim-key-input';
|
|
604
|
+
input.className = 'key-input';
|
|
605
|
+
input.placeholder = 'fraim_…';
|
|
606
|
+
input.autocapitalize = 'off';
|
|
607
|
+
input.autocomplete = 'off';
|
|
608
|
+
input.spellcheck = false;
|
|
609
|
+
input.setAttribute('data-testid', 'key-input');
|
|
610
|
+
card.appendChild(input);
|
|
611
|
+
|
|
612
|
+
const err = document.createElement('p');
|
|
613
|
+
err.className = 'locked-note';
|
|
614
|
+
err.setAttribute('data-testid', 'key-error');
|
|
615
|
+
err.hidden = true;
|
|
616
|
+
card.appendChild(err);
|
|
617
|
+
|
|
618
|
+
const submit = button('Continue', 'primary');
|
|
619
|
+
submit.setAttribute('data-testid', 'key-submit');
|
|
620
|
+
const onSubmit = async () => {
|
|
621
|
+
const value = input.value.trim();
|
|
622
|
+
err.hidden = true;
|
|
623
|
+
submit.disabled = true;
|
|
624
|
+
submit.textContent = 'Checking…';
|
|
625
|
+
try {
|
|
626
|
+
const resp = await api('/api/first-run/set-key', 'POST', { key: value });
|
|
627
|
+
state.session = resp.session;
|
|
628
|
+
if (!state.session.state.agentInstalls) state.session.state.agentInstalls = {};
|
|
629
|
+
state.activeStep = chooseActiveStep();
|
|
630
|
+
render();
|
|
631
|
+
} catch (e) {
|
|
632
|
+
err.textContent = e.message || 'That key was not accepted. Copy it again from your account page.';
|
|
633
|
+
err.hidden = false;
|
|
634
|
+
submit.disabled = false;
|
|
635
|
+
submit.textContent = 'Continue';
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
submit.addEventListener('click', onSubmit);
|
|
639
|
+
input.addEventListener('keydown', (e) => { if (e.key === 'Enter') onSubmit(); });
|
|
640
|
+
card.appendChild(submit);
|
|
641
|
+
|
|
642
|
+
CHECKLIST_EL.appendChild(card);
|
|
643
|
+
input.focus();
|
|
644
|
+
}
|
|
645
|
+
|
|
583
646
|
function render() {
|
|
584
647
|
if (!state.session) return;
|
|
648
|
+
if (state.session.needsKey) { renderKeyEntry(); return; }
|
|
585
649
|
if (!STEP_ORDER.includes(state.activeStep)) state.activeStep = chooseActiveStep();
|
|
586
650
|
renderShell((pane) => {
|
|
587
651
|
if (state.activeStep === 'prereqs') renderPrereqs(pane);
|
|
@@ -494,6 +494,20 @@ body {
|
|
|
494
494
|
font-size: 14px;
|
|
495
495
|
}
|
|
496
496
|
.locked-note { color: var(--warn); }
|
|
497
|
+
|
|
498
|
+
/* Issue #646: paste-your-key step (no-terminal macOS installer path). */
|
|
499
|
+
.key-input {
|
|
500
|
+
width: 100%;
|
|
501
|
+
margin: 10px 0 4px;
|
|
502
|
+
padding: 10px 12px;
|
|
503
|
+
font-family: ui-monospace, 'SFMono-Regular', Menlo, monospace;
|
|
504
|
+
font-size: 14px;
|
|
505
|
+
border: 1px solid var(--border, #2c3343);
|
|
506
|
+
border-radius: 8px;
|
|
507
|
+
background: var(--panel, #12151c);
|
|
508
|
+
color: var(--text, #e6e9ef);
|
|
509
|
+
}
|
|
510
|
+
.key-input:focus-visible { outline: 2px solid var(--accent-strong, #6366f1); outline-offset: 1px; }
|
|
497
511
|
.row-list {
|
|
498
512
|
list-style: none;
|
|
499
513
|
margin: 0;
|