fraim 2.0.129 → 2.0.130
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/ai-hub/catalog.js +7 -4
- package/dist/src/cli/commands/init-project.js +55 -121
- package/dist/src/cli/commands/setup.js +68 -43
- package/dist/src/cli/commands/workspace-config.js +31 -0
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/setup/ide-global-integration.js +19 -0
- package/dist/src/cli/setup/user-level-sync.js +5 -0
- package/dist/src/cli/utils/project-bootstrap.js +3 -3
- package/dist/src/core/fraim-config-contract.js +145 -0
- package/dist/src/core/fraim-config-schema.generated.js +296 -0
- package/dist/src/core/utils/setup-preferences.js +41 -0
- package/dist/src/first-run/server.js +59 -4
- package/dist/src/first-run/session-service.js +255 -364
- package/dist/src/first-run/types.js +10 -21
- package/dist/src/local-mcp-server/stdio-server.js +28 -29
- package/dist/src/local-mcp-server/usage-collector.js +3 -0
- package/index.js +1 -1
- package/package.json +7 -5
- package/public/ai-hub/script.js +187 -1
- package/public/first-run/error-frame.js +100 -100
- package/public/first-run/index.html +5 -6
- package/public/first-run/script.js +275 -238
- package/public/first-run/styles.css +603 -395
|
@@ -269,10 +269,13 @@ function readJobFrontmatter(filePath) {
|
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
function findJobStubPath(projectPath, jobId) {
|
|
272
|
-
// Walk
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
const layers = discoverLayers(projectPath,
|
|
272
|
+
// Walk both employee and manager layer sets. The analytics surfaces can
|
|
273
|
+
// inspect any FRAIM job run, including manager-side jobs such as
|
|
274
|
+
// sleep-on-learnings.
|
|
275
|
+
const layers = discoverLayers(projectPath, [
|
|
276
|
+
...EMPLOYEE_JOB_LAYERS,
|
|
277
|
+
...MANAGER_JOB_LAYERS,
|
|
278
|
+
]);
|
|
276
279
|
let resolved = null;
|
|
277
280
|
for (const layer of layers) {
|
|
278
281
|
const candidate = path_1.default.join(layer.categoryDir, `${jobId}.md`);
|
|
@@ -9,49 +9,16 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const os_1 = __importDefault(require("os"));
|
|
11
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
-
const prompts_1 = __importDefault(require("prompts"));
|
|
13
12
|
const child_process_1 = require("child_process");
|
|
14
13
|
const sync_1 = require("./sync");
|
|
15
14
|
const platform_detection_1 = require("../utils/platform-detection");
|
|
16
|
-
const version_utils_1 = require("../utils/version-utils");
|
|
17
15
|
const ide_detector_1 = require("../setup/ide-detector");
|
|
18
16
|
const codex_local_config_1 = require("../setup/codex-local-config");
|
|
19
17
|
const claude_code_telemetry_1 = require("../setup/claude-code-telemetry");
|
|
20
|
-
const provider_registry_1 = require("../providers/provider-registry");
|
|
21
18
|
const fraim_gitignore_1 = require("../utils/fraim-gitignore");
|
|
22
|
-
const config_writer_1 = require("../../core/config-writer");
|
|
23
19
|
const agent_adapters_1 = require("../utils/agent-adapters");
|
|
24
20
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
25
21
|
const project_bootstrap_1 = require("../utils/project-bootstrap");
|
|
26
|
-
const promptForJiraProjectKey = async (jiraBaseUrl) => {
|
|
27
|
-
console.log(chalk_1.default.blue('\nJira Project Configuration'));
|
|
28
|
-
console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
|
|
29
|
-
console.log(chalk_1.default.gray('Enter the Jira project key for this repository (e.g., TEAM, PROJ, DEV)\n'));
|
|
30
|
-
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
31
|
-
const defaultKey = process.env.FRAIM_JIRA_PROJECT_KEY || 'PROJ';
|
|
32
|
-
console.log(chalk_1.default.yellow(`\nNon-interactive mode: using Jira project key "${defaultKey}"`));
|
|
33
|
-
return defaultKey;
|
|
34
|
-
}
|
|
35
|
-
const response = await (0, prompts_1.default)({
|
|
36
|
-
type: 'text',
|
|
37
|
-
name: 'projectKey',
|
|
38
|
-
message: 'Jira Project Key:',
|
|
39
|
-
validate: (value) => {
|
|
40
|
-
if (!value || value.trim().length === 0) {
|
|
41
|
-
return 'Project key is required';
|
|
42
|
-
}
|
|
43
|
-
if (!/^[A-Z][A-Z0-9]*$/.test(value.trim())) {
|
|
44
|
-
return 'Project key must start with a letter and contain only uppercase letters and numbers';
|
|
45
|
-
}
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
if (!response.projectKey) {
|
|
50
|
-
console.log(chalk_1.default.red('\nJira project key is required for split mode'));
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
return response.projectKey.trim().toUpperCase();
|
|
54
|
-
};
|
|
55
22
|
const checkGlobalSetup = () => {
|
|
56
23
|
const fraimUserDir = process.env.FRAIM_USER_DIR || path_1.default.join(os_1.default.homedir(), '.fraim');
|
|
57
24
|
const globalConfigPath = path_1.default.join(fraimUserDir, 'config.json');
|
|
@@ -158,6 +125,18 @@ const createGitHubLabels = (projectRoot) => {
|
|
|
158
125
|
console.log(chalk_1.default.yellow(`Error reading labels.json: ${error.message}`));
|
|
159
126
|
}
|
|
160
127
|
};
|
|
128
|
+
function formatPlatformLabel(provider) {
|
|
129
|
+
switch (provider) {
|
|
130
|
+
case 'ado':
|
|
131
|
+
return 'Azure DevOps';
|
|
132
|
+
case 'github':
|
|
133
|
+
return 'GitHub';
|
|
134
|
+
case 'gitlab':
|
|
135
|
+
return 'GitLab';
|
|
136
|
+
default:
|
|
137
|
+
return provider.toUpperCase();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
161
140
|
const runInitProject = async (options = {}) => {
|
|
162
141
|
console.log(chalk_1.default.blue('Initializing FRAIM project...'));
|
|
163
142
|
const failHard = options.failHard ?? 'exit';
|
|
@@ -188,94 +167,7 @@ const runInitProject = async (options = {}) => {
|
|
|
188
167
|
console.log(chalk_1.default.yellow(`${fraimDirDisplayPath} directory already exists`));
|
|
189
168
|
(0, project_bootstrap_1.recordPathStatus)(result, fraimDirDisplayPath, false);
|
|
190
169
|
}
|
|
191
|
-
if (
|
|
192
|
-
const preferredMode = globalSetup.mode || 'integrated';
|
|
193
|
-
result.mode = preferredMode;
|
|
194
|
-
let config;
|
|
195
|
-
if (preferredMode === 'conversational') {
|
|
196
|
-
config = {
|
|
197
|
-
version: (0, version_utils_1.getFraimVersion)(),
|
|
198
|
-
project: {
|
|
199
|
-
name: projectName
|
|
200
|
-
},
|
|
201
|
-
customizations: {}
|
|
202
|
-
};
|
|
203
|
-
console.log(chalk_1.default.blue(' conversational mode: no platform integration'));
|
|
204
|
-
console.log(chalk_1.default.gray(` Project: ${projectName}`));
|
|
205
|
-
}
|
|
206
|
-
else {
|
|
207
|
-
const detection = (0, platform_detection_1.detectPlatformFromGit)();
|
|
208
|
-
if (detection.provider !== 'unknown' && detection.repository) {
|
|
209
|
-
result.repositoryDetected = true;
|
|
210
|
-
let issueTracking;
|
|
211
|
-
const jiraConfig = globalSetup.providerConfigs?.jiraConfig;
|
|
212
|
-
if (preferredMode === 'split' && globalSetup.tokens?.jira && jiraConfig?.baseUrl) {
|
|
213
|
-
const projectKey = process.env.FRAIM_JIRA_PROJECT_KEY ||
|
|
214
|
-
await promptForJiraProjectKey(jiraConfig.baseUrl);
|
|
215
|
-
issueTracking = {
|
|
216
|
-
provider: 'jira',
|
|
217
|
-
baseUrl: jiraConfig.baseUrl,
|
|
218
|
-
projectKey,
|
|
219
|
-
email: jiraConfig.email
|
|
220
|
-
};
|
|
221
|
-
result.issueTrackingDetected = true;
|
|
222
|
-
console.log(chalk_1.default.blue(` Code Repository: ${detection.provider.toUpperCase()}`));
|
|
223
|
-
console.log(chalk_1.default.blue(` Issue Tracking: JIRA (${projectKey})`));
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
issueTracking = {
|
|
227
|
-
provider: detection.provider,
|
|
228
|
-
...detection.repository
|
|
229
|
-
};
|
|
230
|
-
result.issueTrackingDetected = true;
|
|
231
|
-
if (preferredMode === 'split') {
|
|
232
|
-
result.mode = 'integrated';
|
|
233
|
-
result.warnings.push('Split mode is not fully configured for this repo yet, so issue tracking fell back to the repository provider.');
|
|
234
|
-
}
|
|
235
|
-
const providerDef = await (0, provider_registry_1.getProvider)(detection.provider);
|
|
236
|
-
console.log(chalk_1.default.blue(` Platform: ${providerDef?.displayName || detection.provider.toUpperCase()}`));
|
|
237
|
-
}
|
|
238
|
-
config = {
|
|
239
|
-
version: (0, version_utils_1.getFraimVersion)(),
|
|
240
|
-
project: {
|
|
241
|
-
name: detection.repository.name || projectName
|
|
242
|
-
},
|
|
243
|
-
repository: detection.repository,
|
|
244
|
-
issueTracking,
|
|
245
|
-
customizations: {}
|
|
246
|
-
};
|
|
247
|
-
const repo = detection.repository;
|
|
248
|
-
if (repo.owner && repo.name) {
|
|
249
|
-
console.log(chalk_1.default.gray(` Repository: ${repo.owner}/${repo.name}`));
|
|
250
|
-
}
|
|
251
|
-
else if (repo.organization && repo.project && repo.name) {
|
|
252
|
-
console.log(chalk_1.default.gray(` Organization: ${repo.organization}`));
|
|
253
|
-
console.log(chalk_1.default.gray(` Project: ${repo.project}`));
|
|
254
|
-
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
255
|
-
}
|
|
256
|
-
else if (repo.namespace && repo.name) {
|
|
257
|
-
console.log(chalk_1.default.gray(` Namespace: ${repo.namespace || '(none)'}`));
|
|
258
|
-
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
else {
|
|
262
|
-
config = {
|
|
263
|
-
version: (0, version_utils_1.getFraimVersion)(),
|
|
264
|
-
project: {
|
|
265
|
-
name: projectName
|
|
266
|
-
},
|
|
267
|
-
customizations: {}
|
|
268
|
-
};
|
|
269
|
-
result.mode = 'conversational';
|
|
270
|
-
result.warnings.push('No git remote detected. FRAIM fell back to conversational project setup.');
|
|
271
|
-
console.log(chalk_1.default.yellow(' No git remote detected. Falling back to conversational mode.'));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
(0, config_writer_1.writeFraimConfig)(configPath, config);
|
|
275
|
-
console.log(chalk_1.default.green(`Created ${configDisplayPath}`));
|
|
276
|
-
(0, project_bootstrap_1.recordPathStatus)(result, configDisplayPath, true);
|
|
277
|
-
}
|
|
278
|
-
else {
|
|
170
|
+
if (fs_1.default.existsSync(configPath)) {
|
|
279
171
|
(0, project_bootstrap_1.recordPathStatus)(result, configDisplayPath, false);
|
|
280
172
|
try {
|
|
281
173
|
const existingConfig = JSON.parse(fs_1.default.readFileSync(configPath, 'utf8'));
|
|
@@ -289,6 +181,48 @@ const runInitProject = async (options = {}) => {
|
|
|
289
181
|
result.warnings.push(`Existing ${configDisplayPath} could not be parsed for summary details.`);
|
|
290
182
|
}
|
|
291
183
|
}
|
|
184
|
+
else {
|
|
185
|
+
const preferredMode = globalSetup.mode || 'integrated';
|
|
186
|
+
result.mode = preferredMode;
|
|
187
|
+
const detection = (0, platform_detection_1.detectPlatformFromGit)();
|
|
188
|
+
if (detection.provider !== 'unknown' && detection.repository) {
|
|
189
|
+
result.repositoryDetected = true;
|
|
190
|
+
result.projectName = detection.repository.name || projectName;
|
|
191
|
+
const jiraConfig = globalSetup.providerConfigs?.jiraConfig;
|
|
192
|
+
if (preferredMode === 'split' && globalSetup.tokens?.jira && jiraConfig?.baseUrl) {
|
|
193
|
+
result.issueTrackingDetected = true;
|
|
194
|
+
console.log(chalk_1.default.blue(` Code Repository: ${formatPlatformLabel(detection.provider)}`));
|
|
195
|
+
console.log(chalk_1.default.blue(' Issue Tracking: JIRA'));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
result.issueTrackingDetected = preferredMode !== 'conversational';
|
|
199
|
+
if (preferredMode === 'split') {
|
|
200
|
+
result.mode = 'integrated';
|
|
201
|
+
result.warnings.push('Split mode is not fully configured for this repo yet, so issue tracking will fall back to the repository provider during onboarding.');
|
|
202
|
+
}
|
|
203
|
+
console.log(chalk_1.default.blue(` Platform: ${formatPlatformLabel(detection.provider)}`));
|
|
204
|
+
}
|
|
205
|
+
const repo = detection.repository;
|
|
206
|
+
if (repo.owner && repo.name) {
|
|
207
|
+
console.log(chalk_1.default.gray(` Repository: ${repo.owner}/${repo.name}`));
|
|
208
|
+
}
|
|
209
|
+
else if (repo.organization && repo.project && repo.name) {
|
|
210
|
+
console.log(chalk_1.default.gray(` Organization: ${repo.organization}`));
|
|
211
|
+
console.log(chalk_1.default.gray(` Project: ${repo.project}`));
|
|
212
|
+
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
213
|
+
}
|
|
214
|
+
else if (repo.namespace && repo.name) {
|
|
215
|
+
console.log(chalk_1.default.gray(` Namespace: ${repo.namespace || '(none)'}`));
|
|
216
|
+
console.log(chalk_1.default.gray(` Repository: ${repo.name}`));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
result.mode = 'conversational';
|
|
221
|
+
result.warnings.push('No git remote detected. FRAIM fell back to conversational project setup.');
|
|
222
|
+
console.log(chalk_1.default.yellow(' No git remote detected. Falling back to conversational mode.'));
|
|
223
|
+
}
|
|
224
|
+
result.warnings.push(`${configDisplayPath} was not created by init-project. The manager \`project-onboarding\` job is the only supported config-writing path.`);
|
|
225
|
+
}
|
|
292
226
|
['jobs', 'ai-employee/jobs', 'ai-employee/skills', 'ai-manager/jobs', 'personalized-employee'].forEach((dir) => {
|
|
293
227
|
const dirPath = path_1.default.join(fraimDir, dir);
|
|
294
228
|
if (!fs_1.default.existsSync(dirPath)) {
|
|
@@ -45,11 +45,9 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
45
45
|
const path_1 = __importDefault(require("path"));
|
|
46
46
|
const auto_mcp_setup_1 = require("../setup/auto-mcp-setup");
|
|
47
47
|
const version_utils_1 = require("../utils/version-utils");
|
|
48
|
-
const platform_detection_1 = require("../utils/platform-detection");
|
|
49
48
|
const provider_client_1 = require("../api/provider-client");
|
|
50
49
|
const provider_prompts_1 = require("../setup/provider-prompts");
|
|
51
50
|
const provider_registry_1 = require("../providers/provider-registry");
|
|
52
|
-
const init_project_1 = require("./init-project");
|
|
53
51
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
54
52
|
function parseModeOption(mode) {
|
|
55
53
|
if (mode === 'conversational' || mode === 'integrated' || mode === 'split') {
|
|
@@ -650,17 +648,6 @@ const runSetup = async (options) => {
|
|
|
650
648
|
catch (e) {
|
|
651
649
|
console.log(chalk_1.default.yellow(`⚠️ IDE integration encountered issues: ${e.message}`));
|
|
652
650
|
}
|
|
653
|
-
// Auto-run project init if we're in a git repo (only on initial setup)
|
|
654
|
-
if (!isUpdate) {
|
|
655
|
-
if ((0, platform_detection_1.isGitRepository)()) {
|
|
656
|
-
console.log(chalk_1.default.blue('\n📁 Git repository detected — initializing project...'));
|
|
657
|
-
await (0, init_project_1.runInitProject)();
|
|
658
|
-
}
|
|
659
|
-
else {
|
|
660
|
-
console.log(chalk_1.default.yellow('\nℹ️ Not in a git repository — skipping project initialization.'));
|
|
661
|
-
console.log(chalk_1.default.cyan(' To initialize a project later, cd into a repo and run: fraim init-project'));
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
651
|
// Show mode-aware summary with clear value prop and next steps
|
|
665
652
|
console.log(chalk_1.default.green('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'));
|
|
666
653
|
console.log(chalk_1.default.green(' FRAIM is ready!'));
|
|
@@ -671,40 +658,78 @@ const runSetup = async (options) => {
|
|
|
671
658
|
console.log(chalk_1.default.white(' your leadership gains clear optics on AI proficiency.'));
|
|
672
659
|
console.log(chalk_1.default.gray('\n 120+ jobs across engineering, marketing, fundraising,'));
|
|
673
660
|
console.log(chalk_1.default.gray(' legal, product, hiring, customer development, and more.'));
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
console.log(chalk_1.default.
|
|
661
|
+
// Ask how the user wants to work with their AI employees.
|
|
662
|
+
// Skip prompt when the user already chose on a previous run (R2.2/R3.2/R4.6).
|
|
663
|
+
const { writeSetupHandoffChoice, readSetupHandoffChoice } = await Promise.resolve().then(() => __importStar(require('../../core/utils/setup-preferences')));
|
|
664
|
+
const storedChoice = readSetupHandoffChoice();
|
|
665
|
+
let choice;
|
|
666
|
+
if (storedChoice) {
|
|
667
|
+
console.log(chalk_1.default.gray(`\n Using your saved preference: ${storedChoice === 'ide' ? 'In my IDE' : 'In FRAIM Hub'}`));
|
|
668
|
+
choice = storedChoice;
|
|
681
669
|
}
|
|
682
670
|
else {
|
|
683
|
-
|
|
671
|
+
const userTypeResponse = await (0, prompts_1.default)({
|
|
672
|
+
type: 'select',
|
|
673
|
+
name: 'choice',
|
|
674
|
+
message: 'How would you like to work with your AI employees?',
|
|
675
|
+
choices: [
|
|
676
|
+
{ title: 'In my IDE (Claude Code, Cursor, or another AI tool)', value: 'ide' },
|
|
677
|
+
{ title: 'In FRAIM Hub (browser-based — no terminal needed)', value: 'hub' },
|
|
678
|
+
],
|
|
679
|
+
});
|
|
680
|
+
choice = userTypeResponse.choice;
|
|
684
681
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
console.log(chalk_1.default.gray(' This enables project-specific customizations,'));
|
|
696
|
-
console.log(chalk_1.default.gray(' GitHub workflows, and team learning.'));
|
|
697
|
-
const configuredProviders = await Promise.all(Object.keys(tokens).map(async (id) => await (0, provider_registry_1.getProviderDisplayName)(id)));
|
|
698
|
-
if (configuredProviders.length > 0) {
|
|
699
|
-
console.log(chalk_1.default.gray(`\n Platforms: ${configuredProviders.join(', ')}`));
|
|
682
|
+
if (!choice)
|
|
683
|
+
return; // user cancelled (Ctrl+C)
|
|
684
|
+
writeSetupHandoffChoice(choice);
|
|
685
|
+
if (choice === 'ide') {
|
|
686
|
+
const { detectInstalledIDEs: detectIDEs } = await Promise.resolve().then(() => __importStar(require('../setup/ide-detector')));
|
|
687
|
+
const configuredIDEs = detectIDEs();
|
|
688
|
+
if (configuredIDEs.length > 0) {
|
|
689
|
+
const ideNames = configuredIDEs.map((ide) => ide.name).join(', ');
|
|
690
|
+
console.log(chalk_1.default.cyan(`\n FRAIM is configured for: `) + chalk_1.default.white(ideNames));
|
|
691
|
+
console.log(chalk_1.default.cyan('\n Open any of those tools and type:'));
|
|
700
692
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
});
|
|
693
|
+
else {
|
|
694
|
+
console.log(chalk_1.default.cyan('\n Open your AI tool and type:'));
|
|
695
|
+
}
|
|
696
|
+
const { describeConfiguredInvocationSurfaces } = await Promise.resolve().then(() => __importStar(require('../setup/ide-global-integration')));
|
|
697
|
+
const invocationSummaries = describeConfiguredInvocationSurfaces(configuredIDEs);
|
|
698
|
+
invocationSummaries.forEach((summary) => {
|
|
699
|
+
console.log(chalk_1.default.white(` ${summary}`));
|
|
700
|
+
});
|
|
701
|
+
console.log(chalk_1.default.white(' "What can FRAIM help me with?"'));
|
|
702
|
+
console.log(chalk_1.default.gray('\n Just tell your AI what you need — FRAIM will find the right job.'));
|
|
703
|
+
if (mode !== 'conversational') {
|
|
704
|
+
console.log(chalk_1.default.cyan('\n To set up FRAIM in a specific project:'));
|
|
705
|
+
console.log(chalk_1.default.white(' cd your-project && fraim init-project'));
|
|
706
|
+
console.log(chalk_1.default.gray(' This enables project-specific customizations,'));
|
|
707
|
+
console.log(chalk_1.default.gray(' GitHub workflows, and team learning.'));
|
|
708
|
+
const configuredProviders = await Promise.all(Object.keys(tokens).map(async (id) => await (0, provider_registry_1.getProviderDisplayName)(id)));
|
|
709
|
+
if (configuredProviders.length > 0) {
|
|
710
|
+
console.log(chalk_1.default.gray(`\n Platforms: ${configuredProviders.join(', ')}`));
|
|
711
|
+
}
|
|
712
|
+
const allProviderIds = await (0, provider_registry_1.getAllProviderIds)();
|
|
713
|
+
const unconfiguredProviders = allProviderIds.filter(id => !tokens[id]);
|
|
714
|
+
if (unconfiguredProviders.length > 0) {
|
|
715
|
+
console.log(chalk_1.default.gray('\n To add more platforms later:'));
|
|
716
|
+
unconfiguredProviders.forEach(id => {
|
|
717
|
+
console.log(chalk_1.default.gray(` fraim setup --${id}`));
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// Hub path: launch the Hub with firstRun=true and let the user pick a project there.
|
|
724
|
+
const { FirstRunSessionService } = await Promise.resolve().then(() => __importStar(require('../../first-run/session-service')));
|
|
725
|
+
const svc = new FirstRunSessionService({ key: 'setup-hub-launch', projectRoot: process.cwd() });
|
|
726
|
+
const result = await svc.openHub();
|
|
727
|
+
if (result.ok) {
|
|
728
|
+
console.log(chalk_1.default.green(`\n Hub is open at ${result.hubUrl}`));
|
|
729
|
+
console.log(chalk_1.default.gray(' Your AI employee will help you onboard your first project.'));
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
console.log(chalk_1.default.yellow(`\n ${result.message}`));
|
|
708
733
|
}
|
|
709
734
|
}
|
|
710
735
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
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.workspaceConfigCommand = void 0;
|
|
7
|
+
exports.runWorkspaceConfigValidate = runWorkspaceConfigValidate;
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const fraim_config_contract_1 = require("../../core/fraim-config-contract");
|
|
12
|
+
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
13
|
+
function resolveConfigPath(projectRoot, explicitPath) {
|
|
14
|
+
if (explicitPath) {
|
|
15
|
+
return path_1.default.resolve(explicitPath);
|
|
16
|
+
}
|
|
17
|
+
return (0, project_fraim_paths_1.getWorkspaceConfigPath)(projectRoot ? path_1.default.resolve(projectRoot) : process.cwd());
|
|
18
|
+
}
|
|
19
|
+
function runWorkspaceConfigValidate(options) {
|
|
20
|
+
const configPath = resolveConfigPath(options.projectRoot, options.configPath);
|
|
21
|
+
const rawConfig = JSON.parse((0, fs_1.readFileSync)(configPath, 'utf8'));
|
|
22
|
+
(0, fraim_config_contract_1.assertValidWorkspaceFraimConfig)(rawConfig);
|
|
23
|
+
console.log(`Validated ${path_1.default.relative(process.cwd(), configPath) || (0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)('config.json')}`);
|
|
24
|
+
}
|
|
25
|
+
exports.workspaceConfigCommand = new commander_1.Command('workspace-config')
|
|
26
|
+
.description('Deterministically validate workspace fraim/config.json')
|
|
27
|
+
.addCommand(new commander_1.Command('validate')
|
|
28
|
+
.description('Validate fraim/config.json against the shared workspace config contract')
|
|
29
|
+
.option('--project-root <path>', 'Project root used to resolve fraim/config.json')
|
|
30
|
+
.option('--config-path <path>', 'Explicit path to fraim/config.json')
|
|
31
|
+
.action((options) => runWorkspaceConfigValidate(options)));
|
package/dist/src/cli/fraim.js
CHANGED
|
@@ -52,6 +52,7 @@ const mcp_1 = require("./commands/mcp");
|
|
|
52
52
|
const migrate_project_fraim_1 = require("./commands/migrate-project-fraim");
|
|
53
53
|
const hub_1 = require("./commands/hub");
|
|
54
54
|
const first_run_1 = require("./commands/first-run");
|
|
55
|
+
const workspace_config_1 = require("./commands/workspace-config");
|
|
55
56
|
const fs_1 = __importDefault(require("fs"));
|
|
56
57
|
const path_1 = __importDefault(require("path"));
|
|
57
58
|
const program = new commander_1.Command();
|
|
@@ -93,6 +94,7 @@ program.addCommand(mcp_1.mcpCommand);
|
|
|
93
94
|
program.addCommand(migrate_project_fraim_1.migrateProjectFraimCommand);
|
|
94
95
|
program.addCommand(hub_1.hubCommand);
|
|
95
96
|
program.addCommand(first_run_1.firstRunCommand);
|
|
97
|
+
program.addCommand(workspace_config_1.workspaceConfigCommand);
|
|
96
98
|
// Wait for async command initialization before parsing
|
|
97
99
|
(async () => {
|
|
98
100
|
// Import the initialization promise from setup command
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.describeConfiguredInvocationSurfaces = describeConfiguredInvocationSurfaces;
|
|
7
|
+
exports.describeOnboardingInvocationSurfaces = describeOnboardingInvocationSurfaces;
|
|
7
8
|
exports.installSlashCommands = installSlashCommands;
|
|
8
9
|
exports.installGlobalRules = installGlobalRules;
|
|
9
10
|
/**
|
|
@@ -20,6 +21,24 @@ const ide_invocation_surfaces_1 = require("./ide-invocation-surfaces");
|
|
|
20
21
|
function describeConfiguredInvocationSurfaces(installedIDEs) {
|
|
21
22
|
return installedIDEs.map((ide) => (0, ide_invocation_surfaces_1.describeInvocationSurface)(ide.name, ide.invocationProfile));
|
|
22
23
|
}
|
|
24
|
+
// Returns IDE-specific onboarding commands for the post-setup routing screen.
|
|
25
|
+
// Maps each IDE's invocation prefix to the project-onboarding task string so
|
|
26
|
+
// the user sees the exact command to type, not a generic FRAIM invocation.
|
|
27
|
+
function describeOnboardingInvocationSurfaces(installedIDEs) {
|
|
28
|
+
if (installedIDEs.length === 0)
|
|
29
|
+
return ['/fraim onboard this project'];
|
|
30
|
+
return installedIDEs.map((ide) => {
|
|
31
|
+
const profile = ide.invocationProfile;
|
|
32
|
+
if (profile === 'cursor-mention')
|
|
33
|
+
return `${ide.name}: @fraim onboard this project`;
|
|
34
|
+
if (profile === 'launch-phrase')
|
|
35
|
+
return `${ide.name}: onboard this project`;
|
|
36
|
+
if (profile === 'instructions-only')
|
|
37
|
+
return `${ide.name}: "onboard this project"`;
|
|
38
|
+
// claude-slash, vscode-prompt, codex-skill, windsurf-command, kiro-hashtag, gemini-command
|
|
39
|
+
return `${ide.name}: /fraim onboard this project`;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
23
42
|
/**
|
|
24
43
|
* Install Claude FRAIM discovery artifacts at the user level.
|
|
25
44
|
* Writes a skill to ~/.claude/skills/fraim/SKILL.md and a compatibility command
|
|
@@ -69,6 +69,10 @@ function ensureUserLevelDependencies(userFraimDir) {
|
|
|
69
69
|
const baseDir = userFraimDir || (0, script_sync_utils_1.getUserFraimDir)();
|
|
70
70
|
const pkgPath = path_1.default.join(baseDir, 'package.json');
|
|
71
71
|
const nodeModulesDir = path_1.default.join(baseDir, 'node_modules');
|
|
72
|
+
const childEnv = { ...process.env };
|
|
73
|
+
delete childEnv.NODE_CHANNEL_FD;
|
|
74
|
+
delete childEnv.NODE_CHANNEL_SERIALIZATION_MODE;
|
|
75
|
+
delete childEnv.NODE_UNIQUE_ID;
|
|
72
76
|
let pkg = {};
|
|
73
77
|
if (fs_1.default.existsSync(pkgPath)) {
|
|
74
78
|
try {
|
|
@@ -92,6 +96,7 @@ function ensureUserLevelDependencies(userFraimDir) {
|
|
|
92
96
|
(0, child_process_1.execSync)('npm install --no-audit --no-fund --no-save --no-package-lock --omit=dev', {
|
|
93
97
|
cwd: baseDir,
|
|
94
98
|
stdio: 'pipe',
|
|
99
|
+
env: childEnv,
|
|
95
100
|
});
|
|
96
101
|
console.log(chalk_1.default.green(`✅ Installed: ${missing.join(', ')}`));
|
|
97
102
|
}
|
|
@@ -22,11 +22,11 @@ function formatModeLabel(mode) {
|
|
|
22
22
|
function getModeSpecificNextStep(mode) {
|
|
23
23
|
switch (mode) {
|
|
24
24
|
case 'conversational':
|
|
25
|
-
return `The agent will focus on project context, validation commands, and durable repo rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
25
|
+
return `The agent will create fraim/config.json during onboarding, then focus on project context, validation commands, and durable repo rules. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
26
26
|
case 'split':
|
|
27
|
-
return `The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
27
|
+
return `The agent will create fraim/config.json during onboarding, confirm the code-host and issue-tracker split, then ask only for the missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
28
28
|
default:
|
|
29
|
-
return `The agent will review the detected repo setup, then ask only for the highest-value missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
29
|
+
return `The agent will create fraim/config.json during onboarding, review the detected repo setup, then ask only for the highest-value missing project details. For a walkthrough, watch the onboarding videos at ${ONBOARDING_VIDEO_PLAYLIST_URL}.`;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
function createInitProjectResult(projectName, mode) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FRAIM_CONFIG_SCHEMA = void 0;
|
|
4
|
+
exports.listSupportedFraimConfigPaths = listSupportedFraimConfigPaths;
|
|
5
|
+
exports.isValidFraimConfigField = isValidFraimConfigField;
|
|
6
|
+
exports.sanitizeFraimConfigToSchema = sanitizeFraimConfigToSchema;
|
|
7
|
+
exports.validateWorkspaceFraimConfig = validateWorkspaceFraimConfig;
|
|
8
|
+
exports.assertValidWorkspaceFraimConfig = assertValidWorkspaceFraimConfig;
|
|
9
|
+
const fraim_config_schema_generated_1 = require("./fraim-config-schema.generated");
|
|
10
|
+
exports.FRAIM_CONFIG_SCHEMA = fraim_config_schema_generated_1.FRAIM_CONFIG_SCHEMA;
|
|
11
|
+
const SUPPORTED_FRAIM_CONFIG_PATHS = [...fraim_config_schema_generated_1.SUPPORTED_FRAIM_CONFIG_PATHS];
|
|
12
|
+
const SUPPORTED_FRAIM_CONFIG_PATH_SET = new Set(SUPPORTED_FRAIM_CONFIG_PATHS);
|
|
13
|
+
function isPlainObject(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function listSupportedFraimConfigPaths() {
|
|
17
|
+
return [...SUPPORTED_FRAIM_CONFIG_PATHS];
|
|
18
|
+
}
|
|
19
|
+
function isValidFraimConfigField(path) {
|
|
20
|
+
if (SUPPORTED_FRAIM_CONFIG_PATH_SET.has(path)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
for (const validField of SUPPORTED_FRAIM_CONFIG_PATH_SET) {
|
|
24
|
+
if (validField.startsWith(`${path}.`)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
function sanitizeNode(node, value) {
|
|
31
|
+
if (value === undefined) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
if (node.kind === 'object') {
|
|
35
|
+
if (!isPlainObject(value)) {
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
const sanitized = {};
|
|
39
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
40
|
+
const childValue = sanitizeNode(child, value[key]);
|
|
41
|
+
if (childValue !== undefined) {
|
|
42
|
+
sanitized[key] = childValue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return sanitized;
|
|
46
|
+
}
|
|
47
|
+
if (node.kind === 'record') {
|
|
48
|
+
if (!isPlainObject(value)) {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
const sanitized = {};
|
|
52
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
53
|
+
const childValue = sanitizeNode(node.value, entryValue);
|
|
54
|
+
if (childValue !== undefined) {
|
|
55
|
+
sanitized[key] = childValue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return sanitized;
|
|
59
|
+
}
|
|
60
|
+
if (node.kind === 'array' && Array.isArray(value)) {
|
|
61
|
+
return value.map((entry) => sanitizeNode(node.element, entry));
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function validateNode(node, value, path, issues) {
|
|
66
|
+
if (value === undefined) {
|
|
67
|
+
if (node.required) {
|
|
68
|
+
issues.push({ path, message: `${path} is required.` });
|
|
69
|
+
}
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
switch (node.kind) {
|
|
73
|
+
case 'string':
|
|
74
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
75
|
+
issues.push({ path, message: `${path} must be a non-empty string.` });
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
case 'number':
|
|
79
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
80
|
+
issues.push({ path, message: `${path} must be a finite number.` });
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
case 'boolean':
|
|
84
|
+
if (typeof value !== 'boolean') {
|
|
85
|
+
issues.push({ path, message: `${path} must be a boolean.` });
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
case 'enum':
|
|
89
|
+
if (typeof value !== 'string' || !node.values.includes(value)) {
|
|
90
|
+
issues.push({ path, message: `${path} must be one of: ${node.values.join(', ')}.` });
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
case 'array':
|
|
94
|
+
if (!Array.isArray(value)) {
|
|
95
|
+
issues.push({ path, message: `${path} must be an array.` });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
value.forEach((entry, index) => validateNode(node.element, entry, `${path}[${index}]`, issues));
|
|
99
|
+
return;
|
|
100
|
+
case 'record':
|
|
101
|
+
if (!isPlainObject(value)) {
|
|
102
|
+
issues.push({ path, message: `${path} must be an object.` });
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
for (const [key, entryValue] of Object.entries(value)) {
|
|
106
|
+
validateNode(node.value, entryValue, `${path}.${key}`, issues);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
case 'object':
|
|
110
|
+
if (!isPlainObject(value)) {
|
|
111
|
+
issues.push({ path, message: `${path} must be an object.` });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
for (const key of Object.keys(value)) {
|
|
115
|
+
if (!(key in node.properties)) {
|
|
116
|
+
issues.push({
|
|
117
|
+
path: path ? `${path}.${key}` : key,
|
|
118
|
+
message: `${path ? `${path}.${key}` : key} is not part of the supported FRAIM config schema.`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const [key, child] of Object.entries(node.properties)) {
|
|
123
|
+
validateNode(child, value[key], path ? `${path}.${key}` : key, issues);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function sanitizeFraimConfigToSchema(rawConfig) {
|
|
129
|
+
return sanitizeNode(exports.FRAIM_CONFIG_SCHEMA, rawConfig);
|
|
130
|
+
}
|
|
131
|
+
function validateWorkspaceFraimConfig(rawConfig) {
|
|
132
|
+
const issues = [];
|
|
133
|
+
validateNode(exports.FRAIM_CONFIG_SCHEMA, rawConfig, '', issues);
|
|
134
|
+
return issues.map((issue) => ({
|
|
135
|
+
path: issue.path || '$',
|
|
136
|
+
message: issue.message
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
function assertValidWorkspaceFraimConfig(rawConfig) {
|
|
140
|
+
const issues = validateWorkspaceFraimConfig(rawConfig);
|
|
141
|
+
if (issues.length === 0) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`Workspace FRAIM config validation failed:\n${issues.map((issue) => `- ${issue.path}: ${issue.message}`).join('\n')}`);
|
|
145
|
+
}
|