Skip to content

Commit 4e407e0

Browse files
committed
refactor: progress is injected to context now
1 parent 3b7fed0 commit 4e407e0

14 files changed

Lines changed: 128 additions & 137 deletions

File tree

.claude/settings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"hooks": {
3+
"PostToolUse": [
4+
{
5+
"matcher": "Edit|Write",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "bun fix"
10+
}
11+
]
12+
}
13+
]
14+
}
15+
}

docs/src/content/docs/guides/progress-indicators.md

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ Padrone provides a built-in progress indicator system for commands that take tim
88
## Quick Example
99

1010
```typescript
11-
import { createPadrone } from 'padrone';
11+
import { createPadrone, padroneProgress } from 'padrone';
1212

1313
const program = createPadrone('app')
1414
.command('deploy', (c) =>
1515
c
1616
.async()
17-
.progress('Deploying...')
17+
.extend(padroneProgress('Deploying...'))
1818
.action(async () => {
1919
await deploy();
2020
return { version: '2.0' };
@@ -26,31 +26,24 @@ Running `app deploy` shows a spinner with "Deploying..." that auto-succeeds when
2626

2727
## Auto-Managed Progress
2828

29-
Use the `.progress()` builder method to configure automatic progress indicators. The indicator starts before validation and is automatically stopped on success or failure.
29+
Use `padroneProgress()` to configure automatic progress indicators. Register it with `.intercept()` on a command. The indicator starts before validation and is automatically stopped on success or failure.
3030

3131
### Simple Message
3232

3333
```typescript
34-
.progress('Deploying...')
35-
```
36-
37-
### Boolean Shorthand
38-
39-
```typescript
40-
.progress(true)
41-
// Shows "Running <command-name>..."
34+
c.extend(padroneProgress('Deploying...'))
4235
```
4336

4437
### Full Configuration
4538

4639
```typescript
47-
.progress({
40+
c.extend(padroneProgress({
4841
validation: 'Validating inputs...', // Shown during async validation
4942
progress: 'Deploying...', // Shown during execution
5043
success: 'Deployed successfully!', // Shown on success
5144
error: 'Deploy failed', // Shown on failure
5245
spinner: 'line', // Spinner preset
53-
})
46+
}))
5447
```
5548

5649
The `validation` message is shown first (during schema validation), then replaced by the `progress` message when execution begins.
@@ -60,19 +53,19 @@ The `validation` message is shown first (during schema validation), then replace
6053
The `success` and `error` fields accept callbacks that receive the actual result or error:
6154

6255
```typescript
63-
.progress({
56+
c.extend(padroneProgress({
6457
progress: 'Deploying...',
6558
success: (result) => `Deployed v${result.version}`,
6659
error: (err) => `Deploy failed: ${err.message}`,
67-
})
60+
}))
6861
```
6962

7063
### Custom Indicator Icons
7164

7265
Callbacks can return an object with `message` and `indicator` to override the success/error icon per-call:
7366

7467
```typescript
75-
.progress({
68+
c.extend(padroneProgress({
7669
progress: 'Running checks...',
7770
success: (result) => ({
7871
message: `All ${result.count} checks passed`,
@@ -82,27 +75,27 @@ Callbacks can return an object with `message` and `indicator` to override the su
8275
message: 'Checks failed',
8376
indicator: '💥',
8477
}),
85-
})
78+
}))
8679
```
8780

8881
Static values also support the object form:
8982

9083
```typescript
91-
.progress({
84+
c.extend(padroneProgress({
9285
progress: 'Building...',
9386
success: { message: 'Build complete', indicator: '🏗️' },
94-
})
87+
}))
9588
```
9689

9790
### Suppressing Messages
9891

9992
Pass `null` to suppress the success or error message entirely (the spinner just clears):
10093

10194
```typescript
102-
.progress({
95+
c.extend(padroneProgress({
10396
progress: 'Working...',
10497
success: null, // No success message
105-
})
98+
}))
10699
```
107100

108101
Callbacks can also return `null`:
@@ -111,36 +104,22 @@ Callbacks can also return `null`:
111104
success: (result) => result.silent ? null : `Done: ${result.count} items`
112105
```
113106

114-
## Manual Progress via `ctx.progress`
107+
## Manual Progress via `ctx.context.progress`
115108

116-
The action context provides a `progress` property for manual control:
109+
When `padroneProgress()` is registered, the action context provides a typed `progress` property on `ctx.context`:
117110

118111
```typescript
119-
.progress('Importing...')
120-
.action((args, ctx) => {
121-
for (const item of items) {
122-
process(item);
123-
ctx.progress.update(`Importing ${item.name}...`);
124-
}
125-
return `Imported ${items.length} items`;
126-
})
127-
```
128-
129-
### Without `.progress()` Config
130-
131-
`ctx.progress` works even without calling `.progress()` on the builder. It lazily creates a real indicator on first use:
132-
133-
```typescript
134-
.action((_args, ctx) => {
135-
ctx.progress.update('Starting work...');
136-
// A real spinner appears now
137-
doWork();
138-
ctx.progress.update('Almost done...');
139-
return 'done';
140-
})
112+
c.extend(padroneProgress('Importing...'))
113+
.action((args, ctx) => {
114+
for (const item of items) {
115+
process(item);
116+
ctx.context.progress.update(`Importing ${item.name}...`);
117+
}
118+
return `Imported ${items.length} items`;
119+
})
141120
```
142121

143-
Lazily-created indicators are automatically stopped (not succeeded/failed) when execution finishes. When the runtime has no progress factory, `ctx.progress` is a silent no-op.
122+
`padroneProgress()` uses the [context-providing interceptor](/guides/interceptors#context-providing-interceptors) mechanism — it declares `.provides<{ progress: PadroneProgressIndicator }>()`, so `ctx.context.progress` is fully typed when the interceptor is registered on the command.
144123

145124
### `PadroneProgressIndicator` Methods
146125

@@ -167,24 +146,24 @@ Padrone includes four built-in spinner presets:
167146
| `bounce` | `⠁ ⠂ ⠄ ⡀ ⢀ ⠠ ⠐ ⠈` |
168147

169148
```typescript
170-
.progress({ progress: 'Loading...', spinner: 'line' })
149+
c.extend(padroneProgress({ progress: 'Loading...', spinner: 'line' }))
171150
```
172151

173152
### Custom Frames
174153

175154
```typescript
176-
.progress({
155+
c.extend(padroneProgress({
177156
progress: 'Loading...',
178157
spinner: { frames: ['🌑', '🌒', '🌓', '🌔', '🌕'], interval: 150 },
179-
})
158+
}))
180159
```
181160

182161
### Disabling the Spinner
183162

184163
Set `spinner: false` to show static text without animation:
185164

186165
```typescript
187-
.progress({ progress: 'Processing...', spinner: false })
166+
c.extend(padroneProgress({ progress: 'Processing...', spinner: false }))
188167
```
189168

190169
## Runtime Progress Factory
@@ -237,7 +216,7 @@ const { factory, indicators } = createMockProgress();
237216
const program = createPadrone('app')
238217
.runtime({ progress: factory })
239218
.command('cmd', (c) =>
240-
c.progress('Working...').action(() => 'done')
219+
c.extend(padroneProgress('Working...')).action(() => 'done')
241220
);
242221

243222
program.eval('cmd');
@@ -248,23 +227,22 @@ program.eval('cmd');
248227

249228
When auto-progress is active, `runtime.output` and `runtime.error` are automatically wrapped to pause/resume the spinner. This prevents garbled output when writing to the terminal while a spinner is animating.
250229

251-
Manual calls to `ctx.progress.pause()` and `ctx.progress.resume()` are available if you need explicit control.
230+
Manual calls to `ctx.context.progress.pause()` and `ctx.context.progress.resume()` are available if you need explicit control.
252231

253232
## Integration with Interceptors
254233

255-
Progress indicators interact naturally with the interceptor system. The indicator starts before validation interceptors run and is cleaned up in the lifecycle shutdown. Interceptor errors are caught and reflected in the progress indicator:
234+
Progress indicators interact naturally with the interceptor system. The indicator starts before validation interceptors run and is cleaned up after execution. Interceptor errors are caught and reflected in the progress indicator:
256235

257236
```typescript
258237
program
259-
.intercept({
260-
name: 'auth',
238+
.intercept(defineInterceptor({ name: 'auth' }, () => ({
261239
execute: (ctx, next) => {
262240
// If this throws, the progress indicator shows the error
263241
checkAuth();
264242
return next();
265243
},
266-
})
244+
})))
267245
.command('deploy', (c) =>
268-
c.progress('Deploying...').action(handler)
246+
c.extend(padroneProgress('Deploying...')).action(handler)
269247
);
270248
```

examples/padrone-example/src/tasks.ts

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -281,17 +281,6 @@ export const tasksProgram = createPadrone('tasks')
281281
.meta({ flags: 't' }),
282282
}),
283283
)
284-
.action(async (_args, ctx) => {
285-
await new Promise((resolve) => setTimeout(resolve, 1500));
286-
ctx.progress.update('Finalizing sync...');
287-
await new Promise((resolve) => setTimeout(resolve, 1000));
288-
289-
const tasks = getTasks();
290-
return {
291-
count: tasks.length,
292-
message: `Synced ${tasks.length} task(s) to remote.`,
293-
};
294-
})
295284
.extend(
296285
padroneProgress({
297286
validation: 'Validating before sync...',
@@ -302,7 +291,18 @@ export const tasksProgram = createPadrone('tasks')
302291
}),
303292
error: 'Failed to sync tasks.',
304293
}),
305-
),
294+
)
295+
.action(async (_args, ctx) => {
296+
await new Promise((resolve) => setTimeout(resolve, 1500));
297+
ctx.context.progress.update('Finalizing sync...');
298+
await new Promise((resolve) => setTimeout(resolve, 1000));
299+
300+
const tasks = getTasks();
301+
return {
302+
count: tasks.length,
303+
message: `Synced ${tasks.length} task(s) to remote.`,
304+
};
305+
}),
306306
)
307307
.command('import', (c) =>
308308
c
@@ -313,21 +313,21 @@ export const tasksProgram = createPadrone('tasks')
313313
}),
314314
{ positional: ['file'] },
315315
)
316+
.extend(
317+
padroneProgress({
318+
progress: 'Importing tasks...',
319+
success: (res) => res as string,
320+
}),
321+
)
316322
.action((args, ctx) => {
317323
// Simulate reading and importing
318324
const count = 3;
319325
for (let i = 1; i <= count; i++) {
320326
addTask({ title: `Imported task ${i} from ${args.file}`, priority: 'medium', tags: ['imported'] });
321-
ctx.progress.update(`Imported ${i}/${count} tasks...`);
327+
ctx.context.progress.update(`Imported ${i}/${count} tasks...`);
322328
}
323329
return `Successfully imported ${count} task(s) from ${args.file}`;
324-
})
325-
.extend(
326-
padroneProgress({
327-
progress: 'Importing tasks...',
328-
success: (res) => res as string,
329-
}),
330-
),
330+
}),
331331
)
332332
.command('board', (c) =>
333333
c

packages/padrone/src/core/exec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
} from '../types/index.ts';
1616
import { getCommandRuntime } from './commands.ts';
1717
import { RoutingError, SignalError, ValidationError } from './errors.ts';
18-
import { noopIndicator, resolveRegisteredInterceptors, runInterceptorChain, wrapWithLifecycle } from './interceptors.ts';
18+
import { resolveRegisteredInterceptors, runInterceptorChain, wrapWithLifecycle } from './interceptors.ts';
1919
import { errorResult, noop, thenMaybe, warnIfUnexpectedAsync, withDrain } from './results.ts';
2020
import { buildCommandArgs, formatIssueMessages, validateCommandArgs } from './validate.ts';
2121

@@ -207,7 +207,6 @@ export function execCommand(
207207
runtime: effectiveRuntime,
208208
command: executeCtx.command,
209209
program: ctx.builder as any,
210-
progress: executeCtx.progress ?? noopIndicator,
211210
signal: executeCtx.signal,
212211
context: executeCtx.context,
213212
caller,

packages/padrone/src/core/interceptors.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
ResolvedInterceptor,
1313
} from '../types/index.ts';
1414
import { thenMaybe } from './results.ts';
15-
import type { PadroneProgressIndicator, ResolvedPadroneRuntime } from './runtime.ts';
15+
import type { ResolvedPadroneRuntime } from './runtime.ts';
1616

1717
// ---------------------------------------------------------------------------
1818
// defineInterceptor — creates a single-value distributable interceptor
@@ -151,16 +151,6 @@ export function runInterceptorChain<TCtx extends object, TResult>(
151151
return next(ctx);
152152
}
153153

154-
/** No-op progress indicator returned when no progress extension is active. */
155-
export const noopIndicator: PadroneProgressIndicator = {
156-
update() {},
157-
succeed() {},
158-
fail() {},
159-
stop() {},
160-
pause() {},
161-
resume() {},
162-
};
163-
164154
/**
165155
* Wraps a pipeline with start → error → shutdown lifecycle hooks.
166156
* - `start` interceptors wrap the pipeline (onion pattern, root interceptors only).

packages/padrone/src/core/program-methods.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { findCommandByName, getCommandRuntime, resolveAllCommands } from './comm
1616
import { RoutingError } from './errors.ts';
1717
import type { ExecContext } from './exec.ts';
1818
import { collectInterceptors, errorResultWithSignal, execCommand } from './exec.ts';
19-
import { noopIndicator, resolveRegisteredInterceptors, runInterceptorChain } from './interceptors.ts';
19+
import { resolveRegisteredInterceptors, runInterceptorChain } from './interceptors.ts';
2020
import { errorResult, makeThenable, thenMaybe, warnIfUnexpectedAsync, withDrain, withPromiseDrain } from './results.ts';
2121
import { coreValidateForParse } from './validate.ts';
2222

@@ -128,7 +128,6 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
128128
runtime: executeCtx.runtime,
129129
command: executeCtx.command,
130130
program: ctx.builder as any,
131-
progress: noopIndicator,
132131
signal: inertSignal,
133132
context: executeCtx.context,
134133
caller: 'run',

packages/padrone/src/extension/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export type { WithMan } from './man.ts';
1414
export { padroneMan } from './man.ts';
1515
export type { WithMcp } from './mcp.ts';
1616
export { padroneMcp } from './mcp.ts';
17-
export type { PadroneProgressConfig, PadroneProgressMessage } from './progress.ts';
17+
export type { PadroneProgressConfig, PadroneProgressMessage, WithProgress } from './progress.ts';
1818
export { padroneProgress } from './progress.ts';
1919
export type { WithRepl } from './repl.ts';
2020
export { padroneRepl } from './repl.ts';

0 commit comments

Comments
 (0)