agentsclimarketplace

Branded git path types

Skill kjuhwa/skills-hub/skills/typescript/branded-git-path-types

Use branded string types (`RepoPath`, `WorktreePath`, `BranchName`) with constructor helpers to prevent accidentally passing one string primitive where another is expected.From its SKILL.md

Install
npx -y skills add kjuhwa/skills-hub --skill branded-git-path-types

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

5.1 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it

Branded String Types for Git Paths and Branch Names

When to use

  • Your codebase has several kinds of string primitives that look the same but are semantically different: RepoPath (canonical git repo root), WorktreePath (git worktree directory), BranchName, CodebaseId, ConversationId, etc.
  • You keep hitting bugs where someone passes a branch name where a path is expected (or worse, a path where a branch name is expected — the shell swallows it silently).
  • You want the compiler to enforce the distinction with minimal runtime cost.

Steps

  1. Declare private brand symbols and intersect with string:

    declare const REPO_PATH_BRAND: unique symbol;
    declare const BRANCH_NAME_BRAND: unique symbol;
    declare const WORKTREE_PATH_BRAND: unique symbol;
    
    export type RepoPath = string & { readonly [REPO_PATH_BRAND]: true };
    export type BranchName = string & { readonly [BRANCH_NAME_BRAND]: true };
    export type WorktreePath = string & { readonly [WORKTREE_PATH_BRAND]: true };
    

    The brands never exist at runtime (phantom types). The string & intersection means you can pass a RepoPath anywhere a string is expected (great for library APIs) but cannot pass a plain string where RepoPath is required.

  2. Provide one cast helper per brand, which validates and asserts the type:

    export function toRepoPath(path: string): RepoPath {
      if (!path) throw new Error('RepoPath cannot be empty');
      return path as RepoPath;
    }
    export function toBranchName(name: string): BranchName {
      if (!name) throw new Error('BranchName cannot be empty');
      return name as BranchName;
    }
    

    Runtime validation inside the helper (empty-string rejection, maybe format regex) makes the cast safer than raw as RepoPath. The empty-string check alone has caught real bugs.

  3. Design function signatures to consume the branded types, forcing callers to go through the helpers:

    async function isBranchMerged(
      repoPath: RepoPath,
      branchName: BranchName,
      mainBranch: BranchName,
    ): Promise<boolean> { … }
    

    Now isBranchMerged('my-branch', repoPath, main) is a compile error — you physically cannot get the argument order wrong.

  4. Union helpers for functions that accept multiple kinds:

    async function hasUncommittedChanges(
      workingPath: RepoPath | WorktreePath
    ): Promise<boolean> { … }
    

    |-union the brands when a function works on either.

  5. Pair with discriminated-union error results:

    export type GitResult<T> = { ok: true; value: T } | { ok: false; error: GitError };
    export type GitError =
      | { code: 'not_a_repo'; path: string }
      | { code: 'permission_denied'; path: string }
      | { code: 'branch_not_found'; branch: string }
      | { code: 'no_space'; path: string }
      | { code: 'unknown'; message: string };
    

    At the package boundary, return a GitResult<T> instead of throwing raw strings. Callers pattern-match on code and get typed path / branch fields.

Counter / Caveats

  • Brands do not survive JSON round-trips. Deserializing from the DB gives you a string; you must go back through toRepoPath(row.path) to rebrand.
  • Brands are not runtime-distinguishable. typeof repoPath === 'string' is true. If you need runtime distinction (e.g. structured logging), carry the kind explicitly (e.g. { kind: 'repo', path }).
  • Too many brands becomes noise. Good candidates: identifiers from external systems (ids, handles, paths), and values where accidental swap is catastrophic. Bad candidates: every internal function parameter.
  • Helpers should throw on invalid input rather than returning Option<T> — at the boundary you either have a valid value or you don't want to continue.
  • If you're in strict mode, the unique symbol declaration trick requires skipLibCheck: false to stay robust. Consider a named const symbol if you hit library-type conflicts.

Evidence

  • packages/git/src/types.ts:1-26: full declaration of REPO_PATH_BRAND, BRANCH_NAME_BRAND, WORKTREE_PATH_BRAND plus constructors toRepoPath, toBranchName, toWorktreePath with empty-string guards.
  • Discriminated-union GitResult<T> + GitError at types.ts:29-37.
  • Consumers across the codebase: packages/git/src/branch.ts (every signature uses RepoPath / BranchName), packages/isolation/src/resolver.ts, packages/core/src/services/cleanup-service.ts.
  • Example union at packages/git/src/branch.ts:118: hasUncommittedChanges(workingPath: RepoPath | WorktreePath).
  • Commit SHA: d89bc767d291f52687beea91c9fcf155459be0d9.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 625,150. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.