Browse Source

refactor(agent-sync): simplify ensureLink function and improve error handling

softword 1 month ago
parent
commit
7b919e0a27
1 changed files with 15 additions and 39 deletions
  1. 15 39
      bin/agent-sync.mjs

+ 15 - 39
bin/agent-sync.mjs

@@ -8,7 +8,7 @@
 import { createHash } from 'node:crypto';
 import {
   cpSync, existsSync, lstatSync, mkdirSync, readdirSync,
-  readFileSync, realpathSync, rmSync, statSync, symlinkSync, linkSync,
+  readFileSync, realpathSync, rmSync, statSync, symlinkSync,
 } from 'node:fs';
 import { homedir, platform } from 'node:os';
 import { basename, dirname, join, relative } from 'node:path';
@@ -244,7 +244,7 @@ function syncCanonical(skills, agentFiles) {
     actions.push({ label: `skills/${name}`, status });
   }
   for (const file of agentFiles) {
-    actions.push({ label: `agents/${file}`, status: ensureLink(join(CANONICAL_AGENTS_DIR, file), join(SOURCE_AGENTS_DIR, file), { convertForeign: true }) });
+    actions.push({ label: `agents/${file}`, status: ensureLink(join(CANONICAL_AGENTS_DIR, file), join(SOURCE_AGENTS_DIR, file)) });
   }
   return actions;
 }
@@ -257,57 +257,33 @@ function detectAgents(targetNames) {
     .map((agent) => ({ ...agent, baseDir: agent.base() }));
 }
 
-// Ensures linkPath points to targetPath. Existing entries are never modified:
-// a link already pointing at the target counts as up-to-date, anything else is
-// foreign and skipped. New links use the best mechanism available: relative
-// symlink (unix), junction for directories (windows, no privileges), then
-// hardlink as the windows file fallback. Copies are never created: when no
-// link mechanism works the sync aborts with an explicit error. With
-// convertForeign an existing regular file (leftover from old copy-based
-// installs) is replaced by a proper link instead of being kept as a copy.
-function ensureLink(linkPath, targetPath, { convertForeign = false } = {}) {
+// Ensures linkPath points to targetPath via a symlink: junctions for
+// directories on windows (no privileges needed), file symlinks everywhere
+// else. There is no fallback: if the symlink cannot be created (EPERM without
+// Developer Mode/privileges, cross-filesystem restrictions) the sync aborts
+// with an explicit error — copies are never made. A pre-existing regular file
+// is a leftover from older copy/hardlink installs and is replaced by the
+// symlink; directories or foreign symlinks are skipped, never touched.
+function ensureLink(linkPath, targetPath) {
   const current = lstatOrNull(linkPath);
   if (current) {
     if (current.isSymbolicLink() && pointsTo(linkPath, targetPath)) return 'up-to-date';
-    if (convertForeign && current.isFile()) {
+    if (current.isFile()) {
       if (DRY_RUN) return 'relink';
       rmSync(linkPath, { force: true });
     } else {
-      // Regular files left by this tool's own windows fallbacks (hardlink, or
-      // copies from versions that still had a copy fallback) converge to
-      // up-to-date when their content matches the target.
-      // Missing or non-file targets degrade to 'skipped': in dry-run the
-      // canonical target may not be installed yet.
-      if (current.isFile() && lstatOrNull(targetPath)?.isFile() && fileHash(linkPath) === fileHash(targetPath)) {
-        return 'up-to-date';
-      }
       return 'skipped';
     }
   }
   if (DRY_RUN) return 'link';
   mkdirSync(dirname(linkPath), { recursive: true });
   const isDirectory = statSync(targetPath).isDirectory();
-  if (!IS_WINDOWS) {
-    symlinkSync(relative(dirname(linkPath), targetPath), linkPath);
-    return 'linked';
-  }
-  if (isDirectory) {
-    symlinkSync(targetPath, linkPath, 'junction');
-    return 'linked';
-  }
   try {
-    symlinkSync(relative(dirname(linkPath), targetPath), linkPath, 'file');
+    if (IS_WINDOWS && isDirectory) symlinkSync(targetPath, linkPath, 'junction');
+    else symlinkSync(relative(dirname(linkPath), targetPath), linkPath);
     return 'linked';
-  } catch {
-    // Windows symlinks need Developer Mode: degrade to a hardlink (same
-    // volume). A copy is never a fallback: failing loudly keeps the "links
-    // only" guarantee instead of silently diverging content.
-    try {
-      linkSync(targetPath, linkPath);
-      return 'hardlinked';
-    } catch (error) {
-      throw new Error(`cannot link '${linkPath}' without a copy (symlink and hardlink both failed: ${error.message})`);
-    }
+  } catch (error) {
+    throw new Error(`cannot create symlink '${linkPath}' (no fallback, no copy: ${error.message})`);
   }
 }