mepcli 1.3.1 → 1.4.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
@@ -226,7 +226,7 @@ const result = await MepCLI.pipeline()
226
226
 
227
227
  ## Keyboard & Mouse Support
228
228
 
229
- For a detailed list of shortcuts and mouse interactions, please refer to the **[Shortcuts Documentation](./docs/shortcuts.md)**.
229
+ For a detailed list of shortcuts and mouse interactions, please refer to the **[Shortcuts Documentation](./docs/features/shortcuts.md)**.
230
230
 
231
231
  ## License
232
232
 
@@ -1,11 +1,58 @@
1
1
  import { TaskRunner } from './tasks';
2
- export type PipelineAction<Ctx, T> = (context: Ctx, tasks?: TaskRunner) => Promise<T>;
3
- export type PipelineCondition<Ctx> = (context: Ctx) => boolean;
4
- export interface PipelineStep<Ctx> {
5
- name: keyof Ctx;
2
+ export declare const PipelineExit: unique symbol;
3
+ export type PipelineAction<Ctx, T> = (context: Ctx, tasks?: TaskRunner) => Promise<T | typeof PipelineExit> | T | typeof PipelineExit;
4
+ export type PipelineCondition<Ctx> = (context: Readonly<Ctx>) => boolean;
5
+ export interface ValidationSchema {
6
+ parse?: (data: any) => any;
7
+ safeParse?: (data: any) => {
8
+ success: boolean;
9
+ error?: any;
10
+ issues?: any;
11
+ data?: any;
12
+ };
13
+ }
14
+ export type Validator = ((value: any) => boolean | string | Promise<boolean | string>) | ValidationSchema;
15
+ export type Transformer<Ctx> = (value: any, context: Readonly<Ctx>) => any | Promise<any>;
16
+ export interface PipelineStep<Ctx> extends StepConfig<Ctx> {
17
+ name?: keyof Ctx;
6
18
  action: PipelineAction<Ctx, any>;
7
19
  condition?: PipelineCondition<Ctx>;
8
20
  }
21
+ export interface StepConfig<Ctx> {
22
+ validate?: Validator;
23
+ transform?: Transformer<Ctx>;
24
+ onError?: (error: unknown, context: Readonly<Ctx>) => void | Promise<void>;
25
+ fallback?: any | ((error: unknown, context: Readonly<Ctx>) => any | Promise<any>);
26
+ optional?: boolean;
27
+ timeout?: number;
28
+ }
29
+ export interface StepMetadata {
30
+ index: number;
31
+ name?: string;
32
+ type: 'named' | 'anonymous';
33
+ }
34
+ export interface PipelineOptions<Ctx> {
35
+ onStepStart?: (meta: StepMetadata, context: Readonly<Ctx>) => void | Promise<void>;
36
+ onStepComplete?: (meta: StepMetadata, context: Readonly<Ctx>) => void | Promise<void>;
37
+ onError?: (error: unknown, meta: StepMetadata, context: Readonly<Ctx>) => void | Promise<void>;
38
+ validate?: Validator;
39
+ onPipelineStart?: (context: Readonly<Ctx>) => void | Promise<void>;
40
+ onPipelineComplete?: (context: Readonly<Ctx>) => void | Promise<void>;
41
+ signal?: AbortSignal;
42
+ }
43
+ export declare class PipelineValidationError<Ctx = any> extends Error {
44
+ step?: StepMetadata;
45
+ context?: Ctx;
46
+ constructor(message: string, step?: StepMetadata, context?: Ctx);
47
+ }
48
+ export declare class PipelineTimeoutError<Ctx = any> extends Error {
49
+ step?: StepMetadata;
50
+ context?: Ctx;
51
+ constructor(message: string, step?: StepMetadata, context?: Ctx);
52
+ }
53
+ export declare class PipelineAbortError extends Error {
54
+ constructor(message?: string);
55
+ }
9
56
  /**
10
57
  * Pipeline (Workflow Engine)
11
58
  *
@@ -16,20 +63,24 @@ export interface PipelineStep<Ctx> {
16
63
  */
17
64
  export declare class Pipeline<Ctx extends Record<string, any> = Record<string, any>> {
18
65
  private steps;
19
- constructor();
66
+ private options;
67
+ constructor(options?: PipelineOptions<Ctx>);
68
+ /**
69
+ * Adds a named step to the pipeline. The result of the action is stored in the context under the given name.
70
+ */
71
+ step<K extends keyof Ctx>(name: K, action: PipelineAction<Ctx, Ctx[K]> | Pipeline<any>, config?: StepConfig<Ctx>): this;
72
+ /**
73
+ * Adds an anonymous step to the pipeline. The action can mutate the context directly or return a partial context to merge in.
74
+ */
75
+ step(action: PipelineAction<Ctx, void | Partial<Ctx>> | Pipeline<any>, config?: StepConfig<Ctx>): this;
20
76
  /**
21
- * Adds a step to the pipeline.
22
- * @param name The key in the context to store the result.
23
- * @param action The function to execute, returning a Promise (usually a Prompt).
77
+ * Adds a conditional named step to the pipeline.
24
78
  */
25
- step<K extends keyof Ctx>(name: K, action: PipelineAction<Ctx, Ctx[K]>): this;
79
+ stepIf<K extends keyof Ctx>(condition: PipelineCondition<Ctx>, name: K, action: PipelineAction<Ctx, Ctx[K]> | Pipeline<any>, config?: StepConfig<Ctx>): this;
26
80
  /**
27
- * Adds a conditional step to the pipeline.
28
- * @param condition A function that returns true if the step should run.
29
- * @param name The key in the context to store the result.
30
- * @param action The function to execute.
81
+ * Adds a conditional anonymous step to the pipeline.
31
82
  */
32
- stepIf<K extends keyof Ctx>(condition: PipelineCondition<Ctx>, name: K, action: PipelineAction<Ctx, Ctx[K]>): this;
83
+ stepIf(condition: PipelineCondition<Ctx>, action: PipelineAction<Ctx, void | Partial<Ctx>> | Pipeline<any>, config?: StepConfig<Ctx>): this;
33
84
  /**
34
85
  * Executes the pipeline steps sequentially.
35
86
  * @param initialContext Optional initial context.
package/dist/pipeline.js CHANGED
@@ -1,6 +1,32 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Pipeline = void 0;
3
+ exports.Pipeline = exports.PipelineAbortError = exports.PipelineTimeoutError = exports.PipelineValidationError = exports.PipelineExit = void 0;
4
+ exports.PipelineExit = Symbol.for('PipelineExit');
5
+ class PipelineValidationError extends Error {
6
+ constructor(message, step, context) {
7
+ super(message);
8
+ this.name = 'PipelineValidationError';
9
+ this.step = step;
10
+ this.context = context;
11
+ }
12
+ }
13
+ exports.PipelineValidationError = PipelineValidationError;
14
+ class PipelineTimeoutError extends Error {
15
+ constructor(message, step, context) {
16
+ super(message);
17
+ this.name = 'PipelineTimeoutError';
18
+ this.step = step;
19
+ this.context = context;
20
+ }
21
+ }
22
+ exports.PipelineTimeoutError = PipelineTimeoutError;
23
+ class PipelineAbortError extends Error {
24
+ constructor(message = 'Pipeline aborted') {
25
+ super(message);
26
+ this.name = 'PipelineAbortError';
27
+ }
28
+ }
29
+ exports.PipelineAbortError = PipelineAbortError;
4
30
  /**
5
31
  * Pipeline (Workflow Engine)
6
32
  *
@@ -10,26 +36,54 @@ exports.Pipeline = void 0;
10
36
  * Philosophy: Enter-and-Forget (EAF), Zero-Dependency, Method Chaining.
11
37
  */
12
38
  class Pipeline {
13
- constructor() {
39
+ constructor(options) {
14
40
  this.steps = [];
41
+ this.options = options || {};
15
42
  }
16
- /**
17
- * Adds a step to the pipeline.
18
- * @param name The key in the context to store the result.
19
- * @param action The function to execute, returning a Promise (usually a Prompt).
20
- */
21
- step(name, action) {
22
- this.steps.push({ name, action });
43
+ step(nameOrAction, actionOrConfig, config) {
44
+ if (typeof nameOrAction === 'function' || nameOrAction instanceof Pipeline) {
45
+ const action = nameOrAction instanceof Pipeline
46
+ ? ((ctx, tasks) => nameOrAction.run(ctx, tasks))
47
+ : nameOrAction;
48
+ this.steps.push({
49
+ action: action,
50
+ ...(actionOrConfig || {})
51
+ });
52
+ }
53
+ else {
54
+ const action = actionOrConfig instanceof Pipeline
55
+ ? ((ctx, tasks) => actionOrConfig.run(ctx, tasks))
56
+ : actionOrConfig;
57
+ this.steps.push({
58
+ name: nameOrAction,
59
+ action: action,
60
+ ...(config || {})
61
+ });
62
+ }
23
63
  return this;
24
64
  }
25
- /**
26
- * Adds a conditional step to the pipeline.
27
- * @param condition A function that returns true if the step should run.
28
- * @param name The key in the context to store the result.
29
- * @param action The function to execute.
30
- */
31
- stepIf(condition, name, action) {
32
- this.steps.push({ name, action, condition });
65
+ stepIf(condition, nameOrAction, actionOrConfig, config) {
66
+ if (typeof nameOrAction === 'function' || nameOrAction instanceof Pipeline) {
67
+ const action = nameOrAction instanceof Pipeline
68
+ ? ((ctx, tasks) => nameOrAction.run(ctx, tasks))
69
+ : nameOrAction;
70
+ this.steps.push({
71
+ condition,
72
+ action: action,
73
+ ...(actionOrConfig || {})
74
+ });
75
+ }
76
+ else {
77
+ const action = actionOrConfig instanceof Pipeline
78
+ ? ((ctx, tasks) => actionOrConfig.run(ctx, tasks))
79
+ : actionOrConfig;
80
+ this.steps.push({
81
+ condition,
82
+ name: nameOrAction,
83
+ action: action,
84
+ ...(config || {})
85
+ });
86
+ }
33
87
  return this;
34
88
  }
35
89
  /**
@@ -39,14 +93,152 @@ class Pipeline {
39
93
  */
40
94
  async run(initialContext = {}, tasks) {
41
95
  const context = { ...initialContext };
42
- for (const step of this.steps) {
43
- if (step.condition && !step.condition(context)) {
44
- continue;
96
+ if (this.options.onPipelineStart) {
97
+ await this.options.onPipelineStart({ ...context });
98
+ }
99
+ try {
100
+ for (let i = 0; i < this.steps.length; i++) {
101
+ if (this.options.signal?.aborted) {
102
+ throw new PipelineAbortError();
103
+ }
104
+ const step = this.steps[i];
105
+ const meta = {
106
+ index: i,
107
+ name: step.name,
108
+ type: step.name ? 'named' : 'anonymous'
109
+ };
110
+ if (step.condition && !step.condition(context)) {
111
+ continue;
112
+ }
113
+ if (this.options.onStepStart) {
114
+ await this.options.onStepStart(meta, { ...context });
115
+ }
116
+ try {
117
+ let result;
118
+ if (step.timeout && step.timeout > 0) {
119
+ const timeoutPromise = new Promise((_, reject) => {
120
+ setTimeout(() => {
121
+ reject(new PipelineTimeoutError(`Step timed out after ${step.timeout}ms`, meta, context));
122
+ }, step.timeout);
123
+ });
124
+ result = await Promise.race([step.action(context, tasks), timeoutPromise]);
125
+ }
126
+ else {
127
+ result = await step.action(context, tasks);
128
+ }
129
+ if (result === exports.PipelineExit) {
130
+ break;
131
+ }
132
+ if (!step.name) {
133
+ // Anonymous action
134
+ if (result && typeof result === 'object' && !Array.isArray(result)) {
135
+ Object.assign(context, result);
136
+ }
137
+ }
138
+ else {
139
+ // Named action
140
+ if (step.transform) {
141
+ result = await step.transform(result, { ...context });
142
+ }
143
+ if (step.validate) {
144
+ const validator = step.validate;
145
+ if (typeof validator === 'function') {
146
+ const valid = await validator(result);
147
+ if (valid === false) {
148
+ throw new PipelineValidationError(`Validation failed for step at index ${i} (${String(step.name)})`, meta, context);
149
+ }
150
+ if (typeof valid === 'string') {
151
+ throw new PipelineValidationError(valid, meta, context);
152
+ }
153
+ }
154
+ else if (typeof validator === 'object') {
155
+ if (typeof validator.safeParse === 'function') {
156
+ const res = validator.safeParse(result);
157
+ if (!res.success) {
158
+ const errorMsg = res.error?.message || res.issues?.[0]?.message || 'Schema validation failed';
159
+ throw new PipelineValidationError(`Validation failed: ${errorMsg}`, meta, context);
160
+ }
161
+ result = res.data !== undefined ? res.data : result;
162
+ }
163
+ else if (typeof validator.parse === 'function') {
164
+ try {
165
+ const parsed = validator.parse(result);
166
+ result = parsed !== undefined ? parsed : result;
167
+ }
168
+ catch (err) {
169
+ throw new PipelineValidationError(`Validation failed: ${err.message || 'Schema parsing error'}`, meta, context);
170
+ }
171
+ }
172
+ }
173
+ }
174
+ context[step.name] = result;
175
+ }
176
+ if (this.options.onStepComplete) {
177
+ await this.options.onStepComplete(meta, { ...context });
178
+ }
179
+ }
180
+ catch (err) {
181
+ let handled = false;
182
+ if (step.onError) {
183
+ await step.onError(err, { ...context });
184
+ }
185
+ if ('fallback' in step) {
186
+ const fallbackValue = typeof step.fallback === 'function' ? await step.fallback(err, { ...context }) : step.fallback;
187
+ if (step.name) {
188
+ context[step.name] = fallbackValue;
189
+ }
190
+ handled = true;
191
+ }
192
+ else if (step.optional) {
193
+ if (step.name) {
194
+ context[step.name] = undefined;
195
+ }
196
+ handled = true;
197
+ }
198
+ if (!handled) {
199
+ if (this.options.onError) {
200
+ await this.options.onError(err, meta, { ...context });
201
+ }
202
+ throw err;
203
+ }
204
+ }
205
+ }
206
+ if (this.options.validate) {
207
+ const validator = this.options.validate;
208
+ if (typeof validator === 'function') {
209
+ const valid = await validator(context);
210
+ if (valid === false) {
211
+ throw new PipelineValidationError(`Final validation failed`);
212
+ }
213
+ if (typeof valid === 'string') {
214
+ throw new PipelineValidationError(valid);
215
+ }
216
+ }
217
+ else if (typeof validator === 'object') {
218
+ if (typeof validator.safeParse === 'function') {
219
+ const res = validator.safeParse(context);
220
+ if (!res.success) {
221
+ const errorMsg = res.error?.message || res.issues?.[0]?.message || 'Schema validation failed';
222
+ throw new PipelineValidationError(`Final validation failed: ${errorMsg}`);
223
+ }
224
+ Object.assign(context, res.data !== undefined ? res.data : context);
225
+ }
226
+ else if (typeof validator.parse === 'function') {
227
+ try {
228
+ const parsed = validator.parse(context);
229
+ Object.assign(context, parsed !== undefined ? parsed : context);
230
+ }
231
+ catch (err) {
232
+ throw new PipelineValidationError(`Final validation failed: ${err.message || 'Schema parsing error'}`);
233
+ }
234
+ }
235
+ }
236
+ }
237
+ }
238
+ finally {
239
+ if (this.options.onPipelineComplete) {
240
+ await this.options.onPipelineComplete({ ...context });
45
241
  }
46
- // In EAF (Enter-And-Forget) philosophy, if a prompt fails or is cancelled,
47
- // the exception will propagate up and stop the pipeline.
48
- const result = await step.action(context, tasks);
49
- context[step.name] = result;
50
242
  }
51
243
  return context;
52
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mepcli",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "Zero-dependency, interactive CLI prompt",
5
5
  "repository": {
6
6
  "type": "git",
@@ -55,4 +55,4 @@
55
55
  "typescript": "^5",
56
56
  "typescript-eslint": "^8"
57
57
  }
58
- }
58
+ }