@dfosco/storyboard 0.6.0-beta.5 → 0.6.0-beta.6
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/scaffold/gitignore +12 -2
- package/src/core/canvas/configReader.js +110 -0
- package/src/core/canvas/hot-pool.js +1 -1
- package/src/core/canvas/server.js +4 -9
- package/src/core/canvas/terminal-server.js +32 -35
- package/src/core/cli/setup.js +126 -49
- package/src/core/cli/terminal-welcome.js +5 -6
- package/src/core/vite/server-plugin.js +3 -2
- package/terminal.config.json +16 -1
package/package.json
CHANGED
package/scaffold/gitignore
CHANGED
|
@@ -52,8 +52,18 @@ packages/core/dist/storyboard-ui.*
|
|
|
52
52
|
# Agent Browser
|
|
53
53
|
agent-browser.json
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
.storyboard
|
|
55
|
+
# Auto-generated scaffold dir (copies of library config defaults — overwritten on every dev-server boot)
|
|
56
|
+
.storyboard/scaffold/
|
|
57
|
+
|
|
58
|
+
# Real-time canvas selection bridge for Copilot
|
|
59
|
+
.storyboard/.selectedwidgets.json
|
|
60
|
+
|
|
61
|
+
# Runtime/transient state (per-machine, per-session)
|
|
62
|
+
.storyboard/agent-sessions/
|
|
63
|
+
.storyboard/hot-pool/
|
|
64
|
+
.storyboard/logs/
|
|
65
|
+
.storyboard/terminal-buffers/
|
|
66
|
+
.storyboard/messages/
|
|
57
67
|
|
|
58
68
|
# Private canvas images (tilde prefix = not committed)
|
|
59
69
|
src/canvas/images/~*
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side reader for terminal + agents config.
|
|
3
|
+
*
|
|
4
|
+
* Replaces direct `JSON.parse(readFileSync('storyboard.config.json')).canvas.agents`
|
|
5
|
+
* reads in terminal-server / hot-pool / canvas server / terminal-welcome /
|
|
6
|
+
* server-plugin so the new `terminal.config.json` (and the library's default
|
|
7
|
+
* one shipped under `node_modules/@dfosco/storyboard/terminal.config.json`)
|
|
8
|
+
* is honored everywhere with leaf-level merge.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (lowest → highest priority), all leaf-merged:
|
|
11
|
+
* 1. Library default `<root>/{packages/storyboard,node_modules/@dfosco/storyboard}/terminal.config.json`
|
|
12
|
+
* 2. `storyboard.config.json` `canvas.terminal` + `canvas.agents` (legacy)
|
|
13
|
+
* 3. Root `terminal.config.json`
|
|
14
|
+
*
|
|
15
|
+
* Returns `{ terminal, agents, showAgentsInAddMenu }`. Empty objects when
|
|
16
|
+
* nothing is configured (rather than null) so callers can spread freely.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
20
|
+
import { resolve, join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
/** Same shape as data-plugin's `deepMergeBuild`. */
|
|
23
|
+
function deepMerge(target, source) {
|
|
24
|
+
if (!source || typeof source !== 'object') return target
|
|
25
|
+
if (!target || typeof target !== 'object') return source
|
|
26
|
+
const result = { ...target }
|
|
27
|
+
for (const key of Object.keys(source)) {
|
|
28
|
+
const sv = source[key]
|
|
29
|
+
const tv = target[key]
|
|
30
|
+
if (sv && typeof sv === 'object' && !Array.isArray(sv) && tv && typeof tv === 'object' && !Array.isArray(tv)) {
|
|
31
|
+
result[key] = deepMerge(tv, sv)
|
|
32
|
+
} else {
|
|
33
|
+
result[key] = sv
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readJson(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
return JSON.parse(readFileSync(filePath, 'utf8'))
|
|
42
|
+
} catch {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolveLibTerminalConfig(root) {
|
|
48
|
+
const candidates = [
|
|
49
|
+
join(root, 'packages', 'storyboard', 'terminal.config.json'),
|
|
50
|
+
join(root, 'node_modules', '@dfosco', 'storyboard', 'terminal.config.json'),
|
|
51
|
+
]
|
|
52
|
+
for (const p of candidates) {
|
|
53
|
+
if (existsSync(p)) {
|
|
54
|
+
const parsed = readJson(p)
|
|
55
|
+
if (parsed) return parsed
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Read the merged terminal + agents + hotPool config for a project root.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} [root] - Project root, defaults to `process.cwd()`.
|
|
65
|
+
* @returns {{ terminal: object, agents: object, showAgentsInAddMenu: boolean|undefined, hotPool: object }}
|
|
66
|
+
*/
|
|
67
|
+
export function readTerminalConfigMerged(root = process.cwd()) {
|
|
68
|
+
const lib = resolveLibTerminalConfig(root) || {}
|
|
69
|
+
const sb = readJson(resolve(root, 'storyboard.config.json')) || {}
|
|
70
|
+
const userTerminal = readJson(resolve(root, 'terminal.config.json')) || {}
|
|
71
|
+
|
|
72
|
+
const sbCanvas = sb.canvas || {}
|
|
73
|
+
const sbLayer = {
|
|
74
|
+
...(sbCanvas.terminal ? { terminal: sbCanvas.terminal } : {}),
|
|
75
|
+
...(sbCanvas.agents ? { agents: sbCanvas.agents } : {}),
|
|
76
|
+
...(sbCanvas.showAgentsInAddMenu !== undefined
|
|
77
|
+
? { showAgentsInAddMenu: sbCanvas.showAgentsInAddMenu }
|
|
78
|
+
: {}),
|
|
79
|
+
...(sb.hotPool ? { hotPool: sb.hotPool } : {}),
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const merged = deepMerge(deepMerge(lib, sbLayer), userTerminal)
|
|
83
|
+
return {
|
|
84
|
+
terminal: merged.terminal || {},
|
|
85
|
+
agents: merged.agents || {},
|
|
86
|
+
showAgentsInAddMenu: merged.showAgentsInAddMenu,
|
|
87
|
+
hotPool: merged.hotPool || {},
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Convenience: just the agents map.
|
|
93
|
+
*/
|
|
94
|
+
export function readAgentsConfig(root = process.cwd()) {
|
|
95
|
+
return readTerminalConfigMerged(root).agents
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convenience: just the terminal-widget settings.
|
|
100
|
+
*/
|
|
101
|
+
export function readTerminalSettings(root = process.cwd()) {
|
|
102
|
+
return readTerminalConfigMerged(root).terminal
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Convenience: just the hotPool config.
|
|
107
|
+
*/
|
|
108
|
+
export function readHotPoolConfig(root = process.cwd()) {
|
|
109
|
+
return readTerminalConfigMerged(root).hotPool
|
|
110
|
+
}
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* Scale-down: After cooldown minutes with no acquisitions, the pool scales
|
|
26
26
|
* back to pool_size by killing excess warm sessions.
|
|
27
27
|
*
|
|
28
|
-
* ## Configuration (storyboard.config.json → hotPool)
|
|
28
|
+
* ## Configuration (terminal.config.json → hotPool, or storyboard.config.json → hotPool for legacy back-compat)
|
|
29
29
|
*
|
|
30
30
|
* hotPool.enabled — enable/disable all pools (default: true)
|
|
31
31
|
* hotPool.verbose — log to Vite terminal (default: false)
|
|
@@ -52,6 +52,7 @@ import { markCanvasWrite, unmarkCanvasWrite } from './writeGuard.js'
|
|
|
52
52
|
import { devLog } from '../logger/devLogger.js'
|
|
53
53
|
import widgetsConfig from '../../../widgets.config.json' with { type: 'json' }
|
|
54
54
|
import { listHubRoles, getDefaultRoleId } from './hub-roles.js'
|
|
55
|
+
import { readAgentsConfig } from './configReader.js'
|
|
55
56
|
|
|
56
57
|
/**
|
|
57
58
|
* Read the prompt widget's execution config from widgets.config.json.
|
|
@@ -713,9 +714,7 @@ export function createCanvasHandler(ctx) {
|
|
|
713
714
|
// For agent widgets, resolve startupCommand from canvas.agents config if not provided
|
|
714
715
|
if (type === 'agent' && props.agentId && !props.startupCommand) {
|
|
715
716
|
try {
|
|
716
|
-
const
|
|
717
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
718
|
-
const agentCfg = config?.canvas?.agents?.[props.agentId]
|
|
717
|
+
const agentCfg = readAgentsConfig(root)?.[props.agentId]
|
|
719
718
|
if (agentCfg?.startupCommand) {
|
|
720
719
|
props.startupCommand = agentCfg.startupCommand
|
|
721
720
|
}
|
|
@@ -725,9 +724,7 @@ export function createCanvasHandler(ctx) {
|
|
|
725
724
|
// For agent widgets without agentId, default to the first canvas.agents entry
|
|
726
725
|
if (type === 'agent' && !props.agentId) {
|
|
727
726
|
try {
|
|
728
|
-
const
|
|
729
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
730
|
-
const agents = config?.canvas?.agents || {}
|
|
727
|
+
const agents = readAgentsConfig(root) || {}
|
|
731
728
|
const defaultEntry = Object.entries(agents).find(([, cfg]) => cfg.default) || Object.entries(agents)[0]
|
|
732
729
|
if (defaultEntry) {
|
|
733
730
|
const [id, cfg] = defaultEntry
|
|
@@ -3112,9 +3109,7 @@ export function Default() {
|
|
|
3112
3109
|
// Resolve agent config from storyboard.config.json
|
|
3113
3110
|
let agentConfig = null
|
|
3114
3111
|
try {
|
|
3115
|
-
const
|
|
3116
|
-
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'))
|
|
3117
|
-
const agents = config?.canvas?.agents || {}
|
|
3112
|
+
const agents = readAgentsConfig(root) || {}
|
|
3118
3113
|
if (agentId && agents[agentId]) {
|
|
3119
3114
|
agentConfig = agents[agentId]
|
|
3120
3115
|
} else {
|
|
@@ -158,15 +158,11 @@ function cleanEnv() {
|
|
|
158
158
|
return filtered
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
import { readTerminalSettings, readAgentsConfig } from './configReader.js'
|
|
162
|
+
|
|
163
|
+
/** Read terminal-widget settings (merged from lib defaults + storyboard.config.json + terminal.config.json). */
|
|
162
164
|
function readTerminalConfig() {
|
|
163
|
-
|
|
164
|
-
const raw = readFileSync(resolve(process.cwd(), 'storyboard.config.json'), 'utf8')
|
|
165
|
-
const config = JSON.parse(raw)
|
|
166
|
-
return config?.canvas?.terminal ?? {}
|
|
167
|
-
} catch {
|
|
168
|
-
return {}
|
|
169
|
-
}
|
|
165
|
+
return readTerminalSettings()
|
|
170
166
|
}
|
|
171
167
|
|
|
172
168
|
/**
|
|
@@ -175,22 +171,19 @@ function readTerminalConfig() {
|
|
|
175
171
|
*/
|
|
176
172
|
function resolveAgentConfig(startupCommand) {
|
|
177
173
|
if (!startupCommand || startupCommand === 'shell') return null
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (!
|
|
182
|
-
for
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
) {
|
|
190
|
-
return { id, cfg }
|
|
191
|
-
}
|
|
174
|
+
const agentsConfig = readAgentsConfig()
|
|
175
|
+
if (!agentsConfig || typeof agentsConfig !== 'object') return null
|
|
176
|
+
for (const [id, cfg] of Object.entries(agentsConfig)) {
|
|
177
|
+
if (!cfg?.startupCommand) continue
|
|
178
|
+
// Prefer exact/prefix match for deterministic routing; keep binary fallback for backwards compat.
|
|
179
|
+
if (
|
|
180
|
+
startupCommand === cfg.startupCommand
|
|
181
|
+
|| startupCommand.startsWith(`${cfg.startupCommand} `)
|
|
182
|
+
|| startupCommand.startsWith(cfg.startupCommand.split(' ')[0])
|
|
183
|
+
) {
|
|
184
|
+
return { id, cfg }
|
|
192
185
|
}
|
|
193
|
-
}
|
|
186
|
+
}
|
|
194
187
|
return null
|
|
195
188
|
}
|
|
196
189
|
|
|
@@ -1107,8 +1100,7 @@ function handleConnection(ws, widgetId, canvasId, prettyName, widgetStartupComma
|
|
|
1107
1100
|
|
|
1108
1101
|
// Agent shorthand scripts (copilot, claude, codex, etc.)
|
|
1109
1102
|
try {
|
|
1110
|
-
const
|
|
1111
|
-
const agentsConfig = JSON.parse(raw)?.canvas?.agents
|
|
1103
|
+
const agentsConfig = readAgentsConfig()
|
|
1112
1104
|
if (agentsConfig && typeof agentsConfig === 'object') {
|
|
1113
1105
|
for (const [id, cfg] of Object.entries(agentsConfig)) {
|
|
1114
1106
|
if (!cfg.startupCommand) continue
|
|
@@ -1138,8 +1130,7 @@ function handleConnection(ws, widgetId, canvasId, prettyName, widgetStartupComma
|
|
|
1138
1130
|
`start() { if [ $# -eq 0 ]; then ${welcomeBase}; else ${welcomeBase} --startup "$*"; fi; }`,
|
|
1139
1131
|
]
|
|
1140
1132
|
try {
|
|
1141
|
-
const
|
|
1142
|
-
const agentsConfig = JSON.parse(raw)?.canvas?.agents
|
|
1133
|
+
const agentsConfig = readAgentsConfig()
|
|
1143
1134
|
if (agentsConfig && typeof agentsConfig === 'object') {
|
|
1144
1135
|
for (const [id, cfg] of Object.entries(agentsConfig)) {
|
|
1145
1136
|
if (!cfg.startupCommand) continue
|
|
@@ -1246,17 +1237,23 @@ function handleConnection(ws, widgetId, canvasId, prettyName, widgetStartupComma
|
|
|
1246
1237
|
const binDir = join(cwd, '.storyboard', 'terminals', 'bin')
|
|
1247
1238
|
envParts.push(`export PATH="${binDir}:$PATH"`)
|
|
1248
1239
|
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1251
|
-
//
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
}
|
|
1240
|
+
// Write env exports to a per-widget shell script and source it via a
|
|
1241
|
+
// short send-keys. Avoids tmux send-keys -l truncation when the env
|
|
1242
|
+
// soup (especially PATH expansion) gets large — observed: a 4 KB+
|
|
1243
|
+
// chain stops mid-line on macOS, the agent command never runs.
|
|
1244
|
+
const envScriptDir = join(cwd, '.storyboard', 'terminals')
|
|
1245
|
+
try { mkdirSync(envScriptDir, { recursive: true }) } catch { /* empty */ }
|
|
1246
|
+
const envScriptPath = join(envScriptDir, `${widgetId}.env.sh`)
|
|
1247
|
+
try {
|
|
1248
|
+
writeFileSync(envScriptPath, envParts.join('\n') + '\n')
|
|
1249
|
+
} catch { /* empty */ }
|
|
1250
|
+
const envSourceCmd = startupCommand
|
|
1251
|
+
? `clear && source ${JSON.stringify(envScriptPath)} && clear`
|
|
1252
|
+
: `source ${JSON.stringify(envScriptPath)}`
|
|
1256
1253
|
|
|
1257
1254
|
setTimeout(() => {
|
|
1258
1255
|
try {
|
|
1259
|
-
execSync(`tmux send-keys -t "${tmuxName}" -l ${JSON.stringify(
|
|
1256
|
+
execSync(`tmux send-keys -t "${tmuxName}" -l ${JSON.stringify(envSourceCmd)}`, { stdio: 'ignore' })
|
|
1260
1257
|
execSync(`tmux send-keys -t "${tmuxName}" Enter`, { stdio: 'ignore' })
|
|
1261
1258
|
} catch { /* empty */ }
|
|
1262
1259
|
}, 300)
|
package/src/core/cli/setup.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as p from '@clack/prompts'
|
|
13
13
|
import { existsSync, writeFileSync, readFileSync, mkdirSync, readdirSync, symlinkSync } from 'fs'
|
|
14
14
|
import path from 'path'
|
|
15
|
-
import { execSync } from 'child_process'
|
|
15
|
+
import { execSync, spawn } from 'child_process'
|
|
16
16
|
import { gettingStartedLines, dim, magenta, bold, yellow, green } from './intro.js'
|
|
17
17
|
import { parseFlags } from './flags.js'
|
|
18
18
|
|
|
@@ -43,7 +43,8 @@ if (flags.nuke) {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Run a potentially slow task with a spinner that only appears after 500ms.
|
|
46
|
-
*
|
|
46
|
+
* IMPORTANT: `fn` must be async (don't use execSync — it blocks the event loop
|
|
47
|
+
* and prevents the spinner from animating).
|
|
47
48
|
*/
|
|
48
49
|
async function withSpin(label, doneMsg, fn) {
|
|
49
50
|
const spin = p.spinner()
|
|
@@ -59,6 +60,54 @@ async function withSpin(label, doneMsg, fn) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Async command runner — does NOT block the event loop, so spinners animate.
|
|
65
|
+
*/
|
|
66
|
+
function runAsync(cmd, args = [], opts = {}) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const child = spawn(cmd, args, { stdio: 'ignore', ...opts })
|
|
69
|
+
child.on('error', reject)
|
|
70
|
+
child.on('exit', (code) => {
|
|
71
|
+
if (code === 0) resolve()
|
|
72
|
+
else reject(new Error(`${cmd} exited with code ${code}`))
|
|
73
|
+
})
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Install a brew package with an animated spinner.
|
|
79
|
+
*/
|
|
80
|
+
async function brewInstall(pkg, label) {
|
|
81
|
+
const spin = p.spinner()
|
|
82
|
+
spin.start(`Installing ${label}`)
|
|
83
|
+
try {
|
|
84
|
+
await runAsync('brew', ['install', pkg])
|
|
85
|
+
spin.stop(`${label} installed`)
|
|
86
|
+
return true
|
|
87
|
+
} catch {
|
|
88
|
+
spin.stop(`Failed to install ${label}`)
|
|
89
|
+
p.log.warning(`Install manually: brew install ${pkg}`)
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Quick network probe — alerts the user if GitHub looks unreachable.
|
|
96
|
+
* We don't block on failure; downstream installers will surface their own errors.
|
|
97
|
+
*/
|
|
98
|
+
async function checkNetwork() {
|
|
99
|
+
const http = await import('node:https')
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const req = http.request('https://api.github.com', { method: 'HEAD', timeout: 4000 }, (res) => {
|
|
102
|
+
resolve(res.statusCode != null && res.statusCode < 500)
|
|
103
|
+
res.resume()
|
|
104
|
+
})
|
|
105
|
+
req.on('error', () => resolve(false))
|
|
106
|
+
req.on('timeout', () => { req.destroy(); resolve(false) })
|
|
107
|
+
req.end()
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
62
111
|
function mascot() {
|
|
63
112
|
const d = dim('·')
|
|
64
113
|
const f = magenta
|
|
@@ -87,13 +136,20 @@ function isInstalled(cmd) {
|
|
|
87
136
|
|
|
88
137
|
p.intro('storyboard setup')
|
|
89
138
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
139
|
+
// 0. Network probe — alert if GitHub is unreachable; don't block.
|
|
140
|
+
{
|
|
141
|
+
const online = await checkNetwork()
|
|
142
|
+
if (!online) {
|
|
143
|
+
p.log.warning('Network looks offline — installers/downloads may fail')
|
|
144
|
+
p.log.info(dim(' Tried HEAD https://api.github.com; check VPN/proxy/DNS'))
|
|
145
|
+
} else {
|
|
146
|
+
p.log.success('Network reachable')
|
|
147
|
+
}
|
|
95
148
|
}
|
|
96
149
|
|
|
150
|
+
// Node is assumed to be present (you already ran `npx storyboard setup`).
|
|
151
|
+
// node_modules will be installed at the end if missing.
|
|
152
|
+
|
|
97
153
|
// 2. Homebrew
|
|
98
154
|
let hasBrew = isInstalled('brew')
|
|
99
155
|
if (!hasBrew) {
|
|
@@ -121,35 +177,43 @@ if (hasBrew) {
|
|
|
121
177
|
if (isInstalled('git')) {
|
|
122
178
|
p.log.success('Git installed')
|
|
123
179
|
} else {
|
|
124
|
-
|
|
125
|
-
gitSpin.start('Installing Git')
|
|
126
|
-
try {
|
|
127
|
-
run('brew install git')
|
|
128
|
-
gitSpin.stop('Git installed')
|
|
129
|
-
} catch {
|
|
130
|
-
gitSpin.stop('Failed to install Git')
|
|
131
|
-
p.log.warning('Install manually: brew install git')
|
|
132
|
-
}
|
|
180
|
+
await brewInstall('git', 'Git')
|
|
133
181
|
}
|
|
134
182
|
}
|
|
135
183
|
|
|
136
184
|
// 4. Caddy is no longer used. Worktrees run their own Vite directly on
|
|
137
|
-
// `http://localhost:<port>/storyboard/`.
|
|
138
|
-
// skips the previous Caddy install + start steps.
|
|
185
|
+
// `http://localhost:<port>/storyboard/`.
|
|
139
186
|
|
|
140
187
|
if (hasBrew) {
|
|
141
|
-
// 5. GitHub CLI
|
|
188
|
+
// 5. GitHub CLI — required for `gh auth`, `gh pr`, `gh issue` in agents.
|
|
189
|
+
let ghNewlyInstalled = false
|
|
142
190
|
if (isInstalled('gh')) {
|
|
143
191
|
p.log.success('GitHub CLI installed')
|
|
144
192
|
} else {
|
|
145
|
-
|
|
146
|
-
|
|
193
|
+
ghNewlyInstalled = await brewInstall('gh', 'GitHub CLI')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 5a. tmux (required for headless agent sessions)
|
|
197
|
+
if (isInstalled('tmux')) {
|
|
198
|
+
p.log.success('tmux installed')
|
|
199
|
+
} else {
|
|
200
|
+
await brewInstall('tmux', 'tmux')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// 5b. Surface gh auth status. Even if gh was already installed, we should
|
|
204
|
+
// prompt the user to log in — agents that shell out to `gh` will fail
|
|
205
|
+
// silently otherwise.
|
|
206
|
+
if (isInstalled('gh')) {
|
|
207
|
+
let authed = false
|
|
147
208
|
try {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
} catch {
|
|
151
|
-
|
|
152
|
-
p.log.
|
|
209
|
+
execSync('gh auth status', { stdio: 'ignore' })
|
|
210
|
+
authed = true
|
|
211
|
+
} catch { /* not authed */ }
|
|
212
|
+
if (authed) {
|
|
213
|
+
p.log.success('GitHub CLI authenticated')
|
|
214
|
+
} else {
|
|
215
|
+
p.log.warning(ghNewlyInstalled ? 'GitHub CLI installed but not logged in' : 'GitHub CLI is not logged in')
|
|
216
|
+
p.log.info(` Run ${yellow('gh auth login')} to authenticate`)
|
|
153
217
|
}
|
|
154
218
|
}
|
|
155
219
|
}
|
|
@@ -195,24 +259,37 @@ if (isInstalled('code')) {
|
|
|
195
259
|
}
|
|
196
260
|
}
|
|
197
261
|
|
|
198
|
-
// 6a. Copilot CLI
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
262
|
+
// 6a. Copilot CLI — install via the official script (no homebrew dependency).
|
|
263
|
+
// curl ships with macOS and all major Linux distros.
|
|
264
|
+
// Auth is separate from `gh`: copilot has its own credential store.
|
|
265
|
+
{
|
|
266
|
+
let copilotNewlyInstalled = false
|
|
267
|
+
if (isInstalled('copilot')) {
|
|
268
|
+
p.log.success('Copilot CLI installed')
|
|
269
|
+
} else {
|
|
270
|
+
const spin = p.spinner()
|
|
271
|
+
spin.start('Installing Copilot CLI')
|
|
272
|
+
try {
|
|
273
|
+
await runAsync('bash', ['-c', 'curl -fsSL https://gh.io/copilot-install | bash'])
|
|
274
|
+
// Install script drops the binary in ~/.local/bin when run as
|
|
275
|
+
// non-root. Make sure the current process can find it for the
|
|
276
|
+
// remainder of setup, and warn the user to add it to their shell rc.
|
|
277
|
+
const localBin = `${process.env.HOME}/.local/bin`
|
|
278
|
+
if (!process.env.PATH.includes(localBin)) {
|
|
279
|
+
process.env.PATH = `${localBin}:${process.env.PATH}`
|
|
280
|
+
}
|
|
281
|
+
spin.stop('Copilot CLI installed')
|
|
282
|
+
copilotNewlyInstalled = true
|
|
283
|
+
if (!isInstalled('copilot')) {
|
|
284
|
+
p.log.warning(`copilot not on PATH — add ${yellow('export PATH="$HOME/.local/bin:$PATH"')} to your shell rc`)
|
|
285
|
+
}
|
|
286
|
+
} catch {
|
|
287
|
+
spin.stop('Failed to install Copilot CLI')
|
|
288
|
+
p.log.warning('Install manually: curl -fsSL https://gh.io/copilot-install | bash')
|
|
210
289
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
copilotSpin.stop('Failed to install Copilot CLI')
|
|
215
|
-
p.log.warning('Install manually: curl -fsSL https://gh.io/copilot-install | bash')
|
|
290
|
+
}
|
|
291
|
+
if (copilotNewlyInstalled || isInstalled('copilot')) {
|
|
292
|
+
p.log.info(` Auth is separate from gh — run ${yellow('copilot')} then ${yellow('/login')}`)
|
|
216
293
|
}
|
|
217
294
|
}
|
|
218
295
|
|
|
@@ -352,14 +429,14 @@ if (isInstalled('copilot')) {
|
|
|
352
429
|
|
|
353
430
|
// 10. Install / sync dependencies
|
|
354
431
|
{
|
|
432
|
+
const installSpin = p.spinner()
|
|
433
|
+
installSpin.start('Installing dependencies')
|
|
355
434
|
try {
|
|
356
|
-
await
|
|
357
|
-
|
|
358
|
-
'Dependencies installed',
|
|
359
|
-
() => { run('npm install', { stdio: 'ignore' }) }
|
|
360
|
-
)
|
|
435
|
+
await runAsync('npm', ['install'])
|
|
436
|
+
installSpin.stop('Dependencies installed')
|
|
361
437
|
} catch {
|
|
362
|
-
|
|
438
|
+
installSpin.stop('npm install failed')
|
|
439
|
+
p.log.warning('Run it manually to see details:')
|
|
363
440
|
p.log.info(` ${dim('npm install')}`)
|
|
364
441
|
}
|
|
365
442
|
}
|
|
@@ -17,11 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import * as p from '@clack/prompts'
|
|
19
19
|
import { execSync, spawn } from 'node:child_process'
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
20
|
+
import { existsSync } from 'node:fs'
|
|
21
|
+
import { join } from 'node:path'
|
|
22
22
|
import { parseFlags } from './flags.js'
|
|
23
23
|
import { dim, bold } from './intro.js'
|
|
24
24
|
import { takePendingMessages } from '../canvas/terminal-config.js'
|
|
25
|
+
import { readAgentsConfig } from '../canvas/configReader.js'
|
|
25
26
|
|
|
26
27
|
const blue = (s) => `\x1b[34m${s}\x1b[0m`
|
|
27
28
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
@@ -76,14 +77,12 @@ function agentEnv() {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
/**
|
|
79
|
-
* Read agents config
|
|
80
|
+
* Read agents config (lib defaults + storyboard.config.json + terminal.config.json merged).
|
|
80
81
|
* Returns an array of { id, label, startupCommand, resumeCommand } entries.
|
|
81
82
|
*/
|
|
82
83
|
function loadAgents() {
|
|
83
84
|
try {
|
|
84
|
-
const
|
|
85
|
-
const config = JSON.parse(raw)
|
|
86
|
-
const agents = config?.canvas?.agents
|
|
85
|
+
const agents = readAgentsConfig()
|
|
87
86
|
if (!agents || typeof agents !== 'object') return []
|
|
88
87
|
return Object.entries(agents).map(([id, cfg]) => ({
|
|
89
88
|
id,
|
|
@@ -19,6 +19,7 @@ import { serverFeatures as workshopFeatures } from '../workshop/features/registr
|
|
|
19
19
|
import { docsHandler, collectFiles } from './docs-handler.js'
|
|
20
20
|
import { createCanvasHandler } from '../canvas/server.js'
|
|
21
21
|
import { setupSelectedWidgets } from '../canvas/selectedWidgets.js'
|
|
22
|
+
import { readAgentsConfig, readHotPoolConfig } from '../canvas/configReader.js'
|
|
22
23
|
import { HotPoolManager } from '../canvas/hot-pool.js'
|
|
23
24
|
import { createAutosyncHandler } from '../autosync/server.js'
|
|
24
25
|
import { setupTerminalServer } from '../canvas/terminal-server.js'
|
|
@@ -279,8 +280,8 @@ export default function storyboardServer() {
|
|
|
279
280
|
routeHandlers.set('docs', docsHandler({ root, sendJson: sendJsonLogged }))
|
|
280
281
|
|
|
281
282
|
// Create shared hot pool manager (per-type pre-warmed sessions)
|
|
282
|
-
const hotPoolConfig =
|
|
283
|
-
const agentsConfig =
|
|
283
|
+
const hotPoolConfig = readHotPoolConfig(root)
|
|
284
|
+
const agentsConfig = readAgentsConfig(root)
|
|
284
285
|
const wsSend = server.ws.send.bind(server.ws)
|
|
285
286
|
const hotPool = new HotPoolManager({ root, config: hotPoolConfig, agentsConfig, wsSend })
|
|
286
287
|
hotPool.start().catch((err) => {
|
package/terminal.config.json
CHANGED
|
@@ -44,5 +44,20 @@
|
|
|
44
44
|
"resizable": true
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
|
-
"showAgentsInAddMenu": false
|
|
47
|
+
"showAgentsInAddMenu": false,
|
|
48
|
+
"hotPool": {
|
|
49
|
+
"enabled": true,
|
|
50
|
+
"verbose": false,
|
|
51
|
+
"default_pool_size": 1,
|
|
52
|
+
"default_max_pool_size": 3,
|
|
53
|
+
"load_balancer": true,
|
|
54
|
+
"load_balancer_cooldown_mins": 10,
|
|
55
|
+
"pools": {
|
|
56
|
+
"terminal": { "pool_size": 1 },
|
|
57
|
+
"copilot": { "pool_size": 1, "webgl_ready_slots": 1 },
|
|
58
|
+
"claude": { "pool_size": 1, "webgl_ready_slots": 1 },
|
|
59
|
+
"codex": { "pool_size": 0 },
|
|
60
|
+
"prompt": { "pool_size": 1, "webgl_ready_slots": 1 }
|
|
61
|
+
}
|
|
62
|
+
}
|
|
48
63
|
}
|