({
mainCheckout,
projectRoot,
subdirs = DATA_SUBDIRS_TO_LINK,
force = false,
log = console.log
})
| 344 | * @throws {Error} When any subdir's dst is a non-symlink directory and `force` is false. The error message names the offending subdir. |
| 345 | */ |
| 346 | export async function symlinkDataDir({ |
| 347 | mainCheckout, |
| 348 | projectRoot, |
| 349 | subdirs = DATA_SUBDIRS_TO_LINK, |
| 350 | force = false, |
| 351 | log = console.log |
| 352 | }) { |
| 353 | const result = {linked: [], alreadyLinked: [], clobbered: [], skippedNoSource: [], mainCheckout: false}; |
| 354 | |
| 355 | if (path.resolve(projectRoot) === path.resolve(mainCheckout)) { |
| 356 | log(`symlink skip (main checkout): no per-subdir action`); |
| 357 | result.mainCheckout = true; |
| 358 | return result; |
| 359 | } |
| 360 | |
| 361 | // Ensure the parent .neo-ai-data/ exists as a regular dir; we never symlink the parent. |
| 362 | // This preserves the git-tracked concepts/ subdir already present in the worktree. |
| 363 | const parentDst = path.join(projectRoot, '.neo-ai-data'); |
| 364 | await fs.mkdir(parentDst, {recursive: true}); |
| 365 | |
| 366 | for (const subdir of subdirs) { |
| 367 | const src = path.join(mainCheckout, '.neo-ai-data', subdir); |
| 368 | const dst = path.join(parentDst, subdir); |
| 369 | const lstat = await fs.lstat(dst).catch(() => null); |
| 370 | |
| 371 | if (lstat?.isSymbolicLink()) { |
| 372 | log(`symlink skip (already linked): ${subdir}`); |
| 373 | result.alreadyLinked.push(subdir); |
| 374 | continue; |
| 375 | } |
| 376 | |
| 377 | // Skip if canonical lacks the subdir — graceful for fresh repos. |
| 378 | const srcExists = await exists(src); |
| 379 | if (!srcExists) { |
| 380 | log(`symlink skip (no source in main checkout): ${subdir}`); |
| 381 | result.skippedNoSource.push(subdir); |
| 382 | continue; |
| 383 | } |
| 384 | |
| 385 | if (lstat?.isDirectory()) { |
| 386 | if (!force) { |
| 387 | throw new Error( |
| 388 | `Refusing to replace non-symlink ${dst}; pass force=true (CLI --force) to opt in. ` + |
| 389 | `This directory contains local data that would be lost.` |
| 390 | ); |
| 391 | } |
| 392 | log(`symlink clobber (force=true): removing ${subdir}`); |
| 393 | await fs.rm(dst, {recursive: true, force: true}); |
| 394 | result.clobbered.push(subdir); |
| 395 | } |
| 396 | |
| 397 | await fs.symlink(src, dst, 'dir'); |
| 398 | log(`symlinked: ${subdir} → ${src}`); |
| 399 | result.linked.push(subdir); |
| 400 | } |
| 401 | |
| 402 | return result; |
| 403 | } |
no test coverage detected