|
|
@@ -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})`);
|
|
|
}
|
|
|
}
|
|
|
|