pf 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ name: Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ id-token: write
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: pnpm/action-setup@v4
18
+ with:
19
+ version: 9
20
+
21
+ - uses: actions/setup-node@v4
22
+ with:
23
+ node-version: '24'
24
+ registry-url: 'https://registry.npmjs.org'
25
+ cache: 'pnpm'
26
+
27
+ - run: pnpm install --frozen-lockfile
28
+ - run: pnpm run build
29
+
30
+ - uses: JS-DevTools/npm-publish@v3
31
+ with:
32
+ token: ${{ secrets.NPM_TOKEN }}
33
+ provenance: true
package/build.mjs ADDED
@@ -0,0 +1,24 @@
1
+ import * as esbuild from "esbuild";
2
+
3
+ const watch = process.argv.includes("--watch");
4
+
5
+ const options = {
6
+ entryPoints: ["index.ts"],
7
+ bundle: true,
8
+ platform: "node",
9
+ target: "node18",
10
+ format: "esm",
11
+ outfile: "dist/index.js",
12
+ banner: {
13
+ js: "#!/usr/bin/env node",
14
+ },
15
+ };
16
+
17
+ if (watch) {
18
+ const ctx = await esbuild.context(options);
19
+ await ctx.watch();
20
+ console.log("Watching...");
21
+ } else {
22
+ await esbuild.build(options);
23
+ console.log("Built dist/index.js");
24
+ }
@@ -0,0 +1,197 @@
1
+ import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from "fs";
2
+ import { dirname, join } from "path";
3
+ import { homedir } from "os";
4
+ import { createInterface } from "readline";
5
+ import { success, dim } from "../utils.js";
6
+
7
+ function detectShell(): string | null {
8
+ const shell = process.env.SHELL || "";
9
+ if (shell.includes("zsh")) return "zsh";
10
+ if (shell.includes("bash")) return "bash";
11
+ if (shell.includes("fish")) return "fish";
12
+ return null;
13
+ }
14
+
15
+ function getCompletionPath(shell: string): string {
16
+ const home = homedir();
17
+ switch (shell) {
18
+ case "zsh":
19
+ return join(home, ".zsh/completions/_pf");
20
+ case "bash":
21
+ return join(home, ".bash_completion.d/pf");
22
+ case "fish":
23
+ return join(home, ".config/fish/completions/pf.fish");
24
+ default:
25
+ return "";
26
+ }
27
+ }
28
+
29
+ function getRcConfig(shell: string): { path: string; lines: string[] } | null {
30
+ const home = homedir();
31
+ switch (shell) {
32
+ case "zsh":
33
+ return {
34
+ path: join(home, ".zshrc"),
35
+ lines: [
36
+ 'fpath=(~/.zsh/completions $fpath)',
37
+ 'autoload -Uz compinit && compinit',
38
+ ],
39
+ };
40
+ case "bash":
41
+ return {
42
+ path: join(home, ".bashrc"),
43
+ lines: ['source ~/.bash_completion.d/pf'],
44
+ };
45
+ case "fish":
46
+ return null; // Fish auto-loads from completions dir
47
+ default:
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function fileContains(path: string, substr: string): boolean {
53
+ try {
54
+ const data = readFileSync(path, "utf-8");
55
+ return data.includes(substr);
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ function getCompletionScript(shell: string): string {
62
+ switch (shell) {
63
+ case "zsh":
64
+ return `#compdef pf
65
+
66
+ _pf() {
67
+ local -a commands worktrees
68
+ commands=(
69
+ 'new:Create a new worktree with a branch'
70
+ 'open:Open a worktree'
71
+ 'ls:List all tracked worktrees'
72
+ 'rm:Remove a worktree'
73
+ 'completion:Install shell completions'
74
+ )
75
+
76
+ _arguments -C \\
77
+ "1: :->command" \\
78
+ "*::arg:->args"
79
+
80
+ case "$state" in
81
+ command)
82
+ _describe 'command' commands
83
+ ;;
84
+ args)
85
+ case "$words[1]" in
86
+ open|rm)
87
+ worktrees=(\${(f)"\$(pf ls --plain 2>/dev/null | cut -f1)"})
88
+ _describe 'worktree' worktrees
89
+ ;;
90
+ esac
91
+ ;;
92
+ esac
93
+ }
94
+
95
+ _pf "$@"
96
+ `;
97
+ case "bash":
98
+ return `_pf() {
99
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
100
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
101
+ local commands="new open ls rm completion"
102
+
103
+ case "$prev" in
104
+ open|rm)
105
+ local worktrees="\$(pf ls --plain 2>/dev/null | cut -f1)"
106
+ COMPREPLY=($(compgen -W "$worktrees" -- "$cur"))
107
+ return
108
+ ;;
109
+ esac
110
+
111
+ COMPREPLY=($(compgen -W "$commands" -- "$cur"))
112
+ }
113
+ complete -F _pf pf
114
+ `;
115
+ case "fish":
116
+ return `complete -c pf -n "__fish_use_subcommand" -a new -d "Create a new worktree with a branch"
117
+ complete -c pf -n "__fish_use_subcommand" -a open -d "Open a worktree"
118
+ complete -c pf -n "__fish_use_subcommand" -a ls -d "List all tracked worktrees"
119
+ complete -c pf -n "__fish_use_subcommand" -a rm -d "Remove a worktree"
120
+ complete -c pf -n "__fish_use_subcommand" -a completion -d "Install shell completions"
121
+ complete -c pf -n "__fish_seen_subcommand_from open rm" -a "(pf ls --plain 2>/dev/null | cut -f1)" -d "worktree"
122
+ `;
123
+ default:
124
+ return "";
125
+ }
126
+ }
127
+
128
+ async function prompt(question: string): Promise<string> {
129
+ const rl = createInterface({
130
+ input: process.stdin,
131
+ output: process.stdout,
132
+ });
133
+ return new Promise((resolve) => {
134
+ rl.question(question, (answer) => {
135
+ rl.close();
136
+ resolve(answer);
137
+ });
138
+ });
139
+ }
140
+
141
+ export async function completionCommand(shell?: string): Promise<void> {
142
+ // If shell specified, output script to stdout
143
+ if (shell) {
144
+ const script = getCompletionScript(shell);
145
+ if (!script) {
146
+ console.error(`Unknown shell: ${shell}`);
147
+ process.exit(1);
148
+ }
149
+ console.log(script);
150
+ return;
151
+ }
152
+
153
+ // Interactive mode
154
+ const detectedShell = detectShell();
155
+ if (!detectedShell) {
156
+ console.error("Could not detect shell. Run: pf completion [bash|zsh|fish]");
157
+ process.exit(1);
158
+ }
159
+
160
+ const completionPath = getCompletionPath(detectedShell);
161
+ const rcConfig = getRcConfig(detectedShell);
162
+
163
+ let desc = `1. Write completion script to ${completionPath}\n`;
164
+ if (rcConfig && !fileContains(rcConfig.path, rcConfig.lines[0])) {
165
+ desc += `2. Add to ${rcConfig.path}:\n`;
166
+ for (const line of rcConfig.lines) {
167
+ desc += ` ${line}\n`;
168
+ }
169
+ }
170
+
171
+ console.log(`Install ${detectedShell} completions? This will:`);
172
+ console.log(desc);
173
+ const response = await prompt("Proceed? [y/N] ");
174
+
175
+ if (response.toLowerCase() !== "y" && response.toLowerCase() !== "yes") {
176
+ console.log("Canceled.");
177
+ return;
178
+ }
179
+
180
+ // Write completion script
181
+ mkdirSync(dirname(completionPath), { recursive: true });
182
+ writeFileSync(completionPath, getCompletionScript(detectedShell));
183
+ console.log(success("✓"), "Wrote", completionPath);
184
+
185
+ // Update rc file if needed
186
+ if (rcConfig && !fileContains(rcConfig.path, rcConfig.lines[0])) {
187
+ appendFileSync(rcConfig.path, "\n# pf completions\n" + rcConfig.lines.join("\n") + "\n");
188
+ console.log(success("✓"), "Updated", rcConfig.path);
189
+ }
190
+
191
+ console.log();
192
+ if (rcConfig) {
193
+ console.log("Restart your shell or run:", dim(`source ${rcConfig.path}`));
194
+ } else {
195
+ console.log("Restart your shell to enable completions.");
196
+ }
197
+ }
package/commands/ls.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { resolve } from "path";
2
+ import Table from "cli-table3";
3
+ import pc from "picocolors";
4
+ import { loadStore } from "../store.js";
5
+ import { getWorktreeBranch, getWorktreeStatus } from "../utils.js";
6
+
7
+ export function lsCommand(plain: boolean = false): void {
8
+ const store = loadStore();
9
+
10
+ if (store.worktrees.length === 0) {
11
+ console.log("No worktrees. Use 'pf new <name>' to create one.");
12
+ return;
13
+ }
14
+
15
+ // Detect current worktree
16
+ const cwd = process.cwd();
17
+ let currentWorktree = "";
18
+ for (const wt of store.worktrees) {
19
+ const absPath = resolve(wt.path);
20
+ if (cwd.startsWith(absPath)) {
21
+ currentWorktree = wt.name;
22
+ break;
23
+ }
24
+ }
25
+
26
+ if (plain) {
27
+ for (const wt of store.worktrees) {
28
+ const branch = getWorktreeBranch(wt.path);
29
+ const status = getWorktreeStatus(wt.path);
30
+ console.log(`${wt.name}\t${branch}\t${status}`);
31
+ }
32
+ return;
33
+ }
34
+
35
+ const table = new Table({
36
+ head: ["name", "branch", "git status"],
37
+ style: { head: [], border: [] },
38
+ chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" },
39
+ });
40
+
41
+ for (const wt of store.worktrees) {
42
+ const branch = getWorktreeBranch(wt.path);
43
+ const status = getWorktreeStatus(wt.path);
44
+ const name = wt.name === currentWorktree ? `${pc.green("*")}${wt.name}` : wt.name;
45
+ table.push([name, branch, status]);
46
+ }
47
+
48
+ console.log(table.toString());
49
+ }
@@ -0,0 +1,82 @@
1
+ import { basename, dirname, join } from "path";
2
+ import { mkdirSync } from "fs";
3
+ import {
4
+ loadStore,
5
+ saveStore,
6
+ addWorktree,
7
+ findWorktree,
8
+ getGitRoot,
9
+ getGitCommonDir,
10
+ } from "../store.js";
11
+ import {
12
+ branchExists,
13
+ isInsideWorktree,
14
+ getCurrentBranch,
15
+ createWorktree,
16
+ generateName,
17
+ spawnShell,
18
+ success,
19
+ bold,
20
+ dim,
21
+ } from "../utils.js";
22
+
23
+ export function newCommand(name?: string): void {
24
+ const worktreeName = name ?? generateName();
25
+
26
+ // Check if inside a worktree
27
+ if (isInsideWorktree()) {
28
+ console.error("Error: You are inside a pf worktree.");
29
+ console.error("");
30
+ console.error("Each time you run 'pf new' or 'pf open', pf starts a subshell in the");
31
+ console.error("worktree directory. To keep subshells shallow and avoid nesting, pf");
32
+ console.error("requires you to return to the main repo before opening another worktree.");
33
+ console.error("");
34
+ console.error("To exit the current worktree:");
35
+ console.error(" exit");
36
+ console.error("");
37
+ console.error("Then re-run:");
38
+ console.error(` pf new ${worktreeName}`);
39
+ process.exit(1);
40
+ }
41
+
42
+ // Check if branch already exists
43
+ if (branchExists(worktreeName)) {
44
+ console.error(`Error: branch '${worktreeName}' already exists`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // Check if worktree already exists in store
49
+ const store = loadStore();
50
+ if (findWorktree(store, worktreeName)) {
51
+ console.error(`Error: worktree '${worktreeName}' already exists - use 'pf open ${worktreeName}'`);
52
+ process.exit(1);
53
+ }
54
+
55
+ const baseBranch = getCurrentBranch();
56
+ const gitRoot = getGitRoot();
57
+ const gitDir = getGitCommonDir();
58
+ const repoName = basename(gitRoot);
59
+ const worktreePath = join(gitDir, "pf", "worktrees", worktreeName, repoName);
60
+
61
+ // Create directory
62
+ mkdirSync(dirname(worktreePath), { recursive: true });
63
+
64
+ // Create worktree
65
+ createWorktree(worktreeName, worktreePath);
66
+
67
+ // Track in store
68
+ addWorktree(store, worktreeName, worktreePath);
69
+ saveStore(store);
70
+
71
+ console.log();
72
+ console.log(success(bold(worktreeName)), dim(`(from ${baseBranch})`));
73
+ console.log(dim(" Subshell started. Type 'exit' to return."));
74
+ console.log();
75
+
76
+ // Spawn shell
77
+ spawnShell(worktreePath);
78
+
79
+ console.log();
80
+ console.log(dim(" Back in main worktree"));
81
+ console.log();
82
+ }
@@ -0,0 +1,61 @@
1
+ import { existsSync } from "fs";
2
+ import { loadStore, findWorktree } from "../store.js";
3
+ import {
4
+ isInsideWorktree,
5
+ hasUncommittedChanges,
6
+ getWorktreeBranch,
7
+ spawnShell,
8
+ success,
9
+ bold,
10
+ dim,
11
+ } from "../utils.js";
12
+
13
+ export function openCommand(name: string): void {
14
+ // Check if inside a worktree
15
+ if (isInsideWorktree()) {
16
+ console.error("Error: You are inside a pf worktree.");
17
+ console.error("");
18
+ console.error("Each time you run 'pf new' or 'pf open', pf starts a subshell in the");
19
+ console.error("worktree directory. To keep subshells shallow and avoid nesting, pf");
20
+ console.error("requires you to return to the main repo before opening another worktree.");
21
+ console.error("");
22
+ console.error("To exit the current worktree:");
23
+ console.error(" exit");
24
+ console.error("");
25
+ console.error("Then re-run:");
26
+ console.error(` pf open ${name}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ // Check for uncommitted changes
31
+ if (hasUncommittedChanges()) {
32
+ console.error("Error: uncommitted changes in current worktree - commit or stash first");
33
+ process.exit(1);
34
+ }
35
+
36
+ const store = loadStore();
37
+ const wt = findWorktree(store, name);
38
+
39
+ if (!wt) {
40
+ console.error(`Error: worktree '${name}' not found`);
41
+ process.exit(1);
42
+ }
43
+
44
+ if (!existsSync(wt.path)) {
45
+ console.error(`Error: worktree directory does not exist: ${wt.path}`);
46
+ process.exit(1);
47
+ }
48
+
49
+ const branch = getWorktreeBranch(wt.path);
50
+
51
+ console.log();
52
+ console.log(success(bold(name)), dim(`(${branch})`));
53
+ console.log(dim(" Subshell started. Type 'exit' to return."));
54
+ console.log();
55
+
56
+ spawnShell(wt.path);
57
+
58
+ console.log();
59
+ console.log(dim(" Back in main worktree"));
60
+ console.log();
61
+ }
package/commands/rm.ts ADDED
@@ -0,0 +1,61 @@
1
+ import { resolve } from "path";
2
+ import { loadStore, saveStore, findWorktree, removeWorktree as removeFromStore } from "../store.js";
3
+ import {
4
+ getWorktreeBranch,
5
+ removeWorktree,
6
+ pruneWorktrees,
7
+ deleteBranch,
8
+ success,
9
+ bold,
10
+ dim,
11
+ } from "../utils.js";
12
+
13
+ export function rmCommand(name: string, deleteBranchFlag: boolean = false): void {
14
+ const store = loadStore();
15
+ const wt = findWorktree(store, name);
16
+
17
+ if (!wt) {
18
+ console.error(`Error: worktree '${name}' not found`);
19
+ process.exit(1);
20
+ }
21
+
22
+ // Check if we're currently in this worktree
23
+ const cwd = process.cwd();
24
+ const absWtPath = resolve(wt.path);
25
+ const absCwd = resolve(cwd);
26
+ if (absCwd.startsWith(absWtPath)) {
27
+ console.error("Error: cannot remove current worktree - exit first");
28
+ process.exit(1);
29
+ }
30
+
31
+ // Get branch before removing
32
+ const branch = getWorktreeBranch(wt.path);
33
+
34
+ // Remove worktree
35
+ try {
36
+ removeWorktree(wt.path);
37
+ } catch {
38
+ // May already be gone
39
+ }
40
+
41
+ pruneWorktrees();
42
+
43
+ // Delete branch if requested
44
+ if (deleteBranchFlag && branch !== "[missing]" && branch !== "[detached]") {
45
+ try {
46
+ deleteBranch(branch);
47
+ } catch (err) {
48
+ console.error(`Warning: failed to delete branch ${branch}`);
49
+ }
50
+ }
51
+
52
+ // Remove from store
53
+ removeFromStore(store, name);
54
+ saveStore(store);
55
+
56
+ if (deleteBranchFlag && branch !== "[missing]" && branch !== "[detached]") {
57
+ console.log(success("Removed"), bold(name), dim("(branch deleted)"));
58
+ } else {
59
+ console.log(success("Removed"), bold(name));
60
+ }
61
+ }