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.
- package/CHANGELOG.md +70 -0
- package/CODE_REVIEW_ANALYSIS.md +177 -0
- package/CONTRIBUTING.md +132 -0
- package/FIXES_IMPLEMENTED.md +546 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/bin/andrud.js +24 -0
- package/package.json +80 -0
- package/src/__tests__/context.test.ts +133 -0
- package/src/__tests__/generator.test.ts +107 -0
- package/src/__tests__/validation.test.ts +105 -0
- package/src/cli/commands/create.ts +252 -0
- package/src/cli/commands/info.ts +178 -0
- package/src/cli/commands/init.ts +186 -0
- package/src/cli/commands/list.ts +156 -0
- package/src/cli/commands/new.ts +316 -0
- package/src/cli/index.ts +116 -0
- package/src/core/config.ts +172 -0
- package/src/core/context.ts +212 -0
- package/src/core/generator.ts +1350 -0
- package/src/core/types.ts +184 -0
- package/src/templates/index.ts +162 -0
- package/src/types/gradient-string.d.ts +25 -0
- package/src/ui/colors.ts +139 -0
- package/src/ui/output.ts +230 -0
- package/src/ui/prompts.ts +170 -0
- package/src/ui/spinners.ts +95 -0
- package/src/ui/types.ts +41 -0
- package/src/utils/filesystem.ts +222 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/object.ts +456 -0
- package/src/utils/validation.ts +345 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Object and array utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Object utilities
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Deep merge objects
|
|
9
|
+
*/
|
|
10
|
+
export function merge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
|
11
|
+
for (const source of sources) {
|
|
12
|
+
for (const key of Object.keys(source) as Array<keyof T>) {
|
|
13
|
+
const targetValue = target[key];
|
|
14
|
+
const sourceValue = source[key];
|
|
15
|
+
if (
|
|
16
|
+
sourceValue &&
|
|
17
|
+
typeof sourceValue === 'object' &&
|
|
18
|
+
!Array.isArray(sourceValue) &&
|
|
19
|
+
targetValue &&
|
|
20
|
+
typeof targetValue === 'object' &&
|
|
21
|
+
!Array.isArray(targetValue)
|
|
22
|
+
) {
|
|
23
|
+
(target as Record<string, unknown>)[key as string] = merge(
|
|
24
|
+
targetValue as object,
|
|
25
|
+
sourceValue as object
|
|
26
|
+
);
|
|
27
|
+
} else {
|
|
28
|
+
(target as Record<string, unknown>)[key as string] = sourceValue as unknown;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return target;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Omit keys from object
|
|
37
|
+
*/
|
|
38
|
+
export function omit<T extends object, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
|
|
39
|
+
const result = { ...obj };
|
|
40
|
+
keys.forEach((key) => delete result[key]);
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Pick keys from object
|
|
46
|
+
*/
|
|
47
|
+
export function pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
|
|
48
|
+
const result = {} as Pick<T, K>;
|
|
49
|
+
keys.forEach((key) => {
|
|
50
|
+
result[key] = obj[key];
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Invert object keys and values
|
|
57
|
+
*/
|
|
58
|
+
export function invert<T extends Record<string, string | number>>(obj: T): Record<string, string> {
|
|
59
|
+
const result: Record<string, string> = {};
|
|
60
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
61
|
+
result[value.toString()] = key;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Map values of object
|
|
68
|
+
*/
|
|
69
|
+
export function mapValues<T extends object, U>(
|
|
70
|
+
obj: T,
|
|
71
|
+
fn: (value: T[keyof T], key: keyof T) => U
|
|
72
|
+
): Record<keyof T, U> {
|
|
73
|
+
const result = {} as Record<keyof T, U>;
|
|
74
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
75
|
+
result[key as keyof T] = fn(value as T[keyof T], key as keyof T);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Filter object keys
|
|
82
|
+
*/
|
|
83
|
+
export function filterKeys<T extends object>(
|
|
84
|
+
obj: T,
|
|
85
|
+
fn: (key: keyof T) => boolean
|
|
86
|
+
): Partial<T> {
|
|
87
|
+
const result: Partial<T> = {};
|
|
88
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
89
|
+
if (fn(key as keyof T)) {
|
|
90
|
+
result[key as keyof T] = value as T[keyof T];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Array utilities
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Group array items by key
|
|
100
|
+
*/
|
|
101
|
+
export function groupBy<T, K extends string | number>(
|
|
102
|
+
array: T[],
|
|
103
|
+
keyFn: (item: T) => K
|
|
104
|
+
): Record<K, T[]> {
|
|
105
|
+
return array.reduce((result, item) => {
|
|
106
|
+
const key = keyFn(item);
|
|
107
|
+
if (!result[key]) {
|
|
108
|
+
result[key] = [];
|
|
109
|
+
}
|
|
110
|
+
result[key].push(item);
|
|
111
|
+
return result;
|
|
112
|
+
}, {} as Record<K, T[]>);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get unique values
|
|
117
|
+
*/
|
|
118
|
+
export function unique<T>(array: T[]): T[] {
|
|
119
|
+
return Array.from(new Set(array));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get unique values by predicate
|
|
124
|
+
*/
|
|
125
|
+
export function uniqueBy<T>(array: T[], predicate: (item: T) => unknown): T[] {
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
return array.filter((item) => {
|
|
128
|
+
const key = predicate(item);
|
|
129
|
+
if (seen.has(key)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
seen.add(key);
|
|
133
|
+
return true;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Split array into chunks
|
|
139
|
+
*/
|
|
140
|
+
export function chunk<T>(array: T[], size: number): T[][] {
|
|
141
|
+
const chunks: T[][] = [];
|
|
142
|
+
for (let i = 0; i < array.length; i += size) {
|
|
143
|
+
chunks.push(array.slice(i, i + size));
|
|
144
|
+
}
|
|
145
|
+
return chunks;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Flatten array one level
|
|
150
|
+
*/
|
|
151
|
+
export function flatten<T>(array: (T | T[])[]): T[] {
|
|
152
|
+
const result: T[] = [];
|
|
153
|
+
for (const item of array) {
|
|
154
|
+
if (Array.isArray(item)) {
|
|
155
|
+
result.push(...item);
|
|
156
|
+
} else {
|
|
157
|
+
result.push(item);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Deep flatten nested array
|
|
165
|
+
*/
|
|
166
|
+
export function flattenDeep<T>(array: (T | T[])[]): T[] {
|
|
167
|
+
return array.reduce<T[]>((acc, item) =>
|
|
168
|
+
Array.isArray(item) ? acc.concat(flattenDeep(item)) : acc.concat(item), []);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get difference between arrays
|
|
173
|
+
*/
|
|
174
|
+
export function difference<T>(a: T[], b: T[]): T[] {
|
|
175
|
+
const bSet = new Set(b);
|
|
176
|
+
return a.filter((item) => !bSet.has(item));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get intersection of arrays
|
|
181
|
+
*/
|
|
182
|
+
export function intersection<T>(a: T[], b: T[]): T[] {
|
|
183
|
+
const bSet = new Set(b);
|
|
184
|
+
return a.filter((item) => bSet.has(item));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get union of arrays
|
|
189
|
+
*/
|
|
190
|
+
export function union<T>(...arrays: T[][]): T[] {
|
|
191
|
+
return unique(arrays.flat());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Sort array by key
|
|
196
|
+
*/
|
|
197
|
+
export function sortBy<T>(array: T[], keyFn: (item: T) => unknown | unknown[]): T[] {
|
|
198
|
+
return [...array].sort((a, b) => {
|
|
199
|
+
const aKey = keyFn(a);
|
|
200
|
+
const bKey = keyFn(b);
|
|
201
|
+
if ((aKey as string) < (bKey as string)) return -1;
|
|
202
|
+
if ((aKey as string) > (bKey as string)) return 1;
|
|
203
|
+
return 0;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Partition array by predicate
|
|
209
|
+
*/
|
|
210
|
+
export function partition<T>(array: T[], predicate: (item: T) => boolean): [T[], T[]] {
|
|
211
|
+
const truthy: T[] = [];
|
|
212
|
+
const falsy: T[] = [];
|
|
213
|
+
array.forEach((item) => {
|
|
214
|
+
if (predicate(item)) {
|
|
215
|
+
truthy.push(item);
|
|
216
|
+
} else {
|
|
217
|
+
falsy.push(item);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return [truthy, falsy];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Remove falsy values from array
|
|
225
|
+
*/
|
|
226
|
+
export function compact<T>(array: (T | null | undefined | false | '' | 0)[]): T[] {
|
|
227
|
+
return array.filter(Boolean) as T[];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Zip arrays together
|
|
232
|
+
*/
|
|
233
|
+
export function zip<T, U>(a: T[], b: U[]): Array<[T, U]> {
|
|
234
|
+
const length = Math.min(a.length, b.length);
|
|
235
|
+
const result: Array<[T, U]> = [];
|
|
236
|
+
for (let i = 0; i < length; i++) {
|
|
237
|
+
const aItem = a[i];
|
|
238
|
+
const bItem = b[i];
|
|
239
|
+
if (aItem !== undefined && bItem !== undefined) {
|
|
240
|
+
result.push([aItem, bItem]);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Unzip zipped arrays
|
|
248
|
+
*/
|
|
249
|
+
export function unzip<T, U>(zipped: Array<[T, U]>): [T[], U[]] {
|
|
250
|
+
const a: T[] = [];
|
|
251
|
+
const b: U[] = [];
|
|
252
|
+
zipped.forEach(([first, second]) => {
|
|
253
|
+
a.push(first);
|
|
254
|
+
b.push(second);
|
|
255
|
+
});
|
|
256
|
+
return [a, b];
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Generate range of numbers
|
|
261
|
+
*/
|
|
262
|
+
export function range(start: number, end?: number, step: number = 1): number[] {
|
|
263
|
+
if (end === undefined) {
|
|
264
|
+
end = start;
|
|
265
|
+
start = 0;
|
|
266
|
+
}
|
|
267
|
+
const result: number[] = [];
|
|
268
|
+
for (let i = start; i < end; i += step) {
|
|
269
|
+
result.push(i);
|
|
270
|
+
}
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Generate array of repeated values
|
|
276
|
+
*/
|
|
277
|
+
export function times<T>(n: number, fn: (index: number) => T): T[] {
|
|
278
|
+
return Array.from({ length: n }, (_, i) => fn(i));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Math utilities
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Clamp number between min and max
|
|
285
|
+
*/
|
|
286
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
287
|
+
return Math.min(Math.max(value, min), max);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Generate random integer between min and max
|
|
292
|
+
*/
|
|
293
|
+
export function randomInt(min: number, max: number): number {
|
|
294
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Get random element from array
|
|
299
|
+
*/
|
|
300
|
+
export function randomElement<T>(array: T[]): T | undefined {
|
|
301
|
+
if (array.length === 0) return undefined;
|
|
302
|
+
return array[Math.floor(Math.random() * array.length)] as T;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Shuffle array in place
|
|
307
|
+
*/
|
|
308
|
+
export function shuffle<T>(array: T[]): T[] {
|
|
309
|
+
const result = [...array];
|
|
310
|
+
for (let i = result.length - 1; i > 0; i--) {
|
|
311
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
312
|
+
const temp = result[i];
|
|
313
|
+
const swap = result[j];
|
|
314
|
+
if (temp !== undefined && swap !== undefined) {
|
|
315
|
+
result[i] = swap;
|
|
316
|
+
result[j] = temp;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Async utilities
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Sleep for specified milliseconds
|
|
326
|
+
*/
|
|
327
|
+
export function sleep(ms: number): Promise<void> {
|
|
328
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export interface RetryOptions {
|
|
332
|
+
retries?: number;
|
|
333
|
+
delay?: number;
|
|
334
|
+
backoff?: number;
|
|
335
|
+
onRetry?: (error: Error, attempt: number) => void;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Retry function on failure
|
|
340
|
+
*/
|
|
341
|
+
export async function retry<T>(
|
|
342
|
+
fn: () => Promise<T>,
|
|
343
|
+
options: RetryOptions = {}
|
|
344
|
+
): Promise<T> {
|
|
345
|
+
const {
|
|
346
|
+
retries = 3,
|
|
347
|
+
delay = 1000,
|
|
348
|
+
backoff = 2,
|
|
349
|
+
onRetry
|
|
350
|
+
} = options;
|
|
351
|
+
|
|
352
|
+
let lastError: Error;
|
|
353
|
+
|
|
354
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
355
|
+
try {
|
|
356
|
+
return await fn();
|
|
357
|
+
} catch (error) {
|
|
358
|
+
lastError = error as Error;
|
|
359
|
+
|
|
360
|
+
if (attempt < retries) {
|
|
361
|
+
if (onRetry) {
|
|
362
|
+
onRetry(lastError, attempt + 1);
|
|
363
|
+
}
|
|
364
|
+
await sleep(delay * Math.pow(backoff, attempt));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
throw lastError!;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Function utilities
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Memoize function results
|
|
376
|
+
*/
|
|
377
|
+
export function memoize<T extends (...args: unknown[]) => unknown>(
|
|
378
|
+
fn: T
|
|
379
|
+
): T & { clear: () => void } {
|
|
380
|
+
const cache = new Map<string, unknown>();
|
|
381
|
+
const memoized = ((...args: unknown[]) => {
|
|
382
|
+
const key = JSON.stringify(args);
|
|
383
|
+
if (cache.has(key)) {
|
|
384
|
+
return cache.get(key);
|
|
385
|
+
}
|
|
386
|
+
const result = fn(...args);
|
|
387
|
+
cache.set(key, result);
|
|
388
|
+
return result;
|
|
389
|
+
}) as T & { clear: () => void };
|
|
390
|
+
|
|
391
|
+
memoized.clear = () => cache.clear();
|
|
392
|
+
return memoized;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Deep utilities
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Deep clone an object
|
|
399
|
+
*/
|
|
400
|
+
export function deepClone<T>(obj: T): T {
|
|
401
|
+
if (obj === null || typeof obj !== 'object') {
|
|
402
|
+
return obj;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (Array.isArray(obj)) {
|
|
406
|
+
return obj.map((item) => deepClone(item)) as unknown as T;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (obj instanceof Date) {
|
|
410
|
+
return new Date(obj.getTime()) as unknown as T;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (obj instanceof RegExp) {
|
|
414
|
+
return new RegExp(obj.source, obj.flags) as unknown as T;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const cloned = {} as T;
|
|
418
|
+
for (const key of Object.keys(obj) as Array<keyof T>) {
|
|
419
|
+
cloned[key] = deepClone(obj[key]);
|
|
420
|
+
}
|
|
421
|
+
return cloned;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Deep equality check
|
|
426
|
+
*/
|
|
427
|
+
export function deepEqual(a: unknown, b: unknown): boolean {
|
|
428
|
+
if (a === b) return true;
|
|
429
|
+
|
|
430
|
+
if (a === null || b === null) return false;
|
|
431
|
+
if (typeof a !== 'object' || typeof b !== 'object') return false;
|
|
432
|
+
|
|
433
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
434
|
+
|
|
435
|
+
const aObj = a as Record<string, unknown>;
|
|
436
|
+
const bObj = b as Record<string, unknown>;
|
|
437
|
+
|
|
438
|
+
const aKeys = Object.keys(aObj);
|
|
439
|
+
const bKeys = Object.keys(bObj);
|
|
440
|
+
|
|
441
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
442
|
+
|
|
443
|
+
for (const key of aKeys) {
|
|
444
|
+
if (!bKeys.includes(key)) return false;
|
|
445
|
+
if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Deep merge objects
|
|
453
|
+
*/
|
|
454
|
+
export function deepMerge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
|
455
|
+
return merge(target, ...sources);
|
|
456
|
+
}
|