@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
package/src/core/cli/index.js
CHANGED
|
@@ -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: '
|
|
397
|
-
: { value: 'copilot', label:
|
|
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: '
|
|
405
|
-
{ value: 'sessions', label: '
|
|
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:
|
|
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:
|
|
464
|
+
label: `> ${a.label} sessions`,
|
|
465
465
|
})),
|
|
466
|
-
{ value: 'terminal', label: '
|
|
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 || ''}...`
|