@springfield/ham-radio-registry 1.0.1

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,167 @@
1
+ import type { ILogLayer } from 'loglayer';
2
+ import type { NpmPluginInfo } from '../types/plugin-module.js';
3
+
4
+ /**
5
+ * NPM registry client for plugin discovery and validation
6
+ */
7
+ export interface NpmClient {
8
+ // Get package information from npm registry
9
+ getPackageInfo(packageName: string): Promise<any>;
10
+
11
+ // Search for packages with query
12
+ searchPackages(query: string): Promise<NpmPluginInfo[]>;
13
+
14
+ // Get package download statistics
15
+ getPackageStats(packageName: string): Promise<any>;
16
+
17
+ // Validate package exists and is accessible
18
+ validatePackage(packageName: string): Promise<boolean>;
19
+ }
20
+
21
+ /**
22
+ * NPM search response interface
23
+ */
24
+ interface NpmSearchResponse {
25
+ objects: Array<{
26
+ package: {
27
+ name: string;
28
+ version: string;
29
+ description?: string;
30
+ author?: any;
31
+ license?: string;
32
+ repository?: { url: string };
33
+ homepage?: string;
34
+ keywords?: string[];
35
+ date?: string;
36
+ springfield?: any;
37
+ };
38
+ }>;
39
+ }
40
+
41
+ /**
42
+ * Default implementation of NPM client using npm registry API
43
+ */
44
+ export class DefaultNpmClient implements NpmClient {
45
+ private logger: ILogLayer;
46
+ private baseUrl = 'https://registry.npmjs.org';
47
+
48
+ constructor(logger: ILogLayer) {
49
+ this.logger = logger;
50
+ }
51
+
52
+ async getPackageInfo(packageName: string): Promise<any> {
53
+ try {
54
+ const response = await fetch(`${this.baseUrl}/${packageName}`);
55
+
56
+ if (!response.ok) {
57
+ if (response.status === 404) {
58
+ throw new Error(`Package not found: ${packageName}`);
59
+ }
60
+ throw new Error(`Failed to fetch package info: ${response.statusText}`);
61
+ }
62
+
63
+ const packageInfo = await response.json();
64
+
65
+ this.logger.withMetadata({ packageName }).info('Retrieved package info');
66
+ return packageInfo;
67
+ } catch (error) {
68
+ this.logger.withError(error).error('Failed to get package info');
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ async searchPackages(query: string): Promise<NpmPluginInfo[]> {
74
+ try {
75
+ const searchQuery = encodeURIComponent(query);
76
+ const response = await fetch(`${this.baseUrl}/-/v1/search?text=${searchQuery}&size=50`);
77
+
78
+ if (!response.ok) {
79
+ throw new Error(`Failed to search packages: ${response.statusText}`);
80
+ }
81
+
82
+ const searchResults = (await response.json()) as NpmSearchResponse;
83
+ const packages = searchResults.objects || [];
84
+
85
+ // Filter and transform results
86
+ const pluginPackages: NpmPluginInfo[] = [];
87
+
88
+ for (const pkg of packages) {
89
+ const packageInfo = pkg.package;
90
+
91
+ // Check if this is a radio module
92
+ if (this.isRadioModule(packageInfo)) {
93
+ pluginPackages.push(this.transformToNpmPluginInfo(packageInfo));
94
+ }
95
+ }
96
+
97
+ this.logger.withMetadata({ query, results: pluginPackages.length }).info('Searched packages');
98
+ return pluginPackages;
99
+ } catch (error) {
100
+ this.logger.withError(error).error('Failed to search packages');
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ async getPackageStats(packageName: string): Promise<any> {
106
+ try {
107
+ // Note: npm registry doesn't provide download stats via public API
108
+ // This would require npmjs.com API or alternative data source
109
+ this.logger.withMetadata({ packageName }).warn('Package stats not available via public npm registry API');
110
+ return { downloads: 0, lastUpdated: new Date().toISOString() };
111
+ } catch (error) {
112
+ this.logger.withError(error).error('Failed to get package stats');
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ async validatePackage(packageName: string): Promise<boolean> {
118
+ try {
119
+ await this.getPackageInfo(packageName);
120
+ return true;
121
+ } catch (error) {
122
+ if (error instanceof Error && error.message.includes('not found')) {
123
+ return false;
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+
129
+ private isRadioModule(packageInfo: any): boolean {
130
+ const name = packageInfo.name || '';
131
+
132
+ // Check naming convention
133
+ const isNamedCorrectly = name.includes('radio-module') || name.startsWith('@springfield/radio-module-');
134
+
135
+ // Check springfield plugin field
136
+ const hasSpringfieldField = packageInfo.springfield?.pluginType === 'radio-module';
137
+
138
+ // Check keywords
139
+ const hasKeywords = packageInfo.keywords?.includes('radio-module');
140
+
141
+ return isNamedCorrectly || hasSpringfieldField || hasKeywords;
142
+ }
143
+
144
+ private transformToNpmPluginInfo(packageInfo: any): NpmPluginInfo {
145
+ return {
146
+ name: packageInfo.name,
147
+ version: packageInfo.version,
148
+ description: packageInfo.description || '',
149
+ author: this.extractAuthor(packageInfo.author),
150
+ license: packageInfo.license || 'Unknown',
151
+ repository: packageInfo.repository?.url || packageInfo.homepage,
152
+ downloads: 0, // Not available via public API
153
+ lastUpdated: packageInfo.date || new Date().toISOString(),
154
+ springfield: packageInfo.springfield,
155
+ };
156
+ }
157
+
158
+ private extractAuthor(author: any): string {
159
+ if (typeof author === 'string') {
160
+ return author;
161
+ }
162
+ if (typeof author === 'object' && author.name) {
163
+ return author.name;
164
+ }
165
+ return 'Unknown';
166
+ }
167
+ }