recker 1.0.27 → 1.0.28-next.9eb3868

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.
Files changed (64) hide show
  1. package/dist/browser/scrape/extractors.js +2 -1
  2. package/dist/browser/scrape/types.d.ts +2 -1
  3. package/dist/cli/index.js +142 -3
  4. package/dist/cli/tui/shell.d.ts +1 -0
  5. package/dist/cli/tui/shell.js +157 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/scrape/extractors.js +2 -1
  9. package/dist/scrape/types.d.ts +2 -1
  10. package/dist/seo/analyzer.d.ts +42 -0
  11. package/dist/seo/analyzer.js +727 -0
  12. package/dist/seo/index.d.ts +5 -0
  13. package/dist/seo/index.js +2 -0
  14. package/dist/seo/rules/accessibility.d.ts +2 -0
  15. package/dist/seo/rules/accessibility.js +128 -0
  16. package/dist/seo/rules/content.d.ts +2 -0
  17. package/dist/seo/rules/content.js +236 -0
  18. package/dist/seo/rules/crawl.d.ts +2 -0
  19. package/dist/seo/rules/crawl.js +307 -0
  20. package/dist/seo/rules/cwv.d.ts +2 -0
  21. package/dist/seo/rules/cwv.js +337 -0
  22. package/dist/seo/rules/ecommerce.d.ts +2 -0
  23. package/dist/seo/rules/ecommerce.js +252 -0
  24. package/dist/seo/rules/i18n.d.ts +2 -0
  25. package/dist/seo/rules/i18n.js +222 -0
  26. package/dist/seo/rules/images.d.ts +2 -0
  27. package/dist/seo/rules/images.js +180 -0
  28. package/dist/seo/rules/index.d.ts +52 -0
  29. package/dist/seo/rules/index.js +141 -0
  30. package/dist/seo/rules/internal-linking.d.ts +2 -0
  31. package/dist/seo/rules/internal-linking.js +375 -0
  32. package/dist/seo/rules/links.d.ts +2 -0
  33. package/dist/seo/rules/links.js +150 -0
  34. package/dist/seo/rules/local.d.ts +2 -0
  35. package/dist/seo/rules/local.js +265 -0
  36. package/dist/seo/rules/meta.d.ts +2 -0
  37. package/dist/seo/rules/meta.js +523 -0
  38. package/dist/seo/rules/mobile.d.ts +2 -0
  39. package/dist/seo/rules/mobile.js +71 -0
  40. package/dist/seo/rules/performance.d.ts +2 -0
  41. package/dist/seo/rules/performance.js +246 -0
  42. package/dist/seo/rules/pwa.d.ts +2 -0
  43. package/dist/seo/rules/pwa.js +302 -0
  44. package/dist/seo/rules/readability.d.ts +2 -0
  45. package/dist/seo/rules/readability.js +255 -0
  46. package/dist/seo/rules/schema.d.ts +2 -0
  47. package/dist/seo/rules/schema.js +54 -0
  48. package/dist/seo/rules/security.d.ts +2 -0
  49. package/dist/seo/rules/security.js +147 -0
  50. package/dist/seo/rules/social.d.ts +2 -0
  51. package/dist/seo/rules/social.js +373 -0
  52. package/dist/seo/rules/structural.d.ts +2 -0
  53. package/dist/seo/rules/structural.js +155 -0
  54. package/dist/seo/rules/technical.d.ts +2 -0
  55. package/dist/seo/rules/technical.js +223 -0
  56. package/dist/seo/rules/thresholds.d.ts +196 -0
  57. package/dist/seo/rules/thresholds.js +118 -0
  58. package/dist/seo/rules/types.d.ts +322 -0
  59. package/dist/seo/rules/types.js +11 -0
  60. package/dist/seo/types.d.ts +160 -0
  61. package/dist/seo/types.js +1 -0
  62. package/dist/utils/columns.d.ts +14 -0
  63. package/dist/utils/columns.js +69 -0
  64. package/package.json +1 -1
@@ -0,0 +1,322 @@
1
+ import type { SeoStatus } from '../types.js';
2
+ import type { ExtractedLink } from '../../scrape/types.js';
3
+ export type RuleSeverity = 'error' | 'warning' | 'info';
4
+ export type RuleCategory = 'title' | 'meta' | 'og' | 'twitter' | 'headings' | 'images' | 'links' | 'content' | 'technical' | 'security' | 'mobile' | 'structured-data' | 'performance' | 'accessibility';
5
+ export interface RuleContext {
6
+ title?: string;
7
+ titleLength?: number;
8
+ metaDescription?: string;
9
+ metaDescriptionLength?: number;
10
+ metaKeywords?: string[];
11
+ metaRobots?: string[];
12
+ ogTitle?: string;
13
+ ogDescription?: string;
14
+ ogImage?: string;
15
+ ogUrl?: string;
16
+ ogType?: string;
17
+ ogSiteName?: string;
18
+ twitterCard?: string;
19
+ twitterTitle?: string;
20
+ twitterDescription?: string;
21
+ twitterImage?: string;
22
+ twitterSite?: string;
23
+ h1Count?: number;
24
+ h1Text?: string;
25
+ h1Length?: number;
26
+ h2Count?: number;
27
+ headingHierarchyValid?: boolean;
28
+ headingSkippedLevels?: string[];
29
+ sectionWordCounts?: number[];
30
+ totalImages?: number;
31
+ imagesWithAlt?: number;
32
+ imagesWithoutAlt?: number;
33
+ imagesWithLazyLoad?: number;
34
+ imagesWithDimensions?: number;
35
+ imagesMissingDimensions?: number;
36
+ imagesWithEmptyAlt?: number;
37
+ imagesDecorativeCount?: number;
38
+ imagesUsingModernFormats?: number;
39
+ altTextLengths?: number[];
40
+ imageFilenames?: string[];
41
+ imagesWithAsyncDecoding?: number;
42
+ buttonsWithoutAriaLabel?: number;
43
+ linksWithoutAriaLabel?: number;
44
+ inputsWithoutLabel?: number;
45
+ formsWithoutAction?: number;
46
+ tablesWithoutCaption?: number;
47
+ iframesWithoutTitle?: number;
48
+ svgsWithoutTitle?: number;
49
+ interactiveElementsCount?: number;
50
+ ariaLabelledByMissing?: number;
51
+ allLinks?: ExtractedLink[];
52
+ totalLinks?: number;
53
+ internalLinks?: number;
54
+ externalLinks?: number;
55
+ linksWithoutText?: number;
56
+ nofollowLinks?: number;
57
+ sponsoredLinks?: number;
58
+ ugcLinks?: number;
59
+ brokenLinks?: number;
60
+ linksWithGenericText?: number;
61
+ externalLinksWithoutNoopener?: number;
62
+ externalLinksWithoutNoreferrer?: number;
63
+ problematicLinks?: {
64
+ withoutText?: ExtractedLink[];
65
+ genericText?: ExtractedLink[];
66
+ missingNoopener?: ExtractedLink[];
67
+ missingNoreferrer?: ExtractedLink[];
68
+ };
69
+ wordCount?: number;
70
+ characterCount?: number;
71
+ sentenceCount?: number;
72
+ paragraphCount?: number;
73
+ avgWordsPerSentence?: number;
74
+ avgParagraphLength?: number;
75
+ listCount?: number;
76
+ strongTagCount?: number;
77
+ emTagCount?: number;
78
+ subheadingFrequency?: number;
79
+ paragraphWordCounts?: number[];
80
+ avgSentenceLength?: number;
81
+ faqCount?: number;
82
+ imagePerWordRatio?: number;
83
+ mainKeyword?: string;
84
+ keywordDensity?: number;
85
+ fleschReadingEase?: number;
86
+ hasQuestionHeadings?: boolean;
87
+ hasHeader?: boolean;
88
+ hasNav?: boolean;
89
+ hasMain?: boolean;
90
+ hasArticle?: boolean;
91
+ hasSection?: boolean;
92
+ hasFooter?: boolean;
93
+ hasAboutPageLink?: boolean;
94
+ hasContactPageLink?: boolean;
95
+ hasPrivacyPolicyLink?: boolean;
96
+ hasTermsOfServiceLink?: boolean;
97
+ hasBreadcrumbsHtml?: boolean;
98
+ hasBreadcrumbsSchema?: boolean;
99
+ videoCount?: number;
100
+ audioCount?: number;
101
+ hasCanonical?: boolean;
102
+ canonicalUrl?: string;
103
+ hasViewport?: boolean;
104
+ viewportContent?: string;
105
+ hasCharset?: boolean;
106
+ charset?: string;
107
+ hasLang?: boolean;
108
+ langValue?: string;
109
+ isHttps?: boolean;
110
+ hasMixedContent?: boolean;
111
+ responseHeaders?: Record<string, string | string[]>;
112
+ textHtmlRatio?: number;
113
+ hasFavicon?: boolean;
114
+ faviconUrl?: string;
115
+ hasPreconnect?: boolean;
116
+ preconnectCount?: number;
117
+ hasDnsPrefetch?: boolean;
118
+ dnsPrefetchCount?: number;
119
+ hasPreload?: boolean;
120
+ preloadCount?: number;
121
+ renderBlockingResources?: number;
122
+ inlineScriptsCount?: number;
123
+ inlineStylesCount?: number;
124
+ lcpHints?: {
125
+ hasLargeImages?: boolean;
126
+ hasLazyLcp?: boolean;
127
+ hasPriorityHints?: boolean;
128
+ };
129
+ clsHints?: {
130
+ imagesWithoutDimensions?: number;
131
+ dynamicContent?: number;
132
+ };
133
+ jsonLdCount?: number;
134
+ jsonLdTypes?: string[];
135
+ url?: string;
136
+ urlLength?: number;
137
+ hreflangTags?: Array<{
138
+ lang: string;
139
+ href: string;
140
+ }>;
141
+ ogLocale?: string;
142
+ alternateLanguages?: string[];
143
+ titleMatchesH1?: boolean;
144
+ urlHasUppercase?: boolean;
145
+ urlHasSpecialChars?: boolean;
146
+ urlHasAccents?: boolean;
147
+ bodyTextLength?: number;
148
+ scriptCount?: number;
149
+ hasNoscriptContent?: boolean;
150
+ timings?: {
151
+ ttfb?: number;
152
+ dnsLookup?: number;
153
+ tcpConnect?: number;
154
+ tlsHandshake?: number;
155
+ download?: number;
156
+ total?: number;
157
+ };
158
+ responseSize?: number;
159
+ htmlSize?: number;
160
+ compressedSize?: number;
161
+ isCompressed?: boolean;
162
+ isProductPage?: boolean;
163
+ productSchema?: {
164
+ name?: string;
165
+ image?: string | string[];
166
+ offers?: {
167
+ price?: number | string;
168
+ lowPrice?: number | string;
169
+ priceCurrency?: string;
170
+ availability?: string;
171
+ priceValidUntil?: string;
172
+ validFrom?: string;
173
+ validThrough?: string;
174
+ };
175
+ aggregateRating?: {
176
+ ratingValue?: number | string;
177
+ reviewCount?: number;
178
+ ratingCount?: number;
179
+ };
180
+ review?: unknown;
181
+ brand?: string | {
182
+ name?: string;
183
+ };
184
+ sku?: string;
185
+ gtin?: string;
186
+ gtin13?: string;
187
+ gtin14?: string;
188
+ gtin8?: string;
189
+ mpn?: string;
190
+ };
191
+ hasLocalBusinessSignals?: boolean;
192
+ localBusinessSchema?: {
193
+ '@type'?: string;
194
+ name?: string;
195
+ address?: {
196
+ streetAddress?: string;
197
+ addressLocality?: string;
198
+ addressRegion?: string;
199
+ postalCode?: string;
200
+ addressCountry?: string;
201
+ };
202
+ telephone?: string;
203
+ openingHoursSpecification?: unknown;
204
+ openingHours?: string | string[];
205
+ geo?: {
206
+ latitude?: number | string;
207
+ longitude?: number | string;
208
+ };
209
+ areaServed?: unknown;
210
+ priceRange?: string;
211
+ };
212
+ hasPhoneOnPage?: boolean;
213
+ hasAddressOnPage?: boolean;
214
+ lcpCandidate?: {
215
+ element?: string;
216
+ src?: string;
217
+ loading?: string;
218
+ fetchpriority?: string;
219
+ };
220
+ hasLcpPreload?: boolean;
221
+ webFonts?: Array<{
222
+ family?: string;
223
+ hasSwap?: boolean;
224
+ hasOptional?: boolean;
225
+ hasSizeAdjust?: boolean;
226
+ hasAscentOverride?: boolean;
227
+ }>;
228
+ renderBlockingStylesheets?: number;
229
+ renderBlockingScripts?: number;
230
+ hasAspectRatioCss?: boolean;
231
+ hasResponsiveImages?: boolean;
232
+ hasAdsWithoutReservedSpace?: boolean;
233
+ hasBannersWithoutMinHeight?: boolean;
234
+ hasInfiniteScroll?: boolean;
235
+ largeInlineScripts?: number;
236
+ inlineEventHandlers?: number;
237
+ hasHeavyAnimations?: boolean;
238
+ externalOrigins?: number;
239
+ hasCriticalResources?: boolean;
240
+ hasInlineCriticalCss?: boolean;
241
+ hasSitemapLink?: boolean;
242
+ sitemapUrl?: string;
243
+ robotsHasSitemap?: boolean;
244
+ isPaginatedPage?: boolean;
245
+ hasRelPrev?: boolean;
246
+ hasRelNext?: boolean;
247
+ passiveVoicePercentage?: number;
248
+ transitionWordPercentage?: number;
249
+ consecutiveSentenceStarts?: number;
250
+ complexWordPercentage?: number;
251
+ hasManifest?: boolean;
252
+ manifestUrl?: string;
253
+ themeColor?: string;
254
+ hasAppleTouchIcon?: boolean;
255
+ hasAppleMobileWebAppCapable?: boolean;
256
+ appleStatusBarStyle?: string;
257
+ hasMaskableIcon?: boolean;
258
+ manifestStartUrl?: string;
259
+ manifestDisplay?: string;
260
+ manifestScope?: string;
261
+ manifestIconSizes?: number[];
262
+ manifestShortName?: string;
263
+ manifestName?: string;
264
+ manifestBackgroundColor?: string;
265
+ ogImageDimensions?: {
266
+ width: number;
267
+ height: number;
268
+ };
269
+ ogLocaleAlternate?: string[];
270
+ ogArticleTags?: string[];
271
+ ogArticlePublishedTime?: string;
272
+ ogArticleAuthor?: string;
273
+ twitterCreator?: string;
274
+ twitterImageAlt?: string;
275
+ linkedinAuthor?: string;
276
+ pinterestRichPinSupport?: boolean;
277
+ hasPinterestNopin?: boolean;
278
+ fbAppId?: string;
279
+ navLinkCount?: number;
280
+ footerLinkCount?: number;
281
+ contextualLinkCount?: number;
282
+ incomingInternalLinks?: number;
283
+ selfReferencingLinks?: number;
284
+ brokenInternalLinks?: number;
285
+ redirectChainLinks?: number;
286
+ pageClickDepth?: number;
287
+ }
288
+ export interface RuleEvidence {
289
+ found?: string | number | string[];
290
+ expected?: string | number | string[];
291
+ location?: string;
292
+ issue?: string;
293
+ impact?: string;
294
+ example?: string;
295
+ learnMore?: string;
296
+ }
297
+ export interface RuleResult {
298
+ id: string;
299
+ name: string;
300
+ category: RuleCategory;
301
+ severity: RuleSeverity;
302
+ status: SeoStatus;
303
+ message: string;
304
+ value?: string | number;
305
+ recommendation?: string;
306
+ evidence?: RuleEvidence;
307
+ details?: Record<string, unknown>;
308
+ }
309
+ export interface SeoRule {
310
+ id: string;
311
+ name: string;
312
+ category: RuleCategory;
313
+ severity: RuleSeverity;
314
+ description: string;
315
+ check: (ctx: RuleContext) => RuleResult | null;
316
+ }
317
+ export declare function createResult(rule: Pick<SeoRule, 'id' | 'name' | 'category' | 'severity'>, status: SeoStatus, message: string, options?: {
318
+ value?: string | number;
319
+ recommendation?: string;
320
+ evidence?: RuleEvidence;
321
+ details?: Record<string, unknown>;
322
+ }): RuleResult;
@@ -0,0 +1,11 @@
1
+ export function createResult(rule, status, message, options) {
2
+ return {
3
+ id: rule.id,
4
+ name: rule.name,
5
+ category: rule.category,
6
+ severity: rule.severity,
7
+ status,
8
+ message,
9
+ ...options,
10
+ };
11
+ }
@@ -0,0 +1,160 @@
1
+ export type SeoStatus = 'pass' | 'warn' | 'fail' | 'info';
2
+ export interface SeoCheckEvidence {
3
+ found?: string | number | string[];
4
+ expected?: string | number | string[];
5
+ location?: string;
6
+ issue?: string;
7
+ impact?: string;
8
+ example?: string;
9
+ learnMore?: string;
10
+ }
11
+ export interface SeoCheckResult {
12
+ name: string;
13
+ status: SeoStatus;
14
+ message: string;
15
+ value?: string | number;
16
+ recommendation?: string;
17
+ evidence?: SeoCheckEvidence;
18
+ }
19
+ export interface HeadingInfo {
20
+ level: number;
21
+ text: string;
22
+ count: number;
23
+ }
24
+ export interface HeadingAnalysis {
25
+ structure: HeadingInfo[];
26
+ h1Count: number;
27
+ hasProperHierarchy: boolean;
28
+ issues: string[];
29
+ }
30
+ export interface ContentMetrics {
31
+ wordCount: number;
32
+ characterCount: number;
33
+ sentenceCount: number;
34
+ paragraphCount: number;
35
+ readingTimeMinutes: number;
36
+ avgWordsPerSentence: number;
37
+ avgParagraphLength: number;
38
+ listCount: number;
39
+ strongTagCount: number;
40
+ emTagCount: number;
41
+ }
42
+ export interface LinkAnalysis {
43
+ total: number;
44
+ internal: number;
45
+ external: number;
46
+ nofollow: number;
47
+ broken: number;
48
+ withoutText: number;
49
+ }
50
+ export interface ImageAnalysis {
51
+ total: number;
52
+ withAlt: number;
53
+ withoutAlt: number;
54
+ lazy: number;
55
+ missingDimensions: number;
56
+ modernFormats: number;
57
+ altTextLengths: number[];
58
+ imageFilenames: string[];
59
+ imagesWithAsyncDecoding: number;
60
+ }
61
+ export interface SocialMetaAnalysis {
62
+ openGraph: {
63
+ present: boolean;
64
+ hasTitle: boolean;
65
+ hasDescription: boolean;
66
+ hasImage: boolean;
67
+ hasUrl: boolean;
68
+ issues: string[];
69
+ };
70
+ twitterCard: {
71
+ present: boolean;
72
+ hasCard: boolean;
73
+ hasTitle: boolean;
74
+ hasDescription: boolean;
75
+ hasImage: boolean;
76
+ issues: string[];
77
+ };
78
+ }
79
+ export interface TechnicalSeo {
80
+ hasCanonical: boolean;
81
+ canonicalUrl?: string;
82
+ hasRobotsMeta: boolean;
83
+ robotsContent?: string[];
84
+ hasViewport: boolean;
85
+ hasCharset: boolean;
86
+ hasLang: boolean;
87
+ langValue?: string;
88
+ }
89
+ export interface SeoReport {
90
+ url: string;
91
+ timestamp: Date;
92
+ grade: string;
93
+ score: number;
94
+ checks: SeoCheckResult[];
95
+ title?: {
96
+ text: string;
97
+ length: number;
98
+ };
99
+ metaDescription?: {
100
+ text: string;
101
+ length: number;
102
+ };
103
+ headings: HeadingAnalysis;
104
+ content: ContentMetrics;
105
+ links: LinkAnalysis;
106
+ images: ImageAnalysis;
107
+ social: SocialMetaAnalysis;
108
+ technical: TechnicalSeo;
109
+ jsonLd: {
110
+ count: number;
111
+ types: string[];
112
+ };
113
+ }
114
+ export interface SeoAnalyzerOptions {
115
+ baseUrl?: string;
116
+ analyzeContent?: boolean;
117
+ checkBrokenLinks?: boolean;
118
+ responseHeaders?: Record<string, string | string[]>;
119
+ }
120
+ export interface ExtractedLink {
121
+ href: string;
122
+ text: string;
123
+ rel?: string;
124
+ target?: string;
125
+ title?: string;
126
+ type?: 'internal' | 'external' | 'anchor' | 'mailto' | 'tel';
127
+ }
128
+ export interface ExtractedImage {
129
+ src: string;
130
+ alt?: string;
131
+ title?: string;
132
+ width?: number;
133
+ height?: number;
134
+ srcset?: string;
135
+ loading?: 'lazy' | 'eager';
136
+ }
137
+ export interface LinkAnalysis {
138
+ total: number;
139
+ internal: number;
140
+ external: number;
141
+ nofollow: number;
142
+ broken: number;
143
+ withoutText: number;
144
+ sponsoredLinks: number;
145
+ ugcLinks: number;
146
+ }
147
+ export interface ContentMetrics {
148
+ wordCount: number;
149
+ characterCount: number;
150
+ sentenceCount: number;
151
+ paragraphCount: number;
152
+ readingTimeMinutes: number;
153
+ avgWordsPerSentence: number;
154
+ avgParagraphLength: number;
155
+ listCount: number;
156
+ strongTagCount: number;
157
+ emTagCount: number;
158
+ fleschReadingEase?: number;
159
+ hasQuestionHeadings?: boolean;
160
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ export interface ColumnOptions {
2
+ minWidth?: number;
3
+ maxColumns?: number;
4
+ padding?: number;
5
+ prefix?: string;
6
+ indent?: number;
7
+ transform?: (item: string) => string;
8
+ }
9
+ export declare function getTerminalWidth(): number;
10
+ export declare function formatColumns(items: string[], options?: ColumnOptions): string;
11
+ export declare function formatColumnsRowMajor(items: string[], options?: ColumnOptions): string;
12
+ export declare function formatGroupedColumns(groups: Record<string, string[]>, options?: ColumnOptions & {
13
+ groupPrefix?: string;
14
+ }): string;
@@ -0,0 +1,69 @@
1
+ export function getTerminalWidth() {
2
+ return process.stdout.columns || 80;
3
+ }
4
+ export function formatColumns(items, options = {}) {
5
+ const { minWidth = 15, maxColumns = Infinity, padding = 2, prefix = '', indent = 2, transform, } = options;
6
+ if (items.length === 0)
7
+ return '';
8
+ const prefixedItems = items.map(item => prefix + item);
9
+ const maxItemWidth = Math.max(...prefixedItems.map(item => item.length));
10
+ const columnWidth = Math.max(minWidth, maxItemWidth + padding);
11
+ const terminalWidth = getTerminalWidth();
12
+ const availableWidth = terminalWidth - indent;
13
+ const calculatedColumns = Math.floor(availableWidth / columnWidth);
14
+ const numColumns = Math.min(Math.max(1, calculatedColumns), maxColumns, items.length);
15
+ const actualColumnWidth = Math.floor(availableWidth / numColumns);
16
+ const numRows = Math.ceil(prefixedItems.length / numColumns);
17
+ const lines = [];
18
+ const indentStr = ' '.repeat(indent);
19
+ for (let row = 0; row < numRows; row++) {
20
+ let line = indentStr;
21
+ for (let col = 0; col < numColumns; col++) {
22
+ const index = col * numRows + row;
23
+ if (index < prefixedItems.length) {
24
+ const rawItem = prefixedItems[index];
25
+ const displayItem = transform ? transform(rawItem) : rawItem;
26
+ const paddingNeeded = actualColumnWidth - rawItem.length;
27
+ line += displayItem + ' '.repeat(Math.max(0, paddingNeeded));
28
+ }
29
+ }
30
+ lines.push(line.trimEnd());
31
+ }
32
+ return lines.join('\n');
33
+ }
34
+ export function formatColumnsRowMajor(items, options = {}) {
35
+ const { minWidth = 15, maxColumns = Infinity, padding = 2, prefix = '', indent = 2, } = options;
36
+ if (items.length === 0)
37
+ return '';
38
+ const prefixedItems = items.map(item => prefix + item);
39
+ const maxItemWidth = Math.max(...prefixedItems.map(item => item.length));
40
+ const columnWidth = Math.max(minWidth, maxItemWidth + padding);
41
+ const terminalWidth = getTerminalWidth();
42
+ const availableWidth = terminalWidth - indent;
43
+ const calculatedColumns = Math.floor(availableWidth / columnWidth);
44
+ const numColumns = Math.min(Math.max(1, calculatedColumns), maxColumns, items.length);
45
+ const actualColumnWidth = Math.floor(availableWidth / numColumns);
46
+ const lines = [];
47
+ const indentStr = ' '.repeat(indent);
48
+ for (let i = 0; i < prefixedItems.length; i += numColumns) {
49
+ let line = indentStr;
50
+ for (let col = 0; col < numColumns && i + col < prefixedItems.length; col++) {
51
+ const item = prefixedItems[i + col];
52
+ line += item.padEnd(actualColumnWidth);
53
+ }
54
+ lines.push(line.trimEnd());
55
+ }
56
+ return lines.join('\n');
57
+ }
58
+ export function formatGroupedColumns(groups, options = {}) {
59
+ const lines = [];
60
+ const { groupPrefix = '', ...columnOptions } = options;
61
+ for (const [groupName, items] of Object.entries(groups)) {
62
+ if (items.length === 0)
63
+ continue;
64
+ lines.push(groupPrefix + groupName + ':');
65
+ lines.push(formatColumns(items, columnOptions));
66
+ lines.push('');
67
+ }
68
+ return lines.join('\n').trimEnd();
69
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "recker",
3
- "version": "1.0.27",
3
+ "version": "1.0.28-next.9eb3868",
4
4
  "description": "AI & DevX focused HTTP client for Node.js 18+",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",