fraim 2.0.100
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/README.md +445 -0
- package/bin/fraim.js +23 -0
- package/dist/src/cli/api/get-provider-client.js +41 -0
- package/dist/src/cli/api/provider-client.js +107 -0
- package/dist/src/cli/commands/add-ide.js +430 -0
- package/dist/src/cli/commands/add-provider.js +233 -0
- package/dist/src/cli/commands/doctor.js +149 -0
- package/dist/src/cli/commands/init-project.js +301 -0
- package/dist/src/cli/commands/list-overridable.js +184 -0
- package/dist/src/cli/commands/list.js +57 -0
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/mcp.js +15 -0
- package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
- package/dist/src/cli/commands/override.js +177 -0
- package/dist/src/cli/commands/setup.js +651 -0
- package/dist/src/cli/commands/sync.js +162 -0
- package/dist/src/cli/commands/test-mcp.js +171 -0
- package/dist/src/cli/doctor/check-runner.js +199 -0
- package/dist/src/cli/doctor/checks/global-setup-checks.js +220 -0
- package/dist/src/cli/doctor/checks/ide-config-checks.js +250 -0
- package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +381 -0
- package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
- package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
- package/dist/src/cli/doctor/checks/workflow-checks.js +251 -0
- package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
- package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
- package/dist/src/cli/doctor/types.js +6 -0
- package/dist/src/cli/fraim.js +100 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/ide-formats.js +243 -0
- package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +160 -0
- package/dist/src/cli/mcp/types.js +3 -0
- package/dist/src/cli/providers/local-provider-registry.js +166 -0
- package/dist/src/cli/providers/provider-registry.js +230 -0
- package/dist/src/cli/setup/auto-mcp-setup.js +331 -0
- package/dist/src/cli/setup/codex-local-config.js +37 -0
- package/dist/src/cli/setup/first-run.js +242 -0
- package/dist/src/cli/setup/ide-detector.js +179 -0
- package/dist/src/cli/setup/mcp-config-generator.js +192 -0
- package/dist/src/cli/setup/provider-prompts.js +339 -0
- package/dist/src/cli/utils/agent-adapters.js +126 -0
- package/dist/src/cli/utils/digest-utils.js +47 -0
- package/dist/src/cli/utils/fraim-gitignore.js +40 -0
- package/dist/src/cli/utils/platform-detection.js +258 -0
- package/dist/src/cli/utils/project-bootstrap.js +93 -0
- package/dist/src/cli/utils/remote-sync.js +315 -0
- package/dist/src/cli/utils/script-sync-utils.js +221 -0
- package/dist/src/cli/utils/version-utils.js +32 -0
- package/dist/src/core/ai-mentor.js +230 -0
- package/dist/src/core/config-loader.js +114 -0
- package/dist/src/core/config-writer.js +75 -0
- package/dist/src/core/types.js +23 -0
- package/dist/src/core/utils/git-utils.js +95 -0
- package/dist/src/core/utils/include-resolver.js +92 -0
- package/dist/src/core/utils/inheritance-parser.js +288 -0
- package/dist/src/core/utils/job-parser.js +176 -0
- package/dist/src/core/utils/local-registry-resolver.js +616 -0
- package/dist/src/core/utils/object-utils.js +11 -0
- package/dist/src/core/utils/project-fraim-migration.js +103 -0
- package/dist/src/core/utils/project-fraim-paths.js +38 -0
- package/dist/src/core/utils/provider-utils.js +18 -0
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +147 -0
- package/dist/src/core/utils/workflow-parser.js +174 -0
- package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
- package/dist/src/local-mcp-server/stdio-server.js +1698 -0
- package/dist/src/local-mcp-server/usage-collector.js +264 -0
- package/index.js +85 -0
- package/package.json +139 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform Detection Utilities
|
|
4
|
+
*
|
|
5
|
+
* Detects development platform (GitHub, ADO) from git remote URLs
|
|
6
|
+
* and provides repository information extraction.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.detectPlatformFromGit = detectPlatformFromGit;
|
|
10
|
+
exports.getGitRemoteUrl = getGitRemoteUrl;
|
|
11
|
+
exports.detectPlatformFromUrl = detectPlatformFromUrl;
|
|
12
|
+
exports.validateRepositoryConfig = validateRepositoryConfig;
|
|
13
|
+
exports.getCurrentBranch = getCurrentBranch;
|
|
14
|
+
exports.isGitRepository = isGitRepository;
|
|
15
|
+
const child_process_1 = require("child_process");
|
|
16
|
+
/**
|
|
17
|
+
* Detect platform from git remote URL
|
|
18
|
+
*/
|
|
19
|
+
function detectPlatformFromGit() {
|
|
20
|
+
try {
|
|
21
|
+
const remoteUrl = getGitRemoteUrl();
|
|
22
|
+
if (!remoteUrl) {
|
|
23
|
+
return { provider: 'unknown', confidence: 'low' };
|
|
24
|
+
}
|
|
25
|
+
return detectPlatformFromUrl(remoteUrl);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.warn('⚠️ Failed to detect platform from git:', error);
|
|
29
|
+
return { provider: 'unknown', confidence: 'low' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get git remote URL
|
|
34
|
+
*/
|
|
35
|
+
function getGitRemoteUrl() {
|
|
36
|
+
try {
|
|
37
|
+
// Try origin first
|
|
38
|
+
const originUrl = (0, child_process_1.execSync)('git remote get-url origin', {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
41
|
+
}).trim();
|
|
42
|
+
if (originUrl)
|
|
43
|
+
return originUrl;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
// Ignore error, try alternative methods
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
// Try any remote
|
|
50
|
+
const remotes = (0, child_process_1.execSync)('git remote', {
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
53
|
+
}).trim().split('\n');
|
|
54
|
+
if (remotes.length > 0 && remotes[0]) {
|
|
55
|
+
const firstRemoteUrl = (0, child_process_1.execSync)(`git remote get-url ${remotes[0]}`, {
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
58
|
+
}).trim();
|
|
59
|
+
return firstRemoteUrl;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// Ignore error
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Detect platform from URL
|
|
69
|
+
*/
|
|
70
|
+
function detectPlatformFromUrl(url) {
|
|
71
|
+
const normalizedUrl = url.toLowerCase();
|
|
72
|
+
// GitHub detection
|
|
73
|
+
if (normalizedUrl.includes('github.com')) {
|
|
74
|
+
const repository = extractGitHubInfo(url);
|
|
75
|
+
return {
|
|
76
|
+
provider: 'github',
|
|
77
|
+
repository,
|
|
78
|
+
confidence: 'high'
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// GitLab detection
|
|
82
|
+
if (normalizedUrl.includes('gitlab.com') || normalizedUrl.includes('gitlab.')) {
|
|
83
|
+
const repository = extractGitLabInfo(url);
|
|
84
|
+
return {
|
|
85
|
+
provider: 'gitlab',
|
|
86
|
+
repository,
|
|
87
|
+
confidence: 'high'
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ADO detection
|
|
91
|
+
if (normalizedUrl.includes('dev.azure.com') ||
|
|
92
|
+
normalizedUrl.includes('visualstudio.com') ||
|
|
93
|
+
normalizedUrl.includes('azure.com')) {
|
|
94
|
+
const repository = extractAdoInfo(url);
|
|
95
|
+
return {
|
|
96
|
+
provider: 'ado',
|
|
97
|
+
repository,
|
|
98
|
+
confidence: 'high'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return { provider: 'unknown', confidence: 'low' };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Extract GitHub repository information from URL
|
|
105
|
+
*/
|
|
106
|
+
function extractGitHubInfo(url) {
|
|
107
|
+
// Handle both HTTPS and SSH URLs
|
|
108
|
+
// HTTPS: https://github.com/owner/repo.git
|
|
109
|
+
// SSH: git@github.com:owner/repo.git
|
|
110
|
+
let match;
|
|
111
|
+
// HTTPS format
|
|
112
|
+
match = url.match(/github\.com[\/:]([^\/]+)\/([^\/\.]+)/i);
|
|
113
|
+
if (match) {
|
|
114
|
+
return {
|
|
115
|
+
provider: 'github',
|
|
116
|
+
owner: match[1],
|
|
117
|
+
name: match[2],
|
|
118
|
+
url: url,
|
|
119
|
+
defaultBranch: 'main'
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Fallback - just mark as GitHub
|
|
123
|
+
return {
|
|
124
|
+
provider: 'github',
|
|
125
|
+
url: url,
|
|
126
|
+
defaultBranch: 'main'
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract GitLab repository information from URL
|
|
131
|
+
*/
|
|
132
|
+
function extractGitLabInfo(url) {
|
|
133
|
+
// GitLab URL formats:
|
|
134
|
+
// HTTPS: https://gitlab.com/group/subgroup/repo.git
|
|
135
|
+
// SSH: git@gitlab.com:group/subgroup/repo.git
|
|
136
|
+
const match = url.match(/gitlab[^\/:]*[\/:]([^?\s#]+?)(?:\.git)?$/i);
|
|
137
|
+
if (match) {
|
|
138
|
+
const projectPath = match[1].replace(/^\/+/, '');
|
|
139
|
+
const segments = projectPath.split('/').filter(Boolean);
|
|
140
|
+
const name = segments[segments.length - 1];
|
|
141
|
+
const namespace = segments.slice(0, -1).join('/');
|
|
142
|
+
return {
|
|
143
|
+
provider: 'gitlab',
|
|
144
|
+
namespace: namespace || undefined,
|
|
145
|
+
name,
|
|
146
|
+
projectPath,
|
|
147
|
+
url,
|
|
148
|
+
defaultBranch: 'main'
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
// Fallback - just mark as GitLab
|
|
152
|
+
return {
|
|
153
|
+
provider: 'gitlab',
|
|
154
|
+
url,
|
|
155
|
+
defaultBranch: 'main'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Extract ADO repository information from URL
|
|
160
|
+
*/
|
|
161
|
+
function extractAdoInfo(url) {
|
|
162
|
+
// ADO URL formats:
|
|
163
|
+
// https://dev.azure.com/organization/project/_git/repository
|
|
164
|
+
// https://organization.visualstudio.com/project/_git/repository
|
|
165
|
+
let match;
|
|
166
|
+
// dev.azure.com format
|
|
167
|
+
match = url.match(/dev\.azure\.com\/([^\/]+)\/([^\/]+)\/_git\/([^\/\.]+)/i);
|
|
168
|
+
if (match) {
|
|
169
|
+
return {
|
|
170
|
+
provider: 'ado',
|
|
171
|
+
organization: match[1],
|
|
172
|
+
project: match[2],
|
|
173
|
+
name: match[3],
|
|
174
|
+
url: url,
|
|
175
|
+
defaultBranch: 'main'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// visualstudio.com format
|
|
179
|
+
match = url.match(/https?:\/\/([^\.]+)\.visualstudio\.com\/([^\/]+)\/_git\/([^\/\.]+)/i);
|
|
180
|
+
if (match) {
|
|
181
|
+
return {
|
|
182
|
+
provider: 'ado',
|
|
183
|
+
organization: match[1],
|
|
184
|
+
project: match[2],
|
|
185
|
+
name: match[3],
|
|
186
|
+
url: url,
|
|
187
|
+
defaultBranch: 'main'
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Fallback - just mark as ADO
|
|
191
|
+
return {
|
|
192
|
+
provider: 'ado',
|
|
193
|
+
url: url,
|
|
194
|
+
defaultBranch: 'main'
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Validate repository configuration
|
|
199
|
+
*/
|
|
200
|
+
function validateRepositoryConfig(config) {
|
|
201
|
+
const errors = [];
|
|
202
|
+
if (!config.provider) {
|
|
203
|
+
errors.push('Provider is required');
|
|
204
|
+
}
|
|
205
|
+
if (config.provider === 'github') {
|
|
206
|
+
if (!config.owner)
|
|
207
|
+
errors.push('GitHub owner is required');
|
|
208
|
+
if (!config.name)
|
|
209
|
+
errors.push('GitHub repository name is required');
|
|
210
|
+
}
|
|
211
|
+
if (config.provider === 'ado') {
|
|
212
|
+
if (!config.organization)
|
|
213
|
+
errors.push('ADO organization is required');
|
|
214
|
+
if (!config.project)
|
|
215
|
+
errors.push('ADO project is required');
|
|
216
|
+
if (!config.name)
|
|
217
|
+
errors.push('ADO repository name is required');
|
|
218
|
+
}
|
|
219
|
+
if (config.provider === 'gitlab') {
|
|
220
|
+
const hasProjectPath = typeof config.projectPath === 'string' && config.projectPath.length > 0;
|
|
221
|
+
const hasNamespaceAndName = typeof config.namespace === 'string' && config.namespace.length > 0 && typeof config.name === 'string' && config.name.length > 0;
|
|
222
|
+
if (!hasProjectPath && !hasNamespaceAndName) {
|
|
223
|
+
errors.push('GitLab repository requires projectPath or namespace + name');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {
|
|
227
|
+
valid: errors.length === 0,
|
|
228
|
+
errors
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get current git branch
|
|
233
|
+
*/
|
|
234
|
+
function getCurrentBranch() {
|
|
235
|
+
try {
|
|
236
|
+
return (0, child_process_1.execSync)('git branch --show-current', {
|
|
237
|
+
encoding: 'utf-8',
|
|
238
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
239
|
+
}).trim();
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if we're in a git repository
|
|
247
|
+
*/
|
|
248
|
+
function isGitRepository() {
|
|
249
|
+
try {
|
|
250
|
+
(0, child_process_1.execSync)('git rev-parse --git-dir', {
|
|
251
|
+
stdio: ['ignore', 'ignore', 'ignore']
|
|
252
|
+
});
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
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.createInitProjectResult = createInitProjectResult;
|
|
7
|
+
exports.recordPathStatus = recordPathStatus;
|
|
8
|
+
exports.buildInitProjectSummary = buildInitProjectSummary;
|
|
9
|
+
exports.printInitProjectSummary = printInitProjectSummary;
|
|
10
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
+
function formatModeLabel(mode) {
|
|
12
|
+
switch (mode) {
|
|
13
|
+
case 'conversational':
|
|
14
|
+
return 'Conversational';
|
|
15
|
+
case 'split':
|
|
16
|
+
return 'Split';
|
|
17
|
+
default:
|
|
18
|
+
return 'Integrated';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function getModeSpecificNextStep(mode) {
|
|
22
|
+
switch (mode) {
|
|
23
|
+
case 'conversational':
|
|
24
|
+
return 'The agent will focus on project context, validation commands, and durable repo rules.';
|
|
25
|
+
case 'split':
|
|
26
|
+
return 'The agent will confirm the code-host and issue-tracker split, then ask only for the missing project details.';
|
|
27
|
+
default:
|
|
28
|
+
return 'The agent will review the detected repo setup, then ask only for the highest-value missing project details.';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createInitProjectResult(projectName, mode) {
|
|
32
|
+
return {
|
|
33
|
+
mode,
|
|
34
|
+
projectName,
|
|
35
|
+
repositoryDetected: false,
|
|
36
|
+
issueTrackingDetected: false,
|
|
37
|
+
createdPaths: [],
|
|
38
|
+
reusedPaths: [],
|
|
39
|
+
warnings: [],
|
|
40
|
+
syncPerformed: false,
|
|
41
|
+
bootstrapNeeded: false
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function recordPathStatus(result, relativePath, created) {
|
|
45
|
+
if (created) {
|
|
46
|
+
result.createdPaths.push(relativePath);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
result.reusedPaths.push(relativePath);
|
|
50
|
+
}
|
|
51
|
+
function buildInitProjectSummary(result) {
|
|
52
|
+
return {
|
|
53
|
+
status: 'FRAIM project initialized.',
|
|
54
|
+
fields: {
|
|
55
|
+
mode: formatModeLabel(result.mode),
|
|
56
|
+
project: result.projectName,
|
|
57
|
+
repositoryDetection: result.repositoryDetected ? 'detected' : 'not detected',
|
|
58
|
+
issueTracking: result.issueTrackingDetected ? 'detected' : 'not detected',
|
|
59
|
+
sync: result.syncPerformed ? 'completed' : 'skipped for this run',
|
|
60
|
+
createdPaths: [...result.createdPaths],
|
|
61
|
+
reusedPaths: [...result.reusedPaths]
|
|
62
|
+
},
|
|
63
|
+
warnings: [...result.warnings],
|
|
64
|
+
nextStep: {
|
|
65
|
+
prompt: 'Tell your AI agent "Onboard this project".',
|
|
66
|
+
explanation: getModeSpecificNextStep(result.mode)
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function printInitProjectSummary(result) {
|
|
71
|
+
const summary = buildInitProjectSummary(result);
|
|
72
|
+
console.log(chalk_1.default.green(`\n${summary.status}`));
|
|
73
|
+
console.log(chalk_1.default.blue('Project summary:'));
|
|
74
|
+
console.log(chalk_1.default.gray(` Mode: ${summary.fields.mode}`));
|
|
75
|
+
console.log(chalk_1.default.gray(` Project: ${summary.fields.project}`));
|
|
76
|
+
console.log(chalk_1.default.gray(` Repository detection: ${summary.fields.repositoryDetection}`));
|
|
77
|
+
console.log(chalk_1.default.gray(` Issue tracking: ${summary.fields.issueTracking}`));
|
|
78
|
+
console.log(chalk_1.default.gray(` Sync: ${summary.fields.sync}`));
|
|
79
|
+
if (summary.fields.createdPaths.length > 0) {
|
|
80
|
+
console.log(chalk_1.default.gray(` Created: ${summary.fields.createdPaths.join(', ')}`));
|
|
81
|
+
}
|
|
82
|
+
if (summary.fields.reusedPaths.length > 0) {
|
|
83
|
+
console.log(chalk_1.default.gray(` Reused: ${summary.fields.reusedPaths.join(', ')}`));
|
|
84
|
+
}
|
|
85
|
+
if (summary.warnings.length > 0) {
|
|
86
|
+
console.log(chalk_1.default.yellow('\nWarnings:'));
|
|
87
|
+
summary.warnings.forEach((warning) => {
|
|
88
|
+
console.log(chalk_1.default.yellow(` - ${warning}`));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk_1.default.cyan(`\nNext step: ${summary.nextStep.prompt}`));
|
|
92
|
+
console.log(chalk_1.default.gray(summary.nextStep.explanation));
|
|
93
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Remote Registry Sync
|
|
4
|
+
*
|
|
5
|
+
* Fetches jobs and scripts from the remote FRAIM server
|
|
6
|
+
* instead of bundling them in the npm package.
|
|
7
|
+
*
|
|
8
|
+
* Issue: #83 - Minimize client package by fetching registry remotely
|
|
9
|
+
*/
|
|
10
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
+
};
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.syncFromRemote = syncFromRemote;
|
|
15
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
const script_sync_utils_1 = require("./script-sync-utils");
|
|
20
|
+
const fraim_gitignore_1 = require("./fraim-gitignore");
|
|
21
|
+
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
22
|
+
const LOCK_SYNCED_CONTENT_ENV = 'FRAIM_LOCK_SYNCED_CONTENT';
|
|
23
|
+
const SYNCED_CONTENT_BANNER_MARKER = '<!-- FRAIM_SYNC_MANAGED_CONTENT -->';
|
|
24
|
+
function shouldLockSyncedContent() {
|
|
25
|
+
const raw = process.env[LOCK_SYNCED_CONTENT_ENV];
|
|
26
|
+
if (!raw) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const normalized = raw.trim().toLowerCase();
|
|
30
|
+
return !['0', 'false', 'off', 'no'].includes(normalized);
|
|
31
|
+
}
|
|
32
|
+
function getSyncedContentLockTargets(projectRoot) {
|
|
33
|
+
return fraim_gitignore_1.FRAIM_SYNC_GITIGNORE_ENTRIES
|
|
34
|
+
.map((entry) => entry.replace(/[\\/]+$/, ''))
|
|
35
|
+
.filter((entry) => entry.length > 0)
|
|
36
|
+
.map((entry) => (0, path_1.join)(projectRoot, entry));
|
|
37
|
+
}
|
|
38
|
+
function setFileWriteLockRecursively(dirPath, readOnly) {
|
|
39
|
+
if (!(0, fs_1.existsSync)(dirPath)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
setFileWriteLockRecursively(fullPath, readOnly);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
// Cross-platform write lock for text files:
|
|
51
|
+
// - Unix: mode bits
|
|
52
|
+
// - Windows: toggles read-only attribute behavior for file writes
|
|
53
|
+
(0, fs_1.chmodSync)(fullPath, readOnly ? 0o444 : 0o666);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Best-effort permission adjustment; keep sync non-blocking.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function getBannerRegistryPath(file) {
|
|
61
|
+
if (file.type === 'job') {
|
|
62
|
+
return `jobs/${file.path}`;
|
|
63
|
+
}
|
|
64
|
+
if (file.type === 'skill') {
|
|
65
|
+
return `skills/${file.path}`;
|
|
66
|
+
}
|
|
67
|
+
if (file.type === 'rule') {
|
|
68
|
+
return `rules/${file.path}`;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function insertAfterFrontmatter(content, banner) {
|
|
73
|
+
const normalized = content.replace(/^\uFEFF/, '');
|
|
74
|
+
const frontmatterMatch = normalized.match(/^---\r?\n[\s\S]*?\r?\n---(?:\r?\n)?/);
|
|
75
|
+
if (!frontmatterMatch) {
|
|
76
|
+
return `${banner}${normalized}`;
|
|
77
|
+
}
|
|
78
|
+
const frontmatter = frontmatterMatch[0];
|
|
79
|
+
const body = normalized.slice(frontmatter.length);
|
|
80
|
+
return `${frontmatter}${banner}${body}`;
|
|
81
|
+
}
|
|
82
|
+
function buildSyncedContentBanner(typeLabel) {
|
|
83
|
+
return `${SYNCED_CONTENT_BANNER_MARKER}\r\n> [!IMPORTANT]\r\n> This ${typeLabel} is synced from FRAIM and will be overwritten on the next \`fraim sync\`.\r\n> Do not edit this file.\r\n`;
|
|
84
|
+
}
|
|
85
|
+
function applySyncedContentBanner(file) {
|
|
86
|
+
const registryPath = getBannerRegistryPath(file);
|
|
87
|
+
if (!registryPath) {
|
|
88
|
+
return file.content;
|
|
89
|
+
}
|
|
90
|
+
const typeLabel = file.type === 'job' || file.type === 'skill' || file.type === 'rule'
|
|
91
|
+
? `${file.type} stub`
|
|
92
|
+
: `${file.type} file`;
|
|
93
|
+
const banner = buildSyncedContentBanner(typeLabel);
|
|
94
|
+
return insertAfterFrontmatter(file.content, banner);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Sync jobs and scripts from remote FRAIM server
|
|
98
|
+
*/
|
|
99
|
+
async function syncFromRemote(options) {
|
|
100
|
+
const remoteUrl = options.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
101
|
+
const apiKey = options.apiKey || process.env.FRAIM_API_KEY || '';
|
|
102
|
+
if (!apiKey) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
employeeJobsSynced: 0,
|
|
106
|
+
managerJobsSynced: 0,
|
|
107
|
+
skillsSynced: 0,
|
|
108
|
+
rulesSynced: 0,
|
|
109
|
+
scriptsSynced: 0,
|
|
110
|
+
docsSynced: 0,
|
|
111
|
+
error: 'FRAIM_API_KEY not set'
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
try {
|
|
115
|
+
console.log(chalk_1.default.blue('🔄 Syncing from remote FRAIM server...'));
|
|
116
|
+
console.log(chalk_1.default.gray(` Remote: ${remoteUrl}`));
|
|
117
|
+
// Fetch registry files from remote server
|
|
118
|
+
const response = await axios_1.default.get(`${remoteUrl}/api/registry/sync`, {
|
|
119
|
+
headers: {
|
|
120
|
+
'x-api-key': apiKey
|
|
121
|
+
},
|
|
122
|
+
timeout: 30000
|
|
123
|
+
});
|
|
124
|
+
const files = response.data.files || [];
|
|
125
|
+
if (!files || files.length === 0) {
|
|
126
|
+
console.log(chalk_1.default.yellow('⚠️ No files received from remote server'));
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
employeeJobsSynced: 0,
|
|
130
|
+
managerJobsSynced: 0,
|
|
131
|
+
skillsSynced: 0,
|
|
132
|
+
rulesSynced: 0,
|
|
133
|
+
scriptsSynced: 0,
|
|
134
|
+
docsSynced: 0,
|
|
135
|
+
error: 'No files received'
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const lockTargets = getSyncedContentLockTargets(options.projectRoot);
|
|
139
|
+
if (shouldLockSyncedContent()) {
|
|
140
|
+
// If previous sync locked these paths read-only, temporarily unlock before cleanup/write.
|
|
141
|
+
for (const target of lockTargets) {
|
|
142
|
+
setFileWriteLockRecursively(target, false);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Sync job stubs to role-specific folders under fraim/
|
|
146
|
+
const allJobFiles = files.filter(f => f.type === 'job');
|
|
147
|
+
const managerJobFiles = allJobFiles.filter(f => f.path.startsWith('ai-manager/'));
|
|
148
|
+
const jobFiles = allJobFiles.filter(f => !f.path.startsWith('ai-manager/'));
|
|
149
|
+
const employeeJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'jobs');
|
|
150
|
+
if (!(0, fs_1.existsSync)(employeeJobsDir)) {
|
|
151
|
+
(0, fs_1.mkdirSync)(employeeJobsDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
cleanDirectory(employeeJobsDir);
|
|
154
|
+
for (const file of jobFiles) {
|
|
155
|
+
// Strip "jobs/" prefix and "ai-employee/" role prefix
|
|
156
|
+
let relPath = file.path;
|
|
157
|
+
if (relPath.startsWith('jobs/'))
|
|
158
|
+
relPath = relPath.substring('jobs/'.length);
|
|
159
|
+
relPath = relPath.replace(/^ai-employee\//, '');
|
|
160
|
+
const filePath = (0, path_1.join)(employeeJobsDir, relPath);
|
|
161
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
162
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
163
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
166
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/jobs/${relPath}`)}`));
|
|
167
|
+
}
|
|
168
|
+
// Sync ai-manager job stubs to fraim/ai-manager/jobs/
|
|
169
|
+
const managerJobsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-manager', 'jobs');
|
|
170
|
+
if (!(0, fs_1.existsSync)(managerJobsDir)) {
|
|
171
|
+
(0, fs_1.mkdirSync)(managerJobsDir, { recursive: true });
|
|
172
|
+
}
|
|
173
|
+
cleanDirectory(managerJobsDir);
|
|
174
|
+
for (const file of managerJobFiles) {
|
|
175
|
+
// Strip "jobs/" prefix and "ai-manager/" role prefix
|
|
176
|
+
let relPath = file.path;
|
|
177
|
+
if (relPath.startsWith('jobs/'))
|
|
178
|
+
relPath = relPath.substring('jobs/'.length);
|
|
179
|
+
relPath = relPath.replace(/^ai-manager\//, '');
|
|
180
|
+
const filePath = (0, path_1.join)(managerJobsDir, relPath);
|
|
181
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
182
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
183
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
186
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-manager/jobs/${relPath}`)}`));
|
|
187
|
+
}
|
|
188
|
+
// Sync skill STUBS to fraim/ai-employee/skills/
|
|
189
|
+
const skillFiles = files.filter(f => f.type === 'skill');
|
|
190
|
+
const skillsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'skills');
|
|
191
|
+
if (!(0, fs_1.existsSync)(skillsDir)) {
|
|
192
|
+
(0, fs_1.mkdirSync)(skillsDir, { recursive: true });
|
|
193
|
+
}
|
|
194
|
+
cleanDirectory(skillsDir);
|
|
195
|
+
for (const file of skillFiles) {
|
|
196
|
+
// Strip "skills/" prefix to avoid redundant nesting in fraim/ai-employee/skills/
|
|
197
|
+
let relPath = file.path;
|
|
198
|
+
if (relPath.startsWith('skills/'))
|
|
199
|
+
relPath = relPath.substring('skills/'.length);
|
|
200
|
+
const filePath = (0, path_1.join)(skillsDir, relPath);
|
|
201
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
202
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
203
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
204
|
+
}
|
|
205
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
206
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/skills/${relPath}`)} (stub)`));
|
|
207
|
+
}
|
|
208
|
+
// Sync rule STUBS to fraim/ai-employee/rules/
|
|
209
|
+
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
210
|
+
const rulesDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'ai-employee', 'rules');
|
|
211
|
+
if (!(0, fs_1.existsSync)(rulesDir)) {
|
|
212
|
+
(0, fs_1.mkdirSync)(rulesDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
cleanDirectory(rulesDir);
|
|
215
|
+
for (const file of ruleFiles) {
|
|
216
|
+
// Strip "rules/" prefix to avoid redundant nesting in fraim/ai-employee/rules/
|
|
217
|
+
let relPath = file.path;
|
|
218
|
+
if (relPath.startsWith('rules/'))
|
|
219
|
+
relPath = relPath.substring('rules/'.length);
|
|
220
|
+
const filePath = (0, path_1.join)(rulesDir, relPath);
|
|
221
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
222
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
223
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
226
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`ai-employee/rules/${relPath}`)} (stub)`));
|
|
227
|
+
}
|
|
228
|
+
// Sync scripts to user directory
|
|
229
|
+
const scriptFiles = files.filter(f => f.type === 'script');
|
|
230
|
+
const userDir = (0, script_sync_utils_1.getUserFraimDir)();
|
|
231
|
+
const scriptsDir = (0, path_1.join)(userDir, 'scripts');
|
|
232
|
+
if (!(0, fs_1.existsSync)(scriptsDir)) {
|
|
233
|
+
(0, fs_1.mkdirSync)(scriptsDir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
// Clean existing scripts
|
|
236
|
+
cleanDirectory(scriptsDir);
|
|
237
|
+
// Write script files
|
|
238
|
+
for (const file of scriptFiles) {
|
|
239
|
+
const filePath = (0, path_1.join)(scriptsDir, file.path);
|
|
240
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
241
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
242
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
245
|
+
console.log(chalk_1.default.gray(` + ${file.path}`));
|
|
246
|
+
}
|
|
247
|
+
// Sync docs to fraim/docs/
|
|
248
|
+
const docsFiles = files.filter(f => f.type === 'docs');
|
|
249
|
+
const docsDir = (0, project_fraim_paths_1.getWorkspaceFraimPath)(options.projectRoot, 'docs');
|
|
250
|
+
if (!(0, fs_1.existsSync)(docsDir)) {
|
|
251
|
+
(0, fs_1.mkdirSync)(docsDir, { recursive: true });
|
|
252
|
+
}
|
|
253
|
+
cleanDirectory(docsDir);
|
|
254
|
+
for (const file of docsFiles) {
|
|
255
|
+
const filePath = (0, path_1.join)(docsDir, file.path);
|
|
256
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
257
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
258
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
259
|
+
}
|
|
260
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
261
|
+
console.log(chalk_1.default.gray(` + ${(0, project_fraim_paths_1.getWorkspaceFraimDisplayPath)(`docs/${file.path}`)}`));
|
|
262
|
+
}
|
|
263
|
+
if (shouldLockSyncedContent()) {
|
|
264
|
+
for (const target of lockTargets) {
|
|
265
|
+
setFileWriteLockRecursively(target, true);
|
|
266
|
+
}
|
|
267
|
+
console.log(chalk_1.default.gray(` 🔒 Synced FRAIM content locked as read-only (set ${LOCK_SYNCED_CONTENT_ENV}=false to disable)`));
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
employeeJobsSynced: jobFiles.length,
|
|
272
|
+
managerJobsSynced: managerJobFiles.length,
|
|
273
|
+
skillsSynced: skillFiles.length,
|
|
274
|
+
rulesSynced: ruleFiles.length,
|
|
275
|
+
scriptsSynced: scriptFiles.length,
|
|
276
|
+
docsSynced: docsFiles.length
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
console.error(chalk_1.default.red(`❌ Remote sync failed: ${error.message}`));
|
|
281
|
+
return {
|
|
282
|
+
success: false,
|
|
283
|
+
employeeJobsSynced: 0,
|
|
284
|
+
managerJobsSynced: 0,
|
|
285
|
+
skillsSynced: 0,
|
|
286
|
+
rulesSynced: 0,
|
|
287
|
+
scriptsSynced: 0,
|
|
288
|
+
docsSynced: 0,
|
|
289
|
+
error: error.message
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Clean directory contents (but keep the directory itself)
|
|
295
|
+
*/
|
|
296
|
+
function cleanDirectory(dirPath) {
|
|
297
|
+
if (!(0, fs_1.existsSync)(dirPath))
|
|
298
|
+
return;
|
|
299
|
+
const entries = (0, fs_1.readdirSync)(dirPath, { withFileTypes: true });
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
const fullPath = (0, path_1.join)(dirPath, entry.name);
|
|
302
|
+
if (entry.isDirectory()) {
|
|
303
|
+
cleanDirectory(fullPath);
|
|
304
|
+
try {
|
|
305
|
+
(0, fs_1.rmdirSync)(fullPath);
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
// Directory not empty, skip
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
(0, fs_1.unlinkSync)(fullPath);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|