berget 2.2.7 → 2.2.9

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 (130) hide show
  1. package/.github/workflows/publish.yml +6 -6
  2. package/.github/workflows/test.yml +1 -1
  3. package/.prettierrc +5 -3
  4. package/dist/index.js +24 -25
  5. package/dist/package.json +7 -3
  6. package/dist/src/agents/app.js +8 -8
  7. package/dist/src/agents/backend.js +3 -3
  8. package/dist/src/agents/devops.js +8 -8
  9. package/dist/src/agents/frontend.js +3 -3
  10. package/dist/src/agents/fullstack.js +3 -3
  11. package/dist/src/agents/index.js +18 -18
  12. package/dist/src/agents/quality.js +8 -8
  13. package/dist/src/agents/security.js +8 -8
  14. package/dist/src/client.js +115 -127
  15. package/dist/src/commands/api-keys.js +181 -202
  16. package/dist/src/commands/auth.js +16 -25
  17. package/dist/src/commands/autocomplete.js +8 -8
  18. package/dist/src/commands/billing.js +10 -19
  19. package/dist/src/commands/chat.js +139 -170
  20. package/dist/src/commands/clusters.js +21 -30
  21. package/dist/src/commands/code/__tests__/auth-sync.test.js +189 -186
  22. package/dist/src/commands/code/__tests__/fake-api-key-service.js +3 -13
  23. package/dist/src/commands/code/__tests__/fake-auth-service.js +21 -29
  24. package/dist/src/commands/code/__tests__/fake-command-runner.js +22 -33
  25. package/dist/src/commands/code/__tests__/fake-file-store.js +19 -41
  26. package/dist/src/commands/code/__tests__/fake-prompter.js +81 -97
  27. package/dist/src/commands/code/__tests__/setup-flow.test.js +295 -295
  28. package/dist/src/commands/code/adapters/clack-prompter.js +15 -32
  29. package/dist/src/commands/code/adapters/fs-file-store.js +25 -44
  30. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -41
  31. package/dist/src/commands/code/auth-sync.js +215 -228
  32. package/dist/src/commands/code/errors.js +15 -12
  33. package/dist/src/commands/code/setup.js +390 -425
  34. package/dist/src/commands/code.js +279 -294
  35. package/dist/src/commands/index.js +5 -5
  36. package/dist/src/commands/models.js +16 -25
  37. package/dist/src/commands/users.js +9 -18
  38. package/dist/src/constants/command-structure.js +138 -138
  39. package/dist/src/services/api-key-service.js +132 -152
  40. package/dist/src/services/auth-service.js +81 -95
  41. package/dist/src/services/browser-auth.js +121 -131
  42. package/dist/src/services/chat-service.js +369 -386
  43. package/dist/src/services/cluster-service.js +47 -62
  44. package/dist/src/services/collaborator-service.js +9 -21
  45. package/dist/src/services/flux-service.js +13 -25
  46. package/dist/src/services/helm-service.js +9 -21
  47. package/dist/src/services/kubectl-service.js +15 -29
  48. package/dist/src/utils/config-checker.js +8 -8
  49. package/dist/src/utils/config-loader.js +109 -109
  50. package/dist/src/utils/default-api-key.js +129 -139
  51. package/dist/src/utils/env-manager.js +55 -66
  52. package/dist/src/utils/error-handler.js +62 -62
  53. package/dist/src/utils/logger.js +74 -67
  54. package/dist/src/utils/markdown-renderer.js +28 -28
  55. package/dist/src/utils/opencode-validator.js +67 -69
  56. package/dist/src/utils/token-manager.js +67 -65
  57. package/dist/tests/commands/chat.test.js +30 -39
  58. package/dist/tests/commands/code.test.js +186 -195
  59. package/dist/tests/utils/config-loader.test.js +107 -107
  60. package/dist/tests/utils/env-manager.test.js +81 -90
  61. package/dist/tests/utils/opencode-validator.test.js +42 -41
  62. package/dist/vitest.config.js +1 -1
  63. package/eslint.config.mjs +65 -30
  64. package/index.ts +30 -31
  65. package/package.json +7 -3
  66. package/src/agents/app.ts +9 -9
  67. package/src/agents/backend.ts +4 -4
  68. package/src/agents/devops.ts +9 -9
  69. package/src/agents/frontend.ts +4 -4
  70. package/src/agents/fullstack.ts +4 -4
  71. package/src/agents/index.ts +27 -25
  72. package/src/agents/quality.ts +9 -9
  73. package/src/agents/security.ts +9 -9
  74. package/src/agents/types.ts +10 -10
  75. package/src/client.ts +85 -77
  76. package/src/commands/api-keys.ts +180 -185
  77. package/src/commands/auth.ts +15 -14
  78. package/src/commands/autocomplete.ts +10 -10
  79. package/src/commands/billing.ts +13 -12
  80. package/src/commands/chat.ts +145 -142
  81. package/src/commands/clusters.ts +20 -19
  82. package/src/commands/code/__tests__/auth-sync.test.ts +176 -175
  83. package/src/commands/code/__tests__/fake-api-key-service.ts +2 -2
  84. package/src/commands/code/__tests__/fake-auth-service.ts +18 -18
  85. package/src/commands/code/__tests__/fake-command-runner.ts +28 -22
  86. package/src/commands/code/__tests__/fake-file-store.ts +15 -15
  87. package/src/commands/code/__tests__/fake-prompter.ts +86 -85
  88. package/src/commands/code/__tests__/setup-flow.test.ts +253 -251
  89. package/src/commands/code/adapters/clack-prompter.ts +32 -30
  90. package/src/commands/code/adapters/fs-file-store.ts +18 -17
  91. package/src/commands/code/adapters/spawn-command-runner.ts +20 -15
  92. package/src/commands/code/auth-sync.ts +210 -210
  93. package/src/commands/code/errors.ts +11 -11
  94. package/src/commands/code/ports/auth-services.ts +7 -7
  95. package/src/commands/code/ports/command-runner.ts +2 -2
  96. package/src/commands/code/ports/file-store.ts +3 -3
  97. package/src/commands/code/ports/prompter.ts +13 -13
  98. package/src/commands/code/setup.ts +408 -406
  99. package/src/commands/code.ts +288 -287
  100. package/src/commands/index.ts +11 -10
  101. package/src/commands/models.ts +19 -18
  102. package/src/commands/users.ts +11 -10
  103. package/src/constants/command-structure.ts +159 -159
  104. package/src/services/api-key-service.ts +85 -85
  105. package/src/services/auth-service.ts +55 -54
  106. package/src/services/browser-auth.ts +62 -62
  107. package/src/services/chat-service.ts +170 -171
  108. package/src/services/cluster-service.ts +28 -28
  109. package/src/services/collaborator-service.ts +6 -6
  110. package/src/services/flux-service.ts +17 -17
  111. package/src/services/helm-service.ts +11 -11
  112. package/src/services/kubectl-service.ts +12 -12
  113. package/src/types/api.d.ts +1933 -1933
  114. package/src/types/json.d.ts +1 -1
  115. package/src/utils/config-checker.ts +7 -7
  116. package/src/utils/config-loader.ts +130 -129
  117. package/src/utils/default-api-key.ts +81 -80
  118. package/src/utils/env-manager.ts +37 -37
  119. package/src/utils/error-handler.ts +64 -64
  120. package/src/utils/logger.ts +72 -66
  121. package/src/utils/markdown-renderer.ts +28 -28
  122. package/src/utils/opencode-validator.ts +72 -71
  123. package/src/utils/token-manager.ts +69 -68
  124. package/tests/commands/chat.test.ts +32 -31
  125. package/tests/commands/code.test.ts +182 -181
  126. package/tests/utils/config-loader.test.ts +111 -110
  127. package/tests/utils/env-manager.test.ts +83 -79
  128. package/tests/utils/opencode-validator.test.ts +43 -42
  129. package/tsconfig.json +2 -1
  130. package/vitest.config.ts +2 -2
@@ -1,4 +1,4 @@
1
- import type { ApiKeyServicePort } from "../ports/auth-services";
1
+ import type { ApiKeyServicePort } from '../ports/auth-services';
2
2
 
3
3
  export class FakeApiKeyService implements ApiKeyServicePort {
4
4
  private readonly _key: string;
@@ -7,7 +7,7 @@ export class FakeApiKeyService implements ApiKeyServicePort {
7
7
  this._key = key;
8
8
  }
9
9
 
10
- async create(_options: { name: string; description?: string }): Promise<{ key: string }> {
10
+ async create(_options: { description?: string; name: string }): Promise<{ key: string }> {
11
11
  return { key: this._key };
12
12
  }
13
13
  }
@@ -1,14 +1,4 @@
1
- import type { AuthServicePort } from "../ports/auth-services";
2
-
3
- function base64urlEncode(data: string): string {
4
- return Buffer.from(data).toString("base64url");
5
- }
6
-
7
- function makeJwt(payload: Record<string, unknown>): string {
8
- const header = base64urlEncode(JSON.stringify({ alg: "none", typ: "JWT" }));
9
- const body = base64urlEncode(JSON.stringify(payload));
10
- return `${header}.${body}.signature`;
11
- }
1
+ import type { AuthServicePort } from '../ports/auth-services';
12
2
 
13
3
  export class FakeAuthService implements AuthServicePort {
14
4
  loginCallCount = 0;
@@ -17,7 +7,7 @@ export class FakeAuthService implements AuthServicePort {
17
7
  constructor(
18
8
  private readonly _shouldSucceed: boolean,
19
9
  private readonly _hasSeat: boolean = true,
20
- private readonly _validToken: boolean = true
10
+ private readonly _validToken: boolean = true,
21
11
  ) {}
22
12
 
23
13
  async login(): Promise<boolean> {
@@ -25,26 +15,36 @@ export class FakeAuthService implements AuthServicePort {
25
15
  return this._shouldSucceed;
26
16
  }
27
17
 
28
- loginInteractive(): ReturnType<AuthServicePort["loginInteractive"]> {
18
+ loginInteractive(): ReturnType<AuthServicePort['loginInteractive']> {
29
19
  this.loginInteractiveCallCount++;
30
20
  if (!this._shouldSucceed) {
31
- return Promise.resolve({ success: false, error: "Login failed" });
21
+ return Promise.resolve({ error: 'Login failed', success: false });
32
22
  }
33
23
 
34
24
  const farFuture = Math.floor(Date.now() / 1000) + 3600 * 24 * 365; // 1 year from now in seconds
35
25
 
36
26
  const accessToken = this._validToken
37
27
  ? makeJwt({
38
- realm_access: { roles: this._hasSeat ? ["berget_code_seat"] : ["default-roles-berget"] },
39
28
  exp: farFuture,
29
+ realm_access: { roles: this._hasSeat ? ['berget_code_seat'] : ['default-roles-berget'] },
40
30
  })
41
- : "invalid.token.here";
31
+ : 'invalid.token.here';
42
32
 
43
33
  return Promise.resolve({
44
- success: true,
45
34
  accessToken,
46
- refreshToken: "refresh",
47
35
  expiresIn: 3600,
36
+ refreshToken: 'refresh',
37
+ success: true,
48
38
  });
49
39
  }
50
40
  }
41
+
42
+ function base64urlEncode(data: string): string {
43
+ return Buffer.from(data).toString('base64url');
44
+ }
45
+
46
+ function makeJwt(payload: Record<string, unknown>): string {
47
+ const header = base64urlEncode(JSON.stringify({ alg: 'none', typ: 'JWT' }));
48
+ const body = base64urlEncode(JSON.stringify(payload));
49
+ return `${header}.${body}.signature`;
50
+ }
@@ -1,19 +1,28 @@
1
- import type { CommandRunner } from "../ports/command-runner";
1
+ import type { CommandRunner } from '../ports/command-runner';
2
2
 
3
3
  type Handler = {
4
- match: (command: string, args: readonly string[]) => boolean;
5
- response: string | Error | ((command: string, args: readonly string[]) => string | Error);
4
+ match: (command: string, arguments_: readonly string[]) => boolean;
5
+ response: ((command: string, arguments_: readonly string[]) => Error | string) | Error | string;
6
6
  };
7
7
 
8
8
  export class FakeCommandRunner implements CommandRunner {
9
+ get calls() {
10
+ return this._calls;
11
+ }
12
+ private _calls: Array<{ args: string[]; command: string; options?: { cwd?: string } }> = [];
13
+
9
14
  private handlers: Handler[] = [];
10
- private _calls: Array<{ command: string; args: string[]; options?: { cwd?: string } }> = [];
11
15
 
12
- handle(match: string | RegExp, response: string | Error): this {
16
+ checkInstalled(binary: string): Promise<boolean> {
17
+ this._calls.push({ args: [], command: `check:${binary}` });
18
+ return Promise.resolve(this.handlers.some((h) => h.match(binary, ['--version'])) || false);
19
+ }
20
+
21
+ handle(match: RegExp | string, response: Error | string): this {
13
22
  this.handlers.push({
14
- match: (cmd, args) => {
15
- const full = `${cmd} ${args.join(" ")}`;
16
- if (typeof match === "string") return full.startsWith(match);
23
+ match: (cmd, arguments_) => {
24
+ const full = `${cmd} ${arguments_.join(' ')}`;
25
+ if (typeof match === 'string') return full.startsWith(match);
17
26
  return match.test(full);
18
27
  },
19
28
  response,
@@ -21,24 +30,21 @@ export class FakeCommandRunner implements CommandRunner {
21
30
  return this;
22
31
  }
23
32
 
24
- checkInstalled(binary: string): Promise<boolean> {
25
- this._calls.push({ command: `check:${binary}`, args: [] });
26
- return Promise.resolve(this.handlers.some(h => h.match(binary, ["--version"])) || false);
27
- }
28
-
29
- async run(command: string, args: readonly string[], options?: { cwd?: string }): Promise<string> {
30
- this._calls.push({ command, args: [...args], options });
31
- const handler = this.handlers.find(h => h.match(command, args));
32
- if (!handler) throw new Error(`Unexpected command: ${command} ${args.join(" ")}`);
33
+ async run(
34
+ command: string,
35
+ arguments_: readonly string[],
36
+ options?: { cwd?: string },
37
+ ): Promise<string> {
38
+ this._calls.push({ args: [...arguments_], command, options });
39
+ const handler = this.handlers.find((h) => h.match(command, arguments_));
40
+ if (!handler) throw new Error(`Unexpected command: ${command} ${arguments_.join(' ')}`);
33
41
 
34
42
  const result =
35
- typeof handler.response === "function" ? handler.response(command, args) : handler.response;
43
+ typeof handler.response === 'function'
44
+ ? handler.response(command, arguments_)
45
+ : handler.response;
36
46
 
37
47
  if (result instanceof Error) throw result;
38
48
  return result;
39
49
  }
40
-
41
- get calls() {
42
- return this._calls;
43
- }
44
50
  }
@@ -1,4 +1,4 @@
1
- import type { FileStore } from "../ports/file-store";
1
+ import type { FileStore } from '../ports/file-store';
2
2
 
3
3
  export interface FileEntry {
4
4
  content: string;
@@ -6,39 +6,39 @@ export interface FileEntry {
6
6
  }
7
7
 
8
8
  export class FakeFileStore implements FileStore {
9
- private files: Map<string, string> = new Map();
9
+ private _chmodCalls: Array<{ mode: number; path: string }> = [];
10
10
  private dirs: Set<string> = new Set();
11
- private _chmodCalls: Array<{ path: string; mode: number }> = [];
11
+ private files: Map<string, string> = new Map();
12
12
 
13
- seed(path: string, content: string): void {
14
- this.files.set(path, content);
13
+ async chmod(path: string, mode: number): Promise<void> {
14
+ this._chmodCalls.push({ mode, path });
15
15
  }
16
16
 
17
17
  async exists(path: string): Promise<boolean> {
18
18
  return this.files.has(path) || this.dirs.has(path);
19
19
  }
20
20
 
21
- async readFile(path: string): Promise<string | null> {
22
- return this.files.get(path) ?? null;
21
+ getChmodCalls(): Array<{ mode: number; path: string }> {
22
+ return this._chmodCalls;
23
23
  }
24
24
 
25
- async writeFile(path: string, content: string): Promise<void> {
26
- this.files.set(path, content);
25
+ getWrittenFiles(): Map<string, string> {
26
+ return new Map(this.files);
27
27
  }
28
28
 
29
29
  async mkdir(path: string): Promise<void> {
30
30
  this.dirs.add(path);
31
31
  }
32
32
 
33
- async chmod(path: string, mode: number): Promise<void> {
34
- this._chmodCalls.push({ path, mode });
33
+ async readFile(path: string): Promise<null | string> {
34
+ return this.files.get(path) ?? null;
35
35
  }
36
36
 
37
- getWrittenFiles(): Map<string, string> {
38
- return new Map(this.files);
37
+ seed(path: string, content: string): void {
38
+ this.files.set(path, content);
39
39
  }
40
40
 
41
- getChmodCalls(): Array<{ path: string; mode: number }> {
42
- return this._chmodCalls;
41
+ async writeFile(path: string, content: string): Promise<void> {
42
+ this.files.set(path, content);
43
43
  }
44
44
  }
@@ -1,121 +1,122 @@
1
- import { CancelledError } from "../errors";
2
- import type { Prompter, Spinner } from "../ports/prompter";
1
+ import type { Prompter, Spinner } from '../ports/prompter';
3
2
 
4
- export const CANCEL = Symbol("cancel");
3
+ import { CancelledError } from '../errors';
4
+
5
+ export const CANCEL = Symbol('cancel');
5
6
 
6
7
  type PromptEntry =
7
- | { kind: "select"; match?: RegExp; response: string | symbol }
8
- | { kind: "confirm"; match?: RegExp; response: boolean | symbol }
9
- | { kind: "text"; match?: RegExp; response: string | symbol }
10
- | { kind: "multiselect"; match?: RegExp; response: (string | symbol)[] };
11
-
12
- export const select = <T>(value: T | symbol, match?: string | RegExp): PromptEntry => ({
13
- kind: "select",
14
- match: typeof match === "string" ? new RegExp(match) : match,
15
- response: typeof value === "symbol" ? value : String(value),
8
+ | { kind: 'confirm'; match?: RegExp; response: boolean | symbol }
9
+ | { kind: 'multiselect'; match?: RegExp; response: (string | symbol)[] }
10
+ | { kind: 'select'; match?: RegExp; response: string | symbol }
11
+ | { kind: 'text'; match?: RegExp; response: string | symbol };
12
+
13
+ export const select = <T>(value: symbol | T, match?: RegExp | string): PromptEntry => ({
14
+ kind: 'select',
15
+ match: typeof match === 'string' ? new RegExp(match) : match,
16
+ response: typeof value === 'symbol' ? value : String(value),
16
17
  });
17
18
 
18
- export const text = (value: string | symbol, match?: string | RegExp): PromptEntry => ({
19
- kind: "text",
20
- match: typeof match === "string" ? new RegExp(match) : match,
19
+ export const text = (value: string | symbol, match?: RegExp | string): PromptEntry => ({
20
+ kind: 'text',
21
+ match: typeof match === 'string' ? new RegExp(match) : match,
21
22
  response: value,
22
23
  });
23
24
 
24
- export const confirm = (value: boolean | symbol, match?: string | RegExp): PromptEntry => ({
25
- kind: "confirm",
26
- match: typeof match === "string" ? new RegExp(match) : match,
25
+ export const confirm = (value: boolean | symbol, match?: RegExp | string): PromptEntry => ({
26
+ kind: 'confirm',
27
+ match: typeof match === 'string' ? new RegExp(match) : match,
27
28
  response: value,
28
29
  });
29
30
 
30
- export const multiselect = <T>(values: T[] | symbol, match?: string | RegExp): PromptEntry => ({
31
- kind: "multiselect",
32
- match: typeof match === "string" ? new RegExp(match) : match,
33
- response:
34
- values === CANCEL ? [CANCEL] : ((values as T[]).map(v => String(v)) as (string | symbol)[]),
31
+ export const multiselect = <T>(values: symbol | T[], match?: RegExp | string): PromptEntry => ({
32
+ kind: 'multiselect',
33
+ match: typeof match === 'string' ? new RegExp(match) : match,
34
+ response: values === CANCEL ? [CANCEL] : ((values as T[]).map(String) as (string | symbol)[]),
35
35
  });
36
36
 
37
37
  export class FakePrompter implements Prompter {
38
- private _calls: Array<{ method: string; args: unknown }> = [];
38
+ get calls() {
39
+ return this._calls;
40
+ }
41
+ private _calls: Array<{ args: unknown; method: string }> = [];
42
+
39
43
  private _cursor = 0;
40
44
 
41
45
  constructor(private readonly _script: PromptEntry[]) {}
42
-
46
+ assertExhausted() {
47
+ if (this._cursor !== this._script.length) {
48
+ throw new Error(`Script not exhausted: ${this._script.length - this._cursor} entries left`);
49
+ }
50
+ }
51
+ async confirm(options: { message: string }): Promise<boolean> {
52
+ this._calls.push({ args: options, method: 'confirm' });
53
+ const entry = this._script[this._cursor++];
54
+ if (!entry)
55
+ throw new Error(`No script entry for confirm #${this._cursor} (${options.message})`);
56
+ if (entry.kind !== 'confirm')
57
+ throw new Error(`Expected confirm, got ${entry.kind} for ${options.message}`);
58
+ if (entry.match && !entry.match.test(options.message))
59
+ throw new Error(`Message mismatch: got "${options.message}"`);
60
+ if (entry.response === CANCEL) throw new CancelledError();
61
+ return entry.response as boolean;
62
+ }
43
63
  intro(message: string): void {
44
- this._calls.push({ method: "intro", args: { message } });
64
+ this._calls.push({ args: { message }, method: 'intro' });
45
65
  }
46
- outro(message: string): void {
47
- this._calls.push({ method: "outro", args: { message } });
66
+
67
+ async multiselect<T>(options: { message: string }): Promise<T[]> {
68
+ this._calls.push({ args: options, method: 'multiselect' });
69
+ const entry = this._script[this._cursor++];
70
+ if (!entry)
71
+ throw new Error(`No script entry for multiselect #${this._cursor} (${options.message})`);
72
+ if (entry.kind !== 'multiselect')
73
+ throw new Error(`Expected multiselect, got ${entry.kind} for ${options.message}`);
74
+ if (entry.match && !entry.match.test(options.message))
75
+ throw new Error(`Message mismatch: got "${options.message}"`);
76
+ if (entry.response.includes(CANCEL)) throw new CancelledError();
77
+ return entry.response as T[];
48
78
  }
79
+
49
80
  note(message: string, title?: string): void {
50
- this._calls.push({ method: "note", args: { message, title } });
81
+ this._calls.push({ args: { message, title }, method: 'note' });
51
82
  }
52
- spinner(): Spinner {
53
- return {
54
- start: (msg: string) => {
55
- this._calls.push({ method: "spinner.start", args: { message: msg } });
56
- },
57
- stop: (msg: string) => {
58
- this._calls.push({ method: "spinner.stop", args: { message: msg } });
59
- },
60
- };
83
+
84
+ outro(message: string): void {
85
+ this._calls.push({ args: { message }, method: 'outro' });
61
86
  }
62
87
 
63
- async select<T>(opts: { message: string }): Promise<T> {
64
- this._calls.push({ method: "select", args: opts });
88
+ async select<T>(options: { message: string }): Promise<T> {
89
+ this._calls.push({ args: options, method: 'select' });
65
90
  const entry = this._script[this._cursor++];
66
- if (!entry) throw new Error(`No script entry for select #${this._cursor} (${opts.message})`);
67
- if (entry.kind !== "select")
68
- throw new Error(`Expected select, got ${entry.kind} for ${opts.message}`);
69
- if (entry.match && !entry.match.test(opts.message))
70
- throw new Error(`Message mismatch: got "${opts.message}"`);
91
+ if (!entry) throw new Error(`No script entry for select #${this._cursor} (${options.message})`);
92
+ if (entry.kind !== 'select')
93
+ throw new Error(`Expected select, got ${entry.kind} for ${options.message}`);
94
+ if (entry.match && !entry.match.test(options.message))
95
+ throw new Error(`Message mismatch: got "${options.message}"`);
71
96
  if (entry.response === CANCEL) throw new CancelledError();
72
97
  return entry.response as T;
73
98
  }
74
99
 
75
- async confirm(opts: { message: string }): Promise<boolean> {
76
- this._calls.push({ method: "confirm", args: opts });
77
- const entry = this._script[this._cursor++];
78
- if (!entry) throw new Error(`No script entry for confirm #${this._cursor} (${opts.message})`);
79
- if (entry.kind !== "confirm")
80
- throw new Error(`Expected confirm, got ${entry.kind} for ${opts.message}`);
81
- if (entry.match && !entry.match.test(opts.message))
82
- throw new Error(`Message mismatch: got "${opts.message}"`);
83
- if (entry.response === CANCEL) throw new CancelledError();
84
- return entry.response as boolean;
100
+ spinner(): Spinner {
101
+ return {
102
+ start: (message: string) => {
103
+ this._calls.push({ args: { message: message }, method: 'spinner.start' });
104
+ },
105
+ stop: (message: string) => {
106
+ this._calls.push({ args: { message: message }, method: 'spinner.stop' });
107
+ },
108
+ };
85
109
  }
86
110
 
87
- async text(opts: { message: string }): Promise<string> {
88
- this._calls.push({ method: "text", args: opts });
111
+ async text(options: { message: string }): Promise<string> {
112
+ this._calls.push({ args: options, method: 'text' });
89
113
  const entry = this._script[this._cursor++];
90
- if (!entry) throw new Error(`No script entry for text #${this._cursor} (${opts.message})`);
91
- if (entry.kind !== "text")
92
- throw new Error(`Expected text, got ${entry.kind} for ${opts.message}`);
93
- if (entry.match && !entry.match.test(opts.message))
94
- throw new Error(`Message mismatch: got "${opts.message}"`);
114
+ if (!entry) throw new Error(`No script entry for text #${this._cursor} (${options.message})`);
115
+ if (entry.kind !== 'text')
116
+ throw new Error(`Expected text, got ${entry.kind} for ${options.message}`);
117
+ if (entry.match && !entry.match.test(options.message))
118
+ throw new Error(`Message mismatch: got "${options.message}"`);
95
119
  if (entry.response === CANCEL) throw new CancelledError();
96
120
  return entry.response as string;
97
121
  }
98
-
99
- async multiselect<T>(opts: { message: string }): Promise<T[]> {
100
- this._calls.push({ method: "multiselect", args: opts });
101
- const entry = this._script[this._cursor++];
102
- if (!entry)
103
- throw new Error(`No script entry for multiselect #${this._cursor} (${opts.message})`);
104
- if (entry.kind !== "multiselect")
105
- throw new Error(`Expected multiselect, got ${entry.kind} for ${opts.message}`);
106
- if (entry.match && !entry.match.test(opts.message))
107
- throw new Error(`Message mismatch: got "${opts.message}"`);
108
- if (entry.response.includes(CANCEL)) throw new CancelledError();
109
- return entry.response as T[];
110
- }
111
-
112
- get calls() {
113
- return this._calls;
114
- }
115
-
116
- assertExhausted() {
117
- if (this._cursor !== this._script.length) {
118
- throw new Error(`Script not exhausted: ${this._script.length - this._cursor} entries left`);
119
- }
120
- }
121
122
  }