opencode-teammate 0.1.0

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 (50) hide show
  1. package/.bunli/commands.gen.ts +87 -0
  2. package/.github/workflows/ci.yml +31 -0
  3. package/.github/workflows/release.yml +140 -0
  4. package/.oxfmtrc.json +3 -0
  5. package/.oxlintrc.json +4 -0
  6. package/.zed/settings.json +76 -0
  7. package/README.md +15 -0
  8. package/bunli.config.ts +11 -0
  9. package/bunup.config.ts +31 -0
  10. package/package.json +36 -0
  11. package/src/adapters/assets/index.ts +1 -0
  12. package/src/adapters/assets/specifications.ts +70 -0
  13. package/src/adapters/beads/agents.ts +105 -0
  14. package/src/adapters/beads/config.ts +17 -0
  15. package/src/adapters/beads/index.ts +4 -0
  16. package/src/adapters/beads/issues.ts +156 -0
  17. package/src/adapters/beads/specifications.ts +55 -0
  18. package/src/adapters/environments/index.ts +43 -0
  19. package/src/adapters/environments/worktrees.ts +78 -0
  20. package/src/adapters/teammates/index.ts +15 -0
  21. package/src/assets/agent/planner.md +196 -0
  22. package/src/assets/command/brainstorm.md +60 -0
  23. package/src/assets/command/specify.md +135 -0
  24. package/src/assets/command/work.md +247 -0
  25. package/src/assets/index.ts +37 -0
  26. package/src/cli/commands/manifest.ts +6 -0
  27. package/src/cli/commands/spec/sync.ts +47 -0
  28. package/src/cli/commands/work.ts +110 -0
  29. package/src/cli/index.ts +11 -0
  30. package/src/plugin.ts +45 -0
  31. package/src/tools/i-am-done.ts +44 -0
  32. package/src/tools/i-am-stuck.ts +49 -0
  33. package/src/tools/index.ts +2 -0
  34. package/src/use-cases/index.ts +5 -0
  35. package/src/use-cases/inject-beads-issue.ts +97 -0
  36. package/src/use-cases/sync-specifications.ts +48 -0
  37. package/src/use-cases/sync-teammates.ts +35 -0
  38. package/src/use-cases/track-specs.ts +91 -0
  39. package/src/use-cases/work-on-issue.ts +110 -0
  40. package/src/utils/chain.ts +60 -0
  41. package/src/utils/frontmatter.spec.ts +491 -0
  42. package/src/utils/frontmatter.ts +317 -0
  43. package/src/utils/opencode.ts +102 -0
  44. package/src/utils/polling.ts +41 -0
  45. package/src/utils/projects.ts +35 -0
  46. package/src/utils/shell/client.spec.ts +106 -0
  47. package/src/utils/shell/client.ts +117 -0
  48. package/src/utils/shell/error.ts +29 -0
  49. package/src/utils/shell/index.ts +2 -0
  50. package/tsconfig.json +9 -0
@@ -0,0 +1,491 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import { parse, extract, stringify } from "./frontmatter";
3
+
4
+ /**
5
+ * Test suite for the frontmatter utility module.
6
+ *
7
+ * Tests cover parsing, extracting, and stringifying YAML frontmatter,
8
+ * including edge cases, data type handling, and roundtrip conversions.
9
+ */
10
+ describe("frontmatter", () => {
11
+ describe("parse", () => {
12
+ it("should parse basic frontmatter", () => {
13
+ const content = `---
14
+ title: My Title
15
+ description: My Description
16
+ ---
17
+
18
+ content here`;
19
+
20
+ const result = parse(content);
21
+ expect(result).toEqual({
22
+ title: "My Title",
23
+ description: "My Description",
24
+ });
25
+ });
26
+
27
+ it("should parse nested objects", () => {
28
+ const content = `---
29
+ title: My Title
30
+ nested:
31
+ key: value
32
+ another: data
33
+ ---`;
34
+
35
+ const result = parse(content);
36
+ expect(result).toEqual({
37
+ title: "My Title",
38
+ nested: {
39
+ key: "value",
40
+ another: "data",
41
+ },
42
+ });
43
+ });
44
+
45
+ it("should parse multiple levels of nesting", () => {
46
+ const content = `---
47
+ level1:
48
+ level2:
49
+ level3: deep value
50
+ another: value
51
+ sibling: data
52
+ ---`;
53
+
54
+ const result = parse(content);
55
+ expect(result).toEqual({
56
+ level1: {
57
+ level2: {
58
+ level3: "deep value",
59
+ another: "value",
60
+ },
61
+ sibling: "data",
62
+ },
63
+ });
64
+ });
65
+
66
+ it("should parse different data types", () => {
67
+ const content = `---
68
+ string: hello
69
+ number: 42
70
+ float: 3.14
71
+ boolean_true: true
72
+ boolean_false: false
73
+ null_value: null
74
+ ---`;
75
+
76
+ const result = parse(content);
77
+ expect(result).toEqual({
78
+ string: "hello",
79
+ number: 42,
80
+ float: 3.14,
81
+ boolean_true: true,
82
+ boolean_false: false,
83
+ null_value: null,
84
+ });
85
+ });
86
+
87
+ it("should parse quoted strings", () => {
88
+ const content = `---
89
+ single: 'single quotes'
90
+ double: "double quotes"
91
+ ---`;
92
+
93
+ const result = parse(content);
94
+ expect(result).toEqual({
95
+ single: "single quotes",
96
+ double: "double quotes",
97
+ });
98
+ });
99
+
100
+ it("should return empty object when no frontmatter", () => {
101
+ const content = `# Just a title
102
+
103
+ Some content`;
104
+
105
+ const result = parse(content);
106
+ expect(result).toEqual({});
107
+ });
108
+
109
+ it("should return empty object when delimiter not found", () => {
110
+ const content = `---
111
+ title: Test
112
+ no closing delimiter`;
113
+
114
+ const result = parse(content);
115
+ expect(result).toEqual({});
116
+ });
117
+
118
+ it("should handle empty frontmatter", () => {
119
+ const content = `---
120
+ ---
121
+
122
+ content`;
123
+
124
+ const result = parse(content);
125
+ expect(result).toEqual({});
126
+ });
127
+
128
+ it("should ignore comments in frontmatter", () => {
129
+ const content = `---
130
+ # This is a comment
131
+ title: My Title
132
+ # Another comment
133
+ description: My Description
134
+ ---`;
135
+
136
+ const result = parse(content);
137
+ expect(result).toEqual({
138
+ title: "My Title",
139
+ description: "My Description",
140
+ });
141
+ });
142
+
143
+ it("should handle custom delimiter", () => {
144
+ const content = `+++
145
+ title: My Title
146
+ description: My Description
147
+ +++
148
+
149
+ content here`;
150
+
151
+ const result = parse(content, { delimiter: "+++" });
152
+ expect(result).toEqual({
153
+ title: "My Title",
154
+ description: "My Description",
155
+ });
156
+ });
157
+
158
+ it("should handle content with whitespace before delimiter", () => {
159
+ const content = ` ---
160
+ title: My Title
161
+ ---`;
162
+
163
+ const result = parse(content);
164
+ expect(result).toEqual({
165
+ title: "My Title",
166
+ });
167
+ });
168
+
169
+ it("should handle strings that look like numbers", () => {
170
+ const content = `---
171
+ version: "1.0"
172
+ code: "007"
173
+ ---`;
174
+
175
+ const result = parse(content);
176
+ expect(result).toEqual({
177
+ version: "1.0",
178
+ code: "007",
179
+ });
180
+ });
181
+ });
182
+
183
+ describe("extract", () => {
184
+ it("should extract frontmatter and content", () => {
185
+ const content = `---
186
+ title: My Title
187
+ description: My Description
188
+ ---
189
+
190
+ # Main Content
191
+
192
+ This is the body of the document.`;
193
+
194
+ const result = extract(content);
195
+ expect(result.metadata).toEqual({
196
+ title: "My Title",
197
+ description: "My Description",
198
+ });
199
+ expect(result.content).toBe(`
200
+ # Main Content
201
+
202
+ This is the body of the document.`);
203
+ });
204
+
205
+ it("should handle content immediately after closing delimiter", () => {
206
+ const content = `---
207
+ title: Test
208
+ ---
209
+ Content starts here`;
210
+
211
+ const result = extract(content);
212
+ expect(result.metadata).toEqual({ title: "Test" });
213
+ expect(result.content).toBe("Content starts here");
214
+ });
215
+
216
+ it("should return empty data and full content when no frontmatter", () => {
217
+ const content = `# Just a title
218
+
219
+ Some content`;
220
+
221
+ const result = extract(content);
222
+ expect(result.metadata).toEqual({});
223
+ expect(result.content).toBe(content);
224
+ });
225
+
226
+ it("should extract with nested objects", () => {
227
+ const content = `---
228
+ title: My Title
229
+ nested:
230
+ key: value
231
+ ---
232
+
233
+ Content here`;
234
+
235
+ const result = extract(content);
236
+ expect(result.metadata).toEqual({
237
+ title: "My Title",
238
+ nested: {
239
+ key: "value",
240
+ },
241
+ });
242
+ expect(result.content).toBe("\nContent here");
243
+ });
244
+
245
+ it("should handle custom delimiter", () => {
246
+ const content = `~~~
247
+ title: Custom
248
+ ~~~
249
+ Body content`;
250
+
251
+ const result = extract(content, { delimiter: "~~~" });
252
+ expect(result.metadata).toEqual({ title: "Custom" });
253
+ expect(result.content).toBe("Body content");
254
+ });
255
+
256
+ it("should preserve empty lines in content", () => {
257
+ const content = `---
258
+ title: Test
259
+ ---
260
+
261
+
262
+ Content with empty lines above`;
263
+
264
+ const result = extract(content);
265
+ expect(result.content).toBe("\n\nContent with empty lines above");
266
+ });
267
+ });
268
+
269
+ describe("stringify", () => {
270
+ it("should stringify basic data", () => {
271
+ const data = {
272
+ title: "My Title",
273
+ description: "My Description",
274
+ };
275
+
276
+ const result = stringify(data);
277
+ expect(result).toBe(`---
278
+ title: My Title
279
+ description: My Description
280
+ ---
281
+ `);
282
+ });
283
+
284
+ it("should stringify nested objects", () => {
285
+ const data = {
286
+ title: "My Title",
287
+ nested: {
288
+ key: "value",
289
+ another: "data",
290
+ },
291
+ };
292
+
293
+ const result = stringify(data);
294
+ expect(result).toBe(`---
295
+ title: My Title
296
+ nested:
297
+ key: value
298
+ another: data
299
+ ---
300
+ `);
301
+ });
302
+
303
+ it("should stringify different data types", () => {
304
+ const data = {
305
+ string: "hello",
306
+ number: 42,
307
+ boolean: true,
308
+ null_value: null,
309
+ };
310
+
311
+ const result = stringify(data);
312
+ expect(result).toBe(`---
313
+ string: hello
314
+ number: 42
315
+ boolean: true
316
+ null_value: null
317
+ ---
318
+ `);
319
+ });
320
+
321
+ it("should handle deeply nested objects", () => {
322
+ const data = {
323
+ level1: {
324
+ level2: {
325
+ level3: "deep",
326
+ },
327
+ },
328
+ };
329
+
330
+ const result = stringify(data);
331
+ expect(result).toBe(`---
332
+ level1:
333
+ level2:
334
+ level3: deep
335
+ ---
336
+ `);
337
+ });
338
+
339
+ it("should use custom delimiter", () => {
340
+ const data = {
341
+ title: "Test",
342
+ };
343
+
344
+ const result = stringify(data, { delimiter: "+++" });
345
+ expect(result).toBe(`+++
346
+ title: Test
347
+ +++
348
+ `);
349
+ });
350
+
351
+ it("should quote strings with special characters", () => {
352
+ const data = {
353
+ title: "Title: With Colon",
354
+ description: "Has # hash",
355
+ };
356
+
357
+ const result = stringify(data);
358
+ expect(result).toContain('"Title: With Colon"');
359
+ expect(result).toContain('"Has # hash"');
360
+ });
361
+
362
+ it("should handle empty object", () => {
363
+ const data = {};
364
+
365
+ const result = stringify(data);
366
+ expect(result).toBe(`---
367
+ ---
368
+ `);
369
+ });
370
+
371
+ it("should handle arrays", () => {
372
+ const data = {
373
+ tags: ["typescript", "testing", "bun"],
374
+ items: [1, 2, 3],
375
+ };
376
+
377
+ const result = stringify(data);
378
+ expect(result).toContain("tags:");
379
+ expect(result).toContain("- typescript");
380
+ expect(result).toContain("- testing");
381
+ expect(result).toContain("- bun");
382
+ });
383
+ });
384
+
385
+ describe("roundtrip", () => {
386
+ it("should parse and stringify back to similar format", () => {
387
+ const original = {
388
+ title: "My Title",
389
+ description: "My Description",
390
+ nested: {
391
+ key: "value",
392
+ },
393
+ };
394
+
395
+ const stringified = stringify(original);
396
+ const parsed = parse(stringified);
397
+
398
+ expect(parsed).toEqual(original);
399
+ });
400
+
401
+ it("should handle complex nested structures", () => {
402
+ const original = {
403
+ title: "Complex",
404
+ metadata: {
405
+ author: "John Doe",
406
+ date: "2024-01-01",
407
+ tags: {
408
+ primary: "typescript",
409
+ secondary: "testing",
410
+ },
411
+ },
412
+ settings: {
413
+ enabled: true,
414
+ count: 42,
415
+ },
416
+ };
417
+
418
+ const stringified = stringify(original);
419
+ const parsed = parse(stringified);
420
+
421
+ expect(parsed).toEqual(original);
422
+ });
423
+
424
+ it("should preserve data types through roundtrip", () => {
425
+ const original = {
426
+ string: "hello",
427
+ number: 42,
428
+ float: 3.14,
429
+ boolean_true: true,
430
+ boolean_false: false,
431
+ null_value: null,
432
+ };
433
+
434
+ const stringified = stringify(original);
435
+ const parsed = parse(stringified);
436
+
437
+ expect(parsed).toEqual(original);
438
+ expect(typeof parsed.string).toBe("string");
439
+ expect(typeof parsed.number).toBe("number");
440
+ expect(typeof parsed.float).toBe("number");
441
+ expect(typeof parsed.boolean_true).toBe("boolean");
442
+ expect(typeof parsed.boolean_false).toBe("boolean");
443
+ expect(parsed.null_value).toBeNull();
444
+ });
445
+ });
446
+
447
+ describe("edge cases", () => {
448
+ it("should handle empty string", () => {
449
+ const result = parse("");
450
+ expect(result).toEqual({});
451
+ });
452
+
453
+ it("should handle only delimiters", () => {
454
+ const content = `---
455
+ ---`;
456
+ const result = parse(content);
457
+ expect(result).toEqual({});
458
+ });
459
+
460
+ it("should handle whitespace-only frontmatter", () => {
461
+ const content = `---
462
+
463
+
464
+ ---`;
465
+ const result = parse(content);
466
+ expect(result).toEqual({});
467
+ });
468
+
469
+ it("should handle keys with no values correctly", () => {
470
+ const content = `---
471
+ title:
472
+ description: Has value
473
+ ---`;
474
+ const result = parse(content);
475
+ expect(result).toEqual({
476
+ title: {},
477
+ description: "Has value",
478
+ });
479
+ });
480
+
481
+ it("should handle multiple colons in value", () => {
482
+ const content = `---
483
+ url: "https://example.com:8080/path"
484
+ ---`;
485
+ const result = parse(content);
486
+ expect(result).toEqual({
487
+ url: "https://example.com:8080/path",
488
+ });
489
+ });
490
+ });
491
+ });