@promus/cli 0.24.17
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/README.md +18 -0
- package/bin/promus +33 -0
- package/package.json +51 -0
- package/src/commands/_agents.ts +14 -0
- package/src/commands/_inft-ref.ts +43 -0
- package/src/commands/_unlock.ts +74 -0
- package/src/commands/admin-autotopup-tick.ts +73 -0
- package/src/commands/admin.test.ts +34 -0
- package/src/commands/admin.ts +32 -0
- package/src/commands/balance.test.ts +10 -0
- package/src/commands/balance.ts +112 -0
- package/src/commands/chat-sandbox.tsx +520 -0
- package/src/commands/chat-telegram.ts +398 -0
- package/src/commands/chat.tsx +1916 -0
- package/src/commands/deploy.ts +204 -0
- package/src/commands/drain.ts +90 -0
- package/src/commands/gateway-logs.ts +47 -0
- package/src/commands/gateway-run.ts +54 -0
- package/src/commands/gateway-start.ts +218 -0
- package/src/commands/gateway-status.ts +88 -0
- package/src/commands/gateway-stop.ts +133 -0
- package/src/commands/gateway.ts +101 -0
- package/src/commands/init/cost.test.ts +169 -0
- package/src/commands/init/cost.ts +154 -0
- package/src/commands/init/funding-gate.ts +67 -0
- package/src/commands/init/model-picker.ts +81 -0
- package/src/commands/init/operator-picker.ts +263 -0
- package/src/commands/init/resume.ts +136 -0
- package/src/commands/init/sandbox-provision.test.ts +497 -0
- package/src/commands/init/sandbox-provision.ts +1177 -0
- package/src/commands/init/telegram-step.ts +229 -0
- package/src/commands/init/wizard-state.ts +95 -0
- package/src/commands/init.ts +612 -0
- package/src/commands/inspect.ts +529 -0
- package/src/commands/ledger.ts +176 -0
- package/src/commands/logs.ts +86 -0
- package/src/commands/migrate-keystore.ts +155 -0
- package/src/commands/model.ts +48 -0
- package/src/commands/pairing-approve.ts +114 -0
- package/src/commands/pairing-clear.ts +42 -0
- package/src/commands/pairing-list.ts +58 -0
- package/src/commands/pairing-revoke.ts +52 -0
- package/src/commands/pairing.test.ts +88 -0
- package/src/commands/pairing.ts +81 -0
- package/src/commands/pause.ts +99 -0
- package/src/commands/profile.ts +184 -0
- package/src/commands/restore.ts +221 -0
- package/src/commands/resume.ts +181 -0
- package/src/commands/status.ts +119 -0
- package/src/commands/sync.ts +147 -0
- package/src/commands/telegram-remove.ts +65 -0
- package/src/commands/telegram-setup.ts +74 -0
- package/src/commands/telegram-status.ts +89 -0
- package/src/commands/telegram.test.ts +50 -0
- package/src/commands/telegram.ts +44 -0
- package/src/commands/topup.ts +303 -0
- package/src/commands/transfer.test.ts +111 -0
- package/src/commands/transfer.ts +520 -0
- package/src/commands/upgrade.test.ts +137 -0
- package/src/commands/upgrade.ts +690 -0
- package/src/config/load.ts +35 -0
- package/src/config/render.test.ts +96 -0
- package/src/config/render.ts +110 -0
- package/src/index.ts +378 -0
- package/src/sandbox/client.test.ts +251 -0
- package/src/sandbox/client.ts +424 -0
- package/src/ui/app.tsx +677 -0
- package/src/ui/approval-summary.test.ts +154 -0
- package/src/ui/approval-summary.ts +34 -0
- package/src/ui/markdown-parse.ts +219 -0
- package/src/ui/markdown.test.ts +146 -0
- package/src/ui/markdown.tsx +37 -0
- package/src/ui/state.test.ts +74 -0
- package/src/ui/state.ts +198 -0
- package/src/util/bootstrap-mode.test.ts +40 -0
- package/src/util/bootstrap-mode.ts +25 -0
- package/src/util/bootstrap-progress-box.test.ts +190 -0
- package/src/util/bootstrap-progress-box.ts +378 -0
- package/src/util/brain-secrets.ts +96 -0
- package/src/util/cli-version.ts +28 -0
- package/src/util/format.test.ts +16 -0
- package/src/util/format.ts +11 -0
- package/src/util/gateway-spawn.test.ts +86 -0
- package/src/util/gateway-spawn.ts +128 -0
- package/src/util/gateway-version.test.ts +113 -0
- package/src/util/gateway-version.ts +154 -0
- package/src/util/github-releases.test.ts +116 -0
- package/src/util/github-releases.ts +79 -0
- package/src/util/profile-key.test.ts +60 -0
- package/src/util/profile-key.ts +25 -0
- package/src/util/ref-resolver.test.ts +77 -0
- package/src/util/ref-resolver.ts +55 -0
- package/src/util/silence-console.test.ts +53 -0
- package/src/util/silence-console.ts +40 -0
- package/src/util/telegram-secrets.test.ts +227 -0
- package/src/util/telegram-secrets.ts +223 -0
package/src/ui/state.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PermissionDecision,
|
|
3
|
+
PermissionMode,
|
|
4
|
+
PermissionRequest,
|
|
5
|
+
SlashCommand,
|
|
6
|
+
} from '@promus/core'
|
|
7
|
+
import { type JobEvent, isJobTerminalKind } from '@promus/plugin-comms'
|
|
8
|
+
import { createSignal } from 'solid-js'
|
|
9
|
+
|
|
10
|
+
export type TurnRole =
|
|
11
|
+
| 'user'
|
|
12
|
+
| 'assistant'
|
|
13
|
+
| 'system'
|
|
14
|
+
| 'tool-call'
|
|
15
|
+
| 'tool-result'
|
|
16
|
+
| 'inbox'
|
|
17
|
+
| 'market'
|
|
18
|
+
| 'inbox-tg'
|
|
19
|
+
| 'telegram-assistant'
|
|
20
|
+
|
|
21
|
+
export interface TurnRow {
|
|
22
|
+
id: string
|
|
23
|
+
role: TurnRole
|
|
24
|
+
text: string
|
|
25
|
+
// tool-call rows: tool name + formatted args (rendered as `name(args)`)
|
|
26
|
+
toolName?: string
|
|
27
|
+
args?: string
|
|
28
|
+
// tool-result rows: failure flag drives icon + color
|
|
29
|
+
failed?: boolean
|
|
30
|
+
// v0.21.2: drives the ↪ prefix so operators see the SAME logical fetch was
|
|
31
|
+
// escalated, not a fresh brain decision.
|
|
32
|
+
autoEscalated?: boolean
|
|
33
|
+
// True only for the first row in an "promus block" (assistant + tool-call rows
|
|
34
|
+
// that share the same speaker turn). Computed once at push time so the For
|
|
35
|
+
// loop renderer doesn't re-walk neighbors on every state mutation.
|
|
36
|
+
firstOfBlock?: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PendingApproval {
|
|
40
|
+
request: PermissionRequest
|
|
41
|
+
resolve: (decision: PermissionDecision) => void
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface CreateChatStateOpts {
|
|
45
|
+
initialSystem: string
|
|
46
|
+
identityLabel: string
|
|
47
|
+
approvalsMode: PermissionMode
|
|
48
|
+
// v0.24.4: true when the TUI talks to a local gateway daemon over a unix
|
|
49
|
+
// socket (`~/.promus/agents/<id>/gateway.sock`) instead of a remote Daytona
|
|
50
|
+
// sandbox endpoint. Drives statusbar copy (drops the "sandbox X" prefix on
|
|
51
|
+
// the system line) and hides the sandbox-billing balance segment (which is
|
|
52
|
+
// meaningless for local deploys — there is no billing reserve to surface).
|
|
53
|
+
// Defaults to false so existing call sites that don't pass it (i.e. nothing
|
|
54
|
+
// today, since both call sites set it explicitly) keep sandbox semantics.
|
|
55
|
+
isLocalGateway?: boolean
|
|
56
|
+
/** Native gas-token symbol for the agent's network ('ETH' / '0G'). */
|
|
57
|
+
currency?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createChatState(opts: CreateChatStateOpts) {
|
|
61
|
+
const [rows, setRows] = createSignal<TurnRow[]>([
|
|
62
|
+
{ id: 'sys-0', role: 'system', text: opts.initialSystem },
|
|
63
|
+
])
|
|
64
|
+
const [input, setInput] = createSignal('')
|
|
65
|
+
const [status, setStatus] = createSignal<'idle' | 'thinking' | 'error'>('idle')
|
|
66
|
+
const [usage, setUsage] = createSignal<{ total?: number; cached?: number } | null>(null)
|
|
67
|
+
const [pendingApproval, setPendingApproval] = createSignal<PendingApproval | null>(null)
|
|
68
|
+
const [approvalsMode, setApprovalsMode] = createSignal<PermissionMode>(opts.approvalsMode)
|
|
69
|
+
|
|
70
|
+
// 0G Compute ledger balance, in 0G. Refreshed at chat init and after each
|
|
71
|
+
// per-turn auto-sync. null = not yet fetched / fetch failed.
|
|
72
|
+
const [balance, setBalance] = createSignal<number | null>(null)
|
|
73
|
+
// Agent EOA balance, in 0G. Pays gas for chain writes (agent.message
|
|
74
|
+
// inbox.send, sync's updateSlots anchor). Typically starves before the
|
|
75
|
+
// compute ledger in long sessions (~0.001 0G/send at 4 gwei).
|
|
76
|
+
const [eoaBalance, setEoaBalance] = createSignal<number | null>(null)
|
|
77
|
+
// v0.22.0: 0G Sandbox billing reserve, in 0G. Sandbox-deployed agents only —
|
|
78
|
+
// local-mode TUI stays null and the statusline `<Show>` hides the segment.
|
|
79
|
+
// Auto-topup refills this when it dips below the configured threshold; the
|
|
80
|
+
// statusline mirror lets operators see the same balance without leaving TUI.
|
|
81
|
+
const [sandboxBalance, setSandboxBalance] = createSignal<number | null>(null)
|
|
82
|
+
// ms epoch when current turn started (status flipped to 'thinking'). The
|
|
83
|
+
// spinner row reads this and renders elapsed seconds. Cleared on idle.
|
|
84
|
+
const [turnStartedAt, setTurnStartedAt] = createSignal<number | null>(null)
|
|
85
|
+
|
|
86
|
+
// Phase 8: in-flight escrow jobs the agent is a party to (buyer or
|
|
87
|
+
// provider). Incremented on JobCreated, decremented once per terminal
|
|
88
|
+
// event per jobId. The contract emits both JobForceClosed AND JobSettled
|
|
89
|
+
// when force-closing a Done job (force-close routes through _settle), so
|
|
90
|
+
// we de-dup by jobId here to keep the counter honest.
|
|
91
|
+
const [activeJobCount, setActiveJobCount] = createSignal(0)
|
|
92
|
+
const terminatedJobs = new Set<string>()
|
|
93
|
+
const bumpActiveJobs = (e: JobEvent) => {
|
|
94
|
+
if (e.kind === 'created') {
|
|
95
|
+
setActiveJobCount(c => c + 1)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
if (!isJobTerminalKind(e.kind)) return
|
|
99
|
+
const id = e.jobId.toString()
|
|
100
|
+
if (terminatedJobs.has(id)) return
|
|
101
|
+
terminatedJobs.add(id)
|
|
102
|
+
setActiveJobCount(c => Math.max(0, c - 1))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Per-turn AbortController. Set when handleSubmit kicks off brain.infer;
|
|
106
|
+
// cleared (set to null) after the turn ends or is aborted. The keyboard
|
|
107
|
+
// handler reads it to wire Esc → abort.
|
|
108
|
+
const [activeAbort, setActiveAbort] = createSignal<AbortController | null>(null)
|
|
109
|
+
|
|
110
|
+
// v0.20.0: slash-command autocomplete popup state. `slashMatches` is the
|
|
111
|
+
// filtered list of commands matching the current input prefix; populated
|
|
112
|
+
// when input starts with `/`, cleared otherwise. `slashIndex` tracks the
|
|
113
|
+
// selected row inside `slashMatches`. Both reset to defaults on submit.
|
|
114
|
+
const [slashMatches, setSlashMatches] = createSignal<SlashCommand[]>([])
|
|
115
|
+
const [slashIndex, setSlashIndex] = createSignal(0)
|
|
116
|
+
|
|
117
|
+
// Status-change subscribers. Phase 12 telegram-dispatch hooks here so it
|
|
118
|
+
// can drain its queue when the brain returns to idle from a stdin turn.
|
|
119
|
+
type StatusListener = (next: 'idle' | 'thinking' | 'error') => void
|
|
120
|
+
const statusListeners = new Set<StatusListener>()
|
|
121
|
+
const onStatusChange = (cb: StatusListener): (() => void) => {
|
|
122
|
+
statusListeners.add(cb)
|
|
123
|
+
return () => statusListeners.delete(cb)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Wrap status setter so the turn-start timestamp tracks status changes
|
|
127
|
+
// automatically. Every code path that flips to 'thinking' starts the
|
|
128
|
+
// clock; every flip to idle/error stops it. Removes the burden from
|
|
129
|
+
// call sites.
|
|
130
|
+
const setStatusTracked: typeof setStatus = next => {
|
|
131
|
+
const prev = status()
|
|
132
|
+
const result = setStatus(next)
|
|
133
|
+
const after = status()
|
|
134
|
+
if (prev !== 'thinking' && after === 'thinking') setTurnStartedAt(Date.now())
|
|
135
|
+
else if (prev === 'thinking' && after !== 'thinking') setTurnStartedAt(null)
|
|
136
|
+
if (prev !== after) {
|
|
137
|
+
for (const cb of statusListeners) {
|
|
138
|
+
try {
|
|
139
|
+
cb(after)
|
|
140
|
+
} catch {
|
|
141
|
+
// listener errors must not break status updates
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return result
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let idCounter = 1
|
|
149
|
+
const nextId = () => `row-${idCounter++}`
|
|
150
|
+
|
|
151
|
+
const pushRow = (row: Omit<TurnRow, 'id' | 'firstOfBlock'>) => {
|
|
152
|
+
setRows(prev => {
|
|
153
|
+
const last = prev[prev.length - 1] ?? null
|
|
154
|
+
const isAssistantBlock = row.role === 'assistant' || row.role === 'tool-call'
|
|
155
|
+
const continuesBlock =
|
|
156
|
+
last?.role === 'assistant' || last?.role === 'tool-call' || last?.role === 'tool-result'
|
|
157
|
+
const firstOfBlock = isAssistantBlock && !continuesBlock
|
|
158
|
+
return [...prev, { ...row, id: nextId(), firstOfBlock }]
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
rows,
|
|
164
|
+
input,
|
|
165
|
+
status,
|
|
166
|
+
usage,
|
|
167
|
+
pendingApproval,
|
|
168
|
+
approvalsMode,
|
|
169
|
+
balance,
|
|
170
|
+
eoaBalance,
|
|
171
|
+
sandboxBalance,
|
|
172
|
+
turnStartedAt,
|
|
173
|
+
activeAbort,
|
|
174
|
+
activeJobCount,
|
|
175
|
+
slashMatches,
|
|
176
|
+
slashIndex,
|
|
177
|
+
setInput,
|
|
178
|
+
setStatus: setStatusTracked,
|
|
179
|
+
setUsage,
|
|
180
|
+
setPendingApproval,
|
|
181
|
+
setApprovalsMode,
|
|
182
|
+
setBalance,
|
|
183
|
+
setEoaBalance,
|
|
184
|
+
setSandboxBalance,
|
|
185
|
+
setTurnStartedAt,
|
|
186
|
+
setActiveAbort,
|
|
187
|
+
setSlashMatches,
|
|
188
|
+
setSlashIndex,
|
|
189
|
+
bumpActiveJobs,
|
|
190
|
+
pushRow,
|
|
191
|
+
onStatusChange,
|
|
192
|
+
identityLabel: opts.identityLabel,
|
|
193
|
+
isLocalGateway: opts.isLocalGateway ?? false,
|
|
194
|
+
currency: opts.currency ?? 'ETH',
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type ChatState = ReturnType<typeof createChatState>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { resolveBootstrapMode } from './bootstrap-mode'
|
|
3
|
+
|
|
4
|
+
describe('resolveBootstrapMode', () => {
|
|
5
|
+
it('defaults to npm when no env is set', () => {
|
|
6
|
+
expect(resolveBootstrapMode({})).toBe('npm')
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('respects explicit PROMUS_BOOTSTRAP_MODE=git', () => {
|
|
10
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'git' })).toBe('git')
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('respects explicit PROMUS_BOOTSTRAP_MODE=npm', () => {
|
|
14
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'npm' })).toBe('npm')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("auto-implies git when PROMUS_BOOTSTRAP_REF is set without explicit mode (preserves 'deploy main' workflow)", () => {
|
|
18
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_REF: 'main' })).toBe('git')
|
|
19
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_REF: 'abc1234' })).toBe('git')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('lets explicit MODE win over REF (e.g. NPM=v0.21.20 alongside REF=ignored)', () => {
|
|
23
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'npm', PROMUS_BOOTSTRAP_REF: 'main' })).toBe(
|
|
24
|
+
'npm',
|
|
25
|
+
)
|
|
26
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'git', PROMUS_BOOTSTRAP_REF: 'main' })).toBe(
|
|
27
|
+
'git',
|
|
28
|
+
)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('falls through to npm on garbage PROMUS_BOOTSTRAP_MODE (typos, uppercase, etc.)', () => {
|
|
32
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'GIT' })).toBe('npm')
|
|
33
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'Npm' })).toBe('npm')
|
|
34
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_MODE: 'docker' })).toBe('npm')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('treats empty-string PROMUS_BOOTSTRAP_REF as unset (does not flip to git)', () => {
|
|
38
|
+
expect(resolveBootstrapMode({ PROMUS_BOOTSTRAP_REF: '' })).toBe('npm')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { BootstrapMode } from '@promus/gateway'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the sandbox bootstrap mode from operator env.
|
|
5
|
+
*
|
|
6
|
+
* Default is `'npm'` since v0.21.20 (~10x faster cold start: `bun add -g
|
|
7
|
+
* promus@<ver>` finishes in ~30-60s vs ~5-8min for `git clone +
|
|
8
|
+
* bun install`). The npm path was shipped in v0.21.15 and lived as opt-in
|
|
9
|
+
* for several releases before this flip.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order:
|
|
12
|
+
* 1. `PROMUS_BOOTSTRAP_MODE=git|npm` — explicit operator override, wins.
|
|
13
|
+
* 2. `PROMUS_BOOTSTRAP_REF` set without explicit mode → 'git'. The REF env
|
|
14
|
+
* is a git-mode concept (branch tip / commit SHA); auto-implying git
|
|
15
|
+
* preserves the existing "deploy main", "deploy <sha>" dev workflows.
|
|
16
|
+
* 3. Otherwise → 'npm'.
|
|
17
|
+
*
|
|
18
|
+
* Callers pass `opts.mode` directly to bypass this resolver entirely.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveBootstrapMode(env: NodeJS.ProcessEnv = process.env): BootstrapMode {
|
|
21
|
+
if (env.PROMUS_BOOTSTRAP_MODE === 'git') return 'git'
|
|
22
|
+
if (env.PROMUS_BOOTSTRAP_MODE === 'npm') return 'npm'
|
|
23
|
+
if (env.PROMUS_BOOTSTRAP_REF) return 'git'
|
|
24
|
+
return 'npm'
|
|
25
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { Writable } from 'node:stream'
|
|
3
|
+
import {
|
|
4
|
+
BootstrapProgressBox,
|
|
5
|
+
__testing,
|
|
6
|
+
mapBootstrapMarkerToStage,
|
|
7
|
+
} from './bootstrap-progress-box'
|
|
8
|
+
|
|
9
|
+
class CapturingStream extends Writable {
|
|
10
|
+
chunks: string[] = []
|
|
11
|
+
isTTY: boolean
|
|
12
|
+
constructor(isTTY: boolean) {
|
|
13
|
+
super()
|
|
14
|
+
this.isTTY = isTTY
|
|
15
|
+
}
|
|
16
|
+
override _write(chunk: Buffer | string, _enc: string, cb: () => void): void {
|
|
17
|
+
this.chunks.push(chunk.toString())
|
|
18
|
+
cb()
|
|
19
|
+
}
|
|
20
|
+
combined(): string {
|
|
21
|
+
return this.chunks.join('')
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('mapBootstrapMarkerToStage', () => {
|
|
26
|
+
test('maps every gateway bootstrap.ts marker', () => {
|
|
27
|
+
expect(mapBootstrapMarkerToStage('updating package index')).toBe('apt-update')
|
|
28
|
+
expect(
|
|
29
|
+
mapBootstrapMarkerToStage('installing system deps (build-essential, curl, git, xvfb)'),
|
|
30
|
+
).toBe('system-deps')
|
|
31
|
+
expect(mapBootstrapMarkerToStage('installing bun runtime')).toBe('bun-install')
|
|
32
|
+
expect(mapBootstrapMarkerToStage('installing promus (0.24.7)')).toBe('promus-install')
|
|
33
|
+
expect(mapBootstrapMarkerToStage('installing promus (git main)')).toBe('promus-install')
|
|
34
|
+
expect(mapBootstrapMarkerToStage('installing chrome for browser tools')).toBe('browser-deps')
|
|
35
|
+
expect(mapBootstrapMarkerToStage('starting harness daemon')).toBe('harness-spawn')
|
|
36
|
+
expect(mapBootstrapMarkerToStage('harness ready')).toBe('harness-spawn')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('returns null for unknown markers', () => {
|
|
40
|
+
expect(mapBootstrapMarkerToStage('something else entirely')).toBeNull()
|
|
41
|
+
expect(mapBootstrapMarkerToStage('')).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('BootstrapProgressBox TTY mode', () => {
|
|
46
|
+
test('start renders the initial frame with all stages pending', () => {
|
|
47
|
+
const out = new CapturingStream(true)
|
|
48
|
+
const box = new BootstrapProgressBox({ out })
|
|
49
|
+
box.start()
|
|
50
|
+
const combined = out.combined()
|
|
51
|
+
expect(combined).toContain('bootstrap progress')
|
|
52
|
+
expect(combined).toContain('launchScript uploaded to Daytona')
|
|
53
|
+
expect(combined).toContain('apt update')
|
|
54
|
+
expect(combined).toContain('/healthz Ready')
|
|
55
|
+
expect(combined).toContain('╰')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('markStage running uses spinner glyph; done uses checkmark', () => {
|
|
59
|
+
const out = new CapturingStream(true)
|
|
60
|
+
const box = new BootstrapProgressBox({ out })
|
|
61
|
+
box.start()
|
|
62
|
+
box.markStage('launch-upload', 'running')
|
|
63
|
+
const afterRunning = out.combined()
|
|
64
|
+
const runningGlyph = __testing.SPINNER_FRAMES.find(g => afterRunning.includes(g))
|
|
65
|
+
expect(runningGlyph).toBeDefined()
|
|
66
|
+
|
|
67
|
+
box.markStage('launch-upload', 'done')
|
|
68
|
+
expect(out.combined()).toContain('✓')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('markStage running auto-completes prior pending stages (handles skipped bun-install)', () => {
|
|
72
|
+
const out = new CapturingStream(true)
|
|
73
|
+
const box = new BootstrapProgressBox({ out })
|
|
74
|
+
box.start()
|
|
75
|
+
box.markStage('apt-update', 'running')
|
|
76
|
+
box.markStage('system-deps', 'running')
|
|
77
|
+
// Skip bun-install — bun already installed
|
|
78
|
+
box.markStage('promus-install', 'running')
|
|
79
|
+
const checkmarks = (out.combined().match(/✓/g) ?? []).length
|
|
80
|
+
// Stages auto-completed: launch-upload (skipped), apt-update, system-deps, bun-install (skipped)
|
|
81
|
+
expect(checkmarks).toBeGreaterThanOrEqual(4)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('stop finalizes any running stage as done and appends a newline', () => {
|
|
85
|
+
const out = new CapturingStream(true)
|
|
86
|
+
const box = new BootstrapProgressBox({ out })
|
|
87
|
+
box.start()
|
|
88
|
+
box.markStage('healthz-ready', 'running')
|
|
89
|
+
box.stop()
|
|
90
|
+
const combined = out.combined()
|
|
91
|
+
const checkmarks = (combined.match(/✓/g) ?? []).length
|
|
92
|
+
expect(checkmarks).toBeGreaterThanOrEqual(1)
|
|
93
|
+
expect(combined.endsWith('\n')).toBe(true)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('fail marks running stage as failed (✗) and leaves later pending stages alone', () => {
|
|
97
|
+
const out = new CapturingStream(true)
|
|
98
|
+
const box = new BootstrapProgressBox({ out })
|
|
99
|
+
box.start()
|
|
100
|
+
box.markStage('promus-install', 'running')
|
|
101
|
+
box.fail()
|
|
102
|
+
expect(out.combined()).toContain('✗')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('uses ANSI cursor-up + clear-to-end between renders', () => {
|
|
106
|
+
const out = new CapturingStream(true)
|
|
107
|
+
const box = new BootstrapProgressBox({ out })
|
|
108
|
+
box.start()
|
|
109
|
+
box.markStage('apt-update', 'running')
|
|
110
|
+
const combined = out.combined()
|
|
111
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: validates emitted ANSI escapes
|
|
112
|
+
expect(combined).toMatch(/\x1b\[\d+A/)
|
|
113
|
+
expect(combined).toContain('\x1b[0J')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('honors custom label override', () => {
|
|
117
|
+
const out = new CapturingStream(true)
|
|
118
|
+
const box = new BootstrapProgressBox({
|
|
119
|
+
out,
|
|
120
|
+
labels: { 'promus-install': 'promus 0.24.7 installed' },
|
|
121
|
+
})
|
|
122
|
+
box.start()
|
|
123
|
+
expect(out.combined()).toContain('promus 0.24.7 installed')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('row layout pads to constant width regardless of label length', () => {
|
|
127
|
+
const out = new CapturingStream(true)
|
|
128
|
+
const box = new BootstrapProgressBox({ out })
|
|
129
|
+
box.start()
|
|
130
|
+
const lines = out
|
|
131
|
+
.combined()
|
|
132
|
+
.split('\n')
|
|
133
|
+
.filter(l => l.startsWith('│'))
|
|
134
|
+
const widths = lines.map(l => stripAnsi(l).length)
|
|
135
|
+
const unique = new Set(widths)
|
|
136
|
+
expect(unique.size).toBe(1)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('BootstrapProgressBox non-TTY fallback', () => {
|
|
141
|
+
test('emits per-transition lines without ANSI', () => {
|
|
142
|
+
const out = new CapturingStream(false)
|
|
143
|
+
const box = new BootstrapProgressBox({ out })
|
|
144
|
+
box.start()
|
|
145
|
+
box.markStage('apt-update', 'running')
|
|
146
|
+
box.markStage('apt-update', 'done')
|
|
147
|
+
const combined = out.combined()
|
|
148
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: asserts absence of ANSI escapes
|
|
149
|
+
expect(combined).not.toMatch(/\x1b\[/)
|
|
150
|
+
expect(combined).toContain('[..]')
|
|
151
|
+
expect(combined).toContain('[ok]')
|
|
152
|
+
expect(combined).toContain('apt update')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('does not re-print the same status twice in non-TTY mode', () => {
|
|
156
|
+
const out = new CapturingStream(false)
|
|
157
|
+
const box = new BootstrapProgressBox({ out })
|
|
158
|
+
box.start()
|
|
159
|
+
box.markStage('apt-update', 'done')
|
|
160
|
+
box.tick()
|
|
161
|
+
box.tick()
|
|
162
|
+
const okLines = (out.combined().match(/\[ok\] /g) ?? []).length
|
|
163
|
+
expect(okLines).toBe(1)
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('formatTime', () => {
|
|
168
|
+
test('00:00 for zero', () => {
|
|
169
|
+
expect(__testing.formatTime(0)).toBe('00:00')
|
|
170
|
+
})
|
|
171
|
+
test('zero pads single-digit seconds and minutes', () => {
|
|
172
|
+
expect(__testing.formatTime(7)).toBe('00:07')
|
|
173
|
+
expect(__testing.formatTime(65)).toBe('01:05')
|
|
174
|
+
expect(__testing.formatTime(601)).toBe('10:01')
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('truncate', () => {
|
|
179
|
+
test('returns as-is when within limit', () => {
|
|
180
|
+
expect(__testing.truncate('hello', 10)).toBe('hello')
|
|
181
|
+
})
|
|
182
|
+
test('truncates with ellipsis when over limit', () => {
|
|
183
|
+
expect(__testing.truncate('a very long label that overflows', 12)).toBe('a very long…')
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
function stripAnsi(s: string): string {
|
|
188
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping ANSI escapes
|
|
189
|
+
return s.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
|
|
190
|
+
}
|