@bsb/base 9.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.
Files changed (182) hide show
  1. package/LICENSE +665 -0
  2. package/LICENSE.commercial +32 -0
  3. package/README.md +263 -0
  4. package/bsb-plugin.json +62 -0
  5. package/lib/base/BSBConfig.d.ts +130 -0
  6. package/lib/base/BSBConfig.js +95 -0
  7. package/lib/base/BSBConfig.js.map +1 -0
  8. package/lib/base/BSBEvents.d.ts +207 -0
  9. package/lib/base/BSBEvents.js +101 -0
  10. package/lib/base/BSBEvents.js.map +1 -0
  11. package/lib/base/BSBObservable.d.ts +178 -0
  12. package/lib/base/BSBObservable.js +91 -0
  13. package/lib/base/BSBObservable.js.map +1 -0
  14. package/lib/base/BSBService.d.ts +277 -0
  15. package/lib/base/BSBService.js +366 -0
  16. package/lib/base/BSBService.js.map +1 -0
  17. package/lib/base/BSBServiceClient.d.ts +135 -0
  18. package/lib/base/BSBServiceClient.js +130 -0
  19. package/lib/base/BSBServiceClient.js.map +1 -0
  20. package/lib/base/EventValidator.d.ts +137 -0
  21. package/lib/base/EventValidator.js +210 -0
  22. package/lib/base/EventValidator.js.map +1 -0
  23. package/lib/base/ObservableBackend.d.ts +281 -0
  24. package/lib/base/ObservableBackend.js +515 -0
  25. package/lib/base/ObservableBackend.js.map +1 -0
  26. package/lib/base/PluginConfig.d.ts +196 -0
  27. package/lib/base/PluginConfig.js +96 -0
  28. package/lib/base/PluginConfig.js.map +1 -0
  29. package/lib/base/PluginEvents.d.ts +140 -0
  30. package/lib/base/PluginEvents.js +268 -0
  31. package/lib/base/PluginEvents.js.map +1 -0
  32. package/lib/base/PluginObservable.d.ts +196 -0
  33. package/lib/base/PluginObservable.js +250 -0
  34. package/lib/base/PluginObservable.js.map +1 -0
  35. package/lib/base/ResourceContext.d.ts +70 -0
  36. package/lib/base/ResourceContext.js +54 -0
  37. package/lib/base/ResourceContext.js.map +1 -0
  38. package/lib/base/base.d.ts +264 -0
  39. package/lib/base/base.js +182 -0
  40. package/lib/base/base.js.map +1 -0
  41. package/lib/base/errorMessages.d.ts +56 -0
  42. package/lib/base/errorMessages.js +70 -0
  43. package/lib/base/errorMessages.js.map +1 -0
  44. package/lib/base/factory.d.ts +58 -0
  45. package/lib/base/factory.js +167 -0
  46. package/lib/base/factory.js.map +1 -0
  47. package/lib/base/functions.d.ts +117 -0
  48. package/lib/base/functions.js +152 -0
  49. package/lib/base/functions.js.map +1 -0
  50. package/lib/base/index.d.ts +44 -0
  51. package/lib/base/index.js +64 -0
  52. package/lib/base/index.js.map +1 -0
  53. package/lib/base/logFormatter.d.ts +50 -0
  54. package/lib/base/logFormatter.js +105 -0
  55. package/lib/base/logFormatter.js.map +1 -0
  56. package/lib/base/tools.d.ts +316 -0
  57. package/lib/base/tools.js +666 -0
  58. package/lib/base/tools.js.map +1 -0
  59. package/lib/cli.d.ts +28 -0
  60. package/lib/cli.js +254 -0
  61. package/lib/cli.js.map +1 -0
  62. package/lib/dev.d.ts +27 -0
  63. package/lib/dev.js +200 -0
  64. package/lib/dev.js.map +1 -0
  65. package/lib/index.d.ts +32 -0
  66. package/lib/index.js +49 -0
  67. package/lib/index.js.map +1 -0
  68. package/lib/interfaces/events.d.ts +67 -0
  69. package/lib/interfaces/events.js +44 -0
  70. package/lib/interfaces/events.js.map +1 -0
  71. package/lib/interfaces/index.d.ts +38 -0
  72. package/lib/interfaces/index.js +59 -0
  73. package/lib/interfaces/index.js.map +1 -0
  74. package/lib/interfaces/logging.d.ts +106 -0
  75. package/lib/interfaces/logging.js +39 -0
  76. package/lib/interfaces/logging.js.map +1 -0
  77. package/lib/interfaces/metrics.d.ts +365 -0
  78. package/lib/interfaces/metrics.js +46 -0
  79. package/lib/interfaces/metrics.js.map +1 -0
  80. package/lib/interfaces/observable-types.d.ts +63 -0
  81. package/lib/interfaces/observable-types.js +49 -0
  82. package/lib/interfaces/observable-types.js.map +1 -0
  83. package/lib/interfaces/observable.d.ts +297 -0
  84. package/lib/interfaces/observable.js +29 -0
  85. package/lib/interfaces/observable.js.map +1 -0
  86. package/lib/interfaces/options.d.ts +164 -0
  87. package/lib/interfaces/options.js +56 -0
  88. package/lib/interfaces/options.js.map +1 -0
  89. package/lib/interfaces/plugins.d.ts +143 -0
  90. package/lib/interfaces/plugins.js +45 -0
  91. package/lib/interfaces/plugins.js.map +1 -0
  92. package/lib/interfaces/result.d.ts +129 -0
  93. package/lib/interfaces/result.js +162 -0
  94. package/lib/interfaces/result.js.map +1 -0
  95. package/lib/interfaces/schema-events.d.ts +378 -0
  96. package/lib/interfaces/schema-events.js +247 -0
  97. package/lib/interfaces/schema-events.js.map +1 -0
  98. package/lib/interfaces/schema-types.d.ts +407 -0
  99. package/lib/interfaces/schema-types.js +581 -0
  100. package/lib/interfaces/schema-types.js.map +1 -0
  101. package/lib/interfaces/service.d.ts +48 -0
  102. package/lib/interfaces/service.js +29 -0
  103. package/lib/interfaces/service.js.map +1 -0
  104. package/lib/interfaces/tools.d.ts +65 -0
  105. package/lib/interfaces/tools.js +50 -0
  106. package/lib/interfaces/tools.js.map +1 -0
  107. package/lib/plugins/config-default/index.d.ts +59 -0
  108. package/lib/plugins/config-default/index.js +197 -0
  109. package/lib/plugins/config-default/index.js.map +1 -0
  110. package/lib/plugins/config-default/interfaces.d.ts +92 -0
  111. package/lib/plugins/config-default/interfaces.js +36 -0
  112. package/lib/plugins/config-default/interfaces.js.map +1 -0
  113. package/lib/plugins/events-default/events/broadcast.d.ts +36 -0
  114. package/lib/plugins/events-default/events/broadcast.js +85 -0
  115. package/lib/plugins/events-default/events/broadcast.js.map +1 -0
  116. package/lib/plugins/events-default/events/emit.d.ts +38 -0
  117. package/lib/plugins/events-default/events/emit.js +104 -0
  118. package/lib/plugins/events-default/events/emit.js.map +1 -0
  119. package/lib/plugins/events-default/events/emitAndReturn.d.ts +36 -0
  120. package/lib/plugins/events-default/events/emitAndReturn.js +100 -0
  121. package/lib/plugins/events-default/events/emitAndReturn.js.map +1 -0
  122. package/lib/plugins/events-default/events/emitStreamAndReceiveStream.d.ts +38 -0
  123. package/lib/plugins/events-default/events/emitStreamAndReceiveStream.js +134 -0
  124. package/lib/plugins/events-default/events/emitStreamAndReceiveStream.js.map +1 -0
  125. package/lib/plugins/events-default/events/index.d.ts +30 -0
  126. package/lib/plugins/events-default/events/index.js +38 -0
  127. package/lib/plugins/events-default/events/index.js.map +1 -0
  128. package/lib/plugins/events-default/index.d.ts +57 -0
  129. package/lib/plugins/events-default/index.js +86 -0
  130. package/lib/plugins/events-default/index.js.map +1 -0
  131. package/lib/plugins/observable-default/index.d.ts +43 -0
  132. package/lib/plugins/observable-default/index.js +151 -0
  133. package/lib/plugins/observable-default/index.js.map +1 -0
  134. package/lib/schemas/config-default.json +34 -0
  135. package/lib/schemas/config-default.plugin.json +36 -0
  136. package/lib/schemas/events-default.json +18 -0
  137. package/lib/schemas/events-default.plugin.json +17 -0
  138. package/lib/schemas/observable-default.json +33 -0
  139. package/lib/schemas/observable-default.plugin.json +24 -0
  140. package/lib/scripts/bsb-client-cli.d.ts +21 -0
  141. package/lib/scripts/bsb-client-cli.js +701 -0
  142. package/lib/scripts/bsb-client-cli.js.map +1 -0
  143. package/lib/scripts/bsb-plugin-cli.d.ts +15 -0
  144. package/lib/scripts/bsb-plugin-cli.js +547 -0
  145. package/lib/scripts/bsb-plugin-cli.js.map +1 -0
  146. package/lib/scripts/export-schemas.d.ts +17 -0
  147. package/lib/scripts/export-schemas.js +205 -0
  148. package/lib/scripts/export-schemas.js.map +1 -0
  149. package/lib/scripts/extract-schemas-from-source.d.ts +23 -0
  150. package/lib/scripts/extract-schemas-from-source.js +604 -0
  151. package/lib/scripts/extract-schemas-from-source.js.map +1 -0
  152. package/lib/scripts/generate-client-types.d.ts +22 -0
  153. package/lib/scripts/generate-client-types.js +537 -0
  154. package/lib/scripts/generate-client-types.js.map +1 -0
  155. package/lib/scripts/generate-plugin-json.d.ts +17 -0
  156. package/lib/scripts/generate-plugin-json.js +219 -0
  157. package/lib/scripts/generate-plugin-json.js.map +1 -0
  158. package/lib/serviceBase/config.d.ts +83 -0
  159. package/lib/serviceBase/config.js +236 -0
  160. package/lib/serviceBase/config.js.map +1 -0
  161. package/lib/serviceBase/events.d.ts +91 -0
  162. package/lib/serviceBase/events.js +519 -0
  163. package/lib/serviceBase/events.js.map +1 -0
  164. package/lib/serviceBase/index.d.ts +33 -0
  165. package/lib/serviceBase/index.js +50 -0
  166. package/lib/serviceBase/index.js.map +1 -0
  167. package/lib/serviceBase/observable.d.ts +249 -0
  168. package/lib/serviceBase/observable.js +551 -0
  169. package/lib/serviceBase/observable.js.map +1 -0
  170. package/lib/serviceBase/plugins.d.ts +48 -0
  171. package/lib/serviceBase/plugins.js +184 -0
  172. package/lib/serviceBase/plugins.js.map +1 -0
  173. package/lib/serviceBase/serviceBase.d.ts +228 -0
  174. package/lib/serviceBase/serviceBase.js +420 -0
  175. package/lib/serviceBase/serviceBase.js.map +1 -0
  176. package/lib/serviceBase/services.d.ts +63 -0
  177. package/lib/serviceBase/services.js +346 -0
  178. package/lib/serviceBase/services.js.map +1 -0
  179. package/lib/tests.d.ts +27 -0
  180. package/lib/tests.js +44 -0
  181. package/lib/tests.js.map +1 -0
  182. package/package.json +91 -0
@@ -0,0 +1,701 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * BSB Client CLI
5
+ *
6
+ * Commands for interacting with BSB Registry.
7
+ * Provides plugin publishing, searching, and discovery.
8
+ *
9
+ * Usage:
10
+ * bsb-client list - List all plugins from registry
11
+ * bsb-client search <query> - Search plugins
12
+ * bsb-client publish - Publish current plugin(s) to registry
13
+ * bsb-client schema <name> - Get plugin event schema
14
+ * bsb-client info <name> - Get plugin details
15
+ * bsb-client install <name> - Download schema and generate types
16
+ * bsb-client token generate - Generate a new API token
17
+ *
18
+ * Environment:
19
+ * BSB_REGISTRY_URL - Registry URL (default: https://registry.bsbcode.dev)
20
+ * BSB_REGISTRY_TOKEN - API token for authentication
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ const fs = require("fs");
24
+ const path = require("path");
25
+ const https = require("https");
26
+ const http = require("http");
27
+ // Colors for terminal output
28
+ const colors = {
29
+ reset: '\x1b[0m',
30
+ bright: '\x1b[1m',
31
+ red: '\x1b[31m',
32
+ green: '\x1b[32m',
33
+ yellow: '\x1b[33m',
34
+ blue: '\x1b[34m',
35
+ cyan: '\x1b[36m',
36
+ };
37
+ function log(message, color = 'reset') {
38
+ console.log(`${colors[color]}${message}${colors.reset}`);
39
+ }
40
+ function error(message) {
41
+ log(`ERROR: ${message}`, 'red');
42
+ process.exit(1);
43
+ }
44
+ function success(message) {
45
+ log(`[OK] ${message}`, 'green');
46
+ }
47
+ function info(message) {
48
+ log(`→ ${message}`, 'cyan');
49
+ }
50
+ function warn(message) {
51
+ log(`[WARN] ${message}`, 'yellow');
52
+ }
53
+ // Get registry URL from env or use default
54
+ const REGISTRY_URL = process.env.BSB_REGISTRY_URL || 'https://io.bsbcode.dev';
55
+ const REGISTRY_TOKEN = process.env.BSB_REGISTRY_TOKEN;
56
+ const VALID_CATEGORIES = new Set(['service', 'observable', 'events', 'config']);
57
+ const COMMAND = process.argv[2];
58
+ const ARGS = process.argv.slice(3);
59
+ /**
60
+ * Parse a plugin ID into org and name.
61
+ * Accepts both "org/name" and plain "name" formats.
62
+ * When no org is provided, defaults to "_" (unaffiliated).
63
+ */
64
+ function parsePluginId(pluginId) {
65
+ if (pluginId.includes('/')) {
66
+ const [org, ...rest] = pluginId.split('/');
67
+ return { org, name: rest.join('/') };
68
+ }
69
+ return { org: '_', name: pluginId };
70
+ }
71
+ /**
72
+ * Format a plugin ID for display.
73
+ * Hides the "_" sentinel org for unaffiliated plugins.
74
+ */
75
+ function displayPluginId(org, name) {
76
+ return org === '_' ? name : `${org}/${name}`;
77
+ }
78
+ function normalizeIgnoredPluginId(raw, org) {
79
+ const value = raw.trim();
80
+ if (value.startsWith(`${org}/`)) {
81
+ return value.substring(org.length + 1);
82
+ }
83
+ if (value.startsWith('_/')) {
84
+ return value.substring(2);
85
+ }
86
+ return value;
87
+ }
88
+ function isPng(buffer) {
89
+ if (buffer.length < 8)
90
+ return false;
91
+ const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
92
+ return signature.every((byte, idx) => buffer[idx] === byte);
93
+ }
94
+ function resolveCategory(pluginMeta) {
95
+ const raw = (pluginMeta.category || pluginMeta.id.split('-')[0] || '').toLowerCase();
96
+ if (!VALID_CATEGORIES.has(raw)) {
97
+ throw new Error(`Invalid category "${raw}" for plugin "${pluginMeta.id}". Valid categories: service, observable, events, config.`);
98
+ }
99
+ return raw;
100
+ }
101
+ function resolveImagePath(pluginMeta) {
102
+ if (!pluginMeta.image)
103
+ return null;
104
+ const basePath = pluginMeta.basePath || '.';
105
+ return path.resolve(process.cwd(), basePath, pluginMeta.image);
106
+ }
107
+ function formatRegistryError(parsed, statusCode, raw) {
108
+ if (!parsed || typeof parsed !== 'object') {
109
+ return raw && raw.trim().length > 0
110
+ ? raw
111
+ : `HTTP ${statusCode ?? 'error'}`;
112
+ }
113
+ const base = parsed.error || `HTTP ${statusCode ?? 'error'}`;
114
+ const code = parsed.code ? ` [${parsed.code}]` : '';
115
+ if (Array.isArray(parsed.details) && parsed.details.length > 0) {
116
+ const detailText = parsed.details
117
+ .map((detail) => {
118
+ const path = detail?.path ? String(detail.path) : '<root>';
119
+ const message = detail?.message ? String(detail.message) : 'Invalid value';
120
+ return `${path}: ${message}`;
121
+ })
122
+ .join('; ');
123
+ return `${base}${code} - ${detailText}`;
124
+ }
125
+ if (parsed.message && typeof parsed.message === 'string' && parsed.message.trim().length > 0) {
126
+ return `${base}${code} - ${parsed.message}`;
127
+ }
128
+ return `${base}${code}`;
129
+ }
130
+ /**
131
+ * Make HTTP request to registry
132
+ */
133
+ async function registryRequest(method, path, body, requireAuth = false) {
134
+ return new Promise((resolve, reject) => {
135
+ const url = new URL(path, REGISTRY_URL);
136
+ const isHttps = url.protocol === 'https:';
137
+ const lib = isHttps ? https : http;
138
+ const options = {
139
+ hostname: url.hostname,
140
+ port: url.port || (isHttps ? 443 : 80),
141
+ path: url.pathname + url.search,
142
+ method,
143
+ headers: {
144
+ 'Content-Type': 'application/json',
145
+ 'Accept': 'application/json',
146
+ ...(requireAuth && REGISTRY_TOKEN ? { Authorization: `Bearer ${REGISTRY_TOKEN}` } : {}),
147
+ },
148
+ };
149
+ const req = lib.request(options, (res) => {
150
+ let data = '';
151
+ res.on('data', (chunk) => {
152
+ data += chunk;
153
+ });
154
+ res.on('end', () => {
155
+ try {
156
+ const parsed = JSON.parse(data);
157
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
158
+ resolve(parsed);
159
+ }
160
+ else {
161
+ reject(new Error(formatRegistryError(parsed, res.statusCode, data)));
162
+ }
163
+ }
164
+ catch (err) {
165
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
166
+ resolve(data);
167
+ }
168
+ else {
169
+ reject(new Error(formatRegistryError(undefined, res.statusCode, data)));
170
+ }
171
+ }
172
+ });
173
+ });
174
+ req.on('error', (err) => {
175
+ reject(err);
176
+ });
177
+ if (body) {
178
+ req.write(JSON.stringify(body));
179
+ }
180
+ req.end();
181
+ });
182
+ }
183
+ async function uploadPluginImage(org, pluginName, imagePath) {
184
+ if (!REGISTRY_TOKEN) {
185
+ throw new Error('BSB_REGISTRY_TOKEN environment variable not set');
186
+ }
187
+ if (!imagePath.toLowerCase().endsWith('.png')) {
188
+ throw new Error(`Only PNG images are supported for upload: ${imagePath}`);
189
+ }
190
+ if (!fs.existsSync(imagePath)) {
191
+ throw new Error(`Image file not found: ${imagePath}`);
192
+ }
193
+ const imageBuffer = fs.readFileSync(imagePath);
194
+ if (!isPng(imageBuffer)) {
195
+ throw new Error(`Image is not a valid PNG file: ${imagePath}`);
196
+ }
197
+ const boundary = `----bsb-boundary-${Date.now().toString(16)}`;
198
+ const fileName = path.basename(imagePath);
199
+ const multipartHeader = `--${boundary}\r\n` +
200
+ `Content-Disposition: form-data; name="image"; filename="${fileName}"\r\n` +
201
+ `Content-Type: image/png\r\n\r\n`;
202
+ const multipartFooter = `\r\n--${boundary}--\r\n`;
203
+ const body = Buffer.concat([
204
+ Buffer.from(multipartHeader, 'utf-8'),
205
+ imageBuffer,
206
+ Buffer.from(multipartFooter, 'utf-8'),
207
+ ]);
208
+ return new Promise((resolve, reject) => {
209
+ const uploadPath = `/plugins/${encodeURIComponent(org)}/${encodeURIComponent(pluginName)}/image`;
210
+ const url = new URL(uploadPath, REGISTRY_URL);
211
+ const isHttps = url.protocol === 'https:';
212
+ const lib = isHttps ? https : http;
213
+ const req = lib.request({
214
+ hostname: url.hostname,
215
+ port: url.port || (isHttps ? 443 : 80),
216
+ path: url.pathname + url.search,
217
+ method: 'POST',
218
+ headers: {
219
+ 'Accept': 'application/json',
220
+ 'Authorization': `Bearer ${REGISTRY_TOKEN}`,
221
+ 'Content-Type': `multipart/form-data; boundary=${boundary}`,
222
+ 'Content-Length': body.length,
223
+ },
224
+ }, (res) => {
225
+ let data = '';
226
+ res.on('data', (chunk) => { data += chunk; });
227
+ res.on('end', () => {
228
+ let parsed = {};
229
+ try {
230
+ parsed = data ? JSON.parse(data) : {};
231
+ }
232
+ catch {
233
+ // Keep raw payload fallback for error messages
234
+ }
235
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
236
+ resolve(parsed);
237
+ }
238
+ else {
239
+ reject(new Error(parsed.error || data || `HTTP ${res.statusCode}`));
240
+ }
241
+ });
242
+ });
243
+ req.on('error', reject);
244
+ req.write(body);
245
+ req.end();
246
+ });
247
+ }
248
+ /**
249
+ * List plugins from registry
250
+ */
251
+ async function listPlugins() {
252
+ info('Fetching plugins from registry...');
253
+ try {
254
+ const result = await registryRequest('GET', '/plugins?limit=100');
255
+ if (result.results.length === 0) {
256
+ warn('No plugins found in registry');
257
+ return;
258
+ }
259
+ log(`\nFound ${result.total} plugins:\n`, 'bright');
260
+ result.results.forEach((plugin) => {
261
+ log(` ${plugin.id} @ ${plugin.version}`, 'cyan');
262
+ log(` ${plugin.description}`, 'reset');
263
+ log(` Language: ${plugin.language} | Category: ${plugin.category}`, 'reset');
264
+ log('');
265
+ });
266
+ success(`Listed ${result.results.length} plugins`);
267
+ }
268
+ catch (err) {
269
+ error(`Failed to list plugins: ${err.message}`);
270
+ }
271
+ }
272
+ /**
273
+ * Search plugins
274
+ */
275
+ async function searchPlugins(query) {
276
+ if (!query) {
277
+ error('Please provide a search query: bsb-client search <query>');
278
+ }
279
+ info(`Searching for "${query}"...`);
280
+ try {
281
+ const result = await registryRequest('GET', `/plugins?query=${encodeURIComponent(query)}&limit=100`);
282
+ if (result.results.length === 0) {
283
+ warn(`No plugins found matching "${query}"`);
284
+ return;
285
+ }
286
+ log(`\nFound ${result.total} matches:\n`, 'bright');
287
+ result.results.forEach((plugin) => {
288
+ log(` ${plugin.id} @ ${plugin.version}`, 'cyan');
289
+ log(` ${plugin.description}`, 'reset');
290
+ log(` Language: ${plugin.language} | Category: ${plugin.category}`, 'reset');
291
+ log('');
292
+ });
293
+ success(`Found ${result.results.length} matches`);
294
+ }
295
+ catch (err) {
296
+ error(`Failed to search plugins: ${err.message}`);
297
+ }
298
+ }
299
+ /**
300
+ * Get plugin info
301
+ */
302
+ async function getPluginInfo(pluginId) {
303
+ if (!pluginId) {
304
+ error('Please provide a plugin ID: bsb-client info <name> or bsb-client info <org/name>');
305
+ }
306
+ const { org, name } = parsePluginId(pluginId);
307
+ const display = displayPluginId(org, name);
308
+ info(`Fetching plugin info for ${display}...`);
309
+ try {
310
+ const result = await registryRequest('GET', `/plugins/${org}/${name}`);
311
+ const plugin = result.plugin || result;
312
+ log(`\nPlugin: ${plugin.displayName}\n`, 'bright');
313
+ log(` ID: ${plugin.id}`, 'reset');
314
+ log(` Version: ${plugin.version}`, 'reset');
315
+ log(` Language: ${plugin.language}`, 'reset');
316
+ log(` Category: ${plugin.category}`, 'reset');
317
+ log(` Description: ${plugin.description}`, 'reset');
318
+ log(` Author: ${plugin.author || 'N/A'}`, 'reset');
319
+ log(` License: ${plugin.license || 'N/A'}`, 'reset');
320
+ log(` Homepage: ${plugin.homepage || 'N/A'}`, 'reset');
321
+ log(` Repository: ${plugin.repository || 'N/A'}`, 'reset');
322
+ log(` Events: ${plugin.eventCount} total`, 'reset');
323
+ log(` Downloads: ${plugin.downloads || 0}`, 'reset');
324
+ log('');
325
+ success('Plugin info retrieved');
326
+ }
327
+ catch (err) {
328
+ error(`Failed to get plugin info: ${err.message}`);
329
+ }
330
+ }
331
+ /**
332
+ * Get plugin schema
333
+ */
334
+ async function getPluginSchema(pluginId) {
335
+ if (!pluginId) {
336
+ error('Please provide a plugin ID: bsb-client schema <name> or bsb-client schema <org/name>');
337
+ }
338
+ const { org, name } = parsePluginId(pluginId);
339
+ const display = displayPluginId(org, name);
340
+ info(`Fetching schema for ${display}...`);
341
+ try {
342
+ // Get plugin to find latest version
343
+ const result = await registryRequest('GET', `/plugins/${org}/${name}`);
344
+ const plugin = result.plugin || result;
345
+ const schema = await registryRequest('GET', `/plugins/${org}/${name}/${plugin.version}/schema`);
346
+ log(`\nEvent Schema for ${pluginId} @ ${plugin.version}:\n`, 'bright');
347
+ log(JSON.stringify(schema, null, 2), 'reset');
348
+ success('Schema retrieved');
349
+ }
350
+ catch (err) {
351
+ error(`Failed to get schema: ${err.message}`);
352
+ }
353
+ }
354
+ /**
355
+ * Publish plugin(s) to registry.
356
+ * Iterates over all plugins in bsb-plugin.json and publishes each separately.
357
+ * Org is read from package.json "bsb.orgId" field, defaulting to "_" (unaffiliated).
358
+ */
359
+ async function publishPlugin() {
360
+ if (!REGISTRY_TOKEN) {
361
+ error('BSB_REGISTRY_TOKEN environment variable not set. Get a token from the registry admin.');
362
+ }
363
+ info('Publishing plugin(s) to registry...');
364
+ try {
365
+ // Read package.json
366
+ const pkgPath = path.join(process.cwd(), 'package.json');
367
+ if (!fs.existsSync(pkgPath)) {
368
+ error('No package.json found in current directory');
369
+ }
370
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
371
+ // Read publish manifest from bsb-plugin.json
372
+ const manifestPath = path.join(process.cwd(), 'bsb-plugin.json');
373
+ if (!fs.existsSync(manifestPath)) {
374
+ error('No bsb-plugin.json found. Run "bsb-plugin-cli build" first.');
375
+ }
376
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
377
+ const plugins = Array.isArray(manifest.nodejs) ? manifest.nodejs : [];
378
+ if (plugins.length === 0) {
379
+ error('No Node.js plugins found in bsb-plugin.json');
380
+ }
381
+ // Schemas are still sourced from generated lib/schemas/{plugin}.json
382
+ const schemasDir = path.join(process.cwd(), 'lib', 'schemas');
383
+ if (!fs.existsSync(schemasDir)) {
384
+ error('No lib/schemas/ directory found. Run "bsb-plugin-cli build" first.');
385
+ }
386
+ // Read project README.md as fallback documentation
387
+ const readmePath = path.join(process.cwd(), 'README.md');
388
+ const readmeContent = fs.existsSync(readmePath) ? fs.readFileSync(readmePath, 'utf-8') : undefined;
389
+ // Org from package.json bsb.orgId, default to "_" (unaffiliated)
390
+ const org = pkg.bsb?.orgId || '_';
391
+ const publishIgnoreRaw = pkg.bsb?.publishIgnore;
392
+ const publishIgnore = new Set(Array.isArray(publishIgnoreRaw)
393
+ ? publishIgnoreRaw
394
+ .filter((entry) => typeof entry === 'string' && entry.trim().length > 0)
395
+ .map((entry) => normalizeIgnoredPluginId(entry, org))
396
+ : []);
397
+ let published = 0;
398
+ let skipped = 0;
399
+ let errors = 0;
400
+ for (const pluginMeta of plugins) {
401
+ const pluginName = pluginMeta.id;
402
+ const display = displayPluginId(org, pluginName);
403
+ if (publishIgnore.has(pluginName)) {
404
+ info(`Skipping ${display} (listed in package.json bsb.publishIgnore)`);
405
+ skipped++;
406
+ continue;
407
+ }
408
+ try {
409
+ const category = resolveCategory({ id: pluginName, category: pluginMeta.category });
410
+ const imagePath = resolveImagePath({ basePath: pluginMeta.basePath, image: pluginMeta.image });
411
+ // Read event schema from lib/schemas/{pluginId}.json
412
+ const schemaPath = path.join(schemasDir, `${pluginName}.json`);
413
+ let eventSchema = { pluginName, version: pkg.version, events: {} };
414
+ let configSchema;
415
+ let schemaDeps;
416
+ if (fs.existsSync(schemaPath)) {
417
+ try {
418
+ const parsed = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'));
419
+ eventSchema = {
420
+ pluginName: parsed.pluginName || pluginName,
421
+ version: parsed.version || pkg.version,
422
+ events: parsed.events || {},
423
+ };
424
+ if (parsed.capabilities && typeof parsed.capabilities === 'object') {
425
+ eventSchema.capabilities = parsed.capabilities;
426
+ }
427
+ if (Array.isArray(parsed.dependencies) && parsed.dependencies.length > 0) {
428
+ eventSchema.dependencies = parsed.dependencies;
429
+ schemaDeps = parsed.dependencies;
430
+ }
431
+ if (parsed.configSchema && typeof parsed.configSchema === 'object') {
432
+ configSchema = parsed.configSchema;
433
+ }
434
+ }
435
+ catch {
436
+ // Non-fatal -- use defaults
437
+ }
438
+ }
439
+ // Fallback: configSchema from plugin.json
440
+ if (!configSchema && pluginMeta.configSchema && typeof pluginMeta.configSchema === 'object') {
441
+ configSchema = pluginMeta.configSchema;
442
+ }
443
+ // Read documentation files listed in plugin metadata
444
+ const documentation = [];
445
+ const docPaths = Array.isArray(pluginMeta.documentation) ? pluginMeta.documentation : [];
446
+ for (const docPath of docPaths) {
447
+ const fullPath = path.resolve(process.cwd(), docPath);
448
+ if (fs.existsSync(fullPath)) {
449
+ documentation.push(fs.readFileSync(fullPath, 'utf-8'));
450
+ }
451
+ else {
452
+ warn(`Documentation file not found: ${docPath}`);
453
+ }
454
+ }
455
+ // Fallback to project README.md
456
+ if (documentation.length === 0) {
457
+ if (readmeContent) {
458
+ documentation.push(readmeContent);
459
+ }
460
+ else {
461
+ error(`No documentation found for ${display}. Add documentation paths to Config metadata or provide a README.md.`);
462
+ }
463
+ }
464
+ const publishRequest = {
465
+ org,
466
+ name: pluginName,
467
+ version: pkg.version,
468
+ language: 'nodejs',
469
+ metadata: {
470
+ displayName: pluginMeta.name || pluginName,
471
+ description: pluginMeta.description || pkg.description || '',
472
+ category,
473
+ tags: pluginMeta.tags || pkg.keywords || [],
474
+ author: pluginMeta.author || pkg.author,
475
+ license: pluginMeta.license || pkg.license,
476
+ homepage: pluginMeta.homepage || pkg.homepage,
477
+ repository: pluginMeta.repository || (typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url),
478
+ },
479
+ eventSchema,
480
+ documentation,
481
+ package: {
482
+ nodejs: pkg.name,
483
+ },
484
+ visibility: 'public',
485
+ };
486
+ if (configSchema) {
487
+ publishRequest.configSchema = configSchema;
488
+ }
489
+ // Top-level dependencies (registry gives these priority over eventSchema.dependencies)
490
+ if (schemaDeps) {
491
+ publishRequest.dependencies = schemaDeps;
492
+ }
493
+ info(`Publishing ${display} @ ${pkg.version}...`);
494
+ const result = await registryRequest('POST', '/plugins', publishRequest, true);
495
+ if (imagePath) {
496
+ info(`Uploading image for ${display}...`);
497
+ await uploadPluginImage(org, pluginName, imagePath);
498
+ }
499
+ success(`Published: ${display} @ ${result.version}${imagePath ? ' (with image)' : ''}`);
500
+ published++;
501
+ }
502
+ catch (err) {
503
+ log(` Failed to publish ${display}: ${err.message}`, 'red');
504
+ errors++;
505
+ }
506
+ }
507
+ log('');
508
+ if (published > 0) {
509
+ success(`Published ${published} plugin(s)${skipped > 0 ? `, ${skipped} skipped` : ''}${errors > 0 ? `, ${errors} failed` : ''}`);
510
+ }
511
+ if (published === 0 && skipped > 0 && errors === 0) {
512
+ success(`No plugins published (${skipped} skipped via package.json bsb.publishIgnore).`);
513
+ }
514
+ if (errors > 0 && published === 0) {
515
+ error(`All ${errors} plugin(s) failed to publish`);
516
+ }
517
+ }
518
+ catch (err) {
519
+ error(`Failed to publish plugin: ${err.message}`);
520
+ }
521
+ }
522
+ /**
523
+ * Ensure the project's .gitignore contains the src/.bsb/ entry.
524
+ */
525
+ function ensureGitignore() {
526
+ const projectRoot = process.cwd();
527
+ const gitignorePath = path.join(projectRoot, '.gitignore');
528
+ const bsbDir = path.join(projectRoot, 'src', '.bsb');
529
+ let relativeBsbDir = path.relative(projectRoot, bsbDir).replace(/\\/g, '/');
530
+ if (!relativeBsbDir.endsWith('/')) {
531
+ relativeBsbDir += '/';
532
+ }
533
+ try {
534
+ if (fs.existsSync(gitignorePath)) {
535
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
536
+ const lines = content.split(/\r?\n/);
537
+ const alreadyIgnored = lines.some(line => {
538
+ const trimmed = line.trim();
539
+ return trimmed === relativeBsbDir ||
540
+ trimmed === relativeBsbDir.replace(/\/$/, '') ||
541
+ trimmed === '.bsb/' ||
542
+ trimmed === '.bsb' ||
543
+ trimmed === 'src/.bsb/' ||
544
+ trimmed === 'src/.bsb';
545
+ });
546
+ if (!alreadyIgnored) {
547
+ const newline = content.endsWith('\n') ? '' : '\n';
548
+ fs.writeFileSync(gitignorePath, content + newline + relativeBsbDir + '\n', 'utf-8');
549
+ success(`Added '${relativeBsbDir}' to .gitignore`);
550
+ }
551
+ }
552
+ else {
553
+ fs.writeFileSync(gitignorePath, relativeBsbDir + '\n', 'utf-8');
554
+ success(`Created .gitignore with '${relativeBsbDir}'`);
555
+ }
556
+ }
557
+ catch {
558
+ // Non-fatal
559
+ }
560
+ }
561
+ /**
562
+ * Install plugin from registry (download schema and generate virtual client)
563
+ */
564
+ async function installPlugin(pluginId) {
565
+ if (!pluginId) {
566
+ error('Please provide a plugin ID: bsb-client install <name> or bsb-client install <org/name>');
567
+ }
568
+ const { org, name } = parsePluginId(pluginId);
569
+ const display = displayPluginId(org, name);
570
+ info(`Installing plugin ${display}...`);
571
+ try {
572
+ // Get plugin metadata
573
+ const detailResult = await registryRequest('GET', `/plugins/${org}/${name}`);
574
+ const plugin = detailResult.plugin || detailResult;
575
+ // Get plugin schema
576
+ const schema = await registryRequest('GET', `/plugins/${org}/${name}/${plugin.version}/schema`);
577
+ // Create directories for remote schemas and virtual clients
578
+ const schemasDir = path.join(process.cwd(), 'src', '.bsb', 'schemas');
579
+ const clientsDir = path.join(process.cwd(), 'src', '.bsb', 'clients');
580
+ if (!fs.existsSync(schemasDir)) {
581
+ fs.mkdirSync(schemasDir, { recursive: true });
582
+ }
583
+ if (!fs.existsSync(clientsDir)) {
584
+ fs.mkdirSync(clientsDir, { recursive: true });
585
+ }
586
+ // Ensure .gitignore covers the generated directory
587
+ ensureGitignore();
588
+ // Save schema
589
+ const schemaFile = path.join(schemasDir, `${name}.json`);
590
+ fs.writeFileSync(schemaFile, JSON.stringify(schema, null, 2), 'utf-8');
591
+ success(`Downloaded schema for ${display}`);
592
+ // Generate virtual client by calling the generator
593
+ const generatorPath = path.join(__dirname, 'generate-client-types.js');
594
+ if (fs.existsSync(generatorPath)) {
595
+ const { execSync } = require('child_process');
596
+ try {
597
+ execSync(`node "${generatorPath}"`, {
598
+ cwd: process.cwd(),
599
+ stdio: 'pipe',
600
+ });
601
+ success(`Generated virtual client for ${display}`);
602
+ }
603
+ catch (err) {
604
+ warn('Failed to generate virtual client automatically. Run your build to regenerate.');
605
+ }
606
+ }
607
+ log('');
608
+ success(`Plugin ${display} @ ${plugin.version} installed`);
609
+ log(` Schema: ${schemaFile}`, 'reset');
610
+ log(` Import: import ${pluginNameToClassName(name)} from './.bsb/clients/${name}'`, 'reset');
611
+ }
612
+ catch (err) {
613
+ error(`Failed to install plugin: ${err.message}`);
614
+ }
615
+ }
616
+ /**
617
+ * Convert plugin key/ID to PascalCase client class name.
618
+ * Strips non-alphanumeric characters to ensure valid TypeScript identifiers.
619
+ */
620
+ function pluginNameToClassName(pluginId) {
621
+ let name = pluginId;
622
+ if (name.startsWith('service-')) {
623
+ name = name.substring('service-'.length);
624
+ }
625
+ const pascal = name
626
+ .replace(/[^a-zA-Z0-9-]/g, '')
627
+ .split('-')
628
+ .filter(part => part.length > 0)
629
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
630
+ .join('');
631
+ return pascal + 'Client';
632
+ }
633
+ /**
634
+ * Generate API token
635
+ */
636
+ async function generateToken() {
637
+ warn('Token generation is not yet implemented.');
638
+ warn('Please contact the registry administrator to obtain an API token.');
639
+ }
640
+ /**
641
+ * Main CLI handler
642
+ */
643
+ async function main() {
644
+ if (!COMMAND) {
645
+ log('BSB Client CLI - Plugin Registry Commands', 'bright');
646
+ log('');
647
+ log('Usage:', 'cyan');
648
+ log(' bsb-client list - List all plugins from registry');
649
+ log(' bsb-client search <query> - Search plugins');
650
+ log(' bsb-client info <name> - Get plugin details');
651
+ log(' bsb-client schema <name> - Get plugin event schema');
652
+ log(' bsb-client install <name> - Download schema and generate types');
653
+ log(' bsb-client publish - Publish current plugin(s) to registry');
654
+ log(' bsb-client token generate - Generate a new API token');
655
+ log('');
656
+ log('Plugin IDs:', 'cyan');
657
+ log(' Use plain name: bsb-client info service-bsb-registry');
658
+ log(' Or with org: bsb-client info myorg/service-bsb-registry');
659
+ log('');
660
+ log('Environment:', 'cyan');
661
+ log(` BSB_REGISTRY_URL = ${REGISTRY_URL}`);
662
+ log(` BSB_REGISTRY_TOKEN = ${REGISTRY_TOKEN ? '***' + REGISTRY_TOKEN.substring(REGISTRY_TOKEN.length - 4) : '(not set)'}`);
663
+ log('');
664
+ process.exit(0);
665
+ }
666
+ switch (COMMAND) {
667
+ case 'list':
668
+ await listPlugins();
669
+ break;
670
+ case 'search':
671
+ await searchPlugins(ARGS[0]);
672
+ break;
673
+ case 'info':
674
+ await getPluginInfo(ARGS[0]);
675
+ break;
676
+ case 'schema':
677
+ await getPluginSchema(ARGS[0]);
678
+ break;
679
+ case 'install':
680
+ await installPlugin(ARGS[0]);
681
+ break;
682
+ case 'publish':
683
+ await publishPlugin();
684
+ break;
685
+ case 'token':
686
+ if (ARGS[0] === 'generate') {
687
+ await generateToken();
688
+ }
689
+ else {
690
+ error('Unknown token command. Use: bsb-client token generate');
691
+ }
692
+ break;
693
+ default:
694
+ error(`Unknown command: ${COMMAND}`);
695
+ }
696
+ }
697
+ // Run CLI
698
+ main().catch((err) => {
699
+ error(`Unexpected error: ${err.message}`);
700
+ });
701
+ //# sourceMappingURL=bsb-client-cli.js.map