@dfosco/storyboard-core 4.2.7 → 4.2.8

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.7",
3
+ "version": "4.2.8",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -20,12 +20,15 @@ If the branch already exists locally or on the remote:
20
20
  git worktree add worktrees/<branch-name> <branch-name>
21
21
  ```
22
22
 
23
- If the branch does NOT exist yet, create it from the current HEAD:
23
+ If the branch does NOT exist yet, create it **from the current branch** (NOT from main):
24
24
 
25
25
  ```bash
26
- git worktree add worktrees/<branch-name> -b <branch-name>
26
+ CURRENT_BRANCH=$(git branch --show-current)
27
+ git worktree add worktrees/<branch-name> -b <branch-name> "$CURRENT_BRANCH"
27
28
  ```
28
29
 
30
+ > **⚠️ CRITICAL:** Always pass the current branch as the start-point. Without it, git defaults to HEAD of the main worktree, which may be a completely different branch.
31
+
29
32
  ### Step 2: Register a dev-server port
30
33
 
31
34
  Assign a unique port for this worktree so multiple dev servers can run simultaneously.
package/src/cli/branch.js CHANGED
@@ -2,18 +2,24 @@
2
2
  * storyboard branch — Interactive guide to switch to a branch worktree.
3
3
  *
4
4
  * Deterministic flow (no AI):
5
- * 1. Ask which branch to work on
6
- * 2. Stash uncommitted work (named stash for safety)
7
- * 3. Create worktree if needed (git worktree add + npm install)
8
- * 4. Pull --rebase from origin
9
- * 5. Apply stash into the new worktree
10
- * 6. Confirm to user
5
+ * 1. Ask which branch to work on (or accept --worktree flag)
6
+ * 2. If existing worktree:
7
+ * a. Stash uncommitted work in source (named stash)
8
+ * b. Ensure target is on the correct branch
9
+ * c. Apply source stash in target
10
+ * 3. If new worktree:
11
+ * a. Stash uncommitted work in source
12
+ * b. Create worktree (git worktree add + npm install)
13
+ * c. Pull --rebase from origin
14
+ * d. Apply source stash in target
15
+ * 4. Confirm to user
11
16
  *
12
17
  * Also available as post-setup prompt in setup.js.
13
18
  *
14
19
  * Usage:
15
- * npx storyboard branch
16
- * npx storyboard branch <name> # skip the prompt
20
+ * npx storyboard branch # interactive
21
+ * npx storyboard branch <name> # positional, skip prompt
22
+ * npx storyboard branch --worktree=<name> # non-interactive flag
17
23
  */
18
24
 
19
25
  import * as p from '@clack/prompts'
@@ -22,8 +28,14 @@ import { existsSync } from 'fs'
22
28
  import { resolve } from 'path'
23
29
  import { repoRoot, worktreeDir, listWorktrees, getPort, detectWorktreeName } from '../worktree/port.js'
24
30
  import { hasUncommittedChanges, localBranchExists } from './dev-helpers.js'
31
+ import { parseFlags } from './flags.js'
25
32
  import { dim, green, bold, cyan } from './intro.js'
26
33
 
34
+ const flagSchema = {
35
+ worktree: { type: 'string', description: 'Target worktree/branch name (non-interactive)' },
36
+ cd: { type: 'boolean', default: false, description: 'Output shell-evaluable cd command (for eval)' },
37
+ }
38
+
27
39
  /** Check if a remote branch exists on origin. */
28
40
  function remoteBranchExists(name, cwd) {
29
41
  try {
@@ -34,7 +46,7 @@ function remoteBranchExists(name, cwd) {
34
46
  }
35
47
  }
36
48
 
37
- /** Get the current branch name. */
49
+ /** Get the current branch name in a given directory. */
38
50
  function currentBranch(cwd) {
39
51
  try {
40
52
  return execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim()
@@ -55,96 +67,151 @@ function isValidBranchName(name) {
55
67
  return undefined
56
68
  }
57
69
 
58
- export async function runBranchGuide(branchArg) {
59
- const root = repoRoot()
60
- const existing = listWorktrees()
61
- const fromBranch = currentBranch(root)
62
- const fromWorktree = detectWorktreeName()
70
+ /** Build a timestamped stash message. */
71
+ function stashMessage(from, to) {
72
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
73
+ return `from-${from}-to-${to}-${ts}`
74
+ }
63
75
 
64
- // 1. Get branch name
65
- let targetBranch = branchArg?.trim()
76
+ /**
77
+ * Stash uncommitted changes (including untracked files) and return the stash ref SHA.
78
+ * Returns null if nothing was stashed.
79
+ */
80
+ function stashChanges(cwd, message) {
81
+ if (!hasUncommittedChanges(cwd)) return null
66
82
 
67
- if (!targetBranch) {
68
- if (existing.length > 0) {
69
- // Render as columns that fit the terminal width
70
- const maxLen = Math.max(...existing.map(w => w.length))
71
- const colWidth = maxLen + 2
72
- const termWidth = process.stdout.columns || 80
73
- const cols = Math.max(1, Math.floor(termWidth / colWidth))
74
- const rows = Math.ceil(existing.length / cols)
75
- const lines = []
76
- for (let r = 0; r < rows; r++) {
77
- const parts = []
78
- for (let c = 0; c < cols; c++) {
79
- const idx = c * rows + r
80
- if (idx < existing.length) {
81
- parts.push(cyan(existing[idx].padEnd(colWidth)))
82
- }
83
- }
84
- lines.push(` ${parts.join('')}`)
83
+ p.log.step('Stashing uncommitted work…')
84
+ try {
85
+ execFileSync('git', ['stash', 'push', '-u', '-m', message], { cwd, stdio: 'pipe' })
86
+ // Capture the exact stash SHA so we can apply it by ref later
87
+ const sha = execFileSync('git', ['stash', 'list', '--format=%H', '-1'], { cwd, encoding: 'utf8' }).trim()
88
+ p.log.success(`Work stashed: ${dim(message)}`)
89
+ return sha
90
+ } catch {
91
+ p.log.warning('Could not stash changes — proceeding anyway')
92
+ return null
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Apply a specific stash by SHA in the given directory.
98
+ * Uses apply (not pop) so the backup remains in the stash list.
99
+ */
100
+ function applyStash(cwd, stashSha) {
101
+ try {
102
+ execFileSync('git', ['stash', 'apply', stashSha], { cwd, stdio: 'pipe' })
103
+ p.log.success('Previous work applied to this branch')
104
+ return true
105
+ } catch {
106
+ p.log.warning('Stash apply had conflicts — resolve them manually')
107
+ p.log.info(` Your work is safe in ${dim('git stash list')}`)
108
+ return false
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Core logic for switching to an EXISTING worktree.
114
+ *
115
+ * 1. Stash source changes (if any)
116
+ * 2. Ensure target worktree is on the expected branch
117
+ * 3. Apply source stash in target
118
+ */
119
+ function switchToExistingWorktree(targetBranch, { sourceDir, fromBranch }) {
120
+ const targetDir = worktreeDir(targetBranch)
121
+
122
+ // 1. Stash source changes
123
+ const sourceStashSha = stashChanges(sourceDir, stashMessage(fromBranch, targetBranch))
124
+
125
+ // 2. Ensure target is on the correct branch
126
+ const targetCurrentBranch = currentBranch(targetDir)
127
+
128
+ if (targetCurrentBranch !== targetBranch) {
129
+ // Target worktree is on a different branch — check if it's clean
130
+ if (hasUncommittedChanges(targetDir)) {
131
+ p.log.error(
132
+ `Worktree ${bold(targetBranch)} is on branch ${bold(targetCurrentBranch)} with uncommitted changes.`
133
+ )
134
+ p.log.info(` Clean up the worktree first:`)
135
+ p.log.info(` ${green('cd')} ${dim(`worktrees/${targetBranch}`)}`)
136
+ p.log.info(` ${dim('git stash')} ${dim('or')} ${dim('git commit')}`)
137
+ p.log.info(` ${dim(`git checkout ${targetBranch}`)}`)
138
+ if (sourceStashSha) {
139
+ p.log.info(`\n Your source stash is safe — apply it later with:`)
140
+ p.log.info(` ${dim(`git stash apply ${sourceStashSha.slice(0, 8)}`)}`)
85
141
  }
86
- p.log.info(`${dim('Existing worktrees:')}\n${lines.join('\n')}`)
142
+ p.outro('')
143
+ process.exit(1)
87
144
  }
88
145
 
89
- const result = await p.text({
90
- message: 'Which branch do you want to work on? Select one from above or type a new one',
91
- placeholder: 'e.g. 4.3.0--my-feature',
92
- validate: isValidBranchName,
93
- })
146
+ // Clean worktree on wrong branch — switch it
147
+ p.log.step(`Switching worktree from ${bold(targetCurrentBranch)} to ${bold(targetBranch)}…`)
148
+ try {
149
+ execFileSync('git', ['checkout', targetBranch], { cwd: targetDir, stdio: 'pipe' })
150
+ p.log.success(`Now on branch ${bold(targetBranch)}`)
151
+ } catch (err) {
152
+ p.log.error(`Could not switch branch: ${err.message || 'git checkout failed'}`)
153
+ if (sourceStashSha) {
154
+ p.log.info(` Your source stash is safe: ${dim(`git stash apply ${sourceStashSha.slice(0, 8)}`)}`)
155
+ }
156
+ p.outro('')
157
+ process.exit(1)
158
+ }
159
+ } else {
160
+ p.log.success(`Worktree ${bold(targetBranch)} is on the correct branch`)
161
+ }
94
162
 
95
- if (p.isCancel(result)) {
96
- p.cancel('Cancelled')
97
- process.exit(0)
163
+ // 3. Apply source stash in target
164
+ if (sourceStashSha) {
165
+ if (hasUncommittedChanges(targetDir)) {
166
+ p.log.warning(`Target worktree has uncommitted changes — skipping stash apply`)
167
+ p.log.info(` Apply your stash manually: ${dim(`git stash apply ${sourceStashSha.slice(0, 8)}`)}`)
168
+ } else {
169
+ applyStash(targetDir, sourceStashSha)
98
170
  }
99
- targetBranch = result.trim()
100
171
  }
101
172
 
102
- // 2. Check if worktree already exists
103
- const wtDir = worktreeDir(targetBranch)
104
- if (existsSync(resolve(wtDir, '.git'))) {
105
- p.log.success(`Worktree ${bold(targetBranch)} already exists`)
106
- p.note(
107
- [
108
- ` ${green('cd')} ${dim(`worktrees/${targetBranch}`)}`,
109
- ` ${green('npx storyboard dev')} ${dim('to start developing')}`,
110
- ].join('\n'),
111
- 'Ready to go'
112
- )
113
- p.outro('')
114
- return
115
- }
116
-
117
- // 3. Stash uncommitted work
118
- let didStash = false
119
- const sourceDir = fromWorktree === 'main' ? root : worktreeDir(fromWorktree)
173
+ // 4. Summary
174
+ const lines = [
175
+ ` Worktree ready: ${green(`worktrees/${targetBranch}`)}`,
176
+ ]
177
+ if (sourceStashSha) {
178
+ lines.push(` Your uncommitted work has been safely moved`)
179
+ }
180
+ lines.push('')
181
+ lines.push(` ${green('cd')} ${dim(`worktrees/${targetBranch}`)}`)
182
+ lines.push(` ${green('npx storyboard dev')} ${dim('to start developing')}`)
183
+ lines.push('')
184
+ lines.push(` ${dim('Tip: auto-cd with')} ${green('eval "$(npx sb branch --cd)"')}`)
120
185
 
121
- if (hasUncommittedChanges(sourceDir)) {
122
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
123
- const stashName = `${fromBranch}→${targetBranch}@${timestamp}`
186
+ p.note(lines.join('\n'), `Branch ${bold(targetBranch)} is ready`)
187
+ p.outro('')
188
+ return targetDir
189
+ }
124
190
 
125
- p.log.step('Stashing uncommitted work…')
126
- try {
127
- execFileSync('git', ['stash', 'push', '-m', stashName], { cwd: sourceDir, stdio: 'pipe' })
128
- didStash = true
129
- p.log.success(`Work stashed: ${dim(stashName)}`)
130
- } catch {
131
- p.log.warning('Could not stash changes — proceeding anyway')
132
- }
133
- }
191
+ /**
192
+ * Core logic for creating a NEW worktree.
193
+ *
194
+ * 1. Stash source changes
195
+ * 2. Resolve branch (local/remote/new)
196
+ * 3. Create worktree + npm install
197
+ * 4. Pull --rebase
198
+ * 5. Apply source stash
199
+ */
200
+ function createNewWorktree(targetBranch, { sourceDir, fromBranch, root }) {
201
+ // 1. Stash source changes
202
+ const sourceStashSha = stashChanges(sourceDir, stashMessage(fromBranch, targetBranch))
134
203
 
135
- // 4. Resolve branch and create worktree
204
+ // 2. Resolve branch
136
205
  const hasLocal = localBranchExists(targetBranch, root)
137
206
  const hasRemote = !hasLocal && remoteBranchExists(targetBranch, root)
138
207
  const isNew = !hasLocal && !hasRemote
139
208
 
140
209
  if (isNew) {
141
- p.log.step(`Creating new branch ${bold(targetBranch)} from HEAD`)
210
+ p.log.step(`Creating new branch ${bold(targetBranch)} from ${bold(fromBranch)}`)
142
211
  } else if (hasRemote) {
143
- // Fetch the remote branch so git worktree add can use it
144
212
  p.log.step(`Fetching ${bold(targetBranch)} from origin…`)
145
213
  try {
146
214
  execFileSync('git', ['fetch', 'origin', targetBranch], { cwd: root, stdio: 'pipe' })
147
- // Create a local tracking branch
148
215
  try {
149
216
  execFileSync('git', ['branch', targetBranch, `origin/${targetBranch}`], { cwd: root, stdio: 'pipe' })
150
217
  } catch { /* may already exist after fetch */ }
@@ -155,13 +222,14 @@ export async function runBranchGuide(branchArg) {
155
222
  p.log.step(`Using existing branch ${bold(targetBranch)}`)
156
223
  }
157
224
 
158
- // Create the worktree
225
+ // 3. Create the worktree
159
226
  const targetDir = resolve(root, 'worktrees', targetBranch)
160
227
  const spin = p.spinner()
161
228
 
162
229
  try {
230
+ // For new branches, use current branch as start-point (not main's HEAD)
163
231
  const gitArgs = isNew
164
- ? ['worktree', 'add', targetDir, '-b', targetBranch]
232
+ ? ['worktree', 'add', targetDir, '-b', targetBranch, fromBranch]
165
233
  : ['worktree', 'add', targetDir, targetBranch]
166
234
 
167
235
  spin.start(`Creating worktree worktrees/${targetBranch}`)
@@ -186,7 +254,7 @@ export async function runBranchGuide(branchArg) {
186
254
  // Assign a dev server port
187
255
  getPort(targetBranch)
188
256
 
189
- // 5. Pull --rebase from origin
257
+ // 4. Pull --rebase from origin
190
258
  if (!isNew) {
191
259
  try {
192
260
  spin.start('Pulling latest changes…')
@@ -197,36 +265,122 @@ export async function runBranchGuide(branchArg) {
197
265
  }
198
266
  }
199
267
 
200
- // 6. Apply stash (if we stashed earlier)
201
- if (didStash) {
202
- try {
203
- // Apply (not pop) — keeps the backup in stash list
204
- execFileSync('git', ['stash', 'apply'], { cwd: targetDir, stdio: 'pipe' })
205
- p.log.success('Previous work applied to this branch')
206
- } catch {
207
- p.log.warning('Stash apply had conflicts — resolve them manually')
208
- p.log.info(` Your work is safe in ${dim('git stash list')}`)
209
- }
268
+ // 5. Apply source stash
269
+ if (sourceStashSha) {
270
+ applyStash(targetDir, sourceStashSha)
210
271
  }
211
272
 
212
- // 7. Summary
273
+ // 6. Summary
213
274
  const lines = [
214
275
  ` Your branch is set up as a worktree in ${green(`worktrees/${targetBranch}`)}`,
215
276
  ]
216
- if (didStash) {
277
+ if (sourceStashSha) {
217
278
  lines.push(` Your uncommitted work has been safely moved`)
218
279
  }
219
280
  lines.push('')
220
281
  lines.push(` ${green('cd')} ${dim(`worktrees/${targetBranch}`)}`)
221
282
  lines.push(` ${green('npx storyboard dev')} ${dim('to start developing')}`)
222
283
  lines.push('')
223
- lines.push(` ${dim('Tip: ask your AI agent about worktrees — they\'re great!')}`)
284
+ lines.push(` ${dim('Tip: auto-cd with')} ${green('eval "$(npx sb branch --cd)"')}`)
224
285
 
225
286
  p.note(lines.join('\n'), `Branch ${bold(targetBranch)} is ready`)
226
287
  p.outro('')
288
+ return targetDir
289
+ }
290
+
291
+ // ─── Main ───
292
+
293
+ export async function runBranchGuide(branchArg) {
294
+ const root = repoRoot()
295
+ const existing = listWorktrees()
296
+ const fromWorktree = detectWorktreeName()
297
+ const sourceDir = fromWorktree === 'main' ? root : worktreeDir(fromWorktree)
298
+ const fromBranch = currentBranch(sourceDir)
299
+
300
+ // 1. Get branch name — select from existing or type a new one
301
+ let targetBranch = branchArg?.trim()
302
+
303
+ if (!targetBranch) {
304
+ if (existing.length > 0) {
305
+ // Build select options from existing worktrees + "new branch" option
306
+ const NEW_BRANCH = Symbol('new')
307
+ const options = [
308
+ ...existing.map(name => ({ value: name, label: name })),
309
+ { value: NEW_BRANCH, label: dim('Create a new branch…') },
310
+ ]
311
+
312
+ const selected = await p.select({
313
+ message: 'Which branch do you want to work on?',
314
+ options,
315
+ })
316
+
317
+ if (p.isCancel(selected)) {
318
+ p.cancel('Cancelled')
319
+ process.exit(0)
320
+ }
321
+
322
+ if (selected === NEW_BRANCH) {
323
+ const newName = await p.text({
324
+ message: 'New branch name:',
325
+ placeholder: 'e.g. 4.3.0--my-feature',
326
+ validate: isValidBranchName,
327
+ })
328
+ if (p.isCancel(newName)) {
329
+ p.cancel('Cancelled')
330
+ process.exit(0)
331
+ }
332
+ targetBranch = newName.trim()
333
+ } else {
334
+ targetBranch = selected
335
+ }
336
+ } else {
337
+ // No existing worktrees — just ask for a name
338
+ const result = await p.text({
339
+ message: 'Branch name for new worktree:',
340
+ placeholder: 'e.g. 4.3.0--my-feature',
341
+ validate: isValidBranchName,
342
+ })
343
+
344
+ if (p.isCancel(result)) {
345
+ p.cancel('Cancelled')
346
+ process.exit(0)
347
+ }
348
+ targetBranch = result.trim()
349
+ }
350
+ }
351
+
352
+ // 2. Show equivalent non-interactive command (when user used TUI to select)
353
+ if (!branchArg) {
354
+ p.log.info(`${dim('Non-interactive:')} ${green(`npx sb branch --worktree=${targetBranch}`)}`)
355
+ }
356
+
357
+ // 3. Route to existing or new worktree flow
358
+ const wtDir = worktreeDir(targetBranch)
359
+ if (existsSync(resolve(wtDir, '.git'))) {
360
+ return switchToExistingWorktree(targetBranch, { sourceDir, fromBranch })
361
+ } else {
362
+ return createNewWorktree(targetBranch, { sourceDir, fromBranch, root })
363
+ }
364
+ }
365
+
366
+ // ─── Direct invocation ───
367
+
368
+ const { flags, positional } = parseFlags(process.argv.slice(3), flagSchema)
369
+ const branchArg = flags.worktree || positional[0] || undefined
370
+
371
+ // When --cd is set, redirect all TUI output (Clack) to stderr so that
372
+ // stdout contains only the shell-evaluable `cd <path>` command.
373
+ // Usage: eval "$(npx sb branch --worktree=<name> --cd)"
374
+ const realStdoutWrite = process.stdout.write.bind(process.stdout)
375
+ if (flags.cd) {
376
+ process.stdout.write = (chunk, encoding, callback) =>
377
+ process.stderr.write(chunk, encoding, callback)
227
378
  }
228
379
 
229
- // Direct invocation
230
- const branchArg = process.argv[3] || undefined
231
380
  p.intro('storyboard branch')
232
- runBranchGuide(branchArg)
381
+ runBranchGuide(branchArg).then((targetDir) => {
382
+ if (flags.cd && targetDir) {
383
+ process.stdout.write = realStdoutWrite
384
+ realStdoutWrite(`cd ${JSON.stringify(targetDir)}\n`)
385
+ }
386
+ })
package/src/cli/index.js CHANGED
@@ -93,8 +93,11 @@ function helpScreen(version) {
93
93
  '',
94
94
  ` ${bold(cyan('Setup'))}`,
95
95
  cmd('setup', 'Install deps, Caddy proxy, start proxy'),
96
+ cmd('setup --skip-branch', 'Non-interactive setup (skip branch prompt)'),
97
+ cmd('setup --branch=<name>', 'Setup + switch to a branch'),
96
98
  cmd('branch', 'Switch to a branch (interactive worktree guide)'),
97
99
  cmd('branch <name>', 'Switch to a specific branch directly'),
100
+ cmd('branch --worktree=<name>', 'Non-interactive branch switch'),
98
101
  cmd('proxy start', 'Start or reload Caddy proxy'),
99
102
  cmd('proxy close', 'Stop Caddy proxy'),
100
103
  '',
package/src/cli/setup.js CHANGED
@@ -2,6 +2,11 @@
2
2
  * storyboard setup — One-time setup for the Storyboard dev environment.
3
3
  *
4
4
  * Idempotent: safe to run multiple times, only does what's needed.
5
+ *
6
+ * Usage:
7
+ * npx storyboard setup # interactive
8
+ * npx storyboard setup --skip-branch # non-interactive, skip branch prompt
9
+ * npx storyboard setup --branch=<name> # non-interactive, switch to branch after setup
5
10
  */
6
11
 
7
12
  import * as p from '@clack/prompts'
@@ -9,7 +14,15 @@ import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, symlin
9
14
  import path from 'path'
10
15
  import { execSync } from 'child_process'
11
16
  import { generateCaddyfile, isCaddyInstalled, isCaddyRunning, startCaddy, reloadCaddy } from './proxy.js'
12
- import { gettingStartedLines, dim, magenta, bold, yellow } from './intro.js'
17
+ import { gettingStartedLines, dim, magenta, bold, yellow, green } from './intro.js'
18
+ import { parseFlags } from './flags.js'
19
+
20
+ const flagSchema = {
21
+ 'skip-branch': { type: 'boolean', default: false, description: 'Skip the branch prompt at the end' },
22
+ branch: { type: 'string', description: 'Switch to a branch after setup (non-interactive)' },
23
+ }
24
+
25
+ const { flags } = parseFlags(process.argv.slice(3), flagSchema)
13
26
 
14
27
  /**
15
28
  * Run a potentially slow task with a spinner that only appears after 500ms.
@@ -330,11 +343,16 @@ if (isCaddyInstalled()) {
330
343
  p.log.info(dim('Skipping npm install — dev server is running (would cause restart)'))
331
344
  p.log.info(dim('Run `npm install` manually after stopping the dev server if needed'))
332
345
  } else {
333
- await withSpin(
334
- 'Installing dependencies...',
335
- 'Dependencies installed',
336
- () => { run('npm install', { stdio: 'ignore' }) }
337
- )
346
+ try {
347
+ await withSpin(
348
+ 'Installing dependencies...',
349
+ 'Dependencies installed',
350
+ () => { run('npm install', { stdio: 'ignore' }) }
351
+ )
352
+ } catch {
353
+ p.log.warning('npm install failed — run it manually to see details')
354
+ p.log.info(` ${dim('npm install')}`)
355
+ }
338
356
  }
339
357
  }
340
358
 
@@ -349,26 +367,38 @@ p.note(
349
367
 
350
368
  // Offer branch guide
351
369
  {
352
- let currentBranchName = 'main'
353
- try {
354
- currentBranchName = execSync('git branch --show-current', { encoding: 'utf8' }).trim() || 'main'
355
- } catch { /* empty */ }
356
-
357
- const wantBranch = await p.select({
358
- message: 'Want to work from a different branch?',
359
- options: [
360
- { value: true, label: 'Yes (help me create it)' },
361
- { value: false, label: `No (stay on current branch ${bold(currentBranchName)})` },
362
- ],
363
- initialValue: false,
364
- })
365
-
366
- if (!p.isCancel(wantBranch) && wantBranch) {
370
+ // Non-interactive: --branch=<name> or --skip-branch
371
+ if (flags.branch) {
367
372
  const { runBranchGuide } = await import('./branch.js')
368
- await runBranchGuide()
369
- } else {
373
+ await runBranchGuide(flags.branch)
374
+ } else if (flags['skip-branch']) {
370
375
  console.log()
371
376
  console.log(mascot())
372
377
  p.outro('')
378
+ } else {
379
+ // Interactive: ask the user
380
+ let currentBranchName = 'main'
381
+ try {
382
+ currentBranchName = execSync('git branch --show-current', { encoding: 'utf8' }).trim() || 'main'
383
+ } catch { /* empty */ }
384
+
385
+ const wantBranch = await p.select({
386
+ message: 'Want to work from a different branch?',
387
+ options: [
388
+ { value: true, label: 'Yes (help me create it)' },
389
+ { value: false, label: `No (stay on current branch ${bold(currentBranchName)})` },
390
+ ],
391
+ initialValue: false,
392
+ })
393
+
394
+ if (!p.isCancel(wantBranch) && wantBranch) {
395
+ const { runBranchGuide } = await import('./branch.js')
396
+ await runBranchGuide()
397
+ } else {
398
+ console.log()
399
+ console.log(mascot())
400
+ p.log.info(`${dim('Non-interactive:')} ${green('npx sb setup --skip-branch')}`)
401
+ p.outro('')
402
+ }
373
403
  }
374
404
  }