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 @@
|
|
|
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
|
package/dist/parser.d.ts
ADDED
|
@@ -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
|