fraim-framework 2.0.85 ā 2.0.87
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/bin/fraim.js
CHANGED
|
@@ -16,6 +16,7 @@ const version_utils_1 = require("../utils/version-utils");
|
|
|
16
16
|
const ide_detector_1 = require("../setup/ide-detector");
|
|
17
17
|
const codex_local_config_1 = require("../setup/codex-local-config");
|
|
18
18
|
const provider_registry_1 = require("../providers/provider-registry");
|
|
19
|
+
const fraim_gitignore_1 = require("../utils/fraim-gitignore");
|
|
19
20
|
const promptForJiraProjectKey = async (jiraBaseUrl) => {
|
|
20
21
|
console.log(chalk_1.default.blue('\nš« Jira Project Configuration'));
|
|
21
22
|
console.log(chalk_1.default.gray(`Jira instance: ${jiraBaseUrl}`));
|
|
@@ -166,6 +167,9 @@ const runInitProject = async () => {
|
|
|
166
167
|
console.log(chalk_1.default.green(`Created .fraim/${dir}`));
|
|
167
168
|
}
|
|
168
169
|
});
|
|
170
|
+
if ((0, fraim_gitignore_1.ensureFraimSyncedContentIgnored)(projectRoot)) {
|
|
171
|
+
console.log(chalk_1.default.green('Updated .gitignore with FRAIM synced content ignore rules'));
|
|
172
|
+
}
|
|
169
173
|
if (!process.env.FRAIM_SKIP_SYNC) {
|
|
170
174
|
await (0, sync_1.runSync)({});
|
|
171
175
|
}
|
|
@@ -46,11 +46,19 @@ 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
48
|
const platform_detection_1 = require("../utils/platform-detection");
|
|
49
|
-
const
|
|
49
|
+
const provider_client_1 = require("../api/provider-client");
|
|
50
50
|
const provider_prompts_1 = require("../setup/provider-prompts");
|
|
51
51
|
const provider_registry_1 = require("../providers/provider-registry");
|
|
52
52
|
const init_project_1 = require("./init-project");
|
|
53
53
|
const script_sync_utils_1 = require("../utils/script-sync-utils");
|
|
54
|
+
function parseModeOption(mode) {
|
|
55
|
+
if (mode === 'conversational' || mode === 'integrated' || mode === 'split') {
|
|
56
|
+
return mode;
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk_1.default.red(`ā Invalid mode "${mode}"`));
|
|
59
|
+
console.log(chalk_1.default.yellow('š” Valid values: integrated, split, conversational'));
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
54
62
|
const promptForFraimKey = async () => {
|
|
55
63
|
console.log(chalk_1.default.blue('š FRAIM Key Setup'));
|
|
56
64
|
console.log('FRAIM requires a valid API key to access workflows and features.\n');
|
|
@@ -293,22 +301,28 @@ const runSetup = async (options) => {
|
|
|
293
301
|
// Get FRAIM key
|
|
294
302
|
fraimKey = options.key || await promptForFraimKey();
|
|
295
303
|
console.log(chalk_1.default.green('ā
FRAIM key accepted\n'));
|
|
296
|
-
// Ask for mode preference
|
|
297
|
-
mode = await promptForMode();
|
|
304
|
+
// Ask for mode preference (or use explicit option)
|
|
305
|
+
mode = options.mode ? parseModeOption(options.mode) : await promptForMode();
|
|
306
|
+
// Parse provider CLI flags on first-time setup too
|
|
307
|
+
const parsed = await parseLegacyOptions(options, fraimKey);
|
|
308
|
+
requestedProviders = parsed.requestedProviders;
|
|
309
|
+
providedTokens = parsed.providedTokens;
|
|
310
|
+
providedConfigs = parsed.providedConfigs;
|
|
298
311
|
}
|
|
312
|
+
const providerClient = new provider_client_1.ProviderClient(fraimKey, process.env.FRAIM_REMOTE_URL || undefined);
|
|
299
313
|
// Handle platform tokens based on mode
|
|
300
314
|
if (mode === 'integrated') {
|
|
301
315
|
let providersToSetup = requestedProviders;
|
|
302
316
|
// If no specific providers requested and not an update, ask user
|
|
303
317
|
if (!isUpdate && providersToSetup.length === 0) {
|
|
304
|
-
providersToSetup = await (0, provider_prompts_1.promptForProviders)(
|
|
318
|
+
providersToSetup = await (0, provider_prompts_1.promptForProviders)(providerClient);
|
|
305
319
|
}
|
|
306
320
|
// Get credentials for each provider
|
|
307
321
|
for (const providerId of providersToSetup) {
|
|
308
322
|
if (!tokens[providerId]) {
|
|
309
323
|
try {
|
|
310
324
|
// Use provided tokens if available, otherwise prompt
|
|
311
|
-
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(
|
|
325
|
+
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, providerId, providedTokens[providerId], providedConfigs[providerId]);
|
|
312
326
|
tokens[providerId] = creds.token;
|
|
313
327
|
if (creds.config) {
|
|
314
328
|
configs[providerId] = creds.config;
|
|
@@ -336,12 +350,12 @@ const runSetup = async (options) => {
|
|
|
336
350
|
console.log(chalk_1.default.blue('\nš Split Mode Configuration'));
|
|
337
351
|
console.log(chalk_1.default.gray('Configure separate platforms for code hosting and issue tracking.\n'));
|
|
338
352
|
// Get code repository platform
|
|
339
|
-
const codeRepoProvider = await (0, provider_prompts_1.promptForSingleProvider)(
|
|
353
|
+
const codeRepoProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'code');
|
|
340
354
|
// Get code repository credentials
|
|
341
355
|
if (!tokens[codeRepoProvider]) {
|
|
342
356
|
try {
|
|
343
357
|
// Use provided tokens if available, otherwise prompt
|
|
344
|
-
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(
|
|
358
|
+
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, codeRepoProvider, providedTokens[codeRepoProvider], providedConfigs[codeRepoProvider]);
|
|
345
359
|
tokens[codeRepoProvider] = creds.token;
|
|
346
360
|
if (creds.config) {
|
|
347
361
|
configs[codeRepoProvider] = creds.config;
|
|
@@ -354,12 +368,12 @@ const runSetup = async (options) => {
|
|
|
354
368
|
}
|
|
355
369
|
}
|
|
356
370
|
// Get issue tracking platform
|
|
357
|
-
const issueProvider = await (0, provider_prompts_1.promptForSingleProvider)(
|
|
371
|
+
const issueProvider = await (0, provider_prompts_1.promptForSingleProvider)(providerClient, 'issues');
|
|
358
372
|
// Get issue tracking credentials (if different from code repo)
|
|
359
373
|
if (!tokens[issueProvider]) {
|
|
360
374
|
try {
|
|
361
375
|
// Use provided tokens if available, otherwise prompt
|
|
362
|
-
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(
|
|
376
|
+
const creds = await (0, provider_prompts_1.promptForProviderCredentials)(providerClient, issueProvider, providedTokens[issueProvider], providedConfigs[issueProvider]);
|
|
363
377
|
tokens[issueProvider] = creds.token;
|
|
364
378
|
if (creds.config) {
|
|
365
379
|
configs[issueProvider] = creds.config;
|
|
@@ -474,6 +488,7 @@ exports.runSetup = runSetup;
|
|
|
474
488
|
exports.setupCommand = new commander_1.Command('setup')
|
|
475
489
|
.description('Complete global FRAIM setup with platform configuration')
|
|
476
490
|
.option('--key <key>', 'FRAIM API key')
|
|
491
|
+
.option('--mode <mode>', 'Usage mode: integrated | split | conversational')
|
|
477
492
|
.option('--ide <ides>', 'Configure specific IDEs');
|
|
478
493
|
// Track initialization promise for CLI entry point
|
|
479
494
|
exports.setupCommandInitialization = null;
|
|
@@ -0,0 +1,46 @@
|
|
|
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.ensureFraimSyncedContentIgnored = exports.FRAIM_SYNC_GITIGNORE_ENTRIES = exports.FRAIM_SYNC_GITIGNORE_END = exports.FRAIM_SYNC_GITIGNORE_START = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
exports.FRAIM_SYNC_GITIGNORE_START = '# BEGIN FRAIM SYNCED CONTENT';
|
|
10
|
+
exports.FRAIM_SYNC_GITIGNORE_END = '# END FRAIM SYNCED CONTENT';
|
|
11
|
+
exports.FRAIM_SYNC_GITIGNORE_ENTRIES = [
|
|
12
|
+
'.fraim/workflows/',
|
|
13
|
+
'.fraim/docs/',
|
|
14
|
+
'.fraim/ai-employee/',
|
|
15
|
+
'.fraim/ai-manager/'
|
|
16
|
+
];
|
|
17
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
/**
|
|
19
|
+
* Ensures the repo .gitignore contains a managed FRAIM synced-content block.
|
|
20
|
+
* Returns true when the file was modified.
|
|
21
|
+
*/
|
|
22
|
+
const ensureFraimSyncedContentIgnored = (projectRoot) => {
|
|
23
|
+
const gitignorePath = path_1.default.join(projectRoot, '.gitignore');
|
|
24
|
+
const existingRaw = fs_1.default.existsSync(gitignorePath)
|
|
25
|
+
? fs_1.default.readFileSync(gitignorePath, 'utf8')
|
|
26
|
+
: '';
|
|
27
|
+
const newline = existingRaw.includes('\r\n') ? '\r\n' : '\n';
|
|
28
|
+
const normalized = existingRaw.replace(/\r\n/g, '\n');
|
|
29
|
+
const managedBlock = [
|
|
30
|
+
exports.FRAIM_SYNC_GITIGNORE_START,
|
|
31
|
+
'# Synced by fraim init-project (generated content)',
|
|
32
|
+
...exports.FRAIM_SYNC_GITIGNORE_ENTRIES,
|
|
33
|
+
exports.FRAIM_SYNC_GITIGNORE_END
|
|
34
|
+
].join('\n');
|
|
35
|
+
const blockPattern = new RegExp(`${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_START)}[\\s\\S]*?${escapeRegExp(exports.FRAIM_SYNC_GITIGNORE_END)}\\n?`, 'm');
|
|
36
|
+
const hasManagedBlock = blockPattern.test(normalized);
|
|
37
|
+
const updatedNormalized = hasManagedBlock
|
|
38
|
+
? normalized.replace(blockPattern, `${managedBlock}\n`)
|
|
39
|
+
: `${normalized.trimEnd()}${normalized.trimEnd().length > 0 ? '\n\n' : ''}${managedBlock}\n`;
|
|
40
|
+
if (updatedNormalized !== normalized) {
|
|
41
|
+
fs_1.default.writeFileSync(gitignorePath, updatedNormalized.replace(/\n/g, newline), 'utf8');
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
exports.ensureFraimSyncedContentIgnored = ensureFraimSyncedContentIgnored;
|
|
@@ -17,6 +17,84 @@ const fs_1 = require("fs");
|
|
|
17
17
|
const path_1 = require("path");
|
|
18
18
|
const chalk_1 = __importDefault(require("chalk"));
|
|
19
19
|
const script_sync_utils_1 = require("./script-sync-utils");
|
|
20
|
+
const fraim_gitignore_1 = require("./fraim-gitignore");
|
|
21
|
+
const LOCK_SYNCED_CONTENT_ENV = 'FRAIM_LOCK_SYNCED_CONTENT';
|
|
22
|
+
const SYNCED_CONTENT_BANNER_MARKER = '<!-- FRAIM_SYNC_MANAGED_CONTENT -->';
|
|
23
|
+
function shouldLockSyncedContent() {
|
|
24
|
+
const raw = process.env[LOCK_SYNCED_CONTENT_ENV];
|
|
25
|
+
if (!raw) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
const normalized = raw.trim().toLowerCase();
|
|
29
|
+
return !['0', 'false', 'off', 'no'].includes(normalized);
|
|
30
|
+
}
|
|
31
|
+
function getSyncedContentLockTargets(projectRoot) {
|
|
32
|
+
return fraim_gitignore_1.FRAIM_SYNC_GITIGNORE_ENTRIES
|
|
33
|
+
.map((entry) => entry.replace(/[\\/]+$/, ''))
|
|
34
|
+
.filter((entry) => entry.length > 0)
|
|
35
|
+
.map((entry) => (0, path_1.join)(projectRoot, entry));
|
|
36
|
+
}
|
|
37
|
+
function setFileWriteLockRecursively(dirPath, readOnly) {
|
|
38
|
+
if (!(0, fs_1.existsSync)(dirPath)) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
setFileWriteLockRecursively(fullPath, readOnly);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Cross-platform write lock for text files:
|
|
50
|
+
// - Unix: mode bits
|
|
51
|
+
// - Windows: toggles read-only attribute behavior for file writes
|
|
52
|
+
(0, fs_1.chmodSync)(fullPath, readOnly ? 0o444 : 0o666);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Best-effort permission adjustment; keep sync non-blocking.
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getBannerRegistryPath(file) {
|
|
60
|
+
if (file.type === 'job') {
|
|
61
|
+
return `jobs/${file.path}`;
|
|
62
|
+
}
|
|
63
|
+
if (file.type === 'skill') {
|
|
64
|
+
return `skills/${file.path}`;
|
|
65
|
+
}
|
|
66
|
+
if (file.type === 'rule') {
|
|
67
|
+
return `rules/${file.path}`;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function insertAfterFrontmatter(content, banner) {
|
|
72
|
+
const normalized = content.replace(/^\uFEFF/, '');
|
|
73
|
+
const frontmatterMatch = normalized.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n)?/);
|
|
74
|
+
if (!frontmatterMatch) {
|
|
75
|
+
return `${banner}${normalized}`;
|
|
76
|
+
}
|
|
77
|
+
const frontmatter = frontmatterMatch[0];
|
|
78
|
+
const body = normalized.slice(frontmatter.length);
|
|
79
|
+
return `${frontmatter}${banner}${body}`;
|
|
80
|
+
}
|
|
81
|
+
function buildSyncedContentBanner(typeLabel, registryPath) {
|
|
82
|
+
const overridePath = `.fraim/personalized-employee/${registryPath}`;
|
|
83
|
+
return `${SYNCED_CONTENT_BANNER_MARKER}
|
|
84
|
+
> [!IMPORTANT]
|
|
85
|
+
> This ${typeLabel} is synced from FRAIM and will be overwritten on the next \`fraim sync\`.
|
|
86
|
+
> Do not edit this file.
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
function applySyncedContentBanner(file) {
|
|
90
|
+
const registryPath = getBannerRegistryPath(file);
|
|
91
|
+
if (!registryPath) {
|
|
92
|
+
return file.content;
|
|
93
|
+
}
|
|
94
|
+
const typeLabel = file.type === 'job' ? 'job stub' : `${file.type} file`;
|
|
95
|
+
const banner = buildSyncedContentBanner(typeLabel, registryPath);
|
|
96
|
+
return insertAfterFrontmatter(file.content, banner);
|
|
97
|
+
}
|
|
20
98
|
/**
|
|
21
99
|
* Sync workflows and scripts from remote FRAIM server
|
|
22
100
|
*/
|
|
@@ -61,6 +139,13 @@ async function syncFromRemote(options) {
|
|
|
61
139
|
error: 'No files received'
|
|
62
140
|
};
|
|
63
141
|
}
|
|
142
|
+
const lockTargets = getSyncedContentLockTargets(options.projectRoot);
|
|
143
|
+
if (shouldLockSyncedContent()) {
|
|
144
|
+
// If previous sync locked these paths read-only, temporarily unlock before cleanup/write.
|
|
145
|
+
for (const target of lockTargets) {
|
|
146
|
+
setFileWriteLockRecursively(target, false);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
64
149
|
// Sync workflows
|
|
65
150
|
const workflowFiles = files.filter(f => f.type === 'workflow');
|
|
66
151
|
const workflowsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'workflows');
|
|
@@ -94,7 +179,7 @@ async function syncFromRemote(options) {
|
|
|
94
179
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
95
180
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
96
181
|
}
|
|
97
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
182
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
98
183
|
console.log(chalk_1.default.gray(` + ai-employee/jobs/${file.path}`));
|
|
99
184
|
}
|
|
100
185
|
// Sync ai-manager job stubs to .fraim/ai-manager/jobs/
|
|
@@ -110,7 +195,7 @@ async function syncFromRemote(options) {
|
|
|
110
195
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
111
196
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
112
197
|
}
|
|
113
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
198
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
114
199
|
console.log(chalk_1.default.gray(` + ai-manager/jobs/${managerRelativePath}`));
|
|
115
200
|
}
|
|
116
201
|
// Sync full skill files to .fraim/ai-employee/skills/
|
|
@@ -126,7 +211,7 @@ async function syncFromRemote(options) {
|
|
|
126
211
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
127
212
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
128
213
|
}
|
|
129
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
214
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
130
215
|
console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path}`));
|
|
131
216
|
}
|
|
132
217
|
// Sync full rule files to .fraim/ai-employee/rules/
|
|
@@ -142,7 +227,7 @@ async function syncFromRemote(options) {
|
|
|
142
227
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
143
228
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
144
229
|
}
|
|
145
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
230
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
146
231
|
console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path}`));
|
|
147
232
|
}
|
|
148
233
|
// Sync scripts to user directory
|
|
@@ -180,6 +265,12 @@ async function syncFromRemote(options) {
|
|
|
180
265
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
181
266
|
console.log(chalk_1.default.gray(` + docs/${file.path}`));
|
|
182
267
|
}
|
|
268
|
+
if (shouldLockSyncedContent()) {
|
|
269
|
+
for (const target of lockTargets) {
|
|
270
|
+
setFileWriteLockRecursively(target, true);
|
|
271
|
+
}
|
|
272
|
+
console.log(chalk_1.default.gray(` š Synced FRAIM content locked as read-only (set ${LOCK_SYNCED_CONTENT_ENV}=false to disable)`));
|
|
273
|
+
}
|
|
183
274
|
return {
|
|
184
275
|
success: true,
|
|
185
276
|
workflowsSynced: workflowFiles.length,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fraim-framework",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.87",
|
|
4
4
|
"description": "FRAIM v2: Framework for Rigor-based AI Management - Transform from solo developer to AI manager orchestrating production-ready code with enterprise-grade discipline",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -114,6 +114,7 @@
|
|
|
114
114
|
"express": "^5.2.1",
|
|
115
115
|
"mongodb": "^7.0.0",
|
|
116
116
|
"prompts": "^2.4.2",
|
|
117
|
+
"resend": "^6.9.3",
|
|
117
118
|
"stripe": "^20.3.1",
|
|
118
119
|
"toml": "^3.0.0",
|
|
119
120
|
"tree-kill": "^1.2.2"
|