fraim 2.0.261 → 2.0.263

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.
@@ -230,15 +230,15 @@ 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.
233
+ // Migrate stranded content from the legacy standard path to contentRoot,
234
+ // then report any files that still could not be moved.
236
235
  try {
237
- const { findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
236
+ const { migrateStrandedContent, findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
237
+ migrateStrandedContent('org');
238
238
  const stranded = findStrandedLegacyContent('org');
239
239
  if (stranded.length > 0) {
240
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:`));
241
+ console.log(chalk_1.default.yellow(`${stranded.length} org file(s) could not be migrated to your org home and are no longer read:`));
242
242
  for (const rel of stranded.slice(0, 10))
243
243
  console.log(chalk_1.default.yellow(` ${rel}`));
244
244
  if (stranded.length > 10)
@@ -274,15 +274,15 @@ const runSync = async (options) => {
274
274
  else if (outcome.status === 'absent') {
275
275
  console.log(chalk_1.default.yellow(`Manager context not synced: ${outcome.error}`));
276
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.
277
+ // Migrate stranded content from the legacy standard path to contentRoot,
278
+ // then report any files that still could not be moved.
280
279
  try {
281
- const { findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
280
+ const { migrateStrandedContent, findStrandedLegacyContent, resolvePackHome } = await Promise.resolve().then(() => __importStar(require('../utils/pack-home')));
281
+ migrateStrandedContent('manager');
282
282
  const stranded = findStrandedLegacyContent('manager');
283
283
  if (stranded.length > 0) {
284
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:`));
285
+ console.log(chalk_1.default.yellow(`${stranded.length} manager file(s) could not be migrated to your manager home and are no longer read:`));
286
286
  for (const rel of stranded.slice(0, 10))
287
287
  console.log(chalk_1.default.yellow(` ${rel}`));
288
288
  if (stranded.length > 10)
@@ -224,6 +224,12 @@ async function syncManagerCache(options) {
224
224
  const clone = (0, pack_home_1.ensurePackClone)(managerStorage.gitUrl, home.root);
225
225
  if (clone.sha === null)
226
226
  throw new Error(clone.offlineReason ?? 'Pack clone unavailable.');
227
+ // When a clone exists but the remote was unreachable, offlineReason is
228
+ // set and the SHA is the last cached value. Return stale so callers
229
+ // know the content is potentially out of date (R4.3).
230
+ if (clone.offlineReason) {
231
+ return failureOutcome(new Error(clone.offlineReason));
232
+ }
227
233
  const metadata = {
228
234
  version: clone.sha,
229
235
  backend: 'git',
@@ -275,6 +275,12 @@ async function syncOrgCache(options) {
275
275
  const clone = (0, pack_home_1.ensurePackClone)(organization.gitUrl, home.root);
276
276
  if (clone.sha === null)
277
277
  throw new Error(clone.offlineReason ?? 'Pack clone unavailable.');
278
+ // When a clone exists but the remote was unreachable, offlineReason is
279
+ // set and the SHA is the last cached value. Return stale so callers
280
+ // know the content is potentially out of date (R4.3).
281
+ if (clone.offlineReason) {
282
+ return failureOutcome(new Error(clone.offlineReason));
283
+ }
278
284
  const metadata = {
279
285
  version: clone.sha,
280
286
  backend: 'git',
@@ -7,6 +7,7 @@ exports.packsDir = packsDir;
7
7
  exports.defaultCloneDir = defaultCloneDir;
8
8
  exports.resolvePackHome = resolvePackHome;
9
9
  exports.packReadRoots = packReadRoots;
10
+ exports.migrateStrandedContent = migrateStrandedContent;
10
11
  exports.findStrandedLegacyContent = findStrandedLegacyContent;
11
12
  exports.gitUrlHasUserinfo = gitUrlHasUserinfo;
12
13
  exports.ensurePackClone = ensurePackClone;
@@ -172,6 +173,78 @@ function resolvePackHome(layer) {
172
173
  function packReadRoots(layer) {
173
174
  return [resolvePackHome(layer).contentRoot];
174
175
  }
176
+ /**
177
+ * Auto-migrate content stranded at the legacy standard path to the configured
178
+ * contentRoot. Runs at most once per process per (layer + contentRoot) pair.
179
+ * Never throws: migration failures are silently skipped.
180
+ *
181
+ * Applies when a synced backend (local-folder, git, fraim-cloud) is configured
182
+ * and the legacy standard path differs from contentRoot. Each file is moved
183
+ * with renameSync; on a cross-device rename (e.g. ~/.fraim → OneDrive) it
184
+ * falls back to copyFileSync + unlinkSync. Files already present in contentRoot
185
+ * are never overwritten — contentRoot is always authoritative.
186
+ */
187
+ const _migratedStrandedLayers = new Set();
188
+ function migrateStrandedContent(layer) {
189
+ const home = resolvePackHome(layer);
190
+ const key = `${layer}:${home.contentRoot}`;
191
+ if (_migratedStrandedLayers.has(key))
192
+ return;
193
+ _migratedStrandedLayers.add(key);
194
+ const legacyContentRoot = home.legacyRoot ? path_1.default.join(home.legacyRoot, 'personalized-employee') : null;
195
+ if (!legacyContentRoot || legacyContentRoot === home.contentRoot)
196
+ return;
197
+ if (!fs_1.default.existsSync(legacyContentRoot))
198
+ return;
199
+ try {
200
+ const walk = (absDir, relDir) => {
201
+ let entries;
202
+ try {
203
+ entries = fs_1.default.readdirSync(absDir, { withFileTypes: true });
204
+ }
205
+ catch {
206
+ return;
207
+ }
208
+ for (const entry of entries) {
209
+ const rel = relDir ? `${relDir}/${entry.name}` : entry.name;
210
+ const src = path_1.default.join(absDir, entry.name);
211
+ const dst = path_1.default.join(home.contentRoot, rel);
212
+ if (entry.isDirectory()) {
213
+ walk(src, rel);
214
+ continue;
215
+ }
216
+ if (!entry.isFile())
217
+ continue;
218
+ if (fs_1.default.existsSync(dst))
219
+ continue;
220
+ try {
221
+ fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
222
+ fs_1.default.renameSync(src, dst);
223
+ }
224
+ catch {
225
+ try {
226
+ fs_1.default.copyFileSync(src, dst);
227
+ fs_1.default.unlinkSync(src);
228
+ }
229
+ catch { /* leave source intact */ }
230
+ }
231
+ }
232
+ };
233
+ walk(legacyContentRoot, '');
234
+ // Clean up empty dirs left behind (best-effort).
235
+ for (const dir of ORG_CONTENT_DIRS) {
236
+ try {
237
+ fs_1.default.rmdirSync(path_1.default.join(legacyContentRoot, dir));
238
+ }
239
+ catch { /* not empty — fine */ }
240
+ }
241
+ try {
242
+ fs_1.default.rmdirSync(legacyContentRoot);
243
+ }
244
+ catch { /* not empty — fine */ }
245
+ }
246
+ catch { /* migration is best-effort; never fail a sync over it */ }
247
+ }
175
248
  /**
176
249
  * Content sitting at the standard local path that the configured home does not
177
250
  * hold. Since the home is the single read location, this content is stranded:
@@ -72,11 +72,18 @@ function managerSecondaryLearningsBase() {
72
72
  function getLearningRoots(workspaceRoot) {
73
73
  const managerHome = (0, pack_home_1.resolvePackHome)('manager');
74
74
  const managerHomeBase = (0, path_1.join)(managerHome.contentRoot, 'learnings');
75
+ const managerCacheBase = managerSecondaryLearningsBase();
76
+ // Derive the display base from the actual cache path relative to ~/.fraim so
77
+ // the display string always matches the filesystem path (fixed by #1043r2).
78
+ const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
79
+ const managerCacheDisplayBase = managerCacheBase.startsWith(fraimDir)
80
+ ? (0, project_fraim_paths_1.getUserFraimDisplayPath)(managerCacheBase.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, ''))
81
+ : managerCacheBase.replace(/\\/g, '/');
75
82
  return {
76
83
  globalPersonalBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDir)(workspaceRoot),
77
84
  globalPersonalDisplayBase: (0, project_fraim_paths_1.getConfiguredPortableLearningsDisplayPath)(workspaceRoot),
78
- managerCacheBase: managerSecondaryLearningsBase(),
79
- managerCacheDisplayBase: (0, project_fraim_paths_1.getUserFraimDisplayPath)('manager/learnings'),
85
+ managerCacheBase,
86
+ managerCacheDisplayBase,
80
87
  managerHomeBase,
81
88
  managerHomeDisplayBase: managerHomeBase.replace(/\\/g, '/'),
82
89
  repoLearningsBase: (0, project_fraim_paths_1.getWorkspaceLearningsDir)(workspaceRoot)
@@ -637,11 +644,20 @@ function resolveOrgContextFile(workspaceRoot, relativePath, orgCacheEligible = t
637
644
  }
638
645
  }
639
646
  if (!orgCacheEligible) {
640
- const managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, relativePath);
647
+ const managerHome = (0, pack_home_1.resolvePackHome)('manager');
648
+ const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
641
649
  if ((0, fs_1.existsSync)(managerCachePath)) {
650
+ // Derive the display path relative to ~/.fraim. For the standard
651
+ // single-machine backend contentRoot is ~/.fraim/personalized-employee
652
+ // so the display path is ~/.fraim/personalized-employee/... For a
653
+ // synced backend it may be elsewhere (e.g. ~/.fraim/packs/manager-repo).
654
+ const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
655
+ const relToFraim = managerCachePath.startsWith(fraimDir)
656
+ ? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
657
+ : `personalized-employee/${relativePath}`;
642
658
  return {
643
659
  present: true,
644
- displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`manager/${relativePath}`)
660
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim)
645
661
  };
646
662
  }
647
663
  }
@@ -812,13 +828,18 @@ function resolveTeamContextFile(workspaceRoot, key) {
812
828
  }
813
829
  }
814
830
  if (key === 'manager' || key === 'managerRules') {
815
- const managerCachePath = (0, path_1.join)((0, pack_home_1.resolvePackHome)('manager').contentRoot, relativePath);
831
+ const managerHome = (0, pack_home_1.resolvePackHome)('manager');
832
+ const managerCachePath = (0, path_1.join)(managerHome.contentRoot, relativePath);
816
833
  if ((0, fs_1.existsSync)(managerCachePath)) {
834
+ const fraimDir = (0, project_fraim_paths_1.getUserFraimDirPath)();
835
+ const relToFraim = managerCachePath.startsWith(fraimDir)
836
+ ? managerCachePath.slice(fraimDir.length).replace(/\\/g, '/').replace(/^\//, '')
837
+ : `personalized-employee/${relativePath}`;
817
838
  return {
818
839
  present: true,
819
840
  readPath: managerCachePath,
820
841
  writePath: '',
821
- displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(`manager/${relativePath}`),
842
+ displayPath: (0, project_fraim_paths_1.getUserFraimDisplayPath)(relToFraim),
822
843
  scope,
823
844
  managedByManagerSync: true
824
845
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fraim",
3
- "version": "2.0.261",
3
+ "version": "2.0.263",
4
4
  "description": "FRAIM core CLI and MCP package.",
5
5
  "main": "index.js",
6
6
  "bin": {