fraim-framework 2.0.86 → 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
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * FRAIM Framework CLI Entry Point
@@ -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.content, 'utf8');
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.content, 'utf8');
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.content, 'utf8');
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.content, 'utf8');
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.86",
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"