@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder apply - Apply patches from completed task to local repos
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request } from '../http-client.js';
|
|
6
|
+
import { select } from '@inquirer/prompts';
|
|
7
|
+
import { groupRelatedTasks, formatGroupForDisplay, formatTaskForDisplay } from '../task-grouping.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch recent completed tasks from the server
|
|
11
|
+
*/
|
|
12
|
+
async function fetchRecentTasks() {
|
|
13
|
+
try {
|
|
14
|
+
// Fetch most recent completed tasks (only completed tasks can be applied)
|
|
15
|
+
// Include summary to show more details about each task
|
|
16
|
+
const data = await request('/tasks?status=completed&limit=50&include=summary');
|
|
17
|
+
|
|
18
|
+
// Filter out test command tasks (they're for testing, not for applying)
|
|
19
|
+
const tasks = (data.tasks || []).filter(task => task.taskType !== 'test');
|
|
20
|
+
|
|
21
|
+
return tasks;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Failed to fetch tasks:', error.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prompt user to select a task from a list of grouped tasks
|
|
30
|
+
*/
|
|
31
|
+
async function selectTaskInteractively() {
|
|
32
|
+
console.log('Fetching your most recent completed tasks...\n');
|
|
33
|
+
|
|
34
|
+
const tasks = await fetchRecentTasks();
|
|
35
|
+
|
|
36
|
+
if (!tasks || tasks.length === 0) {
|
|
37
|
+
console.log('No completed tasks found.');
|
|
38
|
+
console.log('\nPossible reasons:');
|
|
39
|
+
console.log(' - No tasks have been run yet');
|
|
40
|
+
console.log(' - All tasks are still running or have failed');
|
|
41
|
+
console.log(' - Server was restarted (task history is not persistent)');
|
|
42
|
+
console.log('\nRun "coder list" to see all tasks.');
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Group tasks by variants (same as web UI)
|
|
47
|
+
const groups = groupRelatedTasks(tasks);
|
|
48
|
+
|
|
49
|
+
// Create choices for the prompt
|
|
50
|
+
const choices = groups.map(group => ({
|
|
51
|
+
name: formatGroupForDisplay(group),
|
|
52
|
+
value: group
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
// Prompt user to select a task group
|
|
56
|
+
let selectedGroup;
|
|
57
|
+
try {
|
|
58
|
+
selectedGroup = await select({
|
|
59
|
+
message: 'Select a task to apply (Ctrl+C to cancel):',
|
|
60
|
+
choices,
|
|
61
|
+
pageSize: 15,
|
|
62
|
+
loop: false // Disable wrapping to make start/end of list clear
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
// User cancelled (Ctrl+C) or other error
|
|
66
|
+
if (error.name === 'ExitPromptError' || error.message.includes('User force closed')) {
|
|
67
|
+
console.log('\nCancelled.');
|
|
68
|
+
process.exit(0);
|
|
69
|
+
}
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// If the group has multiple variants, prompt for variant selection
|
|
74
|
+
if (selectedGroup.isGroup && selectedGroup.variants.length > 1) {
|
|
75
|
+
console.log(''); // Empty line for spacing
|
|
76
|
+
|
|
77
|
+
const variantChoices = selectedGroup.variants.map(variant => ({
|
|
78
|
+
name: formatTaskForDisplay(variant, true),
|
|
79
|
+
value: variant
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
let selectedVariant;
|
|
83
|
+
try {
|
|
84
|
+
selectedVariant = await select({
|
|
85
|
+
message: 'Select a variant (Ctrl+C to cancel):',
|
|
86
|
+
choices: variantChoices,
|
|
87
|
+
pageSize: 10,
|
|
88
|
+
loop: false // Disable wrapping to make start/end of list clear
|
|
89
|
+
});
|
|
90
|
+
} catch (error) {
|
|
91
|
+
// User cancelled (Ctrl+C) or other error
|
|
92
|
+
if (error.name === 'ExitPromptError' || error.message.includes('User force closed')) {
|
|
93
|
+
console.log('\nCancelled.');
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return selectedVariant.taskId;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return selectedGroup.primary.taskId;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function applyTask(taskIdArg, filePathArg) {
|
|
106
|
+
let taskId = taskIdArg;
|
|
107
|
+
|
|
108
|
+
// If no task ID provided, show interactive selection
|
|
109
|
+
if (!taskId) {
|
|
110
|
+
taskId = await selectTaskInteractively();
|
|
111
|
+
console.log(''); // Empty line for spacing
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Fetch task details to show context
|
|
115
|
+
let taskDetails;
|
|
116
|
+
try {
|
|
117
|
+
taskDetails = await request(`/tasks/${taskId}?include=summary,instructions`);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`Failed to fetch task details: ${error.message}`);
|
|
120
|
+
// Continue anyway - we can still try to apply patches
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Show task information
|
|
124
|
+
if (taskDetails) {
|
|
125
|
+
console.log('Task Details:');
|
|
126
|
+
console.log('─'.repeat(60));
|
|
127
|
+
|
|
128
|
+
// Show template if used
|
|
129
|
+
if (taskDetails.taskType && taskDetails.taskType !== 'manual') {
|
|
130
|
+
console.log(`Template: ${taskDetails.taskType}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Show template parameters if any (excluding instructions which we show separately)
|
|
134
|
+
if (taskDetails.parameters && Object.keys(taskDetails.parameters).length > 0) {
|
|
135
|
+
const params = { ...taskDetails.parameters };
|
|
136
|
+
delete params.instructions; // We'll show this separately
|
|
137
|
+
|
|
138
|
+
if (Object.keys(params).length > 0) {
|
|
139
|
+
console.log('\nParameters:');
|
|
140
|
+
for (const [key, value] of Object.entries(params)) {
|
|
141
|
+
// Format the value nicely
|
|
142
|
+
let displayValue = value;
|
|
143
|
+
if (typeof value === 'string' && value.length > 60) {
|
|
144
|
+
displayValue = value.substring(0, 57) + '...';
|
|
145
|
+
} else if (typeof value === 'object') {
|
|
146
|
+
displayValue = JSON.stringify(value);
|
|
147
|
+
}
|
|
148
|
+
console.log(` ${key}: ${displayValue}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Show instructions
|
|
154
|
+
const instructions = taskDetails.instructions || taskDetails.parameters?.instructions;
|
|
155
|
+
if (instructions) {
|
|
156
|
+
console.log('\nInstructions:');
|
|
157
|
+
// Show first 5 lines or all if less than 5
|
|
158
|
+
const lines = instructions.split('\n').slice(0, 5);
|
|
159
|
+
lines.forEach(line => console.log(` ${line}`));
|
|
160
|
+
const totalLines = instructions.split('\n').length;
|
|
161
|
+
if (totalLines > 5) {
|
|
162
|
+
console.log(` ... (${totalLines - 5} more lines)`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('─'.repeat(60));
|
|
167
|
+
console.log('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(`Applying patches...\n`);
|
|
171
|
+
|
|
172
|
+
// Get patches from server
|
|
173
|
+
const data = await request(`/tasks/${taskId}/patches`);
|
|
174
|
+
|
|
175
|
+
if (!data.patches || data.patches.length === 0) {
|
|
176
|
+
console.log('No patches found for this task.');
|
|
177
|
+
console.log('Task may not have made any changes.');
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log(`Found ${data.patches.length} repository${data.patches.length > 1 ? 'ies' : ''} with changes:\n`);
|
|
182
|
+
|
|
183
|
+
// Apply each patch
|
|
184
|
+
for (const patch of data.patches) {
|
|
185
|
+
console.log(`📦 ${patch.repo_name}`);
|
|
186
|
+
console.log(` Files changed: ${patch.files_changed}`);
|
|
187
|
+
|
|
188
|
+
// Use local_path from server (includes environment config mapping)
|
|
189
|
+
// Falls back to repo_name if local_path not provided (backwards compatibility)
|
|
190
|
+
const localPath = patch.local_path || patch.repo_name;
|
|
191
|
+
|
|
192
|
+
// Check if repo exists in current directory
|
|
193
|
+
const { existsSync } = await import('fs');
|
|
194
|
+
const repoPath = `./${localPath}`;
|
|
195
|
+
|
|
196
|
+
if (!existsSync(repoPath)) {
|
|
197
|
+
console.log(` ✗ Repository not found: ${repoPath}`);
|
|
198
|
+
console.log(` Expected path: ${repoPath}`);
|
|
199
|
+
console.log(` Skipping ${patch.repo_name}\n`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Write patch to temporary file
|
|
204
|
+
const { writeFileSync, unlinkSync } = await import('fs');
|
|
205
|
+
const tmpPatch = `/tmp/${taskId}-${patch.patch_file}`;
|
|
206
|
+
writeFileSync(tmpPatch, patch.patch_content, { encoding: 'utf-8' });
|
|
207
|
+
|
|
208
|
+
// Apply patch
|
|
209
|
+
const { execSync } = await import('child_process');
|
|
210
|
+
try {
|
|
211
|
+
let applyCommand = `git -C ${repoPath} apply --ignore-whitespace --whitespace=nowarn`;
|
|
212
|
+
|
|
213
|
+
if (filePathArg) {
|
|
214
|
+
// Use --include to only apply changes for specific file
|
|
215
|
+
// Note: git apply expects the path as it appears in the patch (relative to repo root)
|
|
216
|
+
applyCommand += ` --include=${filePathArg}`;
|
|
217
|
+
console.log(` (Applying only changes for: ${filePathArg})`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
applyCommand += ` ${tmpPatch}`;
|
|
221
|
+
|
|
222
|
+
execSync(applyCommand, { stdio: 'pipe' });
|
|
223
|
+
console.log(` ✓ Patch applied successfully\n`);
|
|
224
|
+
|
|
225
|
+
// Clean up temp file
|
|
226
|
+
unlinkSync(tmpPatch);
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.log(` ✗ Failed to apply patch: ${error.message}`);
|
|
229
|
+
console.log(` You may need to apply manually\n`);
|
|
230
|
+
unlinkSync(tmpPatch);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
console.log('Next steps:');
|
|
235
|
+
console.log(' 1. Review changes: git status');
|
|
236
|
+
console.log(' 2. Stage changes: git add .');
|
|
237
|
+
console.log(' 3. Commit changes: git commit -m "Your commit message"');
|
|
238
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command: coder attach [container-id] - Attach to a running container
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { request } from '../http-client.js';
|
|
6
|
+
import { getLastContainerId, saveLastContainerId } from '../config.js';
|
|
7
|
+
import { connectTerminal } from '../terminal-client.js';
|
|
8
|
+
|
|
9
|
+
export async function attachToContainer(args = []) {
|
|
10
|
+
let containerId = null;
|
|
11
|
+
let startShell = false;
|
|
12
|
+
let agentOverride = null;
|
|
13
|
+
|
|
14
|
+
// Parse arguments - process flags first, then find container ID
|
|
15
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
16
|
+
const arg = args[index];
|
|
17
|
+
if (arg === '--shell') {
|
|
18
|
+
startShell = true;
|
|
19
|
+
} else if (arg === '--agent') {
|
|
20
|
+
const next = args[index + 1];
|
|
21
|
+
if (!next) {
|
|
22
|
+
console.error('Error: --agent requires a value (claude or codex)');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
agentOverride = next;
|
|
26
|
+
index += 1;
|
|
27
|
+
} else if (arg.startsWith('--agent=')) {
|
|
28
|
+
agentOverride = arg.substring('--agent='.length);
|
|
29
|
+
} else if (!containerId && !arg.startsWith('--')) {
|
|
30
|
+
// This is the container ID
|
|
31
|
+
containerId = arg;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If no container ID provided, try to use the last one
|
|
36
|
+
if (!containerId) {
|
|
37
|
+
containerId = await getLastContainerId();
|
|
38
|
+
if (!containerId) {
|
|
39
|
+
console.error('Error: No container ID specified and no previous container found.');
|
|
40
|
+
console.error('\nUsage:');
|
|
41
|
+
console.error(' coder attach <container-id> # Attach to specific container');
|
|
42
|
+
console.error(' coder attach # Attach to last container');
|
|
43
|
+
console.error('\nTo see available containers, run:');
|
|
44
|
+
console.error(' coder containers');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
console.log(`Attaching to last container: ${containerId}`);
|
|
48
|
+
} else {
|
|
49
|
+
console.log(`Attaching to container: ${containerId}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Fetch container info from server to verify it exists and get details
|
|
53
|
+
let containerInfo;
|
|
54
|
+
try {
|
|
55
|
+
containerInfo = await request(`/containers/${containerId}`);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`\n✗ Failed to find container: ${error.message}`);
|
|
58
|
+
console.error('\nTo see available containers, run:');
|
|
59
|
+
console.error(' coder containers');
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Display container info
|
|
64
|
+
console.log(`\n✓ Container found`);
|
|
65
|
+
console.log(` Container ID: ${containerInfo.containerId}`);
|
|
66
|
+
console.log(` Environment: ${containerInfo.environment}`);
|
|
67
|
+
console.log(` Image: ${containerInfo.image || 'N/A'}`);
|
|
68
|
+
console.log(` Default Agent: ${containerInfo.defaultAgent}`);
|
|
69
|
+
console.log(` Status: ${containerInfo.status}`);
|
|
70
|
+
if (containerInfo.isTaskContainer) {
|
|
71
|
+
console.log(` Type: Task container (${containerInfo.taskId})`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (containerInfo.status !== 'running') {
|
|
75
|
+
console.error(`\n✗ Container is not running (status: ${containerInfo.status})`);
|
|
76
|
+
console.error('You can only attach to running containers.');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Determine command to run
|
|
81
|
+
let cmd;
|
|
82
|
+
let descriptiveAction;
|
|
83
|
+
|
|
84
|
+
if (startShell) {
|
|
85
|
+
cmd = null; // Just start a shell
|
|
86
|
+
descriptiveAction = 'Connecting to shell';
|
|
87
|
+
} else if (agentOverride) {
|
|
88
|
+
// Override the default agent with resume
|
|
89
|
+
if (agentOverride === 'claude') {
|
|
90
|
+
cmd = 'cd /workspace && claude --resume';
|
|
91
|
+
descriptiveAction = 'Resuming with Claude';
|
|
92
|
+
} else if (agentOverride === 'codex') {
|
|
93
|
+
cmd = 'cd /workspace && codex resume';
|
|
94
|
+
descriptiveAction = 'Resuming with Codex';
|
|
95
|
+
} else {
|
|
96
|
+
console.error(`Error: Unknown agent: ${agentOverride}`);
|
|
97
|
+
console.error('Valid agents: claude, codex');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Use the default agent from container with resume
|
|
102
|
+
const agent = containerInfo.defaultAgent || 'claude';
|
|
103
|
+
if (agent === 'claude') {
|
|
104
|
+
cmd = 'cd /workspace && claude --resume';
|
|
105
|
+
descriptiveAction = 'Resuming with Claude';
|
|
106
|
+
} else if (agent === 'codex') {
|
|
107
|
+
cmd = 'cd /workspace && codex resume';
|
|
108
|
+
descriptiveAction = 'Resuming with Codex';
|
|
109
|
+
} else {
|
|
110
|
+
// Fallback for unknown agents
|
|
111
|
+
cmd = `cd /workspace && ${agent}`;
|
|
112
|
+
descriptiveAction = `Starting ${agent}`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(`\n${descriptiveAction}...\n`);
|
|
117
|
+
|
|
118
|
+
// Save this as the last container
|
|
119
|
+
await saveLastContainerId(containerInfo.containerId);
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
const exitCode = await connectTerminal(containerInfo.containerId, cmd);
|
|
123
|
+
|
|
124
|
+
if (exitCode === 0) {
|
|
125
|
+
console.log(`\n✓ Disconnected from container ${containerInfo.containerId}`);
|
|
126
|
+
} else if (exitCode === 130) {
|
|
127
|
+
console.log(`\n✓ Terminal session interrupted`);
|
|
128
|
+
} else {
|
|
129
|
+
console.error(`\n✗ Terminal session exited with code ${exitCode}`);
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
console.error(`\n✗ Failed to connect to terminal: ${error.message}`);
|
|
133
|
+
console.log('\nContainer may still be running.');
|
|
134
|
+
console.log('\nTo see container status, run:');
|
|
135
|
+
console.log(` coder containers`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('\nContainer is still running and will auto-cleanup after 2 hours of inactivity.');
|
|
140
|
+
console.log(`Container ID: ${containerInfo.containerId}`);
|
|
141
|
+
console.log('\nTo reconnect:');
|
|
142
|
+
console.log(` coder attach`);
|
|
143
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config command - manage coder-setup configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { getConfigPathForDisplay, loadClientConfig, invalidateConfigCache, loadRawClientConfig } from '../config.js';
|
|
9
|
+
import { getActiveProfileName, loadProfile, getProfilePath } from '../profile.js';
|
|
10
|
+
|
|
11
|
+
// Client-side config keys (stored in ~/.coder/config.json)
|
|
12
|
+
// Note: Server-side keys (coder_setup_path, server_port, etc.) are managed by coder-server config
|
|
13
|
+
const CLIENT_CONFIG_KEYS = new Set([
|
|
14
|
+
'server',
|
|
15
|
+
'apiKey',
|
|
16
|
+
'last_container_id',
|
|
17
|
+
'default_environment'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Handle config command
|
|
22
|
+
* Usage:
|
|
23
|
+
* coder config set ssh_host sandbox.example.com
|
|
24
|
+
* coder config get ssh_host
|
|
25
|
+
* coder config show
|
|
26
|
+
*/
|
|
27
|
+
export async function handleConfig(args) {
|
|
28
|
+
const subcommand = args[0];
|
|
29
|
+
|
|
30
|
+
if (!subcommand) {
|
|
31
|
+
console.error('Usage: coder config <set|get|remove|show> [key] [value]');
|
|
32
|
+
console.error('');
|
|
33
|
+
console.error('Commands:');
|
|
34
|
+
console.error(' set <key> <value> Set a configuration value');
|
|
35
|
+
console.error(' get <key> Get a configuration value');
|
|
36
|
+
console.error(' remove <key> Remove a configuration value');
|
|
37
|
+
console.error(' show Show all configuration');
|
|
38
|
+
console.error('');
|
|
39
|
+
console.error('Configuration keys:');
|
|
40
|
+
console.error(' server Server URL (e.g., http://localhost:3000)');
|
|
41
|
+
console.error(' default_environment Default environment name');
|
|
42
|
+
console.error(' apiKey API authentication key');
|
|
43
|
+
console.error('');
|
|
44
|
+
console.error('Note: Server-side configuration is managed by coder-server config');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
switch (subcommand) {
|
|
50
|
+
case 'set':
|
|
51
|
+
await setConfig(args[1], args[2]);
|
|
52
|
+
break;
|
|
53
|
+
|
|
54
|
+
case 'get':
|
|
55
|
+
await getConfig(args[1]);
|
|
56
|
+
break;
|
|
57
|
+
|
|
58
|
+
case 'remove':
|
|
59
|
+
case 'unset':
|
|
60
|
+
await removeConfig(args[1]);
|
|
61
|
+
break;
|
|
62
|
+
|
|
63
|
+
case 'show':
|
|
64
|
+
await showConfig();
|
|
65
|
+
break;
|
|
66
|
+
|
|
67
|
+
default:
|
|
68
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
69
|
+
console.error('Run "coder config" for usage information');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`Error: ${error.message}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function setConfig(key, value) {
|
|
79
|
+
if (!key || value === undefined) {
|
|
80
|
+
console.error('Usage: coder config set <key> <value>');
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!CLIENT_CONFIG_KEYS.has(key)) {
|
|
85
|
+
console.error(`Unknown configuration key: ${key}`);
|
|
86
|
+
console.error('Run "coder config" for a list of available keys');
|
|
87
|
+
console.error('');
|
|
88
|
+
console.error('Note: Server-side keys are managed by coder-server config');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await setClientConfig(key, value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function setClientConfig(key, value) {
|
|
96
|
+
const configPath = getConfigPathForDisplay();
|
|
97
|
+
const configDir = path.dirname(configPath);
|
|
98
|
+
|
|
99
|
+
// Ensure .coder directory exists
|
|
100
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Load existing config or create new one
|
|
103
|
+
let config = await loadClientConfig() || {};
|
|
104
|
+
|
|
105
|
+
// Update the value
|
|
106
|
+
config[key] = value;
|
|
107
|
+
|
|
108
|
+
// Write back to file
|
|
109
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
110
|
+
|
|
111
|
+
// Invalidate cache so next read gets fresh data
|
|
112
|
+
invalidateConfigCache();
|
|
113
|
+
|
|
114
|
+
console.log(`✓ Set ${key} = ${value}`);
|
|
115
|
+
console.log(` Location: ${configPath}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function getConfig(key) {
|
|
119
|
+
if (!key) {
|
|
120
|
+
console.error('Usage: coder config get <key>');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!CLIENT_CONFIG_KEYS.has(key)) {
|
|
125
|
+
console.error(`Unknown configuration key: ${key}`);
|
|
126
|
+
console.error('Run "coder config" for a list of available keys');
|
|
127
|
+
console.error('');
|
|
128
|
+
console.error('Note: Server-side keys are managed by coder-server config');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const config = await loadClientConfig();
|
|
133
|
+
const value = config?.[key];
|
|
134
|
+
|
|
135
|
+
if (value !== undefined) {
|
|
136
|
+
console.log(value);
|
|
137
|
+
} else {
|
|
138
|
+
console.error(`Key "${key}" not found in configuration`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function removeConfig(key) {
|
|
144
|
+
if (!key) {
|
|
145
|
+
console.error('Usage: coder config remove <key>');
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (!CLIENT_CONFIG_KEYS.has(key)) {
|
|
150
|
+
console.error(`Unknown configuration key: ${key}`);
|
|
151
|
+
console.error('Run "coder config" for a list of available keys');
|
|
152
|
+
console.error('');
|
|
153
|
+
console.error('Note: Server-side keys are managed by coder-server config');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await removeClientConfig(key);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function removeClientConfig(key) {
|
|
161
|
+
const configPath = getConfigPathForDisplay();
|
|
162
|
+
|
|
163
|
+
// Load existing config
|
|
164
|
+
let config = await loadClientConfig();
|
|
165
|
+
if (!config || config[key] === undefined) {
|
|
166
|
+
console.error(`Key "${key}" not found in client configuration`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Remove the key
|
|
171
|
+
delete config[key];
|
|
172
|
+
|
|
173
|
+
// Write back to file
|
|
174
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
|
|
175
|
+
|
|
176
|
+
// Invalidate cache so next read gets fresh data
|
|
177
|
+
invalidateConfigCache();
|
|
178
|
+
|
|
179
|
+
console.log(`✓ Removed ${key}`);
|
|
180
|
+
console.log(` Location: ${configPath}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function showConfig() {
|
|
184
|
+
// Show active profile if set
|
|
185
|
+
const activeProfileName = await getActiveProfileName();
|
|
186
|
+
if (activeProfileName) {
|
|
187
|
+
console.log(`=== Active Profile: ${activeProfileName} ===`);
|
|
188
|
+
console.log('');
|
|
189
|
+
const profile = await loadProfile(activeProfileName);
|
|
190
|
+
if (profile) {
|
|
191
|
+
// Mask API key for display
|
|
192
|
+
const displayProfile = { ...profile };
|
|
193
|
+
if (displayProfile.apiKey) {
|
|
194
|
+
displayProfile.apiKey = displayProfile.apiKey.slice(0, 4) + '****' + displayProfile.apiKey.slice(-4);
|
|
195
|
+
}
|
|
196
|
+
console.log(JSON.stringify(displayProfile, null, 2));
|
|
197
|
+
}
|
|
198
|
+
console.log('');
|
|
199
|
+
console.log(`Location: ${getProfilePath(activeProfileName)}`);
|
|
200
|
+
console.log('');
|
|
201
|
+
console.log('Tip: Use "coder profile" commands to manage profiles');
|
|
202
|
+
console.log('');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
console.log('=== Client Configuration (Legacy) ===');
|
|
206
|
+
console.log('');
|
|
207
|
+
|
|
208
|
+
// Read raw client config (not merged with profile)
|
|
209
|
+
const clientConfigPath = getConfigPathForDisplay();
|
|
210
|
+
const clientConfig = await loadRawClientConfig();
|
|
211
|
+
|
|
212
|
+
if (clientConfig && Object.keys(clientConfig).length > 0) {
|
|
213
|
+
// Mask API key for display
|
|
214
|
+
const displayConfig = { ...clientConfig };
|
|
215
|
+
if (displayConfig.apiKey) {
|
|
216
|
+
displayConfig.apiKey = displayConfig.apiKey.slice(0, 4) + '****' + displayConfig.apiKey.slice(-4);
|
|
217
|
+
}
|
|
218
|
+
console.log(JSON.stringify(displayConfig, null, 2));
|
|
219
|
+
} else {
|
|
220
|
+
console.log('(empty)');
|
|
221
|
+
}
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(`Location: ${clientConfigPath}`);
|
|
224
|
+
console.log('');
|
|
225
|
+
console.log('Note: Server-side configuration is managed by coder-server config');
|
|
226
|
+
}
|