fraim 2.0.248 → 2.0.249
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/dist/src/cli/commands/manager.js +26 -7
- package/dist/src/cli/commands/org.js +26 -7
- package/dist/src/cli/commands/sync.js +34 -0
- package/dist/src/cli/utils/local-folder-sync.js +100 -48
- package/dist/src/cli/utils/manager-pack-sync.js +77 -40
- package/dist/src/cli/utils/manager-publish.js +38 -40
- package/dist/src/cli/utils/org-pack-sync.js +80 -41
- package/dist/src/cli/utils/org-publish.js +37 -40
- package/dist/src/cli/utils/pack-git-publish.js +248 -0
- package/dist/src/cli/utils/pack-home.js +279 -0
- package/dist/src/cli/utils/user-config.js +10 -2
- package/dist/src/core/capability-pack.js +23 -1
- package/dist/src/core/manager-pack.js +21 -5
- package/dist/src/local-mcp-server/learning-context-builder.js +29 -8
- package/dist/src/local-mcp-server/stdio-server.js +12 -4
- package/package.json +1 -1
|
@@ -12,25 +12,44 @@ exports.managerCommand = new commander_1.Command('manager')
|
|
|
12
12
|
.description('Manage your portable personal manager context');
|
|
13
13
|
exports.managerCommand
|
|
14
14
|
.command('publish')
|
|
15
|
-
.description('Publish manager context
|
|
16
|
-
.argument('<files...>', '
|
|
17
|
-
.
|
|
15
|
+
.description('Publish manager context, rules, learnings, capabilities, or employees to the configured manager backend')
|
|
16
|
+
.argument('<files...>', 'Files to publish: manager_context.md, manager_rules.md, personal learning files, or pack-relative capability paths such as jobs/<cat>/<name>.md and employees/<slug>.json')
|
|
17
|
+
.option('--remove <paths...>', 'Pack-relative paths to remove in the same change (the source side of a move)')
|
|
18
|
+
.action(async (files, options) => {
|
|
18
19
|
try {
|
|
19
20
|
const artifacts = files.map((filePath) => {
|
|
20
21
|
if (!fs_1.default.existsSync(filePath))
|
|
21
22
|
throw new Error(`File not found: ${filePath}`);
|
|
22
23
|
return { relativePath: (0, manager_publish_1.managerPackRelativePathFor)(filePath), content: fs_1.default.readFileSync(filePath, 'utf8') };
|
|
23
24
|
});
|
|
24
|
-
const
|
|
25
|
+
const deletions = (options.remove ?? []).map(manager_publish_1.managerPackRelativePathFor);
|
|
26
|
+
const result = await (0, manager_publish_1.publishManagerArtifacts)(artifacts, { deletions });
|
|
25
27
|
if (result.backend === 'fraim-cloud') {
|
|
26
28
|
console.log(chalk_1.default.green(`Published ${artifacts.length} manager artifact(s) to your FRAIM account (version ${result.version}).`));
|
|
27
29
|
console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (result.backend === 'local-folder') {
|
|
33
|
+
console.log(chalk_1.default.green(`Wrote ${artifacts.length} manager artifact(s) to your synced folder.`));
|
|
34
|
+
for (const p of result.writtenPaths ?? [])
|
|
35
|
+
console.log(chalk_1.default.gray(` ${p}`));
|
|
36
|
+
if (result.deletedPaths?.length) {
|
|
37
|
+
console.log(chalk_1.default.green(`Removed ${result.deletedPaths.length} source file(s).`));
|
|
38
|
+
}
|
|
39
|
+
console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (result.credentialWarning)
|
|
43
|
+
console.log(chalk_1.default.yellow(result.credentialWarning));
|
|
44
|
+
console.log(chalk_1.default.green(`Pushed ${artifacts.length} manager artifact(s) to branch "${result.branch}".`));
|
|
45
|
+
if (result.prCreated) {
|
|
46
|
+
console.log(chalk_1.default.cyan(`Opened the pull request: ${result.prUrl}`));
|
|
47
|
+
console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
|
|
28
48
|
}
|
|
29
49
|
else {
|
|
30
|
-
console.log(chalk_1.default.
|
|
50
|
+
console.log(chalk_1.default.yellow(`No pull request was opened. ${result.prBlockedReason ?? ''}`.trim()));
|
|
31
51
|
if (result.prUrl)
|
|
32
|
-
console.log(chalk_1.default.cyan(`Open
|
|
33
|
-
console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
|
|
52
|
+
console.log(chalk_1.default.cyan(`Open one here: ${result.prUrl}`));
|
|
34
53
|
}
|
|
35
54
|
}
|
|
36
55
|
catch (error) {
|
|
@@ -12,25 +12,44 @@ exports.orgCommand = new commander_1.Command('org')
|
|
|
12
12
|
.description('Manage your shared organization context');
|
|
13
13
|
exports.orgCommand
|
|
14
14
|
.command('publish')
|
|
15
|
-
.description('Publish org context
|
|
16
|
-
.argument('<files...>', '
|
|
17
|
-
.
|
|
15
|
+
.description('Publish org context, rules, learnings, brand, capabilities, or employees to the configured org backend')
|
|
16
|
+
.argument('<files...>', 'Files to publish: org_context.md, org_rules.md, org_brand.json, org-*.md learnings, or pack-relative capability paths such as jobs/<cat>/<name>.md and employees/<slug>.json')
|
|
17
|
+
.option('--remove <paths...>', 'Pack-relative paths to remove in the same change (the source side of a move)')
|
|
18
|
+
.action(async (files, options) => {
|
|
18
19
|
try {
|
|
19
20
|
const artifacts = files.map((f) => {
|
|
20
21
|
if (!fs_1.default.existsSync(f))
|
|
21
22
|
throw new Error(`File not found: ${f}`);
|
|
22
23
|
return { relativePath: (0, org_publish_1.packRelativePathFor)(f), content: fs_1.default.readFileSync(f, 'utf8') };
|
|
23
24
|
});
|
|
24
|
-
const
|
|
25
|
+
const deletions = (options.remove ?? []).map(org_publish_1.packRelativePathFor);
|
|
26
|
+
const result = await (0, org_publish_1.publishOrgArtifacts)(artifacts, { deletions });
|
|
25
27
|
if (result.backend === 'fraim-cloud') {
|
|
26
28
|
console.log(chalk_1.default.green(`Published ${artifacts.length} org artifact(s) to your FRAIM organization (version ${result.version}).`));
|
|
27
29
|
console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (result.backend === 'local-folder') {
|
|
33
|
+
console.log(chalk_1.default.green(`Wrote ${artifacts.length} org artifact(s) to your synced folder.`));
|
|
34
|
+
for (const p of result.writtenPaths ?? [])
|
|
35
|
+
console.log(chalk_1.default.gray(` ${p}`));
|
|
36
|
+
if (result.deletedPaths?.length) {
|
|
37
|
+
console.log(chalk_1.default.green(`Removed ${result.deletedPaths.length} source file(s).`));
|
|
38
|
+
}
|
|
39
|
+
console.log(chalk_1.default.gray('Run "fraim sync" on each machine to pull the update.'));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (result.credentialWarning)
|
|
43
|
+
console.log(chalk_1.default.yellow(result.credentialWarning));
|
|
44
|
+
console.log(chalk_1.default.green(`Pushed ${artifacts.length} org artifact(s) to branch "${result.branch}".`));
|
|
45
|
+
if (result.prCreated) {
|
|
46
|
+
console.log(chalk_1.default.cyan(`Opened the pull request: ${result.prUrl}`));
|
|
47
|
+
console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
|
|
28
48
|
}
|
|
29
49
|
else {
|
|
30
|
-
console.log(chalk_1.default.
|
|
50
|
+
console.log(chalk_1.default.yellow(`No pull request was opened. ${result.prBlockedReason ?? ''}`.trim()));
|
|
31
51
|
if (result.prUrl)
|
|
32
|
-
console.log(chalk_1.default.cyan(`Open
|
|
33
|
-
console.log(chalk_1.default.gray('After it merges, run "fraim sync" to pull the update.'));
|
|
52
|
+
console.log(chalk_1.default.cyan(`Open one here: ${result.prUrl}`));
|
|
34
53
|
}
|
|
35
54
|
}
|
|
36
55
|
catch (error) {
|
|
@@ -230,6 +230,23 @@ const runSync = async (options) => {
|
|
|
230
230
|
else if (outcome.status === 'absent') {
|
|
231
231
|
console.log(chalk_1.default.yellow(`Org context not synced: ${outcome.error}`));
|
|
232
232
|
}
|
|
233
|
+
// Issue #1043 round 2: the home is the only read location, so anything
|
|
234
|
+
// still sitting at the old standard path is invisible to FRAIM. Say so;
|
|
235
|
+
// silently orphaning a manager's work is worse than a noisy sync.
|
|
236
|
+
try {
|
|
237
|
+
const { findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
|
|
238
|
+
const stranded = findStrandedLegacyContent('org');
|
|
239
|
+
if (stranded.length > 0) {
|
|
240
|
+
const home = resolvePackHome('org');
|
|
241
|
+
console.log(chalk_1.default.yellow(`${stranded.length} org file(s) are not in your org home and are no longer read:`));
|
|
242
|
+
for (const rel of stranded.slice(0, 10))
|
|
243
|
+
console.log(chalk_1.default.yellow(` ${rel}`));
|
|
244
|
+
if (stranded.length > 10)
|
|
245
|
+
console.log(chalk_1.default.yellow(` ...and ${stranded.length - 10} more`));
|
|
246
|
+
console.log(chalk_1.default.yellow(`Move them into ${home.root} to make them live again. Nothing was deleted.`));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch { /* reporting must never fail a sync */ }
|
|
233
250
|
// 'disabled' (no org configured) stays silent.
|
|
234
251
|
// R8.1: one-time publish offer for legacy machine-local org files. The
|
|
235
252
|
// publish itself runs through organization-onboarding (propose-and-approve);
|
|
@@ -257,6 +274,23 @@ const runSync = async (options) => {
|
|
|
257
274
|
else if (outcome.status === 'absent') {
|
|
258
275
|
console.log(chalk_1.default.yellow(`Manager context not synced: ${outcome.error}`));
|
|
259
276
|
}
|
|
277
|
+
// Issue #1043 round 2: the home is the only read location, so anything
|
|
278
|
+
// still sitting at the old standard path is invisible to FRAIM. Say so;
|
|
279
|
+
// silently orphaning a manager's work is worse than a noisy sync.
|
|
280
|
+
try {
|
|
281
|
+
const { findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
|
|
282
|
+
const stranded = findStrandedLegacyContent('manager');
|
|
283
|
+
if (stranded.length > 0) {
|
|
284
|
+
const home = resolvePackHome('manager');
|
|
285
|
+
console.log(chalk_1.default.yellow(`${stranded.length} manager file(s) are not in your manager home and are no longer read:`));
|
|
286
|
+
for (const rel of stranded.slice(0, 10))
|
|
287
|
+
console.log(chalk_1.default.yellow(` ${rel}`));
|
|
288
|
+
if (stranded.length > 10)
|
|
289
|
+
console.log(chalk_1.default.yellow(` ...and ${stranded.length - 10} more`));
|
|
290
|
+
console.log(chalk_1.default.yellow(`Move them into ${home.root} to make them live again. Nothing was deleted.`));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
catch { /* reporting must never fail a sync */ }
|
|
260
294
|
};
|
|
261
295
|
const isNpx = process.env.npm_config_prefix === undefined || process.env.npm_lifecycle_event === 'npx';
|
|
262
296
|
const isGlobal = !isNpx && (process.env.npm_config_global === 'true' || process.env.npm_config_prefix);
|
|
@@ -37,42 +37,84 @@ exports.isConflictCopy = isConflictCopy;
|
|
|
37
37
|
exports.countConflictCopies = countConflictCopies;
|
|
38
38
|
exports.collectLocalFolderFiles = collectLocalFolderFiles;
|
|
39
39
|
exports.writeLocalFolderFile = writeLocalFolderFile;
|
|
40
|
+
exports.removeLocalFolderFiles = removeLocalFolderFiles;
|
|
40
41
|
exports.localFolderVersion = localFolderVersion;
|
|
41
42
|
const fs_1 = __importDefault(require("fs"));
|
|
42
43
|
const path_1 = __importDefault(require("path"));
|
|
43
44
|
const crypto_1 = __importDefault(require("crypto"));
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
const capability_pack_1 = require("../../core/capability-pack");
|
|
46
|
+
/**
|
|
47
|
+
* Pack subdirectories expected in every local-folder backend.
|
|
48
|
+
*
|
|
49
|
+
* Issue #1043: the capability directories (and `employees`) were missing, so a
|
|
50
|
+
* job, skill, nested rule, or employee record published into a local-folder
|
|
51
|
+
* pack was never collected on the way back down. The pack was write-only for
|
|
52
|
+
* those types. `CAPABILITY_DIRS` is the single allowlist for the capability
|
|
53
|
+
* half; this list is context/rules/learnings plus that, deduplicated because
|
|
54
|
+
* `rules` appears in both.
|
|
55
|
+
*/
|
|
56
|
+
exports.LOCAL_FOLDER_PACK_DIRS = [
|
|
57
|
+
...new Set(['context', 'rules', 'learnings', ...capability_pack_1.CAPABILITY_DIRS]),
|
|
58
|
+
];
|
|
46
59
|
/**
|
|
47
60
|
* Matches the conflict-copy filename patterns produced by OneDrive,
|
|
48
61
|
* SharePoint, Google Drive for Desktop, and Dropbox.
|
|
49
62
|
*
|
|
50
63
|
* Deliberately does NOT match ordinary parenthesised names like
|
|
51
64
|
* "sprint-retro (Q2 2026).md" — only the well-known suffix patterns.
|
|
65
|
+
*
|
|
66
|
+
* Issue #1043 widened the extension from `.md` to `.md|.json`: employee
|
|
67
|
+
* records are JSON pack members and get conflict copies like any other file.
|
|
52
68
|
*/
|
|
53
|
-
exports.CONFLICT_COPY_RE = /[\s-](\([^)]*'s conflicted copy \d{4}-\d{2}-\d{2}\)|\(\d+\)|[\w-]+-PC)\.md$/i;
|
|
69
|
+
exports.CONFLICT_COPY_RE = /[\s-](\([^)]*'s conflicted copy \d{4}-\d{2}-\d{2}\)|\(\d+\)|[\w-]+-PC)\.(md|json)$/i;
|
|
54
70
|
/** True when the filename looks like a sync-client conflict copy. */
|
|
55
71
|
function isConflictCopy(fileName) {
|
|
56
72
|
return exports.CONFLICT_COPY_RE.test(fileName);
|
|
57
73
|
}
|
|
58
74
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
75
|
+
* Walk every pack file under localPath, depth-first, yielding pack-relative
|
|
76
|
+
* paths with forward slashes.
|
|
77
|
+
*
|
|
78
|
+
* Issue #1043: this replaces three separate one-level `readdirSync` loops.
|
|
79
|
+
* Capability members live at `jobs/<category>/<name>.md`, so a one-level scan
|
|
80
|
+
* could never see them — which is why a published job neither synced back down
|
|
81
|
+
* nor moved the pack version.
|
|
61
82
|
*/
|
|
62
|
-
function
|
|
63
|
-
|
|
83
|
+
function walkPackFiles(localPath, visit) {
|
|
84
|
+
const descend = (absDir, relDir) => {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = fs_1.default.readdirSync(absDir, { withFileTypes: true });
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return; // non-fatal — missing or unreadable subdirs are fine
|
|
91
|
+
}
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
const abs = path_1.default.join(absDir, entry.name);
|
|
94
|
+
const rel = `${relDir}/${entry.name}`;
|
|
95
|
+
if (entry.isDirectory())
|
|
96
|
+
descend(abs, rel);
|
|
97
|
+
else if (entry.isFile())
|
|
98
|
+
visit(rel, abs);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
64
101
|
for (const dirName of exports.LOCAL_FOLDER_PACK_DIRS) {
|
|
65
102
|
const dir = path_1.default.join(localPath, dirName);
|
|
66
103
|
if (!fs_1.default.existsSync(dir))
|
|
67
104
|
continue;
|
|
68
|
-
|
|
69
|
-
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
70
|
-
if (entry.isFile() && isConflictCopy(entry.name))
|
|
71
|
-
count++;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
catch { /* non-fatal */ }
|
|
105
|
+
descend(dir, dirName);
|
|
75
106
|
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Count conflict-copy files across the pack directories.
|
|
110
|
+
* Used by the Hub UI to surface an amber "N conflicts" indicator.
|
|
111
|
+
*/
|
|
112
|
+
function countConflictCopies(localPath) {
|
|
113
|
+
let count = 0;
|
|
114
|
+
walkPackFiles(localPath, (rel) => {
|
|
115
|
+
if (isConflictCopy(path_1.default.basename(rel)))
|
|
116
|
+
count++;
|
|
117
|
+
});
|
|
76
118
|
return count;
|
|
77
119
|
}
|
|
78
120
|
/**
|
|
@@ -84,25 +126,16 @@ function countConflictCopies(localPath) {
|
|
|
84
126
|
*/
|
|
85
127
|
function collectLocalFolderFiles(localPath, pathGuard) {
|
|
86
128
|
const files = [];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
walkPackFiles(localPath, (rel, abs) => {
|
|
130
|
+
if (isConflictCopy(path_1.default.basename(rel)))
|
|
131
|
+
return;
|
|
132
|
+
if (!pathGuard(rel))
|
|
133
|
+
return;
|
|
91
134
|
try {
|
|
92
|
-
|
|
93
|
-
if (!entry.isFile())
|
|
94
|
-
continue;
|
|
95
|
-
if (isConflictCopy(entry.name))
|
|
96
|
-
continue;
|
|
97
|
-
const rel = `${dirName}/${entry.name}`;
|
|
98
|
-
if (!pathGuard(rel))
|
|
99
|
-
continue;
|
|
100
|
-
const content = fs_1.default.readFileSync(path_1.default.join(dir, entry.name), 'utf8');
|
|
101
|
-
files.push({ relativePath: rel, content });
|
|
102
|
-
}
|
|
135
|
+
files.push({ relativePath: rel, content: fs_1.default.readFileSync(abs, 'utf8') });
|
|
103
136
|
}
|
|
104
|
-
catch { /* non-fatal —
|
|
105
|
-
}
|
|
137
|
+
catch { /* non-fatal — skip files we cannot read */ }
|
|
138
|
+
});
|
|
106
139
|
return files;
|
|
107
140
|
}
|
|
108
141
|
/**
|
|
@@ -147,32 +180,51 @@ function writeLocalFolderFile(localPath, relativePath, content, lastSyncedAt) {
|
|
|
147
180
|
throw err;
|
|
148
181
|
}
|
|
149
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Remove the source side of a move from a local-folder pack.
|
|
185
|
+
*
|
|
186
|
+
* Call this only after every destination write has landed, so a partial failure
|
|
187
|
+
* never leaves the artifact at neither level. A path that is also a write target
|
|
188
|
+
* is skipped: a same-path move is a no-op, not a delete of what was just written.
|
|
189
|
+
*
|
|
190
|
+
* Returns the absolute paths actually removed.
|
|
191
|
+
*/
|
|
192
|
+
function removeLocalFolderFiles(localPath, deletions, writtenRelativePaths) {
|
|
193
|
+
const written = new Set(writtenRelativePaths);
|
|
194
|
+
const removed = [];
|
|
195
|
+
for (const relativePath of deletions) {
|
|
196
|
+
if (written.has(relativePath))
|
|
197
|
+
continue;
|
|
198
|
+
const target = path_1.default.join(localPath, relativePath);
|
|
199
|
+
if (!fs_1.default.existsSync(target))
|
|
200
|
+
continue;
|
|
201
|
+
fs_1.default.rmSync(target, { force: true });
|
|
202
|
+
removed.push(target);
|
|
203
|
+
}
|
|
204
|
+
return removed;
|
|
205
|
+
}
|
|
150
206
|
/**
|
|
151
207
|
* A stable version string for the local-folder backend — a hex digest
|
|
152
208
|
* of the most recent mtime across all pack files. Changes whenever any
|
|
153
209
|
* file in the folder is written (by this machine or the sync client).
|
|
154
210
|
*/
|
|
155
211
|
function localFolderVersion(localPath) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
212
|
+
// Issue #1043: the digest mixes in the pack-relative path, not just mtimes.
|
|
213
|
+
// Two files written in the same filesystem-clock tick produce identical
|
|
214
|
+
// mtimes, so an mtime-only digest could leave the version unchanged after a
|
|
215
|
+
// publish and make sync skip the update.
|
|
216
|
+
const entries = [];
|
|
217
|
+
walkPackFiles(localPath, (rel, abs) => {
|
|
218
|
+
if (isConflictCopy(path_1.default.basename(rel)))
|
|
219
|
+
return;
|
|
161
220
|
try {
|
|
162
|
-
|
|
163
|
-
if (!entry.isFile() || isConflictCopy(entry.name))
|
|
164
|
-
continue;
|
|
165
|
-
try {
|
|
166
|
-
mtimes.push(fs_1.default.statSync(path_1.default.join(dir, entry.name)).mtimeMs);
|
|
167
|
-
}
|
|
168
|
-
catch { /* skip files we can't stat */ }
|
|
169
|
-
}
|
|
221
|
+
entries.push(`${rel}:${fs_1.default.statSync(abs).mtimeMs}`);
|
|
170
222
|
}
|
|
171
|
-
catch { /*
|
|
172
|
-
}
|
|
173
|
-
if (
|
|
223
|
+
catch { /* skip files we can't stat */ }
|
|
224
|
+
});
|
|
225
|
+
if (entries.length === 0)
|
|
174
226
|
return '0';
|
|
175
227
|
const hash = crypto_1.default.createHash('sha1');
|
|
176
|
-
hash.update(
|
|
228
|
+
hash.update(entries.sort().join(','));
|
|
177
229
|
return hash.digest('hex').slice(0, 12);
|
|
178
230
|
}
|
|
@@ -3,10 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.
|
|
7
|
-
exports.
|
|
8
|
-
exports.
|
|
9
|
-
exports.
|
|
6
|
+
exports.MANAGER_MANAGED_HEADER = exports.MANAGER_SYNC_METADATA_FILE = exports.MANAGER_HOME_DIRNAME = void 0;
|
|
7
|
+
exports.getManagerHomeDir = getManagerHomeDir;
|
|
8
|
+
exports.readManagerSyncMetadata = readManagerSyncMetadata;
|
|
9
|
+
exports.getManagerSyncAgeHours = getManagerSyncAgeHours;
|
|
10
10
|
exports.syncManagerCache = syncManagerCache;
|
|
11
11
|
/**
|
|
12
12
|
* Manager cache sync (issue #580).
|
|
@@ -25,29 +25,60 @@ const path_1 = __importDefault(require("path"));
|
|
|
25
25
|
const project_fraim_paths_1 = require("../../core/utils/project-fraim-paths");
|
|
26
26
|
const manager_pack_1 = require("../../core/manager-pack");
|
|
27
27
|
const capability_pack_1 = require("../../core/capability-pack");
|
|
28
|
-
const
|
|
28
|
+
const pack_home_1 = require("./pack-home");
|
|
29
29
|
const local_folder_sync_1 = require("./local-folder-sync");
|
|
30
30
|
const user_config_1 = require("./user-config");
|
|
31
|
-
exports.
|
|
31
|
+
exports.MANAGER_HOME_DIRNAME = 'manager';
|
|
32
32
|
exports.MANAGER_SYNC_METADATA_FILE = '.manager-sync-metadata.json';
|
|
33
|
-
exports.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
exports.MANAGER_MANAGED_HEADER = '<!-- FRAIM_MANAGER_SYNC_MANAGED_CONTENT -->';
|
|
34
|
+
// Issue #1043 adds 'employees' so a published employee record is walked out of
|
|
35
|
+
// a git snapshot. isManagerPackRelativePath still decides what is admitted.
|
|
36
|
+
const MANAGER_PACK_DIRS = ['context', 'rules', 'learnings', 'jobs', 'skills', 'templates', 'scripts', 'employees'];
|
|
37
|
+
/**
|
|
38
|
+
* The layer's single home. Named `getManagerHomeDir` for backward compatibility with
|
|
39
|
+
* existing callers; issue #1043 round 2 removed the separate cache it used to
|
|
40
|
+
* point at, so this now resolves the one place the layer lives.
|
|
41
|
+
*/
|
|
42
|
+
function getManagerHomeDir() {
|
|
43
|
+
return (0, pack_home_1.resolvePackHome)('manager').root;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Where sync provenance is recorded.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately NOT inside the home. The home is the user's own content: for a
|
|
49
|
+
* git backend it is a real working tree, so a metadata file there shows up as
|
|
50
|
+
* an untracked change in their pack repo, and for a synced folder it would
|
|
51
|
+
* replicate machine-local bookkeeping to every other machine. Provenance is
|
|
52
|
+
* local state, so it belongs in ~/.fraim beside the config.
|
|
53
|
+
*/
|
|
54
|
+
function syncMetadataPath() {
|
|
55
|
+
return path_1.default.join((0, project_fraim_paths_1.getUserFraimDirPath)(), exports.MANAGER_SYNC_METADATA_FILE);
|
|
37
56
|
}
|
|
38
|
-
|
|
57
|
+
/** Record when and from where this machine last synced. Never touches the home. */
|
|
58
|
+
function writeHomeMetadata(metadata) {
|
|
39
59
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
fs_1.default.mkdirSync((0, project_fraim_paths_1.getUserFraimDirPath)(), { recursive: true });
|
|
61
|
+
fs_1.default.writeFileSync(syncMetadataPath(), JSON.stringify(metadata, null, 2));
|
|
62
|
+
}
|
|
63
|
+
catch { /* provenance is best-effort; never fail a sync over it */ }
|
|
64
|
+
}
|
|
65
|
+
function readManagerSyncMetadata() {
|
|
66
|
+
try {
|
|
67
|
+
const metadataPath = syncMetadataPath();
|
|
68
|
+
if (fs_1.default.existsSync(metadataPath))
|
|
69
|
+
return JSON.parse(fs_1.default.readFileSync(metadataPath, 'utf8'));
|
|
70
|
+
// A machine that last synced before provenance moved out of the home.
|
|
71
|
+
const legacy = path_1.default.join(getManagerHomeDir(), exports.MANAGER_SYNC_METADATA_FILE);
|
|
72
|
+
if (!fs_1.default.existsSync(legacy))
|
|
42
73
|
return null;
|
|
43
|
-
return JSON.parse(fs_1.default.readFileSync(
|
|
74
|
+
return JSON.parse(fs_1.default.readFileSync(legacy, 'utf8'));
|
|
44
75
|
}
|
|
45
76
|
catch {
|
|
46
77
|
return null;
|
|
47
78
|
}
|
|
48
79
|
}
|
|
49
|
-
function
|
|
50
|
-
const metadata =
|
|
80
|
+
function getManagerSyncAgeHours() {
|
|
81
|
+
const metadata = readManagerSyncMetadata();
|
|
51
82
|
if (!metadata?.syncedAt)
|
|
52
83
|
return null;
|
|
53
84
|
const syncedAt = Date.parse(metadata.syncedAt);
|
|
@@ -57,7 +88,7 @@ function getManagerCacheAgeHours() {
|
|
|
57
88
|
}
|
|
58
89
|
function decorateManagedManagerFile(content, backend) {
|
|
59
90
|
const normalized = content.replace(/^\uFEFF/, '');
|
|
60
|
-
if (normalized.startsWith(exports.
|
|
91
|
+
if (normalized.startsWith(exports.MANAGER_MANAGED_HEADER))
|
|
61
92
|
return normalized;
|
|
62
93
|
const writePath = backend === 'git'
|
|
63
94
|
? 'open a pull request against your manager storage repo'
|
|
@@ -65,7 +96,7 @@ function decorateManagedManagerFile(content, backend) {
|
|
|
65
96
|
? 'edit the file in your synced folder and let the sync client upload it'
|
|
66
97
|
: 'publish the change through your FRAIM manager storage flow';
|
|
67
98
|
const marker = [
|
|
68
|
-
exports.
|
|
99
|
+
exports.MANAGER_MANAGED_HEADER,
|
|
69
100
|
'> [!IMPORTANT]',
|
|
70
101
|
'> Synced from your personal manager storage. Local edits are overwritten on the next sync.',
|
|
71
102
|
`> To change this content, ${writePath}.`,
|
|
@@ -106,8 +137,8 @@ async function fetchCloudPack(remoteUrl, apiKey) {
|
|
|
106
137
|
version: String(response.data?.version ?? '0')
|
|
107
138
|
};
|
|
108
139
|
}
|
|
109
|
-
function
|
|
110
|
-
const cacheDir =
|
|
140
|
+
function materializeCloudHome(files, metadata) {
|
|
141
|
+
const cacheDir = getManagerHomeDir();
|
|
111
142
|
const stagingDir = `${cacheDir}.staging-${process.pid}`;
|
|
112
143
|
fs_1.default.rmSync(stagingDir, { recursive: true, force: true });
|
|
113
144
|
fs_1.default.mkdirSync(stagingDir, { recursive: true });
|
|
@@ -125,7 +156,7 @@ function materializeCache(files, metadata) {
|
|
|
125
156
|
: file.content;
|
|
126
157
|
fs_1.default.writeFileSync(destination, content);
|
|
127
158
|
}
|
|
128
|
-
|
|
159
|
+
// Provenance is written outside the home by writeHomeMetadata.
|
|
129
160
|
fs_1.default.rmSync(cacheDir, { recursive: true, force: true });
|
|
130
161
|
fs_1.default.renameSync(stagingDir, cacheDir);
|
|
131
162
|
}
|
|
@@ -147,12 +178,12 @@ async function discoverCloudManagerStorage(options) {
|
|
|
147
178
|
}
|
|
148
179
|
function failureOutcome(error) {
|
|
149
180
|
const message = error instanceof Error ? error.message : String(error);
|
|
150
|
-
const existing =
|
|
181
|
+
const existing = readManagerSyncMetadata();
|
|
151
182
|
if (existing) {
|
|
152
183
|
return {
|
|
153
184
|
status: 'stale',
|
|
154
185
|
metadata: existing,
|
|
155
|
-
ageHours:
|
|
186
|
+
ageHours: getManagerSyncAgeHours() ?? 0,
|
|
156
187
|
error: message
|
|
157
188
|
};
|
|
158
189
|
}
|
|
@@ -172,32 +203,35 @@ async function syncManagerCache(options) {
|
|
|
172
203
|
}
|
|
173
204
|
try {
|
|
174
205
|
if (managerStorage.backend === 'local-folder') {
|
|
206
|
+
// Issue #1043 round 2: the configured folder IS the home. There is
|
|
207
|
+
// no cache to materialize into; the OS sync client is the transport
|
|
208
|
+
// and the files are already the single place FRAIM reads.
|
|
175
209
|
const localPath = managerStorage.localPath;
|
|
176
|
-
const files = (0, local_folder_sync_1.collectLocalFolderFiles)(localPath, manager_pack_1.isManagerPackRelativePath);
|
|
177
210
|
const metadata = {
|
|
178
211
|
version: (0, local_folder_sync_1.localFolderVersion)(localPath),
|
|
179
212
|
backend: 'local-folder',
|
|
180
213
|
source: localPath,
|
|
181
214
|
syncedAt: new Date().toISOString(),
|
|
182
215
|
};
|
|
183
|
-
|
|
216
|
+
writeHomeMetadata(metadata);
|
|
184
217
|
return { status: 'synced', metadata };
|
|
185
218
|
}
|
|
186
219
|
if (managerStorage.backend === 'git') {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
// Issue #1043 round 2: the durable clone IS the home. ensurePackClone
|
|
221
|
+
// already fast-forwarded it, so syncing is that pull, not a copy into
|
|
222
|
+
// a second location.
|
|
223
|
+
const home = (0, pack_home_1.resolvePackHome)('manager');
|
|
224
|
+
const clone = (0, pack_home_1.ensurePackClone)(managerStorage.gitUrl, home.root);
|
|
225
|
+
if (clone.sha === null)
|
|
226
|
+
throw new Error(clone.offlineReason ?? 'Pack clone unavailable.');
|
|
227
|
+
const metadata = {
|
|
228
|
+
version: clone.sha,
|
|
229
|
+
backend: 'git',
|
|
230
|
+
source: managerStorage.gitUrl,
|
|
231
|
+
syncedAt: new Date().toISOString()
|
|
232
|
+
};
|
|
233
|
+
writeHomeMetadata(metadata);
|
|
234
|
+
return { status: 'synced', metadata };
|
|
201
235
|
}
|
|
202
236
|
const remoteUrl = options?.remoteUrl || process.env.FRAIM_REMOTE_URL || 'https://fraim.wellnessatwork.me';
|
|
203
237
|
const apiKey = options?.apiKey || (0, user_config_1.readUserFraimConfig)().apiKey;
|
|
@@ -210,7 +244,10 @@ async function syncManagerCache(options) {
|
|
|
210
244
|
source: remoteUrl,
|
|
211
245
|
syncedAt: new Date().toISOString()
|
|
212
246
|
};
|
|
213
|
-
|
|
247
|
+
// The cloud has no user-visible folder, so this managed directory is the
|
|
248
|
+
// home for that backend, not a cache beside one.
|
|
249
|
+
materializeCloudHome(pack.files, metadata);
|
|
250
|
+
writeHomeMetadata(metadata);
|
|
214
251
|
return { status: 'synced', metadata };
|
|
215
252
|
}
|
|
216
253
|
catch (error) {
|