bbk-cli 1.1.2 → 1.2.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/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +14 -0
- package/README.md +113 -132
- package/dist/cli/wrapper.d.ts +0 -1
- package/dist/cli/wrapper.d.ts.map +1 -1
- package/dist/cli/wrapper.js +19 -54
- package/dist/cli/wrapper.js.map +1 -1
- package/dist/commands/helpers.js +1 -1
- package/dist/commands/runner.d.ts.map +1 -1
- package/dist/commands/runner.js +22 -18
- package/dist/commands/runner.js.map +1 -1
- package/dist/config/constants.d.ts.map +1 -1
- package/dist/config/constants.js +37 -52
- package/dist/config/constants.js.map +1 -1
- package/dist/utils/arg-parser.d.ts.map +1 -1
- package/dist/utils/arg-parser.js +23 -8
- package/dist/utils/arg-parser.js.map +1 -1
- package/dist/utils/bitbucket-client.d.ts +24 -37
- package/dist/utils/bitbucket-client.d.ts.map +1 -1
- package/dist/utils/bitbucket-client.js +38 -52
- package/dist/utils/bitbucket-client.js.map +1 -1
- package/dist/utils/bitbucket-utils.d.ts +48 -68
- package/dist/utils/bitbucket-utils.d.ts.map +1 -1
- package/dist/utils/bitbucket-utils.js +100 -125
- package/dist/utils/bitbucket-utils.js.map +1 -1
- package/dist/utils/config-loader.d.ts +10 -29
- package/dist/utils/config-loader.d.ts.map +1 -1
- package/dist/utils/config-loader.js +277 -51
- package/dist/utils/config-loader.js.map +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -3
- package/tests/integration/cli-integration.test.ts +96 -217
- package/tests/unit/cli/wrapper.test.ts +28 -137
- package/tests/unit/commands/runner.test.ts +69 -197
- package/tests/unit/utils/arg-parser.test.ts +53 -4
- package/tests/unit/utils/config-loader.test.ts +441 -106
|
@@ -1,158 +1,493 @@
|
|
|
1
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';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { loadConfig, setupConfig } from '../../../src/utils/config-loader.js';
|
|
7
|
+
|
|
8
|
+
// Create shared mock interface for readline
|
|
9
|
+
const mockQuestion = vi.fn();
|
|
10
|
+
const mockClose = vi.fn();
|
|
11
|
+
const mockOn = vi.fn();
|
|
12
|
+
|
|
13
|
+
const mockRlInterface = {
|
|
14
|
+
question: mockQuestion,
|
|
15
|
+
close: mockClose,
|
|
16
|
+
on: mockOn,
|
|
17
|
+
write: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Mock readline at module level
|
|
21
|
+
vi.mock('readline', () => ({
|
|
22
|
+
default: {
|
|
23
|
+
createInterface: vi.fn(() => mockRlInterface),
|
|
24
|
+
},
|
|
25
|
+
}));
|
|
7
26
|
|
|
8
27
|
describe('config-loader', () => {
|
|
9
|
-
|
|
28
|
+
describe('loadConfig', () => {
|
|
29
|
+
let testConfigDir: string;
|
|
30
|
+
let homedirSpy: vi.SpyInstance;
|
|
10
31
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
fs.mkdirSync(path.join(testDir, '.claude'));
|
|
15
|
-
});
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
// Create a temporary directory for test configs
|
|
34
|
+
testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bbk-cli-test-'));
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
});
|
|
36
|
+
// Spy on os.homedir() to return test directory (works on all platforms)
|
|
37
|
+
homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(testConfigDir);
|
|
38
|
+
});
|
|
21
39
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
`;
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
// Clean up test directory
|
|
42
|
+
if (testConfigDir) {
|
|
43
|
+
fs.rmSync(testConfigDir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
46
|
+
// Restore original os.homedir()
|
|
47
|
+
homedirSpy.mockRestore();
|
|
42
48
|
|
|
43
|
-
|
|
49
|
+
// Clear mock calls
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
it('should load valid INI configuration file', async () => {
|
|
54
|
+
const configContent = `[auth]
|
|
55
|
+
email=user@example.com
|
|
56
|
+
api_token=app_token_here
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
[defaults]
|
|
59
|
+
workspace=myworkspace
|
|
60
|
+
format=json
|
|
61
|
+
`;
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
});
|
|
63
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
64
|
+
fs.writeFileSync(configPath, configContent);
|
|
56
65
|
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
const config = await loadConfig();
|
|
67
|
+
|
|
68
|
+
expect(config.email).toBe('user@example.com');
|
|
69
|
+
expect(config.apiToken).toBe('app_token_here');
|
|
70
|
+
expect(config.defaultWorkspace).toBe('myworkspace');
|
|
71
|
+
expect(config.defaultFormat).toBe('json');
|
|
59
72
|
});
|
|
60
73
|
|
|
61
|
-
it('should
|
|
62
|
-
const configContent =
|
|
74
|
+
it('should load config without optional default workspace', async () => {
|
|
75
|
+
const configContent = `[auth]
|
|
76
|
+
email=user@example.com
|
|
77
|
+
api_token=app_token_here
|
|
63
78
|
|
|
64
|
-
|
|
79
|
+
[defaults]
|
|
80
|
+
format=json
|
|
65
81
|
`;
|
|
66
82
|
|
|
67
|
-
const configPath = path.join(
|
|
83
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
68
84
|
fs.writeFileSync(configPath, configContent);
|
|
69
85
|
|
|
70
|
-
|
|
86
|
+
const config = await loadConfig();
|
|
87
|
+
|
|
88
|
+
expect(config.email).toBe('user@example.com');
|
|
89
|
+
expect(config.apiToken).toBe('app_token_here');
|
|
90
|
+
expect(config.defaultWorkspace).toBeUndefined();
|
|
91
|
+
expect(config.defaultFormat).toBe('json');
|
|
71
92
|
});
|
|
72
93
|
|
|
73
|
-
it('should
|
|
74
|
-
const configContent =
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
it('should use json as default format if not specified', async () => {
|
|
95
|
+
const configContent = `[auth]
|
|
96
|
+
email=user@example.com
|
|
97
|
+
api_token=app_token_here
|
|
77
98
|
`;
|
|
78
99
|
|
|
79
|
-
const configPath = path.join(
|
|
100
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
80
101
|
fs.writeFileSync(configPath, configContent);
|
|
81
102
|
|
|
82
|
-
|
|
103
|
+
const config = await loadConfig();
|
|
104
|
+
|
|
105
|
+
expect(config.defaultFormat).toBe('json');
|
|
83
106
|
});
|
|
84
107
|
|
|
85
|
-
it('should
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
108
|
+
it('should support all output formats: json, toon', async () => {
|
|
109
|
+
const formats: Array<'json' | 'toon'> = ['json', 'toon'];
|
|
110
|
+
|
|
111
|
+
for (const format of formats) {
|
|
112
|
+
const configContent = `[auth]
|
|
113
|
+
email=user@example.com
|
|
114
|
+
api_token=app_token_here
|
|
115
|
+
|
|
116
|
+
[defaults]
|
|
117
|
+
format=${format}
|
|
92
118
|
`;
|
|
93
119
|
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
121
|
+
fs.writeFileSync(configPath, configContent);
|
|
122
|
+
|
|
123
|
+
const config = await loadConfig();
|
|
124
|
+
expect(config.defaultFormat).toBe(format);
|
|
96
125
|
|
|
97
|
-
|
|
126
|
+
// Clean up for next iteration
|
|
127
|
+
fs.rmSync(configPath);
|
|
128
|
+
}
|
|
98
129
|
});
|
|
99
130
|
|
|
100
|
-
it('should
|
|
101
|
-
const configContent =
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
131
|
+
it('should handle comments and empty lines in config file', async () => {
|
|
132
|
+
const configContent = `# This is a comment
|
|
133
|
+
[auth]
|
|
134
|
+
email=user@example.com
|
|
135
|
+
api_token=app_token_here
|
|
136
|
+
|
|
137
|
+
# Another comment
|
|
138
|
+
|
|
139
|
+
[defaults]
|
|
140
|
+
workspace=myworkspace
|
|
141
|
+
|
|
142
|
+
# format comment
|
|
143
|
+
format=json
|
|
110
144
|
`;
|
|
111
145
|
|
|
112
|
-
const configPath = path.join(
|
|
146
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
113
147
|
fs.writeFileSync(configPath, configContent);
|
|
114
148
|
|
|
115
|
-
const config = loadConfig(
|
|
149
|
+
const config = await loadConfig();
|
|
150
|
+
|
|
151
|
+
expect(config.email).toBe('user@example.com');
|
|
152
|
+
expect(config.apiToken).toBe('app_token_here');
|
|
153
|
+
expect(config.defaultWorkspace).toBe('myworkspace');
|
|
154
|
+
expect(config.defaultFormat).toBe('json');
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('setupConfig', () => {
|
|
159
|
+
let testConfigDir: string;
|
|
160
|
+
let homedirSpy: vi.SpyInstance;
|
|
161
|
+
|
|
162
|
+
beforeEach(() => {
|
|
163
|
+
testConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bbk-cli-test-'));
|
|
164
|
+
|
|
165
|
+
// Spy on os.homedir() to return test directory (works on all platforms)
|
|
166
|
+
homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(testConfigDir);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
afterEach(() => {
|
|
170
|
+
if (testConfigDir) {
|
|
171
|
+
fs.rmSync(testConfigDir, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Restore original os.homedir()
|
|
175
|
+
homedirSpy.mockRestore();
|
|
176
|
+
|
|
177
|
+
vi.clearAllMocks();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should create config file with all fields', async () => {
|
|
181
|
+
// Mock readline to provide all inputs
|
|
182
|
+
mockQuestion
|
|
183
|
+
.mockImplementationOnce((_, callback) => callback('user@example.com'))
|
|
184
|
+
.mockImplementationOnce((_, callback) => callback('test_token'))
|
|
185
|
+
.mockImplementationOnce((_, callback) => callback('myworkspace'))
|
|
186
|
+
.mockImplementationOnce((_, callback) => callback('toon')); // Use non-default format
|
|
187
|
+
|
|
188
|
+
await setupConfig();
|
|
189
|
+
|
|
190
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
191
|
+
expect(fs.existsSync(configPath)).toBe(true);
|
|
192
|
+
|
|
193
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
194
|
+
expect(content).toContain('[auth]');
|
|
195
|
+
expect(content).toContain('email=user@example.com');
|
|
196
|
+
expect(content).toContain('api_token=test_token');
|
|
197
|
+
expect(content).toContain('[defaults]');
|
|
198
|
+
expect(content).toContain('workspace=myworkspace');
|
|
199
|
+
expect(content).toContain('format=toon');
|
|
200
|
+
|
|
201
|
+
// Check file permissions (0o600 = read/write for owner only)
|
|
202
|
+
// Note: Windows doesn't support Unix-style permissions, so we skip the check
|
|
203
|
+
if (process.platform !== 'win32') {
|
|
204
|
+
const stats = fs.statSync(configPath);
|
|
205
|
+
const mode = stats.mode & 0o777;
|
|
206
|
+
expect(mode).toBe(0o600);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should create minimal config file with only required fields', async () => {
|
|
211
|
+
mockQuestion
|
|
212
|
+
.mockImplementationOnce((_, callback) => callback('minimal@example.com'))
|
|
213
|
+
.mockImplementationOnce((_, callback) => callback('minimal_token'))
|
|
214
|
+
.mockImplementationOnce((_, callback) => callback('')) // empty workspace
|
|
215
|
+
.mockImplementationOnce((_, callback) => callback('')); // empty format (defaults to json)
|
|
216
|
+
|
|
217
|
+
await setupConfig();
|
|
218
|
+
|
|
219
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
220
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
221
|
+
|
|
222
|
+
expect(content).toContain('[auth]');
|
|
223
|
+
expect(content).toContain('email=minimal@example.com');
|
|
224
|
+
expect(content).toContain('api_token=minimal_token');
|
|
225
|
+
// Should not include defaults section when both are empty
|
|
226
|
+
expect(content).not.toContain('[defaults]');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('should reject invalid email format and re-prompt', async () => {
|
|
230
|
+
const emailInputs = ['invalid-email', 'still-invalid', 'valid@example.com'];
|
|
231
|
+
let emailInputIndex = 0;
|
|
232
|
+
|
|
233
|
+
mockQuestion.mockImplementation((prompt, callback) => {
|
|
234
|
+
if (prompt.includes('email:')) {
|
|
235
|
+
callback(emailInputs[emailInputIndex++]);
|
|
236
|
+
} else if (prompt.includes('api_token:')) {
|
|
237
|
+
callback('token');
|
|
238
|
+
} else if (prompt.includes('workspace:')) {
|
|
239
|
+
callback('');
|
|
240
|
+
} else if (prompt.includes('format:')) {
|
|
241
|
+
callback('');
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
await setupConfig();
|
|
116
246
|
|
|
117
|
-
|
|
247
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
248
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
249
|
+
expect(content).toContain('email=valid@example.com');
|
|
118
250
|
});
|
|
119
251
|
|
|
120
|
-
it('should
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
252
|
+
it('should reject empty email and re-prompt', async () => {
|
|
253
|
+
const emailInputs = ['', 'test@example.com'];
|
|
254
|
+
let emailInputIndex = 0;
|
|
255
|
+
|
|
256
|
+
mockQuestion.mockImplementation((prompt, callback) => {
|
|
257
|
+
if (prompt.includes('email:')) {
|
|
258
|
+
callback(emailInputs[emailInputIndex++]);
|
|
259
|
+
} else if (prompt.includes('api_token:')) {
|
|
260
|
+
callback('token');
|
|
261
|
+
} else if (prompt.includes('workspace:')) {
|
|
262
|
+
callback('');
|
|
263
|
+
} else if (prompt.includes('format:')) {
|
|
264
|
+
callback('');
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await setupConfig();
|
|
269
|
+
|
|
270
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
271
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
272
|
+
expect(content).toContain('email=test@example.com');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('should reject empty api_token and re-prompt', async () => {
|
|
276
|
+
const tokenInputs = ['', 'real_token'];
|
|
277
|
+
let tokenInputIndex = 0;
|
|
278
|
+
|
|
279
|
+
mockQuestion.mockImplementation((prompt, callback) => {
|
|
280
|
+
if (prompt.includes('email:')) {
|
|
281
|
+
callback('test@example.com');
|
|
282
|
+
} else if (prompt.includes('api_token:')) {
|
|
283
|
+
callback(tokenInputs[tokenInputIndex++]);
|
|
284
|
+
} else if (prompt.includes('workspace:')) {
|
|
285
|
+
callback('');
|
|
286
|
+
} else if (prompt.includes('format:')) {
|
|
287
|
+
callback('');
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
await setupConfig();
|
|
292
|
+
|
|
293
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
294
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
295
|
+
expect(content).toContain('api_token=real_token');
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('should reject invalid format and re-prompt', async () => {
|
|
299
|
+
const formatInputs = ['xml', 'toon']; // Use non-default format
|
|
300
|
+
let formatInputIndex = 0;
|
|
301
|
+
|
|
302
|
+
mockQuestion.mockImplementation((prompt, callback) => {
|
|
303
|
+
if (prompt.includes('email:')) {
|
|
304
|
+
callback('test@example.com');
|
|
305
|
+
} else if (prompt.includes('api_token:')) {
|
|
306
|
+
callback('token');
|
|
307
|
+
} else if (prompt.includes('workspace:')) {
|
|
308
|
+
callback('ws');
|
|
309
|
+
} else if (prompt.includes('format:')) {
|
|
310
|
+
callback(formatInputs[formatInputIndex++]);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
await setupConfig();
|
|
315
|
+
|
|
316
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
317
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
318
|
+
expect(content).toContain('format=toon');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should handle write permission errors gracefully', async () => {
|
|
322
|
+
// Create a directory instead of a file to cause write error
|
|
323
|
+
const existingPath = path.join(testConfigDir, '.bbkcli');
|
|
324
|
+
fs.mkdirSync(existingPath);
|
|
325
|
+
|
|
326
|
+
mockQuestion
|
|
327
|
+
.mockImplementationOnce((_, callback) => callback('test@example.com'))
|
|
328
|
+
.mockImplementationOnce((_, callback) => callback('token'))
|
|
329
|
+
.mockImplementationOnce((_, callback) => callback('ws'))
|
|
330
|
+
.mockImplementationOnce((_, callback) => callback('json'));
|
|
331
|
+
|
|
332
|
+
await expect(setupConfig()).rejects.toThrow('Cannot write config');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should include defaults section when only format is specified', async () => {
|
|
336
|
+
mockQuestion
|
|
337
|
+
.mockImplementationOnce((_, callback) => callback('test@example.com'))
|
|
338
|
+
.mockImplementationOnce((_, callback) => callback('token'))
|
|
339
|
+
.mockImplementationOnce((_, callback) => callback('')) // empty workspace
|
|
340
|
+
.mockImplementationOnce((_, callback) => callback('toon')); // non-default format
|
|
341
|
+
|
|
342
|
+
await setupConfig();
|
|
343
|
+
|
|
344
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
345
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
346
|
+
|
|
347
|
+
expect(content).toContain('[defaults]');
|
|
348
|
+
expect(content).toContain('format=toon');
|
|
349
|
+
expect(content).not.toContain('workspace=');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('should include defaults section when only workspace is specified', async () => {
|
|
353
|
+
mockQuestion
|
|
354
|
+
.mockImplementationOnce((_, callback) => callback('test@example.com'))
|
|
355
|
+
.mockImplementationOnce((_, callback) => callback('token'))
|
|
356
|
+
.mockImplementationOnce((_, callback) => callback('myworkspace'))
|
|
357
|
+
.mockImplementationOnce((_, callback) => callback('')); // empty format (defaults to json)
|
|
358
|
+
|
|
359
|
+
await setupConfig();
|
|
360
|
+
|
|
361
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
362
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
363
|
+
|
|
364
|
+
expect(content).toContain('[defaults]');
|
|
365
|
+
expect(content).toContain('workspace=myworkspace');
|
|
366
|
+
expect(content).not.toContain('format=');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it.skipIf(process.platform === 'win32')('should set secure file permissions (0o600)', async () => {
|
|
370
|
+
mockQuestion
|
|
371
|
+
.mockImplementationOnce((_, callback) => callback('secure@example.com'))
|
|
372
|
+
.mockImplementationOnce((_, callback) => callback('secure_token'))
|
|
373
|
+
.mockImplementationOnce((_, callback) => callback('workspace'))
|
|
374
|
+
.mockImplementationOnce((_, callback) => callback('toon'));
|
|
375
|
+
|
|
376
|
+
await setupConfig();
|
|
377
|
+
|
|
378
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
379
|
+
const stats = fs.statSync(configPath);
|
|
380
|
+
const mode = stats.mode & 0o777;
|
|
381
|
+
|
|
382
|
+
// 0o600 = read/write for owner only (rw-------)
|
|
383
|
+
expect(mode).toBe(0o600);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('should pre-populate existing config values when user presses Enter for all fields', async () => {
|
|
387
|
+
// Create existing config
|
|
388
|
+
const existingConfig = `[auth]
|
|
389
|
+
email=old@example.com
|
|
390
|
+
api_token=old_token_123
|
|
391
|
+
|
|
392
|
+
[defaults]
|
|
393
|
+
workspace=old_workspace
|
|
394
|
+
format=toon
|
|
127
395
|
`;
|
|
396
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
397
|
+
fs.writeFileSync(configPath, existingConfig);
|
|
398
|
+
|
|
399
|
+
// User presses Enter for all prompts (accepts existing values)
|
|
400
|
+
mockQuestion
|
|
401
|
+
.mockImplementationOnce((_, callback) => callback('')) // keep email
|
|
402
|
+
.mockImplementationOnce((_, callback) => callback('')) // keep token
|
|
403
|
+
.mockImplementationOnce((_, callback) => callback('')) // keep workspace
|
|
404
|
+
.mockImplementationOnce((_, callback) => callback('')); // keep format
|
|
405
|
+
|
|
406
|
+
await setupConfig();
|
|
407
|
+
|
|
408
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
409
|
+
expect(content).toContain('email=old@example.com');
|
|
410
|
+
expect(content).toContain('api_token=old_token_123');
|
|
411
|
+
expect(content).toContain('workspace=old_workspace');
|
|
412
|
+
expect(content).toContain('format=toon');
|
|
413
|
+
});
|
|
128
414
|
|
|
129
|
-
|
|
130
|
-
|
|
415
|
+
it('should pre-populate and update individual fields while keeping others', async () => {
|
|
416
|
+
// Create existing config with toon format (non-default)
|
|
417
|
+
const existingConfig = `[auth]
|
|
418
|
+
email=old@example.com
|
|
419
|
+
api_token=old_token_123
|
|
131
420
|
|
|
132
|
-
|
|
421
|
+
[defaults]
|
|
422
|
+
workspace=old_workspace
|
|
423
|
+
format=toon
|
|
424
|
+
`;
|
|
425
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
426
|
+
fs.writeFileSync(configPath, existingConfig);
|
|
427
|
+
|
|
428
|
+
// User updates email and workspace but keeps token and format
|
|
429
|
+
mockQuestion
|
|
430
|
+
.mockImplementationOnce((_, callback) => callback('new@example.com')) // new email
|
|
431
|
+
.mockImplementationOnce((_, callback) => callback('')) // keep token
|
|
432
|
+
.mockImplementationOnce((_, callback) => callback('new_workspace')) // new workspace
|
|
433
|
+
.mockImplementationOnce((_, callback) => callback('')); // keep format
|
|
434
|
+
|
|
435
|
+
await setupConfig();
|
|
436
|
+
|
|
437
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
438
|
+
expect(content).toContain('email=new@example.com');
|
|
439
|
+
expect(content).toContain('api_token=old_token_123');
|
|
440
|
+
expect(content).toContain('workspace=new_workspace');
|
|
441
|
+
expect(content).toContain('format=toon');
|
|
442
|
+
});
|
|
133
443
|
|
|
134
|
-
|
|
444
|
+
it('should handle corrupted existing config file gracefully', async () => {
|
|
445
|
+
// Create a corrupted existing config
|
|
446
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
447
|
+
fs.writeFileSync(configPath, 'this is not valid INI at all {{{');
|
|
448
|
+
|
|
449
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
450
|
+
|
|
451
|
+
// User provides new values
|
|
452
|
+
mockQuestion
|
|
453
|
+
.mockImplementationOnce((_, callback) => callback('fresh@example.com'))
|
|
454
|
+
.mockImplementationOnce((_, callback) => callback('fresh_token'))
|
|
455
|
+
.mockImplementationOnce((_, callback) => callback('fresh_workspace'))
|
|
456
|
+
.mockImplementationOnce((_, callback) => callback('json'));
|
|
457
|
+
|
|
458
|
+
await setupConfig();
|
|
459
|
+
|
|
460
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
461
|
+
expect(content).toContain('email=fresh@example.com');
|
|
462
|
+
expect(content).toContain('api_token=fresh_token');
|
|
463
|
+
|
|
464
|
+
consoleWarnSpy.mockRestore();
|
|
135
465
|
});
|
|
136
466
|
|
|
137
|
-
it('should
|
|
138
|
-
|
|
467
|
+
it('should preserve api_token when user deletes asterisks and presses Enter', async () => {
|
|
468
|
+
// Create existing config
|
|
469
|
+
const existingConfig = `[auth]
|
|
470
|
+
email=test@example.com
|
|
471
|
+
api_token=existing_token
|
|
139
472
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
profiles:
|
|
143
|
-
cloud:
|
|
144
|
-
email: user@example.com
|
|
145
|
-
apiToken: token_here
|
|
146
|
-
defaultFormat: ${format}
|
|
147
|
-
---
|
|
473
|
+
[defaults]
|
|
474
|
+
format=json
|
|
148
475
|
`;
|
|
476
|
+
const configPath = path.join(testConfigDir, '.bbkcli');
|
|
477
|
+
fs.writeFileSync(configPath, existingConfig);
|
|
149
478
|
|
|
150
|
-
|
|
151
|
-
|
|
479
|
+
// User keeps email, deletes all asterisks (backspaces) and presses Enter to keep token
|
|
480
|
+
mockQuestion
|
|
481
|
+
.mockImplementationOnce((_, callback) => callback('')) // keep email
|
|
482
|
+
.mockImplementationOnce((_, callback) => callback('')) // deleted asterisks, keep token
|
|
483
|
+
.mockImplementationOnce((_, callback) => callback('')) // no workspace
|
|
484
|
+
.mockImplementationOnce((_, callback) => callback('')); // keep format
|
|
152
485
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
486
|
+
await setupConfig();
|
|
487
|
+
|
|
488
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
489
|
+
expect(content).toContain('email=test@example.com');
|
|
490
|
+
expect(content).toContain('api_token=existing_token');
|
|
156
491
|
});
|
|
157
492
|
});
|
|
158
493
|
});
|