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,667 @@
1
+ /**
2
+ * CLI Package Management Tests
3
+ *
4
+ * Tests for busy init, check, and package commands.
5
+ * TDD approach for package manager implementation.
6
+ */
7
+
8
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
9
+ import { promises as fs } from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import * as os from 'node:os';
12
+
13
+ // We'll implement these commands
14
+ import {
15
+ initWorkspace,
16
+ checkWorkspace,
17
+ addPackage,
18
+ removePackage,
19
+ upgradePackage,
20
+ listPackages,
21
+ getPackageInfo,
22
+ } from '../commands/package.js';
23
+
24
+ describe('initWorkspace', () => {
25
+ let tempDir: string;
26
+
27
+ beforeEach(async () => {
28
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
29
+ });
30
+
31
+ afterEach(async () => {
32
+ await fs.rm(tempDir, { recursive: true, force: true });
33
+ });
34
+
35
+ it('should create package.busy.md', async () => {
36
+ const result = await initWorkspace(tempDir);
37
+
38
+ const packagePath = path.join(tempDir, 'package.busy.md');
39
+ const exists = await fs.stat(packagePath).then(() => true).catch(() => false);
40
+
41
+ expect(exists).toBe(true);
42
+ expect(result.created).toContain('package.busy.md');
43
+ });
44
+
45
+ it('should create .libraries directory', async () => {
46
+ const result = await initWorkspace(tempDir);
47
+
48
+ const librariesPath = path.join(tempDir, '.libraries');
49
+ const exists = await fs.stat(librariesPath).then(() => true).catch(() => false);
50
+
51
+ expect(exists).toBe(true);
52
+ expect(result.created).toContain('.libraries');
53
+ });
54
+
55
+ it('should not overwrite existing package.busy.md', async () => {
56
+ const packagePath = path.join(tempDir, 'package.busy.md');
57
+ await fs.writeFile(packagePath, 'existing content');
58
+
59
+ const result = await initWorkspace(tempDir);
60
+
61
+ const content = await fs.readFile(packagePath, 'utf-8');
62
+ expect(content).toBe('existing content');
63
+ expect(result.skipped).toContain('package.busy.md');
64
+ });
65
+
66
+ it('should return workspace info', async () => {
67
+ const result = await initWorkspace(tempDir);
68
+
69
+ expect(result.workspaceRoot).toBe(tempDir);
70
+ expect(result.initialized).toBe(true);
71
+ });
72
+ });
73
+
74
+ describe('checkWorkspace', () => {
75
+ let tempDir: string;
76
+
77
+ beforeEach(async () => {
78
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
79
+ await initWorkspace(tempDir);
80
+ });
81
+
82
+ afterEach(async () => {
83
+ await fs.rm(tempDir, { recursive: true, force: true });
84
+ });
85
+
86
+ it('should pass for empty workspace', async () => {
87
+ const result = await checkWorkspace(tempDir);
88
+
89
+ expect(result.valid).toBe(true);
90
+ expect(result.errors).toHaveLength(0);
91
+ });
92
+
93
+ it('should report missing cached files', async () => {
94
+ // Add a package entry but don't create the cached file
95
+ const packagePath = path.join(tempDir, 'package.busy.md');
96
+ const content = `---
97
+ Name: package
98
+ Type: Document
99
+ Description: Test
100
+ ---
101
+
102
+ # Definitions
103
+
104
+ ## Package Entry
105
+
106
+ | Field | Required | Description |
107
+ |-------|----------|-------------|
108
+ | Source | Yes | URL |
109
+
110
+ ---
111
+
112
+ # Package Registry
113
+
114
+ ## Packages
115
+
116
+ ### test-package
117
+
118
+ Test package
119
+
120
+ | Field | Value |
121
+ |-------|-------|
122
+ | Source | https://example.com/file.md |
123
+ | Provider | url |
124
+ | Cached | .libraries/file.md |
125
+ | Version | v1.0.0 |
126
+ | Fetched | 2026-01-21T00:00:00Z |
127
+ `;
128
+
129
+ await fs.writeFile(packagePath, content);
130
+
131
+ const result = await checkWorkspace(tempDir);
132
+
133
+ expect(result.valid).toBe(false);
134
+ expect(result.errors.some(e => e.includes('test-package'))).toBe(true);
135
+ expect(result.errors.some(e => e.includes('cached file'))).toBe(true);
136
+ });
137
+
138
+ it('should pass when cached files exist', async () => {
139
+ // Add a package entry with a cached file
140
+ const packagePath = path.join(tempDir, 'package.busy.md');
141
+ const content = `---
142
+ Name: package
143
+ Type: Document
144
+ Description: Test
145
+ ---
146
+
147
+ # Definitions
148
+
149
+ ## Package Entry
150
+
151
+ | Field | Required | Description |
152
+ |-------|----------|-------------|
153
+ | Source | Yes | URL |
154
+
155
+ ---
156
+
157
+ # Package Registry
158
+
159
+ ## Packages
160
+
161
+ ### test-package
162
+
163
+ Test package
164
+
165
+ | Field | Value |
166
+ |-------|-------|
167
+ | Source | https://example.com/file.md |
168
+ | Provider | url |
169
+ | Cached | .libraries/file.md |
170
+ | Version | v1.0.0 |
171
+ | Fetched | 2026-01-21T00:00:00Z |
172
+ `;
173
+
174
+ await fs.writeFile(packagePath, content);
175
+
176
+ // Create the cached file
177
+ const cachePath = path.join(tempDir, '.libraries', 'file.md');
178
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
179
+ await fs.writeFile(cachePath, '# Cached content');
180
+
181
+ const result = await checkWorkspace(tempDir);
182
+
183
+ expect(result.valid).toBe(true);
184
+ });
185
+
186
+ it('should verify integrity when specified', async () => {
187
+ const packagePath = path.join(tempDir, 'package.busy.md');
188
+ const content = `---
189
+ Name: package
190
+ Type: Document
191
+ Description: Test
192
+ ---
193
+
194
+ # Definitions
195
+
196
+ ## Package Entry
197
+
198
+ | Field | Required | Description |
199
+ |-------|----------|-------------|
200
+ | Source | Yes | URL |
201
+
202
+ ---
203
+
204
+ # Package Registry
205
+
206
+ ## Packages
207
+
208
+ ### test-package
209
+
210
+ Test package
211
+
212
+ | Field | Value |
213
+ |-------|-------|
214
+ | Source | https://example.com/file.md |
215
+ | Provider | url |
216
+ | Cached | .libraries/file.md |
217
+ | Version | v1.0.0 |
218
+ | Fetched | 2026-01-21T00:00:00Z |
219
+ | Integrity | sha256:0000000000000000000000000000000000000000000000000000000000000000 |
220
+ `;
221
+
222
+ await fs.writeFile(packagePath, content);
223
+
224
+ // Create cached file with different content (integrity mismatch)
225
+ const cachePath = path.join(tempDir, '.libraries', 'file.md');
226
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
227
+ await fs.writeFile(cachePath, '# Different content');
228
+
229
+ const result = await checkWorkspace(tempDir);
230
+
231
+ expect(result.warnings.some(w => w.includes('integrity'))).toBe(true);
232
+ });
233
+
234
+ it('should throw for uninitialized workspace', async () => {
235
+ const uninitDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-uninit-'));
236
+
237
+ await expect(checkWorkspace(uninitDir)).rejects.toThrow();
238
+
239
+ await fs.rm(uninitDir, { recursive: true, force: true });
240
+ });
241
+ });
242
+
243
+ describe('addPackage', () => {
244
+ let tempDir: string;
245
+ let originalFetch: typeof global.fetch;
246
+
247
+ beforeEach(async () => {
248
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
249
+ await initWorkspace(tempDir);
250
+ originalFetch = global.fetch;
251
+ });
252
+
253
+ afterEach(async () => {
254
+ global.fetch = originalFetch;
255
+ await fs.rm(tempDir, { recursive: true, force: true });
256
+ });
257
+
258
+ it('should add package from GitHub URL', async () => {
259
+ const mockContent = '# Test Content\n\nThis is test content.';
260
+
261
+ // Mock fetch for both raw content and API calls
262
+ global.fetch = vi.fn().mockImplementation(async (url: string) => {
263
+ if (url.includes('raw.githubusercontent.com')) {
264
+ return {
265
+ ok: true,
266
+ text: () => Promise.resolve(mockContent),
267
+ };
268
+ }
269
+ return {
270
+ ok: true,
271
+ json: () => Promise.resolve([]),
272
+ };
273
+ });
274
+
275
+ const url = 'https://github.com/org/repo/blob/v1.0.0/path/file.md';
276
+ const result = await addPackage(tempDir, url);
277
+
278
+ expect(result.id).toBe('file');
279
+ expect(result.version).toBe('v1.0.0');
280
+ expect(result.provider).toBe('github');
281
+
282
+ // Verify cached file exists
283
+ const cachePath = path.join(tempDir, result.cached);
284
+ const exists = await fs.stat(cachePath).then(() => true).catch(() => false);
285
+ expect(exists).toBe(true);
286
+ });
287
+
288
+ it('should derive entry ID from anchor', async () => {
289
+ const mockContent = '# Content';
290
+
291
+ global.fetch = vi.fn().mockResolvedValue({
292
+ ok: true,
293
+ text: () => Promise.resolve(mockContent),
294
+ });
295
+
296
+ const url = 'https://github.com/org/repo/blob/main/file.md#my-section';
297
+ const result = await addPackage(tempDir, url);
298
+
299
+ expect(result.id).toBe('my-section');
300
+ });
301
+
302
+ it('should update package.busy.md', async () => {
303
+ const mockContent = '# Content';
304
+
305
+ global.fetch = vi.fn().mockResolvedValue({
306
+ ok: true,
307
+ text: () => Promise.resolve(mockContent),
308
+ });
309
+
310
+ const url = 'https://github.com/org/repo/blob/v1.0.0/file.md';
311
+ await addPackage(tempDir, url);
312
+
313
+ // Read package.busy.md and verify entry
314
+ const packagePath = path.join(tempDir, 'package.busy.md');
315
+ const content = await fs.readFile(packagePath, 'utf-8');
316
+
317
+ expect(content).toContain('### file');
318
+ expect(content).toContain('v1.0.0');
319
+ });
320
+ });
321
+
322
+ describe('removePackage', () => {
323
+ let tempDir: string;
324
+
325
+ beforeEach(async () => {
326
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
327
+ await initWorkspace(tempDir);
328
+ });
329
+
330
+ afterEach(async () => {
331
+ await fs.rm(tempDir, { recursive: true, force: true });
332
+ });
333
+
334
+ it('should remove package from registry', async () => {
335
+ // Set up a package first
336
+ const packagePath = path.join(tempDir, 'package.busy.md');
337
+ const content = `---
338
+ Name: package
339
+ Type: Document
340
+ Description: Test
341
+ ---
342
+
343
+ # Definitions
344
+
345
+ ## Package Entry
346
+
347
+ | Field | Required | Description |
348
+ |-------|----------|-------------|
349
+ | Source | Yes | URL |
350
+
351
+ ---
352
+
353
+ # Package Registry
354
+
355
+ ## Packages
356
+
357
+ ### test-package
358
+
359
+ Test
360
+
361
+ | Field | Value |
362
+ |-------|-------|
363
+ | Source | https://example.com/file.md |
364
+ | Provider | url |
365
+ | Cached | .libraries/file.md |
366
+ | Version | v1.0.0 |
367
+ | Fetched | 2026-01-21T00:00:00Z |
368
+ `;
369
+
370
+ await fs.writeFile(packagePath, content);
371
+
372
+ // Create cached file
373
+ const cachePath = path.join(tempDir, '.libraries', 'file.md');
374
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
375
+ await fs.writeFile(cachePath, '# Content');
376
+
377
+ // Remove package
378
+ const result = await removePackage(tempDir, 'test-package');
379
+
380
+ expect(result.removed).toBe(true);
381
+ expect(result.id).toBe('test-package');
382
+
383
+ // Verify cached file is removed
384
+ const exists = await fs.stat(cachePath).then(() => true).catch(() => false);
385
+ expect(exists).toBe(false);
386
+
387
+ // Verify registry entry is removed
388
+ const newContent = await fs.readFile(packagePath, 'utf-8');
389
+ expect(newContent).not.toContain('test-package');
390
+ });
391
+
392
+ it('should return false for non-existent package', async () => {
393
+ const result = await removePackage(tempDir, 'nonexistent');
394
+
395
+ expect(result.removed).toBe(false);
396
+ });
397
+ });
398
+
399
+ describe('listPackages', () => {
400
+ let tempDir: string;
401
+
402
+ beforeEach(async () => {
403
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
404
+ await initWorkspace(tempDir);
405
+ });
406
+
407
+ afterEach(async () => {
408
+ await fs.rm(tempDir, { recursive: true, force: true });
409
+ });
410
+
411
+ it('should list all packages', async () => {
412
+ const packagePath = path.join(tempDir, 'package.busy.md');
413
+ const content = `---
414
+ Name: package
415
+ Type: Document
416
+ Description: Test
417
+ ---
418
+
419
+ # Definitions
420
+
421
+ ## Package Entry
422
+
423
+ | Field | Required | Description |
424
+ |-------|----------|-------------|
425
+ | Source | Yes | URL |
426
+
427
+ ---
428
+
429
+ # Package Registry
430
+
431
+ ## Packages
432
+
433
+ ### package-one
434
+
435
+ Test one
436
+
437
+ | Field | Value |
438
+ |-------|-------|
439
+ | Source | https://example.com/one.md |
440
+ | Provider | url |
441
+ | Cached | .libraries/one.md |
442
+ | Version | v1.0.0 |
443
+ | Fetched | 2026-01-21T00:00:00Z |
444
+
445
+ ### package-two
446
+
447
+ Test two
448
+
449
+ | Field | Value |
450
+ |-------|-------|
451
+ | Source | https://example.com/two.md |
452
+ | Provider | github |
453
+ | Cached | .libraries/two.md |
454
+ | Version | v2.0.0 |
455
+ | Fetched | 2026-01-21T00:00:00Z |
456
+ `;
457
+
458
+ await fs.writeFile(packagePath, content);
459
+
460
+ const result = await listPackages(tempDir);
461
+
462
+ expect(result.packages).toHaveLength(2);
463
+ expect(result.packages.some(p => p.id === 'package-one')).toBe(true);
464
+ expect(result.packages.some(p => p.id === 'package-two')).toBe(true);
465
+ });
466
+
467
+ it('should return empty array for empty registry', async () => {
468
+ const result = await listPackages(tempDir);
469
+
470
+ expect(result.packages).toHaveLength(0);
471
+ });
472
+ });
473
+
474
+ describe('getPackageInfo', () => {
475
+ let tempDir: string;
476
+
477
+ beforeEach(async () => {
478
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
479
+ await initWorkspace(tempDir);
480
+ });
481
+
482
+ afterEach(async () => {
483
+ await fs.rm(tempDir, { recursive: true, force: true });
484
+ });
485
+
486
+ it('should return package details', async () => {
487
+ const packagePath = path.join(tempDir, 'package.busy.md');
488
+ const content = `---
489
+ Name: package
490
+ Type: Document
491
+ Description: Test
492
+ ---
493
+
494
+ # Definitions
495
+
496
+ ## Package Entry
497
+
498
+ | Field | Required | Description |
499
+ |-------|----------|-------------|
500
+ | Source | Yes | URL |
501
+
502
+ ---
503
+
504
+ # Package Registry
505
+
506
+ ## Packages
507
+
508
+ ### test-package
509
+
510
+ Test description
511
+
512
+ | Field | Value |
513
+ |-------|-------|
514
+ | Source | https://example.com/file.md |
515
+ | Provider | github |
516
+ | Cached | .libraries/file.md |
517
+ | Version | v1.5.0 |
518
+ | Fetched | 2026-01-21T10:30:00Z |
519
+ | Integrity | sha256:abc123 |
520
+ `;
521
+
522
+ await fs.writeFile(packagePath, content);
523
+
524
+ const result = await getPackageInfo(tempDir, 'test-package');
525
+
526
+ expect(result).not.toBeNull();
527
+ expect(result?.id).toBe('test-package');
528
+ expect(result?.version).toBe('v1.5.0');
529
+ expect(result?.provider).toBe('github');
530
+ expect(result?.integrity).toBe('sha256:abc123');
531
+ });
532
+
533
+ it('should return null for non-existent package', async () => {
534
+ const result = await getPackageInfo(tempDir, 'nonexistent');
535
+ expect(result).toBeNull();
536
+ });
537
+ });
538
+
539
+ describe('upgradePackage', () => {
540
+ let tempDir: string;
541
+ let originalFetch: typeof global.fetch;
542
+
543
+ beforeEach(async () => {
544
+ tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'busy-cli-test-'));
545
+ await initWorkspace(tempDir);
546
+ originalFetch = global.fetch;
547
+ });
548
+
549
+ afterEach(async () => {
550
+ global.fetch = originalFetch;
551
+ await fs.rm(tempDir, { recursive: true, force: true });
552
+ });
553
+
554
+ it('should upgrade package to latest version', async () => {
555
+ // Set up existing package
556
+ const packagePath = path.join(tempDir, 'package.busy.md');
557
+ const content = `---
558
+ Name: package
559
+ Type: Document
560
+ Description: Test
561
+ ---
562
+
563
+ # Definitions
564
+
565
+ ## Package Entry
566
+
567
+ | Field | Required | Description |
568
+ |-------|----------|-------------|
569
+ | Source | Yes | URL |
570
+
571
+ ---
572
+
573
+ # Package Registry
574
+
575
+ ## Packages
576
+
577
+ ### test-package
578
+
579
+ Test
580
+
581
+ | Field | Value |
582
+ |-------|-------|
583
+ | Source | https://github.com/org/repo/blob/v1.0.0/file.md |
584
+ | Provider | github |
585
+ | Cached | .libraries/repo/file.md |
586
+ | Version | v1.0.0 |
587
+ | Fetched | 2026-01-21T00:00:00Z |
588
+ `;
589
+
590
+ await fs.writeFile(packagePath, content);
591
+
592
+ // Create cached file
593
+ const cachePath = path.join(tempDir, '.libraries', 'repo', 'file.md');
594
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
595
+ await fs.writeFile(cachePath, '# Old content');
596
+
597
+ // Mock fetch for tags API and content
598
+ global.fetch = vi.fn().mockImplementation(async (url: string) => {
599
+ if (url.includes('api.github.com')) {
600
+ return {
601
+ ok: true,
602
+ json: () => Promise.resolve([{ name: 'v2.0.0' }, { name: 'v1.0.0' }]),
603
+ };
604
+ }
605
+ return {
606
+ ok: true,
607
+ text: () => Promise.resolve('# New content'),
608
+ };
609
+ });
610
+
611
+ const result = await upgradePackage(tempDir, 'test-package');
612
+
613
+ expect(result.upgraded).toBe(true);
614
+ expect(result.oldVersion).toBe('v1.0.0');
615
+ expect(result.newVersion).toBe('v2.0.0');
616
+ });
617
+
618
+ it('should return not upgraded if already latest', async () => {
619
+ const packagePath = path.join(tempDir, 'package.busy.md');
620
+ const content = `---
621
+ Name: package
622
+ Type: Document
623
+ Description: Test
624
+ ---
625
+
626
+ # Definitions
627
+
628
+ ## Package Entry
629
+
630
+ | Field | Required | Description |
631
+ |-------|----------|-------------|
632
+ | Source | Yes | URL |
633
+
634
+ ---
635
+
636
+ # Package Registry
637
+
638
+ ## Packages
639
+
640
+ ### test-package
641
+
642
+ Test
643
+
644
+ | Field | Value |
645
+ |-------|-------|
646
+ | Source | https://github.com/org/repo/blob/v2.0.0/file.md |
647
+ | Provider | github |
648
+ | Cached | .libraries/repo/file.md |
649
+ | Version | v2.0.0 |
650
+ | Fetched | 2026-01-21T00:00:00Z |
651
+ `;
652
+
653
+ await fs.writeFile(packagePath, content);
654
+
655
+ // Mock fetch - already at latest
656
+ global.fetch = vi.fn().mockResolvedValue({
657
+ ok: true,
658
+ json: () => Promise.resolve([{ name: 'v2.0.0' }]),
659
+ });
660
+
661
+ const result = await upgradePackage(tempDir, 'test-package');
662
+
663
+ expect(result.upgraded).toBe(false);
664
+ expect(result.oldVersion).toBe('v2.0.0');
665
+ expect(result.newVersion).toBe('v2.0.0');
666
+ });
667
+ });