@dfosco/storyboard 0.5.0-alpha.5 → 0.5.0-alpha.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",
3
- "version": "0.5.0-alpha.5",
3
+ "version": "0.5.0-alpha.7",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -123,6 +123,8 @@ function helpScreen(version) {
123
123
  cmd('branch', 'Switch to a branch (interactive worktree guide)'),
124
124
  cmd('branch <name>', 'Switch to a specific branch directly'),
125
125
  cmd('branch --worktree=<name>', 'Non-interactive branch switch'),
126
+ cmd('pull', 'Pull latest changes from remote (untracked-safe)'),
127
+ cmd('publish', 'Push local commits to remote (pulls first)'),
126
128
  cmd('proxy start', 'Start or reload Caddy proxy'),
127
129
  cmd('proxy close', 'Stop Caddy proxy'),
128
130
  '',
@@ -152,6 +154,12 @@ switch (command) {
152
154
  case 'branch':
153
155
  import('./branch.js')
154
156
  break
157
+ case 'pull':
158
+ import('./pull.js')
159
+ break
160
+ case 'publish':
161
+ import('./publish.js')
162
+ break
155
163
  case 'proxy':
156
164
  import('./proxy.js')
157
165
  break
@@ -0,0 +1,148 @@
1
+ /**
2
+ * storyboard publish — Push local commits to the remote branch (untracked-safe).
3
+ *
4
+ * Deterministic flow (no AI):
5
+ * 1. Stash uncommitted work (including untracked files)
6
+ * 2. Pull --rebase from origin (stay up to date)
7
+ * 3. If conflict: abort rebase, restore stash, inform user
8
+ * 4. Push to origin
9
+ * 5. Re-apply stash
10
+ *
11
+ * Usage:
12
+ * npx storyboard publish
13
+ * npx sb publish
14
+ */
15
+
16
+ import * as p from '@clack/prompts'
17
+ import { execFileSync } from 'child_process'
18
+ import { detectWorktreeName, worktreeDir, repoRoot } from '../worktree/port.js'
19
+ import { hasUncommittedChanges } from './dev-helpers.js'
20
+ import { dim, green, bold } from './intro.js'
21
+
22
+ function currentBranch(cwd) {
23
+ try {
24
+ return execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim()
25
+ } catch {
26
+ return 'unknown'
27
+ }
28
+ }
29
+
30
+ function hasUnpushedCommits(cwd, branch) {
31
+ try {
32
+ const count = execFileSync(
33
+ 'git', ['rev-list', '--count', `origin/${branch}..${branch}`],
34
+ { cwd, encoding: 'utf8' }
35
+ ).trim()
36
+ return parseInt(count, 10) > 0
37
+ } catch {
38
+ // Remote branch may not exist yet — that's fine, we'll push anyway
39
+ return true
40
+ }
41
+ }
42
+
43
+ p.intro('storyboard publish')
44
+
45
+ const wtName = detectWorktreeName()
46
+ const root = repoRoot()
47
+ const cwd = wtName === 'main' ? root : worktreeDir(wtName)
48
+ const branch = currentBranch(cwd)
49
+
50
+ p.log.info(`Branch: ${bold(branch)}`)
51
+
52
+ // 1. Stash uncommitted work (including untracked files)
53
+ let stashSha = null
54
+ if (hasUncommittedChanges(cwd)) {
55
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
56
+ const message = `publish-stash-${branch}-${ts}`
57
+ p.log.step('Stashing uncommitted work…')
58
+ try {
59
+ execFileSync('git', ['stash', 'push', '-u', '-m', message], { cwd, stdio: 'pipe' })
60
+ stashSha = execFileSync('git', ['stash', 'list', '--format=%H', '-1'], { cwd, encoding: 'utf8' }).trim()
61
+ p.log.success(`Work stashed: ${dim(message)}`)
62
+ } catch {
63
+ p.log.warning('Could not stash changes — proceeding anyway')
64
+ }
65
+ }
66
+
67
+ // 2. Pull --rebase from origin first
68
+ const pullSpin = p.spinner()
69
+ pullSpin.start('Pulling latest from remote…')
70
+
71
+ try {
72
+ execFileSync('git', ['pull', '--rebase', 'origin', branch], { cwd, stdio: 'pipe' })
73
+ pullSpin.stop('Up to date with origin')
74
+ } catch {
75
+ pullSpin.stop('Pull failed — conflict detected')
76
+
77
+ // Abort rebase to restore clean state
78
+ try {
79
+ execFileSync('git', ['rebase', '--abort'], { cwd, stdio: 'pipe' })
80
+ p.log.step('Rebase aborted — working tree restored')
81
+ } catch {
82
+ // May not be in rebase state
83
+ }
84
+
85
+ // Re-apply stash
86
+ if (stashSha) {
87
+ try {
88
+ execFileSync('git', ['stash', 'apply', stashSha], { cwd, stdio: 'pipe' })
89
+ p.log.success('Stashed work restored')
90
+ } catch {
91
+ p.log.warning(`Could not restore stash — find it with: ${dim('git stash list')}`)
92
+ }
93
+ }
94
+
95
+ p.log.error('Could not publish — there are conflicts with the remote branch.')
96
+ p.log.info('')
97
+ p.log.info(` Resolve conflicts manually:`)
98
+ p.log.info(` ${green('git pull --rebase origin ' + branch)}`)
99
+ p.log.info(` ${dim('Fix conflicts, then:')} ${green('git rebase --continue')}`)
100
+ p.log.info(` ${green('git push origin ' + branch)}`)
101
+ p.log.info('')
102
+ p.log.info(` Or ask an agent to help resolve the conflicts.`)
103
+ p.outro('')
104
+ process.exit(1)
105
+ }
106
+
107
+ // 3. Push to origin
108
+ const pushSpin = p.spinner()
109
+ pushSpin.start('Publishing to remote…')
110
+
111
+ try {
112
+ execFileSync('git', ['push', 'origin', branch], { cwd, stdio: 'pipe' })
113
+ pushSpin.stop('Published to origin')
114
+ } catch (err) {
115
+ pushSpin.stop('Push failed')
116
+
117
+ // Re-apply stash before exiting
118
+ if (stashSha) {
119
+ try {
120
+ execFileSync('git', ['stash', 'apply', stashSha], { cwd, stdio: 'pipe' })
121
+ p.log.success('Stashed work restored')
122
+ } catch {
123
+ p.log.warning(`Could not restore stash — find it with: ${dim('git stash list')}`)
124
+ }
125
+ }
126
+
127
+ p.log.error('Could not push to remote.')
128
+ p.log.info('')
129
+ p.log.info(` Try manually:`)
130
+ p.log.info(` ${green('git push origin ' + branch)}`)
131
+ p.log.info('')
132
+ p.outro('')
133
+ process.exit(1)
134
+ }
135
+
136
+ // 4. Re-apply stash
137
+ if (stashSha) {
138
+ try {
139
+ execFileSync('git', ['stash', 'apply', stashSha], { cwd, stdio: 'pipe' })
140
+ p.log.success('Previous work restored')
141
+ } catch {
142
+ p.log.warning('Stash apply had conflicts — resolve them manually')
143
+ p.log.info(` Your work is safe in ${dim('git stash list')}`)
144
+ }
145
+ }
146
+
147
+ p.log.success(`${bold(branch)} published to origin`)
148
+ p.outro('')
@@ -0,0 +1,109 @@
1
+ /**
2
+ * storyboard pull — Pull latest changes from remote (untracked-safe).
3
+ *
4
+ * Deterministic flow (no AI):
5
+ * 1. Stash uncommitted work (including untracked files)
6
+ * 2. Pull --rebase from origin
7
+ * 3. If conflict: abort rebase, restore stash, inform user
8
+ * 4. If clean: re-apply stash
9
+ *
10
+ * Usage:
11
+ * npx storyboard pull
12
+ * npx sb pull
13
+ */
14
+
15
+ import * as p from '@clack/prompts'
16
+ import { execFileSync } from 'child_process'
17
+ import { detectWorktreeName, worktreeDir, repoRoot } from '../worktree/port.js'
18
+ import { hasUncommittedChanges } from './dev-helpers.js'
19
+ import { dim, green, bold } from './intro.js'
20
+
21
+ function currentBranch(cwd) {
22
+ try {
23
+ return execFileSync('git', ['branch', '--show-current'], { cwd, encoding: 'utf8' }).trim()
24
+ } catch {
25
+ return 'unknown'
26
+ }
27
+ }
28
+
29
+ p.intro('storyboard pull')
30
+
31
+ const wtName = detectWorktreeName()
32
+ const root = repoRoot()
33
+ const cwd = wtName === 'main' ? root : worktreeDir(wtName)
34
+ const branch = currentBranch(cwd)
35
+
36
+ p.log.info(`Branch: ${bold(branch)}`)
37
+
38
+ // 1. Stash uncommitted work (including untracked files)
39
+ let stashSha = null
40
+ if (hasUncommittedChanges(cwd)) {
41
+ const ts = new Date().toISOString().replace(/[:.]/g, '-')
42
+ const message = `pull-stash-${branch}-${ts}`
43
+ p.log.step('Stashing uncommitted work…')
44
+ try {
45
+ execFileSync('git', ['stash', 'push', '-u', '-m', message], { cwd, stdio: 'pipe' })
46
+ stashSha = execFileSync('git', ['stash', 'list', '--format=%H', '-1'], { cwd, encoding: 'utf8' }).trim()
47
+ p.log.success(`Work stashed: ${dim(message)}`)
48
+ } catch {
49
+ p.log.warning('Could not stash changes — proceeding anyway')
50
+ }
51
+ }
52
+
53
+ // 2. Pull --rebase from origin
54
+ const spin = p.spinner()
55
+ spin.start('Pulling latest changes…')
56
+
57
+ let pullFailed = false
58
+ try {
59
+ execFileSync('git', ['pull', '--rebase', 'origin', branch], { cwd, stdio: 'pipe' })
60
+ spin.stop('Up to date with origin')
61
+ } catch (err) {
62
+ spin.stop('Pull failed — conflict detected')
63
+ pullFailed = true
64
+
65
+ // 3. Abort the rebase to restore clean state
66
+ try {
67
+ execFileSync('git', ['rebase', '--abort'], { cwd, stdio: 'pipe' })
68
+ p.log.step('Rebase aborted — working tree restored')
69
+ } catch {
70
+ // May not be in rebase state
71
+ }
72
+
73
+ // Re-apply stash if we stashed
74
+ if (stashSha) {
75
+ try {
76
+ execFileSync('git', ['stash', 'apply', stashSha], { cwd, stdio: 'pipe' })
77
+ p.log.success('Stashed work restored')
78
+ } catch {
79
+ p.log.warning(`Could not restore stash — find it with: ${dim('git stash list')}`)
80
+ }
81
+ }
82
+
83
+ p.log.error('Could not pull — there are conflicts with the remote branch.')
84
+ p.log.info('')
85
+ p.log.info(` Resolve conflicts manually:`)
86
+ p.log.info(` ${green('git pull --rebase origin ' + branch)}`)
87
+ p.log.info(` ${dim('Fix conflicts, then:')} ${green('git rebase --continue')}`)
88
+ p.log.info('')
89
+ p.log.info(` Or ask an agent to help resolve the conflicts.`)
90
+ p.outro('')
91
+ process.exit(1)
92
+ }
93
+
94
+ // 4. Re-apply stash
95
+ if (stashSha) {
96
+ try {
97
+ execFileSync('git', ['stash', 'apply', stashSha], { cwd, stdio: 'pipe' })
98
+ p.log.success('Previous work restored')
99
+ } catch {
100
+ p.log.warning('Stash apply had conflicts — resolve them manually')
101
+ p.log.info(` Your work is safe in ${dim('git stash list')}`)
102
+ }
103
+ }
104
+
105
+ if (!pullFailed) {
106
+ p.log.success(`${bold(branch)} is up to date`)
107
+ }
108
+
109
+ p.outro('')
@@ -393,16 +393,16 @@ async function welcomeLoop() {
393
393
 
394
394
  // Build the first option based on number of configured agents
395
395
  const agentOption = agents.length > 1
396
- ? { value: 'agents', label: ' Start a new agent session' }
397
- : { value: 'copilot', label: `✦ Start a new ${agents[0]?.label || 'Copilot'} session` }
396
+ ? { value: 'agents', label: '> Start a new agent session' }
397
+ : { value: 'copilot', label: `> Start a new ${agents[0]?.label || 'Copilot'} session` }
398
398
 
399
399
  drainStdin()
400
400
  const action = await p.select({
401
401
  message: 'How would you like to start?',
402
402
  options: [
403
403
  agentOption,
404
- { value: 'shell', label: ' Start a new terminal session' },
405
- { value: 'sessions', label: ' Browse existing sessions' },
404
+ { value: 'shell', label: '> Start a new terminal session' },
405
+ { value: 'sessions', label: '> Browse existing sessions' },
406
406
  ],
407
407
  })
408
408
 
@@ -418,7 +418,7 @@ async function welcomeLoop() {
418
418
  message: 'Which agent?',
419
419
  options: agents.map(a => ({
420
420
  value: a.id,
421
- label: `✦ Start a new ${a.label} session`,
421
+ label: `> Start a new ${a.label} session`,
422
422
  })),
423
423
  })
424
424
 
@@ -461,9 +461,9 @@ async function welcomeLoop() {
461
461
  const sessionOptions = [
462
462
  ...resumableAgents.map(a => ({
463
463
  value: `agent:${a.id}`,
464
- label: `✦ ${a.label} sessions`,
464
+ label: `> ${a.label} sessions`,
465
465
  })),
466
- { value: 'terminal', label: ' Terminal sessions' },
466
+ { value: 'terminal', label: '> Terminal sessions' },
467
467
  ]
468
468
 
469
469
  drainStdin()
@@ -1213,6 +1213,7 @@ export default function StoryboardCommandPalette({ basePath }) {
1213
1213
  }}
1214
1214
  >
1215
1215
  <Command.Input
1216
+ autoFocus
1216
1217
  placeholder={activePage === 'root'
1217
1218
  ? 'Search commands, prototypes, canvases, stories...'
1218
1219
  : `Search ${toolMenus.find(m => m.id === activePage)?.label || ''}...`