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,317 @@
1
+ import { isString, isObject, isArray, isNullish } from "radashi";
2
+
3
+ export type Metadata = Record<string, unknown>;
4
+
5
+ export interface Options {
6
+ /** @default '---' */
7
+ delimiter?: string;
8
+ }
9
+
10
+ /**
11
+ * Parses frontmatter from a content string and returns the data object.
12
+ *
13
+ * Extracts YAML-formatted frontmatter from the beginning of a string, parsing it
14
+ * into a JavaScript object. The frontmatter must be enclosed by delimiter lines
15
+ * (default: `---`).
16
+ *
17
+ * @param content - The string containing frontmatter and optional content
18
+ * @param options - Configuration options
19
+ * @param options.delimiter - The delimiter string to use (default: '---')
20
+ *
21
+ * @returns Parsed frontmatter as a data object
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const content = `---
26
+ * title: My Title
27
+ * description: My Description
28
+ * nested:
29
+ * key: value
30
+ * ---
31
+ *
32
+ * any other content`;
33
+ *
34
+ * const metadata = parse(content);
35
+ * // { title: "My Title", description: "My Description", nested: { key: "value" } }
36
+ * ```
37
+ */
38
+ export function parse(content: string, options?: Options): Metadata {
39
+ const { metadata } = extract(content, options);
40
+ return metadata;
41
+ }
42
+
43
+ /**
44
+ * Extracts both frontmatter data and remaining content from a string.
45
+ *
46
+ * Similar to `parse()`, but also returns the content that appears after the
47
+ * closing frontmatter delimiter. Useful when you need to process both metadata
48
+ * and document body.
49
+ *
50
+ * @param content - The string containing frontmatter and content
51
+ * @param options - Configuration options
52
+ * @param options.delimiter - The delimiter string to use (default: '---')
53
+ *
54
+ * @returns An object containing both `metadata` (parsed frontmatter) and `content` (remaining text)
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const content = `---
59
+ * title: My Title
60
+ * ---
61
+ *
62
+ * # Main Content
63
+ * This is the body.`;
64
+ *
65
+ * const result = extract(content);
66
+ * // {
67
+ * // metadata: { title: "My Title" },
68
+ * // content: "\n# Main Content\nThis is the body."
69
+ * // }
70
+ * ```
71
+ */
72
+ export function extract(
73
+ content: string,
74
+ options?: Options,
75
+ ): { metadata: Metadata; content: string } {
76
+ const delimiter = options?.delimiter ?? "---";
77
+
78
+ // Check if content starts with delimiter (allowing leading whitespace)
79
+ if (!content.trimStart().startsWith(delimiter)) {
80
+ return { metadata: {}, content };
81
+ }
82
+
83
+ const lines = content.split("\n");
84
+ let startIndex = -1;
85
+ let endIndex = -1;
86
+
87
+ // Find the opening delimiter
88
+ for (let i = 0; i < lines.length; i++) {
89
+ if (lines[i]?.trim() === delimiter) {
90
+ startIndex = i;
91
+ break;
92
+ }
93
+ }
94
+
95
+ // No valid opening delimiter found
96
+ if (startIndex === -1) {
97
+ return { metadata: {}, content };
98
+ }
99
+
100
+ // Find the closing delimiter
101
+ for (let i = startIndex + 1; i < lines.length; i++) {
102
+ if (lines[i]?.trim() === delimiter) {
103
+ endIndex = i;
104
+ break;
105
+ }
106
+ }
107
+
108
+ // No valid closing delimiter found
109
+ if (endIndex === -1) {
110
+ return { metadata: {}, content };
111
+ }
112
+
113
+ // Extract and parse the frontmatter content
114
+ const frontmatterLines = lines.slice(startIndex + 1, endIndex);
115
+ const yamlContent = frontmatterLines.join("\n");
116
+ const metadata = parseYAML(yamlContent);
117
+
118
+ // Extract remaining content after frontmatter
119
+ const remainingContent = lines
120
+ .slice(endIndex + 1)
121
+ .join("\n")
122
+ .replace(/^\n+/, "");
123
+
124
+ return { metadata, content: remainingContent };
125
+ }
126
+
127
+ /**
128
+ * Converts a data object into YAML-formatted frontmatter string.
129
+ *
130
+ * Serializes a JavaScript object into YAML format and wraps it with delimiter
131
+ * lines. The output can be prepended to content to create a complete document
132
+ * with frontmatter.
133
+ *
134
+ * @param data - The data object to convert to frontmatter
135
+ * @param options - Configuration options
136
+ * @param options.delimiter - The delimiter string to use (default: '---')
137
+ *
138
+ * @returns YAML frontmatter string with delimiters
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * const data = {
143
+ * title: "My Title",
144
+ * nested: { key: "value" }
145
+ * };
146
+ *
147
+ * const frontmatter = stringify(data);
148
+ * // ---
149
+ * // title: My Title
150
+ * // nested:
151
+ * // key: value
152
+ * // ---
153
+ * ```
154
+ */
155
+ export function stringify(data: Metadata, options?: Options): string {
156
+ const delimiter = options?.delimiter ?? "---";
157
+ const yamlContent = stringifyYAML(data);
158
+ return `${delimiter}\n${yamlContent}${delimiter}\n`;
159
+ }
160
+
161
+ export function apply(content: string, data: Metadata, options?: Options): string {
162
+ const frontmatter = stringify(data, options);
163
+ return `${frontmatter}\n${content}`;
164
+ }
165
+
166
+ /**
167
+ * Parses a YAML string into a JavaScript object.
168
+ *
169
+ * This is a lightweight YAML parser that supports basic key-value pairs,
170
+ * nested objects, and common data types. It's designed specifically for
171
+ * frontmatter use cases.
172
+ *
173
+ * Supported features:
174
+ * - String, number, boolean, and null values
175
+ * - Nested objects (via indentation)
176
+ * - Single and double quoted strings
177
+ * - Comments (lines starting with #)
178
+ *
179
+ * @param yaml - The YAML string to parse
180
+ * @returns Parsed data object
181
+ */
182
+ function parseYAML(yaml: string): Metadata {
183
+ const result: Metadata = {};
184
+ const lines = yaml.split("\n");
185
+ const stack: Array<{ obj: Metadata; indent: number }> = [{ obj: result, indent: -1 }];
186
+
187
+ for (const line of lines) {
188
+ // Skip empty lines and comments
189
+ if (!line.trim() || line.trim().startsWith("#")) {
190
+ continue;
191
+ }
192
+
193
+ const indent = line.length - (line.trimStart()?.length ?? 0);
194
+ const trimmed = line.trim();
195
+
196
+ // Pop stack to find the correct parent based on indentation
197
+ while (stack.length > 1) {
198
+ const top = stack[stack.length - 1];
199
+ if (!top || indent > top.indent) {
200
+ break;
201
+ }
202
+ stack.pop();
203
+ }
204
+
205
+ // Parse key-value pair
206
+ const colonIndex = trimmed.indexOf(":");
207
+ if (colonIndex === -1) {
208
+ continue;
209
+ }
210
+
211
+ const key = trimmed.substring(0, colonIndex).trim();
212
+ const valueStr = trimmed.substring(colonIndex + 1).trim();
213
+
214
+ const currentObj = stack[stack.length - 1]?.obj;
215
+ if (!currentObj) continue;
216
+
217
+ if (valueStr === "") {
218
+ // Empty value indicates a nested object
219
+ const nestedObj: Metadata = {};
220
+ currentObj[key] = nestedObj;
221
+ stack.push({ obj: nestedObj, indent });
222
+ } else {
223
+ // Parse the value and assign it
224
+ if (currentObj) {
225
+ currentObj[key] = parseYAMLValue(valueStr);
226
+ }
227
+ }
228
+ }
229
+
230
+ return result;
231
+ }
232
+
233
+ /**
234
+ * Parses a YAML value string into its JavaScript type.
235
+ *
236
+ * Handles type coercion for strings, numbers, booleans, and null values.
237
+ * Quoted strings are unquoted, and special YAML values are converted.
238
+ *
239
+ * @param value - The YAML value string to parse
240
+ * @returns The parsed value with appropriate type
241
+ */
242
+ function parseYAMLValue(value: string): unknown {
243
+ // Remove quotes from quoted strings
244
+ if (
245
+ (value.startsWith('"') && value.endsWith('"')) ||
246
+ (value.startsWith("'") && value.endsWith("'"))
247
+ ) {
248
+ return value.slice(1, -1);
249
+ }
250
+
251
+ // Parse boolean values
252
+ if (value === "true") return true;
253
+ if (value === "false") return false;
254
+
255
+ // Parse null/undefined
256
+ if (value === "null" || value === "~") return null;
257
+
258
+ // Try to parse as number
259
+ const num = Number(value);
260
+ if (!Number.isNaN(num) && value !== "") {
261
+ return num;
262
+ }
263
+
264
+ // Return as string by default
265
+ return value;
266
+ }
267
+
268
+ /**
269
+ * Converts a JavaScript object to YAML format.
270
+ *
271
+ * Recursively serializes an object into YAML syntax with proper indentation.
272
+ * Handles nested objects, arrays, and various data types. Strings with special
273
+ * characters are automatically quoted.
274
+ *
275
+ * @param data - The data object to stringify
276
+ * @param indent - Current indentation level (internal use)
277
+ * @returns YAML formatted string
278
+ */
279
+ function stringifyYAML(data: Metadata, indent = 0): string {
280
+ const indentStr = " ".repeat(indent);
281
+ let result = "";
282
+
283
+ for (const [key, value] of Object.entries(data)) {
284
+ // Handle null and undefined
285
+ if (isNullish(value)) {
286
+ result += `${indentStr}${key}: null\n`;
287
+ }
288
+ // Handle nested objects (but not arrays)
289
+ else if (isObject(value) && !isArray(value)) {
290
+ result += `${indentStr}${key}:\n`;
291
+ result += stringifyYAML(value as Metadata, indent + 1);
292
+ }
293
+ // Handle strings
294
+ else if (isString(value)) {
295
+ // Quote strings that contain special YAML characters
296
+ const needsQuotes = value.includes(":") || value.includes("#") || value.includes("\n");
297
+ result += `${indentStr}${key}: ${needsQuotes ? `"${value}"` : value}\n`;
298
+ }
299
+ // Handle arrays
300
+ else if (isArray(value)) {
301
+ result += `${indentStr}${key}:\n`;
302
+ for (const item of value) {
303
+ if (isObject(item) && !isArray(item)) {
304
+ result += `${indentStr}- \n${stringifyYAML(item as Metadata, indent + 2)}`;
305
+ } else {
306
+ result += `${indentStr} - ${item}\n`;
307
+ }
308
+ }
309
+ }
310
+ // Handle primitives (numbers, booleans, etc.)
311
+ else {
312
+ result += `${indentStr}${key}: ${value}\n`;
313
+ }
314
+ }
315
+
316
+ return result;
317
+ }
@@ -0,0 +1,102 @@
1
+ import { createOpencodeClient, type OpencodeClientConfig } from "@opencode-ai/sdk/v2/client";
2
+ import { assign } from "radashi";
3
+
4
+ const STORE = Bun.file(
5
+ `${Bun.env.HOME}/Library/Application Support/ai.opencode.desktop/store.json`,
6
+ );
7
+
8
+ interface ServerConfig {
9
+ username?: string;
10
+ password?: string;
11
+ hostname?: string;
12
+ port?: number;
13
+ }
14
+
15
+ const context = {
16
+ client: Bun.env.OPENCODE_CLIENT ?? "unknown",
17
+ password: Bun.env.OPENCODE_SERVER_PASSWORD,
18
+ username: Bun.env.OPENCODE_SERVER_USERNAME,
19
+ };
20
+
21
+ export async function register(options: {
22
+ project?: { id?: string; worktree?: string };
23
+ directory?: string;
24
+ }) {
25
+ if (context.client === "desktop") {
26
+ const server = await getServer();
27
+ const config = {
28
+ username: context.username,
29
+ password: context.password,
30
+ seenAt: new Date(),
31
+ directory: options.directory,
32
+ project: options.project,
33
+ ...server,
34
+ };
35
+
36
+ await Bun.write(STORE, JSON.stringify(config));
37
+
38
+ return createOpencodeClient(createOpencodeClientConfigFromServerConfig(config));
39
+ }
40
+
41
+ return createOpencodeClient({
42
+ directory: options.directory,
43
+ });
44
+ }
45
+
46
+ export interface ClientClientOptions extends OpencodeClientConfig {
47
+ directory?: string;
48
+ loopUpForDesktopServer?: boolean;
49
+ }
50
+
51
+ export async function createClient(options: ClientClientOptions) {
52
+ if (options.loopUpForDesktopServer) {
53
+ const registered = await createRegisteredServerConfig();
54
+
55
+ if (registered) {
56
+ const client = createOpencodeClient(assign(options, registered ?? {}));
57
+
58
+ const health = await client.global.health();
59
+ if (health.data?.healthy) return client;
60
+ else console.warn("Desktop server is not healthy, re-launch Opencode app");
61
+ } else console.warn("Desktop server is not registered, launch Opencode app");
62
+ }
63
+
64
+ return createOpencodeClient(options);
65
+ }
66
+
67
+ async function createRegisteredServerConfig() {
68
+ const exists = await STORE.exists();
69
+ if (!exists) return undefined;
70
+
71
+ const config: ServerConfig = await STORE.json();
72
+
73
+ return createOpencodeClientConfigFromServerConfig(config);
74
+ }
75
+
76
+ function createOpencodeClientConfigFromServerConfig(
77
+ server: ServerConfig,
78
+ ): OpencodeClientConfig | undefined {
79
+ return {
80
+ baseUrl: `http://${server.hostname}:${server.port}`,
81
+ headers: {
82
+ Authorization: `Basic ${btoa(`${server.username}:${server.password}`)}`,
83
+ },
84
+ } satisfies OpencodeClientConfig;
85
+ }
86
+
87
+ async function getServer() {
88
+ const process =
89
+ await Bun.$`ps ax -o pid=,command= | grep "opencode-cli " | grep "serve" | grep -v grep`
90
+ .nothrow()
91
+ .text();
92
+
93
+ const pid = process.match(/^(\d+)\s+/)?.[1];
94
+ const hostname = process.match(/--hostname\s+([\S]+)/)?.[1];
95
+ const port = process.match(/--port\s+(\d+)/)?.[1];
96
+
97
+ return {
98
+ pid: pid ? parseInt(pid, 10) : undefined,
99
+ hostname,
100
+ port: port ? parseInt(port, 10) : undefined,
101
+ };
102
+ }
@@ -0,0 +1,41 @@
1
+ import { type DurationString, isEmpty, parseDuration, sleep } from "radashi";
2
+
3
+ export interface PollOptions {
4
+ interval?: DurationString;
5
+ /** Stop polling when the result is empty or undefined */
6
+ exitOnEmpty?: boolean;
7
+ }
8
+
9
+ export function poll<T extends Array<unknown>>(
10
+ fn: () => Promise<T | undefined>,
11
+ options?: PollOptions,
12
+ ): AsyncGenerator<T[number]>;
13
+
14
+ export function poll<T>(fn: () => Promise<T | undefined>, options?: PollOptions): AsyncGenerator<T>;
15
+
16
+ export async function* poll(fn: () => Promise<unknown>, options?: PollOptions) {
17
+ const interval = parseDuration(options?.interval ?? "1s");
18
+
19
+ while (true) {
20
+ const result = await fn();
21
+
22
+ if (Array.isArray(result)) {
23
+ if (isEmpty(result) && options?.exitOnEmpty) break;
24
+ yield* result;
25
+ } else if (result === undefined && options?.exitOnEmpty) {
26
+ break;
27
+ } else yield result;
28
+
29
+ await sleep(interval);
30
+ }
31
+ }
32
+
33
+ export type WaitForOptions = Omit<PollOptions, "exitOnEmpty">;
34
+
35
+ export async function waitFor<T>(fn: () => Promise<T | undefined>, options?: WaitForOptions) {
36
+ for await (const result of poll(fn, options)) {
37
+ if (result) return result;
38
+ }
39
+
40
+ throw Error("Timeout: waiting for condition");
41
+ }
@@ -0,0 +1,35 @@
1
+ import type { OpencodeClient, Project } from "@opencode-ai/sdk/v2/client";
2
+ import { prompt } from "@bunli/utils";
3
+ import { assert } from "radashi";
4
+
5
+ export async function use(client: OpencodeClient, directory?: string) {
6
+ const { data: project } = await client.project.current({
7
+ directory: directory ?? process.cwd(),
8
+ });
9
+
10
+ if (project && project.id !== "global") return project.worktree;
11
+
12
+ const { data: projects, error } = await client.project.list();
13
+ assert(error === undefined, `Failed to list projects: ${error}`);
14
+ assert(projects, `No projects found`);
15
+ assert(projects.length > 0, `No projects found: ${projects.length}`);
16
+
17
+ return prompt.select("Select a project", {
18
+ options: projects.map((project) => ({
19
+ label: project.name ?? project.worktree,
20
+ value: project.worktree,
21
+ hint: project.name
22
+ ? `${project.worktree} - ${project.id.slice(0, 5)}`
23
+ : project.id.slice(0, 5),
24
+ })),
25
+ });
26
+ }
27
+
28
+ export function getProjectLabel(project: Project) {
29
+ return project.name ?? project.worktree;
30
+ }
31
+
32
+ export function getProjectHint(project: Project) {
33
+ const shortId = project.id.slice(0, 5);
34
+ return project.name ? `${project.worktree} - ${shortId}` : shortId;
35
+ }
@@ -0,0 +1,106 @@
1
+ import { describe, expect, expectTypeOf, it } from "bun:test";
2
+ import { $ } from "bun";
3
+ import { toResult } from "radashi";
4
+
5
+ import * as client from "./client";
6
+ import { InvalidExitCodeError, ShellError } from "./error";
7
+
8
+ const PRINT_FLAGS_SH = `
9
+ printf "%d:" "$#"
10
+ for arg in "$@"; do
11
+ printf " <%s>" "$arg"
12
+ done
13
+ echo ""
14
+ `;
15
+
16
+ describe("shell client wrapper", () => {
17
+ it("returns the same client when already a client", () => {
18
+ const shell = client.create($);
19
+ expect(client.use(shell)).toBe(shell);
20
+ });
21
+
22
+ it("formats CLI options and positional arguments", async () => {
23
+ const shell = client.create($);
24
+
25
+ const [, output] = await shell`bash -c ${PRINT_FLAGS_SH} -- ${"pos"} ${{
26
+ flag: true,
27
+ count: 3,
28
+ list: ["a", "b"],
29
+ name: "hello world",
30
+ skip: undefined,
31
+ }}`.text();
32
+
33
+ expect(output).toBe("5: <pos> <--flag> <--count='3'> <--list=a,b> <--name='hello world'>");
34
+ });
35
+
36
+ it("ignores empty options objects", async () => {
37
+ const shell = client.create($);
38
+ const [, output] = await shell`bash -c ${PRINT_FLAGS_SH} -- pos ${{}}`.text();
39
+
40
+ expect(output).toBe("1: <pos>");
41
+ });
42
+
43
+ it("parses JSON output", async () => {
44
+ const shell = client.create($);
45
+ type Output = { ok: true };
46
+
47
+ const output = await shell`echo '{\"ok\":true}'`.json<Output>();
48
+ expect(output).toEqual({ ok: true });
49
+ expectTypeOf(output).toEqualTypeOf<Output>();
50
+
51
+ const second = await shell<Output>`echo '{\"ok\":true}'`.json();
52
+ expectTypeOf(second).toEqualTypeOf(output);
53
+ });
54
+
55
+ it("returns an error when stdout JSON parsing fails", async () => {
56
+ const shell = client.create($);
57
+
58
+ const [error] = await toResult(shell`echo 'not json'`.json());
59
+
60
+ expect(error).toBeInstanceOf(ShellError);
61
+ expect(error?.message).toBe("not json");
62
+ expect(error?.cause).toBeInstanceOf(SyntaxError);
63
+ });
64
+
65
+ it("falls back to raw stderr when JSON parsing is failing", async () => {
66
+ const shell = client.create($);
67
+
68
+ const [error] = await toResult(shell`echo "This is an error message" 2> /dev/stderr`.json());
69
+
70
+ expect(error).toBeInstanceOf(ShellError);
71
+ expect(error?.message).toBe("This is an error message");
72
+ expect(error?.cause).toBeInstanceOf(SyntaxError);
73
+ });
74
+
75
+ it("uses chained configuration methods with real commands", async () => {
76
+ const shell = client.create($);
77
+
78
+ const [, pwd] = await shell.cwd(import.meta.dir)`pwd`.text();
79
+ expect(pwd).toBe(import.meta.dir);
80
+
81
+ const [, value] = await shell.env({
82
+ TEST_ENV: "works",
83
+ })`printenv TEST_ENV`.text();
84
+ expect(value).toBe("works");
85
+ });
86
+
87
+ it("keeps the bun shell context", async () => {
88
+ const shell = client.create($.cwd(import.meta.dir).env({ TEST_ENV: "works" }));
89
+
90
+ const [, pwd] = await shell`pwd`.text();
91
+ expect(pwd).toBe(import.meta.dir);
92
+
93
+ const [, value] = await shell`printenv TEST_ENV`.text();
94
+ expect(value).toBe("works");
95
+ });
96
+
97
+ it("text() output returns error on exit 1", async () => {
98
+ const shell = client.create($);
99
+
100
+ const [error, output] = await shell`exit 1`.text();
101
+ expect(output).toBeUndefined();
102
+ expect(error).toBeInstanceOf(ShellError);
103
+ expect(error?.message).toBe("Unknown shell error");
104
+ expect(error?.cause).toBeInstanceOf(InvalidExitCodeError);
105
+ });
106
+ });