effective-progress 0.6.2 → 0.7.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/README.md CHANGED
@@ -83,12 +83,14 @@ Support for `either`/`validate` modes of `Effect.all` and render the amount of s
83
83
  - In fail-fast runs, unresolved units remain unprocessed.
84
84
  - `mode: "either"` and `mode: "validate"` run all effects and keep mixed outcomes in the task counters.
85
85
  - Mixed outcomes can still finalize as `done` when all units are accounted for.
86
+ - Empty collections are valid inputs for `Progress.all` / `Progress.forEach` and render as `0/0` instead of failing.
86
87
 
87
88
  ### Other examples
88
89
 
89
90
  - `examples/simpleExample.ts` - low-boilerplate real-world flow
90
91
  - `examples/advancedExample.ts` - full API usage and manual task control
91
92
  - `examples/mixedOutcomes.ts` - fail-fast vs `either`/`validate` with mixed success/failure counters
93
+ - `examples/cliProgressSemantics.ts` - zero totals, negative totals, overflow counts, and empty `all` / `forEach`
92
94
  - `examples/unknownTotalCounting.ts` - count successes/failures without a known total and render `processed/?`
93
95
  - `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
94
96
  - `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
@@ -107,6 +109,8 @@ Support for `either`/`validate` modes of `Effect.all` and render the amount of s
107
109
  - Built-in columns are: description, bar, amount/spinner, elapsed, and ETA.
108
110
  - Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
109
111
  - Determinate amount text shows counters without prefixes: `<succeeded> <failed> <processed>/<total>`.
112
+ - Counts can exceed `total`; the amount text keeps those raw values (for example `12/10`) while the bar stays visually clamped at full.
113
+ - `total: 0` is valid for determinate tasks and renders as a full bar by default.
110
114
  - Column widths are shared per frame (widest visible cell wins), so rows stay aligned.
111
115
  - Elapsed and ETA reserve stable widths to reduce jitter while tasks transition states.
112
116
  - Layout uses a 100-column baseline and grows when content requires more space.
@@ -132,6 +136,12 @@ const program = Progress.task(
132
136
  );
133
137
  ```
134
138
 
139
+ Manual total behavior follows cli-progress-style semantics:
140
+
141
+ - negative totals on task creation fall back to `100`
142
+ - negative totals on later `updateTask` calls are ignored
143
+ - explicit `total: undefined` on `updateTask` clears the total and switches back to indeterminate rendering
144
+
135
145
  ## Column customization
136
146
 
137
147
  Custom column APIs are not part of the first Ink release. The renderer ships with built-in columns only, and old renderer config APIs (`RendererConfig`, `ProgressBarConfig`, custom column definitions) are intentionally removed in this iteration.
package/dist/index.d.mts CHANGED
@@ -10,13 +10,6 @@ declare const TaskStatusSchema: Schema.Literal<["running", "done", "failed"]>;
10
10
  type TaskStatus = typeof TaskStatusSchema.Type;
11
11
  declare const TaskCountDisplaySchema: Schema.Literal<["processedOnly", "detailed"]>;
12
12
  type TaskCountDisplay = typeof TaskCountDisplaySchema.Type;
13
- declare const InvalidTaskTotalError_base: Schema.TaggedErrorClass<InvalidTaskTotalError, "InvalidTaskTotalError", {
14
- readonly _tag: Schema.tag<"InvalidTaskTotalError">;
15
- } & {
16
- total: typeof Schema.Number;
17
- message: typeof Schema.String;
18
- }>;
19
- declare class InvalidTaskTotalError extends InvalidTaskTotalError_base {}
20
13
  interface AddTaskOptions {
21
14
  readonly description: string;
22
15
  readonly total?: number;
@@ -68,8 +61,8 @@ interface TaskStore {
68
61
  readonly renderOrder: ReadonlyArray<RenderRow>;
69
62
  }
70
63
  interface ProgressService {
71
- readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId, InvalidTaskTotalError>;
72
- readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void, InvalidTaskTotalError>;
64
+ readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId>;
65
+ readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void>;
73
66
  readonly incrementSucceeded: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
74
67
  readonly incrementFailed: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
75
68
  readonly completeTask: (taskId: TaskId) => Effect.Effect<void>;
@@ -78,12 +71,12 @@ interface ProgressService {
78
71
  readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
79
72
  readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
80
73
  readonly runTask: {
81
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
82
- <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
74
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E, Exclude<R, Task>>;
75
+ <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
83
76
  };
84
77
  readonly withTask: {
85
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
86
- <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Task>>;
78
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions): Effect.Effect<A, E, Exclude<R, Task>>;
79
+ <A, E, R>(options: AddTaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
87
80
  };
88
81
  }
89
82
  declare const Task_base: Context.TagClass<Task, "stromseng.dev/effective-progress/Task", number & Brand.Brand<"TaskId">>;
@@ -159,15 +152,15 @@ interface EffectAllExecutionOptions extends EffectExecutionOptions {
159
152
  readonly mode?: "default" | "validate" | "either";
160
153
  }
161
154
  type AllOptions = Omit<TrackOptions, "total" | "countDisplay"> & EffectAllExecutionOptions;
162
- type AllReturn<Arg extends ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions> = [[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>] ? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : [Arg] extends [Record<string, Effect.Effect<any, any, any>>] ? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : never] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Progress | Task>> : never;
155
+ type AllReturn<Arg extends ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions> = [[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>] ? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : [Arg] extends [Record<string, Effect.Effect<any, any, any>>] ? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : never] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress | Task>> : never;
163
156
  interface ForEachExecutionOptions extends EffectExecutionOptions {
164
157
  readonly discard?: false | undefined;
165
158
  }
166
159
  type ForEachOptions = Omit<TrackOptions, "countDisplay"> & ForEachExecutionOptions;
167
160
  type TaskOptions = AddTaskOptions;
168
161
  declare const task: {
169
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions): Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
170
- <A, E, R>(options: TaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
162
+ <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
163
+ <A, E, R>(options: TaskOptions): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
171
164
  };
172
165
  type AllArg = ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>;
173
166
  declare const all: {
@@ -175,8 +168,8 @@ declare const all: {
175
168
  <O extends EffectAllExecutionOptions>(options: Omit<TrackOptions, "total" | "countDisplay"> & O): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
176
169
  };
177
170
  declare const forEach: {
178
- <A, B, E, R>(iterable: Iterable<A>, f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): Effect.Effect<ReadonlyArray<B>, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
179
- <A, B, E, R>(f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E | InvalidTaskTotalError, Exclude<R, Progress | Task>>;
171
+ <A, B, E, R>(iterable: Iterable<A>, f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
172
+ <A, B, E, R>(f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
180
173
  };
181
174
  //#endregion
182
175
  //#region src/services/stdio.d.ts
@@ -189,4 +182,4 @@ declare class ProgressStdio extends ProgressStdio_base {
189
182
  static readonly Default: Layer.Layer<ProgressStdio, never, never>;
190
183
  }
191
184
  //#endregion
192
- export { AddTaskOptions, AllOptions, AllReturn, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, InvalidTaskTotalError, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
185
+ export { AddTaskOptions, AllOptions, AllReturn, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
package/dist/index.mjs CHANGED
@@ -10,10 +10,6 @@ const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
10
10
  const TaskId = Brand.nominal();
11
11
  const TaskStatusSchema = Schema.Literal("running", "done", "failed");
12
12
  const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
13
- var InvalidTaskTotalError = class extends Schema.TaggedError()("InvalidTaskTotalError", {
14
- total: Schema.Number,
15
- message: Schema.String
16
- }) {};
17
13
  const TaskUnitsSchema = Schema.Struct({
18
14
  succeeded: Schema.Number,
19
15
  failed: Schema.Number,
@@ -117,74 +113,47 @@ const toRenderSnapshot = (store) => {
117
113
  //#endregion
118
114
  //#region src/ink-renderer/store.ts
119
115
  const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
120
- const INVALID_TASK_TOTAL_MESSAGE = "Task total must be greater than 0 when provided.";
121
- const normalizeTotal = (total) => {
122
- if (total <= 0) return Effect.fail(new InvalidTaskTotalError({
123
- total,
124
- message: INVALID_TASK_TOTAL_MESSAGE
125
- }));
126
- return Effect.succeed(total);
116
+ const DEFAULT_TOTAL = 100;
117
+ const sanitizeTotalOnAdd = (total) => {
118
+ if (total === void 0) return;
119
+ return total < 0 ? DEFAULT_TOTAL : total;
127
120
  };
128
- const normalizeUnits = (counts) => {
129
- const total = counts.total;
130
- if (total === void 0) {
131
- const succeeded = Math.max(0, counts.succeeded);
132
- const failed = Math.max(0, counts.failed);
133
- return Effect.succeed({
134
- succeeded,
135
- failed,
136
- processed: succeeded + failed
137
- });
138
- }
139
- return Effect.map(normalizeTotal(total), (normalizedTotal) => {
140
- const failed = Math.min(normalizedTotal, Math.max(0, counts.failed));
141
- const succeeded = Math.min(normalizedTotal - failed, Math.max(0, counts.succeeded));
142
- return {
143
- succeeded,
144
- failed,
145
- processed: succeeded + failed,
146
- total: normalizedTotal
147
- };
148
- });
121
+ const sanitizeTotalOnUpdate = (currentTotal, nextTotal) => {
122
+ if (nextTotal === void 0) return;
123
+ return nextTotal < 0 ? currentTotal : nextTotal;
149
124
  };
150
- const normalizeUnitsUnsafe = (counts) => {
151
- const total = counts.total;
152
- if (total === void 0) {
153
- const succeeded = Math.max(0, counts.succeeded);
154
- const failed = Math.max(0, counts.failed);
155
- return {
156
- succeeded,
157
- failed,
158
- processed: succeeded + failed
159
- };
160
- }
161
- const failed = Math.min(total, Math.max(0, counts.failed));
162
- const succeeded = Math.min(total - failed, Math.max(0, counts.succeeded));
163
- return {
125
+ const normalizeUnits = (counts) => {
126
+ const succeeded = Math.max(0, counts.succeeded);
127
+ const failed = Math.max(0, counts.failed);
128
+ return counts.total === void 0 ? {
129
+ succeeded,
130
+ failed,
131
+ processed: succeeded + failed
132
+ } : {
164
133
  succeeded,
165
134
  failed,
166
135
  processed: succeeded + failed,
167
- total
136
+ total: counts.total
168
137
  };
169
138
  };
170
139
  const updatedSnapshot = (snapshot, options) => {
171
140
  const currentUnits = snapshot.units;
172
- const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? Effect.succeed(currentUnits) : normalizeUnits({
141
+ const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? currentUnits : normalizeUnits({
173
142
  succeeded: options.succeeded ?? currentUnits.succeeded,
174
143
  failed: options.failed ?? currentUnits.failed,
175
- total: hasExplicitTotal(options) ? options.total : currentUnits.total
144
+ total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(currentUnits.total, options.total) : currentUnits.total
176
145
  });
177
- return Effect.map(units, (resolvedUnits) => new TaskSnapshot({
146
+ return new TaskSnapshot({
178
147
  id: snapshot.id,
179
148
  parentId: snapshot.parentId,
180
149
  description: options.description ?? snapshot.description,
181
150
  status: snapshot.status,
182
151
  countDisplay: options.countDisplay ?? snapshot.countDisplay,
183
152
  transient: options.transient ?? snapshot.transient,
184
- units: resolvedUnits,
153
+ units,
185
154
  startedAt: snapshot.startedAt,
186
155
  completedAt: snapshot.completedAt
187
- }));
156
+ });
188
157
  };
189
158
  const withTransient = (snapshot, transient) => new TaskSnapshot({
190
159
  id: snapshot.id,
@@ -291,10 +260,10 @@ const makeProgressRenderStore = () => {
291
260
  },
292
261
  addTask: (options) => Effect.gen(function* () {
293
262
  const taskId = TaskId(++nextTaskId);
294
- const units = yield* normalizeUnits({
263
+ const units = normalizeUnits({
295
264
  succeeded: 0,
296
265
  failed: 0,
297
- total: options.total
266
+ total: sanitizeTotalOnAdd(options.total)
298
267
  });
299
268
  const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
300
269
  const now = yield* Clock.currentTimeMillis;
@@ -330,7 +299,7 @@ const makeProgressRenderStore = () => {
330
299
  updateTask: (taskId, options) => Effect.gen(function* () {
331
300
  const currentTask = state.tasks.get(taskId);
332
301
  if (!currentTask) return;
333
- const nextTask = yield* updatedSnapshot(currentTask, options);
302
+ const nextTask = updatedSnapshot(currentTask, options);
334
303
  updateState((current) => {
335
304
  if (!current.tasks.get(taskId)) return current;
336
305
  const nextTasks = new Map(current.tasks);
@@ -366,7 +335,7 @@ const makeProgressRenderStore = () => {
366
335
  status: currentTask.status,
367
336
  countDisplay: currentTask.countDisplay,
368
337
  transient: currentTask.transient,
369
- units: normalizeUnitsUnsafe({
338
+ units: normalizeUnits({
370
339
  succeeded: currentTask.units.succeeded + amount,
371
340
  failed: currentTask.units.failed,
372
341
  total: currentTask.units.total
@@ -392,7 +361,7 @@ const makeProgressRenderStore = () => {
392
361
  status: currentTask.status,
393
362
  countDisplay: currentTask.countDisplay,
394
363
  transient: currentTask.transient,
395
- units: normalizeUnitsUnsafe({
364
+ units: normalizeUnits({
396
365
  succeeded: currentTask.units.succeeded,
397
366
  failed: currentTask.units.failed + amount,
398
367
  total: currentTask.units.total
@@ -426,11 +395,11 @@ const makeProgressRenderStore = () => {
426
395
  status: "done",
427
396
  countDisplay: currentTask.countDisplay,
428
397
  transient: currentTask.transient,
429
- units: currentTask.units.total !== void 0 ? normalizeUnitsUnsafe({
430
- succeeded: currentTask.units.total - currentTask.units.failed,
398
+ units: currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
399
+ succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
431
400
  failed: currentTask.units.failed,
432
401
  total: currentTask.units.total
433
- }) : currentTask.units.processed > 0 ? normalizeUnitsUnsafe({
402
+ }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
434
403
  succeeded: currentTask.units.succeeded,
435
404
  failed: currentTask.units.failed,
436
405
  total: currentTask.units.processed
@@ -814,13 +783,14 @@ const createAmountColumnSpec = (context) => {
814
783
  const DEFAULT_BAR_WIDTH = 30;
815
784
  const MIN_BAR_WIDTH = 8;
816
785
  const segmentLengths = (width, total, succeeded, failed) => {
817
- if (total <= 0) return {
818
- succeeded: 0,
786
+ if (total === 0) return {
787
+ succeeded: width,
819
788
  failed: 0,
820
- remaining: width
789
+ remaining: 0
821
790
  };
822
- const succeededEnd = Math.round(succeeded / total * width);
823
- const failedEnd = Math.round((succeeded + failed) / total * width);
791
+ const displayTotal = Math.max(total, succeeded + failed);
792
+ const succeededEnd = Math.round(succeeded / displayTotal * width);
793
+ const failedEnd = Math.round((succeeded + failed) / displayTotal * width);
824
794
  const succeededLength = Math.max(0, Math.min(width, succeededEnd));
825
795
  const failedLength = Math.max(0, Math.min(width, failedEnd) - succeededLength);
826
796
  return {
@@ -1651,4 +1621,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1651
1621
  })));
1652
1622
 
1653
1623
  //#endregion
1654
- export { InvalidTaskTotalError, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1624
+ export { Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Effect-first terminal progress bars with nested multibar support",
5
5
  "homepage": "https://github.com/stromseng/effective-progress#readme",
6
6
  "bugs": {