@objectstack/core 3.0.7 → 3.0.9
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +14 -0
- package/dist/index.cjs +396 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +231 -1
- package/dist/index.d.ts +231 -1
- package/dist/index.js +394 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +4 -0
- package/src/namespace-resolver.test.ts +130 -0
- package/src/namespace-resolver.ts +188 -0
- package/src/package-manager.test.ts +225 -0
- package/src/package-manager.ts +428 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import type { ObjectLogger } from './logger.js';
|
|
4
|
+
import { DependencyResolver, SemanticVersionManager } from './dependency-resolver.js';
|
|
5
|
+
import { NamespaceResolver } from './namespace-resolver.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Installed package record in the runtime registry.
|
|
9
|
+
*/
|
|
10
|
+
export interface InstalledPackageRecord {
|
|
11
|
+
/** Package identifier */
|
|
12
|
+
packageId: string;
|
|
13
|
+
/** Package version */
|
|
14
|
+
version: string;
|
|
15
|
+
/** Package manifest */
|
|
16
|
+
manifest: Record<string, unknown>;
|
|
17
|
+
/** Installation timestamp */
|
|
18
|
+
installedAt: string;
|
|
19
|
+
/** Current status */
|
|
20
|
+
status: 'installed' | 'disabled' | 'installing' | 'upgrading' | 'uninstalling' | 'error';
|
|
21
|
+
/** Namespaces registered by this package */
|
|
22
|
+
namespaces: string[];
|
|
23
|
+
/** Dependencies of this package */
|
|
24
|
+
dependencies: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Snapshot of a package's state before upgrade (for rollback).
|
|
29
|
+
*/
|
|
30
|
+
export interface PackageSnapshot {
|
|
31
|
+
/** Package identifier */
|
|
32
|
+
packageId: string;
|
|
33
|
+
/** Version before upgrade */
|
|
34
|
+
previousVersion: string;
|
|
35
|
+
/** Full manifest before upgrade */
|
|
36
|
+
previousManifest: Record<string, unknown>;
|
|
37
|
+
/** Namespaces before upgrade */
|
|
38
|
+
previousNamespaces: string[];
|
|
39
|
+
/** Original installation timestamp */
|
|
40
|
+
installedAt: string;
|
|
41
|
+
/** Snapshot timestamp */
|
|
42
|
+
createdAt: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Result of a package installation attempt.
|
|
47
|
+
*/
|
|
48
|
+
export interface InstallResult {
|
|
49
|
+
success: boolean;
|
|
50
|
+
packageId: string;
|
|
51
|
+
version: string;
|
|
52
|
+
installedDependencies: string[];
|
|
53
|
+
namespaceConflicts: Array<{ namespace: string; existingPackageId: string }>;
|
|
54
|
+
errorMessage?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Result of an upgrade attempt.
|
|
59
|
+
*/
|
|
60
|
+
export interface UpgradeResult {
|
|
61
|
+
success: boolean;
|
|
62
|
+
packageId: string;
|
|
63
|
+
fromVersion: string;
|
|
64
|
+
toVersion: string;
|
|
65
|
+
snapshot: PackageSnapshot;
|
|
66
|
+
errorMessage?: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Result of a rollback attempt.
|
|
71
|
+
*/
|
|
72
|
+
export interface RollbackResult {
|
|
73
|
+
success: boolean;
|
|
74
|
+
packageId: string;
|
|
75
|
+
restoredVersion: string;
|
|
76
|
+
errorMessage?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Package Manager
|
|
81
|
+
*
|
|
82
|
+
* Runtime implementation for the full package lifecycle:
|
|
83
|
+
* install → upgrade → rollback → uninstall.
|
|
84
|
+
*
|
|
85
|
+
* Consumes the protocol schemas defined in @objectstack/spec:
|
|
86
|
+
* - DependencyResolutionResultSchema
|
|
87
|
+
* - NamespaceConflictErrorSchema
|
|
88
|
+
* - UpgradePlanSchema / UpgradeSnapshotSchema
|
|
89
|
+
* - PackageArtifactSchema
|
|
90
|
+
*
|
|
91
|
+
* Coordinates with:
|
|
92
|
+
* - DependencyResolver for topological ordering and conflict detection
|
|
93
|
+
* - NamespaceResolver for metadata collision prevention
|
|
94
|
+
*/
|
|
95
|
+
export class PackageManager {
|
|
96
|
+
private logger: ObjectLogger;
|
|
97
|
+
private packages: Map<string, InstalledPackageRecord> = new Map();
|
|
98
|
+
private snapshots: Map<string, PackageSnapshot> = new Map();
|
|
99
|
+
private dependencyResolver: DependencyResolver;
|
|
100
|
+
private namespaceResolver: NamespaceResolver;
|
|
101
|
+
private platformVersion: string;
|
|
102
|
+
|
|
103
|
+
constructor(
|
|
104
|
+
logger: ObjectLogger,
|
|
105
|
+
options: { platformVersion?: string } = {},
|
|
106
|
+
) {
|
|
107
|
+
this.logger = logger.child({ component: 'PackageManager' });
|
|
108
|
+
this.dependencyResolver = new DependencyResolver(logger);
|
|
109
|
+
this.namespaceResolver = new NamespaceResolver(logger);
|
|
110
|
+
this.platformVersion = options.platformVersion || '3.0.0';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Install a package with full dependency resolution and namespace checking.
|
|
115
|
+
*/
|
|
116
|
+
async install(
|
|
117
|
+
packageId: string,
|
|
118
|
+
version: string,
|
|
119
|
+
manifest: Record<string, unknown>,
|
|
120
|
+
): Promise<InstallResult> {
|
|
121
|
+
this.logger.info('Installing package', { packageId, version });
|
|
122
|
+
|
|
123
|
+
// 1. Check if already installed
|
|
124
|
+
if (this.packages.has(packageId)) {
|
|
125
|
+
const existing = this.packages.get(packageId)!;
|
|
126
|
+
if (existing.status === 'installed') {
|
|
127
|
+
return {
|
|
128
|
+
success: false,
|
|
129
|
+
packageId,
|
|
130
|
+
version,
|
|
131
|
+
installedDependencies: [],
|
|
132
|
+
namespaceConflicts: [],
|
|
133
|
+
errorMessage: `Package ${packageId}@${existing.version} is already installed. Use upgrade instead.`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 2. Check platform compatibility
|
|
139
|
+
const engine = (manifest as any).engine?.objectstack as string | undefined;
|
|
140
|
+
if (engine) {
|
|
141
|
+
const platformSemver = SemanticVersionManager.parse(this.platformVersion);
|
|
142
|
+
if (!SemanticVersionManager.satisfies(platformSemver, engine)) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
packageId,
|
|
146
|
+
version,
|
|
147
|
+
installedDependencies: [],
|
|
148
|
+
namespaceConflicts: [],
|
|
149
|
+
errorMessage: `Package requires platform ${engine}, but current platform is v${this.platformVersion}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Check namespace conflicts
|
|
155
|
+
const namespaces = this.namespaceResolver.extractNamespaces(manifest);
|
|
156
|
+
const nsCheck = this.namespaceResolver.checkAvailability(packageId, namespaces);
|
|
157
|
+
if (!nsCheck.available) {
|
|
158
|
+
return {
|
|
159
|
+
success: false,
|
|
160
|
+
packageId,
|
|
161
|
+
version,
|
|
162
|
+
installedDependencies: [],
|
|
163
|
+
namespaceConflicts: nsCheck.conflicts.map(c => ({
|
|
164
|
+
namespace: c.namespace,
|
|
165
|
+
existingPackageId: c.existingPackageId,
|
|
166
|
+
})),
|
|
167
|
+
errorMessage: `Namespace conflicts detected: ${nsCheck.conflicts.map(c => c.namespace).join(', ')}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 4. Resolve dependencies
|
|
172
|
+
const deps = (manifest as any).dependencies as Record<string, string> | undefined;
|
|
173
|
+
const depNames = deps ? Object.keys(deps) : [];
|
|
174
|
+
const missingDeps = depNames.filter(d => !this.packages.has(d));
|
|
175
|
+
if (missingDeps.length > 0) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
packageId,
|
|
179
|
+
version,
|
|
180
|
+
installedDependencies: [],
|
|
181
|
+
namespaceConflicts: [],
|
|
182
|
+
errorMessage: `Missing dependencies: ${missingDeps.join(', ')}`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Register package
|
|
187
|
+
this.packages.set(packageId, {
|
|
188
|
+
packageId,
|
|
189
|
+
version,
|
|
190
|
+
manifest,
|
|
191
|
+
installedAt: new Date().toISOString(),
|
|
192
|
+
status: 'installed',
|
|
193
|
+
namespaces,
|
|
194
|
+
dependencies: depNames,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// 6. Register namespaces
|
|
198
|
+
this.namespaceResolver.register(packageId, namespaces);
|
|
199
|
+
|
|
200
|
+
this.logger.info('Package installed', { packageId, version, namespaces: namespaces.length });
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
success: true,
|
|
204
|
+
packageId,
|
|
205
|
+
version,
|
|
206
|
+
installedDependencies: depNames,
|
|
207
|
+
namespaceConflicts: [],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Uninstall a package, checking for dependents first.
|
|
213
|
+
*/
|
|
214
|
+
async uninstall(packageId: string): Promise<{ success: boolean; errorMessage?: string }> {
|
|
215
|
+
const pkg = this.packages.get(packageId);
|
|
216
|
+
if (!pkg) {
|
|
217
|
+
return { success: false, errorMessage: `Package ${packageId} is not installed` };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if other packages depend on this one
|
|
221
|
+
const dependents: string[] = [];
|
|
222
|
+
for (const [id, record] of this.packages) {
|
|
223
|
+
if (id !== packageId && record.dependencies.includes(packageId)) {
|
|
224
|
+
dependents.push(id);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (dependents.length > 0) {
|
|
229
|
+
return {
|
|
230
|
+
success: false,
|
|
231
|
+
errorMessage: `Cannot uninstall ${packageId}: depended upon by ${dependents.join(', ')}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Remove namespaces and package
|
|
236
|
+
this.namespaceResolver.unregister(packageId);
|
|
237
|
+
this.packages.delete(packageId);
|
|
238
|
+
this.snapshots.delete(packageId);
|
|
239
|
+
|
|
240
|
+
this.logger.info('Package uninstalled', { packageId });
|
|
241
|
+
return { success: true };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Upgrade a package: snapshot → update → register.
|
|
246
|
+
*/
|
|
247
|
+
async upgrade(
|
|
248
|
+
packageId: string,
|
|
249
|
+
newVersion: string,
|
|
250
|
+
newManifest: Record<string, unknown>,
|
|
251
|
+
): Promise<UpgradeResult> {
|
|
252
|
+
const existing = this.packages.get(packageId);
|
|
253
|
+
if (!existing) {
|
|
254
|
+
return {
|
|
255
|
+
success: false,
|
|
256
|
+
packageId,
|
|
257
|
+
fromVersion: '',
|
|
258
|
+
toVersion: newVersion,
|
|
259
|
+
snapshot: { packageId, previousVersion: '', previousManifest: {}, previousNamespaces: [], installedAt: '', createdAt: new Date().toISOString() },
|
|
260
|
+
errorMessage: `Package ${packageId} is not installed`,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 1. Create snapshot for rollback
|
|
265
|
+
const snapshot: PackageSnapshot = {
|
|
266
|
+
packageId,
|
|
267
|
+
previousVersion: existing.version,
|
|
268
|
+
previousManifest: existing.manifest,
|
|
269
|
+
previousNamespaces: [...existing.namespaces],
|
|
270
|
+
installedAt: existing.installedAt,
|
|
271
|
+
createdAt: new Date().toISOString(),
|
|
272
|
+
};
|
|
273
|
+
this.snapshots.set(packageId, snapshot);
|
|
274
|
+
|
|
275
|
+
// 2. Check platform compatibility
|
|
276
|
+
const engine = (newManifest as any).engine?.objectstack as string | undefined;
|
|
277
|
+
if (engine) {
|
|
278
|
+
const platformSemver = SemanticVersionManager.parse(this.platformVersion);
|
|
279
|
+
if (!SemanticVersionManager.satisfies(platformSemver, engine)) {
|
|
280
|
+
return {
|
|
281
|
+
success: false,
|
|
282
|
+
packageId,
|
|
283
|
+
fromVersion: existing.version,
|
|
284
|
+
toVersion: newVersion,
|
|
285
|
+
snapshot,
|
|
286
|
+
errorMessage: `New version requires platform ${engine}, current is v${this.platformVersion}`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 3. Check namespace changes
|
|
292
|
+
const newNamespaces = this.namespaceResolver.extractNamespaces(newManifest);
|
|
293
|
+
// Temporarily remove old namespaces to check new ones
|
|
294
|
+
this.namespaceResolver.unregister(packageId);
|
|
295
|
+
const nsCheck = this.namespaceResolver.checkAvailability(packageId, newNamespaces);
|
|
296
|
+
if (!nsCheck.available) {
|
|
297
|
+
// Restore old namespaces on failure
|
|
298
|
+
this.namespaceResolver.register(packageId, existing.namespaces);
|
|
299
|
+
return {
|
|
300
|
+
success: false,
|
|
301
|
+
packageId,
|
|
302
|
+
fromVersion: existing.version,
|
|
303
|
+
toVersion: newVersion,
|
|
304
|
+
snapshot,
|
|
305
|
+
errorMessage: `Namespace conflicts in new version: ${nsCheck.conflicts.map(c => c.namespace).join(', ')}`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 4. Register new namespaces and update record
|
|
310
|
+
this.namespaceResolver.register(packageId, newNamespaces);
|
|
311
|
+
const deps = (newManifest as any).dependencies as Record<string, string> | undefined;
|
|
312
|
+
|
|
313
|
+
this.packages.set(packageId, {
|
|
314
|
+
packageId,
|
|
315
|
+
version: newVersion,
|
|
316
|
+
manifest: newManifest,
|
|
317
|
+
installedAt: existing.installedAt,
|
|
318
|
+
status: 'installed',
|
|
319
|
+
namespaces: newNamespaces,
|
|
320
|
+
dependencies: deps ? Object.keys(deps) : [],
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
this.logger.info('Package upgraded', { packageId, from: existing.version, to: newVersion });
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
success: true,
|
|
327
|
+
packageId,
|
|
328
|
+
fromVersion: existing.version,
|
|
329
|
+
toVersion: newVersion,
|
|
330
|
+
snapshot,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Rollback a package to its pre-upgrade snapshot.
|
|
336
|
+
*/
|
|
337
|
+
async rollback(packageId: string): Promise<RollbackResult> {
|
|
338
|
+
const snapshot = this.snapshots.get(packageId);
|
|
339
|
+
if (!snapshot) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
packageId,
|
|
343
|
+
restoredVersion: '',
|
|
344
|
+
errorMessage: `No upgrade snapshot found for ${packageId}`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Restore previous state
|
|
349
|
+
this.namespaceResolver.unregister(packageId);
|
|
350
|
+
this.namespaceResolver.register(packageId, snapshot.previousNamespaces);
|
|
351
|
+
|
|
352
|
+
const deps = (snapshot.previousManifest as any).dependencies as Record<string, string> | undefined;
|
|
353
|
+
this.packages.set(packageId, {
|
|
354
|
+
packageId,
|
|
355
|
+
version: snapshot.previousVersion,
|
|
356
|
+
manifest: snapshot.previousManifest,
|
|
357
|
+
installedAt: snapshot.installedAt,
|
|
358
|
+
status: 'installed',
|
|
359
|
+
namespaces: snapshot.previousNamespaces,
|
|
360
|
+
dependencies: deps ? Object.keys(deps) : [],
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
this.snapshots.delete(packageId);
|
|
364
|
+
|
|
365
|
+
this.logger.info('Package rolled back', { packageId, to: snapshot.previousVersion });
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
success: true,
|
|
369
|
+
packageId,
|
|
370
|
+
restoredVersion: snapshot.previousVersion,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get an installed package record.
|
|
376
|
+
*/
|
|
377
|
+
getPackage(packageId: string): InstalledPackageRecord | undefined {
|
|
378
|
+
return this.packages.get(packageId);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* List all installed packages.
|
|
383
|
+
*/
|
|
384
|
+
listPackages(): InstalledPackageRecord[] {
|
|
385
|
+
return Array.from(this.packages.values());
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resolve dependencies for a set of packages.
|
|
390
|
+
*/
|
|
391
|
+
resolveDependencies(
|
|
392
|
+
packages: Map<string, { version?: string; dependencies?: string[] }>,
|
|
393
|
+
): string[] {
|
|
394
|
+
return this.dependencyResolver.resolve(packages);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Check namespace availability for a package's metadata.
|
|
399
|
+
*/
|
|
400
|
+
checkNamespaces(packageId: string, config: Record<string, unknown>): {
|
|
401
|
+
available: boolean;
|
|
402
|
+
conflicts: Array<{ namespace: string; existingPackageId: string }>;
|
|
403
|
+
} {
|
|
404
|
+
const namespaces = this.namespaceResolver.extractNamespaces(config);
|
|
405
|
+
const result = this.namespaceResolver.checkAvailability(packageId, namespaces);
|
|
406
|
+
return {
|
|
407
|
+
available: result.available,
|
|
408
|
+
conflicts: result.conflicts.map(c => ({
|
|
409
|
+
namespace: c.namespace,
|
|
410
|
+
existingPackageId: c.existingPackageId,
|
|
411
|
+
})),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Get the namespace resolver instance.
|
|
417
|
+
*/
|
|
418
|
+
getNamespaceResolver(): NamespaceResolver {
|
|
419
|
+
return this.namespaceResolver;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Get a snapshot for a given package (if available).
|
|
424
|
+
*/
|
|
425
|
+
getSnapshot(packageId: string): PackageSnapshot | undefined {
|
|
426
|
+
return this.snapshots.get(packageId);
|
|
427
|
+
}
|
|
428
|
+
}
|