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.
- package/.bunli/commands.gen.ts +87 -0
- package/.github/workflows/ci.yml +31 -0
- package/.github/workflows/release.yml +140 -0
- package/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +4 -0
- package/.zed/settings.json +76 -0
- package/README.md +15 -0
- package/bunli.config.ts +11 -0
- package/bunup.config.ts +31 -0
- package/package.json +36 -0
- package/src/adapters/assets/index.ts +1 -0
- package/src/adapters/assets/specifications.ts +70 -0
- package/src/adapters/beads/agents.ts +105 -0
- package/src/adapters/beads/config.ts +17 -0
- package/src/adapters/beads/index.ts +4 -0
- package/src/adapters/beads/issues.ts +156 -0
- package/src/adapters/beads/specifications.ts +55 -0
- package/src/adapters/environments/index.ts +43 -0
- package/src/adapters/environments/worktrees.ts +78 -0
- package/src/adapters/teammates/index.ts +15 -0
- package/src/assets/agent/planner.md +196 -0
- package/src/assets/command/brainstorm.md +60 -0
- package/src/assets/command/specify.md +135 -0
- package/src/assets/command/work.md +247 -0
- package/src/assets/index.ts +37 -0
- package/src/cli/commands/manifest.ts +6 -0
- package/src/cli/commands/spec/sync.ts +47 -0
- package/src/cli/commands/work.ts +110 -0
- package/src/cli/index.ts +11 -0
- package/src/plugin.ts +45 -0
- package/src/tools/i-am-done.ts +44 -0
- package/src/tools/i-am-stuck.ts +49 -0
- package/src/tools/index.ts +2 -0
- package/src/use-cases/index.ts +5 -0
- package/src/use-cases/inject-beads-issue.ts +97 -0
- package/src/use-cases/sync-specifications.ts +48 -0
- package/src/use-cases/sync-teammates.ts +35 -0
- package/src/use-cases/track-specs.ts +91 -0
- package/src/use-cases/work-on-issue.ts +110 -0
- package/src/utils/chain.ts +60 -0
- package/src/utils/frontmatter.spec.ts +491 -0
- package/src/utils/frontmatter.ts +317 -0
- package/src/utils/opencode.ts +102 -0
- package/src/utils/polling.ts +41 -0
- package/src/utils/projects.ts +35 -0
- package/src/utils/shell/client.spec.ts +106 -0
- package/src/utils/shell/client.ts +117 -0
- package/src/utils/shell/error.ts +29 -0
- package/src/utils/shell/index.ts +2 -0
- 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
|
+
});
|