@objectstack/core 4.0.3 → 4.0.5

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 (75) hide show
  1. package/README.md +95 -10
  2. package/dist/index.cjs +169 -507
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +24 -223
  5. package/dist/index.d.ts +24 -223
  6. package/dist/index.js +175 -505
  7. package/dist/index.js.map +1 -1
  8. package/dist/logger.cjs +177 -0
  9. package/dist/logger.cjs.map +1 -0
  10. package/dist/logger.d.cts +26 -0
  11. package/dist/logger.d.ts +26 -0
  12. package/dist/logger.js +158 -0
  13. package/dist/logger.js.map +1 -0
  14. package/package.json +36 -15
  15. package/.turbo/turbo-build.log +0 -22
  16. package/ADVANCED_FEATURES.md +0 -380
  17. package/API_REGISTRY.md +0 -392
  18. package/CHANGELOG.md +0 -465
  19. package/PHASE2_IMPLEMENTATION.md +0 -388
  20. package/REFACTORING_SUMMARY.md +0 -40
  21. package/examples/api-registry-example.ts +0 -559
  22. package/examples/kernel-features-example.ts +0 -311
  23. package/examples/phase2-integration.ts +0 -357
  24. package/src/api-registry-plugin.test.ts +0 -393
  25. package/src/api-registry-plugin.ts +0 -89
  26. package/src/api-registry.test.ts +0 -1089
  27. package/src/api-registry.ts +0 -739
  28. package/src/contracts/data-engine.ts +0 -57
  29. package/src/contracts/http-server.ts +0 -151
  30. package/src/contracts/logger.ts +0 -72
  31. package/src/dependency-resolver.test.ts +0 -287
  32. package/src/dependency-resolver.ts +0 -390
  33. package/src/fallbacks/fallbacks.test.ts +0 -281
  34. package/src/fallbacks/index.ts +0 -26
  35. package/src/fallbacks/memory-cache.ts +0 -34
  36. package/src/fallbacks/memory-i18n.ts +0 -112
  37. package/src/fallbacks/memory-job.ts +0 -23
  38. package/src/fallbacks/memory-metadata.ts +0 -50
  39. package/src/fallbacks/memory-queue.ts +0 -28
  40. package/src/health-monitor.test.ts +0 -81
  41. package/src/health-monitor.ts +0 -318
  42. package/src/hot-reload.ts +0 -382
  43. package/src/index.ts +0 -50
  44. package/src/kernel-base.ts +0 -273
  45. package/src/kernel.test.ts +0 -624
  46. package/src/kernel.ts +0 -631
  47. package/src/lite-kernel.test.ts +0 -248
  48. package/src/lite-kernel.ts +0 -137
  49. package/src/logger.test.ts +0 -116
  50. package/src/logger.ts +0 -355
  51. package/src/namespace-resolver.test.ts +0 -130
  52. package/src/namespace-resolver.ts +0 -188
  53. package/src/package-manager.test.ts +0 -225
  54. package/src/package-manager.ts +0 -428
  55. package/src/plugin-loader.test.ts +0 -421
  56. package/src/plugin-loader.ts +0 -484
  57. package/src/qa/adapter.ts +0 -16
  58. package/src/qa/http-adapter.ts +0 -116
  59. package/src/qa/index.ts +0 -5
  60. package/src/qa/runner.ts +0 -189
  61. package/src/security/index.ts +0 -50
  62. package/src/security/permission-manager.test.ts +0 -256
  63. package/src/security/permission-manager.ts +0 -338
  64. package/src/security/plugin-config-validator.test.ts +0 -276
  65. package/src/security/plugin-config-validator.ts +0 -193
  66. package/src/security/plugin-permission-enforcer.test.ts +0 -251
  67. package/src/security/plugin-permission-enforcer.ts +0 -436
  68. package/src/security/plugin-signature-verifier.ts +0 -403
  69. package/src/security/sandbox-runtime.ts +0 -462
  70. package/src/security/security-scanner.ts +0 -367
  71. package/src/types.ts +0 -120
  72. package/src/utils/env.test.ts +0 -62
  73. package/src/utils/env.ts +0 -53
  74. package/tsconfig.json +0 -10
  75. package/vitest.config.ts +0 -10
package/src/logger.ts DELETED
@@ -1,355 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { LoggerConfig, LogLevel } from '@objectstack/spec/system';
4
- import type { Logger } from '@objectstack/spec/contracts';
5
- import { isNode } from './utils/env.js';
6
-
7
- /**
8
- * Universal Logger Implementation
9
- *
10
- * A configurable logger that works in both browser and Node.js environments.
11
- * - Node.js: Uses Pino for high-performance structured logging
12
- * - Browser: Simple console-based implementation
13
- *
14
- * Features:
15
- * - Structured logging with multiple formats (json, text, pretty)
16
- * - Log level filtering
17
- * - Sensitive data redaction
18
- * - File logging with rotation (Node.js only via Pino)
19
- * - Browser console integration
20
- * - Distributed tracing support (traceId, spanId)
21
- */
22
- export class ObjectLogger implements Logger {
23
- private config: Required<Omit<LoggerConfig, 'file' | 'rotation' | 'name'>> & { file?: string; rotation?: { maxSize: string; maxFiles: number }; name?: string };
24
- private isNode: boolean;
25
- private pinoLogger?: any; // Pino logger instance for Node.js
26
- private pinoInstance?: any; // Base Pino instance for creating child loggers
27
- private require?: any; // CommonJS require function for Node.js
28
-
29
- constructor(config: Partial<LoggerConfig> = {}) {
30
- // Detect runtime environment
31
- this.isNode = isNode;
32
-
33
- // Set defaults
34
- this.config = {
35
- name: config.name,
36
- level: config.level ?? 'info',
37
- format: config.format ?? (this.isNode ? 'json' : 'pretty'),
38
- redact: config.redact ?? ['password', 'token', 'secret', 'key'],
39
- sourceLocation: config.sourceLocation ?? false,
40
- file: config.file,
41
- rotation: config.rotation ?? {
42
- maxSize: '10m',
43
- maxFiles: 5
44
- }
45
- };
46
-
47
- // Initialize Pino logger for Node.js
48
- if (this.isNode) {
49
- this.initPinoLogger();
50
- }
51
- }
52
-
53
- /**
54
- * Initialize Pino logger for Node.js
55
- */
56
- private async initPinoLogger() {
57
- if (!this.isNode) return;
58
-
59
- try {
60
- // Create require function dynamically for Node.js (avoids bundling issues in browser)
61
- // @ts-ignore - dynamic import of Node.js module
62
- const { createRequire } = await import('module');
63
- this.require = createRequire(import.meta.url);
64
-
65
- // Synchronous import for Pino using createRequire (works in ESM)
66
- const pino = this.require('pino');
67
-
68
- // Build Pino options
69
- const pinoOptions: any = {
70
- level: this.config.level,
71
- redact: {
72
- paths: this.config.redact,
73
- censor: '***REDACTED***'
74
- }
75
- };
76
-
77
- // Add name if provided
78
- if (this.config.name) {
79
- pinoOptions.name = this.config.name;
80
- }
81
-
82
- // Transport configuration for pretty printing or file output
83
- const targets: any[] = [];
84
-
85
- // Console transport
86
- if (this.config.format === 'pretty') {
87
- // Check if pino-pretty is available
88
- let hasPretty = false;
89
- try {
90
- this.require.resolve('pino-pretty');
91
- hasPretty = true;
92
- } catch (e) {
93
- // ignore
94
- }
95
-
96
- if (hasPretty) {
97
- targets.push({
98
- target: 'pino-pretty',
99
- options: {
100
- colorize: true,
101
- translateTime: 'SYS:standard',
102
- ignore: 'pid,hostname'
103
- },
104
- level: this.config.level
105
- });
106
- } else {
107
- console.warn('[Logger] pino-pretty not found. Install it for pretty logging: pnpm add -D pino-pretty');
108
- // Fallback to text/simple
109
- targets.push({
110
- target: 'pino/file',
111
- options: { destination: 1 },
112
- level: this.config.level
113
- });
114
- }
115
- } else if (this.config.format === 'json') {
116
- // JSON to stdout
117
- targets.push({
118
- target: 'pino/file',
119
- options: { destination: 1 }, // stdout
120
- level: this.config.level
121
- });
122
- } else {
123
- // text format (simple)
124
- targets.push({
125
- target: 'pino/file',
126
- options: { destination: 1 },
127
- level: this.config.level
128
- });
129
- }
130
-
131
- // File transport (if configured)
132
- if (this.config.file) {
133
- targets.push({
134
- target: 'pino/file',
135
- options: {
136
- destination: this.config.file,
137
- mkdir: true
138
- },
139
- level: this.config.level
140
- });
141
- }
142
-
143
- // Create transport
144
- if (targets.length > 0) {
145
- pinoOptions.transport = targets.length === 1 ? targets[0] : { targets };
146
- }
147
-
148
- // Create Pino logger
149
- this.pinoInstance = pino(pinoOptions);
150
- this.pinoLogger = this.pinoInstance;
151
-
152
- } catch (error) {
153
- // Fallback to console if Pino is not available
154
- console.warn('[Logger] Pino not available, falling back to console:', error);
155
- this.pinoLogger = null;
156
- }
157
- }
158
-
159
- /**
160
- * Redact sensitive keys from context object (for browser)
161
- */
162
- private redactSensitive(obj: any): any {
163
- if (!obj || typeof obj !== 'object') return obj;
164
-
165
- const redacted = Array.isArray(obj) ? [...obj] : { ...obj };
166
-
167
- for (const key in redacted) {
168
- const lowerKey = key.toLowerCase();
169
- const shouldRedact = this.config.redact.some((pattern: string) =>
170
- lowerKey.includes(pattern.toLowerCase())
171
- );
172
-
173
- if (shouldRedact) {
174
- redacted[key] = '***REDACTED***';
175
- } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
176
- redacted[key] = this.redactSensitive(redacted[key]);
177
- }
178
- }
179
-
180
- return redacted;
181
- }
182
-
183
- /**
184
- * Format log entry for browser
185
- */
186
- private formatBrowserLog(level: LogLevel, message: string, context?: Record<string, any>): string {
187
- if (this.config.format === 'json') {
188
- return JSON.stringify({
189
- timestamp: new Date().toISOString(),
190
- level,
191
- message,
192
- ...context
193
- });
194
- }
195
-
196
- if (this.config.format === 'text') {
197
- const parts = [new Date().toISOString(), level.toUpperCase(), message];
198
- if (context && Object.keys(context).length > 0) {
199
- parts.push(JSON.stringify(context));
200
- }
201
- return parts.join(' | ');
202
- }
203
-
204
- // Pretty format
205
- const levelColors: Record<LogLevel, string> = {
206
- debug: '\x1b[36m', // Cyan
207
- info: '\x1b[32m', // Green
208
- warn: '\x1b[33m', // Yellow
209
- error: '\x1b[31m', // Red
210
- fatal: '\x1b[35m', // Magenta
211
- silent: ''
212
- };
213
- const reset = '\x1b[0m';
214
- const color = levelColors[level] || '';
215
-
216
- let output = `${color}[${level.toUpperCase()}]${reset} ${message}`;
217
-
218
- if (context && Object.keys(context).length > 0) {
219
- output += ` ${JSON.stringify(context, null, 2)}`;
220
- }
221
-
222
- return output;
223
- }
224
-
225
- /**
226
- * Log using browser console
227
- */
228
- private logBrowser(level: LogLevel, message: string, context?: Record<string, any>, error?: Error) {
229
- const redactedContext = context ? this.redactSensitive(context) : undefined;
230
- const mergedContext = error ? { ...redactedContext, error: { message: error.message, stack: error.stack } } : redactedContext;
231
-
232
- const formatted = this.formatBrowserLog(level, message, mergedContext);
233
-
234
- const consoleMethod = level === 'debug' ? 'debug' :
235
- level === 'info' ? 'log' :
236
- level === 'warn' ? 'warn' :
237
- level === 'error' || level === 'fatal' ? 'error' :
238
- 'log';
239
-
240
- console[consoleMethod](formatted);
241
- }
242
-
243
- /**
244
- * Public logging methods
245
- */
246
- debug(message: string, meta?: Record<string, any>): void {
247
- if (this.isNode && this.pinoLogger) {
248
- this.pinoLogger.debug(meta || {}, message);
249
- } else {
250
- this.logBrowser('debug', message, meta);
251
- }
252
- }
253
-
254
- info(message: string, meta?: Record<string, any>): void {
255
- if (this.isNode && this.pinoLogger) {
256
- this.pinoLogger.info(meta || {}, message);
257
- } else {
258
- this.logBrowser('info', message, meta);
259
- }
260
- }
261
-
262
- warn(message: string, meta?: Record<string, any>): void {
263
- if (this.isNode && this.pinoLogger) {
264
- this.pinoLogger.warn(meta || {}, message);
265
- } else {
266
- this.logBrowser('warn', message, meta);
267
- }
268
- }
269
-
270
- error(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {
271
- let error: Error | undefined;
272
- let context: Record<string, any> = {};
273
-
274
- if (errorOrMeta instanceof Error) {
275
- error = errorOrMeta;
276
- context = meta || {};
277
- } else {
278
- context = errorOrMeta || {};
279
- }
280
-
281
- if (this.isNode && this.pinoLogger) {
282
- const errorContext = error ? { err: error, ...context } : context;
283
- this.pinoLogger.error(errorContext, message);
284
- } else {
285
- this.logBrowser('error', message, context, error);
286
- }
287
- }
288
-
289
- fatal(message: string, errorOrMeta?: Error | Record<string, any>, meta?: Record<string, any>): void {
290
- let error: Error | undefined;
291
- let context: Record<string, any> = {};
292
-
293
- if (errorOrMeta instanceof Error) {
294
- error = errorOrMeta;
295
- context = meta || {};
296
- } else {
297
- context = errorOrMeta || {};
298
- }
299
-
300
- if (this.isNode && this.pinoLogger) {
301
- const errorContext = error ? { err: error, ...context } : context;
302
- this.pinoLogger.fatal(errorContext, message);
303
- } else {
304
- this.logBrowser('fatal', message, context, error);
305
- }
306
- }
307
-
308
- /**
309
- * Create a child logger with additional context
310
- * Note: Child loggers share the parent's Pino instance
311
- */
312
- child(context: Record<string, any>): ObjectLogger {
313
- const childLogger = new ObjectLogger(this.config);
314
-
315
- // For Node.js with Pino, create a Pino child logger
316
- if (this.isNode && this.pinoInstance) {
317
- childLogger.pinoLogger = this.pinoInstance.child(context);
318
- childLogger.pinoInstance = this.pinoInstance;
319
- }
320
-
321
- return childLogger;
322
- }
323
-
324
- /**
325
- * Set trace context for distributed tracing
326
- */
327
- withTrace(traceId: string, spanId?: string): ObjectLogger {
328
- return this.child({ traceId, spanId });
329
- }
330
-
331
- /**
332
- * Cleanup resources
333
- */
334
- async destroy(): Promise<void> {
335
- if (this.pinoLogger && this.pinoLogger.flush) {
336
- await new Promise<void>((resolve) => {
337
- this.pinoLogger.flush(() => resolve());
338
- });
339
- }
340
- }
341
-
342
- /**
343
- * Compatibility method for console.log usage
344
- */
345
- log(message: string, ...args: any[]): void {
346
- this.info(message, args.length > 0 ? { args } : undefined);
347
- }
348
- }
349
-
350
- /**
351
- * Create a logger instance
352
- */
353
- export function createLogger(config?: Partial<LoggerConfig>): ObjectLogger {
354
- return new ObjectLogger(config);
355
- }
@@ -1,130 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import { NamespaceResolver } from './namespace-resolver.js';
3
- import { createLogger } from './logger.js';
4
-
5
- describe('NamespaceResolver', () => {
6
- let resolver: NamespaceResolver;
7
-
8
- beforeEach(() => {
9
- const logger = createLogger({ level: 'silent' });
10
- resolver = new NamespaceResolver(logger);
11
- });
12
-
13
- describe('register', () => {
14
- it('should register namespaces for a package', () => {
15
- resolver.register('pkg-a', ['objects.task', 'views.task_list']);
16
-
17
- const registry = resolver.getRegistry();
18
- expect(registry.size).toBe(2);
19
- expect(registry.get('objects.task')?.packageId).toBe('pkg-a');
20
- expect(registry.get('views.task_list')?.packageId).toBe('pkg-a');
21
- });
22
-
23
- it('should overwrite namespace when same package re-registers', () => {
24
- resolver.register('pkg-a', ['objects.task']);
25
- resolver.register('pkg-a', ['objects.task']);
26
-
27
- expect(resolver.getRegistry().size).toBe(1);
28
- expect(resolver.getRegistry().get('objects.task')?.packageId).toBe('pkg-a');
29
- });
30
- });
31
-
32
- describe('unregister', () => {
33
- it('should remove all namespaces for a package', () => {
34
- resolver.register('pkg-a', ['objects.task', 'views.task_list']);
35
- resolver.register('pkg-b', ['objects.project']);
36
-
37
- const removed = resolver.unregister('pkg-a');
38
- expect(removed).toEqual(['objects.task', 'views.task_list']);
39
- expect(resolver.getRegistry().size).toBe(1);
40
- expect(resolver.getRegistry().has('objects.project')).toBe(true);
41
- });
42
-
43
- it('should return empty array for unknown package', () => {
44
- const removed = resolver.unregister('unknown');
45
- expect(removed).toEqual([]);
46
- });
47
- });
48
-
49
- describe('checkAvailability', () => {
50
- it('should report no conflicts for unused namespaces', () => {
51
- const result = resolver.checkAvailability('pkg-a', ['objects.task']);
52
- expect(result.available).toBe(true);
53
- expect(result.conflicts).toHaveLength(0);
54
- });
55
-
56
- it('should detect conflict with another package', () => {
57
- resolver.register('pkg-a', ['objects.task']);
58
-
59
- const result = resolver.checkAvailability('pkg-b', ['objects.task']);
60
- expect(result.available).toBe(false);
61
- expect(result.conflicts).toHaveLength(1);
62
- expect(result.conflicts[0].namespace).toBe('objects.task');
63
- expect(result.conflicts[0].existingPackageId).toBe('pkg-a');
64
- expect(result.conflicts[0].incomingPackageId).toBe('pkg-b');
65
- });
66
-
67
- it('should not conflict with own namespaces', () => {
68
- resolver.register('pkg-a', ['objects.task']);
69
-
70
- const result = resolver.checkAvailability('pkg-a', ['objects.task']);
71
- expect(result.available).toBe(true);
72
- });
73
-
74
- it('should provide suggestions for conflicts', () => {
75
- resolver.register('pkg-a', ['objects.task']);
76
-
77
- const result = resolver.checkAvailability('@myorg/plugin-crm', ['objects.task']);
78
- expect(result.available).toBe(false);
79
- expect(result.suggestions['objects.task']).toBeDefined();
80
- expect(result.suggestions['objects.task']).toContain('crm');
81
- });
82
- });
83
-
84
- describe('extractNamespaces', () => {
85
- it('should extract namespaces from object-style metadata', () => {
86
- const config = {
87
- objects: { task: {}, project: {} },
88
- views: { task_list: {} },
89
- };
90
-
91
- const ns = resolver.extractNamespaces(config);
92
- expect(ns).toContain('objects.task');
93
- expect(ns).toContain('objects.project');
94
- expect(ns).toContain('views.task_list');
95
- expect(ns).toHaveLength(3);
96
- });
97
-
98
- it('should extract namespaces from array-style metadata', () => {
99
- const config = {
100
- objects: [{ name: 'task' }, { name: 'project' }],
101
- flows: [{ name: 'approval_flow' }],
102
- };
103
-
104
- const ns = resolver.extractNamespaces(config);
105
- expect(ns).toContain('objects.task');
106
- expect(ns).toContain('objects.project');
107
- expect(ns).toContain('flows.approval_flow');
108
- expect(ns).toHaveLength(3);
109
- });
110
-
111
- it('should return empty array for empty config', () => {
112
- const ns = resolver.extractNamespaces({});
113
- expect(ns).toEqual([]);
114
- });
115
- });
116
-
117
- describe('getPackageNamespaces', () => {
118
- it('should return namespaces for a specific package', () => {
119
- resolver.register('pkg-a', ['objects.task', 'views.task_list']);
120
- resolver.register('pkg-b', ['objects.project']);
121
-
122
- expect(resolver.getPackageNamespaces('pkg-a')).toEqual(['objects.task', 'views.task_list']);
123
- expect(resolver.getPackageNamespaces('pkg-b')).toEqual(['objects.project']);
124
- });
125
-
126
- it('should return empty for unknown package', () => {
127
- expect(resolver.getPackageNamespaces('unknown')).toEqual([]);
128
- });
129
- });
130
- });
@@ -1,188 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { ObjectLogger } from './logger.js';
4
-
5
- /**
6
- * Namespace entry representing an object/view/flow etc. registered by a package.
7
- */
8
- export interface NamespaceEntry {
9
- /** The namespace path (e.g. "objects.project_task", "views.task_list") */
10
- namespace: string;
11
- /** The package that owns this namespace */
12
- packageId: string;
13
- /** When this entry was registered */
14
- registeredAt: string;
15
- }
16
-
17
- /**
18
- * Result of a namespace conflict check.
19
- */
20
- export interface NamespaceConflict {
21
- /** The conflicting namespace path */
22
- namespace: string;
23
- /** The package that currently owns this namespace */
24
- existingPackageId: string;
25
- /** The package attempting to register the same namespace */
26
- incomingPackageId: string;
27
- /** A suggested alternative name to avoid the conflict */
28
- suggestion?: string;
29
- }
30
-
31
- /**
32
- * Result of namespace availability check.
33
- */
34
- export interface NamespaceCheckResult {
35
- /** Whether all requested namespaces are available */
36
- available: boolean;
37
- /** List of conflicts detected */
38
- conflicts: NamespaceConflict[];
39
- /** Suggested alternatives for each conflict */
40
- suggestions: Record<string, string>;
41
- }
42
-
43
- /**
44
- * Namespace Resolver
45
- *
46
- * Manages namespace registration for installed packages and detects collisions
47
- * during install-time. Each metadata item (object, view, flow, page, etc.)
48
- * produces a namespace like `objects.<name>` or `views.<name>`.
49
- *
50
- * When a new package declares objects, views, or other metadata that would
51
- * collide with an existing package's metadata, this resolver reports the
52
- * conflicts and suggests prefixed alternatives.
53
- */
54
- export class NamespaceResolver {
55
- private logger: ObjectLogger;
56
- private registry: Map<string, NamespaceEntry> = new Map();
57
-
58
- constructor(logger: ObjectLogger) {
59
- this.logger = logger.child({ component: 'NamespaceResolver' });
60
- }
61
-
62
- /**
63
- * Register namespaces owned by a package.
64
- */
65
- register(packageId: string, namespaces: string[]): void {
66
- const now = new Date().toISOString();
67
- for (const ns of namespaces) {
68
- if (this.registry.has(ns)) {
69
- const existing = this.registry.get(ns)!;
70
- if (existing.packageId !== packageId) {
71
- this.logger.warn('Overwriting namespace entry', { namespace: ns, existing: existing.packageId, incoming: packageId });
72
- }
73
- }
74
- this.registry.set(ns, { namespace: ns, packageId, registeredAt: now });
75
- this.logger.debug('Namespace registered', { namespace: ns, packageId });
76
- }
77
- }
78
-
79
- /**
80
- * Unregister all namespaces belonging to a package.
81
- */
82
- unregister(packageId: string): string[] {
83
- const removed: string[] = [];
84
- for (const [ns, entry] of this.registry) {
85
- if (entry.packageId === packageId) {
86
- this.registry.delete(ns);
87
- removed.push(ns);
88
- }
89
- }
90
- this.logger.debug('Namespaces unregistered', { packageId, count: removed.length });
91
- return removed;
92
- }
93
-
94
- /**
95
- * Check whether a set of namespaces is available for a given package.
96
- */
97
- checkAvailability(packageId: string, namespaces: string[]): NamespaceCheckResult {
98
- const conflicts: NamespaceConflict[] = [];
99
- const suggestions: Record<string, string> = {};
100
-
101
- for (const ns of namespaces) {
102
- const existing = this.registry.get(ns);
103
- if (existing && existing.packageId !== packageId) {
104
- const suggestion = this.suggestAlternative(ns, packageId);
105
- conflicts.push({
106
- namespace: ns,
107
- existingPackageId: existing.packageId,
108
- incomingPackageId: packageId,
109
- suggestion,
110
- });
111
- suggestions[ns] = suggestion;
112
- }
113
- }
114
-
115
- return {
116
- available: conflicts.length === 0,
117
- conflicts,
118
- suggestions,
119
- };
120
- }
121
-
122
- /**
123
- * Extract namespace strings from a package's metadata definition.
124
- */
125
- extractNamespaces(config: Record<string, unknown>): string[] {
126
- const namespaces: string[] = [];
127
- const categories = [
128
- 'objects', 'views', 'pages', 'flows', 'workflows',
129
- 'apps', 'dashboards', 'reports', 'actions', 'agents',
130
- ];
131
-
132
- for (const category of categories) {
133
- const items = config[category];
134
- if (Array.isArray(items)) {
135
- for (const item of items) {
136
- const name = (item as Record<string, unknown>)?.name;
137
- if (typeof name === 'string') {
138
- namespaces.push(`${category}.${name}`);
139
- }
140
- }
141
- } else if (items && typeof items === 'object') {
142
- for (const key of Object.keys(items as object)) {
143
- namespaces.push(`${category}.${key}`);
144
- }
145
- }
146
- }
147
-
148
- return namespaces;
149
- }
150
-
151
- /**
152
- * Get all registered entries.
153
- */
154
- getRegistry(): ReadonlyMap<string, NamespaceEntry> {
155
- return this.registry;
156
- }
157
-
158
- /**
159
- * Get all namespaces belonging to a specific package.
160
- */
161
- getPackageNamespaces(packageId: string): string[] {
162
- const namespaces: string[] = [];
163
- for (const [ns, entry] of this.registry) {
164
- if (entry.packageId === packageId) {
165
- namespaces.push(ns);
166
- }
167
- }
168
- return namespaces;
169
- }
170
-
171
- /**
172
- * Generate a prefixed alternative namespace to avoid conflicts.
173
- */
174
- private suggestAlternative(ns: string, packageId: string): string {
175
- // Extract the short package name for prefixing
176
- const shortName = packageId
177
- .replace(/^@[^/]+\//, '')
178
- .replace(/^plugin-/, '')
179
- .replace(/-/g, '_');
180
-
181
- const parts = ns.split('.');
182
- if (parts.length >= 2) {
183
- // e.g. "objects.task" → "objects.crm_task"
184
- return `${parts[0]}.${shortName}_${parts.slice(1).join('.')}`;
185
- }
186
- return `${shortName}_${ns}`;
187
- }
188
- }