Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ A CLI tool for downloading and managing [Point-Free Way](https://www.pointfree.c
$ pfw install --tool codex
```

## Installation Options

By default, `pfw` creates symbolic links from your AI tool's skills directory to a central location (`~/.pfw/skills/`). This allows skills to be updated in one place and immediately available to all tools.

### When to use `--copy`

If skills aren't being detected by your AI tool after installation, the tool may not support symbolic links. In this case, use the `--copy` flag to create full copies instead:

```sh
$ pfw install --tool <tool> --copy
```

> **Note:** When using `--copy`, skills are duplicated for each tool (using more disk space), and you'll need to re-run `pfw install --copy` after each update to get the latest skill versions.

## Supported AI Tools

| `pfw install --tool` | Install Path |
Expand Down
1 change: 1 addition & 0 deletions Sources/pfw/Dependencies/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ protocol FileSystem: Sendable {
func data(at url: URL) throws -> Data
func createSymbolicLink(at url: URL, withDestinationURL destURL: URL) throws
func moveItem(at srcURL: URL, to dstURL: URL) throws
func copyItem(at srcURL: URL, to dstURL: URL) throws
func contentsOfDirectory(at url: URL) throws -> [URL]
func unzipItem(at sourceURL: URL, to destinationURL: URL) throws
}
Expand Down
6 changes: 5 additions & 1 deletion Sources/pfw/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,11 @@ struct Install: AsyncParsableCommand {
(try? fileSystem.contentsOfDirectory(at: centralSkillsURL)) ?? []
for directory in centralSkillDirectories {
let toolDestination = skillsURL.appendingPathComponent("pfw-\(directory.lastPathComponent)")
try fileSystem.createSymbolicLink(at: toolDestination, withDestinationURL: directory)
if target.tool == .cursor {
try fileSystem.copyItem(at: directory, to: toolDestination)
} else {
try fileSystem.createSymbolicLink(at: toolDestination, withDestinationURL: directory)
}
}
if let tool = target.tool {
print(" • \(tool.rawValue): \(skillsURL.path)")
Expand Down
8 changes: 6 additions & 2 deletions Tests/pfwTests/InstallTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -452,8 +452,12 @@ extension BaseSuite {
blob/
.cursor/
skills/
pfw-ComposableArchitecture@ -> /Users/blob/.pfw/skills/ComposableArchitecture
pfw-SQLiteData@ -> /Users/blob/.pfw/skills/SQLiteData
pfw-ComposableArchitecture/
SKILL.md "# Composable Architecture"
references/
navigation.md "# Navigation"
pfw-SQLiteData/
SKILL.md "# SQLiteData"
.pfw/
machine "00000000-0000-0000-0000-000000000001"
sha "cafebeef"
Expand Down
40 changes: 40 additions & 0 deletions Tests/pfwTests/Internal/InMemoryFileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,46 @@ final class InMemoryFileSystem: FileSystem {
return children.sorted().map { url.appendingPathComponent($0) }
}
}

func copyItem(at srcURL: URL, to dstURL: URL) throws {
let sourcePath = normalize(srcURL)
let destinationPath = normalize(dstURL)
let destinationParent = normalize(dstURL.deletingLastPathComponent())
try state.withValue { state in
guard state.directories.contains(destinationParent) else {
throw Error.directoryNotFound(destinationParent)
}
guard state.files[destinationPath] == nil,
state.directories.contains(destinationPath) == false,
state.symbolicLinks[destinationPath] == nil
else {
throw Error.fileExists(destinationPath)
}

guard state.directories.contains(sourcePath) else {
throw Error.fileNotFound(sourcePath)
}

state.directories.insert(destinationPath)

let sourcePrefix = sourcePath.hasSuffix("/") ? sourcePath : sourcePath + "/"
let destinationPrefix = destinationPath.hasSuffix("/") ? destinationPath : destinationPath + "/"

for directory in state.directories {
if directory.hasPrefix(sourcePrefix) {
let suffix = directory.dropFirst(sourcePrefix.count)
state.directories.insert(destinationPrefix + suffix)
}
}

for (path, data) in state.files {
if path.hasPrefix(sourcePrefix) {
let suffix = path.dropFirst(sourcePrefix.count)
state.files[destinationPrefix + suffix] = data
}
}
}
}
}

extension InMemoryFileSystem: CustomStringConvertible {
Expand Down
Loading