@profoundlogic/coderflow-cli 0.2.1
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/LICENSE.txt +322 -0
- package/README.md +102 -0
- package/coder.js +202 -0
- package/lib/commands/apply.js +238 -0
- package/lib/commands/attach.js +143 -0
- package/lib/commands/config.js +226 -0
- package/lib/commands/containers.js +213 -0
- package/lib/commands/discard.js +167 -0
- package/lib/commands/interactive.js +292 -0
- package/lib/commands/jira.js +464 -0
- package/lib/commands/license.js +172 -0
- package/lib/commands/list.js +104 -0
- package/lib/commands/login.js +329 -0
- package/lib/commands/logs.js +66 -0
- package/lib/commands/profile.js +539 -0
- package/lib/commands/reject.js +53 -0
- package/lib/commands/results.js +89 -0
- package/lib/commands/run.js +237 -0
- package/lib/commands/server.js +537 -0
- package/lib/commands/status.js +39 -0
- package/lib/commands/test.js +335 -0
- package/lib/config.js +378 -0
- package/lib/help.js +444 -0
- package/lib/http-client.js +180 -0
- package/lib/oidc.js +126 -0
- package/lib/profile.js +296 -0
- package/lib/state-capture.js +336 -0
- package/lib/task-grouping.js +210 -0
- package/lib/terminal-client.js +162 -0
- package/package.json +35 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder test - Run ephemeral test containers with local state
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request } from '../http-client.js';
|
|
6
|
+
import { getDefaultEnvironment, getServerUrl, getApiKey } from '../config.js';
|
|
7
|
+
import { captureAllRepos, validateState } from '../state-capture.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load test definitions from server for the given environment
|
|
11
|
+
*/
|
|
12
|
+
async function loadTestDefinitions(environmentName) {
|
|
13
|
+
try {
|
|
14
|
+
const envConfig = await request(`/environments/${environmentName}`);
|
|
15
|
+
return envConfig.tests || {};
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`Failed to load test definitions from server: ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Parse command line arguments
|
|
23
|
+
*/
|
|
24
|
+
function parseTestArgs(args) {
|
|
25
|
+
let testName = null;
|
|
26
|
+
let environment = null;
|
|
27
|
+
let customCommand = null;
|
|
28
|
+
let noLocalState = false;
|
|
29
|
+
let listTests = false;
|
|
30
|
+
|
|
31
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
32
|
+
const arg = args[index];
|
|
33
|
+
|
|
34
|
+
if (arg === '--list') {
|
|
35
|
+
listTests = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (arg === '--no-local-state') {
|
|
40
|
+
noLocalState = true;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (arg === '--environment' || arg === '--env') {
|
|
45
|
+
const next = args[index + 1];
|
|
46
|
+
if (!next) {
|
|
47
|
+
console.error('Error: --environment requires a value');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
environment = next;
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (arg.startsWith('--environment=')) {
|
|
56
|
+
environment = arg.substring('--environment='.length);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (arg.startsWith('--env=')) {
|
|
61
|
+
environment = arg.substring('--env='.length);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (arg === '--cmd') {
|
|
66
|
+
const next = args[index + 1];
|
|
67
|
+
if (!next) {
|
|
68
|
+
console.error('Error: --cmd requires a command string');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
customCommand = next;
|
|
72
|
+
index += 1;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (arg.startsWith('--cmd=')) {
|
|
77
|
+
customCommand = arg.substring('--cmd='.length);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (arg.startsWith('--')) {
|
|
82
|
+
console.error(`Error: Unrecognized option: ${arg}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// First non-flag argument is the test name
|
|
87
|
+
if (!testName) {
|
|
88
|
+
testName = arg;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.error(`Error: Unexpected argument: ${arg}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { testName, environment, customCommand, noLocalState, listTests };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* List available tests for environment
|
|
101
|
+
*/
|
|
102
|
+
async function listAvailableTests(environmentName) {
|
|
103
|
+
const tests = await loadTestDefinitions(environmentName);
|
|
104
|
+
const testNames = Object.keys(tests);
|
|
105
|
+
|
|
106
|
+
if (testNames.length === 0) {
|
|
107
|
+
console.log(`No tests defined for environment: ${environmentName}`);
|
|
108
|
+
console.log(`\nCreate tests.json in: environments/${environmentName}/tests.json`);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
console.log(`Available tests for ${environmentName}:\n`);
|
|
113
|
+
for (const name of testNames) {
|
|
114
|
+
const test = tests[name];
|
|
115
|
+
console.log(` ${name}`);
|
|
116
|
+
if (test.description) {
|
|
117
|
+
console.log(` ${test.description}`);
|
|
118
|
+
}
|
|
119
|
+
// Show command or commands
|
|
120
|
+
if (test.commands && Array.isArray(test.commands)) {
|
|
121
|
+
console.log(` Commands: ${test.commands.length} steps`);
|
|
122
|
+
} else {
|
|
123
|
+
console.log(` Command: ${test.command}`);
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(test.repos)) {
|
|
126
|
+
if (test.repos.length === 0) {
|
|
127
|
+
console.log(` Repos: none (skip local state)`);
|
|
128
|
+
} else {
|
|
129
|
+
console.log(` Repos: ${test.repos.join(', ')}`);
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
console.log(` Repos: all`);
|
|
133
|
+
}
|
|
134
|
+
console.log('');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run a test in an ephemeral container
|
|
140
|
+
*/
|
|
141
|
+
export async function runTest(args = []) {
|
|
142
|
+
const { testName, environment, customCommand, noLocalState, listTests } = parseTestArgs(args);
|
|
143
|
+
|
|
144
|
+
// Resolve environment
|
|
145
|
+
let resolvedEnvironment = environment;
|
|
146
|
+
if (!resolvedEnvironment) {
|
|
147
|
+
resolvedEnvironment = await getDefaultEnvironment();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!resolvedEnvironment) {
|
|
151
|
+
console.error('Error: No environment specified and no default environment configured');
|
|
152
|
+
console.error('Use: coder test <test-name> --env=<environment>');
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle --list
|
|
157
|
+
if (listTests) {
|
|
158
|
+
await listAvailableTests(resolvedEnvironment);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Load test definitions
|
|
163
|
+
const tests = await loadTestDefinitions(resolvedEnvironment);
|
|
164
|
+
|
|
165
|
+
// Determine command to run
|
|
166
|
+
let command;
|
|
167
|
+
let description;
|
|
168
|
+
let reposFilter; // Optional array of repo names to include
|
|
169
|
+
|
|
170
|
+
if (customCommand) {
|
|
171
|
+
// Custom command
|
|
172
|
+
command = customCommand;
|
|
173
|
+
description = 'Custom test command';
|
|
174
|
+
} else if (testName) {
|
|
175
|
+
// Named test
|
|
176
|
+
const test = tests[testName];
|
|
177
|
+
if (!test) {
|
|
178
|
+
console.error(`Error: Test "${testName}" not found in environment "${resolvedEnvironment}"`);
|
|
179
|
+
console.error(`\nAvailable tests: ${Object.keys(tests).join(', ') || 'none'}`);
|
|
180
|
+
console.error(`\nOr use: coder test --cmd="your command"`);
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Support both 'command' (string) and 'commands' (array)
|
|
185
|
+
if (test.commands && Array.isArray(test.commands)) {
|
|
186
|
+
command = test.commands.join(' && ');
|
|
187
|
+
} else {
|
|
188
|
+
command = test.command;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
description = test.description || testName;
|
|
192
|
+
reposFilter = test.repos; // undefined, array, or empty array
|
|
193
|
+
} else {
|
|
194
|
+
console.error('Error: No test name or --cmd specified');
|
|
195
|
+
console.error('Usage: coder test <test-name> [options]');
|
|
196
|
+
console.error(' or: coder test --cmd="command" [options]');
|
|
197
|
+
console.error(' or: coder test --list');
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Show what we're running
|
|
202
|
+
console.log(`Running test in environment: ${resolvedEnvironment}`);
|
|
203
|
+
if (testName) {
|
|
204
|
+
console.log(`Test: ${testName}`);
|
|
205
|
+
}
|
|
206
|
+
if (description) {
|
|
207
|
+
console.log(`Description: ${description}`);
|
|
208
|
+
}
|
|
209
|
+
console.log(`Command: ${command}`);
|
|
210
|
+
|
|
211
|
+
// Prepare request body
|
|
212
|
+
const requestBody = {
|
|
213
|
+
environment: resolvedEnvironment,
|
|
214
|
+
test_command: command
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
// Apply local state by default (unless --no-local-state)
|
|
218
|
+
// Also check if test specifies repos: [] (empty array = skip local state)
|
|
219
|
+
const shouldCaptureState = !noLocalState && !(Array.isArray(reposFilter) && reposFilter.length === 0);
|
|
220
|
+
|
|
221
|
+
if (shouldCaptureState) {
|
|
222
|
+
console.log('\n🔍 Capturing local repository state...');
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
// Fetch environment config to get repo configurations
|
|
226
|
+
const envConfig = await request(`/environments/${resolvedEnvironment}`);
|
|
227
|
+
let repoConfigs = envConfig.repos || [];
|
|
228
|
+
|
|
229
|
+
// Filter repos if test specifies which ones to include
|
|
230
|
+
if (Array.isArray(reposFilter) && reposFilter.length > 0) {
|
|
231
|
+
const originalCount = repoConfigs.length;
|
|
232
|
+
repoConfigs = repoConfigs.filter(r => reposFilter.includes(r.name));
|
|
233
|
+
console.log(` Filtering to ${repoConfigs.length}/${originalCount} repositories: ${reposFilter.join(', ')}`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (repoConfigs.length === 0) {
|
|
237
|
+
console.warn('Warning: No repositories configured for this environment');
|
|
238
|
+
} else {
|
|
239
|
+
// Capture state locally on the client side
|
|
240
|
+
const localState = await captureAllRepos(process.cwd(), repoConfigs);
|
|
241
|
+
|
|
242
|
+
// Validate the captured state
|
|
243
|
+
const validation = validateState(localState);
|
|
244
|
+
if (!validation.valid) {
|
|
245
|
+
console.error('Error: Invalid local state captured');
|
|
246
|
+
validation.errors.forEach(err => console.error(` - ${err}`));
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (validation.warnings && validation.warnings.length > 0) {
|
|
251
|
+
validation.warnings.forEach(warn => console.warn(` Warning: ${warn}`));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Send the captured state JSON to the server
|
|
255
|
+
requestBody.local_state = localState;
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error(`Error capturing local state: ${error.message}`);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
} else if (Array.isArray(reposFilter) && reposFilter.length === 0) {
|
|
262
|
+
console.log('\n⏭️ Skipping local state (test configured with empty repos array)');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Create test container
|
|
266
|
+
const data = await request('/test', {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
body: JSON.stringify(requestBody)
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
console.log(`\n✓ Test container created`);
|
|
272
|
+
console.log(` Container ID: ${data.containerId}`);
|
|
273
|
+
|
|
274
|
+
// Display local state info if captured
|
|
275
|
+
if (data.localState && data.localState.repos_found && data.localState.repos_found.length > 0) {
|
|
276
|
+
console.log('\n📦 Local State Applied:');
|
|
277
|
+
console.log(` Repositories: ${data.localState.repos_found.join(', ')}`);
|
|
278
|
+
if (data.localState.repos_missing && data.localState.repos_missing.length > 0) {
|
|
279
|
+
console.log(` Missing: ${data.localState.repos_missing.join(', ')} (using defaults)`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
console.log('\n\n' + '='.repeat(60));
|
|
284
|
+
console.log('Test Output:');
|
|
285
|
+
console.log('='.repeat(60) + '\n');
|
|
286
|
+
|
|
287
|
+
// Stream logs from container
|
|
288
|
+
const serverUrl = await getServerUrl();
|
|
289
|
+
const apiKey = await getApiKey();
|
|
290
|
+
|
|
291
|
+
// Use fetch to stream logs
|
|
292
|
+
const logsUrl = `${serverUrl}/test/${data.containerId}/logs`;
|
|
293
|
+
const headers = {
|
|
294
|
+
'Authorization': `Bearer ${apiKey}`
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
const response = await fetch(logsUrl, { headers });
|
|
299
|
+
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Stream response body
|
|
305
|
+
const reader = response.body.getReader();
|
|
306
|
+
const decoder = new TextDecoder();
|
|
307
|
+
|
|
308
|
+
while (true) {
|
|
309
|
+
const { done, value } = await reader.read();
|
|
310
|
+
if (done) break;
|
|
311
|
+
|
|
312
|
+
const text = decoder.decode(value, { stream: true });
|
|
313
|
+
process.stdout.write(text);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
console.log('='.repeat(60));
|
|
317
|
+
|
|
318
|
+
// Check final status and exit with proper code
|
|
319
|
+
const statusCheck = await request(`/test/${data.containerId}/status`).catch(() => null);
|
|
320
|
+
|
|
321
|
+
if (statusCheck) {
|
|
322
|
+
const exitCode = statusCheck.exitCode || 0;
|
|
323
|
+
console.log(`Exit code: ${exitCode}`);
|
|
324
|
+
process.exit(exitCode);
|
|
325
|
+
} else {
|
|
326
|
+
// Container already removed, assume success
|
|
327
|
+
console.log('Exit code: 0');
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
} catch (error) {
|
|
332
|
+
console.error(`\n✗ Error streaming test output: ${error.message}`);
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
}
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import {
|
|
5
|
+
getEffectiveConfig,
|
|
6
|
+
getActiveProfileName,
|
|
7
|
+
loadProfile,
|
|
8
|
+
getMainConfigPath,
|
|
9
|
+
loadMainConfig,
|
|
10
|
+
saveMainConfig
|
|
11
|
+
} from './profile.js';
|
|
12
|
+
|
|
13
|
+
let cachedConfig = null;
|
|
14
|
+
let configLoaded = false;
|
|
15
|
+
|
|
16
|
+
// CLI profile override (set via --profile flag)
|
|
17
|
+
let cliProfileOverride = null;
|
|
18
|
+
|
|
19
|
+
function getDefaultConfigPath() {
|
|
20
|
+
return path.join(os.homedir(), '.coder', 'config.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getConfigPath() {
|
|
24
|
+
return process.env.CODER_CONFIG_PATH || getDefaultConfigPath();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Set the CLI profile override (from --profile flag)
|
|
29
|
+
* This takes precedence over the activeProfile in config.json
|
|
30
|
+
*/
|
|
31
|
+
export function setCliProfileOverride(profileName) {
|
|
32
|
+
cliProfileOverride = profileName;
|
|
33
|
+
invalidateConfigCache();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the current CLI profile override
|
|
38
|
+
*/
|
|
39
|
+
export function getCliProfileOverride() {
|
|
40
|
+
return cliProfileOverride;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Invalidate the config cache (used after external writes)
|
|
45
|
+
*/
|
|
46
|
+
export function invalidateConfigCache() {
|
|
47
|
+
cachedConfig = null;
|
|
48
|
+
configLoaded = false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sanitizeServerUrl(url) {
|
|
52
|
+
if (!url) {
|
|
53
|
+
return url;
|
|
54
|
+
}
|
|
55
|
+
return url.endsWith('/') ? url.slice(0, -1) : url;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load client config with profile support
|
|
60
|
+
* Returns the merged configuration from profile + legacy config
|
|
61
|
+
*/
|
|
62
|
+
export async function loadClientConfig() {
|
|
63
|
+
if (configLoaded) {
|
|
64
|
+
return cachedConfig;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Use the new profile-aware config loading
|
|
69
|
+
cachedConfig = await getEffectiveConfig(cliProfileOverride);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
// Fall back to legacy behavior if profile system fails
|
|
72
|
+
const configPath = getConfigPath();
|
|
73
|
+
try {
|
|
74
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
75
|
+
cachedConfig = JSON.parse(content);
|
|
76
|
+
} catch (readError) {
|
|
77
|
+
if (readError.code === 'ENOENT') {
|
|
78
|
+
cachedConfig = null;
|
|
79
|
+
} else {
|
|
80
|
+
throw new Error(`Failed to read client config at ${configPath}: ${readError.message}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
configLoaded = true;
|
|
86
|
+
return cachedConfig;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load raw client config (legacy config.json only, no profile merging)
|
|
91
|
+
* Used for config management commands
|
|
92
|
+
*/
|
|
93
|
+
export async function loadRawClientConfig() {
|
|
94
|
+
const configPath = getConfigPath();
|
|
95
|
+
try {
|
|
96
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
97
|
+
return JSON.parse(content);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error.code === 'ENOENT') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Failed to read client config at ${configPath}: ${error.message}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getServerUrl() {
|
|
107
|
+
if (process.env.CODER_SERVER_URL) {
|
|
108
|
+
return sanitizeServerUrl(process.env.CODER_SERVER_URL);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const config = await loadClientConfig();
|
|
112
|
+
if (config?.server) {
|
|
113
|
+
return sanitizeServerUrl(config.server);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return 'http://localhost:3000';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function getDefaultEnvironment() {
|
|
120
|
+
// Check client config first
|
|
121
|
+
const config = await loadClientConfig();
|
|
122
|
+
if (config?.default_environment) {
|
|
123
|
+
return config.default_environment;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fall back to fetching from server
|
|
127
|
+
try {
|
|
128
|
+
const { request } = await import('./http-client.js');
|
|
129
|
+
const environmentsData = await request('/environments');
|
|
130
|
+
return environmentsData?.default_environment || null;
|
|
131
|
+
} catch (error) {
|
|
132
|
+
// If server call fails, fall back to local setup.json if available
|
|
133
|
+
const setupPath = await getCoderSetupPath();
|
|
134
|
+
if (setupPath) {
|
|
135
|
+
try {
|
|
136
|
+
const setupJsonPath = path.join(setupPath, 'setup.json');
|
|
137
|
+
const setupContent = await fs.readFile(setupJsonPath, 'utf-8');
|
|
138
|
+
const setupJson = JSON.parse(setupContent);
|
|
139
|
+
return setupJson?.default_environment || null;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getConfigPathForDisplay() {
|
|
149
|
+
return getConfigPath();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get the path where credentials will be saved (profile or legacy config)
|
|
154
|
+
*/
|
|
155
|
+
export async function getCredentialsPathForDisplay() {
|
|
156
|
+
const activeProfileName = cliProfileOverride || await getActiveProfileName();
|
|
157
|
+
if (activeProfileName) {
|
|
158
|
+
const { getProfilePath } = await import('./profile.js');
|
|
159
|
+
return getProfilePath(activeProfileName);
|
|
160
|
+
}
|
|
161
|
+
return getConfigPath();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Save API key to config file or active profile
|
|
166
|
+
*/
|
|
167
|
+
export async function saveApiKey(apiKey) {
|
|
168
|
+
// Check if there's an active profile (including CLI override)
|
|
169
|
+
const activeProfileName = cliProfileOverride || await getActiveProfileName();
|
|
170
|
+
|
|
171
|
+
if (activeProfileName) {
|
|
172
|
+
// Save to active profile
|
|
173
|
+
const { loadProfile, saveProfile } = await import('./profile.js');
|
|
174
|
+
const profile = await loadProfile(activeProfileName) || { name: activeProfileName };
|
|
175
|
+
profile.apiKey = apiKey;
|
|
176
|
+
await saveProfile(activeProfileName, profile);
|
|
177
|
+
|
|
178
|
+
// Invalidate cache so next read gets fresh data
|
|
179
|
+
invalidateConfigCache();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Fall back to legacy config
|
|
184
|
+
const configPath = getConfigPath();
|
|
185
|
+
const configDir = path.dirname(configPath);
|
|
186
|
+
|
|
187
|
+
// Ensure .coder directory exists
|
|
188
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
189
|
+
|
|
190
|
+
// Load existing config or create new one
|
|
191
|
+
let config = await loadRawClientConfig() || {};
|
|
192
|
+
|
|
193
|
+
// Update API key
|
|
194
|
+
config.apiKey = apiKey;
|
|
195
|
+
|
|
196
|
+
// Write config file
|
|
197
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
198
|
+
|
|
199
|
+
// Update cache
|
|
200
|
+
invalidateConfigCache();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get API key from config or environment variable
|
|
205
|
+
*/
|
|
206
|
+
export async function getApiKey() {
|
|
207
|
+
// Check environment variable first
|
|
208
|
+
if (process.env.CODER_API_KEY) {
|
|
209
|
+
return process.env.CODER_API_KEY;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check config file
|
|
213
|
+
const config = await loadClientConfig();
|
|
214
|
+
return config?.apiKey || null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clear API key from config file (logout)
|
|
219
|
+
*/
|
|
220
|
+
export async function clearApiKey() {
|
|
221
|
+
const configPath = getConfigPath();
|
|
222
|
+
|
|
223
|
+
let config = await loadRawClientConfig();
|
|
224
|
+
if (!config) {
|
|
225
|
+
return; // No config file, nothing to clear
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Remove API key
|
|
229
|
+
delete config.apiKey;
|
|
230
|
+
|
|
231
|
+
// Write updated config
|
|
232
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
233
|
+
|
|
234
|
+
// Invalidate cache
|
|
235
|
+
invalidateConfigCache();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Save coder setup path to config file
|
|
240
|
+
*/
|
|
241
|
+
export async function saveCoderSetupPath(setupPath) {
|
|
242
|
+
const configPath = getConfigPath();
|
|
243
|
+
const configDir = path.dirname(configPath);
|
|
244
|
+
|
|
245
|
+
// Ensure .coder directory exists
|
|
246
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
247
|
+
|
|
248
|
+
// Load existing config or create new one (use raw to preserve activeProfile)
|
|
249
|
+
let config = await loadRawClientConfig() || {};
|
|
250
|
+
|
|
251
|
+
// Update coder setup path
|
|
252
|
+
config.coder_setup_path = setupPath;
|
|
253
|
+
|
|
254
|
+
// Write config file
|
|
255
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
256
|
+
|
|
257
|
+
// Invalidate cache
|
|
258
|
+
invalidateConfigCache();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get coder setup path from config or environment variable
|
|
263
|
+
*/
|
|
264
|
+
export async function getCoderSetupPath() {
|
|
265
|
+
// Check environment variable first
|
|
266
|
+
if (process.env.CODER_SETUP_PATH) {
|
|
267
|
+
return process.env.CODER_SETUP_PATH;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Check config file
|
|
271
|
+
const config = await loadClientConfig();
|
|
272
|
+
return config?.coder_setup_path || null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Save server port to config file
|
|
277
|
+
*/
|
|
278
|
+
export async function saveServerPort(port) {
|
|
279
|
+
const configPath = getConfigPath();
|
|
280
|
+
const configDir = path.dirname(configPath);
|
|
281
|
+
|
|
282
|
+
// Ensure .coder directory exists
|
|
283
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
284
|
+
|
|
285
|
+
// Load existing config or create new one (use raw to preserve activeProfile)
|
|
286
|
+
let config = await loadRawClientConfig() || {};
|
|
287
|
+
|
|
288
|
+
// Update server port
|
|
289
|
+
config.server_port = port;
|
|
290
|
+
|
|
291
|
+
// Write config file
|
|
292
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
293
|
+
|
|
294
|
+
// Invalidate cache
|
|
295
|
+
invalidateConfigCache();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Get server port from config or environment variable
|
|
300
|
+
*/
|
|
301
|
+
export async function getServerPort() {
|
|
302
|
+
// Check environment variable first
|
|
303
|
+
if (process.env.PORT) {
|
|
304
|
+
return parseInt(process.env.PORT, 10);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check config file
|
|
308
|
+
const config = await loadClientConfig();
|
|
309
|
+
return config?.server_port || 3000;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Save profound coder path to config file
|
|
314
|
+
*/
|
|
315
|
+
export async function saveProfoundCoderPath(profoundCoderPath) {
|
|
316
|
+
const configPath = getConfigPath();
|
|
317
|
+
const configDir = path.dirname(configPath);
|
|
318
|
+
|
|
319
|
+
// Ensure .coder directory exists
|
|
320
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
321
|
+
|
|
322
|
+
// Load existing config or create new one (use raw to preserve activeProfile)
|
|
323
|
+
let config = await loadRawClientConfig() || {};
|
|
324
|
+
|
|
325
|
+
// Update profound coder path
|
|
326
|
+
config.profound_coder_path = profoundCoderPath;
|
|
327
|
+
|
|
328
|
+
// Write config file
|
|
329
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
330
|
+
|
|
331
|
+
// Invalidate cache
|
|
332
|
+
invalidateConfigCache();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Get profound coder path from config or environment variable
|
|
337
|
+
*/
|
|
338
|
+
export async function getProfoundCoderPath() {
|
|
339
|
+
// Check environment variable first
|
|
340
|
+
if (process.env.PROFOUND_CODER_PATH) {
|
|
341
|
+
return process.env.PROFOUND_CODER_PATH;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Check config file
|
|
345
|
+
const config = await loadClientConfig();
|
|
346
|
+
return config?.profound_coder_path || null;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Save last container ID to config file
|
|
351
|
+
*/
|
|
352
|
+
export async function saveLastContainerId(containerId) {
|
|
353
|
+
const configPath = getConfigPath();
|
|
354
|
+
const configDir = path.dirname(configPath);
|
|
355
|
+
|
|
356
|
+
// Ensure .coder directory exists
|
|
357
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
358
|
+
|
|
359
|
+
// Load existing config or create new one (use raw to preserve activeProfile)
|
|
360
|
+
let config = await loadRawClientConfig() || {};
|
|
361
|
+
|
|
362
|
+
// Update last container ID
|
|
363
|
+
config.last_container_id = containerId;
|
|
364
|
+
|
|
365
|
+
// Write config file
|
|
366
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
367
|
+
|
|
368
|
+
// Invalidate cache
|
|
369
|
+
invalidateConfigCache();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get last container ID from config
|
|
374
|
+
*/
|
|
375
|
+
export async function getLastContainerId() {
|
|
376
|
+
const config = await loadClientConfig();
|
|
377
|
+
return config?.last_container_id || null;
|
|
378
|
+
}
|