fraim-framework 2.0.176 → 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.
@@ -65,11 +65,9 @@ function openBrowser(url) {
65
65
  }
66
66
  }
67
67
  const runFirstRun = async (options) => {
68
- const key = options.key || process.env.FRAIM_API_KEY || process.env.FRAIM_SETUP_KEY || process.env.FRAIM_INSTALL_KEY;
69
- if (!key) {
70
- console.log(chalk_1.default.red('FRAIM key is required for first-run.'));
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://deepmind.google/technologies/gemini/',
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
- this.key = options.key;
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-framework",
3
- "version": "2.0.176",
3
+ "version": "2.0.177",
4
4
  "description": "FRAIM: AI Workforce Infrastructure — the organizational capability that turns AI agents into an accountable workforce, their operators into capable AI managers, and executives into leaders with clear optics on AI proficiency.",
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;