@nan0web/ui-cli 1.0.2 → 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.
@@ -1,36 +1,128 @@
1
1
  /**
2
- * CLIInputAdapter – bridges UI‑CLI utilities with generic UI core.
2
+ * CLiInputAdapter – bridges UI‑CLI utilities with generic UI core.
3
3
  *
4
4
  * @module InputAdapter
5
5
  */
6
6
 
7
- import { Message } from "@nan0web/co"
8
7
  import {
9
- UIForm,
8
+ UiForm,
10
9
  InputAdapter as BaseInputAdapter,
10
+ UiMessage,
11
11
  } from "@nan0web/ui"
12
- import { ask } from "./ui/input.js"
13
- import { select } from "./ui/select.js"
14
12
  import { CancelError } from "@nan0web/ui/core"
15
13
 
14
+ import { ask as baseAsk, createInput, createPredefinedInput, Input } from "./ui/input.js"
15
+ import { select as baseSelect } from "./ui/select.js"
16
+ import { generateForm } from "./ui/form.js"
17
+
18
+ /** @typedef {import("./ui/select.js").InputFn} InputFn */
19
+ /** @typedef {import("./ui/select.js").ConsoleLike} ConsoleLike */
20
+
16
21
  /**
17
22
  * Extends the generic {@link BaseInputAdapter} with CLI‑specific behaviour.
18
23
  *
19
24
  * @class
20
25
  * @extends BaseInputAdapter
21
26
  */
22
- export default class CLIInputAdapter extends BaseInputAdapter {
27
+ export default class CLiInputAdapter extends BaseInputAdapter {
28
+ /** @type {string[]} Queue of predefined answers. */
29
+ #answers = []
30
+ /** @type {number} Current position in the answers queue. */
31
+ #cursor = 0
32
+ /** @type {ConsoleLike} */
33
+ #console
34
+ #stdout
35
+ /** @type {Map<string, () => Promise<Function>>} */
36
+ #components = new Map()
37
+
38
+ constructor(options = {}) {
39
+ super()
40
+ const {
41
+ predefined = process.env.PLAY_DEMO_SEQUENCE ?? [],
42
+ divider = process.env.PLAY_DEMO_DIVIDER ?? ",",
43
+ console: initialConsole = console,
44
+ stdout = process.stdout,
45
+ components = new Map(),
46
+ } = options
47
+
48
+ this.#console = initialConsole
49
+ this.#stdout = stdout
50
+ this.#components = components
51
+
52
+ if (Array.isArray(predefined)) {
53
+ this.#answers = predefined.map(v => String(v))
54
+ } else if (typeof predefined === "string") {
55
+ this.#answers = predefined
56
+ .split(divider)
57
+ .map(v => v.trim())
58
+ .filter(Boolean)
59
+ } else {
60
+ this.#answers = []
61
+ }
62
+ }
63
+
64
+ /** @returns {ConsoleLike} */
65
+ get console() { return this.#console }
66
+ /** @returns {NodeJS.WriteStream} */
67
+ get stdout() { return this.#stdout }
68
+
69
+ #nextAnswer() {
70
+ if (this.#cursor < this.#answers.length) {
71
+ const val = this.#answers[this.#cursor]
72
+ this.#cursor++
73
+ return val
74
+ }
75
+ return null
76
+ }
77
+
78
+ /**
79
+ * Normalise a value that can be either a raw string or an {@link Input}
80
+ * instance (which carries the actual value in its `value` property).
81
+ *
82
+ * @param {any} val – Raw value or {@link Input}.
83
+ * @returns {string} Plain string value.
84
+ */
85
+ #normalise(val) {
86
+ if (val && typeof val === "object" && "value" in val) {
87
+ return String(val.value)
88
+ }
89
+ return String(val ?? "")
90
+ }
91
+
92
+ /**
93
+ * Create a handler with stop words that supports predefined answers.
94
+ *
95
+ * @param {string[]} stops - Stop words for cancellation.
96
+ * @returns {InputFn}
97
+ */
98
+ createHandler(stops = []) {
99
+ const self = this
100
+ return async (question, loop = false, nextQuestion = undefined) => {
101
+ const predefined = self.#nextAnswer()
102
+ if (predefined !== null) {
103
+ this.stdout.write(`${question}${predefined}\n`)
104
+ const input = new Input({ value: predefined, stops })
105
+ if (input.cancelled) {
106
+ throw new CancelError("Cancelled via stop word")
107
+ }
108
+ return input
109
+ }
110
+ const interactive = createInput(stops)
111
+ return interactive(question, loop, nextQuestion)
112
+ }
113
+ }
114
+
23
115
  /**
24
116
  * Prompt the user for a full form, handling navigation and validation.
25
117
  *
26
- * @param {UIForm} form - Form definition to present.
118
+ * @param {UiForm} form - Form definition to present.
27
119
  * @param {Object} [options={}]
28
120
  * @param {boolean} [options.silent=true] - Suppress console output if `true`.
29
121
  * @returns {Promise<Object>} Result object containing form data and meta‑information.
30
122
  */
31
123
  async requestForm(form, options = {}) {
32
124
  const { silent = true } = options
33
- if (!silent) console.log(`\n${form.title}\n`)
125
+ if (!silent) this.console.info(`\n${form.title}\n`)
34
126
 
35
127
  let formData = { ...form.state }
36
128
  let idx = 0
@@ -38,23 +130,56 @@ export default class CLIInputAdapter extends BaseInputAdapter {
38
130
  while (idx < form.fields.length) {
39
131
  const field = form.fields[idx]
40
132
  const prompt = `${field.label || field.name}${field.required ? " *" : ""}: `
41
- const answer = await this.ask(prompt)
42
133
 
43
- if (["", "esc"].includes(answer)) {
44
- // @ts-ignore `UIForm` may not expose `id` in its TS definition
45
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
134
+ // ---- Handle select fields (options) ----------------------------------------
135
+ if (Array.isArray(field.options ?? 0) && field.options.length) {
136
+ const options = /** @type {any[]} */ (field.options)
137
+
138
+ const predefined = this.#nextAnswer()
139
+ const selConfig = {
140
+ title: field.label,
141
+ prompt: "Choose (number): ",
142
+ options: options.map(opt =>
143
+ typeof opt === "string"
144
+ ? { label: opt, value: opt }
145
+ : opt
146
+ ),
147
+ console: { info: this.console.info.bind(this.console) },
148
+ }
149
+
150
+ let selResult
151
+ if (predefined !== null) {
152
+ selConfig.ask = createPredefinedInput([predefined], this.console)
153
+ selResult = await this.select(selConfig)
154
+ } else {
155
+ selConfig.ask = this.createHandler(["cancel", "quit", "exit"])
156
+ selResult = await this.select(selConfig)
157
+ }
158
+
159
+ const chosen = selResult?.value
160
+ if (!options.includes(chosen)) {
161
+ this.console.error("\nEnumeration must have one value")
162
+ continue
163
+ }
164
+ formData[field.name] = String(chosen)
165
+ idx++
166
+ continue
167
+ }
168
+ // ---------------------------------------------------------------------------
169
+
170
+ const rawAnswer = await this.ask(prompt)
171
+ const answerStr = this.#normalise(rawAnswer)
172
+
173
+ if (["", "esc"].includes(answerStr)) {
46
174
  return {
47
- // @ts-ignore
48
- body: { action: "form-cancel", escaped: true, form: {}, id: form.id },
175
+ body: { action: "form-cancel", cancelled: true, form: {} },
49
176
  form: {},
50
- escaped: true,
177
+ cancelled: true,
51
178
  action: "form-cancel",
52
- // @ts-ignore
53
- id: form.id,
54
179
  }
55
180
  }
56
181
 
57
- const trimmed = answer.trim()
182
+ const trimmed = answerStr.trim()
58
183
  if (trimmed === "::prev" || trimmed === "::back") {
59
184
  idx = Math.max(0, idx - 1)
60
185
  continue
@@ -68,13 +193,13 @@ export default class CLIInputAdapter extends BaseInputAdapter {
68
193
  continue
69
194
  }
70
195
  if (field.required && trimmed === "") {
71
- console.log("\nField is required.")
196
+ this.console.info("\nField is required.")
72
197
  continue
73
198
  }
74
199
  const schema = field.constructor
75
200
  const { isValid, errors } = form.validateValue(field.name, trimmed, schema)
76
201
  if (!isValid) {
77
- console.log("\n" + Object.values(errors).join("\n"))
202
+ this.console.warn("\n" + Object.values(errors).join("\n"))
78
203
  continue
79
204
  }
80
205
  formData[field.name] = trimmed
@@ -82,39 +207,78 @@ export default class CLIInputAdapter extends BaseInputAdapter {
82
207
  }
83
208
 
84
209
  const finalForm = form.setData(formData)
85
- const { isValid, errors } = finalForm.validate()
86
- if (!isValid) {
87
- console.log("\n" + Object.values(errors).join("\n"))
210
+ const errors = finalForm.validate()
211
+ if (errors.size) {
212
+ this.console.warn("\n" + Object.values(errors).join("\n"))
88
213
  return await this.requestForm(form, options)
89
214
  }
90
-
91
- // @ts-ignore – `UIForm` may not expose `id`
92
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
93
215
  return {
94
- // @ts-ignore
95
- body: { action: "form-submit", escaped: false, form: finalForm, id: form.id },
216
+ body: { action: "form-submit", cancelled: false, form: finalForm },
96
217
  form: finalForm,
97
- escaped: false,
218
+ cancelled: false,
98
219
  action: "form-submit",
99
- // @ts-ignore
100
- id: form.id,
101
220
  }
102
221
  }
103
222
 
223
+ /**
224
+ * Render a UI component in the CLI environment.
225
+ *
226
+ * The current CLI adapter only supports simple textual rendering.
227
+ *
228
+ * @param {string} component - Component name (e.g. `"Alert"`).
229
+ * @param {object} props - Props object passed to the component.
230
+ * @returns {Promise<void>}
231
+ */
232
+ async render(component, props) {
233
+ const compLoader = this.#components.get(component)
234
+ if (compLoader) {
235
+ try {
236
+ const compFn = await compLoader()
237
+ if (typeof compFn === "function") {
238
+ compFn.call(this, props)
239
+ return
240
+ }
241
+ } catch (/** @type {any} */ err) {
242
+ this.console.error(`Failed to render component "${component}": ${err.message}`)
243
+ this.console.debug?.(err.stack)
244
+ }
245
+ }
246
+ if (props && typeof props === "object") {
247
+ const { variant, content } = props
248
+ const prefix = variant ? `[${variant}]` : ""
249
+ this.console.info(`${prefix} ${String(content)}`)
250
+ } else {
251
+ this.console.info(String(component))
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Process a full form – thin wrapper around {@link requestForm}.
257
+ *
258
+ * @param {UiForm} form - Form definition.
259
+ * @param {object} [_state] - Unused, kept for compatibility with `CLiMessage`.
260
+ * @returns {Promise<Object>} Same shape as {@link requestForm} result.
261
+ */
262
+ async processForm(form, _state) {
263
+ return this.requestForm(form)
264
+ }
265
+
104
266
  /**
105
267
  * Prompt the user to select an option from a list.
106
268
  *
107
- * @param {Object} config - Configuration passed to {@link select}.
108
- * @returns {Promise<any>} Selected value, or empty string on cancellation.
269
+ * @param {Object} config - Configuration object.
270
+ * @returns {Promise<string>} Selected value (or empty string on cancel).
109
271
  */
110
272
  async requestSelect(config) {
111
273
  try {
112
- const result = await this.select({
113
- title: config.title ?? "Select an option:",
114
- prompt: config.prompt ?? "Choose (1‑N): ",
115
- options: config.options,
116
- console: console,
117
- })
274
+ const predefined = this.#nextAnswer()
275
+ if (!config.console) config.console = { info: () => { } }
276
+
277
+ if (predefined !== null) {
278
+ config.yes = true
279
+ config.ask = createPredefinedInput([predefined], config.console)
280
+ }
281
+ const result = await this.select(config)
118
282
  return result.value
119
283
  } catch (e) {
120
284
  if (e instanceof CancelError) return ""
@@ -126,23 +290,86 @@ export default class CLIInputAdapter extends BaseInputAdapter {
126
290
  * Prompt for a single string input.
127
291
  *
128
292
  * @param {Object} config - Prompt configuration.
129
- * @param {string} [config.prompt] - Prompt text.
130
- * @param {string} [config.label] - Optional label.
131
- * @param {string} [config.name] - Optional identifier.
132
293
  * @returns {Promise<string>} User response string.
133
294
  */
134
295
  async requestInput(config) {
296
+ const predefined = this.#nextAnswer()
297
+ if (predefined !== null) return predefined
298
+
299
+ if (config.yes === true && config.value !== undefined) {
300
+ return config.value
301
+ }
135
302
  const prompt = config.prompt ?? `${config.label ?? config.name}: `
136
- const input = await this.ask(prompt)
137
- return input
303
+ const answer = await this.ask(prompt)
304
+ return this.#normalise(answer)
138
305
  }
139
306
 
140
- /** @inheritDoc */
141
- async ask(question) {
142
- return ask(question)
307
+ /**
308
+ * Asks user a question or form and returns the completed form
309
+ * @param {string | UiForm} question
310
+ * @param {object} [options={}]
311
+ *
312
+ */
313
+ async ask(question, options = {}) {
314
+ if (question instanceof UiForm) {
315
+ return await this.requestForm(question, options)
316
+ }
317
+ const predefined = this.#nextAnswer()
318
+ if (predefined !== null) {
319
+ this.stdout.write(`${question}${predefined}\n`)
320
+ return predefined
321
+ }
322
+ const input = await baseAsk(question)
323
+ return input.value
143
324
  }
325
+
144
326
  /** @inheritDoc */
145
327
  async select(cfg) {
146
- return select(cfg)
328
+ return baseSelect(cfg)
329
+ }
330
+
331
+ /**
332
+ * **New API** – Require input for a {@link UiMessage} instance.
333
+ *
334
+ * This method mirrors the previous `UiMessage.requireInput` logic, but is now
335
+ * owned by the UI adapter. It validates the message according to its static
336
+ * {@link UiMessage.Body} schema, presents a generated {@link UiForm} and
337
+ * returns the updated body. Cancellation results in a {@link CancelError}.
338
+ *
339
+ * @param {UiMessage} msg - Message instance needing input.
340
+ * @returns {Promise<any>} Updated message body.
341
+ * @throws {CancelError} When user cancels the input process.
342
+ */
343
+ async requireInput(msg) {
344
+ if (!msg) {
345
+ throw new Error("Message instance is required")
346
+ }
347
+ if (!(msg instanceof UiMessage)) {
348
+ throw new TypeError("Message must be an instance UiMessage")
349
+ }
350
+ /** @type {Map<string,string>} */
351
+ let errors = msg.validate()
352
+ while (errors.size > 0) {
353
+ const form = generateForm(
354
+ /** @type {any} */(msg.constructor).Body,
355
+ { initialState: msg.body }
356
+ )
357
+
358
+ const formResult = await this.processForm(form, msg.body)
359
+ if (formResult.cancelled) {
360
+ throw new CancelError("User cancelled form")
361
+ }
362
+
363
+ const updatedBody = { ...msg.body, ...formResult.form.state }
364
+ const updatedErrors = msg.validate(updatedBody)
365
+
366
+ if (updatedErrors.size > 0) {
367
+ errors = updatedErrors
368
+ continue
369
+ }
370
+ msg.body = updatedBody
371
+ break
372
+ }
373
+ return /** @type {any} */ (msg.body)
147
374
  }
148
375
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * OutputAdapter handles UI output operations in command-line environment.
3
+ * @class
4
+ */
5
+ export default class OutputAdapter {
6
+ /** @type {any} */
7
+ #console
8
+ /** @type {Map<string, () => Promise<Function>>} */
9
+ #components = new Map()
10
+
11
+ /**
12
+ * Creates new output adapter.
13
+ * @param {Object} [options] - Configuration options.
14
+ * @param {any} [options.console] - Console implementation.
15
+ * @param {Map<string, () => Promise<Function>>} [options.components] - Component loaders.
16
+ */
17
+ constructor(options = {}) {
18
+ const {
19
+ console: initialConsole = console,
20
+ components = new Map(),
21
+ } = options
22
+
23
+ this.#console = initialConsole
24
+ this.#components = components
25
+ }
26
+
27
+ /** @returns {any} */
28
+ get console() { return this.#console; }
29
+
30
+ /**
31
+ * Render a UI component in the CLI environment.
32
+ *
33
+ * The current implementation supports simple textual rendering.
34
+ *
35
+ * @param {string} component - Component name (e.g. `"Alert"`).
36
+ * @param {object} props - Props object passed to the component.
37
+ * @returns {Promise<void>}
38
+ */
39
+ async render(component, props) {
40
+ const loader = this.#components.get(component)
41
+ if (loader) {
42
+ try {
43
+ const compFn = await loader()
44
+ if (typeof compFn === "function") {
45
+ compFn.call(this, props)
46
+ return
47
+ }
48
+ } catch (/** @type {any} */ err) {
49
+ this.#console.error(`Failed to render component "${component}": ${err.message}`)
50
+ this.#console.debug?.(err.stack)
51
+ }
52
+ }
53
+ if (props && typeof props === "object") {
54
+ const { variant, content } = props
55
+ const prefix = variant ? `[${variant}]` : ""
56
+ this.#console.info(`${prefix} ${String(content)}`)
57
+ } else {
58
+ this.#console.info(String(component))
59
+ }
60
+ }
61
+ }
package/src/README.md.js CHANGED
@@ -2,8 +2,7 @@
2
2
  import { describe, it, before, beforeEach } from "node:test"
3
3
  import assert from "node:assert/strict"
4
4
  import FS from "@nan0web/db-fs"
5
- import Message from "@nan0web/co"
6
- import { UIForm } from "@nan0web/ui"
5
+ import { UiForm } from "@nan0web/ui"
7
6
  import { NoConsole } from "@nan0web/log"
8
7
  import {
9
8
  DatasetParser,
@@ -11,7 +10,7 @@ import {
11
10
  runSpawn,
12
11
  } from "@nan0web/test"
13
12
  import {
14
- CLIInputAdapter as BaseCLIInputAdapter,
13
+ CLiInputAdapter as BaseCLiInputAdapter,
15
14
  CancelError,
16
15
  createInput,
17
16
  Input,
@@ -39,7 +38,7 @@ async function next() {
39
38
  return Promise.resolve(" ")
40
39
  }
41
40
 
42
- class CLIInputAdapter extends BaseCLIInputAdapter {
41
+ class CLiInputAdapter extends BaseCLiInputAdapter {
43
42
  async ask(question) {
44
43
  return await ask(question)
45
44
  }
@@ -89,7 +88,7 @@ function testRender() {
89
88
  *
90
89
  * Core classes:
91
90
  *
92
- * - `CLIInputAdapter` — handles form, input, and select requests in CLI.
91
+ * - `CLiInputAdapter` — handles form, input, and select requests in CLI.
93
92
  * - `Input` — wraps user input with value and cancellation status.
94
93
  * - `CancelError` — thrown when a user cancels an operation.
95
94
  *
@@ -133,7 +132,7 @@ function testRender() {
133
132
  * @docs
134
133
  * ## Usage
135
134
  *
136
- * ### CLIInputAdapter
135
+ * ### CLiInputAdapter
137
136
  *
138
137
  * The adapter provides methods to handle form, input, and select requests.
139
138
  *
@@ -141,9 +140,9 @@ function testRender() {
141
140
  *
142
141
  * Displays a form and collects user input field-by-field with validation.
143
142
  */
144
- it("How to request form input via CLIInputAdapter?", async () => {
145
- //import { CLIInputAdapter } from '@nan0web/ui-cli'
146
- const adapter = new CLIInputAdapter()
143
+ it("How to request form input via CLiInputAdapter?", async () => {
144
+ //import { CLiInputAdapter } from '@nan0web/ui-cli'
145
+ const adapter = new CLiInputAdapter()
147
146
  const fields = [
148
147
  { name: "name", label: "Full Name", required: true },
149
148
  { name: "email", label: "Email", type: "email", required: true },
@@ -159,7 +158,7 @@ function testRender() {
159
158
  newForm.state = data
160
159
  return newForm
161
160
  }
162
- const form = UIForm.from({
161
+ const form = UiForm.from({
163
162
  title: "User Profile",
164
163
  fields,
165
164
  id: "user-profile-form",
@@ -177,9 +176,9 @@ function testRender() {
177
176
  /**
178
177
  * @docs
179
178
  */
180
- it("How to request select input via CLIInputAdapter?", async () => {
181
- //import { CLIInputAdapter } from '@nan0web/ui-cli'
182
- const adapter = new CLIInputAdapter()
179
+ it("How to request select input via CLiInputAdapter?", async () => {
180
+ //import { CLiInputAdapter } from '@nan0web/ui-cli'
181
+ const adapter = new CLiInputAdapter()
183
182
  const config = {
184
183
  title: "Choose Language:",
185
184
  prompt: "Language (1-2): ",
@@ -313,7 +312,7 @@ function testRender() {
313
312
  * @docs
314
313
  * ## API
315
314
  *
316
- * ### CLIInputAdapter
315
+ * ### CLiInputAdapter
317
316
  *
318
317
  * * **Methods**
319
318
  * * `requestForm(form, options)` — (async) handles form request
@@ -368,7 +367,7 @@ function testRender() {
368
367
  * Extends `Error`, thrown when an input is cancelled.
369
368
  */
370
369
  it("All exported classes and functions should pass basic tests", () => {
371
- assert.ok(CLIInputAdapter)
370
+ assert.ok(CLiInputAdapter)
372
371
  assert.ok(CancelError)
373
372
  assert.ok(createInput)
374
373
  assert.ok(baseAsk)
@@ -404,7 +403,7 @@ function testRender() {
404
403
  assert.ok(String(pkg.scripts?.playground))
405
404
  const response = await runSpawn("git", ["remote", "get-url", "origin"], { timeout: 2_000 })
406
405
  if (response.code === 0) {
407
- assert.ok(response.text.trim().endsWith(":nan0web/ui-cli.git"))
406
+ assert.ok(response.text.trim().endsWith("nan0web/ui-cli.git"))
408
407
  } else {
409
408
  // git command may fail if not in a repo or no remote, skip assertion
410
409
  console.warn("Git command skipped due to non-zero exit code or timeout.")
@@ -0,0 +1,22 @@
1
+ /** @typedef {import("../InputAdapter.js").default} InputAdapter */
2
+
3
+ /**
4
+ * Alert component for CLI rendering.
5
+ *
6
+ * @this {InputAdapter}
7
+ * @param {Object} input - Component props.
8
+ * @param {string} [input.variant="info"] - Alert variant (maps to console method).
9
+ * @param {string} [input.content=""] - Alert message content.
10
+ * @throws {Error} If variant maps to undefined console method.
11
+ */
12
+ export default function (input = {}) {
13
+ const {
14
+ variant = "info",
15
+ content = "",
16
+ } = input
17
+ const fn = variant === "success" ? "info" : variant
18
+ if (typeof this.console[fn] !== "function") {
19
+ throw new Error(`Undefined variant: ${variant}`)
20
+ }
21
+ this.console[fn](content)
22
+ }
package/src/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import CLIInputAdapter from "./InputAdapter.js"
2
1
  import { CancelError } from "@nan0web/ui/core"
2
+
3
+ import CLiInputAdapter from "./InputAdapter.js"
4
+ import OutputAdapter from "./OutputAdapter.js"
3
5
  import CLI from "./CLI.js"
4
6
  import Command from "./Command.js"
5
7
  import CommandError from "./CommandError.js"
@@ -21,8 +23,9 @@ export {
21
23
 
22
24
  export {
23
25
  CLI,
24
- CLIInputAdapter,
26
+ CLiInputAdapter,
25
27
  CancelError,
28
+ OutputAdapter,
26
29
 
27
30
  /** @deprecated */
28
31
  Command,
@@ -35,6 +38,9 @@ export {
35
38
  CommandHelp,
36
39
  }
37
40
 
41
+ /* New public API */
42
+ export { generateForm } from "./ui/form.js"
43
+
38
44
  export const renderers = new Map([
39
45
  [
40
46
  "UIProcess",
@@ -44,4 +50,4 @@ export const renderers = new Map([
44
50
  ],
45
51
  ])
46
52
 
47
- export default CLIInputAdapter
53
+ export default CLiInputAdapter