berget 2.2.6 → 2.2.8

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 (145) hide show
  1. package/.github/workflows/publish.yml +2 -2
  2. package/.github/workflows/test.yml +10 -4
  3. package/.husky/pre-commit +1 -0
  4. package/.prettierignore +15 -0
  5. package/.prettierrc +7 -3
  6. package/CONTRIBUTING.md +38 -0
  7. package/README.md +2 -148
  8. package/dist/index.js +10 -11
  9. package/dist/package.json +30 -2
  10. package/dist/src/agents/app.js +28 -0
  11. package/dist/src/agents/backend.js +25 -0
  12. package/dist/src/agents/devops.js +34 -0
  13. package/dist/src/agents/frontend.js +25 -0
  14. package/dist/src/agents/fullstack.js +25 -0
  15. package/dist/src/agents/index.js +61 -0
  16. package/dist/src/agents/quality.js +70 -0
  17. package/dist/src/agents/security.js +26 -0
  18. package/dist/src/agents/types.js +2 -0
  19. package/dist/src/client.js +97 -117
  20. package/dist/src/commands/api-keys.js +75 -90
  21. package/dist/src/commands/auth.js +7 -16
  22. package/dist/src/commands/autocomplete.js +1 -1
  23. package/dist/src/commands/billing.js +6 -17
  24. package/dist/src/commands/chat.js +68 -101
  25. package/dist/src/commands/clusters.js +9 -18
  26. package/dist/src/commands/code/__tests__/auth-sync.test.js +351 -0
  27. package/dist/src/commands/code/__tests__/fake-api-key-service.js +13 -0
  28. package/dist/src/commands/code/__tests__/fake-auth-service.js +47 -0
  29. package/dist/src/commands/code/__tests__/fake-command-runner.js +21 -34
  30. package/dist/src/commands/code/__tests__/fake-file-store.js +20 -33
  31. package/dist/src/commands/code/__tests__/fake-prompter.js +83 -57
  32. package/dist/src/commands/code/__tests__/setup-flow.test.js +359 -92
  33. package/dist/src/commands/code/adapters/clack-prompter.js +15 -22
  34. package/dist/src/commands/code/adapters/fs-file-store.js +26 -40
  35. package/dist/src/commands/code/adapters/spawn-command-runner.js +27 -37
  36. package/dist/src/commands/code/auth-sync.js +270 -0
  37. package/dist/src/commands/code/errors.js +12 -9
  38. package/dist/src/commands/code/ports/auth-services.js +2 -0
  39. package/dist/src/commands/code/setup.js +387 -281
  40. package/dist/src/commands/code.js +205 -332
  41. package/dist/src/commands/index.js +5 -5
  42. package/dist/src/commands/models.js +6 -17
  43. package/dist/src/commands/users.js +5 -16
  44. package/dist/src/constants/command-structure.js +104 -104
  45. package/dist/src/services/api-key-service.js +132 -157
  46. package/dist/src/services/auth-service.js +89 -342
  47. package/dist/src/services/browser-auth.js +268 -0
  48. package/dist/src/services/chat-service.js +371 -401
  49. package/dist/src/services/cluster-service.js +47 -62
  50. package/dist/src/services/collaborator-service.js +10 -25
  51. package/dist/src/services/flux-service.js +14 -29
  52. package/dist/src/services/helm-service.js +10 -25
  53. package/dist/src/services/kubectl-service.js +16 -33
  54. package/dist/src/utils/config-checker.js +3 -3
  55. package/dist/src/utils/config-loader.js +95 -95
  56. package/dist/src/utils/default-api-key.js +124 -134
  57. package/dist/src/utils/env-manager.js +55 -66
  58. package/dist/src/utils/error-handler.js +20 -21
  59. package/dist/src/utils/logger.js +72 -65
  60. package/dist/src/utils/markdown-renderer.js +27 -27
  61. package/dist/src/utils/opencode-validator.js +63 -68
  62. package/dist/src/utils/token-manager.js +74 -45
  63. package/dist/tests/commands/chat.test.js +16 -25
  64. package/dist/tests/commands/code.test.js +95 -104
  65. package/dist/tests/utils/config-loader.test.js +48 -48
  66. package/dist/tests/utils/env-manager.test.js +43 -52
  67. package/dist/tests/utils/opencode-validator.test.js +22 -21
  68. package/dist/vitest.config.js +1 -1
  69. package/eslint.config.mjs +67 -0
  70. package/index.ts +35 -42
  71. package/package.json +30 -2
  72. package/src/agents/app.ts +27 -0
  73. package/src/agents/backend.ts +24 -0
  74. package/src/agents/devops.ts +33 -0
  75. package/src/agents/frontend.ts +24 -0
  76. package/src/agents/fullstack.ts +24 -0
  77. package/src/agents/index.ts +73 -0
  78. package/src/agents/quality.ts +69 -0
  79. package/src/agents/security.ts +26 -0
  80. package/src/agents/types.ts +17 -0
  81. package/src/client.ts +118 -152
  82. package/src/commands/api-keys.ts +241 -333
  83. package/src/commands/auth.ts +22 -27
  84. package/src/commands/autocomplete.ts +9 -9
  85. package/src/commands/billing.ts +20 -24
  86. package/src/commands/chat.ts +248 -338
  87. package/src/commands/clusters.ts +27 -26
  88. package/src/commands/code/__tests__/auth-sync.test.ts +482 -0
  89. package/src/commands/code/__tests__/fake-api-key-service.ts +13 -0
  90. package/src/commands/code/__tests__/fake-auth-service.ts +50 -0
  91. package/src/commands/code/__tests__/fake-command-runner.ts +45 -42
  92. package/src/commands/code/__tests__/fake-file-store.ts +32 -23
  93. package/src/commands/code/__tests__/fake-prompter.ts +116 -77
  94. package/src/commands/code/__tests__/setup-flow.test.ts +624 -268
  95. package/src/commands/code/adapters/clack-prompter.ts +53 -39
  96. package/src/commands/code/adapters/fs-file-store.ts +32 -27
  97. package/src/commands/code/adapters/spawn-command-runner.ts +38 -29
  98. package/src/commands/code/auth-sync.ts +329 -0
  99. package/src/commands/code/errors.ts +18 -18
  100. package/src/commands/code/ports/auth-services.ts +14 -0
  101. package/src/commands/code/ports/command-runner.ts +8 -4
  102. package/src/commands/code/ports/file-store.ts +5 -4
  103. package/src/commands/code/ports/prompter.ts +24 -18
  104. package/src/commands/code/setup.ts +570 -340
  105. package/src/commands/code.ts +338 -539
  106. package/src/commands/index.ts +20 -19
  107. package/src/commands/models.ts +28 -32
  108. package/src/commands/users.ts +15 -21
  109. package/src/constants/command-structure.ts +134 -157
  110. package/src/services/api-key-service.ts +105 -122
  111. package/src/services/auth-service.ts +99 -345
  112. package/src/services/browser-auth.ts +296 -0
  113. package/src/services/chat-service.ts +265 -299
  114. package/src/services/cluster-service.ts +42 -45
  115. package/src/services/collaborator-service.ts +14 -19
  116. package/src/services/flux-service.ts +23 -25
  117. package/src/services/helm-service.ts +19 -21
  118. package/src/services/kubectl-service.ts +17 -19
  119. package/src/types/api.d.ts +1905 -1907
  120. package/src/types/json.d.ts +2 -2
  121. package/src/utils/config-checker.ts +10 -10
  122. package/src/utils/config-loader.ts +162 -178
  123. package/src/utils/default-api-key.ts +114 -125
  124. package/src/utils/env-manager.ts +53 -57
  125. package/src/utils/error-handler.ts +61 -56
  126. package/src/utils/logger.ts +79 -73
  127. package/src/utils/markdown-renderer.ts +31 -31
  128. package/src/utils/opencode-validator.ts +85 -89
  129. package/src/utils/token-manager.ts +108 -87
  130. package/templates/agents/app.md +1 -0
  131. package/templates/agents/backend.md +1 -0
  132. package/templates/agents/devops.md +2 -0
  133. package/templates/agents/frontend.md +1 -0
  134. package/templates/agents/fullstack.md +1 -0
  135. package/templates/agents/quality.md +45 -40
  136. package/templates/agents/security.md +1 -0
  137. package/tests/commands/chat.test.ts +53 -62
  138. package/tests/commands/code.test.ts +265 -310
  139. package/tests/utils/config-loader.test.ts +189 -188
  140. package/tests/utils/env-manager.test.ts +110 -113
  141. package/tests/utils/opencode-validator.test.ts +52 -56
  142. package/tsconfig.json +4 -3
  143. package/vitest.config.ts +3 -3
  144. package/AGENTS.md +0 -374
  145. package/TODO.md +0 -19
@@ -1,47 +1,50 @@
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)
6
- }
4
+ match: (command: string, arguments_: readonly string[]) => boolean;
5
+ response: ((command: string, arguments_: readonly string[]) => Error | string) | Error | string;
6
+ };
7
7
 
8
8
  export class FakeCommandRunner implements CommandRunner {
9
- private handlers: Handler[] = []
10
- private _calls: Array<{ command: string; args: string[]; options?: { cwd?: string } }> = []
11
-
12
- handle(match: string | RegExp, response: string | Error): this {
13
- this.handlers.push({
14
- match: (cmd, args) => {
15
- const full = `${cmd} ${args.join(' ')}`
16
- if (typeof match === 'string') return full.startsWith(match)
17
- return match.test(full)
18
- },
19
- response,
20
- })
21
- return this
22
- }
23
-
24
- checkInstalled(binary: string): Promise<boolean> {
25
- this._calls.push({ command: `check:${binary}`, args: [] })
26
- return Promise.resolve(
27
- this.handlers.some(h => h.match(binary, ['--version'])) || false
28
- )
29
- }
30
-
31
- async run(command: string, args: readonly string[], options?: { cwd?: string }): Promise<string> {
32
- this._calls.push({ command, args: [...args], options })
33
- const handler = this.handlers.find(h => h.match(command, args))
34
- if (!handler) throw new Error(`Unexpected command: ${command} ${args.join(' ')}`)
35
-
36
- const result = typeof handler.response === 'function'
37
- ? handler.response(command, args)
38
- : handler.response
39
-
40
- if (result instanceof Error) throw result
41
- return result
42
- }
43
-
44
- get calls() {
45
- return this._calls
46
- }
9
+ get calls() {
10
+ return this._calls;
11
+ }
12
+ private _calls: Array<{ args: string[]; command: string; options?: { cwd?: string } }> = [];
13
+
14
+ private handlers: Handler[] = [];
15
+
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 {
22
+ this.handlers.push({
23
+ match: (cmd, arguments_) => {
24
+ const full = `${cmd} ${arguments_.join(' ')}`;
25
+ if (typeof match === 'string') return full.startsWith(match);
26
+ return match.test(full);
27
+ },
28
+ response,
29
+ });
30
+ return this;
31
+ }
32
+
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(' ')}`);
41
+
42
+ const result =
43
+ typeof handler.response === 'function'
44
+ ? handler.response(command, arguments_)
45
+ : handler.response;
46
+
47
+ if (result instanceof Error) throw result;
48
+ return result;
49
+ }
47
50
  }
@@ -1,35 +1,44 @@
1
- import type { FileStore } from '../ports/file-store'
1
+ import type { FileStore } from '../ports/file-store';
2
2
 
3
3
  export interface FileEntry {
4
- content: string
5
- isDirectory?: boolean
4
+ content: string;
5
+ isDirectory?: boolean;
6
6
  }
7
7
 
8
8
  export class FakeFileStore implements FileStore {
9
- private files: Map<string, string> = new Map()
10
- private dirs: Set<string> = new Set()
9
+ private _chmodCalls: Array<{ mode: number; path: string }> = [];
10
+ private dirs: Set<string> = new Set();
11
+ private files: Map<string, string> = new Map();
11
12
 
12
- seed(path: string, content: string): void {
13
- this.files.set(path, content)
14
- }
13
+ async chmod(path: string, mode: number): Promise<void> {
14
+ this._chmodCalls.push({ mode, path });
15
+ }
15
16
 
16
- async exists(path: string): Promise<boolean> {
17
- return this.files.has(path) || this.dirs.has(path)
18
- }
17
+ async exists(path: string): Promise<boolean> {
18
+ return this.files.has(path) || this.dirs.has(path);
19
+ }
19
20
 
20
- async readFile(path: string): Promise<string | null> {
21
- return this.files.get(path) ?? null
22
- }
21
+ getChmodCalls(): Array<{ mode: number; path: string }> {
22
+ return this._chmodCalls;
23
+ }
23
24
 
24
- async writeFile(path: string, content: string): Promise<void> {
25
- this.files.set(path, content)
26
- }
25
+ getWrittenFiles(): Map<string, string> {
26
+ return new Map(this.files);
27
+ }
27
28
 
28
- async mkdir(path: string): Promise<void> {
29
- this.dirs.add(path)
30
- }
29
+ async mkdir(path: string): Promise<void> {
30
+ this.dirs.add(path);
31
+ }
31
32
 
32
- getWrittenFiles(): Map<string, string> {
33
- return new Map(this.files)
34
- }
33
+ async readFile(path: string): Promise<null | string> {
34
+ return this.files.get(path) ?? null;
35
+ }
36
+
37
+ seed(path: string, content: string): void {
38
+ this.files.set(path, content);
39
+ }
40
+
41
+ async writeFile(path: string, content: string): Promise<void> {
42
+ this.files.set(path, content);
43
+ }
35
44
  }
@@ -1,83 +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
-
10
- export const select = <T>(
11
- value: T | symbol,
12
- match?: string | RegExp
13
- ): PromptEntry => ({
14
- kind: 'select',
15
- match: typeof match === 'string' ? new RegExp(match) : match,
16
- response: typeof value === 'symbol' ? value : String(value),
17
- })
18
-
19
- export const confirm = (
20
- value: boolean | symbol,
21
- match?: string | RegExp
22
- ): PromptEntry => ({
23
- kind: 'confirm',
24
- match: typeof match === 'string' ? new RegExp(match) : match,
25
- response: value,
26
- })
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),
17
+ });
18
+
19
+ export const text = (value: string | symbol, match?: RegExp | string): PromptEntry => ({
20
+ kind: 'text',
21
+ match: typeof match === 'string' ? new RegExp(match) : match,
22
+ response: value,
23
+ });
24
+
25
+ export const confirm = (value: boolean | symbol, match?: RegExp | string): PromptEntry => ({
26
+ kind: 'confirm',
27
+ match: typeof match === 'string' ? new RegExp(match) : match,
28
+ response: value,
29
+ });
30
+
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
+ });
27
36
 
28
37
  export class FakePrompter implements Prompter {
29
- private _calls: Array<{ method: string; args: unknown }> = []
30
- private _cursor = 0
31
-
32
- constructor(private readonly _script: PromptEntry[]) {}
33
-
34
- intro(message: string): void {
35
- this._calls.push({ method: 'intro', args: { message } })
36
- }
37
- outro(message: string): void {
38
- this._calls.push({ method: 'outro', args: { message } })
39
- }
40
- note(message: string, title?: string): void {
41
- this._calls.push({ method: 'note', args: { message, title } })
42
- }
43
- spinner(): Spinner {
44
- return {
45
- start: (msg: string) => {
46
- this._calls.push({ method: 'spinner.start', args: { message: msg } })
47
- },
48
- stop: (msg: string) => {
49
- this._calls.push({ method: 'spinner.stop', args: { message: msg } })
50
- },
51
- }
52
- }
53
-
54
- async select<T>(opts: { message: string }): Promise<T> {
55
- this._calls.push({ method: 'select', args: opts })
56
- const entry = this._script[this._cursor++]
57
- if (!entry) throw new Error(`No script entry for select #${this._cursor} (${opts.message})`)
58
- if (entry.kind !== 'select') throw new Error(`Expected confirm, got select for ${opts.message}`)
59
- if (entry.match && !entry.match.test(opts.message)) throw new Error(`Message mismatch: got "${opts.message}"`)
60
- if (entry.response === CANCEL) throw new CancelledError()
61
- return entry.response as T
62
- }
63
-
64
- async confirm(opts: { message: string }): Promise<boolean> {
65
- this._calls.push({ method: 'confirm', args: opts })
66
- const entry = this._script[this._cursor++]
67
- if (!entry) throw new Error(`No script entry for confirm #${this._cursor} (${opts.message})`)
68
- if (entry.kind !== 'confirm') throw new Error(`Expected select, got confirm for ${opts.message}`)
69
- if (entry.match && !entry.match.test(opts.message)) throw new Error(`Message mismatch: got "${opts.message}"`)
70
- if (entry.response === CANCEL) throw new CancelledError()
71
- return entry.response as boolean
72
- }
73
-
74
- get calls() {
75
- return this._calls
76
- }
77
-
78
- assertExhausted() {
79
- if (this._cursor !== this._script.length) {
80
- throw new Error(`Script not exhausted: ${this._script.length - this._cursor} entries left`)
81
- }
82
- }
38
+ get calls() {
39
+ return this._calls;
40
+ }
41
+ private _calls: Array<{ args: unknown; method: string }> = [];
42
+
43
+ private _cursor = 0;
44
+
45
+ constructor(private readonly _script: PromptEntry[]) {}
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
+ }
63
+ intro(message: string): void {
64
+ this._calls.push({ args: { message }, method: 'intro' });
65
+ }
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[];
78
+ }
79
+
80
+ note(message: string, title?: string): void {
81
+ this._calls.push({ args: { message, title }, method: 'note' });
82
+ }
83
+
84
+ outro(message: string): void {
85
+ this._calls.push({ args: { message }, method: 'outro' });
86
+ }
87
+
88
+ async select<T>(options: { message: string }): Promise<T> {
89
+ this._calls.push({ args: options, method: 'select' });
90
+ const entry = this._script[this._cursor++];
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}"`);
96
+ if (entry.response === CANCEL) throw new CancelledError();
97
+ return entry.response as T;
98
+ }
99
+
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
+ };
109
+ }
110
+
111
+ async text(options: { message: string }): Promise<string> {
112
+ this._calls.push({ args: options, method: 'text' });
113
+ const entry = this._script[this._cursor++];
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}"`);
119
+ if (entry.response === CANCEL) throw new CancelledError();
120
+ return entry.response as string;
121
+ }
83
122
  }