@portl/cli 0.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.
- package/LICENSE +201 -0
- package/README.md +348 -0
- package/bin/portl.js +1837 -0
- package/package.json +41 -0
- package/src/commands/init.js +663 -0
- package/src/utils/prompts.js +210 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced prompt utilities for intuitive CLI interactions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const ANSI = {
|
|
6
|
+
reset: '\x1b[0m',
|
|
7
|
+
bold: '\x1b[1m',
|
|
8
|
+
dim: '\x1b[2m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
green: '\x1b[32m',
|
|
11
|
+
yellow: '\x1b[33m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
blue: '\x1b[34m',
|
|
14
|
+
magenta: '\x1b[35m',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function c(text, color) {
|
|
18
|
+
return `${color}${text}${ANSI.reset}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Display a visual section separator with emoji
|
|
23
|
+
*/
|
|
24
|
+
export function showSection(title, emoji = 'š¦') {
|
|
25
|
+
console.log('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
26
|
+
console.log(`${emoji} ${c(title.toUpperCase(), ANSI.bold)}`);
|
|
27
|
+
console.log('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Display an inline tip/help message
|
|
32
|
+
*/
|
|
33
|
+
export function showTip(message) {
|
|
34
|
+
console.log(c(`š” ${message}`, ANSI.dim));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Display a success message
|
|
39
|
+
*/
|
|
40
|
+
export function showSuccess(message) {
|
|
41
|
+
console.log(c(`ā ${message}`, ANSI.green));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Display an error message
|
|
46
|
+
*/
|
|
47
|
+
export function showError(message) {
|
|
48
|
+
console.log(c(`ā ${message}`, ANSI.red));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Display a warning message
|
|
53
|
+
*/
|
|
54
|
+
export function showWarning(message) {
|
|
55
|
+
console.log(c(`ā ${message}`, ANSI.yellow));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Display an info message
|
|
60
|
+
*/
|
|
61
|
+
export function showInfo(message) {
|
|
62
|
+
console.log(c(`ā¹ ${message}`, ANSI.blue));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Ask a simple yes/no question
|
|
67
|
+
* Returns true for yes, false for no
|
|
68
|
+
*/
|
|
69
|
+
export async function askYesNo(rl, question, defaultYes = true) {
|
|
70
|
+
const suffix = defaultYes ? c('(Y/n)', ANSI.dim) : c('(y/N)', ANSI.dim);
|
|
71
|
+
const answer = await rl.question(`${question} ${suffix}: `);
|
|
72
|
+
|
|
73
|
+
if (!answer || !answer.trim()) {
|
|
74
|
+
return defaultYes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const normalized = answer.trim().toLowerCase();
|
|
78
|
+
return normalized === 'y' || normalized === 'yes' || normalized === 's' || normalized === 'si';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Ask a question with a default value
|
|
83
|
+
*/
|
|
84
|
+
export async function ask(rl, question, defaultValue = '') {
|
|
85
|
+
const suffix = defaultValue ? ` ${c(`(${defaultValue})`, ANSI.dim)}` : '';
|
|
86
|
+
const answer = await rl.question(`${question}${suffix}: `);
|
|
87
|
+
const trimmed = answer.trim();
|
|
88
|
+
return trimmed || defaultValue || '';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Ask a question that requires a non-empty answer
|
|
93
|
+
*/
|
|
94
|
+
export async function askNonEmpty(rl, question, defaultValue = '') {
|
|
95
|
+
while (true) {
|
|
96
|
+
const value = await ask(rl, question, defaultValue);
|
|
97
|
+
if (value.trim()) {
|
|
98
|
+
return value.trim();
|
|
99
|
+
}
|
|
100
|
+
showWarning('Value cannot be empty. Try again.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Display a numbered list and let user select one option
|
|
106
|
+
*/
|
|
107
|
+
export async function selectOption(rl, title, options) {
|
|
108
|
+
console.log(`\n${c(title, ANSI.bold)}`);
|
|
109
|
+
options.forEach((option, index) => {
|
|
110
|
+
const description = option.description ? c(` - ${option.description}`, ANSI.dim) : '';
|
|
111
|
+
console.log(` ${c(String(index + 1), ANSI.cyan)}. ${option.label}${description}`);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
while (true) {
|
|
115
|
+
const answer = await rl.question(`\n${c('Select (1-' + options.length + '):', ANSI.bold)} `);
|
|
116
|
+
const selected = Number.parseInt(answer.trim(), 10);
|
|
117
|
+
if (!Number.isNaN(selected) && selected >= 1 && selected <= options.length) {
|
|
118
|
+
return options[selected - 1];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
showWarning('Invalid selection. Try again.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Display the Portl ASCII banner
|
|
127
|
+
*/
|
|
128
|
+
export function showBanner() {
|
|
129
|
+
const banner = `
|
|
130
|
+
āāāāāāā āāāāāāā āāāāāāā āāāāāāāāāāāā
|
|
131
|
+
āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
132
|
+
āāāāāāāāāāā āāāāāāāāāāā āāā āāā
|
|
133
|
+
āāāāāāā āāā āāāāāāāāāāā āāā āāā
|
|
134
|
+
āāā āāāāāāāāāāāā āāā āāā āāāāāāāā
|
|
135
|
+
āāā āāāāāāā āāā āāā āāā āāāāāāāā
|
|
136
|
+
`;
|
|
137
|
+
console.log(c(banner, ANSI.cyan));
|
|
138
|
+
console.log(c('Portl Setup Wizard', ANSI.bold));
|
|
139
|
+
console.log(c('Bring your infra. Ship with AI.', ANSI.dim));
|
|
140
|
+
console.log('');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Display a summary section
|
|
145
|
+
*/
|
|
146
|
+
export function showSummary(title, items) {
|
|
147
|
+
showSection(title, 'ā
');
|
|
148
|
+
if (items.length === 0) {
|
|
149
|
+
console.log(c(' No integrations configured', ANSI.dim));
|
|
150
|
+
} else {
|
|
151
|
+
items.forEach(item => {
|
|
152
|
+
console.log(` ${c('ā', ANSI.green)} ${item}`);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
console.log('');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Display "next steps" instructions
|
|
160
|
+
*/
|
|
161
|
+
export function showNextSteps(steps) {
|
|
162
|
+
console.log(c('Next steps:', ANSI.bold));
|
|
163
|
+
steps.forEach((step, index) => {
|
|
164
|
+
console.log(` ${c(String(index + 1), ANSI.cyan)}. ${step}`);
|
|
165
|
+
});
|
|
166
|
+
console.log('');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Simple spinner for loading states
|
|
171
|
+
*/
|
|
172
|
+
export class Spinner {
|
|
173
|
+
constructor(message) {
|
|
174
|
+
this.message = message;
|
|
175
|
+
this.frames = ['ā ', 'ā ', 'ā ¹', 'ā ø', 'ā ¼', 'ā “', 'ā ¦', 'ā §', 'ā ', 'ā '];
|
|
176
|
+
this.index = 0;
|
|
177
|
+
this.intervalId = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
start() {
|
|
181
|
+
process.stdout.write(`${this.frames[0]} ${this.message}`);
|
|
182
|
+
this.intervalId = setInterval(() => {
|
|
183
|
+
this.index = (this.index + 1) % this.frames.length;
|
|
184
|
+
process.stdout.write(`\r${this.frames[this.index]} ${this.message}`);
|
|
185
|
+
}, 80);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
success(message) {
|
|
189
|
+
if (this.intervalId) {
|
|
190
|
+
clearInterval(this.intervalId);
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write(`\r${c('ā', ANSI.green)} ${message || this.message}\n`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fail(message) {
|
|
196
|
+
if (this.intervalId) {
|
|
197
|
+
clearInterval(this.intervalId);
|
|
198
|
+
}
|
|
199
|
+
process.stdout.write(`\r${c('ā', ANSI.red)} ${message || this.message}\n`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
stop() {
|
|
203
|
+
if (this.intervalId) {
|
|
204
|
+
clearInterval(this.intervalId);
|
|
205
|
+
}
|
|
206
|
+
process.stdout.write('\r' + ' '.repeat(this.message.length + 3) + '\r');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { ANSI, c };
|