fraim-framework 2.0.86 ā 2.0.88
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 +30 -0
- package/bin/fraim.js +1 -1
- package/dist/src/cli/commands/add-provider.js +16 -6
- package/dist/src/cli/commands/init-project.js +103 -1
- package/dist/src/cli/commands/login.js +84 -0
- package/dist/src/cli/commands/setup.js +135 -13
- package/dist/src/cli/fraim.js +2 -0
- package/dist/src/cli/internal/device-flow-service.js +83 -0
- package/dist/src/cli/mcp/mcp-server-registry.js +11 -10
- package/dist/src/cli/providers/local-provider-registry.js +22 -1
- package/dist/src/cli/services/device-flow-service.js +83 -0
- package/dist/src/cli/setup/provider-prompts.js +39 -0
- package/dist/src/cli/utils/remote-sync.js +159 -28
- package/dist/src/core/ai-mentor.js +248 -0
- package/dist/src/core/utils/git-utils.js +6 -6
- package/dist/src/core/utils/include-resolver.js +45 -0
- package/dist/src/core/utils/inheritance-parser.js +154 -16
- package/dist/src/core/utils/local-registry-resolver.js +326 -22
- package/dist/src/core/utils/server-startup.js +34 -0
- package/dist/src/core/utils/stub-generator.js +34 -27
- package/dist/src/core/utils/workflow-parser.js +32 -2
- package/dist/src/local-mcp-server/stdio-server.js +240 -284
- package/index.js +26 -5
- package/package.json +15 -5
|
@@ -51,7 +51,15 @@ const LOCAL_PROVIDERS = [
|
|
|
51
51
|
capabilities: ['code', 'issues', 'integrated'],
|
|
52
52
|
docsUrl: 'https://docs.microsoft.com/azure/devops',
|
|
53
53
|
setupInstructions: 'Create a Personal Access Token in Azure DevOps',
|
|
54
|
-
hasAdditionalConfig:
|
|
54
|
+
hasAdditionalConfig: true,
|
|
55
|
+
mcpServer: {
|
|
56
|
+
type: 'stdio',
|
|
57
|
+
command: 'npx',
|
|
58
|
+
args: ['-y', '@azure-devops/mcp', '{config.organization}', '--authentication', 'envvar'],
|
|
59
|
+
envTemplate: {
|
|
60
|
+
ADO_MCP_AUTH_TOKEN: '{token}'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
55
63
|
},
|
|
56
64
|
{
|
|
57
65
|
id: 'jira',
|
|
@@ -73,6 +81,16 @@ const LOCAL_PROVIDERS = [
|
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
];
|
|
84
|
+
const ADO_CONFIG_REQUIREMENTS = [
|
|
85
|
+
{
|
|
86
|
+
key: 'organization',
|
|
87
|
+
displayName: 'Azure DevOps Organization',
|
|
88
|
+
description: 'Your Azure DevOps organization name (e.g., contoso)',
|
|
89
|
+
required: true,
|
|
90
|
+
type: 'string',
|
|
91
|
+
cliOptionName: 'organization'
|
|
92
|
+
}
|
|
93
|
+
];
|
|
76
94
|
const JIRA_CONFIG_REQUIREMENTS = [
|
|
77
95
|
{
|
|
78
96
|
key: 'baseUrl',
|
|
@@ -111,6 +129,9 @@ function getLocalProviderSetupInstructions(providerId) {
|
|
|
111
129
|
return provider?.setupInstructions || '';
|
|
112
130
|
}
|
|
113
131
|
function getLocalProviderConfigRequirements(providerId) {
|
|
132
|
+
if (providerId === 'ado') {
|
|
133
|
+
return ADO_CONFIG_REQUIREMENTS;
|
|
134
|
+
}
|
|
114
135
|
if (providerId === 'jira') {
|
|
115
136
|
return JIRA_CONFIG_REQUIREMENTS;
|
|
116
137
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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.DeviceFlowService = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
9
|
+
class DeviceFlowService {
|
|
10
|
+
constructor(config) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Start the Device Flow Login
|
|
15
|
+
*/
|
|
16
|
+
async login() {
|
|
17
|
+
console.log(chalk_1.default.blue('\nš Starting Authentication...'));
|
|
18
|
+
try {
|
|
19
|
+
// 1. Request device and user codes
|
|
20
|
+
const deviceCode = await this.requestDeviceCode();
|
|
21
|
+
console.log(chalk_1.default.yellow('\nACTION REQUIRED:'));
|
|
22
|
+
console.log(`1. Go to: ${chalk_1.default.cyan.underline(deviceCode.verification_uri)}`);
|
|
23
|
+
console.log(`2. Enter the code: ${chalk_1.default.bold.green(deviceCode.user_code)}`);
|
|
24
|
+
console.log(chalk_1.default.gray(`\nWaiting for authorization (expires in ${Math.floor(deviceCode.expires_in / 60)} minutes)...`));
|
|
25
|
+
// 2. Poll for the access token
|
|
26
|
+
const token = await this.pollForToken(deviceCode.device_code, deviceCode.interval);
|
|
27
|
+
console.log(chalk_1.default.green('\nā
Authentication Successful!'));
|
|
28
|
+
return token;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.error(chalk_1.default.red(`\nā Authentication failed: ${error.message}`));
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async requestDeviceCode() {
|
|
36
|
+
const response = await axios_1.default.post(this.config.authUrl, {
|
|
37
|
+
client_id: this.config.clientId,
|
|
38
|
+
scope: this.config.scope
|
|
39
|
+
}, {
|
|
40
|
+
headers: { Accept: 'application/json' }
|
|
41
|
+
});
|
|
42
|
+
return response.data;
|
|
43
|
+
}
|
|
44
|
+
async pollForToken(deviceCode, interval) {
|
|
45
|
+
let currentInterval = interval * 1000;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const poll = async () => {
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios_1.default.post(this.config.tokenUrl, {
|
|
50
|
+
client_id: this.config.clientId,
|
|
51
|
+
device_code: deviceCode,
|
|
52
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code'
|
|
53
|
+
}, {
|
|
54
|
+
headers: { Accept: 'application/json' }
|
|
55
|
+
});
|
|
56
|
+
if (response.data.access_token) {
|
|
57
|
+
resolve(response.data.access_token);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (response.data.error) {
|
|
61
|
+
const error = response.data.error;
|
|
62
|
+
if (error === 'authorization_pending') {
|
|
63
|
+
// Keep polling
|
|
64
|
+
setTimeout(poll, currentInterval);
|
|
65
|
+
}
|
|
66
|
+
else if (error === 'slow_down') {
|
|
67
|
+
currentInterval += 5000;
|
|
68
|
+
setTimeout(poll, currentInterval);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
reject(new Error(response.data.error_description || error));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
reject(error);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
setTimeout(poll, currentInterval);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.DeviceFlowService = DeviceFlowService;
|
|
@@ -60,6 +60,10 @@ async function promptForProviders(client, preselectedIds) {
|
|
|
60
60
|
value: provider.id,
|
|
61
61
|
selected: preselectedIds?.includes(provider.id) ?? provider.id === defaultProviderId
|
|
62
62
|
}));
|
|
63
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
64
|
+
console.log(chalk_1.default.yellow(`\nā¹ļø Non-interactive mode: defaulting to ${integratedProviders[0]?.displayName || 'first available provider'}`));
|
|
65
|
+
return [defaultProviderId];
|
|
66
|
+
}
|
|
63
67
|
const response = await (0, prompts_1.default)({
|
|
64
68
|
type: 'multiselect',
|
|
65
69
|
name: 'providers',
|
|
@@ -109,6 +113,10 @@ async function promptForSingleProvider(client, purpose, availableIds) {
|
|
|
109
113
|
title: provider.displayName,
|
|
110
114
|
value: provider.id
|
|
111
115
|
}));
|
|
116
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
117
|
+
console.log(chalk_1.default.yellow(`\nā¹ļø Non-interactive mode: defaulting to ${providers[0]?.displayName || 'first available provider'}`));
|
|
118
|
+
return defaultProviderId;
|
|
119
|
+
}
|
|
112
120
|
const response = await (0, prompts_1.default)({
|
|
113
121
|
type: 'select',
|
|
114
122
|
name: 'provider',
|
|
@@ -145,6 +153,9 @@ async function promptForProviderToken(client, providerId) {
|
|
|
145
153
|
}
|
|
146
154
|
console.log(chalk_1.default.blue(`\nš§ ${provider.displayName} Integration Setup`));
|
|
147
155
|
console.log(`FRAIM requires a ${provider.displayName} token for integration.\n`);
|
|
156
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
157
|
+
throw new Error(`Non-interactive mode: ${provider.displayName} token is missing and cannot prompt.`);
|
|
158
|
+
}
|
|
148
159
|
const hasToken = await (0, prompts_1.default)({
|
|
149
160
|
type: 'confirm',
|
|
150
161
|
name: 'hasToken',
|
|
@@ -152,6 +163,27 @@ async function promptForProviderToken(client, providerId) {
|
|
|
152
163
|
initial: false
|
|
153
164
|
});
|
|
154
165
|
if (!hasToken.hasToken) {
|
|
166
|
+
if (providerId === 'github') {
|
|
167
|
+
const loginChoice = await (0, prompts_1.default)({
|
|
168
|
+
type: 'confirm',
|
|
169
|
+
name: 'login',
|
|
170
|
+
message: `Would you like to login to ${provider.displayName} now using your browser? (Recommended)`,
|
|
171
|
+
initial: true
|
|
172
|
+
});
|
|
173
|
+
if (loginChoice.login) {
|
|
174
|
+
const { DeviceFlowService } = await Promise.resolve().then(() => __importStar(require('../internal/device-flow-service')));
|
|
175
|
+
if (!provider.deviceFlowConfig) {
|
|
176
|
+
throw new Error(`Device flow configuration not found for provider: ${providerId}`);
|
|
177
|
+
}
|
|
178
|
+
const deviceFlow = new DeviceFlowService(provider.deviceFlowConfig);
|
|
179
|
+
try {
|
|
180
|
+
return await deviceFlow.login();
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
console.log(chalk_1.default.yellow('\nBrowser login failed or was cancelled. Fallback to manual token entry.'));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
155
187
|
console.log(chalk_1.default.yellow(`\nš To create a ${provider.displayName} token:`));
|
|
156
188
|
console.log(chalk_1.default.gray(` ${provider.setupInstructions}`));
|
|
157
189
|
console.log(chalk_1.default.gray(` Visit: ${provider.docsUrl}\n`));
|
|
@@ -225,6 +257,13 @@ async function promptForProviderConfig(client, providerId) {
|
|
|
225
257
|
console.log(`Additional configuration required for ${provider.displayName}.\n`);
|
|
226
258
|
const config = {};
|
|
227
259
|
for (const req of requirements) {
|
|
260
|
+
if (process.env.FRAIM_NON_INTERACTIVE) {
|
|
261
|
+
if (req.required) {
|
|
262
|
+
throw new Error(`Non-interactive mode: Required configuration "${req.displayName}" for ${provider.displayName} is missing.`);
|
|
263
|
+
}
|
|
264
|
+
console.log(chalk_1.default.yellow(`\nā¹ļø Non-interactive mode: skipping optional configuration "${req.displayName}"`));
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
228
267
|
const response = await (0, prompts_1.default)({
|
|
229
268
|
type: req.type === 'email' ? 'text' : req.type === 'url' ? 'text' : 'text',
|
|
230
269
|
name: 'value',
|
|
@@ -17,6 +17,81 @@ 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) {
|
|
82
|
+
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`;
|
|
83
|
+
}
|
|
84
|
+
function applySyncedContentBanner(file) {
|
|
85
|
+
const registryPath = getBannerRegistryPath(file);
|
|
86
|
+
if (!registryPath) {
|
|
87
|
+
return file.content;
|
|
88
|
+
}
|
|
89
|
+
const typeLabel = file.type === 'job' || file.type === 'skill' || file.type === 'rule'
|
|
90
|
+
? `${file.type} stub`
|
|
91
|
+
: `${file.type} file`;
|
|
92
|
+
const banner = buildSyncedContentBanner(typeLabel);
|
|
93
|
+
return insertAfterFrontmatter(file.content, banner);
|
|
94
|
+
}
|
|
20
95
|
/**
|
|
21
96
|
* Sync workflows and scripts from remote FRAIM server
|
|
22
97
|
*/
|
|
@@ -61,23 +136,56 @@ async function syncFromRemote(options) {
|
|
|
61
136
|
error: 'No files received'
|
|
62
137
|
};
|
|
63
138
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
139
|
+
const lockTargets = getSyncedContentLockTargets(options.projectRoot);
|
|
140
|
+
if (shouldLockSyncedContent()) {
|
|
141
|
+
// If previous sync locked these paths read-only, temporarily unlock before cleanup/write.
|
|
142
|
+
for (const target of lockTargets) {
|
|
143
|
+
setFileWriteLockRecursively(target, false);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Sync workflows to role-specific folders under .fraim
|
|
147
|
+
const allWorkflowFiles = files.filter(f => f.type === 'workflow');
|
|
148
|
+
const managerWorkflowFiles = allWorkflowFiles.filter(f => f.path.startsWith('ai-manager/'));
|
|
149
|
+
const employeeWorkflowFiles = allWorkflowFiles.filter(f => !f.path.startsWith('ai-manager/'));
|
|
150
|
+
// Write employee workflows
|
|
151
|
+
const employeeWorkflowsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'workflows');
|
|
152
|
+
if (!(0, fs_1.existsSync)(employeeWorkflowsDir)) {
|
|
153
|
+
(0, fs_1.mkdirSync)(employeeWorkflowsDir, { recursive: true });
|
|
154
|
+
}
|
|
155
|
+
cleanDirectory(employeeWorkflowsDir);
|
|
156
|
+
for (const file of employeeWorkflowFiles) {
|
|
157
|
+
// Strip "workflows/" prefix and "ai-employee/" role prefix for cleaner local layout
|
|
158
|
+
let relPath = file.path;
|
|
159
|
+
if (relPath.startsWith('workflows/'))
|
|
160
|
+
relPath = relPath.substring('workflows/'.length);
|
|
161
|
+
relPath = relPath.replace(/^ai-employee\//, '');
|
|
162
|
+
const filePath = (0, path_1.join)(employeeWorkflowsDir, relPath);
|
|
75
163
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
76
164
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
77
165
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
78
166
|
}
|
|
79
167
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
80
|
-
console.log(chalk_1.default.gray(` +
|
|
168
|
+
console.log(chalk_1.default.gray(` + .fraim/ai-employee/workflows/${relPath}`));
|
|
169
|
+
}
|
|
170
|
+
// Write manager workflows
|
|
171
|
+
const managerWorkflowsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'workflows');
|
|
172
|
+
if (!(0, fs_1.existsSync)(managerWorkflowsDir)) {
|
|
173
|
+
(0, fs_1.mkdirSync)(managerWorkflowsDir, { recursive: true });
|
|
174
|
+
}
|
|
175
|
+
cleanDirectory(managerWorkflowsDir);
|
|
176
|
+
for (const file of managerWorkflowFiles) {
|
|
177
|
+
// Strip "workflows/" prefix and "ai-manager/" role prefix
|
|
178
|
+
let relPath = file.path;
|
|
179
|
+
if (relPath.startsWith('workflows/'))
|
|
180
|
+
relPath = relPath.substring('workflows/'.length);
|
|
181
|
+
relPath = relPath.replace(/^ai-manager\//, '');
|
|
182
|
+
const filePath = (0, path_1.join)(managerWorkflowsDir, relPath);
|
|
183
|
+
const fileDir = (0, path_1.dirname)(filePath);
|
|
184
|
+
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
185
|
+
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
186
|
+
}
|
|
187
|
+
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
188
|
+
console.log(chalk_1.default.gray(` + .fraim/ai-manager/workflows/${relPath}`));
|
|
81
189
|
}
|
|
82
190
|
// Sync job stubs to role-specific folders under .fraim
|
|
83
191
|
const allJobFiles = files.filter(f => f.type === 'job');
|
|
@@ -89,13 +197,18 @@ async function syncFromRemote(options) {
|
|
|
89
197
|
}
|
|
90
198
|
cleanDirectory(employeeJobsDir);
|
|
91
199
|
for (const file of jobFiles) {
|
|
92
|
-
|
|
200
|
+
// Strip "jobs/" prefix and "ai-employee/" role prefix
|
|
201
|
+
let relPath = file.path;
|
|
202
|
+
if (relPath.startsWith('jobs/'))
|
|
203
|
+
relPath = relPath.substring('jobs/'.length);
|
|
204
|
+
relPath = relPath.replace(/^ai-employee\//, '');
|
|
205
|
+
const filePath = (0, path_1.join)(employeeJobsDir, relPath);
|
|
93
206
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
94
207
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
95
208
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
96
209
|
}
|
|
97
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
98
|
-
console.log(chalk_1.default.gray(` + ai-employee/jobs/${
|
|
210
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
211
|
+
console.log(chalk_1.default.gray(` + ai-employee/jobs/${relPath}`));
|
|
99
212
|
}
|
|
100
213
|
// Sync ai-manager job stubs to .fraim/ai-manager/jobs/
|
|
101
214
|
const managerJobsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-manager', 'jobs');
|
|
@@ -104,16 +217,20 @@ async function syncFromRemote(options) {
|
|
|
104
217
|
}
|
|
105
218
|
cleanDirectory(managerJobsDir);
|
|
106
219
|
for (const file of managerJobFiles) {
|
|
107
|
-
|
|
108
|
-
|
|
220
|
+
// Strip "jobs/" prefix and "ai-manager/" role prefix
|
|
221
|
+
let relPath = file.path;
|
|
222
|
+
if (relPath.startsWith('jobs/'))
|
|
223
|
+
relPath = relPath.substring('jobs/'.length);
|
|
224
|
+
relPath = relPath.replace(/^ai-manager\//, '');
|
|
225
|
+
const filePath = (0, path_1.join)(managerJobsDir, relPath);
|
|
109
226
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
110
227
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
111
228
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
112
229
|
}
|
|
113
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
114
|
-
console.log(chalk_1.default.gray(` + ai-manager/jobs/${
|
|
230
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
231
|
+
console.log(chalk_1.default.gray(` + .fraim/ai-manager/jobs/${relPath}`));
|
|
115
232
|
}
|
|
116
|
-
// Sync
|
|
233
|
+
// Sync skill STUBS to .fraim/ai-employee/skills/
|
|
117
234
|
const skillFiles = files.filter(f => f.type === 'skill');
|
|
118
235
|
const skillsDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'skills');
|
|
119
236
|
if (!(0, fs_1.existsSync)(skillsDir)) {
|
|
@@ -121,15 +238,19 @@ async function syncFromRemote(options) {
|
|
|
121
238
|
}
|
|
122
239
|
cleanDirectory(skillsDir);
|
|
123
240
|
for (const file of skillFiles) {
|
|
124
|
-
|
|
241
|
+
// Strip "skills/" prefix to avoid redundant nesting in .fraim/ai-employee/skills/
|
|
242
|
+
let relPath = file.path;
|
|
243
|
+
if (relPath.startsWith('skills/'))
|
|
244
|
+
relPath = relPath.substring('skills/'.length);
|
|
245
|
+
const filePath = (0, path_1.join)(skillsDir, relPath);
|
|
125
246
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
126
247
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
127
248
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
128
249
|
}
|
|
129
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
130
|
-
console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path}`));
|
|
250
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
251
|
+
console.log(chalk_1.default.gray(` + ai-employee/skills/${file.path} (stub)`));
|
|
131
252
|
}
|
|
132
|
-
// Sync
|
|
253
|
+
// Sync rule STUBS to .fraim/ai-employee/rules/
|
|
133
254
|
const ruleFiles = files.filter(f => f.type === 'rule');
|
|
134
255
|
const rulesDir = (0, path_1.join)(options.projectRoot, '.fraim', 'ai-employee', 'rules');
|
|
135
256
|
if (!(0, fs_1.existsSync)(rulesDir)) {
|
|
@@ -137,13 +258,17 @@ async function syncFromRemote(options) {
|
|
|
137
258
|
}
|
|
138
259
|
cleanDirectory(rulesDir);
|
|
139
260
|
for (const file of ruleFiles) {
|
|
140
|
-
|
|
261
|
+
// Strip "rules/" prefix to avoid redundant nesting in .fraim/ai-employee/rules/
|
|
262
|
+
let relPath = file.path;
|
|
263
|
+
if (relPath.startsWith('rules/'))
|
|
264
|
+
relPath = relPath.substring('rules/'.length);
|
|
265
|
+
const filePath = (0, path_1.join)(rulesDir, relPath);
|
|
141
266
|
const fileDir = (0, path_1.dirname)(filePath);
|
|
142
267
|
if (!(0, fs_1.existsSync)(fileDir)) {
|
|
143
268
|
(0, fs_1.mkdirSync)(fileDir, { recursive: true });
|
|
144
269
|
}
|
|
145
|
-
(0, fs_1.writeFileSync)(filePath, file
|
|
146
|
-
console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path}`));
|
|
270
|
+
(0, fs_1.writeFileSync)(filePath, applySyncedContentBanner(file), 'utf8');
|
|
271
|
+
console.log(chalk_1.default.gray(` + ai-employee/rules/${file.path} (stub)`));
|
|
147
272
|
}
|
|
148
273
|
// Sync scripts to user directory
|
|
149
274
|
const scriptFiles = files.filter(f => f.type === 'script');
|
|
@@ -180,9 +305,15 @@ async function syncFromRemote(options) {
|
|
|
180
305
|
(0, fs_1.writeFileSync)(filePath, file.content, 'utf8');
|
|
181
306
|
console.log(chalk_1.default.gray(` + docs/${file.path}`));
|
|
182
307
|
}
|
|
308
|
+
if (shouldLockSyncedContent()) {
|
|
309
|
+
for (const target of lockTargets) {
|
|
310
|
+
setFileWriteLockRecursively(target, true);
|
|
311
|
+
}
|
|
312
|
+
console.log(chalk_1.default.gray(` š Synced FRAIM content locked as read-only (set ${LOCK_SYNCED_CONTENT_ENV}=false to disable)`));
|
|
313
|
+
}
|
|
183
314
|
return {
|
|
184
315
|
success: true,
|
|
185
|
-
workflowsSynced:
|
|
316
|
+
workflowsSynced: allWorkflowFiles.length,
|
|
186
317
|
employeeJobsSynced: jobFiles.length,
|
|
187
318
|
managerJobsSynced: managerJobFiles.length,
|
|
188
319
|
skillsSynced: skillFiles.length,
|