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.
Files changed (70) hide show
  1. package/README.md +445 -0
  2. package/bin/fraim.js +23 -0
  3. package/dist/src/cli/api/get-provider-client.js +41 -0
  4. package/dist/src/cli/api/provider-client.js +107 -0
  5. package/dist/src/cli/commands/add-ide.js +430 -0
  6. package/dist/src/cli/commands/add-provider.js +233 -0
  7. package/dist/src/cli/commands/doctor.js +149 -0
  8. package/dist/src/cli/commands/init-project.js +301 -0
  9. package/dist/src/cli/commands/list-overridable.js +184 -0
  10. package/dist/src/cli/commands/list.js +57 -0
  11. package/dist/src/cli/commands/login.js +84 -0
  12. package/dist/src/cli/commands/mcp.js +15 -0
  13. package/dist/src/cli/commands/migrate-project-fraim.js +42 -0
  14. package/dist/src/cli/commands/override.js +177 -0
  15. package/dist/src/cli/commands/setup.js +651 -0
  16. package/dist/src/cli/commands/sync.js +162 -0
  17. package/dist/src/cli/commands/test-mcp.js +171 -0
  18. package/dist/src/cli/doctor/check-runner.js +199 -0
  19. package/dist/src/cli/doctor/checks/global-setup-checks.js +220 -0
  20. package/dist/src/cli/doctor/checks/ide-config-checks.js +250 -0
  21. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +381 -0
  22. package/dist/src/cli/doctor/checks/project-setup-checks.js +282 -0
  23. package/dist/src/cli/doctor/checks/scripts-checks.js +157 -0
  24. package/dist/src/cli/doctor/checks/workflow-checks.js +251 -0
  25. package/dist/src/cli/doctor/reporters/console-reporter.js +96 -0
  26. package/dist/src/cli/doctor/reporters/json-reporter.js +11 -0
  27. package/dist/src/cli/doctor/types.js +6 -0
  28. package/dist/src/cli/fraim.js +100 -0
  29. package/dist/src/cli/internal/device-flow-service.js +83 -0
  30. package/dist/src/cli/mcp/ide-formats.js +243 -0
  31. package/dist/src/cli/mcp/mcp-server-builder.js +48 -0
  32. package/dist/src/cli/mcp/mcp-server-registry.js +160 -0
  33. package/dist/src/cli/mcp/types.js +3 -0
  34. package/dist/src/cli/providers/local-provider-registry.js +166 -0
  35. package/dist/src/cli/providers/provider-registry.js +230 -0
  36. package/dist/src/cli/setup/auto-mcp-setup.js +331 -0
  37. package/dist/src/cli/setup/codex-local-config.js +37 -0
  38. package/dist/src/cli/setup/first-run.js +242 -0
  39. package/dist/src/cli/setup/ide-detector.js +179 -0
  40. package/dist/src/cli/setup/mcp-config-generator.js +192 -0
  41. package/dist/src/cli/setup/provider-prompts.js +339 -0
  42. package/dist/src/cli/utils/agent-adapters.js +126 -0
  43. package/dist/src/cli/utils/digest-utils.js +47 -0
  44. package/dist/src/cli/utils/fraim-gitignore.js +40 -0
  45. package/dist/src/cli/utils/platform-detection.js +258 -0
  46. package/dist/src/cli/utils/project-bootstrap.js +93 -0
  47. package/dist/src/cli/utils/remote-sync.js +315 -0
  48. package/dist/src/cli/utils/script-sync-utils.js +221 -0
  49. package/dist/src/cli/utils/version-utils.js +32 -0
  50. package/dist/src/core/ai-mentor.js +230 -0
  51. package/dist/src/core/config-loader.js +114 -0
  52. package/dist/src/core/config-writer.js +75 -0
  53. package/dist/src/core/types.js +23 -0
  54. package/dist/src/core/utils/git-utils.js +95 -0
  55. package/dist/src/core/utils/include-resolver.js +92 -0
  56. package/dist/src/core/utils/inheritance-parser.js +288 -0
  57. package/dist/src/core/utils/job-parser.js +176 -0
  58. package/dist/src/core/utils/local-registry-resolver.js +616 -0
  59. package/dist/src/core/utils/object-utils.js +11 -0
  60. package/dist/src/core/utils/project-fraim-migration.js +103 -0
  61. package/dist/src/core/utils/project-fraim-paths.js +38 -0
  62. package/dist/src/core/utils/provider-utils.js +18 -0
  63. package/dist/src/core/utils/server-startup.js +34 -0
  64. package/dist/src/core/utils/stub-generator.js +147 -0
  65. package/dist/src/core/utils/workflow-parser.js +174 -0
  66. package/dist/src/local-mcp-server/learning-context-builder.js +229 -0
  67. package/dist/src/local-mcp-server/stdio-server.js +1698 -0
  68. package/dist/src/local-mcp-server/usage-collector.js +264 -0
  69. package/index.js +85 -0
  70. 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
+ }