fraim 2.0.280 → 2.0.283

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.
Files changed (31) hide show
  1. package/README.md +467 -467
  2. package/bin/fraim.js +12 -12
  3. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +266 -182
  4. package/dist/src/cli/mcp/mcp-server-registry.js +11 -3
  5. package/dist/src/cli/setup/ide-invocation-surfaces.js +64 -64
  6. package/dist/src/cli/utils/agent-adapters.js +61 -61
  7. package/dist/src/core/ai-mentor.js +35 -0
  8. package/dist/src/core/handoff-contracts.js +73 -46
  9. package/dist/src/core/resolve-phase-edge.js +33 -0
  10. package/dist/src/core/utils/stub-generator.js +53 -53
  11. package/dist/src/first-run/server.js +5 -1
  12. package/dist/src/first-run/session-service.js +48 -12
  13. package/dist/src/fraim/issues.js +4 -4
  14. package/dist/src/local-mcp-server/stdio-server.js +43 -4
  15. package/dist/src/mcp/tool-schemas.js +40 -40
  16. package/dist/src/middleware/telemetry.js +21 -21
  17. package/dist/src/services/email-service.js +623 -623
  18. package/dist/src/services/installer-service.js +22 -22
  19. package/index.js +83 -83
  20. package/package.json +59 -59
  21. package/public/first-run/error-frame.js +100 -100
  22. package/public/first-run/index.html +35 -35
  23. package/public/first-run/script.js +747 -742
  24. package/public/first-run/styles.css +929 -929
  25. package/dist/src/cli/commands/learning-usage.js +0 -412
  26. package/dist/src/cli/commands/test-mcp.js +0 -171
  27. package/dist/src/cli/setup/first-run.js +0 -242
  28. package/dist/src/core/config-writer.js +0 -75
  29. package/dist/src/core/utils/job-aliases.js +0 -47
  30. package/dist/src/core/utils/workflow-parser.js +0 -174
  31. package/dist/src/services/email-service-clean.js +0 -782
@@ -40,18 +40,18 @@ function getMarketingBaseUrl() {
40
40
  }
41
41
  function buildLayoutShell(marketingBaseUrl) {
42
42
  const m = marketingBaseUrl;
43
- return `<!DOCTYPE html>
44
- <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=5.0,user-scalable=yes">
45
- <script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
46
- <title>{{TITLE}}</title><meta name="theme-color" content="#6366f1">
47
- <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
48
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
49
- <link rel="stylesheet" href="${m}/css/styles.css"></head><body>
50
- <nav class="nav"><div class="nav-container"><div class="nav-logo"><a href="${m}/"><img src="${m}/images/fraim-logo.png" alt="FRAIM" class="logo-icon"/><span class="logo-text">FRAIM</span></a></div>
51
- <div class="nav-links"><a href="${m}/#features">Features</a><a href="${m}/#how-it-works">How it Works</a><a href="${m}/pricing.html">Pricing</a><a href="${m}/#results">Results</a>
52
- <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode"><span class="theme-icon">🌙</span></button>
53
- <a href="${m}/" class="nav-cta join-waitlist-btn">Get Started</a></div></div></nav>
54
- <main style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:80px 20px 40px">{{BODY}}</main>
43
+ return `<!DOCTYPE html>
44
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=5.0,user-scalable=yes">
45
+ <script>(function(){var t=localStorage.getItem('theme')||(window.matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light');document.documentElement.setAttribute('data-theme',t);})();</script>
46
+ <title>{{TITLE}}</title><meta name="theme-color" content="#6366f1">
47
+ <link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
48
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
49
+ <link rel="stylesheet" href="${m}/css/styles.css"></head><body>
50
+ <nav class="nav"><div class="nav-container"><div class="nav-logo"><a href="${m}/"><img src="${m}/images/fraim-logo.png" alt="FRAIM" class="logo-icon"/><span class="logo-text">FRAIM</span></a></div>
51
+ <div class="nav-links"><a href="${m}/#features">Features</a><a href="${m}/#how-it-works">How it Works</a><a href="${m}/pricing.html">Pricing</a><a href="${m}/#results">Results</a>
52
+ <button class="theme-toggle" id="theme-toggle" aria-label="Toggle dark mode"><span class="theme-icon">🌙</span></button>
53
+ <a href="${m}/" class="nav-cta join-waitlist-btn">Get Started</a></div></div></nav>
54
+ <main style="min-height:100vh;display:flex;align-items:center;justify-content:center;padding:80px 20px 40px">{{BODY}}</main>
55
55
  <script src="${m}/js/script.js"></script></body></html>`;
56
56
  }
57
57
  class InstallerService {
@@ -87,22 +87,22 @@ class InstallerService {
87
87
  * bytes through curl avoids the quarantine attribute entirely.
88
88
  */
89
89
  generateDashboardCard(_data, _baseUrl) {
90
- return `<div class="container" style="max-width:520px;width:100%">
91
- <div class="card" style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px;max-width:520px;width:100%;box-shadow:var(--shadow-lg);overflow:hidden">
92
- <div class="card-header" style="padding:24px;background:linear-gradient(135deg,#22c55e 0%,#16a34a 100%);color:#fff;text-align:center;border-radius:16px 16px 0 0">
93
- <div style="font-size:48px;margin-bottom:8px">&#x2705;</div><h2 style="font-size:20px;font-weight:700">Open Account to view your FRAIM key</h2><p style="font-size:14px;opacity:.9;margin-top:4px">Your key and install commands now live in Account.</p></div>
94
- <div style="padding:24px">
95
- <p style="font-size:14px;color:var(--text-muted);line-height:1.6;margin:0 0 20px">For security, FRAIM no longer displays API keys on this standalone page. Continue to Account, reveal the key when you need it, and generate fresh install commands there.</p>
96
- <a href="/#account" class="btn-primary" style="display:block;padding:14px 20px;border-radius:10px;font-size:14px;font-weight:600;text-align:center;text-decoration:none">Open Account</a>
90
+ return `<div class="container" style="max-width:520px;width:100%">
91
+ <div class="card" style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px;max-width:520px;width:100%;box-shadow:var(--shadow-lg);overflow:hidden">
92
+ <div class="card-header" style="padding:24px;background:linear-gradient(135deg,#22c55e 0%,#16a34a 100%);color:#fff;text-align:center;border-radius:16px 16px 0 0">
93
+ <div style="font-size:48px;margin-bottom:8px">&#x2705;</div><h2 style="font-size:20px;font-weight:700">Open Account to view your FRAIM key</h2><p style="font-size:14px;opacity:.9;margin-top:4px">Your key and install commands now live in Account.</p></div>
94
+ <div style="padding:24px">
95
+ <p style="font-size:14px;color:var(--text-muted);line-height:1.6;margin:0 0 20px">For security, FRAIM no longer displays API keys on this standalone page. Continue to Account, reveal the key when you need it, and generate fresh install commands there.</p>
96
+ <a href="/#account" class="btn-primary" style="display:block;padding:14px 20px;border-radius:10px;font-size:14px;font-weight:600;text-align:center;text-decoration:none">Open Account</a>
97
97
  </div></div></div>`;
98
98
  }
99
99
  /**
100
100
  * Generate an error page card (missing token, invalid token, etc.).
101
101
  */
102
102
  generateErrorCard(title, message, linkText, linkHref) {
103
- return `<div class="container" style="text-align:center;max-width:400px">
104
- <div class="card" style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px;padding:32px">
105
- <h1 style="color:var(--error-color);margin-bottom:12px">${title}</h1><p style="color:var(--text-secondary)">${message}</p>
103
+ return `<div class="container" style="text-align:center;max-width:400px">
104
+ <div class="card" style="background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:16px;padding:32px">
105
+ <h1 style="color:var(--error-color);margin-bottom:12px">${title}</h1><p style="color:var(--text-secondary)">${message}</p>
106
106
  <p style="margin-top:16px"><a href="${linkHref}" class="btn-primary">${linkText}</a></p></div></div>`;
107
107
  }
108
108
  /**
package/index.js CHANGED
@@ -1,85 +1,85 @@
1
1
  #!/usr/bin/env node
2
-
3
- /**
4
- * FRAIM Framework - Smart Entry Point
5
- * This file handles both production (dist/) and development (src/) environments.
6
- */
7
-
8
- const path = require('path');
9
- const fs = require('fs');
10
- const { spawnSync } = require('child_process');
11
-
12
- /**
13
- * Runs the CLI using either the compiled JS or the source TS via tsx
14
- */
15
- function runCLI() {
16
- const distPath = path.join(__dirname, 'dist', 'src', 'cli', 'fraim.js');
17
- const srcPath = path.join(__dirname, 'src', 'cli', 'fraim.ts');
18
-
19
- // 1. Check if we have a compiled version (Production / CI)
20
- if (fs.existsSync(distPath)) {
21
- require(distPath);
22
- return;
23
- }
24
-
25
- // 2. Explicitly fail in production if dist/ is missing
26
- if (process.env.NODE_ENV === 'production') {
27
- console.error('❌ FRAIM Error: Production build (dist/) not found.');
28
- console.error('In production environments, you must run the compiled version.');
29
- console.error(`Expected: ${distPath}`);
30
- process.exit(1);
31
- }
32
-
33
- // 3. Fallback to source version using tsx (Development)
34
- if (fs.existsSync(srcPath)) {
35
- // We use spawnSync to run tsx so we don't have to require it in memory
36
- // if it's not needed, and it handles the process arguments correctly.
37
- //
38
- // IMPORTANT FIX: Directory names with spaces and dashes (e.g., "FRAIM - Issue 166")
39
- // cause argument parsing issues on Windows when shell: true is used.
40
- // Without quoting, a path like "C:\...\FRAIM - Issue 166\src\cli\fraim.ts" gets
41
- // split into multiple arguments, with the dash interpreted as a command flag,
42
- // resulting in "error: unknown command '-'".
43
- //
44
- // Solution: On Windows with shell: true, quote paths containing spaces.
45
- // On Unix with shell: false, pass the path unquoted (spawnSync handles it correctly).
46
- const isWindows = process.platform === 'win32';
47
-
48
- // On Windows with shell, quote paths with spaces to prevent shell misinterpretation
49
- // On Unix without shell, pass path as-is (spawnSync handles spaces correctly)
50
- const processedSrcPath = (isWindows && srcPath.includes(' '))
51
- ? `"${srcPath}"`
52
- : srcPath;
53
-
54
- const result = spawnSync(
55
- 'npx',
56
- ['tsx', processedSrcPath, ...process.argv.slice(2)],
57
- {
58
- stdio: 'inherit',
59
- shell: isWindows, // Windows needs shell for npx, Unix doesn't
60
- windowsHide: true
61
- }
62
- );
63
- process.exit(result.status || 0);
64
- }
65
-
66
- console.error('❌ FRAIM Error: Could not find CLI entry point.');
67
- console.error('Expected one of:');
68
- console.error(` - ${distPath}`);
69
- console.error(` - ${srcPath}`);
70
- process.exit(1);
71
- }
72
-
73
- // Global programmatic exports
74
- module.exports = {
75
- FRAIM_INFO: {
76
- name: 'FRAIM',
77
- version: '2.0.98',
78
- repository: 'https://github.com/mathursrus/FRAIM'
79
- }
80
- };
81
-
82
- // If this file is run directly (via npx or global link), run the CLI
83
- if (require.main === module) {
84
- runCLI();
2
+
3
+ /**
4
+ * FRAIM Framework - Smart Entry Point
5
+ * This file handles both production (dist/) and development (src/) environments.
6
+ */
7
+
8
+ const path = require('path');
9
+ const fs = require('fs');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ /**
13
+ * Runs the CLI using either the compiled JS or the source TS via tsx
14
+ */
15
+ function runCLI() {
16
+ const distPath = path.join(__dirname, 'dist', 'src', 'cli', 'fraim.js');
17
+ const srcPath = path.join(__dirname, 'src', 'cli', 'fraim.ts');
18
+
19
+ // 1. Check if we have a compiled version (Production / CI)
20
+ if (fs.existsSync(distPath)) {
21
+ require(distPath);
22
+ return;
23
+ }
24
+
25
+ // 2. Explicitly fail in production if dist/ is missing
26
+ if (process.env.NODE_ENV === 'production') {
27
+ console.error('❌ FRAIM Error: Production build (dist/) not found.');
28
+ console.error('In production environments, you must run the compiled version.');
29
+ console.error(`Expected: ${distPath}`);
30
+ process.exit(1);
31
+ }
32
+
33
+ // 3. Fallback to source version using tsx (Development)
34
+ if (fs.existsSync(srcPath)) {
35
+ // We use spawnSync to run tsx so we don't have to require it in memory
36
+ // if it's not needed, and it handles the process arguments correctly.
37
+ //
38
+ // IMPORTANT FIX: Directory names with spaces and dashes (e.g., "FRAIM - Issue 166")
39
+ // cause argument parsing issues on Windows when shell: true is used.
40
+ // Without quoting, a path like "C:\...\FRAIM - Issue 166\src\cli\fraim.ts" gets
41
+ // split into multiple arguments, with the dash interpreted as a command flag,
42
+ // resulting in "error: unknown command '-'".
43
+ //
44
+ // Solution: On Windows with shell: true, quote paths containing spaces.
45
+ // On Unix with shell: false, pass the path unquoted (spawnSync handles it correctly).
46
+ const isWindows = process.platform === 'win32';
47
+
48
+ // On Windows with shell, quote paths with spaces to prevent shell misinterpretation
49
+ // On Unix without shell, pass path as-is (spawnSync handles spaces correctly)
50
+ const processedSrcPath = (isWindows && srcPath.includes(' '))
51
+ ? `"${srcPath}"`
52
+ : srcPath;
53
+
54
+ const result = spawnSync(
55
+ 'npx',
56
+ ['tsx', processedSrcPath, ...process.argv.slice(2)],
57
+ {
58
+ stdio: 'inherit',
59
+ shell: isWindows, // Windows needs shell for npx, Unix doesn't
60
+ windowsHide: true
61
+ }
62
+ );
63
+ process.exit(result.status || 0);
64
+ }
65
+
66
+ console.error('❌ FRAIM Error: Could not find CLI entry point.');
67
+ console.error('Expected one of:');
68
+ console.error(` - ${distPath}`);
69
+ console.error(` - ${srcPath}`);
70
+ process.exit(1);
71
+ }
72
+
73
+ // Global programmatic exports
74
+ module.exports = {
75
+ FRAIM_INFO: {
76
+ name: 'FRAIM',
77
+ version: '2.0.98',
78
+ repository: 'https://github.com/mathursrus/FRAIM'
79
+ }
80
+ };
81
+
82
+ // If this file is run directly (via npx or global link), run the CLI
83
+ if (require.main === module) {
84
+ runCLI();
85
85
  }
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "fraim",
3
- "version": "2.0.280",
4
- "description": "FRAIM core CLI and MCP package.",
5
- "main": "index.js",
6
- "bin": {
7
- "fraim": "bin/fraim.js"
8
- },
9
- "files": [
10
- "dist/src/local-mcp-server/",
11
- "dist/src/cli/",
12
- "!dist/src/cli/fraim-hub.js",
13
- "dist/src/first-run/",
14
- "dist/src/core/",
15
- "dist/src/config/",
16
- "dist/src/fraim/",
17
- "dist/src/db/",
18
- "dist/src/routes/",
19
- "dist/src/services/",
20
- "dist/src/api/",
21
- "dist/src/middleware/",
1
+ {
2
+ "name": "fraim",
3
+ "version": "2.0.283",
4
+ "description": "FRAIM core CLI and MCP package.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "fraim": "bin/fraim.js"
8
+ },
9
+ "files": [
10
+ "dist/src/local-mcp-server/",
11
+ "dist/src/cli/",
12
+ "!dist/src/cli/fraim-hub.js",
13
+ "dist/src/first-run/",
14
+ "dist/src/core/",
15
+ "dist/src/config/",
16
+ "dist/src/fraim/",
17
+ "dist/src/db/",
18
+ "dist/src/routes/",
19
+ "dist/src/services/",
20
+ "dist/src/api/",
21
+ "dist/src/middleware/",
22
22
  "dist/src/models/",
23
23
  "dist/src/mcp/",
24
24
  "dist/src/types/",
25
- "dist/src/utils/",
26
- "bin/fraim.js",
27
- "public/first-run/",
28
- "index.js",
29
- "README.md",
30
- "CHANGELOG.md",
31
- "LICENSE",
32
- "package.json"
33
- ],
34
- "publishConfig": {
35
- "access": "public"
36
- },
37
- "engines": {
38
- "node": ">=16.0.0"
39
- },
40
- "dependencies": {
41
- "@octokit/rest": "^22.0.1",
42
- "adm-zip": "^0.6.0",
43
- "axios": "^1.7.0",
44
- "chalk": "4.1.2",
45
- "commander": "^14.0.2",
46
- "cors": "^2.8.5",
47
- "dotenv": "^16.4.7",
48
- "express": "^5.2.1",
49
- "mongodb": "^7.0.0",
50
- "node-cron": "4.2.1",
51
- "node-edge-tts": "^1.2.10",
52
- "nodemailer": "^8.0.3",
53
- "prompts": "^2.4.2",
54
- "resend": "^6.9.3",
55
- "selfsigned": "^5.5.0",
56
- "semver": "^7.7.4",
57
- "stripe": "^20.3.1",
58
- "toml": "^3.0.0",
59
- "tree-kill": "^1.2.2",
60
- "xml2js": "^0.6.2"
61
- }
62
- }
25
+ "dist/src/utils/",
26
+ "bin/fraim.js",
27
+ "public/first-run/",
28
+ "index.js",
29
+ "README.md",
30
+ "CHANGELOG.md",
31
+ "LICENSE",
32
+ "package.json"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "engines": {
38
+ "node": ">=16.0.0"
39
+ },
40
+ "dependencies": {
41
+ "@octokit/rest": "^22.0.1",
42
+ "adm-zip": "^0.6.0",
43
+ "axios": "^1.7.0",
44
+ "chalk": "4.1.2",
45
+ "commander": "^14.0.2",
46
+ "cors": "^2.8.5",
47
+ "dotenv": "^16.4.7",
48
+ "express": "^5.2.1",
49
+ "mongodb": "^7.0.0",
50
+ "node-cron": "4.2.1",
51
+ "node-edge-tts": "^1.2.10",
52
+ "nodemailer": "^8.0.3",
53
+ "prompts": "^2.4.2",
54
+ "resend": "^6.9.3",
55
+ "selfsigned": "^5.5.0",
56
+ "semver": "^7.7.4",
57
+ "stripe": "^20.3.1",
58
+ "toml": "^3.0.0",
59
+ "tree-kill": "^1.2.2",
60
+ "xml2js": "^0.6.2"
61
+ }
62
+ }
@@ -1,100 +1,100 @@
1
- /**
2
- * R6.1 — unified error-frame renderer.
3
- *
4
- * Exposes a single function `renderErrorFrame(frame, onAction)` which returns
5
- * a DOM Node containing exactly:
6
- * - "What we tried" sentence
7
- * - "What happened" verbatim stderr (last 12 lines, with a Show-full toggle
8
- * that surfaces the rest on demand)
9
- * - Up to three actions in fixed order (Retry / Try alternative / Skip and continue)
10
- *
11
- * The same renderer must be lifted into public/ai-hub/ in the v2 follow-up
12
- * (issue #355, "Hub-side error-frame adoption") so bootstrap and Hub share
13
- * one frame across surfaces.
14
- */
15
- (function () {
16
- 'use strict';
17
-
18
- function escapeText(text) {
19
- if (typeof text !== 'string') return '';
20
- return text;
21
- }
22
-
23
- function truncateToLastLines(text, lineCount) {
24
- if (typeof text !== 'string') return { visible: '', hidden: '' };
25
- const lines = text.split(/\r?\n/);
26
- if (lines.length <= lineCount) {
27
- return { visible: text, hidden: '' };
28
- }
29
- const visible = lines.slice(-lineCount).join('\n');
30
- const hidden = lines.slice(0, -lineCount).join('\n');
31
- return { visible, hidden };
32
- }
33
-
34
- function renderErrorFrame(frame, onAction) {
35
- const root = document.createElement('div');
36
- root.className = 'error-frame';
37
- root.setAttribute('data-testid', 'error-frame');
38
- root.setAttribute('role', 'alert');
39
-
40
- const whatTried = document.createElement('div');
41
- whatTried.className = 'what-tried';
42
- whatTried.setAttribute('data-testid', 'error-what-tried');
43
- whatTried.textContent = escapeText(frame.whatTried || '');
44
- root.appendChild(whatTried);
45
-
46
- // Optional plain-language hint rendered ABOVE the verbatim stderr so
47
- // a non-tech user reads "your install key was rejected — get a new
48
- // one" before they parse `Request failed with status code 401`.
49
- if (frame.hint) {
50
- const hint = document.createElement('div');
51
- hint.className = 'error-hint';
52
- hint.setAttribute('data-testid', 'error-hint');
53
- hint.textContent = escapeText(frame.hint);
54
- root.appendChild(hint);
55
- }
56
-
57
- const { visible, hidden } = truncateToLastLines(frame.whatHappened || '', 12);
58
- const whatHappened = document.createElement('pre');
59
- whatHappened.className = 'what-happened';
60
- whatHappened.setAttribute('data-testid', 'error-what-happened');
61
- whatHappened.textContent = visible;
62
- root.appendChild(whatHappened);
63
-
64
- if (hidden) {
65
- const showFull = document.createElement('button');
66
- showFull.type = 'button';
67
- showFull.className = 'show-full';
68
- showFull.setAttribute('data-testid', 'error-show-full');
69
- showFull.textContent = 'Show full output';
70
- showFull.addEventListener('click', () => {
71
- whatHappened.textContent = (hidden + '\n' + visible).trimEnd();
72
- showFull.remove();
73
- });
74
- root.appendChild(showFull);
75
- }
76
-
77
- const actions = Array.isArray(frame.actions) ? frame.actions.slice(0, 3) : [];
78
- const actionRow = document.createElement('div');
79
- actionRow.className = 'actions';
80
- for (const action of actions) {
81
- const btn = document.createElement('button');
82
- btn.type = 'button';
83
- btn.className = 'action';
84
- btn.setAttribute('data-testid', 'error-action');
85
- btn.setAttribute('data-action-id', action.id);
86
- btn.setAttribute('data-variant', action.variant || 'secondary');
87
- btn.textContent = action.label || action.id;
88
- btn.addEventListener('click', () => {
89
- if (typeof onAction === 'function') {
90
- onAction(action);
91
- }
92
- });
93
- actionRow.appendChild(btn);
94
- }
95
- root.appendChild(actionRow);
96
- return root;
97
- }
98
-
99
- window.FraimErrorFrame = { render: renderErrorFrame };
100
- })();
1
+ /**
2
+ * R6.1 — unified error-frame renderer.
3
+ *
4
+ * Exposes a single function `renderErrorFrame(frame, onAction)` which returns
5
+ * a DOM Node containing exactly:
6
+ * - "What we tried" sentence
7
+ * - "What happened" verbatim stderr (last 12 lines, with a Show-full toggle
8
+ * that surfaces the rest on demand)
9
+ * - Up to three actions in fixed order (Retry / Try alternative / Skip and continue)
10
+ *
11
+ * The same renderer must be lifted into public/ai-hub/ in the v2 follow-up
12
+ * (issue #355, "Hub-side error-frame adoption") so bootstrap and Hub share
13
+ * one frame across surfaces.
14
+ */
15
+ (function () {
16
+ 'use strict';
17
+
18
+ function escapeText(text) {
19
+ if (typeof text !== 'string') return '';
20
+ return text;
21
+ }
22
+
23
+ function truncateToLastLines(text, lineCount) {
24
+ if (typeof text !== 'string') return { visible: '', hidden: '' };
25
+ const lines = text.split(/\r?\n/);
26
+ if (lines.length <= lineCount) {
27
+ return { visible: text, hidden: '' };
28
+ }
29
+ const visible = lines.slice(-lineCount).join('\n');
30
+ const hidden = lines.slice(0, -lineCount).join('\n');
31
+ return { visible, hidden };
32
+ }
33
+
34
+ function renderErrorFrame(frame, onAction) {
35
+ const root = document.createElement('div');
36
+ root.className = 'error-frame';
37
+ root.setAttribute('data-testid', 'error-frame');
38
+ root.setAttribute('role', 'alert');
39
+
40
+ const whatTried = document.createElement('div');
41
+ whatTried.className = 'what-tried';
42
+ whatTried.setAttribute('data-testid', 'error-what-tried');
43
+ whatTried.textContent = escapeText(frame.whatTried || '');
44
+ root.appendChild(whatTried);
45
+
46
+ // Optional plain-language hint rendered ABOVE the verbatim stderr so
47
+ // a non-tech user reads "your install key was rejected — get a new
48
+ // one" before they parse `Request failed with status code 401`.
49
+ if (frame.hint) {
50
+ const hint = document.createElement('div');
51
+ hint.className = 'error-hint';
52
+ hint.setAttribute('data-testid', 'error-hint');
53
+ hint.textContent = escapeText(frame.hint);
54
+ root.appendChild(hint);
55
+ }
56
+
57
+ const { visible, hidden } = truncateToLastLines(frame.whatHappened || '', 12);
58
+ const whatHappened = document.createElement('pre');
59
+ whatHappened.className = 'what-happened';
60
+ whatHappened.setAttribute('data-testid', 'error-what-happened');
61
+ whatHappened.textContent = visible;
62
+ root.appendChild(whatHappened);
63
+
64
+ if (hidden) {
65
+ const showFull = document.createElement('button');
66
+ showFull.type = 'button';
67
+ showFull.className = 'show-full';
68
+ showFull.setAttribute('data-testid', 'error-show-full');
69
+ showFull.textContent = 'Show full output';
70
+ showFull.addEventListener('click', () => {
71
+ whatHappened.textContent = (hidden + '\n' + visible).trimEnd();
72
+ showFull.remove();
73
+ });
74
+ root.appendChild(showFull);
75
+ }
76
+
77
+ const actions = Array.isArray(frame.actions) ? frame.actions.slice(0, 3) : [];
78
+ const actionRow = document.createElement('div');
79
+ actionRow.className = 'actions';
80
+ for (const action of actions) {
81
+ const btn = document.createElement('button');
82
+ btn.type = 'button';
83
+ btn.className = 'action';
84
+ btn.setAttribute('data-testid', 'error-action');
85
+ btn.setAttribute('data-action-id', action.id);
86
+ btn.setAttribute('data-variant', action.variant || 'secondary');
87
+ btn.textContent = action.label || action.id;
88
+ btn.addEventListener('click', () => {
89
+ if (typeof onAction === 'function') {
90
+ onAction(action);
91
+ }
92
+ });
93
+ actionRow.appendChild(btn);
94
+ }
95
+ root.appendChild(actionRow);
96
+ return root;
97
+ }
98
+
99
+ window.FraimErrorFrame = { render: renderErrorFrame };
100
+ })();
@@ -1,35 +1,35 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="utf-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>FRAIM Setup</title>
7
- <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='10' fill='%230f2540'/%3E%3Ctext x='24' y='31' text-anchor='middle' font-family='Helvetica,Arial,sans-serif' font-size='22' font-weight='700' fill='%23f6efe4'%3EF%3C/text%3E%3C/svg%3E">
8
- <link rel="stylesheet" href="/first-run/styles.css">
9
- </head>
10
- <body>
11
- <main class="page">
12
- <header class="page-header">
13
- <h1>Set up FRAIM</h1>
14
- <p class="lede" id="lede">We are getting your machine ready for FRAIM.</p>
15
- </header>
16
-
17
- <section class="card">
18
- <ul class="checklist" id="checklist" data-testid="setup-checklist" aria-label="FRAIM setup checklist">
19
- <!-- Skeleton rows shown until the async session-load completes.
20
- Replaced wholesale by script.js once /api/first-run/session returns. -->
21
- <li class="row skeleton-row" data-row-id="node" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Node.js</span><span class="verb" data-testid="row-verb">checking...</span></li>
22
- <li class="row skeleton-row" data-row-id="git" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">git</span><span class="verb" data-testid="row-verb">checking...</span></li>
23
- <li class="row skeleton-row" data-row-id="agent" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Locally installed AI agents</span><span class="verb" data-testid="row-verb">checking...</span></li>
24
- <li class="row skeleton-row" data-row-id="fraim" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">FRAIM</span><span class="verb" data-testid="row-verb">checking...</span></li>
25
- </ul>
26
- <div class="actions">
27
- <button id="primary-button" class="primary-button" data-testid="primary-button">Set up FRAIM</button>
28
- </div>
29
- <p class="status" id="status" role="status" aria-live="polite"></p>
30
- </section>
31
- </main>
32
- <script src="/first-run/error-frame.js"></script>
33
- <script src="/first-run/script.js"></script>
34
- </body>
35
- </html>
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>FRAIM Setup</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Crect width='48' height='48' rx='10' fill='%230f2540'/%3E%3Ctext x='24' y='31' text-anchor='middle' font-family='Helvetica,Arial,sans-serif' font-size='22' font-weight='700' fill='%23f6efe4'%3EF%3C/text%3E%3C/svg%3E">
8
+ <link rel="stylesheet" href="/first-run/styles.css">
9
+ </head>
10
+ <body>
11
+ <main class="page">
12
+ <header class="page-header">
13
+ <h1>Set up FRAIM</h1>
14
+ <p class="lede" id="lede">We are getting your machine ready for FRAIM.</p>
15
+ </header>
16
+
17
+ <section class="card">
18
+ <ul class="checklist" id="checklist" data-testid="setup-checklist" aria-label="FRAIM setup checklist">
19
+ <!-- Skeleton rows shown until the async session-load completes.
20
+ Replaced wholesale by script.js once /api/first-run/session returns. -->
21
+ <li class="row skeleton-row" data-row-id="node" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Node.js</span><span class="verb" data-testid="row-verb">checking...</span></li>
22
+ <li class="row skeleton-row" data-row-id="git" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">git</span><span class="verb" data-testid="row-verb">checking...</span></li>
23
+ <li class="row skeleton-row" data-row-id="agent" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">Locally installed AI agents</span><span class="verb" data-testid="row-verb">checking...</span></li>
24
+ <li class="row skeleton-row" data-row-id="fraim" data-row-status="pending"><span class="icon" aria-hidden="true">.</span><span class="label">FRAIM</span><span class="verb" data-testid="row-verb">checking...</span></li>
25
+ </ul>
26
+ <div class="actions">
27
+ <button id="primary-button" class="primary-button" data-testid="primary-button">Set up FRAIM</button>
28
+ </div>
29
+ <p class="status" id="status" role="status" aria-live="polite"></p>
30
+ </section>
31
+ </main>
32
+ <script src="/first-run/error-frame.js"></script>
33
+ <script src="/first-run/script.js"></script>
34
+ </body>
35
+ </html>