@nan0web/ui-cli 1.0.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.
@@ -0,0 +1,34 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { select } from './select.js'
4
+ import { CancelError } from '@nan0web/ui/core'
5
+
6
+ describe('Select utility', () => {
7
+ it('should throw error for empty options', async () => {
8
+ await assert.rejects(
9
+ select({
10
+ title: 'Test',
11
+ prompt: 'Choose:',
12
+ options: [],
13
+ console: console
14
+ }),
15
+ Error
16
+ )
17
+ })
18
+
19
+ it('should throw CancelError for cancellation', async () => {
20
+ // Мокаємо ask для скасування
21
+ const mockAsk = () => Promise.resolve({ cancelled: true })
22
+
23
+ await assert.rejects(
24
+ select({
25
+ title: 'Test',
26
+ prompt: 'Choose:',
27
+ options: ['option1', 'option2'],
28
+ console: console,
29
+ ask: mockAsk
30
+ }),
31
+ CancelError
32
+ )
33
+ })
34
+ })
package/system.md ADDED
@@ -0,0 +1,99 @@
1
+ ---
2
+ PROJECT_NAME: "ui-cli"
3
+ VERSION: "1.0.0"
4
+ CORE_PRINCIPLE: "Універсальний CLI‑адаптер – простота + надійність."
5
+ VALIDATION_PHRASE: "UI‑CLI відповідає"
6
+ FAILURE_RESPONSE: "Це — не UI‑CLI. Я не можу працювати далі."
7
+ IDENTITY_MODEL: "Я / тИ / мИ / вИ"
8
+ LOGIC_BASE: "Запит → відповідь → валідація → результат"
9
+ ---
10
+
11
+ # ✨ UI‑CLI – системне керівництво
12
+
13
+ > **Мета** – надати мінімальний, беззалежний інтерфейс вводу/виводу для Node‑CLI‑додатків, що працює без сторонніх бібліотек, лише чистий JavaScript.
14
+
15
+ ## 📦 Основні компоненти
16
+
17
+ | Компонент | Опис | Експорт |
18
+ |----------|------|---------|
19
+ | `CLIInputAdapter` | Клас‑адаптер, що обгортає процес запиту форм, окремих полів та списків. | `default`, `CLIInputAdapter` |
20
+ | `Input` | Об’єкт, який зберігає рядок вводу та статус скасування. | `Input` |
21
+ | `CancelError` | Помилка, кидається при скасуванні запиту. | `CancelError` |
22
+ | `ask` | Проста функція‑проміс, що виводить питання та повертає відповідь. | `ask` |
23
+ | `createInput` | Фабрика, що створює кастомізований обробник вводу з ключовими словами «stop». | `createInput` |
24
+ | `select` | Генерує список варіантів, приймає номер, повертає обраний `value`. | `select` |
25
+ | `next` | Очікує будь‑яку клавішу (можна вказати схеми). | `next` |
26
+ | `pause` | Пауза на вказану кількість мілісекунд. | `pause` |
27
+
28
+ ## 🚀 Швидкий старт
29
+
30
+ ```bash
31
+ npm i @nan0web/ui-cli
32
+ ```
33
+
34
+ ```js
35
+ import {
36
+ CLIInputAdapter,
37
+ ask,
38
+ select,
39
+ next,
40
+ pause,
41
+ CancelError,
42
+ Input,
43
+ createInput,
44
+ } from '@nan0web/ui-cli'
45
+
46
+ const adapter = new CLIInputAdapter()
47
+
48
+ // Приклад: простий запит
49
+ const name = await adapter.ask('Ваше ім’я?')
50
+ console.log('Вітаємо,', name)
51
+
52
+ // Приклад: вибір зі списку
53
+ const lang = await adapter.select({
54
+ title: 'Виберіть мову',
55
+ prompt: 'Введіть номер: ',
56
+ options: new Map([['en', 'English'], ['uk', 'Українська']]),
57
+ console: console,
58
+ })
59
+ console.log('Обрана мова:', lang.value)
60
+ ```
61
+
62
+ ## 🛠️ Тестування (TDD)
63
+
64
+ - **Команда** `npm test` – запускає всі `*.test.js` у `src/`.
65
+ - **Coverage** `npm run test:coverage` – мінімум 90 %.
66
+ - **Документація** `npm run test:docs` – генерує `README.md` з тестових блоків.
67
+ - **Release** `npm run test:release` – перевірка `releases/**/*.test.js`.
68
+
69
+ Кожна публічна функція/клас має **принаймні один тест** у `.test.js` поряд з файлом коду.
70
+
71
+ ## 🧭 Доступні CLI‑методи
72
+
73
+ | Метод | Параметри | Повертає | Опис |
74
+ |-------|-----------|----------|------|
75
+ | `ask(question)` | `string` – підказка | `Promise<string>` | Запит користувачу, повертає введений рядок. |
76
+ | `createInput(stops?)` | `string[]` – слова‑сигнали | `Promise<Input>` | Створює інстанс `Input` з автоскасуванням. |
77
+ | `select(config)` | `{title, prompt, options, console, ask?}` | `Promise<{index, value}>` | Виводить нумерований список, повертає вибір. |
78
+ | `next(conf?)` | `string|array` – послідовність клавіш | `Promise<string>` | Чекає на натискання клавіші (або набору). |
79
+ | `pause(ms)` | `number` – мілісекунди | `Promise<void>` | Затримка виконання. |
80
+ | `CLIInputAdapter.requestForm(form, {silent})` | `UIForm` + опції | `Promise<FormMessage>` | Показує форму, проходить по полях, валідує, повертає результат або `escaped:true`. |
81
+ | `CLIInputAdapter.requestSelect(config)` | `config` (аналог `select`) | `Promise<InputMessage>` | Викликає `select`, обгортає результат у `InputMessage`. |
82
+ | `CLIInputAdapter.requestInput(config)` | `{prompt, id, label, name}` | `Promise<InputMessage>` | Простий рядковий запит. |
83
+
84
+ ## 📝 Рекомендації для розробників
85
+
86
+ 1. **JSDoc** – кожна функція/клас має повний опис (`@param`, `@returns`, `@throws`). Це забезпечує автодоповнення у IDE без TypeScript.
87
+ 2. **Імпорти** – використовуйте `import { … } from '@nan0web/ui-cli'` лише в точках входу.
88
+ 3. **Структура** – залишайте `src/` чистим, `tests/` поруч, `types/` лише декларації (`*.d.ts`), `README.md` генерується з тестів.
89
+ 4. **Локальність** – UI‑текст передається через `i18n` (передача функції `t`). В `playground` вже реалізовано приклад.
90
+ 5. **Нульові залежності** – не підключайте сторонні пакети у `ui-cli`; лише Node‑вбудовані (`readline`, `process`).
91
+
92
+ ## 🌐 Підтримка та внесок
93
+
94
+ - **Bug‑репорт** – відкривайте Issue у репозиторії.
95
+ - **Pull‑request** – додайте нові функції з тестами та оновленою документацією.
96
+ - **CI** – автоматично запускає `npm test` та `npm run test:coverage`.
97
+
98
+ > **UI‑CLI відповідає** – коли команда виконується без помилок, коли тестове покриття достатнє, коли документація генерується без «шуму».
99
+
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "nodenext",
5
+ "lib": ["esnext"],
6
+ "declaration": true,
7
+ "declarationMap": false,
8
+ "emitDeclarationOnly": true,
9
+ "outDir": "./types",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "moduleResolution": "nodenext",
16
+ "allowSyntheticDefaultImports": true,
17
+ "noImplicitAny": false,
18
+ "allowJs": true,
19
+ "checkJs": true
20
+ },
21
+ "include": ["src/**/*"],
22
+ "exclude": ["node_modules", "src/**/*.test.js"]
23
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * CLI specific adapter extending the generic {@link BaseInputAdapter}.
3
+ * Implements concrete `ask` and `select` helpers that rely on the CLI utilities.
4
+ */
5
+ export default class CLIInputAdapter extends BaseInputAdapter {
6
+ /**
7
+ * Interactively fill a {@link UIForm} field‑by‑field.
8
+ *
9
+ * @param {UIForm} form – Form definition to be filled.
10
+ * @param {Object} options – Request options.
11
+ * @param {boolean} [options.silent=true] Suppress title output when `true`.
12
+ * @returns {Promise<FormMessage>} Message with `escaped` = true on cancel,
13
+ * otherwise `escaped` = false and the completed form attached as `form`.
14
+ */
15
+ requestForm(form: UIForm, options?: {
16
+ silent?: boolean | undefined;
17
+ }): Promise<FormMessage>;
18
+ /**
19
+ * Request a selection from a list of options.
20
+ *
21
+ * @param {Object} config – Selection configuration.
22
+ * @param {string} config.title – Title displayed before the list.
23
+ * @param {string} config.prompt – Prompt text.
24
+ * @param {Array<string>|Map<string,string>|Array<{label:string,value:string}>} config.options – Options to choose from.
25
+ * @param {string} config.id – Identifier for the resulting message.
26
+ * @returns {Promise<BaseInputMessage>} Message containing chosen value and metadata.
27
+ */
28
+ requestSelect(config: {
29
+ title: string;
30
+ prompt: string;
31
+ options: Array<string> | Map<string, string> | Array<{
32
+ label: string;
33
+ value: string;
34
+ }>;
35
+ id: string;
36
+ }): Promise<BaseInputMessage>;
37
+ /**
38
+ * Simple string input request.
39
+ *
40
+ * @param {Object} config Input configuration.
41
+ * @param {string} config.prompt Prompt text.
42
+ * @param {string} config.id Identifier for the resulting message.
43
+ * @param {string} [config.label] Optional label used as fallback.
44
+ * @param {string} [config.name] Optional name used as fallback.
45
+ * @returns {Promise<BaseInputMessage>} Message containing the entered text.
46
+ */
47
+ requestInput(config: {
48
+ prompt: string;
49
+ id: string;
50
+ label?: string | undefined;
51
+ name?: string | undefined;
52
+ }): Promise<BaseInputMessage>;
53
+ /** @inheritDoc */
54
+ ask(question: any): Promise<any>;
55
+ /** @inheritDoc */
56
+ select(config: any): Promise<{
57
+ index: number; /** @type {UIForm} Form data associated with the message */
58
+ value: string | null;
59
+ }>;
60
+ }
61
+ export type FormMessageValue = Partial<UIForm>;
62
+ export type InputMessageValue = Partial<Message> | null;
63
+ import { InputAdapter as BaseInputAdapter } from '@nan0web/ui';
64
+ import { UIForm } from '@nan0web/ui';
65
+ /** @typedef {Partial<UIForm>} FormMessageValue */
66
+ /** @typedef {Partial<Message> | null} InputMessageValue */
67
+ /**
68
+ * Extends the generic {@link BaseInputMessage} to carry a {@link UIForm}
69
+ * instance alongside the usual input message payload.
70
+ *
71
+ * The original {@link BaseInputMessage} expects a `value` of type
72
+ * {@link InputMessageValue} (a {@link Message} payload). To remain
73
+ * compatible we keep `value` unchanged and store the form data in a
74
+ * separate `form` property.
75
+ */
76
+ declare class FormMessage extends BaseInputMessage {
77
+ /**
78
+ * Creates a {@link FormMessage} from an existing instance or plain data.
79
+ *
80
+ * @param {FormMessage|object} input – Existing message or raw data.
81
+ * @returns {FormMessage}
82
+ */
83
+ static from(input: FormMessage | object): FormMessage;
84
+ /**
85
+ * Creates a new {@link FormMessage}.
86
+ *
87
+ * @param {object} props - Message properties.
88
+ * @param {FormMessageValue} [props.form={}] UIForm instance or data.
89
+ * @param {InputMessageValue} [props.value=null] Retained for compatibility.
90
+ * @param {string[]|string} [props.options=[]] Available options.
91
+ * @param {boolean} [props.waiting=false] Waiting flag.
92
+ * @param {boolean} [props.escaped=false] Escape flag.
93
+ */
94
+ constructor(props?: {
95
+ form?: Partial<UIForm> | undefined;
96
+ value?: InputMessageValue | undefined;
97
+ options?: string | string[] | undefined;
98
+ waiting?: boolean | undefined;
99
+ escaped?: boolean | undefined;
100
+ });
101
+ /** @type {UIForm} Form data associated with the message */
102
+ form: UIForm;
103
+ }
104
+ import { InputMessage as BaseInputMessage } from '@nan0web/ui';
105
+ import { Message } from '@nan0web/co';
106
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ export const renderers: Map<string, (data: any) => string>;
2
+ export default CLIInputAdapter;
3
+ import CLIInputAdapter from './InputAdapter.js';
4
+ import { CancelError } from '@nan0web/ui/core';
5
+ import { createInput } from './ui/index.js';
6
+ import { ask } from './ui/index.js';
7
+ import { Input } from './ui/index.js';
8
+ import { select } from './ui/index.js';
9
+ import { next } from './ui/index.js';
10
+ import { pause } from './ui/index.js';
11
+ export { CLIInputAdapter, CancelError, createInput, ask, Input, select, next, pause };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export class CancelError extends Error {
2
+ constructor(message?: string);
3
+ }
@@ -0,0 +1,4 @@
1
+ export { CancelError } from "@nan0web/ui/core";
2
+ export { select } from "./select.js";
3
+ export { Input, ask, createInput } from "./input.js";
4
+ export { next, pause } from "./next.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Helper to ask a question
3
+ * @param {string} question
4
+ */
5
+ export function ask(question: string): Promise<any>;
6
+ export function createInput(stops?: any[]): (question: string, loop?: boolean | Function | undefined, nextQuestion?: false | Function | undefined) => Promise<Input>;
7
+ export class Input {
8
+ constructor(input?: {});
9
+ value: string;
10
+ stops: any[];
11
+ get cancelled(): boolean;
12
+ toString(): string;
13
+ #private;
14
+ }
15
+ export default createInput;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Waits for the confirmation message (input) from user
3
+ * @param {string | string[] | undefined} conf - Confirmation message or one of messages if array or any if undefined.
4
+ * @returns {Promise<string>}
5
+ */
6
+ export function next(conf?: string | string[] | undefined): Promise<string>;
7
+ /**
8
+ * Make a pause.
9
+ * @param {number} ms - Amount in miliseconds
10
+ * @returns {Promise<void>}
11
+ */
12
+ export function pause(ms: number): Promise<void>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Generic selection prompt for CLI.
3
+ * Automatically creates its own input handler.
4
+ *
5
+ * @param {Object} config
6
+ * @param {string} config.title - Title shown before the list (e.g. "Select currency:")
7
+ * @param {string} config.prompt - Main prompt text (e.g. "Choose (1-3): ")
8
+ * @param {string} [config.invalidPrompt] - Retry message when input is invalid
9
+ * @param {Array<string> | Map<string, string> | Array<{ label: string, value: string }>} config.options - List of displayable options
10
+ * @param {Object} config.console - Logger (console.info, console.error, etc.)
11
+ * @param {Function} [config.ask] - Input handler
12
+ *
13
+ * @returns {Promise<{ index: number, value: string | null }>} Selected option
14
+ * @throws {CancelError} if the user cancels
15
+ */
16
+ export function select({ title, prompt, invalidPrompt, options, console, ask, }: {
17
+ title: string;
18
+ prompt: string;
19
+ invalidPrompt?: string | undefined;
20
+ options: Array<string> | Map<string, string> | Array<{
21
+ label: string;
22
+ value: string;
23
+ }>;
24
+ console: any;
25
+ ask?: Function | undefined;
26
+ }): Promise<{
27
+ index: number;
28
+ value: string | null;
29
+ }>;
30
+ export default select;