A universal Agent whose obsolete private copy cannot be retired installs nothing there, yet every entry point counted that retirement failure as an install failure — aborting `npm install`, `dws skill setup`, and the shell installers even when the canonical store and all links published correctly, and skipping the skills-state write. Route retirement failures to a separate warning path across all surfaces (Go upgrade + skill setup, npm, PowerShell, install.sh, install-skills.sh, install-event.sh, install-devapp.sh). Also: - Add a checked-rename fallback for filesystems that reject the atomic no-replace flag (NFS, FUSE, overlayfs); the no-clobber contract is kept and the previously unsupported platforms build and work. - PowerShell multi-mode links only bundle skills, never the shared canonical store, so third-party/user skills are no longer fanned into every Agent root. - Prune ~/.dws/skill-backups to the newest 5 on every surface; encode HOME-relative backup names on PowerShell to preserve origin. - Add simulated-win32 junction coverage and rewrite the tautological no-replace test; remove dead code whose tests gave false coverage. - Soften the overstated Windows ownership-proof comment (NTFS tunneling).
41 lines
1.6 KiB
Go
41 lines
1.6 KiB
Go
package upgrade
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
)
|
|
|
|
// errNoReplaceRenameUnsupported marks a platform that has no atomic no-replace
|
|
// rename primitive at all.
|
|
var errNoReplaceRenameUnsupported = errors.New("平台不支持原子 no-replace rename")
|
|
|
|
var skillPathRenameNoReplaceAtomic = renameSkillPathNoReplaceAtomic
|
|
|
|
// renameSkillPathNoReplace moves source onto destination and never replaces an
|
|
// object that already occupies destination.
|
|
//
|
|
// The atomic primitives (Linux RENAME_NOREPLACE, Darwin RENAME_EXCL) require
|
|
// support from the underlying filesystem and report EINVAL/ENOTSUP when it is
|
|
// missing — rename(2) lists only ext4, btrfs, tmpfs and cifs, so NFS, FUSE and
|
|
// overlayfs homes reject the flag. Failing there would abort the whole install
|
|
// on those filesystems, so they degrade to an explicit existence check plus a
|
|
// plain rename: the no-clobber contract is preserved, only its atomicity is
|
|
// lost. That check-then-rename window is the same exposure a plain rename
|
|
// always had, and callers additionally verify identity after publication.
|
|
func renameSkillPathNoReplace(source, destination string) error {
|
|
err := skillPathRenameNoReplaceAtomic(source, destination)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !errors.Is(err, errNoReplaceRenameUnsupported) && !isNoReplaceRenameUnsupported(err) {
|
|
return err
|
|
}
|
|
if _, statErr := skillPathLstat(destination); statErr == nil {
|
|
return &os.LinkError{Op: "rename", Old: source, New: destination, Err: os.ErrExist}
|
|
} else if !os.IsNotExist(statErr) {
|
|
return fmt.Errorf("检查发布目标失败 %s: %w", destination, statErr)
|
|
}
|
|
return skillPathRename(source, destination)
|
|
}
|