workshell 0.0.6 → 0.1.0

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.
Files changed (3) hide show
  1. package/README.md +11 -6
  2. package/dist/index.js +277 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -20,7 +20,12 @@
20
20
  ## Install
21
21
 
22
22
  ```bash
23
- $ npm i -g workshell
23
+ $ npm i -g workshell # aliases to `wk`
24
+ $ wk --help
25
+ wk v0.0.6 - Open branches in ephemeral subshells
26
+
27
+ Usage: wk <command> [options]
28
+ # ...
24
29
  ```
25
30
 
26
31
  <br/>
@@ -34,7 +39,7 @@ There have been many attempts to nail a DX for parallel work in the agentic codi
34
39
  Here's how it works (key points in **bold**).
35
40
 
36
41
  - You open a Git branch with `wk open <branch>` or create a new one with `wk new <branch>`.
37
- - An ephemeral worktree is created for this branch (in `.git/workshell/worktrees`) and **opened in a fresh subshell**.
42
+ - An ephemeral worktree is created for this branch (in `~/.workshell/worktrees`) and **opened in a fresh subshell**.
38
43
  - You are now in an fresh checkout of your repo that is isolated on disk. Make changes with your agent/editor of choice and commit them.
39
44
  - You close the subshell with `wk close`. **The associated worktree is auto-pruned**.
40
45
  - Your changes still exist on the associated branch, as Git commits/branches are shared among all worktrees. The worktree is destroyed—but your commits aren't.
@@ -48,7 +53,7 @@ There have been many attempts to nail a DX for parallel work in the agentic codi
48
53
 
49
54
  Here's how it works (key points in **bold**).
50
55
 
51
- - **A fresh worktree is created** — The `wk` utility a) creates a new worktree in `.git/workshell/worktrees` and b) opens it in a *subshell*.
56
+ - **A fresh worktree is created** — The `wk` utility a) creates a new worktree in `~/.workshell/worktrees` and b) opens it in a *subshell*.
52
57
  - **Make edits and commit** — From the worksh your preferred IDE/agent.
53
58
  - **Commit your changes** — Though your file system is isolated, Git commit history (including branches) is still shared among all worktrees.
54
59
  - **Exit the subshell** — Run `wk close` to exit the subshell. As with `git switch`, `wk close` won't let you close the subshell if you have unstaged/uncommitted changes.
@@ -106,7 +111,7 @@ After cloning, the `main` branch is checked out. Let's say we want to start work
106
111
  $ wk new feat-1
107
112
 
108
113
  ✓ feat-1 (from main)
109
- Opened branch in ephemeral subshell at .git/workshell/worktrees/zod@feat-1
114
+ Opened branch in ephemeral subshell
110
115
  Type 'wk close' to return.
111
116
  ```
112
117
 
@@ -116,7 +121,7 @@ You're now in a workshell. Check where you are:
116
121
 
117
122
  ```bash
118
123
  $ pwd
119
- /Users/colinmcd94/Documents/repos/zod/.git/workshell/worktrees/zod@feat-1
124
+ ~/.workshell/worktrees/{repo-id}/zod@feat-1
120
125
 
121
126
  $ git branch --show-current
122
127
  feat-1
@@ -325,6 +330,6 @@ Currently only one setting is supported: `setup`.
325
330
  # the following variable substitutions are supported
326
331
  # `{{ branch }}` — The name of the opened branch, e.g. `feature/auth`
327
332
  # `{{ repo_path }}` — The absolute path to main repo, e.g. `/path/to/repo`
328
- # `{{ worktree_path }}` — The absolute path to worktree, e.g. `/path/to/repo/.git/workshell/worktrees/repo@feat`
333
+ # `{{ worktree_path }}` — The absolute path to worktree, e.g. `~/.workshell/worktrees/.../repo@feat`
329
334
  setup = "npm install && cp {{ repo_path }}/.env {{ worktree_path }}/.env"
330
335
  ```
package/dist/index.js CHANGED
@@ -5785,13 +5785,15 @@ var require_src = __commonJS({
5785
5785
  var import_picocolors4 = __toESM(require_picocolors(), 1);
5786
5786
 
5787
5787
  // commands/new.ts
5788
- import { basename as basename3, dirname as dirname3, join as join3, relative } from "path";
5788
+ import { basename as basename3, dirname as dirname3, join as join3 } from "path";
5789
5789
  import { mkdirSync as mkdirSync3 } from "fs";
5790
5790
 
5791
5791
  // store.ts
5792
5792
  import { execSync } from "child_process";
5793
5793
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5794
5794
  import { basename, dirname, isAbsolute, join } from "path";
5795
+ import { homedir } from "os";
5796
+ import { randomUUID } from "crypto";
5795
5797
 
5796
5798
  // node_modules/.pnpm/smol-toml@1.6.0/node_modules/smol-toml/dist/error.js
5797
5799
  function getLineColFromPtr(string, ptr) {
@@ -6480,6 +6482,25 @@ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
6480
6482
 
6481
6483
  // store.ts
6482
6484
  var STORE_FILE = "store.json";
6485
+ function getWorkshellRoot() {
6486
+ const xdgDataHome = process.env.XDG_DATA_HOME;
6487
+ if (xdgDataHome) {
6488
+ return join(xdgDataHome, "workshell");
6489
+ }
6490
+ return join(homedir(), ".workshell");
6491
+ }
6492
+ function getRepoId() {
6493
+ try {
6494
+ const id = execSync("git config --local workshell.id", { encoding: "utf-8" }).trim();
6495
+ if (id) {
6496
+ return id;
6497
+ }
6498
+ } catch {
6499
+ }
6500
+ const newId = randomUUID().replace(/-/g, "").slice(0, 8);
6501
+ execSync(`git config --local workshell.id "${newId}"`, { encoding: "utf-8" });
6502
+ return newId;
6503
+ }
6483
6504
  function getGitRoot() {
6484
6505
  return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
6485
6506
  }
@@ -6494,33 +6515,51 @@ function getMainWorktree() {
6494
6515
  return dirname(getGitCommonDir());
6495
6516
  }
6496
6517
  function getWorktreesDir() {
6497
- return join(getGitCommonDir(), "workshell", "worktrees");
6518
+ return join(getWorkshellRoot(), "worktrees", getRepoId());
6498
6519
  }
6499
6520
  function getStorePath() {
6500
- return join(getGitCommonDir(), "workshell");
6521
+ return getWorkshellRoot();
6501
6522
  }
6502
- function loadStore() {
6523
+ function loadGlobalStore() {
6503
6524
  const storePath = getStorePath();
6504
6525
  const filePath = join(storePath, STORE_FILE);
6505
6526
  if (!existsSync(filePath)) {
6506
- return { worktrees: {} };
6527
+ return { repos: {} };
6507
6528
  }
6508
6529
  try {
6509
6530
  const data = readFileSync(filePath, "utf-8");
6510
6531
  const raw = JSON.parse(data);
6511
- if (Array.isArray(raw.worktrees) || typeof raw.worktrees !== "object") {
6512
- return { worktrees: {}, auto_merge_prompt: raw.auto_merge_prompt };
6532
+ if (raw.repos && typeof raw.repos === "object") {
6533
+ return raw;
6513
6534
  }
6514
- return raw;
6535
+ return { repos: {} };
6515
6536
  } catch {
6516
- return { worktrees: {} };
6537
+ return { repos: {} };
6517
6538
  }
6518
6539
  }
6519
- function saveStore(store) {
6540
+ function saveGlobalStore(globalStore) {
6520
6541
  const storePath = getStorePath();
6521
6542
  mkdirSync(storePath, { recursive: true });
6522
6543
  const filePath = join(storePath, STORE_FILE);
6523
- writeFileSync(filePath, JSON.stringify(store, null, 2));
6544
+ writeFileSync(filePath, JSON.stringify(globalStore, null, 2));
6545
+ }
6546
+ function loadStore() {
6547
+ const globalStore = loadGlobalStore();
6548
+ const repoId = getRepoId();
6549
+ const repoData = globalStore.repos[repoId];
6550
+ if (!repoData) {
6551
+ return { worktrees: {} };
6552
+ }
6553
+ if (Array.isArray(repoData.worktrees) || typeof repoData.worktrees !== "object") {
6554
+ return { worktrees: {}, auto_merge_prompt: repoData.auto_merge_prompt };
6555
+ }
6556
+ return repoData;
6557
+ }
6558
+ function saveStore(store) {
6559
+ const globalStore = loadGlobalStore();
6560
+ const repoId = getRepoId();
6561
+ globalStore.repos[repoId] = store;
6562
+ saveGlobalStore(globalStore);
6524
6563
  }
6525
6564
  function getWorktreeMeta(store, id) {
6526
6565
  return store.worktrees[id];
@@ -7043,7 +7082,7 @@ function newCommand(branchName, fromBranch) {
7043
7082
  const parentBranch = getCurrentBranch();
7044
7083
  console.log();
7045
7084
  console.log(success(bold(branch)), dim(`(from ${cyan(baseBranch)})`));
7046
- console.log(dim(`Opened branch in ephemeral subshell at ${relative(mainWorktree, worktreePath)}`));
7085
+ console.log(dim(`Opened branch in ephemeral subshell`));
7047
7086
  console.log(dim("Type 'wk close' to return."));
7048
7087
  console.log();
7049
7088
  spawnShell(worktreePath, {
@@ -7060,7 +7099,7 @@ function newCommand(branchName, fromBranch) {
7060
7099
 
7061
7100
  // commands/open.ts
7062
7101
  import { mkdirSync as mkdirSync4 } from "fs";
7063
- import { basename as basename4, dirname as dirname4, join as join4, relative as relative2 } from "path";
7102
+ import { basename as basename4, dirname as dirname4, join as join4 } from "path";
7064
7103
  function openCommand(branch) {
7065
7104
  if (isInsideWorktree()) {
7066
7105
  const currentBranch = getCurrentBranch();
@@ -7112,7 +7151,7 @@ function openCommand(branch) {
7112
7151
  saveStore(store);
7113
7152
  console.log();
7114
7153
  console.log(success(bold(branch)));
7115
- console.log(dim(`Opened branch in ephemeral subshell at ${relative2(mainWorktree, worktreePath)}`));
7154
+ console.log(dim(`Opened branch in ephemeral subshell`));
7116
7155
  console.log(dim("Type 'exit' or 'wk close' to return."));
7117
7156
  console.log();
7118
7157
  spawnShell(worktreePath, {
@@ -8182,8 +8221,162 @@ function promptChoice() {
8182
8221
  }
8183
8222
  }
8184
8223
 
8224
+ // commands/pr.ts
8225
+ import { execSync as execSync6 } from "child_process";
8226
+ import { mkdirSync as mkdirSync5 } from "fs";
8227
+ import { basename as basename5, dirname as dirname5, join as join6 } from "path";
8228
+ function checkGhInstalled() {
8229
+ try {
8230
+ execSync6("gh --version", { stdio: "pipe", encoding: "utf-8" });
8231
+ } catch {
8232
+ console.error(fail("GitHub CLI (gh) is not installed."));
8233
+ console.error(dim(" Install it from: https://cli.github.com/"));
8234
+ process.exit(1);
8235
+ }
8236
+ }
8237
+ function checkGhAuthenticated() {
8238
+ try {
8239
+ execSync6("gh auth status", { stdio: "pipe", encoding: "utf-8" });
8240
+ } catch {
8241
+ console.error(fail("Not authenticated with GitHub CLI."));
8242
+ console.error(dim(" Run: gh auth login"));
8243
+ process.exit(1);
8244
+ }
8245
+ }
8246
+ function getPRInfo(prRef) {
8247
+ try {
8248
+ const output = execSync6(
8249
+ `gh pr view ${prRef} --json number,headRefName,headRepository,headRepositoryOwner,isCrossRepository`,
8250
+ { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
8251
+ );
8252
+ return JSON.parse(output);
8253
+ } catch (err) {
8254
+ const message = err instanceof Error && "stderr" in err ? err.stderr : String(err);
8255
+ if (message.includes("Could not resolve")) {
8256
+ console.error(fail(`PR '${prRef}' not found.`));
8257
+ console.error(dim(" Make sure you're in a GitHub repository and the PR exists."));
8258
+ } else if (message.includes("no pull requests found")) {
8259
+ console.error(fail(`No pull request found for '${prRef}'.`));
8260
+ } else {
8261
+ console.error(fail(`Failed to get PR info: ${message}`));
8262
+ }
8263
+ process.exit(1);
8264
+ }
8265
+ }
8266
+ function fetchPRBranch(prRef, prInfo) {
8267
+ try {
8268
+ execSync6(`git fetch origin ${prInfo.headRefName}:${prInfo.headRefName}`, { stdio: "pipe" });
8269
+ return;
8270
+ } catch {
8271
+ }
8272
+ const currentBranch = getCurrentBranch();
8273
+ try {
8274
+ execSync6(`gh pr checkout ${prRef}`, { stdio: "pipe" });
8275
+ execSync6(`git checkout "${currentBranch}"`, { stdio: "pipe" });
8276
+ return;
8277
+ } catch {
8278
+ try {
8279
+ execSync6(`git checkout "${currentBranch}"`, { stdio: "pipe" });
8280
+ } catch {
8281
+ }
8282
+ }
8283
+ try {
8284
+ execSync6(`git fetch origin pull/${prInfo.number}/head:${prInfo.headRefName}`, { stdio: "pipe" });
8285
+ return;
8286
+ } catch (fetchErr) {
8287
+ const message = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
8288
+ console.error(fail(`Failed to fetch PR branch: ${message}`));
8289
+ process.exit(1);
8290
+ }
8291
+ }
8292
+ function branchExistsLocally(branch) {
8293
+ try {
8294
+ execSync6(`git show-ref --verify --quiet refs/heads/${branch}`);
8295
+ return true;
8296
+ } catch {
8297
+ return false;
8298
+ }
8299
+ }
8300
+ function createWorktreeForPR(branch, path) {
8301
+ execSync6(`git worktree add "${path}" "${branch}"`, { stdio: "ignore" });
8302
+ }
8303
+ function prOpenCommand(prRef) {
8304
+ checkGhInstalled();
8305
+ checkGhAuthenticated();
8306
+ const prInfo = getPRInfo(prRef);
8307
+ const branch = prInfo.headRefName;
8308
+ const prNumber = prInfo.number;
8309
+ if (isInsideWorktree()) {
8310
+ const currentBranch = getCurrentBranch();
8311
+ console.log();
8312
+ console.log(warn(`You're inside a worktree (branch: ${cyan(currentBranch)})`));
8313
+ console.log(` This will nest subshells (subshell inside subshell).`);
8314
+ console.log();
8315
+ if (!confirm("Continue?")) {
8316
+ process.exit(0);
8317
+ }
8318
+ console.log();
8319
+ }
8320
+ const parentBranch = getCurrentBranch();
8321
+ const existingWorktreePath = getWorktreeForBranch(branch);
8322
+ if (existingWorktreePath) {
8323
+ const worktreeId2 = getWorktreeId(existingWorktreePath);
8324
+ const mainWorktree2 = getMainWorktree();
8325
+ console.log();
8326
+ console.log(success(bold(`PR #${prNumber}`)), dim(`branch: ${cyan(branch)} (existing worktree)`));
8327
+ console.log(dim("Type 'wk close' to return."));
8328
+ console.log();
8329
+ spawnShell(existingWorktreePath, {
8330
+ branch,
8331
+ worktreePath: existingWorktreePath,
8332
+ repoPath: mainWorktree2
8333
+ });
8334
+ console.log();
8335
+ console.log(success(`Back in ${bold(parentBranch)}`));
8336
+ const currentStore2 = loadStore();
8337
+ autoCleanupWorktree(worktreeId2, existingWorktreePath, currentStore2, saveStore);
8338
+ console.log();
8339
+ return;
8340
+ }
8341
+ if (!branchExistsLocally(branch)) {
8342
+ console.log(dim(`Fetching PR #${prNumber}...`));
8343
+ fetchPRBranch(prRef, prInfo);
8344
+ }
8345
+ if (!branchExistsLocally(branch)) {
8346
+ console.error(fail(`Failed to fetch branch '${branch}' for PR #${prNumber}`));
8347
+ process.exit(1);
8348
+ }
8349
+ const store = loadStore();
8350
+ const mainWorktree = getMainWorktree();
8351
+ const repoName = basename5(mainWorktree);
8352
+ const worktreeId = `${repoName}@${slugify(branch)}`;
8353
+ const worktreePath = join6(getWorktreesDir(), worktreeId);
8354
+ mkdirSync5(dirname5(worktreePath), { recursive: true });
8355
+ createWorktreeForPR(branch, worktreePath);
8356
+ initSubmodules(worktreePath);
8357
+ setWorktreeMeta(store, worktreeId, {
8358
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
8359
+ });
8360
+ saveStore(store);
8361
+ console.log();
8362
+ console.log(success(bold(`PR #${prNumber}`)), dim(`branch: ${cyan(branch)}`));
8363
+ console.log(dim(`Opened PR in ephemeral subshell`));
8364
+ console.log(dim("Type 'exit' or 'wk close' to return."));
8365
+ console.log();
8366
+ spawnShell(worktreePath, {
8367
+ branch,
8368
+ worktreePath,
8369
+ repoPath: mainWorktree
8370
+ });
8371
+ console.log();
8372
+ console.log(success(`Back in ${bold(parentBranch)}`));
8373
+ const currentStore = loadStore();
8374
+ autoCleanupWorktree(worktreeId, worktreePath, currentStore, saveStore);
8375
+ console.log();
8376
+ }
8377
+
8185
8378
  // index.ts
8186
- var VERSION = "0.0.6";
8379
+ var VERSION = "0.1.0";
8187
8380
  function printHelp() {
8188
8381
  const dim2 = import_picocolors4.default.dim;
8189
8382
  const cyan2 = import_picocolors4.default.cyan;
@@ -8195,6 +8388,7 @@ ${import_picocolors4.default.bold("Usage:")} ${cyan2("wk")} ${dim2("<command>")}
8195
8388
  ${import_picocolors4.default.bold("Commands:")}
8196
8389
  ${green2("new")} ${dim2("[branch]")} Create a branch and open it ${dim2("[--from <branch>]")}
8197
8390
  ${green2("open")} ${dim2("<branch>")} Open a branch in an ephemeral subshell
8391
+ ${green2("pr open")} ${dim2("<ref>")} Open a GitHub PR in an ephemeral subshell
8198
8392
  ${green2("close")} Exit current subshell
8199
8393
  ${green2("ls")} List open branches
8200
8394
  ${green2("status")} Show current branch
@@ -8350,6 +8544,51 @@ ${import_picocolors4.default.bold("Config locations")} ${dim2("(in precedence or
8350
8544
  ${cyan2(".git/workshell.toml")} Local only, not committed
8351
8545
  ${cyan2("workshell.toml")} Project root, can be committed`);
8352
8546
  }
8547
+ function printPrHelp() {
8548
+ const dim2 = import_picocolors4.default.dim;
8549
+ const cyan2 = import_picocolors4.default.cyan;
8550
+ console.log(`${import_picocolors4.default.bold("wk pr")} - Work with GitHub Pull Requests
8551
+
8552
+ ${import_picocolors4.default.bold("Usage:")} ${cyan2("wk pr")} ${dim2("<subcommand>")} ${dim2("[options]")}
8553
+
8554
+ ${import_picocolors4.default.bold("Subcommands:")}
8555
+ ${cyan2("open")} ${dim2("<ref>")} Open a PR in an ephemeral subshell
8556
+
8557
+ ${import_picocolors4.default.bold("Examples:")}
8558
+ ${dim2("$")} ${cyan2("wk pr open 123")} ${dim2("# Open PR by number")}
8559
+ ${dim2("$")} ${cyan2("wk pr open https://github.com/o/r/pull/123")} ${dim2("# Open PR by URL")}
8560
+
8561
+ ${import_picocolors4.default.bold("Requirements:")}
8562
+ Requires GitHub CLI (gh) to be installed and authenticated.
8563
+ ${dim2("Install: https://cli.github.com/")}
8564
+ ${dim2("Auth: gh auth login")}`);
8565
+ }
8566
+ function printPrOpenHelp() {
8567
+ const dim2 = import_picocolors4.default.dim;
8568
+ const cyan2 = import_picocolors4.default.cyan;
8569
+ console.log(`${import_picocolors4.default.bold("wk pr open")} - Open a GitHub PR in an ephemeral subshell
8570
+
8571
+ ${import_picocolors4.default.bold("Usage:")} ${cyan2("wk pr open")} ${dim2("<ref>")}
8572
+
8573
+ ${import_picocolors4.default.bold("Arguments:")}
8574
+ ${dim2("<ref>")} PR number, URL, or branch name ${dim2("(required)")}
8575
+
8576
+ ${import_picocolors4.default.bold("Examples:")}
8577
+ ${dim2("$")} ${cyan2("wk pr open 123")}
8578
+ ${dim2("$")} ${cyan2("wk pr open https://github.com/owner/repo/pull/123")}
8579
+ ${dim2("$")} ${cyan2("wk pr open feature-branch")}
8580
+
8581
+ ${import_picocolors4.default.bold("Description:")}
8582
+ Fetches the PR branch and opens it in an ephemeral subshell.
8583
+ If a worktree already exists for the branch, uses that.
8584
+ Otherwise, creates a new worktree automatically.
8585
+ Type 'wk close' to return.
8586
+
8587
+ ${import_picocolors4.default.bold("Requirements:")}
8588
+ Requires GitHub CLI (gh) to be installed and authenticated.
8589
+ ${dim2("Install: https://cli.github.com/")}
8590
+ ${dim2("Auth: gh auth login")}`);
8591
+ }
8353
8592
  function isHelp(arg) {
8354
8593
  return arg === "--help" || arg === "-h";
8355
8594
  }
@@ -8428,6 +8667,29 @@ switch (cmd) {
8428
8667
  }
8429
8668
  configCommand();
8430
8669
  break;
8670
+ case "pr": {
8671
+ const subCmd = args[1];
8672
+ if (!subCmd || isHelp(subCmd)) {
8673
+ printPrHelp();
8674
+ process.exit(0);
8675
+ }
8676
+ if (subCmd === "open") {
8677
+ if (isHelp(args[2])) {
8678
+ printPrOpenHelp();
8679
+ process.exit(0);
8680
+ }
8681
+ if (!args[2]) {
8682
+ console.error("Usage: wk pr open <pr-number|url|branch>");
8683
+ process.exit(1);
8684
+ }
8685
+ prOpenCommand(args[2]);
8686
+ } else {
8687
+ console.error(`Unknown pr subcommand: ${subCmd}`);
8688
+ console.error("Run 'wk pr --help' for usage.");
8689
+ process.exit(1);
8690
+ }
8691
+ break;
8692
+ }
8431
8693
  case "close":
8432
8694
  if (isHelp(args[1])) {
8433
8695
  printCloseHelp();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workshell",
3
- "version": "0.0.6",
3
+ "version": "0.1.0",
4
4
  "description": "Agent- and human-friendly Git multitasking, powered by worktrees",
5
5
  "type": "module",
6
6
  "license": "MIT",