@dfosco/storyboard-core 4.2.5 → 4.2.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-core",
3
- "version": "4.2.5",
3
+ "version": "4.2.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -66,7 +66,7 @@ The default location is in `.agents/plans`, but the user may ask for a specific
66
66
 
67
67
  - **create** (`.agents/skills/create/SKILL.md`) — Walks through creating Storyboard assets: prototype, external prototype, flow, page, canvas, object, or record.
68
68
 
69
- - **worktree** (`.agents/skills/worktree/SKILL.md`) — Creates a git worktree in `.worktrees/<branch-name>` and switches into it.
69
+ - **worktree** (`.agents/skills/worktree/SKILL.md`) — Creates a git worktree in `worktrees/<branch-name>` and switches into it.
70
70
 
71
71
  - **tools** (`.agents/skills/tools/SKILL.md`) — Reference for creating toolbar tools: config schema, handlers, surfaces, and render types.
72
72
 
@@ -34,7 +34,7 @@ Invoke the **worktree** skill to create a git worktree for the feature branch.
34
34
  - If the user provided an explicit branch name, use that instead.
35
35
  - Use `ask_user` to confirm the branch name before creating the worktree.
36
36
 
37
- After the worktree is created, all subsequent work happens inside `.worktrees/<branch-name>` **at the repository root** (use `git rev-parse --show-toplevel` to find the root). Never create worktrees nested inside other worktrees.
37
+ After the worktree is created, all subsequent work happens inside `worktrees/<branch-name>` **at the repository root** (use `git rev-parse --show-toplevel` to find the root). Never create worktrees nested inside other worktrees.
38
38
 
39
39
  ### Step 2: Plan the feature
40
40
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## What This Does
6
6
 
7
- Creates a git worktree for a given branch name inside `.worktrees/` and switches into it.
7
+ Creates a git worktree for a given branch name inside `worktrees/` and switches into it.
8
8
 
9
9
  ---
10
10
 
@@ -12,35 +12,18 @@ Creates a git worktree for a given branch name inside `.worktrees/` and switches
12
12
 
13
13
  When the user asks for a worktree named `<branch-name>`:
14
14
 
15
- ### Step 0: Slugify the branch name
16
-
17
- Sanitize the branch name to avoid filesystem and subdomain issues:
18
-
19
- 1. Convert to lowercase.
20
- 2. Replace dots (`.`), spaces, underscores, and other non-alphanumeric characters (except `-` and `/`) with hyphens.
21
- 3. Collapse consecutive hyphens into one.
22
- 4. Trim leading/trailing hyphens from each segment.
23
-
24
- **Examples:**
25
- - `feature.v2` → `feature-v2`
26
- - `v3.11.0` → `v3-11-0`
27
- - `my_cool.feature` → `my-cool-feature`
28
- - `UPPER.Case` → `upper-case`
29
-
30
- Use the slugified name for both the **branch name** and **worktree directory** throughout the rest of the workflow.
31
-
32
15
  ### Step 1: Create the worktree
33
16
 
34
17
  If the branch already exists locally or on the remote:
35
18
 
36
19
  ```bash
37
- git worktree add .worktrees/<branch-name> <branch-name>
20
+ git worktree add worktrees/<branch-name> <branch-name>
38
21
  ```
39
22
 
40
23
  If the branch does NOT exist yet, create it from the current HEAD:
41
24
 
42
25
  ```bash
43
- git worktree add .worktrees/<branch-name> -b <branch-name>
26
+ git worktree add worktrees/<branch-name> -b <branch-name>
44
27
  ```
45
28
 
46
29
  ### Step 2: Register a dev-server port
@@ -59,12 +42,12 @@ Or if the project has `scripts/worktree-port.js`:
59
42
  node scripts/worktree-port.js <branch-name>
60
43
  ```
61
44
 
62
- This writes to `.worktrees/ports.json` (gitignored). The dev server (`npx storyboard-dev`) reads from this file automatically.
45
+ This writes to `worktrees/ports.json` (gitignored). The dev server (`npx storyboard-dev`) reads from this file automatically.
63
46
 
64
47
  ### Step 3: Change into the worktree directory
65
48
 
66
49
  ```bash
67
- cd .worktrees/<branch-name>
50
+ cd worktrees/<branch-name>
68
51
  ```
69
52
 
70
53
  All subsequent commands in the session should run from this directory.
@@ -99,9 +82,9 @@ The dev server automatically uses the port assigned in Step 2.
99
82
 
100
83
  ## Notes
101
84
 
102
- - Worktrees live in `.worktrees/` at the repo root — this directory is already gitignored.
85
+ - Worktrees live in `worktrees/` at the repo root — this directory is already gitignored.
103
86
  - The branch name comes from the user's request (e.g., "create worktree comments-redo" → branch is `comments-redo`).
104
87
  - **Always slugify** the branch name (Step 0) before creating the worktree. Dots cause issues with subdomain routing and are replaced with hyphens.
105
88
  - If the worktree already exists, inform the user and `cd` into it instead of recreating it.
106
89
  - Port assignments are stable — once a worktree gets a port, it keeps it across restarts.
107
- - To see all assigned ports, check `.worktrees/ports.json`.
90
+ - To see all assigned ports, check `worktrees/ports.json`.
package/src/CoreUIBar.jsx CHANGED
@@ -147,6 +147,9 @@ export default function CoreUIBar({ basePath = '/', toolbarConfig, customHandler
147
147
  const [visible, setVisible] = useState(
148
148
  () => !document.documentElement.classList.contains('storyboard-chrome-hidden')
149
149
  )
150
+ const [completelyHidden, setCompletelyHidden] = useState(
151
+ () => document.documentElement.classList.contains('storyboard-chrome-completely-hidden')
152
+ )
150
153
  const [toolComponents, setToolComponents] = useState({})
151
154
  const [toolData, setToolData] = useState({})
152
155
  const [navVersion, setNavVersion] = useState(0)
@@ -338,10 +341,25 @@ export default function CoreUIBar({ basePath = '/', toolbarConfig, customHandler
338
341
  setVisible((v) => {
339
342
  const next = !v
340
343
  document.documentElement.classList.toggle('storyboard-chrome-hidden', !next)
344
+ // Always clear completely-hidden when toggling via cmd+.
345
+ document.documentElement.classList.remove('storyboard-chrome-completely-hidden')
341
346
  return next
342
347
  })
343
348
  }, [])
344
349
 
350
+ const toggleCompletelyHidden = useCallback(() => {
351
+ const isAnyHidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
352
+ if (isAnyHidden) {
353
+ document.documentElement.classList.remove('storyboard-chrome-hidden')
354
+ document.documentElement.classList.remove('storyboard-chrome-completely-hidden')
355
+ setVisible(true)
356
+ } else {
357
+ document.documentElement.classList.add('storyboard-chrome-hidden')
358
+ document.documentElement.classList.add('storyboard-chrome-completely-hidden')
359
+ setVisible(false)
360
+ }
361
+ }, [])
362
+
345
363
  function showFlowInfoDialog(name, json, error) {
346
364
  setFlowName(name)
347
365
  setFlowJson(json)
@@ -497,13 +515,15 @@ export default function CoreUIBar({ basePath = '/', toolbarConfig, customHandler
497
515
 
498
516
  setRoutingBasePath(basePath)
499
517
 
500
- // Sync visible state when storyboard-chrome-hidden is toggled externally
518
+ // Sync visible + completelyHidden state when classes are toggled externally
501
519
  const chromeObserver = new MutationObserver(() => {
502
520
  const hidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
521
+ const fully = document.documentElement.classList.contains('storyboard-chrome-completely-hidden')
503
522
  setVisible((v) => {
504
523
  if (v === !hidden) return v
505
524
  return !hidden
506
525
  })
526
+ setCompletelyHidden(fully)
507
527
  })
508
528
  chromeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
509
529
 
@@ -690,10 +710,21 @@ export default function CoreUIBar({ basePath = '/', toolbarConfig, customHandler
690
710
  function handleKeydown(e) {
691
711
  const hideKey = shortcutsConfig.hideChrome?.key || '.'
692
712
 
693
- if (e.key === hideKey && (e.metaKey || e.ctrlKey)) {
694
- e.preventDefault()
695
- toggleToolsVisibility()
713
+ if ((e.metaKey || e.ctrlKey) && !e.shiftKey) {
714
+ // Alt+Cmd+. — completely hide (use e.code since alt changes e.key on macOS)
715
+ if (e.altKey && e.code === 'Period') {
716
+ e.preventDefault()
717
+ toggleCompletelyHidden()
718
+ return
719
+ }
720
+ // Cmd+. — regular hide/show
721
+ if (!e.altKey && e.key === hideKey) {
722
+ e.preventDefault()
723
+ toggleToolsVisibility()
724
+ return
725
+ }
696
726
  }
727
+
697
728
  for (const menu of cleanedMenus) {
698
729
  const shortcut = menu.shortcut
699
730
  if (!shortcut?.key) continue
@@ -711,14 +742,14 @@ export default function CoreUIBar({ basePath = '/', toolbarConfig, customHandler
711
742
 
712
743
  window.addEventListener('keydown', handleKeydown)
713
744
  return () => window.removeEventListener('keydown', handleKeydown)
714
- }, [shortcutsConfig, cleanedMenus, toggleToolsVisibility])
745
+ }, [shortcutsConfig, cleanedMenus, toggleToolsVisibility, toggleCompletelyHidden])
715
746
 
716
747
  if (isEmbed) return null
717
748
 
718
749
  return (
719
750
  <>
720
751
  {/* Canvas toolbar */}
721
- {canvasActive && canvasMenus.length > 0 && (
752
+ {canvasActive && !completelyHidden && canvasMenus.length > 0 && (
722
753
  <div
723
754
  className="fixed bottom-6 left-6 z-[9999] font-sans flex items-center gap-3"
724
755
  role="toolbar"
@@ -2,6 +2,7 @@
2
2
  * HideChromeTrigger — toolbar button that toggles toolbar/branch bar visibility.
3
3
  * Always visible (even in hide mode). Uses the lightbulb icon.
4
4
  * In hide mode: goes 50% opacity.
5
+ * In completely-hidden mode: not rendered at all.
5
6
  */
6
7
 
7
8
  import { useState, useEffect, useCallback } from 'react'
@@ -12,10 +13,14 @@ export default function HideChromeTrigger({ config = {}, tabindex }) {
12
13
  const [hidden, setHidden] = useState(
13
14
  () => document.documentElement.classList.contains('storyboard-chrome-hidden')
14
15
  )
16
+ const [completelyHidden, setCompletelyHidden] = useState(
17
+ () => document.documentElement.classList.contains('storyboard-chrome-completely-hidden')
18
+ )
15
19
 
16
20
  useEffect(() => {
17
21
  const observer = new MutationObserver(() => {
18
22
  setHidden(document.documentElement.classList.contains('storyboard-chrome-hidden'))
23
+ setCompletelyHidden(document.documentElement.classList.contains('storyboard-chrome-completely-hidden'))
19
24
  })
20
25
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
21
26
  return () => observer.disconnect()
@@ -23,8 +28,11 @@ export default function HideChromeTrigger({ config = {}, tabindex }) {
23
28
 
24
29
  const toggle = useCallback(() => {
25
30
  document.documentElement.classList.toggle('storyboard-chrome-hidden')
31
+ document.documentElement.classList.remove('storyboard-chrome-completely-hidden')
26
32
  }, [])
27
33
 
34
+ if (completelyHidden) return null
35
+
28
36
  return (
29
37
  <span style={{ opacity: hidden ? 0.5 : 1, transition: 'opacity 0.15s' }}>
30
38
  <TriggerButton
@@ -42,9 +42,9 @@ function readDevDomain() {
42
42
  /** Detect worktree name */
43
43
  function getWorktreeName() {
44
44
  try {
45
- // Check if we're in a .worktrees/ directory
45
+ // Check if we're in a worktrees/ directory
46
46
  const cwd = rootDir
47
- const match = cwd.match(/\.worktrees\/([^/]+)/)
47
+ const match = cwd.match(/worktrees\/([^/]+)/)
48
48
  return match ? match[1] : 'main'
49
49
  } catch { return 'main' }
50
50
  }
package/src/cli/branch.js CHANGED
@@ -105,7 +105,7 @@ export async function runBranchGuide(branchArg) {
105
105
  p.log.success(`Worktree ${bold(targetBranch)} already exists`)
106
106
  p.note(
107
107
  [
108
- ` ${green('cd')} ${dim(`.worktrees/${targetBranch}`)}`,
108
+ ` ${green('cd')} ${dim(`worktrees/${targetBranch}`)}`,
109
109
  ` ${green('npx storyboard dev')} ${dim('to start developing')}`,
110
110
  ].join('\n'),
111
111
  'Ready to go'
@@ -156,7 +156,7 @@ export async function runBranchGuide(branchArg) {
156
156
  }
157
157
 
158
158
  // Create the worktree
159
- const targetDir = resolve(root, '.worktrees', targetBranch)
159
+ const targetDir = resolve(root, 'worktrees', targetBranch)
160
160
  const spin = p.spinner()
161
161
 
162
162
  try {
@@ -164,9 +164,9 @@ export async function runBranchGuide(branchArg) {
164
164
  ? ['worktree', 'add', targetDir, '-b', targetBranch]
165
165
  : ['worktree', 'add', targetDir, targetBranch]
166
166
 
167
- spin.start(`Creating worktree .worktrees/${targetBranch}`)
167
+ spin.start(`Creating worktree worktrees/${targetBranch}`)
168
168
  execFileSync('git', gitArgs, { cwd: root, stdio: 'pipe' })
169
- spin.stop(`Worktree created: .worktrees/${targetBranch}`)
169
+ spin.stop(`Worktree created: worktrees/${targetBranch}`)
170
170
  } catch (err) {
171
171
  spin.stop('Failed to create worktree')
172
172
  p.log.error(err.message || 'git worktree add failed')
@@ -211,13 +211,13 @@ export async function runBranchGuide(branchArg) {
211
211
 
212
212
  // 7. Summary
213
213
  const lines = [
214
- ` Your branch is set up as a worktree in ${green(`.worktrees/${targetBranch}`)}`,
214
+ ` Your branch is set up as a worktree in ${green(`worktrees/${targetBranch}`)}`,
215
215
  ]
216
216
  if (didStash) {
217
217
  lines.push(` Your uncommitted work has been safely moved`)
218
218
  }
219
219
  lines.push('')
220
- lines.push(` ${green('cd')} ${dim(`.worktrees/${targetBranch}`)}`)
220
+ lines.push(` ${green('cd')} ${dim(`worktrees/${targetBranch}`)}`)
221
221
  lines.push(` ${green('npx storyboard dev')} ${dim('to start developing')}`)
222
222
  lines.push('')
223
223
  lines.push(` ${dim('Tip: ask your AI agent about worktrees — they\'re great!')}`)
package/src/cli/code.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Usage:
5
5
  * storyboard code # open current worktree or repo root
6
6
  * storyboard code main # open repo root
7
- * storyboard code <branch> # open .worktrees/<branch>/
7
+ * storyboard code <branch> # open worktrees/<branch>/
8
8
  */
9
9
 
10
10
  import * as p from '@clack/prompts'
@@ -33,7 +33,7 @@ if (!branch) {
33
33
  const name = detectWorktreeName()
34
34
  const dir = name === 'main' ? root : worktreeDir(name)
35
35
  if (openInCode(dir)) {
36
- p.outro(`Opened ${name === 'main' ? 'repo root' : `.worktrees/${name}/`}`)
36
+ p.outro(`Opened ${name === 'main' ? 'repo root' : `worktrees/${name}/`}`)
37
37
  } else {
38
38
  p.log.error('Could not open VS Code. Is the `code` CLI installed?')
39
39
  p.log.info('Run `npx storyboard setup` to install it, or open VS Code and run:')
@@ -59,7 +59,7 @@ if (!branch) {
59
59
  process.exit(1)
60
60
  }
61
61
  if (openInCode(dir)) {
62
- p.outro(`Opened .worktrees/${branch}/`)
62
+ p.outro(`Opened worktrees/${branch}/`)
63
63
  } else {
64
64
  p.log.error('Could not open VS Code.')
65
65
  process.exit(1)
package/src/cli/dev.js CHANGED
@@ -60,13 +60,13 @@ function remoteBranchExists(name, cwd) {
60
60
  * @returns {string} path to the new worktree directory
61
61
  */
62
62
  function createWorktree(name, root, { newBranch = false } = {}) {
63
- const targetDir = resolve(root, '.worktrees', name)
63
+ const targetDir = resolve(root, 'worktrees', name)
64
64
 
65
65
  const gitArgs = newBranch
66
66
  ? ['worktree', 'add', targetDir, '-b', name]
67
67
  : ['worktree', 'add', targetDir, name]
68
68
 
69
- p.log.step(`Creating worktree: .worktrees/${name}`)
69
+ p.log.step(`Creating worktree: worktrees/${name}`)
70
70
  execFileSync('git', gitArgs, { cwd: root, stdio: 'inherit' })
71
71
 
72
72
  p.log.step('Installing dependencies…')
@@ -112,7 +112,7 @@ async function resolveDevTarget(branchArg, { allowCreate = true } = {}) {
112
112
  // No worktree exists — prompt the user to convert
113
113
  p.log.warning(`Root is on branch "${branch}" instead of main.`)
114
114
  const shouldConvert = await p.confirm({
115
- message: `Convert "${branch}" to a worktree? (moves branch to .worktrees/${branch}/)`,
115
+ message: `Convert "${branch}" to a worktree? (moves branch to worktrees/${branch}/)`,
116
116
  initialValue: true,
117
117
  })
118
118
 
@@ -403,9 +403,9 @@ async function main() {
403
403
  const { worktreeName, targetCwd, created } = await resolveDevTarget(branchArg, { allowCreate })
404
404
 
405
405
  if (created) {
406
- p.log.success(`Worktree ready: .worktrees/${worktreeName}`)
406
+ p.log.success(`Worktree ready: worktrees/${worktreeName}`)
407
407
  } else if (branchArg) {
408
- p.log.info(`Using ${worktreeName === 'main' ? 'main repo' : `.worktrees/${worktreeName}`}`)
408
+ p.log.info(`Using ${worktreeName === 'main' ? 'main repo' : `worktrees/${worktreeName}`}`)
409
409
  }
410
410
 
411
411
  const domain = readDevDomain(targetCwd)
@@ -28,6 +28,7 @@ import {
28
28
  let _mounted = false
29
29
 
30
30
  const CHROME_HIDDEN_KEY = 'sb-chrome-hidden'
31
+ const CHROME_COMPLETELY_HIDDEN_KEY = 'sb-chrome-completely-hidden'
31
32
 
32
33
  /**
33
34
  * Migrate localStorage keys renamed in 4.3.0.
@@ -54,20 +55,26 @@ function migrateLocalStorageKeys() {
54
55
  function applyEarlyChromeState() {
55
56
  if (typeof document === 'undefined' || typeof localStorage === 'undefined') return
56
57
  const hidden = localStorage.getItem(CHROME_HIDDEN_KEY) === '1'
58
+ const completelyHidden = localStorage.getItem(CHROME_COMPLETELY_HIDDEN_KEY) === '1'
57
59
  if (hidden) {
58
60
  document.documentElement.classList.add('storyboard-chrome-hidden')
59
61
  }
62
+ if (completelyHidden) {
63
+ document.documentElement.classList.add('storyboard-chrome-completely-hidden')
64
+ }
60
65
  }
61
66
 
62
67
  /**
63
- * Watch for changes to the storyboard-chrome-hidden class and persist to
64
- * localStorage. Works regardless of which code path toggles the class.
68
+ * Watch for changes to chrome-hidden / chrome-completely-hidden classes
69
+ * and persist to localStorage. Works regardless of which code path toggles them.
65
70
  */
66
71
  function installChromeStatePersistence() {
67
72
  if (typeof document === 'undefined' || typeof localStorage === 'undefined') return
68
73
  const observer = new MutationObserver(() => {
69
74
  const hidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
75
+ const completelyHidden = document.documentElement.classList.contains('storyboard-chrome-completely-hidden')
70
76
  localStorage.setItem(CHROME_HIDDEN_KEY, hidden ? '1' : '0')
77
+ localStorage.setItem(CHROME_COMPLETELY_HIDDEN_KEY, completelyHidden ? '1' : '0')
71
78
  })
72
79
  observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] })
73
80
  }
@@ -89,7 +89,7 @@ async function parseBody(req) {
89
89
  function resolveWorktreeCwd(branch) {
90
90
  const root = repoRoot()
91
91
  if (branch === 'main') return root
92
- const wtDir = join(root, '.worktrees', branch)
92
+ const wtDir = join(root, 'worktrees', branch)
93
93
  if (existsSync(wtDir)) return wtDir
94
94
  return null
95
95
  }
@@ -17,6 +17,7 @@ export async function handler() {
17
17
  execute: () => {
18
18
  const isHidden = document.documentElement.classList.contains('storyboard-chrome-hidden')
19
19
  document.documentElement.classList.toggle('storyboard-chrome-hidden', !isHidden)
20
+ document.documentElement.classList.remove('storyboard-chrome-completely-hidden')
20
21
  },
21
22
  }]
22
23
  },
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Worktree Port Registry
3
3
  *
4
- * Manages a JSON registry (.worktrees/ports.json) that maps worktree names
4
+ * Manages a JSON registry (worktrees/ports.json) that maps worktree names
5
5
  * to unique dev-server ports. Main always gets 1234; worktrees get 1235+.
6
6
  *
7
7
  * This module is published as part of @dfosco/storyboard-core so client
@@ -19,10 +19,10 @@ import { findByWorktree } from './serverRegistry.js'
19
19
  const BASE_PORT = 1234
20
20
 
21
21
  /**
22
- * Resolve the path to .worktrees/ports.json.
22
+ * Resolve the path to worktrees/ports.json.
23
23
  *
24
24
  * Derives the repo root from directory structure so it works whether
25
- * running from the repo root or from inside .worktrees/<name>/ — even
25
+ * running from the repo root or from inside worktrees/<name>/ — even
26
26
  * when ports.json does not exist yet.
27
27
  *
28
28
  * @param {string} [cwd] — override working directory
@@ -31,34 +31,34 @@ const BASE_PORT = 1234
31
31
  export function portsFilePath(cwd = process.cwd()) {
32
32
  const realCwd = realpathSync(cwd)
33
33
 
34
- // Check if we're inside .worktrees/<name>/
35
- const worktreeMatch = realCwd.match(/^(.+)[/\\]\.worktrees[/\\][^/\\]+/)
34
+ // Check if we're inside worktrees/<name>/
35
+ const worktreeMatch = realCwd.match(/^(.+)[/\\]worktrees[/\\][^/\\]+/)
36
36
  if (worktreeMatch) {
37
- return join(worktreeMatch[1], '.worktrees', 'ports.json')
37
+ return join(worktreeMatch[1], 'worktrees', 'ports.json')
38
38
  }
39
39
 
40
40
  // We're at the repo root (or somewhere else) — default location
41
- return join(realCwd, '.worktrees', 'ports.json')
41
+ return join(realCwd, 'worktrees', 'ports.json')
42
42
  }
43
43
 
44
44
  /**
45
45
  * Detect the worktree name from the current git context.
46
46
  *
47
- * Returns 'main' when not inside a .worktrees/<name>/ directory.
47
+ * Returns 'main' when not inside a worktrees/<name>/ directory.
48
48
  */
49
49
  export function detectWorktreeName() {
50
50
  try {
51
51
  const topLevel = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim()
52
52
  const realTop = realpathSync(topLevel)
53
53
 
54
- // Check if we're inside a .worktrees/<name> directory
55
- if (realTop.includes('.worktrees/') || realTop.includes('.worktrees\\')) {
54
+ // Check if we're inside a worktrees/<name> directory
55
+ if (realTop.includes('worktrees/') || realTop.includes('worktrees\\')) {
56
56
  return basename(realTop)
57
57
  }
58
58
 
59
59
  // Also check the cwd pattern
60
60
  const realCwd = realpathSync(process.cwd())
61
- const worktreeMatch = realCwd.match(/\.worktrees[/\\]([^/\\]+)/)
61
+ const worktreeMatch = realCwd.match(/worktrees[/\\]([^/\\]+)/)
62
62
  if (worktreeMatch) return worktreeMatch[1]
63
63
 
64
64
  // Not a worktree — check the current branch name
@@ -90,7 +90,7 @@ function isPortInUse(port) {
90
90
  /**
91
91
  * Get or assign a port for the given worktree name.
92
92
  *
93
- * Creates .worktrees/ports.json if it doesn't exist. Assigns ports
93
+ * Creates worktrees/ports.json if it doesn't exist. Assigns ports
94
94
  * starting at BASE_PORT+1 (1235) for non-main worktrees.
95
95
  * If the previously assigned port was stolen by another process,
96
96
  * reassigns to the next available port.
@@ -179,7 +179,7 @@ export function resolveRunningPort(worktreeName) {
179
179
  }
180
180
 
181
181
  /**
182
- * Resolve the port for a worktree from .worktrees/ports.json
182
+ * Resolve the port for a worktree from worktrees/ports.json
183
183
  * without assigning a new one if missing.
184
184
  *
185
185
  * @param {string} worktreeName
@@ -216,9 +216,9 @@ export function slugify(name) {
216
216
  }
217
217
 
218
218
  /**
219
- * Resolve the repo root — the directory that contains `.worktrees/`.
219
+ * Resolve the repo root — the directory that contains `worktrees/`.
220
220
  *
221
- * Works whether cwd is the repo root itself or inside `.worktrees/<name>/`.
221
+ * Works whether cwd is the repo root itself or inside `worktrees/<name>/`.
222
222
  *
223
223
  * @param {string} [cwd]
224
224
  * @returns {string} absolute path to repo root
@@ -226,7 +226,7 @@ export function slugify(name) {
226
226
  export function repoRoot(cwd = process.cwd()) {
227
227
  const realCwd = realpathSync(cwd)
228
228
 
229
- const worktreeMatch = realCwd.match(/^(.+)[/\\]\.worktrees[/\\][^/\\]+/)
229
+ const worktreeMatch = realCwd.match(/^(.+)[/\\]worktrees[/\\][^/\\]+/)
230
230
  if (worktreeMatch) return worktreeMatch[1]
231
231
 
232
232
  return realCwd
@@ -235,7 +235,7 @@ export function repoRoot(cwd = process.cwd()) {
235
235
  /**
236
236
  * Resolve the full path to a worktree directory.
237
237
  *
238
- * Returns repo root for 'main', `.worktrees/<name>` otherwise.
238
+ * Returns repo root for 'main', `worktrees/<name>` otherwise.
239
239
  *
240
240
  * @param {string} name — worktree name
241
241
  * @param {string} [cwd]
@@ -244,11 +244,11 @@ export function repoRoot(cwd = process.cwd()) {
244
244
  export function worktreeDir(name, cwd) {
245
245
  const root = repoRoot(cwd)
246
246
  if (name === 'main') return root
247
- return join(root, '.worktrees', name)
247
+ return join(root, 'worktrees', name)
248
248
  }
249
249
 
250
250
  /**
251
- * List existing worktree directory names from `.worktrees/`.
251
+ * List existing worktree directory names from `worktrees/`.
252
252
  *
253
253
  * Only returns directories that look like real worktrees (contain a `.git` file).
254
254
  * Does not include 'main'.
@@ -258,7 +258,7 @@ export function worktreeDir(name, cwd) {
258
258
  */
259
259
  export function listWorktrees(cwd) {
260
260
  const root = repoRoot(cwd)
261
- const worktreesDir = join(root, '.worktrees')
261
+ const worktreesDir = join(root, 'worktrees')
262
262
 
263
263
  if (!existsSync(worktreesDir)) return []
264
264