@dfosco/storyboard-core 4.0.0-beta.3 → 4.0.0-beta.5
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 +1 -1
- package/src/autosync/server.js +1 -1
- package/src/cli/dev.js +156 -23
- package/src/cli/index.js +1 -0
- package/src/cli/proxy.js +2 -2
- package/src/cli/updateVersion.js +13 -3
- package/src/worktree/port.js +57 -1
- package/src/worktree/port.test.js +91 -1
- package/widgets.config.json +5 -6
package/package.json
CHANGED
package/src/autosync/server.js
CHANGED
|
@@ -81,7 +81,7 @@ function hasScopedStagedChanges(root, files) {
|
|
|
81
81
|
function listChangedFiles(root) {
|
|
82
82
|
const tracked = git(['diff', '--name-only'], root)
|
|
83
83
|
const untracked = git(['ls-files', '--others', '--exclude-standard'], root)
|
|
84
|
-
return [
|
|
84
|
+
return [tracked, untracked]
|
|
85
85
|
.flatMap((raw) => raw.split('\n'))
|
|
86
86
|
.map((file) => file.trim())
|
|
87
87
|
.filter(Boolean)
|
package/src/cli/dev.js
CHANGED
|
@@ -1,20 +1,164 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* storyboard dev — Start Vite with correct base path
|
|
2
|
+
* storyboard dev [branch] — Start Vite with correct base path.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* storyboard dev # detect worktree from cwd
|
|
6
|
+
* storyboard dev main # start dev for repo root
|
|
7
|
+
* storyboard dev <worktree> # start dev for existing worktree
|
|
8
|
+
* storyboard dev <branch> # auto-create worktree + start dev
|
|
3
9
|
*
|
|
4
10
|
* Main: http://<devDomain>.localhost/
|
|
5
11
|
* Branch: http://<devDomain>.localhost/branch--<name>/
|
|
6
12
|
*/
|
|
7
13
|
|
|
8
14
|
import * as p from '@clack/prompts'
|
|
9
|
-
import { spawn } from 'child_process'
|
|
15
|
+
import { spawn, execFileSync } from 'child_process'
|
|
10
16
|
import { existsSync } from 'fs'
|
|
11
17
|
import { resolve } from 'path'
|
|
12
|
-
import { detectWorktreeName, getPort } from '../worktree/port.js'
|
|
18
|
+
import { detectWorktreeName, getPort, repoRoot, worktreeDir, listWorktrees } from '../worktree/port.js'
|
|
13
19
|
import { generateCaddyfile, generateRouteConfig, upsertCaddyRoute, isCaddyRunning, reloadCaddy, readDevDomain } from './proxy.js'
|
|
14
20
|
import { startRenameWatcher } from '../rename-watcher/watcher.js'
|
|
21
|
+
import { parseFlags } from './flags.js'
|
|
22
|
+
|
|
23
|
+
const flagSchema = {
|
|
24
|
+
port: { type: 'number', description: 'Override dev server port' },
|
|
25
|
+
create: { type: 'boolean', default: true, description: 'Allow creating worktrees/branches (disable with --no-create)' },
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a local branch exists.
|
|
30
|
+
* @param {string} name
|
|
31
|
+
* @param {string} cwd
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function localBranchExists(name, cwd) {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync('git', ['show-ref', '--verify', `refs/heads/${name}`], { cwd, stdio: 'ignore' })
|
|
37
|
+
return true
|
|
38
|
+
} catch {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a remote branch exists on origin.
|
|
45
|
+
* @param {string} name
|
|
46
|
+
* @param {string} cwd
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
function remoteBranchExists(name, cwd) {
|
|
50
|
+
try {
|
|
51
|
+
const result = execFileSync('git', ['ls-remote', '--exit-code', '--heads', 'origin', name], { cwd, encoding: 'utf8' })
|
|
52
|
+
return result.trim().length > 0
|
|
53
|
+
} catch {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a git worktree and install dependencies.
|
|
60
|
+
* @param {string} name — worktree/branch name
|
|
61
|
+
* @param {string} root — repo root path
|
|
62
|
+
* @param {object} opts
|
|
63
|
+
* @param {boolean} opts.newBranch — create a new branch from HEAD
|
|
64
|
+
* @returns {string} path to the new worktree directory
|
|
65
|
+
*/
|
|
66
|
+
function createWorktree(name, root, { newBranch = false } = {}) {
|
|
67
|
+
const targetDir = resolve(root, '.worktrees', name)
|
|
68
|
+
|
|
69
|
+
const gitArgs = newBranch
|
|
70
|
+
? ['worktree', 'add', targetDir, '-b', name]
|
|
71
|
+
: ['worktree', 'add', targetDir, name]
|
|
72
|
+
|
|
73
|
+
p.log.step(`Creating worktree: .worktrees/${name}`)
|
|
74
|
+
execFileSync('git', gitArgs, { cwd: root, stdio: 'inherit' })
|
|
75
|
+
|
|
76
|
+
p.log.step('Installing dependencies…')
|
|
77
|
+
const npmBin = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
78
|
+
execFileSync(npmBin, ['install'], { cwd: targetDir, stdio: 'inherit' })
|
|
79
|
+
|
|
80
|
+
getPort(name)
|
|
81
|
+
return targetDir
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve the target worktree for `storyboard dev [branch]`.
|
|
86
|
+
*
|
|
87
|
+
* @param {string|undefined} branchArg — positional branch argument
|
|
88
|
+
* @param {object} opts
|
|
89
|
+
* @param {boolean} opts.allowCreate — whether creation is allowed
|
|
90
|
+
* @returns {Promise<{ worktreeName: string, targetCwd: string, created: boolean }>}
|
|
91
|
+
*/
|
|
92
|
+
async function resolveDevTarget(branchArg, { allowCreate = true } = {}) {
|
|
93
|
+
// No argument — detect from cwd (current behavior)
|
|
94
|
+
if (!branchArg) {
|
|
95
|
+
return { worktreeName: detectWorktreeName(), targetCwd: process.cwd(), created: false }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const root = repoRoot()
|
|
99
|
+
|
|
100
|
+
// "main" → repo root
|
|
101
|
+
if (branchArg === 'main') {
|
|
102
|
+
return { worktreeName: 'main', targetCwd: root, created: false }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Existing worktree directory
|
|
106
|
+
const existingDir = worktreeDir(branchArg)
|
|
107
|
+
if (existsSync(resolve(existingDir, '.git'))) {
|
|
108
|
+
return { worktreeName: branchArg, targetCwd: existingDir, created: false }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// From here on we need to create — check if allowed
|
|
112
|
+
if (!allowCreate) {
|
|
113
|
+
p.log.error(`Worktree "${branchArg}" does not exist. Use without --no-create to auto-create.`)
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Branch exists (local or remote) — create worktree from it
|
|
118
|
+
const hasLocal = localBranchExists(branchArg, root)
|
|
119
|
+
const hasRemote = !hasLocal && remoteBranchExists(branchArg, root)
|
|
120
|
+
|
|
121
|
+
if (hasLocal || hasRemote) {
|
|
122
|
+
const targetDir = createWorktree(branchArg, root, { newBranch: false })
|
|
123
|
+
return { worktreeName: branchArg, targetCwd: targetDir, created: true }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Branch doesn't exist — interactive TTY gets a prompt, non-interactive auto-creates
|
|
127
|
+
const isTTY = process.stdin.isTTY
|
|
128
|
+
|
|
129
|
+
if (isTTY) {
|
|
130
|
+
const confirmed = await p.confirm({
|
|
131
|
+
message: `Branch "${branchArg}" doesn't exist. Create it from HEAD?`,
|
|
132
|
+
})
|
|
133
|
+
if (p.isCancel(confirmed) || !confirmed) {
|
|
134
|
+
p.cancel('Cancelled.')
|
|
135
|
+
process.exit(0)
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
p.log.step(`Branch "${branchArg}" not found — creating from HEAD`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const targetDir = createWorktree(branchArg, root, { newBranch: true })
|
|
142
|
+
return { worktreeName: branchArg, targetCwd: targetDir, created: true }
|
|
143
|
+
}
|
|
15
144
|
|
|
16
145
|
async function main() {
|
|
17
|
-
const
|
|
146
|
+
const { flags, positional } = parseFlags(process.argv.slice(3), flagSchema)
|
|
147
|
+
|
|
148
|
+
const branchArg = positional[0] || undefined
|
|
149
|
+
const overridePort = flags.port || null
|
|
150
|
+
const allowCreate = flags.create
|
|
151
|
+
|
|
152
|
+
p.intro('storyboard dev')
|
|
153
|
+
|
|
154
|
+
const { worktreeName, targetCwd, created } = await resolveDevTarget(branchArg, { allowCreate })
|
|
155
|
+
|
|
156
|
+
if (created) {
|
|
157
|
+
p.log.success(`Worktree ready: .worktrees/${worktreeName}`)
|
|
158
|
+
} else if (branchArg) {
|
|
159
|
+
p.log.info(`Using ${worktreeName === 'main' ? 'main repo' : `.worktrees/${worktreeName}`}`)
|
|
160
|
+
}
|
|
161
|
+
|
|
18
162
|
const port = getPort(worktreeName)
|
|
19
163
|
const isMain = worktreeName === 'main'
|
|
20
164
|
|
|
@@ -22,42 +166,31 @@ async function main() {
|
|
|
22
166
|
? '/'
|
|
23
167
|
: `/branch--${worktreeName}/`
|
|
24
168
|
|
|
25
|
-
const domain = readDevDomain()
|
|
169
|
+
const domain = readDevDomain(targetCwd)
|
|
26
170
|
const proxyUrl = `http://${domain}${basePath}`
|
|
27
171
|
const directUrl = `http://localhost:${port}${basePath}`
|
|
28
172
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Parse --port override from argv (skip 'dev' subcommand)
|
|
32
|
-
const args = process.argv.slice(3)
|
|
33
|
-
const portFlagIdx = args.indexOf('--port')
|
|
34
|
-
const overridePort = portFlagIdx >= 0 ? Number(args[portFlagIdx + 1]) : null
|
|
35
|
-
|
|
36
|
-
const extraArgs = args.filter((arg, i, arr) => {
|
|
37
|
-
if (arg === '--port') return false
|
|
38
|
-
if (i > 0 && arr[i - 1] === '--port') return false
|
|
39
|
-
return true
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// Resolve Vite binary directly to skip npx overhead (~5s)
|
|
43
|
-
const localVite = resolve(process.cwd(), 'node_modules', '.bin', 'vite')
|
|
173
|
+
// Resolve Vite binary relative to target worktree
|
|
174
|
+
const localVite = resolve(targetCwd, 'node_modules', '.bin', 'vite')
|
|
44
175
|
const useLocalVite = existsSync(localVite)
|
|
45
176
|
|
|
46
177
|
// Start Vite — let it find a free port if assigned one is busy.
|
|
47
178
|
// Capture stdout to detect actual port and update Caddy.
|
|
48
|
-
const viteArgs = ['--port', String(overridePort || port)
|
|
179
|
+
const viteArgs = ['--port', String(overridePort || port)]
|
|
49
180
|
const child = useLocalVite
|
|
50
181
|
? spawn(localVite, viteArgs, {
|
|
182
|
+
cwd: targetCwd,
|
|
51
183
|
env: { ...process.env, VITE_BASE_PATH: basePath },
|
|
52
184
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
53
185
|
})
|
|
54
186
|
: spawn('npx', ['vite', ...viteArgs], {
|
|
187
|
+
cwd: targetCwd,
|
|
55
188
|
env: { ...process.env, VITE_BASE_PATH: basePath },
|
|
56
189
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
57
190
|
})
|
|
58
191
|
|
|
59
|
-
// Start rename watcher
|
|
60
|
-
const renameWatcher = startRenameWatcher(
|
|
192
|
+
// Start rename watcher in target directory
|
|
193
|
+
const renameWatcher = startRenameWatcher(targetCwd)
|
|
61
194
|
|
|
62
195
|
let caddyUpdated = false
|
|
63
196
|
let ready = false
|
package/src/cli/index.js
CHANGED
|
@@ -70,6 +70,7 @@ function helpScreen(version) {
|
|
|
70
70
|
'',
|
|
71
71
|
` ${bold(cyan('Development'))}`,
|
|
72
72
|
cmd('dev', 'Start Vite dev server + update proxy'),
|
|
73
|
+
cmd('dev [branch]', 'Start dev for a specific worktree/branch'),
|
|
73
74
|
cmd('exit', 'Stop all dev servers and proxy'),
|
|
74
75
|
'',
|
|
75
76
|
` ${bold(cyan('Create'))}`,
|
package/src/cli/proxy.js
CHANGED
|
@@ -14,9 +14,9 @@ import { execSync } from 'child_process'
|
|
|
14
14
|
import { dirname, resolve } from 'path'
|
|
15
15
|
import { portsFilePath } from '../worktree/port.js'
|
|
16
16
|
|
|
17
|
-
export function readDevDomain() {
|
|
17
|
+
export function readDevDomain(cwd) {
|
|
18
18
|
try {
|
|
19
|
-
const configPath = resolve(process.cwd(), 'storyboard.config.json')
|
|
19
|
+
const configPath = resolve(cwd || process.cwd(), 'storyboard.config.json')
|
|
20
20
|
const config = JSON.parse(readFileSync(configPath, 'utf8'))
|
|
21
21
|
return `${config.devDomain || 'storyboard'}.localhost`
|
|
22
22
|
} catch {
|
package/src/cli/updateVersion.js
CHANGED
|
@@ -79,18 +79,28 @@ try {
|
|
|
79
79
|
p.log.warn('Scaffold sync failed — run `npx storyboard-scaffold` manually')
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// Auto-commit the version update
|
|
82
|
+
// Auto-commit the version update (only if package.json or lock file changed)
|
|
83
83
|
try {
|
|
84
84
|
// Read the installed version from the core package
|
|
85
85
|
const corePkg = JSON.parse(readFileSync(resolve(process.cwd(), 'node_modules', '@dfosco', 'storyboard-core', 'package.json'), 'utf8'))
|
|
86
86
|
const installedVersion = corePkg.version || suffix.slice(1)
|
|
87
87
|
const commitMsg = `[storyboard-update] Update storyboard to ${installedVersion}`
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// Only stage update-related files (package.json, lock files, scaffold outputs)
|
|
90
|
+
const filesToStage = [
|
|
91
|
+
'package.json',
|
|
92
|
+
'package-lock.json',
|
|
93
|
+
'yarn.lock',
|
|
94
|
+
'pnpm-lock.yaml',
|
|
95
|
+
'.github/skills',
|
|
96
|
+
'scripts',
|
|
97
|
+
]
|
|
98
|
+
execSync(`git add ${filesToStage.join(' ')} 2>/dev/null || true`, { cwd: process.cwd(), stdio: 'pipe' })
|
|
99
|
+
|
|
90
100
|
// Only commit if there are staged changes
|
|
91
101
|
try {
|
|
92
102
|
execSync('git diff --cached --quiet', { cwd: process.cwd(), stdio: 'pipe' })
|
|
93
|
-
p.log.message('No changes to commit')
|
|
103
|
+
p.log.message('No changes to commit — already up to date')
|
|
94
104
|
} catch {
|
|
95
105
|
execSync(`git commit -m "${commitMsg}"`, { cwd: process.cwd(), stdio: 'pipe' })
|
|
96
106
|
p.log.success(`Committed: ${commitMsg}`)
|
package/src/worktree/port.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* import { getPort, detectWorktreeName, resolvePort } from '@dfosco/storyboard-core/worktree/port'
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, realpathSync } from 'fs'
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, realpathSync } from 'fs'
|
|
15
15
|
import { join, dirname, basename } from 'path'
|
|
16
16
|
import { execSync } from 'child_process'
|
|
17
17
|
|
|
@@ -60,6 +60,10 @@ export function detectWorktreeName() {
|
|
|
60
60
|
const worktreeMatch = realCwd.match(/\.worktrees[/\\]([^/\\]+)/)
|
|
61
61
|
if (worktreeMatch) return worktreeMatch[1]
|
|
62
62
|
|
|
63
|
+
// Not a worktree — check the current branch name
|
|
64
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim()
|
|
65
|
+
if (branch && branch !== 'main' && branch !== 'master') return branch
|
|
66
|
+
|
|
63
67
|
return 'main'
|
|
64
68
|
} catch {
|
|
65
69
|
return 'main'
|
|
@@ -138,3 +142,55 @@ export function slugify(name) {
|
|
|
138
142
|
.map((s) => s.replace(/^-+|-+$/g, ''))
|
|
139
143
|
.join('/')
|
|
140
144
|
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the repo root — the directory that contains `.worktrees/`.
|
|
148
|
+
*
|
|
149
|
+
* Works whether cwd is the repo root itself or inside `.worktrees/<name>/`.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} [cwd]
|
|
152
|
+
* @returns {string} absolute path to repo root
|
|
153
|
+
*/
|
|
154
|
+
export function repoRoot(cwd = process.cwd()) {
|
|
155
|
+
const realCwd = realpathSync(cwd)
|
|
156
|
+
|
|
157
|
+
const worktreeMatch = realCwd.match(/^(.+)[/\\]\.worktrees[/\\][^/\\]+/)
|
|
158
|
+
if (worktreeMatch) return worktreeMatch[1]
|
|
159
|
+
|
|
160
|
+
return realCwd
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the full path to a worktree directory.
|
|
165
|
+
*
|
|
166
|
+
* Returns repo root for 'main', `.worktrees/<name>` otherwise.
|
|
167
|
+
*
|
|
168
|
+
* @param {string} name — worktree name
|
|
169
|
+
* @param {string} [cwd]
|
|
170
|
+
* @returns {string} absolute path
|
|
171
|
+
*/
|
|
172
|
+
export function worktreeDir(name, cwd) {
|
|
173
|
+
const root = repoRoot(cwd)
|
|
174
|
+
if (name === 'main') return root
|
|
175
|
+
return join(root, '.worktrees', name)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* List existing worktree directory names from `.worktrees/`.
|
|
180
|
+
*
|
|
181
|
+
* Only returns directories that look like real worktrees (contain a `.git` file).
|
|
182
|
+
* Does not include 'main'.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} [cwd]
|
|
185
|
+
* @returns {string[]}
|
|
186
|
+
*/
|
|
187
|
+
export function listWorktrees(cwd) {
|
|
188
|
+
const root = repoRoot(cwd)
|
|
189
|
+
const worktreesDir = join(root, '.worktrees')
|
|
190
|
+
|
|
191
|
+
if (!existsSync(worktreesDir)) return []
|
|
192
|
+
|
|
193
|
+
return readdirSync(worktreesDir, { withFileTypes: true })
|
|
194
|
+
.filter((d) => d.isDirectory() && existsSync(join(worktreesDir, d.name, '.git')))
|
|
195
|
+
.map((d) => d.name)
|
|
196
|
+
}
|
|
@@ -4,7 +4,7 @@ import { join } from 'path'
|
|
|
4
4
|
import { tmpdir } from 'os'
|
|
5
5
|
|
|
6
6
|
// We test the pure functions by importing and overriding cwd
|
|
7
|
-
import { portsFilePath, getPort, resolvePort, slugify } from './port.js'
|
|
7
|
+
import { portsFilePath, getPort, resolvePort, slugify, repoRoot, worktreeDir, listWorktrees } from './port.js'
|
|
8
8
|
|
|
9
9
|
describe('slugify', () => {
|
|
10
10
|
it('lowercases and replaces dots with hyphens', () => {
|
|
@@ -130,3 +130,93 @@ describe('getPort / resolvePort', () => {
|
|
|
130
130
|
expect(port).toBe(1235)
|
|
131
131
|
})
|
|
132
132
|
})
|
|
133
|
+
|
|
134
|
+
describe('repoRoot', () => {
|
|
135
|
+
let tempRoot
|
|
136
|
+
|
|
137
|
+
beforeEach(() => {
|
|
138
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-root-test-')))
|
|
139
|
+
mkdirSync(join(tempRoot, '.worktrees', 'my-branch'), { recursive: true })
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('returns cwd when at repo root', () => {
|
|
147
|
+
expect(repoRoot(tempRoot)).toBe(tempRoot)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('returns parent of .worktrees when inside a worktree', () => {
|
|
151
|
+
const wt = join(tempRoot, '.worktrees', 'my-branch')
|
|
152
|
+
expect(repoRoot(wt)).toBe(tempRoot)
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
describe('worktreeDir', () => {
|
|
157
|
+
let tempRoot
|
|
158
|
+
|
|
159
|
+
beforeEach(() => {
|
|
160
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-wtdir-test-')))
|
|
161
|
+
mkdirSync(join(tempRoot, '.worktrees'), { recursive: true })
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('returns repo root for main', () => {
|
|
169
|
+
expect(worktreeDir('main', tempRoot)).toBe(tempRoot)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('returns .worktrees/<name> for branches', () => {
|
|
173
|
+
expect(worktreeDir('my-feature', tempRoot)).toBe(join(tempRoot, '.worktrees', 'my-feature'))
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('listWorktrees', () => {
|
|
178
|
+
let tempRoot
|
|
179
|
+
|
|
180
|
+
beforeEach(() => {
|
|
181
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-list-test-')))
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('returns empty array when .worktrees does not exist', () => {
|
|
189
|
+
expect(listWorktrees(tempRoot)).toEqual([])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('returns only directories with a .git file', () => {
|
|
193
|
+
const wtDir = join(tempRoot, '.worktrees')
|
|
194
|
+
mkdirSync(wtDir)
|
|
195
|
+
|
|
196
|
+
// Valid worktree — has .git file
|
|
197
|
+
mkdirSync(join(wtDir, 'valid'))
|
|
198
|
+
writeFileSync(join(wtDir, 'valid', '.git'), 'gitdir: /some/path')
|
|
199
|
+
|
|
200
|
+
// Not a worktree — no .git file
|
|
201
|
+
mkdirSync(join(wtDir, 'no-git'))
|
|
202
|
+
|
|
203
|
+
// Not a directory — file
|
|
204
|
+
writeFileSync(join(wtDir, 'ports.json'), '{}')
|
|
205
|
+
|
|
206
|
+
const result = listWorktrees(tempRoot)
|
|
207
|
+
expect(result).toEqual(['valid'])
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('returns multiple worktrees', () => {
|
|
211
|
+
const wtDir = join(tempRoot, '.worktrees')
|
|
212
|
+
mkdirSync(wtDir)
|
|
213
|
+
|
|
214
|
+
for (const name of ['alpha', 'beta', 'gamma']) {
|
|
215
|
+
mkdirSync(join(wtDir, name))
|
|
216
|
+
writeFileSync(join(wtDir, name, '.git'), 'gitdir: /some/path')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const result = listWorktrees(tempRoot)
|
|
220
|
+
expect(result.sort()).toEqual(['alpha', 'beta', 'gamma'])
|
|
221
|
+
})
|
|
222
|
+
})
|
package/widgets.config.json
CHANGED
|
@@ -495,12 +495,11 @@
|
|
|
495
495
|
{
|
|
496
496
|
"action": "copy-as-png",
|
|
497
497
|
"label": "$label:copy-png",
|
|
498
|
-
"icon": "copy"
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
"icon": "link"
|
|
498
|
+
"icon": "copy",
|
|
499
|
+
"alt": {
|
|
500
|
+
"label": "$label:copy-path",
|
|
501
|
+
"action": "copy-file-path"
|
|
502
|
+
}
|
|
504
503
|
}
|
|
505
504
|
]
|
|
506
505
|
},
|