bbk-cli 1.0.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 (76) hide show
  1. package/.claude/bitbucket-config.local.md.example +58 -0
  2. package/.eslintcache +1 -0
  3. package/.github/dependabot.yml +15 -0
  4. package/.github/workflows/convetional-commit.yml +24 -0
  5. package/.github/workflows/publish-on-tag.yml +47 -0
  6. package/.github/workflows/release-please.yml +21 -0
  7. package/.github/workflows/run-tests.yml +75 -0
  8. package/.nvmrc +1 -0
  9. package/.prettierignore +2 -0
  10. package/.prettierrc.cjs +17 -0
  11. package/.release-please-manifest.json +3 -0
  12. package/CHANGELOG.md +21 -0
  13. package/LICENSE +202 -0
  14. package/README.md +381 -0
  15. package/dist/cli/index.d.ts +2 -0
  16. package/dist/cli/index.d.ts.map +1 -0
  17. package/dist/cli/index.js +2 -0
  18. package/dist/cli/index.js.map +1 -0
  19. package/dist/cli/wrapper.d.ts +38 -0
  20. package/dist/cli/wrapper.d.ts.map +1 -0
  21. package/dist/cli/wrapper.js +326 -0
  22. package/dist/cli/wrapper.js.map +1 -0
  23. package/dist/commands/helpers.d.ts +11 -0
  24. package/dist/commands/helpers.d.ts.map +1 -0
  25. package/dist/commands/helpers.js +40 -0
  26. package/dist/commands/helpers.js.map +1 -0
  27. package/dist/commands/index.d.ts +3 -0
  28. package/dist/commands/index.d.ts.map +1 -0
  29. package/dist/commands/index.js +3 -0
  30. package/dist/commands/index.js.map +1 -0
  31. package/dist/commands/runner.d.ts +7 -0
  32. package/dist/commands/runner.d.ts.map +1 -0
  33. package/dist/commands/runner.js +126 -0
  34. package/dist/commands/runner.js.map +1 -0
  35. package/dist/config/constants.d.ts +16 -0
  36. package/dist/config/constants.d.ts.map +1 -0
  37. package/dist/config/constants.js +171 -0
  38. package/dist/config/constants.js.map +1 -0
  39. package/dist/config/index.d.ts +2 -0
  40. package/dist/config/index.d.ts.map +1 -0
  41. package/dist/config/index.js +2 -0
  42. package/dist/config/index.js.map +1 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +24 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/utils/arg-parser.d.ts +7 -0
  48. package/dist/utils/arg-parser.d.ts.map +1 -0
  49. package/dist/utils/arg-parser.js +67 -0
  50. package/dist/utils/arg-parser.js.map +1 -0
  51. package/dist/utils/bitbucket-client.d.ts +122 -0
  52. package/dist/utils/bitbucket-client.d.ts.map +1 -0
  53. package/dist/utils/bitbucket-client.js +182 -0
  54. package/dist/utils/bitbucket-client.js.map +1 -0
  55. package/dist/utils/bitbucket-utils.d.ts +110 -0
  56. package/dist/utils/bitbucket-utils.d.ts.map +1 -0
  57. package/dist/utils/bitbucket-utils.js +491 -0
  58. package/dist/utils/bitbucket-utils.js.map +1 -0
  59. package/dist/utils/config-loader.d.ts +41 -0
  60. package/dist/utils/config-loader.d.ts.map +1 -0
  61. package/dist/utils/config-loader.js +76 -0
  62. package/dist/utils/config-loader.js.map +1 -0
  63. package/dist/utils/index.d.ts +5 -0
  64. package/dist/utils/index.d.ts.map +1 -0
  65. package/dist/utils/index.js +4 -0
  66. package/dist/utils/index.js.map +1 -0
  67. package/eslint.config.ts +15 -0
  68. package/package.json +62 -0
  69. package/release-please-config.json +33 -0
  70. package/tests/integration/cli-integration.test.ts +528 -0
  71. package/tests/unit/cli/wrapper.test.ts +727 -0
  72. package/tests/unit/commands/helpers.test.ts +268 -0
  73. package/tests/unit/commands/runner.test.ts +758 -0
  74. package/tests/unit/utils/arg-parser.test.ts +350 -0
  75. package/tests/unit/utils/config-loader.test.ts +158 -0
  76. package/vitest.config.ts +22 -0
@@ -0,0 +1,350 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import { parseArguments } from '../../../src/utils/arg-parser.js';
4
+
5
+ // Mock the imported modules
6
+ vi.mock('../../../src/commands/index.js', () => ({
7
+ getCurrentVersion: vi.fn().mockReturnValue('0.0.0'),
8
+ printAvailableCommands: vi.fn(),
9
+ printCommandDetail: vi.fn(),
10
+ runCommand: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../../../src/config/index.js', () => ({
14
+ COMMANDS: [
15
+ 'list-repos',
16
+ 'get-repo',
17
+ 'list-prs',
18
+ 'get-pr',
19
+ 'create-pr',
20
+ 'update-pr',
21
+ 'add-comment',
22
+ 'delete-pr',
23
+ 'get-user',
24
+ 'test-connection',
25
+ ],
26
+ }));
27
+
28
+ describe('arg-parser', () => {
29
+ beforeEach(() => {
30
+ vi.clearAllMocks();
31
+ });
32
+
33
+ afterEach(() => {
34
+ vi.restoreAllMocks();
35
+ });
36
+
37
+ describe('parseArguments', () => {
38
+ it('should handle --version flag', async () => {
39
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
40
+ throw new Error('process.exit called');
41
+ });
42
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
43
+
44
+ try {
45
+ await parseArguments(['--version']);
46
+ } catch {
47
+ // Expected
48
+ }
49
+
50
+ expect(consoleLogSpy).toHaveBeenCalledWith('0.0.0');
51
+ expect(exitSpy).toHaveBeenCalledWith(0);
52
+
53
+ exitSpy.mockRestore();
54
+ consoleLogSpy.mockRestore();
55
+ });
56
+
57
+ it('should handle -v flag (short version)', async () => {
58
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
59
+ throw new Error('process.exit called');
60
+ });
61
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
62
+
63
+ try {
64
+ await parseArguments(['-v']);
65
+ } catch {
66
+ // Expected
67
+ }
68
+
69
+ expect(consoleLogSpy).toHaveBeenCalledWith('0.0.0');
70
+ expect(exitSpy).toHaveBeenCalledWith(0);
71
+
72
+ exitSpy.mockRestore();
73
+ consoleLogSpy.mockRestore();
74
+ });
75
+
76
+ it('should handle --commands flag', async () => {
77
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
78
+ throw new Error('process.exit called');
79
+ });
80
+ const { printAvailableCommands } = await import('../../../src/commands/index.js');
81
+
82
+ try {
83
+ await parseArguments(['--commands']);
84
+ } catch {
85
+ // Expected
86
+ }
87
+
88
+ expect(printAvailableCommands).toHaveBeenCalled();
89
+ expect(exitSpy).toHaveBeenCalledWith(0);
90
+
91
+ exitSpy.mockRestore();
92
+ });
93
+
94
+ it('should handle command -h for command-specific help', async () => {
95
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
96
+ throw new Error('process.exit called');
97
+ });
98
+ const { printCommandDetail } = await import('../../../src/commands/index.js');
99
+
100
+ try {
101
+ await parseArguments(['list-repos', '-h']);
102
+ } catch {
103
+ // Expected
104
+ }
105
+
106
+ expect(printCommandDetail).toHaveBeenCalledWith('list-repos');
107
+ expect(exitSpy).toHaveBeenCalledWith(0);
108
+
109
+ exitSpy.mockRestore();
110
+ });
111
+
112
+ it('should handle --help flag', async () => {
113
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
114
+ throw new Error('process.exit called');
115
+ });
116
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
117
+
118
+ try {
119
+ await parseArguments(['--help']);
120
+ } catch {
121
+ // Expected
122
+ }
123
+
124
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Bitbucket CLI'));
125
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Usage:'));
126
+ expect(exitSpy).toHaveBeenCalledWith(0);
127
+
128
+ exitSpy.mockRestore();
129
+ consoleLogSpy.mockRestore();
130
+ });
131
+
132
+ it('should handle -h flag (short help)', async () => {
133
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
134
+ throw new Error('process.exit called');
135
+ });
136
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
137
+
138
+ try {
139
+ await parseArguments(['-h']);
140
+ } catch {
141
+ // Expected
142
+ }
143
+
144
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Bitbucket CLI'));
145
+ expect(exitSpy).toHaveBeenCalledWith(0);
146
+
147
+ exitSpy.mockRestore();
148
+ consoleLogSpy.mockRestore();
149
+ });
150
+
151
+ it('should execute valid command in headless mode', async () => {
152
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
153
+ throw new Error('process.exit called');
154
+ });
155
+ const { runCommand } = await import('../../../src/commands/index.js');
156
+
157
+ try {
158
+ await parseArguments(['list-repos', '{"workspace":"myworkspace"}']);
159
+ } catch {
160
+ // Expected
161
+ }
162
+
163
+ expect(runCommand).toHaveBeenCalledWith('list-repos', '{"workspace":"myworkspace"}', null);
164
+ expect(exitSpy).toHaveBeenCalledWith(0);
165
+
166
+ exitSpy.mockRestore();
167
+ });
168
+
169
+ it('should execute command without arguments', async () => {
170
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
171
+ throw new Error('process.exit called');
172
+ });
173
+ const { runCommand } = await import('../../../src/commands/index.js');
174
+
175
+ try {
176
+ await parseArguments(['test-connection']);
177
+ } catch {
178
+ // Expected
179
+ }
180
+
181
+ expect(runCommand).toHaveBeenCalledWith('test-connection', null, null);
182
+ expect(exitSpy).toHaveBeenCalledWith(0);
183
+
184
+ exitSpy.mockRestore();
185
+ });
186
+
187
+ it('should parse command with flag parameter', async () => {
188
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
189
+ throw new Error('process.exit called');
190
+ });
191
+ const { runCommand } = await import('../../../src/commands/index.js');
192
+
193
+ try {
194
+ await parseArguments(['get-repo', '{"workspace":"myworkspace","repo":"myrepo"}', '--format', 'json']);
195
+ } catch {
196
+ // Expected
197
+ }
198
+
199
+ expect(runCommand).toHaveBeenCalledWith('get-repo', '{"workspace":"myworkspace","repo":"myrepo"}', '--format');
200
+ expect(exitSpy).toHaveBeenCalledWith(0);
201
+
202
+ exitSpy.mockRestore();
203
+ });
204
+
205
+ it('should return false for interactive mode (no arguments)', async () => {
206
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
207
+ const result = await parseArguments([]);
208
+
209
+ expect(result).toBe(false);
210
+ expect(exitSpy).not.toHaveBeenCalled();
211
+
212
+ exitSpy.mockRestore();
213
+ });
214
+
215
+ it('should return false for empty arguments', async () => {
216
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
217
+ const result = await parseArguments([]);
218
+
219
+ expect(result).toBe(false);
220
+ expect(exitSpy).not.toHaveBeenCalled();
221
+
222
+ exitSpy.mockRestore();
223
+ });
224
+
225
+ it('should return false for unrecognized flags when not first argument', async () => {
226
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {});
227
+ const result = await parseArguments(['--unknown']);
228
+
229
+ expect(result).toBe(false);
230
+ expect(exitSpy).not.toHaveBeenCalled();
231
+
232
+ exitSpy.mockRestore();
233
+ });
234
+
235
+ it('should prioritize --version flag even if other arguments exist', async () => {
236
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
237
+ throw new Error('process.exit called');
238
+ });
239
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
240
+ const { runCommand } = await import('../../../src/commands/index.js');
241
+
242
+ try {
243
+ await parseArguments(['--version', 'list-repos', '{"workspace":"myworkspace"}']);
244
+ } catch {
245
+ // Expected
246
+ }
247
+
248
+ expect(consoleLogSpy).toHaveBeenCalledWith('0.0.0');
249
+ expect(exitSpy).toHaveBeenCalledWith(0);
250
+ expect(runCommand).not.toHaveBeenCalled();
251
+
252
+ exitSpy.mockRestore();
253
+ consoleLogSpy.mockRestore();
254
+ });
255
+
256
+ it('should prioritize --commands flag', async () => {
257
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
258
+ throw new Error('process.exit called');
259
+ });
260
+ const { printAvailableCommands } = await import('../../../src/commands/index.js');
261
+ const { runCommand } = await import('../../../src/commands/index.js');
262
+
263
+ try {
264
+ await parseArguments(['--commands', 'list-repos']);
265
+ } catch {
266
+ // Expected
267
+ }
268
+
269
+ expect(printAvailableCommands).toHaveBeenCalled();
270
+ expect(runCommand).not.toHaveBeenCalled();
271
+
272
+ exitSpy.mockRestore();
273
+ });
274
+
275
+ it('should handle all 10 commands as valid', async () => {
276
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
277
+ throw new Error('process.exit called');
278
+ });
279
+ const { runCommand } = await import('../../../src/commands/index.js');
280
+
281
+ const commands = [
282
+ 'list-repos',
283
+ 'get-repo',
284
+ 'list-prs',
285
+ 'get-pr',
286
+ 'create-pr',
287
+ 'update-pr',
288
+ 'add-comment',
289
+ 'delete-pr',
290
+ 'get-user',
291
+ 'test-connection',
292
+ ];
293
+
294
+ for (const cmd of commands) {
295
+ try {
296
+ await parseArguments([cmd]);
297
+ } catch {
298
+ // Expected for each command
299
+ }
300
+ }
301
+
302
+ expect(runCommand).toHaveBeenCalledTimes(commands.length);
303
+
304
+ exitSpy.mockRestore();
305
+ });
306
+ });
307
+
308
+ describe('printGeneralHelp', () => {
309
+ it('should display help message with all commands', async () => {
310
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
311
+ throw new Error('process.exit called');
312
+ });
313
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
314
+
315
+ try {
316
+ await parseArguments(['--help']);
317
+ } catch {
318
+ // Expected
319
+ }
320
+
321
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Bitbucket CLI'));
322
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('npx bbk-cli'));
323
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('npx bbk-cli --commands'));
324
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('list-repos'));
325
+
326
+ exitSpy.mockRestore();
327
+ consoleLogSpy.mockRestore();
328
+ });
329
+
330
+ it('should display examples section', async () => {
331
+ const exitSpy = vi.spyOn(process, 'exit').mockImplementation((): never => {
332
+ throw new Error('process.exit called');
333
+ });
334
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
335
+
336
+ try {
337
+ await parseArguments(['--help']);
338
+ } catch {
339
+ // Expected
340
+ }
341
+
342
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Examples:'));
343
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('npx bbk-cli list-repos'));
344
+ expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('npx bbk-cli get-repo'));
345
+
346
+ exitSpy.mockRestore();
347
+ consoleLogSpy.mockRestore();
348
+ });
349
+ });
350
+ });
@@ -0,0 +1,158 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+
6
+ import { loadConfig } from '../../../src/utils/config-loader.js';
7
+
8
+ describe('config-loader', () => {
9
+ let testDir: string;
10
+
11
+ beforeEach(() => {
12
+ // Create a temporary directory for test configs
13
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bbk-cli-test-'));
14
+ fs.mkdirSync(path.join(testDir, '.claude'));
15
+ });
16
+
17
+ afterEach(() => {
18
+ // Clean up test directory
19
+ fs.rmSync(testDir, { recursive: true, force: true });
20
+ });
21
+
22
+ describe('loadConfig', () => {
23
+ it('should load valid Bitbucket configuration file', () => {
24
+ const configContent = `---
25
+ profiles:
26
+ cloud:
27
+ email: user@example.com
28
+ apiToken: app_token_here
29
+ staging:
30
+ email: staging@example.com
31
+ apiToken: staging_token_here
32
+
33
+ defaultProfile: cloud
34
+ defaultFormat: json
35
+ ---
36
+
37
+ # Bitbucket Connection Profiles
38
+ `;
39
+
40
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
41
+ fs.writeFileSync(configPath, configContent);
42
+
43
+ const config = loadConfig(testDir);
44
+
45
+ expect(config.profiles).toBeDefined();
46
+ expect(config.profiles.cloud).toBeDefined();
47
+ expect(config.profiles.cloud.email).toBe('user@example.com');
48
+ expect(config.profiles.cloud.apiToken).toBe('app_token_here');
49
+
50
+ expect(config.profiles.staging).toBeDefined();
51
+ expect(config.profiles.staging.apiToken).toBe('staging_token_here');
52
+
53
+ expect(config.defaultProfile).toBe('cloud');
54
+ expect(config.defaultFormat).toBe('json');
55
+ });
56
+
57
+ it('should throw error if config file does not exist', () => {
58
+ expect(() => loadConfig(testDir)).toThrow('Configuration file not found');
59
+ });
60
+
61
+ it('should throw error if frontmatter is missing', () => {
62
+ const configContent = `# Bitbucket Connection Profiles
63
+
64
+ This is just markdown content without frontmatter.
65
+ `;
66
+
67
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
68
+ fs.writeFileSync(configPath, configContent);
69
+
70
+ expect(() => loadConfig(testDir)).toThrow('Invalid configuration file format');
71
+ });
72
+
73
+ it('should throw error if profiles are missing', () => {
74
+ const configContent = `---
75
+ defaultProfile: cloud
76
+ ---
77
+ `;
78
+
79
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
80
+ fs.writeFileSync(configPath, configContent);
81
+
82
+ expect(() => loadConfig(testDir)).toThrow('Configuration must include "profiles" object');
83
+ });
84
+
85
+ it('should throw error if profile is missing required auth fields', () => {
86
+ const configContent = `---
87
+ profiles:
88
+ incomplete:
89
+ email: test@example.com
90
+ # Missing apiToken
91
+ ---
92
+ `;
93
+
94
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
95
+ fs.writeFileSync(configPath, configContent);
96
+
97
+ expect(() => loadConfig(testDir)).toThrow('must have both "email" and "apiToken"');
98
+ });
99
+
100
+ it('should use first profile as default if defaultProfile not specified', () => {
101
+ const configContent = `---
102
+ profiles:
103
+ first:
104
+ email: first@example.com
105
+ apiToken: first_token
106
+ second:
107
+ email: second@example.com
108
+ apiToken: second_token
109
+ ---
110
+ `;
111
+
112
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
113
+ fs.writeFileSync(configPath, configContent);
114
+
115
+ const config = loadConfig(testDir);
116
+
117
+ expect(config.defaultProfile).toBe('first');
118
+ });
119
+
120
+ it('should use json as default format if not specified', () => {
121
+ const configContent = `---
122
+ profiles:
123
+ cloud:
124
+ email: user@example.com
125
+ apiToken: token_here
126
+ ---
127
+ `;
128
+
129
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
130
+ fs.writeFileSync(configPath, configContent);
131
+
132
+ const config = loadConfig(testDir);
133
+
134
+ expect(config.defaultFormat).toBe('json');
135
+ });
136
+
137
+ it('should support all output formats: json, toon', () => {
138
+ const formats: Array<'json' | 'toon'> = ['json', 'toon'];
139
+
140
+ formats.forEach(format => {
141
+ const configContent = `---
142
+ profiles:
143
+ cloud:
144
+ email: user@example.com
145
+ apiToken: token_here
146
+ defaultFormat: ${format}
147
+ ---
148
+ `;
149
+
150
+ const configPath = path.join(testDir, '.claude', 'bitbucket-config.local.md');
151
+ fs.writeFileSync(configPath, configContent);
152
+
153
+ const config = loadConfig(testDir);
154
+ expect(config.defaultFormat).toBe(format);
155
+ });
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,22 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html'],
10
+ exclude: [
11
+ 'node_modules/',
12
+ 'dist/',
13
+ '**/*.d.ts',
14
+ '**/*.config.*',
15
+ '**/index.ts', // Barrel exports
16
+ 'tests/',
17
+ ],
18
+ },
19
+ include: ['tests/**/*.test.ts'],
20
+ exclude: ['node_modules', 'dist'],
21
+ },
22
+ });