Skip to content

V1.0.0 - #5

Merged
leogdion merged 2 commits into
mainfrom
v1.0.0
Jul 23, 2026
Merged

V1.0.0#5
leogdion merged 2 commits into
mainfrom
v1.0.0

Conversation

@leogdion

@leogdion leogdion commented Jul 22, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added comprehensive filesystem APIs for creating, reading, writing, appending, copying, moving, renaming, and deleting files and folders.
    • Added recursive and hidden-item enumeration, folder search paths, metadata access, and detailed operation errors.
    • Added support for Swift 6.4 and current Apple platform versions.
  • Documentation

    • Added fork attribution, licensing notices, release notes, and contributor guidance.
  • Chores

    • Added cross-platform CI, formatting, linting, compatibility, and safety checks.

leogdion and others added 2 commits July 22, 2026 09:54
* Update subrepo parent refs after squash [skip ci]

* Add BrightDigit CI scaffolding for Plot and Files.

Leaf Publish-stack packages had lint tooling but no workflows; mirror the
Ink/Contribute leaf template (no path-dep rewrite needed).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Raise Apple platform floors for Mutex and SyndiKit consumers.

Files/Publish stack need iOS 18 / tvOS 18 / watchOS 11 for Synchronization.Mutex; ContributeRSS/WordPress declare iOS/tvOS 16 to match standalone CI deps.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix subrepo CI: hosted xcode-27 macOS runners + lint

Publish stack (Files/Ink/Publish):
- Move build-macos + build-macos-platforms off [self-hosted, macOS] to the
  GitHub-hosted xcode-27 runner (/Applications/Xcode_27.0.app, Swift 6.4-dev,
  27.0 SDKs); add download-platform: true on the sim legs.
- Files: remove the stale legacy Files.xcodeproj (schemes Files-iOS/-macOS/-tvOS
  shadowed swift-build's auto-calculated "Files" scheme) and the dead
  buddybuild_postbuild.sh, so xcodebuild resolves the SPM auto-scheme like Plot.
- Ink: add iOS 18 / tvOS 18 / watchOS 11 platform floors for Synchronization.Mutex
  used in InkTests, mirroring Files.

Lint:
- TailwindKit: remove the superseded legacy 2022 Tailwind.* API
  (TailwindKit.swift's Tailwind struct + Flexbox/AspectRatio/Display/Breakpoints
  and their XCTest suites), exclude the Danger DSL script from SwiftLint; lint now
  clean (0 violations).
- ReadingTimePublishPlugin: doc-comment public API, split test helpers into
  fixtures files, explicit ACL, rename ReadingTime.swift -> ReadingTimeMetadata.swift
  to satisfy file_name; lint clean, 8 XCTest tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Files: add Windows path support (canonical forward-slash internally)

Files hardcoded POSIX `/` separators while Foundation returns native `\` paths on
Windows, producing broken hybrid paths (C:\Users\foo/.filesTest/, /C:/Users/foo/)
and an internal inconsistency where makeParentPath rejoined URL.pathComponents with
`/` so a folder's computed parent.path never string-== its stored path.

Fix: store every path in canonical forward-slash form; keep all `/`-based internal
logic; convert to native separators only at FileManager / URL(fileURLWithPath:)
boundaries. New Sources/Path.swift adds String.canonicalizedPath / .nativePath /
.isDriveRoot and Path.rootPath / .nativeSeparator, all no-ops off Windows (so
macOS/Linux behavior is provably unchanged). makeParentPath preserves the Windows
drive prefix (C:/Users/foo/ -> C:/Users/); `~` resolves via
homeDirectoryForCurrentUser (drops a POSIX-only HOME force-unwrap); Folder.root is
the current volume root on Windows.

Tests stay XCTest: 3 assertions made platform-aware via a rootPath/canonical helper
(no `#if` in test bodies), plus a filesystem-free PathTests suite exercising the
separator + drive-prefix math on every platform. All 71 tests pass on macOS; lint
clean (CI=1 LINT_MODE=STRICT). Pragmatic scope: UNC best-effort, == stays
case-sensitive on all platforms. Windows CI is the ground truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Files: use NSHomeDirectory() for ~ (fixes iOS/tvOS/watchOS regression)

homeDirectoryForCurrentUser is unavailable on iOS/tvOS/watchOS, so the previous
commit's ~ change broke the Apple-platform builds (which were green). NSHomeDirectory()
is available on every Apple platform AND is platform-aware on Windows (resolves the
user profile, not the POSIX-only HOME variable). Verified: iOS simulator build
succeeds; 71/71 tests pass on macOS; lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix remaining subrepo CI failures

* Cache lint tools and align Publish Ink pin
)

* Apply Wave 0 review feedback + preserve Sundell fork attribution

- fail-fast: true across all matrix legs
- Ubuntu coverage → sersoft-gmbh/swift-coverage-action@v5 (drop fork + fail-on-empty-output)
- Codecov: drop verbose, keep fail_ci_if_error: false
- build-macos-platforms: ungate watchOS, add visionOS leg
- .spi.yml normalized (swift_version 6.1); devcontainer → swiftlang/swift:nightly-6.4.x-noble
- Preserve John Sundell MIT attribution: add NOTICE + README fork note; guard header.sh to never rewrite upstream headers
- Add RELEASE_NOTES.md, .claude/agent-notes.md, Memory & Corrections Convention, .claude/skills/

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix .spi.yml swift_version (6.4); header.sh emits upstream John Sundell header

- .spi.yml swift_version 6.1 -> 6.4 (matches swift-tools-version:6.4)
- header.sh now emits each file's ORIGINAL upstream Sundell header (verbatim from
  JohnSundell/Files) for any Sundell-attributed file, instead of skipping — so the MIT
  attribution is actively maintained and consistent across the fork. Idempotent.
- Source header comments normalized to the upstream form (comment-only; no code changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Unify fork header.sh: single full-license John Sundell template, no per-package branching

header.sh is now byte-identical across Plot/Ink/Files and emits one consistent
upstream John Sundell header (full inline MIT license /** block) for every
Sundell-attributed file — package name + per-file year parameterized, no
if $package branching. Plot/Ink headers expand to the full-license form to match;
comment-only, no code changes. Idempotent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Simplify Sundell header.sh to compact /** block

Replace the dual-template fork header.sh with the two-version model: one
compact John Sundell header — Copyright (c) John Sundell 2019, MIT license see
LICENSE file. Update Scripts/lint.sh to invoke -c "John Sundell" -y 2019 and
re-header all Sources (comment-only; code bodies and LICENSE untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix header.sh strip to preserve /// doc comments

The prior compact-header commit stripped the /// API doc comments that sit
between the license and the first declaration, so swift-format's
AllPublicDeclarationsHaveDocumentation failed in CI (Plot 48, Files 4, Ink 2).
Narrow the awk strip to remove only the leading license block + // lines,
preserving /// docs (and any second /** doc block). Sources re-headered with
docs intact; code bodies and LICENSE untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change migrates the package to Swift 6.4, adds a filesystem abstraction with file/folder operations, introduces repository tooling and CI workflows, adds Claude skills and conventions, preserves attribution, and supplies broad cross-platform tests.

Changes

Files platform and repository migration

Layer / File(s) Summary
Agent skills and working conventions
.claude/..., CLAUDE.md
Adds repository directives and explicit-invocation skills for handoffs, grilling, workflows, research, specifications, tickets, and skill authoring.
Swift 6.4 tooling and CI
.github/..., .mise.toml, .swift-*, .swiftlint.yml, Scripts/*, .devcontainer/*
Adds multi-platform Swift CI, tool caching, linting, formatting, header management, unsafe-flag checks, cache cleanup, and development-container configuration.
Filesystem contracts and storage
Sources/Location.swift, Sources/Storage.swift, Sources/Path.swift, Sources/*Error*, Sources/LocationKind.swift
Introduces typed filesystem locations, path normalization, concurrency-safe storage, metadata, copying, moving, deletion, and typed errors.
File and folder APIs
Sources/File.swift, Sources/Folder*.swift, Sources/Storage+Folder.swift
Adds file I/O, folder creation and lookup, search-path resolution, content management, and recursive or hidden-item traversal.
Filesystem behavior tests
Tests/FilesTests/*, Tests/LinuxMain.swift, codecov.yml
Adds tests for filesystem operations, traversal, metadata, errors, path behavior, platform setup, and coverage configuration.

Estimated code review effort: 5 (Critical) | ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title is only a version label and does not describe the change in the pull request. Replace it with a concise summary of the main change, such as the release or feature area being introduced.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 95.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch v1.0.0

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
.claude/skills/claude-handoff/agents/openai.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

.claude/skills/grill-me/agents/openai.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

.claude/skills/grill-with-docs/agents/openai.yaml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'

  • 19 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 20

🧹 Nitpick comments (4)
.devcontainer/devcontainer.json (2)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Pin the development image to a reproducible Swift toolchain.

swiftlang/swift:nightly-6.4.x-noble can change without a repository change. Pin an exact image or digest and verify it matches the declared Swift version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/devcontainer.json at line 3, Update the devcontainer image
declaration to use an immutable Swift toolchain reference, such as an exact
version tag or image digest, and ensure it corresponds to the declared Swift
version instead of the floating nightly tag.

16-19: 🔒 Security & Privacy | 🔵 Trivial | 🏗️ Heavy lift

Use least privilege for the default devcontainer.

The default session runs as root and adds SYS_PTRACE plus seccomp=unconfined, broadening the impact of extensions and project scripts. Use the provisioned vscode user by default, and isolate debugger-only privileges in an opt-in profile if LLDB requires them.

Also applies to: 39-39

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.devcontainer/devcontainer.json around lines 16 - 19, Update the
devcontainer configuration to use the provisioned vscode user as the default
session user instead of root, and remove SYS_PTRACE and seccomp=unconfined from
the default runArgs. If LLDB requires these privileges, move them into a
separate opt-in debugger profile rather than applying them by default.
.github/actions/setup-tools/action.yml (1)

10-10: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin external actions to immutable SHAs.

actions/cache@v4 and jdx/mise-action@v4 execute code from movable tags. Pin them to reviewed full-length commit SHAs and retain the release tag in a comment for maintainability. GitHub recommends SHA pinning for third-party actions. (docs.github.com)

Also applies to: 18-23

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/setup-tools/action.yml at line 10, Update the external
action references in the setup-tools workflow, including actions/cache and
jdx/mise-action, to use reviewed immutable full-length commit SHAs instead of
movable version tags. Retain each original release tag in an adjacent comment
for maintainability.
Sources/Folder+SearchPath.swift (1)

28-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use typed throws for consistency with the rest of the API.

matching(_:in:resolvedBy:) is declared with a plain throws -> Folder, while every sibling API in this cohort (Storage+Folder.swift, Folder+Files.swift, Folder+Subfolders.swift) consistently uses typed throws (throws(LocationError)). Since this method only ever throws LocationError, aligning it now — before v1.0.0 ships — keeps the public error-handling contract consistent for consumers pattern-matching on typed throws elsewhere.

♻️ Proposed fix
     public static func matching(
       _ searchPath: FileManager.SearchPathDirectory,
       in domain: FileManager.SearchPathDomainMask = .userDomainMask,
       resolvedBy fileManager: FileManager = .default
-    ) throws -> Folder {
+    ) throws(LocationError) -> Folder {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Folder`+SearchPath.swift around lines 28 - 43, Update
Folder.matching(_:in:resolvedBy:) to use typed throws(LocationError) instead of
untyped throws, preserving its existing LocationError behavior and return value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/skills/loop-me/SKILL.md:
- Line 8: Update the stateful /grilling workflow in the skill instructions to
require explicit confirmation before deleting or replacing any workflow spec;
identify and show the affected files first, then proceed only after
confirmation, while preserving the existing create and edit behavior.

In @.claude/skills/to-spec/SKILL.md:
- Around line 17-19: Update the workflow description around “Check with the user
that these seams match their expectations” to remove this confirmation step and
preserve the promised no-interview behavior, or explicitly reframe the skill as
requiring user approval before publishing. Keep the subsequent spec publication
and ready-for-agent labeling steps unchanged.

In @.github/actions/setup-tools/action.yml:
- Around line 13-15: Update the cache key and restore key in the setup-tools
action to include the runner image identity in addition to runner.os,
runner.arch, and the .mise.toml hash; also pin the mise version explicitly in
the action input. Keep the existing cache configuration and ensure all key
components consistently distinguish the Ubuntu, macOS, and Windows toolchains.

In @.github/workflows/cleanup-caches.yml:
- Around line 16-29: Update the cache cleanup logic around getActionsCacheList
to paginate through all cache entries before reporting completion. Use
github.paginate or an explicit page loop, preserve deletion via
deleteActionsCacheById for every returned cache, and ensure the final count
reflects all deleted caches for ref.

In @.github/workflows/Files.yml:
- Line 121: Replace the unsupported runs-on label xcode-27 in both macOS jobs
with a supported macos-* GitHub-hosted runner label, and update the
corresponding Xcode path configuration to match that runner image.
- Around line 246-247: Update the lint job condition alongside its needs
declaration so it includes always(), allowing lint to run when optional matrix
dependencies such as build-windows or other skipped jobs are not executed.
Preserve the existing cancellation, failure, and “ci skip” checks.

In `@RELEASE_NOTES.md`:
- Around line 9-10: Update the PR reference in the release note to identify this
release as PR `#5`. If retaining PR `#1`, explicitly label it as the upstream or
monorepo change it represents so the traceability references are unambiguous.

In `@Scripts/header.sh`:
- Around line 76-109: Preserve existing upstream headers verbatim: update the
header-processing logic in Scripts/header.sh around the awk cleanup to skip
files that already contain a header or add a non-mutating check-only mode,
rather than rewriting them. In Scripts/lint.sh lines 83-86, remove the mutating
header.sh invocation and call only the non-mutating validation behavior.

In `@Scripts/lint.sh`:
- Line 66: Update the directory-change command around pushd in the lint script
to quote PACKAGE_DIR and immediately terminate with a nonzero status if entering
the directory fails, preventing subsequent relative commands from running in the
caller’s directory.
- Line 80: Update the Swift build invocation in Scripts/lint.sh to run through
the existing MISE_BIN-managed TOOL_CMD wrapper, matching the formatting,
linting, and periphery commands. Preserve the --build-tests argument while
ensuring the build uses the same mise-managed toolchain.

In `@Sources/File.swift`:
- Around line 56-65: Update append(_:) to use the throwing
handle.write(contentsOf:) API so write failures enter the existing WriteError
conversion, and register handle.closeFile() with defer immediately after opening
it to guarantee closure on all exit paths. Preserve the current seek-to-end
behavior and deployment compatibility as required by the supported targets.

In `@Sources/Folder.ChildSequence.swift`:
- Around line 7-14: Update the documentation for Folder.ChildSequence.recursive
to describe its actual depth-first, subtree-first traversal order instead of
breadth-first ordering. Keep the implementation and complexity claim unchanged.

In `@Sources/Folder`+Children.swift:
- Around line 62-111: Update ChildIterator.next() to use an iterative loop
instead of recursively calling next() when skipping hidden entries, exhausted
nested iterators, or children that do not match the requested kind. Preserve the
current traversal order, nested iterator handling, and nil result when all
entries are exhausted, while keeping stack usage bounded regardless of skip
count.

In `@Sources/Location.swift`:
- Around line 128-145: Validate newName at the start of
Location.rename(to:keepExtension:) before appending the extension or
constructing the destination: reject empty values, "." and "..", and any name
containing path separators, including both slash styles. Throw the appropriate
LocationError for invalid names, while preserving the existing parent lookup,
extension handling, and storage.move behavior for valid basenames.
- Around line 49-55: Update the nameExcludingExtension property to remove only
the final extension while preserving periods in the remaining filename, so
“archive.tar.gz” returns “archive.tar”. Join the retained components with "." or
use the equivalent URL path-extension removal behavior.

In `@Sources/Storage.swift`:
- Around line 44-51: Update the tilde expansion logic in the path
canonicalization flow to expand only when the canonicalized path is exactly "~"
or begins with "~/". Leave names such as "~draft" unchanged, while preserving
the existing NSHomeDirectory-based expansion for valid home-directory prefixes.
- Around line 53-62: Update the parent traversal logic around the
path-resolution loop to recognize “..” only when it is a complete path
component, not when it appears at the end of a directory name such as
“report..”. Validate component boundaries or resolve split path components while
preserving genuine parent traversal and existing missing-folder errors.
- Around line 30-42: Update validatedPath to resolve every relative input
against the current working directory before storing or comparing it, not only
parent-directory paths. Preserve canonical separator handling and existing
file/folder validation, including folder trailing-slash normalization, while
ensuring equivalent relative and absolute inputs produce the same stored path.

In `@Sources/Storage`+Folder.swift:
- Around line 31-76: Update createSubfolder(at:) and createFile(at:contents:) to
enforce the documented already-exists contract: check whether the target path
exists before creation and throw the corresponding folderAlreadyExists or
fileAlreadyExists WriteError reason. Ensure the named overloads inherit this
behavior through these methods, while preserving the existing creation and
failure handling for non-existing paths.

In `@Tests/FilesTests/FilesTests.swift`:
- Around line 33-105: Add the missing test entries for
testCreatingFileIfNeededAtPath and testNameExcludingExtensionWithoutExtension to
the allTests array, preserving the existing naming and function-reference format
so test runners such as LinuxMain execute both tests.

---

Nitpick comments:
In @.devcontainer/devcontainer.json:
- Line 3: Update the devcontainer image declaration to use an immutable Swift
toolchain reference, such as an exact version tag or image digest, and ensure it
corresponds to the declared Swift version instead of the floating nightly tag.
- Around line 16-19: Update the devcontainer configuration to use the
provisioned vscode user as the default session user instead of root, and remove
SYS_PTRACE and seccomp=unconfined from the default runArgs. If LLDB requires
these privileges, move them into a separate opt-in debugger profile rather than
applying them by default.

In @.github/actions/setup-tools/action.yml:
- Line 10: Update the external action references in the setup-tools workflow,
including actions/cache and jdx/mise-action, to use reviewed immutable
full-length commit SHAs instead of movable version tags. Retain each original
release tag in an adjacent comment for maintainability.

In `@Sources/Folder`+SearchPath.swift:
- Around line 28-43: Update Folder.matching(_:in:resolvedBy:) to use typed
throws(LocationError) instead of untyped throws, preserving its existing
LocationError behavior and return value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 856fb561-b4ff-41ab-b995-c08ac14164ba

📥 Commits

Reviewing files that changed from the base of the PR and between e85f2b4 and c48be64.

⛔ Files ignored due to path filters (1)
  • Files.xcodeproj/project.xcworkspace/contents.xcworkspacedata is excluded by !**/*.xcworkspace/contents.xcworkspacedata
📒 Files selected for processing (81)
  • .claude/agent-notes.md
  • .claude/skills/claude-handoff/SKILL.md
  • .claude/skills/claude-handoff/agents/openai.yaml
  • .claude/skills/grill-me/SKILL.md
  • .claude/skills/grill-me/agents/openai.yaml
  • .claude/skills/grill-with-docs/SKILL.md
  • .claude/skills/grill-with-docs/agents/openai.yaml
  • .claude/skills/grilling/SKILL.md
  • .claude/skills/grilling/agents/openai.yaml
  • .claude/skills/handoff/SKILL.md
  • .claude/skills/handoff/agents/openai.yaml
  • .claude/skills/loop-me/SKILL.md
  • .claude/skills/loop-me/agents/openai.yaml
  • .claude/skills/research/SKILL.md
  • .claude/skills/research/agents/openai.yaml
  • .claude/skills/to-spec/SKILL.md
  • .claude/skills/to-spec/agents/openai.yaml
  • .claude/skills/to-tickets/SKILL.md
  • .claude/skills/to-tickets/agents/openai.yaml
  • .claude/skills/writing-great-skills/GLOSSARY.md
  • .claude/skills/writing-great-skills/SKILL.md
  • .claude/skills/writing-great-skills/agents/openai.yaml
  • .devcontainer/devcontainer.json
  • .github/actions/setup-tools/action.yml
  • .github/workflows/Files.yml
  • .github/workflows/check-unsafe-flags.yml
  • .github/workflows/claude-code-review.yml
  • .github/workflows/claude.yml
  • .github/workflows/cleanup-caches.yml
  • .github/workflows/swift-source-compat.yml
  • .mise.toml
  • .periphery.yml
  • .spi.yml
  • .swift-format
  • .swift-version
  • .swiftlint.yml
  • .travis.yml
  • CLAUDE.md
  • Files.xcodeproj/project.pbxproj
  • Files.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • Files.xcodeproj/xcshareddata/xcschemes/Files-iOS.xcscheme
  • Files.xcodeproj/xcshareddata/xcschemes/Files-macOS.xcscheme
  • Files.xcodeproj/xcshareddata/xcschemes/Files-tvOS.xcscheme
  • NOTICE
  • Package.swift
  • README.md
  • RELEASE_NOTES.md
  • Scripts/header.sh
  • Scripts/lint.sh
  • Sources/File.swift
  • Sources/FileManager+Files.swift
  • Sources/Files.swift
  • Sources/FilesError.swift
  • Sources/Folder+Children.swift
  • Sources/Folder+Contents.swift
  • Sources/Folder+Files.swift
  • Sources/Folder+SearchPath.swift
  • Sources/Folder+Subfolders.swift
  • Sources/Folder.ChildSequence.swift
  • Sources/Folder.swift
  • Sources/Location.swift
  • Sources/LocationErrorReason.swift
  • Sources/LocationKind.swift
  • Sources/Path.swift
  • Sources/ReadErrorReason.swift
  • Sources/Storage+Folder.swift
  • Sources/Storage.swift
  • Sources/String+Files.swift
  • Sources/WriteErrorReason.swift
  • Tests/FilesTests/FilesTests+Enumeration.swift
  • Tests/FilesTests/FilesTests+Files.swift
  • Tests/FilesTests/FilesTests+Folders.swift
  • Tests/FilesTests/FilesTests+Metadata.swift
  • Tests/FilesTests/FilesTests+MovingCopying.swift
  • Tests/FilesTests/FilesTests+Operations.swift
  • Tests/FilesTests/FilesTests+SearchPath.swift
  • Tests/FilesTests/FilesTests.swift
  • Tests/FilesTests/PathTests.swift
  • Tests/LinuxMain.swift
  • buddybuild_postbuild.sh
  • codecov.yml
💤 Files with no reviewable changes (8)
  • Files.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  • Files.xcodeproj/xcshareddata/xcschemes/Files-macOS.xcscheme
  • Files.xcodeproj/xcshareddata/xcschemes/Files-iOS.xcscheme
  • Files.xcodeproj/xcshareddata/xcschemes/Files-tvOS.xcscheme
  • buddybuild_postbuild.sh
  • Files.xcodeproj/project.pbxproj
  • .travis.yml
  • Sources/Files.swift

argument-hint: "A workflow to design, or nothing to go find one"
---

Run a stateful `/grilling` session whose only output is **workflow** specs. Use the grilling discipline — relentless, one question at a time, a recommended answer attached to each — aimed at the vocabulary and goal below. Create, edit, and delete specs as the grilling resolves things.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require confirmation before deleting or replacing workflow specs.

Line 8 permits destructive changes as grilling evolves. Show the affected files and require explicit confirmation before deletion or overwrite.

🧰 Tools
🪛 SkillSpector (2.3.11)

[warning] 27: [EA2] Autonomous Decision Making: Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Remediation: Add human-in-the-loop confirmation for destructive, irreversible, or high-impact operations. Never auto-execute commands that modify files, send data, or alter system state.

(Excessive Agency (EA2))

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/loop-me/SKILL.md at line 8, Update the stateful /grilling
workflow in the skill instructions to require explicit confirmation before
deleting or replacing any workflow spec; identify and show the affected files
first, then proceed only after confirmation, while preserving the existing
create and edit behavior.

Source: Linters/SAST tools

Comment on lines +17 to +19
Check with the user that these seams match their expectations.

3. Write the spec using the template below, then publish it to the project issue tracker. Apply the `ready-for-agent` triage label - no need for additional triage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove or explicitly reframe the user confirmation step.

This skill promises “no interview,” but this step pauses for user feedback before publishing. Either remove the confirmation or change the description to make this an approval-based workflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/skills/to-spec/SKILL.md around lines 17 - 19, Update the workflow
description around “Check with the user that these seams match their
expectations” to remove this confirmation step and preserve the promised
no-interview behavior, or explicitly reframe the skill as requiring user
approval before publishing. Keep the subsequent spec publication and
ready-for-agent labeling steps unchanged.

Comment on lines +13 to +15
key: mise-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.mise.toml') }}
restore-keys: |
mise-v2-${{ runner.os }}-${{ runner.arch }}-

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== action file =="
cat -n .github/actions/setup-tools/action.yml

echo
echo "== workflow usages of setup-tools =="
rg -n "setup-tools|setup-mise|mise-action|tools" .github/workflows -S || true

echo
echo "== relevant refs =="
git ls-files | rg '(^|/)(workflow|action)\.ya?ml$|.mise\.toml|mise\.lock|package(-lock)?\.json|pnpm-lock\.yaml|yarn.lock'

Repository: brightdigit/Files

Length of output: 1635


🌐 Web query:

jdx mise-action version default latest README cache key useCacheDefault cache

💡 Result:

In jdx/mise-action, the cache input is enabled by default [1][2]. The action automatically manages cache keys based on a template that includes the mise version, platform, and hashes of configuration files and install arguments [3][4]. Key technical details regarding cache management: 1. Default Version: The version input defaults to latest if not specified [1][2]. 2. Default Cache Key: The action uses a default template to generate a cache key [3]. You can view the logic for this in the src/index.ts file or by using the {{default}} template variable in your own configuration [3][4]. 3. Customization: You can override the entire cache key using the cache_key input [4]. This input supports template variables such as: - {{version}}: The mise version [4]. - {{default}}: The processed default cache key (useful for extending) [4]. - {{file_hash}}: Hash of configuration files [4]. - {{platform}}, {{mise_env}}, {{install_args_hash}}, and environment variables via {{env.VAR_NAME}} [5][4]. 4. Invalidating Cache: If you need to force an update or invalidate the cache, you can change the cache_key_prefix (defaulting to mise-v1) or provide a custom cache_key [4]. While there have been discussions and reports regarding cache key prefix matching and save behavior [6], the standard usage involves allowing the action to manage these keys automatically based on your version and tool configuration [1][4]. If you find the action is using an older cached version, verify that your version input is set correctly or consider appending a custom string to cache_key using {{default}}-suffix [4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== cached workflow section =="
sed -n '230,270p' .github/workflows/Files.yml

echo
echo "== .mise.toml =="
cat -n .mise.toml

echo
echo "== search for pinning/workflow runner policy =="
rg -n "needs: approval|permissions|runs-on:|uses: actions/cache|uses: jdx/mise-action|setup-tools|sha|commit|pin|unmovable|mutable" .github/workloads .github/workflows README.md package.json .git/config 2>/dev/null || true

Repository: brightdigit/Files

Length of output: 4197


🌐 Web query:

jdx mise-action source default cache key includes path source install arguments

💡 Result:

The default cache key mechanism in the jdx/mise-action GitHub Action is constructed using a Handlebars template [1][2]. It does not explicitly include every file path as a literal component, but it incorporates the state of your environment through hashed values to ensure cache integrity [2]. The default cache key template is designed to include the following components [2][3]: 1. {{cache_key_prefix}}: Defaults to mise-v1. 2. {{platform}}: Includes the target OS, architecture, and a runner image discriminator (derived from process.env.ImageOS or self-hosted to prevent cross-provider collisions) [4][2]. 3. {{file_hash}}: A SHA256 hash of all mise configuration files (such as mise.toml or .tool-versions), which ensures the cache is invalidated when your configuration changes [2][3]. 4. {{version}}: The mise version being installed [2]. 5. {{mise_env}}: The value of the MISE_ENV environment variable [2]. 6. {{install_args_hash}}: A SHA256 hash of the sorted tools provided via install arguments, ensuring that changes to the requested tools trigger a cache refresh [2]. 7. {{bootstrap_hash}}: A SHA256 hash incorporating bootstrap mode, skip lists, and arguments [2]. The template specifically places the file_hash at the end of the key to act as a terminator, preventing unintended prefix-matching issues where one workflow's cache might be incorrectly restored by another [5][3]. If you need to customize this, you can override the entire key or extend the default key using the cache_key input [1]: - uses: jdx/mise-action@v4 with: cache_key: "{{default}}-custom-suffix" Note that while mise itself has local caching mechanisms for environments and tool data (configured via MISE_CACHE_DIR and settings like env_cache) [6][7][8], the jdx/mise-action cache refers specifically to GitHub Actions caching of the installed tool runtimes [1][9].

Citations:


🌐 Web query:

jdx mise-action v4 version input default latest missing version installed mise version GitHub Actions

💡 Result:

In jdx/mise-action v4, the version input defaults to latest [1][2]. This refers to the version of the mise binary itself that the action installs on the runner, not the versions of the tools defined in your project [1][3]. If you are encountering issues where a tool remains "missing" after the action runs, or if you suspect mise is not updating to the latest versions of your tools, consider the following technical context: 1. Installation vs. Environment: By default, mise-action runs mise install [1][2]. If you have a mise.lock file in your repository, the action automatically appends --locked to this command [4][3][5]. If tools are missing, ensure they are correctly defined in your configuration (e.g., .mise.toml or .tool-versions) [1]. 2. Cache Persistence: The action caches mise and its installed tools by default [1][6]. A known issue is that if the cache key does not change, the action may not re-install tools even if your configuration has changed [6]. The cache key is derived from your configuration files (like .mise.toml or mise.lock) [7]. If tools are not updating as expected, verify that your configuration files are correctly included in the action's hashing process [7]. 3. Troubleshooting "Missing" Tools: - If a tool appears missing even after installation, it may be due to a discrepancy between where the tool is installed and where mise expects it to be (e.g., if a plugin installs binaries in a non-standard location that mise env does not correctly map to PATH) [8]. - If you rely on mise's "not_found_auto_install" feature, be aware that mise cannot auto-install a tool that has never had any version installed previously, because it cannot determine which plugin provides the requested binary [9]. You must perform an explicit mise install for the tool at least once [9]. - If you are on a self-hosted runner, ensure there are no conflicting mise installations in different locations (e.g., ~/.local/bin/mise vs ~/.local/share/mise/bin/mise) [6]. 4. Action Version: Ensure you are using jdx/mise-action@v4 as it includes important fixes for caching and environment variable handling that were absent in v3 [4][5]. If you are still seeing issues, check the "Actions" tab in your repository to see if mise install is actually executing and if it is reporting any errors in the logs [1]. If it is not running, ensure that install: true (the default) is not being inadvertently disabled [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== build matrix runs-on =="
sed -n '135,190p' .github/workflows/Files.yml

Repository: brightdigit/Files

Length of output: 2629


Make the cached mise toolchain identity explicit.

This cache is shared across ubuntu-latest, xcode-27, and Windows runners used by Files.yml, so runner.os/runner.arch can overlap and restore a toolchain that doesn’t match the runner image. Pin mise explicitly in the action input and include the runner image in the cache key with the config hash, e.g. mise-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.mise.toml') }}.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/setup-tools/action.yml around lines 13 - 15, Update the
cache key and restore key in the setup-tools action to include the runner image
identity in addition to runner.os, runner.arch, and the .mise.toml hash; also
pin the mise version explicitly in the action input. Keep the existing cache
configuration and ensure all key components consistently distinguish the Ubuntu,
macOS, and Windows toolchains.

Comment on lines +16 to +29
const caches = await github.rest.actions.getActionsCacheList({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
});
for (const cache of caches.data.actions_caches) {
console.log(`Deleting cache: ${cache.key}`);
await github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
});
}
console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg '(^|/)\.github/workflows/cleanup-caches\.ya?ml$|yml|yaml' || true

echo "== cleanup workflow =="
sed -n '1,220p' .github/workflows/cleanup-caches.yml 2>/dev/null || true

echo "== relevant package/action versions =="
rg -n "`@actions/github`|rest\.actions\.getActionsCacheList|getActionsCacheList|paginate|per_page" .github/workflows -S

Repository: brightdigit/Files

Length of output: 2050


🌐 Web query:

GitHub REST API get cache entries list cache entries per page default pagination actions api docs

💡 Result:

To retrieve a list of GitHub Actions cache entries using the GitHub REST API, you can use the following endpoint [1][2][3]: GET /repos/{owner}/{repo}/actions/caches This endpoint supports pagination to help manage large sets of cache entries [1][4]. The default pagination parameters are as follows [1][4]: per_page: The number of results to return per page. The default is 30, with a maximum limit of 100 [1][4]. page: The page number of the results to fetch. The default is 1 [1][4]. You can adjust these values by appending them as query parameters to your request, for example: /repos/{owner}/{repo}/actions/caches?per_page=100&page=1 [1][4][5]. For more information on handling paginated responses, including the use of the Link header to navigate between pages, see the official GitHub documentation on using pagination in the REST API [6].

Citations:


Paginate cache deletion.

getActionsCacheList returns 30 entries by default, so repositories with more caches will leave later pages undeleted. Use github.paginate or an explicit page loop while deleting.

Proposed fix
-            const caches = await github.rest.actions.getActionsCacheList({
+            const caches = await github.paginate(github.rest.actions.getActionsCacheList, {
               owner: context.repo.owner,
               repo: context.repo.repo,
               ref: ref,
+              per_page: 100,
             });
-            for (const cache of caches.data.actions_caches) {
+            for (const cache of caches) {
               console.log(`Deleting cache: ${cache.key}`);
               await github.rest.actions.deleteActionsCacheById({
                 owner: context.repo.owner,
                 repo: context.repo.repo,
                 cache_id: cache.id,
               });
             }
-            console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`);
+            console.log(`Deleted ${caches.length} cache(s) for ${ref}`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const caches = await github.rest.actions.getActionsCacheList({
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
});
for (const cache of caches.data.actions_caches) {
console.log(`Deleting cache: ${cache.key}`);
await github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
});
}
console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`);
const caches = await github.paginate(github.rest.actions.getActionsCacheList, {
owner: context.repo.owner,
repo: context.repo.repo,
ref: ref,
per_page: 100,
});
for (const cache of caches) {
console.log(`Deleting cache: ${cache.key}`);
await github.rest.actions.deleteActionsCacheById({
owner: context.repo.owner,
repo: context.repo.repo,
cache_id: cache.id,
});
}
console.log(`Deleted ${caches.length} cache(s) for ${ref}`);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/cleanup-caches.yml around lines 16 - 29, Update the cache
cleanup logic around getActionsCacheList to paginate through all cache entries
before reporting completion. Use github.paginate or an explicit page loop,
preserve deletion via deleteActionsCacheById for every returned cache, and
ensure the final count reflects all deleted caches for ref.

# (/Applications/Xcode_27.0.app; Xcode 27 ships Swift 6.4-dev).
build-macos:
name: Build on macOS
runs-on: xcode-27

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use a supported GitHub-hosted macOS runner label.

xcode-27 is not a supported GitHub-hosted runner label, so both macOS jobs will remain queued or fail to start. Select a supported macos-* label and align the configured Xcode path with that image. (docs.github.com)

Also applies to: 177-177

🧰 Tools
🪛 actionlint (1.7.12)

[error] 121-121: label "xcode-27" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-intel", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file

(runner-label)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/Files.yml at line 121, Replace the unsupported runs-on
label xcode-27 in both macOS jobs with a supported macos-* GitHub-hosted runner
label, and update the corresponding Xcode path configuration to match that
runner image.

Source: Linters/SAST tools

Comment thread Sources/Storage.swift
Comment on lines +30 to +42
private static func validatedPath(from inputPath: String) throws(LocationError) -> String {
// Accept either separator on input and work in canonical (forward-slash) form.
var path = inputPath.canonicalizedPath

switch LocationType.kind {
case .file:
guard !path.isEmpty else {
throw LocationError(path: path, reason: .emptyFilePath)
}
case .folder:
if path.isEmpty { path = FileManager.default.currentDirectoryPath.canonicalizedPath }
if !path.hasSuffix("/") { path += "/" }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve ordinary relative paths before storing them.

Only ../ paths become absolute. File(path: "file") stores "file", so changing the process working directory later makes the same File instance target a different file; it also compares unequal to the equivalent absolute path. Resolve every relative input against the current directory during initialization. The supplied metadata test exercises this accepted relative-path API.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Storage.swift` around lines 30 - 42, Update validatedPath to resolve
every relative input against the current working directory before storing or
comparing it, not only parent-directory paths. Preserve canonical separator
handling and existing file/folder validation, including folder trailing-slash
normalization, while ensuring equivalent relative and absolute inputs produce
the same stored path.

Comment thread Sources/Storage.swift
Comment on lines +44 to +51
if path.hasPrefix("~") {
// `NSHomeDirectory()` is platform-aware (Windows resolves the user profile,
// not the POSIX-only `HOME` variable) and, unlike
// `FileManager.homeDirectoryForCurrentUser`, is available on every Apple
// platform (iOS/tvOS/watchOS included).
let homePath = NSHomeDirectory().canonicalizedPath
path = homePath + path.dropFirst()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Expand only a standalone home-directory prefix.

A valid relative filename such as ~draft is rewritten to <home>draft. Restrict expansion to ~ and ~/ (after separator canonicalization); leave other names beginning with ~ unchanged.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Storage.swift` around lines 44 - 51, Update the tilde expansion logic
in the path canonicalization flow to expand only when the canonicalized path is
exactly "~" or begins with "~/". Leave names such as "~draft" unchanged, while
preserving the existing NSHomeDirectory-based expansion for valid home-directory
prefixes.

Comment thread Sources/Storage.swift
Comment on lines +53 to +62
while let parentReferenceRange = path.range(of: "../") {
let folderPath = String(path[..<parentReferenceRange.lowerBound])
let parentPath = makeParentPath(for: folderPath) ?? "/"

guard FileManager.default.locationExists(at: parentPath, kind: .folder) else {
throw LocationError(path: parentPath, reason: .missing)
}

path.replaceSubrange(..<parentReferenceRange.upperBound, with: parentPath)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Recognize .. only as a complete path component.

range(of: "../") treats /tmp/report../file as parent traversal even though report.. is a valid directory name. Split and resolve path components, or validate component boundaries before replacing, so ordinary paths are not redirected or rejected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Storage.swift` around lines 53 - 62, Update the parent traversal
logic around the path-resolution loop to recognize “..” only when it is a
complete path component, not when it appears at the end of a directory name such
as “report..”. Validate component boundaries or resolve split path components
while preserving genuine parent traversal and existing missing-folder errors.

Comment on lines +31 to +76
internal func createSubfolder(at folderPath: String) throws(WriteError) -> Folder {
let folderPath = path + folderPath.canonicalizedPath.removingPrefix("/")

guard folderPath != path else {
throw WriteError(path: folderPath, reason: .emptyPath)
}

do {
try FileManager.default.createDirectory(
atPath: folderPath.nativePath,
withIntermediateDirectories: true
)

let storage = try Storage(path: folderPath)
return Folder(storage: storage)
} catch {
throw WriteError(path: folderPath, reason: .folderCreationFailed(error))
}
}

internal func createFile(at filePath: String, contents: Data?) throws(WriteError) -> File {
let filePath = path + filePath.canonicalizedPath.removingPrefix("/")

guard let parentPath = makeParentPath(for: filePath) else {
throw WriteError(path: filePath, reason: .emptyPath)
}

if parentPath != path {
do {
try FileManager.default.createDirectory(
atPath: parentPath.nativePath,
withIntermediateDirectories: true
)
} catch {
throw WriteError(path: parentPath, reason: .folderCreationFailed(error))
}
}

guard FileManager.default.createFile(atPath: filePath.nativePath, contents: contents),
let storage = try? Storage<File>(path: filePath)
else {
throw WriteError(path: filePath, reason: .fileCreationFailed)
}

return File(storage: storage)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate relevant files =="
fd -a 'Storage\+Folder\.swift|Folder\+Subfolders\.swift|Folder\+Files\.swift|WriteErrorReason\.swift|WriteError\.swift' . | sed 's#^\./##'

echo
echo "== Storage+Folder.swift =="
storage_file="$(fd 'Storage\+Folder\.swift' . | head -n 1)"
if [ -n "${storage_file:-}" ]; then
  wc -l "$storage_file"
  cat -n "$storage_file"
fi

echo
echo "== Folder+Subfolders.swift =="
subfolders_file="$(fd 'Folder\+Subfolders\.swift' . | head -n 1)"
if [ -n "${subfolders_file:-}" ]; then
  wc -l "$subfolders_file"
  cat -n "$subfolders_file"
fi

echo
echo "== Folder+Files.swift =="
files_file="$(fd 'Folder\+Files\.swift' . | head -n 1)"
if [ -n "${files_file:-}" ]; then
  wc -l "$files_file"
  cat -n "$files_file"
fi

echo
echo "== WriteErrorReason.swift =="
reason_file="$(fd 'WriteErrorReason\.swift' . | head -n 1)"
if [ -n "${reason_file:-}" ]; then
  wc -l "$reason_file"
  cat -n "$reason_file"
fi

Repository: brightdigit/Files

Length of output: 13713


🌐 Web query:

Apple FileManager createDirectory withIntermediateDirectories existing directory behavior no error returns true

💡 Result:

In Apple's FileManager API, the methods createDirectory(at:withIntermediateDirectories:attributes:) and createDirectory(atPath:withIntermediateDirectories:attributes:) behave as follows regarding existing directories: If you set the withIntermediateDirectories parameter to true, the method will succeed (i.e., not throw an error) if the target directory already exists [1][2]. Important distinction in Swift vs. Objective-C: In Swift, these methods are marked with the throws keyword and return Void [3]. You do not receive a boolean return value to check for success [3]. Instead, the method succeeds if it completes without throwing an error [3]. If the directory already exists (and withIntermediateDirectories is true), the method simply returns normally [3][2]. If a failure occurs—such as a lack of permissions or an intermediate path element being a file instead of a directory—an error is thrown, which you must handle using a do-catch block [4][5][3]. In Objective-C, the equivalent methods return a BOOL indicating success or failure, and they take an NSError pointer to provide error details if the operation fails [6]. In that context, the method returns YES (true) if the directory was created, or YES (true) if the directory already existed and withIntermediateDirectories was set to true [1][2]. It returns NO (false) only if an error occurred [1][2].

Citations:


🌐 Web query:

Apple FileManager createFile atPath contents existing file overwrites behavior

💡 Result:

The FileManager method createFile(atPath:contents:attributes:) (or createFileAtPath:contents:attributes: in Objective-C) is designed to overwrite existing files [1][2][3]. If a file already exists at the specified path, this method will overwrite its contents, provided the current process has the necessary file system privileges to perform the operation [1][2]. When successful, the method returns true (or YES in Objective-C) [1][2][4]. Note that it also returns true if the file already exists and was successfully overwritten [1]. If the operation fails—for example, due to a lack of permissions—it returns false (or NO) [1][4]. While this method remains available for compatibility, Apple's documentation notes that the NSData methods (such as writing data to a URL) should generally be preferred as the primary way to create or write files, as they provide better error handling via NSError [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tests referencing createSubfolder/createFile/CreateError =="
rg -n "createSubfolder|createFile|fileAlreadyExists|folderAlreadyExists|CreateError" Tests Sources --glob '!**/.build/**' || true

echo
echo "== inspect WriteErrorReason.swift around line bounds =="
cat -n Sources/WriteErrorReason.swift

echo
echo "== inspect Storage+Folder.swift imports/types =="
sed -n '1,90p' Sources/Storage+Folder.swift

Repository: brightdigit/Files

Length of output: 21201


Make createSubfolder/createFile fail when the target already exists, or make the public docs match the implemented overwrite behavior.

createDirectory(..., withIntermediateDirectories: true) succeeds for an existing directory, and createFile(atPath:contents:) can overwrite an existing file, so createSubfolder(at:)/createSubfolder(named:) and createFile(at:contents:)/createFile(named:contents:) do not honor their fold erAlreadyExists/fileAlreadyExists-style documented contract. Add pre-existence checks with appropriate WriteErrorReason cases, or update the docs to make the existing-path behavior explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Storage`+Folder.swift around lines 31 - 76, Update
createSubfolder(at:) and createFile(at:contents:) to enforce the documented
already-exists contract: check whether the target path exists before creation
and throw the corresponding folderAlreadyExists or fileAlreadyExists WriteError
reason. Ensure the named overloads inherit this behavior through these methods,
while preserving the existing creation and failure handling for non-existing
paths.

Comment on lines +33 to +105
internal static let allTests = [
("testCreatingAndDeletingFile", testCreatingAndDeletingFile),
("testCreatingFileAtPath", testCreatingFileAtPath),
(
"testDroppingLeadingSlashWhenCreatingFileAtPath",
testDroppingLeadingSlashWhenCreatingFileAtPath
),
("testCreatingAndDeletingFolder", testCreatingAndDeletingFolder),
("testCreatingSubfolderAtPath", testCreatingSubfolderAtPath),
(
"testDroppingLeadingSlashWhenCreatingSubfolderAtPath",
testDroppingLeadingSlashWhenCreatingSubfolderAtPath
),
("testReadingFileAsString", testReadingFileAsString),
("testReadingFileAsInt", testReadingFileAsInt),
("testRenamingFile", testRenamingFile),
("testRenamingFileWithNameIncludingExtension", testRenamingFileWithNameIncludingExtension),
("testReadingFileWithRelativePath", testReadingFileWithRelativePath),
("testReadingFileWithTildePath", testReadingFileWithTildePath),
("testReadingFileFromCurrentFoldersParent", testReadingFileFromCurrentFoldersParent),
(
"testReadingFileWithMultipleParentReferencesWithinPath",
testReadingFileWithMultipleParentReferencesWithinPath
),
("testRenamingFolder", testRenamingFolder),
("testAccesingFileByPath", testAccesingFileByPath),
("testAccessingSubfolderByPath", testAccessingSubfolderByPath),
("testEmptyingFolder", testEmptyingFolder),
("testEmptyingFolderWithHiddenFiles", testEmptyingFolderWithHiddenFiles),
("testCheckingEmptyFolders", testCheckingEmptyFolders),
("testMovingFiles", testMovingFiles),
("testCopyingFiles", testCopyingFiles),
("testCopyingFolders", testCopyingFolders),
("testEnumeratingFiles", testEnumeratingFiles),
("testEnumeratingFilesIncludingHidden", testEnumeratingFilesIncludingHidden),
("testEnumeratingFilesRecursively", testEnumeratingFilesRecursively),
("testEnumeratingSubfolders", testEnumeratingSubfolders),
("testEnumeratingSubfoldersRecursively", testEnumeratingSubfoldersRecursively),
(
"testRenamingFoldersWhileEnumeratingSubfoldersRecursively",
testRenamingFoldersWhileEnumeratingSubfoldersRecursively
),
("testFirstAndLastInFileSequence", testFirstAndLastInFileSequence),
("testConvertingFileSequenceToRecursive", testConvertingFileSequenceToRecursive),
("testModificationDate", testModificationDate),
("testParent", testParent),
("testRootFolderParentIsNil", testRootFolderParentIsNil),
("testRootSubfolderParentIsRoot", testRootSubfolderParentIsRoot),
("testOpeningFileWithEmptyPathThrows", testOpeningFileWithEmptyPathThrows),
("testDeletingNonExistingFileThrows", testDeletingNonExistingFileThrows),
("testWritingDataToFile", testWritingDataToFile),
("testWritingStringToFile", testWritingStringToFile),
("testAppendingDataToFile", testAppendingDataToFile),
("testAppendingStringToFile", testAppendingStringToFile),
("testFileDescription", testFileDescription),
("testFolderDescription", testFolderDescription),
("testFilesDescription", testFilesDescription),
("testSubfoldersDescription", testSubfoldersDescription),
("testMovingFolderContents", testMovingFolderContents),
("testMovingFolderHiddenContents", testMovingFolderHiddenContents),
("testAccessingHomeFolder", testAccessingHomeFolder),
("testAccessingCurrentWorkingDirectory", testAccessingCurrentWorkingDirectory),
("testNameExcludingExtensionWithLongFileName", testNameExcludingExtensionWithLongFileName),
("testRelativePaths", testRelativePaths),
("testRelativePathIsAbsolutePathForNonParent", testRelativePathIsAbsolutePathForNonParent),
("testCreateFileIfNeeded", testCreateFileIfNeeded),
("testCreateFolderIfNeeded", testCreateFolderIfNeeded),
("testCreateSubfolderIfNeeded", testCreateSubfolderIfNeeded),
("testCreatingFileWithString", testCreatingFileWithString),
("testFolderContainsFile", testFolderContainsFile),
("testFolderContainsSubfolder", testFolderContainsSubfolder),
("testErrorDescriptions", testErrorDescriptions),
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Two new tests are missing from allTests.

testCreatingFileIfNeededAtPath (Tests/FilesTests/FilesTests+Files.swift lines 47-61) and testNameExcludingExtensionWithoutExtension (Tests/FilesTests/FilesTests+Metadata.swift lines 165-173) are defined but not listed here. Wherever this array drives test execution (e.g. Tests/LinuxMain.swift), these tests will silently never run.

🐛 Proposed fix
     ("testReadingFileAsString", testReadingFileAsString),
     ("testReadingFileAsInt", testReadingFileAsInt),
+    ("testCreatingFileIfNeededAtPath", testCreatingFileIfNeededAtPath),
     ("testRenamingFile", testRenamingFile),
     ("testNameExcludingExtensionWithLongFileName", testNameExcludingExtensionWithLongFileName),
+    ("testNameExcludingExtensionWithoutExtension", testNameExcludingExtensionWithoutExtension),
     ("testRelativePaths", testRelativePaths),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
internal static let allTests = [
("testCreatingAndDeletingFile", testCreatingAndDeletingFile),
("testCreatingFileAtPath", testCreatingFileAtPath),
(
"testDroppingLeadingSlashWhenCreatingFileAtPath",
testDroppingLeadingSlashWhenCreatingFileAtPath
),
("testCreatingAndDeletingFolder", testCreatingAndDeletingFolder),
("testCreatingSubfolderAtPath", testCreatingSubfolderAtPath),
(
"testDroppingLeadingSlashWhenCreatingSubfolderAtPath",
testDroppingLeadingSlashWhenCreatingSubfolderAtPath
),
("testReadingFileAsString", testReadingFileAsString),
("testReadingFileAsInt", testReadingFileAsInt),
("testRenamingFile", testRenamingFile),
("testRenamingFileWithNameIncludingExtension", testRenamingFileWithNameIncludingExtension),
("testReadingFileWithRelativePath", testReadingFileWithRelativePath),
("testReadingFileWithTildePath", testReadingFileWithTildePath),
("testReadingFileFromCurrentFoldersParent", testReadingFileFromCurrentFoldersParent),
(
"testReadingFileWithMultipleParentReferencesWithinPath",
testReadingFileWithMultipleParentReferencesWithinPath
),
("testRenamingFolder", testRenamingFolder),
("testAccesingFileByPath", testAccesingFileByPath),
("testAccessingSubfolderByPath", testAccessingSubfolderByPath),
("testEmptyingFolder", testEmptyingFolder),
("testEmptyingFolderWithHiddenFiles", testEmptyingFolderWithHiddenFiles),
("testCheckingEmptyFolders", testCheckingEmptyFolders),
("testMovingFiles", testMovingFiles),
("testCopyingFiles", testCopyingFiles),
("testCopyingFolders", testCopyingFolders),
("testEnumeratingFiles", testEnumeratingFiles),
("testEnumeratingFilesIncludingHidden", testEnumeratingFilesIncludingHidden),
("testEnumeratingFilesRecursively", testEnumeratingFilesRecursively),
("testEnumeratingSubfolders", testEnumeratingSubfolders),
("testEnumeratingSubfoldersRecursively", testEnumeratingSubfoldersRecursively),
(
"testRenamingFoldersWhileEnumeratingSubfoldersRecursively",
testRenamingFoldersWhileEnumeratingSubfoldersRecursively
),
("testFirstAndLastInFileSequence", testFirstAndLastInFileSequence),
("testConvertingFileSequenceToRecursive", testConvertingFileSequenceToRecursive),
("testModificationDate", testModificationDate),
("testParent", testParent),
("testRootFolderParentIsNil", testRootFolderParentIsNil),
("testRootSubfolderParentIsRoot", testRootSubfolderParentIsRoot),
("testOpeningFileWithEmptyPathThrows", testOpeningFileWithEmptyPathThrows),
("testDeletingNonExistingFileThrows", testDeletingNonExistingFileThrows),
("testWritingDataToFile", testWritingDataToFile),
("testWritingStringToFile", testWritingStringToFile),
("testAppendingDataToFile", testAppendingDataToFile),
("testAppendingStringToFile", testAppendingStringToFile),
("testFileDescription", testFileDescription),
("testFolderDescription", testFolderDescription),
("testFilesDescription", testFilesDescription),
("testSubfoldersDescription", testSubfoldersDescription),
("testMovingFolderContents", testMovingFolderContents),
("testMovingFolderHiddenContents", testMovingFolderHiddenContents),
("testAccessingHomeFolder", testAccessingHomeFolder),
("testAccessingCurrentWorkingDirectory", testAccessingCurrentWorkingDirectory),
("testNameExcludingExtensionWithLongFileName", testNameExcludingExtensionWithLongFileName),
("testRelativePaths", testRelativePaths),
("testRelativePathIsAbsolutePathForNonParent", testRelativePathIsAbsolutePathForNonParent),
("testCreateFileIfNeeded", testCreateFileIfNeeded),
("testCreateFolderIfNeeded", testCreateFolderIfNeeded),
("testCreateSubfolderIfNeeded", testCreateSubfolderIfNeeded),
("testCreatingFileWithString", testCreatingFileWithString),
("testFolderContainsFile", testFolderContainsFile),
("testFolderContainsSubfolder", testFolderContainsSubfolder),
("testErrorDescriptions", testErrorDescriptions),
]
internal static let allTests = [
("testCreatingAndDeletingFile", testCreatingAndDeletingFile),
("testCreatingFileAtPath", testCreatingFileAtPath),
(
"testDroppingLeadingSlashWhenCreatingFileAtPath",
testDroppingLeadingSlashWhenCreatingFileAtPath
),
("testCreatingAndDeletingFolder", testCreatingAndDeletingFolder),
("testCreatingSubfolderAtPath", testCreatingSubfolderAtPath),
(
"testDroppingLeadingSlashWhenCreatingSubfolderAtPath",
testDroppingLeadingSlashWhenCreatingSubfolderAtPath
),
("testReadingFileAsString", testReadingFileAsString),
("testReadingFileAsInt", testReadingFileAsInt),
("testCreatingFileIfNeededAtPath", testCreatingFileIfNeededAtPath),
("testRenamingFile", testRenamingFile),
("testRenamingFileWithNameIncludingExtension", testRenamingFileWithNameIncludingExtension),
("testReadingFileWithRelativePath", testReadingFileWithRelativePath),
("testReadingFileWithTildePath", testReadingFileWithTildePath),
("testReadingFileFromCurrentFoldersParent", testReadingFileFromCurrentFoldersParent),
(
"testReadingFileWithMultipleParentReferencesWithinPath",
testReadingFileWithMultipleParentReferencesWithinPath
),
("testRenamingFolder", testRenamingFolder),
("testAccesingFileByPath", testAccesingFileByPath),
("testAccessingSubfolderByPath", testAccessingSubfolderByPath),
("testEmptyingFolder", testEmptyingFolder),
("testEmptyingFolderWithHiddenFiles", testEmptyingFolderWithHiddenFiles),
("testCheckingEmptyFolders", testCheckingEmptyFolders),
("testMovingFiles", testMovingFiles),
("testCopyingFiles", testCopyingFiles),
("testCopyingFolders", testCopyingFolders),
("testEnumeratingFiles", testEnumeratingFiles),
("testEnumeratingFilesIncludingHidden", testEnumeratingFilesIncludingHidden),
("testEnumeratingFilesRecursively", testEnumeratingFilesRecursively),
("testEnumeratingSubfolders", testEnumeratingSubfolders),
("testEnumeratingSubfoldersRecursively", testEnumeratingSubfoldersRecursively),
(
"testRenamingFoldersWhileEnumeratingSubfoldersRecursively",
testRenamingFoldersWhileEnumeratingSubfoldersRecursively
),
("testFirstAndLastInFileSequence", testFirstAndLastInFileSequence),
("testConvertingFileSequenceToRecursive", testConvertingFileSequenceToRecursive),
("testModificationDate", testModificationDate),
("testParent", testParent),
("testRootFolderParentIsNil", testRootFolderParentIsNil),
("testRootSubfolderParentIsRoot", testRootSubfolderParentIsRoot),
("testOpeningFileWithEmptyPathThrows", testOpeningFileWithEmptyPathThrows),
("testDeletingNonExistingFileThrows", testDeletingNonExistingFileThrows),
("testWritingDataToFile", testWritingDataToFile),
("testWritingStringToFile", testWritingStringToFile),
("testAppendingDataToFile", testAppendingDataToFile),
("testAppendingStringToFile", testAppendingStringToFile),
("testFileDescription", testFileDescription),
("testFolderDescription", testFolderDescription),
("testFilesDescription", testFilesDescription),
("testSubfoldersDescription", testSubfoldersDescription),
("testMovingFolderContents", testMovingFolderContents),
("testMovingFolderHiddenContents", testMovingFolderHiddenContents),
("testAccessingHomeFolder", testAccessingHomeFolder),
("testAccessingCurrentWorkingDirectory", testAccessingCurrentWorkingDirectory),
("testNameExcludingExtensionWithLongFileName", testNameExcludingExtensionWithLongFileName),
("testNameExcludingExtensionWithoutExtension", testNameExcludingExtensionWithoutExtension),
("testRelativePaths", testRelativePaths),
("testRelativePathIsAbsolutePathForNonParent", testRelativePathIsAbsolutePathForNonParent),
("testCreateFileIfNeeded", testCreateFileIfNeeded),
("testCreateFolderIfNeeded", testCreateFolderIfNeeded),
("testCreateSubfolderIfNeeded", testCreateSubfolderIfNeeded),
("testCreatingFileWithString", testCreatingFileWithString),
("testFolderContainsFile", testFolderContainsFile),
("testFolderContainsSubfolder", testFolderContainsSubfolder),
("testErrorDescriptions", testErrorDescriptions),
]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Tests/FilesTests/FilesTests.swift` around lines 33 - 105, Add the missing
test entries for testCreatingFileIfNeededAtPath and
testNameExcludingExtensionWithoutExtension to the allTests array, preserving the
existing naming and function-reference format so test runners such as LinuxMain
execute both tests.

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.83223% with 37 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@e85f2b4). Learn more about missing BASE report.

Files with missing lines Patch % Lines
Sources/Folder.swift 66.66% 8 Missing ⚠️
Sources/File.swift 81.57% 7 Missing ⚠️
Sources/Storage+Folder.swift 88.88% 5 Missing ⚠️
Sources/Folder+SearchPath.swift 66.66% 4 Missing ⚠️
Sources/Folder.ChildSequence.swift 87.87% 4 Missing ⚠️
Sources/Location.swift 95.00% 3 Missing ⚠️
Sources/Storage.swift 96.38% 3 Missing ⚠️
Sources/Path.swift 75.00% 2 Missing ⚠️
Sources/Folder+Subfolders.swift 95.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main       #5   +/-   ##
=======================================
  Coverage        ?   91.83%           
=======================================
  Files           ?       15           
  Lines           ?      453           
  Branches        ?        0           
=======================================
  Hits            ?      416           
  Misses          ?       37           
  Partials        ?        0           
Flag Coverage Δ
macos 91.83% <91.83%> (?)
noble 92.93% <92.93%> (?)
spm 91.83% <91.83%> (?)
swift-6.4 92.93% <92.93%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leogdion
leogdion merged commit 923a4c9 into main Jul 23, 2026
38 checks passed
@leogdion
leogdion deleted the v1.0.0 branch July 26, 2026 21:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant