@silvery/ui 0.3.0
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 +71 -0
- package/src/animation/easing.ts +38 -0
- package/src/animation/index.ts +18 -0
- package/src/animation/useAnimation.ts +143 -0
- package/src/animation/useInterval.ts +39 -0
- package/src/animation/useLatest.ts +35 -0
- package/src/animation/useTimeout.ts +65 -0
- package/src/animation/useTransition.ts +110 -0
- package/src/animation.ts +24 -0
- package/src/ansi/index.ts +43 -0
- package/src/canvas/index.ts +169 -0
- package/src/cli/ansi.ts +85 -0
- package/src/cli/index.ts +39 -0
- package/src/cli/multi-progress.ts +340 -0
- package/src/cli/progress-bar.ts +222 -0
- package/src/cli/spinner.ts +275 -0
- package/src/components/Badge.tsx +54 -0
- package/src/components/Breadcrumb.tsx +72 -0
- package/src/components/Button.tsx +73 -0
- package/src/components/CommandPalette.tsx +186 -0
- package/src/components/Console.tsx +79 -0
- package/src/components/CursorLine.tsx +71 -0
- package/src/components/Divider.tsx +67 -0
- package/src/components/EditContextDisplay.tsx +164 -0
- package/src/components/ErrorBoundary.tsx +179 -0
- package/src/components/Form.tsx +86 -0
- package/src/components/GridCell.tsx +42 -0
- package/src/components/HorizontalVirtualList.tsx +375 -0
- package/src/components/ModalDialog.tsx +179 -0
- package/src/components/PickerDialog.tsx +208 -0
- package/src/components/PickerList.tsx +93 -0
- package/src/components/ProgressBar.tsx +126 -0
- package/src/components/Screen.tsx +78 -0
- package/src/components/ScrollbackList.tsx +92 -0
- package/src/components/ScrollbackView.tsx +390 -0
- package/src/components/SelectList.tsx +176 -0
- package/src/components/Skeleton.tsx +87 -0
- package/src/components/Spinner.tsx +64 -0
- package/src/components/SplitView.tsx +199 -0
- package/src/components/Table.tsx +139 -0
- package/src/components/Tabs.tsx +203 -0
- package/src/components/TextArea.tsx +264 -0
- package/src/components/TextInput.tsx +240 -0
- package/src/components/Toast.tsx +216 -0
- package/src/components/Toggle.tsx +73 -0
- package/src/components/Tooltip.tsx +60 -0
- package/src/components/TreeView.tsx +212 -0
- package/src/components/Typography.tsx +233 -0
- package/src/components/VirtualList.tsx +318 -0
- package/src/components/VirtualView.tsx +221 -0
- package/src/components/useReadline.ts +213 -0
- package/src/components/useTextArea.ts +648 -0
- package/src/components.ts +133 -0
- package/src/display/Table.tsx +179 -0
- package/src/display/index.ts +13 -0
- package/src/hooks/useTea.ts +133 -0
- package/src/image/Image.tsx +187 -0
- package/src/image/index.ts +15 -0
- package/src/image/kitty-graphics.ts +161 -0
- package/src/image/sixel-encoder.ts +194 -0
- package/src/images.ts +22 -0
- package/src/index.ts +34 -0
- package/src/input/Select.tsx +155 -0
- package/src/input/TextInput.tsx +227 -0
- package/src/input/index.ts +25 -0
- package/src/progress/als-context.ts +160 -0
- package/src/progress/declarative.ts +519 -0
- package/src/progress/index.ts +54 -0
- package/src/progress/step-node.ts +152 -0
- package/src/progress/steps.ts +425 -0
- package/src/progress/task.ts +138 -0
- package/src/progress/tasks.ts +216 -0
- package/src/react/ProgressBar.tsx +146 -0
- package/src/react/Spinner.tsx +74 -0
- package/src/react/Tasks.tsx +144 -0
- package/src/react/context.tsx +145 -0
- package/src/react/index.ts +30 -0
- package/src/types.ts +252 -0
- package/src/utils/eta.ts +155 -0
- package/src/utils/index.ts +13 -0
- package/src/wrappers/index.ts +36 -0
- package/src/wrappers/with-progress.ts +250 -0
- package/src/wrappers/with-select.ts +194 -0
- package/src/wrappers/with-spinner.ts +108 -0
- package/src/wrappers/with-text-input.ts +388 -0
- package/src/wrappers/wrap-emitter.ts +158 -0
- package/src/wrappers/wrap-generator.ts +143 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative steps implementation
|
|
3
|
+
*
|
|
4
|
+
* Provides the declarative overload for steps() that accepts an object
|
|
5
|
+
* structure and shows all steps upfront before execution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MultiProgress, type TaskHandle } from "../cli/multi-progress"
|
|
9
|
+
import { step as getStepContext, createStepContext, runWithStepContext, type InternalStepContext } from "./als-context"
|
|
10
|
+
import { parseStepsDef, flattenStepNodes, getLeafNodes, type StepNode, type StepsDef } from "./step-node"
|
|
11
|
+
|
|
12
|
+
// Re-export step() for convenience
|
|
13
|
+
export { step } from "./als-context"
|
|
14
|
+
|
|
15
|
+
// Node.js globals for yielding to event loop
|
|
16
|
+
declare function setImmediate(callback: (value?: unknown) => void): unknown
|
|
17
|
+
declare function setTimeout(callback: (value?: unknown) => void, ms: number): unknown
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Options for run() and pipe() execution
|
|
21
|
+
*/
|
|
22
|
+
export interface ExecuteOptions {
|
|
23
|
+
/** Clear progress display after completion (default: false) */
|
|
24
|
+
clear?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract the return type from a generator or async generator
|
|
29
|
+
*/
|
|
30
|
+
type GeneratorReturn<T> =
|
|
31
|
+
T extends Generator<unknown, infer R, unknown> ? R : T extends AsyncGenerator<unknown, infer R, unknown> ? R : T
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Unwrap the result type, handling generators specially
|
|
35
|
+
*/
|
|
36
|
+
type UnwrapResult<T> = Awaited<GeneratorReturn<Awaited<T>>>
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Result type: maps step keys to their return values
|
|
40
|
+
*/
|
|
41
|
+
type StepResults<T extends StepsDef> = {
|
|
42
|
+
[K in keyof T]: T[K] extends (...args: unknown[]) => infer R
|
|
43
|
+
? UnwrapResult<R>
|
|
44
|
+
: T[K] extends [string, (...args: unknown[]) => infer R]
|
|
45
|
+
? UnwrapResult<R>
|
|
46
|
+
: T[K] extends StepsDef
|
|
47
|
+
? StepResults<T[K]>
|
|
48
|
+
: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The runner object returned by steps()
|
|
53
|
+
*/
|
|
54
|
+
export interface StepsRunner<T extends StepsDef> {
|
|
55
|
+
/** Internal: the parsed step nodes (for testing) */
|
|
56
|
+
readonly _steps: StepNode[]
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Execute all steps sequentially
|
|
60
|
+
* @returns Results keyed by step name
|
|
61
|
+
*/
|
|
62
|
+
run(options?: ExecuteOptions): Promise<StepResults<T>>
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Execute all steps in a pipeline (each receives previous result)
|
|
66
|
+
* @returns Final step's result
|
|
67
|
+
*/
|
|
68
|
+
pipe(options?: ExecuteOptions): Promise<unknown>
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Manually signal completion (for manual execution mode)
|
|
72
|
+
*/
|
|
73
|
+
done(options?: { clear?: boolean }): void
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Create a declarative steps runner
|
|
78
|
+
*
|
|
79
|
+
* @param def - Object structure defining steps
|
|
80
|
+
* @returns StepsRunner with run(), pipe(), and done() methods
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* const loader = stepsDeclarative({
|
|
85
|
+
* loadModules, // "Load modules"
|
|
86
|
+
* loadRepo: { // "Load repo" (group)
|
|
87
|
+
* discover, // "Discover"
|
|
88
|
+
* parse, // "Parse"
|
|
89
|
+
* },
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* const results = await loader.run({ clear: true });
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export function stepsDeclarative<T extends StepsDef>(def: T): StepsRunner<T> {
|
|
96
|
+
const rootNodes = parseStepsDef(def)
|
|
97
|
+
const allNodes = flattenStepNodes(rootNodes)
|
|
98
|
+
|
|
99
|
+
let multi: MultiProgress | null = null
|
|
100
|
+
const handles = new Map<StepNode, TaskHandle>()
|
|
101
|
+
|
|
102
|
+
// Build group tracking: map each group to its leaf nodes
|
|
103
|
+
const groupLeaves = new Map<StepNode, StepNode[]>()
|
|
104
|
+
const leafToGroups = new Map<StepNode, StepNode[]>()
|
|
105
|
+
|
|
106
|
+
for (const node of allNodes) {
|
|
107
|
+
if (node.children) {
|
|
108
|
+
const leaves = getLeafNodes([node])
|
|
109
|
+
groupLeaves.set(node, leaves)
|
|
110
|
+
for (const leaf of leaves) {
|
|
111
|
+
const groups = leafToGroups.get(leaf) ?? []
|
|
112
|
+
groups.push(node)
|
|
113
|
+
leafToGroups.set(leaf, groups)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
get _steps() {
|
|
120
|
+
return rootNodes
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
async run(options?: ExecuteOptions): Promise<StepResults<T>> {
|
|
124
|
+
multi = new MultiProgress()
|
|
125
|
+
|
|
126
|
+
// Register all steps upfront (shows pending state)
|
|
127
|
+
registerAllSteps(allNodes, multi, handles)
|
|
128
|
+
|
|
129
|
+
// Group timing tracking
|
|
130
|
+
const groupStartTimes = new Map<StepNode, number>()
|
|
131
|
+
const completedLeaves = new Set<StepNode>()
|
|
132
|
+
|
|
133
|
+
multi.start()
|
|
134
|
+
|
|
135
|
+
// Yield to event loop to ensure initial render is displayed
|
|
136
|
+
// before we start modifying task states
|
|
137
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
138
|
+
|
|
139
|
+
const results: Record<string, unknown> = {}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Execute each step with work
|
|
143
|
+
for (const node of allNodes) {
|
|
144
|
+
if (node.work) {
|
|
145
|
+
// Start parent groups if not started
|
|
146
|
+
const groups = leafToGroups.get(node) ?? []
|
|
147
|
+
for (const group of groups) {
|
|
148
|
+
if (!groupStartTimes.has(group)) {
|
|
149
|
+
groupStartTimes.set(group, Date.now())
|
|
150
|
+
handles.get(group)?.start()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = await executeStep(node, handles, multi)
|
|
155
|
+
setNestedResult(results, node.key, result)
|
|
156
|
+
|
|
157
|
+
// Mark leaf as complete and check group completion
|
|
158
|
+
completedLeaves.add(node)
|
|
159
|
+
for (const group of groups) {
|
|
160
|
+
const leaves = groupLeaves.get(group) ?? []
|
|
161
|
+
if (leaves.every((l) => completedLeaves.has(l))) {
|
|
162
|
+
const elapsed = Date.now() - groupStartTimes.get(group)!
|
|
163
|
+
handles.get(group)?.complete(elapsed)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
multi.stop(options?.clear ?? false)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return results as StepResults<T>
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
async pipe(options?: ExecuteOptions): Promise<unknown> {
|
|
176
|
+
multi = new MultiProgress()
|
|
177
|
+
|
|
178
|
+
// Register all steps upfront
|
|
179
|
+
registerAllSteps(allNodes, multi, handles)
|
|
180
|
+
|
|
181
|
+
// Group timing tracking
|
|
182
|
+
const groupStartTimes = new Map<StepNode, number>()
|
|
183
|
+
const completedLeaves = new Set<StepNode>()
|
|
184
|
+
|
|
185
|
+
multi.start()
|
|
186
|
+
|
|
187
|
+
// Yield to event loop to ensure initial render is displayed
|
|
188
|
+
// before we start modifying task states
|
|
189
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
190
|
+
|
|
191
|
+
let previousResult: unknown = undefined
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Execute each step, passing previous result
|
|
195
|
+
for (const node of allNodes) {
|
|
196
|
+
if (node.work) {
|
|
197
|
+
// Start parent groups if not started
|
|
198
|
+
const groups = leafToGroups.get(node) ?? []
|
|
199
|
+
for (const group of groups) {
|
|
200
|
+
if (!groupStartTimes.has(group)) {
|
|
201
|
+
groupStartTimes.set(group, Date.now())
|
|
202
|
+
handles.get(group)?.start()
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
previousResult = await executeStep(node, handles, multi, previousResult)
|
|
207
|
+
|
|
208
|
+
// Mark leaf as complete and check group completion
|
|
209
|
+
completedLeaves.add(node)
|
|
210
|
+
for (const group of groups) {
|
|
211
|
+
const leaves = groupLeaves.get(group) ?? []
|
|
212
|
+
if (leaves.every((l) => completedLeaves.has(l))) {
|
|
213
|
+
const elapsed = Date.now() - groupStartTimes.get(group)!
|
|
214
|
+
handles.get(group)?.complete(elapsed)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} finally {
|
|
220
|
+
multi.stop(options?.clear ?? false)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return previousResult
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
done(options?: { clear?: boolean }) {
|
|
227
|
+
if (multi) {
|
|
228
|
+
multi.stop(options?.clear ?? false)
|
|
229
|
+
multi = null
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Register all steps with MultiProgress upfront
|
|
237
|
+
*/
|
|
238
|
+
function registerAllSteps(nodes: StepNode[], multi: MultiProgress, handles: Map<StepNode, TaskHandle>): void {
|
|
239
|
+
// Register in order without insertAfter - simpler and correct
|
|
240
|
+
for (const node of nodes) {
|
|
241
|
+
const isGroup = node.children && !node.work
|
|
242
|
+
const handle = multi.add(node.label, {
|
|
243
|
+
type: isGroup ? "group" : "spinner",
|
|
244
|
+
indent: node.indent,
|
|
245
|
+
})
|
|
246
|
+
handles.set(node, handle)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Execute a single step
|
|
252
|
+
*/
|
|
253
|
+
async function executeStep(
|
|
254
|
+
node: StepNode,
|
|
255
|
+
handles: Map<StepNode, TaskHandle>,
|
|
256
|
+
multi: MultiProgress,
|
|
257
|
+
input?: unknown,
|
|
258
|
+
): Promise<unknown> {
|
|
259
|
+
const handle = handles.get(node)!
|
|
260
|
+
const startTime = Date.now()
|
|
261
|
+
|
|
262
|
+
// Yield to event loop before starting
|
|
263
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
264
|
+
|
|
265
|
+
// Create step context for ALS
|
|
266
|
+
const ctx = createStepContext(node.label, handle, (subLabel) => {
|
|
267
|
+
// Create sub-step handle when step().sub() is called
|
|
268
|
+
return multi.add(subLabel, {
|
|
269
|
+
type: "spinner",
|
|
270
|
+
indent: node.indent + 1,
|
|
271
|
+
insertAfter: handle.id,
|
|
272
|
+
})
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
handle.start()
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
// Run work function with ALS context
|
|
279
|
+
const result = await runWithStepContext(ctx, () => {
|
|
280
|
+
if (input !== undefined) {
|
|
281
|
+
return (node.work as (input: unknown) => unknown)(input)
|
|
282
|
+
}
|
|
283
|
+
return node.work!()
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
// Handle generator results
|
|
287
|
+
if (isGenerator(result)) {
|
|
288
|
+
return await runGenerator(result, ctx, node, multi)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (isAsyncGenerator(result)) {
|
|
292
|
+
return await runAsyncGenerator(result, ctx, node, multi)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Complete any remaining sub-step
|
|
296
|
+
ctx._completeSubStep()
|
|
297
|
+
|
|
298
|
+
// Complete the step with timing
|
|
299
|
+
const elapsed = Date.now() - startTime
|
|
300
|
+
handle.complete(elapsed)
|
|
301
|
+
|
|
302
|
+
return result
|
|
303
|
+
} catch (error) {
|
|
304
|
+
handle.fail()
|
|
305
|
+
throw error
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Run a sync generator step
|
|
311
|
+
*/
|
|
312
|
+
async function runGenerator<T>(
|
|
313
|
+
gen: Generator<unknown, T, unknown>,
|
|
314
|
+
ctx: InternalStepContext,
|
|
315
|
+
node: StepNode,
|
|
316
|
+
multi: MultiProgress,
|
|
317
|
+
): Promise<T> {
|
|
318
|
+
const startTime = Date.now()
|
|
319
|
+
let result = gen.next()
|
|
320
|
+
let hasSubSteps = false
|
|
321
|
+
// Track last inserted handle to maintain correct order
|
|
322
|
+
// Each new sub-step inserts after the previous one, not after parent
|
|
323
|
+
let lastInsertedId = ctx.handle.id
|
|
324
|
+
|
|
325
|
+
while (!result.done) {
|
|
326
|
+
const value = result.value
|
|
327
|
+
|
|
328
|
+
// Handle yielded values
|
|
329
|
+
if (isDeclareSteps(value)) {
|
|
330
|
+
// Declare all sub-steps upfront (show as pending)
|
|
331
|
+
if (!hasSubSteps) {
|
|
332
|
+
hasSubSteps = true
|
|
333
|
+
ctx.handle.setType("group")
|
|
334
|
+
}
|
|
335
|
+
for (const label of value.declare) {
|
|
336
|
+
const subHandle = multi.add(label, {
|
|
337
|
+
type: "spinner",
|
|
338
|
+
indent: node.indent + 1,
|
|
339
|
+
insertAfter: lastInsertedId,
|
|
340
|
+
})
|
|
341
|
+
lastInsertedId = subHandle.id
|
|
342
|
+
ctx._addSubHandle(label, subHandle)
|
|
343
|
+
}
|
|
344
|
+
} else if (typeof value === "string") {
|
|
345
|
+
// String = start a sub-step with this label
|
|
346
|
+
ctx._completeSubStep()
|
|
347
|
+
|
|
348
|
+
// First sub-step: change parent from spinner to group (no animation)
|
|
349
|
+
if (!hasSubSteps) {
|
|
350
|
+
hasSubSteps = true
|
|
351
|
+
ctx.handle.setType("group")
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Check if already declared, otherwise create new
|
|
355
|
+
const existingHandle = ctx._getSubHandle?.(value)
|
|
356
|
+
if (existingHandle) {
|
|
357
|
+
ctx._setCurrentSubHandle(value, existingHandle)
|
|
358
|
+
existingHandle.start()
|
|
359
|
+
} else {
|
|
360
|
+
const subHandle = multi.add(value, {
|
|
361
|
+
type: "spinner",
|
|
362
|
+
indent: node.indent + 1,
|
|
363
|
+
insertAfter: lastInsertedId,
|
|
364
|
+
})
|
|
365
|
+
lastInsertedId = subHandle.id
|
|
366
|
+
ctx._addSubHandle(value, subHandle)
|
|
367
|
+
subHandle.start()
|
|
368
|
+
}
|
|
369
|
+
} else if (isProgressUpdate(value)) {
|
|
370
|
+
// Progress update
|
|
371
|
+
ctx.progress(value.current ?? 0, value.total ?? 0)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Yield to event loop for animation
|
|
375
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
376
|
+
|
|
377
|
+
result = gen.next()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Complete any remaining sub-step
|
|
381
|
+
ctx._completeSubStep()
|
|
382
|
+
|
|
383
|
+
// Complete the step with timing
|
|
384
|
+
const elapsed = Date.now() - startTime
|
|
385
|
+
ctx.handle.complete(elapsed)
|
|
386
|
+
|
|
387
|
+
return result.value
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Run an async generator step
|
|
392
|
+
*/
|
|
393
|
+
async function runAsyncGenerator<T>(
|
|
394
|
+
gen: AsyncGenerator<unknown, T, unknown>,
|
|
395
|
+
ctx: InternalStepContext,
|
|
396
|
+
node: StepNode,
|
|
397
|
+
multi: MultiProgress,
|
|
398
|
+
): Promise<T> {
|
|
399
|
+
const startTime = Date.now()
|
|
400
|
+
let result = await gen.next()
|
|
401
|
+
let hasSubSteps = false
|
|
402
|
+
// Track last inserted handle to maintain correct order
|
|
403
|
+
// Each new sub-step inserts after the previous one, not after parent
|
|
404
|
+
let lastInsertedId = ctx.handle.id
|
|
405
|
+
|
|
406
|
+
while (!result.done) {
|
|
407
|
+
const value = result.value
|
|
408
|
+
|
|
409
|
+
// Handle yielded values
|
|
410
|
+
if (isDeclareSteps(value)) {
|
|
411
|
+
// Declare all sub-steps upfront (show as pending)
|
|
412
|
+
if (!hasSubSteps) {
|
|
413
|
+
hasSubSteps = true
|
|
414
|
+
ctx.handle.setType("group")
|
|
415
|
+
}
|
|
416
|
+
for (const label of value.declare) {
|
|
417
|
+
const subHandle = multi.add(label, {
|
|
418
|
+
type: "spinner",
|
|
419
|
+
indent: node.indent + 1,
|
|
420
|
+
insertAfter: lastInsertedId,
|
|
421
|
+
})
|
|
422
|
+
lastInsertedId = subHandle.id
|
|
423
|
+
ctx._addSubHandle(label, subHandle)
|
|
424
|
+
}
|
|
425
|
+
} else if (typeof value === "string") {
|
|
426
|
+
// String = start a sub-step with this label
|
|
427
|
+
ctx._completeSubStep()
|
|
428
|
+
|
|
429
|
+
// First sub-step: change parent from spinner to group (no animation)
|
|
430
|
+
if (!hasSubSteps) {
|
|
431
|
+
hasSubSteps = true
|
|
432
|
+
ctx.handle.setType("group")
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check if already declared, otherwise create new
|
|
436
|
+
const existingHandle = ctx._getSubHandle(value)
|
|
437
|
+
if (existingHandle) {
|
|
438
|
+
ctx._setCurrentSubHandle(value, existingHandle)
|
|
439
|
+
existingHandle.start()
|
|
440
|
+
} else {
|
|
441
|
+
const subHandle = multi.add(value, {
|
|
442
|
+
type: "spinner",
|
|
443
|
+
indent: node.indent + 1,
|
|
444
|
+
insertAfter: lastInsertedId,
|
|
445
|
+
})
|
|
446
|
+
lastInsertedId = subHandle.id
|
|
447
|
+
ctx._addSubHandle(value, subHandle)
|
|
448
|
+
subHandle.start()
|
|
449
|
+
}
|
|
450
|
+
} else if (isProgressUpdate(value)) {
|
|
451
|
+
// Progress update
|
|
452
|
+
ctx.progress(value.current ?? 0, value.total ?? 0)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Yield to event loop for animation
|
|
456
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
457
|
+
|
|
458
|
+
result = await gen.next()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Complete any remaining sub-step
|
|
462
|
+
ctx._completeSubStep()
|
|
463
|
+
|
|
464
|
+
// Complete the step with timing
|
|
465
|
+
const elapsed = Date.now() - startTime
|
|
466
|
+
ctx.handle.complete(elapsed)
|
|
467
|
+
|
|
468
|
+
return result.value
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Set a nested result value by key path
|
|
473
|
+
*/
|
|
474
|
+
function setNestedResult(results: Record<string, unknown>, key: string, value: unknown): void {
|
|
475
|
+
// For now, flat keys only - nested groups would need path handling
|
|
476
|
+
results[key] = value
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Type guards
|
|
481
|
+
*/
|
|
482
|
+
function isGenerator(value: unknown): value is Generator<unknown, unknown, unknown> {
|
|
483
|
+
return (
|
|
484
|
+
value !== null &&
|
|
485
|
+
typeof value === "object" &&
|
|
486
|
+
typeof (value as Generator).next === "function" &&
|
|
487
|
+
typeof (value as Generator)[Symbol.iterator] === "function"
|
|
488
|
+
)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function isAsyncGenerator(value: unknown): value is AsyncGenerator<unknown, unknown, unknown> {
|
|
492
|
+
return (
|
|
493
|
+
value !== null &&
|
|
494
|
+
typeof value === "object" &&
|
|
495
|
+
typeof (value as AsyncGenerator).next === "function" &&
|
|
496
|
+
typeof (value as AsyncGenerator)[Symbol.asyncIterator] === "function"
|
|
497
|
+
)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
interface ProgressUpdate {
|
|
501
|
+
current?: number
|
|
502
|
+
total?: number
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
interface DeclareSteps {
|
|
506
|
+
declare: string[]
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function isProgressUpdate(value: unknown): value is ProgressUpdate {
|
|
510
|
+
return (
|
|
511
|
+
value !== null && typeof value === "object" && !Array.isArray(value) && ("current" in value || "total" in value)
|
|
512
|
+
)
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function isDeclareSteps(value: unknown): value is DeclareSteps {
|
|
516
|
+
return (
|
|
517
|
+
value !== null && typeof value === "object" && "declare" in value && Array.isArray((value as DeclareSteps).declare)
|
|
518
|
+
)
|
|
519
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progress utilities for CLI applications
|
|
3
|
+
*
|
|
4
|
+
* Provides declarative and fluent APIs for displaying progress during async operations.
|
|
5
|
+
*
|
|
6
|
+
* @example Declarative mode (recommended)
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { steps, step } from "@silvery/ui/progress";
|
|
9
|
+
*
|
|
10
|
+
* const loader = steps({
|
|
11
|
+
* loadModules, // Auto-named: "Load modules"
|
|
12
|
+
* loadRepo: { // Group: "Load repo"
|
|
13
|
+
* discover, // "Discover"
|
|
14
|
+
* parse, // "Parse"
|
|
15
|
+
* },
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const results = await loader.run({ clear: true });
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example Fluent mode (legacy)
|
|
22
|
+
* ```typescript
|
|
23
|
+
* await steps()
|
|
24
|
+
* .run("Loading", loadModules)
|
|
25
|
+
* .run("Building", buildView)
|
|
26
|
+
* .execute({ clear: true });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
// Modern API (recommended)
|
|
31
|
+
export {
|
|
32
|
+
steps,
|
|
33
|
+
step,
|
|
34
|
+
type StepBuilder,
|
|
35
|
+
type ExecuteOptions,
|
|
36
|
+
type StepsRunner,
|
|
37
|
+
type StepsDef,
|
|
38
|
+
type StepNode,
|
|
39
|
+
type StepContext,
|
|
40
|
+
} from "./steps"
|
|
41
|
+
|
|
42
|
+
// Legacy task wrappers (deprecated - use steps() instead)
|
|
43
|
+
/** @deprecated Use steps() instead */
|
|
44
|
+
export { task, type TaskWrapper } from "./task"
|
|
45
|
+
/** @deprecated Use steps() instead */
|
|
46
|
+
export { tasks, type TaskBuilder, type RunOptions } from "./tasks"
|
|
47
|
+
|
|
48
|
+
// Re-export CLI progress components
|
|
49
|
+
export { Spinner, createSpinner, type CallableSpinner } from "../cli/spinner"
|
|
50
|
+
export { ProgressBar } from "../cli/progress-bar"
|
|
51
|
+
export { MultiProgress, type TaskHandle } from "../cli/multi-progress"
|
|
52
|
+
|
|
53
|
+
// Re-export types
|
|
54
|
+
export type { ProgressInfo, StepProgress } from "../types.js"
|