andrud 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.
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Output formatting utilities
3
+ */
4
+
5
+ import pc from 'picocolors';
6
+ import { style, bold, dim, muted, primary, success, error, warning, info, section, printSeparator } from './colors.js';
7
+ import gradient from 'gradient-string';
8
+
9
+ export interface Logger {
10
+ log: (message: string, ...args: unknown[]) => void;
11
+ info: (message: string, ...args: unknown[]) => void;
12
+ success: (message: string, ...args: unknown[]) => void;
13
+ warn: (message: string, ...args: unknown[]) => void;
14
+ error: (message: string, ...args: unknown[]) => void;
15
+ debug: (message: string, ...args: unknown[]) => void;
16
+ verbose: (message: string, ...args: unknown[]) => void;
17
+ }
18
+
19
+ const logger: Logger = {
20
+ log: (message: string, ...args: unknown[]) => {
21
+ console.log(message, ...args);
22
+ },
23
+ info: (message: string, ...args: unknown[]) => {
24
+ console.log(pc.blue('INFO: ') + message, ...args);
25
+ },
26
+ success: (message: string, ...args: unknown[]) => {
27
+ console.log(pc.green('SUCCESS: ') + message, ...args);
28
+ },
29
+ warn: (message: string, ...args: unknown[]) => {
30
+ console.log(pc.yellow('WARN: ') + message, ...args);
31
+ },
32
+ error: (message: string, ...args: unknown[]) => {
33
+ console.error(pc.red('ERROR: ') + message, ...args);
34
+ },
35
+ debug: (message: string, ...args: unknown[]) => {
36
+ if (process.env.DEBUG || process.env.VERBOSE) {
37
+ console.log(pc.gray('DEBUG: ') + message, ...args);
38
+ }
39
+ },
40
+ verbose: (message: string, ...args: unknown[]) => {
41
+ if (process.env.VERBOSE) {
42
+ console.log(pc.gray('VERBOSE: ') + message, ...args);
43
+ }
44
+ }
45
+ };
46
+
47
+ export { logger };
48
+
49
+ /**
50
+ * Creates a gradient from cyan to green (singleton pattern to prevent memory leak)
51
+ */
52
+ const teenGradient = gradient('#00C9FF', '#92FE9D');
53
+
54
+ /**
55
+ * Prints the welcome banner
56
+ */
57
+ export function printWelcome(): void {
58
+ console.log('');
59
+ console.log(pc.cyan(`
60
+ ╔═══════════════════════════════════════════════════════════╗
61
+ ║ ║
62
+ ║ ██╗ ██╗ █████╗ ██████╗ ███████╗███╗ ██╗ ║
63
+ ║ ██║ ██║██╔══██╗██╔══██╗ ██╔════╝████╗ ██║ ║
64
+ ║ ██║ █╗ ██║███████║██████╔╝ █████╗ ██╔██╗ ██║ ║
65
+ ║ ██║███╗██║██╔══██║██╔══██╗ ██╔══╝ ██║╚██╗██║ ║
66
+ ║ ╚███╔███╔╝██║ ██║██║ ██║ ███████╗██║ ╚████║ ║
67
+ ║ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ║
68
+ ║ ║
69
+ ║ Android Project Generator v1.0.0 ║
70
+ ║ Modern Android Development CLI ║
71
+ ║ ║
72
+ ╚═══════════════════════════════════════════════════════════╝
73
+ `));
74
+ console.log('');
75
+ }
76
+
77
+ /**
78
+ * Prints the goodbye message
79
+ */
80
+ export function printGoodbye(success: boolean = true): void {
81
+ // Uses module-level teenGradient constant
82
+ if (success) {
83
+ console.log('');
84
+ console.log(teenGradient(`
85
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86
+
87
+ Thanks for using andrud! Happy coding! 🚀
88
+
89
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90
+ `));
91
+ } else {
92
+ console.log('');
93
+ console.log(pc.red(`
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
+
96
+ Operation failed. Please check the error above.
97
+
98
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99
+ `));
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Prints a success message
105
+ */
106
+ export function printSuccess(message: string, details?: string): void {
107
+ console.log(pc.green(' ✓ ') + pc.white(message));
108
+ if (details) {
109
+ console.log(pc.gray(` ${details}`));
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Prints an error message
115
+ */
116
+ export function printError(message: string, details?: string): void {
117
+ console.error(pc.red(' ✗ ') + pc.white(message));
118
+ if (details) {
119
+ console.error(pc.gray(` ${details}`));
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Prints a section header
125
+ */
126
+ export function printSection(text: string): void {
127
+ console.log('');
128
+ console.log(section(text));
129
+ console.log(dim(printSeparator('─', 50)));
130
+ }
131
+
132
+ /**
133
+ * Prints key-value pairs
134
+ */
135
+ export function printKeyValue(items: Array<{ key: string; value: string }>): void {
136
+ const maxKeyLength = Math.max(...items.map(item => item.key.length));
137
+ items.forEach(item => {
138
+ const padding = ' '.repeat(maxKeyLength - item.key.length);
139
+ console.log(` ${pc.cyan(item.key)}${padding} ${item.value}`);
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Prints a separator line
145
+ */
146
+ export function printSeparatorLine(char: string = '─', length: number = 60): void {
147
+ console.log(dim(char.repeat(length)));
148
+ }
149
+
150
+ /**
151
+ * Prints a banner with text
152
+ */
153
+ export function printBanner(text: string, color: 'primary' | 'success' | 'warning' | 'error' = 'primary'): void {
154
+ const padding = 4;
155
+ const line = ' '.repeat(padding);
156
+ const border = '━'.repeat(text.length + padding * 2);
157
+
158
+ console.log('');
159
+ console.log(pc.cyan(' ' + border));
160
+ console.log(pc.cyan(' ║') + ' '.repeat(padding) + text + ' '.repeat(padding) + pc.cyan('║'));
161
+ console.log(pc.cyan(' ' + border));
162
+ console.log('');
163
+ }
164
+
165
+ /**
166
+ * Prints an ASCII box with content
167
+ */
168
+ export function printAsciiBox(lines: string[], options: { border?: string; padding?: number } = {}): void {
169
+ const border = options.border ?? '─';
170
+ const padding = options.padding ?? 2;
171
+ const maxLength = Math.max(...lines.map(l => l.length));
172
+ const width = maxLength + padding * 2;
173
+
174
+ console.log(pc.gray(' ┌' + border.repeat(width) + '┐'));
175
+
176
+ if (lines.length === 0) {
177
+ console.log(pc.gray(' │' + ' '.repeat(width) + '│'));
178
+ } else {
179
+ lines.forEach(line => {
180
+ const padded = line + ' '.repeat(maxLength - line.length);
181
+ console.log(pc.gray(' │') + ' '.repeat(padding) + padded + ' '.repeat(padding) + pc.gray('│'));
182
+ });
183
+ }
184
+
185
+ console.log(pc.gray(' └' + border.repeat(width) + '┘'));
186
+ }
187
+
188
+ /**
189
+ * Prints a table with columns
190
+ */
191
+ export function printTable<T>(
192
+ columns: Array<{ header: string; accessor: (row: T) => string; width?: number }>,
193
+ rows: T[]
194
+ ): void {
195
+ const colWidths = columns.map(col => {
196
+ const headerWidth = col.header.length;
197
+ const dataWidths = rows.map(row => col.accessor(row).length);
198
+ return col.width ?? Math.max(headerWidth, ...dataWidths);
199
+ });
200
+
201
+ // Print header
202
+ const headerRow = columns.map((_col, i) => {
203
+ const width = colWidths[i] ?? 10;
204
+ return _col.header.substring(0, width).padEnd(width);
205
+ });
206
+ console.log(bold(headerRow.join(' ')));
207
+ console.log(pc.gray(colWidths.map(w => '─'.repeat(w)).join(' ')));
208
+
209
+ // Print rows
210
+ rows.forEach(row => {
211
+ const dataRow = columns.map((col, i) => {
212
+ const width = colWidths[i] ?? 10;
213
+ const value = col.accessor(row);
214
+ return value.substring(0, width).padEnd(width);
215
+ });
216
+ console.log(dataRow.join(' '));
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Prints numbered steps
222
+ */
223
+ export function printSteps(steps: string[]): void {
224
+ console.log('');
225
+ steps.forEach((step, index) => {
226
+ const num = pc.cyan(`${index + 1}.`);
227
+ console.log(` ${num} ${step}`);
228
+ });
229
+ console.log('');
230
+ }
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Wrapper around @clack/prompts with proper TypeScript types
3
+ */
4
+
5
+ import {
6
+ select,
7
+ text,
8
+ multiselect,
9
+ confirm,
10
+ isCancel,
11
+ cancel,
12
+ type SelectOptions,
13
+ type TextOptions,
14
+ type MultiSelectOptions,
15
+ type ConfirmOptions
16
+ } from '@clack/prompts';
17
+
18
+ // Re-export core functions
19
+ export {
20
+ select,
21
+ text,
22
+ multiselect,
23
+ confirm,
24
+ isCancel,
25
+ cancel,
26
+ type SelectOptions,
27
+ type TextOptions,
28
+ type MultiSelectOptions,
29
+ type ConfirmOptions
30
+ };
31
+
32
+ // Helper function to ask for app name
33
+ export async function askAppName(defaultValue?: string): Promise<string> {
34
+ const value = await text({
35
+ message: '? What is the app name?',
36
+ placeholder: 'MyAwesomeApp',
37
+ defaultValue: defaultValue ?? 'MyAwesomeApp',
38
+ validate: (value: string) => {
39
+ if (!value || value.trim().length === 0) {
40
+ return 'App name cannot be empty';
41
+ }
42
+ if (!/^[a-zA-Z][a-zA-Z0-9_]*$/.test(value)) {
43
+ return 'App name must start with a letter and contain only letters, numbers, and underscores';
44
+ }
45
+ return undefined;
46
+ }
47
+ });
48
+
49
+ if (isCancel(value)) {
50
+ cancel();
51
+ process.exit(0);
52
+ }
53
+
54
+ return value as string;
55
+ }
56
+
57
+ // Helper function to ask for package name
58
+ export async function askPackageName(defaultValue?: string): Promise<string> {
59
+ const value = await text({
60
+ message: '? What is the package name?',
61
+ placeholder: 'com.example.myapp',
62
+ defaultValue: defaultValue ?? 'com.example.myapp',
63
+ validate: (value: string) => {
64
+ if (!value || value.trim().length === 0) {
65
+ return 'Package name cannot be empty';
66
+ }
67
+ if (!/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/.test(value)) {
68
+ return 'Package name must be a valid domain structure (e.g., com.example.myapp)';
69
+ }
70
+ return undefined;
71
+ }
72
+ });
73
+
74
+ if (isCancel(value)) {
75
+ cancel();
76
+ process.exit(0);
77
+ }
78
+
79
+ return value as string;
80
+ }
81
+
82
+ // Helper function to ask for project directory
83
+ export async function askDirectory(defaultValue?: string): Promise<string> {
84
+ const value = await text({
85
+ message: '? In which directory should the project be created?',
86
+ placeholder: './my-app',
87
+ defaultValue: defaultValue ?? './my-app',
88
+ validate: (value: string) => {
89
+ if (!value || value.trim().length === 0) {
90
+ return 'Directory path cannot be empty';
91
+ }
92
+ if (value.includes('..')) {
93
+ return 'Directory path cannot contain ".." to prevent directory traversal';
94
+ }
95
+ return undefined;
96
+ }
97
+ });
98
+
99
+ if (isCancel(value)) {
100
+ cancel();
101
+ process.exit(0);
102
+ }
103
+
104
+ return value as string;
105
+ }
106
+
107
+ // Template selection helper
108
+ export async function selectTemplate<T extends string>(
109
+ templates: Array<{ label: string; value: T; hint?: string }>
110
+ ): Promise<T> {
111
+ const options = templates.map(t => ({
112
+ label: t.label + (t.hint ? ` ${t.hint}` : ''),
113
+ value: t.value
114
+ }));
115
+
116
+ const value = await select({
117
+ message: '? Select a project template',
118
+ options
119
+ });
120
+
121
+ if (isCancel(value)) {
122
+ cancel();
123
+ process.exit(0);
124
+ }
125
+
126
+ return value as T;
127
+ }
128
+
129
+ // Multi-select helper
130
+ export async function askMultiSelect<T>(
131
+ options: Array<{ label: string; value: T; hint?: string }>,
132
+ message: string = '? Select options',
133
+ min?: number
134
+ ): Promise<T[]> {
135
+ const formattedOptions = options.map(o => ({
136
+ label: o.label + (o.hint ? ` (${o.hint})` : ''),
137
+ value: o.value
138
+ }));
139
+
140
+ const value = await multiselect({
141
+ message,
142
+ options: formattedOptions,
143
+ ...(min !== undefined ? { min } : {})
144
+ });
145
+
146
+ if (isCancel(value)) {
147
+ cancel();
148
+ process.exit(0);
149
+ }
150
+
151
+ return value as T[];
152
+ }
153
+
154
+ // Confirmation helper
155
+ export async function askConfirmation(
156
+ message: string,
157
+ initialValue: boolean = false
158
+ ): Promise<boolean> {
159
+ const value = await confirm({
160
+ message: `? ${message}`,
161
+ initialValue
162
+ });
163
+
164
+ if (isCancel(value)) {
165
+ cancel();
166
+ process.exit(0);
167
+ }
168
+
169
+ return value as boolean;
170
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Spinner utilities using ora
3
+ */
4
+
5
+ import ora, { type Ora } from 'ora';
6
+
7
+ export class AsyncSpinner {
8
+ private spinner: Ora | null = null;
9
+ private startTime: number = 0;
10
+
11
+ start(text?: string): void {
12
+ this.startTime = Date.now();
13
+ this.spinner = ora({
14
+ text: text ?? 'Loading...',
15
+ spinner: 'dots'
16
+ }).start();
17
+ }
18
+
19
+ update(text: string): void {
20
+ if (this.spinner) {
21
+ this.spinner.text = text;
22
+ }
23
+ }
24
+
25
+ succeed(text?: string): void {
26
+ if (this.spinner) {
27
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
28
+ this.spinner.succeed(text ? `${text} (${elapsed}s)` : undefined);
29
+ this.spinner = null;
30
+ }
31
+ }
32
+
33
+ fail(text?: string): void {
34
+ if (this.spinner) {
35
+ const elapsed = ((Date.now() - this.startTime) / 1000).toFixed(1);
36
+ this.spinner.fail(text ? `${text} (${elapsed}s)` : undefined);
37
+ this.spinner = null;
38
+ }
39
+ }
40
+
41
+ warn(text?: string): void {
42
+ if (this.spinner) {
43
+ this.spinner.warn(text);
44
+ }
45
+ }
46
+
47
+ stop(): void {
48
+ if (this.spinner) {
49
+ this.spinner.stop();
50
+ this.spinner = null;
51
+ }
52
+ }
53
+
54
+ isSpinning(): boolean {
55
+ return this.spinner?.isSpinning ?? false;
56
+ }
57
+ }
58
+
59
+ export interface WithSpinnerOptions {
60
+ text?: string;
61
+ successText?: string;
62
+ failText?: string;
63
+ warnText?: string;
64
+ }
65
+
66
+ /**
67
+ * Executes an async function with a spinner
68
+ */
69
+ export async function withSpinner<T>(
70
+ text: string,
71
+ fn: () => Promise<T>,
72
+ options?: WithSpinnerOptions
73
+ ): Promise<T> {
74
+ const spinner = new AsyncSpinner();
75
+ spinner.start(text);
76
+
77
+ try {
78
+ const result = await fn();
79
+ spinner.succeed(options?.successText);
80
+ return result;
81
+ } catch (error) {
82
+ spinner.fail(options?.failText ?? (error instanceof Error ? error.message : 'An error occurred'));
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Creates a simple loading indicator
89
+ */
90
+ export function createSpinner(text?: string): Ora {
91
+ return ora({
92
+ text,
93
+ spinner: 'dots'
94
+ });
95
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Type definitions for UI components and utilities
3
+ */
4
+
5
+ export type StyleType = 'primary' | 'success' | 'warning' | 'error' | 'info' | 'muted';
6
+
7
+ export interface SpinnerOptions {
8
+ text?: string;
9
+ color?: string;
10
+ }
11
+
12
+ export interface TableColumn<T> {
13
+ header: string;
14
+ accessor: (row: T) => string;
15
+ width?: number;
16
+ }
17
+
18
+ export interface ConfirmationOptions {
19
+ message: string;
20
+ initialValue?: boolean;
21
+ }
22
+
23
+ export interface SelectOption<T> {
24
+ label: string;
25
+ value: T;
26
+ hint?: string;
27
+ }
28
+
29
+ export interface TextInputOptions {
30
+ message: string;
31
+ placeholder?: string;
32
+ defaultValue?: string;
33
+ validate?: (value: string) => string | null;
34
+ }
35
+
36
+ export interface MultiSelectOptions<T> {
37
+ message: string;
38
+ options: SelectOption<T>[];
39
+ min?: number;
40
+ max?: number;
41
+ }