gulp-mu-gulp-api 0.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/README.md +245 -0
- package/package.json +46 -0
- package/src/index.mjs +322 -0
package/README.md
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# gulp-mu-gulp-api
|
|
2
|
+
|
|
3
|
+
Public task API of the **µGulp™** orchestrator — progress reporting and interactive dashboard inputs from within gulp tasks.
|
|
4
|
+
|
|
5
|
+
Part of the [µGulp™ project on GitHub](https://github.com/mamekudz/microGulp) (subdirectory [`gulp-mu-gulp-api/`](https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api)).
|
|
6
|
+
|
|
7
|
+
[English](#english) · [Deutsch](#deutsch)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# English
|
|
12
|
+
|
|
13
|
+
**gulp-mu-gulp-api** gives gulp tasks (and any npm package inside the task stream) access to the extended µGulp™ dashboard features: progress bars, validated text inputs, color and font pickers, single and multi selections, sliders and complete forms.
|
|
14
|
+
|
|
15
|
+
The module is deliberately **dependency-free** and knows nothing about µGulp™ internals: when the task runs under µGulp™, it talks directly to the engine over the worker process IPC channel. When the gulpfile runs through the classic gulp CLI, every function degrades gracefully — progress renders on the terminal, inputs are asked via readline (TTY) or answered with the declared defaults (CI/non-TTY). Tasks stay fully runnable without µGulp™.
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install gulp-mu-gulp-api
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
```javascript
|
|
26
|
+
import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsUGulp } from 'gulp-mu-gulp-api';
|
|
27
|
+
|
|
28
|
+
export async function BUILD_THEME() {
|
|
29
|
+
// single inputs
|
|
30
|
+
let accent = await RequestColorInput({ label: 'Accent color', default: '#00e5ff' });
|
|
31
|
+
let font = await RequestFontInput({ label: 'Headline font', options: ['Segoe UI', 'Cascadia Mono'] });
|
|
32
|
+
let title = await RequestTextInput({
|
|
33
|
+
label: 'Product title',
|
|
34
|
+
validate: { required: true, minLength: 3, pattern: '^[A-Za-z0-9 ]+$' },
|
|
35
|
+
});
|
|
36
|
+
let servers = await RequestMultiSelectInput({
|
|
37
|
+
label: 'Rollout servers',
|
|
38
|
+
options: [{ value: 'SMPMD', checked: true }, 'iMedOne', 'ACM'],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// progress (determinate neon bar in the µGulp dashboard)
|
|
42
|
+
let progress = CreateProgress('compiling theme');
|
|
43
|
+
for (let step = 1; step <= 10; step++) {
|
|
44
|
+
// ... work ...
|
|
45
|
+
progress.Update(step / 10);
|
|
46
|
+
}
|
|
47
|
+
progress.Done();
|
|
48
|
+
|
|
49
|
+
// sound + speech (rendered by the dashboard, configured in its settings)
|
|
50
|
+
PlaySignal('success');
|
|
51
|
+
Speak('Theme build finished.');
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Complete forms in one go:
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
let values = await RequestForm({
|
|
59
|
+
title: 'Build parameters',
|
|
60
|
+
fields: [
|
|
61
|
+
{ id: 'mode', type: 'radio', label: 'Build mode', default: 'release', options: ['debug', 'release'] },
|
|
62
|
+
{ id: 'threads', type: 'range', label: 'Worker threads', min: 1, max: 16, step: 1, default: 4 },
|
|
63
|
+
{ id: 'notes', type: 'textarea', label: 'Release notes', rows: 4, placeholder: 'What changed?' },
|
|
64
|
+
{ id: 'apikey', type: 'password', label: 'API key', validate: { required: true } },
|
|
65
|
+
{ id: 'targets', type: 'checkbox', label: 'Targets', options: ['Alpha', 'Beta', 'Gamma'], validate: { minSelected: 1 } },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
// values => { mode: 'release', threads: '8', notes: '…', apikey: '…', targets: ['Alpha', 'Gamma'] }
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## API reference
|
|
72
|
+
|
|
73
|
+
| Function | Purpose |
|
|
74
|
+
| :--- | :--- |
|
|
75
|
+
| `IsUGulp()` | `true` when the task runs under µGulp™ with an attached dashboard |
|
|
76
|
+
| `ReportProgress(value, label?)` | Report progress `0..1` to the task progress bar |
|
|
77
|
+
| `CreateProgress(label?)` | Reporter object with `Update(value, stepLabel?)` and `Done()` |
|
|
78
|
+
| `PlaySignal(signal?)` | Acoustic signal in the dashboard: `'success'` \| `'error'` \| `'attention'` (default) |
|
|
79
|
+
| `Speak(text, options?)` | Speech output in the dashboard; `options`: `{ rate?, pitch?, volume? }` per-call overrides |
|
|
80
|
+
| `RequestForm(form)` | Request a complete form; resolves with `{ fieldId: value, … }` |
|
|
81
|
+
| `RequestTextInput(options)` | Single text field (with `validate` rules) |
|
|
82
|
+
| `RequestColorInput(options)` | Color picker, returns `#rrggbb` |
|
|
83
|
+
| `RequestFontInput(options)` | Font selection (`options`: font list) |
|
|
84
|
+
| `RequestSelectInput(options)` | Single selection from a fixed option list |
|
|
85
|
+
| `RequestMultiSelectInput(options)` | Multi selection (checkbox group), always resolves to a `string[]` |
|
|
86
|
+
|
|
87
|
+
### Field types (for `RequestForm`)
|
|
88
|
+
|
|
89
|
+
A field descriptor is `{ id, type, label, default?, options?, validate?, min?, max?, step?, rows?, placeholder? }`.
|
|
90
|
+
|
|
91
|
+
| `type` | Dashboard control | Notes |
|
|
92
|
+
| :--- | :--- | :--- |
|
|
93
|
+
| `text` | text field | default type |
|
|
94
|
+
| `password` | masked text field | |
|
|
95
|
+
| `number` | number field | honors `min`, `max`, `step` |
|
|
96
|
+
| `textarea` | multi-line text field | `rows`, `placeholder` |
|
|
97
|
+
| `color` | native color picker | value as `#rrggbb` |
|
|
98
|
+
| `font` | font selection | `options`: font family names |
|
|
99
|
+
| `select` | dropdown | single choice |
|
|
100
|
+
| `radio` | radio group | single exclusive choice, vertical option column |
|
|
101
|
+
| `checkbox` | checkbox group | **multi-select** — resolves to a `string[]` |
|
|
102
|
+
| `range` | slider + synced number box | `min`, `max`, `step`, `default` |
|
|
103
|
+
| `date` / `time` / `datetime` | native date/time pickers | |
|
|
104
|
+
| `file` | file selection | |
|
|
105
|
+
|
|
106
|
+
**Options** (`select`, `radio`, `checkbox`) accept plain strings or objects `{ value, label?, checked?, disabled? }` — `checked` preselects checkbox options, `disabled` greys an option out.
|
|
107
|
+
|
|
108
|
+
**Validation rules:** `{ required?, minLength?, pattern?, minSelected? }` (`minSelected` applies to `checkbox`). Checks run asynchronously in the dashboard; invalid forms cannot be submitted.
|
|
109
|
+
|
|
110
|
+
## Fallback behavior without µGulp™
|
|
111
|
+
|
|
112
|
+
| Function | TTY (interactive shell) | non-TTY (CI, pipe) |
|
|
113
|
+
| :--- | :--- | :--- |
|
|
114
|
+
| `ReportProgress` | updating progress line | log line every 10% step |
|
|
115
|
+
| `PlaySignal` | terminal bell (`attention`/`error`) | silent |
|
|
116
|
+
| `Speak` | `[speech]` log line | `[speech]` log line |
|
|
117
|
+
| text/password/number/range input | readline prompt (Enter = default) | default value |
|
|
118
|
+
| font/select/radio input | numbered list + readline | default value |
|
|
119
|
+
| checkbox input | numbered list, comma-separated picks | values of the `checked` options |
|
|
120
|
+
|
|
121
|
+
Sound and speech playback (mute, volume, speech rate/pitch) is configured centrally in the µGulp™ dashboard settings (gear icon in the header) — tasks never need to care about the user's audio preferences.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT — © 2026 Meinolf Amekudzi
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
# Deutsch
|
|
130
|
+
|
|
131
|
+
**gulp-mu-gulp-api** ist die öffentliche Task-API des **µGulp™**-Orchestrators. Gulp-Tasks (und beliebige npm-Pakete innerhalb des Task-Streams) erhalten damit Zugriff auf die erweiterten µGulp™-Dashboard-Funktionen: Fortschrittsanzeige, Texteingaben mit Validierung, Farb- und Schriftart-Auswahl, Einzel- und Mehrfachauswahl, Slider und komplette Formulare.
|
|
132
|
+
|
|
133
|
+
Teil des [µGulp™-Projekts auf GitHub](https://github.com/mamekudz/microGulp) (Unterverzeichnis [`gulp-mu-gulp-api/`](https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api)).
|
|
134
|
+
|
|
135
|
+
Das Modul ist bewusst **abhängigkeitsfrei** und kennt keine µGulp™-Interna: Läuft der Task unter µGulp™, spricht es direkt über den IPC-Kanal des Worker-Prozesses mit der Engine. Läuft das Gulpfile klassisch über die gulp-CLI, degradieren alle Funktionen sauber — Fortschritt landet auf dem Terminal, Eingaben werden per readline abgefragt (TTY) oder mit den deklarierten Default-Werten beantwortet (CI/non-TTY). Tasks bleiben ohne µGulp™ voll lauffähig.
|
|
136
|
+
|
|
137
|
+
## Installation
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
npm install gulp-mu-gulp-api
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Verwendung
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
import { ReportProgress, CreateProgress, PlaySignal, Speak, RequestTextInput, RequestColorInput, RequestFontInput, RequestSelectInput, RequestMultiSelectInput, RequestForm, IsUGulp } from 'gulp-mu-gulp-api';
|
|
147
|
+
|
|
148
|
+
export async function BUILD_THEME() {
|
|
149
|
+
// Einzeleingaben
|
|
150
|
+
let accent = await RequestColorInput({ label: 'Accent color', default: '#00e5ff' });
|
|
151
|
+
let font = await RequestFontInput({ label: 'Headline font', options: ['Segoe UI', 'Cascadia Mono'] });
|
|
152
|
+
let title = await RequestTextInput({
|
|
153
|
+
label: 'Product title',
|
|
154
|
+
validate: { required: true, minLength: 3, pattern: '^[A-Za-z0-9 ]+$' },
|
|
155
|
+
});
|
|
156
|
+
let servers = await RequestMultiSelectInput({
|
|
157
|
+
label: 'Rollout servers',
|
|
158
|
+
options: [{ value: 'SMPMD', checked: true }, 'iMedOne', 'ACM'],
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Fortschritt (determinierte Neon-Progressbar im µGulp-Dashboard)
|
|
162
|
+
let progress = CreateProgress('compiling theme');
|
|
163
|
+
for (let step = 1; step <= 10; step++) {
|
|
164
|
+
// ... Arbeit ...
|
|
165
|
+
progress.Update(step / 10);
|
|
166
|
+
}
|
|
167
|
+
progress.Done();
|
|
168
|
+
|
|
169
|
+
// sound + speech (rendered by the dashboard, configured in its settings)
|
|
170
|
+
PlaySignal('success');
|
|
171
|
+
Speak('Theme build finished.');
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Komplette Formulare in einem Rutsch:
|
|
176
|
+
|
|
177
|
+
```javascript
|
|
178
|
+
let values = await RequestForm({
|
|
179
|
+
title: 'Build parameters',
|
|
180
|
+
fields: [
|
|
181
|
+
{ id: 'mode', type: 'radio', label: 'Build mode', default: 'release', options: ['debug', 'release'] },
|
|
182
|
+
{ id: 'threads', type: 'range', label: 'Worker threads', min: 1, max: 16, step: 1, default: 4 },
|
|
183
|
+
{ id: 'notes', type: 'textarea', label: 'Release notes', rows: 4, placeholder: 'What changed?' },
|
|
184
|
+
{ id: 'apikey', type: 'password', label: 'API key', validate: { required: true } },
|
|
185
|
+
{ id: 'targets', type: 'checkbox', label: 'Targets', options: ['Alpha', 'Beta', 'Gamma'], validate: { minSelected: 1 } },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
// values => { mode: 'release', threads: '8', notes: '…', apikey: '…', targets: ['Alpha', 'Gamma'] }
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## API-Referenz
|
|
192
|
+
|
|
193
|
+
| Funktion | Zweck |
|
|
194
|
+
| :--- | :--- |
|
|
195
|
+
| `IsUGulp()` | `true`, wenn der Task unter µGulp™ mit angebundenem Dashboard läuft |
|
|
196
|
+
| `ReportProgress(value, label?)` | Fortschritt `0..1` an die Task-Progressbar melden |
|
|
197
|
+
| `CreateProgress(label?)` | Reporter-Objekt mit `Update(value, stepLabel?)` und `Done()` |
|
|
198
|
+
| `PlaySignal(signal?)` | Akustisches Signal im Dashboard: `'success'` \| `'error'` \| `'attention'` (Default) |
|
|
199
|
+
| `Speak(text, options?)` | Sprachausgabe im Dashboard; `options`: `{ rate?, pitch?, volume? }` je Aufruf |
|
|
200
|
+
| `RequestForm(form)` | Komplettes Formular anfordern; löst mit `{ fieldId: value, … }` auf |
|
|
201
|
+
| `RequestTextInput(options)` | Einzelnes Textfeld (mit `validate`-Regeln) |
|
|
202
|
+
| `RequestColorInput(options)` | Farbwähler, Rückgabe `#rrggbb` |
|
|
203
|
+
| `RequestFontInput(options)` | Schriftart-Auswahl (`options`: Font-Liste) |
|
|
204
|
+
| `RequestSelectInput(options)` | Einzelauswahl aus fester Optionsliste |
|
|
205
|
+
| `RequestMultiSelectInput(options)` | Mehrfachauswahl (Checkbox-Gruppe), löst immer mit `string[]` auf |
|
|
206
|
+
|
|
207
|
+
### Feldtypen (für `RequestForm`)
|
|
208
|
+
|
|
209
|
+
Ein Feld-Deskriptor ist `{ id, type, label, default?, options?, validate?, min?, max?, step?, rows?, placeholder? }`.
|
|
210
|
+
|
|
211
|
+
| `type` | Dashboard-Steuerelement | Hinweise |
|
|
212
|
+
| :--- | :--- | :--- |
|
|
213
|
+
| `text` | Textfeld | Standardtyp |
|
|
214
|
+
| `password` | maskiertes Textfeld | |
|
|
215
|
+
| `number` | Zahlenfeld | berücksichtigt `min`, `max`, `step` |
|
|
216
|
+
| `textarea` | mehrzeiliges Textfeld | `rows`, `placeholder` |
|
|
217
|
+
| `color` | nativer Farbwähler | Wert als `#rrggbb` |
|
|
218
|
+
| `font` | Schriftart-Auswahl | `options`: Font-Familiennamen |
|
|
219
|
+
| `select` | Dropdown | Einzelauswahl |
|
|
220
|
+
| `radio` | Radiogruppe | exklusive Einzelauswahl, vertikale Optionsspalte |
|
|
221
|
+
| `checkbox` | Checkbox-Gruppe | **Mehrfachauswahl** — löst mit `string[]` auf |
|
|
222
|
+
| `range` | Slider + synchronisiertes Zahlenfeld | `min`, `max`, `step`, `default` |
|
|
223
|
+
| `date` / `time` / `datetime` | native Datums-/Zeitwähler | |
|
|
224
|
+
| `file` | Dateiauswahl | |
|
|
225
|
+
|
|
226
|
+
**Options** (`select`, `radio`, `checkbox`) akzeptieren einfache Strings oder Objekte `{ value, label?, checked?, disabled? }` — `checked` selektiert Checkbox-Optionen vor, `disabled` graut eine Option aus.
|
|
227
|
+
|
|
228
|
+
**Validierungsregeln:** `{ required?, minLength?, pattern?, minSelected? }` (`minSelected` gilt für `checkbox`). Die Prüfung läuft asynchron im Dashboard; ungültige Formulare können nicht abgeschickt werden.
|
|
229
|
+
|
|
230
|
+
## Fallback-Verhalten ohne µGulp™
|
|
231
|
+
|
|
232
|
+
| Funktion | TTY (interaktive Shell) | non-TTY (CI, Pipe) |
|
|
233
|
+
| :--- | :--- | :--- |
|
|
234
|
+
| `ReportProgress` | aktualisierende Fortschrittszeile | Log-Zeile je 10-%-Schritt |
|
|
235
|
+
| `PlaySignal` | Terminal-Glocke (`attention`/`error`) | stumm |
|
|
236
|
+
| `Speak` | `[speech]`-Log-Zeile | `[speech]`-Log-Zeile |
|
|
237
|
+
| Text-/Passwort-/Zahlen-/Range-Eingabe | readline-Prompt (Enter = Default) | Default-Wert |
|
|
238
|
+
| Font-/Select-/Radio-Eingabe | nummerierte Liste + readline | Default-Wert |
|
|
239
|
+
| Checkbox-Eingabe | nummerierte Liste, Kommaeingabe | Werte der `checked`-Optionen |
|
|
240
|
+
|
|
241
|
+
Die Sound- und Sprachwiedergabe (Mute, Lautstärke, Sprechgeschwindigkeit/-höhe) wird zentral in den µGulp™-Dashboard-Settings konfiguriert (Zahnrad im Kopfbereich) — Tasks müssen sich um die Audio-Einstellungen der Anwender nicht kümmern.
|
|
242
|
+
|
|
243
|
+
## Lizenz
|
|
244
|
+
|
|
245
|
+
MIT — © 2026 Meinolf Amekudzi
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gulp-mu-gulp-api",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Public task API for the µGulp orchestrator: progress reporting, sound signals, speech output and interactive UI inputs (text, password, number, textarea, color, font, select, radio, multi-select checkbox, range slider, date/time, file) from within gulp tasks — with graceful CLI fallbacks when running without µGulp.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.mjs",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test \"tests/**/*.test.mjs\""
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"gulp",
|
|
19
|
+
"ugulp",
|
|
20
|
+
"microgulp",
|
|
21
|
+
"progress",
|
|
22
|
+
"interactive",
|
|
23
|
+
"sound",
|
|
24
|
+
"speech",
|
|
25
|
+
"prompt",
|
|
26
|
+
"multiselect",
|
|
27
|
+
"form",
|
|
28
|
+
"task-api"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/mamekudz/microGulp.git",
|
|
34
|
+
"directory": "gulp-mu-gulp-api"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/mamekudz/microGulp/tree/main/gulp-mu-gulp-api#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/mamekudz/microGulp/issues"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// ===========================================
|
|
2
|
+
// gulp-mu-gulp-api — µGulp task API
|
|
3
|
+
// © 2026 Meinolf Amekudzi
|
|
4
|
+
// (published under MIT license)
|
|
5
|
+
// ===========================================
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Public API for gulp tasks running under the µGulp orchestrator.
|
|
9
|
+
*
|
|
10
|
+
* Under µGulp every task runs inside a forked worker process with an IPC
|
|
11
|
+
* channel to the engine; this module talks to that channel directly, so it
|
|
12
|
+
* has zero coupling to µGulp internals and works from any npm package in
|
|
13
|
+
* the pipeline. Without µGulp (plain "gulp" CLI run) every function
|
|
14
|
+
* degrades gracefully: progress renders on the terminal, inputs fall back
|
|
15
|
+
* to readline prompts on a TTY or to the declared defaults otherwise.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* import { ReportProgress, RequestTextInput, RequestColorInput } from 'gulp-mu-gulp-api';
|
|
19
|
+
*
|
|
20
|
+
* export async function BUILD_THEME() {
|
|
21
|
+
* let accent = await RequestColorInput({ label: 'Accent color', default: '#00e5ff' });
|
|
22
|
+
* for (let step = 0; step < 10; step++) {
|
|
23
|
+
* // ... work ...
|
|
24
|
+
* ReportProgress((step + 1) / 10, 'compiling theme');
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* @module gulp-mu-gulp-api
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
let uiRequestCounter = 0;
|
|
32
|
+
let ipcListenerAttached = false;
|
|
33
|
+
let lastLoggedPercent = -1;
|
|
34
|
+
const pendingUiRequests = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @returns {boolean} true when the task runs inside a µGulp worker with an
|
|
38
|
+
* attached dashboard (progress and inputs are rendered in the webview)
|
|
39
|
+
*/
|
|
40
|
+
export function IsUGulp() {
|
|
41
|
+
return typeof process.send === 'function' && process.env.UGULP === '1';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// -------------------------------------------------
|
|
45
|
+
// progress
|
|
46
|
+
// -------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reports task progress to the µGulp dashboard (determinate neon progress
|
|
50
|
+
* bar in the task's sector). CLI fallback: an updating line on a TTY,
|
|
51
|
+
* 10%-step log lines otherwise.
|
|
52
|
+
*
|
|
53
|
+
* @param {number} _value progress in the range 0..1
|
|
54
|
+
* @param {string} [_label] short status label shown next to the bar
|
|
55
|
+
*/
|
|
56
|
+
export function ReportProgress(_value, _label) {
|
|
57
|
+
let value = Math.max(0, Math.min(1, Number(_value) || 0));
|
|
58
|
+
if (IsUGulp()) {
|
|
59
|
+
process.send({ type: 'progress', value, label: _label });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let percent = Math.round(value * 100);
|
|
63
|
+
if (process.stdout.isTTY) {
|
|
64
|
+
process.stdout.write('\r' + (_label ?? 'progress') + ' ' + percent + '%' + (value >= 1 ? '\n' : ''));
|
|
65
|
+
} else if (percent >= 100 || percent - lastLoggedPercent >= 10) {
|
|
66
|
+
lastLoggedPercent = percent >= 100 ? -1 : percent;
|
|
67
|
+
console.log((_label ?? 'progress') + ' ' + percent + '%');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Convenience wrapper that carries a fixed label.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} [_label]
|
|
75
|
+
* @returns {{Update: function(number, string=): void, Done: function(): void}}
|
|
76
|
+
*/
|
|
77
|
+
export function CreateProgress(_label) {
|
|
78
|
+
return {
|
|
79
|
+
Update(_value, _stepLabel) {
|
|
80
|
+
ReportProgress(_value, _stepLabel ?? _label);
|
|
81
|
+
},
|
|
82
|
+
Done() {
|
|
83
|
+
ReportProgress(1, _label);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// -------------------------------------------------
|
|
89
|
+
// sound & speech
|
|
90
|
+
// -------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Plays an acoustic signal in the µGulp dashboard (WebAudio, respects the
|
|
94
|
+
* dashboard audio settings: mute/volume). CLI fallback: terminal bell for
|
|
95
|
+
* 'attention' and 'error' on a TTY, silent otherwise.
|
|
96
|
+
*
|
|
97
|
+
* @param {'success'|'error'|'attention'} [_signal]
|
|
98
|
+
*/
|
|
99
|
+
export function PlaySignal(_signal = 'attention') {
|
|
100
|
+
if (IsUGulp()) {
|
|
101
|
+
process.send({ type: 'sound', signal: _signal });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (process.stdout.isTTY && (_signal === 'attention' || _signal === 'error')) {
|
|
105
|
+
process.stdout.write('\u0007');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Speaks a text through the µGulp dashboard speech output (Web Speech API,
|
|
111
|
+
* respects the dashboard speech settings: enabled/volume/rate/pitch).
|
|
112
|
+
* CLI fallback: the text is printed as a log line.
|
|
113
|
+
*
|
|
114
|
+
* @param {string} _text
|
|
115
|
+
* @param {object} [_options] { rate?, pitch?, volume? } — per-call overrides (0..2 / 0..2 / 0..1)
|
|
116
|
+
*/
|
|
117
|
+
export function Speak(_text, _options) {
|
|
118
|
+
let text = String(_text ?? '').trim();
|
|
119
|
+
if (!text) return;
|
|
120
|
+
if (IsUGulp()) {
|
|
121
|
+
process.send({ type: 'speech', text, options: _options ?? null });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
console.log('[speech] ' + text);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// -------------------------------------------------
|
|
128
|
+
// interactive inputs
|
|
129
|
+
// -------------------------------------------------
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Requests a complete form from the µGulp dashboard.
|
|
133
|
+
*
|
|
134
|
+
* Field descriptor: { id, type, label, default?, options?, validate?,
|
|
135
|
+
* min?, max?, step?, rows?, placeholder? }
|
|
136
|
+
*
|
|
137
|
+
* Supported types: 'text' | 'password' | 'number' | 'textarea' | 'color' |
|
|
138
|
+
* 'font' | 'select' | 'radio' | 'checkbox' (multi-select, resolves to a
|
|
139
|
+
* string array) | 'range' (slider with synced number box) | 'date' |
|
|
140
|
+
* 'time' | 'datetime' | 'file'.
|
|
141
|
+
*
|
|
142
|
+
* Options (select/radio/checkbox) accept plain strings or objects
|
|
143
|
+
* { value, label?, checked?, disabled? }.
|
|
144
|
+
*
|
|
145
|
+
* Validation rules: { required?, minLength?, pattern?, minSelected? } —
|
|
146
|
+
* checks run asynchronously in the webview before submission.
|
|
147
|
+
*
|
|
148
|
+
* @param {object} _form { title, fields: [...] }
|
|
149
|
+
* @returns {Promise<object>} map fieldId -> value
|
|
150
|
+
*/
|
|
151
|
+
export async function RequestForm(_form) {
|
|
152
|
+
if (IsUGulp()) {
|
|
153
|
+
_EnsureIpcListener();
|
|
154
|
+
let requestId = 'api-req-' + process.pid + '-' + (++uiRequestCounter);
|
|
155
|
+
return new Promise((_resolve) => {
|
|
156
|
+
pendingUiRequests.set(requestId, _resolve);
|
|
157
|
+
process.send({ type: 'ui-request', requestId, form: _form });
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return _FallbackForm(_form);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Requests a single validated text value.
|
|
165
|
+
* @param {object} _options { label, title?, default?, validate? }
|
|
166
|
+
* @returns {Promise<string|null>}
|
|
167
|
+
*/
|
|
168
|
+
export async function RequestTextInput(_options) {
|
|
169
|
+
return _SingleField({ ..._options, type: 'text' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Requests a color (native color picker in the dashboard).
|
|
174
|
+
* @param {object} _options { label, title?, default? } — default as #rrggbb
|
|
175
|
+
* @returns {Promise<string|null>}
|
|
176
|
+
*/
|
|
177
|
+
export async function RequestColorInput(_options) {
|
|
178
|
+
return _SingleField({ ..._options, type: 'color' });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Requests a font selection.
|
|
183
|
+
* @param {object} _options { label, title?, default?, options? } — options: font family names
|
|
184
|
+
* @returns {Promise<string|null>}
|
|
185
|
+
*/
|
|
186
|
+
export async function RequestFontInput(_options) {
|
|
187
|
+
return _SingleField({ ..._options, type: 'font' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Requests a selection from a fixed option list.
|
|
192
|
+
* @param {object} _options { label, title?, default?, options: string[] }
|
|
193
|
+
* @returns {Promise<string|null>}
|
|
194
|
+
*/
|
|
195
|
+
export async function RequestSelectInput(_options) {
|
|
196
|
+
return _SingleField({ ..._options, type: 'select' });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Requests a multi-selection (checkbox group in the dashboard).
|
|
201
|
+
* @param {object} _options { label, title?, options: Array<string|{value, label?, checked?, disabled?}>, validate? }
|
|
202
|
+
* @returns {Promise<string[]>} selected values (empty array when nothing was picked)
|
|
203
|
+
*/
|
|
204
|
+
export async function RequestMultiSelectInput(_options) {
|
|
205
|
+
let value = await _SingleField({ ..._options, type: 'checkbox' });
|
|
206
|
+
return Array.isArray(value) ? value : (value != null ? [value] : []);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// -------------------------------------------------
|
|
210
|
+
// internal
|
|
211
|
+
// -------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function _EnsureIpcListener() {
|
|
214
|
+
if (ipcListenerAttached) return;
|
|
215
|
+
ipcListenerAttached = true;
|
|
216
|
+
process.on('message', (_message) => {
|
|
217
|
+
if (_message && _message.type === 'ui-response' && pendingUiRequests.has(_message.requestId)) {
|
|
218
|
+
let resolver = pendingUiRequests.get(_message.requestId);
|
|
219
|
+
pendingUiRequests.delete(_message.requestId);
|
|
220
|
+
resolver(_message.payload);
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function _SingleField(_options) {
|
|
226
|
+
let values = await RequestForm({
|
|
227
|
+
title: _options.title ?? _options.label,
|
|
228
|
+
fields: [{
|
|
229
|
+
id: 'value',
|
|
230
|
+
type: _options.type,
|
|
231
|
+
label: _options.label,
|
|
232
|
+
default: _options.default,
|
|
233
|
+
options: _options.options,
|
|
234
|
+
validate: _options.validate,
|
|
235
|
+
}],
|
|
236
|
+
});
|
|
237
|
+
return values.value ?? null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function _FallbackForm(_form) {
|
|
241
|
+
let values = {};
|
|
242
|
+
for (let field of _form.fields ?? []) {
|
|
243
|
+
values[field.id] = await _FallbackField(field);
|
|
244
|
+
}
|
|
245
|
+
return values;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function _NormalizeOptions(_options) {
|
|
249
|
+
let result = [];
|
|
250
|
+
for (let option of _options ?? []) {
|
|
251
|
+
if (option == null) continue;
|
|
252
|
+
if (typeof option === 'object') {
|
|
253
|
+
if (option.separator) continue;
|
|
254
|
+
let value = option.value ?? option.label ?? option.name ?? '';
|
|
255
|
+
result.push({
|
|
256
|
+
value: String(value),
|
|
257
|
+
label: String(option.label ?? option.name ?? value),
|
|
258
|
+
checked: !!option.checked,
|
|
259
|
+
});
|
|
260
|
+
} else {
|
|
261
|
+
result.push({ value: String(option), label: String(option), checked: false });
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function _FallbackDefault(_field) {
|
|
268
|
+
if (_field.type === 'checkbox') {
|
|
269
|
+
return _NormalizeOptions(_field.options).filter((_option) => _option.checked).map((_option) => _option.value);
|
|
270
|
+
}
|
|
271
|
+
return _field.default ?? null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function _FallbackField(_field) {
|
|
275
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
276
|
+
return _FallbackDefault(_field);
|
|
277
|
+
}
|
|
278
|
+
const readline = await import('node:readline/promises');
|
|
279
|
+
let prompt = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
280
|
+
try {
|
|
281
|
+
let label = _field.label ?? _field.id;
|
|
282
|
+
let options = _NormalizeOptions(_field.options);
|
|
283
|
+
let hasOptions = options.length > 0;
|
|
284
|
+
if (_field.type === 'checkbox' && hasOptions) {
|
|
285
|
+
options.forEach((_option, _index) => console.log(' [' + (_index + 1) + '] ' + _option.label + (_option.checked ? ' *' : '')));
|
|
286
|
+
let answer = await prompt.question(label + ' (comma-separated numbers, Enter = defaults marked *): ');
|
|
287
|
+
let picked = answer.split(',')
|
|
288
|
+
.map((_part) => parseInt(_part.trim(), 10))
|
|
289
|
+
.filter((_index) => _index >= 1 && _index <= options.length)
|
|
290
|
+
.map((_index) => options[_index - 1].value);
|
|
291
|
+
return picked.length > 0 ? picked : _FallbackDefault(_field);
|
|
292
|
+
}
|
|
293
|
+
if ((_field.type === 'select' || _field.type === 'font' || _field.type === 'radio') && hasOptions) {
|
|
294
|
+
options.forEach((_option, _index) => console.log(' [' + (_index + 1) + '] ' + _option.label));
|
|
295
|
+
let answer = await prompt.question(label + ' (1-' + options.length + (_field.default ? ', default "' + _field.default + '"' : '') + '): ');
|
|
296
|
+
let index = parseInt(answer, 10);
|
|
297
|
+
if (index >= 1 && index <= options.length) return options[index - 1].value;
|
|
298
|
+
return _field.default ?? null;
|
|
299
|
+
}
|
|
300
|
+
let hint = _field.type === 'range' || _field.type === 'number'
|
|
301
|
+
? ' (' + (_field.min ?? 0) + '-' + (_field.max ?? 100) + (_field.default != null ? ', default ' + _field.default : '') + ')'
|
|
302
|
+
: (_field.default ? ' [' + _field.default + ']' : '');
|
|
303
|
+
let answer = await prompt.question(label + hint + ': ');
|
|
304
|
+
return answer || (_field.default ?? null);
|
|
305
|
+
} finally {
|
|
306
|
+
prompt.close();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export default {
|
|
311
|
+
IsUGulp,
|
|
312
|
+
ReportProgress,
|
|
313
|
+
CreateProgress,
|
|
314
|
+
PlaySignal,
|
|
315
|
+
Speak,
|
|
316
|
+
RequestForm,
|
|
317
|
+
RequestTextInput,
|
|
318
|
+
RequestColorInput,
|
|
319
|
+
RequestFontInput,
|
|
320
|
+
RequestSelectInput,
|
|
321
|
+
RequestMultiSelectInput,
|
|
322
|
+
};
|