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,432 @@
1
+ /**
2
+ * Integration Tests - Full document parsing matching busy-python behavior
3
+ *
4
+ * These tests verify the complete parsing pipeline produces
5
+ * output matching busy-python's parse_document() function.
6
+ */
7
+
8
+ import { describe, it, expect, beforeAll } from 'vitest';
9
+ import { readFileSync } from 'fs';
10
+ import { join } from 'path';
11
+ import { parseDocument, resolveImports } from '../parser';
12
+ import type { BusyDocument, ToolDocument, Import } from '../types/schema';
13
+
14
+ const FIXTURES_DIR = join(__dirname, 'fixtures');
15
+
16
+ function loadFixture(name: string): string {
17
+ return readFileSync(join(FIXTURES_DIR, name), 'utf-8');
18
+ }
19
+
20
+ describe('parseDocument', () => {
21
+ describe('Basic Document Parsing', () => {
22
+ it('should parse document metadata', () => {
23
+ const content = loadFixture('document.busy.md');
24
+ const doc = parseDocument(content);
25
+
26
+ expect(doc.metadata.name).toBe('Document');
27
+ expect(doc.metadata.type).toBe('[Document]');
28
+ expect(doc.metadata.description).toBe('Base document type for all BUSY documents.');
29
+ });
30
+
31
+ it('should parse imports with concept names and paths', () => {
32
+ const content = loadFixture('document.busy.md');
33
+ const doc = parseDocument(content);
34
+
35
+ expect(doc.imports).toHaveLength(1);
36
+ expect(doc.imports[0].conceptName).toBe('Concept');
37
+ expect(doc.imports[0].path).toBe('./concept.busy.md');
38
+ });
39
+
40
+ it('should parse local definitions', () => {
41
+ const content = loadFixture('document.busy.md');
42
+ const doc = parseDocument(content);
43
+
44
+ expect(doc.definitions.length).toBeGreaterThan(0);
45
+ const importsDef = doc.definitions.find((d) => d.name === 'Imports Section');
46
+ expect(importsDef).toBeDefined();
47
+ expect(importsDef?.content).toContain('external document references');
48
+ });
49
+
50
+ it('should parse setup section', () => {
51
+ const content = loadFixture('document.busy.md');
52
+ const doc = parseDocument(content);
53
+
54
+ expect(doc.setup).toBeDefined();
55
+ expect(doc.setup).toContain('evaluated before any operations');
56
+ });
57
+
58
+ it('should parse operations with structured steps', () => {
59
+ const content = loadFixture('document.busy.md');
60
+ const doc = parseDocument(content);
61
+
62
+ expect(doc.operations).toHaveLength(1);
63
+ const op = doc.operations[0];
64
+
65
+ expect(op.name).toBe('EvaluateDocument');
66
+ expect(op.steps).toHaveLength(4);
67
+ expect(op.steps[0].stepNumber).toBe(1);
68
+ expect(op.steps[0].instruction).toBe('Parse frontmatter for metadata');
69
+ expect(op.checklist?.items).toHaveLength(4);
70
+ });
71
+ });
72
+
73
+ describe('Document with Triggers', () => {
74
+ it('should parse triggers from section', () => {
75
+ const content = loadFixture('automated-workflow.busy.md');
76
+ const doc = parseDocument(content);
77
+
78
+ expect(doc.triggers.length).toBeGreaterThan(0);
79
+ });
80
+
81
+ it('should parse alarm trigger with schedule', () => {
82
+ const content = loadFixture('automated-workflow.busy.md');
83
+ const doc = parseDocument(content);
84
+
85
+ const alarmTrigger = doc.triggers.find((t) => t.triggerType === 'alarm');
86
+ expect(alarmTrigger).toBeDefined();
87
+ expect(alarmTrigger?.operation).toBe('DailyReport');
88
+ expect(alarmTrigger?.schedule).toBe('0 6 * * *');
89
+ });
90
+
91
+ it('should parse event trigger with filter', () => {
92
+ const content = loadFixture('automated-workflow.busy.md');
93
+ const doc = parseDocument(content);
94
+
95
+ const eventTrigger = doc.triggers.find(
96
+ (t) => t.triggerType === 'event' && t.eventType === 'data.received'
97
+ );
98
+ expect(eventTrigger).toBeDefined();
99
+ expect(eventTrigger?.filter).toEqual({ from: '*@trusted.com' });
100
+ expect(eventTrigger?.operation).toBe('ProcessIncoming');
101
+ });
102
+
103
+ it('should parse event trigger without filter', () => {
104
+ const content = loadFixture('automated-workflow.busy.md');
105
+ const doc = parseDocument(content);
106
+
107
+ const webhookTrigger = doc.triggers.find(
108
+ (t) => t.eventType === 'webhook.triggered'
109
+ );
110
+ expect(webhookTrigger).toBeDefined();
111
+ expect(webhookTrigger?.filter).toBeUndefined();
112
+ });
113
+ });
114
+
115
+ describe('Document with Operation References', () => {
116
+ it('should extract operation references from steps', () => {
117
+ const content = loadFixture('automated-workflow.busy.md');
118
+ const doc = parseDocument(content);
119
+
120
+ const processOp = doc.operations.find((o) => o.name === 'ProcessIncoming');
121
+ expect(processOp).toBeDefined();
122
+
123
+ // Step 1: "Run [ValidateInput] on the payload"
124
+ expect(processOp?.steps[0].operationReferences).toContain('ValidateInput');
125
+
126
+ // Step 2: "Apply processing rules from [ProcessingRule]"
127
+ expect(processOp?.steps[1].operationReferences).toContain('ProcessingRule');
128
+
129
+ // Step 3: "Execute [ProcessData] with validated input"
130
+ expect(processOp?.steps[2].operationReferences).toContain('ProcessData');
131
+ });
132
+ });
133
+
134
+ describe('Tool Document Parsing', () => {
135
+ it('should return ToolDocument for Type: [Tool]', () => {
136
+ const content = loadFixture('tool.busy.md');
137
+ const doc = parseDocument(content);
138
+
139
+ // Type guard to check if it's a ToolDocument
140
+ expect('tools' in doc).toBe(true);
141
+ });
142
+
143
+ it('should parse provider from metadata', () => {
144
+ const content = loadFixture('tool.busy.md');
145
+ const doc = parseDocument(content);
146
+
147
+ expect(doc.metadata.provider).toBe('composio');
148
+ });
149
+
150
+ it('should parse tools with providers', () => {
151
+ const content = loadFixture('tool.busy.md');
152
+ const doc = parseDocument(content) as ToolDocument;
153
+
154
+ expect(doc.tools).toHaveLength(2);
155
+
156
+ const sendEmail = doc.tools.find((t) => t.name === 'send_email');
157
+ expect(sendEmail).toBeDefined();
158
+ expect(sendEmail?.providers?.composio?.action).toBe('GMAIL_SEND_EMAIL');
159
+ expect(sendEmail?.providers?.composio?.parameters).toEqual({
160
+ to: 'to',
161
+ subject: 'subject',
162
+ body: 'body',
163
+ });
164
+ });
165
+
166
+ it('should parse tool inputs and outputs', () => {
167
+ const content = loadFixture('tool.busy.md');
168
+ const doc = parseDocument(content) as ToolDocument;
169
+
170
+ const sendEmail = doc.tools.find((t) => t.name === 'send_email');
171
+ expect(sendEmail?.inputs).toHaveLength(3);
172
+ expect(sendEmail?.outputs).toHaveLength(2);
173
+ expect(sendEmail?.inputs[0]).toBe('to: Recipient email address');
174
+ });
175
+ });
176
+
177
+ describe('Edge Cases', () => {
178
+ it('should handle document with no imports', () => {
179
+ const content = `
180
+ ---
181
+ Name: NoImports
182
+ Type: [Document]
183
+ Description: Document without imports
184
+ ---
185
+
186
+ # [Setup]
187
+
188
+ No imports needed.
189
+
190
+ # [Operations]
191
+
192
+ ## SimpleOp
193
+
194
+ ### [Steps]
195
+ 1. Do something
196
+ `;
197
+
198
+ const doc = parseDocument(content);
199
+ expect(doc.imports).toHaveLength(0);
200
+ });
201
+
202
+ it('should handle document with no operations', () => {
203
+ const content = `
204
+ ---
205
+ Name: NoOps
206
+ Type: [Document]
207
+ Description: Document without operations
208
+ ---
209
+
210
+ # [Local Definitions]
211
+
212
+ ## SomeDefinition
213
+
214
+ Just a definition.
215
+ `;
216
+
217
+ const doc = parseDocument(content);
218
+ expect(doc.operations).toHaveLength(0);
219
+ });
220
+
221
+ it('should handle document with no setup', () => {
222
+ const content = `
223
+ ---
224
+ Name: NoSetup
225
+ Type: [Document]
226
+ Description: Document without setup
227
+ ---
228
+
229
+ # [Operations]
230
+
231
+ ## SomeOp
232
+
233
+ ### [Steps]
234
+ 1. Do it
235
+ `;
236
+
237
+ const doc = parseDocument(content);
238
+ expect(doc.setup).toBeUndefined();
239
+ });
240
+
241
+ it('should handle missing frontmatter gracefully', () => {
242
+ const content = `
243
+ # Just Markdown
244
+
245
+ No frontmatter here.
246
+ `;
247
+
248
+ expect(() => parseDocument(content)).toThrow();
249
+ });
250
+
251
+ it('should handle empty document', () => {
252
+ const content = `
253
+ ---
254
+ Name: Empty
255
+ Type: [Document]
256
+ Description: Empty document
257
+ ---
258
+ `;
259
+
260
+ const doc = parseDocument(content);
261
+ expect(doc.metadata.name).toBe('Empty');
262
+ expect(doc.imports).toHaveLength(0);
263
+ expect(doc.definitions).toHaveLength(0);
264
+ expect(doc.operations).toHaveLength(0);
265
+ expect(doc.triggers).toHaveLength(0);
266
+ });
267
+ });
268
+ });
269
+
270
+ describe('resolveImports', () => {
271
+ it('should resolve import paths to document IDs', () => {
272
+ const doc: BusyDocument = {
273
+ metadata: {
274
+ name: 'Test',
275
+ type: '[Document]',
276
+ description: 'Test',
277
+ },
278
+ imports: [
279
+ { conceptName: 'Concept', path: './concept.busy.md' },
280
+ { conceptName: 'Operation', path: './operation.busy.md' },
281
+ ],
282
+ definitions: [],
283
+ operations: [],
284
+ triggers: [],
285
+ };
286
+
287
+ const basePath = FIXTURES_DIR;
288
+ // This would resolve imports against actual files
289
+ // For now, just verify the function signature
290
+ expect(typeof resolveImports).toBe('function');
291
+ });
292
+
293
+ it('should detect circular imports', async () => {
294
+ // Circular import detection test
295
+ // doc-a imports doc-b, doc-b imports doc-a
296
+ const docA = `
297
+ ---
298
+ Name: DocA
299
+ Type: [Document]
300
+ Description: A
301
+ ---
302
+
303
+ [DocB]: ./doc-b.busy.md
304
+ `;
305
+
306
+ const docB = `
307
+ ---
308
+ Name: DocB
309
+ Type: [Document]
310
+ Description: B
311
+ ---
312
+
313
+ [DocA]: ./doc-a.busy.md
314
+ `;
315
+
316
+ // Would need actual file system or mock for this test
317
+ // The test documents the expected behavior
318
+ });
319
+
320
+ it('should validate anchor references exist in target', () => {
321
+ // Anchor validation test
322
+ const doc: BusyDocument = {
323
+ metadata: {
324
+ name: 'Test',
325
+ type: '[Document]',
326
+ description: 'Test',
327
+ },
328
+ imports: [
329
+ { conceptName: 'NonExistent', path: './file.busy.md', anchor: 'nonexistent-anchor' },
330
+ ],
331
+ definitions: [],
332
+ operations: [],
333
+ triggers: [],
334
+ };
335
+
336
+ // Should throw BusyImportError when anchor doesn't exist
337
+ });
338
+ });
339
+
340
+ describe('Output Format Compatibility', () => {
341
+ it('should produce output matching busy-python structure', () => {
342
+ const content = loadFixture('document.busy.md');
343
+ const doc = parseDocument(content);
344
+
345
+ // Verify structure matches busy-python BusyDocument
346
+ expect(doc).toHaveProperty('metadata');
347
+ expect(doc).toHaveProperty('imports');
348
+ expect(doc).toHaveProperty('definitions');
349
+ expect(doc).toHaveProperty('setup');
350
+ expect(doc).toHaveProperty('operations');
351
+ expect(doc).toHaveProperty('triggers');
352
+
353
+ // Metadata structure
354
+ expect(doc.metadata).toHaveProperty('name');
355
+ expect(doc.metadata).toHaveProperty('type');
356
+ expect(doc.metadata).toHaveProperty('description');
357
+
358
+ // Import structure
359
+ if (doc.imports.length > 0) {
360
+ expect(doc.imports[0]).toHaveProperty('conceptName');
361
+ expect(doc.imports[0]).toHaveProperty('path');
362
+ }
363
+
364
+ // Operation structure
365
+ if (doc.operations.length > 0) {
366
+ expect(doc.operations[0]).toHaveProperty('name');
367
+ expect(doc.operations[0]).toHaveProperty('inputs');
368
+ expect(doc.operations[0]).toHaveProperty('outputs');
369
+ expect(doc.operations[0]).toHaveProperty('steps');
370
+
371
+ // Step structure
372
+ if (doc.operations[0].steps.length > 0) {
373
+ expect(doc.operations[0].steps[0]).toHaveProperty('stepNumber');
374
+ expect(doc.operations[0].steps[0]).toHaveProperty('instruction');
375
+ }
376
+ }
377
+ });
378
+
379
+ it('should serialize to JSON matching busy-python output', () => {
380
+ const content = loadFixture('document.busy.md');
381
+ const doc = parseDocument(content);
382
+
383
+ // Should be serializable to JSON
384
+ const json = JSON.stringify(doc);
385
+ const parsed = JSON.parse(json);
386
+
387
+ expect(parsed.metadata.name).toBe(doc.metadata.name);
388
+ expect(parsed.imports).toEqual(doc.imports);
389
+ expect(parsed.operations.length).toBe(doc.operations.length);
390
+ });
391
+ });
392
+
393
+ describe('Frontmatter Parsing', () => {
394
+ it('should handle Type as bracketed string', () => {
395
+ const content = `
396
+ ---
397
+ Name: Test
398
+ Type: [Document]
399
+ Description: Test
400
+ ---
401
+ `;
402
+
403
+ const doc = parseDocument(content);
404
+ expect(doc.metadata.type).toBe('[Document]');
405
+ });
406
+
407
+ it('should NOT include Extends or Tags (busy-python removed these)', () => {
408
+ const content = `
409
+ ---
410
+ Name: Test
411
+ Type: [Document]
412
+ Description: Test
413
+ Extends:
414
+ - Parent
415
+ Tags:
416
+ - tag1
417
+ - tag2
418
+ ---
419
+ `;
420
+
421
+ const doc = parseDocument(content);
422
+ expect(doc.metadata).not.toHaveProperty('extends');
423
+ expect(doc.metadata).not.toHaveProperty('tags');
424
+ });
425
+
426
+ it('should include Provider for tool documents', () => {
427
+ const content = loadFixture('tool.busy.md');
428
+ const doc = parseDocument(content);
429
+
430
+ expect(doc.metadata.provider).toBe('composio');
431
+ });
432
+ });