fraim-framework 2.0.127 → 2.0.128

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.
@@ -160,11 +160,15 @@ const createGitHubLabels = (projectRoot) => {
160
160
  };
161
161
  const runInitProject = async (options = {}) => {
162
162
  console.log(chalk_1.default.blue('Initializing FRAIM project...'));
163
+ const failHard = options.failHard ?? 'exit';
163
164
  const globalSetup = checkGlobalSetup();
164
165
  if (!globalSetup.exists) {
165
166
  console.log(chalk_1.default.red('Global FRAIM setup not found.'));
166
167
  console.log(chalk_1.default.yellow('Please run global setup first:'));
167
168
  console.log(chalk_1.default.cyan(' fraim setup'));
169
+ if (failHard === 'throw') {
170
+ throw new Error('Global FRAIM setup not found.');
171
+ }
168
172
  process.exit(1);
169
173
  }
170
174
  const projectRoot = options.projectRoot || process.cwd();
@@ -312,7 +316,7 @@ const runInitProject = async (options = {}) => {
312
316
  result.repositoryDetected = true;
313
317
  }
314
318
  if (!process.env.FRAIM_SKIP_SYNC) {
315
- await (0, sync_1.runSync)({ projectRoot });
319
+ await (0, sync_1.runSync)({ projectRoot, failHard });
316
320
  result.syncPerformed = true;
317
321
  }
318
322
  else {
@@ -101,7 +101,14 @@ function updateVersionInConfig(fraimDir) {
101
101
  console.warn(chalk_1.default.yellow('Could not update version in config.json.'));
102
102
  }
103
103
  }
104
+ function failSync(mode, message) {
105
+ if (mode === 'throw') {
106
+ throw new Error(message);
107
+ }
108
+ process.exit(1);
109
+ }
104
110
  const runSync = async (options) => {
111
+ const failHard = options.failHard ?? 'exit';
105
112
  // Handle --global flag: sync to user-level ~/.fraim/ instead of project
106
113
  if (options.global) {
107
114
  console.log(chalk_1.default.blue('Syncing FRAIM content to user-level directory (~/.fraim/)...'));
@@ -112,7 +119,7 @@ const runSync = async (options) => {
112
119
  }
113
120
  catch (error) {
114
121
  console.error(chalk_1.default.red(`User-level sync failed: ${error.message}`));
115
- process.exit(1);
122
+ failSync(failHard, `User-level sync failed: ${error.message}`);
116
123
  }
117
124
  return;
118
125
  }
@@ -158,7 +165,7 @@ const runSync = async (options) => {
158
165
  }
159
166
  console.error(chalk_1.default.red(`Local sync failed: ${result.error}`));
160
167
  console.error(chalk_1.default.yellow('Make sure the FRAIM MCP server is running locally (npm run dev).'));
161
- process.exit(1);
168
+ failSync(failHard, `Local sync failed: ${result.error}`);
162
169
  }
163
170
  let apiKey = loadUserApiKey() || config.apiKey || process.env.FRAIM_API_KEY;
164
171
  if (!apiKey) {
@@ -170,7 +177,7 @@ const runSync = async (options) => {
170
177
  console.error(chalk_1.default.red('No API key configured. Cannot sync.'));
171
178
  console.error(chalk_1.default.yellow(`Set FRAIM_API_KEY in your environment, or add apiKey to ~/.fraim/config.json or ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')}`));
172
179
  console.error(chalk_1.default.yellow('Or use --local to sync from a locally running FRAIM server.'));
173
- process.exit(1);
180
+ failSync(failHard, 'No API key configured.');
174
181
  }
175
182
  }
176
183
  console.log(chalk_1.default.blue('Syncing FRAIM jobs from remote server...'));
@@ -188,7 +195,7 @@ const runSync = async (options) => {
188
195
  updateVersionInConfig(fraimDir);
189
196
  return;
190
197
  }
191
- process.exit(1);
198
+ failSync(failHard, `Remote sync failed: ${result.error}`);
192
199
  }
193
200
  console.log(chalk_1.default.green(`Successfully synced ${result.employeeJobsSynced} ai-employee jobs, ${result.managerJobsSynced} ai-manager jobs, ${result.skillsSynced} skills, ${result.rulesSynced} rules, ${result.scriptsSynced} scripts, and ${result.docsSynced} docs from remote`));
194
201
  updateVersionInConfig(fraimDir);
@@ -27,14 +27,12 @@ function maskInstallKey(key) {
27
27
  function createInitialFirstRunState(key) {
28
28
  const now = new Date().toISOString();
29
29
  return {
30
- version: 1,
30
+ version: 2,
31
31
  installKeyRef: maskInstallKey(key),
32
32
  platform: process.platform,
33
- detectedAgents: [],
34
- configuredAgents: [],
35
- restartDeferredAgents: [],
33
+ agentId: 'claude-code',
34
+ rows: (0, types_1.createInitialRows)(),
36
35
  resourcesUrl: types_1.FIRST_RUN_RESOURCES_URL,
37
- stepStates: (0, types_1.createDefaultStepStates)(),
38
36
  createdAt: now,
39
37
  updatedAt: now,
40
38
  };
@@ -46,6 +44,10 @@ function loadFirstRunState() {
46
44
  }
47
45
  try {
48
46
  const state = JSON.parse(fs_1.default.readFileSync(statePath, 'utf8'));
47
+ // Reject persisted v1 state — schema changed materially in #352 v1.
48
+ if (state.version !== 2) {
49
+ return null;
50
+ }
49
51
  if (typeof state.installKeyRef === 'string' && !state.installKeyRef.includes('...')) {
50
52
  state.installKeyRef = maskInstallKey(state.installKeyRef);
51
53
  }
@@ -8,6 +8,7 @@ const express_1 = __importDefault(require("express"));
8
8
  const fs_1 = __importDefault(require("fs"));
9
9
  const path_1 = __importDefault(require("path"));
10
10
  const child_process_1 = require("child_process");
11
+ const session_service_1 = require("./session-service");
11
12
  function resolveFirstRunPublicDir() {
12
13
  const candidates = [
13
14
  path_1.default.resolve(process.cwd(), 'public/first-run'),
@@ -47,6 +48,9 @@ function pickProjectPath() {
47
48
  });
48
49
  return result.status === 0 ? result.stdout.trim() || null : null;
49
50
  }
51
+ function isCanonicalRowId(value) {
52
+ return typeof value === 'string' && session_service_1.FIRST_RUN_ROW_IDS.includes(value);
53
+ }
50
54
  class FirstRunServer {
51
55
  constructor(options) {
52
56
  this.app = (0, express_1.default)();
@@ -100,21 +104,25 @@ class FirstRunServer {
100
104
  this.app.get('/api/first-run/session', (_req, res) => {
101
105
  res.json(this.sessionService.getSession());
102
106
  });
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.' });
107
+ this.app.post('/api/first-run/rows/:rowId/run', async (req, res) => {
108
+ const { rowId } = req.params;
109
+ if (!isCanonicalRowId(rowId)) {
110
+ return res.status(400).json({ error: `Unknown row id: ${rowId}` });
111
+ }
112
+ try {
113
+ const result = await this.sessionService.runRow(rowId, req.body || {});
114
+ return res.json(result);
115
+ }
116
+ catch (error) {
117
+ return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not run row.' });
109
118
  }
110
- return res.json(this.sessionService.selectAgent(req.body.agentId));
111
119
  });
112
- this.app.post('/api/first-run/configure', async (_req, res) => {
120
+ this.app.post('/api/first-run/agent/change', (req, res) => {
113
121
  try {
114
- return res.json(await this.sessionService.configureFraim());
122
+ return res.json(this.sessionService.changeAgent(req.body || {}));
115
123
  }
116
124
  catch (error) {
117
- return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not configure FRAIM.' });
125
+ return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not change agent.' });
118
126
  }
119
127
  });
120
128
  this.app.post('/api/first-run/project-path/pick', (_req, res) => {
@@ -129,25 +137,22 @@ class FirstRunServer {
129
137
  return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open the folder picker.' });
130
138
  }
131
139
  });
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
140
  this.app.post('/api/first-run/finish', (_req, res) => {
147
141
  const result = this.sessionService.finish();
148
142
  this.finishResolver?.();
149
143
  return res.json(result);
150
144
  });
145
+ // Hub-launch helper — starts an AiHubServer for the chosen project and
146
+ // opens the user's browser. v2 (#355) replaces the in-process spawn with
147
+ // a durable launcher binary.
148
+ this.app.post('/api/first-run/open-hub', async (_req, res) => {
149
+ try {
150
+ return res.json(await this.sessionService.openHub());
151
+ }
152
+ catch (error) {
153
+ return res.status(500).json({ error: error instanceof Error ? error.message : 'Could not open Hub.' });
154
+ }
155
+ });
151
156
  }
152
157
  }
153
158
  exports.FirstRunServer = FirstRunServer;