@nan0web/ui 1.0.3 → 1.1.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.
Files changed (43) hide show
  1. package/README.md +18 -20
  2. package/package.json +15 -16
  3. package/src/App/Command/DepsCommand.js +29 -0
  4. package/src/App/Core/UI.js +1 -50
  5. package/src/App/Core/Widget.js +4 -6
  6. package/src/App/User/Command/Message.js +2 -43
  7. package/src/App/User/Command/index.js +2 -8
  8. package/src/App/User/UserApp.js +31 -11
  9. package/src/README.md.js +33 -33
  10. package/src/StdIn.js +12 -13
  11. package/src/View/View.js +7 -7
  12. package/src/core/Form/Form.js +8 -6
  13. package/src/core/Form/Input.js +13 -13
  14. package/src/core/Form/Message.js +6 -5
  15. package/src/core/InputAdapter.js +2 -2
  16. package/src/core/Message/Message.js +109 -19
  17. package/src/core/Message/OutputMessage.js +7 -7
  18. package/src/core/Message/index.js +3 -4
  19. package/src/core/Stream.js +11 -10
  20. package/src/core/UiAdapter.js +216 -0
  21. package/src/core/index.js +10 -7
  22. package/src/index.js +4 -4
  23. package/types/App/Command/DepsCommand.d.ts +16 -0
  24. package/types/App/Core/UI.d.ts +2 -13
  25. package/types/App/Core/Widget.d.ts +6 -7
  26. package/types/App/User/Command/Message.d.ts +2 -29
  27. package/types/App/User/Command/index.d.ts +2 -4
  28. package/types/App/User/UserApp.d.ts +14 -7
  29. package/types/StdIn.d.ts +13 -13
  30. package/types/View/View.d.ts +6 -6
  31. package/types/core/Form/Form.d.ts +2 -5
  32. package/types/core/Form/Input.d.ts +4 -4
  33. package/types/core/Form/Message.d.ts +5 -10
  34. package/types/core/Intent.d.ts +91 -0
  35. package/types/core/Message/Message.d.ts +58 -15
  36. package/types/core/Message/OutputMessage.d.ts +3 -3
  37. package/types/core/Message/index.d.ts +3 -4
  38. package/types/core/Stream.d.ts +4 -4
  39. package/types/core/UiAdapter.d.ts +122 -0
  40. package/types/core/index.d.ts +6 -5
  41. package/types/index.d.ts +4 -4
  42. package/src/App/User/Command/Options.js +0 -48
  43. package/src/core/Message/InputMessage.js +0 -119
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Intent represents the user's declared will to perform an action.
3
+ * It is interface-agnostic and can be validated or executed without
4
+ * any UI, CLI, or external dependencies.
5
+ *
6
+ * An Intent may be:
7
+ * - Ready: all required data is valid → can be executed immediately
8
+ * - Partial: some data missing or invalid → requires user action
9
+ * - Invalid: cannot be fulfilled under any interface (e.g., restricted field)
10
+ *
11
+ * @example
12
+ * const intent = new Intent({
13
+ * target: LoginMessage,
14
+ * body: { username: "alice" }
15
+ * })
16
+ * if (!intent.isReady()) {
17
+ * const validMsg = await handleIntent(intent, adapter)
18
+ * }
19
+ */
20
+ declare class Intent extends Message {
21
+ /**
22
+ * Create a new Intent.
23
+ *
24
+ * @param {object} input
25
+ * @param {typeof Message} input.target - message class this intent wants to create
26
+ * @param {any} [input.context] - execution context
27
+ * @param {object} [body] - partial or complete data for the target message
28
+ */
29
+ constructor({ target, context, ...body }?: {
30
+ target: typeof Message;
31
+ context?: any;
32
+ });
33
+ /**
34
+ * The target Message class this Intent aims to produce.
35
+ * Must be a class with a static `.Body` schema.
36
+ *
37
+ * @type {typeof Message | null}
38
+ */
39
+ target: typeof Message | null;
40
+ /**
41
+ * Optional context for execution (session, locale, state, etc.).
42
+ *
43
+ * @type {any}
44
+ */
45
+ context: any;
46
+ /**
47
+ * Validates all fields in the Intent's body against the target's schema.
48
+ *
49
+ * Checks:
50
+ * - Required fields presence and non-emptiness
51
+ * - Pattern matching (RegExp)
52
+ * - Value within allowed options (if defined)
53
+ * - Custom validation logic (if `validate` function is defined)
54
+ *
55
+ * @returns {Map<string, string>} Map of field → error message, empty if valid
56
+ */
57
+ validateIntent(): Map<string, string>;
58
+ /**
59
+ * Executes the Intent by creating a valid instance of the target message.
60
+ *
61
+ * If the Intent is not ready (has validation errors), returns null.
62
+ *
63
+ * @returns {Message | null} the created message, or null if invalid
64
+ */
65
+ execute(): Message | null;
66
+ /**
67
+ * Checks if the Intent can be executed immediately, without user input.
68
+ *
69
+ * @returns {boolean} true if all required fields are valid
70
+ */
71
+ isReady(): boolean;
72
+ /**
73
+ * Converts the Intent to a plain object for logging or inspection.
74
+ *
75
+ * @returns {object} serializable object with intent, body, and context
76
+ */
77
+ toObject(): object;
78
+ }
79
+ declare namespace Intent {
80
+ /**
81
+ * Handles an Intent by fulfilling missing or invalid fields.
82
+ *
83
+ * @param {Intent} intent - the declared intent to handle
84
+ * @param {object} inputAdapter - must have `.ask(prompt)`
85
+ * @returns {Promise<Message | null>} resolved when intent is fulfilled
86
+ * @throws {CancelError} if user cancels
87
+ */
88
+ function handleIntent(intent: Intent, inputAdapter: object): Promise<Message | null>;
89
+ }
90
+ export default Intent;
91
+ import { Message } from "@nan0web/co";
@@ -1,11 +1,33 @@
1
- export default UIMessage;
2
1
  /**
3
- * Base UI message class.
2
+ * @typedef {Object} MessageBodySchema
3
+ * @property {boolean} [required]
4
+ * @property {string} [help]
5
+ * @property {RegExp} [pattern]
6
+ * @property {string[]} [options]
7
+ * @property {*} [defaultValue]
8
+ * @property {Function} [validate]
9
+ */
10
+ /**
11
+ * Base message class for UI communications.
12
+ * A message holds structured data (body) defined by a static Body class.
13
+ * It can represent commands, forms, alerts, or any UI unit.
14
+ *
15
+ * @class UiMessage
16
+ * @extends Message
4
17
  *
5
- * @class UIMessage
6
- * @extends BaseMessage
18
+ * @example
19
+ * class UserLoginMessage extends UiMessage {
20
+ * static Body = class {
21
+ * static username = { required: true, help: "Enter username" }
22
+ * static password = { required: true, type: "password" }
23
+ * constructor({ username = "", password = "" }) {
24
+ * this.username = username
25
+ * this.password = password
26
+ * }
27
+ * }
28
+ * }
7
29
  */
8
- declare class UIMessage extends BaseMessage {
30
+ export default class UiMessage extends Message {
9
31
  static TYPES: {
10
32
  TEXT: string;
11
33
  FORM: string;
@@ -18,14 +40,22 @@ declare class UIMessage extends BaseMessage {
18
40
  NAVIGATION: string;
19
41
  };
20
42
  /**
21
- * Creates a UIMessage instance from plain data.
43
+ * Creates a UiMessage instance from plain data.
22
44
  *
23
45
  * @param {Object} data - Message data.
24
- * @returns {UIMessage}
46
+ * @returns {UiMessage}
47
+ */
48
+ static from(data: any): UiMessage;
49
+ /**
50
+ * Initializes body from input using static Body schema.
51
+ *
52
+ * @param {Object} input - Input object.
53
+ * @param {Function} BodyClass - Static body class with defaults and schema.
54
+ * @returns {Object} Parsed body.
25
55
  */
26
- static from(data: any): UIMessage;
56
+ static parseBody(input: any, BodyClass: Function): any;
27
57
  /**
28
- * Creates a UIMessage.
58
+ * Creates a UiMessage.
29
59
  *
30
60
  * @param {Object} [input={}] - Message properties.
31
61
  */
@@ -35,16 +65,29 @@ declare class UIMessage extends BaseMessage {
35
65
  /** @type {string} */
36
66
  id: string;
37
67
  /**
38
- * Checks if the message type is valid.
68
+ * Validates the message body against its schema.
39
69
  *
40
- * @returns {boolean}
70
+ * NOTE: The signature must exactly match `Message.validate` – it returns a
71
+ * `Map<string,string>` regardless of the generic type, otherwise TypeScript
72
+ * reports incompatibility with the base class.
73
+ *
74
+ * @param {any} [body=this.body] - Optional body to validate.
75
+ * @returns {Map<string,string>} Map of validation errors, empty if valid.
41
76
  */
42
- isValidType(): boolean;
77
+ validate(body?: any): Map<string, string>;
43
78
  /**
44
- * Checks whether the message contains any body content.
79
+ * Checks if the message type is valid.
45
80
  *
46
81
  * @returns {boolean}
47
82
  */
48
- isEmpty(): boolean;
83
+ isValidType(): boolean;
49
84
  }
50
- import { Message as BaseMessage } from "@nan0web/co";
85
+ export type MessageBodySchema = {
86
+ required?: boolean | undefined;
87
+ help?: string | undefined;
88
+ pattern?: RegExp | undefined;
89
+ options?: string[] | undefined;
90
+ defaultValue?: any;
91
+ validate?: Function | undefined;
92
+ };
93
+ import { Message } from "@nan0web/co";
@@ -2,9 +2,9 @@
2
2
  * OutputMessage – message sent from the system to the UI.
3
3
  *
4
4
  * @class OutputMessage
5
- * @extends UIMessage
5
+ * @extends UiMessage
6
6
  */
7
- export default class OutputMessage extends UIMessage {
7
+ export default class OutputMessage extends UiMessage {
8
8
  static PRIORITY: {
9
9
  LOW: number;
10
10
  NORMAL: number;
@@ -50,4 +50,4 @@ export default class OutputMessage extends UIMessage {
50
50
  */
51
51
  toJSON(): any;
52
52
  }
53
- import UIMessage from "./Message.js";
53
+ import UiMessage from "./Message.js";
@@ -1,5 +1,4 @@
1
- export default UIMessage;
2
- import UIMessage from "./Message.js";
3
- import InputMessage from "./InputMessage.js";
1
+ export default UiMessage;
2
+ import UiMessage from "./Message.js";
4
3
  import OutputMessage from "./OutputMessage.js";
5
- export { UIMessage, InputMessage, OutputMessage };
4
+ export { UiMessage, OutputMessage };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Agnostic UI stream for processing progress.
2
+ * Agnostic UI stream for processing progress using async generators.
3
3
  *
4
4
  * @class UIStream
5
5
  */
@@ -13,15 +13,15 @@ export default class UIStream {
13
13
  */
14
14
  static createProcessor(signal: AbortSignal, processorFn: () => Promise<StreamEntry>): () => AsyncGenerator<StreamEntry>;
15
15
  /**
16
- * Runs a generator with progress callbacks and abort handling.
16
+ * Runs an async generator with progress callbacks and abort handling.
17
17
  *
18
18
  * @param {AbortSignal} signal - Abort signal.
19
- * @param {Function} generator - Function returning an async iterator.
19
+ * @param {() => AsyncGenerator<StreamEntry>} generatorFn - Function that returns an async generator.
20
20
  * @param {Function} [onProgress] - Called with (progress, item).
21
21
  * @param {Function} [onError] - Called with (errorMessage, item).
22
22
  * @param {Function} [onComplete] - Called with (item) when done.
23
23
  * @returns {Promise<void>}
24
24
  */
25
- static process(signal: AbortSignal, generator: Function, onProgress?: Function, onError?: Function, onComplete?: Function): Promise<void>;
25
+ static process(signal: AbortSignal, generatorFn: () => AsyncGenerator<StreamEntry>, onProgress?: Function, onError?: Function, onComplete?: Function): Promise<void>;
26
26
  }
27
27
  import StreamEntry from "./StreamEntry.js";
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Generates a UIForm from a static Body schema.
3
+ *
4
+ * @param {Function} BodyClass - Class defining field schema.
5
+ * @param {Object} [options={}] - Generation options.
6
+ * @param {Object} [options.initialState={}] - Pre-filled form values.
7
+ * @param {Function} [options.t] - Optional translation function.
8
+ * @returns {UIForm} Form instance ready for input.
9
+ */
10
+ export function generateForm(BodyClass: Function, options?: {
11
+ initialState?: any;
12
+ t?: Function | undefined;
13
+ }): UIForm;
14
+ /**
15
+ * Unified UI Adapter that handles both input and output operations.
16
+ * It manages user interactions and rendering of messages, forms, and progress.
17
+ *
18
+ * @class UiAdapter
19
+ * @extends EventProcessor
20
+ *
21
+ * @example
22
+ * const adapter = new UiAdapter()
23
+ * adapter.output = new View()
24
+ *
25
+ * const result = await adapter.requireInput(new LoginMessage())
26
+ * console.log(result) // { username: "user", password: "pass" }
27
+ */
28
+ export default class UiAdapter extends EventProcessor {
29
+ static CancelError: typeof CancelError;
30
+ /** @returns {typeof CancelError} */
31
+ get CancelError(): typeof CancelError;
32
+ /** @type {OutputAdapter | null} Output interface for rendering */
33
+ output: OutputAdapter | null;
34
+ /**
35
+ * Starts listening for input and emits an `input` event.
36
+ *
37
+ * @returns {void}
38
+ */
39
+ start(): void;
40
+ /**
41
+ * Stops listening for input and output streams.
42
+ * Default implementation does nothing; override in subclasses to perform cleanup.
43
+ *
44
+ * @returns {void}
45
+ */
46
+ stop(): void;
47
+ /**
48
+ * Checks whether the adapter is ready to receive input.
49
+ *
50
+ * @returns {boolean} Always true in base class; override for specific checks.
51
+ */
52
+ isReady(): boolean;
53
+ /**
54
+ * Helper to ask a question.
55
+ * Must be implemented by subclasses.
56
+ *
57
+ * @param {string} question - Question to ask the user.
58
+ * @returns {Promise<string>} User's response.
59
+ * @throws {Error} If not implemented in subclass.
60
+ */
61
+ ask(question: string): Promise<string>;
62
+ /**
63
+ * Generic selection prompt.
64
+ * Must be implemented by subclasses.
65
+ *
66
+ * @param {object} config - Selection configuration.
67
+ * @param {string[]} [config.options=[]] - List of options to choose from.
68
+ * @returns {Promise<{ index: number, value: string | null }>} Selected option.
69
+ * @throws {Error} If not implemented in subclass.
70
+ */
71
+ select(config: {
72
+ options?: string[] | undefined;
73
+ }): Promise<{
74
+ index: number;
75
+ value: string | null;
76
+ }>;
77
+ /**
78
+ * Process a UIForm and return its result.
79
+ *
80
+ * This default implementation follows an **agnostic UI** approach:
81
+ * it simply returns the form instance (with optional initial state merged)
82
+ * without UI interaction. Concrete adapters (CLI, Web, etc.) can override
83
+ * this method to render the form, collect user input and return a richer
84
+ * result object (`{ form, cancelled }`).
85
+ *
86
+ * @param {UIForm} form - The UIForm instance to process.
87
+ * @param {object} [initialState={}] - Pre‑filled values for the form.
88
+ * @returns {Promise<{ form: UIForm, cancelled?: boolean }>} Form processing result.
89
+ */
90
+ processForm(form: UIForm, initialState?: object): Promise<{
91
+ form: UIForm;
92
+ cancelled?: boolean;
93
+ }>;
94
+ /**
95
+ * Ensures a message's body is fully and validly filled.
96
+ * Generates a form from the message's static Body schema,
97
+ * then iteratively collects input until all fields are valid or cancelled.
98
+ *
99
+ * @template {UIMessage} T
100
+ * @param {T} msg - Message instance needing input.
101
+ * @returns {Promise<T['body']>} Updated and validated message body.
102
+ *
103
+ * @example
104
+ * const body = await adapter.requireInput(new LoginMessage({ body: { username: "user" } }))
105
+ * // → prompts for password, returns { username: "user", password: "..." }
106
+ */
107
+ requireInput<T extends UIMessage>(msg: T): Promise<T["body"]>;
108
+ /**
109
+ * Renders a message to the user interface.
110
+ * Must be implemented by subclasses.
111
+ *
112
+ * @param {UIMessage} message - Message to render.
113
+ * @emits rendered
114
+ * @throws {Error} If not implemented in subclass.
115
+ */
116
+ render(message: UIMessage): void;
117
+ }
118
+ import UIForm from "./Form/Form.js";
119
+ import EventProcessor from "@nan0web/event/oop";
120
+ import CancelError from "./Error/CancelError.js";
121
+ import OutputAdapter from "./OutputAdapter.js";
122
+ import UIMessage from "./Message/Message.js";
@@ -1,10 +1,11 @@
1
1
  export { default as InputAdapter } from "./InputAdapter.js";
2
2
  export { default as OutputAdapter } from "./OutputAdapter.js";
3
- export { default as UIStream } from "./Stream.js";
4
- export { default as UIMessage } from "./Message/Message.js";
5
- export { default as InputMessage } from "./Message/InputMessage.js";
6
- export { default as OutputMessage } from "./Message/OutputMessage.js";
3
+ export { default as UiMessage } from "./Message/Message.js";
7
4
  export { default as FormMessage } from "./Form/Message.js";
8
5
  export { default as FormInput } from "./Form/Input.js";
9
- export { default as UIForm } from "./Form/Form.js";
6
+ export { default as UiAdapter } from "./UiAdapter.js";
7
+ import UIStream from "./Stream.js";
8
+ import StreamEntry from "./StreamEntry.js";
9
+ import UIForm from "./Form/Form.js";
10
+ export { UIStream, UIStream as UiStream, StreamEntry, StreamEntry as UiStreamEntry, UIForm, UIForm as UiForm };
10
11
  export { default as Error, CancelError } from "./Error/index.js";
package/types/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  export { default as FormMessage } from "./core/Form/Message.js";
2
2
  export { default as FormInput } from "./core/Form/Input.js";
3
3
  export { default as InputAdapter } from "./core/InputAdapter.js";
4
- export { default as InputMessage } from "./core/Message/InputMessage.js";
5
4
  export { default as OutputAdapter } from "./core/OutputAdapter.js";
6
5
  export { default as OutputMessage } from "./core/Message/OutputMessage.js";
7
- export { default as UIForm } from "./core/Form/Form.js";
8
- export { default as UIMessage } from "./core/Message/Message.js";
9
- export { default as UIStream } from "./core/Stream.js";
6
+ export { default as UiForm } from "./core/Form/Form.js";
7
+ export { default as UiMessage } from "./core/Message/Message.js";
8
+ export { default as UiStream } from "./core/Stream.js";
9
+ export { default as UiAdapter } from "./core/UiAdapter.js";
10
10
  import Frame from "./Frame/Frame.js";
11
11
  import FrameProps from "./Frame/Props.js";
12
12
  import Locale from "./Locale.js";
@@ -1,48 +0,0 @@
1
- import CommandOptions from "../../Command/Options.js"
2
-
3
- /**
4
- * Extends CommandOptions to include user-specific options.
5
- */
6
- class UserAppCommandOptions extends CommandOptions {
7
- /**
8
- * Default option values including inherited ones.
9
- * @type {object}
10
- * @property {boolean} help - Whether help is requested
11
- * @property {string} cwd - Current working directory
12
- * @property {string} user - User name
13
- */
14
- static DEFAULTS = {
15
- ...CommandOptions.DEFAULTS,
16
- user: "",
17
- }
18
-
19
- /** @type {string} User name */
20
- user
21
-
22
- /**
23
- * Creates a new UserAppCommandOptions instance.
24
- * @param {object} props - Options properties
25
- * @param {boolean} [props.help=false] - Whether help is requested
26
- * @param {string} [props.cwd=""] - Current working directory
27
- * @param {string} [props.user=""] - User name
28
- */
29
- constructor(props = {}) {
30
- const {
31
- user = UserAppCommandOptions.DEFAULTS.user,
32
- } = props
33
- super(props)
34
- this.user = String(user)
35
- }
36
-
37
- /**
38
- * Creates a UserAppCommandOptions instance from the given props.
39
- * @param {UserAppCommandOptions|object} props - The properties to create from
40
- * @returns {UserAppCommandOptions} A UserAppCommandOptions instance
41
- */
42
- static from(props) {
43
- if (props instanceof UserAppCommandOptions) return props
44
- return new this(props)
45
- }
46
- }
47
-
48
- export default UserAppCommandOptions
@@ -1,119 +0,0 @@
1
- import { notEmpty } from "@nan0web/types"
2
- import { Message } from "@nan0web/co"
3
-
4
- /** @typedef {Partial<Message> | null} InputMessageValue */
5
-
6
- /**
7
- * Represents a message input with value, options, and metadata.
8
- */
9
- export default class InputMessage {
10
- static ESCAPE = String.fromCharCode(27)
11
- /** @type {Message} Input value */
12
- value
13
-
14
- /** @type {string[]} Available options for this input */
15
- options
16
-
17
- /** @type {boolean} Whether this input is waiting for response */
18
- waiting
19
-
20
- /** @type {number} Timestamp when input was created */
21
- #time
22
-
23
- /**
24
- * Creates a new InputMessage instance.
25
- * @param {object} props - Input message properties
26
- * @param {InputMessageValue} [props.value=null] - Input value
27
- * @param {string[]|string} [props.options=[]] - Available options
28
- * @param {boolean} [props.waiting=false] - Waiting state flag
29
- * @param {boolean} [props.escaped=false] - Sets value to escape when true
30
- */
31
- constructor(props = {}) {
32
- if ("string" === typeof props) {
33
- props = { value: { body: props } }
34
- }
35
- const {
36
- value = { body: "" },
37
- waiting = false,
38
- options = [],
39
- escaped = false,
40
- } = props
41
- this.#time = Date.now()
42
- this.waiting = Boolean(waiting)
43
-
44
- // Properly handle string options by converting to array
45
- this.options = Array.isArray(options) ? options.map(String) : [String(options)]
46
- this.value = Message.from(escaped ? { body: this.ESCAPE } : value)
47
- }
48
-
49
- /**
50
- * Checks if the input value is empty.
51
- * @returns {boolean} True if value is empty or null, false otherwise
52
- */
53
- get empty() {
54
- if (this.value instanceof Message) {
55
- return this.value.empty
56
- }
57
- return null === this.value || 0 === String(this.value).length
58
- }
59
-
60
- /**
61
- * Gets the timestamp when input was created.
62
- * @returns {number} Creation timestamp
63
- */
64
- get time() {
65
- return this.#time
66
- }
67
-
68
- /**
69
- * Returns the escape value.
70
- * @returns {string}
71
- */
72
- get ESCAPE() {
73
- return /** @type {typeof InputMessage} */ (this.constructor).ESCAPE
74
- }
75
-
76
- /**
77
- * Checks if the input is an escape sequence.
78
- * @returns {boolean} True if input value is escape sequence, false otherwise
79
- */
80
- get escaped() {
81
- return this.ESCAPE === this.value.body
82
- }
83
-
84
- /**
85
- * Validates if the input has a non-empty value.
86
- * @returns {boolean} True if input is valid, false otherwise
87
- */
88
- get isValid() {
89
- // An input is valid only if it has a non-empty value and is not an escape sequence
90
- return notEmpty(this.value) && this.value.body !== this.ESCAPE
91
- }
92
-
93
- /**
94
- * Converts the input to a plain object representation.
95
- * @returns {object} Object with all properties including timestamp
96
- */
97
- toObject() {
98
- return { ...this, time: this.time }
99
- }
100
-
101
- /**
102
- * Converts the input to a string representation including timestamp.
103
- * @returns {string} String representation with timestamp and value
104
- */
105
- toString() {
106
- const date = new Date(this.time)
107
- return `${date.toISOString().split(".")[0]} ${this.value}`
108
- }
109
-
110
- /**
111
- * Creates an InputMessage instance from the given value.
112
- * @param {InputMessage|object|string} value - The value to create from
113
- * @returns {InputMessage} An InputMessage instance
114
- */
115
- static from(value) {
116
- if (value instanceof InputMessage) return value
117
- return new this(value)
118
- }
119
- }