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.
Files changed (128) hide show
  1. package/README.md +129 -0
  2. package/dist/builders/context.d.ts +50 -0
  3. package/dist/builders/context.d.ts.map +1 -0
  4. package/dist/builders/context.js +190 -0
  5. package/dist/cache/index.d.ts +100 -0
  6. package/dist/cache/index.d.ts.map +1 -0
  7. package/dist/cache/index.js +270 -0
  8. package/dist/cli/index.d.ts +3 -0
  9. package/dist/cli/index.d.ts.map +1 -0
  10. package/dist/cli/index.js +463 -0
  11. package/dist/commands/package.d.ts +96 -0
  12. package/dist/commands/package.d.ts.map +1 -0
  13. package/dist/commands/package.js +285 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +7 -0
  17. package/dist/loader.d.ts +6 -0
  18. package/dist/loader.d.ts.map +1 -0
  19. package/dist/loader.js +361 -0
  20. package/dist/merge.d.ts +16 -0
  21. package/dist/merge.d.ts.map +1 -0
  22. package/dist/merge.js +102 -0
  23. package/dist/package/manifest.d.ts +59 -0
  24. package/dist/package/manifest.d.ts.map +1 -0
  25. package/dist/package/manifest.js +265 -0
  26. package/dist/parser.d.ts +28 -0
  27. package/dist/parser.d.ts.map +1 -0
  28. package/dist/parser.js +220 -0
  29. package/dist/parsers/frontmatter.d.ts +14 -0
  30. package/dist/parsers/frontmatter.d.ts.map +1 -0
  31. package/dist/parsers/frontmatter.js +110 -0
  32. package/dist/parsers/imports.d.ts +48 -0
  33. package/dist/parsers/imports.d.ts.map +1 -0
  34. package/dist/parsers/imports.js +147 -0
  35. package/dist/parsers/links.d.ts +12 -0
  36. package/dist/parsers/links.d.ts.map +1 -0
  37. package/dist/parsers/links.js +79 -0
  38. package/dist/parsers/localdefs.d.ts +6 -0
  39. package/dist/parsers/localdefs.d.ts.map +1 -0
  40. package/dist/parsers/localdefs.js +132 -0
  41. package/dist/parsers/operations.d.ts +32 -0
  42. package/dist/parsers/operations.d.ts.map +1 -0
  43. package/dist/parsers/operations.js +313 -0
  44. package/dist/parsers/sections.d.ts +15 -0
  45. package/dist/parsers/sections.d.ts.map +1 -0
  46. package/dist/parsers/sections.js +173 -0
  47. package/dist/parsers/tools.d.ts +30 -0
  48. package/dist/parsers/tools.d.ts.map +1 -0
  49. package/dist/parsers/tools.js +178 -0
  50. package/dist/parsers/triggers.d.ts +35 -0
  51. package/dist/parsers/triggers.d.ts.map +1 -0
  52. package/dist/parsers/triggers.js +219 -0
  53. package/dist/providers/base.d.ts +60 -0
  54. package/dist/providers/base.d.ts.map +1 -0
  55. package/dist/providers/base.js +34 -0
  56. package/dist/providers/github.d.ts +18 -0
  57. package/dist/providers/github.d.ts.map +1 -0
  58. package/dist/providers/github.js +109 -0
  59. package/dist/providers/gitlab.d.ts +18 -0
  60. package/dist/providers/gitlab.d.ts.map +1 -0
  61. package/dist/providers/gitlab.js +101 -0
  62. package/dist/providers/index.d.ts +13 -0
  63. package/dist/providers/index.d.ts.map +1 -0
  64. package/dist/providers/index.js +17 -0
  65. package/dist/providers/local.d.ts +31 -0
  66. package/dist/providers/local.d.ts.map +1 -0
  67. package/dist/providers/local.js +116 -0
  68. package/dist/providers/url.d.ts +16 -0
  69. package/dist/providers/url.d.ts.map +1 -0
  70. package/dist/providers/url.js +45 -0
  71. package/dist/registry/index.d.ts +99 -0
  72. package/dist/registry/index.d.ts.map +1 -0
  73. package/dist/registry/index.js +320 -0
  74. package/dist/types/schema.d.ts +3259 -0
  75. package/dist/types/schema.d.ts.map +1 -0
  76. package/dist/types/schema.js +258 -0
  77. package/dist/utils/logger.d.ts +19 -0
  78. package/dist/utils/logger.d.ts.map +1 -0
  79. package/dist/utils/logger.js +23 -0
  80. package/dist/utils/slugify.d.ts +14 -0
  81. package/dist/utils/slugify.d.ts.map +1 -0
  82. package/dist/utils/slugify.js +28 -0
  83. package/package.json +61 -0
  84. package/src/__tests__/cache.test.ts +393 -0
  85. package/src/__tests__/cli-package.test.ts +667 -0
  86. package/src/__tests__/fixtures/automated-workflow.busy.md +84 -0
  87. package/src/__tests__/fixtures/concept.busy.md +30 -0
  88. package/src/__tests__/fixtures/document.busy.md +44 -0
  89. package/src/__tests__/fixtures/simple-operation.busy.md +45 -0
  90. package/src/__tests__/fixtures/tool-document.busy.md +71 -0
  91. package/src/__tests__/fixtures/tool.busy.md +54 -0
  92. package/src/__tests__/imports.test.ts +244 -0
  93. package/src/__tests__/integration.test.ts +432 -0
  94. package/src/__tests__/operations.test.ts +408 -0
  95. package/src/__tests__/package-manifest.test.ts +455 -0
  96. package/src/__tests__/providers.test.ts +672 -0
  97. package/src/__tests__/registry.test.ts +402 -0
  98. package/src/__tests__/schema.test.ts +467 -0
  99. package/src/__tests__/tools.test.ts +376 -0
  100. package/src/__tests__/triggers.test.ts +312 -0
  101. package/src/builders/context.ts +294 -0
  102. package/src/cache/index.ts +312 -0
  103. package/src/cli/index.ts +514 -0
  104. package/src/commands/package.ts +392 -0
  105. package/src/index.ts +46 -0
  106. package/src/loader.ts +474 -0
  107. package/src/merge.ts +126 -0
  108. package/src/package/manifest.ts +349 -0
  109. package/src/parser.ts +278 -0
  110. package/src/parsers/frontmatter.ts +135 -0
  111. package/src/parsers/imports.ts +196 -0
  112. package/src/parsers/links.ts +108 -0
  113. package/src/parsers/localdefs.ts +166 -0
  114. package/src/parsers/operations.ts +404 -0
  115. package/src/parsers/sections.ts +230 -0
  116. package/src/parsers/tools.ts +215 -0
  117. package/src/parsers/triggers.ts +252 -0
  118. package/src/providers/base.ts +77 -0
  119. package/src/providers/github.ts +129 -0
  120. package/src/providers/gitlab.ts +121 -0
  121. package/src/providers/index.ts +25 -0
  122. package/src/providers/local.ts +129 -0
  123. package/src/providers/url.ts +56 -0
  124. package/src/registry/index.ts +408 -0
  125. package/src/types/schema.ts +369 -0
  126. package/src/utils/logger.ts +25 -0
  127. package/src/utils/slugify.ts +31 -0
  128. package/tsconfig.json +21 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAoE,MAAM,mBAAmB,CAAC;AAG3G;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI,CAiGjD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,GAAG,IAAI,CAGpE;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAOnD"}
package/dist/merge.js ADDED
@@ -0,0 +1,102 @@
1
+ import { debug } from './utils/logger.js';
2
+ /**
3
+ * Merge multiple repos into a single repo
4
+ * Later repos override earlier ones when there are conflicts
5
+ */
6
+ export function mergeRepos(...repos) {
7
+ if (repos.length === 0) {
8
+ throw new Error('At least one repo is required');
9
+ }
10
+ if (repos.length === 1) {
11
+ return repos[0];
12
+ }
13
+ debug.parser('Merging %d repos', repos.length);
14
+ const merged = {
15
+ files: [],
16
+ concepts: [],
17
+ localdefs: {},
18
+ operations: {},
19
+ imports: [],
20
+ byId: {},
21
+ byFile: {},
22
+ edges: [],
23
+ };
24
+ // Merge files (later repos override)
25
+ const filesByDocId = new Map();
26
+ for (const repo of repos) {
27
+ for (const file of repo.files) {
28
+ filesByDocId.set(file.docId, file);
29
+ }
30
+ }
31
+ merged.files = Array.from(filesByDocId.values());
32
+ // Merge concepts (later repos override)
33
+ const conceptsById = new Map();
34
+ for (const repo of repos) {
35
+ for (const concept of repo.concepts) {
36
+ conceptsById.set(concept.id, concept);
37
+ }
38
+ }
39
+ merged.concepts = Array.from(conceptsById.values());
40
+ // Merge localdefs (later repos override)
41
+ for (const repo of repos) {
42
+ Object.assign(merged.localdefs, repo.localdefs);
43
+ }
44
+ // Merge operations (later repos override)
45
+ for (const repo of repos) {
46
+ Object.assign(merged.operations, repo.operations);
47
+ }
48
+ // Merge imports (append all, handle duplicates by ID)
49
+ const importsByDocId = new Map();
50
+ for (const repo of repos) {
51
+ for (const imp of repo.imports) {
52
+ if (!importsByDocId.has(imp.docId)) {
53
+ importsByDocId.set(imp.docId, new Map());
54
+ }
55
+ importsByDocId.get(imp.docId).set(imp.id, imp);
56
+ }
57
+ }
58
+ for (const docImports of importsByDocId.values()) {
59
+ merged.imports.push(...docImports.values());
60
+ }
61
+ // Merge byId (later repos override)
62
+ for (const repo of repos) {
63
+ Object.assign(merged.byId, repo.byId);
64
+ }
65
+ // Merge byFile (later repos override)
66
+ for (const repo of repos) {
67
+ Object.assign(merged.byFile, repo.byFile);
68
+ }
69
+ // Merge edges (append all, deduplicate)
70
+ const edgeSet = new Set();
71
+ for (const repo of repos) {
72
+ for (const edge of repo.edges) {
73
+ const key = `${edge.from}→${edge.to}:${edge.role}`;
74
+ if (!edgeSet.has(key)) {
75
+ edgeSet.add(key);
76
+ merged.edges.push(edge);
77
+ }
78
+ }
79
+ }
80
+ debug.parser('Merged repo: %d files, %d concepts, %d localdefs, %d operations, %d imports, %d edges', merged.files.length, merged.concepts.length, Object.keys(merged.localdefs).length, Object.keys(merged.operations).length, merged.imports.length, merged.edges.length);
81
+ return merged;
82
+ }
83
+ /**
84
+ * Extend a base repo with additional files
85
+ * This is a convenience wrapper around mergeRepos
86
+ */
87
+ export function extendRepo(baseRepo, extensionRepo) {
88
+ debug.parser('Extending base repo with extension');
89
+ return mergeRepos(baseRepo, extensionRepo);
90
+ }
91
+ /**
92
+ * Load a repo from JSON and validate it against the schema
93
+ */
94
+ export function loadRepoFromJSON(json) {
95
+ const data = JSON.parse(json);
96
+ // Basic validation - could use Zod here but keeping it simple for now
97
+ if (!data.files || !data.concepts) {
98
+ throw new Error('Invalid repo JSON: missing required fields');
99
+ }
100
+ return data;
101
+ }
102
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Package Manifest Parser
3
+ *
4
+ * Parses package.busy.md files that define a package's contents.
5
+ * A package manifest lists all documents that are part of the package.
6
+ */
7
+ import '../providers/local.js';
8
+ import '../providers/github.js';
9
+ import '../providers/gitlab.js';
10
+ import '../providers/url.js';
11
+ /**
12
+ * A document listed in a package manifest
13
+ */
14
+ export interface PackageDocument {
15
+ name: string;
16
+ relativePath: string;
17
+ type?: string;
18
+ anchor?: string;
19
+ description?: string;
20
+ category?: string;
21
+ }
22
+ /**
23
+ * Parsed package manifest
24
+ */
25
+ export interface PackageManifest {
26
+ name: string;
27
+ type: string;
28
+ version: string;
29
+ description: string;
30
+ documents: PackageDocument[];
31
+ }
32
+ /**
33
+ * Result of fetching a package from manifest
34
+ */
35
+ export interface FetchPackageResult {
36
+ name: string;
37
+ version: string;
38
+ description: string;
39
+ documents: PackageDocument[];
40
+ cached: string;
41
+ integrity: string;
42
+ }
43
+ /**
44
+ * Check if a URL/path points to a package.busy.md manifest or a directory containing one
45
+ */
46
+ export declare function isPackageManifestUrl(url: string): boolean;
47
+ /**
48
+ * Parse a package manifest (package.busy.md content)
49
+ *
50
+ * Supports two formats:
51
+ * 1. Table-based: H3 headers with Field/Value tables (Path, Type, Description)
52
+ * 2. Link-based: Markdown links like - [Name](./path) - Description
53
+ */
54
+ export declare function parsePackageManifest(content: string): PackageManifest;
55
+ /**
56
+ * Fetch a package from its manifest URL or local path
57
+ */
58
+ export declare function fetchPackageFromManifest(workspaceRoot: string, manifestUrl: string): Promise<FetchPackageResult>;
59
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/package/manifest.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAUH,OAAO,uBAAuB,CAAC;AAC/B,OAAO,wBAAwB,CAAC;AAChC,OAAO,wBAAwB,CAAC;AAChC,OAAO,qBAAqB,CAAC;AAE7B;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,eAAe,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAazD;AAyBD;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAiHrE;AAkBD;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,kBAAkB,CAAC,CA4G7B"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Package Manifest Parser
3
+ *
4
+ * Parses package.busy.md files that define a package's contents.
5
+ * A package manifest lists all documents that are part of the package.
6
+ */
7
+ import { promises as fs } from 'node:fs';
8
+ import * as path from 'node:path';
9
+ import matter from 'gray-matter';
10
+ import { CacheManager, calculateIntegrity } from '../cache/index.js';
11
+ import { PackageRegistry } from '../registry/index.js';
12
+ import { providerRegistry } from '../providers/index.js';
13
+ // Ensure providers are registered
14
+ import '../providers/local.js';
15
+ import '../providers/github.js';
16
+ import '../providers/gitlab.js';
17
+ import '../providers/url.js';
18
+ /**
19
+ * Check if a URL/path points to a package.busy.md manifest or a directory containing one
20
+ */
21
+ export function isPackageManifestUrl(url) {
22
+ // Explicit package.busy.md reference
23
+ if (url.endsWith('/package.busy.md') || url.includes('/package.busy.md#')) {
24
+ return true;
25
+ }
26
+ if (url.endsWith('package.busy.md')) {
27
+ return true;
28
+ }
29
+ // Local path that might be a directory - check if it looks like a local path without extension
30
+ if ((url.startsWith('./') || url.startsWith('../') || url.startsWith('/')) && !url.endsWith('.md') && !url.endsWith('.busy')) {
31
+ return true; // Treat as potential package directory
32
+ }
33
+ return false;
34
+ }
35
+ /**
36
+ * Parse a Field/Value table and return a record
37
+ */
38
+ function parseFieldValueTable(tableContent) {
39
+ const fields = {};
40
+ const rowPattern = /\|\s*([^|]+?)\s*\|\s*([^|]+?)\s*\|/g;
41
+ let match;
42
+ while ((match = rowPattern.exec(tableContent)) !== null) {
43
+ const field = match[1].trim();
44
+ const value = match[2].trim();
45
+ // Skip header row and separator
46
+ if (field === 'Field' || field.startsWith('-')) {
47
+ continue;
48
+ }
49
+ fields[field] = value;
50
+ }
51
+ return fields;
52
+ }
53
+ /**
54
+ * Parse a package manifest (package.busy.md content)
55
+ *
56
+ * Supports two formats:
57
+ * 1. Table-based: H3 headers with Field/Value tables (Path, Type, Description)
58
+ * 2. Link-based: Markdown links like - [Name](./path) - Description
59
+ */
60
+ export function parsePackageManifest(content) {
61
+ // Extract only first frontmatter block to avoid "multiple documents" error
62
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
63
+ let frontmatter = {};
64
+ let body = content;
65
+ if (frontmatterMatch) {
66
+ const { data } = matter(frontmatterMatch[0]);
67
+ frontmatter = data;
68
+ body = content.slice(frontmatterMatch[0].length);
69
+ }
70
+ const manifest = {
71
+ name: frontmatter.Name || frontmatter.name || 'unknown',
72
+ type: frontmatter.Type || frontmatter.type || 'Package',
73
+ version: frontmatter.Version || frontmatter.version || 'latest',
74
+ description: frontmatter.Description || frontmatter.description || '',
75
+ documents: [],
76
+ };
77
+ // Find Package Contents section
78
+ const contentsMatch = body.match(/# Package Contents\n([\s\S]*?)(?=\n# |$)/);
79
+ if (!contentsMatch) {
80
+ return manifest;
81
+ }
82
+ const contentsBody = contentsMatch[1];
83
+ let currentCategory;
84
+ // Parse categories (H2 headers) and entries (H3 headers with tables)
85
+ const categoryPattern = /## ([^\n]+)\n([\s\S]*?)(?=\n## |$)/g;
86
+ let categoryMatch;
87
+ while ((categoryMatch = categoryPattern.exec(contentsBody)) !== null) {
88
+ currentCategory = categoryMatch[1].trim();
89
+ const categoryContent = categoryMatch[2];
90
+ // Parse entries (H3 headers with tables)
91
+ const entryPattern = /### ([^\n]+)\n([\s\S]*?)(?=\n### |$)/g;
92
+ let entryMatch;
93
+ while ((entryMatch = entryPattern.exec(categoryContent)) !== null) {
94
+ const name = entryMatch[1].trim();
95
+ const entryContent = entryMatch[2];
96
+ // Check if this entry uses table format (has | Path |)
97
+ if (entryContent.includes('| Path |') || entryContent.includes('| Path |')) {
98
+ const fields = parseFieldValueTable(entryContent);
99
+ if (fields['Path']) {
100
+ let relativePath = fields['Path'];
101
+ let anchor;
102
+ // Extract anchor if present
103
+ const anchorIndex = relativePath.indexOf('#');
104
+ if (anchorIndex !== -1) {
105
+ anchor = relativePath.slice(anchorIndex + 1).toLowerCase();
106
+ relativePath = relativePath.slice(0, anchorIndex);
107
+ }
108
+ manifest.documents.push({
109
+ name,
110
+ relativePath,
111
+ type: fields['Type'],
112
+ description: fields['Description'],
113
+ anchor,
114
+ category: currentCategory,
115
+ });
116
+ }
117
+ }
118
+ }
119
+ }
120
+ // If no table-based entries found, try link-based format
121
+ if (manifest.documents.length === 0) {
122
+ const lines = body.split('\n');
123
+ currentCategory = undefined;
124
+ for (const line of lines) {
125
+ // Check for H2 headers (categories)
126
+ const h2Match = line.match(/^## (.+)$/);
127
+ if (h2Match) {
128
+ currentCategory = h2Match[1].trim();
129
+ continue;
130
+ }
131
+ // Check for markdown links in list items
132
+ // Format: - [Name](./relative/path.md) - Description
133
+ const linkMatch = line.match(/^-\s*\[([^\]]+)\]\(([^)]+)\)(?:\s*-\s*(.+))?$/);
134
+ if (linkMatch) {
135
+ const name = linkMatch[1].trim();
136
+ let relativePath = linkMatch[2].trim();
137
+ const description = linkMatch[3]?.trim();
138
+ // Extract anchor if present
139
+ let anchor;
140
+ const anchorIndex = relativePath.indexOf('#');
141
+ if (anchorIndex !== -1) {
142
+ anchor = relativePath.slice(anchorIndex + 1).toLowerCase();
143
+ relativePath = relativePath.slice(0, anchorIndex);
144
+ }
145
+ manifest.documents.push({
146
+ name,
147
+ relativePath,
148
+ anchor,
149
+ description,
150
+ category: currentCategory,
151
+ });
152
+ }
153
+ }
154
+ }
155
+ return manifest;
156
+ }
157
+ /**
158
+ * Resolve a relative path from a manifest to an absolute URL
159
+ */
160
+ function resolveDocumentUrl(manifestUrl, relativePath) {
161
+ // Get the base URL (directory containing the manifest)
162
+ const baseUrl = manifestUrl.substring(0, manifestUrl.lastIndexOf('/'));
163
+ // Handle ./ prefix
164
+ let cleanPath = relativePath;
165
+ if (cleanPath.startsWith('./')) {
166
+ cleanPath = cleanPath.slice(2);
167
+ }
168
+ return `${baseUrl}/${cleanPath}`;
169
+ }
170
+ /**
171
+ * Fetch a package from its manifest URL or local path
172
+ */
173
+ export async function fetchPackageFromManifest(workspaceRoot, manifestUrl) {
174
+ // Find provider for the manifest URL
175
+ const provider = providerRegistry.findProvider(manifestUrl);
176
+ if (!provider) {
177
+ throw new Error(`No provider found for URL: ${manifestUrl}`);
178
+ }
179
+ // Parse the manifest URL to get raw URL
180
+ const parsedManifest = provider.parse(manifestUrl);
181
+ // Fetch the manifest content
182
+ const manifestContent = await provider.fetch(manifestUrl);
183
+ // Parse the manifest
184
+ const manifest = parsePackageManifest(manifestContent);
185
+ // Initialize cache
186
+ const cache = new CacheManager(workspaceRoot);
187
+ await cache.init();
188
+ // Calculate the base path/URL for resolving relative paths
189
+ const rawManifestUrl = provider.getRawUrl(parsedManifest);
190
+ const basePath = rawManifestUrl.substring(0, rawManifestUrl.lastIndexOf('/'));
191
+ const isLocal = provider.name === 'local';
192
+ // Fetch all documents listed in the manifest
193
+ const fetchedDocs = [];
194
+ let combinedContent = '';
195
+ for (const doc of manifest.documents) {
196
+ // Resolve the document path
197
+ let docRelativePath = doc.relativePath;
198
+ if (docRelativePath.startsWith('./')) {
199
+ docRelativePath = docRelativePath.slice(2);
200
+ }
201
+ const docPath = `${basePath}/${docRelativePath}`;
202
+ try {
203
+ let content;
204
+ if (isLocal) {
205
+ // Use local file system for local packages
206
+ content = await fs.readFile(docPath, 'utf-8');
207
+ }
208
+ else {
209
+ // Use fetch for remote packages
210
+ const response = await fetch(docPath);
211
+ if (!response.ok) {
212
+ console.warn(`Warning: Failed to fetch ${docPath}: ${response.status}`);
213
+ continue;
214
+ }
215
+ content = await response.text();
216
+ }
217
+ combinedContent += content;
218
+ // Save to cache under package name
219
+ const cachePath = path.join(manifest.name, docRelativePath);
220
+ await cache.save(cachePath, content);
221
+ fetchedDocs.push(docRelativePath);
222
+ }
223
+ catch (error) {
224
+ console.warn(`Warning: Failed to fetch ${doc.name}: ${error}`);
225
+ }
226
+ }
227
+ // Save the manifest itself
228
+ const manifestCachePath = path.join(manifest.name, 'package.busy.md');
229
+ await cache.save(manifestCachePath, manifestContent);
230
+ // Calculate integrity hash of all content
231
+ const integrity = calculateIntegrity(combinedContent);
232
+ // Add to local package registry
233
+ const registry = new PackageRegistry(workspaceRoot);
234
+ try {
235
+ await registry.load();
236
+ }
237
+ catch {
238
+ await registry.init();
239
+ await registry.load();
240
+ }
241
+ // Use absolute path for local sources
242
+ const resolvedSource = isLocal ? parsedManifest.path : manifestUrl;
243
+ const entry = {
244
+ id: manifest.name,
245
+ description: manifest.description,
246
+ source: resolvedSource,
247
+ provider: provider.name,
248
+ cached: `.libraries/${manifest.name}`,
249
+ version: manifest.version,
250
+ fetched: new Date().toISOString(),
251
+ integrity,
252
+ category: 'Packages',
253
+ };
254
+ registry.addPackage(entry);
255
+ await registry.save();
256
+ return {
257
+ name: manifest.name,
258
+ version: manifest.version,
259
+ description: manifest.description,
260
+ documents: manifest.documents,
261
+ cached: `.libraries/${manifest.name}`,
262
+ integrity,
263
+ };
264
+ }
265
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Main Parser Module - busy-python compatible document parsing
3
+ *
4
+ * This module provides the main entry points for parsing BUSY documents:
5
+ * - parseDocument: Parse a markdown string into a BusyDocument or ToolDocument
6
+ * - resolveImports: Resolve import references in a document
7
+ */
8
+ import { NewBusyDocument as BusyDocument, // Use new schema types for busy-python compat
9
+ ToolDocument } from './types/schema.js';
10
+ /**
11
+ * Parse a BUSY markdown document
12
+ *
13
+ * @param content - The markdown content to parse
14
+ * @returns BusyDocument or ToolDocument (if Type is [Tool])
15
+ * @throws Error if frontmatter is missing or invalid
16
+ */
17
+ export declare function parseDocument(content: string): BusyDocument | ToolDocument;
18
+ /**
19
+ * Resolve imports in a document to their parsed documents
20
+ *
21
+ * @param document - The document with imports to resolve
22
+ * @param basePath - Base path for resolving relative imports
23
+ * @param visited - Set of visited paths (for circular import detection)
24
+ * @returns Record mapping concept names to their resolved documents
25
+ * @throws Error if circular import detected or file not found
26
+ */
27
+ export declare function resolveImports(document: BusyDocument | ToolDocument, basePath: string, visited?: Set<string>): Record<string, BusyDocument | ToolDocument>;
28
+ //# sourceMappingURL=parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EACL,eAAe,IAAI,YAAY,EAAG,8CAA8C;AAChF,YAAY,EAGb,MAAM,mBAAmB,CAAC;AA6G3B;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,YAAY,CAiE1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,GAAG,YAAY,EACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,GAAG,CAAC,MAAM,CAAa,GAC/B,MAAM,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAAC,CAuD7C"}
package/dist/parser.js ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Main Parser Module - busy-python compatible document parsing
3
+ *
4
+ * This module provides the main entry points for parsing BUSY documents:
5
+ * - parseDocument: Parse a markdown string into a BusyDocument or ToolDocument
6
+ * - resolveImports: Resolve import references in a document
7
+ */
8
+ import matter from 'gray-matter';
9
+ import { resolve, dirname } from 'path';
10
+ import { readFileSync, existsSync } from 'fs';
11
+ import { MetadataSchema, } from './types/schema.js';
12
+ import { parseImports } from './parsers/imports.js';
13
+ import { parseOperations } from './parsers/operations.js';
14
+ import { parseTriggers } from './parsers/triggers.js';
15
+ import { parseTools } from './parsers/tools.js';
16
+ /**
17
+ * Parse local definitions from markdown content
18
+ */
19
+ function parseLocalDefinitions(content) {
20
+ const definitions = [];
21
+ // Find Local Definitions section
22
+ const localDefsMatch = content.match(/^#\s*\[?Local\s*Definitions\]?\s*$/im);
23
+ if (!localDefsMatch) {
24
+ return definitions;
25
+ }
26
+ // Get content after Local Definitions heading
27
+ const startIndex = localDefsMatch.index + localDefsMatch[0].length;
28
+ const restContent = content.slice(startIndex);
29
+ // Find next top-level heading
30
+ const nextH1Match = restContent.match(/\n#\s+[^\#]/);
31
+ const defsContent = nextH1Match
32
+ ? restContent.slice(0, nextH1Match.index)
33
+ : restContent;
34
+ // Split by ## headings
35
+ const parts = defsContent.split(/\n(?=##\s+)/);
36
+ for (const part of parts) {
37
+ if (!part.trim())
38
+ continue;
39
+ // Match definition heading: ## DefinitionName
40
+ const headingMatch = part.match(/^##\s+([^\n]+)\s*\n?([\s\S]*)/);
41
+ if (headingMatch) {
42
+ const name = headingMatch[1].trim();
43
+ const defContent = (headingMatch[2] || '').trim();
44
+ definitions.push({
45
+ name,
46
+ content: defContent,
47
+ });
48
+ }
49
+ }
50
+ return definitions;
51
+ }
52
+ /**
53
+ * Parse setup section from markdown content
54
+ */
55
+ function parseSetup(content) {
56
+ // Find Setup section
57
+ const setupMatch = content.match(/^#\s*\[?Setup\]?\s*$/im);
58
+ if (!setupMatch) {
59
+ return undefined;
60
+ }
61
+ // Get content after Setup heading
62
+ const startIndex = setupMatch.index + setupMatch[0].length;
63
+ const restContent = content.slice(startIndex);
64
+ // Find next top-level heading
65
+ const nextH1Match = restContent.match(/\n#\s+[^\#]/);
66
+ const setupContent = nextH1Match
67
+ ? restContent.slice(0, nextH1Match.index)
68
+ : restContent;
69
+ const trimmed = setupContent.trim();
70
+ return trimmed || undefined;
71
+ }
72
+ /**
73
+ * Parse metadata from frontmatter
74
+ */
75
+ function parseMetadata(data) {
76
+ // Normalize Type field - YAML might parse [Document] as array
77
+ let type = data.Type;
78
+ if (Array.isArray(type)) {
79
+ type = `[${type.join(', ')}]`;
80
+ }
81
+ else if (typeof type === 'string' && !type.startsWith('[')) {
82
+ type = `[${type}]`;
83
+ }
84
+ const metadata = {
85
+ name: data.Name || '',
86
+ type: type || '[Document]',
87
+ description: data.Description || '',
88
+ };
89
+ // Add provider if present (for tool documents)
90
+ if (data.Provider) {
91
+ metadata.provider = data.Provider;
92
+ }
93
+ // Validate
94
+ const result = MetadataSchema.safeParse(metadata);
95
+ if (!result.success) {
96
+ throw new Error(`Invalid metadata: ${result.error.message}`);
97
+ }
98
+ return result.data;
99
+ }
100
+ /**
101
+ * Parse a BUSY markdown document
102
+ *
103
+ * @param content - The markdown content to parse
104
+ * @returns BusyDocument or ToolDocument (if Type is [Tool])
105
+ * @throws Error if frontmatter is missing or invalid
106
+ */
107
+ export function parseDocument(content) {
108
+ // Trim leading whitespace for gray-matter
109
+ const trimmedContent = content.trimStart();
110
+ // Extract frontmatter - only parse the first block to avoid "multiple documents" error
111
+ // when the body contains --- horizontal rules
112
+ const frontmatterMatch = trimmedContent.match(/^---\n([\s\S]*?)\n---/);
113
+ if (!frontmatterMatch) {
114
+ throw new Error('Missing or empty frontmatter');
115
+ }
116
+ const frontmatterOnly = frontmatterMatch[0];
117
+ const { data } = matter(frontmatterOnly);
118
+ if (!data || Object.keys(data).length === 0) {
119
+ throw new Error('Missing or empty frontmatter');
120
+ }
121
+ // Parse metadata
122
+ const metadata = parseMetadata(data);
123
+ // Parse imports
124
+ const { imports } = parseImports(trimmedContent);
125
+ // Parse local definitions
126
+ const definitions = parseLocalDefinitions(trimmedContent);
127
+ // Parse setup
128
+ const setup = parseSetup(trimmedContent);
129
+ // Parse operations
130
+ const operations = parseOperations(trimmedContent);
131
+ // Parse triggers
132
+ const triggers = parseTriggers(trimmedContent);
133
+ // Check if this is a tool document
134
+ const isToolDocument = metadata.type.toLowerCase().includes('tool');
135
+ if (isToolDocument) {
136
+ // Parse tools section
137
+ const tools = parseTools(trimmedContent);
138
+ const toolDoc = {
139
+ metadata,
140
+ imports,
141
+ definitions,
142
+ setup,
143
+ operations,
144
+ triggers,
145
+ tools,
146
+ };
147
+ return toolDoc;
148
+ }
149
+ const doc = {
150
+ metadata,
151
+ imports,
152
+ definitions,
153
+ setup,
154
+ operations,
155
+ triggers,
156
+ };
157
+ return doc;
158
+ }
159
+ /**
160
+ * Resolve imports in a document to their parsed documents
161
+ *
162
+ * @param document - The document with imports to resolve
163
+ * @param basePath - Base path for resolving relative imports
164
+ * @param visited - Set of visited paths (for circular import detection)
165
+ * @returns Record mapping concept names to their resolved documents
166
+ * @throws Error if circular import detected or file not found
167
+ */
168
+ export function resolveImports(document, basePath, visited = new Set()) {
169
+ const resolved = {};
170
+ for (const imp of document.imports) {
171
+ // Resolve the import path
172
+ const importPath = resolve(dirname(basePath), imp.path);
173
+ // Check for circular imports
174
+ if (visited.has(importPath)) {
175
+ throw new Error(`Circular import detected: ${importPath}`);
176
+ }
177
+ // Check if file exists
178
+ if (!existsSync(importPath)) {
179
+ throw new Error(`Import not found: ${imp.path} (resolved to ${importPath})`);
180
+ }
181
+ // Mark as visited
182
+ visited.add(importPath);
183
+ try {
184
+ // Read and parse the imported document
185
+ const importContent = readFileSync(importPath, 'utf-8');
186
+ const importedDoc = parseDocument(importContent);
187
+ // Validate anchor if specified
188
+ if (imp.anchor) {
189
+ // Check if anchor exists in operations or definitions
190
+ const hasOperation = importedDoc.operations.some((op) => op.name.toLowerCase() === imp.anchor.toLowerCase() ||
191
+ slugify(op.name) === imp.anchor.toLowerCase());
192
+ const hasDefinition = importedDoc.definitions.some((def) => def.name.toLowerCase() === imp.anchor.toLowerCase() ||
193
+ slugify(def.name) === imp.anchor.toLowerCase());
194
+ if (!hasOperation && !hasDefinition) {
195
+ throw new Error(`Anchor '${imp.anchor}' not found in ${imp.path}`);
196
+ }
197
+ }
198
+ // Store resolved document
199
+ resolved[imp.conceptName] = importedDoc;
200
+ // Recursively resolve imports in the imported document
201
+ const nestedResolved = resolveImports(importedDoc, importPath, visited);
202
+ Object.assign(resolved, nestedResolved);
203
+ }
204
+ finally {
205
+ // Remove from visited after processing (allow same doc in different branches)
206
+ visited.delete(importPath);
207
+ }
208
+ }
209
+ return resolved;
210
+ }
211
+ /**
212
+ * Simple slugify function for anchor matching
213
+ */
214
+ function slugify(text) {
215
+ return text
216
+ .toLowerCase()
217
+ .replace(/[^\w\s-]/g, '')
218
+ .replace(/\s+/g, '-');
219
+ }
220
+ //# sourceMappingURL=parser.js.map