cli-questionnaire 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.
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2025 Edmar Langendoen
2
+
3
+ Permission to use, copy, modify, and distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # CLI Questionnaire 🎯
2
+
3
+ A CLI tool built with TypeScript for creating interactive questionnaires.
4
+
5
+ ## Table of Contents 📚
6
+
7
+ - [Installation](#installation)
8
+ - [Usage](#usage)
9
+ - [Build](#build)
10
+ - [Testing](#testing)
11
+ - [License](#license)
12
+
13
+ ## Installation ⚙️
14
+
15
+ ### From npm (once published)
16
+
17
+ You can install the package globally using npm:
18
+
19
+ ```sh
20
+ npm install -g cli-questionnaire
21
+ ```
22
+
23
+ Or use it directly with `npx`:
24
+
25
+ ```sh
26
+ npx cli-questionnaire
27
+ ```
28
+
29
+ ### From Source 🛠️
30
+
31
+ 1. Clone the repository:
32
+ ```sh
33
+ git clone https://github.com/elangendoen/cli-questionnaire.git
34
+ cd cli-questionnaire
35
+ ```
36
+ 2. Install dependencies:
37
+
38
+ ```sh
39
+ npm install
40
+ ```
41
+
42
+ 3. Build the project:
43
+ ```sh
44
+ npm run build
45
+ ```
46
+
47
+ ## Usage 🚀
48
+
49
+ ### Using the CLI
50
+
51
+ Once installed globally or via `npx`, you can run the CLI tool:
52
+
53
+ ```sh
54
+ cli-questionnaire
55
+ ```
56
+
57
+ ### Programmatic Usage 🖥️
58
+
59
+ You can also use the `Prompt` function programmatically in your TypeScript or JavaScript projects:
60
+
61
+ ```typescript
62
+ import { Prompt, Question } from 'cli-questionnaire';
63
+
64
+ const questions: Question[] = [
65
+ {
66
+ id: 'q1',
67
+ type: 'multiple-choice',
68
+ question: 'What is your favorite programming language?',
69
+ options: ['JavaScript', 'TypeScript', 'Python'],
70
+ },
71
+ {
72
+ id: 'q2',
73
+ type: 'open',
74
+ question: 'What is your name?',
75
+ allowBackNavigation: true,
76
+ },
77
+ {
78
+ id: 'q3',
79
+ type: 'number',
80
+ question: 'How many years of experience do you have in programming?',
81
+ condition: (answers) => {
82
+ const q1Answer = answers.find((a) => a.id === 'q1')?.answer;
83
+ return q1Answer === 'TypeScript';
84
+ },
85
+ },
86
+ ];
87
+
88
+ (async () => {
89
+ const answers = await Prompt({
90
+ questions,
91
+ allowBackNavigation: true,
92
+ allowSkip: true,
93
+ });
94
+
95
+ console.log('Your answers:', answers);
96
+ })();
97
+ ```
98
+
99
+ #### Question Properties 📝
100
+
101
+ Each question in the `questions` array can have the following properties:
102
+
103
+ - **`id`** (required): A unique identifier for the question.
104
+ - **`type`** (required): The type of question. Can be one of:
105
+ - `'multiple-choice'`: A question with predefined options.
106
+ - `'open'`: A free-text question.
107
+ - `'number'`: A question expecting a numeric answer.
108
+ - **`question`** (required): The text of the question to display to the user.
109
+ - **`options`** (required for `'multiple-choice'`): An array of strings representing the available options.
110
+ - **`allowBackNavigation`** (optional): A boolean indicating whether the user can navigate back to this question. Defaults to `false`.
111
+ - **`allowSkip`** (optional): A boolean indicating whether the user can skip this question. Defaults to `false`.
112
+ - **`condition`** (optional): A function that determines whether this question should be asked. The function receives the current `answers` array and should return `true` or `false`.
113
+
114
+ ---
115
+
116
+ ### Question Types with Examples 🛠️
117
+
118
+ Here are examples of the different question types supported by the `Prompt` function:
119
+
120
+ #### Multiple-Choice Question
121
+
122
+ A question with predefined options that the user can select from:
123
+
124
+ ```typescript
125
+ {
126
+ id: 'q1',
127
+ type: 'multiple-choice',
128
+ question: 'What is your favorite programming language?',
129
+ options: ['JavaScript', 'TypeScript', 'Python', 'Java'],
130
+ }
131
+ ```
132
+
133
+ #### Open Question
134
+
135
+ A free-text question where the user can type their answer:
136
+
137
+ ```typescript
138
+ {
139
+ id: 'q1',
140
+ type: 'open',
141
+ question: 'What is your name?',
142
+ }
143
+ ```
144
+
145
+ #### Number Question
146
+
147
+ A question expecting a numeric answer:
148
+
149
+ ```typescript
150
+ {
151
+ id: 'q1',
152
+ type: 'number',
153
+ question: 'How many years of experience do you have in programming?',
154
+ }
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Build 🏗️
160
+
161
+ To compile the TypeScript code to JavaScript, run:
162
+
163
+ ```sh
164
+ npm run build
165
+ ```
166
+
167
+ The compiled files will be output to the `dist` directory.
168
+
169
+ ## Testing 🧪
170
+
171
+ Run the tests using Jest:
172
+
173
+ ```sh
174
+ npm test
175
+ ```
176
+
177
+ To generate a coverage report:
178
+
179
+ ```sh
180
+ npm run coverage
181
+ ```
182
+
183
+ ## License 📜
184
+
185
+ This project is licensed under the ISC License.
@@ -0,0 +1,2 @@
1
+ export { Prompt } from './prompt';
2
+ export type { Question, Answer } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Prompt = void 0;
4
+ var prompt_1 = require("./prompt");
5
+ Object.defineProperty(exports, "Prompt", { enumerable: true, get: function () { return prompt_1.Prompt; } });
6
+ // import { Prompt } from './prompt';
7
+ // import { Question } from './types';
8
+ // (async () => {
9
+ // const questions: Question[] = [
10
+ // {
11
+ // id: 'q1',
12
+ // type: 'multiple-choice',
13
+ // question: 'What is your favorite programming language?',
14
+ // options: ['JavaScript', 'TypeScript', 'Python', 'Java'],
15
+ // },
16
+ // {
17
+ // id: 'q2',
18
+ // type: 'open',
19
+ // question: 'What is your name?',
20
+ // allowBackNavigation: true,
21
+ // },
22
+ // {
23
+ // id: 'q3',
24
+ // type: 'number',
25
+ // question: 'How many years of experience do you have in programming?',
26
+ // condition: (answers) => {
27
+ // // Only ask this question if the user selected "TypeScript" in q1
28
+ // const q1Answer = answers.find((a) => a.id === 'q1')?.answer;
29
+ // return q1Answer === 'TypeScript';
30
+ // },
31
+ // },
32
+ // {
33
+ // id: 'q4',
34
+ // type: 'open',
35
+ // question: 'What is your last name?',
36
+ // allowBackNavigation: true,
37
+ // },
38
+ // ];
39
+ // const answers = await Prompt({
40
+ // questions,
41
+ // allowBackNavigation: true,
42
+ // allowSkip: true,
43
+ // });
44
+ // console.log('\nYour answers:', answers);
45
+ // })();
@@ -0,0 +1,2 @@
1
+ import { Question, Answer } from '../../types';
2
+ export declare function handleCondition(question: Question, answers: Answer[]): boolean;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleCondition = handleCondition;
4
+ function handleCondition(question, answers) {
5
+ return question.condition ? question.condition(answers) : true;
6
+ }
@@ -0,0 +1,3 @@
1
+ import { Answer, MultipleChoiceQuestion } from '../../types';
2
+ import * as readline from 'readline';
3
+ export declare function handleMultipleChoice(question: MultipleChoiceQuestion, answers: Answer[], rl: readline.Interface, allowBackNavigation: boolean, allowSkip: boolean, currentIndex: number): Promise<string | number | 'back' | 'skip'>;
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.handleMultipleChoice = handleMultipleChoice;
13
+ const helpers_1 = require("../helpers");
14
+ function handleMultipleChoice(question, answers, rl, allowBackNavigation, allowSkip, currentIndex) {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ console.log(`\n${question.question}`);
17
+ question.options.forEach((option, index) => {
18
+ console.log(`${index + 1}. ${option}`);
19
+ });
20
+ // Add "Go Back" and "Skip" options if applicable
21
+ const extraOptions = [];
22
+ if (allowBackNavigation && currentIndex > 0) {
23
+ extraOptions.push('b. Go Back');
24
+ }
25
+ if (allowSkip) {
26
+ extraOptions.push('s. Skip');
27
+ }
28
+ if (extraOptions.length > 0) {
29
+ console.log('\nAdditional Options:');
30
+ extraOptions.forEach((option) => {
31
+ console.log(option);
32
+ });
33
+ }
34
+ const choice = yield (0, helpers_1.askQuestion)(rl, 'Choose an option (number or letter): ');
35
+ const index = parseInt(choice, 10) - 1;
36
+ if (allowBackNavigation && currentIndex > 0 && choice.toLowerCase() === 'b') {
37
+ return 'back';
38
+ }
39
+ if (allowSkip && choice.toLowerCase() === 's') {
40
+ return 'skip';
41
+ }
42
+ if (index >= 0 && index < question.options.length) {
43
+ return question.options[index];
44
+ }
45
+ console.log('Invalid choice. Please try again.');
46
+ return yield handleMultipleChoice(question, answers, rl, allowBackNavigation, allowSkip, currentIndex);
47
+ });
48
+ }
@@ -0,0 +1,3 @@
1
+ import { Question } from '../../types';
2
+ import * as readline from 'readline';
3
+ export declare function handleNumber(question: Question, rl: readline.Interface, allowBackNavigation: boolean, allowSkip: boolean, currentIndex: number): Promise<number | 'back' | 'skip'>;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.handleNumber = handleNumber;
13
+ const helpers_1 = require("../helpers");
14
+ function handleNumber(question, rl, allowBackNavigation, allowSkip, currentIndex) {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ let extraOptions = '';
17
+ if (allowBackNavigation && currentIndex > 0) {
18
+ extraOptions += ' (b: Go Back';
19
+ }
20
+ if (allowSkip) {
21
+ extraOptions += `${extraOptions ? ', ' : ' ('}s: Skip`;
22
+ }
23
+ extraOptions += extraOptions ? ')' : '';
24
+ const response = yield (0, helpers_1.askQuestion)(rl, `${question.question}${extraOptions}: `);
25
+ if (allowBackNavigation &&
26
+ currentIndex > 0 &&
27
+ response.toLowerCase() === 'b') {
28
+ return 'back';
29
+ }
30
+ if (allowSkip && response.toLowerCase() === 's') {
31
+ return 'skip';
32
+ }
33
+ const numberResponse = parseFloat(response);
34
+ if (!isNaN(numberResponse)) {
35
+ return numberResponse;
36
+ }
37
+ console.log('Invalid number. Please try again.');
38
+ return yield handleNumber(question, rl, allowBackNavigation, allowSkip, currentIndex);
39
+ });
40
+ }
@@ -0,0 +1,3 @@
1
+ import { Question } from '../../types';
2
+ import * as readline from 'readline';
3
+ export declare function handleOpen(question: Question, rl: readline.Interface, allowBackNavigation: boolean, allowSkip: boolean, currentIndex: number): Promise<string | 'back' | 'skip'>;
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.handleOpen = handleOpen;
13
+ const helpers_1 = require("../helpers");
14
+ function handleOpen(question, rl, allowBackNavigation, allowSkip, currentIndex) {
15
+ return __awaiter(this, void 0, void 0, function* () {
16
+ let extraOptions = '';
17
+ if (allowBackNavigation && currentIndex > 0) {
18
+ extraOptions += ' (b: Go Back';
19
+ }
20
+ if (allowSkip) {
21
+ extraOptions += `${extraOptions ? ', ' : ' ('}s: Skip`;
22
+ }
23
+ extraOptions += extraOptions ? ')' : '';
24
+ const response = yield (0, helpers_1.askQuestion)(rl, `${question.question}${extraOptions}: `);
25
+ if (allowBackNavigation &&
26
+ currentIndex > 0 &&
27
+ response.toLowerCase() === 'b') {
28
+ return 'back';
29
+ }
30
+ if (allowSkip && response.toLowerCase() === 's') {
31
+ return 'skip';
32
+ }
33
+ return response;
34
+ });
35
+ }
@@ -0,0 +1,2 @@
1
+ import * as readline from 'readline';
2
+ export declare const askQuestion: (rl: readline.Interface, query: string) => Promise<string>;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.askQuestion = void 0;
4
+ const askQuestion = (rl, query) => {
5
+ return new Promise((resolve) => rl.question(query, resolve));
6
+ };
7
+ exports.askQuestion = askQuestion;
@@ -0,0 +1,8 @@
1
+ import { Question, Answer } from '../types';
2
+ interface PromptParams {
3
+ questions: Question[];
4
+ allowBackNavigation?: boolean;
5
+ allowSkip?: boolean;
6
+ }
7
+ export declare function Prompt({ questions, allowBackNavigation, allowSkip, }: PromptParams): Promise<Answer[]>;
8
+ export {};
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Prompt = Prompt;
13
+ const readline = require("readline");
14
+ const multipleChoiceHandler_1 = require("./handlers/multipleChoiceHandler");
15
+ const openHandler_1 = require("./handlers/openHandler");
16
+ const numberHandler_1 = require("./handlers/numberHandler");
17
+ const conditionHandler_1 = require("./handlers/conditionHandler");
18
+ function Prompt(_a) {
19
+ return __awaiter(this, arguments, void 0, function* ({ questions, allowBackNavigation = false, allowSkip = false, }) {
20
+ var _b, _c;
21
+ const answers = [];
22
+ const rl = readline.createInterface({
23
+ input: process.stdin,
24
+ output: process.stdout,
25
+ });
26
+ let currentIndex = 0;
27
+ try {
28
+ while (currentIndex < questions.length) {
29
+ const question = questions[currentIndex];
30
+ // Check the condition function, if it exists
31
+ if (!(0, conditionHandler_1.handleCondition)(question, [...answers])) {
32
+ // Skip the question if the condition returns false
33
+ answers.push({ id: question.id, answer: undefined });
34
+ currentIndex++;
35
+ continue;
36
+ }
37
+ const backNavigationAllowed = (_b = question.allowBackNavigation) !== null && _b !== void 0 ? _b : allowBackNavigation;
38
+ const skipAllowed = (_c = question.allowSkip) !== null && _c !== void 0 ? _c : allowSkip;
39
+ let userAnswer = undefined;
40
+ // Delegate to the appropriate handler based on question type
41
+ switch (question.type) {
42
+ case 'multiple-choice':
43
+ userAnswer = yield (0, multipleChoiceHandler_1.handleMultipleChoice)(question, [...answers], // Pass a copy of the current answers
44
+ rl, backNavigationAllowed, skipAllowed, currentIndex);
45
+ break;
46
+ case 'open':
47
+ userAnswer = yield (0, openHandler_1.handleOpen)(question, rl, backNavigationAllowed, skipAllowed, currentIndex);
48
+ break;
49
+ case 'number':
50
+ userAnswer = yield (0, numberHandler_1.handleNumber)(question, rl, backNavigationAllowed, skipAllowed, currentIndex);
51
+ break;
52
+ default:
53
+ console.log('Unknown question type. Skipping question.');
54
+ userAnswer = undefined;
55
+ }
56
+ // Handle navigation
57
+ if (userAnswer === 'back') {
58
+ if (currentIndex > 0) {
59
+ currentIndex--;
60
+ answers.pop(); // Remove the last answer
61
+ }
62
+ else {
63
+ console.log('You are already at the first question. Cannot go back.');
64
+ }
65
+ continue;
66
+ }
67
+ else if (userAnswer === 'skip') {
68
+ answers.push({ id: question.id, answer: undefined });
69
+ }
70
+ else {
71
+ answers.push({ id: question.id, answer: userAnswer });
72
+ }
73
+ currentIndex++;
74
+ }
75
+ }
76
+ finally {
77
+ // Ensure the readline interface is always closed
78
+ rl.close();
79
+ }
80
+ return answers;
81
+ });
82
+ }
@@ -0,0 +1,24 @@
1
+ export type QuestionType = 'multiple-choice' | 'open' | 'number';
2
+ export interface MultipleChoiceQuestion {
3
+ id: string;
4
+ type: 'multiple-choice';
5
+ question: string;
6
+ options: string[];
7
+ allowBackNavigation?: boolean;
8
+ allowSkip?: boolean;
9
+ condition?: (answers: Answer[]) => boolean;
10
+ }
11
+ export interface OpenOrNumberQuestion {
12
+ id: string;
13
+ type: 'open' | 'number';
14
+ question: string;
15
+ options?: undefined;
16
+ allowBackNavigation?: boolean;
17
+ allowSkip?: boolean;
18
+ condition?: (answers: Answer[]) => boolean;
19
+ }
20
+ export type Question = MultipleChoiceQuestion | OpenOrNumberQuestion;
21
+ export type Answer = {
22
+ id: string;
23
+ answer: string | number | undefined;
24
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "cli-questionnaire",
3
+ "version": "1.0.0",
4
+ "main": "dist/index.js",
5
+ "types": "dist/index.d.ts",
6
+ "bin": {
7
+ "cli-questionnaire": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "prepare": "npm run build",
12
+ "start": "ts-node src/index.ts",
13
+ "test": "jest --forceExit",
14
+ "coverage": "jest --coverage"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/elangendoen/cli-questionnaire.git"
23
+ },
24
+ "keywords": ["cli", "questionnaire", "interactive", "prompts"],
25
+ "author": "Edmar Langendoen",
26
+ "license": "ISC",
27
+ "description": "A CLI questionnaire tool for interactive prompts.",
28
+ "devDependencies": {
29
+ "@eslint/js": "^9.26.0",
30
+ "@types/jest": "^29.5.14",
31
+ "@types/node": "^22.15.3",
32
+ "eslint": "^9.26.0",
33
+ "eslint-config-prettier": "^10.1.2",
34
+ "eslint-plugin-prettier": "^5.3.1",
35
+ "globals": "^16.0.0",
36
+ "jest": "^29.7.0",
37
+ "prettier": "^3.5.3",
38
+ "ts-jest": "^29.3.2",
39
+ "ts-node": "^10.9.2",
40
+ "typescript": "^5.8.3",
41
+ "typescript-eslint": "^8.31.1"
42
+ }
43
+ }