fraim-framework 2.0.126 → 2.0.127

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 (33) hide show
  1. package/dist/src/ai-hub/catalog.js +280 -44
  2. package/dist/src/ai-hub/desktop-main.js +2 -2
  3. package/dist/src/ai-hub/hosts.js +384 -10
  4. package/dist/src/ai-hub/server.js +255 -9
  5. package/dist/src/cli/commands/add-ide.js +4 -3
  6. package/dist/src/cli/commands/first-run.js +61 -0
  7. package/dist/src/cli/commands/hub.js +4 -4
  8. package/dist/src/cli/commands/init-project.js +4 -4
  9. package/dist/src/cli/commands/setup.js +4 -3
  10. package/dist/src/cli/commands/sync.js +21 -2
  11. package/dist/src/cli/doctor/checks/ide-config-checks.js +20 -2
  12. package/dist/src/cli/fraim.js +2 -0
  13. package/dist/src/cli/mcp/ide-formats.js +29 -1
  14. package/dist/src/cli/mcp/mcp-server-registry.js +1 -0
  15. package/dist/src/cli/setup/auto-mcp-setup.js +14 -8
  16. package/dist/src/cli/setup/ide-detector.js +32 -1
  17. package/dist/src/cli/setup/ide-global-integration.js +5 -1
  18. package/dist/src/cli/setup/ide-invocation-surfaces.js +14 -0
  19. package/dist/src/cli/setup/mcp-config-generator.js +12 -1
  20. package/dist/src/cli/utils/agent-adapters.js +10 -0
  21. package/dist/src/core/utils/git-utils.js +14 -6
  22. package/dist/src/first-run/install-state.js +68 -0
  23. package/dist/src/first-run/server.js +153 -0
  24. package/dist/src/first-run/session-service.js +302 -0
  25. package/dist/src/first-run/types.js +40 -0
  26. package/dist/src/local-mcp-server/otlp-metrics-receiver.js +7 -1
  27. package/dist/src/local-mcp-server/stdio-server.js +41 -9
  28. package/package.json +3 -1
  29. package/public/ai-hub/index.html +149 -102
  30. package/public/ai-hub/script.js +1154 -271
  31. package/public/ai-hub/styles.css +753 -450
  32. package/public/first-run/index.html +221 -0
  33. package/public/first-run/script.js +361 -0
@@ -2,7 +2,7 @@
2
2
  // IDE Format Adapters - transform logical server structure to IDE-specific formats
3
3
  // Uses the centralized registry to determine server types
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
- exports.IDE_FORMATS = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
5
+ exports.IDE_FORMATS = exports.CodexFormat = exports.WindsurfFormat = exports.ClaudeCodeFormat = exports.ClaudeFormat = exports.GeminiCliFormat = exports.VSCodeFormat = exports.KiroFormat = exports.StandardFormat = void 0;
6
6
  exports.getIDEFormat = getIDEFormat;
7
7
  const mcp_server_registry_1 = require("./mcp-server-registry");
8
8
  const provider_registry_1 = require("../providers/provider-registry");
@@ -93,6 +93,33 @@ class VSCodeFormat {
93
93
  }
94
94
  }
95
95
  exports.VSCodeFormat = VSCodeFormat;
96
+ class GeminiCliFormat {
97
+ constructor() {
98
+ this.name = 'gemini-cli';
99
+ }
100
+ transform(servers) {
101
+ const mcpServers = {};
102
+ for (const [key, server] of servers) {
103
+ if (server.url) {
104
+ mcpServers[key] = {
105
+ ...(server.type === 'sse'
106
+ ? { url: server.url }
107
+ : { httpUrl: server.httpUrl || server.url }),
108
+ ...(server.headers && { headers: server.headers })
109
+ };
110
+ }
111
+ else {
112
+ mcpServers[key] = {
113
+ command: server.command,
114
+ ...(server.args && { args: server.args }),
115
+ ...(server.env && { env: server.env })
116
+ };
117
+ }
118
+ }
119
+ return { mcpServers };
120
+ }
121
+ }
122
+ exports.GeminiCliFormat = GeminiCliFormat;
96
123
  // Claude Desktop format (excludes provider servers - Issue #132)
97
124
  class ClaudeFormat {
98
125
  constructor() {
@@ -230,6 +257,7 @@ exports.IDE_FORMATS = {
230
257
  standard: new StandardFormat(),
231
258
  kiro: new KiroFormat(),
232
259
  vscode: new VSCodeFormat(),
260
+ 'gemini-cli': new GeminiCliFormat(),
233
261
  claude: new ClaudeFormat(),
234
262
  'claude-code': new ClaudeCodeFormat(),
235
263
  windsurf: new WindsurfFormat(),
@@ -85,6 +85,7 @@ function buildHTTPServer(mcpConfig, token) {
85
85
  }
86
86
  const authHeader = mcpConfig.authHeaderTemplate?.replace('{token}', token) || `Bearer ${token}`;
87
87
  return {
88
+ type: 'http',
88
89
  url: mcpConfig.url,
89
90
  headers: {
90
91
  Authorization: authHeader
@@ -252,9 +252,9 @@ const configureIDEMCP = async (ide, fraimKey, tokenInput, providerConfigs) => {
252
252
  const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConfigs) => {
253
253
  const tokens = normalizePlatformTokens(tokenInput);
254
254
  const detectedIDEs = (0, ide_detector_1.detectInstalledIDEs)();
255
- if (detectedIDEs.length === 0) {
255
+ if (detectedIDEs.length === 0 && (!selectedIDEs || selectedIDEs.length === 0)) {
256
256
  console.log(chalk_1.default.yellow('⚠️ No supported IDEs detected.'));
257
- console.log(chalk_1.default.gray('Supported IDEs: Claude, Antigravity, Kiro, Cursor, VSCode, Codex, Windsurf'));
257
+ console.log(chalk_1.default.gray('Supported IDEs: Claude, Claude Code, Antigravity, Gemini CLI, Kiro, Cursor, VSCode, Codex, Windsurf'));
258
258
  console.log(chalk_1.default.blue('\n💡 You can install an IDE and run setup again later.'));
259
259
  console.log(chalk_1.default.gray(' Or continue with manual MCP configuration.'));
260
260
  if (process.env.FRAIM_NON_INTERACTIVE) {
@@ -275,13 +275,19 @@ const autoConfigureMCP = async (fraimKey, tokenInput, selectedIDEs, providerConf
275
275
  }
276
276
  let idesToConfigure;
277
277
  if (selectedIDEs && selectedIDEs.length > 0) {
278
- // Use command line specified IDEs
279
- idesToConfigure = detectedIDEs.filter(ide => selectedIDEs.some(selected => ide.name.toLowerCase().includes(selected.toLowerCase())));
278
+ idesToConfigure = detectedIDEs.filter((ide) => selectedIDEs.some((selected) => ide.name.toLowerCase().includes(selected.toLowerCase())));
280
279
  if (idesToConfigure.length === 0) {
281
- console.log(chalk_1.default.yellow(`⚠️ No IDEs found matching: ${selectedIDEs.join(', ')}`));
282
- console.log(chalk_1.default.gray('Available IDEs:'));
283
- detectedIDEs.forEach(ide => console.log(chalk_1.default.gray(` • ${ide.name}`)));
284
- return;
280
+ idesToConfigure = (0, ide_detector_1.getAllSupportedIDEs)().filter((ide) => selectedIDEs.some((selected) => {
281
+ const normalized = selected.toLowerCase();
282
+ return ide.name.toLowerCase().includes(normalized)
283
+ || ide.aliases?.some((alias) => alias.includes(normalized));
284
+ }));
285
+ if (idesToConfigure.length === 0) {
286
+ console.log(chalk_1.default.yellow(`⚠️ No IDEs found matching: ${selectedIDEs.join(', ')}`));
287
+ console.log(chalk_1.default.gray('Available IDEs:'));
288
+ detectedIDEs.forEach((ide) => console.log(chalk_1.default.gray(` • ${ide.name}`)));
289
+ return;
290
+ }
285
291
  }
286
292
  }
287
293
  else {
@@ -7,6 +7,7 @@ exports.expandPath = exports.findIDEByName = exports.getAllSupportedIDEs = expor
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const os_1 = __importDefault(require("os"));
10
+ const child_process_1 = require("child_process");
10
11
  const expandPath = (filePath) => {
11
12
  if (filePath.startsWith('~/')) {
12
13
  return path_1.default.join(os_1.default.homedir(), filePath.slice(2));
@@ -17,6 +18,12 @@ exports.expandPath = expandPath;
17
18
  const checkMultiplePaths = (paths) => {
18
19
  return paths.some(p => fs_1.default.existsSync(expandPath(p)));
19
20
  };
21
+ const availableByVersionProbe = (command) => {
22
+ const result = process.platform === 'win32'
23
+ ? (0, child_process_1.spawnSync)('cmd.exe', ['/d', '/s', '/c', `${command} --version`], { encoding: 'utf8', timeout: 1500 })
24
+ : (0, child_process_1.spawnSync)(command, ['--version'], { encoding: 'utf8', timeout: 1500 });
25
+ return result.status === 0;
26
+ };
20
27
  const detectClaude = () => {
21
28
  const paths = [
22
29
  '~/.claude.json',
@@ -54,6 +61,15 @@ const detectWindsurf = () => {
54
61
  ];
55
62
  return checkMultiplePaths(paths);
56
63
  };
64
+ const detectGeminiCli = () => {
65
+ const paths = [
66
+ '~/.gemini/settings.json',
67
+ '~/.gemini',
68
+ '~/AppData/Roaming/gemini/settings.json',
69
+ '~/.config/gemini/settings.json'
70
+ ];
71
+ return checkMultiplePaths(paths) || availableByVersionProbe('gemini');
72
+ };
57
73
  exports.IDE_CONFIGS = [
58
74
  {
59
75
  name: 'Claude Code',
@@ -90,6 +106,21 @@ exports.IDE_CONFIGS = [
90
106
  detectMethod: () => fs_1.default.existsSync(expandPath('~/.gemini/antigravity')),
91
107
  description: 'Google Gemini Antigravity IDE'
92
108
  },
109
+ {
110
+ name: 'Gemini CLI',
111
+ configPath: '~/.gemini/settings.json',
112
+ configFormat: 'json',
113
+ configType: 'gemini-cli',
114
+ invocationProfile: 'gemini-command',
115
+ detectMethod: detectGeminiCli,
116
+ supportsConfigBootstrap: true,
117
+ aliases: ['gemini', 'gemini-cli', 'gemini cli'],
118
+ alternativePaths: [
119
+ '~/AppData/Roaming/gemini/settings.json',
120
+ '~/.config/gemini/settings.json'
121
+ ],
122
+ description: 'Google Gemini CLI local settings'
123
+ },
93
124
  {
94
125
  name: 'Kiro',
95
126
  configPath: '~/.kiro/settings/mcp.json',
@@ -186,6 +217,6 @@ const findIDEByName = (name) => {
186
217
  const normalized = name.toLowerCase();
187
218
  return exports.IDE_CONFIGS.find(ide => ide.name.toLowerCase().includes(normalized) ||
188
219
  normalized.includes(ide.name.toLowerCase()) ||
189
- ide.aliases?.some(alias => alias.includes(normalized) || normalized.includes(alias)));
220
+ ide.aliases?.some(alias => alias.toLowerCase().includes(normalized) || normalized.includes(alias.toLowerCase())));
190
221
  };
191
222
  exports.findIDEByName = findIDEByName;
@@ -36,7 +36,7 @@ async function installSlashCommands(homeDir) {
36
36
  }
37
37
  /**
38
38
  * Install FRAIM invocation artifacts for non-Claude IDEs.
39
- * Supports: Cursor, Codex, Windsurf, Kiro
39
+ * Supports: Cursor, Codex, Gemini CLI, Windsurf, Kiro
40
40
  * Does not overwrite existing files.
41
41
  */
42
42
  async function installGlobalRules(homeDir) {
@@ -49,6 +49,10 @@ async function installGlobalRules(homeDir) {
49
49
  if (fs_1.default.existsSync(codexDir)) {
50
50
  installFileIfMissing(path_1.default.join(codexDir, 'skills', 'fraim', 'SKILL.md'), (0, ide_invocation_surfaces_1.buildCodexSkillContent)(), 'Codex FRAIM skill (~/.codex/skills/fraim/SKILL.md)');
51
51
  }
52
+ const geminiDir = path_1.default.join(home, '.gemini');
53
+ if (fs_1.default.existsSync(geminiDir)) {
54
+ installFileIfMissing(path_1.default.join(geminiDir, 'commands', 'fraim.toml'), (0, ide_invocation_surfaces_1.buildGeminiCommandContent)(), 'Gemini CLI FRAIM command (~/.gemini/commands/fraim.toml)');
55
+ }
52
56
  const windsurfDir = path_1.default.join(home, '.codeium', 'windsurf');
53
57
  if (fs_1.default.existsSync(windsurfDir)) {
54
58
  installFileIfMissing(path_1.default.join(windsurfDir, 'commands', 'fraim.md'), (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)(), 'Windsurf FRAIM command (~/.codeium/windsurf/commands/fraim.md)');
@@ -9,6 +9,7 @@ exports.buildCursorMentionRuleContent = buildCursorMentionRuleContent;
9
9
  exports.buildCodexSkillContent = buildCodexSkillContent;
10
10
  exports.buildWindsurfCommandContent = buildWindsurfCommandContent;
11
11
  exports.buildKiroCommandContent = buildKiroCommandContent;
12
+ exports.buildGeminiCommandContent = buildGeminiCommandContent;
12
13
  exports.describeInvocationSurface = describeInvocationSurface;
13
14
  exports.FRAIM_LAUNCH_PHRASE = 'Use FRAIM for <job or task>';
14
15
  exports.CURSOR_MDC_FRONTMATTER = `---
@@ -107,6 +108,17 @@ function buildKiroCommandContent() {
107
108
 
108
109
  ${buildFraimInvocationBody('generic-tool-discovery')}`;
109
110
  }
111
+ function escapeTomlMultiline(value) {
112
+ return value.replace(/"""/g, '\\"""');
113
+ }
114
+ function buildGeminiCommandContent() {
115
+ return `description = "Discover and execute FRAIM jobs and skills"
116
+ prompt = """
117
+ # FRAIM
118
+
119
+ ${escapeTomlMultiline(buildFraimInvocationBody('generic-tool-discovery'))}
120
+ """`;
121
+ }
110
122
  function describeInvocationSurface(ideName, invocationProfile) {
111
123
  switch (invocationProfile) {
112
124
  case 'claude-slash':
@@ -125,6 +137,8 @@ function describeInvocationSurface(ideName, invocationProfile) {
125
137
  return `${ideName}: /fraim`;
126
138
  case 'kiro-hashtag':
127
139
  return `${ideName}: #fraim`;
140
+ case 'gemini-command':
141
+ return `${ideName}: /fraim`;
128
142
  case 'instructions-only':
129
143
  default:
130
144
  return `${ideName}: natural language`;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.generateMCPConfig = exports.generateWindsurfMCPServers = exports.generateVSCodeMCPServers = exports.generateCodexMCPServers = exports.generateKiroMCPServers = exports.generateClaudeCodeMCPServers = exports.generateClaudeMCPServers = exports.generateStandardMCPServers = exports.mergeTomlMCPServers = exports.extractTomlMcpServerBlock = void 0;
3
+ exports.generateMCPConfig = exports.generateWindsurfMCPServers = exports.generateGeminiCliMCPServers = exports.generateVSCodeMCPServers = exports.generateCodexMCPServers = exports.generateKiroMCPServers = exports.generateClaudeCodeMCPServers = exports.generateClaudeMCPServers = exports.generateStandardMCPServers = exports.mergeTomlMCPServers = exports.extractTomlMcpServerBlock = void 0;
4
4
  const mcp_server_builder_1 = require("../mcp/mcp-server-builder");
5
5
  const ide_formats_1 = require("../mcp/ide-formats");
6
6
  const normalizeTokens = (tokenInput) => {
@@ -160,6 +160,15 @@ const generateVSCodeMCPServers = async (fraimKey, tokenInput, providerConfigs) =
160
160
  return format.transform(builder.getServers());
161
161
  };
162
162
  exports.generateVSCodeMCPServers = generateVSCodeMCPServers;
163
+ const generateGeminiCliMCPServers = async (fraimKey, tokenInput, providerConfigs) => {
164
+ const tokens = normalizeTokens(tokenInput);
165
+ const builder = new mcp_server_builder_1.MCPServerBuilder();
166
+ builder.addBaseServers(fraimKey);
167
+ await addProviderServers(builder, tokens, providerConfigs);
168
+ const format = (0, ide_formats_1.getIDEFormat)('gemini-cli');
169
+ return format.transform(builder.getServers());
170
+ };
171
+ exports.generateGeminiCliMCPServers = generateGeminiCliMCPServers;
163
172
  const generateWindsurfMCPServers = async (fraimKey, tokenInput, providerConfigs) => {
164
173
  const tokens = normalizeTokens(tokenInput);
165
174
  const builder = new mcp_server_builder_1.MCPServerBuilder();
@@ -181,6 +190,8 @@ const generateMCPConfig = async (configType, fraimKey, tokenInput, providerConfi
181
190
  return await (0, exports.generateKiroMCPServers)(fraimKey, tokenInput, providerConfigs);
182
191
  case 'vscode':
183
192
  return await (0, exports.generateVSCodeMCPServers)(fraimKey, tokenInput, providerConfigs);
193
+ case 'gemini-cli':
194
+ return await (0, exports.generateGeminiCliMCPServers)(fraimKey, tokenInput, providerConfigs);
184
195
  case 'codex':
185
196
  return await (0, exports.generateCodexMCPServers)(fraimKey, tokenInput, providerConfigs);
186
197
  case 'windsurf':
@@ -15,6 +15,8 @@ const CLAUDE_FRAIM_COMMAND_PATH = path_1.default.join('.claude', 'commands', 'fr
15
15
  const CLAUDE_FRAIM_SKILL_PATH = path_1.default.join('.claude', 'skills', 'fraim', 'SKILL.md');
16
16
  const VSCODE_FRAIM_PROMPT_PATH = path_1.default.join('.github', 'prompts', 'fraim.prompt.md');
17
17
  const CODEX_FRAIM_SKILL_PATH = path_1.default.join('.codex', 'skills', 'fraim', 'SKILL.md');
18
+ const GEMINI_FRAIM_COMMAND_PATH = path_1.default.join('.gemini', 'commands', 'fraim.toml');
19
+ const GEMINI_PROJECT_INSTRUCTIONS_PATH = path_1.default.join('.gemini', 'GEMINI.md');
18
20
  const WINDSURF_FRAIM_COMMAND_PATH = path_1.default.join('.windsurf', 'commands', 'fraim.md');
19
21
  const KIRO_FRAIM_COMMAND_PATH = path_1.default.join('.kiro', 'commands', 'fraim.md');
20
22
  function buildManagedSection(body) {
@@ -102,6 +104,10 @@ Use the stubs here to discover which FRAIM job, skill, or rule is relevant, then
102
104
  const vscodePrompt = `# FRAIM
103
105
 
104
106
  ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discovery')}`;
107
+ const geminiProjectInstructions = `# Gemini Project Instructions
108
+
109
+ @../AGENTS.md
110
+ `;
105
111
  return [
106
112
  { path: 'AGENTS.md', content: markdownBody },
107
113
  { path: 'CLAUDE.md', content: markdownBody },
@@ -112,6 +118,8 @@ ${(0, ide_invocation_surfaces_1.buildFraimInvocationBody)('generic-tool-discover
112
118
  { path: CLAUDE_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeSkillContent)() },
113
119
  { path: CLAUDE_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildClaudeCommandShimContent)() },
114
120
  { path: CODEX_FRAIM_SKILL_PATH, content: (0, ide_invocation_surfaces_1.buildCodexSkillContent)() },
121
+ { path: GEMINI_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildGeminiCommandContent)() },
122
+ { path: GEMINI_PROJECT_INSTRUCTIONS_PATH, content: geminiProjectInstructions },
115
123
  { path: WINDSURF_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildWindsurfCommandContent)() },
116
124
  { path: KIRO_FRAIM_COMMAND_PATH, content: (0, ide_invocation_surfaces_1.buildKiroCommandContent)() }
117
125
  ];
@@ -132,6 +140,8 @@ function ensureAgentAdapterFiles(projectRoot) {
132
140
  || file.path === CLAUDE_FRAIM_SKILL_PATH
133
141
  || file.path === CLAUDE_FRAIM_COMMAND_PATH
134
142
  || file.path === CODEX_FRAIM_SKILL_PATH
143
+ || file.path === GEMINI_FRAIM_COMMAND_PATH
144
+ || file.path === GEMINI_PROJECT_INSTRUCTIONS_PATH
135
145
  || file.path === WINDSURF_FRAIM_COMMAND_PATH
136
146
  || file.path === KIRO_FRAIM_COMMAND_PATH
137
147
  ? file.content
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isExplicitLocalRepoPath = isExplicitLocalRepoPath;
3
4
  exports.getPort = getPort;
4
5
  exports.determineDatabaseName = determineDatabaseName;
5
6
  exports.getCurrentGitBranch = getCurrentGitBranch;
@@ -19,6 +20,18 @@ function extractLocalFolderLabel(rawPath) {
19
20
  const candidate = parts[parts.length - 1];
20
21
  return candidate || undefined;
21
22
  }
23
+ function isExplicitLocalRepoPath(repoUrl) {
24
+ if (!repoUrl || typeof repoUrl !== 'string') {
25
+ return false;
26
+ }
27
+ const trimmed = repoUrl.trim();
28
+ return (trimmed.startsWith('/') ||
29
+ trimmed.startsWith('\\') ||
30
+ /^[a-zA-Z]:[\\/]/.test(trimmed) ||
31
+ trimmed.startsWith('./') ||
32
+ trimmed.startsWith('../') ||
33
+ trimmed.startsWith('file://'));
34
+ }
22
35
  /**
23
36
  * Gets a unique port based on the current git branch name (if it's an issue branch)
24
37
  * Default to 15302 if not on an issue branch
@@ -119,12 +132,7 @@ function sanitizeRepoIdentifier(repoUrl) {
119
132
  }
120
133
  const trimmed = repoUrl.trim();
121
134
  // Convert local paths to just the folder name for privacy-safe workspace labeling.
122
- if (trimmed.startsWith('/') ||
123
- trimmed.startsWith('\\') ||
124
- /^[a-zA-Z]:[\\/]/.test(trimmed) ||
125
- trimmed.startsWith('./') ||
126
- trimmed.startsWith('../') ||
127
- trimmed.startsWith('file://')) {
135
+ if (isExplicitLocalRepoPath(trimmed)) {
128
136
  return extractLocalFolderLabel(trimmed);
129
137
  }
130
138
  // Normalize GitHub/GitLab SSH and HTTPS URLs
@@ -0,0 +1,68 @@
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.createInitialFirstRunState = createInitialFirstRunState;
7
+ exports.loadFirstRunState = loadFirstRunState;
8
+ exports.saveFirstRunState = saveFirstRunState;
9
+ exports.clearFirstRunState = clearFirstRunState;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const script_sync_utils_1 = require("../cli/utils/script-sync-utils");
13
+ const types_1 = require("./types");
14
+ function getStatePath() {
15
+ return path_1.default.join((0, script_sync_utils_1.getUserFraimDir)(), 'install-state.json');
16
+ }
17
+ function ensureUserDir() {
18
+ fs_1.default.mkdirSync((0, script_sync_utils_1.getUserFraimDir)(), { recursive: true });
19
+ }
20
+ function maskInstallKey(key) {
21
+ const normalized = key.trim();
22
+ if (normalized.length <= 8) {
23
+ return '<redacted>';
24
+ }
25
+ return `${normalized.slice(0, 4)}...${normalized.slice(-4)}`;
26
+ }
27
+ function createInitialFirstRunState(key) {
28
+ const now = new Date().toISOString();
29
+ return {
30
+ version: 1,
31
+ installKeyRef: maskInstallKey(key),
32
+ platform: process.platform,
33
+ detectedAgents: [],
34
+ configuredAgents: [],
35
+ restartDeferredAgents: [],
36
+ resourcesUrl: types_1.FIRST_RUN_RESOURCES_URL,
37
+ stepStates: (0, types_1.createDefaultStepStates)(),
38
+ createdAt: now,
39
+ updatedAt: now,
40
+ };
41
+ }
42
+ function loadFirstRunState() {
43
+ const statePath = getStatePath();
44
+ if (!fs_1.default.existsSync(statePath)) {
45
+ return null;
46
+ }
47
+ try {
48
+ const state = JSON.parse(fs_1.default.readFileSync(statePath, 'utf8'));
49
+ if (typeof state.installKeyRef === 'string' && !state.installKeyRef.includes('...')) {
50
+ state.installKeyRef = maskInstallKey(state.installKeyRef);
51
+ }
52
+ return state;
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
58
+ function saveFirstRunState(state) {
59
+ ensureUserDir();
60
+ state.updatedAt = new Date().toISOString();
61
+ fs_1.default.writeFileSync(getStatePath(), JSON.stringify(state, null, 2));
62
+ }
63
+ function clearFirstRunState() {
64
+ const statePath = getStatePath();
65
+ if (fs_1.default.existsSync(statePath)) {
66
+ fs_1.default.unlinkSync(statePath);
67
+ }
68
+ }
@@ -0,0 +1,153 @@
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.FirstRunServer = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const child_process_1 = require("child_process");
11
+ function resolveFirstRunPublicDir() {
12
+ const candidates = [
13
+ path_1.default.resolve(process.cwd(), 'public/first-run'),
14
+ path_1.default.resolve(__dirname, '..', '..', 'public/first-run'),
15
+ path_1.default.resolve(__dirname, '..', '..', '..', 'public/first-run'),
16
+ ];
17
+ for (const candidate of candidates) {
18
+ if (fs_1.default.existsSync(candidate)) {
19
+ return candidate;
20
+ }
21
+ }
22
+ throw new Error('Could not locate public/first-run assets.');
23
+ }
24
+ function pickProjectPath() {
25
+ if (process.platform === 'win32') {
26
+ const script = [
27
+ 'Add-Type -AssemblyName System.Windows.Forms',
28
+ '$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
29
+ '$dialog.ShowNewFolderButton = $true',
30
+ 'if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {',
31
+ ' Write-Output $dialog.SelectedPath',
32
+ '}',
33
+ ].join('; ');
34
+ const result = (0, child_process_1.spawnSync)('powershell', ['-NoProfile', '-Command', script], {
35
+ encoding: 'utf8',
36
+ });
37
+ return result.status === 0 ? result.stdout.trim() || null : null;
38
+ }
39
+ if (process.platform === 'darwin') {
40
+ const result = (0, child_process_1.spawnSync)('osascript', ['-e', 'POSIX path of (choose folder with prompt "Select a FRAIM project folder")'], {
41
+ encoding: 'utf8',
42
+ });
43
+ return result.status === 0 ? result.stdout.trim() || null : null;
44
+ }
45
+ const result = (0, child_process_1.spawnSync)('bash', ['-lc', 'zenity --file-selection --directory 2>/dev/null || kdialog --getexistingdirectory 2>/dev/null'], {
46
+ encoding: 'utf8',
47
+ });
48
+ return result.status === 0 ? result.stdout.trim() || null : null;
49
+ }
50
+ class FirstRunServer {
51
+ constructor(options) {
52
+ this.app = (0, express_1.default)();
53
+ this.sessionService = options.sessionService;
54
+ this.finishPromise = new Promise((resolve) => {
55
+ this.finishResolver = resolve;
56
+ });
57
+ this.app.use(express_1.default.json());
58
+ this.app.use('/api/first-run', (req, res, next) => {
59
+ if (req.method === 'GET') {
60
+ return next();
61
+ }
62
+ const requestToken = req.header('x-fraim-first-run-token');
63
+ if (requestToken !== this.sessionService.getRequestToken()) {
64
+ return res.status(403).json({ error: 'Invalid first-run session token.' });
65
+ }
66
+ return next();
67
+ });
68
+ this.app.use('/first-run', express_1.default.static(resolveFirstRunPublicDir()));
69
+ this.app.get('/health', (_req, res) => {
70
+ res.json({ status: 'ok', service: 'fraim-first-run' });
71
+ });
72
+ this.registerRoutes();
73
+ }
74
+ async start(port) {
75
+ await new Promise((resolve, reject) => {
76
+ this.httpServer = this.app.listen(port, '127.0.0.1');
77
+ this.httpServer.once('listening', () => resolve());
78
+ this.httpServer.once('error', (error) => reject(error));
79
+ });
80
+ }
81
+ async waitForFinish() {
82
+ await this.finishPromise;
83
+ }
84
+ async stop() {
85
+ if (!this.httpServer) {
86
+ return;
87
+ }
88
+ await new Promise((resolve, reject) => {
89
+ this.httpServer.close((error) => {
90
+ if (error) {
91
+ reject(error);
92
+ return;
93
+ }
94
+ resolve();
95
+ });
96
+ });
97
+ this.httpServer = undefined;
98
+ }
99
+ registerRoutes() {
100
+ this.app.get('/api/first-run/session', (_req, res) => {
101
+ res.json(this.sessionService.getSession());
102
+ });
103
+ this.app.post('/api/first-run/prereqs', (_req, res) => {
104
+ res.json(this.sessionService.runPrereqChecks());
105
+ });
106
+ this.app.post('/api/first-run/agents/select', (req, res) => {
107
+ if (!req.body.agentId) {
108
+ return res.status(400).json({ error: 'agentId is required.' });
109
+ }
110
+ return res.json(this.sessionService.selectAgent(req.body.agentId));
111
+ });
112
+ this.app.post('/api/first-run/configure', async (_req, res) => {
113
+ try {
114
+ return res.json(await this.sessionService.configureFraim());
115
+ }
116
+ catch (error) {
117
+ return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not configure FRAIM.' });
118
+ }
119
+ });
120
+ this.app.post('/api/first-run/project-path/pick', (_req, res) => {
121
+ try {
122
+ const selectedPath = pickProjectPath();
123
+ if (!selectedPath) {
124
+ return res.status(204).end();
125
+ }
126
+ return res.json({ path: selectedPath });
127
+ }
128
+ catch (error) {
129
+ return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open the folder picker.' });
130
+ }
131
+ });
132
+ this.app.post('/api/first-run/project', async (req, res) => {
133
+ if (!req.body.projectPath) {
134
+ return res.status(400).json({ error: 'projectPath is required.' });
135
+ }
136
+ try {
137
+ return res.json(await this.sessionService.initializeProject(req.body.projectPath, req.body.initializeGit !== false));
138
+ }
139
+ catch (error) {
140
+ return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not initialize the project.' });
141
+ }
142
+ });
143
+ this.app.post('/api/first-run/launch', (_req, res) => {
144
+ return res.json(this.sessionService.launchAndProbe());
145
+ });
146
+ this.app.post('/api/first-run/finish', (_req, res) => {
147
+ const result = this.sessionService.finish();
148
+ this.finishResolver?.();
149
+ return res.json(result);
150
+ });
151
+ }
152
+ }
153
+ exports.FirstRunServer = FirstRunServer;