@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.
@@ -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
+ }