@optique/clack 1.2.0-dev.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/LICENSE +20 -0
- package/README.md +60 -0
- package/dist/index.cjs +181 -0
- package/dist/index.d.cts +159 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +158 -0
- package/package.json +84 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025–2026 Hong Minhee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
@optique/clack
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
Interactive prompt support for [Optique] via [Clack].
|
|
5
|
+
|
|
6
|
+
This package wraps any Optique parser with a Clack prompt that fires when no
|
|
7
|
+
CLI value is provided.
|
|
8
|
+
|
|
9
|
+
[Optique]: https://optique.dev/
|
|
10
|
+
[Clack]: https://github.com/bombshell-dev/clack
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
Installation
|
|
14
|
+
------------
|
|
15
|
+
|
|
16
|
+
~~~~ bash
|
|
17
|
+
deno add jsr:@optique/clack
|
|
18
|
+
npm add @optique/clack
|
|
19
|
+
pnpm add @optique/clack
|
|
20
|
+
yarn add @optique/clack
|
|
21
|
+
bun add @optique/clack
|
|
22
|
+
~~~~
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Documentation
|
|
26
|
+
-------------
|
|
27
|
+
|
|
28
|
+
For full documentation, visit <https://optique.dev/integrations/clack>.
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
Quick start
|
|
32
|
+
-----------
|
|
33
|
+
|
|
34
|
+
~~~~ typescript
|
|
35
|
+
import { object } from "@optique/core/constructs";
|
|
36
|
+
import { option } from "@optique/core/primitives";
|
|
37
|
+
import { integer, string } from "@optique/core/valueparser";
|
|
38
|
+
import { prompt } from "@optique/clack";
|
|
39
|
+
import { run } from "@optique/run";
|
|
40
|
+
|
|
41
|
+
const parser = object({
|
|
42
|
+
name: prompt(option("--name", string()), {
|
|
43
|
+
type: "text",
|
|
44
|
+
message: "Project name:",
|
|
45
|
+
}),
|
|
46
|
+
port: prompt(option("--port", integer()), {
|
|
47
|
+
type: "number",
|
|
48
|
+
message: "Port:",
|
|
49
|
+
initialValue: 3000,
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
await run(parser);
|
|
54
|
+
~~~~
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
License
|
|
58
|
+
-------
|
|
59
|
+
|
|
60
|
+
MIT License. See [LICENSE](../../LICENSE) for details.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
10
|
+
key = keys[i];
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
12
|
+
get: ((k) => from[k]).bind(null, key),
|
|
13
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
19
|
+
value: mod,
|
|
20
|
+
enumerable: true
|
|
21
|
+
}) : target, mod));
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
const __clack_prompts = __toESM(require("@clack/prompts"));
|
|
25
|
+
const __optique_core_message = __toESM(require("@optique/core/message"));
|
|
26
|
+
const __optique_prompt = __toESM(require("@optique/prompt"));
|
|
27
|
+
|
|
28
|
+
//#region src/index.ts
|
|
29
|
+
const promptFunctionsOverrideSymbol = Symbol.for("@optique/clack/prompt-functions");
|
|
30
|
+
const defaultPromptFunctions = {
|
|
31
|
+
text: __clack_prompts.text,
|
|
32
|
+
password: __clack_prompts.password,
|
|
33
|
+
confirm: __clack_prompts.confirm,
|
|
34
|
+
select: __clack_prompts.select,
|
|
35
|
+
multiselect: __clack_prompts.multiselect,
|
|
36
|
+
isCancel: __clack_prompts.isCancel
|
|
37
|
+
};
|
|
38
|
+
function promptFunctionKeys() {
|
|
39
|
+
return Object.keys(defaultPromptFunctions);
|
|
40
|
+
}
|
|
41
|
+
function assignPromptFunctionOverride(override, key, candidate) {
|
|
42
|
+
if (typeof candidate === "function") override[key] = candidate;
|
|
43
|
+
}
|
|
44
|
+
function getPromptFunctionsOverride(value) {
|
|
45
|
+
if (typeof value !== "object" || value == null) return void 0;
|
|
46
|
+
const override = {};
|
|
47
|
+
for (const key of promptFunctionKeys()) assignPromptFunctionOverride(override, key, Reflect.get(value, key));
|
|
48
|
+
return override;
|
|
49
|
+
}
|
|
50
|
+
function getPromptFunctions() {
|
|
51
|
+
const override = getPromptFunctionsOverride(Reflect.get(globalThis, promptFunctionsOverrideSymbol));
|
|
52
|
+
return override != null ? {
|
|
53
|
+
...defaultPromptFunctions,
|
|
54
|
+
...override
|
|
55
|
+
} : defaultPromptFunctions;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Wraps a parser with an interactive Clack prompt fallback.
|
|
59
|
+
*
|
|
60
|
+
* @param parser Inner parser that reads CLI values.
|
|
61
|
+
* @param config Type-safe Clack prompt configuration.
|
|
62
|
+
* @returns A parser with interactive prompt fallback, always in async mode.
|
|
63
|
+
* @throws {Error} If prompt execution fails with an unexpected error or if the
|
|
64
|
+
* inner parser throws while parsing or completing.
|
|
65
|
+
* @since 1.2.0
|
|
66
|
+
*/
|
|
67
|
+
function prompt(parser, config) {
|
|
68
|
+
const promptWithAdapter = (0, __optique_prompt.createPromptAdapter)({
|
|
69
|
+
execute: (cfg) => executePromptRaw(cfg),
|
|
70
|
+
getDefaultValue: getConfigDefault
|
|
71
|
+
});
|
|
72
|
+
return promptWithAdapter(parser, config);
|
|
73
|
+
}
|
|
74
|
+
function getConfigDefault(config) {
|
|
75
|
+
if (config != null && typeof config === "object" && "initialValue" in config) return config.initialValue;
|
|
76
|
+
return void 0;
|
|
77
|
+
}
|
|
78
|
+
async function executePromptRaw(config) {
|
|
79
|
+
const cfg = config;
|
|
80
|
+
const type = config.type;
|
|
81
|
+
if (!isPromptType(type)) throw new TypeError(`Unsupported prompt type: ${String(type)}.`);
|
|
82
|
+
const prompts = getPromptFunctions();
|
|
83
|
+
if ("prompter" in cfg && cfg.prompter != null) {
|
|
84
|
+
const value = await cfg.prompter();
|
|
85
|
+
if (cfg.type === "number" && value === void 0) return {
|
|
86
|
+
success: false,
|
|
87
|
+
error: __optique_core_message.message`No number provided.`
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
value
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const result = await executeClackPrompt(cfg, prompts);
|
|
95
|
+
if (prompts.isCancel(result)) return {
|
|
96
|
+
success: false,
|
|
97
|
+
error: __optique_core_message.message`Prompt cancelled.`
|
|
98
|
+
};
|
|
99
|
+
if (cfg.type === "number") return normalizeNumberResult(result);
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
value: result
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function isPromptType(value) {
|
|
106
|
+
return value === "text" || value === "password" || value === "confirm" || value === "number" || value === "select" || value === "multiselect";
|
|
107
|
+
}
|
|
108
|
+
function executeClackPrompt(cfg, prompts) {
|
|
109
|
+
switch (cfg.type) {
|
|
110
|
+
case "text": return prompts.text({
|
|
111
|
+
message: cfg.message,
|
|
112
|
+
...cfg.placeholder !== void 0 ? { placeholder: cfg.placeholder } : {},
|
|
113
|
+
...cfg.initialValue !== void 0 ? { initialValue: cfg.initialValue } : {},
|
|
114
|
+
...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
|
|
115
|
+
});
|
|
116
|
+
case "password": return prompts.password({
|
|
117
|
+
message: cfg.message,
|
|
118
|
+
...cfg.mask !== void 0 ? { mask: cfg.mask } : {},
|
|
119
|
+
...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
|
|
120
|
+
});
|
|
121
|
+
case "confirm": return prompts.confirm({
|
|
122
|
+
message: cfg.message,
|
|
123
|
+
...cfg.initialValue !== void 0 ? { initialValue: cfg.initialValue } : {}
|
|
124
|
+
});
|
|
125
|
+
case "number": return prompts.text({
|
|
126
|
+
message: cfg.message,
|
|
127
|
+
...cfg.placeholder !== void 0 ? { placeholder: cfg.placeholder } : {},
|
|
128
|
+
...cfg.initialValue !== void 0 ? { initialValue: String(cfg.initialValue) } : {},
|
|
129
|
+
validate: async (value) => {
|
|
130
|
+
const parsed = parseNumberPromptValue(value);
|
|
131
|
+
if (parsed == null) return "Enter a number.";
|
|
132
|
+
if (cfg.min !== void 0 && parsed < cfg.min) return `Must be at least ${cfg.min}.`;
|
|
133
|
+
if (cfg.max !== void 0 && parsed > cfg.max) return `Must be at most ${cfg.max}.`;
|
|
134
|
+
return await cfg.validate?.(parsed);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
case "select": return prompts.select({
|
|
138
|
+
message: cfg.message,
|
|
139
|
+
options: normalizeOptions(cfg.options),
|
|
140
|
+
...cfg.initialValue !== void 0 ? { initialValue: cfg.initialValue } : {}
|
|
141
|
+
});
|
|
142
|
+
case "multiselect": return prompts.multiselect({
|
|
143
|
+
message: cfg.message,
|
|
144
|
+
options: normalizeOptions(cfg.options),
|
|
145
|
+
...cfg.required !== void 0 ? { required: cfg.required } : {}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function normalizeNumberResult(result) {
|
|
150
|
+
const parsed = typeof result === "number" ? result : typeof result === "string" ? parseNumberPromptValue(result) : null;
|
|
151
|
+
if (parsed == null) return {
|
|
152
|
+
success: false,
|
|
153
|
+
error: __optique_core_message.message`No number provided.`
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
value: parsed
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function parseNumberPromptValue(value) {
|
|
161
|
+
if (value.trim() === "") return null;
|
|
162
|
+
const parsed = Number(value);
|
|
163
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
164
|
+
}
|
|
165
|
+
function normalizeOptions(options) {
|
|
166
|
+
return options.map((option) => {
|
|
167
|
+
if (typeof option === "string") return {
|
|
168
|
+
value: option,
|
|
169
|
+
label: option
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
value: option.value,
|
|
173
|
+
...option.label !== void 0 ? { label: option.label } : {},
|
|
174
|
+
...option.hint !== void 0 ? { hint: option.hint } : {},
|
|
175
|
+
...option.disabled !== void 0 ? { disabled: option.disabled } : {}
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
//#endregion
|
|
181
|
+
exports.prompt = prompt;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { FluentParser } from "@optique/core/fluent";
|
|
2
|
+
import { Mode, Parser } from "@optique/core/parser";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A choice item for `select` and `multiselect` prompts.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.2.0
|
|
10
|
+
*/
|
|
11
|
+
interface Option {
|
|
12
|
+
/**
|
|
13
|
+
* The value returned when this option is selected.
|
|
14
|
+
*/
|
|
15
|
+
readonly value: string;
|
|
16
|
+
/**
|
|
17
|
+
* Display label shown in the prompt. Defaults to `value`.
|
|
18
|
+
*/
|
|
19
|
+
readonly label?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Additional hint shown next to the option.
|
|
22
|
+
*/
|
|
23
|
+
readonly hint?: string;
|
|
24
|
+
/**
|
|
25
|
+
* If truthy, the option cannot be selected.
|
|
26
|
+
*/
|
|
27
|
+
readonly disabled?: boolean | string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for a `text` prompt.
|
|
31
|
+
*
|
|
32
|
+
* @since 1.2.0
|
|
33
|
+
*/
|
|
34
|
+
interface TextConfig {
|
|
35
|
+
readonly type: "text";
|
|
36
|
+
/** The question to display to the user. */
|
|
37
|
+
readonly message: string;
|
|
38
|
+
/** Placeholder text shown before input. */
|
|
39
|
+
readonly placeholder?: string;
|
|
40
|
+
/** Initial value pre-filled in the prompt. */
|
|
41
|
+
readonly initialValue?: string;
|
|
42
|
+
/** Validation function called when the user submits. */
|
|
43
|
+
readonly validate?: (value: string) => string | void | Promise<string | void>;
|
|
44
|
+
/** Override the prompt execution. Useful for testing. */
|
|
45
|
+
readonly prompter?: () => Promise<string>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for a `password` prompt.
|
|
49
|
+
*
|
|
50
|
+
* @since 1.2.0
|
|
51
|
+
*/
|
|
52
|
+
interface PasswordConfig {
|
|
53
|
+
readonly type: "password";
|
|
54
|
+
/** The question to display to the user. */
|
|
55
|
+
readonly message: string;
|
|
56
|
+
/** Mask character shown while typing. */
|
|
57
|
+
readonly mask?: string;
|
|
58
|
+
/** Validation function called when the user submits. */
|
|
59
|
+
readonly validate?: (value: string) => string | void | Promise<string | void>;
|
|
60
|
+
/** Override the prompt execution. Useful for testing. */
|
|
61
|
+
readonly prompter?: () => Promise<string>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for a `confirm` prompt.
|
|
65
|
+
*
|
|
66
|
+
* @since 1.2.0
|
|
67
|
+
*/
|
|
68
|
+
interface ConfirmConfig {
|
|
69
|
+
readonly type: "confirm";
|
|
70
|
+
/** The question to display to the user. */
|
|
71
|
+
readonly message: string;
|
|
72
|
+
/** Initial Boolean value. */
|
|
73
|
+
readonly initialValue?: boolean;
|
|
74
|
+
/** Override the prompt execution. Useful for testing. */
|
|
75
|
+
readonly prompter?: () => Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Configuration for a `number` prompt.
|
|
79
|
+
*
|
|
80
|
+
* Clack does not provide a dedicated number prompt, so *@optique/clack* uses a
|
|
81
|
+
* text prompt and converts the submitted value to a number.
|
|
82
|
+
*
|
|
83
|
+
* @since 1.2.0
|
|
84
|
+
*/
|
|
85
|
+
interface NumberPromptConfig {
|
|
86
|
+
readonly type: "number";
|
|
87
|
+
/** The question to display to the user. */
|
|
88
|
+
readonly message: string;
|
|
89
|
+
/** Placeholder text shown before input. */
|
|
90
|
+
readonly placeholder?: string;
|
|
91
|
+
/** Initial numeric value. */
|
|
92
|
+
readonly initialValue?: number;
|
|
93
|
+
/** Minimum accepted value. */
|
|
94
|
+
readonly min?: number;
|
|
95
|
+
/** Maximum accepted value. */
|
|
96
|
+
readonly max?: number;
|
|
97
|
+
/** Additional validation after numeric conversion. */
|
|
98
|
+
readonly validate?: (value: number) => string | void | Promise<string | void>;
|
|
99
|
+
/** Override the prompt execution. Useful for testing. */
|
|
100
|
+
readonly prompter?: () => Promise<number | undefined>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Configuration for a `select` prompt.
|
|
104
|
+
*
|
|
105
|
+
* @since 1.2.0
|
|
106
|
+
*/
|
|
107
|
+
interface SelectConfig {
|
|
108
|
+
readonly type: "select";
|
|
109
|
+
/** The question to display to the user. */
|
|
110
|
+
readonly message: string;
|
|
111
|
+
/** Available options. */
|
|
112
|
+
readonly options: readonly (string | Option)[];
|
|
113
|
+
/** Initially selected option value. */
|
|
114
|
+
readonly initialValue?: string;
|
|
115
|
+
/** Override the prompt execution. Useful for testing. */
|
|
116
|
+
readonly prompter?: () => Promise<string>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Configuration for a `multiselect` prompt.
|
|
120
|
+
*
|
|
121
|
+
* @since 1.2.0
|
|
122
|
+
*/
|
|
123
|
+
interface MultiselectConfig {
|
|
124
|
+
readonly type: "multiselect";
|
|
125
|
+
/** The question to display to the user. */
|
|
126
|
+
readonly message: string;
|
|
127
|
+
/** Available options. */
|
|
128
|
+
readonly options: readonly (string | Option)[];
|
|
129
|
+
/** Whether at least one option must be selected. */
|
|
130
|
+
readonly required?: boolean;
|
|
131
|
+
/** Override the prompt execution. Useful for testing. */
|
|
132
|
+
readonly prompter?: () => Promise<readonly string[]>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A union of all string-valued prompt configurations.
|
|
136
|
+
*
|
|
137
|
+
* @since 1.2.0
|
|
138
|
+
*/
|
|
139
|
+
type StringPromptConfig = TextConfig | PasswordConfig | SelectConfig;
|
|
140
|
+
/**
|
|
141
|
+
* Type-safe Clack prompt configuration for a given parser value type `T`.
|
|
142
|
+
*
|
|
143
|
+
* @since 1.2.0
|
|
144
|
+
*/
|
|
145
|
+
type PromptConfig<T> = BasePromptConfig<Exclude<T, null | undefined>>;
|
|
146
|
+
type BasePromptConfig<T> = T extends boolean ? ConfirmConfig : T extends number ? NumberPromptConfig : T extends string ? StringPromptConfig : T extends readonly string[] ? MultiselectConfig : never;
|
|
147
|
+
/**
|
|
148
|
+
* Wraps a parser with an interactive Clack prompt fallback.
|
|
149
|
+
*
|
|
150
|
+
* @param parser Inner parser that reads CLI values.
|
|
151
|
+
* @param config Type-safe Clack prompt configuration.
|
|
152
|
+
* @returns A parser with interactive prompt fallback, always in async mode.
|
|
153
|
+
* @throws {Error} If prompt execution fails with an unexpected error or if the
|
|
154
|
+
* inner parser throws while parsing or completing.
|
|
155
|
+
* @since 1.2.0
|
|
156
|
+
*/
|
|
157
|
+
declare function prompt<M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfig<TValue>): FluentParser<"async", TValue, TState>;
|
|
158
|
+
//#endregion
|
|
159
|
+
export { ConfirmConfig, MultiselectConfig, NumberPromptConfig, Option, PasswordConfig, PromptConfig, SelectConfig, StringPromptConfig, TextConfig, prompt };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { FluentParser } from "@optique/core/fluent";
|
|
2
|
+
import { Mode, Parser } from "@optique/core/parser";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A choice item for `select` and `multiselect` prompts.
|
|
8
|
+
*
|
|
9
|
+
* @since 1.2.0
|
|
10
|
+
*/
|
|
11
|
+
interface Option {
|
|
12
|
+
/**
|
|
13
|
+
* The value returned when this option is selected.
|
|
14
|
+
*/
|
|
15
|
+
readonly value: string;
|
|
16
|
+
/**
|
|
17
|
+
* Display label shown in the prompt. Defaults to `value`.
|
|
18
|
+
*/
|
|
19
|
+
readonly label?: string;
|
|
20
|
+
/**
|
|
21
|
+
* Additional hint shown next to the option.
|
|
22
|
+
*/
|
|
23
|
+
readonly hint?: string;
|
|
24
|
+
/**
|
|
25
|
+
* If truthy, the option cannot be selected.
|
|
26
|
+
*/
|
|
27
|
+
readonly disabled?: boolean | string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Configuration for a `text` prompt.
|
|
31
|
+
*
|
|
32
|
+
* @since 1.2.0
|
|
33
|
+
*/
|
|
34
|
+
interface TextConfig {
|
|
35
|
+
readonly type: "text";
|
|
36
|
+
/** The question to display to the user. */
|
|
37
|
+
readonly message: string;
|
|
38
|
+
/** Placeholder text shown before input. */
|
|
39
|
+
readonly placeholder?: string;
|
|
40
|
+
/** Initial value pre-filled in the prompt. */
|
|
41
|
+
readonly initialValue?: string;
|
|
42
|
+
/** Validation function called when the user submits. */
|
|
43
|
+
readonly validate?: (value: string) => string | void | Promise<string | void>;
|
|
44
|
+
/** Override the prompt execution. Useful for testing. */
|
|
45
|
+
readonly prompter?: () => Promise<string>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Configuration for a `password` prompt.
|
|
49
|
+
*
|
|
50
|
+
* @since 1.2.0
|
|
51
|
+
*/
|
|
52
|
+
interface PasswordConfig {
|
|
53
|
+
readonly type: "password";
|
|
54
|
+
/** The question to display to the user. */
|
|
55
|
+
readonly message: string;
|
|
56
|
+
/** Mask character shown while typing. */
|
|
57
|
+
readonly mask?: string;
|
|
58
|
+
/** Validation function called when the user submits. */
|
|
59
|
+
readonly validate?: (value: string) => string | void | Promise<string | void>;
|
|
60
|
+
/** Override the prompt execution. Useful for testing. */
|
|
61
|
+
readonly prompter?: () => Promise<string>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Configuration for a `confirm` prompt.
|
|
65
|
+
*
|
|
66
|
+
* @since 1.2.0
|
|
67
|
+
*/
|
|
68
|
+
interface ConfirmConfig {
|
|
69
|
+
readonly type: "confirm";
|
|
70
|
+
/** The question to display to the user. */
|
|
71
|
+
readonly message: string;
|
|
72
|
+
/** Initial Boolean value. */
|
|
73
|
+
readonly initialValue?: boolean;
|
|
74
|
+
/** Override the prompt execution. Useful for testing. */
|
|
75
|
+
readonly prompter?: () => Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Configuration for a `number` prompt.
|
|
79
|
+
*
|
|
80
|
+
* Clack does not provide a dedicated number prompt, so *@optique/clack* uses a
|
|
81
|
+
* text prompt and converts the submitted value to a number.
|
|
82
|
+
*
|
|
83
|
+
* @since 1.2.0
|
|
84
|
+
*/
|
|
85
|
+
interface NumberPromptConfig {
|
|
86
|
+
readonly type: "number";
|
|
87
|
+
/** The question to display to the user. */
|
|
88
|
+
readonly message: string;
|
|
89
|
+
/** Placeholder text shown before input. */
|
|
90
|
+
readonly placeholder?: string;
|
|
91
|
+
/** Initial numeric value. */
|
|
92
|
+
readonly initialValue?: number;
|
|
93
|
+
/** Minimum accepted value. */
|
|
94
|
+
readonly min?: number;
|
|
95
|
+
/** Maximum accepted value. */
|
|
96
|
+
readonly max?: number;
|
|
97
|
+
/** Additional validation after numeric conversion. */
|
|
98
|
+
readonly validate?: (value: number) => string | void | Promise<string | void>;
|
|
99
|
+
/** Override the prompt execution. Useful for testing. */
|
|
100
|
+
readonly prompter?: () => Promise<number | undefined>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Configuration for a `select` prompt.
|
|
104
|
+
*
|
|
105
|
+
* @since 1.2.0
|
|
106
|
+
*/
|
|
107
|
+
interface SelectConfig {
|
|
108
|
+
readonly type: "select";
|
|
109
|
+
/** The question to display to the user. */
|
|
110
|
+
readonly message: string;
|
|
111
|
+
/** Available options. */
|
|
112
|
+
readonly options: readonly (string | Option)[];
|
|
113
|
+
/** Initially selected option value. */
|
|
114
|
+
readonly initialValue?: string;
|
|
115
|
+
/** Override the prompt execution. Useful for testing. */
|
|
116
|
+
readonly prompter?: () => Promise<string>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Configuration for a `multiselect` prompt.
|
|
120
|
+
*
|
|
121
|
+
* @since 1.2.0
|
|
122
|
+
*/
|
|
123
|
+
interface MultiselectConfig {
|
|
124
|
+
readonly type: "multiselect";
|
|
125
|
+
/** The question to display to the user. */
|
|
126
|
+
readonly message: string;
|
|
127
|
+
/** Available options. */
|
|
128
|
+
readonly options: readonly (string | Option)[];
|
|
129
|
+
/** Whether at least one option must be selected. */
|
|
130
|
+
readonly required?: boolean;
|
|
131
|
+
/** Override the prompt execution. Useful for testing. */
|
|
132
|
+
readonly prompter?: () => Promise<readonly string[]>;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A union of all string-valued prompt configurations.
|
|
136
|
+
*
|
|
137
|
+
* @since 1.2.0
|
|
138
|
+
*/
|
|
139
|
+
type StringPromptConfig = TextConfig | PasswordConfig | SelectConfig;
|
|
140
|
+
/**
|
|
141
|
+
* Type-safe Clack prompt configuration for a given parser value type `T`.
|
|
142
|
+
*
|
|
143
|
+
* @since 1.2.0
|
|
144
|
+
*/
|
|
145
|
+
type PromptConfig<T> = BasePromptConfig<Exclude<T, null | undefined>>;
|
|
146
|
+
type BasePromptConfig<T> = T extends boolean ? ConfirmConfig : T extends number ? NumberPromptConfig : T extends string ? StringPromptConfig : T extends readonly string[] ? MultiselectConfig : never;
|
|
147
|
+
/**
|
|
148
|
+
* Wraps a parser with an interactive Clack prompt fallback.
|
|
149
|
+
*
|
|
150
|
+
* @param parser Inner parser that reads CLI values.
|
|
151
|
+
* @param config Type-safe Clack prompt configuration.
|
|
152
|
+
* @returns A parser with interactive prompt fallback, always in async mode.
|
|
153
|
+
* @throws {Error} If prompt execution fails with an unexpected error or if the
|
|
154
|
+
* inner parser throws while parsing or completing.
|
|
155
|
+
* @since 1.2.0
|
|
156
|
+
*/
|
|
157
|
+
declare function prompt<M extends Mode, TValue, TState>(parser: Parser<M, TValue, TState>, config: PromptConfig<TValue>): FluentParser<"async", TValue, TState>;
|
|
158
|
+
//#endregion
|
|
159
|
+
export { ConfirmConfig, MultiselectConfig, NumberPromptConfig, Option, PasswordConfig, PromptConfig, SelectConfig, StringPromptConfig, TextConfig, prompt };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { confirm, isCancel, multiselect, password, select, text } from "@clack/prompts";
|
|
2
|
+
import { message } from "@optique/core/message";
|
|
3
|
+
import { createPromptAdapter } from "@optique/prompt";
|
|
4
|
+
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
const promptFunctionsOverrideSymbol = Symbol.for("@optique/clack/prompt-functions");
|
|
7
|
+
const defaultPromptFunctions = {
|
|
8
|
+
text,
|
|
9
|
+
password,
|
|
10
|
+
confirm,
|
|
11
|
+
select,
|
|
12
|
+
multiselect,
|
|
13
|
+
isCancel
|
|
14
|
+
};
|
|
15
|
+
function promptFunctionKeys() {
|
|
16
|
+
return Object.keys(defaultPromptFunctions);
|
|
17
|
+
}
|
|
18
|
+
function assignPromptFunctionOverride(override, key, candidate) {
|
|
19
|
+
if (typeof candidate === "function") override[key] = candidate;
|
|
20
|
+
}
|
|
21
|
+
function getPromptFunctionsOverride(value) {
|
|
22
|
+
if (typeof value !== "object" || value == null) return void 0;
|
|
23
|
+
const override = {};
|
|
24
|
+
for (const key of promptFunctionKeys()) assignPromptFunctionOverride(override, key, Reflect.get(value, key));
|
|
25
|
+
return override;
|
|
26
|
+
}
|
|
27
|
+
function getPromptFunctions() {
|
|
28
|
+
const override = getPromptFunctionsOverride(Reflect.get(globalThis, promptFunctionsOverrideSymbol));
|
|
29
|
+
return override != null ? {
|
|
30
|
+
...defaultPromptFunctions,
|
|
31
|
+
...override
|
|
32
|
+
} : defaultPromptFunctions;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Wraps a parser with an interactive Clack prompt fallback.
|
|
36
|
+
*
|
|
37
|
+
* @param parser Inner parser that reads CLI values.
|
|
38
|
+
* @param config Type-safe Clack prompt configuration.
|
|
39
|
+
* @returns A parser with interactive prompt fallback, always in async mode.
|
|
40
|
+
* @throws {Error} If prompt execution fails with an unexpected error or if the
|
|
41
|
+
* inner parser throws while parsing or completing.
|
|
42
|
+
* @since 1.2.0
|
|
43
|
+
*/
|
|
44
|
+
function prompt(parser, config) {
|
|
45
|
+
const promptWithAdapter = createPromptAdapter({
|
|
46
|
+
execute: (cfg) => executePromptRaw(cfg),
|
|
47
|
+
getDefaultValue: getConfigDefault
|
|
48
|
+
});
|
|
49
|
+
return promptWithAdapter(parser, config);
|
|
50
|
+
}
|
|
51
|
+
function getConfigDefault(config) {
|
|
52
|
+
if (config != null && typeof config === "object" && "initialValue" in config) return config.initialValue;
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
55
|
+
async function executePromptRaw(config) {
|
|
56
|
+
const cfg = config;
|
|
57
|
+
const type = config.type;
|
|
58
|
+
if (!isPromptType(type)) throw new TypeError(`Unsupported prompt type: ${String(type)}.`);
|
|
59
|
+
const prompts = getPromptFunctions();
|
|
60
|
+
if ("prompter" in cfg && cfg.prompter != null) {
|
|
61
|
+
const value = await cfg.prompter();
|
|
62
|
+
if (cfg.type === "number" && value === void 0) return {
|
|
63
|
+
success: false,
|
|
64
|
+
error: message`No number provided.`
|
|
65
|
+
};
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
value
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const result = await executeClackPrompt(cfg, prompts);
|
|
72
|
+
if (prompts.isCancel(result)) return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: message`Prompt cancelled.`
|
|
75
|
+
};
|
|
76
|
+
if (cfg.type === "number") return normalizeNumberResult(result);
|
|
77
|
+
return {
|
|
78
|
+
success: true,
|
|
79
|
+
value: result
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isPromptType(value) {
|
|
83
|
+
return value === "text" || value === "password" || value === "confirm" || value === "number" || value === "select" || value === "multiselect";
|
|
84
|
+
}
|
|
85
|
+
function executeClackPrompt(cfg, prompts) {
|
|
86
|
+
switch (cfg.type) {
|
|
87
|
+
case "text": return prompts.text({
|
|
88
|
+
message: cfg.message,
|
|
89
|
+
...cfg.placeholder !== void 0 ? { placeholder: cfg.placeholder } : {},
|
|
90
|
+
...cfg.initialValue !== void 0 ? { initialValue: cfg.initialValue } : {},
|
|
91
|
+
...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
|
|
92
|
+
});
|
|
93
|
+
case "password": return prompts.password({
|
|
94
|
+
message: cfg.message,
|
|
95
|
+
...cfg.mask !== void 0 ? { mask: cfg.mask } : {},
|
|
96
|
+
...cfg.validate !== void 0 ? { validate: cfg.validate } : {}
|
|
97
|
+
});
|
|
98
|
+
case "confirm": return prompts.confirm({
|
|
99
|
+
message: cfg.message,
|
|
100
|
+
...cfg.initialValue !== void 0 ? { initialValue: cfg.initialValue } : {}
|
|
101
|
+
});
|
|
102
|
+
case "number": return prompts.text({
|
|
103
|
+
message: cfg.message,
|
|
104
|
+
...cfg.placeholder !== void 0 ? { placeholder: cfg.placeholder } : {},
|
|
105
|
+
...cfg.initialValue !== void 0 ? { initialValue: String(cfg.initialValue) } : {},
|
|
106
|
+
validate: async (value) => {
|
|
107
|
+
const parsed = parseNumberPromptValue(value);
|
|
108
|
+
if (parsed == null) return "Enter a number.";
|
|
109
|
+
if (cfg.min !== void 0 && parsed < cfg.min) return `Must be at least ${cfg.min}.`;
|
|
110
|
+
if (cfg.max !== void 0 && parsed > cfg.max) return `Must be at most ${cfg.max}.`;
|
|
111
|
+
return await cfg.validate?.(parsed);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
case "select": return prompts.select({
|
|
115
|
+
message: cfg.message,
|
|
116
|
+
options: normalizeOptions(cfg.options),
|
|
117
|
+
...cfg.initialValue !== void 0 ? { initialValue: cfg.initialValue } : {}
|
|
118
|
+
});
|
|
119
|
+
case "multiselect": return prompts.multiselect({
|
|
120
|
+
message: cfg.message,
|
|
121
|
+
options: normalizeOptions(cfg.options),
|
|
122
|
+
...cfg.required !== void 0 ? { required: cfg.required } : {}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function normalizeNumberResult(result) {
|
|
127
|
+
const parsed = typeof result === "number" ? result : typeof result === "string" ? parseNumberPromptValue(result) : null;
|
|
128
|
+
if (parsed == null) return {
|
|
129
|
+
success: false,
|
|
130
|
+
error: message`No number provided.`
|
|
131
|
+
};
|
|
132
|
+
return {
|
|
133
|
+
success: true,
|
|
134
|
+
value: parsed
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function parseNumberPromptValue(value) {
|
|
138
|
+
if (value.trim() === "") return null;
|
|
139
|
+
const parsed = Number(value);
|
|
140
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
141
|
+
}
|
|
142
|
+
function normalizeOptions(options) {
|
|
143
|
+
return options.map((option) => {
|
|
144
|
+
if (typeof option === "string") return {
|
|
145
|
+
value: option,
|
|
146
|
+
label: option
|
|
147
|
+
};
|
|
148
|
+
return {
|
|
149
|
+
value: option.value,
|
|
150
|
+
...option.label !== void 0 ? { label: option.label } : {},
|
|
151
|
+
...option.hint !== void 0 ? { hint: option.hint } : {},
|
|
152
|
+
...option.disabled !== void 0 ? { disabled: option.disabled } : {}
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#endregion
|
|
158
|
+
export { prompt };
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@optique/clack",
|
|
3
|
+
"version": "1.2.0-dev.0",
|
|
4
|
+
"description": "Interactive prompt support for Optique via Clack",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"CLI",
|
|
7
|
+
"command-line",
|
|
8
|
+
"commandline",
|
|
9
|
+
"parser",
|
|
10
|
+
"prompt",
|
|
11
|
+
"interactive",
|
|
12
|
+
"clack"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "Hong Minhee",
|
|
17
|
+
"email": "hong@minhee.org",
|
|
18
|
+
"url": "https://hongminhee.org/"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://optique.dev/",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/dahlia/optique.git",
|
|
24
|
+
"directory": "packages/clack/"
|
|
25
|
+
},
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/dahlia/optique/issues"
|
|
28
|
+
},
|
|
29
|
+
"funding": [
|
|
30
|
+
"https://github.com/sponsors/dahlia"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.0.0",
|
|
34
|
+
"bun": ">=1.2.0",
|
|
35
|
+
"deno": ">=2.3.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist/",
|
|
39
|
+
"package.json",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"type": "module",
|
|
43
|
+
"module": "./dist/index.js",
|
|
44
|
+
"main": "./dist/index.cjs",
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"exports": {
|
|
47
|
+
".": {
|
|
48
|
+
"types": {
|
|
49
|
+
"import": "./dist/index.d.ts",
|
|
50
|
+
"require": "./dist/index.d.cts"
|
|
51
|
+
},
|
|
52
|
+
"import": "./dist/index.js",
|
|
53
|
+
"require": "./dist/index.cjs"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"imports": {
|
|
57
|
+
"#src/*.ts": {
|
|
58
|
+
"node": "./dist/*.js",
|
|
59
|
+
"default": "./src/*.ts"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"sideEffects": false,
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"@clack/prompts": "^1.6.0",
|
|
65
|
+
"@optique/core": "1.2.0",
|
|
66
|
+
"@optique/prompt": "1.2.0"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/node": "^24.0.0",
|
|
70
|
+
"fast-check": "^4.7.0",
|
|
71
|
+
"tsdown": "^0.13.0",
|
|
72
|
+
"typescript": "^5.8.3",
|
|
73
|
+
"@optique/run": "1.2.0",
|
|
74
|
+
"@optique/env": "1.2.0"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsdown",
|
|
78
|
+
"prepublish": "tsdown",
|
|
79
|
+
"test": "node --test",
|
|
80
|
+
"test:bun": "bun test",
|
|
81
|
+
"test:deno": "deno test --allow-env",
|
|
82
|
+
"test-all": "tsdown && node --test && bun test && deno test --allow-env"
|
|
83
|
+
}
|
|
84
|
+
}
|