@portel/photon-core 1.1.0 → 1.2.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/dist/generator.d.ts +587 -107
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js +313 -151
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/generator.ts +779 -220
- package/src/index.ts +50 -13
package/src/generator.ts
CHANGED
|
@@ -1,192 +1,651 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generator-based Tool Execution
|
|
2
|
+
* Generator-based Tool Execution with Ask/Emit Pattern
|
|
3
3
|
*
|
|
4
|
-
* Enables photon tools to use generator functions with `yield` for:
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* - Streaming responses
|
|
8
|
-
* - Multi-step wizards
|
|
4
|
+
* Enables photon tools to use async generator functions with `yield` for:
|
|
5
|
+
* - Interactive user input (ask) - blocks until user responds
|
|
6
|
+
* - Real-time output (emit) - fire and forget, no response needed
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* ══════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
* DESIGN PHILOSOPHY
|
|
10
|
+
* ══════════════════════════════════════════════════════════════════════════════
|
|
11
|
+
*
|
|
12
|
+
* The `ask` vs `emit` pattern provides instant clarity:
|
|
13
|
+
* - `ask` = "I need something FROM the user" (blocks, returns value)
|
|
14
|
+
* - `emit` = "I'm sending something TO the user" (non-blocking, void)
|
|
15
|
+
*
|
|
16
|
+
* This maps naturally to all runtime contexts:
|
|
17
|
+
*
|
|
18
|
+
* | Runtime | ask (input) | emit (output) |
|
|
19
|
+
* |------------|--------------------------|----------------------------|
|
|
20
|
+
* | REST API | Returns 202 + continue | Included in response or SSE|
|
|
21
|
+
* | WebSocket | Server request → client | Server push to client |
|
|
22
|
+
* | CLI | Readline prompt | Console output |
|
|
23
|
+
* | MCP | Elicitation dialog | Notification/logging |
|
|
24
|
+
* | Chatbot | Bot question → user reply| Status message, typing... |
|
|
25
|
+
*
|
|
26
|
+
* ══════════════════════════════════════════════════════════════════════════════
|
|
27
|
+
* REST API CONTINUATION PATTERN
|
|
28
|
+
* ══════════════════════════════════════════════════════════════════════════════
|
|
29
|
+
*
|
|
30
|
+
* When a generator yields `ask`, REST APIs can implement a continuation flow:
|
|
31
|
+
*
|
|
32
|
+
* ```
|
|
33
|
+
* POST /api/google-tv/connect
|
|
34
|
+
* Body: { ip: "192.168.1.100" }
|
|
35
|
+
*
|
|
36
|
+
* Response (202 Accepted):
|
|
37
|
+
* {
|
|
38
|
+
* "status": "awaiting_input",
|
|
39
|
+
* "continuation_id": "ctx_abc123",
|
|
40
|
+
* "ask": { "type": "text", "id": "pairing_code", "message": "Enter code:" },
|
|
41
|
+
* "continue": "/api/google-tv/connect/ctx_abc123"
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* POST /api/google-tv/connect/ctx_abc123
|
|
45
|
+
* Body: { "pairing_code": "123456" }
|
|
46
|
+
*
|
|
47
|
+
* Response (200 OK):
|
|
48
|
+
* { "status": "complete", "result": { "success": true } }
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* ══════════════════════════════════════════════════════════════════════════════
|
|
52
|
+
* USAGE EXAMPLE
|
|
53
|
+
* ══════════════════════════════════════════════════════════════════════════════
|
|
15
54
|
*
|
|
16
|
-
* @example
|
|
17
55
|
* ```typescript
|
|
18
56
|
* async *connect(params: { ip: string }) {
|
|
19
|
-
*
|
|
57
|
+
* yield { emit: 'status', message: 'Connecting to TV...' };
|
|
58
|
+
*
|
|
59
|
+
* await this.startPairing(params.ip);
|
|
20
60
|
*
|
|
61
|
+
* yield { emit: 'progress', value: 0.3, message: 'Waiting for code...' };
|
|
62
|
+
*
|
|
63
|
+
* // Blocks until user provides input
|
|
21
64
|
* const code: string = yield {
|
|
22
|
-
*
|
|
23
|
-
*
|
|
65
|
+
* ask: 'text',
|
|
66
|
+
* id: 'pairing_code',
|
|
67
|
+
* message: 'Enter the 6-digit code shown on TV:',
|
|
68
|
+
* pattern: '^[0-9]{6}$',
|
|
69
|
+
* required: true
|
|
24
70
|
* };
|
|
25
71
|
*
|
|
72
|
+
* yield { emit: 'status', message: 'Verifying code...' };
|
|
73
|
+
*
|
|
26
74
|
* await this.sendCode(code);
|
|
27
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* yield { emit: 'toast', message: 'Connected!', type: 'success' };
|
|
77
|
+
*
|
|
78
|
+
* return { success: true, paired: true };
|
|
28
79
|
* }
|
|
29
80
|
* ```
|
|
81
|
+
*
|
|
82
|
+
* @module generator
|
|
30
83
|
*/
|
|
31
84
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
85
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
86
|
+
// ASK YIELDS - Input from user (blocks until response)
|
|
87
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
35
88
|
|
|
36
89
|
/**
|
|
37
|
-
*
|
|
90
|
+
* Base properties shared by all ask yields
|
|
38
91
|
*/
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
92
|
+
interface AskBase {
|
|
93
|
+
/**
|
|
94
|
+
* Unique identifier for this input.
|
|
95
|
+
* Used for:
|
|
96
|
+
* - REST API parameter mapping (pre-provided inputs)
|
|
97
|
+
* - Continuation token correlation
|
|
98
|
+
* - Form field identification
|
|
99
|
+
*
|
|
100
|
+
* Auto-generated if not provided (ask_0, ask_1, etc.)
|
|
101
|
+
*/
|
|
44
102
|
id?: string;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The prompt message shown to the user.
|
|
106
|
+
* Should be clear and actionable.
|
|
107
|
+
*/
|
|
108
|
+
message: string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether this input is required.
|
|
112
|
+
* If false, user can skip/cancel.
|
|
113
|
+
* @default true
|
|
114
|
+
*/
|
|
48
115
|
required?: boolean;
|
|
49
116
|
}
|
|
50
117
|
|
|
51
118
|
/**
|
|
52
|
-
*
|
|
119
|
+
* Text input - single line string
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const name: string = yield {
|
|
123
|
+
* ask: 'text',
|
|
124
|
+
* message: 'Enter your name:',
|
|
125
|
+
* default: 'Guest',
|
|
126
|
+
* placeholder: 'John Doe'
|
|
127
|
+
* };
|
|
128
|
+
*/
|
|
129
|
+
export interface AskText extends AskBase {
|
|
130
|
+
ask: 'text';
|
|
131
|
+
/** Default value if user submits empty */
|
|
132
|
+
default?: string;
|
|
133
|
+
/** Placeholder hint shown in input field */
|
|
134
|
+
placeholder?: string;
|
|
135
|
+
/** Regex pattern for validation */
|
|
136
|
+
pattern?: string;
|
|
137
|
+
/** Minimum length */
|
|
138
|
+
minLength?: number;
|
|
139
|
+
/** Maximum length */
|
|
140
|
+
maxLength?: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Password input - hidden/masked string
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* const apiKey: string = yield {
|
|
148
|
+
* ask: 'password',
|
|
149
|
+
* message: 'Enter your API key:'
|
|
150
|
+
* };
|
|
151
|
+
*/
|
|
152
|
+
export interface AskPassword extends AskBase {
|
|
153
|
+
ask: 'password';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Confirmation - yes/no boolean
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const confirmed: boolean = yield {
|
|
161
|
+
* ask: 'confirm',
|
|
162
|
+
* message: 'Delete this file permanently?',
|
|
163
|
+
* dangerous: true
|
|
164
|
+
* };
|
|
53
165
|
*/
|
|
54
|
-
export interface
|
|
55
|
-
|
|
56
|
-
/**
|
|
166
|
+
export interface AskConfirm extends AskBase {
|
|
167
|
+
ask: 'confirm';
|
|
168
|
+
/**
|
|
169
|
+
* Mark as dangerous/destructive action.
|
|
170
|
+
* UI may show warning styling (red button, confirmation dialog).
|
|
171
|
+
*/
|
|
57
172
|
dangerous?: boolean;
|
|
58
|
-
|
|
173
|
+
/** Default value if user just presses enter */
|
|
174
|
+
default?: boolean;
|
|
59
175
|
}
|
|
60
176
|
|
|
61
177
|
/**
|
|
62
|
-
* Selection from options
|
|
178
|
+
* Selection from predefined options
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* // Simple string options
|
|
182
|
+
* const env: string = yield {
|
|
183
|
+
* ask: 'select',
|
|
184
|
+
* message: 'Choose environment:',
|
|
185
|
+
* options: ['development', 'staging', 'production']
|
|
186
|
+
* };
|
|
187
|
+
*
|
|
188
|
+
* // Rich options with labels
|
|
189
|
+
* const region: string = yield {
|
|
190
|
+
* ask: 'select',
|
|
191
|
+
* message: 'Select region:',
|
|
192
|
+
* options: [
|
|
193
|
+
* { value: 'us-east-1', label: 'US East (N. Virginia)' },
|
|
194
|
+
* { value: 'eu-west-1', label: 'EU West (Ireland)' }
|
|
195
|
+
* ]
|
|
196
|
+
* };
|
|
197
|
+
*
|
|
198
|
+
* // Multi-select
|
|
199
|
+
* const features: string[] = yield {
|
|
200
|
+
* ask: 'select',
|
|
201
|
+
* message: 'Enable features:',
|
|
202
|
+
* options: ['auth', 'logging', 'metrics'],
|
|
203
|
+
* multi: true
|
|
204
|
+
* };
|
|
63
205
|
*/
|
|
64
|
-
export interface
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
206
|
+
export interface AskSelect extends AskBase {
|
|
207
|
+
ask: 'select';
|
|
208
|
+
/** Available options */
|
|
209
|
+
options: Array<string | { value: string; label: string; description?: string }>;
|
|
210
|
+
/** Allow selecting multiple options */
|
|
68
211
|
multi?: boolean;
|
|
69
|
-
|
|
212
|
+
/** Default selected value(s) */
|
|
213
|
+
default?: string | string[];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Number input with optional constraints
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* const quantity: number = yield {
|
|
221
|
+
* ask: 'number',
|
|
222
|
+
* message: 'Enter quantity:',
|
|
223
|
+
* min: 1,
|
|
224
|
+
* max: 100,
|
|
225
|
+
* step: 1
|
|
226
|
+
* };
|
|
227
|
+
*/
|
|
228
|
+
export interface AskNumber extends AskBase {
|
|
229
|
+
ask: 'number';
|
|
230
|
+
/** Minimum value */
|
|
231
|
+
min?: number;
|
|
232
|
+
/** Maximum value */
|
|
233
|
+
max?: number;
|
|
234
|
+
/** Step increment */
|
|
235
|
+
step?: number;
|
|
236
|
+
/** Default value */
|
|
237
|
+
default?: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* File selection (for supported runtimes)
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* const file: FileInfo = yield {
|
|
245
|
+
* ask: 'file',
|
|
246
|
+
* message: 'Select a document:',
|
|
247
|
+
* accept: '.pdf,.doc,.docx',
|
|
248
|
+
* multiple: false
|
|
249
|
+
* };
|
|
250
|
+
*/
|
|
251
|
+
export interface AskFile extends AskBase {
|
|
252
|
+
ask: 'file';
|
|
253
|
+
/** Accepted file types (MIME types or extensions) */
|
|
254
|
+
accept?: string;
|
|
255
|
+
/** Allow multiple file selection */
|
|
256
|
+
multiple?: boolean;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Date/time selection
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* const date: string = yield {
|
|
264
|
+
* ask: 'date',
|
|
265
|
+
* message: 'Select delivery date:',
|
|
266
|
+
* min: '2024-01-01',
|
|
267
|
+
* max: '2024-12-31'
|
|
268
|
+
* };
|
|
269
|
+
*/
|
|
270
|
+
export interface AskDate extends AskBase {
|
|
271
|
+
ask: 'date';
|
|
272
|
+
/** Include time selection */
|
|
273
|
+
includeTime?: boolean;
|
|
274
|
+
/** Minimum date (ISO string) */
|
|
275
|
+
min?: string;
|
|
276
|
+
/** Maximum date (ISO string) */
|
|
277
|
+
max?: string;
|
|
278
|
+
/** Default value (ISO string) */
|
|
279
|
+
default?: string;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Union of all ask (input) yield types
|
|
284
|
+
*/
|
|
285
|
+
export type AskYield =
|
|
286
|
+
| AskText
|
|
287
|
+
| AskPassword
|
|
288
|
+
| AskConfirm
|
|
289
|
+
| AskSelect
|
|
290
|
+
| AskNumber
|
|
291
|
+
| AskFile
|
|
292
|
+
| AskDate;
|
|
293
|
+
|
|
294
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
295
|
+
// EMIT YIELDS - Output to user (fire and forget)
|
|
296
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Status message - general purpose user notification
|
|
300
|
+
*
|
|
301
|
+
* Use for: progress updates, step completions, informational messages
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* yield { emit: 'status', message: 'Connecting to server...' };
|
|
305
|
+
* yield { emit: 'status', message: 'Upload complete!', type: 'success' };
|
|
306
|
+
*/
|
|
307
|
+
export interface EmitStatus {
|
|
308
|
+
emit: 'status';
|
|
309
|
+
/** Message to display */
|
|
310
|
+
message: string;
|
|
311
|
+
/** Message type for styling */
|
|
312
|
+
type?: 'info' | 'success' | 'warning' | 'error';
|
|
70
313
|
}
|
|
71
314
|
|
|
72
315
|
/**
|
|
73
|
-
* Progress update
|
|
316
|
+
* Progress update - for long-running operations
|
|
317
|
+
*
|
|
318
|
+
* Runtimes may display as: progress bar, percentage, spinner
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* yield { emit: 'progress', value: 0.0, message: 'Starting...' };
|
|
322
|
+
* yield { emit: 'progress', value: 0.5, message: 'Halfway there...' };
|
|
323
|
+
* yield { emit: 'progress', value: 1.0, message: 'Complete!' };
|
|
74
324
|
*/
|
|
75
|
-
export interface
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
325
|
+
export interface EmitProgress {
|
|
326
|
+
emit: 'progress';
|
|
327
|
+
/** Progress value from 0 to 1 (0% to 100%) */
|
|
328
|
+
value: number;
|
|
329
|
+
/** Optional status message */
|
|
330
|
+
message?: string;
|
|
331
|
+
/** Additional metadata */
|
|
332
|
+
meta?: Record<string, any>;
|
|
80
333
|
}
|
|
81
334
|
|
|
82
335
|
/**
|
|
83
|
-
*
|
|
336
|
+
* Streaming data - for chunked responses
|
|
337
|
+
*
|
|
338
|
+
* Use for: streaming text, large file transfers, real-time data
|
|
339
|
+
*
|
|
340
|
+
* @example
|
|
341
|
+
* for await (const chunk of aiStream) {
|
|
342
|
+
* yield { emit: 'stream', data: chunk.text };
|
|
343
|
+
* }
|
|
344
|
+
* yield { emit: 'stream', data: '', final: true };
|
|
84
345
|
*/
|
|
85
|
-
export interface
|
|
86
|
-
|
|
346
|
+
export interface EmitStream {
|
|
347
|
+
emit: 'stream';
|
|
348
|
+
/** Data chunk to send */
|
|
349
|
+
data: any;
|
|
87
350
|
/** Whether this is the final chunk */
|
|
88
351
|
final?: boolean;
|
|
352
|
+
/** Content type hint */
|
|
353
|
+
contentType?: string;
|
|
89
354
|
}
|
|
90
355
|
|
|
91
356
|
/**
|
|
92
|
-
* Log
|
|
357
|
+
* Log message - for debugging/development
|
|
358
|
+
*
|
|
359
|
+
* May be hidden in production or routed to logging system
|
|
360
|
+
*
|
|
361
|
+
* @example
|
|
362
|
+
* yield { emit: 'log', message: 'Processing item', level: 'debug', data: { id: 123 } };
|
|
93
363
|
*/
|
|
94
|
-
export interface
|
|
95
|
-
|
|
364
|
+
export interface EmitLog {
|
|
365
|
+
emit: 'log';
|
|
366
|
+
/** Log message */
|
|
367
|
+
message: string;
|
|
368
|
+
/** Log level */
|
|
96
369
|
level?: 'debug' | 'info' | 'warn' | 'error';
|
|
370
|
+
/** Additional structured data */
|
|
371
|
+
data?: Record<string, any>;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Toast notification - ephemeral popup message
|
|
376
|
+
*
|
|
377
|
+
* Use for: success confirmations, quick alerts, non-blocking notices
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* yield { emit: 'toast', message: 'Settings saved!', type: 'success' };
|
|
381
|
+
* yield { emit: 'toast', message: 'Connection lost', type: 'error', duration: 5000 };
|
|
382
|
+
*/
|
|
383
|
+
export interface EmitToast {
|
|
384
|
+
emit: 'toast';
|
|
385
|
+
/** Toast message */
|
|
386
|
+
message: string;
|
|
387
|
+
/** Toast type for styling */
|
|
388
|
+
type?: 'info' | 'success' | 'warning' | 'error';
|
|
389
|
+
/** Display duration in ms (0 = sticky) */
|
|
390
|
+
duration?: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Thinking indicator - for chatbot/AI contexts
|
|
395
|
+
*
|
|
396
|
+
* Shows user that processing is happening (typing dots, spinner)
|
|
397
|
+
*
|
|
398
|
+
* @example
|
|
399
|
+
* yield { emit: 'thinking', active: true };
|
|
400
|
+
* const result = await this.heavyComputation();
|
|
401
|
+
* yield { emit: 'thinking', active: false };
|
|
402
|
+
*/
|
|
403
|
+
export interface EmitThinking {
|
|
404
|
+
emit: 'thinking';
|
|
405
|
+
/** Whether thinking indicator should be shown */
|
|
406
|
+
active: boolean;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Rich artifact - embedded content preview
|
|
411
|
+
*
|
|
412
|
+
* Use for: images, code blocks, documents, embeds
|
|
413
|
+
*
|
|
414
|
+
* @example
|
|
415
|
+
* yield {
|
|
416
|
+
* emit: 'artifact',
|
|
417
|
+
* type: 'image',
|
|
418
|
+
* url: 'https://example.com/chart.png',
|
|
419
|
+
* title: 'Sales Chart Q4'
|
|
420
|
+
* };
|
|
421
|
+
*
|
|
422
|
+
* yield {
|
|
423
|
+
* emit: 'artifact',
|
|
424
|
+
* type: 'code',
|
|
425
|
+
* language: 'typescript',
|
|
426
|
+
* content: 'const x = 1;',
|
|
427
|
+
* title: 'Example'
|
|
428
|
+
* };
|
|
429
|
+
*/
|
|
430
|
+
export interface EmitArtifact {
|
|
431
|
+
emit: 'artifact';
|
|
432
|
+
/** Artifact type */
|
|
433
|
+
type: 'image' | 'code' | 'document' | 'embed' | 'json';
|
|
434
|
+
/** Title/label */
|
|
435
|
+
title?: string;
|
|
436
|
+
/** URL for external content */
|
|
437
|
+
url?: string;
|
|
438
|
+
/** Inline content */
|
|
439
|
+
content?: string;
|
|
440
|
+
/** Language hint for code */
|
|
441
|
+
language?: string;
|
|
442
|
+
/** MIME type hint */
|
|
443
|
+
mimeType?: string;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Union of all emit (output) yield types
|
|
448
|
+
*/
|
|
449
|
+
export type EmitYield =
|
|
450
|
+
| EmitStatus
|
|
451
|
+
| EmitProgress
|
|
452
|
+
| EmitStream
|
|
453
|
+
| EmitLog
|
|
454
|
+
| EmitToast
|
|
455
|
+
| EmitThinking
|
|
456
|
+
| EmitArtifact;
|
|
457
|
+
|
|
458
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
459
|
+
// COMBINED TYPES
|
|
460
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* All possible yield types from a photon generator
|
|
464
|
+
*/
|
|
465
|
+
export type PhotonYield = AskYield | EmitYield;
|
|
466
|
+
|
|
467
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
468
|
+
// TYPE GUARDS - Check what kind of yield we have
|
|
469
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Check if yield is an ask (requires user input)
|
|
473
|
+
*
|
|
474
|
+
* @example
|
|
475
|
+
* if (isAskYield(yielded)) {
|
|
476
|
+
* const userInput = await promptUser(yielded);
|
|
477
|
+
* generator.next(userInput);
|
|
478
|
+
* }
|
|
479
|
+
*/
|
|
480
|
+
export function isAskYield(y: PhotonYield): y is AskYield {
|
|
481
|
+
return 'ask' in y;
|
|
97
482
|
}
|
|
98
483
|
|
|
99
484
|
/**
|
|
100
|
-
*
|
|
485
|
+
* Check if yield is an emit (output only, no response needed)
|
|
486
|
+
*
|
|
487
|
+
* @example
|
|
488
|
+
* if (isEmitYield(yielded)) {
|
|
489
|
+
* handleOutput(yielded);
|
|
490
|
+
* generator.next(); // Continue without value
|
|
491
|
+
* }
|
|
101
492
|
*/
|
|
102
|
-
export
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
| SelectYield
|
|
106
|
-
| ProgressYield
|
|
107
|
-
| StreamYield
|
|
108
|
-
| LogYield;
|
|
493
|
+
export function isEmitYield(y: PhotonYield): y is EmitYield {
|
|
494
|
+
return 'emit' in y;
|
|
495
|
+
}
|
|
109
496
|
|
|
110
497
|
/**
|
|
111
|
-
*
|
|
498
|
+
* Get the type of an ask yield
|
|
112
499
|
*/
|
|
113
|
-
export function
|
|
114
|
-
return
|
|
500
|
+
export function getAskType(y: AskYield): AskYield['ask'] {
|
|
501
|
+
return y.ask;
|
|
115
502
|
}
|
|
116
503
|
|
|
117
504
|
/**
|
|
118
|
-
*
|
|
505
|
+
* Get the type of an emit yield
|
|
119
506
|
*/
|
|
120
|
-
export function
|
|
121
|
-
return
|
|
507
|
+
export function getEmitType(y: EmitYield): EmitYield['emit'] {
|
|
508
|
+
return y.emit;
|
|
122
509
|
}
|
|
123
510
|
|
|
511
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
512
|
+
// GENERATOR DETECTION
|
|
513
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
514
|
+
|
|
124
515
|
/**
|
|
125
|
-
* Check if a
|
|
516
|
+
* Check if a function is an async generator function
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* if (isAsyncGeneratorFunction(method)) {
|
|
520
|
+
* const gen = method.call(instance, params);
|
|
521
|
+
* await executeGenerator(gen, config);
|
|
522
|
+
* }
|
|
126
523
|
*/
|
|
127
|
-
export function
|
|
128
|
-
|
|
524
|
+
export function isAsyncGeneratorFunction(fn: any): fn is (...args: any[]) => AsyncGenerator {
|
|
525
|
+
if (!fn) return false;
|
|
526
|
+
const constructor = fn.constructor;
|
|
527
|
+
if (!constructor) return false;
|
|
528
|
+
if (constructor.name === 'AsyncGeneratorFunction') return true;
|
|
529
|
+
const prototype = Object.getPrototypeOf(fn);
|
|
530
|
+
return prototype?.constructor?.name === 'AsyncGeneratorFunction';
|
|
129
531
|
}
|
|
130
532
|
|
|
131
533
|
/**
|
|
132
|
-
* Check if a
|
|
534
|
+
* Check if a value is an async generator instance (already invoked)
|
|
535
|
+
*
|
|
536
|
+
* @example
|
|
537
|
+
* const result = method.call(instance, params);
|
|
538
|
+
* if (isAsyncGenerator(result)) {
|
|
539
|
+
* await executeGenerator(result, config);
|
|
540
|
+
* }
|
|
133
541
|
*/
|
|
134
|
-
export function
|
|
135
|
-
return
|
|
542
|
+
export function isAsyncGenerator(obj: any): obj is AsyncGenerator {
|
|
543
|
+
return obj &&
|
|
544
|
+
typeof obj.next === 'function' &&
|
|
545
|
+
typeof obj.return === 'function' &&
|
|
546
|
+
typeof obj.throw === 'function' &&
|
|
547
|
+
typeof obj[Symbol.asyncIterator] === 'function';
|
|
136
548
|
}
|
|
137
549
|
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
550
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
551
|
+
// INPUT PROVIDER - How runtimes supply values for ask yields
|
|
552
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
141
553
|
|
|
142
554
|
/**
|
|
143
|
-
* Function that provides input for
|
|
144
|
-
*
|
|
555
|
+
* Function that provides input for an ask yield.
|
|
556
|
+
*
|
|
557
|
+
* Runtimes implement this based on their capabilities:
|
|
558
|
+
* - CLI: readline prompts
|
|
559
|
+
* - MCP: elicitation dialogs
|
|
560
|
+
* - WebSocket: request/response messages
|
|
561
|
+
* - REST: throw NeedsInputError for continuation flow
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* const cliInputProvider: InputProvider = async (ask) => {
|
|
565
|
+
* if (ask.ask === 'text') return await readline(ask.message);
|
|
566
|
+
* if (ask.ask === 'confirm') return await confirm(ask.message);
|
|
567
|
+
* // ...
|
|
568
|
+
* };
|
|
145
569
|
*/
|
|
146
|
-
export type InputProvider = (
|
|
570
|
+
export type InputProvider = (ask: AskYield) => Promise<any>;
|
|
147
571
|
|
|
148
572
|
/**
|
|
149
|
-
* Handler for
|
|
573
|
+
* Handler for emit yields (output).
|
|
574
|
+
*
|
|
575
|
+
* Runtimes implement this to handle output:
|
|
576
|
+
* - CLI: console.log, progress bar
|
|
577
|
+
* - WebSocket: push message to client
|
|
578
|
+
* - REST: collect for response or send via SSE
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* const cliOutputHandler: OutputHandler = (emit) => {
|
|
582
|
+
* if (emit.emit === 'status') console.log(emit.message);
|
|
583
|
+
* if (emit.emit === 'progress') updateProgressBar(emit.value);
|
|
584
|
+
* };
|
|
150
585
|
*/
|
|
151
|
-
export type OutputHandler = (
|
|
586
|
+
export type OutputHandler = (emit: EmitYield) => void | Promise<void>;
|
|
152
587
|
|
|
153
588
|
/**
|
|
154
589
|
* Configuration for generator execution
|
|
155
590
|
*/
|
|
156
591
|
export interface GeneratorExecutorConfig {
|
|
157
|
-
/**
|
|
592
|
+
/**
|
|
593
|
+
* Provides input values for ask yields.
|
|
594
|
+
* Required unless all asks are pre-provided.
|
|
595
|
+
*/
|
|
158
596
|
inputProvider: InputProvider;
|
|
159
|
-
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Handles emit yields (optional).
|
|
600
|
+
* If not provided, emits are silently ignored.
|
|
601
|
+
*/
|
|
160
602
|
outputHandler?: OutputHandler;
|
|
161
|
-
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Pre-provided inputs keyed by ask id.
|
|
606
|
+
* Used by REST APIs to pass all inputs upfront.
|
|
607
|
+
*
|
|
608
|
+
* @example
|
|
609
|
+
* // If photon yields { ask: 'text', id: 'name', message: '...' }
|
|
610
|
+
* // and preProvidedInputs = { name: 'John' }
|
|
611
|
+
* // The generator receives 'John' without calling inputProvider
|
|
612
|
+
*/
|
|
162
613
|
preProvidedInputs?: Record<string, any>;
|
|
163
|
-
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Timeout for waiting on input (ms).
|
|
617
|
+
* @default 300000 (5 minutes)
|
|
618
|
+
*/
|
|
164
619
|
inputTimeout?: number;
|
|
165
620
|
}
|
|
166
621
|
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
//
|
|
622
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
623
|
+
// GENERATOR EXECUTOR - Runs generator tools to completion
|
|
624
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
170
625
|
|
|
171
626
|
/**
|
|
172
|
-
* Execute a generator-based tool
|
|
627
|
+
* Execute a generator-based photon tool to completion.
|
|
628
|
+
*
|
|
629
|
+
* Handles the yield/resume loop:
|
|
630
|
+
* 1. Run generator until it yields
|
|
631
|
+
* 2. If ask yield: get input from provider (or pre-provided), resume with value
|
|
632
|
+
* 3. If emit yield: call output handler, resume without value
|
|
633
|
+
* 4. Repeat until generator returns
|
|
173
634
|
*
|
|
174
635
|
* @param generator - The async generator to execute
|
|
175
636
|
* @param config - Configuration for handling yields
|
|
176
637
|
* @returns The final return value of the generator
|
|
177
638
|
*
|
|
178
639
|
* @example
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* if ('
|
|
183
|
-
* if ('confirm' in y) return await confirm(y.confirm);
|
|
640
|
+
* const result = await executeGenerator(photon.connect({ ip: '192.168.1.1' }), {
|
|
641
|
+
* inputProvider: async (ask) => {
|
|
642
|
+
* if (ask.ask === 'text') return await readline(ask.message);
|
|
643
|
+
* if (ask.ask === 'confirm') return await confirm(ask.message);
|
|
184
644
|
* },
|
|
185
|
-
* outputHandler: (
|
|
186
|
-
* if ('progress'
|
|
645
|
+
* outputHandler: (emit) => {
|
|
646
|
+
* if (emit.emit === 'progress') console.log(`${emit.value * 100}%`);
|
|
187
647
|
* }
|
|
188
648
|
* });
|
|
189
|
-
* ```
|
|
190
649
|
*/
|
|
191
650
|
export async function executeGenerator<T>(
|
|
192
651
|
generator: AsyncGenerator<PhotonYield, T, any>,
|
|
@@ -194,20 +653,20 @@ export async function executeGenerator<T>(
|
|
|
194
653
|
): Promise<T> {
|
|
195
654
|
const { inputProvider, outputHandler, preProvidedInputs } = config;
|
|
196
655
|
|
|
197
|
-
let
|
|
656
|
+
let askIndex = 0;
|
|
198
657
|
let result = await generator.next();
|
|
199
658
|
|
|
200
659
|
while (!result.done) {
|
|
201
660
|
const yielded = result.value;
|
|
202
661
|
|
|
203
|
-
// Handle
|
|
204
|
-
if (
|
|
205
|
-
// Generate
|
|
206
|
-
const
|
|
662
|
+
// Handle ask yields (need input)
|
|
663
|
+
if (isAskYield(yielded)) {
|
|
664
|
+
// Generate id if not provided
|
|
665
|
+
const askId = yielded.id || `ask_${askIndex++}`;
|
|
207
666
|
|
|
208
667
|
// Check for pre-provided input (REST API style)
|
|
209
|
-
if (preProvidedInputs &&
|
|
210
|
-
result = await generator.next(preProvidedInputs[
|
|
668
|
+
if (preProvidedInputs && askId in preProvidedInputs) {
|
|
669
|
+
result = await generator.next(preProvidedInputs[askId]);
|
|
211
670
|
continue;
|
|
212
671
|
}
|
|
213
672
|
|
|
@@ -215,80 +674,64 @@ export async function executeGenerator<T>(
|
|
|
215
674
|
const input = await inputProvider(yielded);
|
|
216
675
|
result = await generator.next(input);
|
|
217
676
|
}
|
|
218
|
-
// Handle
|
|
219
|
-
else {
|
|
677
|
+
// Handle emit yields (output only)
|
|
678
|
+
else if (isEmitYield(yielded)) {
|
|
220
679
|
if (outputHandler) {
|
|
221
680
|
await outputHandler(yielded);
|
|
222
681
|
}
|
|
223
682
|
// Continue without providing a value
|
|
224
683
|
result = await generator.next();
|
|
225
684
|
}
|
|
685
|
+
// Unknown yield type - skip
|
|
686
|
+
else {
|
|
687
|
+
console.warn('[generator] Unknown yield type:', yielded);
|
|
688
|
+
result = await generator.next();
|
|
689
|
+
}
|
|
226
690
|
}
|
|
227
691
|
|
|
228
692
|
return result.value;
|
|
229
693
|
}
|
|
230
694
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Check if a function is an async generator function
|
|
237
|
-
*/
|
|
238
|
-
export function isAsyncGeneratorFunction(fn: any): fn is (...args: any[]) => AsyncGenerator {
|
|
239
|
-
if (!fn) return false;
|
|
240
|
-
const constructor = fn.constructor;
|
|
241
|
-
if (!constructor) return false;
|
|
242
|
-
if (constructor.name === 'AsyncGeneratorFunction') return true;
|
|
243
|
-
// Check prototype chain
|
|
244
|
-
const prototype = Object.getPrototypeOf(fn);
|
|
245
|
-
return prototype && prototype.constructor &&
|
|
246
|
-
prototype.constructor.name === 'AsyncGeneratorFunction';
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Check if a value is an async generator (already invoked)
|
|
251
|
-
*/
|
|
252
|
-
export function isAsyncGenerator(obj: any): obj is AsyncGenerator {
|
|
253
|
-
return obj &&
|
|
254
|
-
typeof obj.next === 'function' &&
|
|
255
|
-
typeof obj.return === 'function' &&
|
|
256
|
-
typeof obj.throw === 'function' &&
|
|
257
|
-
typeof obj[Symbol.asyncIterator] === 'function';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// ============================================================================
|
|
261
|
-
// Yield Extraction - Extract yields from generator for schema generation
|
|
262
|
-
// ============================================================================
|
|
695
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
696
|
+
// YIELD EXTRACTION - For REST API schema generation
|
|
697
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
263
698
|
|
|
264
699
|
/**
|
|
265
|
-
* Information about
|
|
700
|
+
* Information about an ask yield extracted from a generator.
|
|
701
|
+
* Used to generate REST API schemas (optional parameters).
|
|
266
702
|
*/
|
|
267
|
-
export interface
|
|
703
|
+
export interface ExtractedAsk {
|
|
268
704
|
id: string;
|
|
269
|
-
type: '
|
|
270
|
-
|
|
271
|
-
options?: Array<string | { value: string; label: string }>;
|
|
272
|
-
default?: string;
|
|
705
|
+
type: AskYield['ask'];
|
|
706
|
+
message: string;
|
|
273
707
|
required?: boolean;
|
|
708
|
+
default?: any;
|
|
709
|
+
options?: Array<string | { value: string; label: string }>;
|
|
274
710
|
pattern?: string;
|
|
275
|
-
|
|
276
|
-
|
|
711
|
+
min?: number;
|
|
712
|
+
max?: number;
|
|
277
713
|
}
|
|
278
714
|
|
|
279
715
|
/**
|
|
280
|
-
* Extract yield information by running generator with mock provider
|
|
281
|
-
*
|
|
716
|
+
* Extract ask yield information by running generator with mock provider.
|
|
717
|
+
*
|
|
718
|
+
* This is used for REST API schema generation - each ask becomes
|
|
719
|
+
* an optional request parameter.
|
|
282
720
|
*
|
|
283
|
-
* Note:
|
|
284
|
-
*
|
|
721
|
+
* Note: Only extracts asks reachable with default/empty inputs.
|
|
722
|
+
* Conditional asks may not be discovered.
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* const asks = await extractAsks(Photon.prototype.connect, { ip: '' });
|
|
726
|
+
* // Returns: [{ id: 'pairing_code', type: 'text', message: '...' }]
|
|
727
|
+
* // These become optional query/body params in REST API
|
|
285
728
|
*/
|
|
286
|
-
export async function
|
|
729
|
+
export async function extractAsks(
|
|
287
730
|
generatorFn: (...args: any[]) => AsyncGenerator<PhotonYield, any, any>,
|
|
288
731
|
mockParams: any = {}
|
|
289
|
-
): Promise<
|
|
290
|
-
const
|
|
291
|
-
let
|
|
732
|
+
): Promise<ExtractedAsk[]> {
|
|
733
|
+
const asks: ExtractedAsk[] = [];
|
|
734
|
+
let askIndex = 0;
|
|
292
735
|
|
|
293
736
|
try {
|
|
294
737
|
const generator = generatorFn(mockParams);
|
|
@@ -297,108 +740,224 @@ export async function extractYields(
|
|
|
297
740
|
while (!result.done) {
|
|
298
741
|
const yielded = result.value;
|
|
299
742
|
|
|
300
|
-
if (
|
|
301
|
-
const id = yielded.id || `
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
} else if ('confirm'
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
yields.push({
|
|
324
|
-
id,
|
|
325
|
-
type: 'select',
|
|
326
|
-
prompt: yielded.select,
|
|
327
|
-
options: yielded.options,
|
|
328
|
-
multi: yielded.multi,
|
|
329
|
-
});
|
|
330
|
-
const firstOption = yielded.options[0];
|
|
331
|
-
const mockValue = typeof firstOption === 'string' ? firstOption : firstOption.value;
|
|
332
|
-
result = await generator.next(yielded.multi ? [mockValue] : mockValue);
|
|
743
|
+
if (isAskYield(yielded)) {
|
|
744
|
+
const id = yielded.id || `ask_${askIndex++}`;
|
|
745
|
+
|
|
746
|
+
const extracted: ExtractedAsk = {
|
|
747
|
+
id,
|
|
748
|
+
type: yielded.ask,
|
|
749
|
+
message: yielded.message,
|
|
750
|
+
required: yielded.required,
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// Extract type-specific properties
|
|
754
|
+
if (yielded.ask === 'text') {
|
|
755
|
+
extracted.default = yielded.default;
|
|
756
|
+
extracted.pattern = yielded.pattern;
|
|
757
|
+
} else if (yielded.ask === 'confirm') {
|
|
758
|
+
extracted.default = yielded.default;
|
|
759
|
+
} else if (yielded.ask === 'select') {
|
|
760
|
+
extracted.options = yielded.options;
|
|
761
|
+
extracted.default = yielded.default;
|
|
762
|
+
} else if (yielded.ask === 'number') {
|
|
763
|
+
extracted.default = yielded.default;
|
|
764
|
+
extracted.min = yielded.min;
|
|
765
|
+
extracted.max = yielded.max;
|
|
333
766
|
}
|
|
767
|
+
|
|
768
|
+
asks.push(extracted);
|
|
769
|
+
|
|
770
|
+
// Provide mock value to continue
|
|
771
|
+
const mockValue = getMockValue(yielded);
|
|
772
|
+
result = await generator.next(mockValue);
|
|
334
773
|
} else {
|
|
335
|
-
// Skip
|
|
774
|
+
// Skip emit yields
|
|
336
775
|
result = await generator.next();
|
|
337
776
|
}
|
|
338
777
|
}
|
|
339
778
|
} catch (error) {
|
|
340
779
|
// Generator may throw if it needs real resources
|
|
341
780
|
// Return what we've extracted so far
|
|
342
|
-
console.warn('[generator]
|
|
781
|
+
console.warn('[generator] Ask extraction incomplete:', error);
|
|
343
782
|
}
|
|
344
783
|
|
|
345
|
-
return
|
|
784
|
+
return asks;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Get a mock value for an ask yield (for extraction purposes)
|
|
789
|
+
*/
|
|
790
|
+
function getMockValue(ask: AskYield): any {
|
|
791
|
+
switch (ask.ask) {
|
|
792
|
+
case 'text':
|
|
793
|
+
case 'password':
|
|
794
|
+
return (ask as AskText).default || '';
|
|
795
|
+
case 'confirm':
|
|
796
|
+
return (ask as AskConfirm).default ?? true;
|
|
797
|
+
case 'select':
|
|
798
|
+
const select = ask as AskSelect;
|
|
799
|
+
const firstOpt = select.options[0];
|
|
800
|
+
const firstVal = typeof firstOpt === 'string' ? firstOpt : firstOpt.value;
|
|
801
|
+
return select.multi ? [firstVal] : firstVal;
|
|
802
|
+
case 'number':
|
|
803
|
+
return (ask as AskNumber).default ?? 0;
|
|
804
|
+
case 'file':
|
|
805
|
+
return null;
|
|
806
|
+
case 'date':
|
|
807
|
+
return (ask as AskDate).default || new Date().toISOString();
|
|
808
|
+
default:
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
346
811
|
}
|
|
347
812
|
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
813
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
814
|
+
// BUILT-IN INPUT PROVIDERS
|
|
815
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
816
|
+
|
|
817
|
+
/**
|
|
818
|
+
* Error thrown when input is required but not available.
|
|
819
|
+
*
|
|
820
|
+
* REST APIs can catch this to return a continuation response.
|
|
821
|
+
*
|
|
822
|
+
* @example
|
|
823
|
+
* try {
|
|
824
|
+
* await executeGenerator(gen, { inputProvider: createPrefilledProvider({}) });
|
|
825
|
+
* } catch (e) {
|
|
826
|
+
* if (e instanceof NeedsInputError) {
|
|
827
|
+
* return {
|
|
828
|
+
* status: 'awaiting_input',
|
|
829
|
+
* ask: e.ask,
|
|
830
|
+
* continuation_id: saveContinuation(gen)
|
|
831
|
+
* };
|
|
832
|
+
* }
|
|
833
|
+
* }
|
|
834
|
+
*/
|
|
835
|
+
export class NeedsInputError extends Error {
|
|
836
|
+
public readonly ask: AskYield;
|
|
837
|
+
|
|
838
|
+
constructor(ask: AskYield) {
|
|
839
|
+
super(`Input required: ${ask.message}`);
|
|
840
|
+
this.name = 'NeedsInputError';
|
|
841
|
+
this.ask = ask;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
351
844
|
|
|
352
845
|
/**
|
|
353
|
-
* Create an input provider from pre-provided values
|
|
354
|
-
* Throws if a required value is missing
|
|
846
|
+
* Create an input provider from pre-provided values.
|
|
847
|
+
* Throws NeedsInputError if a required value is missing.
|
|
848
|
+
*
|
|
849
|
+
* Use for REST APIs where all inputs are provided upfront.
|
|
850
|
+
*
|
|
851
|
+
* @example
|
|
852
|
+
* const provider = createPrefilledProvider({
|
|
853
|
+
* name: 'John',
|
|
854
|
+
* confirmed: true
|
|
855
|
+
* });
|
|
355
856
|
*/
|
|
356
857
|
export function createPrefilledProvider(inputs: Record<string, any>): InputProvider {
|
|
357
|
-
|
|
358
|
-
if (!isInputYield(yielded)) return undefined;
|
|
858
|
+
let askIndex = 0;
|
|
359
859
|
|
|
360
|
-
|
|
860
|
+
return async (ask: AskYield) => {
|
|
861
|
+
const id = ask.id || `ask_${askIndex++}`;
|
|
361
862
|
|
|
362
863
|
if (id in inputs) {
|
|
363
864
|
return inputs[id];
|
|
364
865
|
}
|
|
365
866
|
|
|
366
867
|
// Check for default value
|
|
367
|
-
if ('
|
|
368
|
-
return
|
|
868
|
+
if ('default' in ask && ask.default !== undefined) {
|
|
869
|
+
return ask.default;
|
|
369
870
|
}
|
|
370
871
|
|
|
371
|
-
|
|
872
|
+
// No input available
|
|
873
|
+
throw new NeedsInputError(ask);
|
|
372
874
|
};
|
|
373
875
|
}
|
|
374
876
|
|
|
877
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
878
|
+
// UTILITY: Wrap regular function as generator
|
|
879
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
880
|
+
|
|
375
881
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
882
|
+
* Wrap a regular async function to behave like a generator.
|
|
883
|
+
* Useful for uniform handling in runtimes.
|
|
884
|
+
*
|
|
885
|
+
* @example
|
|
886
|
+
* const gen = wrapAsGenerator(() => photon.simpleMethod(params));
|
|
887
|
+
* const result = await executeGenerator(gen, config);
|
|
378
888
|
*/
|
|
379
|
-
export
|
|
380
|
-
|
|
889
|
+
export async function* wrapAsGenerator<T>(
|
|
890
|
+
asyncFn: () => Promise<T>
|
|
891
|
+
): AsyncGenerator<never, T, unknown> {
|
|
892
|
+
return await asyncFn();
|
|
893
|
+
}
|
|
381
894
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
895
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
896
|
+
// LEGACY COMPATIBILITY - Map old format to new
|
|
897
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* @deprecated Use AskYield instead
|
|
901
|
+
*/
|
|
902
|
+
export type PromptYield = AskText | AskPassword;
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* @deprecated Use AskConfirm instead
|
|
906
|
+
*/
|
|
907
|
+
export type ConfirmYield = AskConfirm;
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* @deprecated Use AskSelect instead
|
|
911
|
+
*/
|
|
912
|
+
export type SelectYield = AskSelect;
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* @deprecated Use EmitProgress instead
|
|
916
|
+
*/
|
|
917
|
+
export type ProgressYield = EmitProgress;
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* @deprecated Use EmitStream instead
|
|
921
|
+
*/
|
|
922
|
+
export type StreamYield = EmitStream;
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* @deprecated Use EmitLog instead
|
|
926
|
+
*/
|
|
927
|
+
export type LogYield = EmitLog;
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* @deprecated Use isAskYield instead
|
|
931
|
+
*/
|
|
932
|
+
export const isInputYield = isAskYield;
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* @deprecated Use isEmitYield instead
|
|
936
|
+
*/
|
|
937
|
+
export function isProgressYield(y: PhotonYield): y is EmitProgress {
|
|
938
|
+
return isEmitYield(y) && y.emit === 'progress';
|
|
390
939
|
}
|
|
391
940
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
941
|
+
/**
|
|
942
|
+
* @deprecated Use isEmitYield instead
|
|
943
|
+
*/
|
|
944
|
+
export function isStreamYield(y: PhotonYield): y is EmitStream {
|
|
945
|
+
return isEmitYield(y) && y.emit === 'stream';
|
|
946
|
+
}
|
|
395
947
|
|
|
396
948
|
/**
|
|
397
|
-
*
|
|
398
|
-
* Useful for uniform handling in runtimes
|
|
949
|
+
* @deprecated Use isEmitYield instead
|
|
399
950
|
*/
|
|
400
|
-
export
|
|
401
|
-
|
|
402
|
-
): AsyncGenerator<never, T, unknown> {
|
|
403
|
-
return await asyncFn();
|
|
951
|
+
export function isLogYield(y: PhotonYield): y is EmitLog {
|
|
952
|
+
return isEmitYield(y) && y.emit === 'log';
|
|
404
953
|
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* @deprecated Use extractAsks instead
|
|
957
|
+
*/
|
|
958
|
+
export const extractYields = extractAsks;
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* @deprecated Use ExtractedAsk instead
|
|
962
|
+
*/
|
|
963
|
+
export type ExtractedYield = ExtractedAsk;
|