busy-cli 0.1.2
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/README.md +129 -0
- package/dist/builders/context.d.ts +50 -0
- package/dist/builders/context.d.ts.map +1 -0
- package/dist/builders/context.js +190 -0
- package/dist/cache/index.d.ts +100 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +270 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +463 -0
- package/dist/commands/package.d.ts +96 -0
- package/dist/commands/package.d.ts.map +1 -0
- package/dist/commands/package.js +285 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/loader.d.ts +6 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +361 -0
- package/dist/merge.d.ts +16 -0
- package/dist/merge.d.ts.map +1 -0
- package/dist/merge.js +102 -0
- package/dist/package/manifest.d.ts +59 -0
- package/dist/package/manifest.d.ts.map +1 -0
- package/dist/package/manifest.js +265 -0
- package/dist/parser.d.ts +28 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +220 -0
- package/dist/parsers/frontmatter.d.ts +14 -0
- package/dist/parsers/frontmatter.d.ts.map +1 -0
- package/dist/parsers/frontmatter.js +110 -0
- package/dist/parsers/imports.d.ts +48 -0
- package/dist/parsers/imports.d.ts.map +1 -0
- package/dist/parsers/imports.js +147 -0
- package/dist/parsers/links.d.ts +12 -0
- package/dist/parsers/links.d.ts.map +1 -0
- package/dist/parsers/links.js +79 -0
- package/dist/parsers/localdefs.d.ts +6 -0
- package/dist/parsers/localdefs.d.ts.map +1 -0
- package/dist/parsers/localdefs.js +132 -0
- package/dist/parsers/operations.d.ts +32 -0
- package/dist/parsers/operations.d.ts.map +1 -0
- package/dist/parsers/operations.js +313 -0
- package/dist/parsers/sections.d.ts +15 -0
- package/dist/parsers/sections.d.ts.map +1 -0
- package/dist/parsers/sections.js +173 -0
- package/dist/parsers/tools.d.ts +30 -0
- package/dist/parsers/tools.d.ts.map +1 -0
- package/dist/parsers/tools.js +178 -0
- package/dist/parsers/triggers.d.ts +35 -0
- package/dist/parsers/triggers.d.ts.map +1 -0
- package/dist/parsers/triggers.js +219 -0
- package/dist/providers/base.d.ts +60 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +34 -0
- package/dist/providers/github.d.ts +18 -0
- package/dist/providers/github.d.ts.map +1 -0
- package/dist/providers/github.js +109 -0
- package/dist/providers/gitlab.d.ts +18 -0
- package/dist/providers/gitlab.d.ts.map +1 -0
- package/dist/providers/gitlab.js +101 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +17 -0
- package/dist/providers/local.d.ts +31 -0
- package/dist/providers/local.d.ts.map +1 -0
- package/dist/providers/local.js +116 -0
- package/dist/providers/url.d.ts +16 -0
- package/dist/providers/url.d.ts.map +1 -0
- package/dist/providers/url.js +45 -0
- package/dist/registry/index.d.ts +99 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +320 -0
- package/dist/types/schema.d.ts +3259 -0
- package/dist/types/schema.d.ts.map +1 -0
- package/dist/types/schema.js +258 -0
- package/dist/utils/logger.d.ts +19 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +23 -0
- package/dist/utils/slugify.d.ts +14 -0
- package/dist/utils/slugify.d.ts.map +1 -0
- package/dist/utils/slugify.js +28 -0
- package/package.json +61 -0
- package/src/__tests__/cache.test.ts +393 -0
- package/src/__tests__/cli-package.test.ts +667 -0
- package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
- package/src/__tests__/fixtures/concept.busy.md +30 -0
- package/src/__tests__/fixtures/document.busy.md +44 -0
- package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
- package/src/__tests__/fixtures/tool-document.busy.md +71 -0
- package/src/__tests__/fixtures/tool.busy.md +54 -0
- package/src/__tests__/imports.test.ts +244 -0
- package/src/__tests__/integration.test.ts +432 -0
- package/src/__tests__/operations.test.ts +408 -0
- package/src/__tests__/package-manifest.test.ts +455 -0
- package/src/__tests__/providers.test.ts +672 -0
- package/src/__tests__/registry.test.ts +402 -0
- package/src/__tests__/schema.test.ts +467 -0
- package/src/__tests__/tools.test.ts +376 -0
- package/src/__tests__/triggers.test.ts +312 -0
- package/src/builders/context.ts +294 -0
- package/src/cache/index.ts +312 -0
- package/src/cli/index.ts +514 -0
- package/src/commands/package.ts +392 -0
- package/src/index.ts +46 -0
- package/src/loader.ts +474 -0
- package/src/merge.ts +126 -0
- package/src/package/manifest.ts +349 -0
- package/src/parser.ts +278 -0
- package/src/parsers/frontmatter.ts +135 -0
- package/src/parsers/imports.ts +196 -0
- package/src/parsers/links.ts +108 -0
- package/src/parsers/localdefs.ts +166 -0
- package/src/parsers/operations.ts +404 -0
- package/src/parsers/sections.ts +230 -0
- package/src/parsers/tools.ts +215 -0
- package/src/parsers/triggers.ts +252 -0
- package/src/providers/base.ts +77 -0
- package/src/providers/github.ts +129 -0
- package/src/providers/gitlab.ts +121 -0
- package/src/providers/index.ts +25 -0
- package/src/providers/local.ts +129 -0
- package/src/providers/url.ts +56 -0
- package/src/registry/index.ts +408 -0
- package/src/types/schema.ts +369 -0
- package/src/utils/logger.ts +25 -0
- package/src/utils/slugify.ts +31 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package Registry for package.busy.md
|
|
3
|
+
*
|
|
4
|
+
* Parses and manages the package registry file.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { promises as fs } from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import matter from 'gray-matter';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Package entry in the registry
|
|
13
|
+
*/
|
|
14
|
+
export interface PackageEntry {
|
|
15
|
+
id: string;
|
|
16
|
+
description: string;
|
|
17
|
+
source: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
cached: string;
|
|
20
|
+
version: string;
|
|
21
|
+
fetched: string;
|
|
22
|
+
integrity?: string;
|
|
23
|
+
category: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registry metadata from frontmatter
|
|
28
|
+
*/
|
|
29
|
+
export interface RegistryMetadata {
|
|
30
|
+
name: string;
|
|
31
|
+
type: string;
|
|
32
|
+
description: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parsed package registry
|
|
37
|
+
*/
|
|
38
|
+
export interface ParsedRegistry {
|
|
39
|
+
metadata: RegistryMetadata;
|
|
40
|
+
packages: PackageEntry[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Derive entry ID from URL
|
|
45
|
+
*
|
|
46
|
+
* If URL has #anchor, use anchor as ID
|
|
47
|
+
* Else, use filename without extension
|
|
48
|
+
* Slugify result (lowercase, hyphens)
|
|
49
|
+
*/
|
|
50
|
+
export function deriveEntryId(url: string): string {
|
|
51
|
+
// Check for anchor
|
|
52
|
+
const hashIndex = url.indexOf('#');
|
|
53
|
+
if (hashIndex !== -1) {
|
|
54
|
+
const anchor = url.slice(hashIndex + 1);
|
|
55
|
+
return slugify(anchor);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Use filename without extension
|
|
59
|
+
const urlPath = url.split('?')[0]; // Remove query params
|
|
60
|
+
const filename = path.basename(urlPath);
|
|
61
|
+
const nameWithoutExt = filename.replace(/\.busy\.md$/, '').replace(/\.md$/, '');
|
|
62
|
+
return slugify(nameWithoutExt);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Derive category from URL path
|
|
67
|
+
*/
|
|
68
|
+
export function deriveCategory(url: string): string {
|
|
69
|
+
const lowerUrl = url.toLowerCase();
|
|
70
|
+
|
|
71
|
+
if (lowerUrl.includes('/core/')) {
|
|
72
|
+
return 'Core Library';
|
|
73
|
+
}
|
|
74
|
+
if (lowerUrl.includes('/tools/')) {
|
|
75
|
+
return 'Tools';
|
|
76
|
+
}
|
|
77
|
+
if (lowerUrl.includes('/concepts/')) {
|
|
78
|
+
return 'Concepts';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return 'Packages';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Slugify a string (lowercase, replace spaces with hyphens)
|
|
86
|
+
*/
|
|
87
|
+
function slugify(str: string): string {
|
|
88
|
+
return str
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/\s+/g, '-')
|
|
91
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
92
|
+
.replace(/-+/g, '-')
|
|
93
|
+
.replace(/^-|-$/g, '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Parse a field/value table
|
|
98
|
+
*/
|
|
99
|
+
function parseFieldTable(content: string): Record<string, string> {
|
|
100
|
+
const fields: Record<string, string> = {};
|
|
101
|
+
|
|
102
|
+
// Match table rows: | Field | Value |
|
|
103
|
+
const rowPattern = /\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/g;
|
|
104
|
+
let match;
|
|
105
|
+
|
|
106
|
+
while ((match = rowPattern.exec(content)) !== null) {
|
|
107
|
+
const field = match[1].trim();
|
|
108
|
+
const value = match[2].trim();
|
|
109
|
+
|
|
110
|
+
// Skip header row
|
|
111
|
+
if (field === 'Field' || field.startsWith('-')) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
fields[field] = value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return fields;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Parse package.busy.md content
|
|
123
|
+
*/
|
|
124
|
+
export function parsePackageRegistry(content: string): ParsedRegistry {
|
|
125
|
+
// Extract only first frontmatter block to avoid "multiple documents" error
|
|
126
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
127
|
+
let frontmatter: Record<string, any> = {};
|
|
128
|
+
let body = content;
|
|
129
|
+
if (frontmatterMatch) {
|
|
130
|
+
const { data } = matter(frontmatterMatch[0]);
|
|
131
|
+
frontmatter = data;
|
|
132
|
+
body = content.slice(frontmatterMatch[0].length);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const metadata: RegistryMetadata = {
|
|
136
|
+
name: frontmatter.Name || frontmatter.name || 'package',
|
|
137
|
+
type: frontmatter.Type || frontmatter.type || 'Document',
|
|
138
|
+
description: frontmatter.Description || frontmatter.description || '',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const packages: PackageEntry[] = [];
|
|
142
|
+
|
|
143
|
+
// Find Dependencies section (or legacy Package Registry section)
|
|
144
|
+
const registryMatch = body.match(/# (?:Dependencies|Package Registry)\n([\s\S]*?)(?=\n# |$)/);
|
|
145
|
+
if (!registryMatch) {
|
|
146
|
+
return { metadata, packages };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const registryContent = registryMatch[1];
|
|
150
|
+
|
|
151
|
+
// Parse entries directly under Dependencies (H2 headers) or in categories (H3 headers)
|
|
152
|
+
// Try H2 entries first (new format: ### package-name under # Dependencies)
|
|
153
|
+
const h2EntryPattern = /## ([^\n]+)\n([\s\S]*?)(?=\n## |$)/g;
|
|
154
|
+
let h2Match;
|
|
155
|
+
|
|
156
|
+
while ((h2Match = h2EntryPattern.exec(registryContent)) !== null) {
|
|
157
|
+
const entryId = h2Match[1].trim();
|
|
158
|
+
const entryContent = h2Match[2];
|
|
159
|
+
|
|
160
|
+
// Check if this is a category with H3 entries or a direct package entry
|
|
161
|
+
if (entryContent.includes('### ')) {
|
|
162
|
+
// This is a category, parse H3 entries
|
|
163
|
+
const category = entryId;
|
|
164
|
+
const entryPattern = /### ([^\n]+)\n([\s\S]*?)(?=\n### |$)/g;
|
|
165
|
+
let entryMatch;
|
|
166
|
+
|
|
167
|
+
while ((entryMatch = entryPattern.exec(entryContent)) !== null) {
|
|
168
|
+
const pkgId = entryMatch[1].trim();
|
|
169
|
+
const pkgContent = entryMatch[2];
|
|
170
|
+
|
|
171
|
+
const descMatch = pkgContent.match(/^([\s\S]*?)(?=\n\|)/);
|
|
172
|
+
const description = descMatch ? descMatch[1].trim() : '';
|
|
173
|
+
|
|
174
|
+
const fields = parseFieldTable(pkgContent);
|
|
175
|
+
|
|
176
|
+
if (fields['Source']) {
|
|
177
|
+
packages.push({
|
|
178
|
+
id: pkgId,
|
|
179
|
+
description,
|
|
180
|
+
source: fields['Source'],
|
|
181
|
+
provider: fields['Provider'] || 'url',
|
|
182
|
+
cached: fields['Cached'] || '',
|
|
183
|
+
version: fields['Version'] || '',
|
|
184
|
+
fetched: fields['Fetched'] || '',
|
|
185
|
+
integrity: fields['Integrity'] || undefined,
|
|
186
|
+
category,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
// This is a direct package entry (no category)
|
|
192
|
+
const descMatch = entryContent.match(/^([\s\S]*?)(?=\n\|)/);
|
|
193
|
+
const description = descMatch ? descMatch[1].trim() : '';
|
|
194
|
+
|
|
195
|
+
const fields = parseFieldTable(entryContent);
|
|
196
|
+
|
|
197
|
+
if (fields['Source']) {
|
|
198
|
+
packages.push({
|
|
199
|
+
id: entryId,
|
|
200
|
+
description,
|
|
201
|
+
source: fields['Source'],
|
|
202
|
+
provider: fields['Provider'] || 'url',
|
|
203
|
+
cached: fields['Cached'] || '',
|
|
204
|
+
version: fields['Version'] || '',
|
|
205
|
+
fetched: fields['Fetched'] || '',
|
|
206
|
+
integrity: fields['Integrity'] || undefined,
|
|
207
|
+
category: 'Packages',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { metadata, packages };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Generate package.busy.md content
|
|
218
|
+
*/
|
|
219
|
+
function generateRegistryContent(
|
|
220
|
+
metadata: RegistryMetadata,
|
|
221
|
+
packages: PackageEntry[]
|
|
222
|
+
): string {
|
|
223
|
+
const lines: string[] = [];
|
|
224
|
+
|
|
225
|
+
// Frontmatter
|
|
226
|
+
lines.push('---');
|
|
227
|
+
lines.push(`Name: ${metadata.name}`);
|
|
228
|
+
lines.push(`Type: ${metadata.type}`);
|
|
229
|
+
lines.push(`Description: ${metadata.description}`);
|
|
230
|
+
lines.push('---');
|
|
231
|
+
lines.push('');
|
|
232
|
+
|
|
233
|
+
// Imports section
|
|
234
|
+
lines.push('# [Imports]');
|
|
235
|
+
lines.push('');
|
|
236
|
+
|
|
237
|
+
// Package Contents section
|
|
238
|
+
lines.push('# Package Contents');
|
|
239
|
+
lines.push('');
|
|
240
|
+
lines.push('No local documents yet.');
|
|
241
|
+
lines.push('');
|
|
242
|
+
|
|
243
|
+
// Dependencies section
|
|
244
|
+
lines.push('# Dependencies');
|
|
245
|
+
lines.push('');
|
|
246
|
+
|
|
247
|
+
if (packages.length === 0) {
|
|
248
|
+
lines.push('No dependencies installed.');
|
|
249
|
+
lines.push('');
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Group packages by category
|
|
254
|
+
const categories = new Map<string, PackageEntry[]>();
|
|
255
|
+
|
|
256
|
+
for (const pkg of packages) {
|
|
257
|
+
if (!categories.has(pkg.category)) {
|
|
258
|
+
categories.set(pkg.category, []);
|
|
259
|
+
}
|
|
260
|
+
categories.get(pkg.category)!.push(pkg);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Render each category
|
|
264
|
+
for (const [category, pkgs] of categories) {
|
|
265
|
+
lines.push(`## ${category}`);
|
|
266
|
+
lines.push('');
|
|
267
|
+
|
|
268
|
+
for (const pkg of pkgs) {
|
|
269
|
+
lines.push(`### ${pkg.id}`);
|
|
270
|
+
lines.push('');
|
|
271
|
+
if (pkg.description) {
|
|
272
|
+
lines.push(pkg.description);
|
|
273
|
+
lines.push('');
|
|
274
|
+
}
|
|
275
|
+
lines.push('| Field | Value |');
|
|
276
|
+
lines.push('|-------|-------|');
|
|
277
|
+
lines.push(`| Source | ${pkg.source} |`);
|
|
278
|
+
lines.push(`| Provider | ${pkg.provider} |`);
|
|
279
|
+
lines.push(`| Cached | ${pkg.cached} |`);
|
|
280
|
+
lines.push(`| Version | ${pkg.version} |`);
|
|
281
|
+
lines.push(`| Fetched | ${pkg.fetched} |`);
|
|
282
|
+
if (pkg.integrity) {
|
|
283
|
+
lines.push(`| Integrity | ${pkg.integrity} |`);
|
|
284
|
+
}
|
|
285
|
+
lines.push('');
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return lines.join('\n');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const DEFAULT_REGISTRY_CONTENT = `---
|
|
293
|
+
Name: workspace
|
|
294
|
+
Type: [Package]
|
|
295
|
+
Description: BUSY workspace package manifest
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
# [Imports]
|
|
299
|
+
|
|
300
|
+
# Package Contents
|
|
301
|
+
|
|
302
|
+
No local documents yet.
|
|
303
|
+
|
|
304
|
+
# Dependencies
|
|
305
|
+
|
|
306
|
+
No dependencies installed.
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Package Registry Manager
|
|
311
|
+
*
|
|
312
|
+
* Manages the package.busy.md file in a workspace.
|
|
313
|
+
*/
|
|
314
|
+
export class PackageRegistry {
|
|
315
|
+
private workspaceRoot: string;
|
|
316
|
+
private registryPath: string;
|
|
317
|
+
private metadata: RegistryMetadata;
|
|
318
|
+
private packages: Map<string, PackageEntry>;
|
|
319
|
+
|
|
320
|
+
constructor(workspaceRoot: string) {
|
|
321
|
+
this.workspaceRoot = workspaceRoot;
|
|
322
|
+
this.registryPath = path.join(workspaceRoot, 'package.busy.md');
|
|
323
|
+
this.metadata = {
|
|
324
|
+
name: 'workspace',
|
|
325
|
+
type: '[Package]',
|
|
326
|
+
description: 'BUSY workspace package manifest',
|
|
327
|
+
};
|
|
328
|
+
this.packages = new Map();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Initialize a new package.busy.md if it doesn't exist
|
|
333
|
+
*/
|
|
334
|
+
async init(): Promise<void> {
|
|
335
|
+
const exists = await fs.stat(this.registryPath).then(() => true).catch(() => false);
|
|
336
|
+
if (!exists) {
|
|
337
|
+
await fs.writeFile(this.registryPath, DEFAULT_REGISTRY_CONTENT, 'utf-8');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Load existing package.busy.md
|
|
343
|
+
*/
|
|
344
|
+
async load(): Promise<void> {
|
|
345
|
+
const content = await fs.readFile(this.registryPath, 'utf-8');
|
|
346
|
+
const parsed = parsePackageRegistry(content);
|
|
347
|
+
|
|
348
|
+
this.metadata = parsed.metadata;
|
|
349
|
+
this.packages = new Map(parsed.packages.map(p => [p.id, p]));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Save changes to package.busy.md
|
|
354
|
+
*/
|
|
355
|
+
async save(): Promise<void> {
|
|
356
|
+
const content = generateRegistryContent(
|
|
357
|
+
this.metadata,
|
|
358
|
+
Array.from(this.packages.values())
|
|
359
|
+
);
|
|
360
|
+
await fs.writeFile(this.registryPath, content, 'utf-8');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get all packages
|
|
365
|
+
*/
|
|
366
|
+
getPackages(): PackageEntry[] {
|
|
367
|
+
return Array.from(this.packages.values());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Get package by ID
|
|
372
|
+
*/
|
|
373
|
+
getPackage(id: string): PackageEntry | undefined {
|
|
374
|
+
return this.packages.get(id);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Add or update a package
|
|
379
|
+
*/
|
|
380
|
+
addPackage(entry: PackageEntry): void {
|
|
381
|
+
this.packages.set(entry.id, entry);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Remove a package by ID
|
|
386
|
+
*/
|
|
387
|
+
removePackage(id: string): boolean {
|
|
388
|
+
return this.packages.delete(id);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get packages by category
|
|
393
|
+
*/
|
|
394
|
+
getPackagesByCategory(category: string): PackageEntry[] {
|
|
395
|
+
return Array.from(this.packages.values()).filter(p => p.category === category);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Get all unique categories
|
|
400
|
+
*/
|
|
401
|
+
getCategories(): string[] {
|
|
402
|
+
const categories = new Set<string>();
|
|
403
|
+
for (const pkg of this.packages.values()) {
|
|
404
|
+
categories.add(pkg.category);
|
|
405
|
+
}
|
|
406
|
+
return Array.from(categories);
|
|
407
|
+
}
|
|
408
|
+
}
|